diff --git a/ledger/store/src/block/mod.rs b/ledger/store/src/block/mod.rs index 856499fb06..63454ff162 100644 --- a/ledger/store/src/block/mod.rs +++ b/ledger/store/src/block/mod.rs @@ -1197,7 +1197,7 @@ impl> BlockStore { /// Returns the current block height. pub fn current_block_height(&self) -> u32 { - u32::try_from(self.tree.read().number_of_leaves()).unwrap() - 1 + u32::try_from(self.tree.read().number_of_leaves()).unwrap().saturating_sub(1) } /// Returns the state root that contains the given `block height`. diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index 36ad74bdeb..d806fd4540 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -788,6 +788,9 @@ impl> VM { /// - The transaction is producing a duplicate transition public key /// - The transaction is another deployment in the block from the same public fee payer. /// - The transaction is an execution for a program following its deployment or redeployment in this block. + /// - The transaction is an execution with a state root whose corresponding block height is greater than or + /// equal to the latest block at which any of the execution's programs were deployed or upgraded. This + /// check is enforced only after `ConsensusVersion::V8` when program upgrades were introduced. /// /// - Note: If a transaction is a deployment for a program following its deployment or redeployment in this block, /// it is not aborted. Instead, it will be rejected and its fee will be consumed. @@ -847,16 +850,81 @@ impl> VM { } } - // If the transaction is an execution, ensure that its corresponding program(s) - // have not been deployed or redeployed prior to this transaction in this block. - // Note: This logic is compatible with deployments prior to `ConsensusVersion::V8`. - if let Transaction::Execute(_, _, execution, _) = transaction { - // If one of the component programs have been deployed or upgraded in this block, abort the transaction. + if let Transaction::Execute(_, id, execution, _) = transaction { + // If the transaction is an execution, ensure that its corresponding program(s) + // have not been deployed or redeployed prior to this transaction in this block. + // Note: This logic is compatible with deployments prior to `ConsensusVersion::V8`. for program_id in execution.transitions().map(|t| t.program_id()) { if deployments.contains(program_id) { return Some(format!("Program {program_id} has been deployed or upgraded in this block")); } } + + // If the current height is at `ConsensusVersion::V8` or higher, then enforce that the execution's + // state root is from a block whose height is greater than the latest block height at which any of + // the execution's programs were deployed or upgraded. + let current_height = self.block_store().current_block_height(); + let Ok(current_version) = N::CONSENSUS_VERSION(current_height) else { + return Some(format!("Failed to get consensus version for the current height: '{current_height}'")); + }; + if current_version >= ConsensusVersion::V8 { + // Track the maximum block height and the associated program ID. + let mut max_block_height = 0; + let mut latest_program = None; + // For each transition in the execution, get the block height at which the program was deployed or upgraded and update the maximum block height. + for transition in execution.transitions() { + // Get the program ID. + let program_id = transition.program_id(); + // If the program is `credits.aleo`, set the appropriate state and continue. + if program_id.to_string() == "credits.aleo" { + latest_program = Some(*program_id); + continue; + } + // Get the transaction ID of the transaction that last deployed or upgraded the program. + let Ok(Some(transaction_id)) = + self.block_store().transaction_store().find_latest_transaction_id_from_program_id(program_id) + else { + return Some(format!( + "Program '{program_id}' does not have a corresponding transaction ID in the store" + )); + }; + // Get the block hash associated with the transaction ID. + let Ok(Some(block_hash)) = self.block_store().find_block_hash(&transaction_id) else { + return Some(format!( + "Transaction '{transaction_id}' does not have a corresponding block hash in the store" + )); + }; + // Get the block height associated with the block hash. + let Ok(Some(block_height)) = self.block_store().get_block_height(&block_hash) else { + return Some(format!( + "Block hash '{block_hash}' does not have a corresponding block height in the store" + )); + }; + // Update the maximum block height. + if max_block_height < block_height { + max_block_height = block_height; + latest_program = Some(*program_id); + } + } + // Get the block height of the execution. + let Ok(Some(block_height)) = + self.block_store().find_block_height_from_state_root(execution.global_state_root()) + else { + return Some(format!( + "The state root of execution '{id}' does not have a corresponding block height in the store" + )); + }; + // If the block height of the execution is less than the maximum block height, abort the transaction. + if block_height < max_block_height { + // Note: This unwrap is safe because `latest_program` must have been set in the loop above. + // - Either the program was `credits.aleo`, in which case `latest_program` was explicitly set. + // - Or the program was deployed after the genesis block and `latest_program` was set to the program ID. + return Some(format!( + "Execution '{id}' state root is earlier than the last deployment or upgrade for program '{}'", + latest_program.unwrap() + )); + } + } } // Return `None` because the transaction is well-formed. diff --git a/synthesizer/src/vm/tests/test_vm_upgrade.rs b/synthesizer/src/vm/tests/test_vm_upgrade.rs index b9f2926cb7..b00f9a2d1a 100644 --- a/synthesizer/src/vm/tests/test_vm_upgrade.rs +++ b/synthesizer/src/vm/tests/test_vm_upgrade.rs @@ -20,6 +20,8 @@ use crate::vm::test_helpers::*; use console::{account::ViewKey, program::Value}; use synthesizer_program::{Program, StackProgram}; +use crate::vm::test_helpers::sample_vm_at_height; +use console::network::ConsensusVersion; use std::panic::AssertUnwindSafe; // This test checks that: @@ -2247,3 +2249,144 @@ constructor: assert!(stack.program_owner().is_some()); assert_eq!(stack.program_owner().unwrap(), caller_address); } + +#[test] +fn test_old_execution_is_aborted_after_upgrade() { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + + // Initialize the VM. + let vm = sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8).unwrap(), rng); + + // Define the programs. + let program_v0 = Program::from_str( + r" +program test_program.aleo; +constructor: + assert.eq true true; +function dummy:", + ) + .unwrap(); + + let program_v1 = Program::from_str( + r" +program test_program.aleo; +constructor: + assert.eq true true; +function dummy: +function dummy2:", + ) + .unwrap(); + + // Deploy the first version of the program. + let transaction = vm.deploy(&caller_private_key, &program_v0, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); + + // Pre-generate 3 executions of the dummy function. + let executions = (0..3) + .map(|_| { + vm.execute( + &caller_private_key, + ("test_program.aleo", "dummy"), + Vec::>::new().into_iter(), + None, + 0, + None, + rng, + ) + .unwrap() + }) + .collect::>(); + + // Add 2 transactions individually to blocks. + // They are expected to pass because the program has not been upgraded. + for execution in &executions[0..2] { + let block = sample_next_block(&vm, &caller_private_key, &[execution.clone()], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); + } + + // Upgrade the program. + let transaction = vm.deploy(&caller_private_key, &program_v1, None, 0, None, rng).unwrap(); + let block = sample_next_block(&vm, &caller_private_key, &[transaction], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); + + // Add the third transaction to a block. + // It is expected to be aborted because the program has been upgraded. + let block = sample_next_block(&vm, &caller_private_key, &[executions[2].clone()], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 0); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 1); + vm.add_next_block(&block).unwrap(); +} + +// This test verifies that `credits.aleo` transactions can be executed and added to blocks after the upgrade to V8. +#[test] +fn test_credits_executions() { + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = sample_genesis_private_key(rng); + let caller_address = Address::try_from(&caller_private_key).unwrap(); + + // Initialize the VM. + let vm: crate::VM = + sample_vm_at_height(CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8).unwrap() - 1, rng); + + // Generate two executions of `transfer_public`. + let transfer_1 = vm + .execute( + &caller_private_key, + ("credits.aleo", "transfer_public"), + vec![Value::from_str(&format!("{caller_address}")).unwrap(), Value::from_str("2u64").unwrap()].into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + assert!(vm.check_transaction(&transfer_1, None, rng).is_ok()); + + let transfer_2 = vm + .execute( + &caller_private_key, + ("credits.aleo", "transfer_public"), + vec![Value::from_str(&format!("{caller_address}")).unwrap(), Value::from_str("4u64").unwrap()].into_iter(), + None, + 0, + None, + rng, + ) + .unwrap(); + assert!(vm.check_transaction(&transfer_2, None, rng).is_ok()); + + // Add the first transaction to a block. + let block = sample_next_block(&vm, &caller_private_key, &[transfer_1], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); + vm.add_next_block(&block).unwrap(); + + // Skip to consensus height V8. + while vm.block_store().current_block_height() <= CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V8).unwrap() { + let block = sample_next_block(&vm, &caller_private_key, &[], rng).unwrap(); + vm.add_next_block(&block).unwrap(); + } + + // Add the second transaction to a block. + let block = sample_next_block(&vm, &caller_private_key, &[transfer_2], rng).unwrap(); + assert_eq!(block.transactions().num_accepted(), 1); + assert_eq!(block.transactions().num_rejected(), 0); + assert_eq!(block.aborted_transaction_ids().len(), 0); +}