miden_standards/account/auth/tx_fee_collector.rs
1use miden_protocol::account::auth::{AuthScheme, PublicKey};
2use miden_protocol::account::component::{
3 AccountComponentCode,
4 AccountComponentMetadata,
5 SchemaType,
6 StorageSchema,
7 StorageSlotSchema,
8};
9use miden_protocol::account::{
10 AccountComponent,
11 AccountComponentName,
12 AccountId,
13 StorageSlot,
14 StorageSlotName,
15};
16use miden_protocol::crypto::dsa::{ecdsa_k256_keccak, falcon512_poseidon2};
17use miden_protocol::note::{NoteTag, NoteType};
18use miden_protocol::utils::sync::LazyLock;
19use miden_protocol::{Felt, Hasher, Word};
20
21use super::Approver;
22use crate::account::account_component_code;
23
24account_component_code!(AUTH_TX_FEE_COLLECTOR_CODE, "miden-standards-auth-tx-fee-collector.masp");
25
26// CONSTANTS
27// ================================================================================================
28
29static PUBKEY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
30 StorageSlotName::new("miden::standards::auth::tx_fee_collector::pub_key")
31 .expect("storage slot name should be valid")
32});
33
34static SIGNATURE_SCHEME_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
35 StorageSlotName::new("miden::standards::auth::tx_fee_collector::signature_scheme")
36 .expect("storage slot name should be valid")
37});
38
39/// An [`AccountComponent`] implementing the authentication scheme of a TX_FEE collector account: an
40/// account that forwards the assets of the notes it consumes and is never changed by a
41/// transaction.
42///
43/// It exports the procedure `auth_tx_fee_collector`, which:
44/// - Creates a P2ID note for the target given by the auth args (see [`Self::auth_args`]) and moves
45/// the single asset of every consumed note into it, straight from the note; the account's vault
46/// is never touched. A transaction consuming no notes is rejected, except for the one creating
47/// the account, which then creates no P2ID note.
48/// - Asserts the account's commitment is the one it had at the start of the transaction.
49/// - Never increments the nonce, except once when the account is created. That transaction is
50/// checked against the vault instead of the full commitment, so the account cannot be created
51/// holding assets.
52/// - Verifies a signature over the transaction summary against the public key in storage, under the
53/// signature scheme stored alongside it.
54/// - Creates no TX_FEE note.
55///
56/// The account is stateless after deployment, and transactions against it can be built
57/// concurrently. A batch builder collects the TX_FEE notes of many batches this way: each
58/// transaction consumes one batch's fee notes and forwards their assets to the builder's own
59/// account.
60///
61/// Replay protection comes from the input notes the signed transaction summary binds: a
62/// transaction leaving the account unchanged must consume at least one input note to be valid, and
63/// a note can be consumed once.
64///
65/// Like every auth component, it is installed alongside at least one other component, typically
66/// [`BasicWallet`](crate::account::wallets::BasicWallet).
67pub struct AuthTxFeeCollector {
68 approver: Approver,
69}
70
71impl AuthTxFeeCollector {
72 /// The name of the component.
73 pub const NAME: &'static str = "miden::standards::auth::tx_fee_collector";
74
75 /// Returns the canonical [`AccountComponentName`] of this component.
76 pub const fn name() -> AccountComponentName {
77 AccountComponentName::from_static_str(Self::NAME)
78 }
79
80 /// Returns the [`AccountComponentCode`] of this component.
81 pub fn code() -> &'static AccountComponentCode {
82 &AUTH_TX_FEE_COLLECTOR_CODE
83 }
84
85 /// Creates a new [`AuthTxFeeCollector`] component with the given approver.
86 pub fn new(approver: Approver) -> Self {
87 Self { approver }
88 }
89
90 /// Creates a new [`AuthTxFeeCollector`] component using the Falcon512Poseidon2 signature
91 /// scheme.
92 ///
93 /// The public key commitment is derived from the provided Falcon512 public key.
94 pub fn falcon512_poseidon2(pub_key: falcon512_poseidon2::PublicKey) -> Self {
95 Self {
96 approver: Approver::new(pub_key.into(), AuthScheme::Falcon512Poseidon2),
97 }
98 }
99
100 /// Creates a new [`AuthTxFeeCollector`] component using the EcdsaK256Keccak signature scheme.
101 ///
102 /// The public key commitment is derived from the provided ECDSA K256 public key.
103 ///
104 /// # Privacy
105 /// This scheme discloses the signer's public key and signature at proving time and
106 /// therefore does not provide public-key privacy. See
107 /// [`AuthScheme::EcdsaK256Keccak`][scheme] for details, and prefer
108 /// [`falcon512_poseidon2`](Self::falcon512_poseidon2) if signer-key privacy is required.
109 ///
110 /// [scheme]: miden_protocol::account::auth::AuthScheme::EcdsaK256Keccak
111 pub fn ecdsa_k256_keccak(pub_key: ecdsa_k256_keccak::PublicKey) -> Self {
112 Self {
113 approver: Approver::new(pub_key.into(), AuthScheme::EcdsaK256Keccak),
114 }
115 }
116
117 /// Creates a new [`AuthTxFeeCollector`] component from a [`PublicKey`].
118 ///
119 /// The authentication scheme and public key commitment are derived from the provided key.
120 pub fn from_public_key(pub_key: PublicKey) -> Self {
121 Self {
122 approver: Approver::new(pub_key.to_commitment(), pub_key.auth_scheme()),
123 }
124 }
125
126 /// Returns the approver of this component.
127 pub fn approver(&self) -> Approver {
128 self.approver
129 }
130
131 /// Returns the auth args that make the auth procedure create its P2ID note for `target`.
132 ///
133 /// The word is `[target_id_suffix, target_id_prefix, tag, note_type]`, with the tag derived
134 /// from `target` as by [`NoteTag::with_account_target`]. Pass it as the transaction's auth
135 /// args.
136 pub fn auth_args(target: AccountId, note_type: NoteType) -> Word {
137 Word::new([
138 target.suffix(),
139 target.prefix().as_felt(),
140 Felt::from(NoteTag::with_account_target(target)),
141 Felt::from(note_type),
142 ])
143 }
144
145 /// Derives the serial number of the P2ID note the auth procedure creates in a transaction
146 /// with the given auth args and input notes commitment.
147 ///
148 /// The serial number is `hash(auth_args || input_notes_commitment)`, which is unique per
149 /// transaction since no two valid transactions consume the same notes. Together with the
150 /// target it lets the note's recipient be computed before the transaction executes.
151 ///
152 /// This derivation must be kept in sync with `forward_note_assets` in the component's MASM
153 /// code.
154 pub fn derive_serial_number(auth_args: Word, input_notes_commitment: Word) -> Word {
155 Hasher::merge(&[auth_args, input_notes_commitment])
156 }
157
158 /// Returns the [`StorageSlotName`] where the public key is stored.
159 pub fn public_key_slot() -> &'static StorageSlotName {
160 &PUBKEY_SLOT_NAME
161 }
162
163 /// Returns the [`StorageSlotName`] where the signature scheme is stored.
164 pub fn signature_scheme_slot() -> &'static StorageSlotName {
165 &SIGNATURE_SCHEME_SLOT_NAME
166 }
167
168 /// Returns the storage slot schema for the public key slot.
169 pub fn public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
170 (
171 Self::public_key_slot().clone(),
172 StorageSlotSchema::value("Public key commitment", SchemaType::pub_key()),
173 )
174 }
175
176 /// Returns the storage slot schema for the signature scheme slot.
177 pub fn signature_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
178 (
179 Self::signature_scheme_slot().clone(),
180 StorageSlotSchema::value("Signature scheme", SchemaType::auth_scheme()),
181 )
182 }
183
184 /// Returns the [`AccountComponentMetadata`] for this component.
185 pub fn component_metadata() -> AccountComponentMetadata {
186 let storage_schema = StorageSchema::new(vec![
187 Self::public_key_slot_schema(),
188 Self::signature_scheme_slot_schema(),
189 ])
190 .expect("storage schema should be valid");
191
192 AccountComponentMetadata::new(Self::NAME)
193 .with_description("TX_FEE collector authentication component")
194 .with_storage_schema(storage_schema)
195 }
196}
197
198impl From<AuthTxFeeCollector> for AccountComponent {
199 fn from(collector: AuthTxFeeCollector) -> Self {
200 let metadata = AuthTxFeeCollector::component_metadata();
201
202 let storage_slots = vec![
203 StorageSlot::with_value(
204 AuthTxFeeCollector::public_key_slot().clone(),
205 collector.approver.pub_key().into(),
206 ),
207 StorageSlot::with_value(
208 AuthTxFeeCollector::signature_scheme_slot().clone(),
209 Word::from([collector.approver.auth_scheme().as_u8(), 0, 0, 0]),
210 ),
211 ];
212
213 AccountComponent::new(AuthTxFeeCollector::code().clone(), storage_slots, metadata).expect(
214 "AuthTxFeeCollector component should satisfy the requirements of a valid account \
215 component",
216 )
217 }
218}