use alloc::collections::BTreeMap;
use alloc::vec::Vec;
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 FailedNote {
pub note: Note,
pub error: TransactionExecutorError,
}
impl FailedNote {
pub fn new(note: Note, error: TransactionExecutorError) -> Self {
Self { note, error }
}
}
#[derive(Default, Debug)]
pub struct NoteConsumptionInfo {
pub successful: Vec<Note>,
pub failed: Vec<FailedNote>,
}
impl NoteConsumptionInfo {
pub fn new_successful(successful: Vec<Note>) -> Self {
Self { successful, ..Default::default() }
}
pub fn new(successful: Vec<Note>, failed: Vec<FailedNote>) -> Self {
Self { successful, 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(()) => 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(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(()) => {
let successful = candidate_notes;
return Ok(NoteConsumptionInfo::new(successful, failed_notes));
},
Err(TransactionCheckerError::NoteExecution { failed_note_index, error }) => {
let failed_note = candidate_notes.remove(failed_note_index);
failed_notes.push(FailedNote::new(failed_note, error));
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 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(()) => {
failed_note_index.remove(¬e.id());
remaining_notes.remove(idx);
break;
},
Err(error) => {
let failed_note =
successful_notes.pop().expect("successful notes should not be empty");
failed_note_index
.insert(failed_note.id(), FailedNote::new(failed_note, error.into()));
},
}
}
}
failed_notes.extend(failed_note_index.into_values());
NoteConsumptionInfo::new(successful_notes, failed_notes)
}
async fn try_execute_notes(
&self,
tx_inputs: &mut TransactionInputs,
) -> Result<(), TransactionCheckerError> {
if tx_inputs.input_notes().is_empty() {
return Ok(());
}
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 (_, 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(())
},
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() {
Err(TransactionCheckerError::EpilogueExecution(error))
} else {
let failed_note_index = success_notes.len();
Err(TransactionCheckerError::NoteExecution { failed_note_index, error })
}
},
}
}
}
fn handle_epilogue_error(epilogue_error: TransactionExecutorError) -> NoteConsumptionStatus {
match epilogue_error {
TransactionExecutorError::Unauthorized(_)
| TransactionExecutorError::MissingAuthenticator => {
NoteConsumptionStatus::ConsumableWithAuthorization
},
_ => NoteConsumptionStatus::UnconsumableConditions,
}
}