From 7dbd6083f49fceee41e328807805ebaef0029aef Mon Sep 17 00:00:00 2001 From: igamigo Date: Mon, 30 Sep 2024 11:58:21 -0300 Subject: [PATCH] feat: Set transaction expiry block delta (#897) * feat: Set transaction recency conditions * Correctly build stack outputs for verifier * Format correctly --- CHANGELOG.md | 1 + miden-lib/asm/kernels/transaction/api.masm | 26 ++++++ .../asm/kernels/transaction/lib/epilogue.masm | 8 +- .../asm/kernels/transaction/lib/memory.masm | 19 ++++ .../asm/kernels/transaction/lib/prologue.masm | 8 ++ miden-lib/asm/kernels/transaction/lib/tx.masm | 55 +++++++++++- miden-lib/asm/miden/kernel_proc_offsets.masm | 28 +++++- miden-lib/src/transaction/memory.rs | 5 +- miden-lib/src/transaction/mod.rs | 63 ++++++++++--- miden-lib/src/transaction/outputs.rs | 3 + .../src/transaction/procedures/kernel_v0.rs | 6 +- miden-tx/src/errors/tx_kernel_errors.rs | 4 +- miden-tx/src/prover/mod.rs | 1 + .../src/tests/kernel_tests/test_epilogue.rs | 90 ++++++++++++++++++- miden-tx/src/verifier/mod.rs | 1 + miden-tx/tests/integration/scripts/faucet.rs | 2 +- miden-tx/tests/integration/wallet/mod.rs | 2 +- objects/src/transaction/outputs.rs | 4 + objects/src/transaction/proven_tx.rs | 17 ++++ 19 files changed, 318 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ee7be277..a29326653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.6.0 (TBD) +- Implemented kernel procedure to set transaction expiration block delta (#897). - Created a proving service that receives `TransactionWitness` and returns the proof using gRPC (#881). - Made note scripts public (#880). - Implemented serialization for `TransactionWitness`, `ChainMmr`, `TransactionInputs` and `TransactionArgs` (#888). diff --git a/miden-lib/asm/kernels/transaction/api.masm b/miden-lib/asm/kernels/transaction/api.masm index 26327d0c2..7fee6b195 100644 --- a/miden-lib/asm/kernels/transaction/api.masm +++ b/miden-lib/asm/kernels/transaction/api.masm @@ -893,6 +893,32 @@ export.end_foreign_context dropw end +#! Updates the transaction expiration time delta. +#! Once set, the delta can be decreased but not increased. +#! +#! The input block height delta is added to the reference block in order to output an upper limit +#! up until which the transaction will be considered valid (not expired). +#! +#! Inputs: [block_height_delta, ...] +#! Output: [...] +#! +#! Where: +#! - block_height_delta is the desired expiration time delta (1 to 0xFFFF). +export.update_expiration_block_num + exec.tx::update_expiration_block_num +end + +#! Gets the transaction expiration delta. +#! +#! Inputs: [...] +#! Output: [block_height_delta, ...] +#! +#! Where: +#! - block_height_delta is the stored expiration time delta (1 to 0xFFFF). +export.get_expiration_delta + exec.tx::get_expiration_delta +end + #! Executes a kernel procedure specified by its offset. #! #! Inputs: [procedure_offset, , ] diff --git a/miden-lib/asm/kernels/transaction/lib/epilogue.masm b/miden-lib/asm/kernels/transaction/lib/epilogue.masm index 5db8f4681..ecb58d1ce 100644 --- a/miden-lib/asm/kernels/transaction/lib/epilogue.masm +++ b/miden-lib/asm/kernels/transaction/lib/epilogue.masm @@ -3,6 +3,7 @@ use.kernel::asset_vault use.kernel::constants use.kernel::memory use.kernel::note +use.kernel::tx use.std::crypto::hashes::native @@ -224,7 +225,7 @@ end #! - asserts that the input and output vault roots are equal #! #! Stack: [] -#! Output: [OUTPUT_NOTES_COMMITMENT, FINAL_ACCOUNT_HASH] +#! Output: [OUTPUT_NOTES_COMMITMENT, FINAL_ACCOUNT_HASH, tx_expiration_block_num] #! #! - OUTPUT_NOTES_COMMITMENT is the commitment of the output notes #! - FINAL_ACCOUNT_HASH is the final account hash @@ -308,4 +309,9 @@ export.finalize_transaction # assert no net creation or destruction of assets over the transaction exec.memory::get_input_vault_root exec.memory::get_output_vault_root assert_eqw.err=ERR_EPILOGUE_ASSETS_DONT_ADD_UP # => [OUTPUT_NOTES_COMMITMENT, FINAL_ACCOUNT_HASH] + + swapdw + exec.memory::get_expiration_block_num + swap drop swapdw + # => [OUTPUT_NOTES_COMMITMENT, FINAL_ACCOUNT_HASH, tx_expiration_block_num] end diff --git a/miden-lib/asm/kernels/transaction/lib/memory.masm b/miden-lib/asm/kernels/transaction/lib/memory.masm index 5abfcc947..4e51b9064 100644 --- a/miden-lib/asm/kernels/transaction/lib/memory.masm +++ b/miden-lib/asm/kernels/transaction/lib/memory.masm @@ -32,6 +32,9 @@ const.CURRENT_ACCOUNT_DATA_PTR=5 # The memory address at which the native account's new code commitment is stored. const.NEW_CODE_ROOT_PTR=6 +# The memory address at which the absolute expiration block number is stored. +const.TX_EXPIRATION_BLOCK_NUM_PTR=7 + # GLOBAL INPUTS # ------------------------------------------------------------------------------------------------- @@ -829,6 +832,22 @@ export.set_new_acct_code_commitment mem_storew end +#! Sets the transaction expiration block number. +#! +#! Inputs: [tx_expiration_block_num, ...] +#! Output: [...] +export.set_expiration_block_num + push.TX_EXPIRATION_BLOCK_NUM_PTR mem_store +end + +#! Gets the transaction expiration block number. +#! +#! Inputs: [] +#! Output: [tx_expiration_block_num] +export.get_expiration_block_num + push.TX_EXPIRATION_BLOCK_NUM_PTR mem_load +end + #! Returns the number of procedures contained in the account code. #! #! Stack: [] diff --git a/miden-lib/asm/kernels/transaction/lib/prologue.masm b/miden-lib/asm/kernels/transaction/lib/prologue.masm index dd13f42cc..d783faa09 100644 --- a/miden-lib/asm/kernels/transaction/lib/prologue.masm +++ b/miden-lib/asm/kernels/transaction/lib/prologue.masm @@ -9,6 +9,12 @@ use.kernel::constants use.kernel::memory use.kernel::utils +# CONSTS +# ================================================================================================= + +# Max U32 value, used for initializing the expiration block number +const.MAX_BLOCK_NUM=0xFFFFFFFF + # ERRORS # ================================================================================================= @@ -1233,4 +1239,6 @@ export.prepare_transaction exec.process_input_notes_data exec.process_tx_script_root # => [] + + push.MAX_BLOCK_NUM exec.memory::set_expiration_block_num end diff --git a/miden-lib/asm/kernels/transaction/lib/tx.masm b/miden-lib/asm/kernels/transaction/lib/tx.masm index 53799a10d..972612fc6 100644 --- a/miden-lib/asm/kernels/transaction/lib/tx.masm +++ b/miden-lib/asm/kernels/transaction/lib/tx.masm @@ -15,6 +15,9 @@ const.ENCRYPTED_NOTE=3 # 0b11 # Two raised to the power of 38 (2^38), used for shifting the note type value const.TWO_POW_38=274877906944 +# Max value for U16, used as the upper limit for expiration block delta +const.EXPIRY_UPPER_LIMIT=0xFFFF+1 + # ERRORS # ================================================================================================= @@ -61,6 +64,9 @@ const.ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS=0x00020051 # Note idx must be within [0, num_of_notes] const.ERR_INVALID_NOTE_IDX=0x00020052 +# Input transaction expiration block delta is not within 0x1 and 0xFFFF. +const.ERR_INVALID_TX_EXPIRATION_DELTA=0x00020055 + # EVENTS # ================================================================================================= @@ -73,7 +79,7 @@ const.NOTE_AFTER_CREATED_EVENT=131084 const.NOTE_BEFORE_ADD_ASSET_EVENT=131085 # Event emitted after an ASSET is added to a note const.NOTE_AFTER_ADD_ASSET_EVENT=131086 - + #! Returns the block hash of the reference block to memory. #! #! Stack: [] @@ -178,6 +184,53 @@ proc.add_non_fungible_asset_to_note # => [note_ptr, note_idx] end +#! Updates the transaction expiration block number. +#! +#! The input block_height_delta is added to the block reference number in order to output an upper +#! limit at which the transaction will be considered valid (not expired). +#! This value can be later decreased, but not increased. +#! +#! Inputs: [block_height_delta, ...] +#! Output: [...] +#! +#! Where: +#! - block_height_delta is the desired expiration time delta (1 to 0xFFFF). +export.update_expiration_block_num + # Ensure block_height_delta is between 1 and 0xFFFF (inclusive) + dup neq.0 assert.err=ERR_INVALID_TX_EXPIRATION_DELTA + dup push.EXPIRY_UPPER_LIMIT lt assert.err=ERR_INVALID_TX_EXPIRATION_DELTA + # => [block_height_delta] + + exec.get_block_number add + # => [absolute_expiration_num] + + # Load the current stored delta from memory + dup exec.memory::get_expiration_block_num + # => [stored_expiration_block_num, absolute_expiration_num, absolute_expiration_num] + + # Check if block_height_delta is greater + u32lt + if.true + # Set new expiration delta + exec.memory::set_expiration_block_num + else + drop + end +end + +#! Gets the transaction expiration delta. +#! +#! Inputs: [...] +#! Output: [block_height_delta, ...] +#! +#! Where: +#! - block_height_delta is the stored expiration time delta (1 to 0xFFFF). +export.get_expiration_delta + exec.memory::get_expiration_block_num exec.get_block_number + sub +end + + #! Adds a fungible asset to a note. If the note already holds an asset issued by the #! same faucet id the two quantities are summed up and the new quantity is stored at the #! old position in the note. In the other case, the asset is stored at the next available diff --git a/miden-lib/asm/miden/kernel_proc_offsets.masm b/miden-lib/asm/miden/kernel_proc_offsets.masm index 2f48ec6f2..d5a5028d9 100644 --- a/miden-lib/asm/miden/kernel_proc_offsets.masm +++ b/miden-lib/asm/miden/kernel_proc_offsets.masm @@ -38,6 +38,8 @@ const.GET_BLOCK_HASH_OFFSET=26 const.GET_BLOCK_NUMBER_OFFSET=27 const.START_FOREIGN_CONTEXT_OFFSET=28 const.END_FOREIGN_CONTEXT_OFFSET=29 +const.UPDATE_EXPIRATION_BLOCK_NUM_OFFSET=30 +const.GET_EXPIRATION_DELTA_OFFSET=31 # ACCESSORS # ------------------------------------------------------------------------------------------------- @@ -105,7 +107,7 @@ end #! Returns an offset of the `get_account_item` kernel procedure. #! #! Stack: [] -#! Output: [proc_offset] +#! Output: [proc_offset] #! #! Where: #! - proc_offset is the offset of the `get_account_item` kernel procedure required to get the @@ -258,6 +260,30 @@ export.get_block_number_offset push.GET_BLOCK_NUMBER_OFFSET end +#! Returns an offset of the `update_expiration_block_num` kernel procedure. +#! +#! Stack: [] +#! Output: [proc_offset] +#! +#! Where: +#! - proc_offset is the offset of the `set_tx_expiration_delta` kernel procedure required to get the +#! address where this procedure is stored. +export.update_expiration_block_num_offset + push.UPDATE_EXPIRATION_BLOCK_NUM_OFFSET +end + +#! Returns an offset of the `get_expiration_delta` kernel procedure. +#! +#! Stack: [] +#! Output: [proc_offset] +#! +#! Where: +#! - proc_offset is the offset of the `set_tx_expiration_delta` kernel procedure required to get the +#! address where this procedure is stored. +export.get_expiration_block_delta_offset + push.GET_EXPIRATION_DELTA_OFFSET +end + #! Returns an offset of the `get_block_hash` kernel procedure. #! #! Stack: [] diff --git a/miden-lib/src/transaction/memory.rs b/miden-lib/src/transaction/memory.rs index b5b33cce5..f8ee290de 100644 --- a/miden-lib/src/transaction/memory.rs +++ b/miden-lib/src/transaction/memory.rs @@ -14,7 +14,7 @@ pub type StorageSlot = u8; // // | Section | Start address | End address | // | ------------- | :------------:| :-----------:| -// | Bookkeeping | 0 | 6 | +// | Bookkeeping | 0 | 7 | // | Global inputs | 100 | 105 | // | Block header | 200 | 208 | // | Chain MMR | 300 | 332? | @@ -73,6 +73,9 @@ pub const CURRENT_ACCOUNT_DATA_PTR: MemoryAddress = 5; /// The memory address at which the native account's new code commitment is stored. pub const NEW_CODE_ROOT_PTR: MemoryAddress = 6; +/// The memory address at which the transaction expiration block number is stored. +pub const TX_EXPIRATION_BLOCK_NUM_PTR: MemoryAddress = 7; + // GLOBAL INPUTS // ------------------------------------------------------------------------------------------------ diff --git a/miden-lib/src/transaction/mod.rs b/miden-lib/src/transaction/mod.rs index 623d6858d..78a5cc519 100644 --- a/miden-lib/src/transaction/mod.rs +++ b/miden-lib/src/transaction/mod.rs @@ -13,6 +13,7 @@ use miden_objects::{ Digest, Felt, TransactionOutputError, Word, EMPTY_WORD, }; use miden_stdlib::StdLibrary; +use outputs::EXPIRATION_BLOCK_ELEMENT_IDX; use super::MidenLib; @@ -160,8 +161,28 @@ impl TransactionKernel { .expect("Invalid stack input") } - pub fn build_output_stack(final_acct_hash: Digest, output_notes_hash: Digest) -> StackOutputs { + /// Builds the stack for expected transaction execution outputs. + /// The transaction kernel's output stack is formed like so: + /// + /// ```text + /// [ + /// expiration_block_num, + /// OUTPUT_NOTES_COMMITMENT, + /// FINAL_ACCOUNT_HASH, + /// ] + /// ``` + /// + /// Where: + /// - OUTPUT_NOTES_COMMITMENT is a commitment to the output notes. + /// - FINAL_ACCOUNT_HASH is a hash of the account's final state. + /// - expiration_block_num is the block number at which the transaction will expire. + pub fn build_output_stack( + final_acct_hash: Digest, + output_notes_hash: Digest, + expiration_block_num: u32, + ) -> StackOutputs { let mut outputs: Vec = Vec::with_capacity(9); + outputs.push(Felt::from(expiration_block_num)); outputs.extend(final_acct_hash); outputs.extend(output_notes_hash); outputs.reverse(); @@ -174,12 +195,15 @@ impl TransactionKernel { /// /// The data on the stack is expected to be arranged as follows: /// - /// Stack: [CNC, FAH] + /// Stack: [CNC, FAH, tx_expiration_block_num] /// /// Where: /// - CNC is the commitment to the notes created by the transaction. /// - FAH is the final account hash of the account that the transaction is being executed /// against. + /// - tx_expiration_block_num is the block height at which the transaction will become expired, + /// defined by the sum of the execution block ref and the transaction's block expiration delta + /// (if set during transaction execution). /// /// # Errors /// Returns an error if: @@ -187,22 +211,27 @@ impl TransactionKernel { /// - Overflow addresses are not empty. pub fn parse_output_stack( stack: &StackOutputs, - ) -> Result<(Digest, Digest), TransactionOutputError> { + ) -> Result<(Digest, Digest, u32), TransactionOutputError> { let output_notes_hash = stack .get_stack_word(OUTPUT_NOTES_COMMITMENT_WORD_IDX * 4) .expect("first word missing") .into(); + let final_account_hash = stack .get_stack_word(FINAL_ACCOUNT_HASH_WORD_IDX * 4) .expect("second word missing") .into(); - // make sure that the stack has been properly cleaned - if stack.get_stack_word(8).expect("third word missing") != EMPTY_WORD { - return Err(TransactionOutputError::OutputStackInvalid( - "Third word on output stack should consist only of ZEROs".into(), - )); - } + let expiration_block_num = stack + .get_stack_item(EXPIRATION_BLOCK_ELEMENT_IDX) + .expect("element on index 8 missing"); + + let expiration_block_num = u32::try_from(expiration_block_num.as_int()).map_err(|_| { + TransactionOutputError::OutputStackInvalid( + "Expiration block number should be smaller than u32::MAX".into(), + ) + })?; + if stack.get_stack_word(12).expect("fourth word missing") != EMPTY_WORD { return Err(TransactionOutputError::OutputStackInvalid( "Fourth word on output stack should consist only of ZEROs".into(), @@ -214,7 +243,7 @@ impl TransactionKernel { )); } - Ok((final_account_hash, output_notes_hash)) + Ok((final_account_hash, output_notes_hash, expiration_block_num)) } // TRANSACTION OUTPUT PARSER @@ -224,12 +253,15 @@ impl TransactionKernel { /// /// The output stack is expected to be arrange as follows: /// - /// Stack: [CNC, FAH] + /// Stack: [CNC, FAH, tx_expiration_block_num] /// /// Where: /// - CNC is the commitment to the notes created by the transaction. /// - FAH is the final account hash of the account that the transaction is being executed /// against. + /// - tx_expiration_block_num is the block height at which the transaction will become expired, + /// defined by the sum of the execution block ref and the transaction's block expiration delta + /// (if set during transaction execution). /// /// The actual data describing the new account state and output notes is expected to be located /// in the provided advice map under keys CNC and FAH. @@ -238,7 +270,8 @@ impl TransactionKernel { adv_map: &AdviceMap, output_notes: Vec, ) -> Result { - let (final_acct_hash, output_notes_hash) = Self::parse_output_stack(stack)?; + let (final_acct_hash, output_notes_hash, expiration_block_num) = + Self::parse_output_stack(stack)?; // parse final account state let final_account_data: &[Word] = group_slice_elements( @@ -258,7 +291,11 @@ impl TransactionKernel { )); } - Ok(TransactionOutputs { account, output_notes }) + Ok(TransactionOutputs { + account, + output_notes, + expiration_block_num, + }) } } diff --git a/miden-lib/src/transaction/outputs.rs b/miden-lib/src/transaction/outputs.rs index 3cf55a071..da3cfba80 100644 --- a/miden-lib/src/transaction/outputs.rs +++ b/miden-lib/src/transaction/outputs.rs @@ -17,6 +17,9 @@ pub const OUTPUT_NOTES_COMMITMENT_WORD_IDX: usize = 0; /// The index of the word at which the final account hash is stored on the output stack. pub const FINAL_ACCOUNT_HASH_WORD_IDX: usize = 1; +/// The index of the item at which the expiration block height is stored on the output stack. +pub const EXPIRATION_BLOCK_ELEMENT_IDX: usize = 8; + // ACCOUNT HEADER EXTRACTOR // ================================================================================================ diff --git a/miden-lib/src/transaction/procedures/kernel_v0.rs b/miden-lib/src/transaction/procedures/kernel_v0.rs index f318b41e5..76e21aad8 100644 --- a/miden-lib/src/transaction/procedures/kernel_v0.rs +++ b/miden-lib/src/transaction/procedures/kernel_v0.rs @@ -6,7 +6,7 @@ use miden_objects::{digest, Digest, Felt}; // ================================================================================================ /// Hashes of all dynamically executed procedures from the kernel 0. -pub const KERNEL0_PROCEDURES: [Digest; 30] = [ +pub const KERNEL0_PROCEDURES: [Digest; 32] = [ // account_vault_add_asset digest!(0xb8815bfacbdcb4c2, 0x6c7e694cf4f6a517, 0xf6233da2865ca264, 0xe51463cd0df6e896), // account_vault_get_balance @@ -67,4 +67,8 @@ pub const KERNEL0_PROCEDURES: [Digest; 30] = [ digest!(0x9d231f21bd27ff27, 0x5cc4476fad12b66d, 0x82f40fd18e7abb0a, 0xc09c240f2a1d82af), // end_foreign_context digest!(0x3770db711ce9aaf1, 0xb6f3c929151a5d52, 0x3ed145ec5dbee85f, 0xf979d975d7951bf6), + // update_expiration_block_num + digest!(0xb5b796c8143e57de, 0x43d6914fb889f3ba, 0xf65308f85c7c73b7, 0xe86bfcaccebe6b49), + // get_expiration_delta + digest!(0x2d93af519fa32359, 0x14275beadcb2ab9c, 0x68f9336f45c32c86, 0x75ee8ba0f3c11c83), ]; diff --git a/miden-tx/src/errors/tx_kernel_errors.rs b/miden-tx/src/errors/tx_kernel_errors.rs index 96e831117..5e46aa7e9 100644 --- a/miden-tx/src/errors/tx_kernel_errors.rs +++ b/miden-tx/src/errors/tx_kernel_errors.rs @@ -86,8 +86,9 @@ pub const ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS: u32 = 131153; pub const ERR_INVALID_NOTE_IDX: u32 = 131154; pub const ERR_KERNEL_PROCEDURE_OFFSET_OUT_OF_BOUNDS: u32 = 131155; pub const ERR_CURRENT_ACCOUNT_IS_NOT_NATIVE: u32 = 131156; +pub const ERR_INVALID_TX_EXPIRATION_DELTA: u32 = 131157; -pub const KERNEL_ERRORS: [(u32, &str); 85] = [ +pub const KERNEL_ERRORS: [(u32, &str); 86] = [ (ERR_FAUCET_RESERVED_DATA_SLOT, "For faucets, storage slot 254 is reserved and can not be used with set_account_item procedure"), (ERR_ACCT_MUST_BE_A_FAUCET, "Procedure can only be called from faucet accounts"), (ERR_P2ID_WRONG_NUMBER_OF_INPUTS, "P2ID scripts expect exactly 1 note input"), @@ -173,4 +174,5 @@ pub const KERNEL_ERRORS: [(u32, &str); 85] = [ (ERR_INVALID_FAUCET_STORAGE_OFFSET, "Storage offset is invalid for a faucet account (0 is prohibited being the reserved faucet data slot)"), (ERR_KERNEL_PROCEDURE_OFFSET_OUT_OF_BOUNDS, "Provided kernel procedure offset is out of bounds"), (ERR_CURRENT_ACCOUNT_IS_NOT_NATIVE, "Procedure can be called only for the native account"), + (ERR_INVALID_TX_EXPIRATION_DELTA, "Invalid transaction expiration block delta was set."), ]; diff --git a/miden-tx/src/prover/mod.rs b/miden-tx/src/prover/mod.rs index 3452895d0..098ff3999 100644 --- a/miden-tx/src/prover/mod.rs +++ b/miden-tx/src/prover/mod.rs @@ -108,6 +108,7 @@ impl TransactionProver for LocalTransactionProver { account.init_hash(), tx_outputs.account.hash(), block_hash, + tx_outputs.expiration_block_num, proof, ) .add_input_notes(input_notes) diff --git a/miden-tx/src/tests/kernel_tests/test_epilogue.rs b/miden-tx/src/tests/kernel_tests/test_epilogue.rs index 10256ed21..bd160b1d5 100644 --- a/miden-tx/src/tests/kernel_tests/test_epilogue.rs +++ b/miden-tx/src/tests/kernel_tests/test_epilogue.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{string::ToString, vec::Vec}; use miden_lib::transaction::{ memory::{NOTE_MEM_SIZE, OUTPUT_NOTE_ASSET_HASH_OFFSET, OUTPUT_NOTE_SECTION_OFFSET}, @@ -8,12 +8,15 @@ use miden_objects::{ accounts::Account, transaction::{OutputNote, OutputNotes}, }; -use vm_processor::ONE; +use vm_processor::{Felt, ProcessState, ONE}; use super::{output_notes_data_procedure, ZERO}; use crate::{ assert_execution_error, - errors::tx_kernel_errors::{ERR_EPILOGUE_ASSETS_DONT_ADD_UP, ERR_NONCE_DID_NOT_INCREASE}, + errors::tx_kernel_errors::{ + ERR_EPILOGUE_ASSETS_DONT_ADD_UP, ERR_INVALID_TX_EXPIRATION_DELTA, + ERR_NONCE_DID_NOT_INCREASE, + }, testing::TransactionContextBuilder, tests::kernel_tests::read_root_mem_value, }; @@ -69,7 +72,8 @@ fn test_epilogue() { let mut expected_stack = Vec::with_capacity(16); expected_stack.extend(output_notes.commitment().as_elements().iter().rev()); expected_stack.extend(final_account.hash().as_elements().iter().rev()); - expected_stack.extend((8..16).map(|_| ZERO)); + expected_stack.push(Felt::from(u32::MAX)); // Value for tx expiration block number + expected_stack.extend((9..16).map(|_| ZERO)); assert_eq!( process.stack.build_stack_outputs().stack(), @@ -191,6 +195,84 @@ fn test_epilogue_asset_preservation_violation_too_many_fungible_input() { assert_execution_error!(process, ERR_EPILOGUE_ASSETS_DONT_ADD_UP); } +#[test] +fn test_block_expiration_height_monotonically_decreases() { + let tx_context = TransactionContextBuilder::with_standard_account(ONE).build(); + + let test_pairs: [(u64, u64); 3] = [(9, 12), (18, 3), (20, 20)]; + let code_template = " + use.kernel::prologue + use.kernel::tx + use.kernel::epilogue + + begin + exec.prologue::prepare_transaction + push.{value_1} + exec.tx::update_expiration_block_num + push.{value_2} + exec.tx::update_expiration_block_num + + push.{min_value} exec.tx::get_expiration_delta assert_eq + + exec.epilogue::finalize_transaction + end + "; + + for (v1, v2) in test_pairs { + let code = &code_template + .replace("{value_1}", &v1.to_string()) + .replace("{value_2}", &v2.to_string()) + .replace("{min_value}", &v2.min(v1).to_string()); + + let process = tx_context.execute_code(code).unwrap(); + + // Expiry block should be set to transaction's block + the stored expiration delta + // (which can only decrease, not increase) + let expected_expiry = v1.min(v2) + tx_context.tx_inputs().block_header().block_num() as u64; + assert_eq!(process.get_stack_item(8).as_int(), expected_expiry); + } +} + +#[test] +fn test_invalid_expiration_deltas() { + let tx_context = TransactionContextBuilder::with_standard_account(ONE).build(); + + let test_values = [0u64, u16::MAX as u64 + 1, u32::MAX as u64]; + let code_template = " + use.kernel::tx + + begin + push.{value_1} + exec.tx::update_expiration_block_num + end + "; + + for value in test_values { + let code = &code_template.replace("{value_1}", &value.to_string()); + let process = tx_context.execute_code(code); + + assert_execution_error!(process, ERR_INVALID_TX_EXPIRATION_DELTA); + } +} + +#[test] +fn test_no_expiration_delta_set() { + let tx_context = TransactionContextBuilder::with_standard_account(ONE).build(); + + let code_template = " + use.kernel::prologue + use.kernel::epilogue + + begin + exec.prologue::prepare_transaction + exec.epilogue::finalize_transaction + end + "; + let process = tx_context.execute_code(code_template).unwrap(); + // Default value should be equal to u32::max, set in the prologue + assert_eq!(process.get_stack_item(8).as_int() as u32, u32::MAX); +} + #[test] fn test_epilogue_increment_nonce_success() { let tx_context = TransactionContextBuilder::with_standard_account(ONE) diff --git a/miden-tx/src/verifier/mod.rs b/miden-tx/src/verifier/mod.rs index af69ecbe9..284d1d9ac 100644 --- a/miden-tx/src/verifier/mod.rs +++ b/miden-tx/src/verifier/mod.rs @@ -41,6 +41,7 @@ impl TransactionVerifier { let stack_outputs = TransactionKernel::build_output_stack( transaction.account_update().final_state_hash(), transaction.output_notes().commitment(), + transaction.expiration_block_num(), ); // verify transaction proof diff --git a/miden-tx/tests/integration/scripts/faucet.rs b/miden-tx/tests/integration/scripts/faucet.rs index 4147c2dc1..bc13e3a9d 100644 --- a/miden-tx/tests/integration/scripts/faucet.rs +++ b/miden-tx/tests/integration/scripts/faucet.rs @@ -86,7 +86,7 @@ fn prove_faucet_contract_mint_fungible_asset_succeeds() { .execute_transaction(faucet_account.id(), block_ref, ¬e_ids, tx_args) .unwrap(); - assert!(prove_and_verify_transaction(executed_transaction.clone()).is_ok()); + prove_and_verify_transaction(executed_transaction.clone()).unwrap(); let fungible_asset: Asset = FungibleAsset::new(faucet_account.id(), amount.into()).unwrap().into(); diff --git a/miden-tx/tests/integration/wallet/mod.rs b/miden-tx/tests/integration/wallet/mod.rs index 46335cdec..c9905b561 100644 --- a/miden-tx/tests/integration/wallet/mod.rs +++ b/miden-tx/tests/integration/wallet/mod.rs @@ -152,7 +152,7 @@ fn prove_send_note_without_asset_via_wallet() { .execute_transaction(sender_account.id(), block_ref, ¬e_ids, tx_args) .unwrap(); - assert!(prove_and_verify_transaction(executed_transaction.clone()).is_ok()); + prove_and_verify_transaction(executed_transaction.clone()).unwrap(); // clones account info let sender_account_storage = AccountStorage::new(vec![ diff --git a/objects/src/transaction/outputs.rs b/objects/src/transaction/outputs.rs index 3d38a3ae8..8de8e2db1 100644 --- a/objects/src/transaction/outputs.rs +++ b/objects/src/transaction/outputs.rs @@ -15,8 +15,12 @@ use crate::{ /// Describes the result of executing a transaction. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TransactionOutputs { + /// Information related to the account's final state. pub account: AccountHeader, + /// Set of output notes created by the transaction. pub output_notes: OutputNotes, + /// Defines up to which block the transaction is considered valid. + pub expiration_block_num: u32, } // OUTPUT NOTES diff --git a/objects/src/transaction/proven_tx.rs b/objects/src/transaction/proven_tx.rs index 1ba8faa19..4b31e985d 100644 --- a/objects/src/transaction/proven_tx.rs +++ b/objects/src/transaction/proven_tx.rs @@ -36,6 +36,9 @@ pub struct ProvenTransaction { /// The block hash of the last known block at the time the transaction was executed. block_ref: Digest, + /// The block number by which the transaction will expire, as defined by the executed scripts. + expiration_block_num: u32, + /// A STARK proof that attests to the correct execution of the transaction. proof: ExecutionProof, } @@ -81,6 +84,11 @@ impl ProvenTransaction { self.input_notes.iter().filter_map(|note| note.header()) } + /// Returns the block number at which the transaction will expire. + pub fn expiration_block_num(&self) -> u32 { + self.expiration_block_num + } + /// Returns an iterator over the nullifiers of all input notes in this transaction. /// /// This includes both authenticated and unauthenticated notes. @@ -145,6 +153,7 @@ impl Serializable for ProvenTransaction { self.input_notes.write_into(target); self.output_notes.write_into(target); self.block_ref.write_into(target); + self.expiration_block_num.write_into(target); self.proof.write_into(target); } } @@ -157,6 +166,7 @@ impl Deserializable for ProvenTransaction { let output_notes = OutputNotes::read_from(source)?; let block_ref = Digest::read_from(source)?; + let expiration_block_num = u32::read_from(source)?; let proof = ExecutionProof::read_from(source)?; let id = TransactionId::new( @@ -172,6 +182,7 @@ impl Deserializable for ProvenTransaction { input_notes, output_notes, block_ref, + expiration_block_num, proof, }; @@ -208,6 +219,9 @@ pub struct ProvenTransactionBuilder { /// Block [Digest] of the transaction's reference block. block_ref: Digest, + /// The block number by which the transaction will expire, as defined by the executed scripts. + expiration_block_num: u32, + /// A STARK proof that attests to the correct execution of the transaction. proof: ExecutionProof, } @@ -222,6 +236,7 @@ impl ProvenTransactionBuilder { initial_account_hash: Digest, final_account_hash: Digest, block_ref: Digest, + expiration_block_num: u32, proof: ExecutionProof, ) -> Self { Self { @@ -232,6 +247,7 @@ impl ProvenTransactionBuilder { input_notes: Vec::new(), output_notes: Vec::new(), block_ref, + expiration_block_num, proof, } } @@ -294,6 +310,7 @@ impl ProvenTransactionBuilder { input_notes, output_notes, block_ref: self.block_ref, + expiration_block_num: self.expiration_block_num, proof: self.proof, };