fin-proof: the two cost fixes, the sha2 precompile for the fold's hashes and the certificate path over validated signer points (no square root, no subgroup check per signer)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 08:39:12 +00:00
parent 09f08ed2e0
commit 6c5bb87268
6 changed files with 106 additions and 17 deletions

View file

@ -5230,8 +5230,7 @@ checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
source = "git+https://github.com/sp1-patches/RustCrypto-hashes?tag=patch-sha2-0.10.9-sp1-6.2.0#e48b656ebc806117554bb33c2f8687e4637e37ff"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",

View file

@ -42,3 +42,6 @@ k256 = { git = "https://github.com/sp1-patches/elliptic-curves", tag = "patch-k2
# Finality in the proof (7 October 2026): BLS12-381 field operations on the precompiles for the certificate check
# in the aggregator guest; natively the same crate's own arithmetic.
bls12_381 = { git = "https://github.com/sp1-patches/bls12_381", tag = "patch-0.8.0-sp1-6.2.0" }
# The SHA-256 precompile for the key table, ring and history hashes of the finality fold (and the hash-to-curve's
# expand_message inside bls12_381); natively the upstream code, the same bytes.
sha2 = { git = "https://github.com/sp1-patches/RustCrypto-hashes", package = "sha2", tag = "patch-sha2-0.10.9-sp1-6.2.0" }

View file

@ -5,10 +5,19 @@
use crate::{PUBKEY_LEN, SIG_LEN};
pub const POINT_LEN: usize = 96;
pub trait Bls {
/// `fast_aggregate_verify`: the aggregate of `pubkeys` signed `msg` under `dst` with `sig`.
/// `fast_aggregate_verify`: the aggregate of `pubkeys` signed `msg` under `dst` with `sig`. Every key is
/// decompressed in full (a square root and a subgroup check per key): the reveal path.
fn verify_aggregate(&self, pubkeys: &[[u8; PUBKEY_LEN]], msg: &[u8], dst: &[u8], sig: &[u8; SIG_LEN]) -> bool;
/// The certificate path (docs/design/finality-in-proof.md 6.1, the second fix): every signer's key arrives
/// uncompressed (`points`, 96 bytes each) beside its committed compressed form (`pubkeys`); the point must lie
/// on the curve and compress to the committed bytes, which pins it to the key the reveal validated (the
/// x-coordinate and the sign of y name one curve point), so no square root and no subgroup check per signer.
fn verify_aggregate_points(&self, points: &[[u8; POINT_LEN]], pubkeys: &[[u8; PUBKEY_LEN]], msg: &[u8], dst: &[u8], sig: &[u8; SIG_LEN]) -> bool;
fn verify_one(&self, pubkey: &[u8; PUBKEY_LEN], msg: &[u8], dst: &[u8], sig: &[u8; SIG_LEN]) -> bool {
self.verify_aggregate(std::slice::from_ref(pubkey), msg, dst, sig)
}
@ -21,9 +30,7 @@ pub struct ZkBls;
#[cfg(feature = "zk-bls")]
impl Bls for ZkBls {
fn verify_aggregate(&self, pubkeys: &[[u8; PUBKEY_LEN]], msg: &[u8], dst: &[u8], sig: &[u8; SIG_LEN]) -> bool {
use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve};
use bls12_381::{multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt};
use group::Curve;
use bls12_381::{G1Affine, G1Projective, G2Affine};
if pubkeys.is_empty() {
return false;
}
@ -44,10 +51,42 @@ impl Bls for ZkBls {
return false;
}
let s = s.unwrap();
let hm: G2Projective = <G2Projective as HashToCurve<ExpandMsgXmd<sha2::Sha256>>>::hash_to_curve(&[msg], dst);
let agg_affine = agg.to_affine();
let neg_g1 = -G1Affine::generator();
let r = multi_miller_loop(&[(&agg_affine, &G2Prepared::from(hm.to_affine())), (&neg_g1, &G2Prepared::from(s))]).final_exponentiation();
r == Gt::identity()
zk_pair(agg, msg, dst, s)
}
fn verify_aggregate_points(&self, points: &[[u8; POINT_LEN]], pubkeys: &[[u8; PUBKEY_LEN]], msg: &[u8], dst: &[u8], sig: &[u8; SIG_LEN]) -> bool {
use bls12_381::{G1Affine, G1Projective, G2Affine};
if points.is_empty() || points.len() != pubkeys.len() {
return false;
}
let mut agg = G1Projective::identity();
for (pt, pk) in points.iter().zip(pubkeys) {
let p = G1Affine::from_uncompressed_unchecked(pt);
if p.is_none().into() {
return false;
}
let p = p.unwrap();
if !bool::from(p.is_on_curve()) || p.is_identity().into() || p.to_compressed() != *pk {
return false;
}
agg += G1Projective::from(p);
}
let s = G2Affine::from_compressed(sig);
if s.is_none().into() {
return false;
}
zk_pair(agg, msg, dst, s.unwrap())
}
}
#[cfg(feature = "zk-bls")]
fn zk_pair(agg: bls12_381::G1Projective, msg: &[u8], dst: &[u8], s: bls12_381::G2Affine) -> bool {
use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve};
use bls12_381::{multi_miller_loop, G1Affine, G2Prepared, G2Projective, Gt};
use group::Curve;
let hm: G2Projective = <G2Projective as HashToCurve<ExpandMsgXmd<sha2::Sha256>>>::hash_to_curve(&[msg], dst);
let agg_affine = agg.to_affine();
let neg_g1 = -G1Affine::generator();
let r = multi_miller_loop(&[(&agg_affine, &G2Prepared::from(hm.to_affine())), (&neg_g1, &G2Prepared::from(s))]).final_exponentiation();
r == Gt::identity()
}

View file

@ -24,6 +24,10 @@ pub struct CertificateWitness {
pub bitmap: Vec<u8>,
#[serde(with = "crate::serde_arrays")]
pub signature: [u8; SIG_LEN],
/// Every signer's key uncompressed (96 bytes), in bitmap order: the certificate path takes no square root and
/// no subgroup check per signer (design 6.1, the second fix); each must compress to the table's key.
#[serde(default)]
pub signer_points: Vec<Vec<u8>>,
/// The table at the checkpoint block.
pub at: TableAt,
/// The table at the previous lock's block, when a lock exists.
@ -103,7 +107,21 @@ pub fn verify_certificate(state: &mut FinState, params: &FinParams, c: &Certific
pubkeys.push(k.pubkey);
signer_hashes.push(k.key_hash);
}
if !bls.verify_aggregate(&pubkeys, &vote_message(&params.chain_id, c.index, &c.checkpoint), crate::DST_VOTE, &c.signature) {
let msg = vote_message(&params.chain_id, c.index, &c.checkpoint);
let sig_ok = if c.signer_points.is_empty() {
bls.verify_aggregate(&pubkeys, &msg, crate::DST_VOTE, &c.signature)
} else {
if c.signer_points.len() != pubkeys.len() {
return Err("the certificate carries a point count other than its signer count".into());
}
let mut points = Vec::with_capacity(pubkeys.len());
for p in &c.signer_points {
let a: [u8; crate::bls::POINT_LEN] = p.as_slice().try_into().map_err(|_| "a signer point is not 96 bytes")?;
points.push(a);
}
bls.verify_aggregate_points(&points, &pubkeys, &msg, crate::DST_VOTE, &c.signature)
};
if !sig_ok {
return Err("the certificate's aggregate signature does not verify".into());
}
if total == 0 || !FinParams::floor_met(signed, total) {

View file

@ -49,6 +49,20 @@ impl Bls for BlstBls {
let refs: Vec<&blst::min_pk::PublicKey> = pks.iter().collect();
sig.fast_aggregate_verify(true, msg, dst, &refs) == blst::BLST_ERROR::BLST_SUCCESS
}
fn verify_aggregate_points(&self, points: &[[u8; 96]], pubkeys: &[[u8; PUBKEY_LEN]], msg: &[u8], dst: &[u8], sig: &[u8; SIG_LEN]) -> bool {
let mut pks = Vec::with_capacity(points.len());
for (p, want) in points.iter().zip(pubkeys) {
let Ok(pk) = blst::min_pk::PublicKey::deserialize(p) else { return false };
if pk.compress() != *want {
return false;
}
pks.push(pk);
}
let Some(sig) = Signature::from_bytes(sig).ok() else { return false };
let refs: Vec<&blst::min_pk::PublicKey> = pks.iter().collect();
sig.fast_aggregate_verify(true, msg, dst, &refs) == blst::BLST_ERROR::BLST_SUCCESS
}
}
/// The simulated chain: one chain block per DAA second, every block blue, keys mine in proportion to `share`.
@ -157,7 +171,10 @@ impl Sim {
bitmap[p / 8] |= 1 << (p % 8);
}
let frozen = (self.state.lock.index > 0).then(|| self.table_at(self.state.lock.number));
CertificateWitness { index, checkpoint, voter_count: voters.len() as u32, bitmap, signature: agg, at: self.table_at(number), frozen }
let mut order: Vec<usize> = signers.to_vec();
order.sort_by_key(|&s| voters.iter().position(|&v| table[v].key_hash == self.keys[s].hash).unwrap());
let signer_points: Vec<Vec<u8>> = order.iter().map(|&s| self.keys[s].sk.sk_to_pk().serialize().to_vec()).collect();
CertificateWitness { index, checkpoint, voter_count: voters.len() as u32, bitmap, signature: agg, signer_points, at: self.table_at(number), frozen }
}
}
@ -264,12 +281,12 @@ fn known_failed_first_todays_light_client_takes_the_voter_list_from_a_node_and_t
// refused too (e holds nothing there and is no voter: the bitmap names a position outside the list, or e's
// weight is zero).
let forged_at = TableAt { leaf: sim.leaves[&cp].clone(), proof: sim.mmr.proof(cp).unwrap(), keys: forged.clone() };
let cert = CertificateWitness { index: 9, checkpoint, voter_count: forged_voters.len() as u32, bitmap: bitmap.clone(), signature: sig, at: forged_at, frozen: None };
let cert = CertificateWitness { index: 9, checkpoint, voter_count: forged_voters.len() as u32, bitmap: bitmap.clone(), signature: sig, signer_points: Vec::new(), at: forged_at, frozen: None };
let mut w = FoldWitness::default();
w.certificates.push(cert);
let err = sim.mine(1, &[], w).unwrap_err();
assert!(err.contains("not the one the proof committed"), "the forged table is refused: {err}");
let cert_real = CertificateWitness { index: 9, checkpoint, voter_count: forged_voters.len() as u32, bitmap, signature: sig, at: sim.table_at(cp), frozen: None };
let cert_real = CertificateWitness { index: 9, checkpoint, voter_count: forged_voters.len() as u32, bitmap, signature: sig, signer_points: Vec::new(), at: sim.table_at(cp), frozen: None };
let mut w = FoldWitness::default();
w.certificates.push(cert_real);
let err = sim.mine(1, &[], w).unwrap_err();
@ -455,6 +472,15 @@ fn the_guest_curve_and_blst_agree_on_real_certificates_and_disagree_with_nothing
assert!(!ZkBls.verify_aggregate(&pks, &msg, DST_VOTE, &bad));
let other = vote_message(&sim.params.chain_id, 10, &cert.checkpoint);
assert!(!ZkBls.verify_aggregate(&pks, &other, DST_VOTE, &cert.signature));
// the certificate path with uncompressed points, both curves; a point that compresses to another key refused
let pts: Vec<[u8; 96]> = [0usize, 1].iter().map(|&i| sim.keys[i].sk.sk_to_pk().serialize()).collect();
assert!(ZkBls.verify_aggregate_points(&pts, &pks, &msg, DST_VOTE, &cert.signature));
assert!(BlstBls.verify_aggregate_points(&pts, &pks, &msg, DST_VOTE, &cert.signature));
let swapped = vec![pts[1], pts[0]];
assert!(!ZkBls.verify_aggregate_points(&swapped, &pks, &msg, DST_VOTE, &cert.signature), "a point must compress to its own table key");
let mut off = pts.clone();
off[0][95] ^= 1;
assert!(!ZkBls.verify_aggregate_points(&off, &pks, &msg, DST_VOTE, &cert.signature), "a point off the curve is refused");
// a proof of possession
let r = reveal(&sim.keys[0]);
assert!(ZkBls.verify_one(&r.pubkey, &r.pubkey, DST_POP, &r.pop));

View file

@ -230,12 +230,16 @@ pub fn synth(fixtures: &[Fixture], keys: usize, voters: usize, window_blocks: u6
let refs: Vec<&blst::min_pk::Signature> = sigs.iter().collect();
let agg = blst::min_pk::AggregateSignature::aggregate(&refs, true).unwrap().to_signature().compress();
let mut bitmap = vec![0u8; vlist.len().div_ceil(8)];
for p in positions {
for p in &positions {
bitmap[p / 8] |= 1 << (p % 8);
}
// the signers' uncompressed points in bitmap order
let mut by_pos: Vec<(usize, &usize)> = positions.iter().zip(heavy.iter()).map(|(p, v)| (*p, v)).collect();
by_pos.sort();
let signer_points: Vec<Vec<u8>> = by_pos.iter().map(|(_, v)| { let kh = cp_keys[**v].key_hash; synth_keys.iter().find(|k| k.hash == kh).unwrap().sk.sk_to_pk().serialize().to_vec() }).collect();
let position = cp_number - root_state.history_first;
let proof = mmr.proof(position).ok_or_else(|| anyhow!("no history proof for {cp_number}"))?;
w.certificates.push(CertificateWitness { index, checkpoint: cp_leaf.block_hash, voter_count: vlist.len() as u32, bitmap, signature: agg, at: TableAt { leaf: cp_leaf, proof, keys: cp_keys }, frozen: None });
w.certificates.push(CertificateWitness { index, checkpoint: cp_leaf.block_hash, voter_count: vlist.len() as u32, bitmap, signature: agg, signer_points, at: TableAt { leaf: cp_leaf, proof, keys: cp_keys }, frozen: None });
}
fold_block(&mut state, &params, &block, &mut ring, &w, &bls).map_err(|e| anyhow!("fold {n}: {e}"))?;
let (_, total) = voters_at(&state.keys, daa, params.dust);