Skip to main content

binius_circuits/hash_based_sig/
aggregate.rs

1// Copyright 2026 The Binius Developers
2//! Aggregation of XMSS signatures on a common message.
3//!
4//! Each signer has an independent tree, so a public key is a `(root, public parameter)` pair and
5//! both are per-signer. All signers sign the same message at the same epoch, and one proof stands
6//! for every signature.
7//!
8//! The same aggregate is built two ways over one set of wires: [`circuit_xmss_multisig`] emits
9//! every signer's verification inline, and [`circuit_xmss_multisig_chip`] states it once as an M4
10//! chip and calls that chip per signer.
11//!
12//! Both publish every signer's key, so their statement grows with the number of signers.
13//!
14//! A third form publishes one commitment to the whole set instead.
15//! That is what a statement has to look like before an aggregate can be a step in a tree.
16
17use std::iter;
18
19use binius_core::Word;
20use binius_frontend::{CircuitBuilder, CircuitM4, Wire, WitnessFiller};
21
22use super::{
23	DIGEST_LEN, DIGEST_WIRES, MESSAGE_WIRES, Message, PUBLIC_PARAM_LEN, PUBLIC_PARAM_WIRES,
24	xmss::{XmssPublicKey, XmssSignature, XmssSignatureWires, circuit_xmss_verify},
25};
26use crate::{
27	bignum::{BigUint, biguint_lt},
28	blake3::{Blake3Compress2x, blake3_fixed},
29	util::split_u32_words,
30};
31
32/// One signer's public key and signature wires.
33#[derive(Debug, Clone)]
34pub struct SignerWires {
35	pub public_param: [Wire; PUBLIC_PARAM_WIRES],
36	pub merkle_root: [Wire; DIGEST_WIRES],
37	pub signature: XmssSignatureWires,
38}
39
40impl SignerWires {
41	/// Allocates a signer whose key is public and whose signature is private.
42	pub fn new_inout(builder: &CircuitBuilder) -> Self {
43		Self {
44			public_param: std::array::from_fn(|_| builder.add_inout()),
45			merkle_root: std::array::from_fn(|_| builder.add_inout()),
46			signature: XmssSignatureWires::new_witness(builder),
47		}
48	}
49
50	/// Allocates a signer whose key is private as well.
51	///
52	/// A key held privately reaches the statement only through a commitment to the whole set.
53	pub fn new_witness(builder: &CircuitBuilder) -> Self {
54		Self {
55			public_param: std::array::from_fn(|_| builder.add_witness()),
56			merkle_root: std::array::from_fn(|_| builder.add_witness()),
57			signature: XmssSignatureWires::new_witness(builder),
58		}
59	}
60
61	/// Fills the key and the signature.
62	fn populate(
63		&self,
64		w: &mut WitnessFiller<'_>,
65		public_key: &XmssPublicKey,
66		signature: &XmssSignature,
67	) {
68		w.pack_bytes_le(&self.public_param, &public_key.public_param);
69		w.pack_bytes_le(&self.merkle_root, &public_key.merkle_root);
70		self.signature.populate(w, signature);
71	}
72
73	/// The key read as one integer, least significant limb first.
74	///
75	/// The packing puts the key's first byte in the lowest limb's lowest byte.
76	/// So the integer is the key's bytes read little-endian end to end.
77	fn key_as_integer(&self) -> BigUint {
78		BigUint {
79			limbs: self
80				.public_param
81				.iter()
82				.chain(&self.merkle_root)
83				.copied()
84				.collect(),
85		}
86	}
87}
88
89/// The wires an aggregate verification occupies.
90///
91/// The message, the epoch and every signer's public key are public inputs; the signatures are
92/// private witnesses.
93#[derive(Debug, Clone)]
94pub struct MultiSigWires {
95	pub message: [Wire; MESSAGE_WIRES],
96	pub epoch: Wire,
97	pub signers: Vec<SignerWires>,
98}
99
100impl MultiSigWires {
101	/// Allocates the wires for `num_signers` signers.
102	pub fn new(builder: &CircuitBuilder, num_signers: usize) -> Self {
103		Self {
104			message: std::array::from_fn(|_| builder.add_inout()),
105			epoch: builder.add_inout(),
106			signers: (0..num_signers)
107				.map(|_| SignerWires::new_inout(builder))
108				.collect(),
109		}
110	}
111
112	/// Populates every wire from a message, an epoch and one key-and-signature pair per signer.
113	///
114	/// # Panics
115	///
116	/// If the number of pairs is not the number of signers the wires were allocated for.
117	pub fn populate(
118		&self,
119		w: &mut WitnessFiller<'_>,
120		message: &Message,
121		epoch: u32,
122		signatures: &[(XmssPublicKey, XmssSignature)],
123	) {
124		assert_eq!(
125			signatures.len(),
126			self.signers.len(),
127			"expected {} signatures, got {}",
128			self.signers.len(),
129			signatures.len()
130		);
131
132		w.pack_bytes_le(&self.message, message);
133		w[self.epoch] = Word::from_u64(epoch as u64);
134		for (wires, (public_key, signature)) in iter::zip(&self.signers, signatures) {
135			wires.populate(w, public_key, signature);
136		}
137	}
138}
139
140/// Verifies every signer's XMSS signature on the common message at the common epoch.
141///
142/// Sharing one epoch wire across the signers is what makes the epoch common: there is no
143/// per-signer epoch to disagree with.
144pub fn circuit_xmss_multisig(builder: &CircuitBuilder, wires: &MultiSigWires) {
145	for (i, signer) in wires.signers.iter().enumerate() {
146		let builder = builder.subcircuit(format!("signer[{i}]"));
147		circuit_xmss_verify(
148			&builder,
149			&signer.public_param,
150			&signer.merkle_root,
151			&wires.message,
152			wires.epoch,
153			&signer.signature,
154		);
155	}
156}
157
158/// Bytes one signer contributes to a signer-set commitment.
159pub const SIGNER_BYTES: usize = PUBLIC_PARAM_LEN + DIGEST_LEN;
160
161/// Wires holding a signer-set commitment.
162///
163/// Eight words, each holding a little-endian 32-bit half.
164/// That is the form the hash gadget returns.
165pub const SIGNER_SET_WIRES: usize = 8;
166
167/// The order signers are declared in.
168///
169/// Only totality matters here, not which order it is.
170///
171/// This one is the order the wire packing already induces.
172/// So nothing in circuit has to rearrange a key to compare it.
173///
174/// A key's bytes read as one little-endian integer.
175/// An integer comparison is therefore a comparison of the reversed byte string.
176pub fn signer_order_key(public_key: &XmssPublicKey) -> [u8; SIGNER_BYTES] {
177	let mut bytes = [0u8; SIGNER_BYTES];
178	bytes[..PUBLIC_PARAM_LEN].copy_from_slice(&public_key.public_param);
179	bytes[PUBLIC_PARAM_LEN..].copy_from_slice(&public_key.merkle_root);
180	bytes.reverse();
181	bytes
182}
183
184/// Puts signatures into the declared order.
185pub fn sort_by_signer(signatures: &mut [(XmssPublicKey, XmssSignature)]) {
186	signatures.sort_unstable_by_key(|(public_key, _)| signer_order_key(public_key));
187}
188
189/// The commitment to a declared signer set.
190///
191/// The keys are hashed in the order given, so this binds an order as well as a set.
192/// Requiring that order to be strictly increasing is what makes the two the same thing.
193///
194/// The digest is the full 256 bits rather than the scheme's truncated 128.
195///
196/// A signature needs its hashes to make a *chosen* target hard to collide.
197/// 128 bits carry that.
198///
199/// A set commitment needs *any* two sets to be hard to collide.
200/// 128 bits leave that at 64.
201pub fn signer_set_digest(public_keys: &[XmssPublicKey]) -> [u8; 32] {
202	// The layout is the wire layout: each signer's parameter, then its root, in declared order.
203	let mut bytes = Vec::with_capacity(public_keys.len() * SIGNER_BYTES);
204	for public_key in public_keys {
205		bytes.extend_from_slice(&public_key.public_param);
206		bytes.extend_from_slice(&public_key.merkle_root);
207	}
208	*blake3::hash(&bytes).as_bytes()
209}
210
211/// The wires a committed aggregate occupies.
212///
213/// The message, the epoch and one commitment to the signer set are public.
214/// Every key and every signature is private.
215///
216/// So the statement is the same size whatever the number of signers.
217#[derive(Debug, Clone)]
218pub struct CommittedMultiSigWires {
219	pub message: [Wire; MESSAGE_WIRES],
220	pub epoch: Wire,
221	pub signer_set: [Wire; SIGNER_SET_WIRES],
222	pub signers: Vec<SignerWires>,
223}
224
225impl CommittedMultiSigWires {
226	/// Allocates the wires for `num_signers` signers.
227	///
228	/// # Panics
229	///
230	/// If there are no signers.
231	/// A commitment to nothing states nothing.
232	/// So an empty aggregate is a mistake, not a degenerate case worth supporting.
233	pub fn new(builder: &CircuitBuilder, num_signers: usize) -> Self {
234		assert!(num_signers > 0, "an aggregate needs at least one signer");
235		Self {
236			message: std::array::from_fn(|_| builder.add_inout()),
237			epoch: builder.add_inout(),
238			signer_set: std::array::from_fn(|_| builder.add_inout()),
239			signers: (0..num_signers)
240				.map(|_| SignerWires::new_witness(builder))
241				.collect(),
242		}
243	}
244
245	/// Populates every wire from a message, an epoch and one key-and-signature pair per signer.
246	///
247	/// The commitment is computed over the pairs in the order given.
248	/// Nothing is sorted here.
249	///
250	/// So a caller that has not sorted gets a statement its own circuit rejects.
251	/// It never gets a statement about a set it did not mean.
252	///
253	/// # Panics
254	///
255	/// If the number of pairs is not the number of signers the wires were allocated for.
256	pub fn populate(
257		&self,
258		w: &mut WitnessFiller<'_>,
259		message: &Message,
260		epoch: u32,
261		signatures: &[(XmssPublicKey, XmssSignature)],
262	) {
263		assert_eq!(
264			signatures.len(),
265			self.signers.len(),
266			"expected {} signatures, got {}",
267			self.signers.len(),
268			signatures.len()
269		);
270
271		w.pack_bytes_le(&self.message, message);
272		w[self.epoch] = Word::from_u64(epoch as u64);
273		for (wires, (public_key, signature)) in iter::zip(&self.signers, signatures) {
274			wires.populate(w, public_key, signature);
275		}
276
277		// The commitment goes in as the hash gadget reports it.
278		// One little-endian 32-bit word per wire, in the wire's low half.
279		let public_keys: Vec<_> = signatures
280			.iter()
281			.map(|&(public_key, _)| public_key)
282			.collect();
283		let digest = signer_set_digest(&public_keys);
284		for (&wire, chunk) in iter::zip(&self.signer_set, digest.chunks_exact(4)) {
285			let word = u32::from_le_bytes(chunk.try_into().expect("a chunk is four bytes"));
286			w[wire] = Word::from_u64(word as u64);
287		}
288	}
289}
290
291/// The aggregate verification against a committed signer set rather than a published one.
292///
293/// Three things are checked, and all three are needed:
294///
295/// - every signature verifies, as in the published form,
296/// - the keys strictly increase, so no two signers are the same signer,
297/// - they hash to the published commitment.
298///
299/// # Why all three
300///
301/// Verification alone counts signatures, not signers: one key repeated across every slot passes it.
302///
303/// The commitment alone fixes a sequence, not a set.
304/// One set of keys in two orders gives two commitments, and so does one key repeated.
305///
306/// A verifier holding the set would not know which of them to expect.
307///
308/// Strict increase settles both at once.
309/// A repeat is not an increase, so repeats are out.
310/// One order survives per set, so the commitment commits to the set.
311pub fn circuit_xmss_multisig_committed(builder: &CircuitBuilder, wires: &CommittedMultiSigWires) {
312	// Every signer's key contributes its bytes to the commitment, in declared order.
313	let mut payload = Vec::with_capacity(wires.signers.len() * (PUBLIC_PARAM_WIRES + DIGEST_WIRES));
314
315	for (i, signer) in wires.signers.iter().enumerate() {
316		let signer_builder = builder.subcircuit(format!("signer[{i}]"));
317		circuit_xmss_verify(
318			&signer_builder,
319			&signer.public_param,
320			&signer.merkle_root,
321			&wires.message,
322			wires.epoch,
323			&signer.signature,
324		);
325
326		// Each neighbouring pair is compared once.
327		// The chain of those comparisons orders the whole list.
328		if i > 0 {
329			let previous = &wires.signers[i - 1];
330			let increases =
331				biguint_lt(&signer_builder, &previous.key_as_integer(), &signer.key_as_integer());
332			signer_builder.assert_true("signer keys strictly increase", increases);
333		}
334
335		payload.extend_from_slice(&signer.public_param);
336		payload.extend_from_slice(&signer.merkle_root);
337	}
338
339	// The hash gadget reads 32-bit words and a signer's key is 64-bit wires.
340	// So the payload is split in half word by word.
341	let len_bytes = wires.signers.len() * SIGNER_BYTES;
342	let message = split_u32_words(builder, &payload, len_bytes / 4);
343	let digest = blake3_fixed(builder, &message, len_bytes);
344
345	builder.assert_eq_v("signer set commitment", digest, wires.signer_set);
346}
347
348/// The words one call to the XMSS chip passes, in the chip's inout order.
349///
350/// [`xmss_verify_chip`] allocates its interface in this same order, so a call's operands line up
351/// with the chip's inout segment position by position.
352fn chip_call_words(
353	signer: &SignerWires,
354	message: &[Wire; MESSAGE_WIRES],
355	epoch: Wire,
356) -> Vec<Wire> {
357	let XmssSignatureWires {
358		randomness,
359		chain_tips,
360		merkle_path,
361	} = &signer.signature;
362	signer
363		.public_param
364		.iter()
365		.chain(&signer.merkle_root)
366		.chain(message)
367		.chain(iter::once(&epoch))
368		.chain(randomness)
369		.chain(chain_tips.iter().flatten())
370		.chain(merkle_path.iter().flatten())
371		.copied()
372		.collect()
373}
374
375/// One XMSS verification, as a system of its own for a caller to embed as a chip.
376///
377/// Every word the check relates is an inout word, so a call supplies the signer's public key, the
378/// message, the epoch and the whole signature, and the chip derives the rest. There is nothing for
379/// the caller to compute on the chip's behalf: the chip has no outputs, only the constraints that
380/// make its operands a valid signature.
381///
382/// The system registers [`Blake3Compress2x`] for itself, so the paired chain compressions inside it
383/// are calls to a chip of its own. Embedding this system therefore brings in two chips: this one,
384/// and the compression it calls.
385fn xmss_verify_chip() -> CircuitM4 {
386	let builder = CircuitBuilder::new();
387	builder.register_chip(Blake3Compress2x, &[]);
388
389	// The inout segment is ordered by wire creation, so allocating in `chip_call_words` order is
390	// what makes the interface that order. The assertion below holds the two together.
391	let public_param = std::array::from_fn(|_| builder.add_inout());
392	let merkle_root = std::array::from_fn(|_| builder.add_inout());
393	let message: [Wire; MESSAGE_WIRES] = std::array::from_fn(|_| builder.add_inout());
394	let epoch = builder.add_inout();
395	let signature = XmssSignatureWires {
396		randomness: std::array::from_fn(|_| builder.add_inout()),
397		chain_tips: std::array::from_fn(|_| std::array::from_fn(|_| builder.add_inout())),
398		merkle_path: std::array::from_fn(|_| std::array::from_fn(|_| builder.add_inout())),
399	};
400	let signer = SignerWires {
401		public_param,
402		merkle_root,
403		signature,
404	};
405
406	circuit_xmss_verify(
407		&builder,
408		&signer.public_param,
409		&signer.merkle_root,
410		&message,
411		epoch,
412		&signer.signature,
413	);
414
415	let system = builder.build_m4();
416	assert_eq!(
417		system.main.circuit.inout(),
418		chip_call_words(&signer, &message, epoch),
419		"the chip's inout segment must be the order its call sites pass"
420	);
421	system
422}
423
424/// [`circuit_xmss_multisig`] with each signer's verification dispatched to a chip.
425///
426/// The main circuit is left holding no hash gates at all: it declares the statement, witnesses the
427/// signatures, and passes both to one chip call per signer. Every signer's verification is the same
428/// relation over different words, which is what a chip is for — the constraints are stated once and
429/// the trace pays for them once per instance.
430///
431/// The wires are [`MultiSigWires`] unchanged, so [`MultiSigWires::populate`] fills this circuit as
432/// it does the inline one. The built circuit carries chips, though, so it builds with
433/// [`CircuitBuilder::build_m4`] rather than [`CircuitBuilder::build`].
434pub fn circuit_xmss_multisig_chip(builder: &CircuitBuilder, wires: &MultiSigWires) {
435	let chip = builder.add_chip(xmss_verify_chip());
436	for signer in &wires.signers {
437		builder.call_chip(chip, &chip_call_words(signer, &wires.message, wires.epoch));
438	}
439}
440
441#[cfg(test)]
442mod tests {
443	use rand::{Rng, SeedableRng, rngs::StdRng};
444
445	use super::*;
446	use crate::hash_based_sig::{MESSAGE_LEN, xmss::generate_signature};
447
448	/// Generates `num_signers` independent signatures on one message at one epoch.
449	fn generate(
450		seed: u64,
451		num_signers: usize,
452		epoch: u32,
453	) -> (Message, Vec<(XmssPublicKey, XmssSignature)>) {
454		let mut rng = StdRng::seed_from_u64(seed);
455		let mut message = [0u8; MESSAGE_LEN];
456		rng.fill_bytes(&mut message);
457		let signatures = (0..num_signers)
458			.map(|_| generate_signature(&mut rng, &message, epoch))
459			.collect();
460		(message, signatures)
461	}
462
463	fn run(
464		message: &Message,
465		epoch: u32,
466		signatures: &[(XmssPublicKey, XmssSignature)],
467	) -> Result<(), String> {
468		let b = CircuitBuilder::new();
469		let wires = MultiSigWires::new(&b, signatures.len());
470		circuit_xmss_multisig(&b, &wires);
471
472		let circuit = b.build();
473		let mut w = circuit.new_witness_filler();
474		wires.populate(&mut w, message, epoch, signatures);
475
476		circuit
477			.populate_wire_witness(&mut w)
478			.map_err(|e| format!("populate: {e:?}"))?;
479		circuit
480			.constraint_system()
481			.verify(&w.into_value_vec())
482			.map_err(|e| format!("verify: {e:?}"))
483	}
484
485	/// [`run`] over the chip-dispatching circuit, checking the chips as well as the main circuit.
486	///
487	/// The main circuit populating is only half of it: a call whose operands its chip's instance
488	/// does not reproduce shows up in [`WitnessM4::verify`](binius_core::m4::WitnessM4::verify)
489	/// alone, which is what the final check here covers.
490	fn run_chip(
491		message: &Message,
492		epoch: u32,
493		signatures: &[(XmssPublicKey, XmssSignature)],
494	) -> Result<(), String> {
495		let b = CircuitBuilder::new();
496		let wires = MultiSigWires::new(&b, signatures.len());
497		circuit_xmss_multisig_chip(&b, &wires);
498
499		let circuit = b.build_m4();
500		circuit.validate().map_err(|e| format!("validate: {e:?}"))?;
501		let cs = circuit.to_constraint_system();
502		cs.validate().map_err(|e| format!("validate cs: {e:?}"))?;
503
504		let witness = circuit
505			.generate_witness(|w| wires.populate(w, message, epoch, signatures))
506			.map_err(|e| format!("populate: {e:?}"))?;
507		witness.verify(&cs).map_err(|e| format!("verify: {e:?}"))
508	}
509
510	/// Runs the committed aggregate, letting a caller disturb the witness before it is checked.
511	///
512	/// The commitment is a public input.
513	/// So a test that wants a wrong one overwrites its wires after populating.
514	fn run_committed(
515		message: &Message,
516		epoch: u32,
517		signatures: &[(XmssPublicKey, XmssSignature)],
518		disturb: impl FnOnce(&mut WitnessFiller<'_>, &CommittedMultiSigWires),
519	) -> Result<(), String> {
520		let b = CircuitBuilder::new();
521		let wires = CommittedMultiSigWires::new(&b, signatures.len());
522		circuit_xmss_multisig_committed(&b, &wires);
523
524		let circuit = b.build();
525		let mut w = circuit.new_witness_filler();
526		wires.populate(&mut w, message, epoch, signatures);
527		disturb(&mut w, &wires);
528
529		circuit
530			.populate_wire_witness(&mut w)
531			.map_err(|e| format!("populate: {e:?}"))?;
532		circuit
533			.constraint_system()
534			.verify(&w.into_value_vec())
535			.map_err(|e| format!("verify: {e:?}"))
536	}
537
538	/// Signatures in declared order, which is what an honest caller supplies.
539	fn generate_sorted(
540		seed: u64,
541		num_signers: usize,
542		epoch: u32,
543	) -> (Message, Vec<(XmssPublicKey, XmssSignature)>) {
544		let (message, mut signatures) = generate(seed, num_signers, epoch);
545		sort_by_signer(&mut signatures);
546		(message, signatures)
547	}
548
549	#[test]
550	fn a_committed_aggregate_verifies() {
551		let (message, signatures) = generate_sorted(1, 3, 42);
552		run_committed(&message, 42, &signatures, |_, _| ()).unwrap();
553	}
554
555	#[test]
556	fn the_statement_is_the_same_size_whatever_the_number_of_signers() {
557		// Invariant: the public segment holds the message, the epoch and the commitment.
558		// Nothing in it is per-signer.
559		//
560		//     published:  message (4) + epoch (1) + commitment (8) = 13 words
561		//     witnessed:  every key, every signature
562		let published = |num_signers| {
563			let b = CircuitBuilder::new();
564			let wires = CommittedMultiSigWires::new(&b, num_signers);
565			circuit_xmss_multisig_committed(&b, &wires);
566			b.build().constraint_system().n_inout
567		};
568		assert_eq!(published(1), published(8));
569	}
570
571	#[test]
572	fn the_published_form_grows_with_the_signers_and_this_one_does_not() {
573		// The defect this aggregate exists to fix, stated as a comparison.
574		let published_inout = |num_signers| {
575			let b = CircuitBuilder::new();
576			let wires = MultiSigWires::new(&b, num_signers);
577			circuit_xmss_multisig(&b, &wires);
578			b.build().constraint_system().n_inout
579		};
580		assert!(published_inout(8) > published_inout(1));
581	}
582
583	#[test]
584	fn a_repeated_signer_fails_the_committed_aggregate() {
585		// Invariant: a set of three signers means three *distinct* signers.
586		//
587		// Mutation: give slot 1 slot 0's key and signature.
588		//
589		//     keys:  [k0, k0, k2]
590		//     -> the pair (0, 1) is not an increase
591		//
592		// Both slots verify, since each carries its own valid signature.
593		// So verification alone does not catch this.
594		let (message, mut signatures) = generate_sorted(5, 3, 42);
595		signatures[1] = signatures[0].clone();
596		let err = run_committed(&message, 42, &signatures, |_, _| ())
597			.expect_err("a repeated signer must be rejected");
598		assert!(err.contains("strictly increase"), "unexpected failure: {err}");
599	}
600
601	#[test]
602	fn signers_out_of_order_fail_the_committed_aggregate() {
603		// Invariant: one order per set, so a verifier holding the set knows what to recompute.
604		//
605		// Mutation: swap two neighbours of an otherwise sorted list.
606		//
607		// The commitment still matches the witness, because it is computed over the order given.
608		// What fails is the ordering check.
609		let (message, mut signatures) = generate_sorted(6, 3, 42);
610		signatures.swap(0, 1);
611		let err = run_committed(&message, 42, &signatures, |_, _| ())
612			.expect_err("an unsorted signer list must be rejected");
613		assert!(err.contains("strictly increase"), "unexpected failure: {err}");
614	}
615
616	#[test]
617	fn a_wrong_commitment_fails_the_committed_aggregate() {
618		// Invariant: the published commitment is the one the witnessed keys hash to.
619		//
620		// Mutation: flip one bit of the published commitment, leaving every signature valid.
621		let (message, signatures) = generate_sorted(7, 2, 42);
622		let err = run_committed(&message, 42, &signatures, |w, wires| {
623			w[wires.signer_set[0]] = Word(w[wires.signer_set[0]].0 ^ 1);
624		})
625		.expect_err("a commitment the keys do not hash to must be rejected");
626		assert!(err.contains("commitment"), "unexpected failure: {err}");
627	}
628
629	#[test]
630	fn a_bad_signature_fails_the_committed_aggregate() {
631		// A private key is still a key that has to sign.
632		let (message, mut signatures) = generate_sorted(8, 3, 42);
633		signatures[1].1.chain_tips[0][0] ^= 0xFF;
634		assert!(run_committed(&message, 42, &signatures, |_, _| ()).is_err());
635	}
636
637	#[test]
638	fn the_commitment_is_over_the_keys_and_nothing_else() {
639		// Invariant: the native commitment is what the circuit recomputes.
640		// So a caller can predict the statement without building a circuit.
641		//
642		// Two sets differing in one byte of one key must commit differently.
643		// The same set must commit the same way twice.
644		let (_, signatures) = generate_sorted(9, 3, 42);
645		let keys: Vec<_> = signatures.iter().map(|&(k, _)| k).collect();
646
647		let mut other = keys.clone();
648		other[2].merkle_root[0] ^= 1;
649
650		assert_eq!(signer_set_digest(&keys), signer_set_digest(&keys));
651		assert_ne!(signer_set_digest(&keys), signer_set_digest(&other));
652	}
653
654	#[test]
655	fn the_declared_order_is_the_order_the_circuit_compares_in() {
656		// The native ordering and the in-circuit comparison have to agree.
657		// Otherwise an honestly sorted caller builds a statement its own circuit rejects.
658		//
659		// Sorting natively and running the circuit is the check.
660		// The ordering assertions pass only if the two orders are the same order.
661		let (message, signatures) = generate_sorted(10, 4, 42);
662		let keys: Vec<_> = signatures.iter().map(|&(k, _)| k).collect();
663
664		// The native order is strictly increasing, so no two keys collide in it either.
665		for pair in keys.windows(2) {
666			assert!(signer_order_key(&pair[0]) < signer_order_key(&pair[1]));
667		}
668
669		run_committed(&message, 42, &signatures, |_, _| ()).unwrap();
670	}
671
672	#[test]
673	fn independent_signers_verify_together() {
674		let (message, signatures) = generate(1, 3, 42);
675		run(&message, 42, &signatures).unwrap();
676	}
677
678	#[test]
679	fn one_bad_signature_fails_the_aggregate() {
680		let (message, mut signatures) = generate(2, 3, 42);
681		signatures[1].1.chain_tips[0][0] ^= 0xFF;
682		assert!(run(&message, 42, &signatures).is_err());
683	}
684
685	#[test]
686	fn a_signature_on_another_message_fails_the_aggregate() {
687		// Signers share the message wires, so a signature over anything else has nowhere to hide.
688		let (message, mut signatures) = generate(3, 2, 42);
689		let mut rng = StdRng::seed_from_u64(4);
690		let mut other = [0u8; MESSAGE_LEN];
691		rng.fill_bytes(&mut other);
692		signatures[0] = generate_signature(&mut rng, &other, 42);
693		assert!(run(&message, 42, &signatures).is_err());
694	}
695
696	#[test]
697	fn independent_signers_verify_together_through_the_chip() {
698		let (message, signatures) = generate(1, 2, 42);
699		run_chip(&message, 42, &signatures).unwrap();
700	}
701
702	#[test]
703	fn one_bad_signature_fails_the_chip_aggregate() {
704		let (message, mut signatures) = generate(2, 2, 42);
705		signatures[1].1.chain_tips[0][0] ^= 0xFF;
706		assert!(run_chip(&message, 42, &signatures).is_err());
707	}
708}