From 0168241e5ff540c6dddcacb49a5934541f20bffb Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Fri, 20 Feb 2026 20:50:47 +0400 Subject: [PATCH 1/2] feat: forward original bincoded transaction keep original bincoded transaction along with the sanitized version, this can will be used by future ledger and replicator implementations --- Cargo.lock | 2 + magicblock-aperture/src/requests/http/mod.rs | 17 +++-- .../src/requests/http/send_transaction.rs | 12 ++-- .../src/requests/http/simulate_transaction.rs | 2 +- magicblock-core/Cargo.toml | 2 + magicblock-core/src/link/transactions.rs | 71 +++++++++++++++++-- magicblock-processor/src/scheduler/tests.rs | 1 + 7 files changed, 89 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f20f34d93..5fe079ccc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3242,8 +3242,10 @@ dependencies = [ name = "magicblock-core" version = "0.8.0" dependencies = [ + "bincode", "flume", "magicblock-magic-program-api", + "serde", "solana-account", "solana-account-decoder", "solana-hash", diff --git a/magicblock-aperture/src/requests/http/mod.rs b/magicblock-aperture/src/requests/http/mod.rs index 9314462d5..58b350b10 100644 --- a/magicblock-aperture/src/requests/http/mod.rs +++ b/magicblock-aperture/src/requests/http/mod.rs @@ -8,7 +8,9 @@ use hyper::{ Request, Response, }; use magicblock_accounts_db::traits::AccountsBank; -use magicblock_core::link::transactions::SanitizeableTransaction; +use magicblock_core::link::transactions::{ + SanitizeableTransaction, WithEncoded, +}; use magicblock_metrics::metrics::{AccountFetchOrigin, ENSURE_ACCOUNTS_TIME}; use prelude::JsonBody; use solana_account::AccountSharedData; @@ -170,14 +172,18 @@ impl HttpDispatcher { /// 3. Validates the transaction's `recent_blockhash` against the ledger, optionally /// replacing it with the latest one. /// 4. Sanitizes the transaction, which includes verifying signatures unless disabled. + /// + /// Returns `WithEncoded` with the original wire bytes. + /// For execution (replace_blockhash=false), bytes are preserved for replication. + /// For simulation (replace_blockhash=true), bytes are unused. fn prepare_transaction( &self, txn: &str, encoding: UiTransactionEncoding, sigverify: bool, replace_blockhash: bool, - ) -> RpcResult { - let decoded = match encoding { + ) -> RpcResult> { + let encoded = match encoding { UiTransactionEncoding::Base58 => { bs58::decode(txn).into_vec().map_err(RpcError::parse_error) } @@ -190,7 +196,7 @@ impl HttpDispatcher { }?; let mut transaction: VersionedTransaction = - bincode::deserialize(&decoded).map_err(RpcError::invalid_params)?; + bincode::deserialize(&encoded).map_err(RpcError::invalid_params)?; if replace_blockhash { transaction @@ -205,7 +211,8 @@ impl HttpDispatcher { }; } - Ok(transaction.sanitize(sigverify)?) + let txn = transaction.sanitize(sigverify)?; + Ok(WithEncoded { txn, encoded }) } /// Ensures all accounts required for a transaction are present in the `AccountsDb`. diff --git a/magicblock-aperture/src/requests/http/send_transaction.rs b/magicblock-aperture/src/requests/http/send_transaction.rs index 5e2c9ef8d..f44553182 100644 --- a/magicblock-aperture/src/requests/http/send_transaction.rs +++ b/magicblock-aperture/src/requests/http/send_transaction.rs @@ -32,27 +32,23 @@ impl HttpDispatcher { .inspect_err( |err| debug!(error = ?err, "Failed to prepare transaction"), )?; - let signature = *transaction.signature(); - tracing::Span::current() - .record("signature", tracing::field::display(signature)); + let signature = *transaction.txn.signature(); - // Perform a replay check and reserve the signature in the cache. This prevents - // a transaction from being processed twice within the blockhash validity period. + // Perform a replay check and reserve the signature in the cache if self.transactions.contains(&signature) || !self.transactions.push(signature, None) { return Err(TransactionError::AlreadyProcessed.into()); } - self.ensure_transaction_accounts(&transaction).await?; + + self.ensure_transaction_accounts(&transaction.txn).await?; // Based on the preflight flag, either execute and await the result, // or schedule (fire-and-forget) for background processing. if config.skip_preflight { TRANSACTION_SKIP_PREFLIGHT.inc(); self.transactions_scheduler.schedule(transaction).await?; - trace!("Transaction scheduled"); } else { - trace!("Transaction executing"); self.transactions_scheduler.execute(transaction).await?; } diff --git a/magicblock-aperture/src/requests/http/simulate_transaction.rs b/magicblock-aperture/src/requests/http/simulate_transaction.rs index 281368d2a..22a606725 100644 --- a/magicblock-aperture/src/requests/http/simulate_transaction.rs +++ b/magicblock-aperture/src/requests/http/simulate_transaction.rs @@ -43,7 +43,7 @@ impl HttpDispatcher { .inspect_err(|err| { debug!(error = ?err, "Failed to prepare transaction to simulate") })?; - self.ensure_transaction_accounts(&transaction).await?; + self.ensure_transaction_accounts(&transaction.txn).await?; let replacement_blockhash = config .replace_recent_blockhash diff --git a/magicblock-core/Cargo.toml b/magicblock-core/Cargo.toml index 308b441a1..2347615f3 100644 --- a/magicblock-core/Cargo.toml +++ b/magicblock-core/Cargo.toml @@ -11,6 +11,8 @@ edition.workspace = true tokio = { workspace = true, features = ["sync"] } flume = { workspace = true } +bincode = { workspace = true } +serde = { workspace = true } solana-account = { workspace = true } solana-account-decoder = { workspace = true } diff --git a/magicblock-core/src/link/transactions.rs b/magicblock-core/src/link/transactions.rs index c00865047..e7cda92b8 100644 --- a/magicblock-core/src/link/transactions.rs +++ b/magicblock-core/src/link/transactions.rs @@ -1,5 +1,6 @@ use flume::{Receiver as MpmcReceiver, Sender as MpmcSender}; use magicblock_magic_program_api::args::TaskRequest; +use serde::Serialize; use solana_program::message::{ inner_instruction::InnerInstructionsList, SimpleAddressLoader, }; @@ -69,6 +70,9 @@ pub struct TransactionStatus { pub struct ProcessableTransaction { pub transaction: SanitizedTransaction, pub mode: TransactionProcessingMode, + /// Pre-encoded bincode bytes for the transaction. + /// Used by the replicator to avoid redundant serialization. + pub encoded: Option>, } /// An enum that specifies how a transaction should be processed by the scheduler. @@ -115,6 +119,57 @@ pub trait SanitizeableTransaction { self, verify: bool, ) -> Result; + + /// Sanitizes the transaction and optionally provides pre-encoded bincode bytes. + /// + /// Default implementation delegates to `sanitize()` and returns `None` for encoded bytes. + /// Override this method when you have pre-encoded bytes (e.g., from the wire) to avoid + /// redundant serialization. + fn sanitize_with_encoded( + self, + verify: bool, + ) -> Result<(SanitizedTransaction, Option>), TransactionError> + where + Self: Sized, + { + let txn = self.sanitize(verify)?; + Ok((txn, None)) + } +} + +/// Wraps a transaction with its pre-encoded bincode representation. +/// Use for internally-constructed transactions that need encoded bytes. +pub struct WithEncoded { + pub txn: T, + pub encoded: Vec, +} + +impl SanitizeableTransaction for WithEncoded { + fn sanitize( + self, + verify: bool, + ) -> Result { + self.txn.sanitize(verify) + } + + fn sanitize_with_encoded( + self, + verify: bool, + ) -> Result<(SanitizedTransaction, Option>), TransactionError> { + let txn = self.txn.sanitize(verify)?; + Ok((txn, Some(self.encoded))) + } +} + +/// Encodes a transaction to bincode and wraps it with its encoded form. +/// Use for internally-constructed transactions that need the encoded bytes. +pub fn with_encoded(txn: T) -> Result, TransactionError> +where + T: Serialize, +{ + let encoded = bincode::serialize(&txn) + .map_err(|_| TransactionError::SanitizeFailure)?; + Ok(WithEncoded { txn, encoded }) } impl SanitizeableTransaction for SanitizedTransaction { @@ -162,9 +217,13 @@ impl TransactionSchedulerHandle { &self, txn: impl SanitizeableTransaction, ) -> TransactionResult { - let transaction = txn.sanitize(true)?; + let (transaction, encoded) = txn.sanitize_with_encoded(true)?; let mode = TransactionProcessingMode::Execution(None); - let txn = ProcessableTransaction { transaction, mode }; + let txn = ProcessableTransaction { + transaction, + mode, + encoded, + }; let r = self.0.send(txn).await; r.map_err(|_| TransactionError::ClusterMaintenance) } @@ -208,10 +267,14 @@ impl TransactionSchedulerHandle { txn: impl SanitizeableTransaction, mode: fn(oneshot::Sender) -> TransactionProcessingMode, ) -> Result { - let transaction = txn.sanitize(true)?; + let (transaction, encoded) = txn.sanitize_with_encoded(true)?; let (tx, rx) = oneshot::channel(); let mode = mode(tx); - let txn = ProcessableTransaction { transaction, mode }; + let txn = ProcessableTransaction { + transaction, + mode, + encoded, + }; self.0 .send(txn) .await diff --git a/magicblock-processor/src/scheduler/tests.rs b/magicblock-processor/src/scheduler/tests.rs index 89db00c1e..9aa5c7cd3 100644 --- a/magicblock-processor/src/scheduler/tests.rs +++ b/magicblock-processor/src/scheduler/tests.rs @@ -49,6 +49,7 @@ fn mock_txn(accounts: &[(Pubkey, bool)]) -> TransactionWithId { TransactionWithId::new(ProcessableTransaction { transaction: transaction.sanitize(false).unwrap(), mode: TransactionProcessingMode::Execution(None), + encoded: None, }) } From 9edc6c4b0cb0a5e47c503285551f310534287d6c Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Mon, 23 Feb 2026 21:21:58 +0400 Subject: [PATCH 2/2] fix: encode internal transactions --- magicblock-account-cloner/src/lib.rs | 6 ++++-- .../src/scheduled_commits_processor.rs | 14 +++++++++----- magicblock-aperture/src/requests/http/mod.rs | 4 ++-- .../src/requests/http/request_airdrop.rs | 12 +++++++----- .../src/requests/http/simulate_transaction.rs | 2 +- magicblock-api/src/tickers.rs | 8 +++++++- 6 files changed, 30 insertions(+), 16 deletions(-) diff --git a/magicblock-account-cloner/src/lib.rs b/magicblock-account-cloner/src/lib.rs index f722619e5..7814ab0a9 100644 --- a/magicblock-account-cloner/src/lib.rs +++ b/magicblock-account-cloner/src/lib.rs @@ -40,7 +40,9 @@ use magicblock_chainlink::{ }; use magicblock_committor_service::{BaseIntentCommittor, CommittorService}; use magicblock_config::config::ChainLinkConfig; -use magicblock_core::link::transactions::TransactionSchedulerHandle; +use magicblock_core::link::transactions::{ + with_encoded, TransactionSchedulerHandle, +}; use magicblock_ledger::LatestBlock; use magicblock_magic_program_api::{ args::ScheduleTaskArgs, @@ -102,7 +104,7 @@ impl ChainlinkCloner { async fn send_tx(&self, tx: Transaction) -> ClonerResult { let sig = tx.signatures[0]; - self.tx_scheduler.execute(tx).await?; + self.tx_scheduler.execute(with_encoded(tx)?).await?; Ok(sig) } diff --git a/magicblock-accounts/src/scheduled_commits_processor.rs b/magicblock-accounts/src/scheduled_commits_processor.rs index 88c4ed4ed..e79d187c5 100644 --- a/magicblock-accounts/src/scheduled_commits_processor.rs +++ b/magicblock-accounts/src/scheduled_commits_processor.rs @@ -18,7 +18,9 @@ use magicblock_committor_service::{ intent_execution_manager::BroadcastedIntentExecutionResult, intent_executor::ExecutionOutput, BaseIntentCommittor, CommittorService, }; -use magicblock_core::link::transactions::TransactionSchedulerHandle; +use magicblock_core::link::transactions::{ + with_encoded, TransactionSchedulerHandle, +}; use magicblock_program::{ magic_scheduled_base_intent::ScheduledIntentBundle, register_scheduled_commit_sent, SentCommit, TransactionScheduler, @@ -199,10 +201,12 @@ impl ScheduledCommitsProcessorImpl { let sent_commit = Self::build_sent_commit(intent_id, intent_meta, result); register_scheduled_commit_sent(sent_commit); - match internal_transaction_scheduler - .execute(intent_sent_transaction) - .await - { + let Ok(txn) = with_encoded(intent_sent_transaction) else { + // Unreachable case, all intent transactions are smaller than 64KB by construction + error!("Failed to bincode intent transaction"); + return; + }; + match internal_transaction_scheduler.execute(txn).await { Ok(()) => { debug!("Sent commit signaled") } diff --git a/magicblock-aperture/src/requests/http/mod.rs b/magicblock-aperture/src/requests/http/mod.rs index 58b350b10..79c1befba 100644 --- a/magicblock-aperture/src/requests/http/mod.rs +++ b/magicblock-aperture/src/requests/http/mod.rs @@ -216,7 +216,7 @@ impl HttpDispatcher { } /// Ensures all accounts required for a transaction are present in the `AccountsDb`. - #[instrument(skip(self, transaction), fields(signature = %transaction.signature()))] + #[instrument(skip_all)] async fn ensure_transaction_accounts( &self, transaction: &SanitizedTransaction, @@ -231,7 +231,7 @@ impl HttpDispatcher { { Ok(res) if res.is_ok() => Ok(()), Ok(res) => { - debug!(result = %res, "Transaction account resolution encountered issues"); + debug!(%res, "Transaction account resolution encountered issues"); Ok(()) } Err(err) => { diff --git a/magicblock-aperture/src/requests/http/request_airdrop.rs b/magicblock-aperture/src/requests/http/request_airdrop.rs index 11c827629..0618cf12e 100644 --- a/magicblock-aperture/src/requests/http/request_airdrop.rs +++ b/magicblock-aperture/src/requests/http/request_airdrop.rs @@ -1,4 +1,4 @@ -use magicblock_core::link::transactions::SanitizeableTransaction; +use magicblock_core::link::transactions::with_encoded; use super::prelude::*; @@ -35,11 +35,13 @@ impl HttpDispatcher { lamports, self.blocks.get_latest().hash, ); - // we don't need to verify transaction that we just signed - let txn = txn.sanitize(false)?; - let signature = SerdeSignature(*txn.signature()); + // we just signed the transaction, it must have a signature + let signature = + SerdeSignature(txn.signatures.first().cloned().unwrap_or_default()); - self.transactions_scheduler.execute(txn).await?; + self.transactions_scheduler + .execute(with_encoded(txn)?) + .await?; Ok(ResponsePayload::encode_no_context(&request.id, signature)) } diff --git a/magicblock-aperture/src/requests/http/simulate_transaction.rs b/magicblock-aperture/src/requests/http/simulate_transaction.rs index 22a606725..46c822bef 100644 --- a/magicblock-aperture/src/requests/http/simulate_transaction.rs +++ b/magicblock-aperture/src/requests/http/simulate_transaction.rs @@ -52,7 +52,7 @@ impl HttpDispatcher { // Submit the transaction to the scheduler for simulation. let result = self .transactions_scheduler - .simulate(transaction) + .simulate(transaction.txn) .await .map_err(RpcError::transaction_simulation)?; diff --git a/magicblock-api/src/tickers.rs b/magicblock-api/src/tickers.rs index 10e964ee2..7ea151f8b 100644 --- a/magicblock-api/src/tickers.rs +++ b/magicblock-api/src/tickers.rs @@ -9,7 +9,8 @@ use std::{ use magicblock_accounts::ScheduledCommitsProcessor; use magicblock_accounts_db::{traits::AccountsBank, AccountsDb}; use magicblock_core::link::{ - blocks::BlockUpdateTx, transactions::TransactionSchedulerHandle, + blocks::BlockUpdateTx, + transactions::{with_encoded, TransactionSchedulerHandle}, }; use magicblock_ledger::{LatestBlock, Ledger}; use magicblock_magic_program_api as magic_program; @@ -89,6 +90,11 @@ async fn handle_scheduled_commits( let tx = InstructionUtils::accept_scheduled_commits( latest_block.load().blockhash, ); + let Ok(tx) = with_encoded(tx) else { + // Unreachable case, all schedule commit txns are smaller than 64KB by construction + error!("Failed to bincode intent transaction"); + return; + }; if let Err(err) = transaction_scheduler.execute(tx).await { error!(error = ?err, "Failed to accept scheduled commits"); return;