use super::*;
macro_rules! ensure_is_unique {
($name:expr, $self:expr, $method:ident, $iter:expr) => {
if has_duplicates($iter) {
bail!("Found a duplicate {} in the transaction", $name);
}
for item in $iter {
if $self.transition_store().$method(item)? {
bail!("The {} '{}' already exists in the ledger", $name, item)
}
}
};
}
impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
pub(crate) const MAX_PARALLEL_DEPLOY_VERIFICATIONS: usize = 5;
pub(crate) const MAX_PARALLEL_EXECUTE_VERIFICATIONS: usize = 1000;
pub fn check_transactions<R: CryptoRng + Rng>(
&self,
transactions: &[(&Transaction<N>, Option<Field<N>>)],
rng: &mut R,
) -> Result<()> {
let (deployments, executions): (Vec<_>, Vec<_>) = transactions.iter().partition(|(tx, _)| tx.is_deploy());
let deployments_for_verification = deployments.chunks(Self::MAX_PARALLEL_DEPLOY_VERIFICATIONS);
let executions_for_verification = executions.chunks(Self::MAX_PARALLEL_EXECUTE_VERIFICATIONS);
for transactions in deployments_for_verification.chain(executions_for_verification) {
let rngs = (0..transactions.len()).map(|_| StdRng::from_seed(rng.gen())).collect::<Vec<_>>();
cfg_iter!(transactions).zip(rngs).try_for_each(|((transaction, rejected_id), mut rng)| {
self.check_transaction(transaction, *rejected_id, &mut rng)
.map_err(|e| anyhow!("Invalid transaction found in the transactions list: {e}"))
})?;
}
Ok(())
}
}
impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
#[inline]
pub fn check_transaction<R: CryptoRng + Rng>(
&self,
transaction: &Transaction<N>,
rejected_id: Option<Field<N>>,
rng: &mut R,
) -> Result<()> {
let timer = timer!("VM::check_transaction");
let mut buffer = Vec::with_capacity(N::MAX_TRANSACTION_SIZE);
if let Err(error) = transaction.write_le(LimitedWriter::new(&mut buffer, N::MAX_TRANSACTION_SIZE)) {
bail!("Transaction '{}' is not well-formed: {error}", transaction.id())
}
if self.block_store().contains_transaction_id(&transaction.id())? {
bail!("Transaction '{}' already exists in the ledger", transaction.id())
}
match transaction.to_root() {
Ok(root) if *transaction.id() != root => bail!("Incorrect transaction ID ({})", transaction.id()),
Ok(_) => (),
Err(error) => {
bail!("Failed to compute the Merkle root of the transaction: {error}\n{transaction}");
}
};
lap!(timer, "Verify the transaction ID");
ensure_is_unique!("transition ID", self, contains_transition_id, transaction.transition_ids());
ensure_is_unique!("input ID", self, contains_input_id, transaction.input_ids());
ensure_is_unique!("serial number", self, contains_serial_number, transaction.serial_numbers());
ensure_is_unique!("tag", self, contains_tag, transaction.tags());
ensure_is_unique!("output ID", self, contains_output_id, transaction.output_ids());
ensure_is_unique!("commitment", self, contains_commitment, transaction.commitments());
ensure_is_unique!("nonce", self, contains_nonce, transaction.nonces());
ensure_is_unique!("transition public key", self, contains_tpk, transaction.transition_public_keys());
ensure_is_unique!("transition commitment", self, contains_tcm, transaction.transition_commitments());
lap!(timer, "Check for duplicate elements");
self.check_fee(transaction, rejected_id)?;
let checksum = Data::<Transaction<N>>::Buffer(transaction.to_bytes_le()?.into()).to_checksum::<N>()?;
let is_partially_verified =
self.partially_verified_transactions.read().peek(&(transaction.id())) == Some(&checksum);
match transaction {
Transaction::Deploy(id, owner, deployment, _) => {
let Ok(deployment_id) = deployment.to_deployment_id() else {
bail!("Failed to compute the Merkle root for a deployment transaction '{id}'")
};
ensure!(owner.verify(deployment_id), "Invalid owner signature for deployment transaction '{id}'");
if deployment.edition() != N::EDITION {
bail!("Invalid deployment transaction '{id}' - expected edition {}", N::EDITION)
}
if self.transaction_store().contains_program_id(deployment.program_id())? {
bail!("Program ID '{}' is already deployed", deployment.program_id())
}
if self.contains_program(deployment.program_id()) {
bail!("Program ID '{}' already exists", deployment.program_id());
}
if !is_partially_verified {
match try_vm_runtime!(|| self.check_deployment_internal(deployment, rng)) {
Ok(result) => result?,
Err(_) => bail!("VM safely halted transaction '{id}' during verification"),
}
}
}
Transaction::Execute(id, execution, _) => {
let Ok(execution_id) = execution.to_execution_id() else {
bail!("Failed to compute the Merkle root for an execution transaction '{id}'")
};
if self.block_store().contains_rejected_deployment_or_execution_id(&execution_id)? {
bail!("Transaction '{id}' contains a previously rejected execution")
}
match try_vm_runtime!(|| self.check_execution_internal(execution, is_partially_verified)) {
Ok(result) => result?,
Err(_) => bail!("VM safely halted transaction '{id}' during verification"),
}
}
Transaction::Fee(..) => { }
}
if !matches!(transaction, Transaction::Fee(..)) && !is_partially_verified {
self.partially_verified_transactions.write().push(transaction.id(), checksum);
}
finish!(timer, "Verify the transaction");
Ok(())
}
#[inline]
pub fn check_fee(&self, transaction: &Transaction<N>, rejected_id: Option<Field<N>>) -> Result<()> {
match transaction {
Transaction::Deploy(id, _, deployment, fee) => {
ensure!(rejected_id.is_none(), "Transaction '{id}' should not have a rejected ID (deployment)");
let Ok(deployment_id) = deployment.to_deployment_id() else {
bail!("Failed to compute the Merkle root for deployment transaction '{id}'")
};
let (cost, _) = deployment_cost(deployment)?;
if *fee.base_amount()? < cost {
bail!("Transaction '{id}' has an insufficient base fee (deployment) - requires {cost} microcredits")
}
self.check_fee_internal(fee, deployment_id)?;
}
Transaction::Execute(id, execution, fee) => {
ensure!(rejected_id.is_none(), "Transaction '{id}' should not have a rejected ID (execution)");
let Ok(execution_id) = execution.to_execution_id() else {
bail!("Failed to compute the Merkle root for execution transaction '{id}'")
};
let is_fee_required = !(execution.len() == 1 && transaction.contains_split());
if let Some(fee) = fee {
if is_fee_required {
let block_height = self
.block_store()
.find_block_height_from_state_root(execution.global_state_root())?
.unwrap_or_default();
let (cost, (_, _)) = match block_height < N::CONSENSUS_V2_HEIGHT {
true => execution_cost_v1(&self.process().read(), execution)?,
false => execution_cost_v2(&self.process().read(), execution)?,
};
if *fee.base_amount()? < cost {
bail!(
"Transaction '{id}' has an insufficient base fee (execution) - requires {cost} microcredits"
)
}
} else {
ensure!(*fee.base_amount()? == 0, "Transaction '{id}' has a non-zero base fee (execution)");
}
self.check_fee_internal(fee, execution_id)?;
} else {
ensure!(!is_fee_required, "Transaction '{id}' is missing a fee (execution)");
}
}
Transaction::Fee(id, fee) => {
match rejected_id {
Some(rejected_id) => self.check_fee_internal(fee, rejected_id)?,
None => bail!("Transaction '{id}' is missing a rejected ID (fee)"),
}
}
}
Ok(())
}
}
impl<N: Network, C: ConsensusStorage<N>> VM<N, C> {
#[inline]
fn check_deployment_internal<R: CryptoRng + Rng>(&self, deployment: &Deployment<N>, rng: &mut R) -> Result<()> {
macro_rules! logic {
($process:expr, $network:path, $aleo:path) => {{
let deployment = cast_ref!(&deployment as Deployment<$network>);
$process.verify_deployment::<$aleo, _>(&deployment, rng)
}};
}
let timer = timer!("VM::check_deployment");
let result = process!(self, logic).map_err(|error| anyhow!("Deployment verification failed - {error}"));
finish!(timer);
result
}
#[inline]
fn check_execution_internal(&self, execution: &Execution<N>, is_partially_verified: bool) -> Result<()> {
let timer = timer!("VM::check_execution");
let block_height = self.block_store().current_block_height();
if self.restrictions.contains_restricted_transitions(execution, block_height) {
bail!("Execution verification failed - restricted transition found");
}
let verification = match is_partially_verified {
true => Ok(()),
false => self.process.read().verify_execution(execution),
};
lap!(timer, "Verify the execution");
let result = match verification {
Ok(()) => match self.block_store().contains_state_root(&execution.global_state_root()) {
Ok(true) => Ok(()),
Ok(false) => bail!("Execution verification failed - global state root does not exist (yet)"),
Err(error) => bail!("Execution verification failed - {error}"),
},
Err(error) => bail!("Execution verification failed - {error}"),
};
finish!(timer, "Check the global state root");
result
}
#[inline]
fn check_fee_internal(&self, fee: &Fee<N>, deployment_or_execution_id: Field<N>) -> Result<()> {
let timer = timer!("VM::check_fee");
let fee_amount = fee.amount()?;
ensure!(*fee_amount <= N::MAX_FEE, "Fee verification failed: fee exceeds the maximum limit");
let verification = self.process.read().verify_fee(fee, deployment_or_execution_id);
lap!(timer, "Verify the fee");
if fee.is_fee_public() {
let Some(payer) = fee.payer() else {
bail!("Fee verification failed: fee is public, but the payer is missing");
};
let Some(Value::Plaintext(Plaintext::Literal(Literal::U64(balance), _))) =
self.finalize_store().get_value_speculative(
ProgramID::from_str("credits.aleo")?,
Identifier::from_str("account")?,
&Plaintext::from(Literal::Address(payer)),
)?
else {
bail!("Fee verification failed: fee is public, but the payer account balance is missing");
};
ensure!(balance >= fee_amount, "Fee verification failed: insufficient balance");
}
let result = match verification {
Ok(()) => match self.block_store().contains_state_root(&fee.global_state_root()) {
Ok(true) => Ok(()),
Ok(false) => bail!("Fee verification failed: global state root not found"),
Err(error) => bail!("Fee verification failed: {error}"),
},
Err(error) => bail!("Fee verification failed: {error}"),
};
finish!(timer, "Check the global state root");
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vm::test_helpers::sample_finalize_state;
use console::{
account::{Address, ViewKey},
types::Field,
};
use ledger_block::{Block, Header, Metadata, Transaction, Transition};
type CurrentNetwork = test_helpers::CurrentNetwork;
#[test]
fn test_verify() {
let rng = &mut TestRng::default();
let vm = crate::vm::test_helpers::sample_vm_with_genesis_block(rng);
let deployment_transaction = crate::vm::test_helpers::sample_deployment_transaction(rng);
vm.check_transaction(&deployment_transaction, None, rng).unwrap();
let execution_transaction = crate::vm::test_helpers::sample_execution_transaction_with_private_fee(rng);
vm.check_transaction(&execution_transaction, None, rng).unwrap();
let execution_transaction = crate::vm::test_helpers::sample_execution_transaction_with_public_fee(rng);
vm.check_transaction(&execution_transaction, None, rng).unwrap();
}
#[test]
fn test_verify_deployment() {
let rng = &mut TestRng::default();
let vm = crate::vm::test_helpers::sample_vm();
let program = crate::vm::test_helpers::sample_program();
let deployment = vm.deploy_raw(&program, rng).unwrap();
vm.check_deployment_internal(&deployment, rng).unwrap();
let serialized_deployment = deployment.to_string();
let deployment_transaction: Deployment<CurrentNetwork> = serde_json::from_str(&serialized_deployment).unwrap();
vm.check_deployment_internal(&deployment_transaction, rng).unwrap();
}
#[test]
fn test_verify_execution() {
let rng = &mut TestRng::default();
let vm = crate::vm::test_helpers::sample_vm_with_genesis_block(rng);
let transactions = [
crate::vm::test_helpers::sample_execution_transaction_with_private_fee(rng),
crate::vm::test_helpers::sample_execution_transaction_with_public_fee(rng),
];
for transaction in transactions {
match transaction {
Transaction::Execute(_, execution, _) => {
assert!(execution.proof().is_some());
vm.check_execution_internal(&execution, false).unwrap();
let serialized_execution = execution.to_string();
let recovered_execution: Execution<CurrentNetwork> =
serde_json::from_str(&serialized_execution).unwrap();
vm.check_execution_internal(&recovered_execution, false).unwrap();
}
_ => panic!("Expected an execution transaction"),
}
}
}
#[test]
fn test_verify_fee() {
let rng = &mut TestRng::default();
let vm = crate::vm::test_helpers::sample_vm_with_genesis_block(rng);
let transactions = [
crate::vm::test_helpers::sample_execution_transaction_with_private_fee(rng),
crate::vm::test_helpers::sample_execution_transaction_with_public_fee(rng),
];
for transaction in transactions {
match transaction {
Transaction::Execute(_, execution, Some(fee)) => {
let execution_id = execution.to_execution_id().unwrap();
assert!(fee.proof().is_some());
vm.check_fee_internal(&fee, execution_id).unwrap();
let serialized_fee = fee.to_string();
let recovered_fee: Fee<CurrentNetwork> = serde_json::from_str(&serialized_fee).unwrap();
vm.check_fee_internal(&recovered_fee, execution_id).unwrap();
}
_ => panic!("Expected an execution with a fee"),
}
}
}
#[test]
fn test_check_transaction_execution() {
let rng = &mut TestRng::default();
let vm = crate::vm::test_helpers::sample_vm();
let genesis = crate::vm::test_helpers::sample_genesis_block(rng);
vm.add_next_block(&genesis).unwrap();
let valid_transaction = crate::vm::test_helpers::sample_execution_transaction_with_private_fee(rng);
vm.check_transaction(&valid_transaction, None, rng).unwrap();
let valid_transaction = crate::vm::test_helpers::sample_execution_transaction_with_public_fee(rng);
vm.check_transaction(&valid_transaction, None, rng).unwrap();
let valid_transaction = crate::vm::test_helpers::sample_execution_transaction_without_fee(rng);
vm.check_transaction(&valid_transaction, None, rng).unwrap();
}
#[test]
fn test_verify_deploy_and_execute() {
let rng = &mut TestRng::default();
let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng);
let caller_view_key = ViewKey::try_from(&caller_private_key).unwrap();
let address = Address::try_from(&caller_private_key).unwrap();
let genesis = crate::vm::test_helpers::sample_genesis_block(rng);
let records = genesis.records().collect::<indexmap::IndexMap<_, _>>();
let credits = records.values().next().unwrap().decrypt(&caller_view_key).unwrap();
let vm = crate::vm::test_helpers::sample_vm();
vm.add_next_block(&genesis).unwrap();
let program = crate::vm::test_helpers::sample_program();
let deployment_transaction = vm.deploy(&caller_private_key, &program, Some(credits), 10, None, rng).unwrap();
let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64;
let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm
.speculate(
sample_finalize_state(1),
time_since_last_block,
Some(0u64),
vec![],
&None.into(),
[deployment_transaction].iter(),
rng,
)
.unwrap();
assert!(aborted_transaction_ids.is_empty());
let deployment_metadata = Metadata::new(
CurrentNetwork::ID,
1,
1,
0,
0,
CurrentNetwork::GENESIS_COINBASE_TARGET,
CurrentNetwork::GENESIS_PROOF_TARGET,
genesis.last_coinbase_target(),
genesis.last_coinbase_timestamp(),
genesis.timestamp().saturating_add(time_since_last_block),
)
.unwrap();
let deployment_header = Header::from(
vm.block_store().current_state_root(),
transactions.to_transactions_root().unwrap(),
transactions.to_finalize_root(ratified_finalize_operations).unwrap(),
ratifications.to_ratifications_root().unwrap(),
Field::zero(),
Field::zero(),
deployment_metadata,
)
.unwrap();
let deployment_block = Block::new_beacon(
&caller_private_key,
genesis.hash(),
deployment_header,
ratifications,
None.into(),
vec![],
transactions,
aborted_transaction_ids,
rng,
)
.unwrap();
vm.add_next_block(&deployment_block).unwrap();
let records = deployment_block.records().collect::<indexmap::IndexMap<_, _>>();
let inputs = [
Value::<CurrentNetwork>::from_str(&address.to_string()).unwrap(),
Value::<CurrentNetwork>::from_str("10u64").unwrap(),
]
.into_iter();
let credits = Some(records.values().next().unwrap().decrypt(&caller_view_key).unwrap());
let transaction =
vm.execute(&caller_private_key, ("testing.aleo", "initialize"), inputs, credits, 10, None, rng).unwrap();
vm.check_transaction(&transaction, None, rng).unwrap();
}
#[test]
fn test_failed_credits_deployment() {
let rng = &mut TestRng::default();
let vm = crate::vm::test_helpers::sample_vm();
let program = Program::credits().unwrap();
assert!(vm.deploy_raw(&program, rng).is_err());
let program = Program::from_str(
r"
program credits.aleo;
record token:
owner as address.private;
amount as u64.private;
function compute:
input r0 as u32.private;
add r0 r0 into r1;
output r1 as u32.public;",
)
.unwrap();
assert!(vm.deploy_raw(&program, rng).is_err());
}
#[test]
fn test_check_mutated_execution() {
let rng = &mut TestRng::default();
let vm = crate::vm::test_helpers::sample_vm();
let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng);
let genesis = crate::vm::test_helpers::sample_genesis_block(rng);
vm.add_next_block(&genesis).unwrap();
let valid_transaction = crate::vm::test_helpers::sample_execution_transaction_with_public_fee(rng);
vm.check_transaction(&valid_transaction, None, rng).unwrap();
let execution = valid_transaction.execution().unwrap();
let transitions: Vec<_> = execution.transitions().collect();
assert_eq!(transitions.len(), 1);
let transition = transitions[0].clone();
let added_output = Output::ExternalRecord(Field::zero());
let mutated_outputs = [transition.outputs(), &[added_output]].concat();
let mutated_transition = Transition::new(
*transition.program_id(),
*transition.function_name(),
transition.inputs().to_vec(),
mutated_outputs,
*transition.tpk(),
*transition.tcm(),
*transition.scm(),
)
.unwrap();
let mutated_execution = Execution::from(
[mutated_transition].into_iter(),
execution.global_state_root(),
execution.proof().cloned(),
)
.unwrap();
let authorization = vm
.authorize_fee_public(
&caller_private_key,
10_000_000,
100,
mutated_execution.to_execution_id().unwrap(),
rng,
)
.unwrap();
let fee = vm.execute_fee_authorization(authorization, None, rng).unwrap();
let mutated_transaction = Transaction::from_execution(mutated_execution, Some(fee)).unwrap();
assert!(vm.check_transaction(&mutated_transaction, None, rng).is_err());
}
#[cfg(feature = "test")]
#[test]
fn test_fee_migration() {
assert_ne!(0, CurrentNetwork::CONSENSUS_V2_HEIGHT);
let minimum_credits_transfer_public_fee = 34_060;
let old_minimum_credits_transfer_public_fee = 51_060;
let rng = &mut TestRng::default();
let vm = crate::vm::test_helpers::sample_vm();
let genesis = crate::vm::test_helpers::sample_genesis_block(rng);
vm.add_next_block(&genesis).unwrap();
let transaction = crate::vm::test_helpers::sample_execution_transaction_with_public_fee(rng);
let fee_too_low_transaction = crate::vm::test_helpers::create_new_transaction_with_different_fee(
rng,
transaction.clone(),
minimum_credits_transfer_public_fee,
);
assert!(vm.check_transaction(&fee_too_low_transaction, None, rng).is_err());
let old_valid_transaction = crate::vm::test_helpers::create_new_transaction_with_different_fee(
rng,
transaction.clone(),
old_minimum_credits_transfer_public_fee,
);
assert!(vm.check_transaction(&old_valid_transaction, None, rng).is_ok());
let private_key = test_helpers::sample_genesis_private_key(rng);
let transactions: [Transaction<CurrentNetwork>; 0] = [];
for _ in 0..CurrentNetwork::CONSENSUS_V2_HEIGHT {
let next_block = crate::vm::test_helpers::sample_next_block(&vm, &private_key, &transactions, rng).unwrap();
vm.add_next_block(&next_block).unwrap();
}
let transaction = {
let address = Address::try_from(&private_key).unwrap();
let inputs = [
Value::<CurrentNetwork>::from_str(&address.to_string()).unwrap(),
Value::<CurrentNetwork>::from_str("1u64").unwrap(),
]
.into_iter();
let transaction_without_fee =
vm.execute(&private_key, ("credits.aleo", "transfer_public"), inputs, None, 0, None, rng).unwrap();
let execution = transaction_without_fee.execution().unwrap().clone();
let authorization = vm
.authorize_fee_public(&private_key, 10_000_000, 100, execution.to_execution_id().unwrap(), rng)
.unwrap();
let fee = vm.execute_fee_authorization(authorization, None, rng).unwrap();
Transaction::from_execution(execution, Some(fee)).unwrap()
};
let fee_too_high_transaction = crate::vm::test_helpers::create_new_transaction_with_different_fee(
rng,
transaction.clone(),
old_minimum_credits_transfer_public_fee,
);
assert!(vm.check_transaction(&fee_too_high_transaction, None, rng).is_ok());
let valid_transaction = crate::vm::test_helpers::create_new_transaction_with_different_fee(
rng,
transaction.clone(),
minimum_credits_transfer_public_fee,
);
assert!(vm.check_transaction(&valid_transaction, None, rng).is_ok());
}
}