use std::collections::{BTreeMap, BTreeSet};
use primitives::{
algebra::{
elliptic_curve::{BaseFieldElement, Curve25519Ristretto as EC, Point, Scalar},
field::Bit,
BoxedUint,
},
utils::codec::bincode_io,
};
use strum::VariantNames;
use crate::{
circuit::latest::{
AlgebraicType,
BitPlaintextBinaryOp,
BitPlaintextUnaryOp,
BitShareBinaryOp,
BitShareUnaryOp,
Circuit,
Constant,
ConstraintClause,
ConstraintExpr,
DigestAlgorithm,
Encoding,
FieldPlaintextBinaryOp,
FieldPlaintextUnaryOp,
FieldShareBinaryOp,
FieldShareUnaryOp,
FieldType,
Gate,
Input,
OnAmbiguity,
PlaintextBitConstraint,
PointPlaintextBinaryOp,
PointPlaintextUnaryOp,
PointShareBinaryOp,
PointShareUnaryOp,
Relation,
SignatureScheme,
Slice,
SliceEnum,
},
config::{DefaultConfig as C, MpcFieldElement},
};
fn all_gates() -> Vec<Gate<C>> {
let scalar = || Scalar::<EC>::from(1u64);
let base_field = || BaseFieldElement::<EC>::from(1u64);
let mpc_field = || MpcFieldElement::<C>::from(1u64);
let bit = || Bit::from(false);
let point = || Point::<EC>::identity();
let exp = || BoxedUint::from(vec![1u64]);
vec![
Gate::Input(Input::Plaintext {
algebraic_type: AlgebraicType::BaseField,
batch_size: 1,
}),
Gate::Input(Input::SecretPlaintext {
inputer: 0,
algebraic_type: AlgebraicType::ScalarField,
batch_size: 1,
}),
Gate::Input(Input::Share {
algebraic_type: AlgebraicType::Point,
batch_size: 1,
}),
Gate::Constant(Constant::Scalar(scalar())),
Gate::Constant(Constant::ScalarBatch(vec![scalar()])),
Gate::Constant(Constant::BaseField(base_field())),
Gate::Constant(Constant::BaseFieldBatch(vec![base_field()])),
Gate::Constant(Constant::MpcField(mpc_field())),
Gate::Constant(Constant::MpcFieldBatch(vec![mpc_field()])),
Gate::Constant(Constant::Bit(bit())),
Gate::Constant(Constant::BitBatch(vec![bit()])),
Gate::Constant(Constant::Point(Box::new(point()))),
Gate::Constant(Constant::PointBatch(vec![point()])),
Gate::Random {
algebraic_type: AlgebraicType::BaseField,
batch_size: 1,
},
Gate::Random {
algebraic_type: AlgebraicType::ScalarField,
batch_size: 1,
},
Gate::Random {
algebraic_type: AlgebraicType::Point,
batch_size: 1,
},
Gate::Random {
algebraic_type: AlgebraicType::Bit,
batch_size: 1,
},
Gate::Random {
algebraic_type: AlgebraicType::MpcField,
batch_size: 1,
},
Gate::FieldShareUnaryOp {
x: 0,
op: FieldShareUnaryOp::Neg,
},
Gate::FieldShareUnaryOp {
x: 0,
op: FieldShareUnaryOp::MulInverse,
},
Gate::FieldShareUnaryOp {
x: 0,
op: FieldShareUnaryOp::Open,
},
Gate::FieldShareUnaryOp {
x: 0,
op: FieldShareUnaryOp::IsZero,
},
Gate::FieldShareBinaryOp {
x: 0,
y: 1,
op: FieldShareBinaryOp::Add,
},
Gate::FieldShareBinaryOp {
x: 0,
y: 1,
op: FieldShareBinaryOp::Mul,
},
Gate::BatchSummation { x: 0 },
Gate::BitShareUnaryOp {
x: 0,
op: BitShareUnaryOp::Not,
},
Gate::BitShareUnaryOp {
x: 0,
op: BitShareUnaryOp::Open,
},
Gate::BitShareBinaryOp {
x: 0,
y: 1,
op: BitShareBinaryOp::Xor,
},
Gate::BitShareBinaryOp {
x: 0,
y: 1,
op: BitShareBinaryOp::Or,
},
Gate::BitShareBinaryOp {
x: 0,
y: 1,
op: BitShareBinaryOp::And,
},
Gate::PointShareUnaryOp {
p: 0,
op: PointShareUnaryOp::Neg,
},
Gate::PointShareUnaryOp {
p: 0,
op: PointShareUnaryOp::Open,
},
Gate::PointShareUnaryOp {
p: 0,
op: PointShareUnaryOp::IsZero,
},
Gate::PointShareBinaryOp {
p: 0,
y: 1,
op: PointShareBinaryOp::Add,
},
Gate::PointShareBinaryOp {
p: 0,
y: 1,
op: PointShareBinaryOp::ScalarMul,
},
Gate::FieldPlaintextUnaryOp {
x: 0,
op: FieldPlaintextUnaryOp::Neg,
},
Gate::FieldPlaintextUnaryOp {
x: 0,
op: FieldPlaintextUnaryOp::MulInverse,
},
Gate::FieldPlaintextUnaryOp {
x: 0,
op: FieldPlaintextUnaryOp::BitExtract {
little_endian_bit_idx: 0,
signed: false,
},
},
Gate::FieldPlaintextUnaryOp {
x: 0,
op: FieldPlaintextUnaryOp::Sqrt,
},
Gate::FieldPlaintextUnaryOp {
x: 0,
op: FieldPlaintextUnaryOp::Pow { exp: exp() },
},
Gate::FieldPlaintextBinaryOp {
x: 0,
y: 1,
op: FieldPlaintextBinaryOp::Add,
},
Gate::FieldPlaintextBinaryOp {
x: 0,
y: 1,
op: FieldPlaintextBinaryOp::Mul,
},
Gate::FieldPlaintextBinaryOp {
x: 0,
y: 1,
op: FieldPlaintextBinaryOp::EuclDiv,
},
Gate::FieldPlaintextBinaryOp {
x: 0,
y: 1,
op: FieldPlaintextBinaryOp::Mod,
},
Gate::FieldPlaintextBinaryOp {
x: 0,
y: 1,
op: FieldPlaintextBinaryOp::Gt,
},
Gate::FieldPlaintextBinaryOp {
x: 0,
y: 1,
op: FieldPlaintextBinaryOp::Ge,
},
Gate::FieldPlaintextBinaryOp {
x: 0,
y: 1,
op: FieldPlaintextBinaryOp::Eq,
},
Gate::FieldPlaintextBinaryOp {
x: 0,
y: 1,
op: FieldPlaintextBinaryOp::Xor,
},
Gate::FieldPlaintextBinaryOp {
x: 0,
y: 1,
op: FieldPlaintextBinaryOp::Or,
},
Gate::BitPlaintextUnaryOp {
x: 0,
op: BitPlaintextUnaryOp::Not,
},
Gate::BitPlaintextBinaryOp {
x: 0,
y: 1,
op: BitPlaintextBinaryOp::Xor,
},
Gate::BitPlaintextBinaryOp {
x: 0,
y: 1,
op: BitPlaintextBinaryOp::Or,
},
Gate::BitPlaintextBinaryOp {
x: 0,
y: 1,
op: BitPlaintextBinaryOp::And,
},
Gate::PointPlaintextUnaryOp {
p: 0,
op: PointPlaintextUnaryOp::Neg,
},
Gate::PointPlaintextBinaryOp {
p: 0,
y: 1,
op: PointPlaintextBinaryOp::Add,
},
Gate::PointPlaintextBinaryOp {
p: 0,
y: 1,
op: PointPlaintextBinaryOp::ScalarMul,
},
Gate::DaBit {
field_type: FieldType::BaseField,
batch_size: 1,
},
Gate::DaBit {
field_type: FieldType::ScalarField,
batch_size: 1,
},
Gate::DaBit {
field_type: FieldType::MpcField,
batch_size: 1,
},
Gate::GetDaBitFieldShare { x: 0 },
Gate::GetDaBitSharedBit { x: 0 },
Gate::BaseFieldPow { x: 0, exp: exp() },
Gate::BitPlaintextToField {
x: 0,
field_type: FieldType::BaseField,
},
Gate::BitPlaintextToField {
x: 0,
field_type: FieldType::ScalarField,
},
Gate::BitPlaintextToField {
x: 0,
field_type: FieldType::MpcField,
},
Gate::FieldPlaintextToBit { x: 0 },
Gate::ExtractFromBatch {
x: 0,
slice: Slice::single(0),
},
Gate::ExtractFromBatch {
x: 0,
slice: Slice::range(0, 2, 1).expect("valid slice"),
},
Gate::ExtractFromBatch {
x: 0,
slice: Slice::range2d(0, 2, 2, 1, 1).expect("valid slice"),
},
Gate::ExtractFromBatch {
x: 0,
slice: {
let mut s = Slice::empty();
s.append(Slice::single(0));
s
},
},
Gate::CollectToBatch { wires: vec![0, 1] },
Gate::PointFromPlaintextCoordinates { wires: vec![0, 1] },
Gate::PlaintextPointToCoordinates { point: 0 },
Gate::PlaintextKeccakF1600 { x: 0 },
Gate::CompressPlaintextPoint { point: 0 },
Gate::KeyRecoveryPlaintextComputeErrors {
d_minus_one: 0,
syndromes: 1,
},
Gate::AesGcmKeyStream {
round_keys: 0,
iv: 1,
n_ciphertext_blocks: 1,
},
Gate::GhashPowersOfH {
h: 0,
n_ciphertext_blocks: 1,
},
Gate::Ghash {
x: 0,
powers_of_h: 1,
},
Gate::AesKeySchedule { key: 0 },
Gate::ConstrainPlaintextBits {
x: 0,
on_ambiguity: OnAmbiguity::Fail,
clauses: vec![
ConstraintClause::new(vec![
PlaintextBitConstraint::Signature {
scheme: SignatureScheme::Ed25519,
signature: ConstraintExpr::Slice(
Slice::range(0, 512, 1).expect("valid slice"),
),
message: ConstraintExpr::Concat(vec![
ConstraintExpr::Constant(vec![0x31]),
ConstraintExpr::Slice(Slice::range(512, 8, 1).expect("valid slice")),
ConstraintExpr::Digest {
algorithm: DigestAlgorithm::Sha256,
of: Box::new(ConstraintExpr::Wire(1)),
},
]),
public_key: ConstraintExpr::Wire(1),
},
PlaintextBitConstraint::Equality {
bits: ConstraintExpr::Slice(Slice::range(0, 2, 1).expect("valid slice")),
expected: ConstraintExpr::Constant(vec![0xff]),
},
]),
ConstraintClause::new(vec![
PlaintextBitConstraint::Comparison {
relation: Relation::AtMost,
lhs: ConstraintExpr::Slice(Slice::range(0, 8, 1).expect("valid slice")),
rhs: ConstraintExpr::Decode {
encoding: Encoding::Base64UrlNoPad,
of: Box::new(ConstraintExpr::Wire(1)),
},
},
PlaintextBitConstraint::Comparison {
relation: Relation::AtLeast,
lhs: ConstraintExpr::Slice(Slice::range(0, 8, 1).expect("valid slice")),
rhs: ConstraintExpr::Constant(vec![0x01]),
},
]),
],
},
Gate::ConstrainPlaintextBits {
x: 0,
on_ambiguity: OnAmbiguity::TakeSmallestBits,
clauses: vec![ConstraintClause::new(vec![
PlaintextBitConstraint::Equality {
bits: ConstraintExpr::Slice(Slice::range(0, 2, 1).expect("valid slice")),
expected: ConstraintExpr::Constant(vec![0xff]),
},
])],
},
Gate::GatherFromBatches {
parts: vec![(0, Slice::single(0)), (1, Slice::range(0, 2, 1).unwrap())],
},
]
}
fn sample_circuit() -> Circuit<C> {
let mut circuit = Circuit::new();
let x = circuit
.add_gate(Gate::Input(Input::SecretPlaintext {
inputer: 0,
algebraic_type: AlgebraicType::ScalarField,
batch_size: 1,
}))
.expect("valid gate");
let y = circuit
.add_gate(Gate::Input(Input::SecretPlaintext {
inputer: 1,
algebraic_type: AlgebraicType::ScalarField,
batch_size: 1,
}))
.expect("valid gate");
let z = circuit
.add_gate(Gate::FieldShareBinaryOp {
x,
y,
op: FieldShareBinaryOp::Add,
})
.expect("valid gate");
circuit.add_output(z).expect("valid output");
circuit
}
fn assert_covered<T: VariantNames>(ty: &str, covered: &BTreeSet<&str>) {
let missing: Vec<&str> = T::VARIANTS
.iter()
.copied()
.filter(|name| !covered.contains(name))
.collect();
assert!(
missing.is_empty(),
"\n\n`all_gates()` has no instance of {ty} variant(s) {missing:?}. Every variant MUST \
appear in `all_gates()`, otherwise it is never encoded into `testdata/circuit.bin` and a \
later reorder of it is invisible. Append one instance per variant, then regenerate the \
fixture by re-running the tests.\n"
);
}
#[test]
fn all_gates_covers_every_variant() {
let gates = all_gates();
assert_covered::<Gate<C>>("Gate", &gates.iter().map(AsRef::as_ref).collect());
macro_rules! assert_op_covered {
($ty:ty, $pattern:pat => $op:expr) => {
assert_covered::<$ty>(
stringify!($ty),
&gates
.iter()
.filter_map(|gate| match gate {
$pattern => Some($op),
_ => None,
})
.collect(),
);
};
}
assert_op_covered!(Input, Gate::Input(input) => input.as_ref());
assert_op_covered!(Constant<C>, Gate::Constant(constant) => constant.as_ref());
assert_op_covered!(FieldShareUnaryOp, Gate::FieldShareUnaryOp { op, .. } => op.as_ref());
assert_op_covered!(FieldShareBinaryOp, Gate::FieldShareBinaryOp { op, .. } => op.as_ref());
assert_op_covered!(BitShareUnaryOp, Gate::BitShareUnaryOp { op, .. } => op.as_ref());
assert_op_covered!(BitShareBinaryOp, Gate::BitShareBinaryOp { op, .. } => op.as_ref());
assert_op_covered!(PointShareUnaryOp, Gate::PointShareUnaryOp { op, .. } => op.as_ref());
assert_op_covered!(PointShareBinaryOp, Gate::PointShareBinaryOp { op, .. } => op.as_ref());
assert_op_covered!(FieldPlaintextUnaryOp, Gate::FieldPlaintextUnaryOp { op, .. } => op.as_ref());
assert_op_covered!(FieldPlaintextBinaryOp, Gate::FieldPlaintextBinaryOp { op, .. } => op.as_ref());
assert_op_covered!(BitPlaintextUnaryOp, Gate::BitPlaintextUnaryOp { op, .. } => op.as_ref());
assert_op_covered!(BitPlaintextBinaryOp, Gate::BitPlaintextBinaryOp { op, .. } => op.as_ref());
assert_op_covered!(PointPlaintextUnaryOp, Gate::PointPlaintextUnaryOp { op, .. } => op.as_ref());
assert_op_covered!(PointPlaintextBinaryOp, Gate::PointPlaintextBinaryOp { op, .. } => op.as_ref());
assert_op_covered!(AlgebraicType, Gate::Random { algebraic_type, .. } => algebraic_type.as_ref());
assert_op_covered!(FieldType, Gate::DaBit { field_type, .. } => field_type.as_ref());
assert_op_covered!(SliceEnum, Gate::ExtractFromBatch { slice, .. } => slice.inner().as_ref());
assert_op_covered!(OnAmbiguity, Gate::ConstrainPlaintextBits { on_ambiguity, .. } => on_ambiguity.as_ref());
let mut found: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
for gate in &gates {
let Gate::ConstrainPlaintextBits { clauses, .. } = gate else {
continue;
};
for constraint in clauses.iter().flat_map(ConstraintClause::constraints) {
found
.entry("constraint")
.or_default()
.insert(constraint.as_ref());
match constraint {
PlaintextBitConstraint::Signature { scheme, .. } => {
found.entry("scheme").or_default().insert(scheme.as_ref());
}
PlaintextBitConstraint::Comparison { relation, .. } => {
found
.entry("relation")
.or_default()
.insert(relation.as_ref());
}
PlaintextBitConstraint::Equality { .. } => {}
}
for operand in constraint.operands() {
collect_exprs(operand, &mut found);
}
}
}
let empty = BTreeSet::new();
let of = |key| found.get(key).unwrap_or(&empty).clone();
assert_covered::<PlaintextBitConstraint>("PlaintextBitConstraint", &of("constraint"));
assert_covered::<ConstraintExpr>("ConstraintExpr", &of("expr"));
assert_covered::<DigestAlgorithm>("DigestAlgorithm", &of("digest"));
assert_covered::<SignatureScheme>("SignatureScheme", &of("scheme"));
assert_covered::<Relation>("Relation", &of("relation"));
assert_covered::<Encoding>("Encoding", &of("encoding"));
}
fn collect_exprs<'a>(expr: &'a ConstraintExpr, found: &mut BTreeMap<&str, BTreeSet<&'a str>>) {
found.entry("expr").or_default().insert(expr.as_ref());
match expr {
ConstraintExpr::Concat(parts) => {
for part in parts {
collect_exprs(part, found);
}
}
ConstraintExpr::Digest { algorithm, of } => {
found
.entry("digest")
.or_default()
.insert(algorithm.as_ref());
collect_exprs(of, found);
}
ConstraintExpr::Decode { encoding, of } => {
found
.entry("encoding")
.or_default()
.insert(encoding.as_ref());
collect_exprs(of, found);
}
ConstraintExpr::Slice(_) | ConstraintExpr::Constant(_) | ConstraintExpr::Wire(_) => {}
}
}
#[test]
fn fixture_is_current() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/circuit/latest/testdata/circuit.bin"
);
let bytes = bincode_io::serialize(&(sample_circuit(), all_gates()))
.expect("circuit/gate bincode serialization");
if std::fs::read(path).unwrap_or_default() != bytes {
std::fs::write(path, &bytes).unwrap_or_else(|e| panic!("write {path}: {e}"));
panic!(
"\n\n`testdata/circuit.bin` was stale and has been regenerated -- commit it. This is \
not by itself a wire-format break: appending a gate is compatible. \
`base_fixture_still_decodes` decides that, and CI runs it against the base branch's \
fixture.\n"
);
}
}
#[test]
fn base_fixture_still_decodes() {
let Ok(path) = std::env::var("CIRCUIT_BASE_FIXTURE") else {
return;
};
let fixture = std::fs::read(&path).unwrap_or_else(|e| panic!("read base fixture {path}: {e}"));
let (circuit, gates): (Circuit<C>, Vec<Gate<C>>) = bincode_io::deserialize(&fixture)
.unwrap_or_else(|e| {
panic!(
"\n\nThe base branch's circuit fixture no longer deserializes with today's code \
({e}), so the wire format broke. See this module's docs for the bump steps.\n"
)
});
assert_eq!(
circuit,
sample_circuit(),
"\n\nThe base branch's fixture no longer decodes to `sample_circuit()`. Either a \
`latest::Circuit` field changed -- a wire-format break, see the module docs for the bump \
steps -- or `sample_circuit()` was edited, which MUST NOT happen without a bump.\n"
);
let current = all_gates();
assert!(
gates.len() <= current.len(),
"\n\nThe base branch's fixture holds {} gates but `all_gates()` now has only {}. Gates \
MUST only ever be appended to `all_gates()`, never removed or replaced.\n",
gates.len(),
current.len()
);
if let Some((index, (before, now))) = gates
.iter()
.zip(current.iter())
.enumerate()
.find(|(_, (before, now))| before != now)
{
panic!(
"\n\nGate {index} of the base branch's fixture decodes to a different value than \
`all_gates()[{index}]`, so the wire format changed:\n base: {before:?}\n now: \
{now:?}\nA `Gate`/op variant was reordered, inserted or had a field changed. New \
variants MUST be appended, never inserted. If the change is intentional, see the \
module docs for the bump steps.\n"
);
}
}