Skip to main content

miden_node_proto/domain/
sequencer.rs

1//! Domain types for the sequencer API.
2
3use 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    /// Verify transaction proofs at the supplied security level and decode store inputs. Check the
26    /// batch proof against the proposed batch. The caller must trust the sender's store
27    /// authentication data. The mempool checks dependencies, conflicts, and expiration.
28    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/// Information needed from the store to verify a transaction.
44#[derive(Debug)]
45pub struct TransactionInputs {
46    /// The account ID.
47    pub account_id: AccountId,
48    /// The account commitment in the store.
49    pub account_commitment: Option<Word>,
50    /// Map each nullifier to the block that consumed the note, or `None` if it is unspent.
51    ///
52    /// The wire format uses 0 to encode `None`.
53    pub nullifiers: HashMap<Nullifier, Option<NonZeroU32>>,
54    /// IDs of unauthenticated notes that are present in the store.
55    ///
56    /// These notes were committed after the transaction was created.
57    pub found_unauthenticated_notes: HashSet<NoteId>,
58    /// The current block height.
59    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/// The supplied store inputs do not authenticate the transaction.
139#[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/// A transaction with store authentication data supplied by a trusted caller.
150///
151/// The caller must verify the proof. [`Self::new_unchecked`] checks the supplied nullifier results
152/// and records which input notes the store has committed. Protobuf conversion trusts the sender's
153/// authentication data. The mempool resolves remaining note and account dependencies and checks
154/// conflicts and expiration.
155///
156/// Clones share the transaction through an [`Arc`].
157///
158/// Authentication is valid only at the recorded chain height.
159#[derive(Clone, Debug, PartialEq)]
160pub struct AuthenticatedTransaction {
161    inner: Arc<ProvenTransaction>,
162    /// The account state from the store [inputs](TransactionInputs).
163    ///
164    /// Pending transactions can cause this to differ from the initial transaction state.
165    store_account_state: Option<Word>,
166    /// Input notes that were unauthenticated when the transaction was proven. Store inputs or
167    /// committed mempool history have since authenticated these notes.
168    notes_authenticated_by_store: HashSet<NoteId>,
169    /// The chain height at authentication.
170    ///
171    /// FIXME: Include the block commitment to identify the exact state used for authentication.
172    authentication_height: BlockNumber,
173}
174
175impl AuthenticatedTransaction {
176    /// Check that the supplied store inputs match the account and contain an unspent result for
177    /// every transaction nullifier.
178    ///
179    /// The caller must verify the transaction proof and supply trusted store inputs.
180    ///
181    /// # Errors
182    ///
183    /// Return an error if the account ID differs or any transaction nullifier is missing or spent.
184    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    /// Return input note IDs that neither the transaction nor committed state authenticates.
262    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    /// Mark these note IDs as authenticated by committed state. The caller must check that the
270    /// notes belong to committed state.
271    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
288// PROTO CONVERSIONS
289// ================================================================================================
290
291impl 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    /// Construct a transaction authenticated by a trusted sequencer client. The caller must ensure
311    /// that the client verified and authenticated the transaction.
312    fn build_unchecked(self) -> Result<Self::Output, Self::Error> {
313        // SAFETY: The caller must trust the sender to verify the transaction proof and store
314        // authentication data. This constructor does not establish that trust.
315        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}