From 40d7fe0eff43918b2228121c0db6016b4d9e5719 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 1 Oct 2025 19:56:44 +0800 Subject: [PATCH] pczt: Add `update-with-signature` and `extract` commands --- src/commands/pczt.rs | 6 + src/commands/pczt/extract.rs | 36 ++++++ src/commands/pczt/update_with_signature.rs | 136 +++++++++++++++++++++ src/main.rs | 2 + 4 files changed, 180 insertions(+) create mode 100644 src/commands/pczt/extract.rs create mode 100644 src/commands/pczt/update_with_signature.rs diff --git a/src/commands/pczt.rs b/src/commands/pczt.rs index 1a67dbd..378430d 100644 --- a/src/commands/pczt.rs +++ b/src/commands/pczt.rs @@ -4,6 +4,7 @@ pub(crate) mod combine; pub(crate) mod create; pub(crate) mod create_manual; pub(crate) mod create_max; +pub(crate) mod extract; pub(crate) mod inspect; pub(crate) mod pay_manual; pub(crate) mod prove; @@ -13,6 +14,7 @@ pub(crate) mod send_without_storing; pub(crate) mod shield; pub(crate) mod sign; pub(crate) mod update_with_derivation; +pub(crate) mod update_with_signature; #[cfg(feature = "pczt-qr")] pub(crate) mod qr; @@ -40,8 +42,12 @@ pub(crate) enum Command { Prove(prove::Command), /// Apply signatures to a PCZT Sign(sign::Command), + /// Adds an externally-created signature to a PCZT + UpdateWithSignature(update_with_signature::Command), /// Combine two PCZTs Combine(combine::Command), + /// Extract a finished transaction from a PCZT + Extract(extract::Command), /// Extract a finished transaction and send it Send(send::Command), /// Extract a finished transaction and send it, without storing in the wallet. diff --git a/src/commands/pczt/extract.rs b/src/commands/pczt/extract.rs new file mode 100644 index 0000000..b92f785 --- /dev/null +++ b/src/commands/pczt/extract.rs @@ -0,0 +1,36 @@ +use anyhow::anyhow; +use clap::Args; +use pczt::{ + Pczt, + roles::{spend_finalizer::SpendFinalizer, tx_extractor::TransactionExtractor}, +}; +use tokio::io::{AsyncReadExt, stdin}; +use zcash_proofs::prover::LocalTxProver; + +// Options accepted for the `pczt extract` command +#[derive(Debug, Args)] +pub(crate) struct Command {} + +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 prover = LocalTxProver::bundled(); + let (spend_vk, output_vk) = prover.verifying_keys(); + + let finalized = SpendFinalizer::new(pczt) + .finalize_spends() + .map_err(|e| anyhow!("Failed to finalize PCZT spends: {e:?}"))?; + + let tx = TransactionExtractor::new(finalized) + .with_sapling(&spend_vk, &output_vk) + .extract() + .map_err(|e| anyhow!("Failed to extract transaction from PCZT: {e:?}"))?; + let txid = tx.txid(); + println!("{txid}"); + Ok(()) + } +} diff --git a/src/commands/pczt/update_with_signature.rs b/src/commands/pczt/update_with_signature.rs new file mode 100644 index 0000000..1873068 --- /dev/null +++ b/src/commands/pczt/update_with_signature.rs @@ -0,0 +1,136 @@ +use anyhow::anyhow; +use blake2b_simd::Hash as Blake2bHash; +use clap::Args; +use pczt::{ + Pczt, + roles::{low_level_signer, signer::EffectsOnly}, +}; +use tokio::io::{AsyncReadExt, AsyncWriteExt, stdin, stdout}; +use zcash_primitives::transaction::{ + TransactionData, TxDigests, sighash::SignableInput, sighash_v5::v5_signature_hash, + txid::TxIdDigester, +}; +use zcash_protocol::PoolType; + +// Options accepted for the `pczt update-with-signature` command +#[derive(Debug, Args)] +pub(crate) struct Command { + /// The pool that the signature is for. + #[arg(value_parser = parse_pool_type)] + pool: PoolType, + + /// The index of the transparent input or shielded spend that the signature is for. + index: usize, + + /// The hex-encoded signature. + signature: String, +} + +impl Command { + pub(crate) async fn run(self) -> anyhow::Result<()> { + let sig_bytes = hex::decode(self.signature)?; + + 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 tx_data = pczt.clone().into_effects().map_err(|e| anyhow!("{e:?}"))?; + let txid_parts = tx_data.digest(TxIdDigester); + + let signer = low_level_signer::Signer::new(pczt); + + let signer = match self.pool { + PoolType::Transparent => { + add_transparent(signer, &tx_data, &txid_parts, self.index, sig_bytes) + } + PoolType::SAPLING => Err(anyhow!("TODO: Maybe support this")), + PoolType::ORCHARD => Err(anyhow!("TODO: Maybe support this")), + } + .map_err(|e| anyhow!("{e:?}"))?; + + let pczt = signer.finish(); + + stdout().write_all(&pczt.serialize()).await?; + + Ok(()) + } +} + +fn parse_pool_type(s: &str) -> anyhow::Result { + match s { + "transparent" => Ok(PoolType::Transparent), + "sapling" => Ok(PoolType::SAPLING), + "orchard" => Ok(PoolType::ORCHARD), + _ => Err(anyhow!( + "Invalid pool type '{s}', must be one of ['transparent', 'sapling', 'orchard']" + )), + } +} + +fn add_transparent( + signer: low_level_signer::Signer, + tx_data: &TransactionData, + txid_parts: &TxDigests, + index: usize, + sig_bytes: Vec, +) -> anyhow::Result { + // Signature has to have the SighashType appended to it. + let (sighash_type, sig_der) = sig_bytes + .split_last() + .ok_or_else(|| anyhow!("Invalid signature bytes"))?; + let sig = secp256k1::ecdsa::Signature::from_der(sig_der) + .map_err(|_| anyhow!("Invalid signature bytes"))?; + + let mut found = false; + + let signer = signer + .sign_transparent_with(|_, bundle, _| { + if let Some(input) = bundle.inputs_mut().get_mut(index) { + found = true; + if *sighash_type != input.sighash_type().encode() { + return Err(TempError::Parser( + transparent::pczt::ParseError::InvalidSighashType, + )); + } + input.append_signature( + index, + |input| { + v5_signature_hash(tx_data, &SignableInput::Transparent(input), txid_parts) + .as_ref() + .try_into() + .unwrap() + }, + sig, + &secp256k1::Secp256k1::verification_only(), + )?; + } + Ok::<_, TempError>(()) + }) + .map_err(|e| anyhow!("{e:?}"))?; + + if found { + Ok(signer) + } else { + Err(anyhow!("No inputs matched the given derivation path")) + } +} + +#[allow(unused)] +#[derive(Debug)] +enum TempError { + Parser(transparent::pczt::ParseError), + Signer(transparent::pczt::SignerError), +} + +impl From for TempError { + fn from(e: transparent::pczt::ParseError) -> Self { + Self::Parser(e) + } +} + +impl From for TempError { + fn from(e: transparent::pczt::SignerError) -> Self { + Self::Signer(e) + } +} diff --git a/src/main.rs b/src/main.rs index 0132beb..340a7df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -199,7 +199,9 @@ fn main() -> Result<(), anyhow::Error> { commands::pczt::Command::Redact(command) => command.run().await, commands::pczt::Command::Prove(command) => command.run(wallet_dir).await, commands::pczt::Command::Sign(command) => command.run(wallet_dir).await, + commands::pczt::Command::UpdateWithSignature(command) => command.run().await, commands::pczt::Command::Combine(command) => command.run().await, + commands::pczt::Command::Extract(command) => command.run().await, commands::pczt::Command::Send(command) => command.run(wallet_dir).await, commands::pczt::Command::SendWithoutStoring(command) => { command.run(wallet_dir).await