Track anchors and note commitment trees in zebra-state (#2458)
* Tidy chain Cargo.toml * Organize imports * Add method to get note commitments from all Actions in Orchard shielded data * Add method to get note commitments from all JoinSplits in Sprout JoinSplitData * Add Request and Response variants for awaiting anchors * Add anchors and note commitment trees to finalized state db * Add (From|Into)Disk impls for tree::Roots and stubs for NoteCommitmentTrees * Track anchors and note commitment trees in Chain Append note commitments to their trees when doing update_chain_state_with, then use the resulting Sapling and Orchard roots to pass to history_tree, and add new roots to the anchor sets. * Handle errors when appending to note commitment trees * Add comments explaining why note commitment are not removed from the tree in revert_chain_state_with * Implementing note commitments in finalized state * Finish serialization of Orchard tree; remove old tree when updating finalize state * Add serialization and finalized state updates for Sprout and Sapling trees * Partially handle trees in non-finalized state. Use Option for trees in Chain * Rebuild trees when forking; change finalized state tree getters to not require height * Pass empty trees to tests; use empty trees by default in Chain * Also rebuild anchor sets when forking * Use empty tree as default in finalized state tree getters (for now) * Use HashMultiSet for anchors in order to make pop_root() work correctly * Reduce DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES and MAX_PARTIAL_CHAIN_BLOCKS * Reduce DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES and MAX_PARTIAL_CHAIN_BLOCKS even more * Apply suggestions from code review * Add comments about order of note commitments and related methods/fields * Don't use Option for trees * Set DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES=1 and restore MAX_PARTIAL_CHAIN_BLOCKS * Remove unneeded anchor set rebuilding in fork() * Improve proptest formatting * Add missing comparisons to eq_internal_state * Renamed sprout::tree::NoteCommitmentTree::hash() to root() * Improve comments * Add asserts, add issues to TODOs * Remove impl Default for Chain since it was only used by tests * Improve documentation and assertions; add tree serialization tests * Remove Sprout code, which will be moved to another branch * Add todo! in Sprout tree append() * Remove stub request, response *Anchor* handling for now * Add test for validating Sapling note commitment tree using test blocks * Increase database version (new columns added for note commitment trees and anchors) * Update test to make sure the order of sapling_note_commitments() is being tested * Improve comments and structure of the test * Improve variable names again * Rustfmt Co-authored-by: Deirdre Connolly <deirdre@zfnd.org> Co-authored-by: Conrado P. L. Gouvea <conradoplg@gmail.com> Co-authored-by: Conrado Gouvea <conrado@zfnd.org> Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
parent
3d792f7195
commit
e719c46b1b
|
@ -300,6 +300,15 @@ dependencies = [
|
||||||
"crunchy 0.1.6",
|
"crunchy 0.1.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bincode"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bindgen"
|
name = "bindgen"
|
||||||
version = "0.57.0"
|
version = "0.57.0"
|
||||||
|
@ -2131,6 +2140,12 @@ dependencies = [
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multiset"
|
||||||
|
version = "0.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce8738c9ddd350996cb8b8b718192851df960803764bcdaa3afb44a63b1ddb5c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "net2"
|
name = "net2"
|
||||||
version = "0.2.36"
|
version = "0.2.36"
|
||||||
|
@ -4623,15 +4638,19 @@ dependencies = [
|
||||||
name = "zebra-state"
|
name = "zebra-state"
|
||||||
version = "1.0.0-alpha.13"
|
version = "1.0.0-alpha.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"dirs",
|
"dirs",
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"futures 0.3.15",
|
"futures 0.3.15",
|
||||||
|
"halo2",
|
||||||
"hex",
|
"hex",
|
||||||
"itertools 0.10.1",
|
"itertools 0.10.1",
|
||||||
|
"jubjub",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"metrics",
|
"metrics",
|
||||||
|
"multiset",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proptest",
|
"proptest",
|
||||||
"proptest-derive",
|
"proptest-derive",
|
||||||
|
|
|
@ -15,10 +15,12 @@ bench = ["zebra-test"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes = "0.6"
|
aes = "0.6"
|
||||||
bech32 = "0.8.1"
|
bech32 = "0.8.1"
|
||||||
|
bigint = "4"
|
||||||
bitflags = "1.2.1"
|
bitflags = "1.2.1"
|
||||||
bitvec = "0.22"
|
bitvec = "0.22"
|
||||||
blake2b_simd = "0.5.11"
|
blake2b_simd = "0.5.11"
|
||||||
blake2s_simd = "0.5.11"
|
blake2s_simd = "0.5.11"
|
||||||
|
bls12_381 = "0.5.0"
|
||||||
bs58 = { version = "0.4", features = ["check"] }
|
bs58 = { version = "0.4", features = ["check"] }
|
||||||
byteorder = "1.4"
|
byteorder = "1.4"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
@ -30,6 +32,7 @@ group = "0.10"
|
||||||
# Note: if updating this, also update the workspace Cargo.toml to match.
|
# Note: if updating this, also update the workspace Cargo.toml to match.
|
||||||
halo2 = { git = "https://github.com/zcash/halo2.git", rev = "236115917df9db45282fec24d1e1e36f275f71ab" }
|
halo2 = { git = "https://github.com/zcash/halo2.git", rev = "236115917df9db45282fec24d1e1e36f275f71ab" }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
incrementalmerkletree = "0.1.0"
|
||||||
jubjub = "0.7.0"
|
jubjub = "0.7.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
rand_core = "0.6"
|
rand_core = "0.6"
|
||||||
|
@ -40,13 +43,10 @@ serde-big-array = "0.3.2"
|
||||||
sha2 = { version = "0.9.5", features=["compress"] }
|
sha2 = { version = "0.9.5", features=["compress"] }
|
||||||
subtle = "2.4"
|
subtle = "2.4"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
uint = "0.9.1"
|
||||||
x25519-dalek = { version = "1.1", features = ["serde"] }
|
x25519-dalek = { version = "1.1", features = ["serde"] }
|
||||||
zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "0c3ed159985affa774e44d10172d4471d798a85a" }
|
zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "0c3ed159985affa774e44d10172d4471d798a85a" }
|
||||||
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "0c3ed159985affa774e44d10172d4471d798a85a" }
|
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "0c3ed159985affa774e44d10172d4471d798a85a" }
|
||||||
bigint = "4"
|
|
||||||
uint = "0.9.1"
|
|
||||||
bls12_381 = "0.5.0"
|
|
||||||
incrementalmerkletree = "0.1.0"
|
|
||||||
|
|
||||||
proptest = { version = "0.10", optional = true }
|
proptest = { version = "0.10", optional = true }
|
||||||
proptest-derive = { version = "0.3.0", optional = true }
|
proptest-derive = { version = "0.3.0", optional = true }
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
//! Orchard shielded data for `V5` `Transaction`s.
|
//! Orchard shielded data for `V5` `Transaction`s.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
cmp::{Eq, PartialEq},
|
||||||
|
fmt::Debug,
|
||||||
|
io,
|
||||||
|
};
|
||||||
|
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
|
use halo2::pasta::pallas;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
amount::{Amount, NegativeAllowed},
|
amount::{Amount, NegativeAllowed},
|
||||||
block::MAX_BLOCK_BYTES,
|
block::MAX_BLOCK_BYTES,
|
||||||
|
@ -13,14 +22,6 @@ use crate::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
cmp::{Eq, PartialEq},
|
|
||||||
fmt::Debug,
|
|
||||||
io,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A bundle of [`Action`] descriptions and signature data.
|
/// A bundle of [`Action`] descriptions and signature data.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
pub struct ShieldedData {
|
pub struct ShieldedData {
|
||||||
|
@ -32,14 +33,15 @@ pub struct ShieldedData {
|
||||||
pub shared_anchor: tree::Root,
|
pub shared_anchor: tree::Root,
|
||||||
/// The aggregated zk-SNARK proof for all the actions in this transaction.
|
/// The aggregated zk-SNARK proof for all the actions in this transaction.
|
||||||
pub proof: Halo2Proof,
|
pub proof: Halo2Proof,
|
||||||
/// The Orchard Actions.
|
/// The Orchard Actions, in the order they appear in the transaction.
|
||||||
pub actions: AtLeastOne<AuthorizedAction>,
|
pub actions: AtLeastOne<AuthorizedAction>,
|
||||||
/// A signature on the transaction `sighash`.
|
/// A signature on the transaction `sighash`.
|
||||||
pub binding_sig: Signature<Binding>,
|
pub binding_sig: Signature<Binding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShieldedData {
|
impl ShieldedData {
|
||||||
/// Iterate over the [`Action`]s for the [`AuthorizedAction`]s in this transaction.
|
/// Iterate over the [`Action`]s for the [`AuthorizedAction`]s in this
|
||||||
|
/// transaction, in the order they appear in it.
|
||||||
pub fn actions(&self) -> impl Iterator<Item = &Action> {
|
pub fn actions(&self) -> impl Iterator<Item = &Action> {
|
||||||
self.actions.actions()
|
self.actions.actions()
|
||||||
}
|
}
|
||||||
|
@ -55,6 +57,12 @@ impl ShieldedData {
|
||||||
pub fn value_balance(&self) -> Amount<NegativeAllowed> {
|
pub fn value_balance(&self) -> Amount<NegativeAllowed> {
|
||||||
self.value_balance
|
self.value_balance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collect the cm_x's for this transaction, if it contains [`Action`]s with
|
||||||
|
/// outputs, in the order they appear in the transaction.
|
||||||
|
pub fn note_commitments(&self) -> impl Iterator<Item = &pallas::Base> {
|
||||||
|
self.actions().map(|action| &action.cm_x)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AtLeastOne<AuthorizedAction> {
|
impl AtLeastOne<AuthorizedAction> {
|
||||||
|
|
|
@ -111,6 +111,12 @@ impl From<Root> for [u8; 32] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&Root> for [u8; 32] {
|
||||||
|
fn from(root: &Root) -> Self {
|
||||||
|
(*root).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Hash for Root {
|
impl Hash for Root {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
self.0.to_bytes().hash(state)
|
self.0.to_bytes().hash(state)
|
||||||
|
@ -177,15 +183,36 @@ impl From<pallas::Base> for Node {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl serde::Serialize for Node {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.0.to_bytes().serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for Node {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let bytes = <[u8; 32]>::deserialize(deserializer)?;
|
||||||
|
Option::<pallas::Base>::from(pallas::Base::from_bytes(&bytes))
|
||||||
|
.map(Node)
|
||||||
|
.ok_or_else(|| serde::de::Error::custom("invalid Pallas field element"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code, missing_docs)]
|
#[allow(dead_code, missing_docs)]
|
||||||
#[derive(Error, Debug, PartialEq)]
|
#[derive(Error, Debug, PartialEq, Eq)]
|
||||||
pub enum NoteCommitmentTreeError {
|
pub enum NoteCommitmentTreeError {
|
||||||
#[error("The note commitment tree is full")]
|
#[error("The note commitment tree is full")]
|
||||||
FullTree,
|
FullTree,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Orchard Incremental Note Commitment Tree
|
/// Orchard Incremental Note Commitment Tree
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct NoteCommitmentTree {
|
pub struct NoteCommitmentTree {
|
||||||
/// The tree represented as a Frontier.
|
/// The tree represented as a Frontier.
|
||||||
///
|
///
|
||||||
|
|
|
@ -104,7 +104,7 @@ where
|
||||||
pub value_balance: Amount,
|
pub value_balance: Amount,
|
||||||
|
|
||||||
/// A bundle of spends and outputs, containing at least one spend or
|
/// A bundle of spends and outputs, containing at least one spend or
|
||||||
/// output.
|
/// output, in the order they appear in the transaction.
|
||||||
///
|
///
|
||||||
/// In V5 transactions, also contains a shared anchor, if there are any
|
/// In V5 transactions, also contains a shared anchor, if there are any
|
||||||
/// spends.
|
/// spends.
|
||||||
|
@ -154,7 +154,8 @@ where
|
||||||
/// [`Spend`]s in this `TransferData`.
|
/// [`Spend`]s in this `TransferData`.
|
||||||
spends: AtLeastOne<Spend<AnchorV>>,
|
spends: AtLeastOne<Spend<AnchorV>>,
|
||||||
|
|
||||||
/// Maybe some outputs (can be empty).
|
/// Maybe some outputs (can be empty), in the order they appear in the
|
||||||
|
/// transaction.
|
||||||
///
|
///
|
||||||
/// Use the [`ShieldedData::outputs`] method to get an iterator over the
|
/// Use the [`ShieldedData::outputs`] method to get an iterator over the
|
||||||
/// [`Outputs`]s in this `TransferData`.
|
/// [`Outputs`]s in this `TransferData`.
|
||||||
|
@ -167,7 +168,7 @@ where
|
||||||
/// In Transaction::V5, if there are no spends, there must not be a shared
|
/// In Transaction::V5, if there are no spends, there must not be a shared
|
||||||
/// anchor.
|
/// anchor.
|
||||||
JustOutputs {
|
JustOutputs {
|
||||||
/// At least one output.
|
/// At least one output, in the order they appear in the transaction.
|
||||||
///
|
///
|
||||||
/// Use the [`ShieldedData::outputs`] method to get an iterator over the
|
/// Use the [`ShieldedData::outputs`] method to get an iterator over the
|
||||||
/// [`Outputs`]s in this `TransferData`.
|
/// [`Outputs`]s in this `TransferData`.
|
||||||
|
@ -205,7 +206,8 @@ where
|
||||||
self.transfers.spends()
|
self.transfers.spends()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over the [`Output`]s for this transaction.
|
/// Iterate over the [`Output`]s for this transaction, in the order they
|
||||||
|
/// appear in it.
|
||||||
pub fn outputs(&self) -> impl Iterator<Item = &Output> {
|
pub fn outputs(&self) -> impl Iterator<Item = &Output> {
|
||||||
self.transfers.outputs()
|
self.transfers.outputs()
|
||||||
}
|
}
|
||||||
|
@ -225,7 +227,8 @@ where
|
||||||
self.spends().map(|spend| &spend.nullifier)
|
self.spends().map(|spend| &spend.nullifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collect the cm_u's for this transaction, if it contains [`Output`]s.
|
/// Collect the cm_u's for this transaction, if it contains [`Output`]s,
|
||||||
|
/// in the order they appear in the transaction.
|
||||||
pub fn note_commitments(&self) -> impl Iterator<Item = &jubjub::Fq> {
|
pub fn note_commitments(&self) -> impl Iterator<Item = &jubjub::Fq> {
|
||||||
self.outputs().map(|output| &output.cm_u)
|
self.outputs().map(|output| &output.cm_u)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use color_eyre::eyre;
|
||||||
|
use eyre::Result;
|
||||||
use hex::FromHex;
|
use hex::FromHex;
|
||||||
|
|
||||||
use crate::sapling::tests::test_vectors;
|
use crate::block::Block;
|
||||||
use crate::sapling::tree::*;
|
use crate::parameters::NetworkUpgrade;
|
||||||
|
use crate::sapling::{self, tree::*};
|
||||||
|
use crate::serialization::ZcashDeserializeInto;
|
||||||
|
use crate::{parameters::Network, sapling::tests::test_vectors};
|
||||||
|
use zebra_test::vectors::{
|
||||||
|
MAINNET_BLOCKS, MAINNET_FINAL_SAPLING_ROOTS, TESTNET_BLOCKS, TESTNET_FINAL_SAPLING_ROOTS,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_roots() {
|
fn empty_roots() {
|
||||||
|
@ -41,3 +51,83 @@ fn incremental_roots() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn incremental_roots_with_blocks() -> Result<()> {
|
||||||
|
incremental_roots_with_blocks_for_network(Network::Mainnet)?;
|
||||||
|
incremental_roots_with_blocks_for_network(Network::Testnet)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn incremental_roots_with_blocks_for_network(network: Network) -> Result<()> {
|
||||||
|
let (blocks, sapling_roots) = match network {
|
||||||
|
Network::Mainnet => (&*MAINNET_BLOCKS, &*MAINNET_FINAL_SAPLING_ROOTS),
|
||||||
|
Network::Testnet => (&*TESTNET_BLOCKS, &*TESTNET_FINAL_SAPLING_ROOTS),
|
||||||
|
};
|
||||||
|
let height = NetworkUpgrade::Sapling
|
||||||
|
.activation_height(network)
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
|
||||||
|
// Build empty note commitment tree
|
||||||
|
let mut tree = sapling::tree::NoteCommitmentTree::default();
|
||||||
|
|
||||||
|
// Load Sapling activation block
|
||||||
|
let sapling_activation_block = Arc::new(
|
||||||
|
blocks
|
||||||
|
.get(&height)
|
||||||
|
.expect("test vector exists")
|
||||||
|
.zcash_deserialize_into::<Block>()
|
||||||
|
.expect("block is structurally valid"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add note commitments from the Sapling activation block to the tree
|
||||||
|
for transaction in sapling_activation_block.transactions.iter() {
|
||||||
|
for sapling_note_commitment in transaction.sapling_note_commitments() {
|
||||||
|
tree.append(*sapling_note_commitment)
|
||||||
|
.expect("test vector is correct");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if root of the tree of the activation block is correct
|
||||||
|
let sapling_activation_block_root =
|
||||||
|
sapling::tree::Root(**sapling_roots.get(&height).expect("test vector exists"));
|
||||||
|
assert_eq!(sapling_activation_block_root, tree.root());
|
||||||
|
|
||||||
|
// Load the block immediately after Sapling activation (activation + 1)
|
||||||
|
let block_after_sapling_activation = Arc::new(
|
||||||
|
blocks
|
||||||
|
.get(&(height + 1))
|
||||||
|
.expect("test vector exists")
|
||||||
|
.zcash_deserialize_into::<Block>()
|
||||||
|
.expect("block is structurally valid"),
|
||||||
|
);
|
||||||
|
let block_after_sapling_activation_root = sapling::tree::Root(
|
||||||
|
**sapling_roots
|
||||||
|
.get(&(height + 1))
|
||||||
|
.expect("test vector exists"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add note commitments from the block after Sapling activatoin to the tree
|
||||||
|
let mut appended_count = 0;
|
||||||
|
for transaction in block_after_sapling_activation.transactions.iter() {
|
||||||
|
for sapling_note_commitment in transaction.sapling_note_commitments() {
|
||||||
|
tree.append(*sapling_note_commitment)
|
||||||
|
.expect("test vector is correct");
|
||||||
|
appended_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We also want to make sure that sapling_note_commitments() is returning
|
||||||
|
// the commitments in the right order. But this will only be actually tested
|
||||||
|
// if there are more than one note commitment in a block.
|
||||||
|
// In the test vectors this applies only for the block 1 in mainnet,
|
||||||
|
// so we make this explicit in this assert.
|
||||||
|
if network == Network::Mainnet {
|
||||||
|
assert!(appended_count > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if root of the second block is correct
|
||||||
|
assert_eq!(block_after_sapling_activation_root, tree.root());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -147,15 +147,36 @@ impl From<jubjub::Fq> for Node {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl serde::Serialize for Node {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.0.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for Node {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let bytes = <[u8; 32]>::deserialize(deserializer)?;
|
||||||
|
Option::<jubjub::Fq>::from(jubjub::Fq::from_bytes(&bytes))
|
||||||
|
.map(Node::from)
|
||||||
|
.ok_or_else(|| serde::de::Error::custom("invalid JubJub field element"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code, missing_docs)]
|
#[allow(dead_code, missing_docs)]
|
||||||
#[derive(Error, Debug, PartialEq)]
|
#[derive(Error, Debug, PartialEq, Eq)]
|
||||||
pub enum NoteCommitmentTreeError {
|
pub enum NoteCommitmentTreeError {
|
||||||
#[error("The note commitment tree is full")]
|
#[error("The note commitment tree is full")]
|
||||||
FullTree,
|
FullTree,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sapling Incremental Note Commitment Tree.
|
/// Sapling Incremental Note Commitment Tree.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct NoteCommitmentTree {
|
pub struct NoteCommitmentTree {
|
||||||
/// The tree represented as a Frontier.
|
/// The tree represented as a Frontier.
|
||||||
///
|
///
|
||||||
|
|
|
@ -104,10 +104,22 @@ impl From<Root> for [u8; 32] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&[u8; 32]> for Root {
|
||||||
|
fn from(bytes: &[u8; 32]) -> Root {
|
||||||
|
(*bytes).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Root> for [u8; 32] {
|
||||||
|
fn from(root: &Root) -> Self {
|
||||||
|
(*root).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sprout Note Commitment Tree
|
/// Sprout Note Commitment Tree
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
|
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
|
||||||
struct NoteCommitmentTree {
|
pub struct NoteCommitmentTree {
|
||||||
/// The root node of the tree (often used as an anchor).
|
/// The root node of the tree (often used as an anchor).
|
||||||
root: Root,
|
root: Root,
|
||||||
/// The height of the tree (maximum height for Sprout is 29).
|
/// The height of the tree (maximum height for Sprout is 29).
|
||||||
|
@ -164,8 +176,14 @@ impl From<Vec<NoteCommitment>> for NoteCommitmentTree {
|
||||||
impl NoteCommitmentTree {
|
impl NoteCommitmentTree {
|
||||||
/// Get the Jubjub-based Pedersen hash of root node of this merkle tree of
|
/// Get the Jubjub-based Pedersen hash of root node of this merkle tree of
|
||||||
/// commitment notes.
|
/// commitment notes.
|
||||||
pub fn hash(&self) -> [u8; 32] {
|
pub fn root(&self) -> Root {
|
||||||
self.root.0
|
self.root
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a note commitment to the tree.
|
||||||
|
pub fn append(&mut self, _cm: &NoteCommitment) {
|
||||||
|
// TODO: https://github.com/ZcashFoundation/zebra/issues/2485
|
||||||
|
todo!("implement sprout note commitment trees #2485");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,7 +295,7 @@ mod tests {
|
||||||
|
|
||||||
let tree = NoteCommitmentTree::from(leaves.clone());
|
let tree = NoteCommitmentTree::from(leaves.clone());
|
||||||
|
|
||||||
assert_eq!(hex::encode(tree.hash()), roots[i]);
|
assert_eq!(hex::encode(<[u8; 32]>::from(tree.root())), roots[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
//! Transactions and transaction-related structures.
|
//! Transactions and transaction-related structures.
|
||||||
|
|
||||||
|
use halo2::pasta::pallas;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
mod hash;
|
mod hash;
|
||||||
|
@ -590,6 +591,36 @@ impl Transaction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the note commitments in this transaction, regardless of version.
|
||||||
|
pub fn sapling_note_commitments(&self) -> Box<dyn Iterator<Item = &jubjub::Fq> + '_> {
|
||||||
|
// This function returns a boxed iterator because the different
|
||||||
|
// transaction variants end up having different iterator types
|
||||||
|
match self {
|
||||||
|
// Spends with Groth16 Proofs
|
||||||
|
Transaction::V4 {
|
||||||
|
sapling_shielded_data: Some(sapling_shielded_data),
|
||||||
|
..
|
||||||
|
} => Box::new(sapling_shielded_data.note_commitments()),
|
||||||
|
Transaction::V5 {
|
||||||
|
sapling_shielded_data: Some(sapling_shielded_data),
|
||||||
|
..
|
||||||
|
} => Box::new(sapling_shielded_data.note_commitments()),
|
||||||
|
|
||||||
|
// No Spends
|
||||||
|
Transaction::V1 { .. }
|
||||||
|
| Transaction::V2 { .. }
|
||||||
|
| Transaction::V3 { .. }
|
||||||
|
| Transaction::V4 {
|
||||||
|
sapling_shielded_data: None,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| Transaction::V5 {
|
||||||
|
sapling_shielded_data: None,
|
||||||
|
..
|
||||||
|
} => Box::new(std::iter::empty()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return if the transaction has any Sapling shielded data.
|
/// Return if the transaction has any Sapling shielded data.
|
||||||
pub fn has_sapling_shielded_data(&self) -> bool {
|
pub fn has_sapling_shielded_data(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
@ -643,6 +674,15 @@ impl Transaction {
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the note commitments in this transaction, if there are any,
|
||||||
|
/// regardless of version.
|
||||||
|
pub fn orchard_note_commitments(&self) -> impl Iterator<Item = &pallas::Base> {
|
||||||
|
self.orchard_shielded_data()
|
||||||
|
.into_iter()
|
||||||
|
.map(orchard::ShieldedData::note_commitments)
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
/// Access the [`orchard::Flags`] in this transaction, if there is any,
|
/// Access the [`orchard::Flags`] in this transaction, if there is any,
|
||||||
/// regardless of version.
|
/// regardless of version.
|
||||||
pub fn orchard_flags(&self) -> Option<orchard::shielded_data::Flags> {
|
pub fn orchard_flags(&self) -> Option<orchard::shielded_data::Flags> {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::{
|
use crate::{
|
||||||
amount::{Amount, Error},
|
amount::{Amount, Error},
|
||||||
primitives::{ed25519, ZkSnarkProof},
|
primitives::{ed25519, ZkSnarkProof},
|
||||||
sprout::{JoinSplit, Nullifier},
|
sprout::{self, JoinSplit, Nullifier},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A bundle of [`JoinSplit`] descriptions and signature data.
|
/// A bundle of [`JoinSplit`] descriptions and signature data.
|
||||||
|
@ -16,7 +16,8 @@ use crate::{
|
||||||
/// JoinSplit data.
|
/// JoinSplit data.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct JoinSplitData<P: ZkSnarkProof> {
|
pub struct JoinSplitData<P: ZkSnarkProof> {
|
||||||
/// The first JoinSplit description, using proofs of type `P`.
|
/// The first JoinSplit description in the transaction,
|
||||||
|
/// using proofs of type `P`.
|
||||||
///
|
///
|
||||||
/// Storing this separately from `rest` ensures that it is impossible
|
/// Storing this separately from `rest` ensures that it is impossible
|
||||||
/// to construct an invalid `JoinSplitData` with no `JoinSplit`s.
|
/// to construct an invalid `JoinSplitData` with no `JoinSplit`s.
|
||||||
|
@ -29,7 +30,8 @@ pub struct JoinSplitData<P: ZkSnarkProof> {
|
||||||
deserialize = "JoinSplit<P>: Deserialize<'de>"
|
deserialize = "JoinSplit<P>: Deserialize<'de>"
|
||||||
))]
|
))]
|
||||||
pub first: JoinSplit<P>,
|
pub first: JoinSplit<P>,
|
||||||
/// The rest of the JoinSplit descriptions, using proofs of type `P`.
|
/// The rest of the JoinSplit descriptions, using proofs of type `P`,
|
||||||
|
/// in the order they appear in the transaction.
|
||||||
///
|
///
|
||||||
/// The [`JoinSplitData::joinsplits`] method provides an iterator over
|
/// The [`JoinSplitData::joinsplits`] method provides an iterator over
|
||||||
/// all `JoinSplit`s.
|
/// all `JoinSplit`s.
|
||||||
|
@ -45,7 +47,8 @@ pub struct JoinSplitData<P: ZkSnarkProof> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: ZkSnarkProof> JoinSplitData<P> {
|
impl<P: ZkSnarkProof> JoinSplitData<P> {
|
||||||
/// Iterate over the [`JoinSplit`]s in `self`.
|
/// Iterate over the [`JoinSplit`]s in `self`, in the order they appear
|
||||||
|
/// in the transaction.
|
||||||
pub fn joinsplits(&self) -> impl Iterator<Item = &JoinSplit<P>> {
|
pub fn joinsplits(&self) -> impl Iterator<Item = &JoinSplit<P>> {
|
||||||
std::iter::once(&self.first).chain(self.rest.iter())
|
std::iter::once(&self.first).chain(self.rest.iter())
|
||||||
}
|
}
|
||||||
|
@ -64,4 +67,11 @@ impl<P: ZkSnarkProof> JoinSplitData<P> {
|
||||||
.flat_map(|j| j.vpub_old.constrain() - j.vpub_new.constrain()?)
|
.flat_map(|j| j.vpub_old.constrain() - j.vpub_new.constrain()?)
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collect the Sprout note commitments for this transaction, if it contains [`Output`]s,
|
||||||
|
/// in the order they appear in the transaction.
|
||||||
|
pub fn note_commitments(&self) -> impl Iterator<Item = &sprout::commitment::NoteCommitment> {
|
||||||
|
self.joinsplits()
|
||||||
|
.flat_map(|joinsplit| &joinsplit.commitments)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ hex = "0.4.3"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
serde = { version = "1", features = ["serde_derive"] }
|
serde = { version = "1", features = ["serde_derive"] }
|
||||||
|
bincode = "1"
|
||||||
|
|
||||||
futures = "0.3.15"
|
futures = "0.3.15"
|
||||||
metrics = "0.13.0-alpha.8"
|
metrics = "0.13.0-alpha.8"
|
||||||
|
@ -28,6 +29,9 @@ rocksdb = "0.16.0"
|
||||||
tempdir = "0.3.7"
|
tempdir = "0.3.7"
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
rlimit = "0.5.4"
|
rlimit = "0.5.4"
|
||||||
|
# TODO: this crate is not maintained anymore. Replace it?
|
||||||
|
# https://github.com/ZcashFoundation/zebra/issues/2523
|
||||||
|
multiset = "0.0.5"
|
||||||
|
|
||||||
proptest = { version = "0.10.1", optional = true }
|
proptest = { version = "0.10.1", optional = true }
|
||||||
zebra-test = { path = "../zebra-test/", optional = true }
|
zebra-test = { path = "../zebra-test/", optional = true }
|
||||||
|
@ -42,6 +46,10 @@ itertools = "0.10.1"
|
||||||
spandoc = "0.2"
|
spandoc = "0.2"
|
||||||
tempdir = "0.3.7"
|
tempdir = "0.3.7"
|
||||||
tokio = { version = "0.3.6", features = ["full"] }
|
tokio = { version = "0.3.6", features = ["full"] }
|
||||||
|
# TODO: replace w/ crate version when released: https://github.com/ZcashFoundation/zebra/issues/2083
|
||||||
|
# Note: if updating this, also update the workspace Cargo.toml to match.
|
||||||
|
halo2 = { git = "https://github.com/zcash/halo2.git", rev = "236115917df9db45282fec24d1e1e36f275f71ab" }
|
||||||
|
jubjub = "0.7.0"
|
||||||
|
|
||||||
proptest = "0.10.1"
|
proptest = "0.10.1"
|
||||||
proptest-derive = "0.3"
|
proptest-derive = "0.3"
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub const MIN_TRANSPARENT_COINBASE_MATURITY: u32 = 100;
|
||||||
pub const MAX_BLOCK_REORG_HEIGHT: u32 = MIN_TRANSPARENT_COINBASE_MATURITY - 1;
|
pub const MAX_BLOCK_REORG_HEIGHT: u32 = MIN_TRANSPARENT_COINBASE_MATURITY - 1;
|
||||||
|
|
||||||
/// The database format version, incremented each time the database format changes.
|
/// The database format version, incremented each time the database format changes.
|
||||||
pub const DATABASE_FORMAT_VERSION: u32 = 5;
|
pub const DATABASE_FORMAT_VERSION: u32 = 6;
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
|
@ -146,6 +146,12 @@ pub enum ValidateContextError {
|
||||||
transaction_hash: zebra_chain::transaction::Hash,
|
transaction_hash: zebra_chain::transaction::Hash,
|
||||||
in_finalized_state: bool,
|
in_finalized_state: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[error("error in Sapling note commitment tree")]
|
||||||
|
SaplingNoteCommitmentTreeError(#[from] zebra_chain::sapling::tree::NoteCommitmentTreeError),
|
||||||
|
|
||||||
|
#[error("error in Orchard note commitment tree")]
|
||||||
|
OrchardNoteCommitmentTreeError(#[from] zebra_chain::orchard::tree::NoteCommitmentTreeError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for creating the corresponding duplicate nullifier error from a nullifier.
|
/// Trait for creating the corresponding duplicate nullifier error from a nullifier.
|
||||||
|
|
|
@ -50,6 +50,16 @@ impl FinalizedState {
|
||||||
rocksdb::ColumnFamilyDescriptor::new("sprout_nullifiers", db_options.clone()),
|
rocksdb::ColumnFamilyDescriptor::new("sprout_nullifiers", db_options.clone()),
|
||||||
rocksdb::ColumnFamilyDescriptor::new("sapling_nullifiers", db_options.clone()),
|
rocksdb::ColumnFamilyDescriptor::new("sapling_nullifiers", db_options.clone()),
|
||||||
rocksdb::ColumnFamilyDescriptor::new("orchard_nullifiers", db_options.clone()),
|
rocksdb::ColumnFamilyDescriptor::new("orchard_nullifiers", db_options.clone()),
|
||||||
|
rocksdb::ColumnFamilyDescriptor::new("sapling_anchors", db_options.clone()),
|
||||||
|
rocksdb::ColumnFamilyDescriptor::new("orchard_anchors", db_options.clone()),
|
||||||
|
rocksdb::ColumnFamilyDescriptor::new(
|
||||||
|
"sapling_note_commitment_tree",
|
||||||
|
db_options.clone(),
|
||||||
|
),
|
||||||
|
rocksdb::ColumnFamilyDescriptor::new(
|
||||||
|
"orchard_note_commitment_tree",
|
||||||
|
db_options.clone(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
let db_result = rocksdb::DB::open_cf_descriptors(&db_options, &path, column_families);
|
let db_result = rocksdb::DB::open_cf_descriptors(&db_options, &path, column_families);
|
||||||
|
|
||||||
|
@ -196,15 +206,26 @@ impl FinalizedState {
|
||||||
transaction_hashes,
|
transaction_hashes,
|
||||||
} = finalized;
|
} = finalized;
|
||||||
|
|
||||||
|
let finalized_tip_height = self.finalized_tip_height();
|
||||||
|
|
||||||
let hash_by_height = self.db.cf_handle("hash_by_height").unwrap();
|
let hash_by_height = self.db.cf_handle("hash_by_height").unwrap();
|
||||||
let height_by_hash = self.db.cf_handle("height_by_hash").unwrap();
|
let height_by_hash = self.db.cf_handle("height_by_hash").unwrap();
|
||||||
let block_by_height = self.db.cf_handle("block_by_height").unwrap();
|
let block_by_height = self.db.cf_handle("block_by_height").unwrap();
|
||||||
let tx_by_hash = self.db.cf_handle("tx_by_hash").unwrap();
|
let tx_by_hash = self.db.cf_handle("tx_by_hash").unwrap();
|
||||||
let utxo_by_outpoint = self.db.cf_handle("utxo_by_outpoint").unwrap();
|
let utxo_by_outpoint = self.db.cf_handle("utxo_by_outpoint").unwrap();
|
||||||
|
|
||||||
let sprout_nullifiers = self.db.cf_handle("sprout_nullifiers").unwrap();
|
let sprout_nullifiers = self.db.cf_handle("sprout_nullifiers").unwrap();
|
||||||
let sapling_nullifiers = self.db.cf_handle("sapling_nullifiers").unwrap();
|
let sapling_nullifiers = self.db.cf_handle("sapling_nullifiers").unwrap();
|
||||||
let orchard_nullifiers = self.db.cf_handle("orchard_nullifiers").unwrap();
|
let orchard_nullifiers = self.db.cf_handle("orchard_nullifiers").unwrap();
|
||||||
|
|
||||||
|
let sapling_anchors = self.db.cf_handle("sapling_anchors").unwrap();
|
||||||
|
let orchard_anchors = self.db.cf_handle("orchard_anchors").unwrap();
|
||||||
|
|
||||||
|
let sapling_note_commitment_tree_cf =
|
||||||
|
self.db.cf_handle("sapling_note_commitment_tree").unwrap();
|
||||||
|
let orchard_note_commitment_tree_cf =
|
||||||
|
self.db.cf_handle("orchard_note_commitment_tree").unwrap();
|
||||||
|
|
||||||
// Assert that callers (including unit tests) get the chain order correct
|
// Assert that callers (including unit tests) get the chain order correct
|
||||||
if self.is_empty(hash_by_height) {
|
if self.is_empty(hash_by_height) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -220,9 +241,7 @@ impl FinalizedState {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
self.finalized_tip_height()
|
finalized_tip_height.expect("state must have a genesis block committed") + 1,
|
||||||
.expect("state must have a genesis block committed")
|
|
||||||
+ 1,
|
|
||||||
Some(height),
|
Some(height),
|
||||||
"committed block height must be 1 more than the finalized tip height, source: {}",
|
"committed block height must be 1 more than the finalized tip height, source: {}",
|
||||||
source,
|
source,
|
||||||
|
@ -236,6 +255,11 @@ impl FinalizedState {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read the current note commitment trees. If there are no blocks in the
|
||||||
|
// state, these will contain the empty trees.
|
||||||
|
let mut sapling_note_commitment_tree = self.sapling_note_commitment_tree();
|
||||||
|
let mut orchard_note_commitment_tree = self.orchard_note_commitment_tree();
|
||||||
|
|
||||||
// We use a closure so we can use an early return for control flow in
|
// We use a closure so we can use an early return for control flow in
|
||||||
// the genesis case
|
// the genesis case
|
||||||
let prepare_commit = || -> rocksdb::WriteBatch {
|
let prepare_commit = || -> rocksdb::WriteBatch {
|
||||||
|
@ -246,12 +270,24 @@ impl FinalizedState {
|
||||||
batch.zs_insert(height_by_hash, hash, height);
|
batch.zs_insert(height_by_hash, hash, height);
|
||||||
batch.zs_insert(block_by_height, height, &block);
|
batch.zs_insert(block_by_height, height, &block);
|
||||||
|
|
||||||
// TODO: sprout and sapling anchors (per block)
|
|
||||||
|
|
||||||
// "A transaction MUST NOT spend an output of the genesis block coinbase transaction.
|
// "A transaction MUST NOT spend an output of the genesis block coinbase transaction.
|
||||||
// (There is one such zero-valued output, on each of Testnet and Mainnet .)"
|
// (There is one such zero-valued output, on each of Testnet and Mainnet .)"
|
||||||
// https://zips.z.cash/protocol/protocol.pdf#txnconsensus
|
// https://zips.z.cash/protocol/protocol.pdf#txnconsensus
|
||||||
if block.header.previous_block_hash == GENESIS_PREVIOUS_BLOCK_HASH {
|
if block.header.previous_block_hash == GENESIS_PREVIOUS_BLOCK_HASH {
|
||||||
|
// Insert empty note commitment trees. Note that these can't be
|
||||||
|
// used too early (e.g. the Orchard tree before Nu5 activates)
|
||||||
|
// since the block validation will make sure only appropriate
|
||||||
|
// transactions are allowed in a block.
|
||||||
|
batch.zs_insert(
|
||||||
|
sapling_note_commitment_tree_cf,
|
||||||
|
height,
|
||||||
|
sapling_note_commitment_tree,
|
||||||
|
);
|
||||||
|
batch.zs_insert(
|
||||||
|
orchard_note_commitment_tree_cf,
|
||||||
|
height,
|
||||||
|
orchard_note_commitment_tree,
|
||||||
|
);
|
||||||
return batch;
|
return batch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,7 +333,38 @@ impl FinalizedState {
|
||||||
for orchard_nullifier in transaction.orchard_nullifiers() {
|
for orchard_nullifier in transaction.orchard_nullifiers() {
|
||||||
batch.zs_insert(orchard_nullifiers, orchard_nullifier, ());
|
batch.zs_insert(orchard_nullifiers, orchard_nullifier, ());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for sapling_note_commitment in transaction.sapling_note_commitments() {
|
||||||
|
sapling_note_commitment_tree
|
||||||
|
.append(*sapling_note_commitment)
|
||||||
|
.expect("must work since it was already appended before in the non-finalized state");
|
||||||
}
|
}
|
||||||
|
for orchard_note_commitment in transaction.orchard_note_commitments() {
|
||||||
|
orchard_note_commitment_tree
|
||||||
|
.append(*orchard_note_commitment)
|
||||||
|
.expect("must work since it was already appended before in the non-finalized state");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the new anchors and index them
|
||||||
|
batch.zs_insert(sapling_anchors, height, sapling_note_commitment_tree.root());
|
||||||
|
batch.zs_insert(orchard_anchors, height, orchard_note_commitment_tree.root());
|
||||||
|
|
||||||
|
// Update the note commitment trees
|
||||||
|
if let Some(h) = finalized_tip_height {
|
||||||
|
batch.zs_delete(sapling_note_commitment_tree_cf, h);
|
||||||
|
batch.zs_delete(orchard_note_commitment_tree_cf, h);
|
||||||
|
}
|
||||||
|
batch.zs_insert(
|
||||||
|
sapling_note_commitment_tree_cf,
|
||||||
|
height,
|
||||||
|
sapling_note_commitment_tree,
|
||||||
|
);
|
||||||
|
batch.zs_insert(
|
||||||
|
orchard_note_commitment_tree_cf,
|
||||||
|
height,
|
||||||
|
orchard_note_commitment_tree,
|
||||||
|
);
|
||||||
|
|
||||||
batch
|
batch
|
||||||
};
|
};
|
||||||
|
@ -408,6 +475,34 @@ impl FinalizedState {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the Sapling note commitment tree of the finalized tip
|
||||||
|
/// or the empty tree if the state is empty.
|
||||||
|
pub fn sapling_note_commitment_tree(&self) -> sapling::tree::NoteCommitmentTree {
|
||||||
|
let height = match self.finalized_tip_height() {
|
||||||
|
Some(h) => h,
|
||||||
|
None => return Default::default(),
|
||||||
|
};
|
||||||
|
let sapling_note_commitment_tree =
|
||||||
|
self.db.cf_handle("sapling_note_commitment_tree").unwrap();
|
||||||
|
self.db
|
||||||
|
.zs_get(sapling_note_commitment_tree, &height)
|
||||||
|
.expect("note commitment tree must exist if there is a finalized tip")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Orchard note commitment tree of the finalized tip
|
||||||
|
/// or the empty tree if the state is empty.
|
||||||
|
pub fn orchard_note_commitment_tree(&self) -> orchard::tree::NoteCommitmentTree {
|
||||||
|
let height = match self.finalized_tip_height() {
|
||||||
|
Some(h) => h,
|
||||||
|
None => return Default::default(),
|
||||||
|
};
|
||||||
|
let orchard_note_commitment_tree =
|
||||||
|
self.db.cf_handle("orchard_note_commitment_tree").unwrap();
|
||||||
|
self.db
|
||||||
|
.zs_get(orchard_note_commitment_tree, &height)
|
||||||
|
.expect("note commitment tree must exist if there is a finalized tip")
|
||||||
|
}
|
||||||
|
|
||||||
/// If the database is `ephemeral`, delete it.
|
/// If the database is `ephemeral`, delete it.
|
||||||
fn delete_ephemeral(&self) {
|
fn delete_ephemeral(&self) {
|
||||||
if self.ephemeral {
|
if self.ephemeral {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
//! Module defining exactly how to move types in and out of rocksdb
|
//! Module defining exactly how to move types in and out of rocksdb
|
||||||
use std::{convert::TryInto, fmt::Debug, sync::Arc};
|
use std::{convert::TryInto, fmt::Debug, sync::Arc};
|
||||||
|
|
||||||
|
use bincode::Options;
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block,
|
block,
|
||||||
block::Block,
|
block::Block,
|
||||||
|
@ -232,6 +233,65 @@ impl IntoDisk for transparent::OutPoint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntoDisk for sapling::tree::Root {
|
||||||
|
type Bytes = [u8; 32];
|
||||||
|
|
||||||
|
fn as_bytes(&self) -> Self::Bytes {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoDisk for orchard::tree::Root {
|
||||||
|
type Bytes = [u8; 32];
|
||||||
|
|
||||||
|
fn as_bytes(&self) -> Self::Bytes {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following implementations for the note commitment trees use `serde` and
|
||||||
|
// `bincode` because currently the inner Merkle tree frontier (from
|
||||||
|
// `incrementalmerkletree`) only supports `serde` for serialization. `bincode`
|
||||||
|
// was chosen because it is small and fast. We explicitly use `DefaultOptions`
|
||||||
|
// in particular to disallow trailing bytes; see
|
||||||
|
// https://docs.rs/bincode/1.3.3/bincode/config/index.html#options-struct-vs-bincode-functions
|
||||||
|
|
||||||
|
impl IntoDisk for sapling::tree::NoteCommitmentTree {
|
||||||
|
type Bytes = Vec<u8>;
|
||||||
|
|
||||||
|
fn as_bytes(&self) -> Self::Bytes {
|
||||||
|
bincode::DefaultOptions::new()
|
||||||
|
.serialize(self)
|
||||||
|
.expect("serialization to vec doesn't fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromDisk for sapling::tree::NoteCommitmentTree {
|
||||||
|
fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
|
||||||
|
bincode::DefaultOptions::new()
|
||||||
|
.deserialize(bytes.as_ref())
|
||||||
|
.expect("deserialization format should match the serialization format used by IntoDisk")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoDisk for orchard::tree::NoteCommitmentTree {
|
||||||
|
type Bytes = Vec<u8>;
|
||||||
|
|
||||||
|
fn as_bytes(&self) -> Self::Bytes {
|
||||||
|
bincode::DefaultOptions::new()
|
||||||
|
.serialize(self)
|
||||||
|
.expect("serialization to vec doesn't fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromDisk for orchard::tree::NoteCommitmentTree {
|
||||||
|
fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
|
||||||
|
bincode::DefaultOptions::new()
|
||||||
|
.deserialize(bytes.as_ref())
|
||||||
|
.expect("deserialization format should match the serialization format used by IntoDisk")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper trait for inserting (Key, Value) pairs into rocksdb with a consistently
|
/// Helper trait for inserting (Key, Value) pairs into rocksdb with a consistently
|
||||||
/// defined format
|
/// defined format
|
||||||
pub trait DiskSerialize {
|
pub trait DiskSerialize {
|
||||||
|
@ -241,6 +301,11 @@ pub trait DiskSerialize {
|
||||||
where
|
where
|
||||||
K: IntoDisk + Debug,
|
K: IntoDisk + Debug,
|
||||||
V: IntoDisk;
|
V: IntoDisk;
|
||||||
|
|
||||||
|
/// Remove the given key form rocksdb column family if it exists.
|
||||||
|
fn zs_delete<K>(&mut self, cf: &rocksdb::ColumnFamily, key: K)
|
||||||
|
where
|
||||||
|
K: IntoDisk + Debug;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiskSerialize for rocksdb::WriteBatch {
|
impl DiskSerialize for rocksdb::WriteBatch {
|
||||||
|
@ -253,6 +318,14 @@ impl DiskSerialize for rocksdb::WriteBatch {
|
||||||
let value_bytes = value.as_bytes();
|
let value_bytes = value.as_bytes();
|
||||||
self.put_cf(cf, key_bytes, value_bytes);
|
self.put_cf(cf, key_bytes, value_bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn zs_delete<K>(&mut self, cf: &rocksdb::ColumnFamily, key: K)
|
||||||
|
where
|
||||||
|
K: IntoDisk + Debug,
|
||||||
|
{
|
||||||
|
let key_bytes = key.as_bytes();
|
||||||
|
self.delete_cf(cf, key_bytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper trait for retrieving values from rocksdb column familys with a consistently
|
/// Helper trait for retrieving values from rocksdb column familys with a consistently
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
mod prop;
|
mod prop;
|
||||||
|
mod vectors;
|
||||||
|
|
|
@ -12,7 +12,7 @@ use crate::{
|
||||||
ContextuallyValidBlock,
|
ContextuallyValidBlock,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES: u32 = 16;
|
const DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES: u32 = 1;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blocks_with_v5_transactions() -> Result<()> {
|
fn blocks_with_v5_transactions() -> Result<()> {
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
use halo2::arithmetic::FieldExt;
|
||||||
|
use halo2::pasta::pallas;
|
||||||
|
use hex::FromHex;
|
||||||
|
|
||||||
|
use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk};
|
||||||
|
use zebra_chain::{orchard, sapling};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sapling_note_commitment_tree_serialization() {
|
||||||
|
zebra_test::init();
|
||||||
|
|
||||||
|
let mut incremental_tree = sapling::tree::NoteCommitmentTree::default();
|
||||||
|
|
||||||
|
// Some commitments from zebra-chain/src/sapling/tests/test_vectors.rs
|
||||||
|
let hex_commitments = [
|
||||||
|
"b02310f2e087e55bfd07ef5e242e3b87ee5d00c9ab52f61e6bd42542f93a6f55",
|
||||||
|
"225747f3b5d5dab4e5a424f81f85c904ff43286e0f3fd07ef0b8c6a627b11458",
|
||||||
|
"7c3ea01a6e3a3d90cf59cd789e467044b5cd78eb2c84cc6816f960746d0e036c",
|
||||||
|
];
|
||||||
|
|
||||||
|
for cm_u_hex in hex_commitments {
|
||||||
|
let bytes = <[u8; 32]>::from_hex(cm_u_hex).unwrap();
|
||||||
|
|
||||||
|
let cm_u = jubjub::Fq::from_bytes(&bytes).unwrap();
|
||||||
|
incremental_tree.append(cm_u).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test vector was generated by the code itself.
|
||||||
|
// The purpose of this test is to make sure the serialization format does
|
||||||
|
// not change by accident.
|
||||||
|
let expected_serialized_tree_hex = "0102007c3ea01a6e3a3d90cf59cd789e467044b5cd78eb2c84cc6816f960746d0e036c0162324ff2c329e99193a74d28a585a3c167a93bf41a255135529c913bd9b1e666";
|
||||||
|
let serialized_tree = incremental_tree.as_bytes();
|
||||||
|
assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex);
|
||||||
|
|
||||||
|
let deserialized_tree = sapling::tree::NoteCommitmentTree::from_bytes(serialized_tree);
|
||||||
|
|
||||||
|
assert_eq!(incremental_tree.root(), deserialized_tree.root());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn orchard_note_commitment_tree_serialization() {
|
||||||
|
zebra_test::init();
|
||||||
|
|
||||||
|
let mut incremental_tree = orchard::tree::NoteCommitmentTree::default();
|
||||||
|
|
||||||
|
// Some commitments from zebra-chain/src/orchard/tests/tree.rs
|
||||||
|
let commitments = [
|
||||||
|
[
|
||||||
|
0x68, 0x13, 0x5c, 0xf4, 0x99, 0x33, 0x22, 0x90, 0x99, 0xa4, 0x4e, 0xc9, 0x9a, 0x75,
|
||||||
|
0xe1, 0xe1, 0xcb, 0x46, 0x40, 0xf9, 0xb5, 0xbd, 0xec, 0x6b, 0x32, 0x23, 0x85, 0x6f,
|
||||||
|
0xea, 0x16, 0x39, 0x0a,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0x78, 0x31, 0x50, 0x08, 0xfb, 0x29, 0x98, 0xb4, 0x30, 0xa5, 0x73, 0x1d, 0x67, 0x26,
|
||||||
|
0x20, 0x7d, 0xc0, 0xf0, 0xec, 0x81, 0xea, 0x64, 0xaf, 0x5c, 0xf6, 0x12, 0x95, 0x69,
|
||||||
|
0x01, 0xe7, 0x2f, 0x0e,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0xee, 0x94, 0x88, 0x05, 0x3a, 0x30, 0xc5, 0x96, 0xb4, 0x30, 0x14, 0x10, 0x5d, 0x34,
|
||||||
|
0x77, 0xe6, 0xf5, 0x78, 0xc8, 0x92, 0x40, 0xd1, 0xd1, 0xee, 0x17, 0x43, 0xb7, 0x7b,
|
||||||
|
0xb6, 0xad, 0xc4, 0x0a,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
for cm_x_bytes in &commitments {
|
||||||
|
let cm_x = pallas::Base::from_bytes(cm_x_bytes).unwrap();
|
||||||
|
incremental_tree.append(cm_x).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test vector was generated by the code itself.
|
||||||
|
// The purpose of this test is to make sure the serialization format does
|
||||||
|
// not change by accident.
|
||||||
|
let expected_serialized_tree_hex = "010200ee9488053a30c596b43014105d3477e6f578c89240d1d1ee1743b77bb6adc40a01a34b69a4e4d9ccf954d46e5da1004d361a5497f511aeb4d481d23c0be1778133";
|
||||||
|
let serialized_tree = incremental_tree.as_bytes();
|
||||||
|
assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex);
|
||||||
|
|
||||||
|
let deserialized_tree = orchard::tree::NoteCommitmentTree::from_bytes(serialized_tree);
|
||||||
|
|
||||||
|
assert_eq!(incremental_tree.root(), deserialized_tree.root());
|
||||||
|
}
|
|
@ -14,13 +14,15 @@ use std::{collections::BTreeSet, mem, ops::Deref, sync::Arc};
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::{self, Block},
|
block::{self, Block},
|
||||||
|
orchard,
|
||||||
parameters::Network,
|
parameters::Network,
|
||||||
|
sapling,
|
||||||
transaction::{self, Transaction},
|
transaction::{self, Transaction},
|
||||||
transparent,
|
transparent,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use zebra_chain::{orchard, sapling, sprout};
|
use zebra_chain::sprout;
|
||||||
|
|
||||||
use crate::{FinalizedBlock, HashOrHeight, PreparedBlock, ValidateContextError};
|
use crate::{FinalizedBlock, HashOrHeight, PreparedBlock, ValidateContextError};
|
||||||
|
|
||||||
|
@ -123,7 +125,11 @@ impl NonFinalizedState {
|
||||||
let parent_hash = prepared.block.header.previous_block_hash;
|
let parent_hash = prepared.block.header.previous_block_hash;
|
||||||
let (height, hash) = (prepared.height, prepared.hash);
|
let (height, hash) = (prepared.height, prepared.hash);
|
||||||
|
|
||||||
let parent_chain = self.parent_chain(parent_hash)?;
|
let parent_chain = self.parent_chain(
|
||||||
|
parent_hash,
|
||||||
|
finalized_state.sapling_note_commitment_tree(),
|
||||||
|
finalized_state.orchard_note_commitment_tree(),
|
||||||
|
)?;
|
||||||
|
|
||||||
// We might have taken a chain, so all validation must happen within
|
// We might have taken a chain, so all validation must happen within
|
||||||
// validate_and_commit, so that the chain is restored correctly.
|
// validate_and_commit, so that the chain is restored correctly.
|
||||||
|
@ -154,7 +160,10 @@ impl NonFinalizedState {
|
||||||
prepared: PreparedBlock,
|
prepared: PreparedBlock,
|
||||||
finalized_state: &FinalizedState,
|
finalized_state: &FinalizedState,
|
||||||
) -> Result<(), ValidateContextError> {
|
) -> Result<(), ValidateContextError> {
|
||||||
let chain = Chain::default();
|
let chain = Chain::new(
|
||||||
|
finalized_state.sapling_note_commitment_tree(),
|
||||||
|
finalized_state.orchard_note_commitment_tree(),
|
||||||
|
);
|
||||||
let (height, hash) = (prepared.height, prepared.hash);
|
let (height, hash) = (prepared.height, prepared.hash);
|
||||||
|
|
||||||
// if the block is invalid, drop the newly created chain fork
|
// if the block is invalid, drop the newly created chain fork
|
||||||
|
@ -345,9 +354,14 @@ impl NonFinalizedState {
|
||||||
///
|
///
|
||||||
/// The chain can be an existing chain in the non-finalized state or a freshly
|
/// The chain can be an existing chain in the non-finalized state or a freshly
|
||||||
/// created fork, if needed.
|
/// created fork, if needed.
|
||||||
|
///
|
||||||
|
/// The note commitment trees must be the trees of the finalized tip.
|
||||||
|
/// They are used to recreate the trees if a fork is needed.
|
||||||
fn parent_chain(
|
fn parent_chain(
|
||||||
&mut self,
|
&mut self,
|
||||||
parent_hash: block::Hash,
|
parent_hash: block::Hash,
|
||||||
|
sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree,
|
||||||
|
orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree,
|
||||||
) -> Result<Box<Chain>, ValidateContextError> {
|
) -> Result<Box<Chain>, ValidateContextError> {
|
||||||
match self.take_chain_if(|chain| chain.non_finalized_tip_hash() == parent_hash) {
|
match self.take_chain_if(|chain| chain.non_finalized_tip_hash() == parent_hash) {
|
||||||
// An existing chain in the non-finalized state
|
// An existing chain in the non-finalized state
|
||||||
|
@ -356,7 +370,15 @@ impl NonFinalizedState {
|
||||||
None => Ok(Box::new(
|
None => Ok(Box::new(
|
||||||
self.chain_set
|
self.chain_set
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|chain| chain.fork(parent_hash).transpose())
|
.find_map(|chain| {
|
||||||
|
chain
|
||||||
|
.fork(
|
||||||
|
parent_hash,
|
||||||
|
sapling_note_commitment_tree.clone(),
|
||||||
|
orchard_note_commitment_tree.clone(),
|
||||||
|
)
|
||||||
|
.transpose()
|
||||||
|
})
|
||||||
.expect(
|
.expect(
|
||||||
"commit_block is only called with blocks that are ready to be commited",
|
"commit_block is only called with blocks that are ready to be commited",
|
||||||
)?,
|
)?,
|
||||||
|
|
|
@ -4,6 +4,7 @@ use std::{
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use multiset::HashMultiSet;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
|
@ -13,7 +14,7 @@ use zebra_chain::{
|
||||||
|
|
||||||
use crate::{service::check, ContextuallyValidBlock, PreparedBlock, ValidateContextError};
|
use crate::{service::check, ContextuallyValidBlock, PreparedBlock, ValidateContextError};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Chain {
|
pub struct Chain {
|
||||||
/// The contextually valid blocks which form this non-finalized partial chain, in height order.
|
/// The contextually valid blocks which form this non-finalized partial chain, in height order.
|
||||||
pub(crate) blocks: BTreeMap<block::Height, ContextuallyValidBlock>,
|
pub(crate) blocks: BTreeMap<block::Height, ContextuallyValidBlock>,
|
||||||
|
@ -32,20 +33,25 @@ pub struct Chain {
|
||||||
/// including those created by earlier transactions or blocks in the chain.
|
/// including those created by earlier transactions or blocks in the chain.
|
||||||
pub(crate) spent_utxos: HashSet<transparent::OutPoint>,
|
pub(crate) spent_utxos: HashSet<transparent::OutPoint>,
|
||||||
|
|
||||||
/// The sprout anchors created by `blocks`.
|
/// The Sapling note commitment tree of the tip of this Chain.
|
||||||
///
|
pub(super) sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree,
|
||||||
/// TODO: does this include intersitial anchors?
|
/// The Orchard note commitment tree of the tip of this Chain.
|
||||||
pub(super) sprout_anchors: HashSet<sprout::tree::Root>,
|
pub(super) orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree,
|
||||||
/// The sapling anchors created by `blocks`.
|
|
||||||
pub(super) sapling_anchors: HashSet<sapling::tree::Root>,
|
|
||||||
/// The orchard anchors created by `blocks`.
|
|
||||||
pub(super) orchard_anchors: HashSet<orchard::tree::Root>,
|
|
||||||
|
|
||||||
/// The sprout nullifiers revealed by `blocks`.
|
/// The Sapling anchors created by `blocks`.
|
||||||
|
pub(super) sapling_anchors: HashMultiSet<sapling::tree::Root>,
|
||||||
|
/// The Sapling anchors created by each block in the chain.
|
||||||
|
pub(super) sapling_anchors_by_height: BTreeMap<block::Height, sapling::tree::Root>,
|
||||||
|
/// The Orchard anchors created by `blocks`.
|
||||||
|
pub(super) orchard_anchors: HashMultiSet<orchard::tree::Root>,
|
||||||
|
/// The Orchard anchors created by each block in the chain.
|
||||||
|
pub(super) orchard_anchors_by_height: BTreeMap<block::Height, orchard::tree::Root>,
|
||||||
|
|
||||||
|
/// The Sprout nullifiers revealed by `blocks`.
|
||||||
pub(super) sprout_nullifiers: HashSet<sprout::Nullifier>,
|
pub(super) sprout_nullifiers: HashSet<sprout::Nullifier>,
|
||||||
/// The sapling nullifiers revealed by `blocks`.
|
/// The Sapling nullifiers revealed by `blocks`.
|
||||||
pub(super) sapling_nullifiers: HashSet<sapling::Nullifier>,
|
pub(super) sapling_nullifiers: HashSet<sapling::Nullifier>,
|
||||||
/// The orchard nullifiers revealed by `blocks`.
|
/// The Orchard nullifiers revealed by `blocks`.
|
||||||
pub(super) orchard_nullifiers: HashSet<orchard::Nullifier>,
|
pub(super) orchard_nullifiers: HashSet<orchard::Nullifier>,
|
||||||
|
|
||||||
/// The cumulative work represented by this partial non-finalized chain.
|
/// The cumulative work represented by this partial non-finalized chain.
|
||||||
|
@ -53,6 +59,30 @@ pub struct Chain {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chain {
|
impl Chain {
|
||||||
|
// Create a new Chain with the given note commitment trees.
|
||||||
|
pub(crate) fn new(
|
||||||
|
sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree,
|
||||||
|
orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
blocks: Default::default(),
|
||||||
|
height_by_hash: Default::default(),
|
||||||
|
tx_by_hash: Default::default(),
|
||||||
|
created_utxos: Default::default(),
|
||||||
|
sapling_note_commitment_tree,
|
||||||
|
orchard_note_commitment_tree,
|
||||||
|
spent_utxos: Default::default(),
|
||||||
|
sapling_anchors: HashMultiSet::new(),
|
||||||
|
sapling_anchors_by_height: Default::default(),
|
||||||
|
orchard_anchors: HashMultiSet::new(),
|
||||||
|
orchard_anchors_by_height: Default::default(),
|
||||||
|
sprout_nullifiers: Default::default(),
|
||||||
|
sapling_nullifiers: Default::default(),
|
||||||
|
orchard_nullifiers: Default::default(),
|
||||||
|
partial_cumulative_work: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Is the internal state of `self` the same as `other`?
|
/// Is the internal state of `self` the same as `other`?
|
||||||
///
|
///
|
||||||
/// [`Chain`] has custom [`Eq`] and [`Ord`] implementations based on proof of work,
|
/// [`Chain`] has custom [`Eq`] and [`Ord`] implementations based on proof of work,
|
||||||
|
@ -76,10 +106,15 @@ impl Chain {
|
||||||
self.created_utxos == other.created_utxos &&
|
self.created_utxos == other.created_utxos &&
|
||||||
self.spent_utxos == other.spent_utxos &&
|
self.spent_utxos == other.spent_utxos &&
|
||||||
|
|
||||||
|
// note commitment trees
|
||||||
|
self.sapling_note_commitment_tree.root() == other.sapling_note_commitment_tree.root() &&
|
||||||
|
self.orchard_note_commitment_tree.root() == other.orchard_note_commitment_tree.root() &&
|
||||||
|
|
||||||
// anchors
|
// anchors
|
||||||
self.sprout_anchors == other.sprout_anchors &&
|
|
||||||
self.sapling_anchors == other.sapling_anchors &&
|
self.sapling_anchors == other.sapling_anchors &&
|
||||||
|
self.sapling_anchors_by_height == other.sapling_anchors_by_height &&
|
||||||
self.orchard_anchors == other.orchard_anchors &&
|
self.orchard_anchors == other.orchard_anchors &&
|
||||||
|
self.orchard_anchors_by_height == other.orchard_anchors_by_height &&
|
||||||
|
|
||||||
// nullifiers
|
// nullifiers
|
||||||
self.sprout_nullifiers == other.sprout_nullifiers &&
|
self.sprout_nullifiers == other.sprout_nullifiers &&
|
||||||
|
@ -133,17 +168,46 @@ impl Chain {
|
||||||
|
|
||||||
/// Fork a chain at the block with the given hash, if it is part of this
|
/// Fork a chain at the block with the given hash, if it is part of this
|
||||||
/// chain.
|
/// chain.
|
||||||
pub fn fork(&self, fork_tip: block::Hash) -> Result<Option<Self>, ValidateContextError> {
|
///
|
||||||
|
/// The note commitment trees must be the trees of the finalized tip.
|
||||||
|
pub fn fork(
|
||||||
|
&self,
|
||||||
|
fork_tip: block::Hash,
|
||||||
|
sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree,
|
||||||
|
orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree,
|
||||||
|
) -> Result<Option<Self>, ValidateContextError> {
|
||||||
if !self.height_by_hash.contains_key(&fork_tip) {
|
if !self.height_by_hash.contains_key(&fork_tip) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut forked = self.clone();
|
let mut forked =
|
||||||
|
self.with_trees(sapling_note_commitment_tree, orchard_note_commitment_tree);
|
||||||
|
|
||||||
while forked.non_finalized_tip_hash() != fork_tip {
|
while forked.non_finalized_tip_hash() != fork_tip {
|
||||||
forked.pop_tip();
|
forked.pop_tip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rebuild the note commitment trees, starting from the finalized tip tree.
|
||||||
|
// TODO: change to a more efficient approach by removing nodes
|
||||||
|
// from the tree of the original chain (in `pop_tip()`).
|
||||||
|
// See https://github.com/ZcashFoundation/zebra/issues/2378
|
||||||
|
for block in forked.blocks.values() {
|
||||||
|
for transaction in block.block.transactions.iter() {
|
||||||
|
for sapling_note_commitment in transaction.sapling_note_commitments() {
|
||||||
|
forked
|
||||||
|
.sapling_note_commitment_tree
|
||||||
|
.append(*sapling_note_commitment)
|
||||||
|
.expect("must work since it was already appended before the fork");
|
||||||
|
}
|
||||||
|
for orchard_note_commitment in transaction.orchard_note_commitments() {
|
||||||
|
forked
|
||||||
|
.orchard_note_commitment_tree
|
||||||
|
.append(*orchard_note_commitment)
|
||||||
|
.expect("must work since it was already appended before the fork");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Some(forked))
|
Ok(Some(forked))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,6 +258,34 @@ impl Chain {
|
||||||
unspent_utxos.retain(|out_point, _utxo| !self.spent_utxos.contains(out_point));
|
unspent_utxos.retain(|out_point, _utxo| !self.spent_utxos.contains(out_point));
|
||||||
unspent_utxos
|
unspent_utxos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clone the Chain but not the history and note commitment trees, using
|
||||||
|
/// the specified trees instead.
|
||||||
|
///
|
||||||
|
/// Useful when forking, where the trees are rebuilt anyway.
|
||||||
|
fn with_trees(
|
||||||
|
&self,
|
||||||
|
sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree,
|
||||||
|
orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree,
|
||||||
|
) -> Self {
|
||||||
|
Chain {
|
||||||
|
blocks: self.blocks.clone(),
|
||||||
|
height_by_hash: self.height_by_hash.clone(),
|
||||||
|
tx_by_hash: self.tx_by_hash.clone(),
|
||||||
|
created_utxos: self.created_utxos.clone(),
|
||||||
|
spent_utxos: self.spent_utxos.clone(),
|
||||||
|
sapling_note_commitment_tree,
|
||||||
|
orchard_note_commitment_tree,
|
||||||
|
sapling_anchors: self.sapling_anchors.clone(),
|
||||||
|
orchard_anchors: self.orchard_anchors.clone(),
|
||||||
|
sapling_anchors_by_height: self.sapling_anchors_by_height.clone(),
|
||||||
|
orchard_anchors_by_height: self.orchard_anchors_by_height.clone(),
|
||||||
|
sprout_nullifiers: self.sprout_nullifiers.clone(),
|
||||||
|
sapling_nullifiers: self.sapling_nullifiers.clone(),
|
||||||
|
orchard_nullifiers: self.orchard_nullifiers.clone(),
|
||||||
|
partial_cumulative_work: self.partial_cumulative_work,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper trait to organize inverse operations done on the `Chain` type. Used to
|
/// Helper trait to organize inverse operations done on the `Chain` type. Used to
|
||||||
|
@ -300,14 +392,25 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
|
||||||
self.update_chain_state_with(orchard_shielded_data)?;
|
self.update_chain_state_with(orchard_shielded_data)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Having updated all the note commitment trees and nullifier sets in
|
||||||
|
// this block, the roots of the note commitment trees as of the last
|
||||||
|
// transaction are the treestates of this block.
|
||||||
|
let root = self.sapling_note_commitment_tree.root();
|
||||||
|
self.sapling_anchors.insert(root);
|
||||||
|
self.sapling_anchors_by_height.insert(height, root);
|
||||||
|
let root = self.orchard_note_commitment_tree.root();
|
||||||
|
self.orchard_anchors.insert(root);
|
||||||
|
self.orchard_anchors_by_height.insert(height, root);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, contextually_valid), fields(block = %contextually_valid.block))]
|
#[instrument(skip(self, contextually_valid), fields(block = %contextually_valid.block))]
|
||||||
fn revert_chain_state_with(&mut self, contextually_valid: &ContextuallyValidBlock) {
|
fn revert_chain_state_with(&mut self, contextually_valid: &ContextuallyValidBlock) {
|
||||||
let (block, hash, new_outputs, transaction_hashes) = (
|
let (block, hash, height, new_outputs, transaction_hashes) = (
|
||||||
contextually_valid.block.as_ref(),
|
contextually_valid.block.as_ref(),
|
||||||
contextually_valid.hash,
|
contextually_valid.hash,
|
||||||
|
contextually_valid.height,
|
||||||
&contextually_valid.new_outputs,
|
&contextually_valid.new_outputs,
|
||||||
&contextually_valid.transaction_hashes,
|
&contextually_valid.transaction_hashes,
|
||||||
);
|
);
|
||||||
|
@ -377,6 +480,22 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
|
||||||
self.revert_chain_state_with(sapling_shielded_data_shared_anchor);
|
self.revert_chain_state_with(sapling_shielded_data_shared_anchor);
|
||||||
self.revert_chain_state_with(orchard_shielded_data);
|
self.revert_chain_state_with(orchard_shielded_data);
|
||||||
}
|
}
|
||||||
|
let anchor = self
|
||||||
|
.sapling_anchors_by_height
|
||||||
|
.remove(&height)
|
||||||
|
.expect("Sapling anchor must be present if block was added to chain");
|
||||||
|
assert!(
|
||||||
|
self.sapling_anchors.remove(&anchor),
|
||||||
|
"Sapling anchor must be present if block was added to chain"
|
||||||
|
);
|
||||||
|
let anchor = self
|
||||||
|
.orchard_anchors_by_height
|
||||||
|
.remove(&height)
|
||||||
|
.expect("Orchard anchor must be present if block was added to chain");
|
||||||
|
assert!(
|
||||||
|
self.orchard_anchors.remove(&anchor),
|
||||||
|
"Orchard anchor must be present if block was added to chain"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,6 +593,10 @@ where
|
||||||
sapling_shielded_data: &Option<sapling::ShieldedData<AnchorV>>,
|
sapling_shielded_data: &Option<sapling::ShieldedData<AnchorV>>,
|
||||||
) -> Result<(), ValidateContextError> {
|
) -> Result<(), ValidateContextError> {
|
||||||
if let Some(sapling_shielded_data) = sapling_shielded_data {
|
if let Some(sapling_shielded_data) = sapling_shielded_data {
|
||||||
|
for cm_u in sapling_shielded_data.note_commitments() {
|
||||||
|
self.sapling_note_commitment_tree.append(*cm_u)?;
|
||||||
|
}
|
||||||
|
|
||||||
check::nullifier::add_to_non_finalized_chain_unique(
|
check::nullifier::add_to_non_finalized_chain_unique(
|
||||||
&mut self.sapling_nullifiers,
|
&mut self.sapling_nullifiers,
|
||||||
sapling_shielded_data.nullifiers(),
|
sapling_shielded_data.nullifiers(),
|
||||||
|
@ -493,6 +616,10 @@ where
|
||||||
sapling_shielded_data: &Option<sapling::ShieldedData<AnchorV>>,
|
sapling_shielded_data: &Option<sapling::ShieldedData<AnchorV>>,
|
||||||
) {
|
) {
|
||||||
if let Some(sapling_shielded_data) = sapling_shielded_data {
|
if let Some(sapling_shielded_data) = sapling_shielded_data {
|
||||||
|
// Note commitments are not removed from the tree here because we
|
||||||
|
// don't support that operation yet. Instead, we recreate the tree
|
||||||
|
// from the finalized tip in NonFinalizedState.
|
||||||
|
|
||||||
check::nullifier::remove_from_non_finalized_chain(
|
check::nullifier::remove_from_non_finalized_chain(
|
||||||
&mut self.sapling_nullifiers,
|
&mut self.sapling_nullifiers,
|
||||||
sapling_shielded_data.nullifiers(),
|
sapling_shielded_data.nullifiers(),
|
||||||
|
@ -508,6 +635,10 @@ impl UpdateWith<Option<orchard::ShieldedData>> for Chain {
|
||||||
orchard_shielded_data: &Option<orchard::ShieldedData>,
|
orchard_shielded_data: &Option<orchard::ShieldedData>,
|
||||||
) -> Result<(), ValidateContextError> {
|
) -> Result<(), ValidateContextError> {
|
||||||
if let Some(orchard_shielded_data) = orchard_shielded_data {
|
if let Some(orchard_shielded_data) = orchard_shielded_data {
|
||||||
|
for cm_x in orchard_shielded_data.note_commitments() {
|
||||||
|
self.orchard_note_commitment_tree.append(*cm_x)?;
|
||||||
|
}
|
||||||
|
|
||||||
check::nullifier::add_to_non_finalized_chain_unique(
|
check::nullifier::add_to_non_finalized_chain_unique(
|
||||||
&mut self.orchard_nullifiers,
|
&mut self.orchard_nullifiers,
|
||||||
orchard_shielded_data.nullifiers(),
|
orchard_shielded_data.nullifiers(),
|
||||||
|
@ -524,6 +655,10 @@ impl UpdateWith<Option<orchard::ShieldedData>> for Chain {
|
||||||
#[instrument(skip(self, orchard_shielded_data))]
|
#[instrument(skip(self, orchard_shielded_data))]
|
||||||
fn revert_chain_state_with(&mut self, orchard_shielded_data: &Option<orchard::ShieldedData>) {
|
fn revert_chain_state_with(&mut self, orchard_shielded_data: &Option<orchard::ShieldedData>) {
|
||||||
if let Some(orchard_shielded_data) = orchard_shielded_data {
|
if let Some(orchard_shielded_data) = orchard_shielded_data {
|
||||||
|
// Note commitments are not removed from the tree here because we
|
||||||
|
// don't support that operation yet. Instead, we recreate the tree
|
||||||
|
// from the finalized tip in NonFinalizedState.
|
||||||
|
|
||||||
check::nullifier::remove_from_non_finalized_chain(
|
check::nullifier::remove_from_non_finalized_chain(
|
||||||
&mut self.orchard_nullifiers,
|
&mut self.orchard_nullifiers,
|
||||||
orchard_shielded_data.nullifiers(),
|
orchard_shielded_data.nullifiers(),
|
||||||
|
|
|
@ -19,7 +19,7 @@ use crate::{
|
||||||
Config,
|
Config,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES: u32 = 16;
|
const DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES: u32 = 1;
|
||||||
|
|
||||||
/// Check that a forked chain is the same as a chain that had the same blocks appended.
|
/// Check that a forked chain is the same as a chain that had the same blocks appended.
|
||||||
///
|
///
|
||||||
|
@ -37,8 +37,9 @@ fn forked_equals_pushed() -> Result<()> {
|
||||||
|((chain, fork_at_count, _network) in PreparedChain::default())| {
|
|((chain, fork_at_count, _network) in PreparedChain::default())| {
|
||||||
// use `fork_at_count` as the fork tip
|
// use `fork_at_count` as the fork tip
|
||||||
let fork_tip_hash = chain[fork_at_count - 1].hash;
|
let fork_tip_hash = chain[fork_at_count - 1].hash;
|
||||||
let mut full_chain = Chain::default();
|
|
||||||
let mut partial_chain = Chain::default();
|
let mut full_chain = Chain::new(Default::default(), Default::default());
|
||||||
|
let mut partial_chain = Chain::new(Default::default(), Default::default());
|
||||||
let mut has_prevouts = false;
|
let mut has_prevouts = false;
|
||||||
|
|
||||||
for block in chain.iter().take(fork_at_count) {
|
for block in chain.iter().take(fork_at_count) {
|
||||||
|
@ -72,7 +73,14 @@ fn forked_equals_pushed() -> Result<()> {
|
||||||
.is_some();
|
.is_some();
|
||||||
}
|
}
|
||||||
|
|
||||||
let forked = full_chain.fork(fork_tip_hash).expect("fork works").expect("hash is present");
|
let forked = full_chain
|
||||||
|
.fork(
|
||||||
|
fork_tip_hash,
|
||||||
|
Default::default(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.expect("fork works")
|
||||||
|
.expect("hash is present");
|
||||||
|
|
||||||
// the first check is redundant, but it's useful for debugging
|
// the first check is redundant, but it's useful for debugging
|
||||||
prop_assert_eq!(forked.blocks.len(), partial_chain.blocks.len());
|
prop_assert_eq!(forked.blocks.len(), partial_chain.blocks.len());
|
||||||
|
@ -99,13 +107,19 @@ fn finalized_equals_pushed() -> Result<()> {
|
||||||
|((chain, end_count, _network) in PreparedChain::default())| {
|
|((chain, end_count, _network) in PreparedChain::default())| {
|
||||||
// use `end_count` as the number of non-finalized blocks at the end of the chain
|
// use `end_count` as the number of non-finalized blocks at the end of the chain
|
||||||
let finalized_count = chain.len() - end_count;
|
let finalized_count = chain.len() - end_count;
|
||||||
let mut full_chain = Chain::default();
|
let mut full_chain = Chain::new(Default::default(), Default::default());
|
||||||
let mut partial_chain = Chain::default();
|
|
||||||
|
|
||||||
|
for block in chain.iter().take(finalized_count) {
|
||||||
|
full_chain = full_chain.push(block.clone())?;
|
||||||
|
}
|
||||||
|
let mut partial_chain = Chain::new(
|
||||||
|
full_chain.sapling_note_commitment_tree.clone(),
|
||||||
|
full_chain.orchard_note_commitment_tree.clone(),
|
||||||
|
);
|
||||||
for block in chain.iter().skip(finalized_count) {
|
for block in chain.iter().skip(finalized_count) {
|
||||||
partial_chain = partial_chain.push(block.clone())?;
|
partial_chain = partial_chain.push(block.clone())?;
|
||||||
}
|
}
|
||||||
for block in chain.iter() {
|
for block in chain.iter().skip(finalized_count) {
|
||||||
full_chain = full_chain.push(block.clone())?;
|
full_chain = full_chain.push(block.clone())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,8 +229,8 @@ fn different_blocks_different_chains() -> Result<()> {
|
||||||
.prop_flat_map(|block_strategy| (block_strategy.clone(), block_strategy))
|
.prop_flat_map(|block_strategy| (block_strategy.clone(), block_strategy))
|
||||||
.prop_map(|(block1, block2)| (DisplayToDebug(block1), DisplayToDebug(block2)))
|
.prop_map(|(block1, block2)| (DisplayToDebug(block1), DisplayToDebug(block2)))
|
||||||
)| {
|
)| {
|
||||||
let chain1 = Chain::default();
|
let chain1 = Chain::new(Default::default(), Default::default());
|
||||||
let chain2 = Chain::default();
|
let chain2 = Chain::new(Default::default(), Default::default());
|
||||||
|
|
||||||
let block1 = Arc::new(block1.0).prepare();
|
let block1 = Arc::new(block1.0).prepare();
|
||||||
let block2 = Arc::new(block2.0).prepare();
|
let block2 = Arc::new(block2.0).prepare();
|
||||||
|
@ -250,10 +264,15 @@ fn different_blocks_different_chains() -> Result<()> {
|
||||||
chain1.created_utxos = chain2.created_utxos.clone();
|
chain1.created_utxos = chain2.created_utxos.clone();
|
||||||
chain1.spent_utxos = chain2.spent_utxos.clone();
|
chain1.spent_utxos = chain2.spent_utxos.clone();
|
||||||
|
|
||||||
|
// note commitment trees
|
||||||
|
chain1.sapling_note_commitment_tree = chain2.sapling_note_commitment_tree.clone();
|
||||||
|
chain1.orchard_note_commitment_tree = chain2.orchard_note_commitment_tree.clone();
|
||||||
|
|
||||||
// anchors
|
// anchors
|
||||||
chain1.sprout_anchors = chain2.sprout_anchors.clone();
|
|
||||||
chain1.sapling_anchors = chain2.sapling_anchors.clone();
|
chain1.sapling_anchors = chain2.sapling_anchors.clone();
|
||||||
|
chain1.sapling_anchors_by_height = chain2.sapling_anchors_by_height.clone();
|
||||||
chain1.orchard_anchors = chain2.orchard_anchors.clone();
|
chain1.orchard_anchors = chain2.orchard_anchors.clone();
|
||||||
|
chain1.orchard_anchors_by_height = chain2.orchard_anchors_by_height.clone();
|
||||||
|
|
||||||
// nullifiers
|
// nullifiers
|
||||||
chain1.sprout_nullifiers = chain2.sprout_nullifiers.clone();
|
chain1.sprout_nullifiers = chain2.sprout_nullifiers.clone();
|
||||||
|
|
|
@ -18,7 +18,7 @@ use self::assert_eq;
|
||||||
#[test]
|
#[test]
|
||||||
fn construct_empty() {
|
fn construct_empty() {
|
||||||
zebra_test::init();
|
zebra_test::init();
|
||||||
let _chain = Chain::default();
|
let _chain = Chain::new(Default::default(), Default::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -27,7 +27,7 @@ fn construct_single() -> Result<()> {
|
||||||
let block: Arc<Block> =
|
let block: Arc<Block> =
|
||||||
zebra_test::vectors::BLOCK_MAINNET_434873_BYTES.zcash_deserialize_into()?;
|
zebra_test::vectors::BLOCK_MAINNET_434873_BYTES.zcash_deserialize_into()?;
|
||||||
|
|
||||||
let mut chain = Chain::default();
|
let mut chain = Chain::new(Default::default(), Default::default());
|
||||||
chain = chain.push(block.prepare())?;
|
chain = chain.push(block.prepare())?;
|
||||||
|
|
||||||
assert_eq!(1, chain.blocks.len());
|
assert_eq!(1, chain.blocks.len());
|
||||||
|
@ -49,7 +49,7 @@ fn construct_many() -> Result<()> {
|
||||||
block = next_block;
|
block = next_block;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut chain = Chain::default();
|
let mut chain = Chain::new(Default::default(), Default::default());
|
||||||
|
|
||||||
for block in blocks {
|
for block in blocks {
|
||||||
chain = chain.push(block.prepare())?;
|
chain = chain.push(block.prepare())?;
|
||||||
|
@ -68,10 +68,10 @@ fn ord_matches_work() -> Result<()> {
|
||||||
.set_work(1);
|
.set_work(1);
|
||||||
let more_block = less_block.clone().set_work(10);
|
let more_block = less_block.clone().set_work(10);
|
||||||
|
|
||||||
let mut lesser_chain = Chain::default();
|
let mut lesser_chain = Chain::new(Default::default(), Default::default());
|
||||||
lesser_chain = lesser_chain.push(less_block.prepare())?;
|
lesser_chain = lesser_chain.push(less_block.prepare())?;
|
||||||
|
|
||||||
let mut bigger_chain = Chain::default();
|
let mut bigger_chain = Chain::new(Default::default(), Default::default());
|
||||||
bigger_chain = bigger_chain.push(more_block.prepare())?;
|
bigger_chain = bigger_chain.push(more_block.prepare())?;
|
||||||
|
|
||||||
assert!(bigger_chain > lesser_chain);
|
assert!(bigger_chain > lesser_chain);
|
||||||
|
|
Loading…
Reference in New Issue