use alloc::collections::BTreeMap;
use alloc::format;
use alloc::vec::Vec;
use miden_protocol::account::AccountId;
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::crypto::SequentialCommit;
use miden_protocol::crypto::merkle::MerklePath;
use miden_protocol::note::{
Note,
NoteAttachment,
NoteAttachmentHeader,
NoteAttachmentScheme,
NoteAttachments,
NoteDetails,
NoteDetailsCommitment,
NoteHeader,
NoteId,
NoteInclusionProof,
NoteMetadata,
NoteScript,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_protocol::{Felt, MastForest, MastNodeId, Word};
use miden_tx::utils::serde::Deserializable;
use super::{MissingFieldHelper, RpcConversionError};
use crate::rpc::{RpcError, generated as proto};
impl From<NoteId> for proto::note::NoteId {
fn from(value: NoteId) -> Self {
proto::note::NoteId { id: Some(value.into()) }
}
}
impl TryFrom<proto::note::NoteId> for NoteId {
type Error = RpcConversionError;
fn try_from(value: proto::note::NoteId) -> Result<Self, Self::Error> {
let word =
Word::try_from(value.id.ok_or(proto::note::NoteId::missing_field(stringify!(id)))?)?;
Ok(Self::from_raw(word))
}
}
fn note_type_from_proto(raw: i32) -> Result<NoteType, RpcConversionError> {
let proto_note_type = proto::note::NoteType::try_from(raw)
.map_err(|_| RpcConversionError::InvalidField(alloc::format!("note_type={raw}")))?;
match proto_note_type {
proto::note::NoteType::Public => Ok(NoteType::Public),
proto::note::NoteType::Private => Ok(NoteType::Private),
proto::note::NoteType::Unspecified => {
Err(RpcConversionError::InvalidField("note_type=NOTE_TYPE_UNSPECIFIED".into()))
},
}
}
fn note_type_to_proto(note_type: NoteType) -> i32 {
let proto_note_type = match note_type {
NoteType::Public => proto::note::NoteType::Public,
NoteType::Private => proto::note::NoteType::Private,
};
proto_note_type as i32
}
fn attachment_headers_from_proto(
schemes: &[u32],
) -> Result<[NoteAttachmentHeader; NoteAttachments::MAX_COUNT], RpcConversionError> {
if schemes.len() > NoteAttachments::MAX_COUNT {
return Err(RpcConversionError::InvalidField(alloc::format!(
"attachment_schemes length {} exceeds NoteAttachments::MAX_COUNT",
schemes.len(),
)));
}
let mut headers = [NoteAttachmentHeader::absent(); NoteAttachments::MAX_COUNT];
for (slot, raw) in schemes.iter().enumerate() {
if *raw == 0 {
continue;
}
let raw_u16 = u16::try_from(*raw).map_err(|_| {
RpcConversionError::InvalidField(alloc::format!(
"attachment_schemes[{slot}]={raw} does not fit in u16",
))
})?;
let scheme = NoteAttachmentScheme::new(raw_u16).map_err(|err| {
RpcConversionError::InvalidField(alloc::format!("attachment_schemes[{slot}]: {err}"))
})?;
headers[slot] = NoteAttachmentHeader::new(scheme);
}
Ok(headers)
}
fn attachment_schemes_to_proto(
headers: &[NoteAttachmentHeader; NoteAttachments::MAX_COUNT],
) -> Vec<u32> {
let mut encoded: Vec<u32> = headers
.iter()
.map(|h| h.scheme().map_or(0, |s| u32::from(s.as_u16())))
.collect();
while matches!(encoded.last(), Some(0)) {
encoded.pop();
}
encoded
}
impl TryFrom<proto::note::NoteMetadata> for NoteMetadata {
type Error = RpcConversionError;
fn try_from(value: proto::note::NoteMetadata) -> Result<Self, Self::Error> {
let partial_metadata: PartialNoteMetadata = (&value).try_into()?;
let attachment_headers = attachment_headers_from_proto(&value.attachment_schemes)?;
let attachments_commitment = value
.attachments_commitment
.ok_or_else(|| {
proto::note::NoteMetadata::missing_field(stringify!(attachments_commitment))
})?
.try_into()?;
Ok(NoteMetadata::from_parts(
partial_metadata,
attachment_headers,
attachments_commitment,
))
}
}
struct AttachmentCommitments(Vec<Word>);
impl SequentialCommit for AttachmentCommitments {
type Commitment = Word;
fn to_elements(&self) -> Vec<Felt> {
let mut elements = Vec::with_capacity(self.0.len() * miden_protocol::WORD_SIZE);
for commitment in &self.0 {
elements.extend_from_slice(commitment.as_elements());
}
elements
}
}
impl TryFrom<proto::note::NoteSyncMetadata> for NoteMetadata {
type Error = RpcConversionError;
fn try_from(value: proto::note::NoteSyncMetadata) -> Result<Self, Self::Error> {
let sender = value
.sender
.ok_or_else(|| proto::note::NoteSyncMetadata::missing_field(stringify!(sender)))?
.try_into()?;
let note_type = note_type_from_proto(value.note_type)?;
let tag = NoteTag::new(value.tag);
let partial_metadata = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
if value.attachments.len() > NoteAttachments::MAX_COUNT {
return Err(RpcConversionError::InvalidField(format!(
"attachments length {} exceeds NoteAttachments::MAX_COUNT",
value.attachments.len(),
)));
}
let mut attachment_headers = [NoteAttachmentHeader::absent(); NoteAttachments::MAX_COUNT];
let mut commitments = Vec::with_capacity(value.attachments.len());
for (slot, attachment) in value.attachments.into_iter().enumerate() {
let raw_scheme = u16::try_from(attachment.scheme).map_err(|_| {
RpcConversionError::InvalidField(format!(
"attachments[{slot}].scheme={} does not fit in u16",
attachment.scheme,
))
})?;
let scheme = NoteAttachmentScheme::new(raw_scheme).map_err(|err| {
RpcConversionError::InvalidField(format!("attachments[{slot}].scheme: {err}"))
})?;
attachment_headers[slot] = NoteAttachmentHeader::new(scheme);
let payload = attachment.payload.ok_or_else(|| {
proto::note::NoteSyncAttachment::missing_field(stringify!(payload))
})?;
let commitment = match payload {
proto::note::note_sync_attachment::Payload::Value(value) => {
NoteAttachment::with_word(scheme, Word::try_from(value)?).to_commitment()
},
proto::note::note_sync_attachment::Payload::Commitment(commitment) => {
Word::try_from(commitment)?
},
};
commitments.push(commitment);
}
let attachments_commitment = AttachmentCommitments(commitments).to_commitment();
Ok(NoteMetadata::from_parts(
partial_metadata,
attachment_headers,
attachments_commitment,
))
}
}
impl TryFrom<&proto::note::NoteMetadata> for PartialNoteMetadata {
type Error = RpcConversionError;
fn try_from(value: &proto::note::NoteMetadata) -> Result<Self, Self::Error> {
let sender = value
.sender
.clone()
.ok_or_else(|| proto::note::NoteMetadata::missing_field(stringify!(sender)))?
.try_into()?;
let note_type = note_type_from_proto(value.note_type)?;
let tag = NoteTag::new(value.tag);
Ok(PartialNoteMetadata::new(sender, note_type).with_tag(tag))
}
}
impl From<NoteMetadata> for proto::note::NoteMetadata {
fn from(value: NoteMetadata) -> Self {
proto::note::NoteMetadata {
sender: Some(value.sender().into()),
note_type: note_type_to_proto(value.note_type()),
tag: value.tag().as_u32(),
attachment_schemes: attachment_schemes_to_proto(value.attachment_headers()),
attachments_commitment: Some(value.attachments_commitment().into()),
}
}
}
impl TryFrom<proto::note::NoteHeader> for NoteHeader {
type Error = RpcConversionError;
fn try_from(value: proto::note::NoteHeader) -> Result<Self, Self::Error> {
let details_commitment_word: Word = value
.details_commitment
.ok_or(proto::note::NoteHeader::missing_field(stringify!(details_commitment)))?
.try_into()?;
let metadata = value
.metadata
.ok_or(proto::note::NoteHeader::missing_field(stringify!(metadata)))?
.try_into()?;
Ok(NoteHeader::new(
NoteDetailsCommitment::from_raw(details_commitment_word),
metadata,
))
}
}
impl TryFrom<proto::note::NoteInclusionInBlockProof> for NoteInclusionProof {
type Error = RpcConversionError;
fn try_from(value: proto::note::NoteInclusionInBlockProof) -> Result<Self, Self::Error> {
Ok(NoteInclusionProof::new(
value.block_num.into(),
u16::try_from(value.note_index_in_block)
.map_err(|_| RpcConversionError::InvalidField("NoteIndexInBlock".into()))?,
value
.inclusion_path
.ok_or_else(|| {
proto::note::NoteInclusionInBlockProof::missing_field(stringify!(
inclusion_path
))
})?
.try_into()?,
)?)
}
}
#[derive(Debug, Clone)]
pub struct SyncNotesBlock {
pub block_header: BlockHeader,
pub mmr_path: MerklePath,
pub notes: BTreeMap<NoteId, CommittedNote>,
}
impl TryFrom<proto::rpc::sync_notes_response::NoteSyncBlock> for SyncNotesBlock {
type Error = RpcError;
fn try_from(
block: proto::rpc::sync_notes_response::NoteSyncBlock,
) -> Result<Self, Self::Error> {
let block_header = block
.block_header
.ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(blocks.block_header)))?
.try_into()?;
let mmr_path = block
.mmr_path
.ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(blocks.mmr_path)))?
.try_into()?;
let notes: BTreeMap<NoteId, CommittedNote> = block
.notes
.into_iter()
.map(|n| {
let note = CommittedNote::try_from(n)?;
Ok((*note.note_id(), note))
})
.collect::<Result<_, RpcConversionError>>()?;
Ok(SyncNotesBlock { block_header, mmr_path, notes })
}
}
#[derive(Debug, Clone)]
pub struct ResolvedSyncNotesBlock {
pub block_header: BlockHeader,
pub mmr_path: MerklePath,
pub notes: BTreeMap<NoteId, SyncedNote>,
}
#[derive(Debug, Clone)]
pub struct SyncedNote {
pub committed: CommittedNote,
pub content: Option<ResolvedNoteContent>,
}
impl SyncedNote {
pub fn new(
committed: CommittedNote,
content: Option<ResolvedNoteContent>,
) -> Result<Self, RpcError> {
match &content {
Some(resolved) => {
let expected_note_type = match resolved {
ResolvedNoteContent::Public { .. } => NoteType::Public,
ResolvedNoteContent::Private { .. } => NoteType::Private,
};
if committed.note_type() != expected_note_type {
return Err(RpcError::InvalidResponse(format!(
"content returned for note {} does not match the note's type",
committed.note_id()
)));
}
if resolved.attachments().to_commitment()
!= committed.metadata().attachments_commitment()
{
return Err(RpcError::InvalidResponse(format!(
"attachment content returned for note {} does not match the note's \
attachments commitment",
committed.note_id()
)));
}
},
None => {
if committed.has_attachments() {
return Err(RpcError::InvalidResponse(format!(
"note {} advertises attachments but the node did not return their content",
committed.note_id()
)));
}
},
}
Ok(Self { committed, content })
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum ResolvedNoteContent {
Public {
details: NoteDetails,
attachments: NoteAttachments,
},
Private {
attachments: NoteAttachments,
},
}
impl ResolvedNoteContent {
pub fn attachments(&self) -> &NoteAttachments {
match self {
Self::Public { attachments, .. } | Self::Private { attachments } => attachments,
}
}
pub fn into_attachments(self) -> NoteAttachments {
match self {
Self::Public { attachments, .. } | Self::Private { attachments } => attachments,
}
}
}
#[derive(Debug, Clone)]
pub struct CommittedNote {
note_id: NoteId,
metadata: NoteMetadata,
inclusion_proof: NoteInclusionProof,
}
impl CommittedNote {
pub fn new(
note_id: NoteId,
metadata: NoteMetadata,
inclusion_proof: NoteInclusionProof,
) -> Self {
Self { note_id, metadata, inclusion_proof }
}
pub fn note_id(&self) -> &NoteId {
&self.note_id
}
pub fn note_type(&self) -> NoteType {
self.metadata.note_type()
}
pub fn tag(&self) -> NoteTag {
self.metadata.tag()
}
pub fn sender(&self) -> AccountId {
self.metadata.sender()
}
pub fn metadata(&self) -> &NoteMetadata {
&self.metadata
}
pub fn has_attachments(&self) -> bool {
self.metadata
.attachment_headers()
.iter()
.any(|header| header.scheme().is_some())
}
pub fn inclusion_proof(&self) -> &NoteInclusionProof {
&self.inclusion_proof
}
pub fn block_num(&self) -> BlockNumber {
self.inclusion_proof.location().block_num()
}
}
impl TryFrom<proto::note::NoteSyncRecord> for CommittedNote {
type Error = RpcConversionError;
fn try_from(note: proto::note::NoteSyncRecord) -> Result<Self, Self::Error> {
let proto_metadata = note
.metadata
.ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.metadata)))?;
let metadata: NoteMetadata = proto_metadata.try_into()?;
let proto_inclusion_proof = note.inclusion_proof.ok_or(
proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.inclusion_proof)),
)?;
let note_id: NoteId = proto_inclusion_proof
.note_id
.ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(
notes.inclusion_proof.note_id
)))?
.try_into()?;
let inclusion_proof: NoteInclusionProof = proto_inclusion_proof.try_into()?;
Ok(CommittedNote::new(note_id, metadata, inclusion_proof))
}
}
#[allow(clippy::large_enum_variant)]
pub enum FetchedNote {
Private(NoteId, NoteMetadata, NoteAttachments, NoteInclusionProof),
Public(Note, NoteInclusionProof),
}
impl FetchedNote {
pub fn inclusion_proof(&self) -> &NoteInclusionProof {
match self {
FetchedNote::Private(_, _, _, inclusion_proof)
| FetchedNote::Public(_, inclusion_proof) => inclusion_proof,
}
}
pub fn metadata(&self) -> &NoteMetadata {
match self {
FetchedNote::Private(_, metadata, ..) => metadata,
FetchedNote::Public(note, _) => note.metadata(),
}
}
pub fn attachments(&self) -> &NoteAttachments {
match self {
FetchedNote::Private(_, _, attachments, _) => attachments,
FetchedNote::Public(note, _) => note.attachments(),
}
}
pub fn id(&self) -> NoteId {
match self {
FetchedNote::Private(note_id, ..) => *note_id,
FetchedNote::Public(note, _) => note.id(),
}
}
}
impl TryFrom<proto::note::CommittedNote> for FetchedNote {
type Error = RpcConversionError;
fn try_from(value: proto::note::CommittedNote) -> Result<Self, Self::Error> {
let inclusion_proof = value.inclusion_proof.ok_or_else(|| {
proto::note::CommittedNote::missing_field(stringify!(inclusion_proof))
})?;
let note_id: NoteId = inclusion_proof
.note_id
.ok_or_else(|| {
proto::note::CommittedNote::missing_field(stringify!(inclusion_proof.note_id))
})?
.try_into()?;
let inclusion_proof = NoteInclusionProof::try_from(inclusion_proof)?;
let note = value
.note
.ok_or_else(|| proto::note::CommittedNote::missing_field(stringify!(note)))?;
let proto_metadata = note
.metadata
.ok_or_else(|| proto::note::CommittedNote::missing_field(stringify!(note.metadata)))?;
let metadata: NoteMetadata = proto_metadata.clone().try_into()?;
let partial_metadata: PartialNoteMetadata = (&proto_metadata).try_into()?;
let attachments = if note.attachments.is_empty() {
NoteAttachments::empty()
} else {
NoteAttachments::read_from_bytes(¬e.attachments)?
};
if let Some(detail_bytes) = note.details {
let details = NoteDetails::read_from_bytes(&detail_bytes)?;
let (assets, recipient) = details.into_parts();
Ok(FetchedNote::Public(
Note::with_attachments(assets, partial_metadata, recipient, attachments),
inclusion_proof,
))
} else {
Ok(FetchedNote::Private(note_id, metadata, attachments, inclusion_proof))
}
}
}
impl TryFrom<proto::note::NoteScript> for NoteScript {
type Error = RpcConversionError;
fn try_from(note_script: proto::note::NoteScript) -> Result<Self, Self::Error> {
let mast_forest = MastForest::read_from_bytes(¬e_script.mast)?;
let entrypoint = MastNodeId::from_u32_safe(note_script.entrypoint, &mast_forest)?;
Ok(NoteScript::from_parts(alloc::sync::Arc::new(mast_forest), entrypoint))
}
}
#[cfg(test)]
mod tests {
use miden_protocol::account::{AccountIdVersion, AccountType, AssetCallbackFlag};
use super::*;
fn sender() -> AccountId {
AccountId::dummy(
[1; 15],
AccountIdVersion::Version1,
AccountType::Public,
AssetCallbackFlag::Disabled,
)
}
fn sync_attachments(attachments: &NoteAttachments) -> Vec<proto::note::NoteSyncAttachment> {
attachments
.iter()
.map(|attachment| {
let payload = if attachment.num_words() == 1 {
proto::note::note_sync_attachment::Payload::Value(
attachment.content().as_words()[0].into(),
)
} else {
proto::note::note_sync_attachment::Payload::Commitment(
attachment.to_commitment().into(),
)
};
proto::note::NoteSyncAttachment {
scheme: u32::from(attachment.attachment_scheme().as_u16()),
payload: Some(payload),
}
})
.collect()
}
fn sync_metadata(
attachments: Vec<proto::note::NoteSyncAttachment>,
) -> proto::note::NoteSyncMetadata {
proto::note::NoteSyncMetadata {
sender: Some(sender().into()),
note_type: note_type_to_proto(NoteType::Private),
tag: 7,
attachments,
}
}
#[test]
fn sync_metadata_reconstructs_metadata_with_mixed_attachments() {
let attachments = NoteAttachments::new(vec![
NoteAttachment::with_word(
NoteAttachmentScheme::new(42).unwrap(),
Word::from([1u32, 2, 3, 4]),
),
NoteAttachment::with_words(
NoteAttachmentScheme::new(100).unwrap(),
vec![Word::from([5u32, 6, 7, 8]), Word::from([9u32, 10, 11, 12])],
)
.unwrap(),
])
.unwrap();
let expected = NoteMetadata::new(
PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
&attachments,
);
let reconstructed: NoteMetadata =
sync_metadata(sync_attachments(&attachments)).try_into().unwrap();
assert_eq!(reconstructed, expected);
}
#[test]
fn sync_metadata_reconstructs_metadata_without_attachments() {
let attachments = NoteAttachments::empty();
let expected = NoteMetadata::new(
PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
&attachments,
);
let reconstructed: NoteMetadata = sync_metadata(Vec::new()).try_into().unwrap();
assert_eq!(reconstructed, expected);
}
#[test]
fn sync_metadata_rejects_too_many_attachments() {
let attachment = proto::note::NoteSyncAttachment {
scheme: 42,
payload: Some(proto::note::note_sync_attachment::Payload::Value(Word::empty().into())),
};
let attachments = vec![attachment; NoteAttachments::MAX_COUNT + 1];
let err = NoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
assert!(matches!(err, RpcConversionError::InvalidField(_)), "got {err:?}");
}
#[test]
fn sync_metadata_rejects_reserved_absent_scheme() {
let attachments = vec![proto::note::NoteSyncAttachment {
scheme: 0,
payload: Some(proto::note::note_sync_attachment::Payload::Value(Word::empty().into())),
}];
let err = NoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
assert!(matches!(err, RpcConversionError::InvalidField(_)), "got {err:?}");
}
#[test]
fn sync_metadata_rejects_missing_attachment_payload() {
let attachments = vec![proto::note::NoteSyncAttachment { scheme: 42, payload: None }];
let err = NoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
assert!(
matches!(err, RpcConversionError::MissingFieldInProtobufRepresentation { .. }),
"got {err:?}"
);
}
}