-
Notifications
You must be signed in to change notification settings - Fork 124
Description
Coming out of #1449, this is a concrete proposal for how to refactor the NoteTag as well as the aux value in NoteMetadata to NoteAttachment, that allows storing more data than a single Felt.
The new structure could look like this, expressed in Rust with explanatory comments:
// We no longer need the encrypted note type because an encrypted note is a private note with its
// encrypted content attached. At the protocol level, there is no need to represent encrypted notes.
// We can provide a convenience procedure in `miden` lib to create encrypted notes that would create
// a private note, encrypt the note content and attach it to the note.
//
// At the same time, we gain the network note type. This is needed at some level (meaning not
// necessarily here) to differentiate local and network note metadata in the tx kernel so it knows
// how to encode these two (because they are different - see below).
pub enum NoteType {
Private,
Public,
Network,
}
// Note metadata is different for local and network notes.
//
// Encodes to a total of two words METADATA_0 and METADATA_1 and the note commitment is updated to
// be computed as hash(NOTE_ID, hash(METADATA_0, METADATA_1))
pub enum NoteMetadata {
Local(LocalNoteMetadata),
Network(NetworkNoteMetadata),
}
/// The optional attachment to a note intended for local execution.
pub enum NoteAttachment {
/// A raw [`Word`] that is used as-is when computing the note commitment.
Raw(Word),
/// Arbitrary data whose sequential hash is used when computing the note commitment.
///
/// At most 2^12 words (= 2^14 felts = 2^16 bytes assuming 4 bytes per felt).
/// (Let me know if you have different opinions, I picked this rather arbitrarily)
Commitment(Vec<Felt>),
}
/// Represents a tag for a note intended for local execution.
///
/// Encodes to [tag_variant (1 bit) | is_public (1 bit) | tag_data (30 bits)]
/// `is_public` defines whether the note tag requires the note to be public.
/// This addresses the comment from here:
/// https://github.com/0xMiden/miden-base/pull/1322#discussion_r2108348981
///
/// It would be nice to not need the `is_public`, and my question is whether this could be done
/// by allowing clients to filter at the node API level by tag + note visibility?
///
/// Despite the space being available in note metadata, I don't think there's value in increasing
/// the note tag width beyond 32 bits, as 32 bits already allow for narrowing down the number of
/// notes to a high enough degree.
pub enum NoteTag {
/// The partial account ID prefix of the intended receiver of the note.
PartialAccountId {
is_public: bool,
/// The partial account id.
///
/// Uses https://docs.rs/ux/ - we could do our own newtype wrappers as well.
id: ux::u30,
},
/// A specific use case of a note.
///
/// For instance, a SWAP note is not targeted at a specific account for consumption, instead it
/// is a note for a certain use case.
///
/// For such a SWAP note, this could encode the first 14 bits of the SWAP script root as a
/// use case identifier (leaks the note's script, but any use case identifier essentially does
/// that and this avoids having to come up with manual use case IDs), and the remaining 16 bits
/// using 8 bits of the offered asset faucet ID and 8 bits of the requested asset faucet ID.
UseCase {
is_public: bool,
/// The identifier of the use case.
id: ux::u14,
/// The payload of the use case.
payload: u16,
},
}
/// The metadata of a note for consumption in a local transaction.
///
/// Encodes to the following two words:
///
/// ```text
/// 0th felt: [sender_id_suffix (56 bits) | 6 zero bits | note_type (2 bits)]
/// 1st felt: [sender_id_prefix (64 bits)]
/// 2nd felt: [25 zero bits | has_attachment (1 bit) | note_execution_hint_tag (6 bits)
/// | note_execution_hint_payload (32 bits)]
/// 3rd felt: [32 zero bits | note_tag (32 bits)]
/// 4th-7th felt: NOTE_ATTACHMENT (either its raw or commitment representation)
/// ```
///
/// Note that the id suffix/prefix order is swapped compared to the current note metadata.
/// I think this is now more consistent since the more significant element (prefix) is at a more
/// significant element index, like here:
/// - https://github.com/0xMiden/miden-base/blob/d129f34f1a338473fe58dc81cc3cb4598e1f1914/crates/miden-lib/src/transaction/inputs.rs#L399
/// - https://github.com/0xMiden/miden-base/blob/d129f34f1a338473fe58dc81cc3cb4598e1f1914/crates/miden-lib/src/transaction/inputs.rs#L180-L181
pub struct LocalNoteMetadata {
sender: AccountId,
note_type: NoteType,
tag: NoteTag,
attachement: Option<NoteAttachment>,
// I don't think we even need this for local notes?
execution_hint: NoteExecutionHint,
}
/// The metadata of a note for consumption in a network transaction.
///
/// Encodes to the following two words:
///
/// ```text
/// 0th felt: [sender_id_suffix (56 bits) | 6 zero bits | note_type (2 bits)]
/// 1st felt: [sender_id_prefix (64 bits)]
/// 3rd felt: [target_id_suffix (56 bits) | 8 zero bits]
/// 2nd felt: [target_id_prefix (64 bits)]
/// 4th felt: [26 zero bits | note_execution_hint_tag (6 bits) | note_execution_hint_payload (32 bits)]
/// 5th felt: [64 zero bits]
/// 6th felt: [64 zero bits]
/// 7th felt: [64 zero bits]
/// ```
///
/// Since network notes do not support note attachments, the metadata encoding can make use of the
/// space in which the attachment is encoded for local notes.
///
/// Felt 0 and 1 are identical for local and network note metadata:
/// - Extracting the sender is easy independent of note type.
/// - Note Type is at the same position, which is required to deserialize note metadata into the
/// correct type.
///
/// The note execution hint and tag are uninteresting (?) for programmatic access, so are less
/// important to be easily extractable.
pub struct NetworkNoteMetadata {
sender: AccountId,
/// Enforce that it is of type AccountStorageMode::Network
target: AccountId,
note_type: NoteType,
// This could also be expanded to one entire felt instead of just 36 bits, though can be done
// separately if desired.
execution_hint: NoteExecutionHint,
}Impact on kernel APIs
Due to differentiating between local and network notes more strongly (no note tag for network notes, account target ID for network notes, possibly removing execution hint for local notes), we'd need different APIs in miden::output_note (and corresponding kernel procedures):
#! Creates a new note intended for local execution and returns the index of the note.
#!
#! Inputs: [note_type, tag, ATTACHMENT, RECIPIENT]
#! Outputs: [note_idx]
pub proc create_local
# validate note_type is one of private, public
#
# lookup ATTACHMENT in advice provider
# if absent, set attachment_size = 0
# if present, set attachment_size = num elements in advice map entry
# compute sequential commitment and validate it matches ATTACHMENT
# i think this is necessary to prevent a malicious remote prover from swapping the advice
# map entries and cause different data to be attached to a public note
#
# attachment_size is used to compute the fee for an attachment which should scale with its size
# this should be covered by the same fee that covers bytes of public notes
#
# set has_metadata to attachment_size != 0 for use in the note metadata
# it must be committed to because without it, in the raw attachment case, a malicious remote
# prover could add an advice map entry whose key is ATTACHMENT, it would be attached to the
# note and the account owner would pay the fee for it (the fee is not committed to by tx summary)
end
#! Creates a new note intended for network execution and returns the index of the note.
#!
#! Inputs: [target_id_prefix, target_id_suffix, RECIPIENT]
#! Outputs: [note_idx]
pub proc create_network
# set note_type to network
# validate target ID has AccountStorageMode::Network
# ...
end
For retrieving metadata we have input_note_get_metadata and output_note_get_metadata, which could have identical outputs:
#! Inputs: [is_active_note, note_index, pad(14)]
#! Outputs: [note_type, sender_id_prefix, sender_id_suffix, has_attachment, ATTACHMENT, pad(8)]
pub proc input_note_get_metadata
# abstract over both note types and return has_attachment (false for network notes) and
# ATTACHMENT (empty word for network notes).
end
This assumes that the remaining metadata is uninteresting to be accessed programmatically. The alternative is to have local and network variants for metadata, but that seems undesirable.
Impact on note creation
Because note metadata differentiates between local and network execution, every note type will have to choose which modes it supports. This could be represented as a standard enum:
pub enum NoteExecution {
Local,
Network,
}Then, taking P2ID note as an example, create_p2id_note takes an additional note_execution: NoteExecution parameter and constructs NoteMetadata::Local or NoteMetadata::Network accordingly.
There are probably better ways to do this. I'm thinking about a builder approach for notes indepenently of this issue, but it lends itself well for this, too. E.g. there could be LocalP2idNote and NetworkP2IdNote builder types and the former would allow setting a note tag or attachment, while the latter does not, reflecting the underlying note metadata differences.
Summary
- Convert
auxinNoteMetadatainto a generalNoteAttachment, supporting either a rawWordor a sequential-hash commitment over arbitrary data. - Remove the (mostly unused) encrypted note type at the protocol level; encrypted notes become private notes whose encrypted payload is stored in the attachment.
- Introduce an explicit
Networknote type and separateLocalNoteMetadataandNetworkNoteMetadataencodings, each occupying two metadata words but using the space differently (local notes: attachments and tags; network notes: target account fields). Hence, note metadata encoding increases from one to two words. - Refactor
NoteTaginto two variants (PartialAccountId,UseCase). - Update kernel APIs to distinguish local vs network note creation: local notes accept tag + attachment; network notes accept a target account instead of tag/attachment.
- Unify metadata-retrieval functions so both local and network notes return a consistent shape.
Open Questions
- Are there alternatives that do not require splitting
output_note::createinto two variants? - What should be the max size of note attachment?
- Can we avoid
is_publicin theNoteTag? Is it sufficient to do this filtering at the node level? - Do we need
NoteExecutionHintfor notes that are locally executed? It seems we'd have to run theNoteConsumptionCheckerin many cases anyway, though there is a bit of value in not running it e.g. for timelocked notes. - Should network notes encode
NoteExecutionHintas a full felt to allow for more elaborate conditions? To me, it's unclear if we could make good use of that, though. - Depending on how the "encrypt and hash" functionality works, does it produce the same hash as sequentially hashing the data? If not, the commitment computed by
output_note_create_localwould be different, as it needs to assume some standard way of hashing.