use miden_node_block_producer::store::get_tx_inputs;
use miden_node_proto::clients::{SequencerClient, ValidatorClient};
use miden_node_proto::{DecodeMessageExt, TransactionBatchSubmission, generated as proto};
use miden_node_tracing::spawn::spawn_blocking_in_current_span;
use miden_node_tracing::{ErrorReport, debug, miden_instrument, miden_span_record, trace};
use miden_protocol::MIN_PROOF_SECURITY_LEVEL;
use miden_protocol::batch::{ProposedBatch, ProvenBatch};
use miden_tx_batch::BatchVerifier;
use tonic::{Request, Status};
use super::{RpcBackend, RpcService, submit_batch_to_validators};
use crate::{COMPONENT, LOG_TARGET};
#[tonic::async_trait]
impl proto::server::rpc_api::SubmitProvenTxBatch for RpcService {
type Input = proto::submission::TransactionBatch;
type Output = proto::blockchain::BlockNumber;
fn decode(request: proto::submission::TransactionBatch) -> tonic::Result<Self::Input> {
Ok(request)
}
fn encode(output: Self::Output) -> tonic::Result<proto::blockchain::BlockNumber> {
Ok(output)
}
#[miden_instrument(
target = COMPONENT,
name = "submit_proven_tx_batch",
err,
)]
async fn handle(
&self,
input: Self::Input,
metadata: &tonic::metadata::MetadataMap,
_extensions: &tonic::codegen::http::Extensions,
) -> tonic::Result<Self::Output> {
let mut request = input;
let is_authorized_network_tx = self.is_authorized_network_tx(metadata);
let original_accept_header = metadata.get(http::header::ACCEPT.as_str()).cloned();
let preserve_batch_fields =
matches!(&self.backend, RpcBackend::FullNode { pre_auth: None, .. });
trace!(
target: LOG_TARGET,
"Received transaction batch",
batch.size = request.sealed_transaction_inputs.len()
);
let submission = if preserve_batch_fields {
request.clone()
} else {
std::mem::take(&mut request)
};
let TransactionBatchSubmission {
batch: proven_batch,
proposed_batch,
sealed_transaction_inputs,
} = spawn_blocking_in_current_span(move || submission.decode_and_verify())
.await
.map_err(|err| Status::internal(format!("batch decoding task failed: {err}")))?
.map_err(miden_node_proto::errors::ConversionError::into_status)?;
miden_span_record!(
batch.id = proven_batch.id(),
batch.expires_at = proven_batch.batch_expiration_block_num(),
batch.reference_block.number = proven_batch.reference_block_num(),
batch.reference_block.commitment = proven_batch.reference_block_commitment()
);
debug!(target: LOG_TARGET, "Submitting transaction batch");
if let RpcBackend::Sequencer { account_admission, .. } = &self.backend {
for tx in proposed_batch.transactions() {
account_admission.check(tx.account_update()).await?;
}
}
self.verify_reference_commitment(
proven_batch.reference_block_num(),
proven_batch.reference_block_commitment(),
)
.await?;
if !is_authorized_network_tx {
let non_deployment_ids = proposed_batch
.transactions()
.iter()
.filter(|tx| {
!tx.account_update().initial_state_commitment().is_empty()
&& tx.account_id().is_public()
})
.map(|tx| tx.account_id());
self.reject_if_any_network_accounts(non_deployment_ids).await?;
}
verify_batch_proof(&proven_batch, &proposed_batch).await?;
match &self.backend {
RpcBackend::Sequencer { block_producer, validators, .. } => {
submit_batch_to_validators(
validators.as_slice(),
&proposed_batch,
&sealed_transaction_inputs,
)
.await?;
block_producer
.submit_proven_tx_batch(proven_batch, proposed_batch)
.await
.map(Into::into)
.map_err(Into::into)
},
RpcBackend::FullNode { pre_auth: Some(pre_auth), .. } => {
self.submit_authenticated_batch_to_sequencer(
pre_auth.validators().as_slice(),
pre_auth.sequencer().clone(),
proven_batch,
proposed_batch,
&sealed_transaction_inputs,
)
.await
},
RpcBackend::FullNode { source_rpc, pre_auth: None, .. } => {
let mut forwarded_request = Request::new(request);
if let Some(accept) = original_accept_header {
forwarded_request.metadata_mut().insert(http::header::ACCEPT.as_str(), accept);
}
source_rpc
.as_ref()
.clone()
.submit_proven_tx_batch(forwarded_request)
.await
.map(tonic::Response::into_inner)
},
}
}
}
impl RpcService {
async fn submit_authenticated_batch_to_sequencer(
&self,
validators: &[ValidatorClient],
mut sequencer: SequencerClient,
proven_batch: ProvenBatch,
proposed_batch: ProposedBatch,
sealed_transaction_inputs: &[proto::submission::SealedTransactionInputs],
) -> tonic::Result<proto::blockchain::BlockNumber> {
submit_batch_to_validators(validators, &proposed_batch, sealed_transaction_inputs).await?;
let mut auth_inputs = Vec::with_capacity(proposed_batch.transactions().len());
for tx in proposed_batch.transactions() {
let inputs = get_tx_inputs(&self.state, tx).await.map_err(|err| {
Status::internal(err.as_report_context("failed to authenticate transaction"))
})?;
auth_inputs.push(inputs.into());
}
let authenticated_batch = proto::sequencer::AuthenticatedTransactionBatch {
proposed_batch: Some((&proposed_batch).into()),
auth_inputs,
batch_proof: Some((&proven_batch).into()),
};
sequencer
.submit_authenticated_tx_batch(authenticated_batch)
.await
.map(tonic::Response::into_inner)
}
}
async fn verify_batch_proof(
proven_batch: &ProvenBatch,
proposed_batch: &ProposedBatch,
) -> tonic::Result<()> {
if proven_batch.id() != proposed_batch.id() {
return Err(Status::invalid_argument(format!(
"batch proof did not match proposed batch in block commitment {}: proven id={}, proposed id={}",
proposed_batch.reference_block_header().commitment(),
proven_batch.id(),
proposed_batch.id()
)));
}
let proven_batch = proven_batch.clone();
let batch_id = proven_batch.id();
spawn_blocking_in_current_span(move || {
BatchVerifier::new(MIN_PROOF_SECURITY_LEVEL)
.verify(&proven_batch)
.map_err(|err| {
Status::invalid_argument(format!(
"Invalid proof for batch {}: {}",
batch_id,
err.as_report()
))
})
})
.await
.map_err(|err| Status::internal(format!("batch proof verification task failed: {err}")))??;
Ok(())
}