/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{collections::HashMap, sync::Arc}; use clap::Parser; use darkfi::{ blockchain::{ Blockchain, BlockchainOverlay, BlockchainOverlayPtr, block_store::append_tx_to_merkle_tree, }, cli_desc, error::TxVerifyFailed, runtime::vm_runtime::{Runtime, TxLocalState}, tx::{MAX_TX_CALLS, MIN_TX_CALLS, Transaction}, util::path::expand_path, validator::{ fees::{GasData, PALLAS_SCHNORR_SIGNATURE_FEE, circuit_gas_use}, verification::verify_transaction, }, zk::VerifyingKey, }; use darkfi_sdk::{ blockchain::compute_fee, crypto::{ContractId, MerkleTree, PublicKey}, dark_tree::dark_forest_leaf_vec_integrity_check, deploy::DeployParamsV1, pasta::pallas, tx::TransactionHash, }; use darkfi_serial::{AsyncDecodable, AsyncEncodable, deserialize_async, serialize_async}; use parking_lot::Mutex; use smol::io::Cursor; #[derive(Parser)] #[command(about = cli_desc!())] struct Args { #[arg(short, long)] database_path: String, #[arg(short, long)] tx_hash: String, #[arg(long, conflicts_with_all = ["zkp", "sig"])] wasm: bool, #[arg(long, conflicts_with_all = ["wasm", "sig"])] zkp: bool, #[arg(long, conflicts_with_all = ["wasm", "zkp"])] sig: bool, } fn main() { smol::block_on(async { let args = Args::parse(); replay_tx(args).await; }); } async fn replay_tx(args: Args) { let db_path = expand_path(&args.database_path).unwrap(); let kvdb = kvdb_overlay::Database::open_default(&db_path).unwrap(); let blockchain = Blockchain::new(&kvdb).unwrap(); let txh: TransactionHash = args.tx_hash.parse().unwrap(); let (tx_height, _) = blockchain.transactions.get_location(&[txh], true).unwrap().first().unwrap().unwrap(); let block_header_hash = blockchain.blocks.get_order(&[tx_height], true).unwrap().first().unwrap().unwrap(); // Get all the transactions in the block of our target tx let block = blockchain .blocks .get(&[block_header_hash], true) .unwrap() .first() .unwrap() .clone() .unwrap(); let txs: Vec = blockchain .transactions .get(&block.txs, true) .unwrap() .into_iter() .map(|t| t.unwrap()) .collect(); let (overlay, new_height) = rollback_database(&blockchain, txh).await; // Apply all transactions upto and including our target tx let mut tree = MerkleTree::new(1); for tx in txs { perform_tx_verification(&tx, new_height, &overlay, &mut tree, &args).await; // We have applied our target tx so let's bail out if tx.hash() == txh { break; } } } async fn perform_tx_verification( tx: &Transaction, new_height: u32, overlay: &BlockchainOverlayPtr, tree: &mut MerkleTree, args: &Args, ) { let mut vks: HashMap<[u8; 32], HashMap> = HashMap::new(); for call in &tx.calls { vks.insert(call.data.contract_id.to_bytes(), HashMap::new()); } let result = if args.wasm { verify_transaction_wasm(overlay, new_height, 2, tx, tree, &mut vks, true).await.unwrap() } else if args.zkp { verify_transaction_zkps(overlay, new_height, 2, tx, tree, &mut vks, true).await.unwrap() } else if args.sig { verify_transaction_signatures(overlay, new_height, 2, tx, tree, &mut vks, true) .await .unwrap() } else { verify_transaction(overlay, new_height, 2, tx, tree, &mut vks, true).await.unwrap() }; println!("Verify Transaction Result: {:?}", result); } /// Resets the blockchain in memory to a height before the transaction. async fn rollback_database( blockchain: &Blockchain, txh: TransactionHash, ) -> (BlockchainOverlayPtr, u32) { let (tx_height, _) = blockchain.transactions.get_location(&[txh], true).unwrap().first().unwrap().unwrap(); let new_height = tx_height - 1; println!("Rolling back database to Height: {new_height}"); let (last, _) = blockchain.last().unwrap(); let heights: Vec = (new_height + 1..=last).rev().collect(); let inverse_diffs = blockchain.blocks.get_state_inverse_diff(&heights, true).unwrap(); let overlay = BlockchainOverlay::new(blockchain).unwrap(); let overlay_lock = overlay.lock().unwrap(); let mut lock = overlay_lock.overlay.lock().unwrap(); for inverse_diff in inverse_diffs { let inverse_diff = inverse_diff.unwrap(); lock.add_diff(&inverse_diff).unwrap(); } drop(lock); drop(overlay_lock); (overlay, new_height) } async fn verify_transaction_wasm( overlay: &BlockchainOverlayPtr, verifying_block_height: u32, block_target: u32, tx: &Transaction, tree: &mut MerkleTree, _verifying_keys: &mut HashMap<[u8; 32], HashMap>, verify_fee: bool, ) -> darkfi::Result { let tx_hash = tx.hash(); // Create a FeeData instance to hold the calculated fee data let mut gas_data = GasData::default(); // Verify calls indexes integrity if verify_fee { dark_forest_leaf_vec_integrity_check( &tx.calls, Some(MIN_TX_CALLS + 1), Some(MAX_TX_CALLS), )?; } else { dark_forest_leaf_vec_integrity_check(&tx.calls, Some(MIN_TX_CALLS), Some(MAX_TX_CALLS))?; } // Index of the Fee-paying call let mut fee_call_idx = 0; if verify_fee { // Verify that there is a single money fee call in the transaction let mut found_fee = false; for (call_idx, call) in tx.calls.iter().enumerate() { if !call.data.is_money_fee() { continue } if found_fee { return Err(TxVerifyFailed::InvalidFee.into()) } found_fee = true; fee_call_idx = call_idx; } if !found_fee { return Err(TxVerifyFailed::InvalidFee.into()) } } // Write the transaction calls payload data let mut payload = vec![]; tx.calls.encode_async(&mut payload).await?; // Define a buffer in case we want to use a different payload in a specific call let mut _call_payload = vec![]; // Create the transaction-local state instance // This state exists only during the single transaction verification. let tx_local_state = Arc::new(Mutex::new(TxLocalState::new())); // Iterate over all calls to get the metadata for (idx, call) in tx.calls.iter().enumerate() { // Transaction must not contain a Pow reward call if call.data.is_money_pow_reward() { return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into()) } // Check if its the fee call so we only pass its payload let (call_idx, call_payload) = if call.data.is_money_fee() { _call_payload = vec![]; vec![call.clone()].encode_async(&mut _call_payload).await?; (0_u8, &_call_payload) } else { (idx as u8, &payload) }; let wasm = overlay.lock().unwrap().contracts.get(call.data.contract_id)?; let mut runtime = Runtime::new( &wasm, overlay.clone(), tx_local_state.clone(), call.data.contract_id, verifying_block_height, block_target, tx_hash, call_idx, )?; // After getting the metadata, we run the "exec" function with the same runtime // and the same payload. We keep the returned state update in a buffer, prefixed // by the call function ID, enforcing the state update function in the contract. let mut state_update = vec![call.data.data[0]]; state_update.append(&mut runtime.exec(call_payload)?); // If that was successful, we apply the state update in the ephemeral overlay. runtime.apply(&state_update)?; // If this call is supposed to deploy a new contract, we have to instantiate // a new `Runtime` and run its deploy function. if call.data.is_deployment() /* DeployV1 */ { // Deserialize the deployment parameters let deploy_params: DeployParamsV1 = deserialize_async(&call.data.data[1..]).await?; let deploy_cid = ContractId::derive_public(deploy_params.public_key); // Instantiate the new deployment runtime let mut deploy_runtime = Runtime::new( &deploy_params.wasm_bincode, overlay.clone(), tx_local_state.clone(), deploy_cid, verifying_block_height, block_target, tx_hash, call_idx, )?; deploy_runtime.deploy(&deploy_params.ix)?; let deploy_gas_used = deploy_runtime.gas_used(); gas_data.deployments += deploy_gas_used; } // At this point we're done with the call and move on to the next one. // Accumulate the WASM gas used. let wasm_gas_used = runtime.gas_used(); // Append the used wasm gas gas_data.wasm += wasm_gas_used; } // Store the calculated total gas used to avoid recalculating it for subsequent uses let total_gas_used = gas_data.total_gas_used(); if verify_fee { // Deserialize the fee call to find the paid fee let fee: u64 = match deserialize_async(&tx.calls[fee_call_idx].data.data[1..9]).await { Ok(v) => v, Err(_) => return Err(TxVerifyFailed::InvalidFee.into()), }; // Compute the required fee for this transaction let required_fee = compute_fee(&total_gas_used); // Check that enough fee has been paid for the used gas in this transaction if required_fee > fee { return Err(TxVerifyFailed::InsufficientFee.into()) } // Store paid fee gas_data.paid = fee; } // Append hash to merkle tree append_tx_to_merkle_tree(tree, tx); Ok(gas_data) } async fn verify_transaction_zkps( overlay: &BlockchainOverlayPtr, verifying_block_height: u32, block_target: u32, tx: &Transaction, tree: &mut MerkleTree, verifying_keys: &mut HashMap<[u8; 32], HashMap>, verify_fee: bool, ) -> darkfi::Result { let tx_hash = tx.hash(); // Create a FeeData instance to hold the calculated fee data let mut gas_data = GasData::default(); // Verify calls indexes integrity if verify_fee { dark_forest_leaf_vec_integrity_check( &tx.calls, Some(MIN_TX_CALLS + 1), Some(MAX_TX_CALLS), )?; } else { dark_forest_leaf_vec_integrity_check(&tx.calls, Some(MIN_TX_CALLS), Some(MAX_TX_CALLS))?; } // Table of public inputs used for ZK proof verification let mut zkp_table = vec![]; // Table of public keys used for signature verification let mut sig_table = vec![]; // Index of the Fee-paying call let mut fee_call_idx = 0; if verify_fee { // Verify that there is a single money fee call in the transaction let mut found_fee = false; for (call_idx, call) in tx.calls.iter().enumerate() { if !call.data.is_money_fee() { continue } if found_fee { return Err(TxVerifyFailed::InvalidFee.into()) } found_fee = true; fee_call_idx = call_idx; } if !found_fee { return Err(TxVerifyFailed::InvalidFee.into()) } } // Write the transaction calls payload data let mut payload = vec![]; tx.calls.encode_async(&mut payload).await?; // Define a buffer in case we want to use a different payload in a specific call let mut _call_payload = vec![]; // We'll also take note of all the circuits in a Vec so we can calculate their verification cost. let mut circuits_to_verify = vec![]; // Create the transaction-local state instance // This state exists only during the single transaction verification. let tx_local_state = Arc::new(Mutex::new(TxLocalState::new())); // Iterate over all calls to get the metadata for (idx, call) in tx.calls.iter().enumerate() { // Transaction must not contain a Pow reward call if call.data.is_money_pow_reward() { return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into()) } // Check if its the fee call so we only pass its payload let (call_idx, call_payload) = if call.data.is_money_fee() { _call_payload = vec![]; vec![call.clone()].encode_async(&mut _call_payload).await?; (0_u8, &_call_payload) } else { (idx as u8, &payload) }; let wasm = overlay.lock().unwrap().contracts.get(call.data.contract_id)?; let mut runtime = Runtime::new( &wasm, overlay.clone(), tx_local_state.clone(), call.data.contract_id, verifying_block_height, block_target, tx_hash, call_idx, )?; let metadata = runtime.metadata(call_payload)?; // Decode the metadata retrieved from the execution let mut decoder = Cursor::new(&metadata); // The tuple is (zkas_ns, public_inputs) let zkp_pub: Vec<(String, Vec)> = AsyncDecodable::decode_async(&mut decoder).await?; let sig_pub: Vec = AsyncDecodable::decode_async(&mut decoder).await?; if decoder.position() != metadata.len() as u64 { return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into()) } // Here we'll look up verifying keys and insert them into the per-contract map. // TODO: This vk map can potentially use a lot of RAM. Perhaps load keys on-demand at verification time? for (zkas_ns, _) in &zkp_pub { let inner_vk_map = verifying_keys.get_mut(&call.data.contract_id.to_bytes()).unwrap(); // TODO: This will be a problem in case of ::deploy, unless we force a different // namespace and disable updating existing circuit. Might be a smart idea to do // so in order to have to care less about being able to verify historical txs. if inner_vk_map.contains_key(zkas_ns.as_str()) { continue } let (zkbin, vk) = overlay.lock().unwrap().contracts.get_zkas(&call.data.contract_id, zkas_ns)?; inner_vk_map.insert(zkas_ns.to_string(), vk); circuits_to_verify.push(zkbin); } zkp_table.push(zkp_pub); sig_table.push(sig_pub); // At this point we're done with the call and move on to the next one. // Accumulate the WASM gas used. let wasm_gas_used = runtime.gas_used(); // Append the used wasm gas gas_data.wasm += wasm_gas_used; } // The ZK circuit fee is calculated using a function in validator/fees.rs for zkbin in circuits_to_verify.iter() { let zk_circuit_gas_used = circuit_gas_use(zkbin); // Append the used zk circuit gas gas_data.zk_circuits += zk_circuit_gas_used; } // Store the calculated total gas used to avoid recalculating it for subsequent uses let total_gas_used = gas_data.total_gas_used(); if verify_fee { // Deserialize the fee call to find the paid fee let fee: u64 = match deserialize_async(&tx.calls[fee_call_idx].data.data[1..9]).await { Ok(v) => v, Err(_) => return Err(TxVerifyFailed::InvalidFee.into()), }; // Compute the required fee for this transaction let required_fee = compute_fee(&total_gas_used); // Check that enough fee has been paid for the used gas in this transaction if required_fee > fee { return Err(TxVerifyFailed::InsufficientFee.into()) } // Store paid fee gas_data.paid = fee; } if tx.verify_zkps(verifying_keys, zkp_table).await.is_err() { return Err(TxVerifyFailed::InvalidZkProof.into()) } // Append hash to merkle tree append_tx_to_merkle_tree(tree, tx); Ok(gas_data) } async fn verify_transaction_signatures( overlay: &BlockchainOverlayPtr, verifying_block_height: u32, block_target: u32, tx: &Transaction, tree: &mut MerkleTree, _verifying_keys: &mut HashMap<[u8; 32], HashMap>, verify_fee: bool, ) -> darkfi::Result { let tx_hash = tx.hash(); // Create a FeeData instance to hold the calculated fee data let mut gas_data = GasData::default(); // Verify calls indexes integrity if verify_fee { dark_forest_leaf_vec_integrity_check( &tx.calls, Some(MIN_TX_CALLS + 1), Some(MAX_TX_CALLS), )?; } else { dark_forest_leaf_vec_integrity_check(&tx.calls, Some(MIN_TX_CALLS), Some(MAX_TX_CALLS))?; } // Table of public inputs used for ZK proof verification let mut zkp_table = vec![]; // Table of public keys used for signature verification let mut sig_table = vec![]; // Index of the Fee-paying call let mut fee_call_idx = 0; if verify_fee { // Verify that there is a single money fee call in the transaction let mut found_fee = false; for (call_idx, call) in tx.calls.iter().enumerate() { if !call.data.is_money_fee() { continue } if found_fee { return Err(TxVerifyFailed::InvalidFee.into()) } found_fee = true; fee_call_idx = call_idx; } if !found_fee { return Err(TxVerifyFailed::InvalidFee.into()) } } // Write the transaction calls payload data let mut payload = vec![]; tx.calls.encode_async(&mut payload).await?; // Define a buffer in case we want to use a different payload in a specific call let mut _call_payload = vec![]; // Create the transaction-local state instance // This state exists only during the single transaction verification. let tx_local_state = Arc::new(Mutex::new(TxLocalState::new())); // Iterate over all calls to get the metadata for (idx, call) in tx.calls.iter().enumerate() { // Transaction must not contain a Pow reward call if call.data.is_money_pow_reward() { return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into()) } // Check if its the fee call so we only pass its payload let (call_idx, call_payload) = if call.data.is_money_fee() { _call_payload = vec![]; vec![call.clone()].encode_async(&mut _call_payload).await?; (0_u8, &_call_payload) } else { (idx as u8, &payload) }; let wasm = overlay.lock().unwrap().contracts.get(call.data.contract_id)?; let mut runtime = Runtime::new( &wasm, overlay.clone(), tx_local_state.clone(), call.data.contract_id, verifying_block_height, block_target, tx_hash, call_idx, )?; let metadata = runtime.metadata(call_payload)?; // Decode the metadata retrieved from the execution let mut decoder = Cursor::new(&metadata); // The tuple is (zkas_ns, public_inputs) let zkp_pub: Vec<(String, Vec)> = AsyncDecodable::decode_async(&mut decoder).await?; let sig_pub: Vec = AsyncDecodable::decode_async(&mut decoder).await?; if decoder.position() != metadata.len() as u64 { return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into()) } zkp_table.push(zkp_pub); sig_table.push(sig_pub); // At this point we're done with the call and move on to the next one. // Accumulate the WASM gas used. let wasm_gas_used = runtime.gas_used(); // Append the used wasm gas gas_data.wasm += wasm_gas_used; } // The signature fee is tx_size + fixed_sig_fee * n_signatures gas_data.signatures = (PALLAS_SCHNORR_SIGNATURE_FEE * tx.signatures.len() as u64) + serialize_async(tx).await.len() as u64; // Store the calculated total gas used to avoid recalculating it for subsequent uses let total_gas_used = gas_data.total_gas_used(); if verify_fee { // Deserialize the fee call to find the paid fee let fee: u64 = match deserialize_async(&tx.calls[fee_call_idx].data.data[1..9]).await { Ok(v) => v, Err(_) => return Err(TxVerifyFailed::InvalidFee.into()), }; // Compute the required fee for this transaction let required_fee = compute_fee(&total_gas_used); // Check that enough fee has been paid for the used gas in this transaction if required_fee > fee { return Err(TxVerifyFailed::InsufficientFee.into()) } // Store paid fee gas_data.paid = fee; } // When we're done looping and executing over the tx's contract calls and // (optionally) made sure that enough fee was paid, we now move on with // verification. First we verify the transaction signatures and then we // verify any accompanying ZK proofs. if sig_table.len() != tx.signatures.len() { return Err(TxVerifyFailed::MissingSignatures.into()) } if tx.verify_sigs(sig_table).is_err() { return Err(TxVerifyFailed::InvalidSignature.into()) } // Append hash to merkle tree append_tx_to_merkle_tree(tree, tx); Ok(gas_data) }