use alloc::vec::Vec;
use group::{Curve, GroupEncoding};
use halo2_proofs::{
circuit::{floor_planner, AssignedCell, Layouter, Value},
plonk::{self, Advice, Column, Constraints, Expression, Instance as InstanceColumn, Selector},
poly::Rotation,
};
use pasta_curves::{arithmetic::CurveAffine, pallas, vesta};
use crate::circuit::address_ownership::prove_address_ownership;
use crate::circuit::mul_chip::{MulChip, MulConfig, MulInstruction};
use orchard::{
circuit::{
commit_ivk::{CommitIvkChip, CommitIvkConfig},
gadget::{
add_chip::{AddChip, AddConfig},
assign_constant, assign_free_advice, derive_nullifier, note_commit, AddInstruction,
},
note_commit::{NoteCommitChip, NoteCommitConfig},
},
constants::{OrchardCommitDomains, OrchardFixedBases, OrchardHashDomains},
keys::{
CommitIvkRandomness, DiversifiedTransmissionKey, FullViewingKey, NullifierDerivingKey,
Scope, SpendValidatingKey,
},
note::{
commitment::{NoteCommitTrapdoor, NoteCommitment},
nullifier::Nullifier,
Note,
},
primitives::redpallas::{SpendAuth, VerificationKey},
spec::NonIdentityPallasPoint,
tree::MerkleHashOrchard,
value::NoteValue,
};
use halo2_gadgets::{
ecc::{
chip::{EccChip, EccConfig},
NonIdentityPoint, Point, ScalarFixed, ScalarVar,
},
poseidon::{
primitives::{self as poseidon, ConstantLength},
Hash as PoseidonHash, Pow5Chip as PoseidonChip, Pow5Config as PoseidonConfig,
},
sinsemilla::{
chip::{SinsemillaChip, SinsemillaConfig},
merkle::{
chip::{MerkleChip, MerkleConfig},
MerklePath as GadgetMerklePath,
},
},
utilities::{
bool_check,
lookup_range_check::LookupRangeCheckConfig,
},
};
use super::imt::IMT_DEPTH;
use super::imt_circuit::{ImtNonMembershipConfig, synthesize_imt_non_membership};
use crate::circuit::van_integrity;
use orchard::constants::MERKLE_DEPTH_ORCHARD;
pub const K: u32 = 14;
const NF_SIGNED: usize = 0;
const RK_X: usize = 1;
const RK_Y: usize = 2;
const CMX_NEW: usize = 3;
const VAN_COMM: usize = 4;
const VOTE_ROUND_ID: usize = 5;
const NC_ROOT: usize = 6;
const NF_IMT_ROOT: usize = 7;
const GOV_NULL_1: usize = 8;
const GOV_NULL_2: usize = 9;
const GOV_NULL_3: usize = 10;
const GOV_NULL_4: usize = 11;
const GOV_NULL_5: usize = 12;
const GOV_NULL_OFFSETS: [usize; 5] = [GOV_NULL_1, GOV_NULL_2, GOV_NULL_3, GOV_NULL_4, GOV_NULL_5];
const DOM: usize = 13;
pub(crate) const MAX_PROPOSAL_AUTHORITY: u64 = 65535;
pub(crate) fn rho_binding_hash(
cmx_1: pallas::Base,
cmx_2: pallas::Base,
cmx_3: pallas::Base,
cmx_4: pallas::Base,
cmx_5: pallas::Base,
van_comm: pallas::Base,
vote_round_id: pallas::Base,
) -> pallas::Base {
poseidon::Hash::<_, poseidon::P128Pow5T3, ConstantLength<7>, 3, 2>::init()
.hash([cmx_1, cmx_2, cmx_3, cmx_4, cmx_5, van_comm, vote_round_id])
}
pub(crate) const BALLOT_DIVISOR: u64 = 12_500_000;
pub(crate) fn van_commitment_hash(
g_d_new_x: pallas::Base,
pk_d_new_x: pallas::Base,
num_ballots: pallas::Base,
vote_round_id: pallas::Base,
van_comm_rand: pallas::Base,
) -> pallas::Base {
van_integrity::van_integrity_hash(
g_d_new_x,
pk_d_new_x,
num_ballots,
vote_round_id,
pallas::Base::from(MAX_PROPOSAL_AUTHORITY),
van_comm_rand,
)
}
#[derive(Clone, Debug)]
pub struct Config {
primary: Column<InstanceColumn>,
advices: [Column<Advice>; 10],
add_config: AddConfig,
mul_config: MulConfig,
ecc_config: EccConfig<OrchardFixedBases>,
poseidon_config: PoseidonConfig<pallas::Base, 3, 2>,
sinsemilla_config_1:
SinsemillaConfig<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases>,
sinsemilla_config_2:
SinsemillaConfig<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases>,
commit_ivk_config: CommitIvkConfig,
signed_note_commit_config: NoteCommitConfig,
new_note_commit_config: NoteCommitConfig,
range_check: LookupRangeCheckConfig<pallas::Base, 10>,
merkle_config_1: MerkleConfig<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases>,
merkle_config_2: MerkleConfig<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases>,
q_per_note: Selector,
q_scope_select: Selector,
imt_config: ImtNonMembershipConfig,
}
impl Config {
fn add_chip(&self) -> AddChip {
AddChip::construct(self.add_config.clone())
}
fn mul_chip(&self) -> MulChip {
MulChip::construct(self.mul_config.clone())
}
fn ecc_chip(&self) -> EccChip<OrchardFixedBases> {
EccChip::construct(self.ecc_config.clone())
}
fn poseidon_chip(&self) -> PoseidonChip<pallas::Base, 3, 2> {
PoseidonChip::construct(self.poseidon_config.clone())
}
fn commit_ivk_chip(&self) -> CommitIvkChip {
CommitIvkChip::construct(self.commit_ivk_config.clone())
}
fn sinsemilla_chip_1(
&self,
) -> SinsemillaChip<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases> {
SinsemillaChip::construct(self.sinsemilla_config_1.clone())
}
fn sinsemilla_chip_2(
&self,
) -> SinsemillaChip<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases> {
SinsemillaChip::construct(self.sinsemilla_config_2.clone())
}
fn note_commit_chip_signed(&self) -> NoteCommitChip {
NoteCommitChip::construct(self.signed_note_commit_config.clone())
}
fn note_commit_chip_new(&self) -> NoteCommitChip {
NoteCommitChip::construct(self.new_note_commit_config.clone())
}
fn merkle_chip_1(
&self,
) -> MerkleChip<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases> {
MerkleChip::construct(self.merkle_config_1.clone())
}
fn merkle_chip_2(
&self,
) -> MerkleChip<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases> {
MerkleChip::construct(self.merkle_config_2.clone())
}
fn range_check_config(&self) -> LookupRangeCheckConfig<pallas::Base, 10> {
self.range_check
}
}
#[derive(Clone, Debug, Default)]
pub struct NoteSlotWitness {
pub(crate) g_d: Value<NonIdentityPallasPoint>,
pub(crate) pk_d: Value<NonIdentityPallasPoint>,
pub(crate) v: Value<NoteValue>,
pub(crate) rho: Value<pallas::Base>,
pub(crate) psi: Value<pallas::Base>,
pub(crate) rcm: Value<NoteCommitTrapdoor>,
pub(crate) cm: Value<NoteCommitment>,
pub(crate) path: Value<[MerkleHashOrchard; MERKLE_DEPTH_ORCHARD]>,
pub(crate) pos: Value<u32>,
pub(crate) imt_nf_bounds: Value<[pallas::Base; 3]>,
pub(crate) imt_leaf_pos: Value<u32>,
pub(crate) imt_path: Value<[pallas::Base; IMT_DEPTH]>,
pub(crate) is_internal: Value<bool>,
}
#[derive(Clone, Debug, Default)]
pub struct Circuit {
nk: Value<NullifierDerivingKey>,
rho_signed: Value<pallas::Base>,
psi_signed: Value<pallas::Base>,
cm_signed: Value<NoteCommitment>,
ak: Value<SpendValidatingKey>,
alpha: Value<pallas::Scalar>,
rivk: Value<CommitIvkRandomness>,
rivk_internal: Value<CommitIvkRandomness>,
rcm_signed: Value<NoteCommitTrapdoor>,
g_d_signed: Value<NonIdentityPallasPoint>,
pk_d_signed: Value<DiversifiedTransmissionKey>,
g_d_new: Value<NonIdentityPallasPoint>,
pk_d_new: Value<DiversifiedTransmissionKey>,
psi_new: Value<pallas::Base>,
rcm_new: Value<NoteCommitTrapdoor>,
notes: [NoteSlotWitness; 5],
van_comm_rand: Value<pallas::Base>,
num_ballots: Value<pallas::Base>,
remainder: Value<pallas::Base>,
}
impl Circuit {
pub fn from_note_unchecked(fvk: &FullViewingKey, note: &Note, alpha: pallas::Scalar) -> Self {
let sender_address = note.recipient();
let rho_signed = note.rho();
let psi_signed = note.rseed().psi(&rho_signed);
let rcm_signed = note.rseed().rcm(&rho_signed);
Circuit {
nk: Value::known(*fvk.nk()),
rho_signed: Value::known(rho_signed.into_inner()),
psi_signed: Value::known(psi_signed),
cm_signed: Value::known(note.commitment()),
ak: Value::known(fvk.clone().into()),
alpha: Value::known(alpha),
rivk: Value::known(fvk.rivk(Scope::External)),
rivk_internal: Value::known(fvk.rivk(Scope::Internal)),
rcm_signed: Value::known(rcm_signed),
g_d_signed: Value::known(sender_address.g_d()),
pk_d_signed: Value::known(*sender_address.pk_d()),
..Default::default()
}
}
pub fn with_output_note(mut self, output_note: &Note) -> Self {
let rho_new = output_note.rho();
let psi_new = output_note.rseed().psi(&rho_new);
let rcm_new = output_note.rseed().rcm(&rho_new);
self.g_d_new = Value::known(output_note.recipient().g_d());
self.pk_d_new = Value::known(*output_note.recipient().pk_d());
self.psi_new = Value::known(psi_new);
self.rcm_new = Value::known(rcm_new);
self
}
pub fn with_notes(mut self, notes: [NoteSlotWitness; 5]) -> Self {
self.notes = notes;
self
}
pub fn with_van_comm_rand(mut self, van_comm_rand: pallas::Base) -> Self {
self.van_comm_rand = Value::known(van_comm_rand);
self
}
pub fn with_ballot_scaling(mut self, num_ballots: pallas::Base, remainder: pallas::Base) -> Self {
self.num_ballots = Value::known(num_ballots);
self.remainder = Value::known(remainder);
self
}
}
impl plonk::Circuit<pallas::Base> for Circuit {
type Config = Config;
type FloorPlanner = floor_planner::V1;
fn without_witnesses(&self) -> Self {
Self::default()
}
fn configure(meta: &mut plonk::ConstraintSystem<pallas::Base>) -> Self::Config {
let advices = [
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
meta.advice_column(),
];
let primary = meta.instance_column();
let table_idx = meta.lookup_table_column();
let lookup = (
table_idx,
meta.lookup_table_column(),
meta.lookup_table_column(),
);
let lagrange_coeffs = [
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
meta.fixed_column(),
];
let rc_a = lagrange_coeffs[2..5].try_into().unwrap();
let rc_b = lagrange_coeffs[5..8].try_into().unwrap();
meta.enable_equality(primary);
for advice in advices.iter() {
meta.enable_equality(*advice);
}
meta.enable_constant(lagrange_coeffs[0]);
let q_per_note = meta.selector();
meta.create_gate("Per-note checks", |meta| {
let q_per_note = meta.query_selector(q_per_note);
let v = meta.query_advice(advices[0], Rotation::cur());
let root = meta.query_advice(advices[1], Rotation::cur());
let anchor = meta.query_advice(advices[2], Rotation::cur());
let imt_root = meta.query_advice(advices[3], Rotation::cur());
let nf_imt_root = meta.query_advice(advices[4], Rotation::cur());
Constraints::with_selector(
q_per_note,
[
(
"v * (root - anchor) = 0",
v * (root - anchor),
),
("imt_root = nf_imt_root", imt_root - nf_imt_root),
],
)
});
let q_scope_select = meta.selector();
meta.create_gate("scope ivk select", |meta| {
let q = meta.query_selector(q_scope_select);
let is_internal = meta.query_advice(advices[0], Rotation::cur());
let ivk = meta.query_advice(advices[1], Rotation::cur());
let ivk_internal = meta.query_advice(advices[2], Rotation::cur());
let selected_ivk = meta.query_advice(advices[3], Rotation::cur());
let expected = ivk.clone() + is_internal.clone() * (ivk_internal - ivk);
Constraints::with_selector(q, [
("bool_check is_internal", bool_check(is_internal)),
("scope select", selected_ivk - expected),
])
});
let imt_config = ImtNonMembershipConfig::configure(meta, &advices);
let add_config = AddChip::configure(meta, advices[7], advices[8], advices[6]);
let mul_config = MulChip::configure(meta, advices[7], advices[8], advices[6]);
let range_check = LookupRangeCheckConfig::configure(meta, advices[9], table_idx);
let ecc_config =
EccChip::<OrchardFixedBases>::configure(meta, advices, lagrange_coeffs, range_check);
let poseidon_config = PoseidonChip::configure::<poseidon::P128Pow5T3>(
meta,
advices[6..9].try_into().unwrap(),
advices[5],
rc_a,
rc_b,
);
let configure_sinsemilla_merkle =
|meta: &mut plonk::ConstraintSystem<pallas::Base>,
advice_cols: [Column<Advice>; 5],
witness_col: Column<Advice>,
lagrange_col: Column<plonk::Fixed>| {
let sinsemilla =
SinsemillaChip::configure(meta, advice_cols, witness_col, lagrange_col, lookup, range_check);
let merkle = MerkleChip::configure(meta, sinsemilla.clone());
(sinsemilla, merkle)
};
let (sinsemilla_config_1, merkle_config_1) = configure_sinsemilla_merkle(
meta, advices[..5].try_into().unwrap(), advices[6], lagrange_coeffs[0],
);
let (sinsemilla_config_2, merkle_config_2) = configure_sinsemilla_merkle(
meta, advices[5..].try_into().unwrap(), advices[7], lagrange_coeffs[1],
);
let commit_ivk_config = CommitIvkChip::configure(meta, advices);
let signed_note_commit_config =
NoteCommitChip::configure(meta, advices, sinsemilla_config_1.clone());
let new_note_commit_config =
NoteCommitChip::configure(meta, advices, sinsemilla_config_2.clone());
Config {
primary,
advices,
add_config,
mul_config,
ecc_config,
poseidon_config,
sinsemilla_config_1,
sinsemilla_config_2,
commit_ivk_config,
signed_note_commit_config,
new_note_commit_config,
range_check,
merkle_config_1,
merkle_config_2,
q_per_note,
q_scope_select,
imt_config,
}
}
#[allow(non_snake_case)]
fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<pallas::Base>,
) -> Result<(), plonk::Error> {
SinsemillaChip::load(config.sinsemilla_config_1.clone(), &mut layouter)?;
let ecc_chip = config.ecc_chip();
let ak_P: Value<pallas::Point> = self.ak.as_ref().map(|ak| ak.into());
let ak_P = NonIdentityPoint::new(
ecc_chip.clone(),
layouter.namespace(|| "witness ak_P"),
ak_P.map(|ak_P| ak_P.to_affine()),
)?;
let g_d_signed = NonIdentityPoint::new(
ecc_chip.clone(),
layouter.namespace(|| "witness g_d_signed"),
self.g_d_signed.as_ref().map(|gd| gd.to_affine()),
)?;
let pk_d_signed = NonIdentityPoint::new(
ecc_chip.clone(),
layouter.namespace(|| "witness pk_d_signed"),
self.pk_d_signed
.as_ref()
.map(|pk_d_signed| pk_d_signed.inner().to_affine()),
)?;
let nk = assign_free_advice(
layouter.namespace(|| "witness nk"),
config.advices[0],
self.nk.map(|nk| nk.inner()),
)?;
let rho_signed = assign_free_advice(
layouter.namespace(|| "witness rho_signed"),
config.advices[0],
self.rho_signed,
)?;
let psi_signed = assign_free_advice(
layouter.namespace(|| "witness psi_signed"),
config.advices[0],
self.psi_signed,
)?;
let cm_signed = Point::new(
ecc_chip.clone(),
layouter.namespace(|| "witness cm_signed"),
self.cm_signed.as_ref().map(|cm| cm.inner().to_affine()),
)?;
let nf_signed = derive_nullifier(
layouter
.namespace(|| "nf_signed = DeriveNullifier_nk(rho_signed, psi_signed, cm_signed)"),
config.poseidon_chip(),
config.add_chip(),
ecc_chip.clone(),
rho_signed.clone(), &psi_signed,
&cm_signed,
nk.clone(), )?;
layouter.constrain_instance(nf_signed.inner().cell(), config.primary, NF_SIGNED)?;
crate::circuit::spend_authority::prove_spend_authority(
ecc_chip.clone(),
layouter.namespace(|| "cond4 spend authority"),
self.alpha,
&ak_P.clone().into(),
config.primary,
RK_X,
RK_Y,
)?;
let ak = ak_P.extract_p().inner().clone();
let ak_for_internal = ak.clone();
let rivk = ScalarFixed::new(
ecc_chip.clone(),
layouter.namespace(|| "rivk"),
self.rivk.map(|rivk| rivk.inner()),
)?;
let ivk_cell = prove_address_ownership(
config.sinsemilla_chip_1(),
ecc_chip.clone(),
config.commit_ivk_chip(),
layouter.namespace(|| "cond5"),
"cond5",
ak,
nk.clone(),
rivk,
&g_d_signed,
&pk_d_signed,
)?;
let ivk_internal_cell = {
use orchard::circuit::commit_ivk::gadgets::commit_ivk;
let rivk_internal = ScalarFixed::new(
ecc_chip.clone(),
layouter.namespace(|| "rivk_internal"),
self.rivk_internal.map(|rivk| rivk.inner()),
)?;
let ivk_internal = commit_ivk(
config.sinsemilla_chip_1(),
ecc_chip.clone(),
config.commit_ivk_chip(),
layouter.namespace(|| "commit_ivk_internal"),
ak_for_internal,
nk.clone(),
rivk_internal,
)?;
ivk_internal.inner().clone()
};
{
let pk_d_signed_for_nc = NonIdentityPoint::new(
ecc_chip.clone(),
layouter.namespace(|| "pk_d_signed for note_commit"),
self.pk_d_signed
.map(|pk_d_signed| pk_d_signed.inner().to_affine()),
)?;
pk_d_signed_for_nc.constrain_equal(
layouter.namespace(|| "pk_d_signed_for_nc == pk_d_signed"),
&pk_d_signed,
)?;
let rcm_signed = ScalarFixed::new(
ecc_chip.clone(),
layouter.namespace(|| "rcm_signed"),
self.rcm_signed.as_ref().map(|rcm| rcm.inner()),
)?;
let v_signed = assign_free_advice(
layouter.namespace(|| "v_signed = 1"),
config.advices[0],
Value::known(NoteValue::from_raw(1)),
)?;
let derived_cm_signed = note_commit(
layouter.namespace(|| "NoteCommit_rcm_signed(g_d, pk_d, 1, rho, psi)"),
config.sinsemilla_chip_1(),
config.ecc_chip(),
config.note_commit_chip_signed(),
g_d_signed.inner(),
pk_d_signed_for_nc.inner(),
v_signed,
rho_signed.clone(),
psi_signed,
rcm_signed,
)?;
derived_cm_signed
.constrain_equal(layouter.namespace(|| "cm_signed integrity"), &cm_signed)?;
}
let van_comm_cell = layouter.assign_region(
|| "copy van_comm from instance",
|mut region| {
region.assign_advice_from_instance(
|| "van_comm",
config.primary,
VAN_COMM,
config.advices[0],
0,
)
},
)?;
let vote_round_id_cell = layouter.assign_region(
|| "copy vote_round_id from instance",
|mut region| {
region.assign_advice_from_instance(
|| "vote_round_id",
config.primary,
VOTE_ROUND_ID,
config.advices[0],
0,
)
},
)?;
let dom_cell = layouter.assign_region(
|| "copy dom from instance",
|mut region| {
region.assign_advice_from_instance(
|| "dom",
config.primary,
DOM,
config.advices[0],
0,
)
},
)?;
let nc_root_cell = layouter.assign_region(
|| "copy nc_root from instance",
|mut region| {
region.assign_advice_from_instance(
|| "nc_root",
config.primary,
NC_ROOT,
config.advices[0],
0,
)
},
)?;
let nf_imt_root_cell = layouter.assign_region(
|| "copy nf_imt_root from instance",
|mut region| {
region.assign_advice_from_instance(
|| "nf_imt_root",
config.primary,
NF_IMT_ROOT,
config.advices[0],
0,
)
},
)?;
let mut cmx_cells = Vec::with_capacity(5);
let mut v_cells = Vec::with_capacity(5);
let mut gov_null_cells = Vec::with_capacity(5);
for i in 0..5 {
let (cmx_i, v_i, gov_null_i) = synthesize_note_slot(
&config,
&mut layouter,
ecc_chip.clone(),
&ivk_cell,
&ivk_internal_cell,
&nk,
&dom_cell,
&nc_root_cell,
&nf_imt_root_cell,
&self.notes[i],
i,
GOV_NULL_OFFSETS[i],
)?;
cmx_cells.push(cmx_i);
v_cells.push(v_i);
gov_null_cells.push(gov_null_i);
}
{
let poseidon_message = [
cmx_cells[0].clone(),
cmx_cells[1].clone(),
cmx_cells[2].clone(),
cmx_cells[3].clone(),
cmx_cells[4].clone(),
van_comm_cell.clone(),
vote_round_id_cell.clone(),
];
let poseidon_hasher = PoseidonHash::<
pallas::Base,
_,
poseidon::P128Pow5T3,
ConstantLength<7>,
3,
2,
>::init(
config.poseidon_chip(),
layouter.namespace(|| "rho binding Poseidon init"),
)?;
let derived_rho = poseidon_hasher.hash(
layouter.namespace(|| "Poseidon(cmx_1..5, van_comm, vote_round_id)"),
poseidon_message,
)?;
layouter.assign_region(
|| "rho binding equality",
|mut region| region.constrain_equal(derived_rho.cell(), rho_signed.cell()),
)?;
}
let (g_d_new_x, pk_d_new_x) = {
let g_d_new = NonIdentityPoint::new(
ecc_chip.clone(),
layouter.namespace(|| "witness g_d_new"),
self.g_d_new.as_ref().map(|gd| gd.to_affine()),
)?;
let pk_d_new = NonIdentityPoint::new(
ecc_chip.clone(),
layouter.namespace(|| "witness pk_d_new"),
self.pk_d_new.map(|pk_d_new| pk_d_new.inner().to_affine()),
)?;
let rho_new = nf_signed.inner().clone();
let psi_new = assign_free_advice(
layouter.namespace(|| "witness psi_new"),
config.advices[0],
self.psi_new,
)?;
let rcm_new = ScalarFixed::new(
ecc_chip.clone(),
layouter.namespace(|| "rcm_new"),
self.rcm_new.as_ref().map(|rcm_new| rcm_new.inner()),
)?;
let v_new = assign_free_advice(
layouter.namespace(|| "v_new = 0"),
config.advices[0],
Value::known(NoteValue::zero()),
)?;
let cm_new = note_commit(
layouter.namespace(|| "NoteCommit_rcm_new(g_d_new, pk_d_new, 0, rho_new, psi_new)"),
config.sinsemilla_chip_2(),
config.ecc_chip(),
config.note_commit_chip_new(),
g_d_new.inner(),
pk_d_new.inner(),
v_new,
rho_new,
psi_new,
rcm_new,
)?;
let cmx = cm_new.extract_p();
layouter.constrain_instance(cmx.inner().cell(), config.primary, CMX_NEW)?;
(
g_d_new.extract_p().inner().clone(),
pk_d_new.extract_p().inner().clone(),
)
};
let add_chip = config.add_chip();
let sum_12 =
add_chip.add(layouter.namespace(|| "v_1 + v_2"), &v_cells[0], &v_cells[1])?;
let sum_123 = add_chip.add(
layouter.namespace(|| "(v_1 + v_2) + v_3"),
&sum_12,
&v_cells[2],
)?;
let sum_1234 = add_chip.add(
layouter.namespace(|| "(v_1 + v_2 + v_3) + v_4"),
&sum_123,
&v_cells[3],
)?;
let v_total = add_chip.add(
layouter.namespace(|| "(v_1 + v_2 + v_3 + v_4) + v_5"),
&sum_1234,
&v_cells[4],
)?;
let num_ballots = {
let num_ballots = assign_free_advice(
layouter.namespace(|| "witness num_ballots"),
config.advices[0],
self.num_ballots,
)?;
let remainder = assign_free_advice(
layouter.namespace(|| "witness remainder"),
config.advices[0],
self.remainder,
)?;
let ballot_divisor = assign_constant(
layouter.namespace(|| "BALLOT_DIVISOR constant"),
config.advices[0],
pallas::Base::from(BALLOT_DIVISOR),
)?;
let product = config.mul_chip().mul(
layouter.namespace(|| "num_ballots * BALLOT_DIVISOR"),
&num_ballots,
&ballot_divisor,
)?;
let reconstructed = config.add_chip().add(
layouter.namespace(|| "product + remainder"),
&product,
&remainder,
)?;
layouter.assign_region(
|| "num_ballots * BALLOT_DIVISOR + remainder == v_total",
|mut region| region.constrain_equal(reconstructed.cell(), v_total.cell()),
)?;
let shift_6 = assign_constant(
layouter.namespace(|| "2^6 shift constant"),
config.advices[0],
pallas::Base::from(1u64 << 6),
)?;
let remainder_shifted = config.mul_chip().mul(
layouter.namespace(|| "remainder * 2^6"),
&remainder,
&shift_6,
)?;
config.range_check_config().copy_check(
layouter.namespace(|| "remainder * 2^6 < 2^30 (i.e. remainder < 2^24)"),
remainder_shifted,
3, true, )?;
let one = assign_constant(
layouter.namespace(|| "one constant"),
config.advices[0],
pallas::Base::one(),
)?;
let nb_minus_one = num_ballots.value().map(|v| *v - pallas::Base::one());
let nb_minus_one = assign_free_advice(
layouter.namespace(|| "witness nb_minus_one"),
config.advices[0],
nb_minus_one,
)?;
let nb_recomputed = config.add_chip().add(
layouter.namespace(|| "nb_minus_one + 1"),
&nb_minus_one,
&one,
)?;
layouter.assign_region(
|| "nb_minus_one + 1 == num_ballots",
|mut region| region.constrain_equal(nb_recomputed.cell(), num_ballots.cell()),
)?;
config.range_check_config().copy_check(
layouter.namespace(|| "nb_minus_one < 2^30"),
nb_minus_one,
3, true, )?;
num_ballots
};
{
let van_comm_rand = assign_free_advice(
layouter.namespace(|| "witness van_comm_rand"),
config.advices[0],
self.van_comm_rand,
)?;
let domain_van = assign_constant(
layouter.namespace(|| "DOMAIN_VAN constant"),
config.advices[0],
pallas::Base::from(van_integrity::DOMAIN_VAN),
)?;
let max_proposal_authority = assign_constant(
layouter.namespace(|| "MAX_PROPOSAL_AUTHORITY constant"),
config.advices[0],
pallas::Base::from(MAX_PROPOSAL_AUTHORITY),
)?;
let derived_van_comm = van_integrity::van_integrity_poseidon(
&config.poseidon_config,
&mut layouter,
"Gov commitment",
domain_van,
g_d_new_x,
pk_d_new_x,
num_ballots,
vote_round_id_cell,
max_proposal_authority,
van_comm_rand,
)?;
layouter.assign_region(
|| "van_comm integrity",
|mut region| region.constrain_equal(derived_van_comm.cell(), van_comm_cell.cell()),
)?;
}
Ok(())
}
}
#[allow(clippy::too_many_arguments, non_snake_case)]
fn synthesize_note_slot(
config: &Config,
layouter: &mut impl Layouter<pallas::Base>,
ecc_chip: EccChip<OrchardFixedBases>,
ivk_cell: &AssignedCell<pallas::Base, pallas::Base>,
ivk_internal_cell: &AssignedCell<pallas::Base, pallas::Base>,
nk_cell: &AssignedCell<pallas::Base, pallas::Base>,
dom_cell: &AssignedCell<pallas::Base, pallas::Base>,
nc_root_cell: &AssignedCell<pallas::Base, pallas::Base>,
nf_imt_root_cell: &AssignedCell<pallas::Base, pallas::Base>,
note: &NoteSlotWitness,
slot: usize,
gov_null_offset: usize,
) -> Result<
(
AssignedCell<pallas::Base, pallas::Base>,
AssignedCell<pallas::Base, pallas::Base>,
AssignedCell<pallas::Base, pallas::Base>,
),
plonk::Error,
> {
let s = slot;
let g_d = NonIdentityPoint::new(
ecc_chip.clone(),
layouter.namespace(|| format!("note {s} witness g_d")),
note.g_d.as_ref().map(|gd| gd.to_affine()),
)?;
let pk_d = NonIdentityPoint::new(
ecc_chip.clone(),
layouter.namespace(|| format!("note {s} witness pk_d")),
note.pk_d.as_ref().map(|pk| pk.to_affine()),
)?;
let v = assign_free_advice(
layouter.namespace(|| format!("note {s} witness v")),
config.advices[0],
note.v,
)?;
let rho = assign_free_advice(
layouter.namespace(|| format!("note {s} witness rho")),
config.advices[0],
note.rho,
)?;
let psi = assign_free_advice(
layouter.namespace(|| format!("note {s} witness psi")),
config.advices[0],
note.psi,
)?;
let rcm = ScalarFixed::new(
ecc_chip.clone(),
layouter.namespace(|| format!("note {s} rcm")),
note.rcm.as_ref().map(|rcm| rcm.inner()),
)?;
let cm = Point::new(
ecc_chip.clone(),
layouter.namespace(|| format!("note {s} witness cm")),
note.cm.as_ref().map(|cm| cm.inner().to_affine()),
)?;
let derived_cm = note_commit(
layouter.namespace(|| format!("note {s} NoteCommit")),
config.sinsemilla_chip_1(),
config.ecc_chip(),
config.note_commit_chip_signed(),
g_d.inner(),
pk_d.inner(),
v.clone(),
rho.clone(),
psi.clone(),
rcm,
)?;
derived_cm.constrain_equal(layouter.namespace(|| format!("note {s} cm integrity")), &cm)?;
let cmx_cell = cm.extract_p().inner().clone();
let v_base = assign_free_advice(
layouter.namespace(|| format!("note {s} witness v_base")),
config.advices[0],
note.v.map(|val| pallas::Base::from(val.inner())),
)?;
layouter.assign_region(
|| format!("note {s} v = v_base"),
|mut region| region.constrain_equal(v.cell(), v_base.cell()),
)?;
let is_internal = assign_free_advice(
layouter.namespace(|| format!("note {s} witness is_internal")),
config.advices[0],
note.is_internal.map(|b| pallas::Base::from(b as u64)),
)?;
let selected_ivk = layouter.assign_region(
|| format!("note {s} scope ivk select"),
|mut region| {
config.q_scope_select.enable(&mut region, 0)?;
is_internal.copy_advice(|| "is_internal", &mut region, config.advices[0], 0)?;
ivk_cell.copy_advice(|| "ivk", &mut region, config.advices[1], 0)?;
ivk_internal_cell.copy_advice(|| "ivk_internal", &mut region, config.advices[2], 0)?;
let selected = ivk_cell.value().zip(ivk_internal_cell.value()).zip(is_internal.value()).map(
|((ivk, ivk_int), flag)| {
if *flag == pallas::Base::one() { *ivk_int } else { *ivk }
},
);
region.assign_advice(|| "selected_ivk", config.advices[3], 0, || selected)
},
)?;
let ivk_scalar = ScalarVar::from_base(
ecc_chip.clone(),
layouter.namespace(|| format!("note {s} selected_ivk to scalar")),
&selected_ivk,
)?;
let (derived_pk_d, _ivk) = g_d.mul(
layouter.namespace(|| format!("note {s} [selected_ivk] g_d")),
ivk_scalar,
)?;
derived_pk_d.constrain_equal(
layouter.namespace(|| format!("note {s} pk_d equality")),
&pk_d,
)?;
let real_nf = derive_nullifier(
layouter.namespace(|| format!("note {s} real_nf = DeriveNullifier")),
config.poseidon_chip(),
config.add_chip(),
ecc_chip.clone(),
rho.clone(),
&psi,
&cm,
nk_cell.clone(),
)?;
let gov_null = {
let poseidon_hasher =
PoseidonHash::<pallas::Base, _, poseidon::P128Pow5T3, ConstantLength<3>, 3, 2>::init(
config.poseidon_chip(),
layouter.namespace(|| format!("note {s} gov_null init")),
)?;
poseidon_hasher.hash(
layouter.namespace(|| format!("note {s} Poseidon(nk, dom, real_nf)")),
[nk_cell.clone(), dom_cell.clone(), real_nf.inner().clone()],
)?
};
let gov_null_cell = gov_null.clone();
layouter.constrain_instance(gov_null.cell(), config.primary, gov_null_offset)?;
let root = {
let path = note
.path
.map(|typed_path| typed_path.map(|node| node.inner()));
let merkle_inputs = GadgetMerklePath::construct(
[config.merkle_chip_1(), config.merkle_chip_2()],
OrchardHashDomains::MerkleCrh,
note.pos,
path,
);
let leaf = cm.extract_p().inner().clone();
merkle_inputs
.calculate_root(layouter.namespace(|| format!("note {s} Merkle path")), leaf)?
};
let imt_root = synthesize_imt_non_membership(
&config.imt_config,
&config.poseidon_config,
&config.ecc_config,
layouter,
note.imt_nf_bounds,
note.imt_leaf_pos,
note.imt_path,
real_nf.inner(),
s,
)?;
layouter.assign_region(
|| format!("note {s} per-note checks"),
|mut region| {
config.q_per_note.enable(&mut region, 0)?;
v.copy_advice(|| "v", &mut region, config.advices[0], 0)?;
root.copy_advice(|| "calculated root", &mut region, config.advices[1], 0)?;
nc_root_cell.copy_advice(|| "nc_root (anchor)", &mut region, config.advices[2], 0)?;
imt_root.copy_advice(|| "imt_root", &mut region, config.advices[3], 0)?;
nf_imt_root_cell.copy_advice(|| "nf_imt_root", &mut region, config.advices[4], 0)?;
Ok(())
},
)?;
Ok((cmx_cell, v_base, gov_null_cell))
}
#[derive(Clone, Debug)]
pub struct Instance {
pub nf_signed: Nullifier,
pub rk: VerificationKey<SpendAuth>,
pub cmx_new: pallas::Base,
pub van_comm: pallas::Base,
pub vote_round_id: pallas::Base,
pub nc_root: pallas::Base,
pub nf_imt_root: pallas::Base,
pub gov_null: [pallas::Base; 5],
pub dom: pallas::Base,
}
impl Instance {
pub fn from_parts(
nf_signed: Nullifier,
rk: VerificationKey<SpendAuth>,
cmx_new: pallas::Base,
van_comm: pallas::Base,
vote_round_id: pallas::Base,
nc_root: pallas::Base,
nf_imt_root: pallas::Base,
gov_null: [pallas::Base; 5],
dom: pallas::Base,
) -> Self {
Instance {
nf_signed,
rk,
cmx_new,
van_comm,
vote_round_id,
nc_root,
nf_imt_root,
gov_null,
dom,
}
}
pub fn to_halo2_instance(&self) -> Vec<vesta::Scalar> {
let rk = pallas::Point::from_bytes(&self.rk.clone().into())
.expect("rk is a valid curve point (guaranteed by VerificationKey)")
.to_affine()
.coordinates()
.expect("rk is not the identity point (guaranteed by VerificationKey)");
vec![
self.nf_signed.0,
*rk.x(),
*rk.y(),
self.cmx_new,
self.van_comm,
self.vote_round_id,
self.nc_root,
self.nf_imt_root,
self.gov_null[0],
self.gov_null[1],
self.gov_null[2],
self.gov_null[3],
self.gov_null[4],
self.dom,
]
}
}
#[cfg(test)]
mod tests {
use alloc::string::{String, ToString};
use super::*;
use crate::delegation::imt::{derive_nullifier_domain, gov_null_hash, ImtProofData, ImtProvider, SpacedLeafImtProvider};
use orchard::{
keys::{FullViewingKey, Scope, SpendValidatingKey, SpendingKey},
note::{commitment::ExtractedNoteCommitment, Note, Rho},
};
use ff::Field;
use halo2_proofs::dev::MockProver;
use incrementalmerkletree::{Hashable, Level};
use pasta_curves::{arithmetic::CurveAffine, pallas};
use rand::rngs::OsRng;
use super::K;
fn make_note_slot(
note: &Note,
auth_path: &[MerkleHashOrchard; MERKLE_DEPTH_ORCHARD],
pos: u32,
imt: &ImtProofData,
is_internal: bool,
) -> NoteSlotWitness {
let rho = note.rho();
let psi = note.rseed().psi(&rho);
let rcm = note.rseed().rcm(&rho);
let cm = note.commitment();
let recipient = note.recipient();
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(*auth_path),
pos: Value::known(pos),
imt_nf_bounds: Value::known(imt.nf_bounds),
imt_leaf_pos: Value::known(imt.leaf_pos),
imt_path: Value::known(imt.path),
is_internal: Value::known(is_internal),
}
}
struct TestData {
circuit: Circuit,
instance: Instance,
}
fn make_test_data() -> TestData {
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 nk_val = fvk.nk().inner();
let ak: SpendValidatingKey = fvk.clone().into();
let vote_round_id = pallas::Base::random(&mut rng);
let dom = derive_nullifier_domain(vote_round_id);
let van_comm_rand = pallas::Base::random(&mut rng);
let imt_provider = SpacedLeafImtProvider::new();
let nf_imt_root = imt_provider.root();
let recipient = fvk.address_at(0u32, Scope::External);
let note_value = NoteValue::from_raw(13_000_000);
let (_, _, dummy_parent) = Note::dummy(&mut rng, None);
let real_note = Note::new(
recipient,
note_value,
Rho::from_nf_old(dummy_parent.nullifier(&fvk)),
&mut rng,
);
let cmx_real_e = ExtractedNoteCommitment::from(real_note.commitment());
let cmx_real = cmx_real_e.inner();
let empty_leaf = MerkleHashOrchard::empty_leaf();
let leaves = [
MerkleHashOrchard::from_cmx(&cmx_real_e),
empty_leaf,
empty_leaf,
empty_leaf,
];
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 l2_0 = MerkleHashOrchard::combine(Level::from(1), &l1_0, &l1_1);
let mut current = l2_0;
for level in 2..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 mut auth_path_0 = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
auth_path_0[0] = leaves[1];
auth_path_0[1] = l1_1;
for level in 2..MERKLE_DEPTH_ORCHARD {
auth_path_0[level] = MerkleHashOrchard::empty_root(Level::from(level as u8));
}
let real_nf = real_note.nullifier(&fvk);
let imt_0 = imt_provider.non_membership_proof(real_nf.0).unwrap();
let gov_null_0 = gov_null_hash(nk_val, dom, real_nf.0);
let slot_0 = make_note_slot(&real_note, &auth_path_0, 0u32, &imt_0, false);
let mut note_slots = vec![slot_0];
let mut cmx_values = vec![cmx_real];
let mut gov_nulls = vec![gov_null_0];
let dummy_auth_path = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
for i in 1..5u32 {
let pad_addr = fvk.address_at(100 + i, Scope::External);
let (_, _, dummy) = Note::dummy(&mut rng, None);
let pad_note = Note::new(
pad_addr,
NoteValue::zero(),
Rho::from_nf_old(dummy.nullifier(&fvk)),
&mut rng,
);
let pad_cmx = ExtractedNoteCommitment::from(pad_note.commitment()).inner();
let pad_nf = pad_note.nullifier(&fvk);
let pad_imt = imt_provider.non_membership_proof(pad_nf.0).unwrap();
let pad_gov_null = gov_null_hash(nk_val, dom, pad_nf.0);
note_slots.push(make_note_slot(
&pad_note,
&dummy_auth_path,
0u32,
&pad_imt,
false,
));
cmx_values.push(pad_cmx);
gov_nulls.push(pad_gov_null);
}
let notes: [NoteSlotWitness; 5] = note_slots.try_into().unwrap();
let v_total_u64: u64 = 13_000_000;
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_note = Note::new(
sender_address,
NoteValue::from_raw(1),
Rho::from_nf_old(Nullifier(rho)),
&mut rng,
);
let nf_signed = signed_note.nullifier(&fvk);
let output_note = Note::new(
output_recipient,
NoteValue::zero(),
Rho::from_nf_old(nf_signed),
&mut rng,
);
let cmx_new = ExtractedNoteCommitment::from(output_note.commitment()).inner();
let alpha = pallas::Scalar::random(&mut rng);
let rk = ak.randomize(&alpha);
let 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 = 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,
);
TestData { circuit, instance }
}
#[test]
fn happy_path() {
let t = make_test_data();
let pi = t.instance.to_halo2_instance();
let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
assert_eq!(prover.verify(), Ok(()));
}
#[test]
fn wrong_nf_fails() {
let t = make_test_data();
let mut instance = t.instance.clone();
instance.nf_signed = Nullifier(pallas::Base::random(&mut OsRng));
let pi = instance.to_halo2_instance();
let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn wrong_rk_fails() {
let mut rng = OsRng;
let t = make_test_data();
let sk2 = SpendingKey::random(&mut rng);
let fvk2: FullViewingKey = (&sk2).into();
let ak2: SpendValidatingKey = fvk2.into();
let wrong_rk = ak2.randomize(&pallas::Scalar::random(&mut rng));
let mut instance = t.instance.clone();
instance.rk = wrong_rk;
let pi = instance.to_halo2_instance();
let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn wrong_gov_null_fails() {
let t = make_test_data();
let mut instance = t.instance.clone();
instance.gov_null[0] = pallas::Base::random(&mut OsRng);
let pi = instance.to_halo2_instance();
let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn wrong_nc_root_fails() {
let t = make_test_data();
let mut instance = t.instance.clone();
instance.nc_root = pallas::Base::random(&mut OsRng);
let pi = instance.to_halo2_instance();
let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn wrong_imt_root_fails() {
let t = make_test_data();
let mut instance = t.instance.clone();
instance.nf_imt_root = pallas::Base::random(&mut OsRng);
let pi = instance.to_halo2_instance();
let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn wrong_van_comm_fails() {
let t = make_test_data();
let mut instance = t.instance.clone();
instance.van_comm = pallas::Base::random(&mut OsRng);
let pi = instance.to_halo2_instance();
let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn wrong_vote_round_id_fails() {
let t = make_test_data();
let mut instance = t.instance.clone();
instance.vote_round_id = pallas::Base::random(&mut OsRng);
let pi = instance.to_halo2_instance();
let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn instance_to_halo2_roundtrip() {
let t = make_test_data();
let pi = t.instance.to_halo2_instance();
assert_eq!(pi.len(), 14, "Expected exactly 14 public inputs");
assert_eq!(pi[NF_SIGNED], t.instance.nf_signed.0);
assert_eq!(pi[CMX_NEW], t.instance.cmx_new);
assert_eq!(pi[VAN_COMM], t.instance.van_comm);
assert_eq!(pi[NC_ROOT], t.instance.nc_root);
assert_eq!(pi[NF_IMT_ROOT], t.instance.nf_imt_root);
assert_eq!(pi[GOV_NULL_1], t.instance.gov_null[0]);
assert_eq!(pi[DOM], t.instance.dom);
}
#[test]
fn default_circuit_shape() {
let t = make_test_data();
let empty = plonk::Circuit::without_witnesses(&t.circuit);
let params = halo2_proofs::poly::commitment::Params::<vesta::Affine>::new(K);
let vk = halo2_proofs::plonk::keygen_vk(¶ms, &empty);
assert!(
vk.is_ok(),
"keygen_vk must succeed on without_witnesses circuit"
);
}
#[test]
fn fake_real_note_nonzero_value_fails() {
let mut rng = OsRng;
let t = make_test_data();
let mut circuit = t.circuit;
let pi = t.instance.to_halo2_instance();
let sk2 = SpendingKey::random(&mut rng);
let fvk2: FullViewingKey = (&sk2).into();
let addr2 = fvk2.address_at(0u32, Scope::External);
let (_, _, dummy_parent) = Note::dummy(&mut rng, None);
let fake_note = Note::new(
addr2,
NoteValue::from_raw(100), Rho::from_nf_old(dummy_parent.nullifier(&fvk2)),
&mut rng,
);
let imt_provider = SpacedLeafImtProvider::new();
let fake_nf = fake_note.nullifier(&fvk2);
let fake_imt = imt_provider.non_membership_proof(fake_nf.0).unwrap();
let dummy_auth_path = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
let fake_slot = make_note_slot(&fake_note, &dummy_auth_path, 0u32, &fake_imt, false);
circuit.notes[1] = fake_slot;
let prover = MockProver::run(K, &circuit, vec![pi]).unwrap();
assert!(prover.verify().is_err());
}
#[test]
fn different_ivk_per_note_fails() {
let mut rng = OsRng;
let t = make_test_data();
let mut circuit = t.circuit;
let pi = t.instance.to_halo2_instance();
let sk2 = SpendingKey::random(&mut rng);
let fvk2: FullViewingKey = (&sk2).into();
let addr2 = fvk2.address_at(100u32, Scope::External);
let (_, _, dummy_parent) = Note::dummy(&mut rng, None);
let foreign_note = Note::new(
addr2,
NoteValue::zero(),
Rho::from_nf_old(dummy_parent.nullifier(&fvk2)),
&mut rng,
);
let imt_provider = SpacedLeafImtProvider::new();
let foreign_nf = foreign_note.nullifier(&fvk2);
let foreign_imt = imt_provider.non_membership_proof(foreign_nf.0).unwrap();
let dummy_auth_path = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
let foreign_slot = make_note_slot(
&foreign_note,
&dummy_auth_path,
0u32,
&foreign_imt,
false,
);
circuit.notes[1] = foreign_slot;
let prover = MockProver::run(K, &circuit, vec![pi]).unwrap();
assert!(prover.verify().is_err());
}
use std::collections::BTreeMap;
use halo2_proofs::plonk::{Any, Assigned, Assignment, Column, Error, Fixed, FloorPlanner};
struct RegionInfo {
name: String,
min_row: Option<usize>,
max_row: Option<usize>,
}
impl RegionInfo {
fn track_row(&mut self, row: usize) {
self.min_row = Some(self.min_row.map_or(row, |m| m.min(row)));
self.max_row = Some(self.max_row.map_or(row, |m| m.max(row)));
}
fn row_count(&self) -> usize {
match (self.min_row, self.max_row) {
(Some(lo), Some(hi)) => hi - lo + 1,
_ => 0,
}
}
}
struct RegionTracker {
regions: Vec<RegionInfo>,
current_region: Option<usize>,
total_rows: usize,
namespace_stack: Vec<String>,
}
impl RegionTracker {
fn new() -> Self {
Self {
regions: Vec::new(),
current_region: None,
total_rows: 0,
namespace_stack: Vec::new(),
}
}
fn current_prefix(&self) -> String {
if self.namespace_stack.is_empty() {
String::new()
} else {
format!("{}/", self.namespace_stack.join("/"))
}
}
}
impl Assignment<pallas::Base> for RegionTracker {
fn enter_region<NR, N>(&mut self, name_fn: N)
where
NR: Into<String>,
N: FnOnce() -> NR,
{
let idx = self.regions.len();
let raw_name: String = name_fn().into();
let prefixed = format!("{}{}", self.current_prefix(), raw_name);
self.regions.push(RegionInfo {
name: prefixed,
min_row: None,
max_row: None,
});
self.current_region = Some(idx);
}
fn exit_region(&mut self) {
self.current_region = None;
}
fn enable_selector<A, AR>(
&mut self,
_: A,
_selector: &Selector,
row: usize,
) -> Result<(), Error>
where
A: FnOnce() -> AR,
AR: Into<String>,
{
if let Some(idx) = self.current_region {
self.regions[idx].track_row(row);
}
if row + 1 > self.total_rows {
self.total_rows = row + 1;
}
Ok(())
}
fn query_instance(
&self,
_column: Column<InstanceColumn>,
_row: usize,
) -> Result<Value<pallas::Base>, Error> {
Ok(Value::unknown())
}
fn assign_advice<V, VR, A, AR>(
&mut self,
_: A,
_column: Column<Advice>,
row: usize,
_to: V,
) -> Result<(), Error>
where
V: FnOnce() -> Value<VR>,
VR: Into<Assigned<pallas::Base>>,
A: FnOnce() -> AR,
AR: Into<String>,
{
if let Some(idx) = self.current_region {
self.regions[idx].track_row(row);
}
if row + 1 > self.total_rows {
self.total_rows = row + 1;
}
Ok(())
}
fn assign_fixed<V, VR, A, AR>(
&mut self,
_: A,
_column: Column<Fixed>,
row: usize,
_to: V,
) -> Result<(), Error>
where
V: FnOnce() -> Value<VR>,
VR: Into<Assigned<pallas::Base>>,
A: FnOnce() -> AR,
AR: Into<String>,
{
if let Some(idx) = self.current_region {
self.regions[idx].track_row(row);
}
if row + 1 > self.total_rows {
self.total_rows = row + 1;
}
Ok(())
}
fn copy(
&mut self,
_left_column: Column<Any>,
_left_row: usize,
_right_column: Column<Any>,
_right_row: usize,
) -> Result<(), Error> {
Ok(())
}
fn fill_from_row(
&mut self,
_column: Column<Fixed>,
_row: usize,
_to: Value<Assigned<pallas::Base>>,
) -> Result<(), Error> {
Ok(())
}
fn push_namespace<NR, N>(&mut self, name_fn: N)
where
NR: Into<String>,
N: FnOnce() -> NR,
{
self.namespace_stack.push(name_fn().into());
}
fn pop_namespace(&mut self, _: Option<String>) {
self.namespace_stack.pop();
}
}
#[test]
fn cost_breakdown() {
let mut cs = plonk::ConstraintSystem::default();
let config = <Circuit as plonk::Circuit<pallas::Base>>::configure(&mut cs);
let constants_col = cs.fixed_column();
let circuit = Circuit::default();
let mut tracker = RegionTracker::new();
floor_planner::V1::synthesize(&mut tracker, &circuit, config, vec![constants_col])
.unwrap();
let mut regions: Vec<_> = tracker
.regions
.iter()
.filter(|r| r.row_count() > 0)
.collect();
regions.sort_by(|a, b| b.row_count().cmp(&a.row_count()));
std::println!(
"\n=== Delegation Circuit Cost Breakdown (K={}, {} total rows) ===",
K,
1u64 << K
);
std::println!("Total rows used: {}\n", tracker.total_rows);
std::println!("Per-region (sorted by cost):");
for r in ®ions {
std::println!(
" {:60} {:>6} rows (rows {}-{})",
r.name,
r.row_count(),
r.min_row.unwrap(),
r.max_row.unwrap()
);
}
std::println!("\nAggregated by top-level condition:");
let mut aggregated: BTreeMap<String, (usize, usize)> = BTreeMap::new();
for r in &tracker.regions {
if r.row_count() == 0 {
continue;
}
let key = if r.name.starts_with("note ")
&& r.name.as_bytes().get(5).map_or(false, |b| b.is_ascii_digit())
{
if let Some(slash) = r.name.find('/') {
let rest = &r.name[slash + 1..];
let top = rest.split('/').next().unwrap_or(rest);
let top = if top.starts_with("MerkleCRH(") {
"Merkle path (Sinsemilla)"
} else if top.starts_with("Poseidon(left, right) level") {
"IMT Poseidon path"
} else if top.starts_with("imt swap level") {
"IMT swap"
} else {
top
};
format!("Per-note: {}", top)
} else {
r.name.clone()
}
} else {
let top = r.name.split('/').next().unwrap_or(&r.name);
top.to_string()
};
let entry = aggregated.entry(key).or_insert((0, 0));
entry.0 += r.row_count();
entry.1 += 1;
}
let mut agg_sorted: Vec<_> = aggregated.into_iter().collect();
agg_sorted.sort_by(|a, b| b.1 .0.cmp(&a.1 .0));
for (name, (total, count)) in &agg_sorted {
if *count > 1 {
std::println!(
" {:60} {:>6} rows ({} x{})",
name, total, total / count, count
);
} else {
std::println!(" {:60} {:>6} rows", name, total);
}
}
std::println!();
}
#[test]
#[ignore]
fn row_budget() {
use std::println;
use halo2_proofs::dev::CircuitCost;
use pasta_curves::vesta;
let t = make_test_data();
let cost = CircuitCost::<vesta::Point, _>::measure(K, &t.circuit);
let debug = alloc::format!("{cost:?}");
let extract = |field: &str| -> usize {
let prefix = alloc::format!("{field}: ");
debug.split(&prefix)
.nth(1)
.and_then(|s| s.split([',', ' ', '}']).next())
.and_then(|n| n.parse().ok())
.unwrap_or(0)
};
let max_rows = extract("max_rows");
let max_advice_rows = extract("max_advice_rows");
let max_fixed_rows = extract("max_fixed_rows");
let total_available = 1usize << K;
println!("=== delegation circuit row budget (K={K}) ===");
println!(" max_rows (floor-planner high-water mark): {max_rows}");
println!(" max_advice_rows: {max_advice_rows}");
println!(" max_fixed_rows: {max_fixed_rows}");
println!(" 2^K (total available rows): {total_available}");
println!(" headroom: {}", total_available.saturating_sub(max_rows));
println!(" utilisation: {:.1}%",
100.0 * max_rows as f64 / total_available as f64);
println!();
println!(" Full debug: {debug}");
let cost_default = CircuitCost::<vesta::Point, _>::measure(K, &Circuit::default());
let debug_default = alloc::format!("{cost_default:?}");
let max_rows_default = debug_default
.split("max_rows: ").nth(1)
.and_then(|s| s.split([',', ' ', '}']).next())
.and_then(|n| n.parse::<usize>().ok())
.unwrap_or(0);
if max_rows_default == max_rows {
println!(" Witness-independence: PASS \
(Circuit::default() max_rows={max_rows_default} == filled max_rows={max_rows})");
} else {
println!(" Witness-independence: FAIL \
(Circuit::default() max_rows={max_rows_default} != filled max_rows={max_rows}) \
— row count depends on witness values!");
}
println!(" MERKLE_DEPTH_ORCHARD (circuit constant): {MERKLE_DEPTH_ORCHARD}");
println!(" IMT_DEPTH (circuit constant): {IMT_DEPTH}");
for probe_k in 11u32..=K {
let t = make_test_data();
match MockProver::run(probe_k, &t.circuit, vec![t.instance.to_halo2_instance()]) {
Err(_) => {
println!(" K={probe_k}: not enough rows (synthesizer rejected)");
continue;
}
Ok(p) => match p.verify() {
Ok(()) => {
println!(" Minimum viable K: {probe_k} (2^{probe_k} = {} rows, {:.1}% headroom)",
1usize << probe_k,
100.0 * (1.0 - max_rows as f64 / (1usize << probe_k) as f64));
break;
}
Err(_) => println!(" K={probe_k}: too small"),
},
}
}
}
}