280 lines
11 KiB
Rust
280 lines
11 KiB
Rust
//! Consensus rule checks for the finalized state.
|
||
|
||
use std::collections::{HashMap, HashSet};
|
||
|
||
use zebra_chain::{
|
||
amount, block,
|
||
transparent::{self, utxos_from_ordered_utxos, CoinbaseSpendRestriction::*},
|
||
};
|
||
|
||
use crate::{
|
||
constants::MIN_TRANSPARENT_COINBASE_MATURITY,
|
||
service::finalized_state::ZebraDb,
|
||
PreparedBlock,
|
||
ValidateContextError::{
|
||
self, DuplicateTransparentSpend, EarlyTransparentSpend, ImmatureTransparentCoinbaseSpend,
|
||
MissingTransparentOutput, UnshieldedTransparentCoinbaseSpend,
|
||
},
|
||
};
|
||
|
||
/// Lookup all the [`transparent::Utxo`]s spent by a [`PreparedBlock`].
|
||
/// If any of the spends are invalid, return an error.
|
||
/// Otherwise, return the looked up UTXOs.
|
||
///
|
||
/// Checks for the following kinds of invalid spends:
|
||
///
|
||
/// Double-spends:
|
||
/// - duplicate spends that are both in this block,
|
||
/// - spends of an output that was spent by a previous block,
|
||
///
|
||
/// Missing spends:
|
||
/// - spends of an output that hasn't been created yet,
|
||
/// (in linear chain and transaction order),
|
||
/// - spends of UTXOs that were never created in this chain,
|
||
///
|
||
/// Invalid spends:
|
||
/// - spends of an immature transparent coinbase output,
|
||
/// - unshielded spends of a transparent coinbase output.
|
||
pub fn transparent_spend(
|
||
prepared: &PreparedBlock,
|
||
non_finalized_chain_unspent_utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||
non_finalized_chain_spent_utxos: &HashSet<transparent::OutPoint>,
|
||
finalized_state: &ZebraDb,
|
||
) -> Result<HashMap<transparent::OutPoint, transparent::OrderedUtxo>, ValidateContextError> {
|
||
let mut block_spends = HashMap::new();
|
||
|
||
for (spend_tx_index_in_block, transaction) in prepared.block.transactions.iter().enumerate() {
|
||
// Coinbase inputs represent new coins,
|
||
// so there are no UTXOs to mark as spent.
|
||
let spends = transaction
|
||
.inputs()
|
||
.iter()
|
||
.filter_map(transparent::Input::outpoint);
|
||
|
||
for spend in spends {
|
||
let utxo = transparent_spend_chain_order(
|
||
spend,
|
||
spend_tx_index_in_block,
|
||
&prepared.new_outputs,
|
||
non_finalized_chain_unspent_utxos,
|
||
non_finalized_chain_spent_utxos,
|
||
finalized_state,
|
||
)?;
|
||
|
||
// The state service returns UTXOs from pending blocks,
|
||
// which can be rejected by later contextual checks.
|
||
// This is a particular issue for v5 transactions,
|
||
// because their authorizing data is only bound to the block data
|
||
// during contextual validation (#2336).
|
||
//
|
||
// We don't want to use UTXOs from invalid pending blocks,
|
||
// so we check transparent coinbase maturity and shielding
|
||
// using known valid UTXOs during non-finalized chain validation.
|
||
let spend_restriction = transaction.coinbase_spend_restriction(prepared.height);
|
||
let utxo = transparent_coinbase_spend(spend, spend_restriction, utxo)?;
|
||
|
||
// We don't delete the UTXOs until the block is committed,
|
||
// so we need to check for duplicate spends within the same block.
|
||
//
|
||
// See `transparent_spend_chain_order` for the relevant consensus rule.
|
||
if block_spends.insert(spend, utxo).is_some() {
|
||
return Err(DuplicateTransparentSpend {
|
||
outpoint: spend,
|
||
location: "the same block",
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
remaining_transaction_value(prepared, &block_spends)?;
|
||
|
||
Ok(block_spends)
|
||
}
|
||
|
||
/// Check that transparent spends occur in chain order.
|
||
///
|
||
/// Because we are in the non-finalized state, we need to check spends within the same block,
|
||
/// spent non-finalized UTXOs, and unspent non-finalized and finalized UTXOs.
|
||
///
|
||
/// "Any input within this block can spend an output which also appears in this block
|
||
/// (assuming the spend is otherwise valid).
|
||
/// However, the TXID corresponding to the output must be placed at some point
|
||
/// before the TXID corresponding to the input.
|
||
/// This ensures that any program parsing block chain transactions linearly
|
||
/// will encounter each output before it is used as an input."
|
||
///
|
||
/// <https://developer.bitcoin.org/reference/block_chain.html#merkle-trees>
|
||
///
|
||
/// "each output of a particular transaction
|
||
/// can only be used as an input once in the block chain.
|
||
/// Any subsequent reference is a forbidden double spend-
|
||
/// an attempt to spend the same satoshis twice."
|
||
///
|
||
/// <https://developer.bitcoin.org/devguide/block_chain.html#introduction>
|
||
///
|
||
/// # Consensus
|
||
///
|
||
/// > Every non-null prevout MUST point to a unique UTXO in either a preceding block,
|
||
/// > or a previous transaction in the same block.
|
||
///
|
||
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
|
||
fn transparent_spend_chain_order(
|
||
spend: transparent::OutPoint,
|
||
spend_tx_index_in_block: usize,
|
||
block_new_outputs: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||
non_finalized_chain_unspent_utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||
non_finalized_chain_spent_utxos: &HashSet<transparent::OutPoint>,
|
||
finalized_state: &ZebraDb,
|
||
) -> Result<transparent::OrderedUtxo, ValidateContextError> {
|
||
if let Some(output) = block_new_outputs.get(&spend) {
|
||
// reject the spend if it uses an output from this block,
|
||
// but the output was not created by an earlier transaction
|
||
//
|
||
// we know the spend is invalid, because transaction IDs are unique
|
||
//
|
||
// (transaction IDs also commit to transaction inputs,
|
||
// so it should be cryptographically impossible for a transaction
|
||
// to spend its own outputs)
|
||
if output.tx_index_in_block >= spend_tx_index_in_block {
|
||
return Err(EarlyTransparentSpend { outpoint: spend });
|
||
} else {
|
||
// a unique spend of a previous transaction's output is ok
|
||
return Ok(output.clone());
|
||
}
|
||
}
|
||
|
||
if non_finalized_chain_spent_utxos.contains(&spend) {
|
||
// reject the spend if its UTXO is already spent in the
|
||
// non-finalized parent chain
|
||
return Err(DuplicateTransparentSpend {
|
||
outpoint: spend,
|
||
location: "the non-finalized chain",
|
||
});
|
||
}
|
||
|
||
match (
|
||
non_finalized_chain_unspent_utxos.get(&spend),
|
||
finalized_state.utxo(&spend),
|
||
) {
|
||
(None, None) => {
|
||
// we don't keep spent UTXOs in the finalized state,
|
||
// so all we can say is that it's missing from both
|
||
// the finalized and non-finalized chains
|
||
// (it might have been spent in the finalized state,
|
||
// or it might never have existed in this chain)
|
||
Err(MissingTransparentOutput {
|
||
outpoint: spend,
|
||
location: "the non-finalized and finalized chain",
|
||
})
|
||
}
|
||
|
||
(Some(utxo), _) => Ok(utxo.clone()),
|
||
(_, Some(utxo)) => Ok(utxo),
|
||
}
|
||
}
|
||
|
||
/// Check that `utxo` is spendable, based on the coinbase `spend_restriction`.
|
||
///
|
||
/// # Consensus
|
||
///
|
||
/// > A transaction with one or more transparent inputs from coinbase transactions
|
||
/// > MUST have no transparent outputs (i.e. tx_out_count MUST be 0).
|
||
/// > Inputs from coinbase transactions include Founders’ Reward outputs and
|
||
/// > funding stream outputs.
|
||
///
|
||
/// > A transaction MUST NOT spend a transparent output of a coinbase transaction
|
||
/// > from a block less than 100 blocks prior to the spend.
|
||
/// > Note that transparent outputs of coinbase transactions include
|
||
/// > Founders’ Reward outputs and transparent funding stream outputs.
|
||
///
|
||
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
|
||
pub fn transparent_coinbase_spend(
|
||
outpoint: transparent::OutPoint,
|
||
spend_restriction: transparent::CoinbaseSpendRestriction,
|
||
utxo: transparent::OrderedUtxo,
|
||
) -> Result<transparent::OrderedUtxo, ValidateContextError> {
|
||
if !utxo.utxo.from_coinbase {
|
||
return Ok(utxo);
|
||
}
|
||
|
||
match spend_restriction {
|
||
OnlyShieldedOutputs { spend_height } => {
|
||
let min_spend_height =
|
||
utxo.utxo.height + block::Height(MIN_TRANSPARENT_COINBASE_MATURITY);
|
||
let min_spend_height =
|
||
min_spend_height.expect("valid UTXOs have coinbase heights far below Height::MAX");
|
||
if spend_height >= min_spend_height {
|
||
Ok(utxo)
|
||
} else {
|
||
Err(ImmatureTransparentCoinbaseSpend {
|
||
outpoint,
|
||
spend_height,
|
||
min_spend_height,
|
||
created_height: utxo.utxo.height,
|
||
})
|
||
}
|
||
}
|
||
SomeTransparentOutputs => Err(UnshieldedTransparentCoinbaseSpend { outpoint }),
|
||
}
|
||
}
|
||
|
||
/// Reject negative remaining transaction value.
|
||
///
|
||
/// "As in Bitcoin, the remaining value in the transparent transaction value pool
|
||
/// of a non-coinbase transaction is available to miners as a fee.
|
||
///
|
||
/// The remaining value in the transparent transaction value pool of a
|
||
/// coinbase transaction is destroyed.
|
||
///
|
||
/// Consensus rule: The remaining value in the transparent transaction value pool
|
||
/// MUST be nonnegative."
|
||
///
|
||
/// <https://zips.z.cash/protocol/protocol.pdf#transactions>
|
||
pub fn remaining_transaction_value(
|
||
prepared: &PreparedBlock,
|
||
utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||
) -> Result<(), ValidateContextError> {
|
||
for (tx_index_in_block, transaction) in prepared.block.transactions.iter().enumerate() {
|
||
// TODO: check coinbase transaction remaining value (#338, #1162)
|
||
if transaction.is_coinbase() {
|
||
continue;
|
||
}
|
||
|
||
// Check the remaining transparent value pool for this transaction
|
||
let value_balance = transaction.value_balance(&utxos_from_ordered_utxos(utxos.clone()));
|
||
match value_balance {
|
||
Ok(vb) => match vb.remaining_transaction_value() {
|
||
Ok(_) => Ok(()),
|
||
Err(amount_error @ amount::Error::Constraint { .. })
|
||
if amount_error.invalid_value() < 0 =>
|
||
{
|
||
Err(ValidateContextError::NegativeRemainingTransactionValue {
|
||
amount_error,
|
||
height: prepared.height,
|
||
tx_index_in_block,
|
||
transaction_hash: prepared.transaction_hashes[tx_index_in_block],
|
||
})
|
||
}
|
||
Err(amount_error) => {
|
||
Err(ValidateContextError::CalculateRemainingTransactionValue {
|
||
amount_error,
|
||
height: prepared.height,
|
||
tx_index_in_block,
|
||
transaction_hash: prepared.transaction_hashes[tx_index_in_block],
|
||
})
|
||
}
|
||
},
|
||
Err(value_balance_error) => {
|
||
Err(ValidateContextError::CalculateTransactionValueBalances {
|
||
value_balance_error,
|
||
height: prepared.height,
|
||
tx_index_in_block,
|
||
transaction_hash: prepared.transaction_hashes[tx_index_in_block],
|
||
})
|
||
}
|
||
}?
|
||
}
|
||
|
||
Ok(())
|
||
}
|