Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions Cargo.lock

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

6 changes: 4 additions & 2 deletions magicblock-account-cloner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -102,7 +104,7 @@ impl ChainlinkCloner {

async fn send_tx(&self, tx: Transaction) -> ClonerResult<Signature> {
let sig = tx.signatures[0];
self.tx_scheduler.execute(tx).await?;
self.tx_scheduler.execute(with_encoded(tx)?).await?;
Ok(sig)
}

Expand Down
14 changes: 9 additions & 5 deletions magicblock-accounts/src/scheduled_commits_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
}
Expand Down
21 changes: 14 additions & 7 deletions magicblock-aperture/src/requests/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<SanitizedTransaction>` 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<SanitizedTransaction> {
let decoded = match encoding {
) -> RpcResult<WithEncoded<SanitizedTransaction>> {
let encoded = match encoding {
UiTransactionEncoding::Base58 => {
bs58::decode(txn).into_vec().map_err(RpcError::parse_error)
}
Expand All @@ -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
Expand All @@ -205,11 +211,12 @@ 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`.
#[instrument(skip(self, transaction), fields(signature = %transaction.signature()))]
#[instrument(skip_all)]
async fn ensure_transaction_accounts(
&self,
transaction: &SanitizedTransaction,
Expand All @@ -224,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) => {
Expand Down
12 changes: 7 additions & 5 deletions magicblock-aperture/src/requests/http/request_airdrop.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use magicblock_core::link::transactions::SanitizeableTransaction;
use magicblock_core::link::transactions::with_encoded;

use super::prelude::*;

Expand Down Expand Up @@ -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());
Comment on lines +38 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

unwrap_or_default() silently returns an all-zero signature on invariant violation

If txn.signatures is unexpectedly empty, SerdeSignature(Signature::default()) (all zeros) is returned as a success response. The client receives a zero signature it can never look up, masking the failure entirely. Since the invariant can be verified, prefer a hard error:

🛡️ Proposed fix
-        // we just signed the transaction, it must have a signature
-        let signature =
-            SerdeSignature(txn.signatures.first().cloned().unwrap_or_default());
+        let signature = SerdeSignature(
+            txn.signatures
+                .first()
+                .cloned()
+                .ok_or_else(|| RpcError::internal("airdrop transaction has no signature"))?,
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// we just signed the transaction, it must have a signature
let signature =
SerdeSignature(txn.signatures.first().cloned().unwrap_or_default());
let signature = SerdeSignature(
txn.signatures
.first()
.cloned()
.ok_or_else(|| RpcError::internal("airdrop transaction has no signature"))?,
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@magicblock-aperture/src/requests/http/request_airdrop.rs` around lines 38 -
40, The code currently constructs SerdeSignature from
txn.signatures.first().cloned().unwrap_or_default(), which silently returns an
all-zero signature if the invariant is violated; change this to fail hard and
surface the error instead: in the scope that builds the signature (referencing
SerdeSignature and txn.signatures), replace unwrap_or_default() with an explicit
check or expect() that returns an Err (or propagates with ?/bail!) when
txn.signatures.first() is None so the handler returns a clear error rather than
a zero signature.


self.transactions_scheduler.execute(txn).await?;
self.transactions_scheduler
.execute(with_encoded(txn)?)
.await?;

Ok(ResponsePayload::encode_no_context(&request.id, signature))
}
Expand Down
12 changes: 4 additions & 8 deletions magicblock-aperture/src/requests/http/send_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
}

Expand Down
4 changes: 2 additions & 2 deletions magicblock-aperture/src/requests/http/simulate_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)?;

Expand Down
8 changes: 7 additions & 1 deletion magicblock-api/src/tickers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -89,6 +90,11 @@ async fn handle_scheduled_commits<C: ScheduledCommitsProcessor>(
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;
};
Comment on lines +93 to +97
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Comment incorrectly attributes infallibility to transaction size

bincode::serialize on a Transaction does not fail due to a 64 KB limit — Solana transactions are capped at ~1232 bytes by the protocol, far below that. The correct invariant is that Transaction is always serializable by construction (all fields implement Serialize). Same comment appears verbatim in magicblock-accounts/src/scheduled_commits_processor.rs.

📝 Suggested comment correction
-        // Unreachable case, all schedule commit txns are smaller than 64KB by construction
+        // Unreachable case: Transaction always implements Serialize correctly by construction
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
};
let Ok(tx) = with_encoded(tx) else {
// Unreachable case: Transaction always implements Serialize correctly by construction
error!("Failed to bincode intent transaction");
return;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@magicblock-api/src/tickers.rs` around lines 93 - 97, The comment incorrectly
attributes infallibility of bincode::serialize to a 64KB size guarantee; instead
update the comment and log to state that serialization should not fail because
the Transaction type (and all its fields) implements Serialize, not because of
any size limit. Locate the with_encoded(tx) call and its surrounding
comment/error (the block that currently says "Unreachable case, all schedule
commit txns are smaller than 64KB by construction" and the error!("Failed to
bincode intent transaction")), replace the explanatory text to mention the
Serialize invariant (or remove the unreachable claim) and adjust the log message
to reflect an unexpected serialization failure rather than a size-related one;
apply the same fix in the analogous spot in scheduled_commits_processor.rs.

if let Err(err) = transaction_scheduler.execute(tx).await {
error!(error = ?err, "Failed to accept scheduled commits");
return;
Expand Down
2 changes: 2 additions & 0 deletions magicblock-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
71 changes: 67 additions & 4 deletions magicblock-core/src/link/transactions.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down Expand Up @@ -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<Vec<u8>>,
}

/// An enum that specifies how a transaction should be processed by the scheduler.
Expand Down Expand Up @@ -115,6 +119,57 @@ pub trait SanitizeableTransaction {
self,
verify: bool,
) -> Result<SanitizedTransaction, TransactionError>;

/// 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<Vec<u8>>), 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<T> {
pub txn: T,
pub encoded: Vec<u8>,
}

impl<T: SanitizeableTransaction> SanitizeableTransaction for WithEncoded<T> {
fn sanitize(
self,
verify: bool,
) -> Result<SanitizedTransaction, TransactionError> {
self.txn.sanitize(verify)
}

fn sanitize_with_encoded(
self,
verify: bool,
) -> Result<(SanitizedTransaction, Option<Vec<u8>>), 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<T>(txn: T) -> Result<WithEncoded<T>, TransactionError>
where
T: Serialize,
{
let encoded = bincode::serialize(&txn)
.map_err(|_| TransactionError::SanitizeFailure)?;
Ok(WithEncoded { txn, encoded })
Comment on lines +169 to +172
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

TransactionError::SanitizeFailure is misleading for a serialization error.

A bincode serialization failure in with_encoded has nothing to do with transaction sanitization. If TransactionError has no better variant (e.g., InvalidTransaction or SerializationError), at minimum add an inline comment explaining the substitution so future readers understand the intent.

     let encoded = bincode::serialize(&txn)
-        .map_err(|_| TransactionError::SanitizeFailure)?;
+        // No specific serialization error variant in TransactionError; SanitizeFailure is used as the closest proxy.
+        .map_err(|_| TransactionError::SanitizeFailure)?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
let encoded = bincode::serialize(&txn)
.map_err(|_| TransactionError::SanitizeFailure)?;
Ok(WithEncoded { txn, encoded })
{
let encoded = bincode::serialize(&txn)
// No specific serialization error variant in TransactionError; SanitizeFailure is used as the closest proxy.
.map_err(|_| TransactionError::SanitizeFailure)?;
Ok(WithEncoded { txn, encoded })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@magicblock-core/src/link/transactions.rs` around lines 169 - 172, The bincode
serialization failure in with_encoded is incorrectly mapped to
TransactionError::SanitizeFailure; update the error handling to either
(preferred) add a new enum variant like TransactionError::SerializationError (or
TransactionError::InvalidTransaction) in the TransactionError definition and map
bincode::serialize errors to that variant, updating all match/uses accordingly,
or (if adding a variant is not possible now) add a clear inline comment above
the map_err call explaining that serialization failures are being mapped to
SanitizeFailure as a deliberate substitution and why; reference the with_encoded
function and the WithEncoded result wrapper when making the change.

}
Comment on lines +164 to 173
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all call sites of with_encoded across the repo
rg -n --type rust '\bwith_encoded\s*\(' -A 2 -B 2

Repository: magicblock-labs/magicblock-validator

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Broader search for with_encoded without strict pattern matching
rg 'with_encoded' --type rust -i

Repository: magicblock-labs/magicblock-validator

Length of output: 561


🏁 Script executed:

#!/bin/bash
# Search for direct calls to with_encoded function, excluding method references
rg '::with_encoded\s*\(|with_encoded\s*\(' --type rust -B 2 -A 2

Repository: magicblock-labs/magicblock-validator

Length of output: 1670


🏁 Script executed:

#!/bin/bash
# Search for with_encoded in test files and check module exports
rg 'with_encoded' --type rust -C 3 | grep -E '(test|pub use|mod)'
# Also explicitly search test files
fd '\.rs$' | xargs grep -l 'with_encoded' | grep -i test

Repository: magicblock-labs/magicblock-validator

Length of output: 361


🏁 Script executed:

#!/bin/bash
# Final verification - simple search for function calls
rg 'with_encoded\s*\(' --type rust
# Also check if it's referenced in documentation or comments that might indicate intended usage
rg -i 'with_encoded' --type rust | grep -v 'sanitize_with_encoded' | grep -v 'pub fn with_encoded'

Repository: magicblock-labs/magicblock-validator

Length of output: 445


Remove the unused with_encoded function.

The function has no call sites in the codebase and is dead code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@magicblock-core/src/link/transactions.rs` around lines 164 - 173, Remove the
dead helper function by deleting the with_encoded<T> function (the block that
calls bincode::serialize, maps errors to TransactionError::SanitizeFailure, and
returns WithEncoded { txn, encoded }); also remove any now-unused imports or
references introduced solely for that function (e.g., bincode::serialize,
Serialize bound, and WithEncoded if it is only used by this helper) so there are
no lingering unused symbols.


impl SanitizeableTransaction for SanitizedTransaction {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -208,10 +267,14 @@ impl TransactionSchedulerHandle {
txn: impl SanitizeableTransaction,
mode: fn(oneshot::Sender<R>) -> TransactionProcessingMode,
) -> Result<R, TransactionError> {
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
Expand Down
1 change: 1 addition & 0 deletions magicblock-processor/src/scheduler/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}

Expand Down