Add PCZT commands

This commit is contained in:
Jack Grigg 2024-12-10 11:54:00 +00:00
parent 472362363b
commit ff072fb1a6
11 changed files with 859 additions and 27 deletions

169
Cargo.lock generated
View File

@ -348,6 +348,15 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "atomic-polyfill"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
dependencies = [
"critical-section",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -504,6 +513,9 @@ checksum = "aa13fae8b6255872fd86f7faf4b41168661d7d78609f7bfe6771b85c6739a15b"
dependencies = [
"bs58",
"hmac",
"k256",
"once_cell",
"pbkdf2",
"rand_core",
"ripemd",
"secp256k1",
@ -794,6 +806,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "cobs"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
[[package]]
name = "compact_str"
version = "0.8.0"
@ -871,6 +889,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-channel"
version = "0.5.13"
@ -1383,6 +1407,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "embedded-io"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
[[package]]
name = "embedded-io"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[package]]
name = "enum-ordinalize"
version = "3.1.15"
@ -1399,7 +1435,7 @@ dependencies = [
[[package]]
name = "equihash"
version = "0.2.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=e0f04e6c7749751e7f590b2c25275f1fa3421d50#e0f04e6c7749751e7f590b2c25275f1fa3421d50"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"blake2b_simd",
"byteorder",
@ -1435,7 +1471,7 @@ dependencies = [
[[package]]
name = "f4jumble"
version = "0.1.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=e0f04e6c7749751e7f590b2c25275f1fa3421d50#e0f04e6c7749751e7f590b2c25275f1fa3421d50"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"blake2b_simd",
]
@ -1766,6 +1802,18 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "getset"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c"
dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "gimli"
version = "0.31.1"
@ -1869,6 +1917,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -1907,6 +1964,20 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heapless"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
dependencies = [
"atomic-polyfill",
"hash32",
"rustc_version",
"serde",
"spin 0.9.8",
"stable_deref_trait",
]
[[package]]
name = "heck"
version = "0.5.0"
@ -2351,6 +2422,18 @@ dependencies = [
"subtle",
]
[[package]]
name = "k256"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
dependencies = [
"cfg-if",
"ecdsa",
"elliptic-curve",
"sha2",
]
[[package]]
name = "keccak"
version = "0.1.5"
@ -2768,14 +2851,14 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "orchard"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f18e997fa121de5c73e95cdc7e8512ae43b7de38904aeea5e5713cc48f3c0ba"
source = "git+https://github.com/zcash/orchard.git?rev=bcd08e1d23e70c42a338f3e3f79d6f4c0c219805#bcd08e1d23e70c42a338f3e3f79d6f4c0c219805"
dependencies = [
"aes",
"bitvec",
"blake2b_simd",
"ff",
"fpe",
"getset",
"group",
"halo2_gadgets",
"halo2_proofs",
@ -2930,6 +3013,31 @@ dependencies = [
"password-hash",
]
[[package]]
name = "pczt"
version = "0.0.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"blake2b_simd",
"bls12_381",
"ff",
"getset",
"jubjub",
"nonempty",
"orchard",
"pasta_curves",
"postcard",
"rand_core",
"redjubjub",
"sapling-crypto",
"secp256k1",
"serde",
"serde_with",
"zcash_note_encryption",
"zcash_primitives",
"zcash_protocol",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@ -3082,6 +3190,19 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "postcard"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8"
dependencies = [
"cobs",
"embedded-io 0.4.0",
"embedded-io 0.6.1",
"heapless",
"serde",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -3785,8 +3906,7 @@ dependencies = [
[[package]]
name = "sapling-crypto"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfff8cfce16aeb38da50b8e2ed33c9018f30552beff2210c266662a021b17f38"
source = "git+https://github.com/zcash/sapling-crypto.git?rev=29cff9683cdf2f0c522ff3224081dfb4fbc80248#29cff9683cdf2f0c522ff3224081dfb4fbc80248"
dependencies = [
"aes",
"bellman",
@ -3798,6 +3918,7 @@ dependencies = [
"document-features",
"ff",
"fpe",
"getset",
"group",
"hex",
"incrementalmerkletree",
@ -4209,6 +4330,9 @@ name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "spki"
@ -4261,6 +4385,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
@ -5709,6 +5839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
"serde",
]
[[package]]
@ -6153,11 +6284,12 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zcash_address"
version = "0.6.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=e0f04e6c7749751e7f590b2c25275f1fa3421d50#e0f04e6c7749751e7f590b2c25275f1fa3421d50"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"bech32",
"bs58",
"f4jumble",
"serde",
"zcash_encoding",
"zcash_protocol",
]
@ -6165,7 +6297,7 @@ dependencies = [
[[package]]
name = "zcash_client_backend"
version = "0.15.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=e0f04e6c7749751e7f590b2c25275f1fa3421d50#e0f04e6c7749751e7f590b2c25275f1fa3421d50"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"arti-client",
"base64 0.22.1",
@ -6189,7 +6321,9 @@ dependencies = [
"nonempty",
"orchard",
"pasta_curves",
"pczt",
"percent-encoding",
"postcard",
"prost",
"rand",
"rand_core",
@ -6225,7 +6359,7 @@ dependencies = [
[[package]]
name = "zcash_client_sqlite"
version = "0.13.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=e0f04e6c7749751e7f590b2c25275f1fa3421d50#e0f04e6c7749751e7f590b2c25275f1fa3421d50"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"bip32",
"bs58",
@ -6244,6 +6378,7 @@ dependencies = [
"schemerz",
"schemerz-rusqlite",
"secrecy 0.8.0",
"serde",
"shardtree",
"static_assertions",
"subtle",
@ -6262,7 +6397,7 @@ dependencies = [
[[package]]
name = "zcash_encoding"
version = "0.2.1"
source = "git+https://github.com/zcash/librustzcash.git?rev=e0f04e6c7749751e7f590b2c25275f1fa3421d50#e0f04e6c7749751e7f590b2c25275f1fa3421d50"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"byteorder",
"nonempty",
@ -6271,7 +6406,7 @@ dependencies = [
[[package]]
name = "zcash_keys"
version = "0.5.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=e0f04e6c7749751e7f590b2c25275f1fa3421d50#e0f04e6c7749751e7f590b2c25275f1fa3421d50"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"bech32",
"bip32",
@ -6312,7 +6447,7 @@ dependencies = [
[[package]]
name = "zcash_primitives"
version = "0.20.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=e0f04e6c7749751e7f590b2c25275f1fa3421d50#e0f04e6c7749751e7f590b2c25275f1fa3421d50"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"aes",
"bip32",
@ -6323,6 +6458,7 @@ dependencies = [
"equihash",
"ff",
"fpe",
"getset",
"group",
"hex",
"incrementalmerkletree",
@ -6350,7 +6486,7 @@ dependencies = [
[[package]]
name = "zcash_proofs"
version = "0.20.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=e0f04e6c7749751e7f590b2c25275f1fa3421d50#e0f04e6c7749751e7f590b2c25275f1fa3421d50"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"bellman",
"blake2b_simd",
@ -6372,7 +6508,7 @@ dependencies = [
[[package]]
name = "zcash_protocol"
version = "0.4.1"
source = "git+https://github.com/zcash/librustzcash.git?rev=e0f04e6c7749751e7f590b2c25275f1fa3421d50#e0f04e6c7749751e7f590b2c25275f1fa3421d50"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"document-features",
"memuse",
@ -6394,12 +6530,15 @@ dependencies = [
"age",
"anyhow",
"bip0039",
"bip32",
"crossterm",
"futures-util",
"gumdrop",
"hex",
"iso_currency",
"jubjub",
"orchard",
"pczt",
"prost",
"ratatui",
"rayon",
@ -6486,7 +6625,7 @@ dependencies = [
[[package]]
name = "zip321"
version = "0.2.0"
source = "git+https://github.com/zcash/librustzcash.git?rev=e0f04e6c7749751e7f590b2c25275f1fa3421d50#e0f04e6c7749751e7f590b2c25275f1fa3421d50"
source = "git+https://github.com/zcash/librustzcash.git?rev=1e274c892a11cd15f643f08ffa579166d60180bb#1e274c892a11cd15f643f08ffa579166d60180bb"
dependencies = [
"base64 0.22.1",
"nom",

View File

@ -9,9 +9,11 @@ publish = false
[dependencies]
anyhow = "1"
bip0039 = { version = "0.12", features = ["std", "all-languages"] }
bip32 = "0.5"
futures-util = "0.3"
gumdrop = "0.8"
hex = "0.4"
jubjub = "0.10"
prost = "0.13"
rayon = "1.7"
rusqlite = { version = "0.32", features = ["time"] }
@ -27,10 +29,11 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
uuid = "1"
orchard = { version = "0.10", default-features = false }
pczt = "0.0"
sapling = { package = "sapling-crypto", version = "0.3" }
zcash_address = "0.6"
zcash_client_backend = { version = "0.15", features = ["lightwalletd-tonic-tls-webpki-roots", "orchard", "tor"] }
zcash_client_sqlite = { version = "0.13", features = ["unstable", "orchard"] }
zcash_client_backend = { version = "0.15", features = ["lightwalletd-tonic-tls-webpki-roots", "orchard", "pczt", "tor"] }
zcash_client_sqlite = { version = "0.13", features = ["unstable", "orchard", "serde"] }
zcash_keys = { version = "0.5", features = ["unstable", "orchard"] }
zcash_primitives = "0.20"
zcash_proofs = "0.20"
@ -66,11 +69,14 @@ tui = [
]
[patch.crates-io]
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
zip321 = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
orchard = { git = "https://github.com/zcash/orchard.git", rev = "bcd08e1d23e70c42a338f3e3f79d6f4c0c219805" }
pczt = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
sapling-crypto = { git = "https://github.com/zcash/sapling-crypto.git", rev = "29cff9683cdf2f0c522ff3224081dfb4fbc80248" }
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
zip321 = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }

View File

@ -7,6 +7,7 @@ pub(crate) mod list_accounts;
pub(crate) mod list_addresses;
pub(crate) mod list_tx;
pub(crate) mod list_unspent;
pub(crate) mod pczt;
pub(crate) mod propose;
pub(crate) mod reset;
pub(crate) mod send;

View File

@ -28,7 +28,7 @@ impl Command {
println!("Account {}", self.account_id);
let (ua, _) = account
.uivk()
.default_address(UnifiedAddressRequest::all().unwrap())?;
.default_address(UnifiedAddressRequest::all())?;
println!(" Default Address: {}", ua.encode(&params));
Ok(())
}

21
src/commands/pczt.rs Normal file
View File

@ -0,0 +1,21 @@
use gumdrop::Options;
pub(crate) mod combine;
pub(crate) mod create;
pub(crate) mod prove;
pub(crate) mod send;
pub(crate) mod sign;
#[derive(Debug, Options)]
pub(crate) enum Command {
#[options(help = "create a PCZT")]
Create(create::Command),
#[options(help = "create proofs for a PCZT")]
Prove(prove::Command),
#[options(help = "apply signatures to a PCZT")]
Sign(sign::Command),
#[options(help = "combine two PCZTs")]
Combine(combine::Command),
#[options(help = "extract a finished transaction and send it")]
Send(send::Command),
}

View File

@ -0,0 +1,40 @@
use std::path::PathBuf;
use anyhow::anyhow;
use gumdrop::Options;
use pczt::{roles::combiner::Combiner, Pczt};
use tokio::{
fs::File,
io::{stdout, AsyncReadExt, AsyncWriteExt},
};
// Options accepted for the `pczt combine` command
#[derive(Debug, Options)]
pub(crate) struct Command {
#[options(help = "a list of PCZT files to combine")]
input: Vec<PathBuf>,
}
impl Command {
pub(crate) async fn run(self) -> Result<(), anyhow::Error> {
let mut pczts = vec![];
for f in self.input {
let mut f = File::open(f).await?;
let mut buf = vec![];
f.read_to_end(&mut buf).await?;
let pczt = Pczt::parse(&buf).map_err(|e| anyhow!("Failed to read PCZT: {:?}", e))?;
pczts.push(pczt);
}
let pczt = Combiner::new(pczts)
.combine()
.map_err(|e| anyhow!("Failed to combine PCZTs: {:?}", e))?;
stdout().write_all(&pczt.serialize()).await?;
Ok(())
}
}

121
src/commands/pczt/create.rs Normal file
View File

@ -0,0 +1,121 @@
#![allow(deprecated)]
use std::{num::NonZeroUsize, str::FromStr};
use anyhow::anyhow;
use gumdrop::Options;
use tokio::io::{stdout, AsyncWriteExt};
use zcash_address::ZcashAddress;
use zcash_client_backend::{
data_api::{
wallet::{
create_pczt_from_proposal, input_selection::GreedyInputSelector, propose_transfer,
},
WalletRead,
},
fees::{standard::MultiOutputChangeStrategy, DustOutputPolicy, SplitPolicy, StandardFeeRule},
wallet::OvkPolicy,
ShieldedProtocol,
};
use zcash_client_sqlite::WalletDb;
use zcash_protocol::{
memo::{Memo, MemoBytes},
value::Zatoshis,
};
use zip321::{Payment, TransactionRequest};
use crate::{config::WalletConfig, data::get_db_paths, error, MIN_CONFIRMATIONS};
// Options accepted for the `pczt create` command
#[derive(Debug, Options)]
pub(crate) struct Command {
#[options(
required,
help = "the recipient's Unified, Sapling or transparent address"
)]
address: String,
#[options(required, help = "the amount in zatoshis")]
value: u64,
#[options(help = "a memo to send to the recipient")]
memo: Option<String>,
#[options(
help = "note management: the number of notes to maintain in the wallet",
default = "4"
)]
target_note_count: usize,
#[options(
help = "note management: the minimum allowed value for split change amounts",
default = "10000000"
)]
min_split_output_value: u64,
}
impl Command {
pub(crate) async fn run(self, wallet_dir: Option<String>) -> Result<(), anyhow::Error> {
let config = WalletConfig::read(wallet_dir.as_ref())?;
let params = config.network();
let (_, db_data) = get_db_paths(wallet_dir.as_ref());
let mut db_data = WalletDb::for_path(db_data, params)?;
let account_id = *db_data
.get_account_ids()?
.first()
.ok_or(anyhow!("Wallet has no accounts"))?;
// Create the PCZT.
let change_strategy = MultiOutputChangeStrategy::new(
StandardFeeRule::Zip317,
None,
ShieldedProtocol::Orchard,
DustOutputPolicy::default(),
SplitPolicy::with_min_output_value(
NonZeroUsize::new(self.target_note_count)
.ok_or(anyhow!("target note count must be nonzero"))?,
Zatoshis::from_u64(self.min_split_output_value)?,
),
);
let input_selector = GreedyInputSelector::new();
let request = TransactionRequest::new(vec![Payment::new(
ZcashAddress::from_str(&self.address).map_err(|_| error::Error::InvalidRecipient)?,
Zatoshis::from_u64(self.value).map_err(|_| error::Error::InvalidAmount)?,
self.memo
.map(|memo| Memo::from_str(&memo))
.transpose()?
.map(MemoBytes::from),
None,
None,
vec![],
)
.ok_or_else(|| anyhow!("Invalid memo"))?])
.map_err(error::Error::from)?;
let proposal = propose_transfer(
&mut db_data,
&params,
account_id,
&input_selector,
&change_strategy,
request,
MIN_CONFIRMATIONS,
)
.map_err(error::Error::from)?;
let pczt = create_pczt_from_proposal(
&mut db_data,
&params,
account_id,
OvkPolicy::Sender,
&proposal,
)
.map_err(error::Error::from)?;
stdout().write_all(&pczt.serialize()).await?;
Ok(())
}
}

198
src/commands/pczt/prove.rs Normal file
View File

@ -0,0 +1,198 @@
use anyhow::anyhow;
use gumdrop::Options;
use pczt::{
roles::{prover::Prover, updater::Updater},
Pczt,
};
use sapling::ProofGenerationKey;
use secrecy::ExposeSecret;
use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt};
use zcash_keys::keys::UnifiedSpendingKey;
use zcash_proofs::prover::LocalTxProver;
use zcash_protocol::consensus::{NetworkConstants, Parameters};
use zip32::fingerprint::SeedFingerprint;
use crate::config::WalletConfig;
// Options accepted for the `pczt prove` command
#[derive(Debug, Options)]
pub(crate) struct Command {
#[options(
help = "hex encoding of the Sapling proof generation key",
parse(try_from_str = "hex::decode")
)]
sapling_proof_generation_key: Option<Vec<u8>>,
#[options(
help = "age identity file to decrypt the mnemonic phrase with for deriving the Sapling proof generation key"
)]
identity: Option<String>,
}
impl Command {
pub(crate) async fn run(self, wallet_dir: Option<String>) -> Result<(), anyhow::Error> {
let mut buf = vec![];
stdin().read_to_end(&mut buf).await?;
let pczt = Pczt::parse(&buf).map_err(|e| anyhow!("Failed to read PCZT: {:?}", e))?;
// If we have Sapling spends, we need Sapling proof generation keys.
let pczt = if !pczt.sapling().spends().is_empty() {
enum PgkSource {
Provided(ProofGenerationKey),
Wallet {
config: WalletConfig,
seed_fp: SeedFingerprint,
},
}
impl PgkSource {
fn proof_generation_key(
&self,
derivation: Option<([u8; 32], Vec<zip32::ChildIndex>)>,
) -> anyhow::Result<ProofGenerationKey> {
match self {
PgkSource::Provided(proof_generation_key) => {
Ok(proof_generation_key.clone())
}
PgkSource::Wallet { config, seed_fp } => {
if let Some((seed_fingerprint, derivation_path)) = derivation {
let params = config.network();
if seed_fingerprint == seed_fp.to_bytes()
&& derivation_path.len() == 3
&& derivation_path[0] == zip32::ChildIndex::hardened(32)
&& derivation_path[1]
== zip32::ChildIndex::hardened(
params.network_type().coin_type(),
)
{
let account_index = zip32::AccountId::try_from(
derivation_path[2].index() - (1 << 31),
)
.expect("valid");
let usk = UnifiedSpendingKey::from_seed(
&params,
config
.seed()
.ok_or(anyhow!(
"Seed must be present to enable signing"
))?
.expose_secret(),
account_index,
)?;
Ok(usk.sapling().expsk.proof_generation_key())
} else {
Err(anyhow!(
"Invalid ZIP 32 derivation path for PCZT Sapling spend"
))
}
} else {
Err(anyhow!(
"Missing ZIP 32 derivation path for PCZT Sapling spend"
))
}
}
}
}
}
let pkg_source = match (self.sapling_proof_generation_key, self.identity) {
(Some(proof_generation_key), _) => {
if proof_generation_key.len() == 64 {
Ok(PgkSource::Provided(sapling::keys::ProofGenerationKey {
ak: sapling::keys::SpendValidatingKey::temporary_zcash_from_bytes(
&proof_generation_key[..32],
)
.ok_or_else(|| anyhow!("Invalid Sapling proof generation key"))?,
nsk: jubjub::Scalar::from_bytes(
&proof_generation_key[32..].try_into().unwrap(),
)
.into_option()
.ok_or_else(|| anyhow!("Invalid Sapling proof generation key"))?,
}))
} else {
Err(anyhow!("Invalid Sapling proof generation key"))
}
}
(None, Some(identity)) => {
// Try to load it from the wallet config.
let mut config = WalletConfig::read(wallet_dir.as_ref())?;
// Decrypt the mnemonic to access the seed.
let identities = age::IdentityFile::from_file(identity)?.into_identities()?;
config.decrypt(identities.iter().map(|i| i.as_ref() as _))?;
// Cache the seed fingerprint for matching.
let seed = config
.seed()
.ok_or(anyhow!("Seed must be present to enable signing"))?
.expose_secret();
let seed_fingerprint = SeedFingerprint::from_seed(seed)
.ok_or_else(|| anyhow!("Invalid seed length"))?;
Ok(PgkSource::Wallet {
config,
seed_fp: seed_fingerprint,
})
}
(None, None) => Err(anyhow!(
"Cannot create Sapling proofs without a proof generation key"
)),
}?;
// Add Sapling proof generation key.
Updater::new(pczt)
.update_sapling_with(|mut updater| {
let non_dummy_spends = updater
.bundle()
.spends()
.iter()
.enumerate()
// Dummy spends will already have a proof generation key.
.filter(|(_, spend)| spend.proof_generation_key().is_none())
.map(|(index, spend)| {
(
index,
spend
.zip32_derivation()
.as_ref()
.map(|d| (*d.seed_fingerprint(), d.derivation_path().clone())),
)
})
.collect::<Vec<_>>();
// Assume all non-dummy spent notes are from the same account.
for (index, derivation) in non_dummy_spends {
updater.update_spend_with(index, |mut spend_updater| {
spend_updater.set_proof_generation_key(
pkg_source.proof_generation_key(derivation).unwrap(),
)
})?;
}
Ok(())
})
.map_err(|e| anyhow!("Failed to add Sapling proof generation key: {:?}", e))?
.finish()
} else {
pczt
};
let prover =
LocalTxProver::with_default_location().ok_or(anyhow!("Missing Sapling parameters"))?;
let pczt = Prover::new(pczt)
.create_orchard_proof(&orchard::circuit::ProvingKey::build())
.map_err(|e| anyhow!("Failed to create Orchard proof: {:?}", e))?
.create_sapling_proofs(&prover, &prover)
.map_err(|e| anyhow!("Failed to create Sapling proofs: {:?}", e))?
.finish();
stdout().write_all(&pczt.serialize()).await?;
Ok(())
}
}

90
src/commands/pczt/send.rs Normal file
View File

@ -0,0 +1,90 @@
use anyhow::anyhow;
use gumdrop::Options;
use pczt::Pczt;
use tokio::io::{stdin, AsyncReadExt};
use zcash_client_backend::{
data_api::{wallet::extract_and_store_transaction_from_pczt, WalletRead},
proto::service,
};
use zcash_client_sqlite::WalletDb;
use zcash_proofs::prover::LocalTxProver;
use crate::{
config::WalletConfig,
data::get_db_paths,
error,
remote::{tor_client, Servers},
};
// Options accepted for the `pczt send` command
#[derive(Debug, Options)]
pub(crate) struct Command {
#[options(
help = "the server to send via (default is \"ecc\")",
default = "ecc",
parse(try_from_str = "Servers::parse")
)]
server: Servers,
#[options(help = "disable connections via TOR")]
disable_tor: bool,
}
impl Command {
pub(crate) async fn run(self, wallet_dir: Option<String>) -> Result<(), anyhow::Error> {
let config = WalletConfig::read(wallet_dir.as_ref())?;
let params = config.network();
let (_, db_data) = get_db_paths(wallet_dir.as_ref());
let mut db_data = WalletDb::for_path(db_data, params)?;
let server = self.server.pick(params)?;
let mut client = if self.disable_tor {
server.connect_direct().await?
} else {
server.connect(|| tor_client(wallet_dir.as_ref())).await?
};
let mut buf = vec![];
stdin().read_to_end(&mut buf).await?;
let pczt = Pczt::parse(&buf).map_err(|e| anyhow!("Failed to read PCZT: {:?}", e))?;
let prover =
LocalTxProver::with_default_location().ok_or(anyhow!("Missing Sapling parameters"))?;
let (spend_vk, output_vk) = prover.verifying_keys();
let txid = extract_and_store_transaction_from_pczt::<_, ()>(
&mut db_data,
pczt,
&spend_vk,
&output_vk,
&orchard::circuit::VerifyingKey::build(),
)
.map_err(|e| anyhow!("Failed to extract and store transaction from PCZT: {:?}", e))?;
// Send the transaction.
println!("Sending transaction...");
let (txid, raw_tx) = db_data
.get_transaction(txid)?
.map(|tx| {
let mut raw_tx = service::RawTransaction::default();
tx.write(&mut raw_tx.data).unwrap();
(tx.txid(), raw_tx)
})
.ok_or(anyhow!("Transaction not found for id {:?}", txid))?;
let response = client.send_transaction(raw_tx).await?.into_inner();
if response.error_code != 0 {
Err(error::Error::SendFailed {
code: response.error_code,
reason: response.error_message,
}
.into())
} else {
println!("{}", txid);
Ok(())
}
}
}

206
src/commands/pczt/sign.rs Normal file
View File

@ -0,0 +1,206 @@
use std::collections::BTreeMap;
use anyhow::anyhow;
use gumdrop::Options;
use pczt::{
roles::{signer::Signer, updater::Updater},
Pczt,
};
use secrecy::ExposeSecret;
use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt};
use zcash_keys::keys::UnifiedSpendingKey;
use zcash_primitives::legacy::keys::{NonHardenedChildIndex, TransparentKeyScope};
use zcash_protocol::consensus::{NetworkConstants, Parameters};
use zip32::fingerprint::SeedFingerprint;
use crate::config::WalletConfig;
// Options accepted for the `pczt sign` command
#[derive(Debug, Options)]
pub(crate) struct Command {
#[options(help = "age identity file to decrypt the mnemonic phrase with")]
identity: String,
}
impl Command {
pub(crate) async fn run(self, wallet_dir: Option<String>) -> Result<(), anyhow::Error> {
let mut config = WalletConfig::read(wallet_dir.as_ref())?;
let params = config.network();
let mut buf = vec![];
stdin().read_to_end(&mut buf).await?;
let pczt = Pczt::parse(&buf).map_err(|e| anyhow!("Failed to read PCZT: {:?}", e))?;
// Decrypt the mnemonic to access the seed.
let identities = age::IdentityFile::from_file(self.identity)?.into_identities()?;
config.decrypt(identities.iter().map(|i| i.as_ref() as _))?;
let seed = config
.seed()
.ok_or(anyhow!("Seed must be present to enable signing"))?
.expose_secret();
let seed_fp =
SeedFingerprint::from_seed(seed).ok_or_else(|| anyhow!("Invalid seed length"))?;
// Find all the spends matching our seed. For now as a hack, we use the Updater
// role to access the bundle data we need.
enum KeyRef {
Orchard {
index: usize,
},
Sapling {
index: usize,
},
Transparent {
index: usize,
scope: TransparentKeyScope,
address_index: NonHardenedChildIndex,
},
}
let mut keys = BTreeMap::<zip32::AccountId, Vec<KeyRef>>::new();
let pczt = Updater::new(pczt)
.update_orchard_with(|updater| {
for (index, action) in updater.bundle().actions().iter().enumerate() {
if let Some(derivation) = action.spend().zip32_derivation() {
if derivation.seed_fingerprint() == &seed_fp.to_bytes()
&& derivation.derivation_path().len() == 3
&& derivation.derivation_path()[0] == zip32::ChildIndex::hardened(32)
&& derivation.derivation_path()[1]
== zip32::ChildIndex::hardened(params.network_type().coin_type())
{
let account_index = zip32::AccountId::try_from(
derivation.derivation_path()[2].index() - (1 << 31),
)
.expect("valid");
keys.entry(account_index)
.or_default()
.push(KeyRef::Orchard { index });
}
}
}
Ok(())
})
.expect("no errors")
.update_sapling_with(|updater| {
for (index, spend) in updater.bundle().spends().iter().enumerate() {
if let Some(derivation) = spend.zip32_derivation() {
if derivation.seed_fingerprint() == &seed_fp.to_bytes()
&& derivation.derivation_path().len() == 3
&& derivation.derivation_path()[0] == zip32::ChildIndex::hardened(32)
&& derivation.derivation_path()[1]
== zip32::ChildIndex::hardened(params.network_type().coin_type())
{
let account_index = zip32::AccountId::try_from(
derivation.derivation_path()[2].index() - (1 << 31),
)
.expect("valid");
keys.entry(account_index)
.or_default()
.push(KeyRef::Sapling { index });
}
}
}
Ok(())
})
.expect("no errors")
.update_transparent_with(|updater| {
for (index, input) in updater.bundle().inputs().iter().enumerate() {
for derivation in input.bip32_derivation().values() {
if derivation.seed_fingerprint() == &seed_fp.to_bytes()
&& derivation.derivation_path().len() == 5
&& derivation.derivation_path()[0]
== bip32::ChildNumber::new(32, true).expect("valid")
&& derivation.derivation_path()[1]
== bip32::ChildNumber::new(params.network_type().coin_type(), true)
.expect("valid")
&& derivation.derivation_path()[2].is_hardened()
&& !derivation.derivation_path()[3].is_hardened()
&& !derivation.derivation_path()[4].is_hardened()
{
let account_index = zip32::AccountId::try_from(
derivation.derivation_path()[2].index() - (1 << 31),
)
.expect("valid");
let scope = TransparentKeyScope::custom(
derivation.derivation_path()[3].index(),
)
.expect("valid");
let address_index = NonHardenedChildIndex::from_index(
derivation.derivation_path()[4].index(),
)
.expect("valid");
keys.entry(account_index)
.or_default()
.push(KeyRef::Transparent {
index,
scope,
address_index,
});
}
}
}
Ok(())
})
.expect("no errors")
.finish();
let mut signer =
Signer::new(pczt).map_err(|e| anyhow!("Failed to initialize Signer: {:?}", e))?;
for (account_index, spends) in keys {
let usk = UnifiedSpendingKey::from_seed(&params, seed, account_index)?;
for keyref in spends {
match keyref {
KeyRef::Orchard { index } => {
signer
.sign_orchard(
index,
&orchard::keys::SpendAuthorizingKey::from(usk.orchard()),
)
.map_err(|e| {
anyhow!("Failed to sign Orchard spend {index}: {:?}", e)
})?;
}
KeyRef::Sapling { index } => {
signer
.sign_sapling(index, &usk.sapling().expsk.ask)
.map_err(|e| {
anyhow!("Failed to sign Sapling spend {index}: {:?}", e)
})?;
}
KeyRef::Transparent {
index,
scope,
address_index,
} => signer
.sign_transparent(
index,
&usk.transparent()
.derive_secret_key(scope, address_index)
.map_err(|e| {
anyhow!(
"Failed to derive transparent key at .../{:?}/{:?}: {:?}",
scope,
address_index,
e,
)
})?,
)
.map_err(|e| {
anyhow!("Failed to sign transparent input {index}: {:?}", e)
})?,
}
}
}
let pczt = signer.finish();
stdout().write_all(&pczt.serialize()).await?;
Ok(())
}
}

View File

@ -78,6 +78,9 @@ enum Command {
#[options(help = "send funds to the given address")]
Send(commands::send::Command),
#[options(help = "send funds using PCZTs")]
Pczt(commands::pczt::Command),
}
fn main() -> Result<(), anyhow::Error> {
@ -151,7 +154,14 @@ fn main() -> Result<(), anyhow::Error> {
Some(Command::ListUnspent(command)) => command.run(opts.wallet_dir),
Some(Command::Propose(command)) => command.run(opts.wallet_dir).await,
Some(Command::Send(command)) => command.run(opts.wallet_dir).await,
_ => Ok(()),
Some(Command::Pczt(command)) => match command {
commands::pczt::Command::Create(command) => command.run(opts.wallet_dir).await,
commands::pczt::Command::Prove(command) => command.run(opts.wallet_dir).await,
commands::pczt::Command::Sign(command) => command.run(opts.wallet_dir).await,
commands::pczt::Command::Combine(command) => command.run().await,
commands::pczt::Command::Send(command) => command.run(opts.wallet_dir).await,
},
None => Ok(()),
}
})
}