Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
64f85bf
chore: use local svm crate
thlorenz Dec 29, 2025
1d1617a
feat: configuring enforce access permissions
thlorenz Dec 29, 2025
3b4e121
chore: iniitial lifecycle mode test
thlorenz Dec 29, 2025
8f20d36
fix: forwarding lifecycle mode on chainlink init
thlorenz Dec 29, 2025
95de7d9
feat: disable account state verification when not enforcing access pe…
thlorenz Dec 29, 2025
7f9b033
chore: dump logs on airdrop failure in integration tests
thlorenz Dec 29, 2025
dac2d4a
chore: remove obsolete method
thlorenz Dec 29, 2025
3345c74
chore: passing test verifying we can write to any account in non-ephe…
thlorenz Dec 29, 2025
822f355
chore: run all config tests in series for more stability
thlorenz Dec 29, 2025
94cf735
chore: better name for test
thlorenz Dec 29, 2025
c0afd16
Merge branch 'master' into thlorenz/mode01
thlorenz Feb 2, 2026
c0d5cb2
chore: update cargo lock
thlorenz Feb 2, 2026
248df6e
chore: use tracing log
thlorenz Feb 2, 2026
0006b16
test: add lifecycle modes cloning tests
thlorenz Feb 3, 2026
1dc6a8a
refactor: convert clone_accounts_and_programs to instance method
thlorenz Feb 3, 2026
acd9b89
chore: provide lifecycle mode down to fetch/clone decision point
thlorenz Feb 3, 2026
58a6b7f
chore: respect programs replica cloning restriction
thlorenz Feb 3, 2026
006fee4
chore: update svm dep + fix compile issue
thlorenz Feb 4, 2026
c228988
chore: move setup methods to test utils for reuse
thlorenz Feb 4, 2026
c2217e2
chore: verify that program replica clones programs fully
thlorenz Feb 5, 2026
88ba1d6
chore: fmt
thlorenz Feb 5, 2026
a8833d6
chore: fix svm reference
thlorenz Feb 5, 2026
a072c2b
Merge branch 'master' into thlorenz/mode01
thlorenz Feb 5, 2026
c2ce3a3
chore: update cargo locks
thlorenz Feb 5, 2026
af2c55f
test: Replace fixed sleep with polling loop in lifecycle_modes_cloning
thlorenz Feb 5, 2026
c3e660c
chore: fix out of diff issue
thlorenz Feb 5, 2026
61b6cff
chore: dump logs on tx failure
thlorenz Feb 5, 2026
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ spl-token-2022 = "7.0"

[workspace.dependencies.solana-svm]
git = "https://github.com/magicblock-labs/magicblock-svm.git"
rev = "3e9456ec4"
rev = "569cb82"
features = ["dev-context-only-utils"]

[workspace.dependencies.rocksdb]
Expand All @@ -231,7 +231,7 @@ version = "0.22.0"
# and we use protobuf-src v2.1.1. Otherwise compilation fails
solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "2246929" }
solana-storage-proto = { path = "./storage-proto" }
solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "3e9456ec4" }
solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "569cb82" }
# Fork is used to enable `disable_manual_compaction` usage
# Fork is based on commit d4e9e16 of rocksdb (parent commit of 0.23.0 release)
# without patching update isn't possible due to conflict with solana deps
Expand Down
11 changes: 0 additions & 11 deletions magicblock-accounts/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,3 @@ pub enum LifecycleMode {
Ephemeral,
Offline,
}

impl LifecycleMode {
pub fn requires_ephemeral_validation(&self) -> bool {
match self {
LifecycleMode::Replica => false,
LifecycleMode::ProgramsReplica => false,
LifecycleMode::Ephemeral => true,
LifecycleMode::Offline => false,
}
}
}
5 changes: 4 additions & 1 deletion magicblock-api/src/magic_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,13 @@ impl MagicValidator {
// runtime, -1 is taken up by the transaction scheduler itself
let transaction_executors =
(num_cpus::get() / 2).saturating_sub(1).max(1) as u32;
let enforce_access_permissions =
config.lifecycle.enforce_access_permissions();
let step_start = Instant::now();
let transaction_scheduler = TransactionScheduler::new(
transaction_executors,
txn_scheduler_state,
enforce_access_permissions,
);
log_timing("startup", "transaction_scheduler_init", step_start);
info!(
Expand Down Expand Up @@ -427,7 +430,7 @@ impl MagicValidator {
let accounts_bank = accountsdb.clone();
let mut chainlink_config =
ChainlinkConfig::default_with_lifecycle_mode(
LifecycleMode::Ephemeral,
config.lifecycle.clone(),
)
.with_remove_confined_accounts(
config.chainlink.remove_confined_accounts,
Expand Down
21 changes: 14 additions & 7 deletions magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use dlp::{
pda::delegation_record_pda_from_delegated_account, state::DelegationRecord,
};
use magicblock_accounts_db::traits::AccountsBank;
use magicblock_config::config::AllowedProgram;
use magicblock_config::config::{AllowedProgram, LifecycleMode};
use magicblock_core::token_programs::{
is_ata, try_derive_eata_address_and_bump, MaybeIntoAta,
};
Expand Down Expand Up @@ -87,6 +87,9 @@ where
/// If specified, only these programs will be cloned. If None or empty,
/// all programs are allowed.
allowed_programs: Option<HashSet<Pubkey>>,

/// The lifecycle mode of the validator, used to determine cloning behavior
lifecycle_mode: LifecycleMode,
}

impl<T, U, V, C> FetchCloner<T, U, V, C>
Expand All @@ -97,6 +100,7 @@ where
C: Cloner,
{
/// Create FetchCloner with subscription updates properly connected
#[allow(clippy::too_many_arguments)]
pub fn new(
remote_account_provider: &Arc<RemoteAccountProvider<T, U>>,
accounts_bank: &Arc<V>,
Expand All @@ -105,6 +109,7 @@ where
faucet_pubkey: Pubkey,
subscription_updates_rx: mpsc::Receiver<ForwardedSubscriptionUpdate>,
allowed_programs: Option<Vec<AllowedProgram>>,
lifecycle_mode: LifecycleMode,
) -> Arc<Self> {
let blacklisted_accounts =
blacklisted_accounts(&validator_pubkey, &faucet_pubkey);
Expand All @@ -120,6 +125,7 @@ where
fetch_count: Arc::new(AtomicU64::new(0)),
blacklisted_accounts,
allowed_programs,
lifecycle_mode,
});

me.clone()
Expand Down Expand Up @@ -782,6 +788,11 @@ where
.await;
accounts_to_clone.extend(ata_accounts);

// If lifecycle mode requires programs-only cloning, filter out non-program accounts
if self.lifecycle_mode.clones_programs_only() {
accounts_to_clone.retain(|request| request.account.executable());
}

// Compute sub cancellations now since we may potentially fail during a cloning step
let cancel_strategy = pipeline::compute_cancel_strategy(
pubkeys,
Expand All @@ -795,12 +806,8 @@ where

cancel_subs(&self.remote_account_provider, cancel_strategy).await;

pipeline::clone_accounts_and_programs(
self,
accounts_to_clone,
loaded_programs,
)
.await?;
self.clone_accounts_and_programs(accounts_to_clone, loaded_programs)
.await?;

Ok(FetchAndCloneResult {
not_found_on_chain: not_found,
Expand Down
78 changes: 43 additions & 35 deletions magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -638,50 +638,58 @@ pub(crate) fn compute_cancel_strategy(
}
}

/// Clones accounts and programs into the bank
#[instrument(skip(this, accounts_to_clone, loaded_programs))]
pub(crate) async fn clone_accounts_and_programs<T, U, V, C>(
this: &FetchCloner<T, U, V, C>,
accounts_to_clone: Vec<AccountCloneRequest>,
loaded_programs: Vec<
crate::remote_account_provider::program_account::LoadedProgram,
>,
) -> ClonerResult<()>
impl<T, U, V, C> FetchCloner<T, U, V, C>
where
T: ChainRpcClient,
U: ChainPubsubClient,
V: AccountsBank,
C: Cloner,
{
let mut join_set = JoinSet::new();
for request in accounts_to_clone {
if tracing::enabled!(tracing::Level::TRACE) {
trace!(
pubkey = %request.pubkey,
slot = request.account.remote_slot(),
owner = %request.account.owner(),
"Cloning account"
);
};
/// Clones accounts and programs into the bank
#[instrument(skip(self, accounts_to_clone, loaded_programs))]
pub(crate) async fn clone_accounts_and_programs(
&self,
accounts_to_clone: Vec<AccountCloneRequest>,
loaded_programs: Vec<
crate::remote_account_provider::program_account::LoadedProgram,
>,
) -> ClonerResult<()>
where
T: ChainRpcClient,
U: ChainPubsubClient,
V: AccountsBank,
C: Cloner,
{
let mut join_set = JoinSet::new();
for request in accounts_to_clone {
if tracing::enabled!(tracing::Level::TRACE) {
trace!(
pubkey = %request.pubkey,
slot = request.account.remote_slot(),
owner = %request.account.owner(),
"Cloning account"
);
};

let cloner = this.cloner.clone();
join_set.spawn(async move { cloner.clone_account(request).await });
}
let cloner = self.cloner.clone();
join_set.spawn(async move { cloner.clone_account(request).await });
}

for acc in loaded_programs {
if !this.is_program_allowed(&acc.program_id) {
debug!(program_id = %acc.program_id, "Skipping clone of program");
continue;
for acc in loaded_programs {
if !self.is_program_allowed(&acc.program_id) {
debug!(program_id = %acc.program_id, "Skipping clone of program");
continue;
}
let cloner = self.cloner.clone();
join_set.spawn(async move { cloner.clone_program(acc).await });
}
let cloner = this.cloner.clone();
join_set.spawn(async move { cloner.clone_program(acc).await });
}

join_set
.join_all()
.await
.into_iter()
.collect::<ClonerResult<Vec<_>>>()?;
join_set
.join_all()
.await
.into_iter()
.collect::<ClonerResult<Vec<_>>>()?;

Ok(())
Ok(())
}
}
5 changes: 5 additions & 0 deletions magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{collections::HashMap, sync::Arc};

use magicblock_config::config::LifecycleMode;
use solana_account::{Account, AccountSharedData, WritableAccount};
use solana_sdk_ids::system_program;
use tokio::sync::mpsc;
Expand Down Expand Up @@ -184,6 +185,7 @@ fn init_fetch_cloner(
faucet_pubkey,
subscription_rx,
None,
LifecycleMode::Ephemeral,
);
(fetch_cloner, subscription_tx)
}
Expand Down Expand Up @@ -1499,6 +1501,7 @@ async fn test_allowed_programs_filters_programs() {
random_pubkey(),
subscription_rx,
allowed_programs,
LifecycleMode::Ephemeral,
);

// Fetch and clone both programs
Expand Down Expand Up @@ -1567,6 +1570,7 @@ async fn test_allowed_programs_none_allows_all() {
random_pubkey(),
subscription_rx,
None, // No restriction
LifecycleMode::Ephemeral,
);

// Fetch and clone both programs
Expand Down Expand Up @@ -1634,6 +1638,7 @@ async fn test_allowed_programs_empty_allows_all() {
random_pubkey(),
subscription_rx,
allowed_programs,
LifecycleMode::Ephemeral,
);

// Fetch and clone both programs
Expand Down
3 changes: 3 additions & 0 deletions magicblock-chainlink/src/chainlink/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ impl<T: ChainRpcClient, U: ChainPubsubClient, V: AccountsBank, C: Cloner>
.await?;
let fetch_cloner = if let Some(provider) = account_provider {
let provider = Arc::new(provider);
let lifecycle_mode =
config.remote_account_provider.lifecycle_mode().clone();
let fetch_cloner = FetchCloner::new(
&provider,
accounts_bank,
Expand All @@ -139,6 +141,7 @@ impl<T: ChainRpcClient, U: ChainPubsubClient, V: AccountsBank, C: Cloner>
faucet_pubkey,
rx,
chainlink_config.allowed_programs.clone(),
lifecycle_mode,
);
Some(fetch_cloner)
} else {
Expand Down
2 changes: 2 additions & 0 deletions magicblock-chainlink/src/remote_account_provider/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ impl Default for MatchSlotsConfig {
impl
RemoteAccountProvider<ChainRpcClientImpl, SubMuxClient<ChainUpdatesClient>>
{
/// Creates a RemoteAccountProvider from the given endpoints and config if needed.
/// NOTE: for offline [RemoteAccountProviderConfig::lifecycle_mode] we return None
pub async fn try_from_urls_and_config(
endpoints: &Endpoints,
commitment: CommitmentConfig,
Expand Down
3 changes: 2 additions & 1 deletion magicblock-chainlink/tests/utils/test_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl TestContext {
let (tx, rx) = tokio::sync::mpsc::channel(100);
let config =
RemoteAccountProviderConfig::default_with_lifecycle_mode(
lifecycle_mode,
lifecycle_mode.clone(),
);
let subscribed_accounts =
create_test_lru_cache_with_config(&config);
Expand Down Expand Up @@ -99,6 +99,7 @@ impl TestContext {
faucet_pubkey,
rx,
None,
lifecycle_mode.clone(),
)),
Some(provider),
)
Expand Down
12 changes: 12 additions & 0 deletions magicblock-config/src/config/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,16 @@ impl LifecycleMode {
pub fn needs_remote_account_provider(&self) -> bool {
!matches!(self, LifecycleMode::Offline)
}

/// Check whether the validator lifecycle dictates to
/// clone program accounts only
pub fn clones_programs_only(&self) -> bool {
matches!(self, LifecycleMode::ProgramsReplica)
}

/// Check whether the validator lifecycle enforces access permissions
/// which is only the case currently in [LifecycleMode::Ephemeral] mode
pub fn enforce_access_permissions(&self) -> bool {
matches!(self, LifecycleMode::Ephemeral)
}
}
5 changes: 5 additions & 0 deletions magicblock-processor/src/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub(super) struct TransactionExecutor {

// Config
is_auto_airdrop_lamports_enabled: bool,
/// Whether to enforce access permissions during transaction execution.
is_enforcing_access_permissions: bool,
}

impl TransactionExecutor {
Expand All @@ -62,11 +64,13 @@ impl TransactionExecutor {
rx: TransactionToProcessRx,
ready_tx: Sender<ExecutorId>,
programs_cache: Arc<RwLock<ProgramCache<SimpleForkGraph>>>,
enforce_access_permissions: bool,
) -> Self {
let slot = state.accountsdb.slot();
let mut processor = TransactionBatchProcessor::new_uninitialized(
slot,
Default::default(),
enforce_access_permissions,
);

// Use global program cache to share compilation results across executors
Expand Down Expand Up @@ -97,6 +101,7 @@ impl TransactionExecutor {
tasks_tx: state.tasks_tx.clone(),
is_auto_airdrop_lamports_enabled: state
.is_auto_airdrop_lamports_enabled,
is_enforcing_access_permissions: enforce_access_permissions,
};

this.processor.fill_missing_sysvar_cache_entries(&this);
Expand Down
3 changes: 3 additions & 0 deletions magicblock-processor/src/executor/processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,9 @@ impl super::TransactionExecutor {
}

fn verify_account_states(&self, processed: &mut ProcessedTransaction) {
if !self.is_enforcing_access_permissions {
return;
}
let ProcessedTransaction::Executed(executed) = processed else {
return;
};
Expand Down
7 changes: 6 additions & 1 deletion magicblock-processor/src/scheduler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ impl TransactionScheduler {
/// 1. Prepares the shared program cache and ensures necessary sysvars are in the `AccountsDb`.
/// 2. Creates a pool of `TransactionExecutor` workers, each with its own dedicated channel.
/// 3. Spawns each worker in its own OS thread for maximum isolation and performance.
pub fn new(executors: u32, state: TransactionSchedulerState) -> Self {
pub fn new(
executors: u32,
state: TransactionSchedulerState,
enforce_access_permissions: bool,
) -> Self {
let count = executors.clamp(1, MAX_SVM_EXECUTORS) as usize;
let mut executors = Vec::with_capacity(count);

Expand All @@ -86,6 +90,7 @@ impl TransactionScheduler {
transactions_rx,
ready_tx.clone(),
program_cache.clone(),
enforce_access_permissions,
);
executor.populate_builtins();
executor.spawn();
Expand Down
Loading
Loading