Skip to content

feat(standards): add expiration to network account config notes - #3715

Open
onurinanc wants to merge 12 commits into
nextfrom
fix-unconsumed-admin-action-notes
Open

feat(standards): add expiration to network account config notes#3715
onurinanc wants to merge 12 commits into
nextfrom
fix-unconsumed-admin-action-notes

Conversation

@onurinanc

Copy link
Copy Markdown
Collaborator

Summary

  • Encode an optional expiry block in the unused fourth felt of the NetworkAccountTarget attachment.
  • Add assert_not_expired, which caps the transaction expiration block delta so a stale reference block cannot pass the check.
  • Call assert_not_expired from the config note scripts and add an optional expiry to their builders.
  • Cover delayed consumption of a PauseConfig note held past its expiry block.

Part of #3560.

@PhilippGackstatter PhilippGackstatter left a comment

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.

Looks good! Left a few suggestions, with the important one being avoiding finding and loading the attachment twice.

#!
#! The attachment is expected to have the following layout:
#! [account_id_suffix, account_id_prefix, exec_hint_tag, 0]
#! [account_id_suffix, account_id_prefix, exec_hint_tag, expiry_block]

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.

nit(consistency): in other places we use expiration_block_num rather than expiry. Consider calling it that. Applies to procedure names, too.

Comment on lines 73 to +82
# bind consumption to the targeted account: reject if the consuming account is not the note's
# target account
exec.network_account_target::active_account_matches_target_account
assert.err=ERR_BLOCKLIST_CONFIG_TARGET_ACCOUNT_MISMATCH
# => [pad(16)]

# reject a note held past the expiry block of its target attachment
exec.network_account_target::assert_not_expired
# => [pad(16)]

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.

While this pattern is really easy to read, we find and unhash the same attachment twice, which adds quite a few unnecessary cycles and hashes.

I think we should either:

  • load the attachment explicitly, then invoke the two procedures on a copy.
  • refactor network_account_target::active_account_matches_target_account into a single network_account_target::assert_target_id_and_expiration that does both checks.

I think the second option would be best, unless we need to check the target match individually from expiration, but I doubt this is or should be the case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nice one! I would go with your second option for two reasons:

  • It seems like the target matches are individually checked in agglayer notes
  • moving the assertion into the shared procedure would collapse all note specific error messages into a single one ERR_*_TARGET_ACCOUNT_MISMATCH

dup dup.2 u32lt assert.err=ERR_NETWORK_ACCOUNT_TARGET_EXPIRED
# => [reference_block, expiry_block]

u32wrapping_sub

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.

Since we checked above, sub is sufficient. We don't need wrapping behavior here.

Comment on lines +150 to +153
#! Otherwise the procedure applies two bounds, and both are required for the expiry to hold:
#! - it requires the transaction's reference block to lie strictly before the expiry block, and
#! - it caps the transaction expiration block delta at the number of blocks left until the expiry
#! block, or [`DEFAULT_EXPIRATION_BLOCK_DELTA`] if that is smaller.

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.

Makes sense 👍

Comment on lines +232 to +235
# cap the inclusion window at whichever is smaller: the default delta, or the blocks left
# until the expiration block
push.DEFAULT_EXPIRATION_BLOCK_DELTA u32min
# => [expiration_block_delta]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not sure this is desirable.
I can imagine some config notes' expiration could be longer than DEFAULT_EXPIRATION_BLOCK_DELTA, in which case a user-supplied expiration would just be ignored.

I think that if the caller has taken enough caution to set the expiration != NETWORK_ACCOUNT_TARGET_NO_EXPIRATION, then they will have provided a meaningful value that they care about enforcing, rather than being min-capped at the default

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think the documentation makes this a bit confusing as the user-supplied expiration is not
ignored here. It is enforced by the assert just above:

dup dup.2 u32lt assert.err=ERR_NETWORK_ACCOUNT_TARGET_EXPIRED

The u32min only bounds how long after proving the transaction may still be included into the chain. Also, dropping the u32min would break long expirations as the kernel's update_expiration_block_delta only accepts a delta of at most 65535 blocks and aborts the transaction otherwise, so passing expiration_block_num - reference_block would make any note expiring more than 65535 blocks out permanently unconsumable.

So I think fixing the documentation is sufficient here. Or, am I missing something?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I might be missing something, but I think the numbers don't add up. Walking through some concrete example:

  • DEFAULT_EXPIRATION_BLOCK_DELTA = 20
  • assume the tx is proven against a reference_block = 100

Case 1: tx is expired, can't even be proven, expiration_block_num = 90

throws ERR_NETWORK_ACCOUNT_TARGET_EXPIRED

Case 2: tx can be proven, valid for a few more blocks, expiration_block_num = 105

blocks_until_expiration = 5
expiration_block_delta = min(5, 20) = 5

Case 2a

  • the current block is 102, tx succeeds as expected ✅

Case 2b

  • the current block is 122, tx fails as expected because it's too stale ✅

Case 3: tx is valid for more than the default, expiration_block_num = 125

blocks_until_expiration = 25
expiration_block_delta = min(25, 20) = 20

update_expiration_block_delta(20) -> admits at most 20 block past

Case 3a:

  • current block is 102. Tx is fresh enough ✅

Case 3b:

  • current block is 122. Tx is "fresh enough", i.e. within the caller-supplied expiration_block_num of 25, but because we updated update_expiration_block_delta with a value of 20, the transaction fails - unexpected

I might have made some mistakes here, I still get confused sometimes about the expiration, so please feel free to point any gaps in my understanding.

Comment thread crates/miden-standards/asm/standards/attachments/network_account_target.masm Outdated
Comment thread crates/miden-standards/asm/standards/attachments/network_account_target.masm Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants