Skip to main content

miden_standards/note/
owner_action.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::crypto::rand::FeltRng;
6use miden_protocol::errors::NoteError;
7use miden_protocol::note::{
8    Note,
9    NoteAssets,
10    NoteAttachment,
11    NoteAttachments,
12    NoteRecipient,
13    NoteScript,
14    NoteScriptRoot,
15    NoteStorage,
16    NoteTag,
17    NoteType,
18    PartialNoteMetadata,
19};
20use miden_protocol::utils::sync::LazyLock;
21use miden_protocol::{Felt, Word};
22
23use crate::StandardsLib;
24
25// NOTE SCRIPT
26// ================================================================================================
27
28/// Path to the OWNER_ACTION note script procedure in the standards library.
29const OWNER_ACTION_SCRIPT_PATH: &str = "::miden::standards::notes::owner_action::main";
30
31// Initialize the OWNER_ACTION note script only once.
32static OWNER_ACTION_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
33    let standards_lib = StandardsLib::default();
34    let path = Path::new(OWNER_ACTION_SCRIPT_PATH);
35    NoteScript::from_library_reference(standards_lib.as_ref(), path)
36        .expect("Standards library contains OWNER_ACTION note script procedure")
37});
38
39// OWNER ACTION
40// ================================================================================================
41
42/// A management action of the [`Ownable2Step`](crate::account::access::Ownable2Step) component
43/// that an [`OwnerActionNote`] triggers on the account that consumes it.
44///
45/// The action, together with its arguments, is encoded into the note's storage (see
46/// [`NoteStorage`] conversion below). Because the storage is fixed at note creation and bound into
47/// the note commitment, the authorized party is the note sender: the consuming account's
48/// `Ownable2Step` procedures authorize against `active_note::get_sender`.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum OwnerAction {
51    /// Nominate `new_owner` as the new owner (two-step transfer; the nominee must later accept via
52    /// [`OwnerAction::AcceptOwnership`]). A `new_owner` of `None` cancels any pending nomination.
53    /// Only the current owner is authorized.
54    TransferOwnership { new_owner: Option<AccountId> },
55    /// Accept a pending ownership nomination. Only the nominated owner is authorized.
56    AcceptOwnership,
57    /// Renounce ownership, leaving the component permanently ownerless. Only the current owner is
58    /// authorized.
59    RenounceOwnership,
60}
61
62impl OwnerAction {
63    // SELECTORS
64    // --------------------------------------------------------------------------------------------
65
66    // Action selectors stored in the first storage item. Keep in sync with `owner_action.masm`.
67    const SELECTOR_TRANSFER_OWNERSHIP: u8 = 0;
68    const SELECTOR_ACCEPT_OWNERSHIP: u8 = 1;
69    const SELECTOR_RENOUNCE_OWNERSHIP: u8 = 2;
70
71    /// Returns the note storage values encoding this action, laid out as `[selector, ..args]`.
72    fn to_storage_values(self) -> Vec<Felt> {
73        match self {
74            OwnerAction::TransferOwnership { new_owner } => {
75                // [selector, new_owner_suffix, new_owner_prefix]; the zero address (0, 0) is the
76                // cancel value understood by `ownable2step::transfer_ownership`.
77                let (suffix, prefix) = match new_owner {
78                    Some(id) => (id.suffix(), id.prefix().as_felt()),
79                    None => (Felt::ZERO, Felt::ZERO),
80                };
81                vec![Felt::from(Self::SELECTOR_TRANSFER_OWNERSHIP), suffix, prefix]
82            },
83            OwnerAction::AcceptOwnership => {
84                vec![Felt::from(Self::SELECTOR_ACCEPT_OWNERSHIP)]
85            },
86            OwnerAction::RenounceOwnership => {
87                vec![Felt::from(Self::SELECTOR_RENOUNCE_OWNERSHIP)]
88            },
89        }
90    }
91}
92
93impl From<OwnerAction> for NoteStorage {
94    fn from(action: OwnerAction) -> Self {
95        NoteStorage::new(action.to_storage_values())
96            .expect("number of storage items should not exceed max storage items")
97    }
98}
99
100// OWNER ACTION NOTE
101// ================================================================================================
102
103/// An OwnerAction note: triggers an [`Ownable2Step`](crate::account::access::Ownable2Step)
104/// management action on the account that consumes it.
105///
106/// A single note script dispatches on a selector in the note's storage to one of the component's
107/// management procedures (`transfer_ownership`, `accept_ownership`, `renounce_ownership`). All
108/// authorization is enforced by those procedures against the note sender, so the note carries no
109/// assets and its authorization is bound to `sender` at creation time.
110///
111/// The note is always public (for network execution) and tagged for `account` — the account
112/// carrying the `Ownable2Step` component whose ownership state is being managed. The `sender` is
113/// the account authorized for the selected action: the current owner for `TransferOwnership` /
114/// `RenounceOwnership`, or the nominated owner for `AcceptOwnership`.
115///
116/// Construct one with the [builder](OwnerActionNote::builder); convert it into a protocol [`Note`]
117/// infallibly via `Note::from`.
118#[derive(Debug, Clone)]
119pub struct OwnerActionNote {
120    sender: AccountId,
121    account: AccountId,
122    action: OwnerAction,
123    serial_number: Word,
124    attachments: NoteAttachments,
125}
126
127#[bon::bon]
128impl OwnerActionNote {
129    /// Builds a new [`OwnerActionNote`] that triggers `action` on `account`.
130    ///
131    /// # Errors
132    ///
133    /// Returns an error if the attachments exceed their protocol limit (see
134    /// [`NoteAttachments::new`]).
135    #[builder]
136    pub fn new(
137        #[builder(field)] attachments: Vec<NoteAttachment>,
138        sender: AccountId,
139        account: AccountId,
140        action: OwnerAction,
141        serial_number: Word,
142    ) -> Result<Self, NoteError> {
143        let attachments = NoteAttachments::new(attachments)?;
144
145        Ok(Self {
146            sender,
147            account,
148            action,
149            serial_number,
150            attachments,
151        })
152    }
153}
154
155impl OwnerActionNote {
156    // CONSTANTS
157    // --------------------------------------------------------------------------------------------
158
159    /// Upper bound on the number of storage items of an OwnerAction note.
160    ///
161    /// The layout is variable: `TransferOwnership` uses 3 items (`[selector, new_owner_suffix,
162    /// new_owner_prefix]`), while `AcceptOwnership` / `RenounceOwnership` use 1 (`[selector]`).
163    pub const MAX_NUM_STORAGE_ITEMS: usize = 3;
164
165    // PUBLIC ACCESSORS
166    // --------------------------------------------------------------------------------------------
167
168    /// Returns the script of the OwnerAction note.
169    pub fn script() -> NoteScript {
170        OWNER_ACTION_SCRIPT.clone()
171    }
172
173    /// Returns the OwnerAction note script root.
174    pub fn script_root() -> NoteScriptRoot {
175        OWNER_ACTION_SCRIPT.root()
176    }
177
178    /// Returns the account ID of the note's sender (the account authorized for the action).
179    pub fn sender(&self) -> AccountId {
180        self.sender
181    }
182
183    /// Returns the account ID of the managed account (the account the note is tagged for).
184    pub fn account(&self) -> AccountId {
185        self.account
186    }
187
188    /// Returns the management action carried by the note.
189    pub fn action(&self) -> OwnerAction {
190        self.action
191    }
192
193    /// Returns the note's serial number.
194    pub fn serial_number(&self) -> Word {
195        self.serial_number
196    }
197
198    /// Returns the attachments carried by the note.
199    pub fn attachments(&self) -> &NoteAttachments {
200        &self.attachments
201    }
202}
203
204// BUILDER EXTENSIONS
205// ================================================================================================
206
207impl<S: owner_action_note_builder::State> OwnerActionNoteBuilder<S> {
208    /// Adds a single attachment to the note.
209    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
210        self.attachments.push(attachment.into());
211        self
212    }
213
214    /// Adds multiple attachments to the note.
215    pub fn attachments(
216        mut self,
217        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
218    ) -> Self {
219        self.attachments.extend(attachments.into_iter().map(Into::into));
220        self
221    }
222}
223
224impl<S: owner_action_note_builder::State> OwnerActionNoteBuilder<S>
225where
226    S::SerialNumber: owner_action_note_builder::IsUnset,
227{
228    /// Draws a serial number from `rng` and sets it on the builder.
229    pub fn generate_serial_number(
230        self,
231        rng: &mut impl FeltRng,
232    ) -> OwnerActionNoteBuilder<owner_action_note_builder::SetSerialNumber<S>> {
233        self.serial_number(rng.draw_word())
234    }
235}
236
237// CONVERSIONS
238// ================================================================================================
239
240impl From<OwnerActionNote> for Note {
241    fn from(note: OwnerActionNote) -> Self {
242        // OwnerAction notes carry no assets and are always public for network execution; the action
243        // and its arguments live in the note storage.
244        let metadata = PartialNoteMetadata::new(note.sender, NoteType::Public)
245            .with_tag(NoteTag::with_account_target(note.account));
246        let recipient = NoteRecipient::new(
247            note.serial_number,
248            OwnerActionNote::script(),
249            NoteStorage::from(note.action),
250        );
251
252        Note::with_attachments(NoteAssets::default(), metadata, recipient, note.attachments)
253    }
254}
255
256// TESTS
257// ================================================================================================
258
259#[cfg(test)]
260mod tests {
261    use miden_protocol::account::AccountType;
262    use miden_protocol::crypto::rand::RandomCoin;
263
264    use super::*;
265
266    fn account_id(seed: u8) -> AccountId {
267        AccountId::builder()
268            .account_type(AccountType::Public)
269            .build_with_seed([seed; 32])
270    }
271
272    /// The builder produces a public, asset-less note tagged for the managed account.
273    #[test]
274    fn builder_builds_owner_action_note() {
275        let mut rng = RandomCoin::new(Word::empty());
276        let managed = account_id(1);
277        let owner = account_id(2);
278        let new_owner = account_id(3);
279
280        let note = OwnerActionNote::builder()
281            .sender(owner)
282            .account(managed)
283            .action(OwnerAction::TransferOwnership { new_owner: Some(new_owner) })
284            .generate_serial_number(&mut rng)
285            .build()
286            .unwrap();
287
288        assert_eq!(note.sender(), owner);
289        assert_eq!(note.account(), managed);
290
291        let note = Note::from(note);
292        assert_eq!(note.metadata().note_type(), NoteType::Public);
293        assert_eq!(note.metadata().tag(), NoteTag::with_account_target(managed));
294        assert_eq!(note.assets().num_assets(), 0);
295    }
296
297    /// `TransferOwnership` storage is `[selector, new_owner_suffix, new_owner_prefix]`.
298    #[test]
299    fn transfer_ownership_storage_layout() {
300        let new_owner = account_id(3);
301        let storage =
302            NoteStorage::from(OwnerAction::TransferOwnership { new_owner: Some(new_owner) });
303
304        assert_eq!(
305            storage.items(),
306            &[
307                Felt::from(OwnerAction::SELECTOR_TRANSFER_OWNERSHIP),
308                new_owner.suffix(),
309                new_owner.prefix().as_felt(),
310            ]
311        );
312    }
313
314    /// A cancelling `TransferOwnership` encodes the zero address.
315    #[test]
316    fn cancel_transfer_ownership_storage_layout() {
317        let storage = NoteStorage::from(OwnerAction::TransferOwnership { new_owner: None });
318
319        assert_eq!(
320            storage.items(),
321            &[Felt::from(OwnerAction::SELECTOR_TRANSFER_OWNERSHIP), Felt::ZERO, Felt::ZERO]
322        );
323    }
324
325    /// `AcceptOwnership` / `RenounceOwnership` storage is a single selector item.
326    #[test]
327    fn accept_and_renounce_storage_layout() {
328        let accept = NoteStorage::from(OwnerAction::AcceptOwnership);
329        assert_eq!(accept.items(), &[Felt::from(OwnerAction::SELECTOR_ACCEPT_OWNERSHIP)]);
330
331        let renounce = NoteStorage::from(OwnerAction::RenounceOwnership);
332        assert_eq!(renounce.items(), &[Felt::from(OwnerAction::SELECTOR_RENOUNCE_OWNERSHIP)]);
333    }
334}