From 31dbeb3b936aaeae82400140708e698fcefa047a Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Fri, 31 Oct 2025 11:30:10 +0200 Subject: [PATCH 01/37] feat: clients also return subscription count --- .../src/remote_account_provider/chain_pubsub_actor.rs | 4 ++++ .../src/remote_account_provider/chain_pubsub_client.rs | 4 ++++ magicblock-chainlink/src/remote_account_provider/mod.rs | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs index a8259f71e..224a56c44 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs @@ -202,6 +202,10 @@ impl ChainPubsubActor { subs.keys().copied().collect() } + pub fn subscription_count(&self) -> usize { + self.subscriptions.lock().unwrap().len() + } + pub async fn send_msg( &self, msg: ChainPubsubActorMessage, diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs index 719d12345..73056da07 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs @@ -276,6 +276,10 @@ impl ReconnectableClient for ChainPubsubClientImpl { } Ok(()) } + + async fn subscription_count(&self) -> usize { + self.actor.subscription_count() + } } // ----------------- diff --git a/magicblock-chainlink/src/remote_account_provider/mod.rs b/magicblock-chainlink/src/remote_account_provider/mod.rs index 3c3700a75..8b991830e 100644 --- a/magicblock-chainlink/src/remote_account_provider/mod.rs +++ b/magicblock-chainlink/src/remote_account_provider/mod.rs @@ -58,7 +58,7 @@ pub use remote_account::{ResolvedAccount, ResolvedAccountSharedData}; use crate::{errors::ChainlinkResult, submux::SubMuxClient}; -const ACTIVE_SUBSCRIPTIONS_UPDATE_INTERVAL_MS: u64 = 60_000; +const ACTIVE_SUBSCRIPTIONS_UPDATE_INTERVAL_MS: u64 = 5_000; // Maps pubkey -> (fetch_start_slot, requests_waiting) type FetchResult = Result; From 4e473802a1acd1aa7b0c0259bc871b62a3f7726f Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Sun, 2 Nov 2025 12:07:13 +0200 Subject: [PATCH 02/37] chore: fix unsub on already evicted + metric counts --- .../remote_account_provider/chain_pubsub_actor.rs | 14 ++++++++++++-- .../remote_account_provider/chain_pubsub_client.rs | 13 +++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs index 224a56c44..63a929b02 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs @@ -202,8 +202,18 @@ impl ChainPubsubActor { subs.keys().copied().collect() } - pub fn subscription_count(&self) -> usize { - self.subscriptions.lock().unwrap().len() + pub fn subscription_count(&self, filter: &[Pubkey]) -> usize { + let subs = self + .subscriptions + .lock() + .expect("subscriptions lock poisoned"); + if filter.is_empty() { + return subs.len(); + } else { + subs.keys() + .filter(|pubkey| !filter.contains(pubkey)) + .count() + } } pub async fn send_msg( diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs index 73056da07..56c4c1879 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs @@ -277,8 +277,17 @@ impl ReconnectableClient for ChainPubsubClientImpl { Ok(()) } - async fn subscription_count(&self) -> usize { - self.actor.subscription_count() + async fn subscription_count( + &self, + exclude: Option<&[Pubkey]>, + ) -> (usize, usize) { + let total = self.actor.subscription_count(&[]); + let filtered = if let Some(exclude) = exclude { + self.actor.subscription_count(exclude) + } else { + total + }; + (total, filtered) } } From 2b3adf479fbf334d247e353753f3cbe3565b6290 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Sun, 2 Nov 2025 14:09:32 +0200 Subject: [PATCH 03/37] chore: log discrepant account --- .../src/remote_account_provider/chain_pubsub_actor.rs | 10 +++++++++- .../src/remote_account_provider/chain_pubsub_client.rs | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs index 63a929b02..51e88ff6c 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs @@ -208,7 +208,7 @@ impl ChainPubsubActor { .lock() .expect("subscriptions lock poisoned"); if filter.is_empty() { - return subs.len(); + subs.len() } else { subs.keys() .filter(|pubkey| !filter.contains(pubkey)) @@ -216,6 +216,14 @@ impl ChainPubsubActor { } } + pub fn subscriptions(&self) -> Vec { + let subs = self + .subscriptions + .lock() + .expect("subscriptions lock poisoned"); + subs.keys().copied().collect() + } + pub async fn send_msg( &self, msg: ChainPubsubActorMessage, diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs index 56c4c1879..7227461e6 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs @@ -289,6 +289,10 @@ impl ReconnectableClient for ChainPubsubClientImpl { }; (total, filtered) } + + fn subscriptions(&self) -> Vec { + self.actor.subscriptions() + } } // ----------------- From 250b0bac36380be15feada7fedcf435730cbfca5 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Mon, 3 Nov 2025 16:30:50 +0200 Subject: [PATCH 04/37] chore: update correct metric + log on info for now --- magicblock-chainlink/src/remote_account_provider/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicblock-chainlink/src/remote_account_provider/mod.rs b/magicblock-chainlink/src/remote_account_provider/mod.rs index 8b991830e..72533752e 100644 --- a/magicblock-chainlink/src/remote_account_provider/mod.rs +++ b/magicblock-chainlink/src/remote_account_provider/mod.rs @@ -265,7 +265,7 @@ impl RemoteAccountProvider { } } - debug!("Updating active subscriptions: count={}", pubsub_total); + info!("Updating active subscriptions: count={}", pubsub_total); trace!("All subscriptions: {}", pubkeys_str(&all_pubsub_subs)); set_monitored_accounts_count(pubsub_total); } From 2fdc3b42ae3dcaf0528dfcc743531b749b592b86 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Mon, 3 Nov 2025 18:17:38 +0200 Subject: [PATCH 05/37] chore: merge bmuddha/fix/ws-reconnects, adjusting the changes --- .../src/remote_account_provider/chain_pubsub_actor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs index 51e88ff6c..334414604 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs @@ -28,6 +28,7 @@ use super::{ // Log every 10 secs (given chain slot time is 400ms) const CLOCK_LOG_SLOT_FREQ: u64 = 25; +const MAX_SUBSCRIBE_ATTEMPTS: usize = 3; #[derive(Debug, Clone)] pub struct PubsubClientConfig { From ff548df14239c4458a650141167637604b2976c5 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 4 Nov 2025 09:39:19 +0200 Subject: [PATCH 06/37] chore: fix max log level override --- magicblock-chainlink/src/remote_account_provider/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicblock-chainlink/src/remote_account_provider/mod.rs b/magicblock-chainlink/src/remote_account_provider/mod.rs index 72533752e..8b991830e 100644 --- a/magicblock-chainlink/src/remote_account_provider/mod.rs +++ b/magicblock-chainlink/src/remote_account_provider/mod.rs @@ -265,7 +265,7 @@ impl RemoteAccountProvider { } } - info!("Updating active subscriptions: count={}", pubsub_total); + debug!("Updating active subscriptions: count={}", pubsub_total); trace!("All subscriptions: {}", pubkeys_str(&all_pubsub_subs)); set_monitored_accounts_count(pubsub_total); } From 78a249c15e610bc9b99c9f3d922a8e93e2278151 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 5 Nov 2025 11:39:00 +0200 Subject: [PATCH 07/37] chore: fix recycle connections deadlock --- .../src/remote_account_provider/chain_pubsub_actor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs index 334414604..54b58b913 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs @@ -140,6 +140,7 @@ impl ChainPubsubActor { mpsc::channel(MESSAGE_CHANNEL_SIZE); let shutdown_token = CancellationToken::new(); + let recycle_lock = Arc::new(AsyncMutex::new(())); let me = Self { pubsub_client_config, pubsub_connection, From b75457746ab3c4e914579e4f944d991cb46b3c14 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 5 Nov 2025 16:16:59 +0200 Subject: [PATCH 08/37] chore: recycle with backoff --- .../remote_account_provider/chain_pubsub_actor.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs index 54b58b913..f4d49c342 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs @@ -30,6 +30,19 @@ use super::{ const CLOCK_LOG_SLOT_FREQ: u64 = 25; const MAX_SUBSCRIBE_ATTEMPTS: usize = 3; +/// Fibonacci backoff delay for retry attempts (in seconds) +fn fib_backoff_seconds(attempt: usize) -> u64 { + match attempt { + 1 => 0, + 2 => 1, + 3 => 2, + 4 => 3, + 5 => 5, + 6 => 8, + _ => 13, // cap at 13s for higher attempts + } +} + #[derive(Debug, Clone)] pub struct PubsubClientConfig { pub pubsub_url: String, From bc0009579981acf1ec61ce4207a25adbec8e1df3 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 5 Nov 2025 17:16:35 +0200 Subject: [PATCH 09/37] chore: try to resub before recycle --- .../chain_pubsub_actor.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs index f4d49c342..229f7a527 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs @@ -28,10 +28,11 @@ use super::{ // Log every 10 secs (given chain slot time is 400ms) const CLOCK_LOG_SLOT_FREQ: u64 = 25; -const MAX_SUBSCRIBE_ATTEMPTS: usize = 3; +const MAX_RECYCLE_ATTEMPTS: usize = 3; +const SUBSCRIBE_ATTEMPTS_PER_RECYCLE: usize = 4; /// Fibonacci backoff delay for retry attempts (in seconds) -fn fib_backoff_seconds(attempt: usize) -> u64 { +fn fib_backoff_recycle_second(attempt: usize) -> u64 { match attempt { 1 => 0, 2 => 1, @@ -43,6 +44,17 @@ fn fib_backoff_seconds(attempt: usize) -> u64 { } } +fn fib_backoff_subscribe_millis(attempt: usize) -> u64 { + match attempt { + 1 => 50, + 2 => 100, + 3 => 200, + 4 => 300, + 5 => 500, + _ => 1000, + } +} + #[derive(Debug, Clone)] pub struct PubsubClientConfig { pub pubsub_url: String, From c5bbad6e788300ea934176ff0df0145d61da56f5 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Thu, 6 Nov 2025 07:57:56 +0200 Subject: [PATCH 10/37] feat: better orchestrated reconnection logic --- .../chain_pubsub_actor.rs | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs index 229f7a527..bb4c5db4c 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs @@ -28,32 +28,6 @@ use super::{ // Log every 10 secs (given chain slot time is 400ms) const CLOCK_LOG_SLOT_FREQ: u64 = 25; -const MAX_RECYCLE_ATTEMPTS: usize = 3; -const SUBSCRIBE_ATTEMPTS_PER_RECYCLE: usize = 4; - -/// Fibonacci backoff delay for retry attempts (in seconds) -fn fib_backoff_recycle_second(attempt: usize) -> u64 { - match attempt { - 1 => 0, - 2 => 1, - 3 => 2, - 4 => 3, - 5 => 5, - 6 => 8, - _ => 13, // cap at 13s for higher attempts - } -} - -fn fib_backoff_subscribe_millis(attempt: usize) -> u64 { - match attempt { - 1 => 50, - 2 => 100, - 3 => 200, - 4 => 300, - 5 => 500, - _ => 1000, - } -} #[derive(Debug, Clone)] pub struct PubsubClientConfig { @@ -165,7 +139,6 @@ impl ChainPubsubActor { mpsc::channel(MESSAGE_CHANNEL_SIZE); let shutdown_token = CancellationToken::new(); - let recycle_lock = Arc::new(AsyncMutex::new(())); let me = Self { pubsub_client_config, pubsub_connection, @@ -244,6 +217,9 @@ impl ChainPubsubActor { } pub fn subscriptions(&self) -> Vec { + if !self.is_connected.load(Ordering::SeqCst) { + return vec![]; + } let subs = self .subscriptions .lock() From 2111589afa441932aa41183b33bedf4a6e81176d Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Fri, 7 Nov 2025 14:17:23 +0200 Subject: [PATCH 11/37] chore: debug fetched accounts --- magicblock-chainlink/src/chainlink/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magicblock-chainlink/src/chainlink/mod.rs b/magicblock-chainlink/src/chainlink/mod.rs index 5b8606e3a..2940d183d 100644 --- a/magicblock-chainlink/src/chainlink/mod.rs +++ b/magicblock-chainlink/src/chainlink/mod.rs @@ -359,7 +359,7 @@ Kept: {} delegated, {} blacklisted", pubkeys: &[Pubkey], mark_empty_if_not_found: Option<&[Pubkey]>, ) -> ChainlinkResult { - if log::log_enabled!(log::Level::Trace) { + if log::log_enabled!(log::Level::Debug) { let pubkeys_str = pubkeys .iter() .map(|p| p.to_string()) @@ -373,7 +373,7 @@ Kept: {} delegated, {} blacklisted", .join(", ") }) .unwrap_or_default(); - trace!("Fetching accounts: {pubkeys_str}, mark_empty_if_not_found: {mark_empty_str}"); + debug!("Fetching accounts: {pubkeys_str}, mark_empty_if_not_found: {mark_empty_str}"); } Self::promote_accounts( fetch_cloner, From f9ae604cf57c501026535fc633e14250b4bb9134 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Fri, 7 Nov 2025 17:57:21 +0200 Subject: [PATCH 12/37] chore: less fetch account chatter on debug --- magicblock-chainlink/src/chainlink/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicblock-chainlink/src/chainlink/mod.rs b/magicblock-chainlink/src/chainlink/mod.rs index 2940d183d..559f7be53 100644 --- a/magicblock-chainlink/src/chainlink/mod.rs +++ b/magicblock-chainlink/src/chainlink/mod.rs @@ -373,7 +373,7 @@ Kept: {} delegated, {} blacklisted", .join(", ") }) .unwrap_or_default(); - debug!("Fetching accounts: {pubkeys_str}, mark_empty_if_not_found: {mark_empty_str}"); + trace!("Fetching accounts: {pubkeys_str}, mark_empty_if_not_found: {mark_empty_str}"); } Self::promote_accounts( fetch_cloner, From 15306b4709471bd896d36be1583daa7b52f0e96e Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Fri, 7 Nov 2025 21:06:36 +0200 Subject: [PATCH 13/37] fix: log level issue --- magicblock-chainlink/src/chainlink/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicblock-chainlink/src/chainlink/mod.rs b/magicblock-chainlink/src/chainlink/mod.rs index 559f7be53..5b8606e3a 100644 --- a/magicblock-chainlink/src/chainlink/mod.rs +++ b/magicblock-chainlink/src/chainlink/mod.rs @@ -359,7 +359,7 @@ Kept: {} delegated, {} blacklisted", pubkeys: &[Pubkey], mark_empty_if_not_found: Option<&[Pubkey]>, ) -> ChainlinkResult { - if log::log_enabled!(log::Level::Debug) { + if log::log_enabled!(log::Level::Trace) { let pubkeys_str = pubkeys .iter() .map(|p| p.to_string()) From c4af369fbb554fd07795717261aae4e8bdd2d8a1 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Sat, 8 Nov 2025 21:15:31 +0200 Subject: [PATCH 14/37] chore: less frequent sub metric update but with more info --- magicblock-chainlink/src/remote_account_provider/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicblock-chainlink/src/remote_account_provider/mod.rs b/magicblock-chainlink/src/remote_account_provider/mod.rs index 8b991830e..3c3700a75 100644 --- a/magicblock-chainlink/src/remote_account_provider/mod.rs +++ b/magicblock-chainlink/src/remote_account_provider/mod.rs @@ -58,7 +58,7 @@ pub use remote_account::{ResolvedAccount, ResolvedAccountSharedData}; use crate::{errors::ChainlinkResult, submux::SubMuxClient}; -const ACTIVE_SUBSCRIPTIONS_UPDATE_INTERVAL_MS: u64 = 5_000; +const ACTIVE_SUBSCRIPTIONS_UPDATE_INTERVAL_MS: u64 = 60_000; // Maps pubkey -> (fetch_start_slot, requests_waiting) type FetchResult = Result; From d9d47ba7c25799bb90a185a9a3299b5e11abb97a Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Thu, 13 Nov 2025 14:14:50 +0200 Subject: [PATCH 15/37] chore: enable ledger size metric --- magicblock-api/src/tickers.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/magicblock-api/src/tickers.rs b/magicblock-api/src/tickers.rs index d1df1e993..279d19baa 100644 --- a/magicblock-api/src/tickers.rs +++ b/magicblock-api/src/tickers.rs @@ -175,3 +175,4 @@ pub fn init_system_metrics_ticker( } }) } +*/ From 3b7e1f27cfb082d298582db0632d822b94e57643 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 14 Nov 2025 14:43:16 +0400 Subject: [PATCH 16/37] fix: use the latest SVM with gasless feepayer check --- Cargo.lock | 2 +- Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0afa4a4d2..1ed2988ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9039,7 +9039,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2#11bbaf2249aeb16cec4111e86f2e18a0c45ff1f2" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862#4d278626742352432e5a6a856e73be7ca4bbd727" dependencies = [ "ahash 0.8.12", "log", diff --git a/Cargo.toml b/Cargo.toml index 8f7b05070..b54648166 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -220,7 +220,7 @@ vergen = "8.3.1" [workspace.dependencies.solana-svm] git = "https://github.com/magicblock-labs/magicblock-svm.git" -rev = "11bbaf2" +rev = "4d27862" features = ["dev-context-only-utils"] [patch.crates-io] @@ -229,4 +229,4 @@ features = ["dev-context-only-utils"] # and we use protobuf-src v2.1.1. Otherwise compilation fails solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "731fa50" } solana-storage-proto = { path = "./storage-proto" } -solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "11bbaf2" } +solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "4d27862" } From f2a7d79225a047219731ed03b4e62071a30f9a75 Mon Sep 17 00:00:00 2001 From: Luca Cillario Date: Fri, 14 Nov 2025 12:31:56 +0100 Subject: [PATCH 17/37] ci: trigger synchronize From b3b3f251632b49ec86080cb03fb82bf02e887043 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 14 Nov 2025 17:11:59 +0400 Subject: [PATCH 18/37] fix: check for privileged mode when filtering empty accounts --- magicblock-processor/src/executor/processing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicblock-processor/src/executor/processing.rs b/magicblock-processor/src/executor/processing.rs index c1e6a9ce2..ca25e75ad 100644 --- a/magicblock-processor/src/executor/processing.rs +++ b/magicblock-processor/src/executor/processing.rs @@ -10,7 +10,7 @@ use magicblock_core::{ tls::ExecutionTlsStash, }; use magicblock_metrics::metrics::FAILED_TRANSACTIONS_COUNT; -use solana_account::ReadableAccount; +use solana_account::{AccountSharedData, ReadableAccount}; use solana_pubkey::Pubkey; use solana_svm::{ account_loader::{AccountsBalances, CheckedTransactionDetails}, From 6f76bf7a2c029c58ead499fe421976e5f703227d Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Fri, 14 Nov 2025 15:25:17 +0200 Subject: [PATCH 19/37] chore: fix lint --- magicblock-processor/src/executor/processing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicblock-processor/src/executor/processing.rs b/magicblock-processor/src/executor/processing.rs index ca25e75ad..c1e6a9ce2 100644 --- a/magicblock-processor/src/executor/processing.rs +++ b/magicblock-processor/src/executor/processing.rs @@ -10,7 +10,7 @@ use magicblock_core::{ tls::ExecutionTlsStash, }; use magicblock_metrics::metrics::FAILED_TRANSACTIONS_COUNT; -use solana_account::{AccountSharedData, ReadableAccount}; +use solana_account::ReadableAccount; use solana_pubkey::Pubkey; use solana_svm::{ account_loader::{AccountsBalances, CheckedTransactionDetails}, From 8b65dc793d94f367fcf119ffd135d0d042b5a540 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Fri, 14 Nov 2025 15:36:38 +0200 Subject: [PATCH 20/37] chore: add extra check that unescrowed payer cannot pay/write for tx --- test-integration/Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index a161243a7..50e8d3436 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -3602,7 +3602,7 @@ dependencies = [ "solana-rpc", "solana-rpc-client", "solana-sdk", - "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2)", + "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862)", "solana-transaction", "tempfile", "thiserror 1.0.69", @@ -3790,7 +3790,7 @@ dependencies = [ "solana-metrics", "solana-sdk", "solana-storage-proto 0.2.3", - "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2)", + "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862)", "solana-timings", "solana-transaction-status", "thiserror 1.0.69", @@ -3857,7 +3857,7 @@ dependencies = [ "solana-pubkey", "solana-rent-collector", "solana-sdk-ids", - "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2)", + "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862)", "solana-svm-transaction", "solana-system-program", "solana-transaction", @@ -3934,7 +3934,7 @@ dependencies = [ "solana-program", "solana-pubsub-client", "solana-sdk", - "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2)", + "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862)", "solana-timings", "thiserror 1.0.69", "tokio", @@ -9120,7 +9120,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2#11bbaf2249aeb16cec4111e86f2e18a0c45ff1f2" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862#4d278626742352432e5a6a856e73be7ca4bbd727" dependencies = [ "ahash 0.8.12", "log", From 2ff7121432af9237dbfed65270e71756fe73b205 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Fri, 14 Nov 2025 17:10:41 +0200 Subject: [PATCH 21/37] Revert "fix: use the latest SVM with gasless feepayer check" This reverts commit a8900e1e66914c63dee3adce4b3c98d01405d540. --- Cargo.lock | 2 +- Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ed2988ea..0afa4a4d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9039,7 +9039,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862#4d278626742352432e5a6a856e73be7ca4bbd727" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2#11bbaf2249aeb16cec4111e86f2e18a0c45ff1f2" dependencies = [ "ahash 0.8.12", "log", diff --git a/Cargo.toml b/Cargo.toml index b54648166..8f7b05070 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -220,7 +220,7 @@ vergen = "8.3.1" [workspace.dependencies.solana-svm] git = "https://github.com/magicblock-labs/magicblock-svm.git" -rev = "4d27862" +rev = "11bbaf2" features = ["dev-context-only-utils"] [patch.crates-io] @@ -229,4 +229,4 @@ features = ["dev-context-only-utils"] # and we use protobuf-src v2.1.1. Otherwise compilation fails solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "731fa50" } solana-storage-proto = { path = "./storage-proto" } -solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "4d27862" } +solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "11bbaf2" } From 67487ba75c7897e88d7f921035f9cd5655849fb7 Mon Sep 17 00:00:00 2001 From: Gabriele Picco Date: Sun, 16 Nov 2025 22:34:10 +0400 Subject: [PATCH 22/37] Allow not existing feepayer in gasless mode (#631) --- test-integration/Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 50e8d3436..a161243a7 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -3602,7 +3602,7 @@ dependencies = [ "solana-rpc", "solana-rpc-client", "solana-sdk", - "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862)", + "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2)", "solana-transaction", "tempfile", "thiserror 1.0.69", @@ -3790,7 +3790,7 @@ dependencies = [ "solana-metrics", "solana-sdk", "solana-storage-proto 0.2.3", - "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862)", + "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2)", "solana-timings", "solana-transaction-status", "thiserror 1.0.69", @@ -3857,7 +3857,7 @@ dependencies = [ "solana-pubkey", "solana-rent-collector", "solana-sdk-ids", - "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862)", + "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2)", "solana-svm-transaction", "solana-system-program", "solana-transaction", @@ -3934,7 +3934,7 @@ dependencies = [ "solana-program", "solana-pubsub-client", "solana-sdk", - "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862)", + "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2)", "solana-timings", "thiserror 1.0.69", "tokio", @@ -9120,7 +9120,7 @@ dependencies = [ [[package]] name = "solana-svm" version = "2.2.1" -source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=4d27862#4d278626742352432e5a6a856e73be7ca4bbd727" +source = "git+https://github.com/magicblock-labs/magicblock-svm.git?rev=11bbaf2#11bbaf2249aeb16cec4111e86f2e18a0c45ff1f2" dependencies = [ "ahash 0.8.12", "log", From 747099f98913f71d10c05a2982f7e2fd89438e76 Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 18 Nov 2025 17:02:33 +0400 Subject: [PATCH 23/37] chore: skip rare case undeleg/redeleg test for now --- .../test-chainlink/tests/ix_07_redeleg_us_same_slot.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs b/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs index 984d1c3d1..ee7dc89d4 100644 --- a/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs +++ b/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs @@ -11,6 +11,7 @@ use magicblock_chainlink::{ use solana_sdk::{signature::Keypair, signer::Signer}; use test_chainlink::ixtest_context::IxtestContext; +#[ignore = "Started failing when fixing excessive subs, last time passing ded9c50a"] #[tokio::test] async fn ixtest_undelegate_redelegate_to_us_in_same_slot() { init_logger(); From 7d55d123a99b2bece60f0b14089c801efb7047ad Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 19 Nov 2025 11:04:15 +0400 Subject: [PATCH 24/37] chore: proper evict counter --- magicblock-metrics/src/metrics/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicblock-metrics/src/metrics/mod.rs b/magicblock-metrics/src/metrics/mod.rs index b2f1b44ad..c4024eda1 100644 --- a/magicblock-metrics/src/metrics/mod.rs +++ b/magicblock-metrics/src/metrics/mod.rs @@ -510,4 +510,4 @@ pub fn inc_table_mania_a_count() { pub fn inc_table_mania_close_a_count() { TABLE_MANIA_CLOSED_A_COUNT.inc() -} +} \ No newline at end of file From 407ab08bbac7d7625f76959481ce0038a352521e Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 19 Nov 2025 11:15:29 +0400 Subject: [PATCH 25/37] chore: increase sleep to hopefully pass redelegation tests --- .../test-chainlink/tests/ix_07_redeleg_us_same_slot.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs b/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs index ee7dc89d4..984d1c3d1 100644 --- a/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs +++ b/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs @@ -11,7 +11,6 @@ use magicblock_chainlink::{ use solana_sdk::{signature::Keypair, signer::Signer}; use test_chainlink::ixtest_context::IxtestContext; -#[ignore = "Started failing when fixing excessive subs, last time passing ded9c50a"] #[tokio::test] async fn ixtest_undelegate_redelegate_to_us_in_same_slot() { init_logger(); From 91435fa472f4dd2c67516c0cad4f44a33901218a Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Wed, 19 Nov 2025 11:27:05 +0400 Subject: [PATCH 26/37] chore: fmt --- magicblock-metrics/src/metrics/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicblock-metrics/src/metrics/mod.rs b/magicblock-metrics/src/metrics/mod.rs index c4024eda1..b2f1b44ad 100644 --- a/magicblock-metrics/src/metrics/mod.rs +++ b/magicblock-metrics/src/metrics/mod.rs @@ -510,4 +510,4 @@ pub fn inc_table_mania_a_count() { pub fn inc_table_mania_close_a_count() { TABLE_MANIA_CLOSED_A_COUNT.inc() -} \ No newline at end of file +} From 6531ac66b2d563227e4e266c5013591ad3bf30a1 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Tue, 18 Nov 2025 18:09:27 +0400 Subject: [PATCH 27/37] fix: better transaction diagnostics & rent exemption check --- magicblock-aperture/src/tests.rs | 18 ++- magicblock-api/src/magic_validator.rs | 5 +- magicblock-config/src/validator.rs | 7 +- magicblock-processor/Cargo.toml | 6 +- .../src/executor/processing.rs | 133 ++++++++++++------ magicblock-processor/src/lib.rs | 7 +- magicblock-processor/tests/fees.rs | 29 ++-- programs/guinea/src/lib.rs | 10 ++ .../test-scenarios/tests/01_commits.rs | 15 +- test-kit/src/lib.rs | 10 +- 10 files changed, 140 insertions(+), 100 deletions(-) diff --git a/magicblock-aperture/src/tests.rs b/magicblock-aperture/src/tests.rs index 643fbb737..d41dd1ed7 100644 --- a/magicblock-aperture/src/tests.rs +++ b/magicblock-aperture/src/tests.rs @@ -27,6 +27,8 @@ use crate::{ EventProcessor, }; +const LAMPORTS_PER_SOL: u64 = 1_000_000_000; + /// A test helper to create a unique WebSocket connection channel pair. fn ws_channel() -> (WsConnectionChannel, Receiver) { static CHAN_ID: AtomicU32 = AtomicU32::new(0); @@ -100,7 +102,9 @@ mod event_processor { #[tokio::test] async fn test_account_update() { let (state, env) = setup(); - let acc = env.create_account_with_config(1, 1, guinea::ID).pubkey(); + let acc = env + .create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID) + .pubkey(); let (tx, mut rx) = ws_channel(); // Subscribe to both the specific account and the program that owns it. @@ -140,7 +144,9 @@ mod event_processor { #[tokio::test] async fn test_transaction_update() { let (state, env) = setup(); - let acc = env.create_account_with_config(1, 42, guinea::ID).pubkey(); + let acc = env + .create_account_with_config(LAMPORTS_PER_SOL, 42, guinea::ID) + .pubkey(); let (tx, mut rx) = ws_channel(); let ix = Instruction::new_with_bincode( @@ -201,7 +207,9 @@ mod event_processor { let (state, env) = setup(); // Test multiple subscriptions to the same ACCOUNT. - let acc1 = env.create_account_with_config(1, 1, guinea::ID).pubkey(); + let acc1 = env + .create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID) + .pubkey(); let (acc_tx1, mut acc_rx1) = ws_channel(); let (acc_tx2, mut acc_rx2) = ws_channel(); @@ -227,7 +235,9 @@ mod event_processor { assert_receives_update(&mut acc_rx2, "second account subscriber").await; // Test multiple subscriptions to the same PROGRAM. - let acc2 = env.create_account_with_config(1, 1, guinea::ID).pubkey(); + let acc2 = env + .create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID) + .pubkey(); let (prog_tx1, mut prog_rx1) = ws_channel(); let (prog_tx2, mut prog_rx2) = ws_channel(); let prog_encoder = ProgramAccountEncoder { diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index f418dd405..7f8b13575 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -157,7 +157,6 @@ impl MagicValidator { let GenesisConfigInfo { genesis_config, validator_pubkey, - .. } = create_genesis_config_with_leader( u64::MAX, &validator_pubkey, @@ -274,7 +273,7 @@ impl MagicValidator { }); validator::init_validator_authority(identity_keypair); - + let base_fee = config.validator.base_fees.unwrap_or_default(); let txn_scheduler_state = TransactionSchedulerState { accountsdb: accountsdb.clone(), ledger: ledger.clone(), @@ -298,7 +297,7 @@ impl MagicValidator { let node_context = NodeContext { identity: validator_pubkey, faucet, - base_fee: config.validator.base_fees.unwrap_or_default(), + base_fee, featureset: txn_scheduler_state.environment.feature_set.clone(), }; let transaction_scheduler = diff --git a/magicblock-config/src/validator.rs b/magicblock-config/src/validator.rs index 890cb712a..7eeec0ac6 100644 --- a/magicblock-config/src/validator.rs +++ b/magicblock-config/src/validator.rs @@ -35,7 +35,6 @@ pub struct ValidatorConfig { #[derive_env_var] #[clap_from_serde_skip] // Skip because it defaults to None #[arg(help = "The base fees to use for the validator.")] - #[serde(default = "default_base_fees")] pub base_fees: Option, /// Uses alpha2 country codes following https://en.wikipedia.org/wiki/ISO_3166-1 @@ -65,7 +64,7 @@ impl Default for ValidatorConfig { millis_per_slot: default_millis_per_slot(), sigverify: default_sigverify(), fqdn: default_fqdn(), - base_fees: default_base_fees(), + base_fees: None, country_code: default_country_code(), claim_fees_interval_secs: default_claim_fees_interval_secs(), } @@ -84,10 +83,6 @@ fn default_fqdn() -> Option { None } -fn default_base_fees() -> Option { - None -} - fn default_country_code() -> CountryCode { CountryCode::for_alpha2("US").unwrap() } diff --git a/magicblock-processor/Cargo.toml b/magicblock-processor/Cargo.toml index 1cde50705..5419c62c4 100644 --- a/magicblock-processor/Cargo.toml +++ b/magicblock-processor/Cargo.toml @@ -20,14 +20,14 @@ magicblock-metrics = { workspace = true } magicblock-program = { workspace = true } solana-account = { workspace = true } +solana-address-lookup-table-program = { workspace = true } solana-bpf-loader-program = { workspace = true } solana-compute-budget-program = { workspace = true } solana-feature-set = { workspace = true } solana-fee = { workspace = true } solana-fee-structure = { workspace = true } -solana-address-lookup-table-program = { workspace = true } -solana-program = { workspace = true } solana-loader-v4-program = { workspace = true } +solana-program = { workspace = true } solana-program-runtime = { workspace = true } solana-pubkey = { workspace = true } solana-rent-collector = { workspace = true } @@ -36,8 +36,8 @@ solana-svm = { workspace = true } solana-svm-transaction = { workspace = true } solana-system-program = { workspace = true } solana-transaction = { workspace = true } -solana-transaction-status = { workspace = true } solana-transaction-error = { workspace = true } +solana-transaction-status = { workspace = true } [dev-dependencies] guinea = { workspace = true } diff --git a/magicblock-processor/src/executor/processing.rs b/magicblock-processor/src/executor/processing.rs index c1e6a9ce2..5244dbf62 100644 --- a/magicblock-processor/src/executor/processing.rs +++ b/magicblock-processor/src/executor/processing.rs @@ -61,28 +61,31 @@ impl super::TransactionExecutor { self.commit_failed_transaction(txn, status.clone()); FAILED_TRANSACTIONS_COUNT.inc(); tx.map(|tx| tx.send(status)); + // NOTE: + // Transactions that failed to load, cannot have touched the thread + // local storage, thus there's no need to clear it before returning return; } }; // The transaction has been processed, we can commit the account state changes - // Failed transactions still pay fees, so we need to commit the accounts even if the transaction failed + // NOTE: + // Failed transactions still pay fees, so we need to + // commit the accounts even if the transaction failed let feepayer = *txn.fee_payer(); self.commit_accounts(feepayer, &processed, is_replay); let result = processed.status(); - if result.is_ok() { + if result.is_ok() && !is_replay { // If the transaction succeeded, check for potential tasks // that may have been scheduled during the transaction execution // TODO: send intents here as well once implemented - if !is_replay { - while let Some(task) = ExecutionTlsStash::next_task() { - // This is a best effort send, if the tasks service has terminated - // for some reason, logging is the best we can do at this point - let _ = self.tasks_tx.send(task).inspect_err(|_| - error!("Scheduled tasks service has hung up and is no longer running") - ); - } + while let Some(task) = ExecutionTlsStash::next_task() { + // This is a best effort send, if the tasks service has terminated + // for some reason, logging is the best we can do at this point + let _ = self.tasks_tx.send(task).inspect_err(|_| + error!("Scheduled tasks service has hung up and is no longer running") + ); } } @@ -109,9 +112,6 @@ impl super::TransactionExecutor { transaction: [SanitizedTransaction; 1], tx: TxnSimulationResultTx, ) { - // Defensively clear any stale data from previous calls - ExecutionTlsStash::clear(); - let (result, _) = self.process(&transaction); let result = match result { Ok(processed) => { @@ -171,37 +171,11 @@ impl super::TransactionExecutor { let mut result = output.processing_results.pop().expect( "single transaction result is always present in the output", ); - - let gasless = self.environment.fee_lamports_per_signature == 0; - // If we are running in the gasless mode, we should not allow - // any mutation of the feepayer account, since that would make - // it possible for malicious actors to perform transfer operations - // from undelegated feepayers to delegated accounts, which would - // result in validator losing funds upon balance settling. - if gasless { - let undelegated_feepayer_was_modified = result - .as_ref() - .ok() - .and_then(|r| r.executed_transaction()) - .and_then(|txn| { - let first_acc = txn.loaded_transaction.accounts.first(); - let rollback_lamports = rollback_feepayer_lamports( - &txn.loaded_transaction.rollback_accounts, - ); - first_acc.map(|acc| (acc, rollback_lamports)) - }) - .map(|(acc, rollback_lamports)| { - (acc.1.is_dirty() - && (acc.1.lamports() != 0 || rollback_lamports != 0)) - && !acc.1.delegated() - && !acc.1.privileged() - }) - .unwrap_or(false); - - if undelegated_feepayer_was_modified { - result = Err(TransactionError::InvalidAccountForFee); - } + // Verify that account state invariants haven't been violated + if let Ok(ref mut processed) = result { + self.verify_account_states(processed); } + (result, output.balances) } @@ -361,9 +335,80 @@ impl super::TransactionExecutor { let _ = self.accounts_tx.send(account); } } + + /// Ensure that no post execution account state violations occurred: + /// 1. No modification of the non-delegated feepayer in gasless mode + /// 2. No illegal account resizing when the balance is zero + fn verify_account_states(&self, processed: &mut ProcessedTransaction) { + let ProcessedTransaction::Executed(executed) = processed else { + return; + }; + let txn = &executed.loaded_transaction; + let feepayer = txn.accounts.first(); + let rollback_lamports = + rollback_feepayer_lamports(&txn.rollback_accounts); + + let gasless = self.environment.fee_lamports_per_signature == 0; + if gasless { + // If we are running in the gasless mode, we should not allow + // any mutation of the feepayer account, since that would make + // it possible for malicious actors to peform transfer operations + // from undelegated feepayers to delegated accounts, which would + // result in validator loosing funds upon balance settling. + let undelegated_feepayer_was_modified = feepayer + .map(|acc| { + (acc.1.is_dirty() + && (acc.1.lamports() != 0 || rollback_lamports != 0)) + && !acc.1.delegated() + && !acc.1.privileged() + }) + .unwrap_or_default(); + if undelegated_feepayer_was_modified { + executed.execution_details.status = + Err(TransactionError::InvalidAccountForFee); + if let Some(logs) = &mut executed.execution_details.log_messages + { + let msg = "Feepayer balance has been modified illegally" + .to_string(); + logs.push(msg); + } + return; + } + } + // SVM ignores rent exemption enforcement for accounts, which have + // 0 lamports, so it's possible to call realloc on account with zero + // balance bypassing the runtime checks. In order to prevent this + // edge case we perform explicit post execution check here. + for (i, (pubkey, acc)) in txn.accounts.iter().enumerate() { + if !acc.is_dirty() { + continue; + } + let Some(rent) = self.environment.rent_collector else { + continue; + }; + if acc.lamports() == 0 && acc.data().is_empty() { + continue; + } + let rent_exemption_balance = + rent.get_rent().minimum_balance(acc.data().len()); + if acc.lamports() >= rent_exemption_balance { + continue; + } + let error = Err(TransactionError::InsufficientFundsForRent { + account_index: i as u8, + }); + executed.execution_details.status = error; + if let Some(logs) = &mut executed.execution_details.log_messages { + let msg = + format!("Account {pubkey} has violated rent exemption",); + logs.push(msg); + } + return; + } + } } -// A utils to extract the rollback lamports of the feepayer +// A utility to extract the rollback lamports of the feepayer fn rollback_feepayer_lamports(rollback: &RollbackAccounts) -> u64 { match rollback { RollbackAccounts::FeePayerOnly { fee_payer_account } => { diff --git a/magicblock-processor/src/lib.rs b/magicblock-processor/src/lib.rs index d01b9a9d9..a1696e933 100644 --- a/magicblock-processor/src/lib.rs +++ b/magicblock-processor/src/lib.rs @@ -46,9 +46,10 @@ pub fn build_svm_env( } // We have a static rent which is setup once at startup, - // and never changes afterwards. For now we use the same - // values as the vanila solana validator (default()) - let rent_collector = Box::leak(Box::new(RentCollector::default())); + // and never changes afterwards, so we just extend the + // lifetime to 'static by leaking the allocation. + let rent_collector = RentCollector::default(); + let rent_collector = Box::leak(Box::new(rent_collector)); TransactionProcessingEnvironment { blockhash, diff --git a/magicblock-processor/tests/fees.rs b/magicblock-processor/tests/fees.rs index 3c1898313..4632053b1 100644 --- a/magicblock-processor/tests/fees.rs +++ b/magicblock-processor/tests/fees.rs @@ -7,6 +7,7 @@ use solana_keypair::Keypair; use solana_program::{ instruction::{AccountMeta, Instruction}, native_token::LAMPORTS_PER_SOL, + rent::Rent, }; use solana_pubkey::Pubkey; use solana_transaction_error::TransactionError; @@ -31,8 +32,9 @@ fn setup_guinea_instruction( ix_data: &GuineaInstruction, is_writable: bool, ) -> (Instruction, Pubkey) { + let balance = Rent::default().minimum_balance(128); let account = env - .create_account_with_config(LAMPORTS_PER_SOL, 128, guinea::ID) + .create_account_with_config(balance, 128, guinea::ID) .pubkey(); let meta = if is_writable { AccountMeta::new(account, false) @@ -137,13 +139,9 @@ async fn test_escrowed_payer_success() { let fee_payer_initial_balance = env.get_payer().lamports(); let escrow_initial_balance = env.get_account(escrow).lamports(); - const ACCOUNT_SIZE: usize = 1024; - let (ix, account_to_resize) = setup_guinea_instruction( - &env, - &GuineaInstruction::Resize(ACCOUNT_SIZE), - true, - ); + let (ix, _) = + setup_guinea_instruction(&env, &GuineaInstruction::PrintSizes, false); let txn = env.build_transaction(&[ix]); env.execute_transaction(txn) @@ -152,13 +150,11 @@ async fn test_escrowed_payer_success() { let fee_payer_final_balance = env.get_payer().lamports(); let escrow_final_balance = env.get_account(escrow).lamports(); - let final_account_size = env.get_account(account_to_resize).data().len(); let mut updated_accounts = HashSet::new(); while let Ok(acc) = env.dispatch.account_update.try_recv() { updated_accounts.insert(acc.account.pubkey); } - println!("escrow: {escrow}\naccounts: {updated_accounts:?}"); assert_eq!( fee_payer_final_balance, fee_payer_initial_balance, "primary payer should not be charged" @@ -176,10 +172,6 @@ async fn test_escrowed_payer_success() { !updated_accounts.contains(&env.payer.pubkey()), "orginal payer account update should not have been sent" ); - assert_eq!( - final_account_size, ACCOUNT_SIZE, - "instruction side effects should be committed on success" - ); } /// Verifies the fee payer is charged even when the transaction fails during execution. @@ -269,7 +261,7 @@ async fn test_escrow_charged_for_failed_transaction() { #[tokio::test] async fn test_transaction_gasless_mode() { // Initialize the environment with a base fee of 0. - let env = ExecutionTestEnv::new_with_fee(0); + let env = ExecutionTestEnv::new_with_config(0); let mut payer = env.get_payer(); payer.set_lamports(1); // Not enough to cover standard fee payer.set_delegated(false); // Explicitly set the payer as NON-delegated. @@ -315,7 +307,7 @@ async fn test_transaction_gasless_mode() { #[tokio::test] async fn test_transaction_gasless_mode_with_not_existing_account() { // Initialize the environment with a base fee of 0. - let env = ExecutionTestEnv::new_with_fee(0); + let env = ExecutionTestEnv::new_with_config(0); let mut payer = env.get_payer(); payer.set_lamports(1); // Not enough to cover standard fee payer.set_delegated(false); // Explicitly set the payer as NON-delegated. @@ -365,8 +357,9 @@ async fn test_transaction_gasless_mode_with_not_existing_account() { #[tokio::test] async fn test_transaction_gasless_mode_not_existing_feepayer() { // Initialize the environment with a base fee of 0. - let payer = Keypair::new(); - let env = ExecutionTestEnv::new_with_payer_and_fees(&payer, 0); + let env = ExecutionTestEnv::new_with_config(0); + let payer = env.get_payer().pubkey; + env.accountsdb.remove_account(&payer); // Simple noop instruction that does not touch the fee payer account let ix = Instruction::new_with_bincode( @@ -398,7 +391,7 @@ async fn test_transaction_gasless_mode_not_existing_feepayer() { // Verify that the payer balance is zero (or doesn't exist) let final_balance = env .accountsdb - .get_account(&payer.pubkey()) + .get_account(&payer) .unwrap_or_default() .lamports(); assert_eq!( diff --git a/programs/guinea/src/lib.rs b/programs/guinea/src/lib.rs index c64284876..e141af857 100644 --- a/programs/guinea/src/lib.rs +++ b/programs/guinea/src/lib.rs @@ -14,6 +14,8 @@ use solana_program::{ program::{invoke, set_return_data}, program_error::ProgramError, pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, }; entrypoint::entrypoint!(process_instruction); @@ -40,7 +42,15 @@ fn resize_account( mut accounts: slice::Iter, size: usize, ) -> ProgramResult { + let feepayer = next_account_info(&mut accounts)?; let account = next_account_info(&mut accounts)?; + let rent = ::get()?; + let new_account_balance = rent.minimum_balance(size) as i64; + let delta = new_account_balance - account.try_lamports()? as i64; + **account.try_borrow_mut_lamports()? = new_account_balance as u64; + let feepayer_balance = feepayer.try_lamports()? as i64; + **feepayer.try_borrow_mut_lamports()? = (feepayer_balance - delta) as u64; + account.realloc(size, false)?; Ok(()) } diff --git a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs index e2869303a..afb4a3dce 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs @@ -8,7 +8,6 @@ use program_schedulecommit::{ ScheduleCommitCpiArgs, ScheduleCommitInstruction, }; use schedulecommit_client::{verify, ScheduleCommitTestContextFields}; -use solana_program::instruction::InstructionError; use solana_rpc_client::rpc_client::SerializableTransaction; use solana_rpc_client_api::config::RpcSendTransactionConfig; use solana_sdk::{ @@ -168,15 +167,9 @@ fn test_committing_account_delegated_to_another_validator() { // Schedule commit of account delegated to another validator let res = schedule_commit_tx(&ctx, &payer, &player, player_pda, false); - // We expect IllegalOwner error since account isn't delegated to our validator + // We expect InvalidAccountForFee error since account isn't delegated to our validator let (_, tx_err) = extract_transaction_error(res); - assert_eq!( - tx_err.unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::IllegalOwner - ) - ) + assert_eq!(tx_err.unwrap(), TransactionError::InvalidAccountForFee) }); } @@ -205,9 +198,9 @@ fn test_undelegating_account_delegated_to_another_validator() { // Schedule undelegation of account delegated to another validator let res = schedule_commit_tx(&ctx, &payer, &player, player_pda, true); - // We expect IllegalOwner error since account isn't delegated to our validator + // We expect InvalidAccountForFee error since account isn't delegated to our validator let (_, tx_err) = extract_transaction_error(res); - assert_eq!(tx_err.unwrap(), TransactionError::InvalidWritableAccount); + assert_eq!(tx_err.unwrap(), TransactionError::InvalidAccountForFee); }); } diff --git a/test-kit/src/lib.rs b/test-kit/src/lib.rs index d9dbd1d81..5ea46df60 100644 --- a/test-kit/src/lib.rs +++ b/test-kit/src/lib.rs @@ -81,13 +81,7 @@ impl ExecutionTestEnv { /// 4. Pre-loads a test program (`guinea`) for use in tests. /// 5. Funds a default `payer` keypair with 1 SOL. pub fn new() -> Self { - Self::new_with_fee(Self::BASE_FEE) - } - - pub fn new_with_payer_and_fees(payer: &Keypair, fee: u64) -> Self { - let mut ctx = Self::new_with_fee(fee); - ctx.payer = payer.insecure_clone(); - ctx + Self::new_with_config(Self::BASE_FEE) } /// Creates a new, fully initialized validator test environment with given base fee @@ -98,7 +92,7 @@ impl ExecutionTestEnv { /// 3. Spawns a `TransactionScheduler` with one worker thread. /// 4. Pre-loads a test program (`guinea`) for use in tests. /// 5. Funds a default `payer` keypair with 1 SOL. - pub fn new_with_fee(fee: u64) -> Self { + pub fn new_with_config(fee: u64) -> Self { init_logger!(); let dir = tempfile::tempdir().expect("creating temp dir for validator state"); From d7107f0dafa126276b89fb7534e9b70eed3c9a85 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 19 Nov 2025 14:50:22 +0400 Subject: [PATCH 28/37] chore: resolve merge conflicts --- magicblock-api/src/tickers.rs | 1 - .../chain_pubsub_actor.rs | 25 ------------------- .../chain_pubsub_client.rs | 17 ------------- 3 files changed, 43 deletions(-) diff --git a/magicblock-api/src/tickers.rs b/magicblock-api/src/tickers.rs index 279d19baa..d1df1e993 100644 --- a/magicblock-api/src/tickers.rs +++ b/magicblock-api/src/tickers.rs @@ -175,4 +175,3 @@ pub fn init_system_metrics_ticker( } }) } -*/ diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs index bb4c5db4c..a8259f71e 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs @@ -202,31 +202,6 @@ impl ChainPubsubActor { subs.keys().copied().collect() } - pub fn subscription_count(&self, filter: &[Pubkey]) -> usize { - let subs = self - .subscriptions - .lock() - .expect("subscriptions lock poisoned"); - if filter.is_empty() { - subs.len() - } else { - subs.keys() - .filter(|pubkey| !filter.contains(pubkey)) - .count() - } - } - - pub fn subscriptions(&self) -> Vec { - if !self.is_connected.load(Ordering::SeqCst) { - return vec![]; - } - let subs = self - .subscriptions - .lock() - .expect("subscriptions lock poisoned"); - subs.keys().copied().collect() - } - pub async fn send_msg( &self, msg: ChainPubsubActorMessage, diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs index 7227461e6..719d12345 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs @@ -276,23 +276,6 @@ impl ReconnectableClient for ChainPubsubClientImpl { } Ok(()) } - - async fn subscription_count( - &self, - exclude: Option<&[Pubkey]>, - ) -> (usize, usize) { - let total = self.actor.subscription_count(&[]); - let filtered = if let Some(exclude) = exclude { - self.actor.subscription_count(exclude) - } else { - total - }; - (total, filtered) - } - - fn subscriptions(&self) -> Vec { - self.actor.subscriptions() - } } // ----------------- From 19e6a1721f0e957038205e7e78f2328fdb55bab0 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 19 Nov 2025 15:52:55 +0400 Subject: [PATCH 29/37] fix: test 01_commit --- .../schedulecommit/test-scenarios/tests/01_commits.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs index afb4a3dce..1a6199819 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs @@ -198,9 +198,9 @@ fn test_undelegating_account_delegated_to_another_validator() { // Schedule undelegation of account delegated to another validator let res = schedule_commit_tx(&ctx, &payer, &player, player_pda, true); - // We expect InvalidAccountForFee error since account isn't delegated to our validator + // We expect InvalidWriteableAccount error since account isn't delegated to our validator let (_, tx_err) = extract_transaction_error(res); - assert_eq!(tx_err.unwrap(), TransactionError::InvalidAccountForFee); + assert_eq!(tx_err.unwrap(), TransactionError::InvalidWritableAccount); }); } From 492d14f77638b9bb738e79377ee133a75cb06e35 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 19 Nov 2025 17:10:09 +0400 Subject: [PATCH 30/37] fix: 01_commits rollback to previous transaction error --- .../schedulecommit/test-scenarios/tests/01_commits.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs index 1a6199819..da5dd1d00 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs @@ -169,7 +169,7 @@ fn test_committing_account_delegated_to_another_validator() { // We expect InvalidAccountForFee error since account isn't delegated to our validator let (_, tx_err) = extract_transaction_error(res); - assert_eq!(tx_err.unwrap(), TransactionError::InvalidAccountForFee) + assert_eq!(tx_err.unwrap(), InstructionError(0, IllegalOwner)) }); } From 7514dbd6710d725a01983224d3117e5909f2eb01 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Wed, 19 Nov 2025 17:56:48 +0400 Subject: [PATCH 31/37] fix: use correct transaction error in comparison --- .../schedulecommit/test-scenarios/tests/01_commits.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs index da5dd1d00..75a9ddb7a 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs @@ -11,6 +11,7 @@ use schedulecommit_client::{verify, ScheduleCommitTestContextFields}; use solana_rpc_client::rpc_client::SerializableTransaction; use solana_rpc_client_api::config::RpcSendTransactionConfig; use solana_sdk::{ + instruction::InstructionError, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, signature::{Keypair, Signature}, @@ -169,7 +170,13 @@ fn test_committing_account_delegated_to_another_validator() { // We expect InvalidAccountForFee error since account isn't delegated to our validator let (_, tx_err) = extract_transaction_error(res); - assert_eq!(tx_err.unwrap(), InstructionError(0, IllegalOwner)) + assert_eq!( + tx_err.unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::IllegalOwner + ) + ) }); } From 61a1a753701af3ebcfac6507491cf98821469002 Mon Sep 17 00:00:00 2001 From: Gabriele Picco Date: Thu, 20 Nov 2025 12:28:49 +0400 Subject: [PATCH 32/37] Update test-integration/schedulecommit/test-scenarios/tests/01_commits.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../schedulecommit/test-scenarios/tests/01_commits.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs index 75a9ddb7a..8a4015a95 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs @@ -205,7 +205,7 @@ fn test_undelegating_account_delegated_to_another_validator() { // Schedule undelegation of account delegated to another validator let res = schedule_commit_tx(&ctx, &payer, &player, player_pda, true); - // We expect InvalidWriteableAccount error since account isn't delegated to our validator + // We expect InvalidWritableAccount error since account isn't delegated to our validator let (_, tx_err) = extract_transaction_error(res); assert_eq!(tx_err.unwrap(), TransactionError::InvalidWritableAccount); }); From 143484146bfefc9558170ebe937616f3f1d96027 Mon Sep 17 00:00:00 2001 From: Gabriele Picco Date: Wed, 19 Nov 2025 19:11:10 +0400 Subject: [PATCH 33/37] fix: allow auto airdrop to pay for rent & simulate inner instructions response (#646) * **New Features** * Configurable inclusion of inner instructions in transaction simulation responses. * Added an observable auto-airdrop toggle so automatic lamport airdrops can be enabled or disabled. * **Bug Fixes** * When auto-airdrop is disabled, fee-related failures are now recorded inside execution details (rather than turning the whole simulation into a hard error), improving error visibility. --- .../src/requests/http/simulate_transaction.rs | 20 +++++++++++-------- magicblock-api/src/magic_validator.rs | 5 +++++ magicblock-processor/src/executor/mod.rs | 4 ++++ magicblock-processor/src/scheduler/state.rs | 2 ++ test-kit/src/lib.rs | 1 + 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/magicblock-aperture/src/requests/http/simulate_transaction.rs b/magicblock-aperture/src/requests/http/simulate_transaction.rs index c5fdf9b28..c797a3772 100644 --- a/magicblock-aperture/src/requests/http/simulate_transaction.rs +++ b/magicblock-aperture/src/requests/http/simulate_transaction.rs @@ -70,20 +70,24 @@ impl HttpDispatcher { } .into() }; - let result = RpcSimulateTransactionResult { - err: result.result.err(), - logs: result.logs, - accounts: None, - units_consumed: Some(result.units_consumed), - return_data: result.return_data.map(Into::into), - inner_instructions: result + + let inner_instructions = config.inner_instructions.then(|| { + result .inner_instructions .into_iter() .flatten() .enumerate() .map(converter) .collect::>() - .into(), + }); + + let result = RpcSimulateTransactionResult { + err: result.result.err(), + logs: result.logs, + accounts: None, + units_consumed: Some(result.units_consumed), + return_data: result.return_data.map(Into::into), + inner_instructions, replacement_blockhash, }; diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index 7f8b13575..e1e856c3a 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -282,6 +282,11 @@ impl MagicValidator { account_update_tx: validator_channels.account_update, environment: build_svm_env(&accountsdb, latest_block.blockhash, 0), tasks_tx: validator_channels.tasks_service, + is_auto_airdrop_lamports_enabled: config + .accounts + .clone + .auto_airdrop_lamports + > 0, }; txn_scheduler_state .load_upgradeable_programs(&programs_to_load(&config.programs)) diff --git a/magicblock-processor/src/executor/mod.rs b/magicblock-processor/src/executor/mod.rs index 3aa47ef1b..e808251ef 100644 --- a/magicblock-processor/src/executor/mod.rs +++ b/magicblock-processor/src/executor/mod.rs @@ -57,6 +57,8 @@ pub(super) struct TransactionExecutor { /// A read lock held during a slot's processing to synchronize with critical global /// operations like `AccountsDb` snapshots. sync: StWLock, + /// True when auto airdrop for fee payers is enabled (auto_airdrop_lamports > 0). + is_auto_airdrop_lamports_enabled: bool, } impl TransactionExecutor { @@ -103,6 +105,8 @@ impl TransactionExecutor { accounts_tx: state.account_update_tx.clone(), transaction_tx: state.transaction_status_tx.clone(), tasks_tx: state.tasks_tx.clone(), + is_auto_airdrop_lamports_enabled: state + .is_auto_airdrop_lamports_enabled, }; this.processor.fill_missing_sysvar_cache_entries(&this); diff --git a/magicblock-processor/src/scheduler/state.rs b/magicblock-processor/src/scheduler/state.rs index bed813087..dc58f20bf 100644 --- a/magicblock-processor/src/scheduler/state.rs +++ b/magicblock-processor/src/scheduler/state.rs @@ -44,6 +44,8 @@ pub struct TransactionSchedulerState { pub transaction_status_tx: TransactionStatusTx, /// A channel to send scheduled (crank) tasks created by transactions. pub tasks_tx: ScheduledTasksTx, + /// True when auto airdrop for fee payers is enabled. + pub is_auto_airdrop_lamports_enabled: bool, } impl TransactionSchedulerState { diff --git a/test-kit/src/lib.rs b/test-kit/src/lib.rs index 5ea46df60..c5572c140 100644 --- a/test-kit/src/lib.rs +++ b/test-kit/src/lib.rs @@ -126,6 +126,7 @@ impl ExecutionTestEnv { txn_to_process_rx: validator_channels.transaction_to_process, tasks_tx: validator_channels.tasks_service, environment, + is_auto_airdrop_lamports_enabled: false, }; // Load test program From 72cb8183831f7b825e2b7785eab0732a3106483e Mon Sep 17 00:00:00 2001 From: Gabriele Picco Date: Thu, 20 Nov 2025 10:56:53 +0400 Subject: [PATCH 34/37] feat: persist all accounts (#648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary by CodeRabbit * **Refactor** * Simplified transaction fee handling logic to improve reliability and code maintainability. * Enhanced account management for automatic airdrop functionality with updated processing order. ✏️ Tip: You can customize this high-level summary in your review settings. --- magicblock-chainlink/src/chainlink/mod.rs | 32 +++++++++-------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/magicblock-chainlink/src/chainlink/mod.rs b/magicblock-chainlink/src/chainlink/mod.rs index 5b8606e3a..1ef11a28b 100644 --- a/magicblock-chainlink/src/chainlink/mod.rs +++ b/magicblock-chainlink/src/chainlink/mod.rs @@ -243,36 +243,29 @@ Kept: {} delegated, {} blacklisted", .copied() .collect::>(); let feepayer = tx.message().fee_payer(); - // In the case of transactions we need to clone the feepayer account - let clone_escrow = { - // If the fee payer account is in the bank we only clone the balance - // escrow account if the fee payer is not delegated - // If it is not in the bank we include it just in case, it is fine - // if it doesn't exist and once we cloned the feepayer account itself - // and it turns out to be delegated, then we will avoid cloning the - // escrow account next time - self.accounts_bank - .get_account(feepayer) - .is_none_or(|a| !a.delegated()) - }; - // Always allow the fee payer to be treated as empty-if-not-found so that - // transactions can still be processed in gasless mode - let mut mark_empty_if_not_found = vec![*feepayer]; + // Determine if we need to clone the escrow account for the feepayer + let clone_escrow = self + .accounts_bank + .get_account(feepayer) + .is_none_or(|a| !a.delegated()); + // If cloning escrow, add the balance PDA if clone_escrow { let balance_pda = ephemeral_balance_pda_from_payer(feepayer, 0); trace!("Adding balance PDA {balance_pda} for feepayer {feepayer}"); pubkeys.push(balance_pda); - mark_empty_if_not_found.push(balance_pda); } - let mark_empty_if_not_found = (!mark_empty_if_not_found.is_empty()) - .then(|| &mark_empty_if_not_found[..]); + + // Mark *all* pubkeys as empty-if-not-found + let mark_empty_if_not_found = Some(pubkeys.as_slice()); + + // Ensure accounts let res = self .ensure_accounts(&pubkeys, mark_empty_if_not_found) .await?; - // Best-effort auto airdrop for fee payer if configured and still empty locally + // Best-effort auto airdrop for fee payer if configured if self.auto_airdrop_lamports > 0 { if let Some(fetch_cloner) = self.fetch_cloner() { let lamports = self @@ -280,6 +273,7 @@ Kept: {} delegated, {} blacklisted", .get_account(feepayer) .map(|a| a.lamports()) .unwrap_or(0); + if lamports == 0 { if let Err(err) = fetch_cloner .airdrop_account_if_empty( From f6f29a35f6c270ffaa5a2af562786c2b90be678b Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Thu, 20 Nov 2025 02:32:45 -0700 Subject: [PATCH 35/37] fix: await until sub is established and perform them in parallel (#650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Ensure RemoteAccountProvider::subscribe waits for successful subscription and submux subscribes to inner clients in parallel. ## Problem When `RemoteAccountProvider::subscribe` was called, the code had two critical issues: 1. **Premature response before RPC completion**: The `add_sub` function was spawning the RPC subscription call asynchronously but responding to the caller immediately after spawn, not after the subscription actually succeeded. 2. **Sequential subscription to all clients**: The `SubMuxClient::subscribe` method was subscribing to all clients sequentially and only returning when all succeeded, rather than waiting for just one client to succeed. This meant that: - The caller would get an `Ok()` response without verifying the subscription actually worked - If the subscription failed, the error would not be propagated back to the caller - The LRU cache would be updated before verifying the subscription actually worked - Subscription would fail if any single client was disconnected ## Root Causes ### Issue 1: Sequential subscription in SubMuxClient Original code in `submux/mod.rs`: ```rust async fn subscribe(&self, pubkey: Pubkey) -> RemoteAccountProviderResult<()> { for client in &self.clients { client.subscribe(pubkey).await? // Fails if ANY client fails } Ok(()) } ``` This required ALL clients to succeed, instead of waiting for the first success. ### Issue 2: Response sent before RPC call in ChainPubsubActor Original code in `chain_pubsub_actor.rs`: ```rust fn add_sub(...) { tokio::spawn(async move { // Make RPC call here (async) let (mut update_stream, unsubscribe) = match pubsub_connection .account_subscribe(&pubkey, config.clone()) .await { Ok(res) => res, Err(err) => return, }; // Send response AFTER spawn let _ = sub_response.send(Ok(())); // Listen for updates }); // Function returns before RPC call completes } ``` The response was sent inside the spawned task, so the function returned before the RPC call completed. ### Issue 3: Disconnected actor returning success When the actor was disconnected, it would still return `Ok(())`: ```rust if !is_connected.load(Ordering::SeqCst) { send_ok(response, client_id); // Returns success despite being disconnected return; } ``` ## Solution ### Change 1: Parallel subscription with first-success semantics Created a new `AccountSubscriptionTask` enum and `process()` method in `submux/subscription_task.rs` that: - Spawns subscription tasks to all clients in parallel using `FuturesUnordered` - Returns `Ok()` as soon as ANY client succeeds - Collects errors from all clients only if ALL fail - Ignores errors from clients after the first success ```rust pub enum AccountSubscriptionTask { Subscribe(Pubkey), Unsubscribe(Pubkey), Shutdown, } impl AccountSubscriptionTask { pub async fn process(self, clients: Vec>) -> RemoteAccountProviderResult<()> { tokio::spawn(async move { let mut futures = FuturesUnordered::new(); // Spawn all client subscriptions in parallel for (i, client) in clients.iter().enumerate() { futures.push(async move { let result = match task { Subscribe(pubkey) => client.subscribe(pubkey).await, // ... }; (i, result) }); } let mut tx = Some(tx); while let Some((i, result)) = futures.next().await { match result { Ok(_) => { // First success - send and drop tx if let Some(tx) = tx.take() { let _ = tx.send(Ok(())); } } Err(e) => { if tx.is_none() { // Already succeeded once, ignore subsequent errors warn!("Error from client {}: {:?}", i, e); } else { errors.push(format!("Client {}: {:?}", i, e)); } } } } }); } } ``` Updated `SubMuxClient` to use this: ```rust async fn subscribe(&self, pubkey: Pubkey) -> RemoteAccountProviderResult<()> { AccountSubscriptionTask::Subscribe(pubkey) .process(self.clients.clone()) .await } ``` ### Change 2: Make add_sub async and verify RPC before responding Changed `add_sub` from synchronous function to async, and moved the RPC call outside the spawned task: ```rust async fn add_sub(...) { // ... setup ... let config = RpcAccountInfoConfig { /* ... */ }; // Perform the subscription BEFORE spawning let (mut update_stream, unsubscribe) = match pubsub_connection .account_subscribe(&pubkey, config.clone()) .await { Ok(res) => res, Err(err) => { error!("[client_id={client_id}] Failed to subscribe to account {pubkey} {err:?}"); Self::abort_and_signal_connection_issue(/* ... */); // RPC failed - inform the requester let _ = sub_response.send(Err(err.into())); return; } }; // RPC succeeded - confirm to the requester BEFORE spawning let _ = sub_response.send(Ok(())); // NOW spawn the background task to listen for updates tokio::spawn(async move { // Listen for updates and relay them loop { tokio::select! { _ = cancellation_token.cancelled() => break, update = update_stream.next() => { // Relay update } } } // Cleanup }); } ``` Updated caller to await it: ```rust Self::add_sub(...).await; // Now awaits completion of RPC call ``` ### Change 3: Return error when subscribing while disconnected Changed actor to return error instead of success: ```rust if !is_connected.load(Ordering::SeqCst) { warn!("[client_id={client_id}] Ignoring subscribe request for {pubkey} because disconnected"); let _ = response.send(Err( RemoteAccountProviderError::AccountSubscriptionsTaskFailed( format!("Client {client_id} disconnected"), ), )); return; } ``` ### Change 4: Error type consistency Renamed and clarified error type from `AccountSubscriptionsFailed` to `AccountSubscriptionsTaskFailed` across all files for consistency. ## Subscription Flow (After Fix) 1. **Entry Point**: `RemoteAccountProvider::subscribe(pubkey)` is called 2. Calls `register_subscription(pubkey)` → `SubMuxClient::subscribe(pubkey)` 3. `SubMuxClient` creates `AccountSubscriptionTask::Subscribe(pubkey)` and calls `process()` 4. **Parallel Subscription**: `process()` spawns subscription tasks to ALL clients in parallel using `FuturesUnordered` 5. **First Success Wins**: Returns `Ok()` as soon as ANY client succeeds 6. Each client sends `AccountSubscribe` message to its `ChainPubsubActor` and awaits response 7. **Actor Validates Connection**: Checks `is_connected` flag - returns error if disconnected 8. **RPC Call Happens Now** (in the actor, not spawned): Calls `account_subscribe()` and awaits result 9. **On RPC Success**: Sends `Ok()` response back (response goes all the way back to caller) 10. **On RPC Failure**: Sends `Err` response back (tries next client) 11. **After RPC Confirmed**: Spawns background task to listen for update stream 12. **Completion**: Caller receives response only after RPC subscription is actually established ## Result After these fixes, `RemoteAccountProvider::subscribe` will: - Wait until at least one client successfully establishes an RPC subscription - Return error if all clients fail (rather than succeeding without confirming) - Fail fast if a client is disconnected - Properly propagate RPC errors back to the caller ## ## Summary by CodeRabbit * **Bug Fixes** * Immediate failure notification when subscription setup fails or a client disconnects; EOF and cancellation now trigger proper cleanup and error replies. * **Refactor** * Subscription setup now occurs before per-subscription listeners to avoid races; lifecycle and cancellation flows made more robust. * **New Features** * Centralized subscription task to coordinate subscribe/unsubscribe/shutdown across clients. * **Other** * Renamed and standardized subscription error variant and message for clearer reporting. ✏️ Tip: You can customize this high-level summary in your review settings. --- .../chain_pubsub_actor.rs | 64 ++++++----- .../chain_pubsub_client.rs | 4 +- .../src/remote_account_provider/errors.rs | 4 +- .../src/remote_account_provider/mod.rs | 2 +- magicblock-chainlink/src/submux/mod.rs | 28 ++--- .../src/submux/subscription_task.rs | 100 ++++++++++++++++++ 6 files changed, 157 insertions(+), 45 deletions(-) create mode 100644 magicblock-chainlink/src/submux/subscription_task.rs diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs index a8259f71e..becdfb73e 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_actor.rs @@ -279,8 +279,12 @@ impl ChainPubsubActor { match msg { ChainPubsubActorMessage::AccountSubscribe { pubkey, response } => { if !is_connected.load(Ordering::SeqCst) { - trace!("[client_id={client_id}] Ignoring subscribe request for {pubkey} because disconnected"); - send_ok(response, client_id); + warn!("[client_id={client_id}] Ignoring subscribe request for {pubkey} because disconnected"); + let _ = response.send(Err( + RemoteAccountProviderError::AccountSubscriptionsTaskFailed( + format!("Client {client_id} disconnected"), + ), + )); return; } let commitment_config = pubsub_client_config.commitment_config; @@ -294,7 +298,8 @@ impl ChainPubsubActor { is_connected, commitment_config, client_id, - ); + ) + .await; } ChainPubsubActorMessage::AccountUnsubscribe { pubkey, @@ -334,7 +339,7 @@ impl ChainPubsubActor { } #[allow(clippy::too_many_arguments)] - fn add_sub( + async fn add_sub( pubkey: Pubkey, sub_response: oneshot::Sender>, subs: Arc>>, @@ -375,33 +380,36 @@ impl ChainPubsubActor { ); } - tokio::spawn(async move { - let config = RpcAccountInfoConfig { - commitment: Some(commitment_config), - encoding: Some(UiAccountEncoding::Base64Zstd), - ..Default::default() - }; - let (mut update_stream, unsubscribe) = match pubsub_connection - .account_subscribe(&pubkey, config.clone()) - .await - { - Ok(res) => res, - Err(err) => { - error!("[client_id={client_id}] Failed to subscribe to account {pubkey} {err:?}"); - Self::abort_and_signal_connection_issue( - client_id, - subs.clone(), - abort_sender, - is_connected.clone(), - ); + let config = RpcAccountInfoConfig { + commitment: Some(commitment_config), + encoding: Some(UiAccountEncoding::Base64Zstd), + ..Default::default() + }; - return; - } - }; + // Perform the subscription + let (mut update_stream, unsubscribe) = match pubsub_connection + .account_subscribe(&pubkey, config.clone()) + .await + { + Ok(res) => res, + Err(err) => { + error!("[client_id={client_id}] Failed to subscribe to account {pubkey} {err:?}"); + Self::abort_and_signal_connection_issue( + client_id, + subs.clone(), + abort_sender, + is_connected.clone(), + ); + // RPC failed - inform the requester + let _ = sub_response.send(Err(err.into())); + return; + } + }; - // RPC succeeded - confirm to the requester that the subscription was made - let _ = sub_response.send(Ok(())); + // RPC succeeded - confirm to the requester that the subscription was made + let _ = sub_response.send(Ok(())); + tokio::spawn(async move { // Now keep listening for updates and relay them to the // subscription updates sender until it is cancelled loop { diff --git a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs index 719d12345..e8a014611 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_pubsub_client.rs @@ -388,7 +388,7 @@ pub mod mock { ) -> RemoteAccountProviderResult<()> { if !*self.connected.lock().unwrap() { return Err( - RemoteAccountProviderError::AccountSubscriptionsFailed( + RemoteAccountProviderError::AccountSubscriptionsTaskFailed( "mock: subscribe while disconnected".to_string(), ), ); @@ -452,7 +452,7 @@ pub mod mock { if *to_fail > 0 { *to_fail -= 1; return Err( - RemoteAccountProviderError::AccountSubscriptionsFailed( + RemoteAccountProviderError::AccountSubscriptionsTaskFailed( "mock: forced resubscribe failure".to_string(), ), ); diff --git a/magicblock-chainlink/src/remote_account_provider/errors.rs b/magicblock-chainlink/src/remote_account_provider/errors.rs index db52b1c0a..c1ad41405 100644 --- a/magicblock-chainlink/src/remote_account_provider/errors.rs +++ b/magicblock-chainlink/src/remote_account_provider/errors.rs @@ -29,8 +29,8 @@ pub enum RemoteAccountProviderError { #[error("Failed to send message to pubsub actor: {0} ({1})")] ChainPubsubActorSendError(String, String), - #[error("Failed to setup an account subscription ({0})")] - AccountSubscriptionsFailed(String), + #[error("Failed to manage subscriptions ({0})")] + AccountSubscriptionsTaskFailed(String), #[error("Failed to resolve accounts ({0})")] AccountResolutionsFailed(String), diff --git a/magicblock-chainlink/src/remote_account_provider/mod.rs b/magicblock-chainlink/src/remote_account_provider/mod.rs index 3c3700a75..c41318767 100644 --- a/magicblock-chainlink/src/remote_account_provider/mod.rs +++ b/magicblock-chainlink/src/remote_account_provider/mod.rs @@ -345,7 +345,7 @@ impl RemoteAccountProvider { > { if endpoints.is_empty() { return Err( - RemoteAccountProviderError::AccountSubscriptionsFailed( + RemoteAccountProviderError::AccountSubscriptionsTaskFailed( "No endpoints provided".to_string(), ), ); diff --git a/magicblock-chainlink/src/submux/mod.rs b/magicblock-chainlink/src/submux/mod.rs index 8c4e1f9d6..ea5e29f8b 100644 --- a/magicblock-chainlink/src/submux/mod.rs +++ b/magicblock-chainlink/src/submux/mod.rs @@ -23,6 +23,9 @@ const DEBOUNCE_INTERVAL_MILLIS: u64 = 2_000; mod debounce_state; pub use self::debounce_state::DebounceState; +mod subscription_task; +pub use self::subscription_task::AccountSubscriptionTask; + #[derive(Debug, Clone, Copy, Default)] pub struct DebounceConfig { /// The deduplication window in milliseconds. If None, defaults to @@ -143,7 +146,10 @@ struct ForwarderParams { allowed_count: usize, } -impl SubMuxClient { +impl SubMuxClient +where + T: ChainPubsubClient + ReconnectableClient, +{ pub fn new( clients: Vec<(Arc, mpsc::Receiver<()>)>, dedupe_window_millis: Option, @@ -578,26 +584,24 @@ where &self, pubkey: Pubkey, ) -> RemoteAccountProviderResult<()> { - for client in &self.clients { - client.subscribe(pubkey).await?; - } - Ok(()) + AccountSubscriptionTask::Subscribe(pubkey) + .process(self.clients.clone()) + .await } async fn unsubscribe( &self, pubkey: Pubkey, ) -> RemoteAccountProviderResult<()> { - for client in &self.clients { - client.unsubscribe(pubkey).await?; - } - Ok(()) + AccountSubscriptionTask::Unsubscribe(pubkey) + .process(self.clients.clone()) + .await } async fn shutdown(&self) { - for client in &self.clients { - client.shutdown().await; - } + let _ = AccountSubscriptionTask::Shutdown + .process(self.clients.clone()) + .await; } fn take_updates(&self) -> mpsc::Receiver { diff --git a/magicblock-chainlink/src/submux/subscription_task.rs b/magicblock-chainlink/src/submux/subscription_task.rs new file mode 100644 index 000000000..233e227e9 --- /dev/null +++ b/magicblock-chainlink/src/submux/subscription_task.rs @@ -0,0 +1,100 @@ +use std::sync::Arc; + +use futures_util::stream::{FuturesUnordered, StreamExt}; +use log::*; +use solana_pubkey::Pubkey; +use tokio::sync::oneshot; + +use crate::remote_account_provider::{ + chain_pubsub_client::{ChainPubsubClient, ReconnectableClient}, + errors::{RemoteAccountProviderError, RemoteAccountProviderResult}, +}; + +#[derive(Clone)] +pub enum AccountSubscriptionTask { + Subscribe(Pubkey), + Unsubscribe(Pubkey), + Shutdown, +} + +impl AccountSubscriptionTask { + pub async fn process( + self, + clients: Vec>, + ) -> RemoteAccountProviderResult<()> + where + T: ChainPubsubClient + ReconnectableClient + Send + Sync + 'static, + { + use AccountSubscriptionTask::*; + let (tx, rx) = oneshot::channel(); + + tokio::spawn(async move { + let mut futures = FuturesUnordered::new(); + for (i, client) in clients.iter().enumerate() { + let client = client.clone(); + let task = self.clone(); + futures.push(async move { + let result = match task { + Subscribe(pubkey) => client.subscribe(pubkey).await, + Unsubscribe(pubkey) => client.unsubscribe(pubkey).await, + Shutdown => { + client.shutdown().await; + Ok(()) + } + }; + (i, result) + }); + } + + let mut errors = Vec::new(); + let mut tx = Some(tx); + let op_name = match self { + Subscribe(_) => "Subscribe", + Unsubscribe(_) => "Unsubscribe", + Shutdown => "Shutdown", + }; + + while let Some((i, result)) = futures.next().await { + match result { + Ok(_) => { + if let Some(tx) = tx.take() { + let _ = tx.send(Ok(())); + } + } + Err(e) => { + if tx.is_none() { + // If at least one client returned an `OK` response, ignore any `ERR` responses + // after that. These clients will also trigger the reconnection logic + // which takes care of fixing the RPC connection. + warn!( + "{} failed for client {}: {:?}", + op_name, i, e + ); + } else { + errors.push(format!("Client {}: {:?}", i, e)); + } + } + } + } + + if let Some(tx) = tx { + let msg = format!( + "All clients failed to {}: {}", + op_name.to_lowercase(), + errors.join(", ") + ); + let _ = tx.send(Err( + RemoteAccountProviderError::AccountSubscriptionsTaskFailed( + msg, + ), + )); + } + }); + + rx.await.unwrap_or_else(|_| { + Err(RemoteAccountProviderError::AccountSubscriptionsTaskFailed( + "Orchestration task panicked or dropped channel".to_string(), + )) + }) + } +} From 239fa2fdbcc90b6106ef9f50a1ce725193e733b9 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov <31780624+bmuddha@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:28:06 +0400 Subject: [PATCH 36/37] fix(aperture): prevent racy getLatestBlockhash (#649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes the issue when the blockhash returned to the client didn't exist in the cache do to timing differences between block update and cache update. ## Summary by CodeRabbit * **Refactor** * Improved block update processing for more timely and reliable event handling by reordering event handling and removing duplicate paths * Replaced prior block state tracking with a lock-free, atomic approach to reduce contention and improve performance * Simplified initialization flow by consolidating how the latest block state is provided * **Chores** * Introduced an atomic caching library to support the new lock-free latest-block tracking ✏️ Tip: You can customize this high-level summary in your review settings. --- Cargo.lock | 1 + magicblock-aperture/Cargo.toml | 1 + magicblock-aperture/src/processor.rs | 20 +++++++---------- magicblock-aperture/src/state/blocks.rs | 30 +++++++++++++++++++------ magicblock-aperture/src/state/mod.rs | 8 +++++-- 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0afa4a4d2..8b366cf74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3540,6 +3540,7 @@ dependencies = [ name = "magicblock-aperture" version = "0.2.3" dependencies = [ + "arc-swap", "base64 0.21.7", "bincode", "bs58", diff --git a/magicblock-aperture/Cargo.toml b/magicblock-aperture/Cargo.toml index 50ce6c6db..153a956a4 100644 --- a/magicblock-aperture/Cargo.toml +++ b/magicblock-aperture/Cargo.toml @@ -20,6 +20,7 @@ tokio = { workspace = true } tokio-util = { workspace = true } # containers +arc-swap = { workspace = true } scc = { workspace = true } # sync diff --git a/magicblock-aperture/src/processor.rs b/magicblock-aperture/src/processor.rs index 44faf1717..ca6035164 100644 --- a/magicblock-aperture/src/processor.rs +++ b/magicblock-aperture/src/processor.rs @@ -83,16 +83,20 @@ impl EventProcessor { } /// The main event processing loop for a single worker instance. - /// - /// This function listens on all event channels concurrently and processes messages - /// as they arrive. The `tokio::select!` macro is biased to prioritize account - /// processing, as it is typically the most frequent and time-sensitive event. async fn run(self, id: usize, cancel: CancellationToken) { info!("event processor {id} is running"); loop { tokio::select! { biased; + // Process a new block. + Ok(latest) = self.block_update_rx.recv_async() => { + // Notify subscribers waiting on slot updates. + self.subscriptions.send_slot(latest.meta.slot); + // Update the global blocks cache with the latest block. + self.blocks.set_latest(latest); + } + // Process a new account state update. Ok(state) = self.account_update_rx.recv_async() => { // Notify subscribers for this specific account. @@ -121,14 +125,6 @@ impl EventProcessor { self.transactions.push(status.signature, Some(result)); } - // Process a new block. - Ok(latest) = self.block_update_rx.recv_async() => { - // Notify subscribers waiting on slot updates. - self.subscriptions.send_slot(latest.meta.slot); - // Update the global blocks cache with the latest block. - self.blocks.set_latest(latest); - } - // Listen for the cancellation signal to gracefully shut down. _ = cancel.cancelled() => { break; diff --git a/magicblock-aperture/src/state/blocks.rs b/magicblock-aperture/src/state/blocks.rs index f5d44c1da..263606ee7 100644 --- a/magicblock-aperture/src/state/blocks.rs +++ b/magicblock-aperture/src/state/blocks.rs @@ -1,10 +1,10 @@ -use std::{ops::Deref, time::Duration}; +use std::{ops::Deref, sync::Arc, time::Duration}; +use arc_swap::ArcSwapAny; use magicblock_core::{ link::blocks::{BlockHash, BlockMeta, BlockUpdate}, Slot, }; -use magicblock_ledger::LatestBlock; use solana_rpc_client_api::response::RpcBlockhash; use super::ExpiringCache; @@ -23,12 +23,20 @@ pub(crate) struct BlocksCache { /// The number of slots for which a blockhash is considered valid. /// This is calculated based on the host ER's block time relative to Solana's. block_validity: u64, - /// The most recent block update received, protected by a `RwLock` for concurrent access. - latest: LatestBlock, + /// Latest observed block (updated whenever the ledger transitions to new slot) + latest: ArcSwapAny>, /// An underlying time-based cache for storing `BlockHash` to `BlockMeta` mappings. cache: ExpiringCache, } +/// Last produced block that has been put into cache. We need to keep this separately, +/// as there's no way to access the cache efficiently to find the latest inserted entry +#[derive(Default, Debug, Clone, Copy)] +pub(crate) struct LastCachedBlock { + pub(crate) blockhash: BlockHash, + pub(crate) slot: Slot, +} + impl Deref for BlocksCache { type Target = ExpiringCache; fn deref(&self) -> &Self::Target { @@ -44,7 +52,7 @@ impl BlocksCache { /// /// # Panics /// Panics if `blocktime` is zero. - pub(crate) fn new(blocktime: u64, latest: LatestBlock) -> Self { + pub(crate) fn new(blocktime: u64, latest: LastCachedBlock) -> Self { const BLOCK_CACHE_TTL: Duration = Duration::from_secs(60); assert!(blocktime != 0, "blocktime cannot be zero"); @@ -54,7 +62,7 @@ impl BlocksCache { let block_validity = blocktime_ratio * MAX_VALID_BLOCKHASH_SLOTS; let cache = ExpiringCache::new(BLOCK_CACHE_TTL); Self { - latest, + latest: ArcSwapAny::new(latest.into()), block_validity: block_validity as u64, cache, } @@ -62,8 +70,15 @@ impl BlocksCache { /// Updates the latest block information in the cache. pub(crate) fn set_latest(&self, latest: BlockUpdate) { - // The `push` method adds the blockhash to the underlying expiring cache. + let last = LastCachedBlock { + blockhash: latest.hash, + slot: latest.meta.slot, + }; + + // Register the block in the expiring cache self.cache.push(latest.hash, latest.meta); + // And mark it as latest observed + self.latest.swap(last.into()); } /// Retrieves information about the latest block, including its calculated validity period. @@ -83,6 +98,7 @@ impl BlocksCache { } /// A data structure containing essential details about a blockhash for RPC responses. +#[derive(Default)] pub(crate) struct BlockHashInfo { /// The blockhash. pub(crate) hash: BlockHash, diff --git a/magicblock-aperture/src/state/mod.rs b/magicblock-aperture/src/state/mod.rs index 641316321..5fb7c0928 100644 --- a/magicblock-aperture/src/state/mod.rs +++ b/magicblock-aperture/src/state/mod.rs @@ -1,6 +1,6 @@ use std::{sync::Arc, time::Duration}; -use blocks::BlocksCache; +use blocks::{BlocksCache, LastCachedBlock}; use cache::ExpiringCache; use magicblock_account_cloner::ChainlinkCloner; use magicblock_accounts_db::AccountsDb; @@ -81,7 +81,11 @@ impl SharedState { blocktime: u64, ) -> Self { const TRANSACTIONS_CACHE_TTL: Duration = Duration::from_secs(75); - let latest = ledger.latest_block().clone(); + let block = ledger.latest_block().load(); + let latest = LastCachedBlock { + blockhash: block.blockhash, + slot: block.slot, + }; Self { context, accountsdb, From f521c5d756998659263a808429b80e8d0bbabb99 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Thu, 20 Nov 2025 16:33:19 +0400 Subject: [PATCH 37/37] chore: post rebase fixes --- magicblock-processor/src/executor/mod.rs | 2 ++ .../src/executor/processing.rs | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/magicblock-processor/src/executor/mod.rs b/magicblock-processor/src/executor/mod.rs index e808251ef..30cca792c 100644 --- a/magicblock-processor/src/executor/mod.rs +++ b/magicblock-processor/src/executor/mod.rs @@ -57,6 +57,8 @@ pub(super) struct TransactionExecutor { /// A read lock held during a slot's processing to synchronize with critical global /// operations like `AccountsDb` snapshots. sync: StWLock, + /// Hacky temporary solution to allow automatic airdrops, the flag + /// is tightly contolled and will be removed in the nearest future /// True when auto airdrop for fee payers is enabled (auto_airdrop_lamports > 0). is_auto_airdrop_lamports_enabled: bool, } diff --git a/magicblock-processor/src/executor/processing.rs b/magicblock-processor/src/executor/processing.rs index 5244dbf62..98a656452 100644 --- a/magicblock-processor/src/executor/processing.rs +++ b/magicblock-processor/src/executor/processing.rs @@ -358,6 +358,7 @@ impl super::TransactionExecutor { let undelegated_feepayer_was_modified = feepayer .map(|acc| { (acc.1.is_dirty() + && !self.is_auto_airdrop_lamports_enabled && (acc.1.lamports() != 0 || rollback_lamports != 0)) && !acc.1.delegated() && !acc.1.privileged() @@ -366,12 +367,12 @@ impl super::TransactionExecutor { if undelegated_feepayer_was_modified { executed.execution_details.status = Err(TransactionError::InvalidAccountForFee); - if let Some(logs) = &mut executed.execution_details.log_messages - { - let msg = "Feepayer balance has been modified illegally" - .to_string(); - logs.push(msg); - } + let logs = executed + .execution_details + .log_messages + .get_or_insert_default(); + let msg = "Feepayer balance has been modified illegally".into(); + logs.push(msg); return; } } @@ -398,11 +399,12 @@ impl super::TransactionExecutor { account_index: i as u8, }); executed.execution_details.status = error; - if let Some(logs) = &mut executed.execution_details.log_messages { - let msg = - format!("Account {pubkey} has violated rent exemption",); - logs.push(msg); - } + let logs = executed + .execution_details + .log_messages + .get_or_insert_default(); + let msg = format!("Account {pubkey} has violated rent exemption"); + logs.push(msg); return; } }