Merge pull request #114 from str4d/compact-blocks
CompactBlock parsing and scanning
This commit is contained in:
commit
2cd8a7f4d2
|
@ -14,6 +14,14 @@ jobs:
|
|||
toolchain: 1.37.0
|
||||
override: true
|
||||
|
||||
# cargo fmt does not build the code, and running it in a fresh clone of
|
||||
# the codebase will fail because the protobuf code has not been generated.
|
||||
- name: cargo build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --all
|
||||
|
||||
# Ensure all code has been formatted with rustfmt
|
||||
- run: rustup component add rustfmt
|
||||
- name: Check formatting
|
||||
|
|
|
@ -14,8 +14,8 @@ before_script:
|
|||
- rustup component add rustfmt
|
||||
|
||||
script:
|
||||
- cargo fmt --all -- --check
|
||||
- cargo build --verbose --release --all
|
||||
- cargo fmt --all -- --check
|
||||
- cargo test --verbose --release --all
|
||||
- cargo test --verbose --release --all -- --ignored
|
||||
|
||||
|
|
|
@ -467,6 +467,28 @@ dependencies = [
|
|||
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "protobuf-codegen"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf-codegen-pure"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"protobuf-codegen 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.2"
|
||||
|
@ -512,6 +534,15 @@ dependencies = [
|
|||
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_os"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xorshift"
|
||||
version = "0.2.0"
|
||||
|
@ -557,6 +588,11 @@ dependencies = [
|
|||
"opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.5"
|
||||
|
@ -606,9 +642,15 @@ name = "zcash_client_backend"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bech32 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ff 0.5.0",
|
||||
"hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pairing 0.15.0",
|
||||
"protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"protobuf-codegen-pure 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand_os 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand_xorshift 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"subtle 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"zcash_primitives 0.1.0",
|
||||
]
|
||||
|
||||
|
@ -700,17 +742,22 @@ dependencies = [
|
|||
"checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b"
|
||||
"checksum proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e688f31d92ffd7c1ddc57a1b4e6d773c0f2a14ee437a4b0a4f5a69c80eb221c8"
|
||||
"checksum proc-macro2 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e98a83a9f9b331f54b924e68a66acb1bb35cb01fb0a23645139967abefb697e8"
|
||||
"checksum protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40361836defdd5871ff7e84096c6f6444af7fc157f8ef1789f54f147687caa20"
|
||||
"checksum protobuf-codegen 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "12c6abd78435445fc86898ebbd0521a68438063d4a73e23527b7134e6bf58b4a"
|
||||
"checksum protobuf-codegen-pure 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c1646acda5319f5b28b0bff4a484324df43ddae2c0f5a3f3e63c0b26095cd600"
|
||||
"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
|
||||
"checksum rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d47eab0e83d9693d40f825f86948aa16eff6750ead4bdffc4ab95b8b3a7f052c"
|
||||
"checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853"
|
||||
"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
"checksum rand_os 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a788ae3edb696cfcba1c19bfd388cc4b8c21f8a408432b199c072825084da58a"
|
||||
"checksum rand_xorshift 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8"
|
||||
"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
|
||||
"checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d"
|
||||
"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
||||
"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||
"checksum sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4d8bfd0e469f417657573d8451fb33d16cfe0989359b93baf3a1ffc639543d"
|
||||
"checksum subtle 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab3af2eb31c42e8f0ccf43548232556c42737e01a96db6e1777b0be108e79799"
|
||||
"checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf"
|
||||
"checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9"
|
||||
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
# Protobufs
|
||||
src/proto/
|
|
@ -13,11 +13,19 @@ edition = "2018"
|
|||
|
||||
[dependencies]
|
||||
bech32 = "0.7"
|
||||
ff = { version = "0.5.0", path = "../ff" }
|
||||
hex = "0.3"
|
||||
pairing = { version = "0.15.0", path = "../pairing" }
|
||||
protobuf = "2"
|
||||
subtle = "2"
|
||||
zcash_primitives = { version = "0.1.0", path = "../zcash_primitives" }
|
||||
|
||||
[build-dependencies]
|
||||
protobuf-codegen-pure = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = "0.5"
|
||||
rand_os = "0.2"
|
||||
rand_xorshift = "0.2"
|
||||
|
||||
[badges]
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
use protobuf_codegen_pure;
|
||||
|
||||
fn main() {
|
||||
protobuf_codegen_pure::run(protobuf_codegen_pure::Args {
|
||||
out_dir: "src/proto",
|
||||
input: &["proto/compact_formats.proto"],
|
||||
includes: &["proto"],
|
||||
customize: Default::default(),
|
||||
})
|
||||
.expect("protoc");
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
syntax = "proto3";
|
||||
package cash.z.wallet.sdk.rpc;
|
||||
option go_package = "walletrpc";
|
||||
|
||||
// Remember that proto3 fields are all optional. A field that is not present will be set to its zero value.
|
||||
// bytes fields of hashes are in canonical little-endian format.
|
||||
|
||||
// CompactBlock is a packaging of ONLY the data from a block that's needed to:
|
||||
// 1. Detect a payment to your shielded Sapling address
|
||||
// 2. Detect a spend of your shielded Sapling notes
|
||||
// 3. Update your witnesses to generate new Sapling spend proofs.
|
||||
message CompactBlock {
|
||||
uint32 protoVersion = 1; // the version of this wire format, for storage
|
||||
uint64 height = 2; // the height of this block
|
||||
bytes hash = 3;
|
||||
bytes prevHash = 4;
|
||||
uint32 time = 5;
|
||||
bytes header = 6; // (hash, prevHash, and time) OR (full header)
|
||||
repeated CompactTx vtx = 7; // compact transactions from this block
|
||||
}
|
||||
|
||||
message CompactTx {
|
||||
// Index and hash will allow the receiver to call out to chain
|
||||
// explorers or other data structures to retrieve more information
|
||||
// about this transaction.
|
||||
uint64 index = 1;
|
||||
bytes hash = 2;
|
||||
|
||||
// The transaction fee: present if server can provide. In the case of a
|
||||
// stateless server and a transaction with transparent inputs, this will be
|
||||
// unset because the calculation requires reference to prior transactions.
|
||||
// in a pure-Sapling context, the fee will be calculable as:
|
||||
// valueBalance + (sum(vPubNew) - sum(vPubOld) - sum(tOut))
|
||||
uint32 fee = 3;
|
||||
|
||||
repeated CompactSpend spends = 4;
|
||||
repeated CompactOutput outputs = 5;
|
||||
}
|
||||
|
||||
message CompactSpend {
|
||||
bytes nf = 1;
|
||||
}
|
||||
|
||||
message CompactOutput {
|
||||
bytes cmu = 1;
|
||||
bytes epk = 2;
|
||||
bytes ciphertext = 3;
|
||||
}
|
|
@ -9,3 +9,6 @@
|
|||
pub mod constants;
|
||||
pub mod encoding;
|
||||
pub mod keys;
|
||||
pub mod proto;
|
||||
pub mod wallet;
|
||||
pub mod welding_rig;
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
//! Generated code for handling light client protobuf structs.
|
||||
|
||||
use ff::{PrimeField, PrimeFieldRepr};
|
||||
use pairing::bls12_381::{Bls12, Fr, FrRepr};
|
||||
use zcash_primitives::{
|
||||
block::{BlockHash, BlockHeader},
|
||||
jubjub::{edwards, PrimeOrder},
|
||||
JUBJUB,
|
||||
};
|
||||
|
||||
pub mod compact_formats;
|
||||
|
||||
impl compact_formats::CompactBlock {
|
||||
/// Returns the [`BlockHash`] for this block.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if [`CompactBlock.header`] is not set and
|
||||
/// [`CompactBlock.hash`] is not exactly 32 bytes.
|
||||
///
|
||||
/// [`CompactBlock.header`]: #structfield.header
|
||||
/// [`CompactBlock.hash`]: #structfield.hash
|
||||
pub fn hash(&self) -> BlockHash {
|
||||
if let Some(header) = self.header() {
|
||||
header.hash()
|
||||
} else {
|
||||
BlockHash::from_slice(&self.hash)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`BlockHash`] for this block's parent.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if [`CompactBlock.header`] is not set and
|
||||
/// [`CompactBlock.prevHash`] is not exactly 32 bytes.
|
||||
///
|
||||
/// [`CompactBlock.header`]: #structfield.header
|
||||
/// [`CompactBlock.prevHash`]: #structfield.prevHash
|
||||
pub fn prev_hash(&self) -> BlockHash {
|
||||
if let Some(header) = self.header() {
|
||||
header.prev_block
|
||||
} else {
|
||||
BlockHash::from_slice(&self.prevHash)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`BlockHeader`] for this block if present.
|
||||
///
|
||||
/// A convenience method that parses [`CompactBlock.header`] if present.
|
||||
///
|
||||
/// [`CompactBlock.header`]: #structfield.header
|
||||
pub fn header(&self) -> Option<BlockHeader> {
|
||||
if self.header.is_empty() {
|
||||
None
|
||||
} else {
|
||||
BlockHeader::read(&self.header[..]).ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl compact_formats::CompactOutput {
|
||||
/// Returns the note commitment for this output.
|
||||
///
|
||||
/// A convenience method that parses [`CompactOutput.cmu`].
|
||||
///
|
||||
/// [`CompactOutput.cmu`]: #structfield.cmu
|
||||
pub fn cmu(&self) -> Result<Fr, ()> {
|
||||
let mut repr = FrRepr::default();
|
||||
repr.read_le(&self.cmu[..]).map_err(|_| ())?;
|
||||
Fr::from_repr(repr).map_err(|_| ())
|
||||
}
|
||||
|
||||
/// Returns the ephemeral public key for this output.
|
||||
///
|
||||
/// A convenience method that parses [`CompactOutput.epk`].
|
||||
///
|
||||
/// [`CompactOutput.epk`]: #structfield.epk
|
||||
pub fn epk(&self) -> Result<edwards::Point<Bls12, PrimeOrder>, ()> {
|
||||
let p = edwards::Point::<Bls12, _>::read(&self.epk[..], &JUBJUB).map_err(|_| ())?;
|
||||
p.as_prime_order(&JUBJUB).ok_or(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
//! Structs representing transaction data scanned from the block chain by a wallet or
|
||||
//! light client.
|
||||
|
||||
use pairing::bls12_381::{Bls12, Fr};
|
||||
use zcash_primitives::{
|
||||
jubjub::{edwards, PrimeOrder},
|
||||
merkle_tree::IncrementalWitness,
|
||||
primitives::{Note, PaymentAddress},
|
||||
sapling::Node,
|
||||
transaction::TxId,
|
||||
};
|
||||
|
||||
/// A subset of a [`Transaction`] relevant to wallets and light clients.
|
||||
///
|
||||
/// [`Transaction`]: zcash_primitives::transaction::Transaction
|
||||
pub struct WalletTx {
|
||||
pub txid: TxId,
|
||||
pub index: usize,
|
||||
pub num_spends: usize,
|
||||
pub num_outputs: usize,
|
||||
pub shielded_spends: Vec<WalletShieldedSpend>,
|
||||
pub shielded_outputs: Vec<WalletShieldedOutput>,
|
||||
}
|
||||
|
||||
/// A subset of a [`SpendDescription`] relevant to wallets and light clients.
|
||||
///
|
||||
/// [`SpendDescription`]: zcash_primitives::transaction::components::SpendDescription
|
||||
pub struct WalletShieldedSpend {
|
||||
pub index: usize,
|
||||
pub nf: Vec<u8>,
|
||||
pub account: usize,
|
||||
}
|
||||
|
||||
/// A subset of an [`OutputDescription`] relevant to wallets and light clients.
|
||||
///
|
||||
/// [`OutputDescription`]: zcash_primitives::transaction::components::OutputDescription
|
||||
pub struct WalletShieldedOutput {
|
||||
pub index: usize,
|
||||
pub cmu: Fr,
|
||||
pub epk: edwards::Point<Bls12, PrimeOrder>,
|
||||
pub account: usize,
|
||||
pub note: Note<Bls12>,
|
||||
pub to: PaymentAddress<Bls12>,
|
||||
pub is_change: bool,
|
||||
pub witness: IncrementalWitness<Node>,
|
||||
}
|
|
@ -0,0 +1,399 @@
|
|||
//! Tools for scanning a compact representation of the Zcash block chain.
|
||||
|
||||
use ff::PrimeField;
|
||||
use std::collections::HashSet;
|
||||
use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption};
|
||||
use zcash_primitives::{
|
||||
jubjub::fs::Fs,
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
note_encryption::try_sapling_compact_note_decryption,
|
||||
sapling::Node,
|
||||
transaction::TxId,
|
||||
zip32::ExtendedFullViewingKey,
|
||||
};
|
||||
|
||||
use crate::proto::compact_formats::{CompactBlock, CompactOutput};
|
||||
use crate::wallet::{WalletShieldedOutput, WalletShieldedSpend, WalletTx};
|
||||
|
||||
/// Scans a [`CompactOutput`] with a set of [`ExtendedFullViewingKey`]s.
|
||||
///
|
||||
/// Returns a [`WalletShieldedOutput`] and corresponding [`IncrementalWitness`] if this
|
||||
/// output belongs to any of the given [`ExtendedFullViewingKey`]s.
|
||||
///
|
||||
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are incremented
|
||||
/// with this output's commitment.
|
||||
fn scan_output(
|
||||
(index, output): (usize, CompactOutput),
|
||||
ivks: &[Fs],
|
||||
spent_from_accounts: &HashSet<usize>,
|
||||
tree: &mut CommitmentTree<Node>,
|
||||
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||
block_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||
new_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||
) -> Option<WalletShieldedOutput> {
|
||||
let cmu = output.cmu().ok()?;
|
||||
let epk = output.epk().ok()?;
|
||||
let ct = output.ciphertext;
|
||||
|
||||
// Increment tree and witnesses
|
||||
let node = Node::new(cmu.into_repr());
|
||||
for witness in existing_witnesses {
|
||||
witness.append(node).unwrap();
|
||||
}
|
||||
for witness in block_witnesses {
|
||||
witness.append(node).unwrap();
|
||||
}
|
||||
for witness in new_witnesses {
|
||||
witness.append(node).unwrap();
|
||||
}
|
||||
tree.append(node).unwrap();
|
||||
|
||||
for (account, ivk) in ivks.iter().enumerate() {
|
||||
let (note, to) = match try_sapling_compact_note_decryption(ivk, &epk, &cmu, &ct) {
|
||||
Some(ret) => ret,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// A note is marked as "change" if the account that received it
|
||||
// also spent notes in the same transaction. This will catch,
|
||||
// for instance:
|
||||
// - Change created by spending fractions of notes.
|
||||
// - Notes created by consolidation transactions.
|
||||
// - Notes sent from one account to itself.
|
||||
let is_change = spent_from_accounts.contains(&account);
|
||||
|
||||
return Some(WalletShieldedOutput {
|
||||
index,
|
||||
cmu,
|
||||
epk,
|
||||
account,
|
||||
note,
|
||||
to,
|
||||
is_change,
|
||||
witness: IncrementalWitness::from_tree(tree),
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Scans a [`CompactBlock`] with a set of [`ExtendedFullViewingKey`]s.
|
||||
///
|
||||
/// Returns a vector of [`WalletTx`]s belonging to any of the given
|
||||
/// [`ExtendedFullViewingKey`]s, and the corresponding new [`IncrementalWitness`]es.
|
||||
///
|
||||
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are
|
||||
/// incremented appropriately.
|
||||
pub fn scan_block(
|
||||
block: CompactBlock,
|
||||
extfvks: &[ExtendedFullViewingKey],
|
||||
nullifiers: &[(&[u8], usize)],
|
||||
tree: &mut CommitmentTree<Node>,
|
||||
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||
) -> Vec<WalletTx> {
|
||||
let mut wtxs: Vec<WalletTx> = vec![];
|
||||
let ivks: Vec<_> = extfvks.iter().map(|extfvk| extfvk.fvk.vk.ivk()).collect();
|
||||
|
||||
for tx in block.vtx.into_iter() {
|
||||
let num_spends = tx.spends.len();
|
||||
let num_outputs = tx.outputs.len();
|
||||
|
||||
// Check for spent notes
|
||||
// The only step that is not constant-time is the filter() at the end.
|
||||
let shielded_spends: Vec<_> = tx
|
||||
.spends
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, spend)| {
|
||||
// Find the first tracked nullifier that matches this spend, and produce
|
||||
// a WalletShieldedSpend if there is a match, in constant time.
|
||||
nullifiers
|
||||
.iter()
|
||||
.map(|&(nf, account)| CtOption::new(account as u64, nf.ct_eq(&spend.nf[..])))
|
||||
.fold(CtOption::new(0, 0.into()), |first, next| {
|
||||
CtOption::conditional_select(&next, &first, first.is_some())
|
||||
})
|
||||
.map(|account| WalletShieldedSpend {
|
||||
index,
|
||||
nf: spend.nf,
|
||||
account: account as usize,
|
||||
})
|
||||
})
|
||||
.filter(|spend| spend.is_some().into())
|
||||
.map(|spend| spend.unwrap())
|
||||
.collect();
|
||||
|
||||
// Collect the set of accounts that were spent from in this transaction
|
||||
let spent_from_accounts: HashSet<_> =
|
||||
shielded_spends.iter().map(|spend| spend.account).collect();
|
||||
|
||||
// Check for incoming notes while incrementing tree and witnesses
|
||||
let mut shielded_outputs: Vec<WalletShieldedOutput> = vec![];
|
||||
{
|
||||
// Grab mutable references to new witnesses from previous transactions
|
||||
// in this block so that we can update them. Scoped so we don't hold
|
||||
// mutable references to wtxs for too long.
|
||||
let mut block_witnesses: Vec<_> = wtxs
|
||||
.iter_mut()
|
||||
.map(|tx| {
|
||||
tx.shielded_outputs
|
||||
.iter_mut()
|
||||
.map(|output| &mut output.witness)
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
for to_scan in tx.outputs.into_iter().enumerate() {
|
||||
// Grab mutable references to new witnesses from previous outputs
|
||||
// in this transaction so that we can update them. Scoped so we
|
||||
// don't hold mutable references to shielded_outputs for too long.
|
||||
let mut new_witnesses: Vec<_> = shielded_outputs
|
||||
.iter_mut()
|
||||
.map(|output| &mut output.witness)
|
||||
.collect();
|
||||
|
||||
if let Some(output) = scan_output(
|
||||
to_scan,
|
||||
&ivks,
|
||||
&spent_from_accounts,
|
||||
tree,
|
||||
existing_witnesses,
|
||||
&mut block_witnesses,
|
||||
&mut new_witnesses,
|
||||
) {
|
||||
shielded_outputs.push(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !(shielded_spends.is_empty() && shielded_outputs.is_empty()) {
|
||||
let mut txid = TxId([0u8; 32]);
|
||||
txid.0.copy_from_slice(&tx.hash);
|
||||
wtxs.push(WalletTx {
|
||||
txid,
|
||||
index: tx.index as usize,
|
||||
num_spends,
|
||||
num_outputs,
|
||||
shielded_spends,
|
||||
shielded_outputs,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
wtxs
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ff::{Field, PrimeField, PrimeFieldRepr};
|
||||
use pairing::bls12_381::{Bls12, Fr};
|
||||
use rand_core::RngCore;
|
||||
use rand_os::OsRng;
|
||||
use zcash_primitives::{
|
||||
jubjub::{fs::Fs, FixedGenerators, JubjubParams, ToUniform},
|
||||
merkle_tree::CommitmentTree,
|
||||
note_encryption::{Memo, SaplingNoteEncryption},
|
||||
primitives::Note,
|
||||
transaction::components::Amount,
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
JUBJUB,
|
||||
};
|
||||
|
||||
use super::scan_block;
|
||||
use crate::proto::compact_formats::{CompactBlock, CompactOutput, CompactSpend, CompactTx};
|
||||
|
||||
fn random_compact_tx<R: RngCore>(rng: &mut R) -> CompactTx {
|
||||
let fake_nf = {
|
||||
let mut nf = vec![0; 32];
|
||||
rng.fill_bytes(&mut nf);
|
||||
nf
|
||||
};
|
||||
let fake_cmu = {
|
||||
let fake_cmu = Fr::random(rng);
|
||||
let mut bytes = vec![];
|
||||
fake_cmu.into_repr().write_le(&mut bytes).unwrap();
|
||||
bytes
|
||||
};
|
||||
let fake_epk = {
|
||||
let mut buffer = vec![0; 64];
|
||||
rng.fill_bytes(&mut buffer);
|
||||
let fake_esk = Fs::to_uniform(&buffer[..]);
|
||||
let fake_epk = JUBJUB
|
||||
.generator(FixedGenerators::SpendingKeyGenerator)
|
||||
.mul(fake_esk, &JUBJUB);
|
||||
let mut bytes = vec![];
|
||||
fake_epk.write(&mut bytes).unwrap();
|
||||
bytes
|
||||
};
|
||||
let mut cspend = CompactSpend::new();
|
||||
cspend.set_nf(fake_nf);
|
||||
let mut cout = CompactOutput::new();
|
||||
cout.set_cmu(fake_cmu);
|
||||
cout.set_epk(fake_epk);
|
||||
cout.set_ciphertext(vec![0; 52]);
|
||||
let mut ctx = CompactTx::new();
|
||||
let mut txid = vec![0; 32];
|
||||
rng.fill_bytes(&mut txid);
|
||||
ctx.set_hash(txid);
|
||||
ctx.spends.push(cspend);
|
||||
ctx.outputs.push(cout);
|
||||
ctx
|
||||
}
|
||||
|
||||
/// Create a fake CompactBlock at the given height, with a transaction containing a
|
||||
/// single spend of the given nullifier and a single output paying the given address.
|
||||
/// Returns the CompactBlock.
|
||||
fn fake_compact_block(
|
||||
height: i32,
|
||||
nf: [u8; 32],
|
||||
extfvk: ExtendedFullViewingKey,
|
||||
value: Amount,
|
||||
tx_after: bool,
|
||||
) -> CompactBlock {
|
||||
let to = extfvk.default_address().unwrap().1;
|
||||
|
||||
// Create a fake Note for the account
|
||||
let mut rng = OsRng;
|
||||
let note = Note {
|
||||
g_d: to.diversifier().g_d::<Bls12>(&JUBJUB).unwrap(),
|
||||
pk_d: to.pk_d().clone(),
|
||||
value: value.into(),
|
||||
r: Fs::random(&mut rng),
|
||||
};
|
||||
let encryptor = SaplingNoteEncryption::new(
|
||||
extfvk.fvk.ovk,
|
||||
note.clone(),
|
||||
to.clone(),
|
||||
Memo::default(),
|
||||
&mut rng,
|
||||
);
|
||||
let mut cmu = vec![];
|
||||
note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap();
|
||||
let mut epk = vec![];
|
||||
encryptor.epk().write(&mut epk).unwrap();
|
||||
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
||||
|
||||
// Create a fake CompactBlock containing the note
|
||||
let mut cb = CompactBlock::new();
|
||||
cb.set_height(height as u64);
|
||||
|
||||
// Add a random Sapling tx before ours
|
||||
{
|
||||
let mut tx = random_compact_tx(&mut rng);
|
||||
tx.index = cb.vtx.len() as u64;
|
||||
cb.vtx.push(tx);
|
||||
}
|
||||
|
||||
let mut cspend = CompactSpend::new();
|
||||
cspend.set_nf(nf.to_vec());
|
||||
let mut cout = CompactOutput::new();
|
||||
cout.set_cmu(cmu);
|
||||
cout.set_epk(epk);
|
||||
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
|
||||
let mut ctx = CompactTx::new();
|
||||
let mut txid = vec![0; 32];
|
||||
rng.fill_bytes(&mut txid);
|
||||
ctx.set_hash(txid);
|
||||
ctx.spends.push(cspend);
|
||||
ctx.outputs.push(cout);
|
||||
ctx.index = cb.vtx.len() as u64;
|
||||
cb.vtx.push(ctx);
|
||||
|
||||
// Optionally add another random Sapling tx after ours
|
||||
if tx_after {
|
||||
let mut tx = random_compact_tx(&mut rng);
|
||||
tx.index = cb.vtx.len() as u64;
|
||||
cb.vtx.push(tx);
|
||||
}
|
||||
|
||||
cb
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_block_with_my_tx() {
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
|
||||
let cb = fake_compact_block(
|
||||
1,
|
||||
[0; 32],
|
||||
extfvk.clone(),
|
||||
Amount::from_u64(5).unwrap(),
|
||||
false,
|
||||
);
|
||||
assert_eq!(cb.vtx.len(), 2);
|
||||
|
||||
let mut tree = CommitmentTree::new();
|
||||
let txs = scan_block(cb, &[extfvk], &[], &mut tree, &mut []);
|
||||
assert_eq!(txs.len(), 1);
|
||||
|
||||
let tx = &txs[0];
|
||||
assert_eq!(tx.index, 1);
|
||||
assert_eq!(tx.num_spends, 1);
|
||||
assert_eq!(tx.num_outputs, 1);
|
||||
assert_eq!(tx.shielded_spends.len(), 0);
|
||||
assert_eq!(tx.shielded_outputs.len(), 1);
|
||||
assert_eq!(tx.shielded_outputs[0].index, 0);
|
||||
assert_eq!(tx.shielded_outputs[0].account, 0);
|
||||
assert_eq!(tx.shielded_outputs[0].note.value, 5);
|
||||
|
||||
// Check that the witness root matches
|
||||
assert_eq!(tx.shielded_outputs[0].witness.root(), tree.root());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_block_with_txs_after_my_tx() {
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
|
||||
let cb = fake_compact_block(
|
||||
1,
|
||||
[0; 32],
|
||||
extfvk.clone(),
|
||||
Amount::from_u64(5).unwrap(),
|
||||
true,
|
||||
);
|
||||
assert_eq!(cb.vtx.len(), 3);
|
||||
|
||||
let mut tree = CommitmentTree::new();
|
||||
let txs = scan_block(cb, &[extfvk], &[], &mut tree, &mut []);
|
||||
assert_eq!(txs.len(), 1);
|
||||
|
||||
let tx = &txs[0];
|
||||
assert_eq!(tx.index, 1);
|
||||
assert_eq!(tx.num_spends, 1);
|
||||
assert_eq!(tx.num_outputs, 1);
|
||||
assert_eq!(tx.shielded_spends.len(), 0);
|
||||
assert_eq!(tx.shielded_outputs.len(), 1);
|
||||
assert_eq!(tx.shielded_outputs[0].index, 0);
|
||||
assert_eq!(tx.shielded_outputs[0].account, 0);
|
||||
assert_eq!(tx.shielded_outputs[0].note.value, 5);
|
||||
|
||||
// Check that the witness root matches
|
||||
assert_eq!(tx.shielded_outputs[0].witness.root(), tree.root());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_block_with_my_spend() {
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
let nf = [7; 32];
|
||||
let account = 12;
|
||||
|
||||
let cb = fake_compact_block(1, nf, extfvk, Amount::from_u64(5).unwrap(), false);
|
||||
assert_eq!(cb.vtx.len(), 2);
|
||||
|
||||
let mut tree = CommitmentTree::new();
|
||||
let txs = scan_block(cb, &[], &[(&nf, account)], &mut tree, &mut []);
|
||||
assert_eq!(txs.len(), 1);
|
||||
|
||||
let tx = &txs[0];
|
||||
assert_eq!(tx.index, 1);
|
||||
assert_eq!(tx.num_spends, 1);
|
||||
assert_eq!(tx.num_outputs, 1);
|
||||
assert_eq!(tx.shielded_spends.len(), 1);
|
||||
assert_eq!(tx.shielded_outputs.len(), 0);
|
||||
assert_eq!(tx.shielded_spends[0].index, 0);
|
||||
assert_eq!(tx.shielded_spends[0].nf, nf);
|
||||
assert_eq!(tx.shielded_spends[0].account, account);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use hex;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fmt;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::ops::Deref;
|
||||
|
@ -21,14 +22,31 @@ impl fmt::Display for BlockHash {
|
|||
}
|
||||
}
|
||||
|
||||
impl BlockHash {
|
||||
/// Constructs a [`BlockHash`] from the given slice.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if the slice is not exactly 32 bytes.
|
||||
pub fn from_slice(bytes: &[u8]) -> Self {
|
||||
assert_eq!(bytes.len(), 32);
|
||||
let mut hash = [0; 32];
|
||||
hash.copy_from_slice(&bytes);
|
||||
BlockHash(hash)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Zcash block header.
|
||||
pub struct BlockHeader(BlockHeaderData);
|
||||
pub struct BlockHeader {
|
||||
hash: BlockHash,
|
||||
data: BlockHeaderData,
|
||||
}
|
||||
|
||||
impl Deref for BlockHeader {
|
||||
type Target = BlockHeaderData;
|
||||
|
||||
fn deref(&self) -> &BlockHeaderData {
|
||||
&self.0
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,12 +62,31 @@ pub struct BlockHeaderData {
|
|||
}
|
||||
|
||||
impl BlockHeaderData {
|
||||
pub fn freeze(self) -> BlockHeader {
|
||||
BlockHeader(self)
|
||||
pub fn freeze(self) -> io::Result<BlockHeader> {
|
||||
BlockHeader::from_data(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockHeader {
|
||||
fn from_data(data: BlockHeaderData) -> io::Result<Self> {
|
||||
let mut header = BlockHeader {
|
||||
hash: BlockHash([0; 32]),
|
||||
data,
|
||||
};
|
||||
let mut raw = vec![];
|
||||
header.write(&mut raw)?;
|
||||
header
|
||||
.hash
|
||||
.0
|
||||
.copy_from_slice(&Sha256::digest(&Sha256::digest(&raw)));
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
/// Returns the hash of this header.
|
||||
pub fn hash(&self) -> BlockHash {
|
||||
self.hash
|
||||
}
|
||||
|
||||
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||
let version = reader.read_i32::<LittleEndian>()?;
|
||||
|
||||
|
@ -70,7 +107,7 @@ impl BlockHeader {
|
|||
|
||||
let solution = Vector::read(&mut reader, |r| r.read_u8())?;
|
||||
|
||||
Ok(BlockHeader(BlockHeaderData {
|
||||
BlockHeader::from_data(BlockHeaderData {
|
||||
version,
|
||||
prev_block,
|
||||
merkle_root,
|
||||
|
@ -79,7 +116,7 @@ impl BlockHeader {
|
|||
bits,
|
||||
nonce,
|
||||
solution,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||
|
@ -206,6 +243,10 @@ mod tests {
|
|||
#[test]
|
||||
fn header_read_write() {
|
||||
let header = BlockHeader::read(&HEADER_MAINNET_415000[..]).unwrap();
|
||||
assert_eq!(
|
||||
format!("{}", header.hash()),
|
||||
"0000000001ab37793ce771262b2ffa082519aa3fe891250a1adb43baaf856168"
|
||||
);
|
||||
let mut encoded = Vec::with_capacity(HEADER_MAINNET_415000.len());
|
||||
header.write(&mut encoded).unwrap();
|
||||
assert_eq!(&HEADER_MAINNET_415000[..], &encoded[..]);
|
||||
|
|
Loading…
Reference in New Issue