1use std::collections::{HashMap, HashSet};
4use std::fmt::{Display, Formatter};
5use std::num::NonZeroU32;
6use std::sync::Arc;
7
8use miden_node_utils::formatting::format_opt;
9use miden_protobuf::{BuildUnchecked, Verify, VerifyWith};
10use miden_protocol::Word;
11use miden_protocol::account::AccountId;
12use miden_protocol::batch::{ProposedBatch, ProvenBatch};
13use miden_protocol::block::BlockNumber;
14use miden_protocol::note::{NoteHeader, NoteId, Nullifier};
15use miden_protocol::transaction::{OutputNote, ProvenTransaction, TransactionId, TxAccountUpdate};
16use thiserror::Error;
17
18use crate::errors::{ConversionError, ConversionResultExt};
19use crate::generated::miden::sequencer::v1 as sequencer;
20
21impl VerifyWith<u32> for sequencer::DecodedAuthenticatedTransactionBatch {
22 type Verified = (ProvenBatch, ProposedBatch, Vec<TransactionInputs>);
23 type Error = ConversionError;
24
25 fn verify_with(self, security_level: u32) -> Result<Self::Verified, Self::Error> {
29 let batch = self.proposed_batch.verify_with(security_level).context("proposed_batch")?;
30 let proof = self.batch_proof.verify_with(&batch).context("batch_proof")?;
31 if batch.transactions().len() != self.auth_inputs.as_slice().len() {
32 return Err(ConversionError::message(format!(
33 "authentication input count {} does not match transaction count {}",
34 self.auth_inputs.as_slice().len(),
35 batch.transactions().len()
36 )));
37 }
38 let inputs = self.auth_inputs.verify()?;
39 Ok((proof, batch, inputs))
40 }
41}
42
43#[derive(Debug)]
45pub struct TransactionInputs {
46 pub account_id: AccountId,
48 pub account_commitment: Option<Word>,
50 pub nullifiers: HashMap<Nullifier, Option<NonZeroU32>>,
54 pub found_unauthenticated_notes: HashSet<NoteId>,
58 pub current_block_height: BlockNumber,
60}
61
62impl From<TransactionInputs> for sequencer::AuthInputs {
63 fn from(value: TransactionInputs) -> Self {
64 Self {
65 account_id: Some(value.account_id.into()),
66 account_commitment: value.account_commitment.map(Into::into),
67 nullifiers: value
68 .nullifiers
69 .into_iter()
70 .map(|(nullifier, block_num)| sequencer::NullifierRecord {
71 nullifier: Some(nullifier.as_word().into()),
72 block_num: block_num.map_or(0, NonZeroU32::get),
73 })
74 .collect(),
75 found_unauthenticated_notes: value
76 .found_unauthenticated_notes
77 .into_iter()
78 .map(|note_id| note_id.as_word().into())
79 .collect(),
80 current_block_height: value.current_block_height.as_u32(),
81 }
82 }
83}
84
85impl Verify for sequencer::DecodedAuthInputs {
86 type Verified = TransactionInputs;
87 type Error = ConversionError;
88
89 fn verify(self) -> Result<Self::Verified, Self::Error> {
90 let account_id = self.account_id.verify().context("account_id")?;
91 let mut nullifiers = HashMap::with_capacity(self.nullifiers.as_slice().len());
92 for (index, record) in self.nullifiers.into_inner().into_iter().enumerate() {
93 let nullifier = Nullifier::from_raw(record.nullifier);
94 if nullifiers.insert(nullifier, NonZeroU32::new(record.block_num)).is_some() {
95 return Err(ConversionError::message(format!("duplicate nullifier {nullifier}"))
96 .context(format!("nullifiers[{index}]")));
97 }
98 }
99 Ok(TransactionInputs {
100 account_id,
101 account_commitment: self.account_commitment.into_inner(),
102 nullifiers,
103 found_unauthenticated_notes: self
104 .found_unauthenticated_notes
105 .into_inner()
106 .into_iter()
107 .map(NoteId::from_raw)
108 .collect(),
109 current_block_height: self.current_block_height.into(),
110 })
111 }
112}
113
114impl Display for TransactionInputs {
115 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
116 let nullifiers = self
117 .nullifiers
118 .iter()
119 .map(|(k, v)| format!("{k}: {}", format_opt(v.as_ref())))
120 .collect::<Vec<_>>()
121 .join(", ");
122
123 let nullifiers = if nullifiers.is_empty() {
124 "None".to_owned()
125 } else {
126 format!("{{ {nullifiers} }}")
127 };
128
129 f.write_fmt(format_args!(
130 "{{ account_id: {}, account_commitment: {}, nullifiers: {} }}",
131 self.account_id,
132 format_opt(self.account_commitment.as_ref()),
133 nullifiers
134 ))
135 }
136}
137
138#[derive(Debug, Error, PartialEq, Eq)]
140pub enum TransactionAuthenticationError {
141 #[error("authentication account ID {actual} does not match transaction account ID {expected}")]
142 AccountIdMismatch { expected: AccountId, actual: AccountId },
143 #[error("authentication inputs omit transaction nullifiers: {0:?}")]
144 MissingNullifiers(Vec<Nullifier>),
145 #[error("nullifiers already exist: {0:?}")]
146 NullifiersAlreadyExist(Vec<Nullifier>),
147}
148
149#[derive(Clone, Debug, PartialEq)]
160pub struct AuthenticatedTransaction {
161 inner: Arc<ProvenTransaction>,
162 store_account_state: Option<Word>,
166 notes_authenticated_by_store: HashSet<NoteId>,
169 authentication_height: BlockNumber,
173}
174
175impl AuthenticatedTransaction {
176 pub fn new_unchecked(
185 tx: Arc<ProvenTransaction>,
186 inputs: TransactionInputs,
187 ) -> Result<AuthenticatedTransaction, TransactionAuthenticationError> {
188 if inputs.account_id != tx.account_id() {
189 return Err(TransactionAuthenticationError::AccountIdMismatch {
190 expected: tx.account_id(),
191 actual: inputs.account_id,
192 });
193 }
194
195 let mut missing_nullifiers = Vec::new();
196 let mut nullifiers_already_spent = Vec::new();
197 for nullifier in tx.nullifiers() {
198 match inputs.nullifiers.get(&nullifier) {
199 None => missing_nullifiers.push(nullifier),
200 Some(Some(_)) => nullifiers_already_spent.push(nullifier),
201 Some(None) => {},
202 }
203 }
204 if !missing_nullifiers.is_empty() {
205 return Err(TransactionAuthenticationError::MissingNullifiers(missing_nullifiers));
206 }
207 if !nullifiers_already_spent.is_empty() {
208 return Err(TransactionAuthenticationError::NullifiersAlreadyExist(
209 nullifiers_already_spent,
210 ));
211 }
212
213 Ok(AuthenticatedTransaction {
214 inner: tx,
215 notes_authenticated_by_store: inputs.found_unauthenticated_notes,
216 authentication_height: inputs.current_block_height,
217 store_account_state: inputs.account_commitment,
218 })
219 }
220
221 pub fn id(&self) -> TransactionId {
222 self.inner.id()
223 }
224
225 pub fn account_id(&self) -> AccountId {
226 self.inner.account_id()
227 }
228
229 pub fn account_update(&self) -> &TxAccountUpdate {
230 self.inner.account_update()
231 }
232
233 pub fn store_account_state(&self) -> Option<Word> {
234 self.store_account_state
235 }
236
237 pub fn authentication_height(&self) -> BlockNumber {
238 self.authentication_height
239 }
240
241 pub fn nullifiers(&self) -> impl Iterator<Item = Nullifier> + '_ {
242 self.inner.nullifiers()
243 }
244
245 pub fn output_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
246 self.inner.output_notes().iter().map(OutputNote::id)
247 }
248
249 pub fn output_note_count(&self) -> usize {
250 self.inner.output_notes().num_notes()
251 }
252
253 pub fn input_note_count(&self) -> usize {
254 self.inner.input_notes().num_notes() as usize
255 }
256
257 pub fn reference_block(&self) -> (BlockNumber, Word) {
258 (self.inner.ref_block_num(), self.inner.ref_block_commitment())
259 }
260
261 pub fn unauthenticated_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
263 self.inner
264 .unauthenticated_notes()
265 .map(NoteHeader::id)
266 .filter(|note_id| !self.notes_authenticated_by_store.contains(note_id))
267 }
268
269 pub fn mark_notes_authenticated(&mut self, notes: impl IntoIterator<Item = NoteId>) {
272 self.notes_authenticated_by_store.extend(notes);
273 }
274
275 pub fn proven_transaction(&self) -> Arc<ProvenTransaction> {
276 Arc::clone(&self.inner)
277 }
278
279 pub fn expires_at(&self) -> BlockNumber {
280 self.inner.expiration_block_num()
281 }
282
283 pub fn raw_proven_transaction(&self) -> &ProvenTransaction {
284 &self.inner
285 }
286}
287
288impl From<AuthenticatedTransaction> for sequencer::AuthenticatedTransaction {
292 fn from(value: AuthenticatedTransaction) -> Self {
293 Self {
294 transaction: Some(value.inner.as_ref().into()),
295 store_account_state: value.store_account_state.map(Into::into),
296 notes_authenticated_by_store: value
297 .notes_authenticated_by_store
298 .into_iter()
299 .map(|note_id| note_id.as_word().into())
300 .collect(),
301 authentication_height: value.authentication_height.as_u32(),
302 }
303 }
304}
305
306impl BuildUnchecked for sequencer::DecodedAuthenticatedTransaction {
307 type Output = AuthenticatedTransaction;
308 type Error = ConversionError;
309
310 fn build_unchecked(self) -> Result<Self::Output, Self::Error> {
313 let inner = self.transaction.build_unchecked().context("transaction")?;
316 Ok(AuthenticatedTransaction {
317 inner: Arc::new(inner),
318 store_account_state: self.store_account_state.into_inner(),
319 notes_authenticated_by_store: self
320 .notes_authenticated_by_store
321 .into_inner()
322 .into_iter()
323 .map(NoteId::from_raw)
324 .collect(),
325 authentication_height: self.authentication_height.into(),
326 })
327 }
328}