Skip to main content

zebra_state/service/check/
utxo.rs

1//! Consensus rule checks for the finalized state.
2
3use std::collections::HashMap;
4
5use zebra_chain::{
6    amount,
7    transparent::{self, utxos_from_ordered_utxos, CoinbaseSpendRestriction::*},
8};
9
10use crate::{
11    constants::MIN_TRANSPARENT_COINBASE_MATURITY,
12    service::{finalized_state::ZebraDb, non_finalized_state::SpendingTransactionId},
13    SemanticallyVerifiedBlock,
14    ValidateContextError::{
15        self, DuplicateTransparentSpend, EarlyTransparentSpend, ImmatureTransparentCoinbaseSpend,
16        MissingTransparentOutput, UnshieldedTransparentCoinbaseSpend,
17    },
18};
19
20/// Lookup all the [`transparent::Utxo`]s spent by a [`SemanticallyVerifiedBlock`].
21/// If any of the spends are invalid, return an error.
22/// Otherwise, return the looked up UTXOs.
23///
24/// Checks for the following kinds of invalid spends:
25///
26/// Double-spends:
27/// - duplicate spends that are both in this block,
28/// - spends of an output that was spent by a previous block,
29///
30/// Missing spends:
31/// - spends of an output that hasn't been created yet,
32///   (in linear chain and transaction order),
33/// - spends of UTXOs that were never created in this chain,
34///
35/// Invalid spends:
36/// - spends of an immature transparent coinbase output,
37/// - unshielded spends of a transparent coinbase output.
38pub fn transparent_spend(
39    semantically_verified: &SemanticallyVerifiedBlock,
40    non_finalized_chain_unspent_utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
41    non_finalized_chain_spent_utxos: &HashMap<transparent::OutPoint, SpendingTransactionId>,
42    finalized_state: &ZebraDb,
43) -> Result<HashMap<transparent::OutPoint, transparent::OrderedUtxo>, ValidateContextError> {
44    let mut block_spends = HashMap::new();
45
46    for (spend_tx_index_in_block, transaction) in
47        semantically_verified.block.transactions.iter().enumerate()
48    {
49        // Coinbase inputs represent new coins,
50        // so there are no UTXOs to mark as spent.
51        let inputs = transaction.inputs();
52        let spends = inputs.iter().filter_map(transparent::Input::outpoint);
53
54        for spend in spends {
55            let utxo = transparent_spend_chain_order(
56                spend,
57                spend_tx_index_in_block,
58                &semantically_verified.new_outputs,
59                non_finalized_chain_unspent_utxos,
60                non_finalized_chain_spent_utxos,
61                finalized_state,
62            )?;
63
64            // The state service returns UTXOs from pending blocks,
65            // which can be rejected by later contextual checks.
66            // This is a particular issue for v5 transactions,
67            // because their authorizing data is only bound to the block data
68            // during contextual validation (#2336).
69            //
70            // We don't want to use UTXOs from invalid pending blocks,
71            // so we check transparent coinbase maturity and shielding
72            // using known valid UTXOs during non-finalized chain validation.
73            let spend_restriction = transaction.coinbase_spend_restriction(
74                &finalized_state.network(),
75                semantically_verified.height,
76            );
77            transparent_coinbase_spend(spend, spend_restriction, utxo.as_ref())?;
78
79            // We don't delete the UTXOs until the block is committed,
80            // so we  need to check for duplicate spends within the same block.
81            //
82            // See `transparent_spend_chain_order` for the relevant consensus rule.
83            if block_spends.insert(spend, utxo).is_some() {
84                return Err(DuplicateTransparentSpend {
85                    outpoint: spend,
86                    location: "the same block",
87                });
88            }
89        }
90    }
91
92    remaining_transaction_value(semantically_verified, &block_spends)?;
93
94    Ok(block_spends)
95}
96
97/// Check that transparent spends occur in chain order.
98///
99/// Because we are in the non-finalized state, we need to check spends within the same block,
100/// spent non-finalized UTXOs, and unspent non-finalized and finalized UTXOs.
101///
102/// "Any input within this block can spend an output which also appears in this block
103/// (assuming the spend is otherwise valid).
104/// However, the TXID corresponding to the output must be placed at some point
105/// before the TXID corresponding to the input.
106/// This ensures that any program parsing block chain transactions linearly
107/// will encounter each output before it is used as an input."
108///
109/// <https://developer.bitcoin.org/reference/block_chain.html#merkle-trees>
110///
111/// "each output of a particular transaction
112/// can only be used as an input once in the block chain.
113/// Any subsequent reference is a forbidden double spend-
114/// an attempt to spend the same satoshis twice."
115///
116/// <https://developer.bitcoin.org/devguide/block_chain.html#introduction>
117///
118/// # Consensus
119///
120/// > Every non-null prevout MUST point to a unique UTXO in either a preceding block,
121/// > or a previous transaction in the same block.
122///
123/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
124fn transparent_spend_chain_order(
125    spend: transparent::OutPoint,
126    spend_tx_index_in_block: usize,
127    block_new_outputs: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
128    non_finalized_chain_unspent_utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
129    non_finalized_chain_spent_utxos: &HashMap<transparent::OutPoint, SpendingTransactionId>,
130    finalized_state: &ZebraDb,
131) -> Result<transparent::OrderedUtxo, ValidateContextError> {
132    if let Some(output) = block_new_outputs.get(&spend) {
133        // reject the spend if it uses an output from this block,
134        // but the output was not created by an earlier transaction
135        //
136        // we know the spend is invalid, because transaction IDs are unique
137        //
138        // (transaction IDs also commit to transaction inputs,
139        // so it should be cryptographically impossible for a transaction
140        // to spend its own outputs)
141        if output.tx_index_in_block >= spend_tx_index_in_block {
142            return Err(EarlyTransparentSpend { outpoint: spend });
143        } else {
144            // a unique spend of a previous transaction's output is ok
145            return Ok(output.clone());
146        }
147    }
148
149    if non_finalized_chain_spent_utxos.contains_key(&spend) {
150        // reject the spend if its UTXO is already spent in the
151        // non-finalized parent chain
152        return Err(DuplicateTransparentSpend {
153            outpoint: spend,
154            location: "the non-finalized chain",
155        });
156    }
157
158    non_finalized_chain_unspent_utxos
159        .get(&spend)
160        .cloned()
161        .or_else(|| finalized_state.utxo(&spend))
162        // we don't keep spent UTXOs in the finalized state,
163        // so all we can say is that it's missing from both
164        // the finalized and non-finalized chains
165        // (it might have been spent in the finalized state,
166        // or it might never have existed in this chain)
167        .ok_or(MissingTransparentOutput {
168            outpoint: spend,
169            location: "the non-finalized and finalized chain",
170        })
171}
172
173/// Check that `utxo` is spendable, based on the coinbase `spend_restriction`.
174///
175/// # Consensus
176///
177/// > A transaction with one or more transparent inputs from coinbase transactions
178/// > MUST have no transparent outputs (i.e. tx_out_count MUST be 0).
179/// > Inputs from coinbase transactions include Founders’ Reward outputs and
180/// > funding stream outputs.
181///
182/// > A transaction MUST NOT spend a transparent output of a coinbase transaction
183/// > from a block less than 100 blocks prior to the spend.
184/// > Note that transparent outputs of coinbase transactions include
185/// > Founders’ Reward outputs and transparent funding stream outputs.
186///
187/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
188pub fn transparent_coinbase_spend(
189    outpoint: transparent::OutPoint,
190    spend_restriction: transparent::CoinbaseSpendRestriction,
191    utxo: &transparent::Utxo,
192) -> Result<(), ValidateContextError> {
193    if !utxo.from_coinbase {
194        return Ok(());
195    }
196
197    match spend_restriction {
198        CheckCoinbaseMaturity { spend_height } => {
199            let min_spend_height = utxo.height + MIN_TRANSPARENT_COINBASE_MATURITY.into();
200            let min_spend_height =
201                min_spend_height.expect("valid UTXOs have coinbase heights far below Height::MAX");
202            if spend_height >= min_spend_height {
203                Ok(())
204            } else {
205                Err(ImmatureTransparentCoinbaseSpend {
206                    outpoint,
207                    spend_height,
208                    min_spend_height,
209                    created_height: utxo.height,
210                })
211            }
212        }
213        DisallowCoinbaseSpend => Err(UnshieldedTransparentCoinbaseSpend { outpoint }),
214    }
215}
216
217/// Reject negative remaining transaction value.
218///
219/// "As in Bitcoin, the remaining value in the transparent transaction value pool
220/// of a non-coinbase transaction is available to miners as a fee.
221///
222/// The remaining value in the transparent transaction value pool of a
223/// coinbase transaction is destroyed.
224///
225/// Consensus rule: The remaining value in the transparent transaction value pool
226/// MUST be nonnegative."
227///
228/// <https://zips.z.cash/protocol/protocol.pdf#transactions>
229pub fn remaining_transaction_value(
230    semantically_verified: &SemanticallyVerifiedBlock,
231    utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
232) -> Result<(), ValidateContextError> {
233    let utxos = utxos_from_ordered_utxos(
234        utxos
235            .iter()
236            .map(|(outpoint, utxo)| (*outpoint, utxo.clone())),
237    );
238
239    for (tx_index_in_block, transaction) in
240        semantically_verified.block.transactions.iter().enumerate()
241    {
242        if transaction.is_coinbase() {
243            continue;
244        }
245
246        // Check the remaining transparent value pool for this transaction
247        let value_balance = transaction.value_balance(&utxos);
248        match value_balance {
249            Ok(vb) => match vb.remaining_transaction_value() {
250                Ok(_) => Ok(()),
251                Err(amount_error @ amount::Error::Constraint { .. })
252                    if amount_error.invalid_value() < 0 =>
253                {
254                    Err(ValidateContextError::NegativeRemainingTransactionValue {
255                        amount_error,
256                        height: semantically_verified.height,
257                        tx_index_in_block,
258                        transaction_hash: semantically_verified.transaction_hashes
259                            [tx_index_in_block],
260                    })
261                }
262                Err(amount_error) => {
263                    Err(ValidateContextError::CalculateRemainingTransactionValue {
264                        amount_error,
265                        height: semantically_verified.height,
266                        tx_index_in_block,
267                        transaction_hash: semantically_verified.transaction_hashes
268                            [tx_index_in_block],
269                    })
270                }
271            },
272            Err(value_balance_error) => {
273                Err(ValidateContextError::CalculateTransactionValueBalances {
274                    value_balance_error,
275                    height: semantically_verified.height,
276                    tx_index_in_block,
277                    transaction_hash: semantically_verified.transaction_hashes[tx_index_in_block],
278                })
279            }
280        }?
281    }
282
283    Ok(())
284}