use std::collections::{HashMap, HashSet};
use std::fmt::{Display, Formatter};
use std::num::NonZeroU32;
use std::sync::Arc;
use miden_node_utils::formatting::format_opt;
use miden_protobuf::{BuildUnchecked, Verify, VerifyWith};
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::batch::ProposedBatch;
use miden_protocol::block::BlockNumber;
use miden_protocol::note::Nullifier;
use miden_protocol::transaction::{ProvenTransaction, TransactionId, TxAccountUpdate};
use thiserror::Error;
use crate::errors::{ConversionError, ConversionResultExt};
use crate::generated::sequencer;
impl VerifyWith<u32> for sequencer::DecodedAuthenticatedTransactionBatch {
type Verified = (ProposedBatch, Vec<TransactionInputs>);
type Error = ConversionError;
fn verify_with(self, security_level: u32) -> Result<Self::Verified, Self::Error> {
let batch = self.proposed_batch.verify_with(security_level).context("proposed_batch")?;
if batch.transactions().len() != self.auth_inputs.as_slice().len() {
return Err(ConversionError::message(format!(
"authentication input count {} does not match transaction count {}",
self.auth_inputs.as_slice().len(),
batch.transactions().len()
)));
}
let inputs = self
.auth_inputs
.into_inner()
.into_iter()
.enumerate()
.map(|(index, inputs)| inputs.verify().with_context(|| format!("auth_inputs[{index}]")))
.collect::<Result<Vec<_>, _>>()?;
Ok((batch, inputs))
}
}
#[derive(Debug)]
pub struct TransactionInputs {
pub account_id: AccountId,
pub account_commitment: Option<Word>,
pub nullifiers: HashMap<Nullifier, Option<NonZeroU32>>,
pub found_unauthenticated_notes: HashSet<Word>,
pub current_block_height: BlockNumber,
}
impl From<TransactionInputs> for sequencer::AuthInputs {
fn from(value: TransactionInputs) -> Self {
Self {
account_id: Some(value.account_id.into()),
account_commitment: value.account_commitment.map(Into::into),
nullifiers: value
.nullifiers
.into_iter()
.map(|(nullifier, block_num)| sequencer::NullifierRecord {
nullifier: Some(nullifier.as_word().into()),
block_num: block_num.map_or(0, NonZeroU32::get),
})
.collect(),
found_unauthenticated_notes: value
.found_unauthenticated_notes
.into_iter()
.map(Into::into)
.collect(),
current_block_height: value.current_block_height.as_u32(),
}
}
}
impl Verify for sequencer::DecodedAuthInputs {
type Verified = TransactionInputs;
type Error = ConversionError;
fn verify(self) -> Result<Self::Verified, Self::Error> {
let account_id = self.account_id.verify().context("account_id")?;
let mut nullifiers = HashMap::with_capacity(self.nullifiers.as_slice().len());
for (index, record) in self.nullifiers.into_inner().into_iter().enumerate() {
let nullifier = Nullifier::from_raw(record.nullifier);
if nullifiers.insert(nullifier, NonZeroU32::new(record.block_num)).is_some() {
return Err(ConversionError::message(format!("duplicate nullifier {nullifier}"))
.context(format!("nullifiers[{index}]")));
}
}
Ok(TransactionInputs {
account_id,
account_commitment: self.account_commitment.into_inner(),
nullifiers,
found_unauthenticated_notes: self
.found_unauthenticated_notes
.into_inner()
.into_iter()
.collect(),
current_block_height: self.current_block_height.into(),
})
}
}
impl Display for TransactionInputs {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let nullifiers = self
.nullifiers
.iter()
.map(|(k, v)| format!("{k}: {}", format_opt(v.as_ref())))
.collect::<Vec<_>>()
.join(", ");
let nullifiers = if nullifiers.is_empty() {
"None".to_owned()
} else {
format!("{{ {nullifiers} }}")
};
f.write_fmt(format_args!(
"{{ account_id: {}, account_commitment: {}, nullifiers: {} }}",
self.account_id,
format_opt(self.account_commitment.as_ref()),
nullifiers
))
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum TransactionAuthenticationError {
#[error("authentication account ID {actual} does not match transaction account ID {expected}")]
AccountIdMismatch { expected: AccountId, actual: AccountId },
#[error("authentication inputs omit transaction nullifiers: {0:?}")]
MissingNullifiers(Vec<Nullifier>),
#[error("nullifiers already exist: {0:?}")]
NullifiersAlreadyExist(Vec<Nullifier>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct AuthenticatedTransaction {
inner: Arc<ProvenTransaction>,
store_account_state: Option<Word>,
notes_authenticated_by_store: HashSet<Word>,
authentication_height: BlockNumber,
}
impl AuthenticatedTransaction {
pub fn new_unchecked(
tx: Arc<ProvenTransaction>,
inputs: TransactionInputs,
) -> Result<AuthenticatedTransaction, TransactionAuthenticationError> {
if inputs.account_id != tx.account_id() {
return Err(TransactionAuthenticationError::AccountIdMismatch {
expected: tx.account_id(),
actual: inputs.account_id,
});
}
let mut missing_nullifiers = Vec::new();
let mut nullifiers_already_spent = Vec::new();
for nullifier in tx.nullifiers() {
match inputs.nullifiers.get(&nullifier) {
None => missing_nullifiers.push(nullifier),
Some(Some(_)) => nullifiers_already_spent.push(nullifier),
Some(None) => {},
}
}
if !missing_nullifiers.is_empty() {
return Err(TransactionAuthenticationError::MissingNullifiers(missing_nullifiers));
}
if !nullifiers_already_spent.is_empty() {
return Err(TransactionAuthenticationError::NullifiersAlreadyExist(
nullifiers_already_spent,
));
}
Ok(AuthenticatedTransaction {
inner: tx,
notes_authenticated_by_store: inputs.found_unauthenticated_notes,
authentication_height: inputs.current_block_height,
store_account_state: inputs.account_commitment,
})
}
pub fn id(&self) -> TransactionId {
self.inner.id()
}
pub fn account_id(&self) -> AccountId {
self.inner.account_id()
}
pub fn account_update(&self) -> &TxAccountUpdate {
self.inner.account_update()
}
pub fn store_account_state(&self) -> Option<Word> {
self.store_account_state
}
pub fn authentication_height(&self) -> BlockNumber {
self.authentication_height
}
pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
self.inner.nullifiers()
}
pub fn output_note_ids(&self) -> impl Iterator<Item = Word> + '_ {
self.inner.output_notes().iter().map(|n| n.id().as_word())
}
pub fn output_note_count(&self) -> usize {
self.inner.output_notes().num_notes()
}
pub fn input_note_count(&self) -> usize {
self.inner.input_notes().num_notes() as usize
}
pub fn reference_block(&self) -> (BlockNumber, Word) {
(self.inner.ref_block_num(), self.inner.ref_block_commitment())
}
pub fn unauthenticated_note_ids(&self) -> impl Iterator<Item = Word> + '_ {
self.inner
.unauthenticated_notes()
.map(|h| h.id().as_word())
.filter(|commitment| !self.notes_authenticated_by_store.contains(commitment))
}
pub fn mark_notes_authenticated(&mut self, notes: impl IntoIterator<Item = Word>) {
self.notes_authenticated_by_store.extend(notes);
}
pub fn proven_transaction(&self) -> Arc<ProvenTransaction> {
Arc::clone(&self.inner)
}
pub fn expires_at(&self) -> BlockNumber {
self.inner.expiration_block_num()
}
pub fn raw_proven_transaction(&self) -> &ProvenTransaction {
&self.inner
}
}
impl From<AuthenticatedTransaction> for sequencer::AuthenticatedTransaction {
fn from(value: AuthenticatedTransaction) -> Self {
Self {
transaction: Some(value.inner.as_ref().into()),
store_account_state: value.store_account_state.map(Into::into),
notes_authenticated_by_store: value
.notes_authenticated_by_store
.into_iter()
.map(Into::into)
.collect(),
authentication_height: value.authentication_height.as_u32(),
}
}
}
impl BuildUnchecked for sequencer::DecodedAuthenticatedTransaction {
type Output = AuthenticatedTransaction;
type Error = ConversionError;
fn build_unchecked(self) -> Result<Self::Output, Self::Error> {
let inner = self.transaction.build_unchecked().context("transaction")?;
Ok(AuthenticatedTransaction {
inner: Arc::new(inner),
store_account_state: self.store_account_state.into_inner(),
notes_authenticated_by_store: self
.notes_authenticated_by_store
.into_inner()
.into_iter()
.collect(),
authentication_height: self.authentication_height.into(),
})
}
}