Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f7b1139
fix(replicator): disable automatic failover
bmuddha May 20, 2026
e054070
fix: address review comments
bmuddha May 21, 2026
34e2ee7
feat(chainlink): add mode-aware wrapper
thlorenz May 22, 2026
68dcb88
feat(chainlink): use mode-aware wrapper aliases
thlorenz May 22, 2026
11535ac
feat(chainlink): disable chainlink for replicas
thlorenz May 22, 2026
5966d1e
feat(chainlink): gate aperture rpc by mode
thlorenz May 22, 2026
2f2ccf2
test(chainlink): cover mode-aware rpc gating
thlorenz May 22, 2026
e85a398
chore: verify chainlink mode invariants
thlorenz May 22, 2026
49411db
chore: minor fixes
thlorenz May 22, 2026
6bafea5
chore: dedupe chainlink aliases
thlorenz May 22, 2026
5b8df27
feat: add accounts bank resetter
thlorenz May 22, 2026
aa00b77
refactor: wire replication resetter
thlorenz May 22, 2026
7424b4f
fix: remove disabled chainlink stubs
thlorenz May 22, 2026
a2716de
fix: restore disabled chainlink reset
thlorenz May 22, 2026
1163b5f
chore: generic resetter plumbing
thlorenz May 22, 2026
e8e2f4b
chore: rename inner chainlink
thlorenz May 22, 2026
3415ab0
chore: rename mode-aware chainlink
thlorenz May 22, 2026
17e2445
chore: rename real chainlink alias
thlorenz May 22, 2026
8facc01
chore: rename prod chainlink alias
thlorenz May 22, 2026
74d2860
chore: rename inner chainlink impl
thlorenz May 22, 2026
90188a1
chore: rename test chainlink alias
thlorenz May 22, 2026
9ba4d74
chore: fix ix test build issues
thlorenz May 22, 2026
08b0408
feat: forward fetch cloner
thlorenz May 22, 2026
dab5d50
feat: use mode-aware test chainlink
thlorenz May 22, 2026
6853331
chore: increase the snapshot bytes limit
bmuddha May 22, 2026
5c593b1
Merge branch 'bmuddha/fix/disable-auto-failover' into thlorenz/replic…
thlorenz May 25, 2026
f572b1e
Merge branch 'master' into thlorenz/replica-chainlink-simple
thlorenz May 27, 2026
ee6b83d
Merge branch 'master' into thlorenz/replica-chainlink-simple
thlorenz May 27, 2026
4d4ea6b
Merge branch 'master' into thlorenz/replica-chainlink-simple
thlorenz May 28, 2026
1838a2f
chore: remove replicator resetter hook
thlorenz May 28, 2026
4d87c3d
chore: remove chainlink resetter trait
thlorenz May 28, 2026
803b867
refactor: extract transaction validation
thlorenz May 28, 2026
a9c7c7a
test: cover v0 lookup table rejection
thlorenz May 28, 2026
9a48cc3
test: cover transaction primary mode
thlorenz May 28, 2026
6376c8c
test: remove obsolete http unit tests
thlorenz May 28, 2026
3ace75b
chore: update cargo lock
thlorenz May 28, 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
19 changes: 4 additions & 15 deletions magicblock-accounts/src/scheduled_commits_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,7 @@ use std::{

use async_trait::async_trait;
use magicblock_account_cloner::ChainlinkCloner;
use magicblock_accounts_db::AccountsDb;
use magicblock_chainlink::{
remote_account_provider::{
chain_rpc_client::ChainRpcClientImpl,
chain_updates_client::ChainUpdatesClient,
},
submux::SubMuxClient,
Chainlink,
};
use magicblock_chainlink::{ProdChainlink, ProdInnerChainlink};
use magicblock_committor_service::{
intent_execution_manager::BroadcastedIntentExecutionResult,
intent_executor::ExecutionOutput, BaseIntentCommittor, CommittorService,
Expand Down Expand Up @@ -43,12 +35,9 @@ use crate::{
const POISONED_MUTEX_MSG: &str =
"Mutex of RemoteScheduledCommitsProcessor.intents_meta_map is poisoned";

pub type ChainlinkImpl = Chainlink<
ChainRpcClientImpl,
SubMuxClient<ChainUpdatesClient>,
AccountsDb,
ChainlinkCloner,
>;
pub type InnerChainlinkImpl = ProdInnerChainlink<ChainlinkCloner>;

pub type ChainlinkImpl = ProdChainlink<ChainlinkCloner>;

pub struct ScheduledCommitsProcessorImpl {
committor: Arc<CommittorService>,
Expand Down
66 changes: 35 additions & 31 deletions magicblock-aperture/src/requests/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ use hyper::{
Request, Response,
};
use magicblock_accounts_db::traits::AccountsBank;
use magicblock_core::link::transactions::{
SanitizeableTransaction, WithEncoded,
use magicblock_core::{
coordination_mode::CoordinationMode,
link::transactions::{SanitizeableTransaction, WithEncoded},
};
use magicblock_metrics::metrics::{AccountFetchOrigin, ENSURE_ACCOUNTS_TIME};
use prelude::JsonBody;
use solana_account::{AccountSharedData, ReadableAccount};
use solana_message::VersionedMessage;
use solana_pubkey::Pubkey;
use solana_transaction::{
sanitized::SanitizedTransaction, versioned::VersionedTransaction,
};
use solana_transaction_status::UiTransactionEncoding;
use tracing::*;
use transaction_validation::validate_supported_transaction_shape;

use super::RpcRequest;
use crate::{
Expand All @@ -29,10 +30,6 @@ use crate::{

pub(crate) type HandlerResult = RpcResult<Response<JsonBody>>;

// Solana's builtin-program filters in compute-budget processing assume program
// indices fit within a packet-bounded pubkey table (1232 / 32 = 38).
const MAX_RUNTIME_PROGRAM_ID_INDEX_EXCLUSIVE: usize =
1232 / size_of::<Pubkey>();
const SYSTEM_PROGRAM_ID: Pubkey =
Pubkey::from_str_const("11111111111111111111111111111111");

Expand Down Expand Up @@ -123,13 +120,34 @@ impl HttpDispatcher {
&& account.owner() == &SYSTEM_PROGRAM_ID
}

fn needs_onchain_interactions(&self) -> bool {
CoordinationMode::current().needs_onchain_interactions()
}

fn require_primary_rpc_method(
&self,
method: &'static str,
) -> RpcResult<()> {
if self.needs_onchain_interactions() {
Ok(())
} else {
Err(RpcError::transaction_verification(format!(
"{method} is only available while validator is primary"
)))
}
}

/// Fetches an account's data from the `AccountsDb` filling it in from chain
/// as needed.
#[instrument(skip_all)]
async fn read_account_with_ensure(
&self,
pubkey: &Pubkey,
) -> Option<AccountSharedData> {
if !self.needs_onchain_interactions() {
return self.accountsdb.get_account(pubkey);
}

let mark_empty_if_not_found = [*pubkey];
let _timer = ENSURE_ACCOUNTS_TIME
.with_label_values(&["account"])
Expand Down Expand Up @@ -158,6 +176,13 @@ impl HttpDispatcher {
&self,
pubkeys: &[Pubkey],
) -> Vec<Option<AccountSharedData>> {
if !self.needs_onchain_interactions() {
return pubkeys
.iter()
.map(|pubkey| self.accountsdb.get_account(pubkey))
.collect();
}

trace!("Ensuring accounts");
let _timer = ENSURE_ACCOUNTS_TIME
.with_label_values(&["multi-account"])
Expand Down Expand Up @@ -246,6 +271,8 @@ impl HttpDispatcher {
&self,
transaction: &SanitizedTransaction,
) -> RpcResult<()> {
self.require_primary_rpc_method("transaction")?;

let _timer = ENSURE_ACCOUNTS_TIME
.with_label_values(&["transaction"])
.start_timer();
Expand All @@ -271,30 +298,6 @@ impl HttpDispatcher {
}
}

fn validate_supported_transaction_shape(
transaction: &VersionedTransaction,
) -> RpcResult<()> {
if let VersionedMessage::V0(message) = &transaction.message {
if !message.address_table_lookups.is_empty() {
return Err(RpcError::transaction_verification(
"v0 transactions with address lookup tables are not supported",
));
}
}

for instruction in transaction.message.instructions() {
let program_id_index = usize::from(instruction.program_id_index);
if program_id_index >= MAX_RUNTIME_PROGRAM_ID_INDEX_EXCLUSIVE {
return Err(RpcError::transaction_verification(format!(
"unsupported program id index {program_id_index}; max supported is {}",
MAX_RUNTIME_PROGRAM_ID_INDEX_EXCLUSIVE - 1
)));
}
}

Ok(())
}

/// A prelude module to provide common imports for all RPC handler modules.
mod prelude {
pub(super) use magicblock_core::{link::accounts::LockedAccount, Slot};
Expand Down Expand Up @@ -358,6 +361,7 @@ pub(crate) mod mocked;
pub(crate) mod request_airdrop;
pub(crate) mod send_transaction;
pub(crate) mod simulate_transaction;
mod transaction_validation;

// Magic Router compatibility methods.
pub(crate) mod get_delegation_status;
1 change: 1 addition & 0 deletions magicblock-aperture/src/requests/http/send_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ impl HttpDispatcher {
&self,
request: &mut JsonRequest,
) -> HandlerResult {
self.require_primary_rpc_method("sendTransaction")?;
let _timer = TRANSACTION_PROCESSING_TIME.start_timer();
let (transaction_str, config) =
parse_params!(request.params()?, String, RpcSendTransactionConfig);
Expand Down
2 changes: 2 additions & 0 deletions magicblock-aperture/src/requests/http/simulate_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ impl HttpDispatcher {
&self,
request: &mut JsonRequest,
) -> HandlerResult {
self.require_primary_rpc_method("simulateTransaction")?;

let (transaction_str, config) = parse_params!(
request.params()?,
String,
Expand Down
151 changes: 151 additions & 0 deletions magicblock-aperture/src/requests/http/transaction_validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use std::mem::size_of;

use solana_message::VersionedMessage;
use solana_pubkey::Pubkey;
use solana_transaction::versioned::VersionedTransaction;

use crate::{error::RpcError, RpcResult};

// Solana's builtin-program filters in compute-budget processing assume program
// indices fit within a packet-bounded pubkey table (1232 / 32 = 38).
const MAX_RUNTIME_PROGRAM_ID_INDEX_EXCLUSIVE: usize =
1232 / size_of::<Pubkey>();

pub(super) fn validate_supported_transaction_shape(
transaction: &VersionedTransaction,
) -> RpcResult<()> {
if let VersionedMessage::V0(message) = &transaction.message {
if !message.address_table_lookups.is_empty() {
return Err(RpcError::transaction_verification(
"v0 transactions with address lookup tables are not supported",
));
}
}

for instruction in transaction.message.instructions() {
let program_id_index = usize::from(instruction.program_id_index);
if program_id_index >= MAX_RUNTIME_PROGRAM_ID_INDEX_EXCLUSIVE {
return Err(RpcError::transaction_verification(format!(
"unsupported program id index {program_id_index}; max supported is {}",
MAX_RUNTIME_PROGRAM_ID_INDEX_EXCLUSIVE - 1
)));
}
}

Ok(())
}

#[cfg(test)]
mod tests {
use magicblock_core::link::blocks::BlockHash;
use solana_message::{
compiled_instruction::CompiledInstruction,
legacy::Message,
v0::{Message as V0Message, MessageAddressTableLookup},
MessageHeader, VersionedMessage,
};
use solana_pubkey::Pubkey;
use solana_signature::Signature;
use solana_transaction::versioned::VersionedTransaction;

use super::validate_supported_transaction_shape;

const SYSTEM_PROGRAM_ID: Pubkey =
Pubkey::from_str_const("11111111111111111111111111111111");
const COMPUTE_BUDGET_ID: Pubkey =
Pubkey::from_str_const("ComputeBudget111111111111111111111111111111");

#[test]
fn accepts_program_id_index_within_runtime_limit() {
let transaction = VersionedTransaction {
signatures: vec![Signature::default()],
message: VersionedMessage::Legacy(Message {
header: MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 37,
},
account_keys: {
let mut keys = vec![SYSTEM_PROGRAM_ID];
keys.extend(std::iter::repeat_n(SYSTEM_PROGRAM_ID, 36));
keys.push(COMPUTE_BUDGET_ID);
keys
},
recent_blockhash: BlockHash::new_unique(),
instructions: vec![CompiledInstruction {
program_id_index: 37,
accounts: vec![],
data: vec![],
}],
}),
};

validate_supported_transaction_shape(&transaction).unwrap();
}

#[test]
fn rejects_program_id_index_outside_runtime_limit() {
let transaction = VersionedTransaction {
signatures: vec![Signature::default()],
message: VersionedMessage::Legacy(Message {
header: MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 38,
},
account_keys: {
let mut keys = vec![SYSTEM_PROGRAM_ID];
keys.extend(std::iter::repeat_n(SYSTEM_PROGRAM_ID, 37));
keys.push(COMPUTE_BUDGET_ID);
keys
},
recent_blockhash: BlockHash::new_unique(),
instructions: vec![CompiledInstruction {
program_id_index: 38,
accounts: vec![],
data: vec![],
}],
}),
};

let error =
validate_supported_transaction_shape(&transaction).unwrap_err();
assert!(
error
.to_string()
.contains("unsupported program id index 38"),
"unexpected error: {error}"
);
}

#[test]
fn rejects_v0_transactions_with_address_lookup_tables() {
let transaction = VersionedTransaction {
signatures: vec![Signature::default()],
message: VersionedMessage::V0(V0Message {
header: MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
},
account_keys: vec![SYSTEM_PROGRAM_ID],
recent_blockhash: BlockHash::new_unique(),
instructions: vec![],
address_table_lookups: vec![MessageAddressTableLookup {
account_key: Pubkey::new_unique(),
writable_indexes: vec![0],
readonly_indexes: vec![1],
}],
}),
};

let error =
validate_supported_transaction_shape(&transaction).unwrap_err();
assert!(
error.to_string().contains(
"v0 transactions with address lookup tables are not supported"
),
"unexpected error: {error}"
);
}
}
18 changes: 4 additions & 14 deletions magicblock-aperture/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,16 @@ use blocks::{BlocksCache, LastCachedBlock};
use cache::ExpiringCache;
use magicblock_account_cloner::ChainlinkCloner;
use magicblock_accounts_db::AccountsDb;
use magicblock_chainlink::{
remote_account_provider::{
chain_rpc_client::ChainRpcClientImpl,
chain_updates_client::ChainUpdatesClient,
},
submux::SubMuxClient,
Chainlink,
};
use magicblock_chainlink::{ProdChainlink, ProdInnerChainlink};
use magicblock_ledger::Ledger;
use solana_feature_set::FeatureSet;
use solana_pubkey::Pubkey;
use subscriptions::SubscriptionsDb;
use transactions::TransactionsCache;

pub type ChainlinkImpl = Chainlink<
ChainRpcClientImpl,
SubMuxClient<ChainUpdatesClient>,
AccountsDb,
ChainlinkCloner,
>;
pub type InnerChainlinkImpl = ProdInnerChainlink<ChainlinkCloner>;

pub type ChainlinkImpl = ProdChainlink<ChainlinkCloner>;

/// A container for the shared, global state of the RPC service.
///
Expand Down
12 changes: 9 additions & 3 deletions magicblock-aperture/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use tokio_util::sync::CancellationToken;
use crate::{
encoder::{AccountEncoder, ProgramAccountEncoder, TransactionLogsEncoder},
server::websocket::dispatch::WsConnectionChannel,
state::{ChainlinkImpl, SharedState},
state::{ChainlinkImpl, InnerChainlinkImpl, SharedState},
utils::ProgramFilters,
EventProcessor,
};
Expand All @@ -42,8 +42,14 @@ fn ws_channel() -> (WsConnectionChannel, Receiver<Bytes>) {

fn chainlink(accounts_db: &Arc<AccountsDb>) -> ChainlinkImpl {
let cfg = ChainLinkConfig::default();
ChainlinkImpl::try_new(accounts_db, None, Pubkey::new_unique(), &cfg)
.expect("Failed to create Chainlink")
let real = InnerChainlinkImpl::try_new(
accounts_db,
None,
Pubkey::new_unique(),
&cfg,
)
.expect("Failed to create Chainlink");
ChainlinkImpl::enabled(real)
}

mod event_processor {
Expand Down
Loading
Loading