-
Notifications
You must be signed in to change notification settings - Fork 124
Description
Motivation
Having a type that represents a note, e.g. P2idNote, is useful for a few reasons:
- It represents a single point where P2ID related functionality can be defined, e.g. building recipients or note inputs or a getter for the note script root.
- In the future, these types can implement
NoteInterface. This would allow checking compatibility with anAccountInterface, as an example. This interface should be covered by a separate issue. - It allows writing a builder API for convenient creation of this type.
- This is useful because many parameters for notes are optional:
- note type can default to private
- attachment is optional
- assets can be empty by default
- reclaim or timelock functionality on P2IDE is optional
- The functions we currently have for creating them, like
create_p2ide_noterequire all parameters which makes it cumbersome to use. This is a good use case for builder-like APIs that automatically set the defaults.
- This is useful because many parameters for notes are optional:
- Once a
P2idNoteis created, it should be possible to convert it intoNoteinfallibly, e.g.Note::from(p2id_note), allowing abstractions that takeimpl Into<Note>. - With
NoteExecutionHintmoving from protocol to standards, we could add a network account target-based extension trait that builds on top ofNoteInterfacethat has anexecution_hint(&self) -> NoteExecutionHintmethod. It returns the hint for this note. This would be particularly useful for the P2IDE note to encode its timelock into an execution hint. It would be a way for standard notes to implement the "network account target standard".
P2ID Example
Note that NoteInputs were recently renamed to NoteStorage which should be updated in the following example.
// Imports omitted
// P2ID Inputs
// ----------------------------------------------
// A type for a note's inputs is likely useful: A Swap note could take the P2IdInputs
// for the payback note as a parameter.
// Also mentioned in https://github.com/0xMiden/miden-base/pull/2249#discussion_r2679329498.
#[derive(Debug, Clone, Copy)]
pub struct P2IdInputs {
target: AccountId,
}
impl P2IdInputs {
/// Consumes the inputs and returns a P2ID [`NoteRecipient`] with the provided serial number.
///
/// Notes created with this recipient will be P2ID notes consumable by the specified target
/// account stored in [`P2IdInputs`].
///
/// TODO: This could be a trait to make all standard note inputs convertible into `NoteRecipient`
/// when a serial is provided - though not clear yet if this abstraction is needed, so doesn't
/// need to be part of the initial version.
pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
NoteRecipient::new(serial_num, WellKnownNote::P2ID.script(), NoteInputs::from(self))
}
}
impl From<P2IdInputs> for NoteInputs {
fn from(inputs: P2IdInputs) -> Self {
NoteInputs::new(vec![inputs.target.suffix(), inputs.target.prefix().as_felt()])
.expect("number of inputs should be below max")
}
}
// P2ID Note
// ----------------------------------------------
#[derive(Debug, Clone, bon::Builder)]
#[builder(finish_fn(vis = "", name = build_internal))]
pub struct P2IdNote {
sender: AccountId,
#[builder(name = target, with = |target: AccountId| P2IdInputs { target })]
inputs: P2IdInputs,
serial_number: Word,
#[builder(default = NoteType::Private)]
note_type: NoteType,
#[builder(default)]
assets: NoteAssets,
#[builder(default)]
attachment: NoteAttachment,
}
impl P2IdNote {
pub fn sender(&self) -> AccountId {
self.sender
}
// TODO: Getters for all fields, written by hand.
// ...
}
// Extend the builder with a custom method to allow generating a serial
// number from an RNG. This is optional, but nice to have for basically any note.
impl<S: State> P2IdNoteBuilder<S>
where
S::SerialNumber: IsUnset,
{
pub fn serial_number_from_rng(
self,
rng: &mut impl FeltRng,
) -> P2IdNoteBuilder<SetSerialNumber<S>> {
self.serial_number(rng.draw_word())
}
}
// Use a custom build method for custom validation.
impl<S: IsComplete> P2IdNoteBuilder<S> {
pub fn build(self) -> Result<P2IdNote, NoteError> {
let note = self.build_internal();
// This is just an example.
if note.inputs.target.storage_mode().is_network() && note.note_type != NoteType::Public {
return Err(NoteError::other("network targets require public notes"));
}
Ok(note)
}
}
// Converting to a Note should be infallible.
impl From<P2IdNote> for Note {
fn from(note: P2IdNote) -> Self {
let recipient = note.inputs.into_recipient(note.serial_number);
let tag = NoteTag::with_account_target(note.inputs.target);
let metadata =
NoteMetadata::new(note.sender, note.note_type, tag).with_attachment(note.attachment);
Note::new(note.assets, metadata, recipient)
}
}
// Tests showcasing usage
// ----------------------------------------------
#[cfg(test)]
mod tests2 {
use miden_processor::crypto::RpoRandomCoin;
use miden_protocol::Word;
use miden_protocol::asset::FungibleAsset;
use miden_protocol::note::{Note, NoteAssets, NoteAttachment, NoteAttachmentScheme, NoteType};
use miden_protocol::testing::account_id::{
ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
ACCOUNT_ID_SENDER,
};
use crate::note::P2IdNote;
#[test]
fn test_p2id_builder() -> anyhow::Result<()> {
let sender = ACCOUNT_ID_SENDER.try_into()?;
let target = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?;
// Minimal example
let p2id_note0 = P2IdNote::builder()
.sender(sender)
.target(target)
.serial_number_from_rng(&mut RpoRandomCoin::new(Word::empty()))
.build()?;
// Full example
let p2id_note1 = P2IdNote::builder()
.sender(sender)
.target(target)
.serial_number(Word::empty())
.assets(NoteAssets::new(vec![FungibleAsset::mock(34)])?)
.attachment(NoteAttachment::new_word(
NoteAttachmentScheme::default(),
Word::from([1, 2, 3, 4u32]),
))
.note_type(NoteType::Public)
.build()?;
let note0 = Note::from(p2id_note0.clone());
let note1 = Note::from(p2id_note1.clone());
assert_eq!(note0.assets(), &p2id_note0.assets);
assert_eq!(note1.assets(), &p2id_note1.assets);
Ok(())
}
}Bon Crate
Is bon absolutely necessary? No, it's just convenient to take away boilerplate which we would have to implement for every standard note (a lot). Plus, the typestate pattern is a nice bonus, ensuring that all fields are set and the absence of fields doesn't have to be handled at runtime.
Semver Compatibility
When using bon::Builder, adding a new field to P2idNote that implements Default is not a breaking change, which is a nice bonus. Tested by adding:
#[builder(default)]
is_encrypted: bool,and bumping the version from 0.13.0 to 0.13.1, then running cargo semver-checks -p miden-standards --baseline-rev <prev_commit>, which printed "no semver update required".
Conclusion
I think a builder pattern makes sense to use for notes:
- Notes have many optional parameters and this makes a single create function cumbersome.
- Having a type for every note will likely be required for implementing
NoteInterface.
bon would take away most of the pain to create these builders, so using it seems ideal to me.