From 555bf581fd9731fde9f680627981a855da99c86d Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 28 Nov 2024 09:20:24 +0000 Subject: [PATCH] Implement PCZT support --- CHANGELOG.md | 10 ++ Cargo.lock | 39 +++++- Cargo.toml | 3 + src/builder.rs | 288 +++++++++++++++++++++++++++++--------- src/bundle.rs | 10 ++ src/keys.rs | 4 +- src/lib.rs | 1 + src/pczt.rs | 294 ++++++++++++++++++++++++++++++++++++++ src/pczt/io_finalizer.rs | 91 ++++++++++++ src/pczt/parse.rs | 295 +++++++++++++++++++++++++++++++++++++++ src/pczt/prover.rs | 102 ++++++++++++++ src/pczt/signer.rs | 38 +++++ src/pczt/tx_extractor.rs | 165 ++++++++++++++++++++++ src/tree.rs | 4 + src/value/sums.rs | 14 ++ 15 files changed, 1290 insertions(+), 68 deletions(-) create mode 100644 src/pczt.rs create mode 100644 src/pczt/io_finalizer.rs create mode 100644 src/pczt/parse.rs create mode 100644 src/pczt/prover.rs create mode 100644 src/pczt/signer.rs create mode 100644 src/pczt/tx_extractor.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 95bd5e2..5272ac8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this library adheres to Rust's notion of ## [Unreleased] +### Added +- Support for Partially-Created Zcash Transactions: + - `sapling_crypto::builder::Builder::build_for_pczt` + - `sapling_crypto::pczt` module. +- `sapling_crypto::bundle::EffectsOnly` +- `sapling_crypto::keys`: + - `SpendAuthorizingKey::to_bytes` + - `SpendValidatingKey::to_bytes` +- `sapling_crypto::value::ValueSum::to_raw` + ### Fixed - `sapling_crypto::prover::OutputProver::prepare_circuit` now takes `esk` as an `sapling_crypto::keys::EphemeralSecretKey`, matching the existing public APIs diff --git a/Cargo.lock b/Cargo.lock index b5ab48d..2f9358b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,6 +566,18 @@ dependencies = [ "wasi", ] +[[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", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1029,6 +1041,28 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.70" @@ -1287,6 +1321,7 @@ dependencies = [ "document-features", "ff", "fpe", + "getset", "group", "hex", "incrementalmerkletree", @@ -1369,9 +1404,9 @@ checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symbolic-common" diff --git a/Cargo.toml b/Cargo.toml index b68f8d2..b74cbcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ jubjub = "0.10" redjubjub = "0.7" zcash_spec = "0.1" +# Boilerplate +getset = "0.1" + # Circuits bellman = { version = "0.14", default-features = false, features = ["groth16"] } diff --git a/src/builder.rs b/src/builder.rs index 2b32c50..93459ad 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,20 +1,22 @@ //! Types and functions for building Sapling transaction components. use core::fmt; -use std::{iter, marker::PhantomData}; +use std::{collections::BTreeMap, iter, marker::PhantomData}; use group::ff::Field; use incrementalmerkletree::Position; use rand::{seq::SliceRandom, RngCore}; use rand_core::CryptoRng; use redjubjub::{Binding, SpendAuth}; +use zcash_note_encryption::EphemeralKeyBytes; use crate::{ bundle::{ Authorization, Authorized, Bundle, GrothProofBytes, OutputDescription, SpendDescription, }, circuit, - keys::{OutgoingViewingKey, SpendAuthorizingKey, SpendValidatingKey}, + keys::{EphemeralSecretKey, OutgoingViewingKey, SpendAuthorizingKey, SpendValidatingKey}, + note::ExtractedNoteCommitment, note_encryption::{sapling_note_encryption, Zip212Enforcement}, prover::{OutputProver, SpendProver}, util::generate_random_rseed_internal, @@ -22,8 +24,8 @@ use crate::{ CommitmentSum, NoteValue, TrapdoorSum, ValueCommitTrapdoor, ValueCommitment, ValueSum, }, zip32::ExtendedSpendingKey, - Anchor, Diversifier, MerklePath, Node, Note, PaymentAddress, ProofGenerationKey, SaplingIvk, - NOTE_COMMITMENT_TREE_DEPTH, + Anchor, Diversifier, MerklePath, Node, Note, Nullifier, PaymentAddress, ProofGenerationKey, + SaplingIvk, NOTE_COMMITMENT_TREE_DEPTH, }; /// If there are any shielded inputs, always have at least two shielded outputs, padding @@ -226,15 +228,18 @@ struct PreparedSpendInfo { } impl PreparedSpendInfo { - fn build( - self, + fn build_inner( + &self, mut rng: R, - ) -> Result>, Error> { + ) -> ( + ValueCommitment, + Nullifier, + redjubjub::VerificationKey, + jubjub::Scalar, + ) { // Construct the value commitment. let alpha = jubjub::Fr::random(&mut rng); let cv = ValueCommitment::derive(self.note.value(), self.rcv.clone()); - let node = Node::from_cmu(&self.note.cmu()); - let anchor = *self.merkle_path.root(node).inner(); let ak = self.proof_generation_key.ak.clone(); @@ -247,6 +252,21 @@ impl PreparedSpendInfo { .expect("Sapling note commitment tree position must fit into a u64"), ); + (cv, nullifier, rk, alpha) + } + + fn build( + self, + rng: R, + ) -> Result>, Error> { + let (cv, nullifier, rk, alpha) = self.build_inner(rng); + + // Construct the value commitment. + let node = Node::from_cmu(&self.note.cmu()); + let anchor = *self.merkle_path.root(node).inner(); + + let ak = self.proof_generation_key.ak.clone(); + let zkproof = Pr::prepare_circuit( self.proof_generation_key, *self.note.recipient().diversifier(), @@ -271,6 +291,28 @@ impl PreparedSpendInfo { }, )) } + + fn into_pczt(self, rng: R) -> crate::pczt::Spend { + let (cv, nullifier, rk, alpha) = self.build_inner(rng); + + crate::pczt::Spend { + cv, + nullifier, + rk, + zkproof: None, + spend_auth_sig: None, + recipient: Some(self.note.recipient()), + value: Some(self.note.value()), + rseed: Some(*self.note.rseed()), + rcv: Some(self.rcv), + proof_generation_key: Some(self.proof_generation_key), + witness: Some(self.merkle_path), + alpha: Some(alpha), + zip32_derivation: None, + dummy_ask: self.dummy_ask, + proprietary: BTreeMap::new(), + } + } } /// A struct containing the information required in order to construct a @@ -358,23 +400,24 @@ struct PreparedOutputInfo { } impl PreparedOutputInfo { - fn build( - self, + fn build_inner( + &self, + zkproof: impl FnOnce(&EphemeralSecretKey) -> P, rng: &mut R, - ) -> OutputDescription { + ) -> ( + ValueCommitment, + ExtractedNoteCommitment, + EphemeralKeyBytes, + [u8; 580], + [u8; 80], + P, + ) { let encryptor = sapling_note_encryption::(self.ovk, self.note.clone(), self.memo, rng); // Construct the value commitment. let cv = ValueCommitment::derive(self.note.value(), self.rcv.clone()); - // Prepare the circuit that will be used to construct the proof. - let zkproof = Pr::prepare_circuit( - encryptor.esk(), - self.note.recipient(), - self.note.rcm(), - self.note.value(), - self.rcv, - ); + let zkproof = zkproof(encryptor.esk()); let cmu = self.note.cmu(); @@ -383,7 +426,7 @@ impl PreparedOutputInfo { let epk = encryptor.epk(); - OutputDescription::from_parts( + ( cv, cmu, epk.to_bytes(), @@ -392,6 +435,55 @@ impl PreparedOutputInfo { zkproof, ) } + + fn build( + self, + rng: &mut R, + ) -> OutputDescription { + let (cv, cmu, ephemeral_key, enc_ciphertext, out_ciphertext, zkproof) = self.build_inner( + |esk| { + Pr::prepare_circuit( + esk, + self.note.recipient(), + self.note.rcm(), + self.note.value(), + self.rcv.clone(), + ) + }, + rng, + ); + + OutputDescription::from_parts( + cv, + cmu, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + zkproof, + ) + } + + fn into_pczt(self, rng: &mut R) -> crate::pczt::Output { + let (cv, cmu, ephemeral_key, enc_ciphertext, out_ciphertext, _) = + self.build_inner(|_| (), rng); + + crate::pczt::Output { + cv, + cmu, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + zkproof: None, + recipient: Some(self.note.recipient()), + value: Some(self.note.value()), + rseed: Some(*self.note.rseed()), + rcv: Some(self.rcv), + // TODO: Save this? + ock: None, + zip32_derivation: None, + proprietary: BTreeMap::new(), + } + } } /// Metadata about a transaction created by a [`Builder`]. @@ -549,18 +641,127 @@ impl Builder { self.outputs, ) } + + /// Builds a bundle containing the given spent notes and outputs along with their + /// metadata, for inclusion in a PCZT. + pub fn build_for_pczt>( + self, + rng: R, + ) -> Result<(crate::pczt::Bundle, SaplingMetadata), Error> { + build_bundle::<_, (), (), _, V>( + rng, + self.bundle_type, + self.zip212_enforcement, + self.anchor, + self.spends, + self.outputs, + |spend_infos, output_infos, value_sum, tx_metadata, mut rng| { + // Create the PCZT Spends and Outputs. + let spends = spend_infos + .into_iter() + .map(|a| a.into_pczt(&mut rng)) + .collect::>(); + let outputs = output_infos + .into_iter() + .map(|a| a.into_pczt(&mut rng)) + .collect::>(); + + Ok(( + crate::pczt::Bundle { + spends, + outputs, + value_sum, + anchor: self.anchor, + bsk: None, + }, + tx_metadata, + )) + }, + ) + } } /// Constructs a new Sapling transaction bundle of the given type from the specified set of spends /// and outputs. pub fn bundle>( - mut rng: R, + rng: R, bundle_type: BundleType, zip212_enforcement: Zip212Enforcement, anchor: Anchor, spends: Vec, outputs: Vec, ) -> Result, SaplingMetadata)>, Error> { + build_bundle::<_, SP, OP, _, V>( + rng, + bundle_type, + zip212_enforcement, + anchor, + spends, + outputs, + |spend_infos, output_infos, value_balance, tx_metadata, mut rng| { + let value_balance_i64 = + i64::try_from(value_balance).map_err(|_| Error::InvalidAmount)?; + + // Compute the transaction binding signing key. + let bsk = { + let spends: TrapdoorSum = spend_infos.iter().map(|spend| &spend.rcv).sum(); + let outputs: TrapdoorSum = output_infos.iter().map(|output| &output.rcv).sum(); + (spends - outputs).into_bsk() + }; + + // Create the unauthorized Spend and Output descriptions. + let shielded_spends = spend_infos + .into_iter() + .map(|a| a.build::(&mut rng)) + .collect::, _>>()?; + let shielded_outputs = output_infos + .into_iter() + .map(|a| a.build::(&mut rng)) + .collect::>(); + + // Verify that bsk and bvk are consistent. + let bvk = { + let spends = shielded_spends + .iter() + .map(|spend| spend.cv()) + .sum::(); + let outputs = shielded_outputs + .iter() + .map(|output| output.cv()) + .sum::(); + (spends - outputs).into_bvk(value_balance_i64) + }; + assert_eq!(redjubjub::VerificationKey::from(&bsk), bvk); + + Ok(Bundle::from_parts( + shielded_spends, + shielded_outputs, + V::try_from(value_balance_i64).map_err(|_| Error::InvalidAmount)?, + InProgress { + sigs: Unsigned { bsk }, + _proof_state: PhantomData::default(), + }, + ) + .map(|b| (b, tx_metadata))) + }, + ) +} + +fn build_bundle>( + mut rng: R, + bundle_type: BundleType, + zip212_enforcement: Zip212Enforcement, + anchor: Anchor, + spends: Vec, + outputs: Vec, + finisher: impl FnOnce( + Vec, + Vec, + ValueSum, + SaplingMetadata, + R, + ) -> Result, +) -> Result { match bundle_type { BundleType::Transactional { .. } => { for spend in &spends { @@ -640,13 +841,6 @@ pub fn bundle>( }) .collect::>(); - // Compute the transaction binding signing key. - let bsk = { - let spends: TrapdoorSum = spend_infos.iter().map(|spend| &spend.rcv).sum(); - let outputs: TrapdoorSum = output_infos.iter().map(|output| &output.rcv).sum(); - (spends - outputs).into_bsk() - }; - // Compute the Sapling value balance of the bundle for comparison to `bvk` and `bsk` let input_total = spend_infos .iter() @@ -658,42 +852,8 @@ pub fn bundle>( .try_fold(input_total, |balance, output| { (balance - output.note.value()).ok_or(Error::InvalidAmount) })?; - let value_balance_i64 = i64::try_from(value_balance).map_err(|_| Error::InvalidAmount)?; - // Create the unauthorized Spend and Output descriptions. - let shielded_spends = spend_infos - .into_iter() - .map(|a| a.build::(&mut rng)) - .collect::, _>>()?; - let shielded_outputs = output_infos - .into_iter() - .map(|a| a.build::(&mut rng)) - .collect::>(); - - // Verify that bsk and bvk are consistent. - let bvk = { - let spends = shielded_spends - .iter() - .map(|spend| spend.cv()) - .sum::(); - let outputs = shielded_outputs - .iter() - .map(|output| output.cv()) - .sum::(); - (spends - outputs).into_bvk(value_balance_i64) - }; - assert_eq!(redjubjub::VerificationKey::from(&bsk), bvk); - - Ok(Bundle::from_parts( - shielded_spends, - shielded_outputs, - V::try_from(value_balance_i64).map_err(|_| Error::InvalidAmount)?, - InProgress { - sigs: Unsigned { bsk }, - _proof_state: PhantomData::default(), - }, - ) - .map(|b| (b, tx_metadata))) + finisher(spend_infos, output_infos, value_balance, tx_metadata, rng) } /// Type alias for an in-progress bundle that has no proofs or signatures. diff --git a/src/bundle.rs b/src/bundle.rs index 7ade87e..b015f13 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -25,6 +25,16 @@ pub trait Authorization: Debug { type AuthSig: Clone + Debug; } +/// Marker type for a bundle that contains no authorizing data. +#[derive(Debug)] +pub struct EffectsOnly; + +impl Authorization for EffectsOnly { + type SpendProof = (); + type OutputProof = (); + type AuthSig = (); +} + /// Authorizing data for a bundle of Sapling spends and outputs, ready to be committed to /// the ledger. #[derive(Debug, Copy, Clone)] diff --git a/src/keys.rs b/src/keys.rs index 06348c7..b636785 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -108,7 +108,7 @@ impl SpendAuthorizingKey { } /// Converts this spend authorizing key to its serialized form. - pub(crate) fn to_bytes(&self) -> [u8; 32] { + pub fn to_bytes(&self) -> [u8; 32] { <[u8; 32]>::from(self.0) } @@ -193,7 +193,7 @@ impl SpendValidatingKey { /// Converts this spend validating key to its serialized form, /// `LEBS2OSP_256(repr_J(ak))`. - pub(crate) fn to_bytes(&self) -> [u8; 32] { + pub fn to_bytes(&self) -> [u8; 32] { <[u8; 32]>::from(self.0) } diff --git a/src/lib.rs b/src/lib.rs index 5ffc49b..654b767 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ pub mod group_hash; pub mod keys; pub mod note; pub mod note_encryption; +pub mod pczt; pub mod pedersen_hash; pub mod prover; mod spec; diff --git a/src/pczt.rs b/src/pczt.rs new file mode 100644 index 0000000..e2d2808 --- /dev/null +++ b/src/pczt.rs @@ -0,0 +1,294 @@ +//! PCZT support for Sapling. + +use core::fmt; +use std::collections::BTreeMap; + +use getset::Getters; +use redjubjub::{Binding, SpendAuth}; +use zcash_note_encryption::{ + EphemeralKeyBytes, OutgoingCipherKey, ENC_CIPHERTEXT_SIZE, OUT_CIPHERTEXT_SIZE, +}; +use zip32::ChildIndex; + +use crate::{ + bundle::GrothProofBytes, + keys::SpendAuthorizingKey, + note::ExtractedNoteCommitment, + value::{NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum}, + Anchor, MerklePath, Nullifier, PaymentAddress, ProofGenerationKey, Rseed, +}; + +mod parse; +pub use parse::ParseError; + +mod io_finalizer; +pub use io_finalizer::IoFinalizerError; + +mod prover; +pub use prover::ProverError; + +mod signer; +pub use signer::SignerError; + +mod tx_extractor; +pub use tx_extractor::{TxExtractorError, Unbound}; + +/// PCZT fields that are specific to producing the transaction's Sapling bundle (if any). +/// +/// This struct is for representing Sapling in a partially-created transaction. If you +/// have a fully-created transaction, use [the regular `Bundle` struct]. +/// +/// [the regular `Bundle` struct]: crate::Bundle +#[derive(Debug, Getters)] +#[getset(get = "pub")] +pub struct Bundle { + /// The Sapling spends in this bundle. + /// + /// Entries are added by the Constructor, and modified by an Updater, IO Finalizer, + /// Prover, Signer, Combiner, or Spend Finalizer. + pub(crate) spends: Vec, + + /// The Sapling outputs in this bundle. + /// + /// Entries are added by the Constructor, and modified by an Updater, IO Finalizer, + /// Prover, Signer, Combiner, or Spend Finalizer. + pub(crate) outputs: Vec, + + /// The net value of Sapling `spends` minus `outputs`. + /// + /// This is initialized by the Creator, and updated by the Constructor as spends or + /// outputs are added to the PCZT. It enables per-spend and per-output values to be + /// redacted from the PCZT after they are no longer necessary. + pub(crate) value_sum: ValueSum, + + /// The Sapling anchor for this transaction. + /// + /// Set by the Creator. + pub(crate) anchor: Anchor, + + /// The Sapling binding signature signing key. + /// + /// - This is `None` until it is set by the IO Finalizer. + /// - The Transaction Extractor uses this to produce the binding signature. + pub(crate) bsk: Option>, +} + +impl Bundle { + /// Returns a mutable reference to the spends in this bundle. + /// + /// This is used by Signers to apply signatures with [`Spend::sign`]. + pub fn spends_mut(&mut self) -> &mut [Spend] { + &mut self.spends + } +} + +/// Information about a Sapling spend within a transaction. +/// +/// This struct is for representing Sapling spends in a partially-created transaction. If +/// you have a fully-created transaction, use [the regular `SpendDescription` struct]. +/// +/// [the regular `SpendDescription` struct]: crate::bundle::SpendDescription +#[derive(Debug, Getters)] +#[getset(get = "pub")] +pub struct Spend { + /// A commitment to the value consumed by this spend. + pub(crate) cv: ValueCommitment, + + /// The nullifier of the note being spent. + pub(crate) nullifier: Nullifier, + + /// The randomized verification key for the note being spent. + pub(crate) rk: redjubjub::VerificationKey, + + /// The Spend proof. + /// + /// This is set by the Prover. + pub(crate) zkproof: Option, + + /// The spend authorization signature. + /// + /// This is set by the Signer. + pub(crate) spend_auth_sig: Option>, + + /// The address that received the note being spent. + /// + /// - This is set by the Constructor (or Updater?). + /// - This is required by the Prover. + pub(crate) recipient: Option, + + /// The value of the input being spent. + /// + /// This may be used by Signers to verify that the value matches `cv`, and to confirm + /// the values and change involved in the transaction. + /// + /// This exposes the input value to all participants. For Signers who don't need this + /// information, or after signatures have been applied, this can be redacted. + pub(crate) value: Option, + + /// The seed randomness for the note being spent. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + pub(crate) rseed: Option, + + /// The value commitment randomness. + /// + /// - This is set by the Constructor. + /// - The IO Finalizer compresses it into the `bsk`. + /// - This is required by the Prover. + /// - This may be used by Signers to verify that the value correctly matches `cv`. + /// + /// This opens `cv` for all participants. For Signers who don't need this information, + /// or after proofs / signatures have been applied, this can be redacted. + pub(crate) rcv: Option, + + /// The proof generation key `(ak, nsk)` corresponding to the recipient that received + /// the note being spent. + /// + /// - This is set by the Updater. + /// - This is required by the Prover. + pub(crate) proof_generation_key: Option, + + /// A witness from the note to the bundle's anchor. + /// + /// - This is set by the Updater. + /// - This is required by the Prover. + pub(crate) witness: Option, + + /// The spend authorization randomizer. + /// + /// - This is chosen by the Constructor. + /// - This is required by the Signer for creating `spend_auth_sig`, and may be used to + /// validate `rk`. + /// - After`zkproof` / `spend_auth_sig` has been set, this can be redacted. + pub(crate) alpha: Option, + + /// The ZIP 32 derivation path at which the spending key can be found for the note + /// being spent. + pub(crate) zip32_derivation: Option, + + /// The spend authorizing key for this spent note, if it is a dummy note. + /// + /// - This is chosen by the Constructor. + /// - This is required by the IO Finalizer, and is cleared by it once used. + /// - Signers MUST reject PCZTs that contain `dummy_ask` values. + pub(crate) dummy_ask: Option, + + /// Proprietary fields related to the note being spent. + pub(crate) proprietary: BTreeMap>, +} + +/// Information about a Sapling output within a transaction. +/// +/// This struct is for representing Sapling outputs in a partially-created transaction. If +/// you have a fully-created transaction, use [the regular `OutputDescription` struct]. +/// +/// [the regular `OutputDescription` struct]: crate::bundle::OutputDescription +#[derive(Getters)] +#[getset(get = "pub")] +pub struct Output { + /// A commitment to the value created by this output. + pub(crate) cv: ValueCommitment, + + /// A commitment to the new note being created. + pub(crate) cmu: ExtractedNoteCommitment, + + /// The ephemeral key used to encrypt the note plaintext. + pub(crate) ephemeral_key: EphemeralKeyBytes, + + /// The encrypted note plaintext for the output. + /// + /// Once we have memo bundles, we will be able to set memos independently of Outputs. + /// For now, the Constructor sets both at the same time. + pub(crate) enc_ciphertext: [u8; ENC_CIPHERTEXT_SIZE], + + /// The encrypted output plaintext for the output. + pub(crate) out_ciphertext: [u8; OUT_CIPHERTEXT_SIZE], + + /// The Output proof. + /// + /// This is set by the Prover. + pub(crate) zkproof: Option, + + /// The address that will receive the output. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + /// - The Signer can use `recipient` and `rseed` (if present) to verify that + /// `enc_ciphertext` is correctly encrypted (and contains a note plaintext matching + /// the public commitments), and to confirm the value of the memo. + pub(crate) recipient: Option, + + /// The value of the output. + /// + /// This may be used by Signers to verify that the value matches `cv`, and to confirm + /// the values and change involved in the transaction. + /// + /// This exposes the output value to all participants. For Signers who don't need this + /// information, or after signatures have been applied, this can be redacted. + pub(crate) value: Option, + + /// The seed randomness for the output. + /// + /// - This is set by the Constructor. It is always [`Rseed::AfterZip212`]. + /// - This is required by the Prover. + /// - The Signer can use `recipient` and `rseed` (if present) to verify that + /// `enc_ciphertext` is correctly encrypted (and contains a note plaintext matching + /// the public commitments), and to confirm the value of the memo. + pub(crate) rseed: Option, + + /// The value commitment randomness. + /// + /// - This is set by the Constructor. + /// - The IO Finalizer compresses it into the bsk. + /// - This is required by the Prover. + /// - This may be used by Signers to verify that the value correctly matches `cv`. + /// + /// This opens `cv` for all participants. For Signers who don't need this information, + /// or after proofs / signatures have been applied, this can be redacted. + pub(crate) rcv: Option, + + /// The `ock` value used to encrypt `out_ciphertext`. + /// + /// This enables Signers to verify that `out_ciphertext` is correctly encrypted. + /// + /// This may be `None` if the Constructor added the output using an OVK policy of + /// "None", to make the output unrecoverable from the chain by the sender. + pub(crate) ock: Option, + + /// The ZIP 32 derivation path at which the spending key can be found for the output. + pub(crate) zip32_derivation: Option, + + /// Proprietary fields related to the note being created. + pub(crate) proprietary: BTreeMap>, +} + +impl fmt::Debug for Output { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Output") + .field("cv", &self.cv) + .field("cmu", &self.cmu) + .field("ephemeral_key", &self.ephemeral_key) + .field("enc_ciphertext", &self.enc_ciphertext) + .field("out_ciphertext", &self.out_ciphertext) + .field("zkproof", &self.zkproof) + .field("recipient", &self.recipient) + .field("value", &self.value) + .field("rseed", &self.rseed) + .field("rcv", &self.rcv) + .field("zip32_derivation", &self.zip32_derivation) + .field("proprietary", &self.proprietary) + .finish_non_exhaustive() + } +} + +/// The ZIP 32 derivation path at which a key can be found. +#[derive(Debug, Getters, PartialEq, Eq)] +#[getset(get = "pub")] +pub struct Zip32Derivation { + /// The [ZIP 32 seed fingerprint](https://zips.z.cash/zip-0032#seed-fingerprints). + seed_fingerprint: [u8; 32], + + /// The sequence of indices corresponding to the shielded HD path. + derivation_path: Vec, +} diff --git a/src/pczt/io_finalizer.rs b/src/pczt/io_finalizer.rs new file mode 100644 index 0000000..563d4d2 --- /dev/null +++ b/src/pczt/io_finalizer.rs @@ -0,0 +1,91 @@ +use rand::{CryptoRng, RngCore}; + +use crate::value::{CommitmentSum, TrapdoorSum}; + +use super::SignerError; + +impl super::Bundle { + /// Finalizes the IO for this bundle. + pub fn finalize_io( + &mut self, + sighash: [u8; 32], + mut rng: R, + ) -> Result<(), IoFinalizerError> { + // Compute the transaction binding signing key. + let bsk = { + let spend_rcvs = self + .spends + .iter() + .map(|spend| { + spend + .rcv + .as_ref() + .ok_or(IoFinalizerError::MissingValueCommitTrapdoor) + }) + .collect::, _>>()?; + + let output_rcvs = self + .outputs + .iter() + .map(|output| { + output + .rcv + .as_ref() + .ok_or(IoFinalizerError::MissingValueCommitTrapdoor) + }) + .collect::, _>>()?; + + let spends: TrapdoorSum = spend_rcvs.into_iter().sum(); + let outputs: TrapdoorSum = output_rcvs.into_iter().sum(); + (spends - outputs).into_bsk() + }; + + // Verify that bsk and bvk are consistent. + let bvk = { + let spends = self + .spends + .iter() + .map(|spend| spend.cv()) + .sum::(); + let outputs = self + .outputs + .iter() + .map(|output| output.cv()) + .sum::(); + (spends - outputs).into_bvk( + i64::try_from(self.value_sum).map_err(|_| IoFinalizerError::InvalidValueSum)?, + ) + }; + if redjubjub::VerificationKey::from(&bsk) != bvk { + return Err(IoFinalizerError::ValueCommitMismatch); + } + self.bsk = Some(bsk); + + // Add signatures to dummy spends. + for spend in self.spends.iter_mut() { + // The `Option::take` ensures we don't have any spend authorizing keys in the + // PCZT after the IO Finalizer has run. + if let Some(ask) = spend.dummy_ask.take() { + spend + .sign(sighash, &ask, &mut rng) + .map_err(IoFinalizerError::DummySignature)?; + } + } + + Ok(()) + } +} + +/// Errors that can occur while extracting a regular Orchard bundle from a PCZT bundle. +#[derive(Debug)] +pub enum IoFinalizerError { + /// An error occurred while signing a dummy spend. + DummySignature(SignerError), + /// The `value_sum` is too large for the `value_balance` field. + InvalidValueSum, + /// The IO Finalizer role requires all `rcv` fields to be set. + MissingValueCommitTrapdoor, + /// The `cv_net`, `rcv`, and `value_sum` values within the Orchard bundle are + /// inconsistent. + ValueCommitMismatch, +} diff --git a/src/pczt/parse.rs b/src/pczt/parse.rs new file mode 100644 index 0000000..3de0d59 --- /dev/null +++ b/src/pczt/parse.rs @@ -0,0 +1,295 @@ +use std::collections::BTreeMap; + +use ff::PrimeField; +use zcash_note_encryption::{EphemeralKeyBytes, OutgoingCipherKey}; +use zip32::ChildIndex; + +use super::{Bundle, Output, Spend, Zip32Derivation}; +use crate::{ + bundle::GrothProofBytes, + keys::{SpendAuthorizingKey, SpendValidatingKey}, + note::ExtractedNoteCommitment, + value::{NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum}, + Anchor, MerklePath, Node, Nullifier, PaymentAddress, ProofGenerationKey, Rseed, +}; + +impl Bundle { + /// Parses a PCZT bundle from its component parts. + pub fn parse( + spends: Vec, + outputs: Vec, + value_sum: i128, + anchor: [u8; 32], + bsk: Option<[u8; 32]>, + ) -> Result { + let value_sum = ValueSum::from_raw(value_sum); + + let anchor = Anchor::from_bytes(anchor) + .into_option() + .ok_or(ParseError::InvalidAnchor)?; + + let bsk = bsk + .map(redjubjub::SigningKey::try_from) + .transpose() + .map_err(|_| ParseError::InvalidBindingSignatureSigningKey)?; + + Ok(Self { + spends, + outputs, + value_sum, + anchor, + bsk, + }) + } +} + +impl Spend { + /// Parses a PCZT spend from its component parts. + #[allow(clippy::too_many_arguments)] + pub fn parse( + cv: [u8; 32], + nullifier: [u8; 32], + rk: [u8; 32], + zkproof: Option, + spend_auth_sig: Option<[u8; 64]>, + recipient: Option<[u8; 43]>, + value: Option, + rcm: Option<[u8; 32]>, + rseed: Option<[u8; 32]>, + rcv: Option<[u8; 32]>, + proof_generation_key: Option<([u8; 32], [u8; 32])>, + witness: Option<(u32, [[u8; 32]; 32])>, + alpha: Option<[u8; 32]>, + zip32_derivation: Option, + dummy_ask: Option<[u8; 32]>, + proprietary: BTreeMap>, + ) -> Result { + let cv = ValueCommitment::from_bytes_not_small_order(&cv) + .into_option() + .ok_or(ParseError::InvalidValueCommitment)?; + + let nullifier = Nullifier(nullifier); + + let rk = redjubjub::VerificationKey::try_from(rk) + .map_err(|_| ParseError::InvalidRandomizedKey)?; + + let spend_auth_sig = spend_auth_sig.map(redjubjub::Signature::from); + + let recipient = recipient + .as_ref() + .map(|r| PaymentAddress::from_bytes(r).ok_or(ParseError::InvalidRecipient)) + .transpose()?; + + let value = value.map(NoteValue::from_raw); + + let rseed = match (rcm, rseed) { + (None, None) => Ok(None), + (Some(rcm), None) => jubjub::Scalar::from_repr(rcm) + .into_option() + .ok_or(ParseError::InvalidNoteCommitRandomness) + .map(Rseed::BeforeZip212) + .map(Some), + (None, Some(rseed)) => Ok(Some(Rseed::AfterZip212(rseed))), + (Some(_), Some(_)) => Err(ParseError::MixedNoteCommitRandomnessAndRseed), + }?; + + let rcv = rcv + .map(|rcv| { + ValueCommitTrapdoor::from_bytes(rcv) + .into_option() + .ok_or(ParseError::InvalidValueCommitTrapdoor) + }) + .transpose()?; + + let proof_generation_key = proof_generation_key + .map(|(ak, nsk)| { + Ok(ProofGenerationKey { + ak: SpendValidatingKey::from_bytes(&ak) + .ok_or(ParseError::InvalidProofGenerationKey)?, + nsk: jubjub::Scalar::from_repr(nsk) + .into_option() + .ok_or(ParseError::InvalidProofGenerationKey)?, + }) + }) + .transpose()?; + + let witness = witness + .map(|(position, auth_path_bytes)| { + let path_elems = auth_path_bytes + .into_iter() + .map(|hash| { + Node::from_bytes(hash) + .into_option() + .ok_or(ParseError::InvalidWitness) + }) + .collect::, _>>()?; + + MerklePath::from_parts(path_elems, u64::from(position).into()) + .map_err(|()| ParseError::InvalidWitness) + }) + .transpose()?; + + let alpha = alpha + .map(|alpha| { + jubjub::Scalar::from_repr(alpha) + .into_option() + .ok_or(ParseError::InvalidSpendAuthRandomizer) + }) + .transpose()?; + + let dummy_ask = dummy_ask + .map(|dummy_ask| { + SpendAuthorizingKey::from_bytes(&dummy_ask) + .ok_or(ParseError::InvalidDummySpendAuthorizingKey) + }) + .transpose()?; + + Ok(Self { + cv, + nullifier, + rk, + zkproof, + spend_auth_sig, + recipient, + value, + rseed, + rcv, + proof_generation_key, + witness, + alpha, + zip32_derivation, + dummy_ask, + proprietary, + }) + } +} + +impl Output { + /// Parses a PCZT output from its component parts. + #[allow(clippy::too_many_arguments)] + pub fn parse( + cv: [u8; 32], + cmu: [u8; 32], + ephemeral_key: [u8; 32], + enc_ciphertext: Vec, + out_ciphertext: Vec, + zkproof: Option, + recipient: Option<[u8; 43]>, + value: Option, + rseed: Option<[u8; 32]>, + rcv: Option<[u8; 32]>, + ock: Option<[u8; 32]>, + zip32_derivation: Option, + proprietary: BTreeMap>, + ) -> Result { + let cv = ValueCommitment::from_bytes_not_small_order(&cv) + .into_option() + .ok_or(ParseError::InvalidValueCommitment)?; + + let cmu = ExtractedNoteCommitment::from_bytes(&cmu) + .into_option() + .ok_or(ParseError::InvalidExtractedNoteCommitment)?; + + let ephemeral_key = EphemeralKeyBytes(ephemeral_key); + + let enc_ciphertext = enc_ciphertext + .as_slice() + .try_into() + .map_err(|_| ParseError::InvalidEncCiphertext)?; + + let out_ciphertext = out_ciphertext + .as_slice() + .try_into() + .map_err(|_| ParseError::InvalidOutCiphertext)?; + + let recipient = recipient + .as_ref() + .map(|r| PaymentAddress::from_bytes(r).ok_or(ParseError::InvalidRecipient)) + .transpose()?; + + let value = value.map(NoteValue::from_raw); + let rseed = rseed.map(Rseed::AfterZip212); + + let rcv = rcv + .map(|rcv| { + ValueCommitTrapdoor::from_bytes(rcv) + .into_option() + .ok_or(ParseError::InvalidValueCommitTrapdoor) + }) + .transpose()?; + + let ock = ock.map(OutgoingCipherKey); + + Ok(Self { + cv, + cmu, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + zkproof, + recipient, + value, + rseed, + rcv, + ock, + zip32_derivation, + proprietary, + }) + } +} + +impl Zip32Derivation { + /// Parses a ZIP 32 derivation path from its component parts. + /// + /// Returns an error if any of the derivation path indices are non-hardened (which + /// this crate does not support, even though Sapling does). + pub fn parse( + seed_fingerprint: [u8; 32], + derivation_path: Vec, + ) -> Result { + Ok(Self { + seed_fingerprint, + derivation_path: derivation_path + .into_iter() + .map(|i| ChildIndex::from_index(i).ok_or(ParseError::InvalidZip32Derivation)) + .collect::>()?, + }) + } +} + +/// Errors that can occur while parsing a PCZT bundle. +#[derive(Debug)] +pub enum ParseError { + /// An invalid `anchor` was provided. + InvalidAnchor, + /// An invalid `bsk` was provided. + InvalidBindingSignatureSigningKey, + /// An invalid `dummy_ask` was provided. + InvalidDummySpendAuthorizingKey, + /// An invalid `enc_ciphertext` was provided. + InvalidEncCiphertext, + /// An invalid `cmu` was provided. + InvalidExtractedNoteCommitment, + /// An invalid `rcm` was provided. + InvalidNoteCommitRandomness, + /// An invalid `out_ciphertext` was provided. + InvalidOutCiphertext, + /// An invalid `proof_generation_key` was provided. + InvalidProofGenerationKey, + /// An invalid `rk` was provided. + InvalidRandomizedKey, + /// An invalid `recipient` was provided. + InvalidRecipient, + /// An invalid `alpha` was provided. + InvalidSpendAuthRandomizer, + /// An invalid `cv` was provided. + InvalidValueCommitment, + /// An invalid `rcv` was provided. + InvalidValueCommitTrapdoor, + /// An invalid `witness` was provided. + InvalidWitness, + /// An invalid `zip32_derivation` was provided. + InvalidZip32Derivation, + /// Both `rcm` and `rseed` were provided for a Spend. + MixedNoteCommitRandomnessAndRseed, +} diff --git a/src/pczt/prover.rs b/src/pczt/prover.rs new file mode 100644 index 0000000..aff1257 --- /dev/null +++ b/src/pczt/prover.rs @@ -0,0 +1,102 @@ +use rand::{CryptoRng, RngCore}; + +use crate::{ + prover::{OutputProver, SpendProver}, + Note, +}; + +impl super::Bundle { + /// Adds a proof to this PCZT bundle. + pub fn create_proofs( + &mut self, + spend_prover: &S, + output_prover: &O, + mut rng: R, + ) -> Result<(), ProverError> + where + S: SpendProver, + O: OutputProver, + { + for spend in &mut self.spends { + let proof_generation_key = spend + .proof_generation_key + .clone() + .ok_or(ProverError::MissingProofGenerationKey)?; + + let note = Note::from_parts( + spend.recipient.ok_or(ProverError::MissingRecipient)?, + spend.value.ok_or(ProverError::MissingValue)?, + spend.rseed.ok_or(ProverError::MissingRandomSeed)?, + ); + + let alpha = spend.alpha.ok_or(ProverError::MissingSpendAuthRandomizer)?; + + let rcv = spend + .rcv + .clone() + .ok_or(ProverError::MissingValueCommitTrapdoor)?; + + let merkle_path = spend.witness.clone().ok_or(ProverError::MissingWitness)?; + + let circuit = S::prepare_circuit( + proof_generation_key, + *note.recipient().diversifier(), + *note.rseed(), + note.value(), + alpha, + rcv, + self.anchor.inner(), + merkle_path, + ) + .ok_or(ProverError::InvalidDiversifier)?; + + let proof = spend_prover.create_proof(circuit, &mut rng); + spend.zkproof = Some(S::encode_proof(proof)); + } + + for output in &mut self.outputs { + let recipient = output.recipient.ok_or(ProverError::MissingRecipient)?; + let value = output.value.ok_or(ProverError::MissingValue)?; + + let note = Note::from_parts( + recipient, + value, + output.rseed.ok_or(ProverError::MissingRandomSeed)?, + ); + + let esk = note.generate_or_derive_esk(&mut rng); + let rcm = note.rcm(); + + let rcv = output + .rcv + .clone() + .ok_or(ProverError::MissingValueCommitTrapdoor)?; + + let circuit = O::prepare_circuit(&esk, recipient, rcm, value, rcv); + let proof = output_prover.create_proof(circuit, &mut rng); + output.zkproof = Some(O::encode_proof(proof)); + } + + Ok(()) + } +} + +/// Errors that can occur while creating Sapling proofs for a PCZT. +#[derive(Debug)] +pub enum ProverError { + InvalidDiversifier, + /// The Prover role requires all `proof_generation_key` fields to be set. + MissingProofGenerationKey, + /// The Prover role requires all `rseed` fields to be set. + MissingRandomSeed, + /// The Prover role requires all `recipient` fields to be set. + MissingRecipient, + /// The Prover role requires all `alpha` fields to be set. + MissingSpendAuthRandomizer, + /// The Prover role requires all `value` fields to be set. + MissingValue, + /// The Prover role requires all `rcv` fields to be set. + MissingValueCommitTrapdoor, + /// The Prover role requires all `witness` fields to be set. + MissingWitness, +} diff --git a/src/pczt/signer.rs b/src/pczt/signer.rs new file mode 100644 index 0000000..9077635 --- /dev/null +++ b/src/pczt/signer.rs @@ -0,0 +1,38 @@ +use rand::{CryptoRng, RngCore}; + +use crate::keys::SpendAuthorizingKey; + +impl super::Spend { + /// Signs the Sapling spend with the given spend authorizing key. + /// + /// It is the caller's responsibility to perform any semantic validity checks on the + /// PCZT (for example, comfirming that the change amounts are correct) before calling + /// this method. + pub fn sign( + &mut self, + sighash: [u8; 32], + ask: &SpendAuthorizingKey, + rng: R, + ) -> Result<(), SignerError> { + let alpha = self.alpha.ok_or(SignerError::MissingSpendAuthRandomizer)?; + + let rsk = ask.randomize(&alpha); + let rk = redjubjub::VerificationKey::from(&rsk); + + if self.rk == rk { + self.spend_auth_sig = Some(rsk.sign(rng, &sighash)); + Ok(()) + } else { + Err(SignerError::WrongSpendAuthorizingKey) + } + } +} + +/// Errors that can occur while signing an Orchard action in a PCZT. +#[derive(Debug)] +pub enum SignerError { + /// The Signer role requires `alpha` to be set. + MissingSpendAuthRandomizer, + /// The provided `ask` does not own the action's spent note. + WrongSpendAuthorizingKey, +} diff --git a/src/pczt/tx_extractor.rs b/src/pczt/tx_extractor.rs new file mode 100644 index 0000000..9a6cf66 --- /dev/null +++ b/src/pczt/tx_extractor.rs @@ -0,0 +1,165 @@ +use rand::{CryptoRng, RngCore}; + +use crate::{ + bundle::{ + Authorization, Authorized, EffectsOnly, GrothProofBytes, OutputDescription, + SpendDescription, + }, + Bundle, +}; + +use super::{Output, Spend}; + +impl super::Bundle { + /// Extracts the effects of this PCZT bundle as a [regular `Bundle`]. + /// + /// This is used by the Signer role to produce the transaction sighash. + /// + /// [regular `Bundle`]: crate::Bundle + pub fn extract_effects>( + &self, + ) -> Result>, TxExtractorError> { + self.to_tx_data(|_| Ok(()), |_| Ok(()), |_| Ok(()), |_| Ok(EffectsOnly)) + } + + /// Extracts a fully authorized [regular `Bundle`] from this PCZT bundle. + /// + /// This is used by the Transaction Extractor role to produce the final transaction. + /// + /// [regular `Bundle`]: crate::Bundle + pub fn extract>( + self, + ) -> Result>, TxExtractorError> { + self.to_tx_data( + |spend| spend.zkproof.ok_or(TxExtractorError::MissingProof), + |spend| { + spend + .spend_auth_sig + .ok_or(TxExtractorError::MissingSpendAuthSig) + }, + |output| output.zkproof.ok_or(TxExtractorError::MissingProof), + |bundle| { + Ok(Unbound { + bsk: bundle + .bsk + .ok_or(TxExtractorError::MissingBindingSignatureSigningKey)?, + }) + }, + ) + } + + fn to_tx_data( + &self, + spend_proof: F, + spend_auth: G, + output_proof: H, + bundle_auth: I, + ) -> Result>, E> + where + A: Authorization, + E: From, + F: Fn(&Spend) -> Result<::SpendProof, E>, + G: Fn(&Spend) -> Result<::AuthSig, E>, + H: Fn(&Output) -> Result<::OutputProof, E>, + I: FnOnce(&Self) -> Result, + V: TryFrom, + { + let spends = self + .spends + .iter() + .map(|spend| { + Ok(SpendDescription::from_parts( + spend.cv.clone(), + self.anchor.inner(), + spend.nullifier, + spend.rk, + spend_proof(spend)?, + spend_auth(spend)?, + )) + }) + .collect::>()?; + + let outputs = self + .outputs + .iter() + .map(|output| { + Ok(OutputDescription::from_parts( + output.cv.clone(), + output.cmu, + output.ephemeral_key.clone(), + output.enc_ciphertext, + output.out_ciphertext, + output_proof(output)?, + )) + }) + .collect::>()?; + + let value_balance = i64::try_from(self.value_sum) + .ok() + .and_then(|v| v.try_into().ok()) + .ok_or(TxExtractorError::ValueSumOutOfRange)?; + + let authorization = bundle_auth(self)?; + + Ok(Bundle::from_parts( + spends, + outputs, + value_balance, + authorization, + )) + } +} + +/// Errors that can occur while extracting a regular Sapling bundle from a PCZT bundle. +#[derive(Debug)] +pub enum TxExtractorError { + /// The Transaction Extractor role requires `bsk` to be set. + MissingBindingSignatureSigningKey, + /// The Transaction Extractor role requires all `zkproof` fields to be set. + MissingProof, + /// The Transaction Extractor role requires all `spend_auth_sig` fields to be set. + MissingSpendAuthSig, + /// The value sum does not fit into a `valueBalance`. + ValueSumOutOfRange, +} + +/// Authorizing data for a bundle of actions that is just missing a binding signature. +#[derive(Debug)] +pub struct Unbound { + bsk: redjubjub::SigningKey, +} + +impl Authorization for Unbound { + type SpendProof = GrothProofBytes; + type OutputProof = GrothProofBytes; + type AuthSig = redjubjub::Signature; +} + +impl crate::Bundle { + /// Verifies the given sighash with every `spend_auth_sig`, and then binds the bundle. + /// + /// Returns `None` if the given sighash does not validate against every `spend_auth_sig`. + pub fn apply_binding_signature( + self, + sighash: [u8; 32], + rng: R, + ) -> Option> { + if self + .shielded_spends() + .iter() + .all(|spend| spend.rk().verify(&sighash, spend.spend_auth_sig()).is_ok()) + { + Some(self.map_authorization( + &mut (), + |_, p| p, + |_, p| p, + |_, s| s, + |_, Unbound { bsk }| Authorized { + binding_sig: bsk.sign(rng, &sighash), + }, + )) + } else { + None + } + } +} diff --git a/src/tree.rs b/src/tree.rs index 1efb9d5..ea339b4 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -93,6 +93,10 @@ impl Anchor { Anchor(Node::empty_root(NOTE_COMMITMENT_TREE_DEPTH.into()).0) } + pub(crate) fn inner(&self) -> jubjub::Base { + self.0 + } + /// Parses a Sapling anchor from a byte encoding. pub fn from_bytes(bytes: [u8; 32]) -> CtOption { jubjub::Base::from_repr(bytes).map(Self) diff --git a/src/value/sums.rs b/src/value/sums.rs index b598e0e..0543606 100644 --- a/src/value/sums.rs +++ b/src/value/sums.rs @@ -38,6 +38,20 @@ impl ValueSum { pub fn zero() -> Self { ValueSum(0) } + + /// Instantiates a value sum from a raw encoding. + /// + /// Only intended for use with PCZTs. + pub(crate) fn from_raw(value_sum: i128) -> Self { + Self(value_sum) + } + + /// Extracts the raw encoding of this value sum. + /// + /// Only intended for use with PCZTs. + pub fn to_raw(self) -> i128 { + self.0 + } } impl Add for ValueSum {