use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Context;
use miden_node_tracing::{error, info, miden_instrument};
use miden_node_utils::lru_cache::LruCache;
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::block::BlockNumber;
use miden_protocol::note::{Note, NoteScript, Nullifier};
use miden_protocol::transaction::{TransactionArgs, TransactionId};
use crate::candidate::TransactionCandidate;
use crate::chain_state::ChainState;
use crate::clients::{RemoteTransactionProver, RpcClient};
use crate::db::NtxDbReader;
use crate::execute::{NtxError, NtxExecutionResult};
use crate::selection::{
Selection,
attribute_failed_notes,
log_deferred_notes,
log_oversized_notes,
select_candidate,
};
use crate::{LOG_TARGET, NoteError, execute};
#[derive(Clone)]
pub struct GrpcClients {
pub rpc: RpcClient,
pub prover: RemoteTransactionProver,
}
#[derive(Debug, Clone, Copy)]
pub struct NetworkTransactionConfig {
pub max_notes_per_tx: NonZeroUsize,
pub max_note_attempts: usize,
pub max_cycles: u32,
pub request_backoff_initial: Duration,
pub request_backoff_max: Duration,
}
#[derive(Clone)]
pub struct NetworkTransactionContext {
pub clients: GrpcClients,
pub db: NtxDbReader,
pub script_cache: LruCache<Word, NoteScript>,
pub tx_args: TransactionArgs,
pub config: NetworkTransactionConfig,
}
#[cfg(test)]
impl NetworkTransactionContext {
pub fn test(db: &NtxDbReader) -> Self {
use url::Url;
let url = Url::parse("http://127.0.0.1:1").unwrap();
let block_header = crate::test_utils::mock_block_header(0_u32.into());
let trusted_validator_signing_keys = block_header.validator_config().keys().to_vec();
Self {
clients: GrpcClients {
rpc: RpcClient::new(
url.clone(),
Word::default(),
trusted_validator_signing_keys,
Duration::from_secs(10),
Duration::from_millis(1),
Duration::from_millis(10),
)
.expect("rpc client should be constructed"),
prover: RemoteTransactionProver::new(url, Duration::from_secs(10))
.expect("prover client should be constructed"),
},
db: db.clone(),
script_cache: LruCache::new(NonZeroUsize::new(1).unwrap()),
tx_args: crate::selection::build_tx_args(
std::num::NonZeroU16::new(30).expect("literal is non-zero"),
),
config: NetworkTransactionConfig {
max_notes_per_tx: NonZeroUsize::new(20).expect("literal is non-zero"),
max_note_attempts: 30,
max_cycles: 1 << 18,
request_backoff_initial: Duration::from_millis(1),
request_backoff_max: Duration::from_millis(10),
},
}
}
}
#[derive(Default)]
pub struct NoteUpdates {
pub failed: Vec<(Nullifier, NoteError)>,
pub discarded: Vec<Nullifier>,
pub eligibility: Vec<(Nullifier, BlockNumber)>,
pub scripts: Vec<(Word, NoteScript)>,
}
pub enum NetworkTransactionResult {
Submitted { tx_id: TransactionId },
NoWork,
Failed,
Aborted(anyhow::Error),
}
pub struct NetworkTransactionOutcome {
pub account_id: AccountId,
pub block_num: BlockNumber,
pub notes: NoteUpdates,
pub result: NetworkTransactionResult,
}
impl NetworkTransactionContext {
#[miden_instrument(
name = "ntx.build",
fields(
account.id = account_id,
reference_block.number = chain.chain_tip_header.block_num(),
),
)]
pub async fn build(
self,
account_id: AccountId,
chain: ChainState,
) -> NetworkTransactionOutcome {
let block_num = chain.chain_tip_header.block_num();
match self.try_build(account_id, chain).await {
Ok(outcome) => outcome,
Err(err) => NetworkTransactionOutcome {
account_id,
block_num,
notes: NoteUpdates::default(),
result: NetworkTransactionResult::Aborted(err),
},
}
}
async fn try_build(
&self,
account_id: AccountId,
chain: ChainState,
) -> anyhow::Result<NetworkTransactionOutcome> {
let block_num = chain.chain_tip_header.block_num();
let Some(account) = self
.db
.get_account(account_id)
.await
.context("failed to load committed account")?
else {
return Ok(NetworkTransactionOutcome {
account_id,
block_num,
notes: NoteUpdates::default(),
result: NetworkTransactionResult::NoWork,
});
};
let account = Arc::new(account);
let Selection { candidate, rejected, stale_eligibility } = select_candidate(
&self.db,
&account,
chain,
self.config.max_notes_per_tx,
self.config.max_note_attempts,
)
.await?;
let mut notes = NoteUpdates {
failed: rejected,
eligibility: stale_eligibility,
..NoteUpdates::default()
};
let Some(candidate) = candidate else {
return Ok(NetworkTransactionOutcome {
account_id,
block_num,
notes,
result: NetworkTransactionResult::NoWork,
});
};
let result = self.execute_candidate(account_id, candidate, &mut notes).await;
Ok(NetworkTransactionOutcome { account_id, block_num, notes, result })
}
async fn execute_candidate(
&self,
account_id: AccountId,
candidate: TransactionCandidate,
notes: &mut NoteUpdates,
) -> NetworkTransactionResult {
let sponsor_to_feature = candidate.sponsor_to_feature_nullifier();
let note_ids: Vec<_> = candidate
.notes
.iter()
.flat_map(|sponsored| {
std::iter::once(sponsored.feature.as_note().id())
.chain(sponsored.sponsorships.iter().map(Note::id))
})
.collect();
let feature_nullifiers: Vec<_> = candidate
.notes
.iter()
.map(|sponsored| sponsored.feature.as_note().nullifier())
.collect();
info!(
target: LOG_TARGET,
"executing network transaction",
account.id = account_id,
note.ids = note_ids.as_slice(),
note.count = note_ids.len()
);
let context = execute::NtxContext::new(
self.clients.prover.clone(),
self.clients.rpc.clone(),
self.script_cache.clone(),
self.db.clone(),
self.config.max_cycles,
self.tx_args.clone(),
self.config.request_backoff_initial,
self.config.request_backoff_max,
);
match context.execute_transaction(candidate).await {
Ok(NtxExecutionResult {
tx_id,
failed_notes,
deferred_notes,
oversized_notes,
fetched_scripts,
}) => {
info!(
target: LOG_TARGET,
"network transaction executed",
account.id = account_id,
transaction.id = tx_id,
note.failed.count = failed_notes.len(),
note.deferred.count = deferred_notes.len(),
note.oversized.count = oversized_notes.len()
);
notes.scripts = fetched_scripts;
log_deferred_notes(deferred_notes);
let (oversized_sponsorships, oversized_features): (Vec<_>, Vec<_>) =
oversized_notes
.into_iter()
.partition(|f| sponsor_to_feature.contains_key(&f.note().id()));
let mut to_penalize = failed_notes;
to_penalize.extend(oversized_sponsorships);
notes.failed.extend(attribute_failed_notes(to_penalize, &sponsor_to_feature));
notes.discarded = log_oversized_notes(oversized_features);
NetworkTransactionResult::Submitted { tx_id }
},
Err(err) => {
error!(
&err,
target: LOG_TARGET,
"network transaction failed",
account.id = account_id,
note.ids = note_ids.as_slice()
);
let failed = match err {
NtxError::AllNotesFailed(per_note) => {
attribute_failed_notes(per_note, &sponsor_to_feature)
},
other => {
let error: NoteError = Arc::new(other);
feature_nullifiers
.into_iter()
.map(|nullifier| {
info!(
error.as_ref(),
target: LOG_TARGET,
"note failed: transaction execution error",
note.nullifier = nullifier
);
(nullifier, error.clone())
})
.collect()
},
};
notes.failed.extend(failed);
NetworkTransactionResult::Failed
},
}
}
}