orchard 0.15.5

The Orchard shielded transaction protocol
Documentation
//! Feature-gated Orchard fixtures for Halo2 verifier fingerprints.
//!
//! # What these fixtures do and do not establish
//!
//! Each capture runs the deployed verifier once and exports the assembled MSM, transcript, and
//! challenges as a Lean fixture. This checks that Ironwood's Lean model reproduces one *concrete*
//! Rust verifier run; it is not a verifier soundness proof.
//!
//! Two limitations are load-bearing for anyone consuming the exported fixtures:
//!
//! * **Honest-run captures are blind off the honest locus; random captures cover the rest.** The
//!   honest captures export accepting runs only: the Halo2 honest exporter fails fast on a
//!   non-identity capture, and the negative captures in `rejected` confirm in Rust that a
//!   tampered/short/desynchronized proof assembles a non-identity MSM and is rejected by the
//!   deployed verifier without being handed to Lean. `MsmMatch` pins the full coefficient
//!   structure of those accepting runs — not merely acceptance — so a trivially-accepting Lean
//!   `assemble` would already fail it. The residual an honest capture cannot see — a Lean
//!   `assemble` that agrees at honest proofs yet diverges elsewhere (e.g. recomputing a
//!   transcript-claimed evaluation instead of reading it) — is covered by the `random` captures:
//!   the deployed verifier run on a random proof string, exported *match-only* (deliberately
//!   non-accepting, no eval theorem; seeds `0x52`/`0x72`/`0x33`, ASCII `R`/`r`/`3`,
//!   nothing-up-my-sleeve). What stays Rust-only is rejection-path modeling: which inputs reach
//!   the MSM at all.
//! * **The exported Fiat–Shamir schedule is not injective with respect to real transcripts.** The
//!   dumper models each squeeze by appending the challenge as a `TranscriptElt.scalar`, the same
//!   constructor used for proof-read scalars, whereas the deployed Blake2b transcript uses a
//!   domain-separator byte and never absorbs the challenge. The schedule faithfully pins one
//!   concrete run but must not be relied on downstream as a transcript-binding / uniqueness model.

use alloc::vec::Vec;

use halo2_proofs::plonk::fingerprint::{capture_proof_fingerprint, ChallengeRecorder};
use halo2_proofs::transcript::Challenge255;
use incrementalmerkletree::Hashable;
use pasta_curves::vesta;
use rand::SeedableRng;
use rand_chacha::ChaCha20Rng;

use super::{OrchardCircuitVersion, ProvingKey, VerifyingKey, K};
use crate::{
    builder::{Builder, BundleType},
    bundle::BundleVersion,
    constants::MERKLE_DEPTH_ORCHARD,
    tree::MerkleHashOrchard,
};

mod random;
mod rejected;

fn fixture_rng(seed: u8) -> ChaCha20Rng {
    ChaCha20Rng::from_seed([seed; 32])
}

fn build_fixture_bundle(
    rng: &mut ChaCha20Rng,
    pk: &ProvingKey,
    num_actions: u8,
) -> crate::Bundle<crate::bundle::Authorized, i64> {
    let bundle_version = BundleVersion::orchard_v3();
    let builder = Builder::new(
        BundleType::Transactional {
            bundle_required: true,
            pad_to_minimum: Some(num_actions),
        },
        bundle_version,
        bundle_version.default_flags(),
        MerkleHashOrchard::empty_root((MERKLE_DEPTH_ORCHARD as u8).into()).into(),
    )
    .unwrap();
    let bundle = builder.build::<i64>(&mut *rng).unwrap().unwrap().0;
    assert_eq!(bundle.actions().len(), usize::from(num_actions));
    assert!(!bundle.flags().cross_address_enabled());

    bundle
        .create_proof(pk, &mut *rng)
        .unwrap()
        .apply_signatures(&mut *rng, [0; 32], &[])
        .unwrap()
}

fn raw_instances(instances: &[super::Instance]) -> Vec<Vec<Vec<vesta::Scalar>>> {
    instances
        .iter()
        .map(|instance| {
            instance
                .to_halo2_instance()
                .iter()
                .map(|column| column.to_vec())
                .collect()
        })
        .collect()
}

fn raw_instance_refs(raw_instances: &[Vec<Vec<vesta::Scalar>>]) -> Vec<Vec<&[vesta::Scalar]>> {
    raw_instances
        .iter()
        .map(|instance| instance.iter().map(|column| &column[..]).collect())
        .collect()
}

fn assert_pinned_verifying_key(vk: &VerifyingKey) {
    assert_eq!(
        format!("{:#?}\n", vk.vk.pinned()),
        include_str!("../../circuit_data/circuit_description_post_nu6_3").replace("\r\n", "\n")
    );
}

fn capture_fixture(seed: u8, num_actions: u8, namespace: &str, output_var: &str) {
    let mut rng = fixture_rng(seed);
    let pk = ProvingKey::build(OrchardCircuitVersion::PostNu6_3);
    let vk = VerifyingKey::build(OrchardCircuitVersion::PostNu6_3);
    assert!(vk.supports_cross_address_restriction());
    assert_pinned_verifying_key(&vk);

    let bundle = build_fixture_bundle(&mut rng, &pk, num_actions);
    let instances = bundle.to_instances();
    let proof = bundle.authorization().proof().clone();
    assert!(bundle.verify_proof(&vk).is_ok());

    let raw_instances = raw_instances(&instances);
    let raw_instance_refs = raw_instance_refs(&raw_instances);
    let raw_instance_refs: Vec<_> = raw_instance_refs
        .iter()
        .map(|instance| &instance[..])
        .collect();

    let mut transcript = ChallengeRecorder::<_, _, Challenge255<_>>::init(&proof.0[..]);
    let msm =
        capture_proof_fingerprint(&vk.params, &vk.vk, &raw_instance_refs, &mut transcript).unwrap();

    assert!(!transcript.challenges.is_empty());
    assert!(
        msm.clone().eval(),
        "captured Orchard fingerprint must be the identity for a valid proof"
    );
    std::eprintln!(
        "Captured {num_actions}-action Orchard verifier fingerprint at k={} with {} challenges",
        K,
        transcript.challenges.len(),
    );

    let fixture = vk.vk.dump_vesta_lean_fixture(
        namespace,
        "PostNu6_3",
        K,
        &raw_instance_refs,
        &transcript,
        &msm,
    );
    if let Some(path) = std::env::var_os(output_var) {
        std::fs::write(std::path::PathBuf::from(path), fixture).unwrap();
    }
}

/// Regenerate the deterministic checked-in single-action Ironwood fixture with:
///
/// ```text
/// ORCHARD_LEAN_SINGLE_FIXTURE_OUT=/path/to/ironwood/Zcash/Snark/Fixtures/SingleAction/Fixture.lean \
///   cargo test --features verifier-fingerprint circuit::fingerprint::fingerprint_capture -- --exact
/// ```
#[test]
fn fingerprint_capture() {
    capture_fixture(
        0x53,
        1,
        "Zcash.Snark.Fixture",
        "ORCHARD_LEAN_SINGLE_FIXTURE_OUT",
    );
}

/// Regenerate the deterministic checked-in two-action Ironwood fixture with:
///
/// ```text
/// ORCHARD_LEAN_MULTI_FIXTURE_OUT=/path/to/ironwood/Zcash/Snark/Fixtures/MultiAction/Fixture.lean \
///   cargo test --features verifier-fingerprint \
///     circuit::fingerprint::fingerprint_capture_two_actions -- --exact
/// ```
///
/// The real two-action bundle exercises the verifier's per-sub-proof read schedule, expression
/// folds, and query wiring that a single-action capture never reaches. See
/// https://github.com/zcash/ironwood/issues/17.
#[test]
fn fingerprint_capture_two_actions() {
    capture_fixture(
        0x4d,
        2,
        "Zcash.Snark.Fixture2",
        "ORCHARD_LEAN_MULTI_FIXTURE_OUT",
    );
}

/// Regenerate the deterministic single-action random match-only fixture (see `random`) with:
///
/// ```text
/// ORCHARD_LEAN_SINGLE_RANDOM_FIXTURE_OUT=/path/to/ironwood/Zcash/Snark/Fixtures/SingleActionRandom/Fixture.lean \
///   cargo test --features verifier-fingerprint \
///     circuit::fingerprint::fingerprint_capture_random -- --exact
/// ```
///
/// `ORCHARD_LEAN_SINGLE_RANDOM_PROOF_OUT` additionally writes the fabricated proof byte string as
/// hex (a non-Lean sibling artifact for byte-level decode modeling).
#[test]
fn fingerprint_capture_random() {
    random::capture_random_fixture(
        0x52,
        1,
        "Zcash.Snark.FixtureRandom",
        "ORCHARD_LEAN_SINGLE_RANDOM_FIXTURE_OUT",
        "ORCHARD_LEAN_SINGLE_RANDOM_PROOF_OUT",
    );
}

/// Regenerate the deterministic two-action random match-only fixture (see `random`) with:
///
/// ```text
/// ORCHARD_LEAN_MULTI_RANDOM_FIXTURE_OUT=/path/to/ironwood/Zcash/Snark/Fixtures/MultiActionRandom/Fixture.lean \
///   cargo test --features verifier-fingerprint \
///     circuit::fingerprint::fingerprint_capture_random_two_actions -- --exact
/// ```
///
/// `ORCHARD_LEAN_MULTI_RANDOM_PROOF_OUT` additionally writes the fabricated proof byte string as
/// hex.
#[test]
fn fingerprint_capture_random_two_actions() {
    random::capture_random_fixture(
        0x72,
        2,
        "Zcash.Snark.FixtureRandom2",
        "ORCHARD_LEAN_MULTI_RANDOM_FIXTURE_OUT",
        "ORCHARD_LEAN_MULTI_RANDOM_PROOF_OUT",
    );
}

/// Regenerate the deterministic three-action random match-only fixture (see `random`) with:
///
/// ```text
/// ORCHARD_LEAN_TRIPLE_RANDOM_FIXTURE_OUT=/path/to/ironwood/Zcash/Snark/Fixtures/TripleActionRandom/Fixture.lean \
///   cargo test --features verifier-fingerprint \
///     circuit::fingerprint::fingerprint_capture_random_three_actions -- --exact
/// ```
///
/// `ORCHARD_LEAN_TRIPLE_RANDOM_PROOF_OUT` additionally writes the fabricated proof byte string as
/// hex. Whether this shape is ingested by Ironwood is a build-cost decision made there; the
/// capture exists so deployed-side action-count uniformity can be tested without new Rust work.
#[test]
fn fingerprint_capture_random_three_actions() {
    random::capture_random_fixture(
        0x33,
        3,
        "Zcash.Snark.FixtureRandom3",
        "ORCHARD_LEAN_TRIPLE_RANDOM_FIXTURE_OUT",
        "ORCHARD_LEAN_TRIPLE_RANDOM_PROOF_OUT",
    );
}