Merge branch 'master' into zcash_client_sqlite-0.2.1
This commit is contained in:
commit
450d68f073
|
@ -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:
|
||||
|
|
|
@ -3,6 +3,7 @@ members = [
|
|||
"components/equihash",
|
||||
"zcash_client_backend",
|
||||
"zcash_client_sqlite",
|
||||
"zcash_extensions",
|
||||
"zcash_history",
|
||||
"zcash_primitives",
|
||||
"zcash_proofs",
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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: [] }] }
|
|
@ -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(
|
||||
¶ms.b58_pubkey_address_prefix(),
|
||||
¶ms.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,
|
||||
¶ms.b58_pubkey_address_prefix(),
|
||||
¶ms.b58_script_address_prefix(),
|
||||
addr,
|
||||
),
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
//! Zcash global and per-network constants.
|
||||
|
||||
pub mod mainnet;
|
||||
pub mod regtest;
|
||||
pub mod testnet;
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
/// ```
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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(¤t, &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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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" }
|
|
@ -0,0 +1 @@
|
|||
pub mod transparent;
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
pub mod consensus;
|
||||
pub mod transparent;
|
|
@ -0,0 +1,3 @@
|
|||
//! Zcash transparent extensions.
|
||||
|
||||
pub mod demo;
|
|
@ -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(())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
pub mod transparent;
|
|
@ -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>;
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[..]);
|
||||
|
|
|
@ -60,7 +60,7 @@ pub trait TxProver {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod mock {
|
||||
pub mod mock {
|
||||
use ff::Field;
|
||||
use rand_core::OsRng;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
Loading…
Reference in New Issue