/* This file is part of DarkFi (https://dark.fi)
 *
 * Copyright (C) 2020-2025 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 darkfi_sdk::{
    blockchain::block_version,
    crypto::{schnorr::SchnorrPublic, ContractId, MerkleTree, PublicKey},
    dark_tree::dark_forest_leaf_vec_integrity_check,
    deploy::DeployParamsV1,
    pasta::pallas,
};
use darkfi_serial::{deserialize_async, serialize_async, AsyncDecodable, AsyncEncodable};
use log::{debug, error, warn};
use num_bigint::BigUint;
use smol::io::Cursor;

use crate::{
    blockchain::{
        block_store::append_tx_to_merkle_tree, BlockInfo, Blockchain, BlockchainOverlayPtr,
        HeaderHash,
    },
    error::TxVerifyFailed,
    runtime::vm_runtime::Runtime,
    tx::{Transaction, MAX_TX_CALLS, MIN_TX_CALLS},
    validator::{
        consensus::{Consensus, Fork, Proposal, GAS_LIMIT_UNPROPOSED_TXS},
        fees::{circuit_gas_use, compute_fee, GasData, PALLAS_SCHNORR_SIGNATURE_FEE},
        pow::PoWModule,
    },
    zk::VerifyingKey,
    Error, Result,
};

/// Verify given genesis [`BlockInfo`], and apply it to the provided overlay.
pub async fn verify_genesis_block(
    overlay: &BlockchainOverlayPtr,
    block: &BlockInfo,
    block_target: u32,
) -> Result<()> {
    let block_hash = block.hash().as_string();
    debug!(target: "validator::verification::verify_genesis_block", "Validating genesis block {}", block_hash);

    // Check if block already exists
    if overlay.lock().unwrap().has_block(block)? {
        return Err(Error::BlockAlreadyExists(block_hash))
    }

    // Block height must be 0
    if block.header.height != 0 {
        return Err(Error::BlockIsInvalid(block_hash))
    }

    // Block version must be correct
    if block.header.version != block_version(block.header.height) {
        return Err(Error::BlockIsInvalid(block_hash))
    }

    // Verify transactions vector contains at least one(producers) transaction
    if block.txs.is_empty() {
        return Err(Error::BlockContainsNoTransactions(block_hash))
    }

    // Genesis producer transaction must be the Transaction::default() one(empty)
    let producer_tx = block.txs.last().unwrap();
    if producer_tx != &Transaction::default() {
        error!(target: "validator::verification::verify_genesis_block", "Genesis producer transaction is not default one");
        return Err(TxVerifyFailed::ErroneousTxs(vec![producer_tx.clone()]).into())
    }

    // Verify transactions, exluding producer(last) one/
    // Genesis block doesn't check for fees
    let mut tree = MerkleTree::new(1);
    let txs = &block.txs[..block.txs.len() - 1];
    if let Err(e) =
        verify_transactions(overlay, block.header.height, block_target, txs, &mut tree, false).await
    {
        warn!(
            target: "validator::verification::verify_genesis_block",
            "[VALIDATOR] Erroneous transactions found in set",
        );
        overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?;
        return Err(e)
    }

    // Append producer transaction to the tree and check tree matches header one
    append_tx_to_merkle_tree(&mut tree, producer_tx);
    if tree.root(0).unwrap() != block.header.root {
        error!(target: "validator::verification::verify_genesis_block", "Genesis Merkle tree is invalid");
        return Err(Error::BlockIsInvalid(block_hash))
    }

    // Insert block
    overlay.lock().unwrap().add_block(block)?;

    debug!(target: "validator::verification::verify_genesis_block", "Genesis block {} verified successfully", block_hash);
    Ok(())
}

/// Validate provided block according to set rules.
///
/// A block is considered valid when the following rules apply:
///     1. Block version is correct for its height
///     2. Parent hash is equal to the hash of the previous block
///     3. Block height increments previous block height by 1
///     4. Timestamp is valid based on PoWModule validation
///     5. Block hash is valid based on PoWModule validation
/// Additional validity rules can be applied.
pub fn validate_block(block: &BlockInfo, previous: &BlockInfo, module: &PoWModule) -> Result<()> {
    // Check block version (1)
    if block.header.version != block_version(block.header.height) {
        return Err(Error::BlockIsInvalid(block.hash().as_string()))
    }

    // Check previous hash (2)
    if block.header.previous != previous.hash() {
        return Err(Error::BlockIsInvalid(block.hash().as_string()))
    }

    // Check heights are incremental (3)
    if block.header.height != previous.header.height + 1 {
        return Err(Error::BlockIsInvalid(block.hash().as_string()))
    }

    // Check timestamp validity (4)
    if !module.verify_timestamp_by_median(block.header.timestamp) {
        return Err(Error::BlockIsInvalid(block.hash().as_string()))
    }

    // Check block hash corresponds to next one (5)
    module.verify_block_hash(block)?;

    Ok(())
}

/// A blockchain is considered valid, when every block is valid,
/// based on validate_block checks.
/// Be careful as this will try to load everything in memory.
pub fn validate_blockchain(
    blockchain: &Blockchain,
    pow_target: u32,
    pow_fixed_difficulty: Option<BigUint>,
) -> Result<()> {
    // Generate a PoW module
    let mut module = PoWModule::new(blockchain.clone(), pow_target, pow_fixed_difficulty, None)?;
    // We use block order store here so we have all blocks in order
    let blocks = blockchain.blocks.get_all_order()?;
    for (index, block) in blocks[1..].iter().enumerate() {
        let full_blocks = blockchain.get_blocks_by_hash(&[blocks[index].1, block.1])?;
        let full_block = &full_blocks[1];
        validate_block(full_block, &full_blocks[0], &module)?;
        // Update PoW module
        module.append(full_block.header.timestamp, &module.next_difficulty()?);
    }

    Ok(())
}

/// Verify given [`BlockInfo`], and apply it to the provided overlay.
pub async fn verify_block(
    overlay: &BlockchainOverlayPtr,
    module: &PoWModule,
    block: &BlockInfo,
    previous: &BlockInfo,
    verify_fees: bool,
) -> Result<()> {
    let block_hash = block.hash();
    debug!(target: "validator::verification::verify_block", "Validating block {}", block_hash);

    // Check if block already exists
    if overlay.lock().unwrap().has_block(block)? {
        return Err(Error::BlockAlreadyExists(block_hash.as_string()))
    }

    // Validate block, using its previous
    validate_block(block, previous, module)?;

    // Verify transactions vector contains at least one(producers) transaction
    if block.txs.is_empty() {
        return Err(Error::BlockContainsNoTransactions(block_hash.as_string()))
    }

    // Verify transactions, exluding producer(last) one
    let mut tree = MerkleTree::new(1);
    let txs = &block.txs[..block.txs.len() - 1];
    let e = verify_transactions(
        overlay,
        block.header.height,
        module.target,
        txs,
        &mut tree,
        verify_fees,
    )
    .await;
    if let Err(e) = e {
        warn!(
            target: "validator::verification::verify_block",
            "[VALIDATOR] Erroneous transactions found in set",
        );
        overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?;
        return Err(e)
    }

    // Verify producer transaction
    let public_key = verify_producer_transaction(
        overlay,
        block.header.height,
        module.target,
        block.txs.last().unwrap(),
        &mut tree,
    )
    .await?;

    // Verify transactions merkle tree root matches header one
    if tree.root(0).unwrap() != block.header.root {
        error!(target: "validator::verification::verify_block", "Block Merkle tree root is invalid");
        return Err(Error::BlockIsInvalid(block_hash.as_string()))
    }

    // Verify producer signature
    verify_producer_signature(block, &public_key)?;

    // Insert block
    overlay.lock().unwrap().add_block(block)?;

    debug!(target: "validator::verification::verify_block", "Block {} verified successfully", block_hash);
    Ok(())
}

/// Verify given checkpoint [`BlockInfo`], and apply it to the provided overlay.
pub async fn verify_checkpoint_block(
    overlay: &BlockchainOverlayPtr,
    block: &BlockInfo,
    header: &HeaderHash,
    block_target: u32,
) -> Result<()> {
    let block_hash = block.hash();
    debug!(target: "validator::verification::verify_checkpoint_block", "Validating block {}", block_hash);

    // Check if block already exists
    if overlay.lock().unwrap().has_block(block)? {
        return Err(Error::BlockAlreadyExists(block_hash.as_string()))
    }

    // Check if block hash matches the expected(provided) one
    if block_hash != *header {
        error!(target: "validator::verification::verify_checkpoint_block", "Block hash doesn't match the expected one");
        return Err(Error::BlockIsInvalid(block_hash.as_string()))
    }

    // Verify transactions vector contains at least one(producers) transaction
    if block.txs.is_empty() {
        return Err(Error::BlockContainsNoTransactions(block_hash.as_string()))
    }

    // Apply transactions, excluding producer(last) one
    let mut tree = MerkleTree::new(1);
    let txs = &block.txs[..block.txs.len() - 1];
    let e = apply_transactions(overlay, block.header.height, block_target, txs, &mut tree).await;
    if let Err(e) = e {
        warn!(
            target: "validator::verification::verify_checkpoint_block",
            "[VALIDATOR] Erroneous transactions found in set",
        );
        overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?;
        return Err(e)
    }

    // Apply producer transaction
    let public_key = apply_producer_transaction(
        overlay,
        block.header.height,
        block_target,
        block.txs.last().unwrap(),
        &mut tree,
    )
    .await?;

    // Verify transactions merkle tree root matches header one
    if tree.root(0).unwrap() != block.header.root {
        error!(target: "validator::verification::verify_checkpoint_block", "Block Merkle tree root is invalid");
        return Err(Error::BlockIsInvalid(block_hash.as_string()))
    }

    // Verify producer signature
    verify_producer_signature(block, &public_key)?;

    // Insert block
    overlay.lock().unwrap().add_block(block)?;

    debug!(target: "validator::verification::verify_checkpoint_block", "Block {} verified successfully", block_hash);
    Ok(())
}

/// Verify block proposer signature, using the producer transaction signature as signing key
/// over blocks header hash.
pub fn verify_producer_signature(block: &BlockInfo, public_key: &PublicKey) -> Result<()> {
    if !public_key.verify(block.header.hash().inner(), &block.signature) {
        warn!(target: "validator::verification::verify_producer_signature", "Proposer {} signature could not be verified", public_key);
        return Err(Error::InvalidSignature)
    }

    Ok(())
}

/// Verify provided producer [`Transaction`].
///
/// Verify WASM execution, signatures, and ZK proofs and apply it to the provided,
/// provided overlay. Returns transaction signature public key. Additionally,
/// append its hash to the provided Merkle tree.
pub async fn verify_producer_transaction(
    overlay: &BlockchainOverlayPtr,
    verifying_block_height: u32,
    block_target: u32,
    tx: &Transaction,
    tree: &mut MerkleTree,
) -> Result<PublicKey> {
    let tx_hash = tx.hash();
    debug!(target: "validator::verification::verify_producer_transaction", "Validating producer transaction {}", tx_hash);

    // Transaction must be a PoW reward one
    if !tx.is_pow_reward() {
        return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
    }

    // Retrieve first call from the transaction for further processing
    let call = &tx.calls[0];

    // Map of ZK proof verifying keys for the current transaction
    let mut verifying_keys: HashMap<[u8; 32], HashMap<String, VerifyingKey>> = HashMap::new();

    // Initialize the map
    verifying_keys.insert(call.data.contract_id.to_bytes(), HashMap::new());

    // 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![];

    debug!(target: "validator::verification::verify_producer_transaction", "Executing contract call");

    // Write the actual payload data
    let mut payload = vec![];
    tx.calls.encode_async(&mut payload).await?; // Actual call data

    debug!(target: "validator::verification::verify_producer_transaction", "Instantiating WASM runtime");
    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 index in producer tx is 0
        0,
    )?;

    debug!(target: "validator::verification::verify_producer_transaction", "Executing \"metadata\" call");
    let metadata = runtime.metadata(&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?;

    // Check that only one ZK proof and signature public key exist
    if zkp_pub.len() != 1 || sig_pub.len() != 1 {
        error!(target: "validator::verification::verify_producer_transaction", "Producer transaction contains multiple ZK proofs or signature public keys");
        return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
    }

    // TODO: Make sure we've read all the bytes above.
    debug!(target: "validator::verification::verify_producer_transaction", "Successfully executed \"metadata\" call");

    // Here we'll look up verifying keys and insert them into the map.
    debug!(target: "validator::verification::verify_producer_transaction", "Performing VerifyingKey lookups from the sled db");
    for (zkas_ns, _) in &zkp_pub {
        // TODO: verify this is correct behavior
        let inner_vk_map = verifying_keys.get_mut(&call.data.contract_id.to_bytes()).unwrap();
        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);
    }

    zkp_table.push(zkp_pub);
    let signature_public_key = *sig_pub.last().unwrap();
    sig_table.push(sig_pub);

    // 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.
    debug!(target: "validator::verification::verify_producer_transaction", "Executing \"exec\" call");
    let mut state_update = vec![call.data.data[0]];
    state_update.append(&mut runtime.exec(&payload)?);
    debug!(target: "validator::verification::verify_producer_transaction", "Successfully executed \"exec\" call");

    // If that was successful, we apply the state update in the ephemeral overlay.
    debug!(target: "validator::verification::verify_producer_transaction", "Executing \"apply\" call");
    runtime.apply(&state_update)?;
    debug!(target: "validator::verification::verify_producer_transaction", "Successfully executed \"apply\" call");

    // When we're done executing over the tx's contract call, we now move on with verification.
    // First we verify the signatures as that's cheaper, and then finally we verify the ZK proofs.
    debug!(target: "validator::verification::verify_producer_transaction", "Verifying signatures for transaction {}", tx_hash);
    if sig_table.len() != tx.signatures.len() {
        error!(target: "validator::verification::verify_producer_transaction", "Incorrect number of signatures in tx {}", tx_hash);
        return Err(TxVerifyFailed::MissingSignatures.into())
    }

    // TODO: Go through the ZK circuits that have to be verified and account for the opcodes.

    if let Err(e) = tx.verify_sigs(sig_table) {
        error!(target: "validator::verification::verify_producer_transaction", "Signature verification for tx {} failed: {}", tx_hash, e);
        return Err(TxVerifyFailed::InvalidSignature.into())
    }

    debug!(target: "validator::verification::verify_producer_transaction", "Signature verification successful");

    debug!(target: "validator::verification::verify_producer_transaction", "Verifying ZK proofs for transaction {}", tx_hash);
    if let Err(e) = tx.verify_zkps(&verifying_keys, zkp_table).await {
        error!(target: "validator::verification::verify_producer_transaction", "ZK proof verification for tx {} failed: {}", tx_hash, e);
        return Err(TxVerifyFailed::InvalidZkProof.into())
    }
    debug!(target: "validator::verification::verify_producer_transaction", "ZK proof verification successful");

    // Append hash to merkle tree
    append_tx_to_merkle_tree(tree, tx);

    debug!(target: "validator::verification::verify_producer_transaction", "Producer transaction {} verified successfully", tx_hash);

    Ok(signature_public_key)
}

/// Apply given producer [`Transaction`] to the provided overlay, without formal verification.
/// Returns transaction signature public key. Additionally, append its hash to the provided Merkle tree.
async fn apply_producer_transaction(
    overlay: &BlockchainOverlayPtr,
    verifying_block_height: u32,
    block_target: u32,
    tx: &Transaction,
    tree: &mut MerkleTree,
) -> Result<PublicKey> {
    let tx_hash = tx.hash();
    debug!(target: "validator::verification::apply_producer_transaction", "Applying producer transaction {}", tx_hash);

    // Producer transactions must contain a single, non-empty call
    if !tx.is_single_call() {
        return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
    }

    debug!(target: "validator::verification::apply_producer_transaction", "Executing contract call");

    // Write the actual payload data
    let mut payload = vec![];
    tx.calls.encode_async(&mut payload).await?; // Actual call data

    debug!(target: "validator::verification::apply_producer_transaction", "Instantiating WASM runtime");
    let call = &tx.calls[0];
    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 index in producer tx is 0
        0,
    )?;

    debug!(target: "validator::verification::apply_producer_transaction", "Executing \"metadata\" call");
    let metadata = runtime.metadata(&payload)?;

    // Decode the metadata retrieved from the execution
    let mut decoder = Cursor::new(&metadata);

    // The tuple is (zkas_ns, public_inputs)
    let _: Vec<(String, Vec<pallas::Base>)> = AsyncDecodable::decode_async(&mut decoder).await?;
    let sig_pub: Vec<PublicKey> = AsyncDecodable::decode_async(&mut decoder).await?;

    // Check that only one ZK proof and signature public key exist
    if sig_pub.len() != 1 {
        error!(target: "validator::verification::apply_producer_transaction", "Producer transaction contains multiple ZK proofs or signature public keys");
        return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
    }

    let signature_public_key = *sig_pub.last().unwrap();

    // 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.
    debug!(target: "validator::verification::apply_producer_transaction", "Executing \"exec\" call");
    let mut state_update = vec![call.data.data[0]];
    state_update.append(&mut runtime.exec(&payload)?);
    debug!(target: "validator::verification::apply_producer_transaction", "Successfully executed \"exec\" call");

    // If that was successful, we apply the state update in the ephemeral overlay.
    debug!(target: "validator::verification::apply_producer_transaction", "Executing \"apply\" call");
    runtime.apply(&state_update)?;
    debug!(target: "validator::verification::apply_producer_transaction", "Successfully executed \"apply\" call");

    // Append hash to merkle tree
    append_tx_to_merkle_tree(tree, tx);

    debug!(target: "validator::verification::apply_producer_transaction", "Producer transaction {} executed successfully", tx_hash);

    Ok(signature_public_key)
}

/// Verify WASM execution, signatures, and ZK proofs for a given [`Transaction`],
/// and apply it to the provided overlay. Additionally, append its hash to the
/// provided Merkle tree.
pub async fn verify_transaction(
    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,
) -> Result<GasData> {
    let tx_hash = tx.hash();
    debug!(target: "validator::verification::verify_transaction", "Validating transaction {}", 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 {
        let mut found_fee = false;
        // Verify that there is a money fee call in the transaction
        for (call_idx, call) in tx.calls.iter().enumerate() {
            if call.data.is_money_fee() {
                found_fee = true;
                fee_call_idx = call_idx;
                break
            }
        }

        if !found_fee {
            error!(
                target: "validator::verification::verify_transcation",
                "[VALIDATOR] Transaction {} does not contain fee payment call", tx_hash,
            );
            return Err(TxVerifyFailed::InvalidFee.into())
        }
    }

    // Write the transaction calls payload data
    let mut payload = vec![];
    tx.calls.encode_async(&mut payload).await?;

    // 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() {
        debug!(target: "validator::verification::verify_transaction", "Executing contract call {}", idx);

        // Transaction must not contain a Pow reward call
        if call.data.is_money_pow_reward() {
            error!(target: "validator::verification::verify_transaction", "Reward transaction detected");
            return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
        }

        debug!(target: "validator::verification::verify_transaction", "Instantiating WASM runtime");
        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,
            idx as u8,
        )?;

        debug!(target: "validator::verification::verify_transaction", "Executing \"metadata\" call");
        let metadata = runtime.metadata(&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 {
            error!(
                target: "validator::verification::verify_transaction",
                "[VALIDATOR] Failed decoding entire metadata buffer for {}:{}", tx_hash, idx,
            );
            return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
        }

        debug!(target: "validator::verification::verify_transaction", "Successfully executed \"metadata\" call");

        // 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?
        debug!(target: "validator::verification::verify_transaction", "Performing VerifyingKey lookups from the sled db");
        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);

        // 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.
        debug!(target: "validator::verification::verify_transaction", "Executing \"exec\" call");
        let mut state_update = vec![call.data.data[0]];
        state_update.append(&mut runtime.exec(&payload)?);
        debug!(target: "validator::verification::verify_transaction", "Successfully executed \"exec\" call");

        // If that was successful, we apply the state update in the ephemeral overlay.
        debug!(target: "validator::verification::verify_transaction", "Executing \"apply\" call");
        runtime.apply(&state_update)?;
        debug!(target: "validator::verification::verify_transaction", "Successfully executed \"apply\" call");

        // 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 */
        {
            debug!(target: "validator::verification::verify_transaction", "Deploying new contract");
            // 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,
                idx as u8,
            )?;

            deploy_runtime.deploy(&deploy_params.ix)?;

            let deploy_gas_used = deploy_runtime.gas_used();
            debug!(target: "validator::verification::verify_transaction", "The gas used for deployment call {:?} of transaction {}: {}", call, tx_hash, deploy_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();
        debug!(target: "validator::verification::verify_transaction", "The gas used for WASM call {:?} of transaction {}: {}", call, tx_hash, wasm_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;
    debug!(target: "validator::verification::verify_transaction", "The gas used for signature of transaction {}: {}", tx_hash, gas_data.signatures);

    // 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);
        debug!(target: "validator::verification::verify_transaction", "The gas used for ZK circuit in namespace {} of transaction {}: {}", zkbin.namespace, tx_hash, zk_circuit_gas_used);

        // 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(e) => {
                error!(
                    target: "validator::verification::verify_transaction",
                    "[VALIDATOR] Failed deserializing tx {} fee call: {}", tx_hash, e,
                );
                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 {
            error!(
                target: "validator::verification::verify_transaction",
                "[VALIDATOR] Transaction {} has insufficient fee. Required: {}, Paid: {}",
                tx_hash, required_fee, fee,
            );
            return Err(TxVerifyFailed::InsufficientFee.into())
        }
        debug!(target: "validator::verification::verify_transaction", "The gas paid for transaction {}: {}", tx_hash, gas_data.paid);

        // 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.
    debug!(target: "validator::verification::verify_transaction", "Verifying signatures for transaction {}", tx_hash);
    if sig_table.len() != tx.signatures.len() {
        error!(
            target: "validator::verification::verify_transaction",
            "[VALIDATOR] Incorrect number of signatures in tx {}", tx_hash,
        );
        return Err(TxVerifyFailed::MissingSignatures.into())
    }

    if let Err(e) = tx.verify_sigs(sig_table) {
        error!(
            target: "validator::verification::verify_transaction",
            "[VALIDATOR] Signature verification for tx {} failed: {}", tx_hash, e,
        );
        return Err(TxVerifyFailed::InvalidSignature.into())
    }
    debug!(target: "validator::verification::verify_transaction", "Signature verification successful");

    debug!(target: "validator::verification::verify_transaction", "Verifying ZK proofs for transaction {}", tx_hash);
    if let Err(e) = tx.verify_zkps(verifying_keys, zkp_table).await {
        error!(
            target: "validator::verification::verify_transaction",
            "[VALIDATOR] ZK proof verification for tx {} failed: {}", tx_hash, e,
        );
        return Err(TxVerifyFailed::InvalidZkProof.into())
    }
    debug!(target: "validator::verification::verify_transaction", "ZK proof verification successful");

    // Append hash to merkle tree
    append_tx_to_merkle_tree(tree, tx);

    debug!(target: "validator::verification::verify_transaction", "The total gas used for transaction {}: {}", tx_hash, total_gas_used);
    debug!(target: "validator::verification::verify_transaction", "Transaction {} verified successfully", tx_hash);
    Ok(gas_data)
}

/// Apply given [`Transaction`] to the provided overlay.
/// Additionally, append its hash to the provided Merkle tree.
async fn apply_transaction(
    overlay: &BlockchainOverlayPtr,
    verifying_block_height: u32,
    block_target: u32,
    tx: &Transaction,
    tree: &mut MerkleTree,
) -> Result<()> {
    let tx_hash = tx.hash();
    debug!(target: "validator::verification::apply_transaction", "Applying transaction {}", tx_hash);

    // Write the transaction calls payload data
    let mut payload = vec![];
    tx.calls.encode_async(&mut payload).await?;

    // Iterate over all calls to get the metadata
    for (idx, call) in tx.calls.iter().enumerate() {
        debug!(target: "validator::verification::apply_transaction", "Executing contract call {}", idx);

        debug!(target: "validator::verification::apply_transaction", "Instantiating WASM runtime");
        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,
            idx as u8,
        )?;

        // Run the "exec" function. We keep the returned state update in a buffer, prefixed
        // by the call function ID, enforcing the state update function in the contract.
        debug!(target: "validator::verification::apply_transaction", "Executing \"exec\" call");
        let mut state_update = vec![call.data.data[0]];
        state_update.append(&mut runtime.exec(&payload)?);
        debug!(target: "validator::verification::apply_transaction", "Successfully executed \"exec\" call");

        // If that was successful, we apply the state update in the ephemeral overlay
        debug!(target: "validator::verification::apply_transaction", "Executing \"apply\" call");
        runtime.apply(&state_update)?;
        debug!(target: "validator::verification::apply_transaction", "Successfully executed \"apply\" call");

        // 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 */
        {
            debug!(target: "validator::verification::apply_transaction", "Deploying new contract");
            // 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,
                idx as u8,
            )?;

            deploy_runtime.deploy(&deploy_params.ix)?;
        }
    }

    // Append hash to merkle tree
    append_tx_to_merkle_tree(tree, tx);

    debug!(target: "validator::verification::apply_transaction", "Transaction {} applied successfully", tx_hash);
    Ok(())
}

/// Verify a set of [`Transaction`] in sequence and apply them if all are valid.
///
/// In case any of the transactions fail, they will be returned to the caller as an error.
/// If all transactions are valid, the function will return the total gas used and total
/// paid fees from all the transactions. Additionally, their hash is appended to the provided
/// Merkle tree.
pub async fn verify_transactions(
    overlay: &BlockchainOverlayPtr,
    verifying_block_height: u32,
    block_target: u32,
    txs: &[Transaction],
    tree: &mut MerkleTree,
    verify_fees: bool,
) -> Result<(u64, u64)> {
    debug!(target: "validator::verification::verify_transactions", "Verifying {} transactions", txs.len());
    if txs.is_empty() {
        return Ok((0, 0))
    }

    // Tracker for failed txs
    let mut erroneous_txs = vec![];

    // Total gas accumulators
    let mut total_gas_used = 0;
    let mut total_gas_paid = 0;

    // Map of ZK proof verifying keys for the current transaction batch
    let mut vks: HashMap<[u8; 32], HashMap<String, VerifyingKey>> = HashMap::new();

    // Initialize the map
    for tx in txs {
        for call in &tx.calls {
            vks.insert(call.data.contract_id.to_bytes(), HashMap::new());
        }
    }

    // Iterate over transactions and attempt to verify them
    for tx in txs {
        overlay.lock().unwrap().checkpoint();
        let gas_data = match verify_transaction(
            overlay,
            verifying_block_height,
            block_target,
            tx,
            tree,
            &mut vks,
            verify_fees,
        )
        .await
        {
            Ok(gas_values) => gas_values,
            Err(e) => {
                warn!(target: "validator::verification::verify_transactions", "Transaction verification failed: {}", e);
                erroneous_txs.push(tx.clone());
                overlay.lock().unwrap().revert_to_checkpoint()?;
                continue
            }
        };

        // Store the gas used by the verified transaction
        let tx_gas_used = gas_data.total_gas_used();

        // Calculate current accumulated gas usage
        let accumulated_gas_usage = total_gas_used + tx_gas_used;

        // Check gas limit - if accumulated gas used exceeds it, break out of loop
        if accumulated_gas_usage > GAS_LIMIT_UNPROPOSED_TXS {
            warn!(target: "validator::verification::verify_transactions", "Transaction {} exceeds configured transaction gas limit: {} - {}", tx.hash(), accumulated_gas_usage, GAS_LIMIT_UNPROPOSED_TXS);
            erroneous_txs.push(tx.clone());
            overlay.lock().unwrap().revert_to_checkpoint()?;
            break
        }

        // Update accumulated total gas
        total_gas_used += tx_gas_used;
        total_gas_paid += gas_data.paid;
    }

    if !erroneous_txs.is_empty() {
        return Err(TxVerifyFailed::ErroneousTxs(erroneous_txs).into())
    }

    Ok((total_gas_used, total_gas_paid))
}

/// Apply given set of [`Transaction`] in sequence, without formal verification.
/// In case any of the transactions fail, they will be returned to the caller as an error.
/// Additionally, their hash is appended to the provided Merkle tree.
async fn apply_transactions(
    overlay: &BlockchainOverlayPtr,
    verifying_block_height: u32,
    block_target: u32,
    txs: &[Transaction],
    tree: &mut MerkleTree,
) -> Result<()> {
    debug!(target: "validator::verification::apply_transactions", "Applying {} transactions", txs.len());
    if txs.is_empty() {
        return Ok(())
    }

    // Tracker for failed txs
    let mut erroneous_txs = vec![];

    // Iterate over transactions and attempt to apply them
    for tx in txs {
        overlay.lock().unwrap().checkpoint();
        if let Err(e) =
            apply_transaction(overlay, verifying_block_height, block_target, tx, tree).await
        {
            warn!(target: "validator::verification::apply_transactions", "Transaction apply failed: {}", e);
            erroneous_txs.push(tx.clone());
            overlay.lock().unwrap().revert_to_checkpoint()?;
        };
    }

    if !erroneous_txs.is_empty() {
        return Err(TxVerifyFailed::ErroneousTxs(erroneous_txs).into())
    }

    Ok(())
}

/// Verify given [`Proposal`] against provided consensus state.
///
/// A proposal is considered valid when the following rules apply:
///     1. Proposal hash matches the actual block one
///     2. Block is valid
/// Additional validity rules can be applied.
pub async fn verify_proposal(
    consensus: &Consensus,
    proposal: &Proposal,
    verify_fees: bool,
) -> Result<(Fork, Option<usize>)> {
    // Check if proposal hash matches actual one (1)
    let proposal_hash = proposal.block.hash();
    if proposal.hash != proposal_hash {
        warn!(
            target: "validator::verification::verify_proposal", "Received proposal contains mismatched hashes: {} - {}",
            proposal.hash, proposal_hash
        );
        return Err(Error::ProposalHashesMissmatchError)
    }

    // Check if proposal extends any existing forks
    let (fork, index) = consensus.find_extended_fork(proposal).await?;

    // Grab overlay last block
    let previous = fork.overlay.lock().unwrap().last_block()?;

    // Verify proposal block (2)
    if verify_block(&fork.overlay, &fork.module, &proposal.block, &previous, verify_fees)
        .await
        .is_err()
    {
        error!(target: "validator::verification::verify_proposal", "Erroneous proposal block found");
        fork.overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?;
        return Err(Error::BlockIsInvalid(proposal.hash.as_string()))
    };

    Ok((fork, index))
}

/// Verify given [`Proposal`] against provided fork state.
///
/// A proposal is considered valid when the following rules apply:
///     1. Proposal hash matches the actual block one
///     2. Block is valid
/// Additional validity rules can be applied.
pub async fn verify_fork_proposal(
    fork: &Fork,
    proposal: &Proposal,
    verify_fees: bool,
) -> Result<()> {
    // Check if proposal hash matches actual one (1)
    let proposal_hash = proposal.block.hash();
    if proposal.hash != proposal_hash {
        warn!(
            target: "validator::verification::verify_fork_proposal", "Received proposal contains mismatched hashes: {} - {}",
            proposal.hash, proposal_hash
        );
        return Err(Error::ProposalHashesMissmatchError)
    }

    // Grab overlay last block
    let previous = fork.overlay.lock().unwrap().last_block()?;

    // Verify proposal block (2)
    if verify_block(&fork.overlay, &fork.module, &proposal.block, &previous, verify_fees)
        .await
        .is_err()
    {
        error!(target: "validator::verification::verify_fork_proposal", "Erroneous proposal block found");
        fork.overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?;
        return Err(Error::BlockIsInvalid(proposal.hash.as_string()))
    };

    Ok(())
}
