Merge pull request #68 from Electric-Coin-Company/pczt-improvements

PCZT improvements
This commit is contained in:
Kris Nuttycombe 2024-12-19 08:15:19 -07:00 committed by GitHub
commit 3b9de014c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 645 additions and 107 deletions

49
Cargo.lock generated
View File

@ -1747,8 +1747,7 @@ dependencies = [
[[package]] [[package]]
name = "equihash" name = "equihash"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "ab579d7cf78477773b03e80bc2f89702ef02d7112c711d54ca93dcdce68533d5"
dependencies = [ dependencies = [
"blake2b_simd", "blake2b_simd",
"byteorder", "byteorder",
@ -1799,8 +1798,7 @@ dependencies = [
[[package]] [[package]]
name = "f4jumble" name = "f4jumble"
version = "0.1.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "0d42773cb15447644d170be20231a3268600e0c4cea8987d013b93ac973d3cf7"
dependencies = [ dependencies = [
"blake2b_simd", "blake2b_simd",
] ]
@ -3400,9 +3398,9 @@ dependencies = [
[[package]] [[package]]
name = "nonempty" name = "nonempty"
version = "0.7.0" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d"
[[package]] [[package]]
name = "noop_proc_macro" name = "noop_proc_macro"
@ -3611,8 +3609,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "orchard" name = "orchard"
version = "0.10.1" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/orchard.git?rev=4fa6d3b549f8803016a309281404eab095d04de8#4fa6d3b549f8803016a309281404eab095d04de8"
checksum = "02f7152474406422f572de163e0bc63b2126cdbfe17bc849efbbde36fcfe647e"
dependencies = [ dependencies = [
"aes", "aes",
"bitvec", "bitvec",
@ -3780,8 +3777,7 @@ dependencies = [
[[package]] [[package]]
name = "pczt" name = "pczt"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "545f95dc3d5295b9ed0360aa3b63e610bd4a73f2f382c616b2fda66c3288e989"
dependencies = [ dependencies = [
"blake2b_simd", "blake2b_simd",
"bls12_381", "bls12_381",
@ -4857,8 +4853,7 @@ dependencies = [
[[package]] [[package]]
name = "sapling-crypto" name = "sapling-crypto"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/sapling-crypto.git?rev=3c2235747553da642fb142d1eeb9b1afa8391987#3c2235747553da642fb142d1eeb9b1afa8391987"
checksum = "85c2acdbbab83d554fc2dceea5f7d6d3da71e57adb18a6c80b8901bd0eee54b0"
dependencies = [ dependencies = [
"aes", "aes",
"bellman", "bellman",
@ -7420,8 +7415,7 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]] [[package]]
name = "zcash_address" name = "zcash_address"
version = "0.6.2" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "9b955fe87f2d9052e3729bdbeb0e94975355f4fe39f7d26aea9457bec6a0bb55"
dependencies = [ dependencies = [
"bech32 0.11.0", "bech32 0.11.0",
"bs58", "bs58",
@ -7434,8 +7428,7 @@ dependencies = [
[[package]] [[package]]
name = "zcash_client_backend" name = "zcash_client_backend"
version = "0.16.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "c2a187ad05cdfe13707c07e6aedca8026b34921d081decfd0b43aac1efd438a7"
dependencies = [ dependencies = [
"arti-client", "arti-client",
"base64 0.22.1", "base64 0.22.1",
@ -7497,8 +7490,7 @@ dependencies = [
[[package]] [[package]]
name = "zcash_client_sqlite" name = "zcash_client_sqlite"
version = "0.14.0" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "9b8046e94c3d746cc00e0ceb4ec4263c4fb93271a8681b425365ad600148ab15"
dependencies = [ dependencies = [
"bip32", "bip32",
"bs58", "bs58",
@ -7536,8 +7528,7 @@ dependencies = [
[[package]] [[package]]
name = "zcash_encoding" name = "zcash_encoding"
version = "0.2.2" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "3654116ae23ab67dd1f849b01f8821a8a156f884807ff665eac109bf28306c4d"
dependencies = [ dependencies = [
"core2", "core2",
"nonempty", "nonempty",
@ -7546,8 +7537,7 @@ dependencies = [
[[package]] [[package]]
name = "zcash_keys" name = "zcash_keys"
version = "0.6.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "6ad3cf576c6e6094cd03f446fcb83ad241ec315a088593cd50940f135cb03ce1"
dependencies = [ dependencies = [
"bech32 0.11.0", "bech32 0.11.0",
"bip32", "bip32",
@ -7589,8 +7579,7 @@ dependencies = [
[[package]] [[package]]
name = "zcash_primitives" name = "zcash_primitives"
version = "0.21.0" version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "9b45f3ca3a9df34fcdbf036c2c814417bb417bde742812abc09d744bb3d7ed72"
dependencies = [ dependencies = [
"aes", "aes",
"bip32", "bip32",
@ -7630,8 +7619,7 @@ dependencies = [
[[package]] [[package]]
name = "zcash_proofs" name = "zcash_proofs"
version = "0.21.0" version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "d5826910c516675eca1f34b3557e159f4e35a4a1711b39fa4f01fb0adb9a9c24"
dependencies = [ dependencies = [
"bellman", "bellman",
"blake2b_simd", "blake2b_simd",
@ -7653,8 +7641,7 @@ dependencies = [
[[package]] [[package]]
name = "zcash_protocol" name = "zcash_protocol"
version = "0.4.3" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "82cb36b15b5a1be70b30c32ce40372dead6561df8a467e297f96b892873a63a2"
dependencies = [ dependencies = [
"core2", "core2",
"document-features", "document-features",
@ -7677,8 +7664,7 @@ dependencies = [
[[package]] [[package]]
name = "zcash_transparent" name = "zcash_transparent"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "ed0512e8e02af804e852fbbc4bd5db35a9037bc253d2ce396506293a6e7dd745"
dependencies = [ dependencies = [
"bip32", "bip32",
"blake2b_simd", "blake2b_simd",
@ -7809,8 +7795,7 @@ dependencies = [
[[package]] [[package]]
name = "zip321" name = "zip321"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/librustzcash.git?rev=9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b#9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b"
checksum = "1f3e613defb0940acef1f54774b51c7f48f2fa705613dd800870dc69f35cd2ea"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"nom", "nom",

View File

@ -78,3 +78,17 @@ tui = [
"dep:tokio-util", "dep:tokio-util",
"dep:tui-logger", "dep:tui-logger",
] ]
[patch.crates-io]
orchard = { git = "https://github.com/zcash/orchard.git", rev = "4fa6d3b549f8803016a309281404eab095d04de8" }
pczt = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
sapling = { package = "sapling-crypto", git = "https://github.com/zcash/sapling-crypto.git", rev = "3c2235747553da642fb142d1eeb9b1afa8391987" }
transparent = { package = "zcash_transparent", git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
zip321 = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }

View File

@ -4,6 +4,7 @@ pub(crate) mod combine;
pub(crate) mod create; pub(crate) mod create;
pub(crate) mod inspect; pub(crate) mod inspect;
pub(crate) mod prove; pub(crate) mod prove;
pub(crate) mod redact;
pub(crate) mod send; pub(crate) mod send;
pub(crate) mod shield; pub(crate) mod shield;
pub(crate) mod sign; pub(crate) mod sign;
@ -19,6 +20,8 @@ pub(crate) enum Command {
Shield(shield::Command), Shield(shield::Command),
#[options(help = "inspect a PCZT")] #[options(help = "inspect a PCZT")]
Inspect(inspect::Command), Inspect(inspect::Command),
#[options(help = "redact a PCZT")]
Redact(redact::Command),
#[options(help = "create proofs for a PCZT")] #[options(help = "create proofs for a PCZT")]
Prove(prove::Command), Prove(prove::Command),
#[options(help = "apply signatures to a PCZT")] #[options(help = "apply signatures to a PCZT")]

View File

@ -1,6 +1,7 @@
use anyhow::anyhow; use anyhow::anyhow;
use gumdrop::Options; use gumdrop::Options;
use pczt::{roles::verifier::Verifier, Pczt}; use pczt::{roles::verifier::Verifier, Pczt};
use secrecy::ExposeSecret;
use tokio::io::{stdin, AsyncReadExt}; use tokio::io::{stdin, AsyncReadExt};
use zcash_primitives::transaction::{ use zcash_primitives::transaction::{
sighash::{SighashType, SignableInput}, sighash::{SighashType, SignableInput},
@ -8,18 +9,62 @@ use zcash_primitives::transaction::{
txid::{to_txid, TxIdDigester}, txid::{to_txid, TxIdDigester},
TxVersion, TxVersion,
}; };
use zcash_protocol::consensus::{NetworkConstants, Parameters};
use zip32::fingerprint::SeedFingerprint;
use crate::config::WalletConfig;
// Options accepted for the `pczt inspect` command // Options accepted for the `pczt inspect` command
#[derive(Debug, Options)] #[derive(Debug, Options)]
pub(crate) struct Command {} pub(crate) struct Command {
#[options(
help = "age identity file to decrypt the mnemonic phrase with (if a wallet is provided)"
)]
identity: Option<String>,
}
impl Command { impl Command {
pub(crate) async fn run(self) -> Result<(), anyhow::Error> { pub(crate) async fn run(self, wallet_dir: Option<String>) -> Result<(), anyhow::Error> {
// Allow the user to optionally provide the wallet dir, to inspect the PCZT in the
// context of a wallet.
let mut config = match (WalletConfig::read(wallet_dir.as_ref()), wallet_dir) {
(Err(_), None) => Ok(None),
(res, _) => res.map(Some),
}?;
let mut buf = vec![]; let mut buf = vec![];
stdin().read_to_end(&mut buf).await?; stdin().read_to_end(&mut buf).await?;
let pczt = Pczt::parse(&buf).map_err(|e| anyhow!("Failed to read PCZT: {:?}", e))?; let pczt = Pczt::parse(&buf).map_err(|e| anyhow!("Failed to read PCZT: {:?}", e))?;
let seed_fp = config
.as_mut()
.zip(self.identity)
.map(|(config, identity)| {
// 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 _))?;
let seed = config
.seed()
.ok_or(anyhow!(
"Identity provided for a wallet that doesn't have a seed"
))?
.expose_secret();
SeedFingerprint::from_seed(seed)
.ok_or_else(|| anyhow!("Invalid seed length"))
.map(|seed_fp| {
(
seed_fp,
zip32::ChildIndex::hardened(
config.network().network_type().coin_type(),
),
)
})
})
.transpose()?;
let mut transparent_inputs = vec![]; let mut transparent_inputs = vec![];
let mut transparent_outputs = vec![]; let mut transparent_outputs = vec![];
let mut sapling_spends = vec![]; let mut sapling_spends = vec![];
@ -53,7 +98,19 @@ impl Command {
sapling_outputs = bundle sapling_outputs = bundle
.outputs() .outputs()
.iter() .iter()
.map(|output| (output.user_address().clone(), *output.value())) .map(|output| {
(
output.user_address().clone(),
*output.value(),
output
.zip32_derivation()
.as_ref()
.zip(seed_fp.as_ref())
.and_then(|(derivation, (seed_fp, coin_type))| {
derivation.extract_account_index(seed_fp, *coin_type)
}),
)
})
.collect(); .collect();
Ok::<_, pczt::roles::verifier::SaplingError<()>>(()) Ok::<_, pczt::roles::verifier::SaplingError<()>>(())
}) })
@ -67,6 +124,14 @@ impl Command {
*action.spend().value(), *action.spend().value(),
action.output().user_address().clone(), action.output().user_address().clone(),
*action.output().value(), *action.output().value(),
action
.output()
.zip32_derivation()
.as_ref()
.zip(seed_fp.as_ref())
.and_then(|(derivation, (seed_fp, coin_type))| {
derivation.extract_account_index(seed_fp, *coin_type)
}),
) )
}) })
.collect(); .collect();
@ -129,29 +194,40 @@ impl Command {
if !pczt.sapling().outputs().is_empty() { if !pczt.sapling().outputs().is_empty() {
println!("{} Sapling outputs", pczt.sapling().outputs().len()); println!("{} Sapling outputs", pczt.sapling().outputs().len());
for (index, (user_address, value)) in sapling_outputs.iter().enumerate() { for (index, (user_address, value, account_index)) in sapling_outputs.iter().enumerate()
{
if let Some(value) = value { if let Some(value) = value {
if value.inner() == 0 { if value.inner() == 0 {
println!("- {index}: Zero value (likely a dummy)"); println!("- {index}: Zero value (likely a dummy)");
} else { } else {
println!( println!(
"- {index}: {} zatoshis{}", "- {index}: {} zatoshis{}{}",
value.inner(), value.inner(),
match user_address { match user_address {
Some(addr) => format!(" to {addr}"), Some(addr) => format!(" to {addr}"),
None => "".into(), None => "".into(),
},
match account_index {
Some(idx) =>
format!(" (change to ZIP 32 account index {})", u32::from(*idx)),
None => "".into(),
} }
); );
} }
} else if let Some(addr) = user_address { } else if let Some(addr) = user_address {
println!("- {index}: {addr}"); println!("- {index}: {addr}");
} else if let Some(idx) = account_index {
println!(
"- {index}: change to ZIP 32 account index {}",
u32::from(*idx)
);
} }
} }
} }
if !pczt.orchard().actions().is_empty() { if !pczt.orchard().actions().is_empty() {
println!("{} Orchard actions:", pczt.orchard().actions().len()); println!("{} Orchard actions:", pczt.orchard().actions().len());
for (index, (spend_value, output_user_address, output_value)) in for (index, (spend_value, output_user_address, output_value, output_account_index)) in
orchard_actions.iter().enumerate() orchard_actions.iter().enumerate()
{ {
println!("- {index}:"); println!("- {index}:");
@ -167,16 +243,26 @@ impl Command {
println!(" - Output: Zero value (likely a dummy)"); println!(" - Output: Zero value (likely a dummy)");
} else { } else {
println!( println!(
" - Output: {} zatoshis{}", " - Output: {} zatoshis{}{}",
value.inner(), value.inner(),
match output_user_address { match output_user_address {
Some(addr) => format!(" to {addr}"), Some(addr) => format!(" to {addr}"),
None => "".into(), None => "".into(),
},
match output_account_index {
Some(idx) =>
format!(" (change to ZIP 32 account index {})", u32::from(*idx)),
None => "".into(),
} }
); );
} }
} else if let Some(addr) = output_user_address { } else if let Some(addr) = output_user_address {
println!(" - Output: {addr}"); println!(" - Output: {addr}");
} else if let Some(idx) = output_account_index {
println!(
"- {index}: change to ZIP 32 account index {}",
u32::from(*idx)
);
} }
} }
} }

472
src/commands/pczt/redact.rs Normal file
View File

@ -0,0 +1,472 @@
use anyhow::anyhow;
use gumdrop::Options;
use pczt::{
roles::redactor::{
orchard::ActionRedactor,
sapling::{OutputRedactor as SaplingOutputRedactor, SpendRedactor},
transparent::{InputRedactor, OutputRedactor as TransparentOutputRedactor},
Redactor,
},
Pczt,
};
use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt};
// Options accepted for the `pczt redact` command
#[derive(Debug, Options)]
pub(crate) struct Command {
#[options(help = "a list of PCZT keys to redact, in foo.bar.baz notation")]
key: Vec<String>,
}
impl Command {
pub(crate) async fn run(self) -> 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))?;
let pczt = self
.key
.into_iter()
.try_fold(Redactor::new(pczt), |r, key| {
redact_pczt(r, key.split('.')).map_err(|e| anyhow!("Failed to redact '{key}': {e}"))
})?
.finish();
stdout().write_all(&pczt.serialize()).await?;
Ok(())
}
}
fn redact_pczt<'a>(
r: Redactor,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
match key.next() {
Some("global") => redact_global(r, key),
Some("transparent") => redact_transparent(r, key),
Some("sapling") => redact_sapling(r, key),
Some("orchard") => redact_orchard(r, key),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}
fn redact_global<'a>(
r: Redactor,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
match key.next() {
Some(
field @ ("tx_version"
| "version_group_id"
| "consensus_branch_id"
| "fallback_lock_time"
| "expiry_height"
| "coin_type"
| "tx_modifiable"),
) => Err(anyhow!("Cannot redact '{}'", field)),
Some("proprietary") => Ok(r.redact_global_with(|mut global| match key.next() {
Some(key) => global.redact_proprietary(key),
None => global.clear_proprietary(),
})),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}
fn redact_transparent<'a>(
r: Redactor,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
match key.next() {
Some("inputs") => redact_transparent_input(r, None, key),
Some("outputs") => redact_transparent_output(r, None, key),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}
fn redact_transparent_input<'a>(
r: Redactor,
index: Option<usize>,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
fn redact<F>(r: Redactor, index: Option<usize>, f: F) -> Redactor
where
F: FnMut(InputRedactor),
{
r.redact_transparent_with(|mut transparent| match index {
Some(index) => transparent.redact_input(index, f),
None => transparent.redact_inputs(f),
})
}
fn redact_map<F, const N: usize>(
r: Redactor,
index: Option<usize>,
key: &str,
mut f: F,
) -> Result<Redactor, anyhow::Error>
where
F: FnMut(InputRedactor, [u8; N]),
{
match hex::decode(key) {
Ok(key) => match key[..].try_into() {
Ok(key) => Ok(redact(r, index, |input| f(input, key))),
_ => Err(anyhow!("Invalid map key length")),
},
_ => Err(anyhow!("Invalid hex '{key}'")),
}
}
match key.next() {
Some(
field @ ("prevout_txid"
| "prevout_index"
| "sequence"
| "required_time_lock_time"
| "required_height_lock_time"
| "value"
| "script_pubkey"
| "sighash_type"),
) => Err(anyhow!("Cannot redact '{}'", field)),
Some("script_sig") => Ok(redact(r, index, |mut output| output.clear_script_sig())),
Some("redeem_script") => Ok(redact(r, index, |mut output| output.clear_redeem_script())),
Some("partial_signatures") => match key.next() {
Some(pubkey) => redact_map(r, index, pubkey, |mut input, pubkey| {
input.redact_partial_signature(pubkey)
}),
None => Ok(redact(r, index, |mut input| {
input.clear_partial_signatures()
})),
},
Some("bip32_derivation") => match key.next() {
Some(pubkey) => redact_map(r, index, pubkey, |mut input, pubkey| {
input.redact_bip32_derivation(pubkey)
}),
None => Ok(redact(r, index, |mut input| input.clear_bip32_derivation())),
},
Some("ripemd160_preimages") => match key.next() {
Some(pubkey) => redact_map(r, index, pubkey, |mut input, pubkey| {
input.redact_ripemd160_preimage(pubkey)
}),
None => Ok(redact(r, index, |mut input| {
input.clear_ripemd160_preimages()
})),
},
Some("sha256_preimages") => match key.next() {
Some(pubkey) => redact_map(r, index, pubkey, |mut input, pubkey| {
input.redact_sha256_preimage(pubkey)
}),
None => Ok(redact(r, index, |mut input| input.clear_sha256_preimages())),
},
Some("hash160_preimages") => match key.next() {
Some(pubkey) => redact_map(r, index, pubkey, |mut input, pubkey| {
input.redact_hash160_preimage(pubkey)
}),
None => Ok(redact(r, index, |mut input| {
input.clear_hash160_preimages()
})),
},
Some("hash256_preimages") => match key.next() {
Some(pubkey) => redact_map(r, index, pubkey, |mut input, pubkey| {
input.redact_hash256_preimage(pubkey)
}),
None => Ok(redact(r, index, |mut input| {
input.clear_hash256_preimages()
})),
},
Some("proprietary") => Ok(redact(r, index, |mut input| match key.next() {
Some(key) => input.redact_proprietary(key),
None => input.clear_proprietary(),
})),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}
fn redact_transparent_output<'a>(
r: Redactor,
index: Option<usize>,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
fn redact<F>(r: Redactor, index: Option<usize>, f: F) -> Redactor
where
F: FnMut(TransparentOutputRedactor),
{
r.redact_transparent_with(|mut transparent| match index {
Some(index) => transparent.redact_output(index, f),
None => transparent.redact_outputs(f),
})
}
match key.next() {
Some(field @ ("value" | "script_pubkey")) => Err(anyhow!("Cannot redact '{}'", field)),
Some("redeem_script") => Ok(redact(r, index, |mut output| output.clear_redeem_script())),
Some("bip32_derivation") => match key.next() {
Some(data) => match hex::decode(data) {
Ok(pubkey) => match pubkey[..].try_into() {
Ok(pubkey) => Ok(redact(r, index, |mut output| {
output.redact_bip32_derivation(pubkey)
})),
_ => Err(anyhow!("Invalid pubkey length")),
},
_ => Err(anyhow!("Invalid hex pubkey '{data}'")),
},
None => Ok(redact(r, index, |mut output| {
output.clear_bip32_derivation()
})),
},
Some("user_address") => Ok(redact(r, index, |mut output| output.clear_user_address())),
Some("proprietary") => Ok(redact(r, index, |mut output| match key.next() {
Some(key) => output.redact_proprietary(key),
None => output.clear_proprietary(),
})),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}
fn redact_sapling<'a>(
r: Redactor,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
match key.next() {
Some("spends") => redact_sapling_spend(r, None, key),
Some("spend") => match key.next() {
Some(index) => match index.parse() {
Ok(index) => redact_sapling_spend(r, Some(index), key),
Err(_) => Err(anyhow!("Invalid index '{index}'")),
},
None => Err(anyhow!("Missing index")),
},
Some("outputs") => redact_sapling_output(r, None, key),
Some("output") => match key.next() {
Some(index) => match index.parse() {
Ok(index) => redact_sapling_output(r, Some(index), key),
Err(_) => Err(anyhow!("Invalid index '{index}'")),
},
None => Err(anyhow!("Missing index")),
},
Some(field @ ("value_sum" | "anchor")) => Err(anyhow!("Cannot redact '{}'", field)),
Some("bsk") => Ok(r.redact_sapling_with(|mut sapling| sapling.clear_bsk())),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}
fn redact_sapling_spend<'a>(
r: Redactor,
index: Option<usize>,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
fn redact<F>(r: Redactor, index: Option<usize>, f: F) -> Redactor
where
F: FnMut(SpendRedactor),
{
r.redact_sapling_with(|mut sapling| match index {
Some(index) => sapling.redact_spend(index, f),
None => sapling.redact_spends(f),
})
}
match key.next() {
Some(field @ ("cv" | "nullifier" | "rk")) => Err(anyhow!("Cannot redact '{}'", field)),
Some("zkproof") => Ok(redact(r, index, |mut spend| spend.clear_zkproof())),
Some("spend_auth_sig") => Ok(redact(r, index, |mut spend| spend.clear_spend_auth_sig())),
Some("recipient") => Ok(redact(r, index, |mut spend| spend.clear_recipient())),
Some("value") => Ok(redact(r, index, |mut spend| spend.clear_value())),
Some("rcm") => Ok(redact(r, index, |mut spend| spend.clear_rcm())),
Some("rseed") => Ok(redact(r, index, |mut spend| spend.clear_rseed())),
Some("rcv") => Ok(redact(r, index, |mut spend| spend.clear_rcv())),
Some("proof_generation_key") => Ok(redact(r, index, |mut spend| {
spend.clear_proof_generation_key()
})),
Some("witness") => Ok(redact(r, index, |mut spend| spend.clear_witness())),
Some("alpha") => Ok(redact(r, index, |mut spend| spend.clear_alpha())),
Some("zip32_derivation") => {
Ok(redact(r, index, |mut spend| spend.clear_zip32_derivation()))
}
Some("dummy_ask") => Ok(redact(r, index, |mut spend| spend.clear_dummy_ask())),
Some("proprietary") => Ok(redact(r, index, |mut spend| match key.next() {
Some(key) => spend.redact_proprietary(key),
None => spend.clear_proprietary(),
})),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}
fn redact_sapling_output<'a>(
r: Redactor,
index: Option<usize>,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
fn redact<F>(r: Redactor, index: Option<usize>, f: F) -> Redactor
where
F: FnMut(SaplingOutputRedactor),
{
r.redact_sapling_with(|mut sapling| match index {
Some(index) => sapling.redact_output(index, f),
None => sapling.redact_outputs(f),
})
}
match key.next() {
Some(field @ ("cv" | "cmu" | "ephemeral_key" | "enc_ciphertext" | "out_ciphertext")) => {
Err(anyhow!("Cannot redact '{}'", field))
}
Some("zkproof") => Ok(redact(r, index, |mut output| output.clear_zkproof())),
Some("recipient") => Ok(redact(r, index, |mut output| output.clear_recipient())),
Some("value") => Ok(redact(r, index, |mut output| output.clear_value())),
Some("rseed") => Ok(redact(r, index, |mut output| output.clear_rseed())),
Some("rcv") => Ok(redact(r, index, |mut output| output.clear_rcv())),
Some("ock") => Ok(redact(r, index, |mut output| output.clear_ock())),
Some("zip32_derivation") => Ok(redact(r, index, |mut output| {
output.clear_zip32_derivation()
})),
Some("user_address") => Ok(redact(r, index, |mut output| output.clear_user_address())),
Some("proprietary") => Ok(redact(r, index, |mut output| match key.next() {
Some(key) => output.redact_proprietary(key),
None => output.clear_proprietary(),
})),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}
fn redact_orchard<'a>(
r: Redactor,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
match key.next() {
Some("actions") => redact_orchard_action(r, None, key),
Some("action") => match key.next() {
Some(index) => match index.parse() {
Ok(index) => redact_orchard_action(r, Some(index), key),
Err(_) => Err(anyhow!("Invalid index '{index}'")),
},
None => Err(anyhow!("Missing index")),
},
Some(field @ ("flags" | "value_sum" | "anchor")) => {
Err(anyhow!("Cannot redact '{}'", field))
}
Some("zkproof") => Ok(r.redact_orchard_with(|mut orchard| orchard.clear_zkproof())),
Some("bsk") => Ok(r.redact_orchard_with(|mut orchard| orchard.clear_bsk())),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}
fn redact_orchard_action<'a>(
r: Redactor,
index: Option<usize>,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
fn redact<F>(r: Redactor, index: Option<usize>, f: F) -> Redactor
where
F: FnMut(ActionRedactor),
{
r.redact_orchard_with(|mut orchard| match index {
Some(index) => orchard.redact_action(index, f),
None => orchard.redact_actions(f),
})
}
match key.next() {
Some(field @ "cv_net") => Err(anyhow!("Cannot redact '{}'", field)),
Some("spend") => redact_orchard_spend(r, index, key),
Some("output") => redact_orchard_output(r, index, key),
Some("rcv") => Ok(redact(r, index, |mut action| action.clear_rcv())),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}
fn redact_orchard_spend<'a>(
r: Redactor,
index: Option<usize>,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
fn redact<F>(r: Redactor, index: Option<usize>, f: F) -> Redactor
where
F: FnMut(ActionRedactor),
{
r.redact_orchard_with(|mut orchard| match index {
Some(index) => orchard.redact_action(index, f),
None => orchard.redact_actions(f),
})
}
match key.next() {
Some(field @ ("nullifier" | "rk")) => Err(anyhow!("Cannot redact '{}'", field)),
Some("spend_auth_sig") => Ok(redact(r, index, |mut action| action.clear_spend_auth_sig())),
Some("recipient") => Ok(redact(r, index, |mut action| {
action.clear_spend_recipient()
})),
Some("value") => Ok(redact(r, index, |mut action| action.clear_spend_value())),
Some("rho") => Ok(redact(r, index, |mut action| action.clear_spend_rho())),
Some("rseed") => Ok(redact(r, index, |mut action| action.clear_spend_rseed())),
Some("fvk") => Ok(redact(r, index, |mut action| action.clear_spend_fvk())),
Some("witness") => Ok(redact(r, index, |mut action| action.clear_spend_witness())),
Some("alpha") => Ok(redact(r, index, |mut action| action.clear_spend_alpha())),
Some("zip32_derivation") => Ok(redact(r, index, |mut action| {
action.clear_spend_zip32_derivation()
})),
Some("dummy_sk") => Ok(redact(r, index, |mut action| action.clear_spend_dummy_sk())),
Some("proprietary") => Ok(r.redact_orchard_with(|mut orchard| match index {
Some(index) => orchard.redact_action(index, |mut action| match key.next() {
Some(key) => action.redact_spend_proprietary(key),
None => action.clear_spend_proprietary(),
}),
None => orchard.redact_actions(|mut actions| match key.next() {
Some(key) => actions.redact_spend_proprietary(key),
None => actions.clear_spend_proprietary(),
}),
})),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}
fn redact_orchard_output<'a>(
r: Redactor,
index: Option<usize>,
mut key: impl Iterator<Item = &'a str>,
) -> Result<Redactor, anyhow::Error> {
fn redact<F>(r: Redactor, index: Option<usize>, f: F) -> Redactor
where
F: FnMut(ActionRedactor),
{
r.redact_orchard_with(|mut orchard| match index {
Some(index) => orchard.redact_action(index, f),
None => orchard.redact_actions(f),
})
}
match key.next() {
Some(field @ ("cmx" | "ephemeral_key" | "enc_ciphertext" | "out_ciphertext")) => {
Err(anyhow!("Cannot redact '{}'", field))
}
Some("recipient") => Ok(redact(r, index, |mut action| {
action.clear_output_recipient()
})),
Some("value") => Ok(redact(r, index, |mut action| action.clear_output_value())),
Some("rseed") => Ok(redact(r, index, |mut action| action.clear_output_rseed())),
Some("ock") => Ok(redact(r, index, |mut action| action.clear_output_ock())),
Some("zip32_derivation") => Ok(redact(r, index, |mut action| {
action.clear_output_zip32_derivation()
})),
Some("user_address") => Ok(redact(r, index, |mut action| {
action.clear_output_user_address()
})),
Some("proprietary") => Ok(redact(r, index, |mut action| match key.next() {
Some(key) => action.redact_output_proprietary(key),
None => action.clear_output_proprietary(),
})),
Some(field) => Err(anyhow!("Unknown field '{}'", field)),
None => Err(anyhow!("Empty field")),
}
}

View File

@ -1,9 +1,9 @@
use std::collections::BTreeMap; use std::{collections::BTreeMap, convert::Infallible};
use anyhow::anyhow; use anyhow::anyhow;
use gumdrop::Options; use gumdrop::Options;
use pczt::{ use pczt::{
roles::{signer::Signer, updater::Updater}, roles::{signer::Signer, verifier::Verifier},
Pczt, Pczt,
}; };
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
@ -46,8 +46,7 @@ impl Command {
let seed_fp = let seed_fp =
SeedFingerprint::from_seed(seed).ok_or_else(|| anyhow!("Invalid seed length"))?; 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 // Find all the spends matching our seed.
// role to access the bundle data we need.
enum KeyRef { enum KeyRef {
Orchard { Orchard {
index: usize, index: usize,
@ -62,80 +61,58 @@ impl Command {
}, },
} }
let mut keys = BTreeMap::<zip32::AccountId, Vec<KeyRef>>::new(); let mut keys = BTreeMap::<zip32::AccountId, Vec<KeyRef>>::new();
let pczt = Updater::new(pczt) let pczt = Verifier::new(pczt)
.update_orchard_with(|updater| { .with_orchard::<Infallible, _>(|bundle| {
for (index, action) in updater.bundle().actions().iter().enumerate() { for (index, action) in bundle.actions().iter().enumerate() {
if let Some(derivation) = action.spend().zip32_derivation() { if let Some(account_index) = action
if derivation.seed_fingerprint() == &seed_fp.to_bytes() .spend()
&& derivation.derivation_path().len() == 3 .zip32_derivation()
&& derivation.derivation_path()[0] == zip32::ChildIndex::hardened(32) .as_ref()
&& derivation.derivation_path()[1] .and_then(|derivation| {
== zip32::ChildIndex::hardened(params.network_type().coin_type()) derivation.extract_account_index(
{ &seed_fp,
let account_index = zip32::AccountId::try_from( zip32::ChildIndex::hardened(params.network_type().coin_type()),
derivation.derivation_path()[2].index() - (1 << 31),
) )
.expect("valid"); })
{
keys.entry(account_index) keys.entry(account_index)
.or_default() .or_default()
.push(KeyRef::Orchard { index }); .push(KeyRef::Orchard { index });
}
} }
} }
Ok(()) Ok(())
}) })
.expect("no errors") .map_err(|e| anyhow!("Invalid PCZT: {:?}", e))?
.update_sapling_with(|updater| { .with_sapling::<Infallible, _>(|bundle| {
for (index, spend) in updater.bundle().spends().iter().enumerate() { for (index, spend) in bundle.spends().iter().enumerate() {
if let Some(derivation) = spend.zip32_derivation() { if let Some(account_index) =
if derivation.seed_fingerprint() == &seed_fp.to_bytes() spend.zip32_derivation().as_ref().and_then(|derivation| {
&& derivation.derivation_path().len() == 3 derivation.extract_account_index(
&& derivation.derivation_path()[0] == zip32::ChildIndex::hardened(32) &seed_fp,
&& derivation.derivation_path()[1] zip32::ChildIndex::hardened(params.network_type().coin_type()),
== 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) keys.entry(account_index)
.or_default() .or_default()
.push(KeyRef::Sapling { index }); .push(KeyRef::Sapling { index });
}
} }
} }
Ok(()) Ok(())
}) })
.expect("no errors") .map_err(|e| anyhow!("Invalid PCZT: {:?}", e))?
.update_transparent_with(|updater| { .with_transparent::<Infallible, _>(|bundle| {
for (index, input) in updater.bundle().inputs().iter().enumerate() { for (index, input) in bundle.inputs().iter().enumerate() {
for derivation in input.bip32_derivation().values() { for derivation in input.bip32_derivation().values() {
if derivation.seed_fingerprint() == &seed_fp.to_bytes() if let Some((account_index, scope, address_index)) = derivation
&& derivation.derivation_path().len() == 5 .extract_bip_44_fields(
&& derivation.derivation_path()[0] &seed_fp,
== bip32::ChildNumber::new(44, true).expect("valid") bip32::ChildNumber(
&& derivation.derivation_path()[1] params.network_type().coin_type()
== bip32::ChildNumber::new(params.network_type().coin_type(), true) | bip32::ChildNumber::HARDENED_FLAG,
.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())
.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) keys.entry(account_index)
.or_default() .or_default()
.push(KeyRef::Transparent { .push(KeyRef::Transparent {
@ -148,7 +125,7 @@ impl Command {
} }
Ok(()) Ok(())
}) })
.expect("no errors") .map_err(|e| anyhow!("Invalid PCZT: {:?}", e))?
.finish(); .finish();
let mut signer = let mut signer =

View File

@ -174,7 +174,8 @@ fn main() -> Result<(), anyhow::Error> {
Some(Command::Pczt(command)) => match command { Some(Command::Pczt(command)) => match command {
commands::pczt::Command::Create(command) => command.run(opts.wallet_dir).await, commands::pczt::Command::Create(command) => command.run(opts.wallet_dir).await,
commands::pczt::Command::Shield(command) => command.run(opts.wallet_dir).await, commands::pczt::Command::Shield(command) => command.run(opts.wallet_dir).await,
commands::pczt::Command::Inspect(command) => command.run().await, commands::pczt::Command::Inspect(command) => command.run(opts.wallet_dir).await,
commands::pczt::Command::Redact(command) => command.run().await,
commands::pczt::Command::Prove(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::Sign(command) => command.run(opts.wallet_dir).await,
commands::pczt::Command::Combine(command) => command.run().await, commands::pczt::Command::Combine(command) => command.run().await,