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
//! Block subsidy, funding stream, and miner fee consensus checks.
use mset::MultiSet;
use crate::{
amount::{
Amount, DeferredPoolBalanceChange, Error as AmountError, NegativeAllowed, NonNegative,
},
block::{Block, Height},
parameters::{Network, NetworkUpgrade},
transaction::Transaction,
transparent::{Address, Output},
};
use super::{
founders_reward, founders_reward_address, funding_stream_address, funding_stream_values,
nsm_fee_contribution, FundingStreamReceiver, ParameterSubsidy, SubsidyError,
};
/// An error from the block subsidy or miner fee checks on a block's coinbase transaction.
#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum CoinbaseTransactionError {
#[error(transparent)]
Subsidy(#[from] SubsidyError),
#[error("founders reward address must be defined for height: {0:?}")]
FoundersRewardAddressNotFound(Height),
#[error("missing lockbox disbursements for NU6.1 activation block")]
MissingLockboxDisbursements,
#[error("A funding stream other than the deferred pool must have an address")]
FundingStreamAddressNotFound,
#[error("invalid transparent value balance: {0}")]
InvalidTransparentValueBalance(AmountError),
}
impl From<AmountError> for CoinbaseTransactionError {
fn from(error: AmountError) -> Self {
Self::Subsidy(error.into())
}
}
/// Returns `Ok()` with the deferred pool balance change of the coinbase transaction if the block
/// subsidy in `block` is valid for `network`
///
/// [3.9]: https://zips.z.cash/protocol/protocol.pdf#subsidyconcepts
pub fn subsidy_is_valid(
block: &Block,
net: &Network,
expected_block_subsidy: Amount<NonNegative>,
) -> Result<DeferredPoolBalanceChange, CoinbaseTransactionError> {
let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?;
// A zero subsidy removes proportional payouts, but not fixed lockbox disbursements.
if expected_block_subsidy.is_zero()
&& Some(height) != NetworkUpgrade::Nu6_1.activation_height(net)
{
return Ok(DeferredPoolBalanceChange::zero());
}
let mut coinbase_outputs: MultiSet<Output> = block
.transactions
.first()
.ok_or(SubsidyError::NoCoinbase)?
.outputs()
.iter()
.cloned()
.collect();
let mut has_amount =
|addr: &Address, amount| coinbase_outputs.remove(&Output::new(amount, addr.script()));
// # Note
//
// Canopy activation is at the first halving on Mainnet, but not on Testnet. [ZIP-1014] only
// applies to Mainnet; [ZIP-214] contains the specific rules for Testnet funding stream amount
// values.
//
// [ZIP-1014]: <https://zips.z.cash/zip-1014>
// [ZIP-214]: <https://zips.z.cash/zip-0214
if NetworkUpgrade::current(net, height) < NetworkUpgrade::Canopy {
// # Consensus
//
// > [Pre-Canopy] A coinbase transaction at `height ∈ {1 .. FoundersRewardLastBlockHeight}`
// > MUST include at least one output that pays exactly `FoundersReward(height)` zatoshi
// > with a standard P2SH script of the form `OP_HASH160 FounderRedeemScriptHash(height)
// > OP_EQUAL` as its `scriptPubKey`.
//
// ## Notes
//
// - `FoundersRewardLastBlockHeight := max({height : N | Halving(height) < 1})`
//
// <https://zips.z.cash/protocol/protocol.pdf#foundersreward>
if Height::MIN < height && height < net.height_for_first_halving() {
let addr = founders_reward_address(net, height).ok_or(
CoinbaseTransactionError::FoundersRewardAddressNotFound(height),
)?;
if !has_amount(&addr, founders_reward(net, height)) {
Err(SubsidyError::FoundersRewardNotFound)?;
}
}
Ok(DeferredPoolBalanceChange::zero())
} else {
// # Consensus
//
// > [Canopy onward] In each block with coinbase transaction `cb` at block height `height`,
// > `cb` MUST contain at least the given number of distinct outputs for each of the
// > following:
//
// > • for each funding stream `fs` active at that block height with a recipient identifier
// > other than `DEFERRED_POOL` given by `fs.Recipient(height)`, one output that pays
// > `fs.Value(height)` zatoshi in the prescribed way to the address represented by that
// > recipient identifier;
//
// > • [NU6.1 onward] if the block height is `ZIP271ActivationHeight`,
// > `ZIP271DisbursementChunks` equal outputs paying a total of `ZIP271DisbursementAmount`
// > zatoshi in the prescribed way to the Key-Holder Organizations’ P2SH multisig address
// > represented by `ZIP271DisbursementAddress`, as specified by [ZIP-271].
//
// > The term “prescribed way” is defined as follows:
//
// > The prescribed way to pay a transparent P2SH address is to use a standard P2SH script
// > of the form `OP_HASH160 fs.RedeemScriptHash(height) OP_EQUAL` as the `scriptPubKey`.
// > Here `fs.RedeemScriptHash(height)` is the standard redeem script hash for the recipient
// > address for `fs.Recipient(height)` in _Base58Check_ form. Standard redeem script hashes
// > are defined in [ZIP-48] for P2SH multisig addresses, or [Bitcoin-P2SH] for other P2SH
// > addresses.
//
// <https://zips.z.cash/protocol/protocol.pdf#fundingstreams>
//
// ZIP 2008 assigns a P2PKH recipient. Match the standard script of the assigned address
// exactly, retaining the prescribed P2SH scripts for all historical recipients.
// <https://zips.z.cash/zip-2008>
//
// [ZIP-271]: <https://zips.z.cash/zip-0271>
// [ZIP-48]: <https://zips.z.cash/zip-0048>
// [Bitcoin-P2SH]: <https://developer.bitcoin.org/devguide/transactions.html#pay-to-script-hash-p2sh>
let mut funding_streams = funding_stream_values(height, net, expected_block_subsidy)?;
// The deferred pool contribution is checked in `miner_fees_are_valid()` according to
// [ZIP-1015](https://zips.z.cash/zip-1015).
let mut deferred_pool_balance_change = funding_streams
.remove(&FundingStreamReceiver::Deferred)
.unwrap_or_default()
.constrain::<NegativeAllowed>()?;
// Check the one-time lockbox disbursements in the NU6.1 activation block's coinbase tx
// according to [ZIP-271] and [ZIP-1016].
//
// [ZIP-271]: <https://zips.z.cash/zip-0271>
// [ZIP-1016]: <https://zips.z.cash/zip-101>
if Some(height) == NetworkUpgrade::Nu6_1.activation_height(net) {
let lockbox_disbursements = net.lockbox_disbursements(height);
// The Mainnet and default Testnet disbursement lists are hardcoded and must be
// non-empty. Custom testnets and Regtest may configure no disbursements, in which
// case the NU6.1 activation block is not required to contain any disbursement
// outputs.
let must_have_disbursements =
matches!(net, Network::Mainnet) || net.is_default_testnet();
if lockbox_disbursements.is_empty() && must_have_disbursements {
Err(CoinbaseTransactionError::MissingLockboxDisbursements)?;
}
deferred_pool_balance_change = lockbox_disbursements.into_iter().try_fold(
deferred_pool_balance_change,
|balance, (addr, expected_amount)| {
if !has_amount(&addr, expected_amount) {
Err(SubsidyError::OneTimeLockboxDisbursementNotFound)?;
}
balance
.checked_sub(expected_amount)
.ok_or(SubsidyError::Underflow)
},
)?;
};
// Check each funding stream output.
funding_streams.into_iter().try_for_each(
|(receiver, expected_amount)| -> Result<(), CoinbaseTransactionError> {
let addr = funding_stream_address(height, net, receiver)
.ok_or(CoinbaseTransactionError::FundingStreamAddressNotFound)?;
if !has_amount(addr, expected_amount) {
Err(SubsidyError::FundingStreamNotFound)?;
}
Ok(())
},
)?;
Ok(DeferredPoolBalanceChange::new(deferred_pool_balance_change))
}
}
/// Returns `Ok(())` if the miner fees consensus rule is valid.
///
/// [7.1.2]: https://zips.z.cash/protocol/protocol.pdf#txnconsensus
pub fn miner_fees_are_valid(
coinbase_tx: &Transaction,
height: Height,
block_miner_fees: Amount<NonNegative>,
expected_block_subsidy: Amount<NonNegative>,
expected_deferred_pool_balance_change: DeferredPoolBalanceChange,
network: &Network,
) -> Result<(), CoinbaseTransactionError> {
let transparent_value_balance = coinbase_tx
.outputs()
.iter()
.map(|output| output.value())
.sum::<Result<Amount<NonNegative>, AmountError>>()
.map_err(|_| SubsidyError::Overflow)?
.constrain()
.map_err(CoinbaseTransactionError::InvalidTransparentValueBalance)?;
let sapling_value_balance = coinbase_tx.sapling_value_balance().sapling_amount();
let orchard_value_balance = coinbase_tx.orchard_value_balance().orchard_amount();
// [NU6.3 onward] The Ironwood pool is shielded too, so its value balance affects the coinbase
// output value exactly like Sapling and Orchard. This is zero for pre-v6 coinbase transactions
// (no Ironwood bundle), so it is a no-op before NU6.3.
let ironwood_value_balance = coinbase_tx.ironwood_value_balance().ironwood_amount();
// # Consensus
//
// > - define the total output value of its coinbase transaction to be the total value in zatoshi of its transparent
// > outputs, minus vbalanceSapling, minus vbalanceOrchard, minus vbalanceIronwood, plus totalDeferredOutput(height);
// > – define the total input value of its coinbase transaction to be the value in zatoshi of the block subsidy,
// > plus the transaction fees paid by transactions in the block.
//
// https://zips.z.cash/protocol/protocol.pdf#txnconsensus
//
// The expected lockbox funding stream output of the coinbase transaction is also subtracted
// from the block subsidy value plus the transaction fees paid by transactions in this block.
let total_output_value = (transparent_value_balance
- sapling_value_balance
- orchard_value_balance
- ironwood_value_balance
+ expected_deferred_pool_balance_change.value())
.map_err(|_| SubsidyError::Overflow)?;
// # Consensus
//
// > For every block from NU7 activation onward, the coinbase transaction MUST be balanced using
// > MinerFees in place of TransactionFees
//
// where `MinerFees(height) := TransactionFees(height) - NSMFeeContribution(height)`.
//
// https://github.com/zcash/zips/pull/1363
let claimable_miner_fees = (block_miner_fees
- nsm_fee_contribution(height, network, block_miner_fees))
.map_err(|_| SubsidyError::Overflow)?;
let total_input_value =
(expected_block_subsidy + claimable_miner_fees).map_err(|_| SubsidyError::Overflow)?;
// # Consensus
//
// > [Pre-NU6] The total output of a coinbase transaction MUST NOT be greater than its total
// input.
//
// > [NU6 onward] The total output of a coinbase transaction MUST be equal to its total input.
if if NetworkUpgrade::current(network, height) < NetworkUpgrade::Nu6 {
total_output_value > total_input_value
} else {
total_output_value != total_input_value
} {
Err(SubsidyError::InvalidMinerFees)?
};
Ok(())
}