Skip to content
Closed
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
2 changes: 1 addition & 1 deletion ledger/store/src/block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ impl<N: Network, B: BlockStorage<N>> BlockStore<N, B> {

/// 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`.
Expand Down
78 changes: 73 additions & 5 deletions synthesizer/src/vm/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,9 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
/// - 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.
Expand Down Expand Up @@ -847,16 +850,81 @@ impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
}
}

// 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.
Expand Down
143 changes: 143 additions & 0 deletions synthesizer/src/vm/tests/test_vm_upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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::<Value<_>>::new().into_iter(),
None,
0,
None,
rng,
)
.unwrap()
})
.collect::<Vec<_>>();

// 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<CurrentNetwork, LedgerType> =
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);
}