Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion crates/miden-multisig-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ let recipient = AccountId::from_hex("0x7b7b7b7a7b7b7b017b7b7b7b7b7b7b")?;
let faucet = AccountId::from_hex("0x7c7c7c7c7c7c7c017c7c7c7c7c7c7c")?;
let tx = TransactionType::transfer(recipient, faucet, 1_000);

// P2ID notes are public by default. For a private note (only its hash is
// published on chain; the recipient needs the note shared out-of-band) use
// `transfer_with_note_type`. `NoteType` is re-exported from `miden_protocol::note`.
use miden_protocol::note::NoteType;
let tx = TransactionType::transfer_with_note_type(recipient, faucet, 1_000, NoteType::Private);

// Proposer creates the delta on GUARDIAN
let proposal = client.propose_transaction(tx).await?;

Expand Down Expand Up @@ -152,11 +158,12 @@ on-chain transaction — the integration owns that recipe and drives execution.
```rust
use miden_client::Serializable;
use miden_multisig_client::{build_p2id_transaction_request, generate_salt};
use miden_protocol::note::NoteType;

// Producer: build a transaction and propose it under a custom label.
let salt = generate_salt();
let mut request = build_p2id_transaction_request(
account.inner(), recipient, vec![asset], salt, std::iter::empty(),
account.inner(), recipient, vec![asset], NoteType::Public, salt, std::iter::empty(),
)?;
let proposal = client.propose_custom_transaction(&request.to_bytes(), "b2agg").await?;

Expand Down
1 change: 1 addition & 0 deletions crates/miden-multisig-client/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ mod tests {
recipient: account_id,
faucet_id: account_id,
amount: 10,
note_type: miden_protocol::note::NoteType::Public,
})
.expect("threshold"),
1
Expand Down
2 changes: 2 additions & 0 deletions crates/miden-multisig-client/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,15 @@ pub async fn build_final_transaction_request(
recipient,
faucet_id,
amount,
note_type,
} => {
let asset = build_transfer_asset(account, *faucet_id, *amount)?;

crate::transaction::build_p2id_transaction_request(
account,
*recipient,
vec![asset.into()],
*note_type,
salt,
signature_advice,
)
Expand Down
8 changes: 8 additions & 0 deletions crates/miden-multisig-client/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ pub struct ExportedMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<u64>,

/// P2ID note visibility, `"public"` or `"private"` (issue #322).
/// Absent => public (pre-#322 exports).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note_type: Option<String>,

#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub note_ids_hex: Vec<String>,

Expand Down Expand Up @@ -120,6 +125,7 @@ impl ExportedProposal {
recipient_hex: self.metadata.recipient_hex.clone(),
faucet_id_hex: self.metadata.faucet_id_hex.clone(),
amount: self.metadata.amount,
note_type: self.metadata.note_type.clone(),
note_ids_hex: self.metadata.note_ids_hex.clone(),
consume_notes_metadata_version: self.metadata.consume_notes_metadata_version,
consume_notes_notes: self
Expand Down Expand Up @@ -278,6 +284,7 @@ impl ExportedProposal {
recipient_hex: proposal.metadata.recipient_hex.clone(),
faucet_id_hex: proposal.metadata.faucet_id_hex.clone(),
amount: proposal.metadata.amount,
note_type: proposal.metadata.note_type.clone(),
note_ids_hex: proposal.metadata.note_ids_hex.clone(),
consume_notes_metadata_version: proposal.metadata.consume_notes_metadata_version,
consume_notes_notes: proposal
Expand Down Expand Up @@ -486,6 +493,7 @@ mod tests {
recipient_hex: None,
faucet_id_hex: None,
amount: None,
note_type: None,
note_ids_hex: vec![],
consume_notes_metadata_version: None,
consume_notes_notes: Vec::new(),
Expand Down
2 changes: 1 addition & 1 deletion crates/miden-multisig-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,4 @@ pub use miden_protocol::account::AccountId;
pub use miden_protocol::asset::Asset;
pub use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey as EcdsaSecretKey;
pub use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey;
pub use miden_protocol::note::NoteId;
pub use miden_protocol::note::{NoteId, NoteType};
56 changes: 55 additions & 1 deletion crates/miden-multisig-client/src/payload.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Payload types for multisig transaction proposals.

use guardian_shared::{DeltaSignature, ProposalSignature, ToJson};
use miden_protocol::note::NoteType;
use miden_protocol::transaction::TransactionSummary;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -33,6 +34,12 @@ pub struct ProposalMetadataPayload {
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<String>,

/// P2ID note visibility, `"public"` or `"private"` (issue #322). Omitted
/// for public notes so the pre-#322 wire shape stays byte-identical;
/// absent => public.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note_type: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub required_signatures: Option<u64>,

Expand Down Expand Up @@ -145,19 +152,23 @@ impl ProposalPayload {
self
}

/// Sets the metadata for P2ID payment transfers.
/// Sets the metadata for P2ID payment transfers. `note_type` is written to
/// the wire only when it is private, so public payloads keep the legacy
/// shape (issue #322).
pub fn with_payment_metadata(
mut self,
recipient_id: String,
faucet_id: String,
amount: u64,
salt: String,
note_type: NoteType,
) -> Self {
self.metadata = Some(ProposalMetadataPayload {
proposal_type: "p2id".to_string(),
recipient_id: Some(recipient_id),
faucet_id: Some(faucet_id),
amount: Some(amount.to_string()),
note_type: (note_type != NoteType::Public).then(|| note_type.to_string()),
salt: Some(salt),
..Default::default()
});
Expand Down Expand Up @@ -337,6 +348,7 @@ mod tests {
"0xfaucet".to_string(),
1000,
"0xsalt".to_string(),
NoteType::Public,
);

let meta = payload.metadata.unwrap();
Expand All @@ -345,6 +357,48 @@ mod tests {
assert_eq!(meta.faucet_id, Some("0xfaucet".to_string()));
assert_eq!(meta.amount, Some("1000".to_string()));
assert_eq!(meta.salt, Some("0xsalt".to_string()));
assert_eq!(meta.note_type, None);
}

/// A public P2ID payload must keep the pre-#322 wire shape: no
/// `note_type` key at all.
#[test]
fn with_payment_metadata_public_omits_note_type_on_wire() {
let payload = ProposalPayload {
tx_summary: serde_json::json!({}),
signatures: vec![],
metadata: None,
}
.with_payment_metadata(
"0xrecipient".to_string(),
"0xfaucet".to_string(),
1000,
"0xsalt".to_string(),
Comment thread
haseebrabbani marked this conversation as resolved.
Dismissed
NoteType::Public,
);

let json = serde_json::to_value(payload.metadata.unwrap()).unwrap();
assert!(json.get("note_type").is_none());
}

#[test]
fn with_payment_metadata_private_round_trips_note_type() {
let payload = ProposalPayload {
tx_summary: serde_json::json!({}),
signatures: vec![],
metadata: None,
}
.with_payment_metadata(
"0xrecipient".to_string(),
"0xfaucet".to_string(),
1000,
"0xsalt".to_string(),
Comment thread
haseebrabbani marked this conversation as resolved.
Dismissed
NoteType::Private,
);

let json = serde_json::to_string(&payload.metadata.unwrap()).unwrap();
let parsed: ProposalMetadataPayload = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.note_type, Some("private".to_string()));
}

#[test]
Expand Down
97 changes: 94 additions & 3 deletions crates/miden-multisig-client/src/proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{
PublicKey as EcdsaPublicKey, Signature as EcdsaSignature,
};
use miden_protocol::crypto::dsa::falcon512_poseidon2::Signature as Poseidon2FalconSignature;
use miden_protocol::note::{Note, NoteId};
use miden_protocol::note::{Note, NoteId, NoteType};
use miden_protocol::transaction::TransactionSummary;
use miden_protocol::utils::serde::{Deserializable, Serializable};
use miden_protocol::{Felt, Word};
Expand Down Expand Up @@ -94,6 +94,9 @@ pub enum TransactionType {
recipient: AccountId,
faucet_id: AccountId,
amount: u64,
/// Visibility of the created note (issue #322). Absent in legacy
/// proposal metadata, which maps to [`NoteType::Public`].
note_type: NoteType,
},
ConsumeNotes {
note_ids: Vec<NoteId>,
Expand Down Expand Up @@ -130,12 +133,24 @@ pub enum TransactionType {
}

impl TransactionType {
/// Creates a P2ID transfer transaction.
/// Creates a P2ID transfer transaction with a public output note.
pub fn transfer(recipient: AccountId, faucet_id: AccountId, amount: u64) -> Self {
Self::transfer_with_note_type(recipient, faucet_id, amount, NoteType::Public)
}

/// Creates a P2ID transfer transaction with an explicit note visibility
/// (issue #322).
pub fn transfer_with_note_type(
recipient: AccountId,
faucet_id: AccountId,
amount: u64,
note_type: NoteType,
) -> Self {
Self::P2ID {
recipient,
faucet_id,
amount,
note_type,
}
}

Expand Down Expand Up @@ -261,6 +276,9 @@ pub struct ProposalMetadata {
pub recipient_hex: Option<String>,
pub faucet_id_hex: Option<String>,
pub amount: Option<u64>,
/// P2ID note visibility, `"public"` or `"private"` (issue #322).
/// `None` => public, the wire shape of pre-#322 proposals.
pub note_type: Option<String>,

pub note_ids_hex: Vec<String>,

Expand Down Expand Up @@ -316,6 +334,22 @@ impl ProposalMetadata {
Ok(commitments)
}

/// Parses `note_type` for a P2ID proposal. Absent => public (the only
/// behavior before issue #322); an unrecognized value is rejected rather
/// than silently rebuilt as a public note that could never match the
/// signed tx_summary commitment.
pub fn p2id_note_type(&self) -> Result<NoteType> {
match self.note_type.as_deref() {
None => Ok(NoteType::Public),
Some(value) => value.parse().map_err(|_| {
MultisigError::InvalidConfig(format!(
"unsupported metadata.note_type '{}': expected 'public' or 'private'",
value
))
}),
}
}

/// Converts note ID hex strings to NoteIds.
pub fn note_ids(&self) -> Result<Vec<NoteId>> {
self.note_ids_hex
Expand Down Expand Up @@ -373,6 +407,7 @@ impl ProposalMetadata {
recipient,
faucet_id,
amount: parsed_amount,
note_type: self.p2id_note_type()?,
})
}
"switch_guardian" => {
Expand Down Expand Up @@ -579,6 +614,7 @@ impl Proposal {
Some(parsed) => Some(parsed?),
None => None,
};
let note_type = metadata_payload.note_type;
let note_ids_hex = metadata_payload.note_ids;
let consume_notes_metadata_version = metadata_payload.consume_notes_metadata_version;
let consume_notes_notes = metadata_payload
Expand All @@ -600,6 +636,7 @@ impl Proposal {
recipient_hex: recipient_hex.clone(),
faucet_id_hex: faucet_id_hex.clone(),
amount,
note_type,
note_ids_hex: note_ids_hex.clone(),
consume_notes_metadata_version,
consume_notes_notes,
Expand Down Expand Up @@ -826,7 +863,8 @@ mod tests {
TransactionType::P2ID {
recipient,
faucet_id,
amount
amount,
note_type: NoteType::Public,
}
);
}
Expand Down Expand Up @@ -1258,6 +1296,59 @@ mod tests {
assert!(err.to_string().contains("proposal_type is required"));
}

// ---------- p2id note_type (issue #322) ----------

fn p2id_metadata(note_type: Option<&str>) -> ProposalMetadata {
ProposalMetadata {
recipient_hex: Some("0x7b7b7b7a7b7b7b017b7b7b7b7b7b7b".to_string()),
faucet_id_hex: Some("0x7c7c7c7c7c7c7c017c7c7c7c7c7c7c".to_string()),
amount: Some(1000),
note_type: note_type.map(str::to_string),
..Default::default()
}
}

/// Absent `note_type` must keep mapping to a public note — the only
/// behavior that existed before the field, so pre-#322 proposals
/// rebuild identically.
#[test]
fn to_transaction_type_p2id_defaults_to_public_note() {
let tx_type = p2id_metadata(None)
.to_transaction_type("p2id")
.expect("to_transaction_type");
assert!(matches!(
tx_type,
TransactionType::P2ID {
note_type: NoteType::Public,
..
}
));
}

#[test]
fn to_transaction_type_p2id_threads_private_note_type() {
let tx_type = p2id_metadata(Some("private"))
.to_transaction_type("p2id")
.expect("to_transaction_type");
assert!(matches!(
tx_type,
TransactionType::P2ID {
note_type: NoteType::Private,
..
}
));
}

/// An unknown `note_type` must be rejected, not silently rebuilt as a
/// public note that could never match the signed tx_summary commitment.
#[test]
fn to_transaction_type_p2id_rejects_unknown_note_type() {
let err = p2id_metadata(Some("encrypted"))
.to_transaction_type("p2id")
.expect_err("unknown note_type must be rejected");
assert!(err.to_string().contains("unsupported metadata.note_type"));
}

#[test]
fn builtin_proposal_types_are_recognized() {
for label in [
Expand Down
Loading
Loading