diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6944b140..6ebcf063 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,12 +41,36 @@ jobs: matrix: target: - wasm32-wasi + - thumbv7em-none-eabihf steps: - uses: actions/checkout@v4 + with: + path: crate_root + # We use a synthetic crate to ensure no dev-dependencies are enabled, which can + # be incompatible with some of these targets. + - name: Create synthetic crate for testing + run: cargo init --lib ci-build + - name: Copy Rust version into synthetic crate + run: cp crate_root/rust-toolchain.toml ci-build/ + - name: Copy patch directives into synthetic crate + run: | + echo "[patch.crates-io]" >> ./ci-build/Cargo.toml + cat ./crate_root/Cargo.toml | sed "0,/.\+\(patch.crates.\+\)/d" >> ./ci-build/Cargo.toml + - name: Add no_std pragma to lib.rs + run: | + echo "#![no_std]" > ./ci-build/src/lib.rs + - name: Add sapling-crypto as a dependency of the synthetic crate + working-directory: ./ci-build + run: cargo add --no-default-features --path ../crate_root + - name: Add lazy_static with the spin_no_std feature + working-directory: ./ci-build + run: cargo add lazy_static --features "spin_no_std" - name: Add target + working-directory: ./ci-build run: rustup target add ${{ matrix.target }} - - name: Build crate - run: cargo build --no-default-features --verbose --target ${{ matrix.target }} + - name: Build for target + working-directory: ./ci-build + run: cargo build --verbose --target ${{ matrix.target }} bitrot: name: Bitrot check @@ -63,7 +87,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run Clippy - uses: auguwu/clippy-action@1.3.0 + uses: auguwu/clippy-action@1.4.0 with: token: ${{ secrets.GITHUB_TOKEN }} working-directory: ${{ inputs.target }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 71d33b03..7e640932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,65 @@ and this library adheres to Rust's notion of ## [Unreleased] +## [0.5.0] - 2025-02-20 + +### Added +- `sapling_crypto::pczt::Zip32Derivation::extract_account_index` +- `no_std` compatibility has been introduced by means of a default-enabled + `std` feature flag. +- A default-enabled `circuit` feature is now provided to enable downstream + users to avoid the need to depend upon the `bellman` crate. + +### Changed +- MSRV is now 1.70 +- Updated to `incrementalmerkletree 0.8.1`, `redjubjub 0.8`, `zcash_spec 0.2`, + `zip32 0.2` +- `sapling_crypto::builder::SaplingBuilder::add_output` now takes `[u8; 512]` + for its `memo` argument instead of an optional value. + +## [0.4.0] - 2024-12-16 + +### 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` +- `sapling_crypto::zip32::DiversifiableFullViewingKey::to_internal_fvk` + +### Fixed +- `sapling_crypto::prover::OutputProver::prepare_circuit` now takes `esk` as an + `sapling_crypto::keys::EphemeralSecretKey`, matching the existing public APIs + that expose it. + +### Changed +- `sapling_crypto::builder`: + - `SpendInfo::new` now takes a `FullViewingKey` instead of a + `ProofGenerationKey`. + - `Builder::add_spend` now takes a `FullViewingKey` instead of an + `&ExtendedSpendingKey`. + - `Builder::build` and `bundle` now take an `&[ExtendedSpendingKey]` argument. + - `Error` has new variants: + - `MissingSpendingKey` + - `PcztRequiresZip212` + - `WrongSpendingKey` +- `sapling_crypto::bundle::SpendDescriptionV5::into_spend_description` now + supports any `Authorization` for which the `SpendDescription` itself is fully + authorized. + +## [0.3.0] - 2024-10-02 + +### Changed +- Updated to `incrementalmerkletree` version `0.7`. + +## [0.2.0] - 2024-08-12 + +### Changed +- Updated to `incrementalmerkletree` version `0.6`. + ## [0.1.3] - 2024-03-25 ### Added @@ -121,7 +180,7 @@ The entries below are relative to the `zcash_primitives::sapling` module as of - `Builder::new` now takes a `Zip212Enforcement` argument instead of a `P: zcash_primitives::consensus::Parameters` argument and a target height. It also now takes as an argument the Sapling anchor to be used for all - spends in the bundle. + spends in the bundle. - `Builder::add_spend` now takes `extsk` by reference. Also, it no longer takes a `diversifier` argument as the diversifier may be obtained from the note. All calls to `add_spend` are now required to use an anchor @@ -144,8 +203,8 @@ The entries below are relative to the `zcash_primitives::sapling` module as of - `Bundle` now has a second generic parameter `V`. - `Bundle::value_balance` now returns `&V` instead of `&zcash_primitives::transaction::components::Amount`. - - `Bundle::map_authorization` now takes a context argument and explicit - functions for each mappable field, rather than a `MapAuth` value, in + - `Bundle::map_authorization` now takes a context argument and explicit + functions for each mappable field, rather than a `MapAuth` value, in order to simplify handling of context values. - `Authorized::binding_sig` now has type `redjubjub::Signature`. - `Authorized::AuthSig` now has type `redjubjub::Signature`. diff --git a/Cargo.lock b/Cargo.lock index 49b66c0f..6cdaea95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,6 +342,15 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" +[[package]] +name = "core2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" +dependencies = [ + "memchr", +] + [[package]] name = "cpp_demangle" version = "0.4.3" @@ -566,6 +575,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" @@ -625,12 +646,14 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "incrementalmerkletree" -version = "0.5.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "361c467824d4d9d4f284be4b2608800839419dccc4d4608f28345237fe354623" +checksum = "53227276b450ef58e4899e656ab4e9b4188aab0efc465f761bdbe56403ede61a" dependencies = [ "either", "proptest", + "rand", + "rand_core", ] [[package]] @@ -801,9 +824,9 @@ dependencies = [ [[package]] name = "memuse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2145869435ace5ea6ea3d35f59be559317ec9a0d04e1812d5f185a87b6d36f1a" +checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" [[package]] name = "miniz_oxide" @@ -1027,6 +1050,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" @@ -1165,13 +1210,12 @@ dependencies = [ [[package]] name = "redjubjub" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a60db2c3bc9c6fd1e8631fee75abc008841d27144be744951d6b9b75f9b569c" +checksum = "89b0ac1bc6bb3696d2c6f52cff8fba57238b81da8c0214ee6cd146eb8fde364e" dependencies = [ "rand_core", "reddsa", - "serde", "thiserror", "zeroize", ] @@ -1271,7 +1315,7 @@ dependencies = [ [[package]] name = "sapling-crypto" -version = "0.1.3" +version = "0.5.0" dependencies = [ "aes", "bellman", @@ -1279,12 +1323,13 @@ dependencies = [ "blake2b_simd", "blake2s_simd", "bls12_381", - "byteorder", "chacha20poly1305", + "core2", "criterion", "document-features", "ff", "fpe", + "getset", "group", "hex", "incrementalmerkletree", @@ -1367,9 +1412,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" @@ -1467,29 +1512,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", -] [[package]] name = "typenum" @@ -1807,9 +1837,9 @@ dependencies = [ [[package]] name = "zcash_spec" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a3bf58b673cb3dacd8ae09ba345998923a197ab0da70d6239d8e8838949e9b" +checksum = "ded3f58b93486aa79b85acba1001f5298f27a46489859934954d262533ee2915" dependencies = [ "blake2b_simd", ] @@ -1856,11 +1886,12 @@ dependencies = [ [[package]] name = "zip32" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d724a63be4dfb50b7f3617e542984e22e4b4a5b8ca5de91f55613152885e6b22" +checksum = "13ff9ea444cdbce820211f91e6aa3d3a56bde7202d3c0961b7c38f793abf5637" dependencies = [ "blake2b_simd", "memuse", "subtle", + "zcash_spec", ] diff --git a/Cargo.toml b/Cargo.toml index 0732624f..868d3162 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "sapling-crypto" -version = "0.1.3" +version = "0.5.0" authors = [ "Sean Bowe ", "Jack Grigg ", "Kris Nuttycombe ", ] edition = "2021" -rust-version = "1.65" +rust-version = "1.70" description = "Cryptographic library for Zcash Sapling" homepage = "https://github.com/zcash/sapling-crypto" repository = "https://github.com/zcash/sapling-crypto" @@ -18,45 +18,50 @@ features = ["test-dependencies"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -ff = "0.13" -group = { version = "0.13", features = ["wnaf-memuse"] } +ff = { version = "0.13", default-features = false } +group = "0.13" -bls12_381 = "0.8" -jubjub = "0.10" -redjubjub = "0.7" -zcash_spec = "0.1" +bls12_381 = { version = "0.8", default-features = false, features = ["alloc"] } +jubjub = { version = "0.10", default-features = false, features = ["alloc"] } +redjubjub = { version = "0.8", default-features = false } +zcash_spec = "0.2" + +# Boilerplate +getset = "0.1" + +# No-std support +core2 = { version = "0.3", default-features = false, features = ["alloc"] } # Circuits -bellman = { version = "0.14", default-features = false, features = ["groth16"] } +bellman = { version = "0.14", default-features = false, features = ["groth16"], optional = true } # CSPRNG -rand = "0.8" -rand_core = "0.6" +rand = { version = "0.8", default-features = false } +rand_core = { version = "0.6", default-features = false } # Digests -blake2b_simd = "1" -blake2s_simd = "1" +blake2b_simd = { version = "1", default-features = false } +blake2s_simd = { version = "1", default-features = false } # Documentation -document-features = "0.2" +document-features = { version = "0.2", optional = true } # Encodings -byteorder = "1" -hex = "0.4" +hex = { version = "0.4", default-features = false, features = ["alloc"] } # Logging and metrics -memuse = "0.2.1" -tracing = "0.1" +memuse = { version = "0.2.2", default-features = false } +tracing = { version = "0.1", default-features = false } # Note Commitment Trees -bitvec = "1" -incrementalmerkletree = { version = "0.5", features = ["legacy-api"] } +bitvec = { version = "1", default-features = false } +incrementalmerkletree = { version = "0.8.1", default-features = false, features = ["legacy-api"] } # Note encryption zcash_note_encryption = { version = "0.4", features = ["pre-zip-212"] } # Secret management -subtle = "2.2.3" +subtle = { version = "2.2.3", default-features = false } # Static constants lazy_static = "1" @@ -66,13 +71,14 @@ proptest = { version = "1", optional = true } # ZIP 32 aes = "0.8" -fpe = "0.6" -zip32 = "0.1" +fpe = { version = "0.6", default-features = false, features = ["alloc"] } +zip32 = { version = "0.2", default-features = false } + [dev-dependencies] chacha20poly1305 = "0.10" criterion = "0.4" -incrementalmerkletree = { version = "0.5", features = ["legacy-api", "test-dependencies"] } +incrementalmerkletree = { version = "0.8.1", features = ["legacy-api", "test-dependencies"] } proptest = "1" rand_xorshift = "0.3" @@ -80,10 +86,26 @@ rand_xorshift = "0.3" pprof = { version = "0.11", features = ["criterion", "flamegraph"] } # MSRV 1.56 [features] -default = ["multicore"] +default = ["multicore", "circuit"] +std = [ + "core2/std", + "dep:document-features", + "group/wnaf-memuse", + "redjubjub/std" +] + +## Enables creation of Sapling proofs +circuit = [ + "dep:bellman", + "bls12_381/bits", + "bls12_381/groups", + "bls12_381/pairings", + "jubjub/bits", + "std" +] ## Enables multithreading support for creating proofs. -multicore = ["bellman/multicore"] +multicore = ["bellman?/multicore"] ### A temporary feature flag that exposes granular APIs needed by `zcashd`. These APIs ### should not be relied upon and will be removed in a future release. diff --git a/README.md b/README.md index 6becc043..4f9c0df8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ # sapling-crypto -This repository contains a (work-in-progress) implementation of Zcash's "Sapling" cryptography. +This repository contains an implementation of Zcash's "Sapling" cryptography. + +## `no_std` compatibility + +Downstream users of this crate must enable the `spin_no_std` feature of the +`lazy_static` crate in order to take advantage of `no_std` builds; this is due +to the fact that `--no-default-features` builds of `lazy_static` still rely on +`std`. ## License diff --git a/rust-toolchain.toml b/rust-toolchain.toml index ce699381..5299106e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.65.0" +channel = "1.70.0" components = ["clippy", "rustfmt"] diff --git a/src/address.rs b/src/address.rs index fa71c2d6..04d8f6a4 100644 --- a/src/address.rs +++ b/src/address.rs @@ -10,7 +10,7 @@ use super::{ /// /// - `diversifier` is guaranteed to be valid for Sapling (only 50% of diversifiers are). /// - `pk_d` is guaranteed to be prime-order (i.e. in the prime-order subgroup of Jubjub, -/// and not the identity). +/// and not the identity). #[derive(Clone, Copy, Debug)] pub struct PaymentAddress { pk_d: DiversifiedTransmissionKey, diff --git a/src/builder.rs b/src/builder.rs index 80dcf814..ac11e8a1 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,31 +1,40 @@ //! Types and functions for building Sapling transaction components. -use core::fmt; -use std::{iter, marker::PhantomData}; +use alloc::collections::BTreeMap; +use alloc::vec::Vec; +use core::{fmt, 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, + bundle::{Authorization, Authorized, Bundle, GrothProofBytes}, + keys::{ + EphemeralSecretKey, ExpandedSpendingKey, FullViewingKey, OutgoingViewingKey, + SpendAuthorizingKey, SpendValidatingKey, }, - circuit, - keys::{OutgoingViewingKey, SpendAuthorizingKey, SpendValidatingKey}, + note::ExtractedNoteCommitment, note_encryption::{sapling_note_encryption, Zip212Enforcement}, - prover::{OutputProver, SpendProver}, util::generate_random_rseed_internal, - value::{ - CommitmentSum, NoteValue, TrapdoorSum, ValueCommitTrapdoor, ValueCommitment, ValueSum, - }, - zip32::ExtendedSpendingKey, - Anchor, Diversifier, MerklePath, Node, Note, PaymentAddress, ProofGenerationKey, SaplingIvk, + value::{NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum}, + Anchor, Diversifier, MerklePath, Node, Note, Nullifier, PaymentAddress, SaplingIvk, NOTE_COMMITMENT_TREE_DEPTH, }; +#[cfg(feature = "circuit")] +use crate::{ + bundle::{OutputDescription, SpendDescription}, + circuit, + prover::{OutputProver, SpendProver}, + value::{CommitmentSum, TrapdoorSum}, + zip32::ExtendedSpendingKey, + ProofGenerationKey, +}; + /// If there are any shielded inputs, always have at least two shielded outputs, padding /// with dummy outputs if necessary. See . const MIN_SHIELDED_OUTPUTS: usize = 2; @@ -121,9 +130,15 @@ pub enum Error { InvalidExternalSignature, /// A bundle could not be built because required signatures were missing. MissingSignatures, + /// A required spending key was not provided. + MissingSpendingKey, + /// [`Builder::build_for_pczt`] requires [`Zip212Enforcement::On`]. + PcztRequiresZip212, SpendProof, /// The bundle being constructed violated the construction rules for the requested bundle type. BundleTypeNotSatisfiable, + /// The wrong spending key was provided. + WrongSpendingKey, } impl fmt::Display for Error { @@ -138,10 +153,15 @@ impl fmt::Display for Error { Error::InvalidAmount => write!(f, "Invalid amount"), Error::InvalidExternalSignature => write!(f, "External signature was invalid"), Error::MissingSignatures => write!(f, "Required signatures were missing during build"), + Error::MissingSpendingKey => write!(f, "A required spending key was not provided"), + Error::PcztRequiresZip212 => { + write!(f, "PCZTs require that ZIP 212 is enforced for outputs") + } Error::SpendProof => write!(f, "Failed to create Sapling spend proof"), Error::BundleTypeNotSatisfiable => { f.write_str("Bundle structure did not conform to requested bundle type.") } + Error::WrongSpendingKey => write!(f, "The wrong spending key was provided"), } } } @@ -149,24 +169,20 @@ impl fmt::Display for Error { /// A struct containing the information necessary to add a spend to a bundle. #[derive(Debug, Clone)] pub struct SpendInfo { - proof_generation_key: ProofGenerationKey, + fvk: FullViewingKey, note: Note, merkle_path: MerklePath, - dummy_ask: Option, + dummy_expsk: Option, } impl SpendInfo { /// Constructs a [`SpendInfo`] from its constituent parts. - pub fn new( - proof_generation_key: ProofGenerationKey, - note: Note, - merkle_path: MerklePath, - ) -> Self { + pub fn new(fvk: FullViewingKey, note: Note, merkle_path: MerklePath) -> Self { Self { - proof_generation_key, + fvk, note, merkle_path, - dummy_ask: None, + dummy_expsk: None, } } @@ -189,10 +205,13 @@ impl SpendInfo { .expect("The path length corresponds to the length of the generated vector."); SpendInfo { - proof_generation_key: sk.proof_generation_key(), + fvk: FullViewingKey { + vk: sk.proof_generation_key().to_viewing_key(), + ovk: sk.ovk, + }, note, merkle_path, - dummy_ask: Some(sk.ask), + dummy_expsk: Some(sk), } } @@ -207,48 +226,77 @@ impl SpendInfo { fn prepare(self, rng: R) -> PreparedSpendInfo { PreparedSpendInfo { - proof_generation_key: self.proof_generation_key, + fvk: self.fvk, note: self.note, merkle_path: self.merkle_path, rcv: ValueCommitTrapdoor::random(rng), - dummy_ask: self.dummy_ask, + dummy_expsk: self.dummy_expsk, } } } #[derive(Debug, Clone)] struct PreparedSpendInfo { - proof_generation_key: ProofGenerationKey, + fvk: FullViewingKey, note: Note, merkle_path: MerklePath, rcv: ValueCommitTrapdoor, - dummy_ask: Option, + dummy_expsk: Option, } 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(); + let ak = self.fvk.vk.ak.clone(); // This is the result of the re-randomization, we compute it for the caller let rk = ak.randomize(&alpha); - let nullifier = self.note.nf( - &self.proof_generation_key.to_viewing_key().nk, - u64::try_from(self.merkle_path.position()) - .expect("Sapling note commitment tree position must fit into a u64"), - ); + let nullifier = self + .note + .nf(&self.fvk.vk.nk, u64::from(self.merkle_path.position())); + + (cv, nullifier, rk, alpha) + } + + #[cfg(feature = "circuit")] + fn build( + self, + proof_generation_key: Option, + rng: R, + ) -> Result>, Error> { + let proof_generation_key = match (proof_generation_key, self.dummy_expsk.as_ref()) { + (Some(proof_generation_key), None) => Ok(proof_generation_key), + (None, Some(expsk)) => Ok(expsk.proof_generation_key()), + (Some(_), Some(_)) => Err(Error::WrongSpendingKey), + (None, None) => Err(Error::MissingSpendingKey), + }?; + let expected_vk = proof_generation_key.to_viewing_key(); + if (&expected_vk.ak, &expected_vk.nk) != (&self.fvk.vk.ak, &self.fvk.vk.nk) { + return Err(Error::WrongSpendingKey); + } + + 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.fvk.vk.ak.clone(); let zkproof = Pr::prepare_circuit( - self.proof_generation_key, + proof_generation_key, *self.note.recipient().diversifier(), *self.note.rseed(), self.note.value(), @@ -266,11 +314,36 @@ impl PreparedSpendInfo { rk, zkproof, SigningMetadata { - dummy_ask: self.dummy_ask, + dummy_ask: self.dummy_expsk.map(|expsk| expsk.ask), parts: SigningParts { ak, alpha }, }, )) } + + 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: self + .dummy_expsk + .as_ref() + .map(|expsk| expsk.proof_generation_key()), + witness: Some(self.merkle_path), + alpha: Some(alpha), + zip32_derivation: None, + dummy_ask: self.dummy_expsk.map(|expsk| expsk.ask), + proprietary: BTreeMap::new(), + } + } } /// A struct containing the information required in order to construct a @@ -290,17 +363,13 @@ impl OutputInfo { ovk: Option, to: PaymentAddress, value: NoteValue, - memo: Option<[u8; 512]>, + memo: [u8; 512], ) -> Self { Self { ovk, to, value, - memo: memo.unwrap_or_else(|| { - let mut memo = [0; 512]; - memo[0] = 0xf6; - memo - }), + memo, } } @@ -328,7 +397,7 @@ impl OutputInfo { } }; - Self::new(None, dummy_to, NoteValue::ZERO, None) + Self::new(None, dummy_to, NoteValue::ZERO, [0u8; 512]) } fn prepare( @@ -358,23 +427,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().0, - self.note.recipient(), - self.note.rcm(), - self.note.value(), - self.rcv, - ); + let zkproof = zkproof(encryptor.esk()); let cmu = self.note.cmu(); @@ -383,7 +453,7 @@ impl PreparedOutputInfo { let epk = encryptor.epk(); - OutputDescription::from_parts( + ( cv, cmu, epk.to_bytes(), @@ -392,6 +462,64 @@ impl PreparedOutputInfo { zkproof, ) } + + #[cfg(feature = "circuit")] + 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); + + let rseed = match self.note.rseed() { + crate::Rseed::BeforeZip212(_) => { + panic!("Builder::build_for_pczt should prevent pre-ZIP 212 outputs") + } + crate::Rseed::AfterZip212(rseed) => Some(*rseed), + }; + + crate::pczt::Output { + cv, + cmu, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + zkproof: None, + recipient: Some(self.note.recipient()), + value: Some(self.note.value()), + rseed, + rcv: Some(self.rcv), + // TODO: Save this? + ock: None, + zip32_derivation: None, + user_address: None, + proprietary: BTreeMap::new(), + } + } } /// Metadata about a transaction created by a [`Builder`]. @@ -491,11 +619,11 @@ impl Builder { /// paths for previous Sapling notes. pub fn add_spend( &mut self, - extsk: &ExtendedSpendingKey, + fvk: FullViewingKey, note: Note, merkle_path: MerklePath, ) -> Result<(), Error> { - let spend = SpendInfo::new(extsk.expsk.proof_generation_key(), note, merkle_path); + let spend = SpendInfo::new(fvk, note, merkle_path); // Consistency check: all anchors must equal the first one match self.bundle_type { @@ -523,7 +651,7 @@ impl Builder { ovk: Option, to: PaymentAddress, value: NoteValue, - memo: Option<[u8; 512]>, + memo: [u8; 512], ) -> Result<(), Error> { let output = OutputInfo::new(ovk, to, value, memo); @@ -536,8 +664,10 @@ impl Builder { } /// Constructs the Sapling bundle from the builder's accumulated state. + #[cfg(feature = "circuit")] pub fn build>( self, + extsks: &[ExtendedSpendingKey], rng: R, ) -> Result, SaplingMetadata)>, Error> { bundle::( @@ -547,20 +677,151 @@ impl Builder { self.anchor, self.spends, self.outputs, + extsks, ) } + + /// 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: impl RngCore, + ) -> Result<(crate::pczt::Bundle, SaplingMetadata), Error> { + match self.zip212_enforcement { + Zip212Enforcement::Off | Zip212Enforcement::GracePeriod => { + Err(Error::PcztRequiresZip212) + } + Zip212Enforcement::On => build_bundle( + rng, + self.bundle_type, + Zip212Enforcement::On, + 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. +#[cfg(feature = "circuit")] pub fn bundle>( - mut rng: R, + rng: R, bundle_type: BundleType, zip212_enforcement: Zip212Enforcement, anchor: Anchor, spends: Vec, outputs: Vec, + extsks: &[ExtendedSpendingKey], ) -> Result, SaplingMetadata)>, Error> { + build_bundle( + 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| { + // Look up the proof generation key for non-dummy spends. + let proof_generation_key = a + .dummy_expsk + .is_none() + .then(|| { + extsks.iter().find_map(|extsk| { + let dfvk = extsk.to_diversifiable_full_viewing_key(); + (dfvk.fvk().to_bytes() == a.fvk.to_bytes()) + .then(|| extsk.expsk.proof_generation_key()) + }) + }) + .flatten(); + a.build::(proof_generation_key, &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, + }, + ) + .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 +901,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,47 +912,14 @@ 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. /// /// This is returned by [`Builder::build`]. +#[cfg(feature = "circuit")] pub type UnauthorizedBundle = Bundle, V>; /// Marker trait representing bundle proofs in the process of being created. @@ -733,9 +954,11 @@ impl Authorization for InProgress< /// /// The [`SpendDescription`]s and [`OutputDescription`]s within the bundle contain the /// private data needed to create proofs. +#[cfg(feature = "circuit")] #[derive(Clone, Copy, Debug)] pub struct Unproven; +#[cfg(feature = "circuit")] impl InProgressProofs for Unproven { type SpendProof = circuit::Spend; type OutputProof = circuit::Output; @@ -761,6 +984,7 @@ impl ProverProgress for () { fn update(&mut self, _: u32, _: u32) {} } +#[cfg(feature = "std")] impl> ProverProgress for std::sync::mpsc::Sender { fn update(&mut self, cur: u32, end: u32) { // If the send fails, we should ignore the error, not crash. @@ -774,6 +998,7 @@ impl ProverProgress for &mut U { } } +#[cfg(feature = "circuit")] struct CreateProofs<'a, SP: SpendProver, OP: OutputProver, R: RngCore, U: ProverProgress> { spend_prover: &'a SP, output_prover: &'a OP, @@ -783,6 +1008,7 @@ struct CreateProofs<'a, SP: SpendProver, OP: OutputProver, R: RngCore, U: Prover progress: u32, } +#[cfg(feature = "circuit")] impl<'a, SP: SpendProver, OP: OutputProver, R: RngCore, U: ProverProgress> CreateProofs<'a, SP, OP, R, U> { @@ -829,11 +1055,12 @@ impl<'a, SP: SpendProver, OP: OutputProver, R: RngCore, U: ProverProgress> ) -> InProgress { InProgress { sigs: a.sigs, - _proof_state: PhantomData::default(), + _proof_state: PhantomData, } } } +#[cfg(feature = "circuit")] impl Bundle, V> { /// Creates the proofs for this bundle. pub fn create_proofs( @@ -956,7 +1183,7 @@ impl Bundle, V> { binding_signature: auth.sigs.bsk.sign(rng, &sighash), sighash, }, - _proof_state: PhantomData::default(), + _proof_state: PhantomData, }, ) } @@ -1064,9 +1291,9 @@ impl Bundle, V> { } } -#[cfg(any(test, feature = "test-dependencies"))] +#[cfg(all(feature = "circuit", any(test, feature = "test-dependencies")))] pub(crate) mod testing { - use std::fmt; + use core::fmt; use proptest::collection::vec; use proptest::prelude::*; @@ -1075,7 +1302,6 @@ pub(crate) mod testing { use crate::{ bundle::{Authorized, Bundle}, note_encryption::Zip212Enforcement, - prover::mock::{MockOutputProver, MockSpendProver}, testing::{arb_node, arb_note}, value::testing::arb_positive_note_value, zip32::testing::arb_extended_spending_key, @@ -1087,7 +1313,11 @@ pub(crate) mod testing { use super::{Builder, BundleType}; + #[cfg(feature = "circuit")] + use crate::prover::mock::{MockOutputProver, MockSpendProver}; + #[allow(dead_code)] + #[cfg(feature = "circuit")] fn arb_bundle>( max_money: u64, zip212_enforcement: Zip212Enforcement, @@ -1101,8 +1331,12 @@ pub(crate) mod testing { n_notes, ), vec( - arb_commitment_tree::<_, _, 32>(n_notes, arb_node()) - .prop_map(|t| IncrementalWitness::from_tree(t).path().unwrap()), + arb_commitment_tree::<_, _, 32>(n_notes, arb_node()).prop_map(|t| { + IncrementalWitness::from_tree(t) + .expect("valid encoding of an incremental witness") + .path() + .unwrap() + }), n_notes, ), prop::array::uniform32(any::()), @@ -1111,6 +1345,7 @@ pub(crate) mod testing { }) .prop_map( move |(extsk, spendable_notes, commitment_trees, rng_seed, fake_sighash_bytes)| { + let dfvk = extsk.to_diversifiable_full_viewing_key(); let anchor = spendable_notes .first() .zip(commitment_trees.first()) @@ -1125,11 +1360,14 @@ pub(crate) mod testing { .into_iter() .zip(commitment_trees.into_iter()) { - builder.add_spend(&extsk, note, path).unwrap(); + builder.add_spend(dfvk.fvk().clone(), note, path).unwrap(); } let (bundle, _) = builder - .build::(&mut rng) + .build::( + &[extsk.clone()], + &mut rng, + ) .unwrap() .unwrap(); diff --git a/src/bundle.rs b/src/bundle.rs index c8fedbbc..f81353c3 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -1,3 +1,4 @@ +use alloc::vec::Vec; use core::fmt::Debug; use memuse::DynamicUsage; @@ -9,7 +10,7 @@ use zcash_note_encryption::{ }; use crate::{ - circuit::GROTH_PROOF_SIZE, + constants::GROTH_PROOF_SIZE, note::ExtractedNoteCommitment, note_encryption::{CompactOutputDescription, SaplingDomain}, value::ValueCommitment, @@ -25,6 +26,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)] @@ -210,8 +221,8 @@ pub struct SpendDescription { spend_auth_sig: A::AuthSig, } -impl std::fmt::Debug for SpendDescription { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { +impl core::fmt::Debug for SpendDescription { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { write!( f, "SpendDescription(cv = {:?}, anchor = {:?}, nullifier = {:?}, rk = {:?}, spend_auth_sig = {:?})", @@ -298,12 +309,15 @@ impl SpendDescriptionV5 { Self { cv, nullifier, rk } } - pub fn into_spend_description( + pub fn into_spend_description( self, anchor: bls12_381::Scalar, zkproof: GrothProofBytes, spend_auth_sig: redjubjub::Signature, - ) -> SpendDescription { + ) -> SpendDescription + where + A: Authorization>, + { SpendDescription { cv: self.cv, anchor, @@ -418,8 +432,8 @@ impl ShieldedOutput for OutputDescription } } -impl std::fmt::Debug for OutputDescription { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { +impl core::fmt::Debug for OutputDescription { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { write!( f, "OutputDescription(cv = {:?}, cmu = {:?}, ephemeral_key = {:?})", @@ -485,7 +499,7 @@ impl From> for CompactOutputDescription { #[cfg(any(test, feature = "test-dependencies"))] #[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))] pub mod testing { - use std::fmt; + use core::fmt; use ff::Field; use group::{Group, GroupEncoding}; @@ -494,7 +508,7 @@ pub mod testing { use rand::{rngs::StdRng, SeedableRng}; use crate::{ - circuit::GROTH_PROOF_SIZE, + constants::GROTH_PROOF_SIZE, note::testing::arb_cmu, value::{ testing::{arb_note_value_bounded, arb_trapdoor}, diff --git a/src/circuit.rs b/src/circuit.rs index 611dc887..d2f083e1 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -1,15 +1,14 @@ //! The Sapling circuits. +use alloc::vec::Vec; use core::fmt; -use std::io; +use core2::io; use group::{ff::PrimeField, Curve}; use bellman::{groth16, Circuit, ConstraintSystem, SynthesisError}; use bls12_381::Bls12; -use super::{value::NoteValue, PaymentAddress, ProofGenerationKey}; - use bellman::gadgets::blake2s; use bellman::gadgets::boolean; use bellman::gadgets::multipack; @@ -21,6 +20,7 @@ use self::constants::{ PROOF_GENERATION_KEY_GENERATOR, SPENDING_KEY_GENERATOR, VALUE_COMMITMENT_RANDOMNESS_GENERATOR, VALUE_COMMITMENT_VALUE_GENERATOR, }; +use crate::{value::NoteValue, PaymentAddress, ProofGenerationKey}; #[cfg(test)] use group::ff::PrimeFieldBits; @@ -29,9 +29,6 @@ mod constants; mod ecc; mod pedersen_hash; -// π_A + π_B + π_C -pub(crate) const GROTH_PROOF_SIZE: usize = 48 + 96 + 48; - /// The opening (value and randomness) of a Sapling value commitment. #[derive(Clone)] pub struct ValueCommitmentOpening { @@ -565,7 +562,10 @@ impl SpendParameters { /// Only set `verify_point_encodings` to false if you are verifying the parameters in /// another way (such as checking the hash of the parameters file on disk). pub fn read(reader: R, verify_point_encodings: bool) -> io::Result { - groth16::Parameters::::read(reader, verify_point_encodings).map(Self) + Ok(Self(groth16::Parameters::::read( + reader, + verify_point_encodings, + )?)) } /// Returns the verifying key for the Sapling Spend circuit. @@ -703,7 +703,7 @@ fn test_input_circuit_with_bls12_381() { let mut rhs = uncle; if b { - ::std::mem::swap(&mut lhs, &mut rhs); + ::core::mem::swap(&mut lhs, &mut rhs); } let lhs = lhs.to_le_bits(); @@ -799,7 +799,7 @@ fn test_input_circuit_with_bls12_381_external_test_vectors() { let tree_depth = 32; - let expected_commitment_us = vec![ + let expected_commitment_us = [ "43821661663052659750276289184181083197337192946256245809816728673021647664276", "7220807656052227578299730541645543434083158611414003423211850718229633594616", "13239753550660714843257636471668037031928211668773449453628093339627668081697", @@ -812,7 +812,7 @@ fn test_input_circuit_with_bls12_381_external_test_vectors() { "18269767207277008186871145355531741929166733260352590789136389380124992250945", ]; - let expected_commitment_vs = vec![ + let expected_commitment_vs = [ "27630722367128086497290371604583225252915685718989450292520883698391703910", "23310648738313092772044712773481584369462075017189681529702825235349449805260", "25709635353183537915646348052945798827495141780341329896098121888376871589480", @@ -886,7 +886,7 @@ fn test_input_circuit_with_bls12_381_external_test_vectors() { let mut rhs = uncle; if b { - ::std::mem::swap(&mut lhs, &mut rhs); + ::core::mem::swap(&mut lhs, &mut rhs); } let lhs = lhs.to_le_bits(); diff --git a/src/circuit/constants.rs b/src/circuit/constants.rs index 692c688f..17ae9593 100644 --- a/src/circuit/constants.rs +++ b/src/circuit/constants.rs @@ -2,6 +2,7 @@ use crate::constants::{PEDERSEN_HASH_CHUNKS_PER_GENERATOR, PEDERSEN_HASH_GENERATORS}; +use alloc::vec::Vec; use bls12_381::Scalar; use group::{ff::Field, Curve, Group}; use jubjub::ExtendedPoint; diff --git a/src/circuit/ecc.rs b/src/circuit/ecc.rs index 6bffd876..ef67b629 100644 --- a/src/circuit/ecc.rs +++ b/src/circuit/ecc.rs @@ -1,6 +1,7 @@ //! Gadgets implementing Jubjub elliptic curve operations. -use std::ops::{AddAssign, MulAssign, Neg, SubAssign}; +use alloc::vec::Vec; +use core::ops::{AddAssign, MulAssign, Neg, SubAssign}; use bellman::{ConstraintSystem, SynthesisError}; @@ -37,7 +38,7 @@ where for (i, (chunk, window)) in by.chunks(3).zip(base.iter()).enumerate() { let chunk_a = chunk - .get(0) + .first() .cloned() .unwrap_or_else(|| Boolean::constant(false)); let chunk_b = chunk @@ -620,6 +621,7 @@ impl MontgomeryPoint { #[cfg(test)] mod test { + use alloc::vec::Vec; use bellman::ConstraintSystem; use group::{ ff::{Field, PrimeField, PrimeFieldBits}, diff --git a/src/circuit/pedersen_hash.rs b/src/circuit/pedersen_hash.rs index 94b90140..29718a8a 100644 --- a/src/circuit/pedersen_hash.rs +++ b/src/circuit/pedersen_hash.rs @@ -3,6 +3,7 @@ use super::ecc::{EdwardsPoint, MontgomeryPoint}; pub use crate::pedersen_hash::Personalization; +use alloc::vec::Vec; use bellman::gadgets::boolean::Boolean; use bellman::gadgets::lookup::*; use bellman::{ConstraintSystem, SynthesisError}; diff --git a/src/constants.rs b/src/constants.rs index bb730a97..94dd55ff 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,5 +1,6 @@ //! Various constants used by the Sapling protocol. +use alloc::vec::Vec; use ff::PrimeField; use group::Group; use jubjub::SubgroupPoint; @@ -38,6 +39,9 @@ pub const VALUE_COMMITMENT_GENERATOR_PERSONALIZATION: &[u8; 8] = b"Zcash_cv"; /// BLAKE2s Personalization for the nullifier position generator (for computing rho) pub const NULLIFIER_POSITION_IN_TREE_GENERATOR_PERSONALIZATION: &[u8; 8] = b"Zcash_J_"; +// π_A + π_B + π_C +pub(crate) const GROTH_PROOF_SIZE: usize = 48 + 96 + 48; + /// The prover will demonstrate knowledge of discrete log with respect to this base when /// they are constructing a proof, in order to authorize proof construction. pub const PROOF_GENERATION_KEY_GENERATOR: SubgroupPoint = SubgroupPoint::from_raw_unchecked( @@ -287,7 +291,7 @@ mod tests { let gh = group_hash(&tag, personalization); // We don't want to overflow and start reusing generators - assert!(tag[i] != u8::max_value()); + assert!(tag[i] != u8::MAX); tag[i] += 1; if let Some(gh) = gh { diff --git a/src/keys.rs b/src/keys.rs index 06348c74..4c357f83 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -4,8 +4,9 @@ //! //! [section 4.2.2]: https://zips.z.cash/protocol/protocol.pdf#saplingkeycomponents -use std::fmt; -use std::io::{self, Read, Write}; +use alloc::vec::Vec; +use core::fmt; +use core2::io::{self, Read, Write}; use super::{ address::PaymentAddress, @@ -26,7 +27,7 @@ use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; use zcash_note_encryption::EphemeralKeyBytes; use zcash_spec::PrfExpand; -#[cfg(test)] +#[cfg(all(feature = "circuit", test))] use rand_core::RngCore; /// Errors that can occur in the decoding of Sapling spending keys. @@ -108,7 +109,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) } @@ -153,7 +154,7 @@ impl Eq for SpendValidatingKey {} impl SpendValidatingKey { /// For circuit tests only. - #[cfg(test)] + #[cfg(all(feature = "circuit", test))] pub(crate) fn fake_random(mut rng: R) -> Self { loop { if let Some(k) = Self::from_bytes(&jubjub::SubgroupPoint::random(&mut rng).to_bytes()) { @@ -193,7 +194,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) } @@ -444,6 +445,7 @@ impl SaplingIvk { #[derive(Clone, Debug)] pub struct PreparedIncomingViewingKey(PreparedScalar); +#[cfg(feature = "std")] impl memuse::DynamicUsage for PreparedIncomingViewingKey { fn dynamic_usage(&self) -> usize { self.0.dynamic_usage() @@ -690,6 +692,7 @@ pub mod testing { #[cfg(test)] mod tests { + use alloc::string::ToString; use group::{Group, GroupEncoding}; use super::{FullViewingKey, SpendAuthorizingKey, SpendValidatingKey}; diff --git a/src/lib.rs b/src/lib.rs index 5ffc49b7..65fbaa62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,30 +7,42 @@ //! shielded payment address; we implicitly mean it is an Sapling payment address (as //! opposed to e.g. an Orchard payment address, which is also shielded). //! -//! ## Feature flags -#![doc = document_features::document_features!()] +#![cfg_attr(feature = "std", doc = "## Feature flags")] +#![cfg_attr(feature = "std", doc = document_features::document_features!())] //! +#![no_std] #![cfg_attr(docsrs, feature(doc_cfg))] // Catch documentation errors caused by code changes. #![deny(rustdoc::broken_intra_doc_links)] #![deny(unsafe_code)] +#[macro_use] +extern crate alloc; + +#[cfg(feature = "std")] +extern crate std; + mod address; pub mod builder; pub mod bundle; + +#[cfg(feature = "circuit")] pub mod circuit; pub mod constants; pub mod group_hash; pub mod keys; pub mod note; pub mod note_encryption; +pub mod pczt; pub mod pedersen_hash; +#[cfg(feature = "circuit")] pub mod prover; mod spec; mod tree; pub mod util; pub mod value; +#[cfg(feature = "circuit")] mod verifier; pub mod zip32; @@ -42,6 +54,8 @@ pub use tree::{ merkle_hash, Anchor, CommitmentTree, IncrementalWitness, MerklePath, Node, NOTE_COMMITMENT_TREE_DEPTH, }; + +#[cfg(feature = "circuit")] pub use verifier::{BatchValidator, SaplingVerificationContext}; #[cfg(any(test, feature = "test-dependencies"))] diff --git a/src/note/nullifier.rs b/src/note/nullifier.rs index 8176f40d..58fd01b8 100644 --- a/src/note/nullifier.rs +++ b/src/note/nullifier.rs @@ -1,5 +1,6 @@ -use std::array::TryFromSliceError; -use std::fmt; +use alloc::fmt; +use alloc::vec::Vec; +use core::array::TryFromSliceError; use subtle::{Choice, ConstantTimeEq}; diff --git a/src/note_encryption.rs b/src/note_encryption.rs index 48f03afa..597349d4 100644 --- a/src/note_encryption.rs +++ b/src/note_encryption.rs @@ -2,8 +2,8 @@ //! //! NB: the example code is only covering the post-Canopy case. +use alloc::vec::Vec; use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams}; -use byteorder::{LittleEndian, WriteBytesExt}; use ff::PrimeField; use memuse::DynamicUsage; use rand_core::RngCore; @@ -193,9 +193,7 @@ impl Domain for SaplingDomain { Rseed::AfterZip212(_) => 2, }; input[1..12].copy_from_slice(¬e.recipient().diversifier().0); - (&mut input[12..20]) - .write_u64::(note.value().inner()) - .unwrap(); + input[12..20].copy_from_slice(¬e.value().inner().to_le_bytes()); match note.rseed() { Rseed::BeforeZip212(rcm) => { @@ -296,7 +294,7 @@ impl BatchDomain for SaplingDomain { let (shared_secrets, ephemeral_keys): (Vec<_>, Vec<_>) = items.unzip(); SharedSecret::batch_to_affine(shared_secrets) - .zip(ephemeral_keys.into_iter()) + .zip(ephemeral_keys) .map(|(secret, ephemeral_key)| { secret.map(|dhsecret| SharedSecret::kdf_sapling_inner(dhsecret, ephemeral_key)) }) @@ -309,7 +307,7 @@ impl BatchDomain for SaplingDomain { let ephemeral_keys: Vec<_> = ephemeral_keys.collect(); let epks = jubjub::AffinePoint::batch_from_bytes(ephemeral_keys.iter().map(|b| b.0)); epks.into_iter() - .zip(ephemeral_keys.into_iter()) + .zip(ephemeral_keys) .map(|(epk, ephemeral_key)| { ( Option::from(epk) @@ -462,6 +460,7 @@ pub fn try_sapling_output_recovery( #[cfg(test)] mod tests { + use alloc::vec::Vec; use chacha20poly1305::{ aead::{AeadInPlace, KeyInit}, ChaCha20Poly1305, @@ -486,7 +485,7 @@ mod tests { use crate::{ bundle::{GrothProofBytes, OutputDescription}, - circuit::GROTH_PROOF_SIZE, + constants::GROTH_PROOF_SIZE, keys::{DiversifiedTransmissionKey, EphemeralSecretKey, OutgoingViewingKey}, note::ExtractedNoteCommitment, note_encryption::PreparedIncomingViewingKey, diff --git a/src/pczt.rs b/src/pczt.rs new file mode 100644 index 00000000..c0badbb4 --- /dev/null +++ b/src/pczt.rs @@ -0,0 +1,341 @@ +//! PCZT support for Sapling. + +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; +use core::fmt; + +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 verify; +pub use verify::VerifyError; + +mod io_finalizer; +pub use io_finalizer::IoFinalizerError; + +mod updater; +pub use updater::{OutputUpdater, SpendUpdater, Updater, UpdaterError}; + +#[cfg(feature = "circuit")] +mod prover; +#[cfg(feature = "circuit")] +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. + /// - 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<[u8; 32]>, + + /// 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, + + /// The user-facing address to which this output is being sent, if any. + /// + /// - This is set by an Updater. + /// - Signers must parse this address (if present) and confirm that it contains + /// `recipient` (either directly, or e.g. as a receiver within a Unified Address). + pub(crate) user_address: Option, + + /// Proprietary fields related to the note being created. + pub(crate) proprietary: BTreeMap>, +} + +impl fmt::Debug for Output { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::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("user_address", &self.user_address) + .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, +} + +impl Zip32Derivation { + /// Extracts the ZIP 32 account index from this derivation path. + /// + /// Returns `None` if the seed fingerprints don't match, or if this is a non-standard + /// derivation path. + pub fn extract_account_index( + &self, + seed_fp: &zip32::fingerprint::SeedFingerprint, + expected_coin_type: zip32::ChildIndex, + ) -> Option { + if self.seed_fingerprint == seed_fp.to_bytes() { + match &self.derivation_path[..] { + [purpose, coin_type, account_index] + if purpose == &zip32::ChildIndex::hardened(32) + && coin_type == &expected_coin_type => + { + Some( + zip32::AccountId::try_from(account_index.index() - (1 << 31)) + .expect("zip32::ChildIndex only supports hardened"), + ) + } + _ => None, + } + } else { + None + } + } +} diff --git a/src/pczt/io_finalizer.rs b/src/pczt/io_finalizer.rs new file mode 100644 index 00000000..81642296 --- /dev/null +++ b/src/pczt/io_finalizer.rs @@ -0,0 +1,92 @@ +use alloc::vec::Vec; +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 finalizing the I/O for 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 00000000..dc11af4d --- /dev/null +++ b/src/pczt/parse.rs @@ -0,0 +1,298 @@ +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; + +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, + user_address: 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 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, + user_address, + 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 00000000..7a9db7a7 --- /dev/null +++ b/src/pczt/prover.rs @@ -0,0 +1,105 @@ +use rand::{CryptoRng, RngCore}; + +use crate::{ + prover::{OutputProver, SpendProver}, + Note, Rseed, +}; + +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 + .map(Rseed::AfterZip212) + .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 00000000..90776357 --- /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 00000000..9a6cf667 --- /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/pczt/updater.rs b/src/pczt/updater.rs new file mode 100644 index 00000000..4dd025c6 --- /dev/null +++ b/src/pczt/updater.rs @@ -0,0 +1,110 @@ +use alloc::string::String; +use alloc::vec::Vec; + +use crate::ProofGenerationKey; + +use super::{Bundle, Output, Spend, Zip32Derivation}; + +impl Bundle { + /// Updates the bundle with information provided in the given closure. + pub fn update_with(&mut self, f: F) -> Result<(), UpdaterError> + where + F: FnOnce(Updater<'_>) -> Result<(), UpdaterError>, + { + f(Updater(self)) + } +} + +/// An updater for a Sapling PCZT bundle. +pub struct Updater<'a>(&'a mut Bundle); + +impl Updater<'_> { + /// Provides read access to the bundle being updated. + pub fn bundle(&self) -> &Bundle { + self.0 + } + + /// Updates the spend at the given index with information provided in the given + /// closure. + pub fn update_spend_with(&mut self, index: usize, f: F) -> Result<(), UpdaterError> + where + F: FnOnce(SpendUpdater<'_>) -> Result<(), UpdaterError>, + { + f(SpendUpdater( + self.0 + .spends + .get_mut(index) + .ok_or(UpdaterError::InvalidIndex)?, + )) + } + + /// Updates the output at the given index with information provided in the given + /// closure. + pub fn update_output_with(&mut self, index: usize, f: F) -> Result<(), UpdaterError> + where + F: FnOnce(OutputUpdater<'_>) -> Result<(), UpdaterError>, + { + f(OutputUpdater( + self.0 + .outputs + .get_mut(index) + .ok_or(UpdaterError::InvalidIndex)?, + )) + } +} + +/// An updater for a Sapling PCZT spend. +pub struct SpendUpdater<'a>(&'a mut Spend); + +impl SpendUpdater<'_> { + /// Sets the proof generation key for this spend. + /// + /// Returns an error if the proof generation key does not match the spend. + pub fn set_proof_generation_key( + &mut self, + proof_generation_key: ProofGenerationKey, + ) -> Result<(), UpdaterError> { + // TODO: Verify that the proof generation key matches the spend, if possible. + self.0.proof_generation_key = Some(proof_generation_key); + Ok(()) + } + + /// Sets the ZIP 32 derivation path for the spent note's signing key. + pub fn set_zip32_derivation(&mut self, derivation: Zip32Derivation) { + self.0.zip32_derivation = Some(derivation); + } + + /// Stores the given proprietary value at the given key. + pub fn set_proprietary(&mut self, key: String, value: Vec) { + self.0.proprietary.insert(key, value); + } +} + +/// An updater for a Sapling PCZT output. +pub struct OutputUpdater<'a>(&'a mut Output); + +impl OutputUpdater<'_> { + /// Sets the ZIP 32 derivation path for the new note's signing key. + pub fn set_zip32_derivation(&mut self, derivation: Zip32Derivation) { + self.0.zip32_derivation = Some(derivation); + } + + /// Sets the user-facing address that the new note is being sent to. + pub fn set_user_address(&mut self, user_address: String) { + self.0.user_address = Some(user_address); + } + + /// Stores the given proprietary value at the given key. + pub fn set_proprietary(&mut self, key: String, value: Vec) { + self.0.proprietary.insert(key, value); + } +} + +/// Errors that can occur while updating a Sapling bundle in a PCZT. +#[derive(Debug)] +pub enum UpdaterError { + /// An out-of-bounds index was provided when looking up a spend or output. + InvalidIndex, + /// The provided `proof_generation_key` does not match the spend. + WrongProofGenerationKey, +} diff --git a/src/pczt/verify.rs b/src/pczt/verify.rs new file mode 100644 index 00000000..2aae492c --- /dev/null +++ b/src/pczt/verify.rs @@ -0,0 +1,188 @@ +use crate::{keys::FullViewingKey, value::ValueCommitment, Note, ViewingKey}; + +impl super::Spend { + /// Verifies that the `cv` field is consistent with the note fields. + /// + /// Requires that the following optional fields are set: + /// - `value` + /// - `rcv` + pub fn verify_cv(&self) -> Result<(), VerifyError> { + let value = self.value.ok_or(VerifyError::MissingValue)?; + let rcv = self + .rcv + .clone() + .ok_or(VerifyError::MissingValueCommitTrapdoor)?; + + let cv_net = ValueCommitment::derive(value, rcv); + if cv_net.to_bytes() == self.cv.to_bytes() { + Ok(()) + } else { + Err(VerifyError::InvalidValueCommitment) + } + } + + /// Returns the [`ViewingKey`] to use when validating this note. + /// + /// Handles dummy notes when the `value` field is set. + fn vk_for_validation( + &self, + expected_fvk: Option<&FullViewingKey>, + ) -> Result { + let vk = self + .proof_generation_key + .as_ref() + .map(|proof_generation_key| proof_generation_key.to_viewing_key()); + + match (expected_fvk, vk, self.value.as_ref()) { + (Some(expected_fvk), Some(vk), _) + if vk.ak == expected_fvk.vk.ak && vk.nk == expected_fvk.vk.nk => + { + Ok(vk) + } + // `expected_fvk` is ignored if the spent note is a dummy note. + (Some(_), Some(vk), Some(value)) if value.inner() == 0 => Ok(vk), + (Some(_), Some(_), _) => Err(VerifyError::MismatchedFullViewingKey), + (Some(expected_fvk), None, _) => Ok(expected_fvk.vk.clone()), + (None, Some(vk), _) => Ok(vk), + (None, None, _) => Err(VerifyError::MissingProofGenerationKey), + } + } + + /// Verifies that the `nullifier` field is consistent with the note fields. + /// + /// Requires that the following optional fields are set: + /// - `recipient` + /// - `value` + /// - `rseed` + /// - `witness` + /// + /// In addition, at least one of the `proof_generation_key` field or `expected_fvk` + /// must be provided. + /// + /// The provided [`FullViewingKey`] is ignored if the spent note is a dummy note. + /// Otherwise, it will be checked against the `proof_generation_key` field (if both + /// are set). + pub fn verify_nullifier( + &self, + expected_fvk: Option<&FullViewingKey>, + ) -> Result<(), VerifyError> { + let vk = self.vk_for_validation(expected_fvk)?; + + let note = Note::from_parts( + self.recipient.ok_or(VerifyError::MissingRecipient)?, + self.value.ok_or(VerifyError::MissingValue)?, + self.rseed.ok_or(VerifyError::MissingRandomSeed)?, + ); + + // We need both the note and the VK to verify the nullifier; we have everything + // needed to also verify that the correct VK was provided (the nullifier check + // itself only constrains `nk` within the VK). + if vk.to_payment_address(*note.recipient().diversifier()) != Some(note.recipient()) { + return Err(VerifyError::WrongFvkForNote); + } + + let merkle_path = self.witness().as_ref().ok_or(VerifyError::MissingWitness)?; + + if note.nf(&vk.nk, merkle_path.position().into()) == self.nullifier { + Ok(()) + } else { + Err(VerifyError::InvalidNullifier) + } + } + + /// Verifies that the `rk` field is consistent with the given FVK. + /// + /// Requires that the following optional fields are set: + /// - `alpha` + /// + /// The provided [`FullViewingKey`] is ignored if the spent note is a dummy note + /// (which can only be determined if the `value` field is set). Otherwise, it will be + /// checked against the `proof_generation_key` field (if set). + pub fn verify_rk(&self, expected_fvk: Option<&FullViewingKey>) -> Result<(), VerifyError> { + let vk = self.vk_for_validation(expected_fvk)?; + + let alpha = self + .alpha + .as_ref() + .ok_or(VerifyError::MissingSpendAuthRandomizer)?; + + if vk.ak.randomize(alpha) == self.rk { + Ok(()) + } else { + Err(VerifyError::InvalidRandomizedVerificationKey) + } + } +} + +impl super::Output { + /// Verifies that the `cv` field is consistent with the note fields. + /// + /// Requires that the following optional fields are set: + /// - `value` + /// - `rcv` + pub fn verify_cv(&self) -> Result<(), VerifyError> { + let value = self.value.ok_or(VerifyError::MissingValue)?; + let rcv = self + .rcv + .clone() + .ok_or(VerifyError::MissingValueCommitTrapdoor)?; + + let cv_net = ValueCommitment::derive(value, rcv); + if cv_net.to_bytes() == self.cv.to_bytes() { + Ok(()) + } else { + Err(VerifyError::InvalidValueCommitment) + } + } + + /// Verifies that the `cmu` field is consistent with the note fields. + /// + /// Requires that the following optional fields are set: + /// - `recipient` + /// - `value` + /// - `rseed` + pub fn verify_note_commitment(&self) -> Result<(), VerifyError> { + let note = Note::from_parts( + self.recipient.ok_or(VerifyError::MissingRecipient)?, + self.value.ok_or(VerifyError::MissingValue)?, + crate::Rseed::AfterZip212(self.rseed.ok_or(VerifyError::MissingRandomSeed)?), + ); + + if note.cmu() == self.cmu { + Ok(()) + } else { + Err(VerifyError::InvalidExtractedNoteCommitment) + } + } +} + +/// Errors that can occur while verifying a PCZT bundle. +#[derive(Debug)] +pub enum VerifyError { + /// The output note's components do not produce the expected `cmx`. + InvalidExtractedNoteCommitment, + /// The spent note's components do not produce the expected `nullifier`. + InvalidNullifier, + /// The Spend's FVK and `alpha` do not produce the expected `rk`. + InvalidRandomizedVerificationKey, + /// The action's `cv_net` does not match the provided note values and `rcv`. + InvalidValueCommitment, + /// The spend or output's `fvk` field does not match the provided FVK. + MismatchedFullViewingKey, + /// Dummy notes must have their `proof_generation_key` field set in order to be verified. + MissingProofGenerationKey, + /// `nullifier` verification requires `rseed` to be set. + MissingRandomSeed, + /// `nullifier` verification requires `recipient` to be set. + MissingRecipient, + /// `rk` verification requires `alpha` to be set. + MissingSpendAuthRandomizer, + /// Verification requires all `value` fields to be set. + MissingValue, + /// `cv_net` verification requires `rcv` to be set. + MissingValueCommitTrapdoor, + /// `nullifier` verification requires `witness` to be set. + MissingWitness, + /// The provided `fvk` does not own the spent note. + WrongFvkForNote, +} diff --git a/src/pedersen_hash.rs b/src/pedersen_hash.rs index 20cc40a5..f2193481 100644 --- a/src/pedersen_hash.rs +++ b/src/pedersen_hash.rs @@ -3,14 +3,12 @@ #[cfg(test)] pub(crate) mod test_vectors; -use byteorder::{ByteOrder, LittleEndian}; +use alloc::vec::Vec; +use core::ops::{AddAssign, Neg}; use ff::PrimeField; use group::Group; -use std::ops::{AddAssign, Neg}; -use super::constants::{ - PEDERSEN_HASH_CHUNKS_PER_GENERATOR, PEDERSEN_HASH_EXP_TABLE, PEDERSEN_HASH_EXP_WINDOW_SIZE, -}; +use super::constants::{PEDERSEN_HASH_CHUNKS_PER_GENERATOR, PEDERSEN_HASH_EXP_WINDOW_SIZE}; #[derive(Copy, Clone)] pub enum Personalization { @@ -35,13 +33,10 @@ pub fn pedersen_hash(personalization: Personalization, bits: I) -> jubjub::Su where I: IntoIterator, { - let mut bits = personalization - .get_bits() - .into_iter() - .chain(bits.into_iter()); + let mut bits = personalization.get_bits().into_iter().chain(bits); let mut result = jubjub::SubgroupPoint::identity(); - let mut generators = PEDERSEN_HASH_EXP_TABLE.iter(); + let mut generators = crate::constants::PEDERSEN_HASH_EXP_TABLE.iter(); loop { let mut acc = jubjub::Fr::zero(); @@ -94,7 +89,9 @@ where let acc = acc.to_repr(); let num_limbs: usize = acc.as_ref().len() / 8; let mut limbs = vec![0u64; num_limbs + 1]; - LittleEndian::read_u64_into(acc.as_ref(), &mut limbs[..num_limbs]); + for (src, dst) in acc.chunks_exact(8).zip(limbs[..num_limbs].iter_mut()) { + *dst = u64::from_le_bytes(src.try_into().expect("correct length")); + } let mut tmp = jubjub::SubgroupPoint::identity(); @@ -124,6 +121,7 @@ where #[cfg(test)] pub mod test { + use alloc::string::ToString; use group::Curve; use super::*; diff --git a/src/pedersen_hash/test_vectors.rs b/src/pedersen_hash/test_vectors.rs index 4d051afc..84cd8d84 100644 --- a/src/pedersen_hash/test_vectors.rs +++ b/src/pedersen_hash/test_vectors.rs @@ -1,6 +1,7 @@ //! Test vectors from https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/sapling_pedersen.py use super::{test::TestVector, Personalization}; +use alloc::vec::Vec; pub fn get_vectors<'a>() -> Vec> { vec![ diff --git a/src/prover.rs b/src/prover.rs index d9b874c4..38c34b09 100644 --- a/src/prover.rs +++ b/src/prover.rs @@ -6,7 +6,9 @@ use rand_core::RngCore; use crate::{ bundle::GrothProofBytes, - circuit::{self, GROTH_PROOF_SIZE}, + circuit, + constants::GROTH_PROOF_SIZE, + keys::EphemeralSecretKey, value::{NoteValue, ValueCommitTrapdoor}, MerklePath, }; @@ -56,7 +58,7 @@ pub trait OutputProver { /// /// Returns `None` if `diversifier` is not a valid Sapling diversifier. fn prepare_circuit( - esk: jubjub::Fr, + esk: &EphemeralSecretKey, payment_address: PaymentAddress, rcm: jubjub::Fr, value: NoteValue, @@ -113,7 +115,7 @@ impl SpendProver for SpendParameters { .path_elems() .iter() .enumerate() - .map(|(i, node)| Some(((*node).into(), pos >> i & 0x1 == 1))) + .map(|(i, node)| Some(((*node).into(), (pos >> i) & 0x1 == 1))) .collect(), anchor: Some(anchor), }) @@ -136,7 +138,7 @@ impl OutputProver for OutputParameters { type Proof = Proof; fn prepare_circuit( - esk: jubjub::Fr, + esk: &EphemeralSecretKey, payment_address: PaymentAddress, rcm: jubjub::Fr, value: NoteValue, @@ -153,7 +155,7 @@ impl OutputProver for OutputParameters { value_commitment_opening: Some(value_commitment_opening), payment_address: Some(payment_address), commitment_randomness: Some(rcm), - esk: Some(esk), + esk: Some(esk.0), } } @@ -178,7 +180,9 @@ pub mod mock { use super::{OutputProver, SpendProver}; use crate::{ bundle::GrothProofBytes, - circuit::{self, ValueCommitmentOpening, GROTH_PROOF_SIZE}, + circuit::{self, ValueCommitmentOpening}, + constants::GROTH_PROOF_SIZE, + keys::EphemeralSecretKey, value::{NoteValue, ValueCommitTrapdoor}, Diversifier, MerklePath, PaymentAddress, ProofGenerationKey, Rseed, }; @@ -235,7 +239,7 @@ pub mod mock { type Proof = GrothProofBytes; fn prepare_circuit( - esk: jubjub::Fr, + esk: &EphemeralSecretKey, payment_address: PaymentAddress, rcm: jubjub::Fr, value: NoteValue, @@ -248,7 +252,7 @@ pub mod mock { }), payment_address: Some(payment_address), commitment_randomness: Some(rcm), - esk: Some(esk), + esk: Some(esk.0), } } diff --git a/src/test_vectors/note_encryption.rs b/src/test_vectors/note_encryption.rs index 09209f29..9f7073c6 100644 --- a/src/test_vectors/note_encryption.rs +++ b/src/test_vectors/note_encryption.rs @@ -1,3 +1,5 @@ +use alloc::vec::Vec; + pub(crate) struct TestVector { pub ovk: [u8; 32], pub ivk: [u8; 32], diff --git a/src/test_vectors/signatures.rs b/src/test_vectors/signatures.rs index 7ce341cf..bec89fbd 100644 --- a/src/test_vectors/signatures.rs +++ b/src/test_vectors/signatures.rs @@ -1,3 +1,5 @@ +use alloc::vec::Vec; + pub(crate) struct TestVector { pub(crate) sk: [u8; 32], pub(crate) vk: [u8; 32], diff --git a/src/tree.rs b/src/tree.rs index 1efb9d54..11c3906d 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -4,7 +4,8 @@ use incrementalmerkletree::{Hashable, Level}; use lazy_static::lazy_static; use subtle::CtOption; -use std::fmt; +use alloc::vec::Vec; +use core::fmt; use super::{ note::ExtractedNoteCommitment, @@ -20,14 +21,16 @@ pub type MerklePath = incrementalmerkletree::MerklePath = { - let mut v = vec![Node::empty_leaf()]; - for d in 0..NOTE_COMMITMENT_TREE_DEPTH { - let next = Node::combine(d.into(), &v[usize::from(d)], &v[usize::from(d)]); - v.push(next); - } - v - }; + static ref EMPTY_ROOTS: Vec = empty_roots(); +} + +fn empty_roots() -> Vec { + let mut v = vec![Node::empty_leaf()]; + for d in 0..NOTE_COMMITMENT_TREE_DEPTH { + let next = Node::combine(d.into(), &v[usize::from(d)], &v[usize::from(d)]); + v.push(next); + } + v } /// Compute a parent node in the Sapling commitment tree given its two children. @@ -93,6 +96,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) @@ -140,6 +147,7 @@ impl Node { } /// Returns the wrapped value + #[cfg(feature = "circuit")] pub(crate) fn inner(&self) -> &jubjub::Base { &self.0 } diff --git a/src/value/sums.rs b/src/value/sums.rs index b598e0e5..5edb3b5f 100644 --- a/src/value/sums.rs +++ b/src/value/sums.rs @@ -18,6 +18,7 @@ impl fmt::Display for OverflowError { } } +#[cfg(feature = "std")] impl std::error::Error for OverflowError {} /// A sum of Sapling note values. @@ -38,6 +39,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 { @@ -59,14 +74,16 @@ impl Sub for ValueSum { } impl<'a> Sum<&'a NoteValue> for Result { - fn sum>(iter: I) -> Self { - iter.fold(Ok(ValueSum(0)), |acc, v| (acc? + *v).ok_or(OverflowError)) + fn sum>(mut iter: I) -> Self { + iter.try_fold(ValueSum(0), |acc, v| acc + *v) + .ok_or(OverflowError) } } impl Sum for Result { - fn sum>(iter: I) -> Self { - iter.fold(Ok(ValueSum(0)), |acc, v| (acc? + v).ok_or(OverflowError)) + fn sum>(mut iter: I) -> Self { + iter.try_fold(ValueSum(0), |acc, v| acc + v) + .ok_or(OverflowError) } } diff --git a/src/verifier/batch.rs b/src/verifier/batch.rs index 0eb196e0..d6306202 100644 --- a/src/verifier/batch.rs +++ b/src/verifier/batch.rs @@ -140,7 +140,10 @@ impl BatchValidator { } if let Err(e) = self.signatures.verify(&mut rng) { + #[cfg(feature = "std")] tracing::debug!("Signature batch validation failed: {}", e); + #[cfg(not(feature = "std"))] + tracing::debug!("Signature batch validation failed: {:?}", e); return false; } diff --git a/src/zip32.rs b/src/zip32.rs index c2b7ea63..66ae8f91 100644 --- a/src/zip32.rs +++ b/src/zip32.rs @@ -6,14 +6,13 @@ use aes::Aes256; use blake2b_simd::Params as Blake2bParams; -use byteorder::{ByteOrder, LittleEndian, ReadBytesExt, WriteBytesExt}; use fpe::ff1::{BinaryNumeralString, FF1}; use subtle::CtOption; use zcash_spec::PrfExpand; use zip32::{ChainCode, ChildIndex, DiversifierIndex, Scope}; -use std::io::{self, Read, Write}; -use std::ops::AddAssign; +use core::ops::AddAssign; +use core2::io::{self, Read, Write}; use super::{Diversifier, NullifierDerivingKey, PaymentAddress, ViewingKey}; use crate::note_encryption::PreparedIncomingViewingKey; @@ -269,7 +268,7 @@ pub struct ExtendedSpendingKey { dk: DiversifierKey, } -impl std::cmp::PartialEq for ExtendedSpendingKey { +impl core::cmp::PartialEq for ExtendedSpendingKey { fn eq(&self, rhs: &ExtendedSpendingKey) -> bool { self.depth == rhs.depth && self.parent_fvk_tag == rhs.parent_fvk_tag @@ -282,8 +281,8 @@ impl std::cmp::PartialEq for ExtendedSpendingKey { } } -impl std::fmt::Debug for ExtendedSpendingKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { +impl core::fmt::Debug for ExtendedSpendingKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { write!( f, "ExtendedSpendingKey(d = {}, tag_p = {:?}, i = {:?})", @@ -354,17 +353,20 @@ impl ExtendedSpendingKey { /// Reads and decodes the encoded form of the extended spending key as defined in /// [ZIP 32](https://zips.z.cash/zip-0032) from the provided reader. pub fn read(mut reader: R) -> io::Result { - let depth = reader.read_u8()?; + let mut depth = [0; 1]; + reader.read_exact(&mut depth)?; + let depth = depth[0]; let mut tag = [0; 4]; reader.read_exact(&mut tag)?; - let child_index = reader.read_u32::().and_then(|i| { - KeyIndex::new(depth, i).ok_or_else(|| { - io::Error::new( - io::ErrorKind::Unsupported, + let mut child_index_bytes = [0; 4]; + reader.read_exact(&mut child_index_bytes)?; + let child_index = + KeyIndex::new(depth, u32::from_le_bytes(child_index_bytes)).ok_or_else(|| { + core2::io::Error::new( + core2::io::ErrorKind::InvalidData, "Unsupported child index in encoding", ) - }) - })?; + })?; let mut c = [0; 32]; reader.read_exact(&mut c)?; let expsk = ExpandedSpendingKey::read(&mut reader)?; @@ -419,8 +421,7 @@ impl ExtendedSpendingKey { pub fn derive_child(&self, i: ChildIndex) -> Self { let fvk = FullViewingKey::from_expanded_spending_key(&self.expsk); let tmp = { - let mut le_i = [0; 4]; - LittleEndian::write_u32(&mut le_i, i.index()); + let le_i = i.index().to_le_bytes(); PrfExpand::SAPLING_ZIP32_CHILD_HARDENED.with( self.chain_code.as_bytes(), &self.expsk.to_bytes(), @@ -529,7 +530,7 @@ pub struct ExtendedFullViewingKey { pub(crate) dk: DiversifierKey, } -impl std::cmp::PartialEq for ExtendedFullViewingKey { +impl core::cmp::PartialEq for ExtendedFullViewingKey { fn eq(&self, rhs: &ExtendedFullViewingKey) -> bool { self.depth == rhs.depth && self.parent_fvk_tag == rhs.parent_fvk_tag @@ -542,8 +543,8 @@ impl std::cmp::PartialEq for ExtendedFullViewingKey { } } -impl std::fmt::Debug for ExtendedFullViewingKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { +impl core::fmt::Debug for ExtendedFullViewingKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { write!( f, "ExtendedFullViewingKey(d = {}, tag_p = {:?}, i = {:?})", @@ -554,17 +555,20 @@ impl std::fmt::Debug for ExtendedFullViewingKey { impl ExtendedFullViewingKey { pub fn read(mut reader: R) -> io::Result { - let depth = reader.read_u8()?; + let mut depth = [0; 1]; + reader.read_exact(&mut depth)?; + let depth = depth[0]; let mut tag = [0; 4]; reader.read_exact(&mut tag)?; - let child_index = reader.read_u32::().and_then(|i| { - KeyIndex::new(depth, i).ok_or_else(|| { - io::Error::new( - io::ErrorKind::Unsupported, + let mut child_index_bytes = [0; 4]; + reader.read_exact(&mut child_index_bytes)?; + let child_index = + KeyIndex::new(depth, u32::from_le_bytes(child_index_bytes)).ok_or_else(|| { + core2::io::Error::new( + core2::io::ErrorKind::InvalidData, "Unsupported child index in encoding", ) - }) - })?; + })?; let mut c = [0; 32]; reader.read_exact(&mut c)?; let fvk = FullViewingKey::read(&mut reader)?; @@ -582,9 +586,9 @@ impl ExtendedFullViewingKey { } pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_u8(self.depth)?; + writer.write_all(&[self.depth])?; writer.write_all(&self.parent_fvk_tag.0)?; - writer.write_u32::(self.child_index.index())?; + writer.write_all(&self.child_index.index().to_le_bytes())?; writer.write_all(self.chain_code.as_bytes())?; writer.write_all(&self.fvk.to_bytes())?; writer.write_all(&self.dk.0)?; @@ -705,6 +709,11 @@ impl DiversifiableFullViewingKey { &self.fvk } + /// Returns the internal [`FullViewingKey`] component of this diversifiable full viewing key. + pub fn to_internal_fvk(&self) -> FullViewingKey { + self.derive_internal().fvk + } + /// Derives a nullifier-deriving key for the provided scope. /// /// This API is provided so that nullifiers for change notes can be correctly computed.