From 8cc8e743f877bbdec894811915705fe58755fcc4 Mon Sep 17 00:00:00 2001 From: Babur Makhmudov Date: Thu, 20 Aug 2026 18:21:36 +0400 Subject: [PATCH] fix: skip signature verification during local replay --- engine/src/accessor.rs | 15 +++++++++++++-- engine/src/lib.rs | 8 +++++++- engine/src/transaction.rs | 10 +++------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/engine/src/accessor.rs b/engine/src/accessor.rs index 87116c34..2723bf43 100644 --- a/engine/src/accessor.rs +++ b/engine/src/accessor.rs @@ -11,7 +11,11 @@ use solana_pubkey::Pubkey; use solana_transaction::TransactionResult; use tokio::time; -use crate::{Engine, error::EngineError, error::Result, transaction}; +use crate::{ + Engine, IntoTransactionView, + error::{EngineError, Result}, + transaction, +}; /// Upper bound on awaiting a submitted transaction's committed result. const EXECUTION_TIMEOUT: Duration = Duration::from_secs(8); @@ -63,7 +67,14 @@ impl AccountAccessor<'_> { } } -impl TransactionAccessor<'_> { +impl<'a> TransactionAccessor<'a> { + /// Composes a trusted local-ledger transaction without verifying its signatures. + pub(super) fn replay(engine: &'a Engine, transaction: Vec) -> Result { + let sanitized = TransactionView::try_new_sanitized(transaction.into(), true)?; + let transaction = sanitized.compose(engine)?; + Ok(Self { engine, transaction }) + } + /// Submits `transaction` for execution and awaits its committed result. /// A timeout does not cancel the submitted transaction. pub async fn execute(self) -> Result> { diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 954737d0..bd85dab0 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -129,6 +129,7 @@ impl Engine { T: IntoTransactionView, { let transaction = transaction.compose(self)?; + transaction::sigverify(&transaction)?; Ok(TransactionAccessor { engine: self, transaction }) } @@ -190,7 +191,12 @@ impl Engine { terminating: Default::default(), }; while let Some(entry) = replayer.rx.recv().await { - engine.replay(entry).await?; + match entry { + OwnedBlockstoreEntry::Transaction(transaction) => { + TransactionAccessor::replay(&engine, transaction)?.schedule().await?; + } + entry => engine.replay(entry).await?, + } } replayer .response diff --git a/engine/src/transaction.rs b/engine/src/transaction.rs index d4be48c3..4d1a00ef 100644 --- a/engine/src/transaction.rs +++ b/engine/src/transaction.rs @@ -64,19 +64,15 @@ impl IntoTransactionView for TransactionView { { return Err(EngineError::SignatureVerification); } - sigverify(&self)?; Ok(self) } } /// The engine's sole signature-verification point. /// -/// Execution is trustless: every submission funnels through the -/// [`TransactionView`] `compose` and is verified here, including replay and -/// replication of already-committed transactions. No path reaches the -/// sequencer unverified, so downstream code may assume the fee payer and every -/// required signer actually signed. -fn sigverify(view: &TransactionView) -> Result<()> { +/// Every public transaction accessor verifies here; trusted local replay is +/// the only bypass. TODO: Remove the bypass before replaying untrusted ledgers. +pub(super) fn sigverify(view: &TransactionView) -> Result<()> { // Sanitization guarantees one static key for every required signature. let message = view.message_data(); for (signature, key) in view.signatures().iter().zip(view.static_account_keys()) {