Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- Added pagination to `SyncNotes` endpoint ([#1257](https://github.com/0xMiden/miden-node/pull/1257)).
- Added application level error in gRPC endpoints ([#1266](https://github.com/0xMiden/miden-node/pull/1266)).
- [BREAKING] Response type nuances of `GetAccountProof` in the public store API (#[1277](https://github.com/0xMiden/miden-node/pull/1277)).
- Add optional `TransactionInputs` field to `SubmitProvenTransaction` endpoint for transaction re-execution (#[1278](https://github.com/0xMiden/miden-node/pull/1278)).

## v0.11.2 (2025-09-10)

Expand Down
11 changes: 7 additions & 4 deletions crates/block-producer/src/server/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ use std::time::Duration;

use miden_air::{ExecutionProof, HashFunction};
use miden_node_proto::generated::{
self as proto,
block_producer::api_client as block_producer_client,
self as proto, block_producer::api_client as block_producer_client,
};
use miden_node_store::{GenesisState, Store};
use miden_objects::{
Expand Down Expand Up @@ -149,7 +148,8 @@ async fn block_producer_startup_is_robust_to_network_failures() {
async fn send_request(
mut client: block_producer_client::ApiClient<Channel>,
i: u8,
) -> Result<tonic::Response<proto::block_producer::SubmitProvenTransactionResponse>, tonic::Status> {
) -> Result<tonic::Response<proto::block_producer::SubmitProvenTransactionResponse>, tonic::Status>
{
let tx = ProvenTransactionBuilder::new(
AccountId::dummy(
[0; 15],
Expand All @@ -167,6 +167,9 @@ async fn send_request(
)
.build()
.unwrap();
let request = proto::transaction::ProvenTransaction { transaction: tx.to_bytes() };
let request = proto::transaction::ProvenTransaction {
transaction: tx.to_bytes(),
transaction_replay: None,
};
client.submit_proven_transaction(request).await
}
5 changes: 4 additions & 1 deletion crates/ntx-builder/src/block_producer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ impl BlockProducerClient {
&self,
proven_tx: ProvenTransaction,
) -> Result<(), Status> {
let request = proto::transaction::ProvenTransaction { transaction: proven_tx.to_bytes() };
let request = proto::transaction::ProvenTransaction {
transaction: proven_tx.to_bytes(),
transaction_inputs: None,
};

self.client.clone().submit_proven_transaction(request).await?;

Expand Down
17 changes: 7 additions & 10 deletions crates/ntx-builder/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,8 @@ impl DataStore for NtxDataStore {
ref_blocks: BTreeSet<BlockNumber>,
) -> impl FutureMaybeSend<Result<(PartialAccount, BlockHeader, PartialBlockchain), DataStoreError>>
{
let account = self.account.clone();
async move {
if account.id() != account_id {
if self.account.id() != account_id {
return Err(DataStoreError::AccountNotFound(account_id));
}

Expand All @@ -277,7 +276,7 @@ impl DataStore for NtxDataStore {
None => return Err(DataStoreError::other("no reference block requested")),
}

let partial_account = PartialAccount::from(&account);
let partial_account = PartialAccount::from(&self.account);

Ok((partial_account, self.reference_header.clone(), self.chain_mmr.clone()))
}
Expand All @@ -297,20 +296,19 @@ impl DataStore for NtxDataStore {
vault_root: Word,
vault_key: Word,
) -> impl FutureMaybeSend<Result<AssetWitness, DataStoreError>> {
let account = self.account.clone();
async move {
if account.id() != account_id {
if self.account.id() != account_id {
return Err(DataStoreError::AccountNotFound(account_id));
}

if account.vault().root() != vault_root {
if self.account.vault().root() != vault_root {
return Err(DataStoreError::Other {
error_msg: "vault root mismatch".into(),
source: None,
});
}

AssetWitness::new(account.vault().open(vault_key).into()).map_err(|err| {
AssetWitness::new(self.account.vault().open(vault_key).into()).map_err(|err| {
DataStoreError::Other {
error_msg: "failed to open vault asset tree".into(),
source: Some(Box::new(err)),
Expand All @@ -325,14 +323,13 @@ impl DataStore for NtxDataStore {
map_root: Word,
map_key: Word,
) -> impl FutureMaybeSend<Result<StorageMapWitness, DataStoreError>> {
let account = self.account.clone();
async move {
if account.id() != account_id {
if self.account.id() != account_id {
return Err(DataStoreError::AccountNotFound(account_id));
}

let mut map_witness = None;
for slot in account.storage().slots() {
for slot in self.account.storage().slots() {
if let StorageSlot::Map(map) = slot {
if map.root() == map_root {
map_witness = Some(map.open(&map_key));
Expand Down
4 changes: 4 additions & 0 deletions crates/proto/src/generated/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ pub struct ProvenTransaction {
/// \[miden_objects::transaction::proven_tx::ProvenTransaction\].
#[prost(bytes = "vec", tag = "1")]
pub transaction: ::prost::alloc::vec::Vec<u8>,
/// Transaction inputs encoded using \[winter_utils::Serializable\] implementation for
/// \[miden_objects::transaction::TransactionInputs\].
#[prost(bytes = "vec", optional, tag = "2")]
pub transaction_inputs: ::core::option::Option<::prost::alloc::vec::Vec<u8>>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProvenTransactionBatch {
Expand Down
36 changes: 34 additions & 2 deletions crates/rpc/src/server/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,21 @@ use miden_objects::account::delta::AccountUpdateDetails;
use miden_objects::batch::ProvenBatch;
use miden_objects::block::{BlockHeader, BlockNumber};
use miden_objects::note::{Note, NoteRecipient, NoteScript};
use miden_objects::transaction::{OutputNote, ProvenTransaction, ProvenTransactionBuilder};
use miden_objects::transaction::{
OutputNote,
ProvenTransaction,
ProvenTransactionBuilder,
TransactionInputs,
};
use miden_objects::utils::serde::{Deserializable, Serializable};
use miden_objects::{MIN_PROOF_SECURITY_LEVEL, Word};
use miden_tx::TransactionVerifier;
use tonic::{IntoRequest, Request, Response, Status};
use tracing::{debug, info, instrument};
use tracing::{debug, info, instrument, warn};
use url::Url;

use crate::COMPONENT;
use crate::server::validator;

// RPC SERVICE
// ================================================================================================
Expand Down Expand Up @@ -392,6 +398,32 @@ impl api_server::Api for RpcService {
))
})?;

// If transaction inputs are provided, re-execute the transaction to validate it.
if let Some(tx_inputs_bytes) = &request.transaction_inputs {
// Deserialize the transaction inputs.
let tx_inputs = TransactionInputs::read_from_bytes(tx_inputs_bytes).map_err(|err| {
Status::invalid_argument(err.as_report_context("Invalid transaction inputs"))
})?;
// Re-execute the transaction.
match validator::re_execute_transaction(tx_inputs).await {
Ok(_executed_tx) => {
debug!(
target = COMPONENT,
tx_id = %tx.id().to_hex(),
"Transaction re-execution successful"
);
},
Err(e) => {
warn!(
target = COMPONENT,
tx_id = %tx.id().to_hex(),
error = %e,
"Transaction re-execution failed, but continuing with submission"
);
},
}
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@bobbinth

  1. Do we want this to be blocking as it is now?
  2. Do we want this to return an error if it fails? Or just log like it does now?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this is opt in for debugging only, we probably want to create bounded queue but do the re-execution async. This implies it's not going to block by default, but will eventually if we're under high load, mostly to avoid OOM as it would occur in the unbounded channel scenario. This would hint at 2. being logging only.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This re-execution logic will move to the validator component and this RPC stack will talk to that via gRPC.

Having said that, I think the bounded queue and async processing logic would be the same for this PR and the followup work. Its just a question of what we want to put into this PR for now.


block_producer.clone().submit_proven_transaction(request).await
}

Expand Down
1 change: 1 addition & 0 deletions crates/rpc/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::server::health::HealthCheckLayer;
mod accept;
mod api;
mod health;
mod validator;

/// The RPC server component.
///
Expand Down
148 changes: 148 additions & 0 deletions crates/rpc/src/server/validator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/// NOTE: This module contains logic that will eventually be moved to the Validator component
/// when it is added to this repository.
use std::collections::BTreeSet;

use miden_objects::Word;
use miden_objects::account::{AccountId, PartialAccount, StorageMapWitness};
use miden_objects::asset::AssetWitness;
use miden_objects::block::{BlockHeader, BlockNumber};
use miden_objects::transaction::{
AccountInputs,
ExecutedTransaction,
PartialBlockchain,
TransactionInputs,
};
use miden_objects::vm::FutureMaybeSend;
use miden_tx::auth::UnreachableAuth;
use miden_tx::{
DataStore,
DataStoreError,
MastForestStore,
TransactionExecutor,
TransactionExecutorError,
TransactionMastStore,
};

/// Executes a transaction using the provided transaction inputs.
pub async fn re_execute_transaction(
tx_inputs: TransactionInputs,
) -> Result<ExecutedTransaction, TransactionExecutorError> {
// Create a DataStore from the transaction inputs.
let data_store = TransactionInputsDataStore::new(tx_inputs.clone());

// Execute the transaction.
let (account, block_header, _, input_notes, tx_args) = tx_inputs.into_parts();
let executor: TransactionExecutor<'_, '_, _, UnreachableAuth> =
TransactionExecutor::new(&data_store);
executor
.execute_transaction(account.id(), block_header.block_num(), input_notes, tx_args)
.await
}

/// A [`DataStore`] implementation that wraps [`TransactionInputs`]
struct TransactionInputsDataStore {
tx_inputs: TransactionInputs,
mast_store: TransactionMastStore,
}

impl TransactionInputsDataStore {
fn new(tx_inputs: TransactionInputs) -> Self {
let mast_store = TransactionMastStore::new();
mast_store.load_account_code(tx_inputs.account().code());
Self { tx_inputs, mast_store }
}
}

impl DataStore for TransactionInputsDataStore {
fn get_transaction_inputs(
&self,
account_id: AccountId,
_ref_blocks: BTreeSet<BlockNumber>,
) -> impl FutureMaybeSend<Result<(PartialAccount, BlockHeader, PartialBlockchain), DataStoreError>>
{
async move {
if self.tx_inputs.account().id() != account_id {
return Err(DataStoreError::AccountNotFound(account_id));
}

Ok((
self.tx_inputs.account().clone(),
self.tx_inputs.block_header().clone(),
self.tx_inputs.blockchain().clone(),
))
}
}

fn get_foreign_account_inputs(
&self,
foreign_account_id: AccountId,
_ref_block: BlockNumber,
) -> impl FutureMaybeSend<Result<AccountInputs, DataStoreError>> {
async move { Err(DataStoreError::AccountNotFound(foreign_account_id)) }
}

fn get_vault_asset_witness(
&self,
account_id: AccountId,
vault_root: Word,
vault_key: Word,
) -> impl FutureMaybeSend<Result<AssetWitness, DataStoreError>> {
async move {
if self.tx_inputs.account().id() != account_id {
return Err(DataStoreError::AccountNotFound(account_id));
}

if self.tx_inputs.account().vault().root() != vault_root {
return Err(DataStoreError::Other {
error_msg: "vault root mismatch".into(),
source: None,
});
}

match self.tx_inputs.account().vault().open(vault_key) {
Ok(vault_proof) => {
AssetWitness::new(vault_proof.into()).map_err(|err| DataStoreError::Other {
error_msg: "failed to open vault asset tree".into(),
source: Some(err.into()),
})
},
Err(err) => Err(DataStoreError::Other {
error_msg: "failed to open vault".into(),
source: Some(err.into()),
}),
}
}
}

fn get_storage_map_witness(
&self,
account_id: AccountId,
_map_root: Word,
_map_key: Word,
) -> impl FutureMaybeSend<Result<StorageMapWitness, DataStoreError>> {
async move {
if self.tx_inputs.account().id() != account_id {
return Err(DataStoreError::AccountNotFound(account_id));
}

// For partial accounts, storage map witness is not available.
Err(DataStoreError::Other {
error_msg: "storage map witness not available with partial account state".into(),
source: None,
})
}
}

fn get_note_script(
&self,
script_root: Word,
) -> impl FutureMaybeSend<Result<miden_objects::note::NoteScript, DataStoreError>> {
async move { Err(DataStoreError::NoteScriptNotFound(script_root)) }
}
}

impl MastForestStore for TransactionInputsDataStore {
fn get(&self, procedure_hash: &Word) -> Option<std::sync::Arc<miden_objects::MastForest>> {
self.mast_store.get(procedure_hash)
}
}
5 changes: 4 additions & 1 deletion crates/rpc/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,10 @@ async fn rpc_server_rejects_proven_transactions_with_invalid_commitment() {
))
.build()
.unwrap();
let request = proto::transaction::ProvenTransaction { transaction: tx.to_bytes() };
let request = proto::transaction::ProvenTransaction {
transaction: tx.to_bytes(),
transaction_inputs: None,
};

let response = rpc_client.submit_proven_transaction(request).await;

Expand Down
3 changes: 3 additions & 0 deletions proto/proto/types/transaction.proto
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ message ProvenTransaction {
// Transaction encoded using [winter_utils::Serializable] implementation for
// [miden_objects::transaction::proven_tx::ProvenTransaction].
bytes transaction = 1;
// Transaction inputs encoded using [winter_utils::Serializable] implementation for
// [miden_objects::transaction::TransactionInputs].
optional bytes transaction_inputs = 2;
}

message ProvenTransactionBatch {
Expand Down