Skip to content

Commit 59dfd11

Browse files
authored
Network Sustainability Mechanism - ZIP 233 implementation (ZcashFoundation#8930)
1 parent a042d4c commit 59dfd11

File tree

15 files changed

+419
-9
lines changed

15 files changed

+419
-9
lines changed

zebra-chain/src/parameters/network_upgrade.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,9 @@ pub(crate) const CONSENSUS_BRANCH_IDS: &[(NetworkUpgrade, ConsensusBranchId)] =
236236
(Nu5, ConsensusBranchId(0xc2d6d0b4)),
237237
(Nu6, ConsensusBranchId(0xc8e71055)),
238238
(Nu6_1, ConsensusBranchId(0x4dec4df0)),
239+
// TODO: set below to (Nu7, ConsensusBranchId(0x77190ad8)), once the same value is set in librustzcash
239240
#[cfg(any(test, feature = "zebra-test"))]
240-
(Nu7, ConsensusBranchId(0x77190ad8)),
241+
(Nu7, ConsensusBranchId(0xffffffff)),
241242
#[cfg(zcash_unstable = "zfuture")]
242243
(ZFuture, ConsensusBranchId(0xffffffff)),
243244
];

zebra-chain/src/transaction.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ pub enum Transaction {
161161
lock_time: LockTime,
162162
/// The latest block height that this transaction can be added to the chain.
163163
expiry_height: block::Height,
164+
/// The burn amount for this transaction, if any.
165+
zip233_amount: Amount<NonNegative>,
164166
/// The transparent inputs to the transaction.
165167
inputs: Vec<transparent::Input>,
166168
/// The transparent outputs from the transaction.
@@ -169,7 +171,6 @@ pub enum Transaction {
169171
sapling_shielded_data: Option<sapling::ShieldedData<sapling::SharedAnchor>>,
170172
/// The orchard data for this transaction, if any.
171173
orchard_shielded_data: Option<orchard::ShieldedData>,
172-
// TODO: Add the rest of the v6 fields.
173174
},
174175
}
175176

@@ -190,6 +191,8 @@ impl fmt::Display for Transaction {
190191
if let Some(expiry_height) = self.expiry_height() {
191192
fmter.field("expiry_height", &expiry_height);
192193
}
194+
#[cfg(feature = "tx_v6")]
195+
fmter.field("zip233_amount", &self.zip233_amount());
193196

194197
fmter.field("transparent_inputs", &self.inputs().len());
195198
fmter.field("transparent_outputs", &self.outputs().len());
@@ -321,6 +324,11 @@ impl Transaction {
321324
.contains(orchard::Flags::ENABLE_SPENDS))
322325
}
323326

327+
/// Does this transaction have zip233_amount output?
328+
#[cfg(feature = "tx_v6")]
329+
pub fn has_zip233_amount(&self) -> bool {
330+
self.zip233_amount() > Amount::<NonNegative>::zero()
331+
}
324332
/// Does this transaction have shielded outputs?
325333
///
326334
/// See [`Self::has_transparent_or_shielded_outputs`] for details.
@@ -1537,6 +1545,19 @@ impl Transaction {
15371545
Transaction::V6 { .. } => Some(TX_V6_VERSION_GROUP_ID),
15381546
}
15391547
}
1548+
1549+
/// Access the zip233 amount field of this transaction, regardless of version.
1550+
pub fn zip233_amount(&self) -> Amount<NonNegative> {
1551+
match self {
1552+
Transaction::V1 { .. }
1553+
| Transaction::V2 { .. }
1554+
| Transaction::V3 { .. }
1555+
| Transaction::V4 { .. }
1556+
| Transaction::V5 { .. } => Amount::zero(),
1557+
#[cfg(feature = "tx_v6")]
1558+
Transaction::V6 { zip233_amount, .. } => *zip233_amount,
1559+
}
1560+
}
15401561
}
15411562

15421563
#[cfg(any(test, feature = "proptest-impl"))]

zebra-chain/src/transaction/builder.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,101 @@ use crate::{
99
};
1010

1111
impl Transaction {
12+
/// Returns a new version 6 coinbase transaction for `network` and `height`,
13+
/// which contains the specified `outputs`.
14+
#[cfg(feature = "tx_v6")]
15+
pub fn new_v6_coinbase(
16+
network: &Network,
17+
height: Height,
18+
outputs: impl IntoIterator<Item = (Amount<NonNegative>, transparent::Script)>,
19+
miner_data: Vec<u8>,
20+
zip233_amount: Option<Amount<NonNegative>>,
21+
) -> Transaction {
22+
// # Consensus
23+
//
24+
// These consensus rules apply to v5 coinbase transactions after NU5 activation:
25+
//
26+
// > If effectiveVersion ≥ 5 then this condition MUST hold:
27+
// > tx_in_count > 0 or nSpendsSapling > 0 or
28+
// > (nActionsOrchard > 0 and enableSpendsOrchard = 1).
29+
//
30+
// > A coinbase transaction for a block at block height greater than 0 MUST have
31+
// > a script that, as its first item, encodes the block height as follows. ...
32+
// > let heightBytes be the signed little-endian representation of height,
33+
// > using the minimum nonzero number of bytes such that the most significant byte
34+
// > is < 0x80. The length of heightBytes MUST be in the range {1 .. 5}.
35+
// > Then the encoding is the length of heightBytes encoded as one byte,
36+
// > followed by heightBytes itself. This matches the encoding used by Bitcoin
37+
// > in the implementation of [BIP-34]
38+
// > (but the description here is to be considered normative).
39+
//
40+
// > A coinbase transaction script MUST have length in {2 .. 100} bytes.
41+
//
42+
// Zebra adds extra coinbase data if configured to do so.
43+
//
44+
// Since we're not using a lock time, any sequence number is valid here.
45+
// See `Transaction::lock_time()` for the relevant consensus rules.
46+
//
47+
// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
48+
let inputs = vec![transparent::Input::new_coinbase(height, miner_data, None)];
49+
50+
// > The block subsidy is composed of a miner subsidy and a series of funding streams.
51+
//
52+
// <https://zips.z.cash/protocol/protocol.pdf#subsidyconcepts>
53+
//
54+
// > The total value in zatoshi of transparent outputs from a coinbase transaction,
55+
// > minus vbalanceSapling, minus vbalanceOrchard, MUST NOT be greater than
56+
// > the value in zatoshi of block subsidy plus the transaction fees
57+
// > paid by transactions in this block.
58+
//
59+
// > If effectiveVersion ≥ 5 then this condition MUST hold:
60+
// > tx_out_count > 0 or nOutputsSapling > 0 or
61+
// > (nActionsOrchard > 0 and enableOutputsOrchard = 1).
62+
//
63+
// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
64+
let outputs: Vec<_> = outputs
65+
.into_iter()
66+
.map(|(amount, lock_script)| transparent::Output::new_coinbase(amount, lock_script))
67+
.collect();
68+
assert!(
69+
!outputs.is_empty(),
70+
"invalid coinbase transaction: must have at least one output"
71+
);
72+
73+
Transaction::V6 {
74+
// > The transaction version number MUST be 4 or 5. ...
75+
// > If the transaction version number is 5 then the version group ID
76+
// > MUST be 0x26A7270A.
77+
// > If effectiveVersion ≥ 5, the nConsensusBranchId field MUST match the consensus
78+
// > branch ID used for SIGHASH transaction hashes, as specified in [ZIP-244].
79+
network_upgrade: NetworkUpgrade::current(network, height),
80+
81+
// There is no documented consensus rule for the lock time field in coinbase
82+
// transactions, so we just leave it unlocked. (We could also set it to `height`.)
83+
lock_time: LockTime::unlocked(),
84+
85+
// > The nExpiryHeight field of a coinbase transaction MUST be equal to its
86+
// > block height.
87+
expiry_height: height,
88+
89+
// > The NSM zip233_amount field [ZIP-233] must be set. It must be >= 0.
90+
zip233_amount: zip233_amount.unwrap_or(Amount::zero()),
91+
92+
inputs,
93+
outputs,
94+
95+
// Zebra does not support shielded coinbase yet.
96+
//
97+
// > In a version 5 coinbase transaction, the enableSpendsOrchard flag MUST be 0.
98+
// > In a version 5 transaction, the reserved bits 2 .. 7 of the flagsOrchard field
99+
// > MUST be zero.
100+
//
101+
// See the Zcash spec for additional shielded coinbase consensus rules.
102+
sapling_shielded_data: None,
103+
orchard_shielded_data: None,
104+
}
105+
}
106+
12107
/// Returns a new version 5 coinbase transaction for `network` and `height`,
13108
/// which contains the specified `outputs`.
14109
pub fn new_v5_coinbase(

zebra-chain/src/transaction/serialize.rs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ use crate::{
2020
},
2121
};
2222

23+
#[cfg(feature = "tx_v6")]
24+
use crate::parameters::TX_V6_VERSION_GROUP_ID;
25+
2326
use super::*;
2427
use crate::sapling;
2528

@@ -678,6 +681,7 @@ impl ZcashSerialize for Transaction {
678681
network_upgrade,
679682
lock_time,
680683
expiry_height,
684+
zip233_amount,
681685
inputs,
682686
outputs,
683687
sapling_shielded_data,
@@ -687,7 +691,7 @@ impl ZcashSerialize for Transaction {
687691
// https://zips.z.cash/zip-0230#specification
688692

689693
// Denoted as `nVersionGroupId` in the spec.
690-
writer.write_u32::<LittleEndian>(TX_V5_VERSION_GROUP_ID)?;
694+
writer.write_u32::<LittleEndian>(TX_V6_VERSION_GROUP_ID)?;
691695

692696
// Denoted as `nConsensusBranchId` in the spec.
693697
writer.write_u32::<LittleEndian>(u32::from(
@@ -702,6 +706,9 @@ impl ZcashSerialize for Transaction {
702706
// Denoted as `nExpiryHeight` in the spec.
703707
writer.write_u32::<LittleEndian>(expiry_height.0)?;
704708

709+
// Denoted as `zip233_amount` in the spec.
710+
zip233_amount.zcash_serialize(&mut writer)?;
711+
705712
// Denoted as `tx_in_count` and `tx_in` in the spec.
706713
inputs.zcash_serialize(&mut writer)?;
707714

@@ -718,8 +725,6 @@ impl ZcashSerialize for Transaction {
718725
// `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`,
719726
// `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`.
720727
orchard_shielded_data.zcash_serialize(&mut writer)?;
721-
722-
// TODO: Add the rest of v6 transaction fields.
723728
}
724729
}
725730
Ok(())
@@ -972,6 +977,55 @@ impl ZcashDeserialize for Transaction {
972977
orchard_shielded_data,
973978
})
974979
}
980+
#[cfg(feature = "tx_v6")]
981+
(6, true) => {
982+
// Denoted as `nVersionGroupId` in the spec.
983+
let id = limited_reader.read_u32::<LittleEndian>()?;
984+
if id != TX_V6_VERSION_GROUP_ID {
985+
return Err(SerializationError::Parse("expected TX_V6_VERSION_GROUP_ID"));
986+
}
987+
// Denoted as `nConsensusBranchId` in the spec.
988+
// Convert it to a NetworkUpgrade
989+
let network_upgrade =
990+
NetworkUpgrade::try_from(limited_reader.read_u32::<LittleEndian>()?)?;
991+
992+
// Denoted as `lock_time` in the spec.
993+
let lock_time = LockTime::zcash_deserialize(&mut limited_reader)?;
994+
995+
// Denoted as `nExpiryHeight` in the spec.
996+
let expiry_height = block::Height(limited_reader.read_u32::<LittleEndian>()?);
997+
998+
// Denoted as `zip233_amount` in the spec.
999+
let zip233_amount = (&mut limited_reader).zcash_deserialize_into()?;
1000+
1001+
// Denoted as `tx_in_count` and `tx_in` in the spec.
1002+
let inputs = Vec::zcash_deserialize(&mut limited_reader)?;
1003+
1004+
// Denoted as `tx_out_count` and `tx_out` in the spec.
1005+
let outputs = Vec::zcash_deserialize(&mut limited_reader)?;
1006+
1007+
// A bundle of fields denoted in the spec as `nSpendsSapling`, `vSpendsSapling`,
1008+
// `nOutputsSapling`,`vOutputsSapling`, `valueBalanceSapling`, `anchorSapling`,
1009+
// `vSpendProofsSapling`, `vSpendAuthSigsSapling`, `vOutputProofsSapling` and
1010+
// `bindingSigSapling`.
1011+
let sapling_shielded_data = (&mut limited_reader).zcash_deserialize_into()?;
1012+
1013+
// A bundle of fields denoted in the spec as `nActionsOrchard`, `vActionsOrchard`,
1014+
// `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`,
1015+
// `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`.
1016+
let orchard_shielded_data = (&mut limited_reader).zcash_deserialize_into()?;
1017+
1018+
Ok(Transaction::V6 {
1019+
network_upgrade,
1020+
lock_time,
1021+
expiry_height,
1022+
zip233_amount,
1023+
inputs,
1024+
outputs,
1025+
sapling_shielded_data,
1026+
orchard_shielded_data,
1027+
})
1028+
}
9751029
(_, _) => Err(SerializationError::Parse("bad tx header")),
9761030
}
9771031
}

zebra-consensus/src/transaction.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,13 +555,22 @@ where
555555
// Get the `value_balance` to calculate the transaction fee.
556556
let value_balance = tx.value_balance(&spent_utxos);
557557

558+
let zip233_amount = match *tx {
559+
#[cfg(feature="tx_v6")]
560+
Transaction::V6{ .. } => tx.zip233_amount(),
561+
_ => Amount::zero()
562+
};
563+
558564
// Calculate the fee only for non-coinbase transactions.
559565
let mut miner_fee = None;
560566
if !tx.is_coinbase() {
561567
// TODO: deduplicate this code with remaining_transaction_value()?
562568
miner_fee = match value_balance {
563569
Ok(vb) => match vb.remaining_transaction_value() {
564-
Ok(tx_rtv) => Some(tx_rtv),
570+
Ok(tx_rtv) => match tx_rtv - zip233_amount {
571+
Ok(fee) => Some(fee),
572+
Err(_) => return Err(TransactionError::IncorrectFee),
573+
}
565574
Err(_) => return Err(TransactionError::IncorrectFee),
566575
},
567576
Err(_) => return Err(TransactionError::IncorrectFee),

zebra-consensus/src/transaction/check.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,15 @@ pub fn lock_time_has_passed(
122122
///
123123
/// This check counts both `Coinbase` and `PrevOut` transparent inputs.
124124
pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> {
125+
#[cfg(feature = "tx_v6")]
126+
let has_other_circulation_effects = tx.has_zip233_amount();
127+
128+
#[cfg(not(feature = "tx_v6"))]
129+
let has_other_circulation_effects = false;
130+
125131
if !tx.has_transparent_or_shielded_inputs() {
126132
Err(TransactionError::NoInputs)
127-
} else if !tx.has_transparent_or_shielded_outputs() {
133+
} else if !tx.has_transparent_or_shielded_outputs() && !has_other_circulation_effects {
128134
Err(TransactionError::NoOutputs)
129135
} else {
130136
Ok(())

zebra-consensus/src/transaction/tests/prop.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,9 @@ fn mock_transparent_transaction(
303303
// Create the mock transaction
304304
let expiry_height = block_height;
305305

306+
#[cfg(feature = "tx_v6")]
307+
let zip233_amount = Amount::zero();
308+
306309
let transaction = match transaction_version {
307310
4 => Transaction::V4 {
308311
inputs,
@@ -321,6 +324,17 @@ fn mock_transparent_transaction(
321324
orchard_shielded_data: None,
322325
network_upgrade,
323326
},
327+
#[cfg(feature = "tx_v6")]
328+
6 => Transaction::V6 {
329+
inputs,
330+
outputs,
331+
lock_time,
332+
expiry_height,
333+
zip233_amount,
334+
sapling_shielded_data: None,
335+
orchard_shielded_data: None,
336+
network_upgrade,
337+
},
324338
invalid_version => unreachable!("invalid transaction version: {}", invalid_version),
325339
};
326340

zebra-rpc/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ categories = [
2323
exclude = ["*.proto"]
2424

2525
[features]
26+
tx_v6 = ["zebra-chain/tx_v6", "zebra-state/tx_v6", "zebra-consensus/tx_v6"]
2627

2728
# Production features that activate extra dependencies, or extra features in
2829
# dependencies
@@ -39,6 +40,7 @@ proptest-impl = [
3940
"zebra-chain/proptest-impl",
4041
]
4142

43+
4244
[dependencies]
4345
chrono = { workspace = true, features = ["clock", "std"] }
4446
futures = { workspace = true }

zebra-rpc/src/methods.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2413,6 +2413,8 @@ where
24132413
mempool_txs,
24142414
mempool_tx_deps,
24152415
extra_coinbase_data.clone(),
2416+
#[cfg(feature = "tx_v6")]
2417+
None,
24162418
);
24172419

24182420
tracing::debug!(
@@ -2433,6 +2435,8 @@ where
24332435
mempool_txs,
24342436
submit_old,
24352437
extra_coinbase_data,
2438+
#[cfg(feature = "tx_v6")]
2439+
None,
24362440
);
24372441

24382442
Ok(response.into())

0 commit comments

Comments
 (0)