|
|
@@ -26,8 +26,9 @@ use darkfi_sdk::{
|
|
|
},
|
|
|
dark_tree::dark_forest_leaf_vec_integrity_check,
|
|
|
deploy::DeployParamsV1,
|
|
|
- fee::minimum_fee,
|
|
|
+ fee::{accumulate_fee, burn_fee, miner_claimable_fee, minimum_fee},
|
|
|
pasta::pallas,
|
|
|
+ tx::TransactionHash,
|
|
|
};
|
|
|
use darkfi_serial::{deserialize_async, serialize_async, AsyncDecodable, AsyncEncodable};
|
|
|
use kvdb_overlay::DatabaseOverlayStateDiff;
|
|
|
@@ -47,7 +48,7 @@ use crate::{
|
|
|
util::time::Timestamp,
|
|
|
validator::{
|
|
|
consensus::{Consensus, Fork, Proposal, BLOCK_GAS_LIMIT},
|
|
|
- fees::{circuit_gas_use, GasData, PALLAS_SCHNORR_VERIFY_GAS},
|
|
|
+ fees::{add_miner_claimable_fee, circuit_gas_use, GasData, PALLAS_SCHNORR_VERIFY_GAS},
|
|
|
pow::PoWModule,
|
|
|
},
|
|
|
zk::VerifyingKey,
|
|
|
@@ -324,6 +325,7 @@ pub async fn verify_checkpoint_block(
|
|
|
block: &BlockInfo,
|
|
|
header: &HeaderHash,
|
|
|
block_target: u32,
|
|
|
+ verify_fees: bool,
|
|
|
) -> Result<()> {
|
|
|
let block_hash = block.hash();
|
|
|
debug!(target: "validator::verification::verify_checkpoint_block", "Validating block {block_hash}");
|
|
|
@@ -349,7 +351,8 @@ pub async fn verify_checkpoint_block(
|
|
|
let mut tree = MerkleTree::new(1);
|
|
|
let txs = &block.txs[..block.txs.len() - 1];
|
|
|
if let Err(e) =
|
|
|
- apply_transactions(overlay, block.header.height, block_target, txs, &mut tree).await
|
|
|
+ apply_transactions(overlay, block.header.height, block_target, txs, &mut tree, verify_fees)
|
|
|
+ .await
|
|
|
{
|
|
|
warn!(
|
|
|
target: "validator::verification::verify_checkpoint_block",
|
|
|
@@ -406,6 +409,122 @@ pub fn verify_producer_signature(block: &BlockInfo, public_key: &PublicKey) -> R
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
+fn extract_fee_call(
|
|
|
+ tx_hash: &TransactionHash,
|
|
|
+ tx: &Transaction,
|
|
|
+ verify_fee: bool,
|
|
|
+) -> Result<Option<(usize, u64)>> {
|
|
|
+ if !verify_fee {
|
|
|
+ return Ok(None)
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut fee_call = None;
|
|
|
+
|
|
|
+ for (call_idx, call) in tx.calls.iter().enumerate() {
|
|
|
+ if !call.data.is_money_fee() {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if fee_call.is_some() {
|
|
|
+ error!(
|
|
|
+ target: "validator::verification::extract_fee_call",
|
|
|
+ "[VALIDATOR] Transaction {tx_hash} contains multiple fee payment calls"
|
|
|
+ );
|
|
|
+ return Err(TxVerifyFailed::InvalidFee.into())
|
|
|
+ }
|
|
|
+
|
|
|
+ let fee = match call.data.money_fee_value() {
|
|
|
+ Ok(fee) => fee,
|
|
|
+ Err(e) => {
|
|
|
+ error!(
|
|
|
+ target: "validator::verification::extract_fee_call",
|
|
|
+ "[VALIDATOR] Failed parsing tx {tx_hash} fee call: {e}"
|
|
|
+ );
|
|
|
+ return Err(TxVerifyFailed::InvalidFee.into())
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ if fee == 0 {
|
|
|
+ error!(
|
|
|
+ target: "validator::verification::extract_fee_call",
|
|
|
+ "[VALIDATOR] Transaction {tx_hash} contains zero fee payment"
|
|
|
+ );
|
|
|
+ return Err(TxVerifyFailed::InvalidFee.into())
|
|
|
+ }
|
|
|
+
|
|
|
+ fee_call = Some((call_idx, fee));
|
|
|
+ }
|
|
|
+
|
|
|
+ if fee_call.is_none() {
|
|
|
+ error!(
|
|
|
+ target: "validator::verification::extract_fee_call",
|
|
|
+ "[VALIDATOR] Transaction {tx_hash} does not contain fee payment call"
|
|
|
+ );
|
|
|
+ return Err(TxVerifyFailed::InvalidFee.into())
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(fee_call)
|
|
|
+}
|
|
|
+
|
|
|
+fn calculate_miner_claimable_fee(
|
|
|
+ tx_hash: &TransactionHash,
|
|
|
+ total_gas_used: u64,
|
|
|
+ fee: u64,
|
|
|
+) -> Result<u64> {
|
|
|
+ let required_fee = match minimum_fee(total_gas_used) {
|
|
|
+ Ok(fee) => fee,
|
|
|
+ Err(e) => {
|
|
|
+ error!(
|
|
|
+ target: "validator::verification::calculate_miner_claimable_fee",
|
|
|
+ "[VALIDATOR] Failed calculating tx {tx_hash} fee: {e}"
|
|
|
+ );
|
|
|
+ return Err(TxVerifyFailed::InvalidFee.into())
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ if required_fee > fee {
|
|
|
+ error!(
|
|
|
+ target: "validator::verification::calculate_miner_claimable_fee",
|
|
|
+ "[VALIDATOR] Transaction {tx_hash} has insufficient fee. \
|
|
|
+ Required: {required_fee}, Paid: {fee}"
|
|
|
+ );
|
|
|
+ return Err(TxVerifyFailed::InsufficientFee.into())
|
|
|
+ }
|
|
|
+
|
|
|
+ let burned_fee = match burn_fee(required_fee) {
|
|
|
+ Ok(fee) => fee,
|
|
|
+ Err(e) => {
|
|
|
+ error!(
|
|
|
+ target: "validator::verification::calculate_miner_claimable_fee",
|
|
|
+ "[VALIDATOR] Failed calculating tx {tx_hash} burned fee: {e}"
|
|
|
+ );
|
|
|
+ return Err(TxVerifyFailed::InvalidFee.into())
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ let claimable_fee = match miner_claimable_fee(fee, burned_fee) {
|
|
|
+ Ok(fee) => fee,
|
|
|
+ Err(e) => {
|
|
|
+ error!(
|
|
|
+ target: "validator::verification::calculate_miner_claimable_fee",
|
|
|
+ "[VALIDATOR] Failed calculating tx {tx_hash} miner-claimable fee: {e}"
|
|
|
+ );
|
|
|
+ return Err(TxVerifyFailed::InvalidFee.into())
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ let tip = fee - required_fee;
|
|
|
+
|
|
|
+ debug!(target: "validator::verification::calculate_miner_claimable_fee", "The fee paid for transaction {tx_hash}: {fee}");
|
|
|
+ debug!(
|
|
|
+ target: "validator::verification::calculate_miner_claimable_fee",
|
|
|
+ "The fee split for transaction {tx_hash}: required={required_fee}, \
|
|
|
+ burned={burned_fee}, miner_claimable={claimable_fee}, tip={tip}"
|
|
|
+ );
|
|
|
+
|
|
|
+ Ok(claimable_fee)
|
|
|
+}
|
|
|
+
|
|
|
/// Verify provided producer [`Transaction`].
|
|
|
///
|
|
|
/// Verify WASM execution, signatures, and ZK proofs and apply it to
|
|
|
@@ -670,38 +789,7 @@ pub async fn verify_transaction(
|
|
|
// 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 {
|
|
|
- error!(
|
|
|
- target: "validator::verification::verify_transcation",
|
|
|
- "[VALIDATOR] Transaction {tx_hash} contains multiple fee payment calls"
|
|
|
- );
|
|
|
- return Err(TxVerifyFailed::InvalidFee.into())
|
|
|
- }
|
|
|
-
|
|
|
- found_fee = true;
|
|
|
- fee_call_idx = call_idx;
|
|
|
- }
|
|
|
-
|
|
|
- if !found_fee {
|
|
|
- error!(
|
|
|
- target: "validator::verification::verify_transcation",
|
|
|
- "[VALIDATOR] Transaction {tx_hash} does not contain fee payment call"
|
|
|
- );
|
|
|
- return Err(TxVerifyFailed::InvalidFee.into())
|
|
|
- }
|
|
|
- }
|
|
|
+ let fee_call = extract_fee_call(&tx_hash, tx, verify_fee)?;
|
|
|
|
|
|
// Write the transaction calls payload data
|
|
|
let mut payload = vec![];
|
|
|
@@ -792,14 +880,13 @@ pub async fn verify_transaction(
|
|
|
// 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);
|
|
|
+ if !inner_vk_map.contains_key(zkas_ns.as_str()) {
|
|
|
+ inner_vk_map.insert(zkas_ns.to_string(), vk);
|
|
|
+ }
|
|
|
+
|
|
|
circuits_to_verify.push(zkbin);
|
|
|
}
|
|
|
|
|
|
@@ -888,45 +975,14 @@ pub async fn verify_transaction(
|
|
|
return Err(TxVerifyFailed::GasLimitExceeded.into())
|
|
|
}
|
|
|
|
|
|
- if verify_fee {
|
|
|
- // Extract the paid fee from the fee call.
|
|
|
- let fee = match tx.calls[fee_call_idx].data.money_fee_value() {
|
|
|
- Ok(v) => v,
|
|
|
- Err(e) => {
|
|
|
- error!(
|
|
|
- target: "validator::verification::verify_transaction",
|
|
|
- "[VALIDATOR] Failed parsing tx {tx_hash} fee call: {e}"
|
|
|
- );
|
|
|
- return Err(TxVerifyFailed::InvalidFee.into())
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // Compute the required fee for this transaction
|
|
|
- let required_fee = match minimum_fee(total_gas_used) {
|
|
|
- Ok(fee) => fee,
|
|
|
- Err(e) => {
|
|
|
- error!(
|
|
|
- target: "validator::verification::verify_transaction",
|
|
|
- "[VALIDATOR] Failed calculating tx {tx_hash} fee: {e}"
|
|
|
- );
|
|
|
- return Err(TxVerifyFailed::InvalidFee.into())
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 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 {tx_hash} has insufficient fee. Required: {required_fee}, Paid: {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
|
|
|
+ let miner_claimable = if let Some((_, fee)) = fee_call {
|
|
|
+ let claimable_fee = calculate_miner_claimable_fee(&tx_hash, total_gas_used, fee)?;
|
|
|
gas_data.paid = fee;
|
|
|
- }
|
|
|
+ gas_data.miner_claimable = claimable_fee;
|
|
|
+ Some(claimable_fee)
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ };
|
|
|
|
|
|
// When we're done looping and executing over the tx's contract
|
|
|
// calls and (optionally) made sure that enough fee was paid, we
|
|
|
@@ -960,6 +1016,10 @@ pub async fn verify_transaction(
|
|
|
}
|
|
|
debug!(target: "validator::verification::verify_transaction", "ZK proof verification successful");
|
|
|
|
|
|
+ if let Some(claimable_fee) = miner_claimable {
|
|
|
+ add_miner_claimable_fee(overlay, verifying_block_height, claimable_fee)?;
|
|
|
+ }
|
|
|
+
|
|
|
// Append hash to merkle tree
|
|
|
append_tx_to_merkle_tree(tree, tx);
|
|
|
|
|
|
@@ -976,14 +1036,37 @@ pub async fn apply_transaction(
|
|
|
block_target: u32,
|
|
|
tx: &Transaction,
|
|
|
tree: &mut MerkleTree,
|
|
|
-) -> Result<()> {
|
|
|
+ verify_fee: bool,
|
|
|
+) -> Result<GasData> {
|
|
|
let tx_hash = tx.hash();
|
|
|
debug!(target: "validator::verification::apply_transaction", "Applying transaction {tx_hash}");
|
|
|
|
|
|
+ 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))?;
|
|
|
+ }
|
|
|
+
|
|
|
+ let fee_call = extract_fee_call(&tx_hash, tx, verify_fee)?;
|
|
|
+
|
|
|
+ let mut gas_data = GasData::default();
|
|
|
+
|
|
|
// 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 tx-local state
|
|
|
let tx_local_state = Arc::new(Mutex::new(TxLocalState::new()));
|
|
|
|
|
|
@@ -991,6 +1074,26 @@ pub async fn apply_transaction(
|
|
|
for (idx, call) in tx.calls.iter().enumerate() {
|
|
|
debug!(target: "validator::verification::apply_transaction", "Executing contract call {idx}");
|
|
|
|
|
|
+ // Transaction call must contain a function code.
|
|
|
+ let Some(func) = call.data.data.first() else {
|
|
|
+ error!(target: "validator::verification::apply_transaction", "Call contains no data");
|
|
|
+ return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
|
|
|
+ };
|
|
|
+
|
|
|
+ if call.data.is_money_pow_reward() {
|
|
|
+ error!(target: "validator::verification::apply_transaction", "Reward transaction detected");
|
|
|
+ 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)
|
|
|
+ };
|
|
|
+
|
|
|
debug!(target: "validator::verification::apply_transaction", "Instantiating WASM runtime");
|
|
|
let wasm = overlay.lock().unwrap().contracts.get(call.data.contract_id)?;
|
|
|
|
|
|
@@ -1002,15 +1105,38 @@ pub async fn apply_transaction(
|
|
|
verifying_block_height,
|
|
|
block_target,
|
|
|
tx_hash,
|
|
|
- idx as u8,
|
|
|
+ call_idx,
|
|
|
)?;
|
|
|
|
|
|
+ debug!(target: "validator::verification::apply_transaction", "Executing \"metadata\" call");
|
|
|
+ let metadata = runtime.metadata(call_payload)?;
|
|
|
+
|
|
|
+ // Decode the metadata retrieved from the execution.
|
|
|
+ let mut decoder = Cursor::new(&metadata);
|
|
|
+ let zkp_pub: Vec<(String, Vec<pallas::Base>)> =
|
|
|
+ AsyncDecodable::decode_async(&mut decoder).await?;
|
|
|
+ let _: Vec<PublicKey> = AsyncDecodable::decode_async(&mut decoder).await?;
|
|
|
+
|
|
|
+ if decoder.position() != metadata.len() as u64 {
|
|
|
+ error!(
|
|
|
+ target: "validator::verification::apply_transaction",
|
|
|
+ "[VALIDATOR] Failed decoding entire metadata buffer for {tx_hash}:{idx}"
|
|
|
+ );
|
|
|
+ return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
|
|
|
+ }
|
|
|
+
|
|
|
+ for (zkas_ns, _) in &zkp_pub {
|
|
|
+ let (zkbin, _) =
|
|
|
+ overlay.lock().unwrap().contracts.get_zkas(&call.data.contract_id, zkas_ns)?;
|
|
|
+ circuits_to_verify.push(zkbin);
|
|
|
+ }
|
|
|
+
|
|
|
// 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)?);
|
|
|
+ let mut state_update = vec![*func];
|
|
|
+ state_update.append(&mut runtime.exec(call_payload)?);
|
|
|
debug!(target: "validator::verification::apply_transaction", "Successfully executed \"exec\" call");
|
|
|
|
|
|
// If that was successful, we apply the state update in the
|
|
|
@@ -1038,24 +1164,61 @@ pub async fn apply_transaction(
|
|
|
verifying_block_height,
|
|
|
block_target,
|
|
|
tx_hash,
|
|
|
- idx as u8,
|
|
|
+ call_idx,
|
|
|
)?;
|
|
|
|
|
|
deploy_runtime.deploy(&deploy_params.ix)?;
|
|
|
+
|
|
|
+ let deploy_gas_used = deploy_runtime.gas_used();
|
|
|
+ debug!(target: "validator::verification::apply_transaction", "The gas used for deployment call {call:?} of transaction {tx_hash}: {deploy_gas_used}");
|
|
|
+ gas_data.deployments = gas_data.deployments.saturating_add(deploy_gas_used);
|
|
|
}
|
|
|
+
|
|
|
+ let wasm_gas_used = runtime.gas_used();
|
|
|
+ debug!(target: "validator::verification::apply_transaction", "The gas used for WASM call {call:?} of transaction {tx_hash}: {wasm_gas_used}");
|
|
|
+ gas_data.wasm = gas_data.wasm.saturating_add(wasm_gas_used);
|
|
|
+ }
|
|
|
+
|
|
|
+ gas_data.signatures = PALLAS_SCHNORR_VERIFY_GAS
|
|
|
+ .saturating_mul(tx.signatures.len() as u64)
|
|
|
+ .saturating_add(serialize_async(tx).await.len() as u64);
|
|
|
+ debug!(target: "validator::verification::apply_transaction", "The gas used for signature of transaction {tx_hash}: {}", gas_data.signatures);
|
|
|
+
|
|
|
+ for zkbin in circuits_to_verify.iter() {
|
|
|
+ let zk_circuit_gas_used = circuit_gas_use(zkbin);
|
|
|
+ debug!(target: "validator::verification::apply_transaction", "The gas used for ZK circuit in namespace {} of transaction {tx_hash}: {zk_circuit_gas_used}", zkbin.namespace);
|
|
|
+ gas_data.zk_circuits = gas_data.zk_circuits.saturating_add(zk_circuit_gas_used);
|
|
|
+ }
|
|
|
+
|
|
|
+ let total_gas_used = gas_data.total_gas_used();
|
|
|
+
|
|
|
+ if total_gas_used > TX_GAS_LIMIT {
|
|
|
+ error!(
|
|
|
+ target: "validator::verification::apply_transaction",
|
|
|
+ "[VALIDATOR] Transaction {tx_hash} exceeds TX_GAS_LIMIT: \
|
|
|
+ {total_gas_used} > {TX_GAS_LIMIT}"
|
|
|
+ );
|
|
|
+ return Err(TxVerifyFailed::GasLimitExceeded.into())
|
|
|
+ }
|
|
|
+
|
|
|
+ if let Some((_, fee)) = fee_call {
|
|
|
+ let claimable_fee = calculate_miner_claimable_fee(&tx_hash, total_gas_used, fee)?;
|
|
|
+ gas_data.paid = fee;
|
|
|
+ gas_data.miner_claimable = claimable_fee;
|
|
|
+ add_miner_claimable_fee(overlay, verifying_block_height, claimable_fee)?;
|
|
|
}
|
|
|
|
|
|
// Append hash to merkle tree
|
|
|
append_tx_to_merkle_tree(tree, tx);
|
|
|
|
|
|
debug!(target: "validator::verification::apply_transaction", "Transaction {tx_hash} applied successfully");
|
|
|
- Ok(())
|
|
|
+ Ok(gas_data)
|
|
|
}
|
|
|
|
|
|
/// 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
|
|
|
+/// the function will return the total gas used and total miner-claimable fees
|
|
|
/// from all the transactions. Additionally, their hash is appended to
|
|
|
/// the provided Merkle tree.
|
|
|
///
|
|
|
@@ -1079,7 +1242,7 @@ pub async fn verify_transactions(
|
|
|
|
|
|
// Total gas accumulators
|
|
|
let mut total_gas_used = 0_u64;
|
|
|
- let mut total_gas_paid = 0_u64;
|
|
|
+ let mut total_miner_claimable = 0_u64;
|
|
|
|
|
|
// Map of ZK proof verifying keys for the current transaction batch
|
|
|
let mut vks: HashMap<[u8; 32], HashMap<String, VerifyingKey>> = HashMap::new();
|
|
|
@@ -1135,14 +1298,14 @@ pub async fn verify_transactions(
|
|
|
|
|
|
// Update accumulated total gas
|
|
|
total_gas_used = total_gas_used.saturating_add(tx_gas_used);
|
|
|
- total_gas_paid = total_gas_paid.saturating_add(gas_data.paid);
|
|
|
+ total_miner_claimable = accumulate_fee(total_miner_claimable, gas_data.miner_claimable)?;
|
|
|
}
|
|
|
|
|
|
if !erroneous_txs.is_empty() {
|
|
|
return Err(TxVerifyFailed::ErroneousTxs(erroneous_txs).into())
|
|
|
}
|
|
|
|
|
|
- Ok((total_gas_used, total_gas_paid))
|
|
|
+ Ok((total_gas_used, total_miner_claimable))
|
|
|
}
|
|
|
|
|
|
/// Apply given set of [`Transaction`] in sequence, without formal
|
|
|
@@ -1155,34 +1318,64 @@ async fn apply_transactions(
|
|
|
block_target: u32,
|
|
|
txs: &[Transaction],
|
|
|
tree: &mut MerkleTree,
|
|
|
-) -> Result<()> {
|
|
|
+ verify_fees: bool,
|
|
|
+) -> Result<(u64, u64)> {
|
|
|
debug!(target: "validator::verification::apply_transactions", "Applying {} transactions", txs.len());
|
|
|
if txs.is_empty() {
|
|
|
- return Ok(())
|
|
|
+ return Ok((0, 0))
|
|
|
}
|
|
|
|
|
|
// Tracker for failed txs
|
|
|
let mut erroneous_txs = vec![];
|
|
|
+ let mut total_gas_used = 0_u64;
|
|
|
+ let mut total_miner_claimable = 0_u64;
|
|
|
|
|
|
// 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
|
|
|
+ let gas_data = match apply_transaction(
|
|
|
+ overlay,
|
|
|
+ verifying_block_height,
|
|
|
+ block_target,
|
|
|
+ tx,
|
|
|
+ tree,
|
|
|
+ verify_fees,
|
|
|
+ )
|
|
|
+ .await
|
|
|
{
|
|
|
- warn!(target: "validator::verification::apply_transactions", "Transaction apply failed: {e}");
|
|
|
+ Ok(gas_values) => gas_values,
|
|
|
+ Err(e) => {
|
|
|
+ warn!(target: "validator::verification::apply_transactions", "Transaction apply failed: {e}");
|
|
|
+ erroneous_txs.push(tx.clone());
|
|
|
+ overlay.lock().unwrap().revert_to_checkpoint();
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ let tx_gas_used = gas_data.total_gas_used();
|
|
|
+ let accumulated_gas_usage = total_gas_used.saturating_add(tx_gas_used);
|
|
|
+ if accumulated_gas_usage > BLOCK_GAS_LIMIT {
|
|
|
+ warn!(
|
|
|
+ target: "validator::verification::apply_transactions",
|
|
|
+ "Transaction {} exceeds configured transaction gas limit: \
|
|
|
+ {accumulated_gas_usage} - {BLOCK_GAS_LIMIT}",
|
|
|
+ tx.hash()
|
|
|
+ );
|
|
|
erroneous_txs.push(tx.clone());
|
|
|
overlay.lock().unwrap().revert_to_checkpoint();
|
|
|
- };
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ total_gas_used = total_gas_used.saturating_add(tx_gas_used);
|
|
|
+ total_miner_claimable = accumulate_fee(total_miner_claimable, gas_data.miner_claimable)?;
|
|
|
}
|
|
|
|
|
|
if !erroneous_txs.is_empty() {
|
|
|
return Err(TxVerifyFailed::ErroneousTxs(erroneous_txs).into())
|
|
|
}
|
|
|
|
|
|
- Ok(())
|
|
|
+ Ok((total_gas_used, total_miner_claimable))
|
|
|
}
|
|
|
-
|
|
|
/// Verify given [`Proposal`] against provided consensus state.
|
|
|
///
|
|
|
/// A proposal is considered valid when the following rules apply:
|
|
|
@@ -1279,3 +1472,88 @@ pub async fn verify_fork_proposal(
|
|
|
|
|
|
Ok(())
|
|
|
}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+mod tests {
|
|
|
+ use darkfi_sdk::{
|
|
|
+ crypto::{DAO_CONTRACT_ID, MONEY_CONTRACT_ID},
|
|
|
+ dark_tree::DarkLeaf,
|
|
|
+ fee::{burn_fee, minimum_fee},
|
|
|
+ tx::ContractCall,
|
|
|
+ };
|
|
|
+ use darkfi_serial::serialize;
|
|
|
+
|
|
|
+ use super::*;
|
|
|
+
|
|
|
+ fn leaf(call: ContractCall) -> DarkLeaf<ContractCall> {
|
|
|
+ DarkLeaf { data: call, parent_index: None, children_indexes: vec![] }
|
|
|
+ }
|
|
|
+
|
|
|
+ fn fee_call(fee: u64) -> DarkLeaf<ContractCall> {
|
|
|
+ let mut data = vec![0x00];
|
|
|
+ data.extend(serialize(&fee));
|
|
|
+ leaf(ContractCall { contract_id: *MONEY_CONTRACT_ID, data })
|
|
|
+ }
|
|
|
+
|
|
|
+ fn short_fee_call() -> DarkLeaf<ContractCall> {
|
|
|
+ leaf(ContractCall { contract_id: *MONEY_CONTRACT_ID, data: vec![0x00] })
|
|
|
+ }
|
|
|
+
|
|
|
+ fn non_fee_call() -> DarkLeaf<ContractCall> {
|
|
|
+ leaf(ContractCall { contract_id: *DAO_CONTRACT_ID, data: vec![0x00] })
|
|
|
+ }
|
|
|
+
|
|
|
+ fn tx(calls: Vec<DarkLeaf<ContractCall>>) -> Transaction {
|
|
|
+ Transaction { calls, proofs: vec![], signatures: vec![] }
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn fee_call_extraction_is_disabled_when_fee_checks_are_disabled() {
|
|
|
+ let tx = tx(vec![]);
|
|
|
+ assert_eq!(extract_fee_call(&tx.hash(), &tx, false).unwrap(), None);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn fee_call_extraction_allows_one_fee_call_with_other_calls() {
|
|
|
+ let tx = tx(vec![non_fee_call(), fee_call(42)]);
|
|
|
+
|
|
|
+ assert_eq!(extract_fee_call(&tx.hash(), &tx, true).unwrap(), Some((1, 42)));
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn fee_call_extraction_rejects_missing_duplicate_malformed_and_zero_fee() {
|
|
|
+ let missing = tx(vec![non_fee_call()]);
|
|
|
+ assert!(extract_fee_call(&missing.hash(), &missing, true).is_err());
|
|
|
+
|
|
|
+ let duplicate = tx(vec![fee_call(1), fee_call(2)]);
|
|
|
+ assert!(extract_fee_call(&duplicate.hash(), &duplicate, true).is_err());
|
|
|
+
|
|
|
+ let malformed = tx(vec![short_fee_call()]);
|
|
|
+ assert!(extract_fee_call(&malformed.hash(), &malformed, true).is_err());
|
|
|
+
|
|
|
+ let zero = tx(vec![fee_call(0)]);
|
|
|
+ assert!(extract_fee_call(&zero.hash(), &zero, true).is_err());
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn miner_claimable_fee_split_handles_exact_fee_and_tip() {
|
|
|
+ let tx_hash = TransactionHash::none();
|
|
|
+ let gas = 10;
|
|
|
+ let required = minimum_fee(gas).unwrap();
|
|
|
+ let burned = burn_fee(required).unwrap();
|
|
|
+
|
|
|
+ assert_eq!(required, 50);
|
|
|
+ assert_eq!(burned, 37);
|
|
|
+ assert_eq!(calculate_miner_claimable_fee(&tx_hash, gas, required).unwrap(), 13);
|
|
|
+ assert_eq!(calculate_miner_claimable_fee(&tx_hash, gas, required + 7).unwrap(), 20);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn miner_claimable_fee_split_rejects_insufficient_fee() {
|
|
|
+ let tx_hash = TransactionHash::none();
|
|
|
+ let gas = 10;
|
|
|
+ let required = minimum_fee(gas).unwrap();
|
|
|
+
|
|
|
+ assert!(calculate_miner_claimable_fee(&tx_hash, gas, required - 1).is_err());
|
|
|
+ }
|
|
|
+}
|