zebra_state/service/check/
utxo.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
//! Consensus rule checks for the finalized state.

use std::collections::{HashMap, HashSet};

use zebra_chain::{
    amount,
    transparent::{self, utxos_from_ordered_utxos, CoinbaseSpendRestriction::*},
};

use crate::{
    constants::MIN_TRANSPARENT_COINBASE_MATURITY,
    service::finalized_state::ZebraDb,
    SemanticallyVerifiedBlock,
    ValidateContextError::{
        self, DuplicateTransparentSpend, EarlyTransparentSpend, ImmatureTransparentCoinbaseSpend,
        MissingTransparentOutput, UnshieldedTransparentCoinbaseSpend,
    },
};

/// Lookup all the [`transparent::Utxo`]s spent by a [`SemanticallyVerifiedBlock`].
/// 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(
    semantically_verified: &SemanticallyVerifiedBlock,
    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
        semantically_verified.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,
                &semantically_verified.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(
                &finalized_state.network(),
                semantically_verified.height,
            );
            transparent_coinbase_spend(spend, spend_restriction, utxo.as_ref())?;

            // 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(semantically_verified, &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",
        });
    }

    non_finalized_chain_unspent_utxos
        .get(&spend)
        .cloned()
        .or_else(|| finalized_state.utxo(&spend))
        // 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)
        .ok_or(MissingTransparentOutput {
            outpoint: spend,
            location: "the non-finalized and finalized chain",
        })
}

/// 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::Utxo,
) -> Result<(), ValidateContextError> {
    if !utxo.from_coinbase {
        return Ok(());
    }

    match spend_restriction {
        CheckCoinbaseMaturity { spend_height } => {
            let min_spend_height = utxo.height + MIN_TRANSPARENT_COINBASE_MATURITY.into();
            let min_spend_height =
                min_spend_height.expect("valid UTXOs have coinbase heights far below Height::MAX");
            if spend_height >= min_spend_height {
                Ok(())
            } else {
                Err(ImmatureTransparentCoinbaseSpend {
                    outpoint,
                    spend_height,
                    min_spend_height,
                    created_height: utxo.height,
                })
            }
        }
        DisallowCoinbaseSpend => 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(
    semantically_verified: &SemanticallyVerifiedBlock,
    utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
) -> Result<(), ValidateContextError> {
    for (tx_index_in_block, transaction) in
        semantically_verified.block.transactions.iter().enumerate()
    {
        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: semantically_verified.height,
                        tx_index_in_block,
                        transaction_hash: semantically_verified.transaction_hashes
                            [tx_index_in_block],
                    })
                }
                Err(amount_error) => {
                    Err(ValidateContextError::CalculateRemainingTransactionValue {
                        amount_error,
                        height: semantically_verified.height,
                        tx_index_in_block,
                        transaction_hash: semantically_verified.transaction_hashes
                            [tx_index_in_block],
                    })
                }
            },
            Err(value_balance_error) => {
                Err(ValidateContextError::CalculateTransactionValueBalances {
                    value_balance_error,
                    height: semantically_verified.height,
                    tx_index_in_block,
                    transaction_hash: semantically_verified.transaction_hashes[tx_index_in_block],
                })
            }
        }?
    }

    Ok(())
}