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}