| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650 |
- /* 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 <https://www.gnu.org/licenses/>.
- */
- use std::collections::HashMap;
- use clap::Parser;
- use darkfi::{
- blockchain::{
- Blockchain, BlockchainOverlay, BlockchainOverlayPtr, block_store::append_tx_to_merkle_tree,
- },
- cli_desc,
- error::TxVerifyFailed,
- runtime::vm_runtime::Runtime,
- 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 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 sled_db = sled_overlay::sled::open(&db_path).unwrap();
- let blockchain = Blockchain::new(&sled_db).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<Transaction> = 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<String, VerifyingKey>> = 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<u32> = (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<String, VerifyingKey>>,
- verify_fee: bool,
- ) -> darkfi::Result<GasData> {
- 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![];
- // 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(),
- 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(),
- 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<String, VerifyingKey>>,
- verify_fee: bool,
- ) -> darkfi::Result<GasData> {
- 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![];
- // 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(),
- 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<pallas::Base>)> =
- AsyncDecodable::decode_async(&mut decoder).await?;
- let sig_pub: Vec<PublicKey> = 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<String, VerifyingKey>>,
- verify_fee: bool,
- ) -> darkfi::Result<GasData> {
- 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![];
- // 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(),
- 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<pallas::Base>)> =
- AsyncDecodable::decode_async(&mut decoder).await?;
- let sig_pub: Vec<PublicKey> = 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)
- }
|