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> {
#[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");
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)?;
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())
}
self.check_deployment_internal(deployment, rng)?;
}
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")
}
self.check_execution_internal(execution)?;
}
Transaction::Fee(..) => { }
}
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 (cost, _) = execution_cost(self, 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>) -> Result<()> {
let timer = timer!("VM::check_execution");
let verification = 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 not found"),
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};
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).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).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 (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) =
vm.speculate(sample_finalize_state(1), Some(0u64), vec![], None, [deployment_transaction].iter()).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(),
CurrentNetwork::GENESIS_TIMESTAMP + 1,
)
.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,
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());
}
}