change(consensus): Add lockbox funding stream (#8694)
* Addresses clippy lints * checks network magic and returns early from `is_regtest()` * Moves `subsidy.rs` to `zebra-chain`, refactors funding streams into structs, splits them into pre/post NU6 funding streams, and adds them as a field on `testnet::Parameters` * Replaces Vec with HashMap, adds `ConfiguredFundingStreams` type and conversion logic with constraints. Minor refactors * Empties recipients list * Adds a comment on num_addresses calculation being invalid for configured Testnets, but that being okay since configured testnet parameters are checked when they're being built * Documentation fixes, minor cleanup, renames a test, adds TODOs, and fixes test logic * Removes unnecessary `ParameterSubsidy` impl for &Network, adds docs and TODOs * Adds a "deferred" FundingStreamReceiver, adds a post-NU6 funding streams, updates the `miner_fees_are_valid()` and `subsidy_is_valid()` functions to check that the deferred pool contribution is valid and that there are no unclaimed block subsidies after NU6 activation, and adds some TODOs * adds `lockbox_input_value()` fn * Adds TODOs for linking to relevant ZIPs and updating height ranges * Adds `nu6_lockbox_funding_stream` acceptance test * updates funding stream values test to check post-NU6 funding streams too, adds Mainnet/Testnet NU6 activation heights, fixes lints/compilation issue * Reverts Mainnet/Testnet NU6 activation height definitions, updates `test_funding_stream_values()` to use a configured testnet with the post-NU6 Mainnet funding streams height range * reverts unnecessary refactor * appease clippy * Adds a test for `lockbox_input_value()` * Applies suggestions from code review * Fixes potential panic * Fixes bad merge * Update zebra-chain/src/parameters/network_upgrade.rs * Updates acceptance test to check that invalid blocks are rejected * Checks that the original valid block template at height 2 is accepted as a block submission * Reverts changes for coinbase should balance exactly ZIP * updates test name * Updates deferred pool funding stream name to "Lockbox", moves post-NU6 height ranges to constants, updates TODO * Updates `get_block_subsidy()` RPC method to exclude lockbox funding stream from `fundingstreams` field * Adds a TODO for updating `FundingStreamReceiver::name()` method docs * Updates `FundingStreamRecipient::new()` to accept an iterator of items instead of an option of an iterator, updates a comment quoting the coinbase transaction balance consensus rule to note that the current code is inconsistent with the protocol spec, adds a TODO for updating the quote there once the protocol spec has been updated. * Uses FPF Testnet address for post-NU6 testnet funding streams * Updates the NU6 consensus branch id --------- Co-authored-by: Pili Guerra <mpguerra@users.noreply.github.com>
This commit is contained in:
parent
45261a26eb
commit
e56ee4c792
|
@ -176,3 +176,7 @@ required-features = ["bench"]
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "redpallas"
|
name = "redpallas"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
# TODO: Remove this once it's no longer needed for NU6.
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(zcash_unstable, values("nu6"))'] }
|
||||||
|
|
|
@ -62,6 +62,9 @@ pub enum FundingStreamReceiver {
|
||||||
|
|
||||||
/// The Major Grants (Zcash Community Grants) funding stream.
|
/// The Major Grants (Zcash Community Grants) funding stream.
|
||||||
MajorGrants,
|
MajorGrants,
|
||||||
|
/// The deferred pool contribution.
|
||||||
|
// TODO: Add link to lockbox stream ZIP
|
||||||
|
Deferred,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FundingStreamReceiver {
|
impl FundingStreamReceiver {
|
||||||
|
@ -69,11 +72,15 @@ impl FundingStreamReceiver {
|
||||||
///
|
///
|
||||||
/// [ZIP-1014]: https://zips.z.cash/zip-1014#abstract
|
/// [ZIP-1014]: https://zips.z.cash/zip-1014#abstract
|
||||||
/// [`zcashd`]: https://github.com/zcash/zcash/blob/3f09cfa00a3c90336580a127e0096d99e25a38d6/src/consensus/funding.cpp#L13-L32
|
/// [`zcashd`]: https://github.com/zcash/zcash/blob/3f09cfa00a3c90336580a127e0096d99e25a38d6/src/consensus/funding.cpp#L13-L32
|
||||||
|
// TODO: Update method documentation with a reference to https://zips.z.cash/draft-nuttycom-funding-allocation once its
|
||||||
|
// status is updated to 'Proposed'.
|
||||||
pub fn name(self) -> &'static str {
|
pub fn name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
FundingStreamReceiver::Ecc => "Electric Coin Company",
|
FundingStreamReceiver::Ecc => "Electric Coin Company",
|
||||||
FundingStreamReceiver::ZcashFoundation => "Zcash Foundation",
|
FundingStreamReceiver::ZcashFoundation => "Zcash Foundation",
|
||||||
FundingStreamReceiver::MajorGrants => "Major Grants",
|
FundingStreamReceiver::MajorGrants => "Major Grants",
|
||||||
|
// TODO: Find out what this should be called and update the funding stream name.
|
||||||
|
FundingStreamReceiver::Deferred => "Lockbox",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,7 +188,7 @@ lazy_static! {
|
||||||
recipients: [
|
recipients: [
|
||||||
(
|
(
|
||||||
FundingStreamReceiver::Ecc,
|
FundingStreamReceiver::Ecc,
|
||||||
FundingStreamRecipient::new(7, FUNDING_STREAM_ECC_ADDRESSES_MAINNET.iter()),
|
FundingStreamRecipient::new(7, FUNDING_STREAM_ECC_ADDRESSES_MAINNET),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
FundingStreamReceiver::ZcashFoundation,
|
FundingStreamReceiver::ZcashFoundation,
|
||||||
|
@ -199,8 +206,21 @@ lazy_static! {
|
||||||
/// The post-NU6 funding streams for Mainnet
|
/// The post-NU6 funding streams for Mainnet
|
||||||
// TODO: Add a reference to lockbox stream ZIP, this is currently based on https://zips.z.cash/draft-nuttycom-funding-allocation
|
// TODO: Add a reference to lockbox stream ZIP, this is currently based on https://zips.z.cash/draft-nuttycom-funding-allocation
|
||||||
pub static ref POST_NU6_FUNDING_STREAMS_MAINNET: FundingStreams = FundingStreams {
|
pub static ref POST_NU6_FUNDING_STREAMS_MAINNET: FundingStreams = FundingStreams {
|
||||||
height_range: Height(2_726_400)..Height(3_146_400),
|
// TODO: Adjust this height range and recipient list once a proposal is selected
|
||||||
recipients: HashMap::new()
|
height_range: POST_NU6_FUNDING_STREAM_START_RANGE_MAINNET,
|
||||||
|
recipients: [
|
||||||
|
(
|
||||||
|
FundingStreamReceiver::Deferred,
|
||||||
|
FundingStreamRecipient::new::<[&str; 0], &str>(12, []),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
FundingStreamReceiver::MajorGrants,
|
||||||
|
// TODO: Update these addresses
|
||||||
|
FundingStreamRecipient::new(8, FUNDING_STREAM_MG_ADDRESSES_MAINNET),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The pre-NU6 funding streams for Testnet as described in [protocol specification §7.10.1][7.10.1]
|
/// The pre-NU6 funding streams for Testnet as described in [protocol specification §7.10.1][7.10.1]
|
||||||
|
@ -229,11 +249,45 @@ lazy_static! {
|
||||||
// TODO: Add a reference to lockbox stream ZIP, this is currently based on the number of blocks between the
|
// TODO: Add a reference to lockbox stream ZIP, this is currently based on the number of blocks between the
|
||||||
// start and end heights for Mainnet in https://zips.z.cash/draft-nuttycom-funding-allocation
|
// start and end heights for Mainnet in https://zips.z.cash/draft-nuttycom-funding-allocation
|
||||||
pub static ref POST_NU6_FUNDING_STREAMS_TESTNET: FundingStreams = FundingStreams {
|
pub static ref POST_NU6_FUNDING_STREAMS_TESTNET: FundingStreams = FundingStreams {
|
||||||
height_range: Height(2_942_000)..Height(3_362_000),
|
// TODO: Adjust this height range and recipient list once a proposal is selected
|
||||||
recipients: HashMap::new()
|
height_range: POST_NU6_FUNDING_STREAM_START_RANGE_TESTNET,
|
||||||
|
recipients: [
|
||||||
|
(
|
||||||
|
FundingStreamReceiver::Deferred,
|
||||||
|
FundingStreamRecipient::new::<[&str; 0], &str>(12, []),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
FundingStreamReceiver::MajorGrants,
|
||||||
|
// TODO: Update these addresses
|
||||||
|
FundingStreamRecipient::new(8, POST_NU6_FUNDING_STREAM_FPF_ADDRESSES_TESTNET),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The start height of post-NU6 funding streams on Mainnet
|
||||||
|
// TODO: Add a reference to lockbox stream ZIP, this is currently based on https://zips.z.cash/draft-nuttycom-funding-allocation
|
||||||
|
const POST_NU6_FUNDING_STREAM_START_HEIGHT_MAINNET: u32 = 2_726_400;
|
||||||
|
|
||||||
|
/// The start height of post-NU6 funding streams on Testnet
|
||||||
|
// TODO: Add a reference to lockbox stream ZIP, this is currently based on https://zips.z.cash/draft-nuttycom-funding-allocation
|
||||||
|
const POST_NU6_FUNDING_STREAM_START_HEIGHT_TESTNET: u32 = 2_942_000;
|
||||||
|
|
||||||
|
/// The number of blocks contained in the post-NU6 funding streams height ranges on Mainnet or Testnet.
|
||||||
|
const POST_NU6_FUNDING_STREAM_NUM_BLOCKS: u32 = 420_000;
|
||||||
|
|
||||||
|
/// The post-NU6 funding stream height range on Mainnet
|
||||||
|
const POST_NU6_FUNDING_STREAM_START_RANGE_MAINNET: std::ops::Range<Height> =
|
||||||
|
Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_MAINNET)
|
||||||
|
..Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_MAINNET + POST_NU6_FUNDING_STREAM_NUM_BLOCKS);
|
||||||
|
|
||||||
|
/// The post-NU6 funding stream height range on Testnet
|
||||||
|
const POST_NU6_FUNDING_STREAM_START_RANGE_TESTNET: std::ops::Range<Height> =
|
||||||
|
Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_TESTNET)
|
||||||
|
..Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_TESTNET + POST_NU6_FUNDING_STREAM_NUM_BLOCKS);
|
||||||
|
|
||||||
/// Address change interval function here as a constant
|
/// Address change interval function here as a constant
|
||||||
/// as described in [protocol specification §7.10.1][7.10.1].
|
/// as described in [protocol specification §7.10.1][7.10.1].
|
||||||
///
|
///
|
||||||
|
@ -402,6 +456,18 @@ pub const FUNDING_STREAM_ZF_ADDRESSES_TESTNET: [&str; FUNDING_STREAMS_NUM_ADDRES
|
||||||
pub const FUNDING_STREAM_MG_ADDRESSES_TESTNET: [&str; FUNDING_STREAMS_NUM_ADDRESSES_TESTNET] =
|
pub const FUNDING_STREAM_MG_ADDRESSES_TESTNET: [&str; FUNDING_STREAMS_NUM_ADDRESSES_TESTNET] =
|
||||||
["t2Gvxv2uNM7hbbACjNox4H6DjByoKZ2Fa3P"; FUNDING_STREAMS_NUM_ADDRESSES_TESTNET];
|
["t2Gvxv2uNM7hbbACjNox4H6DjByoKZ2Fa3P"; FUNDING_STREAMS_NUM_ADDRESSES_TESTNET];
|
||||||
|
|
||||||
|
/// Number of addresses for each post-NU6 funding stream in the Testnet.
|
||||||
|
/// In the spec ([protocol specification §7.10][7.10]) this is defined as: `fs.addressindex(fs.endheight - 1)`
|
||||||
|
/// however we know this value beforehand so we prefer to make it a constant instead.
|
||||||
|
///
|
||||||
|
/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams
|
||||||
|
pub const POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_TESTNET: usize = 13;
|
||||||
|
|
||||||
|
/// List of addresses for the Major Grants post-NU6 funding stream in the Testnet administered by the Financial Privacy Fund (FPF).
|
||||||
|
pub const POST_NU6_FUNDING_STREAM_FPF_ADDRESSES_TESTNET: [&str;
|
||||||
|
POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_TESTNET] =
|
||||||
|
["t2HifwjUj9uyxr9bknR8LFuQbc98c3vkXtu"; POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_TESTNET];
|
||||||
|
|
||||||
/// Returns the address change period
|
/// Returns the address change period
|
||||||
/// as described in [protocol specification §7.10][7.10]
|
/// as described in [protocol specification §7.10][7.10]
|
||||||
///
|
///
|
||||||
|
|
|
@ -68,7 +68,7 @@ pub struct ConfiguredFundingStreamRecipient {
|
||||||
/// The numerator for each funding stream receiver category, see [`FundingStreamRecipient::numerator`] for more details.
|
/// The numerator for each funding stream receiver category, see [`FundingStreamRecipient::numerator`] for more details.
|
||||||
pub numerator: u64,
|
pub numerator: u64,
|
||||||
/// Addresses for the funding stream recipient, see [`FundingStreamRecipient::addresses`] for more details.
|
/// Addresses for the funding stream recipient, see [`FundingStreamRecipient::addresses`] for more details.
|
||||||
pub addresses: Vec<String>,
|
pub addresses: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfiguredFundingStreamRecipient {
|
impl ConfiguredFundingStreamRecipient {
|
||||||
|
@ -76,7 +76,7 @@ impl ConfiguredFundingStreamRecipient {
|
||||||
pub fn into_recipient(self) -> (FundingStreamReceiver, FundingStreamRecipient) {
|
pub fn into_recipient(self) -> (FundingStreamReceiver, FundingStreamRecipient) {
|
||||||
(
|
(
|
||||||
self.receiver,
|
self.receiver,
|
||||||
FundingStreamRecipient::new(self.numerator, self.addresses),
|
FundingStreamRecipient::new(self.numerator, self.addresses.unwrap_or_default()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,8 +133,12 @@ impl ConfiguredFundingStreams {
|
||||||
))
|
))
|
||||||
.expect("no overflow should happen in this sub") as usize;
|
.expect("no overflow should happen in this sub") as usize;
|
||||||
|
|
||||||
for recipient in funding_streams.recipients().values() {
|
for (&receiver, recipient) in funding_streams.recipients() {
|
||||||
// TODO: Make an exception for the `Deferred` receiver.
|
if receiver == FundingStreamReceiver::Deferred {
|
||||||
|
// The `Deferred` receiver doesn't need any addresses.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
recipient.addresses().len() >= expected_min_num_addresses,
|
recipient.addresses().len() >= expected_min_num_addresses,
|
||||||
"recipients must have a sufficient number of addresses for height range, \
|
"recipients must have a sufficient number of addresses for height range, \
|
||||||
|
|
|
@ -320,9 +320,11 @@ fn check_configured_funding_stream_constraints() {
|
||||||
recipients: Some(vec![ConfiguredFundingStreamRecipient {
|
recipients: Some(vec![ConfiguredFundingStreamRecipient {
|
||||||
receiver: FundingStreamReceiver::Ecc,
|
receiver: FundingStreamReceiver::Ecc,
|
||||||
numerator: 20,
|
numerator: 20,
|
||||||
addresses: FUNDING_STREAM_ECC_ADDRESSES_TESTNET
|
addresses: Some(
|
||||||
|
FUNDING_STREAM_ECC_ADDRESSES_TESTNET
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.to_vec(),
|
.to_vec(),
|
||||||
|
),
|
||||||
}]),
|
}]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -330,9 +332,11 @@ fn check_configured_funding_stream_constraints() {
|
||||||
recipients: Some(vec![ConfiguredFundingStreamRecipient {
|
recipients: Some(vec![ConfiguredFundingStreamRecipient {
|
||||||
receiver: FundingStreamReceiver::Ecc,
|
receiver: FundingStreamReceiver::Ecc,
|
||||||
numerator: 100,
|
numerator: 100,
|
||||||
addresses: FUNDING_STREAM_ECC_ADDRESSES_TESTNET
|
addresses: Some(
|
||||||
|
FUNDING_STREAM_ECC_ADDRESSES_TESTNET
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.to_vec(),
|
.to_vec(),
|
||||||
|
),
|
||||||
}]),
|
}]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -398,7 +402,7 @@ fn check_configured_funding_stream_constraints() {
|
||||||
recipients: Some(vec![ConfiguredFundingStreamRecipient {
|
recipients: Some(vec![ConfiguredFundingStreamRecipient {
|
||||||
receiver: FundingStreamReceiver::Ecc,
|
receiver: FundingStreamReceiver::Ecc,
|
||||||
numerator: 10,
|
numerator: 10,
|
||||||
addresses: vec![],
|
addresses: Some(vec![]),
|
||||||
}]),
|
}]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
@ -410,9 +414,11 @@ fn check_configured_funding_stream_constraints() {
|
||||||
recipients: Some(vec![ConfiguredFundingStreamRecipient {
|
recipients: Some(vec![ConfiguredFundingStreamRecipient {
|
||||||
receiver: FundingStreamReceiver::Ecc,
|
receiver: FundingStreamReceiver::Ecc,
|
||||||
numerator: 101,
|
numerator: 101,
|
||||||
addresses: FUNDING_STREAM_ECC_ADDRESSES_TESTNET
|
addresses: Some(
|
||||||
|
FUNDING_STREAM_ECC_ADDRESSES_TESTNET
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.to_vec(),
|
.to_vec(),
|
||||||
|
),
|
||||||
}]),
|
}]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
@ -424,9 +430,11 @@ fn check_configured_funding_stream_constraints() {
|
||||||
recipients: Some(vec![ConfiguredFundingStreamRecipient {
|
recipients: Some(vec![ConfiguredFundingStreamRecipient {
|
||||||
receiver: FundingStreamReceiver::Ecc,
|
receiver: FundingStreamReceiver::Ecc,
|
||||||
numerator: 10,
|
numerator: 10,
|
||||||
addresses: FUNDING_STREAM_ECC_ADDRESSES_MAINNET
|
addresses: Some(
|
||||||
|
FUNDING_STREAM_ECC_ADDRESSES_MAINNET
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.to_vec(),
|
.to_vec(),
|
||||||
|
),
|
||||||
}]),
|
}]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
|
@ -88,7 +88,8 @@ pub(super) const MAINNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)]
|
||||||
(block::Height(903_000), Heartwood),
|
(block::Height(903_000), Heartwood),
|
||||||
(block::Height(1_046_400), Canopy),
|
(block::Height(1_046_400), Canopy),
|
||||||
(block::Height(1_687_104), Nu5),
|
(block::Height(1_687_104), Nu5),
|
||||||
// TODO: Add NU6.
|
// TODO: Add NU6
|
||||||
|
// (block::Height(2_726_400), Nu6),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Fake mainnet network upgrade activation heights, used in tests.
|
/// Fake mainnet network upgrade activation heights, used in tests.
|
||||||
|
@ -124,7 +125,8 @@ pub(super) const TESTNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)]
|
||||||
(block::Height(903_800), Heartwood),
|
(block::Height(903_800), Heartwood),
|
||||||
(block::Height(1_028_500), Canopy),
|
(block::Height(1_028_500), Canopy),
|
||||||
(block::Height(1_842_420), Nu5),
|
(block::Height(1_842_420), Nu5),
|
||||||
// TODO: Add NU6.
|
// TODO: Add NU6
|
||||||
|
// (block::Height(2_942_000), Nu6),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Fake testnet network upgrade activation heights, used in tests.
|
/// Fake testnet network upgrade activation heights, used in tests.
|
||||||
|
@ -214,8 +216,7 @@ pub(crate) const CONSENSUS_BRANCH_IDS: &[(NetworkUpgrade, ConsensusBranchId)] =
|
||||||
(Heartwood, ConsensusBranchId(0xf5b9230b)),
|
(Heartwood, ConsensusBranchId(0xf5b9230b)),
|
||||||
(Canopy, ConsensusBranchId(0xe9ff75a6)),
|
(Canopy, ConsensusBranchId(0xe9ff75a6)),
|
||||||
(Nu5, ConsensusBranchId(0xc2d6d0b4)),
|
(Nu5, ConsensusBranchId(0xc2d6d0b4)),
|
||||||
// TODO: Use the real consensus branch ID once it's specified.
|
(Nu6, ConsensusBranchId(0xc8e71055)),
|
||||||
(Nu6, ConsensusBranchId(0xdeadc0de)),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// The target block spacing before Blossom.
|
/// The target block spacing before Blossom.
|
||||||
|
@ -530,6 +531,8 @@ impl From<zcash_protocol::consensus::NetworkUpgrade> for NetworkUpgrade {
|
||||||
zcash_protocol::consensus::NetworkUpgrade::Heartwood => Self::Heartwood,
|
zcash_protocol::consensus::NetworkUpgrade::Heartwood => Self::Heartwood,
|
||||||
zcash_protocol::consensus::NetworkUpgrade::Canopy => Self::Canopy,
|
zcash_protocol::consensus::NetworkUpgrade::Canopy => Self::Canopy,
|
||||||
zcash_protocol::consensus::NetworkUpgrade::Nu5 => Self::Nu5,
|
zcash_protocol::consensus::NetworkUpgrade::Nu5 => Self::Nu5,
|
||||||
|
#[cfg(zcash_unstable = "nu6")]
|
||||||
|
zcash_protocol::consensus::NetworkUpgrade::Nu6 => Self::Nu6,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use chrono::{DateTime, Utc};
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
amount::{Amount, Error as AmountError, NonNegative},
|
amount::{Amount, Error as AmountError, NonNegative},
|
||||||
block::{Block, Hash, Header, Height},
|
block::{Block, Hash, Header, Height},
|
||||||
parameters::{Network, NetworkUpgrade},
|
parameters::{subsidy::FundingStreamReceiver, Network, NetworkUpgrade},
|
||||||
transaction,
|
transaction,
|
||||||
work::{
|
work::{
|
||||||
difficulty::{ExpandedDifficulty, ParameterDifficulty as _},
|
difficulty::{ExpandedDifficulty, ParameterDifficulty as _},
|
||||||
|
@ -177,7 +177,7 @@ pub fn subsidy_is_valid(block: &Block, network: &Network) -> Result<(), BlockErr
|
||||||
// Founders rewards are paid up to Canopy activation, on both mainnet and testnet.
|
// Founders rewards are paid up to Canopy activation, on both mainnet and testnet.
|
||||||
// But we checkpoint in Canopy so founders reward does not apply for Zebra.
|
// But we checkpoint in Canopy so founders reward does not apply for Zebra.
|
||||||
unreachable!("we cannot verify consensus rules before Canopy activation");
|
unreachable!("we cannot verify consensus rules before Canopy activation");
|
||||||
} else if halving_div < 4 {
|
} else if halving_div < 8 {
|
||||||
// Funding streams are paid from Canopy activation to the second halving
|
// Funding streams are paid from Canopy activation to the second halving
|
||||||
// Note: Canopy activation is at the first halving on mainnet, but not on testnet
|
// 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
|
// ZIP-1014 only applies to mainnet, ZIP-214 contains the specific rules for testnet
|
||||||
|
@ -194,8 +194,16 @@ pub fn subsidy_is_valid(block: &Block, network: &Network) -> Result<(), BlockErr
|
||||||
//
|
//
|
||||||
// https://zips.z.cash/protocol/protocol.pdf#fundingstreams
|
// https://zips.z.cash/protocol/protocol.pdf#fundingstreams
|
||||||
for (receiver, expected_amount) in funding_streams {
|
for (receiver, expected_amount) in funding_streams {
|
||||||
let address =
|
if receiver == FundingStreamReceiver::Deferred {
|
||||||
subsidy::funding_streams::funding_stream_address(height, network, receiver);
|
// The deferred pool contribution is checked in `miner_fees_are_valid()`
|
||||||
|
// TODO: Add link to lockbox stream ZIP
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let address = subsidy::funding_streams::funding_stream_address(
|
||||||
|
height, network, receiver,
|
||||||
|
)
|
||||||
|
.expect("funding stream receivers other than the deferred pool must have an address");
|
||||||
|
|
||||||
let has_expected_output =
|
let has_expected_output =
|
||||||
subsidy::funding_streams::filter_outputs_by_address(coinbase, address)
|
subsidy::funding_streams::filter_outputs_by_address(coinbase, address)
|
||||||
|
@ -237,6 +245,12 @@ pub fn miner_fees_are_valid(
|
||||||
let block_subsidy = subsidy::general::block_subsidy(height, network)
|
let block_subsidy = subsidy::general::block_subsidy(height, network)
|
||||||
.expect("a valid block subsidy for this height and network");
|
.expect("a valid block subsidy for this height and network");
|
||||||
|
|
||||||
|
// TODO: Add link to lockbox stream ZIP
|
||||||
|
let expected_deferred_amount = subsidy::funding_streams::funding_stream_values(height, network)
|
||||||
|
.expect("we always expect a funding stream hashmap response even if empty")
|
||||||
|
.remove(&FundingStreamReceiver::Deferred)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
// # Consensus
|
// # Consensus
|
||||||
//
|
//
|
||||||
// > The total value in zatoshi of transparent outputs from a coinbase transaction,
|
// > The total value in zatoshi of transparent outputs from a coinbase transaction,
|
||||||
|
@ -244,9 +258,16 @@ pub fn miner_fees_are_valid(
|
||||||
// > in zatoshi of block subsidy plus the transaction fees paid by transactions in this block.
|
// > in zatoshi of block subsidy plus the transaction fees paid by transactions in this block.
|
||||||
//
|
//
|
||||||
// https://zips.z.cash/protocol/protocol.pdf#txnconsensus
|
// 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.
|
||||||
|
//
|
||||||
|
// TODO: Update the quote from the protocol specification once its been updated to reflect the changes in
|
||||||
|
// https://zips.z.cash/draft-nuttycom-funding-allocation and https://zips.z.cash/draft-hopwood-coinbase-balance.
|
||||||
let left = (transparent_value_balance - sapling_value_balance - orchard_value_balance)
|
let left = (transparent_value_balance - sapling_value_balance - orchard_value_balance)
|
||||||
.map_err(|_| SubsidyError::SumOverflow)?;
|
.map_err(|_| SubsidyError::SumOverflow)?;
|
||||||
let right = (block_subsidy + block_miner_fees).map_err(|_| SubsidyError::SumOverflow)?;
|
let right = (block_subsidy + block_miner_fees - expected_deferred_amount)
|
||||||
|
.map_err(|_| SubsidyError::SumOverflow)?;
|
||||||
|
|
||||||
if left > right {
|
if left > right {
|
||||||
Err(SubsidyError::InvalidMinerFees)?;
|
Err(SubsidyError::InvalidMinerFees)?;
|
||||||
|
|
|
@ -52,8 +52,17 @@ pub fn funding_stream_values(
|
||||||
/// as described in [protocol specification §7.10][7.10]
|
/// as described in [protocol specification §7.10][7.10]
|
||||||
///
|
///
|
||||||
/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams
|
/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams
|
||||||
fn funding_stream_address_index(height: Height, network: &Network) -> usize {
|
fn funding_stream_address_index(
|
||||||
|
height: Height,
|
||||||
|
network: &Network,
|
||||||
|
receiver: FundingStreamReceiver,
|
||||||
|
) -> Option<usize> {
|
||||||
|
if receiver == FundingStreamReceiver::Deferred {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let funding_streams = network.funding_streams(height);
|
let funding_streams = network.funding_streams(height);
|
||||||
|
let num_addresses = funding_streams.recipient(receiver)?.addresses().len();
|
||||||
|
|
||||||
let index = 1u32
|
let index = 1u32
|
||||||
.checked_add(funding_stream_address_period(height, network))
|
.checked_add(funding_stream_address_period(height, network))
|
||||||
|
@ -64,22 +73,10 @@ fn funding_stream_address_index(height: Height, network: &Network) -> usize {
|
||||||
))
|
))
|
||||||
.expect("no overflow should happen in this sub") as usize;
|
.expect("no overflow should happen in this sub") as usize;
|
||||||
|
|
||||||
// Funding stream recipients may not have the same number of addresses on configured Testnets,
|
|
||||||
// the number of addresses for each recipient should be validated for a configured height range
|
|
||||||
// when configured Testnet parameters are built.
|
|
||||||
let num_addresses = funding_streams
|
|
||||||
.recipients()
|
|
||||||
.values()
|
|
||||||
.next()
|
|
||||||
// TODO: Return an Option from this function and replace `.unwrap()` with `?`
|
|
||||||
.unwrap()
|
|
||||||
.addresses()
|
|
||||||
.len();
|
|
||||||
|
|
||||||
assert!(index > 0 && index <= num_addresses);
|
assert!(index > 0 && index <= num_addresses);
|
||||||
// spec formula will output an index starting at 1 but
|
// spec formula will output an index starting at 1 but
|
||||||
// Zebra indices for addresses start at zero, return converted.
|
// Zebra indices for addresses start at zero, return converted.
|
||||||
index - 1
|
Some(index - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the address corresponding to given height, network and funding stream receiver.
|
/// Return the address corresponding to given height, network and funding stream receiver.
|
||||||
|
@ -90,17 +87,10 @@ pub fn funding_stream_address(
|
||||||
height: Height,
|
height: Height,
|
||||||
network: &Network,
|
network: &Network,
|
||||||
receiver: FundingStreamReceiver,
|
receiver: FundingStreamReceiver,
|
||||||
) -> &transparent::Address {
|
) -> Option<&transparent::Address> {
|
||||||
let index = funding_stream_address_index(height, network);
|
let index = funding_stream_address_index(height, network, receiver)?;
|
||||||
let funding_streams = network.funding_streams(height);
|
let funding_streams = network.funding_streams(height);
|
||||||
funding_streams
|
funding_streams.recipient(receiver)?.addresses().get(index)
|
||||||
.recipient(receiver)
|
|
||||||
// TODO: Change return type to option and return None here instead of panicking
|
|
||||||
.unwrap()
|
|
||||||
.addresses()
|
|
||||||
.get(index)
|
|
||||||
// TODO: Change return type to option and return None here instead of panicking
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a human-readable name and a specification URL for the funding stream `receiver`.
|
/// Return a human-readable name and a specification URL for the funding stream `receiver`.
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
//! Tests for funding streams.
|
//! Tests for funding streams.
|
||||||
|
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use zebra_chain::parameters::{subsidy::FundingStreamReceiver, NetworkKind};
|
use zebra_chain::parameters::{
|
||||||
|
subsidy::FundingStreamReceiver,
|
||||||
|
testnet::{
|
||||||
|
self, ConfiguredActivationHeights, ConfiguredFundingStreamRecipient,
|
||||||
|
ConfiguredFundingStreams,
|
||||||
|
},
|
||||||
|
NetworkKind,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -45,7 +52,6 @@ fn test_funding_stream_values() -> Result<(), Report> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// funding stream period is ending
|
// funding stream period is ending
|
||||||
// TODO: Check post-NU6 funding streams here as well.
|
|
||||||
let range = network.pre_nu6_funding_streams().height_range();
|
let range = network.pre_nu6_funding_streams().height_range();
|
||||||
let end = range.end;
|
let end = range.end;
|
||||||
let last = end - 1;
|
let last = end - 1;
|
||||||
|
@ -56,6 +62,57 @@ fn test_funding_stream_values() -> Result<(), Report> {
|
||||||
);
|
);
|
||||||
assert!(funding_stream_values(end, network)?.is_empty());
|
assert!(funding_stream_values(end, network)?.is_empty());
|
||||||
|
|
||||||
|
// TODO: Replace this with Mainnet once there's an NU6 activation height defined for Mainnet
|
||||||
|
let network = testnet::Parameters::build()
|
||||||
|
.with_activation_heights(ConfiguredActivationHeights {
|
||||||
|
blossom: Some(Blossom.activation_height(network).unwrap().0),
|
||||||
|
nu6: Some(POST_NU6_FUNDING_STREAMS_MAINNET.height_range().start.0),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_post_nu6_funding_streams(ConfiguredFundingStreams {
|
||||||
|
// Start checking funding streams from block height 1
|
||||||
|
height_range: Some(POST_NU6_FUNDING_STREAMS_MAINNET.height_range().clone()),
|
||||||
|
// Use default post-NU6 recipients
|
||||||
|
recipients: Some(
|
||||||
|
POST_NU6_FUNDING_STREAMS_TESTNET
|
||||||
|
.recipients()
|
||||||
|
.iter()
|
||||||
|
.map(|(&receiver, recipient)| ConfiguredFundingStreamRecipient {
|
||||||
|
receiver,
|
||||||
|
numerator: recipient.numerator(),
|
||||||
|
addresses: Some(
|
||||||
|
recipient
|
||||||
|
.addresses()
|
||||||
|
.iter()
|
||||||
|
.map(|addr| addr.to_string())
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.to_network();
|
||||||
|
|
||||||
|
let mut hash_map = HashMap::new();
|
||||||
|
hash_map.insert(
|
||||||
|
FundingStreamReceiver::Deferred,
|
||||||
|
Amount::try_from(18_750_000)?,
|
||||||
|
);
|
||||||
|
hash_map.insert(
|
||||||
|
FundingStreamReceiver::MajorGrants,
|
||||||
|
Amount::try_from(12_500_000)?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let nu6_height = Nu6.activation_height(&network).unwrap();
|
||||||
|
|
||||||
|
for height in [
|
||||||
|
nu6_height,
|
||||||
|
Height(nu6_height.0 + 1),
|
||||||
|
Height(nu6_height.0 + 1),
|
||||||
|
] {
|
||||||
|
assert_eq!(funding_stream_values(height, &network).unwrap(), hash_map);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,10 +118,44 @@ pub fn output_amounts(transaction: &Transaction) -> HashSet<Amount<NonNegative>>
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lockbox funding stream total input value for a block height.
|
||||||
|
///
|
||||||
|
/// Assumes a constant funding stream amount per block.
|
||||||
|
// TODO: Cache the lockbox value balance in zebra-state (will be required for tracking lockbox
|
||||||
|
// value balance after the Zcash Sustainability Fund ZIPs or after a ZIP for spending from the deferred pool)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn lockbox_input_value(network: &Network, height: Height) -> Amount<NonNegative> {
|
||||||
|
let Some(nu6_activation_height) = Nu6.activation_height(network) else {
|
||||||
|
return Amount::zero();
|
||||||
|
};
|
||||||
|
|
||||||
|
let &deferred_amount_per_block = funding_stream_values(nu6_activation_height, network)
|
||||||
|
.expect("we always expect a funding stream hashmap response even if empty")
|
||||||
|
.get(&FundingStreamReceiver::Deferred)
|
||||||
|
.expect("we expect a lockbox funding stream after NU5");
|
||||||
|
|
||||||
|
let post_nu6_funding_stream_height_range = network.post_nu6_funding_streams().height_range();
|
||||||
|
|
||||||
|
// `min(height, last_height_with_deferred_pool_contribution) - (nu6_activation_height - 1)`,
|
||||||
|
// We decrement NU6 activation height since it's an inclusive lower bound.
|
||||||
|
// Funding stream height range end bound is not incremented since it's an exclusive end bound
|
||||||
|
let num_blocks_with_lockbox_output = (height.0 + 1)
|
||||||
|
.min(post_nu6_funding_stream_height_range.end.0)
|
||||||
|
.checked_sub(post_nu6_funding_stream_height_range.start.0)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
(deferred_amount_per_block * num_blocks_with_lockbox_output.into())
|
||||||
|
.expect("lockbox input value should fit in Amount")
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
|
use zebra_chain::parameters::testnet::{
|
||||||
|
self, ConfiguredActivationHeights, ConfiguredFundingStreamRecipient,
|
||||||
|
ConfiguredFundingStreams,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn halving_test() -> Result<(), Report> {
|
fn halving_test() -> Result<(), Report> {
|
||||||
|
@ -391,4 +425,71 @@ mod test {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_lockbox_input_value() -> Result<(), Report> {
|
||||||
|
let _init_guard = zebra_test::init();
|
||||||
|
|
||||||
|
let network = testnet::Parameters::build()
|
||||||
|
.with_activation_heights(ConfiguredActivationHeights {
|
||||||
|
blossom: Some(Blossom.activation_height(&Network::Mainnet).unwrap().0),
|
||||||
|
nu6: Some(POST_NU6_FUNDING_STREAMS_MAINNET.height_range().start.0),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.with_post_nu6_funding_streams(ConfiguredFundingStreams {
|
||||||
|
// Start checking funding streams from block height 1
|
||||||
|
height_range: Some(POST_NU6_FUNDING_STREAMS_MAINNET.height_range().clone()),
|
||||||
|
// Use default post-NU6 recipients
|
||||||
|
recipients: Some(
|
||||||
|
POST_NU6_FUNDING_STREAMS_TESTNET
|
||||||
|
.recipients()
|
||||||
|
.iter()
|
||||||
|
.map(|(&receiver, recipient)| ConfiguredFundingStreamRecipient {
|
||||||
|
receiver,
|
||||||
|
numerator: recipient.numerator(),
|
||||||
|
addresses: Some(
|
||||||
|
recipient
|
||||||
|
.addresses()
|
||||||
|
.iter()
|
||||||
|
.map(|addr| addr.to_string())
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.to_network();
|
||||||
|
|
||||||
|
let nu6_height = Nu6.activation_height(&network).unwrap();
|
||||||
|
let post_nu6_funding_streams = network.post_nu6_funding_streams();
|
||||||
|
let height_range = post_nu6_funding_streams.height_range();
|
||||||
|
|
||||||
|
let last_funding_stream_height = post_nu6_funding_streams
|
||||||
|
.height_range()
|
||||||
|
.end
|
||||||
|
.previous()
|
||||||
|
.expect("the previous height should be valid");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Amount::<NonNegative>::zero(),
|
||||||
|
lockbox_input_value(&network, Height::MIN)
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected_lockbox_value: Amount<NonNegative> = Amount::try_from(18_750_000)?;
|
||||||
|
assert_eq!(
|
||||||
|
expected_lockbox_value,
|
||||||
|
lockbox_input_value(&network, nu6_height)
|
||||||
|
);
|
||||||
|
|
||||||
|
let num_blocks_total = height_range.end.0 - height_range.start.0;
|
||||||
|
let expected_input_per_block: Amount<NonNegative> = Amount::try_from(18_750_000)?;
|
||||||
|
let expected_lockbox_value = (expected_input_per_block * num_blocks_total.into())?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
expected_lockbox_value,
|
||||||
|
lockbox_input_value(&network, last_funding_stream_height)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -636,12 +636,7 @@ where
|
||||||
//
|
//
|
||||||
// Optional TODO:
|
// Optional TODO:
|
||||||
// - add `async changed()` method to ChainSyncStatus (like `ChainTip`)
|
// - add `async changed()` method to ChainSyncStatus (like `ChainTip`)
|
||||||
// TODO:
|
|
||||||
// - Add a `disable_peers` field to `Network` to check instead of `disable_pow()` (#8361)
|
|
||||||
// - Check the field in `sync_status` so it applies to the mempool as well.
|
|
||||||
if !network.disable_pow() {
|
|
||||||
check_synced_to_tip(&network, latest_chain_tip.clone(), sync_status.clone())?;
|
check_synced_to_tip(&network, latest_chain_tip.clone(), sync_status.clone())?;
|
||||||
}
|
|
||||||
// TODO: return an error if we have no peers, like `zcashd` does,
|
// TODO: return an error if we have no peers, like `zcashd` does,
|
||||||
// and add a developer config that mines regardless of how many peers we have.
|
// and add a developer config that mines regardless of how many peers we have.
|
||||||
// https://github.com/zcash/zcash/blob/6fdd9f1b81d3b228326c9826fa10696fc516444b/src/miner.cpp#L865-L880
|
// https://github.com/zcash/zcash/blob/6fdd9f1b81d3b228326c9826fa10696fc516444b/src/miner.cpp#L865-L880
|
||||||
|
@ -1197,9 +1192,9 @@ where
|
||||||
})?;
|
})?;
|
||||||
let mut funding_streams: Vec<_> = funding_streams
|
let mut funding_streams: Vec<_> = funding_streams
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(receiver, value)| {
|
.filter_map(|(receiver, value)| {
|
||||||
let address = funding_stream_address(height, &network, *receiver);
|
let address = funding_stream_address(height, &network, *receiver)?;
|
||||||
(*receiver, FundingStream::new(*receiver, *value, address))
|
Some((*receiver, FundingStream::new(*receiver, *value, address)))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
|
@ -154,6 +154,7 @@ where
|
||||||
// - State and syncer checks
|
// - State and syncer checks
|
||||||
|
|
||||||
/// Returns an error if Zebra is not synced to the consensus chain tip.
|
/// Returns an error if Zebra is not synced to the consensus chain tip.
|
||||||
|
/// Returns early with `Ok(())` if Proof-of-Work is disabled on the provided `network`.
|
||||||
/// This error might be incorrect if the local clock is skewed.
|
/// This error might be incorrect if the local clock is skewed.
|
||||||
pub fn check_synced_to_tip<Tip, SyncStatus>(
|
pub fn check_synced_to_tip<Tip, SyncStatus>(
|
||||||
network: &Network,
|
network: &Network,
|
||||||
|
@ -164,6 +165,13 @@ where
|
||||||
Tip: ChainTip + Clone + Send + Sync + 'static,
|
Tip: ChainTip + Clone + Send + Sync + 'static,
|
||||||
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
|
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
|
// TODO:
|
||||||
|
// - Add a `disable_peers` field to `Network` to check instead of `disable_pow()` (#8361)
|
||||||
|
// - Check the field in `sync_status` so it applies to the mempool as well.
|
||||||
|
if network.disable_pow() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// The tip estimate may not be the same as the one coming from the state
|
// The tip estimate may not be the same as the one coming from the state
|
||||||
// but this is ok for an estimate
|
// but this is ok for an estimate
|
||||||
let (estimated_distance_to_chain_tip, local_tip_height) = latest_chain_tip
|
let (estimated_distance_to_chain_tip, local_tip_height) = latest_chain_tip
|
||||||
|
@ -376,11 +384,11 @@ pub fn standard_coinbase_outputs(
|
||||||
(Amount<NonNegative>, &transparent::Address),
|
(Amount<NonNegative>, &transparent::Address),
|
||||||
> = funding_streams
|
> = funding_streams
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(receiver, amount)| {
|
.filter_map(|(receiver, amount)| {
|
||||||
(
|
Some((
|
||||||
receiver,
|
receiver,
|
||||||
(amount, funding_stream_address(height, network, receiver)),
|
(amount, funding_stream_address(height, network, receiver)?),
|
||||||
)
|
))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,11 @@ impl ProposalResponse {
|
||||||
|
|
||||||
ProposalResponse::Rejected(final_error.to_string())
|
ProposalResponse::Rejected(final_error.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if self is [`ProposalResponse::Valid`]
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
matches!(self, Self::Valid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ProposalResponse> for Response {
|
impl From<ProposalResponse> for Response {
|
||||||
|
|
|
@ -299,4 +299,5 @@ zebra-grpc = { path = "../zebra-grpc", version = "0.1.0-alpha.5" }
|
||||||
zebra-utils = { path = "../zebra-utils", version = "1.0.0-beta.38" }
|
zebra-utils = { path = "../zebra-utils", version = "1.0.0-beta.38" }
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
|
# TODO: Remove 'cfg(zcash_unstable, values("nu6"))' once it's no longer needed for NU6.
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)', 'cfg(zcash_unstable, values("nu6"))'] }
|
||||||
|
|
|
@ -3228,3 +3228,256 @@ async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test successful block template submission as a block proposal or submission on a custom Testnet.
|
||||||
|
///
|
||||||
|
/// This test can be run locally with:
|
||||||
|
/// `RUSTFLAGS='--cfg zcash_unstable="nu6"' cargo test --package zebrad --test acceptance --features getblocktemplate-rpcs -- nu6_funding_streams --exact --show-output`
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
#[cfg(all(feature = "getblocktemplate-rpcs", zcash_unstable = "nu6"))]
|
||||||
|
async fn nu6_funding_streams() -> Result<()> {
|
||||||
|
use zebra_chain::{
|
||||||
|
chain_sync_status::MockSyncStatus,
|
||||||
|
parameters::{
|
||||||
|
subsidy::{FundingStreamReceiver, FUNDING_STREAM_MG_ADDRESSES_TESTNET},
|
||||||
|
testnet::{
|
||||||
|
self, ConfiguredActivationHeights, ConfiguredFundingStreamRecipient,
|
||||||
|
ConfiguredFundingStreams,
|
||||||
|
},
|
||||||
|
NetworkUpgrade,
|
||||||
|
},
|
||||||
|
serialization::ZcashSerialize,
|
||||||
|
work::difficulty::U256,
|
||||||
|
};
|
||||||
|
use zebra_network::address_book_peers::MockAddressBookPeers;
|
||||||
|
use zebra_node_services::mempool;
|
||||||
|
use zebra_rpc::methods::{
|
||||||
|
get_block_template_rpcs::{
|
||||||
|
get_block_template::{
|
||||||
|
fetch_state_tip_and_local_time, generate_coinbase_and_roots,
|
||||||
|
proposal_block_from_template, GetBlockTemplate, GetBlockTemplateRequestMode,
|
||||||
|
},
|
||||||
|
types::get_block_template,
|
||||||
|
types::submit_block,
|
||||||
|
},
|
||||||
|
hex_data::HexData,
|
||||||
|
GetBlockTemplateRpc, GetBlockTemplateRpcImpl,
|
||||||
|
};
|
||||||
|
use zebra_test::mock_service::MockService;
|
||||||
|
let _init_guard = zebra_test::init();
|
||||||
|
|
||||||
|
tracing::info!("running nu6_funding_streams_and_coinbase_balance test");
|
||||||
|
|
||||||
|
let base_network_params = testnet::Parameters::build()
|
||||||
|
// Regtest genesis hash
|
||||||
|
.with_genesis_hash("029f11d80ef9765602235e1bc9727e3eb6ba20839319f761fee920d63401e327")
|
||||||
|
.with_target_difficulty_limit(U256::from_big_endian(&[0x0f; 32]))
|
||||||
|
.with_disable_pow(true)
|
||||||
|
.with_slow_start_interval(Height::MIN)
|
||||||
|
.with_activation_heights(ConfiguredActivationHeights {
|
||||||
|
nu6: Some(1),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let network = base_network_params
|
||||||
|
.clone()
|
||||||
|
.with_post_nu6_funding_streams(ConfiguredFundingStreams {
|
||||||
|
// Start checking funding streams from block height 1
|
||||||
|
height_range: Some(Height(1)..Height(100)),
|
||||||
|
// Use default post-NU6 recipients
|
||||||
|
recipients: None,
|
||||||
|
})
|
||||||
|
.to_network();
|
||||||
|
|
||||||
|
tracing::info!("built configured Testnet, starting state service and block verifier");
|
||||||
|
|
||||||
|
let default_test_config = default_test_config(&network)?;
|
||||||
|
let mining_config = default_test_config.mining;
|
||||||
|
let miner_address = mining_config
|
||||||
|
.miner_address
|
||||||
|
.clone()
|
||||||
|
.expect("hard-coded config should have a miner address");
|
||||||
|
|
||||||
|
let (state, read_state, latest_chain_tip, _chain_tip_change) =
|
||||||
|
zebra_state::init_test_services(&network);
|
||||||
|
|
||||||
|
let (
|
||||||
|
block_verifier_router,
|
||||||
|
_transaction_verifier,
|
||||||
|
_parameter_download_task_handle,
|
||||||
|
_max_checkpoint_height,
|
||||||
|
) = zebra_consensus::router::init(zebra_consensus::Config::default(), &network, state.clone())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
tracing::info!("started state service and block verifier, committing Regtest genesis block");
|
||||||
|
|
||||||
|
let genesis_hash = block_verifier_router
|
||||||
|
.clone()
|
||||||
|
.oneshot(zebra_consensus::Request::Commit(regtest_genesis_block()))
|
||||||
|
.await
|
||||||
|
.expect("should validate Regtest genesis block");
|
||||||
|
|
||||||
|
let mut mempool = MockService::build()
|
||||||
|
.with_max_request_delay(Duration::from_secs(5))
|
||||||
|
.for_unit_tests();
|
||||||
|
let mut mock_sync_status = MockSyncStatus::default();
|
||||||
|
mock_sync_status.set_is_close_to_tip(true);
|
||||||
|
|
||||||
|
let get_block_template_rpc_impl = GetBlockTemplateRpcImpl::new(
|
||||||
|
&network,
|
||||||
|
mining_config,
|
||||||
|
mempool.clone(),
|
||||||
|
read_state.clone(),
|
||||||
|
latest_chain_tip,
|
||||||
|
block_verifier_router,
|
||||||
|
mock_sync_status,
|
||||||
|
MockAddressBookPeers::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let make_mock_mempool_request_handler = || async move {
|
||||||
|
mempool
|
||||||
|
.expect_request(mempool::Request::FullTransactions)
|
||||||
|
.await
|
||||||
|
.respond(mempool::Response::FullTransactions {
|
||||||
|
transactions: vec![],
|
||||||
|
// tip hash needs to match chain info for long poll requests
|
||||||
|
last_seen_tip_hash: genesis_hash,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let block_template_fut = get_block_template_rpc_impl.get_block_template(None);
|
||||||
|
let mock_mempool_request_handler = make_mock_mempool_request_handler.clone()();
|
||||||
|
let (block_template, _) = tokio::join!(block_template_fut, mock_mempool_request_handler);
|
||||||
|
let get_block_template::Response::TemplateMode(block_template) =
|
||||||
|
block_template.expect("unexpected error in getblocktemplate RPC call")
|
||||||
|
else {
|
||||||
|
panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response")
|
||||||
|
};
|
||||||
|
|
||||||
|
let proposal_block = proposal_block_from_template(&block_template, None, NetworkUpgrade::Nu6)?;
|
||||||
|
let hex_proposal_block = HexData(proposal_block.zcash_serialize_to_vec()?);
|
||||||
|
|
||||||
|
// Check that the block template is a valid block proposal
|
||||||
|
let get_block_template::Response::ProposalMode(block_proposal_result) =
|
||||||
|
get_block_template_rpc_impl
|
||||||
|
.get_block_template(Some(get_block_template::JsonParameters {
|
||||||
|
mode: GetBlockTemplateRequestMode::Proposal,
|
||||||
|
data: Some(hex_proposal_block),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
panic!(
|
||||||
|
"this getblocktemplate call should return the `ProposalMode` variant of the response"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
block_proposal_result.is_valid(),
|
||||||
|
"block proposal should succeed"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Submit the same block
|
||||||
|
let submit_block_response = get_block_template_rpc_impl
|
||||||
|
.submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
submit_block_response,
|
||||||
|
submit_block::Response::Accepted,
|
||||||
|
"valid block should be accepted"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use an invalid coinbase transaction (with an output value greater than the `block_subsidy + miner_fees - expected_lockbox_funding_stream`)
|
||||||
|
|
||||||
|
let make_configured_recipients_with_lockbox_numerator = |numerator| {
|
||||||
|
Some(vec![
|
||||||
|
ConfiguredFundingStreamRecipient {
|
||||||
|
receiver: FundingStreamReceiver::Deferred,
|
||||||
|
numerator,
|
||||||
|
addresses: None,
|
||||||
|
},
|
||||||
|
ConfiguredFundingStreamRecipient {
|
||||||
|
receiver: FundingStreamReceiver::MajorGrants,
|
||||||
|
numerator: 8,
|
||||||
|
addresses: Some(
|
||||||
|
FUNDING_STREAM_MG_ADDRESSES_TESTNET
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.to_vec(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gets the next block template
|
||||||
|
let block_template_fut = get_block_template_rpc_impl.get_block_template(None);
|
||||||
|
let mock_mempool_request_handler = make_mock_mempool_request_handler.clone()();
|
||||||
|
let (block_template, _) = tokio::join!(block_template_fut, mock_mempool_request_handler);
|
||||||
|
let get_block_template::Response::TemplateMode(block_template) =
|
||||||
|
block_template.expect("unexpected error in getblocktemplate RPC call")
|
||||||
|
else {
|
||||||
|
panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response")
|
||||||
|
};
|
||||||
|
|
||||||
|
let valid_original_block_template = block_template.clone();
|
||||||
|
|
||||||
|
let zebra_state::GetBlockTemplateChainInfo { history_tree, .. } =
|
||||||
|
fetch_state_tip_and_local_time(read_state.clone()).await?;
|
||||||
|
|
||||||
|
let network = base_network_params
|
||||||
|
.clone()
|
||||||
|
.with_post_nu6_funding_streams(ConfiguredFundingStreams {
|
||||||
|
height_range: Some(Height(1)..Height(100)),
|
||||||
|
recipients: make_configured_recipients_with_lockbox_numerator(0),
|
||||||
|
})
|
||||||
|
.to_network();
|
||||||
|
|
||||||
|
let (coinbase_txn, default_roots) = generate_coinbase_and_roots(
|
||||||
|
&network,
|
||||||
|
Height(block_template.height),
|
||||||
|
&miner_address,
|
||||||
|
&[],
|
||||||
|
history_tree.clone(),
|
||||||
|
true,
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
let block_template = GetBlockTemplate {
|
||||||
|
coinbase_txn,
|
||||||
|
block_commitments_hash: default_roots.block_commitments_hash,
|
||||||
|
light_client_root_hash: default_roots.block_commitments_hash,
|
||||||
|
final_sapling_root_hash: default_roots.block_commitments_hash,
|
||||||
|
default_roots,
|
||||||
|
..(*block_template)
|
||||||
|
};
|
||||||
|
|
||||||
|
let proposal_block = proposal_block_from_template(&block_template, None, NetworkUpgrade::Nu6)?;
|
||||||
|
|
||||||
|
// Submit the invalid block with an excessive coinbase output value
|
||||||
|
let submit_block_response = get_block_template_rpc_impl
|
||||||
|
.submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tracing::info!(?submit_block_response, "submitted invalid block");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
submit_block_response,
|
||||||
|
submit_block::Response::ErrorResponse(submit_block::ErrorResponse::Rejected),
|
||||||
|
"invalid block with excessive coinbase output value should be rejected"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that the original block template can be submitted successfully
|
||||||
|
let proposal_block =
|
||||||
|
proposal_block_from_template(&valid_original_block_template, None, NetworkUpgrade::Nu6)?;
|
||||||
|
let submit_block_response = get_block_template_rpc_impl
|
||||||
|
.submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
submit_block_response,
|
||||||
|
submit_block::Response::Accepted,
|
||||||
|
"valid block should be accepted"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue