use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use miden_processor::ExecutionError;
use miden_processor::advice::AdviceInputs;
use miden_protocol::account::AccountId;
use miden_protocol::block::BlockNumber;
use miden_protocol::note::Note;
use miden_protocol::transaction::{
InputNote,
InputNotes,
TransactionArgs,
TransactionInputs,
TransactionKernel,
};
use miden_standards::note::{NoteConsumptionStatus, StandardNote};
use super::{ProgramExecutor, TransactionExecutor};
use crate::auth::TransactionAuthenticator;
use crate::errors::TransactionCheckerError;
use crate::executor::map_execution_error;
use crate::{DataStore, NoteCheckerError, TransactionExecutorError};
pub const MAX_NUM_CHECKER_NOTES: usize = 20;
#[derive(Debug)]
pub struct SuccessfulNote {
note: Note,
num_cycles: usize,
}
impl SuccessfulNote {
pub fn new(note: Note, num_cycles: usize) -> Self {
Self { note, num_cycles }
}
pub fn note(&self) -> &Note {
&self.note
}
pub fn num_cycles(&self) -> usize {
self.num_cycles
}
}
#[derive(Debug)]
pub struct FailedNote {
note: Note,
error: TransactionExecutorError,
num_cycles: Option<usize>,
}
impl FailedNote {
pub fn new(note: Note, error: TransactionExecutorError, num_cycles: Option<usize>) -> Self {
Self { note, error, num_cycles }
}
pub fn note(&self) -> &Note {
&self.note
}
pub fn error(&self) -> &TransactionExecutorError {
&self.error
}
pub fn num_cycles(&self) -> Option<usize> {
self.num_cycles
}
}
#[derive(Default, Debug)]
pub struct NoteConsumptionInfo {
successful: Vec<SuccessfulNote>,
failed: Vec<FailedNote>,
}
impl NoteConsumptionInfo {
pub fn new_successful(successful: Vec<SuccessfulNote>) -> Self {
Self { successful, ..Default::default() }
}
pub fn new(successful: Vec<SuccessfulNote>, failed: Vec<FailedNote>) -> Self {
Self { successful, failed }
}
pub fn successful(&self) -> &[SuccessfulNote] {
&self.successful
}
pub fn failed(&self) -> &[FailedNote] {
&self.failed
}
pub fn into_parts(self) -> (Vec<SuccessfulNote>, Vec<FailedNote>) {
(self.successful, self.failed)
}
}
pub struct NoteConsumptionChecker<'a, STORE, AUTH, EXEC: ProgramExecutor>(
&'a TransactionExecutor<'a, 'a, STORE, AUTH, EXEC>,
);
impl<'a, STORE, AUTH, EXEC> NoteConsumptionChecker<'a, STORE, AUTH, EXEC>
where
STORE: DataStore + Sync,
AUTH: TransactionAuthenticator + Sync,
EXEC: ProgramExecutor,
{
pub fn new(tx_executor: &'a TransactionExecutor<'a, 'a, STORE, AUTH, EXEC>) -> Self {
NoteConsumptionChecker(tx_executor)
}
pub async fn check_notes_consumability(
&self,
target_account_id: AccountId,
block_ref: BlockNumber,
mut notes: Vec<Note>,
tx_args: TransactionArgs,
) -> Result<NoteConsumptionInfo, NoteCheckerError> {
let num_notes = notes.len();
if num_notes == 0 || num_notes > MAX_NUM_CHECKER_NOTES {
return Err(NoteCheckerError::InputNoteCountOutOfRange(num_notes));
}
notes.sort_unstable_by_key(|note| {
StandardNote::from_script_root(note.script().root()).is_none()
});
let notes = InputNotes::from(notes);
let tx_inputs = self
.0
.prepare_tx_inputs(target_account_id, block_ref, notes, tx_args)
.await
.map_err(NoteCheckerError::TransactionPreparation)?;
self.find_executable_notes_by_elimination(tx_inputs).await
}
pub async fn can_consume(
&self,
target_account_id: AccountId,
block_ref: BlockNumber,
note: InputNote,
tx_args: TransactionArgs,
) -> Result<NoteConsumptionStatus, NoteCheckerError> {
if let Some(standard_note) = StandardNote::from_script_root(note.note().script().root())
&& let Some(consumption_status) =
standard_note.is_consumable(note.note(), target_account_id, block_ref)
{
return Ok(consumption_status);
}
let mut tx_inputs = self
.0
.prepare_tx_inputs(
target_account_id,
block_ref,
InputNotes::new_unchecked(vec![note]),
tx_args,
)
.await
.map_err(NoteCheckerError::TransactionPreparation)?;
match self.try_execute_notes(&mut tx_inputs).await {
Ok(_cycle_counts) => Ok(NoteConsumptionStatus::Consumable),
Err(tx_checker_error) => {
match tx_checker_error {
TransactionCheckerError::TransactionPreparation(e) => {
Err(NoteCheckerError::TransactionPreparation(e))
},
TransactionCheckerError::PrologueExecution(e) => {
Err(NoteCheckerError::PrologueExecution(e))
},
TransactionCheckerError::NoteExecution { .. } => {
Ok(NoteConsumptionStatus::UnconsumableConditions)
},
TransactionCheckerError::EpilogueExecution {
error: epilogue_error, ..
} => Ok(handle_epilogue_error(epilogue_error)),
}
},
}
}
async fn find_executable_notes_by_elimination(
&self,
mut tx_inputs: TransactionInputs,
) -> Result<NoteConsumptionInfo, NoteCheckerError> {
let mut candidate_notes = tx_inputs
.input_notes()
.iter()
.map(|note| note.clone().into_note())
.collect::<Vec<_>>();
let mut failed_notes = Vec::new();
loop {
tx_inputs.set_input_notes(candidate_notes.clone());
match self.try_execute_notes(&mut tx_inputs).await {
Ok(cycle_counts) => {
let successful = candidate_notes
.into_iter()
.zip(cycle_counts)
.map(|(note, num_cycles)| SuccessfulNote::new(note, num_cycles))
.collect();
return Ok(NoteConsumptionInfo::new(successful, failed_notes));
},
Err(TransactionCheckerError::NoteExecution {
failed_note_index,
error,
failed_note_cycle_count,
..
}) => {
let failed_note = candidate_notes.remove(failed_note_index);
failed_notes.push(FailedNote::new(failed_note, error, failed_note_cycle_count));
if candidate_notes.is_empty() {
return Ok(NoteConsumptionInfo::new(Vec::new(), failed_notes));
}
},
Err(TransactionCheckerError::EpilogueExecution { .. }) => {
let consumption_info = self
.find_largest_executable_combination(
candidate_notes,
failed_notes,
tx_inputs,
)
.await;
return Ok(consumption_info);
},
Err(TransactionCheckerError::PrologueExecution(err)) => {
return Err(NoteCheckerError::PrologueExecution(err));
},
Err(TransactionCheckerError::TransactionPreparation(err)) => {
return Err(NoteCheckerError::TransactionPreparation(err));
},
}
}
}
async fn find_largest_executable_combination(
&self,
mut remaining_notes: Vec<Note>,
mut failed_notes: Vec<FailedNote>,
mut tx_inputs: TransactionInputs,
) -> NoteConsumptionInfo {
let mut successful_notes = Vec::new();
let mut successful_cycle_counts = Vec::new();
let mut failed_note_index = BTreeMap::new();
for size in 1..=remaining_notes.len() {
if successful_notes.len() < size - 1 {
break;
}
for (idx, note) in remaining_notes.iter().enumerate() {
successful_notes.push(note.clone());
tx_inputs.set_input_notes(successful_notes.clone());
match self.try_execute_notes(&mut tx_inputs).await {
Ok(cycle_counts) => {
failed_note_index.remove(¬e.id());
successful_cycle_counts = cycle_counts;
remaining_notes.remove(idx);
break;
},
Err(error) => {
let failed_note =
successful_notes.pop().expect("successful notes should not be empty");
let num_cycles = match &error {
TransactionCheckerError::NoteExecution {
failed_note_cycle_count,
..
} => *failed_note_cycle_count,
_ => None,
};
failed_note_index.insert(
failed_note.id(),
FailedNote::new(failed_note, error.into(), num_cycles),
);
},
}
}
}
let successful = successful_notes
.into_iter()
.zip(successful_cycle_counts)
.map(|(note, num_cycles)| SuccessfulNote::new(note, num_cycles))
.collect();
failed_notes.extend(failed_note_index.into_values());
NoteConsumptionInfo::new(successful, failed_notes)
}
async fn try_execute_notes(
&self,
tx_inputs: &mut TransactionInputs,
) -> Result<Vec<usize>, TransactionCheckerError> {
if tx_inputs.input_notes().is_empty() {
return Ok(Vec::new());
}
let (mut host, stack_inputs, advice_inputs) =
self.0
.prepare_transaction(tx_inputs)
.await
.map_err(TransactionCheckerError::TransactionPreparation)?;
let processor = EXEC::new(stack_inputs, advice_inputs, self.0.exec_options);
let result = processor
.execute(&TransactionKernel::main(), &mut host)
.await
.map_err(map_execution_error);
match result {
Ok(execution_output) => {
let cycle_counts = host
.tx_progress()
.note_execution()
.iter()
.map(|(_, interval)| interval.len())
.collect();
let (_, advice_map, merkle_store, _) = execution_output.advice.into_parts();
let advice_inputs = AdviceInputs {
map: advice_map,
store: merkle_store,
..Default::default()
};
tx_inputs.set_advice_inputs(advice_inputs);
Ok(cycle_counts)
},
Err(error) => {
let notes = host.tx_progress().note_execution();
if notes.is_empty() {
return Err(TransactionCheckerError::PrologueExecution(error));
}
let ((_, last_note_interval), success_notes) =
notes.split_last().expect("notes vector is not empty because of earlier check");
if last_note_interval.end().is_some() {
let successful_notes_cycle_counts =
notes.iter().map(|(_, interval)| interval.len()).collect();
Err(TransactionCheckerError::EpilogueExecution {
error,
successful_notes_cycle_counts,
})
} else {
let failed_note_index = success_notes.len();
let successful_notes_cycle_counts =
success_notes.iter().map(|(_, interval)| interval.len()).collect();
let failed_note_cycle_count = match &error {
TransactionExecutorError::TransactionProgramExecutionFailed(
ExecutionError::CycleLimitExceeded(max_cycles),
) => last_note_interval
.start()
.map(|start| *max_cycles as usize - usize::from(start)),
_ => None,
};
Err(TransactionCheckerError::NoteExecution {
failed_note_index,
error,
successful_notes_cycle_counts,
failed_note_cycle_count,
})
}
},
}
}
}
fn handle_epilogue_error(epilogue_error: TransactionExecutorError) -> NoteConsumptionStatus {
match epilogue_error {
TransactionExecutorError::Unauthorized(_)
| TransactionExecutorError::MissingAuthenticator => {
NoteConsumptionStatus::ConsumableWithAuthorization
},
_ => NoteConsumptionStatus::UnconsumableConditions,
}
}