use std::{iter, vec::Vec};
use ff::{Field, PrimeField, PrimeFieldBits};
use group::{Curve, GroupEncoding};
use halo2_proofs::circuit::Value;
use orchard::{
constants::{
fixed_bases::{COMMIT_IVK_PERSONALIZATION, NOTE_COMMITMENT_PERSONALIZATION},
L_ORCHARD_BASE, L_VALUE,
},
keys::{FullViewingKey, Scope, SpendValidatingKey},
note::{commitment::ExtractedNoteCommitment, nullifier::Nullifier, Note, RandomSeed, Rho},
spec::NonIdentityPallasPoint,
tree::MerklePath,
value::NoteValue,
};
use pasta_curves::{
arithmetic::{CurveAffine, CurveExt},
pallas,
};
use rand::{CryptoRng, RngCore};
use super::{
circuit::{self, rho_binding_hash, van_commitment_hash, NoteSlotWitness},
imt::{derive_nullifier_domain, gov_null_hash, ImtProofData, ImtProvider},
};
use crate::{
gadgets::elgamal::base_to_scalar, params::BALLOT_DIVISOR, protocol_hash::poseidon_hash_2,
};
const PADDING_PERSONALIZATION: &str = "shielded-vote/padding-v1";
#[derive(Clone, Debug)]
pub struct PaddedNoteData {
pub rho: [u8; 32],
pub rseed: [u8; 32],
}
#[derive(Clone, Debug)]
pub struct PrecomputedRandomness {
pub padded_notes: Vec<PaddedNoteData>,
pub rseed_signed: [u8; 32],
pub rseed_output: [u8; 32],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(clippy::enum_variant_names)]
pub enum PrecomputedRandomnessLocation {
PaddedNote(usize),
SignedNote,
OutputNote,
}
#[derive(Debug)]
pub struct RealNoteInput {
pub note: Note,
pub fvk: FullViewingKey,
pub merkle_path: MerklePath,
pub imt_proof: ImtProofData,
pub scope: Scope,
}
#[derive(Debug)]
pub struct DelegationBundle {
pub circuit: circuit::Circuit,
pub instance: circuit::Instance,
}
fn point_x(point: &pallas::Point) -> pallas::Base {
point_x_opt(point).expect("ExtractP requires a non-identity Pallas point")
}
fn point_x_opt(point: &pallas::Point) -> Option<pallas::Base> {
point
.to_affine()
.coordinates()
.into_option()
.map(|coords| *coords.x())
}
fn byte_bits(bytes: [u8; 32]) -> impl Iterator<Item = bool> {
bytes
.into_iter()
.flat_map(|byte| (0..8).map(move |bit| ((byte >> bit) & 1) == 1))
}
fn u64_bits(value: u64) -> impl Iterator<Item = bool> {
value
.to_le_bytes()
.into_iter()
.flat_map(|byte| (0..8).map(move |bit| ((byte >> bit) & 1) == 1))
}
fn external_ivk_scalar(fvk: &FullViewingKey, ak: &SpendValidatingKey) -> pallas::Scalar {
let ak_point: pallas::Point = ak.into();
let ak_x = point_x(&ak_point);
let rivk = fvk.rivk(Scope::External).inner();
let domain = sinsemilla::CommitDomain::new(COMMIT_IVK_PERSONALIZATION);
let ivk = domain
.short_commit(
iter::empty()
.chain(ak_x.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
.chain(
fvk.nk()
.inner()
.to_le_bits()
.iter()
.by_vals()
.take(L_ORCHARD_BASE),
),
&rivk,
)
.expect("external ivk must not be bottom");
base_to_scalar(ivk).expect("external ivk must fit in the scalar field")
}
fn non_identity_padding_point(
point: pallas::Point,
slot_index: usize,
component: &'static str,
) -> Result<NonIdentityPallasPoint, DelegationBuildError> {
NonIdentityPallasPoint::from_bytes(&point.to_bytes())
.into_option()
.ok_or(DelegationBuildError::InvalidPaddingPoint {
slot_index,
component,
})
}
fn validate_padding_slot_index(slot_index: usize) -> Result<(), DelegationBuildError> {
if (1..circuit::MAX_REAL_NOTES).contains(&slot_index) {
Ok(())
} else {
Err(DelegationBuildError::InvalidPaddingSlotIndex { slot_index })
}
}
fn padding_points(
slot_index: usize,
ivk: pallas::Scalar,
) -> Result<(NonIdentityPallasPoint, NonIdentityPallasPoint), DelegationBuildError> {
validate_padding_slot_index(slot_index)?;
let slot_index_u32 =
u32::try_from(slot_index).expect("validated padding slot index fits in u32");
let g_d_pad =
pallas::Point::hash_to_curve(PADDING_PERSONALIZATION)(&slot_index_u32.to_le_bytes());
let pk_d_pad = g_d_pad * ivk;
Ok((
non_identity_padding_point(g_d_pad, slot_index, "g_d")?,
non_identity_padding_point(pk_d_pad, slot_index, "pk_d")?,
))
}
fn random_seed_for_rho(rho: &Rho, rng: &mut impl RngCore) -> RandomSeed {
loop {
let mut rseed = [0u8; 32];
rng.fill_bytes(&mut rseed);
let rseed = RandomSeed::from_bytes(rseed, rho);
if bool::from(rseed.is_some()) {
return rseed.unwrap();
}
}
}
fn note_commitment_point(
g_d: pallas::Point,
pk_d: pallas::Point,
value: NoteValue,
rho: pallas::Base,
psi: pallas::Base,
rcm: pallas::Scalar,
) -> Option<pallas::Point> {
let domain = sinsemilla::CommitDomain::new(NOTE_COMMITMENT_PERSONALIZATION);
domain
.commit(
iter::empty()
.chain(byte_bits(g_d.to_bytes()))
.chain(byte_bits(pk_d.to_bytes()))
.chain(u64_bits(value.inner()).take(L_VALUE))
.chain(rho.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
.chain(psi.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE)),
&rcm,
)
.into_option()
}
fn derive_note_nullifier(
nk: pallas::Base,
rho: pallas::Base,
psi: pallas::Base,
cm: pallas::Affine,
) -> Option<pallas::Base> {
let k = pallas::Point::hash_to_curve("z.cash:Orchard")(b"K");
let prf_nf = poseidon_hash_2(nk, rho);
let scalar = pallas::Scalar::from_repr((prf_nf + psi).to_repr())
.expect("Pallas base field is smaller than its scalar field");
point_x_opt(&(k * scalar + cm))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SyntheticPaddingNoteParts {
pub cmx: [u8; 32],
pub nullifier: [u8; 32],
}
struct SyntheticPaddingDerivation {
g_d_pad: NonIdentityPallasPoint,
pk_d_pad: NonIdentityPallasPoint,
psi: pallas::Base,
rcm: orchard::note::NoteCommitTrapdoor,
cm: pallas::Affine,
cmx: pallas::Base,
real_nf: pallas::Base,
}
fn derive_synthetic_padding_note(
nk: pallas::Base,
ivk: pallas::Scalar,
slot_index: usize,
rho: Rho,
rseed: RandomSeed,
location: PrecomputedRandomnessLocation,
) -> Result<SyntheticPaddingDerivation, DelegationBuildError> {
let (g_d_pad, pk_d_pad) = padding_points(slot_index, ivk)?;
let psi = rseed.psi(&rho);
let rcm = rseed.rcm(&rho);
let cm = note_commitment_point(
*g_d_pad,
*pk_d_pad,
NoteValue::ZERO,
rho.into_inner(),
psi,
rcm.inner(),
)
.ok_or(DelegationBuildError::InvalidPaddingNoteCommitment { location })?
.to_affine();
let cmx = *cm
.coordinates()
.into_option()
.ok_or(DelegationBuildError::InvalidPaddingNoteCommitment { location })?
.x();
let real_nf = derive_note_nullifier(nk, rho.into_inner(), psi, cm)
.ok_or(DelegationBuildError::InvalidPaddingNullifier { location })?;
Ok(SyntheticPaddingDerivation {
g_d_pad,
pk_d_pad,
psi,
rcm,
cm,
cmx,
real_nf,
})
}
pub fn synthetic_padding_note_parts(
fvk: &FullViewingKey,
slot_index: usize,
rho: Rho,
rseed: RandomSeed,
) -> Result<SyntheticPaddingNoteParts, DelegationBuildError> {
let ak: SpendValidatingKey = fvk.clone().into();
let ivk = external_ivk_scalar(fvk, &ak);
let location = PrecomputedRandomnessLocation::PaddedNote(slot_index);
let padding =
derive_synthetic_padding_note(fvk.nk().inner(), ivk, slot_index, rho, rseed, location)?;
Ok(SyntheticPaddingNoteParts {
cmx: padding.cmx.to_repr(),
nullifier: padding.real_nf.to_repr(),
})
}
struct PaddingSlot {
witness: NoteSlotWitness,
cmx: pallas::Base,
v_raw: u64,
gov_null: pallas::Base,
#[cfg(test)]
real_nf: pallas::Base,
}
fn build_padding_slot(
slot_index: usize,
pad_idx: usize,
nk: pallas::Base,
dom: pallas::Base,
ivk: pallas::Scalar,
imt_provider: &impl ImtProvider,
rng: &mut impl RngCore,
precomputed: Option<&PrecomputedRandomness>,
) -> Result<PaddingSlot, DelegationBuildError> {
let location = PrecomputedRandomnessLocation::PaddedNote(pad_idx);
let (rho, rseed) = if let Some(pre) = precomputed {
if pad_idx >= pre.padded_notes.len() {
return Err(DelegationBuildError::MissingPrecomputedPaddedNote {
index: pad_idx,
actual: pre.padded_notes.len(),
});
}
let pd = &pre.padded_notes[pad_idx];
let rho = Rho::from_bytes(&pd.rho)
.into_option()
.ok_or(DelegationBuildError::InvalidPrecomputedRho { index: pad_idx })?;
let rseed = RandomSeed::from_bytes(pd.rseed, &rho)
.into_option()
.ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
(rho, rseed)
} else {
let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
let rseed = random_seed_for_rho(&rho, &mut *rng);
(rho, rseed)
};
let padding = derive_synthetic_padding_note(nk, ivk, slot_index, rho, rseed, location)?;
let gov_null = gov_null_hash(nk, dom, padding.real_nf);
let imt_proof = imt_provider.non_membership_proof(padding.real_nf)?;
let merkle_path = MerklePath::dummy(&mut *rng);
let witness = NoteSlotWitness {
g_d: Value::known(padding.g_d_pad),
pk_d: Value::known(padding.pk_d_pad),
v: Value::known(NoteValue::ZERO),
rho: Value::known(rho.into_inner()),
psi: Value::known(padding.psi),
rcm: Value::known(padding.rcm),
cm: Value::known(padding.cm),
path: Value::known(merkle_path.auth_path()),
pos: Value::known(merkle_path.position()),
imt_nf_bounds: Value::known(imt_proof.nf_bounds),
imt_leaf_pos: Value::known(imt_proof.leaf_pos),
imt_path: Value::known(imt_proof.path),
is_internal: Value::known(false),
};
Ok(PaddingSlot {
witness,
cmx: padding.cmx,
v_raw: 0,
gov_null,
#[cfg(test)]
real_nf: padding.real_nf,
})
}
#[cfg(test)]
pub(super) struct PaddingSlotForTesting {
pub witness: NoteSlotWitness,
pub cmx: pallas::Base,
pub gov_null: pallas::Base,
pub real_nf: pallas::Base,
}
#[cfg(test)]
pub(super) fn build_padding_slot_for_testing(
slot_index: usize,
pad_idx: usize,
fvk: &FullViewingKey,
ak: &SpendValidatingKey,
dom: pallas::Base,
imt_provider: &impl ImtProvider,
rng: &mut impl RngCore,
) -> Result<PaddingSlotForTesting, DelegationBuildError> {
let padding = build_padding_slot(
slot_index,
pad_idx,
fvk.nk().inner(),
dom,
external_ivk_scalar(fvk, ak),
imt_provider,
rng,
None,
)?;
Ok(PaddingSlotForTesting {
witness: padding.witness,
cmx: padding.cmx,
gov_null: padding.gov_null,
real_nf: padding.real_nf,
})
}
#[derive(Clone, Debug)]
pub enum DelegationBuildError {
InvalidNoteCount(usize),
Instance(circuit::InstanceError),
InvalidPaddingSlotIndex { slot_index: usize },
InvalidPaddingPoint {
slot_index: usize,
component: &'static str,
},
InvalidPaddingNoteCommitment {
location: PrecomputedRandomnessLocation,
},
InvalidPaddingNullifier {
location: PrecomputedRandomnessLocation,
},
MissingPrecomputedPaddedNote { index: usize, actual: usize },
InvalidPrecomputedRho { index: usize },
InvalidPrecomputedRseed {
location: PrecomputedRandomnessLocation,
},
InvalidPrecomputedNote {
location: PrecomputedRandomnessLocation,
},
ImtFetchFailed(super::imt::ImtError),
}
impl From<circuit::InstanceError> for DelegationBuildError {
fn from(e: circuit::InstanceError) -> Self {
DelegationBuildError::Instance(e)
}
}
impl From<super::imt::ImtError> for DelegationBuildError {
fn from(e: super::imt::ImtError) -> Self {
DelegationBuildError::ImtFetchFailed(e)
}
}
impl std::fmt::Display for DelegationBuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DelegationBuildError::InvalidNoteCount(n) => {
write!(
f,
"invalid note count: {} (expected 1–{})",
n,
circuit::MAX_REAL_NOTES
)
}
DelegationBuildError::Instance(e) => {
write!(f, "instance construction failed: {e}")
}
DelegationBuildError::InvalidPaddingSlotIndex { slot_index } => {
write!(
f,
"invalid padding slot index {slot_index} (expected 1..={})",
circuit::MAX_REAL_NOTES - 1
)
}
DelegationBuildError::InvalidPaddingPoint {
slot_index,
component,
} => {
write!(
f,
"invalid padding point {component} at slot {slot_index}: identity point"
)
}
DelegationBuildError::InvalidPaddingNoteCommitment { location } => {
write!(f, "invalid padding note commitment for {location}")
}
DelegationBuildError::InvalidPaddingNullifier { location } => {
write!(f, "invalid padding nullifier for {location}")
}
DelegationBuildError::MissingPrecomputedPaddedNote { index, actual } => {
write!(
f,
"missing precomputed padded note at index {index} (got {actual} entries)"
)
}
DelegationBuildError::InvalidPrecomputedRho { index } => {
write!(f, "invalid precomputed padded note rho at index {index}")
}
DelegationBuildError::InvalidPrecomputedRseed { location } => {
write!(f, "invalid precomputed rseed for {location}")
}
DelegationBuildError::InvalidPrecomputedNote { location } => {
write!(f, "invalid precomputed note components for {location}")
}
DelegationBuildError::ImtFetchFailed(e) => {
write!(f, "IMT proof fetch failed: {e}")
}
}
}
}
impl std::fmt::Display for PrecomputedRandomnessLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PrecomputedRandomnessLocation::PaddedNote(index) => {
write!(f, "padded note {index}")
}
PrecomputedRandomnessLocation::SignedNote => write!(f, "signed note"),
PrecomputedRandomnessLocation::OutputNote => write!(f, "output note"),
}
}
}
pub fn build_delegation_bundle(
real_notes: Vec<RealNoteInput>,
fvk: &FullViewingKey,
alpha: pallas::Scalar,
output_recipient: orchard::Address,
vote_round_id: pallas::Base,
nc_root: pallas::Base,
van_comm_rand: pallas::Base,
imt_provider: &impl ImtProvider,
rng: &mut (impl RngCore + CryptoRng),
precomputed: Option<&PrecomputedRandomness>,
) -> Result<DelegationBundle, DelegationBuildError> {
let n_real = real_notes.len();
if n_real == 0 || n_real > circuit::MAX_REAL_NOTES {
return Err(DelegationBuildError::InvalidNoteCount(n_real));
}
let nf_imt_root = imt_provider.root();
let nk_val = fvk.nk().inner();
let ak: SpendValidatingKey = fvk.clone().into();
let ivk = external_ivk_scalar(fvk, &ak);
let dom = derive_nullifier_domain(vote_round_id);
let mut note_slots = Vec::with_capacity(circuit::MAX_REAL_NOTES);
let mut cmx_values = Vec::with_capacity(circuit::MAX_REAL_NOTES);
let mut v_values = Vec::with_capacity(circuit::MAX_REAL_NOTES);
let mut gov_nulls = Vec::with_capacity(circuit::MAX_REAL_NOTES);
for input in &real_notes {
let note = &input.note;
let rho = note.rho();
let psi = note.rseed().psi(&rho);
let rcm = note.rseed().rcm(&rho);
let cm = note.commitment();
let cmx = ExtractedNoteCommitment::from(cm.clone()).inner();
let v_raw = note.value().inner();
let recipient = note.recipient();
let real_nf = note.nullifier(fvk);
let gov_null = gov_null_hash(nk_val, dom, real_nf.inner());
let slot = NoteSlotWitness {
g_d: Value::known(recipient.g_d()),
pk_d: Value::known(recipient.pk_d().inner()),
v: Value::known(note.value()),
rho: Value::known(rho.into_inner()),
psi: Value::known(psi),
rcm: Value::known(rcm),
cm: Value::known(cm.inner().to_affine()),
path: Value::known(input.merkle_path.auth_path()),
pos: Value::known(input.merkle_path.position()),
imt_nf_bounds: Value::known(input.imt_proof.nf_bounds),
imt_leaf_pos: Value::known(input.imt_proof.leaf_pos),
imt_path: Value::known(input.imt_proof.path),
is_internal: Value::known(matches!(input.scope, Scope::Internal)),
};
note_slots.push(slot);
cmx_values.push(cmx);
v_values.push(v_raw);
gov_nulls.push(gov_null);
}
for i in n_real..circuit::MAX_REAL_NOTES {
let pad_idx = i - n_real; let padding =
build_padding_slot(i, pad_idx, nk_val, dom, ivk, imt_provider, rng, precomputed)?;
note_slots.push(padding.witness);
cmx_values.push(padding.cmx);
v_values.push(padding.v_raw);
gov_nulls.push(padding.gov_null);
}
let notes: [NoteSlotWitness; circuit::MAX_REAL_NOTES] =
note_slots.try_into().unwrap_or_else(|_| unreachable!());
let v_total_u64: u64 = v_values.iter().sum();
let num_ballots_u64 = v_total_u64 / BALLOT_DIVISOR;
let remainder_u64 = v_total_u64 % BALLOT_DIVISOR;
let num_ballots_field = pallas::Base::from(num_ballots_u64);
let g_d_new_x = *output_recipient
.g_d()
.to_affine()
.coordinates()
.unwrap()
.x();
let pk_d_new_x = *output_recipient
.pk_d()
.inner()
.to_affine()
.coordinates()
.unwrap()
.x();
let van_comm = van_commitment_hash(
g_d_new_x,
pk_d_new_x,
num_ballots_field,
vote_round_id,
van_comm_rand,
);
let rho = rho_binding_hash(
cmx_values[0],
cmx_values[1],
cmx_values[2],
cmx_values[3],
cmx_values[4],
van_comm,
vote_round_id,
);
let sender_address = fvk.address_at(0u32, Scope::External);
let signed_rho = Rho::from_nf_old(Nullifier::from_inner(rho));
let signed_note = if let Some(pre) = precomputed {
let location = PrecomputedRandomnessLocation::SignedNote;
let rseed = RandomSeed::from_bytes(pre.rseed_signed, &signed_rho)
.into_option()
.ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
Note::from_parts(sender_address, NoteValue::from_raw(1), signed_rho, rseed)
.into_option()
.ok_or(DelegationBuildError::InvalidPrecomputedNote { location })?
} else {
Note::new(
sender_address,
NoteValue::from_raw(1),
signed_rho,
&mut *rng,
)
};
let nf_signed = signed_note.nullifier(fvk);
let output_rho = Rho::from_nf_old(nf_signed);
let output_note = if let Some(pre) = precomputed {
let location = PrecomputedRandomnessLocation::OutputNote;
let rseed = RandomSeed::from_bytes(pre.rseed_output, &output_rho)
.into_option()
.ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
Note::from_parts(output_recipient, NoteValue::ZERO, output_rho, rseed)
.into_option()
.ok_or(DelegationBuildError::InvalidPrecomputedNote { location })?
} else {
Note::new(output_recipient, NoteValue::ZERO, output_rho, &mut *rng)
};
let cmx_new = ExtractedNoteCommitment::from(output_note.commitment()).inner();
let rk = ak.randomize(&alpha);
let circuit = circuit::Circuit::from_note_unchecked(fvk, &signed_note, alpha)
.with_output_note(&output_note)
.with_notes(notes)
.with_van_comm_rand(van_comm_rand)
.with_ballot_scaling(
pallas::Base::from(num_ballots_u64),
pallas::Base::from(remainder_u64),
);
let instance = circuit::Instance::from_parts(
nf_signed,
rk,
cmx_new,
van_comm,
vote_round_id,
nc_root,
nf_imt_root,
[
gov_nulls[0],
gov_nulls[1],
gov_nulls[2],
gov_nulls[3],
gov_nulls[4],
],
dom,
)?;
Ok(DelegationBundle { circuit, instance })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::delegation::imt::{ImtError, SpacedLeafImtProvider};
use ff::Field;
use halo2_proofs::dev::MockProver;
use incrementalmerkletree::{Hashable, Level};
use orchard::{
constants::MERKLE_DEPTH_ORCHARD,
keys::{FullViewingKey, Scope, SpendingKey},
note::{commitment::ExtractedNoteCommitment, Note, Rho},
tree::{MerkleHashOrchard, MerklePath},
value::NoteValue,
};
use pasta_curves::pallas;
use rand::rngs::OsRng;
use std::cell::{Cell, RefCell};
const K: u32 = 14;
#[derive(Debug)]
struct RecordingImtProvider {
proof: ImtProofData,
error: Option<ImtError>,
requested_nfs: RefCell<Vec<pallas::Base>>,
}
impl RecordingImtProvider {
fn returning(proof: ImtProofData) -> Self {
Self {
proof,
error: None,
requested_nfs: RefCell::new(Vec::new()),
}
}
fn failing(error: ImtError) -> Self {
Self {
proof: test_imt_proof(),
error: Some(error),
requested_nfs: RefCell::new(Vec::new()),
}
}
}
impl ImtProvider for RecordingImtProvider {
fn root(&self) -> pallas::Base {
self.proof.root
}
fn non_membership_proof(&self, nf: pallas::Base) -> Result<ImtProofData, ImtError> {
self.requested_nfs.borrow_mut().push(nf);
match &self.error {
Some(error) => Err(error.clone()),
None => Ok(self.proof.clone()),
}
}
}
fn test_imt_proof() -> ImtProofData {
ImtProofData {
root: pallas::Base::from(900u64),
nf_bounds: [
pallas::Base::from(10u64),
pallas::Base::from(20u64),
pallas::Base::from(30u64),
],
leaf_pos: 7,
path: std::array::from_fn(|i| pallas::Base::from(1_000u64 + i as u64)),
}
}
fn precomputed_padding_note(rng: &mut impl RngCore) -> (PaddedNoteData, Rho, RandomSeed) {
let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
let rseed = random_seed_for_rho(&rho, &mut *rng);
(
PaddedNoteData {
rho: rho.to_bytes(),
rseed: *rseed.as_bytes(),
},
rho,
rseed,
)
}
fn assert_known<T>(value: &Value<T>, f: impl FnOnce(&T) -> bool) {
let checked = Cell::new(false);
value.assert_if_known(|actual| {
checked.set(true);
f(actual)
});
assert!(checked.get(), "expected known witness value");
}
fn assert_padding_slot_matches(
padding: &PaddingSlot,
slot_index: usize,
nk: pallas::Base,
dom: pallas::Base,
ivk: pallas::Scalar,
rho: Rho,
rseed: RandomSeed,
imt_proof: &ImtProofData,
requested_nfs: &[pallas::Base],
) {
let (g_d_pad, pk_d_pad) =
padding_points(slot_index, ivk).expect("test padding points should be valid");
let psi = rseed.psi(&rho);
let rcm = rseed.rcm(&rho);
let cm = note_commitment_point(
*g_d_pad,
*pk_d_pad,
NoteValue::ZERO,
rho.into_inner(),
psi,
rcm.inner(),
)
.expect("test padding commitment should be valid")
.to_affine();
let real_nf = derive_note_nullifier(nk, rho.into_inner(), psi, cm)
.expect("test padding nullifier should be valid");
assert_eq!(padding.cmx, *cm.coordinates().unwrap().x());
assert_eq!(padding.v_raw, 0);
assert_eq!(padding.gov_null, gov_null_hash(nk, dom, real_nf));
assert_eq!(requested_nfs, &[real_nf]);
assert_known(&padding.witness.g_d, |actual| *actual == g_d_pad);
assert_known(&padding.witness.pk_d, |actual| *actual == pk_d_pad);
assert_known(&padding.witness.v, |actual| *actual == NoteValue::ZERO);
assert_known(&padding.witness.rho, |actual| *actual == rho.into_inner());
assert_known(&padding.witness.psi, |actual| *actual == psi);
assert_known(&padding.witness.rcm, |actual| actual.inner() == rcm.inner());
assert_known(&padding.witness.cm, |actual| *actual == cm);
assert_known(&padding.witness.imt_nf_bounds, |actual| {
*actual == imt_proof.nf_bounds
});
assert_known(&padding.witness.imt_leaf_pos, |actual| {
*actual == imt_proof.leaf_pos
});
assert_known(&padding.witness.imt_path, |actual| {
*actual == imt_proof.path
});
assert_known(&padding.witness.is_internal, |actual| !*actual);
}
fn make_real_note_inputs(
fvk: &FullViewingKey,
values: &[u64],
scopes: &[Scope],
imt_provider: &impl ImtProvider,
rng: &mut impl RngCore,
) -> (Vec<RealNoteInput>, pallas::Base) {
let n = values.len();
assert!((1..=circuit::MAX_REAL_NOTES).contains(&n));
assert_eq!(n, scopes.len());
let mut notes = Vec::with_capacity(n);
for (idx, &v) in values.iter().enumerate() {
let recipient = fvk.address_at(0u32, scopes[idx]);
let note_value = NoteValue::from_raw(v);
let (_, _, dummy_parent) = Note::dummy(&mut *rng, None);
let note = Note::new(
recipient,
note_value,
Rho::from_nf_old(dummy_parent.nullifier(fvk)),
&mut *rng,
);
notes.push(note);
}
let empty_leaf = MerkleHashOrchard::empty_leaf();
let mut leaves = [empty_leaf; 8];
for (i, note) in notes.iter().enumerate() {
let cmx = ExtractedNoteCommitment::from(note.commitment());
leaves[i] = MerkleHashOrchard::from_cmx(&cmx);
}
let l1_0 = MerkleHashOrchard::combine(Level::from(0), &leaves[0], &leaves[1]);
let l1_1 = MerkleHashOrchard::combine(Level::from(0), &leaves[2], &leaves[3]);
let l1_2 = MerkleHashOrchard::combine(Level::from(0), &leaves[4], &leaves[5]);
let l1_3 = MerkleHashOrchard::combine(Level::from(0), &leaves[6], &leaves[7]);
let l2_0 = MerkleHashOrchard::combine(Level::from(1), &l1_0, &l1_1);
let l2_1 = MerkleHashOrchard::combine(Level::from(1), &l1_2, &l1_3);
let l3_0 = MerkleHashOrchard::combine(Level::from(2), &l2_0, &l2_1);
let mut current = l3_0;
for level in 3..MERKLE_DEPTH_ORCHARD {
let sibling = MerkleHashOrchard::empty_root(Level::from(level as u8));
current = MerkleHashOrchard::combine(Level::from(level as u8), ¤t, &sibling);
}
let nc_root = current.inner();
let l1 = [l1_0, l1_1, l1_2, l1_3];
let l2 = [l2_0, l2_1];
let mut inputs = Vec::with_capacity(n);
for (i, note) in notes.into_iter().enumerate() {
let mut auth_path = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
auth_path[0] = leaves[i ^ 1];
auth_path[1] = l1[(i >> 1) ^ 1];
auth_path[2] = l2[1 - (i >> 2)];
for level in 3..MERKLE_DEPTH_ORCHARD {
auth_path[level] = MerkleHashOrchard::empty_root(Level::from(level as u8));
}
let merkle_path = MerklePath::from_parts(i as u32, auth_path);
let real_nf = note.nullifier(fvk);
let imt_proof = imt_provider.non_membership_proof(real_nf.inner()).unwrap();
inputs.push(RealNoteInput {
note,
fvk: fvk.clone(),
merkle_path,
imt_proof,
scope: scopes[i],
});
}
(inputs, nc_root)
}
fn build_bundle(values: &[u64], scopes: &[Scope]) -> DelegationBundle {
assert_eq!(values.len(), scopes.len());
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let output_recipient = fvk.address_at(1u32, Scope::External);
let vote_round_id = pallas::Base::random(&mut rng);
let van_comm_rand = pallas::Base::random(&mut rng);
let alpha = pallas::Scalar::random(&mut rng);
let imt = SpacedLeafImtProvider::new();
let (inputs, nc_root) = make_real_note_inputs(&fvk, values, scopes, &imt, &mut rng);
let bundle = build_delegation_bundle(
inputs,
&fvk,
alpha,
output_recipient,
vote_round_id,
nc_root,
van_comm_rand,
&imt,
&mut rng,
None,
)
.unwrap();
assert_delegation_output_shape(&bundle);
bundle
}
fn build_single_note_bundle_with_precomputed(
precomputed: &PrecomputedRandomness,
) -> Result<DelegationBundle, DelegationBuildError> {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
build_single_note_bundle_with_fvk_and_precomputed(&fvk, precomputed, &mut rng)
}
fn build_single_note_bundle_with_fvk_and_precomputed(
fvk: &FullViewingKey,
precomputed: &PrecomputedRandomness,
rng: &mut (impl RngCore + CryptoRng),
) -> Result<DelegationBundle, DelegationBuildError> {
let output_recipient = fvk.address_at(1u32, Scope::External);
let vote_round_id = pallas::Base::random(&mut *rng);
let van_comm_rand = pallas::Base::random(&mut *rng);
let alpha = pallas::Scalar::random(&mut *rng);
let imt = SpacedLeafImtProvider::new();
let (inputs, nc_root) =
make_real_note_inputs(fvk, &[13_000_000], &[Scope::External], &imt, &mut *rng);
build_delegation_bundle(
inputs,
fvk,
alpha,
output_recipient,
vote_round_id,
nc_root,
van_comm_rand,
&imt,
rng,
Some(precomputed),
)
}
fn make_valid_padded_note_data(rng: &mut impl RngCore) -> PaddedNoteData {
let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
let rseed = random_seed_for_rho(&rho, rng);
PaddedNoteData {
rho: rho.to_bytes(),
rseed: *rseed.as_bytes(),
}
}
fn assert_delegation_output_shape(bundle: &DelegationBundle) {
let pi = bundle.instance.to_halo2_instance();
assert_eq!(pi.len(), 14, "delegation public input shape changed");
assert_eq!(bundle.instance.gov_null.len(), 5);
assert_eq!(pi[0], bundle.instance.nf_signed.inner());
assert_eq!(pi[3], bundle.instance.cmx_new);
assert_eq!(pi[4], bundle.instance.van_comm);
assert_eq!(pi[5], bundle.instance.vote_round_id);
assert_eq!(pi[6], bundle.instance.nc_root);
assert_eq!(pi[7], bundle.instance.nf_imt_root);
assert_eq!(&pi[8..13], &bundle.instance.gov_null);
assert_eq!(pi[13], bundle.instance.dom);
}
fn verify_bundle(bundle: &DelegationBundle) {
let pi = bundle.instance.to_halo2_instance();
let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
assert_eq!(prover.verify(), Ok(()), "merged circuit failed");
}
#[test]
#[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
fn test_single_real_note() {
let bundle = build_bundle(&[13_000_000], &[Scope::External]);
verify_bundle(&bundle);
}
fn build_bundle_for_inspection(
values: &[u64],
scopes: &[Scope],
) -> (DelegationBundle, FullViewingKey, SpendValidatingKey) {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let ak: SpendValidatingKey = fvk.clone().into();
let output_recipient = fvk.address_at(1u32, Scope::External);
let vote_round_id = pallas::Base::random(&mut rng);
let van_comm_rand = pallas::Base::random(&mut rng);
let alpha = pallas::Scalar::random(&mut rng);
let imt = SpacedLeafImtProvider::new();
let (inputs, nc_root) = make_real_note_inputs(&fvk, values, scopes, &imt, &mut rng);
let bundle = build_delegation_bundle(
inputs,
&fvk,
alpha,
output_recipient,
vote_round_id,
nc_root,
van_comm_rand,
&imt,
&mut rng,
None,
)
.unwrap();
(bundle, fvk, ak)
}
#[test]
fn test_single_real_note_locks_padding_witnesses() {
let (bundle, fvk, ak) = build_bundle_for_inspection(&[13_000_000], &[Scope::External]);
let ivk = external_ivk_scalar(&fvk, &ak);
let notes = bundle.circuit.notes_for_testing();
for slot_index in 1..5 {
let (expected_g_d, expected_pk_d) =
padding_points(slot_index, ivk).expect("test padding points should be valid");
assert_known(¬es[slot_index].g_d, |actual| *actual == expected_g_d);
assert_known(¬es[slot_index].pk_d, |actual| *actual == expected_pk_d);
}
}
#[test]
fn test_five_real_notes_uses_no_padding() {
let (bundle, fvk, ak) = build_bundle_for_inspection(&[2_500_000; 5], &[Scope::External; 5]);
let ivk = external_ivk_scalar(&fvk, &ak);
let padding_g_ds: Vec<_> = (1..circuit::MAX_REAL_NOTES)
.map(|i| {
padding_points(i, ivk)
.expect("test padding points should be valid")
.0
})
.collect();
let notes = bundle.circuit.notes_for_testing();
for slot_index in 0..5 {
for pad_g_d in &padding_g_ds {
assert_known(¬es[slot_index].g_d, |actual| actual != pad_g_d);
}
}
}
#[test]
fn test_padding_points_are_synthetic_and_ivk_bound() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let ak: SpendValidatingKey = fvk.clone().into();
let ivk = external_ivk_scalar(&fvk, &ak);
for slot_index in 1..5 {
let (g_d_pad, pk_d_pad) =
padding_points(slot_index, ivk).expect("test padding points should be valid");
let real_orchard_addr = fvk.address_at(slot_index as u32, Scope::External);
assert_eq!(*pk_d_pad, *g_d_pad * ivk);
assert_ne!(*g_d_pad, *real_orchard_addr.g_d());
assert_ne!(*pk_d_pad, *real_orchard_addr.pk_d().inner());
}
}
#[test]
fn test_padding_points_reject_impossible_slot_indices() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let ak: SpendValidatingKey = fvk.clone().into();
let ivk = external_ivk_scalar(&fvk, &ak);
for slot_index in [0, circuit::MAX_REAL_NOTES, usize::MAX] {
assert!(matches!(
padding_points(slot_index, ivk),
Err(DelegationBuildError::InvalidPaddingSlotIndex { slot_index: actual })
if actual == slot_index
));
}
}
#[test]
fn test_padding_personalization_is_domain_separated_from_orchard() {
use orchard::constants::KEY_DIVERSIFICATION_PERSONALIZATION;
assert_ne!(
PADDING_PERSONALIZATION, KEY_DIVERSIFICATION_PERSONALIZATION,
"padding personalization must be domain-separated from Orchard's \
DiversifyHash personalization; otherwise synthetic padding `g_d_pad` \
can collide with real diversified bases and ZCA-450's fix regresses"
);
}
fn fixture_real_note(
scope: Scope,
rng: &mut impl RngCore,
) -> (FullViewingKey, SpendValidatingKey, Note) {
let sk = SpendingKey::random(rng);
let fvk: FullViewingKey = (&sk).into();
let ak: SpendValidatingKey = fvk.clone().into();
let recipient = fvk.address_at(0u32, scope);
let (_, _, dummy_parent) = Note::dummy(rng, None);
let note = Note::new(
recipient,
NoteValue::from_raw(12_500_000),
Rho::from_nf_old(dummy_parent.nullifier(&fvk)),
rng,
);
(fvk, ak, note)
}
#[test]
fn test_note_commitment_point_matches_orchard() {
let mut rng = OsRng;
for scope in [Scope::External, Scope::Internal] {
let (_fvk, _ak, note) = fixture_real_note(scope, &mut rng);
let recipient = note.recipient();
let rho = note.rho();
let psi = note.rseed().psi(&rho);
let rcm = note.rseed().rcm(&rho);
let mirrored = note_commitment_point(
*recipient.g_d(),
*recipient.pk_d().inner(),
note.value(),
rho.into_inner(),
psi,
rcm.inner(),
);
let orchard = note.commitment().inner();
assert_eq!(
mirrored,
Some(orchard),
"note_commitment_point drifted from Orchard NoteCommitment::derive ({scope:?})"
);
}
}
#[test]
fn test_derive_note_nullifier_matches_orchard() {
let mut rng = OsRng;
for scope in [Scope::External, Scope::Internal] {
let (fvk, _ak, note) = fixture_real_note(scope, &mut rng);
let nk = fvk.nk().inner();
let rho = note.rho();
let psi = note.rseed().psi(&rho);
let cm = note.commitment().inner().to_affine();
let mirrored = derive_note_nullifier(nk, rho.into_inner(), psi, cm);
let orchard = note.nullifier(&fvk).inner();
assert_eq!(
mirrored,
Some(orchard),
"derive_note_nullifier drifted from Orchard Nullifier::derive ({scope:?})"
);
}
}
#[test]
fn test_external_ivk_scalar_matches_orchard_address_derivation() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let ak: SpendValidatingKey = fvk.clone().into();
let ivk = external_ivk_scalar(&fvk, &ak);
for idx in [0u32, 1, 7, 1234] {
let addr = fvk.address_at(idx, Scope::External);
assert_eq!(
*addr.g_d() * ivk,
*addr.pk_d().inner(),
"external_ivk_scalar drifted: [ivk] * g_d != pk_d at diversifier index {idx}"
);
}
let internal_addr = fvk.address_at(0u32, Scope::Internal);
assert_ne!(
*internal_addr.g_d() * ivk,
*internal_addr.pk_d().inner(),
"external_ivk_scalar incorrectly validates an internal-scope address"
);
}
#[test]
fn test_build_padding_slot_fresh_randomness_populates_strict_witnesses() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let ak: SpendValidatingKey = fvk.clone().into();
let nk = fvk.nk().inner();
let dom = derive_nullifier_domain(pallas::Base::random(&mut rng));
let ivk = external_ivk_scalar(&fvk, &ak);
let imt_proof = test_imt_proof();
let imt = RecordingImtProvider::returning(imt_proof.clone());
let padding = build_padding_slot(3, 0, nk, dom, ivk, &imt, &mut rng, None).unwrap();
let (g_d_pad, pk_d_pad) =
padding_points(3, ivk).expect("test padding points should be valid");
assert_known(&padding.witness.g_d, |actual| *actual == g_d_pad);
assert_known(&padding.witness.pk_d, |actual| *actual == pk_d_pad);
let generated_padding_values = padding
.witness
.rho
.as_ref()
.copied()
.zip(padding.witness.psi.as_ref().copied())
.zip(padding.witness.rcm.as_ref().cloned())
.zip(padding.witness.cm.as_ref().copied());
assert_known(
&generated_padding_values,
|(((rho_inner, psi), rcm), cm_witness)| {
let cm = note_commitment_point(
*g_d_pad,
*pk_d_pad,
NoteValue::ZERO,
*rho_inner,
*psi,
rcm.inner(),
)
.expect("test padding commitment should be valid")
.to_affine();
let real_nf = derive_note_nullifier(nk, *rho_inner, *psi, cm)
.expect("test padding nullifier should be valid");
*cm_witness == cm
&& padding.cmx == *cm.coordinates().unwrap().x()
&& padding.gov_null == gov_null_hash(nk, dom, real_nf)
&& imt.requested_nfs.borrow().as_slice() == [real_nf]
},
);
assert_known(&padding.witness.v, |actual| *actual == NoteValue::ZERO);
assert_known(&padding.witness.is_internal, |actual| !*actual);
assert_known(&padding.witness.imt_nf_bounds, |actual| {
*actual == imt_proof.nf_bounds
});
assert_known(&padding.witness.imt_leaf_pos, |actual| {
*actual == imt_proof.leaf_pos
});
assert_known(&padding.witness.imt_path, |actual| {
*actual == imt_proof.path
});
assert_eq!(padding.v_raw, 0);
assert_eq!(imt.requested_nfs.borrow().len(), 1);
}
#[test]
fn test_build_padding_slot_reuses_selected_precomputed_randomness() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let ak: SpendValidatingKey = fvk.clone().into();
let nk = fvk.nk().inner();
let dom = derive_nullifier_domain(pallas::Base::random(&mut rng));
let ivk = external_ivk_scalar(&fvk, &ak);
let imt_proof = test_imt_proof();
let imt = RecordingImtProvider::returning(imt_proof.clone());
let (unused_pd, _, _) = precomputed_padding_note(&mut rng);
let (selected_pd, selected_rho, selected_rseed) = precomputed_padding_note(&mut rng);
let precomputed = PrecomputedRandomness {
padded_notes: vec![unused_pd, selected_pd],
rseed_signed: [0; 32],
rseed_output: [0; 32],
};
let padding =
build_padding_slot(4, 1, nk, dom, ivk, &imt, &mut rng, Some(&precomputed)).unwrap();
let requested_nfs = imt.requested_nfs.borrow();
assert_padding_slot_matches(
&padding,
4,
nk,
dom,
ivk,
selected_rho,
selected_rseed,
&imt_proof,
&requested_nfs,
);
}
#[test]
fn test_derive_synthetic_padding_note_matches_manual_derivation() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let ak: SpendValidatingKey = fvk.clone().into();
let nk = fvk.nk().inner();
let ivk = external_ivk_scalar(&fvk, &ak);
let slot_index = 3;
let (_padded_note, rho, rseed) = precomputed_padding_note(&mut rng);
let derived = derive_synthetic_padding_note(
nk,
ivk,
slot_index,
rho,
rseed,
PrecomputedRandomnessLocation::PaddedNote(0),
)
.unwrap();
let (expected_g_d, expected_pk_d) =
padding_points(slot_index, ivk).expect("test padding points should be valid");
let expected_psi = rseed.psi(&rho);
let expected_rcm = rseed.rcm(&rho);
let expected_cm = note_commitment_point(
*expected_g_d,
*expected_pk_d,
NoteValue::ZERO,
rho.into_inner(),
expected_psi,
expected_rcm.inner(),
)
.expect("test padding commitment should be valid")
.to_affine();
let expected_nf = derive_note_nullifier(nk, rho.into_inner(), expected_psi, expected_cm)
.expect("test padding nullifier should be valid");
assert_eq!(derived.g_d_pad, expected_g_d);
assert_eq!(derived.pk_d_pad, expected_pk_d);
assert_eq!(derived.psi, expected_psi);
assert_eq!(derived.rcm.inner(), expected_rcm.inner());
assert_eq!(derived.cm, expected_cm);
assert_eq!(derived.cmx, *expected_cm.coordinates().unwrap().x());
assert_eq!(derived.real_nf, expected_nf);
}
#[test]
fn test_synthetic_padding_note_parts_matches_padding_slot_derivation() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let ak: SpendValidatingKey = fvk.clone().into();
let nk = fvk.nk().inner();
let dom = derive_nullifier_domain(pallas::Base::random(&mut rng));
let ivk = external_ivk_scalar(&fvk, &ak);
let imt = RecordingImtProvider::returning(test_imt_proof());
let (padded_note, rho, rseed) = precomputed_padding_note(&mut rng);
let precomputed = PrecomputedRandomness {
padded_notes: vec![padded_note],
rseed_signed: [0; 32],
rseed_output: [0; 32],
};
let padding =
build_padding_slot(3, 0, nk, dom, ivk, &imt, &mut rng, Some(&precomputed)).unwrap();
let parts = crate::delegation::synthetic_padding_note_parts(&fvk, 3, rho, rseed).unwrap();
assert_eq!(
parts,
SyntheticPaddingNoteParts {
cmx: padding.cmx.to_repr(),
nullifier: padding.real_nf.to_repr(),
}
);
assert_eq!(imt.requested_nfs.borrow().as_slice(), &[padding.real_nf]);
}
#[test]
fn test_synthetic_padding_note_parts_rejects_impossible_slot_indices() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let (_padded_note, rho, rseed) = precomputed_padding_note(&mut rng);
for slot_index in [0, circuit::MAX_REAL_NOTES, usize::MAX] {
assert!(matches!(
crate::delegation::synthetic_padding_note_parts(&fvk, slot_index, rho, rseed),
Err(DelegationBuildError::InvalidPaddingSlotIndex { slot_index: actual })
if actual == slot_index
));
}
}
#[test]
fn test_build_padding_slot_propagates_imt_errors() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let ak: SpendValidatingKey = fvk.clone().into();
let imt = RecordingImtProvider::failing(ImtError("fixture failure".to_string()));
let result = build_padding_slot(
2,
0,
fvk.nk().inner(),
derive_nullifier_domain(pallas::Base::random(&mut rng)),
external_ivk_scalar(&fvk, &ak),
&imt,
&mut rng,
None,
);
assert!(matches!(
result,
Err(DelegationBuildError::ImtFetchFailed(ImtError(message)))
if message == "fixture failure"
));
assert_eq!(imt.requested_nfs.borrow().len(), 1);
}
#[test]
fn test_build_padding_slot_rejects_missing_precomputed_padding_entry() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let ak: SpendValidatingKey = fvk.clone().into();
let imt = RecordingImtProvider::returning(test_imt_proof());
let precomputed = PrecomputedRandomness {
padded_notes: vec![],
rseed_signed: [0; 32],
rseed_output: [0; 32],
};
let result = build_padding_slot(
1,
0,
fvk.nk().inner(),
derive_nullifier_domain(pallas::Base::random(&mut rng)),
external_ivk_scalar(&fvk, &ak),
&imt,
&mut rng,
Some(&precomputed),
);
assert!(matches!(
result,
Err(DelegationBuildError::MissingPrecomputedPaddedNote {
index: 0,
actual: 0
})
));
}
#[test]
fn test_four_real_notes_builds_expected_output_shape() {
build_bundle(
&[3_200_000, 3_200_000, 3_200_000, 3_200_000],
&[
Scope::External,
Scope::External,
Scope::External,
Scope::External,
],
);
}
#[test]
fn test_two_real_notes_builds_expected_output_shape() {
build_bundle(&[7_000_000, 7_000_000], &[Scope::External, Scope::External]);
}
#[test]
fn test_min_weight_boundary_builds_expected_output_shape() {
build_bundle(&[12_500_000], &[Scope::External]);
}
#[test]
#[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
fn test_below_one_ballot() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let output_recipient = fvk.address_at(1u32, Scope::External);
let vote_round_id = pallas::Base::random(&mut rng);
let van_comm_rand = pallas::Base::random(&mut rng);
let alpha = pallas::Scalar::random(&mut rng);
let imt = SpacedLeafImtProvider::new();
let (inputs, nc_root) =
make_real_note_inputs(&fvk, &[12_499_999], &[Scope::External], &imt, &mut rng);
let bundle = build_delegation_bundle(
inputs,
&fvk,
alpha,
output_recipient,
vote_round_id,
nc_root,
van_comm_rand,
&imt,
&mut rng,
None,
)
.unwrap();
let pi = bundle.instance.to_halo2_instance();
let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
assert!(prover.verify().is_err(), "below one ballot should fail");
}
#[test]
fn test_three_ballots_builds_expected_output_shape() {
build_bundle(
&[12_500_000, 12_500_000, 12_500_000],
&[Scope::External, Scope::External, Scope::External],
);
}
#[test]
fn test_zero_notes_error() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let output_recipient = fvk.address_at(1u32, Scope::External);
let imt = SpacedLeafImtProvider::new();
let result = build_delegation_bundle(
vec![],
&fvk,
pallas::Scalar::random(&mut rng),
output_recipient,
pallas::Base::random(&mut rng),
pallas::Base::random(&mut rng),
pallas::Base::random(&mut rng),
&imt,
&mut rng,
None,
);
assert!(matches!(
result,
Err(DelegationBuildError::InvalidNoteCount(0))
));
}
#[test]
fn test_five_real_notes_builds_expected_output_shape() {
build_bundle(
&[2_500_000, 2_500_000, 2_500_000, 2_500_000, 2_500_000],
&[
Scope::External,
Scope::External,
Scope::External,
Scope::External,
Scope::External,
],
);
}
#[test]
fn test_six_notes_error() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let output_recipient = fvk.address_at(1u32, Scope::External);
let imt = SpacedLeafImtProvider::new();
let (inputs, _) = make_real_note_inputs(
&fvk,
&[3_000_000, 3_000_000, 3_000_000, 3_000_000, 3_000_000],
&[
Scope::External,
Scope::External,
Scope::External,
Scope::External,
Scope::External,
],
&imt,
&mut rng,
);
let mut inputs = inputs;
let (extra, _) =
make_real_note_inputs(&fvk, &[3_000_000], &[Scope::External], &imt, &mut rng);
inputs.extend(extra);
let result = build_delegation_bundle(
inputs,
&fvk,
pallas::Scalar::random(&mut rng),
output_recipient,
pallas::Base::random(&mut rng),
pallas::Base::random(&mut rng),
pallas::Base::random(&mut rng),
&imt,
&mut rng,
None,
);
assert!(matches!(
result,
Err(DelegationBuildError::InvalidNoteCount(6))
));
}
#[test]
fn test_missing_precomputed_padded_note_returns_error() {
let precomputed = PrecomputedRandomness {
padded_notes: vec![],
rseed_signed: [0u8; 32],
rseed_output: [0u8; 32],
};
let result = build_single_note_bundle_with_precomputed(&precomputed);
assert!(matches!(
result,
Err(DelegationBuildError::MissingPrecomputedPaddedNote {
index: 0,
actual: 0
})
));
}
#[test]
fn test_partial_precomputed_padded_notes_returns_later_missing_error() {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk: FullViewingKey = (&sk).into();
let precomputed = PrecomputedRandomness {
padded_notes: vec![
make_valid_padded_note_data(&mut rng),
make_valid_padded_note_data(&mut rng),
],
rseed_signed: [0u8; 32],
rseed_output: [0u8; 32],
};
let result =
build_single_note_bundle_with_fvk_and_precomputed(&fvk, &precomputed, &mut rng);
assert!(matches!(
result,
Err(DelegationBuildError::MissingPrecomputedPaddedNote {
index: 2,
actual: 2
})
));
}
#[test]
fn test_invalid_precomputed_padded_rho_returns_error() {
let precomputed = PrecomputedRandomness {
padded_notes: vec![PaddedNoteData {
rho: [0xffu8; 32],
rseed: [0u8; 32],
}],
rseed_signed: [0u8; 32],
rseed_output: [0u8; 32],
};
let result = build_single_note_bundle_with_precomputed(&precomputed);
assert!(matches!(
result,
Err(DelegationBuildError::InvalidPrecomputedRho { index: 0 })
));
}
#[test]
fn test_single_internal_note_builds_expected_output_shape() {
build_bundle(&[13_000_000], &[Scope::Internal]);
}
#[test]
#[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
fn test_mixed_scope_notes() {
let bundle = build_bundle(
&[4_000_000, 4_000_000, 3_000_000, 2_000_000],
&[
Scope::External,
Scope::Internal,
Scope::External,
Scope::Internal,
],
);
verify_bundle(&bundle);
}
#[test]
fn test_all_internal_notes_builds_expected_output_shape() {
build_bundle(
&[4_000_000, 4_000_000, 3_000_000, 2_000_000],
&[
Scope::Internal,
Scope::Internal,
Scope::Internal,
Scope::Internal,
],
);
}
}