Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
31dbeb3
feat: clients also return subscription count
thlorenz Oct 31, 2025
4e47380
chore: fix unsub on already evicted + metric counts
thlorenz Nov 2, 2025
2b3adf4
chore: log discrepant account
thlorenz Nov 2, 2025
250b0ba
chore: update correct metric + log on info for now
thlorenz Nov 3, 2025
2fdc3b4
chore: merge bmuddha/fix/ws-reconnects, adjusting the changes
thlorenz Nov 3, 2025
ff548df
chore: fix max log level override
thlorenz Nov 4, 2025
78a249c
chore: fix recycle connections deadlock
thlorenz Nov 5, 2025
b754577
chore: recycle with backoff
thlorenz Nov 5, 2025
bc00095
chore: try to resub before recycle
thlorenz Nov 5, 2025
c5bbad6
feat: better orchestrated reconnection logic
thlorenz Nov 6, 2025
2111589
chore: debug fetched accounts
thlorenz Nov 7, 2025
f9ae604
chore: less fetch account chatter on debug
thlorenz Nov 7, 2025
15306b4
fix: log level issue
thlorenz Nov 7, 2025
c4af369
chore: less frequent sub metric update but with more info
thlorenz Nov 8, 2025
d9d47ba
chore: enable ledger size metric
thlorenz Nov 13, 2025
3b7e1f2
fix: use the latest SVM with gasless feepayer check
bmuddha Nov 14, 2025
f2a7d79
ci: trigger synchronize
Nov 14, 2025
b3b3f25
fix: check for privileged mode when filtering empty accounts
bmuddha Nov 14, 2025
6f76bf7
chore: fix lint
thlorenz Nov 14, 2025
8b65dc7
chore: add extra check that unescrowed payer cannot pay/write for tx
thlorenz Nov 14, 2025
2ff7121
Revert "fix: use the latest SVM with gasless feepayer check"
thlorenz Nov 14, 2025
67487ba
Allow not existing feepayer in gasless mode (#631)
GabrielePicco Nov 16, 2025
747099f
chore: skip rare case undeleg/redeleg test for now
thlorenz Nov 18, 2025
7d55d12
chore: proper evict counter
thlorenz Nov 19, 2025
407ab08
chore: increase sleep to hopefully pass redelegation tests
thlorenz Nov 19, 2025
91435fa
chore: fmt
thlorenz Nov 19, 2025
6531ac6
fix: better transaction diagnostics & rent exemption check
bmuddha Nov 18, 2025
d7107f0
chore: resolve merge conflicts
bmuddha Nov 19, 2025
19e6a17
fix: test 01_commit
bmuddha Nov 19, 2025
492d14f
fix: 01_commits rollback to previous transaction error
bmuddha Nov 19, 2025
7514dbd
fix: use correct transaction error in comparison
bmuddha Nov 19, 2025
61a1a75
Update test-integration/schedulecommit/test-scenarios/tests/01_commit…
GabrielePicco Nov 20, 2025
1434841
fix: allow auto airdrop to pay for rent & simulate inner instructions…
GabrielePicco Nov 19, 2025
72cb818
feat: persist all accounts (#648)
GabrielePicco Nov 20, 2025
f6f29a3
fix: await until sub is established and perform them in parallel (#650)
thlorenz Nov 20, 2025
239fa2f
fix(aperture): prevent racy getLatestBlockhash (#649)
bmuddha Nov 20, 2025
f521c5d
chore: post rebase fixes
bmuddha Nov 20, 2025
6740fd3
Merge branch 'master' into bmuddha/fix/better-txn-diognostics
bmuddha Nov 20, 2025
50bceef
Merge branch 'master' into bmuddha/fix/better-txn-diognostics
bmuddha Nov 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions magicblock-aperture/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bytes>) {
static CHAN_ID: AtomicU32 = AtomicU32::new(0);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();

Expand All @@ -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 {
Expand Down
5 changes: 2 additions & 3 deletions magicblock-api/src/magic_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ impl MagicValidator {
let GenesisConfigInfo {
genesis_config,
validator_pubkey,
..
} = create_genesis_config_with_leader(
u64::MAX,
&validator_pubkey,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -303,7 +302,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 =
Expand Down
7 changes: 1 addition & 6 deletions magicblock-config/src/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>,

/// Uses alpha2 country codes following https://en.wikipedia.org/wiki/ISO_3166-1
Expand Down Expand Up @@ -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(),
}
Expand All @@ -84,10 +83,6 @@ fn default_fqdn() -> Option<String> {
None
}

fn default_base_fees() -> Option<u64> {
None
}

fn default_country_code() -> CountryCode {
CountryCode::for_alpha2("US").unwrap()
}
Expand Down
6 changes: 3 additions & 3 deletions magicblock-processor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 }
Expand Down
4 changes: 3 additions & 1 deletion magicblock-processor/src/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ 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).
pub is_auto_airdrop_lamports_enabled: bool,
is_auto_airdrop_lamports_enabled: bool,
}

impl TransactionExecutor {
Expand Down
142 changes: 91 additions & 51 deletions magicblock-processor/src/executor/processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
);
}
}

Expand All @@ -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) => {
Expand Down Expand Up @@ -171,44 +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
&& !self.is_auto_airdrop_lamports_enabled
{
if let Ok(ProcessedTransaction::Executed(ref mut executed)) =
&mut result
{
executed.execution_details.status =
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)
}

Expand Down Expand Up @@ -368,9 +335,82 @@ 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()
&& !self.is_auto_airdrop_lamports_enabled
&& (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);
let logs = executed
.execution_details
.log_messages
.get_or_insert_default();
let msg = "Feepayer balance has been modified illegally".into();
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;
let logs = executed
.execution_details
.log_messages
.get_or_insert_default();
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 } => {
Expand Down
7 changes: 4 additions & 3 deletions magicblock-processor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading