use halo2_proofs::{
circuit::{AssignedCell, Layouter, Value},
plonk::{self, Advice, Column, Constraints, Expression, Selector},
poly::Rotation,
};
use pasta_curves::pallas;
use ff::Field;
use halo2_gadgets::{
ecc::chip::EccConfig,
poseidon::{
primitives::{self as poseidon, ConstantLength},
Hash as PoseidonHash, Pow5Chip as PoseidonChip, Pow5Config as PoseidonConfig,
},
};
use orchard::circuit::gadget::assign_free_advice;
use orchard::constants::OrchardFixedBases;
use super::imt::IMT_DEPTH;
use crate::circuit::poseidon_merkle::{MerkleSwapGate, synthesize_poseidon_merkle_path};
#[derive(Clone, Debug)]
pub(crate) struct PuncturedIntervalGate {
pub(crate) q_interval: Selector,
pub(crate) q_neq: Selector,
advices: [Column<Advice>; 3],
}
impl PuncturedIntervalGate {
pub(crate) fn configure(
meta: &mut plonk::ConstraintSystem<pallas::Base>,
advices: [Column<Advice>; 3],
) -> Self {
let q_interval = meta.selector();
let q_neq = meta.selector();
meta.create_gate("Punctured interval check", |meta| {
let q = meta.query_selector(q_interval);
let nf_lo = meta.query_advice(advices[0], Rotation::cur());
let nf_hi = meta.query_advice(advices[1], Rotation::cur());
let real_nf = meta.query_advice(advices[2], Rotation::cur());
let x_lo = meta.query_advice(advices[1], Rotation::next());
let x_hi = meta.query_advice(advices[2], Rotation::next());
let one = Expression::Constant(pallas::Base::one());
Constraints::with_selector(
q,
[
("x_lo = real_nf - nf_lo - 1", x_lo.clone() - (real_nf.clone() - nf_lo - one.clone())),
("x_hi = nf_hi - real_nf - 1", x_hi.clone() - (nf_hi - real_nf - one)),
],
)
});
meta.create_gate("Non-equality check (nf != nf_mid)", |meta| {
let q = meta.query_selector(q_neq);
let real_nf = meta.query_advice(advices[2], Rotation::cur());
let nf_mid = meta.query_advice(advices[0], Rotation::next());
let diff_inv = meta.query_advice(advices[0], Rotation(2));
let one = Expression::Constant(pallas::Base::one());
Constraints::with_selector(
q,
[
("(nf - nf_mid) * inv = 1", (real_nf - nf_mid) * diff_inv - one),
],
)
});
PuncturedIntervalGate {
q_interval,
q_neq,
advices,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn assign(
&self,
region: &mut halo2_proofs::circuit::Region<'_, pallas::Base>,
offset: usize,
nf_lo: &AssignedCell<pallas::Base, pallas::Base>,
nf_mid: &AssignedCell<pallas::Base, pallas::Base>,
nf_hi: &AssignedCell<pallas::Base, pallas::Base>,
real_nf: &AssignedCell<pallas::Base, pallas::Base>,
) -> Result<
(
AssignedCell<pallas::Base, pallas::Base>,
AssignedCell<pallas::Base, pallas::Base>,
),
plonk::Error,
> {
self.q_interval.enable(region, offset)?;
self.q_neq.enable(region, offset)?;
nf_lo.copy_advice(|| "nf_lo", region, self.advices[0], offset)?;
nf_hi.copy_advice(|| "nf_hi", region, self.advices[1], offset)?;
real_nf.copy_advice(|| "real_nf", region, self.advices[2], offset)?;
nf_mid.copy_advice(|| "nf_mid", region, self.advices[0], offset + 1)?;
let x_lo = region.assign_advice(
|| "x_lo = real_nf - nf_lo - 1",
self.advices[1],
offset + 1,
|| {
real_nf
.value()
.copied()
.zip(nf_lo.value().copied())
.map(|(nf, lo)| nf - lo - pallas::Base::one())
},
)?;
let x_hi = region.assign_advice(
|| "x_hi = nf_hi - real_nf - 1",
self.advices[2],
offset + 1,
|| {
nf_hi
.value()
.copied()
.zip(real_nf.value().copied())
.map(|(hi, nf)| hi - nf - pallas::Base::one())
},
)?;
region.assign_advice(
|| "diff_inv = 1/(real_nf - nf_mid)",
self.advices[0],
offset + 2,
|| {
real_nf
.value()
.copied()
.zip(nf_mid.value().copied())
.map(|(nf, mid)| {
let diff = nf - mid;
debug_assert!(
bool::from(!diff.is_zero()),
"real_nf must not equal nf_mid — the nullifier is already in the tree"
);
diff.invert().unwrap_or(pallas::Base::zero())
})
},
)?;
Ok((x_lo, x_hi))
}
}
#[derive(Clone, Debug)]
pub(crate) struct ImtNonMembershipConfig {
pub(crate) swap_gate: MerkleSwapGate,
pub(crate) interval_gate: PuncturedIntervalGate,
pub(crate) advice_0: Column<Advice>,
}
impl ImtNonMembershipConfig {
pub(crate) fn configure(
meta: &mut plonk::ConstraintSystem<pallas::Base>,
advices: &[Column<Advice>; 10],
) -> Self {
let swap_gate = MerkleSwapGate::configure(
meta,
[advices[0], advices[1], advices[2], advices[3], advices[4]],
);
let interval_gate = PuncturedIntervalGate::configure(
meta,
[advices[0], advices[1], advices[2]],
);
ImtNonMembershipConfig {
swap_gate,
interval_gate,
advice_0: advices[0],
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn synthesize_imt_non_membership(
imt_config: &ImtNonMembershipConfig,
poseidon_config: &PoseidonConfig<pallas::Base, 3, 2>,
ecc_config: &EccConfig<OrchardFixedBases>,
layouter: &mut impl Layouter<pallas::Base>,
imt_nf_bounds: Value<[pallas::Base; 3]>,
imt_leaf_pos: Value<u32>,
imt_path: Value<[pallas::Base; IMT_DEPTH]>,
real_nf: &AssignedCell<pallas::Base, pallas::Base>,
slot: usize,
) -> Result<AssignedCell<pallas::Base, pallas::Base>, plonk::Error> {
let s = slot;
let imt_nf_lo = assign_free_advice(
layouter.namespace(|| format!("note {s} imt_nf_lo")),
imt_config.advice_0,
imt_nf_bounds.map(|b| b[0]),
)?;
let imt_nf_mid = assign_free_advice(
layouter.namespace(|| format!("note {s} imt_nf_mid")),
imt_config.advice_0,
imt_nf_bounds.map(|b| b[1]),
)?;
let imt_nf_hi = assign_free_advice(
layouter.namespace(|| format!("note {s} imt_nf_hi")),
imt_config.advice_0,
imt_nf_bounds.map(|b| b[2]),
)?;
let leaf_hash = {
let poseidon_hasher = PoseidonHash::<
pallas::Base,
_,
poseidon::P128Pow5T3,
ConstantLength<3>,
3,
2,
>::init(
PoseidonChip::construct(poseidon_config.clone()),
layouter.namespace(|| format!("note {s} imt leaf hash init")),
)?;
poseidon_hasher.hash(
layouter.namespace(|| format!("note {s} Poseidon3(nf_lo, nf_mid, nf_hi)")),
[imt_nf_lo.clone(), imt_nf_mid.clone(), imt_nf_hi.clone()],
)?
};
let imt_root = synthesize_poseidon_merkle_path::<IMT_DEPTH>(
&imt_config.swap_gate,
poseidon_config,
layouter,
imt_config.advice_0,
leaf_hash,
imt_leaf_pos,
imt_path,
&format!("note {s} imt"),
)?;
let (x_lo, x_hi) = layouter.assign_region(
|| format!("note {s} punctured interval check"),
|mut region| {
imt_config.interval_gate.assign(
&mut region,
0,
&imt_nf_lo,
&imt_nf_mid,
&imt_nf_hi,
real_nf,
)
},
)?;
ecc_config.lookup_config.copy_check(
layouter.namespace(|| format!("note {s} x_lo < 2^250")),
x_lo,
25,
true,
)?;
ecc_config.lookup_config.copy_check(
layouter.namespace(|| format!("note {s} x_hi < 2^250")),
x_hi,
25,
true,
)?;
Ok(imt_root)
}