use alloc::vec::Vec;
use group::Curve;
use halo2_proofs::circuit::Value;
use pasta_curves::{arithmetic::CurveAffine, pallas};
use rand::RngCore;
use orchard::{
keys::{FullViewingKey, Scope, SpendValidatingKey},
note::{commitment::ExtractedNoteCommitment, nullifier::Nullifier, Note, RandomSeed, Rho},
spec::NonIdentityPallasPoint,
tree::MerklePath,
value::NoteValue,
};
use super::{
circuit::{self, van_commitment_hash, rho_binding_hash, NoteSlotWitness},
imt::{derive_nullifier_domain, gov_null_hash, ImtProofData, ImtProvider},
};
#[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(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,
}
#[derive(Clone, Debug)]
pub enum DelegationBuildError {
InvalidNoteCount(usize),
ImtFetchFailed(super::imt::ImtError),
}
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–5)", n)
}
DelegationBuildError::ImtFetchFailed(e) => {
write!(f, "IMT proof fetch failed: {e}")
}
}
}
}
#[allow(clippy::too_many_arguments)]
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,
precomputed: Option<&PrecomputedRandomness>,
) -> Result<DelegationBundle, DelegationBuildError> {
let n_real = real_notes.len();
if n_real == 0 || n_real > 5 {
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 dom = derive_nullifier_domain(vote_round_id);
let mut note_slots = Vec::with_capacity(5);
let mut cmx_values = Vec::with_capacity(5);
let mut v_values = Vec::with_capacity(5);
let mut gov_nulls = Vec::with_capacity(5);
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.0);
let slot = NoteSlotWitness {
g_d: Value::known(recipient.g_d()),
pk_d: Value::known(
NonIdentityPallasPoint::from_bytes(&recipient.pk_d().to_bytes()).unwrap(),
),
v: Value::known(note.value()),
rho: Value::known(rho.into_inner()),
psi: Value::known(psi),
rcm: Value::known(rcm),
cm: Value::known(cm),
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..5 {
let pad_addr = fvk.address_at((1000 + i) as u32, Scope::External);
let pad_idx = i - n_real;
let pad_note = if let Some(pre) = precomputed {
assert!(pad_idx < pre.padded_notes.len(),
"precomputed.padded_notes has {} entries but need index {}",
pre.padded_notes.len(), pad_idx);
let pd = &pre.padded_notes[pad_idx];
let rho = Rho::from_bytes(&pd.rho).expect("precomputed rho must be valid");
let rseed = RandomSeed::from_bytes(pd.rseed, &rho).expect("precomputed rseed must be valid");
Note::from_parts(pad_addr, NoteValue::zero(), rho, rseed).expect("precomputed note must be valid")
} else {
let (_, _, dummy) = Note::dummy(&mut *rng, None);
Note::new(
pad_addr,
NoteValue::zero(),
Rho::from_nf_old(dummy.nullifier(fvk)),
&mut *rng,
)
};
let rho = pad_note.rho();
let psi = pad_note.rseed().psi(&rho);
let rcm = pad_note.rseed().rcm(&rho);
let cm = pad_note.commitment();
let cmx = ExtractedNoteCommitment::from(cm.clone()).inner();
let real_nf = pad_note.nullifier(fvk);
let gov_null = gov_null_hash(nk_val, dom, real_nf.0);
let imt_proof = imt_provider.non_membership_proof(real_nf.0)?;
let merkle_path = MerklePath::dummy(&mut *rng);
let slot = NoteSlotWitness {
g_d: Value::known(pad_addr.g_d()),
pk_d: Value::known(
NonIdentityPallasPoint::from_bytes(&pad_addr.pk_d().to_bytes()).unwrap(),
),
v: Value::known(NoteValue::zero()),
rho: Value::known(rho.into_inner()),
psi: Value::known(psi),
rcm: Value::known(rcm),
cm: Value::known(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),
};
note_slots.push(slot);
cmx_values.push(cmx);
v_values.push(0);
gov_nulls.push(gov_null);
}
let notes: [NoteSlotWitness; 5] = note_slots.try_into().unwrap_or_else(|_| unreachable!());
let v_total_u64: u64 = v_values.iter().sum();
let num_ballots_u64 = v_total_u64 / circuit::BALLOT_DIVISOR;
let remainder_u64 = v_total_u64 % circuit::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(rho));
let signed_note = if let Some(pre) = precomputed {
let rseed = RandomSeed::from_bytes(pre.rseed_signed, &signed_rho)
.expect("precomputed rseed_signed must be valid");
Note::from_parts(sender_address, NoteValue::from_raw(1), signed_rho, rseed)
.expect("precomputed signed note must be valid")
} 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 rseed = RandomSeed::from_bytes(pre.rseed_output, &output_rho)
.expect("precomputed rseed_output must be valid");
Note::from_parts(output_recipient, NoteValue::zero(), output_rho, rseed)
.expect("precomputed output note must be valid")
} 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::SpacedLeafImtProvider;
use orchard::{
constants::MERKLE_DEPTH_ORCHARD,
keys::{FullViewingKey, Scope, SpendingKey},
note::{commitment::ExtractedNoteCommitment, Note, Rho},
tree::{MerkleHashOrchard, MerklePath},
value::NoteValue,
};
use ff::Field;
use halo2_proofs::dev::MockProver;
use incrementalmerkletree::{Hashable, Level};
use pasta_curves::pallas;
use rand::rngs::OsRng;
const K: u32 = 14;
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!(n >= 1 && n <= 5);
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.0).unwrap();
inputs.push(RealNoteInput {
note,
fvk: fvk.clone(),
merkle_path,
imt_proof,
scope: scopes[i],
});
}
(inputs, nc_root)
}
fn build_and_verify(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();
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");
bundle
}
#[test]
fn test_single_real_note() {
build_and_verify(&[13_000_000], &[Scope::External]);
}
#[test]
fn test_four_real_notes() {
build_and_verify(
&[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() {
build_and_verify(&[7_000_000, 7_000_000], &[Scope::External, Scope::External]);
}
#[test]
fn test_min_weight_boundary() {
build_and_verify(&[12_500_000], &[Scope::External]);
}
#[test]
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() {
build_and_verify(
&[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() {
build_and_verify(
&[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_single_internal_note() {
build_and_verify(&[13_000_000], &[Scope::Internal]);
}
#[test]
fn test_mixed_scope_notes() {
build_and_verify(
&[4_000_000, 4_000_000, 3_000_000, 2_000_000],
&[Scope::External, Scope::Internal, Scope::External, Scope::Internal],
);
}
#[test]
fn test_all_internal_notes() {
build_and_verify(
&[4_000_000, 4_000_000, 3_000_000, 2_000_000],
&[Scope::Internal, Scope::Internal, Scope::Internal, Scope::Internal],
);
}
}