Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## v0.16.0 (TBD)

### Changes

- [BREAKING] P2ID and P2IDE notes must carry at least one asset. Enforced in `P2idNote::create` / `P2ideNote::create` (new `NoteError::MissingAsset`) and in the `basic_wallet::add_assets_to_account` MASM helper.

## v0.15.0 (2026-05-22)

### Features
Expand Down
2 changes: 2 additions & 0 deletions crates/miden-protocol/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,8 @@ pub enum NoteError {
NoteAttachmentSchemeExceeded(u32),
#[error("attachment scheme value 0 is reserved")]
NoteAttachmentSchemeZeroReserved,
#[error("note must contain at least one asset")]
MissingAsset,
#[error("{error_msg}")]
Other {
error_msg: Box<str>,
Expand Down
9 changes: 9 additions & 0 deletions crates/miden-standards/asm/standards/wallets/basic.masm
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ use miden::protocol::native_account
use miden::protocol::output_note
use miden::protocol::active_note

# ERRORS
# =================================================================================================

const ERR_BASIC_WALLET_EMPTY_NOTE_ASSETS="active note must contain at least one asset"

# CONSTANTS
# =================================================================================================

Expand Down Expand Up @@ -77,6 +82,10 @@ pub proc add_assets_to_account
locaddr.0 exec.active_note::get_assets
# => [num_of_assets]

# ensure the note carries at least one asset
dup neq.0 assert.err=ERR_BASIC_WALLET_EMPTY_NOTE_ASSETS
# => [num_of_assets]

# compute the pointer at which we should stop iterating
mul.ASSET_SIZE locaddr.0 dup movdn.2 add
# => [end_ptr, ptr]
Expand Down
27 changes: 26 additions & 1 deletion crates/miden-standards/src/note/p2id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ impl P2idNote {
/// tag is set to the target's account ID.
///
/// # Errors
/// Returns an error if deserialization or compilation of the `P2ID` script fails.
/// - Returns [`NoteError::MissingAsset`] if `assets` is empty.
/// - Returns an error if deserialization or compilation of the `P2ID` script fails.
pub fn create<R: FeltRng>(
sender: AccountId,
target: AccountId,
Expand All @@ -82,6 +83,10 @@ impl P2idNote {
attachments: NoteAttachments,
rng: &mut R,
) -> Result<Note, NoteError> {
if assets.is_empty() {
return Err(NoteError::MissingAsset);
}

let serial_num = rng.draw_word();
let recipient = P2idNoteStorage::new(target).into_recipient(serial_num);

Expand Down Expand Up @@ -164,6 +169,7 @@ impl TryFrom<&[Felt]> for P2idNoteStorage {
mod tests {
use miden_protocol::Felt;
use miden_protocol::account::{AccountId, AccountIdVersion, AccountType};
use miden_protocol::crypto::rand::RandomCoin;
use miden_protocol::errors::NoteError;

use super::*;
Expand Down Expand Up @@ -205,4 +211,23 @@ mod tests {

assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
}

#[test]
fn create_rejects_empty_assets() {
let sender = AccountId::dummy([4u8; 15], AccountIdVersion::Version1, AccountType::Private);
let target = AccountId::dummy([5u8; 15], AccountIdVersion::Version1, AccountType::Private);
let mut rng = RandomCoin::new(Word::default());

let err = P2idNote::create(
sender,
target,
vec![],
NoteType::Private,
NoteAttachments::default(),
&mut rng,
)
.expect_err("empty assets must be rejected");

assert!(matches!(err, NoteError::MissingAsset));
}
}
27 changes: 26 additions & 1 deletion crates/miden-standards/src/note/p2ide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ impl P2ideNote {
/// and the note tag is set to the storage target account.
///
/// # Errors
/// Returns an error if construction of the note recipient or asset vault fails.
/// - Returns [`NoteError::MissingAsset`] if `assets` is empty.
/// - Returns an error if construction of the note recipient or asset vault fails.
pub fn create<R: FeltRng>(
sender: AccountId,
storage: P2ideNoteStorage,
Expand All @@ -89,6 +90,10 @@ impl P2ideNote {
attachments: NoteAttachments,
rng: &mut R,
) -> Result<Note, NoteError> {
if assets.is_empty() {
return Err(NoteError::MissingAsset);
}

let serial_num = rng.draw_word();
let recipient = storage.into_recipient(serial_num)?;
let tag = NoteTag::with_account_target(storage.target());
Expand Down Expand Up @@ -216,6 +221,7 @@ mod tests {
use miden_protocol::Felt;
use miden_protocol::account::{AccountId, AccountIdVersion, AccountType};
use miden_protocol::block::BlockNumber;
use miden_protocol::crypto::rand::RandomCoin;
use miden_protocol::errors::NoteError;

use super::*;
Expand Down Expand Up @@ -309,4 +315,23 @@ mod tests {

assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
}

#[test]
fn create_rejects_empty_assets() {
let sender = AccountId::dummy([4u8; 15], AccountIdVersion::Version1, AccountType::Private);
let storage = P2ideNoteStorage::new(dummy_account(), None, None);
let mut rng = RandomCoin::new(Word::default());

let err = P2ideNote::create(
sender,
storage,
vec![],
NoteType::Private,
NoteAttachments::default(),
&mut rng,
)
.expect_err("empty assets must be rejected");

assert!(matches!(err, NoteError::MissingAsset));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use anyhow::Context;
use assert_matches::assert_matches;
use miden_protocol::Word;
use miden_protocol::account::{Account, AccountId, AccountType};
use miden_protocol::asset::FungibleAsset;
use miden_protocol::batch::ProposedBatch;
use miden_protocol::block::BlockNumber;
use miden_protocol::crypto::merkle::MerkleError;
Expand Down Expand Up @@ -60,7 +61,7 @@ fn setup_chain() -> TestSetup {
let account1 = generate_account(&mut builder);
let account2 = generate_account(&mut builder);
let note1 = builder
.add_p2id_note(account1.id(), account2.id(), &[], NoteType::Public)
.add_p2id_note(account1.id(), account2.id(), &[FungibleAsset::mock(1)], NoteType::Public)
.expect("adding p2id note1 should work");
let mut chain = builder.build().expect("genesis should be valid");
chain.prove_next_block().expect("valid setup");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,14 @@ async fn proposed_block_fails_on_duplicate_output_note() -> anyhow::Result<()> {
async fn proposed_block_fails_on_invalid_proof_or_missing_note_inclusion_reference_block()
-> anyhow::Result<()> {
let mut builder = MockChain::builder();
let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?;
// account0 funds the P2ID note via the spawn-note flow, so it needs a balance to give.
let account0 =
builder.add_existing_mock_account_with_assets(Auth::IncrNonce, [FungibleAsset::mock(1)])?;
let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let p2id_note = P2idNote::create(
account0.id(),
account1.id(),
vec![],
vec![FungibleAsset::mock(1)],
NoteType::Private,
NoteAttachments::default(),
builder.rng_mut(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,18 @@ async fn proposed_block_authenticating_unauthenticated_notes() -> anyhow::Result
let mut builder = MockChain::builder();
let account0 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let account1 = builder.add_existing_mock_account(Auth::IncrNonce)?;
let note0 = builder.add_p2id_note(sender_id, account0.id(), &[], NoteType::Private)?;
let note1 = builder.add_p2id_note(sender_id, account1.id(), &[], NoteType::Public)?;
let note0 = builder.add_p2id_note(
sender_id,
account0.id(),
&[FungibleAsset::mock(1)],
NoteType::Private,
)?;
let note1 = builder.add_p2id_note(
sender_id,
account1.id(),
&[FungibleAsset::mock(1)],
NoteType::Public,
)?;
let chain = builder.build()?;

// These txs will use block1 as the reference block.
Expand Down
10 changes: 1 addition & 9 deletions crates/miden-testing/src/kernel_tests/tx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,11 @@ pub fn input_note_data_ptr(note_idx: u32) -> memory::MemoryAddress {
struct TestSetup {
mock_chain: MockChain,
account: Account,
p2id_note_0_assets: Note,
p2id_note_1_asset: Note,
p2id_note_2_assets: Note,
}

/// Return a [`TestSetup`], whose notes contain 0, 1 and 2 assets respectively.
/// Return a [`TestSetup`], whose notes contain 1 and 2 assets respectively.
fn setup_test() -> anyhow::Result<TestSetup> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should retain the 0 assets test case here as the tests that use this setup, like the get_assets test, behave meaningfully different from the non-zero asset use case. So, maybe we can just change the test setup to return P2ANY notes instead of P2ID notes (see create_p2any_note, add_p2any_note).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I will make this change.

let mut builder = MockChain::builder();

Expand Down Expand Up @@ -145,12 +144,6 @@ fn setup_test() -> anyhow::Result<TestSetup> {
)?;

// Notes
let p2id_note_0_assets = builder.add_p2id_note(
ACCOUNT_ID_SENDER.try_into().unwrap(),
account.id(),
&[],
NoteType::Public,
)?;
let p2id_note_1_asset = builder.add_p2id_note(
ACCOUNT_ID_SENDER.try_into().unwrap(),
account.id(),
Expand All @@ -169,7 +162,6 @@ fn setup_test() -> anyhow::Result<TestSetup> {
anyhow::Ok(TestSetup {
mock_chain,
account,
p2id_note_0_assets,
p2id_note_1_asset,
p2id_note_2_assets,
})
Expand Down
33 changes: 8 additions & 25 deletions crates/miden-testing/src/kernel_tests/tx/test_input_note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ use super::{TestSetup, setup_test};
use crate::TxContextInput;

/// Check that the assets number and assets commitment obtained from the
/// `input_note::get_assets_info` procedure is correct for each note with zero, one and two
/// `input_note::get_assets_info` procedure is correct for each note with one and two
/// different assets.
#[tokio::test]
async fn test_get_asset_info() -> anyhow::Result<()> {
let TestSetup {
mock_chain,
account,
p2id_note_0_assets,
p2id_note_1_asset,
p2id_note_2_assets,
} = setup_test()?;
Expand Down Expand Up @@ -51,25 +50,18 @@ async fn test_get_asset_info() -> anyhow::Result<()> {
use miden::protocol::input_note

begin
{check_note_0}

{check_note_1}

{check_note_2}
end
",
check_note_0 = check_asset_info_code(
0,
p2id_note_0_assets.assets().commitment(),
p2id_note_0_assets.assets().num_assets()
),
check_note_1 = check_asset_info_code(
1,
0,
p2id_note_1_asset.assets().commitment(),
p2id_note_1_asset.assets().num_assets()
),
check_note_2 = check_asset_info_code(
2,
1,
p2id_note_2_assets.assets().commitment(),
p2id_note_2_assets.assets().num_assets()
),
Expand All @@ -81,7 +73,7 @@ async fn test_get_asset_info() -> anyhow::Result<()> {
.build_tx_context(
TxContextInput::AccountId(account.id()),
&[],
&[p2id_note_0_assets, p2id_note_1_asset, p2id_note_2_assets],
&[p2id_note_1_asset, p2id_note_2_assets],
)?
.tx_script(tx_script)
.build()?;
Expand All @@ -98,7 +90,6 @@ async fn test_get_recipient_and_metadata() -> anyhow::Result<()> {
let TestSetup {
mock_chain,
account,
p2id_note_0_assets: _,
p2id_note_1_asset,
p2id_note_2_assets: _,
} = setup_test()?;
Expand Down Expand Up @@ -151,7 +142,6 @@ async fn test_get_sender() -> anyhow::Result<()> {
let TestSetup {
mock_chain,
account,
p2id_note_0_assets: _,
p2id_note_1_asset,
p2id_note_2_assets: _,
} = setup_test()?;
Expand Down Expand Up @@ -194,13 +184,12 @@ async fn test_get_sender() -> anyhow::Result<()> {
}

/// Check that the assets number and assets data obtained from the `input_note::get_assets`
/// procedure is correct for each note with zero, one and two different assets.
/// procedure is correct for each note with one and two different assets.
#[tokio::test]
async fn test_get_assets() -> anyhow::Result<()> {
let TestSetup {
mock_chain,
account,
p2id_note_0_assets,
p2id_note_1_asset,
p2id_note_2_assets,
} = setup_test()?;
Expand Down Expand Up @@ -276,16 +265,13 @@ async fn test_get_assets() -> anyhow::Result<()> {
use miden::protocol::input_note

begin
{check_note_0}

{check_note_1}

{check_note_2}
end
",
check_note_0 = check_assets_code(0, 0, &p2id_note_0_assets),
check_note_1 = check_assets_code(1, 8, &p2id_note_1_asset),
check_note_2 = check_assets_code(2, 16, &p2id_note_2_assets),
check_note_1 = check_assets_code(0, 0, &p2id_note_1_asset),
check_note_2 = check_assets_code(1, 8, &p2id_note_2_assets),
);

let tx_script = CodeBuilder::default().compile_tx_script(code)?;
Expand All @@ -294,7 +280,7 @@ async fn test_get_assets() -> anyhow::Result<()> {
.build_tx_context(
TxContextInput::AccountId(account.id()),
&[],
&[p2id_note_0_assets, p2id_note_1_asset, p2id_note_2_assets],
&[p2id_note_1_asset, p2id_note_2_assets],
)?
.tx_script(tx_script)
.build()?;
Expand All @@ -311,7 +297,6 @@ async fn test_get_storage_info() -> anyhow::Result<()> {
let TestSetup {
mock_chain,
account,
p2id_note_0_assets: _,
p2id_note_1_asset,
p2id_note_2_assets: _,
} = setup_test()?;
Expand Down Expand Up @@ -361,7 +346,6 @@ async fn test_get_script_root() -> anyhow::Result<()> {
let TestSetup {
mock_chain,
account,
p2id_note_0_assets: _,
p2id_note_1_asset,
p2id_note_2_assets: _,
} = setup_test()?;
Expand Down Expand Up @@ -404,7 +388,6 @@ async fn test_get_serial_number() -> anyhow::Result<()> {
let TestSetup {
mock_chain,
account,
p2id_note_0_assets: _,
p2id_note_1_asset,
p2id_note_2_assets: _,
} = setup_test()?;
Expand Down
Loading
Loading