Merge branch 'master' into zcash_client_sqlite-0.2.1

This commit is contained in:
Jack Grigg 2020-10-24 00:44:37 +01:00
commit 450d68f073
53 changed files with 4019 additions and 644 deletions

View File

@ -73,17 +73,17 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: build
args: --verbose --release --all --tests
args: --all-features --verbose --release --all --tests
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
args: --verbose --release --all
args: --all-features --verbose --release --all
- name: Run slow tests
uses: actions-rs/cargo@v1
with:
command: test
args: --verbose --release --all -- --ignored
args: --all-features --verbose --release --all -- --ignored
build:
name: Build target ${{ matrix.target }}
@ -145,7 +145,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: tarpaulin
args: --release --timeout 600 --out Xml
args: --all-features --release --timeout 600 --out Xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1.0.3
with:

View File

@ -3,6 +3,7 @@ members = [
"components/equihash",
"zcash_client_backend",
"zcash_client_sqlite",
"zcash_extensions",
"zcash_history",
"zcash_primitives",
"zcash_proofs",

View File

@ -15,20 +15,27 @@ edition = "2018"
bech32 = "0.7"
bls12_381 = "0.3.1"
bs58 = { version = "0.3", features = ["check"] }
base64 = "0.12.3"
ff = "0.8"
group = "0.8"
hex = "0.4"
jubjub = "0.5.1"
nom = "5.1.2"
protobuf = "2.15"
subtle = "2"
subtle = "2.2.3"
zcash_primitives = { version = "0.4", path = "../zcash_primitives" }
proptest = { version = "0.10.1", optional = true }
percent-encoding = "2.1.0"
[build-dependencies]
protobuf-codegen-pure = "2.15"
[dev-dependencies]
rand_core = "0.5"
rand_core = "0.5.1"
rand_xorshift = "0.2"
[features]
test-dependencies = ["proptest", "zcash_primitives/test-dependencies"]
[badges]
maintenance = { status = "actively-developed" }

View File

@ -0,0 +1,9 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 150e79b7e542f49689fad3c6e2bb3f830a78e4f2035e9fbab094d62e65f361d5 # shrinks to req = Zip321Request { payments: [Zip321Payment { recipient_address: SaplingAddress(PaymentAddress { pk_d: SubgroupPoint(ExtendedPoint { u: 0x25621e6b6515e85443822759660e9eef04918451ac4cd1263b8087d00689fdff, v: 0x11baaa27bc00dd67e8e03c98ca86c14b1f3ac420242f8e5cc5f9db1dced59b64, z: 0x168e654728395ce631656f756b4ae31dd19b6be3a31f55fd801f8272c5eb1dac, t1: 0x312ff1fd7ffb718f705f340bee2d80a6a0182efd81dd8f2a78cc468320be470f, t2: 0x0af52c1864feef43acb492b8bb106570ecca0ecd73567d8b2888769bb6022564 }), diversifier: Diversifier([59, 246, 250, 31, 131, 191, 69, 99, 200, 167, 19]) }), amount: Amount(0), memo: None, label: None, message: Some(""), other_params: [] }] }
cc 4921b2aeb95647214cae831c2a40c2da841b33056d596b618b121e8d9bdae004 # shrinks to req = Zip321Request { payments: [Zip321Payment { recipient_address: SaplingAddress(PaymentAddress { pk_d: SubgroupPoint(ExtendedPoint { u: 0x1839876c6f00e403c69acb59cff042cf638856a9f38c4f0c081079b95475d6b8, v: 0x0e77e2bfcd48f1810fd4d9048c43238d8907462c1b12df142afe699cbbff4726, z: 0x57f3eeff0f9f6cf0677400d879d5fcfad7225ab4f89dd92747a1a973245efdc3, t1: 0x738d7029ac18e07c08297164fe6b5a2037c190a001a8ead3bc2f10e99de26c15, t2: 0x2d1743b0cc197ba0f3fce6a1c0408061ac60069bb40cf4e7811bea4ca1131220 }), diversifier: Diversifier([248, 7, 134, 66, 81, 3, 199, 181, 44, 244, 120]) }), amount: Amount(599575172834044), memo: None, label: Some("🕴¶<F09F95B4>"), message: None, other_params: [] }, Zip321Payment { recipient_address: TransparentAddress(PublicKey([206, 232, 130, 17, 189, 45, 39, 178, 188, 235, 119, 192, 125, 77, 41, 16, 138, 114, 87, 129])), amount: Amount(89863836207252), memo: Some(Memo { memo: "[40, 116, 101, 89, 15, 198, 213, 14, 20, 5, 135, 188, 186, 45, 215, 189, 209]..." }), label: None, message: None, other_params: [] }] }
cc cfa93b7c127725cf624d9d47a1c28cda3ce195e2fa7d634de9cd0057bf0a75bd # shrinks to mut req = Zip321Request { payments: [Zip321Payment { recipient_address: Sapling(PaymentAddress { pk_d: SubgroupPoint(ExtendedPoint { u: 0x25621e6b6515e85443822759660e9eef04918451ac4cd1263b8087d00689fdff, v: 0x11baaa27bc00dd67e8e03c98ca86c14b1f3ac420242f8e5cc5f9db1dced59b64, z: 0x168e654728395ce631656f756b4ae31dd19b6be3a31f55fd801f8272c5eb1dac, t1: 0x312ff1fd7ffb718f705f340bee2d80a6a0182efd81dd8f2a78cc468320be470f, t2: 0x0af52c1864feef43acb492b8bb106570ecca0ecd73567d8b2888769bb6022564 }), diversifier: Diversifier([59, 246, 250, 31, 131, 191, 69, 99, 200, 167, 19]) }), amount: Amount(0), memo: Some(Memo("")), label: None, message: None, other_params: [] }] }

View File

@ -1,22 +1,14 @@
//! Structs for handling supported address types.
use zcash_client_backend::encoding::{
use zcash_primitives::{consensus, legacy::TransparentAddress, primitives::PaymentAddress};
use crate::encoding::{
decode_payment_address, decode_transparent_address, encode_payment_address,
encode_transparent_address,
};
use zcash_primitives::{legacy::TransparentAddress, primitives::PaymentAddress};
#[cfg(feature = "mainnet")]
use zcash_client_backend::constants::mainnet::{
B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX, HRP_SAPLING_PAYMENT_ADDRESS,
};
#[cfg(not(feature = "mainnet"))]
use zcash_client_backend::constants::testnet::{
B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX, HRP_SAPLING_PAYMENT_ADDRESS,
};
/// An address that funds can be sent to.
#[derive(Debug, PartialEq, Clone)]
pub enum RecipientAddress {
Shielded(PaymentAddress),
Transparent(TransparentAddress),
@ -35,26 +27,28 @@ impl From<TransparentAddress> for RecipientAddress {
}
impl RecipientAddress {
pub fn from_str(s: &str) -> Option<Self> {
if let Ok(Some(pa)) = decode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, s) {
pub fn decode<P: consensus::Parameters>(params: &P, s: &str) -> Option<Self> {
if let Ok(Some(pa)) = decode_payment_address(params.hrp_sapling_payment_address(), s) {
Some(pa.into())
} else if let Ok(Some(addr)) =
decode_transparent_address(&B58_PUBKEY_ADDRESS_PREFIX, &B58_SCRIPT_ADDRESS_PREFIX, s)
{
} else if let Ok(Some(addr)) = decode_transparent_address(
&params.b58_pubkey_address_prefix(),
&params.b58_script_address_prefix(),
s,
) {
Some(addr.into())
} else {
None
}
}
pub fn to_string(&self) -> String {
pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String {
match self {
RecipientAddress::Shielded(pa) => {
encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, pa)
encode_payment_address(params.hrp_sapling_payment_address(), pa)
}
RecipientAddress::Transparent(addr) => encode_transparent_address(
&B58_PUBKEY_ADDRESS_PREFIX,
&B58_SCRIPT_ADDRESS_PREFIX,
&params.b58_pubkey_address_prefix(),
&params.b58_script_address_prefix(),
addr,
),
}

View File

@ -1,5 +0,0 @@
//! Zcash global and per-network constants.
pub mod mainnet;
pub mod regtest;
pub mod testnet;

View File

@ -1,6 +1,5 @@
use group::cofactor::CofactorGroup;
use zcash_primitives::{
consensus,
consensus::{self, BlockHeight},
note_encryption::{try_sapling_note_decryption, try_sapling_output_recovery, Memo},
primitives::{Note, PaymentAddress},
transaction::Transaction,
@ -31,7 +30,8 @@ pub struct DecryptedOutput {
/// Scans a [`Transaction`] for any information that can be decrypted by the set of
/// [`ExtendedFullViewingKey`]s.
pub fn decrypt_transaction<P: consensus::Parameters>(
height: u32,
params: &P,
height: BlockHeight,
tx: &Transaction,
extfvks: &[ExtendedFullViewingKey],
) -> Vec<DecryptedOutput> {
@ -44,27 +44,23 @@ pub fn decrypt_transaction<P: consensus::Parameters>(
.collect();
for (index, output) in tx.shielded_outputs.iter().enumerate() {
let epk = output.ephemeral_key.into_subgroup();
if epk.is_none().into() {
continue;
}
let epk = epk.unwrap();
for (account, (ivk, ovk)) in vks.iter().enumerate() {
let ((note, to, memo), outgoing) = match try_sapling_note_decryption::<P>(
let ((note, to, memo), outgoing) = match try_sapling_note_decryption(
params,
height,
ivk,
&epk,
&output.ephemeral_key,
&output.cmu,
&output.enc_ciphertext,
) {
Some(ret) => (ret, false),
None => match try_sapling_output_recovery::<P>(
None => match try_sapling_output_recovery(
params,
height,
ovk,
&output.cv,
&output.cmu,
&epk,
&output.ephemeral_key,
&output.enc_ciphertext,
&output.out_ciphertext,
) {

View File

@ -1,9 +1,9 @@
//! Encoding and decoding functions for Zcash key and address structs.
//!
//! Human-Readable Prefixes (HRPs) for Bech32 encodings are located in the [`constants`]
//! Human-Readable Prefixes (HRPs) for Bech32 encodings are located in the [`zcash_primitives::constants`]
//! module.
//!
//! [`constants`]: crate::constants
//! [`constants`]: zcash_primitives::constants
use bech32::{self, Error, FromBase32, ToBase32};
use bs58::{self, decode::Error as Bs58Error};
@ -41,8 +41,10 @@ where
/// # Examples
///
/// ```
/// use zcash_client_backend::{
/// use zcash_primitives::{
/// constants::testnet::{COIN_TYPE, HRP_SAPLING_EXTENDED_SPENDING_KEY},
/// };
/// use zcash_client_backend::{
/// encoding::encode_extended_spending_key,
/// keys::spending_key,
/// };
@ -67,8 +69,10 @@ pub fn decode_extended_spending_key(
/// # Examples
///
/// ```
/// use zcash_client_backend::{
/// use zcash_primitives::{
/// constants::testnet::{COIN_TYPE, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY},
/// };
/// use zcash_client_backend::{
/// encoding::encode_extended_full_viewing_key,
/// keys::spending_key,
/// };
@ -100,10 +104,12 @@ pub fn decode_extended_full_viewing_key(
/// use rand_core::SeedableRng;
/// use rand_xorshift::XorShiftRng;
/// use zcash_client_backend::{
/// constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS,
/// encoding::encode_payment_address,
/// };
/// use zcash_primitives::primitives::{Diversifier, PaymentAddress};
/// use zcash_primitives::{
/// constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS,
/// primitives::{Diversifier, PaymentAddress},
/// };
///
/// let rng = &mut XorShiftRng::from_seed([
/// 0x59, 0x62, 0xbe, 0x3d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06,
@ -135,10 +141,12 @@ pub fn encode_payment_address(hrp: &str, addr: &PaymentAddress) -> String {
/// use rand_core::SeedableRng;
/// use rand_xorshift::XorShiftRng;
/// use zcash_client_backend::{
/// constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS,
/// encoding::decode_payment_address,
/// };
/// use zcash_primitives::primitives::{Diversifier, PaymentAddress};
/// use zcash_primitives::{
/// constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS,
/// primitives::{Diversifier, PaymentAddress},
/// };
///
/// let rng = &mut XorShiftRng::from_seed([
/// 0x59, 0x62, 0xbe, 0x3d, 0x76, 0x3d, 0x31, 0x8d, 0x17, 0xdb, 0x37, 0x32, 0x54, 0x06,
@ -177,10 +185,12 @@ pub fn decode_payment_address(hrp: &str, s: &str) -> Result<Option<PaymentAddres
///
/// ```
/// use zcash_client_backend::{
/// constants::testnet::{B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX},
/// encoding::encode_transparent_address,
/// };
/// use zcash_primitives::legacy::TransparentAddress;
/// use zcash_primitives::{
/// constants::testnet::{B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX},
/// legacy::TransparentAddress,
/// };
///
/// assert_eq!(
/// encode_transparent_address(
@ -227,8 +237,10 @@ pub fn encode_transparent_address(
/// # Examples
///
/// ```
/// use zcash_client_backend::{
/// use zcash_primitives::{
/// constants::testnet::{B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX},
/// };
/// use zcash_client_backend::{
/// encoding::decode_transparent_address,
/// };
/// use zcash_primitives::legacy::TransparentAddress;
@ -279,6 +291,7 @@ mod tests {
use rand_core::SeedableRng;
use rand_xorshift::XorShiftRng;
use zcash_primitives::{
constants,
primitives::{Diversifier, PaymentAddress},
zip32::ExtendedSpendingKey,
};
@ -287,7 +300,6 @@ mod tests {
decode_extended_full_viewing_key, decode_extended_spending_key, decode_payment_address,
encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address,
};
use crate::constants;
#[test]
fn extended_spending_key() {

View File

@ -12,7 +12,8 @@ use zcash_primitives::zip32::{ChildIndex, ExtendedSpendingKey};
/// # Examples
///
/// ```
/// use zcash_client_backend::{constants::testnet::COIN_TYPE, keys::spending_key};
/// use zcash_primitives::{constants::testnet::COIN_TYPE};
/// use zcash_client_backend::{keys::spending_key};
///
/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, 0);
/// ```

View File

@ -6,12 +6,13 @@
// Catch documentation errors caused by code changes.
#![deny(intra_doc_link_resolution_failure)]
pub mod constants;
pub mod address;
mod decrypt;
pub mod encoding;
pub mod keys;
pub mod proto;
pub mod wallet;
pub mod welding_rig;
pub mod zip321;
pub use decrypt::{decrypt_transaction, DecryptedOutput};

View File

@ -3,7 +3,11 @@
use ff::PrimeField;
use group::GroupEncoding;
use std::convert::TryInto;
use zcash_primitives::block::{BlockHash, BlockHeader};
use zcash_primitives::{
block::{BlockHash, BlockHeader},
consensus::BlockHeight,
};
pub mod compact_formats;
@ -54,6 +58,15 @@ impl compact_formats::CompactBlock {
BlockHeader::read(&self.header[..]).ok()
}
}
/// Returns the [`BlockHeight`] for this block.
///
/// A convenience method that wraps [`CompactBlock.height`]
///
/// [`CompactBlock.height`]: #structfield.height
pub fn height(&self) -> BlockHeight {
BlockHeight::from(self.height)
}
}
impl compact_formats::CompactOutput {
@ -73,8 +86,8 @@ impl compact_formats::CompactOutput {
/// A convenience method that parses [`CompactOutput.epk`].
///
/// [`CompactOutput.epk`]: #structfield.epk
pub fn epk(&self) -> Result<jubjub::SubgroupPoint, ()> {
let p = jubjub::SubgroupPoint::from_bytes(&self.epk[..].try_into().map_err(|_| ())?);
pub fn epk(&self) -> Result<jubjub::ExtendedPoint, ()> {
let p = jubjub::ExtendedPoint::from_bytes(&self.epk[..].try_into().map_err(|_| ())?);
if p.is_some().into() {
Ok(p.unwrap())
} else {

View File

@ -35,7 +35,7 @@ pub struct WalletShieldedSpend {
pub struct WalletShieldedOutput {
pub index: usize,
pub cmu: bls12_381::Scalar,
pub epk: jubjub::SubgroupPoint,
pub epk: jubjub::ExtendedPoint,
pub account: usize,
pub note: Note,
pub to: PaymentAddress,

View File

@ -4,7 +4,7 @@ use ff::PrimeField;
use std::collections::HashSet;
use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption};
use zcash_primitives::{
consensus,
consensus::{self, BlockHeight},
merkle_tree::{CommitmentTree, IncrementalWitness},
note_encryption::try_sapling_compact_note_decryption,
sapling::Node,
@ -23,7 +23,8 @@ use crate::wallet::{WalletShieldedOutput, WalletShieldedSpend, WalletTx};
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are incremented
/// with this output's commitment.
fn scan_output<P: consensus::Parameters>(
height: u32,
params: &P,
height: BlockHeight,
(index, output): (usize, CompactOutput),
ivks: &[jubjub::Fr],
spent_from_accounts: &HashSet<usize>,
@ -51,7 +52,7 @@ fn scan_output<P: consensus::Parameters>(
for (account, ivk) in ivks.iter().enumerate() {
let (note, to) =
match try_sapling_compact_note_decryption::<P>(height, ivk, &epk, &cmu, &ct) {
match try_sapling_compact_note_decryption(params, height, ivk, &epk, &cmu, &ct) {
Some(ret) => ret,
None => continue,
};
@ -86,6 +87,7 @@ fn scan_output<P: consensus::Parameters>(
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are
/// incremented appropriately.
pub fn scan_block<P: consensus::Parameters>(
params: &P,
block: CompactBlock,
extfvks: &[ExtendedFullViewingKey],
nullifiers: &[(&[u8], usize)],
@ -94,6 +96,7 @@ pub fn scan_block<P: consensus::Parameters>(
) -> Vec<WalletTx> {
let mut wtxs: Vec<WalletTx> = vec![];
let ivks: Vec<_> = extfvks.iter().map(|extfvk| extfvk.fvk.vk.ivk()).collect();
let block_height = block.height();
for tx in block.vtx.into_iter() {
let num_spends = tx.spends.len();
@ -152,8 +155,9 @@ pub fn scan_block<P: consensus::Parameters>(
.map(|output| &mut output.witness)
.collect();
if let Some(output) = scan_output::<P>(
block.height as u32,
if let Some(output) = scan_output(
params,
block_height,
to_scan,
&ivks,
&spent_from_accounts,
@ -190,7 +194,7 @@ mod tests {
use group::GroupEncoding;
use rand_core::{OsRng, RngCore};
use zcash_primitives::{
consensus::TestNetwork,
consensus::{BlockHeight, Network},
constants::SPENDING_KEY_GENERATOR,
merkle_tree::CommitmentTree,
note_encryption::{Memo, SaplingNoteEncryption},
@ -239,7 +243,7 @@ mod tests {
/// single spend of the given nullifier and a single output paying the given address.
/// Returns the CompactBlock.
fn fake_compact_block(
height: i32,
height: BlockHeight,
nf: [u8; 32],
extfvk: ExtendedFullViewingKey,
value: Amount,
@ -249,7 +253,7 @@ mod tests {
// Create a fake Note for the account
let mut rng = OsRng;
let rseed = generate_random_rseed::<TestNetwork, OsRng>(height as u32, &mut rng);
let rseed = generate_random_rseed(&Network::TestNetwork, height, &mut rng);
let note = Note {
g_d: to.diversifier().g_d().unwrap(),
pk_d: to.pk_d().clone(),
@ -269,7 +273,7 @@ mod tests {
// Create a fake CompactBlock containing the note
let mut cb = CompactBlock::new();
cb.set_height(height as u64);
cb.set_height(height.into());
// Add a random Sapling tx before ours
{
@ -309,7 +313,7 @@ mod tests {
let extfvk = ExtendedFullViewingKey::from(&extsk);
let cb = fake_compact_block(
1,
1u32.into(),
[0; 32],
extfvk.clone(),
Amount::from_u64(5).unwrap(),
@ -318,7 +322,14 @@ mod tests {
assert_eq!(cb.vtx.len(), 2);
let mut tree = CommitmentTree::new();
let txs = scan_block::<TestNetwork>(cb, &[extfvk], &[], &mut tree, &mut []);
let txs = scan_block(
&Network::TestNetwork,
cb,
&[extfvk],
&[],
&mut tree,
&mut [],
);
assert_eq!(txs.len(), 1);
let tx = &txs[0];
@ -341,7 +352,7 @@ mod tests {
let extfvk = ExtendedFullViewingKey::from(&extsk);
let cb = fake_compact_block(
1,
1u32.into(),
[0; 32],
extfvk.clone(),
Amount::from_u64(5).unwrap(),
@ -350,7 +361,14 @@ mod tests {
assert_eq!(cb.vtx.len(), 3);
let mut tree = CommitmentTree::new();
let txs = scan_block::<TestNetwork>(cb, &[extfvk], &[], &mut tree, &mut []);
let txs = scan_block(
&Network::TestNetwork,
cb,
&[extfvk],
&[],
&mut tree,
&mut [],
);
assert_eq!(txs.len(), 1);
let tx = &txs[0];
@ -374,11 +392,18 @@ mod tests {
let nf = [7; 32];
let account = 12;
let cb = fake_compact_block(1, nf, extfvk, Amount::from_u64(5).unwrap(), false);
let cb = fake_compact_block(1u32.into(), nf, extfvk, Amount::from_u64(5).unwrap(), false);
assert_eq!(cb.vtx.len(), 2);
let mut tree = CommitmentTree::new();
let txs = scan_block::<TestNetwork>(cb, &[], &[(&nf, account)], &mut tree, &mut []);
let txs = scan_block(
&Network::TestNetwork,
cb,
&[],
&[(&nf, account)],
&mut tree,
&mut [],
);
assert_eq!(txs.len(), 1);
let tx = &txs[0];

View File

@ -0,0 +1,982 @@
use core::fmt::Debug;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use base64;
use nom::{
character::complete::char, combinator::all_consuming, multi::separated_list, sequence::preceded,
};
use zcash_primitives::{consensus, transaction::components::Amount};
use crate::address::RecipientAddress;
pub struct RawMemo([u8; 512]);
#[derive(Debug)]
pub enum MemoError {
InvalidBase64(base64::DecodeError),
LengthExceeded(usize),
}
impl RawMemo {
pub fn from_str(s: &str) -> Result<Self, MemoError> {
RawMemo::from_bytes(s.as_bytes())
}
// Construct a raw memo from a vector of bytes.
pub fn from_bytes(v: &[u8]) -> Result<Self, MemoError> {
if v.len() > 512 {
Err(MemoError::LengthExceeded(v.len()))
} else {
let mut memo: [u8; 512] = [0; 512];
memo[..v.len()].copy_from_slice(&v);
Ok(RawMemo(memo))
}
}
pub fn to_base64(&self) -> String {
// strip trailing zero bytes.
let mut last_nonzero = -1;
for i in (0..(self.0.len())).rev() {
if self.0[i] != 0x0 {
last_nonzero = i as i64;
break;
}
}
base64::encode_config(
&self.0[..((last_nonzero + 1) as usize)],
base64::URL_SAFE_NO_PAD,
)
}
pub fn from_base64(s: &str) -> Result<Self, MemoError> {
base64::decode_config(s, base64::URL_SAFE_NO_PAD)
.map_err(MemoError::InvalidBase64)
.and_then(|b| RawMemo::from_bytes(&b))
}
}
impl Debug for RawMemo {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
f.debug_struct("RawMemo")
.field("memo", &format!("{:?}...", &self.0[0..17]))
.finish()
}
}
impl PartialEq for RawMemo {
fn eq(&self, other: &Self) -> bool {
self.0[..] == other.0[..]
}
}
impl FromStr for RawMemo {
type Err = MemoError;
fn from_str(memo: &str) -> Result<Self, Self::Err> {
RawMemo::from_str(memo)
}
}
impl Eq for RawMemo {}
impl PartialOrd for RawMemo {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.to_base64().cmp(&other.to_base64()))
}
}
impl Ord for RawMemo {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap()
}
}
// RawMemo is somewhat duplicative of the `Memo` type
// in crate::note_encryption but as that's actively being
// updated at time of this writing, these functions provide
// shims to ease future use of those
pub fn memo_from_vec(v: &[u8]) -> Result<RawMemo, MemoError> {
RawMemo::from_bytes(v)
}
pub fn memo_to_base64(memo: &RawMemo) -> String {
memo.to_base64()
}
pub fn memo_from_base64(s: &str) -> Result<RawMemo, MemoError> {
RawMemo::from_base64(s)
}
#[derive(Debug, PartialEq)]
pub struct Payment {
recipient_address: RecipientAddress,
amount: Amount,
memo: Option<RawMemo>,
label: Option<String>,
message: Option<String>,
other_params: Vec<(String, String)>,
}
impl Payment {
#[cfg(any(test, feature = "test-dependencies"))]
pub(in crate::zip321) fn normalize(&mut self) {
self.other_params.sort();
}
/// Returns a function which compares two normalized payments, with addresses sorted by their
/// string representation given the specified network. This does not perform normalization
/// internally, so payments must be normalized prior to being passed to the comparison function
/// returned from this method.
#[cfg(any(test, feature = "test-dependencies"))]
pub(in crate::zip321) fn compare_normalized<'a, P: consensus::Parameters>(
params: &'a P,
) -> impl Fn(&Payment, &Payment) -> Ordering + 'a {
move |a: &Payment, b: &Payment| {
let a_addr = a.recipient_address.encode(params);
let b_addr = b.recipient_address.encode(params);
a_addr
.cmp(&b_addr)
.then(a.amount.cmp(&b.amount))
.then(a.memo.cmp(&b.memo))
.then(a.label.cmp(&b.label))
.then(a.message.cmp(&b.message))
.then(a.other_params.cmp(&b.other_params))
}
}
}
#[derive(Debug, PartialEq)]
pub struct TransactionRequest {
payments: Vec<Payment>,
}
impl TransactionRequest {
#[cfg(any(test, feature = "test-dependencies"))]
pub(in crate::zip321) fn normalize<P: consensus::Parameters>(&mut self, params: &P) {
for p in &mut self.payments {
p.normalize();
}
self.payments.sort_by(Payment::compare_normalized(params));
}
#[cfg(all(test, feature = "test-dependencies"))]
pub(in crate::zip321) fn normalize_and_eq<P: consensus::Parameters>(
params: &P,
a: &mut TransactionRequest,
b: &mut TransactionRequest,
) -> bool {
a.normalize(params);
b.normalize(params);
a == b
}
/// Convert this request to a URI string.
///
/// Returns None if the payment request is empty.
pub fn to_uri<P: consensus::Parameters>(&self, params: &P) -> Option<String> {
fn payment_params<'a>(
payment: &'a Payment,
payment_index: Option<usize>,
) -> impl IntoIterator<Item = String> + 'a {
std::iter::empty()
.chain(render::amount_param(payment.amount, payment_index))
.chain(
payment
.memo
.as_ref()
.map(|m| render::memo_param(&m, payment_index)),
)
.chain(
payment
.label
.as_ref()
.map(|m| render::str_param("label", &m, payment_index)),
)
.chain(
payment
.message
.as_ref()
.map(|m| render::str_param("message", &m, payment_index)),
)
.chain(
payment
.other_params
.iter()
.map(move |(name, value)| render::str_param(&name, &value, payment_index)),
)
}
match &self.payments[..] {
[] => None,
[payment] => {
let query_params = payment_params(&payment, None)
.into_iter()
.collect::<Vec<String>>();
Some(format!(
"zcash:{}?{}",
payment.recipient_address.encode(params),
query_params.join("&")
))
}
_ => {
let query_params = self
.payments
.iter()
.enumerate()
.flat_map(|(i, payment)| {
let primary_address = payment.recipient_address.clone();
std::iter::empty()
.chain(Some(render::addr_param(params, &primary_address, Some(i))))
.chain(payment_params(&payment, Some(i)))
})
.collect::<Vec<String>>();
Some(format!("zcash:?{}", query_params.join("&")))
}
}
}
/// Parse the provided URI to a payment request value.
pub fn from_uri<P: consensus::Parameters>(params: &P, uri: &str) -> Result<Self, String> {
// Parse the leading zcash:<address>
let (rest, primary_addr_param) =
parse::lead_addr(params)(uri).map_err(|e| e.to_string())?;
// Parse the remaining parameters as an undifferentiated list
let (_, xs) = all_consuming(preceded(
char('?'),
separated_list(char('&'), parse::zcashparam(params)),
))(rest)
.map_err(|e| e.to_string())?;
// Construct sets of payment parameters, keyed by the payment index.
let mut params_by_index: HashMap<usize, Vec<parse::Param>> = HashMap::new();
// Add the primary address, if any, to the index.
if let Some(p) = primary_addr_param {
params_by_index.insert(p.payment_index, vec![p.param]);
}
// Group the remaining parameters by payment index
for p in xs {
match params_by_index.get_mut(&p.payment_index) {
None => {
params_by_index.insert(p.payment_index, vec![p.param]);
}
Some(current) => {
if parse::has_duplicate_param(&current, &p.param) {
return Err(format!(
"Found duplicate parameter {:?} at index {}",
p.param, p.payment_index
));
} else {
current.push(p.param);
}
}
}
}
// Build the actual payment values from the index.
params_by_index
.into_iter()
.map(|(i, params)| parse::to_payment(params, i))
.collect::<Result<Vec<_>, _>>()
.map(|payments| TransactionRequest { payments })
}
}
mod render {
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use zcash_primitives::{
consensus, transaction::components::amount::COIN, transaction::components::Amount,
};
use super::{memo_to_base64, RawMemo, RecipientAddress};
// The set of ASCII characters that must be percent-encoded according
// to the definition of ZIP 321. This is the complement of the subset of
// ASCII characters defined by `qchar`
//
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
// allowed-delims = "!" / "$" / "'" / "(" / ")" / "*" / "+" / "," / ";"
// qchar = unreserved / pct-encoded / allowed-delims / ":" / "@"
pub const QCHAR_ENCODE: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'%')
.add(b'&')
.add(b'/')
.add(b'<')
.add(b'=')
.add(b'>')
.add(b'?')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
pub fn param_index(idx: Option<usize>) -> String {
match idx {
Some(i) if i > 0 => format!(".{}", i),
_otherwise => "".to_string(),
}
}
pub fn addr_param<P: consensus::Parameters>(
params: &P,
addr: &RecipientAddress,
idx: Option<usize>,
) -> String {
format!("address{}={}", param_index(idx), addr.encode(params))
}
pub fn amount_str(amount: Amount) -> Option<String> {
if amount.is_positive() {
let coins = i64::from(amount) / COIN;
let zats = i64::from(amount) % COIN;
Some(if zats == 0 {
format!("{}", coins)
} else {
format!("{}.{:0>8}", coins, zats)
.trim_end_matches('0')
.to_string()
})
} else {
None
}
}
pub fn amount_param(amount: Amount, idx: Option<usize>) -> Option<String> {
amount_str(amount).map(|s| format!("amount{}={}", param_index(idx), s))
}
pub fn memo_param(value: &RawMemo, idx: Option<usize>) -> String {
format!("{}{}={}", "memo", param_index(idx), memo_to_base64(value))
}
pub fn str_param(label: &str, value: &str, idx: Option<usize>) -> String {
format!(
"{}{}={}",
label,
param_index(idx),
utf8_percent_encode(value, QCHAR_ENCODE)
)
}
}
mod parse {
use core::fmt::Debug;
use nom::{
bytes::complete::{tag, take_until},
character::complete::{alpha1, char, digit0, digit1, one_of},
combinator::{map_opt, map_res, opt, recognize},
sequence::{preceded, separated_pair, tuple},
AsChar, IResult, InputTakeAtPosition,
};
use percent_encoding::percent_decode;
use zcash_primitives::{
consensus, transaction::components::amount::COIN, transaction::components::Amount,
};
use crate::address::RecipientAddress;
use super::{memo_from_base64, Payment, RawMemo};
// For purposes of parsing
#[derive(Debug, PartialEq)]
pub enum Param {
Addr(RecipientAddress),
Amount(Amount),
Memo(RawMemo),
Label(String),
Message(String),
Other(String, String),
}
#[derive(Debug)]
pub struct IndexedParam {
pub param: Param,
pub payment_index: usize,
}
pub fn has_duplicate_param(v: &[Param], p: &Param) -> bool {
for p0 in v {
match (p0, p) {
(Param::Addr(_), Param::Addr(_)) => return true,
(Param::Amount(_), Param::Amount(_)) => return true,
(Param::Memo(_), Param::Memo(_)) => return true,
(Param::Label(_), Param::Label(_)) => return true,
(Param::Message(_), Param::Message(_)) => return true,
(Param::Other(n, _), Param::Other(n0, _)) if (n == n0) => return true,
_otherwise => continue,
}
}
return false;
}
pub fn to_payment(vs: Vec<Param>, i: usize) -> Result<Payment, String> {
let addr = vs.iter().find_map(|v| match v {
Param::Addr(a) => Some(a.clone()),
_otherwise => None,
});
let mut payment = Payment {
recipient_address: addr.ok_or(format!("Payment {} had no recipient address.", i))?,
amount: Amount::zero(),
memo: None,
label: None,
message: None,
other_params: vec![],
};
for v in vs {
match v {
Param::Amount(a) => payment.amount = a.clone(),
Param::Memo(m) => {
match payment.recipient_address {
RecipientAddress::Shielded(_) => payment.memo = Some(m),
RecipientAddress::Transparent(_) => return Err(format!("Payment {} attempted to associate a memo with a transparent recipient address", i)),
}
},
Param::Label(m) => payment.label = Some(m),
Param::Message(m) => payment.message = Some(m),
Param::Other(n, m) => payment.other_params.push((n, m)),
_otherwise => {}
}
}
return Ok(payment);
}
/// Parser that consumes the leading "zcash:[address]" from
/// a ZIP 321 URI.
pub fn lead_addr<'a, P: consensus::Parameters>(
params: &'a P,
) -> impl Fn(&str) -> IResult<&str, Option<IndexedParam>> + 'a {
move |input: &str| {
map_opt(preceded(tag("zcash:"), take_until("?")), |addr_str| {
if addr_str == "" {
Some(None) // no address is ok, so wrap in `Some`
} else {
// `decode` returns `None` on error, which we want to
// then cause `map_opt` to fail.
RecipientAddress::decode(params, addr_str).map(|a| {
Some(IndexedParam {
param: Param::Addr(a),
payment_index: 0,
})
})
}
})(input)
}
}
/// The primary parser for <name>=<value> query-string
/// parameter pair.
pub fn zcashparam<'a, P: consensus::Parameters>(
params: &'a P,
) -> impl Fn(&str) -> IResult<&str, IndexedParam> + 'a {
move |input| {
map_res(
separated_pair(indexed_name, char('='), recognize(qchars)),
move |r| to_indexed_param(params, r),
)(input)
}
}
/// Extension for the `alphanumeric0` parser which extends that parser
/// by also permitting the characters that are members of the `allowed`
/// string.
fn alphanum_or(allowed: &str) -> impl (Fn(&str) -> IResult<&str, &str>) + '_ {
move |input| {
input.split_at_position_complete(|item| {
let c = item.as_char();
!(c.is_alphanum() || allowed.contains(c))
})
}
}
pub fn qchars(input: &str) -> IResult<&str, &str> {
alphanum_or("-._~!$'()*+,;:@%")(input)
}
pub fn namechars(input: &str) -> IResult<&str, &str> {
alphanum_or("+-")(input)
}
pub fn indexed_name(input: &str) -> IResult<&str, (&str, Option<&str>)> {
let paramname = recognize(tuple((alpha1, namechars)));
tuple((
paramname,
opt(preceded(
char('.'),
recognize(tuple((
one_of("123456789"),
map_opt(digit0, |s: &str| if s.len() > 3 { None } else { Some(s) }),
))),
)),
))(input)
}
pub fn parse_amount<'a>(input: &'a str) -> IResult<&'a str, Amount> {
map_res(
tuple((
digit1,
opt(preceded(
char('.'),
map_opt(digit0, |s: &str| if s.len() > 8 { None } else { Some(s) }),
)),
)),
|(whole_s, decimal_s): (&str, Option<&str>)| {
let coins: i64 = whole_s
.to_string()
.parse::<i64>()
.map_err(|e| e.to_string())?;
let zats: i64 = match decimal_s {
Some(d) => format!("{:0<8}", d)
.parse::<i64>()
.map_err(|e| e.to_string())?,
None => 0,
};
if coins >= 21000000 && (coins > 21000000 || zats > 0) {
return Err(format!(
"{} coins exceeds the maximum possible Zcash value.",
coins
));
}
let amt = coins * COIN + zats;
Amount::from_nonnegative_i64(amt)
.map_err(|_| format!("Not a valid zat amount: {}", amt))
},
)(input)
}
fn to_indexed_param<'a, P: consensus::Parameters>(
params: &'a P,
((name, iopt), value): ((&str, Option<&str>), &str),
) -> Result<IndexedParam, String> {
let param = match name {
"address" => RecipientAddress::decode(params, value)
.map(Param::Addr)
.ok_or(format!(
"Could not interpret {} as a valid Zcash address.",
value
)),
"amount" => parse_amount(value)
.map(|(_, a)| Param::Amount(a))
.map_err(|e| e.to_string()),
"label" => percent_decode(value.as_bytes())
.decode_utf8()
.map(|s| Param::Label(s.into_owned()))
.map_err(|e| e.to_string()),
"message" => percent_decode(value.as_bytes())
.decode_utf8()
.map(|s| Param::Message(s.into_owned()))
.map_err(|e| e.to_string()),
"memo" => memo_from_base64(value)
.map(Param::Memo)
.map_err(|e| format!("Decoded memo was invalid: {:?}", e)),
other if other.starts_with("req-") => {
Err(format!("Required parameter {} not recognized", other))
}
other => percent_decode(value.as_bytes())
.decode_utf8()
.map(|s| Param::Other(other.to_string(), s.into_owned()))
.map_err(|e| e.to_string()),
}?;
let payment_index = match iopt {
Some(istr) => istr.parse::<usize>().map(Some).map_err(|e| e.to_string()),
None => Ok(None),
}?;
Ok(IndexedParam {
param,
payment_index: payment_index.unwrap_or(0),
})
}
}
#[cfg(feature = "test-dependencies")]
pub mod testing {
use proptest::collection::vec;
use proptest::option;
use proptest::prelude::{any, prop_compose, prop_oneof};
use proptest::strategy::Strategy;
use zcash_primitives::{
consensus::TEST_NETWORK, keys::testing::arb_shielded_addr,
legacy::testing::arb_transparent_addr,
transaction::components::amount::testing::arb_nonnegative_amount,
};
use crate::address::RecipientAddress;
use super::{memo_from_vec, Payment, RawMemo, TransactionRequest};
pub fn arb_addr() -> impl Strategy<Value = RecipientAddress> {
prop_oneof![
arb_shielded_addr().prop_map(RecipientAddress::Shielded),
arb_transparent_addr().prop_map(RecipientAddress::Transparent),
]
}
pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*";
prop_compose! {
pub fn arb_valid_memo()(bytes in vec(any::<u8>(), 0..512)) -> RawMemo {
memo_from_vec(&bytes).unwrap()
}
}
prop_compose! {
pub fn arb_zip321_payment()(
recipient_address in arb_addr(),
amount in arb_nonnegative_amount(),
memo in option::of(arb_valid_memo()),
message in option::of(any::<String>()),
label in option::of(any::<String>()),
other_params in vec((VALID_PARAMNAME, any::<String>()), 0..3),
) -> Payment {
let is_sapling = match recipient_address {
RecipientAddress::Transparent(_) => false,
RecipientAddress::Shielded(_) => true,
};
Payment {
recipient_address,
amount,
memo: memo.filter(|_| is_sapling),
label,
message,
other_params,
}
}
}
prop_compose! {
pub fn arb_zip321_request()(payments in vec(arb_zip321_payment(), 1..10)) -> TransactionRequest {
let mut req = TransactionRequest { payments };
req.normalize(&TEST_NETWORK); // just to make test comparisons easier
req
}
}
prop_compose! {
pub fn arb_zip321_uri()(req in arb_zip321_request()) -> String {
req.to_uri(&TEST_NETWORK).unwrap()
}
}
prop_compose! {
pub fn arb_addr_str()(addr in arb_addr()) -> String {
addr.encode(&TEST_NETWORK)
}
}
}
#[cfg(test)]
mod tests {
use zcash_primitives::{
consensus::{Parameters, TEST_NETWORK},
transaction::components::Amount,
};
use crate::address::RecipientAddress;
use super::{
memo_from_base64, memo_to_base64,
parse::{parse_amount, zcashparam, Param},
render::amount_str,
Payment, RawMemo, TransactionRequest,
};
use crate::encoding::decode_payment_address;
#[cfg(all(test, feature = "test-dependencies"))]
use proptest::prelude::{any, proptest};
#[cfg(all(test, feature = "test-dependencies"))]
use zcash_primitives::transaction::components::amount::testing::arb_nonnegative_amount;
#[cfg(all(test, feature = "test-dependencies"))]
use super::{
render::{memo_param, str_param},
testing::{arb_addr, arb_addr_str, arb_valid_memo, arb_zip321_request, arb_zip321_uri},
};
fn check_roundtrip(req: TransactionRequest) {
if let Some(req_uri) = req.to_uri(&TEST_NETWORK) {
let parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap();
assert_eq!(parsed, req);
} else {
panic!("Generated invalid payment request: {:?}", req);
}
}
#[test]
fn test_zip321_roundtrip_simple_amounts() {
let amounts = vec![1u64, 1000u64, 100000u64, 100000000u64, 100000000000u64];
for amt_u64 in amounts {
let amt = Amount::from_u64(amt_u64).unwrap();
let amt_str = amount_str(amt).unwrap();
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
}
}
#[test]
fn test_zip321_parse_empty_message() {
let fragment = "message=";
let result = zcashparam(&TEST_NETWORK)(fragment).unwrap().1.param;
assert_eq!(result, Param::Message("".to_string()));
}
#[test]
fn test_zip321_parse_simple() {
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k?amount=3768769.02796286&message=";
let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, &uri).unwrap();
let expected = TransactionRequest {
payments: vec![
Payment {
recipient_address: RecipientAddress::Shielded(decode_payment_address(&TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap().unwrap()),
amount: Amount::from_u64(376876902796286).unwrap(),
memo: None,
label: None,
message: Some("".to_string()),
other_params: vec![],
}
]
};
assert_eq!(parse_result, expected);
}
#[test]
fn test_zip321_roundtrip_empty_message() {
let req = TransactionRequest {
payments: vec![
Payment {
recipient_address: RecipientAddress::Shielded(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap().unwrap()),
amount: Amount::from_u64(0).unwrap(),
memo: None,
label: None,
message: Some("".to_string()),
other_params: vec![]
}
]
};
check_roundtrip(req);
}
#[test]
fn test_zip321_memos() {
let m_simple: RawMemo = "This is a simple memo.".parse().unwrap();
let m_simple_64 = memo_to_base64(&m_simple);
assert_eq!(memo_from_base64(&m_simple_64).unwrap(), m_simple);
let m_json: RawMemo = "{ \"key\": \"This is a JSON-structured memo.\" }"
.parse()
.unwrap();
let m_json_64 = memo_to_base64(&m_json);
assert_eq!(memo_from_base64(&m_json_64).unwrap(), m_json);
let m_unicode: RawMemo = "This is a unicode memo ✨🦄🏆🎉".parse().unwrap();
let m_unicode_64 = memo_to_base64(&m_unicode);
assert_eq!(memo_from_base64(&m_unicode_64).unwrap(), m_unicode);
}
#[test]
fn test_zip321_spec_valid_examples() {
let valid_1 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase";
let v1r = TransactionRequest::from_uri(&TEST_NETWORK, &valid_1).unwrap();
assert_eq!(
v1r.payments.get(0).map(|p| p.amount),
Some(Amount::from_u64(100000000).unwrap())
);
let valid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok";
let mut v2r = TransactionRequest::from_uri(&TEST_NETWORK, &valid_2).unwrap();
v2r.normalize(&TEST_NETWORK);
assert_eq!(
v2r.payments.get(0).map(|p| p.amount),
Some(Amount::from_u64(12345600000).unwrap())
);
assert_eq!(
v2r.payments.get(1).map(|p| p.amount),
Some(Amount::from_u64(78900000).unwrap())
);
// valid; amount just less than MAX_MONEY
// 20999999.99999999
let valid_3 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=20999999.99999999";
let v3r = TransactionRequest::from_uri(&TEST_NETWORK, &valid_3).unwrap();
assert_eq!(
v3r.payments.get(0).map(|p| p.amount),
Some(Amount::from_u64(2099999999999999u64).unwrap())
);
// valid; MAX_MONEY
// 21000000
let valid_4 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000";
let v4r = TransactionRequest::from_uri(&TEST_NETWORK, &valid_4).unwrap();
assert_eq!(
v4r.payments.get(0).map(|p| p.amount),
Some(Amount::from_u64(2100000000000000u64).unwrap())
);
}
#[test]
fn test_zip321_spec_invalid_examples() {
// invalid; missing `address=`
let invalid_1 = "zcash:?amount=3491405.05201255&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=5740296.87793245";
let i1r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_1);
assert!(i1r.is_err());
// invalid; missing `address.1=`
let invalid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=1&amount.1=2&address.2=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez";
let i2r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_2);
assert!(i2r.is_err());
// invalid; `address.0=` and `amount.0=` are not permitted (leading 0s).
let invalid_3 = "zcash:?address.0=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.0=2";
let i3r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_3);
assert!(i3r.is_err());
// invalid; duplicate `amount=` field
let invalid_4 =
"zcash:?amount=1.234&amount=2.345&address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
let i4r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_4);
assert!(i4r.is_err());
// invalid; duplicate `amount.1=` field
let invalid_5 =
"zcash:?amount.1=1.234&amount.1=2.345&address.1=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
let i5r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_5);
assert!(i5r.is_err());
//invalid; memo associated with t-addr
let invalid_6 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&memo=eyAia2V5IjogIlRoaXMgaXMgYSBKU09OLXN0cnVjdHVyZWQgbWVtby4iIH0&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok";
let i6r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_6);
assert!(i6r.is_err());
// invalid; amount component exceeds an i64
// 9223372036854775808 = i64::MAX + 1
let invalid_7 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=9223372036854775808";
let i7r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_7);
assert!(i7r.is_err());
// invalid; amount component wraps into a valid small positive i64
// 18446744073709551624
let invalid_7a = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=18446744073709551624";
let i7ar = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_7a);
assert!(i7ar.is_err());
// invalid; amount component is MAX_MONEY
// 21000000.00000001
let invalid_8 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000.00000001";
let i8r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_8);
assert!(i8r.is_err());
// invalid; negative amount
let invalid_9 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=-1";
let i9r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_9);
assert!(i9r.is_err());
// invalid; parameter index too large
let invalid_10 =
"zcash:?amount.10000=1.23&address.10000=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
let i10r = TransactionRequest::from_uri(&TEST_NETWORK, &invalid_10);
assert!(i10r.is_err());
}
#[cfg(all(test, feature = "test-dependencies"))]
proptest! {
#[test]
fn prop_zip321_roundtrip_address(addr in arb_addr()) {
let a = addr.encode(&TEST_NETWORK);
assert_eq!(RecipientAddress::decode(&TEST_NETWORK, &a), Some(addr));
}
#[test]
fn prop_zip321_roundtrip_address_str(a in arb_addr_str()) {
let addr = RecipientAddress::decode(&TEST_NETWORK, &a).unwrap();
assert_eq!(addr.encode(&TEST_NETWORK), a);
}
#[test]
fn prop_zip321_roundtrip_amount(amt in arb_nonnegative_amount()) {
let amt_str = amount_str(amt).unwrap();
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
}
#[test]
fn prop_zip321_roundtrip_str_param(
message in any::<String>(), i in proptest::option::of(0usize..2000)) {
let fragment = str_param("message", &message, i);
let (rest, iparam) = zcashparam(&TEST_NETWORK)(&fragment).unwrap();
assert_eq!(rest, "");
assert_eq!(iparam.param, Param::Message(message));
assert_eq!(iparam.payment_index, i.unwrap_or(0));
}
#[test]
fn prop_zip321_roundtrip_memo_param(
memo in arb_valid_memo(), i in proptest::option::of(0usize..2000)) {
let fragment = memo_param(&memo, i);
let (rest, iparam) = zcashparam(&TEST_NETWORK)(&fragment).unwrap();
assert_eq!(rest, "");
assert_eq!(iparam.param, Param::Memo(memo));
assert_eq!(iparam.payment_index, i.unwrap_or(0));
}
#[test]
fn prop_zip321_roundtrip_request(mut req in arb_zip321_request()) {
if let Some(req_uri) = req.to_uri(&TEST_NETWORK) {
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap();
assert!(TransactionRequest::normalize_and_eq(&TEST_NETWORK, &mut parsed, &mut req));
} else {
panic!("Generated invalid payment request: {:?}", req);
}
}
#[test]
fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri()) {
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &uri).unwrap();
parsed.normalize(&TEST_NETWORK);
let serialized = parsed.to_uri(&TEST_NETWORK);
assert_eq!(serialized, Some(uri))
}
}
}

View File

@ -3,12 +3,18 @@
//! # Examples
//!
//! ```
//! use rusqlite::Connection;
//! use zcash_primitives::{
//! consensus::{BlockHeight, Network, Parameters}
//! };
//!
//! use zcash_client_sqlite::{
//! chain::{rewind_to_height, validate_combined_chain},
//! error::ErrorKind,
//! scan::scan_cached_blocks,
//! };
//!
//! let network = Network::TestNetwork;
//! let db_cache = "/path/to/cache.db";
//! let db_data = "/path/to/data.db";
//!
@ -18,7 +24,7 @@
//! //
//! // Given that we assume the server always gives us correct-at-the-time blocks, any
//! // errors are in the blocks we have previously cached or scanned.
//! if let Err(e) = validate_combined_chain(&db_cache, &db_data) {
//! if let Err(e) = validate_combined_chain(network, &db_cache, &db_data) {
//! match e.kind() {
//! ErrorKind::InvalidChain(upper_bound, _) => {
//! // a) Pick a height to rewind to.
@ -26,10 +32,10 @@
//! // This might be informed by some external chain reorg information, or
//! // heuristics such as the platform, available bandwidth, size of recent
//! // CompactBlocks, etc.
//! let rewind_height = upper_bound - 10;
//! let rewind_height = *upper_bound - 10;
//!
//! // b) Rewind scanned block information.
//! rewind_to_height(&db_data, rewind_height);
//! rewind_to_height(network, &db_data, rewind_height);
//!
//! // c) Delete cached blocks from rewind_height onwards.
//! //
@ -52,28 +58,28 @@
//! // At this point, the cache and scanned data are locally consistent (though not
//! // necessarily consistent with the latest chain tip - this would be discovered the
//! // next time this codepath is executed after new blocks are received).
//! scan_cached_blocks(&db_cache, &db_data, None);
//! scan_cached_blocks(&network, &db_cache, &db_data, None);
//! ```
use protobuf::parse_from_bytes;
use rusqlite::{Connection, NO_PARAMS};
use std::path::Path;
use zcash_primitives::consensus::{self, BlockHeight, NetworkUpgrade};
use zcash_client_backend::proto::compact_formats::CompactBlock;
use crate::{
error::{Error, ErrorKind},
SAPLING_ACTIVATION_HEIGHT,
};
use crate::error::{Error, ErrorKind};
#[derive(Debug)]
pub enum ChainInvalidCause {
PrevHashMismatch,
/// (expected_height, actual_height)
HeightMismatch(i32, i32),
HeightMismatch(BlockHeight, BlockHeight),
}
struct CompactBlockRow {
height: i32,
height: BlockHeight,
data: Vec<u8>,
}
@ -93,28 +99,32 @@ struct CompactBlockRow {
/// - `Err(e)` if there was an error during validation unrelated to chain validity.
///
/// This function does not mutate either of the databases.
pub fn validate_combined_chain<P: AsRef<Path>, Q: AsRef<Path>>(
pub fn validate_combined_chain<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRef<Path>>(
parameters: Params,
db_cache: P,
db_data: Q,
) -> Result<(), Error> {
let cache = Connection::open(db_cache)?;
let data = Connection::open(db_data)?;
let sapling_activation_height = parameters
.activation_height(NetworkUpgrade::Sapling)
.ok_or(Error(ErrorKind::SaplingNotActive))?;
// Recall where we synced up to previously.
// If we have never synced, use Sapling activation height to select all cached CompactBlocks.
let (have_scanned, last_scanned_height) =
data.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
row.get(0)
.map(|h| (true, h))
.or(Ok((false, SAPLING_ACTIVATION_HEIGHT - 1)))
.map(|h: u32| (true, h.into()))
.or(Ok((false, sapling_activation_height - 1)))
})?;
// Fetch the CompactBlocks we need to validate
let mut stmt_blocks = cache
.prepare("SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height DESC")?;
let mut rows = stmt_blocks.query_map(&[last_scanned_height], |row| {
let mut rows = stmt_blocks.query_map(&[u32::from(last_scanned_height)], |row| {
Ok(CompactBlockRow {
height: row.get(0)?,
height: row.get(0).map(u32::into)?,
data: row.get(1)?,
})
})?;
@ -126,13 +136,12 @@ pub fn validate_combined_chain<P: AsRef<Path>, Q: AsRef<Path>>(
None => {
// No cached blocks, and we've already validated the blocks we've scanned,
// so there's nothing to validate.
// TODO: Maybe we still want to check if there are cached blocks that are
// at heights we previously scanned? Check scanning flow again.
// TODO: Maybe we still want to check if there are cached blocks that are at heights we previously scanned? Check scanning flow again.
return Ok(());
}
};
let block: CompactBlock = parse_from_bytes(&assumed_correct.data)?;
(block.height as i32, block.prev_hash())
(block.height(), block.prev_hash())
};
for row in rows {
@ -163,7 +172,7 @@ pub fn validate_combined_chain<P: AsRef<Path>, Q: AsRef<Path>>(
// Cached blocks MUST hash-chain to the last scanned block.
let last_scanned_hash = data.query_row(
"SELECT hash FROM blocks WHERE height = ?",
&[last_scanned_height],
&[u32::from(last_scanned_height)],
|row| row.get::<_, Vec<_>>(0),
)?;
if &last_scanned_hash[..] != &last_prev_hash.0[..] {
@ -182,14 +191,23 @@ pub fn validate_combined_chain<P: AsRef<Path>, Q: AsRef<Path>>(
///
/// If the requested height is greater than or equal to the height of the last scanned
/// block, this function does nothing.
pub fn rewind_to_height<P: AsRef<Path>>(db_data: P, height: i32) -> Result<(), Error> {
pub fn rewind_to_height<Params: consensus::Parameters, P: AsRef<Path>>(
parameters: Params,
db_data: P,
height: BlockHeight,
) -> Result<(), Error> {
let data = Connection::open(db_data)?;
let sapling_activation_height = parameters
.activation_height(NetworkUpgrade::Sapling)
.ok_or(Error(ErrorKind::SaplingNotActive))?;
// Recall where we synced up to previously.
// If we have never synced, use Sapling activation height.
let last_scanned_height =
data.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
row.get(0).or(Ok(SAPLING_ACTIVATION_HEIGHT - 1))
row.get(0)
.map(u32::into)
.or(Ok(sapling_activation_height - 1))
})?;
if height >= last_scanned_height {
@ -201,16 +219,19 @@ pub fn rewind_to_height<P: AsRef<Path>>(db_data: P, height: i32) -> Result<(), E
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
// Decrement witnesses.
data.execute("DELETE FROM sapling_witnesses WHERE block > ?", &[height])?;
data.execute(
"DELETE FROM sapling_witnesses WHERE block > ?",
&[u32::from(height)],
)?;
// Un-mine transactions.
data.execute(
"UPDATE transactions SET block = NULL, tx_index = NULL WHERE block > ?",
&[height],
&[u32::from(height)],
)?;
// Now that they aren't depended on, delete scanned blocks.
data.execute("DELETE FROM blocks WHERE height > ?", &[height])?;
data.execute("DELETE FROM blocks WHERE height > ?", &[u32::from(height)])?;
// Commit the SQL transaction, rewinding atomically.
data.execute("COMMIT", NO_PARAMS)?;
@ -228,13 +249,13 @@ mod tests {
};
use super::{rewind_to_height, validate_combined_chain};
use crate::{
error::ErrorKind,
init::{init_accounts_table, init_cache_database, init_data_database},
query::get_balance,
scan::scan_cached_blocks,
tests::{fake_compact_block, insert_into_cache},
SAPLING_ACTIVATION_HEIGHT,
tests::{self, fake_compact_block, insert_into_cache, sapling_activation_height},
};
#[test]
@ -250,14 +271,14 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
// Empty chain should be valid
validate_combined_chain(db_cache, db_data).unwrap();
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
// Create a fake CompactBlock sending value to the address
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
sapling_activation_height(),
BlockHash([0; 32]),
extfvk.clone(),
Amount::from_u64(5).unwrap(),
@ -265,17 +286,17 @@ mod tests {
insert_into_cache(db_cache, &cb);
// Cache-only chain should be valid
validate_combined_chain(db_cache, db_data).unwrap();
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
// Scan the cache
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Data-only chain should be valid
validate_combined_chain(db_cache, db_data).unwrap();
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
// Create a second fake CompactBlock sending more value to the address
let (cb2, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 1,
sapling_activation_height() + 1,
cb.hash(),
extfvk,
Amount::from_u64(7).unwrap(),
@ -283,13 +304,13 @@ mod tests {
insert_into_cache(db_cache, &cb2);
// Data+cache chain should be valid
validate_combined_chain(db_cache, db_data).unwrap();
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
// Scan the cache again
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Data-only chain should be valid
validate_combined_chain(db_cache, db_data).unwrap();
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
}
#[test]
@ -305,17 +326,17 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
// Create some fake CompactBlocks
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
sapling_activation_height(),
BlockHash([0; 32]),
extfvk.clone(),
Amount::from_u64(5).unwrap(),
);
let (cb2, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 1,
sapling_activation_height() + 1,
cb.hash(),
extfvk.clone(),
Amount::from_u64(7).unwrap(),
@ -324,20 +345,20 @@ mod tests {
insert_into_cache(db_cache, &cb2);
// Scan the cache
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Data-only chain should be valid
validate_combined_chain(db_cache, db_data).unwrap();
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
// Create more fake CompactBlocks that don't connect to the scanned ones
let (cb3, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 2,
sapling_activation_height() + 2,
BlockHash([1; 32]),
extfvk.clone(),
Amount::from_u64(8).unwrap(),
);
let (cb4, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 3,
sapling_activation_height() + 3,
cb3.hash(),
extfvk.clone(),
Amount::from_u64(3).unwrap(),
@ -346,10 +367,10 @@ mod tests {
insert_into_cache(db_cache, &cb4);
// Data+cache chain should be invalid at the data/cache boundary
match validate_combined_chain(db_cache, db_data) {
match validate_combined_chain(tests::network(), db_cache, db_data) {
Err(e) => match e.kind() {
ErrorKind::InvalidChain(upper_bound, _) => {
assert_eq!(*upper_bound, SAPLING_ACTIVATION_HEIGHT + 1)
assert_eq!(*upper_bound, sapling_activation_height() + 1)
}
_ => panic!(),
},
@ -370,17 +391,17 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
// Create some fake CompactBlocks
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
sapling_activation_height(),
BlockHash([0; 32]),
extfvk.clone(),
Amount::from_u64(5).unwrap(),
);
let (cb2, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 1,
sapling_activation_height() + 1,
cb.hash(),
extfvk.clone(),
Amount::from_u64(7).unwrap(),
@ -389,20 +410,20 @@ mod tests {
insert_into_cache(db_cache, &cb2);
// Scan the cache
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Data-only chain should be valid
validate_combined_chain(db_cache, db_data).unwrap();
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
// Create more fake CompactBlocks that contain a reorg
let (cb3, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 2,
sapling_activation_height() + 2,
cb2.hash(),
extfvk.clone(),
Amount::from_u64(8).unwrap(),
);
let (cb4, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 3,
sapling_activation_height() + 3,
BlockHash([1; 32]),
extfvk.clone(),
Amount::from_u64(3).unwrap(),
@ -411,10 +432,10 @@ mod tests {
insert_into_cache(db_cache, &cb4);
// Data+cache chain should be invalid inside the cache
match validate_combined_chain(db_cache, db_data) {
match validate_combined_chain(tests::network(), db_cache, db_data) {
Err(e) => match e.kind() {
ErrorKind::InvalidChain(upper_bound, _) => {
assert_eq!(*upper_bound, SAPLING_ACTIVATION_HEIGHT + 2)
assert_eq!(*upper_bound, sapling_activation_height() + 2)
}
_ => panic!(),
},
@ -435,7 +456,7 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
// Account balance should be zero
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
@ -444,35 +465,36 @@ mod tests {
let value = Amount::from_u64(5).unwrap();
let value2 = Amount::from_u64(7).unwrap();
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
sapling_activation_height(),
BlockHash([0; 32]),
extfvk.clone(),
value,
);
let (cb2, _) = fake_compact_block(SAPLING_ACTIVATION_HEIGHT + 1, cb.hash(), extfvk, value2);
let (cb2, _) =
fake_compact_block(sapling_activation_height() + 1, cb.hash(), extfvk, value2);
insert_into_cache(db_cache, &cb);
insert_into_cache(db_cache, &cb2);
// Scan the cache
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Account balance should reflect both received notes
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);
// "Rewind" to height of last scanned block
rewind_to_height(db_data, SAPLING_ACTIVATION_HEIGHT + 1).unwrap();
rewind_to_height(tests::network(), db_data, sapling_activation_height() + 1).unwrap();
// Account balance should be unaltered
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);
// Rewind so that one block is dropped
rewind_to_height(db_data, SAPLING_ACTIVATION_HEIGHT).unwrap();
rewind_to_height(tests::network(), db_data, sapling_activation_height()).unwrap();
// Account balance should only contain the first received note
assert_eq!(get_balance(db_data, 0).unwrap(), value);
// Scan the cache again
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Account balance should again reflect both received notes
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);

View File

@ -1,6 +1,7 @@
use std::error;
use std::fmt;
use zcash_primitives::{
consensus::BlockHeight,
sapling::Node,
transaction::{builder, TxId},
};
@ -10,13 +11,13 @@ pub enum ErrorKind {
CorruptedData(&'static str),
IncorrectHRPExtFVK,
InsufficientBalance(u64, u64),
InvalidChain(i32, crate::chain::ChainInvalidCause),
InvalidChain(BlockHeight, crate::chain::ChainInvalidCause),
InvalidExtSK(u32),
InvalidHeight(i32, i32),
InvalidHeight(BlockHeight, BlockHeight),
InvalidMemo(std::str::Utf8Error),
InvalidNewWitnessAnchor(usize, TxId, i32, Node),
InvalidNewWitnessAnchor(usize, TxId, BlockHeight, Node),
InvalidNote,
InvalidWitnessAnchor(i64, i32),
InvalidWitnessAnchor(i64, BlockHeight),
ScanRequired,
TableNotEmpty,
Bech32(bech32::Error),
@ -25,6 +26,7 @@ pub enum ErrorKind {
Database(rusqlite::Error),
Io(std::io::Error),
Protobuf(protobuf::ProtobufError),
SaplingNotActive,
}
#[derive(Debug)]
@ -71,6 +73,7 @@ impl fmt::Display for Error {
ErrorKind::Database(e) => write!(f, "{}", e),
ErrorKind::Io(e) => write!(f, "{}", e),
ErrorKind::Protobuf(e) => write!(f, "{}", e),
ErrorKind::SaplingNotActive => write!(f, "Sapling activation height not specified for network."),
}
}
}

View File

@ -3,12 +3,12 @@
use rusqlite::{types::ToSql, Connection, NO_PARAMS};
use std::path::Path;
use zcash_client_backend::encoding::encode_extended_full_viewing_key;
use zcash_primitives::{block::BlockHash, zip32::ExtendedFullViewingKey};
use zcash_primitives::{block::BlockHash, consensus, zip32::ExtendedFullViewingKey};
use crate::{
address_from_extfvk,
error::{Error, ErrorKind},
HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY,
};
/// Sets up the internal structure of the cache database.
@ -141,7 +141,10 @@ pub fn init_data_database<P: AsRef<Path>>(db_data: P) -> Result<(), Error> {
/// ```
/// use tempfile::NamedTempFile;
/// use zcash_client_sqlite::init::{init_accounts_table, init_data_database};
/// use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey};
/// use zcash_primitives::{
/// consensus::Network,
/// zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}
/// };
///
/// let data_file = NamedTempFile::new().unwrap();
/// let db_data = data_file.path();
@ -149,14 +152,15 @@ pub fn init_data_database<P: AsRef<Path>>(db_data: P) -> Result<(), Error> {
///
/// let extsk = ExtendedSpendingKey::master(&[]);
/// let extfvks = [ExtendedFullViewingKey::from(&extsk)];
/// init_accounts_table(&db_data, &extfvks).unwrap();
/// init_accounts_table(&db_data, &Network::TestNetwork, &extfvks).unwrap();
/// ```
///
/// [`get_address`]: crate::query::get_address
/// [`scan_cached_blocks`]: crate::scan::scan_cached_blocks
/// [`create_to_address`]: crate::transact::create_to_address
pub fn init_accounts_table<P: AsRef<Path>>(
db_data: P,
pub fn init_accounts_table<D: AsRef<Path>, P: consensus::Parameters>(
db_data: D,
params: &P,
extfvks: &[ExtendedFullViewingKey],
) -> Result<(), Error> {
let data = Connection::open(db_data)?;
@ -169,9 +173,11 @@ pub fn init_accounts_table<P: AsRef<Path>>(
// Insert accounts atomically
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
for (account, extfvk) in extfvks.iter().enumerate() {
let address = address_from_extfvk(extfvk);
let extfvk =
encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, extfvk);
let address = address_from_extfvk(params, extfvk);
let extfvk = encode_extended_full_viewing_key(
params.hrp_sapling_extended_full_viewing_key(),
extfvk,
);
data.execute(
"INSERT INTO accounts (account, extfvk, address)
VALUES (?, ?, ?)",
@ -244,11 +250,12 @@ mod tests {
use zcash_client_backend::encoding::decode_payment_address;
use zcash_primitives::{
block::BlockHash,
consensus::Parameters,
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
};
use super::{init_accounts_table, init_blocks_table, init_data_database};
use crate::{query::get_address, HRP_SAPLING_PAYMENT_ADDRESS};
use crate::{query::get_address, tests};
#[test]
fn init_accounts_table_only_works_once() {
@ -257,18 +264,18 @@ mod tests {
init_data_database(&db_data).unwrap();
// We can call the function as many times as we want with no data
init_accounts_table(&db_data, &[]).unwrap();
init_accounts_table(&db_data, &[]).unwrap();
init_accounts_table(&db_data, &tests::network(), &[]).unwrap();
init_accounts_table(&db_data, &tests::network(), &[]).unwrap();
// First call with data should initialise the accounts table
let extfvks = [ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(
&[],
))];
init_accounts_table(&db_data, &extfvks).unwrap();
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap();
// Subsequent calls should return an error
init_accounts_table(&db_data, &[]).unwrap_err();
init_accounts_table(&db_data, &extfvks).unwrap_err();
init_accounts_table(&db_data, &tests::network(), &[]).unwrap_err();
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap_err();
}
#[test]
@ -293,11 +300,12 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
init_accounts_table(&db_data, &extfvks).unwrap();
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap();
// The account's address should be in the data DB
let addr = get_address(&db_data, 0).unwrap();
let pa = decode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &addr).unwrap();
let pa =
decode_payment_address(tests::network().hrp_sapling_payment_address(), &addr).unwrap();
assert_eq!(pa.unwrap(), extsk.default_address().unwrap().1);
}
}

View File

@ -26,25 +26,14 @@
use rusqlite::{Connection, NO_PARAMS};
use std::cmp;
use zcash_primitives::{
consensus::{self, BlockHeight},
zip32::ExtendedFullViewingKey,
};
use zcash_client_backend::encoding::encode_payment_address;
use zcash_primitives::zip32::ExtendedFullViewingKey;
#[cfg(feature = "mainnet")]
use zcash_client_backend::constants::mainnet::{
HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, HRP_SAPLING_PAYMENT_ADDRESS,
};
#[cfg(not(feature = "mainnet"))]
use zcash_client_backend::constants::testnet::{
HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, HRP_SAPLING_PAYMENT_ADDRESS,
};
#[cfg(feature = "mainnet")]
pub use zcash_primitives::consensus::MainNetwork as Network;
#[cfg(not(feature = "mainnet"))]
pub use zcash_primitives::consensus::TestNetwork as Network;
pub mod address;
pub mod chain;
pub mod error;
pub mod init;
@ -54,20 +43,19 @@ pub mod transact;
const ANCHOR_OFFSET: u32 = 10;
#[cfg(feature = "mainnet")]
const SAPLING_ACTIVATION_HEIGHT: i32 = 419_200;
#[cfg(not(feature = "mainnet"))]
const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000;
fn address_from_extfvk(extfvk: &ExtendedFullViewingKey) -> String {
fn address_from_extfvk<P: consensus::Parameters>(
params: &P,
extfvk: &ExtendedFullViewingKey,
) -> String {
let addr = extfvk.default_address().unwrap().1;
encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &addr)
encode_payment_address(params.hrp_sapling_payment_address(), &addr)
}
/// Determines the target height for a transaction, and the height from which to
/// select anchors, based on the current synchronised block chain.
fn get_target_and_anchor_heights(data: &Connection) -> Result<(u32, u32), error::Error> {
fn get_target_and_anchor_heights(
data: &Connection,
) -> Result<(BlockHeight, BlockHeight), error::Error> {
data.query_row_and_then(
"SELECT MIN(height), MAX(height) FROM blocks",
NO_PARAMS,
@ -86,7 +74,10 @@ fn get_target_and_anchor_heights(data: &Connection) -> Result<(u32, u32), error:
let anchor_height =
cmp::max(target_height.saturating_sub(ANCHOR_OFFSET), min_height);
Ok((target_height, anchor_height))
Ok((
BlockHeight::from(target_height),
BlockHeight::from(anchor_height),
))
}
},
)
@ -94,18 +85,20 @@ fn get_target_and_anchor_heights(data: &Connection) -> Result<(u32, u32), error:
#[cfg(test)]
mod tests {
use crate::Network;
use ff::PrimeField;
use group::GroupEncoding;
use protobuf::Message;
use rand_core::{OsRng, RngCore};
use rusqlite::{types::ToSql, Connection};
use std::path::Path;
use zcash_client_backend::proto::compact_formats::{
CompactBlock, CompactOutput, CompactSpend, CompactTx,
};
use zcash_primitives::{
block::BlockHash,
consensus::{BlockHeight, Network, NetworkUpgrade, Parameters},
note_encryption::{Memo, SaplingNoteEncryption},
primitives::{Note, PaymentAddress},
transaction::components::Amount,
@ -113,10 +106,34 @@ mod tests {
zip32::ExtendedFullViewingKey,
};
#[cfg(feature = "mainnet")]
pub(crate) fn network() -> Network {
Network::MainNetwork
}
#[cfg(not(feature = "mainnet"))]
pub(crate) fn network() -> Network {
Network::TestNetwork
}
#[cfg(feature = "mainnet")]
pub(crate) fn sapling_activation_height() -> BlockHeight {
Network::MainNetwork
.activation_height(NetworkUpgrade::Sapling)
.unwrap()
}
#[cfg(not(feature = "mainnet"))]
pub(crate) fn sapling_activation_height() -> BlockHeight {
Network::TestNetwork
.activation_height(NetworkUpgrade::Sapling)
.unwrap()
}
/// Create a fake CompactBlock at the given height, containing a single output paying
/// the given address. Returns the CompactBlock and the nullifier for the new note.
pub(crate) fn fake_compact_block(
height: i32,
height: BlockHeight,
prev_hash: BlockHash,
extfvk: ExtendedFullViewingKey,
value: Amount,
@ -125,7 +142,7 @@ mod tests {
// Create a fake Note for the account
let mut rng = OsRng;
let rseed = generate_random_rseed::<Network, OsRng>(height as u32, &mut rng);
let rseed = generate_random_rseed(&network(), height, &mut rng);
let note = Note {
g_d: to.diversifier().g_d().unwrap(),
pk_d: to.pk_d().clone(),
@ -154,7 +171,7 @@ mod tests {
ctx.set_hash(txid);
ctx.outputs.push(cout);
let mut cb = CompactBlock::new();
cb.set_height(height as u64);
cb.set_height(u64::from(height));
cb.hash.resize(32, 0);
rng.fill_bytes(&mut cb.hash);
cb.prevHash.extend_from_slice(&prev_hash.0);
@ -165,7 +182,7 @@ mod tests {
/// Create a fake CompactBlock at the given height, spending a single note from the
/// given address.
pub(crate) fn fake_compact_block_spending(
height: i32,
height: BlockHeight,
prev_hash: BlockHash,
(nf, in_value): (Vec<u8>, Amount),
extfvk: ExtendedFullViewingKey,
@ -173,7 +190,7 @@ mod tests {
value: Amount,
) -> CompactBlock {
let mut rng = OsRng;
let rseed = generate_random_rseed::<Network, OsRng>(height as u32, &mut rng);
let rseed = generate_random_rseed(&network(), height, &mut rng);
// Create a fake CompactBlock containing the note
let mut cspend = CompactSpend::new();
@ -213,7 +230,7 @@ mod tests {
// Create a fake Note for the change
ctx.outputs.push({
let change_addr = extfvk.default_address().unwrap().1;
let rseed = generate_random_rseed::<Network, OsRng>(height as u32, &mut rng);
let rseed = generate_random_rseed(&network(), height, &mut rng);
let note = Note {
g_d: change_addr.diversifier().g_d().unwrap(),
pk_d: change_addr.pk_d().clone(),
@ -239,7 +256,7 @@ mod tests {
});
let mut cb = CompactBlock::new();
cb.set_height(height as u64);
cb.set_height(u64::from(height));
cb.hash.resize(32, 0);
rng.fill_bytes(&mut cb.hash);
cb.prevHash.extend_from_slice(&prev_hash.0);
@ -255,7 +272,7 @@ mod tests {
.prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)")
.unwrap()
.execute(&[
(cb.height as i32).to_sql().unwrap(),
u32::from(cb.height()).to_sql().unwrap(),
cb_bytes.to_sql().unwrap(),
])
.unwrap();

View File

@ -84,7 +84,7 @@ pub fn get_verified_balance<P: AsRef<Path>>(db_data: P, account: u32) -> Result<
"SELECT SUM(value) FROM received_notes
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
WHERE account = ? AND spent IS NULL AND transactions.block <= ?",
&[account, anchor_height],
&[account, u32::from(anchor_height)],
|row| row.get(0).or(Ok(0)),
)?;
@ -176,6 +176,7 @@ mod tests {
use crate::{
error::ErrorKind,
init::{init_accounts_table, init_data_database},
tests,
};
#[test]
@ -187,7 +188,7 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
init_accounts_table(&db_data, &extfvks).unwrap();
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap();
// The account should be empty
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());

View File

@ -1,27 +1,26 @@
//! Functions for scanning the chain and extracting relevant information.
use std::path::Path;
use ff::PrimeField;
use protobuf::parse_from_bytes;
use rusqlite::{types::ToSql, Connection, OptionalExtension, NO_PARAMS};
use std::path::Path;
use zcash_client_backend::{
decrypt_transaction, encoding::decode_extended_full_viewing_key,
address::RecipientAddress, decrypt_transaction, encoding::decode_extended_full_viewing_key,
proto::compact_formats::CompactBlock, welding_rig::scan_block,
};
use zcash_primitives::{
consensus::{self, BlockHeight, NetworkUpgrade},
merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::Node,
transaction::Transaction,
};
use crate::{
address::RecipientAddress,
error::{Error, ErrorKind},
Network, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, SAPLING_ACTIVATION_HEIGHT,
};
use crate::error::{Error, ErrorKind};
struct CompactBlockRow {
height: i32,
height: BlockHeight,
data: Vec<u8>,
}
@ -54,43 +53,60 @@ struct WitnessRow {
/// # Examples
///
/// ```
/// use zcash_primitives::consensus::{
/// Network,
/// Parameters,
/// };
/// use zcash_client_sqlite::scan::scan_cached_blocks;
///
/// scan_cached_blocks("/path/to/cache.db", "/path/to/data.db", None);
/// scan_cached_blocks(&Network::TestNetwork, "/path/to/cache.db", "/path/to/data.db", None);
/// ```
///
/// [`init_blocks_table`]: crate::init::init_blocks_table
pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRef<Path>>(
params: &Params,
db_cache: P,
db_data: Q,
limit: Option<i32>,
limit: Option<u32>,
) -> Result<(), Error> {
let cache = Connection::open(db_cache)?;
let data = Connection::open(db_data)?;
let sapling_activation_height = params
.activation_height(NetworkUpgrade::Sapling)
.ok_or(Error(ErrorKind::SaplingNotActive))?;
// Recall where we synced up to previously.
// If we have never synced, use sapling activation height to select all cached CompactBlocks.
let mut last_height = data.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
row.get(0).or(Ok(SAPLING_ACTIVATION_HEIGHT - 1))
})?;
let mut last_height: BlockHeight =
data.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
row.get::<_, u32>(0)
.map(BlockHeight::from)
.or(Ok(sapling_activation_height - 1))
})?;
// Fetch the CompactBlocks we need to scan
let mut stmt_blocks = cache.prepare(
"SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC LIMIT ?",
)?;
let rows = stmt_blocks.query_map(&[last_height, limit.unwrap_or(i32::max_value())], |row| {
Ok(CompactBlockRow {
height: row.get(0)?,
data: row.get(1)?,
})
})?;
let rows = stmt_blocks.query_map(
&[u32::from(last_height), limit.unwrap_or(u32::max_value())],
|row| {
Ok(CompactBlockRow {
height: row.get::<_, u32>(0).map(BlockHeight::from)?,
data: row.get(1)?,
})
},
)?;
// Fetch the ExtendedFullViewingKeys we are tracking
let mut stmt_fetch_accounts =
data.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
let extfvks = stmt_fetch_accounts.query_map(NO_PARAMS, |row| {
row.get(0).map(|extfvk: String| {
decode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk)
decode_extended_full_viewing_key(
params.hrp_sapling_extended_full_viewing_key(),
&extfvk,
)
})
})?;
// Raise SQL errors from the query, IO errors from parsing, and incorrect HRP errors.
@ -101,7 +117,7 @@ pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
// Get the most recent CommitmentTree
let mut stmt_fetch_tree = data.prepare("SELECT sapling_tree FROM blocks WHERE height = ?")?;
let mut tree = stmt_fetch_tree
.query_row(&[last_height], |row| {
.query_row(&[u32::from(last_height)], |row| {
row.get(0).map(|data: Vec<_>| {
CommitmentTree::read(&data[..]).unwrap_or_else(|_| CommitmentTree::new())
})
@ -111,7 +127,7 @@ pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
// Get most recent incremental witnesses for the notes we are tracking
let mut stmt_fetch_witnesses =
data.prepare("SELECT note, witness FROM sapling_witnesses WHERE block = ?")?;
let witnesses = stmt_fetch_witnesses.query_map(&[last_height], |row| {
let witnesses = stmt_fetch_witnesses.query_map(&[u32::from(last_height)], |row| {
let id_note = row.get(0)?;
let data: Vec<_> = row.get(1)?;
Ok(IncrementalWitness::read(&data[..]).map(|witness| WitnessRow { id_note, witness }))
@ -186,7 +202,8 @@ pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
let txs = {
let nf_refs: Vec<_> = nullifiers.iter().map(|(nf, acc)| (&nf[..], *acc)).collect();
let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.witness).collect();
scan_block::<Network>(
scan_block(
params,
block,
&extfvks[..],
&nf_refs,
@ -226,7 +243,7 @@ pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
tree.write(&mut encoded_tree)
.expect("Should be able to write to a Vec");
stmt_insert_block.execute(&[
row.height.to_sql()?,
u32::from(row.height).to_sql()?,
block_hash.to_sql()?,
block_time.to_sql()?,
encoded_tree.to_sql()?,
@ -236,7 +253,7 @@ pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
// First try update an existing transaction in the database.
let txid = tx.txid.0.to_vec();
let tx_row = if stmt_update_tx.execute(&[
row.height.to_sql()?,
u32::from(row.height).to_sql()?,
(tx.index as i64).to_sql()?,
txid.to_sql()?,
])? == 0
@ -244,7 +261,7 @@ pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
// It isn't there, so insert our transaction into the database.
stmt_insert_tx.execute(&[
txid.to_sql()?,
row.height.to_sql()?,
u32::from(row.height).to_sql()?,
(tx.index as i64).to_sql()?,
])?;
data.last_insert_rowid()
@ -331,16 +348,16 @@ pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
.expect("Should be able to write to a Vec");
stmt_insert_witness.execute(&[
witness_row.id_note.to_sql()?,
last_height.to_sql()?,
u32::from(last_height).to_sql()?,
encoded.to_sql()?,
])?;
}
// Prune the stored witnesses (we only expect rollbacks of at most 100 blocks).
stmt_prune_witnesses.execute(&[last_height - 100])?;
stmt_prune_witnesses.execute(&[u32::from(last_height - 100)])?;
// Update now-expired transactions that didn't get mined.
stmt_update_expired.execute(&[last_height])?;
stmt_update_expired.execute(&[u32::from(last_height)])?;
// Commit the SQL transaction, writing this block's data atomically.
data.execute("COMMIT", NO_PARAMS)?;
@ -351,8 +368,9 @@ pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
/// the wallet, and saves it to the wallet.
pub fn decrypt_and_store_transaction<P: AsRef<Path>>(
db_data: P,
pub fn decrypt_and_store_transaction<D: AsRef<Path>, P: consensus::Parameters>(
db_data: D,
params: &P,
tx: &Transaction,
) -> Result<(), Error> {
let data = Connection::open(db_data)?;
@ -362,7 +380,10 @@ pub fn decrypt_and_store_transaction<P: AsRef<Path>>(
data.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
let extfvks = stmt_fetch_accounts.query_map(NO_PARAMS, |row| {
row.get(0).map(|extfvk: String| {
decode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk)
decode_extended_full_viewing_key(
params.hrp_sapling_extended_full_viewing_key(),
&extfvk,
)
})
})?;
// Raise SQL errors from the query, IO errors from parsing, and incorrect HRP errors.
@ -373,7 +394,9 @@ pub fn decrypt_and_store_transaction<P: AsRef<Path>>(
// Height is block height for mined transactions, and the "mempool height" (chain height + 1) for mempool transactions.
let mut stmt_select_block = data.prepare("SELECT block FROM transactions WHERE txid = ?")?;
let height = match stmt_select_block
.query_row(&[tx.txid().0.to_vec()], |row| row.get(0))
.query_row(&[tx.txid().0.to_vec()], |row| {
row.get::<_, u32>(0).map(BlockHeight::from)
})
.optional()?
{
Some(height) => height,
@ -382,11 +405,12 @@ pub fn decrypt_and_store_transaction<P: AsRef<Path>>(
row.get(0)
})
.optional()?
.map(|last_height: u32| last_height + 1)
.unwrap_or(SAPLING_ACTIVATION_HEIGHT as u32),
.map(|last_height: u32| BlockHeight::from(last_height + 1))
.or_else(|| params.activation_height(NetworkUpgrade::Sapling))
.ok_or(Error(ErrorKind::SaplingNotActive))?,
};
let outputs = decrypt_transaction::<Network>(height as u32, tx, &extfvks);
let outputs = decrypt_transaction(params, height, tx, &extfvks);
if outputs.is_empty() {
// Nothing to see here
@ -429,13 +453,17 @@ pub fn decrypt_and_store_transaction<P: AsRef<Path>>(
let mut raw_tx = vec![];
tx.write(&mut raw_tx)?;
let tx_row = if stmt_update_tx.execute(&[
tx.expiry_height.to_sql()?,
u32::from(tx.expiry_height).to_sql()?,
raw_tx.to_sql()?,
txid.to_sql()?,
])? == 0
{
// It isn't there, so insert our transaction into the database.
stmt_insert_tx.execute(&[txid.to_sql()?, tx.expiry_height.to_sql()?, raw_tx.to_sql()?])?;
stmt_insert_tx.execute(&[
txid.to_sql()?,
u32::from(tx.expiry_height).to_sql()?,
raw_tx.to_sql()?,
])?;
data.last_insert_rowid()
} else {
// It was there, so grab its row number.
@ -448,7 +476,7 @@ pub fn decrypt_and_store_transaction<P: AsRef<Path>>(
let value = output.note.value as i64;
if output.outgoing {
let to_str = RecipientAddress::from(output.to).to_string();
let to_str = RecipientAddress::from(output.to).encode(params);
// Try updating an existing sent note.
if stmt_update_sent_note.execute(&[
@ -516,8 +544,10 @@ mod tests {
use crate::{
init::{init_accounts_table, init_cache_database, init_data_database},
query::get_balance,
tests::{fake_compact_block, fake_compact_block_spending, insert_into_cache},
SAPLING_ACTIVATION_HEIGHT,
tests::{
self, fake_compact_block, fake_compact_block_spending, insert_into_cache,
sapling_activation_height,
},
};
#[test]
@ -533,49 +563,49 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
// Create a block with height SAPLING_ACTIVATION_HEIGHT
let value = Amount::from_u64(50000).unwrap();
let (cb1, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
sapling_activation_height(),
BlockHash([0; 32]),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb1);
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
assert_eq!(get_balance(db_data, 0).unwrap(), value);
// We cannot scan a block of height SAPLING_ACTIVATION_HEIGHT + 2 next
let (cb2, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 1,
sapling_activation_height() + 1,
cb1.hash(),
extfvk.clone(),
value,
);
let (cb3, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 2,
sapling_activation_height() + 2,
cb2.hash(),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb3);
match scan_cached_blocks(db_cache, db_data, None) {
match scan_cached_blocks(&tests::network(), db_cache, db_data, None) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(
e.to_string(),
format!(
"Expected height of next CompactBlock to be {}, but was {}",
SAPLING_ACTIVATION_HEIGHT + 1,
SAPLING_ACTIVATION_HEIGHT + 2
sapling_activation_height() + 1,
sapling_activation_height() + 2
)
),
}
// If we add a block of height SAPLING_ACTIVATION_HEIGHT + 1, we can now scan both
insert_into_cache(db_cache, &cb2);
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
assert_eq!(
get_balance(db_data, 0).unwrap(),
Amount::from_u64(150_000).unwrap()
@ -595,7 +625,7 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
// Account balance should be zero
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
@ -603,7 +633,7 @@ mod tests {
// Create a fake CompactBlock sending value to the address
let value = Amount::from_u64(5).unwrap();
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
sapling_activation_height(),
BlockHash([0; 32]),
extfvk.clone(),
value,
@ -611,18 +641,19 @@ mod tests {
insert_into_cache(db_cache, &cb);
// Scan the cache
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Account balance should reflect the received note
assert_eq!(get_balance(db_data, 0).unwrap(), value);
// Create a second fake CompactBlock sending more value to the address
let value2 = Amount::from_u64(7).unwrap();
let (cb2, _) = fake_compact_block(SAPLING_ACTIVATION_HEIGHT + 1, cb.hash(), extfvk, value2);
let (cb2, _) =
fake_compact_block(sapling_activation_height() + 1, cb.hash(), extfvk, value2);
insert_into_cache(db_cache, &cb2);
// Scan the cache again
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Account balance should reflect both received notes
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);
@ -641,7 +672,7 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
// Account balance should be zero
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
@ -649,7 +680,7 @@ mod tests {
// Create a fake CompactBlock sending value to the address
let value = Amount::from_u64(5).unwrap();
let (cb, nf) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
sapling_activation_height(),
BlockHash([0; 32]),
extfvk.clone(),
value,
@ -657,7 +688,7 @@ mod tests {
insert_into_cache(db_cache, &cb);
// Scan the cache
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Account balance should reflect the received note
assert_eq!(get_balance(db_data, 0).unwrap(), value);
@ -669,7 +700,7 @@ mod tests {
insert_into_cache(
db_cache,
&fake_compact_block_spending(
SAPLING_ACTIVATION_HEIGHT + 1,
sapling_activation_height() + 1,
cb.hash(),
(nf, value),
extfvk,
@ -679,7 +710,7 @@ mod tests {
);
// Scan the cache again
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Account balance should equal the change
assert_eq!(get_balance(db_data, 0).unwrap(), value - value2);

View File

@ -1,11 +1,10 @@
//! Functions for creating transactions.
use ff::PrimeField;
use rand_core::OsRng;
use rusqlite::{types::ToSql, Connection, NO_PARAMS};
use std::convert::TryInto;
use std::path::Path;
use zcash_client_backend::encoding::encode_extended_full_viewing_key;
use zcash_client_backend::{address::RecipientAddress, encoding::encode_extended_full_viewing_key};
use zcash_primitives::{
consensus,
keys::OutgoingViewingKey,
@ -22,9 +21,8 @@ use zcash_primitives::{
};
use crate::{
address::RecipientAddress,
error::{Error, ErrorKind},
get_target_and_anchor_heights, Network, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY,
get_target_and_anchor_heights,
};
/// Describes a policy for which outgoing viewing key should be able to decrypt
@ -88,13 +86,16 @@ struct SelectedNoteRow {
/// # Examples
///
/// ```
/// use zcash_client_backend::{
/// use zcash_primitives::{
/// consensus::{self, Network},
/// constants::testnet::COIN_TYPE,
/// transaction::components::Amount
/// };
/// use zcash_proofs::prover::LocalTxProver;
/// use zcash_client_backend::{
/// keys::spending_key,
/// };
/// use zcash_client_sqlite::transact::{create_to_address, OvkPolicy};
/// use zcash_primitives::{consensus, transaction::components::Amount};
/// use zcash_proofs::prover::LocalTxProver;
///
/// let tx_prover = match LocalTxProver::with_default_location() {
/// Some(tx_prover) => tx_prover,
@ -108,6 +109,7 @@ struct SelectedNoteRow {
/// let to = extsk.default_address().unwrap().1.into();
/// match create_to_address(
/// "/path/to/data.db",
/// &Network::TestNetwork,
/// consensus::BranchId::Sapling,
/// tx_prover,
/// (account, &extsk),
@ -120,8 +122,9 @@ struct SelectedNoteRow {
/// Err(e) => (),
/// }
/// ```
pub fn create_to_address<P: AsRef<Path>>(
db_data: P,
pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
db_data: DB,
params: &P,
consensus_branch_id: consensus::BranchId,
prover: impl TxProver,
(account, extsk): (u32, &ExtendedSpendingKey),
@ -139,8 +142,11 @@ pub fn create_to_address<P: AsRef<Path>>(
.prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")?
.exists(&[
account.to_sql()?,
encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk)
.to_sql()?,
encode_extended_full_viewing_key(
params.hrp_sapling_extended_full_viewing_key(),
&extfvk,
)
.to_sql()?,
])?
{
return Err(Error(ErrorKind::InvalidExtSK(account)));
@ -154,10 +160,7 @@ pub fn create_to_address<P: AsRef<Path>>(
};
// Target the next block, assuming we are up-to-date.
let (height, anchor_height) = {
let (target_height, anchor_height) = get_target_and_anchor_heights(&data)?;
(target_height, i64::from(anchor_height))
};
let (height, anchor_height) = get_target_and_anchor_heights(&data)?;
// The goal of this SQL statement is to select the oldest notes until the required
// value has been reached, and then fetch the witnesses at the desired height for the
@ -204,10 +207,10 @@ pub fn create_to_address<P: AsRef<Path>>(
let notes = stmt_select_notes.query_and_then::<_, Error, _, _>(
&[
i64::from(account),
anchor_height,
i64::from(anchor_height),
target_value,
target_value,
anchor_height,
i64::from(anchor_height),
],
|row| {
let diversifier = {
@ -270,7 +273,7 @@ pub fn create_to_address<P: AsRef<Path>>(
}
// Create the transaction
let mut builder = Builder::<Network, OsRng>::new(height);
let mut builder = Builder::new(params.clone(), height);
for selected in notes {
builder.add_sapling_spend(
extsk.clone(),
@ -306,7 +309,7 @@ pub fn create_to_address<P: AsRef<Path>>(
stmt_insert_tx.execute(&[
tx.txid().0.to_sql()?,
created.to_sql()?,
tx.expiry_height.to_sql()?,
i64::from(tx.expiry_height).to_sql()?,
raw_tx.to_sql()?,
])?;
let id_tx = data.last_insert_rowid();
@ -327,7 +330,7 @@ pub fn create_to_address<P: AsRef<Path>>(
// Save the sent note in the database.
// TODO: Decide how to save transparent output information.
let to_str = to.to_string();
let to_str = to.encode(params);
if let Some(memo) = memo {
let mut stmt_insert_sent_note = data.prepare(
"INSERT INTO sent_notes (tx, output_index, from_account, address, value, memo)
@ -363,9 +366,9 @@ pub fn create_to_address<P: AsRef<Path>>(
#[cfg(test)]
mod tests {
use group::cofactor::CofactorGroup;
use rusqlite::Connection;
use tempfile::NamedTempFile;
use zcash_primitives::{
block::BlockHash,
consensus,
@ -374,17 +377,18 @@ mod tests {
transaction::{components::Amount, Transaction},
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
};
use zcash_proofs::prover::LocalTxProver;
use super::{create_to_address, OvkPolicy};
use crate::{
init::{init_accounts_table, init_blocks_table, init_cache_database, init_data_database},
query::{get_balance, get_verified_balance},
scan::scan_cached_blocks,
tests::{fake_compact_block, insert_into_cache},
Network, SAPLING_ACTIVATION_HEIGHT,
tests::{self, fake_compact_block, insert_into_cache, sapling_activation_height},
};
use super::{create_to_address, OvkPolicy};
fn test_prover() -> impl TxProver {
match LocalTxProver::with_default_location() {
Some(tx_prover) => tx_prover,
@ -407,12 +411,13 @@ mod tests {
ExtendedFullViewingKey::from(&extsk0),
ExtendedFullViewingKey::from(&extsk1),
];
init_accounts_table(&db_data, &extfvks).unwrap();
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap();
let to = extsk0.default_address().unwrap().1.into();
// Invalid extsk for the given account should cause an error
match create_to_address(
db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk1),
@ -426,6 +431,7 @@ mod tests {
}
match create_to_address(
db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(1, &extsk0),
@ -448,12 +454,13 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
init_accounts_table(&db_data, &extfvks).unwrap();
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap();
let to = extsk.default_address().unwrap().1.into();
// We cannot do anything if we aren't synchronised
match create_to_address(
db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
@ -477,7 +484,7 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
init_accounts_table(&db_data, &extfvks).unwrap();
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap();
let to = extsk.default_address().unwrap().1.into();
// Account balance should be zero
@ -486,6 +493,7 @@ mod tests {
// We cannot spend anything
match create_to_address(
db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
@ -515,18 +523,18 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
// Add funds to the wallet in a single note
let value = Amount::from_u64(50000).unwrap();
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
sapling_activation_height(),
BlockHash([0; 32]),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb);
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Verified balance matches total balance
assert_eq!(get_balance(db_data, 0).unwrap(), value);
@ -534,13 +542,13 @@ mod tests {
// Add more funds to the wallet in a second note
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 1,
sapling_activation_height() + 1,
cb.hash(),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb);
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Verified balance does not include the second note
assert_eq!(get_balance(db_data, 0).unwrap(), value + value);
@ -551,6 +559,7 @@ mod tests {
let to = extsk2.default_address().unwrap().1.into();
match create_to_address(
db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
@ -570,18 +579,19 @@ mod tests {
// note is verified
for i in 2..10 {
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + i,
sapling_activation_height() + i,
cb.hash(),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb);
}
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Second spend still fails
match create_to_address(
db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
@ -599,17 +609,18 @@ mod tests {
// Mine block 11 so that the second note becomes verified
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 10,
sapling_activation_height() + 10,
cb.hash(),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb);
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Second spend should now succeed
create_to_address(
db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
@ -634,18 +645,18 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
// Add funds to the wallet in a single note
let value = Amount::from_u64(50000).unwrap();
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
sapling_activation_height(),
BlockHash([0; 32]),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb);
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
assert_eq!(get_balance(db_data, 0).unwrap(), value);
// Send some of the funds to another address
@ -653,6 +664,7 @@ mod tests {
let to = extsk2.default_address().unwrap().1.into();
create_to_address(
db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
@ -666,6 +678,7 @@ mod tests {
// A second spend fails because there are no usable notes
match create_to_address(
db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
@ -685,18 +698,19 @@ mod tests {
// until just before the first transaction expires
for i in 1..22 {
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + i,
sapling_activation_height() + i,
cb.hash(),
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])),
value,
);
insert_into_cache(db_cache, &cb);
}
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Second spend still fails
match create_to_address(
db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
@ -714,17 +728,18 @@ mod tests {
// Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 22,
sapling_activation_height() + 22,
cb.hash(),
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])),
value,
);
insert_into_cache(db_cache, &cb);
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
// Second spend should now succeed
create_to_address(
db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
@ -738,6 +753,7 @@ mod tests {
#[test]
fn ovk_policy_prevents_recovery_from_chain() {
let network = tests::network();
let cache_file = NamedTempFile::new().unwrap();
let db_cache = cache_file.path();
init_cache_database(&db_cache).unwrap();
@ -749,18 +765,18 @@ mod tests {
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
init_accounts_table(&db_data, &network, &[extfvk.clone()]).unwrap();
// Add funds to the wallet in a single note
let value = Amount::from_u64(50000).unwrap();
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
sapling_activation_height(),
BlockHash([0; 32]),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb);
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&network, db_cache, db_data, None).unwrap();
assert_eq!(get_balance(db_data, 0).unwrap(), value);
let extsk2 = ExtendedSpendingKey::master(&[]);
@ -770,6 +786,7 @@ mod tests {
let send_and_recover_with_policy = |ovk_policy| {
let tx_row = create_to_address(
db_data,
&network,
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
@ -804,12 +821,13 @@ mod tests {
.unwrap();
let output = &tx.shielded_outputs[output_index as usize];
try_sapling_output_recovery::<Network>(
SAPLING_ACTIVATION_HEIGHT as u32,
try_sapling_output_recovery(
&network,
sapling_activation_height(),
&extfvk.fvk.ovk,
&output.cv,
&output.cmu,
&output.ephemeral_key.into_subgroup().unwrap(),
&output.ephemeral_key,
&output.enc_ciphertext,
&output.out_ciphertext,
)
@ -824,14 +842,14 @@ mod tests {
// so that the first transaction expires
for i in 1..=22 {
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + i,
sapling_activation_height() + i,
cb.hash(),
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])),
value,
);
insert_into_cache(db_cache, &cb);
}
scan_cached_blocks(db_cache, db_data, None).unwrap();
scan_cached_blocks(&network, db_cache, db_data, None).unwrap();
// Send the funds again, discarding history.
// Neither transaction output is decryptable by the sender.

View File

@ -0,0 +1,19 @@
[package]
name = "zcash_extensions"
description = "Zcash Extension implementations & consensus node integration layer."
version = "0.0.0"
authors = ["Jack Grigg <jack@z.cash>", "Kris Nuttycombe <kris@z.cash>"]
homepage = "https://github.com/zcash/librustzcash"
repository = "https://github.com/zcash/librustzcash"
license = "MIT OR Apache-2.0"
edition = "2018"
[dependencies]
blake2b_simd = "0.5"
zcash_primitives = { version = "0.4.0", path = "../zcash_primitives" }
[dev-dependencies]
ff = "0.8"
jubjub = "0.5.1"
rand_core = "0.5.1"
zcash_proofs = { version = "0.4.0", path = "../zcash_proofs" }

View File

@ -0,0 +1 @@
pub mod transparent;

View File

@ -0,0 +1,122 @@
//! Consensus logic for Transparent Zcash Extensions.
use std::convert::TryFrom;
use zcash_primitives::consensus::{BlockHeight, BranchId};
use zcash_primitives::extensions::transparent::{Error, Extension, Precondition, Witness};
use zcash_primitives::transaction::{components::TzeOut, Transaction};
use crate::transparent::demo;
/// Wire value for the demo extension identifier.
pub const EXTENSION_DEMO: u32 = 0;
/// The set of programs that have assigned type IDs within the Zcash consensus rules.
#[derive(Debug, Clone, Copy)]
pub enum ExtensionId {
Demo,
}
pub struct InvalidExtId(u32);
impl TryFrom<u32> for ExtensionId {
type Error = InvalidExtId;
fn try_from(t: u32) -> Result<Self, Self::Error> {
match t {
EXTENSION_DEMO => Ok(ExtensionId::Demo),
n => Err(InvalidExtId(n)),
}
}
}
impl From<ExtensionId> for u32 {
fn from(type_id: ExtensionId) -> u32 {
match type_id {
ExtensionId::Demo => EXTENSION_DEMO,
}
}
}
/// The complete set of context data that is available to any extension having
/// an assigned extension type ID. This type may be modified in the future if
/// additional context information is required by newly integrated TZEs.
pub struct Context<'a> {
pub height: BlockHeight,
pub tx: &'a Transaction,
}
impl<'a> Context<'a> {
pub fn new(height: BlockHeight, tx: &'a Transaction) -> Self {
Context { height, tx }
}
}
/// Implementations of this trait provide complete extension validation rules
/// for a specific epoch, and handle dispatch of verification to individual
/// TZEs based upon extension ID and mode.
pub trait Epoch {
type Error;
/// For a specific epoch, if the extension ID and mode of the supplied
/// witness matches that of the supplied precondition, these values will
/// be passed to the associated extension for verification, along with
/// whatever that extension requires of the provided [`Context`].
///
/// Successful validation is indicated by the returned Result containing
/// no errors.
fn verify<'a>(
&self,
precondition: &Precondition,
witness: &Witness,
ctx: &Context<'a>,
) -> Result<(), Error<Self::Error>>;
}
/// Implementation of required operations for the demo extension, as satisfied
/// by the context.
impl<'a> demo::Context for Context<'a> {
fn is_tze_only(&self) -> bool {
self.tx.vin.is_empty()
&& self.tx.vout.is_empty()
&& self.tx.shielded_spends.is_empty()
&& self.tx.shielded_outputs.is_empty()
&& self.tx.joinsplits.is_empty()
}
fn tx_tze_outputs(&self) -> &[TzeOut] {
&self.tx.tze_outputs
}
}
/// Identifier for the set of TZEs associated with the ZFUTURE network upgrade.
/// This epoch is intended only for use on private test networks.
struct EpochVTest;
impl Epoch for EpochVTest {
type Error = String;
fn verify<'a>(
&self,
precondition: &Precondition,
witness: &Witness,
ctx: &Context<'a>,
) -> Result<(), Error<Self::Error>> {
let ext_id = ExtensionId::try_from(precondition.extension_id)
.map_err(|InvalidExtId(id)| Error::InvalidExtensionId(id))?;
// This epoch recognizes the following set of extensions:
match ext_id {
ExtensionId::Demo => demo::Program
.verify(precondition, witness, ctx)
.map_err(|e| Error::ProgramError(format!("Epoch vTest program error: {}", e))),
}
}
}
pub fn epoch_for_branch(branch_id: BranchId) -> Option<Box<dyn Epoch<Error = String>>> {
// Map from consensus branch IDs to epochs.
match branch_id {
BranchId::ZFuture => Some(Box::new(EpochVTest)),
_ => None,
}
}

View File

@ -0,0 +1,2 @@
pub mod consensus;
pub mod transparent;

View File

@ -0,0 +1,3 @@
//! Zcash transparent extensions.
pub mod demo;

View File

@ -0,0 +1,799 @@
//! Demo implementation of TZE consensus rules.
//!
//! The demo program implements a dual-hash-lock encumbrance with the following form:
//!
//! > `hash = BLAKE2b_256(preimage_1 || BLAKE2b_256(preimage_2))`
//!
//! The two preimages are revealed in sequential transactions, demonstrating how TZEs can
//! impose constraints on how program modes are chained together.
//!
//! The demo program has two modes:
//!
//! - Mode 0: `hash_1 = BLAKE2b_256(preimage_1 || hash_2)`
//! - Mode 1: `hash_2 = BLAKE2b_256(preimage_2)`
//!
//! and uses the following transaction formats:
//!
//! - `tx_a`: `[ [any input types...] ----> TzeOut(value, hash_1) ]`
//! - `tx_b`: `[ TzeIn(tx_a, preimage_1) -> TzeOut(value, hash_2) ]`
//! - `tx_c`: `[ TzeIn(tx_b, preimage_2) -> [any output types...] ]`
use std::convert::TryFrom;
use std::convert::TryInto;
use std::fmt;
use blake2b_simd::Params;
use zcash_primitives::{
extensions::transparent::{Extension, ExtensionTxBuilder, FromPayload, ToPayload},
transaction::components::{amount::Amount, OutPoint, TzeOut},
};
/// Types and constants used for Mode 0 (open a channel)
mod open {
pub const MODE: u32 = 0;
#[derive(Debug, PartialEq)]
pub struct Precondition(pub [u8; 32]);
#[derive(Debug, PartialEq)]
pub struct Witness(pub [u8; 32]);
}
/// Types and constants used for Mode 1 (close a channel)
mod close {
pub const MODE: u32 = 1;
#[derive(Debug, PartialEq)]
pub struct Precondition(pub [u8; 32]);
#[derive(Debug, PartialEq)]
pub struct Witness(pub [u8; 32]);
}
/// The precondition type for the demo extension.
#[derive(Debug, PartialEq)]
pub enum Precondition {
Open(open::Precondition),
Close(close::Precondition),
}
impl Precondition {
/// Convenience constructor for opening precondition values.
pub fn open(hash: [u8; 32]) -> Self {
Precondition::Open(open::Precondition(hash))
}
/// Convenience constructor for closing precondition values.
pub fn close(hash: [u8; 32]) -> Self {
Precondition::Close(close::Precondition(hash))
}
}
/// Errors that may be produced during parsing and verification of demo preconditions and
/// witnesses.
#[derive(Debug, PartialEq)]
pub enum Error {
/// Parse error indicating that the payload of the condition or the witness was
/// not 32 bytes.
IllegalPayloadLength(usize),
/// Verification error indicating that the specified mode was not recognized by
/// the extension.
ModeInvalid(u32),
/// Verification error indicating that the transaction provided in the verification
/// context was missing required TZE inputs or outputs.
NonTzeTxn,
/// Verification error indicating that the witness being verified did not satisfy the
/// precondition under inspection.
HashMismatch,
/// Verification error indicating that the mode requested by the witness value did not
/// conform to that of the precondition under inspection.
ModeMismatch,
/// Verification error indicating that an `Open`-mode precondition was encountered
/// when a `Close` was expected.
ExpectedClose,
/// Verification error indicating that an unexpected number of TZE outputs (more than
/// one) was encountered in the transaction under inspection, in violation of
/// the extension's invariants.
InvalidOutputQty(usize),
}
impl fmt::Display for Error {
fn fmt<'a>(&self, f: &mut fmt::Formatter<'a>) -> fmt::Result {
match self {
Error::IllegalPayloadLength(sz) => write!(f, "Illegal payload length for demo: {}", sz),
Error::ModeInvalid(m) => write!(f, "Invalid TZE mode for demo program: {}", m),
Error::NonTzeTxn => write!(f, "Transaction has non-TZE inputs."),
Error::HashMismatch => write!(f, "Hash mismatch"),
Error::ModeMismatch => write!(f, "Extension operation mode mismatch."),
Error::ExpectedClose => write!(f, "Got open, expected close."),
Error::InvalidOutputQty(qty) => write!(f, "Incorrect number of outputs: {}", qty),
}
}
}
impl TryFrom<(u32, Precondition)> for Precondition {
type Error = Error;
fn try_from(from: (u32, Self)) -> Result<Self, Self::Error> {
match from {
(open::MODE, Precondition::Open(p)) => Ok(Precondition::Open(p)),
(close::MODE, Precondition::Close(p)) => Ok(Precondition::Close(p)),
_ => Err(Error::ModeInvalid(from.0)),
}
}
}
impl FromPayload for Precondition {
type Error = Error;
fn from_payload(mode: u32, payload: &[u8]) -> Result<Self, Self::Error> {
match mode {
open::MODE => payload
.try_into()
.map_err(|_| Error::IllegalPayloadLength(payload.len()))
.map(Precondition::open),
close::MODE => payload
.try_into()
.map_err(|_| Error::IllegalPayloadLength(payload.len()))
.map(Precondition::close),
_ => Err(Error::ModeInvalid(mode)),
}
}
}
impl ToPayload for Precondition {
fn to_payload(&self) -> (u32, Vec<u8>) {
match self {
Precondition::Open(p) => (open::MODE, p.0.to_vec()),
Precondition::Close(p) => (close::MODE, p.0.to_vec()),
}
}
}
/// The witness type for the demo extension.
#[derive(Debug, PartialEq)]
pub enum Witness {
Open(open::Witness),
Close(close::Witness),
}
impl Witness {
pub fn open(preimage: [u8; 32]) -> Self {
Witness::Open(open::Witness(preimage))
}
pub fn close(preimage: [u8; 32]) -> Self {
Witness::Close(close::Witness(preimage))
}
}
impl TryFrom<(u32, Witness)> for Witness {
type Error = Error;
fn try_from(from: (u32, Self)) -> Result<Self, Self::Error> {
match from {
(open::MODE, Witness::Open(p)) => Ok(Witness::Open(p)),
(close::MODE, Witness::Close(p)) => Ok(Witness::Close(p)),
_ => Err(Error::ModeInvalid(from.0)),
}
}
}
impl FromPayload for Witness {
type Error = Error;
fn from_payload(mode: u32, payload: &[u8]) -> Result<Self, Self::Error> {
match mode {
open::MODE => payload
.try_into()
.map_err(|_| Error::IllegalPayloadLength(payload.len()))
.map(Witness::open),
close::MODE => payload
.try_into()
.map_err(|_| Error::IllegalPayloadLength(payload.len()))
.map(Witness::close),
_ => Err(Error::ModeInvalid(mode)),
}
}
}
impl ToPayload for Witness {
fn to_payload(&self) -> (u32, Vec<u8>) {
match self {
Witness::Open(w) => (open::MODE, w.0.to_vec()),
Witness::Close(w) => (close::MODE, w.0.to_vec()),
}
}
}
/// This trait defines the context information that the demo extension requires
/// be made available to it by a consensus node integrating this extension.
///
/// This context type provides accessors to information relevant to a single
/// transaction being validated by the extension.
pub trait Context {
/// Predicate used to determine whether this transaction has only TZE
/// inputs and outputs. The demo extension does not support verification
/// of transactions which have either shielded or transparent inputs and
/// outputs.
fn is_tze_only(&self) -> bool;
/// List of all TZE outputs in the transaction being validate by the extension.
fn tx_tze_outputs(&self) -> &[TzeOut];
}
/// Marker type for the demo extension.
///
/// A value of this type will be used as the receiver for
/// `zcash_primitives::extensions::transparent::Extension` method invocations.
pub struct Program;
impl<C: Context> Extension<C> for Program {
type Precondition = Precondition;
type Witness = Witness;
type Error = Error;
/// Runs the program against the given precondition, witness, and context.
///
/// At this point the precondition and witness have been parsed and validated
/// non-contextually, and are guaranteed to both be for this program. All subsequent
/// validation is this function's responsibility.
fn verify_inner(
&self,
precondition: &Precondition,
witness: &Witness,
context: &C,
) -> Result<(), Error> {
// This match statement is selecting the mode that the program is operating in,
// based on the enums defined in the parser.
match (precondition, witness) {
(Precondition::Open(p_open), Witness::Open(w_open)) => {
// In OPEN mode, we enforce that the transaction must only contain inputs
// and outputs from this program. The consensus rules enforce that if a
// transaction contains both TZE inputs and TZE outputs, they must all be
// of the same program type. Therefore we only need to check that the
// transaction does not contain any other type of input or output.
if !context.is_tze_only() {
return Err(Error::NonTzeTxn);
}
// Next, check that there is only a single TZE output of the correct type.
let outputs = context.tx_tze_outputs();
match outputs {
[tze_out] => match Precondition::from_payload(
tze_out.precondition.mode,
&tze_out.precondition.payload,
) {
Ok(Precondition::Close(p_close)) => {
// Finally, check the precondition:
// precondition_open = BLAKE2b_256(witness_open || precondition_close)
let hash = Params::new()
.hash_length(32)
.personal(b"demo_pc_h1_perso")
.to_state()
.update(&w_open.0)
.update(&p_close.0)
.finalize();
if hash.as_bytes() == p_open.0 {
Ok(())
} else {
Err(Error::HashMismatch)
}
}
Ok(Precondition::Open(_)) => Err(Error::ExpectedClose),
Err(e) => Err(e),
},
_ => Err(Error::InvalidOutputQty(outputs.len())),
}
}
(Precondition::Close(p), Witness::Close(w)) => {
// In CLOSE mode, we only require that the precondition is satisfied:
// precondition_close = BLAKE2b_256(witness_close)
let hash = Params::new()
.hash_length(32)
.personal(b"demo_pc_h2_perso")
.hash(&w.0);
if hash.as_bytes() == p.0 {
Ok(())
} else {
Err(Error::HashMismatch)
}
}
_ => Err(Error::ModeMismatch),
}
}
}
fn hash_1(preimage_1: &[u8; 32], hash_2: &[u8; 32]) -> [u8; 32] {
let mut hash = [0; 32];
hash.copy_from_slice(
Params::new()
.hash_length(32)
.personal(b"demo_pc_h1_perso")
.to_state()
.update(preimage_1)
.update(hash_2)
.finalize()
.as_bytes(),
);
hash
}
/// Wrapper for [`zcash_primitives::transaction::builder::Builder`] that simplifies
/// constructing transactions that utilize the features of the demo extension.
pub struct DemoBuilder<B> {
/// The wrapped transaction builder.
pub txn_builder: B,
/// The assigned identifier for this extension. This is necessary as the author
/// of the demo extension will not know ahead of time what identifier will be
/// assigned to it at the time of inclusion in the Zcash consensus rules.
pub extension_id: u32,
}
/// Errors that can occur in construction of transactions using `DemoBuilder`.
#[derive(Debug)]
pub enum DemoBuildError<E> {
/// Wrapper for errors returned from the underlying `Builder`
BaseBuilderError(E),
ExpectedOpen,
ExpectedClose,
PrevoutParseFailure(Error),
TransferMismatch {
expected: [u8; 32],
actual: [u8; 32],
},
CloseMismatch {
expected: [u8; 32],
actual: [u8; 32],
},
}
/// Convenience methods for use with [`zcash_primitives::transaction::builder::Builder`]
/// for constructing transactions that utilize the features of the demo extension.
impl<'a, B: ExtensionTxBuilder<'a>> DemoBuilder<&mut B> {
/// Add a channel-opening precondition to the outputs of the transaction under
/// construction.
pub fn demo_open(
&mut self,
value: Amount,
hash_1: [u8; 32],
) -> Result<(), DemoBuildError<B::BuildError>> {
// Call through to the generic builder.
self.txn_builder
.add_tze_output(self.extension_id, value, &Precondition::open(hash_1))
.map_err(DemoBuildError::BaseBuilderError)
}
/// Add a witness to a previous channel-opening precondition and a new channel-closing
/// precondition to the transaction under construction.
pub fn demo_transfer_to_close(
&mut self,
prevout: (OutPoint, TzeOut),
transfer_amount: Amount,
preimage_1: [u8; 32],
hash_2: [u8; 32],
) -> Result<(), DemoBuildError<B::BuildError>> {
let h1 = hash_1(&preimage_1, &hash_2);
// eagerly validate the relationship between prevout.1 and preimage_1
match Precondition::from_payload(
prevout.1.precondition.mode,
&prevout.1.precondition.payload,
) {
Err(parse_failure) => Err(DemoBuildError::PrevoutParseFailure(parse_failure)),
Ok(Precondition::Close(_)) => Err(DemoBuildError::ExpectedOpen),
Ok(Precondition::Open(hash)) if hash.0 != h1 => Err(DemoBuildError::TransferMismatch {
expected: hash.0,
actual: h1,
}),
Ok(Precondition::Open(_)) => {
self.txn_builder
.add_tze_input(self.extension_id, open::MODE, prevout, move |_| {
Ok(Witness::open(preimage_1))
})
.map_err(DemoBuildError::BaseBuilderError)?;
self.txn_builder
.add_tze_output(
self.extension_id,
transfer_amount,
&Precondition::close(hash_2),
)
.map_err(DemoBuildError::BaseBuilderError)
}
}
}
/// Add a channel-closing witness to the transaction under construction.
pub fn demo_close(
&mut self,
prevout: (OutPoint, TzeOut),
preimage_2: [u8; 32],
) -> Result<(), DemoBuildError<B::BuildError>> {
let hash_2 = {
let mut hash = [0; 32];
hash.copy_from_slice(
Params::new()
.hash_length(32)
.personal(b"demo_pc_h2_perso")
.hash(&preimage_2)
.as_bytes(),
);
hash
};
// eagerly validate the relationship between prevout.1 and preimage_2
match Precondition::from_payload(
prevout.1.precondition.mode,
&prevout.1.precondition.payload,
) {
Err(parse_failure) => Err(DemoBuildError::PrevoutParseFailure(parse_failure)),
Ok(Precondition::Open(_)) => Err(DemoBuildError::ExpectedClose),
Ok(Precondition::Close(hash)) if hash.0 != hash_2 => {
Err(DemoBuildError::CloseMismatch {
expected: hash.0,
actual: hash_2,
})
}
Ok(Precondition::Close(_)) => self
.txn_builder
.add_tze_input(self.extension_id, close::MODE, prevout, move |_| {
Ok(Witness::close(preimage_2))
})
.map_err(DemoBuildError::BaseBuilderError),
}
}
}
#[cfg(test)]
mod tests {
use blake2b_simd::Params;
use ff::{Field, PrimeField};
use rand_core::OsRng;
use zcash_proofs::prover::LocalTxProver;
use zcash_primitives::{
consensus::{BranchId, H0, TEST_NETWORK},
extensions::transparent::{self as tze, Extension, FromPayload, ToPayload},
legacy::TransparentAddress,
merkle_tree::{CommitmentTree, IncrementalWitness},
primitives::Rseed,
sapling::Node,
transaction::{
builder::Builder,
components::{Amount, OutPoint, TzeIn, TzeOut},
Transaction, TransactionData,
},
zip32::ExtendedSpendingKey,
};
use super::{close, hash_1, open, Context, DemoBuilder, Precondition, Program, Witness};
fn demo_hashes(preimage_1: &[u8; 32], preimage_2: &[u8; 32]) -> ([u8; 32], [u8; 32]) {
let hash_2 = {
let mut hash = [0; 32];
hash.copy_from_slice(
Params::new()
.hash_length(32)
.personal(b"demo_pc_h2_perso")
.hash(preimage_2)
.as_bytes(),
);
hash
};
(hash_1(preimage_1, &hash_2), hash_2)
}
#[test]
fn precondition_open_round_trip() {
let data = vec![7; 32];
let p = Precondition::from_payload(open::MODE, &data).unwrap();
assert_eq!(p, Precondition::Open(open::Precondition([7; 32])));
assert_eq!(p.to_payload(), (open::MODE, data));
}
#[test]
fn precondition_close_round_trip() {
let data = vec![7; 32];
let p = Precondition::from_payload(close::MODE, &data).unwrap();
assert_eq!(p, Precondition::Close(close::Precondition([7; 32])));
assert_eq!(p.to_payload(), (close::MODE, data));
}
#[test]
fn precondition_rejects_invalid_mode_or_length() {
for mode in 0..3 {
for len in &[31, 33] {
let p = Precondition::from_payload(mode, &vec![7; *len]);
assert!(p.is_err());
}
}
}
#[test]
fn witness_open_round_trip() {
let data = vec![7; 32];
let w = Witness::from_payload(open::MODE, &data).unwrap();
assert_eq!(w, Witness::open([7; 32]));
assert_eq!(w.to_payload(), (open::MODE, data));
}
#[test]
fn witness_close_round_trip() {
let data = vec![7; 32];
let p = Witness::from_payload(close::MODE, &data).unwrap();
assert_eq!(p, Witness::close([7; 32]));
assert_eq!(p.to_payload(), (close::MODE, data));
}
#[test]
fn witness_rejects_invalid_mode_or_length() {
for mode in 0..3 {
for len in &[31, 33] {
let p = Witness::from_payload(mode, &vec![7; *len]);
assert!(p.is_err());
}
}
}
/// Dummy context
pub struct Ctx<'a> {
pub tx: &'a Transaction,
}
/// Implementation of required operations for the demo extension, as satisfied
/// by the context.
impl<'a> Context for Ctx<'a> {
fn is_tze_only(&self) -> bool {
self.tx.vin.is_empty()
&& self.tx.vout.is_empty()
&& self.tx.shielded_spends.is_empty()
&& self.tx.shielded_outputs.is_empty()
&& self.tx.joinsplits.is_empty()
}
fn tx_tze_outputs(&self) -> &[TzeOut] {
&self.tx.tze_outputs
}
}
#[test]
fn demo_program() {
let preimage_1 = [1; 32];
let preimage_2 = [2; 32];
let hash_2 = {
let mut hash = [0; 32];
hash.copy_from_slice(
Params::new()
.hash_length(32)
.personal(b"demo_pc_h2_perso")
.hash(&preimage_2)
.as_bytes(),
);
hash
};
let hash_1 = {
let mut hash = [0; 32];
hash.copy_from_slice(
Params::new()
.hash_length(32)
.personal(b"demo_pc_h1_perso")
.to_state()
.update(&preimage_1)
.update(&hash_2)
.finalize()
.as_bytes(),
);
hash
};
//
// Opening transaction
//
let out_a = TzeOut {
value: Amount::from_u64(1).unwrap(),
precondition: tze::Precondition::from(0, &Precondition::open(hash_1)),
};
let mut mtx_a = TransactionData::zfuture();
mtx_a.tze_outputs.push(out_a);
let tx_a = mtx_a.freeze().unwrap();
//
// Transfer
//
let in_b = TzeIn {
prevout: OutPoint::new(tx_a.txid().0, 0),
witness: tze::Witness::from(0, &Witness::open(preimage_1)),
};
let out_b = TzeOut {
value: Amount::from_u64(1).unwrap(),
precondition: tze::Precondition::from(0, &Precondition::close(hash_2)),
};
let mut mtx_b = TransactionData::zfuture();
mtx_b.tze_inputs.push(in_b);
mtx_b.tze_outputs.push(out_b);
let tx_b = mtx_b.freeze().unwrap();
//
// Closing transaction
//
let in_c = TzeIn {
prevout: OutPoint::new(tx_b.txid().0, 0),
witness: tze::Witness::from(0, &Witness::close(preimage_2)),
};
let mut mtx_c = TransactionData::zfuture();
mtx_c.tze_inputs.push(in_c);
let tx_c = mtx_c.freeze().unwrap();
// Verify tx_b
{
let ctx = Ctx { tx: &tx_b };
assert_eq!(
Program.verify(
&tx_a.tze_outputs[0].precondition,
&tx_b.tze_inputs[0].witness,
&ctx
),
Ok(())
);
}
// Verify tx_c
{
let ctx = Ctx { tx: &tx_c };
assert_eq!(
Program.verify(
&tx_b.tze_outputs[0].precondition,
&tx_c.tze_inputs[0].witness,
&ctx
),
Ok(())
);
}
}
#[test]
fn demo_builder_program() {
let preimage_1 = [1; 32];
let preimage_2 = [2; 32];
// Only run the test if we have the prover parameters.
let prover = match LocalTxProver::with_default_location() {
Some(prover) => prover,
None => return,
};
//
// Opening transaction
//
let mut rng = OsRng;
let mut builder_a = Builder::new_with_rng_zfuture(TEST_NETWORK, H0, rng);
// create some inputs to spend
let extsk = ExtendedSpendingKey::master(&[]);
let to = extsk.default_address().unwrap().1;
let note1 = to
.create_note(110000, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng)))
.unwrap();
let cm1 = Node::new(note1.cmu().to_repr());
let mut tree = CommitmentTree::new();
// fake that the note appears in some previous
// shielded output
tree.append(cm1).unwrap();
let witness1 = IncrementalWitness::from_tree(&tree);
builder_a
.add_sapling_spend(
extsk.clone(),
*to.diversifier(),
note1.clone(),
witness1.path().unwrap(),
)
.unwrap();
let mut db_a = DemoBuilder {
txn_builder: &mut builder_a,
extension_id: 0,
};
let value = Amount::from_u64(100000).unwrap();
let (h1, h2) = demo_hashes(&preimage_1, &preimage_2);
db_a.demo_open(value, h1)
.map_err(|e| format!("open failure: {:?}", e))
.unwrap();
let (tx_a, _) = builder_a
.build(BranchId::Canopy, &prover)
.map_err(|e| format!("build failure: {:?}", e))
.unwrap();
//
// Transfer
//
let mut builder_b = Builder::new_with_rng_zfuture(TEST_NETWORK, H0, rng);
let mut db_b = DemoBuilder {
txn_builder: &mut builder_b,
extension_id: 0,
};
let prevout_a = (OutPoint::new(tx_a.txid().0, 0), tx_a.tze_outputs[0].clone());
let value_xfr = Amount::from_u64(90000).unwrap();
db_b.demo_transfer_to_close(prevout_a, value_xfr, preimage_1, h2)
.map_err(|e| format!("transfer failure: {:?}", e))
.unwrap();
let (tx_b, _) = builder_b
.build(BranchId::Canopy, &prover)
.map_err(|e| format!("build failure: {:?}", e))
.unwrap();
//
// Closing transaction
//
let mut builder_c = Builder::new_with_rng_zfuture(TEST_NETWORK, H0, rng);
let mut db_c = DemoBuilder {
txn_builder: &mut builder_c,
extension_id: 0,
};
let prevout_b = (OutPoint::new(tx_a.txid().0, 0), tx_b.tze_outputs[0].clone());
db_c.demo_close(prevout_b, preimage_2)
.map_err(|e| format!("close failure: {:?}", e))
.unwrap();
builder_c
.add_transparent_output(
&TransparentAddress::PublicKey([0; 20]),
Amount::from_u64(80000).unwrap(),
)
.unwrap();
let (tx_c, _) = builder_c
.build(BranchId::Canopy, &prover)
.map_err(|e| format!("build failure: {:?}", e))
.unwrap();
// Verify tx_b
let ctx0 = Ctx { tx: &tx_b };
assert_eq!(
Program.verify(
&tx_a.tze_outputs[0].precondition,
&tx_b.tze_inputs[0].witness,
&ctx0
),
Ok(())
);
// Verify tx_c
let ctx1 = Ctx { tx: &tx_c };
assert_eq!(
Program.verify(
&tx_b.tze_outputs[0].precondition,
&tx_c.tze_inputs[0].witness,
&ctx1
),
Ok(())
);
}
}

View File

@ -30,20 +30,27 @@ hex = "0.4"
jubjub = "0.5.1"
lazy_static = "1"
log = "0.4"
proptest = { version = "0.10.1", optional = true }
rand = "0.7"
rand_core = "0.5.1"
ripemd160 = { version = "0.9", optional = true }
secp256k1 = { version = "0.19", optional = true }
sha2 = "0.9"
subtle = "2.2.1"
subtle = "2.2.3"
[dev-dependencies]
criterion = "0.3"
hex-literal = "0.2"
proptest = "0.10.1"
rand_xorshift = "0.2"
[features]
transparent-inputs = ["ripemd160", "secp256k1"]
test-dependencies = ["proptest"]
[[bench]]
name = "note_decryption"
harness = false
[[bench]]
name = "pedersen_hash"

View File

@ -0,0 +1,84 @@
use criterion::{criterion_group, criterion_main, Criterion};
use ff::Field;
use rand_core::OsRng;
use zcash_primitives::{
consensus::{NetworkUpgrade::Canopy, Parameters, TEST_NETWORK},
note_encryption::{try_sapling_note_decryption, Memo, SaplingNoteEncryption},
primitives::{Diversifier, PaymentAddress, ValueCommitment},
transaction::components::{OutputDescription, GROTH_PROOF_SIZE},
util::generate_random_rseed,
};
fn bench_note_decryption(c: &mut Criterion) {
let mut rng = OsRng;
let height = TEST_NETWORK.activation_height(Canopy).unwrap();
let valid_ivk = jubjub::Fr::random(&mut rng);
let invalid_ivk = jubjub::Fr::random(&mut rng);
// Construct a fake Sapling output as if we had just deserialized a transaction.
let output = {
let diversifier = Diversifier([0; 11]);
let pk_d = diversifier.g_d().unwrap() * valid_ivk;
let pa = PaymentAddress::from_parts(diversifier, pk_d).unwrap();
let rseed = generate_random_rseed(&TEST_NETWORK, height, &mut rng);
// Construct the value commitment for the proof instance
let value = 100;
let value_commitment = ValueCommitment {
value,
randomness: jubjub::Fr::random(&mut rng),
};
let cv = value_commitment.commitment().into();
let note = pa.create_note(value, rseed).unwrap();
let cmu = note.cmu();
let mut ne = SaplingNoteEncryption::new(None, note, pa, Memo::default(), &mut rng);
let ephemeral_key = ne.epk().clone().into();
let enc_ciphertext = ne.encrypt_note_plaintext();
let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu);
OutputDescription {
cv,
cmu,
ephemeral_key,
enc_ciphertext,
out_ciphertext,
zkproof: [0; GROTH_PROOF_SIZE],
}
};
let mut group = c.benchmark_group("Sapling note decryption");
group.bench_function("valid", |b| {
b.iter(|| {
try_sapling_note_decryption(
&TEST_NETWORK,
height,
&valid_ivk,
&output.ephemeral_key,
&output.cmu,
&output.enc_ciphertext,
)
.unwrap()
})
});
group.bench_function("invalid", |b| {
b.iter(|| {
try_sapling_note_decryption(
&TEST_NETWORK,
height,
&invalid_ivk,
&output.ephemeral_key,
&output.cmu,
&output.enc_ciphertext,
)
})
});
}
criterion_group!(benches, bench_note_decryption);
criterion_main!(benches);

View File

@ -1,48 +1,264 @@
//! Consensus parameters.
//! Consensus logic and parameters.
use std::cmp::{Ord, Ordering};
use std::convert::TryFrom;
use std::fmt;
use std::ops::{Add, Sub};
use crate::constants;
/// A wrapper type representing blockchain heights. Safe conversion from
/// various integer types, as well as addition and subtraction, are provided.
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct BlockHeight(u32);
pub const H0: BlockHeight = BlockHeight(0);
impl BlockHeight {
pub const fn from_u32(v: u32) -> BlockHeight {
BlockHeight(v)
}
}
impl fmt::Display for BlockHeight {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl Ord for BlockHeight {
fn cmp(&self, other: &Self) -> Ordering {
self.0.cmp(&other.0)
}
}
impl PartialOrd for BlockHeight {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl From<u32> for BlockHeight {
fn from(value: u32) -> Self {
BlockHeight(value)
}
}
impl From<u64> for BlockHeight {
fn from(value: u64) -> Self {
BlockHeight(value as u32)
}
}
impl TryFrom<i32> for BlockHeight {
type Error = std::num::TryFromIntError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
u32::try_from(value).map(BlockHeight)
}
}
impl TryFrom<i64> for BlockHeight {
type Error = std::num::TryFromIntError;
fn try_from(value: i64) -> Result<Self, Self::Error> {
u32::try_from(value).map(BlockHeight)
}
}
impl From<BlockHeight> for u32 {
fn from(value: BlockHeight) -> u32 {
value.0
}
}
impl From<BlockHeight> for u64 {
fn from(value: BlockHeight) -> u64 {
value.0 as u64
}
}
impl From<BlockHeight> for i64 {
fn from(value: BlockHeight) -> i64 {
value.0 as i64
}
}
impl Add<u32> for BlockHeight {
type Output = Self;
fn add(self, other: u32) -> Self {
BlockHeight(self.0 + other)
}
}
impl Add for BlockHeight {
type Output = Self;
fn add(self, other: Self) -> Self {
self + other.0
}
}
impl Sub<u32> for BlockHeight {
type Output = Self;
fn sub(self, other: u32) -> Self {
if other > self.0 {
panic!("Subtraction resulted in negative block height.");
}
BlockHeight(self.0 - other)
}
}
impl Sub for BlockHeight {
type Output = Self;
fn sub(self, other: Self) -> Self {
self - other.0
}
}
/// Zcash consensus parameters.
pub trait Parameters {
fn activation_height(nu: NetworkUpgrade) -> Option<u32>;
pub trait Parameters: Clone {
/// Returns the activation height for a particular network upgrade,
/// if an activation height has been set.
fn activation_height(&self, nu: NetworkUpgrade) -> Option<BlockHeight>;
fn is_nu_active(nu: NetworkUpgrade, height: u32) -> bool {
match Self::activation_height(nu) {
Some(h) if h <= height => true,
_ => false,
}
/// Returns the human-readable prefix for Sapling extended full
/// viewing keys for the network to which this Parameters value applies.
fn hrp_sapling_extended_full_viewing_key(&self) -> &str;
/// Returns the human-readable prefix for Sapling payment addresses
/// viewing keys for the network to which this Parameters value applies.
fn hrp_sapling_payment_address(&self) -> &str;
/// Returns the human-readable prefix for transparent pay-to-public-key-hash
/// payment addresses for the network to which this Parameters value applies.
fn b58_pubkey_address_prefix(&self) -> [u8; 2];
/// Returns the human-readable prefix for transparent pay-to-script-hash
/// payment addresses for the network to which this Parameters value applies.
fn b58_script_address_prefix(&self) -> [u8; 2];
/// Determines whether the specified network upgrade is active as of the
/// provided block height on the network to which this Parameters value applies.
fn is_nu_active(&self, nu: NetworkUpgrade, height: BlockHeight) -> bool {
self.activation_height(nu).map_or(false, |h| h <= height)
}
}
/// Marker struct for the production network.
#[derive(Clone, Copy, Debug)]
#[derive(PartialEq, Copy, Clone, Debug)]
pub struct MainNetwork;
pub const MAIN_NETWORK: MainNetwork = MainNetwork;
impl Parameters for MainNetwork {
fn activation_height(nu: NetworkUpgrade) -> Option<u32> {
fn activation_height(&self, nu: NetworkUpgrade) -> Option<BlockHeight> {
match nu {
NetworkUpgrade::Overwinter => Some(347_500),
NetworkUpgrade::Sapling => Some(419_200),
NetworkUpgrade::Blossom => Some(653_600),
NetworkUpgrade::Heartwood => Some(903_000),
NetworkUpgrade::Canopy => Some(1_046_400),
NetworkUpgrade::Overwinter => Some(BlockHeight(347_500)),
NetworkUpgrade::Sapling => Some(BlockHeight(419_200)),
NetworkUpgrade::Blossom => Some(BlockHeight(653_600)),
NetworkUpgrade::Heartwood => Some(BlockHeight(903_000)),
NetworkUpgrade::Canopy => Some(BlockHeight(1_046_400)),
NetworkUpgrade::ZFuture => None,
}
}
fn hrp_sapling_extended_full_viewing_key(&self) -> &str {
constants::mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY
}
fn hrp_sapling_payment_address(&self) -> &str {
constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS
}
fn b58_pubkey_address_prefix(&self) -> [u8; 2] {
constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX
}
fn b58_script_address_prefix(&self) -> [u8; 2] {
constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX
}
}
/// Marker struct for the test network.
#[derive(Clone, Copy, Debug)]
#[derive(PartialEq, Copy, Clone, Debug)]
pub struct TestNetwork;
pub const TEST_NETWORK: TestNetwork = TestNetwork;
impl Parameters for TestNetwork {
fn activation_height(nu: NetworkUpgrade) -> Option<u32> {
fn activation_height(&self, nu: NetworkUpgrade) -> Option<BlockHeight> {
match nu {
NetworkUpgrade::Overwinter => Some(207_500),
NetworkUpgrade::Sapling => Some(280_000),
NetworkUpgrade::Blossom => Some(584_000),
NetworkUpgrade::Heartwood => Some(903_800),
NetworkUpgrade::Canopy => Some(1_028_500),
NetworkUpgrade::Overwinter => Some(BlockHeight(207_500)),
NetworkUpgrade::Sapling => Some(BlockHeight(280_000)),
NetworkUpgrade::Blossom => Some(BlockHeight(584_000)),
NetworkUpgrade::Heartwood => Some(BlockHeight(903_800)),
NetworkUpgrade::Canopy => Some(BlockHeight(1_028_500)),
NetworkUpgrade::ZFuture => None,
}
}
fn hrp_sapling_extended_full_viewing_key(&self) -> &str {
constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY
}
fn hrp_sapling_payment_address(&self) -> &str {
constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS
}
fn b58_pubkey_address_prefix(&self) -> [u8; 2] {
constants::testnet::B58_PUBKEY_ADDRESS_PREFIX
}
fn b58_script_address_prefix(&self) -> [u8; 2] {
constants::testnet::B58_SCRIPT_ADDRESS_PREFIX
}
}
#[derive(PartialEq, Copy, Clone, Debug)]
pub enum Network {
MainNetwork,
TestNetwork,
}
impl Parameters for Network {
fn activation_height(&self, nu: NetworkUpgrade) -> Option<BlockHeight> {
match self {
Network::MainNetwork => MAIN_NETWORK.activation_height(nu),
Network::TestNetwork => TEST_NETWORK.activation_height(nu),
}
}
fn hrp_sapling_extended_full_viewing_key(&self) -> &str {
match self {
Network::MainNetwork => MAIN_NETWORK.hrp_sapling_extended_full_viewing_key(),
Network::TestNetwork => TEST_NETWORK.hrp_sapling_extended_full_viewing_key(),
}
}
fn hrp_sapling_payment_address(&self) -> &str {
match self {
Network::MainNetwork => MAIN_NETWORK.hrp_sapling_payment_address(),
Network::TestNetwork => TEST_NETWORK.hrp_sapling_payment_address(),
}
}
fn b58_pubkey_address_prefix(&self) -> [u8; 2] {
match self {
Network::MainNetwork => MAIN_NETWORK.b58_pubkey_address_prefix(),
Network::TestNetwork => TEST_NETWORK.b58_pubkey_address_prefix(),
}
}
fn b58_script_address_prefix(&self) -> [u8; 2] {
match self {
Network::MainNetwork => MAIN_NETWORK.b58_script_address_prefix(),
Network::TestNetwork => TEST_NETWORK.b58_script_address_prefix(),
}
}
}
@ -73,6 +289,12 @@ pub enum NetworkUpgrade {
///
/// [Canopy]: https://z.cash/upgrade/canopy/
Canopy,
/// The [ZFUTURE] network upgrade.
///
/// This upgrade is expected never to activate on mainnet;
/// it is intended for use in integration testing of functionality
/// that is a candidate for integration in a future network upgrade.
ZFuture,
}
impl fmt::Display for NetworkUpgrade {
@ -83,6 +305,7 @@ impl fmt::Display for NetworkUpgrade {
NetworkUpgrade::Blossom => write!(f, "Blossom"),
NetworkUpgrade::Heartwood => write!(f, "Heartwood"),
NetworkUpgrade::Canopy => write!(f, "Canopy"),
NetworkUpgrade::ZFuture => write!(f, "ZFUTURE"),
}
}
}
@ -95,6 +318,7 @@ impl NetworkUpgrade {
NetworkUpgrade::Blossom => BranchId::Blossom,
NetworkUpgrade::Heartwood => BranchId::Heartwood,
NetworkUpgrade::Canopy => BranchId::Canopy,
NetworkUpgrade::ZFuture => BranchId::ZFuture,
}
}
}
@ -140,6 +364,9 @@ pub enum BranchId {
Heartwood,
/// The consensus rules deployed by [`NetworkUpgrade::Canopy`].
Canopy,
/// Candidates for future consensus rules; this branch will never
/// activate on mainnet.
ZFuture,
}
impl TryFrom<u32> for BranchId {
@ -153,6 +380,7 @@ impl TryFrom<u32> for BranchId {
0x2bb4_0e60 => Ok(BranchId::Blossom),
0xf5b9_230b => Ok(BranchId::Heartwood),
0xe9ff_75a6 => Ok(BranchId::Canopy),
0xffff_ffff => Ok(BranchId::ZFuture),
_ => Err("Unknown consensus branch ID"),
}
}
@ -167,6 +395,7 @@ impl From<BranchId> for u32 {
BranchId::Blossom => 0x2bb4_0e60,
BranchId::Heartwood => 0xf5b9_230b,
BranchId::Canopy => 0xe9ff_75a6,
BranchId::ZFuture => 0xffff_ffff,
}
}
}
@ -176,9 +405,9 @@ impl BranchId {
/// the given height.
///
/// This is the branch ID that should be used when creating transactions.
pub fn for_height<C: Parameters>(height: u32) -> Self {
pub fn for_height<P: Parameters>(parameters: &P, height: BlockHeight) -> Self {
for nu in UPGRADES_IN_ORDER.iter().rev() {
if C::is_nu_active(*nu, height) {
if parameters.is_nu_active(*nu, height) {
return nu.branch_id();
}
}
@ -192,7 +421,9 @@ impl BranchId {
mod tests {
use std::convert::TryFrom;
use super::{BranchId, MainNetwork, NetworkUpgrade, Parameters, UPGRADES_IN_ORDER};
use super::{
BlockHeight, BranchId, NetworkUpgrade, Parameters, MAIN_NETWORK, UPGRADES_IN_ORDER,
};
#[test]
fn nu_ordering() {
@ -200,12 +431,10 @@ mod tests {
let nu_a = UPGRADES_IN_ORDER[i - 1];
let nu_b = UPGRADES_IN_ORDER[i];
match (
MainNetwork::activation_height(nu_a),
MainNetwork::activation_height(nu_b),
MAIN_NETWORK.activation_height(nu_a),
MAIN_NETWORK.activation_height(nu_b),
) {
(Some(a), Some(b)) if a < b => (),
(Some(_), None) => (),
(None, None) => (),
(a, b) if a < b => (),
_ => panic!(
"{} should not be before {} in UPGRADES_IN_ORDER",
nu_a, nu_b
@ -216,15 +445,9 @@ mod tests {
#[test]
fn nu_is_active() {
assert!(!MainNetwork::is_nu_active(NetworkUpgrade::Overwinter, 0));
assert!(!MainNetwork::is_nu_active(
NetworkUpgrade::Overwinter,
347_499
));
assert!(MainNetwork::is_nu_active(
NetworkUpgrade::Overwinter,
347_500
));
assert!(!MAIN_NETWORK.is_nu_active(NetworkUpgrade::Overwinter, BlockHeight(0)));
assert!(!MAIN_NETWORK.is_nu_active(NetworkUpgrade::Overwinter, BlockHeight(347_499)));
assert!(MAIN_NETWORK.is_nu_active(NetworkUpgrade::Overwinter, BlockHeight(347_500)));
}
#[test]
@ -235,25 +458,28 @@ mod tests {
#[test]
fn branch_id_for_height() {
assert_eq!(BranchId::for_height::<MainNetwork>(0), BranchId::Sprout,);
assert_eq!(
BranchId::for_height::<MainNetwork>(419_199),
BranchId::for_height(&MAIN_NETWORK, BlockHeight(0)),
BranchId::Sprout,
);
assert_eq!(
BranchId::for_height(&MAIN_NETWORK, BlockHeight(419_199)),
BranchId::Overwinter,
);
assert_eq!(
BranchId::for_height::<MainNetwork>(419_200),
BranchId::for_height(&MAIN_NETWORK, BlockHeight(419_200)),
BranchId::Sapling,
);
assert_eq!(
BranchId::for_height::<MainNetwork>(903_000),
BranchId::for_height(&MAIN_NETWORK, BlockHeight(903_000)),
BranchId::Heartwood,
);
assert_eq!(
BranchId::for_height::<MainNetwork>(1_046_400),
BranchId::for_height(&MAIN_NETWORK, BlockHeight(1_046_400)),
BranchId::Canopy,
);
assert_eq!(
BranchId::for_height::<MainNetwork>(5_000_000),
BranchId::for_height(&MAIN_NETWORK, BlockHeight(5_000_000)),
BranchId::Canopy,
);
}

View File

@ -5,6 +5,10 @@ use group::Group;
use jubjub::SubgroupPoint;
use lazy_static::lazy_static;
pub mod mainnet;
pub mod regtest;
pub mod testnet;
/// First 64 bytes of the BLAKE2s input during group hash.
/// This is chosen to be some random string that we couldn't have anticipated when we designed
/// the algorithm, for rigidity purposes.

View File

@ -0,0 +1 @@
pub mod transparent;

View File

@ -0,0 +1,195 @@
//! Core traits and structs for Transparent Zcash Extensions.
use crate::transaction::components::{Amount, OutPoint, TzeOut};
use std::fmt;
/// Binary parsing capability for TZE preconditions & witnesses.
///
/// Serialization formats interpreted by implementations of this trait become consensus-critical
/// upon activation of of the extension that uses them.
pub trait FromPayload: Sized {
type Error;
/// Parses an extension-specific witness or precondition from a mode and payload.
fn from_payload(mode: u32, payload: &[u8]) -> Result<Self, Self::Error>;
}
/// Binary serialization capability for TZE preconditions & witnesses.
///
/// Serialization formats used by implementations of this trait become consensus-critical upon
/// activation of of the extension that uses them.
pub trait ToPayload {
/// Returns a serialized payload and its corresponding mode.
fn to_payload(&self) -> (u32, Vec<u8>);
}
/// A condition that can be used to encumber transparent funds.
///
/// This struct is an intermediate representation between the serialized binary format which is
/// used inside of a transaction, and extension-specific types. The payload field of this struct is
/// treated as opaque to all but the extension corresponding to the encapsulated `extension_id`
/// value.
#[derive(Clone, Debug, PartialEq)]
pub struct Precondition {
pub extension_id: u32,
pub mode: u32,
pub payload: Vec<u8>,
}
impl Precondition {
/// Produce the intermediate format for an extension-specific precondition
/// type.
pub fn from<P: ToPayload>(extension_id: u32, value: &P) -> Precondition {
let (mode, payload) = value.to_payload();
Precondition {
extension_id,
mode,
payload,
}
}
/// Attempt to parse an extension-specific precondition value from the
/// intermediate representation.
pub fn try_to<P: FromPayload>(&self) -> Result<P, P::Error> {
P::from_payload(self.mode, &self.payload)
}
}
/// Data that satisfies the precondition for prior encumbered funds, enabling them to be spent.
///
/// This struct is an intermediate representation between the serialized binary format which is
/// used inside of a transaction, and extension-specific types. The payload field of this struct is
/// treated as opaque to all but the extension corresponding to the encapsulated `extension_id`
/// value.
#[derive(Clone, Debug, PartialEq)]
pub struct Witness {
pub extension_id: u32,
pub mode: u32,
pub payload: Vec<u8>,
}
impl Witness {
/// Produce the intermediate format for an extension-specific witness
/// type.
pub fn from<P: ToPayload>(extension_id: u32, value: &P) -> Witness {
let (mode, payload) = value.to_payload();
Witness {
extension_id,
mode,
payload,
}
}
/// Attempt to parse an extension-specific witness value from the
/// intermediate representation.
pub fn try_to<P: FromPayload>(&self) -> Result<P, P::Error> {
P::from_payload(self.mode, &self.payload)
}
}
#[derive(Debug, PartialEq)]
pub enum Error<E> {
InvalidExtensionId(u32),
ProgramError(E),
}
impl<E: fmt::Display> fmt::Display for Error<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidExtensionId(extension_id) => {
write!(f, "Unrecognized program type id {}", extension_id)
}
Error::ProgramError(err) => write!(f, "Program error: {}", err),
}
}
}
/// This is the primary trait which must be implemented by an extension type for that type to be
/// eligible for inclusion in Zcash consensus rules.
pub trait Extension<C> {
/// Extension-specific precondition type. The extension will need to implement
/// [`FromPayload<Error = Self::Error>`] for this type in order for their extension to be
/// eligible for integration into consensus rules.
type Precondition;
/// Extension-specific witness type. The extension will need to implement [`FromPayload<Error =
/// Self::Error>`] for this type in order for their extension to be eligible for integration
/// into consensus rules.
type Witness;
/// Extension-specific error type. This should encompass both parsing and verification errors.
type Error;
/// This is the primary method that an extension must implement. Implementations should return
/// [`Ok(())`] if verification of the witness succeeds against the supplied precondition, and
/// an error in any other case.
fn verify_inner(
&self,
precondition: &Self::Precondition,
witness: &Self::Witness,
context: &C,
) -> Result<(), Self::Error>;
/// This is a convenience method intended for use by consensus nodes at the integration point
/// to provide easy interoperation with the opaque, cross-extension `Precondition` and
/// `Witness` types.
fn verify(
&self,
precondition: &Precondition,
witness: &Witness,
context: &C,
) -> Result<(), Self::Error>
where
Self::Precondition: FromPayload<Error = Self::Error>,
Self::Witness: FromPayload<Error = Self::Error>,
{
self.verify_inner(
&Self::Precondition::from_payload(precondition.mode, &precondition.payload)?,
&Self::Witness::from_payload(witness.mode, &witness.payload)?,
&context,
)
}
}
/// An interface for transaction builders which support addition of TZE inputs and outputs.
///
/// This extension trait is satisfied by [`transaction::builder::Builder`]. It provides a minimal
/// contract for interacting with the transaction builder, that extension library authors can use
/// to add extension-specific builder traits that may be used to interact with the transaction
/// builder. This may make it simpler for projects that include transaction-builder functionality
/// to integrate with third-party extensions without those extensions being coupled to a particular
/// transaction or builder representation.
///
/// [`transaction::builder::Builder`]: crate::transaction::builder::Builder
pub trait ExtensionTxBuilder<'a> {
type BuildCtx;
type BuildError;
/// Adds a TZE input to the transaction by providing a witness to a precondition identified by a
/// prior outpoint.
///
/// The `witness_builder` function allows the transaction builder to provide extra contextual
/// information from the transaction under construction to be used in the production of this
/// witness (for example, so that the witness may internally make commitments based upon this
/// information.) For the standard transaction builder, the value provided here is the
/// transaction under construction.
fn add_tze_input<WBuilder, W: ToPayload>(
&mut self,
extension_id: u32,
mode: u32,
prevout: (OutPoint, TzeOut),
witness_builder: WBuilder,
) -> Result<(), Self::BuildError>
where
WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result<W, Self::BuildError>);
/// Adds a TZE precondition to the transaction which must be satisfied by a future transaction's
/// witness in order to spend the specified `amount`.
fn add_tze_output<Precondition: ToPayload>(
&mut self,
extension_id: u32,
value: Amount,
guarded_by: &Precondition,
) -> Result<(), Self::BuildError>;
}

View File

@ -180,6 +180,30 @@ impl FullViewingKey {
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::collection::vec;
use proptest::prelude::{any, prop_compose};
use crate::{
primitives::PaymentAddress,
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
};
prop_compose! {
pub fn arb_extended_spending_key()(v in vec(any::<u8>(), 32..252)) -> ExtendedSpendingKey {
ExtendedSpendingKey::master(&v)
}
}
prop_compose! {
pub fn arb_shielded_addr()(extsk in arb_extended_spending_key()) -> PaymentAddress {
let extfvk = ExtendedFullViewingKey::from(&extsk);
extfvk.default_address().unwrap().1
}
}
}
#[cfg(test)]
mod tests {
use group::{Group, GroupEncoding};

View File

@ -26,7 +26,7 @@ enum OpCode {
}
/// A serialized script, used inside transparent inputs and outputs of a transaction.
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Script(pub Vec<u8>);
impl Script {
@ -42,18 +42,14 @@ impl Script {
/// Returns the address that this Script contains, if any.
pub fn address(&self) -> Option<TransparentAddress> {
if self.0.len() == 25
&& self.0[0] == OpCode::Dup as u8
&& self.0[1] == OpCode::Hash160 as u8
&& self.0[2] == 0x14
&& self.0[23] == OpCode::EqualVerify as u8
&& self.0[24] == OpCode::CheckSig as u8
&& self.0[0..3] == [OpCode::Dup as u8, OpCode::Hash160 as u8, 0x14]
&& self.0[23..25] == [OpCode::EqualVerify as u8, OpCode::CheckSig as u8]
{
let mut hash = [0; 20];
hash.copy_from_slice(&self.0[3..23]);
Some(TransparentAddress::PublicKey(hash))
} else if self.0.len() == 23
&& self.0[0] == OpCode::Hash160 as u8
&& self.0[1] == 0x14
&& self.0[0..2] == [OpCode::Hash160 as u8, 0x14]
&& self.0[22] == OpCode::Equal as u8
{
let mut hash = [0; 20];
@ -96,7 +92,7 @@ impl Shl<&[u8]> for Script {
}
/// A transparent address corresponding to either a public key or a `Script`.
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, PartialOrd, Hash, Clone)]
pub enum TransparentAddress {
PublicKey([u8; 20]),
Script([u8; 20]),
@ -123,6 +119,19 @@ impl TransparentAddress {
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::prelude::{any, prop_compose};
use super::TransparentAddress;
prop_compose! {
pub fn arb_transparent_addr()(v in proptest::array::uniform20(any::<u8>())) -> TransparentAddress {
TransparentAddress::PublicKey(v)
}
}
}
#[cfg(test)]
mod tests {
use super::{OpCode, Script, TransparentAddress};

View File

@ -10,6 +10,7 @@
pub mod block;
pub mod consensus;
pub mod constants;
pub mod extensions;
pub mod group_hash;
pub mod keys;
pub mod legacy;

View File

@ -1,7 +1,7 @@
//! Implementation of in-band secret distribution for Zcash transactions.
use crate::{
consensus::{self, NetworkUpgrade, ZIP212_GRACE_PERIOD},
consensus::{self, BlockHeight, NetworkUpgrade::Canopy, ZIP212_GRACE_PERIOD},
primitives::{Diversifier, Note, PaymentAddress, Rseed},
};
use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams};
@ -142,7 +142,7 @@ pub fn sapling_ka_agree(esk: &jubjub::Fr, pk_d: &jubjub::ExtendedPoint) -> jubju
/// Sapling KDF for note encryption.
///
/// Implements section 5.4.4.4 of the Zcash Protocol Specification.
fn kdf_sapling(dhsecret: jubjub::SubgroupPoint, epk: &jubjub::SubgroupPoint) -> Blake2bHash {
fn kdf_sapling(dhsecret: jubjub::SubgroupPoint, epk: &jubjub::ExtendedPoint) -> Blake2bHash {
Blake2bParams::new()
.hash_length(32)
.personal(KDF_SAPLING_PERSONALIZATION)
@ -174,7 +174,7 @@ pub fn prf_ock(
ovk: &OutgoingViewingKey,
cv: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar,
epk: &jubjub::SubgroupPoint,
epk: &jubjub::ExtendedPoint,
) -> OutgoingCipherKey {
OutgoingCipherKey(
Blake2bParams::new()
@ -287,7 +287,7 @@ impl<R: RngCore + CryptoRng> SaplingNoteEncryption<R> {
/// Generates `encCiphertext` for this note.
pub fn encrypt_note_plaintext(&self) -> [u8; ENC_CIPHERTEXT_SIZE] {
let shared_secret = sapling_ka_agree(&self.esk, self.to.pk_d().into());
let key = kdf_sapling(shared_secret, &self.epk);
let key = kdf_sapling(shared_secret, &self.epk.into());
// Note plaintext encoding is defined in section 5.5 of the Zcash Protocol
// Specification.
@ -328,7 +328,7 @@ impl<R: RngCore + CryptoRng> SaplingNoteEncryption<R> {
cmu: &bls12_381::Scalar,
) -> [u8; OUT_CIPHERTEXT_SIZE] {
let (ock, input) = if let Some(ovk) = &self.ovk {
let ock = prf_ock(ovk, &cv, &cmu, &self.epk);
let ock = prf_ock(ovk, &cv, &cmu, &self.epk.into());
let mut input = [0u8; OUT_PLAINTEXT_SIZE];
input[0..32].copy_from_slice(&self.note.pk_d.to_bytes());
@ -359,14 +359,15 @@ impl<R: RngCore + CryptoRng> SaplingNoteEncryption<R> {
}
fn parse_note_plaintext_without_memo<P: consensus::Parameters>(
height: u32,
params: &P,
height: BlockHeight,
ivk: &jubjub::Fr,
epk: &jubjub::SubgroupPoint,
epk: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar,
plaintext: &[u8],
) -> Option<(Note, PaymentAddress)> {
// Check note plaintext version
if !plaintext_version_is_valid::<P>(height, plaintext[0]) {
if !plaintext_version_is_valid(params, height, plaintext[0]) {
return None;
}
@ -398,7 +399,8 @@ fn parse_note_plaintext_without_memo<P: consensus::Parameters>(
}
if let Some(derived_esk) = note.derive_esk() {
if (note.g_d * derived_esk) != *epk {
// This enforces that epk is a jubjub::SubgroupPoint.
if (note.g_d * derived_esk).to_bytes() != epk.to_bytes() {
return None;
}
}
@ -406,11 +408,14 @@ fn parse_note_plaintext_without_memo<P: consensus::Parameters>(
Some((note, to))
}
pub fn plaintext_version_is_valid<P: consensus::Parameters>(height: u32, leadbyte: u8) -> bool {
if P::is_nu_active(NetworkUpgrade::Canopy, height) {
let grace_period_end_height = P::activation_height(NetworkUpgrade::Canopy)
.expect("Should have Canopy activation height")
+ ZIP212_GRACE_PERIOD;
pub fn plaintext_version_is_valid<P: consensus::Parameters>(
params: &P,
height: BlockHeight,
leadbyte: u8,
) -> bool {
if params.is_nu_active(Canopy, height) {
let grace_period_end_height =
params.activation_height(Canopy).unwrap() + ZIP212_GRACE_PERIOD;
if height < grace_period_end_height && leadbyte != 0x01 && leadbyte != 0x02 {
// non-{0x01,0x02} received after Canopy activation and before grace period has elapsed
@ -435,15 +440,16 @@ pub fn plaintext_version_is_valid<P: consensus::Parameters>(height: u32, leadbyt
///
/// Implements section 4.17.2 of the Zcash Protocol Specification.
pub fn try_sapling_note_decryption<P: consensus::Parameters>(
height: u32,
params: &P,
height: BlockHeight,
ivk: &jubjub::Fr,
epk: &jubjub::SubgroupPoint,
epk: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar,
enc_ciphertext: &[u8],
) -> Option<(Note, PaymentAddress, Memo)> {
assert_eq!(enc_ciphertext.len(), ENC_CIPHERTEXT_SIZE);
let shared_secret = sapling_ka_agree(ivk, epk.into());
let shared_secret = sapling_ka_agree(ivk, &epk);
let key = kdf_sapling(shared_secret, &epk);
let mut plaintext = [0; ENC_CIPHERTEXT_SIZE];
@ -460,7 +466,7 @@ pub fn try_sapling_note_decryption<P: consensus::Parameters>(
NOTE_PLAINTEXT_SIZE
);
let (note, to) = parse_note_plaintext_without_memo::<P>(height, ivk, epk, cmu, &plaintext)?;
let (note, to) = parse_note_plaintext_without_memo(params, height, ivk, epk, cmu, &plaintext)?;
let mut memo = [0u8; 512];
memo.copy_from_slice(&plaintext[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]);
@ -478,15 +484,16 @@ pub fn try_sapling_note_decryption<P: consensus::Parameters>(
///
/// [`ZIP 307`]: https://zips.z.cash/zip-0307
pub fn try_sapling_compact_note_decryption<P: consensus::Parameters>(
height: u32,
params: &P,
height: BlockHeight,
ivk: &jubjub::Fr,
epk: &jubjub::SubgroupPoint,
epk: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar,
enc_ciphertext: &[u8],
) -> Option<(Note, PaymentAddress)> {
assert_eq!(enc_ciphertext.len(), COMPACT_NOTE_SIZE);
let shared_secret = sapling_ka_agree(ivk, epk.into());
let shared_secret = sapling_ka_agree(ivk, epk);
let key = kdf_sapling(shared_secret, &epk);
// Start from block 1 to skip over Poly1305 keying output
@ -494,7 +501,7 @@ pub fn try_sapling_compact_note_decryption<P: consensus::Parameters>(
plaintext.copy_from_slice(&enc_ciphertext);
ChaCha20Ietf::xor(key.as_bytes(), &[0u8; 12], 1, &mut plaintext);
parse_note_plaintext_without_memo::<P>(height, ivk, epk, cmu, &plaintext)
parse_note_plaintext_without_memo(params, height, ivk, epk, cmu, &plaintext)
}
/// Recovery of the full note plaintext by the sender.
@ -506,10 +513,11 @@ pub fn try_sapling_compact_note_decryption<P: consensus::Parameters>(
/// Implements part of section 4.17.3 of the Zcash Protocol Specification.
/// For decryption using a Full Viewing Key see [`try_sapling_output_recovery`].
pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
height: u32,
params: &P,
height: BlockHeight,
ock: &OutgoingCipherKey,
cmu: &bls12_381::Scalar,
epk: &jubjub::SubgroupPoint,
epk: &jubjub::ExtendedPoint,
enc_ciphertext: &[u8],
out_ciphertext: &[u8],
) -> Option<(Note, PaymentAddress, Memo)> {
@ -558,7 +566,7 @@ pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
);
// Check note plaintext version
if !plaintext_version_is_valid::<P>(height, plaintext[0]) {
if !plaintext_version_is_valid(params, height, plaintext[0]) {
return None;
}
@ -582,7 +590,7 @@ pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
memo.copy_from_slice(&plaintext[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]);
let diversifier = Diversifier(d);
if diversifier.g_d()? * esk != *epk {
if (diversifier.g_d()? * esk).to_bytes() != epk.to_bytes() {
// Published epk doesn't match calculated epk
return None;
}
@ -612,15 +620,17 @@ pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
///
/// Implements section 4.17.3 of the Zcash Protocol Specification.
pub fn try_sapling_output_recovery<P: consensus::Parameters>(
height: u32,
params: &P,
height: BlockHeight,
ovk: &OutgoingViewingKey,
cv: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar,
epk: &jubjub::SubgroupPoint,
epk: &jubjub::ExtendedPoint,
enc_ciphertext: &[u8],
out_ciphertext: &[u8],
) -> Option<(Note, PaymentAddress, Memo)> {
try_sapling_output_recovery_with_ock::<P>(
params,
height,
&prf_ock(&ovk, &cv, &cmu, &epk),
cmu,
@ -648,11 +658,12 @@ mod tests {
COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE, OUT_CIPHERTEXT_SIZE,
OUT_PLAINTEXT_SIZE,
};
use crate::{
consensus::{
NetworkUpgrade,
BlockHeight,
NetworkUpgrade::{Canopy, Sapling},
Parameters, TestNetwork, ZIP212_GRACE_PERIOD,
Parameters, TEST_NETWORK, ZIP212_GRACE_PERIOD,
},
keys::OutgoingViewingKey,
primitives::{Diversifier, PaymentAddress, Rseed, ValueCommitment},
@ -776,7 +787,7 @@ mod tests {
}
fn random_enc_ciphertext<R: RngCore + CryptoRng>(
height: u32,
height: BlockHeight,
mut rng: &mut R,
) -> (
OutgoingViewingKey,
@ -784,7 +795,7 @@ mod tests {
jubjub::Fr,
jubjub::ExtendedPoint,
bls12_381::Scalar,
jubjub::SubgroupPoint,
jubjub::ExtendedPoint,
[u8; ENC_CIPHERTEXT_SIZE],
[u8; OUT_CIPHERTEXT_SIZE],
) {
@ -793,7 +804,8 @@ mod tests {
let (ovk, ock, ivk, cv, cmu, epk, enc_ciphertext, out_ciphertext) =
random_enc_ciphertext_with(height, ivk, rng);
assert!(try_sapling_note_decryption::<TestNetwork>(
assert!(try_sapling_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -801,7 +813,8 @@ mod tests {
&enc_ciphertext
)
.is_some());
assert!(try_sapling_compact_note_decryption::<TestNetwork>(
assert!(try_sapling_compact_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -810,7 +823,8 @@ mod tests {
)
.is_some());
let ovk_output_recovery = try_sapling_output_recovery::<TestNetwork>(
let ovk_output_recovery = try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&cv,
@ -819,7 +833,9 @@ mod tests {
&enc_ciphertext,
&out_ciphertext,
);
let ock_output_recovery = try_sapling_output_recovery_with_ock::<TestNetwork>(
let ock_output_recovery = try_sapling_output_recovery_with_ock(
&TEST_NETWORK,
height,
&ock,
&cmu,
@ -835,7 +851,7 @@ mod tests {
}
fn random_enc_ciphertext_with<R: RngCore + CryptoRng>(
height: u32,
height: BlockHeight,
ivk: jubjub::Fr,
mut rng: &mut R,
) -> (
@ -844,7 +860,7 @@ mod tests {
jubjub::Fr,
jubjub::ExtendedPoint,
bls12_381::Scalar,
jubjub::SubgroupPoint,
jubjub::ExtendedPoint,
[u8; ENC_CIPHERTEXT_SIZE],
[u8; OUT_CIPHERTEXT_SIZE],
) {
@ -860,14 +876,14 @@ mod tests {
};
let cv = value_commitment.commitment().into();
let rseed = generate_random_rseed::<TestNetwork, R>(height, &mut rng);
let rseed = generate_random_rseed(&TEST_NETWORK, height, &mut rng);
let note = pa.create_note(value, rseed).unwrap();
let cmu = note.cmu();
let ovk = OutgoingViewingKey([0; 32]);
let mut ne = SaplingNoteEncryption::new(Some(ovk), note, pa, Memo([0; 512]), &mut rng);
let epk = ne.epk().clone();
let epk = ne.epk().clone().into();
let enc_ciphertext = ne.encrypt_note_plaintext();
let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu);
let ock = prf_ock(&ovk, &cv, &cmu, &epk);
@ -879,7 +895,7 @@ mod tests {
ovk: &OutgoingViewingKey,
cv: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar,
epk: &jubjub::SubgroupPoint,
epk: &jubjub::ExtendedPoint,
enc_ciphertext: &mut [u8; ENC_CIPHERTEXT_SIZE],
out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE],
modify_plaintext: impl Fn(&mut [u8; NOTE_PLAINTEXT_SIZE]),
@ -962,15 +978,16 @@ mod tests {
fn decryption_with_invalid_ivk() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
let (_, _, _, _, cmu, epk, enc_ciphertext, _) = random_enc_ciphertext(height, &mut rng);
assert_eq!(
try_sapling_note_decryption::<TestNetwork>(
try_sapling_note_decryption(
&TEST_NETWORK,
height,
&jubjub::Fr::random(&mut rng),
&epk,
@ -986,18 +1003,19 @@ mod tests {
fn decryption_with_invalid_epk() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
let (_, _, ivk, _, cmu, _, enc_ciphertext, _) = random_enc_ciphertext(height, &mut rng);
assert_eq!(
try_sapling_note_decryption::<TestNetwork>(
try_sapling_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&jubjub::SubgroupPoint::random(&mut rng),
&jubjub::ExtendedPoint::random(&mut rng),
&cmu,
&enc_ciphertext
),
@ -1010,15 +1028,16 @@ mod tests {
fn decryption_with_invalid_cmu() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
let (_, _, ivk, _, _, epk, enc_ciphertext, _) = random_enc_ciphertext(height, &mut rng);
assert_eq!(
try_sapling_note_decryption::<TestNetwork>(
try_sapling_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -1034,8 +1053,8 @@ mod tests {
fn decryption_with_invalid_tag() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1044,7 +1063,8 @@ mod tests {
enc_ciphertext[ENC_CIPHERTEXT_SIZE - 1] ^= 0xff;
assert_eq!(
try_sapling_note_decryption::<TestNetwork>(
try_sapling_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -1059,7 +1079,7 @@ mod tests {
#[test]
fn decryption_with_invalid_version_byte() {
let mut rng = OsRng;
let canopy_activation_height = TestNetwork::activation_height(Canopy).unwrap();
let canopy_activation_height = TEST_NETWORK.activation_height(Canopy).unwrap();
let heights = [
canopy_activation_height - 1,
canopy_activation_height,
@ -1081,7 +1101,8 @@ mod tests {
|pt| pt[0] = leadbyte,
);
assert_eq!(
try_sapling_note_decryption::<TestNetwork>(
try_sapling_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -1097,8 +1118,8 @@ mod tests {
fn decryption_with_invalid_diversifier() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1115,7 +1136,8 @@ mod tests {
|pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0),
);
assert_eq!(
try_sapling_note_decryption::<TestNetwork>(
try_sapling_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -1131,8 +1153,8 @@ mod tests {
fn decryption_with_incorrect_diversifier() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1148,8 +1170,10 @@ mod tests {
&out_ciphertext,
|pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0),
);
assert_eq!(
try_sapling_note_decryption::<TestNetwork>(
try_sapling_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -1165,15 +1189,16 @@ mod tests {
fn compact_decryption_with_invalid_ivk() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
let (_, _, _, _, cmu, epk, enc_ciphertext, _) = random_enc_ciphertext(height, &mut rng);
assert_eq!(
try_sapling_compact_note_decryption::<TestNetwork>(
try_sapling_compact_note_decryption(
&TEST_NETWORK,
height,
&jubjub::Fr::random(&mut rng),
&epk,
@ -1189,18 +1214,19 @@ mod tests {
fn compact_decryption_with_invalid_epk() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
let (_, _, ivk, _, cmu, _, enc_ciphertext, _) = random_enc_ciphertext(height, &mut rng);
assert_eq!(
try_sapling_compact_note_decryption::<TestNetwork>(
try_sapling_compact_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&jubjub::SubgroupPoint::random(&mut rng),
&jubjub::ExtendedPoint::random(&mut rng),
&cmu,
&enc_ciphertext[..COMPACT_NOTE_SIZE]
),
@ -1213,15 +1239,16 @@ mod tests {
fn compact_decryption_with_invalid_cmu() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
let (_, _, ivk, _, _, epk, enc_ciphertext, _) = random_enc_ciphertext(height, &mut rng);
assert_eq!(
try_sapling_compact_note_decryption::<TestNetwork>(
try_sapling_compact_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -1236,7 +1263,7 @@ mod tests {
#[test]
fn compact_decryption_with_invalid_version_byte() {
let mut rng = OsRng;
let canopy_activation_height = TestNetwork::activation_height(Canopy).unwrap();
let canopy_activation_height = TEST_NETWORK.activation_height(Canopy).unwrap();
let heights = [
canopy_activation_height - 1,
canopy_activation_height,
@ -1258,7 +1285,8 @@ mod tests {
|pt| pt[0] = leadbyte,
);
assert_eq!(
try_sapling_compact_note_decryption::<TestNetwork>(
try_sapling_compact_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -1274,8 +1302,8 @@ mod tests {
fn compact_decryption_with_invalid_diversifier() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1292,7 +1320,8 @@ mod tests {
|pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0),
);
assert_eq!(
try_sapling_compact_note_decryption::<TestNetwork>(
try_sapling_compact_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -1308,8 +1337,8 @@ mod tests {
fn compact_decryption_with_incorrect_diversifier() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1326,7 +1355,8 @@ mod tests {
|pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0),
);
assert_eq!(
try_sapling_compact_note_decryption::<TestNetwork>(
try_sapling_compact_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -1342,8 +1372,8 @@ mod tests {
fn recovery_with_invalid_ovk() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1352,7 +1382,8 @@ mod tests {
ovk.0[0] ^= 0xff;
assert_eq!(
try_sapling_output_recovery::<TestNetwork>(
try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&cv,
@ -1370,8 +1401,8 @@ mod tests {
fn recovery_with_invalid_ock() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1379,7 +1410,8 @@ mod tests {
random_enc_ciphertext(height, &mut rng);
assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>(
try_sapling_output_recovery_with_ock(
&TEST_NETWORK,
height,
&OutgoingCipherKey([0u8; 32]),
&cmu,
@ -1396,8 +1428,8 @@ mod tests {
fn recovery_with_invalid_cv() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1405,7 +1437,8 @@ mod tests {
random_enc_ciphertext(height, &mut rng);
assert_eq!(
try_sapling_output_recovery::<TestNetwork>(
try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&jubjub::ExtendedPoint::random(&mut rng),
@ -1423,8 +1456,8 @@ mod tests {
fn recovery_with_invalid_cmu() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1432,7 +1465,8 @@ mod tests {
random_enc_ciphertext(height, &mut rng);
assert_eq!(
try_sapling_output_recovery::<TestNetwork>(
try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&cv,
@ -1443,8 +1477,10 @@ mod tests {
),
None
);
assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>(
try_sapling_output_recovery_with_ock(
&TEST_NETWORK,
height,
&ock,
&bls12_381::Scalar::random(&mut rng),
@ -1461,8 +1497,8 @@ mod tests {
fn recovery_with_invalid_epk() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1470,23 +1506,26 @@ mod tests {
random_enc_ciphertext(height, &mut rng);
assert_eq!(
try_sapling_output_recovery::<TestNetwork>(
try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&cv,
&cmu,
&jubjub::SubgroupPoint::random(&mut rng),
&jubjub::ExtendedPoint::random(&mut rng),
&enc_ciphertext,
&out_ciphertext
),
None
);
assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>(
try_sapling_output_recovery_with_ock(
&TEST_NETWORK,
height,
&ock,
&cmu,
&jubjub::SubgroupPoint::random(&mut rng),
&jubjub::ExtendedPoint::random(&mut rng),
&enc_ciphertext,
&out_ciphertext
),
@ -1499,8 +1538,8 @@ mod tests {
fn recovery_with_invalid_enc_tag() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1509,7 +1548,8 @@ mod tests {
enc_ciphertext[ENC_CIPHERTEXT_SIZE - 1] ^= 0xff;
assert_eq!(
try_sapling_output_recovery::<TestNetwork>(
try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&cv,
@ -1521,7 +1561,8 @@ mod tests {
None
);
assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>(
try_sapling_output_recovery_with_ock(
&TEST_NETWORK,
height,
&ock,
&cmu,
@ -1538,8 +1579,8 @@ mod tests {
fn recovery_with_invalid_out_tag() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1548,7 +1589,8 @@ mod tests {
out_ciphertext[OUT_CIPHERTEXT_SIZE - 1] ^= 0xff;
assert_eq!(
try_sapling_output_recovery::<TestNetwork>(
try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&cv,
@ -1560,7 +1602,8 @@ mod tests {
None
);
assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>(
try_sapling_output_recovery_with_ock(
&TEST_NETWORK,
height,
&ock,
&cmu,
@ -1576,7 +1619,7 @@ mod tests {
#[test]
fn recovery_with_invalid_version_byte() {
let mut rng = OsRng;
let canopy_activation_height = TestNetwork::activation_height(Canopy).unwrap();
let canopy_activation_height = TEST_NETWORK.activation_height(Canopy).unwrap();
let heights = [
canopy_activation_height - 1,
canopy_activation_height,
@ -1598,7 +1641,8 @@ mod tests {
|pt| pt[0] = leadbyte,
);
assert_eq!(
try_sapling_output_recovery::<TestNetwork>(
try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&cv,
@ -1610,7 +1654,8 @@ mod tests {
None
);
assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>(
try_sapling_output_recovery_with_ock(
&TEST_NETWORK,
height,
&ock,
&cmu,
@ -1627,8 +1672,8 @@ mod tests {
fn recovery_with_invalid_diversifier() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1645,7 +1690,8 @@ mod tests {
|pt| pt[1..12].copy_from_slice(&find_invalid_diversifier().0),
);
assert_eq!(
try_sapling_output_recovery::<TestNetwork>(
try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&cv,
@ -1657,7 +1703,8 @@ mod tests {
None
);
assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>(
try_sapling_output_recovery_with_ock(
&TEST_NETWORK,
height,
&ock,
&cmu,
@ -1674,8 +1721,8 @@ mod tests {
fn recovery_with_incorrect_diversifier() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1692,7 +1739,8 @@ mod tests {
|pt| pt[1..12].copy_from_slice(&find_valid_diversifier().0),
);
assert_eq!(
try_sapling_output_recovery::<TestNetwork>(
try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&cv,
@ -1704,7 +1752,8 @@ mod tests {
None
);
assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>(
try_sapling_output_recovery_with_ock(
&TEST_NETWORK,
height,
&ock,
&cmu,
@ -1721,8 +1770,8 @@ mod tests {
fn recovery_with_invalid_pk_d() {
let mut rng = OsRng;
let heights = [
TestNetwork::activation_height(Sapling).unwrap(),
TestNetwork::activation_height(Canopy).unwrap(),
TEST_NETWORK.activation_height(Sapling).unwrap(),
TEST_NETWORK.activation_height(Canopy).unwrap(),
];
for &height in heights.iter() {
@ -1731,7 +1780,8 @@ mod tests {
random_enc_ciphertext_with(height, ivk, &mut rng);
assert_eq!(
try_sapling_output_recovery::<TestNetwork>(
try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&cv,
@ -1743,7 +1793,8 @@ mod tests {
None
);
assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>(
try_sapling_output_recovery_with_ock(
&TEST_NETWORK,
height,
&ock,
&cmu,
@ -1778,8 +1829,7 @@ mod tests {
};
}
let height = TestNetwork::activation_height(NetworkUpgrade::Sapling)
.expect("Should have Sapling activation height");
let height = TEST_NETWORK.activation_height(Sapling).unwrap();
for tv in test_vectors {
//
@ -1792,7 +1842,7 @@ mod tests {
let cv = read_point!(tv.cv);
let cmu = read_bls12_381_scalar!(tv.cmu);
let esk = read_jubjub_scalar!(tv.esk);
let epk = read_point!(tv.epk).into_subgroup().unwrap();
let epk = read_point!(tv.epk);
//
// Test the individual components
@ -1817,7 +1867,7 @@ mod tests {
// (Tested first because it only requires immutable references.)
//
match try_sapling_note_decryption::<TestNetwork>(height, &ivk, &epk, &cmu, &tv.c_enc) {
match try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &epk, &cmu, &tv.c_enc) {
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
assert_eq!(decrypted_note, note);
assert_eq!(decrypted_to, to);
@ -1826,7 +1876,8 @@ mod tests {
None => panic!("Note decryption failed"),
}
match try_sapling_compact_note_decryption::<TestNetwork>(
match try_sapling_compact_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&epk,
@ -1840,8 +1891,15 @@ mod tests {
None => panic!("Compact note decryption failed"),
}
match try_sapling_output_recovery::<TestNetwork>(
height, &ovk, &cv, &cmu, &epk, &tv.c_enc, &tv.c_out,
match try_sapling_output_recovery(
&TEST_NETWORK,
height,
&ovk,
&cv,
&cmu,
&epk,
&tv.c_enc,
&tv.c_out,
) {
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
assert_eq!(decrypted_note, note);
@ -1858,7 +1916,7 @@ mod tests {
let mut ne = SaplingNoteEncryption::new(Some(ovk), note, to, Memo(tv.memo), OsRng);
// Swap in the ephemeral keypair from the test vectors
ne.esk = esk;
ne.epk = epk;
ne.epk = epk.into_subgroup().unwrap();
assert_eq!(&ne.encrypt_note_plaintext()[..], &tv.c_enc[..]);
assert_eq!(&ne.encrypt_outgoing_plaintext(&cv, &cmu)[..], &tv.c_out[..]);

View File

@ -60,7 +60,7 @@ pub trait TxProver {
}
#[cfg(test)]
pub(crate) mod mock {
pub mod mock {
use ff::Field;
use rand_core::OsRng;

View File

@ -36,7 +36,7 @@ pub struct Signature {
pub struct PrivateKey(pub jubjub::Fr);
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct PublicKey(pub ExtendedPoint);
impl Signature {

View File

@ -3,10 +3,10 @@ use std::io::{self, Read, Write};
const MAX_SIZE: usize = 0x02000000;
struct CompactSize;
pub(crate) struct CompactSize;
impl CompactSize {
fn read<R: Read>(mut reader: R) -> io::Result<usize> {
pub(crate) fn read<R: Read>(mut reader: R) -> io::Result<usize> {
let flag = reader.read_u8()?;
match if flag < 253 {
Ok(flag as usize)
@ -43,7 +43,7 @@ impl CompactSize {
}
}
fn write<W: Write>(mut writer: W, size: usize) -> io::Result<()> {
pub(crate) fn write<W: Write>(mut writer: W, size: usize) -> io::Result<()> {
match size {
s if s < 253 => writer.write_u8(s as u8),
s if s <= 0xFFFF => {

View File

@ -1,34 +1,37 @@
//! Structs for building transactions.
use crate::primitives::{Diversifier, Note, PaymentAddress};
use crate::zip32::ExtendedSpendingKey;
use ff::Field;
use rand::{rngs::OsRng, seq::SliceRandom, CryptoRng, RngCore};
use std::boxed::Box;
use std::error;
use std::fmt;
use std::marker::PhantomData;
use ff::Field;
use rand::{rngs::OsRng, seq::SliceRandom, CryptoRng, RngCore};
use crate::{
consensus,
consensus::{self, BlockHeight},
extensions::transparent::{self as tze, ExtensionTxBuilder, ToPayload},
keys::OutgoingViewingKey,
legacy::TransparentAddress,
merkle_tree::MerklePath,
note_encryption::{Memo, SaplingNoteEncryption},
primitives::{Diversifier, Note, PaymentAddress},
prover::TxProver,
redjubjub::PrivateKey,
sapling::{spend_sig, Node},
transaction::{
components::{amount::DEFAULT_FEE, Amount, OutputDescription, SpendDescription, TxOut},
signature_hash_data, Transaction, TransactionData, SIGHASH_ALL,
components::{
amount::Amount, amount::DEFAULT_FEE, OutPoint, OutputDescription, SpendDescription,
TxOut, TzeIn, TzeOut,
},
signature_hash_data, SignableInput, Transaction, TransactionData, SIGHASH_ALL,
},
util::generate_random_rseed,
zip32::ExtendedSpendingKey,
};
#[cfg(feature = "transparent-inputs")]
use crate::{
legacy::Script,
transaction::components::{OutPoint, TxIn},
};
use crate::{legacy::Script, transaction::components::TxIn};
const DEFAULT_TX_EXPIRY_DELTA: u32 = 20;
@ -45,6 +48,7 @@ pub enum Error {
InvalidAmount,
NoChangeAddress,
SpendProof,
TzeWitnessModeMismatch(u32, u32),
}
impl fmt::Display for Error {
@ -61,6 +65,8 @@ impl fmt::Display for Error {
Error::InvalidAmount => write!(f, "Invalid amount"),
Error::NoChangeAddress => write!(f, "No change address specified or discoverable"),
Error::SpendProof => write!(f, "Failed to create Sapling spend proof"),
Error::TzeWitnessModeMismatch(expected, actual) =>
write!(f, "TZE witness builder returned a mode that did not match the mode with which the input was initially constructed: expected = {:?}, actual = {:?}", expected, actual),
}
}
}
@ -85,7 +91,8 @@ pub struct SaplingOutput {
impl SaplingOutput {
pub fn new<R: RngCore + CryptoRng, P: consensus::Parameters>(
height: u32,
params: &P,
height: BlockHeight,
rng: &mut R,
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress,
@ -100,7 +107,7 @@ impl SaplingOutput {
return Err(Error::InvalidAmount);
}
let rseed = generate_random_rseed::<P, R>(height, rng);
let rseed = generate_random_rseed(params, height, rng);
let note = Note {
g_d,
@ -186,17 +193,14 @@ struct TransparentInputs;
impl TransparentInputs {
#[cfg(feature = "transparent-inputs")]
fn push(
&mut self,
mtx: &mut TransactionData,
sk: secp256k1::SecretKey,
utxo: OutPoint,
coin: TxOut,
) -> Result<(), Error> {
fn push(&mut self, sk: secp256k1::SecretKey, coin: TxOut) -> Result<(), Error> {
if coin.value.is_negative() {
return Err(Error::InvalidAmount);
}
// Ensure that the RIPEMD-160 digest of the public key associated with the
// provided secret key matches that of the address to which the provided
// output may be spent.
let pubkey = secp256k1::PublicKey::from_secret_key(&self.secp, &sk).serialize();
match coin.script_pubkey.address() {
Some(TransparentAddress::PublicKey(hash)) => {
@ -210,7 +214,6 @@ impl TransparentInputs {
_ => return Err(Error::InvalidAddress),
}
mtx.vin.push(TxIn::new(utxo));
self.inputs.push(TransparentInputInfo { sk, pubkey, coin });
Ok(())
@ -243,7 +246,7 @@ impl TransparentInputs {
mtx,
consensus_branch_id,
SIGHASH_ALL,
Some((i, &info.coin.script_pubkey, info.coin.value)),
SignableInput::transparent(i, &info.coin.script_pubkey, info.coin.value),
));
let msg = secp256k1::Message::from_slice(&sighash).expect("32 bytes");
@ -262,6 +265,31 @@ impl TransparentInputs {
fn apply_signatures(&self, _: &mut TransactionData, _: consensus::BranchId) {}
}
struct TzeInputInfo<'a, BuildCtx> {
prevout: TzeOut,
builder: Box<dyn FnOnce(&BuildCtx) -> Result<(u32, Vec<u8>), Error> + 'a>,
}
struct TzeInputs<'a, BuildCtx> {
builders: Vec<TzeInputInfo<'a, BuildCtx>>,
}
impl<'a, BuildCtx> TzeInputs<'a, BuildCtx> {
fn default() -> Self {
TzeInputs { builders: vec![] }
}
fn push<WBuilder, W: ToPayload>(&mut self, tzeout: TzeOut, builder: WBuilder)
where
WBuilder: 'a + FnOnce(&BuildCtx) -> Result<W, Error>,
{
self.builders.push(TzeInputInfo {
prevout: tzeout,
builder: Box::new(move |ctx| builder(&ctx).map(|x| x.to_payload())),
});
}
}
/// Metadata about a transaction created by a [`Builder`].
#[derive(Debug, PartialEq)]
pub struct TransactionMetadata {
@ -301,20 +329,22 @@ impl TransactionMetadata {
}
/// Generates a [`Transaction`] from its inputs and outputs.
pub struct Builder<P: consensus::Parameters, R: RngCore + CryptoRng> {
pub struct Builder<'a, P: consensus::Parameters, R: RngCore + CryptoRng> {
params: P,
rng: R,
height: u32,
height: BlockHeight,
mtx: TransactionData,
fee: Amount,
anchor: Option<bls12_381::Scalar>,
spends: Vec<SpendDescriptionInfo>,
outputs: Vec<SaplingOutput>,
transparent_inputs: TransparentInputs,
tze_inputs: TzeInputs<'a, TransactionData>,
change_address: Option<(OutgoingViewingKey, PaymentAddress)>,
phantom: PhantomData<P>,
}
impl<P: consensus::Parameters> Builder<P, OsRng> {
impl<'a, P: consensus::Parameters> Builder<'a, P, OsRng> {
/// Creates a new `Builder` targeted for inclusion in the block with the given height,
/// using default values for general transaction fields and the default OS random.
///
@ -324,12 +354,30 @@ impl<P: consensus::Parameters> Builder<P, OsRng> {
/// expiry delta (20 blocks).
///
/// The fee will be set to the default fee (0.0001 ZEC).
pub fn new(height: u32) -> Self {
Builder::new_with_rng(height, OsRng)
pub fn new(params: P, height: BlockHeight) -> Self {
Builder::new_with_rng(params, height, OsRng)
}
/// Creates a new `Builder` targeted for inclusion in the block with the given height,
/// using default values for general transaction fields and the default OS random,
/// and the `ZFUTURE_TX_VERSION` and `ZFUTURE_VERSION_GROUP_ID` version identifiers.
///
/// # Default values
///
/// The expiry height will be set to the given height plus the default transaction
/// expiry delta (20 blocks).
///
/// The fee will be set to the default fee (0.0001 ZEC).
///
/// The transaction will be constructed and serialized according to the
/// NetworkUpgrade::ZFuture rules. This is intended only for use in
/// integration testing of new features.
pub fn new_zfuture(params: P, height: BlockHeight) -> Self {
Builder::new_with_rng_zfuture(params, height, OsRng)
}
}
impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
/// Creates a new `Builder` targeted for inclusion in the block with the given height
/// and randomness source, using default values for general transaction fields.
///
@ -339,11 +387,39 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
/// expiry delta (20 blocks).
///
/// The fee will be set to the default fee (0.0001 ZEC).
pub fn new_with_rng(height: u32, rng: R) -> Builder<P, R> {
let mut mtx = TransactionData::new();
pub fn new_with_rng(params: P, height: BlockHeight, rng: R) -> Builder<'a, P, R> {
Self::new_with_mtx(params, height, rng, TransactionData::new())
}
/// Creates a new `Builder` targeted for inclusion in the block with the given height,
/// and randomness source, using default values for general transaction fields
/// and the `ZFUTURE_TX_VERSION` and `ZFUTURE_VERSION_GROUP_ID` version identifiers.
///
/// # Default values
///
/// The expiry height will be set to the given height plus the default transaction
/// expiry delta (20 blocks).
///
/// The fee will be set to the default fee (0.0001 ZEC).
///
/// The transaction will be constructed and serialized according to the
/// NetworkUpgrade::ZFuture rules. This is intended only for use in
/// integration testing of new features.
pub fn new_with_rng_zfuture(params: P, height: BlockHeight, rng: R) -> Builder<'a, P, R> {
Self::new_with_mtx(params, height, rng, TransactionData::zfuture())
}
/// Common utility function for builder construction.
fn new_with_mtx(
params: P,
height: BlockHeight,
rng: R,
mut mtx: TransactionData,
) -> Builder<'a, P, R> {
mtx.expiry_height = height + DEFAULT_TX_EXPIRY_DELTA;
Builder {
params,
rng,
height,
mtx,
@ -352,6 +428,7 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
spends: vec![],
outputs: vec![],
transparent_inputs: TransparentInputs::default(),
tze_inputs: TzeInputs::default(),
change_address: None,
phantom: PhantomData,
}
@ -402,7 +479,15 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
value: Amount,
memo: Option<Memo>,
) -> Result<(), Error> {
let output = SaplingOutput::new::<R, P>(self.height, &mut self.rng, ovk, to, value, memo)?;
let output = SaplingOutput::new(
&self.params,
self.height,
&mut self.rng,
ovk,
to,
value,
memo,
)?;
self.mtx.value_balance -= value;
@ -420,7 +505,9 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
utxo: OutPoint,
coin: TxOut,
) -> Result<(), Error> {
self.transparent_inputs.push(&mut self.mtx, sk, utxo, coin)
self.transparent_inputs.push(sk, coin)?;
self.mtx.vin.push(TxIn::new(utxo));
Ok(())
}
/// Adds a transparent address to send funds to.
@ -471,12 +558,20 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
// Valid change
let change = self.mtx.value_balance - self.fee + self.transparent_inputs.value_sum()
- self.mtx.vout.iter().map(|vo| vo.value).sum::<Amount>()
+ self
.tze_inputs
.builders
.iter()
.map(|ein| ein.prevout.value)
.sum::<Amount>()
- self
.mtx
.vout
.tze_outputs
.iter()
.map(|output| output.value)
.map(|tzo| tzo.value)
.sum::<Amount>();
if change.is_negative() {
return Err(Error::ChangeIsNegative(change));
}
@ -613,7 +708,7 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
}
};
let rseed = generate_random_rseed::<P, R>(self.height, &mut self.rng);
let rseed = generate_random_rseed(&self.params, self.height, &mut self.rng);
(
payment_address,
@ -658,7 +753,7 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
}
//
// Signatures
// Signatures -- everything but the signatures must already have been added.
//
let mut sighash = [0u8; 32];
@ -666,7 +761,7 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
&self.mtx,
consensus_branch_id,
SIGHASH_ALL,
None,
SignableInput::Shielded,
));
// Create Sapling spendAuth and binding signatures
@ -680,14 +775,28 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
}
// Add a binding signature if needed
if binding_sig_needed {
self.mtx.binding_sig = Some(
self.mtx.binding_sig = if binding_sig_needed {
Some(
prover
.binding_sig(&mut ctx, self.mtx.value_balance, &sighash)
.map_err(|()| Error::BindingSig)?,
);
.map_err(|_| Error::BindingSig)?,
)
} else {
self.mtx.binding_sig = None;
None
};
// Create TZE input witnesses
for (i, tze_in) in self.tze_inputs.builders.into_iter().enumerate() {
// The witness builder function should have cached/closed over whatever data was necessary for the
// witness to commit to at the time it was added to the transaction builder; here, it then computes those
// commitments.
let (mode, payload) = (tze_in.builder)(&self.mtx)?;
let mut current = self.mtx.tze_inputs.get_mut(i).unwrap();
if mode != current.witness.mode {
return Err(Error::TzeWitnessModeMismatch(current.witness.mode, mode));
}
current.witness.payload = payload;
}
// Transparent signatures
@ -701,16 +810,61 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
}
}
impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a>
for Builder<'a, P, R>
{
type BuildCtx = TransactionData;
type BuildError = Error;
fn add_tze_input<WBuilder, W: ToPayload>(
&mut self,
extension_id: u32,
mode: u32,
(outpoint, prevout): (OutPoint, TzeOut),
witness_builder: WBuilder,
) -> Result<(), Self::BuildError>
where
WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result<W, Self::BuildError>),
{
self.mtx
.tze_inputs
.push(TzeIn::new(outpoint, extension_id, mode));
self.tze_inputs.push(prevout, witness_builder);
Ok(())
}
fn add_tze_output<G: ToPayload>(
&mut self,
extension_id: u32,
value: Amount,
guarded_by: &G,
) -> Result<(), Self::BuildError> {
if value.is_negative() {
return Err(Error::InvalidAmount);
}
let (mode, payload) = guarded_by.to_payload();
self.mtx.tze_outputs.push(TzeOut {
value,
precondition: tze::Precondition {
extension_id,
mode,
payload,
},
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use ff::{Field, PrimeField};
use rand_core::OsRng;
use std::marker::PhantomData;
use super::{Builder, Error};
use crate::{
consensus,
consensus::TestNetwork,
consensus::{self, Parameters, H0, TEST_NETWORK},
legacy::TransparentAddress,
merkle_tree::{CommitmentTree, IncrementalWitness},
primitives::Rseed,
@ -720,6 +874,8 @@ mod tests {
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
};
use super::{Builder, Error, TzeInputs};
#[test]
fn fails_on_negative_output() {
let extsk = ExtendedSpendingKey::master(&[]);
@ -727,7 +883,7 @@ mod tests {
let ovk = extfvk.fvk.ovk;
let to = extfvk.default_address().unwrap().1;
let mut builder = Builder::<TestNetwork, OsRng>::new(0);
let mut builder = Builder::new(TEST_NETWORK, H0);
assert_eq!(
builder.add_sapling_output(Some(ovk), to, Amount::from_i64(-1).unwrap(), None),
Err(Error::InvalidAmount)
@ -736,17 +892,19 @@ mod tests {
#[test]
fn binding_sig_absent_if_no_shielded_spend_or_output() {
use crate::consensus::{NetworkUpgrade, Parameters};
use crate::consensus::NetworkUpgrade;
use crate::transaction::{
builder::{self, TransparentInputs},
TransactionData,
};
let sapling_activation_height =
TestNetwork::activation_height(NetworkUpgrade::Sapling).unwrap();
let sapling_activation_height = TEST_NETWORK
.activation_height(NetworkUpgrade::Sapling)
.unwrap();
// Create a builder with 0 fee, so we can construct t outputs
let mut builder = builder::Builder::<TestNetwork, OsRng> {
let mut builder = builder::Builder {
params: TEST_NETWORK,
rng: OsRng,
height: sapling_activation_height,
mtx: TransactionData::new(),
@ -755,6 +913,7 @@ mod tests {
spends: vec![],
outputs: vec![],
transparent_inputs: TransparentInputs::default(),
tze_inputs: TzeInputs::default(),
change_address: None,
phantom: PhantomData,
};
@ -787,7 +946,7 @@ mod tests {
tree.append(cmu1).unwrap();
let witness1 = IncrementalWitness::from_tree(&tree);
let mut builder = Builder::<TestNetwork, OsRng>::new(0);
let mut builder = Builder::new(TEST_NETWORK, H0);
// Create a tx with a sapling spend. binding_sig should be present
builder
@ -813,7 +972,7 @@ mod tests {
#[test]
fn fails_on_negative_transparent_output() {
let mut builder = Builder::<TestNetwork, OsRng>::new(0);
let mut builder = Builder::new(TEST_NETWORK, H0);
assert_eq!(
builder.add_transparent_output(
&TransparentAddress::PublicKey([0; 20]),
@ -833,7 +992,7 @@ mod tests {
// Fails with no inputs or outputs
// 0.0001 t-ZEC fee
{
let builder = Builder::<TestNetwork, OsRng>::new(0);
let builder = Builder::new(TEST_NETWORK, H0);
assert_eq!(
builder.build(consensus::BranchId::Sapling, &MockTxProver),
Err(Error::ChangeIsNegative(Amount::from_i64(-10000).unwrap()))
@ -847,7 +1006,7 @@ mod tests {
// Fail if there is only a Sapling output
// 0.0005 z-ZEC out, 0.0001 t-ZEC fee
{
let mut builder = Builder::<TestNetwork, OsRng>::new(0);
let mut builder = Builder::new(TEST_NETWORK, H0);
builder
.add_sapling_output(
ovk.clone(),
@ -865,7 +1024,7 @@ mod tests {
// Fail if there is only a transparent output
// 0.0005 t-ZEC out, 0.0001 t-ZEC fee
{
let mut builder = Builder::<TestNetwork, OsRng>::new(0);
let mut builder = Builder::new(TEST_NETWORK, H0);
builder
.add_transparent_output(
&TransparentAddress::PublicKey([0; 20]),
@ -889,7 +1048,7 @@ mod tests {
// Fail if there is insufficient input
// 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.0001 t-ZEC fee, 0.00059999 z-ZEC in
{
let mut builder = Builder::<TestNetwork, OsRng>::new(0);
let mut builder = Builder::new(TEST_NETWORK, H0);
builder
.add_sapling_spend(
extsk.clone(),
@ -932,7 +1091,7 @@ mod tests {
// (Still fails because we are using a MockTxProver which doesn't correctly
// compute bindingSig.)
{
let mut builder = Builder::<TestNetwork, OsRng>::new(0);
let mut builder = Builder::new(TEST_NETWORK, H0);
builder
.add_sapling_spend(
extsk.clone(),

View File

@ -1,12 +1,17 @@
//! Structs representing the components within Zcash transactions.
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use ff::PrimeField;
use group::GroupEncoding;
use std::convert::TryFrom;
use std::io::{self, Read, Write};
use crate::extensions::transparent as tze;
use crate::legacy::Script;
use crate::redjubjub::{PublicKey, Signature};
use crate::serialize::{CompactSize, Vector};
pub mod amount;
pub use self::amount::Amount;
@ -51,7 +56,7 @@ impl OutPoint {
}
}
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
pub struct TxIn {
pub prevout: OutPoint,
pub script_sig: Script,
@ -88,7 +93,7 @@ impl TxIn {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct TxOut {
pub value: Amount,
pub script_pubkey: Script,
@ -116,6 +121,129 @@ impl TxOut {
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TzeIn {
pub prevout: OutPoint,
pub witness: tze::Witness,
}
fn to_io_error(_: std::num::TryFromIntError) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, "value out of range")
}
/// Transaction encoding and decoding functions conforming to ZIP-222
///
/// https://zips.z.cash/zip-0222#encoding-in-transactions
impl TzeIn {
/// Convenience constructor
pub fn new(prevout: OutPoint, extension_id: u32, mode: u32) -> Self {
TzeIn {
prevout,
witness: tze::Witness {
extension_id,
mode,
payload: vec![],
},
}
}
/// Read witness metadata & payload
///
/// Used to decode the encoded form used within a serialized
/// transaction.
pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> {
let prevout = OutPoint::read(&mut reader)?;
let extension_id = CompactSize::read(&mut reader)?;
let mode = CompactSize::read(&mut reader)?;
let payload = Vector::read(&mut reader, |r| r.read_u8())?;
Ok(TzeIn {
prevout,
witness: tze::Witness {
extension_id: u32::try_from(extension_id).map_err(|e| to_io_error(e))?,
mode: u32::try_from(mode).map_err(|e| to_io_error(e))?,
payload,
},
})
}
/// Write without witness data (for signature hashing)
///
/// This is also used as the prefix for the encoded form used
/// within a serialized transaction.
pub fn write_without_witness<W: Write>(&self, mut writer: W) -> io::Result<()> {
self.prevout.write(&mut writer)?;
CompactSize::write(
&mut writer,
usize::try_from(self.witness.extension_id).map_err(|e| to_io_error(e))?,
)?;
CompactSize::write(
&mut writer,
usize::try_from(self.witness.mode).map_err(|e| to_io_error(e))?,
)
}
/// Write prevout, extension, and mode followed by witness data.
///
/// This calls [`write_without_witness`] to serialize witness metadata,
/// then appends the witness bytes themselves. This is the encoded
/// form that is used in a serialized transaction.
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
self.write_without_witness(&mut writer)?;
Vector::write(&mut writer, &self.witness.payload, |w, b| w.write_u8(*b))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TzeOut {
pub value: Amount,
pub precondition: tze::Precondition,
}
impl TzeOut {
pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> {
let value = {
let mut tmp = [0; 8];
reader.read_exact(&mut tmp)?;
Amount::from_nonnegative_i64_le_bytes(tmp)
}
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "value out of range"))?;
let extension_id = CompactSize::read(&mut reader)?;
let mode = CompactSize::read(&mut reader)?;
let payload = Vector::read(&mut reader, |r| r.read_u8())?;
Ok(TzeOut {
value,
precondition: tze::Precondition {
extension_id: u32::try_from(extension_id).map_err(|e| to_io_error(e))?,
mode: u32::try_from(mode).map_err(|e| to_io_error(e))?,
payload,
},
})
}
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_all(&self.value.to_i64_le_bytes())?;
CompactSize::write(
&mut writer,
usize::try_from(self.precondition.extension_id).map_err(|e| to_io_error(e))?,
)?;
CompactSize::write(
&mut writer,
usize::try_from(self.precondition.mode).map_err(|e| to_io_error(e))?,
)?;
Vector::write(&mut writer, &self.precondition.payload, |w, b| {
w.write_u8(*b)
})
}
}
#[derive(Clone)]
pub struct SpendDescription {
pub cv: jubjub::ExtendedPoint,
pub anchor: bls12_381::Scalar,
@ -205,6 +333,7 @@ impl SpendDescription {
}
}
#[derive(Clone)]
pub struct OutputDescription {
pub cv: jubjub::ExtendedPoint,
pub cmu: bls12_381::Scalar,
@ -296,6 +425,7 @@ impl OutputDescription {
}
}
#[derive(Clone)]
enum SproutProof {
Groth([u8; GROTH_PROOF_SIZE]),
PHGR([u8; PHGR_PROOF_SIZE]),
@ -310,6 +440,7 @@ impl std::fmt::Debug for SproutProof {
}
}
#[derive(Clone)]
pub struct JSDescription {
vpub_old: Amount,
vpub_new: Amount,

View File

@ -1,8 +1,8 @@
use std::iter::Sum;
use std::ops::{Add, AddAssign, Sub, SubAssign};
const COIN: i64 = 1_0000_0000;
const MAX_MONEY: i64 = 21_000_000 * COIN;
pub const COIN: i64 = 1_0000_0000;
pub const MAX_MONEY: i64 = 21_000_000 * COIN;
pub const DEFAULT_FEE: Amount = Amount(10000);
@ -17,7 +17,7 @@ pub const DEFAULT_FEE: Amount = Amount(10000);
/// by the network consensus rules.
///
/// [`Transaction`]: crate::transaction::Transaction
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)]
pub struct Amount(i64);
impl Amount {
@ -147,6 +147,19 @@ impl Sum for Amount {
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::prelude::prop_compose;
use super::{Amount, MAX_MONEY};
prop_compose! {
pub fn arb_nonnegative_amount()(amt in 0i64..MAX_MONEY) -> Amount {
Amount::from_i64(amt).unwrap()
}
}
}
#[cfg(test)]
mod tests {
use super::{Amount, MAX_MONEY};

View File

@ -7,8 +7,7 @@ use std::fmt;
use std::io::{self, Read, Write};
use std::ops::Deref;
use crate::redjubjub::Signature;
use crate::serialize::Vector;
use crate::{consensus::BlockHeight, redjubjub::Signature, serialize::Vector};
pub mod builder;
pub mod components;
@ -17,15 +16,26 @@ mod sighash;
#[cfg(test)]
mod tests;
pub use self::sighash::{signature_hash, signature_hash_data, SIGHASH_ALL};
pub use self::sighash::{signature_hash, signature_hash_data, SignableInput, SIGHASH_ALL};
use self::components::{Amount, JSDescription, OutputDescription, SpendDescription, TxIn, TxOut};
use self::components::{
Amount, JSDescription, OutputDescription, SpendDescription, TxIn, TxOut, TzeIn, TzeOut,
};
const OVERWINTER_VERSION_GROUP_ID: u32 = 0x03C48270;
const OVERWINTER_TX_VERSION: u32 = 3;
const SAPLING_VERSION_GROUP_ID: u32 = 0x892F2085;
const SAPLING_TX_VERSION: u32 = 4;
/// These versions are used exclusively for in-development transaction
/// serialization, and will never be active under the consensus rules.
/// When new consensus transaction versions are added, all call sites
/// using these constants should be inspected, and use of these constants
/// should be removed as appropriate in favor of the new consensus
/// transaction version and group.
const ZFUTURE_VERSION_GROUP_ID: u32 = 0xFFFFFFFF;
const ZFUTURE_TX_VERSION: u32 = 0x0000FFFF;
#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct TxId(pub [u8; 32]);
@ -38,7 +48,7 @@ impl fmt::Display for TxId {
}
/// A Zcash transaction.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Transaction {
txid: TxId,
data: TransactionData,
@ -58,14 +68,17 @@ impl PartialEq for Transaction {
}
}
#[derive(Clone)]
pub struct TransactionData {
pub overwintered: bool,
pub version: u32,
pub version_group_id: u32,
pub vin: Vec<TxIn>,
pub vout: Vec<TxOut>,
pub tze_inputs: Vec<TzeIn>,
pub tze_outputs: Vec<TzeOut>,
pub lock_time: u32,
pub expiry_height: u32,
pub expiry_height: BlockHeight,
pub value_balance: Amount,
pub shielded_spends: Vec<SpendDescription>,
pub shielded_outputs: Vec<OutputDescription>,
@ -85,6 +98,8 @@ impl std::fmt::Debug for TransactionData {
version_group_id = {:?},
vin = {:?},
vout = {:?},
tze_inputs = {:?},
tze_outputs = {:?},
lock_time = {:?},
expiry_height = {:?},
value_balance = {:?},
@ -98,6 +113,8 @@ impl std::fmt::Debug for TransactionData {
self.version_group_id,
self.vin,
self.vout,
self.tze_inputs,
self.tze_outputs,
self.lock_time,
self.expiry_height,
self.value_balance,
@ -118,8 +135,31 @@ impl TransactionData {
version_group_id: SAPLING_VERSION_GROUP_ID,
vin: vec![],
vout: vec![],
tze_inputs: vec![],
tze_outputs: vec![],
lock_time: 0,
expiry_height: 0,
expiry_height: 0u32.into(),
value_balance: Amount::zero(),
shielded_spends: vec![],
shielded_outputs: vec![],
joinsplits: vec![],
joinsplit_pubkey: None,
joinsplit_sig: None,
binding_sig: None,
}
}
pub fn zfuture() -> Self {
TransactionData {
overwintered: true,
version: ZFUTURE_TX_VERSION,
version_group_id: ZFUTURE_VERSION_GROUP_ID,
vin: vec![],
vout: vec![],
tze_inputs: vec![],
tze_outputs: vec![],
lock_time: 0,
expiry_height: 0u32.into(),
value_balance: Amount::zero(),
shielded_spends: vec![],
shielded_outputs: vec![],
@ -178,7 +218,11 @@ impl Transaction {
let is_sapling_v4 = overwintered
&& version_group_id == SAPLING_VERSION_GROUP_ID
&& version == SAPLING_TX_VERSION;
if overwintered && !(is_overwinter_v3 || is_sapling_v4) {
let has_tze = overwintered
&& version_group_id == ZFUTURE_VERSION_GROUP_ID
&& version == ZFUTURE_TX_VERSION;
if overwintered && !(is_overwinter_v3 || is_sapling_v4 || has_tze) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Unknown transaction format",
@ -187,14 +231,22 @@ impl Transaction {
let vin = Vector::read(&mut reader, TxIn::read)?;
let vout = Vector::read(&mut reader, TxOut::read)?;
let lock_time = reader.read_u32::<LittleEndian>()?;
let expiry_height = if is_overwinter_v3 || is_sapling_v4 {
reader.read_u32::<LittleEndian>()?
let (tze_inputs, tze_outputs) = if has_tze {
let wi = Vector::read(&mut reader, TzeIn::read)?;
let wo = Vector::read(&mut reader, TzeOut::read)?;
(wi, wo)
} else {
0
(vec![], vec![])
};
let (value_balance, shielded_spends, shielded_outputs) = if is_sapling_v4 {
let lock_time = reader.read_u32::<LittleEndian>()?;
let expiry_height: BlockHeight = if is_overwinter_v3 || is_sapling_v4 || has_tze {
reader.read_u32::<LittleEndian>()?.into()
} else {
0u32.into()
};
let (value_balance, shielded_spends, shielded_outputs) = if is_sapling_v4 || has_tze {
let vb = {
let mut tmp = [0; 8];
reader.read_exact(&mut tmp)?;
@ -226,12 +278,13 @@ impl Transaction {
(vec![], None, None)
};
let binding_sig =
if is_sapling_v4 && !(shielded_spends.is_empty() && shielded_outputs.is_empty()) {
Some(Signature::read(&mut reader)?)
} else {
None
};
let binding_sig = if (is_sapling_v4 || has_tze)
&& !(shielded_spends.is_empty() && shielded_outputs.is_empty())
{
Some(Signature::read(&mut reader)?)
} else {
None
};
Transaction::from_data(TransactionData {
overwintered,
@ -239,6 +292,8 @@ impl Transaction {
version_group_id,
vin,
vout,
tze_inputs,
tze_outputs,
lock_time,
expiry_height,
value_balance,
@ -263,7 +318,11 @@ impl Transaction {
let is_sapling_v4 = self.overwintered
&& self.version_group_id == SAPLING_VERSION_GROUP_ID
&& self.version == SAPLING_TX_VERSION;
if self.overwintered && !(is_overwinter_v3 || is_sapling_v4) {
let has_tze = self.overwintered
&& self.version_group_id == ZFUTURE_VERSION_GROUP_ID
&& self.version == ZFUTURE_TX_VERSION;
if self.overwintered && !(is_overwinter_v3 || is_sapling_v4 || has_tze) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Unknown transaction format",
@ -272,12 +331,16 @@ impl Transaction {
Vector::write(&mut writer, &self.vin, |w, e| e.write(w))?;
Vector::write(&mut writer, &self.vout, |w, e| e.write(w))?;
if has_tze {
Vector::write(&mut writer, &self.tze_inputs, |w, e| e.write(w))?;
Vector::write(&mut writer, &self.tze_outputs, |w, e| e.write(w))?;
}
writer.write_u32::<LittleEndian>(self.lock_time)?;
if is_overwinter_v3 || is_sapling_v4 {
writer.write_u32::<LittleEndian>(self.expiry_height)?;
if is_overwinter_v3 || is_sapling_v4 || has_tze {
writer.write_u32::<LittleEndian>(u32::from(self.expiry_height))?;
}
if is_sapling_v4 {
if is_sapling_v4 || has_tze {
writer.write_all(&self.value_balance.to_i64_le_bytes())?;
Vector::write(&mut writer, &self.shielded_spends, |w, e| e.write(w))?;
Vector::write(&mut writer, &self.shielded_outputs, |w, e| e.write(w))?;
@ -322,7 +385,9 @@ impl Transaction {
}
}
if is_sapling_v4 && !(self.shielded_spends.is_empty() && self.shielded_outputs.is_empty()) {
if (is_sapling_v4 || has_tze)
&& !(self.shielded_spends.is_empty() && self.shielded_outputs.is_empty())
{
match self.binding_sig {
Some(sig) => sig.write(&mut writer)?,
None => {

View File

@ -1,14 +1,22 @@
use std::convert::TryInto;
use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams};
use byteorder::{LittleEndian, WriteBytesExt};
use ff::PrimeField;
use group::GroupEncoding;
use crate::{
consensus,
extensions::transparent::Precondition,
legacy::Script,
serialize::{CompactSize, Vector},
};
use super::{
components::{Amount, TxOut},
Transaction, TransactionData, OVERWINTER_VERSION_GROUP_ID, SAPLING_TX_VERSION,
SAPLING_VERSION_GROUP_ID,
SAPLING_VERSION_GROUP_ID, ZFUTURE_VERSION_GROUP_ID,
};
use crate::{consensus, legacy::Script};
const ZCASH_SIGHASH_PERSONALIZATION_PREFIX: &[u8; 12] = b"ZcashSigHash";
const ZCASH_PREVOUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashPrevoutHash";
@ -17,6 +25,11 @@ const ZCASH_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashOutputsHash";
const ZCASH_JOINSPLITS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashJSplitsHash";
const ZCASH_SHIELDED_SPENDS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSSpendsHash";
const ZCASH_SHIELDED_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashSOutputHash";
const ZCASH_TZE_INPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"Zcash_TzeInsHash";
const ZCASH_TZE_OUTPUTS_HASH_PERSONALIZATION: &[u8; 16] = b"ZcashTzeOutsHash";
const ZCASH_TZE_SIGNED_INPUT_TAG: &[u8; 1] = &[0x00];
const ZCASH_TRANSPARENT_SIGNED_INPUT_TAG: &[u8; 1] = &[0x01];
pub const SIGHASH_ALL: u32 = 1;
const SIGHASH_NONE: u32 = 2;
@ -46,6 +59,7 @@ enum SigHashVersion {
Sprout,
Overwinter,
Sapling,
ZFuture,
}
impl SigHashVersion {
@ -54,6 +68,7 @@ impl SigHashVersion {
match tx.version_group_id {
OVERWINTER_VERSION_GROUP_ID => SigHashVersion::Overwinter,
SAPLING_VERSION_GROUP_ID => SigHashVersion::Sapling,
ZFUTURE_VERSION_GROUP_ID => SigHashVersion::ZFuture,
_ => unimplemented!(),
}
} else {
@ -151,15 +166,69 @@ fn shielded_outputs_hash(tx: &TransactionData) -> Blake2bHash {
.hash(&data)
}
pub fn signature_hash_data(
fn tze_inputs_hash(tx: &TransactionData) -> Blake2bHash {
let mut data = vec![];
for tzein in &tx.tze_inputs {
tzein.write_without_witness(&mut data).unwrap();
}
Blake2bParams::new()
.hash_length(32)
.personal(ZCASH_TZE_INPUTS_HASH_PERSONALIZATION)
.hash(&data)
}
fn tze_outputs_hash(tx: &TransactionData) -> Blake2bHash {
let mut data = vec![];
for tzeout in &tx.tze_outputs {
tzeout.write(&mut data).unwrap();
}
Blake2bParams::new()
.hash_length(32)
.personal(ZCASH_TZE_OUTPUTS_HASH_PERSONALIZATION)
.hash(&data)
}
pub enum SignableInput<'a> {
Shielded,
Transparent {
index: usize,
script_code: &'a Script,
value: Amount,
},
Tze {
index: usize,
precondition: &'a Precondition,
value: Amount,
},
}
impl<'a> SignableInput<'a> {
pub fn transparent(index: usize, script_code: &'a Script, value: Amount) -> Self {
SignableInput::Transparent {
index,
script_code,
value,
}
}
pub fn tze(index: usize, precondition: &'a Precondition, value: Amount) -> Self {
SignableInput::Tze {
index,
precondition,
value,
}
}
}
pub fn signature_hash_data<'a>(
tx: &TransactionData,
consensus_branch_id: consensus::BranchId,
hash_type: u32,
transparent_input: Option<(usize, &Script, Amount)>,
signable_input: SignableInput<'a>,
) -> Vec<u8> {
let sigversion = SigHashVersion::from_tx(tx);
match sigversion {
SigHashVersion::Overwinter | SigHashVersion::Sapling => {
SigHashVersion::Overwinter | SigHashVersion::Sapling | SigHashVersion::ZFuture => {
let mut personal = [0; 16];
(&mut personal[..12]).copy_from_slice(ZCASH_SIGHASH_PERSONALIZATION_PREFIX);
(&mut personal[12..])
@ -182,22 +251,27 @@ pub fn signature_hash_data(
&& (hash_type & SIGHASH_MASK) != SIGHASH_NONE,
sequence_hash(tx)
);
if (hash_type & SIGHASH_MASK) != SIGHASH_SINGLE
&& (hash_type & SIGHASH_MASK) != SIGHASH_NONE
{
h.update(outputs_hash(tx).as_ref());
} else if (hash_type & SIGHASH_MASK) == SIGHASH_SINGLE
&& transparent_input.is_some()
&& transparent_input.as_ref().unwrap().0 < tx.vout.len()
{
h.update(
single_output_hash(&tx.vout[transparent_input.as_ref().unwrap().0]).as_ref(),
);
} else if (hash_type & SIGHASH_MASK) == SIGHASH_SINGLE {
match signable_input {
SignableInput::Transparent { index, .. } if index < tx.vout.len() => {
h.update(single_output_hash(&tx.vout[index]).as_ref())
}
_ => h.update(&[0; 32]),
};
} else {
h.update(&[0; 32]);
};
if sigversion == SigHashVersion::ZFuture {
update_hash!(h, !tx.tze_inputs.is_empty(), tze_inputs_hash(tx));
update_hash!(h, !tx.tze_outputs.is_empty(), tze_outputs_hash(tx));
}
update_hash!(h, !tx.joinsplits.is_empty(), joinsplits_hash(tx));
if sigversion == SigHashVersion::Sapling {
if sigversion == SigHashVersion::Sapling || sigversion == SigHashVersion::ZFuture {
update_hash!(h, !tx.shielded_spends.is_empty(), shielded_spends_hash(tx));
update_hash!(
h,
@ -206,21 +280,58 @@ pub fn signature_hash_data(
);
}
update_u32!(h, tx.lock_time, tmp);
update_u32!(h, tx.expiry_height, tmp);
if sigversion == SigHashVersion::Sapling {
update_u32!(h, tx.expiry_height.into(), tmp);
if sigversion == SigHashVersion::Sapling || sigversion == SigHashVersion::ZFuture {
h.update(&tx.value_balance.to_i64_le_bytes());
}
update_u32!(h, hash_type, tmp);
if let Some((n, script_code, amount)) = transparent_input {
let mut data = vec![];
tx.vin[n].prevout.write(&mut data).unwrap();
script_code.write(&mut data).unwrap();
data.extend_from_slice(&amount.to_i64_le_bytes());
(&mut data)
.write_u32::<LittleEndian>(tx.vin[n].sequence)
.unwrap();
h.update(&data);
match signable_input {
SignableInput::Transparent {
index,
script_code,
value,
} => {
let mut data = if sigversion == SigHashVersion::ZFuture {
// domain separation here is to avoid collision attacks
// between transparent and TZE inputs.
ZCASH_TRANSPARENT_SIGNED_INPUT_TAG.to_vec()
} else {
vec![]
};
tx.vin[index].prevout.write(&mut data).unwrap();
script_code.write(&mut data).unwrap();
data.extend_from_slice(&value.to_i64_le_bytes());
(&mut data)
.write_u32::<LittleEndian>(tx.vin[index].sequence)
.unwrap();
h.update(&data);
}
SignableInput::Tze {
index,
precondition,
value,
} if sigversion == SigHashVersion::ZFuture => {
// domain separation here is to avoid collision attacks
// between transparent and TZE inputs.
let mut data = ZCASH_TZE_SIGNED_INPUT_TAG.to_vec();
tx.tze_inputs[index].prevout.write(&mut data).unwrap();
CompactSize::write(&mut data, precondition.extension_id.try_into().unwrap())
.unwrap();
CompactSize::write(&mut data, precondition.mode.try_into().unwrap()).unwrap();
Vector::write(&mut data, &precondition.payload, |w, e| w.write_u8(*e)).unwrap();
data.extend_from_slice(&value.to_i64_le_bytes());
h.update(&data);
}
SignableInput::Tze { .. } => {
panic!("A request has been made to sign a TZE input, but the signature hash version is not ZFuture");
}
_ => (),
}
h.finalize().as_ref().to_vec()
@ -229,11 +340,11 @@ pub fn signature_hash_data(
}
}
pub fn signature_hash(
pub fn signature_hash<'a>(
tx: &Transaction,
consensus_branch_id: consensus::BranchId,
hash_type: u32,
transparent_input: Option<(usize, &Script, Amount)>,
signable_input: SignableInput<'a>,
) -> Vec<u8> {
signature_hash_data(tx, consensus_branch_id, hash_type, transparent_input)
signature_hash_data(tx, consensus_branch_id, hash_type, signable_input)
}

View File

@ -1,8 +1,133 @@
use ff::Field;
use rand_core::OsRng;
use super::{components::Amount, sighash::signature_hash, Transaction, TransactionData};
use crate::{constants::SPENDING_KEY_GENERATOR, redjubjub::PrivateKey};
use proptest::collection::vec;
use proptest::prelude::*;
use proptest::sample::select;
use crate::{
consensus::BranchId, constants::SPENDING_KEY_GENERATOR, extensions::transparent as tze,
legacy::Script, redjubjub::PrivateKey,
};
use super::{
components::amount::MAX_MONEY,
components::{Amount, OutPoint, TxIn, TxOut, TzeIn, TzeOut},
sighash::{signature_hash, SignableInput},
Transaction, TransactionData, OVERWINTER_TX_VERSION, OVERWINTER_VERSION_GROUP_ID,
SAPLING_TX_VERSION, SAPLING_VERSION_GROUP_ID, ZFUTURE_TX_VERSION, ZFUTURE_VERSION_GROUP_ID,
};
prop_compose! {
fn arb_outpoint()(hash in prop::array::uniform32(1u8..), n in 1..(100 as u32)) -> OutPoint {
OutPoint::new(hash, n)
}
}
const VALID_OPCODES: [u8; 8] = [
0x00, // OP_FALSE,
0x51, // OP_1,
0x52, // OP_2,
0x53, // OP_3,
0xac, // OP_CHECKSIG,
0x63, // OP_IF,
0x65, // OP_VERIF,
0x6a, // OP_RETURN,
];
prop_compose! {
fn arb_script()(v in vec(select(&VALID_OPCODES[..]), 1..256)) -> Script {
Script(v)
}
}
prop_compose! {
fn arb_txin()(prevout in arb_outpoint(), script_sig in arb_script(), sequence in any::<u32>()) -> TxIn {
TxIn { prevout, script_sig, sequence }
}
}
prop_compose! {
fn arb_amount()(value in 0..MAX_MONEY) -> Amount {
Amount::from_i64(value).unwrap()
}
}
prop_compose! {
fn arb_txout()(value in arb_amount(), script_pubkey in arb_script()) -> TxOut {
TxOut { value, script_pubkey }
}
}
prop_compose! {
fn arb_witness()(extension_id in 0..(100 as u32), mode in (0..100 as u32), payload in vec(any::<u8>(), 32..256)) -> tze::Witness {
tze::Witness { extension_id, mode, payload }
}
}
prop_compose! {
fn arb_tzein()(prevout in arb_outpoint(), witness in arb_witness()) -> TzeIn {
TzeIn { prevout, witness }
}
}
prop_compose! {
fn arb_precondition()(extension_id in 0..(100 as u32), mode in (0..100 as u32), payload in vec(any::<u8>(), 32..256)) -> tze::Precondition {
tze::Precondition { extension_id, mode, payload }
}
}
prop_compose! {
fn arb_tzeout()(value in arb_amount(), precondition in arb_precondition()) -> TzeOut {
TzeOut { value, precondition }
}
}
fn tx_versions(branch_id: BranchId) -> impl Strategy<Value = (u32, u32)> {
match branch_id {
BranchId::Sprout => (1..(2 as u32)).prop_map(|i| (i, 0)).boxed(),
BranchId::Overwinter => Just((OVERWINTER_TX_VERSION, OVERWINTER_VERSION_GROUP_ID)).boxed(),
BranchId::ZFuture => Just((ZFUTURE_TX_VERSION, ZFUTURE_VERSION_GROUP_ID)).boxed(),
_otherwise => Just((SAPLING_TX_VERSION, SAPLING_VERSION_GROUP_ID)).boxed(),
}
}
prop_compose! {
fn arb_txdata(branch_id: BranchId)(
(version, version_group_id) in tx_versions(branch_id),
vin in vec(arb_txin(), 0..10),
vout in vec(arb_txout(), 0..10),
tze_inputs in vec(arb_tzein(), 0..10),
tze_outputs in vec(arb_tzeout(), 0..10),
lock_time in any::<u32>(),
expiry_height in any::<u32>(),
value_balance in arb_amount(),
) -> TransactionData {
TransactionData {
overwintered: branch_id != BranchId::Sprout,
version,
version_group_id,
vin, vout,
tze_inputs: if branch_id == BranchId::ZFuture { tze_inputs } else { vec![] },
tze_outputs: if branch_id == BranchId::ZFuture { tze_outputs } else { vec![] },
lock_time,
expiry_height: expiry_height.into(),
value_balance,
shielded_spends: vec![], //FIXME
shielded_outputs: vec![], //FIXME
joinsplits: vec![], //FIXME
joinsplit_pubkey: None, //FIXME
joinsplit_sig: None, //FIXME
binding_sig: None, //FIXME
}
}
}
prop_compose! {
fn arb_tx(branch_id: BranchId)(tx_data in arb_txdata(branch_id)) -> Transaction {
Transaction::from_data(tx_data).unwrap()
}
}
#[test]
fn tx_read_write() {
@ -61,21 +186,72 @@ fn tx_write_rejects_unexpected_binding_sig() {
}
}
proptest! {
#[test]
fn test_tze_roundtrip(tx in arb_tx(BranchId::ZFuture)) {
let mut txn_bytes = vec![];
tx.write(&mut txn_bytes).unwrap();
let txo = Transaction::read(&txn_bytes[..]).unwrap();
assert_eq!(tx.overwintered, txo.overwintered);
assert_eq!(tx.version, txo.version);
assert_eq!(tx.version_group_id, txo.version_group_id);
assert_eq!(tx.vin, txo.vin);
assert_eq!(tx.vout, txo.vout);
assert_eq!(tx.tze_inputs, txo.tze_inputs);
assert_eq!(tx.tze_outputs, txo.tze_outputs);
assert_eq!(tx.lock_time, txo.lock_time);
assert_eq!(tx.value_balance, txo.value_balance);
}
}
#[test]
fn test_tze_tx_parse() {
let txn_bytes = vec![
0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x01, 0x52, 0x52, 0x52, 0x52,
0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52,
0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x20, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x30, 0x75, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x20, 0xd9, 0x81, 0x80, 0x87, 0xde, 0x72, 0x44, 0xab, 0xc1, 0xb5, 0xfc,
0xf2, 0x8e, 0x55, 0xe4, 0x2c, 0x7f, 0xf9, 0xc6, 0x78, 0xc0, 0x60, 0x51, 0x81, 0xf3, 0x7a,
0xc5, 0xd7, 0x41, 0x4a, 0x7b, 0x95, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let tx = Transaction::read(&txn_bytes[..]);
match tx {
Ok(tx) => assert!(!tx.tze_inputs.is_empty()),
Err(e) => assert!(
false,
format!(
"An error occurred parsing a serialized TZE transaction: {}",
e
)
),
}
}
mod data;
#[test]
fn zip_0143() {
for tv in self::data::zip_0143::make_test_vectors() {
let tx = Transaction::read(&tv.tx[..]).unwrap();
let transparent_input = tv.transparent_input.map(|n| {
(
let signable_input = match tv.transparent_input {
Some(n) => SignableInput::transparent(
n as usize,
&tv.script_code,
Amount::from_nonnegative_i64(tv.amount).unwrap(),
)
});
),
_ => SignableInput::Shielded,
};
assert_eq!(
signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, transparent_input),
signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input),
tv.sighash
);
}
@ -85,16 +261,17 @@ fn zip_0143() {
fn zip_0243() {
for tv in self::data::zip_0243::make_test_vectors() {
let tx = Transaction::read(&tv.tx[..]).unwrap();
let transparent_input = tv.transparent_input.map(|n| {
(
let signable_input = match tv.transparent_input {
Some(n) => SignableInput::transparent(
n as usize,
&tv.script_code,
Amount::from_nonnegative_i64(tv.amount).unwrap(),
)
});
),
_ => SignableInput::Shielded,
};
assert_eq!(
signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, transparent_input),
signature_hash(&tx, tv.consensus_branch_id, tv.hash_type, signable_input),
tv.sighash
);
}

View File

@ -1,6 +1,10 @@
use blake2b_simd::Params;
use crate::{consensus, consensus::NetworkUpgrade, primitives::Rseed};
use crate::{
consensus::{self, BlockHeight, NetworkUpgrade},
primitives::Rseed,
};
use ff::Field;
use rand_core::{CryptoRng, RngCore};
@ -13,10 +17,11 @@ pub fn hash_to_scalar(persona: &[u8], a: &[u8], b: &[u8]) -> jubjub::Fr {
}
pub fn generate_random_rseed<P: consensus::Parameters, R: RngCore + CryptoRng>(
height: u32,
params: &P,
height: BlockHeight,
rng: &mut R,
) -> Rseed {
if P::is_nu_active(NetworkUpgrade::Canopy, height) {
if params.is_nu_active(NetworkUpgrade::Canopy, height) {
let mut buffer = [0u8; 32];
&rng.fill_bytes(&mut buffer);
Rseed::AfterZip212(buffer)

View File

@ -8,7 +8,7 @@ use lazy_static::lazy_static;
use zcash_primitives::constants::{PEDERSEN_HASH_CHUNKS_PER_GENERATOR, PEDERSEN_HASH_GENERATORS};
/// The `d` constant of the twisted Edwards curve.
pub const EDWARDS_D: Scalar = Scalar::from_raw([
pub(crate) const EDWARDS_D: Scalar = Scalar::from_raw([
0x0106_5fd6_d634_3eb1,
0x292d_7f6d_3757_9d26,
0xf5fd_9207_e6bd_7fd4,
@ -16,7 +16,7 @@ pub const EDWARDS_D: Scalar = Scalar::from_raw([
]);
/// The `A` constant of the birationally equivalent Montgomery curve.
pub const MONTGOMERY_A: Scalar = Scalar::from_raw([
pub(crate) const MONTGOMERY_A: Scalar = Scalar::from_raw([
0x0000_0000_0000_a002,
0x0000_0000_0000_0000,
0x0000_0000_0000_0000,
@ -24,7 +24,7 @@ pub const MONTGOMERY_A: Scalar = Scalar::from_raw([
]);
/// The scaling factor used for conversion to and from the Montgomery form.
pub const MONTGOMERY_SCALE: Scalar = Scalar::from_raw([
pub(crate) const MONTGOMERY_SCALE: Scalar = Scalar::from_raw([
0x8f45_35f7_cf82_b8d9,
0xce40_6970_3da8_8abd,
0x31de_341e_77d7_64e5,
@ -62,13 +62,13 @@ lazy_static! {
/// The pre-computed window tables `[-4, 3, 2, 1, 1, 2, 3, 4]` of different magnitudes
/// of the Pedersen hash segment generators.
pub static ref PEDERSEN_CIRCUIT_GENERATORS: Vec<Vec<Vec<(Scalar, Scalar)>>> =
pub(crate) static ref PEDERSEN_CIRCUIT_GENERATORS: Vec<Vec<Vec<(Scalar, Scalar)>>> =
generate_pedersen_circuit_generators();
}
/// Creates the 3-bit window table `[0, 1, ..., 8]` for different magnitudes of a fixed
/// generator.
fn generate_circuit_generator(mut gen: jubjub::SubgroupPoint) -> FixedGeneratorOwned {
pub fn generate_circuit_generator(mut gen: jubjub::SubgroupPoint) -> FixedGeneratorOwned {
let mut windows = vec![];
for _ in 0..FIXED_BASE_CHUNKS_PER_GENERATOR {

View File

@ -19,7 +19,7 @@ use directories::BaseDirs;
use std::path::PathBuf;
pub mod circuit;
mod constants;
pub mod constants;
mod hashreader;
pub mod sapling;
pub mod sprout;
@ -135,7 +135,10 @@ pub fn load_parameters(
)
}
fn parse_parameters<R: io::Read>(
/// Parse Bls12 keys from bytes as serialized by [`Parameters::write`].
///
/// This function will panic if it encounters unparseable data.
pub fn parse_parameters<R: io::Read>(
spend_fs: R,
output_fs: R,
sprout_fs: Option<R>,

View File

@ -2,6 +2,7 @@
use bellman::groth16::{Parameters, PreparedVerifyingKey};
use bls12_381::Bls12;
use std::path::Path;
use zcash_primitives::{
merkle_tree::MerklePath,
primitives::{Diversifier, PaymentAddress, ProofGenerationKey, Rseed},
@ -11,15 +12,10 @@ use zcash_primitives::{
transaction::components::{Amount, GROTH_PROOF_SIZE},
};
use crate::sapling::SaplingProvingContext;
use crate::{load_parameters, parse_parameters, sapling::SaplingProvingContext};
#[cfg(feature = "local-prover")]
use crate::{default_params_folder, load_parameters, SAPLING_OUTPUT_NAME, SAPLING_SPEND_NAME};
#[cfg(feature = "local-prover")]
use std::path::Path;
#[cfg(feature = "bundled-prover")]
use crate::parse_parameters;
use crate::{default_params_folder, SAPLING_OUTPUT_NAME, SAPLING_SPEND_NAME};
/// An implementation of [`TxProver`] using Sapling Spend and Output parameters from
/// locally-accessible paths.
@ -48,8 +44,6 @@ impl LocalTxProver {
///
/// This function will panic if the paths do not point to valid parameter files with
/// the expected hashes.
#[cfg(feature = "local-prover")]
#[cfg_attr(docsrs, doc(cfg(feature = "local-prover")))]
pub fn new(spend_path: &Path, output_path: &Path) -> Self {
let (spend_params, spend_vk, output_params, _, _) =
load_parameters(spend_path, output_path, None);
@ -60,6 +54,32 @@ impl LocalTxProver {
}
}
/// Creates a `LocalTxProver` using parameters specified as byte arrays.
///
/// # Examples
///
/// ```should_panic
/// use std::path::Path;
/// use zcash_proofs::prover::LocalTxProver;
///
/// let tx_prover = LocalTxProver::from_bytes(&[0u8], &[0u8]);
/// ```
///
/// # Panics
///
/// This function will panic if the byte arrays do not contain valid parameters with
/// the expected hashes.
pub fn from_bytes(spend_param_bytes: &[u8], output_param_bytes: &[u8]) -> Self {
let (spend_params, spend_vk, output_params, _, _) =
parse_parameters(spend_param_bytes, output_param_bytes, None);
LocalTxProver {
spend_params,
spend_vk,
output_params,
}
}
/// Attempts to create a `LocalTxProver` using parameters from the default local
/// location.
///