diff --git a/.sqlx/query-3b16e7b318ad7861bcda111fdb911efcebaf53a58f850d356e248ea666389a8f.json b/.sqlx/query-3b16e7b318ad7861bcda111fdb911efcebaf53a58f850d356e248ea666389a8f.json
new file mode 100644
index 000000000..775750ad3
--- /dev/null
+++ b/.sqlx/query-3b16e7b318ad7861bcda111fdb911efcebaf53a58f850d356e248ea666389a8f.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "INSERT INTO neo_blocks (block_number, block, created_at) VALUES ($1, $2, NOW())",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Numeric",
+ "Jsonb"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "3b16e7b318ad7861bcda111fdb911efcebaf53a58f850d356e248ea666389a8f"
+}
diff --git a/src/config.rs b/src/config.rs
index 67682d19e..70b850bff 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -23,6 +23,7 @@ use crate::eth::primitives::BlockNumber;
use crate::eth::primitives::BlockSelection;
#[cfg(feature = "dev")]
use crate::eth::primitives::StoragePointInTime;
+use crate::eth::storage::HybridPermanentStorage;
use crate::eth::storage::InMemoryPermanentStorage;
use crate::eth::storage::InMemoryTemporaryStorage;
use crate::eth::storage::PermanentStorage;
@@ -363,6 +364,9 @@ pub enum StorageKind {
#[strum(serialize = "postgres")]
Postgres { url: String },
+
+ #[strum(serialize = "hybrid")]
+ Hybrid { url: String },
}
impl PermanentStorageConfig {
@@ -379,6 +383,14 @@ impl PermanentStorageConfig {
};
Arc::new(Postgres::new(config).await?)
}
+ StorageKind::Hybrid { ref url } => {
+ let config = PostgresClientConfig {
+ url: url.to_owned(),
+ connections: self.perm_connections,
+ acquire_timeout: Duration::from_millis(self.perm_timeout_millis),
+ };
+ Arc::new(HybridPermanentStorage::new(config).await?)
+ }
};
Ok(Arc::new(StratusStorage::new(temp, perm)))
@@ -392,6 +404,10 @@ impl FromStr for StorageKind {
match s {
"inmemory" => Ok(Self::InMemory),
s if s.starts_with("postgres://") => Ok(Self::Postgres { url: s.to_string() }),
+ s if s.starts_with("hybrid://") => {
+ let s = s.replace("hybrid", "postgres"); //TODO there is a better way to do this
+ Ok(Self::Hybrid { url: s.to_string() })
+ }
s => Err(anyhow!("unknown storage: {}", s)),
}
}
diff --git a/src/eth/storage/hybrid/mod.rs b/src/eth/storage/hybrid/mod.rs
new file mode 100644
index 000000000..f1a0cd576
--- /dev/null
+++ b/src/eth/storage/hybrid/mod.rs
@@ -0,0 +1,532 @@
+//! In-memory storage implementations.
+
+use std::collections::HashMap;
+use std::sync::atomic::Ordering;
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use indexmap::IndexMap;
+use metrics::atomics::AtomicU64;
+use rand::rngs::StdRng;
+use rand::seq::IteratorRandom;
+use rand::SeedableRng;
+use serde_json::Value;
+use sqlx::postgres::PgPoolOptions;
+use tokio::sync::mpsc;
+use tokio::sync::mpsc::channel;
+use tokio::sync::RwLock;
+use tokio::sync::RwLockReadGuard;
+use tokio::sync::RwLockWriteGuard;
+
+use crate::eth::primitives::Account;
+use crate::eth::primitives::Address;
+use crate::eth::primitives::Block;
+use crate::eth::primitives::BlockNumber;
+use crate::eth::primitives::BlockSelection;
+use crate::eth::primitives::Bytes;
+use crate::eth::primitives::ExecutionAccountChanges;
+use crate::eth::primitives::ExecutionConflicts;
+use crate::eth::primitives::ExecutionConflictsBuilder;
+use crate::eth::primitives::Hash;
+use crate::eth::primitives::LogFilter;
+use crate::eth::primitives::LogMined;
+use crate::eth::primitives::Nonce;
+use crate::eth::primitives::Slot;
+use crate::eth::primitives::SlotIndex;
+use crate::eth::primitives::SlotSample;
+use crate::eth::primitives::StoragePointInTime;
+use crate::eth::primitives::TransactionMined;
+use crate::eth::primitives::Wei;
+use crate::eth::storage::inmemory::InMemoryHistory;
+use crate::eth::storage::PermanentStorage;
+use crate::eth::storage::StorageError;
+use crate::infra::postgres::PostgresClientConfig;
+
+#[derive(Debug)]
+struct BlockTask {
+ block_number: BlockNumber,
+ block_data: Value,
+}
+
+#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
+pub struct HybridPermanentStorageState {
+ pub accounts: HashMap
,
+ pub transactions: HashMap,
+ pub blocks_by_number: IndexMap>,
+ pub blocks_by_hash: IndexMap>,
+ pub logs: Vec,
+}
+
+#[derive(Debug)]
+pub struct HybridPermanentStorage {
+ state: RwLock,
+ block_number: AtomicU64,
+ task_sender: mpsc::Sender,
+}
+
+impl HybridPermanentStorage {
+ pub async fn new(config: PostgresClientConfig) -> anyhow::Result {
+ tracing::info!("starting hybrid storage");
+
+ let connection_pool = PgPoolOptions::new()
+ .min_connections(config.connections)
+ .max_connections(config.connections)
+ .acquire_timeout(config.acquire_timeout)
+ .connect(&config.url)
+ .await
+ .map_err(|e| {
+ tracing::error!(reason = ?e, "failed to start postgres client");
+ anyhow::anyhow!("failed to start postgres client")
+ })?;
+
+ let (task_sender, task_receiver) = channel::(32);
+ let pool = Arc::new(connection_pool.clone());
+ tokio::spawn(async move {
+ // Assuming you define a 'response_sender' if you plan to handle responses
+ let worker_pool = Arc::>::clone(&pool);
+ // Omitting response channel setup for simplicity
+ Self::worker(task_receiver, worker_pool).await;
+ });
+
+ Ok(Self {
+ state: RwLock::new(HybridPermanentStorageState::default()),
+ block_number: Default::default(),
+ task_sender,
+ })
+ }
+
+ async fn worker(mut receiver: tokio::sync::mpsc::Receiver, pool: Arc>) {
+ tracing::info!("Starting worker");
+ while let Some(block_task) = receiver.recv().await {
+ let pool_clone = Arc::>::clone(&pool);
+ // Here we attempt to insert the block data into the database.
+ // Adjust the SQL query according to your table schema.
+ tokio::spawn(async move {
+ let result = sqlx::query!(
+ "INSERT INTO neo_blocks (block_number, block, created_at) VALUES ($1, $2, NOW())",
+ block_task.block_number as _,
+ block_task.block_data
+ )
+ .execute(&*pool_clone)
+ .await;
+
+ // Handle the result of the insert operation.
+ match result {
+ Ok(_) => tracing::info!("Block {} inserted successfully.", block_task.block_number),
+ Err(e) => tracing::error!("Failed to insert block {}: {}", block_task.block_number, e),
+ }
+ });
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Lock methods
+ // -------------------------------------------------------------------------
+
+ /// Locks inner state for reading.
+ async fn lock_read(&self) -> RwLockReadGuard<'_, HybridPermanentStorageState> {
+ self.state.read().await
+ }
+
+ /// Locks inner state for writing.
+ async fn lock_write(&self) -> RwLockWriteGuard<'_, HybridPermanentStorageState> {
+ self.state.write().await
+ }
+
+ // -------------------------------------------------------------------------
+ // State methods
+ // -------------------------------------------------------------------------
+
+ /// Clears in-memory state.
+ pub async fn clear(&self) {
+ let mut state = self.lock_write().await;
+ state.accounts.clear();
+ state.transactions.clear();
+ state.blocks_by_hash.clear();
+ state.blocks_by_number.clear();
+ state.logs.clear();
+ }
+
+ async fn check_conflicts(state: &HybridPermanentStorageState, account_changes: &[ExecutionAccountChanges]) -> Option {
+ let mut conflicts = ExecutionConflictsBuilder::default();
+
+ for change in account_changes {
+ let address = &change.address;
+
+ if let Some(account) = state.accounts.get(address) {
+ // check account info conflicts
+ if let Some(original_nonce) = change.nonce.take_original_ref() {
+ let account_nonce = account.nonce.get_current_ref();
+ if original_nonce != account_nonce {
+ conflicts.add_nonce(address.clone(), account_nonce.clone(), original_nonce.clone());
+ }
+ }
+ if let Some(original_balance) = change.balance.take_original_ref() {
+ let account_balance = account.balance.get_current_ref();
+ if original_balance != account_balance {
+ conflicts.add_balance(address.clone(), account_balance.clone(), original_balance.clone());
+ }
+ }
+
+ // check slots conflicts
+ for (slot_index, slot_change) in &change.slots {
+ if let Some(account_slot) = account.slots.get(slot_index).map(|value| value.get_current_ref()) {
+ if let Some(original_slot) = slot_change.take_original_ref() {
+ let account_slot_value = account_slot.value.clone();
+ if original_slot.value != account_slot_value {
+ conflicts.add_slot(address.clone(), slot_index.clone(), account_slot_value, original_slot.value.clone());
+ }
+ }
+ }
+ }
+ }
+ }
+ conflicts.build()
+ }
+}
+
+impl HybridPermanentStorage {}
+
+#[async_trait]
+impl PermanentStorage for HybridPermanentStorage {
+ // -------------------------------------------------------------------------
+ // Block number operations
+ // -------------------------------------------------------------------------
+
+ async fn read_current_block_number(&self) -> anyhow::Result {
+ Ok(self.block_number.load(Ordering::SeqCst).into())
+ }
+
+ async fn increment_block_number(&self) -> anyhow::Result {
+ let next = self.block_number.fetch_add(1, Ordering::SeqCst) + 1;
+ Ok(next.into())
+ }
+
+ async fn set_block_number(&self, number: BlockNumber) -> anyhow::Result<()> {
+ self.block_number.store(number.as_u64(), Ordering::SeqCst);
+ Ok(())
+ }
+
+ // -------------------------------------------------------------------------
+ // State operations
+ // ------------------------------------------------------------------------
+
+ async fn maybe_read_account(&self, address: &Address, point_in_time: &StoragePointInTime) -> anyhow::Result