From f97f2c3579f14f3f14253e4ef6a07127444688ff Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 28 Jul 2026 10:44:42 +0100 Subject: [PATCH 01/48] bls beefy verification --- Cargo.lock | 1 + modules/consensus/beefy/prover/Cargo.toml | 5 + modules/consensus/beefy/prover/src/lib.rs | 30 +- modules/consensus/beefy/prover/src/relay.rs | 85 ++++- modules/consensus/beefy/verifier/Cargo.toml | 6 + modules/consensus/beefy/verifier/src/test.rs | 368 +++++++++++++++++++ 6 files changed, 469 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf7c08673..6d6f4e841 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2943,6 +2943,7 @@ dependencies = [ "subxt-utils", "thiserror 2.0.18", "tokio", + "w3f-bls", ] [[package]] diff --git a/modules/consensus/beefy/prover/Cargo.toml b/modules/consensus/beefy/prover/Cargo.toml index 763b870ae..9e754b2ad 100644 --- a/modules/consensus/beefy/prover/Cargo.toml +++ b/modules/consensus/beefy/prover/Cargo.toml @@ -50,3 +50,8 @@ features = [ [features] local = [] +# Prove a relay chain whose BEEFY authorities use the paired (ECDSA, BLS12-381) `ecdsa_bls_crypto` +# key type. On the wire such a chain carries 177-byte paired signatures and 177-byte authority +# public keys; with this feature the prover keeps only the ECDSA half of each (the keccak-ECDSA +# recoverable signature / compressed secp256k1 key), so the existing ECDSA verifier is unchanged. +bls = [] diff --git a/modules/consensus/beefy/prover/src/lib.rs b/modules/consensus/beefy/prover/src/lib.rs index dbefd5ccc..e4ea1811b 100644 --- a/modules/consensus/beefy/prover/src/lib.rs +++ b/modules/consensus/beefy/prover/src/lib.rs @@ -202,15 +202,29 @@ impl Prover { &self, at: Option>, ) -> Result, anyhow::Error> { + let data = self + .relay_rpc + .state_get_storage(BEEFY_AUTHORITIES.as_slice(), at) + .await? + .ok_or_else(|| anyhow!("No beefy authorities found!"))?; + // Encoding and decoding to fix dependency version conflicts - let current_authorities = { - self.relay_rpc - .state_get_storage(BEEFY_AUTHORITIES.as_slice(), at) - .await? - .map(|data| Vec::<[u8; 33]>::decode(&mut data.as_ref())) - .transpose()? - .ok_or_else(|| anyhow!("No beefy authorities found!"))? - }; + #[cfg(not(feature = "bls"))] + let current_authorities = Vec::<[u8; 33]>::decode(&mut data.as_ref())?; + + // `bls`: authorities are stored as 177-byte paired (ECDSA, BLS12-381) keys; keep the ECDSA + // half (first 33 bytes, a compressed secp256k1 key) so the derived address leaves match the + // on-chain keyset commitment, which `BeefyEcdsaBlsToEthereum` builds from those ECDSA halves. + #[cfg(feature = "bls")] + let current_authorities = Vec::<[u8; 177]>::decode(&mut data.as_ref())? + .into_iter() + .map(|key| { + let mut ecdsa_half = [0u8; 33]; + ecdsa_half.copy_from_slice(&key[..33]); + ecdsa_half + }) + .collect::>(); + Ok(current_authorities) } diff --git a/modules/consensus/beefy/prover/src/relay.rs b/modules/consensus/beefy/prover/src/relay.rs index f391a9980..a6bb1cb3b 100644 --- a/modules/consensus/beefy/prover/src/relay.rs +++ b/modules/consensus/beefy/prover/src/relay.rs @@ -73,15 +73,59 @@ pub async fn fetch_latest_beefy_justification( (justfication.0 == sp_consensus_beefy::BEEFY_ENGINE_ID).then(|| justfication.1) }) .expect("Should have valid beefy justification"); - let VersionedFinalityProof::V1(signed_commitment) = VersionedFinalityProof::< - u32, - sp_consensus_beefy::ecdsa_crypto::Signature, - >::decode(&mut &*beefy_justification) - .expect("Beefy justification should decode correctly"); + let signed_commitment = decode_beefy_justification(&beefy_justification)?; Ok((signed_commitment, latest_beefy_finalized)) } +/// Decode a BEEFY justification into a `SignedCommitment` carrying 65-byte ECDSA signatures. +/// +/// On a plain-ECDSA relay the signatures decode directly. With the `bls` feature (a relay whose +/// BEEFY authorities use the paired `ecdsa_bls_crypto` key type) the on-wire signatures are +/// 177-byte paired signatures; we decode them and keep only the ECDSA half (the first 65 bytes, a +/// keccak-ECDSA recoverable signature). Both paths return the same type, so everything downstream +/// — commitment hashing, signature recovery, the ECDSA verifier — is unchanged. +pub fn decode_beefy_justification( + bytes: &[u8], +) -> Result, anyhow::Error> { + #[cfg(not(feature = "bls"))] + { + let VersionedFinalityProof::V1(signed_commitment) = + VersionedFinalityProof::::decode( + &mut &*bytes, + )?; + Ok(signed_commitment) + } + #[cfg(feature = "bls")] + { + /// A 177-byte paired (ECDSA, BLS12-381) signature exactly as SCALE-encoded on the wire. + struct Sig177([u8; 177]); + impl codec::Decode for Sig177 { + fn decode(input: &mut I) -> Result { + let mut bytes = [0u8; 177]; + input.read(&mut bytes)?; + Ok(Sig177(bytes)) + } + } + + let VersionedFinalityProof::V1(paired) = + VersionedFinalityProof::::decode(&mut &*bytes)?; + let signatures = paired + .signatures + .into_iter() + .map(|maybe_sig| { + maybe_sig + .map(|sig| { + // The ECDSA half is the first 65 bytes: r || s || v. + sp_consensus_beefy::ecdsa_crypto::Signature::decode(&mut &sig.0[..65]) + }) + .transpose() + }) + .collect::, _>>()?; + Ok(SignedCommitment { commitment: paired.commitment, signatures }) + } +} + /// Parathreads whitelisted to be added to the beefy mmr leaf parachains header root const BEEFY_WHITELISTED_PARATHREADS: &'static [u32] = &[3367]; @@ -184,11 +228,7 @@ pub async fn fetch_next_beefy_justification( if (current_set_id..=(current_set_id + 1)).contains(&set_id) && beefy_justification.is_some() { - let VersionedFinalityProof::V1(signed_commitment) = - VersionedFinalityProof::::decode( - &mut &*beefy_justification.unwrap(), - ) - .expect("Beefy justification should decode correctly"); + let signed_commitment = decode_beefy_justification(&beefy_justification.unwrap())?; break (signed_commitment, block_hash); } block_hash = SubstrateHeader::::decode(&mut &*block.block.header.encode()) @@ -258,14 +298,23 @@ pub async fn query_mmr_leaf( let leaf_extra = { let heads = paras_parachains(rpc, Some(parent_hash)).await?; - // Calculate leaf hashes from the parachain headers - let leaf_hashes = heads.iter().map(|leaf| keccak_256(&leaf.encode())).collect::>(); - - let tree = MerkleTree::::from_leaves(&leaf_hashes); - let root = tree - .root() - .ok_or_else(|| anyhow!("Failed to parachain heads calculate root!"))?; - H256(root) + if heads.is_empty() { + // A relay chain with no registered parachains commits an empty parachain-heads root. + // The runtime uses `binary_merkle_tree::merkle_root`, which returns `H::Out::default()` + // for an empty leaf set. `rs_merkle`'s empty tree has no root (`.root()` is `None`), so + // mirror the runtime here instead of erroring. + H256::default() + } else { + // Calculate leaf hashes from the parachain headers + let leaf_hashes = + heads.iter().map(|leaf| keccak_256(&leaf.encode())).collect::>(); + + let tree = MerkleTree::::from_leaves(&leaf_hashes); + let root = tree + .root() + .ok_or_else(|| anyhow!("Failed to parachain heads calculate root!"))?; + H256(root) + } }; let leaf = MmrLeaf { version: MmrLeafVersion::new(0, 0), diff --git a/modules/consensus/beefy/verifier/Cargo.toml b/modules/consensus/beefy/verifier/Cargo.toml index 0737eb95a..13347a2e0 100644 --- a/modules/consensus/beefy/verifier/Cargo.toml +++ b/modules/consensus/beefy/verifier/Cargo.toml @@ -40,6 +40,8 @@ subxt-core = { workspace = true, default-features = true } subxt-utils = { workspace = true, default-features = true } futures = { workspace = true } tokio = { workspace = true } +# Aggregate BLS verification prototype (Option B): the same double-scheme BLS as substrate BEEFY. +w3f-bls = { version = "0.1.9", default-features = true } [dev-dependencies.polkadot-sdk] workspace = true @@ -49,6 +51,10 @@ features = ["sp-io"] [features] default = ["std"] +# Runs `test_verify_consensus_bls` against a local BLS-BEEFY relay (paired ecdsa_bls_crypto +# authorities). Pulls the prover's `bls` decode path and `local` storage keys (the local relay +# names its pallet `MmrLeaf`). Enable with `--features bls` when running that test. +bls = ["beefy-prover/bls", "beefy-prover/local"] std = [ "log/std", "anyhow/std", diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index 82bb88953..5c749e0ee 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -458,3 +458,371 @@ fn rejects_sp1_proof_carrying_a_stale_mmr_leaf() { }); assert!(matches!(stale, Err(Error::StaleMmrLeaf { .. })), "got {stale:?}"); } + +/// End-to-end verification against a relay chain whose BEEFY authorities use the paired +/// (ECDSA, BLS12-381) `ecdsa_bls_crypto` key type. Requires the `beefy-prover/bls` feature, which +/// makes the prover keep the ECDSA half of each 177-byte paired signature and authority key. The +/// verifier itself is unchanged: this is "Option A" — a BLS-BEEFY relay is verified through the +/// existing ECDSA path. +/// +/// RELAY_WS_URL=ws://127.0.0.1:9977 \ +/// cargo test -p beefy-verifier test_verify_consensus_bls -- --ignored --nocapture +#[cfg(feature = "bls")] +#[tokio::test] +#[ignore] +async fn test_verify_consensus_bls() { + let max_rpc_payload_size = 15 * 1024 * 1024; + let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); + + let (relay_client, relay_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); + // Relay-only: point the "para" client at the same relay; `para_ids` is empty so no parachain + // headers are proven (our local relay has no registered parachains). + let (para_client, para_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let para_rpc = LegacyRpcMethods::::new(para_rpc_client.clone()); + + let prover = Prover { + beefy_activation_block: 0, + relay: relay_client, + relay_rpc: relay_rpc.clone(), + relay_rpc_client: relay_rpc_client.clone(), + para: para_client, + para_rpc, + para_rpc_client, + para_ids: vec![], + query_batch_size: Some(100), + }; + + let engine_id = polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID; + let latest: H256 = + relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await.unwrap(); + + // Walk back to the previous BEEFY-justified block to seed the trusted state. + let mut previous = H256::default(); + let mut cursor = latest; + for _ in 0..2000 { + let header = relay_rpc.chain_get_header(Some(cursor.into())).await.unwrap().unwrap(); + let parent: H256 = header.parent_hash.into(); + if parent.is_zero() { + panic!("reached genesis without a previous beefy block"); + } + let block = relay_rpc.chain_get_block(Some(parent.into())).await.unwrap().unwrap(); + if block.justifications.map(|js| js.iter().any(|j| j.0 == engine_id)).unwrap_or(false) { + previous = parent; + break; + } + cursor = parent; + } + assert!(!previous.is_zero(), "no previous beefy block found"); + + // Initial trusted state via the prover — exercises the folded BLS justification decode. + let trusted_state = prover.get_initial_consensus_state(Some(previous)).await.unwrap(); + + // Latest justification -> signed commitment with ECDSA-half signatures (folded BLS decode). + let latest_block = relay_rpc.chain_get_block(Some(latest.into())).await.unwrap().unwrap(); + let latest_just = latest_block + .justifications + .expect("latest beefy block must have justifications") + .into_iter() + .find_map(|j| (j.0 == engine_id).then_some(j.1)) + .expect("latest beefy block must have a beefy justification"); + let signed = beefy_prover::relay::decode_beefy_justification(&latest_just).unwrap(); + let block_number = signed.commitment.block_number; + let signed_count = signed.signatures.iter().filter(|s| s.is_some()).count(); + + let signatures = signed + .signatures + .iter() + .enumerate() + .filter_map(|(index, s)| { + s.as_ref().map(|sig| { + let slice: &[u8] = sig.as_ref(); + let signature: [u8; 65] = slice.try_into().expect("ecdsa half is 65 bytes"); + SignatureWithAuthorityIndex { index: index as u32, signature } + }) + }) + .collect::>(); + + let (mmr_leaf_proof, latest_leaf) = + fetch_mmr_proof(&prover.relay_rpc, block_number, None).await.unwrap(); + + // Folded BLS-aware authorities: the ECDSA halves of the paired keys. + let current_authorities = prover.beefy_authorities(Some(latest)).await.unwrap(); + let authority_address_hashes = + hash_authority_addresses(current_authorities.into_iter().map(|x| x.encode()).collect()) + .unwrap(); + + let authority_indices = signatures.iter().map(|x| x.index as usize).collect::>(); + let authority_tree = MerkleTree::::from_leaves(&authority_address_hashes); + let authority_proof = authority_tree.proof(&authority_indices).proof_hashes().to_vec(); + + let signed_commitment = SignedCommitment { commitment: signed.commitment.clone(), signatures }; + let mmr = MmrProof { + signed_commitment, + latest_mmr_leaf: latest_leaf.clone(), + mmr_proof: mmr_leaf_proof, + authority_proof, + }; + // Relay-only: `verify_parachain_headers` short-circuits to `Ok(vec![])` on empty parachains. + let parachain_proof = ParachainProof { parachains: vec![], proof: vec![], total_leaves: 0 }; + let consensus_proof = ConsensusMessage { mmr, parachain: parachain_proof }; + + let result = sp_io::TestExternalities::default() + .execute_with(|| verify_consensus::(trusted_state, consensus_proof)); + + assert!(result.is_ok(), "BLS BEEFY verification failed: {:?}", result.err()); + println!("BLS BEEFY verify OK: verified {signed_count} paired signatures for beefy block #{block_number}"); +} + +/// Option B prototype: actually verify the BLS signatures, aggregated into a single pairing check. +/// +/// This reads the validators' paired keys straight from `Beefy.Authorities` and the paired +/// signatures from the latest justification, extracts the BLS halves (G2 public key, G1 signature), +/// and does two things: +/// 1. Per-signature Chaum-Pedersen verification via w3f-bls (proves our byte extraction and the +/// message hash-to-curve are correct). +/// 2. The aggregate pairing check `e(gen, sum(sig)) == e(sum(pubkey), H(commitment))` by summing +/// the signer G1 signatures and G2 public keys and verifying the sums as one signature. +/// +/// If step 2 passes, plain aggregation over the on-chain-committed key set is sound and we do not +/// need delinearization, which makes the eventual Solidity/EIP-2537 path much simpler. +/// +/// RELAY_WS_URL=ws://127.0.0.1:9977 \ +/// cargo test -p beefy-verifier --features bls test_bls_aggregate_verify -- --ignored --nocapture +#[cfg(feature = "bls")] +#[tokio::test] +#[ignore] +async fn test_bls_aggregate_verify() { + use w3f_bls::{ + DoublePublicKey, DoubleSignature, Message, PublicKey, SerializableToBytes, Signature, + TinyBLS381, + }; + + /// A 177-byte paired signature exactly as SCALE-encoded on the wire. + #[derive(Clone)] + struct Sig177([u8; 177]); + impl codec::Decode for Sig177 { + fn decode(input: &mut I) -> Result { + let mut bytes = [0u8; 177]; + input.read(&mut bytes)?; + Ok(Sig177(bytes)) + } + } + + let max_rpc_payload_size = 15 * 1024 * 1024; + let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); + let (_relay_client, relay_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); + let engine_id = polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID; + + let latest: H256 = + relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await.unwrap(); + let latest_block = relay_rpc.chain_get_block(Some(latest.into())).await.unwrap().unwrap(); + let latest_just = latest_block + .justifications + .expect("justifications") + .into_iter() + .find_map(|j| (j.0 == engine_id).then_some(j.1)) + .expect("beefy justification"); + + let VersionedFinalityProof::V1(sc) = + VersionedFinalityProof::::decode(&mut &*latest_just).unwrap(); + let commitment_encoded = sc.commitment.encode(); + // The BLS half signs the SCALE-encoded commitment with an empty context (see sp-core + // `bls381::Pair::sign` -> `Message::new(b"", message)`), hashed to G1 by w3f-bls. + let message = Message::new(b"", &commitment_encoded); + + // Validator keys are the 177-byte paired keys; the BLS DoublePublicKey is bytes [33..177]. + let raw_auth = relay_rpc + .state_get_storage(beefy_prover::BEEFY_AUTHORITIES.as_slice(), Some(latest.into())) + .await + .unwrap() + .expect("beefy authorities storage"); + let authorities = Vec::<[u8; 177]>::decode(&mut raw_auth.as_ref()).unwrap(); + + let mut agg_sig: Option<::SignatureGroup> = None; + let mut agg_pub: Option<::PublicKeyGroup> = None; + let mut count = 0u32; + + for (i, maybe_sig) in sc.signatures.iter().enumerate() { + let Some(sig) = maybe_sig else { continue }; + // DoublePublicKey = G1(48) || G2(96); it lives at bytes [33..177] of the paired key. + let dpk = DoublePublicKey::::from_bytes(&authorities[i][33..177]) + .expect("double public key"); + // DoubleSignature = G1 sig(48) || SchnorrProof(64); at bytes [65..177] of the paired sig. + let dsig = + DoubleSignature::::from_bytes(&sig.0[65..177]).expect("double signature"); + + // 1. Per-signature Chaum-Pedersen verification. + assert!(dpk.verify(&message, &dsig), "per-signature BLS verify failed for validator {i}"); + + // 2. Accumulate for the aggregate pairing check. + agg_sig = Some(agg_sig.map_or(dsig.0, |acc| acc + dsig.0)); + agg_pub = Some(agg_pub.map_or(dpk.1, |acc| acc + dpk.1)); + count += 1; + } + + let agg_sig = agg_sig.expect("at least one signer"); + let agg_pub = agg_pub.expect("at least one signer"); + + let aggregate_ok = + Signature::(agg_sig).verify(&message, &PublicKey::(agg_pub)); + + assert!(aggregate_ok, "aggregate BLS pairing check failed"); + println!( + "Aggregate BLS verify OK: {count} BLS signatures aggregated into ONE pairing check; \ + per-signature Chaum-Pedersen also verified. Plain aggregation over the committed key set \ + is sound (no delinearization needed)." + ); +} + +/// Phase 1 (trustless): the full aggregate-BLS verification flow a light client would run. +/// +/// Unlike `test_bls_aggregate_verify` (which trusts the keys read from storage), this proves the +/// signing keys against the on-chain keyset commitment. The runtime's `BeefyBls381G2ToKeysetLeaf` +/// converter commits `keccak(g2_pubkey)` leaves, so the flow is: +/// 1. build the merkle tree of all validators' G2 keys and check its root equals the on-chain +/// `keyset_commitment` (confirms the runtime commits G2 keys as expected), +/// 2. check the signer count meets the >2/3 threshold, +/// 3. prove the signers' keys are committed (merkle multi-proof at the signer indices), +/// 4. aggregate the signers' G2 keys and G1 signatures and verify one pairing check. +/// +/// RELAY_WS_URL=ws://127.0.0.1:9977 \ +/// cargo test -p beefy-verifier --features bls test_bls_trustless_verify -- --ignored --nocapture +#[cfg(feature = "bls")] +#[tokio::test] +#[ignore] +async fn test_bls_trustless_verify() { + use beefy_prover::rs_merkle::MerkleProof; + use w3f_bls::{Message, PublicKey, SerializableToBytes, Signature, TinyBLS381}; + + #[derive(Clone)] + struct Sig177([u8; 177]); + impl codec::Decode for Sig177 { + fn decode(input: &mut I) -> Result { + let mut bytes = [0u8; 177]; + input.read(&mut bytes)?; + Ok(Sig177(bytes)) + } + } + + let max_rpc_payload_size = 15 * 1024 * 1024; + let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); + let (_relay_client, relay_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); + let engine_id = polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID; + + // Latest justification -> commitment + per-validator signature slots. + let latest: H256 = + relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await.unwrap(); + let latest_block = relay_rpc.chain_get_block(Some(latest.into())).await.unwrap().unwrap(); + let latest_just = latest_block + .justifications + .expect("justifications") + .into_iter() + .find_map(|j| (j.0 == engine_id).then_some(j.1)) + .expect("beefy justification"); + let VersionedFinalityProof::V1(sc) = + VersionedFinalityProof::::decode(&mut &*latest_just).unwrap(); + let commitment_encoded = sc.commitment.encode(); + let message = Message::new(b"", &commitment_encoded); + + // All validators' G2 public keys, in authority-set order, from `Beefy.Authorities`. + let raw_auth = relay_rpc + .state_get_storage(beefy_prover::BEEFY_AUTHORITIES.as_slice(), Some(latest.into())) + .await + .unwrap() + .expect("beefy authorities"); + let paired = Vec::<[u8; 177]>::decode(&mut raw_auth.as_ref()).unwrap(); + let g2_keys: Vec<[u8; 96]> = paired + .iter() + .map(|k| { + let mut g2 = [0u8; 96]; + g2.copy_from_slice(&k[81..177]); + g2 + }) + .collect(); + let total = g2_keys.len(); + + // The on-chain keyset commitment (root of keccak(g2_pubkey) leaves) from the MmrLeaf pallet. + let raw_set = relay_rpc + .state_get_storage( + beefy_prover::BEEFY_MMR_LEAF_BEEFY_AUTHORITIES.as_slice(), + Some(latest.into()), + ) + .await + .unwrap() + .expect("mmr leaf beefy authorities"); + let authority_set = BeefyAuthoritySet::::decode(&mut raw_set.as_ref()).unwrap(); + let keyset_commitment: [u8; 32] = authority_set.keyset_commitment.into(); + + // 1. Rebuild the tree and confirm its root matches the on-chain commitment. + let leaves: Vec<[u8; 32]> = g2_keys.iter().map(|k| keccak_256(k)).collect(); + let tree = MerkleTree::::from_leaves(&leaves); + assert_eq!( + tree.root().expect("root"), + keyset_commitment, + "rebuilt keyset root does not match on-chain keyset_commitment (runtime converter mismatch)" + ); + + // 2. Signer set from the bitfield + the >2/3 threshold check. + let signer_indices: Vec = sc + .signatures + .iter() + .enumerate() + .filter_map(|(i, s)| s.as_ref().map(|_| i)) + .collect(); + assert!( + signer_indices.len() * 3 > total * 2, + "below supermajority: {} of {}", + signer_indices.len(), + total + ); + + // 3. Prove the signers' keys are committed (merkle multi-proof). + let signer_leaves: Vec<[u8; 32]> = signer_indices.iter().map(|&i| leaves[i]).collect(); + let proof = tree.proof(&signer_indices); + assert!( + MerkleProof::::new(proof.proof_hashes().to_vec()).verify( + keyset_commitment, + &signer_indices, + &signer_leaves, + total, + ), + "merkle multi-proof of signer keys against keyset_commitment failed" + ); + + // 4. Aggregate the signers' G2 keys and G1 signatures, one pairing check. + let mut agg_sig: Option<::SignatureGroup> = None; + let mut agg_pub: Option<::PublicKeyGroup> = None; + for &i in &signer_indices { + let sig = sc.signatures[i].as_ref().unwrap(); + let g1 = Signature::::from_bytes(&sig.0[65..113]).expect("g1 signature"); + let pk = PublicKey::::from_bytes(&g2_keys[i]).expect("g2 public key"); + agg_sig = Some(agg_sig.map_or(g1.0, |acc| acc + g1.0)); + agg_pub = Some(agg_pub.map_or(pk.0, |acc| acc + pk.0)); + } + let aggregate_ok = Signature::(agg_sig.unwrap()) + .verify(&message, &PublicKey::(agg_pub.unwrap())); + assert!(aggregate_ok, "aggregate BLS pairing check failed"); + + println!( + "Trustless aggregate BLS verify OK: {}/{} signers proven against the on-chain \ + keyset_commitment (merkle multi-proof), >2/3 threshold met, aggregated into ONE pairing \ + check.", + signer_indices.len(), + total + ); +} From 57aa8a8a7a9fafd899efbfa041a0b1cd7741735d Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 4 Aug 2026 11:23:03 +0100 Subject: [PATCH 02/48] verify beefy commitments signed with aggregate bls12-381 --- Cargo.lock | 6 + modules/consensus/beefy/primitives/src/lib.rs | 52 ++++ modules/consensus/beefy/prover/Cargo.toml | 3 +- modules/consensus/beefy/prover/src/bls.rs | 195 ++++++++++++++ modules/consensus/beefy/prover/src/lib.rs | 6 +- modules/consensus/beefy/prover/src/relay.rs | 8 +- modules/consensus/beefy/verifier/Cargo.toml | 24 +- modules/consensus/beefy/verifier/src/bls.rs | 58 ++++ modules/consensus/beefy/verifier/src/error.rs | 17 ++ modules/consensus/beefy/verifier/src/lib.rs | 253 ++++++++++++++---- modules/consensus/beefy/verifier/src/test.rs | 240 ++++++++++++++++- modules/ismp/clients/beefy/Cargo.toml | 1 + modules/ismp/clients/beefy/src/consensus.rs | 9 + modules/ismp/clients/beefy/src/lib.rs | 23 +- 14 files changed, 830 insertions(+), 65 deletions(-) create mode 100644 modules/consensus/beefy/prover/src/bls.rs create mode 100644 modules/consensus/beefy/verifier/src/bls.rs diff --git a/Cargo.lock b/Cargo.lock index 6d6f4e841..84b9d4e5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2916,6 +2916,7 @@ dependencies = [ "subxt-utils", "tokio", "tokio-stream", + "w3f-bls", ] [[package]] @@ -2924,6 +2925,10 @@ version = "0.1.0" dependencies = [ "alloy-sol-types 1.5.7", "anyhow", + "ark-bls12-381 0.4.0", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", "beefy-prover", "beefy-verifier-primitives", "ckb-merkle-mountain-range", @@ -2937,6 +2942,7 @@ dependencies = [ "polkadot-sdk", "primitive-types 0.13.1", "rs_merkle", + "sha2 0.10.9", "sp1-verifier 6.1.0", "subxt 0.42.1", "subxt-core 0.42.1", diff --git a/modules/consensus/beefy/primitives/src/lib.rs b/modules/consensus/beefy/primitives/src/lib.rs index cf0609d2d..3460de0b0 100644 --- a/modules/consensus/beefy/primitives/src/lib.rs +++ b/modules/consensus/beefy/primitives/src/lib.rs @@ -126,6 +126,58 @@ pub const PROOF_TYPE_NAIVE: u8 = 0x00; /// Proof type identifier for SP1 ZK proofs pub const PROOF_TYPE_SP1: u8 = 0x01; +/// Proof type identifier for aggregate BLS12-381 proofs +pub const PROOF_TYPE_BLS: u8 = 0x02; + +/// Size of a compressed BLS12-381 G1 point, the group BEEFY signatures live in. +pub const BLS_G1_SIGNATURE_LEN: usize = 48; + +/// Size of a compressed BLS12-381 G2 point, the group BEEFY public keys live in. +pub const BLS_G2_PUBLIC_KEY_LEN: usize = 96; + +/// A validator that contributed to an aggregate BLS signature. +/// +/// Only the public key is carried. The individual signatures are summed by the prover into +/// [`BlsMmrProof::aggregate_signature`], since verification never needs them apart. +#[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode)] +pub struct BlsSigner { + /// Compressed G2 public key, as committed to by the relay chain's keyset commitment. + pub public_key: [u8; BLS_G2_PUBLIC_KEY_LEN], + /// 0-based index of the authority in the authority set + pub index: u32, +} + +/// An MMR root update proven by an aggregate BLS12-381 signature rather than by recovering each +/// authority's ECDSA signature individually. +/// +/// The verifier checks this in one pairing operation regardless of how many validators signed, +/// which is the whole point of the BLS path. The tradeoff is that the signers' public keys travel +/// with the proof, so its size grows with the number of signers. +#[derive(sp_std::fmt::Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct BlsMmrProof { + /// The commitment that was signed + pub commitment: sp_consensus_beefy::Commitment, + /// The validators that signed, with the public keys the aggregate was formed over + pub signers: Vec, + /// Sum of the signers' G1 signatures, as a compressed G1 point + pub aggregate_signature: [u8; BLS_G1_SIGNATURE_LEN], + /// Latest leaf added to mmr + pub latest_mmr_leaf: MmrLeaf, + /// Proof for the latest mmr leaf + pub mmr_proof: sp_mmr_primitives::LeafProof, + /// Flat proof hashes proving the signers' public keys against the keyset commitment + pub authority_proof: Vec<[u8; 32]>, +} + +/// A BEEFY consensus update proven by an aggregate BLS signature. +#[derive(sp_std::fmt::Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct BlsConsensusMessage { + /// Parachain headers + pub parachain: ParachainProof, + /// proof for finalized mmr root + pub mmr: BlsMmrProof, +} + /// SP1 BEEFY proof. The proof bytes are prefixed with [`PROOF_TYPE_SP1`] by the prover. #[derive(sp_std::fmt::Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct Sp1BeefyProof { diff --git a/modules/consensus/beefy/prover/Cargo.toml b/modules/consensus/beefy/prover/Cargo.toml index 9e754b2ad..4833ae859 100644 --- a/modules/consensus/beefy/prover/Cargo.toml +++ b/modules/consensus/beefy/prover/Cargo.toml @@ -18,6 +18,7 @@ derive_more = { workspace = true, features = ["from"], default-features = true } rs_merkle = { workspace = true, default-features = true } hex-literal = "0.4.1" hex = { version = "0.4.3" } +w3f-bls = { version = "0.1.9", default-features = true, optional = true } subxt = { workspace = true, default-features = true } subxt-core = { workspace = true, default-features = true } @@ -54,4 +55,4 @@ local = [] # key type. On the wire such a chain carries 177-byte paired signatures and 177-byte authority # public keys; with this feature the prover keeps only the ECDSA half of each (the keccak-ECDSA # recoverable signature / compressed secp256k1 key), so the existing ECDSA verifier is unchanged. -bls = [] +bls = ["dep:w3f-bls"] diff --git a/modules/consensus/beefy/prover/src/bls.rs b/modules/consensus/beefy/prover/src/bls.rs new file mode 100644 index 000000000..52ba3ccd3 --- /dev/null +++ b/modules/consensus/beefy/prover/src/bls.rs @@ -0,0 +1,195 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Proving BEEFY commitments signed with aggregate BLS12-381. +//! +//! A relay whose BEEFY authorities hold paired `ecdsa_bls_crypto` keys puts both signatures on the +//! wire. `crate::relay::decode_beefy_justification` keeps the ECDSA half so the existing verifier +//! works unchanged; this module keeps the BLS half instead, and builds a proof the aggregate +//! verifier can check in a single pairing. +//! +//! This only applies to a relay whose keyset commitment is over BLS public keys. A chain +//! committing Ethereum addresses cannot be proven this way, and vice versa. + +use anyhow::anyhow; +use codec::{Decode, Encode}; +use polkadot_sdk::*; +use sp_consensus_beefy::{SignedCommitment, VersionedFinalityProof}; +use sp_io::hashing::keccak_256; +use subxt::{backend::legacy::LegacyRpcMethods, Config}; +use subxt_core::config::HashFor; + +use beefy_verifier_primitives::{ + BlsConsensusMessage, BlsMmrProof, BlsSigner, BLS_G1_SIGNATURE_LEN, BLS_G2_PUBLIC_KEY_LEN, +}; + +use crate::{ + build_parachain_proof, + relay::{fetch_mmr_proof, paras_parachains}, + Prover, BEEFY_AUTHORITIES, +}; + +/// Wire size of a paired (ECDSA, BLS12-381) BEEFY key or signature. +pub const PAIRED_LEN: usize = 177; + +/// Offset of the BLS G1 signature within a paired signature: the ECDSA half is 65 bytes, then the +/// `DoubleSignature` begins with its 48-byte G1 point. +const PAIRED_SIGNATURE_G1_OFFSET: usize = 65; + +/// Offset of the BLS G2 public key within a paired public key: the ECDSA half is 33 bytes, then +/// the `DoublePublicKey` is `G1 (48) || G2 (96)`, so G2 starts 48 bytes further in. +const PAIRED_PUBLIC_G2_OFFSET: usize = 33 + 48; + +/// A paired (ECDSA, BLS12-381) signature exactly as SCALE-encoded on the wire. +#[derive(Clone)] +pub struct PairedSignature(pub [u8; PAIRED_LEN]); + +impl Decode for PairedSignature { + fn decode(input: &mut I) -> Result { + let mut bytes = [0u8; PAIRED_LEN]; + input.read(&mut bytes)?; + Ok(PairedSignature(bytes)) + } +} + +impl PairedSignature { + /// The BLS12-381 G1 signature half. + pub fn g1_signature(&self) -> [u8; BLS_G1_SIGNATURE_LEN] { + let mut g1 = [0u8; BLS_G1_SIGNATURE_LEN]; + g1.copy_from_slice( + &self.0[PAIRED_SIGNATURE_G1_OFFSET..PAIRED_SIGNATURE_G1_OFFSET + BLS_G1_SIGNATURE_LEN], + ); + g1 + } +} + +/// Decode a BEEFY justification keeping the whole paired signature. +pub fn decode_paired_justification( + bytes: &[u8], +) -> Result, anyhow::Error> { + let VersionedFinalityProof::V1(signed_commitment) = + VersionedFinalityProof::::decode(&mut &*bytes)?; + Ok(signed_commitment) +} + +/// The validators' BLS12-381 G2 public keys, in authority-set order. +pub async fn beefy_g2_authorities( + rpc: &LegacyRpcMethods, + at: Option>, +) -> Result, anyhow::Error> { + let data = rpc + .state_get_storage(BEEFY_AUTHORITIES.as_slice(), at) + .await? + .ok_or_else(|| anyhow!("No beefy authorities found!"))?; + + let paired = Vec::<[u8; PAIRED_LEN]>::decode(&mut data.as_ref())?; + + Ok(paired + .into_iter() + .map(|key| { + let mut g2 = [0u8; BLS_G2_PUBLIC_KEY_LEN]; + g2.copy_from_slice( + &key[PAIRED_PUBLIC_G2_OFFSET..PAIRED_PUBLIC_G2_OFFSET + BLS_G2_PUBLIC_KEY_LEN], + ); + g2 + }) + .collect()) +} + +/// Sum compressed G1 signatures into the single compressed G1 point the verifier checks. +pub fn aggregate_signatures( + signatures: &[[u8; BLS_G1_SIGNATURE_LEN]], +) -> Result<[u8; BLS_G1_SIGNATURE_LEN], anyhow::Error> { + use w3f_bls::{EngineBLS, SerializableToBytes, Signature, TinyBLS381}; + + let mut aggregate: Option<::SignatureGroup> = None; + for signature in signatures { + let signature = Signature::::from_bytes(signature) + .map_err(|_| anyhow!("Invalid G1 signature encoding"))?; + aggregate = Some(aggregate.map_or(signature.0, |sum| sum + signature.0)); + } + + let aggregate = aggregate.ok_or_else(|| anyhow!("No signatures to aggregate"))?; + Signature::(aggregate) + .to_bytes() + .try_into() + .map_err(|_| anyhow!("Aggregated signature was not {BLS_G1_SIGNATURE_LEN} bytes")) +} + +impl Prover { + /// Build a consensus proof whose commitment is proven by one aggregate BLS signature. + /// + /// The signers' G2 public keys travel with the proof, since the verifier needs them both to + /// form the aggregate key and to prove membership of the authority set. That makes the proof + /// grow with the number of signers, which is the cost of not needing a SNARK. + pub async fn bls_consensus_proof( + &self, + signed_commitment: SignedCommitment, + ) -> Result { + let block_number: u32 = signed_commitment.commitment.block_number; + let block_hash = self + .relay_rpc + .chain_get_block_hash(Some(block_number.into())) + .await? + .ok_or_else(|| anyhow!("Failed to query blockhash for blocknumber"))?; + + let (mmr_proof, latest_leaf) = + fetch_mmr_proof(&self.relay_rpc, block_number, self.query_batch_size).await?; + + let authorities = beefy_g2_authorities(&self.relay_rpc, Some(block_hash)).await?; + + // Signers in authority-set order, which is what the merkle multi-proof expects and what + // the verifier enforces. + let mut signers = Vec::new(); + let mut g1_signatures = Vec::new(); + for (index, maybe_signature) in signed_commitment.signatures.iter().enumerate() { + let Some(signature) = maybe_signature else { continue }; + let public_key = *authorities + .get(index) + .ok_or_else(|| anyhow!("Signature index {index} outside the authority set"))?; + + signers.push(BlsSigner { public_key, index: index as u32 }); + g1_signatures.push(signature.g1_signature()); + } + + let aggregate_signature = aggregate_signatures(&g1_signatures)?; + + // The keyset commitment is a merkle root over the hashed G2 keys, so the tree is built + // over every authority and opened at the signers' positions. + let leaves = authorities.iter().map(|key| keccak_256(key)).collect::>(); + let indices = signers.iter().map(|signer| signer.index as usize).collect::>(); + let tree = rs_merkle::MerkleTree::::from_leaves(&leaves); + let authority_proof = tree.proof(&indices).proof_hashes().to_vec(); + + let mmr = BlsMmrProof { + commitment: signed_commitment.commitment.clone(), + signers, + aggregate_signature, + latest_mmr_leaf: latest_leaf.clone(), + mmr_proof, + authority_proof, + }; + + let heads = paras_parachains( + &self.relay_rpc, + Some(HashFor::::decode(&mut &*latest_leaf.parent_number_and_hash.1.encode())?), + ) + .await?; + + let parachain = build_parachain_proof(&self.para_ids, &heads); + + Ok(BlsConsensusMessage { mmr, parachain }) + } +} diff --git a/modules/consensus/beefy/prover/src/lib.rs b/modules/consensus/beefy/prover/src/lib.rs index e4ea1811b..52b9ddf20 100644 --- a/modules/consensus/beefy/prover/src/lib.rs +++ b/modules/consensus/beefy/prover/src/lib.rs @@ -48,6 +48,9 @@ use relay::{ }; use util::hash_authority_addresses; +/// Proving commitments signed with aggregate BLS12-381 +#[cfg(feature = "bls")] +pub mod bls; /// Methods for querying the relay chain pub mod relay; /// Helper functions and types @@ -214,7 +217,8 @@ impl Prover { // `bls`: authorities are stored as 177-byte paired (ECDSA, BLS12-381) keys; keep the ECDSA // half (first 33 bytes, a compressed secp256k1 key) so the derived address leaves match the - // on-chain keyset commitment, which `BeefyEcdsaBlsToEthereum` builds from those ECDSA halves. + // on-chain keyset commitment, which `BeefyEcdsaBlsToEthereum` builds from those ECDSA + // halves. #[cfg(feature = "bls")] let current_authorities = Vec::<[u8; 177]>::decode(&mut data.as_ref())? .into_iter() diff --git a/modules/consensus/beefy/prover/src/relay.rs b/modules/consensus/beefy/prover/src/relay.rs index a6bb1cb3b..509ab6ab4 100644 --- a/modules/consensus/beefy/prover/src/relay.rs +++ b/modules/consensus/beefy/prover/src/relay.rs @@ -90,10 +90,10 @@ pub fn decode_beefy_justification( ) -> Result, anyhow::Error> { #[cfg(not(feature = "bls"))] { - let VersionedFinalityProof::V1(signed_commitment) = - VersionedFinalityProof::::decode( - &mut &*bytes, - )?; + let VersionedFinalityProof::V1(signed_commitment) = VersionedFinalityProof::< + u32, + sp_consensus_beefy::ecdsa_crypto::Signature, + >::decode(&mut &*bytes)?; Ok(signed_commitment) } #[cfg(feature = "bls")] diff --git a/modules/consensus/beefy/verifier/Cargo.toml b/modules/consensus/beefy/verifier/Cargo.toml index 13347a2e0..db82389c5 100644 --- a/modules/consensus/beefy/verifier/Cargo.toml +++ b/modules/consensus/beefy/verifier/Cargo.toml @@ -21,12 +21,14 @@ rs_merkle = { workspace = true, default-features = false } thiserror = { workspace = true } sp1-verifier = { git = "https://github.com/polytope-labs/sp1.git", branch = "polytope-labs/v6.1.0-wasm-compatible", default-features = false } alloy-sol-types = { workspace = true, default-features = false } +w3f-bls = { version = "0.1.9", default-features = false, optional = true } [dependencies.polkadot-sdk] workspace = true features = [ "sp-consensus-beefy", "sp-core", + "sp-mmr-primitives", "sp-runtime" ] @@ -40,6 +42,13 @@ subxt-core = { workspace = true, default-features = true } subxt-utils = { workspace = true, default-features = true } futures = { workspace = true } tokio = { workspace = true } +# Pinned to the versions w3f-bls builds against, so the hash-to-curve test vector is generated by +# exactly the code path that signs on the relay chain. +ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = false } +ark-ec = { version = "0.4.0", default-features = false } +ark-ff = { version = "0.4.0", default-features = false } +ark-serialize = { version = "0.4.0", default-features = false } +sha2 = { version = "0.10", default-features = false } # Aggregate BLS verification prototype (Option B): the same double-scheme BLS as substrate BEEFY. w3f-bls = { version = "0.1.9", default-features = true } @@ -51,10 +60,16 @@ features = ["sp-io"] [features] default = ["std"] -# Runs `test_verify_consensus_bls` against a local BLS-BEEFY relay (paired ecdsa_bls_crypto -# authorities). Pulls the prover's `bls` decode path and `local` storage keys (the local relay -# names its pallet `MmrLeaf`). Enable with `--features bls` when running that test. -bls = ["beefy-prover/bls", "beefy-prover/local"] +# The `w3f-bls` backed implementation of the aggregate BLS check, in `crate::bls`. A runtime that +# verifies BLS through host functions implements `BlsAggregateVerify` itself and leaves this off. +bls-crypto = ["dep:w3f-bls"] +# Runs the BLS-BEEFY tests against a relay whose BEEFY authorities are paired ecdsa_bls_crypto +# keys. Pulls the prover's `bls` decode path, which reads 177-byte paired signatures and keys. +bls = ["beefy-prover/bls", "bls-crypto"] +# Storage keys for a relay that names its beefy-mmr pallet `MmrLeaf` rather than `BeefyMmrLeaf`. +# Westend (and Polkadot/Kusama) use `BeefyMmrLeaf`, so leave this off for them; our rococo fork +# used `MmrLeaf`, so pair it with `bls` as `--features bls,local` when testing against that chain. +local = ["beefy-prover/local"] std = [ "log/std", "anyhow/std", @@ -67,4 +82,5 @@ std = [ "rs_merkle/std", "sp1-verifier/std", "alloy-sol-types/std", + "w3f-bls?/std", ] \ No newline at end of file diff --git a/modules/consensus/beefy/verifier/src/bls.rs b/modules/consensus/beefy/verifier/src/bls.rs new file mode 100644 index 000000000..cabf39b4d --- /dev/null +++ b/modules/consensus/beefy/verifier/src/bls.rs @@ -0,0 +1,58 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A `w3f-bls` backed implementation of the aggregate BLS check, matching the scheme substrate's +//! BEEFY signs with. +//! +//! A host wires this up by implementing [`crate::BlsAggregateVerify`] and delegating to +//! [`aggregate_verify`]. Runtimes should prefer an implementation backed by host functions, since +//! a BLS12-381 pairing executed inside wasm is considerably more expensive than the `ecrecover` +//! calls it replaces. The saving of the BLS path is that there is one pairing regardless of how +//! many validators signed, not that each individual operation is cheap. + +use beefy_verifier_primitives::{BLS_G1_SIGNATURE_LEN, BLS_G2_PUBLIC_KEY_LEN}; +use w3f_bls::{EngineBLS, Message, PublicKey, SerializableToBytes, Signature, TinyBLS381}; + +/// Verify that `signature` is the sum of the signatures over `message` produced by the holders of +/// `public_keys`, in a single pairing check. +/// +/// The message is hashed onto the signature curve exactly as `w3f-bls` does when signing, with an +/// empty context, matching sp-core's `bls381::Pair::sign`. +/// +/// Errors when a point fails to decode; returns `Ok(false)` when the points are well formed but +/// the aggregate does not verify. +pub fn aggregate_verify( + message: &[u8], + signature: &[u8; BLS_G1_SIGNATURE_LEN], + public_keys: &[[u8; BLS_G2_PUBLIC_KEY_LEN]], +) -> anyhow::Result { + if public_keys.is_empty() { + return Ok(false); + } + + let signature = Signature::::from_bytes(signature) + .map_err(|_| anyhow::anyhow!("invalid G1 signature encoding"))?; + + let mut aggregate: Option<::PublicKeyGroup> = None; + for key in public_keys { + let public_key = PublicKey::::from_bytes(key) + .map_err(|_| anyhow::anyhow!("invalid G2 public key encoding"))?; + aggregate = Some(aggregate.map_or(public_key.0, |sum| sum + public_key.0)); + } + + let aggregate = aggregate.expect("public_keys is non-empty, checked above; qed"); + + Ok(signature.verify(&Message::new(b"", message), &PublicKey::(aggregate))) +} diff --git a/modules/consensus/beefy/verifier/src/error.rs b/modules/consensus/beefy/verifier/src/error.rs index dbd29608a..13cd4d594 100644 --- a/modules/consensus/beefy/verifier/src/error.rs +++ b/modules/consensus/beefy/verifier/src/error.rs @@ -73,6 +73,23 @@ pub enum Error { /// The SP1 Groth16 verifier rejected the proof bytes. #[error("SP1 proof verification failed")] Sp1VerificationFailed, + /// A G1 signature or G2 public key in a BLS proof is not a valid curve point. + #[error("Invalid BLS point encoding")] + InvalidBlsPoint, + /// The aggregate BLS pairing check rejected the signature. + #[error("Aggregate BLS signature verification failed")] + BlsVerificationFailed, + /// A BLS proof carried no signers, so there is no aggregate to verify. + #[error("BLS proof contains no signers")] + NoBlsSigners, + /// A BLS proof's signer indices are not strictly ascending, or address an authority outside + /// the set. Repeating an index would let a prover count one validator many times towards the + /// supermajority threshold and aggregate its key more than once. + #[error("BLS signer indices must be strictly ascending and within the authority set")] + InvalidBlsSignerOrdering, + /// The BLS proof payload failed to SCALE-decode. + #[error("Cannot decode BLS proof: {0}")] + DecodeBlsProof(String), // -- ismp-beefy client wrapper -- /// The trusted state failed to SCALE-decode into a `ConsensusState`. diff --git a/modules/consensus/beefy/verifier/src/lib.rs b/modules/consensus/beefy/verifier/src/lib.rs index 37ef08a06..2c2d94a9e 100644 --- a/modules/consensus/beefy/verifier/src/lib.rs +++ b/modules/consensus/beefy/verifier/src/lib.rs @@ -23,6 +23,8 @@ extern crate alloc; +#[cfg(feature = "bls-crypto")] +pub mod bls; pub mod error; pub mod sp1; #[cfg(test)] @@ -33,6 +35,7 @@ use core::marker::PhantomData; use crate::error::Error; use beefy_verifier_primitives::{ + BLS_G1_SIGNATURE_LEN, BLS_G2_PUBLIC_KEY_LEN, BlsConsensusMessage, BlsMmrProof, ConsensusMessage, ConsensusState, MmrProof, ParachainHeader, ParachainProof, }; use codec::Encode; @@ -41,6 +44,10 @@ use merkle_mountain_range::{ Error as MmrError, Merge as MmrMerge, MerkleProof as MmrMerkleProof, leaf_index_to_mmr_size, leaf_index_to_pos, }; +use polkadot_sdk::{ + sp_consensus_beefy::{Commitment, mmr::MmrLeaf}, + sp_mmr_primitives::LeafProof, +}; use primitive_types::H256; use rs_merkle::{Hasher, MerkleProof}; @@ -55,6 +62,30 @@ pub trait EcdsaRecover { fn secp256k1_recover(prehash: &[u8; 32], signature: &[u8; 65]) -> anyhow::Result<[u8; 64]>; } +/// A trait for verifying an aggregate BLS12-381 signature under BEEFY's signing scheme. +/// +/// BEEFY's BLS half uses `w3f-bls`'s "double" scheme over `TinyBLS381`, in which signatures are G1 +/// points and public keys are G2 points. Note this is the opposite of the Ethereum convention, so +/// implementations written against an eth2 BLS library will not transfer. The signed message is the +/// SCALE-encoded commitment, hashed to a G1 point with an empty context. +/// +/// This is a trait, rather than a direct call into a BLS library, for the same reason +/// [`EcdsaRecover`] is: a runtime can route the pairing through host functions instead of paying to +/// execute it in wasm. Implementations must perform the hash-to-curve themselves so that step can +/// be host-accelerated too. +pub trait BlsAggregateVerify { + /// Check that `signature` is the sum of the signatures over `message` produced by the holders + /// of `public_keys`, using a single pairing check. + /// + /// Returns `Ok(false)` when the points are well formed but the signature does not verify, and + /// an error when a point cannot be decoded. + fn verify_aggregate( + message: &[u8], + signature: &[u8; BLS_G1_SIGNATURE_LEN], + public_keys: &[[u8; BLS_G2_PUBLIC_KEY_LEN]], + ) -> anyhow::Result; +} + /// A hasher implementation for rs_merkle, generic over the hash function pub struct MerkleHasher(PhantomData); @@ -97,29 +128,162 @@ pub fn verify_consensus( Ok((state.encode(), verified_headers)) } +/// Verify a BEEFY consensus proof whose commitment was signed with aggregate BLS12-381, returning +/// the new trusted consensus state and the verified parachain headers. +pub fn verify_bls_consensus( + trusted_state: ConsensusState, + proof: BlsConsensusMessage, +) -> Result<(Vec, Vec), Error> { + let (state, heads_root) = verify_bls_mmr_update_proof::(trusted_state, proof.mmr)?; + let verified_headers = verify_parachain_headers::(heads_root, proof.parachain)?; + Ok((state.encode(), verified_headers)) +} + /// Verifies a new Mmr root update, the relay chain accumulates it's blocks into a merkle mountain /// range tree which light clients can use as a source for log_2(n) ancestry proofs. This new mmr /// root hash is signed by the relay chain authority set and we can verify the membership of the /// authorities that signed this new root using a merkle multi proof and a merkle commitment to the /// total authorities pub fn verify_mmr_update_proof( - mut trusted_state: ConsensusState, + trusted_state: ConsensusState, mmr: MmrProof, ) -> Result<(ConsensusState, H256), Error> { - let signatures_length = mmr.signed_commitment.signatures.len(); - let latest_height = mmr.signed_commitment.commitment.block_number; + let commitment = &mmr.signed_commitment.commitment; + let preamble = + prepare_update(&trusted_state, commitment, mmr.signed_commitment.signatures.len() as u32)?; + + // The ECDSA half signs the keccak hash of the commitment, and the authority set is committed + // to as the keccak of each signer's Ethereum address, so the signers are identified by + // recovering them rather than by being named in the proof. + let commitment_hash = H::keccak256(&commitment.encode()); + let mut authority_leaves: Vec<[u8; 32]> = Vec::new(); + let mut authority_indices = Vec::new(); + + for sig in mmr.signed_commitment.signatures.iter() { + let uncompressed = H::secp256k1_recover(&commitment_hash.0, &sig.signature) + .map_err(|_| Error::FailedToRecoverPublicKey)?; + + let hashed_uncompressed = H::keccak256(&uncompressed); + + let mut eth_address = [0u8; 20]; + eth_address.copy_from_slice(&hashed_uncompressed.as_ref()[12..]); + + let authority_address_hash = H::keccak256(ð_address); + + authority_leaves.push(authority_address_hash.into()); + authority_indices.push(sig.index as usize); + } + + verify_authority_membership::( + preamble.keyset_commitment, + &mmr.authority_proof, + &authority_indices, + &authority_leaves, + preamble.authority_count, + )?; + + verify_mmr_leaf::(&mmr.latest_mmr_leaf, &mmr.mmr_proof, preamble.mmr_root)?; + + let latest_height = commitment.block_number; + let state = apply_update(trusted_state, &mmr.latest_mmr_leaf, latest_height); + + Ok((state, mmr.latest_mmr_leaf.leaf_extra)) +} + +/// Verifies an MMR root update whose authorities signed with BLS12-381 rather than ECDSA. +/// +/// Where the ECDSA path recovers a public key per signature, this sums the signers' keys and +/// checks one aggregate signature in a single pairing operation, so the cost of the signature +/// check no longer grows with the size of the validator set. The signers are named explicitly in +/// the proof and proven to be authorities by the same merkle multi-proof the ECDSA path uses, +/// against a keyset commitment that must be over BLS public keys rather than Ethereum addresses. +/// +/// Note that a chain whose keyset commitment holds Ethereum addresses cannot be verified through +/// this path, and vice versa. The two are separate consensus states. +pub fn verify_bls_mmr_update_proof( + trusted_state: ConsensusState, + mmr: BlsMmrProof, +) -> Result<(ConsensusState, H256), Error> { + if mmr.signers.is_empty() { + return Err(Error::NoBlsSigners); + } + + let preamble = prepare_update(&trusted_state, &mmr.commitment, mmr.signers.len() as u32)?; + + // Strictly ascending indices, so a signer cannot be counted towards the threshold or summed + // into the aggregate more than once, and so the multi-proof sees them in the order it wants. + let within_set = mmr + .signers + .last() + .map(|last| last.index < preamble.authority_count) + .unwrap_or(false); + let ascending = mmr.signers.windows(2).all(|pair| pair[0].index < pair[1].index); + if !ascending || !within_set { + return Err(Error::InvalidBlsSignerOrdering); + } + + // The BLS half signs the SCALE-encoded commitment itself, where the ECDSA half signs its + // keccak hash. Hashing it onto the curve is the implementation's job, so that step can be + // host-accelerated alongside the pairing. + let public_keys = mmr.signers.iter().map(|signer| signer.public_key).collect::>(); + let verified = + H::verify_aggregate(&mmr.commitment.encode(), &mmr.aggregate_signature, &public_keys) + .map_err(|_| Error::InvalidBlsPoint)?; + + if !verified { + return Err(Error::BlsVerificationFailed); + } + + // The pairing check only proves that the holders of *these* keys signed. Proving those keys + // are the authority set's is what the merkle multi-proof is for. + let authority_leaves = mmr + .signers + .iter() + .map(|signer| H::keccak256(&signer.public_key).into()) + .collect::>(); + let authority_indices = + mmr.signers.iter().map(|signer| signer.index as usize).collect::>(); + + verify_authority_membership::( + preamble.keyset_commitment, + &mmr.authority_proof, + &authority_indices, + &authority_leaves, + preamble.authority_count, + )?; + + verify_mmr_leaf::(&mmr.latest_mmr_leaf, &mmr.mmr_proof, preamble.mmr_root)?; + + let latest_height = mmr.commitment.block_number; + let state = apply_update(trusted_state, &mmr.latest_mmr_leaf, latest_height); + + Ok((state, mmr.latest_mmr_leaf.leaf_extra)) +} - if trusted_state.latest_beefy_height >= latest_height { +/// The parts of an update that hold regardless of how the commitment was signed. +struct UpdatePreamble { + /// Commitment to the authority set the signers must belong to. + keyset_commitment: H256, + /// Size of that authority set. + authority_count: u32, + /// MMR root carried in the commitment payload. + mmr_root: H256, +} + +/// Checks staleness, resolves which authority set the commitment claims to be signed under, +/// judges participation against that set alone, and extracts the MMR root from the payload. +fn prepare_update( + trusted_state: &ConsensusState, + commitment: &Commitment, + signer_count: u32, +) -> Result { + if trusted_state.latest_beefy_height >= commitment.block_number { return Err(Error::StaleHeight { trusted_height: trusted_state.latest_beefy_height, - current_height: latest_height, + current_height: commitment.block_number, }); } - let commitment = mmr.signed_commitment.commitment.clone(); - - // Pick the authority set the commitment claims to be signed under, then judge - // participation against that set alone. let authority_set = if commitment.validator_set_id == trusted_state.current_authorities.id { &trusted_state.current_authorities } else if commitment.validator_set_id == trusted_state.next_authorities.id { @@ -128,7 +292,7 @@ pub fn verify_mmr_update_proof( return Err(Error::UnknownAuthoritySet { id: commitment.validator_set_id }); }; - if !check_participation_threshold(signatures_length as u32, authority_set.len) { + if !check_participation_threshold(signer_count, authority_set.len) { return Err(Error::SuperMajorityRequired); } @@ -140,50 +304,48 @@ pub fn verify_mmr_update_proof( if mmr_root_data.len() != 32 { return Err(Error::InvalidMmrRootHashLength { len: mmr_root_data.len() }); } - let mmr_root = H256::from_slice(mmr_root_data); - - let commitment_hash = H::keccak256(&commitment.encode()); - let mut authority_leaves: Vec<[u8; 32]> = Vec::new(); - let mut authority_indices = Vec::new(); - - for sig in mmr.signed_commitment.signatures.iter() { - let uncompressed = H::secp256k1_recover(&commitment_hash.0, &sig.signature) - .map_err(|_| Error::FailedToRecoverPublicKey)?; - - let hashed_uncompressed = H::keccak256(&uncompressed); - let mut eth_address = [0u8; 20]; - eth_address.copy_from_slice(&hashed_uncompressed.as_ref()[12..]); - - let authority_address_hash = H::keccak256(ð_address); - - authority_leaves.push(authority_address_hash.into()); - authority_indices.push(sig.index as usize); - } + Ok(UpdatePreamble { + keyset_commitment: authority_set.keyset_commitment, + authority_count: authority_set.len, + mmr_root: H256::from_slice(mmr_root_data), + }) +} - let merkle_proof = MerkleProof::>::new(mmr.authority_proof.clone()); +/// Proves the signing authorities are members of the committed authority set. +fn verify_authority_membership( + keyset_commitment: H256, + proof: &[[u8; 32]], + indices: &[usize], + leaves: &[[u8; 32]], + authority_count: u32, +) -> Result<(), Error> { + let merkle_proof = MerkleProof::>::new(proof.to_vec()); - let valid = merkle_proof.verify( - authority_set.keyset_commitment.into(), - &authority_indices, - &authority_leaves, - authority_set.len as usize, - ); + let valid = + merkle_proof.verify(keyset_commitment.into(), indices, leaves, authority_count as usize); if !valid { Err(Error::InvalidAuthoritiesProof)?; } - verify_mmr_leaf::(&mmr, mmr_root)?; + Ok(()) +} - if mmr.latest_mmr_leaf.beefy_next_authority_set.id > trusted_state.next_authorities.id { +/// Rotates the tracked authority sets if the leaf announces a newer one, and records the height. +fn apply_update( + mut trusted_state: ConsensusState, + leaf: &MmrLeaf, + latest_height: u32, +) -> ConsensusState { + if leaf.beefy_next_authority_set.id > trusted_state.next_authorities.id { trusted_state.current_authorities = trusted_state.next_authorities.clone(); - trusted_state.next_authorities = mmr.latest_mmr_leaf.beefy_next_authority_set.clone(); + trusted_state.next_authorities = leaf.beefy_next_authority_set.clone(); } trusted_state.latest_beefy_height = latest_height; - Ok((trusted_state, mmr.latest_mmr_leaf.leaf_extra)) + trusted_state } /// Verifies the inclusion of parachain headers in the parachain heads root via a merkle multi proof @@ -223,7 +385,8 @@ pub fn verify_parachain_headers( } fn verify_mmr_leaf( - mmr: &MmrProof, + leaf: &MmrLeaf, + proof: &LeafProof, mmr_root: H256, ) -> Result<(), Error> { // `leaf_indices` is supplied by the relayer in the unsigned consensus message; @@ -231,16 +394,16 @@ fn verify_mmr_leaf( // after the BEEFY signature and authority membership checks had already succeeded. // This verifier checks a single MMR leaf, so reject any proof that does not carry // exactly one leaf index. - if mmr.mmr_proof.leaf_indices.len() != 1 { + if proof.leaf_indices.len() != 1 { Err(Error::InvalidMmrProof)? } - let leaf_index = mmr.mmr_proof.leaf_indices[0]; - let leaf_hash = H::keccak256(&mmr.latest_mmr_leaf.encode()); + let leaf_index = proof.leaf_indices[0]; + let leaf_hash = H::keccak256(&leaf.encode()); let mmr_size = leaf_index_to_mmr_size(leaf_index); let mmr_proof = MmrMerkleProof::<[u8; 32], KeccakMerge>::new( mmr_size, - mmr.mmr_proof.items.iter().map(|h| (*h).into()).collect(), + proof.items.iter().map(|h| (*h).into()).collect(), ); let leaf_pos = leaf_index_to_pos(leaf_index); let leaf = (leaf_pos, leaf_hash.into()); diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index 5c749e0ee..a550ec1f7 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -38,6 +38,8 @@ use polkadot_sdk::sp_consensus_beefy::{ }; use sp_mmr_primitives::LeafProof; +#[cfg(feature = "bls")] +use crate::verify_bls_consensus; use crate::{EcdsaRecover, error::Error, verify_consensus, verify_mmr_update_proof}; struct TestHost; @@ -55,6 +57,17 @@ impl EcdsaRecover for TestHost { } } +#[cfg(feature = "bls-crypto")] +impl crate::BlsAggregateVerify for TestHost { + fn verify_aggregate( + message: &[u8], + signature: &[u8; beefy_verifier_primitives::BLS_G1_SIGNATURE_LEN], + public_keys: &[[u8; beefy_verifier_primitives::BLS_G2_PUBLIC_KEY_LEN]], + ) -> anyhow::Result { + crate::bls::aggregate_verify(message, signature, public_keys) + } +} + // Integration test: hits live Polkadot/parachain RPCs (see RELAY_WS_URL / PARA_WS_URL env vars). // Run explicitly with `cargo test -- --ignored`. #[tokio::test] @@ -319,7 +332,8 @@ fn test_sp1_verify_consensus_accepts_solidity_fixture() { // Proof payload matches SP1Beefy.sol:verifyConsensus's `abi.decode(...)` call: // a sequence of four top-level types, not a struct wrapper. - type ProofTuple = sol! { (MiniCommitment, PartialBeefyMmrLeaf, ParachainHeader[], bytes, bytes32) }; + type ProofTuple = + sol! { (MiniCommitment, PartialBeefyMmrLeaf, ParachainHeader[], bytes, bytes32) }; let (commitment, leaf, headers, plonk_proof, nonce) = ::abi_decode_sequence(&proof_bytes).expect("decode proof tuple"); let sp1_proof = Sp1BeefyProof { @@ -453,9 +467,8 @@ fn rejects_sp1_proof_carrying_a_stale_mmr_leaf() { // Swap in a leaf from an earlier block, as an attacker replaying a historical leaf would. proof.mmr_leaf.parent_number_and_hash.0 = BLOCK_NUMBER - 500; - let stale = sp_io::TestExternalities::default().execute_with(|| { - crate::sp1::verify_sp1_consensus::(trusted_state, proof, VKEY) - }); + let stale = sp_io::TestExternalities::default() + .execute_with(|| crate::sp1::verify_sp1_consensus::(trusted_state, proof, VKEY)); assert!(matches!(stale, Err(Error::StaleMmrLeaf { .. })), "got {stale:?}"); } @@ -466,7 +479,8 @@ fn rejects_sp1_proof_carrying_a_stale_mmr_leaf() { /// existing ECDSA path. /// /// RELAY_WS_URL=ws://127.0.0.1:9977 \ -/// cargo test -p beefy-verifier test_verify_consensus_bls -- --ignored --nocapture +/// cargo test -p beefy-verifier --features bls test_verify_consensus_bls -- --ignored +/// --nocapture #[cfg(feature = "bls")] #[tokio::test] #[ignore] @@ -513,7 +527,11 @@ async fn test_verify_consensus_bls() { panic!("reached genesis without a previous beefy block"); } let block = relay_rpc.chain_get_block(Some(parent.into())).await.unwrap().unwrap(); - if block.justifications.map(|js| js.iter().any(|j| j.0 == engine_id)).unwrap_or(false) { + if block + .justifications + .map(|js| js.iter().any(|j| j.0 == engine_id)) + .unwrap_or(false) + { previous = parent; break; } @@ -577,7 +595,9 @@ async fn test_verify_consensus_bls() { .execute_with(|| verify_consensus::(trusted_state, consensus_proof)); assert!(result.is_ok(), "BLS BEEFY verification failed: {:?}", result.err()); - println!("BLS BEEFY verify OK: verified {signed_count} paired signatures for beefy block #{block_number}"); + println!( + "BLS BEEFY verify OK: verified {signed_count} paired signatures for beefy block #{block_number}" + ); } /// Option B prototype: actually verify the BLS signatures, aggregated into a single pairing check. @@ -594,7 +614,8 @@ async fn test_verify_consensus_bls() { /// need delinearization, which makes the eventual Solidity/EIP-2537 path much simpler. /// /// RELAY_WS_URL=ws://127.0.0.1:9977 \ -/// cargo test -p beefy-verifier --features bls test_bls_aggregate_verify -- --ignored --nocapture +/// cargo test -p beefy-verifier --features bls test_bls_aggregate_verify -- --ignored +/// --nocapture #[cfg(feature = "bls")] #[tokio::test] #[ignore] @@ -697,7 +718,8 @@ async fn test_bls_aggregate_verify() { /// 4. aggregate the signers' G2 keys and G1 signatures and verify one pairing check. /// /// RELAY_WS_URL=ws://127.0.0.1:9977 \ -/// cargo test -p beefy-verifier --features bls test_bls_trustless_verify -- --ignored --nocapture +/// cargo test -p beefy-verifier --features bls test_bls_trustless_verify -- --ignored +/// --nocapture #[cfg(feature = "bls")] #[tokio::test] #[ignore] @@ -826,3 +848,203 @@ async fn test_bls_trustless_verify() { total ); } + +/// Pins down exactly how BEEFY's BLS half hashes a commitment onto the signature curve, and emits +/// a test vector for the Solidity/EIP-2537 implementation to be checked against. +/// +/// This is the make-or-break detail of the EVM path. `w3f-bls` does *not* use the IETF ciphersuite +/// string as the domain separation tag the way a textbook implementation would. It uses a one-byte +/// DST of `0x01`, and prepends the ciphersuite string to the message instead: +/// +/// ```text +/// suite = "BLS_SIG_" || "BLS12381" || "G1" || "_XMD:SHA-256_SSWU_RO_" || "NUL_" +/// preimage = suite || context || message // context is empty for BEEFY +/// point = hash_to_curve(preimage, DST = 0x01) // expand_message_xmd, WB/SSWU map +/// ``` +/// +/// Everything after that composition is standard RFC 9380, which is what the EIP-2537 +/// `MAP_FP_TO_G1` precompile implements, so the contract has to reproduce the composition and the +/// `expand_message_xmd` step and can lean on precompiles for the rest. +/// +/// cargo test -p beefy-verifier --features bls bls_hash_to_curve_vector -- --nocapture +#[cfg(feature = "bls")] +#[test] +fn bls_hash_to_curve_vector() { + use ark_bls12_381::{Fq, G1Affine, g1::Config as G1Config}; + use ark_ec::{ + AffineRepr, CurveGroup, + hashing::{HashToCurve, curve_maps::wb::WBMap, map_to_curve_hasher::MapToCurveBasedHasher}, + }; + use ark_ff::{ + BigInteger, PrimeField, + field_hashers::{DefaultFieldHasher, HashToField}, + }; + use w3f_bls::{Message, TinyBLS381}; + + // Any byte string stands in for a SCALE-encoded commitment here; the composition is what is + // being pinned down, and it does not depend on the contents. + let message = b"beefy-bls-hash-to-curve-vector"; + + let suite = [ + b"BLS_SIG_".as_ref(), + b"BLS12381".as_ref(), + b"G1".as_ref(), + b"_XMD:SHA-256_SSWU_RO_".as_ref(), + b"NUL_".as_ref(), + ] + .concat(); + assert_eq!( + suite.as_slice(), + b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_".as_ref(), + "ciphersuite string drifted from what w3f-bls composes" + ); + + // context is empty for BEEFY, matching sp-core's `bls381::Pair::sign`. + let preimage = [suite.as_slice(), b"".as_ref(), message.as_ref()].concat(); + + // Rebuild the hasher from the documented parameters rather than going through w3f-bls, then + // check it lands on the same point. If this assert holds, the recipe above is the whole story + // and a Solidity implementation has everything it needs. + let hasher = MapToCurveBasedHasher::< + ark_ec::short_weierstrass::Projective, + DefaultFieldHasher, + WBMap, + >::new(&[1u8]) + .expect("hasher construction"); + let reconstructed: G1Affine = hasher.hash(&preimage).expect("hash to curve"); + + let expected = Message::new(b"", message).hash_to_signature_curve::().into_affine(); + + assert_eq!( + reconstructed, expected, + "reconstructing the hash-to-curve from DST=0x01 and suite-prefixed message did not match \ + w3f-bls; the Solidity recipe would be wrong" + ); + + let (x, y) = expected.xy().expect("point is not the identity"); + let fq_hex = |value: &Fq| hex::encode(value.into_bigint().to_bytes_be()); + + // The two field elements the contract has to derive before it can call MAP_FP_TO_G1. This is + // the only part of the pipeline Solidity implements by hand, so it is the part worth pinning. + let field_hasher = as HashToField>::new(&[1u8]); + let u: Vec = field_hasher.hash_to_field(&preimage, 2); + + println!("=== BEEFY BLS hash-to-curve vector (BLS12-381 G1) ==="); + println!("dst 0x01"); + println!("suite {}", core::str::from_utf8(&suite).unwrap()); + println!("message {}", hex::encode(message)); + println!("preimage {}", hex::encode(&preimage)); + println!("u[0] {}", fq_hex(&u[0])); + println!("u[1] {}", fq_hex(&u[1])); + println!("point.x {}", fq_hex(x)); + println!("point.y {}", fq_hex(y)); + println!(); + println!( + "Solidity must: expand_message_xmd(preimage, 0x01, 128) -> 2 field elements," + ); + println!("MAP_FP_TO_G1 each, G1_ADD them. Cofactor clearing is linear, so it may be applied"); + println!("per point by the precompile or once at the end without changing the result."); +} + +/// The production path end to end: the prover assembles a BLS consensus proof and the verifier +/// accepts it, with no proof-building logic in the test itself. +/// +/// `test_bls_trustless_verify` above proves the same thing from first principles, rebuilding the +/// tree by hand so it fails loudly if the runtime's converter ever stops committing G2 keys. This +/// one exercises the API a relayer would actually call. +/// +/// RELAY_WS_URL=ws://127.0.0.1:9979 \ +/// cargo test -p beefy-verifier --features bls test_bls_consensus_via_prover -- --ignored +/// --nocapture +#[cfg(feature = "bls")] +#[tokio::test] +#[ignore] +async fn test_bls_consensus_via_prover() { + use beefy_prover::bls::decode_paired_justification; + + let max_rpc_payload_size = 15 * 1024 * 1024; + let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); + + let (relay_client, relay_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); + // Relay-only: the "para" client points at the same relay and `para_ids` is empty, so no + // parachain headers are proven. + let (para_client, para_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let para_rpc = LegacyRpcMethods::::new(para_rpc_client.clone()); + + let prover = Prover { + beefy_activation_block: 0, + relay: relay_client, + relay_rpc: relay_rpc.clone(), + relay_rpc_client: relay_rpc_client.clone(), + para: para_client, + para_rpc, + para_rpc_client, + para_ids: vec![], + query_batch_size: Some(100), + }; + + let engine_id = polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID; + let latest: H256 = + relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await.unwrap(); + + // Seed the trusted state from an earlier BEEFY-justified block, so the proof advances it. + let mut previous = H256::default(); + let mut cursor = latest; + for _ in 0..2000 { + let header = relay_rpc.chain_get_header(Some(cursor.into())).await.unwrap().unwrap(); + let parent: H256 = header.parent_hash.into(); + if parent.is_zero() { + panic!("reached genesis without a previous beefy block"); + } + let block = relay_rpc.chain_get_block(Some(parent.into())).await.unwrap().unwrap(); + if block + .justifications + .map(|js| js.iter().any(|j| j.0 == engine_id)) + .unwrap_or(false) + { + previous = parent; + break; + } + cursor = parent; + } + assert!(!previous.is_zero(), "no previous beefy block found"); + + let trusted_state = prover.get_initial_consensus_state(Some(previous)).await.unwrap(); + let trusted_height = trusted_state.latest_beefy_height; + + let latest_block = relay_rpc.chain_get_block(Some(latest.into())).await.unwrap().unwrap(); + let justification = latest_block + .justifications + .expect("latest beefy block must have justifications") + .into_iter() + .find_map(|j| (j.0 == engine_id).then_some(j.1)) + .expect("latest beefy block must have a beefy justification"); + + let signed_commitment = decode_paired_justification(&justification).unwrap(); + let block_number = signed_commitment.commitment.block_number; + let signer_count = signed_commitment.signatures.iter().filter(|s| s.is_some()).count(); + + let proof = prover.bls_consensus_proof(signed_commitment).await.unwrap(); + assert_eq!(proof.mmr.signers.len(), signer_count, "prover dropped signers"); + + let result = sp_io::TestExternalities::default() + .execute_with(|| verify_bls_consensus::(trusted_state, proof)); + + let (new_state, _headers) = result.expect("BLS consensus verification failed"); + let new_state = ConsensusState::decode(&mut &new_state[..]).unwrap(); + + assert_eq!(new_state.latest_beefy_height, block_number, "height was not advanced"); + assert!(new_state.latest_beefy_height > trusted_height, "state did not move forward"); + + println!( + "BLS consensus verified via the prover API: {signer_count} signers aggregated, \ + height {trusted_height} -> {block_number}" + ); +} diff --git a/modules/ismp/clients/beefy/Cargo.toml b/modules/ismp/clients/beefy/Cargo.toml index fb447975b..46ad7a5a0 100644 --- a/modules/ismp/clients/beefy/Cargo.toml +++ b/modules/ismp/clients/beefy/Cargo.toml @@ -24,6 +24,7 @@ features = [ [features] default = ["std"] +bls = ["beefy-verifier/bls-crypto"] std = [ "anyhow/std", "codec/std", diff --git a/modules/ismp/clients/beefy/src/consensus.rs b/modules/ismp/clients/beefy/src/consensus.rs index a0af6c4b1..8d390a3dc 100644 --- a/modules/ismp/clients/beefy/src/consensus.rs +++ b/modules/ismp/clients/beefy/src/consensus.rs @@ -15,6 +15,8 @@ use alloc::{boxed::Box, collections::BTreeMap, format, vec, vec::Vec}; use beefy_verifier::{error::Error as BeefyError, verify_consensus}; +#[cfg(feature = "bls")] +use beefy_verifier_primitives::PROOF_TYPE_BLS; use beefy_verifier_primitives::{ ConsensusMessage, ConsensusState, MmrProof, PROOF_TYPE_NAIVE, PROOF_TYPE_SP1, ParachainProof, Sp1BeefyProof, @@ -92,6 +94,13 @@ where .map_err(|e| BeefyError::DecodeNaiveProof(format!("{e:?}")))?; verify_consensus::(consensus_state, consensus_proof)? }, + #[cfg(feature = "bls")] + PROOF_TYPE_BLS => { + let bls_proof: beefy_verifier_primitives::BlsConsensusMessage = + codec::Decode::decode(&mut &payload[..]) + .map_err(|e| BeefyError::DecodeBlsProof(format!("{e:?}")))?; + beefy_verifier::verify_bls_consensus::(consensus_state, bls_proof)? + }, PROOF_TYPE_SP1 => { let sp1_proof: Sp1BeefyProof = codec::Decode::decode(&mut &payload[..]) .map_err(|e| BeefyError::DecodeSp1Proof(format!("{e:?}")))?; diff --git a/modules/ismp/clients/beefy/src/lib.rs b/modules/ismp/clients/beefy/src/lib.rs index 11a28f44b..97e0dc382 100644 --- a/modules/ismp/clients/beefy/src/lib.rs +++ b/modules/ismp/clients/beefy/src/lib.rs @@ -19,7 +19,7 @@ extern crate alloc; extern crate core; pub mod consensus; -pub use beefy_verifier_primitives::{PROOF_TYPE_NAIVE, PROOF_TYPE_SP1}; +pub use beefy_verifier_primitives::{PROOF_TYPE_BLS, PROOF_TYPE_NAIVE, PROOF_TYPE_SP1}; pub use consensus::{BEEFY_CONSENSUS_ID, BeefyConsensusClient}; use polkadot_sdk::*; @@ -40,6 +40,22 @@ impl beefy_verifier::EcdsaRecover for SubstrateCrypto { } } +/// Unlike `secp256k1_ecdsa_recover` above, this runs the pairing inside wasm rather than through a +/// host function. `sp-crypto-ec-utils` exposes `bls12_381_multi_miller_loop` and +/// `bls12_381_final_exponentiation`, but reaching them means working in `ark-bls12-381-ext` types +/// on a different arkworks version than `w3f-bls` pins, so it is a separate piece of work and +/// needs the host functions registered by every collator first. +#[cfg(feature = "bls")] +impl beefy_verifier::BlsAggregateVerify for SubstrateCrypto { + fn verify_aggregate( + message: &[u8], + signature: &[u8; beefy_verifier_primitives::BLS_G1_SIGNATURE_LEN], + public_keys: &[[u8; beefy_verifier_primitives::BLS_G2_PUBLIC_KEY_LEN]], + ) -> anyhow::Result { + beefy_verifier::bls::aggregate_verify(message, signature, public_keys) + } +} + /// Provides parachain tracking and SP1 vkey data to the BEEFY consensus client. pub trait BeefyClientConfig { /// Returns true if the given parachain id is tracked by this consensus client. @@ -52,5 +68,10 @@ pub trait BeefyClientConfig { /// accept. On mainnet set to `&[PROOF_TYPE_SP1]`, on testnets set to /// `&[PROOF_TYPE_NAIVE, PROOF_TYPE_SP1]`. A proof whose type byte is not listed is /// rejected with [`beefy_verifier::error::Error::UnknownProofType`] before verification. + /// + /// [`PROOF_TYPE_BLS`] additionally requires this crate's `bls` feature, and a relay chain + /// whose keyset commitment is over BLS public keys rather than Ethereum addresses. The two + /// commitments are mutually exclusive, so a client cannot accept both that and the ECDSA + /// proof types against the same consensus state. fn allowed_proof_types() -> &'static [u8]; } From fc41d2a8b00a9371c330f6a78caf5b4e05660843 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 4 Aug 2026 13:35:26 +0100 Subject: [PATCH 03/48] accept aggregate bls beefy proofs through the abi submit path --- evm/rust/abi/BlsBeefy.json | 244 ++++++++++++++ evm/rust/src/conversions.rs | 221 ++++++++++++- evm/rust/src/generated/bls_beefy.rs | 24 ++ evm/rust/src/generated/mod.rs | 1 + evm/src/consensus/BlsBeefy.sol | 43 +++ evm/src/consensus/Types.sol | 40 +++ modules/consensus/beefy/prover/Cargo.toml | 9 +- modules/consensus/beefy/prover/src/lib.rs | 2 +- modules/consensus/beefy/verifier/Cargo.toml | 2 +- modules/consensus/beefy/verifier/src/test.rs | 309 ++++++++++++++++++ .../pallets/beefy-consensus-proofs/src/lib.rs | 14 +- .../beefy-consensus-proofs/src/types.rs | 2 + parachain/runtimes/gargantua/Cargo.toml | 4 +- parachain/runtimes/gargantua/src/ismp.rs | 8 +- parachain/simtests/Cargo.toml | 4 +- parachain/simtests/src/lib.rs | 1 + parachain/simtests/src/pallet_beefy_bls.rs | 199 +++++++++++ .../src/pallet_beefy_consensus_proofs.rs | 8 +- 18 files changed, 1119 insertions(+), 16 deletions(-) create mode 100644 evm/rust/abi/BlsBeefy.json create mode 100644 evm/rust/src/generated/bls_beefy.rs create mode 100644 evm/src/consensus/BlsBeefy.sol create mode 100644 parachain/simtests/src/pallet_beefy_bls.rs diff --git a/evm/rust/abi/BlsBeefy.json b/evm/rust/abi/BlsBeefy.json new file mode 100644 index 000000000..aad0e079c --- /dev/null +++ b/evm/rust/abi/BlsBeefy.json @@ -0,0 +1,244 @@ +[ + { + "type": "function", + "name": "noOp", + "inputs": [ + { + "name": "s", + "type": "tuple", + "internalType": "struct BeefyConsensusState", + "components": [ + { + "name": "latestHeight", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "beefyActivationBlock", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "currentAuthoritySet", + "type": "tuple", + "internalType": "struct AuthoritySetCommitment", + "components": [ + { + "name": "id", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "len", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "root", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "name": "nextAuthoritySet", + "type": "tuple", + "internalType": "struct AuthoritySetCommitment", + "components": [ + { + "name": "id", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "len", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "root", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } + ] + }, + { + "name": "p", + "type": "tuple", + "internalType": "struct BlsBeefyConsensusProof", + "components": [ + { + "name": "relay", + "type": "tuple", + "internalType": "struct BlsRelayChainProof", + "components": [ + { + "name": "commitment", + "type": "tuple", + "internalType": "struct Commitment", + "components": [ + { + "name": "payload", + "type": "tuple[]", + "internalType": "struct Payload[]", + "components": [ + { + "name": "id", + "type": "bytes2", + "internalType": "bytes2" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "blockNumber", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "validatorSetId", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "name": "signers", + "type": "tuple[]", + "internalType": "struct BlsSigner[]", + "components": [ + { + "name": "publicKey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "authorityIndex", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "aggregateSignature", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "latestMmrLeaf", + "type": "tuple", + "internalType": "struct BeefyMmrLeaf", + "components": [ + { + "name": "version", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "parentNumber", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "parentHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "nextAuthoritySet", + "type": "tuple", + "internalType": "struct AuthoritySetCommitment", + "components": [ + { + "name": "id", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "len", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "root", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "name": "extra", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "leafIndex", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "mmrProof", + "type": "bytes32[]", + "internalType": "bytes32[]" + }, + { + "name": "proof", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ] + }, + { + "name": "parachain", + "type": "tuple", + "internalType": "struct ParachainProof", + "components": [ + { + "name": "parachains", + "type": "tuple[]", + "internalType": "struct Parachain[]", + "components": [ + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "id", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "header", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "proof", + "type": "bytes32[]", + "internalType": "bytes32[]" + }, + { + "name": "leafCount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ] + } + ], + "outputs": [], + "stateMutability": "pure" + } +] diff --git a/evm/rust/src/conversions.rs b/evm/rust/src/conversions.rs index 312c96201..7f413a764 100644 --- a/evm/rust/src/conversions.rs +++ b/evm/rust/src/conversions.rs @@ -56,9 +56,10 @@ mod beefy { use alloc::{vec, vec::Vec}; use alloy_primitives::{Bytes, FixedBytes}; use beefy_verifier_primitives::{ - ConsensusMessage, ConsensusState, MmrProof, ParachainHeader as BvpParachainHeader, - ParachainProof as BvpParachainProof, SignatureWithAuthorityIndex, - SignedCommitment as BvpSignedCommitment, Sp1BeefyProof, TSignature, + BlsConsensusMessage, BlsMmrProof, BlsSigner, ConsensusMessage, ConsensusState, MmrProof, + ParachainHeader as BvpParachainHeader, ParachainProof as BvpParachainProof, + SignatureWithAuthorityIndex, SignedCommitment as BvpSignedCommitment, Sp1BeefyProof, + TSignature, }; use polkadot_sdk::*; use primitive_types::H256; @@ -393,6 +394,220 @@ mod beefy { } } + // `sol!` emits a distinct set of Rust types per binding, so the shared BEEFY structs appear + // again under `BlsBeefy` even though the Solidity definitions are the same ones. These bridge + // those duplicates onto the `Beefy` types so the conversions to the SCALE primitives stay + // single-sourced above, rather than being written out a second time. + // + // `BlsBeefy.sol` cannot simply reuse the `EcdsaBeefy` artifact: that contract is deployed, and + // adding the BLS structs to its `noOp` would change its ABI and bytecode. + mod bls_bridge { + use super::*; + use crate::bls_beefy::BlsBeefy; + + impl From for Payload { + fn from(value: BlsBeefy::Payload) -> Self { + Payload { id: value.id, data: value.data } + } + } + + impl From for Commitment { + fn from(value: BlsBeefy::Commitment) -> Self { + Commitment { + payload: value.payload.into_iter().map(Into::into).collect(), + blockNumber: value.blockNumber, + validatorSetId: value.validatorSetId, + } + } + } + + impl From for AuthoritySetCommitment { + fn from(value: BlsBeefy::AuthoritySetCommitment) -> Self { + AuthoritySetCommitment { id: value.id, len: value.len, root: value.root } + } + } + + impl From for BeefyMmrLeaf { + fn from(value: BlsBeefy::BeefyMmrLeaf) -> Self { + BeefyMmrLeaf { + version: value.version, + parentNumber: value.parentNumber, + parentHash: value.parentHash, + nextAuthoritySet: value.nextAuthoritySet.into(), + extra: value.extra, + leafIndex: value.leafIndex, + } + } + } + + impl From for Parachain { + fn from(value: BlsBeefy::Parachain) -> Self { + Parachain { index: value.index, id: value.id, header: value.header } + } + } + + impl From for ParachainProof { + fn from(value: BlsBeefy::ParachainProof) -> Self { + ParachainProof { + parachains: value.parachains.into_iter().map(Into::into).collect(), + proof: value.proof, + leafCount: value.leafCount, + } + } + } + + // The same bridges the other way, so a prover can build the payload it submits. + impl From for BlsBeefy::Payload { + fn from(value: Payload) -> Self { + BlsBeefy::Payload { id: value.id, data: value.data } + } + } + + impl From for BlsBeefy::Commitment { + fn from(value: Commitment) -> Self { + BlsBeefy::Commitment { + payload: value.payload.into_iter().map(Into::into).collect(), + blockNumber: value.blockNumber, + validatorSetId: value.validatorSetId, + } + } + } + + impl From for BlsBeefy::AuthoritySetCommitment { + fn from(value: AuthoritySetCommitment) -> Self { + BlsBeefy::AuthoritySetCommitment { id: value.id, len: value.len, root: value.root } + } + } + + impl From for BlsBeefy::BeefyMmrLeaf { + fn from(value: BeefyMmrLeaf) -> Self { + BlsBeefy::BeefyMmrLeaf { + version: value.version, + parentNumber: value.parentNumber, + parentHash: value.parentHash, + nextAuthoritySet: value.nextAuthoritySet.into(), + extra: value.extra, + leafIndex: value.leafIndex, + } + } + } + + impl From for BlsBeefy::Parachain { + fn from(value: Parachain) -> Self { + BlsBeefy::Parachain { index: value.index, id: value.id, header: value.header } + } + } + + impl From for BlsBeefy::ParachainProof { + fn from(value: ParachainProof) -> Self { + BlsBeefy::ParachainProof { + parachains: value.parachains.into_iter().map(Into::into).collect(), + proof: value.proof, + leafCount: value.leafCount, + } + } + } + } + + impl From for BlsMmrProof { + fn from(value: crate::bls_beefy::BlsBeefy::BlsRelayChainProof) -> Self { + let leaf: BeefyMmrLeaf = value.latestMmrLeaf.into(); + let leaf_index: u64 = leaf.leafIndex.try_into().expect("mmr leaf index out of bounds"); + let mmr_proof = LeafProof { + leaf_indices: vec![leaf_index], + leaf_count: leaf_index.saturating_add(1), + items: value.mmrProof.into_iter().map(|h| H256(h.0)).collect(), + }; + + let signers = value + .signers + .into_iter() + .map(|signer| BlsSigner { + public_key: signer + .publicKey + .as_ref() + .try_into() + .expect("BLS public key should be 96 bytes"), + index: signer.authorityIndex.try_into().expect("authority index out of bounds"), + }) + .collect(); + + let commitment: Commitment = value.commitment.into(); + + BlsMmrProof { + commitment: commitment.into(), + signers, + aggregate_signature: value + .aggregateSignature + .as_ref() + .try_into() + .expect("aggregate BLS signature should be 48 bytes"), + latest_mmr_leaf: leaf.into(), + mmr_proof, + authority_proof: value.proof.into_iter().map(|h| h.0).collect(), + } + } + } + + impl From for crate::bls_beefy::BlsBeefy::BlsRelayChainProof { + fn from(value: BlsMmrProof) -> Self { + use crate::bls_beefy::BlsBeefy; + + let leaf_index = value.mmr_proof.leaf_indices[0]; + let commitment: Commitment = value.commitment.into(); + let leaf = BeefyMmrLeaf { + version: 0, + parentNumber: value.latest_mmr_leaf.parent_number_and_hash.0, + parentHash: FixedBytes::from(value.latest_mmr_leaf.parent_number_and_hash.1 .0), + nextAuthoritySet: value.latest_mmr_leaf.beefy_next_authority_set.into(), + extra: FixedBytes::from(value.latest_mmr_leaf.leaf_extra.0), + leafIndex: leaf_index.to_u256(), + }; + + BlsBeefy::BlsRelayChainProof { + commitment: commitment.into(), + signers: value + .signers + .into_iter() + .map(|signer| BlsBeefy::BlsSigner { + publicKey: Bytes::from(signer.public_key.to_vec()), + authorityIndex: signer.index.to_u256(), + }) + .collect(), + aggregateSignature: Bytes::from(value.aggregate_signature.to_vec()), + latestMmrLeaf: leaf.into(), + mmrProof: value + .mmr_proof + .items + .into_iter() + .map(|h| FixedBytes::from(h.0)) + .collect(), + proof: value.authority_proof.into_iter().map(FixedBytes::from).collect(), + } + } + } + + impl From for crate::bls_beefy::BlsBeefy::BlsBeefyConsensusProof { + fn from(value: BlsConsensusMessage) -> Self { + let parachain: ParachainProof = value.parachain.into(); + + crate::bls_beefy::BlsBeefy::BlsBeefyConsensusProof { + relay: value.mmr.into(), + parachain: parachain.into(), + } + } + } + + impl From for BlsConsensusMessage { + fn from(value: crate::bls_beefy::BlsBeefy::BlsBeefyConsensusProof) -> Self { + // Two hops: the duplicate `BlsBeefy` struct bridges onto the `Beefy` one, which + // already knows how to become the SCALE primitive. `into()` will not chain these. + let parachain: ParachainProof = value.parachain.into(); + + BlsConsensusMessage { mmr: value.relay.into(), parachain: parachain.into() } + } + } + impl From for Sp1BeefyProof { fn from(value: crate::sp1_beefy::SP1Beefy::SP1BeefyProof) -> Self { Sp1BeefyProof { diff --git a/evm/rust/src/generated/bls_beefy.rs b/evm/rust/src/generated/bls_beefy.rs new file mode 100644 index 000000000..a229c5047 --- /dev/null +++ b/evm/rust/src/generated/bls_beefy.rs @@ -0,0 +1,24 @@ +//! Aggregate BLS BEEFY contract bindings generated with alloy sol! macro. +//! +//! See [`crate::generated::ecdsa_beefy`] for why the two `sol!` invocations are picked between +//! with `#[cfg]` rather than `cfg_attr`. + +use alloy_sol_macro::sol; + +#[cfg(feature = "std")] +sol!( + #[allow(missing_docs)] + #[sol(rpc, ignore_unlinked)] + #[derive(Debug, PartialEq, Eq)] + BlsBeefy, + "abi/BlsBeefy.json" +); + +#[cfg(not(feature = "std"))] +sol!( + #[allow(missing_docs)] + #[sol(ignore_unlinked)] + #[derive(Debug, PartialEq, Eq)] + BlsBeefy, + "abi/BlsBeefy.json" +); diff --git a/evm/rust/src/generated/mod.rs b/evm/rust/src/generated/mod.rs index 6f8168c38..eeec93e82 100644 --- a/evm/rust/src/generated/mod.rs +++ b/evm/rust/src/generated/mod.rs @@ -10,6 +10,7 @@ //! which is what substrate pallets consume. pub mod bandwidth_manager; +pub mod bls_beefy; pub mod ecdsa_beefy; pub mod erc20; pub mod evm_host; diff --git a/evm/src/consensus/BlsBeefy.sol b/evm/src/consensus/BlsBeefy.sol new file mode 100644 index 000000000..d9571872f --- /dev/null +++ b/evm/src/consensus/BlsBeefy.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) Polytope Labs Ltd. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pragma solidity ^0.8.17; + +import {BlsBeefyConsensusProof, BeefyConsensusState} from "./Types.sol"; + +/** + * @title The aggregate BLS12-381 BEEFY consensus proof types. + * @author Polytope Labs (hello@polytope.technology) + * + * @notice Carries the ABI shape of a BLS BEEFY proof. The verifier itself is not implemented here + * yet; the Rust light client verifies these proofs today, and this exists so both sides agree on + * the encoding before the on-chain verifier is written. + * + * @dev When the verifier lands it verifies the aggregate in one pairing check regardless of how + * many validators signed, which is the point of the BLS path. The steps will be: + * 1. Hash the SCALE-encoded commitment onto G1. Note BEEFY does not use the IETF ciphersuite + * string as the domain separation tag: `w3f-bls` builds its hasher with a one-byte DST of + * 0x01 and prepends the ciphersuite to the message instead. Reproducing that exactly is the + * make-or-break step, and `bls_hash_to_curve_vector` in the Rust verifier pins a test vector. + * 2. Sum the signers' G2 public keys and check the aggregate against the keyset commitment with + * a merkle multi-proof, rejecting repeated or unordered signer indices. + * 3. One EIP-2537 pairing check over the summed key and the aggregate signature. + * + * Signatures live in G1 (48 bytes) and public keys in G2 (96 bytes), the opposite of the Ethereum + * convention, so an eth2 BLS library does not transfer. + */ +contract BlsBeefy { + // @dev so these structs are included in the abi + function noOp(BeefyConsensusState memory s, BlsBeefyConsensusProof memory p) external pure {} +} diff --git a/evm/src/consensus/Types.sol b/evm/src/consensus/Types.sol index fcb3d025e..663ed599a 100644 --- a/evm/src/consensus/Types.sol +++ b/evm/src/consensus/Types.sol @@ -173,6 +173,46 @@ struct BeefyConsensusProof { ParachainProof parachain; } +// A validator that contributed to an aggregate BLS12-381 signature. +// +// Only the public key travels. The individual signatures are summed by the prover into +// BlsRelayChainProof.aggregateSignature, since verification never needs them apart. +struct BlsSigner { + // Compressed BLS12-381 G2 public key, 96 bytes. Note BEEFY puts public keys in G2 and + // signatures in G1, the opposite of the Ethereum convention. + bytes publicKey; + // 0-based index of the authority in the authority set + uint256 authorityIndex; +} + +struct BlsRelayChainProof { + // A commitment to the finalized state + Commitment commitment; + // The validators that signed, in strictly ascending index order + BlsSigner[] signers; + // Sum of the signers' signatures as a compressed BLS12-381 G1 point, 48 bytes + bytes aggregateSignature; + // Latest leaf added to mmr + BeefyMmrLeaf latestMmrLeaf; + // Proof for the latest mmr leaf + bytes32[] mmrProof; + // Proof for the signing authorities against the keyset commitment + bytes32[] proof; +} + +// A BEEFY consensus proof verified by a single aggregate BLS signature rather than by recovering +// each authority's ECDSA signature. +// +// This requires a relay chain whose keyset commitment is over BLS public keys. A chain committing +// ECDSA-derived addresses cannot be verified this way, and vice versa, so the two are separate +// consensus states. +struct BlsBeefyConsensusProof { + // The proof items for the relay chain consensus + BlsRelayChainProof relay; + // Proof items for parachain headers + ParachainProof parachain; +} + struct DigestItem { bytes4 consensusId; bytes data; diff --git a/modules/consensus/beefy/prover/Cargo.toml b/modules/consensus/beefy/prover/Cargo.toml index 4833ae859..971a25945 100644 --- a/modules/consensus/beefy/prover/Cargo.toml +++ b/modules/consensus/beefy/prover/Cargo.toml @@ -55,4 +55,11 @@ local = [] # key type. On the wire such a chain carries 177-byte paired signatures and 177-byte authority # public keys; with this feature the prover keeps only the ECDSA half of each (the keccak-ECDSA # recoverable signature / compressed secp256k1 key), so the existing ECDSA verifier is unchanged. -bls = ["dep:w3f-bls"] +# +# This changes how `beefy_authorities` and `decode_beefy_justification` read the wire, so it must +# not be enabled when proving a plain-ECDSA relay. It is deliberately separate from +# `bls-aggregate`, which only adds code. +bls = [] +# Prove the BLS half instead: `crate::bls`, which builds a proof the aggregate verifier checks in +# a single pairing. Purely additive, so it is safe to enable alongside plain-ECDSA proving. +bls-aggregate = ["dep:w3f-bls"] diff --git a/modules/consensus/beefy/prover/src/lib.rs b/modules/consensus/beefy/prover/src/lib.rs index 52b9ddf20..ad5da352c 100644 --- a/modules/consensus/beefy/prover/src/lib.rs +++ b/modules/consensus/beefy/prover/src/lib.rs @@ -49,7 +49,7 @@ use relay::{ use util::hash_authority_addresses; /// Proving commitments signed with aggregate BLS12-381 -#[cfg(feature = "bls")] +#[cfg(feature = "bls-aggregate")] pub mod bls; /// Methods for querying the relay chain pub mod relay; diff --git a/modules/consensus/beefy/verifier/Cargo.toml b/modules/consensus/beefy/verifier/Cargo.toml index db82389c5..534925230 100644 --- a/modules/consensus/beefy/verifier/Cargo.toml +++ b/modules/consensus/beefy/verifier/Cargo.toml @@ -65,7 +65,7 @@ default = ["std"] bls-crypto = ["dep:w3f-bls"] # Runs the BLS-BEEFY tests against a relay whose BEEFY authorities are paired ecdsa_bls_crypto # keys. Pulls the prover's `bls` decode path, which reads 177-byte paired signatures and keys. -bls = ["beefy-prover/bls", "bls-crypto"] +bls = ["beefy-prover/bls", "beefy-prover/bls-aggregate", "bls-crypto"] # Storage keys for a relay that names its beefy-mmr pallet `MmrLeaf` rather than `BeefyMmrLeaf`. # Westend (and Polkadot/Kusama) use `BeefyMmrLeaf`, so leave this off for them; our rococo fork # used `MmrLeaf`, so pair it with `bls` as `--features bls,local` when testing against that chain. diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index a550ec1f7..2ab9e5bed 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -1048,3 +1048,312 @@ async fn test_bls_consensus_via_prover() { height {trusted_height} -> {block_number}" ); } + +/// Offline coverage for the aggregate BLS path. +/// +/// The BLS integration tests above all need a live relay chain, so none of them run in CI. These +/// build a validator set from deterministic seeds and exercise the verifier directly, including +/// the rejection paths, which is where the interesting behaviour lives. +#[cfg(feature = "bls-crypto")] +mod bls_offline { + use super::*; + use beefy_verifier_primitives::{ + BLS_G1_SIGNATURE_LEN, BLS_G2_PUBLIC_KEY_LEN, BlsMmrProof, BlsSigner, + }; + use w3f_bls::{ + EngineBLS, Message, SecretKeyVT, SerializableToBytes, Signature as BlsSignature, TinyBLS381, + }; + + const SET_ID: ValidatorSetId = 7; + const BLOCK: u32 = 100; + + type Validator = (SecretKeyVT, [u8; BLS_G2_PUBLIC_KEY_LEN]); + + /// Deterministic validators, so failures reproduce. + fn validators(count: usize) -> Vec { + (0..count) + .map(|i| { + let secret = SecretKeyVT::::from_seed(&[b'v', i as u8]); + let public = secret.into_public().to_bytes(); + (secret, public.try_into().expect("G2 public key is 96 bytes")) + }) + .collect() + } + + fn aggregate(signatures: &[BlsSignature]) -> [u8; BLS_G1_SIGNATURE_LEN] { + let mut sum: Option<::SignatureGroup> = None; + for signature in signatures { + sum = Some(sum.map_or(signature.0, |acc| acc + signature.0)); + } + BlsSignature::(sum.expect("no signatures to aggregate")) + .to_bytes() + .try_into() + .expect("G1 signature is 48 bytes") + } + + /// A trusted state and a proof over `signer_indices`, both internally consistent. + /// + /// The MMR is a single leaf, so its root is just the leaf hash and an empty proof verifies. + /// That lets the happy path run offline rather than only against a chain. + fn valid_proof( + validators: &[Validator], + signer_indices: &[usize], + ) -> (ConsensusState, BlsMmrProof) { + let leaf = MmrLeaf { + version: MmrLeafVersion::new(0, 0), + parent_number_and_hash: (BLOCK - 1, H256::zero()), + beefy_next_authority_set: BeefyNextAuthoritySet { + id: SET_ID + 1, + len: validators.len() as u32, + keyset_commitment: H256::zero(), + }, + leaf_extra: H256::zero(), + }; + let mmr_root = H256(keccak_256(&leaf.encode())); + + let payload = Payload::from_single_entry(*b"mh", mmr_root.0.to_vec()); + let commitment = Commitment { payload, block_number: BLOCK, validator_set_id: SET_ID }; + + // The keyset commitment is the merkle root over the hashed G2 keys, matching the runtime's + // converter. + let leaves = validators.iter().map(|(_, key)| keccak_256(key)).collect::>(); + let tree = MerkleTree::::from_leaves(&leaves); + let keyset_commitment = H256(tree.root().expect("keyset tree has a root")); + let authority_proof = tree.proof(signer_indices).proof_hashes().to_vec(); + + let message = Message::new(b"", &commitment.encode()); + let signatures = signer_indices + .iter() + .map(|&i| validators[i].0.sign(&message)) + .collect::>(); + + let signers = signer_indices + .iter() + .map(|&i| BlsSigner { public_key: validators[i].1, index: i as u32 }) + .collect::>(); + + let trusted_state = ConsensusState { + latest_beefy_height: BLOCK - 1, + beefy_activation_block: 0, + mmr_root_hash: H256::zero(), + current_authorities: BeefyAuthoritySet { + id: SET_ID, + len: validators.len() as u32, + keyset_commitment, + }, + next_authorities: BeefyAuthoritySet { + id: SET_ID + 1, + len: validators.len() as u32, + keyset_commitment: H256::zero(), + }, + }; + + let proof = BlsMmrProof { + commitment, + signers, + aggregate_signature: aggregate(&signatures), + latest_mmr_leaf: leaf, + mmr_proof: LeafProof { leaf_indices: vec![0], leaf_count: 1, items: vec![] }, + authority_proof, + }; + + (trusted_state, proof) + } + + fn verify( + trusted_state: ConsensusState, + proof: BlsMmrProof, + ) -> Result<(ConsensusState, H256), Error> { + sp_io::TestExternalities::default() + .execute_with(|| crate::verify_bls_mmr_update_proof::(trusted_state, proof)) + } + + #[test] + fn accepts_a_valid_aggregate() { + let validators = validators(4); + let (trusted_state, proof) = valid_proof(&validators, &[0, 1, 2, 3]); + + let (new_state, _leaf_extra) = verify(trusted_state, proof).expect("should verify"); + + assert_eq!(new_state.latest_beefy_height, BLOCK, "height should advance to the commitment"); + } + + // Three of four is the supermajority, so a partial set must still verify. The aggregate is + // over the signers alone, which is what makes the merkle multi-proof necessary. + #[test] + fn accepts_a_supermajority_subset() { + let validators = validators(4); + let (trusted_state, proof) = valid_proof(&validators, &[0, 1, 3]); + + assert!(verify(trusted_state, proof).is_ok()); + } + + // The check that stops a prover claiming one validator many times. Without it, a single + // signer's key and signature could be repeated to clear the supermajority threshold, and BLS + // aggregation would happily verify the repeated key against the repeated signature. + #[test] + fn rejects_duplicate_signer_indices() { + let validators = validators(4); + let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); + + // One real signer, counted three times. The signature is genuinely the sum of three + // copies of validator 0's signature, so the pairing check itself would pass. + let message = Message::new(b"", &proof.commitment.encode()); + let signature = validators[0].0.sign(&message); + proof.aggregate_signature = + aggregate(&[validators[0].0.sign(&message), validators[0].0.sign(&message), signature]); + for signer in proof.signers.iter_mut() { + signer.public_key = validators[0].1; + signer.index = 0; + } + + assert!(matches!(verify(trusted_state, proof), Err(Error::InvalidBlsSignerOrdering))); + } + + #[test] + fn rejects_unordered_signer_indices() { + let validators = validators(4); + let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); + proof.signers.swap(0, 2); + + assert!(matches!(verify(trusted_state, proof), Err(Error::InvalidBlsSignerOrdering))); + } + + #[test] + fn rejects_signer_outside_the_authority_set() { + let validators = validators(4); + let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); + proof.signers.last_mut().unwrap().index = 9; + + assert!(matches!(verify(trusted_state, proof), Err(Error::InvalidBlsSignerOrdering))); + } + + #[test] + fn rejects_sub_supermajority() { + let validators = validators(4); + // Two of four is short of the >2/3 threshold. + let (trusted_state, proof) = valid_proof(&validators, &[0, 1]); + + assert!(matches!(verify(trusted_state, proof), Err(Error::SuperMajorityRequired))); + } + + #[test] + fn rejects_a_proof_with_no_signers() { + let validators = validators(4); + let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); + proof.signers.clear(); + + assert!(matches!(verify(trusted_state, proof), Err(Error::NoBlsSigners))); + } + + #[test] + fn rejects_a_stale_commitment() { + let validators = validators(4); + let (mut trusted_state, proof) = valid_proof(&validators, &[0, 1, 2]); + trusted_state.latest_beefy_height = BLOCK; + + assert!(matches!(verify(trusted_state, proof), Err(Error::StaleHeight { .. }))); + } + + #[test] + fn rejects_an_unknown_authority_set() { + let validators = validators(4); + let (mut trusted_state, proof) = valid_proof(&validators, &[0, 1, 2]); + trusted_state.current_authorities.id = SET_ID + 5; + trusted_state.next_authorities.id = SET_ID + 6; + + assert!(matches!(verify(trusted_state, proof), Err(Error::UnknownAuthoritySet { .. }))); + } + + // A signature over a different commitment: well formed points, wrong message. + #[test] + fn rejects_an_aggregate_over_the_wrong_message() { + let validators = validators(4); + let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); + + let other = Message::new(b"", b"a different commitment"); + proof.aggregate_signature = aggregate(&[ + validators[0].0.sign(&other), + validators[1].0.sign(&other), + validators[2].0.sign(&other), + ]); + + assert!(matches!(verify(trusted_state, proof), Err(Error::BlsVerificationFailed))); + } + + // Dropping a signer from the aggregate while leaving their key in the proof must not verify, + // or a validator could be credited with a signature they never produced. + #[test] + fn rejects_an_aggregate_missing_a_claimed_signer() { + let validators = validators(4); + let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); + + let message = Message::new(b"", &proof.commitment.encode()); + proof.aggregate_signature = + aggregate(&[validators[0].0.sign(&message), validators[1].0.sign(&message)]); + + assert!(matches!(verify(trusted_state, proof), Err(Error::BlsVerificationFailed))); + } + + // All-ones bytes are not undecodable. Arkworks keeps the point flags in the high bits of the + // last byte, and 0xff sets the infinity flag, so these decode to the identity element instead + // of failing. The identity is harmless (it contributes nothing to either sum, and a signer + // still has to appear in the committed keyset), but the rejection therefore arrives as a + // failed pairing rather than a decode error. + #[test] + fn rejects_an_identity_public_key() { + let validators = validators(4); + let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); + proof.signers[1].public_key = [0xff; BLS_G2_PUBLIC_KEY_LEN]; + + let result = verify(trusted_state, proof); + assert!(matches!(result, Err(Error::BlsVerificationFailed)), "got {result:?}"); + } + + #[test] + fn rejects_an_identity_signature() { + let validators = validators(4); + let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); + proof.aggregate_signature = [0xff; BLS_G1_SIGNATURE_LEN]; + + let result = verify(trusted_state, proof); + assert!(matches!(result, Err(Error::BlsVerificationFailed)), "got {result:?}"); + } + + // Corrupting the coordinate while leaving the flag bits alone does fail to decode, which is + // the path that reports `InvalidBlsPoint`. + #[test] + fn rejects_an_undecodable_public_key() { + let validators = validators(4); + let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); + proof.signers[1].public_key[0] ^= 0xff; + + let result = verify(trusted_state, proof); + assert!(matches!(result, Err(Error::InvalidBlsPoint)), "got {result:?}"); + } + + // Correctly signed by keys that simply are not the committed authority set. The pairing check + // passes; only the merkle multi-proof catches it. + #[test] + fn rejects_signers_outside_the_committed_keyset() { + let committed = validators(4); + let impostors = (0..4) + .map(|i| { + let secret = SecretKeyVT::::from_seed(&[b'x', i as u8]); + let public = secret.into_public().to_bytes(); + (secret, public.try_into().expect("G2 public key is 96 bytes")) + }) + .collect::>(); + + let (trusted_state, mut proof) = valid_proof(&impostors, &[0, 1, 2]); + // Keep the impostors' signatures and keys, but point the state at the real keyset. + let leaves = committed.iter().map(|(_, key)| keccak_256(key)).collect::>(); + let tree = MerkleTree::::from_leaves(&leaves); + let mut trusted_state = trusted_state; + trusted_state.current_authorities.keyset_commitment = + H256(tree.root().expect("keyset tree has a root")); + proof.authority_proof = tree.proof(&[0, 1, 2]).proof_hashes().to_vec(); + + assert!(matches!(verify(trusted_state, proof), Err(Error::InvalidAuthoritiesProof))); + } +} diff --git a/modules/pallets/beefy-consensus-proofs/src/lib.rs b/modules/pallets/beefy-consensus-proofs/src/lib.rs index cacdc0407..176b37af0 100644 --- a/modules/pallets/beefy-consensus-proofs/src/lib.rs +++ b/modules/pallets/beefy-consensus-proofs/src/lib.rs @@ -480,7 +480,10 @@ pub mod pallet { } Some(nonce) }, - types::PROOF_TYPE_NAIVE => None, + // Only SP1 proofs are bound to a prover account. The naive and BLS paths verify + // signatures the relay chain's validators produced, so there is nothing + // prover-specific in them to bind and no anti-theft gate to apply. + types::PROOF_TYPE_NAIVE | types::PROOF_TYPE_BLS => None, _ => Err(Error::::UnknownProofType)?, }; @@ -834,6 +837,15 @@ pub mod pallet { let scale_proof: beefy_verifier_primitives::ConsensusMessage = abi_proof.into(); [&[types::PROOF_TYPE_NAIVE], scale_proof.encode().as_slice()].concat() }, + types::PROOF_TYPE_BLS => { + let abi_proof = ::abi_decode_params( + abi_payload, + ) + .map_err(|_| Error::::AbiDecodeFailed)?; + let scale_proof: beefy_verifier_primitives::BlsConsensusMessage = + abi_proof.into(); + [&[types::PROOF_TYPE_BLS], scale_proof.encode().as_slice()].concat() + }, _ => Err(Error::::UnknownProofType)?, }; diff --git a/modules/pallets/beefy-consensus-proofs/src/types.rs b/modules/pallets/beefy-consensus-proofs/src/types.rs index 5c4e1e4d0..945a20700 100644 --- a/modules/pallets/beefy-consensus-proofs/src/types.rs +++ b/modules/pallets/beefy-consensus-proofs/src/types.rs @@ -34,6 +34,8 @@ pub const ROTATION_OFFCHAIN_PREFIX: &[u8] = b"beefy_consensus_proofs::rotation:: pub const PROOF_TYPE_NAIVE: u8 = 0x00; /// Proof type byte: SP1 ZK BEEFY proof. pub const PROOF_TYPE_SP1: u8 = 0x01; +/// Proof type byte: aggregate BLS12-381 BEEFY proof. +pub const PROOF_TYPE_BLS: u8 = 0x02; fn offchain_key(prefix: &[u8], id: u64) -> Vec { let mut key = Vec::with_capacity(prefix.len() + 8); diff --git a/parachain/runtimes/gargantua/Cargo.toml b/parachain/runtimes/gargantua/Cargo.toml index d4dc51a2b..6fdf24301 100644 --- a/parachain/runtimes/gargantua/Cargo.toml +++ b/parachain/runtimes/gargantua/Cargo.toml @@ -33,7 +33,9 @@ ismp-sync-committee = { workspace = true } ismp-bsc = { workspace = true } ismp-parachain = { workspace = true } ismp-grandpa = { workspace = true } -ismp-beefy = { workspace = true } +# `bls` accepts aggregate BLS12-381 BEEFY proofs. Gargantua is the testnet runtime, so it carries +# the BLS verifier while the measurements that decide whether it belongs on nexus are outstanding. +ismp-beefy = { workspace = true, features = ["bls"] } ismp-parachain-runtime-api = { workspace = true } pallet-ismp-relayer = { workspace = true } pallet-ismp-host-executive = { workspace = true } diff --git a/parachain/runtimes/gargantua/src/ismp.rs b/parachain/runtimes/gargantua/src/ismp.rs index 4c45642d4..734fda3d4 100644 --- a/parachain/runtimes/gargantua/src/ismp.rs +++ b/parachain/runtimes/gargantua/src/ismp.rs @@ -242,8 +242,8 @@ impl ismp_beefy::BeefyClientConfig for Runtime { } fn allowed_proof_types() -> &'static [u8] { - // Testnet: accept both the naive ECDSA and SP1 ZK proof formats. - &[ismp_beefy::PROOF_TYPE_NAIVE, ismp_beefy::PROOF_TYPE_SP1] + // Testnet: accept the naive ECDSA and SP1 ZK proof formats, plus aggregate BLS12-381. + &[ismp_beefy::PROOF_TYPE_NAIVE, ismp_beefy::PROOF_TYPE_SP1, ismp_beefy::PROOF_TYPE_BLS] } } @@ -304,7 +304,9 @@ pub struct HftBenchmarkHelper; #[cfg(feature = "runtime-benchmarks")] impl pallet_hyper_fungible_token::types::BenchmarkHelper for HftBenchmarkHelper { fn create_asset(decimals: u8, who: &AccountId, amount: u128) -> H256 { - use frame_support::traits::fungibles::{metadata::Mutate as MutateMetadata, Create, Mutate}; + use frame_support::traits::fungibles::{ + metadata::Mutate as MutateMetadata, Create, Mutate, + }; let asset_id: H256 = sp_io::hashing::keccak_256(b"HFT_BENCHMARK_ASSET").into(); >::create(asset_id, who.clone(), true, 1) diff --git a/parachain/simtests/Cargo.toml b/parachain/simtests/Cargo.toml index ef0039f20..9b2c1a1ce 100644 --- a/parachain/simtests/Cargo.toml +++ b/parachain/simtests/Cargo.toml @@ -35,7 +35,9 @@ pallet-intents-rpc = { workspace = true } pallet-intents-coprocessor = { workspace = true, default-features = true } ismp-parachain = { workspace = true, default-features = true } pallet-beefy-consensus-proofs = { workspace = true, default-features = true } -beefy-prover = { workspace = true } +# `bls-aggregate` only adds `beefy_prover::bls`; it does not change how the ECDSA paths read the +# wire, so the naive BEEFY simtest against Paseo is unaffected. +beefy-prover = { workspace = true, features = ["bls-aggregate"] } beefy-verifier-primitives = { workspace = true, default-features = true } ismp-abi = { workspace = true, default-features = true } alloy-sol-types = { workspace = true, default-features = true } diff --git a/parachain/simtests/src/lib.rs b/parachain/simtests/src/lib.rs index f48c86b56..93ef66258 100644 --- a/parachain/simtests/src/lib.rs +++ b/parachain/simtests/src/lib.rs @@ -1,6 +1,7 @@ mod base_call_filter; mod intents_rpc; mod migration_test; +mod pallet_beefy_bls; mod pallet_beefy_consensus_proofs; mod pallet_fishermen; mod pallet_ismp; diff --git a/parachain/simtests/src/pallet_beefy_bls.rs b/parachain/simtests/src/pallet_beefy_bls.rs new file mode 100644 index 000000000..b22321a9d --- /dev/null +++ b/parachain/simtests/src/pallet_beefy_bls.rs @@ -0,0 +1,199 @@ +//! Simnode test for the aggregate BLS12-381 BEEFY proof path. +//! +//! Mirrors the naive happy path in [`crate::pallet_beefy_consensus_proofs`], but against a relay +//! chain whose BEEFY authorities hold paired `ecdsa_bls_crypto` keys and whose keyset commitment is +//! over their BLS G2 public keys. The prover builds the proof, the runtime verifies it in a single +//! pairing check, and the consensus state advances. +//! +//! This needs a BLS BEEFY relay, which no public network is. Bring one up from Parity's +//! `skalman--enable-bls-beefy-on-westend` branch with the G2 keyset converter applied (see +//! `docs/bls-beefy-skalman-migration.md`), then point `RELAY_WS_URL` at it. Without that the test +//! cannot run, so it is `#[ignore]`d rather than silently passing. +//! +//! RELAY_WS_URL=ws://127.0.0.1:9979 PORT=9990 \ +//! cargo test -p simtests bls_beefy -- --ignored --nocapture + +#![cfg(test)] + +use std::env; + +use alloy_sol_types::SolType; +use anyhow::anyhow; +use codec::Decode; +use polkadot_sdk::{sp_consensus_beefy, *}; +use sp_keyring::sr25519::Keyring; +use subxt::{ + backend::legacy::LegacyRpcMethods, dynamic::Value, ext::subxt_rpcs::rpc_params, OnlineClient, + PolkadotConfig, +}; +use subxt_utils::Hyperbridge; + +use beefy_prover::{bls::decode_paired_justification, Prover}; +use beefy_verifier_primitives::{BlsConsensusMessage, ConsensusState, PROOF_TYPE_BLS}; +use ismp_abi::{ + bls_beefy::BlsBeefy::BlsBeefyConsensusProof as SolBlsProof, + ecdsa_beefy::BeefyConsensusState as SolBeefyConsensusState, +}; + +use crate::pallet_beefy_consensus_proofs::{ + previous_beefy_anchor, submit_signed, submit_sudo, BEEFY_CONSENSUS_ID, +}; + +/// Build a real BLS consensus proof, and the trusted state it advances from, off a live relay. +async fn build_live_bls_proof() -> Result<(ConsensusState, BlsConsensusMessage), anyhow::Error> { + let max_rpc_payload_size = 15 * 1024 * 1024; + let relay_ws_url = env::var("RELAY_WS_URL") + .map_err(|_| anyhow!("RELAY_WS_URL must point at a BLS BEEFY relay"))?; + + let (relay_client, relay_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await?; + let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); + + // Relay-only. Our BLS relay has no registered parachains, so `para_ids` is empty and the + // parachain half of the proof is empty; the parachain-header path is exercised elsewhere. + let (para_client, para_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await?; + let para_rpc = LegacyRpcMethods::::new(para_rpc_client.clone()); + + let prover = Prover { + beefy_activation_block: 0, + relay: relay_client, + relay_rpc: relay_rpc.clone(), + relay_rpc_client: relay_rpc_client.clone(), + para: para_client, + para_rpc, + para_rpc_client, + para_ids: vec![], + query_batch_size: Some(100), + }; + + let latest_beefy_hash: sp_core::H256 = + relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await?; + let previous_beefy_hash = previous_beefy_anchor(&relay_rpc, latest_beefy_hash).await?; + let initial_state = + prover.get_initial_consensus_state(Some(previous_beefy_hash.into())).await?; + + let block = relay_rpc + .chain_get_block(Some(latest_beefy_hash.into())) + .await? + .ok_or_else(|| anyhow!("missing latest beefy block"))?; + let justification = block + .justifications + .ok_or_else(|| anyhow!("latest beefy block lacks justifications"))? + .into_iter() + .find_map(|j| (j.0 == sp_consensus_beefy::BEEFY_ENGINE_ID).then_some(j.1)) + .ok_or_else(|| anyhow!("latest beefy block lacks a beefy justification"))?; + + // Keeps the whole 177-byte paired signature, where the ECDSA path would slice out the first + // 65 bytes. + let signed_commitment = decode_paired_justification(&justification)?; + let proof = prover.bls_consensus_proof(signed_commitment).await?; + + Ok((initial_state, proof)) +} + +#[tokio::test] +#[ignore] +async fn bls_beefy_proof_happy_path() -> Result<(), anyhow::Error> { + eprintln!("[stage] building live bls proof"); + let (initial_state, consensus_message) = build_live_bls_proof().await?; + + let initial_height = initial_state.latest_beefy_height; + let proof_block = consensus_message.mmr.commitment.block_number; + let signer_count = consensus_message.mmr.signers.len(); + eprintln!( + "[stage] proof built: trusted_height={initial_height} proof_block={proof_block} \ + signers={signer_count}", + ); + assert!( + proof_block > initial_height, + "proof block {proof_block} must be ahead of trusted height {initial_height}", + ); + assert!(signer_count > 0, "proof carries no signers"); + + // The keyset commitment here is over BLS public keys, so this trusted state is only usable by + // the BLS proof type. Feeding it a naive proof would fail the authority merkle proof. + let abi_state: SolBeefyConsensusState = initial_state.into(); + let abi_state_bytes = SolBeefyConsensusState::abi_encode(&abi_state); + + let abi_proof: SolBlsProof = consensus_message.into(); + let abi_proof_bytes = ::abi_encode_params(&abi_proof); + let mut wire_proof = Vec::with_capacity(1 + abi_proof_bytes.len()); + wire_proof.push(PROOF_TYPE_BLS); + wire_proof.extend_from_slice(&abi_proof_bytes); + eprintln!( + "[stage] abi-encoded: state={} bytes proof={} bytes", + abi_state_bytes.len(), + wire_proof.len(), + ); + + let port = env::var("PORT").unwrap_or_else(|_| "9990".into()); + let url = format!("ws://127.0.0.1:{port}"); + let (client, rpc_client) = + subxt_utils::client::ws_client::(&url, u32::MAX).await?; + + let init_call = subxt::dynamic::tx( + "BeefyConsensusProofs", + "initialize_state", + vec![Value::from_bytes(&abi_state_bytes)], + ); + eprintln!("[stage] submitting initialize_state via sudo"); + submit_sudo(&client, &rpc_client, init_call).await?; + + // Same reason as the naive test: keep `ProofReward` at zero so the reward path does not try to + // draw from an unfunded treasury account. + let zero_reward = + subxt::dynamic::tx("BeefyConsensusProofs", "set_proof_reward", vec![Value::u128(0)]); + submit_sudo(&client, &rpc_client, zero_reward).await?; + + let submit_call = subxt::dynamic::tx( + "BeefyConsensusProofs", + "submit_proof", + vec![Value::from_bytes(&wire_proof)], + ); + eprintln!("[stage] submitting submit_proof signed by Bob"); + submit_signed(&client, &rpc_client, submit_call, Keyring::Bob).await?; + eprintln!("[stage] submit_proof finalized"); + + // The consensus state is the thing that matters: it only advances if the aggregate pairing + // check, the keyset merkle multi-proof and the MMR leaf proof all passed inside the runtime. + let final_state = fetch_beefy_consensus_state(&client).await?; + assert_eq!( + final_state.latest_beefy_height, proof_block, + "consensus state should have advanced to the proven block", + ); + assert!( + final_state.latest_beefy_height > initial_height, + "consensus state did not move forward", + ); + + eprintln!( + "[ok] BLS BEEFY verified in-runtime: {signer_count} signers aggregated, \ + height {initial_height} -> {proof_block}" + ); + + Ok(()) +} + +/// Read back the BEEFY consensus state pallet-ismp stores. +async fn fetch_beefy_consensus_state( + client: &OnlineClient, +) -> Result { + let addr = subxt::dynamic::storage( + "Ismp", + "ConsensusStates", + vec![Value::from_bytes(BEEFY_CONSENSUS_ID)], + ); + let raw = client + .storage() + .at_latest() + .await? + .fetch(&addr) + .await? + .ok_or_else(|| anyhow!("no beefy consensus state stored"))? + .as_type::>()?; + + ConsensusState::decode(&mut &raw[..]).map_err(|e| anyhow!("decode consensus state: {e:?}")) +} diff --git a/parachain/simtests/src/pallet_beefy_consensus_proofs.rs b/parachain/simtests/src/pallet_beefy_consensus_proofs.rs index 7df44c00e..55ccff55d 100644 --- a/parachain/simtests/src/pallet_beefy_consensus_proofs.rs +++ b/parachain/simtests/src/pallet_beefy_consensus_proofs.rs @@ -79,7 +79,7 @@ const MAX_UNCLE_PROVERS: usize = 5; /// `ConsensusClientId` for BEEFY (`b"BEEF"`); duplicated here because pulling /// `ismp-beefy` into simtests just for this constant is excessive. -const BEEFY_CONSENSUS_ID: [u8; 4] = *b"BEEF"; +pub(crate) const BEEFY_CONSENSUS_ID: [u8; 4] = *b"BEEF"; /// Path-embedded SP1 fixture produced by the prover (`zk-beefy::tests::test_sp1_beefy`) /// and consumed by the on-chain SP1Beefy fork test under `evm/tests/foundry/`. Sourcing @@ -178,7 +178,7 @@ fn curve_value(fractions: &[(u32, u32)]) -> Value { /// Submit a sudo-wrapped call signed by Alice (the simnode sudo key) and wait /// for finalization. Returns the dispatch result so callers can assert on /// success / failure of the inner call. -async fn submit_sudo( +pub(crate) async fn submit_sudo( client: &OnlineClient, rpc_client: &RpcClient, inner: subxt::tx::DynamicPayload, @@ -187,7 +187,7 @@ async fn submit_sudo( submit_signed(client, rpc_client, sudo_call, Keyring::Alice).await } -async fn submit_signed( +pub(crate) async fn submit_signed( client: &OnlineClient, rpc_client: &RpcClient, call: subxt::tx::DynamicPayload, @@ -407,7 +407,7 @@ async fn test_admin_extrinsics_and_submit_proof_validation() -> Result<(), anyho /// a BEEFY justification. We use that parent as the trusted-state anchor so the /// proof at `latest_beefy_hash` is guaranteed to advance state. Mirrors the lookup /// in `modules/pallets/testsuite/src/tests/pallet_ismp_beefy.rs::setup`. -async fn previous_beefy_anchor( +pub(crate) async fn previous_beefy_anchor( relay_rpc: &LegacyRpcMethods, latest_beefy_hash: H256, ) -> Result { From 45fe9645b9fc27d50066aec404d7e048be3f0e8f Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 4 Aug 2026 17:17:02 +0100 Subject: [PATCH 04/48] add the solidity hash to curve and prove the bls beefy path end to end --- Cargo.lock | 119 +++------------- evm/foundry.toml | 7 + evm/src/consensus/BlsHashToCurve.sol | 158 +++++++++++++++++++++ evm/tests/foundry/BlsHashToCurve.t.sol | 79 +++++++++++ parachain/simtests/Cargo.toml | 2 +- parachain/simtests/src/bls_relay_setup.rs | 116 +++++++++++++++ parachain/simtests/src/lib.rs | 1 + parachain/simtests/src/pallet_beefy_bls.rs | 92 +++++++++--- 8 files changed, 454 insertions(+), 120 deletions(-) create mode 100644 evm/src/consensus/BlsHashToCurve.sol create mode 100644 evm/tests/foundry/BlsHashToCurve.t.sol create mode 100644 parachain/simtests/src/bls_relay_setup.rs diff --git a/Cargo.lock b/Cargo.lock index 84b9d4e5b..5d8457c96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7798,27 +7798,13 @@ dependencies = [ "sp-runtime", ] -[[package]] -name = "frame-decode" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cb8796f93fa038f979a014234d632e9688a120e745f936e2635123c77537f7" -dependencies = [ - "frame-metadata 21.0.0", - "parity-scale-codec", - "scale-decode", - "scale-info", - "scale-type-resolver", - "sp-crypto-hashing", -] - [[package]] name = "frame-decode" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e56c0e51972d7b26ff76966c4d0f2307030df9daa5ce0885149ece1ab7ca5ad" dependencies = [ - "frame-metadata 23.0.1", + "frame-metadata", "parity-scale-codec", "scale-decode", "scale-info", @@ -7832,7 +7818,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c470df86cf28818dd3cd2fc4667b80dbefe2236c722c3dc1d09e7c6c82d6dfcd" dependencies = [ - "frame-metadata 23.0.1", + "frame-metadata", "parity-scale-codec", "scale-decode", "scale-encode", @@ -7891,28 +7877,6 @@ dependencies = [ "sp-tracing", ] -[[package]] -name = "frame-metadata" -version = "20.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26de808fa6461f2485dc51811aefed108850064994fb4a62b3ac21ffa62ac8df" -dependencies = [ - "cfg-if", - "parity-scale-codec", - "scale-info", -] - -[[package]] -name = "frame-metadata" -version = "21.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20dfd1d7eae1d94e32e869e2fb272d81f52dd8db57820a373adb83ea24d7d862" -dependencies = [ - "cfg-if", - "parity-scale-codec", - "scale-info", -] - [[package]] name = "frame-metadata" version = "23.0.1" @@ -7970,7 +7934,7 @@ dependencies = [ "derive-where", "docify", "environmental", - "frame-metadata 23.0.1", + "frame-metadata", "frame-support-procedural", "impl-trait-for-tuples", "k256", @@ -12372,7 +12336,7 @@ checksum = "b3e3e3f549d27d2dc054372f320ddf68045a833fab490563ff70d4cf1b9d91ea" dependencies = [ "array-bytes 9.3.0", "blake3", - "frame-metadata 23.0.1", + "frame-metadata", "parity-scale-codec", "scale-decode", "scale-info", @@ -18346,7 +18310,7 @@ dependencies = [ "docify", "frame-benchmarking", "frame-benchmarking-cli", - "frame-metadata 23.0.1", + "frame-metadata", "frame-support", "frame-system-rpc-runtime-api", "frame-try-runtime", @@ -24463,7 +24427,7 @@ dependencies = [ "sp-core", "substrate-state-machine", "subxt 0.42.1", - "subxt-signer 0.41.0", + "subxt-signer 0.42.1", "subxt-utils", "tesseract-evm", "tesseract-fisherman", @@ -25700,7 +25664,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d4e7855b9d8b356ffd264ed089bc1063675880742bb4535f206831a87fc725e" dependencies = [ "derive-where", - "frame-metadata 23.0.1", + "frame-metadata", "parity-scale-codec", "scale-info", ] @@ -27422,7 +27386,7 @@ dependencies = [ "cargo_metadata 0.15.4", "console 0.15.11", "filetime", - "frame-metadata 23.0.1", + "frame-metadata", "jobserver", "merkleized-metadata", "parity-scale-codec", @@ -27478,7 +27442,7 @@ dependencies = [ "async-trait", "derive-where", "either", - "frame-metadata 23.0.1", + "frame-metadata", "futures", "hex", "jsonrpsee 0.24.11", @@ -27515,7 +27479,7 @@ dependencies = [ "async-trait", "derive-where", "either", - "frame-metadata 23.0.1", + "frame-metadata", "futures", "hex", "parity-scale-codec", @@ -27576,36 +27540,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "subxt-core" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66ef00be9d64885ec94e478a58e4e39d222024b20013ae7df4fc6ece545391aa" -dependencies = [ - "base58", - "blake2 0.10.6", - "derive-where", - "frame-decode 0.7.1", - "frame-metadata 20.0.0", - "hashbrown 0.14.5", - "hex", - "impl-serde 0.5.0", - "keccak-hash", - "parity-scale-codec", - "primitive-types 0.13.1", - "scale-bits", - "scale-decode", - "scale-encode", - "scale-info", - "scale-value", - "serde", - "serde_json", - "sp-crypto-hashing", - "subxt-metadata 0.41.0", - "thiserror 2.0.18", - "tracing", -] - [[package]] name = "subxt-core" version = "0.42.1" @@ -27616,7 +27550,7 @@ dependencies = [ "blake2 0.10.6", "derive-where", "frame-decode 0.8.3", - "frame-metadata 23.0.1", + "frame-metadata", "hashbrown 0.14.5", "hex", "impl-serde 0.5.0", @@ -27646,7 +27580,7 @@ dependencies = [ "blake2 0.10.6", "derive-where", "frame-decode 0.9.0", - "frame-metadata 23.0.1", + "frame-metadata", "hashbrown 0.14.5", "hex", "impl-serde 0.5.0", @@ -27744,21 +27678,6 @@ dependencies = [ "syn 2.0.116", ] -[[package]] -name = "subxt-metadata" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff4591673600c4388e21305788282414d26c791b4dee21b7cb0b19c10076f98" -dependencies = [ - "frame-decode 0.7.1", - "frame-metadata 20.0.0", - "hashbrown 0.14.5", - "parity-scale-codec", - "scale-info", - "sp-crypto-hashing", - "thiserror 2.0.18", -] - [[package]] name = "subxt-metadata" version = "0.42.1" @@ -27766,7 +27685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "243990ca4e0cdb74ef7458f1d5070a1bd5144d744cc146f23a32ab56d23e1db7" dependencies = [ "frame-decode 0.8.3", - "frame-metadata 23.0.1", + "frame-metadata", "hashbrown 0.14.5", "parity-scale-codec", "scale-info", @@ -27781,7 +27700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b2f2a52d97d7539febc0006d6988081150b1c1a3e4a357ca02ab5cdb34072bc" dependencies = [ "frame-decode 0.9.0", - "frame-metadata 23.0.1", + "frame-metadata", "hashbrown 0.14.5", "parity-scale-codec", "scale-info", @@ -27797,7 +27716,7 @@ checksum = "55313e3652f5360b5ed878bfe1d62fe181ecb8c130c81278ab89d1580f89a7ed" dependencies = [ "derive-where", "finito", - "frame-metadata 23.0.1", + "frame-metadata", "futures", "getrandom 0.2.17", "hex", @@ -27824,7 +27743,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dec54130c797530e6aa6a52e8ba9f95fd296d19da2f9f3e23ed5353a83573f74" dependencies = [ "derive-where", - "frame-metadata 23.0.1", + "frame-metadata", "futures", "hex", "impl-serde 0.5.0", @@ -27842,9 +27761,9 @@ dependencies = [ [[package]] name = "subxt-signer" -version = "0.41.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2370298a210ed1df26152db7209a85e0ed8cfbce035309c3b37f7b61755377" +checksum = "b58aeda7bebddedbef69ac55ae592fb9eef499927b50d42c43862d1664b5e5b3" dependencies = [ "base64 0.22.1", "bip39", @@ -27863,7 +27782,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sp-crypto-hashing", - "subxt-core 0.41.0", + "subxt-core 0.42.1", "thiserror 2.0.18", "zeroize", ] diff --git a/evm/foundry.toml b/evm/foundry.toml index da91054c5..a5cac4a9f 100644 --- a/evm/foundry.toml +++ b/evm/foundry.toml @@ -10,6 +10,13 @@ evm_version = "cancun" via-ir = true fs_permissions = [{ access = "read-write", path = "./"}] +# EIP-2537 (BLS12-381 precompiles) only exists from Prague, and the default profile is on cancun. +# Kept separate so the deployed contracts' bytecode is unaffected. SimplexPaymaster is skipped +# because its @openzeppelin/community-contracts dependency is not vendored. +[profile.bls] +evm_version = "prague" +skip = ["src/utils/SimplexPaymaster.sol"] + [profile.ci] [lint] diff --git a/evm/src/consensus/BlsHashToCurve.sol b/evm/src/consensus/BlsHashToCurve.sol new file mode 100644 index 000000000..bebb4b992 --- /dev/null +++ b/evm/src/consensus/BlsHashToCurve.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) Polytope Labs Ltd. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pragma solidity ^0.8.30; + +/** + * @title Hashing a BEEFY commitment onto the BLS12-381 G1 curve. + * @author Polytope Labs (hello@polytope.technology) + * + * @notice Reproduces exactly what substrate's BEEFY signers do, so an aggregate BLS signature can + * be checked on chain. This is the step most likely to be got wrong, because BEEFY does not use + * the IETF ciphersuite string the way a reading of RFC 9380 alone would suggest. + * + * @dev `w3f-bls` builds its hasher with a **one byte domain separation tag of 0x01**, and prepends + * the ciphersuite string to the *message* instead of using it as the tag: + * + * suite = "BLS_SIG_" || "BLS12381" || "G1" || "_XMD:SHA-256_SSWU_RO_" || "NUL_" + * preimage = suite || context || message // context is empty for BEEFY + * point = hash_to_curve(preimage, DST = 0x01) + * + * Using `suite` as the DST, which is what a textbook implementation does, yields a completely + * different point and a silently failing pairing check. `bls_hash_to_curve_vector` in the Rust + * verifier pins a test vector that this library is checked against. + * + * Everything after that composition is standard RFC 9380 and maps onto EIP-2537 precompiles. + * Note BEEFY puts signatures in G1 and public keys in G2, the opposite of the Ethereum + * convention, so an eth2 BLS library does not transfer. + */ +library BlsHashToCurve { + /// @dev EIP-2537 BLS12_G1ADD + address internal constant G1_ADD = address(0x0b); + /// @dev EIP-2537 BLS12_MAP_FP_TO_G1 + address internal constant MAP_FP_TO_G1 = address(0x10); + /// @dev Big-endian modular exponentiation (EIP-198), used to reduce a wide integer mod p. + address internal constant MOD_EXP = address(0x05); + + /// @dev The BLS12-381 base field modulus, 48 bytes big-endian. + bytes internal constant FIELD_MODULUS = + hex"1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab"; + + /// @dev The ciphersuite `w3f-bls` prepends to the message. Not the domain separation tag. + bytes internal constant CIPHER_SUITE = "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; + + /// @dev `DST_prime` = DST || I2OSP(len(DST), 1), with the one byte DST `w3f-bls` uses. + bytes internal constant DST_PRIME = hex"0101"; + + error MapToG1Failed(); + error G1AddFailed(); + error ModExpFailed(); + + /** + * @notice Hash a SCALE-encoded BEEFY commitment onto G1, exactly as its signers did. + * @param commitment the SCALE-encoded commitment + * @return a G1 point, 128 bytes, as EIP-2537 encodes them (x || y, each 64 bytes) + */ + function hashCommitmentToG1(bytes memory commitment) internal view returns (bytes memory) { + return hashToG1(bytes.concat(CIPHER_SUITE, commitment)); + } + + /** + * @notice RFC 9380 `hash_to_curve` for BLS12-381 G1 with the DST `w3f-bls` uses. + * @dev Cofactor clearing is linear, so it makes no difference that MAP_FP_TO_G1 applies it per + * point rather than once after the addition. + */ + function hashToG1(bytes memory preimage) internal view returns (bytes memory) { + (bytes memory u0, bytes memory u1) = hashToField(preimage); + return g1Add(mapToG1(u0), mapToG1(u1)); + } + + /** + * @notice RFC 9380 `hash_to_field` producing the two field elements the map consumes. + * @return u0 and u1, each 64 bytes: a 48 byte big-endian value left padded to 64, which is how + * EIP-2537 wants field elements. + */ + function hashToField(bytes memory preimage) internal view returns (bytes memory u0, bytes memory u1) { + // L = ceil((ceil(log2(p)) + k) / 8) = ceil((381 + 128) / 8) = 64, and we need two of them. + bytes memory uniform = expandMessageXmd(preimage, 128); + + bytes memory wide0 = new bytes(64); + bytes memory wide1 = new bytes(64); + for (uint256 i = 0; i < 64; ++i) { + wide0[i] = uniform[i]; + wide1[i] = uniform[64 + i]; + } + + u0 = toFieldElement(wide0); + u1 = toFieldElement(wide1); + } + + /** + * @notice RFC 9380 section 5.3.1 `expand_message_xmd` with SHA-256. + * @dev Written for `lenInBytes` a multiple of 32 and at most 255 blocks, which covers the only + * caller (128 bytes, so four blocks). + */ + function expandMessageXmd(bytes memory message, uint16 lenInBytes) internal pure returns (bytes memory) { + uint256 ell = (lenInBytes + 31) / 32; + + // msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime + bytes memory zPad = new bytes(64); // SHA-256 block size + bytes memory msgPrime = bytes.concat(zPad, message, bytes2(lenInBytes), hex"00", DST_PRIME); + + bytes32 b0 = sha256(msgPrime); + bytes32 bi = sha256(bytes.concat(b0, hex"01", DST_PRIME)); + + bytes memory out = bytes.concat(bi); + for (uint256 i = 2; i <= ell; ++i) { + bi = sha256(bytes.concat(b0 ^ bi, bytes1(uint8(i)), DST_PRIME)); + out = bytes.concat(out, bi); + } + + return out; + } + + /// @notice Reduce a 64 byte big-endian integer mod p, returned padded to the 64 bytes + /// EIP-2537 expects for a field element. + function toFieldElement(bytes memory wide) internal view returns (bytes memory) { + // modexp(base = wide, exponent = 1, modulus = p) is just `wide mod p`. + bytes memory input = bytes.concat( + bytes32(uint256(64)), // base length + bytes32(uint256(1)), // exponent length + bytes32(uint256(48)), // modulus length + wide, + hex"01", + FIELD_MODULUS + ); + + (bool ok, bytes memory reduced) = MOD_EXP.staticcall(input); + if (!ok || reduced.length != 48) revert ModExpFailed(); + + // EIP-2537 field elements are 64 bytes: 16 zero bytes then the 48 byte value. + return bytes.concat(new bytes(16), reduced); + } + + /// @notice EIP-2537 `MAP_FP_TO_G1`: field element to a G1 point, cofactor already cleared. + function mapToG1(bytes memory fieldElement) internal view returns (bytes memory) { + (bool ok, bytes memory point) = MAP_FP_TO_G1.staticcall(fieldElement); + if (!ok || point.length != 128) revert MapToG1Failed(); + return point; + } + + /// @notice EIP-2537 `G1ADD`. + function g1Add(bytes memory a, bytes memory b) internal view returns (bytes memory) { + (bool ok, bytes memory sum) = G1_ADD.staticcall(bytes.concat(a, b)); + if (!ok || sum.length != 128) revert G1AddFailed(); + return sum; + } +} diff --git a/evm/tests/foundry/BlsHashToCurve.t.sol b/evm/tests/foundry/BlsHashToCurve.t.sol new file mode 100644 index 000000000..07c32a419 --- /dev/null +++ b/evm/tests/foundry/BlsHashToCurve.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {BlsHashToCurve} from "../../src/consensus/BlsHashToCurve.sol"; + +/** + * @title Cross-language check of BEEFY's hash-to-curve. + * + * @notice The expected values come from `bls_hash_to_curve_vector` in the Rust verifier, which + * generates them through the very `w3f-bls` code path the relay chain signs with. If these two + * disagree, an on-chain aggregate BLS check would fail against real chain output while looking + * perfectly correct in isolation. + * + * Needs EIP-2537, so run under an EVM version of at least Prague: + * FOUNDRY_PROFILE=bls forge test --match-contract BlsHashToCurveTest -vv + */ +contract BlsHashToCurveTest is Test { + /// The message the Rust vector was generated for. + bytes constant MESSAGE = "beefy-bls-hash-to-curve-vector"; + + /// `u[0]` and `u[1]` from the Rust vector, before the map to the curve. + bytes constant EXPECTED_U0 = + hex"19fa8d7582393438a7bd7ef7e789de283142e027986bc7f5f1919c106071b346a735f1cde455b6d0e9547783b4ffbbc3"; + bytes constant EXPECTED_U1 = + hex"15a0511a246ac447601f9abf8f3c6639328a907e15ba3981923b222f26d5440390996a0a065a227f75c23c552cf37960"; + + /// The resulting G1 point. + bytes constant EXPECTED_X = + hex"0e1caf33eecf4c4d00dc4c2d7dc2f1d9ffef352cdcf50a359caf0c9ddcb49de2a4124773cc45d92901079834fc0743ea"; + bytes constant EXPECTED_Y = + hex"1427e29396b9a044ac8475c5939054e529c70a5c01cdb71e6a845bcb2ad0342dfb148c6860e084fa7a84aa1ea4c54385"; + + /// The ciphersuite is prepended to the message rather than used as the domain separation tag. + function test_ciphersuite_matches_w3f_bls() public pure { + assertEq( + BlsHashToCurve.CIPHER_SUITE, + bytes("BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"), + "ciphersuite drifted from what w3f-bls composes" + ); + // DST_prime = DST || len(DST), and the DST really is the single byte 0x01. + assertEq(BlsHashToCurve.DST_PRIME, hex"0101", "dst is not the one byte 0x01"); + } + + /// `hash_to_field` is the part the contract implements by hand, so it is checked on its own. + function test_hash_to_field_matches_rust() public view { + bytes memory preimage = bytes.concat(BlsHashToCurve.CIPHER_SUITE, MESSAGE); + (bytes memory u0, bytes memory u1) = BlsHashToCurve.hashToField(preimage); + + // Field elements are returned padded to 64 bytes; the value is the trailing 48. + assertEq(_trim(u0), EXPECTED_U0, "u[0] differs from the Rust vector"); + assertEq(_trim(u1), EXPECTED_U1, "u[1] differs from the Rust vector"); + } + + /// The whole pipeline, including the EIP-2537 map and addition. + function test_hash_commitment_to_g1_matches_rust() public view { + bytes memory point = BlsHashToCurve.hashCommitmentToG1(MESSAGE); + assertEq(point.length, 128, "G1 points are 128 bytes"); + + bytes memory x = new bytes(48); + bytes memory y = new bytes(48); + for (uint256 i = 0; i < 48; ++i) { + x[i] = point[16 + i]; + y[i] = point[64 + 16 + i]; + } + + assertEq(x, EXPECTED_X, "point.x differs from the Rust vector"); + assertEq(y, EXPECTED_Y, "point.y differs from the Rust vector"); + } + + /// Strip the 16 bytes of zero padding EIP-2537 puts in front of a field element. + function _trim(bytes memory padded) private pure returns (bytes memory) { + bytes memory out = new bytes(48); + for (uint256 i = 0; i < 48; ++i) { + out[i] = padded[16 + i]; + } + return out; + } +} diff --git a/parachain/simtests/Cargo.toml b/parachain/simtests/Cargo.toml index 9b2c1a1ce..c00129827 100644 --- a/parachain/simtests/Cargo.toml +++ b/parachain/simtests/Cargo.toml @@ -49,7 +49,7 @@ hex-literal = { workspace = true } hex = { workspace = true } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" -subxt-signer = "0.41.0" +subxt-signer = "0.42" nexus-runtime = { workspace = true, default-features = true} gargantua-runtime = { workspace = true, default-features = true} crypto-utils = { workspace = true, default-features = true } diff --git a/parachain/simtests/src/bls_relay_setup.rs b/parachain/simtests/src/bls_relay_setup.rs new file mode 100644 index 000000000..782a7987c --- /dev/null +++ b/parachain/simtests/src/bls_relay_setup.rs @@ -0,0 +1,116 @@ +//! One-off setup for the BLS BEEFY end-to-end test: register a parachain on the local relay. +//! +//! `pallet-beefy-consensus-proofs` only accepts a proof that finalizes a parachain head it has +//! not seen, and it reads the child trie root out of that head. A relay with no registered +//! parachains therefore cannot satisfy it, no matter how good the BLS proof is. Registering one +//! gives the relay's MMR leaves a non-empty parachain heads root and lets the whole path run. +//! +//! The para id must be 4009, which is what gargantua's `is_parachain_tracked` allows and what its +//! coprocessor state machine resolves to; a head under any other id is filtered out and the +//! commitment lookup misses. +//! +//! Generate the artifacts first: +//! +//! ```text +//! hyperbridge build-spec --chain gargantua-4009 --disable-default-bootnode > plain.json +//! # patch "relay_chain" to the relay's id, then +//! hyperbridge build-spec --chain plain.json --raw --disable-default-bootnode > raw.json +//! hyperbridge export-genesis-head --chain raw.json > head.hex +//! hyperbridge export-genesis-wasm --chain raw.json > wasm.hex +//! ``` +//! +//! then: +//! +//! ```text +//! RELAY_WS_URL=ws://127.0.0.1:9979 PARA_HEAD_PATH=head.hex PARA_WASM_PATH=wasm.hex \ +//! cargo test -p simtests register_parachain -- --ignored --nocapture +//! ``` + +#![cfg(test)] + +use std::{env, fs}; + +use anyhow::anyhow; +use polkadot_sdk::sp_core::Bytes; +use subxt::{dynamic::Value, ext::subxt_rpcs::rpc_params, PolkadotConfig}; + +/// Read a `0x`-prefixed hex blob written by `export-genesis-*`. +fn read_hex(path: &str) -> Result, anyhow::Error> { + let raw = fs::read_to_string(path).map_err(|e| anyhow!("reading {path}: {e}"))?; + let trimmed = raw.trim().trim_start_matches("0x"); + hex::decode(trimmed).map_err(|e| anyhow!("decoding {path}: {e}")) +} + +#[tokio::test] +#[ignore] +async fn register_parachain_on_bls_relay() -> Result<(), anyhow::Error> { + let relay_ws_url = env::var("RELAY_WS_URL") + .map_err(|_| anyhow!("RELAY_WS_URL must point at the BLS BEEFY relay"))?; + let head_path = env::var("PARA_HEAD_PATH").unwrap_or_else(|_| "/tmp/g4009-head.hex".into()); + let wasm_path = env::var("PARA_WASM_PATH").unwrap_or_else(|_| "/tmp/g4009-wasm.hex".into()); + let para_id: u32 = env::var("PARA_ID").unwrap_or_else(|_| "4009".into()).parse()?; + + let genesis_head = read_hex(&head_path)?; + let validation_code = read_hex(&wasm_path)?; + eprintln!( + "[stage] para {para_id}: head {} bytes, validation code {} bytes", + genesis_head.len(), + validation_code.len(), + ); + + // The validation code alone is ~2MB, so the default payload ceiling is not enough. + let (client, rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, u32::MAX).await?; + + let already: Vec = parachains(&rpc_client).await.unwrap_or_default(); + if already.contains(¶_id) { + eprintln!("[ok] para {para_id} is already registered, nothing to do"); + return Ok(()); + } + + // `para_kind: true` registers a parachain rather than a parathread, so it gets a core and + // its heads land in the relay's parachain heads root. + let genesis = Value::named_composite(vec![ + ("genesis_head", Value::from_bytes(&genesis_head)), + ("validation_code", Value::from_bytes(&validation_code)), + ("para_kind", Value::bool(true)), + ]); + + let inner = subxt::dynamic::tx( + "ParasSudoWrapper", + "sudo_schedule_para_initialize", + vec![Value::u128(para_id as u128), genesis], + ); + let sudo_call = subxt::dynamic::tx("Sudo", "sudo", vec![inner.into_value()]); + + // The relay's sudo key is Alice on a dev genesis; verified against `Sudo::Key` on chain. + let signer = subxt_signer::sr25519::dev::alice(); + + eprintln!("[stage] submitting sudoScheduleParaInitialize"); + let progress = client.tx().sign_and_submit_then_watch_default(&sudo_call, &signer).await?; + progress.wait_for_finalized_success().await?; + eprintln!("[stage] registration extrinsic finalized"); + + // Onboarding takes effect at a session boundary, so the para list does not update immediately. + for attempt in 1..=60 { + let current = parachains(&rpc_client).await.unwrap_or_default(); + if current.contains(¶_id) { + eprintln!("[ok] para {para_id} onboarded after {attempt} checks: {current:?}"); + return Ok(()); + } + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + } + + Err(anyhow!("para {para_id} did not appear in Paras::Parachains within the timeout")) +} + +/// `Paras::Parachains`, the ids currently onboarded as parachains. +async fn parachains( + rpc_client: &subxt::ext::subxt_rpcs::RpcClient, +) -> Result, anyhow::Error> { + let key = format!("0x{}", hex::encode(beefy_prover::PARAS_PARACHAINS)); + let raw: Option = rpc_client.request("state_getStorage", rpc_params![key]).await?; + + let Some(bytes) = raw else { return Ok(vec![]) }; + Ok(codec::Decode::decode(&mut &bytes.0[..])?) +} diff --git a/parachain/simtests/src/lib.rs b/parachain/simtests/src/lib.rs index 93ef66258..b6cf23e2b 100644 --- a/parachain/simtests/src/lib.rs +++ b/parachain/simtests/src/lib.rs @@ -1,4 +1,5 @@ mod base_call_filter; +mod bls_relay_setup; mod intents_rpc; mod migration_test; mod pallet_beefy_bls; diff --git a/parachain/simtests/src/pallet_beefy_bls.rs b/parachain/simtests/src/pallet_beefy_bls.rs index b22321a9d..37f4fe5b4 100644 --- a/parachain/simtests/src/pallet_beefy_bls.rs +++ b/parachain/simtests/src/pallet_beefy_bls.rs @@ -35,9 +35,7 @@ use ismp_abi::{ ecdsa_beefy::BeefyConsensusState as SolBeefyConsensusState, }; -use crate::pallet_beefy_consensus_proofs::{ - previous_beefy_anchor, submit_signed, submit_sudo, BEEFY_CONSENSUS_ID, -}; +use crate::pallet_beefy_consensus_proofs::{submit_signed, submit_sudo, BEEFY_CONSENSUS_ID}; /// Build a real BLS consensus proof, and the trusted state it advances from, off a live relay. async fn build_live_bls_proof() -> Result<(ConsensusState, BlsConsensusMessage), anyhow::Error> { @@ -50,10 +48,12 @@ async fn build_live_bls_proof() -> Result<(ConsensusState, BlsConsensusMessage), .await?; let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); - // Relay-only. Our BLS relay has no registered parachains, so `para_ids` is empty and the - // parachain half of the proof is empty; the parachain-header path is exercised elsewhere. + // Track the parachain registered on the BLS relay. `pallet-beefy-consensus-proofs` reads the + // child trie root out of a finalized parachain head, so the proof has to carry one; 4009 is + // the id gargantua's `is_parachain_tracked` allows and the one its coprocessor resolves to. + let para_ws_url = env::var("PARA_WS_URL").unwrap_or_else(|_| "ws://127.0.0.1:9991".into()); let (para_client, para_rpc_client) = - subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + subxt_utils::client::ws_client::(¶_ws_url, max_rpc_payload_size) .await?; let para_rpc = LegacyRpcMethods::::new(para_rpc_client.clone()); @@ -65,33 +65,87 @@ async fn build_live_bls_proof() -> Result<(ConsensusState, BlsConsensusMessage), para: para_client, para_rpc, para_rpc_client, - para_ids: vec![], + para_ids: vec![4009], query_batch_size: Some(100), }; let latest_beefy_hash: sp_core::H256 = relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await?; - let previous_beefy_hash = previous_beefy_anchor(&relay_rpc, latest_beefy_hash).await?; + + let justification = beefy_justification(&relay_rpc, latest_beefy_hash).await?; + // Keeps the whole 177-byte paired signature, where the ECDSA path would slice out the first + // 65 bytes. + let signed_commitment = decode_paired_justification(&justification)?; + let latest_set_id = signed_commitment.commitment.validator_set_id; + + // Anchor the trusted state one authority set back, so this proof rotates the set. + // + // `pallet-beefy-consensus-proofs` rejects a proof that neither rotates the authority set nor + // finalizes a parachain head it has not already seen. Our BLS relay has no registered + // parachains, so no proof can ever finalize a head and only a rotation proof is accepted. + // The anchor has to be exactly one set back: the verifier requires the commitment to be + // signed by the trusted state's current or next set, so a further-back anchor is rejected + // outright with `UnknownAuthoritySet`. + let previous_beefy_hash = + previous_set_anchor(&relay_rpc, latest_beefy_hash, latest_set_id).await?; let initial_state = prover.get_initial_consensus_state(Some(previous_beefy_hash.into())).await?; + let proof = prover.bls_consensus_proof(signed_commitment).await?; + + Ok((initial_state, proof)) +} + +/// The BEEFY justification attached to `hash`. +async fn beefy_justification( + relay_rpc: &LegacyRpcMethods, + hash: sp_core::H256, +) -> Result, anyhow::Error> { let block = relay_rpc - .chain_get_block(Some(latest_beefy_hash.into())) + .chain_get_block(Some(hash.into())) .await? - .ok_or_else(|| anyhow!("missing latest beefy block"))?; - let justification = block + .ok_or_else(|| anyhow!("missing block {hash:?}"))?; + + block .justifications - .ok_or_else(|| anyhow!("latest beefy block lacks justifications"))? + .ok_or_else(|| anyhow!("block {hash:?} lacks justifications"))? .into_iter() .find_map(|j| (j.0 == sp_consensus_beefy::BEEFY_ENGINE_ID).then_some(j.1)) - .ok_or_else(|| anyhow!("latest beefy block lacks a beefy justification"))?; - - // Keeps the whole 177-byte paired signature, where the ECDSA path would slice out the first - // 65 bytes. - let signed_commitment = decode_paired_justification(&justification)?; - let proof = prover.bls_consensus_proof(signed_commitment).await?; + .ok_or_else(|| anyhow!("block {hash:?} lacks a beefy justification")) +} - Ok((initial_state, proof)) +/// Walk back to a BEEFY-justified block signed by the set immediately before `set_id`. +/// +/// Seeding the trusted state there leaves it holding `current = set_id - 1` and `next = set_id`, +/// so a commitment from `set_id` whose leaf announces `set_id + 1` advances the set by one. +async fn previous_set_anchor( + relay_rpc: &LegacyRpcMethods, + from: sp_core::H256, + set_id: u64, +) -> Result { + let mut cursor = from; + + for _ in 0..4000 { + let header = relay_rpc + .chain_get_header(Some(cursor.into())) + .await? + .ok_or_else(|| anyhow!("missing header for {cursor:?}"))?; + let parent: sp_core::H256 = header.parent_hash.into(); + if parent.is_zero() { + break; + } + + if let Ok(justification) = beefy_justification(relay_rpc, parent).await { + let signed = decode_paired_justification(&justification)?; + if signed.commitment.validator_set_id + 1 == set_id { + return Ok(parent); + } + } + + cursor = parent; + } + + Err(anyhow!("no beefy block found for the authority set preceding {set_id}")) } #[tokio::test] From 19e30ee0becbf424583106f9b954e9b2882aba0f Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Thu, 6 Aug 2026 11:06:59 +0100 Subject: [PATCH 05/48] verify bls beefy proofs on the evm with a single pairing check --- Cargo.lock | 7 + evm/rust/src/conversions.rs | 91 ++-- evm/src/consensus/BlsAggregate.sol | 167 ++++++ evm/src/consensus/BlsBeefy.sol | 293 ++++++++++- evm/tests/foundry/BlsAggregate.t.sol | 177 +++++++ evm/tests/foundry/BlsBeefy.t.sol | 84 +++ .../foundry/fixtures/bls-beefy-live-proof.hex | 1 + .../foundry/fixtures/bls-beefy-live-state.hex | 1 + .../foundry/fixtures/bls-beefy-proof.hex | 1 + .../foundry/fixtures/bls-beefy-state.hex | 1 + modules/consensus/beefy/primitives/src/lib.rs | 56 ++ modules/consensus/beefy/prover/Cargo.toml | 10 +- modules/consensus/beefy/prover/src/bls.rs | 128 +++++ modules/consensus/beefy/verifier/Cargo.toml | 1 + modules/consensus/beefy/verifier/src/test.rs | 479 +++++++++++++++++- 15 files changed, 1412 insertions(+), 85 deletions(-) create mode 100644 evm/src/consensus/BlsAggregate.sol create mode 100644 evm/tests/foundry/BlsAggregate.t.sol create mode 100644 evm/tests/foundry/BlsBeefy.t.sol create mode 100644 evm/tests/foundry/fixtures/bls-beefy-live-proof.hex create mode 100644 evm/tests/foundry/fixtures/bls-beefy-live-state.hex create mode 100644 evm/tests/foundry/fixtures/bls-beefy-proof.hex create mode 100644 evm/tests/foundry/fixtures/bls-beefy-state.hex diff --git a/Cargo.lock b/Cargo.lock index 5d8457c96..2de831df9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2898,7 +2898,12 @@ dependencies = [ name = "beefy-prover" version = "0.1.1" dependencies = [ + "alloy-primitives 1.5.7", "anyhow", + "ark-bls12-381 0.4.0", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", "beefy-verifier-primitives", "ckb-merkle-mountain-range", "derive_more 1.0.0", @@ -2906,6 +2911,7 @@ dependencies = [ "hex", "hex-literal 0.4.1", "indicatif 0.18.4", + "ismp-abi", "log", "parity-scale-codec", "polkadot-sdk", @@ -2923,6 +2929,7 @@ dependencies = [ name = "beefy-verifier" version = "0.1.0" dependencies = [ + "alloy-primitives 1.5.7", "alloy-sol-types 1.5.7", "anyhow", "ark-bls12-381 0.4.0", diff --git a/evm/rust/src/conversions.rs b/evm/rust/src/conversions.rs index 7f413a764..8d19661f8 100644 --- a/evm/rust/src/conversions.rs +++ b/evm/rust/src/conversions.rs @@ -56,10 +56,11 @@ mod beefy { use alloc::{vec, vec::Vec}; use alloy_primitives::{Bytes, FixedBytes}; use beefy_verifier_primitives::{ - BlsConsensusMessage, BlsMmrProof, BlsSigner, ConsensusMessage, ConsensusState, MmrProof, - ParachainHeader as BvpParachainHeader, ParachainProof as BvpParachainProof, - SignatureWithAuthorityIndex, SignedCommitment as BvpSignedCommitment, Sp1BeefyProof, - TSignature, + compress_g1, compress_g2, BlsConsensusMessage, BlsMmrProof, BlsSigner, ConsensusMessage, + ConsensusState, MmrProof, ParachainHeader as BvpParachainHeader, + ParachainProof as BvpParachainProof, SignatureWithAuthorityIndex, + SignedCommitment as BvpSignedCommitment, Sp1BeefyProof, TSignature, + BLS_G1_UNCOMPRESSED_LEN, BLS_G2_UNCOMPRESSED_LEN, }; use polkadot_sdk::*; use primitive_types::H256; @@ -519,16 +520,26 @@ mod beefy { items: value.mmrProof.into_iter().map(|h| H256(h.0)).collect(), }; + // The ABI proof carries uncompressed points, because that is what EIP-2537 accepts. + // The verifier and the keyset commitment both work on the compressed encoding, so + // compress on the way in. That direction is pure byte manipulation; the reverse needs + // an Fp2 square root. let signers = value .signers .into_iter() - .map(|signer| BlsSigner { - public_key: signer + .map(|signer| { + let uncompressed: [u8; BLS_G2_UNCOMPRESSED_LEN] = signer .publicKey .as_ref() .try_into() - .expect("BLS public key should be 96 bytes"), - index: signer.authorityIndex.try_into().expect("authority index out of bounds"), + .expect("BLS public key should be 256 bytes uncompressed"); + BlsSigner { + public_key: compress_g2(&uncompressed), + index: signer + .authorityIndex + .try_into() + .expect("authority index out of bounds"), + } }) .collect(); @@ -537,11 +548,14 @@ mod beefy { BlsMmrProof { commitment: commitment.into(), signers, - aggregate_signature: value - .aggregateSignature - .as_ref() - .try_into() - .expect("aggregate BLS signature should be 48 bytes"), + aggregate_signature: { + let uncompressed: [u8; BLS_G1_UNCOMPRESSED_LEN] = value + .aggregateSignature + .as_ref() + .try_into() + .expect("aggregate BLS signature should be 128 bytes uncompressed"); + compress_g1(&uncompressed) + }, latest_mmr_leaf: leaf.into(), mmr_proof, authority_proof: value.proof.into_iter().map(|h| h.0).collect(), @@ -549,54 +563,9 @@ mod beefy { } } - impl From for crate::bls_beefy::BlsBeefy::BlsRelayChainProof { - fn from(value: BlsMmrProof) -> Self { - use crate::bls_beefy::BlsBeefy; - - let leaf_index = value.mmr_proof.leaf_indices[0]; - let commitment: Commitment = value.commitment.into(); - let leaf = BeefyMmrLeaf { - version: 0, - parentNumber: value.latest_mmr_leaf.parent_number_and_hash.0, - parentHash: FixedBytes::from(value.latest_mmr_leaf.parent_number_and_hash.1 .0), - nextAuthoritySet: value.latest_mmr_leaf.beefy_next_authority_set.into(), - extra: FixedBytes::from(value.latest_mmr_leaf.leaf_extra.0), - leafIndex: leaf_index.to_u256(), - }; - - BlsBeefy::BlsRelayChainProof { - commitment: commitment.into(), - signers: value - .signers - .into_iter() - .map(|signer| BlsBeefy::BlsSigner { - publicKey: Bytes::from(signer.public_key.to_vec()), - authorityIndex: signer.index.to_u256(), - }) - .collect(), - aggregateSignature: Bytes::from(value.aggregate_signature.to_vec()), - latestMmrLeaf: leaf.into(), - mmrProof: value - .mmr_proof - .items - .into_iter() - .map(|h| FixedBytes::from(h.0)) - .collect(), - proof: value.authority_proof.into_iter().map(FixedBytes::from).collect(), - } - } - } - - impl From for crate::bls_beefy::BlsBeefy::BlsBeefyConsensusProof { - fn from(value: BlsConsensusMessage) -> Self { - let parachain: ParachainProof = value.parachain.into(); - - crate::bls_beefy::BlsBeefy::BlsBeefyConsensusProof { - relay: value.mmr.into(), - parachain: parachain.into(), - } - } - } + // The SCALE -> ABI direction lives in `beefy-prover`, not here. Building an EVM-bound proof + // means decompressing the points, which needs arkworks, and this crate compiles into the + // runtime. See `beefy_prover::bls::to_abi_proof`. impl From for BlsConsensusMessage { fn from(value: crate::bls_beefy::BlsBeefy::BlsBeefyConsensusProof) -> Self { diff --git a/evm/src/consensus/BlsAggregate.sol b/evm/src/consensus/BlsAggregate.sol new file mode 100644 index 000000000..dacf6874b --- /dev/null +++ b/evm/src/consensus/BlsAggregate.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) Polytope Labs Ltd. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pragma solidity ^0.8.30; + +import {BlsHashToCurve} from "./BlsHashToCurve.sol"; + +/** + * @title Aggregate BLS12-381 signature verification for BEEFY. + * @author Polytope Labs (hello@polytope.technology) + * + * @notice Checks that a supermajority of the validator set signed a commitment, in a single + * pairing operation regardless of how many of them there are. This is the point of the BLS path: + * the ECDSA verifier spends one `ecrecover` per signature, so its cost grows with the set, while + * this does not. + * + * @dev The identity checked is the standard aggregate: + * + * e(sum(sig_i), g2_generator) == e(H(commitment), sum(pubkey_i)) + * + * `PAIRING_CHECK` tests whether a product of pairings equals one, so it is rearranged as + * + * e(sum(sig_i), -g2_generator) * e(H(commitment), sum(pubkey_i)) == 1 + * + * which is why the negated generator is a constant here. + * + * Signatures live in G1 and public keys in G2, the opposite of the Ethereum convention. + * + * Points are taken **uncompressed**. EIP-2537 has no decompression precompile, and recovering a + * G2 point from its compressed form needs an Fp2 square root, which is expensive in Solidity. The + * prover therefore supplies uncompressed coordinates. Compressing them again is cheap, so + * [`compressG2`] derives the form the relay chain commits to when a merkle leaf is needed. + */ +library BlsAggregate { + /// @dev EIP-2537 BLS12_G2ADD + address internal constant G2_ADD = address(0x0d); + /// @dev EIP-2537 BLS12_PAIRING_CHECK + address internal constant PAIRING_CHECK = address(0x0f); + + /// @dev A G1 point: x || y, each a 64 byte field element. + uint256 internal constant G1_POINT_LEN = 128; + /// @dev A G2 point: x.c0 || x.c1 || y.c0 || y.c1, each a 64 byte field element. + uint256 internal constant G2_POINT_LEN = 256; + + /** + * @dev The negated BLS12-381 G2 generator, uncompressed, as EIP-2537 encodes it. Negation on + * this curve is `y -> p - y`, applied to both Fp2 coefficients; x is unchanged. + */ + bytes internal constant NEG_G2_GENERATOR = hex"00000000000000000000000000000000" + hex"024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8" + hex"00000000000000000000000000000000" + hex"13e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e" + hex"00000000000000000000000000000000" + hex"0d1b3cc2c7027888be51d9ef691d77bcb679afda66c73f17f9ee3837a55024f78c71363275a75d75d86bab79f74782aa" + hex"00000000000000000000000000000000" + hex"13fa4d4a0ad8b1ce186ed5061789213d993923066dddaf1040bc3ff59f825c78df74f2d75467e25e0f55f8a00fa030ed"; + + /// @dev `(p - 1) / 2`. A compressed point records which of the two square roots `y` is, and + /// the convention is "the larger one", meaning `y > (p - 1) / 2`. + bytes internal constant HALF_MODULUS = + hex"0d0088f51cbff34d258dd3db21a5d66bb23ba5c279c2895fb39869507b587b120f55ffff58a9ffffdcff7fffffffd555"; + + /// @dev Set on the first byte of a compressed point. + uint8 internal constant COMPRESSION_FLAG = 0x80; + /// @dev Set when `y` is the larger of the two roots. + uint8 internal constant SIGN_FLAG = 0x20; + + error G2AddFailed(); + error PairingFailed(); + error InvalidPointLength(); + error NoSigners(); + + /** + * @notice Verify that `aggregateSignature` is the sum of signatures over `commitment` by the + * holders of `publicKeys`. + * @param commitment the SCALE-encoded BEEFY commitment, hashed onto G1 internally + * @param aggregateSignature a G1 point, 128 bytes uncompressed + * @param publicKeys the signers' G2 points, 256 bytes uncompressed each + */ + function verify(bytes memory commitment, bytes memory aggregateSignature, bytes[] memory publicKeys) + internal + view + returns (bool) + { + if (publicKeys.length == 0) revert NoSigners(); + if (aggregateSignature.length != G1_POINT_LEN) revert InvalidPointLength(); + + bytes memory aggregateKey = sumG2(publicKeys); + bytes memory messagePoint = BlsHashToCurve.hashCommitmentToG1(commitment); + + // Two pairs: (sig, -g2_gen) and (H(msg), aggregate key). Their product is one exactly when + // the aggregate signature is valid for the aggregate key. + bytes memory input = bytes.concat(aggregateSignature, NEG_G2_GENERATOR, messagePoint, aggregateKey); + + (bool ok, bytes memory result) = PAIRING_CHECK.staticcall(input); + if (!ok || result.length != 32) revert PairingFailed(); + + return abi.decode(result, (uint256)) == 1; + } + + /// @notice Sum a set of uncompressed G2 points with `G2ADD`. + function sumG2(bytes[] memory points) internal view returns (bytes memory) { + if (points.length == 0) revert NoSigners(); + if (points[0].length != G2_POINT_LEN) revert InvalidPointLength(); + + bytes memory acc = points[0]; + for (uint256 i = 1; i < points.length; ++i) { + if (points[i].length != G2_POINT_LEN) revert InvalidPointLength(); + + (bool ok, bytes memory sum) = G2_ADD.staticcall(bytes.concat(acc, points[i])); + if (!ok || sum.length != G2_POINT_LEN) revert G2AddFailed(); + acc = sum; + } + + return acc; + } + + /** + * @notice Compress an uncompressed G2 point to the 96 byte form the relay chain commits to. + * + * @dev The keyset commitment is built over compressed keys, but EIP-2537 only accepts + * uncompressed ones, so the prover sends uncompressed and this derives the compressed form for + * the merkle leaf. Going this direction is cheap: take `x` and set two flag bits. The reverse + * would need an Fp2 square root, which is why the proof does not simply carry compressed keys. + * + * The layout is `x.c1 || x.c0`, c1 first, with the flags in the top bits of the first byte. + * The sign bit tracks `y.c1`, confirmed against `bls_compression_rule` in the Rust verifier. + */ + function compressG2(bytes memory point) internal pure returns (bytes memory) { + if (point.length != G2_POINT_LEN) revert InvalidPointLength(); + + bytes memory out = new bytes(96); + for (uint256 i = 0; i < 48; ++i) { + // x.c1 occupies bytes 64..128, its value in the trailing 48; x.c0 is bytes 0..64. + out[i] = point[80 + i]; + out[48 + i] = point[16 + i]; + } + + uint8 flags = COMPRESSION_FLAG; + // y.c1 occupies bytes 192..256, its value in the trailing 48. + if (greaterThanHalfModulus(point, 208)) flags |= SIGN_FLAG; + out[0] = bytes1(uint8(out[0]) | flags); + + return out; + } + + /// @dev Whether the 48 byte big-endian value at `offset` exceeds `(p - 1) / 2`. + function greaterThanHalfModulus(bytes memory value, uint256 offset) internal pure returns (bool) { + for (uint256 i = 0; i < 48; ++i) { + uint8 a = uint8(value[offset + i]); + uint8 b = uint8(HALF_MODULUS[i]); + if (a != b) return a > b; + } + return false; + } +} diff --git a/evm/src/consensus/BlsBeefy.sol b/evm/src/consensus/BlsBeefy.sol index d9571872f..eadf5a8d2 100644 --- a/evm/src/consensus/BlsBeefy.sol +++ b/evm/src/consensus/BlsBeefy.sol @@ -12,32 +12,289 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -pragma solidity ^0.8.17; +pragma solidity ^0.8.30; -import {BlsBeefyConsensusProof, BeefyConsensusState} from "./Types.sol"; +import {IConsensusV2, IntermediateState, StateCommitment} from "@hyperbridge/core/interfaces/IConsensusV2.sol"; +import {MerkleMultiProof} from "@polytope-labs/solidity-merkle-trees/src/MerkleMultiProof.sol"; +import {MerkleMountainRange} from "@polytope-labs/solidity-merkle-trees/src/MerkleMountainRange.sol"; +import {ScaleCodec} from "@polytope-labs/solidity-merkle-trees/src/trie/polkadot/ScaleCodec.sol"; +import {Bytes} from "@polytope-labs/solidity-merkle-trees/src/trie/Bytes.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +import {Codec} from "./Codec.sol"; +import { + Header, + HeaderImpl, + AuthoritySetCommitment, + BlsBeefyConsensusProof, + BlsRelayChainProof, + BlsSigner, + Commitment, + BeefyConsensusState, + PartialBeefyMmrLeaf, + Parachain, + ParachainProof +} from "./Types.sol"; +import {BlsAggregate} from "./BlsAggregate.sol"; /** - * @title The aggregate BLS12-381 BEEFY consensus proof types. + * @title The aggregate BLS12-381 BEEFY consensus client. * @author Polytope Labs (hello@polytope.technology) * - * @notice Carries the ABI shape of a BLS BEEFY proof. The verifier itself is not implemented here - * yet; the Rust light client verifies these proofs today, and this exists so both sides agree on - * the encoding before the on-chain verifier is written. + * @notice Verifies BEEFY finality by checking one aggregate BLS signature rather than recovering + * each authority's ECDSA signature. The signature check is then flat in the size of the validator + * set, where [`EcdsaBeefy`] pays one `ecrecover` per signature. Everything else, MMR leaf + * inclusion and parachain header proofs, is the same work as the ECDSA client does. + * + * @dev The verification flow is: + * 1. Confirm the commitment's validator set id matches a known authority set. + * 2. Confirm enough validators signed to meet the supermajority threshold. + * 3. Confirm the signers' public keys are in the authority set via a merkle multi-proof, and that + * no signer is claimed twice. + * 4. Verify the aggregate signature in one pairing check. + * 5. Extract the MMR root from the commitment payload and verify the latest MMR leaf inclusion. + * 6. Verify parachain header inclusion in the leaf's parachain heads root and decode the + * finalized state commitments. + * + * Stale proofs are a no-op, matching the ECDSA client, so replays are idempotent. * - * @dev When the verifier lands it verifies the aggregate in one pairing check regardless of how - * many validators signed, which is the point of the BLS path. The steps will be: - * 1. Hash the SCALE-encoded commitment onto G1. Note BEEFY does not use the IETF ciphersuite - * string as the domain separation tag: `w3f-bls` builds its hasher with a one-byte DST of - * 0x01 and prepends the ciphersuite to the message instead. Reproducing that exactly is the - * make-or-break step, and `bls_hash_to_curve_vector` in the Rust verifier pins a test vector. - * 2. Sum the signers' G2 public keys and check the aggregate against the keyset commitment with - * a merkle multi-proof, rejecting repeated or unordered signer indices. - * 3. One EIP-2537 pairing check over the summed key and the aggregate signature. + * Two encoding notes that are easy to get wrong: + * - Public keys are taken **uncompressed**, because EIP-2537 has no decompression precompile. + * The keyset commitment is still over the compressed encoding, as the runtime builds it; the + * contract compresses each key to compute its merkle leaf, which is cheap in that direction. + * - BEEFY puts signatures in G1 and public keys in G2, the opposite of the Ethereum convention, + * so an eth2 BLS library does not transfer. * - * Signatures live in G1 (48 bytes) and public keys in G2 (96 bytes), the opposite of the Ethereum - * convention, so an eth2 BLS library does not transfer. + * Requires an EVM with EIP-2537, so Prague or later. */ -contract BlsBeefy { +contract BlsBeefy is IConsensusV2, ERC165 { + using HeaderImpl for Header; + + /// @dev The PayloadId for the mmr root. + bytes2 public constant MMR_ROOT_PAYLOAD_ID = bytes2("mh"); + + /// @dev Provided authority set id was unknown. + error UnknownAuthoritySet(); + /// @dev Mmr root hash was not found in the commitment payload. + error MmrRootHashMissing(); + /// @dev Provided Mmr proof was invalid. + error InvalidMmrProof(); + /// @dev Genesis block should not be provided. + error IllegalGenesisBlock(); + /// @dev Supermajority not reached. + error SuperMajorityRequired(); + /// @dev Provided authorities proof was invalid. + error InvalidAuthoritiesProof(); + /// @dev Signer indices must strictly ascend and lie inside the authority set. + error InvalidSignerOrdering(); + /// @dev The aggregate pairing check rejected the signature. + error InvalidAggregateSignature(); + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IConsensusV2).interfaceId || super.supportsInterface(interfaceId); + } + + /// @dev IConsensusV2 entry point. Decodes the proof, verifies consensus, and returns the + /// updated state along with the latest authority set id. + function verify(bytes calldata previousState, bytes calldata proof) + external + view + returns (bytes memory, IntermediateState[] memory, uint256) + { + BeefyConsensusState memory consensusState = abi.decode(previousState, (BeefyConsensusState)); + (BlsRelayChainProof memory relay, ParachainProof memory parachain) = + abi.decode(proof, (BlsRelayChainProof, ParachainProof)); + + // Stale proofs are a no-op: return the previous state with no intermediates so the caller + // can treat replays as idempotent rather than having to guard against reverts. + if (consensusState.latestHeight >= relay.commitment.blockNumber) { + return (abi.encode(consensusState), new IntermediateState[](0), consensusState.nextAuthoritySet.id); + } + + (BeefyConsensusState memory newState, bytes32 headsRoot) = verifyMmrUpdateProof(consensusState, relay); + IntermediateState[] memory intermediates = verifyParachainHeaderProof(headsRoot, parachain); + + return (abi.encode(newState), intermediates, newState.nextAuthoritySet.id); + } + + /** + * @dev Verifies a new mmr root update. The relay chain accumulates its blocks into a merkle + * mountain range, and the new root is signed by the authority set. Here that signature is a + * single aggregate rather than one per validator, so establishing which validators signed is + * a merkle multi-proof of their public keys plus one pairing check. + */ + function verifyMmrUpdateProof(BeefyConsensusState memory trustedState, BlsRelayChainProof memory relayProof) + internal + view + returns (BeefyConsensusState memory, bytes32) + { + Commitment memory commitment = relayProof.commitment; + if ( + commitment.validatorSetId != trustedState.currentAuthoritySet.id + && commitment.validatorSetId != trustedState.nextAuthoritySet.id + ) { + revert UnknownAuthoritySet(); + } + + bool isCurrentAuthorities = commitment.validatorSetId == trustedState.currentAuthoritySet.id; + AuthoritySetCommitment memory authoritySet = + isCurrentAuthorities ? trustedState.currentAuthoritySet : trustedState.nextAuthoritySet; + + verifyAuthorities( + Codec.Encode(commitment), + relayProof.signers, + relayProof.aggregateSignature, + authoritySet.root, + relayProof.proof, + authoritySet.len + ); + + uint256 payloadLength = commitment.payload.length; + bytes32 mmrRoot; + for (uint256 i = 0; i < payloadLength; i++) { + if (commitment.payload[i].id == MMR_ROOT_PAYLOAD_ID && commitment.payload[i].data.length == 32) { + mmrRoot = Bytes.toBytes32(commitment.payload[i].data); + } + } + if (mmrRoot == bytes32(0)) revert MmrRootHashMissing(); + + verifyMmrLeaf(trustedState, relayProof, mmrRoot); + + if (relayProof.latestMmrLeaf.nextAuthoritySet.id > trustedState.nextAuthoritySet.id) { + trustedState.currentAuthoritySet = trustedState.nextAuthoritySet; + trustedState.nextAuthoritySet = relayProof.latestMmrLeaf.nextAuthoritySet; + } + trustedState.latestHeight = commitment.blockNumber; + + return (trustedState, relayProof.latestMmrLeaf.extra); + } + + /** + * @notice Establish that a supermajority of the committed authority set signed `commitment`. + * @param commitment the SCALE-encoded BEEFY commitment that was signed + * @param signers the validators claiming to have signed, in strictly ascending index order + * @param aggregateSignature the summed G1 signature, 128 bytes uncompressed + * @param keysetRoot the authority set's keyset commitment + * @param authorityProof merkle multi-proof of the signers' keys against `keysetRoot` + * @param authorityCount total validators in the set + */ + function verifyAuthorities( + bytes memory commitment, + BlsSigner[] memory signers, + bytes memory aggregateSignature, + bytes32 keysetRoot, + bytes32[] memory authorityProof, + uint256 authorityCount + ) public view { + uint256 count = signers.length; + if (!checkParticipationThreshold(count, authorityCount)) revert SuperMajorityRequired(); + + // Strictly ascending and inside the set. Without this a prover could repeat one signer to + // clear the threshold, and the aggregate would happily verify the repeated key against the + // repeated signature. + for (uint256 i = 0; i < count; ++i) { + if (signers[i].authorityIndex >= authorityCount) revert InvalidSignerOrdering(); + if (i > 0 && signers[i].authorityIndex <= signers[i - 1].authorityIndex) { + revert InvalidSignerOrdering(); + } + } + + // The pairing check only proves the holders of these keys signed. Proving those keys are + // the authority set's is what the multi-proof is for. + MerkleMultiProof.Leaf[] memory leaves = new MerkleMultiProof.Leaf[](count); + bytes[] memory publicKeys = new bytes[](count); + for (uint256 i = 0; i < count; ++i) { + publicKeys[i] = signers[i].publicKey; + // The runtime commits the compressed encoding, so derive it here rather than asking + // the prover to send both forms, which would then need checking for consistency. + leaves[i] = MerkleMultiProof.Leaf({ + index: signers[i].authorityIndex, hash: keccak256(BlsAggregate.compressG2(signers[i].publicKey)) + }); + } + + if (!MerkleMultiProof.VerifyProof(keysetRoot, authorityProof, leaves, authorityCount)) { + revert InvalidAuthoritiesProof(); + } + + if (!BlsAggregate.verify(commitment, aggregateSignature, publicKeys)) { + revert InvalidAggregateSignature(); + } + } + + // @dev Stack too deep, sigh solidity + function verifyMmrLeaf(BeefyConsensusState memory trustedState, BlsRelayChainProof memory relay, bytes32 mmrRoot) + internal + pure + { + bytes32 hash = keccak256( + Codec.Encode( + PartialBeefyMmrLeaf({ + version: relay.latestMmrLeaf.version, + parentNumber: relay.latestMmrLeaf.parentNumber, + parentHash: relay.latestMmrLeaf.parentHash, + nextAuthoritySet: relay.latestMmrLeaf.nextAuthoritySet, + extra: relay.latestMmrLeaf.extra + }) + ) + ); + uint256 leafCount = leafIndex(trustedState.beefyActivationBlock, relay.latestMmrLeaf.parentNumber) + 1; + MerkleMountainRange.Leaf[] memory leaves = new MerkleMountainRange.Leaf[](1); + leaves[0] = MerkleMountainRange.Leaf({index: relay.latestMmrLeaf.leafIndex, hash: hash}); + bool valid = MerkleMountainRange.VerifyProof(mmrRoot, relay.mmrProof, leaves, leafCount); + + if (!valid) revert InvalidMmrProof(); + } + + // @dev Verifies that some parachain header has been finalized, given the current trusted state. + function verifyParachainHeaderProof(bytes32 headsRoot, ParachainProof memory proof) + internal + pure + returns (IntermediateState[] memory) + { + uint256 len = proof.parachains.length; + MerkleMultiProof.Leaf[] memory leaves = new MerkleMultiProof.Leaf[](len); + IntermediateState[] memory intermediates = new IntermediateState[](len); + + for (uint256 i = 0; i < len; i++) { + Parachain memory para = proof.parachains[i]; + Header memory header = Codec.DecodeHeader(para.header); + if (header.number == 0) revert IllegalGenesisBlock(); + + leaves[i] = MerkleMultiProof.Leaf( + para.index, + keccak256(bytes.concat(ScaleCodec.encode32(uint32(para.id)), ScaleCodec.encodeBytes(para.header))) + ); + + StateCommitment memory commitment = header.stateCommitment(); + intermediates[i] = + IntermediateState({stateMachineId: para.id, height: header.number, commitment: commitment}); + } + + if (len > 0) { + bool valid = MerkleMultiProof.VerifyProof(headsRoot, proof.proof, leaves, proof.leafCount); + if (!valid) revert InvalidMmrProof(); + } + + return intermediates; + } + + // @dev Calculates the mmr leaf index for a block whose parent number is given. + function leafIndex(uint256 activationBlock, uint256 parentNumber) internal pure returns (uint256) { + if (activationBlock == 0) { + return parentNumber; + } else { + return parentNumber - activationBlock; + } + } + + /// @notice The BEEFY supermajority: more than two thirds of the set. + function checkParticipationThreshold(uint256 len, uint256 total) public pure returns (bool) { + return len >= ((2 * total) / 3) + 1; + } + // @dev so these structs are included in the abi function noOp(BeefyConsensusState memory s, BlsBeefyConsensusProof memory p) external pure {} } diff --git a/evm/tests/foundry/BlsAggregate.t.sol b/evm/tests/foundry/BlsAggregate.t.sol new file mode 100644 index 000000000..9be1d9e08 --- /dev/null +++ b/evm/tests/foundry/BlsAggregate.t.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {BlsAggregate} from "../../src/consensus/BlsAggregate.sol"; +import {BlsBeefy} from "../../src/consensus/BlsBeefy.sol"; +import {BlsSigner} from "../../src/consensus/Types.sol"; + +/** + * @title Cross-language check of the aggregate BLS pairing. + * + * @notice Every value here comes from `bls_eip2537_fixture` in the Rust verifier, generated with + * the same `w3f-bls` code path substrate's BEEFY signers use. That test asserts the aggregate + * verifies in Rust before emitting anything, so a disagreement here is unambiguously a fault on + * the EVM side. + * + * Needs EIP-2537: + * FOUNDRY_PROFILE=bls forge test --match-contract BlsAggregateTest -vv + */ +contract BlsAggregateTest is Test { + bytes constant MESSAGE = "beefy-bls-aggregate-fixture"; + + /// Aggregate signature over MESSAGE by all three signers, G1. + bytes constant SIG_X = + hex"16d4d9d336cc264d0f9ddc9086bb5ce441d98896b441253e4d2087aa0fb93a21e44d074d9ac98f525aac914608b001c0"; + bytes constant SIG_Y = + hex"011ae59b33a10da832193b8ec84bb52958d92c8acf571adad597856db4d40dd5455e1c8a5a59ae1af2afee7e8b36bef6"; + + /// The sum of the three public keys, G2. `sumG2` must reproduce this. + bytes constant AGG_X_C0 = + hex"05d4a0f9c007ba1cbe0d2b79006d52f8fdb417cb17e0b4a37740b72b2459ec4d030be125b47692d9f8bbd9e937291831"; + bytes constant AGG_X_C1 = + hex"120a8de45bee1aab433b63d9859f40b560f0b923ecc23bbd2bf41e90290c4ba97b31137e70ab3e18a0e3616c340cb584"; + bytes constant AGG_Y_C0 = + hex"0e5e2bce96aa5ae0b57f3da3add49b35c60ecc276eec38940a5a0cad2a7ddf5555e954bce53d5b2c34b2b4824742e60d"; + bytes constant AGG_Y_C1 = + hex"0530f71228f36639e9d197e48a3d27697d71acca411533eef32d0d602e4d8fd72f52edfff40ada6b634797bfa3b1a19f"; + + /// Build the 64 byte field element EIP-2537 expects: 16 zero bytes then the 48 byte value. + function _fp(bytes memory value) private pure returns (bytes memory) { + return bytes.concat(new bytes(16), value); + } + + function _g1(bytes memory x, bytes memory y) private pure returns (bytes memory) { + return bytes.concat(_fp(x), _fp(y)); + } + + function _g2(bytes memory xc0, bytes memory xc1, bytes memory yc0, bytes memory yc1) + private + pure + returns (bytes memory) + { + return bytes.concat(_fp(xc0), _fp(xc1), _fp(yc0), _fp(yc1)); + } + + function _signers() private pure returns (bytes[] memory keys) { + keys = new bytes[](3); + keys[0] = _g2( + hex"0eb912203efe065b9d9844025f9a85a43fdb21f4c8c0f31b39cc3bedcdbecce8ab3567f9cadbe965adebb7dd4a081ec1", + hex"14168206974b9223cc95e6e1f279f9d10e526aa172b5bd15b101b6f4997e2038ebcb02bfee1bca54f428162e17ade003", + hex"0dbe0ed3b59dbf3c217e879f885df4fce29af686888e77e984b69d6a07fe90b2a1acebc49d6a196c90a9307be82bb9c4", + hex"16bd04776624eab548fc58aeac9da7f75a618206620c9119d517b983b659ca196c2a0761e09a1ee6df87e1cd78bdd4d5" + ); + keys[1] = _g2( + hex"065a060f78222114141a2544d6207ebfe7784788e6310b3a58cebc36df948e387853ce8492c4c8f9d423ebabc6feb027", + hex"026bc8c3c41fb3bb78a7d97855098d8b32ec546433c4c523f62c471b75d89823e2485ff79f3c203961b62c9b8b08e4d1", + hex"0db39f4b44160911c089c2cf24621f3e1df13561ced1640ddafa4c885d0a80b3428156c709f375e00fdb230b5c109e46", + hex"00b7e2a03248e77666c8e12d4eaa31d451c477209a178134afd53ebcc701842206b2b7613ed4807f4600ce212c6679e8" + ); + keys[2] = _g2( + hex"0c2410d03233711f03a7242fd3a8bb141125ac84a07813d8cce6e366038f129d3ef348fc7dd14682e3db28a920d2c748", + hex"008cd3cbf5e8dd6aca7d5fb78061c360910691c3797bcd95a42cf5a45b0612e8555f6e118788872af47d231954914282", + hex"15c168dc4a702011de9bded446897040c88e51831293bbcc691fe85a7f42e6cb454d4931fe9348fa310f455a21ef47ae", + hex"08600a717fc096a40eeb7dba5194779267123c9fef22e3ebe85c34c1aa74928a89c211d9bfa7dc97a296fc7550acc58e" + ); + } + + /// `G2ADD` over the signers must land on the aggregate key Rust computed. + function test_sum_g2_matches_rust() public view { + bytes memory summed = BlsAggregate.sumG2(_signers()); + assertEq(summed, _g2(AGG_X_C0, AGG_X_C1, AGG_Y_C0, AGG_Y_C1), "aggregate key differs from Rust"); + } + + /// The whole thing: one pairing check over the aggregate. + function test_verify_aggregate() public view { + assertTrue(BlsAggregate.verify(MESSAGE, _g1(SIG_X, SIG_Y), _signers()), "aggregate signature should verify"); + } + + /// A different message must not verify, or the check proves nothing. + function test_rejects_wrong_message() public view { + assertFalse( + BlsAggregate.verify("a different commitment", _g1(SIG_X, SIG_Y), _signers()), + "signature over another message must not verify" + ); + } + + /// Dropping a signer from the key set breaks the aggregate, so a validator cannot be credited + /// with a signature they did not produce. + function test_rejects_missing_signer() public view { + bytes[] memory all = _signers(); + bytes[] memory subset = new bytes[](2); + subset[0] = all[0]; + subset[1] = all[1]; + + assertFalse( + BlsAggregate.verify(MESSAGE, _g1(SIG_X, SIG_Y), subset), + "aggregate must not verify against a subset of the signers" + ); + } + + /// Keyset commitment over the four authorities' compressed keys, as the runtime builds it. + bytes32 constant KEYSET_ROOT = 0xd220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a; + /// Multi-proof opening signers 0,1,2 out of 4. + bytes32 constant PROOF_NODE = 0x97e418e070e3ec967bccc1d0aaea6d787a7b68733451c5320cd32eec2be63df8; + + function _blsSigners() private pure returns (BlsSigner[] memory out) { + bytes[] memory keys = _signers(); + out = new BlsSigner[](3); + for (uint256 i = 0; i < 3; ++i) { + out[i] = BlsSigner({publicKey: keys[i], authorityIndex: i}); + } + } + + function _authorityProof() private pure returns (bytes32[] memory nodes) { + nodes = new bytes32[](1); + nodes[0] = PROOF_NODE; + } + + /// The authority half end to end: threshold, ordering, merkle membership, aggregate pairing. + /// Reverts on any failure, so reaching the end is the assertion. + function test_verify_authorities() public { + new BlsBeefy().verifyAuthorities(MESSAGE, _blsSigners(), _g1(SIG_X, SIG_Y), KEYSET_ROOT, _authorityProof(), 4); + } + + /// Repeating a signer must be rejected before any cryptography runs. + function test_rejects_duplicate_signer_index() public { + BlsSigner[] memory signers = _blsSigners(); + signers[2].authorityIndex = 1; + + BlsBeefy client = new BlsBeefy(); + vm.expectRevert(BlsBeefy.InvalidSignerOrdering.selector); + client.verifyAuthorities(MESSAGE, signers, _g1(SIG_X, SIG_Y), KEYSET_ROOT, _authorityProof(), 4); + } + + /// Two of four is short of the supermajority. + function test_rejects_sub_supermajority() public { + BlsSigner[] memory all = _blsSigners(); + BlsSigner[] memory two = new BlsSigner[](2); + two[0] = all[0]; + two[1] = all[1]; + + BlsBeefy client = new BlsBeefy(); + vm.expectRevert(BlsBeefy.SuperMajorityRequired.selector); + client.verifyAuthorities(MESSAGE, two, _g1(SIG_X, SIG_Y), KEYSET_ROOT, _authorityProof(), 4); + } + + /// Compression must reproduce exactly what `w3f-bls` serialises, or the merkle leaves will not + /// match the commitment. Signer 0 has the sign bit set (0xb4), signer 2 does not (0x80), so + /// both branches of the rule are covered. + function test_compress_matches_w3f_bls() public pure { + bytes[] memory keys = _signers(); + + assertEq( + BlsAggregate.compressG2(keys[0]), + hex"b4168206974b9223cc95e6e1f279f9d10e526aa172b5bd15b101b6f4997e2038ebcb02bfee1bca54f428162e17ade003" + hex"0eb912203efe065b9d9844025f9a85a43fdb21f4c8c0f31b39cc3bedcdbecce8ab3567f9cadbe965adebb7dd4a081ec1", + "signer 0 compression differs, sign bit set" + ); + + assertEq( + BlsAggregate.compressG2(keys[2]), + hex"808cd3cbf5e8dd6aca7d5fb78061c360910691c3797bcd95a42cf5a45b0612e8555f6e118788872af47d231954914282" + hex"0c2410d03233711f03a7242fd3a8bb141125ac84a07813d8cce6e366038f129d3ef348fc7dd14682e3db28a920d2c748", + "signer 2 compression differs, sign bit clear" + ); + } +} diff --git a/evm/tests/foundry/BlsBeefy.t.sol b/evm/tests/foundry/BlsBeefy.t.sol new file mode 100644 index 000000000..401c92f98 --- /dev/null +++ b/evm/tests/foundry/BlsBeefy.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {IntermediateState} from "@hyperbridge/core/interfaces/IConsensusV2.sol"; + +import {BlsBeefy} from "../../src/consensus/BlsBeefy.sol"; +import {BeefyConsensusState} from "../../src/consensus/Types.sol"; + +/** + * @title The BLS BEEFY consensus client's entry point. + * + * @notice `state` and `proof` come from `bls_solidity_proof_fixture` in the Rust verifier. The + * validators there sign the SCALE encoding of the same commitment this contract hashes, over a + * single-leaf MMR whose root the commitment carries, so every check in `verify()` runs against + * data the Rust side produced rather than anything constructed to suit the contract. + * + * Needs EIP-2537: + * FOUNDRY_PROFILE=bls forge test --match-contract BlsBeefyTest -vv + */ +contract BlsBeefyTest is Test { + BlsBeefy internal client; + + function setUp() public { + client = new BlsBeefy(); + } + + function _state() internal view returns (bytes memory) { + return vm.parseBytes(vm.readFile("tests/foundry/fixtures/bls-beefy-state.hex")); + } + + function _proof() internal view returns (bytes memory) { + return vm.parseBytes(vm.readFile("tests/foundry/fixtures/bls-beefy-proof.hex")); + } + + /// The whole client: authority set, threshold, ordering, merkle membership, aggregate pairing, + /// MMR leaf inclusion, then the state advances. + function test_verify_advances_state() public view { + (bytes memory newStateBytes,, uint256 nextSetId) = client.verify(_state(), _proof()); + + BeefyConsensusState memory newState = abi.decode(newStateBytes, (BeefyConsensusState)); + BeefyConsensusState memory oldState = abi.decode(_state(), (BeefyConsensusState)); + + assertGt(newState.latestHeight, oldState.latestHeight, "height should advance"); + assertEq(newState.latestHeight, 100, "height should match the proven commitment"); + assertGt(nextSetId, 0, "should report the next authority set id"); + } + + /// Replaying a proof the state has already passed is a no-op rather than a revert, so callers + /// do not have to guard against it. + function test_stale_proof_is_a_noop() public view { + (bytes memory advanced,,) = client.verify(_state(), _proof()); + + (bytes memory again, IntermediateState[] memory intermediates,) = client.verify(advanced, _proof()); + + assertEq(again, advanced, "state should be unchanged"); + assertEq(intermediates.length, 0, "a stale proof finalizes nothing"); + } + + function _liveState() internal view returns (bytes memory) { + return vm.parseBytes(vm.readFile("tests/foundry/fixtures/bls-beefy-live-state.hex")); + } + + function _liveProof() internal view returns (bytes memory) { + return vm.parseBytes(vm.readFile("tests/foundry/fixtures/bls-beefy-live-proof.hex")); + } + + /// The real thing: a proof built by the prover from a running BLS relay, carrying a genuine + /// parachain header and an MMR proof with actual depth, rather than a synthetic single leaf. + /// The keys arrive uncompressed and the client compresses them to rebuild the merkle leaves, + /// so the whole wire format is exercised too. + function test_verify_live_proof() public view { + (bytes memory newStateBytes, IntermediateState[] memory intermediates,) = + client.verify(_liveState(), _liveProof()); + + BeefyConsensusState memory newState = abi.decode(newStateBytes, (BeefyConsensusState)); + BeefyConsensusState memory oldState = abi.decode(_liveState(), (BeefyConsensusState)); + + assertGt(newState.latestHeight, oldState.latestHeight, "height should advance"); + assertEq(intermediates.length, 1, "should finalize the registered parachain"); + assertEq(intermediates[0].stateMachineId, 4009, "should be para 4009"); + assertGt(intermediates[0].height, 0, "parachain height should be non-zero"); + } +} diff --git a/evm/tests/foundry/fixtures/bls-beefy-live-proof.hex b/evm/tests/foundry/fixtures/bls-beefy-live-proof.hex new file mode 100644 index 000000000..44837a447 --- /dev/null +++ b/evm/tests/foundry/fixtures/bls-beefy-live-proof.hex @@ -0,0 +1 @@ +0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000005e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e671ba1f0c0553856eb6e3f31b940306808847c8cd497e88358802584f40435aaf20000000000000000000000000000000000000000000000000000000000000653000000000000000000000000000000000000000000000000000000000000000259354e6a156b4a39ffc56f03fa5b947e341c737964563c0fc16298aaeef002dc1cd8af3d10a89274653ab6c83f167e8bb6fb272d2c4b2bcba899fef93906235b0000000000000000000000000000000000000000000000000000000000007e670000000000000000000000000000000000000000000000000000000000000680000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000007e680000000000000000000000000000000000000000000000000000000000000652000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000202a38ea65b44e6249d92dd3e30a5f299687c05b6308ec296387bcd18e260f471e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000005223c1d776402ea1987eb80d8986897469773e64f60ac162da9bbd43fe64ae970bfbad858a466787d8712eda4fd59e8000000000000000000000000000000000d3987bf4729830439efa2577b0923d38f93f5cffccc059dfaa7779ff10756989b5121eb60c8773cd4a227669b1e430b00000000000000000000000000000000011f34f64d1aea99d50df61fa1c64e8a3708985ad07b2b18181eaa32c031f097005818a639a272ec26090af35aa59ddc0000000000000000000000000000000009940b83f82ccfed097bb7a4f60e93f002c8d726f905cee111aa2141a8e6f0b6bc5fdd8a461a4648dfc18a843366924600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000169a502b8c1834a867af71776a5d79a390dfc6d5db5eed75bc3b9140c54cf61ffd1ab3b15ca2c599d78124f900cf15ef000000000000000000000000000000001818d16b6726d50f1c88dd60266b7a17f717de8fe167db5786bf2dc2adc9bea57e7091b0ca47ef8b43a39425ec25328f00000000000000000000000000000000019fc5274bd3fc51719e140cb337d70c456468b7c5b73f404da5bea585b147a2fad7d570e879b2378033833fdd14bae1000000000000000000000000000000001833bfdffcf2febd0806514ac9d447e6de2ee170c7882f6bc90d70b033975aebd3f62bf8b8c9d0515db843c5ee55663b0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000794ffe09228c09c3084c0088072beaf2a5267ef4dbdc83e7ca828ec314285b10fc4cb3c3092f1da1d0f94927c6cc1a40000000000000000000000000000000010ba10b17a96746c73a62cdb1d352e48aaba0944d56a69a0f8739a046896d2a3cea344ff5f18faf790f5103d44e2506a000000000000000000000000000000000000000000000000000000000000000bb8d03c3213782fce37d6bbbc387ee63ee5b1c8f5ec2fbfcfaedcedbebe2c950d0cc4b1d1416891b2f3c58e5184b0f6257064b763518103dfcd96962563cb928155a61ed3328a80c041b7be84116134370276b533b6b78f2d8fd5e339c16f54bdf467e338a570f838cab69d4fe7b49ee3a65cf7224cf6f3a37e7a0f9c7c9b1271e6bf0aae9fafc93b638133e2a15ce37dbe3d1528c3f576181019d46ce602cff2e201df559b371d5d84e6917153780f6d9bce1efcc6d7db3ac48ff4d597d756aced3ed30b3a62adcdc851c3d6ee8c5aede1c2a6bae1d96f5bbf34fbe754725537c55e1a2e876268d8d68cb36551d3ca9d18ad59e0da7a637ef6cc1319e14104f165d4f7d7a39c5b7c141d63c76e41794d87030eae08b2d8d788ac15cfda22f9f80ca396e39db1bab9b90706c517379eff1bfe0a7651db3adf92fea74fdb1ed1e00f2d206705a8bd079325315d7557f9d0737e52a45f2b30decd69c5e5bd6bc77300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000137807e0e4d516f36ca37d3345d736030cb09587ade6cfa38afb5eb56ce5011a945d9666784ab8a71aead7b26d75ecd5194950ac88455a951c1d4d8a7250c22d2aa29c3098b806aa719802f482e1cf9296e18a4aac40bb5ae4f3e3da9b861d048930bea14066175726120e9e0bd1100000000045250535290c6f000aef94d1bc0ce7fbe6f01f35cf4e593e14f925db4a0b48d0660bf047da596f901000449534d5001010000000000000000000000000000000000000000000000000000000000000000bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a044953544d207645736a00000000056175726101015440033c1aad33b07a28a551bfd24feaaf7a1c0109ebef58b46bd91e8bd7143ad6fc76e73f8a77926f0d670118c14e8a13c38a12250e5cbae238d26fca455f8f0000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-beefy-live-state.hex b/evm/tests/foundry/fixtures/bls-beefy-live-state.hex new file mode 100644 index 000000000..1f646aff5 --- /dev/null +++ b/evm/tests/foundry/fixtures/bls-beefy-live-state.hex @@ -0,0 +1 @@ +0x0000000000000000000000000000000000000000000000000000000000007e5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000651000000000000000000000000000000000000000000000000000000000000000259354e6a156b4a39ffc56f03fa5b947e341c737964563c0fc16298aaeef002dc0000000000000000000000000000000000000000000000000000000000000652000000000000000000000000000000000000000000000000000000000000000259354e6a156b4a39ffc56f03fa5b947e341c737964563c0fc16298aaeef002dc \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-beefy-proof.hex b/evm/tests/foundry/fixtures/bls-beefy-proof.hex new file mode 100644 index 000000000..cdc1e0fee --- /dev/null +++ b/evm/tests/foundry/fixtures/bls-beefy-proof.hex @@ -0,0 +1 @@ +0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008a000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000076000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004d220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000820000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000200c32cca89bdaf348159b895a807cd49d3dff31f124c35c29eae09fe89ad2c03e0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000eb912203efe065b9d9844025f9a85a43fdb21f4c8c0f31b39cc3bedcdbecce8ab3567f9cadbe965adebb7dd4a081ec10000000000000000000000000000000014168206974b9223cc95e6e1f279f9d10e526aa172b5bd15b101b6f4997e2038ebcb02bfee1bca54f428162e17ade003000000000000000000000000000000000dbe0ed3b59dbf3c217e879f885df4fce29af686888e77e984b69d6a07fe90b2a1acebc49d6a196c90a9307be82bb9c40000000000000000000000000000000016bd04776624eab548fc58aeac9da7f75a618206620c9119d517b983b659ca196c2a0761e09a1ee6df87e1cd78bdd4d500000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000065a060f78222114141a2544d6207ebfe7784788e6310b3a58cebc36df948e387853ce8492c4c8f9d423ebabc6feb02700000000000000000000000000000000026bc8c3c41fb3bb78a7d97855098d8b32ec546433c4c523f62c471b75d89823e2485ff79f3c203961b62c9b8b08e4d1000000000000000000000000000000000db39f4b44160911c089c2cf24621f3e1df13561ced1640ddafa4c885d0a80b3428156c709f375e00fdb230b5c109e460000000000000000000000000000000000b7e2a03248e77666c8e12d4eaa31d451c477209a178134afd53ebcc701842206b2b7613ed4807f4600ce212c6679e8000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000c2410d03233711f03a7242fd3a8bb141125ac84a07813d8cce6e366038f129d3ef348fc7dd14682e3db28a920d2c74800000000000000000000000000000000008cd3cbf5e8dd6aca7d5fb78061c360910691c3797bcd95a42cf5a45b0612e8555f6e118788872af47d2319549142820000000000000000000000000000000015c168dc4a702011de9bded446897040c88e51831293bbcc691fe85a7f42e6cb454d4931fe9348fa310f455a21ef47ae0000000000000000000000000000000008600a717fc096a40eeb7dba5194779267123c9fef22e3ebe85c34c1aa74928a89c211d9bfa7dc97a296fc7550acc58e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000013dd22814659a34a3c64b86f1691750b49e54d81e38b98c9b8908843d15e178f5d56b368894c3060d4599e69e49fc41f0000000000000000000000000000000014da396dc7edfda14e4aecde657d465416676424529515978507e9866e0b59cc6f0c74ae66c25a53942aa814fb1435970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000197e418e070e3ec967bccc1d0aaea6d787a7b68733451c5320cd32eec2be63df800000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-beefy-state.hex b/evm/tests/foundry/fixtures/bls-beefy-state.hex new file mode 100644 index 000000000..07221356f --- /dev/null +++ b/evm/tests/foundry/fixtures/bls-beefy-state.hex @@ -0,0 +1 @@ +0x0000000000000000000000000000000000000000000000000000000000000063000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000004d220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a00000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004d220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a \ No newline at end of file diff --git a/modules/consensus/beefy/primitives/src/lib.rs b/modules/consensus/beefy/primitives/src/lib.rs index 3460de0b0..4fe175080 100644 --- a/modules/consensus/beefy/primitives/src/lib.rs +++ b/modules/consensus/beefy/primitives/src/lib.rs @@ -135,6 +135,62 @@ pub const BLS_G1_SIGNATURE_LEN: usize = 48; /// Size of a compressed BLS12-381 G2 point, the group BEEFY public keys live in. pub const BLS_G2_PUBLIC_KEY_LEN: usize = 96; +/// Size of an uncompressed BLS12-381 G2 point as EIP-2537 encodes it: four 64 byte field +/// elements, `x.c0 || x.c1 || y.c0 || y.c1`. +pub const BLS_G2_UNCOMPRESSED_LEN: usize = 256; + +/// Size of an uncompressed BLS12-381 G1 point: `x || y`, each 64 bytes. +pub const BLS_G1_UNCOMPRESSED_LEN: usize = 128; + +/// `(p - 1) / 2` for the BLS12-381 base field, big-endian. +const HALF_MODULUS: [u8; 48] = [ + 0x0d, 0x00, 0x88, 0xf5, 0x1c, 0xbf, 0xf3, 0x4d, 0x25, 0x8d, 0xd3, 0xdb, 0x21, 0xa5, 0xd6, 0x6b, + 0xb2, 0x3b, 0xa5, 0xc2, 0x79, 0xc2, 0x89, 0x5f, 0xb3, 0x98, 0x69, 0x50, 0x7b, 0x58, 0x7b, 0x12, + 0x0f, 0x55, 0xff, 0xff, 0x58, 0xa9, 0xff, 0xff, 0xdc, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xd5, 0x55, +]; + +/// Compress an uncompressed G2 point into the 96 byte form the relay chain commits to. +/// +/// EIP-2537 only accepts uncompressed points, so an EVM-bound proof carries those, while the +/// keyset commitment and the Rust verifier both work on the compressed encoding. Converting this +/// direction is pure byte manipulation: take `x`, ordered `c1` then `c0`, and set the compression +/// flag plus a sign bit recording which square root `y` is. The reverse would need an Fp2 square +/// root, which is why proofs never travel compressed to the EVM. +pub fn compress_g2(point: &[u8; BLS_G2_UNCOMPRESSED_LEN]) -> [u8; BLS_G2_PUBLIC_KEY_LEN] { + let mut out = [0u8; BLS_G2_PUBLIC_KEY_LEN]; + // Each 64 byte field element carries its 48 byte value in the trailing bytes. + out[..48].copy_from_slice(&point[80..128]); // x.c1 + out[48..].copy_from_slice(&point[16..64]); // x.c0 + + let y_c1 = &point[208..256]; + let sign = y_c1 > &HALF_MODULUS[..]; + + out[0] |= 0x80; // compression flag + if sign { + out[0] |= 0x20; + } + + out +} + +/// Compress an uncompressed G1 point into the 48 byte form `w3f-bls` serialises. +/// +/// Same convention as [`compress_g2`], with a single field element instead of a pair. +pub fn compress_g1(point: &[u8; BLS_G1_UNCOMPRESSED_LEN]) -> [u8; BLS_G1_SIGNATURE_LEN] { + let mut out = [0u8; BLS_G1_SIGNATURE_LEN]; + out.copy_from_slice(&point[16..64]); // x + + let y = &point[80..128]; + let sign = y > &HALF_MODULUS[..]; + + out[0] |= 0x80; + if sign { + out[0] |= 0x20; + } + + out +} + /// A validator that contributed to an aggregate BLS signature. /// /// Only the public key is carried. The individual signatures are summed by the prover into diff --git a/modules/consensus/beefy/prover/Cargo.toml b/modules/consensus/beefy/prover/Cargo.toml index 971a25945..ab16dcfea 100644 --- a/modules/consensus/beefy/prover/Cargo.toml +++ b/modules/consensus/beefy/prover/Cargo.toml @@ -20,9 +20,17 @@ hex-literal = "0.4.1" hex = { version = "0.4.3" } w3f-bls = { version = "0.1.9", default-features = true, optional = true } +ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = false, optional = true } +ark-ec = { version = "0.4.0", default-features = false, optional = true } +ark-ff = { version = "0.4.0", default-features = false, optional = true } +ark-serialize = { version = "0.4.0", default-features = false, optional = true } + subxt = { workspace = true, default-features = true } subxt-core = { workspace = true, default-features = true } beefy-verifier-primitives = { workspace = true } + +ismp-abi = { workspace = true, default-features = true, optional = true } +alloy-primitives = { workspace = true, default-features = true, optional = true } merkle-mountain-range = { workspace = true } indicatif = "0.18.0" futures = { workspace = true } @@ -62,4 +70,4 @@ local = [] bls = [] # Prove the BLS half instead: `crate::bls`, which builds a proof the aggregate verifier checks in # a single pairing. Purely additive, so it is safe to enable alongside plain-ECDSA proving. -bls-aggregate = ["dep:w3f-bls"] +bls-aggregate = ["dep:w3f-bls", "dep:ismp-abi", "dep:alloy-primitives", "dep:ark-bls12-381", "dep:ark-ec", "dep:ark-ff", "dep:ark-serialize"] diff --git a/modules/consensus/beefy/prover/src/bls.rs b/modules/consensus/beefy/prover/src/bls.rs index 52ba3ccd3..a199f5fa0 100644 --- a/modules/consensus/beefy/prover/src/bls.rs +++ b/modules/consensus/beefy/prover/src/bls.rs @@ -193,3 +193,131 @@ impl Prover { Ok(BlsConsensusMessage { mmr, parachain }) } } + +/// Building an EVM-bound proof. +/// +/// The SCALE proof carries compressed points, which is what the Rust verifier and the keyset +/// commitment work on. EIP-2537 accepts only uncompressed ones, so an EVM submission has to +/// decompress. That needs curve arithmetic, which is why it lives here rather than in `ismp-abi`, +/// a crate that compiles into the runtime. The contract compresses again to rebuild the merkle +/// leaves, which is cheap in that direction. +pub mod abi { + use anyhow::anyhow; + use ark_bls12_381::{G1Affine, G2Affine}; + use ark_ec::AffineRepr; + use ark_ff::{BigInteger, PrimeField}; + use ark_serialize::CanonicalDeserialize; + use beefy_verifier_primitives::{ + BlsConsensusMessage, BLS_G1_SIGNATURE_LEN, BLS_G1_UNCOMPRESSED_LEN, BLS_G2_PUBLIC_KEY_LEN, + BLS_G2_UNCOMPRESSED_LEN, + }; + use ismp_abi::bls_beefy::BlsBeefy; + + /// Expand a compressed G2 public key into the EIP-2537 encoding: four 64 byte field elements, + /// each a 48 byte big-endian value with 16 bytes of leading zeroes. + pub fn decompress_g2( + compressed: &[u8; BLS_G2_PUBLIC_KEY_LEN], + ) -> Result<[u8; BLS_G2_UNCOMPRESSED_LEN], anyhow::Error> { + let point = G2Affine::deserialize_compressed(&compressed[..]) + .map_err(|e| anyhow!("invalid compressed G2 point: {e:?}"))?; + let (x, y) = point.xy().ok_or_else(|| anyhow!("G2 point is the identity"))?; + + let mut out = [0u8; BLS_G2_UNCOMPRESSED_LEN]; + for (slot, coord) in [&x.c0, &x.c1, &y.c0, &y.c1].iter().enumerate() { + let bytes = coord.into_bigint().to_bytes_be(); + out[slot * 64 + 16..slot * 64 + 64].copy_from_slice(&bytes); + } + + Ok(out) + } + + /// Expand a compressed G1 signature into the EIP-2537 encoding. + pub fn decompress_g1( + compressed: &[u8; BLS_G1_SIGNATURE_LEN], + ) -> Result<[u8; BLS_G1_UNCOMPRESSED_LEN], anyhow::Error> { + let point = G1Affine::deserialize_compressed(&compressed[..]) + .map_err(|e| anyhow!("invalid compressed G1 point: {e:?}"))?; + let (x, y) = point.xy().ok_or_else(|| anyhow!("G1 point is the identity"))?; + + let mut out = [0u8; BLS_G1_UNCOMPRESSED_LEN]; + for (slot, coord) in [x, y].iter().enumerate() { + let bytes = coord.into_bigint().to_bytes_be(); + out[slot * 64 + 16..slot * 64 + 64].copy_from_slice(&bytes); + } + + Ok(out) + } + + /// Convert a consensus proof into the ABI shape the Solidity client consumes. + pub fn to_abi_proof( + message: BlsConsensusMessage, + ) -> Result { + use alloy_primitives::{Bytes, FixedBytes, U256}; + + let mmr = message.mmr; + let leaf_index = mmr.mmr_proof.leaf_indices.first().copied().unwrap_or_default(); + + let signers = mmr + .signers + .iter() + .map(|signer| { + Ok(BlsBeefy::BlsSigner { + publicKey: Bytes::from(decompress_g2(&signer.public_key)?.to_vec()), + authorityIndex: U256::from(signer.index), + }) + }) + .collect::, anyhow::Error>>()?; + + let relay = BlsBeefy::BlsRelayChainProof { + commitment: BlsBeefy::Commitment { + payload: vec![BlsBeefy::Payload { + id: FixedBytes(*b"mh"), + data: Bytes::from( + mmr.commitment + .payload + .get_raw(b"mh") + .ok_or_else(|| anyhow!("mmr payload not present"))? + .clone(), + ), + }], + blockNumber: mmr.commitment.block_number, + validatorSetId: mmr.commitment.validator_set_id, + }, + signers, + aggregateSignature: Bytes::from(decompress_g1(&mmr.aggregate_signature)?.to_vec()), + latestMmrLeaf: BlsBeefy::BeefyMmrLeaf { + version: 0, + parentNumber: mmr.latest_mmr_leaf.parent_number_and_hash.0, + parentHash: FixedBytes(mmr.latest_mmr_leaf.parent_number_and_hash.1 .0), + nextAuthoritySet: BlsBeefy::AuthoritySetCommitment { + id: mmr.latest_mmr_leaf.beefy_next_authority_set.id, + len: mmr.latest_mmr_leaf.beefy_next_authority_set.len, + root: FixedBytes( + mmr.latest_mmr_leaf.beefy_next_authority_set.keyset_commitment.0, + ), + }, + extra: FixedBytes(mmr.latest_mmr_leaf.leaf_extra.0), + leafIndex: U256::from(leaf_index), + }, + mmrProof: mmr.mmr_proof.items.iter().map(|h| FixedBytes(h.0)).collect(), + proof: mmr.authority_proof.iter().map(|h| FixedBytes(*h)).collect(), + }; + + let parachain = BlsBeefy::ParachainProof { + parachains: message + .parachain + .parachains + .iter() + .map(|para| BlsBeefy::Parachain { + index: U256::from(para.index), + id: U256::from(para.para_id), + header: Bytes::from(para.header.clone()), + }) + .collect(), + proof: message.parachain.proof.iter().map(|h| FixedBytes(*h)).collect(), + leafCount: U256::from(message.parachain.total_leaves), + }; + + Ok(BlsBeefy::BlsBeefyConsensusProof { relay, parachain }) + } +} diff --git a/modules/consensus/beefy/verifier/Cargo.toml b/modules/consensus/beefy/verifier/Cargo.toml index 534925230..38e8eeb6c 100644 --- a/modules/consensus/beefy/verifier/Cargo.toml +++ b/modules/consensus/beefy/verifier/Cargo.toml @@ -37,6 +37,7 @@ hex-literal = { workspace = true } hex = { workspace = true, default-features = true } beefy-prover = { workspace = true } ismp-abi = { workspace = true, default-features = true } +alloy-primitives = { workspace = true, default-features = true } subxt = { workspace = true, default-features = true } subxt-core = { workspace = true, default-features = true } subxt-utils = { workspace = true, default-features = true } diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index 2ab9e5bed..2cfc7e135 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -1295,11 +1295,11 @@ mod bls_offline { assert!(matches!(verify(trusted_state, proof), Err(Error::BlsVerificationFailed))); } - // All-ones bytes are not undecodable. Arkworks keeps the point flags in the high bits of the - // last byte, and 0xff sets the infinity flag, so these decode to the identity element instead - // of failing. The identity is harmless (it contributes nothing to either sum, and a signer - // still has to appear in the committed keyset), but the rejection therefore arrives as a - // failed pairing rather than a decode error. + // All-ones bytes are not undecodable. The compressed encoding is big-endian with the point + // flags in the high bits of the *first* byte, and 0xff sets the infinity flag, so these decode + // to the identity element instead of failing. The identity is harmless (it contributes nothing + // to either sum, and a signer still has to appear in the committed keyset), but the rejection + // therefore arrives as a failed pairing rather than a decode error. #[test] fn rejects_an_identity_public_key() { let validators = validators(4); @@ -1357,3 +1357,472 @@ mod bls_offline { assert!(matches!(verify(trusted_state, proof), Err(Error::InvalidAuthoritiesProof))); } } + +/// Emits an EIP-2537 shaped fixture for the Solidity aggregate verifier. +/// +/// The Solidity side cannot consume the compressed points the proof carries, because EIP-2537 has +/// no decompression precompile: it wants uncompressed, big-endian, zero-padded coordinates. This +/// prints exactly that, for a deterministic validator set, and asserts `w3f-bls` accepts the same +/// aggregate first, so the fixture cannot drift from what the chain would produce. +/// +/// cargo test -p beefy-verifier --features bls-crypto bls_eip2537_fixture -- --nocapture +#[cfg(feature = "bls-crypto")] +#[test] +fn bls_eip2537_fixture() { + use ark_bls12_381::{Fq, G1Affine, G2Affine}; + use ark_ec::{AffineRepr, CurveGroup}; + use ark_ff::{BigInteger, PrimeField}; + use w3f_bls::{ + EngineBLS, Message, PublicKey, SecretKeyVT, SerializableToBytes, Signature as BlsSignature, + TinyBLS381, + }; + + let message = b"beefy-bls-aggregate-fixture"; + let msg = Message::new(b"", message); + + // Same deterministic construction the offline tests use. + let validators: Vec<_> = + (0..3).map(|i| SecretKeyVT::::from_seed(&[b'v', i as u8])).collect(); + + let mut agg_sig: Option<::SignatureGroup> = None; + let mut agg_pub: Option<::PublicKeyGroup> = None; + for secret in &validators { + let sig = secret.sign(&msg); + let public = secret.into_public(); + agg_sig = Some(agg_sig.map_or(sig.0, |acc| acc + sig.0)); + agg_pub = Some(agg_pub.map_or(public.0, |acc| acc + public.0)); + } + let agg_sig = agg_sig.expect("signers"); + let agg_pub = agg_pub.expect("signers"); + + // The aggregate must verify before the fixture is worth anything. + assert!( + BlsSignature::(agg_sig).verify(&msg, &PublicKey::(agg_pub)), + "aggregate does not verify, fixture would be meaningless" + ); + + let fq = |v: &Fq| hex::encode(v.into_bigint().to_bytes_be()); + + let sig_affine: G1Affine = agg_sig.into_affine(); + let (sx, sy) = sig_affine.xy().expect("signature is not the identity"); + + let pub_affine: G2Affine = agg_pub.into_affine(); + let (px, py) = pub_affine.xy().expect("public key is not the identity"); + + println!("=== EIP-2537 fixture: {} signers ===", validators.len()); + println!("message {}", hex::encode(message)); + println!( + "compressed signature {}", + hex::encode(BlsSignature::(agg_sig).to_bytes()) + ); + println!("compressed pubkey {}", hex::encode(PublicKey::(agg_pub).to_bytes())); + println!("-- aggregate signature, G1 uncompressed --"); + println!("sig.x {}", fq(sx)); + println!("sig.y {}", fq(sy)); + println!("-- aggregate public key, G2 uncompressed --"); + println!("pk.x.c0 {}", fq(&px.c0)); + println!("pk.x.c1 {}", fq(&px.c1)); + println!("pk.y.c0 {}", fq(&py.c0)); + println!("pk.y.c1 {}", fq(&py.c1)); + + // The keyset commitment the Solidity client verifies against, built over the *uncompressed* + // encoding since that is what the contract can hash without a decompression precompile. Four + // authorities, of which the three above signed, so the multi-proof is non-trivial. + let uncompressed = |secret: &SecretKeyVT| -> Vec { + let affine: G2Affine = secret.into_public().0.into_affine(); + let (x, y) = affine.xy().expect("public key is not the identity"); + let mut out = Vec::with_capacity(4 * 64); + for coord in [&x.c0, &x.c1, &y.c0, &y.c1] { + out.extend_from_slice(&[0u8; 16]); + out.extend_from_slice(&coord.into_bigint().to_bytes_be()); + } + out + }; + + let authorities: Vec<_> = + (0..4).map(|i| SecretKeyVT::::from_seed(&[b'v', i as u8])).collect(); + // The runtime commits the compressed encoding, and the contract compresses to match, so the + // leaves are over compressed keys. + let leaves: Vec<[u8; 32]> = authorities + .iter() + .map(|secret| keccak_256(&secret.into_public().to_bytes())) + .collect(); + let tree = MerkleTree::::from_leaves(&leaves); + let _ = &uncompressed; + + for (i, secret) in authorities.iter().enumerate() { + println!("authority {i} compressed {}", hex::encode(secret.into_public().to_bytes())); + } + println!("-- keyset over compressed keys, {} authorities --", authorities.len()); + println!("keyset root {}", hex::encode(tree.root().expect("root"))); + for hash in tree.proof(&[0, 1, 2]).proof_hashes() { + println!("multiproof node {}", hex::encode(hash)); + } + + // Each signer's key on its own, for the merkle leaves and the G2_ADD path. + for (i, secret) in validators.iter().enumerate() { + let affine: G2Affine = secret.into_public().0.into_affine(); + let (x, y) = affine.xy().expect("public key is not the identity"); + println!("-- signer {i} --"); + println!(" x.c0 {}", fq(&x.c0)); + println!(" x.c1 {}", fq(&x.c1)); + println!(" y.c0 {}", fq(&y.c0)); + println!(" y.c1 {}", fq(&y.c1)); + } +} + +/// Emits a complete ABI-encoded state and proof so the Solidity client's `verify()` entry point +/// can be exercised, not just the pieces it calls. +/// +/// The MMR is a single leaf, so its root is the leaf hash and an empty proof verifies. The +/// commitment carries that root, and the validators sign the SCALE encoding of the commitment, +/// which is exactly what the contract hashes. Keys are emitted uncompressed, because EIP-2537 has +/// no decompression precompile, and the keyset commitment is built over the same encoding. +/// +/// cargo test -p beefy-verifier --features bls-crypto bls_solidity_proof_fixture -- --nocapture +#[cfg(feature = "bls-crypto")] +#[test] +fn bls_solidity_proof_fixture() { + use alloy_sol_types::SolType; + use ark_bls12_381::G2Affine; + use ark_ec::{AffineRepr, CurveGroup}; + use ark_ff::{BigInteger, PrimeField}; + use ismp_abi::bls_beefy::BlsBeefy as Sol; + use w3f_bls::{ + EngineBLS, Message, SecretKeyVT, SerializableToBytes, Signature as BlsSignature, TinyBLS381, + }; + + const SET_ID: u64 = 7; + const BLOCK: u32 = 100; + + let uncompressed = |secret: &SecretKeyVT| -> Vec { + let affine: G2Affine = secret.into_public().0.into_affine(); + let (x, y) = affine.xy().expect("not the identity"); + let mut out = Vec::with_capacity(256); + for coord in [&x.c0, &x.c1, &y.c0, &y.c1] { + out.extend_from_slice(&[0u8; 16]); + out.extend_from_slice(&coord.into_bigint().to_bytes_be()); + } + out + }; + + let authorities: Vec<_> = + (0..4).map(|i| SecretKeyVT::::from_seed(&[b'v', i as u8])).collect(); + let keys: Vec> = authorities.iter().map(uncompressed).collect(); + + // Keyset commitment over the compressed keys, exactly as the runtime converter builds it. The + // contract compresses each uncompressed key it is given to reproduce these leaves. + let leaves: Vec<[u8; 32]> = + authorities.iter().map(|s| keccak_256(&s.into_public().to_bytes())).collect(); + let keyset = MerkleTree::::from_leaves(&leaves); + let keyset_root = keyset.root().expect("keyset root"); + + // The MMR leaf, and the root it implies as the only leaf in the tree. + let leaf = MmrLeaf { + version: MmrLeafVersion::new(0, 0), + parent_number_and_hash: (0u32, H256::zero()), + beefy_next_authority_set: BeefyNextAuthoritySet { + id: SET_ID + 1, + len: authorities.len() as u32, + keyset_commitment: H256(keyset_root), + }, + leaf_extra: H256::zero(), + }; + let mmr_root = H256(keccak_256(&leaf.encode())); + + // The validators sign the SCALE encoding of this commitment; the contract hashes the same. + let payload = Payload::from_single_entry(*b"mh", mmr_root.0.to_vec()); + let commitment = Commitment { payload, block_number: BLOCK, validator_set_id: SET_ID }; + let message = Message::new(b"", &commitment.encode()); + + let signer_indices = [0usize, 1, 2]; + let mut agg: Option<::SignatureGroup> = None; + for &i in &signer_indices { + let sig = authorities[i].sign(&message); + agg = Some(agg.map_or(sig.0, |acc| acc + sig.0)); + } + let agg_affine = BlsSignature::(agg.expect("signers")).0.into_affine(); + let (sx, sy) = agg_affine.xy().expect("not the identity"); + let mut aggregate_signature = Vec::with_capacity(128); + for coord in [sx, sy] { + aggregate_signature.extend_from_slice(&[0u8; 16]); + aggregate_signature.extend_from_slice(&coord.into_bigint().to_bytes_be()); + } + + let authority_proof = keyset.proof(&signer_indices); + + // Assemble the sol types the contract decodes. + let state = Sol::BeefyConsensusState { + latestHeight: alloy_primitives::U256::from(BLOCK - 1), + beefyActivationBlock: alloy_primitives::U256::ZERO, + currentAuthoritySet: Sol::AuthoritySetCommitment { + id: SET_ID, + len: authorities.len() as u32, + root: alloy_primitives::FixedBytes(keyset_root), + }, + nextAuthoritySet: Sol::AuthoritySetCommitment { + id: SET_ID + 1, + len: authorities.len() as u32, + root: alloy_primitives::FixedBytes(keyset_root), + }, + }; + + let relay = Sol::BlsRelayChainProof { + commitment: Sol::Commitment { + payload: vec![Sol::Payload { + id: alloy_primitives::FixedBytes(*b"mh"), + data: alloy_primitives::Bytes::from(mmr_root.0.to_vec()), + }], + blockNumber: BLOCK, + validatorSetId: SET_ID, + }, + signers: signer_indices + .iter() + .map(|&i| Sol::BlsSigner { + publicKey: alloy_primitives::Bytes::from(keys[i].clone()), + authorityIndex: alloy_primitives::U256::from(i), + }) + .collect(), + aggregateSignature: alloy_primitives::Bytes::from(aggregate_signature), + latestMmrLeaf: Sol::BeefyMmrLeaf { + version: 0, + parentNumber: 0, + parentHash: alloy_primitives::FixedBytes([0u8; 32]), + nextAuthoritySet: Sol::AuthoritySetCommitment { + id: SET_ID + 1, + len: authorities.len() as u32, + root: alloy_primitives::FixedBytes(keyset_root), + }, + extra: alloy_primitives::FixedBytes([0u8; 32]), + leafIndex: alloy_primitives::U256::ZERO, + }, + mmrProof: vec![], + proof: authority_proof + .proof_hashes() + .iter() + .map(|h| alloy_primitives::FixedBytes(*h)) + .collect(), + }; + + let parachain = Sol::ParachainProof { + parachains: vec![], + proof: vec![], + leafCount: alloy_primitives::U256::ZERO, + }; + + let encoded_state = Sol::BeefyConsensusState::abi_encode(&state); + let encoded_proof = + <(Sol::BlsRelayChainProof, Sol::ParachainProof) as SolType>::abi_encode_params(&( + relay, parachain, + )); + + println!("=== solidity verify() fixture ==="); + println!("state 0x{}", hex::encode(encoded_state)); + println!("proof 0x{}", hex::encode(encoded_proof)); + println!("expected new latestHeight {BLOCK}"); +} + +/// Works out the compressed-encoding sign rule, so the Solidity client can compress an +/// uncompressed key on the fly and match the leaf the runtime commits. +/// +/// Compressing is cheap; decompressing a G2 point on chain would need Fp2 square roots. So the +/// prover sends uncompressed points, which the pairing needs anyway, and the contract derives the +/// compressed form for the merkle leaf. That only works if the flag convention is pinned down. +/// +/// cargo test -p beefy-verifier --features bls-crypto bls_compression_rule -- --nocapture +#[cfg(feature = "bls-crypto")] +#[test] +fn bls_compression_rule() { + use ark_bls12_381::{Fq, G2Affine}; + use ark_ec::{AffineRepr, CurveGroup}; + use ark_ff::{BigInteger, PrimeField}; + use w3f_bls::{SecretKeyVT, SerializableToBytes, TinyBLS381}; + + // (p - 1) / 2, the threshold the IETF convention uses to call a root "larger". + let half = { + let modulus = Fq::MODULUS; + let mut bytes = modulus.to_bytes_be(); + // divide by two, big-endian, then subtract nothing: (p-1)/2 == p >> 1 for odd p + let mut carry = 0u8; + for b in bytes.iter_mut() { + let cur = *b; + *b = (cur >> 1) | (carry << 7); + carry = cur & 1; + } + bytes + }; + + let gt_half = |v: &Fq| -> bool { v.into_bigint().to_bytes_be() > half }; + + println!("=== compressed flag vs y sign, {} samples ===", 8); + for i in 0..8u8 { + let secret = SecretKeyVT::::from_seed(&[b'c', i]); + let compressed = secret.into_public().to_bytes(); + let affine: G2Affine = secret.into_public().0.into_affine(); + let (_, y) = affine.xy().expect("not the identity"); + + println!( + "seed {i}: flags {:#04x} y.c1>half {} y.c0>half {}", + compressed[0] & 0xe0, + gt_half(&y.c1), + gt_half(&y.c0), + ); + } +} + +/// `compress_g2` / `compress_g1` must reproduce what `w3f-bls` serialises, since the keyset +/// commitment and the Rust verifier both work on the compressed encoding while an EVM-bound proof +/// carries uncompressed points. +#[cfg(feature = "bls-crypto")] +#[test] +fn compression_matches_w3f_bls() { + use ark_bls12_381::{G1Affine, G2Affine}; + use ark_ec::{AffineRepr, CurveGroup}; + use ark_ff::{BigInteger, PrimeField}; + use beefy_verifier_primitives::{compress_g1, compress_g2}; + use w3f_bls::{Message, SecretKeyVT, SerializableToBytes, TinyBLS381}; + + let msg = Message::new(b"", b"compression check"); + + for i in 0..8u8 { + let secret = SecretKeyVT::::from_seed(&[b'c', i]); + + // G2 public key. + let affine: G2Affine = secret.into_public().0.into_affine(); + let (x, y) = affine.xy().expect("not the identity"); + let mut uncompressed = [0u8; 256]; + for (slot, coord) in [&x.c0, &x.c1, &y.c0, &y.c1].iter().enumerate() { + let bytes = coord.into_bigint().to_bytes_be(); + uncompressed[slot * 64 + 16..slot * 64 + 64].copy_from_slice(&bytes); + } + assert_eq!( + compress_g2(&uncompressed).to_vec(), + secret.into_public().to_bytes(), + "G2 compression differs for seed {i}" + ); + + // G1 signature. + let sig = secret.sign(&msg); + let sig_affine: G1Affine = sig.0.into_affine(); + let (sx, sy) = sig_affine.xy().expect("not the identity"); + let mut sig_uncompressed = [0u8; 128]; + for (slot, coord) in [sx, sy].iter().enumerate() { + let bytes = coord.into_bigint().to_bytes_be(); + sig_uncompressed[slot * 64 + 16..slot * 64 + 64].copy_from_slice(&bytes); + } + assert_eq!( + compress_g1(&sig_uncompressed).to_vec(), + sig.to_bytes(), + "G1 compression differs for seed {i}" + ); + } +} + +/// Emits an ABI fixture built from a **live** BLS relay, so the Solidity client can be tested +/// against a real MMR proof and a real parachain header rather than a synthetic single-leaf tree. +/// +/// Needs the relay and its registered parachain running; see `docs/bls-beefy-rust-remaining.md`. +/// +/// RELAY_WS_URL=ws://127.0.0.1:9979 PARA_WS_URL=ws://127.0.0.1:9991 \ +/// cargo test -p beefy-verifier --features bls bls_live_abi_fixture -- --ignored --nocapture +#[cfg(feature = "bls")] +#[tokio::test] +#[ignore] +async fn bls_live_abi_fixture() { + use alloy_sol_types::SolType; + use beefy_prover::bls::{abi::to_abi_proof, decode_paired_justification}; + use ismp_abi::{ + bls_beefy::BlsBeefy, ecdsa_beefy::BeefyConsensusState as SolBeefyConsensusState, + }; + + let max_rpc_payload_size = 15 * 1024 * 1024; + let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); + let para_ws_url = std::env::var("PARA_WS_URL").expect("PARA_WS_URL must be set"); + + let (relay_client, relay_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); + let (para_client, para_rpc_client) = + subxt_utils::client::ws_client::(¶_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let para_rpc = LegacyRpcMethods::::new(para_rpc_client.clone()); + + let prover = Prover { + beefy_activation_block: 0, + relay: relay_client, + relay_rpc: relay_rpc.clone(), + relay_rpc_client: relay_rpc_client.clone(), + para: para_client, + para_rpc, + para_rpc_client, + para_ids: vec![4009], + query_batch_size: Some(100), + }; + + let latest: H256 = + relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await.unwrap(); + let block = relay_rpc.chain_get_block(Some(latest.into())).await.unwrap().unwrap(); + let justification = block + .justifications + .expect("justifications") + .into_iter() + .find_map(|j| (j.0 == polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID).then_some(j.1)) + .expect("beefy justification"); + + let signed = decode_paired_justification(&justification).unwrap(); + let set_id = signed.commitment.validator_set_id; + + // Anchor one authority set back so the proof also exercises a rotation. + let mut anchor = H256::default(); + let mut cursor = latest; + for _ in 0..4000 { + let header = relay_rpc.chain_get_header(Some(cursor.into())).await.unwrap().unwrap(); + let parent: H256 = header.parent_hash.into(); + if parent.is_zero() { + break; + } + if let Ok(Some(b)) = relay_rpc.chain_get_block(Some(parent.into())).await { + if let Some(js) = b.justifications { + if let Some(raw) = js.into_iter().find_map(|j| { + (j.0 == polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID).then_some(j.1) + }) { + let prev = decode_paired_justification(&raw).unwrap(); + if prev.commitment.validator_set_id + 1 == set_id { + anchor = parent; + break; + } + } + } + } + cursor = parent; + } + assert!(!anchor.is_zero(), "no anchor one set back"); + + let state = prover.get_initial_consensus_state(Some(anchor)).await.unwrap(); + let message = prover.bls_consensus_proof(signed).await.unwrap(); + + let signer_count = message.mmr.signers.len(); + let para_count = message.parachain.parachains.len(); + let mmr_nodes = message.mmr.mmr_proof.items.len(); + let block_number = message.mmr.commitment.block_number; + + let abi_state: SolBeefyConsensusState = state.into(); + let abi_proof = to_abi_proof(message).expect("to_abi_proof"); + + let encoded_state = SolBeefyConsensusState::abi_encode(&abi_state); + let encoded_proof = + <(BlsBeefy::BlsRelayChainProof, BlsBeefy::ParachainProof) as SolType>::abi_encode_params( + &(abi_proof.relay, abi_proof.parachain), + ); + + println!("=== live abi fixture ==="); + println!( + "signers {signer_count} | parachains {para_count} | mmr nodes {mmr_nodes} | block {block_number}" + ); + println!("state 0x{}", hex::encode(encoded_state)); + println!("proof 0x{}", hex::encode(encoded_proof)); + assert!(para_count > 0, "expected a parachain header from the registered para"); +} From cca8087c1f51dd71fd2cbd4d50960b0cfb866dd7 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Thu, 6 Aug 2026 23:20:13 +0100 Subject: [PATCH 06/48] prove the bls keyset commitment as an extra leaf of the authority set tree --- evm/rust/abi/BlsBeefy.json | 309 ++++++++++++++++++ evm/rust/src/conversions.rs | 2 + evm/src/consensus/BlsBeefy.sol | 21 +- evm/src/consensus/Types.sol | 8 +- evm/tests/foundry/BlsAggregate.t.sol | 21 +- .../foundry/fixtures/bls-beefy-live-proof.hex | 2 +- .../foundry/fixtures/bls-beefy-live-state.hex | 2 +- .../foundry/fixtures/bls-beefy-proof.hex | 2 +- .../foundry/fixtures/bls-beefy-state.hex | 2 +- modules/consensus/beefy/primitives/src/lib.rs | 9 +- modules/consensus/beefy/prover/src/bls.rs | 56 +++- modules/consensus/beefy/verifier/src/lib.rs | 15 + modules/consensus/beefy/verifier/src/test.rs | 70 +++- parachain/simtests/src/pallet_beefy_bls.rs | 28 +- 14 files changed, 502 insertions(+), 45 deletions(-) diff --git a/evm/rust/abi/BlsBeefy.json b/evm/rust/abi/BlsBeefy.json index aad0e079c..23be33b71 100644 --- a/evm/rust/abi/BlsBeefy.json +++ b/evm/rust/abi/BlsBeefy.json @@ -1,4 +1,41 @@ [ + { + "type": "function", + "name": "MMR_ROOT_PAYLOAD_ID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes2", + "internalType": "bytes2" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "checkParticipationThreshold", + "inputs": [ + { + "name": "len", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "total", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "pure" + }, { "type": "function", "name": "noOp", @@ -189,6 +226,16 @@ "type": "bytes32[]", "internalType": "bytes32[]" }, + { + "name": "blsCommitment", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "keysetProof", + "type": "bytes32[]", + "internalType": "bytes32[]" + }, { "name": "proof", "type": "bytes32[]", @@ -240,5 +287,267 @@ ], "outputs": [], "stateMutability": "pure" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "verify", + "inputs": [ + { + "name": "previousState", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "proof", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "", + "type": "tuple[]", + "internalType": "struct IntermediateState[]", + "components": [ + { + "name": "stateMachineId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "height", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "commitment", + "type": "tuple", + "internalType": "struct StateCommitment", + "components": [ + { + "name": "timestamp", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "overlayRoot", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "stateRoot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } + ] + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "verifyAuthorities", + "inputs": [ + { + "name": "commitment", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "signers", + "type": "tuple[]", + "internalType": "struct BlsSigner[]", + "components": [ + { + "name": "publicKey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "authorityIndex", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "aggregateSignature", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "keysetRoot", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "authorityProof", + "type": "bytes32[]", + "internalType": "bytes32[]" + }, + { + "name": "authorityCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "view" + }, + { + "type": "error", + "name": "EmptyLeaves", + "inputs": [] + }, + { + "type": "error", + "name": "EmptyTree", + "inputs": [] + }, + { + "type": "error", + "name": "EmptyTree", + "inputs": [] + }, + { + "type": "error", + "name": "G1AddFailed", + "inputs": [] + }, + { + "type": "error", + "name": "G2AddFailed", + "inputs": [] + }, + { + "type": "error", + "name": "IllegalGenesisBlock", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAggregateSignature", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAuthoritiesProof", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidMmrProof", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidPointLength", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidSignerOrdering", + "inputs": [] + }, + { + "type": "error", + "name": "LeafIndexOutOfBounds", + "inputs": [] + }, + { + "type": "error", + "name": "MapToG1Failed", + "inputs": [] + }, + { + "type": "error", + "name": "MmrRootHashMissing", + "inputs": [] + }, + { + "type": "error", + "name": "ModExpFailed", + "inputs": [] + }, + { + "type": "error", + "name": "NoSigners", + "inputs": [] + }, + { + "type": "error", + "name": "OutOfBoundsLeaves", + "inputs": [] + }, + { + "type": "error", + "name": "PairingFailed", + "inputs": [] + }, + { + "type": "error", + "name": "ProofExhausted", + "inputs": [] + }, + { + "type": "error", + "name": "SuperMajorityRequired", + "inputs": [] + }, + { + "type": "error", + "name": "TimestampNotFound", + "inputs": [] + }, + { + "type": "error", + "name": "UnconsumedProof", + "inputs": [] + }, + { + "type": "error", + "name": "UnknownAuthoritySet", + "inputs": [] + }, + { + "type": "error", + "name": "UnsortedLeaves", + "inputs": [] + }, + { + "type": "error", + "name": "UnsortedLeaves", + "inputs": [] } ] diff --git a/evm/rust/src/conversions.rs b/evm/rust/src/conversions.rs index 8d19661f8..6b4045a5f 100644 --- a/evm/rust/src/conversions.rs +++ b/evm/rust/src/conversions.rs @@ -546,6 +546,8 @@ mod beefy { let commitment: Commitment = value.commitment.into(); BlsMmrProof { + bls_commitment: H256(value.blsCommitment.0), + keyset_proof: value.keysetProof.into_iter().map(|h| h.0).collect(), commitment: commitment.into(), signers, aggregate_signature: { diff --git a/evm/src/consensus/BlsBeefy.sol b/evm/src/consensus/BlsBeefy.sol index eadf5a8d2..949de3b5c 100644 --- a/evm/src/consensus/BlsBeefy.sol +++ b/evm/src/consensus/BlsBeefy.sol @@ -148,6 +148,8 @@ contract BlsBeefy is IConsensusV2, ERC165 { relayProof.signers, relayProof.aggregateSignature, authoritySet.root, + relayProof.blsCommitment, + relayProof.keysetProof, relayProof.proof, authoritySet.len ); @@ -186,6 +188,8 @@ contract BlsBeefy is IConsensusV2, ERC165 { BlsSigner[] memory signers, bytes memory aggregateSignature, bytes32 keysetRoot, + bytes32 blsCommitment, + bytes32[] memory keysetProof, bytes32[] memory authorityProof, uint256 authorityCount ) public view { @@ -215,7 +219,22 @@ contract BlsBeefy is IConsensusV2, ERC165 { }); } - if (!MerkleMultiProof.VerifyProof(keysetRoot, authorityProof, leaves, authorityCount)) { + // Two levels. The relay chain commits the BLS keys as one extra leaf of the authority set + // tree, so the per-authority leaves keep their positions and bridges verifying ECDSA + // signatures still prove against the same root. Establish that leaf is the authority set's, + // then prove the signers against it. + // + // The keyset tree therefore holds authorityCount + 1 leaves, with the BLS commitment last, + // while the threshold above still judges against authorityCount. + MerkleMultiProof.Leaf[] memory keysetLeaf = new MerkleMultiProof.Leaf[](1); + keysetLeaf[0] = + MerkleMultiProof.Leaf({index: authorityCount, hash: keccak256(abi.encodePacked(blsCommitment))}); + + if (!MerkleMultiProof.VerifyProof(keysetRoot, keysetProof, keysetLeaf, authorityCount + 1)) { + revert InvalidAuthoritiesProof(); + } + + if (!MerkleMultiProof.VerifyProof(blsCommitment, authorityProof, leaves, authorityCount)) { revert InvalidAuthoritiesProof(); } diff --git a/evm/src/consensus/Types.sol b/evm/src/consensus/Types.sol index 663ed599a..fa132181c 100644 --- a/evm/src/consensus/Types.sol +++ b/evm/src/consensus/Types.sol @@ -196,7 +196,13 @@ struct BlsRelayChainProof { BeefyMmrLeaf latestMmrLeaf; // Proof for the latest mmr leaf bytes32[] mmrProof; - // Proof for the signing authorities against the keyset commitment + // Root of the tree over the authorities' BLS public keys. The relay chain commits this as one + // extra leaf of the authority set tree, so it is proven rather than trusted. + bytes32 blsCommitment; + // Proof that blsCommitment is the authority set's extra leaf, against the keyset commitment. + // That tree holds len + 1 leaves: the authorities, then this one. + bytes32[] keysetProof; + // Proof for the signing authorities against blsCommitment bytes32[] proof; } diff --git a/evm/tests/foundry/BlsAggregate.t.sol b/evm/tests/foundry/BlsAggregate.t.sol index 9be1d9e08..5c83118f4 100644 --- a/evm/tests/foundry/BlsAggregate.t.sol +++ b/evm/tests/foundry/BlsAggregate.t.sol @@ -108,9 +108,13 @@ contract BlsAggregateTest is Test { ); } - /// Keyset commitment over the four authorities' compressed keys, as the runtime builds it. - bytes32 constant KEYSET_ROOT = 0xd220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a; - /// Multi-proof opening signers 0,1,2 out of 4. + /// Root of the authority set tree: four per-authority leaves, then the BLS commitment. + bytes32 constant KEYSET_ROOT = 0x9097854fdde72a6cae144165afd1b881ff201a20acbbbef836f9cdf45a1d85a9; + /// Root of the tree over the four authorities' compressed BLS keys, the extra leaf's value. + bytes32 constant BLS_COMMITMENT = 0xd220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a; + /// Opens the BLS commitment as leaf 4 of the five-leaf authority set tree. + bytes32 constant KEYSET_PROOF_NODE = 0x9ca0bbf4e382871c43e750de6df39704e2604f75b83534dfa710289745b331f7; + /// Opens signers 0,1,2 against the BLS commitment. bytes32 constant PROOF_NODE = 0x97e418e070e3ec967bccc1d0aaea6d787a7b68733451c5320cd32eec2be63df8; function _blsSigners() private pure returns (BlsSigner[] memory out) { @@ -126,10 +130,15 @@ contract BlsAggregateTest is Test { nodes[0] = PROOF_NODE; } + function _keysetProof() private pure returns (bytes32[] memory nodes) { + nodes = new bytes32[](1); + nodes[0] = KEYSET_PROOF_NODE; + } + /// The authority half end to end: threshold, ordering, merkle membership, aggregate pairing. /// Reverts on any failure, so reaching the end is the assertion. function test_verify_authorities() public { - new BlsBeefy().verifyAuthorities(MESSAGE, _blsSigners(), _g1(SIG_X, SIG_Y), KEYSET_ROOT, _authorityProof(), 4); + new BlsBeefy().verifyAuthorities(MESSAGE, _blsSigners(), _g1(SIG_X, SIG_Y), KEYSET_ROOT, BLS_COMMITMENT, _keysetProof(), _authorityProof(), 4); } /// Repeating a signer must be rejected before any cryptography runs. @@ -139,7 +148,7 @@ contract BlsAggregateTest is Test { BlsBeefy client = new BlsBeefy(); vm.expectRevert(BlsBeefy.InvalidSignerOrdering.selector); - client.verifyAuthorities(MESSAGE, signers, _g1(SIG_X, SIG_Y), KEYSET_ROOT, _authorityProof(), 4); + client.verifyAuthorities(MESSAGE, signers, _g1(SIG_X, SIG_Y), KEYSET_ROOT, BLS_COMMITMENT, _keysetProof(), _authorityProof(), 4); } /// Two of four is short of the supermajority. @@ -151,7 +160,7 @@ contract BlsAggregateTest is Test { BlsBeefy client = new BlsBeefy(); vm.expectRevert(BlsBeefy.SuperMajorityRequired.selector); - client.verifyAuthorities(MESSAGE, two, _g1(SIG_X, SIG_Y), KEYSET_ROOT, _authorityProof(), 4); + client.verifyAuthorities(MESSAGE, two, _g1(SIG_X, SIG_Y), KEYSET_ROOT, BLS_COMMITMENT, _keysetProof(), _authorityProof(), 4); } /// Compression must reproduce exactly what `w3f-bls` serialises, or the merkle leaves will not diff --git a/evm/tests/foundry/fixtures/bls-beefy-live-proof.hex b/evm/tests/foundry/fixtures/bls-beefy-live-proof.hex index 44837a447..7b8b747b7 100644 --- a/evm/tests/foundry/fixtures/bls-beefy-live-proof.hex +++ b/evm/tests/foundry/fixtures/bls-beefy-live-proof.hex @@ -1 +1 @@ -0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000005e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e671ba1f0c0553856eb6e3f31b940306808847c8cd497e88358802584f40435aaf20000000000000000000000000000000000000000000000000000000000000653000000000000000000000000000000000000000000000000000000000000000259354e6a156b4a39ffc56f03fa5b947e341c737964563c0fc16298aaeef002dc1cd8af3d10a89274653ab6c83f167e8bb6fb272d2c4b2bcba899fef93906235b0000000000000000000000000000000000000000000000000000000000007e670000000000000000000000000000000000000000000000000000000000000680000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000007e680000000000000000000000000000000000000000000000000000000000000652000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000202a38ea65b44e6249d92dd3e30a5f299687c05b6308ec296387bcd18e260f471e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000005223c1d776402ea1987eb80d8986897469773e64f60ac162da9bbd43fe64ae970bfbad858a466787d8712eda4fd59e8000000000000000000000000000000000d3987bf4729830439efa2577b0923d38f93f5cffccc059dfaa7779ff10756989b5121eb60c8773cd4a227669b1e430b00000000000000000000000000000000011f34f64d1aea99d50df61fa1c64e8a3708985ad07b2b18181eaa32c031f097005818a639a272ec26090af35aa59ddc0000000000000000000000000000000009940b83f82ccfed097bb7a4f60e93f002c8d726f905cee111aa2141a8e6f0b6bc5fdd8a461a4648dfc18a843366924600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000169a502b8c1834a867af71776a5d79a390dfc6d5db5eed75bc3b9140c54cf61ffd1ab3b15ca2c599d78124f900cf15ef000000000000000000000000000000001818d16b6726d50f1c88dd60266b7a17f717de8fe167db5786bf2dc2adc9bea57e7091b0ca47ef8b43a39425ec25328f00000000000000000000000000000000019fc5274bd3fc51719e140cb337d70c456468b7c5b73f404da5bea585b147a2fad7d570e879b2378033833fdd14bae1000000000000000000000000000000001833bfdffcf2febd0806514ac9d447e6de2ee170c7882f6bc90d70b033975aebd3f62bf8b8c9d0515db843c5ee55663b0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000794ffe09228c09c3084c0088072beaf2a5267ef4dbdc83e7ca828ec314285b10fc4cb3c3092f1da1d0f94927c6cc1a40000000000000000000000000000000010ba10b17a96746c73a62cdb1d352e48aaba0944d56a69a0f8739a046896d2a3cea344ff5f18faf790f5103d44e2506a000000000000000000000000000000000000000000000000000000000000000bb8d03c3213782fce37d6bbbc387ee63ee5b1c8f5ec2fbfcfaedcedbebe2c950d0cc4b1d1416891b2f3c58e5184b0f6257064b763518103dfcd96962563cb928155a61ed3328a80c041b7be84116134370276b533b6b78f2d8fd5e339c16f54bdf467e338a570f838cab69d4fe7b49ee3a65cf7224cf6f3a37e7a0f9c7c9b1271e6bf0aae9fafc93b638133e2a15ce37dbe3d1528c3f576181019d46ce602cff2e201df559b371d5d84e6917153780f6d9bce1efcc6d7db3ac48ff4d597d756aced3ed30b3a62adcdc851c3d6ee8c5aede1c2a6bae1d96f5bbf34fbe754725537c55e1a2e876268d8d68cb36551d3ca9d18ad59e0da7a637ef6cc1319e14104f165d4f7d7a39c5b7c141d63c76e41794d87030eae08b2d8d788ac15cfda22f9f80ca396e39db1bab9b90706c517379eff1bfe0a7651db3adf92fea74fdb1ed1e00f2d206705a8bd079325315d7557f9d0737e52a45f2b30decd69c5e5bd6bc77300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000137807e0e4d516f36ca37d3345d736030cb09587ade6cfa38afb5eb56ce5011a945d9666784ab8a71aead7b26d75ecd5194950ac88455a951c1d4d8a7250c22d2aa29c3098b806aa719802f482e1cf9296e18a4aac40bb5ae4f3e3da9b861d048930bea14066175726120e9e0bd1100000000045250535290c6f000aef94d1bc0ce7fbe6f01f35cf4e593e14f925db4a0b48d0660bf047da596f901000449534d5001010000000000000000000000000000000000000000000000000000000000000000bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a044953544d207645736a00000000056175726101015440033c1aad33b07a28a551bfd24feaaf7a1c0109ebef58b46bd91e8bd7143ad6fc76e73f8a77926f0d670118c14e8a13c38a12250e5cbae238d26fca455f8f0000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file +0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000062000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000063d0b8898784cc48cba5117faa7f985e7bb46fdd98ab53b8a1da12432d6ba9ebcf00000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002c6640e6634e02cc8a0ba3b8b32a3b4a956c8ac195b2c4ddb897aefc6b2fcfd558685ba548bdc68684d93d6d8a68833ab8872ef7c7bcadb2d1c93cf58d6ecf509000000000000000000000000000000000000000000000000000000000000006300000000000000000000000000000000000000000000000000000000000006c059354e6a156b4a39ffc56f03fa5b947e341c737964563c0fc16298aaeef002dc000000000000000000000000000000000000000000000000000000000000076000000000000000000000000000000000000000000000000000000000000007a0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000205268843b5a6524735cd4836d2b579c3b388307b0f46feb36edacdaf20e2514440000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000005223c1d776402ea1987eb80d8986897469773e64f60ac162da9bbd43fe64ae970bfbad858a466787d8712eda4fd59e8000000000000000000000000000000000d3987bf4729830439efa2577b0923d38f93f5cffccc059dfaa7779ff10756989b5121eb60c8773cd4a227669b1e430b00000000000000000000000000000000011f34f64d1aea99d50df61fa1c64e8a3708985ad07b2b18181eaa32c031f097005818a639a272ec26090af35aa59ddc0000000000000000000000000000000009940b83f82ccfed097bb7a4f60e93f002c8d726f905cee111aa2141a8e6f0b6bc5fdd8a461a4648dfc18a843366924600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000169a502b8c1834a867af71776a5d79a390dfc6d5db5eed75bc3b9140c54cf61ffd1ab3b15ca2c599d78124f900cf15ef000000000000000000000000000000001818d16b6726d50f1c88dd60266b7a17f717de8fe167db5786bf2dc2adc9bea57e7091b0ca47ef8b43a39425ec25328f00000000000000000000000000000000019fc5274bd3fc51719e140cb337d70c456468b7c5b73f404da5bea585b147a2fad7d570e879b2378033833fdd14bae1000000000000000000000000000000001833bfdffcf2febd0806514ac9d447e6de2ee170c7882f6bc90d70b033975aebd3f62bf8b8c9d0515db843c5ee55663b0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000321306435a1c68a4cb716ee1f9335b8fe62d5547d803d679138856e8c554af76295966a2203e1746dd29551065bc2920000000000000000000000000000000018772176c0d3efd6eea239dfc8dd190e9c9819de7bfe7de37f285ba8b4d8adf580630203023e2d3c041347790193fda10000000000000000000000000000000000000000000000000000000000000004085d4cd438536c251ca2cdb35cee62d811cb7b9a193e09239096659a67344a1d8d1dddccff3a0fdd09b88fa2e993290125c9952631d71a5b82997e1efcb62ece7ab1ef80f6c14308049a54303732e6330351ba0c96a7102d2888c3fed32193bc47f0bcbda552b9ddd35fa24b872452ec2d35e20db22277de46713e56017359f90000000000000000000000000000000000000000000000000000000000000001697ea2a8fe5b03468548a7a413424a6292ab44a82a6f5cc594c3fa7dda7ce40200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000134520a9f55f1813bdf8ee6036712ed26da650b0981bfaf1150b537d953e56a3f6a2831a86712dc2bea37104898af1f7106e690d2b4d958881afcb51b09f53226be16a2045c0928ca4abb05a33405710fdd0827e5850ff2c463cf866ff86433444e9014066175726120191bbe11000000000452505352884497dbb3ec883e67c9ca651b5a07aa8375afbacf4aa15d2adc4ff07871adaec57d010449534d5001010000000000000000000000000000000000000000000000000000000000000000bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a044953544d2096a2746a0000000005617572610101ae945eb0ee501e720d6fdb273c066ffc83726e235090532ac97f1f7d72bf3b766739f7b6e3f424e94476bc052b3ccece09dbfb626a4054dc0f2895b1610ddb8e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-beefy-live-state.hex b/evm/tests/foundry/fixtures/bls-beefy-live-state.hex index 1f646aff5..fd133f530 100644 --- a/evm/tests/foundry/fixtures/bls-beefy-live-state.hex +++ b/evm/tests/foundry/fixtures/bls-beefy-live-state.hex @@ -1 +1 @@ -0x0000000000000000000000000000000000000000000000000000000000007e5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000651000000000000000000000000000000000000000000000000000000000000000259354e6a156b4a39ffc56f03fa5b947e341c737964563c0fc16298aaeef002dc0000000000000000000000000000000000000000000000000000000000000652000000000000000000000000000000000000000000000000000000000000000259354e6a156b4a39ffc56f03fa5b947e341c737964563c0fc16298aaeef002dc \ No newline at end of file +0x0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002c6640e6634e02cc8a0ba3b8b32a3b4a956c8ac195b2c4ddb897aefc6b2fcfd5500000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000002c6640e6634e02cc8a0ba3b8b32a3b4a956c8ac195b2c4ddb897aefc6b2fcfd55 \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-beefy-proof.hex b/evm/tests/foundry/fixtures/bls-beefy-proof.hex index cdc1e0fee..418a6b2a9 100644 --- a/evm/tests/foundry/fixtures/bls-beefy-proof.hex +++ b/evm/tests/foundry/fixtures/bls-beefy-proof.hex @@ -1 +1 @@ -0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008a000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000076000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004d220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000820000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000200c32cca89bdaf348159b895a807cd49d3dff31f124c35c29eae09fe89ad2c03e0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000eb912203efe065b9d9844025f9a85a43fdb21f4c8c0f31b39cc3bedcdbecce8ab3567f9cadbe965adebb7dd4a081ec10000000000000000000000000000000014168206974b9223cc95e6e1f279f9d10e526aa172b5bd15b101b6f4997e2038ebcb02bfee1bca54f428162e17ade003000000000000000000000000000000000dbe0ed3b59dbf3c217e879f885df4fce29af686888e77e984b69d6a07fe90b2a1acebc49d6a196c90a9307be82bb9c40000000000000000000000000000000016bd04776624eab548fc58aeac9da7f75a618206620c9119d517b983b659ca196c2a0761e09a1ee6df87e1cd78bdd4d500000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000065a060f78222114141a2544d6207ebfe7784788e6310b3a58cebc36df948e387853ce8492c4c8f9d423ebabc6feb02700000000000000000000000000000000026bc8c3c41fb3bb78a7d97855098d8b32ec546433c4c523f62c471b75d89823e2485ff79f3c203961b62c9b8b08e4d1000000000000000000000000000000000db39f4b44160911c089c2cf24621f3e1df13561ced1640ddafa4c885d0a80b3428156c709f375e00fdb230b5c109e460000000000000000000000000000000000b7e2a03248e77666c8e12d4eaa31d451c477209a178134afd53ebcc701842206b2b7613ed4807f4600ce212c6679e8000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000c2410d03233711f03a7242fd3a8bb141125ac84a07813d8cce6e366038f129d3ef348fc7dd14682e3db28a920d2c74800000000000000000000000000000000008cd3cbf5e8dd6aca7d5fb78061c360910691c3797bcd95a42cf5a45b0612e8555f6e118788872af47d2319549142820000000000000000000000000000000015c168dc4a702011de9bded446897040c88e51831293bbcc691fe85a7f42e6cb454d4931fe9348fa310f455a21ef47ae0000000000000000000000000000000008600a717fc096a40eeb7dba5194779267123c9fef22e3ebe85c34c1aa74928a89c211d9bfa7dc97a296fc7550acc58e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000013dd22814659a34a3c64b86f1691750b49e54d81e38b98c9b8908843d15e178f5d56b368894c3060d4599e69e49fc41f0000000000000000000000000000000014da396dc7edfda14e4aecde657d465416676424529515978507e9866e0b59cc6f0c74ae66c25a53942aa814fb1435970000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000197e418e070e3ec967bccc1d0aaea6d787a7b68733451c5320cd32eec2be63df800000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file +0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000092000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000007a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000049097854fdde72a6cae144165afd1b881ff201a20acbbbef836f9cdf45a1d85a9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000840d220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000008a0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000204a4f59869e34638bb79baedbcc7bc096fd33300839f0cd4f3066a0ca753290b20000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000eb912203efe065b9d9844025f9a85a43fdb21f4c8c0f31b39cc3bedcdbecce8ab3567f9cadbe965adebb7dd4a081ec10000000000000000000000000000000014168206974b9223cc95e6e1f279f9d10e526aa172b5bd15b101b6f4997e2038ebcb02bfee1bca54f428162e17ade003000000000000000000000000000000000dbe0ed3b59dbf3c217e879f885df4fce29af686888e77e984b69d6a07fe90b2a1acebc49d6a196c90a9307be82bb9c40000000000000000000000000000000016bd04776624eab548fc58aeac9da7f75a618206620c9119d517b983b659ca196c2a0761e09a1ee6df87e1cd78bdd4d500000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000065a060f78222114141a2544d6207ebfe7784788e6310b3a58cebc36df948e387853ce8492c4c8f9d423ebabc6feb02700000000000000000000000000000000026bc8c3c41fb3bb78a7d97855098d8b32ec546433c4c523f62c471b75d89823e2485ff79f3c203961b62c9b8b08e4d1000000000000000000000000000000000db39f4b44160911c089c2cf24621f3e1df13561ced1640ddafa4c885d0a80b3428156c709f375e00fdb230b5c109e460000000000000000000000000000000000b7e2a03248e77666c8e12d4eaa31d451c477209a178134afd53ebcc701842206b2b7613ed4807f4600ce212c6679e8000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000c2410d03233711f03a7242fd3a8bb141125ac84a07813d8cce6e366038f129d3ef348fc7dd14682e3db28a920d2c74800000000000000000000000000000000008cd3cbf5e8dd6aca7d5fb78061c360910691c3797bcd95a42cf5a45b0612e8555f6e118788872af47d2319549142820000000000000000000000000000000015c168dc4a702011de9bded446897040c88e51831293bbcc691fe85a7f42e6cb454d4931fe9348fa310f455a21ef47ae0000000000000000000000000000000008600a717fc096a40eeb7dba5194779267123c9fef22e3ebe85c34c1aa74928a89c211d9bfa7dc97a296fc7550acc58e000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000025926c99ee9df0dd30a417a604794fa50629fc87e3d81290b7ad57a753e089987b174a25512e80df6cc412ef59239c80000000000000000000000000000000014c25b4c14965131b589b802ed9e8c36200b0bbaf05c192b5dfbb5f85bc828f1505f458c30e08776eb10e44d546b9b06000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019ca0bbf4e382871c43e750de6df39704e2604f75b83534dfa710289745b331f7000000000000000000000000000000000000000000000000000000000000000197e418e070e3ec967bccc1d0aaea6d787a7b68733451c5320cd32eec2be63df800000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-beefy-state.hex b/evm/tests/foundry/fixtures/bls-beefy-state.hex index 07221356f..8f019f909 100644 --- a/evm/tests/foundry/fixtures/bls-beefy-state.hex +++ b/evm/tests/foundry/fixtures/bls-beefy-state.hex @@ -1 +1 @@ -0x0000000000000000000000000000000000000000000000000000000000000063000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000004d220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a00000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004d220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a \ No newline at end of file +0x00000000000000000000000000000000000000000000000000000000000000630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000049097854fdde72a6cae144165afd1b881ff201a20acbbbef836f9cdf45a1d85a9000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000049097854fdde72a6cae144165afd1b881ff201a20acbbbef836f9cdf45a1d85a9 \ No newline at end of file diff --git a/modules/consensus/beefy/primitives/src/lib.rs b/modules/consensus/beefy/primitives/src/lib.rs index 4fe175080..4beb72e86 100644 --- a/modules/consensus/beefy/primitives/src/lib.rs +++ b/modules/consensus/beefy/primitives/src/lib.rs @@ -221,7 +221,14 @@ pub struct BlsMmrProof { pub latest_mmr_leaf: MmrLeaf, /// Proof for the latest mmr leaf pub mmr_proof: sp_mmr_primitives::LeafProof, - /// Flat proof hashes proving the signers' public keys against the keyset commitment + /// Root of the tree over the authorities' BLS public keys. The relay chain commits this as one + /// extra leaf of the authority set tree, so it is proven rather than trusted. + pub bls_commitment: H256, + /// Flat proof hashes proving [`Self::bls_commitment`] is the authority set's extra leaf, + /// against the keyset commitment. That tree holds `len + 1` leaves: the authorities, then + /// this one. + pub keyset_proof: Vec<[u8; 32]>, + /// Flat proof hashes proving the signers' public keys against [`Self::bls_commitment`] pub authority_proof: Vec<[u8; 32]>, } diff --git a/modules/consensus/beefy/prover/src/bls.rs b/modules/consensus/beefy/prover/src/bls.rs index a199f5fa0..b5a4cf4bb 100644 --- a/modules/consensus/beefy/prover/src/bls.rs +++ b/modules/consensus/beefy/prover/src/bls.rs @@ -26,6 +26,7 @@ use anyhow::anyhow; use codec::{Decode, Encode}; use polkadot_sdk::*; +use primitive_types::H256; use sp_consensus_beefy::{SignedCommitment, VersionedFinalityProof}; use sp_io::hashing::keccak_256; use subxt::{backend::legacy::LegacyRpcMethods, Config}; @@ -128,6 +129,31 @@ pub fn aggregate_signatures( .map_err(|_| anyhow!("Aggregated signature was not {BLS_G1_SIGNATURE_LEN} bytes")) } +/// The ECDSA halves of the paired authority keys, SCALE-encoded as the address converter expects. +/// +/// These are the leaves the ECDSA path proves against, and the BLS commitment is appended after +/// them, so the prover has to rebuild them to open a path to that extra leaf. +async fn ecdsa_halves( + rpc: &LegacyRpcMethods, + at: Option>, +) -> Result>, anyhow::Error> { + let data = rpc + .state_get_storage(BEEFY_AUTHORITIES.as_slice(), at) + .await? + .ok_or_else(|| anyhow!("No beefy authorities found!"))?; + + let paired = Vec::<[u8; PAIRED_LEN]>::decode(&mut data.as_ref())?; + + Ok(paired + .into_iter() + .map(|key| { + let mut ecdsa = [0u8; 33]; + ecdsa.copy_from_slice(&key[..33]); + ecdsa.encode() + }) + .collect()) +} + impl Prover { /// Build a consensus proof whose commitment is proven by one aggregate BLS signature. /// @@ -166,12 +192,28 @@ impl Prover { let aggregate_signature = aggregate_signatures(&g1_signatures)?; - // The keyset commitment is a merkle root over the hashed G2 keys, so the tree is built - // over every authority and opened at the signers' positions. - let leaves = authorities.iter().map(|key| keccak_256(key)).collect::>(); + // Two trees, mirroring how the relay chain commits them. The BLS keys have their own tree, + // and its root sits as one extra leaf of the authority set tree alongside the Ethereum + // address leaves. So the proof carries a path to that leaf, and a path to the signers + // within it. + let bls_leaves = authorities.iter().map(|key| keccak_256(key)).collect::>(); let indices = signers.iter().map(|signer| signer.index as usize).collect::>(); - let tree = rs_merkle::MerkleTree::::from_leaves(&leaves); - let authority_proof = tree.proof(&indices).proof_hashes().to_vec(); + let bls_tree = rs_merkle::MerkleTree::::from_leaves(&bls_leaves); + let bls_commitment = + H256(bls_tree.root().ok_or_else(|| anyhow!("empty BLS authority key tree"))?); + let authority_proof = bls_tree.proof(&indices).proof_hashes().to_vec(); + + // The authority set tree: the Ethereum address leaves the ECDSA path uses, then the BLS + // commitment. Its leaf count is one more than the validator count. + let ecdsa_authorities = crate::util::hash_authority_addresses( + ecdsa_halves(&self.relay_rpc, Some(block_hash)).await?, + )?; + let mut keyset_leaves = ecdsa_authorities; + keyset_leaves.push(keccak_256(bls_commitment.as_bytes())); + let bls_leaf_index = keyset_leaves.len() - 1; + let keyset_tree = + rs_merkle::MerkleTree::::from_leaves(&keyset_leaves); + let keyset_proof = keyset_tree.proof(&[bls_leaf_index]).proof_hashes().to_vec(); let mmr = BlsMmrProof { commitment: signed_commitment.commitment.clone(), @@ -179,6 +221,8 @@ impl Prover { aggregate_signature, latest_mmr_leaf: latest_leaf.clone(), mmr_proof, + bls_commitment, + keyset_proof, authority_proof, }; @@ -300,6 +344,8 @@ pub mod abi { leafIndex: U256::from(leaf_index), }, mmrProof: mmr.mmr_proof.items.iter().map(|h| FixedBytes(h.0)).collect(), + blsCommitment: FixedBytes(mmr.bls_commitment.0), + keysetProof: mmr.keyset_proof.iter().map(|h| FixedBytes(*h)).collect(), proof: mmr.authority_proof.iter().map(|h| FixedBytes(*h)).collect(), }; diff --git a/modules/consensus/beefy/verifier/src/lib.rs b/modules/consensus/beefy/verifier/src/lib.rs index 2c2d94a9e..5d7c6ce13 100644 --- a/modules/consensus/beefy/verifier/src/lib.rs +++ b/modules/consensus/beefy/verifier/src/lib.rs @@ -244,8 +244,23 @@ pub fn verify_bls_mmr_update_proof>(); + // Two levels. The relay chain commits the BLS keys as one extra leaf of the authority set + // tree, so that the per-authority leaves keep their positions and bridges verifying ECDSA + // signatures still prove against the same root. First establish that leaf really is the + // authority set's, then prove the signers against it. + // + // The keyset tree therefore holds `authority_count + 1` leaves, with the BLS commitment last, + // while the threshold above still judges against `authority_count`. verify_authority_membership::( preamble.keyset_commitment, + &mmr.keyset_proof, + &[preamble.authority_count as usize], + &[H::keccak256(mmr.bls_commitment.as_bytes()).into()], + preamble.authority_count.saturating_add(1), + )?; + + verify_authority_membership::( + mmr.bls_commitment, &mmr.authority_proof, &authority_indices, &authority_leaves, diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index 2cfc7e135..59d73d842 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -1114,12 +1114,20 @@ mod bls_offline { let payload = Payload::from_single_entry(*b"mh", mmr_root.0.to_vec()); let commitment = Commitment { payload, block_number: BLOCK, validator_set_id: SET_ID }; - // The keyset commitment is the merkle root over the hashed G2 keys, matching the runtime's - // converter. - let leaves = validators.iter().map(|(_, key)| keccak_256(key)).collect::>(); - let tree = MerkleTree::::from_leaves(&leaves); - let keyset_commitment = H256(tree.root().expect("keyset tree has a root")); - let authority_proof = tree.proof(signer_indices).proof_hashes().to_vec(); + // Two trees, mirroring the runtime. The BLS keys have their own tree, and its root is one + // extra leaf of the authority set tree, after the per-authority leaves the ECDSA path + // proves against. Those are stand-ins here; only their count matters. + let bls_leaves = validators.iter().map(|(_, key)| keccak_256(key)).collect::>(); + let bls_tree = MerkleTree::::from_leaves(&bls_leaves); + let bls_commitment = H256(bls_tree.root().expect("bls tree has a root")); + let authority_proof = bls_tree.proof(signer_indices).proof_hashes().to_vec(); + + let mut keyset_leaves = + (0..validators.len()).map(|i| keccak_256(&[b'a', i as u8])).collect::>(); + keyset_leaves.push(keccak_256(bls_commitment.as_bytes())); + let keyset_tree = MerkleTree::::from_leaves(&keyset_leaves); + let keyset_commitment = H256(keyset_tree.root().expect("keyset tree has a root")); + let keyset_proof = keyset_tree.proof(&[validators.len()]).proof_hashes().to_vec(); let message = Message::new(b"", &commitment.encode()); let signatures = signer_indices @@ -1154,6 +1162,8 @@ mod bls_offline { aggregate_signature: aggregate(&signatures), latest_mmr_leaf: leaf, mmr_proof: LeafProof { leaf_indices: vec![0], leaf_count: 1, items: vec![] }, + bls_commitment, + keyset_proof, authority_proof, }; @@ -1447,16 +1457,28 @@ fn bls_eip2537_fixture() { .iter() .map(|secret| keccak_256(&secret.into_public().to_bytes())) .collect(); - let tree = MerkleTree::::from_leaves(&leaves); + let bls_tree = MerkleTree::::from_leaves(&leaves); + let bls_commitment = bls_tree.root().expect("bls root"); let _ = &uncompressed; + // The authority set tree: per-authority leaves (stand-ins for the ECDSA addresses), then the + // BLS commitment as one extra leaf. + let mut keyset_leaves: Vec<[u8; 32]> = + (0..authorities.len()).map(|i| keccak_256(&[b'a', i as u8])).collect(); + keyset_leaves.push(keccak_256(&bls_commitment)); + let keyset_tree = MerkleTree::::from_leaves(&keyset_leaves); + for (i, secret) in authorities.iter().enumerate() { println!("authority {i} compressed {}", hex::encode(secret.into_public().to_bytes())); } - println!("-- keyset over compressed keys, {} authorities --", authorities.len()); - println!("keyset root {}", hex::encode(tree.root().expect("root"))); - for hash in tree.proof(&[0, 1, 2]).proof_hashes() { - println!("multiproof node {}", hex::encode(hash)); + println!("-- two-level keyset, {} authorities --", authorities.len()); + println!("bls commitment {}", hex::encode(bls_commitment)); + println!("keyset root {}", hex::encode(keyset_tree.root().expect("root"))); + for hash in keyset_tree.proof(&[authorities.len()]).proof_hashes() { + println!("keyset proof node {}", hex::encode(hash)); + } + for hash in bls_tree.proof(&[0, 1, 2]).proof_hashes() { + println!("authority proof node {}", hex::encode(hash)); } // Each signer's key on its own, for the merkle leaves and the G2_ADD path. @@ -1510,12 +1532,19 @@ fn bls_solidity_proof_fixture() { (0..4).map(|i| SecretKeyVT::::from_seed(&[b'v', i as u8])).collect(); let keys: Vec> = authorities.iter().map(uncompressed).collect(); - // Keyset commitment over the compressed keys, exactly as the runtime converter builds it. The - // contract compresses each uncompressed key it is given to reproduce these leaves. - let leaves: Vec<[u8; 32]> = + // Two trees, as the runtime builds them: the BLS keys in their own tree, whose root is one + // extra leaf of the authority set tree. The leaves before it stand in for the ECDSA addresses. + let bls_leaves: Vec<[u8; 32]> = authorities.iter().map(|s| keccak_256(&s.into_public().to_bytes())).collect(); - let keyset = MerkleTree::::from_leaves(&leaves); + let bls_tree = MerkleTree::::from_leaves(&bls_leaves); + let bls_commitment = bls_tree.root().expect("bls root"); + + let mut keyset_leaves: Vec<[u8; 32]> = + (0..authorities.len()).map(|i| keccak_256(&[b'a', i as u8])).collect(); + keyset_leaves.push(keccak_256(&bls_commitment)); + let keyset = MerkleTree::::from_leaves(&keyset_leaves); let keyset_root = keyset.root().expect("keyset root"); + let keyset_proof = keyset.proof(&[authorities.len()]); // The MMR leaf, and the root it implies as the only leaf in the tree. let leaf = MmrLeaf { @@ -1549,7 +1578,7 @@ fn bls_solidity_proof_fixture() { aggregate_signature.extend_from_slice(&coord.into_bigint().to_bytes_be()); } - let authority_proof = keyset.proof(&signer_indices); + let authority_proof = bls_tree.proof(&signer_indices); // Assemble the sol types the contract decodes. let state = Sol::BeefyConsensusState { @@ -1597,6 +1626,12 @@ fn bls_solidity_proof_fixture() { leafIndex: alloy_primitives::U256::ZERO, }, mmrProof: vec![], + blsCommitment: alloy_primitives::FixedBytes(bls_commitment), + keysetProof: keyset_proof + .proof_hashes() + .iter() + .map(|h| alloy_primitives::FixedBytes(*h)) + .collect(), proof: authority_proof .proof_hashes() .iter() @@ -1721,7 +1756,8 @@ fn compression_matches_w3f_bls() { /// Emits an ABI fixture built from a **live** BLS relay, so the Solidity client can be tested /// against a real MMR proof and a real parachain header rather than a synthetic single-leaf tree. /// -/// Needs the relay and its registered parachain running; see `docs/bls-beefy-rust-remaining.md`. +/// Needs a BLS BEEFY relay running with a parachain registered on it, since the proof has to carry +/// a real parachain header. The para id must be 4009, which is what gargantua tracks. /// /// RELAY_WS_URL=ws://127.0.0.1:9979 PARA_WS_URL=ws://127.0.0.1:9991 \ /// cargo test -p beefy-verifier --features bls bls_live_abi_fixture -- --ignored --nocapture diff --git a/parachain/simtests/src/pallet_beefy_bls.rs b/parachain/simtests/src/pallet_beefy_bls.rs index 37f4fe5b4..dd76dee49 100644 --- a/parachain/simtests/src/pallet_beefy_bls.rs +++ b/parachain/simtests/src/pallet_beefy_bls.rs @@ -6,9 +6,9 @@ //! pairing check, and the consensus state advances. //! //! This needs a BLS BEEFY relay, which no public network is. Bring one up from Parity's -//! `skalman--enable-bls-beefy-on-westend` branch with the G2 keyset converter applied (see -//! `docs/bls-beefy-skalman-migration.md`), then point `RELAY_WS_URL` at it. Without that the test -//! cannot run, so it is `#[ignore]`d rather than silently passing. +//! `skalman--enable-bls-beefy-on-westend` branch, built with `--features bls-beefy-experimental` +//! and with a converter committing the validators' BLS G2 keys, then point `RELAY_WS_URL` at it. +//! Without that the test cannot run, so it is `#[ignore]`d rather than silently passing. //! //! RELAY_WS_URL=ws://127.0.0.1:9979 PORT=9990 \ //! cargo test -p simtests bls_beefy -- --ignored --nocapture @@ -28,7 +28,10 @@ use subxt::{ }; use subxt_utils::Hyperbridge; -use beefy_prover::{bls::decode_paired_justification, Prover}; +use beefy_prover::{ + bls::{abi::to_abi_proof, decode_paired_justification}, + Prover, +}; use beefy_verifier_primitives::{BlsConsensusMessage, ConsensusState, PROOF_TYPE_BLS}; use ismp_abi::{ bls_beefy::BlsBeefy::BlsBeefyConsensusProof as SolBlsProof, @@ -81,11 +84,10 @@ async fn build_live_bls_proof() -> Result<(ConsensusState, BlsConsensusMessage), // Anchor the trusted state one authority set back, so this proof rotates the set. // // `pallet-beefy-consensus-proofs` rejects a proof that neither rotates the authority set nor - // finalizes a parachain head it has not already seen. Our BLS relay has no registered - // parachains, so no proof can ever finalize a head and only a rotation proof is accepted. - // The anchor has to be exactly one set back: the verifier requires the commitment to be - // signed by the trusted state's current or next set, so a further-back anchor is rejected - // outright with `UnknownAuthoritySet`. + // finalizes a parachain head it has not already seen, so rotating keeps this test independent + // of how far the parachain happens to have advanced. The anchor has to be exactly one set + // back: the verifier requires the commitment to be signed by the trusted state's current or + // next set, so a further-back anchor is rejected outright with `UnknownAuthoritySet`. let previous_beefy_hash = previous_set_anchor(&relay_rpc, latest_beefy_hash, latest_set_id).await?; let initial_state = @@ -172,8 +174,14 @@ async fn bls_beefy_proof_happy_path() -> Result<(), anyhow::Error> { let abi_state: SolBeefyConsensusState = initial_state.into(); let abi_state_bytes = SolBeefyConsensusState::abi_encode(&abi_state); - let abi_proof: SolBlsProof = consensus_message.into(); + // The ABI carries points uncompressed, since EIP-2537 takes no other form and offers no + // decompression precompile. `to_abi_proof` does that expansion. + let abi_proof: SolBlsProof = to_abi_proof(consensus_message)?; let abi_proof_bytes = ::abi_encode_params(&abi_proof); + // The runtime decodes this payload with the same type and method, so a failure here is an + // encoding bug on this side rather than anything the pallet did. + ::abi_decode_params(&abi_proof_bytes) + .map_err(|e| anyhow!("proof does not round-trip through abi_decode_params: {e}"))?; let mut wire_proof = Vec::with_capacity(1 + abi_proof_bytes.len()); wire_proof.push(PROOF_TYPE_BLS); wire_proof.extend_from_slice(&abi_proof_bytes); From 1ff18517235d27465ca3ebe1a56df37cdaea57c0 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 7 Aug 2026 11:40:41 +0100 Subject: [PATCH 07/48] bind the g1 and g2 halves of the paired beefy keys with a pairing check --- modules/consensus/beefy/prover/src/bls.rs | 32 ++++++ modules/consensus/beefy/verifier/src/test.rs | 107 +++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/modules/consensus/beefy/prover/src/bls.rs b/modules/consensus/beefy/prover/src/bls.rs index b5a4cf4bb..54694c399 100644 --- a/modules/consensus/beefy/prover/src/bls.rs +++ b/modules/consensus/beefy/prover/src/bls.rs @@ -49,6 +49,10 @@ pub const PAIRED_LEN: usize = 177; /// `DoubleSignature` begins with its 48-byte G1 point. const PAIRED_SIGNATURE_G1_OFFSET: usize = 65; +/// Offset of the BLS G1 public key within a paired public key: the ECDSA half is 33 bytes, then +/// the `DoublePublicKey` opens with its 48-byte G1 point. +const PAIRED_PUBLIC_G1_OFFSET: usize = 33; + /// Offset of the BLS G2 public key within a paired public key: the ECDSA half is 33 bytes, then /// the `DoublePublicKey` is `G1 (48) || G2 (96)`, so G2 starts 48 bytes further in. const PAIRED_PUBLIC_G2_OFFSET: usize = 33 + 48; @@ -109,6 +113,34 @@ pub async fn beefy_g2_authorities( .collect()) } +/// The validators' BLS12-381 G1 public keys, in authority-set order. +/// +/// `DoublePublicKey` publishes the same secret in both groups, so these are the G1 counterparts of +/// [`beefy_g2_authorities`]. An APK proof consumes the G1 halves while BEEFY's own signature +/// verifies against the G2 halves. +pub async fn beefy_g1_authorities( + rpc: &LegacyRpcMethods, + at: Option>, +) -> Result, anyhow::Error> { + let data = rpc + .state_get_storage(BEEFY_AUTHORITIES.as_slice(), at) + .await? + .ok_or_else(|| anyhow!("No beefy authorities found!"))?; + + let paired = Vec::<[u8; PAIRED_LEN]>::decode(&mut data.as_ref())?; + + Ok(paired + .into_iter() + .map(|key| { + let mut g1 = [0u8; BLS_G1_SIGNATURE_LEN]; + g1.copy_from_slice( + &key[PAIRED_PUBLIC_G1_OFFSET..PAIRED_PUBLIC_G1_OFFSET + BLS_G1_SIGNATURE_LEN], + ); + g1 + }) + .collect()) +} + /// Sum compressed G1 signatures into the single compressed G1 point the verifier checks. pub fn aggregate_signatures( signatures: &[[u8; BLS_G1_SIGNATURE_LEN]], diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index 59d73d842..5ec89f64d 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -1862,3 +1862,110 @@ async fn bls_live_abi_fixture() { println!("proof 0x{}", hex::encode(encoded_proof)); assert!(para_count > 0, "expected a parachain header from the registered para"); } + +/// Checks the group-bridging step an APK proof would need, against a live BLS relay. +/// +/// `gnark-apk-proofs` aggregates public keys in G1 and pairs them against a G2 signature, while +/// BEEFY signs in G1 with G2 public keys. `DoublePublicKey` publishes the same secret in both +/// groups, so both aggregates describe one aggregate secret and the two can be tied together +/// without changing how the relay signs: +/// +/// ```text +/// e(apk_g1, g2) == e(g1, apk_g2) binds an untrusted apk_g2 to apk_g1 +/// e(sig_g1, g2) == e(hash_to_g1(msg), apk_g2) BEEFY's existing G1 signature +/// ``` +/// +/// A verifier gets `apk_g1` from the SNARK and takes `apk_g2` as an untrusted input, so proving +/// both equations hold for real validator keys is what makes the design viable. Both checks are +/// constant cost, unlike the per-signer merkle paths they would replace. +/// +/// RELAY_WS_URL=ws://127.0.0.1:9979 \ +/// cargo test -p beefy-verifier --features bls,bls-crypto bls_apk_group_binding -- --ignored +/// --nocapture +#[cfg(all(feature = "bls", feature = "bls-crypto"))] +#[tokio::test] +#[ignore] +async fn bls_apk_group_binding() { + use ark_bls12_381::{Bls12_381, G1Affine, G1Projective, G2Affine, G2Projective}; + use ark_ec::{AffineRepr, CurveGroup, Group, pairing::Pairing}; + use ark_serialize::CanonicalDeserialize; + use beefy_prover::bls::{ + aggregate_signatures, beefy_g1_authorities, beefy_g2_authorities, + decode_paired_justification, + }; + use w3f_bls::{EngineBLS, Message, TinyBLS381}; + + let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); + let (_relay_client, relay_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, 15 * 1024 * 1024) + .await + .unwrap(); + let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); + + // A real finalized commitment, signed by the relay's paired ecdsa_bls381 validators. + let latest: H256 = + relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await.unwrap(); + let block = relay_rpc.chain_get_block(Some(latest.into())).await.unwrap().unwrap(); + let justification = block + .justifications + .expect("justifications") + .into_iter() + .find_map(|j| (j.0 == polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID).then_some(j.1)) + .expect("beefy justification"); + let signed = decode_paired_justification(&justification).unwrap(); + + let at = Some(latest); + let g1_keys = beefy_g1_authorities(&relay_rpc, at).await.unwrap(); + let g2_keys = beefy_g2_authorities(&relay_rpc, at).await.unwrap(); + assert_eq!(g1_keys.len(), g2_keys.len(), "both halves come from the same paired keys"); + + // Aggregate only the validators that actually signed, which is what a bitmask selects. + let mut apk_g1 = G1Projective::default(); + let mut apk_g2 = G2Projective::default(); + let mut signatures = Vec::new(); + let mut signer_count = 0usize; + for (index, maybe_signature) in signed.signatures.iter().enumerate() { + let Some(signature) = maybe_signature else { continue }; + apk_g1 += G1Affine::deserialize_compressed(&g1_keys[index][..]) + .expect("validator G1 public key decodes"); + apk_g2 += G2Affine::deserialize_compressed(&g2_keys[index][..]) + .expect("validator G2 public key decodes"); + signatures.push(signature.g1_signature()); + signer_count += 1; + } + assert!(signer_count > 0, "commitment carries no signatures"); + + let apk_g1 = apk_g1.into_affine(); + let apk_g2 = apk_g2.into_affine(); + + // 1. The binding check. Holds only when both aggregates share a discrete log, so a verifier can + // accept apk_g2 from an untrusted relayer once the SNARK has fixed apk_g1. + let bound = Bls12_381::pairing(apk_g1, G2Affine::generator()) == + Bls12_381::pairing(G1Affine::generator(), apk_g2); + assert!(bound, "e(apk_g1, g2) != e(g1, apk_g2): the two aggregates disagree"); + + // 2. BEEFY's unmodified G1 signature, verified against the G2 aggregate just bound above. + let message = signed.commitment.encode(); + let aggregate_signature = aggregate_signatures(&signatures).unwrap(); + let sig_g1 = G1Affine::deserialize_compressed(&aggregate_signature[..]) + .expect("aggregate signature decodes"); + let message_point = Message::new(b"", &message) + .hash_to_signature_curve::() + .into_affine(); + let signed_ok = Bls12_381::pairing(sig_g1, G2Affine::generator()) == + Bls12_381::pairing(message_point, apk_g2); + assert!(signed_ok, "e(sig_g1, g2) != e(H(m), apk_g2): signature does not verify"); + + // 3. A negative control, so the equations are not vacuously true. + let tampered = (apk_g2.into_group() + G2Projective::generator()).into_affine(); + assert!( + Bls12_381::pairing(apk_g1, G2Affine::generator()) != + Bls12_381::pairing(G1Affine::generator(), tampered), + "binding check accepted a tampered apk_g2", + ); + + println!( + "[ok] group binding holds for {signer_count} live signers: apk_g1 <-> apk_g2 bound, \ + and BEEFY's G1 signature verifies against the bound apk_g2", + ); +} From dcc7b2d180ad65c543c951ae21842ea41bc375a0 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 7 Aug 2026 14:41:36 +0100 Subject: [PATCH 08/48] commit the relay chain's next beefy bls keys to a header digest --- Cargo.lock | 28 ++ Cargo.toml | 4 + .../consensus/beefy/apk-commitment/Cargo.toml | 32 ++ .../consensus/beefy/apk-commitment/src/lib.rs | 286 +++++++++++ modules/pallets/beefy-apk-digest/Cargo.toml | 41 ++ modules/pallets/beefy-apk-digest/src/lib.rs | 465 ++++++++++++++++++ parachain/runtimes/gargantua/Cargo.toml | 2 + parachain/runtimes/gargantua/src/lib.rs | 38 +- 8 files changed, 894 insertions(+), 2 deletions(-) create mode 100644 modules/consensus/beefy/apk-commitment/Cargo.toml create mode 100644 modules/consensus/beefy/apk-commitment/src/lib.rs create mode 100644 modules/pallets/beefy-apk-digest/Cargo.toml create mode 100644 modules/pallets/beefy-apk-digest/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2de831df9..94f152b10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -951,6 +951,18 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "apk-commitment" +version = "0.1.0" +dependencies = [ + "ark-bls12-381 0.4.0", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "hex", + "sha3 0.10.8", +] + [[package]] name = "approx" version = "0.5.1" @@ -8361,6 +8373,7 @@ dependencies = [ "log", "mmr-primitives", "pallet-bandwidth", + "pallet-beefy-apk-digest", "pallet-beefy-consensus-proofs", "pallet-call-decompressor", "pallet-collator-manager", @@ -14644,6 +14657,21 @@ dependencies = [ "sp-staking", ] +[[package]] +name = "pallet-beefy-apk-digest" +version = "0.1.0" +dependencies = [ + "apk-commitment", + "ark-bls12-381 0.4.0", + "ark-serialize 0.4.2", + "cumulus-pallet-parachain-system", + "hex", + "log", + "parity-scale-codec", + "polkadot-sdk", + "scale-info", +] + [[package]] name = "pallet-beefy-consensus-proofs" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9d822dc51..1e438b531 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ members = [ "modules/consensus/sync-committee/prover", "modules/consensus/sync-committee/verifier", "modules/consensus/sync-committee/primitives", + "modules/consensus/beefy/apk-commitment", "modules/consensus/beefy/primitives", "modules/consensus/beefy/prover", "modules/consensus/beefy/verifier", @@ -73,6 +74,7 @@ members = [ "modules/ismp/state-machines/pharos", "modules/pallets/consensus-incentives", "modules/pallets/messaging-incentives", + "modules/pallets/beefy-apk-digest", "modules/pallets/beefy-consensus-proofs", # evm stuff @@ -275,6 +277,7 @@ hyperclient = { path = "modules/hyperclient", default-features = false } subxt-utils = { path = "modules/utils/subxt", default-features = false } # consensus provers & verifiers +apk-commitment = { path = "./modules/consensus/beefy/apk-commitment", default-features = false } beefy-verifier-primitives = { version = "0.1.1", path = "./modules/consensus/beefy/primitives", default-features = false } beefy-prover = { path = "./modules/consensus/beefy/prover" } beefy-verifier = { path = "./modules/consensus/beefy/verifier", default-features = false } @@ -316,6 +319,7 @@ pallet-ismp-host-executive = { path = "modules/pallets/host-executive", default- pallet-call-decompressor = { path = "modules/pallets/call-decompressor", default-features = false } pallet-consensus-incentives = { path = "modules/pallets/consensus-incentives", default-features = false } pallet-messaging-incentives = { path = "modules/pallets/messaging-incentives", default-features = false } +pallet-beefy-apk-digest = { path = "./modules/pallets/beefy-apk-digest", default-features = false } pallet-beefy-consensus-proofs = { path = "modules/pallets/beefy-consensus-proofs", default-features = false } pallet-collator-manager = { path = "modules/pallets/collator-manager", default-features = false } pallet-state-coprocessor = { path = "modules/pallets/state-coprocessor", default-features = false } diff --git a/modules/consensus/beefy/apk-commitment/Cargo.toml b/modules/consensus/beefy/apk-commitment/Cargo.toml new file mode 100644 index 000000000..ef40a41dd --- /dev/null +++ b/modules/consensus/beefy/apk-commitment/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "apk-commitment" +version = "0.1.0" +edition = "2021" +authors = ["Polytope Labs "] +description = "Poseidon2 commitment over a BEEFY validator set's BLS12-381 G1 public keys" +license = "Apache-2.0" +repository = "https://github.com/polytope-labs/hyperbridge" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +# Pinned to 0.4 to match w3f-bls, so the runtime does not carry two arkworks versions. +ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = false } +ark-ec = { version = "0.4.0", default-features = false } +ark-ff = { version = "0.4.0", default-features = false } +ark-serialize = { version = "0.4.0", default-features = false } +sha3 = { version = "0.10", default-features = false } + +[dev-dependencies] +hex = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "ark-bls12-381/std", + "ark-ec/std", + "ark-ff/std", + "ark-serialize/std", + "sha3/std", +] diff --git a/modules/consensus/beefy/apk-commitment/src/lib.rs b/modules/consensus/beefy/apk-commitment/src/lib.rs new file mode 100644 index 000000000..decf7badb --- /dev/null +++ b/modules/consensus/beefy/apk-commitment/src/lib.rs @@ -0,0 +1,286 @@ +//! `no_std` Poseidon2 commitment over a validator set's BLS12-381 G1 public keys. +//! +//! A port of `gnark-plonk-verifier`'s `commitment` module so it can run inside a substrate +//! runtime, where hyperbridge has to compute the same value the APK circuit binds to before it can +//! publish it in a header digest. Byte-compatible with the gnark circuit's `PublicKeysCommitment` +//! and with the Go reference `apk.NativePublicKeysCommitment`. +//! +//! Two changes from the upstream module, both forced by `no_std`: +//! +//! - the round keys are derived once per call and threaded through, rather than cached in a +//! `OnceLock`. The derivation is 62 keccaks against 12288 permutations for a full validator set, +//! so it does not show up in the cost. +//! - arkworks 0.4 rather than 0.5, to match `w3f-bls` and keep a single arkworks in the runtime. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::vec::Vec; + +use ark_bls12_381::{Fq, Fr, G1Affine}; +use ark_ff::{BigInteger, Field, PrimeField, Zero}; +use sha3::{Digest, Keccak256}; + +/// gnark-crypto default Poseidon2 parameters for BLS12-381 in compression mode. +const WIDTH: usize = 2; +const FULL_ROUNDS: usize = 6; +const PARTIAL_ROUNDS: usize = 50; + +/// Equals gnark-crypto's `Parameters.String()` for these parameters. The round keys are a +/// Keccak-256 chain seeded with it, so this string is consensus critical: change a character and +/// every commitment changes. +const SEED: &str = "Poseidon2-BLS12_381[t=2,rF=6,rP=50,d=5]"; + +/// Number of validator slots the APK circuit is fixed to. A shorter set is padded to this with the +/// identity point. +pub const NUM_VALIDATORS: usize = 1024; + +/// Round keys, reproducing gnark-crypto's `Parameters.initRC`: `rnd0 = Keccak(seed)`, +/// `rnd(k+1) = Keccak(rnd k)`, each key taken as `rnd mod r` big-endian. Full rounds carry +/// `WIDTH` keys, partial rounds one, since only lane 0 is keyed. +fn round_keys() -> Vec> { + let half_full = FULL_ROUNDS / 2; + let total = FULL_ROUNDS + PARTIAL_ROUNDS; + let mut rnd: [u8; 32] = Keccak256::digest(SEED.as_bytes()).into(); + let mut keys = Vec::with_capacity(total); + for round in 0..total { + let n = if round < half_full || round >= half_full + PARTIAL_ROUNDS { WIDTH } else { 1 }; + let mut row = Vec::with_capacity(n); + for _ in 0..n { + rnd = Keccak256::digest(rnd).into(); + row.push(Fr::from_be_bytes_mod_order(&rnd)); + } + keys.push(row); + } + keys +} + +/// In-place x^5 S-box. +#[inline] +fn sbox(x: &mut Fr) { + let base = *x; + x.square_in_place(); + x.square_in_place(); + *x *= base; +} + +/// External (full-round) MDS for t=2: `[[2,1],[1,2]]`. +#[inline] +fn mat_mul_external(s: &mut [Fr; WIDTH]) { + let sum = s[0] + s[1]; + s[0] += sum; + s[1] += sum; +} + +/// Internal (partial-round) matrix for t=2: `[[2,1],[1,3]]`. +#[inline] +fn mat_mul_internal(s: &mut [Fr; WIDTH]) { + let sum = s[0] + s[1]; + s[0] += sum; + s[1].double_in_place(); + s[1] += sum; +} + +/// The Poseidon2 permutation on a width-2 state. +fn permutation(state: &mut [Fr; WIDTH], rk: &[Vec]) { + let half_full = FULL_ROUNDS / 2; + let first_full = &rk[..half_full]; + let partial = &rk[half_full..half_full + PARTIAL_ROUNDS]; + let last_full = &rk[half_full + PARTIAL_ROUNDS..]; + + mat_mul_external(state); + for keys in first_full { + for (s, k) in state.iter_mut().zip(keys) { + *s += *k; + } + for s in state.iter_mut() { + sbox(s); + } + mat_mul_external(state); + } + for keys in partial { + state[0] += keys[0]; + sbox(&mut state[0]); + mat_mul_internal(state); + } + for keys in last_full { + for (s, k) in state.iter_mut().zip(keys) { + *s += *k; + } + for s in state.iter_mut() { + sbox(s); + } + mat_mul_external(state); + } +} + +/// 2-to-1 compression with feed-forward on the right input, matching gnark-crypto's +/// `Permutation.Compress`: `right + permutation([left, right])[1]`. +#[inline] +fn compress(left: Fr, right: Fr, rk: &[Vec]) -> Fr { + let mut s = [left, right]; + permutation(&mut s, rk); + right + s[1] +} + +/// Decompose a coordinate into six little-endian 64-bit limbs, one `Fr` each, matching gnark's +/// emulated `BLS12381Fp` limb layout. +#[inline] +fn coord_limbs(c: Fq) -> [Fr; 6] { + let limbs = c.into_bigint().0; + core::array::from_fn(|i| Fr::from(limbs[i])) +} + +/// The Poseidon2 commitment over `points`, in the circuit's absorption order: per point, the six +/// limbs of `X` then the six of `Y`, absorbed through a Merkle-Damgard chain with a zero IV. +/// +/// The caller supplies the same list the circuit binds to, which for a validator set means +/// registration order padded to [`NUM_VALIDATORS`] with the identity point. +pub fn public_keys_commitment(points: &[G1Affine]) -> Fr { + let rk = round_keys(); + let mut state = Fr::zero(); + for p in points { + let x = coord_limbs(p.x); + let y = coord_limbs(p.y); + for block in x.into_iter().chain(y) { + state = compress(state, block, &rk); + } + } + state +} + +/// The commitment as a 32-byte big-endian value, the `uint256 publicKeysCommitment` argument of +/// `ApkProof.verify`. +pub fn public_keys_commitment_bytes(points: &[G1Affine]) -> [u8; 32] { + let mut out = [0u8; 32]; + let be = public_keys_commitment(points).into_bigint().to_bytes_be(); + out[32 - be.len()..].copy_from_slice(&be); + out +} + +/// Pad a validator set to the circuit's fixed width with the identity point. +pub fn padded_to_circuit_width(keys: &[G1Affine]) -> Vec { + let mut points = keys.to_vec(); + points.resize(NUM_VALIDATORS, G1Affine::identity()); + points +} + +/// Resumable form of [`public_keys_commitment`], for absorbing a validator set a chunk at a time. +/// +/// A full set costs roughly 420ms in wasm, which does not fit in one block, but the chain is +/// sequential so it splits cleanly: absorb some points, keep the state, carry on next block. The +/// state is a single field element, so a runtime stores 32 bytes and a cursor between blocks. +/// +/// The caller is responsible for feeding the same points in the same order that +/// [`public_keys_commitment`] would, which for a validator set means registration order padded to +/// [`NUM_VALIDATORS`] with the identity point. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PartialCommitment { + state: Fr, +} + +impl Default for PartialCommitment { + fn default() -> Self { + Self::new() + } +} + +impl PartialCommitment { + /// A fresh chain, matching the zero IV the gnark hasher starts from. + pub fn new() -> Self { + Self { state: Fr::zero() } + } + + /// Absorb the next run of points. Round keys are derived once per call, so callers should + /// prefer fewer, larger chunks over many single-point calls. + pub fn absorb(&mut self, points: &[G1Affine]) { + let rk = round_keys(); + for p in points { + let x = coord_limbs(p.x); + let y = coord_limbs(p.y); + for block in x.into_iter().chain(y) { + self.state = compress(self.state, block, &rk); + } + } + } + + /// The commitment so far, as the 32-byte big-endian value the contract takes. Only meaningful + /// once every point has been absorbed. + pub fn finish(&self) -> [u8; 32] { + let mut out = [0u8; 32]; + let be = self.state.into_bigint().to_bytes_be(); + out[32 - be.len()..].copy_from_slice(&be); + out + } + + /// Encode the in-progress state for storage between blocks. + pub fn to_bytes(&self) -> [u8; 32] { + self.finish() + } + + /// Restore a state written by [`Self::to_bytes`]. + pub fn from_bytes(bytes: &[u8; 32]) -> Self { + Self { state: Fr::from_be_bytes_mod_order(bytes) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ark_ec::AffineRepr; + + /// Ground truth from `circuits/apk/commitment_vectors_test.go`, which locks the Poseidon2 + /// digest over `k * G1::generator()` point sets. If gnark-crypto's parameters ever change + /// these move, and so does every commitment. + const VECTORS: [(usize, &str); 4] = [ + (1, "3b14900f1cd55f300914ca5b4393f0fa6a777d5999963f9520b12a60204272e2"), + (2, "528fad7e07c1ec6db4ad009230329123e643e1629733d60d2b4eaa9e45dc5704"), + (3, "14bac0391b3646f28d9b0b6b64acca1c8c585ade555494ce189aa2e4b62e9977"), + (10, "4a401453041545fc28ebf4c3c2824f317d1c4a7b6bff644d6eb12d0edd1f64c5"), + ]; + + fn k_times_generator(n: usize) -> Vec { + let g = G1Affine::generator(); + (1..=n).map(|k| (g * Fr::from(k as u64)).into()).collect() + } + + #[test] + fn matches_the_gnark_vectors() { + for (n, expected) in VECTORS { + let got = hex::encode(public_keys_commitment_bytes(&k_times_generator(n))); + assert_eq!(got, expected, "commitment diverged from gnark for n={n}"); + } + } + + /// The resumable form must agree with the one-shot form for any split, since a runtime chooses + /// chunk sizes by weight and must not change the result by doing so. + #[test] + fn chunked_absorption_matches_the_one_shot_commitment() { + let points = padded_to_circuit_width(&k_times_generator(3)); + let expected = public_keys_commitment_bytes(&points); + + for chunk in [1usize, 7, 64, 512, NUM_VALIDATORS] { + let mut partial = PartialCommitment::new(); + for run in points.chunks(chunk) { + partial.absorb(run); + // round-trip through storage on every boundary, as a runtime would + partial = PartialCommitment::from_bytes(&partial.to_bytes()); + } + assert_eq!(partial.finish(), expected, "chunk size {chunk} changed the commitment"); + } + } + + #[test] + fn identity_padding_reaches_circuit_width() { + let keys = k_times_generator(2); + let padded = padded_to_circuit_width(&keys); + assert_eq!(padded.len(), NUM_VALIDATORS); + assert!(padded[2..].iter().all(|p| p.is_zero()), "padding is not the identity point"); + assert_ne!( + public_keys_commitment_bytes(&padded), + public_keys_commitment_bytes(&keys), + "padding must change the commitment, else set sizes collide" + ); + } +} diff --git a/modules/pallets/beefy-apk-digest/Cargo.toml b/modules/pallets/beefy-apk-digest/Cargo.toml new file mode 100644 index 000000000..5c62a24e1 --- /dev/null +++ b/modules/pallets/beefy-apk-digest/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "pallet-beefy-apk-digest" +version = "0.1.0" +edition = "2021" +authors = ["Polytope Labs "] +license = "Apache-2.0" +description = "Commits the relay chain's BEEFY BLS public keys to a header digest for APK proofs" +publish = false + +[dependencies] +codec = { workspace = true } +scale-info = { workspace = true } +log = { workspace = true } + +apk-commitment = { workspace = true, default-features = false } +cumulus-pallet-parachain-system = { workspace = true, default-features = false } + +ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = false } +ark-serialize = { version = "0.4.0", default-features = false } + +[dependencies.polkadot-sdk] +workspace = true +features = ["frame-support", "frame-system", "sp-io", "sp-runtime"] +default-features = false + +[dev-dependencies] +hex = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "log/std", + "polkadot-sdk/std", + "apk-commitment/std", + "cumulus-pallet-parachain-system/std", + "ark-bls12-381/std", + "ark-serialize/std", +] +try-runtime = ["polkadot-sdk/try-runtime"] diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs new file mode 100644 index 000000000..e4445829d --- /dev/null +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -0,0 +1,465 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Publishes a commitment to the relay chain's BEEFY BLS public keys in this chain's headers. +//! +//! An APK proof binds a prover to a validator set through a single Poseidon2 commitment over the +//! validators' BLS12-381 G1 keys. A client verifying BEEFY finality needs that commitment, and the +//! cheapest trustworthy place to get it is a header this chain already publishes: the relay state +//! proof carried in every parachain block is checked against the relay parent's state root by the +//! validators, so the keys can be read out of it without any new trust assumption. +//! +//! The commitment is expensive. A full 1024-slot set is roughly 420ms of wasm, which does not fit +//! in a block, so it is absorbed a chunk at a time across blocks and published once complete. The +//! authority set for the next session is known a session ahead, which is what makes that possible. +//! +//! What still has to be decided before this is more than a skeleton is marked `DECIDE` below. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::vec::Vec; + +use apk_commitment::{PartialCommitment, NUM_VALIDATORS}; +use ark_bls12_381::G1Affine; +use ark_serialize::CanonicalDeserialize; +use codec::{Decode, Encode, MaxEncodedLen}; +use cumulus_pallet_parachain_system::RelayChainStateProof; +use frame_support::weights::Weight; +use polkadot_sdk::*; +use scale_info::TypeInfo; + +pub use pallet::*; + +/// Wire size of a paired (ECDSA, BLS12-381) BEEFY key: `ecdsa(33) || G1(48) || G2(96)`. +const PAIRED_LEN: usize = 177; +/// Offset of the BLS G1 half within a paired key. +const PAIRED_G1_OFFSET: usize = 33; +/// Size of a compressed G1 point. +const G1_LEN: usize = 48; + +/// `Beefy::NextAuthorities` on the relay chain. +/// +/// The *next* set, not the current one, and the distinction is what makes the scheme work. A client +/// verifying a header signed by set N reads this digest and thereby learns the commitment for set +/// N+1, so it can verify the following update. Committing the current set instead would be +/// circular: you would need set N's commitment to verify the header carrying set N's commitment. +/// +/// Also note this is not `well_known_keys::AUTHORITIES`, which is `Babe::Authorities`; the BEEFY +/// keys live under the `Beefy` prefix. +pub const RELAY_BEEFY_NEXT_AUTHORITIES: [u8; 32] = [ + 0x08, 0xc4, 0x19, 0x74, 0xa9, 0x7d, 0xbf, 0x15, 0xcf, 0xbe, 0xc2, 0x83, 0x65, 0xbe, 0xa2, 0xda, + 0xaa, 0xcf, 0x00, 0xb9, 0xb4, 0x1f, 0xda, 0x7a, 0x92, 0x68, 0x82, 0x1c, 0x2a, 0x2b, 0x3e, 0x4c, +]; + +/// `Beefy::ValidatorSetId` on the relay chain, the id of the *current* set. The digest reports +/// `set_id + 1`, since it describes [`RELAY_BEEFY_NEXT_AUTHORITIES`]. +pub const RELAY_BEEFY_VALIDATOR_SET_ID: [u8; 32] = [ + 0x08, 0xc4, 0x19, 0x74, 0xa9, 0x7d, 0xbf, 0x15, 0xcf, 0xbe, 0xc2, 0x83, 0x65, 0xbe, 0xa2, 0xda, + 0x8f, 0x05, 0xbc, 0xcc, 0x2f, 0x70, 0xec, 0x66, 0xa3, 0x29, 0x99, 0xc5, 0x76, 0x11, 0x56, 0xbe, +]; + +/// Engine id for the digest item carrying the commitment. +pub const APK_ENGINE_ID: [u8; 4] = *b"APKC"; + +/// The payload of the digest item this pallet writes. +/// +/// This is a wire format: an off-chain verifier reads it out of a header it has already +/// authenticated through the BEEFY MMR's parachain heads root, and feeds `commitment` to +/// `ApkProof.verify` as `publicKeysCommitment`. The set id is what lets it tell which authority +/// set the commitment belongs to. +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo)] +pub struct ApkCommitmentDigest { + /// The BEEFY validator set id these keys belong to, which is the relay's current set id plus + /// one, since the commitment describes the next set. + pub set_id: u64, + /// Poseidon2 over the set's G1 keys, padded to the circuit width with the identity point. + pub commitment: [u8; 32], +} + +/// A paired BEEFY authority key exactly as the relay chain stores it. +#[derive(Clone, Encode, Decode, TypeInfo)] +pub struct PairedAuthority(pub [u8; PAIRED_LEN]); + +impl PairedAuthority { + /// The BLS G1 half, which is what the APK circuit consumes. `DoublePublicKey` publishes the + /// same secret in both groups, so this is the counterpart of the G2 key BEEFY verifies with. + pub fn g1(&self) -> [u8; G1_LEN] { + let mut out = [0u8; G1_LEN]; + out.copy_from_slice(&self.0[PAIRED_G1_OFFSET..PAIRED_G1_OFFSET + G1_LEN]); + out + } +} + +/// Where the running commitment has got to. +#[derive(Clone, Encode, Decode, TypeInfo, Default, MaxEncodedLen)] +pub struct Progress { + /// Identifies the set being absorbed, so a rotation part way through restarts rather than + /// mixing keys from two sets into one commitment. + pub set_digest: [u8; 32], + /// How many of the [`NUM_VALIDATORS`] slots have been absorbed. + pub absorbed: u32, + /// The Merkle-Damgard state, carried between blocks. + pub state: [u8; 32], +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::config] + pub trait Config: + polkadot_sdk::frame_system::Config + cumulus_pallet_parachain_system::Config + { + /// How many validator slots to absorb per block. Trades block weight against how many + /// blocks a full set takes: at roughly 410us per slot in wasm, 64 is about 26ms. + #[pallet::constant] + type SlotsPerBlock: Get; + + /// Cost of absorbing a chunk. `()` carries a measured default, see [`WeightInfo`]. + /// + /// Disambiguated at use as `::WeightInfo`, since + /// `cumulus_pallet_parachain_system::Config` also has one. + type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + pub struct Pallet(_); + + /// The commitment currently being absorbed, if any. + #[pallet::storage] + pub type Pending = StorageValue<_, Progress, OptionQuery>; + + /// The last commitment published to a header digest, and the set it describes. + #[pallet::storage] + pub type Published = StorageValue<_, ([u8; 32], [u8; 32]), OptionQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Started absorbing a new authority set. + CommitmentStarted { set_digest: [u8; 32] }, + /// Finished, and wrote the commitment to this block's header. + CommitmentPublished { set_id: u64, set_digest: [u8; 32], commitment: [u8; 32] }, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Runs in `on_finalize` for two reasons: the relay state proof is only fresh after the + /// validation data inherent, and a digest has to be deposited before the header is sealed. + /// + /// `on_finalize` cannot return weight, so the cost is registered explicitly. Absorbing a + /// chunk is real work, tens of milliseconds, and a block that does not account for it can + /// overrun its budget. + fn on_finalize(_now: BlockNumberFor) { + let slots = match Self::advance() { + Ok(slots) => slots, + Err(e) => { + log::debug!(target: "apk-digest", "commitment did not advance: {e:?}"); + 0 + }, + }; + if slots > 0 { + frame_system::Pallet::::register_extra_weight_unchecked( + ::WeightInfo::absorb(slots), + DispatchClass::Mandatory, + ); + } + } + } + + impl Pallet { + /// Absorb the next chunk, starting or restarting if the set changed, and publish once the + /// whole set is in. + fn advance() -> Result> { + let keys = Self::relay_beefy_g1_keys()?; + // Read the set id up front even though it is only needed at the end. It is cheap, and + // discovering it missing after absorbing a chunk would throw that work away: the error + // propagates before `Pending` is written, so the same chunk would be re-absorbed and + // re-fail every block. + let set_id = Self::relay_beefy_set_id()?; + let set_digest = sp_io::hashing::blake2_256(&keys.encode()); + + let mut progress = match Pending::::get() { + // A rotation part way through invalidates the chain, so start again. + Some(p) if p.set_digest != set_digest => Self::start(set_digest), + Some(p) => p, + None => { + // Nothing to do once this set is already published. + if Published::::get().map(|(d, _)| d) == Some(set_digest) { + return Ok(0); + } + Self::start(set_digest) + }, + }; + + let from = progress.absorbed as usize; + let take = (T::SlotsPerBlock::get() as usize).min(NUM_VALIDATORS - from); + if take == 0 { + return Ok(0); + } + + progress.state = absorb_slots(&keys, from, take, progress.state) + .map_err(|_| Error::::MalformedAuthorityKey)?; + progress.absorbed += take as u32; + + if progress.absorbed as usize == NUM_VALIDATORS { + let commitment = progress.state; + let payload = ApkCommitmentDigest { set_id, commitment }; + + // The header is the delivery mechanism: a client that has already authenticated + // this header through the BEEFY MMR's parachain heads root can read the commitment + // straight out of it, with no further proof. + frame_system::Pallet::::deposit_log(sp_runtime::DigestItem::Consensus( + APK_ENGINE_ID, + payload.encode(), + )); + + Pending::::kill(); + Published::::put((set_digest, commitment)); + Self::deposit_event(Event::CommitmentPublished { set_id, set_digest, commitment }); + } else { + Pending::::put(&progress); + } + Ok(take as u32) + } + + /// The id of the set the commitment describes: the relay's current set id plus one, since + /// the keys come from `NextAuthorities`. + fn relay_beefy_set_id() -> Result> { + let current: u64 = Self::relay_state()? + .read_entry(&RELAY_BEEFY_VALIDATOR_SET_ID, None) + .map_err(|_| Error::::KeyNotProven)?; + Ok(current.saturating_add(1)) + } + + fn start(set_digest: [u8; 32]) -> Progress { + Self::deposit_event(Event::CommitmentStarted { set_digest }); + Progress { set_digest, absorbed: 0, state: PartialCommitment::new().to_bytes() } + } + + /// The relay state proof for this block, checked against the relay parent's state root. + /// + /// Only carries the keys the runtime asks for through + /// `KeyToIncludeInRelayProof::keys_to_prove`; anything else reads back as `KeyNotProven`. + fn relay_state() -> Result> { + let proof = cumulus_pallet_parachain_system::RelayStateProof::::get() + .ok_or(Error::::NoRelayProof)?; + let validation_data = cumulus_pallet_parachain_system::ValidationData::::get() + .ok_or(Error::::NoRelayProof)?; + + RelayChainStateProof::new( + T::SelfParaId::get(), + validation_data.relay_parent_storage_root, + proof, + ) + .map_err(|_| Error::::BadRelayProof) + } + + /// The G1 halves of the relay chain's *next* BEEFY authority set. + fn relay_beefy_g1_keys() -> Result, Error> { + let authorities: Vec = Self::relay_state()? + .read_entry(&RELAY_BEEFY_NEXT_AUTHORITIES, None) + .map_err(|_| Error::::KeyNotProven)?; + + Ok(authorities.iter().map(|a| a.g1()).collect()) + } + } + + #[pallet::error] + pub enum Error { + /// No relay state proof in storage yet, which is normal before the first inherent. + NoRelayProof, + /// The relay state proof did not verify against the relay parent's state root. + BadRelayProof, + /// The BEEFY authorities key was absent from the proof, so `keys_to_prove` is not asking + /// for it. + KeyNotProven, + /// An authority's G1 half did not decode as a curve point. + MalformedAuthorityKey, + } +} + +/// Cost of absorbing `slots` validator slots into the running commitment. +pub trait WeightInfo { + fn absorb(slots: u32) -> Weight; +} + +/// Measured rather than benchmarked, and should be replaced by a generated `WeightInfo` before +/// this runs anywhere real. +/// +/// The commitment was timed in wasm at roughly 410us per slot, linear in the number of slots from +/// 64 up to the full 1024. Weight ref time is picoseconds, so a slot is about 410_000_000 units, +/// and the default `SlotsPerBlock` of 64 comes to ~26ms, a little over one percent of a two second +/// block. The storage side is one read and one write of a fixed-size value. +impl WeightInfo for () { + fn absorb(slots: u32) -> Weight { + Weight::from_parts(410_000_000u64.saturating_mul(slots as u64), 0) + .saturating_add(Weight::from_parts(0, 4096)) + } +} + +/// A key that did not decode as a G1 curve point. +#[derive(Debug, PartialEq, Eq)] +pub struct MalformedKey; + +/// Absorb slots `from .. from + take` of a validator set into a running commitment. +/// +/// Kept free of the pallet so the part that can actually be wrong, the chunking and the padding, +/// is testable without a mock chain. Slots past the end of `keys` are the identity point, which is +/// how the circuit pads a set shorter than [`NUM_VALIDATORS`]. +/// +/// `state` is the Merkle-Damgard state carried between blocks; pass +/// `PartialCommitment::new().to_bytes()` to start. +pub fn absorb_slots( + keys: &[[u8; G1_LEN]], + from: usize, + take: usize, + state: [u8; 32], +) -> Result<[u8; 32], MalformedKey> { + let chunk: Vec = (from..from + take) + .map(|i| match keys.get(i) { + Some(k) => G1Affine::deserialize_compressed(&k[..]).map_err(|_| MalformedKey), + None => Ok(G1Affine::identity()), + }) + .collect::>()?; + + let mut partial = PartialCommitment::from_bytes(&state); + partial.absorb(&chunk); + Ok(partial.to_bytes()) +} + +#[cfg(test)] +mod tests { + use super::*; + use apk_commitment::{padded_to_circuit_width, public_keys_commitment_bytes}; + use ark_serialize::CanonicalSerialize; + + /// Real G1 halves from a live BLS relay's `Beefy` authorities. + const RELAY_KEYS: [&str; 2] = [ + "b7235087b611457915f812c4c9af17fe9c590f0a9c9d2f3b62f5d31673a5d5ae02309ea191fe6ebeb66cb3ad23db3b04", + "a3948b7bd16acfa3b7a113826a1a8b192c2c462f31ddd9dc90131f5b014d85ff9669ad1a686f33f89e6b0d821761b2cc", + ]; + + fn relay_keys() -> Vec<[u8; G1_LEN]> { + RELAY_KEYS + .iter() + .map(|h| { + let mut out = [0u8; G1_LEN]; + out.copy_from_slice(&hex::decode(h).unwrap()); + out + }) + .collect() + } + + fn expected_commitment(keys: &[[u8; G1_LEN]]) -> [u8; 32] { + let points: Vec = + keys.iter().map(|k| G1Affine::deserialize_compressed(&k[..]).unwrap()).collect(); + public_keys_commitment_bytes(&padded_to_circuit_width(&points)) + } + + fn run(keys: &[[u8; G1_LEN]], slots_per_block: usize) -> [u8; 32] { + let mut state = PartialCommitment::new().to_bytes(); + let mut absorbed = 0usize; + while absorbed < NUM_VALIDATORS { + let take = slots_per_block.min(NUM_VALIDATORS - absorbed); + state = absorb_slots(keys, absorbed, take, state).unwrap(); + absorbed += take; + } + state + } + + /// The whole point of absorbing across blocks: the block size must not change the answer. + #[test] + fn any_chunk_size_reaches_the_same_commitment() { + let keys = relay_keys(); + let expected = expected_commitment(&keys); + for slots in [1usize, 64, 100, 512, NUM_VALIDATORS] { + assert_eq!(run(&keys, slots), expected, "slots_per_block {slots} changed the result"); + } + } + + /// A chunk that straddles the boundary between real keys and padding is the case most likely + /// to be got wrong, so pin it explicitly. + #[test] + fn padding_boundary_is_handled_within_a_chunk() { + let keys = relay_keys(); + // 2 real keys, so a chunk of 3 from slot 0 crosses into padding immediately. + assert_eq!(run(&keys, 3), expected_commitment(&keys)); + } + + /// An empty set is all padding, and must still be well defined. + #[test] + fn an_empty_set_is_all_padding() { + let commitment = run(&[], 64); + let all_identity: Vec = + (0..NUM_VALIDATORS).map(|_| G1Affine::identity()).collect(); + assert_eq!(commitment, public_keys_commitment_bytes(&all_identity)); + } + + /// Order matters, since the bitlist selects signers positionally. + #[test] + fn key_order_changes_the_commitment() { + let keys = relay_keys(); + let mut swapped = keys.clone(); + swapped.swap(0, 1); + assert_ne!(run(&keys, 64), run(&swapped, 64)); + } + + /// A key whose x coordinate is the field modulus, which is not a canonical field element. + /// + /// Note an all-`0xff` key is *not* a good negative case: the top bits are the compression and + /// infinity flags, so it decodes happily as the identity point. + #[test] + fn a_malformed_key_is_rejected_rather_than_absorbed() { + let mut key = hex::decode( + "1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f624\ + 1eabfffeb153ffffb9feffffffffaaab", + ) + .unwrap(); + key[0] |= 0x80; // compressed form, so the x bytes are actually parsed + let mut fixed = [0u8; G1_LEN]; + fixed.copy_from_slice(&key); + + let state = PartialCommitment::new().to_bytes(); + assert_eq!(absorb_slots(&[fixed], 0, 1, state), Err(MalformedKey)); + } + + /// The identity point encodes as a valid compressed key, so a set genuinely containing one is + /// absorbed rather than rejected. Worth pinning so the malformed-key check is not mistaken for + /// an identity check. + #[test] + fn an_identity_key_is_valid_input() { + let mut encoded = Vec::new(); + G1Affine::identity().serialize_compressed(&mut encoded).unwrap(); + let mut key = [0u8; G1_LEN]; + key.copy_from_slice(&encoded); + let state = PartialCommitment::new().to_bytes(); + assert!(absorb_slots(&[key], 0, 1, state).is_ok()); + } + + /// The digest is a wire format, so its encoding is pinned here: a client decodes these bytes + /// out of a header. + #[test] + fn digest_payload_round_trips() { + let payload = ApkCommitmentDigest { set_id: 42, commitment: [7u8; 32] }; + let encoded = payload.encode(); + assert_eq!(encoded.len(), 8 + 32, "set id then commitment, no padding"); + assert_eq!(ApkCommitmentDigest::decode(&mut &encoded[..]).unwrap(), payload); + } +} diff --git a/parachain/runtimes/gargantua/Cargo.toml b/parachain/runtimes/gargantua/Cargo.toml index 6fdf24301..2efa97375 100644 --- a/parachain/runtimes/gargantua/Cargo.toml +++ b/parachain/runtimes/gargantua/Cargo.toml @@ -27,6 +27,7 @@ ismp = { workspace = true } pallet-ismp = { workspace = true } pallet-fishermen = { workspace = true } pallet-ismp-demo = { workspace = true } +pallet-beefy-apk-digest = { workspace = true } pallet-beefy-consensus-proofs = { workspace = true } pallet-ismp-runtime-api = { workspace = true } ismp-sync-committee = { workspace = true } @@ -145,6 +146,7 @@ std = [ "pallet-ismp/std", "pallet-ismp-runtime-api/std", "pallet-ismp-demo/std", + "pallet-beefy-apk-digest/std", "pallet-beefy-consensus-proofs/std", "ismp-sync-committee/std", "ismp-bsc/std", diff --git a/parachain/runtimes/gargantua/src/lib.rs b/parachain/runtimes/gargantua/src/lib.rs index 22df762b2..d120847b2 100644 --- a/parachain/runtimes/gargantua/src/lib.rs +++ b/parachain/runtimes/gargantua/src/lib.rs @@ -882,6 +882,19 @@ impl pallet_messaging_incentives::Config for Runtime { type AdminOrigin = EnsureRoot; } +parameter_types! { + /// A full 1024-slot commitment is roughly 420ms of wasm, so it is absorbed across blocks. At + /// about 410us per slot this is ~26ms per block, and a whole set lands in 16 blocks, well + /// inside the session in which the next authority set is already known. + pub const ApkSlotsPerBlock: u32 = 64; +} + +impl pallet_beefy_apk_digest::Config for Runtime { + type SlotsPerBlock = ApkSlotsPerBlock; + // Measured, not benchmarked; see the note on the default impl. + type WeightInfo = (); +} + // Create the runtime by composing the FRAME pallets that were previously configured. #[frame_support::runtime] mod runtime { @@ -1006,6 +1019,8 @@ mod runtime { pub type HyperFungibleToken = pallet_hyper_fungible_token; #[runtime::pallet_index(92)] pub type MessagingIncentives = pallet_messaging_incentives; + #[runtime::pallet_index(93)] + pub type BeefyApkDigest = pallet_beefy_apk_digest; #[runtime::pallet_index(255)] pub type IsmpGrandpa = ismp_grandpa; } @@ -1285,8 +1300,27 @@ impl_runtime_apis! { impl cumulus_primitives_core::KeyToIncludeInRelayProof for Runtime { fn keys_to_prove() -> cumulus_primitives_core::RelayProofRequest { - // This runtime reads no extra relay chain storage, so no keys need proving. - Default::default() + // The relay chain's BEEFY authority set, `Beefy::Authorities`. The collator only puts + // keys in the relay state proof that were asked for here, and the proof is checked + // against the relay parent's state root by the validators, so reading the set out of + // it needs no trust beyond what a parachain already places in its relay parent. + // + // Note this is not `well_known_keys::AUTHORITIES`, which is `Babe::Authorities`; both + // end in twox128("Authorities") but differ in the pallet prefix. + // `Beefy::NextAuthorities` and `Beefy::ValidatorSetId`. The next set rather than the + // current one, so a client verifying a header signed by set N learns the commitment + // for N+1 and can verify the following update. The set id comes along because the + // commitment is not usable without knowing which set it describes. + cumulus_primitives_core::RelayProofRequest { + keys: alloc::vec![ + cumulus_primitives_core::RelayStorageKey::Top( + pallet_beefy_apk_digest::RELAY_BEEFY_NEXT_AUTHORITIES.to_vec() + ), + cumulus_primitives_core::RelayStorageKey::Top( + pallet_beefy_apk_digest::RELAY_BEEFY_VALIDATOR_SET_ID.to_vec() + ), + ], + } } } From a7c9d6f0bcecd97c7863a4a0503d21d6fee2e0e6 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 7 Aug 2026 17:37:41 +0100 Subject: [PATCH 09/48] read the apk commitment out of a verified hyperbridge header --- evm/src/consensus/Types.sol | 32 ++++++++ evm/tests/foundry/ApkCommitmentDigest.t.sol | 89 +++++++++++++++++++++ modules/pallets/beefy-apk-digest/src/lib.rs | 55 +++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 evm/tests/foundry/ApkCommitmentDigest.t.sol diff --git a/evm/src/consensus/Types.sol b/evm/src/consensus/Types.sol index fa132181c..a11be10c5 100644 --- a/evm/src/consensus/Types.sol +++ b/evm/src/consensus/Types.sol @@ -249,6 +249,8 @@ library HeaderImpl { bytes4 public constant ISMP_CONSENSUS_ID = bytes4("ISMP"); /// ConsensusID for the ISMP timestamp digest deposited by pallet-ismp bytes4 public constant ISMP_TIMESTAMP_ID = bytes4("ISTM"); + /// ConsensusID for the APK commitment digest deposited by pallet-beefy-apk-digest + bytes4 public constant APK_COMMITMENT_ID = bytes4("APKC"); error TimestampNotFound(); @@ -275,4 +277,34 @@ library HeaderImpl { return StateCommitment({timestamp: timestamp, overlayRoot: mmrRoot, stateRoot: childTrieRoot}); } + + /// @dev The commitment to the relay chain's next BEEFY authority set, if this header carries + /// one. Written by `pallet-beefy-apk-digest` on the block a set finishes being absorbed, so + /// most headers do not have it and `found` is false for those. + /// + /// The header itself is already authenticated, through the parachain heads root in the BEEFY + /// MMR leaf, so no further proof is needed: `commitment` can go straight to `ApkProof.verify` + /// as `publicKeysCommitment`, and `setId` says which authority set it describes. + /// + /// Payload is SCALE: a u64 set id little-endian, then the 32 byte commitment. + function apkCommitment(Header memory self) + internal + pure + returns (bool found, uint64 setId, bytes32 commitment) + { + for (uint256 j = 0; j < self.digests.length; j++) { + if (!self.digests[j].isConsensus) continue; + if (self.digests[j].consensus.consensusId != APK_COMMITMENT_ID) continue; + + bytes memory data = self.digests[j].consensus.data; + // Ignore a malformed item rather than reverting: a wrong length means some other + // producer wrote under this engine id, and the caller should see "absent", not fail. + if (data.length != 40) continue; + + setId = uint64(ScaleCodec.decodeUint256(Bytes.substr(data, 0, 8))); + commitment = Bytes.toBytes32(Bytes.substr(data, 8)); + return (true, setId, commitment); + } + return (false, 0, bytes32(0)); + } } diff --git a/evm/tests/foundry/ApkCommitmentDigest.t.sol b/evm/tests/foundry/ApkCommitmentDigest.t.sol new file mode 100644 index 000000000..f1f6d5af4 --- /dev/null +++ b/evm/tests/foundry/ApkCommitmentDigest.t.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.17; + +import {Test} from "forge-std/Test.sol"; +import {Header, Digest, DigestItem, HeaderImpl} from "../../src/consensus/Types.sol"; + +/// The client side of `pallet-beefy-apk-digest`: reading the APK commitment out of a hyperbridge +/// header that a verifier has already authenticated through the BEEFY MMR's parachain heads root. +/// +/// The payload bytes here are the SCALE encoding the pallet produces, a little-endian u64 set id +/// followed by the 32 byte commitment. Both sides are pinned: `digest_payload_round_trips` in the +/// pallet asserts the same 8 + 32 layout. +contract ApkCommitmentDigestTest is Test { + using HeaderImpl for Header; + + bytes constant PAYLOAD_577 = + hex"41020000000000000303030303030303030303030303030303030303030303030303030303030303"; + + function _header(Digest[] memory digests) internal pure returns (Header memory) { + return Header({ + parentHash: bytes32(0), + number: 1, + stateRoot: bytes32(0), + extrinsicRoot: bytes32(0), + digests: digests + }); + } + + function _consensus(bytes4 id, bytes memory data) internal pure returns (Digest memory d) { + d.isConsensus = true; + d.consensus = DigestItem({consensusId: id, data: data}); + } + + function _preRuntime(bytes4 id) internal pure returns (Digest memory d) { + d.isPreRuntime = true; + d.preruntime = DigestItem({consensusId: id, data: hex"0102"}); + } + + function test_reads_the_commitment_the_pallet_wrote() public view { + Digest[] memory digests = new Digest[](1); + digests[0] = _consensus(HeaderImpl.APK_COMMITMENT_ID, PAYLOAD_577); + + (bool found, uint64 setId, bytes32 commitment) = _header(digests).apkCommitment(); + assertTrue(found, "commitment not found"); + assertEq(setId, 577, "set id decoded wrong; check little-endian"); + assertEq(commitment, bytes32(uint256(0x0303030303030303030303030303030303030303030303030303030303030303))); + } + + /// The normal case: a header carrying aura's items alongside ours. + function test_finds_it_among_other_digests() public view { + Digest[] memory digests = new Digest[](3); + digests[0] = _preRuntime(bytes4("aura")); + digests[1] = _consensus(HeaderImpl.APK_COMMITMENT_ID, PAYLOAD_577); + digests[2] = _preRuntime(bytes4("aura")); + + (bool found, uint64 setId,) = _header(digests).apkCommitment(); + assertTrue(found); + assertEq(setId, 577); + } + + /// Most headers do not complete a set, and that must read as absent rather than revert. + function test_absent_on_a_header_without_one() public view { + Digest[] memory digests = new Digest[](1); + digests[0] = _preRuntime(bytes4("aura")); + + (bool found,, bytes32 commitment) = _header(digests).apkCommitment(); + assertFalse(found); + assertEq(commitment, bytes32(0)); + } + + /// Another engine's consensus item must not be mistaken for ours. Same digest variant, so the + /// engine id is the only thing separating them. + function test_ignores_another_engines_consensus_item() public view { + Digest[] memory digests = new Digest[](1); + digests[0] = _consensus(bytes4("ISMP"), PAYLOAD_577); + + (bool found,,) = _header(digests).apkCommitment(); + assertFalse(found, "an ISMP digest was read as an APK commitment"); + } + + /// A wrong-length payload under our engine id is skipped rather than decoded into garbage. + function test_malformed_payload_reads_as_absent() public view { + Digest[] memory digests = new Digest[](1); + digests[0] = _consensus(HeaderImpl.APK_COMMITMENT_ID, hex"4102000000000000"); + + (bool found,,) = _header(digests).apkCommitment(); + assertFalse(found, "a truncated payload should not decode"); + } +} diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index e4445829d..541d74aae 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -104,6 +104,25 @@ impl PairedAuthority { } } +impl ApkCommitmentDigest { + /// Pull the commitment out of a header's digest logs, if this pallet wrote one into it. + /// + /// This is the client side of the design. A verifier that has already authenticated a + /// hyperbridge header, through the parachain heads root in the BEEFY MMR leaf, can read the + /// commitment straight out of that header with no further proof, and hand it to + /// `ApkProof.verify` as `publicKeysCommitment`. + /// + /// Returns the first matching item. The pallet only ever writes one per block, and only on the + /// block a set completes. + pub fn find_in(digest: &sp_runtime::generic::Digest) -> Option { + digest.logs().iter().find_map(|log| match log { + sp_runtime::DigestItem::Consensus(id, payload) if *id == APK_ENGINE_ID => + Self::decode(&mut &payload[..]).ok(), + _ => None, + }) + } +} + /// Where the running commitment has got to. #[derive(Clone, Encode, Decode, TypeInfo, Default, MaxEncodedLen)] pub struct Progress { @@ -453,6 +472,42 @@ mod tests { assert!(absorb_slots(&[key], 0, 1, state).is_ok()); } + /// A client finds the commitment in a header carrying unrelated digest items too, which is the + /// normal case: aura and the parachain system both write their own. + #[test] + fn commitment_is_found_among_other_digest_items() { + let payload = ApkCommitmentDigest { set_id: 577, commitment: [3u8; 32] }; + let digest = sp_runtime::generic::Digest { + logs: alloc::vec![ + sp_runtime::DigestItem::PreRuntime(*b"aura", alloc::vec![1, 2, 3]), + sp_runtime::DigestItem::Consensus(APK_ENGINE_ID, payload.encode()), + sp_runtime::DigestItem::Seal(*b"aura", alloc::vec![4, 5, 6]), + ], + }; + assert_eq!(ApkCommitmentDigest::find_in(&digest), Some(payload)); + } + + /// A header from a block that did not complete a set carries nothing, and a client must treat + /// that as "no update" rather than an error. + #[test] + fn a_header_without_our_digest_yields_nothing() { + let digest = sp_runtime::generic::Digest { + logs: alloc::vec![sp_runtime::DigestItem::PreRuntime(*b"aura", alloc::vec![1])], + }; + assert_eq!(ApkCommitmentDigest::find_in(&digest), None); + } + + /// Another engine's consensus item must not be mistaken for ours, even though the variant is + /// the same. This is what the engine id is for. + #[test] + fn another_engines_consensus_item_is_ignored() { + let payload = ApkCommitmentDigest { set_id: 1, commitment: [9u8; 32] }; + let digest = sp_runtime::generic::Digest { + logs: alloc::vec![sp_runtime::DigestItem::Consensus(*b"BEEF", payload.encode())], + }; + assert_eq!(ApkCommitmentDigest::find_in(&digest), None); + } + /// The digest is a wire format, so its encoding is pinned here: a client decodes these bytes /// out of a header. #[test] From 167d1a7f31b05725aca260de9af427d51257fc7c Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 7 Aug 2026 17:59:26 +0100 Subject: [PATCH 10/48] verify beefy consensus with an aggregate public key proof instead of per signer merkle paths --- evm/src/consensus/BlsApkBeefy.sol | 304 +++++++++++++++++++ evm/src/consensus/Types.sol | 57 ++++ modules/consensus/beefy/verifier/src/test.rs | 23 +- 3 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 evm/src/consensus/BlsApkBeefy.sol diff --git a/evm/src/consensus/BlsApkBeefy.sol b/evm/src/consensus/BlsApkBeefy.sol new file mode 100644 index 000000000..114f14769 --- /dev/null +++ b/evm/src/consensus/BlsApkBeefy.sol @@ -0,0 +1,304 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pragma solidity ^0.8.17; + +import {IConsensusV2, IntermediateState, StateCommitment} from "@hyperbridge/core/interfaces/IConsensusV2.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {Bytes} from "@polytope-labs/solidity-merkle-trees/src/trie/Bytes.sol"; +import {MerkleMountainRange} from "@polytope-labs/solidity-merkle-trees/src/MerkleMountainRange.sol"; +import {MerkleMultiProof} from "@polytope-labs/solidity-merkle-trees/src/MerkleMultiProof.sol"; +import {ScaleCodec} from "@polytope-labs/solidity-merkle-trees/src/trie/polkadot/ScaleCodec.sol"; + +import {Codec} from "./Codec.sol"; +import { + ApkAuthoritySet, + BlsApkConsensusState, + BlsApkRelayChainProof, + BeefyMmrLeaf, + Commitment, + Digest, + Header, + HeaderImpl, + Parachain, + ParachainProof, + PartialBeefyMmrLeaf +} from "./Types.sol"; + +interface IApkProof { + function verify( + uint256 publicKeysCommitment, + uint256[5] calldata bitlist, + bytes32[3] calldata apk, + bytes calldata apkProof, + bytes32[3] calldata message, + bytes32[3] calldata signature, + bytes32[6] calldata apk2 + ) external view; + + function hashToG1(bytes memory message) external view returns (bytes32[3] memory); +} + +/** + * @title BEEFY consensus verified by an aggregate public key proof + * @notice Verifies BEEFY finality without touching individual validator keys. + * + * @dev The merkle client in `BlsBeefy.sol` proves each signer's public key against the authority + * set's keyset root, so its cost grows with the number of signers: roughly 17k gas each, for the + * G2 addition and the compression needed to rebuild the leaf. At a few hundred validators that + * dominates everything else. + * + * Here a SNARK does that work instead. The prover shows that an aggregate public key corresponds + * to exactly the validators named in a bitlist, against a commitment to the whole set, and the + * contract checks one proof and one pairing. Nothing in the calldata or the verification grows + * with the number of signers. + * + * The commitment the proof is checked against does not come from the relay chain's MMR leaf, the + * way the keyset root does. Hyperbridge computes it over the relay's next authority set and + * publishes it in a header digest, so a client picks it up from a header it has already verified + * and carries it in its consensus state. That is what `ApkAuthoritySet.apkCommitment` holds, and + * why the state has to be seeded with the starting set's commitment at initialisation. + * + * Requires Prague for the EIP-2537 precompiles, same as the merkle client. + */ +contract BlsApkBeefy is IConsensusV2, ERC165 { + /// The payload id for the mmr root in a BEEFY commitment, "mh" + bytes2 public constant MMR_ROOT_PAYLOAD_ID = bytes2("mh"); + + /// The APK proof verifier, holding the circuit's verifying key. + IApkProof public immutable _apk; + + /// The commitment was signed by a set this client does not know. + error UnknownAuthoritySet(); + /// Fewer than two thirds of the set signed. + error SuperMajorityRequired(); + /// The APK proof or the aggregate signature did not verify. + error InvalidAggregateProof(); + /// The commitment carried no mmr root payload. + error MmrRootHashMissing(); + /// The mmr leaf was not in the tree the commitment attests to. + error InvalidMmrProof(); + /// A parachain header was not in the heads root. + error InvalidParachainHeaderProof(); + /// The authority set has no APK commitment yet, so no proof against it can be checked. + error MissingApkCommitment(); + + constructor(address apkProof) { + _apk = IApkProof(apkProof); + } + + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) { + return interfaceId == type(IConsensusV2).interfaceId || super.supportsInterface(interfaceId); + } + + /// @dev IConsensusV2 entry point. + function verify(bytes calldata previousState, bytes calldata proof) + external + view + returns (bytes memory, IntermediateState[] memory, uint256) + { + BlsApkConsensusState memory consensusState = abi.decode(previousState, (BlsApkConsensusState)); + (BlsApkRelayChainProof memory relay, ParachainProof memory parachain) = + abi.decode(proof, (BlsApkRelayChainProof, ParachainProof)); + + // Replays are idempotent rather than reverting, matching the merkle client. + if (consensusState.latestHeight >= relay.commitment.blockNumber) { + return (abi.encode(consensusState), new IntermediateState[](0), consensusState.nextAuthoritySet.id); + } + + (BlsApkConsensusState memory newState, bytes32 headsRoot) = verifyMmrUpdateProof(consensusState, relay); + (IntermediateState[] memory intermediates, bool found, uint64 setId, bytes32 commitment) = + verifyParachainHeaderProof(headsRoot, parachain); + + // Forward chaining: a verified header may carry the commitment for a set this client does + // not have keys for yet. Picking it up here is what lets the next update be verified at + // all, and is the reason the digest names the *next* set rather than the current one. + if (found && setId == newState.nextAuthoritySet.id && newState.nextAuthoritySet.apkCommitment == bytes32(0)) { + newState.nextAuthoritySet.apkCommitment = commitment; + } + + return (abi.encode(newState), intermediates, newState.nextAuthoritySet.id); + } + + /// @dev Verify the signed mmr root, then roll the authority sets forward. + function verifyMmrUpdateProof(BlsApkConsensusState memory trustedState, BlsApkRelayChainProof memory relayProof) + internal + view + returns (BlsApkConsensusState memory, bytes32) + { + Commitment memory commitment = relayProof.commitment; + if ( + commitment.validatorSetId != trustedState.currentAuthoritySet.id + && commitment.validatorSetId != trustedState.nextAuthoritySet.id + ) { + revert UnknownAuthoritySet(); + } + + bool isCurrent = commitment.validatorSetId == trustedState.currentAuthoritySet.id; + ApkAuthoritySet memory authoritySet = isCurrent ? trustedState.currentAuthoritySet : trustedState.nextAuthoritySet; + + // A set whose commitment has not been learned from a digest yet cannot be verified against. + // Reverting here is deliberate: silently accepting would mean checking the proof against a + // zero commitment. + if (authoritySet.apkCommitment == bytes32(0)) revert MissingApkCommitment(); + + verifySignedByApk(Codec.Encode(commitment), relayProof, authoritySet); + + uint256 payloadLength = commitment.payload.length; + bytes32 mmrRoot; + for (uint256 i = 0; i < payloadLength; i++) { + if (commitment.payload[i].id == MMR_ROOT_PAYLOAD_ID && commitment.payload[i].data.length == 32) { + mmrRoot = Bytes.toBytes32(commitment.payload[i].data); + } + } + if (mmrRoot == bytes32(0)) revert MmrRootHashMissing(); + + verifyMmrLeaf(trustedState, relayProof, mmrRoot); + + if (relayProof.latestMmrLeaf.nextAuthoritySet.id > trustedState.nextAuthoritySet.id) { + trustedState.currentAuthoritySet = trustedState.nextAuthoritySet; + // The incoming set's size comes from the mmr leaf, but its APK commitment is not known + // until a header digest supplies it, so it starts empty. + trustedState.nextAuthoritySet = ApkAuthoritySet({ + id: relayProof.latestMmrLeaf.nextAuthoritySet.id, + len: relayProof.latestMmrLeaf.nextAuthoritySet.len, + apkCommitment: bytes32(0) + }); + } + trustedState.latestHeight = commitment.blockNumber; + + return (trustedState, relayProof.latestMmrLeaf.extra); + } + + /** + * @notice Establish that a supermajority of `authoritySet` signed `encodedCommitment`. + * @dev Two things are checked, and the split matters. The SNARK proves that `apk` is the + * aggregate of exactly the validators set in `bitlist`, against the set's commitment. The + * pairing inside `ApkProof` then checks the aggregate signature against that key, and binds + * `apk2` to `apk` so a caller cannot supply an unrelated G2 point. The threshold is counted + * here, because the circuit proves who signed but has no opinion on whether that is enough. + */ + function verifySignedByApk( + bytes memory encodedCommitment, + BlsApkRelayChainProof memory relayProof, + ApkAuthoritySet memory authoritySet + ) internal view { + uint256 signed = countSigners(relayProof.bitlist); + if (!checkParticipationThreshold(signed, authoritySet.len)) revert SuperMajorityRequired(); + + bytes32[3] memory message = _apk.hashToG1(encodedCommitment); + + // `verify` reverts on failure rather than returning false, so a successful call is the + // whole result. Wrapped so the reason surfaces as this contract's error. + try _apk.verify( + uint256(authoritySet.apkCommitment), + relayProof.bitlist, + relayProof.apk, + relayProof.apkProof, + message, + relayProof.signature, + relayProof.apk2 + ) {} catch { + revert InvalidAggregateProof(); + } + } + + /// @dev Population count over the bitlist. Fixed cost regardless of how many signed, which is + /// the point of the whole scheme. + function countSigners(uint256[5] memory bitlist) internal pure returns (uint256 count) { + for (uint256 w = 0; w < 5; w++) { + uint256 word = bitlist[w]; + while (word != 0) { + word &= word - 1; + count++; + } + } + } + + /// @dev Two thirds plus one, matching the merkle client and substrate's own rule. + function checkParticipationThreshold(uint256 signed, uint256 total) internal pure returns (bool) { + return total > 0 && signed * 3 > total * 2; + } + + /// @dev The signed mmr root must attest to the leaf carrying the parachain heads. + function verifyMmrLeaf( + BlsApkConsensusState memory trustedState, + BlsApkRelayChainProof memory relay, + bytes32 mmrRoot + ) internal pure { + bytes32 hash = keccak256( + Codec.Encode( + PartialBeefyMmrLeaf({ + version: relay.latestMmrLeaf.version, + parentNumber: relay.latestMmrLeaf.parentNumber, + parentHash: relay.latestMmrLeaf.parentHash, + nextAuthoritySet: relay.latestMmrLeaf.nextAuthoritySet, + extra: relay.latestMmrLeaf.extra + }) + ) + ); + uint256 leafCount = leafIndex(trustedState.beefyActivationBlock, relay.latestMmrLeaf.parentNumber) + 1; + + MerkleMountainRange.Leaf[] memory leaves = new MerkleMountainRange.Leaf[](1); + leaves[0] = MerkleMountainRange.Leaf({index: relay.latestMmrLeaf.leafIndex, hash: hash}); + + bool valid = MerkleMountainRange.VerifyProof(mmrRoot, relay.mmrProof, leaves, leafCount); + if (!valid) revert InvalidMmrProof(); + } + + /// @dev Verify the parachain headers against the heads root, and surface any APK commitment + /// they carry so the caller can roll it into the consensus state. + function verifyParachainHeaderProof(bytes32 headsRoot, ParachainProof memory proof) + internal + pure + returns (IntermediateState[] memory, bool found, uint64 setId, bytes32 apkCommitment) + { + uint256 len = proof.parachains.length; + MerkleMultiProof.Leaf[] memory leaves = new MerkleMultiProof.Leaf[](len); + IntermediateState[] memory intermediates = new IntermediateState[](len); + + for (uint256 i = 0; i < len; i++) { + Parachain memory para = proof.parachains[i]; + Header memory header = Codec.DecodeHeader(para.header); + + leaves[i] = MerkleMultiProof.Leaf( + para.index, + keccak256(bytes.concat(ScaleCodec.encode32(uint32(para.id)), ScaleCodec.encodeBytes(para.header))) + ); + + intermediates[i] = IntermediateState({ + stateMachineId: para.id, + height: header.number, + commitment: HeaderImpl.stateCommitment(header) + }); + + // Only the first commitment found is used; the pallet writes at most one per block. + if (!found) { + (found, setId, apkCommitment) = HeaderImpl.apkCommitment(header); + } + } + + if (len > 0) { + bool valid = MerkleMultiProof.VerifyProof(headsRoot, proof.proof, leaves, proof.leafCount); + if (!valid) revert InvalidParachainHeaderProof(); + } + + return (intermediates, found, setId, apkCommitment); + } + + /// @dev Leaf index for a relay chain block, given where beefy was activated. + function leafIndex(uint256 activationBlock, uint256 parentNumber) internal pure returns (uint256) { + return activationBlock == 0 ? parentNumber : parentNumber - activationBlock; + } +} diff --git a/evm/src/consensus/Types.sol b/evm/src/consensus/Types.sol index a11be10c5..93fa7bad8 100644 --- a/evm/src/consensus/Types.sol +++ b/evm/src/consensus/Types.sol @@ -219,6 +219,63 @@ struct BlsBeefyConsensusProof { ParachainProof parachain; } +// An authority set identified by its APK commitment rather than by a merkle root over keys. +// +// The commitment is Poseidon2 over the validators' BLS12-381 G1 keys, padded to the circuit's +// fixed width. Unlike the keyset root it does not come from the MMR leaf: hyperbridge publishes it +// in a header digest, and a client picks it up from a header it has already verified. That is why +// it is carried in the consensus state rather than supplied with each proof. +struct ApkAuthoritySet { + /// Id of the set. + uint64 id; + /// Number of validators in the set, for the two-thirds threshold. + uint32 len; + /// Poseidon2 commitment over the set's G1 public keys. + bytes32 apkCommitment; +} + +struct BlsApkConsensusState { + /// block number for the latest mmr_root_hash + uint256 latestHeight; + /// Block number that the beefy protocol was activated on the relay chain. + uint256 beefyActivationBlock; + /// authorities for the current round + ApkAuthoritySet currentAuthoritySet; + /// authorities for the next round + ApkAuthoritySet nextAuthoritySet; +} + +// A BEEFY relay chain proof verified by a SNARK over the aggregate public key, rather than by a +// merkle multi-proof of each signer's key. +// +// The saving is that nothing here grows with the number of signers: the bitlist is fixed width and +// the proof is constant size, where the merkle path costs roughly 17k gas per signer. +struct BlsApkRelayChainProof { + // A commitment to the finalized state + Commitment commitment; + // Which validators signed, one bit each, 1024 slots over five words + uint256[5] bitlist; + // Aggregate public key in G1, proven correct against the set's APK commitment + bytes32[3] apk; + // The same aggregate in G2, bound to `apk` by the pairing check inside ApkProof + bytes32[6] apk2; + // PLONK proof that `apk` is the aggregate of exactly the validators in `bitlist` + bytes apkProof; + // Sum of the signers' BLS signatures, a G1 point + bytes32[3] signature; + // Latest leaf added to mmr + BeefyMmrLeaf latestMmrLeaf; + // Proof for the latest mmr leaf + bytes32[] mmrProof; +} + +struct BlsApkBeefyConsensusProof { + // The proof items for the relay chain consensus + BlsApkRelayChainProof relay; + // Proof items for parachain headers + ParachainProof parachain; +} + struct DigestItem { bytes4 consensusId; bytes data; diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index 5ec89f64d..3fe84d05f 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -1888,7 +1888,7 @@ async fn bls_live_abi_fixture() { async fn bls_apk_group_binding() { use ark_bls12_381::{Bls12_381, G1Affine, G1Projective, G2Affine, G2Projective}; use ark_ec::{AffineRepr, CurveGroup, Group, pairing::Pairing}; - use ark_serialize::CanonicalDeserialize; + use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use beefy_prover::bls::{ aggregate_signatures, beefy_g1_authorities, beefy_g2_authorities, decode_paired_justification, @@ -1968,4 +1968,25 @@ async fn bls_apk_group_binding() { "[ok] group binding holds for {signer_count} live signers: apk_g1 <-> apk_g2 bound, \ and BEEFY's G1 signature verifies against the bound apk_g2", ); + + // Dump everything an APK proof fixture needs, so a test elsewhere can be built against real + // BEEFY data rather than synthetic keypairs. + let hex_affine_g1 = |p: &G1Affine| { + let mut b = Vec::new(); + p.serialize_compressed(&mut b).unwrap(); + hex::encode(b) + }; + let hex_affine_g2 = |p: &G2Affine| { + let mut b = Vec::new(); + p.serialize_compressed(&mut b).unwrap(); + hex::encode(b) + }; + println!("=== apk fixture inputs ==="); + println!("message {}", hex::encode(&message)); + println!("apk_g1 {}", hex_affine_g1(&apk_g1)); + println!("apk_g2 {}", hex_affine_g2(&apk_g2)); + println!("agg_sig {}", hex::encode(aggregate_signature)); + for (i, key) in g1_keys.iter().enumerate() { + println!("g1[{i}] {}", hex::encode(key)); + } } From e530d0caadd2f59d24eaf28da9bdd4698c80b1bd Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 7 Aug 2026 22:57:35 +0100 Subject: [PATCH 11/48] decide the apk digest rotation step in a pure function so the restart case can be tested without a mock chain --- modules/pallets/beefy-apk-digest/src/lib.rs | 132 +++++++++++++++++--- 1 file changed, 116 insertions(+), 16 deletions(-) diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index 541d74aae..ccc404512 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -123,8 +123,15 @@ impl ApkCommitmentDigest { } } +impl Progress { + /// A chain that has absorbed nothing yet. + pub fn fresh(set_digest: [u8; 32]) -> Self { + Self { set_digest, absorbed: 0, state: PartialCommitment::new().to_bytes() } + } +} + /// Where the running commitment has got to. -#[derive(Clone, Encode, Decode, TypeInfo, Default, MaxEncodedLen)] +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo, Default, MaxEncodedLen)] pub struct Progress { /// Identifies the set being absorbed, so a rotation part way through restarts rather than /// mixing keys from two sets into one commitment. @@ -214,17 +221,17 @@ pub mod pallet { let set_id = Self::relay_beefy_set_id()?; let set_digest = sp_io::hashing::blake2_256(&keys.encode()); - let mut progress = match Pending::::get() { - // A rotation part way through invalidates the chain, so start again. - Some(p) if p.set_digest != set_digest => Self::start(set_digest), - Some(p) => p, - None => { - // Nothing to do once this set is already published. - if Published::::get().map(|(d, _)| d) == Some(set_digest) { - return Ok(0); - } - Self::start(set_digest) + let mut progress = match next_progress( + Pending::::get().as_ref(), + Published::::get().map(|(d, _)| d), + set_digest, + ) { + Step::Done => return Ok(0), + Step::Restart => { + Self::deposit_event(Event::CommitmentStarted { set_digest }); + Progress::fresh(set_digest) }, + Step::Continue(p) => p, }; let from = progress.absorbed as usize; @@ -267,11 +274,6 @@ pub mod pallet { Ok(current.saturating_add(1)) } - fn start(set_digest: [u8; 32]) -> Progress { - Self::deposit_event(Event::CommitmentStarted { set_digest }); - Progress { set_digest, absorbed: 0, state: PartialCommitment::new().to_bytes() } - } - /// The relay state proof for this block, checked against the relay parent's state root. /// /// Only carries the keys the runtime asks for through @@ -333,6 +335,36 @@ impl WeightInfo for () { } } +/// What to do with the commitment this block. +#[derive(Debug, PartialEq, Eq)] +pub enum Step { + /// This set is already published; nothing to do. + Done, + /// Begin, or begin again because the set changed under us. + Restart, + /// Carry on from where the last block left off. + Continue(Progress), +} + +/// Decide how to proceed, given what is in progress and what has already been published. +/// +/// Kept pure so the rotation case can be tested without a mock chain. The case that matters is a +/// set changing part way through: the Merkle-Damgard chain is over one specific key list, so +/// carrying the state across a rotation would silently produce a commitment belonging to neither +/// set. Restarting is the only safe answer. +pub fn next_progress( + pending: Option<&Progress>, + published: Option<[u8; 32]>, + set_digest: [u8; 32], +) -> Step { + match pending { + Some(p) if p.set_digest != set_digest => Step::Restart, + Some(p) => Step::Continue(p.clone()), + None if published == Some(set_digest) => Step::Done, + None => Step::Restart, + } +} + /// A key that did not decode as a G1 curve point. #[derive(Debug, PartialEq, Eq)] pub struct MalformedKey; @@ -472,6 +504,74 @@ mod tests { assert!(absorb_slots(&[key], 0, 1, state).is_ok()); } + // ── rotation ──────────────────────────────────────────────────────────────────────────── + + const SET_A: [u8; 32] = [0xaa; 32]; + const SET_B: [u8; 32] = [0xbb; 32]; + + #[test] + fn a_fresh_chain_starts() { + assert_eq!(next_progress(None, None, SET_A), Step::Restart); + } + + #[test] + fn an_already_published_set_is_left_alone() { + assert_eq!(next_progress(None, Some(SET_A), SET_A), Step::Done); + } + + /// A new set arriving after one was published starts rather than stopping. + #[test] + fn a_new_set_starts_even_though_another_was_published() { + assert_eq!(next_progress(None, Some(SET_A), SET_B), Step::Restart); + } + + #[test] + fn work_in_progress_on_the_same_set_continues() { + let p = Progress { set_digest: SET_A, absorbed: 128, state: [1u8; 32] }; + assert_eq!(next_progress(Some(&p), None, SET_A), Step::Continue(p)); + } + + /// The case this whole function exists for: the authority set changed while a commitment was + /// part way through. + #[test] + fn a_rotation_part_way_through_restarts() { + let p = Progress { set_digest: SET_A, absorbed: 512, state: [1u8; 32] }; + assert_eq!(next_progress(Some(&p), None, SET_B), Step::Restart); + } + + /// And restarting has to mean *restarting*, not resuming with a relabelled set. If the state + /// carried over, the commitment would be a Merkle-Damgard chain over the first set's keys + /// followed by the second's, belonging to neither, and nothing downstream would notice. + #[test] + fn a_restart_discards_the_partial_state() { + let fresh = Progress::fresh(SET_B); + assert_eq!(fresh.absorbed, 0); + assert_eq!(fresh.state, PartialCommitment::new().to_bytes()); + assert_eq!(fresh.set_digest, SET_B); + } + + /// End to end over the absorption itself: absorb part of one set, rotate, and the commitment + /// that comes out must be the second set's, identical to having never seen the first. + #[test] + fn a_commitment_interrupted_by_a_rotation_is_not_a_mixture() { + let first = relay_keys(); + let mut second = relay_keys(); + second.swap(0, 1); // a different set, same size + + // absorb 300 slots of the first set, then rotate + let mut state = PartialCommitment::new().to_bytes(); + state = absorb_slots(&first, 0, 300, state).unwrap(); + assert_eq!( + next_progress(Some(&Progress { set_digest: SET_A, absorbed: 300, state }), None, SET_B), + Step::Restart + ); + + // restart discards that state, so the result is the second set's commitment alone + let restarted = run(&second, 64); + assert_eq!(restarted, expected_commitment(&second)); + assert_ne!(restarted, expected_commitment(&first), "the two sets must not collide"); + } + /// A client finds the commitment in a header carrying unrelated digest items too, which is the /// normal case: aura and the parachain system both write their own. #[test] From 4c99200c555c8378a943a937e95c7e3f687bb2e9 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Sat, 8 Aug 2026 19:35:09 +0100 Subject: [PATCH 12/48] drop the per signer merkle path now that beefy bls consensus is verified with an aggregate public key proof --- Cargo.lock | 2 + evm/rust/abi/BlsBeefy.json | 553 ------- evm/rust/src/conversions.rs | 188 +-- evm/rust/src/generated/bls_beefy.rs | 24 - evm/rust/src/generated/mod.rs | 2 +- evm/src/consensus/BlsAggregate.sol | 54 +- evm/src/consensus/BlsApkBeefy.sol | 18 +- evm/src/consensus/BlsBeefy.sol | 319 ---- evm/src/consensus/Types.sol | 46 - evm/tests/foundry/BlsAggregate.t.sol | 78 - evm/tests/foundry/BlsBeefy.t.sol | 84 - .../foundry/fixtures/bls-beefy-live-proof.hex | 1 - .../foundry/fixtures/bls-beefy-live-state.hex | 1 - .../foundry/fixtures/bls-beefy-proof.hex | 1 - .../foundry/fixtures/bls-beefy-state.hex | 1 - modules/consensus/beefy/primitives/src/lib.rs | 93 +- modules/consensus/beefy/prover/src/bls.rs | 224 +-- modules/consensus/beefy/verifier/Cargo.toml | 12 +- modules/consensus/beefy/verifier/src/bls.rs | 58 - modules/consensus/beefy/verifier/src/error.rs | 18 - modules/consensus/beefy/verifier/src/lib.rs | 123 -- modules/consensus/beefy/verifier/src/test.rs | 1386 +++++------------ modules/ismp/clients/beefy/Cargo.toml | 1 - modules/ismp/clients/beefy/src/consensus.rs | 9 - modules/ismp/clients/beefy/src/lib.rs | 23 +- .../pallets/beefy-consensus-proofs/src/lib.rs | 15 +- .../beefy-consensus-proofs/src/types.rs | 2 - parachain/runtimes/gargantua/Cargo.toml | 4 +- parachain/runtimes/gargantua/src/ismp.rs | 4 +- parachain/simtests/src/lib.rs | 1 - parachain/simtests/src/pallet_beefy_bls.rs | 261 ---- 31 files changed, 434 insertions(+), 3172 deletions(-) delete mode 100644 evm/rust/abi/BlsBeefy.json delete mode 100644 evm/rust/src/generated/bls_beefy.rs delete mode 100644 evm/src/consensus/BlsBeefy.sol delete mode 100644 evm/tests/foundry/BlsBeefy.t.sol delete mode 100644 evm/tests/foundry/fixtures/bls-beefy-live-proof.hex delete mode 100644 evm/tests/foundry/fixtures/bls-beefy-live-state.hex delete mode 100644 evm/tests/foundry/fixtures/bls-beefy-proof.hex delete mode 100644 evm/tests/foundry/fixtures/bls-beefy-state.hex delete mode 100644 modules/consensus/beefy/verifier/src/bls.rs delete mode 100644 parachain/simtests/src/pallet_beefy_bls.rs diff --git a/Cargo.lock b/Cargo.lock index 94f152b10..223e12bf6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2944,6 +2944,7 @@ dependencies = [ "alloy-primitives 1.5.7", "alloy-sol-types 1.5.7", "anyhow", + "apk-commitment", "ark-bls12-381 0.4.0", "ark-ec 0.4.2", "ark-ff 0.4.2", @@ -2961,6 +2962,7 @@ dependencies = [ "polkadot-sdk", "primitive-types 0.13.1", "rs_merkle", + "serde_json", "sha2 0.10.9", "sp1-verifier 6.1.0", "subxt 0.42.1", diff --git a/evm/rust/abi/BlsBeefy.json b/evm/rust/abi/BlsBeefy.json deleted file mode 100644 index 23be33b71..000000000 --- a/evm/rust/abi/BlsBeefy.json +++ /dev/null @@ -1,553 +0,0 @@ -[ - { - "type": "function", - "name": "MMR_ROOT_PAYLOAD_ID", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes2", - "internalType": "bytes2" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "checkParticipationThreshold", - "inputs": [ - { - "name": "len", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "total", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "noOp", - "inputs": [ - { - "name": "s", - "type": "tuple", - "internalType": "struct BeefyConsensusState", - "components": [ - { - "name": "latestHeight", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "beefyActivationBlock", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "currentAuthoritySet", - "type": "tuple", - "internalType": "struct AuthoritySetCommitment", - "components": [ - { - "name": "id", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "len", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "root", - "type": "bytes32", - "internalType": "bytes32" - } - ] - }, - { - "name": "nextAuthoritySet", - "type": "tuple", - "internalType": "struct AuthoritySetCommitment", - "components": [ - { - "name": "id", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "len", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "root", - "type": "bytes32", - "internalType": "bytes32" - } - ] - } - ] - }, - { - "name": "p", - "type": "tuple", - "internalType": "struct BlsBeefyConsensusProof", - "components": [ - { - "name": "relay", - "type": "tuple", - "internalType": "struct BlsRelayChainProof", - "components": [ - { - "name": "commitment", - "type": "tuple", - "internalType": "struct Commitment", - "components": [ - { - "name": "payload", - "type": "tuple[]", - "internalType": "struct Payload[]", - "components": [ - { - "name": "id", - "type": "bytes2", - "internalType": "bytes2" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - } - ] - }, - { - "name": "blockNumber", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "validatorSetId", - "type": "uint64", - "internalType": "uint64" - } - ] - }, - { - "name": "signers", - "type": "tuple[]", - "internalType": "struct BlsSigner[]", - "components": [ - { - "name": "publicKey", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "authorityIndex", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "aggregateSignature", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "latestMmrLeaf", - "type": "tuple", - "internalType": "struct BeefyMmrLeaf", - "components": [ - { - "name": "version", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "parentNumber", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "parentHash", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "nextAuthoritySet", - "type": "tuple", - "internalType": "struct AuthoritySetCommitment", - "components": [ - { - "name": "id", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "len", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "root", - "type": "bytes32", - "internalType": "bytes32" - } - ] - }, - { - "name": "extra", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "leafIndex", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "mmrProof", - "type": "bytes32[]", - "internalType": "bytes32[]" - }, - { - "name": "blsCommitment", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "keysetProof", - "type": "bytes32[]", - "internalType": "bytes32[]" - }, - { - "name": "proof", - "type": "bytes32[]", - "internalType": "bytes32[]" - } - ] - }, - { - "name": "parachain", - "type": "tuple", - "internalType": "struct ParachainProof", - "components": [ - { - "name": "parachains", - "type": "tuple[]", - "internalType": "struct Parachain[]", - "components": [ - { - "name": "index", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "header", - "type": "bytes", - "internalType": "bytes" - } - ] - }, - { - "name": "proof", - "type": "bytes32[]", - "internalType": "bytes32[]" - }, - { - "name": "leafCount", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ] - } - ], - "outputs": [], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "supportsInterface", - "inputs": [ - { - "name": "interfaceId", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "verify", - "inputs": [ - { - "name": "previousState", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "proof", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [ - { - "name": "", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "", - "type": "tuple[]", - "internalType": "struct IntermediateState[]", - "components": [ - { - "name": "stateMachineId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "height", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "commitment", - "type": "tuple", - "internalType": "struct StateCommitment", - "components": [ - { - "name": "timestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "overlayRoot", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "stateRoot", - "type": "bytes32", - "internalType": "bytes32" - } - ] - } - ] - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "verifyAuthorities", - "inputs": [ - { - "name": "commitment", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "signers", - "type": "tuple[]", - "internalType": "struct BlsSigner[]", - "components": [ - { - "name": "publicKey", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "authorityIndex", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "aggregateSignature", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "keysetRoot", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "authorityProof", - "type": "bytes32[]", - "internalType": "bytes32[]" - }, - { - "name": "authorityCount", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "view" - }, - { - "type": "error", - "name": "EmptyLeaves", - "inputs": [] - }, - { - "type": "error", - "name": "EmptyTree", - "inputs": [] - }, - { - "type": "error", - "name": "EmptyTree", - "inputs": [] - }, - { - "type": "error", - "name": "G1AddFailed", - "inputs": [] - }, - { - "type": "error", - "name": "G2AddFailed", - "inputs": [] - }, - { - "type": "error", - "name": "IllegalGenesisBlock", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidAggregateSignature", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidAuthoritiesProof", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidMmrProof", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidPointLength", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidSignerOrdering", - "inputs": [] - }, - { - "type": "error", - "name": "LeafIndexOutOfBounds", - "inputs": [] - }, - { - "type": "error", - "name": "MapToG1Failed", - "inputs": [] - }, - { - "type": "error", - "name": "MmrRootHashMissing", - "inputs": [] - }, - { - "type": "error", - "name": "ModExpFailed", - "inputs": [] - }, - { - "type": "error", - "name": "NoSigners", - "inputs": [] - }, - { - "type": "error", - "name": "OutOfBoundsLeaves", - "inputs": [] - }, - { - "type": "error", - "name": "PairingFailed", - "inputs": [] - }, - { - "type": "error", - "name": "ProofExhausted", - "inputs": [] - }, - { - "type": "error", - "name": "SuperMajorityRequired", - "inputs": [] - }, - { - "type": "error", - "name": "TimestampNotFound", - "inputs": [] - }, - { - "type": "error", - "name": "UnconsumedProof", - "inputs": [] - }, - { - "type": "error", - "name": "UnknownAuthoritySet", - "inputs": [] - }, - { - "type": "error", - "name": "UnsortedLeaves", - "inputs": [] - }, - { - "type": "error", - "name": "UnsortedLeaves", - "inputs": [] - } -] diff --git a/evm/rust/src/conversions.rs b/evm/rust/src/conversions.rs index 6b4045a5f..312c96201 100644 --- a/evm/rust/src/conversions.rs +++ b/evm/rust/src/conversions.rs @@ -56,11 +56,9 @@ mod beefy { use alloc::{vec, vec::Vec}; use alloy_primitives::{Bytes, FixedBytes}; use beefy_verifier_primitives::{ - compress_g1, compress_g2, BlsConsensusMessage, BlsMmrProof, BlsSigner, ConsensusMessage, - ConsensusState, MmrProof, ParachainHeader as BvpParachainHeader, + ConsensusMessage, ConsensusState, MmrProof, ParachainHeader as BvpParachainHeader, ParachainProof as BvpParachainProof, SignatureWithAuthorityIndex, SignedCommitment as BvpSignedCommitment, Sp1BeefyProof, TSignature, - BLS_G1_UNCOMPRESSED_LEN, BLS_G2_UNCOMPRESSED_LEN, }; use polkadot_sdk::*; use primitive_types::H256; @@ -395,190 +393,6 @@ mod beefy { } } - // `sol!` emits a distinct set of Rust types per binding, so the shared BEEFY structs appear - // again under `BlsBeefy` even though the Solidity definitions are the same ones. These bridge - // those duplicates onto the `Beefy` types so the conversions to the SCALE primitives stay - // single-sourced above, rather than being written out a second time. - // - // `BlsBeefy.sol` cannot simply reuse the `EcdsaBeefy` artifact: that contract is deployed, and - // adding the BLS structs to its `noOp` would change its ABI and bytecode. - mod bls_bridge { - use super::*; - use crate::bls_beefy::BlsBeefy; - - impl From for Payload { - fn from(value: BlsBeefy::Payload) -> Self { - Payload { id: value.id, data: value.data } - } - } - - impl From for Commitment { - fn from(value: BlsBeefy::Commitment) -> Self { - Commitment { - payload: value.payload.into_iter().map(Into::into).collect(), - blockNumber: value.blockNumber, - validatorSetId: value.validatorSetId, - } - } - } - - impl From for AuthoritySetCommitment { - fn from(value: BlsBeefy::AuthoritySetCommitment) -> Self { - AuthoritySetCommitment { id: value.id, len: value.len, root: value.root } - } - } - - impl From for BeefyMmrLeaf { - fn from(value: BlsBeefy::BeefyMmrLeaf) -> Self { - BeefyMmrLeaf { - version: value.version, - parentNumber: value.parentNumber, - parentHash: value.parentHash, - nextAuthoritySet: value.nextAuthoritySet.into(), - extra: value.extra, - leafIndex: value.leafIndex, - } - } - } - - impl From for Parachain { - fn from(value: BlsBeefy::Parachain) -> Self { - Parachain { index: value.index, id: value.id, header: value.header } - } - } - - impl From for ParachainProof { - fn from(value: BlsBeefy::ParachainProof) -> Self { - ParachainProof { - parachains: value.parachains.into_iter().map(Into::into).collect(), - proof: value.proof, - leafCount: value.leafCount, - } - } - } - - // The same bridges the other way, so a prover can build the payload it submits. - impl From for BlsBeefy::Payload { - fn from(value: Payload) -> Self { - BlsBeefy::Payload { id: value.id, data: value.data } - } - } - - impl From for BlsBeefy::Commitment { - fn from(value: Commitment) -> Self { - BlsBeefy::Commitment { - payload: value.payload.into_iter().map(Into::into).collect(), - blockNumber: value.blockNumber, - validatorSetId: value.validatorSetId, - } - } - } - - impl From for BlsBeefy::AuthoritySetCommitment { - fn from(value: AuthoritySetCommitment) -> Self { - BlsBeefy::AuthoritySetCommitment { id: value.id, len: value.len, root: value.root } - } - } - - impl From for BlsBeefy::BeefyMmrLeaf { - fn from(value: BeefyMmrLeaf) -> Self { - BlsBeefy::BeefyMmrLeaf { - version: value.version, - parentNumber: value.parentNumber, - parentHash: value.parentHash, - nextAuthoritySet: value.nextAuthoritySet.into(), - extra: value.extra, - leafIndex: value.leafIndex, - } - } - } - - impl From for BlsBeefy::Parachain { - fn from(value: Parachain) -> Self { - BlsBeefy::Parachain { index: value.index, id: value.id, header: value.header } - } - } - - impl From for BlsBeefy::ParachainProof { - fn from(value: ParachainProof) -> Self { - BlsBeefy::ParachainProof { - parachains: value.parachains.into_iter().map(Into::into).collect(), - proof: value.proof, - leafCount: value.leafCount, - } - } - } - } - - impl From for BlsMmrProof { - fn from(value: crate::bls_beefy::BlsBeefy::BlsRelayChainProof) -> Self { - let leaf: BeefyMmrLeaf = value.latestMmrLeaf.into(); - let leaf_index: u64 = leaf.leafIndex.try_into().expect("mmr leaf index out of bounds"); - let mmr_proof = LeafProof { - leaf_indices: vec![leaf_index], - leaf_count: leaf_index.saturating_add(1), - items: value.mmrProof.into_iter().map(|h| H256(h.0)).collect(), - }; - - // The ABI proof carries uncompressed points, because that is what EIP-2537 accepts. - // The verifier and the keyset commitment both work on the compressed encoding, so - // compress on the way in. That direction is pure byte manipulation; the reverse needs - // an Fp2 square root. - let signers = value - .signers - .into_iter() - .map(|signer| { - let uncompressed: [u8; BLS_G2_UNCOMPRESSED_LEN] = signer - .publicKey - .as_ref() - .try_into() - .expect("BLS public key should be 256 bytes uncompressed"); - BlsSigner { - public_key: compress_g2(&uncompressed), - index: signer - .authorityIndex - .try_into() - .expect("authority index out of bounds"), - } - }) - .collect(); - - let commitment: Commitment = value.commitment.into(); - - BlsMmrProof { - bls_commitment: H256(value.blsCommitment.0), - keyset_proof: value.keysetProof.into_iter().map(|h| h.0).collect(), - commitment: commitment.into(), - signers, - aggregate_signature: { - let uncompressed: [u8; BLS_G1_UNCOMPRESSED_LEN] = value - .aggregateSignature - .as_ref() - .try_into() - .expect("aggregate BLS signature should be 128 bytes uncompressed"); - compress_g1(&uncompressed) - }, - latest_mmr_leaf: leaf.into(), - mmr_proof, - authority_proof: value.proof.into_iter().map(|h| h.0).collect(), - } - } - } - - // The SCALE -> ABI direction lives in `beefy-prover`, not here. Building an EVM-bound proof - // means decompressing the points, which needs arkworks, and this crate compiles into the - // runtime. See `beefy_prover::bls::to_abi_proof`. - - impl From for BlsConsensusMessage { - fn from(value: crate::bls_beefy::BlsBeefy::BlsBeefyConsensusProof) -> Self { - // Two hops: the duplicate `BlsBeefy` struct bridges onto the `Beefy` one, which - // already knows how to become the SCALE primitive. `into()` will not chain these. - let parachain: ParachainProof = value.parachain.into(); - - BlsConsensusMessage { mmr: value.relay.into(), parachain: parachain.into() } - } - } - impl From for Sp1BeefyProof { fn from(value: crate::sp1_beefy::SP1Beefy::SP1BeefyProof) -> Self { Sp1BeefyProof { diff --git a/evm/rust/src/generated/bls_beefy.rs b/evm/rust/src/generated/bls_beefy.rs deleted file mode 100644 index a229c5047..000000000 --- a/evm/rust/src/generated/bls_beefy.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Aggregate BLS BEEFY contract bindings generated with alloy sol! macro. -//! -//! See [`crate::generated::ecdsa_beefy`] for why the two `sol!` invocations are picked between -//! with `#[cfg]` rather than `cfg_attr`. - -use alloy_sol_macro::sol; - -#[cfg(feature = "std")] -sol!( - #[allow(missing_docs)] - #[sol(rpc, ignore_unlinked)] - #[derive(Debug, PartialEq, Eq)] - BlsBeefy, - "abi/BlsBeefy.json" -); - -#[cfg(not(feature = "std"))] -sol!( - #[allow(missing_docs)] - #[sol(ignore_unlinked)] - #[derive(Debug, PartialEq, Eq)] - BlsBeefy, - "abi/BlsBeefy.json" -); diff --git a/evm/rust/src/generated/mod.rs b/evm/rust/src/generated/mod.rs index eeec93e82..0f21bfb2c 100644 --- a/evm/rust/src/generated/mod.rs +++ b/evm/rust/src/generated/mod.rs @@ -10,7 +10,7 @@ //! which is what substrate pallets consume. pub mod bandwidth_manager; -pub mod bls_beefy; +pub mod bls_apk_beefy; pub mod ecdsa_beefy; pub mod erc20; pub mod evm_host; diff --git a/evm/src/consensus/BlsAggregate.sol b/evm/src/consensus/BlsAggregate.sol index dacf6874b..dcb6ce422 100644 --- a/evm/src/consensus/BlsAggregate.sol +++ b/evm/src/consensus/BlsAggregate.sol @@ -38,9 +38,8 @@ import {BlsHashToCurve} from "./BlsHashToCurve.sol"; * Signatures live in G1 and public keys in G2, the opposite of the Ethereum convention. * * Points are taken **uncompressed**. EIP-2537 has no decompression precompile, and recovering a - * G2 point from its compressed form needs an Fp2 square root, which is expensive in Solidity. The - * prover therefore supplies uncompressed coordinates. Compressing them again is cheap, so - * [`compressG2`] derives the form the relay chain commits to when a merkle leaf is needed. + * G2 point from its compressed form needs an Fp2 square root, which is expensive in Solidity, so + * the prover supplies uncompressed coordinates. */ library BlsAggregate { /// @dev EIP-2537 BLS12_G2ADD @@ -66,16 +65,6 @@ library BlsAggregate { hex"00000000000000000000000000000000" hex"13fa4d4a0ad8b1ce186ed5061789213d993923066dddaf1040bc3ff59f825c78df74f2d75467e25e0f55f8a00fa030ed"; - /// @dev `(p - 1) / 2`. A compressed point records which of the two square roots `y` is, and - /// the convention is "the larger one", meaning `y > (p - 1) / 2`. - bytes internal constant HALF_MODULUS = - hex"0d0088f51cbff34d258dd3db21a5d66bb23ba5c279c2895fb39869507b587b120f55ffff58a9ffffdcff7fffffffd555"; - - /// @dev Set on the first byte of a compressed point. - uint8 internal constant COMPRESSION_FLAG = 0x80; - /// @dev Set when `y` is the larger of the two roots. - uint8 internal constant SIGN_FLAG = 0x20; - error G2AddFailed(); error PairingFailed(); error InvalidPointLength(); @@ -125,43 +114,4 @@ library BlsAggregate { return acc; } - - /** - * @notice Compress an uncompressed G2 point to the 96 byte form the relay chain commits to. - * - * @dev The keyset commitment is built over compressed keys, but EIP-2537 only accepts - * uncompressed ones, so the prover sends uncompressed and this derives the compressed form for - * the merkle leaf. Going this direction is cheap: take `x` and set two flag bits. The reverse - * would need an Fp2 square root, which is why the proof does not simply carry compressed keys. - * - * The layout is `x.c1 || x.c0`, c1 first, with the flags in the top bits of the first byte. - * The sign bit tracks `y.c1`, confirmed against `bls_compression_rule` in the Rust verifier. - */ - function compressG2(bytes memory point) internal pure returns (bytes memory) { - if (point.length != G2_POINT_LEN) revert InvalidPointLength(); - - bytes memory out = new bytes(96); - for (uint256 i = 0; i < 48; ++i) { - // x.c1 occupies bytes 64..128, its value in the trailing 48; x.c0 is bytes 0..64. - out[i] = point[80 + i]; - out[48 + i] = point[16 + i]; - } - - uint8 flags = COMPRESSION_FLAG; - // y.c1 occupies bytes 192..256, its value in the trailing 48. - if (greaterThanHalfModulus(point, 208)) flags |= SIGN_FLAG; - out[0] = bytes1(uint8(out[0]) | flags); - - return out; - } - - /// @dev Whether the 48 byte big-endian value at `offset` exceeds `(p - 1) / 2`. - function greaterThanHalfModulus(bytes memory value, uint256 offset) internal pure returns (bool) { - for (uint256 i = 0; i < 48; ++i) { - uint8 a = uint8(value[offset + i]); - uint8 b = uint8(HALF_MODULUS[i]); - if (a != b) return a > b; - } - return false; - } } diff --git a/evm/src/consensus/BlsApkBeefy.sol b/evm/src/consensus/BlsApkBeefy.sol index 114f14769..f24109197 100644 --- a/evm/src/consensus/BlsApkBeefy.sol +++ b/evm/src/consensus/BlsApkBeefy.sol @@ -24,6 +24,7 @@ import {ScaleCodec} from "@polytope-labs/solidity-merkle-trees/src/trie/polkadot import {Codec} from "./Codec.sol"; import { ApkAuthoritySet, + BlsApkBeefyConsensusProof, BlsApkConsensusState, BlsApkRelayChainProof, BeefyMmrLeaf, @@ -54,10 +55,9 @@ interface IApkProof { * @title BEEFY consensus verified by an aggregate public key proof * @notice Verifies BEEFY finality without touching individual validator keys. * - * @dev The merkle client in `BlsBeefy.sol` proves each signer's public key against the authority - * set's keyset root, so its cost grows with the number of signers: roughly 17k gas each, for the - * G2 addition and the compression needed to rebuild the leaf. At a few hundred validators that - * dominates everything else. + * @dev Naming each signer and proving their public key against the authority set's keyset root + * costs roughly 17k gas per signer, for the G2 addition and the compression needed to rebuild the + * leaf. At a few hundred validators that dominates everything else. * * Here a SNARK does that work instead. The prover shows that an aggregate public key corresponds * to exactly the validators named in a bitlist, against a commitment to the whole set, and the @@ -70,7 +70,7 @@ interface IApkProof { * and carries it in its consensus state. That is what `ApkAuthoritySet.apkCommitment` holds, and * why the state has to be seeded with the starting set's commitment at initialisation. * - * Requires Prague for the EIP-2537 precompiles, same as the merkle client. + * Requires Prague for the EIP-2537 precompiles. */ contract BlsApkBeefy is IConsensusV2, ERC165 { /// The payload id for the mmr root in a BEEFY commitment, "mh" @@ -112,7 +112,7 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { (BlsApkRelayChainProof memory relay, ParachainProof memory parachain) = abi.decode(proof, (BlsApkRelayChainProof, ParachainProof)); - // Replays are idempotent rather than reverting, matching the merkle client. + // Replays are idempotent rather than reverting, matching the ecdsa client. if (consensusState.latestHeight >= relay.commitment.blockNumber) { return (abi.encode(consensusState), new IntermediateState[](0), consensusState.nextAuthoritySet.id); } @@ -226,7 +226,7 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { } } - /// @dev Two thirds plus one, matching the merkle client and substrate's own rule. + /// @dev Two thirds plus one, matching the ecdsa client and substrate's own rule. function checkParticipationThreshold(uint256 signed, uint256 total) internal pure returns (bool) { return total > 0 && signed * 3 > total * 2; } @@ -301,4 +301,8 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { function leafIndex(uint256 activationBlock, uint256 parentNumber) internal pure returns (uint256) { return activationBlock == 0 ? parentNumber : parentNumber - activationBlock; } + + /// @dev Only here so the structs appear in the ABI, which is what the Rust bindings are + /// generated from. `verify` takes bytes, so without this they would be invisible. + function noOp(BlsApkConsensusState memory s, BlsApkBeefyConsensusProof memory p) external pure {} } diff --git a/evm/src/consensus/BlsBeefy.sol b/evm/src/consensus/BlsBeefy.sol deleted file mode 100644 index 949de3b5c..000000000 --- a/evm/src/consensus/BlsBeefy.sol +++ /dev/null @@ -1,319 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) Polytope Labs Ltd. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.30; - -import {IConsensusV2, IntermediateState, StateCommitment} from "@hyperbridge/core/interfaces/IConsensusV2.sol"; -import {MerkleMultiProof} from "@polytope-labs/solidity-merkle-trees/src/MerkleMultiProof.sol"; -import {MerkleMountainRange} from "@polytope-labs/solidity-merkle-trees/src/MerkleMountainRange.sol"; -import {ScaleCodec} from "@polytope-labs/solidity-merkle-trees/src/trie/polkadot/ScaleCodec.sol"; -import {Bytes} from "@polytope-labs/solidity-merkle-trees/src/trie/Bytes.sol"; -import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; - -import {Codec} from "./Codec.sol"; -import { - Header, - HeaderImpl, - AuthoritySetCommitment, - BlsBeefyConsensusProof, - BlsRelayChainProof, - BlsSigner, - Commitment, - BeefyConsensusState, - PartialBeefyMmrLeaf, - Parachain, - ParachainProof -} from "./Types.sol"; -import {BlsAggregate} from "./BlsAggregate.sol"; - -/** - * @title The aggregate BLS12-381 BEEFY consensus client. - * @author Polytope Labs (hello@polytope.technology) - * - * @notice Verifies BEEFY finality by checking one aggregate BLS signature rather than recovering - * each authority's ECDSA signature. The signature check is then flat in the size of the validator - * set, where [`EcdsaBeefy`] pays one `ecrecover` per signature. Everything else, MMR leaf - * inclusion and parachain header proofs, is the same work as the ECDSA client does. - * - * @dev The verification flow is: - * 1. Confirm the commitment's validator set id matches a known authority set. - * 2. Confirm enough validators signed to meet the supermajority threshold. - * 3. Confirm the signers' public keys are in the authority set via a merkle multi-proof, and that - * no signer is claimed twice. - * 4. Verify the aggregate signature in one pairing check. - * 5. Extract the MMR root from the commitment payload and verify the latest MMR leaf inclusion. - * 6. Verify parachain header inclusion in the leaf's parachain heads root and decode the - * finalized state commitments. - * - * Stale proofs are a no-op, matching the ECDSA client, so replays are idempotent. - * - * Two encoding notes that are easy to get wrong: - * - Public keys are taken **uncompressed**, because EIP-2537 has no decompression precompile. - * The keyset commitment is still over the compressed encoding, as the runtime builds it; the - * contract compresses each key to compute its merkle leaf, which is cheap in that direction. - * - BEEFY puts signatures in G1 and public keys in G2, the opposite of the Ethereum convention, - * so an eth2 BLS library does not transfer. - * - * Requires an EVM with EIP-2537, so Prague or later. - */ -contract BlsBeefy is IConsensusV2, ERC165 { - using HeaderImpl for Header; - - /// @dev The PayloadId for the mmr root. - bytes2 public constant MMR_ROOT_PAYLOAD_ID = bytes2("mh"); - - /// @dev Provided authority set id was unknown. - error UnknownAuthoritySet(); - /// @dev Mmr root hash was not found in the commitment payload. - error MmrRootHashMissing(); - /// @dev Provided Mmr proof was invalid. - error InvalidMmrProof(); - /// @dev Genesis block should not be provided. - error IllegalGenesisBlock(); - /// @dev Supermajority not reached. - error SuperMajorityRequired(); - /// @dev Provided authorities proof was invalid. - error InvalidAuthoritiesProof(); - /// @dev Signer indices must strictly ascend and lie inside the authority set. - error InvalidSignerOrdering(); - /// @dev The aggregate pairing check rejected the signature. - error InvalidAggregateSignature(); - - /** - * @dev See {IERC165-supportsInterface}. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - return interfaceId == type(IConsensusV2).interfaceId || super.supportsInterface(interfaceId); - } - - /// @dev IConsensusV2 entry point. Decodes the proof, verifies consensus, and returns the - /// updated state along with the latest authority set id. - function verify(bytes calldata previousState, bytes calldata proof) - external - view - returns (bytes memory, IntermediateState[] memory, uint256) - { - BeefyConsensusState memory consensusState = abi.decode(previousState, (BeefyConsensusState)); - (BlsRelayChainProof memory relay, ParachainProof memory parachain) = - abi.decode(proof, (BlsRelayChainProof, ParachainProof)); - - // Stale proofs are a no-op: return the previous state with no intermediates so the caller - // can treat replays as idempotent rather than having to guard against reverts. - if (consensusState.latestHeight >= relay.commitment.blockNumber) { - return (abi.encode(consensusState), new IntermediateState[](0), consensusState.nextAuthoritySet.id); - } - - (BeefyConsensusState memory newState, bytes32 headsRoot) = verifyMmrUpdateProof(consensusState, relay); - IntermediateState[] memory intermediates = verifyParachainHeaderProof(headsRoot, parachain); - - return (abi.encode(newState), intermediates, newState.nextAuthoritySet.id); - } - - /** - * @dev Verifies a new mmr root update. The relay chain accumulates its blocks into a merkle - * mountain range, and the new root is signed by the authority set. Here that signature is a - * single aggregate rather than one per validator, so establishing which validators signed is - * a merkle multi-proof of their public keys plus one pairing check. - */ - function verifyMmrUpdateProof(BeefyConsensusState memory trustedState, BlsRelayChainProof memory relayProof) - internal - view - returns (BeefyConsensusState memory, bytes32) - { - Commitment memory commitment = relayProof.commitment; - if ( - commitment.validatorSetId != trustedState.currentAuthoritySet.id - && commitment.validatorSetId != trustedState.nextAuthoritySet.id - ) { - revert UnknownAuthoritySet(); - } - - bool isCurrentAuthorities = commitment.validatorSetId == trustedState.currentAuthoritySet.id; - AuthoritySetCommitment memory authoritySet = - isCurrentAuthorities ? trustedState.currentAuthoritySet : trustedState.nextAuthoritySet; - - verifyAuthorities( - Codec.Encode(commitment), - relayProof.signers, - relayProof.aggregateSignature, - authoritySet.root, - relayProof.blsCommitment, - relayProof.keysetProof, - relayProof.proof, - authoritySet.len - ); - - uint256 payloadLength = commitment.payload.length; - bytes32 mmrRoot; - for (uint256 i = 0; i < payloadLength; i++) { - if (commitment.payload[i].id == MMR_ROOT_PAYLOAD_ID && commitment.payload[i].data.length == 32) { - mmrRoot = Bytes.toBytes32(commitment.payload[i].data); - } - } - if (mmrRoot == bytes32(0)) revert MmrRootHashMissing(); - - verifyMmrLeaf(trustedState, relayProof, mmrRoot); - - if (relayProof.latestMmrLeaf.nextAuthoritySet.id > trustedState.nextAuthoritySet.id) { - trustedState.currentAuthoritySet = trustedState.nextAuthoritySet; - trustedState.nextAuthoritySet = relayProof.latestMmrLeaf.nextAuthoritySet; - } - trustedState.latestHeight = commitment.blockNumber; - - return (trustedState, relayProof.latestMmrLeaf.extra); - } - - /** - * @notice Establish that a supermajority of the committed authority set signed `commitment`. - * @param commitment the SCALE-encoded BEEFY commitment that was signed - * @param signers the validators claiming to have signed, in strictly ascending index order - * @param aggregateSignature the summed G1 signature, 128 bytes uncompressed - * @param keysetRoot the authority set's keyset commitment - * @param authorityProof merkle multi-proof of the signers' keys against `keysetRoot` - * @param authorityCount total validators in the set - */ - function verifyAuthorities( - bytes memory commitment, - BlsSigner[] memory signers, - bytes memory aggregateSignature, - bytes32 keysetRoot, - bytes32 blsCommitment, - bytes32[] memory keysetProof, - bytes32[] memory authorityProof, - uint256 authorityCount - ) public view { - uint256 count = signers.length; - if (!checkParticipationThreshold(count, authorityCount)) revert SuperMajorityRequired(); - - // Strictly ascending and inside the set. Without this a prover could repeat one signer to - // clear the threshold, and the aggregate would happily verify the repeated key against the - // repeated signature. - for (uint256 i = 0; i < count; ++i) { - if (signers[i].authorityIndex >= authorityCount) revert InvalidSignerOrdering(); - if (i > 0 && signers[i].authorityIndex <= signers[i - 1].authorityIndex) { - revert InvalidSignerOrdering(); - } - } - - // The pairing check only proves the holders of these keys signed. Proving those keys are - // the authority set's is what the multi-proof is for. - MerkleMultiProof.Leaf[] memory leaves = new MerkleMultiProof.Leaf[](count); - bytes[] memory publicKeys = new bytes[](count); - for (uint256 i = 0; i < count; ++i) { - publicKeys[i] = signers[i].publicKey; - // The runtime commits the compressed encoding, so derive it here rather than asking - // the prover to send both forms, which would then need checking for consistency. - leaves[i] = MerkleMultiProof.Leaf({ - index: signers[i].authorityIndex, hash: keccak256(BlsAggregate.compressG2(signers[i].publicKey)) - }); - } - - // Two levels. The relay chain commits the BLS keys as one extra leaf of the authority set - // tree, so the per-authority leaves keep their positions and bridges verifying ECDSA - // signatures still prove against the same root. Establish that leaf is the authority set's, - // then prove the signers against it. - // - // The keyset tree therefore holds authorityCount + 1 leaves, with the BLS commitment last, - // while the threshold above still judges against authorityCount. - MerkleMultiProof.Leaf[] memory keysetLeaf = new MerkleMultiProof.Leaf[](1); - keysetLeaf[0] = - MerkleMultiProof.Leaf({index: authorityCount, hash: keccak256(abi.encodePacked(blsCommitment))}); - - if (!MerkleMultiProof.VerifyProof(keysetRoot, keysetProof, keysetLeaf, authorityCount + 1)) { - revert InvalidAuthoritiesProof(); - } - - if (!MerkleMultiProof.VerifyProof(blsCommitment, authorityProof, leaves, authorityCount)) { - revert InvalidAuthoritiesProof(); - } - - if (!BlsAggregate.verify(commitment, aggregateSignature, publicKeys)) { - revert InvalidAggregateSignature(); - } - } - - // @dev Stack too deep, sigh solidity - function verifyMmrLeaf(BeefyConsensusState memory trustedState, BlsRelayChainProof memory relay, bytes32 mmrRoot) - internal - pure - { - bytes32 hash = keccak256( - Codec.Encode( - PartialBeefyMmrLeaf({ - version: relay.latestMmrLeaf.version, - parentNumber: relay.latestMmrLeaf.parentNumber, - parentHash: relay.latestMmrLeaf.parentHash, - nextAuthoritySet: relay.latestMmrLeaf.nextAuthoritySet, - extra: relay.latestMmrLeaf.extra - }) - ) - ); - uint256 leafCount = leafIndex(trustedState.beefyActivationBlock, relay.latestMmrLeaf.parentNumber) + 1; - MerkleMountainRange.Leaf[] memory leaves = new MerkleMountainRange.Leaf[](1); - leaves[0] = MerkleMountainRange.Leaf({index: relay.latestMmrLeaf.leafIndex, hash: hash}); - bool valid = MerkleMountainRange.VerifyProof(mmrRoot, relay.mmrProof, leaves, leafCount); - - if (!valid) revert InvalidMmrProof(); - } - - // @dev Verifies that some parachain header has been finalized, given the current trusted state. - function verifyParachainHeaderProof(bytes32 headsRoot, ParachainProof memory proof) - internal - pure - returns (IntermediateState[] memory) - { - uint256 len = proof.parachains.length; - MerkleMultiProof.Leaf[] memory leaves = new MerkleMultiProof.Leaf[](len); - IntermediateState[] memory intermediates = new IntermediateState[](len); - - for (uint256 i = 0; i < len; i++) { - Parachain memory para = proof.parachains[i]; - Header memory header = Codec.DecodeHeader(para.header); - if (header.number == 0) revert IllegalGenesisBlock(); - - leaves[i] = MerkleMultiProof.Leaf( - para.index, - keccak256(bytes.concat(ScaleCodec.encode32(uint32(para.id)), ScaleCodec.encodeBytes(para.header))) - ); - - StateCommitment memory commitment = header.stateCommitment(); - intermediates[i] = - IntermediateState({stateMachineId: para.id, height: header.number, commitment: commitment}); - } - - if (len > 0) { - bool valid = MerkleMultiProof.VerifyProof(headsRoot, proof.proof, leaves, proof.leafCount); - if (!valid) revert InvalidMmrProof(); - } - - return intermediates; - } - - // @dev Calculates the mmr leaf index for a block whose parent number is given. - function leafIndex(uint256 activationBlock, uint256 parentNumber) internal pure returns (uint256) { - if (activationBlock == 0) { - return parentNumber; - } else { - return parentNumber - activationBlock; - } - } - - /// @notice The BEEFY supermajority: more than two thirds of the set. - function checkParticipationThreshold(uint256 len, uint256 total) public pure returns (bool) { - return len >= ((2 * total) / 3) + 1; - } - - // @dev so these structs are included in the abi - function noOp(BeefyConsensusState memory s, BlsBeefyConsensusProof memory p) external pure {} -} diff --git a/evm/src/consensus/Types.sol b/evm/src/consensus/Types.sol index 93fa7bad8..30f37e257 100644 --- a/evm/src/consensus/Types.sol +++ b/evm/src/consensus/Types.sol @@ -173,52 +173,6 @@ struct BeefyConsensusProof { ParachainProof parachain; } -// A validator that contributed to an aggregate BLS12-381 signature. -// -// Only the public key travels. The individual signatures are summed by the prover into -// BlsRelayChainProof.aggregateSignature, since verification never needs them apart. -struct BlsSigner { - // Compressed BLS12-381 G2 public key, 96 bytes. Note BEEFY puts public keys in G2 and - // signatures in G1, the opposite of the Ethereum convention. - bytes publicKey; - // 0-based index of the authority in the authority set - uint256 authorityIndex; -} - -struct BlsRelayChainProof { - // A commitment to the finalized state - Commitment commitment; - // The validators that signed, in strictly ascending index order - BlsSigner[] signers; - // Sum of the signers' signatures as a compressed BLS12-381 G1 point, 48 bytes - bytes aggregateSignature; - // Latest leaf added to mmr - BeefyMmrLeaf latestMmrLeaf; - // Proof for the latest mmr leaf - bytes32[] mmrProof; - // Root of the tree over the authorities' BLS public keys. The relay chain commits this as one - // extra leaf of the authority set tree, so it is proven rather than trusted. - bytes32 blsCommitment; - // Proof that blsCommitment is the authority set's extra leaf, against the keyset commitment. - // That tree holds len + 1 leaves: the authorities, then this one. - bytes32[] keysetProof; - // Proof for the signing authorities against blsCommitment - bytes32[] proof; -} - -// A BEEFY consensus proof verified by a single aggregate BLS signature rather than by recovering -// each authority's ECDSA signature. -// -// This requires a relay chain whose keyset commitment is over BLS public keys. A chain committing -// ECDSA-derived addresses cannot be verified this way, and vice versa, so the two are separate -// consensus states. -struct BlsBeefyConsensusProof { - // The proof items for the relay chain consensus - BlsRelayChainProof relay; - // Proof items for parachain headers - ParachainProof parachain; -} - // An authority set identified by its APK commitment rather than by a merkle root over keys. // // The commitment is Poseidon2 over the validators' BLS12-381 G1 keys, padded to the circuit's diff --git a/evm/tests/foundry/BlsAggregate.t.sol b/evm/tests/foundry/BlsAggregate.t.sol index 5c83118f4..62128ef22 100644 --- a/evm/tests/foundry/BlsAggregate.t.sol +++ b/evm/tests/foundry/BlsAggregate.t.sol @@ -3,8 +3,6 @@ pragma solidity ^0.8.30; import {Test} from "forge-std/Test.sol"; import {BlsAggregate} from "../../src/consensus/BlsAggregate.sol"; -import {BlsBeefy} from "../../src/consensus/BlsBeefy.sol"; -import {BlsSigner} from "../../src/consensus/Types.sol"; /** * @title Cross-language check of the aggregate BLS pairing. @@ -107,80 +105,4 @@ contract BlsAggregateTest is Test { "aggregate must not verify against a subset of the signers" ); } - - /// Root of the authority set tree: four per-authority leaves, then the BLS commitment. - bytes32 constant KEYSET_ROOT = 0x9097854fdde72a6cae144165afd1b881ff201a20acbbbef836f9cdf45a1d85a9; - /// Root of the tree over the four authorities' compressed BLS keys, the extra leaf's value. - bytes32 constant BLS_COMMITMENT = 0xd220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a; - /// Opens the BLS commitment as leaf 4 of the five-leaf authority set tree. - bytes32 constant KEYSET_PROOF_NODE = 0x9ca0bbf4e382871c43e750de6df39704e2604f75b83534dfa710289745b331f7; - /// Opens signers 0,1,2 against the BLS commitment. - bytes32 constant PROOF_NODE = 0x97e418e070e3ec967bccc1d0aaea6d787a7b68733451c5320cd32eec2be63df8; - - function _blsSigners() private pure returns (BlsSigner[] memory out) { - bytes[] memory keys = _signers(); - out = new BlsSigner[](3); - for (uint256 i = 0; i < 3; ++i) { - out[i] = BlsSigner({publicKey: keys[i], authorityIndex: i}); - } - } - - function _authorityProof() private pure returns (bytes32[] memory nodes) { - nodes = new bytes32[](1); - nodes[0] = PROOF_NODE; - } - - function _keysetProof() private pure returns (bytes32[] memory nodes) { - nodes = new bytes32[](1); - nodes[0] = KEYSET_PROOF_NODE; - } - - /// The authority half end to end: threshold, ordering, merkle membership, aggregate pairing. - /// Reverts on any failure, so reaching the end is the assertion. - function test_verify_authorities() public { - new BlsBeefy().verifyAuthorities(MESSAGE, _blsSigners(), _g1(SIG_X, SIG_Y), KEYSET_ROOT, BLS_COMMITMENT, _keysetProof(), _authorityProof(), 4); - } - - /// Repeating a signer must be rejected before any cryptography runs. - function test_rejects_duplicate_signer_index() public { - BlsSigner[] memory signers = _blsSigners(); - signers[2].authorityIndex = 1; - - BlsBeefy client = new BlsBeefy(); - vm.expectRevert(BlsBeefy.InvalidSignerOrdering.selector); - client.verifyAuthorities(MESSAGE, signers, _g1(SIG_X, SIG_Y), KEYSET_ROOT, BLS_COMMITMENT, _keysetProof(), _authorityProof(), 4); - } - - /// Two of four is short of the supermajority. - function test_rejects_sub_supermajority() public { - BlsSigner[] memory all = _blsSigners(); - BlsSigner[] memory two = new BlsSigner[](2); - two[0] = all[0]; - two[1] = all[1]; - - BlsBeefy client = new BlsBeefy(); - vm.expectRevert(BlsBeefy.SuperMajorityRequired.selector); - client.verifyAuthorities(MESSAGE, two, _g1(SIG_X, SIG_Y), KEYSET_ROOT, BLS_COMMITMENT, _keysetProof(), _authorityProof(), 4); - } - - /// Compression must reproduce exactly what `w3f-bls` serialises, or the merkle leaves will not - /// match the commitment. Signer 0 has the sign bit set (0xb4), signer 2 does not (0x80), so - /// both branches of the rule are covered. - function test_compress_matches_w3f_bls() public pure { - bytes[] memory keys = _signers(); - - assertEq( - BlsAggregate.compressG2(keys[0]), - hex"b4168206974b9223cc95e6e1f279f9d10e526aa172b5bd15b101b6f4997e2038ebcb02bfee1bca54f428162e17ade003" - hex"0eb912203efe065b9d9844025f9a85a43fdb21f4c8c0f31b39cc3bedcdbecce8ab3567f9cadbe965adebb7dd4a081ec1", - "signer 0 compression differs, sign bit set" - ); - - assertEq( - BlsAggregate.compressG2(keys[2]), - hex"808cd3cbf5e8dd6aca7d5fb78061c360910691c3797bcd95a42cf5a45b0612e8555f6e118788872af47d231954914282" - hex"0c2410d03233711f03a7242fd3a8bb141125ac84a07813d8cce6e366038f129d3ef348fc7dd14682e3db28a920d2c748", - "signer 2 compression differs, sign bit clear" - ); - } } diff --git a/evm/tests/foundry/BlsBeefy.t.sol b/evm/tests/foundry/BlsBeefy.t.sol deleted file mode 100644 index 401c92f98..000000000 --- a/evm/tests/foundry/BlsBeefy.t.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.30; - -import {Test} from "forge-std/Test.sol"; -import {IntermediateState} from "@hyperbridge/core/interfaces/IConsensusV2.sol"; - -import {BlsBeefy} from "../../src/consensus/BlsBeefy.sol"; -import {BeefyConsensusState} from "../../src/consensus/Types.sol"; - -/** - * @title The BLS BEEFY consensus client's entry point. - * - * @notice `state` and `proof` come from `bls_solidity_proof_fixture` in the Rust verifier. The - * validators there sign the SCALE encoding of the same commitment this contract hashes, over a - * single-leaf MMR whose root the commitment carries, so every check in `verify()` runs against - * data the Rust side produced rather than anything constructed to suit the contract. - * - * Needs EIP-2537: - * FOUNDRY_PROFILE=bls forge test --match-contract BlsBeefyTest -vv - */ -contract BlsBeefyTest is Test { - BlsBeefy internal client; - - function setUp() public { - client = new BlsBeefy(); - } - - function _state() internal view returns (bytes memory) { - return vm.parseBytes(vm.readFile("tests/foundry/fixtures/bls-beefy-state.hex")); - } - - function _proof() internal view returns (bytes memory) { - return vm.parseBytes(vm.readFile("tests/foundry/fixtures/bls-beefy-proof.hex")); - } - - /// The whole client: authority set, threshold, ordering, merkle membership, aggregate pairing, - /// MMR leaf inclusion, then the state advances. - function test_verify_advances_state() public view { - (bytes memory newStateBytes,, uint256 nextSetId) = client.verify(_state(), _proof()); - - BeefyConsensusState memory newState = abi.decode(newStateBytes, (BeefyConsensusState)); - BeefyConsensusState memory oldState = abi.decode(_state(), (BeefyConsensusState)); - - assertGt(newState.latestHeight, oldState.latestHeight, "height should advance"); - assertEq(newState.latestHeight, 100, "height should match the proven commitment"); - assertGt(nextSetId, 0, "should report the next authority set id"); - } - - /// Replaying a proof the state has already passed is a no-op rather than a revert, so callers - /// do not have to guard against it. - function test_stale_proof_is_a_noop() public view { - (bytes memory advanced,,) = client.verify(_state(), _proof()); - - (bytes memory again, IntermediateState[] memory intermediates,) = client.verify(advanced, _proof()); - - assertEq(again, advanced, "state should be unchanged"); - assertEq(intermediates.length, 0, "a stale proof finalizes nothing"); - } - - function _liveState() internal view returns (bytes memory) { - return vm.parseBytes(vm.readFile("tests/foundry/fixtures/bls-beefy-live-state.hex")); - } - - function _liveProof() internal view returns (bytes memory) { - return vm.parseBytes(vm.readFile("tests/foundry/fixtures/bls-beefy-live-proof.hex")); - } - - /// The real thing: a proof built by the prover from a running BLS relay, carrying a genuine - /// parachain header and an MMR proof with actual depth, rather than a synthetic single leaf. - /// The keys arrive uncompressed and the client compresses them to rebuild the merkle leaves, - /// so the whole wire format is exercised too. - function test_verify_live_proof() public view { - (bytes memory newStateBytes, IntermediateState[] memory intermediates,) = - client.verify(_liveState(), _liveProof()); - - BeefyConsensusState memory newState = abi.decode(newStateBytes, (BeefyConsensusState)); - BeefyConsensusState memory oldState = abi.decode(_liveState(), (BeefyConsensusState)); - - assertGt(newState.latestHeight, oldState.latestHeight, "height should advance"); - assertEq(intermediates.length, 1, "should finalize the registered parachain"); - assertEq(intermediates[0].stateMachineId, 4009, "should be para 4009"); - assertGt(intermediates[0].height, 0, "parachain height should be non-zero"); - } -} diff --git a/evm/tests/foundry/fixtures/bls-beefy-live-proof.hex b/evm/tests/foundry/fixtures/bls-beefy-live-proof.hex deleted file mode 100644 index 7b8b747b7..000000000 --- a/evm/tests/foundry/fixtures/bls-beefy-live-proof.hex +++ /dev/null @@ -1 +0,0 @@ -0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000062000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000063d0b8898784cc48cba5117faa7f985e7bb46fdd98ab53b8a1da12432d6ba9ebcf00000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002c6640e6634e02cc8a0ba3b8b32a3b4a956c8ac195b2c4ddb897aefc6b2fcfd558685ba548bdc68684d93d6d8a68833ab8872ef7c7bcadb2d1c93cf58d6ecf509000000000000000000000000000000000000000000000000000000000000006300000000000000000000000000000000000000000000000000000000000006c059354e6a156b4a39ffc56f03fa5b947e341c737964563c0fc16298aaeef002dc000000000000000000000000000000000000000000000000000000000000076000000000000000000000000000000000000000000000000000000000000007a0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000205268843b5a6524735cd4836d2b579c3b388307b0f46feb36edacdaf20e2514440000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000005223c1d776402ea1987eb80d8986897469773e64f60ac162da9bbd43fe64ae970bfbad858a466787d8712eda4fd59e8000000000000000000000000000000000d3987bf4729830439efa2577b0923d38f93f5cffccc059dfaa7779ff10756989b5121eb60c8773cd4a227669b1e430b00000000000000000000000000000000011f34f64d1aea99d50df61fa1c64e8a3708985ad07b2b18181eaa32c031f097005818a639a272ec26090af35aa59ddc0000000000000000000000000000000009940b83f82ccfed097bb7a4f60e93f002c8d726f905cee111aa2141a8e6f0b6bc5fdd8a461a4648dfc18a843366924600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000169a502b8c1834a867af71776a5d79a390dfc6d5db5eed75bc3b9140c54cf61ffd1ab3b15ca2c599d78124f900cf15ef000000000000000000000000000000001818d16b6726d50f1c88dd60266b7a17f717de8fe167db5786bf2dc2adc9bea57e7091b0ca47ef8b43a39425ec25328f00000000000000000000000000000000019fc5274bd3fc51719e140cb337d70c456468b7c5b73f404da5bea585b147a2fad7d570e879b2378033833fdd14bae1000000000000000000000000000000001833bfdffcf2febd0806514ac9d447e6de2ee170c7882f6bc90d70b033975aebd3f62bf8b8c9d0515db843c5ee55663b0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000321306435a1c68a4cb716ee1f9335b8fe62d5547d803d679138856e8c554af76295966a2203e1746dd29551065bc2920000000000000000000000000000000018772176c0d3efd6eea239dfc8dd190e9c9819de7bfe7de37f285ba8b4d8adf580630203023e2d3c041347790193fda10000000000000000000000000000000000000000000000000000000000000004085d4cd438536c251ca2cdb35cee62d811cb7b9a193e09239096659a67344a1d8d1dddccff3a0fdd09b88fa2e993290125c9952631d71a5b82997e1efcb62ece7ab1ef80f6c14308049a54303732e6330351ba0c96a7102d2888c3fed32193bc47f0bcbda552b9ddd35fa24b872452ec2d35e20db22277de46713e56017359f90000000000000000000000000000000000000000000000000000000000000001697ea2a8fe5b03468548a7a413424a6292ab44a82a6f5cc594c3fa7dda7ce40200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000134520a9f55f1813bdf8ee6036712ed26da650b0981bfaf1150b537d953e56a3f6a2831a86712dc2bea37104898af1f7106e690d2b4d958881afcb51b09f53226be16a2045c0928ca4abb05a33405710fdd0827e5850ff2c463cf866ff86433444e9014066175726120191bbe11000000000452505352884497dbb3ec883e67c9ca651b5a07aa8375afbacf4aa15d2adc4ff07871adaec57d010449534d5001010000000000000000000000000000000000000000000000000000000000000000bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a044953544d2096a2746a0000000005617572610101ae945eb0ee501e720d6fdb273c066ffc83726e235090532ac97f1f7d72bf3b766739f7b6e3f424e94476bc052b3ccece09dbfb626a4054dc0f2895b1610ddb8e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-beefy-live-state.hex b/evm/tests/foundry/fixtures/bls-beefy-live-state.hex deleted file mode 100644 index fd133f530..000000000 --- a/evm/tests/foundry/fixtures/bls-beefy-live-state.hex +++ /dev/null @@ -1 +0,0 @@ -0x0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002c6640e6634e02cc8a0ba3b8b32a3b4a956c8ac195b2c4ddb897aefc6b2fcfd5500000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000002c6640e6634e02cc8a0ba3b8b32a3b4a956c8ac195b2c4ddb897aefc6b2fcfd55 \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-beefy-proof.hex b/evm/tests/foundry/fixtures/bls-beefy-proof.hex deleted file mode 100644 index 418a6b2a9..000000000 --- a/evm/tests/foundry/fixtures/bls-beefy-proof.hex +++ /dev/null @@ -1 +0,0 @@ -0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000092000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000007a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000049097854fdde72a6cae144165afd1b881ff201a20acbbbef836f9cdf45a1d85a9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000840d220f3b093a9c3cb95b44e1413e438eb0184b9fe9337591ef13680c44e678a2a000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000008a0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000204a4f59869e34638bb79baedbcc7bc096fd33300839f0cd4f3066a0ca753290b20000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000eb912203efe065b9d9844025f9a85a43fdb21f4c8c0f31b39cc3bedcdbecce8ab3567f9cadbe965adebb7dd4a081ec10000000000000000000000000000000014168206974b9223cc95e6e1f279f9d10e526aa172b5bd15b101b6f4997e2038ebcb02bfee1bca54f428162e17ade003000000000000000000000000000000000dbe0ed3b59dbf3c217e879f885df4fce29af686888e77e984b69d6a07fe90b2a1acebc49d6a196c90a9307be82bb9c40000000000000000000000000000000016bd04776624eab548fc58aeac9da7f75a618206620c9119d517b983b659ca196c2a0761e09a1ee6df87e1cd78bdd4d500000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000065a060f78222114141a2544d6207ebfe7784788e6310b3a58cebc36df948e387853ce8492c4c8f9d423ebabc6feb02700000000000000000000000000000000026bc8c3c41fb3bb78a7d97855098d8b32ec546433c4c523f62c471b75d89823e2485ff79f3c203961b62c9b8b08e4d1000000000000000000000000000000000db39f4b44160911c089c2cf24621f3e1df13561ced1640ddafa4c885d0a80b3428156c709f375e00fdb230b5c109e460000000000000000000000000000000000b7e2a03248e77666c8e12d4eaa31d451c477209a178134afd53ebcc701842206b2b7613ed4807f4600ce212c6679e8000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000c2410d03233711f03a7242fd3a8bb141125ac84a07813d8cce6e366038f129d3ef348fc7dd14682e3db28a920d2c74800000000000000000000000000000000008cd3cbf5e8dd6aca7d5fb78061c360910691c3797bcd95a42cf5a45b0612e8555f6e118788872af47d2319549142820000000000000000000000000000000015c168dc4a702011de9bded446897040c88e51831293bbcc691fe85a7f42e6cb454d4931fe9348fa310f455a21ef47ae0000000000000000000000000000000008600a717fc096a40eeb7dba5194779267123c9fef22e3ebe85c34c1aa74928a89c211d9bfa7dc97a296fc7550acc58e000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000025926c99ee9df0dd30a417a604794fa50629fc87e3d81290b7ad57a753e089987b174a25512e80df6cc412ef59239c80000000000000000000000000000000014c25b4c14965131b589b802ed9e8c36200b0bbaf05c192b5dfbb5f85bc828f1505f458c30e08776eb10e44d546b9b06000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019ca0bbf4e382871c43e750de6df39704e2604f75b83534dfa710289745b331f7000000000000000000000000000000000000000000000000000000000000000197e418e070e3ec967bccc1d0aaea6d787a7b68733451c5320cd32eec2be63df800000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-beefy-state.hex b/evm/tests/foundry/fixtures/bls-beefy-state.hex deleted file mode 100644 index 8f019f909..000000000 --- a/evm/tests/foundry/fixtures/bls-beefy-state.hex +++ /dev/null @@ -1 +0,0 @@ -0x00000000000000000000000000000000000000000000000000000000000000630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000049097854fdde72a6cae144165afd1b881ff201a20acbbbef836f9cdf45a1d85a9000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000049097854fdde72a6cae144165afd1b881ff201a20acbbbef836f9cdf45a1d85a9 \ No newline at end of file diff --git a/modules/consensus/beefy/primitives/src/lib.rs b/modules/consensus/beefy/primitives/src/lib.rs index 4beb72e86..e67ec558e 100644 --- a/modules/consensus/beefy/primitives/src/lib.rs +++ b/modules/consensus/beefy/primitives/src/lib.rs @@ -126,113 +126,30 @@ pub const PROOF_TYPE_NAIVE: u8 = 0x00; /// Proof type identifier for SP1 ZK proofs pub const PROOF_TYPE_SP1: u8 = 0x01; -/// Proof type identifier for aggregate BLS12-381 proofs -pub const PROOF_TYPE_BLS: u8 = 0x02; - /// Size of a compressed BLS12-381 G1 point, the group BEEFY signatures live in. pub const BLS_G1_SIGNATURE_LEN: usize = 48; /// Size of a compressed BLS12-381 G2 point, the group BEEFY public keys live in. pub const BLS_G2_PUBLIC_KEY_LEN: usize = 96; -/// Size of an uncompressed BLS12-381 G2 point as EIP-2537 encodes it: four 64 byte field -/// elements, `x.c0 || x.c1 || y.c0 || y.c1`. -pub const BLS_G2_UNCOMPRESSED_LEN: usize = 256; - -/// Size of an uncompressed BLS12-381 G1 point: `x || y`, each 64 bytes. -pub const BLS_G1_UNCOMPRESSED_LEN: usize = 128; - -/// `(p - 1) / 2` for the BLS12-381 base field, big-endian. -const HALF_MODULUS: [u8; 48] = [ - 0x0d, 0x00, 0x88, 0xf5, 0x1c, 0xbf, 0xf3, 0x4d, 0x25, 0x8d, 0xd3, 0xdb, 0x21, 0xa5, 0xd6, 0x6b, - 0xb2, 0x3b, 0xa5, 0xc2, 0x79, 0xc2, 0x89, 0x5f, 0xb3, 0x98, 0x69, 0x50, 0x7b, 0x58, 0x7b, 0x12, - 0x0f, 0x55, 0xff, 0xff, 0x58, 0xa9, 0xff, 0xff, 0xdc, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xd5, 0x55, -]; - -/// Compress an uncompressed G2 point into the 96 byte form the relay chain commits to. -/// -/// EIP-2537 only accepts uncompressed points, so an EVM-bound proof carries those, while the -/// keyset commitment and the Rust verifier both work on the compressed encoding. Converting this -/// direction is pure byte manipulation: take `x`, ordered `c1` then `c0`, and set the compression -/// flag plus a sign bit recording which square root `y` is. The reverse would need an Fp2 square -/// root, which is why proofs never travel compressed to the EVM. -pub fn compress_g2(point: &[u8; BLS_G2_UNCOMPRESSED_LEN]) -> [u8; BLS_G2_PUBLIC_KEY_LEN] { - let mut out = [0u8; BLS_G2_PUBLIC_KEY_LEN]; - // Each 64 byte field element carries its 48 byte value in the trailing bytes. - out[..48].copy_from_slice(&point[80..128]); // x.c1 - out[48..].copy_from_slice(&point[16..64]); // x.c0 - - let y_c1 = &point[208..256]; - let sign = y_c1 > &HALF_MODULUS[..]; - - out[0] |= 0x80; // compression flag - if sign { - out[0] |= 0x20; - } - - out -} - -/// Compress an uncompressed G1 point into the 48 byte form `w3f-bls` serialises. +/// The relay chain half of a BLS BEEFY update: the signed commitment, the aggregate signature, and +/// the MMR leaf it attests to. /// -/// Same convention as [`compress_g2`], with a single field element instead of a pair. -pub fn compress_g1(point: &[u8; BLS_G1_UNCOMPRESSED_LEN]) -> [u8; BLS_G1_SIGNATURE_LEN] { - let mut out = [0u8; BLS_G1_SIGNATURE_LEN]; - out.copy_from_slice(&point[16..64]); // x - - let y = &point[80..128]; - let sign = y > &HALF_MODULUS[..]; - - out[0] |= 0x80; - if sign { - out[0] |= 0x20; - } - - out -} - -/// A validator that contributed to an aggregate BLS signature. -/// -/// Only the public key is carried. The individual signatures are summed by the prover into -/// [`BlsMmrProof::aggregate_signature`], since verification never needs them apart. -#[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode)] -pub struct BlsSigner { - /// Compressed G2 public key, as committed to by the relay chain's keyset commitment. - pub public_key: [u8; BLS_G2_PUBLIC_KEY_LEN], - /// 0-based index of the authority in the authority set - pub index: u32, -} - -/// An MMR root update proven by an aggregate BLS12-381 signature rather than by recovering each -/// authority's ECDSA signature individually. -/// -/// The verifier checks this in one pairing operation regardless of how many validators signed, -/// which is the whole point of the BLS path. The tradeoff is that the signers' public keys travel -/// with the proof, so its size grows with the number of signers. +/// Which validators signed, and the proof that their aggregate key is the authority set's, are not +/// here. Those come from the APK proof, which is built and checked outside this crate. #[derive(sp_std::fmt::Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct BlsMmrProof { /// The commitment that was signed pub commitment: sp_consensus_beefy::Commitment, - /// The validators that signed, with the public keys the aggregate was formed over - pub signers: Vec, /// Sum of the signers' G1 signatures, as a compressed G1 point pub aggregate_signature: [u8; BLS_G1_SIGNATURE_LEN], /// Latest leaf added to mmr pub latest_mmr_leaf: MmrLeaf, /// Proof for the latest mmr leaf pub mmr_proof: sp_mmr_primitives::LeafProof, - /// Root of the tree over the authorities' BLS public keys. The relay chain commits this as one - /// extra leaf of the authority set tree, so it is proven rather than trusted. - pub bls_commitment: H256, - /// Flat proof hashes proving [`Self::bls_commitment`] is the authority set's extra leaf, - /// against the keyset commitment. That tree holds `len + 1` leaves: the authorities, then - /// this one. - pub keyset_proof: Vec<[u8; 32]>, - /// Flat proof hashes proving the signers' public keys against [`Self::bls_commitment`] - pub authority_proof: Vec<[u8; 32]>, } -/// A BEEFY consensus update proven by an aggregate BLS signature. +/// A BEEFY consensus update signed with aggregate BLS12-381. #[derive(sp_std::fmt::Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct BlsConsensusMessage { /// Parachain headers diff --git a/modules/consensus/beefy/prover/src/bls.rs b/modules/consensus/beefy/prover/src/bls.rs index 54694c399..76d1bedbf 100644 --- a/modules/consensus/beefy/prover/src/bls.rs +++ b/modules/consensus/beefy/prover/src/bls.rs @@ -26,14 +26,12 @@ use anyhow::anyhow; use codec::{Decode, Encode}; use polkadot_sdk::*; -use primitive_types::H256; use sp_consensus_beefy::{SignedCommitment, VersionedFinalityProof}; -use sp_io::hashing::keccak_256; use subxt::{backend::legacy::LegacyRpcMethods, Config}; use subxt_core::config::HashFor; use beefy_verifier_primitives::{ - BlsConsensusMessage, BlsMmrProof, BlsSigner, BLS_G1_SIGNATURE_LEN, BLS_G2_PUBLIC_KEY_LEN, + BlsConsensusMessage, BlsMmrProof, BLS_G1_SIGNATURE_LEN, BLS_G2_PUBLIC_KEY_LEN, }; use crate::{ @@ -161,101 +159,35 @@ pub fn aggregate_signatures( .map_err(|_| anyhow!("Aggregated signature was not {BLS_G1_SIGNATURE_LEN} bytes")) } -/// The ECDSA halves of the paired authority keys, SCALE-encoded as the address converter expects. -/// -/// These are the leaves the ECDSA path proves against, and the BLS commitment is appended after -/// them, so the prover has to rebuild them to open a path to that extra leaf. -async fn ecdsa_halves( - rpc: &LegacyRpcMethods, - at: Option>, -) -> Result>, anyhow::Error> { - let data = rpc - .state_get_storage(BEEFY_AUTHORITIES.as_slice(), at) - .await? - .ok_or_else(|| anyhow!("No beefy authorities found!"))?; - - let paired = Vec::<[u8; PAIRED_LEN]>::decode(&mut data.as_ref())?; - - Ok(paired - .into_iter() - .map(|key| { - let mut ecdsa = [0u8; 33]; - ecdsa.copy_from_slice(&key[..33]); - ecdsa.encode() - }) - .collect()) -} - impl Prover { - /// Build a consensus proof whose commitment is proven by one aggregate BLS signature. + /// Collect the relay chain half of a BLS BEEFY update: the signed commitment, the aggregate + /// signature, the MMR leaf and its proof, and the parachain headers. /// - /// The signers' G2 public keys travel with the proof, since the verifier needs them both to - /// form the aggregate key and to prove membership of the authority set. That makes the proof - /// grow with the number of signers, which is the cost of not needing a SNARK. + /// Which validators signed is left to the caller, which reads it from the justification's + /// bitfield and proves it with an APK proof. Nothing here grows with the number of signers. pub async fn bls_consensus_proof( &self, signed_commitment: SignedCommitment, ) -> Result { let block_number: u32 = signed_commitment.commitment.block_number; - let block_hash = self - .relay_rpc - .chain_get_block_hash(Some(block_number.into())) - .await? - .ok_or_else(|| anyhow!("Failed to query blockhash for blocknumber"))?; - let (mmr_proof, latest_leaf) = fetch_mmr_proof(&self.relay_rpc, block_number, self.query_batch_size).await?; - let authorities = beefy_g2_authorities(&self.relay_rpc, Some(block_hash)).await?; - - // Signers in authority-set order, which is what the merkle multi-proof expects and what - // the verifier enforces. - let mut signers = Vec::new(); - let mut g1_signatures = Vec::new(); - for (index, maybe_signature) in signed_commitment.signatures.iter().enumerate() { - let Some(signature) = maybe_signature else { continue }; - let public_key = *authorities - .get(index) - .ok_or_else(|| anyhow!("Signature index {index} outside the authority set"))?; - - signers.push(BlsSigner { public_key, index: index as u32 }); - g1_signatures.push(signature.g1_signature()); - } - + // Only the signatures are needed here. The keys they belong to are the APK proof's + // business, and reading them is the caller's. + let g1_signatures = signed_commitment + .signatures + .iter() + .flatten() + .map(|signature| signature.g1_signature()) + .collect::>(); let aggregate_signature = aggregate_signatures(&g1_signatures)?; - // Two trees, mirroring how the relay chain commits them. The BLS keys have their own tree, - // and its root sits as one extra leaf of the authority set tree alongside the Ethereum - // address leaves. So the proof carries a path to that leaf, and a path to the signers - // within it. - let bls_leaves = authorities.iter().map(|key| keccak_256(key)).collect::>(); - let indices = signers.iter().map(|signer| signer.index as usize).collect::>(); - let bls_tree = rs_merkle::MerkleTree::::from_leaves(&bls_leaves); - let bls_commitment = - H256(bls_tree.root().ok_or_else(|| anyhow!("empty BLS authority key tree"))?); - let authority_proof = bls_tree.proof(&indices).proof_hashes().to_vec(); - - // The authority set tree: the Ethereum address leaves the ECDSA path uses, then the BLS - // commitment. Its leaf count is one more than the validator count. - let ecdsa_authorities = crate::util::hash_authority_addresses( - ecdsa_halves(&self.relay_rpc, Some(block_hash)).await?, - )?; - let mut keyset_leaves = ecdsa_authorities; - keyset_leaves.push(keccak_256(bls_commitment.as_bytes())); - let bls_leaf_index = keyset_leaves.len() - 1; - let keyset_tree = - rs_merkle::MerkleTree::::from_leaves(&keyset_leaves); - let keyset_proof = keyset_tree.proof(&[bls_leaf_index]).proof_hashes().to_vec(); - let mmr = BlsMmrProof { commitment: signed_commitment.commitment.clone(), - signers, aggregate_signature, latest_mmr_leaf: latest_leaf.clone(), mmr_proof, - bls_commitment, - keyset_proof, - authority_proof, }; let heads = paras_parachains( @@ -269,133 +201,3 @@ impl Prover { Ok(BlsConsensusMessage { mmr, parachain }) } } - -/// Building an EVM-bound proof. -/// -/// The SCALE proof carries compressed points, which is what the Rust verifier and the keyset -/// commitment work on. EIP-2537 accepts only uncompressed ones, so an EVM submission has to -/// decompress. That needs curve arithmetic, which is why it lives here rather than in `ismp-abi`, -/// a crate that compiles into the runtime. The contract compresses again to rebuild the merkle -/// leaves, which is cheap in that direction. -pub mod abi { - use anyhow::anyhow; - use ark_bls12_381::{G1Affine, G2Affine}; - use ark_ec::AffineRepr; - use ark_ff::{BigInteger, PrimeField}; - use ark_serialize::CanonicalDeserialize; - use beefy_verifier_primitives::{ - BlsConsensusMessage, BLS_G1_SIGNATURE_LEN, BLS_G1_UNCOMPRESSED_LEN, BLS_G2_PUBLIC_KEY_LEN, - BLS_G2_UNCOMPRESSED_LEN, - }; - use ismp_abi::bls_beefy::BlsBeefy; - - /// Expand a compressed G2 public key into the EIP-2537 encoding: four 64 byte field elements, - /// each a 48 byte big-endian value with 16 bytes of leading zeroes. - pub fn decompress_g2( - compressed: &[u8; BLS_G2_PUBLIC_KEY_LEN], - ) -> Result<[u8; BLS_G2_UNCOMPRESSED_LEN], anyhow::Error> { - let point = G2Affine::deserialize_compressed(&compressed[..]) - .map_err(|e| anyhow!("invalid compressed G2 point: {e:?}"))?; - let (x, y) = point.xy().ok_or_else(|| anyhow!("G2 point is the identity"))?; - - let mut out = [0u8; BLS_G2_UNCOMPRESSED_LEN]; - for (slot, coord) in [&x.c0, &x.c1, &y.c0, &y.c1].iter().enumerate() { - let bytes = coord.into_bigint().to_bytes_be(); - out[slot * 64 + 16..slot * 64 + 64].copy_from_slice(&bytes); - } - - Ok(out) - } - - /// Expand a compressed G1 signature into the EIP-2537 encoding. - pub fn decompress_g1( - compressed: &[u8; BLS_G1_SIGNATURE_LEN], - ) -> Result<[u8; BLS_G1_UNCOMPRESSED_LEN], anyhow::Error> { - let point = G1Affine::deserialize_compressed(&compressed[..]) - .map_err(|e| anyhow!("invalid compressed G1 point: {e:?}"))?; - let (x, y) = point.xy().ok_or_else(|| anyhow!("G1 point is the identity"))?; - - let mut out = [0u8; BLS_G1_UNCOMPRESSED_LEN]; - for (slot, coord) in [x, y].iter().enumerate() { - let bytes = coord.into_bigint().to_bytes_be(); - out[slot * 64 + 16..slot * 64 + 64].copy_from_slice(&bytes); - } - - Ok(out) - } - - /// Convert a consensus proof into the ABI shape the Solidity client consumes. - pub fn to_abi_proof( - message: BlsConsensusMessage, - ) -> Result { - use alloy_primitives::{Bytes, FixedBytes, U256}; - - let mmr = message.mmr; - let leaf_index = mmr.mmr_proof.leaf_indices.first().copied().unwrap_or_default(); - - let signers = mmr - .signers - .iter() - .map(|signer| { - Ok(BlsBeefy::BlsSigner { - publicKey: Bytes::from(decompress_g2(&signer.public_key)?.to_vec()), - authorityIndex: U256::from(signer.index), - }) - }) - .collect::, anyhow::Error>>()?; - - let relay = BlsBeefy::BlsRelayChainProof { - commitment: BlsBeefy::Commitment { - payload: vec![BlsBeefy::Payload { - id: FixedBytes(*b"mh"), - data: Bytes::from( - mmr.commitment - .payload - .get_raw(b"mh") - .ok_or_else(|| anyhow!("mmr payload not present"))? - .clone(), - ), - }], - blockNumber: mmr.commitment.block_number, - validatorSetId: mmr.commitment.validator_set_id, - }, - signers, - aggregateSignature: Bytes::from(decompress_g1(&mmr.aggregate_signature)?.to_vec()), - latestMmrLeaf: BlsBeefy::BeefyMmrLeaf { - version: 0, - parentNumber: mmr.latest_mmr_leaf.parent_number_and_hash.0, - parentHash: FixedBytes(mmr.latest_mmr_leaf.parent_number_and_hash.1 .0), - nextAuthoritySet: BlsBeefy::AuthoritySetCommitment { - id: mmr.latest_mmr_leaf.beefy_next_authority_set.id, - len: mmr.latest_mmr_leaf.beefy_next_authority_set.len, - root: FixedBytes( - mmr.latest_mmr_leaf.beefy_next_authority_set.keyset_commitment.0, - ), - }, - extra: FixedBytes(mmr.latest_mmr_leaf.leaf_extra.0), - leafIndex: U256::from(leaf_index), - }, - mmrProof: mmr.mmr_proof.items.iter().map(|h| FixedBytes(h.0)).collect(), - blsCommitment: FixedBytes(mmr.bls_commitment.0), - keysetProof: mmr.keyset_proof.iter().map(|h| FixedBytes(*h)).collect(), - proof: mmr.authority_proof.iter().map(|h| FixedBytes(*h)).collect(), - }; - - let parachain = BlsBeefy::ParachainProof { - parachains: message - .parachain - .parachains - .iter() - .map(|para| BlsBeefy::Parachain { - index: U256::from(para.index), - id: U256::from(para.para_id), - header: Bytes::from(para.header.clone()), - }) - .collect(), - proof: message.parachain.proof.iter().map(|h| FixedBytes(*h)).collect(), - leafCount: U256::from(message.parachain.total_leaves), - }; - - Ok(BlsBeefy::BlsBeefyConsensusProof { relay, parachain }) - } -} diff --git a/modules/consensus/beefy/verifier/Cargo.toml b/modules/consensus/beefy/verifier/Cargo.toml index 38e8eeb6c..c2c0fb242 100644 --- a/modules/consensus/beefy/verifier/Cargo.toml +++ b/modules/consensus/beefy/verifier/Cargo.toml @@ -21,7 +21,6 @@ rs_merkle = { workspace = true, default-features = false } thiserror = { workspace = true } sp1-verifier = { git = "https://github.com/polytope-labs/sp1.git", branch = "polytope-labs/v6.1.0-wasm-compatible", default-features = false } alloy-sol-types = { workspace = true, default-features = false } -w3f-bls = { version = "0.1.9", default-features = false, optional = true } [dependencies.polkadot-sdk] workspace = true @@ -37,6 +36,10 @@ hex-literal = { workspace = true } hex = { workspace = true, default-features = true } beefy-prover = { workspace = true } ismp-abi = { workspace = true, default-features = true } +# The APK fixture is built in two halves: this crate collects the live BEEFY data, the SNARK is +# generated by `gnark-apk-proofs` out of tree, and the two meet over a json file. +json = { workspace = true, default-features = true } +apk-commitment = { workspace = true, default-features = true } alloy-primitives = { workspace = true, default-features = true } subxt = { workspace = true, default-features = true } subxt-core = { workspace = true, default-features = true } @@ -61,9 +64,9 @@ features = ["sp-io"] [features] default = ["std"] -# The `w3f-bls` backed implementation of the aggregate BLS check, in `crate::bls`. A runtime that -# verifies BLS through host functions implements `BlsAggregateVerify` itself and leaves this off. -bls-crypto = ["dep:w3f-bls"] +# Tests that need `w3f-bls` itself: the hash-to-curve vector and the aggregate checks the APK path +# is built on. Nothing in the library depends on it. +bls-crypto = [] # Runs the BLS-BEEFY tests against a relay whose BEEFY authorities are paired ecdsa_bls_crypto # keys. Pulls the prover's `bls` decode path, which reads 177-byte paired signatures and keys. bls = ["beefy-prover/bls", "beefy-prover/bls-aggregate", "bls-crypto"] @@ -83,5 +86,4 @@ std = [ "rs_merkle/std", "sp1-verifier/std", "alloy-sol-types/std", - "w3f-bls?/std", ] \ No newline at end of file diff --git a/modules/consensus/beefy/verifier/src/bls.rs b/modules/consensus/beefy/verifier/src/bls.rs deleted file mode 100644 index cabf39b4d..000000000 --- a/modules/consensus/beefy/verifier/src/bls.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (C) Polytope Labs Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! A `w3f-bls` backed implementation of the aggregate BLS check, matching the scheme substrate's -//! BEEFY signs with. -//! -//! A host wires this up by implementing [`crate::BlsAggregateVerify`] and delegating to -//! [`aggregate_verify`]. Runtimes should prefer an implementation backed by host functions, since -//! a BLS12-381 pairing executed inside wasm is considerably more expensive than the `ecrecover` -//! calls it replaces. The saving of the BLS path is that there is one pairing regardless of how -//! many validators signed, not that each individual operation is cheap. - -use beefy_verifier_primitives::{BLS_G1_SIGNATURE_LEN, BLS_G2_PUBLIC_KEY_LEN}; -use w3f_bls::{EngineBLS, Message, PublicKey, SerializableToBytes, Signature, TinyBLS381}; - -/// Verify that `signature` is the sum of the signatures over `message` produced by the holders of -/// `public_keys`, in a single pairing check. -/// -/// The message is hashed onto the signature curve exactly as `w3f-bls` does when signing, with an -/// empty context, matching sp-core's `bls381::Pair::sign`. -/// -/// Errors when a point fails to decode; returns `Ok(false)` when the points are well formed but -/// the aggregate does not verify. -pub fn aggregate_verify( - message: &[u8], - signature: &[u8; BLS_G1_SIGNATURE_LEN], - public_keys: &[[u8; BLS_G2_PUBLIC_KEY_LEN]], -) -> anyhow::Result { - if public_keys.is_empty() { - return Ok(false); - } - - let signature = Signature::::from_bytes(signature) - .map_err(|_| anyhow::anyhow!("invalid G1 signature encoding"))?; - - let mut aggregate: Option<::PublicKeyGroup> = None; - for key in public_keys { - let public_key = PublicKey::::from_bytes(key) - .map_err(|_| anyhow::anyhow!("invalid G2 public key encoding"))?; - aggregate = Some(aggregate.map_or(public_key.0, |sum| sum + public_key.0)); - } - - let aggregate = aggregate.expect("public_keys is non-empty, checked above; qed"); - - Ok(signature.verify(&Message::new(b"", message), &PublicKey::(aggregate))) -} diff --git a/modules/consensus/beefy/verifier/src/error.rs b/modules/consensus/beefy/verifier/src/error.rs index 13cd4d594..2e5a60feb 100644 --- a/modules/consensus/beefy/verifier/src/error.rs +++ b/modules/consensus/beefy/verifier/src/error.rs @@ -73,24 +73,6 @@ pub enum Error { /// The SP1 Groth16 verifier rejected the proof bytes. #[error("SP1 proof verification failed")] Sp1VerificationFailed, - /// A G1 signature or G2 public key in a BLS proof is not a valid curve point. - #[error("Invalid BLS point encoding")] - InvalidBlsPoint, - /// The aggregate BLS pairing check rejected the signature. - #[error("Aggregate BLS signature verification failed")] - BlsVerificationFailed, - /// A BLS proof carried no signers, so there is no aggregate to verify. - #[error("BLS proof contains no signers")] - NoBlsSigners, - /// A BLS proof's signer indices are not strictly ascending, or address an authority outside - /// the set. Repeating an index would let a prover count one validator many times towards the - /// supermajority threshold and aggregate its key more than once. - #[error("BLS signer indices must be strictly ascending and within the authority set")] - InvalidBlsSignerOrdering, - /// The BLS proof payload failed to SCALE-decode. - #[error("Cannot decode BLS proof: {0}")] - DecodeBlsProof(String), - // -- ismp-beefy client wrapper -- /// The trusted state failed to SCALE-decode into a `ConsensusState`. #[error("Cannot decode consensus state: {0}")] diff --git a/modules/consensus/beefy/verifier/src/lib.rs b/modules/consensus/beefy/verifier/src/lib.rs index 5d7c6ce13..73b47cabc 100644 --- a/modules/consensus/beefy/verifier/src/lib.rs +++ b/modules/consensus/beefy/verifier/src/lib.rs @@ -23,8 +23,6 @@ extern crate alloc; -#[cfg(feature = "bls-crypto")] -pub mod bls; pub mod error; pub mod sp1; #[cfg(test)] @@ -35,7 +33,6 @@ use core::marker::PhantomData; use crate::error::Error; use beefy_verifier_primitives::{ - BLS_G1_SIGNATURE_LEN, BLS_G2_PUBLIC_KEY_LEN, BlsConsensusMessage, BlsMmrProof, ConsensusMessage, ConsensusState, MmrProof, ParachainHeader, ParachainProof, }; use codec::Encode; @@ -62,30 +59,6 @@ pub trait EcdsaRecover { fn secp256k1_recover(prehash: &[u8; 32], signature: &[u8; 65]) -> anyhow::Result<[u8; 64]>; } -/// A trait for verifying an aggregate BLS12-381 signature under BEEFY's signing scheme. -/// -/// BEEFY's BLS half uses `w3f-bls`'s "double" scheme over `TinyBLS381`, in which signatures are G1 -/// points and public keys are G2 points. Note this is the opposite of the Ethereum convention, so -/// implementations written against an eth2 BLS library will not transfer. The signed message is the -/// SCALE-encoded commitment, hashed to a G1 point with an empty context. -/// -/// This is a trait, rather than a direct call into a BLS library, for the same reason -/// [`EcdsaRecover`] is: a runtime can route the pairing through host functions instead of paying to -/// execute it in wasm. Implementations must perform the hash-to-curve themselves so that step can -/// be host-accelerated too. -pub trait BlsAggregateVerify { - /// Check that `signature` is the sum of the signatures over `message` produced by the holders - /// of `public_keys`, using a single pairing check. - /// - /// Returns `Ok(false)` when the points are well formed but the signature does not verify, and - /// an error when a point cannot be decoded. - fn verify_aggregate( - message: &[u8], - signature: &[u8; BLS_G1_SIGNATURE_LEN], - public_keys: &[[u8; BLS_G2_PUBLIC_KEY_LEN]], - ) -> anyhow::Result; -} - /// A hasher implementation for rs_merkle, generic over the hash function pub struct MerkleHasher(PhantomData); @@ -128,17 +101,6 @@ pub fn verify_consensus( Ok((state.encode(), verified_headers)) } -/// Verify a BEEFY consensus proof whose commitment was signed with aggregate BLS12-381, returning -/// the new trusted consensus state and the verified parachain headers. -pub fn verify_bls_consensus( - trusted_state: ConsensusState, - proof: BlsConsensusMessage, -) -> Result<(Vec, Vec), Error> { - let (state, heads_root) = verify_bls_mmr_update_proof::(trusted_state, proof.mmr)?; - let verified_headers = verify_parachain_headers::(heads_root, proof.parachain)?; - Ok((state.encode(), verified_headers)) -} - /// Verifies a new Mmr root update, the relay chain accumulates it's blocks into a merkle mountain /// range tree which light clients can use as a source for log_2(n) ancestry proofs. This new mmr /// root hash is signed by the relay chain authority set and we can verify the membership of the @@ -190,91 +152,6 @@ pub fn verify_mmr_update_proof( Ok((state, mmr.latest_mmr_leaf.leaf_extra)) } -/// Verifies an MMR root update whose authorities signed with BLS12-381 rather than ECDSA. -/// -/// Where the ECDSA path recovers a public key per signature, this sums the signers' keys and -/// checks one aggregate signature in a single pairing operation, so the cost of the signature -/// check no longer grows with the size of the validator set. The signers are named explicitly in -/// the proof and proven to be authorities by the same merkle multi-proof the ECDSA path uses, -/// against a keyset commitment that must be over BLS public keys rather than Ethereum addresses. -/// -/// Note that a chain whose keyset commitment holds Ethereum addresses cannot be verified through -/// this path, and vice versa. The two are separate consensus states. -pub fn verify_bls_mmr_update_proof( - trusted_state: ConsensusState, - mmr: BlsMmrProof, -) -> Result<(ConsensusState, H256), Error> { - if mmr.signers.is_empty() { - return Err(Error::NoBlsSigners); - } - - let preamble = prepare_update(&trusted_state, &mmr.commitment, mmr.signers.len() as u32)?; - - // Strictly ascending indices, so a signer cannot be counted towards the threshold or summed - // into the aggregate more than once, and so the multi-proof sees them in the order it wants. - let within_set = mmr - .signers - .last() - .map(|last| last.index < preamble.authority_count) - .unwrap_or(false); - let ascending = mmr.signers.windows(2).all(|pair| pair[0].index < pair[1].index); - if !ascending || !within_set { - return Err(Error::InvalidBlsSignerOrdering); - } - - // The BLS half signs the SCALE-encoded commitment itself, where the ECDSA half signs its - // keccak hash. Hashing it onto the curve is the implementation's job, so that step can be - // host-accelerated alongside the pairing. - let public_keys = mmr.signers.iter().map(|signer| signer.public_key).collect::>(); - let verified = - H::verify_aggregate(&mmr.commitment.encode(), &mmr.aggregate_signature, &public_keys) - .map_err(|_| Error::InvalidBlsPoint)?; - - if !verified { - return Err(Error::BlsVerificationFailed); - } - - // The pairing check only proves that the holders of *these* keys signed. Proving those keys - // are the authority set's is what the merkle multi-proof is for. - let authority_leaves = mmr - .signers - .iter() - .map(|signer| H::keccak256(&signer.public_key).into()) - .collect::>(); - let authority_indices = - mmr.signers.iter().map(|signer| signer.index as usize).collect::>(); - - // Two levels. The relay chain commits the BLS keys as one extra leaf of the authority set - // tree, so that the per-authority leaves keep their positions and bridges verifying ECDSA - // signatures still prove against the same root. First establish that leaf really is the - // authority set's, then prove the signers against it. - // - // The keyset tree therefore holds `authority_count + 1` leaves, with the BLS commitment last, - // while the threshold above still judges against `authority_count`. - verify_authority_membership::( - preamble.keyset_commitment, - &mmr.keyset_proof, - &[preamble.authority_count as usize], - &[H::keccak256(mmr.bls_commitment.as_bytes()).into()], - preamble.authority_count.saturating_add(1), - )?; - - verify_authority_membership::( - mmr.bls_commitment, - &mmr.authority_proof, - &authority_indices, - &authority_leaves, - preamble.authority_count, - )?; - - verify_mmr_leaf::(&mmr.latest_mmr_leaf, &mmr.mmr_proof, preamble.mmr_root)?; - - let latest_height = mmr.commitment.block_number; - let state = apply_update(trusted_state, &mmr.latest_mmr_leaf, latest_height); - - Ok((state, mmr.latest_mmr_leaf.leaf_extra)) -} - /// The parts of an update that hold regardless of how the commitment was signed. struct UpdatePreamble { /// Commitment to the authority set the signers must belong to. diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index 3fe84d05f..16f6b79a6 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -38,8 +38,6 @@ use polkadot_sdk::sp_consensus_beefy::{ }; use sp_mmr_primitives::LeafProof; -#[cfg(feature = "bls")] -use crate::verify_bls_consensus; use crate::{EcdsaRecover, error::Error, verify_consensus, verify_mmr_update_proof}; struct TestHost; @@ -57,17 +55,6 @@ impl EcdsaRecover for TestHost { } } -#[cfg(feature = "bls-crypto")] -impl crate::BlsAggregateVerify for TestHost { - fn verify_aggregate( - message: &[u8], - signature: &[u8; beefy_verifier_primitives::BLS_G1_SIGNATURE_LEN], - public_keys: &[[u8; beefy_verifier_primitives::BLS_G2_PUBLIC_KEY_LEN]], - ) -> anyhow::Result { - crate::bls::aggregate_verify(message, signature, public_keys) - } -} - // Integration test: hits live Polkadot/parachain RPCs (see RELAY_WS_URL / PARA_WS_URL env vars). // Run explicitly with `cargo test -- --ignored`. #[tokio::test] @@ -706,149 +693,6 @@ async fn test_bls_aggregate_verify() { ); } -/// Phase 1 (trustless): the full aggregate-BLS verification flow a light client would run. -/// -/// Unlike `test_bls_aggregate_verify` (which trusts the keys read from storage), this proves the -/// signing keys against the on-chain keyset commitment. The runtime's `BeefyBls381G2ToKeysetLeaf` -/// converter commits `keccak(g2_pubkey)` leaves, so the flow is: -/// 1. build the merkle tree of all validators' G2 keys and check its root equals the on-chain -/// `keyset_commitment` (confirms the runtime commits G2 keys as expected), -/// 2. check the signer count meets the >2/3 threshold, -/// 3. prove the signers' keys are committed (merkle multi-proof at the signer indices), -/// 4. aggregate the signers' G2 keys and G1 signatures and verify one pairing check. -/// -/// RELAY_WS_URL=ws://127.0.0.1:9977 \ -/// cargo test -p beefy-verifier --features bls test_bls_trustless_verify -- --ignored -/// --nocapture -#[cfg(feature = "bls")] -#[tokio::test] -#[ignore] -async fn test_bls_trustless_verify() { - use beefy_prover::rs_merkle::MerkleProof; - use w3f_bls::{Message, PublicKey, SerializableToBytes, Signature, TinyBLS381}; - - #[derive(Clone)] - struct Sig177([u8; 177]); - impl codec::Decode for Sig177 { - fn decode(input: &mut I) -> Result { - let mut bytes = [0u8; 177]; - input.read(&mut bytes)?; - Ok(Sig177(bytes)) - } - } - - let max_rpc_payload_size = 15 * 1024 * 1024; - let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); - let (_relay_client, relay_rpc_client) = - subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) - .await - .unwrap(); - let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); - let engine_id = polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID; - - // Latest justification -> commitment + per-validator signature slots. - let latest: H256 = - relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await.unwrap(); - let latest_block = relay_rpc.chain_get_block(Some(latest.into())).await.unwrap().unwrap(); - let latest_just = latest_block - .justifications - .expect("justifications") - .into_iter() - .find_map(|j| (j.0 == engine_id).then_some(j.1)) - .expect("beefy justification"); - let VersionedFinalityProof::V1(sc) = - VersionedFinalityProof::::decode(&mut &*latest_just).unwrap(); - let commitment_encoded = sc.commitment.encode(); - let message = Message::new(b"", &commitment_encoded); - - // All validators' G2 public keys, in authority-set order, from `Beefy.Authorities`. - let raw_auth = relay_rpc - .state_get_storage(beefy_prover::BEEFY_AUTHORITIES.as_slice(), Some(latest.into())) - .await - .unwrap() - .expect("beefy authorities"); - let paired = Vec::<[u8; 177]>::decode(&mut raw_auth.as_ref()).unwrap(); - let g2_keys: Vec<[u8; 96]> = paired - .iter() - .map(|k| { - let mut g2 = [0u8; 96]; - g2.copy_from_slice(&k[81..177]); - g2 - }) - .collect(); - let total = g2_keys.len(); - - // The on-chain keyset commitment (root of keccak(g2_pubkey) leaves) from the MmrLeaf pallet. - let raw_set = relay_rpc - .state_get_storage( - beefy_prover::BEEFY_MMR_LEAF_BEEFY_AUTHORITIES.as_slice(), - Some(latest.into()), - ) - .await - .unwrap() - .expect("mmr leaf beefy authorities"); - let authority_set = BeefyAuthoritySet::::decode(&mut raw_set.as_ref()).unwrap(); - let keyset_commitment: [u8; 32] = authority_set.keyset_commitment.into(); - - // 1. Rebuild the tree and confirm its root matches the on-chain commitment. - let leaves: Vec<[u8; 32]> = g2_keys.iter().map(|k| keccak_256(k)).collect(); - let tree = MerkleTree::::from_leaves(&leaves); - assert_eq!( - tree.root().expect("root"), - keyset_commitment, - "rebuilt keyset root does not match on-chain keyset_commitment (runtime converter mismatch)" - ); - - // 2. Signer set from the bitfield + the >2/3 threshold check. - let signer_indices: Vec = sc - .signatures - .iter() - .enumerate() - .filter_map(|(i, s)| s.as_ref().map(|_| i)) - .collect(); - assert!( - signer_indices.len() * 3 > total * 2, - "below supermajority: {} of {}", - signer_indices.len(), - total - ); - - // 3. Prove the signers' keys are committed (merkle multi-proof). - let signer_leaves: Vec<[u8; 32]> = signer_indices.iter().map(|&i| leaves[i]).collect(); - let proof = tree.proof(&signer_indices); - assert!( - MerkleProof::::new(proof.proof_hashes().to_vec()).verify( - keyset_commitment, - &signer_indices, - &signer_leaves, - total, - ), - "merkle multi-proof of signer keys against keyset_commitment failed" - ); - - // 4. Aggregate the signers' G2 keys and G1 signatures, one pairing check. - let mut agg_sig: Option<::SignatureGroup> = None; - let mut agg_pub: Option<::PublicKeyGroup> = None; - for &i in &signer_indices { - let sig = sc.signatures[i].as_ref().unwrap(); - let g1 = Signature::::from_bytes(&sig.0[65..113]).expect("g1 signature"); - let pk = PublicKey::::from_bytes(&g2_keys[i]).expect("g2 public key"); - agg_sig = Some(agg_sig.map_or(g1.0, |acc| acc + g1.0)); - agg_pub = Some(agg_pub.map_or(pk.0, |acc| acc + pk.0)); - } - let aggregate_ok = Signature::(agg_sig.unwrap()) - .verify(&message, &PublicKey::(agg_pub.unwrap())); - assert!(aggregate_ok, "aggregate BLS pairing check failed"); - - println!( - "Trustless aggregate BLS verify OK: {}/{} signers proven against the on-chain \ - keyset_commitment (merkle multi-proof), >2/3 threshold met, aggregated into ONE pairing \ - check.", - signer_indices.len(), - total - ); -} - /// Pins down exactly how BEEFY's BLS half hashes a commitment onto the signature curve, and emits /// a test vector for the Solidity/EIP-2537 implementation to be checked against. /// @@ -946,428 +790,6 @@ fn bls_hash_to_curve_vector() { println!("per point by the precompile or once at the end without changing the result."); } -/// The production path end to end: the prover assembles a BLS consensus proof and the verifier -/// accepts it, with no proof-building logic in the test itself. -/// -/// `test_bls_trustless_verify` above proves the same thing from first principles, rebuilding the -/// tree by hand so it fails loudly if the runtime's converter ever stops committing G2 keys. This -/// one exercises the API a relayer would actually call. -/// -/// RELAY_WS_URL=ws://127.0.0.1:9979 \ -/// cargo test -p beefy-verifier --features bls test_bls_consensus_via_prover -- --ignored -/// --nocapture -#[cfg(feature = "bls")] -#[tokio::test] -#[ignore] -async fn test_bls_consensus_via_prover() { - use beefy_prover::bls::decode_paired_justification; - - let max_rpc_payload_size = 15 * 1024 * 1024; - let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); - - let (relay_client, relay_rpc_client) = - subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) - .await - .unwrap(); - let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); - // Relay-only: the "para" client points at the same relay and `para_ids` is empty, so no - // parachain headers are proven. - let (para_client, para_rpc_client) = - subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) - .await - .unwrap(); - let para_rpc = LegacyRpcMethods::::new(para_rpc_client.clone()); - - let prover = Prover { - beefy_activation_block: 0, - relay: relay_client, - relay_rpc: relay_rpc.clone(), - relay_rpc_client: relay_rpc_client.clone(), - para: para_client, - para_rpc, - para_rpc_client, - para_ids: vec![], - query_batch_size: Some(100), - }; - - let engine_id = polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID; - let latest: H256 = - relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await.unwrap(); - - // Seed the trusted state from an earlier BEEFY-justified block, so the proof advances it. - let mut previous = H256::default(); - let mut cursor = latest; - for _ in 0..2000 { - let header = relay_rpc.chain_get_header(Some(cursor.into())).await.unwrap().unwrap(); - let parent: H256 = header.parent_hash.into(); - if parent.is_zero() { - panic!("reached genesis without a previous beefy block"); - } - let block = relay_rpc.chain_get_block(Some(parent.into())).await.unwrap().unwrap(); - if block - .justifications - .map(|js| js.iter().any(|j| j.0 == engine_id)) - .unwrap_or(false) - { - previous = parent; - break; - } - cursor = parent; - } - assert!(!previous.is_zero(), "no previous beefy block found"); - - let trusted_state = prover.get_initial_consensus_state(Some(previous)).await.unwrap(); - let trusted_height = trusted_state.latest_beefy_height; - - let latest_block = relay_rpc.chain_get_block(Some(latest.into())).await.unwrap().unwrap(); - let justification = latest_block - .justifications - .expect("latest beefy block must have justifications") - .into_iter() - .find_map(|j| (j.0 == engine_id).then_some(j.1)) - .expect("latest beefy block must have a beefy justification"); - - let signed_commitment = decode_paired_justification(&justification).unwrap(); - let block_number = signed_commitment.commitment.block_number; - let signer_count = signed_commitment.signatures.iter().filter(|s| s.is_some()).count(); - - let proof = prover.bls_consensus_proof(signed_commitment).await.unwrap(); - assert_eq!(proof.mmr.signers.len(), signer_count, "prover dropped signers"); - - let result = sp_io::TestExternalities::default() - .execute_with(|| verify_bls_consensus::(trusted_state, proof)); - - let (new_state, _headers) = result.expect("BLS consensus verification failed"); - let new_state = ConsensusState::decode(&mut &new_state[..]).unwrap(); - - assert_eq!(new_state.latest_beefy_height, block_number, "height was not advanced"); - assert!(new_state.latest_beefy_height > trusted_height, "state did not move forward"); - - println!( - "BLS consensus verified via the prover API: {signer_count} signers aggregated, \ - height {trusted_height} -> {block_number}" - ); -} - -/// Offline coverage for the aggregate BLS path. -/// -/// The BLS integration tests above all need a live relay chain, so none of them run in CI. These -/// build a validator set from deterministic seeds and exercise the verifier directly, including -/// the rejection paths, which is where the interesting behaviour lives. -#[cfg(feature = "bls-crypto")] -mod bls_offline { - use super::*; - use beefy_verifier_primitives::{ - BLS_G1_SIGNATURE_LEN, BLS_G2_PUBLIC_KEY_LEN, BlsMmrProof, BlsSigner, - }; - use w3f_bls::{ - EngineBLS, Message, SecretKeyVT, SerializableToBytes, Signature as BlsSignature, TinyBLS381, - }; - - const SET_ID: ValidatorSetId = 7; - const BLOCK: u32 = 100; - - type Validator = (SecretKeyVT, [u8; BLS_G2_PUBLIC_KEY_LEN]); - - /// Deterministic validators, so failures reproduce. - fn validators(count: usize) -> Vec { - (0..count) - .map(|i| { - let secret = SecretKeyVT::::from_seed(&[b'v', i as u8]); - let public = secret.into_public().to_bytes(); - (secret, public.try_into().expect("G2 public key is 96 bytes")) - }) - .collect() - } - - fn aggregate(signatures: &[BlsSignature]) -> [u8; BLS_G1_SIGNATURE_LEN] { - let mut sum: Option<::SignatureGroup> = None; - for signature in signatures { - sum = Some(sum.map_or(signature.0, |acc| acc + signature.0)); - } - BlsSignature::(sum.expect("no signatures to aggregate")) - .to_bytes() - .try_into() - .expect("G1 signature is 48 bytes") - } - - /// A trusted state and a proof over `signer_indices`, both internally consistent. - /// - /// The MMR is a single leaf, so its root is just the leaf hash and an empty proof verifies. - /// That lets the happy path run offline rather than only against a chain. - fn valid_proof( - validators: &[Validator], - signer_indices: &[usize], - ) -> (ConsensusState, BlsMmrProof) { - let leaf = MmrLeaf { - version: MmrLeafVersion::new(0, 0), - parent_number_and_hash: (BLOCK - 1, H256::zero()), - beefy_next_authority_set: BeefyNextAuthoritySet { - id: SET_ID + 1, - len: validators.len() as u32, - keyset_commitment: H256::zero(), - }, - leaf_extra: H256::zero(), - }; - let mmr_root = H256(keccak_256(&leaf.encode())); - - let payload = Payload::from_single_entry(*b"mh", mmr_root.0.to_vec()); - let commitment = Commitment { payload, block_number: BLOCK, validator_set_id: SET_ID }; - - // Two trees, mirroring the runtime. The BLS keys have their own tree, and its root is one - // extra leaf of the authority set tree, after the per-authority leaves the ECDSA path - // proves against. Those are stand-ins here; only their count matters. - let bls_leaves = validators.iter().map(|(_, key)| keccak_256(key)).collect::>(); - let bls_tree = MerkleTree::::from_leaves(&bls_leaves); - let bls_commitment = H256(bls_tree.root().expect("bls tree has a root")); - let authority_proof = bls_tree.proof(signer_indices).proof_hashes().to_vec(); - - let mut keyset_leaves = - (0..validators.len()).map(|i| keccak_256(&[b'a', i as u8])).collect::>(); - keyset_leaves.push(keccak_256(bls_commitment.as_bytes())); - let keyset_tree = MerkleTree::::from_leaves(&keyset_leaves); - let keyset_commitment = H256(keyset_tree.root().expect("keyset tree has a root")); - let keyset_proof = keyset_tree.proof(&[validators.len()]).proof_hashes().to_vec(); - - let message = Message::new(b"", &commitment.encode()); - let signatures = signer_indices - .iter() - .map(|&i| validators[i].0.sign(&message)) - .collect::>(); - - let signers = signer_indices - .iter() - .map(|&i| BlsSigner { public_key: validators[i].1, index: i as u32 }) - .collect::>(); - - let trusted_state = ConsensusState { - latest_beefy_height: BLOCK - 1, - beefy_activation_block: 0, - mmr_root_hash: H256::zero(), - current_authorities: BeefyAuthoritySet { - id: SET_ID, - len: validators.len() as u32, - keyset_commitment, - }, - next_authorities: BeefyAuthoritySet { - id: SET_ID + 1, - len: validators.len() as u32, - keyset_commitment: H256::zero(), - }, - }; - - let proof = BlsMmrProof { - commitment, - signers, - aggregate_signature: aggregate(&signatures), - latest_mmr_leaf: leaf, - mmr_proof: LeafProof { leaf_indices: vec![0], leaf_count: 1, items: vec![] }, - bls_commitment, - keyset_proof, - authority_proof, - }; - - (trusted_state, proof) - } - - fn verify( - trusted_state: ConsensusState, - proof: BlsMmrProof, - ) -> Result<(ConsensusState, H256), Error> { - sp_io::TestExternalities::default() - .execute_with(|| crate::verify_bls_mmr_update_proof::(trusted_state, proof)) - } - - #[test] - fn accepts_a_valid_aggregate() { - let validators = validators(4); - let (trusted_state, proof) = valid_proof(&validators, &[0, 1, 2, 3]); - - let (new_state, _leaf_extra) = verify(trusted_state, proof).expect("should verify"); - - assert_eq!(new_state.latest_beefy_height, BLOCK, "height should advance to the commitment"); - } - - // Three of four is the supermajority, so a partial set must still verify. The aggregate is - // over the signers alone, which is what makes the merkle multi-proof necessary. - #[test] - fn accepts_a_supermajority_subset() { - let validators = validators(4); - let (trusted_state, proof) = valid_proof(&validators, &[0, 1, 3]); - - assert!(verify(trusted_state, proof).is_ok()); - } - - // The check that stops a prover claiming one validator many times. Without it, a single - // signer's key and signature could be repeated to clear the supermajority threshold, and BLS - // aggregation would happily verify the repeated key against the repeated signature. - #[test] - fn rejects_duplicate_signer_indices() { - let validators = validators(4); - let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); - - // One real signer, counted three times. The signature is genuinely the sum of three - // copies of validator 0's signature, so the pairing check itself would pass. - let message = Message::new(b"", &proof.commitment.encode()); - let signature = validators[0].0.sign(&message); - proof.aggregate_signature = - aggregate(&[validators[0].0.sign(&message), validators[0].0.sign(&message), signature]); - for signer in proof.signers.iter_mut() { - signer.public_key = validators[0].1; - signer.index = 0; - } - - assert!(matches!(verify(trusted_state, proof), Err(Error::InvalidBlsSignerOrdering))); - } - - #[test] - fn rejects_unordered_signer_indices() { - let validators = validators(4); - let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); - proof.signers.swap(0, 2); - - assert!(matches!(verify(trusted_state, proof), Err(Error::InvalidBlsSignerOrdering))); - } - - #[test] - fn rejects_signer_outside_the_authority_set() { - let validators = validators(4); - let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); - proof.signers.last_mut().unwrap().index = 9; - - assert!(matches!(verify(trusted_state, proof), Err(Error::InvalidBlsSignerOrdering))); - } - - #[test] - fn rejects_sub_supermajority() { - let validators = validators(4); - // Two of four is short of the >2/3 threshold. - let (trusted_state, proof) = valid_proof(&validators, &[0, 1]); - - assert!(matches!(verify(trusted_state, proof), Err(Error::SuperMajorityRequired))); - } - - #[test] - fn rejects_a_proof_with_no_signers() { - let validators = validators(4); - let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); - proof.signers.clear(); - - assert!(matches!(verify(trusted_state, proof), Err(Error::NoBlsSigners))); - } - - #[test] - fn rejects_a_stale_commitment() { - let validators = validators(4); - let (mut trusted_state, proof) = valid_proof(&validators, &[0, 1, 2]); - trusted_state.latest_beefy_height = BLOCK; - - assert!(matches!(verify(trusted_state, proof), Err(Error::StaleHeight { .. }))); - } - - #[test] - fn rejects_an_unknown_authority_set() { - let validators = validators(4); - let (mut trusted_state, proof) = valid_proof(&validators, &[0, 1, 2]); - trusted_state.current_authorities.id = SET_ID + 5; - trusted_state.next_authorities.id = SET_ID + 6; - - assert!(matches!(verify(trusted_state, proof), Err(Error::UnknownAuthoritySet { .. }))); - } - - // A signature over a different commitment: well formed points, wrong message. - #[test] - fn rejects_an_aggregate_over_the_wrong_message() { - let validators = validators(4); - let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); - - let other = Message::new(b"", b"a different commitment"); - proof.aggregate_signature = aggregate(&[ - validators[0].0.sign(&other), - validators[1].0.sign(&other), - validators[2].0.sign(&other), - ]); - - assert!(matches!(verify(trusted_state, proof), Err(Error::BlsVerificationFailed))); - } - - // Dropping a signer from the aggregate while leaving their key in the proof must not verify, - // or a validator could be credited with a signature they never produced. - #[test] - fn rejects_an_aggregate_missing_a_claimed_signer() { - let validators = validators(4); - let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); - - let message = Message::new(b"", &proof.commitment.encode()); - proof.aggregate_signature = - aggregate(&[validators[0].0.sign(&message), validators[1].0.sign(&message)]); - - assert!(matches!(verify(trusted_state, proof), Err(Error::BlsVerificationFailed))); - } - - // All-ones bytes are not undecodable. The compressed encoding is big-endian with the point - // flags in the high bits of the *first* byte, and 0xff sets the infinity flag, so these decode - // to the identity element instead of failing. The identity is harmless (it contributes nothing - // to either sum, and a signer still has to appear in the committed keyset), but the rejection - // therefore arrives as a failed pairing rather than a decode error. - #[test] - fn rejects_an_identity_public_key() { - let validators = validators(4); - let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); - proof.signers[1].public_key = [0xff; BLS_G2_PUBLIC_KEY_LEN]; - - let result = verify(trusted_state, proof); - assert!(matches!(result, Err(Error::BlsVerificationFailed)), "got {result:?}"); - } - - #[test] - fn rejects_an_identity_signature() { - let validators = validators(4); - let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); - proof.aggregate_signature = [0xff; BLS_G1_SIGNATURE_LEN]; - - let result = verify(trusted_state, proof); - assert!(matches!(result, Err(Error::BlsVerificationFailed)), "got {result:?}"); - } - - // Corrupting the coordinate while leaving the flag bits alone does fail to decode, which is - // the path that reports `InvalidBlsPoint`. - #[test] - fn rejects_an_undecodable_public_key() { - let validators = validators(4); - let (trusted_state, mut proof) = valid_proof(&validators, &[0, 1, 2]); - proof.signers[1].public_key[0] ^= 0xff; - - let result = verify(trusted_state, proof); - assert!(matches!(result, Err(Error::InvalidBlsPoint)), "got {result:?}"); - } - - // Correctly signed by keys that simply are not the committed authority set. The pairing check - // passes; only the merkle multi-proof catches it. - #[test] - fn rejects_signers_outside_the_committed_keyset() { - let committed = validators(4); - let impostors = (0..4) - .map(|i| { - let secret = SecretKeyVT::::from_seed(&[b'x', i as u8]); - let public = secret.into_public().to_bytes(); - (secret, public.try_into().expect("G2 public key is 96 bytes")) - }) - .collect::>(); - - let (trusted_state, mut proof) = valid_proof(&impostors, &[0, 1, 2]); - // Keep the impostors' signatures and keys, but point the state at the real keyset. - let leaves = committed.iter().map(|(_, key)| keccak_256(key)).collect::>(); - let tree = MerkleTree::::from_leaves(&leaves); - let mut trusted_state = trusted_state; - trusted_state.current_authorities.keyset_commitment = - H256(tree.root().expect("keyset tree has a root")); - proof.authority_proof = tree.proof(&[0, 1, 2]).proof_hashes().to_vec(); - - assert!(matches!(verify(trusted_state, proof), Err(Error::InvalidAuthoritiesProof))); - } -} - /// Emits an EIP-2537 shaped fixture for the Solidity aggregate verifier. /// /// The Solidity side cannot consume the compressed points the proof carries, because EIP-2537 has @@ -1435,53 +857,7 @@ fn bls_eip2537_fixture() { println!("pk.y.c0 {}", fq(&py.c0)); println!("pk.y.c1 {}", fq(&py.c1)); - // The keyset commitment the Solidity client verifies against, built over the *uncompressed* - // encoding since that is what the contract can hash without a decompression precompile. Four - // authorities, of which the three above signed, so the multi-proof is non-trivial. - let uncompressed = |secret: &SecretKeyVT| -> Vec { - let affine: G2Affine = secret.into_public().0.into_affine(); - let (x, y) = affine.xy().expect("public key is not the identity"); - let mut out = Vec::with_capacity(4 * 64); - for coord in [&x.c0, &x.c1, &y.c0, &y.c1] { - out.extend_from_slice(&[0u8; 16]); - out.extend_from_slice(&coord.into_bigint().to_bytes_be()); - } - out - }; - - let authorities: Vec<_> = - (0..4).map(|i| SecretKeyVT::::from_seed(&[b'v', i as u8])).collect(); - // The runtime commits the compressed encoding, and the contract compresses to match, so the - // leaves are over compressed keys. - let leaves: Vec<[u8; 32]> = authorities - .iter() - .map(|secret| keccak_256(&secret.into_public().to_bytes())) - .collect(); - let bls_tree = MerkleTree::::from_leaves(&leaves); - let bls_commitment = bls_tree.root().expect("bls root"); - let _ = &uncompressed; - - // The authority set tree: per-authority leaves (stand-ins for the ECDSA addresses), then the - // BLS commitment as one extra leaf. - let mut keyset_leaves: Vec<[u8; 32]> = - (0..authorities.len()).map(|i| keccak_256(&[b'a', i as u8])).collect(); - keyset_leaves.push(keccak_256(&bls_commitment)); - let keyset_tree = MerkleTree::::from_leaves(&keyset_leaves); - - for (i, secret) in authorities.iter().enumerate() { - println!("authority {i} compressed {}", hex::encode(secret.into_public().to_bytes())); - } - println!("-- two-level keyset, {} authorities --", authorities.len()); - println!("bls commitment {}", hex::encode(bls_commitment)); - println!("keyset root {}", hex::encode(keyset_tree.root().expect("root"))); - for hash in keyset_tree.proof(&[authorities.len()]).proof_hashes() { - println!("keyset proof node {}", hex::encode(hash)); - } - for hash in bls_tree.proof(&[0, 1, 2]).proof_hashes() { - println!("authority proof node {}", hex::encode(hash)); - } - - // Each signer's key on its own, for the merkle leaves and the G2_ADD path. + // Each signer's key on its own, for the G2_ADD path. for (i, secret) in validators.iter().enumerate() { let affine: G2Affine = secret.into_public().0.into_affine(); let (x, y) = affine.xy().expect("public key is not the identity"); @@ -1493,382 +869,12 @@ fn bls_eip2537_fixture() { } } -/// Emits a complete ABI-encoded state and proof so the Solidity client's `verify()` entry point -/// can be exercised, not just the pieces it calls. +/// Checks the group-bridging step an APK proof would need, against a live BLS relay. /// -/// The MMR is a single leaf, so its root is the leaf hash and an empty proof verifies. The -/// commitment carries that root, and the validators sign the SCALE encoding of the commitment, -/// which is exactly what the contract hashes. Keys are emitted uncompressed, because EIP-2537 has -/// no decompression precompile, and the keyset commitment is built over the same encoding. -/// -/// cargo test -p beefy-verifier --features bls-crypto bls_solidity_proof_fixture -- --nocapture -#[cfg(feature = "bls-crypto")] -#[test] -fn bls_solidity_proof_fixture() { - use alloy_sol_types::SolType; - use ark_bls12_381::G2Affine; - use ark_ec::{AffineRepr, CurveGroup}; - use ark_ff::{BigInteger, PrimeField}; - use ismp_abi::bls_beefy::BlsBeefy as Sol; - use w3f_bls::{ - EngineBLS, Message, SecretKeyVT, SerializableToBytes, Signature as BlsSignature, TinyBLS381, - }; - - const SET_ID: u64 = 7; - const BLOCK: u32 = 100; - - let uncompressed = |secret: &SecretKeyVT| -> Vec { - let affine: G2Affine = secret.into_public().0.into_affine(); - let (x, y) = affine.xy().expect("not the identity"); - let mut out = Vec::with_capacity(256); - for coord in [&x.c0, &x.c1, &y.c0, &y.c1] { - out.extend_from_slice(&[0u8; 16]); - out.extend_from_slice(&coord.into_bigint().to_bytes_be()); - } - out - }; - - let authorities: Vec<_> = - (0..4).map(|i| SecretKeyVT::::from_seed(&[b'v', i as u8])).collect(); - let keys: Vec> = authorities.iter().map(uncompressed).collect(); - - // Two trees, as the runtime builds them: the BLS keys in their own tree, whose root is one - // extra leaf of the authority set tree. The leaves before it stand in for the ECDSA addresses. - let bls_leaves: Vec<[u8; 32]> = - authorities.iter().map(|s| keccak_256(&s.into_public().to_bytes())).collect(); - let bls_tree = MerkleTree::::from_leaves(&bls_leaves); - let bls_commitment = bls_tree.root().expect("bls root"); - - let mut keyset_leaves: Vec<[u8; 32]> = - (0..authorities.len()).map(|i| keccak_256(&[b'a', i as u8])).collect(); - keyset_leaves.push(keccak_256(&bls_commitment)); - let keyset = MerkleTree::::from_leaves(&keyset_leaves); - let keyset_root = keyset.root().expect("keyset root"); - let keyset_proof = keyset.proof(&[authorities.len()]); - - // The MMR leaf, and the root it implies as the only leaf in the tree. - let leaf = MmrLeaf { - version: MmrLeafVersion::new(0, 0), - parent_number_and_hash: (0u32, H256::zero()), - beefy_next_authority_set: BeefyNextAuthoritySet { - id: SET_ID + 1, - len: authorities.len() as u32, - keyset_commitment: H256(keyset_root), - }, - leaf_extra: H256::zero(), - }; - let mmr_root = H256(keccak_256(&leaf.encode())); - - // The validators sign the SCALE encoding of this commitment; the contract hashes the same. - let payload = Payload::from_single_entry(*b"mh", mmr_root.0.to_vec()); - let commitment = Commitment { payload, block_number: BLOCK, validator_set_id: SET_ID }; - let message = Message::new(b"", &commitment.encode()); - - let signer_indices = [0usize, 1, 2]; - let mut agg: Option<::SignatureGroup> = None; - for &i in &signer_indices { - let sig = authorities[i].sign(&message); - agg = Some(agg.map_or(sig.0, |acc| acc + sig.0)); - } - let agg_affine = BlsSignature::(agg.expect("signers")).0.into_affine(); - let (sx, sy) = agg_affine.xy().expect("not the identity"); - let mut aggregate_signature = Vec::with_capacity(128); - for coord in [sx, sy] { - aggregate_signature.extend_from_slice(&[0u8; 16]); - aggregate_signature.extend_from_slice(&coord.into_bigint().to_bytes_be()); - } - - let authority_proof = bls_tree.proof(&signer_indices); - - // Assemble the sol types the contract decodes. - let state = Sol::BeefyConsensusState { - latestHeight: alloy_primitives::U256::from(BLOCK - 1), - beefyActivationBlock: alloy_primitives::U256::ZERO, - currentAuthoritySet: Sol::AuthoritySetCommitment { - id: SET_ID, - len: authorities.len() as u32, - root: alloy_primitives::FixedBytes(keyset_root), - }, - nextAuthoritySet: Sol::AuthoritySetCommitment { - id: SET_ID + 1, - len: authorities.len() as u32, - root: alloy_primitives::FixedBytes(keyset_root), - }, - }; - - let relay = Sol::BlsRelayChainProof { - commitment: Sol::Commitment { - payload: vec![Sol::Payload { - id: alloy_primitives::FixedBytes(*b"mh"), - data: alloy_primitives::Bytes::from(mmr_root.0.to_vec()), - }], - blockNumber: BLOCK, - validatorSetId: SET_ID, - }, - signers: signer_indices - .iter() - .map(|&i| Sol::BlsSigner { - publicKey: alloy_primitives::Bytes::from(keys[i].clone()), - authorityIndex: alloy_primitives::U256::from(i), - }) - .collect(), - aggregateSignature: alloy_primitives::Bytes::from(aggregate_signature), - latestMmrLeaf: Sol::BeefyMmrLeaf { - version: 0, - parentNumber: 0, - parentHash: alloy_primitives::FixedBytes([0u8; 32]), - nextAuthoritySet: Sol::AuthoritySetCommitment { - id: SET_ID + 1, - len: authorities.len() as u32, - root: alloy_primitives::FixedBytes(keyset_root), - }, - extra: alloy_primitives::FixedBytes([0u8; 32]), - leafIndex: alloy_primitives::U256::ZERO, - }, - mmrProof: vec![], - blsCommitment: alloy_primitives::FixedBytes(bls_commitment), - keysetProof: keyset_proof - .proof_hashes() - .iter() - .map(|h| alloy_primitives::FixedBytes(*h)) - .collect(), - proof: authority_proof - .proof_hashes() - .iter() - .map(|h| alloy_primitives::FixedBytes(*h)) - .collect(), - }; - - let parachain = Sol::ParachainProof { - parachains: vec![], - proof: vec![], - leafCount: alloy_primitives::U256::ZERO, - }; - - let encoded_state = Sol::BeefyConsensusState::abi_encode(&state); - let encoded_proof = - <(Sol::BlsRelayChainProof, Sol::ParachainProof) as SolType>::abi_encode_params(&( - relay, parachain, - )); - - println!("=== solidity verify() fixture ==="); - println!("state 0x{}", hex::encode(encoded_state)); - println!("proof 0x{}", hex::encode(encoded_proof)); - println!("expected new latestHeight {BLOCK}"); -} - -/// Works out the compressed-encoding sign rule, so the Solidity client can compress an -/// uncompressed key on the fly and match the leaf the runtime commits. -/// -/// Compressing is cheap; decompressing a G2 point on chain would need Fp2 square roots. So the -/// prover sends uncompressed points, which the pairing needs anyway, and the contract derives the -/// compressed form for the merkle leaf. That only works if the flag convention is pinned down. -/// -/// cargo test -p beefy-verifier --features bls-crypto bls_compression_rule -- --nocapture -#[cfg(feature = "bls-crypto")] -#[test] -fn bls_compression_rule() { - use ark_bls12_381::{Fq, G2Affine}; - use ark_ec::{AffineRepr, CurveGroup}; - use ark_ff::{BigInteger, PrimeField}; - use w3f_bls::{SecretKeyVT, SerializableToBytes, TinyBLS381}; - - // (p - 1) / 2, the threshold the IETF convention uses to call a root "larger". - let half = { - let modulus = Fq::MODULUS; - let mut bytes = modulus.to_bytes_be(); - // divide by two, big-endian, then subtract nothing: (p-1)/2 == p >> 1 for odd p - let mut carry = 0u8; - for b in bytes.iter_mut() { - let cur = *b; - *b = (cur >> 1) | (carry << 7); - carry = cur & 1; - } - bytes - }; - - let gt_half = |v: &Fq| -> bool { v.into_bigint().to_bytes_be() > half }; - - println!("=== compressed flag vs y sign, {} samples ===", 8); - for i in 0..8u8 { - let secret = SecretKeyVT::::from_seed(&[b'c', i]); - let compressed = secret.into_public().to_bytes(); - let affine: G2Affine = secret.into_public().0.into_affine(); - let (_, y) = affine.xy().expect("not the identity"); - - println!( - "seed {i}: flags {:#04x} y.c1>half {} y.c0>half {}", - compressed[0] & 0xe0, - gt_half(&y.c1), - gt_half(&y.c0), - ); - } -} - -/// `compress_g2` / `compress_g1` must reproduce what `w3f-bls` serialises, since the keyset -/// commitment and the Rust verifier both work on the compressed encoding while an EVM-bound proof -/// carries uncompressed points. -#[cfg(feature = "bls-crypto")] -#[test] -fn compression_matches_w3f_bls() { - use ark_bls12_381::{G1Affine, G2Affine}; - use ark_ec::{AffineRepr, CurveGroup}; - use ark_ff::{BigInteger, PrimeField}; - use beefy_verifier_primitives::{compress_g1, compress_g2}; - use w3f_bls::{Message, SecretKeyVT, SerializableToBytes, TinyBLS381}; - - let msg = Message::new(b"", b"compression check"); - - for i in 0..8u8 { - let secret = SecretKeyVT::::from_seed(&[b'c', i]); - - // G2 public key. - let affine: G2Affine = secret.into_public().0.into_affine(); - let (x, y) = affine.xy().expect("not the identity"); - let mut uncompressed = [0u8; 256]; - for (slot, coord) in [&x.c0, &x.c1, &y.c0, &y.c1].iter().enumerate() { - let bytes = coord.into_bigint().to_bytes_be(); - uncompressed[slot * 64 + 16..slot * 64 + 64].copy_from_slice(&bytes); - } - assert_eq!( - compress_g2(&uncompressed).to_vec(), - secret.into_public().to_bytes(), - "G2 compression differs for seed {i}" - ); - - // G1 signature. - let sig = secret.sign(&msg); - let sig_affine: G1Affine = sig.0.into_affine(); - let (sx, sy) = sig_affine.xy().expect("not the identity"); - let mut sig_uncompressed = [0u8; 128]; - for (slot, coord) in [sx, sy].iter().enumerate() { - let bytes = coord.into_bigint().to_bytes_be(); - sig_uncompressed[slot * 64 + 16..slot * 64 + 64].copy_from_slice(&bytes); - } - assert_eq!( - compress_g1(&sig_uncompressed).to_vec(), - sig.to_bytes(), - "G1 compression differs for seed {i}" - ); - } -} - -/// Emits an ABI fixture built from a **live** BLS relay, so the Solidity client can be tested -/// against a real MMR proof and a real parachain header rather than a synthetic single-leaf tree. -/// -/// Needs a BLS BEEFY relay running with a parachain registered on it, since the proof has to carry -/// a real parachain header. The para id must be 4009, which is what gargantua tracks. -/// -/// RELAY_WS_URL=ws://127.0.0.1:9979 PARA_WS_URL=ws://127.0.0.1:9991 \ -/// cargo test -p beefy-verifier --features bls bls_live_abi_fixture -- --ignored --nocapture -#[cfg(feature = "bls")] -#[tokio::test] -#[ignore] -async fn bls_live_abi_fixture() { - use alloy_sol_types::SolType; - use beefy_prover::bls::{abi::to_abi_proof, decode_paired_justification}; - use ismp_abi::{ - bls_beefy::BlsBeefy, ecdsa_beefy::BeefyConsensusState as SolBeefyConsensusState, - }; - - let max_rpc_payload_size = 15 * 1024 * 1024; - let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); - let para_ws_url = std::env::var("PARA_WS_URL").expect("PARA_WS_URL must be set"); - - let (relay_client, relay_rpc_client) = - subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) - .await - .unwrap(); - let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); - let (para_client, para_rpc_client) = - subxt_utils::client::ws_client::(¶_ws_url, max_rpc_payload_size) - .await - .unwrap(); - let para_rpc = LegacyRpcMethods::::new(para_rpc_client.clone()); - - let prover = Prover { - beefy_activation_block: 0, - relay: relay_client, - relay_rpc: relay_rpc.clone(), - relay_rpc_client: relay_rpc_client.clone(), - para: para_client, - para_rpc, - para_rpc_client, - para_ids: vec![4009], - query_batch_size: Some(100), - }; - - let latest: H256 = - relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await.unwrap(); - let block = relay_rpc.chain_get_block(Some(latest.into())).await.unwrap().unwrap(); - let justification = block - .justifications - .expect("justifications") - .into_iter() - .find_map(|j| (j.0 == polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID).then_some(j.1)) - .expect("beefy justification"); - - let signed = decode_paired_justification(&justification).unwrap(); - let set_id = signed.commitment.validator_set_id; - - // Anchor one authority set back so the proof also exercises a rotation. - let mut anchor = H256::default(); - let mut cursor = latest; - for _ in 0..4000 { - let header = relay_rpc.chain_get_header(Some(cursor.into())).await.unwrap().unwrap(); - let parent: H256 = header.parent_hash.into(); - if parent.is_zero() { - break; - } - if let Ok(Some(b)) = relay_rpc.chain_get_block(Some(parent.into())).await { - if let Some(js) = b.justifications { - if let Some(raw) = js.into_iter().find_map(|j| { - (j.0 == polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID).then_some(j.1) - }) { - let prev = decode_paired_justification(&raw).unwrap(); - if prev.commitment.validator_set_id + 1 == set_id { - anchor = parent; - break; - } - } - } - } - cursor = parent; - } - assert!(!anchor.is_zero(), "no anchor one set back"); - - let state = prover.get_initial_consensus_state(Some(anchor)).await.unwrap(); - let message = prover.bls_consensus_proof(signed).await.unwrap(); - - let signer_count = message.mmr.signers.len(); - let para_count = message.parachain.parachains.len(); - let mmr_nodes = message.mmr.mmr_proof.items.len(); - let block_number = message.mmr.commitment.block_number; - - let abi_state: SolBeefyConsensusState = state.into(); - let abi_proof = to_abi_proof(message).expect("to_abi_proof"); - - let encoded_state = SolBeefyConsensusState::abi_encode(&abi_state); - let encoded_proof = - <(BlsBeefy::BlsRelayChainProof, BlsBeefy::ParachainProof) as SolType>::abi_encode_params( - &(abi_proof.relay, abi_proof.parachain), - ); - - println!("=== live abi fixture ==="); - println!( - "signers {signer_count} | parachains {para_count} | mmr nodes {mmr_nodes} | block {block_number}" - ); - println!("state 0x{}", hex::encode(encoded_state)); - println!("proof 0x{}", hex::encode(encoded_proof)); - assert!(para_count > 0, "expected a parachain header from the registered para"); -} - -/// Checks the group-bridging step an APK proof would need, against a live BLS relay. -/// -/// `gnark-apk-proofs` aggregates public keys in G1 and pairs them against a G2 signature, while -/// BEEFY signs in G1 with G2 public keys. `DoublePublicKey` publishes the same secret in both -/// groups, so both aggregates describe one aggregate secret and the two can be tied together -/// without changing how the relay signs: +/// `gnark-apk-proofs` aggregates public keys in G1 and pairs them against a G2 signature, while +/// BEEFY signs in G1 with G2 public keys. `DoublePublicKey` publishes the same secret in both +/// groups, so both aggregates describe one aggregate secret and the two can be tied together +/// without changing how the relay signs: /// /// ```text /// e(apk_g1, g2) == e(g1, apk_g2) binds an untrusted apk_g2 to apk_g1 @@ -1893,7 +899,7 @@ async fn bls_apk_group_binding() { aggregate_signatures, beefy_g1_authorities, beefy_g2_authorities, decode_paired_justification, }; - use w3f_bls::{EngineBLS, Message, TinyBLS381}; + use w3f_bls::{Message, TinyBLS381}; let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); let (_relay_client, relay_rpc_client) = @@ -1990,3 +996,381 @@ async fn bls_apk_group_binding() { println!("g1[{i}] {}", hex::encode(key)); } } + +/// Collects everything an APK consensus proof needs from a live BLS relay, except the SNARK. +/// +/// The SNARK is generated by `gnark-apk-proofs`, which pulls in a Go toolchain through cgo and an +/// 800MB structured reference string, so it stays out of this workspace. The two halves meet over +/// the json this writes: +/// +/// ```text +/// this test -> apk-inputs.json (live BEEFY data, validator G1 keys) +/// gnark-apk-proofs -> apk-snark.json (PLONK proof, public inputs) +/// bls_apk_live_fixture -> the two .hex files BlsApkBeefy.t.sol reads +/// ``` +/// +/// The parachain must be registered as 4009, which is what gargantua tracks. +/// +/// RELAY_WS_URL=ws://127.0.0.1:9979 PARA_WS_URL=ws://127.0.0.1:9981 \ +/// APK_FIXTURE_DIR=/tmp/apk \ +/// cargo test -p beefy-verifier --features bls,bls-crypto bls_apk_live_inputs -- --ignored +/// --nocapture +#[cfg(all(feature = "bls", feature = "bls-crypto"))] +#[tokio::test] +#[ignore] +async fn bls_apk_live_inputs() { + use ark_bls12_381::{G1Affine, G1Projective, G2Affine, G2Projective}; + use ark_ec::{AffineRepr, CurveGroup}; + use ark_ff::{BigInteger, PrimeField}; + use ark_serialize::CanonicalDeserialize; + use beefy_prover::bls::{ + aggregate_signatures, beefy_g1_authorities, beefy_g2_authorities, + decode_paired_justification, + }; + + // EIP-2537 wants padded coordinates, but `ApkProof` takes bytes32[3] and bytes32[6], which are + // the coordinates packed with no padding at all. Getting this wrong produces a well formed + // point and a silent pairing failure, so both encodings exist side by side deliberately. + fn fq_be(fq: &ark_bls12_381::Fq, out: &mut Vec) { + out.extend_from_slice(&fq.into_bigint().to_bytes_be()); + } + fn g1_packed(point: &G1Affine) -> String { + let (x, y) = point.xy().expect("not the identity"); + let mut bytes = Vec::with_capacity(96); + fq_be(x, &mut bytes); + fq_be(y, &mut bytes); + hex::encode(bytes) + } + fn g2_packed(point: &G2Affine) -> String { + let (x, y) = point.xy().expect("not the identity"); + let mut bytes = Vec::with_capacity(192); + fq_be(&x.c0, &mut bytes); + fq_be(&x.c1, &mut bytes); + fq_be(&y.c0, &mut bytes); + fq_be(&y.c1, &mut bytes); + hex::encode(bytes) + } + + let max_rpc_payload_size = 15 * 1024 * 1024; + let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); + let para_ws_url = std::env::var("PARA_WS_URL").expect("PARA_WS_URL must be set"); + let out_dir = std::env::var("APK_FIXTURE_DIR").expect("APK_FIXTURE_DIR must be set"); + + let (relay_client, relay_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); + let (para_client, para_rpc_client) = + subxt_utils::client::ws_client::(¶_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let para_rpc = LegacyRpcMethods::::new(para_rpc_client.clone()); + + let prover = Prover { + beefy_activation_block: 0, + relay: relay_client, + relay_rpc: relay_rpc.clone(), + relay_rpc_client: relay_rpc_client.clone(), + para: para_client, + para_rpc, + para_rpc_client, + para_ids: vec![4009], + query_batch_size: Some(100), + }; + + let latest: H256 = + relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await.unwrap(); + let block = relay_rpc.chain_get_block(Some(latest.into())).await.unwrap().unwrap(); + let justification = block + .justifications + .expect("justifications") + .into_iter() + .find_map(|j| (j.0 == polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID).then_some(j.1)) + .expect("beefy justification"); + + let signed = decode_paired_justification(&justification).unwrap(); + let set_id = signed.commitment.validator_set_id; + + // Anchor one authority set back, so the proof also exercises a rotation rather than only the + // current set. Exactly one back: the verifier accepts the trusted state's current or next set. + let mut anchor = H256::default(); + let mut cursor = latest; + for _ in 0..4000 { + let header = relay_rpc.chain_get_header(Some(cursor.into())).await.unwrap().unwrap(); + let parent: H256 = header.parent_hash.into(); + if parent.is_zero() { + break; + } + if let Ok(Some(b)) = relay_rpc.chain_get_block(Some(parent.into())).await { + if let Some(js) = b.justifications { + if let Some(raw) = js.into_iter().find_map(|j| { + (j.0 == polkadot_sdk::sp_consensus_beefy::BEEFY_ENGINE_ID).then_some(j.1) + }) { + let prev = decode_paired_justification(&raw).unwrap(); + if prev.commitment.validator_set_id + 1 == set_id { + anchor = parent; + break; + } + } + } + } + cursor = parent; + } + assert!(!anchor.is_zero(), "no anchor one set back"); + + let state = prover.get_initial_consensus_state(Some(anchor)).await.unwrap(); + let message = prover.bls_consensus_proof(signed.clone()).await.unwrap(); + + // The validator set that signed, both halves of the same paired keys. + let g1_keys = beefy_g1_authorities(&relay_rpc, Some(latest)).await.unwrap(); + let g2_keys = beefy_g2_authorities(&relay_rpc, Some(latest)).await.unwrap(); + assert_eq!(g1_keys.len(), g2_keys.len()); + + let mut apk_g1 = G1Projective::default(); + let mut apk_g2 = G2Projective::default(); + let mut signatures = Vec::new(); + let mut participation = Vec::new(); + for (index, maybe_signature) in signed.signatures.iter().enumerate() { + let Some(signature) = maybe_signature else { continue }; + apk_g1 += G1Affine::deserialize_compressed(&g1_keys[index][..]).expect("G1 key decodes"); + apk_g2 += G2Affine::deserialize_compressed(&g2_keys[index][..]).expect("G2 key decodes"); + signatures.push(signature.g1_signature()); + participation.push(index as u64); + } + assert!(!participation.is_empty(), "commitment carries no signatures"); + + let apk_g1 = apk_g1.into_affine(); + let apk_g2 = apk_g2.into_affine(); + let aggregate = aggregate_signatures(&signatures).unwrap(); + let sig_g1 = G1Affine::deserialize_compressed(&aggregate[..]).expect("aggregate decodes"); + + // The same commitment the runtime pallet builds, computed here over the signing set so the + // fixture's consensus state can be seeded with it. + let decompressed = g1_keys + .iter() + .map(|k| G1Affine::deserialize_compressed(&k[..]).expect("G1 key decodes")) + .collect::>(); + let padded = apk_commitment::padded_to_circuit_width(&decompressed); + let apk_commitment_bytes = apk_commitment::public_keys_commitment_bytes(&padded); + + let mmr = &message.mmr; + let leaf_index = mmr.mmr_proof.leaf_indices.first().copied().unwrap_or_default(); + let bundle = json::json!({ + "validatorSetId": set_id, + "blockNumber": mmr.commitment.block_number, + "payloadMh": hex::encode(mmr.commitment.payload.get_raw(b"mh").expect("mmr payload")), + "keys": g1_keys.iter().map(|k| { + g1_packed(&G1Affine::deserialize_compressed(&k[..]).expect("G1 key decodes")) + }).collect::>(), + "participation": participation, + "apk": g1_packed(&apk_g1), + "apk2": g2_packed(&apk_g2), + "signature": g1_packed(&sig_g1), + "apkCommitment": hex::encode(apk_commitment_bytes), + "mmrLeaf": { + "parentNumber": mmr.latest_mmr_leaf.parent_number_and_hash.0, + "parentHash": hex::encode(mmr.latest_mmr_leaf.parent_number_and_hash.1.0), + "nextAuthoritySetId": mmr.latest_mmr_leaf.beefy_next_authority_set.id, + "nextAuthoritySetLen": mmr.latest_mmr_leaf.beefy_next_authority_set.len, + "nextAuthoritySetRoot": + hex::encode(mmr.latest_mmr_leaf.beefy_next_authority_set.keyset_commitment.0), + "extra": hex::encode(mmr.latest_mmr_leaf.leaf_extra.0), + "leafIndex": leaf_index, + }, + "mmrProof": mmr.mmr_proof.items.iter().map(|h| hex::encode(h.0)).collect::>(), + "parachains": message.parachain.parachains.iter().map(|p| json::json!({ + "index": p.index, + "id": p.para_id, + "header": hex::encode(&p.header), + })).collect::>(), + "parachainProof": + message.parachain.proof.iter().map(|h| hex::encode(h)).collect::>(), + "parachainLeafCount": message.parachain.total_leaves, + "trusted": { + "latestHeight": state.latest_beefy_height, + "beefyActivationBlock": state.beefy_activation_block, + "currentId": state.current_authorities.id, + "currentLen": state.current_authorities.len, + "nextId": state.next_authorities.id, + "nextLen": state.next_authorities.len, + }, + }); + + std::fs::create_dir_all(&out_dir).expect("create fixture dir"); + let path = format!("{out_dir}/apk-inputs.json"); + std::fs::write(&path, json::to_string_pretty(&bundle).unwrap()).expect("write bundle"); + + println!("=== apk live inputs ==="); + println!( + "set {set_id} | {} validators | {} signed | block {} | mmr nodes {} | parachains {}", + g1_keys.len(), + participation.len(), + mmr.commitment.block_number, + mmr.mmr_proof.items.len(), + message.parachain.parachains.len(), + ); + println!("apk commitment 0x{}", hex::encode(apk_commitment_bytes)); + println!("wrote {path}"); + assert!( + !message.parachain.parachains.is_empty(), + "expected a parachain header from the registered para", + ); +} + +/// Assembles the two hex fixtures `BlsApkBeefy.t.sol` reads, from the live data collected by +/// `bls_apk_live_inputs` and the proof generated by `gnark-apk-proofs`. Needs no chain access, so +/// the fixture can be rebuilt without the relay still running. +/// +/// APK_FIXTURE_DIR=/tmp/apk \ +/// cargo test -p beefy-verifier --features bls bls_apk_live_fixture -- --ignored --nocapture +#[cfg(feature = "bls")] +#[test] +#[ignore] +fn bls_apk_live_fixture() { + use alloy_primitives::{Bytes, FixedBytes, U256}; + use alloy_sol_types::{SolType, SolValue}; + use ismp_abi::bls_apk_beefy::BlsApkBeefy; + + let dir = std::env::var("APK_FIXTURE_DIR").expect("APK_FIXTURE_DIR must be set"); + let read = |name: &str| -> json::Value { + let path = format!("{dir}/{name}"); + json::from_str(&std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path}: {e}"))) + .expect("valid json") + }; + let inputs = read("apk-inputs.json"); + let snark = read("apk-snark.json"); + + let hex_bytes = |value: &json::Value| hex::decode(value.as_str().expect("hex string")).unwrap(); + let fixed32 = |value: &json::Value| FixedBytes::<32>::from_slice(&hex_bytes(value)); + let u64_of = |value: &json::Value| value.as_u64().expect("number"); + // Splits a packed curve point into the bytes32 words the SNARK verifier takes. Not the padded + // EIP-2537 layout the rest of the BLS code uses. + let words = |value: &json::Value, count: usize| -> Vec> { + let raw = hex_bytes(value); + assert_eq!(raw.len(), count * 32, "point is the wrong width for bytes32[{count}]"); + raw.chunks(32).map(FixedBytes::<32>::from_slice).collect() + }; + + // The commitment the proof was generated against has to be the one the client checks it with, + // so take it from the SNARK's own public inputs and require the runtime's version to agree. + let apk_commitment = fixed32(&snark["apkCommitment"]); + assert_eq!( + apk_commitment, + fixed32(&inputs["apkCommitment"]), + "the apk-commitment crate and the circuit disagree about the same validator set", + ); + + let trusted = &inputs["trusted"]; + let signing_set_id = u64_of(&inputs["validatorSetId"]); + // Only the set that signed needs its commitment seeded; the other is learned from a digest. + let authority_set = |id: u64, len: u64| BlsApkBeefy::ApkAuthoritySet { + id, + len: len as u32, + apkCommitment: if id == signing_set_id { apk_commitment } else { FixedBytes::ZERO }, + }; + + let state = BlsApkBeefy::BlsApkConsensusState { + latestHeight: U256::from(u64_of(&trusted["latestHeight"])), + beefyActivationBlock: U256::from(u64_of(&trusted["beefyActivationBlock"])), + currentAuthoritySet: authority_set( + u64_of(&trusted["currentId"]), + u64_of(&trusted["currentLen"]), + ), + nextAuthoritySet: authority_set(u64_of(&trusted["nextId"]), u64_of(&trusted["nextLen"])), + }; + assert!( + state.currentAuthoritySet.apkCommitment != FixedBytes::ZERO || + state.nextAuthoritySet.apkCommitment != FixedBytes::ZERO, + "neither trusted set matches the signing set, so the client would have no commitment", + ); + + let bitlist: [U256; 5] = snark["bitlist"] + .as_array() + .expect("bitlist") + .iter() + .map(|w| U256::from_be_slice(&hex_bytes(w))) + .collect::>() + .try_into() + .expect("five words"); + + let leaf = &inputs["mmrLeaf"]; + let relay = BlsApkBeefy::BlsApkRelayChainProof { + commitment: BlsApkBeefy::Commitment { + payload: vec![BlsApkBeefy::Payload { + id: FixedBytes(*b"mh"), + data: Bytes::from(hex_bytes(&inputs["payloadMh"])), + }], + blockNumber: u64_of(&inputs["blockNumber"]) as u32, + validatorSetId: signing_set_id, + }, + bitlist, + apk: words(&inputs["apk"], 3).try_into().expect("bytes32[3]"), + apk2: words(&inputs["apk2"], 6).try_into().expect("bytes32[6]"), + apkProof: Bytes::from(hex_bytes(&snark["apkProof"])), + signature: words(&inputs["signature"], 3).try_into().expect("bytes32[3]"), + latestMmrLeaf: BlsApkBeefy::BeefyMmrLeaf { + version: 0, + parentNumber: u64_of(&leaf["parentNumber"]) as u32, + parentHash: fixed32(&leaf["parentHash"]), + nextAuthoritySet: BlsApkBeefy::AuthoritySetCommitment { + id: u64_of(&leaf["nextAuthoritySetId"]), + len: u64_of(&leaf["nextAuthoritySetLen"]) as u32, + root: fixed32(&leaf["nextAuthoritySetRoot"]), + }, + extra: fixed32(&leaf["extra"]), + leafIndex: U256::from(u64_of(&leaf["leafIndex"])), + }, + mmrProof: inputs["mmrProof"].as_array().expect("mmrProof").iter().map(fixed32).collect(), + }; + + let parachain = BlsApkBeefy::ParachainProof { + parachains: inputs["parachains"] + .as_array() + .expect("parachains") + .iter() + .map(|para| BlsApkBeefy::Parachain { + index: U256::from(u64_of(¶["index"])), + id: U256::from(u64_of(¶["id"])), + header: Bytes::from(hex_bytes(¶["header"])), + }) + .collect(), + proof: inputs["parachainProof"] + .as_array() + .expect("proof") + .iter() + .map(fixed32) + .collect(), + leafCount: U256::from(u64_of(&inputs["parachainLeafCount"])), + }; + + // SolValue, not SolType: this has to match `abi.encode(struct)` on the Solidity side. + let encoded_state = SolValue::abi_encode(&state); + let encoded_proof = <(BlsApkBeefy::BlsApkRelayChainProof, BlsApkBeefy::ParachainProof) as SolType> + ::abi_encode_params(&(relay.clone(), parachain.clone())); + + let fixtures = std::env::var("APK_FIXTURE_OUT") + .unwrap_or_else(|_| "../../../../evm/tests/foundry/fixtures".to_string()); + std::fs::write( + format!("{fixtures}/bls-apk-beefy-state.hex"), + format!("0x{}", hex::encode(&encoded_state)), + ) + .expect("write state"); + std::fs::write( + format!("{fixtures}/bls-apk-beefy-proof.hex"), + format!("0x{}", hex::encode(&encoded_proof)), + ) + .expect("write proof"); + + println!("=== apk live fixture ==="); + println!( + "signers {} | parachains {} | mmr nodes {} | block {} | apk proof {} bytes", + bitlist.iter().map(|w| w.count_ones()).sum::(), + parachain.parachains.len(), + relay.mmrProof.len(), + relay.commitment.blockNumber, + relay.apkProof.len(), + ); + println!("state {} bytes, proof {} bytes", encoded_state.len(), encoded_proof.len()); + println!("wrote {fixtures}/bls-apk-beefy-{{state,proof}}.hex"); +} diff --git a/modules/ismp/clients/beefy/Cargo.toml b/modules/ismp/clients/beefy/Cargo.toml index 46ad7a5a0..fb447975b 100644 --- a/modules/ismp/clients/beefy/Cargo.toml +++ b/modules/ismp/clients/beefy/Cargo.toml @@ -24,7 +24,6 @@ features = [ [features] default = ["std"] -bls = ["beefy-verifier/bls-crypto"] std = [ "anyhow/std", "codec/std", diff --git a/modules/ismp/clients/beefy/src/consensus.rs b/modules/ismp/clients/beefy/src/consensus.rs index 8d390a3dc..a0af6c4b1 100644 --- a/modules/ismp/clients/beefy/src/consensus.rs +++ b/modules/ismp/clients/beefy/src/consensus.rs @@ -15,8 +15,6 @@ use alloc::{boxed::Box, collections::BTreeMap, format, vec, vec::Vec}; use beefy_verifier::{error::Error as BeefyError, verify_consensus}; -#[cfg(feature = "bls")] -use beefy_verifier_primitives::PROOF_TYPE_BLS; use beefy_verifier_primitives::{ ConsensusMessage, ConsensusState, MmrProof, PROOF_TYPE_NAIVE, PROOF_TYPE_SP1, ParachainProof, Sp1BeefyProof, @@ -94,13 +92,6 @@ where .map_err(|e| BeefyError::DecodeNaiveProof(format!("{e:?}")))?; verify_consensus::(consensus_state, consensus_proof)? }, - #[cfg(feature = "bls")] - PROOF_TYPE_BLS => { - let bls_proof: beefy_verifier_primitives::BlsConsensusMessage = - codec::Decode::decode(&mut &payload[..]) - .map_err(|e| BeefyError::DecodeBlsProof(format!("{e:?}")))?; - beefy_verifier::verify_bls_consensus::(consensus_state, bls_proof)? - }, PROOF_TYPE_SP1 => { let sp1_proof: Sp1BeefyProof = codec::Decode::decode(&mut &payload[..]) .map_err(|e| BeefyError::DecodeSp1Proof(format!("{e:?}")))?; diff --git a/modules/ismp/clients/beefy/src/lib.rs b/modules/ismp/clients/beefy/src/lib.rs index 97e0dc382..11a28f44b 100644 --- a/modules/ismp/clients/beefy/src/lib.rs +++ b/modules/ismp/clients/beefy/src/lib.rs @@ -19,7 +19,7 @@ extern crate alloc; extern crate core; pub mod consensus; -pub use beefy_verifier_primitives::{PROOF_TYPE_BLS, PROOF_TYPE_NAIVE, PROOF_TYPE_SP1}; +pub use beefy_verifier_primitives::{PROOF_TYPE_NAIVE, PROOF_TYPE_SP1}; pub use consensus::{BEEFY_CONSENSUS_ID, BeefyConsensusClient}; use polkadot_sdk::*; @@ -40,22 +40,6 @@ impl beefy_verifier::EcdsaRecover for SubstrateCrypto { } } -/// Unlike `secp256k1_ecdsa_recover` above, this runs the pairing inside wasm rather than through a -/// host function. `sp-crypto-ec-utils` exposes `bls12_381_multi_miller_loop` and -/// `bls12_381_final_exponentiation`, but reaching them means working in `ark-bls12-381-ext` types -/// on a different arkworks version than `w3f-bls` pins, so it is a separate piece of work and -/// needs the host functions registered by every collator first. -#[cfg(feature = "bls")] -impl beefy_verifier::BlsAggregateVerify for SubstrateCrypto { - fn verify_aggregate( - message: &[u8], - signature: &[u8; beefy_verifier_primitives::BLS_G1_SIGNATURE_LEN], - public_keys: &[[u8; beefy_verifier_primitives::BLS_G2_PUBLIC_KEY_LEN]], - ) -> anyhow::Result { - beefy_verifier::bls::aggregate_verify(message, signature, public_keys) - } -} - /// Provides parachain tracking and SP1 vkey data to the BEEFY consensus client. pub trait BeefyClientConfig { /// Returns true if the given parachain id is tracked by this consensus client. @@ -68,10 +52,5 @@ pub trait BeefyClientConfig { /// accept. On mainnet set to `&[PROOF_TYPE_SP1]`, on testnets set to /// `&[PROOF_TYPE_NAIVE, PROOF_TYPE_SP1]`. A proof whose type byte is not listed is /// rejected with [`beefy_verifier::error::Error::UnknownProofType`] before verification. - /// - /// [`PROOF_TYPE_BLS`] additionally requires this crate's `bls` feature, and a relay chain - /// whose keyset commitment is over BLS public keys rather than Ethereum addresses. The two - /// commitments are mutually exclusive, so a client cannot accept both that and the ECDSA - /// proof types against the same consensus state. fn allowed_proof_types() -> &'static [u8]; } diff --git a/modules/pallets/beefy-consensus-proofs/src/lib.rs b/modules/pallets/beefy-consensus-proofs/src/lib.rs index 176b37af0..3d83d8d60 100644 --- a/modules/pallets/beefy-consensus-proofs/src/lib.rs +++ b/modules/pallets/beefy-consensus-proofs/src/lib.rs @@ -480,10 +480,10 @@ pub mod pallet { } Some(nonce) }, - // Only SP1 proofs are bound to a prover account. The naive and BLS paths verify + // Only SP1 proofs are bound to a prover account. The naive path verifies // signatures the relay chain's validators produced, so there is nothing - // prover-specific in them to bind and no anti-theft gate to apply. - types::PROOF_TYPE_NAIVE | types::PROOF_TYPE_BLS => None, + // prover-specific in it to bind and no anti-theft gate to apply. + types::PROOF_TYPE_NAIVE => None, _ => Err(Error::::UnknownProofType)?, }; @@ -837,15 +837,6 @@ pub mod pallet { let scale_proof: beefy_verifier_primitives::ConsensusMessage = abi_proof.into(); [&[types::PROOF_TYPE_NAIVE], scale_proof.encode().as_slice()].concat() }, - types::PROOF_TYPE_BLS => { - let abi_proof = ::abi_decode_params( - abi_payload, - ) - .map_err(|_| Error::::AbiDecodeFailed)?; - let scale_proof: beefy_verifier_primitives::BlsConsensusMessage = - abi_proof.into(); - [&[types::PROOF_TYPE_BLS], scale_proof.encode().as_slice()].concat() - }, _ => Err(Error::::UnknownProofType)?, }; diff --git a/modules/pallets/beefy-consensus-proofs/src/types.rs b/modules/pallets/beefy-consensus-proofs/src/types.rs index 945a20700..5c4e1e4d0 100644 --- a/modules/pallets/beefy-consensus-proofs/src/types.rs +++ b/modules/pallets/beefy-consensus-proofs/src/types.rs @@ -34,8 +34,6 @@ pub const ROTATION_OFFCHAIN_PREFIX: &[u8] = b"beefy_consensus_proofs::rotation:: pub const PROOF_TYPE_NAIVE: u8 = 0x00; /// Proof type byte: SP1 ZK BEEFY proof. pub const PROOF_TYPE_SP1: u8 = 0x01; -/// Proof type byte: aggregate BLS12-381 BEEFY proof. -pub const PROOF_TYPE_BLS: u8 = 0x02; fn offchain_key(prefix: &[u8], id: u64) -> Vec { let mut key = Vec::with_capacity(prefix.len() + 8); diff --git a/parachain/runtimes/gargantua/Cargo.toml b/parachain/runtimes/gargantua/Cargo.toml index 2efa97375..9c0131f55 100644 --- a/parachain/runtimes/gargantua/Cargo.toml +++ b/parachain/runtimes/gargantua/Cargo.toml @@ -34,9 +34,7 @@ ismp-sync-committee = { workspace = true } ismp-bsc = { workspace = true } ismp-parachain = { workspace = true } ismp-grandpa = { workspace = true } -# `bls` accepts aggregate BLS12-381 BEEFY proofs. Gargantua is the testnet runtime, so it carries -# the BLS verifier while the measurements that decide whether it belongs on nexus are outstanding. -ismp-beefy = { workspace = true, features = ["bls"] } +ismp-beefy = { workspace = true } ismp-parachain-runtime-api = { workspace = true } pallet-ismp-relayer = { workspace = true } pallet-ismp-host-executive = { workspace = true } diff --git a/parachain/runtimes/gargantua/src/ismp.rs b/parachain/runtimes/gargantua/src/ismp.rs index 734fda3d4..c1daf3a5f 100644 --- a/parachain/runtimes/gargantua/src/ismp.rs +++ b/parachain/runtimes/gargantua/src/ismp.rs @@ -242,8 +242,8 @@ impl ismp_beefy::BeefyClientConfig for Runtime { } fn allowed_proof_types() -> &'static [u8] { - // Testnet: accept the naive ECDSA and SP1 ZK proof formats, plus aggregate BLS12-381. - &[ismp_beefy::PROOF_TYPE_NAIVE, ismp_beefy::PROOF_TYPE_SP1, ismp_beefy::PROOF_TYPE_BLS] + // Testnet: accept the naive ECDSA and SP1 ZK proof formats. + &[ismp_beefy::PROOF_TYPE_NAIVE, ismp_beefy::PROOF_TYPE_SP1] } } diff --git a/parachain/simtests/src/lib.rs b/parachain/simtests/src/lib.rs index b6cf23e2b..76e87e8ce 100644 --- a/parachain/simtests/src/lib.rs +++ b/parachain/simtests/src/lib.rs @@ -2,7 +2,6 @@ mod base_call_filter; mod bls_relay_setup; mod intents_rpc; mod migration_test; -mod pallet_beefy_bls; mod pallet_beefy_consensus_proofs; mod pallet_fishermen; mod pallet_ismp; diff --git a/parachain/simtests/src/pallet_beefy_bls.rs b/parachain/simtests/src/pallet_beefy_bls.rs deleted file mode 100644 index dd76dee49..000000000 --- a/parachain/simtests/src/pallet_beefy_bls.rs +++ /dev/null @@ -1,261 +0,0 @@ -//! Simnode test for the aggregate BLS12-381 BEEFY proof path. -//! -//! Mirrors the naive happy path in [`crate::pallet_beefy_consensus_proofs`], but against a relay -//! chain whose BEEFY authorities hold paired `ecdsa_bls_crypto` keys and whose keyset commitment is -//! over their BLS G2 public keys. The prover builds the proof, the runtime verifies it in a single -//! pairing check, and the consensus state advances. -//! -//! This needs a BLS BEEFY relay, which no public network is. Bring one up from Parity's -//! `skalman--enable-bls-beefy-on-westend` branch, built with `--features bls-beefy-experimental` -//! and with a converter committing the validators' BLS G2 keys, then point `RELAY_WS_URL` at it. -//! Without that the test cannot run, so it is `#[ignore]`d rather than silently passing. -//! -//! RELAY_WS_URL=ws://127.0.0.1:9979 PORT=9990 \ -//! cargo test -p simtests bls_beefy -- --ignored --nocapture - -#![cfg(test)] - -use std::env; - -use alloy_sol_types::SolType; -use anyhow::anyhow; -use codec::Decode; -use polkadot_sdk::{sp_consensus_beefy, *}; -use sp_keyring::sr25519::Keyring; -use subxt::{ - backend::legacy::LegacyRpcMethods, dynamic::Value, ext::subxt_rpcs::rpc_params, OnlineClient, - PolkadotConfig, -}; -use subxt_utils::Hyperbridge; - -use beefy_prover::{ - bls::{abi::to_abi_proof, decode_paired_justification}, - Prover, -}; -use beefy_verifier_primitives::{BlsConsensusMessage, ConsensusState, PROOF_TYPE_BLS}; -use ismp_abi::{ - bls_beefy::BlsBeefy::BlsBeefyConsensusProof as SolBlsProof, - ecdsa_beefy::BeefyConsensusState as SolBeefyConsensusState, -}; - -use crate::pallet_beefy_consensus_proofs::{submit_signed, submit_sudo, BEEFY_CONSENSUS_ID}; - -/// Build a real BLS consensus proof, and the trusted state it advances from, off a live relay. -async fn build_live_bls_proof() -> Result<(ConsensusState, BlsConsensusMessage), anyhow::Error> { - let max_rpc_payload_size = 15 * 1024 * 1024; - let relay_ws_url = env::var("RELAY_WS_URL") - .map_err(|_| anyhow!("RELAY_WS_URL must point at a BLS BEEFY relay"))?; - - let (relay_client, relay_rpc_client) = - subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) - .await?; - let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); - - // Track the parachain registered on the BLS relay. `pallet-beefy-consensus-proofs` reads the - // child trie root out of a finalized parachain head, so the proof has to carry one; 4009 is - // the id gargantua's `is_parachain_tracked` allows and the one its coprocessor resolves to. - let para_ws_url = env::var("PARA_WS_URL").unwrap_or_else(|_| "ws://127.0.0.1:9991".into()); - let (para_client, para_rpc_client) = - subxt_utils::client::ws_client::(¶_ws_url, max_rpc_payload_size) - .await?; - let para_rpc = LegacyRpcMethods::::new(para_rpc_client.clone()); - - let prover = Prover { - beefy_activation_block: 0, - relay: relay_client, - relay_rpc: relay_rpc.clone(), - relay_rpc_client: relay_rpc_client.clone(), - para: para_client, - para_rpc, - para_rpc_client, - para_ids: vec![4009], - query_batch_size: Some(100), - }; - - let latest_beefy_hash: sp_core::H256 = - relay_rpc_client.request("beefy_getFinalizedHead", rpc_params!()).await?; - - let justification = beefy_justification(&relay_rpc, latest_beefy_hash).await?; - // Keeps the whole 177-byte paired signature, where the ECDSA path would slice out the first - // 65 bytes. - let signed_commitment = decode_paired_justification(&justification)?; - let latest_set_id = signed_commitment.commitment.validator_set_id; - - // Anchor the trusted state one authority set back, so this proof rotates the set. - // - // `pallet-beefy-consensus-proofs` rejects a proof that neither rotates the authority set nor - // finalizes a parachain head it has not already seen, so rotating keeps this test independent - // of how far the parachain happens to have advanced. The anchor has to be exactly one set - // back: the verifier requires the commitment to be signed by the trusted state's current or - // next set, so a further-back anchor is rejected outright with `UnknownAuthoritySet`. - let previous_beefy_hash = - previous_set_anchor(&relay_rpc, latest_beefy_hash, latest_set_id).await?; - let initial_state = - prover.get_initial_consensus_state(Some(previous_beefy_hash.into())).await?; - - let proof = prover.bls_consensus_proof(signed_commitment).await?; - - Ok((initial_state, proof)) -} - -/// The BEEFY justification attached to `hash`. -async fn beefy_justification( - relay_rpc: &LegacyRpcMethods, - hash: sp_core::H256, -) -> Result, anyhow::Error> { - let block = relay_rpc - .chain_get_block(Some(hash.into())) - .await? - .ok_or_else(|| anyhow!("missing block {hash:?}"))?; - - block - .justifications - .ok_or_else(|| anyhow!("block {hash:?} lacks justifications"))? - .into_iter() - .find_map(|j| (j.0 == sp_consensus_beefy::BEEFY_ENGINE_ID).then_some(j.1)) - .ok_or_else(|| anyhow!("block {hash:?} lacks a beefy justification")) -} - -/// Walk back to a BEEFY-justified block signed by the set immediately before `set_id`. -/// -/// Seeding the trusted state there leaves it holding `current = set_id - 1` and `next = set_id`, -/// so a commitment from `set_id` whose leaf announces `set_id + 1` advances the set by one. -async fn previous_set_anchor( - relay_rpc: &LegacyRpcMethods, - from: sp_core::H256, - set_id: u64, -) -> Result { - let mut cursor = from; - - for _ in 0..4000 { - let header = relay_rpc - .chain_get_header(Some(cursor.into())) - .await? - .ok_or_else(|| anyhow!("missing header for {cursor:?}"))?; - let parent: sp_core::H256 = header.parent_hash.into(); - if parent.is_zero() { - break; - } - - if let Ok(justification) = beefy_justification(relay_rpc, parent).await { - let signed = decode_paired_justification(&justification)?; - if signed.commitment.validator_set_id + 1 == set_id { - return Ok(parent); - } - } - - cursor = parent; - } - - Err(anyhow!("no beefy block found for the authority set preceding {set_id}")) -} - -#[tokio::test] -#[ignore] -async fn bls_beefy_proof_happy_path() -> Result<(), anyhow::Error> { - eprintln!("[stage] building live bls proof"); - let (initial_state, consensus_message) = build_live_bls_proof().await?; - - let initial_height = initial_state.latest_beefy_height; - let proof_block = consensus_message.mmr.commitment.block_number; - let signer_count = consensus_message.mmr.signers.len(); - eprintln!( - "[stage] proof built: trusted_height={initial_height} proof_block={proof_block} \ - signers={signer_count}", - ); - assert!( - proof_block > initial_height, - "proof block {proof_block} must be ahead of trusted height {initial_height}", - ); - assert!(signer_count > 0, "proof carries no signers"); - - // The keyset commitment here is over BLS public keys, so this trusted state is only usable by - // the BLS proof type. Feeding it a naive proof would fail the authority merkle proof. - let abi_state: SolBeefyConsensusState = initial_state.into(); - let abi_state_bytes = SolBeefyConsensusState::abi_encode(&abi_state); - - // The ABI carries points uncompressed, since EIP-2537 takes no other form and offers no - // decompression precompile. `to_abi_proof` does that expansion. - let abi_proof: SolBlsProof = to_abi_proof(consensus_message)?; - let abi_proof_bytes = ::abi_encode_params(&abi_proof); - // The runtime decodes this payload with the same type and method, so a failure here is an - // encoding bug on this side rather than anything the pallet did. - ::abi_decode_params(&abi_proof_bytes) - .map_err(|e| anyhow!("proof does not round-trip through abi_decode_params: {e}"))?; - let mut wire_proof = Vec::with_capacity(1 + abi_proof_bytes.len()); - wire_proof.push(PROOF_TYPE_BLS); - wire_proof.extend_from_slice(&abi_proof_bytes); - eprintln!( - "[stage] abi-encoded: state={} bytes proof={} bytes", - abi_state_bytes.len(), - wire_proof.len(), - ); - - let port = env::var("PORT").unwrap_or_else(|_| "9990".into()); - let url = format!("ws://127.0.0.1:{port}"); - let (client, rpc_client) = - subxt_utils::client::ws_client::(&url, u32::MAX).await?; - - let init_call = subxt::dynamic::tx( - "BeefyConsensusProofs", - "initialize_state", - vec![Value::from_bytes(&abi_state_bytes)], - ); - eprintln!("[stage] submitting initialize_state via sudo"); - submit_sudo(&client, &rpc_client, init_call).await?; - - // Same reason as the naive test: keep `ProofReward` at zero so the reward path does not try to - // draw from an unfunded treasury account. - let zero_reward = - subxt::dynamic::tx("BeefyConsensusProofs", "set_proof_reward", vec![Value::u128(0)]); - submit_sudo(&client, &rpc_client, zero_reward).await?; - - let submit_call = subxt::dynamic::tx( - "BeefyConsensusProofs", - "submit_proof", - vec![Value::from_bytes(&wire_proof)], - ); - eprintln!("[stage] submitting submit_proof signed by Bob"); - submit_signed(&client, &rpc_client, submit_call, Keyring::Bob).await?; - eprintln!("[stage] submit_proof finalized"); - - // The consensus state is the thing that matters: it only advances if the aggregate pairing - // check, the keyset merkle multi-proof and the MMR leaf proof all passed inside the runtime. - let final_state = fetch_beefy_consensus_state(&client).await?; - assert_eq!( - final_state.latest_beefy_height, proof_block, - "consensus state should have advanced to the proven block", - ); - assert!( - final_state.latest_beefy_height > initial_height, - "consensus state did not move forward", - ); - - eprintln!( - "[ok] BLS BEEFY verified in-runtime: {signer_count} signers aggregated, \ - height {initial_height} -> {proof_block}" - ); - - Ok(()) -} - -/// Read back the BEEFY consensus state pallet-ismp stores. -async fn fetch_beefy_consensus_state( - client: &OnlineClient, -) -> Result { - let addr = subxt::dynamic::storage( - "Ismp", - "ConsensusStates", - vec![Value::from_bytes(BEEFY_CONSENSUS_ID)], - ); - let raw = client - .storage() - .at_latest() - .await? - .fetch(&addr) - .await? - .ok_or_else(|| anyhow!("no beefy consensus state stored"))? - .as_type::>()?; - - ConsensusState::decode(&mut &raw[..]).map_err(|e| anyhow!("decode consensus state: {e:?}")) -} From 7f6a2cf931b19e2437c5c29785ec030d1d358c29 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Sat, 8 Aug 2026 20:13:05 +0100 Subject: [PATCH 13/48] nit --- Cargo.lock | 1 + modules/consensus/beefy/primitives/src/lib.rs | 32 ++++++++++++++ modules/consensus/beefy/prover/src/bls.rs | 40 ++++-------------- modules/consensus/beefy/prover/src/relay.rs | 4 +- modules/consensus/beefy/verifier/src/test.rs | 6 +-- modules/pallets/beefy-apk-digest/Cargo.toml | 2 + modules/pallets/beefy-apk-digest/src/lib.rs | 42 ++++--------------- 7 files changed, 57 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 223e12bf6..a07f5b4dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14666,6 +14666,7 @@ dependencies = [ "apk-commitment", "ark-bls12-381 0.4.0", "ark-serialize 0.4.2", + "beefy-verifier-primitives", "cumulus-pallet-parachain-system", "hex", "log", diff --git a/modules/consensus/beefy/primitives/src/lib.rs b/modules/consensus/beefy/primitives/src/lib.rs index e67ec558e..b7c9385c3 100644 --- a/modules/consensus/beefy/primitives/src/lib.rs +++ b/modules/consensus/beefy/primitives/src/lib.rs @@ -132,6 +132,38 @@ pub const BLS_G1_SIGNATURE_LEN: usize = 48; /// Size of a compressed BLS12-381 G2 point, the group BEEFY public keys live in. pub const BLS_G2_PUBLIC_KEY_LEN: usize = 96; +/// Wire size of a paired `ecdsa_bls_crypto` BEEFY key: `ecdsa(33) || G1(48) || G2(96)`. +pub const PAIRED_AUTHORITY_LEN: usize = 33 + BLS_G1_SIGNATURE_LEN + BLS_G2_PUBLIC_KEY_LEN; + +/// Where the BLS halves start, after the ECDSA key. +const PAIRED_G1_OFFSET: usize = 33; +const PAIRED_G2_OFFSET: usize = PAIRED_G1_OFFSET + BLS_G1_SIGNATURE_LEN; + +/// A BEEFY authority key on a relay chain that uses paired `ecdsa_bls_crypto`, exactly as the +/// relay stores it. +/// +/// `DoublePublicKey` publishes the same secret in both BLS groups, so a validator has a G1 and a +/// G2 half describing one key. BEEFY verifies signatures against the G2 half while an APK proof +/// aggregates the G1 halves, which is why both accessors exist. +#[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode)] +pub struct PairedAuthority(pub [u8; PAIRED_AUTHORITY_LEN]); + +impl PairedAuthority { + /// The compressed G1 half. + pub fn g1(&self) -> [u8; BLS_G1_SIGNATURE_LEN] { + let mut out = [0u8; BLS_G1_SIGNATURE_LEN]; + out.copy_from_slice(&self.0[PAIRED_G1_OFFSET..PAIRED_G2_OFFSET]); + out + } + + /// The compressed G2 half. + pub fn g2(&self) -> [u8; BLS_G2_PUBLIC_KEY_LEN] { + let mut out = [0u8; BLS_G2_PUBLIC_KEY_LEN]; + out.copy_from_slice(&self.0[PAIRED_G2_OFFSET..PAIRED_AUTHORITY_LEN]); + out + } +} + /// The relay chain half of a BLS BEEFY update: the signed commitment, the aggregate signature, and /// the MMR leaf it attests to. /// diff --git a/modules/consensus/beefy/prover/src/bls.rs b/modules/consensus/beefy/prover/src/bls.rs index 76d1bedbf..8e3ae3f5c 100644 --- a/modules/consensus/beefy/prover/src/bls.rs +++ b/modules/consensus/beefy/prover/src/bls.rs @@ -31,7 +31,7 @@ use subxt::{backend::legacy::LegacyRpcMethods, Config}; use subxt_core::config::HashFor; use beefy_verifier_primitives::{ - BlsConsensusMessage, BlsMmrProof, BLS_G1_SIGNATURE_LEN, BLS_G2_PUBLIC_KEY_LEN, + BlsConsensusMessage, BlsMmrProof, PairedAuthority, BLS_G1_SIGNATURE_LEN, BLS_G2_PUBLIC_KEY_LEN, }; use crate::{ @@ -40,21 +40,13 @@ use crate::{ Prover, BEEFY_AUTHORITIES, }; -/// Wire size of a paired (ECDSA, BLS12-381) BEEFY key or signature. +/// Wire size of a paired (ECDSA, BLS12-381) BEEFY signature. pub const PAIRED_LEN: usize = 177; /// Offset of the BLS G1 signature within a paired signature: the ECDSA half is 65 bytes, then the /// `DoubleSignature` begins with its 48-byte G1 point. const PAIRED_SIGNATURE_G1_OFFSET: usize = 65; -/// Offset of the BLS G1 public key within a paired public key: the ECDSA half is 33 bytes, then -/// the `DoublePublicKey` opens with its 48-byte G1 point. -const PAIRED_PUBLIC_G1_OFFSET: usize = 33; - -/// Offset of the BLS G2 public key within a paired public key: the ECDSA half is 33 bytes, then -/// the `DoublePublicKey` is `G1 (48) || G2 (96)`, so G2 starts 48 bytes further in. -const PAIRED_PUBLIC_G2_OFFSET: usize = 33 + 48; - /// A paired (ECDSA, BLS12-381) signature exactly as SCALE-encoded on the wire. #[derive(Clone)] pub struct PairedSignature(pub [u8; PAIRED_LEN]); @@ -97,17 +89,9 @@ pub async fn beefy_g2_authorities( .await? .ok_or_else(|| anyhow!("No beefy authorities found!"))?; - let paired = Vec::<[u8; PAIRED_LEN]>::decode(&mut data.as_ref())?; - - Ok(paired - .into_iter() - .map(|key| { - let mut g2 = [0u8; BLS_G2_PUBLIC_KEY_LEN]; - g2.copy_from_slice( - &key[PAIRED_PUBLIC_G2_OFFSET..PAIRED_PUBLIC_G2_OFFSET + BLS_G2_PUBLIC_KEY_LEN], - ); - g2 - }) + Ok(Vec::::decode(&mut data.as_ref())? + .iter() + .map(PairedAuthority::g2) .collect()) } @@ -125,17 +109,9 @@ pub async fn beefy_g1_authorities( .await? .ok_or_else(|| anyhow!("No beefy authorities found!"))?; - let paired = Vec::<[u8; PAIRED_LEN]>::decode(&mut data.as_ref())?; - - Ok(paired - .into_iter() - .map(|key| { - let mut g1 = [0u8; BLS_G1_SIGNATURE_LEN]; - g1.copy_from_slice( - &key[PAIRED_PUBLIC_G1_OFFSET..PAIRED_PUBLIC_G1_OFFSET + BLS_G1_SIGNATURE_LEN], - ); - g1 - }) + Ok(Vec::::decode(&mut data.as_ref())? + .iter() + .map(PairedAuthority::g1) .collect()) } diff --git a/modules/consensus/beefy/prover/src/relay.rs b/modules/consensus/beefy/prover/src/relay.rs index 509ab6ab4..8401e5e29 100644 --- a/modules/consensus/beefy/prover/src/relay.rs +++ b/modules/consensus/beefy/prover/src/relay.rs @@ -83,8 +83,8 @@ pub async fn fetch_latest_beefy_justification( /// On a plain-ECDSA relay the signatures decode directly. With the `bls` feature (a relay whose /// BEEFY authorities use the paired `ecdsa_bls_crypto` key type) the on-wire signatures are /// 177-byte paired signatures; we decode them and keep only the ECDSA half (the first 65 bytes, a -/// keccak-ECDSA recoverable signature). Both paths return the same type, so everything downstream -/// — commitment hashing, signature recovery, the ECDSA verifier — is unchanged. +/// keccak-ECDSA recoverable signature). Both paths return the same type, so commitment hashing, +/// signature recovery and the ECDSA verifier all stay as they are. pub fn decode_beefy_justification( bytes: &[u8], ) -> Result, anyhow::Error> { diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index 16f6b79a6..15729cd64 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -462,8 +462,8 @@ fn rejects_sp1_proof_carrying_a_stale_mmr_leaf() { /// End-to-end verification against a relay chain whose BEEFY authorities use the paired /// (ECDSA, BLS12-381) `ecdsa_bls_crypto` key type. Requires the `beefy-prover/bls` feature, which /// makes the prover keep the ECDSA half of each 177-byte paired signature and authority key. The -/// verifier itself is unchanged: this is "Option A" — a BLS-BEEFY relay is verified through the -/// existing ECDSA path. +/// verifier itself is unchanged: this is "Option A", where a BLS-BEEFY relay is verified through +/// the existing ECDSA path. /// /// RELAY_WS_URL=ws://127.0.0.1:9977 \ /// cargo test -p beefy-verifier --features bls test_verify_consensus_bls -- --ignored @@ -526,7 +526,7 @@ async fn test_verify_consensus_bls() { } assert!(!previous.is_zero(), "no previous beefy block found"); - // Initial trusted state via the prover — exercises the folded BLS justification decode. + // Initial trusted state via the prover, which exercises the folded BLS justification decode. let trusted_state = prover.get_initial_consensus_state(Some(previous)).await.unwrap(); // Latest justification -> signed commitment with ECDSA-half signatures (folded BLS decode). diff --git a/modules/pallets/beefy-apk-digest/Cargo.toml b/modules/pallets/beefy-apk-digest/Cargo.toml index 5c62a24e1..0e123cc2d 100644 --- a/modules/pallets/beefy-apk-digest/Cargo.toml +++ b/modules/pallets/beefy-apk-digest/Cargo.toml @@ -13,6 +13,7 @@ scale-info = { workspace = true } log = { workspace = true } apk-commitment = { workspace = true, default-features = false } +beefy-verifier-primitives = { workspace = true, default-features = false } cumulus-pallet-parachain-system = { workspace = true, default-features = false } ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = false } @@ -34,6 +35,7 @@ std = [ "log/std", "polkadot-sdk/std", "apk-commitment/std", + "beefy-verifier-primitives/std", "cumulus-pallet-parachain-system/std", "ark-bls12-381/std", "ark-serialize/std", diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index ccc404512..5176cf173 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -24,8 +24,6 @@ //! The commitment is expensive. A full 1024-slot set is roughly 420ms of wasm, which does not fit //! in a block, so it is absorbed a chunk at a time across blocks and published once complete. The //! authority set for the next session is known a session ahead, which is what makes that possible. -//! -//! What still has to be decided before this is more than a skeleton is marked `DECIDE` below. #![cfg_attr(not(feature = "std"), no_std)] @@ -36,6 +34,7 @@ use alloc::vec::Vec; use apk_commitment::{PartialCommitment, NUM_VALIDATORS}; use ark_bls12_381::G1Affine; use ark_serialize::CanonicalDeserialize; +use beefy_verifier_primitives::{PairedAuthority, BLS_G1_SIGNATURE_LEN}; use codec::{Decode, Encode, MaxEncodedLen}; use cumulus_pallet_parachain_system::RelayChainStateProof; use frame_support::weights::Weight; @@ -44,13 +43,6 @@ use scale_info::TypeInfo; pub use pallet::*; -/// Wire size of a paired (ECDSA, BLS12-381) BEEFY key: `ecdsa(33) || G1(48) || G2(96)`. -const PAIRED_LEN: usize = 177; -/// Offset of the BLS G1 half within a paired key. -const PAIRED_G1_OFFSET: usize = 33; -/// Size of a compressed G1 point. -const G1_LEN: usize = 48; - /// `Beefy::NextAuthorities` on the relay chain. /// /// The *next* set, not the current one, and the distinction is what makes the scheme work. A client @@ -90,20 +82,6 @@ pub struct ApkCommitmentDigest { pub commitment: [u8; 32], } -/// A paired BEEFY authority key exactly as the relay chain stores it. -#[derive(Clone, Encode, Decode, TypeInfo)] -pub struct PairedAuthority(pub [u8; PAIRED_LEN]); - -impl PairedAuthority { - /// The BLS G1 half, which is what the APK circuit consumes. `DoublePublicKey` publishes the - /// same secret in both groups, so this is the counterpart of the G2 key BEEFY verifies with. - pub fn g1(&self) -> [u8; G1_LEN] { - let mut out = [0u8; G1_LEN]; - out.copy_from_slice(&self.0[PAIRED_G1_OFFSET..PAIRED_G1_OFFSET + G1_LEN]); - out - } -} - impl ApkCommitmentDigest { /// Pull the commitment out of a header's digest logs, if this pallet wrote one into it. /// @@ -293,7 +271,7 @@ pub mod pallet { } /// The G1 halves of the relay chain's *next* BEEFY authority set. - fn relay_beefy_g1_keys() -> Result, Error> { + fn relay_beefy_g1_keys() -> Result, Error> { let authorities: Vec = Self::relay_state()? .read_entry(&RELAY_BEEFY_NEXT_AUTHORITIES, None) .map_err(|_| Error::::KeyNotProven)?; @@ -378,7 +356,7 @@ pub struct MalformedKey; /// `state` is the Merkle-Damgard state carried between blocks; pass /// `PartialCommitment::new().to_bytes()` to start. pub fn absorb_slots( - keys: &[[u8; G1_LEN]], + keys: &[[u8; BLS_G1_SIGNATURE_LEN]], from: usize, take: usize, state: [u8; 32], @@ -407,24 +385,24 @@ mod tests { "a3948b7bd16acfa3b7a113826a1a8b192c2c462f31ddd9dc90131f5b014d85ff9669ad1a686f33f89e6b0d821761b2cc", ]; - fn relay_keys() -> Vec<[u8; G1_LEN]> { + fn relay_keys() -> Vec<[u8; BLS_G1_SIGNATURE_LEN]> { RELAY_KEYS .iter() .map(|h| { - let mut out = [0u8; G1_LEN]; + let mut out = [0u8; BLS_G1_SIGNATURE_LEN]; out.copy_from_slice(&hex::decode(h).unwrap()); out }) .collect() } - fn expected_commitment(keys: &[[u8; G1_LEN]]) -> [u8; 32] { + fn expected_commitment(keys: &[[u8; BLS_G1_SIGNATURE_LEN]]) -> [u8; 32] { let points: Vec = keys.iter().map(|k| G1Affine::deserialize_compressed(&k[..]).unwrap()).collect(); public_keys_commitment_bytes(&padded_to_circuit_width(&points)) } - fn run(keys: &[[u8; G1_LEN]], slots_per_block: usize) -> [u8; 32] { + fn run(keys: &[[u8; BLS_G1_SIGNATURE_LEN]], slots_per_block: usize) -> [u8; 32] { let mut state = PartialCommitment::new().to_bytes(); let mut absorbed = 0usize; while absorbed < NUM_VALIDATORS { @@ -484,7 +462,7 @@ mod tests { ) .unwrap(); key[0] |= 0x80; // compressed form, so the x bytes are actually parsed - let mut fixed = [0u8; G1_LEN]; + let mut fixed = [0u8; BLS_G1_SIGNATURE_LEN]; fixed.copy_from_slice(&key); let state = PartialCommitment::new().to_bytes(); @@ -498,14 +476,12 @@ mod tests { fn an_identity_key_is_valid_input() { let mut encoded = Vec::new(); G1Affine::identity().serialize_compressed(&mut encoded).unwrap(); - let mut key = [0u8; G1_LEN]; + let mut key = [0u8; BLS_G1_SIGNATURE_LEN]; key.copy_from_slice(&encoded); let state = PartialCommitment::new().to_bytes(); assert!(absorb_slots(&[key], 0, 1, state).is_ok()); } - // ── rotation ──────────────────────────────────────────────────────────────────────────── - const SET_A: [u8; 32] = [0xaa; 32]; const SET_B: [u8; 32] = [0xbb; 32]; From b3bc5d79ec105262ab8ba12e07c05c694940f102 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 11 Aug 2026 13:42:23 +0100 Subject: [PATCH 14/48] verify beefy consensus with an aggregate public key proof in the runtime and produce one from the prover --- Cargo.lock | 76 ++++ Cargo.toml | 1 + evm/rust/src/conversions.rs | 172 +++++++++ modules/consensus/beefy/primitives/Cargo.toml | 1 + modules/consensus/beefy/primitives/src/lib.rs | 120 ++++++ modules/consensus/beefy/prover/src/bls.rs | 27 +- modules/consensus/beefy/verifier/Cargo.toml | 26 ++ modules/consensus/beefy/verifier/src/apk.rs | 355 ++++++++++++++++++ modules/consensus/beefy/verifier/src/error.rs | 29 ++ modules/consensus/beefy/verifier/src/lib.rs | 4 +- modules/ismp/clients/beefy/Cargo.toml | 1 + modules/ismp/clients/beefy/src/consensus.rs | 28 +- modules/ismp/clients/beefy/src/lib.rs | 10 +- modules/pallets/beefy-apk-digest/src/lib.rs | 38 +- .../pallets/beefy-consensus-proofs/src/lib.rs | 207 +++++++--- .../beefy-consensus-proofs/src/types.rs | 2 + .../beefy-consensus-proofs/src/weights.rs | 5 + modules/pallets/testsuite/src/runtime.rs | 1 + parachain/runtimes/gargantua/Cargo.toml | 2 +- parachain/runtimes/gargantua/src/ismp.rs | 8 +- parachain/runtimes/gargantua/src/lib.rs | 1 + .../weights/pallet_beefy_consensus_proofs.rs | 12 + parachain/runtimes/nexus/src/ismp.rs | 8 +- parachain/runtimes/nexus/src/lib.rs | 1 + .../weights/pallet_beefy_consensus_proofs.rs | 12 + tesseract/consensus/beefy/Cargo.toml | 2 + tesseract/consensus/beefy/apk/Cargo.toml | 42 +++ tesseract/consensus/beefy/apk/src/lib.rs | 315 ++++++++++++++++ tesseract/consensus/beefy/apk/src/local.rs | 103 +++++ tesseract/consensus/beefy/src/prover.rs | 35 +- tesseract/prover/Cargo.toml | 2 +- 31 files changed, 1542 insertions(+), 104 deletions(-) create mode 100644 modules/consensus/beefy/verifier/src/apk.rs create mode 100644 tesseract/consensus/beefy/apk/Cargo.toml create mode 100644 tesseract/consensus/beefy/apk/src/lib.rs create mode 100644 tesseract/consensus/beefy/apk/src/local.rs diff --git a/Cargo.lock b/Cargo.lock index a07f5b4dd..234ee1724 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -951,6 +951,29 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "apk-beefy" +version = "0.1.0" +dependencies = [ + "alloy-primitives 1.5.7", + "anyhow", + "apk-commitment", + "ark-bls12-381 0.4.0", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-serialize 0.5.0", + "async-trait", + "beefy-prover", + "beefy-verifier-primitives", + "gnark-apk-prover", + "hex", + "ismp-abi", + "sp-consensus-beefy", + "subxt 0.42.1", + "tokio", +] + [[package]] name = "apk-commitment" version = "0.1.0" @@ -2946,13 +2969,18 @@ dependencies = [ "anyhow", "apk-commitment", "ark-bls12-381 0.4.0", + "ark-bls12-381 0.5.0", "ark-ec 0.4.2", + "ark-ec 0.5.0", "ark-ff 0.4.2", + "ark-ff 0.5.0", "ark-serialize 0.4.2", + "ark-serialize 0.5.0", "beefy-prover", "beefy-verifier-primitives", "ckb-merkle-mountain-range", "futures", + "gnark-plonk-verifier 0.1.0 (git+https://github.com/polytope-labs/gnark-apk-proofs?rev=48e6aa504994a58fb41465e22eb90b1df6a190f8)", "hex", "hex-literal 0.4.1", "ismp", @@ -8639,6 +8667,53 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "gnark-apk-ffi" +version = "0.1.0" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4#ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4" + +[[package]] +name = "gnark-apk-prover" +version = "0.1.0" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4#ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4" +dependencies = [ + "ark-bls12-381 0.5.0", + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "gnark-apk-ffi", + "gnark-plonk-verifier 0.1.0 (git+https://github.com/polytope-labs/gnark-apk-proofs?rev=ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4)", + "thiserror 2.0.18", +] + +[[package]] +name = "gnark-plonk-verifier" +version = "0.1.0" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=48e6aa504994a58fb41465e22eb90b1df6a190f8#48e6aa504994a58fb41465e22eb90b1df6a190f8" +dependencies = [ + "ark-bls12-381 0.5.0", + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "once_cell", + "sha2 0.10.9", + "sha3 0.10.8", + "thiserror 2.0.18", +] + +[[package]] +name = "gnark-plonk-verifier" +version = "0.1.0" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4#ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4" +dependencies = [ + "ark-bls12-381 0.5.0", + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "sha2 0.10.9", + "sha3 0.10.8", + "thiserror 2.0.18", +] + [[package]] name = "governor" version = "0.6.3" @@ -28431,6 +28506,7 @@ dependencies = [ "alloy-primitives 1.5.7", "alloy-sol-types 1.5.7", "anyhow", + "apk-beefy", "async-stream", "async-trait", "beefy-prover", diff --git a/Cargo.toml b/Cargo.toml index 1e438b531..4ee8126b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,7 @@ members = [ "tesseract/consensus/grandpa", "tesseract/consensus/integration-tests", "tesseract/consensus/beefy", + "tesseract/consensus/beefy/apk", "tesseract/consensus/beefy/zk", "tesseract/prover", "tesseract/consensus/admin-relayer", diff --git a/evm/rust/src/conversions.rs b/evm/rust/src/conversions.rs index 312c96201..2d3d0d96a 100644 --- a/evm/rust/src/conversions.rs +++ b/evm/rust/src/conversions.rs @@ -393,6 +393,178 @@ mod beefy { } } + // `sol!` emits a distinct set of Rust types per binding, so the shared BEEFY structs appear + // again under `BlsApkBeefy` even though the Solidity definitions are the same ones. These + // bridge those duplicates onto the `Beefy` types so the conversions to the SCALE primitives + // stay single-sourced above. + mod apk_bridge { + use super::*; + use crate::bls_apk_beefy::BlsApkBeefy; + + impl From for Payload { + fn from(value: BlsApkBeefy::Payload) -> Self { + Payload { id: value.id, data: value.data } + } + } + + impl From for Commitment { + fn from(value: BlsApkBeefy::Commitment) -> Self { + Commitment { + payload: value.payload.into_iter().map(Into::into).collect(), + blockNumber: value.blockNumber, + validatorSetId: value.validatorSetId, + } + } + } + + impl From for AuthoritySetCommitment { + fn from(value: BlsApkBeefy::AuthoritySetCommitment) -> Self { + AuthoritySetCommitment { id: value.id, len: value.len, root: value.root } + } + } + + impl From for BeefyMmrLeaf { + fn from(value: BlsApkBeefy::BeefyMmrLeaf) -> Self { + BeefyMmrLeaf { + version: value.version, + parentNumber: value.parentNumber, + parentHash: value.parentHash, + nextAuthoritySet: value.nextAuthoritySet.into(), + extra: value.extra, + leafIndex: value.leafIndex, + } + } + } + + impl From for Parachain { + fn from(value: BlsApkBeefy::Parachain) -> Self { + Parachain { index: value.index, id: value.id, header: value.header } + } + } + + impl From for ParachainProof { + fn from(value: BlsApkBeefy::ParachainProof) -> Self { + ParachainProof { + parachains: value.parachains.into_iter().map(Into::into).collect(), + proof: value.proof, + leafCount: value.leafCount, + } + } + } + } + + /// Decoded from calldata a relayer supplied, so every width is checked rather than assumed. + impl TryFrom + for beefy_verifier_primitives::ApkConsensusMessage + { + type Error = &'static str; + + fn try_from( + value: crate::bls_apk_beefy::BlsApkBeefy::BlsApkBeefyConsensusProof, + ) -> Result { + let relay = value.relay; + let leaf: BeefyMmrLeaf = relay.latestMmrLeaf.into(); + let leaf_index: u64 = + leaf.leafIndex.try_into().map_err(|_| "mmr leaf index out of bounds")?; + + let commitment: Commitment = relay.commitment.into(); + let parachain: ParachainProof = value.parachain.into(); + + let mut bitlist = [[0u8; 32]; beefy_verifier_primitives::APK_BITLIST_WORDS]; + for (word, out) in relay.bitlist.iter().zip(bitlist.iter_mut()) { + *out = word.to_be_bytes(); + } + + Ok(beefy_verifier_primitives::ApkMmrProof { + commitment: commitment.into(), + bitlist, + apk: flatten(&relay.apk), + apk2: flatten(&relay.apk2), + apk_proof: relay.apkProof.to_vec(), + signature: flatten(&relay.signature), + latest_mmr_leaf: leaf.into(), + mmr_proof: LeafProof { + leaf_indices: vec![leaf_index], + leaf_count: leaf_index.saturating_add(1), + items: relay.mmrProof.into_iter().map(|h| H256(h.0)).collect(), + }, + }) + .map(|mmr| beefy_verifier_primitives::ApkConsensusMessage { + mmr, + parachain: parachain.into(), + }) + } + } + + /// The direction tooling needs when bootstrapping a chain: the starting set's commitment has + /// to be handed to `initialize_apk_state` in this encoding, since it is otherwise only ever + /// learned from a header digest. + impl From + for crate::bls_apk_beefy::BlsApkBeefy::BlsApkConsensusState + { + fn from(value: beefy_verifier_primitives::ApkConsensusState) -> Self { + let authority_set = |set: beefy_verifier_primitives::ApkAuthoritySet| { + crate::bls_apk_beefy::BlsApkBeefy::ApkAuthoritySet { + id: set.id, + len: set.len, + apkCommitment: FixedBytes(set.apk_commitment.0), + } + }; + + crate::bls_apk_beefy::BlsApkBeefy::BlsApkConsensusState { + latestHeight: value.latest_beefy_height.to_u256(), + beefyActivationBlock: value.beefy_activation_block.to_u256(), + currentAuthoritySet: authority_set(value.current_authorities), + nextAuthoritySet: authority_set(value.next_authorities), + } + } + } + + /// The mmr root is not part of the initial state, since nothing has been proven yet. It is + /// filled by the first update that verifies. + impl TryFrom + for beefy_verifier_primitives::ApkConsensusState + { + type Error = &'static str; + + fn try_from( + value: crate::bls_apk_beefy::BlsApkBeefy::BlsApkConsensusState, + ) -> Result { + let authority_set = |set: crate::bls_apk_beefy::BlsApkBeefy::ApkAuthoritySet| { + beefy_verifier_primitives::ApkAuthoritySet { + id: set.id, + len: set.len, + apk_commitment: H256(set.apkCommitment.0), + } + }; + + Ok(beefy_verifier_primitives::ApkConsensusState { + latest_beefy_height: value + .latestHeight + .try_into() + .map_err(|_| "latest height out of bounds")?, + beefy_activation_block: value + .beefyActivationBlock + .try_into() + .map_err(|_| "beefy activation block out of bounds")?, + mmr_root_hash: H256::zero(), + current_authorities: authority_set(value.currentAuthoritySet), + next_authorities: authority_set(value.nextAuthoritySet), + }) + } + } + + /// Curve points reach the circuit's verifier as raw coordinates packed into 32 byte words. + fn flatten( + words: &[FixedBytes<32>; WORDS], + ) -> [u8; BYTES] { + let mut out = [0u8; BYTES]; + for (word, chunk) in words.iter().zip(out.chunks_mut(32)) { + chunk.copy_from_slice(&word.0); + } + out + } + impl From for Sp1BeefyProof { fn from(value: crate::sp1_beefy::SP1Beefy::SP1BeefyProof) -> Self { Sp1BeefyProof { diff --git a/modules/consensus/beefy/primitives/Cargo.toml b/modules/consensus/beefy/primitives/Cargo.toml index e4d2f7deb..3bb0c556a 100644 --- a/modules/consensus/beefy/primitives/Cargo.toml +++ b/modules/consensus/beefy/primitives/Cargo.toml @@ -23,6 +23,7 @@ features = [ "sp-core", "sp-consensus-beefy", "sp-mmr-primitives", + "sp-runtime", ] [features] diff --git a/modules/consensus/beefy/primitives/src/lib.rs b/modules/consensus/beefy/primitives/src/lib.rs index b7c9385c3..eccc80bbe 100644 --- a/modules/consensus/beefy/primitives/src/lib.rs +++ b/modules/consensus/beefy/primitives/src/lib.rs @@ -126,6 +126,9 @@ pub const PROOF_TYPE_NAIVE: u8 = 0x00; /// Proof type identifier for SP1 ZK proofs pub const PROOF_TYPE_SP1: u8 = 0x01; +/// Proof type identifier for aggregate public key proofs +pub const PROOF_TYPE_APK: u8 = 0x02; + /// Size of a compressed BLS12-381 G1 point, the group BEEFY signatures live in. pub const BLS_G1_SIGNATURE_LEN: usize = 48; @@ -164,6 +167,123 @@ impl PairedAuthority { } } +/// Size of a curve point as the APK circuit's verifier takes it: raw big-endian coordinates, 48 +/// bytes each, with no padding. Not the EIP-2537 layout. +pub const APK_G1_LEN: usize = 96; +/// The same for G2, four coordinates. +pub const APK_G2_LEN: usize = 192; +/// The participation bitlist, one bit per validator slot. +pub const APK_BITLIST_WORDS: usize = 5; + +/// Engine id of the digest item carrying an authority set's APK commitment. +pub const APK_ENGINE_ID: [u8; 4] = *b"APKC"; + +/// The payload hyperbridge writes into a header digest once it has hashed an authority set. +/// +/// This is a wire format shared by the runtime that writes it and every verifier that reads it. A +/// client that has already authenticated a hyperbridge header, through the parachain heads root in +/// the BEEFY MMR leaf, can take the commitment straight out of that header with no further proof. +#[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode)] +pub struct ApkCommitmentDigest { + /// The BEEFY validator set the keys belong to, which is the relay's current set id plus one, + /// since the commitment describes the next set. + pub set_id: u64, + /// Poseidon2 over the set's G1 keys, padded to the circuit width with the identity point. + pub commitment: [u8; 32], +} + +impl ApkCommitmentDigest { + /// Pull the commitment out of a header's digest logs, if one is there. + /// + /// Returns the first matching item, since at most one is ever written per block. A malformed + /// payload reads as absent rather than as an error, on the grounds that some other producer + /// wrote under this engine id and the honest answer is that there is no commitment here. + pub fn find_in(digest: &sp_runtime::generic::Digest) -> Option { + digest.logs().iter().find_map(|log| match log { + sp_runtime::DigestItem::Consensus(id, payload) if *id == APK_ENGINE_ID => + Self::decode(&mut &payload[..]).ok(), + _ => None, + }) + } +} + +/// An authority set identified by a commitment to its keys rather than a merkle root over them. +/// +/// The commitment is Poseidon2 over the validators' G1 keys, which is what the APK circuit binds +/// to. It cannot be read off the relay chain, so it arrives in a header digest and is empty until +/// one has been seen. +#[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode, Default)] +pub struct ApkAuthoritySet { + /// Id of the set + pub id: u64, + /// Number of validators in the set + pub len: u32, + /// Poseidon2 over the set's G1 keys, zero until a digest supplies it + pub apk_commitment: H256, +} + +/// Consensus state for BEEFY verified through an aggregate public key proof. +#[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode, Default)] +pub struct ApkConsensusState { + /// Latest beefy height + pub latest_beefy_height: u32, + /// Height at which beefy was activated + pub beefy_activation_block: u32, + /// Latest mmr root hash + pub mmr_root_hash: H256, + /// Authorities for the current session + pub current_authorities: ApkAuthoritySet, + /// Authorities for the next session + pub next_authorities: ApkAuthoritySet, +} + +impl ApkConsensusState { + /// Record `commitment` for `set_id` if it names a set this state knows and has no commitment + /// for yet. Called with whatever a verified header carried, so an unknown set is ignored + /// rather than treated as an error. + pub fn learn_commitment(&mut self, set_id: u64, commitment: H256) { + for set in [&mut self.next_authorities, &mut self.current_authorities] { + if set.id == set_id && set.apk_commitment.is_zero() { + set.apk_commitment = commitment; + return; + } + } + } +} + +/// The relay chain half of an update proven by an aggregate public key proof. +/// +/// Nothing here grows with the number of signers. The bitlist is fixed width, the aggregates are +/// one point each, and the proof is constant size. +#[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode)] +pub struct ApkMmrProof { + /// The commitment that was signed + pub commitment: sp_consensus_beefy::Commitment, + /// Which validators signed, one bit each, big-endian words + pub bitlist: [[u8; 32]; APK_BITLIST_WORDS], + /// Aggregate of the signers' G1 keys, proven correct by [`Self::apk_proof`] + pub apk: [u8; APK_G1_LEN], + /// The same aggregate in G2, bound to `apk` by the pairing check + pub apk2: [u8; APK_G2_LEN], + /// PLONK proof that `apk` aggregates exactly the validators named in `bitlist` + pub apk_proof: Vec, + /// Sum of the signers' signatures, a G1 point + pub signature: [u8; APK_G1_LEN], + /// Latest leaf added to mmr + pub latest_mmr_leaf: MmrLeaf, + /// Proof for the latest mmr leaf + pub mmr_proof: sp_mmr_primitives::LeafProof, +} + +/// A BEEFY consensus update proven by an aggregate public key proof. +#[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode)] +pub struct ApkConsensusMessage { + /// Parachain headers + pub parachain: ParachainProof, + /// Proof for the finalized mmr root + pub mmr: ApkMmrProof, +} + /// The relay chain half of a BLS BEEFY update: the signed commitment, the aggregate signature, and /// the MMR leaf it attests to. /// diff --git a/modules/consensus/beefy/prover/src/bls.rs b/modules/consensus/beefy/prover/src/bls.rs index 8e3ae3f5c..6a10900f6 100644 --- a/modules/consensus/beefy/prover/src/bls.rs +++ b/modules/consensus/beefy/prover/src/bls.rs @@ -26,7 +26,7 @@ use anyhow::anyhow; use codec::{Decode, Encode}; use polkadot_sdk::*; -use sp_consensus_beefy::{SignedCommitment, VersionedFinalityProof}; +use sp_consensus_beefy::{SignedCommitment, VersionedFinalityProof, BEEFY_ENGINE_ID}; use subxt::{backend::legacy::LegacyRpcMethods, Config}; use subxt_core::config::HashFor; @@ -79,6 +79,31 @@ pub fn decode_paired_justification( Ok(signed_commitment) } +/// The justification at `at`, with both halves of each paired signature kept. +/// +/// `crate::relay::fetch_latest_beefy_justification` reads the same bytes but discards the BLS +/// half, which is the half this path needs. +pub async fn fetch_paired_justification( + rpc: &LegacyRpcMethods, + at: HashFor, +) -> Result, anyhow::Error> { + let block = rpc + .chain_get_block(Some(at)) + .await? + .ok_or_else(|| anyhow!("No block at {at:?}"))?; + + let justification = block + .justifications + .and_then(|justifications| { + justifications + .into_iter() + .find_map(|(id, encoded)| (id == BEEFY_ENGINE_ID).then_some(encoded)) + }) + .ok_or_else(|| anyhow!("Block {at:?} carries no beefy justification"))?; + + decode_paired_justification(&justification) +} + /// The validators' BLS12-381 G2 public keys, in authority-set order. pub async fn beefy_g2_authorities( rpc: &LegacyRpcMethods, diff --git a/modules/consensus/beefy/verifier/Cargo.toml b/modules/consensus/beefy/verifier/Cargo.toml index c2c0fb242..d7f6efe1a 100644 --- a/modules/consensus/beefy/verifier/Cargo.toml +++ b/modules/consensus/beefy/verifier/Cargo.toml @@ -22,6 +22,18 @@ thiserror = { workspace = true } sp1-verifier = { git = "https://github.com/polytope-labs/sp1.git", branch = "polytope-labs/v6.1.0-wasm-compatible", default-features = false } alloy-sol-types = { workspace = true, default-features = false } +sha2 = { version = "0.10", default-features = false, optional = true } +ark-bls12-381 = { version = "0.5", default-features = false, features = ["curve"], optional = true } +ark-ec = { version = "0.5", default-features = false, optional = true } +ark-ff = { version = "0.5", default-features = false, optional = true } +ark-serialize = { version = "0.5", default-features = false, optional = true } + +[dependencies.gnark-plonk-verifier] +git = "https://github.com/polytope-labs/gnark-apk-proofs" +rev = "48e6aa504994a58fb41465e22eb90b1df6a190f8" +default-features = false +optional = true + [dependencies.polkadot-sdk] workspace = true features = [ @@ -67,6 +79,14 @@ default = ["std"] # Tests that need `w3f-bls` itself: the hash-to-curve vector and the aggregate checks the APK path # is built on. Nothing in the library depends on it. bls-crypto = [] +apk = [ + "dep:gnark-plonk-verifier", + "dep:sha2", + "dep:ark-bls12-381", + "dep:ark-ec", + "dep:ark-ff", + "dep:ark-serialize", +] # Runs the BLS-BEEFY tests against a relay whose BEEFY authorities are paired ecdsa_bls_crypto # keys. Pulls the prover's `bls` decode path, which reads 177-byte paired signatures and keys. bls = ["beefy-prover/bls", "beefy-prover/bls-aggregate", "bls-crypto"] @@ -86,4 +106,10 @@ std = [ "rs_merkle/std", "sp1-verifier/std", "alloy-sol-types/std", + "sha2?/std", + "ark-bls12-381?/std", + "ark-ec?/std", + "ark-ff?/std", + "ark-serialize?/std", + "gnark-plonk-verifier?/std", ] \ No newline at end of file diff --git a/modules/consensus/beefy/verifier/src/apk.rs b/modules/consensus/beefy/verifier/src/apk.rs new file mode 100644 index 000000000..c570689e6 --- /dev/null +++ b/modules/consensus/beefy/verifier/src/apk.rs @@ -0,0 +1,355 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Verifying BEEFY finality through an aggregate public key proof. +//! +//! This is the runtime counterpart of `BlsApkBeefy.sol` and the two have to agree exactly, since +//! they check the same proofs. The SNARK establishes that an aggregate key is the sum of precisely +//! the validators named in a bitlist, drawn from the set a commitment describes, and a single +//! pairing then checks the aggregate signature against that key while binding its G2 counterpart. +//! +//! Nothing here grows with the number of signers, which is the whole point of the scheme. + +use alloc::vec::Vec; + +use ark_bls12_381::{Bls12_381, Fq, Fq2, Fr, G1Affine, G1Projective, G2Affine}; +use ark_ec::{AffineRepr, CurveGroup, PrimeGroup, pairing::Pairing}; +use ark_ff::{BigInteger, One, PrimeField, Zero}; +use ark_serialize::CanonicalDeserialize; +use beefy_verifier_primitives::{ + APK_BITLIST_WORDS, APK_G1_LEN, APK_G2_LEN, ApkAuthoritySet, ApkCommitmentDigest, + ApkConsensusMessage, ApkConsensusState, ApkMmrProof, ParachainHeader, +}; +use codec::{Decode, Encode}; +use polkadot_sdk::*; +use primitive_types::H256; +use sp_runtime::traits::BlakeTwo256; + +use crate::Keccak256; +use sha2::{Digest, Sha256}; + +use crate::error::Error; + +/// Half of a G1 point, and the width of every coordinate the circuit's verifier deals in. +const COORDINATE: usize = 48; + +/// Verify a whole update and return the new trusted state with the verified parachain headers. +/// +/// The order matters and mirrors the Solidity client. The commitment is only believed once the +/// aggregate proof and the pairing both pass, the mmr leaf is only believed once the commitment +/// is, and the headers only once the leaf is. Any commitment picked up from those headers is +/// therefore learned from something already proven. +pub fn verify_apk_consensus( + trusted_state: ApkConsensusState, + proof: ApkConsensusMessage, + verifying_key: &[u8], +) -> Result<(ApkConsensusState, Vec), Error> { + let (mut state, heads_root) = + verify_apk_mmr_update_proof::(trusted_state, proof.mmr, verifying_key)?; + let headers = crate::verify_parachain_headers::(heads_root, proof.parachain)?; + + // Forward chaining: a verified header may carry the commitment for a set this client has no + // keys for yet. Picking it up here is what makes the next update verifiable at all, and is + // why the digest names the next set rather than the current one. + for header in headers.iter() { + if let Some(digest) = read_apk_digest(&header.header) { + state.learn_commitment(digest.set_id, H256(digest.commitment)); + break; + } + } + + Ok((state, headers)) +} + +/// Verify the signed mmr root and roll the authority sets forward. +pub fn verify_apk_mmr_update_proof( + mut trusted_state: ApkConsensusState, + mmr: ApkMmrProof, + verifying_key: &[u8], +) -> Result<(ApkConsensusState, H256), Error> { + if trusted_state.latest_beefy_height >= mmr.commitment.block_number { + return Err(Error::StaleHeight { + trusted_height: trusted_state.latest_beefy_height, + current_height: mmr.commitment.block_number, + }); + } + + let set_id = mmr.commitment.validator_set_id; + let authority_set = if set_id == trusted_state.current_authorities.id { + &trusted_state.current_authorities + } else if set_id == trusted_state.next_authorities.id { + &trusted_state.next_authorities + } else { + return Err(Error::UnknownAuthoritySet { id: set_id }); + }; + + // A set whose commitment has not been learned from a digest yet cannot be verified against. + // Refusing is deliberate: proceeding would check the proof against a zero commitment, which + // establishes nothing at all. + if authority_set.apk_commitment.is_zero() { + return Err(Error::ApkCommitmentMissing { id: set_id }); + } + let apk_commitment = authority_set.apk_commitment; + let authority_count = authority_set.len; + + verify_signed_by_apk( + &mmr.commitment.encode(), + &mmr, + apk_commitment, + authority_count, + verifying_key, + )?; + + let mmr_root = mmr + .commitment + .payload + .get_raw(&sp_consensus_beefy::known_payloads::MMR_ROOT_ID) + .and_then(|raw| H256::decode(&mut &raw[..]).ok()) + .ok_or(Error::MmrRootHashMissing)?; + + crate::verify_mmr_leaf::(&mmr.latest_mmr_leaf, &mmr.mmr_proof, mmr_root)?; + + // The leaf names the incoming set and its size, but says nothing about Poseidon2, so its + // commitment starts empty and waits for a digest. + let next = &mmr.latest_mmr_leaf.beefy_next_authority_set; + if next.id > trusted_state.next_authorities.id { + trusted_state.current_authorities = trusted_state.next_authorities.clone(); + trusted_state.next_authorities = + ApkAuthoritySet { id: next.id, len: next.len, apk_commitment: H256::zero() }; + } + trusted_state.latest_beefy_height = mmr.commitment.block_number; + trusted_state.mmr_root_hash = mmr_root; + + Ok((trusted_state, mmr.latest_mmr_leaf.leaf_extra)) +} + +/// Read an APK commitment out of a scale encoded parachain header, if it carries one. +fn read_apk_digest(header: &[u8]) -> Option { + let header = sp_runtime::generic::Header::::decode(&mut &header[..]).ok()?; + ApkCommitmentDigest::find_in(&header.digest) +} + +/// Verify that a supermajority of `authority_set` signed `commitment`. +/// +/// Two separate facts are established and both are needed. The proof says the aggregate key +/// belongs to the set, and the pairing says that key produced the signature. Counting the +/// signers is left here, because the circuit proves who signed but has no opinion on whether it +/// is enough. +pub fn verify_signed_by_apk( + encoded_commitment: &[u8], + mmr: &ApkMmrProof, + apk_commitment: H256, + authority_count: u32, + verifying_key: &[u8], +) -> Result<(), Error> { + if !supermajority(count_signers(&mmr.bitlist), authority_count) { + return Err(Error::SuperMajorityRequired); + } + + verify_apk_proof(mmr, apk_commitment, verifying_key)?; + + let message = hash_commitment_to_g1(encoded_commitment)?; + let apk = read_g1(&mmr.apk)?; + let apk2 = read_g2(&mmr.apk2)?; + let signature = read_g1(&mmr.signature)?; + + if !verify_aggregate(message, apk, apk2, signature) { + return Err(Error::ApkPairingFailed); + } + + Ok(()) +} + +/// Check the PLONK proof against the set's commitment. +/// +/// The public inputs are the bitlist, then the commitment, then the aggregate key as twelve +/// limbs, which is the order the circuit exposes them in and the order the Solidity verifier +/// reads them. +fn verify_apk_proof( + mmr: &ApkMmrProof, + apk_commitment: H256, + verifying_key: &[u8], +) -> Result<(), Error> { + let vk = gnark_plonk_verifier::VerifyingKey::try_from(verifying_key) + .map_err(|_| Error::ApkVerifyingKeyInvalid)?; + let proof = gnark_plonk_verifier::PlonkProof::try_from((&mmr.apk_proof[..], vk.qcp.len())) + .map_err(|_| Error::ApkProofMalformed)?; + + let mut public_inputs = Vec::with_capacity(APK_BITLIST_WORDS + 1 + 12); + for word in mmr.bitlist.iter() { + public_inputs.push(Fr::from_be_bytes_mod_order(word)); + } + public_inputs.push(Fr::from_be_bytes_mod_order(apk_commitment.as_bytes())); + // The aggregate key travels as public input too, as six limbs per coordinate, which is how + // the circuit represents a base field element it cannot hold in one scalar. + for coordinate in [&mmr.apk[..COORDINATE], &mmr.apk[COORDINATE..]] { + for limb in coordinate.chunks(8) { + public_inputs.push(Fr::from_be_bytes_mod_order(limb)); + } + } + + gnark_plonk_verifier::verify(&proof, &vk, &public_inputs) + .map_err(|_| Error::ApkProofVerificationFailed) +} + +/// The batched signature and binding check, matching `ApkProof._verifyBls`: +/// +/// ```text +/// e(sig + t·apk, -g2) · e(msg + t·g1, apk2) == 1 +/// ``` +/// +/// which is the random linear combination of the signature equation and the equation binding +/// `apk2` to `apk`. Doing them together costs one pairing rather than two, and the challenge `t` +/// is derived from all four points so a caller cannot pick points to suit a known `t`. +fn verify_aggregate(message: G1Affine, apk: G1Affine, apk2: G2Affine, signature: G1Affine) -> bool { + let t = challenge(&message, &signature, &apk, &apk2); + + let lhs: G1Affine = (signature.into_group() + apk * t).into_affine(); + let rhs: G1Affine = (message.into_group() + G1Projective::generator() * t).into_affine(); + + let result = Bls12_381::multi_pairing([lhs, rhs], [-G2Affine::generator(), apk2]); + result.0 == ark_bls12_381::Fq12::one() +} + +/// `expand_message_xmd` with SHA-256 over the four points, taking 48 bytes and reducing them into +/// the scalar field. An empty domain separation tag, matching the contract. +fn challenge(message: &G1Affine, signature: &G1Affine, apk: &G1Affine, apk2: &G2Affine) -> Fr { + let mut data = Vec::with_capacity(APK_G1_LEN * 3 + APK_G2_LEN); + data.extend_from_slice(&write_g1(message)); + data.extend_from_slice(&write_g1(signature)); + data.extend_from_slice(&write_g1(apk)); + data.extend_from_slice(&write_g2(apk2)); + + // b0 = SHA256(Z_pad || data || I2OSP(48, 2) || I2OSP(0, 1) || DST_prime), with a 64 byte zero + // pad for SHA-256's block size and an empty DST, so DST_prime is a single zero length byte. + let mut hasher = Sha256::new(); + hasher.update([0u8; 64]); + hasher.update(&data); + hasher.update([0x00, 0x30]); + hasher.update([0x00]); + hasher.update([0x00]); + let b0: [u8; 32] = hasher.finalize().into(); + + let mut hasher = Sha256::new(); + hasher.update(b0); + hasher.update([0x01, 0x00]); + let b1: [u8; 32] = hasher.finalize().into(); + + let mut xored = [0u8; 32]; + for (i, byte) in xored.iter_mut().enumerate() { + *byte = b0[i] ^ b1[i]; + } + let mut hasher = Sha256::new(); + hasher.update(xored); + hasher.update([0x02, 0x00]); + let b2: [u8; 32] = hasher.finalize().into(); + + // t = b1 ‖ the top 16 bytes of b2, reduced. 48 bytes of uniform output, as the spec asks for. + let mut wide = [0u8; 48]; + wide[..32].copy_from_slice(&b1); + wide[32..].copy_from_slice(&b2[..16]); + Fr::from_be_bytes_mod_order(&wide) +} + +/// Population count over the bitlist. Fixed cost regardless of how many signed. +pub fn count_signers(bitlist: &[[u8; 32]; APK_BITLIST_WORDS]) -> u32 { + bitlist.iter().flatten().map(|byte| byte.count_ones()).sum() +} + +/// Two thirds plus one, matching substrate's own rule and the Solidity client. +fn supermajority(signed: u32, total: u32) -> bool { + total > 0 && (signed as u64) * 3 > (total as u64) * 2 +} + +/// Hash the encoded commitment onto G1 the way `w3f-bls` does. +/// +/// Note this is not the textbook construction. The ciphersuite is prepended to the message rather +/// than used as the domain separation tag, and the tag itself is the single byte `0x01`. Getting +/// this wrong yields a well formed point and a pairing that fails with nothing to explain why. +fn hash_commitment_to_g1(encoded_commitment: &[u8]) -> Result { + use ark_ec::hashing::{ + HashToCurve, curve_maps::wb::WBMap, map_to_curve_hasher::MapToCurveBasedHasher, + }; + use ark_ff::field_hashers::DefaultFieldHasher; + + let mut preimage = Vec::with_capacity(CIPHER_SUITE.len() + encoded_commitment.len()); + preimage.extend_from_slice(CIPHER_SUITE); + preimage.extend_from_slice(encoded_commitment); + + let hasher = MapToCurveBasedHasher::< + G1Projective, + DefaultFieldHasher, + WBMap, + >::new(&[0x01]) + .map_err(|_| Error::ApkHashToCurveFailed)?; + + hasher.hash(&preimage).map_err(|_| Error::ApkHashToCurveFailed) +} + +/// The ciphersuite `w3f-bls` prepends to the message. Substrate signs with the basic scheme, so +/// the tail is `NUL` rather than `POP`. +const CIPHER_SUITE: &[u8] = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; + +/// Read a G1 point from raw big-endian coordinates. +fn read_g1(bytes: &[u8; APK_G1_LEN]) -> Result { + let x = read_fq(&bytes[..COORDINATE])?; + let y = read_fq(&bytes[COORDINATE..])?; + let point = G1Affine::new_unchecked(x, y); + if !point.is_on_curve() || !point.is_in_correct_subgroup_assuming_on_curve() { + return Err(Error::ApkPointInvalid); + } + Ok(point) +} + +/// Read a G2 point, whose coordinates are pairs in the quadratic extension. +fn read_g2(bytes: &[u8; APK_G2_LEN]) -> Result { + let x = Fq2::new(read_fq(&bytes[..COORDINATE])?, read_fq(&bytes[COORDINATE..2 * COORDINATE])?); + let y = Fq2::new( + read_fq(&bytes[2 * COORDINATE..3 * COORDINATE])?, + read_fq(&bytes[3 * COORDINATE..])?, + ); + let point = G2Affine::new_unchecked(x, y); + if !point.is_on_curve() || !point.is_in_correct_subgroup_assuming_on_curve() { + return Err(Error::ApkPointInvalid); + } + Ok(point) +} + +fn read_fq(bytes: &[u8]) -> Result { + Fq::deserialize_uncompressed(&reverse(bytes)[..]).map_err(|_| Error::ApkPointInvalid) +} + +/// Arkworks serialises field elements little-endian while the circuit and the contract both work +/// big-endian, so every coordinate is reversed on the way in and out. +fn reverse(bytes: &[u8]) -> Vec { + bytes.iter().rev().copied().collect() +} + +fn write_g1(point: &G1Affine) -> Vec { + let (x, y) = point.xy().unwrap_or((Fq::zero(), Fq::zero())); + let mut out = Vec::with_capacity(APK_G1_LEN); + out.extend_from_slice(&x.into_bigint().to_bytes_be()); + out.extend_from_slice(&y.into_bigint().to_bytes_be()); + out +} + +fn write_g2(point: &G2Affine) -> Vec { + let (x, y) = point.xy().unwrap_or((Fq2::zero(), Fq2::zero())); + let mut out = Vec::with_capacity(APK_G2_LEN); + for coordinate in [&x.c0, &x.c1, &y.c0, &y.c1] { + out.extend_from_slice(&coordinate.into_bigint().to_bytes_be()); + } + out +} diff --git a/modules/consensus/beefy/verifier/src/error.rs b/modules/consensus/beefy/verifier/src/error.rs index 2e5a60feb..43bfd0cca 100644 --- a/modules/consensus/beefy/verifier/src/error.rs +++ b/modules/consensus/beefy/verifier/src/error.rs @@ -73,6 +73,35 @@ pub enum Error { /// The SP1 Groth16 verifier rejected the proof bytes. #[error("SP1 proof verification failed")] Sp1VerificationFailed, + /// The apk proof payload failed to SCALE-decode. + #[error("Cannot decode apk proof: {0}")] + DecodeApkProof(String), + /// No commitment has been learned for this authority set yet, so nothing can be checked + /// against it. + #[error("No apk commitment known for authority set {id}")] + ApkCommitmentMissing { + /// Id of the set + id: u64, + }, + /// The apk verifying key stored for the client did not parse. + #[error("Invalid apk verifying key")] + ApkVerifyingKeyInvalid, + /// The proof bytes are not a well formed PLONK proof. + #[error("Malformed apk proof")] + ApkProofMalformed, + /// The PLONK verifier rejected the proof, so the aggregate key is not this set's. + #[error("Apk proof verification failed")] + ApkProofVerificationFailed, + /// A point in the proof is not on the curve or not in the right subgroup. + #[error("Invalid apk curve point")] + ApkPointInvalid, + /// Hashing the commitment onto the curve failed. + #[error("Apk hash to curve failed")] + ApkHashToCurveFailed, + /// The aggregate signature did not verify against the aggregate key. + #[error("Apk pairing check failed")] + ApkPairingFailed, + // -- ismp-beefy client wrapper -- /// The trusted state failed to SCALE-decode into a `ConsensusState`. #[error("Cannot decode consensus state: {0}")] diff --git a/modules/consensus/beefy/verifier/src/lib.rs b/modules/consensus/beefy/verifier/src/lib.rs index 73b47cabc..de5641add 100644 --- a/modules/consensus/beefy/verifier/src/lib.rs +++ b/modules/consensus/beefy/verifier/src/lib.rs @@ -23,6 +23,8 @@ extern crate alloc; +#[cfg(feature = "apk")] +pub mod apk; pub mod error; pub mod sp1; #[cfg(test)] @@ -276,7 +278,7 @@ pub fn verify_parachain_headers( Ok(parachain_proof.parachains) } -fn verify_mmr_leaf( +pub(crate) fn verify_mmr_leaf( leaf: &MmrLeaf, proof: &LeafProof, mmr_root: H256, diff --git a/modules/ismp/clients/beefy/Cargo.toml b/modules/ismp/clients/beefy/Cargo.toml index fb447975b..9a69ced37 100644 --- a/modules/ismp/clients/beefy/Cargo.toml +++ b/modules/ismp/clients/beefy/Cargo.toml @@ -24,6 +24,7 @@ features = [ [features] default = ["std"] +apk = ["beefy-verifier/apk"] std = [ "anyhow/std", "codec/std", diff --git a/modules/ismp/clients/beefy/src/consensus.rs b/modules/ismp/clients/beefy/src/consensus.rs index a0af6c4b1..80b507720 100644 --- a/modules/ismp/clients/beefy/src/consensus.rs +++ b/modules/ismp/clients/beefy/src/consensus.rs @@ -15,6 +15,8 @@ use alloc::{boxed::Box, collections::BTreeMap, format, vec, vec::Vec}; use beefy_verifier::{error::Error as BeefyError, verify_consensus}; +#[cfg(feature = "apk")] +use beefy_verifier_primitives::PROOF_TYPE_APK; use beefy_verifier_primitives::{ ConsensusMessage, ConsensusState, MmrProof, PROOF_TYPE_NAIVE, PROOF_TYPE_SP1, ParachainProof, Sp1BeefyProof, @@ -76,9 +78,12 @@ where trusted_consensus_state: Vec, proof: Vec, ) -> Result<(Vec, VerifiedCommitments), Error> { - let consensus_state: ConsensusState = + // Decoded per arm rather than up front, since the apk path carries a different state + // shape: authority sets there are identified by a commitment to their keys. + let decode_state = || -> Result { codec::Decode::decode(&mut &trusted_consensus_state[..]) - .map_err(|e| BeefyError::DecodeConsensusState(format!("{e:?}")))?; + .map_err(|e| BeefyError::DecodeConsensusState(format!("{e:?}"))) + }; let proof_type = proof.first().ok_or(BeefyError::EmptyProof)?; if !C::allowed_proof_types().contains(proof_type) { @@ -90,7 +95,7 @@ where PROOF_TYPE_NAIVE => { let consensus_proof: ConsensusMessage = codec::Decode::decode(&mut &payload[..]) .map_err(|e| BeefyError::DecodeNaiveProof(format!("{e:?}")))?; - verify_consensus::(consensus_state, consensus_proof)? + verify_consensus::(decode_state()?, consensus_proof)? }, PROOF_TYPE_SP1 => { let sp1_proof: Sp1BeefyProof = codec::Decode::decode(&mut &payload[..]) @@ -98,11 +103,26 @@ where let vkey_hash = C::sp1_vkey_hash(); let vkey = alloc::format!("0x{:x}", vkey_hash); beefy_verifier::sp1::verify_sp1_consensus::( - consensus_state, + decode_state()?, sp1_proof, &vkey, )? }, + #[cfg(feature = "apk")] + PROOF_TYPE_APK => { + let apk_state: beefy_verifier_primitives::ApkConsensusState = + codec::Decode::decode(&mut &trusted_consensus_state[..]) + .map_err(|e| BeefyError::DecodeConsensusState(format!("{e:?}")))?; + let apk_proof: beefy_verifier_primitives::ApkConsensusMessage = + codec::Decode::decode(&mut &payload[..]) + .map_err(|e| BeefyError::DecodeApkProof(format!("{e:?}")))?; + let (state, headers) = beefy_verifier::apk::verify_apk_consensus::( + apk_state, + apk_proof, + &C::apk_verifying_key(), + )?; + (state.encode(), headers) + }, _ => return Err(BeefyError::UnknownProofType(*proof_type).into()), }; diff --git a/modules/ismp/clients/beefy/src/lib.rs b/modules/ismp/clients/beefy/src/lib.rs index 11a28f44b..7acf3e3ec 100644 --- a/modules/ismp/clients/beefy/src/lib.rs +++ b/modules/ismp/clients/beefy/src/lib.rs @@ -19,7 +19,7 @@ extern crate alloc; extern crate core; pub mod consensus; -pub use beefy_verifier_primitives::{PROOF_TYPE_NAIVE, PROOF_TYPE_SP1}; +pub use beefy_verifier_primitives::{PROOF_TYPE_APK, PROOF_TYPE_NAIVE, PROOF_TYPE_SP1}; pub use consensus::{BEEFY_CONSENSUS_ID, BeefyConsensusClient}; use polkadot_sdk::*; @@ -48,6 +48,14 @@ pub trait BeefyClientConfig { /// Returns the SP1 verification key hash. fn sp1_vkey_hash() -> primitive_types::H256; + /// Verifying key for the aggregate public key circuit, read on every apk proof. + /// + /// This is the key itself rather than a hash, since the verification happens here. A chain + /// that never sees an apk proof can leave it empty. + fn apk_verifying_key() -> alloc::vec::Vec { + Default::default() + } + /// Allowed proof types. Controls which consensus proof formats this client will /// accept. On mainnet set to `&[PROOF_TYPE_SP1]`, on testnets set to /// `&[PROOF_TYPE_NAIVE, PROOF_TYPE_SP1]`. A proof whose type byte is not listed is diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index 5176cf173..a6d858dfc 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -34,6 +34,7 @@ use alloc::vec::Vec; use apk_commitment::{PartialCommitment, NUM_VALIDATORS}; use ark_bls12_381::G1Affine; use ark_serialize::CanonicalDeserialize; +pub use beefy_verifier_primitives::{ApkCommitmentDigest, APK_ENGINE_ID}; use beefy_verifier_primitives::{PairedAuthority, BLS_G1_SIGNATURE_LEN}; use codec::{Decode, Encode, MaxEncodedLen}; use cumulus_pallet_parachain_system::RelayChainStateProof; @@ -64,43 +65,6 @@ pub const RELAY_BEEFY_VALIDATOR_SET_ID: [u8; 32] = [ 0x8f, 0x05, 0xbc, 0xcc, 0x2f, 0x70, 0xec, 0x66, 0xa3, 0x29, 0x99, 0xc5, 0x76, 0x11, 0x56, 0xbe, ]; -/// Engine id for the digest item carrying the commitment. -pub const APK_ENGINE_ID: [u8; 4] = *b"APKC"; - -/// The payload of the digest item this pallet writes. -/// -/// This is a wire format: an off-chain verifier reads it out of a header it has already -/// authenticated through the BEEFY MMR's parachain heads root, and feeds `commitment` to -/// `ApkProof.verify` as `publicKeysCommitment`. The set id is what lets it tell which authority -/// set the commitment belongs to. -#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo)] -pub struct ApkCommitmentDigest { - /// The BEEFY validator set id these keys belong to, which is the relay's current set id plus - /// one, since the commitment describes the next set. - pub set_id: u64, - /// Poseidon2 over the set's G1 keys, padded to the circuit width with the identity point. - pub commitment: [u8; 32], -} - -impl ApkCommitmentDigest { - /// Pull the commitment out of a header's digest logs, if this pallet wrote one into it. - /// - /// This is the client side of the design. A verifier that has already authenticated a - /// hyperbridge header, through the parachain heads root in the BEEFY MMR leaf, can read the - /// commitment straight out of that header with no further proof, and hand it to - /// `ApkProof.verify` as `publicKeysCommitment`. - /// - /// Returns the first matching item. The pallet only ever writes one per block, and only on the - /// block a set completes. - pub fn find_in(digest: &sp_runtime::generic::Digest) -> Option { - digest.logs().iter().find_map(|log| match log { - sp_runtime::DigestItem::Consensus(id, payload) if *id == APK_ENGINE_ID => - Self::decode(&mut &payload[..]).ok(), - _ => None, - }) - } -} - impl Progress { /// A chain that has absorbed nothing yet. pub fn fresh(set_digest: [u8; 32]) -> Self { diff --git a/modules/pallets/beefy-consensus-proofs/src/lib.rs b/modules/pallets/beefy-consensus-proofs/src/lib.rs index 3d83d8d60..3b9d65ded 100644 --- a/modules/pallets/beefy-consensus-proofs/src/lib.rs +++ b/modules/pallets/beefy-consensus-proofs/src/lib.rs @@ -149,6 +149,11 @@ pub mod pallet { #[pallet::constant] type MaxUncleProvers: Get; + /// Upper bound on the apk circuit's verifying key. The key generated for the 1024 slot + /// circuit is around 49KB, so this wants headroom rather than a tight fit. + #[pallet::constant] + type MaxApkVerifyingKeyLen: Get; + /// The pallet-assets instance used for managing the reputation token. /// Mints reputation tokens 1:1 with native token rewards to proof submitters. type ReputationAsset: fungible::Mutate>; @@ -172,6 +177,16 @@ pub mod pallet { #[pallet::storage] pub type Sp1VkeyHash = StorageValue<_, H256, ValueQuery>; + /// Verifying key for the aggregate public key circuit, consumed by + /// `beefy_verifier::apk::verify_apk_consensus`. + /// + /// Unlike the SP1 key this is the key itself rather than a hash of it, since the verification + /// runs here rather than inside a proof system that already knows it. It is around 49KB, set + /// once by governance and read on every apk proof. + #[pallet::storage] + pub type ApkVerifyingKey = + StorageValue<_, BoundedVec, ValueQuery>; + /// Heights of recent messaging proofs (no authority-set rotation). Values are /// strictly increasing because every accepted proof advances the proven height, /// so `vec[0]` is always the oldest — FIFO eviction via `remove(0)` when full. @@ -287,6 +302,8 @@ pub mod pallet { ProofRewardUpdated { new_reward: BalanceOf }, /// SP1 verification key hash updated. Sp1VkeyHashUpdated, + /// Apk circuit verifying key replaced. + ApkVerifyingKeyUpdated, /// Reward curve updated. RewardCurveUpdated, } @@ -309,52 +326,45 @@ pub mod pallet { Error::::AbiDecodeFailed })? .into(); - let current_set_id = state.current_authorities.id; - let next_set_id = state.next_authorities.id; - let latest_beefy_height = state.latest_beefy_height; - let host = pallet_ismp::Pallet::::default(); - // Seed an initial commitment for the host state machine at the current block height. - pallet_ismp::Pallet::::create_consensus_client( - frame_system::RawOrigin::Root.into(), - ismp::messaging::CreateConsensusState { - consensus_state: state.encode(), - consensus_client_id: ismp_beefy::BEEFY_CONSENSUS_ID, - consensus_state_id: ismp_beefy::BEEFY_CONSENSUS_ID, - unbonding_period: T::UnbondingPeriod::get(), - challenge_periods: Default::default(), - state_machine_commitments: vec![( - StateMachineId { - consensus_state_id: T::ConsensusStateId::get(), - state_id: host.host_state_machine(), - }, - StateCommitmentHeight { - height: 1, - commitment: StateCommitment { - timestamp: host.timestamp().as_secs(), - overlay_root: None, - state_root: H256::zero(), - }, - }, - )], - }, + Self::create_state( + state.encode(), + state.current_authorities.id, + state.next_authorities.id, + state.latest_beefy_height, ) - .map_err(|e| { - log::warn!( - target: "ismp", - "[beefy-consensus-proofs]: pallet_ismp::create_consensus_client failed: {e:?}", - ); - Error::::IsmpUpdateFailed - })?; + } - LastRewardedDispatchRoot::::kill(); + /// Initialize or reset the consensus state for a chain verified through aggregate public + /// key proofs, from its solidity-ABI encoding. + /// + /// The starting set's commitment has to be supplied here, since it is normally learned + /// from a header digest and there is no earlier verified header at this point. + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::initialize_state())] + pub fn initialize_apk_state(origin: OriginFor, abi_state: Vec) -> DispatchResult { + ::AdminOrigin::ensure_origin(origin)?; - Self::deposit_event(Event::StateInitialized { - current_set_id, - next_set_id, - latest_beefy_height, - }); - Ok(()) + let state: beefy_verifier_primitives::ApkConsensusState = + ::abi_decode( + &abi_state, + ) + .map_err(|e| { + log::warn!( + target: "ismp", + "[beefy-consensus-proofs]: abi_decode(BlsApkConsensusState) failed: {e}", + ); + Error::::AbiDecodeFailed + })? + .try_into() + .map_err(|_| Error::::AbiDecodeFailed)?; + + Self::create_state( + state.encode(), + state.current_authorities.id, + state.next_authorities.id, + state.latest_beefy_height, + ) } /// Submit a BEEFY consensus proof. Signed: the signer is the reward payee. @@ -383,6 +393,19 @@ pub mod pallet { Ok(()) } + /// Replace the apk circuit's verifying key. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::set_apk_verifying_key())] + pub fn set_apk_verifying_key( + origin: OriginFor, + key: BoundedVec, + ) -> DispatchResult { + ::AdminOrigin::ensure_origin(origin)?; + ApkVerifyingKey::::put(key); + Self::deposit_event(Event::ApkVerifyingKeyUpdated); + Ok(()) + } + /// Update the SP1 verification key hash. #[pallet::call_index(3)] #[pallet::weight(T::WeightInfo::set_sp1_vkey_hash())] @@ -433,6 +456,76 @@ pub mod pallet { impl Pallet { /// Returns the latest proven parachain height from `pallet-ismp` for the /// coprocessor state machine. + /// Register the consensus client with whichever state shape was decoded, and seed a + /// commitment for the host state machine at the current height. + fn create_state( + consensus_state: Vec, + current_set_id: u64, + next_set_id: u64, + latest_beefy_height: u32, + ) -> DispatchResult { + let host = pallet_ismp::Pallet::::default(); + + // Seed an initial commitment for the host state machine at the current block height. + pallet_ismp::Pallet::::create_consensus_client( + frame_system::RawOrigin::Root.into(), + ismp::messaging::CreateConsensusState { + consensus_state, + consensus_client_id: ismp_beefy::BEEFY_CONSENSUS_ID, + consensus_state_id: ismp_beefy::BEEFY_CONSENSUS_ID, + unbonding_period: T::UnbondingPeriod::get(), + challenge_periods: Default::default(), + state_machine_commitments: vec![( + StateMachineId { + consensus_state_id: T::ConsensusStateId::get(), + state_id: host.host_state_machine(), + }, + StateCommitmentHeight { + height: 1, + commitment: StateCommitment { + timestamp: host.timestamp().as_secs(), + overlay_root: None, + state_root: H256::zero(), + }, + }, + )], + }, + ) + .map_err(|e| { + log::warn!( + target: "ismp", + "[beefy-consensus-proofs]: pallet_ismp::create_consensus_client failed: {e:?}", + ); + Error::::IsmpUpdateFailed + })?; + + LastRewardedDispatchRoot::::kill(); + + Self::deposit_event(Event::StateInitialized { + current_set_id, + next_set_id, + latest_beefy_height, + }); + Ok(()) + } + + /// Authority set ids out of a stored consensus state. + /// + /// The shape follows the proof type: an apk state identifies a set by a commitment to its + /// keys where the others carry a merkle root, so the two do not decode into each other. + /// Only the ids are wanted here, and both shapes have them. + fn authority_set_ids(state: &[u8], proof_type: u8) -> Result<(u64, u64), Error> { + if proof_type == types::PROOF_TYPE_APK { + let state: beefy_verifier_primitives::ApkConsensusState = + Decode::decode(&mut &state[..]).map_err(|_| Error::::NotInitialized)?; + Ok((state.current_authorities.id, state.next_authorities.id)) + } else { + let state: beefy_verifier_primitives::ConsensusState = + Decode::decode(&mut &state[..]).map_err(|_| Error::::NotInitialized)?; + Ok((state.current_authorities.id, state.next_authorities.id)) + } + } + fn latest_height() -> Result> { let host = pallet_ismp::Pallet::::default(); let id = ismp::consensus::StateMachineId { @@ -480,10 +573,10 @@ pub mod pallet { } Some(nonce) }, - // Only SP1 proofs are bound to a prover account. The naive path verifies + // Only SP1 proofs are bound to a prover account. The naive and apk paths verify // signatures the relay chain's validators produced, so there is nothing - // prover-specific in it to bind and no anti-theft gate to apply. - types::PROOF_TYPE_NAIVE => None, + // prover-specific in them to bind and no anti-theft gate to apply. + types::PROOF_TYPE_NAIVE | types::PROOF_TYPE_APK => None, _ => Err(Error::::UnknownProofType)?, }; @@ -813,9 +906,7 @@ pub mod pallet { let prev_state_bytes = host .consensus_state(ismp_beefy::BEEFY_CONSENSUS_ID) .map_err(|_| Error::::NotInitialized)?; - let prev_state: beefy_verifier_primitives::ConsensusState = - Decode::decode(&mut &prev_state_bytes[..]) - .map_err(|_| Error::::NotInitialized)?; + let (prev_current_set, _) = Self::authority_set_ids(&prev_state_bytes, proof_type)?; let prev_height = Self::latest_height()?; let consensus_proof = match proof_type { @@ -837,6 +928,15 @@ pub mod pallet { let scale_proof: beefy_verifier_primitives::ConsensusMessage = abi_proof.into(); [&[types::PROOF_TYPE_NAIVE], scale_proof.encode().as_slice()].concat() }, + types::PROOF_TYPE_APK => { + let abi_proof = ::abi_decode_params( + abi_payload, + ) + .map_err(|_| Error::::AbiDecodeFailed)?; + let scale_proof: beefy_verifier_primitives::ApkConsensusMessage = + abi_proof.try_into().map_err(|_| Error::::AbiDecodeFailed)?; + [&[types::PROOF_TYPE_APK], scale_proof.encode().as_slice()].concat() + }, _ => Err(Error::::UnknownProofType)?, }; @@ -886,16 +986,15 @@ pub mod pallet { let new_state_bytes = host .consensus_state(ismp_beefy::BEEFY_CONSENSUS_ID) .map_err(|_| Error::::VerificationFailed)?; - let new_state: beefy_verifier_primitives::ConsensusState = - Decode::decode(&mut &new_state_bytes[..]) - .map_err(|_| Error::::VerificationFailed)?; + let (new_current_set, new_next_set) = + Self::authority_set_ids(&new_state_bytes, proof_type)?; // BEEFY invariant: `next` is always `current + 1`. - if new_state.next_authorities.id != new_state.current_authorities.id.saturating_add(1) { + if new_next_set != new_current_set.saturating_add(1) { Err(Error::::UnexpectedAuthoritySet)?; } - let rotated = new_state.current_authorities.id > prev_state.current_authorities.id; + let rotated = new_current_set > prev_current_set; // Messaging proofs must finalize a parachain head we haven't seen; one that doesn't // carries no new work and is rejected. Rotation proofs are exempt: the session @@ -937,7 +1036,7 @@ pub mod pallet { Ok(VerifyOutcome { latest_height, - current_set_id: new_state.current_authorities.id, + current_set_id: new_current_set, rotated, has_new_messages, child_trie_root, diff --git a/modules/pallets/beefy-consensus-proofs/src/types.rs b/modules/pallets/beefy-consensus-proofs/src/types.rs index 5c4e1e4d0..375861998 100644 --- a/modules/pallets/beefy-consensus-proofs/src/types.rs +++ b/modules/pallets/beefy-consensus-proofs/src/types.rs @@ -34,6 +34,8 @@ pub const ROTATION_OFFCHAIN_PREFIX: &[u8] = b"beefy_consensus_proofs::rotation:: pub const PROOF_TYPE_NAIVE: u8 = 0x00; /// Proof type byte: SP1 ZK BEEFY proof. pub const PROOF_TYPE_SP1: u8 = 0x01; +/// Proof type byte: aggregate public key BEEFY proof. +pub const PROOF_TYPE_APK: u8 = 0x02; fn offchain_key(prefix: &[u8], id: u64) -> Vec { let mut key = Vec::with_capacity(prefix.len() + 8); diff --git a/modules/pallets/beefy-consensus-proofs/src/weights.rs b/modules/pallets/beefy-consensus-proofs/src/weights.rs index d51e10902..4636626dd 100644 --- a/modules/pallets/beefy-consensus-proofs/src/weights.rs +++ b/modules/pallets/beefy-consensus-proofs/src/weights.rs @@ -34,6 +34,8 @@ pub trait WeightInfo { fn set_sp1_vkey_hash() -> Weight; /// Weight of `set_reward_curve`. fn set_reward_curve() -> Weight; + /// Weight of `set_apk_verifying_key`. + fn set_apk_verifying_key() -> Weight; } /// No-op [`WeightInfo`] for tests and genesis bootstrap. @@ -53,4 +55,7 @@ impl WeightInfo for () { fn set_reward_curve() -> Weight { Weight::zero() } + fn set_apk_verifying_key() -> Weight { + Weight::zero() + } } diff --git a/modules/pallets/testsuite/src/runtime.rs b/modules/pallets/testsuite/src/runtime.rs index 247883637..cf0b2e116 100644 --- a/modules/pallets/testsuite/src/runtime.rs +++ b/modules/pallets/testsuite/src/runtime.rs @@ -517,6 +517,7 @@ impl pallet_beefy_consensus_proofs::Config for Test { type ConsensusStateId = BeefyConsensusStateId; type UnbondingPeriod = ConstU64<10>; type MaxUncleProvers = ConstU32<5>; + type MaxApkVerifyingKeyLen = ConstU32<131072>; type ReputationAsset = ReputationAsset; type WeightInfo = (); } diff --git a/parachain/runtimes/gargantua/Cargo.toml b/parachain/runtimes/gargantua/Cargo.toml index 9c0131f55..69b3ac411 100644 --- a/parachain/runtimes/gargantua/Cargo.toml +++ b/parachain/runtimes/gargantua/Cargo.toml @@ -34,7 +34,7 @@ ismp-sync-committee = { workspace = true } ismp-bsc = { workspace = true } ismp-parachain = { workspace = true } ismp-grandpa = { workspace = true } -ismp-beefy = { workspace = true } +ismp-beefy = { workspace = true, features = ["apk"] } ismp-parachain-runtime-api = { workspace = true } pallet-ismp-relayer = { workspace = true } pallet-ismp-host-executive = { workspace = true } diff --git a/parachain/runtimes/gargantua/src/ismp.rs b/parachain/runtimes/gargantua/src/ismp.rs index c1daf3a5f..09237200b 100644 --- a/parachain/runtimes/gargantua/src/ismp.rs +++ b/parachain/runtimes/gargantua/src/ismp.rs @@ -241,9 +241,13 @@ impl ismp_beefy::BeefyClientConfig for Runtime { pallet_beefy_consensus_proofs::Sp1VkeyHash::::get() } + fn apk_verifying_key() -> alloc::vec::Vec { + pallet_beefy_consensus_proofs::ApkVerifyingKey::::get().into_inner() + } + fn allowed_proof_types() -> &'static [u8] { - // Testnet: accept the naive ECDSA and SP1 ZK proof formats. - &[ismp_beefy::PROOF_TYPE_NAIVE, ismp_beefy::PROOF_TYPE_SP1] + // Testnet: accept the naive ECDSA and SP1 ZK proof formats, plus aggregate public key. + &[ismp_beefy::PROOF_TYPE_NAIVE, ismp_beefy::PROOF_TYPE_SP1, ismp_beefy::PROOF_TYPE_APK] } } diff --git a/parachain/runtimes/gargantua/src/lib.rs b/parachain/runtimes/gargantua/src/lib.rs index d120847b2..fd4e87190 100644 --- a/parachain/runtimes/gargantua/src/lib.rs +++ b/parachain/runtimes/gargantua/src/lib.rs @@ -873,6 +873,7 @@ impl pallet_beefy_consensus_proofs::Config for Runtime { type ConsensusStateId = BeefyConsensusStateId; type UnbondingPeriod = BeefyUnbondingPeriod; type MaxUncleProvers = MaxBeefyUncleProvers; + type MaxApkVerifyingKeyLen = ConstU32<131072>; type ReputationAsset = ReputationAsset; type WeightInfo = weights::pallet_beefy_consensus_proofs::WeightInfo; } diff --git a/parachain/runtimes/gargantua/src/weights/pallet_beefy_consensus_proofs.rs b/parachain/runtimes/gargantua/src/weights/pallet_beefy_consensus_proofs.rs index 4598e0da3..f48b55111 100644 --- a/parachain/runtimes/gargantua/src/weights/pallet_beefy_consensus_proofs.rs +++ b/parachain/runtimes/gargantua/src/weights/pallet_beefy_consensus_proofs.rs @@ -108,6 +108,18 @@ impl pallet_beefy_consensus_proofs::WeightInfo for Weig .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: `BeefyConsensusProofs::ApkVerifyingKey` (r:0 w:1) + /// Proof: `BeefyConsensusProofs::ApkVerifyingKey` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_apk_verifying_key() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Approximated: one write like `set_sp1_vkey_hash`, of a value three orders of magnitude + // larger, so the byte cost dominates rather than the write itself. + Weight::from_parts(2_000_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } /// Storage: `BeefyConsensusProofs::Sp1VkeyHash` (r:0 w:1) /// Proof: `BeefyConsensusProofs::Sp1VkeyHash` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn set_sp1_vkey_hash() -> Weight { diff --git a/parachain/runtimes/nexus/src/ismp.rs b/parachain/runtimes/nexus/src/ismp.rs index e3759b1a6..073c9af0a 100644 --- a/parachain/runtimes/nexus/src/ismp.rs +++ b/parachain/runtimes/nexus/src/ismp.rs @@ -210,6 +210,10 @@ impl ismp_beefy::BeefyClientConfig for Runtime { pallet_beefy_consensus_proofs::Sp1VkeyHash::::get() } + fn apk_verifying_key() -> alloc::vec::Vec { + pallet_beefy_consensus_proofs::ApkVerifyingKey::::get().into_inner() + } + fn allowed_proof_types() -> &'static [u8] { // Mainnet: only accept SP1 ZK proofs. &[ismp_beefy::PROOF_TYPE_SP1] @@ -325,7 +329,9 @@ pub struct HftBenchmarkHelper; #[cfg(feature = "runtime-benchmarks")] impl pallet_hyper_fungible_token::types::BenchmarkHelper for HftBenchmarkHelper { fn create_asset(decimals: u8, who: &AccountId, amount: u128) -> H256 { - use frame_support::traits::fungibles::{metadata::Mutate as MutateMetadata, Create, Mutate}; + use frame_support::traits::fungibles::{ + metadata::Mutate as MutateMetadata, Create, Mutate, + }; let asset_id: H256 = sp_io::hashing::keccak_256(b"HFT_BENCHMARK_ASSET").into(); >::create(asset_id, who.clone(), true, 1) diff --git a/parachain/runtimes/nexus/src/lib.rs b/parachain/runtimes/nexus/src/lib.rs index 98e61fef6..ee63d8247 100644 --- a/parachain/runtimes/nexus/src/lib.rs +++ b/parachain/runtimes/nexus/src/lib.rs @@ -1010,6 +1010,7 @@ impl pallet_beefy_consensus_proofs::Config for Runtime { type ConsensusStateId = BeefyConsensusStateId; type UnbondingPeriod = BeefyUnbondingPeriod; type MaxUncleProvers = MaxBeefyUncleProvers; + type MaxApkVerifyingKeyLen = ConstU32<131072>; type ReputationAsset = ReputationAsset; type WeightInfo = weights::pallet_beefy_consensus_proofs::WeightInfo; } diff --git a/parachain/runtimes/nexus/src/weights/pallet_beefy_consensus_proofs.rs b/parachain/runtimes/nexus/src/weights/pallet_beefy_consensus_proofs.rs index 4598e0da3..f48b55111 100644 --- a/parachain/runtimes/nexus/src/weights/pallet_beefy_consensus_proofs.rs +++ b/parachain/runtimes/nexus/src/weights/pallet_beefy_consensus_proofs.rs @@ -108,6 +108,18 @@ impl pallet_beefy_consensus_proofs::WeightInfo for Weig .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: `BeefyConsensusProofs::ApkVerifyingKey` (r:0 w:1) + /// Proof: `BeefyConsensusProofs::ApkVerifyingKey` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn set_apk_verifying_key() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Approximated: one write like `set_sp1_vkey_hash`, of a value three orders of magnitude + // larger, so the byte cost dominates rather than the write itself. + Weight::from_parts(2_000_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } /// Storage: `BeefyConsensusProofs::Sp1VkeyHash` (r:0 w:1) /// Proof: `BeefyConsensusProofs::Sp1VkeyHash` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn set_sp1_vkey_hash() -> Weight { diff --git a/tesseract/consensus/beefy/Cargo.toml b/tesseract/consensus/beefy/Cargo.toml index 94f60c8ab..c43c87407 100644 --- a/tesseract/consensus/beefy/Cargo.toml +++ b/tesseract/consensus/beefy/Cargo.toml @@ -47,6 +47,7 @@ sp-consensus-beefy = { workspace = true } tesseract-substrate = { workspace = true } tesseract-primitives = { workspace = true } zk-beefy = { path = "zk" } +apk-beefy = { path = "apk", default-features = false } rsmq_async = { workspace = true } redis-async = { version = "0.17.1", features = ["with-rustls"] } @@ -55,6 +56,7 @@ workspace = true features = ["sp-runtime"] [features] +apk-local = ["apk-beefy/local"] # a feature that tells the tests to write a new consensus state new-consensus-state = [] diff --git a/tesseract/consensus/beefy/apk/Cargo.toml b/tesseract/consensus/beefy/apk/Cargo.toml new file mode 100644 index 000000000..2389bce2a --- /dev/null +++ b/tesseract/consensus/beefy/apk/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "apk-beefy" +version = "0.1.0" +edition = "2021" +authors = ["Polytope Labs "] +description = "Builds BEEFY consensus proofs carrying an aggregate public key proof" + +[dependencies] +anyhow = "1.0.79" +async-trait = { workspace = true } +hex = { workspace = true } +alloy-primitives = { workspace = true, default-features = true } +subxt = { workspace = true, default-features = true } +sp-consensus-beefy = { workspace = true } + +beefy-prover = { workspace = true, features = ["bls-aggregate"] } +beefy-verifier-primitives = { workspace = true, default-features = true } +apk-commitment = { workspace = true, default-features = true } +ismp-abi = { workspace = true, default-features = true } + +ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = false } +ark-ec = { version = "0.4.0", default-features = false } +ark-ff = { version = "0.4.0", default-features = false } +ark-serialize = { version = "0.4.0", default-features = false } + +[dependencies.gnark-apk-prover] +git = "https://github.com/polytope-labs/gnark-apk-proofs" +rev = "ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4" +optional = true + +[dependencies.ark-serialize-05] +package = "ark-serialize" +version = "0.5" +optional = true + +[dependencies.tokio] +workspace = true +features = ["rt"] + +[features] +default = [] +local = ["dep:gnark-apk-prover", "dep:ark-serialize-05"] diff --git a/tesseract/consensus/beefy/apk/src/lib.rs b/tesseract/consensus/beefy/apk/src/lib.rs new file mode 100644 index 000000000..bd30186bc --- /dev/null +++ b/tesseract/consensus/beefy/apk/src/lib.rs @@ -0,0 +1,315 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Assembles the BEEFY consensus proof that `BlsApkBeefy` verifies. +//! +//! The relay chain half comes from `beefy_prover`, and everything specific to this path is built +//! here: the aggregate of the signers' keys in both groups, their aggregate signature, and the +//! SNARK that ties the aggregate to the authority set. +//! +//! Generating that SNARK needs a Go toolchain through cgo and a large structured reference string, +//! so it sits behind [`ApkProver`] rather than being called directly. Only the binary that +//! actually proves has to take those on. + +use alloy_primitives::{Bytes, FixedBytes, U256}; +use anyhow::anyhow; +use ark_bls12_381::{Fq, G1Affine, G1Projective, G2Affine, G2Projective}; +use ark_ec::{AffineRepr, CurveGroup}; +use ark_ff::{BigInteger, PrimeField}; +use ark_serialize::CanonicalDeserialize; +use std::sync::Arc; +use subxt::config::HashFor; + +use beefy_prover::bls::{ + aggregate_signatures, beefy_g1_authorities, beefy_g2_authorities, fetch_paired_justification, + PairedSignature, +}; +use beefy_verifier_primitives::{ConsensusState, BLS_G1_SIGNATURE_LEN}; +use ismp_abi::bls_apk_beefy::BlsApkBeefy; + +#[cfg(feature = "local")] +mod local; +#[cfg(feature = "local")] +pub use local::LocalProver; + +/// Payload id of the mmr root in a BEEFY commitment. +const MMR_ROOT_ID: &[u8; 2] = b"mh"; + +/// Number of words in the circuit's participation bitlist. +const BITLIST_WORDS: usize = 5; + +/// What the circuit is asked to prove. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ApkProofRequest { + /// Compressed G1 keys of the whole authority set, in authority order. + pub keys: Vec<[u8; BLS_G1_SIGNATURE_LEN]>, + /// Indices of the authorities that signed. + pub participation: Vec, +} + +/// What it produces. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ApkProof { + /// The PLONK proof bytes the contract passes to its verifier. + pub proof: Vec, + /// The participation set as the contract reads it, one bit per authority slot. + pub bitlist: [U256; BITLIST_WORDS], + /// Poseidon2 over the padded key set, a public input of the proof. + pub apk_commitment: [u8; 32], +} + +/// Generates APK proofs. +/// +/// An implementation may prove in process or hand the work to something else. Either way it is +/// slow, minutes rather than seconds, which is why this is only ever called by the prover service +/// and never on the delivery path. +#[async_trait::async_trait] +pub trait ApkProver: Send + Sync { + /// Prove that the aggregate of the participating keys belongs to this key set. + async fn prove(&self, request: ApkProofRequest) -> Result; +} + +/// Consensus prover for BEEFY verified through an aggregate public key proof. +/// +/// The SNARK prover is a trait object rather than a type parameter, since nothing here needs to +/// know which one it is and a second generic would spread through every caller. +pub struct Prover { + /// The relay chain half, shared with every other BEEFY proof variant. + pub inner: beefy_prover::Prover, + /// Produces the SNARK. + pub apk: Arc, +} + +impl Clone for Prover +where + R: subxt::Config, + P: subxt::Config, + beefy_prover::Prover: Clone, +{ + fn clone(&self) -> Self { + Self { inner: self.inner.clone(), apk: self.apk.clone() } + } +} + +impl Prover +where + R: subxt::Config, + P: subxt::Config, +{ + /// Build a prover over an existing relay chain prover. + pub fn new(prover: beefy_prover::Prover, apk: Arc) -> Self { + Self { inner: prover, apk } + } + + /// Build the whole update: the signed commitment, the aggregate proof, the mmr leaf and the + /// parachain headers, in the shape `BlsApkBeefy.verify` decodes. + /// + /// Takes the ECDSA shaped commitment every variant is handed, then reads the justification + /// again itself, since that decode throws away the BLS half this path signs with. + pub async fn consensus_proof( + &self, + signed_commitment: sp_consensus_beefy::SignedCommitment< + u32, + sp_consensus_beefy::ecdsa_crypto::Signature, + >, + consensus_state: ConsensusState, + ) -> Result { + let set_id = signed_commitment.commitment.validator_set_id; + if set_id != consensus_state.current_authorities.id && + set_id != consensus_state.next_authorities.id + { + Err(anyhow!("Unknown validator set {set_id}"))? + } + + let height = signed_commitment.commitment.block_number; + let at = self + .inner + .relay_rpc + .chain_get_block_hash(Some(height.into())) + .await? + .ok_or_else(|| anyhow!("No block hash for beefy block {height}"))?; + + let paired = fetch_paired_justification(&self.inner.relay_rpc, at).await?; + let message = self.inner.bls_consensus_proof(paired.clone()).await?; + let aggregate = self.aggregate(&paired, at).await?; + let proof = self.apk.prove(aggregate.request).await?; + + // The circuit and the runtime must describe the same set, since the client checks the + // proof against whichever of the two it was given. Disagreement here means the relay was + // read at a height where the set had already rotated. + let expected = apk_commitment_of(&aggregate.keys)?; + if proof.apk_commitment != expected { + Err(anyhow!( + "apk commitment mismatch, circuit says {} and the key set hashes to {}", + hex::encode(proof.apk_commitment), + hex::encode(expected) + ))? + } + + let mmr_root = message + .mmr + .commitment + .payload + .get_raw(MMR_ROOT_ID) + .ok_or_else(|| anyhow!("Commitment carries no mmr root payload"))? + .clone(); + + let leaf = &message.mmr.latest_mmr_leaf; + let relay = BlsApkBeefy::BlsApkRelayChainProof { + commitment: BlsApkBeefy::Commitment { + payload: vec![BlsApkBeefy::Payload { + id: FixedBytes(*MMR_ROOT_ID), + data: Bytes::from(mmr_root), + }], + blockNumber: message.mmr.commitment.block_number, + validatorSetId: set_id, + }, + bitlist: proof.bitlist, + apk: aggregate.apk, + apk2: aggregate.apk2, + apkProof: Bytes::from(proof.proof), + signature: aggregate.signature, + latestMmrLeaf: BlsApkBeefy::BeefyMmrLeaf { + // One byte carrying the major version in the top three bits and the minor in the + // rest, which is how the reverse conversion in `ismp-abi` reads it back. + version: { + let (major, minor) = leaf.version.split(); + (major << 5) | minor + }, + parentNumber: leaf.parent_number_and_hash.0, + parentHash: FixedBytes(leaf.parent_number_and_hash.1 .0), + nextAuthoritySet: BlsApkBeefy::AuthoritySetCommitment { + id: leaf.beefy_next_authority_set.id, + len: leaf.beefy_next_authority_set.len, + root: FixedBytes(leaf.beefy_next_authority_set.keyset_commitment.0), + }, + extra: FixedBytes(leaf.leaf_extra.0), + leafIndex: U256::from( + message.mmr.mmr_proof.leaf_indices.first().copied().unwrap_or_default(), + ), + }, + mmrProof: message.mmr.mmr_proof.items.iter().map(|item| FixedBytes(item.0)).collect(), + }; + + let parachain = BlsApkBeefy::ParachainProof { + parachains: message + .parachain + .parachains + .iter() + .map(|para| BlsApkBeefy::Parachain { + index: U256::from(para.index), + id: U256::from(para.para_id), + header: Bytes::from(para.header.clone()), + }) + .collect(), + proof: message.parachain.proof.iter().map(|node| FixedBytes(*node)).collect(), + leafCount: U256::from(message.parachain.total_leaves), + }; + + Ok(BlsApkBeefy::BlsApkBeefyConsensusProof { relay, parachain }) + } + + /// Sum the signers' keys in both groups and their signatures, and note who they were. + /// + /// The G1 halves are what the circuit binds to and the G2 halves are what BEEFY's signature + /// verifies against, so both are needed even though they describe one aggregate secret. + async fn aggregate( + &self, + signed_commitment: &sp_consensus_beefy::SignedCommitment, + at: HashFor, + ) -> Result { + let keys = beefy_g1_authorities(&self.inner.relay_rpc, Some(at)).await?; + let g2_keys = beefy_g2_authorities(&self.inner.relay_rpc, Some(at)).await?; + + let mut apk_g1 = G1Projective::default(); + let mut apk_g2 = G2Projective::default(); + let mut signatures = Vec::new(); + let mut participation = Vec::new(); + for (index, signature) in signed_commitment.signatures.iter().enumerate() { + let Some(signature) = signature else { continue }; + let g1 = + keys.get(index).ok_or_else(|| anyhow!("Signer {index} is not an authority"))?; + let g2 = g2_keys + .get(index) + .ok_or_else(|| anyhow!("Signer {index} is not an authority"))?; + + apk_g1 += decompress_g1(g1)?; + apk_g2 += G2Affine::deserialize_compressed(&g2[..]) + .map_err(|_| anyhow!("Authority {index} has a malformed G2 key"))?; + signatures.push(signature.g1_signature()); + participation.push(index as u64); + } + + if participation.is_empty() { + Err(anyhow!("Commitment carries no signatures"))? + } + + let signature = decompress_g1(&aggregate_signatures(&signatures)?)?; + + Ok(Aggregate { + apk: pack_g1(&apk_g1.into_affine())?, + apk2: pack_g2(&apk_g2.into_affine())?, + signature: pack_g1(&signature)?, + request: ApkProofRequest { keys: keys.clone(), participation }, + keys, + }) + } +} + +/// The aggregated halves of one signed commitment. +struct Aggregate { + apk: [FixedBytes<32>; 3], + apk2: [FixedBytes<32>; 6], + signature: [FixedBytes<32>; 3], + keys: Vec<[u8; BLS_G1_SIGNATURE_LEN]>, + request: ApkProofRequest, +} + +/// Poseidon2 over the key set, padded to the circuit's width. The same value the runtime pallet +/// publishes in a header digest. +fn apk_commitment_of(keys: &[[u8; BLS_G1_SIGNATURE_LEN]]) -> Result<[u8; 32], anyhow::Error> { + let points = keys.iter().map(decompress_g1).collect::, _>>()?; + let padded = apk_commitment::padded_to_circuit_width(&points); + Ok(apk_commitment::public_keys_commitment_bytes(&padded)) +} + +fn decompress_g1(key: &[u8; BLS_G1_SIGNATURE_LEN]) -> Result { + G1Affine::deserialize_compressed(&key[..]).map_err(|_| anyhow!("Malformed G1 point")) +} + +/// Curve points reach the SNARK verifier as raw 48 byte coordinates packed into 32 byte words, +/// which is not the zero padded layout the EIP-2537 precompiles take. +fn pack_g1(point: &G1Affine) -> Result<[FixedBytes<32>; 3], anyhow::Error> { + let (x, y) = point.xy().ok_or_else(|| anyhow!("Aggregate is the identity"))?; + words(&[x, y])?.try_into().map_err(|_| anyhow!("G1 point is not three words")) +} + +fn pack_g2(point: &G2Affine) -> Result<[FixedBytes<32>; 6], anyhow::Error> { + let (x, y) = point.xy().ok_or_else(|| anyhow!("Aggregate is the identity"))?; + words(&[&x.c0, &x.c1, &y.c0, &y.c1])? + .try_into() + .map_err(|_| anyhow!("G2 point is not six words")) +} + +fn words(coordinates: &[&Fq]) -> Result>, anyhow::Error> { + let mut bytes = Vec::with_capacity(coordinates.len() * 48); + for coordinate in coordinates { + bytes.extend_from_slice(&coordinate.into_bigint().to_bytes_be()); + } + if bytes.len() % 32 != 0 { + Err(anyhow!("Packed point is not a whole number of words"))? + } + Ok(bytes.chunks(32).map(FixedBytes::<32>::from_slice).collect()) +} diff --git a/tesseract/consensus/beefy/apk/src/local.rs b/tesseract/consensus/beefy/apk/src/local.rs new file mode 100644 index 000000000..df7de10f9 --- /dev/null +++ b/tesseract/consensus/beefy/apk/src/local.rs @@ -0,0 +1,103 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Proving in process with `gnark-apk-proofs`. + +use std::{path::PathBuf, sync::Arc}; + +use alloy_primitives::U256; +use anyhow::anyhow; +use ark_serialize_05::CanonicalDeserialize; +use gnark_apk_prover::{G1Affine, ProofBuilder, ProverContext}; + +use crate::{ApkProof, ApkProofRequest, ApkProver, BITLIST_WORDS}; + +/// Width of one public input, and of a bitlist word. +const WORD: usize = 32; + +/// The public inputs the circuit exposes: the bitlist, then the commitment, then the aggregate key +/// as twelve limbs. +const PUBLIC_INPUTS: usize = 18; + +/// Proves with the circuit compiled into this process. +/// +/// Construction runs the setup, which compiles the circuit and generates the keys. That takes a +/// few minutes and a large structured reference string, so build one at startup and keep it. +pub struct LocalProver { + context: Arc, +} + +impl LocalProver { + /// Compile the circuit and generate the proving key. + /// + /// `srs_dir` holds the structured reference string. Leave it unset for + /// `$HOME/.config/gnark-apk-proofs/srs`, which the Go side populates from the Filecoin + /// ceremony if it is empty. + pub fn new(srs_dir: Option) -> Result { + let context = ProverContext::setup(srs_dir.as_deref()) + .map_err(|e| anyhow!("APK circuit setup failed: {e}"))?; + Ok(Self { context: Arc::new(context) }) + } +} + +#[async_trait::async_trait] +impl ApkProver for LocalProver { + async fn prove(&self, request: ApkProofRequest) -> Result { + let keys = request + .keys + .iter() + .map(|key| { + G1Affine::deserialize_compressed(&key[..]) + .map_err(|_| anyhow!("Malformed G1 authority key")) + }) + .collect::, _>>()?; + let participation = request + .participation + .iter() + .map(|index| { + u16::try_from(*index) + .map_err(|_| anyhow!("Authority index {index} is out of range")) + }) + .collect::, _>>()?; + + // Proving is minutes of cpu, so keep it off the runtime's worker threads. + let context = self.context.clone(); + let proof = tokio::task::spawn_blocking(move || { + ProofBuilder::new(&context) + .public_keys(keys) + .participation(participation) + .prove() + .map_err(|e| anyhow!("APK proving failed: {e}")) + }) + .await??; + + let inputs = proof.public_inputs_calldata(); + if inputs.len() != PUBLIC_INPUTS * WORD { + Err(anyhow!("Expected {PUBLIC_INPUTS} public inputs, got {}", inputs.len() / WORD))? + } + let word = |i: usize| &inputs[i * WORD..(i + 1) * WORD]; + + let bitlist: [U256; BITLIST_WORDS] = (0..BITLIST_WORDS) + .map(|i| U256::from_be_slice(word(i))) + .collect::>() + .try_into() + .map_err(|_| anyhow!("Bitlist is not {BITLIST_WORDS} words"))?; + + let mut apk_commitment = [0u8; WORD]; + apk_commitment.copy_from_slice(word(BITLIST_WORDS)); + + Ok(ApkProof { proof: proof.proof_calldata().to_vec(), bitlist, apk_commitment }) + } +} diff --git a/tesseract/consensus/beefy/src/prover.rs b/tesseract/consensus/beefy/src/prover.rs index 3a7446651..1c34ee45c 100644 --- a/tesseract/consensus/beefy/src/prover.rs +++ b/tesseract/consensus/beefy/src/prover.rs @@ -105,6 +105,10 @@ pub enum ProofVariant { /// Delegate signature verification to an SP1 zero-knowledge proof (SP1Beefy). #[serde(alias = "zk")] Sp1, + /// Prove the signers with an aggregate public key proof (BlsApkBeefy). Only for a relay whose + /// BEEFY authorities hold paired `ecdsa_bls_crypto` keys. + #[serde(alias = "bls")] + Apk, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -122,6 +126,10 @@ pub struct ProverConfig { pub max_rpc_payload_size: Option, /// Query batch size for mmr leaves pub query_batch_size: Option, + /// Where the apk circuit's structured reference string lives. Only read by the `Apk` variant, + /// and left unset it falls back to `$HOME/.config/gnark-apk-proofs/srs`. + #[serde(default)] + pub apk_srs_dir: Option, } /// The BEEFY prover produces BEEFY consensus proofs using either the naive or zk variety. Consensus @@ -154,6 +162,9 @@ pub const PROOF_TYPE_ECDSA: u8 = 0x00; /// Proof type identifier for ZK proofs (SP1Beefy) pub const PROOF_TYPE_SP1: u8 = 0x01; +/// Proof type identifier for aggregate public key proofs (BlsApkBeefy) +pub const PROOF_TYPE_APK: u8 = 0x02; + impl BeefyProver where R: subxt::Config + Send + Sync + Clone, @@ -200,6 +211,10 @@ where let message = zk.consensus_proof(signed_commitment, consensus_state).await?; [&[PROOF_TYPE_SP1], message.abi_encode_params().as_slice()].concat() }, + Prover::Apk(ref apk) => { + let message = apk.consensus_proof(signed_commitment, consensus_state).await?; + [&[PROOF_TYPE_APK], message.abi_encode_params().as_slice()].concat() + }, }; Ok(encoded) @@ -578,12 +593,14 @@ where } } -/// Beefy prover, can produce ECDSA or SP1 proofs +/// Beefy prover, can produce ECDSA, SP1 or aggregate public key proofs pub enum Prover { /// ECDSA prover — verifies all 2/3+1 signatures on-chain Ecdsa(beefy_prover::Prover, PhantomData), /// SP1 prover — delegates signature verification to an SP1 ZK program Sp1(zk_beefy::Prover), + /// APK prover — proves the signers against a commitment to the authority set + Apk(apk_beefy::Prover), } impl Clone for Prover @@ -598,6 +615,7 @@ where match self { Prover::Ecdsa(p, _) => Prover::Ecdsa(p.clone(), PhantomData), Prover::Sp1(p) => Prover::Sp1(p.clone()), + Prover::Apk(p) => Prover::Apk(p.clone()), } } } @@ -656,6 +674,20 @@ where Prover::Sp1(zk_beefy::Prover::new(prover, sp1_prover, account)) }, ProofVariant::Ecdsa => Prover::Ecdsa(prover, PhantomData), + // Setup compiles the circuit and generates the proving key, minutes of cpu, so it is + // done once here rather than per proof and kept off the runtime's worker threads. + #[cfg(feature = "apk-local")] + ProofVariant::Apk => { + let srs_dir = config.apk_srs_dir.clone(); + let apk_prover = + tokio::task::spawn_blocking(move || apk_beefy::LocalProver::new(srs_dir)) + .await??; + Prover::Apk(apk_beefy::Prover::new(prover, Arc::new(apk_prover))) + }, + #[cfg(not(feature = "apk-local"))] + ProofVariant::Apk => Err(anyhow!( + "This binary was built without apk proving, rebuild with the `apk-local` feature" + ))?, }; Ok(prover) @@ -674,6 +706,7 @@ where match self { Prover::Sp1(ref p) => &p.inner, Prover::Ecdsa(ref p, _) => p, + Prover::Apk(ref p) => &p.inner, } } diff --git a/tesseract/prover/Cargo.toml b/tesseract/prover/Cargo.toml index 1f0cb1e72..275ff642e 100644 --- a/tesseract/prover/Cargo.toml +++ b/tesseract/prover/Cargo.toml @@ -19,7 +19,7 @@ rustls = { version = "0.23.23", features = ["ring"] } primitive-types = { workspace = true } subxt-utils = { workspace = true } -tesseract-beefy = { workspace = true } +tesseract-beefy = { workspace = true, features = ["apk-local"] } tesseract-substrate = { workspace = true } tesseract-primitives = { workspace = true } From 432e356a74f2081b01990fc9ccc56544ce3befc7 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 11 Aug 2026 20:40:30 +0100 Subject: [PATCH 15/48] read paired and plain beefy keys from the wire rather than choosing at build time --- modules/consensus/beefy/prover/Cargo.toml | 9 -- modules/consensus/beefy/prover/src/lib.rs | 45 +++++--- modules/consensus/beefy/prover/src/relay.rs | 83 +++++++------- modules/consensus/beefy/verifier/Cargo.toml | 2 +- tesseract/consensus/beefy/apk/src/lib.rs | 13 ++- tesseract/consensus/beefy/src/host.rs | 115 +++++++++++++++----- 6 files changed, 174 insertions(+), 93 deletions(-) diff --git a/modules/consensus/beefy/prover/Cargo.toml b/modules/consensus/beefy/prover/Cargo.toml index ab16dcfea..1713f1e36 100644 --- a/modules/consensus/beefy/prover/Cargo.toml +++ b/modules/consensus/beefy/prover/Cargo.toml @@ -59,15 +59,6 @@ features = [ [features] local = [] -# Prove a relay chain whose BEEFY authorities use the paired (ECDSA, BLS12-381) `ecdsa_bls_crypto` -# key type. On the wire such a chain carries 177-byte paired signatures and 177-byte authority -# public keys; with this feature the prover keeps only the ECDSA half of each (the keccak-ECDSA -# recoverable signature / compressed secp256k1 key), so the existing ECDSA verifier is unchanged. -# -# This changes how `beefy_authorities` and `decode_beefy_justification` read the wire, so it must -# not be enabled when proving a plain-ECDSA relay. It is deliberately separate from -# `bls-aggregate`, which only adds code. -bls = [] # Prove the BLS half instead: `crate::bls`, which builds a proof the aggregate verifier checks in # a single pairing. Purely additive, so it is safe to enable alongside plain-ECDSA proving. bls-aggregate = ["dep:w3f-bls", "dep:ismp-abi", "dep:alloy-primitives", "dep:ark-bls12-381", "dep:ark-ec", "dep:ark-ff", "dep:ark-serialize"] diff --git a/modules/consensus/beefy/prover/src/lib.rs b/modules/consensus/beefy/prover/src/lib.rs index ad5da352c..ffd1c4bc5 100644 --- a/modules/consensus/beefy/prover/src/lib.rs +++ b/modules/consensus/beefy/prover/src/lib.rs @@ -48,6 +48,12 @@ use relay::{ }; use util::hash_authority_addresses; +/// Size of a compressed secp256k1 BEEFY key, and of the ECDSA half of a paired one. +const ECDSA_PUBLIC_KEY_LEN: usize = 33; + +/// Wire size of a paired (ECDSA, BLS12-381) BEEFY key. +const PAIRED_AUTHORITY_LEN: usize = 177; + /// Proving commitments signed with aggregate BLS12-381 #[cfg(feature = "bls-aggregate")] pub mod bls; @@ -211,23 +217,28 @@ impl Prover { .await? .ok_or_else(|| anyhow!("No beefy authorities found!"))?; - // Encoding and decoding to fix dependency version conflicts - #[cfg(not(feature = "bls"))] - let current_authorities = Vec::<[u8; 33]>::decode(&mut data.as_ref())?; - - // `bls`: authorities are stored as 177-byte paired (ECDSA, BLS12-381) keys; keep the ECDSA - // half (first 33 bytes, a compressed secp256k1 key) so the derived address leaves match the - // on-chain keyset commitment, which `BeefyEcdsaBlsToEthereum` builds from those ECDSA - // halves. - #[cfg(feature = "bls")] - let current_authorities = Vec::<[u8; 177]>::decode(&mut data.as_ref())? - .into_iter() - .map(|key| { - let mut ecdsa_half = [0u8; 33]; - ecdsa_half.copy_from_slice(&key[..33]); - ecdsa_half - }) - .collect::>(); + // A relay whose authorities hold paired `ecdsa_bls_crypto` keys stores 177 bytes each, + // where a plain ECDSA one stores 33. The item width follows from the length prefix and + // what is left after it, so the chain says which it is rather than the build. + let mut rest = data.as_ref(); + let count = codec::Compact::::decode(&mut rest)?.0 as usize; + let width = if count == 0 { ECDSA_PUBLIC_KEY_LEN } else { rest.len() / count }; + + // Either way keep the ECDSA half, a compressed secp256k1 key, so the derived address + // leaves match the on-chain keyset commitment that `BeefyEcdsaBlsToEthereum` builds from + // exactly those halves. + let current_authorities = match width { + ECDSA_PUBLIC_KEY_LEN => Vec::<[u8; ECDSA_PUBLIC_KEY_LEN]>::decode(&mut data.as_ref())?, + PAIRED_AUTHORITY_LEN => Vec::<[u8; PAIRED_AUTHORITY_LEN]>::decode(&mut data.as_ref())? + .into_iter() + .map(|key| { + let mut ecdsa_half = [0u8; ECDSA_PUBLIC_KEY_LEN]; + ecdsa_half.copy_from_slice(&key[..ECDSA_PUBLIC_KEY_LEN]); + ecdsa_half + }) + .collect(), + _ => Err(anyhow!("Beefy authorities are {width} bytes wide, which is neither an ecdsa nor a paired key"))?, + }; Ok(current_authorities) } diff --git a/modules/consensus/beefy/prover/src/relay.rs b/modules/consensus/beefy/prover/src/relay.rs index 8401e5e29..9d625fe8f 100644 --- a/modules/consensus/beefy/prover/src/relay.rs +++ b/modules/consensus/beefy/prover/src/relay.rs @@ -42,6 +42,12 @@ use crate::{ PARAS_PARACHAINS, }; +/// Wire size of a paired (ECDSA, BLS12-381) BEEFY signature. +const PAIRED_SIGNATURE_LEN: usize = 177; + +/// Wire size of the ECDSA half of one. +const ECDSA_SIGNATURE_LEN: usize = 65; + /// Storage key for mmr.numberOfLeaves pub const MMR_NUMBER_OF_LEAVES: [u8; 32] = hex!("a8c65209d47ee80f56b0011e8fd91f508156209906244f2341137c136774c91d"); @@ -80,50 +86,53 @@ pub async fn fetch_latest_beefy_justification( /// Decode a BEEFY justification into a `SignedCommitment` carrying 65-byte ECDSA signatures. /// -/// On a plain-ECDSA relay the signatures decode directly. With the `bls` feature (a relay whose -/// BEEFY authorities use the paired `ecdsa_bls_crypto` key type) the on-wire signatures are -/// 177-byte paired signatures; we decode them and keep only the ECDSA half (the first 65 bytes, a -/// keccak-ECDSA recoverable signature). Both paths return the same type, so commitment hashing, -/// signature recovery and the ECDSA verifier all stay as they are. +/// A relay whose authorities hold paired `ecdsa_bls_crypto` keys puts 177-byte signatures on the +/// wire, where a plain ECDSA one puts 65. Which it is depends on the chain rather than on how this +/// was built, so both are attempted: a correct decode consumes the whole justification, and one +/// that leaves bytes behind read the wrong width. Either way only the ECDSA half survives, so +/// commitment hashing, signature recovery and the verifier are unaffected. pub fn decode_beefy_justification( bytes: &[u8], ) -> Result, anyhow::Error> { - #[cfg(not(feature = "bls"))] - { - let VersionedFinalityProof::V1(signed_commitment) = VersionedFinalityProof::< - u32, - sp_consensus_beefy::ecdsa_crypto::Signature, - >::decode(&mut &*bytes)?; - Ok(signed_commitment) - } - #[cfg(feature = "bls")] + let mut input = bytes; + if let Ok(VersionedFinalityProof::V1(signed_commitment)) = VersionedFinalityProof::< + u32, + sp_consensus_beefy::ecdsa_crypto::Signature, + >::decode(&mut input) { - /// A 177-byte paired (ECDSA, BLS12-381) signature exactly as SCALE-encoded on the wire. - struct Sig177([u8; 177]); - impl codec::Decode for Sig177 { - fn decode(input: &mut I) -> Result { - let mut bytes = [0u8; 177]; - input.read(&mut bytes)?; - Ok(Sig177(bytes)) - } + if input.is_empty() { + return Ok(signed_commitment); } + } - let VersionedFinalityProof::V1(paired) = - VersionedFinalityProof::::decode(&mut &*bytes)?; - let signatures = paired - .signatures - .into_iter() - .map(|maybe_sig| { - maybe_sig - .map(|sig| { - // The ECDSA half is the first 65 bytes: r || s || v. - sp_consensus_beefy::ecdsa_crypto::Signature::decode(&mut &sig.0[..65]) - }) - .transpose() - }) - .collect::, _>>()?; - Ok(SignedCommitment { commitment: paired.commitment, signatures }) + /// A paired (ECDSA, BLS12-381) signature exactly as SCALE-encoded on the wire. + struct PairedSignature([u8; PAIRED_SIGNATURE_LEN]); + impl codec::Decode for PairedSignature { + fn decode(input: &mut I) -> Result { + let mut bytes = [0u8; PAIRED_SIGNATURE_LEN]; + input.read(&mut bytes)?; + Ok(PairedSignature(bytes)) + } } + + let VersionedFinalityProof::V1(paired) = + VersionedFinalityProof::::decode(&mut &*bytes)?; + let signatures = paired + .signatures + .into_iter() + .map(|maybe_signature| { + maybe_signature + .map(|signature| { + // The ECDSA half is the first 65 bytes: r || s || v. + sp_consensus_beefy::ecdsa_crypto::Signature::decode( + &mut &signature.0[..ECDSA_SIGNATURE_LEN], + ) + }) + .transpose() + }) + .collect::, _>>()?; + + Ok(SignedCommitment { commitment: paired.commitment, signatures }) } /// Parathreads whitelisted to be added to the beefy mmr leaf parachains header root diff --git a/modules/consensus/beefy/verifier/Cargo.toml b/modules/consensus/beefy/verifier/Cargo.toml index d7f6efe1a..c7b5e5134 100644 --- a/modules/consensus/beefy/verifier/Cargo.toml +++ b/modules/consensus/beefy/verifier/Cargo.toml @@ -89,7 +89,7 @@ apk = [ ] # Runs the BLS-BEEFY tests against a relay whose BEEFY authorities are paired ecdsa_bls_crypto # keys. Pulls the prover's `bls` decode path, which reads 177-byte paired signatures and keys. -bls = ["beefy-prover/bls", "beefy-prover/bls-aggregate", "bls-crypto"] +bls = ["beefy-prover/bls-aggregate", "bls-crypto"] # Storage keys for a relay that names its beefy-mmr pallet `MmrLeaf` rather than `BeefyMmrLeaf`. # Westend (and Polkadot/Kusama) use `BeefyMmrLeaf`, so leave this off for them; our rococo fork # used `MmrLeaf`, so pair it with `bls` as `--features bls,local` when testing against that chain. diff --git a/tesseract/consensus/beefy/apk/src/lib.rs b/tesseract/consensus/beefy/apk/src/lib.rs index bd30186bc..edfc0c4cd 100644 --- a/tesseract/consensus/beefy/apk/src/lib.rs +++ b/tesseract/consensus/beefy/apk/src/lib.rs @@ -221,6 +221,15 @@ where Ok(BlsApkBeefy::BlsApkBeefyConsensusProof { relay, parachain }) } + /// Poseidon2 over the relay's current BEEFY keys at `at`. + /// + /// Needed when bootstrapping a client, since the commitment for the set that signs the first + /// update has to be seeded by hand. Every later one arrives in a header digest. + pub async fn current_apk_commitment(&self, at: HashFor) -> Result<[u8; 32], anyhow::Error> { + let keys = beefy_g1_authorities(&self.inner.relay_rpc, Some(at)).await?; + apk_commitment_of(&keys) + } + /// Sum the signers' keys in both groups and their signatures, and note who they were. /// /// The G1 halves are what the circuit binds to and the G2 halves are what BEEFY's signature @@ -279,7 +288,9 @@ struct Aggregate { /// Poseidon2 over the key set, padded to the circuit's width. The same value the runtime pallet /// publishes in a header digest. -fn apk_commitment_of(keys: &[[u8; BLS_G1_SIGNATURE_LEN]]) -> Result<[u8; 32], anyhow::Error> { +pub(crate) fn apk_commitment_of( + keys: &[[u8; BLS_G1_SIGNATURE_LEN]], +) -> Result<[u8; 32], anyhow::Error> { let points = keys.iter().map(decompress_g1).collect::, _>>()?; let padded = apk_commitment::padded_to_circuit_width(&points); Ok(apk_commitment::public_keys_commitment_bytes(&padded)) diff --git a/tesseract/consensus/beefy/src/host.rs b/tesseract/consensus/beefy/src/host.rs index 4a7486ca2..25d262897 100644 --- a/tesseract/consensus/beefy/src/host.rs +++ b/tesseract/consensus/beefy/src/host.rs @@ -46,6 +46,13 @@ pub struct BeefyHostConfig { pub consensus_state_id: ConsensusStateId, } +/// What the host needs out of a destination's consensus state, independent of its shape. +struct StateSummary { + latest_beefy_height: u32, + current_set_id: u64, + next_set_id: u64, +} + /// The beefy host is responsible for receiving BEEFY proofs from the queue and submitting /// them to the counterparty. pub struct BeefyHost @@ -86,6 +93,31 @@ where Ok(BeefyHost { backend, prover, client, config }) } + /// The parts of the destination's consensus state this host reasons about. + /// + /// The shape follows the variant this prover produces, since an apk client identifies a set + /// by a commitment to its keys where the others carry a merkle root. Only these three values + /// are wanted here, and both shapes have them. + fn state_summary(&self, encoded: &[u8]) -> Result { + if matches!(self.prover, Prover::Apk(_)) { + let state = beefy_verifier_primitives::ApkConsensusState::decode(&mut &encoded[..]) + .context("Could not decode apk consensus state")?; + Ok(StateSummary { + latest_beefy_height: state.latest_beefy_height, + current_set_id: state.current_authorities.id, + next_set_id: state.next_authorities.id, + }) + } else { + let state = ConsensusState::decode(&mut &encoded[..]) + .context("Could not decode consensus state")?; + Ok(StateSummary { + latest_beefy_height: state.latest_beefy_height, + current_set_id: state.current_authorities.id, + next_set_id: state.next_authorities.id, + }) + } + } + /// Initialize the consensus state for the prover (used by the state storage backend), then /// returns it. pub async fn hydrate_initial_consensus_state( @@ -202,14 +234,12 @@ where .query_consensus_state(None, self.config.consensus_state_id) .await .context("Could not fetch consenus state")?; // somewhat fatal - let consensus_state = ConsensusState::decode(&mut &encoded[..]) - .expect("Infallible, consensus state was encoded correctly"); + let StateSummary { next_set_id, .. } = self.state_summary(&encoded)?; // just some sanity checks - if set_id < consensus_state.next_authorities.id { + if set_id < next_set_id { tracing::error!( - target: crate::LOG_TARGET, "{counterparty_state_machine} got proof with set_id: {set_id} < next_set_id:{}", - consensus_state.next_authorities.id + target: crate::LOG_TARGET, "{counterparty_state_machine} got proof with set_id: {set_id} < next_set_id:{next_set_id}", ); self.backend .delete_message( @@ -222,10 +252,9 @@ where } // just some sanity checks - if set_id != consensus_state.next_authorities.id { + if set_id != next_set_id { tracing::error!( - target: crate::LOG_TARGET, "{counterparty_state_machine} consensus proof with set_id: {set_id} does not match next_set_id: {}", - consensus_state.next_authorities.id + target: crate::LOG_TARGET, "{counterparty_state_machine} consensus proof with set_id: {set_id} does not match next_set_id: {next_set_id}", ); // try to pull something else continue; @@ -283,14 +312,13 @@ where let encoded = counterparty .query_consensus_state(None, self.config.consensus_state_id) .await?; // somewhat fatal - let consensus_state = ConsensusState::decode(&mut &encoded[..]) - .expect("Infallible, consensus state was encoded correctly"); + let StateSummary { latest_beefy_height, current_set_id, next_set_id } = + self.state_summary(&encoded)?; // check if the update is relevant to us. - if consensus_state.latest_beefy_height >= finalized_height { + if latest_beefy_height >= finalized_height { tracing::info!( - target: crate::LOG_TARGET, "{counterparty_state_machine} saw proof for stale height {finalized_height}, current: {}", - consensus_state.latest_beefy_height + target: crate::LOG_TARGET, "{counterparty_state_machine} saw proof for stale height {finalized_height}, current: {latest_beefy_height}", ); // delete the message and pull another one self.backend @@ -303,26 +331,20 @@ where continue; } - if set_id != consensus_state.current_authorities.id && - set_id != consensus_state.next_authorities.id - { + if set_id != current_set_id && set_id != next_set_id { tracing::info!( - target: crate::LOG_TARGET, "{counterparty_state_machine} saw proof for unknown set_id {set_id}, current: {}, next: {}", - consensus_state.current_authorities.id, - consensus_state.next_authorities.id, + target: crate::LOG_TARGET, "{counterparty_state_machine} saw proof for unknown set_id {set_id}, current: {current_set_id}, next: {next_set_id}", ); - if set_id > consensus_state.next_authorities.id { + if set_id > next_set_id { tracing::info!( - target: crate::LOG_TARGET, "{counterparty_state_machine} proof was for future set: {set_id}, next: {}", - consensus_state.next_authorities.id, + target: crate::LOG_TARGET, "{counterparty_state_machine} proof was for future set: {set_id}, next: {next_set_id}", ); // break so that we can process a mandatory update break; - } else if set_id < consensus_state.current_authorities.id { + } else if set_id < current_set_id { tracing::info!( - target: crate::LOG_TARGET, "{counterparty_state_machine} proof was for older set: {set_id}, current: {}", - consensus_state.current_authorities.id, + target: crate::LOG_TARGET, "{counterparty_state_machine} proof was for older set: {set_id}, current: {current_set_id}", ); self.backend .delete_message( @@ -364,11 +386,48 @@ where &self, ) -> Result, anyhow::Error> { use alloy_sol_types::SolValue; - let consensus_state: BeefyConsensusState = - self.prover.query_initial_consensus_state(None).await?.inner.into(); + let prover_state = self.prover.query_initial_consensus_state(None).await?; + + // An apk client is seeded with a commitment to the signing set's keys instead of a merkle + // root, and the set that signs the first update has to be given one by hand, since every + // later commitment arrives in a header digest. The next set's is left empty for that + // reason. + let consensus_state = match self.prover { + Prover::Apk(ref apk) => { + let at = apk + .inner + .relay_rpc + .chain_get_block_hash(Some(prover_state.inner.latest_beefy_height.into())) + .await? + .ok_or_else(|| anyhow!("No block hash for the initial beefy height"))?; + let commitment = apk.current_apk_commitment(at).await?; + + let inner = prover_state.inner.clone(); + let authority_set = |set: sp_consensus_beefy::mmr::BeefyAuthoritySet, + apk_commitment: H256| { + beefy_verifier_primitives::ApkAuthoritySet { + id: set.id, + len: set.len, + apk_commitment, + } + }; + let state = beefy_verifier_primitives::ApkConsensusState { + latest_beefy_height: inner.latest_beefy_height, + beefy_activation_block: inner.beefy_activation_block, + mmr_root_hash: inner.mmr_root_hash, + current_authorities: authority_set(inner.current_authorities, H256(commitment)), + next_authorities: authority_set(inner.next_authorities, H256::zero()), + }; + ismp_abi::bls_apk_beefy::BlsApkBeefy::BlsApkConsensusState::from(state).abi_encode() + }, + _ => { + let state: BeefyConsensusState = prover_state.inner.into(); + state.abi_encode() + }, + }; Ok(Some(CreateConsensusState { - consensus_state: consensus_state.abi_encode(), + consensus_state, consensus_client_id: *b"BEEF", consensus_state_id: self.config.consensus_state_id, unbonding_period: 60 * 60 * 60 * 27, From 6b0612c25c3016e413de33d00a0f3c8a9b0df631 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Wed, 12 Aug 2026 12:33:34 +0100 Subject: [PATCH 16/48] aggregate onto the circuit's seed point and order the key limbs the way the circuit reads them --- .../foundry/fixtures/apk-verifying-key.bin | Bin 0 -> 49200 bytes modules/consensus/beefy/verifier/Cargo.toml | 2 + modules/consensus/beefy/verifier/src/apk.rs | 29 +++++- .../beefy/verifier/tests/apk_fixture.rs | 92 ++++++++++++++++++ 4 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 evm/tests/foundry/fixtures/apk-verifying-key.bin create mode 100644 modules/consensus/beefy/verifier/tests/apk_fixture.rs diff --git a/evm/tests/foundry/fixtures/apk-verifying-key.bin b/evm/tests/foundry/fixtures/apk-verifying-key.bin new file mode 100644 index 0000000000000000000000000000000000000000..b820d080227d2e86f65129624156429779d3352e GIT binary patch literal 49200 zcmeF)Q;aV>{2=_nZ*1GPZQGtXW1q3k*tTukwyiU^ZCm@~&3m)i%f0$u3;{{M1*KtN7cC06qC%a5-D68Xt@qxXn#={i9zYCoWoz&}7hAiz%QAj%cs zY7I(6wcv%#_`D-bKMk7NjvMOe!`{1>IW_-(tH}Rb>;Gp4!v9`9uh3{06%iE{AI|7Z z95K~;gvAXVfu$C@mqUr~Q4+W_(51_S*d*+bVj0NouIw&dUAzgPW@R5QPdW3zSW!g~ zp=escv)=Ynf`3yzM_hIJ=-5kR*w_v@*TA+RPmy!u1zE$-s|Ja``zs#eI>0wrQr0aH z6Y!2G{K(J+z?&#Kr23#VU`c}f0|iMQ5~L9OL%7<}6vuk5j+rVa{?*0B z0S3*oiVs|Tdo_V*(VoJnJs9*`xVpJ^QsaZUYK&uA(kW2hDGbrK;TnI`|L)9nc0ff2 zS3Ior+oA<3N|nF9awTh_BwkNnz7nTua|$>Nv9t0}cjRW!-C8gxcH7~Cfrxu(v_c^& z`3(}eZB`uEqhtMJCxX_P;W_9kg>kl-V1@0>f|MWvl}LpY%wk+QsY-Ch5M#Gyd;le* zsDlooM^?U4FXVPd%)%#ih^VFY$u0YC6SsUuML6lFEM@H3^YgO!VWwpm(`r{h6CtF|| z95=^pq~0x*GNrC0ktZ8RDU??s3|d4fq&l~Q<;!ymVBl0~TC1H3ym?^Xd~n}FZxD}k zChpQ0C_CgDcvmgtg;6M&YFYjLn@-q*-Mnttx#_DY#Ugu$jFP0Qaa83ErVF6)0g*0f zxQ&I5Cx-;1pc_V18aeb>d>7;aX;OkQ@i>@sg{cB1F9YkmS@csM>nG}Nt2aQrYh2(i zet3o)3>JiPzhd7Ms8m@nvQx@(UfH|2F&eLCb2kCZFbrNa5t%pVgFz2?u~9(K_}3zo zmjv#Uue(#|Qw5KoX27*uw%Wp2ohe+0%1UaQb5yH^&Eg{p{kdWx}qI_SkkiO%cD***aTOmeEH!H&XMFrRg8x*3V09*Qipd-q~3 zdKe2t>KQSa;fi6dV{z0dLM zRb3XJG)q9L17T=-bv)d+rQ5kg!aWC_l*VP?#c5e=C4jj~V1I4N_&pnY$pSvS*lI1n zE$(VH6egt+_BHv>{6OHJ9_NzH4Jcf>psETbxal&q^aEp(k9r_8f#0^n=Ffj0vqSNQ zXpm*zmi{DVz7odo-@+ervi5JNly*KSX8m1m-sS4rtubqR?JxSxRCeEOLs1;lPz>&~ zQ(9&)ptWuI2lw zqR>82E92GM9rD>iQ}Be`t$bxhgYdG$5u#d3MfA6T)6;cGcDS?8T?>M_BsVptg5yYq zb&b++fK1y!1MT@{5IaZ&J46_BWgjlF=@AdK2s-k z#}Wuy^`&7^NvD!BKoxN=bnM3C2mPiFd z^-OZLP`l)1KQd_9X_|Wl0H+EmJGn_D6A=^aX)0DQwXG(pAp-Np8>gM@6$kH{DCHtFNuW7e~R2z^0#jBcLd~1 z?73B>Q)V?iKBeZ_ksRW{YwD{w=2@q6IO6Yo3EQ^cjtHS;E`;}y`Fn^qx0YQnxp2E8 z3YSWCdJSI1nuZ_=d)Ax6JOiap!W=ijzhNDp#Rh!uHz3qV$VPi<>AJa|vMCn*fKTdI zzQyhHk0?=E7*||tAyy+oh>}f-1tZir(4%=B2~%nQPNke2qYfc$t!oFDlf=x_rpA4H zvB!aCKR>7%asj7&VF=vnS&s=!I)!+!P(R)4Z+#PUAmjc$UrMdZ>DigXFIb>8wr_;4 z%=6)=J_|eYu=?F-(Vll1(Gh(92ZnzDwXY6T)+d3-Ep*rJIzDy1|MI+t&GRro zblVbhWes%9;4KE>XysUDYjTPMuz7xz0s~|g5>8+P26+otN`(mOv+{a#*DRYrH0R5A z*LbG1*QLU+eObqvn+-b;b@?G&aEAn5yFC<`HDvRoeLH|`=UbbdB32hLQz5P7)Q7l2 zQF>OrC>6Gp=UB5nI@V08V_>6^e_ct zAJk>V606~rkAK~z2{s*I z7h#^mbCKW^mh3x}`h%j20=|!LN)Cl|9&B`3JIiHZx&rhlj^GvbyhOy)W8g4T)@=DW6GGY3?8 ze0t!dw@azE_#IL2Cm#a3rN{0x*8WQ$VmF7V_q8rPzpB#8rSJ7XQp6ZrlJawHn+-#V zW;izZ3ErnSgpW7mTuBZ;6w6^kJK^71Vm*+z1>H3eR+M1^ltPPW z;I%gj@g$4ip%3%>6HGMI~($J4Jse9_Ka#t1QGW z(jYX#L@f+(9a1_mfxgc7QLP9){pvad=j~`Mg~>tW&3b-#ly}uFle{qt2;#*{^M}Am zz1%sKmQGVtFE2xs&HNw3oc9Azd-#U950Uzeo>@cq#J`>^6GO3EvO`fymJD3J^}~ZtZM;#P!E=nux4c%4=r2uD@m0RR z4Z4t@il;pG#n9*IC)}U>QE*)R_qm?vDPpDfyB|=wYnIY)l*5N$?lSFsZ4Ops*!4%v zxTs)=7j}+Q6>Ib^@FFOO1h<{21J_U{3=-1hGoGvD&&&;X&Ei*+h41YSjA(ITjsdLJ8~PX${G(F=2g8U?4PYK zkVcjvjmB{#(Qo?XA)zL-N8Mq;4KE|8Tg<$hvdN~_$8ZQSmtp}#R61NN{~-~B(Wn+8 z_R9mfdwe-e%iMX`0+VVtxMQ4cm}XdUyLU}?r6!Gok}bxDa55xWYV3)_$($UnC@~TA zd7fpCieN$#p(Fj^VjQ-ukAorMM95s=kBrDfdfw_=ldYnxjgB0>SGuP^#0lsy^p1}z zDLKp}^>MV1Hd+IU-VTUX*psnnfEw@NM-#5Q#A2!QV~_m0FQzI}IW#Af3%InwuuP+? z1c$iujZ1e=9CE1yMllt-X9txmv&53!@)XZ;GXJvxEkVv!0cs#LWbR?x7>xQSlb5-H zZyt125(o*Vit>}oO}ULhQG$0@h|+ScO%>)|wCU^!eb#eO=+4?^{|CBy4&sr4DaHeA zDIEyS?GI9mdXj^l1;0JjsJ;3r*@wAl*EUxr}P{8N{bC;uUS*el-zifpS0VIAz2I#kmI&REO$ZeFD-^A7)F8`j8@EG zGy3NH^+EktPz`(9Fd+m?Geaq~XYyswUv~foL%UsLv^C!xB^_PfR%O`O&i0bH-?e;V zc~dJcir<+9(R}<^hZtnAw(_RS!3w{Z{79h&?VlbDCcMPzmb45j`(Wn0O)mmfgg{dk znuSd@^N+N1*s4u{ppTLY=6WU7MGtS0B^9Y$=>G4XDj=Svkxv58MA1ZPHl9jYHG&cGkiyz z+91#kscdV+^dk0*Om>3UPn+5nc|hez!4?$$+M((ALI30z2QI+~K~*{nr1|0c}_s=pb_qo=LBOu@0lG z4}jq?-ic7s5@#Gnc-mAEuI(qrQHOU;z&Z&PqFV}*;K6n_BLN)*eswQ z?dHyhuS?)a%TyD>$tMrg=My-j!8cpw8fOkjm0qJ$)Dc`5Hl8}c!cV5uTELJTK{%7A zi~$i~EC3Z2vHtU@hevfTxF*B1*~>PAtf|3si8*%i`MXmR)sp}4=MgeaEGDC>XV~g? zrzVQ3@egy#Cly%y32DJ*D~uC5nypWWQa7!c(QSPfbWppX_WN5yy*m%zZz5$Z)?}K# zot;>$3}?M#6JE@vGw$k$3qo6Vc##Q|_R)e1utz>>9)W;!zSD^$0Y!d*xx@Vp4#W0j zF#N5FJMjJ@-XYn}->ne-k(9y9Nyiwal8F<* zZs{70gU&o>Ha`HQ21f(jRo({=P(8{jmf&RbgF$02gFHi6#a27J-inoA5pd4J5W**b zEw>`dkRFCshVqxrNUt0S%E-SCwuU{!A(KW_rfbg2G?44EKyFg(rdvxB3u>UBnTORC zT2iLc=#S-B*@)i`0s~^HqvHOHVXI03>rea{2I3Ij zZA#KS$Cpy8{YUr%U}_&Q7|cQFDM#Ib0zH)ZC-Eg_k(HQ96B(8Z$S}EXO%g&DiM)cQ zagm%NpH(_Y?D(`eGRae5S7_Wha=3^LNt2mq));vXUi)bbIoyg>>RdF3a(g~I%4BfK zw2sxw4lDUyYq2lS81nB1PvpfZ+sx!nou!guG#K|FI%H11LIcs z(Z`^gqG?Z7VpmPCtp~@3o0rMZX=9qQFVw6UjOx_U1w=*E)!xJN=@VR1ha}3Dun!bv zr2MnnP2;w1*ghnBPNiPS&%VN*8-QpJL@HvlvXSJurQAa|8r~Q901i5UmJR?MN+YeB z8+bxj#{{gNCbj0cKbld9A!c_2?ZknUaY;9j$@0qzd4^ZBdMAmHuL_le3h*Kst=tcS?8_*xA#3et=;7ZC? z1jbLx(dy8UAa89?bFhuqQu)7IW!hsq_<)5KeUFi@EaF*K*2XS_!YBw@c>}T@WOTba zK*GY8!D*f@`yR`gJ2@H$`{5BKZLN!1lir&@u7lDjZ&GNOt>0)_>8`D;drg6XkcGbq zlw1P<{=M-oAtU+sTrN{I;!Km+BncYHUXZ_T6E!tGVbLy2e4)LMw8hFXQ9!J~hNWhN zSs2c(#qdDs3fzg{ib>g1#~~bU3@tAQDISe&BrvFY$g_NiDbPEex^fZ1|pSnrs@d&^}lvdFgvTL%=kcHSuu z7Jml_!!lZ<;;Q$%2f`@8sx{K-=knQ(A*@YmkGgex7)&G19`&Dlh!tu-#kt~E&QHG7 zZM`&f$uM+XDKfY(`cvp8xJ%>Epr<|vtd`cxXY?XaDIWE$mdFCYNZW~Zz4NN{#jqt6 zb)7*QU0qoQcsVS($_5F2pr^XmzhgJdwM6aBG&OXKJ>ov_9!%U9(XhU=K%0Iq31ti4 zTJV*OcS8|lAQtO7!Q%h&F4;Ji{WQnO+@jd$iNvnk`a2qMXJs!jqvxt9 zs3P%M8<{d>hhT&+i|Nc@xZ#}j`oY(?%}3^n1$|EzNi;|(3d^=IS61cem4EBukNxTi zp((X6!-6DqUmCOQ$c^OnfzjZBVg9^v;>Ej#*hFmjojm^{w!hw136DlyBj1?RMrUCF zjiy2wzob|b11e}_YPPEZsz20%Qs3{6IO>>ZadJMS5oW+%mcH6oMui()J@)s#50sH1 z{L4B+@jFKTWt&rGb;BM`?ZA{pkTK+shB#*DC3(-&L4&Can+WC;HEAevK2(XY=5r{h zX4TC-#4(su&ict%=DT6~vu)xhWCtV}fA&S|G8IMARk-|bi0&a@T0jvFnmO}m|Dvz7 z++d`R0s03e-9z3$L5LdQOA2p-ob}f7g(i>%fkn6jw>tvU6=6}uf;jZ&>&(R;$QKPD6{PrC($c%<&nMTXXP+lx92@7MH6E=yTJxjvfi8tg*I?d1v3Z$#QYXYG@{) z;LtCORI4!tvr132{Tl4;ceL!o0BxK=j;X@uzV3SKSn04V!pkG{xl*PX=}G=?nYg># zN6K3(51uLwC37X<;VZb{LtpMI`C620AZr{mD+d+m)4^|pX5vL+=wj>&w&Vuw zV4+V!dc^5h{o{f0fq}kN1jg2zKnM1;op3;JIj_Nc=t1}vjVnIBKC_&tbf(M(wj&v9 z>|#fQ>9j#C1f(6ECU~}uP?cTRgR5;)QOB^!0?Kanw1TRWx2dGJv$1}p)KcZ>)e%^K z)7KHM06ek2^%ouoi)ClTm&-=?)L~b^8#QJh4t-pqDW%l1Hg=4OESOK190TovEks9t z6rdOy(GjnWSr<}+wEBum3pADLkf;zhzDHj>iCZ{SC9F55QAD`}ZQp$y>gs|<03dz4 z<8upbYdU(e^H~HOjqdP3NLT+@I-?21VM?faiiQ0Q780_fF^Rl*PT(1OYgMYJ&#VK0MhKmGp1Cxl)O65XT32TK?iPLn$Ohy&EfHdnCI zda2*06UZ^T1n~kB?%+@D<48zR@aQ@XGk(25hOsBkDj9 zEN4AfhVhnHCX5uc@e58f!hz9#0MXLDPjomXKv}^JP9(^&JYG4y=&{ft>rVtmFd7So z1>mWZq+3Kkp}(ehx37HK!1D}HiWNM9qu~|U=IN0;*Sd2W<1Sax>p z);9zUB(xyY^Zmg@uujdi{+mzVmp^cn)`&hKnOLIUNEHVuo5P5I6{UpfuBpaPQMMP~ zUgsMj1n`|{;C22x$629)q1K8}Vfoz_I0-HrH8c!Un)5pu0^qXe_vVRCIvekp4?-?0 zHeEOB1sQwq&0LgqMa+^Zb$yx28J@9~;;iQ{FEBr?4FcD-6q*Yk7uE<%NWgt%8VfrD zt3uM#&FGz~o#ta^8H(r`_O}dWChBpO$ECb68@)}WT=l-SHwthkYa@Ln0(#xjBd3`g z9Hs_!8@a*H%ocig+f~#7`TFLUGdn?z0dqGH+O;X5P#9=*rr}^WTs-r=qvoP*1x-nk zlpc^t*v)wQ1^ICYT(qC!g?2{Z2|OvQOOt;#0Zak;&MW)p=6NF_OUJgq`_^Se8!zyf z_~|~Y2%I&EVlyWPpJ_R=5FWVBB}XznXF|N<+e^)I>zYpfnZb&QYNRXSZnCSRdX|ed zmFZ+1hbjAbxOD><3Cy!blRfmRl$?)JB7w|J~gD#d<2u-Q!k;KKN zXTi5p;FbCQ7^A#|dm2UST&5W@)c-mMDNfCZSWA{oNhz5V?hdZ)$V0twg)x#?|}k zTAq3>?~cJIskH=4VG~4nwk=!L-1r&4!Ay*|?=^sv63TQ5NV=M~uXc?6GyLWBAt{q5 zplyDa@>tFZhIpYQHC=QwP;qUd{NXt}^v9PJ8=i3-(-9&~wGk(4t-%3}Y@19+<1?@! zc-8)GZ&yI<&JK0O#tGhN;YZoCP~UZp2*_3ZDLS0dF{Di239$xRH*|9oJmp9@G+ayP zKstNbCt#TG)zSj%88!GQ!pH(65%E-C#>AO?N_}%zD$Vk@E>D0;8ef;J6L7-tkhZ75e~cSsM|@KS-@l8 zC?;F9!Fi}qG0L!JOZ)Cq*A0&3h8DC5Q3xcF1Z7l>NLL3@sV{3bD=N2~%VFfsz%3uF zik;svUvqyRuW z$Izy(f*wWVLFIW?O?`E~RQ>$X=3FuEcsSLJ;>gblsA|Ov)Vrh`=m&y0Cg2xBFGt42 zwBcQi?2$lzD^pzKK{jNZpy=jcBVfz&FL9H&|VTk)d+tPN=nNp0* zW;OGVh{f_n(%e{^_!}KvCrYuQw)4$6rU#QX#FoXy^T1@apPSa%Nirq%j#7y* zi9Z66RTIz`!X3R-3i1|RZ`qMLds|@UJcfu#H#54pWA?d|0Ajr|T#+^!?$r)c#mZjM znUjr!;o8^;D{yYE>VSF_B|wkQLG&d%v{QWe;&zBoY+xhD-!MZjL)StBZz$;Y=k0~m z8=GvB3|am1mDd{(oSi~p(VJbQQzPV%$(@+KIAhJOhn6zpe|=a>B5+43>wCh|@>lf9 z4zsE=njAfhM`#$=Tsq}6#CF%c?zeW@DgW-1(j~&S9k%58Em;nf8Ajkz_z~2{E&{9c zxhx|72EZG;jWl%FJFF7E#URp%Su|?q5yO($7`LpWG@XB<7&4u#_t!Rp0T3D3l#x`> zAu#y;r+UpKNG6x#YJL52IW+lJQJi+Fk-G^l+A?;aBWwtb-roRxf7rU?(=-!wVcD^# zet&ATePH0UC*Kx3PN(+vmt(S(fSIhJk2qy)2_a0z7fA)HH0U=*coWsLYu>pxQ7;3e z{RY+Sj9D^Tbo2F%9PvJyX~u>81tMGn(q+0Xp`U?L>M$ip9VD5R@jFS0&11ooJt#AZlKMMWkcgzSkpsFY&j)_xjL5`9obkp`m;gt4Z zW;vQy74p^s?*@&s60PBgBw%9kJwA1O8aG>k1_(^D(1NIy!r#l0pNzdb%6^TWuc3kF z*s5uA50|c z9|C!&lHc-gte|AvULwNXRKN2UgUiumq0bf0I;icDUZuudVq_TktFtXR*`^!`4-1@R zPYQ?&H=*qKF7m}i>*^peh|+w{KmXejQL~h2{^WQ|_=It)TIGbKgs$V-NhqNyXj)o{ zvjBSDYj*3ByJm&cw<;?n^%!1;5mU}iE(reuO%#3Hq#AIlHb>CbWti5?p9_PhEAcmxZ-l)5n&RG6 z1Bq>+YEv$5)dfHG;4o%m+-REb(V2KFW;x%-HeZ~!2x)A!*aYrD*nm^%$a&7Qw@FLE zr?T)Omduk1qA8z+7CF{Rjf!Jhrf3bVCo>(Yl>pBwK*nm+8AD_&bRaYINpNXZQGgH- z(>;|x16775zr6kGhCX|PS2~1DE zW)G!52_SHN$R&=bs}V8;pJ#uVP^Mg^nw|tvx1hDW93rvMrV0qa@V6bJ)!AC2Taq z`G^h^F+BFl>Ya$dpYhgImm|baphkasUoZXkOI8H0LZ^6GnK)ypumd4yQ_sqQ$DrSD zEuv}US6w1pDS2vfb_LJTEH}A;$-nS~6R9r-_!^P@g1Q$XIs@0t1 zwFN|dOoEj7y9e^fT2`(@VZ;^`sn0^6?_(MAIF3@%vXj4U;3OV8-~7aQ5!1%g#2sQJ z56`6669No*S~u4lG1vc2r1dga zqiLSFilHD?!nAL0WhD>d$$M1W=7q!b2Z{brAz8x~E@89N#hXPDzcb4%igS3r>XT%>I%+nsJMO9!o(M1Gz;~35r;M-Ndvyo@s*{ zGT9Yr707IwSGZS7nZ~><3aFJb^3g+Cg?r<6!Nc_U+J*PgLBbmTI60;5&gkiG$w)L=eTJK1sIB@P!VNfDobX#WJMAfbdo7C-toJ41~E z4&L<>BD_*pz7o?zUGQy?;H!A@NB;(x49q*%U z))~9X1YmfV19}IR)@vdZ`dZ<5_Ng zAXj-2`Cu5$kVl>T8ISa86-&Eh6ae*}`&QP5%TNPt9Ouvgh!BijwP_nLb(i2~o6{;q zo82iPf_Qdfxsn^?=Nd)f`82~0dkzT$4a?4hcwEtecU>B|Zq_3>TLW9~I(_KR5HI$J zixDwjR;!zv5FOPmY%7AVUTWbYHjfbf?;1^SKhg6~bQcvA+cJ*W)8%EJY0N~7fmbuB zq=6F!@-VLJZPU1tNI`s~M z)EDDqlsa*|+fIs4e|1v|5`@E})6UK<8(Mv)$KGBe0W=u$#FU`OHt@D1h!e>s=97Y5 zTKHOU2g|M*Ja|v&Y>Fx2g*Xre(K4(E!kBx}pDa>3v4_tzn_Ft@)|RUoUhr49)SP_7 zxI(3%UF)sP{X(rFGQx+5gOoGB(xN*5lw5LG@D%i*-SQaxo!0EqtF3%-!rv&=wELNi zHtTbFxmrv>av`u+aMOzm$=|QXMWn5>u@J3>CBv5KXm`8+=tKegXm@+pNM~tMguV^S zUAD-9<)5p0OPBzg{VfxQH!bo^QjcSh$xLuM5=WsRA?=>Lzz_oQvs)6p@dix*7FNm% zTiEQCw%Vyeo|=y0W^62RK26{V5=~fxsNiq-;ywLyrrpD@JY9KL%@kGs;~Hv;aox*R z2Fs6-*fBump)P@Y`%KL_;0h*I)MRoL9L8viOi#Edj||cctU`$+##G*$ zvSXZm7xEm7Po$jGY82kzCd_MCPU|%*>b+=-FSEadToYk#SG2$e4%%q)9pUxm+?<*c zwKvcRXWR2ja!+_=GjK;`x1vsLgydoLAbi zCC5nskpWi<{^FVKI=an+|8c`yFSr0LHvtY1X2Q=d_dd0^3wWQL)70$xMs^e)=ZTbo1lLLYyvSjASW`%*Aym8bR(1w~x5 zfa8&KN#4!vi<5JmA+r&dx(bTM2CKTWK3*``O2Q7-hC77?7zzuFg91Kb!iC158b?Tm zEt?X5nf3!I!$!|2SAmUdy91aI(rnQHj^r;$XJ^BsK<-SvR-OGyEcJsWH$L-W7hf)T z*>cF35UVx8roX&?+_YCNKhYZ547Hs4Ww4QzvV5O{W zeJ(?CLQ&i{c0kr62WEP~-<5;hzvM=XBXj#Z^Z=o5M1dhavi>H6eR8QJ@P$9+-DYKZ zIcmf;EDp%u`lA*TRM2ieh4p_GBWBl4IY;`wg|Ix%n;CL8>xiK84OEX; z(B2GoHBv-v=F6&LrO*dYKVfb|Tl|4j(LXU6AELDDMR1UDLp!42u2ib>p20*4pK-~- zz1%Tc)X~U~q1woCvD&qH2B+6^j73usJc?cMMVkDu{Mjzf!C2!=&J<-GfqQ~#a&5sh zN<$@`$k}a%0ru>_k?FjFX>7IG2;Z3Lfo~yL@MSl-W>o4BHJDb~xjSYNYeVfS;$Nv6 z|GUZov`5#z+j3GzyEc79&*D!U4?SZ3(j;dGtQ+Ou_%;Ksqf$~dlF&qtn~^a2sF7|y zua2yP-$Ejzk$!xNz;Mem0v)IFzN$R2M^T$peSEl-j3ots;sT-cE28&w=4NMIQPw9q zlKdeGxuTGceBzdB12){51*XuA8gV&&fM&KehG$N192wfyoy~C&V}XtJwYmBv>V}x8 zK~B_`p`kW#27P%4_t|r}OE*r?K|2I8Ku)r)bDER-Vk+TAST&InSNLrPfL++cIYC*? zw!J^-0jKH+bf*eKpzN30IbJT47exzU!n?j|Nyb%^EGx#@u*QNvRng$@&`LLnr>LQC4pawr$x$m#==&A7qDF- z6tt2z0g2^%r5YWv+Xd6pROO5Kp0XEyLD7-Gr{5o6o7w>gLSF<=X zp$Nhc_Qe4$Mkja?l;QEToyog~Nrs0YN+Ie;ZTLXnClCRufXq?|8%FDTXj~J?m!YY= zX-Boo`n{tPA9%cPPxOm2gh%09I0&6G;}v{!yGFY#|Zrb zWHwT!&~P}Ke`i84))jBf4OM`WmtcrB2*F^1ZYxnX>^P8}L}>Y-oYc^!xNzZTR}$uSEVm-VjU%hdt4yPe+(J2cjJR&cDX@3$S=vNkqvEm<1LF8QN@pGk-ZeVNFJs zhbS!}beArY-td#8cNS=Ig8&;y*dlyW|39yN{44n<@K4~Mz(0Y10{;a53H%fIC-6_; z|5X7JwUYQhsnHomVbNBl!3UMQaHw*lfFhq;J$kPE?$wj$VMx96pwDq+VId~w85G8i zvIHlj952fB<#UU?i$D`o9U2@Y$b@CWynzwCm&v7-Ls$v2Q2_@aE(Li#Ggl6<6Or@} z0BXe;J2y(&j{(2OqH3u6szXS=H&pib4XD8fp0Idd-!LKOrf1I~Mko8$y5%z+O3)pJ zOVf=0j&7d0?QQkjPmGDP3lpzsf`AEKDLg^Gp&C6}kG*ygHHgdqt8w`^u73jm1pW#9 z6Zj|aPvD=xKY@P&{{;RC{1f;m@K4~Mz(0Y10{;a53H%fIC-8qGfT&}*&~i(a>NMz- zptKY(@B<9WLf8++-+Jr(Oxl1Z-(xGRgdP$lj=LFL|Dau6lmNux?bT z0$iAetq@3@fOJakp)iCI>pSr)E;QM~4r$29Zw!Y8{;zilac2pny1mDQm#VD;UiQf^ z3F0?eB2PqRI_lke%rYO`!&R8L4}>ac1;{jDi#OmG?|?t2QgF1bN&YYzs3N=e0|XoM zU4`r*c#myzpD>3riPMf(%P@cH(nC3%`2TOl{;%#ofqw%31pW#9&lTwWdwgp(hvSt0 z4Y=m%6e{OB3_{I-5su>W4|$g%DDE*351BH(VNwL7*&P2_?T^;wR66nz;sK;SEJVEDvuf0PHJ8* z!PhU(7$hgeEA!lR)|k@5P74e(=a_rHIZyjPSN{Kvo~HzD-z>KqNc)P(jWTy^j!$lL zN8q{hd#cQ@4^gXKbW2@=!(yD;;DN!8pbYm$3&B?3G{bQl#;8+kjX}QMWMaexR(e_f z&>Uknt9e;0_;FLgm{v*z4g=Z#4Z(x$AdMOe!{piXwqd!Te4m!n&6p+jEvfg&ppm_Q zWM5(^wFreWZ(fxgcp6@Q~XO^65O_%=%YdH z^E@FkE^OtZ`w7?g_#v#LboW0!3Kh)UA`17N|DFA43q~vU)SVG_Y?AYwI0;JzDafpP znP0q|!1vY75C`t%+M&9pU_B2U?6ZJB@<7A0Bwt3?M{thjq~hdOUK}{KD^Y_wm{C+6 z-}5W6IELspFk|gb`?b=kcO#a zP#Va0(>LPDMYz+Hyn5Qv8#xVfd65oUGa~q;e$+1D!9In}X12e^ znHR)CPsP|=QdtQjGQHHmEs3hj)*F8pfH0-nMZiaLFo;niNSmJVRbhbWe)e&gU7Id) zlHEh{BY`3SYDC*fgKZ=^r$1CWS0%d_S_I$h<;oZW%K_sdnPRtP;JDj)Xqf0Tj1kU? z8Rd;;$Zt&%0cp1O!UY^qT#_-%*XH_d4^Qx5i{<@NB^Yl~;P5G__{C;pZ)4m$zGfP- z^_gV(TIXZ&y_@vQhph5rwlcmA%l7Ee-&G${0cSiN1k;@VO(WQZ#waAHlnALRkH@JO zC)c%<^IV?)peV^CJqX8+6~R@*a~Cfz=j}08m~R}M4n8Anb^t>fJr^OQOB>pDl5FFf z9a)bAd+qPS8AsbxA*di6d-$;Fr*$G_+_v5k?u6|Op59+GXBz$6wM*Z*=B2Z4n7rZ<`8pH!D=Lc()MvRIIH;*yx(9YTCN_RzNM??C3I)c zy(&q$PHv` zt37T*o;=qxlWHdZcPt^@SBh#PW@F)=-1-6z)vwpq7K9R+_YI56gi~GOockxLHsGdi ztDhp5XIvPxs)>M7&9+Z9j0hvY6jnP7d(*?(KFn3D_4CBr8%iG1Fyb(GWIlFPw8-m7 z)?ndG*H3TsJsjk+UT*9_S_BU_IK_;BNN7tRQ>;+3^PKsp9v8E=yC7U2SqbI^%YpkT z4c}~0fNFctHtJ=%>jXvEi(m#RID+pWMTNe9h=%unPuc>l>XdP>Rz63a5Va8Z7ay^b zzXX*go6E9!duUoHl~FLP{Y8E`diyv_1)lWLe7TE2Zp1~K_Sa?ORqtBPTdd}o`XE{1yD42RlE_z(atP$4>G2*FcBc_59$7r*gGy_U~kS11+8tUcqW zgGRxWvkz8`p&(yH}*7ssh3AUX9(056>0lL1^*W+tAh7QP!O>|JmrjuP@rA z&OdF!uYnZ>q{Ya1$vwZ6){J>wJ2nim`lUx(>gWMcmu+(JFoy+wk$J4eO)5qlszJBlE<#)lZI>Ga`blJcmOQ*XGVnQWA}rbIB1(?ZC_#OB^Su8oD8B+;&mrD8Y+Y=oa9a$v)$VF;+cII;$Yhd=P>nQyQzl~1&uM98a}k1|(uU)h6DB=yvA z%1dkhdE|wX{BWtDqKO&pNYO9GmVB`FWEm8*O=PqzNbmO0?duSOn|!)+_)R3o@Oj}Ob6)i(!DzLi4JYVZdfV1S#95E|(I=<&JrpQQ!^4gK?9i6sVUHxZZG}x*O9ux9hjz{7ox{0Wx4^~FKnF1qo8Suswx40K18kw zg7LDyI*cH4ABx)FSqR8B89Y=PTWJ^f=Agr!+eJKASIcvjNW!~ll{f!==Yi5N_#IOS zfMJG=Y>*?7rdXNyL_?2s7$xzGKo{wGh@-kTy{wvvYJZip_uTp|k%)GNcqaLU4Lb<& zijAecURc%dL){#>pwnyTRZ53#=NK*SvB+A?TglyKd88DleA8Bkw8g2-WX}Zg*sP~Q z3N){JYRH?&TB47Wd*k9_{HERMK&ZP3nv77IO@~~r_nWM$4~qr+;vG!urWHE4ZU}0h~E$!n*dBXG7aLy zC4cq2$Lj6foBG5{mp;(T@*xZ`({?b04qmMl#)GhjRu4kZfYNjnY)rZ=e_#Z^0v?HR zl8jt`>s*pV3O6V^p;d^jc8q~z1$>af6&}S_@|0h2FO7S zABxNM1yH}pB!yS05aAU9vS|w!a4~=pabQlz0C{0Ib9yLl%~Qk_Pb3jfGUXs6J~KJ% z&7~_Tr6A^Q$L;0zvM!np!fu@~ngZ2hXJl$5oS_KkB^`As7#8kW5sXSnodKE~JB}2-9Z#!K zfqQbQ3T)v)%Fxnpj?DH)%cf%;W;u(g zshXPX?&4P6^Zd?T+*9|Q|AU_}dL%trcybuwYt5k3Efg8L+Mc`KmDmoFC;+^o!WdMs zR~AFDjP7#z;C3mb$gk8%{ZfVCIt30|yTd?AE2G zH|W|0pOh(=pIl}ja$zur&grXdKY9-<5GSxjbB^_yl-1gauJs)>>s%<-hKnFdTJ=Q8 ziFH&Rr2XVF3MLz32}2t94UW5@46D9e)v7p|EMBuYaT$RA2RnN65!3~*6nE**6R-aM zc<_oIdC%gSGQA{GgM3aZEEu>p{t5Y{N8>Cd>FZk*Bj4KDS$v2}p!7Ss7=$6}wDPw} zpX=|Qy+Eln_N6S>v2cvIT;Qwz8Td|ayYb~tEA27;eX1nIQC+Im^#cGsZB}<=}#WB)y|}EIXL>} z+9F7>pQMFRgVJM3dN61R(-oO-OU@hoBTXH!y)?Hyw@M??r}yPPeNKqb^z8tiU)<_~ z(&RF!ZnQ>wMwd|fP^LPce6Rq%iyDp5~ zvanq7DKe4{$x`h_5PAQd*uP_h{p?aSMR--P5yDQ*wTm4u1kQy9#8y$wX9aJB@iIov z9#Hf^a?q7T^99s5nO{GHTQnXzMejcMC>H?6AUUlJMf#q}V}Tj&S_$ zK{;>RVaNjH+kESUb2zOs!%d^0Jo1bXzS9r?1chx?!;KOtHb;|?)p+MIiD&Xo=`gW( zL;>~dFFs)4?~Uv;id`#{njdswm}+pY-z+~8pVut}vApCw$$>kd_K}13ASH_E@70I7 z{ko2To(qZtFr6|8I(F}lG=C1lGXRtr=skXTS`q57g6aXbIN1k`MLb^p~0eU%@nTe(qzY)C`VFE4vY2ypI_ zu~oY2?Q>uiEx-BMM~--m`5a(%gbRSk_L9NmF$TS@_4&yhOZiV`zE4%yilN7uPtbJ! zM);glYtMBEf)t5vvKhMPJg{k7=LHf2vX6^PXZRTkq~!t;b)|&JL=N8Bi%pA+pIDYE z{2AoW$^a*1f9|(};8ky24i0N0Gn)576*t;50S@qJpL%C>TSzRD&H%XcrdmauhQ>g> z_~drVqMv*j)B8Lc*092}r=5~(Gx0;XSkS=+O3rVvpU?$W2NZ!ajHY1W(b%cFESqOg zuCLsbw%sveqAJK28@Zz769g$Yy9i#h~@HC>BdS7&;iKb5CGbn6*;j z%CkcWol3Uxi^%$Wq+%By*1aE{MI&LEHYheTRHGIS&(e?q(j1z)T-_mVrDw~&S)(!7 z^%hh8?p$T8M42NUeA%L(l*Ql&dv4gQH3m9B9S;Y$OY5l|-q+TEthg7=_#5$T#k?Hq}h4}X&?O*vZFiCkE&h9z?wpUgn%U=3u3w3^VImFX3xQq9&Nx?zxVg2cbU zmGmO>t)c&2^Op`Pp zG>mHqAmnddXi;9Ho};7;F}_!k~9o7KNb)Z|Jrj(c&Z{ETq#R-u2 zv%6s4QdJxj=JPC{DA?}XSL2H;`Tt?c5ktFtJ!U?QGiLlPfRez%&|EAP5xXI-0NS3h zcNxZ<`bp7{nf!M{^{H%cE`i{=+7c>5excUnM@@wtt-qq`9>*x~PZ(4pU}2_0_7FD) zG~-aDv>yDug363&j})(wtOUl6+QG;fFh&tRdNgEX_xFxCX+GM_h+%k}ok6T~DV(__gbio~N&2`xA4*=YJ-$SF}P1<^?Q zg1~+4xjR&P`glD&c1_ZxjOOT8c!KJM5uNrBt z{U#JuMzDJH9J88Te$0#DHM$W!Vu~n}i({nSn2k2#mLYl9sP%{+c28Uy40s2;Qw10r z>B||nSEav5vJ}rEIH*YGVXVa2+^w*r#3!^Pp&qSgV`81+<=dLGa!4jBBjz_=`mEYFsN*lIL6SM}g_(2?@Ji>3w6U{o#Y6ipIAMm-`cd5cX^@vzIT%@AlKiY#VcIjp`j5EyY3%~S8N$=E?;wz|H#gpfamgz$P5Xd$7|E}d zRy4iJ6E79qnpT!w^9X0g^frY0_p=@9^?@Wz(aNB0!#RYL04Ok@H%IN^nE)uBY(-musYW(LyWg{Oft;Lhq(o2@T( zFJ6cb3&SB=@lZ89LYcFJ4%fRERW@@WTII6kM-Q_h>U4O2toduG%ghtK zQAvaaTj>y7eS8ql>>SM%8THbRi{P9SNynBWW|JHn(JFb4u~AQ z&-*u3^*NbO#{yE$wS19a%a^*4-RK0N5eDxni06p?D z_xTE#!hVGG6rbMy! zWAy-Bm#n-T4+2eELe0=3nk|+$#>~izAW{BP`$X(9CW^daSOo_@fRu6X?rbT0gN^$B zIwM8p_pe%KPRVDrf5JT8h@ghotP1!9kxDkMxP#Hl4!1iE1r3I$GY0)~ZSw;8*z4c3 zr`K6Ok(N`3!H}e+aMf2ES?KP@if~%d@Qo+#H`rcHIJ?Y9KuJtnY|htjLG22!NIef$ z$XtV(KSEVA;e8re?-ieSh`@bq@+3%Ks z!PH;+)r*^#m)w!eg*Vh^f6IKf<%fWr%lU4)IFM{ul5yb=$SZ2{u2c7r@;ZFN5c)>D z@qaje{`T9CzR5u^{2(*+;wD&)qqiR$#neo_(r1+{q=1R1Ix0&adqwC*2q*$Kk1#kx4XJ39r`5$1KhoFe>n! zh^00y*Z97_rzV&a?2ec(ZpUKPAk_g9XP-TCrIWKd14V8QM)_W`WgzIvy#?PVehJlnH7 zd6K*>%eRv14f#u%bV!pYc!Cg&?L7q$p%nCQ^PJ04R~j$jc?`!0sLc`vu3m zpRIGtm(bvLbJxpDsXfW}zb*-aDoNRm2F{7XV>bg`Cluo*Qhk4k82Zj4#;^%56!^@y zQ$v|oEtj~Rhm(M%|NHiNv2=O0MA2?Xeb$P?iCUKUdzxFUmAz)1{;CJ~*1LykW&0Hs;ioE{6nj2lt?? zBIvu2*m=Fg^9DURuD3xHcgNI$G`u-jc`yjV#M>DgSEF$V>lJk;{`)^0AZf^X3kHJ! zX^&SW^lpO;vZ=KD&g(CqJYkJ@q=JV&4PMmHvMhyAZ zk&@4txbUM}mS&NMl-sp}C5gbM^{{jXriul&bLF^mO%EAe3*7140U@<93Vi8S&w{xs|5sFH1NV{Kx|U^^LlyGhan!kgu}4 z=sNA=C#Iwy;8p#70%%Z-U^@aJ8I~l?XojN^10_VmU(6++QK1gMH7Tef|H0l=3kbl+-JMxvIT3e!HV?10V3p;MQ-0}OXX23 z`ttw#Y=oTMGZdddF`4V25*lz*h(iB?zBC$~F7T@}PpQ(yF=M7B9ve!DGF}fWL5E&< z#TrvmRWkbAM%gVtwaL+_bA~-+%|cvPoIK_t>&-iD1UFjXJ?w@Vy2lgP={5!ygWhR* zc;%)hI5EO5xchMDUN<(&XE}@Z5mJB4UdtVEJCmjJw1`f;(`W|kg*Sg#Gq!KSTtFQv z*Qr)&VnQ^Nt5`)a4V^k95W)9vDyHJEwXb?#*8C49T8Dm=@ZzlfWWwIW4d-FSU+8$6I@=yppSSiDJBF6)#t1kDwnpa7nLArv;83j4#{o-EJ_zc!&^@h8A z&}jToE>XEq{Rj|}dQ0bHG;<6K%T<_i$5?kYSS9Q^T~I^ANE0$7__C|R4YomLc}{&n zNa*{+&u<_l_LO)cH$WO#bd1qAaH_FiqmY5ies89) z7{%7Wz)>h+m6mtF1`ComiULhT9OMx61((8qM1cI%ed}tApQ)Ji)eL|G46Co<3M)^MRi(Bd7;Wn=#>7e9^5kvAd=uK>bu0N4*$CgF5_V9jLBXatKe0PW$II*1fY1x@kaV$?c896M=d9y~d zN+g*O#Ifueikf((rAcFc634A%@L<$jdk(1ov}QN3+ec(_a)dLB~)PP z>Q8!H1Um5oJIoG2C(K|Ga$8ZAoLs=O)NF{^C3blO4LEinlFUVfxy%+N^{LLiE;T@VFO)(Lb zQ$Q->fEon@J1-4MAYoqpD$0e%&kk&wpP`5pqgqamt`x9wY4ASR9MCL99vjUpFRImi zu+dyh9ybRz!~_j54AJqI~SXnz;p6(aRt^f?Y>QQ2($nm}q~GO3<|7 zvQP&TRaj4w_z1-BxeYN@7+5&N3-^O*g;JO)g3N$xR#xW3$~*G=3+3m zH3MhRo>)bvc%NsYf%-!$>lM8oraB z96?6#5-R#4?MAPFCU6~(#>hQWGP-52Z*8PIzIjJ0`dnWN-@?=-@~t0c15)KyrkU}e z0d&?)CQ85wGKxXVl$0hDe;gX^)3l65*oCC3U3|t@`^nYdq2x@^_uCcW))K{soq|p? z62*xn28#os!hc2=K3S(T=HKM69|)}S_I6BcbgctWO0xnOsv)Y&T8^A})IU_@Gw6U( zE_&`@;n}2vtVgI}HBtPTwS)0`u_eb!&qq<^C!7VW(Mn7_C=6M}ysG(hMCR14bFJH$ zgHDYy!R37a#J7f?{_44c1@qnV1`Ky^Z;lNjw0VgKkqR5;W+xEf)Q2L|E~vEtwX%sT z6i`R^eM!UsC)$62#Z%MT*lTUyH~3LW;?^(K(@>Ae1DU%2LVC?A`d06u^_u?D!(PBH zF8M)nKArN;&<|H)9(qE--7mWUKT*wRrNj%jaHejP&W6lw`S3~ zio5}J1}Pqt@l>_f5(FNDVY*^r!0g6;p)zjZI?yKEz^#9 z@P^{B(XWo3{+BKbT5y!%9@<*Kv~(CvFhwl7ZiK0oWm%TSUzu&2ug;>MgU3=EaPk|^ z1i;2D?M-djGWe4uc#I*A|?Ox5p;r+%-X2BJ`;D1g%6-y64aE;)%5Dhj8 zj9W-xJSD_&SJ)pqavvAV1rA7gnRPAYk#*18wdTE^Vb=gN1YmT%o=Z{6sV?}qYtlDu zs5(Krq@l{Ks6O@&mN}=B6f4wpVno5EiGZdt5wE{Pi3n5t(C0mlv-lVMlwQa zvP0Ou%(^y|wHj0jzwp$gq~VffntmT|VYuFGl|*`dFlpHkbE2YL84DA^!8590FVG_m zt$a?D;jH`8oz~p(yI?pLzax?9Xfct9dfSx0(P9#$U7Pb<(%tkvv{Ot%lS7B*!V(yM z#$C$L^}8n9p<%enOUC=)%%zZB{v#V+m0l+CLLzDmnI~%*PqLHe#bqs;fL2kp6a8lE zVL(yraSL31*P<8~Z2QNBfDAibA3Na;Z`Sa=GoL7nC=NrLsk>4C6UY#)W5V}Hz0K?pAUS|yrzWc1xs~}(- zvM9vBH1D12K&9tx`flQM4Qn~3|HCHyUpQ_G`5KH+}gjHh;j{Rfh^;7n7}2 z<#Lh!CC?iCa$2)&yH;EqgUa`6T~XCrw~3%L@5H=*8vvD_STD}6`Qrv@lD*bS;g%RH z6g&N@D(Eo}>Q=mcnrj|5%aY_ugO#<$?@CtF^)q%EXIZ&+6aoMtfP6Wcd$;UDn?*r#jUfw3Ak9@gMz6uRC1lRX1t*gKN@W+660u)r1|v^4 zHCF`9X@(f5&fRxVTz3)jlx~XY`bZXalxp?FOLb!^L;P1)`+74-`rNNG5prv3vav&` zH4#Tgl>M!+!g4$!dms!F90z25!CTPc-+wVk!?ccMCbOKAS==H=i^}yx3j*)E{gXb; z{{EqLF#RnE?-2(Wo8l9*O)MRqNyK#O$re8~-xNQ1bfbo*J;Gj)wT>1&)UGBgYzW!;wC_m`*aAmbJflfN@6+R0K z7w%e)RIrig380E zvc_}TCf_|Wxp6ufnP_3RuPBQk6SXKRBkbu7cCc)U9%Uy{Pu5_6=c>E`)WF0glPgmi z1q?$7$AvU+?9*953pvWp8|EcdLoJA$tIQ``W{l8iGpu!0*5`b>9#_eFO6+6ah6_w<=YFv#2q+r)GADY+NoClXk} zFL&y4v!X574JyJ-$nupKI0KwPm42$QRnNl?*&%bdiLuGu(4P+WBk}&qTa61!N)*|Z zPLqR#?+1@2pNl?Yqzlhi%(mUg$R>%V{r+_?44d!8wWe|ak6I{FE{C_{TiT(YAu^53 zQvI3VFVzmZZi>wf8*tO$s(|A?U@+9oR?rlKOM4Doyk`kK9Yaww+19l2qmkxQa59zY z&YJq+T9%X`z}_)d*i^e0zQq_4{ouGIs8su64{1G|{)$eAM4Mifu-Bu)NIVPGp$3+S zse*hkrf(g1w0FW{t4t43&sFX}<}`NPnOM{iDI7;{6ld^2iZBuM`Ymsvo`yILfBhC9 zEC2;<-S41f$XI1b!8;rRlJ=S-O3b&fLM&TU=v^{tN4B{d(d4|ssFFp*F2k+n#e_jK zU)%wBBPJf;W-^mf!>jhdV91AFwvpN$B0L#`Y(x4PV83z5cM{^;fJw*IzR^R_z->f; z-2!G3A?Em~bCh!{5mT45TMFNqUzQDR6#|>g!mQe%a0`zzlAG_UI6=6N65n(Xj9Jhg zJ}*MZG>PyrQc^zBAP1aw%D{!>C0}9$No8OXbs2djjZmt{9>HO4rJ(f~@2sIdhvj}k zAfr1fHl)<_>BO?VWNeDP4zjt_Hr>o+W6rr6`d8l;ucUO3;0Z~p< z-AF=!lY9Vs{2EzK^Mqn6s{lwb{5(TEdz+AB+JzTwyi3U)1LtoVsIl%k|8%i^Vrpj4 z>wKZ(mz2m`x1%q4;e%z#I4KV5v~+Ni0+)b;e_Kx*f40KdzP6qSX=bxQZz{OC&;5*A z^b;p}bJs{w;f@~_&YgY76fc9u%scU}w`+}$xAidT+8d5MryWYoL-AV|rQc}~zJ-d- z4)c9Q`mo|XlY%N~v!X)2smyf^dO7u7J{mgq)8Si<`Il-0?2e)s0cnxtYlkVn2BHLb zos5NCwI5=U4@@AEj5(^~=IDKbwz`u0|Cl1L3x=ZUu%<@CJRsYkyk!m2VL|-2U9q9+ zblAW53jYl&X%Dc)qbxXWX|H)$0xYOOh(fSE=JbHL%sdx~X-qJ!!bbO8(H~wERo8vC zwOmB8dM%W!Fi`k^{rtk$_`d|c1il2m1il2m1il2m1il2m1pfC0G_R3mR(#Y)>h~&# zuP9j_UG6;+judb$p7^qOiV65tB=Gl-YdyO5Aklv_YmH~k=d@d@4#tTH-?65>{n)gh zih@z%NvhMbvi@|Bm1gf6K)Kt$GTtyi*XyM+8n-v=;8ibQlyYso3KwkX`1{HieaN zO$EM)5oph1R_a+>^-6(Lf@|oDApG$%^S@u|*PUMiUjknOUjknOUjknOUjknOUjknO zUjknOUjknOUjknOUjknOUjknO|NjV3dCPVhl;={#P9+PeT<;%otj$Wc^){ zBeoj5n=w!i@W8Qw+2Cc0e|K);zMP}@Tsg%?oa;tB=&zW#RBqZ zRSq^ouO=gfmA$%#x=mp*`25v&l1To@gx!0u8)pY|`IB9lDA2Iqg4{y_a+GHiY}TSd zhpF5P=fEUd@Ts!73uL4ogUT?OFBH((>F8BBknnuXW{GT<8jA?@l1v>tYf#GEa+i{Y zPyGw|)DlGc|IbW)Rr)3HCGaKiCGfur6e1iOYK=`L6uO6T|KcX4fY`2vgx$L>?rd*}vSTWr7(EM1hQZI+htxgu+aiqMnBOy9eLgXu zfgSnw6L7&&oA6J~5)ei3GY-88wNbOj1bH`fj|R@ZkPX5xra?QJ4p$3?Zw-KLZZDyu*8r=N$x z|9!Flvrp9H-_^TMbgWp4BIE-L%a<*Gaa0OVpGsKBS#h;+$^4R*A{BE^0x}QPOq?pJ@{0({Xc3+ zWP0DV*q>~6WRA2KZjBe8UT$TD-t|6z`M@osf3myX$E2)#$GgJ}koMi!wXiQRsKE-V zE!~9GP`qPqwb*&7;6Jg3$P1Z^#g{5ztLD7X*L>;M1& literal 0 HcmV?d00001 diff --git a/modules/consensus/beefy/verifier/Cargo.toml b/modules/consensus/beefy/verifier/Cargo.toml index c7b5e5134..80c8cb5a5 100644 --- a/modules/consensus/beefy/verifier/Cargo.toml +++ b/modules/consensus/beefy/verifier/Cargo.toml @@ -23,6 +23,7 @@ sp1-verifier = { git = "https://github.com/polytope-labs/sp1.git", branch = "pol alloy-sol-types = { workspace = true, default-features = false } sha2 = { version = "0.10", default-features = false, optional = true } +hex-literal = { workspace = true, optional = true } ark-bls12-381 = { version = "0.5", default-features = false, features = ["curve"], optional = true } ark-ec = { version = "0.5", default-features = false, optional = true } ark-ff = { version = "0.5", default-features = false, optional = true } @@ -82,6 +83,7 @@ bls-crypto = [] apk = [ "dep:gnark-plonk-verifier", "dep:sha2", + "dep:hex-literal", "dep:ark-bls12-381", "dep:ark-ec", "dep:ark-ff", diff --git a/modules/consensus/beefy/verifier/src/apk.rs b/modules/consensus/beefy/verifier/src/apk.rs index c570689e6..5f69aefeb 100644 --- a/modules/consensus/beefy/verifier/src/apk.rs +++ b/modules/consensus/beefy/verifier/src/apk.rs @@ -45,6 +45,17 @@ use crate::error::Error; /// Half of a G1 point, and the width of every coordinate the circuit's verifier deals in. const COORDINATE: usize = 48; +/// The aggregate key reaches the circuit as six 64 bit limbs per coordinate. +const APK_LIMBS: usize = 12; + +/// The seed point the circuit aggregates onto, `HashToG1(dst="gnark-apk-proofs", msg="apk-seed")`. +/// Hardcoded in the circuit and in `ApkProof.sol`, packed here the same way a key is. +const SEED: [u8; APK_G1_LEN] = hex_literal::hex!( + "054abdb6c5522fe2f71d55922d6f674a4908d39e2b33efcc62520c0621ca0d6a" + "6d84ee717b7fb1cb5f46687265be01ce06e518322165fd114cdf6b4ab59eb45e" + "9289cc4f6f7948d6b680cef9ecc0e0e0f96bd59a578d58c33c0e10db9c25b5ad" +); + /// Verify a whole update and return the new trusted state with the verified parachain headers. /// /// The order matters and mirrors the Solidity client. The commitment is only believed once the @@ -187,15 +198,23 @@ fn verify_apk_proof( let proof = gnark_plonk_verifier::PlonkProof::try_from((&mmr.apk_proof[..], vk.qcp.len())) .map_err(|_| Error::ApkProofMalformed)?; - let mut public_inputs = Vec::with_capacity(APK_BITLIST_WORDS + 1 + 12); + let mut public_inputs = Vec::with_capacity(APK_BITLIST_WORDS + 1 + APK_LIMBS); for word in mmr.bitlist.iter() { public_inputs.push(Fr::from_be_bytes_mod_order(word)); } public_inputs.push(Fr::from_be_bytes_mod_order(apk_commitment.as_bytes())); - // The aggregate key travels as public input too, as six limbs per coordinate, which is how - // the circuit represents a base field element it cannot hold in one scalar. - for coordinate in [&mmr.apk[..COORDINATE], &mmr.apk[COORDINATE..]] { - for limb in coordinate.chunks(8) { + + // The circuit aggregates onto a fixed seed point, so what it proves about is `seed + apk` + // rather than the aggregate on its own. `ApkProof._encodePublicInputs` adds it the same way + // before handing the inputs to the PLONK verifier. + let seeded = (read_g1(&mmr.apk)?.into_group() + read_g1(&SEED)?).into_affine(); + let (x, y) = seeded.xy().ok_or(Error::ApkPointInvalid)?; + + // Each coordinate is too wide for one scalar, so it travels as six 64 bit limbs, least + // significant first. + for coordinate in [x, y] { + let bytes = coordinate.into_bigint().to_bytes_be(); + for limb in bytes.chunks(8).rev() { public_inputs.push(Fr::from_be_bytes_mod_order(limb)); } } diff --git a/modules/consensus/beefy/verifier/tests/apk_fixture.rs b/modules/consensus/beefy/verifier/tests/apk_fixture.rs new file mode 100644 index 000000000..f3c169320 --- /dev/null +++ b/modules/consensus/beefy/verifier/tests/apk_fixture.rs @@ -0,0 +1,92 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The runtime verifier against the fixture `BlsApkBeefy.sol` verifies. +//! +//! Lives out here rather than in the unit tests because those pull `w3f-bls` and its arkworks 0.4, +//! while `crate::apk` is on 0.5, and the two cannot both be in scope in one test binary. + +#![cfg(feature = "apk")] + +use alloy_sol_types::SolType; +use beefy_verifier::apk::{count_signers, verify_apk_consensus}; +use beefy_verifier_primitives::{ApkConsensusMessage, ApkConsensusState}; +use ismp_abi::bls_apk_beefy::BlsApkBeefy; +use polkadot_sdk::*; +use primitive_types::H256; + +/// Generated by the circuit's trusted setup, and the same key that goes into pallet storage. +const VERIFYING_KEY: &[u8] = + include_bytes!("../../../../../evm/tests/foundry/fixtures/apk-verifying-key.bin"); + +struct TestHost; + +impl ismp::messaging::Keccak256 for TestHost { + fn keccak256(bytes: &[u8]) -> H256 { + sp_io::hashing::keccak_256(bytes).into() + } +} + +/// The two verifiers have to agree, since they check the same proofs against the same commitment. +/// This runs the Solidity test's fixture, byte for byte, through the Rust path: same ABI encoding +/// off the wire, same verifying key, same expectations. +/// +/// A disagreement here is the expensive kind to find later, because in production it looks like a +/// proof that verifies on one chain and not the other with nothing to explain why. +/// +/// cargo test -p beefy-verifier --features apk --test apk_fixture -- --nocapture +#[test] +fn apk_verifier_agrees_with_solidity() { + let decode_hex = |raw: &str| hex::decode(raw.trim().trim_start_matches("0x")).expect("hex"); + let state_bytes = decode_hex(include_str!( + "../../../../../evm/tests/foundry/fixtures/bls-apk-beefy-state.hex" + )); + let proof_bytes = decode_hex(include_str!( + "../../../../../evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex" + )); + + // The state is one abi-encoded struct, the proof is the two the client's `verify` takes as + // separate arguments, which is why they decode differently. + let trusted: ApkConsensusState = + ::abi_decode(&state_bytes) + .expect("state decodes") + .try_into() + .expect("state converts"); + let proof: ApkConsensusMessage = + ::abi_decode_params(&proof_bytes) + .expect("proof decodes") + .try_into() + .expect("proof converts"); + + let signing_set = proof.mmr.commitment.validator_set_id; + let signers = count_signers(&proof.mmr.bitlist); + + let (state, headers) = verify_apk_consensus::(trusted.clone(), proof, VERIFYING_KEY) + .expect("the proof solidity verifies must verify here too"); + + assert!( + state.latest_beefy_height > trusted.latest_beefy_height, + "height should advance, was {} now {}", + trusted.latest_beefy_height, + state.latest_beefy_height + ); + assert_eq!(headers.len(), 1, "should finalize the registered parachain"); + assert_eq!(headers[0].para_id, 4009, "should be para 4009"); + + println!( + "apk proof verified in the runtime path: set {signing_set}, {signers} signers, beefy height {} -> {}", + trusted.latest_beefy_height, state.latest_beefy_height + ); +} From 274f17265e6fbed48b7bc69e1d4c73e1d1f45f5b Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Wed, 12 Aug 2026 13:26:39 +0100 Subject: [PATCH 17/48] read the destination's consensus state in the encoding it actually stores --- tesseract/consensus/beefy/src/host.rs | 72 +++++++++++++++++++-------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/tesseract/consensus/beefy/src/host.rs b/tesseract/consensus/beefy/src/host.rs index 25d262897..02556dc83 100644 --- a/tesseract/consensus/beefy/src/host.rs +++ b/tesseract/consensus/beefy/src/host.rs @@ -28,6 +28,7 @@ use subxt::{ use beefy_verifier_primitives::ConsensusState; use ismp::{ consensus::ConsensusStateId, + host::StateMachine, messaging::{CreateConsensusState, Message}, }; use ismp_abi::ecdsa_beefy::BeefyConsensusState; @@ -95,26 +96,52 @@ where /// The parts of the destination's consensus state this host reasons about. /// - /// The shape follows the variant this prover produces, since an apk client identifies a set - /// by a commitment to its keys where the others carry a merkle root. Only these three values - /// are wanted here, and both shapes have them. - fn state_summary(&self, encoded: &[u8]) -> Result { - if matches!(self.prover, Prover::Apk(_)) { - let state = beefy_verifier_primitives::ApkConsensusState::decode(&mut &encoded[..]) - .context("Could not decode apk consensus state")?; - Ok(StateSummary { - latest_beefy_height: state.latest_beefy_height, - current_set_id: state.current_authorities.id, - next_set_id: state.next_authorities.id, - }) - } else { - let state = ConsensusState::decode(&mut &encoded[..]) - .context("Could not decode consensus state")?; - Ok(StateSummary { - latest_beefy_height: state.latest_beefy_height, - current_set_id: state.current_authorities.id, - next_set_id: state.next_authorities.id, - }) + /// Two things decide how the bytes are read. A solidity client stores its state abi-encoded + /// where a substrate one stores it SCALE-encoded, and an apk client identifies an authority + /// set by a commitment to its keys where the others carry a merkle root. Only these three + /// values are wanted here, and every shape has them. + fn state_summary( + &self, + encoded: &[u8], + destination: StateMachine, + ) -> Result { + use alloy_sol_types::SolType; + + let summarise = |state: ConsensusState| StateSummary { + latest_beefy_height: state.latest_beefy_height, + current_set_id: state.current_authorities.id, + next_set_id: state.next_authorities.id, + }; + let summarise_apk = |state: beefy_verifier_primitives::ApkConsensusState| StateSummary { + latest_beefy_height: state.latest_beefy_height, + current_set_id: state.current_authorities.id, + next_set_id: state.next_authorities.id, + }; + + let apk = matches!(self.prover, Prover::Apk(_)); + match (matches!(destination, StateMachine::Evm(_)), apk) { + (true, true) => { + let state = ::abi_decode(encoded) + .context("Could not abi-decode apk consensus state")?; + let state: beefy_verifier_primitives::ApkConsensusState = + state.try_into().map_err(|e| anyhow!("{e}"))?; + Ok(summarise_apk(state)) + }, + (true, false) => { + let state = ::abi_decode(encoded) + .context("Could not abi-decode consensus state")?; + Ok(summarise(state.into())) + }, + (false, true) => { + let state = beefy_verifier_primitives::ApkConsensusState::decode(&mut &encoded[..]) + .context("Could not decode apk consensus state")?; + Ok(summarise_apk(state)) + }, + (false, false) => { + let state = ConsensusState::decode(&mut &encoded[..]) + .context("Could not decode consensus state")?; + Ok(summarise(state)) + }, } } @@ -234,7 +261,8 @@ where .query_consensus_state(None, self.config.consensus_state_id) .await .context("Could not fetch consenus state")?; // somewhat fatal - let StateSummary { next_set_id, .. } = self.state_summary(&encoded)?; + let StateSummary { next_set_id, .. } = + self.state_summary(&encoded, counterparty_state_machine)?; // just some sanity checks if set_id < next_set_id { @@ -313,7 +341,7 @@ where .query_consensus_state(None, self.config.consensus_state_id) .await?; // somewhat fatal let StateSummary { latest_beefy_height, current_set_id, next_set_id } = - self.state_summary(&encoded)?; + self.state_summary(&encoded, counterparty_state_machine)?; // check if the update is relevant to us. if latest_beefy_height >= finalized_height { From c177da6ff66ae437639016199455d209656519ab Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Wed, 12 Aug 2026 14:16:47 +0100 Subject: [PATCH 18/48] commit the apk bindings and proof fixtures the branch already builds against --- evm/rust/abi/BlsApkBeefy.json | 451 ++++++++++++++++++ evm/rust/src/generated/bls_apk_beefy.rs | 24 + .../foundry/fixtures/bls-apk-beefy-proof.hex | 1 + .../foundry/fixtures/bls-apk-beefy-state.hex | 1 + 4 files changed, 477 insertions(+) create mode 100644 evm/rust/abi/BlsApkBeefy.json create mode 100644 evm/rust/src/generated/bls_apk_beefy.rs create mode 100644 evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex create mode 100644 evm/tests/foundry/fixtures/bls-apk-beefy-state.hex diff --git a/evm/rust/abi/BlsApkBeefy.json b/evm/rust/abi/BlsApkBeefy.json new file mode 100644 index 000000000..2402d9462 --- /dev/null +++ b/evm/rust/abi/BlsApkBeefy.json @@ -0,0 +1,451 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "apkProof", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "MMR_ROOT_PAYLOAD_ID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes2", + "internalType": "bytes2" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "_apk", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IApkProof" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "noOp", + "inputs": [ + { + "name": "s", + "type": "tuple", + "internalType": "struct BlsApkConsensusState", + "components": [ + { + "name": "latestHeight", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "beefyActivationBlock", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "currentAuthoritySet", + "type": "tuple", + "internalType": "struct ApkAuthoritySet", + "components": [ + { + "name": "id", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "len", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "apkCommitment", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "name": "nextAuthoritySet", + "type": "tuple", + "internalType": "struct ApkAuthoritySet", + "components": [ + { + "name": "id", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "len", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "apkCommitment", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } + ] + }, + { + "name": "p", + "type": "tuple", + "internalType": "struct BlsApkBeefyConsensusProof", + "components": [ + { + "name": "relay", + "type": "tuple", + "internalType": "struct BlsApkRelayChainProof", + "components": [ + { + "name": "commitment", + "type": "tuple", + "internalType": "struct Commitment", + "components": [ + { + "name": "payload", + "type": "tuple[]", + "internalType": "struct Payload[]", + "components": [ + { + "name": "id", + "type": "bytes2", + "internalType": "bytes2" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "blockNumber", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "validatorSetId", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "name": "bitlist", + "type": "uint256[5]", + "internalType": "uint256[5]" + }, + { + "name": "apk", + "type": "bytes32[3]", + "internalType": "bytes32[3]" + }, + { + "name": "apk2", + "type": "bytes32[6]", + "internalType": "bytes32[6]" + }, + { + "name": "apkProof", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "signature", + "type": "bytes32[3]", + "internalType": "bytes32[3]" + }, + { + "name": "latestMmrLeaf", + "type": "tuple", + "internalType": "struct BeefyMmrLeaf", + "components": [ + { + "name": "version", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "parentNumber", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "parentHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "nextAuthoritySet", + "type": "tuple", + "internalType": "struct AuthoritySetCommitment", + "components": [ + { + "name": "id", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "len", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "root", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "name": "extra", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "leafIndex", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "mmrProof", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ] + }, + { + "name": "parachain", + "type": "tuple", + "internalType": "struct ParachainProof", + "components": [ + { + "name": "parachains", + "type": "tuple[]", + "internalType": "struct Parachain[]", + "components": [ + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "id", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "header", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "proof", + "type": "bytes32[]", + "internalType": "bytes32[]" + }, + { + "name": "leafCount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ] + } + ], + "outputs": [], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "verify", + "inputs": [ + { + "name": "previousState", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "proof", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "", + "type": "tuple[]", + "internalType": "struct IntermediateState[]", + "components": [ + { + "name": "stateMachineId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "height", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "commitment", + "type": "tuple", + "internalType": "struct StateCommitment", + "components": [ + { + "name": "timestamp", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "overlayRoot", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "stateRoot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } + ] + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "error", + "name": "EmptyLeaves", + "inputs": [] + }, + { + "type": "error", + "name": "EmptyTree", + "inputs": [] + }, + { + "type": "error", + "name": "EmptyTree", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAggregateProof", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidMmrProof", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidParachainHeaderProof", + "inputs": [] + }, + { + "type": "error", + "name": "LeafIndexOutOfBounds", + "inputs": [] + }, + { + "type": "error", + "name": "MissingApkCommitment", + "inputs": [] + }, + { + "type": "error", + "name": "MmrRootHashMissing", + "inputs": [] + }, + { + "type": "error", + "name": "OutOfBoundsLeaves", + "inputs": [] + }, + { + "type": "error", + "name": "ProofExhausted", + "inputs": [] + }, + { + "type": "error", + "name": "SuperMajorityRequired", + "inputs": [] + }, + { + "type": "error", + "name": "TimestampNotFound", + "inputs": [] + }, + { + "type": "error", + "name": "UnconsumedProof", + "inputs": [] + }, + { + "type": "error", + "name": "UnknownAuthoritySet", + "inputs": [] + }, + { + "type": "error", + "name": "UnsortedLeaves", + "inputs": [] + }, + { + "type": "error", + "name": "UnsortedLeaves", + "inputs": [] + } +] \ No newline at end of file diff --git a/evm/rust/src/generated/bls_apk_beefy.rs b/evm/rust/src/generated/bls_apk_beefy.rs new file mode 100644 index 000000000..135a80e2b --- /dev/null +++ b/evm/rust/src/generated/bls_apk_beefy.rs @@ -0,0 +1,24 @@ +//! Aggregate public key BLS BEEFY contract bindings generated with alloy sol! macro. +//! +//! See [`crate::generated::ecdsa_beefy`] for why the two `sol!` invocations are picked between +//! with `#[cfg]` rather than `cfg_attr`. + +use alloy_sol_macro::sol; + +#[cfg(feature = "std")] +sol!( + #[allow(missing_docs)] + #[sol(rpc, ignore_unlinked)] + #[derive(Debug, PartialEq, Eq)] + BlsApkBeefy, + "abi/BlsApkBeefy.json" +); + +#[cfg(not(feature = "std"))] +sol!( + #[allow(missing_docs)] + #[sol(ignore_unlinked)] + #[derive(Debug, PartialEq, Eq)] + BlsApkBeefy, + "abi/BlsApkBeefy.json" +); diff --git a/evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex b/evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex new file mode 100644 index 000000000..f3f581bbb --- /dev/null +++ b/evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex @@ -0,0 +1 @@ +0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a600000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000478237b877864af98fe7a2d05f4cc0d78eb019d094da6d70c242f95609ee577c42ca62abc23bb3594dc8f5a3516e5c610cbe3f263a5dc33d9d8d0bc631567cf630c4df86e4ad78887f62f46e1e0daa52bd58eee1613cf211518e561b582b0990868ddaca3321f5ac0f7c57e4bf34b86726e874e5ae4476a08f4c74c21fcb4a9ee94ee8572a6fb82afba59f47745e9d51436fc8946294306c5448880df4c41f82a136985e8963070f035906bcc766f9e9125d3ea66cbc1b293ca2ef37a6c581812dd734fad4bd53feef8ac36b393bfd78ba789203a056f9cafcfc91e8e9afc5a64b724f54d18e983996397faa28bf63713d81c18fec214be9a4536400258d08c9f34acb58680d7b7150f23aadd0e10da0d1e7dfe476a2029a700545fe7a1001000000000000000000000000000000000000000000000000000000000000004a0076ce06f16ec8c703642f8fb98f0d40ba4cbd1b126c773c355eba509bbe1903131ace18227659d204385349ef2ba9b6a1786183ea05bfef0d2404415638232898fda49aca75d4dd1df841b49f5106287246f2f8e77fb6dbdbf91565d28987069000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a82c2163d0b9bfb3902ccf0f96cd24bd11cb106816235dfb84f5b1a127a14c324b00000000000000000000000000000000000000000000000000000000000000330000000000000000000000000000000000000000000000000000000000000002d1cbb453472425036cef3caa539a0e81c6bd9fd9c3f24de7b38867380a19cafba39de86448087761c4970ffadbbba8ee46dbce931cfe74d316623642aba8abf400000000000000000000000000000000000000000000000000000000000003a80000000000000000000000000000000000000000000000000000000000000960000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003a90000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d6800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020336afc2238d61c3f4bd6a64c73d654b3e05c4bfac8df0b61e23268adbb76b67900000000000000000000000000000000000000000000000000000000000004a017ff42ed7d1bb31837a31633ecf1d0b678c2e595ef503df68adefcea79b93d18925c91639a428c9112be06c1fdd2cc770ffed9cfa07f38361b38db2d7865213e1a93a734ed7de2e167044cca861b2c7376ed419e669a7e7ea719434151d5ce1415308f51cdca7b40566fea1343421fe98bcc49ca3e3a6248cb8c9fef4dc096f05e2f37d63fda9122b433ca457503ad0b18a472c676d71fe47bfe0159442ed230672e577c7386a3e6915fc72e8219fa34e58da1a580c010611055f7992b09b7a11701ab3f02f965bd4b33f821bcc1d0ba65fe5a3080ba91ded133e60bc16590f62adaae55517d16311cb05cd52550a3d501a754aea61366ac04d32867d9ba72a882d35a433d665042c62689443f5c3c110f0ff1676763e87ad8613a8cc0be199d082190a576457f4d4a40a7bdbc55d5d88e754ed0124d0cc285cf9ba45660c24be3edf736ce717ea048a205e66c09258f16ce691eb1f70efc86b607d4b05f73ff2d64eb45f754dff9e2029f263ad5581e8b33d0df7bd41a65561155189350c9b40a28938e3f62ffb43d897a94e8bebd91cdbc45fb0f640a98ec9015d4400c8a87b11192090b4726bd6013f2a98b939b0f1907f9beeb1ffb59d3f59485af511eba9d1556522d34af123f9dbf313d83afdf256956e6a2382d7229548e10987e3a370160bf63423912c051cca014e9a327f7ae9e403819a4b4a0418e1b532cf14ded81fc6693df0c0bda2f370c871a9b654e0f7f8abbf94684f0dfee422ad6661dd3242e09ea919528e18d3a18251a238fab287c6a13d013f97af4369fc15833dd4e6dd27fb26bbd96d91263d59fc64ddef222631731394c9d2178f56ad871f696782a2bffb8d3a6b6bd940284338bec13bd6019d0e334136c3fef8b603f7798b8f335a535b73ee19d3a132eb2c5c96ec820e380a5c5f2a66a12fdf005843d275c5373c6dfa5829c57d294f66eac52117cd509db78760642751a5e40f09fe79e343a164e6f31ba4b59392d41295cb74fa4353475d212be2142c7ebb38a69e750180e1951a98e21e5c6b7a400394f58146c9fceb1a39714657f96d84ac89eda57ca44b179cc6b8d5fce98a3f8780aa67f2148128c07b5d082d2cb8805ca9614bfe9bfc0ed95273e0c0bdbe35cc90647b481ec8663e6075a7d3fb5114fb43b6f1a3afe70c7dfb930a4221f1928c98ff85a615b185b6ff8d9e76ab1b6118aa962dcc536008ab3f47096bbe7884cf5a8bfde55e4775aeba68cd5dc3db886147cd909f18577058f4f0307015b02307d7d3061d5d017ff0f20c5c075a8c109fa82a08fdefe4a2a4234c8b997c444a1416af3b20284ba3da1e27c25b8363d01b891512b11350ec229c5aff71c0ad72c072c22ad7857dcbb2cc9677d780fccf1c2e068f4cd8c790cbdac26d69120b112a5f9501f8c49189074d88fb98be727bd2b9f46720c0b473a4e8270b9fb62d76b21f839e88d4c539b1586d77c0770a287b7413b46b06c574a090dcd98f5527e9047f9ca66990f43c6558dfd4ebd3fb5a38265419f8e8d054114f3ff529dc8d509b3e795980a48c26ddd2cdf7b9649f249f770152b978311d4c2629fdd8613e9cf8defb50fd80c180d30a8690f277eabd0e002a6bedde0395f188e5416a5a7a6cb89ab5b67fda36abc51a1fe4cb594c4070542e569dc4e000000000000000000000000000000000000000000000000000000000000000571b72c82f673e768cf1cf43375c6b23b690a5254027366eb8b967dd25cb5a3d6a884b6a78a68c9a081ed6cf6f8c5cde2ce7124594514bebd4c23dd4d162d120c177dc388884191d00e7a92441a1f4049b1df22088e41234dc92c55c74b0fe65545d603612b70e2a308c661e845768e6d1a16abe028d83605454da932e1f65543cdf4544c0087c0bf78cdd85bf57f288484f3d9e67ff592c889aaa631357371390000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa9000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001358c3f177f92982c68fa65022801eea1cf50f545c9c3a29b9455d02eb911cc473a95062970f1431d09737f51b51362a1fa1f45b519c3332879d69e13dd0b7e4b27bddd759d30c71c0768846409fd48abc0bca8ebdeed3162701b99d394847cdc4b87ea14066175726120d989be11000000000452505352883efe9561cb71b9a2bddbcd15366e4aff55699386be14d934c20dc2854767d364850e0449534d5001010000000000000000000000000000000000000000000000000000000000000000bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a044953544d20163b776a0000000005617572610101260a820416df3d93202d7bbf7cd28719f35b8557cec6c453e1606007adf5822c94949cf67811b133bf40f09f7133292dbd25303ab4fdf5e74325f9a9a66b8c8f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex b/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex new file mode 100644 index 000000000..cf691370b --- /dev/null +++ b/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex @@ -0,0 +1 @@ +0x00000000000000000000000000000000000000000000000000000000000003a40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000026d4f3896e0cfadcff125976f8af0cb60caf60992ddef1a384e1c76dab0150cab \ No newline at end of file From d690d2a6c3bd2e6d7b9abac2a9cc2d88c87a3410 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Wed, 12 Aug 2026 14:35:19 +0100 Subject: [PATCH 19/48] charge the aggregate public key verification separately from the rest of a proof submission --- .../pallets/beefy-consensus-proofs/Cargo.toml | 2 + .../src/benchmarking.rs | 58 +++++++++++++++++++ .../pallets/beefy-consensus-proofs/src/lib.rs | 15 ++++- .../beefy-consensus-proofs/src/weights.rs | 5 ++ .../weights/pallet_beefy_consensus_proofs.rs | 11 ++++ .../weights/pallet_beefy_consensus_proofs.rs | 11 ++++ 6 files changed, 101 insertions(+), 1 deletion(-) diff --git a/modules/pallets/beefy-consensus-proofs/Cargo.toml b/modules/pallets/beefy-consensus-proofs/Cargo.toml index 446481c7d..0e63b804a 100644 --- a/modules/pallets/beefy-consensus-proofs/Cargo.toml +++ b/modules/pallets/beefy-consensus-proofs/Cargo.toml @@ -65,5 +65,7 @@ runtime-benchmarks = [ "polkadot-sdk/frame-benchmarking", "polkadot-sdk/runtime-benchmarks", "dep:hex-literal", + "beefy-verifier/apk", + "ismp-beefy/apk", ] try-runtime = ["polkadot-sdk/try-runtime"] diff --git a/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs b/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs index a729edf0e..de093950b 100644 --- a/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs +++ b/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs @@ -16,6 +16,7 @@ #![cfg(feature = "runtime-benchmarks")] use super::*; +use alloy_sol_types::SolType; use frame_benchmarking::v2::*; use frame_system::RawOrigin; use polkadot_sdk::*; @@ -46,6 +47,15 @@ const WIRE_PROOF: [u8; 1281] = hex_literal::hex!("010000000000000000000000000000 const FIXTURE_VKEY: H256 = H256(hex_literal::hex!("007d1720c695842ed647a1a72e981751f9b5e26fc5ca038523b23430a1292f08")); +/// The fixtures are hex text, shared with the solidity tests, so they are decoded here rather +/// than duplicated as byte arrays. +fn decode_hex(raw: &str) -> alloc::vec::Vec { + let trimmed = raw.trim().trim_start_matches("0x"); + (0..trimmed.len() / 2) + .map(|i| u8::from_str_radix(&trimmed[i * 2..i * 2 + 2], 16).expect("fixture is hex")) + .collect() +} + #[benchmarks( where T::AccountId: From<[u8; 32]>, @@ -106,6 +116,54 @@ mod benchmarks { assert_eq!(pallet::AcceptedProvers::::get(0u64).len(), 1); } + /// The cost an apk proof adds on top of what `submit_proof` already measures: decoding the + /// abi payload, the PLONK verification, the pairing that checks the aggregate signature and + /// binds `apk2`, the mmr leaf and the parachain header proof. + /// + /// Measured on the verifier rather than through the extrinsic, because the fixture's + /// parachain header carries no ismp overlay root and the pallet requires one to settle a + /// proof. The cryptography is identical either way, and the storage the pallet writes around + /// it is what the `submit_proof` benchmark already covers. + /// + /// Everything here is the fixture `BlsApkBeefy.sol` verifies and `apk_fixture.rs` runs + /// through the runtime verifier, so this is a real proof from a live relay. + #[benchmark] + fn verify_apk() { + let state_bytes = decode_hex(include_str!( + "../../../../evm/tests/foundry/fixtures/bls-apk-beefy-state.hex" + )); + let proof_bytes = decode_hex(include_str!( + "../../../../evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex" + )); + let verifying_key = + include_bytes!("../../../../evm/tests/foundry/fixtures/apk-verifying-key.bin").to_vec(); + + let state: beefy_verifier_primitives::ApkConsensusState = + ::abi_decode( + &state_bytes, + ) + .expect("apk state fixture decodes") + .try_into() + .expect("apk state fixture converts"); + + #[block] + { + let proof = ::abi_decode_params( + &proof_bytes, + ) + .expect("apk proof fixture decodes"); + let message: beefy_verifier_primitives::ApkConsensusMessage = + proof.try_into().expect("apk proof fixture converts"); + + beefy_verifier::apk::verify_apk_consensus::( + state.clone(), + message, + &verifying_key, + ) + .expect("the fixture proof verifies"); + } + } + #[benchmark] fn set_proof_reward() { let reward: <::Currency as frame_support::traits::fungible::Inspect< diff --git a/modules/pallets/beefy-consensus-proofs/src/lib.rs b/modules/pallets/beefy-consensus-proofs/src/lib.rs index 3b9d65ded..350cb38ee 100644 --- a/modules/pallets/beefy-consensus-proofs/src/lib.rs +++ b/modules/pallets/beefy-consensus-proofs/src/lib.rs @@ -374,7 +374,7 @@ pub mod pallet { /// (first or uncle) refund their transaction fee via `Pays::No`; failed proofs /// pay the fee, which is the spam deterrent. #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::submit_proof())] + #[pallet::weight(Pallet::::submit_proof_weight(proof))] pub fn submit_proof( origin: OriginFor, proof: BoundedVec, @@ -509,6 +509,19 @@ pub mod pallet { Ok(()) } + /// Weight of a `submit_proof` call, which depends on what is being verified. + /// + /// The benchmark covers the storage and the sp1 verification. An apk proof runs a PLONK + /// verification and a pairing instead, which is measured separately and added here. + /// Charging the same for both would let a block of apk proofs overrun its budget. + pub fn submit_proof_weight(proof: &BoundedVec) -> Weight { + let base = T::WeightInfo::submit_proof(); + match proof.first() { + Some(&types::PROOF_TYPE_APK) => base.saturating_add(T::WeightInfo::verify_apk()), + _ => base, + } + } + /// Authority set ids out of a stored consensus state. /// /// The shape follows the proof type: an apk state identifies a set by a commitment to its diff --git a/modules/pallets/beefy-consensus-proofs/src/weights.rs b/modules/pallets/beefy-consensus-proofs/src/weights.rs index 4636626dd..1a1b492f1 100644 --- a/modules/pallets/beefy-consensus-proofs/src/weights.rs +++ b/modules/pallets/beefy-consensus-proofs/src/weights.rs @@ -36,6 +36,8 @@ pub trait WeightInfo { fn set_reward_curve() -> Weight; /// Weight of `set_apk_verifying_key`. fn set_apk_verifying_key() -> Weight; + /// Cost of verifying an apk proof, charged on top of `submit_proof`. + fn verify_apk() -> Weight; } /// No-op [`WeightInfo`] for tests and genesis bootstrap. @@ -58,4 +60,7 @@ impl WeightInfo for () { fn set_apk_verifying_key() -> Weight { Weight::zero() } + fn verify_apk() -> Weight { + Weight::zero() + } } diff --git a/parachain/runtimes/gargantua/src/weights/pallet_beefy_consensus_proofs.rs b/parachain/runtimes/gargantua/src/weights/pallet_beefy_consensus_proofs.rs index f48b55111..6e150adce 100644 --- a/parachain/runtimes/gargantua/src/weights/pallet_beefy_consensus_proofs.rs +++ b/parachain/runtimes/gargantua/src/weights/pallet_beefy_consensus_proofs.rs @@ -108,6 +108,17 @@ impl pallet_beefy_consensus_proofs::WeightInfo for Weig .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: `BeefyConsensusProofs::ApkVerifyingKey` (r:1 w:0) + /// Proof: `BeefyConsensusProofs::ApkVerifyingKey` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn verify_apk() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 41_210_000_000 picoseconds. + Weight::from_parts(41_210_000_000, 0) + .saturating_add(Weight::from_parts(0, 51_200)) + .saturating_add(T::DbWeight::get().reads(1)) + } /// Storage: `BeefyConsensusProofs::ApkVerifyingKey` (r:0 w:1) /// Proof: `BeefyConsensusProofs::ApkVerifyingKey` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn set_apk_verifying_key() -> Weight { diff --git a/parachain/runtimes/nexus/src/weights/pallet_beefy_consensus_proofs.rs b/parachain/runtimes/nexus/src/weights/pallet_beefy_consensus_proofs.rs index f48b55111..6e150adce 100644 --- a/parachain/runtimes/nexus/src/weights/pallet_beefy_consensus_proofs.rs +++ b/parachain/runtimes/nexus/src/weights/pallet_beefy_consensus_proofs.rs @@ -108,6 +108,17 @@ impl pallet_beefy_consensus_proofs::WeightInfo for Weig .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: `BeefyConsensusProofs::ApkVerifyingKey` (r:1 w:0) + /// Proof: `BeefyConsensusProofs::ApkVerifyingKey` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn verify_apk() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 41_210_000_000 picoseconds. + Weight::from_parts(41_210_000_000, 0) + .saturating_add(Weight::from_parts(0, 51_200)) + .saturating_add(T::DbWeight::get().reads(1)) + } /// Storage: `BeefyConsensusProofs::ApkVerifyingKey` (r:0 w:1) /// Proof: `BeefyConsensusProofs::ApkVerifyingKey` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn set_apk_verifying_key() -> Weight { From d2971def824c25cde4fdbc6d785b6d4992e05235 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Thu, 13 Aug 2026 21:26:31 +0100 Subject: [PATCH 20/48] run the apk prover as a separate process, since the circuit setup wedges inside the relayer --- Cargo.lock | 30 +-- tesseract/consensus/beefy/Cargo.toml | 1 - tesseract/consensus/beefy/apk/Cargo.toml | 15 +- tesseract/consensus/beefy/apk/src/command.rs | 123 +++++++++ tesseract/consensus/beefy/apk/src/lib.rs | 26 +- .../beefy/apk/tests/command_prover.rs | 104 +++++++ .../consensus/beefy/apk/tests/live_prover.rs | 254 ++++++++++++++++++ tesseract/consensus/beefy/src/prover.rs | 30 ++- tesseract/prover/Cargo.toml | 2 +- 9 files changed, 548 insertions(+), 37 deletions(-) create mode 100644 tesseract/consensus/beefy/apk/src/command.rs create mode 100644 tesseract/consensus/beefy/apk/tests/command_prover.rs create mode 100644 tesseract/consensus/beefy/apk/tests/live_prover.rs diff --git a/Cargo.lock b/Cargo.lock index 234ee1724..4ed449124 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,6 +956,7 @@ name = "apk-beefy" version = "0.1.0" dependencies = [ "alloy-primitives 1.5.7", + "alloy-sol-types 1.5.7", "anyhow", "apk-commitment", "ark-bls12-381 0.4.0", @@ -965,12 +966,19 @@ dependencies = [ "ark-serialize 0.5.0", "async-trait", "beefy-prover", + "beefy-verifier", "beefy-verifier-primitives", "gnark-apk-prover", "hex", + "ismp", "ismp-abi", + "parity-scale-codec", + "polkadot-sdk", + "primitive-types 0.13.1", + "serde_json", "sp-consensus-beefy", "subxt 0.42.1", + "subxt-utils", "tokio", ] @@ -2980,7 +2988,7 @@ dependencies = [ "beefy-verifier-primitives", "ckb-merkle-mountain-range", "futures", - "gnark-plonk-verifier 0.1.0 (git+https://github.com/polytope-labs/gnark-apk-proofs?rev=48e6aa504994a58fb41465e22eb90b1df6a190f8)", + "gnark-plonk-verifier", "hex", "hex-literal 0.4.1", "ismp", @@ -8670,18 +8678,18 @@ dependencies = [ [[package]] name = "gnark-apk-ffi" version = "0.1.0" -source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4#ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=48e6aa504994a58fb41465e22eb90b1df6a190f8#48e6aa504994a58fb41465e22eb90b1df6a190f8" [[package]] name = "gnark-apk-prover" version = "0.1.0" -source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4#ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=48e6aa504994a58fb41465e22eb90b1df6a190f8#48e6aa504994a58fb41465e22eb90b1df6a190f8" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", "ark-ff 0.5.0", "gnark-apk-ffi", - "gnark-plonk-verifier 0.1.0 (git+https://github.com/polytope-labs/gnark-apk-proofs?rev=ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4)", + "gnark-plonk-verifier", "thiserror 2.0.18", ] @@ -8700,20 +8708,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "gnark-plonk-verifier" -version = "0.1.0" -source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4#ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4" -dependencies = [ - "ark-bls12-381 0.5.0", - "ark-ec 0.5.0", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "sha2 0.10.9", - "sha3 0.10.8", - "thiserror 2.0.18", -] - [[package]] name = "governor" version = "0.6.3" diff --git a/tesseract/consensus/beefy/Cargo.toml b/tesseract/consensus/beefy/Cargo.toml index c43c87407..11957edc3 100644 --- a/tesseract/consensus/beefy/Cargo.toml +++ b/tesseract/consensus/beefy/Cargo.toml @@ -56,7 +56,6 @@ workspace = true features = ["sp-runtime"] [features] -apk-local = ["apk-beefy/local"] # a feature that tells the tests to write a new consensus state new-consensus-state = [] diff --git a/tesseract/consensus/beefy/apk/Cargo.toml b/tesseract/consensus/beefy/apk/Cargo.toml index 2389bce2a..07d345562 100644 --- a/tesseract/consensus/beefy/apk/Cargo.toml +++ b/tesseract/consensus/beefy/apk/Cargo.toml @@ -9,6 +9,7 @@ description = "Builds BEEFY consensus proofs carrying an aggregate public key pr anyhow = "1.0.79" async-trait = { workspace = true } hex = { workspace = true } +codec = { package = "parity-scale-codec", version = "3.2.2" } alloy-primitives = { workspace = true, default-features = true } subxt = { workspace = true, default-features = true } sp-consensus-beefy = { workspace = true } @@ -22,10 +23,11 @@ ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = fa ark-ec = { version = "0.4.0", default-features = false } ark-ff = { version = "0.4.0", default-features = false } ark-serialize = { version = "0.4.0", default-features = false } +json = { workspace = true, default-features = true } [dependencies.gnark-apk-prover] git = "https://github.com/polytope-labs/gnark-apk-proofs" -rev = "ee8c879fac84ba737a9b0bfbfead8fbb2d0228a4" +rev = "48e6aa504994a58fb41465e22eb90b1df6a190f8" optional = true [dependencies.ark-serialize-05] @@ -35,8 +37,17 @@ optional = true [dependencies.tokio] workspace = true -features = ["rt"] +features = ["fs", "process", "rt"] [features] default = [] local = ["dep:gnark-apk-prover", "dep:ark-serialize-05"] + +[dev-dependencies] +alloy-sol-types = { workspace = true, default-features = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +subxt-utils = { workspace = true, default-features = true } +beefy-verifier = { workspace = true, default-features = true, features = ["apk"] } +polkadot-sdk = { workspace = true, default-features = true, features = ["sp-io"] } +ismp = { workspace = true, default-features = true } +primitive-types = { workspace = true, default-features = true } diff --git a/tesseract/consensus/beefy/apk/src/command.rs b/tesseract/consensus/beefy/apk/src/command.rs new file mode 100644 index 000000000..0374e6ad8 --- /dev/null +++ b/tesseract/consensus/beefy/apk/src/command.rs @@ -0,0 +1,123 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Proving by running `gnark-apk-proofs` as a separate process. +//! +//! Linking the prover into this binary is the obvious thing and it does not work: the circuit +//! setup wedges inside the relayer's process while the identical call completes in a plain binary, +//! in a tokio runtime, and in a tokio runtime with forty live threads. Rather than keep hunting, +//! the prover runs where it is known to work and the two sides meet over json. +//! +//! It also keeps cgo, a Go toolchain and an 800MB reference string out of this build entirely. + +use std::path::PathBuf; + +use alloy_primitives::U256; +use anyhow::{anyhow, Context}; +use ark_ec::AffineRepr; +use ark_ff::{BigInteger, PrimeField}; +use tokio::process::Command; + +use crate::{decompress_g1, ApkProof, ApkProofRequest, ApkProver, BITLIST_WORDS}; + +/// Runs the prover binary, handing it a directory to read inputs from and write the proof to. +/// +/// The binary is expected to read `apk-inputs.json` and write `apk-snark.json`, which is what +/// `gnark-apk-proofs`' `prove_from_json` does. +pub struct CommandProver { + /// The prover executable. + binary: PathBuf, + /// Where the two json files live. Each proof overwrites them, so this is not shared. + work_dir: PathBuf, +} + +impl CommandProver { + /// Build a prover that shells out to `binary`, exchanging files under `work_dir`. + pub fn new(binary: PathBuf, work_dir: PathBuf) -> Result { + std::fs::create_dir_all(&work_dir) + .with_context(|| format!("could not create the prover work directory {work_dir:?}"))?; + Ok(Self { binary, work_dir }) + } +} + +#[async_trait::async_trait] +impl ApkProver for CommandProver { + async fn prove(&self, request: ApkProofRequest) -> Result { + // The circuit takes uncompressed coordinates, so the compressed keys are opened up here + // rather than teaching the prover binary about our encoding. + let keys = request + .keys + .iter() + .map(|key| { + let point = decompress_g1(key)?; + let (x, y) = point.xy().ok_or_else(|| anyhow!("authority key is the identity"))?; + let mut packed = Vec::with_capacity(96); + packed.extend_from_slice(&x.into_bigint().to_bytes_be()); + packed.extend_from_slice(&y.into_bigint().to_bytes_be()); + Ok(hex::encode(packed)) + }) + .collect::, anyhow::Error>>()?; + + let inputs = json::json!({ + "keys": keys, + "participation": request.participation, + }); + let input_path = self.work_dir.join("apk-inputs.json"); + let output_path = self.work_dir.join("apk-snark.json"); + tokio::fs::write(&input_path, json::to_vec(&inputs)?) + .await + .with_context(|| format!("could not write {input_path:?}"))?; + // Left over output would be read back as this proof's if the prover failed silently. + let _ = tokio::fs::remove_file(&output_path).await; + + let status = Command::new(&self.binary) + .arg(&self.work_dir) + .status() + .await + .with_context(|| format!("could not run the prover at {:?}", self.binary))?; + if !status.success() { + Err(anyhow!("the prover exited with {status}"))? + } + + let output: json::Value = json::from_slice( + &tokio::fs::read(&output_path) + .await + .with_context(|| format!("the prover wrote no proof to {output_path:?}"))?, + )?; + + let hex_field = |name: &str| -> Result, anyhow::Error> { + let raw = output[name].as_str().ok_or_else(|| anyhow!("proof has no {name}"))?; + Ok(hex::decode(raw.trim_start_matches("0x"))?) + }; + + let bitlist: [U256; BITLIST_WORDS] = output["bitlist"] + .as_array() + .ok_or_else(|| anyhow!("proof has no bitlist"))? + .iter() + .map(|word| { + let raw = word.as_str().ok_or_else(|| anyhow!("bitlist word is not a string"))?; + Ok(U256::from_be_slice(&hex::decode(raw.trim_start_matches("0x"))?)) + }) + .collect::, anyhow::Error>>()? + .try_into() + .map_err(|_| anyhow!("bitlist is not {BITLIST_WORDS} words"))?; + + let commitment = hex_field("apkCommitment")?; + let apk_commitment = <[u8; 32]>::try_from(commitment.as_slice()) + .map_err(|_| anyhow!("apk commitment is not 32 bytes"))?; + + Ok(ApkProof { proof: hex_field("apkProof")?, bitlist, apk_commitment }) + } +} diff --git a/tesseract/consensus/beefy/apk/src/lib.rs b/tesseract/consensus/beefy/apk/src/lib.rs index edfc0c4cd..7517037a8 100644 --- a/tesseract/consensus/beefy/apk/src/lib.rs +++ b/tesseract/consensus/beefy/apk/src/lib.rs @@ -29,6 +29,7 @@ use ark_bls12_381::{Fq, G1Affine, G1Projective, G2Affine, G2Projective}; use ark_ec::{AffineRepr, CurveGroup}; use ark_ff::{BigInteger, PrimeField}; use ark_serialize::CanonicalDeserialize; +use codec::Decode; use std::sync::Arc; use subxt::config::HashFor; @@ -39,6 +40,9 @@ use beefy_prover::bls::{ use beefy_verifier_primitives::{ConsensusState, BLS_G1_SIGNATURE_LEN}; use ismp_abi::bls_apk_beefy::BlsApkBeefy; +mod command; +pub use command::CommandProver; + #[cfg(feature = "local")] mod local; #[cfg(feature = "local")] @@ -230,6 +234,26 @@ where apk_commitment_of(&keys) } + /// Poseidon2 over the relay's *next* BEEFY keys at `at`. + /// + /// Bootstrapping a client cold needs both, since the first proof it sees may already be the + /// rotation into the next set, and a set without a commitment is refused rather than checked + /// against zero. + pub async fn next_apk_commitment(&self, at: HashFor) -> Result<[u8; 32], anyhow::Error> { + let data = self + .inner + .relay_rpc + .state_get_storage(&beefy_verifier_primitives::RELAY_BEEFY_NEXT_AUTHORITIES, Some(at)) + .await? + .ok_or_else(|| anyhow!("No next beefy authorities found"))?; + + let keys = Vec::::decode(&mut data.as_ref())? + .iter() + .map(beefy_verifier_primitives::PairedAuthority::g1) + .collect::>(); + apk_commitment_of(&keys) + } + /// Sum the signers' keys in both groups and their signatures, and note who they were. /// /// The G1 halves are what the circuit binds to and the G2 halves are what BEEFY's signature @@ -296,7 +320,7 @@ pub(crate) fn apk_commitment_of( Ok(apk_commitment::public_keys_commitment_bytes(&padded)) } -fn decompress_g1(key: &[u8; BLS_G1_SIGNATURE_LEN]) -> Result { +pub(crate) fn decompress_g1(key: &[u8; BLS_G1_SIGNATURE_LEN]) -> Result { G1Affine::deserialize_compressed(&key[..]).map_err(|_| anyhow!("Malformed G1 point")) } diff --git a/tesseract/consensus/beefy/apk/tests/command_prover.rs b/tesseract/consensus/beefy/apk/tests/command_prover.rs new file mode 100644 index 000000000..d9925f2e0 --- /dev/null +++ b/tesseract/consensus/beefy/apk/tests/command_prover.rs @@ -0,0 +1,104 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The file contract between this crate and the prover binary. +//! +//! A stub stands in for `gnark-apk-proofs` so these run in a second rather than minutes. What is +//! under test is the handover: what we write, what we read back, and what happens when the prover +//! misbehaves. + +use std::{os::unix::fs::PermissionsExt, path::PathBuf}; + +use apk_beefy::{ApkProofRequest, ApkProver, CommandProver}; +use ark_ec::AffineRepr; +use ark_serialize::CanonicalSerialize; + +/// A key the prover can decompress, which is all this test needs of it. +fn a_key() -> [u8; 48] { + let mut compressed = Vec::new(); + ark_bls12_381::G1Affine::generator() + .serialize_compressed(&mut compressed) + .unwrap(); + compressed.try_into().unwrap() +} + +fn request() -> ApkProofRequest { + ApkProofRequest { keys: vec![a_key(), a_key()], participation: vec![0, 1] } +} + +/// Writes a stub prover into `dir` and returns its path. The body is shell. +fn stub_prover(dir: &PathBuf, body: &str) -> PathBuf { + let path = dir.join("stub-prover"); + std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap(); + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap(); + path +} + +fn work_dir(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("apk-command-prover-{name}")); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir +} + +#[tokio::test] +async fn reads_back_what_the_prover_wrote() { + let dir = work_dir("happy"); + // Echoes a proof shaped like the real one, and proves the inputs arrived by counting the keys. + let prover = stub_prover( + &dir, + r#"test -f "$1/apk-inputs.json" || exit 3 +cat > "$1/apk-snark.json" <<'JSON' +{ + "apkProof": "aabbcc", + "bitlist": ["03", "00", "00", "00", "00"], + "apkCommitment": "1111111111111111111111111111111111111111111111111111111111111111" +} +JSON"#, + ); + + let proof = CommandProver::new(prover, dir.clone()).unwrap().prove(request()).await.unwrap(); + + assert_eq!(proof.proof, vec![0xaa, 0xbb, 0xcc]); + assert_eq!(proof.bitlist[0], alloy_primitives::U256::from(3)); + assert_eq!(proof.apk_commitment, [0x11u8; 32]); + + // The keys reached the prover as uncompressed 96 byte points, which is what it expects. + let written: json::Value = + json::from_slice(&std::fs::read(dir.join("apk-inputs.json")).unwrap()).unwrap(); + assert_eq!(written["keys"].as_array().unwrap().len(), 2); + assert_eq!(written["keys"][0].as_str().unwrap().len(), 192); + assert_eq!(written["participation"].as_array().unwrap().len(), 2); +} + +#[tokio::test] +async fn a_failing_prover_is_an_error() { + let dir = work_dir("failing"); + let prover = stub_prover(&dir, "exit 1"); + + let error = CommandProver::new(prover, dir).unwrap().prove(request()).await.unwrap_err(); + assert!(format!("{error}").contains("exited"), "unexpected error: {error}"); +} + +/// The dangerous case: a prover that fails without writing, leaving the previous proof behind. +#[tokio::test] +async fn stale_output_is_never_read_back() { + let dir = work_dir("stale"); + std::fs::write(dir.join("apk-snark.json"), r#"{"apkProof":"dead"}"#).unwrap(); + let prover = stub_prover(&dir, "exit 0"); + + let error = CommandProver::new(prover, dir).unwrap().prove(request()).await.unwrap_err(); + assert!(format!("{error}").contains("no proof"), "unexpected error: {error}"); +} diff --git a/tesseract/consensus/beefy/apk/tests/live_prover.rs b/tesseract/consensus/beefy/apk/tests/live_prover.rs new file mode 100644 index 000000000..bc00c777e --- /dev/null +++ b/tesseract/consensus/beefy/apk/tests/live_prover.rs @@ -0,0 +1,254 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Assembles a proof from a live chain and checks the verifier accepts it. +//! +//! The fixture tests prove the verifier agrees with Solidity on a proof somebody else built. This +//! closes the other half, that what this crate assembles is something the verifier accepts, which +//! is the path a relayer actually takes. +//! +//! Needs a relay whose BEEFY authorities hold paired `ecdsa_bls_crypto` keys, the parachain it +//! finalizes, and the prover binary. Takes minutes, since it generates a real SNARK. +//! +//! RELAY_WS_URL=ws://127.0.0.1:9979 PARA_WS_URL=ws://127.0.0.1:9981 PARA_ID=4009 \ +//! APK_PROVER_BINARY=~/Documents/polytope/gnark-apk-proofs/target/release/examples/ +//! prove_from_json \ cargo test -p apk-beefy --test live_prover -- --ignored --nocapture + +use std::sync::Arc; + +use apk_beefy::{CommandProver, Prover}; +use beefy_prover::relay::fetch_latest_beefy_justification; +use beefy_verifier_primitives::{ApkAuthoritySet, ApkConsensusMessage, ApkConsensusState}; +use polkadot_sdk::*; +use primitive_types::H256; +use subxt::{backend::legacy::LegacyRpcMethods, config::Header as _, PolkadotConfig}; + +const VERIFYING_KEY: &[u8] = + include_bytes!("../../../../../evm/tests/foundry/fixtures/apk-verifying-key.bin"); + +struct TestHost; + +impl ismp::messaging::Keccak256 for TestHost { + fn keccak256(bytes: &[u8]) -> H256 { + sp_io::hashing::keccak_256(bytes).into() + } +} + +#[tokio::test(flavor = "multi_thread")] +#[ignore = "needs a live BLS relay, its parachain, and the prover binary"] +async fn assembles_a_proof_the_verifier_accepts() { + let max_rpc_payload_size = 15 * 1024 * 1024; + let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); + let para_ws_url = std::env::var("PARA_WS_URL").expect("PARA_WS_URL must be set"); + let para_id: u32 = std::env::var("PARA_ID") + .expect("PARA_ID must be set") + .parse() + .expect("para id is a number"); + let prover_binary = std::env::var("APK_PROVER_BINARY").expect("APK_PROVER_BINARY must be set"); + + let (relay_client, relay_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); + let (para_client, para_rpc_client) = + subxt_utils::client::ws_client::(¶_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let para_rpc = LegacyRpcMethods::::new(para_rpc_client.clone()); + + let inner = beefy_prover::Prover { + beefy_activation_block: 0, + relay: relay_client, + relay_rpc: relay_rpc.clone(), + relay_rpc_client: relay_rpc_client.clone(), + para: para_client, + para_rpc, + para_rpc_client, + para_ids: vec![para_id], + query_batch_size: Some(100), + }; + + let work_dir = std::env::temp_dir().join("apk-live-prover"); + let prover = Prover::new( + inner, + Arc::new(CommandProver::new(prover_binary.into(), work_dir).expect("prover")), + ); + + let latest: H256 = relay_rpc_client + .request("beefy_getFinalizedHead", subxt::ext::subxt_rpcs::rpc_params!()) + .await + .unwrap(); + + // Anchor the trusted state at the previous beefy justified block, so the update is not stale. + let mut previous = H256::default(); + let mut cursor = latest; + for _ in 0..2000 { + let header = relay_rpc.chain_get_header(Some(cursor.into())).await.unwrap().unwrap(); + let parent: H256 = header.parent_hash.into(); + if parent.is_zero() { + panic!("reached genesis without a previous beefy block"); + } + let block = relay_rpc.chain_get_block(Some(parent.into())).await.unwrap().unwrap(); + if block + .justifications + .map(|justifications| { + justifications.iter().any(|j| j.0 == sp_consensus_beefy::BEEFY_ENGINE_ID) + }) + .unwrap_or(false) + { + previous = parent; + break; + } + cursor = parent; + } + assert!(!previous.is_zero(), "no previous beefy block found"); + + let trusted = prover.inner.get_initial_consensus_state(Some(previous)).await.unwrap(); + let (signed_commitment, _) = + fetch_latest_beefy_justification(&prover.inner.relay_rpc, latest).await.unwrap(); + + let signed_set_id = signed_commitment.commitment.validator_set_id; + println!( + "proving beefy block {} for set {signed_set_id} against trusted height {}, this takes minutes", + signed_commitment.commitment.block_number, trusted.latest_beefy_height + ); + let proof = prover + .consensus_proof(signed_commitment, trusted.clone()) + .await + .expect("the prover assembles a proof"); + + // The signing set's commitment has to be seeded, exactly as `initialize_apk_state` does when + // bootstrapping a client. Every later one arrives in a header digest, and a set that has none + // is refused rather than checked against zero, so it has to go on whichever set signed. + let commitment = H256(prover.current_apk_commitment(latest).await.unwrap()); + let signing_set = signed_set_id; + let authority_set = |set: sp_consensus_beefy::mmr::BeefyAuthoritySet| { + let apk_commitment = if set.id == signing_set { commitment } else { H256::zero() }; + ApkAuthoritySet { id: set.id, len: set.len, apk_commitment } + }; + let state = ApkConsensusState { + latest_beefy_height: trusted.latest_beefy_height, + beefy_activation_block: trusted.beefy_activation_block, + mmr_root_hash: trusted.mmr_root_hash, + current_authorities: authority_set(trusted.current_authorities), + next_authorities: authority_set(trusted.next_authorities), + }; + assert!( + state.current_authorities.apk_commitment != H256::zero() || + state.next_authorities.apk_commitment != H256::zero(), + "neither trusted set is the one that signed, so the client would have no commitment", + ); + + let message: ApkConsensusMessage = proof.try_into().expect("proof converts to scale"); + let (new_state, headers) = beefy_verifier::apk::verify_apk_consensus::( + state.clone(), + message, + VERIFYING_KEY, + ) + .expect("the verifier accepts what the prover built"); + + assert!(new_state.latest_beefy_height > state.latest_beefy_height); + assert_eq!(headers.len(), 1, "should finalize the parachain"); + assert_eq!(headers[0].para_id, para_id); + println!( + "prover to verifier round trip: beefy height {} -> {}, para {} finalized", + state.latest_beefy_height, new_state.latest_beefy_height, para_id + ); +} + +/// Writes the abi encoded state `initialize_apk_state` takes, for the set signing right now. +/// +/// This is what the relayer's `query_initial_consensus_state` produces for the apk variant, run on +/// its own so a chain can be bootstrapped by hand. +/// +/// RELAY_WS_URL=ws://127.0.0.1:9979 PARA_WS_URL=ws://127.0.0.1:9981 PARA_ID=4009 \ +/// APK_STATE_OUT=/tmp/apk-state.hex \ +/// cargo test -p apk-beefy --test live_prover -- --ignored --nocapture writes_initial_state +#[tokio::test(flavor = "multi_thread")] +#[ignore = "needs a live BLS relay"] +async fn writes_initial_state_for_bootstrapping() { + use alloy_sol_types::SolValue; + + let max_rpc_payload_size = 15 * 1024 * 1024; + let relay_ws_url = std::env::var("RELAY_WS_URL").expect("RELAY_WS_URL must be set"); + let para_ws_url = std::env::var("PARA_WS_URL").expect("PARA_WS_URL must be set"); + let para_id: u32 = std::env::var("PARA_ID") + .expect("PARA_ID must be set") + .parse() + .expect("para id is a number"); + let out = std::env::var("APK_STATE_OUT").expect("APK_STATE_OUT must be set"); + + let (relay_client, relay_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client.clone()); + let (para_client, para_rpc_client) = + subxt_utils::client::ws_client::(¶_ws_url, max_rpc_payload_size) + .await + .unwrap(); + let para_rpc = LegacyRpcMethods::::new(para_rpc_client.clone()); + + let inner = beefy_prover::Prover { + beefy_activation_block: 0, + relay: relay_client, + relay_rpc: relay_rpc.clone(), + relay_rpc_client: relay_rpc_client.clone(), + para: para_client, + para_rpc, + para_rpc_client, + para_ids: vec![para_id], + query_batch_size: Some(100), + }; + let prover = Prover::new( + inner, + Arc::new(CommandProver::new("/nonexistent".into(), std::env::temp_dir()).unwrap()), + ); + + let latest: H256 = relay_rpc_client + .request("beefy_getFinalizedHead", subxt::ext::subxt_rpcs::rpc_params!()) + .await + .unwrap(); + let trusted = prover.inner.get_initial_consensus_state(Some(latest)).await.unwrap(); + + // Both sets are seeded, not just the current one. A cold start can meet the rotation proof + // into the next set before it has ever seen a header digest, and a set with no commitment is + // refused rather than checked against zero. After this the digests take over. + let current = H256(prover.current_apk_commitment(latest).await.unwrap()); + let next = H256(prover.next_apk_commitment(latest).await.unwrap()); + let state = ApkConsensusState { + latest_beefy_height: trusted.latest_beefy_height, + beefy_activation_block: trusted.beefy_activation_block, + mmr_root_hash: trusted.mmr_root_hash, + current_authorities: ApkAuthoritySet { + id: trusted.current_authorities.id, + len: trusted.current_authorities.len, + apk_commitment: current, + }, + next_authorities: ApkAuthoritySet { + id: trusted.next_authorities.id, + len: trusted.next_authorities.len, + apk_commitment: next, + }, + }; + + let abi = ismp_abi::bls_apk_beefy::BlsApkBeefy::BlsApkConsensusState::from(state.clone()); + std::fs::write(&out, format!("0x{}", hex::encode(abi.abi_encode()))).expect("write"); + println!( + "wrote state for sets {} and {} at beefy height {} to {out}", + state.current_authorities.id, state.next_authorities.id, state.latest_beefy_height + ); +} diff --git a/tesseract/consensus/beefy/src/prover.rs b/tesseract/consensus/beefy/src/prover.rs index 1c34ee45c..4fa247525 100644 --- a/tesseract/consensus/beefy/src/prover.rs +++ b/tesseract/consensus/beefy/src/prover.rs @@ -126,10 +126,14 @@ pub struct ProverConfig { pub max_rpc_payload_size: Option, /// Query batch size for mmr leaves pub query_batch_size: Option, - /// Where the apk circuit's structured reference string lives. Only read by the `Apk` variant, - /// and left unset it falls back to `$HOME/.config/gnark-apk-proofs/srs`. + /// The `gnark-apk-proofs` prover binary, run once per proof. Only read by the `Apk` variant, + /// which cannot link the prover directly, see `apk_beefy::command`. #[serde(default)] - pub apk_srs_dir: Option, + pub apk_prover_binary: Option, + /// Where the prover exchanges its input and proof files. Defaults to a directory beside the + /// binary when unset. + #[serde(default)] + pub apk_prover_dir: Option, } /// The BEEFY prover produces BEEFY consensus proofs using either the naive or zk variety. Consensus @@ -674,20 +678,18 @@ where Prover::Sp1(zk_beefy::Prover::new(prover, sp1_prover, account)) }, ProofVariant::Ecdsa => Prover::Ecdsa(prover, PhantomData), - // Setup compiles the circuit and generates the proving key, minutes of cpu, so it is - // done once here rather than per proof and kept off the runtime's worker threads. - #[cfg(feature = "apk-local")] ProofVariant::Apk => { - let srs_dir = config.apk_srs_dir.clone(); - let apk_prover = - tokio::task::spawn_blocking(move || apk_beefy::LocalProver::new(srs_dir)) - .await??; + let binary = config + .apk_prover_binary + .clone() + .ok_or_else(|| anyhow!("`apk_prover_binary` is required by the apk variant"))?; + let work_dir = config + .apk_prover_dir + .clone() + .unwrap_or_else(|| binary.with_file_name("apk-prover-work")); + let apk_prover = apk_beefy::CommandProver::new(binary, work_dir)?; Prover::Apk(apk_beefy::Prover::new(prover, Arc::new(apk_prover))) }, - #[cfg(not(feature = "apk-local"))] - ProofVariant::Apk => Err(anyhow!( - "This binary was built without apk proving, rebuild with the `apk-local` feature" - ))?, }; Ok(prover) diff --git a/tesseract/prover/Cargo.toml b/tesseract/prover/Cargo.toml index 275ff642e..1f0cb1e72 100644 --- a/tesseract/prover/Cargo.toml +++ b/tesseract/prover/Cargo.toml @@ -19,7 +19,7 @@ rustls = { version = "0.23.23", features = ["ring"] } primitive-types = { workspace = true } subxt-utils = { workspace = true } -tesseract-beefy = { workspace = true, features = ["apk-local"] } +tesseract-beefy = { workspace = true } tesseract-substrate = { workspace = true } tesseract-primitives = { workspace = true } From 31afef39d5675817b3f3720fd45c3141d7ba0892 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Thu, 13 Aug 2026 21:26:45 +0100 Subject: [PATCH 21/48] publish the apk commitment in every header of a set and under each new set id --- modules/consensus/beefy/primitives/src/lib.rs | 11 +++ modules/pallets/beefy-apk-digest/src/lib.rs | 93 ++++++++++++------- .../pallets/beefy-consensus-proofs/src/lib.rs | 15 ++- parachain/runtimes/gargantua/src/lib.rs | 2 +- 4 files changed, 82 insertions(+), 39 deletions(-) diff --git a/modules/consensus/beefy/primitives/src/lib.rs b/modules/consensus/beefy/primitives/src/lib.rs index eccc80bbe..865b70021 100644 --- a/modules/consensus/beefy/primitives/src/lib.rs +++ b/modules/consensus/beefy/primitives/src/lib.rs @@ -175,6 +175,17 @@ pub const APK_G2_LEN: usize = 192; /// The participation bitlist, one bit per validator slot. pub const APK_BITLIST_WORDS: usize = 5; +/// `Beefy::NextAuthorities` on a relay chain. +/// +/// The *next* set, not the current one. A client verifying under set N learns the commitment for +/// set N+1, which is what lets it verify the update after a rotation. Note this is not +/// `well_known_keys::NEXT_AUTHORITIES`, which is Babe's: both end in `twox_128("NextAuthorities")` +/// and only the pallet prefix differs. +pub const RELAY_BEEFY_NEXT_AUTHORITIES: [u8; 32] = [ + 0x08, 0xc4, 0x19, 0x74, 0xa9, 0x7d, 0xbf, 0x15, 0xcf, 0xbe, 0xc2, 0x83, 0x65, 0xbe, 0xa2, 0xda, + 0xaa, 0xcf, 0x00, 0xb9, 0xb4, 0x1f, 0xda, 0x7a, 0x92, 0x68, 0x82, 0x1c, 0x2a, 0x2b, 0x3e, 0x4c, +]; + /// Engine id of the digest item carrying an authority set's APK commitment. pub const APK_ENGINE_ID: [u8; 4] = *b"APKC"; diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index a6d858dfc..1e7b1ab0c 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -34,7 +34,9 @@ use alloc::vec::Vec; use apk_commitment::{PartialCommitment, NUM_VALIDATORS}; use ark_bls12_381::G1Affine; use ark_serialize::CanonicalDeserialize; -pub use beefy_verifier_primitives::{ApkCommitmentDigest, APK_ENGINE_ID}; +pub use beefy_verifier_primitives::{ + ApkCommitmentDigest, APK_ENGINE_ID, RELAY_BEEFY_NEXT_AUTHORITIES, +}; use beefy_verifier_primitives::{PairedAuthority, BLS_G1_SIGNATURE_LEN}; use codec::{Decode, Encode, MaxEncodedLen}; use cumulus_pallet_parachain_system::RelayChainStateProof; @@ -44,20 +46,6 @@ use scale_info::TypeInfo; pub use pallet::*; -/// `Beefy::NextAuthorities` on the relay chain. -/// -/// The *next* set, not the current one, and the distinction is what makes the scheme work. A client -/// verifying a header signed by set N reads this digest and thereby learns the commitment for set -/// N+1, so it can verify the following update. Committing the current set instead would be -/// circular: you would need set N's commitment to verify the header carrying set N's commitment. -/// -/// Also note this is not `well_known_keys::AUTHORITIES`, which is `Babe::Authorities`; the BEEFY -/// keys live under the `Beefy` prefix. -pub const RELAY_BEEFY_NEXT_AUTHORITIES: [u8; 32] = [ - 0x08, 0xc4, 0x19, 0x74, 0xa9, 0x7d, 0xbf, 0x15, 0xcf, 0xbe, 0xc2, 0x83, 0x65, 0xbe, 0xa2, 0xda, - 0xaa, 0xcf, 0x00, 0xb9, 0xb4, 0x1f, 0xda, 0x7a, 0x92, 0x68, 0x82, 0x1c, 0x2a, 0x2b, 0x3e, 0x4c, -]; - /// `Beefy::ValidatorSetId` on the relay chain, the id of the *current* set. The digest reports /// `set_id + 1`, since it describes [`RELAY_BEEFY_NEXT_AUTHORITIES`]. pub const RELAY_BEEFY_VALIDATOR_SET_ID: [u8; 32] = [ @@ -115,7 +103,7 @@ pub mod pallet { /// The last commitment published to a header digest, and the set it describes. #[pallet::storage] - pub type Published = StorageValue<_, ([u8; 32], [u8; 32]), OptionQuery>; + pub type Published = StorageValue<_, (u64, [u8; 32], [u8; 32]), OptionQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -165,10 +153,19 @@ pub mod pallet { let mut progress = match next_progress( Pending::::get().as_ref(), - Published::::get().map(|(d, _)| d), + Published::::get().map(|(id, digest, _)| (id, digest)), + set_id, set_digest, ) { - Step::Done => return Ok(0), + // Already hashed this set, but the digest still goes in every header until it + // rotates. A verifier only reads the one header a proof happens to finalize, so + // publishing once would mean it almost never sees it. + Step::Done => { + if let Some((set_id, _, commitment)) = Published::::get() { + Self::deposit_digest(set_id, commitment); + } + return Ok(0); + }, Step::Restart => { Self::deposit_event(Event::CommitmentStarted { set_digest }); Progress::fresh(set_digest) @@ -188,18 +185,10 @@ pub mod pallet { if progress.absorbed as usize == NUM_VALIDATORS { let commitment = progress.state; - let payload = ApkCommitmentDigest { set_id, commitment }; - - // The header is the delivery mechanism: a client that has already authenticated - // this header through the BEEFY MMR's parachain heads root can read the commitment - // straight out of it, with no further proof. - frame_system::Pallet::::deposit_log(sp_runtime::DigestItem::Consensus( - APK_ENGINE_ID, - payload.encode(), - )); + Self::deposit_digest(set_id, commitment); Pending::::kill(); - Published::::put((set_digest, commitment)); + Published::::put((set_id, set_digest, commitment)); Self::deposit_event(Event::CommitmentPublished { set_id, set_digest, commitment }); } else { Pending::::put(&progress); @@ -207,6 +196,21 @@ pub mod pallet { Ok(take as u32) } + /// Put the commitment in this block's header. + /// + /// The header is the delivery mechanism: a client that has already authenticated it + /// through the BEEFY MMR's parachain heads root reads the commitment straight out of it, + /// with no further proof. It goes in every header the set covers rather than only the one + /// where the hashing finished, since a verifier only ever sees the single header a proof + /// finalizes and cannot choose which. + fn deposit_digest(set_id: u64, commitment: [u8; 32]) { + let payload = ApkCommitmentDigest { set_id, commitment }; + frame_system::Pallet::::deposit_log(sp_runtime::DigestItem::Consensus( + APK_ENGINE_ID, + payload.encode(), + )); + } + /// The id of the set the commitment describes: the relay's current set id plus one, since /// the keys come from `NextAuthorities`. fn relay_beefy_set_id() -> Result> { @@ -296,13 +300,17 @@ pub enum Step { /// set. Restarting is the only safe answer. pub fn next_progress( pending: Option<&Progress>, - published: Option<[u8; 32]>, + published: Option<(u64, [u8; 32])>, + set_id: u64, set_digest: [u8; 32], ) -> Step { match pending { Some(p) if p.set_digest != set_digest => Step::Restart, Some(p) => Step::Continue(p.clone()), - None if published == Some(set_digest) => Step::Done, + // Keyed on the set id as well as the keys. A session can rotate without changing the + // membership, and a client tracks commitments per set id, so the same commitment has to be + // published again under the new one or the client never learns it and stalls there. + None if published == Some((set_id, set_digest)) => Step::Done, None => Step::Restart, } } @@ -451,24 +459,32 @@ mod tests { #[test] fn a_fresh_chain_starts() { - assert_eq!(next_progress(None, None, SET_A), Step::Restart); + assert_eq!(next_progress(None, None, 1, SET_A), Step::Restart); } #[test] fn an_already_published_set_is_left_alone() { - assert_eq!(next_progress(None, Some(SET_A), SET_A), Step::Done); + assert_eq!(next_progress(None, Some((1, SET_A)), 1, SET_A), Step::Done); + } + + /// A session can rotate without the membership changing, which is common on small networks and + /// possible anywhere. The keys hash to the same digest, but the client files commitments under + /// the set id, so this has to publish again rather than deciding there is nothing to do. + #[test] + fn the_same_keys_under_a_new_set_id_are_published_again() { + assert_eq!(next_progress(None, Some((1, SET_A)), 2, SET_A), Step::Restart); } /// A new set arriving after one was published starts rather than stopping. #[test] fn a_new_set_starts_even_though_another_was_published() { - assert_eq!(next_progress(None, Some(SET_A), SET_B), Step::Restart); + assert_eq!(next_progress(None, Some((1, SET_A)), 2, SET_B), Step::Restart); } #[test] fn work_in_progress_on_the_same_set_continues() { let p = Progress { set_digest: SET_A, absorbed: 128, state: [1u8; 32] }; - assert_eq!(next_progress(Some(&p), None, SET_A), Step::Continue(p)); + assert_eq!(next_progress(Some(&p), None, 1, SET_A), Step::Continue(p)); } /// The case this whole function exists for: the authority set changed while a commitment was @@ -476,7 +492,7 @@ mod tests { #[test] fn a_rotation_part_way_through_restarts() { let p = Progress { set_digest: SET_A, absorbed: 512, state: [1u8; 32] }; - assert_eq!(next_progress(Some(&p), None, SET_B), Step::Restart); + assert_eq!(next_progress(Some(&p), None, 2, SET_B), Step::Restart); } /// And restarting has to mean *restarting*, not resuming with a relabelled set. If the state @@ -502,7 +518,12 @@ mod tests { let mut state = PartialCommitment::new().to_bytes(); state = absorb_slots(&first, 0, 300, state).unwrap(); assert_eq!( - next_progress(Some(&Progress { set_digest: SET_A, absorbed: 300, state }), None, SET_B), + next_progress( + Some(&Progress { set_digest: SET_A, absorbed: 300, state }), + None, + 2, + SET_B + ), Step::Restart ); diff --git a/modules/pallets/beefy-consensus-proofs/src/lib.rs b/modules/pallets/beefy-consensus-proofs/src/lib.rs index 350cb38ee..7aba97044 100644 --- a/modules/pallets/beefy-consensus-proofs/src/lib.rs +++ b/modules/pallets/beefy-consensus-proofs/src/lib.rs @@ -481,10 +481,21 @@ pub mod pallet { state_id: host.host_state_machine(), }, StateCommitmentHeight { - height: 1, + // The chain this runs on is rarely at genesis, and a commitment at + // height 1 tells a prover to start from a block whose state has long + // been pruned. + height: { + use sp_runtime::SaturatedConversion; + frame_system::Pallet::::block_number().saturated_into::() + }, commitment: StateCommitment { timestamp: host.timestamp().as_secs(), - overlay_root: None, + // A seeded commitment outranks anything a proof later creates for + // an older height, so it has to carry a real overlay root or every + // proof settles against an empty one. + overlay_root: Some(H256::from_slice( + pallet_ismp::ChildTrieRoot::::get().as_ref(), + )), state_root: H256::zero(), }, }, diff --git a/parachain/runtimes/gargantua/src/lib.rs b/parachain/runtimes/gargantua/src/lib.rs index fd4e87190..bd215a798 100644 --- a/parachain/runtimes/gargantua/src/lib.rs +++ b/parachain/runtimes/gargantua/src/lib.rs @@ -251,7 +251,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: Cow::Borrowed("gargantua"), impl_name: Cow::Borrowed("gargantua"), authoring_version: 1, - spec_version: 8_100, + spec_version: 8_101, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 383483841a0e69ffac4fdd25f786ccfc2775fe77 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Thu, 13 Aug 2026 23:56:15 +0100 Subject: [PATCH 22/48] keep the apk prover alive between proofs so the circuit setup is paid once --- tesseract/consensus/beefy/apk/src/lib.rs | 3 + tesseract/consensus/beefy/apk/src/service.rs | 160 ++++++++++++++++++ .../consensus/beefy/apk/tests/live_prover.rs | 7 +- tesseract/consensus/beefy/src/prover.rs | 36 ++-- 4 files changed, 195 insertions(+), 11 deletions(-) create mode 100644 tesseract/consensus/beefy/apk/src/service.rs diff --git a/tesseract/consensus/beefy/apk/src/lib.rs b/tesseract/consensus/beefy/apk/src/lib.rs index 7517037a8..49f5f54d5 100644 --- a/tesseract/consensus/beefy/apk/src/lib.rs +++ b/tesseract/consensus/beefy/apk/src/lib.rs @@ -43,6 +43,9 @@ use ismp_abi::bls_apk_beefy::BlsApkBeefy; mod command; pub use command::CommandProver; +mod service; +pub use service::ServiceProver; + #[cfg(feature = "local")] mod local; #[cfg(feature = "local")] diff --git a/tesseract/consensus/beefy/apk/src/service.rs b/tesseract/consensus/beefy/apk/src/service.rs new file mode 100644 index 000000000..ec3fdb408 --- /dev/null +++ b/tesseract/consensus/beefy/apk/src/service.rs @@ -0,0 +1,160 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Proving through a prover kept alive between proofs. +//! +//! [`crate::CommandProver`] runs the prover afresh for every proof, and the prover compiles the +//! circuit and generates its keys before it can do anything, about four minutes. Paid once that is +//! setup; paid per proof it is most of the wall clock. This keeps one process alive and talks to it +//! over its stdin and stdout, so the four minutes happen at startup and a proof costs only the two +//! minutes it actually takes. + +use std::{path::PathBuf, process::Stdio}; + +use alloy_primitives::U256; +use anyhow::{anyhow, Context}; +use ark_ec::AffineRepr; +use ark_ff::{BigInteger, PrimeField}; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + process::{Child, ChildStdin, ChildStdout, Command}, + sync::Mutex, +}; + +use crate::{decompress_g1, ApkProof, ApkProofRequest, ApkProver, BITLIST_WORDS}; + +/// Read until the prover says something in json. +/// +/// It shares stdout with the go library underneath it, which logs its progress there, so anything +/// that is not json is that library talking rather than a reply to us. +async fn read_json_line(stdout: &mut BufReader) -> Result { + loop { + let mut line = String::new(); + if stdout.read_line(&mut line).await? == 0 { + Err(anyhow!("the prover closed its output"))? + } + if let Ok(value) = json::from_str::(line.trim()) { + return Ok(value); + } + } +} + +/// A prover process, and the pipes to talk to it. +struct Session { + /// Kept so the process is killed when this is dropped rather than outliving the relayer. + _child: Child, + stdin: ChildStdin, + stdout: BufReader, +} + +/// Talks to a prover that stays running between proofs. +/// +/// The binary is expected to answer one json request per line with one json response per line, +/// which is what `gnark-apk-proofs`' `prove_serve` does. +pub struct ServiceProver { + /// One proof at a time. The prover is single threaded and the protocol is a line each way, so + /// two callers sharing it would read each other's answers. + session: Mutex, +} + +impl ServiceProver { + /// Start the prover and wait for it to finish its setup. + /// + /// This blocks for as long as the circuit takes to compile, so it belongs in startup rather + /// than on the path of the first proof. + pub async fn new(binary: PathBuf, srs_dir: Option) -> Result { + let mut command = Command::new(&binary); + if let Some(dir) = srs_dir { + command.arg(dir); + } + let mut child = command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .with_context(|| format!("could not start the prover at {binary:?}"))?; + + let stdin = child.stdin.take().ok_or_else(|| anyhow!("prover has no stdin"))?; + let mut stdout = + BufReader::new(child.stdout.take().ok_or_else(|| anyhow!("prover has no stdout"))?); + + // The prover announces itself once the setup is done, so a proof is never sent into a + // process that is still compiling and would look like a hang. + let ready = read_json_line(&mut stdout).await.context("the prover exited during setup")?; + if ready["ready"].as_bool() != Some(true) { + Err(anyhow!("the prover failed to start: {ready}"))? + } + + Ok(Self { session: Mutex::new(Session { _child: child, stdin, stdout }) }) + } +} + +#[async_trait::async_trait] +impl ApkProver for ServiceProver { + async fn prove(&self, request: ApkProofRequest) -> Result { + // The circuit takes uncompressed coordinates, so the compressed keys are opened up here + // rather than teaching the prover about our encoding. + let keys = request + .keys + .iter() + .map(|key| { + let point = decompress_g1(key)?; + let (x, y) = point.xy().ok_or_else(|| anyhow!("authority key is the identity"))?; + let mut packed = Vec::with_capacity(96); + packed.extend_from_slice(&x.into_bigint().to_bytes_be()); + packed.extend_from_slice(&y.into_bigint().to_bytes_be()); + Ok(hex::encode(packed)) + }) + .collect::, anyhow::Error>>()?; + + let mut line = json::to_string(&json::json!({ + "keys": keys, + "participation": request.participation, + }))?; + line.push('\n'); + + let mut session = self.session.lock().await; + session.stdin.write_all(line.as_bytes()).await.context("the prover is gone")?; + session.stdin.flush().await.context("the prover is gone")?; + + let response = read_json_line(&mut session.stdout) + .await + .context("the prover exited while proving")?; + if let Some(error) = response["error"].as_str() { + Err(anyhow!("the prover refused: {error}"))? + } + + let hex_field = |name: &str| -> Result, anyhow::Error> { + let raw = response[name].as_str().ok_or_else(|| anyhow!("proof has no {name}"))?; + Ok(hex::decode(raw.trim_start_matches("0x"))?) + }; + + let bitlist: [U256; BITLIST_WORDS] = response["bitlist"] + .as_array() + .ok_or_else(|| anyhow!("proof has no bitlist"))? + .iter() + .map(|word| { + let raw = word.as_str().ok_or_else(|| anyhow!("bitlist word is not a string"))?; + Ok(U256::from_be_slice(&hex::decode(raw.trim_start_matches("0x"))?)) + }) + .collect::, anyhow::Error>>()? + .try_into() + .map_err(|_| anyhow!("bitlist is not {BITLIST_WORDS} words"))?; + + let apk_commitment = <[u8; 32]>::try_from(hex_field("apkCommitment")?.as_slice()) + .map_err(|_| anyhow!("apk commitment is not 32 bytes"))?; + + Ok(ApkProof { proof: hex_field("apkProof")?, bitlist, apk_commitment }) + } +} diff --git a/tesseract/consensus/beefy/apk/tests/live_prover.rs b/tesseract/consensus/beefy/apk/tests/live_prover.rs index bc00c777e..e680ff0d9 100644 --- a/tesseract/consensus/beefy/apk/tests/live_prover.rs +++ b/tesseract/consensus/beefy/apk/tests/live_prover.rs @@ -228,7 +228,12 @@ async fn writes_initial_state_for_bootstrapping() { // into the next set before it has ever seen a header digest, and a set with no commitment is // refused rather than checked against zero. After this the digests take over. let current = H256(prover.current_apk_commitment(latest).await.unwrap()); - let next = H256(prover.next_apk_commitment(latest).await.unwrap()); + // Leaving the next set empty is how forward chaining gets demonstrated: the client then has + // to learn that commitment from a header digest rather than being handed it here. + let next = match std::env::var("APK_SEED_NEXT").as_deref() { + Ok("0") => H256::zero(), + _ => H256(prover.next_apk_commitment(latest).await.unwrap()), + }; let state = ApkConsensusState { latest_beefy_height: trusted.latest_beefy_height, beefy_activation_block: trusted.beefy_activation_block, diff --git a/tesseract/consensus/beefy/src/prover.rs b/tesseract/consensus/beefy/src/prover.rs index 4fa247525..7aa2b2bc4 100644 --- a/tesseract/consensus/beefy/src/prover.rs +++ b/tesseract/consensus/beefy/src/prover.rs @@ -126,14 +126,22 @@ pub struct ProverConfig { pub max_rpc_payload_size: Option, /// Query batch size for mmr leaves pub query_batch_size: Option, - /// The `gnark-apk-proofs` prover binary, run once per proof. Only read by the `Apk` variant, - /// which cannot link the prover directly, see `apk_beefy::command`. + /// The `gnark-apk-proofs` prover binary. Only read by the `Apk` variant, which cannot link the + /// prover directly, see `apk_beefy::command`. #[serde(default)] pub apk_prover_binary: Option, - /// Where the prover exchanges its input and proof files. Defaults to a directory beside the - /// binary when unset. + /// Where a one shot prover exchanges its input and proof files. Ignored when the prover is + /// kept alive, which is the default. #[serde(default)] pub apk_prover_dir: Option, + /// Run the prover once per proof instead of keeping it alive. Costs the circuit setup, four + /// minutes, on every proof, so it is only worth it for a prover with no serve mode. + #[serde(default)] + pub apk_prover_one_shot: bool, + /// Where the circuit's structured reference string lives, passed to the prover. Left unset it + /// falls back to the prover's own default. + #[serde(default)] + pub apk_srs_dir: Option, } /// The BEEFY prover produces BEEFY consensus proofs using either the naive or zk variety. Consensus @@ -683,12 +691,20 @@ where .apk_prover_binary .clone() .ok_or_else(|| anyhow!("`apk_prover_binary` is required by the apk variant"))?; - let work_dir = config - .apk_prover_dir - .clone() - .unwrap_or_else(|| binary.with_file_name("apk-prover-work")); - let apk_prover = apk_beefy::CommandProver::new(binary, work_dir)?; - Prover::Apk(apk_beefy::Prover::new(prover, Arc::new(apk_prover))) + let apk_prover: Arc = if config.apk_prover_one_shot { + let work_dir = config + .apk_prover_dir + .clone() + .unwrap_or_else(|| binary.with_file_name("apk-prover-work")); + Arc::new(apk_beefy::CommandProver::new(binary, work_dir)?) + } else { + // Setup runs here, before any proving, so the first proof is not four minutes + // slower than the rest. + Arc::new( + apk_beefy::ServiceProver::new(binary, config.apk_srs_dir.clone()).await?, + ) + }; + Prover::Apk(apk_beefy::Prover::new(prover, apk_prover)) }, }; From 5f716bbabb921fa67d28cc064f3cb57d9ec7b6a3 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 14 Aug 2026 01:28:10 +0100 Subject: [PATCH 23/48] prove finality inside a session so the client can learn the next set's commitment --- .../pallets/beefy-consensus-proofs/src/lib.rs | 13 ++- .../beefy-consensus-proofs/src/types.rs | 51 ++++++++++ tesseract/consensus/beefy/src/prover.rs | 96 +++++++++++++++++++ 3 files changed, 158 insertions(+), 2 deletions(-) diff --git a/modules/pallets/beefy-consensus-proofs/src/lib.rs b/modules/pallets/beefy-consensus-proofs/src/lib.rs index 7aba97044..e25841744 100644 --- a/modules/pallets/beefy-consensus-proofs/src/lib.rs +++ b/modules/pallets/beefy-consensus-proofs/src/lib.rs @@ -931,6 +931,8 @@ pub mod pallet { .consensus_state(ismp_beefy::BEEFY_CONSENSUS_ID) .map_err(|_| Error::::NotInitialized)?; let (prev_current_set, _) = Self::authority_set_ids(&prev_state_bytes, proof_type)?; + let prev_commitment_unknown = + types::next_commitment_unknown(&prev_state_bytes, proof_type); let prev_height = Self::latest_height()?; let consensus_proof = match proof_type { @@ -1020,6 +1022,13 @@ pub mod pallet { let rotated = new_current_set > prev_current_set; + // An apk client learns each set's commitment from a digest in a parachain header, and + // the proof that carries it neither rotates nor has to finalize anything new. Without + // this the rules below would turn it away as pointless, and the client would sit on a + // set it can never rotate out of. + let learned_commitment = prev_commitment_unknown && + !types::next_commitment_unknown(&new_state_bytes, proof_type); + // Messaging proofs must finalize a parachain head we haven't seen; one that doesn't // carries no new work and is rejected. Rotation proofs are exempt: the session // boundary justification carries whatever head the relay chain held at that block, @@ -1029,7 +1038,7 @@ pub mod pallet { // pinning the consensus state on the old set forever — the mandatory-block // justification is the only one a prover can obtain for that session, so every // retry fails identically. - if !rotated && latest_height <= prev_height { + if !rotated && !learned_commitment && latest_height <= prev_height { Err(Error::::StaleProof)? } @@ -1054,7 +1063,7 @@ pub mod pallet { // Reject proofs that would be no-ops: no rotation and no new messages. let last_rewarded = LastRewardedDispatchRoot::::get().unwrap_or_default(); let has_new_messages = child_trie_root != last_rewarded && latest_height > prev_height; - if !rotated && !has_new_messages { + if !rotated && !learned_commitment && !has_new_messages { Err(Error::::NoNewWork)? } diff --git a/modules/pallets/beefy-consensus-proofs/src/types.rs b/modules/pallets/beefy-consensus-proofs/src/types.rs index 375861998..f2808dc80 100644 --- a/modules/pallets/beefy-consensus-proofs/src/types.rs +++ b/modules/pallets/beefy-consensus-proofs/src/types.rs @@ -16,6 +16,7 @@ //! Types for `pallet-beefy-consensus-proofs`. use alloc::vec::Vec; +use codec::{Decode, Encode}; /// Offchain-storage prefix for messaging proof bytes, combined with the proven parachain /// height. @@ -94,6 +95,56 @@ mod tests { } } +/// Whether an apk state is still missing the commitment for the set it will rotate into. +/// +/// A proof that fills this in is doing work even if it finalizes nothing new, since the client +/// cannot accept the rotation until it knows the incoming set's keys. +pub fn next_commitment_unknown(state: &[u8], proof_type: u8) -> bool { + proof_type == PROOF_TYPE_APK && + beefy_verifier_primitives::ApkConsensusState::decode(&mut &state[..]) + .map(|state| state.next_authorities.apk_commitment.is_zero()) + .unwrap_or(false) +} + +#[cfg(test)] +mod commitment_tests { + use super::*; + use beefy_verifier_primitives::{ApkAuthoritySet, ApkConsensusState}; + use primitive_types::H256; + + fn state(next: H256) -> Vec { + ApkConsensusState { + latest_beefy_height: 100, + beefy_activation_block: 0, + mmr_root_hash: H256::zero(), + current_authorities: ApkAuthoritySet { + id: 7, + len: 2, + apk_commitment: H256::repeat_byte(1), + }, + next_authorities: ApkAuthoritySet { id: 8, len: 2, apk_commitment: next }, + } + .encode() + } + + #[test] + fn an_empty_next_commitment_is_the_only_thing_worth_learning() { + assert!(next_commitment_unknown(&state(H256::zero()), PROOF_TYPE_APK)); + assert!(!next_commitment_unknown(&state(H256::repeat_byte(2)), PROOF_TYPE_APK)); + } + + #[test] + fn other_proof_types_never_learn_a_commitment() { + assert!(!next_commitment_unknown(&state(H256::zero()), PROOF_TYPE_NAIVE)); + assert!(!next_commitment_unknown(&state(H256::zero()), PROOF_TYPE_SP1)); + } + + #[test] + fn a_state_that_is_not_the_apk_shape_is_not_missing_anything() { + assert!(!next_commitment_unknown(&[0u8; 3], PROOF_TYPE_APK)); + } +} + /// BEEFY host-function backed crypto used by `beefy-verifier`. pub struct SubstrateCrypto; diff --git a/tesseract/consensus/beefy/src/prover.rs b/tesseract/consensus/beefy/src/prover.rs index 7aa2b2bc4..b24186535 100644 --- a/tesseract/consensus/beefy/src/prover.rs +++ b/tesseract/consensus/beefy/src/prover.rs @@ -175,6 +175,10 @@ pub const PROOF_TYPE_ECDSA: u8 = 0x00; pub const PROOF_TYPE_SP1: u8 = 0x01; /// Proof type identifier for aggregate public key proofs (BlsApkBeefy) +/// How far back to look for a justification inside the session that is ending. A session is +/// short on a test relay and long on a live one, and this only bounds the search. +const SESSION_SEARCH_WINDOW: u64 = 2400; + pub const PROOF_TYPE_APK: u8 = 0x02; impl BeefyProver @@ -352,6 +356,43 @@ where /// Performs a linear search for the BEEFY justification which finalizes the given epoch /// boundary + /// The last BEEFY justification strictly below `before`, searched back over `window` blocks. + /// + /// Used to prove finality inside the session that is about to end, where scanning forward + /// would land on the session boundary itself and a justification signed by the incoming set. + pub async fn justification_before( + &self, + before: u64, + window: u64, + ) -> anyhow::Result>> { + let relay_rpc = self.prover.inner().relay_rpc.clone(); + for number in (before.saturating_sub(window)..before).rev() { + let Some(hash) = relay_rpc.chain_get_block_hash(Some(number.into())).await? else { + continue; + }; + let Some(justifications) = relay_rpc + .chain_get_block(Some(hash)) + .await? + .ok_or_else(|| anyhow!("failed to find block for {hash:?}"))? + .justifications + else { + continue; + }; + let beefy = justifications + .into_iter() + .find(|(id, _)| id == b"BEEF") + .map(|(_, encoded)| { + VersionedFinalityProof::::decode(&mut &*encoded) + }) + .transpose()? + .map(|VersionedFinalityProof::V1(commitment)| commitment); + if beefy.is_some() { + return Ok(beefy); + } + } + Ok(None) + } + pub async fn epoch_justification_for( &self, start: u64, @@ -441,6 +482,61 @@ where commitment.commitment ); + // An apk client checks a proof against a commitment to the signing set's + // keys, and the justification that rotates into a set is signed by that + // same set, so a set whose commitment is still unknown can never be + // rotated into directly. The commitment reaches the client through a + // digest in a parachain header, and the header this rotation carries sits + // on the session boundary, where the relay's next set is only just being + // queued. Proving finality one block earlier carries a header from the end + // of the session instead, which does name the incoming set, and the + // rotation goes through on the next tick. + if matches!(self.prover, Prover::Apk(_)) && + consensus_state.inner.next_authorities.keyset_commitment.is_zero() + { + let epoch_change_number: u64 = epoch_change_header.number().into(); + let Some(commitment) = self + .justification_before(epoch_change_number, SESSION_SEARCH_WINDOW) + .await? + else { + tracing::warn!( + target: crate::LOG_TARGET, + "No justification below {epoch_change_number} to learn the commitment for {next_set_id} from" + ); + return Ok(()); + }; + + let consensus_proof = self + .consensus_proof( + commitment.clone(), + consensus_state.inner.clone(), + ) + .await?; + let message = ConsensusProof { + finalized_height: commitment.commitment.block_number, + set_id: consensus_state.inner.current_authorities.id, + message: ConsensusMessage { + consensus_proof, + consensus_state_id: self.config.consensus_state_id, + signer: H256::random().encode(), + }, + }; + + tracing::info!( + target: crate::LOG_TARGET, + "Proving finality at {} so the client learns the commitment for {next_set_id}", + commitment.commitment.block_number, + ); + let destinations: Vec = + self.config.state_machines.clone(); + self.backend.send_messages_proof(&destinations, message).await?; + + consensus_state.inner.latest_beefy_height = + commitment.commitment.block_number; + self.backend.save_state(&consensus_state).await?; + return Ok(()); + } + let consensus_proof = self .consensus_proof(commitment.clone(), consensus_state.inner.clone()) .await?; From d13e89dc0f4e535058c5eccf7fc6b17bd9391124 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 14 Aug 2026 12:54:18 +0100 Subject: [PATCH 24/48] pick the block that teaches a commitment by what its header carries, not by where it sits --- .../beefy-consensus-proofs/src/types.rs | 3 +- tesseract/consensus/beefy/src/prover.rs | 76 ++--- .../consensus/beefy/tests/apk_messaging.rs | 268 ++++++++++++++++++ 3 files changed, 314 insertions(+), 33 deletions(-) create mode 100644 tesseract/consensus/beefy/tests/apk_messaging.rs diff --git a/modules/pallets/beefy-consensus-proofs/src/types.rs b/modules/pallets/beefy-consensus-proofs/src/types.rs index f2808dc80..c936bf831 100644 --- a/modules/pallets/beefy-consensus-proofs/src/types.rs +++ b/modules/pallets/beefy-consensus-proofs/src/types.rs @@ -16,7 +16,7 @@ //! Types for `pallet-beefy-consensus-proofs`. use alloc::vec::Vec; -use codec::{Decode, Encode}; +use codec::Decode; /// Offchain-storage prefix for messaging proof bytes, combined with the proven parachain /// height. @@ -110,6 +110,7 @@ pub fn next_commitment_unknown(state: &[u8], proof_type: u8) -> bool { mod commitment_tests { use super::*; use beefy_verifier_primitives::{ApkAuthoritySet, ApkConsensusState}; + use codec::Encode; use primitive_types::H256; fn state(next: H256) -> Vec { diff --git a/tesseract/consensus/beefy/src/prover.rs b/tesseract/consensus/beefy/src/prover.rs index b24186535..04ac632a1 100644 --- a/tesseract/consensus/beefy/src/prover.rs +++ b/tesseract/consensus/beefy/src/prover.rs @@ -44,7 +44,7 @@ use beefy_prover::{ relay::{fetch_latest_beefy_justification, parachain_header_storage_key}, BEEFY_VALIDATOR_SET_ID, }; -use beefy_verifier_primitives::ConsensusState; +use beefy_verifier_primitives::{ApkCommitmentDigest, ConsensusState}; use ismp::{ consensus::ConsensusStateId, events::Event, host::StateMachine, messaging::ConsensusMessage, }; @@ -175,10 +175,6 @@ pub const PROOF_TYPE_ECDSA: u8 = 0x00; pub const PROOF_TYPE_SP1: u8 = 0x01; /// Proof type identifier for aggregate public key proofs (BlsApkBeefy) -/// How far back to look for a justification inside the session that is ending. A session is -/// short on a test relay and long on a live one, and this only bounds the search. -const SESSION_SEARCH_WINDOW: u64 = 2400; - pub const PROOF_TYPE_APK: u8 = 0x02; impl BeefyProver @@ -356,43 +352,47 @@ where /// Performs a linear search for the BEEFY justification which finalizes the given epoch /// boundary - /// The last BEEFY justification strictly below `before`, searched back over `window` blocks. + /// The earliest justification the client can verify whose parachain header names `set_id`. /// - /// Used to prove finality inside the session that is about to end, where scanning forward - /// would land on the session boundary itself and a justification signed by the incoming set. - pub async fn justification_before( + /// Position in the session is not enough to tell. The digest naming a set only appears once + /// the relay has queued it and the parachain has published it, so blocks early in a session + /// still name the set before. Taking the earliest one that does name it, rather than the last + /// block of the session, leaves the rest of the session provable, which is what a proof for + /// new messages needs. + pub async fn teaching_justification( &self, - before: u64, - window: u64, + from: u64, + until: u64, + set_id: u64, + para_id: u32, ) -> anyhow::Result>> { let relay_rpc = self.prover.inner().relay_rpc.clone(); - for number in (before.saturating_sub(window)..before).rev() { - let Some(hash) = relay_rpc.chain_get_block_hash(Some(number.into())).await? else { - continue; + let mut cursor = from; + while cursor < until { + let Some(commitment) = self.epoch_justification_for(cursor).await? else { + return Ok(None); }; - let Some(justifications) = relay_rpc - .chain_get_block(Some(hash)) - .await? - .ok_or_else(|| anyhow!("failed to find block for {hash:?}"))? - .justifications - else { + let number: u64 = commitment.commitment.block_number.into(); + if number >= until { + return Ok(None); + } + cursor = number + 1; + + let Some(hash) = relay_rpc.chain_get_block_hash(Some(number.into())).await? else { continue; }; - let beefy = justifications - .into_iter() - .find(|(id, _)| id == b"BEEF") - .map(|(_, encoded)| { - VersionedFinalityProof::::decode(&mut &*encoded) - }) - .transpose()? - .map(|VersionedFinalityProof::V1(commitment)| commitment); - if beefy.is_some() { - return Ok(beefy); + let header = query_parachain_header(&relay_rpc, hash, para_id).await?; + if ApkCommitmentDigest::find_in(&header.digest).map(|digest| digest.set_id) == + Some(set_id) + { + return Ok(Some(commitment)); } } Ok(None) } + /// Performs a linear search for the BEEFY justification which finalizes the given epoch + /// boundary pub async fn epoch_justification_for( &self, start: u64, @@ -495,13 +495,25 @@ where consensus_state.inner.next_authorities.keyset_commitment.is_zero() { let epoch_change_number: u64 = epoch_change_header.number().into(); + let from = u64::from(consensus_state.inner.latest_beefy_height) + 1; let Some(commitment) = self - .justification_before(epoch_change_number, SESSION_SEARCH_WINDOW) + .teaching_justification( + from, + epoch_change_number, + next_set_id, + para_id, + ) .await? else { + // Either the parachain has not published the commitment yet, in + // which case a later tick picks it up, or the client has consumed + // the session and never will. Proving anyway would spend minutes + // on something the client rejects as stale, and would do it again + // every tick, so nothing is built here. tracing::warn!( target: crate::LOG_TARGET, - "No justification below {epoch_change_number} to learn the commitment for {next_set_id} from" + "No block in {from}..{epoch_change_number} names the commitment for {next_set_id}, the client is on {}", + consensus_state.inner.current_authorities.id, ); return Ok(()); }; diff --git a/tesseract/consensus/beefy/tests/apk_messaging.rs b/tesseract/consensus/beefy/tests/apk_messaging.rs new file mode 100644 index 000000000..ec31658df --- /dev/null +++ b/tesseract/consensus/beefy/tests/apk_messaging.rs @@ -0,0 +1,268 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Checks the pallet accepts an apk proof whose only contribution is new messages. +//! +//! Rotating and teaching are both proven by watching the prover run, but neither of them is what +//! the bridge is for. Messaging is, and it is the one thing the prover's own loop will not reach on +//! a test relay: a session there lasts about as long as a proof takes, so there is always a +//! rotation waiting and the loop takes that path first. The test does what the loop cannot and +//! picks the block itself. +//! +//! The pallet turns away a proof that rotates nothing, finalizes nothing new and teaches nothing, +//! so acceptance has to come through one of the three. Picking a justification from the set the +//! client already trusts rules out rotation, and the state afterwards shows whether a commitment +//! was learned. If it was not, and the proof was accepted anyway, messages are the only thing left +//! it can have been accepted for, which pins the branch without reaching inside the pallet. +//! +//! Needs the relay, its parachain, a warm prover binary, and ismp traffic on the parachain so +//! there is something to prove. Takes minutes, since it generates a real SNARK. +//! +//! RELAY_WS_URL=ws://127.0.0.1:9979 PARA_WS_URL=ws://127.0.0.1:9981 PARA_ID=4009 \ +//! APK_PROVER_BINARY=~/gnark-apk-proofs/target/release/examples/prove_serve \ +//! PARA_SIGNER=0xe5be9a50... \ +//! cargo test -p tesseract-beefy --test apk_messaging -- --ignored --nocapture + +use std::sync::Arc; + +use anyhow::anyhow; +use codec::Decode; +use ismp::{consensus::StateMachineId, host::StateMachine, messaging::ConsensusMessage}; +use polkadot_sdk::sp_consensus_beefy::{ + ecdsa_crypto::Signature, SignedCommitment, VersionedFinalityProof, +}; +use primitive_types::H256; +use subxt::{backend::legacy::LegacyRpcMethods, config::Header as _}; +use tesseract_beefy::{ + backend::{ConsensusProof, OnchainBackend, ProofBackend}, + prover::{ + query_parachain_header, BeefyProver, BeefyProverConfig, ProofVariant, Prover, ProverConfig, + }, +}; +use tesseract_substrate::{ + config::{Blake2SubstrateChain, KeccakSubstrateChain}, + SubstrateClient, SubstrateConfig, +}; + +/// How far back to look for a justification the client can still verify. +const SEARCH_WINDOW: u64 = 2400; + +fn env(name: &str) -> String { + std::env::var(name).unwrap_or_else(|_| panic!("{name} must be set")) +} + +/// The first BEEFY justification at or above `from`, searched over `window` blocks. +/// +/// The prover has its own version of this, but it belongs to a type whose construction compiles +/// the circuit, and the point here is to find out whether there is anything worth proving before +/// paying for that. +async fn justification_at_or_above( + rpc: &LegacyRpcMethods, + from: u64, + window: u64, +) -> Result>, anyhow::Error> { + for number in from..from + window { + let Some(hash) = rpc.chain_get_block_hash(Some(number.into())).await? else { + continue; + }; + let Some(justifications) = rpc + .chain_get_block(Some(hash)) + .await? + .ok_or_else(|| anyhow!("failed to find block for {hash:?}"))? + .justifications + else { + continue; + }; + if let Some(found) = justifications + .into_iter() + .find(|(id, _)| id == b"BEEF") + .map(|(_, encoded)| VersionedFinalityProof::::decode(&mut &*encoded)) + .transpose()? + .map(|VersionedFinalityProof::V1(commitment)| commitment) + { + return Ok(Some(found)); + } + } + Ok(None) +} + +#[tokio::test(flavor = "multi_thread")] +#[ignore = "needs a live BLS relay, its parachain, the prover binary and ismp traffic"] +async fn a_messaging_proof_is_accepted_as_new_work() -> Result<(), anyhow::Error> { + let relay_ws = env("RELAY_WS_URL"); + let para_ws = env("PARA_WS_URL"); + let para_id: u32 = env("PARA_ID").parse().expect("para id is a number"); + let signer = env("PARA_SIGNER"); + + let substrate = SubstrateClient::::new( + SubstrateConfig { + state_machine: Some(StateMachine::Kusama(para_id)), + hashing: None, + consensus_state_id: None, + rpc_ws: para_ws.clone(), + max_rpc_payload_size: None, + signer: Some(signer), + initial_height: None, + max_concurent_queries: None, + poll_interval: None, + fee_token_decimals: None, + } + .resolve() + .await?, + ) + .await?; + + let state_machine_id = + StateMachineId { state_id: StateMachine::Kusama(para_id), consensus_state_id: *b"PAS0" }; + let backend: Arc = Arc::new(OnchainBackend::::new( + substrate.client.clone(), + substrate.rpc_client.clone(), + substrate.signer.clone(), + state_machine_id, + )); + + let before = backend.load_state().await?; + let trusted_set = before.inner.current_authorities.id; + + // The client can only check a signature from the set it currently trusts, and it rejects a + // justification it has already seen, so the block has to sit above its height and below the + // end of its session. It also has to finalize a parachain head the client has not reached, + // which is checked here rather than after spending minutes on a proof the pallet will refuse. + let (_, relay_rpc_client) = + subxt_utils::client::ws_client::(&relay_ws, 15 * 1024 * 1024).await?; + let relay_rpc = LegacyRpcMethods::::new(relay_rpc_client); + + // Search upward rather than down. The earliest usable block is the one whose parachain header + // still names the set the client just rotated into, so it teaches nothing and the acceptance + // can only be about its messages. Blocks later in the session start naming the set after, + // which the client does not know yet, and would be accepted for teaching that instead. + let mut cursor = u64::from(before.inner.latest_beefy_height) + 1; + let commitment = loop { + let Some(candidate) = justification_at_or_above(&relay_rpc, cursor, SEARCH_WINDOW).await? + else { + return Err(anyhow!("no justification above {cursor} on the relay")); + }; + let number = candidate.commitment.block_number; + cursor = u64::from(number) + 1; + + if candidate.commitment.validator_set_id != trusted_set { + return Err(anyhow!( + "the client at {} has consumed everything set {trusted_set} signed, so there is \ + nothing left it can verify. Let the prover run it forward and try again.", + before.inner.latest_beefy_height, + )); + } + let hash = relay_rpc + .chain_get_block_hash(Some(number.into())) + .await? + .ok_or_else(|| anyhow!("relay block {number} vanished"))?; + let para_head: u64 = query_parachain_header(&relay_rpc, hash, para_id).await?.number.into(); + if para_head > before.finalized_parachain_height { + break candidate; + } + // Seeding a client commits it to the parachain head of the block the seeding ran in, and + // the relay's view of the parachain trails that, so right after a seed there is nothing to + // prove until the relay catches up. + println!( + "beefy block {number} only finalizes parachain {para_head}, at or below the client's {}", + before.finalized_parachain_height, + ); + }; + + println!( + "proving beefy block {} for set {trusted_set} against trusted height {}, this takes minutes", + commitment.commitment.block_number, before.inner.latest_beefy_height, + ); + + // The prover is built last. Starting it compiles the circuit, minutes of work, and there is no + // sense paying that before knowing there is a block worth proving. + let prover_config = ProverConfig { + relay_rpc_ws: relay_ws.clone(), + para_rpc_ws: para_ws.clone(), + para_ids: vec![para_id], + proof_variant: ProofVariant::Apk, + max_rpc_payload_size: None, + query_batch_size: None, + apk_prover_binary: Some(env("APK_PROVER_BINARY").into()), + apk_prover_dir: None, + apk_prover_one_shot: false, + apk_srs_dir: None, + }; + let prover: Prover = + Prover::new(prover_config, Default::default()).await?; + let beefy = BeefyProver::< + Blake2SubstrateChain, + KeccakSubstrateChain, + zk_beefy::LocalProver, + dyn ProofBackend, + >::new( + BeefyProverConfig { + consensus_state_id: *b"PAS0", + minimum_finalization_height: 0, + state_machines: vec![StateMachine::Evm(97)], + backend: Default::default(), + }, + substrate, + prover, + backend.clone(), + ) + .await?; + let consensus_proof = beefy.consensus_proof(commitment.clone(), before.inner.clone()).await?; + + // The same call the prover's loop makes, so the test exercises submission rather than + // imitating it. + backend + .send_messages_proof( + &[StateMachine::Evm(97)], + ConsensusProof { + finalized_height: commitment.commitment.block_number, + set_id: trusted_set, + message: ConsensusMessage { + consensus_proof, + consensus_state_id: *b"PAS0", + signer: H256::random().as_bytes().to_vec(), + }, + }, + ) + .await?; + + let after = backend.load_state().await?; + assert_eq!( + after.inner.current_authorities.id, trusted_set, + "the proof rotated the authority set, so it was not accepted for its messages", + ); + assert_eq!( + after.inner.next_authorities.keyset_commitment, + before.inner.next_authorities.keyset_commitment, + "the proof taught a commitment, so that is what it could have been accepted for", + ); + assert!( + after.inner.latest_beefy_height > before.inner.latest_beefy_height, + "the client did not move, so the proof was not applied", + ); + assert!( + after.finalized_parachain_height > before.finalized_parachain_height, + "no new parachain height was finalized, which is the whole point of a messaging proof", + ); + + println!( + "messaging proof accepted on set {trusted_set}: beefy {} -> {}, parachain {} -> {}", + before.inner.latest_beefy_height, + after.inner.latest_beefy_height, + before.finalized_parachain_height, + after.finalized_parachain_height, + ); + Ok(()) +} From 16d0fe5f590ac91f5a219c07892767a9f40755e0 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 14 Aug 2026 23:18:40 +0100 Subject: [PATCH 25/48] take the apk contract without the cipher suite argument --- evm/tests/foundry/BlsApkBeefy.t.sol | 81 ++++ evm/tests/foundry/vendor/ApkProof.sol | 534 ++++++++++++++++++++++++++ 2 files changed, 615 insertions(+) create mode 100644 evm/tests/foundry/BlsApkBeefy.t.sol create mode 100644 evm/tests/foundry/vendor/ApkProof.sol diff --git a/evm/tests/foundry/BlsApkBeefy.t.sol b/evm/tests/foundry/BlsApkBeefy.t.sol new file mode 100644 index 000000000..21e216704 --- /dev/null +++ b/evm/tests/foundry/BlsApkBeefy.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.30; + +import {Test, console} from "forge-std/Test.sol"; +import {IntermediateState} from "@hyperbridge/core/interfaces/IConsensusV2.sol"; + +import {BlsApkBeefy} from "../../src/consensus/BlsApkBeefy.sol"; +import {BlsApkConsensusState} from "../../src/consensus/Types.sol"; +import {ApkProof} from "./vendor/ApkProof.sol"; +import {PlonkVerifier} from "./vendor/PlonkVerifier.sol"; + +/** + * @title BEEFY verified through an aggregate public key proof. + * + * @notice The fixtures come from `bls_apk_live_inputs` and `bls_apk_live_fixture` in the Rust + * verifier, plus a PLONK proof generated by gnark-apk-proofs for the same validator set. Every + * part is real: the commitment was signed by a running relay's paired ecdsa_bls381 validators, the + * MMR proof has actual depth, and the parachain header is a registered para's. + * + * The point of this test is the gas number, and it is only comparable to `BlsBeefyTest`'s + * `test_verify_live_proof` because both run the whole client over live data in the same harness. + * + * Needs EIP-2537: + * FOUNDRY_PROFILE=bls forge test --match-contract BlsApkBeefyTest -vv --gas-report + */ +contract BlsApkBeefyTest is Test { + BlsApkBeefy internal client; + + function setUp() public { + // False selects the basic ciphersuite, which is what substrate's BEEFY signs with. PoP + // would hash the same message to a different point and fail with nothing to explain why. + ApkProof apk = new ApkProof(address(new PlonkVerifier())); + client = new BlsApkBeefy(address(apk)); + } + + function _state() internal view returns (bytes memory) { + return vm.parseBytes(vm.readFile("tests/foundry/fixtures/bls-apk-beefy-state.hex")); + } + + function _proof() internal view returns (bytes memory) { + return vm.parseBytes(vm.readFile("tests/foundry/fixtures/bls-apk-beefy-proof.hex")); + } + + /// A complete consensus update: SNARK over the aggregate key, the batched pairing that binds + /// apk2 and checks the signature, MMR leaf inclusion, and the parachain header proof. + function test_verify_live_proof() public view { + (bytes memory newStateBytes, IntermediateState[] memory intermediates,) = client.verify(_state(), _proof()); + + BlsApkConsensusState memory newState = abi.decode(newStateBytes, (BlsApkConsensusState)); + BlsApkConsensusState memory oldState = abi.decode(_state(), (BlsApkConsensusState)); + + assertGt(newState.latestHeight, oldState.latestHeight, "height should advance"); + assertEq(intermediates.length, 1, "should finalize the registered parachain"); + assertEq(intermediates[0].stateMachineId, 4009, "should be para 4009"); + assertGt(intermediates[0].height, 0, "parachain height should be non-zero"); + } + + /// Isolates the cost of the update itself, so the number can be compared with the merkle + /// client's without the calldata and fixture reading counted in. + function test_gas_verify_live_proof() public view { + bytes memory state = _state(); + bytes memory proof = _proof(); + + uint256 before = gasleft(); + client.verify(state, proof); + uint256 used = before - gasleft(); + + console.log("BlsApkBeefy.verify gas:", used); + } + + /// Replaying a proof the state has already passed is a no-op rather than a revert, matching the + /// merkle client. + function test_stale_proof_is_a_noop() public view { + (bytes memory advanced,,) = client.verify(_state(), _proof()); + + (bytes memory again, IntermediateState[] memory intermediates,) = client.verify(advanced, _proof()); + + assertEq(again, advanced, "state should be unchanged"); + assertEq(intermediates.length, 0, "a stale proof finalizes nothing"); + } +} diff --git a/evm/tests/foundry/vendor/ApkProof.sol b/evm/tests/foundry/vendor/ApkProof.sol new file mode 100644 index 000000000..e324768c1 --- /dev/null +++ b/evm/tests/foundry/vendor/ApkProof.sol @@ -0,0 +1,534 @@ +// Copyright 2026 Polytope Labs. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pragma solidity ^0.8.28; + +import {PlonkVerifier} from "./PlonkVerifier.sol"; + +/** + * @title APK Proof & BLS Aggregate Signature Verifier + * @notice Verifies both APK aggregation proofs (via PLONK) and aggregate BLS + * signatures using the scheme from "Efficient Aggregatable BLS + * Signatures with Chaum-Pedersen Proofs" (https://eprint.iacr.org/2022/1611). + * + * @dev BLS aggregate signature verification equation: + * e(asig + t·apk₁, g₂) = e(H(m) + t·g₁, apk₂) + * Checked as: e(asig + t·apk₁, -g₂) · e(H(m) + t·g₁, apk₂) = 1 + * where t = hash_to_field(H(m) ‖ sig ‖ apk₁ ‖ apk₂) via expand_message_xmd. + * + * BLS12-381 G1 points are passed as `bytes32[3]` (96 bytes total): + * X (48 bytes big-endian) || Y (48 bytes big-endian), matching the + * standard gnark-crypto / EIP-2537 uncompressed G1 format. + * + * G2 points are passed as `bytes32[6]` (192 bytes uncompressed): + * X.c0‖X.c1‖Y.c0‖Y.c1, 48 bytes each. + * + * Requires Prague EVM (Pectra hardfork) for EIP-2537 BLS12-381 precompiles. + * + * Security assumptions and trust boundaries: + * - All elliptic-curve arithmetic (G1ADD, G1MSM, pairing, map-to-G1) is + * delegated to the EIP-2537 precompiles, which are trusted to perform + * on-curve and subgroup validation of their inputs per the EIP. This + * contract therefore performs no separate point validation (finding 50). + * - Verification is stateless and idempotent: it does not track consumed + * proofs, so replay protection (nonce/uniqueness), if required, must be + * enforced by the calling application (finding 53). + * - hashToG1 and the BLS challenge derivation implement expand_message_xmd + * (RFC 9380) with the w3f/bls cipher suite; see the per-function NatSpec + * for the exact DST and parameters (findings 49, 52, 55). + */ +contract ApkProof { + PlonkVerifier public immutable _plonk; + + error G1AddFailed(); + error PlonkVerificationFailed(); + error SignatureVerificationFailed(); + + + // Precompile addresses + uint256 constant PRECOMPILE_MODEXP = 0x05; + uint256 constant PRECOMPILE_BLS12_G1ADD = 0x0b; + uint256 constant PRECOMPILE_BLS12_G1MSM = 0x0c; + uint256 constant PRECOMPILE_BLS12_PAIRING = 0x0f; + uint256 constant PRECOMPILE_BLS12_MAP_FP_TO_G1 = 0x10; + + /** + * Protocol-fixed seed point for APK aggregation. + * Computed as HashToG1(dst="gnark-apk-proofs", msg="apk-seed"). + * The circuit hardcodes this same constant; the contract adds it to the + * caller-supplied APK before passing to the PLONK verifier. + */ + bytes32 constant SEED_0 = 0x054abdb6c5522fe2f71d55922d6f674a4908d39e2b33efcc62520c0621ca0d6a; + bytes32 constant SEED_1 = 0x6d84ee717b7fb1cb5f46687265be01ce06e518322165fd114cdf6b4ab59eb45e; + bytes32 constant SEED_2 = 0x9289cc4f6f7948d6b680cef9ecc0e0e0f96bd59a578d58c33c0e10db9c25b5ad; + + /// BLS signature verification constants + /// + /// BLS12-381 scalar field order r. + uint256 private constant R_MOD = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001; + + /// BLS12-381 G1 generator in EIP-2537 padded format (hi/lo word pairs). + uint256 private constant G1_GEN_X_HI = 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f; + uint256 private constant G1_GEN_X_LO = 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb; + uint256 private constant G1_GEN_Y_HI = 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4; + uint256 private constant G1_GEN_Y_LO = 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1; + + /** + * Negated BLS12-381 G2 generator (-g₂) in EIP-2537 padded format. + * X coordinates are unchanged; Y coordinates are negated (p − Y.c0, p − Y.c1). + * Using -g₂ lets us write the pairing check as: + * e(asig + t·apk₁, -g₂) · e(H(m) + t·g₁, apk₂) = 1 + * avoiding a runtime G1 negation. + */ + uint256 private constant NEG_G2_GEN_X_0_HI = 3045985886519456750490515843806728273; + uint256 private constant NEG_G2_GEN_X_0_LO = + 89961632905173714226157479458612185649920463576279427516307505038263245192632; + uint256 private constant NEG_G2_GEN_X_1_HI = 26419286191256893424348605754143887205; + uint256 private constant NEG_G2_GEN_X_1_LO = + 40446337346877272185227670183527379362551741423616556919902061939448715946878; + uint256 private constant NEG_G2_GEN_Y_0_HI = 17421388336814597573762763446246275004; + uint256 private constant NEG_G2_GEN_Y_0_LO = + 82535940630695547964844822885348920226556672312706312698172214783216175252138; + uint256 private constant NEG_G2_GEN_Y_1_HI = 26554973746327433462396120515077546301; + uint256 private constant NEG_G2_GEN_Y_1_LO = + 69304817850384178235384652711014277219752988873539414788182467642510429663469; + + /** + * w3f/bls cipher suite prefix for message signing, 43 bytes split 32+11. The first 32 bytes + * are common to both schemes; only the trailing tag differs: + * + * "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_" proof of possession + * "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_" basic + * + * The suite is part of the signed preimage, so a verifier has to use the same one the signer + * did. Signing with the basic scheme and verifying with PoP yields a well formed but different + * point, and the pairing simply returns false with nothing to explain why. `w3f_bls` exposes + * both, `Message::new` for basic and `Message::new_assuming_pop` for PoP. Polkadot signs with + * the basic scheme, which is what this hashes with. + */ + uint256 private constant CIPHER_SUITE_FIRST_32 = + 0x424c535f5349475f424c53313233383147315f584d443a5348412d3235365f53; + uint256 private constant CIPHER_SUITE_LAST_11 = 0x5357555f524f5f4e554c5f; + + /// BLS12-381 base field modulus p, split for mstore (32 + 16 bytes). + /// p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab + uint256 private constant BLS_P_FIRST_32 = + 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f624; + uint256 private constant BLS_P_LAST_16 = 0x1eabfffeb153ffffb9feffffffffaaab; + + /** + * @param _verifier The PLONK verifier for the APK circuit. + */ + constructor(address _verifier) { + _plonk = PlonkVerifier(_verifier); + } + + /** + * @notice Verify both APK aggregation proof and aggregate BLS signature in one call. + * @param publicKeysCommitment Poseidon2 hash commitment over all 1024 validator public + * keys: Poseidon2(pk_0.X.limbs || ... || pk_1023.Y.limbs), each Fp coordinate + * decomposed into 6 x 64-bit little-endian limbs (12 limbs per G1 point). + * @param bitlist Participating-validator bitlist (5 field elements); bitlist[0..3] + * encode 250 bits each, bitlist[4] encodes 24 bits. + * @param apk Aggregate public key of participants apk = sum(b_i * pk_i) ∈ G1, + * bytes32[3] (X ‖ Y, 96 bytes). The circuit expects seed + apk; the contract adds + * the seed automatically. + * @param apkProof The serialized PLONK proof bytes. + * @param message H(m) ∈ G1, bytes32[3] (96 bytes). + * @param signature Aggregate signature ∈ G1, bytes32[3] (96 bytes). + * @param apk2 Aggregate public key ∈ G2, bytes32[6] (192 bytes). + */ + function verify( + uint256 publicKeysCommitment, + uint256[5] calldata bitlist, + bytes32[3] calldata apk, + bytes calldata apkProof, + bytes32[3] calldata message, + bytes32[3] calldata signature, + bytes32[6] calldata apk2 + ) external view { + // Verify APK aggregation proof + uint256[18] memory encoded = _encodePublicInputs(publicKeysCommitment, bitlist, apk); + if (!_plonk.Verify(apkProof, encoded)) revert PlonkVerificationFailed(); + + // Verify BLS aggregate signature using the supplied apk + if (!_verifyBls(apk, message, signature, apk2)) revert SignatureVerificationFailed(); + } + + /** + * @notice Hash a message to a BLS12-381 G1 point (w3f/bls compatible). + * Implements hash_to_curve (RFC 9380) with expand_message_xmd (SHA-256), + * DST = 0x01, and the cipher suite prefix + * "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_" prepended internally. + * @param message The raw message to hash. + * @return result Uncompressed G1 point as bytes32[3] (X ‖ Y, 96 bytes). + */ + function hashToG1(bytes memory message) public view returns (bytes32[3] memory result) { + // Immutables are not readable from assembly, so bind it first. + uint256 suiteTail = CIPHER_SUITE_LAST_11; + assembly { + let ptr := mload(0x40) + let sha2 := 0x02 + let msgLen := mload(message) + + // ═══════════════════════════════════════════════════════════ + // Phase 1: expand_message_xmd (SHA-256, DST=0x01, 128-byte output) + // DST_prime = 0x01 ‖ 0x01 (DST ‖ I2OSP(1,1)) + // uniform_bytes = b1 ‖ b2 ‖ b3 ‖ b4 + // ═══════════════════════════════════════════════════════════ + + // msg_prime = Z_pad(64) ‖ cipher_suite(43) ‖ msg ‖ I2OSP(128,2) ‖ I2OSP(0,1) ‖ DST_prime + mstore(ptr, 0) // Z_pad[0..31] + mstore(add(ptr, 0x20), 0) // Z_pad[32..63] + mstore(add(ptr, 0x40), CIPHER_SUITE_FIRST_32) // cipher[0..31] + mstore(add(ptr, 0x60), shl(168, suiteTail)) // cipher[32..42] + mcopy(add(ptr, 0x6B), add(message, 0x20), msgLen) // message + let pos := add(add(ptr, 0x6B), msgLen) + mstore8(pos, 0x00) // I2OSP(128,2) high + mstore8(add(pos, 1), 0x80) // I2OSP(128,2) low + mstore8(add(pos, 2), 0x00) // I2OSP(0,1) + mstore8(add(pos, 3), 0x01) // DST[0] + mstore8(add(pos, 4), 0x01) // I2OSP(1,1) + let msgPrimeLen := add(0x70, msgLen) // 64+43+msg+3+2 = 112+msg + + // b0 = SHA256(msg_prime) — output just past msg_prime + let b0Out := add(ptr, msgPrimeLen) + if iszero(staticcall(gas(), sha2, ptr, msgPrimeLen, b0Out, 0x20)) { revert(0, 0) } + let b0 := mload(b0Out) + + let hashOut := add(ptr, 0x200) + + // b1 = SHA256(b0 ‖ 0x01 ‖ DST_prime) + // DST_prime = 0x01 0x01 → biHashLen = 32+1+2 = 35 = 0x23 + mstore(ptr, b0) + mstore8(add(ptr, 0x20), 0x01) + mstore8(add(ptr, 0x21), 0x01) // DST[0] + mstore8(add(ptr, 0x22), 0x01) // I2OSP(1,1) + if iszero(staticcall(gas(), sha2, ptr, 0x23, hashOut, 0x20)) { revert(0, 0) } + let b1 := mload(hashOut) + + // b2 = SHA256((b0 ⊕ b1) ‖ 0x02 ‖ DST_prime) + mstore(ptr, xor(b0, b1)) + mstore8(add(ptr, 0x20), 0x02) + if iszero(staticcall(gas(), sha2, ptr, 0x23, hashOut, 0x20)) { revert(0, 0) } + let b2 := mload(hashOut) + + // b3 = SHA256((b0 ⊕ b2) ‖ 0x03 ‖ DST_prime) + mstore(ptr, xor(b0, b2)) + mstore8(add(ptr, 0x20), 0x03) + if iszero(staticcall(gas(), sha2, ptr, 0x23, hashOut, 0x20)) { revert(0, 0) } + let b3 := mload(hashOut) + + // b4 = SHA256((b0 ⊕ b3) ‖ 0x04 ‖ DST_prime) + mstore(ptr, xor(b0, b3)) + mstore8(add(ptr, 0x20), 0x04) + if iszero(staticcall(gas(), sha2, ptr, 0x23, hashOut, 0x20)) { revert(0, 0) } + let b4 := mload(hashOut) + + // ═══════════════════════════════════════════════════════════ + // Phase 2: reduce to field elements via MODEXP (x^1 mod p) + // u0 = (b1‖b2) mod p, u1 = (b3‖b4) mod p + // ═══════════════════════════════════════════════════════════ + + // MODEXP input: Bsize(32) ‖ Esize(32) ‖ Msize(32) ‖ base(64) ‖ exp(1) ‖ mod(48) + mstore(ptr, 64) // Bsize + mstore(add(ptr, 0x20), 1) // Esize + mstore(add(ptr, 0x40), 48) // Msize + mstore(add(ptr, 0x60), b1) // base high + mstore(add(ptr, 0x80), b2) // base low + mstore8(add(ptr, 0xA0), 0x01) // exp = 1 + mstore(add(ptr, 0xA1), BLS_P_FIRST_32) // mod[0..31] + mstore(add(ptr, 0xC1), shl(128, BLS_P_LAST_16)) // mod[32..47] + + // Zero MAP_FP_TO_G1 padding (16 bytes at ptr+0x200) + mstore(add(ptr, 0x200), 0) + + // u0 = (b1‖b2) mod p → ptr+0x210 (48 bytes, forming MAP input at ptr+0x200) + if iszero(staticcall(gas(), PRECOMPILE_MODEXP, ptr, 0xD1, add(ptr, 0x210), 48)) { + revert(0, 0) + } + + // MAP_FP_TO_G1(u0) → Q0 at ptr+0x300 (128 bytes) + if iszero(staticcall(gas(), PRECOMPILE_BLS12_MAP_FP_TO_G1, add(ptr, 0x200), 64, add(ptr, 0x300), 128)) { + revert(0, 0) + } + + // u1 = (b3‖b4) mod p + mstore(add(ptr, 0x60), b3) + mstore(add(ptr, 0x80), b4) + if iszero(staticcall(gas(), PRECOMPILE_MODEXP, ptr, 0xD1, add(ptr, 0x210), 48)) { + revert(0, 0) + } + + // MAP_FP_TO_G1(u1) → Q1 at ptr+0x380 (128 bytes) + if iszero(staticcall(gas(), PRECOMPILE_BLS12_MAP_FP_TO_G1, add(ptr, 0x200), 64, add(ptr, 0x380), 128)) { + revert(0, 0) + } + + // ═══════════════════════════════════════════════════════════ + // Phase 3: G1ADD(Q0, Q1) → H(m) + // ═══════════════════════════════════════════════════════════ + + // Q0‖Q1 contiguous at ptr+0x300 (256 bytes) + if iszero(staticcall(gas(), PRECOMPILE_BLS12_G1ADD, add(ptr, 0x300), 256, add(ptr, 0x300), 128)) { + revert(0, 0) + } + + // Extract raw G1 (96 bytes) from padded format (128 bytes) + // X at ptr+0x310 (48 bytes), Y at ptr+0x350 (48 bytes) + mcopy(result, add(ptr, 0x310), 48) + mcopy(add(result, 48), add(ptr, 0x350), 48) + } + } + + /** + * @dev Derive challenge t and verify the BLS pairing check — all in assembly. + * + * Phase 1 — expand_message_xmd (SHA-256, empty DST, 48-byte output): + * msg_prime = Z_pad(64) ‖ message(96) ‖ sig(96) ‖ apk1(96) ‖ apk2(192) ‖ 0x00300000 + * b0 = SHA256(msg_prime), b1 = SHA256(b0‖0x0100), b2 = SHA256((b0⊕b1)‖0x0200) + * t = (b1·2¹²⁸ + b2>>128) mod r + * + * Phase 2 — pairing check: + * e(sig + t·apk₁, -g₂) · e(msg + t·g₁, apk₂) = 1 + */ + function _verifyBls( + bytes32[3] calldata apk1, + bytes32[3] calldata message, + bytes32[3] calldata signature, + bytes32[6] calldata apk2 + ) internal view returns (bool result) { + assembly { + let ptr := mload(0x40) + let sha2 := 0x02 + + // ═══════════════════════════════════════════════════════════ + // Phase 1: derive challenge t + // ═══════════════════════════════════════════════════════════ + // Build msg_prime at ptr (548 = 0x224 bytes) + mstore(ptr, 0) // Z_pad[0..31] + mstore(add(ptr, 0x20), 0) // Z_pad[32..63] + calldatacopy(add(ptr, 0x40), message, 96) // message + calldatacopy(add(ptr, 0xA0), signature, 96) // signature + calldatacopy(add(ptr, 0x100), apk1, 96) // apk1 + calldatacopy(add(ptr, 0x160), apk2, 192) // apk2 + mstore8(add(ptr, 0x220), 0x00) // I2OSP(48,2) high + mstore8(add(ptr, 0x221), 0x30) // I2OSP(48,2) low + mstore8(add(ptr, 0x222), 0x00) // I2OSP(0,1) + mstore8(add(ptr, 0x223), 0x00) // DST_prime + + // b0 = SHA256(msg_prime) + let hashOut := add(ptr, 0x300) + if iszero(staticcall(gas(), sha2, ptr, 0x224, hashOut, 0x20)) { revert(0, 0) } + let b0 := mload(hashOut) + + // b1 = SHA256(b0 ‖ 0x01 ‖ 0x00) + mstore(ptr, b0) + mstore8(add(ptr, 0x20), 0x01) + mstore8(add(ptr, 0x21), 0x00) + if iszero(staticcall(gas(), sha2, ptr, 0x22, hashOut, 0x20)) { revert(0, 0) } + let b1 := mload(hashOut) + + // b2 = SHA256((b0 ⊕ b1) ‖ 0x02 ‖ 0x00) + mstore(ptr, xor(b0, b1)) + mstore8(add(ptr, 0x20), 0x02) + mstore8(add(ptr, 0x21), 0x00) + if iszero(staticcall(gas(), sha2, ptr, 0x22, hashOut, 0x20)) { revert(0, 0) } + let b2 := mload(hashOut) + + // t = (b1 × 2¹²⁸ + b2>>128) mod r + let t := addmod( + mulmod(b1, 0x100000000000000000000000000000000, R_MOD), + shr(128, b2), + R_MOD + ) + + // ═══════════════════════════════════════════════════════════ + // Phase 2: pairing check + // ═══════════════════════════════════════════════════════════ + + // Write -g₂ into pairing buffer [0x080..0x17F] + mstore(add(ptr, 0x080), NEG_G2_GEN_X_0_HI) + mstore(add(ptr, 0x0A0), NEG_G2_GEN_X_0_LO) + mstore(add(ptr, 0x0C0), NEG_G2_GEN_X_1_HI) + mstore(add(ptr, 0x0E0), NEG_G2_GEN_X_1_LO) + mstore(add(ptr, 0x100), NEG_G2_GEN_Y_0_HI) + mstore(add(ptr, 0x120), NEG_G2_GEN_Y_0_LO) + mstore(add(ptr, 0x140), NEG_G2_GEN_Y_1_HI) + mstore(add(ptr, 0x160), NEG_G2_GEN_Y_1_LO) + + // Pad apk₂ into pairing buffer [0x200..0x2FF] + let apk2Off := apk2 + mstore(add(ptr, 0x200), 0) + calldatacopy(add(ptr, 0x210), apk2Off, 48) + mstore(add(ptr, 0x240), 0) + calldatacopy(add(ptr, 0x250), add(apk2Off, 48), 48) + mstore(add(ptr, 0x280), 0) + calldatacopy(add(ptr, 0x290), add(apk2Off, 96), 48) + mstore(add(ptr, 0x2C0), 0) + calldatacopy(add(ptr, 0x2D0), add(apk2Off, 144), 48) + + // G1MSM(apk₁, t) → t·apk₁ + let msmIn := add(ptr, 0x300) + mstore(msmIn, 0) + calldatacopy(add(msmIn, 0x10), apk1, 48) + mstore(add(msmIn, 0x40), 0) + calldatacopy(add(msmIn, 0x50), add(apk1, 48), 48) + mstore(add(msmIn, 0x80), t) + + let msmOut := add(ptr, 0x3A0) + if iszero(staticcall(gas(), PRECOMPILE_BLS12_G1MSM, msmIn, 0xA0, msmOut, 0x80)) { + revert(0, 0) + } + + // G1ADD(signature, t·apk₁) → lhs at pairing [0x000] + let addIn := add(ptr, 0x420) + mstore(addIn, 0) + calldatacopy(add(addIn, 0x10), signature, 48) + mstore(add(addIn, 0x40), 0) + calldatacopy(add(addIn, 0x50), add(signature, 48), 48) + mcopy(add(addIn, 0x80), msmOut, 0x80) + + if iszero(staticcall(gas(), PRECOMPILE_BLS12_G1ADD, addIn, 0x100, ptr, 0x80)) { + revert(0, 0) + } + + // G1MSM(g₁, t) → t·g₁ + mstore(msmIn, G1_GEN_X_HI) + mstore(add(msmIn, 0x20), G1_GEN_X_LO) + mstore(add(msmIn, 0x40), G1_GEN_Y_HI) + mstore(add(msmIn, 0x60), G1_GEN_Y_LO) + mstore(add(msmIn, 0x80), t) + + if iszero(staticcall(gas(), PRECOMPILE_BLS12_G1MSM, msmIn, 0xA0, msmOut, 0x80)) { + revert(0, 0) + } + + // G1ADD(message, t·g₁) → rhs at pairing [0x180] + mstore(addIn, 0) + calldatacopy(add(addIn, 0x10), message, 48) + mstore(add(addIn, 0x40), 0) + calldatacopy(add(addIn, 0x50), add(message, 48), 48) + mcopy(add(addIn, 0x80), msmOut, 0x80) + + if iszero(staticcall(gas(), PRECOMPILE_BLS12_G1ADD, addIn, 0x100, add(ptr, 0x180), 0x80)) { + revert(0, 0) + } + + // Pairing check — e(lhs, -g₂) · e(rhs, apk₂) = 1 + if iszero(staticcall(gas(), PRECOMPILE_BLS12_PAIRING, ptr, 0x300, add(ptr, 0x300), 0x20)) { + revert(0, 0) + } + + result := mload(add(ptr, 0x300)) + } + } + + /** + * @dev Encode the public inputs into the flat uint256[18] format + * expected by the gnark verifier. + * + * Verifier expects: out[0..4]=bitlist, out[5]=commitment, out[6..17]=apk limbs + */ + function _encodePublicInputs(uint256 publicKeysCommitment, uint256[5] calldata bitlist, bytes32[3] calldata apk) + internal + view + returns (uint256[18] memory out) + { + bytes32 s0 = SEED_0; + bytes32 s1 = SEED_1; + bytes32 s2 = SEED_2; + + assembly { + let mask := 0xFFFFFFFFFFFFFFFF + + // --- Copy bitlist and commitment into out[0..5] --- + calldatacopy(out, bitlist, 160) // bitlist (5*32) → out[0..4] + mstore(add(out, 160), publicKeysCommitment) // commitment → out[5] + + /* + * Build G1ADD input in scratch memory at out + 576. + * out occupies 18*32 = 576 bytes; we use the space after it as scratch. + * + * G1ADD input layout (256 bytes, two padded G1 points): + * [0..127] = seed point (padded EIP-2537 format) + * [128..255] = apk point (padded EIP-2537 format) + */ + let scratch := add(out, 576) + + // Zero the 256-byte G1ADD input region + mstore(scratch, 0) + mstore(add(scratch, 32), 0) + mstore(add(scratch, 64), 0) + mstore(add(scratch, 96), 0) + mstore(add(scratch, 128), 0) + mstore(add(scratch, 160), 0) + mstore(add(scratch, 192), 0) + mstore(add(scratch, 224), 0) + + /* + * Seed point in padded EIP-2537 format (128 bytes): + * [0..15] = zero padding + * [16..47] = s0 (seed X high 32 bytes) + * [48..63] = s1[0:16] (seed X low 16 bytes) + * [64..79] = zero padding between X and Y + * [80..95] = s1[16:32](seed Y high 16 bytes) + * [96..127] = s2 (seed Y low 32 bytes) + */ + mstore(add(scratch, 16), s0) + mstore(add(scratch, 48), s1) + mstore(add(scratch, 64), 0) // zero-pad between X and Y + mstore(add(scratch, 80), shl(128, s1)) + mstore(add(scratch, 96), s2) + + // APK point from calldata (padded) + let apkOff := apk + calldatacopy(add(scratch, 144), apkOff, 48) // APK X + calldatacopy(add(scratch, 208), add(apkOff, 48), 48) // APK Y + + // --- Call G1ADD precompile, output to scratch+256 (128 bytes) --- + let res := add(scratch, 256) + let ok := staticcall(gas(), PRECOMPILE_BLS12_G1ADD, scratch, 256, res, 128) + if iszero(ok) { + mstore(0, 0x55d4cbf9) // G1AddFailed() + revert(28, 4) + } + + /* + * Decompose padded G1 result into 12 x 64-bit limbs → out[6..17]. + * Result layout: [16 zero | X 48 bytes | 16 zero | Y 48 bytes] + * X coordinate: hi at res+16, lo at res+32 + * Y coordinate: hi at res+80, lo at res+96 + */ + let xHi := mload(add(res, 16)) + let xLo := mload(add(res, 32)) + mstore(add(out, 192), and(xLo, mask)) // out[6] + mstore(add(out, 224), and(shr(64, xLo), mask)) // out[7] + mstore(add(out, 256), and(shr(128, xLo), mask)) // out[8] + mstore(add(out, 288), and(shr(192, xLo), mask)) // out[9] + mstore(add(out, 320), and(shr(128, xHi), mask)) // out[10] + mstore(add(out, 352), shr(192, xHi)) // out[11] + + let yHi := mload(add(res, 80)) + let yLo := mload(add(res, 96)) + mstore(add(out, 384), and(yLo, mask)) // out[12] + mstore(add(out, 416), and(shr(64, yLo), mask)) // out[13] + mstore(add(out, 448), and(shr(128, yLo), mask)) // out[14] + mstore(add(out, 480), and(shr(192, yLo), mask)) // out[15] + mstore(add(out, 512), and(shr(128, yHi), mask)) // out[16] + mstore(add(out, 544), shr(192, yHi)) // out[17] + } + } +} From c2580cc2e22b64914e84d0100e7b96403432efaf Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Sun, 16 Aug 2026 18:47:48 +0100 Subject: [PATCH 26/48] take the apk contract reading its suite constant directly --- evm/tests/foundry/vendor/ApkProof.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/evm/tests/foundry/vendor/ApkProof.sol b/evm/tests/foundry/vendor/ApkProof.sol index e324768c1..b7042fb86 100644 --- a/evm/tests/foundry/vendor/ApkProof.sol +++ b/evm/tests/foundry/vendor/ApkProof.sol @@ -176,8 +176,6 @@ contract ApkProof { * @return result Uncompressed G1 point as bytes32[3] (X ‖ Y, 96 bytes). */ function hashToG1(bytes memory message) public view returns (bytes32[3] memory result) { - // Immutables are not readable from assembly, so bind it first. - uint256 suiteTail = CIPHER_SUITE_LAST_11; assembly { let ptr := mload(0x40) let sha2 := 0x02 @@ -193,7 +191,7 @@ contract ApkProof { mstore(ptr, 0) // Z_pad[0..31] mstore(add(ptr, 0x20), 0) // Z_pad[32..63] mstore(add(ptr, 0x40), CIPHER_SUITE_FIRST_32) // cipher[0..31] - mstore(add(ptr, 0x60), shl(168, suiteTail)) // cipher[32..42] + mstore(add(ptr, 0x60), shl(168, CIPHER_SUITE_LAST_11)) // cipher[32..42] mcopy(add(ptr, 0x6B), add(message, 0x20), msgLen) // message let pos := add(add(ptr, 0x6B), msgLen) mstore8(pos, 0x00) // I2OSP(128,2) high From c6fd85880e0a342590c223c279ff59250082eeee Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Sun, 16 Aug 2026 19:19:07 +0100 Subject: [PATCH 27/48] pack three limbs into each field element the way the circuit now absorbs them --- .../consensus/beefy/apk-commitment/src/lib.rs | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/modules/consensus/beefy/apk-commitment/src/lib.rs b/modules/consensus/beefy/apk-commitment/src/lib.rs index decf7badb..b330cdbb6 100644 --- a/modules/consensus/beefy/apk-commitment/src/lib.rs +++ b/modules/consensus/beefy/apk-commitment/src/lib.rs @@ -124,16 +124,33 @@ fn compress(left: Fr, right: Fr, rk: &[Vec]) -> Fr { right + s[1] } -/// Decompose a coordinate into six little-endian 64-bit limbs, one `Fr` each, matching gnark's -/// emulated `BLS12381Fp` limb layout. +/// Number of 64-bit limbs packed into one `Fr`. Must match `apk.LimbsPerElement` in the circuit. +const LIMBS_PER_ELEMENT: usize = 3; + +/// Decompose a coordinate into six little-endian 64-bit limbs, matching gnark's emulated +/// `BLS12381Fp` layout, and pack them [`LIMBS_PER_ELEMENT`] at a time into `Fr` as +/// `l[0] + l[1]*2^64 + l[2]*2^128`, least significant limb first. +/// +/// Three limbs span at most 192 bits, comfortably inside `Fr`, so the packing never wraps and +/// stays injective. It binds exactly as tightly as absorbing each limb on its own, at a third of +/// the compressions. #[inline] -fn coord_limbs(c: Fq) -> [Fr; 6] { +fn coord_packed(c: Fq) -> [Fr; 2] { let limbs = c.into_bigint().0; - core::array::from_fn(|i| Fr::from(limbs[i])) + core::array::from_fn(|i| { + // The positional sum written out as big endian bytes, most significant limb first. + let mut be = [0u8; 8 * LIMBS_PER_ELEMENT]; + for j in 0..LIMBS_PER_ELEMENT { + let start = 8 * (LIMBS_PER_ELEMENT - 1 - j); + be[start..start + 8].copy_from_slice(&limbs[i * LIMBS_PER_ELEMENT + j].to_be_bytes()); + } + Fr::from_be_bytes_mod_order(&be) + }) } -/// The Poseidon2 commitment over `points`, in the circuit's absorption order: per point, the six -/// limbs of `X` then the six of `Y`, absorbed through a Merkle-Damgard chain with a zero IV. +/// The Poseidon2 commitment over `points`, in the circuit's absorption order: per point, the two +/// packed halves of `X` then the two of `Y`, absorbed through a Merkle-Damgard chain with a zero +/// IV. /// /// The caller supplies the same list the circuit binds to, which for a validator set means /// registration order padded to [`NUM_VALIDATORS`] with the identity point. @@ -141,8 +158,8 @@ pub fn public_keys_commitment(points: &[G1Affine]) -> Fr { let rk = round_keys(); let mut state = Fr::zero(); for p in points { - let x = coord_limbs(p.x); - let y = coord_limbs(p.y); + let x = coord_packed(p.x); + let y = coord_packed(p.y); for block in x.into_iter().chain(y) { state = compress(state, block, &rk); } @@ -197,8 +214,8 @@ impl PartialCommitment { pub fn absorb(&mut self, points: &[G1Affine]) { let rk = round_keys(); for p in points { - let x = coord_limbs(p.x); - let y = coord_limbs(p.y); + let x = coord_packed(p.x); + let y = coord_packed(p.y); for block in x.into_iter().chain(y) { self.state = compress(self.state, block, &rk); } @@ -234,10 +251,10 @@ mod tests { /// digest over `k * G1::generator()` point sets. If gnark-crypto's parameters ever change /// these move, and so does every commitment. const VECTORS: [(usize, &str); 4] = [ - (1, "3b14900f1cd55f300914ca5b4393f0fa6a777d5999963f9520b12a60204272e2"), - (2, "528fad7e07c1ec6db4ad009230329123e643e1629733d60d2b4eaa9e45dc5704"), - (3, "14bac0391b3646f28d9b0b6b64acca1c8c585ade555494ce189aa2e4b62e9977"), - (10, "4a401453041545fc28ebf4c3c2824f317d1c4a7b6bff644d6eb12d0edd1f64c5"), + (1, "4df3ca8a29f6b37c04fefb167022ae638df17383caf668b718bf3b65aa320652"), + (2, "20b814b4a4cd0249ffee16a12c0e883eac49a18e91f104e0c777d7de9a797267"), + (3, "1d8d8ce5d1437ebe81c7a10c59d25ec6f53bffb9966d019f460157750a7a1cff"), + (10, "5f9529f2a793ad64450341a6ef732dc1e1b71ddcca7d83f3704ff5e637a4b3bd"), ]; fn k_times_generator(n: usize) -> Vec { From b34f16d2d809dba546570ea3b7d8de68794bba40 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Mon, 17 Aug 2026 13:17:31 +0100 Subject: [PATCH 28/48] commit to a validator set in one block and republish an unchanged one --- evm/src/consensus/BlsAggregate.sol | 117 ------- evm/tests/foundry/BlsAggregate.t.sol | 108 ------ modules/pallets/beefy-apk-digest/Cargo.toml | 7 + .../beefy-apk-digest/src/benchmarking.rs | 69 ++++ modules/pallets/beefy-apk-digest/src/lib.rs | 328 +++++------------- parachain/runtimes/gargantua/Cargo.toml | 1 + parachain/runtimes/gargantua/src/ismp.rs | 4 + parachain/runtimes/gargantua/src/lib.rs | 12 +- .../runtimes/gargantua/src/weights/mod.rs | 1 + .../src/weights/pallet_beefy_apk_digest.rs | 48 +++ 10 files changed, 225 insertions(+), 470 deletions(-) delete mode 100644 evm/src/consensus/BlsAggregate.sol delete mode 100644 evm/tests/foundry/BlsAggregate.t.sol create mode 100644 modules/pallets/beefy-apk-digest/src/benchmarking.rs create mode 100644 parachain/runtimes/gargantua/src/weights/pallet_beefy_apk_digest.rs diff --git a/evm/src/consensus/BlsAggregate.sol b/evm/src/consensus/BlsAggregate.sol deleted file mode 100644 index dcb6ce422..000000000 --- a/evm/src/consensus/BlsAggregate.sol +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) Polytope Labs Ltd. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.30; - -import {BlsHashToCurve} from "./BlsHashToCurve.sol"; - -/** - * @title Aggregate BLS12-381 signature verification for BEEFY. - * @author Polytope Labs (hello@polytope.technology) - * - * @notice Checks that a supermajority of the validator set signed a commitment, in a single - * pairing operation regardless of how many of them there are. This is the point of the BLS path: - * the ECDSA verifier spends one `ecrecover` per signature, so its cost grows with the set, while - * this does not. - * - * @dev The identity checked is the standard aggregate: - * - * e(sum(sig_i), g2_generator) == e(H(commitment), sum(pubkey_i)) - * - * `PAIRING_CHECK` tests whether a product of pairings equals one, so it is rearranged as - * - * e(sum(sig_i), -g2_generator) * e(H(commitment), sum(pubkey_i)) == 1 - * - * which is why the negated generator is a constant here. - * - * Signatures live in G1 and public keys in G2, the opposite of the Ethereum convention. - * - * Points are taken **uncompressed**. EIP-2537 has no decompression precompile, and recovering a - * G2 point from its compressed form needs an Fp2 square root, which is expensive in Solidity, so - * the prover supplies uncompressed coordinates. - */ -library BlsAggregate { - /// @dev EIP-2537 BLS12_G2ADD - address internal constant G2_ADD = address(0x0d); - /// @dev EIP-2537 BLS12_PAIRING_CHECK - address internal constant PAIRING_CHECK = address(0x0f); - - /// @dev A G1 point: x || y, each a 64 byte field element. - uint256 internal constant G1_POINT_LEN = 128; - /// @dev A G2 point: x.c0 || x.c1 || y.c0 || y.c1, each a 64 byte field element. - uint256 internal constant G2_POINT_LEN = 256; - - /** - * @dev The negated BLS12-381 G2 generator, uncompressed, as EIP-2537 encodes it. Negation on - * this curve is `y -> p - y`, applied to both Fp2 coefficients; x is unchanged. - */ - bytes internal constant NEG_G2_GENERATOR = hex"00000000000000000000000000000000" - hex"024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8" - hex"00000000000000000000000000000000" - hex"13e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e" - hex"00000000000000000000000000000000" - hex"0d1b3cc2c7027888be51d9ef691d77bcb679afda66c73f17f9ee3837a55024f78c71363275a75d75d86bab79f74782aa" - hex"00000000000000000000000000000000" - hex"13fa4d4a0ad8b1ce186ed5061789213d993923066dddaf1040bc3ff59f825c78df74f2d75467e25e0f55f8a00fa030ed"; - - error G2AddFailed(); - error PairingFailed(); - error InvalidPointLength(); - error NoSigners(); - - /** - * @notice Verify that `aggregateSignature` is the sum of signatures over `commitment` by the - * holders of `publicKeys`. - * @param commitment the SCALE-encoded BEEFY commitment, hashed onto G1 internally - * @param aggregateSignature a G1 point, 128 bytes uncompressed - * @param publicKeys the signers' G2 points, 256 bytes uncompressed each - */ - function verify(bytes memory commitment, bytes memory aggregateSignature, bytes[] memory publicKeys) - internal - view - returns (bool) - { - if (publicKeys.length == 0) revert NoSigners(); - if (aggregateSignature.length != G1_POINT_LEN) revert InvalidPointLength(); - - bytes memory aggregateKey = sumG2(publicKeys); - bytes memory messagePoint = BlsHashToCurve.hashCommitmentToG1(commitment); - - // Two pairs: (sig, -g2_gen) and (H(msg), aggregate key). Their product is one exactly when - // the aggregate signature is valid for the aggregate key. - bytes memory input = bytes.concat(aggregateSignature, NEG_G2_GENERATOR, messagePoint, aggregateKey); - - (bool ok, bytes memory result) = PAIRING_CHECK.staticcall(input); - if (!ok || result.length != 32) revert PairingFailed(); - - return abi.decode(result, (uint256)) == 1; - } - - /// @notice Sum a set of uncompressed G2 points with `G2ADD`. - function sumG2(bytes[] memory points) internal view returns (bytes memory) { - if (points.length == 0) revert NoSigners(); - if (points[0].length != G2_POINT_LEN) revert InvalidPointLength(); - - bytes memory acc = points[0]; - for (uint256 i = 1; i < points.length; ++i) { - if (points[i].length != G2_POINT_LEN) revert InvalidPointLength(); - - (bool ok, bytes memory sum) = G2_ADD.staticcall(bytes.concat(acc, points[i])); - if (!ok || sum.length != G2_POINT_LEN) revert G2AddFailed(); - acc = sum; - } - - return acc; - } -} diff --git a/evm/tests/foundry/BlsAggregate.t.sol b/evm/tests/foundry/BlsAggregate.t.sol deleted file mode 100644 index 62128ef22..000000000 --- a/evm/tests/foundry/BlsAggregate.t.sol +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.30; - -import {Test} from "forge-std/Test.sol"; -import {BlsAggregate} from "../../src/consensus/BlsAggregate.sol"; - -/** - * @title Cross-language check of the aggregate BLS pairing. - * - * @notice Every value here comes from `bls_eip2537_fixture` in the Rust verifier, generated with - * the same `w3f-bls` code path substrate's BEEFY signers use. That test asserts the aggregate - * verifies in Rust before emitting anything, so a disagreement here is unambiguously a fault on - * the EVM side. - * - * Needs EIP-2537: - * FOUNDRY_PROFILE=bls forge test --match-contract BlsAggregateTest -vv - */ -contract BlsAggregateTest is Test { - bytes constant MESSAGE = "beefy-bls-aggregate-fixture"; - - /// Aggregate signature over MESSAGE by all three signers, G1. - bytes constant SIG_X = - hex"16d4d9d336cc264d0f9ddc9086bb5ce441d98896b441253e4d2087aa0fb93a21e44d074d9ac98f525aac914608b001c0"; - bytes constant SIG_Y = - hex"011ae59b33a10da832193b8ec84bb52958d92c8acf571adad597856db4d40dd5455e1c8a5a59ae1af2afee7e8b36bef6"; - - /// The sum of the three public keys, G2. `sumG2` must reproduce this. - bytes constant AGG_X_C0 = - hex"05d4a0f9c007ba1cbe0d2b79006d52f8fdb417cb17e0b4a37740b72b2459ec4d030be125b47692d9f8bbd9e937291831"; - bytes constant AGG_X_C1 = - hex"120a8de45bee1aab433b63d9859f40b560f0b923ecc23bbd2bf41e90290c4ba97b31137e70ab3e18a0e3616c340cb584"; - bytes constant AGG_Y_C0 = - hex"0e5e2bce96aa5ae0b57f3da3add49b35c60ecc276eec38940a5a0cad2a7ddf5555e954bce53d5b2c34b2b4824742e60d"; - bytes constant AGG_Y_C1 = - hex"0530f71228f36639e9d197e48a3d27697d71acca411533eef32d0d602e4d8fd72f52edfff40ada6b634797bfa3b1a19f"; - - /// Build the 64 byte field element EIP-2537 expects: 16 zero bytes then the 48 byte value. - function _fp(bytes memory value) private pure returns (bytes memory) { - return bytes.concat(new bytes(16), value); - } - - function _g1(bytes memory x, bytes memory y) private pure returns (bytes memory) { - return bytes.concat(_fp(x), _fp(y)); - } - - function _g2(bytes memory xc0, bytes memory xc1, bytes memory yc0, bytes memory yc1) - private - pure - returns (bytes memory) - { - return bytes.concat(_fp(xc0), _fp(xc1), _fp(yc0), _fp(yc1)); - } - - function _signers() private pure returns (bytes[] memory keys) { - keys = new bytes[](3); - keys[0] = _g2( - hex"0eb912203efe065b9d9844025f9a85a43fdb21f4c8c0f31b39cc3bedcdbecce8ab3567f9cadbe965adebb7dd4a081ec1", - hex"14168206974b9223cc95e6e1f279f9d10e526aa172b5bd15b101b6f4997e2038ebcb02bfee1bca54f428162e17ade003", - hex"0dbe0ed3b59dbf3c217e879f885df4fce29af686888e77e984b69d6a07fe90b2a1acebc49d6a196c90a9307be82bb9c4", - hex"16bd04776624eab548fc58aeac9da7f75a618206620c9119d517b983b659ca196c2a0761e09a1ee6df87e1cd78bdd4d5" - ); - keys[1] = _g2( - hex"065a060f78222114141a2544d6207ebfe7784788e6310b3a58cebc36df948e387853ce8492c4c8f9d423ebabc6feb027", - hex"026bc8c3c41fb3bb78a7d97855098d8b32ec546433c4c523f62c471b75d89823e2485ff79f3c203961b62c9b8b08e4d1", - hex"0db39f4b44160911c089c2cf24621f3e1df13561ced1640ddafa4c885d0a80b3428156c709f375e00fdb230b5c109e46", - hex"00b7e2a03248e77666c8e12d4eaa31d451c477209a178134afd53ebcc701842206b2b7613ed4807f4600ce212c6679e8" - ); - keys[2] = _g2( - hex"0c2410d03233711f03a7242fd3a8bb141125ac84a07813d8cce6e366038f129d3ef348fc7dd14682e3db28a920d2c748", - hex"008cd3cbf5e8dd6aca7d5fb78061c360910691c3797bcd95a42cf5a45b0612e8555f6e118788872af47d231954914282", - hex"15c168dc4a702011de9bded446897040c88e51831293bbcc691fe85a7f42e6cb454d4931fe9348fa310f455a21ef47ae", - hex"08600a717fc096a40eeb7dba5194779267123c9fef22e3ebe85c34c1aa74928a89c211d9bfa7dc97a296fc7550acc58e" - ); - } - - /// `G2ADD` over the signers must land on the aggregate key Rust computed. - function test_sum_g2_matches_rust() public view { - bytes memory summed = BlsAggregate.sumG2(_signers()); - assertEq(summed, _g2(AGG_X_C0, AGG_X_C1, AGG_Y_C0, AGG_Y_C1), "aggregate key differs from Rust"); - } - - /// The whole thing: one pairing check over the aggregate. - function test_verify_aggregate() public view { - assertTrue(BlsAggregate.verify(MESSAGE, _g1(SIG_X, SIG_Y), _signers()), "aggregate signature should verify"); - } - - /// A different message must not verify, or the check proves nothing. - function test_rejects_wrong_message() public view { - assertFalse( - BlsAggregate.verify("a different commitment", _g1(SIG_X, SIG_Y), _signers()), - "signature over another message must not verify" - ); - } - - /// Dropping a signer from the key set breaks the aggregate, so a validator cannot be credited - /// with a signature they did not produce. - function test_rejects_missing_signer() public view { - bytes[] memory all = _signers(); - bytes[] memory subset = new bytes[](2); - subset[0] = all[0]; - subset[1] = all[1]; - - assertFalse( - BlsAggregate.verify(MESSAGE, _g1(SIG_X, SIG_Y), subset), - "aggregate must not verify against a subset of the signers" - ); - } -} diff --git a/modules/pallets/beefy-apk-digest/Cargo.toml b/modules/pallets/beefy-apk-digest/Cargo.toml index 0e123cc2d..075a7d002 100644 --- a/modules/pallets/beefy-apk-digest/Cargo.toml +++ b/modules/pallets/beefy-apk-digest/Cargo.toml @@ -19,6 +19,8 @@ cumulus-pallet-parachain-system = { workspace = true, default-features = false } ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = false } ark-serialize = { version = "0.4.0", default-features = false } +hex-literal = { workspace = true, optional = true } + [dependencies.polkadot-sdk] workspace = true features = ["frame-support", "frame-system", "sp-io", "sp-runtime"] @@ -41,3 +43,8 @@ std = [ "ark-serialize/std", ] try-runtime = ["polkadot-sdk/try-runtime"] +runtime-benchmarks = [ + "polkadot-sdk/frame-benchmarking", + "polkadot-sdk/runtime-benchmarks", + "dep:hex-literal", +] diff --git a/modules/pallets/beefy-apk-digest/src/benchmarking.rs b/modules/pallets/beefy-apk-digest/src/benchmarking.rs new file mode 100644 index 000000000..d5c318239 --- /dev/null +++ b/modules/pallets/beefy-apk-digest/src/benchmarking.rs @@ -0,0 +1,69 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Weights for absorbing validator keys into the commitment. +//! +//! The cost is the hashing, and it is linear in the number of slots, so the benchmark sweeps the +//! slot count up to the full circuit width. Reading the relay state proof is not measured: it +//! happens once per block regardless and is the same work the parachain already does to build a +//! block at all. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use apk_commitment::NUM_VALIDATORS; +use frame_benchmarking::v2::*; +use polkadot_sdk::*; + +/// The G1 half of a real BEEFY key from a live BLS relay, so the decompression inside +/// `absorb_slots` does the work a live key costs rather than failing early on a made up point. +const A_KEY: [u8; BLS_G1_SIGNATURE_LEN] = hex_literal::hex!( + "b7235087b611457915f812c4c9af17fe9c590f0a9c9d2f3b62f5d31673a5d5ae02309ea191fe6ebeb66cb3ad23db3b04" +); + +#[benchmarks] +mod benches { + use super::*; + + /// Commit to a full validator set, which is what a block costs when the membership changes. + #[benchmark] + fn commit() { + let keys = alloc::vec![A_KEY; NUM_VALIDATORS]; + + #[block] + { + super::commit(&keys).expect("the keys are well formed"); + } + } + + /// Decompressing a full set and nothing else. + /// + /// A key arrives as 48 compressed bytes and recovering `y` needs a square root in the base + /// field, which is not obviously cheaper or dearer than the hashing it feeds. Measuring it + /// apart from `absorb` says which of the two a saving would have to come from. + #[benchmark] + fn decompress() { + let keys = alloc::vec![A_KEY; NUM_VALIDATORS]; + + #[block] + { + for key in keys.iter() { + let point = G1Affine::deserialize_compressed(&key[..]) + .expect("the key is a well formed point"); + core::hint::black_box(point); + } + } + } +} diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index 1e7b1ab0c..3b7d955fc 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -21,9 +21,10 @@ //! proof carried in every parachain block is checked against the relay parent's state root by the //! validators, so the keys can be read out of it without any new trust assumption. //! -//! The commitment is expensive. A full 1024-slot set is roughly 420ms of wasm, which does not fit -//! in a block, so it is absorbed a chunk at a time across blocks and published once complete. The -//! authority set for the next session is known a session ahead, which is what makes that possible. +//! The commitment is expensive, around 800ms of wasm for a full 1024 slot set, most of it spent +//! decompressing the keys rather than hashing them. It is committed in one block all the same, +//! since it happens once per authority set, which is once every four hours on polkadot, and a set +//! that has not changed is republished from the stored commitment rather than hashed again. #![cfg_attr(not(feature = "std"), no_std)] @@ -31,7 +32,7 @@ extern crate alloc; use alloc::vec::Vec; -use apk_commitment::{PartialCommitment, NUM_VALIDATORS}; +use apk_commitment::{padded_to_circuit_width, public_keys_commitment_bytes}; use ark_bls12_381::G1Affine; use ark_serialize::CanonicalDeserialize; pub use beefy_verifier_primitives::{ @@ -53,24 +54,7 @@ pub const RELAY_BEEFY_VALIDATOR_SET_ID: [u8; 32] = [ 0x8f, 0x05, 0xbc, 0xcc, 0x2f, 0x70, 0xec, 0x66, 0xa3, 0x29, 0x99, 0xc5, 0x76, 0x11, 0x56, 0xbe, ]; -impl Progress { - /// A chain that has absorbed nothing yet. - pub fn fresh(set_digest: [u8; 32]) -> Self { - Self { set_digest, absorbed: 0, state: PartialCommitment::new().to_bytes() } - } -} - -/// Where the running commitment has got to. -#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo, Default, MaxEncodedLen)] -pub struct Progress { - /// Identifies the set being absorbed, so a rotation part way through restarts rather than - /// mixing keys from two sets into one commitment. - pub set_digest: [u8; 32], - /// How many of the [`NUM_VALIDATORS`] slots have been absorbed. - pub absorbed: u32, - /// The Merkle-Damgard state, carried between blocks. - pub state: [u8; 32], -} +pub mod benchmarking; #[frame_support::pallet] pub mod pallet { @@ -82,12 +66,7 @@ pub mod pallet { pub trait Config: polkadot_sdk::frame_system::Config + cumulus_pallet_parachain_system::Config { - /// How many validator slots to absorb per block. Trades block weight against how many - /// blocks a full set takes: at roughly 410us per slot in wasm, 64 is about 26ms. - #[pallet::constant] - type SlotsPerBlock: Get; - - /// Cost of absorbing a chunk. `()` carries a measured default, see [`WeightInfo`]. + /// Cost of committing to a set. `()` carries a rough default, see [`WeightInfo`]. /// /// Disambiguated at use as `::WeightInfo`, since /// `cumulus_pallet_parachain_system::Config` also has one. @@ -97,10 +76,6 @@ pub mod pallet { #[pallet::pallet] pub struct Pallet(_); - /// The commitment currently being absorbed, if any. - #[pallet::storage] - pub type Pending = StorageValue<_, Progress, OptionQuery>; - /// The last commitment published to a header digest, and the set it describes. #[pallet::storage] pub type Published = StorageValue<_, (u64, [u8; 32], [u8; 32]), OptionQuery>; @@ -108,9 +83,7 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// Started absorbing a new authority set. - CommitmentStarted { set_digest: [u8; 32] }, - /// Finished, and wrote the commitment to this block's header. + /// Committed to a new authority set, and wrote the commitment to this block's header. CommitmentPublished { set_id: u64, set_digest: [u8; 32], commitment: [u8; 32] }, } @@ -123,16 +96,16 @@ pub mod pallet { /// chunk is real work, tens of milliseconds, and a block that does not account for it can /// overrun its budget. fn on_finalize(_now: BlockNumberFor) { - let slots = match Self::advance() { - Ok(slots) => slots, + let hashed = match Self::advance() { + Ok(hashed) => hashed, Err(e) => { log::debug!(target: "apk-digest", "commitment did not advance: {e:?}"); - 0 + false }, }; - if slots > 0 { + if hashed { frame_system::Pallet::::register_extra_weight_unchecked( - ::WeightInfo::absorb(slots), + ::WeightInfo::commit(), DispatchClass::Mandatory, ); } @@ -142,58 +115,36 @@ pub mod pallet { impl Pallet { /// Absorb the next chunk, starting or restarting if the set changed, and publish once the /// whole set is in. - fn advance() -> Result> { + fn advance() -> Result> { let keys = Self::relay_beefy_g1_keys()?; // Read the set id up front even though it is only needed at the end. It is cheap, and - // discovering it missing after absorbing a chunk would throw that work away: the error - // propagates before `Pending` is written, so the same chunk would be re-absorbed and - // re-fail every block. + // discovering it missing after hashing a whole set would throw that work away. let set_id = Self::relay_beefy_set_id()?; let set_digest = sp_io::hashing::blake2_256(&keys.encode()); - let mut progress = match next_progress( - Pending::::get().as_ref(), - Published::::get().map(|(id, digest, _)| (id, digest)), - set_id, - set_digest, - ) { - // Already hashed this set, but the digest still goes in every header until it - // rotates. A verifier only reads the one header a proof happens to finalize, so - // publishing once would mean it almost never sees it. - Step::Done => { - if let Some((set_id, _, commitment)) = Published::::get() { - Self::deposit_digest(set_id, commitment); - } - return Ok(0); + match next_step(Published::::get().as_ref(), set_digest) { + // The keys have not changed, so the commitment stands. It still goes in this + // header, and under the current set id: a session can rotate without changing + // membership, and a client tracks commitments per set id, so the same commitment + // has to be published again under the new one or the client never learns it. + Step::Republish(commitment) => { + Self::deposit_digest(set_id, commitment); + Published::::put((set_id, set_digest, commitment)); + Ok(false) }, - Step::Restart => { - Self::deposit_event(Event::CommitmentStarted { set_digest }); - Progress::fresh(set_digest) + Step::Commit => { + let commitment = + commit(&keys).map_err(|_| Error::::MalformedAuthorityKey)?; + Self::deposit_digest(set_id, commitment); + Published::::put((set_id, set_digest, commitment)); + Self::deposit_event(Event::CommitmentPublished { + set_id, + set_digest, + commitment, + }); + Ok(true) }, - Step::Continue(p) => p, - }; - - let from = progress.absorbed as usize; - let take = (T::SlotsPerBlock::get() as usize).min(NUM_VALIDATORS - from); - if take == 0 { - return Ok(0); - } - - progress.state = absorb_slots(&keys, from, take, progress.state) - .map_err(|_| Error::::MalformedAuthorityKey)?; - progress.absorbed += take as u32; - - if progress.absorbed as usize == NUM_VALIDATORS { - let commitment = progress.state; - Self::deposit_digest(set_id, commitment); - - Pending::::kill(); - Published::::put((set_id, set_digest, commitment)); - Self::deposit_event(Event::CommitmentPublished { set_id, set_digest, commitment }); - } else { - Pending::::put(&progress); } - Ok(take as u32) } /// Put the commitment in this block's header. @@ -262,56 +213,41 @@ pub mod pallet { } } -/// Cost of absorbing `slots` validator slots into the running commitment. +/// Cost of committing to a validator set. pub trait WeightInfo { - fn absorb(slots: u32) -> Weight; + fn commit() -> Weight; } -/// Measured rather than benchmarked, and should be replaced by a generated `WeightInfo` before -/// this runs anywhere real. +/// A rough default for tests and for a chain that has not generated its own. /// -/// The commitment was timed in wasm at roughly 410us per slot, linear in the number of slots from -/// 64 up to the full 1024. Weight ref time is picoseconds, so a slot is about 410_000_000 units, -/// and the default `SlotsPerBlock` of 64 comes to ~26ms, a little over one percent of a two second -/// block. The storage side is one read and one write of a fixed-size value. +/// A full set is around 800ms in wasm, of which roughly four fifths is decompressing the keys +/// rather than hashing them: a key arrives as 48 compressed bytes and recovering `y` needs a +/// square root in the base field. It is only paid when the membership actually changes. impl WeightInfo for () { - fn absorb(slots: u32) -> Weight { - Weight::from_parts(410_000_000u64.saturating_mul(slots as u64), 0) - .saturating_add(Weight::from_parts(0, 4096)) + fn commit() -> Weight { + Weight::from_parts(821_000_000_000, 0).saturating_add(Weight::from_parts(0, 4096)) } } /// What to do with the commitment this block. #[derive(Debug, PartialEq, Eq)] pub enum Step { - /// This set is already published; nothing to do. - Done, - /// Begin, or begin again because the set changed under us. - Restart, - /// Carry on from where the last block left off. - Continue(Progress), + /// The keys are unchanged, so republish the commitment already computed for them. + Republish([u8; 32]), + /// Nothing published, or a different set of keys: hash them. + Commit, } -/// Decide how to proceed, given what is in progress and what has already been published. +/// Decide how to proceed, given what was published last. /// -/// Kept pure so the rotation case can be tested without a mock chain. The case that matters is a -/// set changing part way through: the Merkle-Damgard chain is over one specific key list, so -/// carrying the state across a rotation would silently produce a commitment belonging to neither -/// set. Restarting is the only safe answer. -pub fn next_progress( - pending: Option<&Progress>, - published: Option<(u64, [u8; 32])>, - set_id: u64, - set_digest: [u8; 32], -) -> Step { - match pending { - Some(p) if p.set_digest != set_digest => Step::Restart, - Some(p) => Step::Continue(p.clone()), - // Keyed on the set id as well as the keys. A session can rotate without changing the - // membership, and a client tracks commitments per set id, so the same commitment has to be - // published again under the new one or the client never learns it and stalls there. - None if published == Some((set_id, set_digest)) => Step::Done, - None => Step::Restart, +/// Kept pure so it can be tested without a mock chain. The case worth being careful about is a +/// session rotating without the membership changing, which is common: the commitment is the same, +/// so rehashing it would spend the better part of a second for nothing, but it still has to be +/// republished under the new set id or a client tracking commitments per set never learns it. +pub fn next_step(published: Option<&(u64, [u8; 32], [u8; 32])>, set_digest: [u8; 32]) -> Step { + match published { + Some((_, digest, commitment)) if *digest == set_digest => Step::Republish(*commitment), + _ => Step::Commit, } } @@ -319,30 +255,17 @@ pub fn next_progress( #[derive(Debug, PartialEq, Eq)] pub struct MalformedKey; -/// Absorb slots `from .. from + take` of a validator set into a running commitment. -/// -/// Kept free of the pallet so the part that can actually be wrong, the chunking and the padding, -/// is testable without a mock chain. Slots past the end of `keys` are the identity point, which is -/// how the circuit pads a set shorter than [`NUM_VALIDATORS`]. +/// The commitment over a validator set, padded to the circuit's width with the identity point. /// -/// `state` is the Merkle-Damgard state carried between blocks; pass -/// `PartialCommitment::new().to_bytes()` to start. -pub fn absorb_slots( - keys: &[[u8; BLS_G1_SIGNATURE_LEN]], - from: usize, - take: usize, - state: [u8; 32], -) -> Result<[u8; 32], MalformedKey> { - let chunk: Vec = (from..from + take) - .map(|i| match keys.get(i) { - Some(k) => G1Affine::deserialize_compressed(&k[..]).map_err(|_| MalformedKey), - None => Ok(G1Affine::identity()), - }) +/// Kept free of the pallet so the part that can actually be wrong, the decompression and the +/// padding, is testable without a mock chain. +pub fn commit(keys: &[[u8; BLS_G1_SIGNATURE_LEN]]) -> Result<[u8; 32], MalformedKey> { + let points: Vec = keys + .iter() + .map(|k| G1Affine::deserialize_compressed(&k[..]).map_err(|_| MalformedKey)) .collect::>()?; - let mut partial = PartialCommitment::from_bytes(&state); - partial.absorb(&chunk); - Ok(partial.to_bytes()) + Ok(public_keys_commitment_bytes(&padded_to_circuit_width(&points))) } #[cfg(test)] @@ -374,43 +297,20 @@ mod tests { public_keys_commitment_bytes(&padded_to_circuit_width(&points)) } - fn run(keys: &[[u8; BLS_G1_SIGNATURE_LEN]], slots_per_block: usize) -> [u8; 32] { - let mut state = PartialCommitment::new().to_bytes(); - let mut absorbed = 0usize; - while absorbed < NUM_VALIDATORS { - let take = slots_per_block.min(NUM_VALIDATORS - absorbed); - state = absorb_slots(keys, absorbed, take, state).unwrap(); - absorbed += take; - } - state - } - - /// The whole point of absorbing across blocks: the block size must not change the answer. - #[test] - fn any_chunk_size_reaches_the_same_commitment() { - let keys = relay_keys(); - let expected = expected_commitment(&keys); - for slots in [1usize, 64, 100, 512, NUM_VALIDATORS] { - assert_eq!(run(&keys, slots), expected, "slots_per_block {slots} changed the result"); - } - } - - /// A chunk that straddles the boundary between real keys and padding is the case most likely - /// to be got wrong, so pin it explicitly. + /// A set shorter than the circuit's width is padded, which is most of what `commit` does + /// beyond hashing. #[test] - fn padding_boundary_is_handled_within_a_chunk() { + fn a_short_set_is_padded_to_the_circuit_width() { let keys = relay_keys(); - // 2 real keys, so a chunk of 3 from slot 0 crosses into padding immediately. - assert_eq!(run(&keys, 3), expected_commitment(&keys)); + assert_eq!(commit(&keys).unwrap(), expected_commitment(&keys)); } /// An empty set is all padding, and must still be well defined. #[test] fn an_empty_set_is_all_padding() { - let commitment = run(&[], 64); let all_identity: Vec = - (0..NUM_VALIDATORS).map(|_| G1Affine::identity()).collect(); - assert_eq!(commitment, public_keys_commitment_bytes(&all_identity)); + (0..apk_commitment::NUM_VALIDATORS).map(|_| G1Affine::identity()).collect(); + assert_eq!(commit(&[]).unwrap(), public_keys_commitment_bytes(&all_identity)); } /// Order matters, since the bitlist selects signers positionally. @@ -419,7 +319,7 @@ mod tests { let keys = relay_keys(); let mut swapped = keys.clone(); swapped.swap(0, 1); - assert_ne!(run(&keys, 64), run(&swapped, 64)); + assert_ne!(commit(&keys).unwrap(), commit(&swapped).unwrap()); } /// A key whose x coordinate is the field modulus, which is not a canonical field element. @@ -427,7 +327,7 @@ mod tests { /// Note an all-`0xff` key is *not* a good negative case: the top bits are the compression and /// infinity flags, so it decodes happily as the identity point. #[test] - fn a_malformed_key_is_rejected_rather_than_absorbed() { + fn a_malformed_key_is_rejected_rather_than_hashed() { let mut key = hex::decode( "1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f624\ 1eabfffeb153ffffb9feffffffffaaab", @@ -437,8 +337,7 @@ mod tests { let mut fixed = [0u8; BLS_G1_SIGNATURE_LEN]; fixed.copy_from_slice(&key); - let state = PartialCommitment::new().to_bytes(); - assert_eq!(absorb_slots(&[fixed], 0, 1, state), Err(MalformedKey)); + assert_eq!(commit(&[fixed]), Err(MalformedKey)); } /// The identity point encodes as a valid compressed key, so a set genuinely containing one is @@ -450,90 +349,49 @@ mod tests { G1Affine::identity().serialize_compressed(&mut encoded).unwrap(); let mut key = [0u8; BLS_G1_SIGNATURE_LEN]; key.copy_from_slice(&encoded); - let state = PartialCommitment::new().to_bytes(); - assert!(absorb_slots(&[key], 0, 1, state).is_ok()); + assert!(commit(&[key]).is_ok()); } const SET_A: [u8; 32] = [0xaa; 32]; const SET_B: [u8; 32] = [0xbb; 32]; #[test] - fn a_fresh_chain_starts() { - assert_eq!(next_progress(None, None, 1, SET_A), Step::Restart); + fn a_fresh_chain_commits() { + assert_eq!(next_step(None, SET_A), Step::Commit); } #[test] - fn an_already_published_set_is_left_alone() { - assert_eq!(next_progress(None, Some((1, SET_A)), 1, SET_A), Step::Done); + fn keys_already_committed_to_are_republished_rather_than_rehashed() { + let published = (1u64, SET_A, [7u8; 32]); + assert_eq!(next_step(Some(&published), SET_A), Step::Republish([7u8; 32])); } /// A session can rotate without the membership changing, which is common on small networks and - /// possible anywhere. The keys hash to the same digest, but the client files commitments under - /// the set id, so this has to publish again rather than deciding there is nothing to do. - #[test] - fn the_same_keys_under_a_new_set_id_are_published_again() { - assert_eq!(next_progress(None, Some((1, SET_A)), 2, SET_A), Step::Restart); - } - - /// A new set arriving after one was published starts rather than stopping. - #[test] - fn a_new_set_starts_even_though_another_was_published() { - assert_eq!(next_progress(None, Some((1, SET_A)), 2, SET_B), Step::Restart); - } - - #[test] - fn work_in_progress_on_the_same_set_continues() { - let p = Progress { set_digest: SET_A, absorbed: 128, state: [1u8; 32] }; - assert_eq!(next_progress(Some(&p), None, 1, SET_A), Step::Continue(p)); - } - - /// The case this whole function exists for: the authority set changed while a commitment was - /// part way through. + /// possible anywhere. Rehashing would cost the better part of a second for a commitment we + /// already hold, so the stored one is republished under the new set id instead. #[test] - fn a_rotation_part_way_through_restarts() { - let p = Progress { set_digest: SET_A, absorbed: 512, state: [1u8; 32] }; - assert_eq!(next_progress(Some(&p), None, 2, SET_B), Step::Restart); + fn the_same_keys_under_a_new_set_id_are_not_rehashed() { + let published = (1u64, SET_A, [7u8; 32]); + assert_eq!(next_step(Some(&published), SET_A), Step::Republish([7u8; 32])); } - /// And restarting has to mean *restarting*, not resuming with a relabelled set. If the state - /// carried over, the commitment would be a Merkle-Damgard chain over the first set's keys - /// followed by the second's, belonging to neither, and nothing downstream would notice. + /// Different keys mean the stored commitment says nothing about them. #[test] - fn a_restart_discards_the_partial_state() { - let fresh = Progress::fresh(SET_B); - assert_eq!(fresh.absorbed, 0); - assert_eq!(fresh.state, PartialCommitment::new().to_bytes()); - assert_eq!(fresh.set_digest, SET_B); + fn a_new_set_is_committed_to_even_though_another_was_published() { + let published = (1u64, SET_A, [7u8; 32]); + assert_eq!(next_step(Some(&published), SET_B), Step::Commit); } - /// End to end over the absorption itself: absorb part of one set, rotate, and the commitment - /// that comes out must be the second set's, identical to having never seen the first. + /// Two different sets must not reach the same commitment, which is the property the set digest + /// is standing in for when deciding whether to rehash. #[test] - fn a_commitment_interrupted_by_a_rotation_is_not_a_mixture() { + fn a_different_set_commits_differently() { let first = relay_keys(); let mut second = relay_keys(); - second.swap(0, 1); // a different set, same size - - // absorb 300 slots of the first set, then rotate - let mut state = PartialCommitment::new().to_bytes(); - state = absorb_slots(&first, 0, 300, state).unwrap(); - assert_eq!( - next_progress( - Some(&Progress { set_digest: SET_A, absorbed: 300, state }), - None, - 2, - SET_B - ), - Step::Restart - ); - - // restart discards that state, so the result is the second set's commitment alone - let restarted = run(&second, 64); - assert_eq!(restarted, expected_commitment(&second)); - assert_ne!(restarted, expected_commitment(&first), "the two sets must not collide"); + second.swap(0, 1); + assert_ne!(commit(&first).unwrap(), commit(&second).unwrap()); } - /// A client finds the commitment in a header carrying unrelated digest items too, which is the /// normal case: aura and the parachain system both write their own. #[test] fn commitment_is_found_among_other_digest_items() { diff --git a/parachain/runtimes/gargantua/Cargo.toml b/parachain/runtimes/gargantua/Cargo.toml index 69b3ac411..654d6cf29 100644 --- a/parachain/runtimes/gargantua/Cargo.toml +++ b/parachain/runtimes/gargantua/Cargo.toml @@ -185,6 +185,7 @@ runtime-benchmarks = [ "ismp-parachain/runtime-benchmarks", "pallet-intents-coprocessor/runtime-benchmarks", "pallet-beefy-consensus-proofs/runtime-benchmarks", + "pallet-beefy-apk-digest/runtime-benchmarks", "pallet-hyper-fungible-token/runtime-benchmarks", ] try-runtime = [ diff --git a/parachain/runtimes/gargantua/src/ismp.rs b/parachain/runtimes/gargantua/src/ismp.rs index 09237200b..b416c4cad 100644 --- a/parachain/runtimes/gargantua/src/ismp.rs +++ b/parachain/runtimes/gargantua/src/ismp.rs @@ -245,6 +245,10 @@ impl ismp_beefy::BeefyClientConfig for Runtime { pallet_beefy_consensus_proofs::ApkVerifyingKey::::get().into_inner() } + fn apk_digest_para_id() -> u32 { + 4009 + } + fn allowed_proof_types() -> &'static [u8] { // Testnet: accept the naive ECDSA and SP1 ZK proof formats, plus aggregate public key. &[ismp_beefy::PROOF_TYPE_NAIVE, ismp_beefy::PROOF_TYPE_SP1, ismp_beefy::PROOF_TYPE_APK] diff --git a/parachain/runtimes/gargantua/src/lib.rs b/parachain/runtimes/gargantua/src/lib.rs index bd215a798..cdd14a37a 100644 --- a/parachain/runtimes/gargantua/src/lib.rs +++ b/parachain/runtimes/gargantua/src/lib.rs @@ -883,17 +883,8 @@ impl pallet_messaging_incentives::Config for Runtime { type AdminOrigin = EnsureRoot; } -parameter_types! { - /// A full 1024-slot commitment is roughly 420ms of wasm, so it is absorbed across blocks. At - /// about 410us per slot this is ~26ms per block, and a whole set lands in 16 blocks, well - /// inside the session in which the next authority set is already known. - pub const ApkSlotsPerBlock: u32 = 64; -} - impl pallet_beefy_apk_digest::Config for Runtime { - type SlotsPerBlock = ApkSlotsPerBlock; - // Measured, not benchmarked; see the note on the default impl. - type WeightInfo = (); + type WeightInfo = weights::pallet_beefy_apk_digest::WeightInfo; } // Create the runtime by composing the FRAME pallets that were previously configured. @@ -1057,6 +1048,7 @@ mod benches { [pallet_vesting, Vesting] [pallet_tx_pause, TxPause] [pallet_beefy_consensus_proofs, BeefyConsensusProofs] + [pallet_beefy_apk_digest, BeefyApkDigest] [pallet_hyper_fungible_token, HyperFungibleToken] ); } diff --git a/parachain/runtimes/gargantua/src/weights/mod.rs b/parachain/runtimes/gargantua/src/weights/mod.rs index 7b666b48d..b7a81f13a 100644 --- a/parachain/runtimes/gargantua/src/weights/mod.rs +++ b/parachain/runtimes/gargantua/src/weights/mod.rs @@ -31,6 +31,7 @@ pub mod ismp_parachain; pub mod pallet_asset_rate; pub mod pallet_assets; pub mod pallet_balances; +pub mod pallet_beefy_apk_digest; pub mod pallet_beefy_consensus_proofs; pub mod pallet_collective; pub mod pallet_hyper_fungible_token; diff --git a/parachain/runtimes/gargantua/src/weights/pallet_beefy_apk_digest.rs b/parachain/runtimes/gargantua/src/weights/pallet_beefy_apk_digest.rs new file mode 100644 index 000000000..f45e1bb2b --- /dev/null +++ b/parachain/runtimes/gargantua/src/weights/pallet_beefy_apk_digest.rs @@ -0,0 +1,48 @@ + +//! Autogenerated weights for `pallet_beefy_apk_digest` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-17, STEPS: `2`, REPEAT: `10`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Akinloses-MacBook-Pro.local`, CPU: `` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// target/release/wbuild/gargantua-runtime/gargantua_runtime.compact.compressed.wasm +// --pallet +// pallet_beefy_apk_digest +// --extrinsic +// commit +// --steps +// 2 +// --repeat +// 10 +// --output +// parachain/runtimes/gargantua/src/weights/pallet_beefy_apk_digest.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use polkadot_sdk::*; +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_beefy_apk_digest`. +pub struct WeightInfo(PhantomData); +impl pallet_beefy_apk_digest::WeightInfo for WeightInfo { + fn commit() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 808_140_000_000 picoseconds. + Weight::from_parts(820_854_000_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } +} From 460becc09b7fad215f2921b470e3e0b570acb56c Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Mon, 17 Aug 2026 13:21:57 +0100 Subject: [PATCH 29/48] read the apk commitment only from hyperbridge's own header --- modules/consensus/beefy/verifier/src/apk.rs | 37 +++++++++++++++---- .../beefy/verifier/tests/apk_fixture.rs | 10 +++-- modules/ismp/clients/beefy/src/consensus.rs | 1 + modules/ismp/clients/beefy/src/lib.rs | 10 +++++ .../src/benchmarking.rs | 5 +++ 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/modules/consensus/beefy/verifier/src/apk.rs b/modules/consensus/beefy/verifier/src/apk.rs index 5f69aefeb..1ccf51681 100644 --- a/modules/consensus/beefy/verifier/src/apk.rs +++ b/modules/consensus/beefy/verifier/src/apk.rs @@ -48,12 +48,17 @@ const COORDINATE: usize = 48; /// The aggregate key reaches the circuit as six 64 bit limbs per coordinate. const APK_LIMBS: usize = 12; -/// The seed point the circuit aggregates onto, `HashToG1(dst="gnark-apk-proofs", msg="apk-seed")`. -/// Hardcoded in the circuit and in `ApkProof.sol`, packed here the same way a key is. +/// The seed point the circuit aggregates onto, hashed to the curve from `dst="gnark-apk-proofs"` +/// and `msg="apk-seed-coset"`. Hardcoded in the circuit and in `ApkProof.sol`, packed here the +/// same way a key is. +/// +/// It sits on the curve but deliberately outside G1, which is what lets the circuit aggregate +/// with incomplete addition. Every key is in G1, so `seed + sum(pk)` stays in the coset `seed + G1` +/// and can never meet a key or its negation, the two cases the chord formula cannot handle. const SEED: [u8; APK_G1_LEN] = hex_literal::hex!( - "054abdb6c5522fe2f71d55922d6f674a4908d39e2b33efcc62520c0621ca0d6a" - "6d84ee717b7fb1cb5f46687265be01ce06e518322165fd114cdf6b4ab59eb45e" - "9289cc4f6f7948d6b680cef9ecc0e0e0f96bd59a578d58c33c0e10db9c25b5ad" + "19742ffba069554d8cacceb8ed5514b2ecf72cd7372d3414203338f4fd3b3cc7" + "42fb160f8eb5818422246de186e0814a0e0f5d1199876e646952fb74d39e0b34" + "042a8d48786adae7e0fccf4b0236c72e82343de94c9d12bf17d22bec9edbbe2b" ); /// Verify a whole update and return the new trusted state with the verified parachain headers. @@ -66,6 +71,7 @@ pub fn verify_apk_consensus( trusted_state: ApkConsensusState, proof: ApkConsensusMessage, verifying_key: &[u8], + digest_para_id: u32, ) -> Result<(ApkConsensusState, Vec), Error> { let (mut state, heads_root) = verify_apk_mmr_update_proof::(trusted_state, proof.mmr, verifying_key)?; @@ -74,7 +80,12 @@ pub fn verify_apk_consensus( // Forward chaining: a verified header may carry the commitment for a set this client has no // keys for yet. Picking it up here is what makes the next update verifiable at all, and is // why the digest names the next set rather than the current one. - for header in headers.iter() { + // + // Only `digest_para_id`'s headers are read. A proof carries whichever parachains the relay + // finalized, and every one of them is proven against the heads root, so a digest from another + // parachain is authentic yet says nothing about this relay's authorities. Left unfiltered any + // parachain could name the keys this client trusts next. + for header in headers.iter().filter(|header| header.para_id == digest_para_id) { if let Some(digest) = read_apk_digest(&header.header) { state.learn_commitment(digest.set_id, H256(digest.commitment)); break; @@ -207,7 +218,7 @@ fn verify_apk_proof( // The circuit aggregates onto a fixed seed point, so what it proves about is `seed + apk` // rather than the aggregate on its own. `ApkProof._encodePublicInputs` adds it the same way // before handing the inputs to the PLONK verifier. - let seeded = (read_g1(&mmr.apk)?.into_group() + read_g1(&SEED)?).into_affine(); + let seeded = (read_g1(&mmr.apk)?.into_group() + read_seed()?).into_affine(); let (x, y) = seeded.xy().ok_or(Error::ApkPointInvalid)?; // Each coordinate is too wide for one scalar, so it travels as six 64 bit limbs, least @@ -322,6 +333,18 @@ fn hash_commitment_to_g1(encoded_commitment: &[u8]) -> Result { const CIPHER_SUITE: &[u8] = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; /// Read a G1 point from raw big-endian coordinates. +/// Read the aggregation seed, which is on the curve but not in G1, so the subgroup check +/// [`read_g1`] applies to keys would reject it. +fn read_seed() -> Result { + let x = read_fq(&SEED[..COORDINATE])?; + let y = read_fq(&SEED[COORDINATE..])?; + let point = G1Affine::new_unchecked(x, y); + if !point.is_on_curve() { + return Err(Error::ApkPointInvalid); + } + Ok(point) +} + fn read_g1(bytes: &[u8; APK_G1_LEN]) -> Result { let x = read_fq(&bytes[..COORDINATE])?; let y = read_fq(&bytes[COORDINATE..])?; diff --git a/modules/consensus/beefy/verifier/tests/apk_fixture.rs b/modules/consensus/beefy/verifier/tests/apk_fixture.rs index f3c169320..169f1db90 100644 --- a/modules/consensus/beefy/verifier/tests/apk_fixture.rs +++ b/modules/consensus/beefy/verifier/tests/apk_fixture.rs @@ -28,6 +28,9 @@ use polkadot_sdk::*; use primitive_types::H256; /// Generated by the circuit's trusted setup, and the same key that goes into pallet storage. +/// The parachain the fixture was captured from, and the only one whose digests it should read. +const PARA_ID: u32 = 4009; + const VERIFYING_KEY: &[u8] = include_bytes!("../../../../../evm/tests/foundry/fixtures/apk-verifying-key.bin"); @@ -73,8 +76,9 @@ fn apk_verifier_agrees_with_solidity() { let signing_set = proof.mmr.commitment.validator_set_id; let signers = count_signers(&proof.mmr.bitlist); - let (state, headers) = verify_apk_consensus::(trusted.clone(), proof, VERIFYING_KEY) - .expect("the proof solidity verifies must verify here too"); + let (state, headers) = + verify_apk_consensus::(trusted.clone(), proof, VERIFYING_KEY, PARA_ID) + .expect("the proof solidity verifies must verify here too"); assert!( state.latest_beefy_height > trusted.latest_beefy_height, @@ -83,7 +87,7 @@ fn apk_verifier_agrees_with_solidity() { state.latest_beefy_height ); assert_eq!(headers.len(), 1, "should finalize the registered parachain"); - assert_eq!(headers[0].para_id, 4009, "should be para 4009"); + assert_eq!(headers[0].para_id, PARA_ID, "should be para {PARA_ID}"); println!( "apk proof verified in the runtime path: set {signing_set}, {signers} signers, beefy height {} -> {}", diff --git a/modules/ismp/clients/beefy/src/consensus.rs b/modules/ismp/clients/beefy/src/consensus.rs index 80b507720..bd3542b18 100644 --- a/modules/ismp/clients/beefy/src/consensus.rs +++ b/modules/ismp/clients/beefy/src/consensus.rs @@ -120,6 +120,7 @@ where apk_state, apk_proof, &C::apk_verifying_key(), + C::apk_digest_para_id(), )?; (state.encode(), headers) }, diff --git a/modules/ismp/clients/beefy/src/lib.rs b/modules/ismp/clients/beefy/src/lib.rs index 7acf3e3ec..9f7d50fb7 100644 --- a/modules/ismp/clients/beefy/src/lib.rs +++ b/modules/ismp/clients/beefy/src/lib.rs @@ -56,6 +56,16 @@ pub trait BeefyClientConfig { Default::default() } + /// The parachain whose header digests carry apk commitments, which is hyperbridge. + /// + /// Every parachain in a proof is proven against the heads root, so a digest from any of them + /// is authentic; only this one's says anything about the relay chain's authorities. The + /// default of zero matches no parachain, so a chain that never sees an apk proof learns + /// nothing rather than learning from whoever turns up. + fn apk_digest_para_id() -> u32 { + 0 + } + /// Allowed proof types. Controls which consensus proof formats this client will /// accept. On mainnet set to `&[PROOF_TYPE_SP1]`, on testnets set to /// `&[PROOF_TYPE_NAIVE, PROOF_TYPE_SP1]`. A proof whose type byte is not listed is diff --git a/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs b/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs index de093950b..3cfdcbe21 100644 --- a/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs +++ b/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs @@ -155,10 +155,15 @@ mod benchmarks { let message: beefy_verifier_primitives::ApkConsensusMessage = proof.try_into().expect("apk proof fixture converts"); + // The parachain the fixture was captured from, so the digest is read and the + // measurement covers the path a real proof takes. + const FIXTURE_PARA_ID: u32 = 4009; + beefy_verifier::apk::verify_apk_consensus::( state.clone(), message, &verifying_key, + FIXTURE_PARA_ID, ) .expect("the fixture proof verifies"); } From 92f99a00fe8dddd5a87ecfc4a793b5655a09346b Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Mon, 17 Aug 2026 13:22:12 +0100 Subject: [PATCH 30/48] take the digest from hyperbridge's header and return it as a struct --- evm/src/consensus/BlsApkBeefy.sol | 38 ++++++++++++++------- evm/src/consensus/Types.sol | 32 +++++++++++------ evm/tests/foundry/ApkCommitmentDigest.t.sol | 26 ++++++-------- evm/tests/foundry/BlsApkBeefy.t.sol | 3 +- evm/tests/foundry/vendor/ApkProof.sol | 26 ++++++++++---- 5 files changed, 79 insertions(+), 46 deletions(-) diff --git a/evm/src/consensus/BlsApkBeefy.sol b/evm/src/consensus/BlsApkBeefy.sol index f24109197..50973a0ae 100644 --- a/evm/src/consensus/BlsApkBeefy.sol +++ b/evm/src/consensus/BlsApkBeefy.sol @@ -24,6 +24,7 @@ import {ScaleCodec} from "@polytope-labs/solidity-merkle-trees/src/trie/polkadot import {Codec} from "./Codec.sol"; import { ApkAuthoritySet, + ApkDigest, BlsApkBeefyConsensusProof, BlsApkConsensusState, BlsApkRelayChainProof, @@ -79,6 +80,12 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { /// The APK proof verifier, holding the circuit's verifying key. IApkProof public immutable _apk; + /// The parachain whose header digests carry apk commitments, which is hyperbridge. + /// + /// Every parachain in a proof is proven against the heads root, so a digest from any of them + /// is authentic; only this one's says anything about the relay chain's authorities. + uint32 public immutable _digestParaId; + /// The commitment was signed by a set this client does not know. error UnknownAuthoritySet(); /// Fewer than two thirds of the set signed. @@ -94,8 +101,9 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { /// The authority set has no APK commitment yet, so no proof against it can be checked. error MissingApkCommitment(); - constructor(address apkProof) { + constructor(address apkProof, uint32 digestParaId) { _apk = IApkProof(apkProof); + _digestParaId = digestParaId; } function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) { @@ -118,14 +126,16 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { } (BlsApkConsensusState memory newState, bytes32 headsRoot) = verifyMmrUpdateProof(consensusState, relay); - (IntermediateState[] memory intermediates, bool found, uint64 setId, bytes32 commitment) = - verifyParachainHeaderProof(headsRoot, parachain); + (IntermediateState[] memory intermediates, ApkDigest memory digest) = + verifyParachainHeaderProof(headsRoot, parachain, _digestParaId); // Forward chaining: a verified header may carry the commitment for a set this client does // not have keys for yet. Picking it up here is what lets the next update be verified at // all, and is the reason the digest names the *next* set rather than the current one. - if (found && setId == newState.nextAuthoritySet.id && newState.nextAuthoritySet.apkCommitment == bytes32(0)) { - newState.nextAuthoritySet.apkCommitment = commitment; + if ( + digest.setId == newState.nextAuthoritySet.id && newState.nextAuthoritySet.apkCommitment == 0 + ) { + newState.nextAuthoritySet.apkCommitment = digest.commitment; } return (abi.encode(newState), intermediates, newState.nextAuthoritySet.id); @@ -151,7 +161,7 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { // A set whose commitment has not been learned from a digest yet cannot be verified against. // Reverting here is deliberate: silently accepting would mean checking the proof against a // zero commitment. - if (authoritySet.apkCommitment == bytes32(0)) revert MissingApkCommitment(); + if (authoritySet.apkCommitment == 0) revert MissingApkCommitment(); verifySignedByApk(Codec.Encode(commitment), relayProof, authoritySet); @@ -173,7 +183,7 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { trustedState.nextAuthoritySet = ApkAuthoritySet({ id: relayProof.latestMmrLeaf.nextAuthoritySet.id, len: relayProof.latestMmrLeaf.nextAuthoritySet.len, - apkCommitment: bytes32(0) + apkCommitment: 0 }); } trustedState.latestHeight = commitment.blockNumber; @@ -202,7 +212,7 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { // `verify` reverts on failure rather than returning false, so a successful call is the // whole result. Wrapped so the reason surfaces as this contract's error. try _apk.verify( - uint256(authoritySet.apkCommitment), + authoritySet.apkCommitment, relayProof.bitlist, relayProof.apk, relayProof.apkProof, @@ -259,10 +269,10 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { /// @dev Verify the parachain headers against the heads root, and surface any APK commitment /// they carry so the caller can roll it into the consensus state. - function verifyParachainHeaderProof(bytes32 headsRoot, ParachainProof memory proof) + function verifyParachainHeaderProof(bytes32 headsRoot, ParachainProof memory proof, uint32 digestParaId) internal pure - returns (IntermediateState[] memory, bool found, uint64 setId, bytes32 apkCommitment) + returns (IntermediateState[] memory, ApkDigest memory digest) { uint256 len = proof.parachains.length; MerkleMultiProof.Leaf[] memory leaves = new MerkleMultiProof.Leaf[](len); @@ -284,8 +294,10 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { }); // Only the first commitment found is used; the pallet writes at most one per block. - if (!found) { - (found, setId, apkCommitment) = HeaderImpl.apkCommitment(header); + // Read it from hyperbridge's header alone, or any parachain in the proof could name + // the keys this client trusts next. + if (digest.setId == 0 && para.id == digestParaId) { + digest = HeaderImpl.apkCommitment(header); } } @@ -294,7 +306,7 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { if (!valid) revert InvalidParachainHeaderProof(); } - return (intermediates, found, setId, apkCommitment); + return (intermediates, digest); } /// @dev Leaf index for a relay chain block, given where beefy was activated. diff --git a/evm/src/consensus/Types.sol b/evm/src/consensus/Types.sol index 30f37e257..458c4bafb 100644 --- a/evm/src/consensus/Types.sol +++ b/evm/src/consensus/Types.sol @@ -179,13 +179,21 @@ struct BeefyConsensusProof { // fixed width. Unlike the keyset root it does not come from the MMR leaf: hyperbridge publishes it // in a header digest, and a client picks it up from a header it has already verified. That is why // it is carried in the consensus state rather than supplied with each proof. +/// An apk commitment read off a header, with `setId` zero meaning the header carried none. +struct ApkDigest { + /// The authority set the commitment describes. + uint64 setId; + /// Poseidon2 commitment over that set's G1 public keys. + uint256 commitment; +} + struct ApkAuthoritySet { /// Id of the set. uint64 id; /// Number of validators in the set, for the two-thirds threshold. uint32 len; - /// Poseidon2 commitment over the set's G1 public keys. - bytes32 apkCommitment; + /// Poseidon2 commitment over the set's G1 public keys, as `ApkProof.verify` takes it. + uint256 apkCommitment; } struct BlsApkConsensusState { @@ -298,11 +306,10 @@ library HeaderImpl { /// as `publicKeysCommitment`, and `setId` says which authority set it describes. /// /// Payload is SCALE: a u64 set id little-endian, then the 32 byte commitment. - function apkCommitment(Header memory self) - internal - pure - returns (bool found, uint64 setId, bytes32 commitment) - { + /// + /// A zero `setId` reads as absent. BEEFY numbers its sets from one, so nothing legitimate + /// names set zero, and the caller then has one thing to check rather than two. + function apkCommitment(Header memory self) internal pure returns (ApkDigest memory digest) { for (uint256 j = 0; j < self.digests.length; j++) { if (!self.digests[j].isConsensus) continue; if (self.digests[j].consensus.consensusId != APK_COMMITMENT_ID) continue; @@ -312,10 +319,13 @@ library HeaderImpl { // producer wrote under this engine id, and the caller should see "absent", not fail. if (data.length != 40) continue; - setId = uint64(ScaleCodec.decodeUint256(Bytes.substr(data, 0, 8))); - commitment = Bytes.toBytes32(Bytes.substr(data, 8)); - return (true, setId, commitment); + uint64 setId = uint64(ScaleCodec.decodeUint256(Bytes.substr(data, 0, 8))); + if (setId == 0) continue; + + return ApkDigest({ + setId: setId, + commitment: uint256(Bytes.toBytes32(Bytes.substr(data, 8))) + }); } - return (false, 0, bytes32(0)); } } diff --git a/evm/tests/foundry/ApkCommitmentDigest.t.sol b/evm/tests/foundry/ApkCommitmentDigest.t.sol index f1f6d5af4..785c4dd94 100644 --- a/evm/tests/foundry/ApkCommitmentDigest.t.sol +++ b/evm/tests/foundry/ApkCommitmentDigest.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import {Test} from "forge-std/Test.sol"; -import {Header, Digest, DigestItem, HeaderImpl} from "../../src/consensus/Types.sol"; +import {ApkDigest, Header, Digest, DigestItem, HeaderImpl} from "../../src/consensus/Types.sol"; /// The client side of `pallet-beefy-apk-digest`: reading the APK commitment out of a hyperbridge /// header that a verifier has already authenticated through the BEEFY MMR's parachain heads root. @@ -40,10 +40,9 @@ contract ApkCommitmentDigestTest is Test { Digest[] memory digests = new Digest[](1); digests[0] = _consensus(HeaderImpl.APK_COMMITMENT_ID, PAYLOAD_577); - (bool found, uint64 setId, bytes32 commitment) = _header(digests).apkCommitment(); - assertTrue(found, "commitment not found"); - assertEq(setId, 577, "set id decoded wrong; check little-endian"); - assertEq(commitment, bytes32(uint256(0x0303030303030303030303030303030303030303030303030303030303030303))); + ApkDigest memory digest = _header(digests).apkCommitment(); + assertEq(digest.setId, 577, "set id decoded wrong; check little-endian"); + assertEq(digest.commitment, uint256(0x0303030303030303030303030303030303030303030303030303030303030303)); } /// The normal case: a header carrying aura's items alongside ours. @@ -53,9 +52,8 @@ contract ApkCommitmentDigestTest is Test { digests[1] = _consensus(HeaderImpl.APK_COMMITMENT_ID, PAYLOAD_577); digests[2] = _preRuntime(bytes4("aura")); - (bool found, uint64 setId,) = _header(digests).apkCommitment(); - assertTrue(found); - assertEq(setId, 577); + ApkDigest memory digest = _header(digests).apkCommitment(); + assertEq(digest.setId, 577); } /// Most headers do not complete a set, and that must read as absent rather than revert. @@ -63,9 +61,9 @@ contract ApkCommitmentDigestTest is Test { Digest[] memory digests = new Digest[](1); digests[0] = _preRuntime(bytes4("aura")); - (bool found,, bytes32 commitment) = _header(digests).apkCommitment(); - assertFalse(found); - assertEq(commitment, bytes32(0)); + ApkDigest memory digest = _header(digests).apkCommitment(); + assertEq(digest.setId, 0, "no digest, so the set id stays zero"); + assertEq(digest.commitment, 0); } /// Another engine's consensus item must not be mistaken for ours. Same digest variant, so the @@ -74,8 +72,7 @@ contract ApkCommitmentDigestTest is Test { Digest[] memory digests = new Digest[](1); digests[0] = _consensus(bytes4("ISMP"), PAYLOAD_577); - (bool found,,) = _header(digests).apkCommitment(); - assertFalse(found, "an ISMP digest was read as an APK commitment"); + assertEq(_header(digests).apkCommitment().setId, 0, "an ISMP digest was read as an APK commitment"); } /// A wrong-length payload under our engine id is skipped rather than decoded into garbage. @@ -83,7 +80,6 @@ contract ApkCommitmentDigestTest is Test { Digest[] memory digests = new Digest[](1); digests[0] = _consensus(HeaderImpl.APK_COMMITMENT_ID, hex"4102000000000000"); - (bool found,,) = _header(digests).apkCommitment(); - assertFalse(found, "a truncated payload should not decode"); + assertEq(_header(digests).apkCommitment().setId, 0, "a truncated payload should not decode"); } } diff --git a/evm/tests/foundry/BlsApkBeefy.t.sol b/evm/tests/foundry/BlsApkBeefy.t.sol index 21e216704..b4a988354 100644 --- a/evm/tests/foundry/BlsApkBeefy.t.sol +++ b/evm/tests/foundry/BlsApkBeefy.t.sol @@ -30,7 +30,8 @@ contract BlsApkBeefyTest is Test { // False selects the basic ciphersuite, which is what substrate's BEEFY signs with. PoP // would hash the same message to a different point and fail with nothing to explain why. ApkProof apk = new ApkProof(address(new PlonkVerifier())); - client = new BlsApkBeefy(address(apk)); + // The fixture's proof carries para 4009's header, which is where its digest comes from. + client = new BlsApkBeefy(address(apk), 4009); } function _state() internal view returns (bytes memory) { diff --git a/evm/tests/foundry/vendor/ApkProof.sol b/evm/tests/foundry/vendor/ApkProof.sol index b7042fb86..02039424d 100644 --- a/evm/tests/foundry/vendor/ApkProof.sol +++ b/evm/tests/foundry/vendor/ApkProof.sol @@ -66,13 +66,27 @@ contract ApkProof { /** * Protocol-fixed seed point for APK aggregation. - * Computed as HashToG1(dst="gnark-apk-proofs", msg="apk-seed"). - * The circuit hardcodes this same constant; the contract adds it to the - * caller-supplied APK before passing to the PLONK verifier. + * + * Derived as hash-to-field(dst="gnark-apk-proofs", msg="apk-seed-coset") + * followed by the RFC 9380 SSWU map + isogeny onto E(Fp), WITHOUT cofactor + * clearing — the point is on the curve but deliberately NOT in the G1 + * subgroup. The circuit's incomplete point addition relies on this: the + * accumulator seed + Sigma(pk_i) stays in the coset seed + G1, disjoint from + * G1, so it can never collide with a public key or reach infinity + * (Ciobotaru et al., eprint 2022/1205, section 5.1). + * + * The G1ADD precompile below accepts it (EIP-2537 ADD checks on-curve only, + * not subgroup); the seed never reaches the pairing precompile, which sees + * only the caller-supplied APK. Mirrors apk.ProtocolSeed() in Go; the + * coordinates are locked by TestProtocolSeedVectors — regenerate both + * together or on-chain verification rejects every proof. + * + * Layout: X = SEED_0 || SEED_1[0:16], Y = SEED_1[16:32] || SEED_2 (48-byte + * big-endian coordinates). */ - bytes32 constant SEED_0 = 0x054abdb6c5522fe2f71d55922d6f674a4908d39e2b33efcc62520c0621ca0d6a; - bytes32 constant SEED_1 = 0x6d84ee717b7fb1cb5f46687265be01ce06e518322165fd114cdf6b4ab59eb45e; - bytes32 constant SEED_2 = 0x9289cc4f6f7948d6b680cef9ecc0e0e0f96bd59a578d58c33c0e10db9c25b5ad; + bytes32 constant SEED_0 = 0x19742ffba069554d8cacceb8ed5514b2ecf72cd7372d3414203338f4fd3b3cc7; + bytes32 constant SEED_1 = 0x42fb160f8eb5818422246de186e0814a0e0f5d1199876e646952fb74d39e0b34; + bytes32 constant SEED_2 = 0x042a8d48786adae7e0fccf4b0236c72e82343de94c9d12bf17d22bec9edbbe2b; /// BLS signature verification constants /// From 0a4c2fb3a7c553a11aa8319b0dd6fdfc970bc7b1 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Mon, 17 Aug 2026 14:01:55 +0100 Subject: [PATCH 31/48] rebuild the apk fixtures against the packed circuit and clear a commitment computed under the old one --- .../foundry/fixtures/apk-verifying-key.bin | Bin 49200 -> 49200 bytes .../foundry/fixtures/bls-apk-beefy-proof.hex | 2 +- .../foundry/fixtures/bls-apk-beefy-state.hex | 2 +- modules/pallets/beefy-apk-digest/src/lib.rs | 32 ++++++++++++++++++ parachain/runtimes/gargantua/src/lib.rs | 1 + 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/evm/tests/foundry/fixtures/apk-verifying-key.bin b/evm/tests/foundry/fixtures/apk-verifying-key.bin index b820d080227d2e86f65129624156429779d3352e..0493d494d4db3df3452911520c75ad4c61035a37 100644 GIT binary patch delta 538 zcmV+#0_FX%fCI3A1CSLN~ZcqOPkOTh!00aRF zzG_}tys9$t(~l7QxY4`dkL-MgIGgUq=cfO#jx%Eb00000000t`aWWr&2ZCbq>=}Vt z*e6*aH)kE_MGE&$6|O#1Wx zykd1O&RJrjb;t;&=!}{d>0?}-W@Y53|7P|lZ{43e;5ZGhC56VwQ49iYpz{sa-F6E9 z9#y%XTN5~?MgyNr6cmkr1ba(bFV?O{s2Jo{R5Fa*+sCrBo@ocNrb`S$(|g}%(}BKe z@{0l?oZeamsyoEi6yGvDSfQt@RJ^mL&I4eXJ@SW?Uy)O;u2Qg9SG*x^!JpI1{Z4Dt zCkraxO0?vfjI@Ki{}>IH01s+}5xkL=-n@YDhx4^EA|5f){GTs>rA;tlXx@~3ob`|G`5l=pvp~mTmK6em^CboYIw*vi+ox<5;a)R@r7;zsC z8)s^EdXMuuAsEp(bHIwt00001j?dtrIzf)Tld)5}!WxPq9g*n=ZfoA^Q_#GT4$rMn c(2BVYnViEA@SH~gDmTd_vor$vy#pPPhe7K8DgXcg delta 538 zcmV+#0_FX%fCI3A1CSLPX{zc*1m^hg1_gLS-QB4mi$JKDJSrXKID(ITwu^m4x|VY9ge>Q@?VCYDWjJObGspsfOt@z6$w zcfvixrl`F?j)M9SLi8_xyaXM}q~t04C}1;`2zChwCWeGhkN+Upy0nlq>Q)$+O-Xkn zZl}T^wpRVTIX49gGuR#hrSR>_5NNrUFj~WeYC^5Gugh2aGOWlq%}8^DPIHJ5f3of# z$AR(bchAC8OB_asr~YWR35--ev8&gd)25MLUrwtVtlZ22h!DGftK?nA@56a)K!lIp zaqBN3I>g7SPmGoM2oku@Mge-d+WX&%EMqVBgzc6v&$S=dHR)(1ABq$qQzQjw$x@W8 zK+a(o;I7HS6Pl#DEeF0#PODyq@ZUvfewV}{wzu_oo$=fpso-By{{U(R24&7=eHR*6 z`l|#C^j&&3>!2JsjC0wN00001mXkJHEk6z;TaU|1umAd3?67&12dT{_i;S?TS*qbV coVkt|fgEGGm1r1X=CzR9vor$vy#r3fBA3|xO8@`> diff --git a/evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex b/evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex index f3f581bbb..5bf08f683 100644 --- a/evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex +++ b/evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex @@ -1 +1 @@ -0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a600000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000478237b877864af98fe7a2d05f4cc0d78eb019d094da6d70c242f95609ee577c42ca62abc23bb3594dc8f5a3516e5c610cbe3f263a5dc33d9d8d0bc631567cf630c4df86e4ad78887f62f46e1e0daa52bd58eee1613cf211518e561b582b0990868ddaca3321f5ac0f7c57e4bf34b86726e874e5ae4476a08f4c74c21fcb4a9ee94ee8572a6fb82afba59f47745e9d51436fc8946294306c5448880df4c41f82a136985e8963070f035906bcc766f9e9125d3ea66cbc1b293ca2ef37a6c581812dd734fad4bd53feef8ac36b393bfd78ba789203a056f9cafcfc91e8e9afc5a64b724f54d18e983996397faa28bf63713d81c18fec214be9a4536400258d08c9f34acb58680d7b7150f23aadd0e10da0d1e7dfe476a2029a700545fe7a1001000000000000000000000000000000000000000000000000000000000000004a0076ce06f16ec8c703642f8fb98f0d40ba4cbd1b126c773c355eba509bbe1903131ace18227659d204385349ef2ba9b6a1786183ea05bfef0d2404415638232898fda49aca75d4dd1df841b49f5106287246f2f8e77fb6dbdbf91565d28987069000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003a82c2163d0b9bfb3902ccf0f96cd24bd11cb106816235dfb84f5b1a127a14c324b00000000000000000000000000000000000000000000000000000000000000330000000000000000000000000000000000000000000000000000000000000002d1cbb453472425036cef3caa539a0e81c6bd9fd9c3f24de7b38867380a19cafba39de86448087761c4970ffadbbba8ee46dbce931cfe74d316623642aba8abf400000000000000000000000000000000000000000000000000000000000003a80000000000000000000000000000000000000000000000000000000000000960000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003a90000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d6800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020336afc2238d61c3f4bd6a64c73d654b3e05c4bfac8df0b61e23268adbb76b67900000000000000000000000000000000000000000000000000000000000004a017ff42ed7d1bb31837a31633ecf1d0b678c2e595ef503df68adefcea79b93d18925c91639a428c9112be06c1fdd2cc770ffed9cfa07f38361b38db2d7865213e1a93a734ed7de2e167044cca861b2c7376ed419e669a7e7ea719434151d5ce1415308f51cdca7b40566fea1343421fe98bcc49ca3e3a6248cb8c9fef4dc096f05e2f37d63fda9122b433ca457503ad0b18a472c676d71fe47bfe0159442ed230672e577c7386a3e6915fc72e8219fa34e58da1a580c010611055f7992b09b7a11701ab3f02f965bd4b33f821bcc1d0ba65fe5a3080ba91ded133e60bc16590f62adaae55517d16311cb05cd52550a3d501a754aea61366ac04d32867d9ba72a882d35a433d665042c62689443f5c3c110f0ff1676763e87ad8613a8cc0be199d082190a576457f4d4a40a7bdbc55d5d88e754ed0124d0cc285cf9ba45660c24be3edf736ce717ea048a205e66c09258f16ce691eb1f70efc86b607d4b05f73ff2d64eb45f754dff9e2029f263ad5581e8b33d0df7bd41a65561155189350c9b40a28938e3f62ffb43d897a94e8bebd91cdbc45fb0f640a98ec9015d4400c8a87b11192090b4726bd6013f2a98b939b0f1907f9beeb1ffb59d3f59485af511eba9d1556522d34af123f9dbf313d83afdf256956e6a2382d7229548e10987e3a370160bf63423912c051cca014e9a327f7ae9e403819a4b4a0418e1b532cf14ded81fc6693df0c0bda2f370c871a9b654e0f7f8abbf94684f0dfee422ad6661dd3242e09ea919528e18d3a18251a238fab287c6a13d013f97af4369fc15833dd4e6dd27fb26bbd96d91263d59fc64ddef222631731394c9d2178f56ad871f696782a2bffb8d3a6b6bd940284338bec13bd6019d0e334136c3fef8b603f7798b8f335a535b73ee19d3a132eb2c5c96ec820e380a5c5f2a66a12fdf005843d275c5373c6dfa5829c57d294f66eac52117cd509db78760642751a5e40f09fe79e343a164e6f31ba4b59392d41295cb74fa4353475d212be2142c7ebb38a69e750180e1951a98e21e5c6b7a400394f58146c9fceb1a39714657f96d84ac89eda57ca44b179cc6b8d5fce98a3f8780aa67f2148128c07b5d082d2cb8805ca9614bfe9bfc0ed95273e0c0bdbe35cc90647b481ec8663e6075a7d3fb5114fb43b6f1a3afe70c7dfb930a4221f1928c98ff85a615b185b6ff8d9e76ab1b6118aa962dcc536008ab3f47096bbe7884cf5a8bfde55e4775aeba68cd5dc3db886147cd909f18577058f4f0307015b02307d7d3061d5d017ff0f20c5c075a8c109fa82a08fdefe4a2a4234c8b997c444a1416af3b20284ba3da1e27c25b8363d01b891512b11350ec229c5aff71c0ad72c072c22ad7857dcbb2cc9677d780fccf1c2e068f4cd8c790cbdac26d69120b112a5f9501f8c49189074d88fb98be727bd2b9f46720c0b473a4e8270b9fb62d76b21f839e88d4c539b1586d77c0770a287b7413b46b06c574a090dcd98f5527e9047f9ca66990f43c6558dfd4ebd3fb5a38265419f8e8d054114f3ff529dc8d509b3e795980a48c26ddd2cdf7b9649f249f770152b978311d4c2629fdd8613e9cf8defb50fd80c180d30a8690f277eabd0e002a6bedde0395f188e5416a5a7a6cb89ab5b67fda36abc51a1fe4cb594c4070542e569dc4e000000000000000000000000000000000000000000000000000000000000000571b72c82f673e768cf1cf43375c6b23b690a5254027366eb8b967dd25cb5a3d6a884b6a78a68c9a081ed6cf6f8c5cde2ce7124594514bebd4c23dd4d162d120c177dc388884191d00e7a92441a1f4049b1df22088e41234dc92c55c74b0fe65545d603612b70e2a308c661e845768e6d1a16abe028d83605454da932e1f65543cdf4544c0087c0bf78cdd85bf57f288484f3d9e67ff592c889aaa631357371390000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa9000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001358c3f177f92982c68fa65022801eea1cf50f545c9c3a29b9455d02eb911cc473a95062970f1431d09737f51b51362a1fa1f45b519c3332879d69e13dd0b7e4b27bddd759d30c71c0768846409fd48abc0bca8ebdeed3162701b99d394847cdc4b87ea14066175726120d989be11000000000452505352883efe9561cb71b9a2bddbcd15366e4aff55699386be14d934c20dc2854767d364850e0449534d5001010000000000000000000000000000000000000000000000000000000000000000bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a044953544d20163b776a0000000005617572610101260a820416df3d93202d7bbf7cd28719f35b8557cec6c453e1606007adf5822c94949cf67811b133bf40f09f7133292dbd25303ab4fdf5e74325f9a9a66b8c8f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file +0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000478237b877864af98fe7a2d05f4cc0d78eb019d094da6d70c242f95609ee577c42ca62abc23bb3594dc8f5a3516e5c610cbe3f263a5dc33d9d8d0bc631567cf630c4df86e4ad78887f62f46e1e0daa52bd58eee1613cf211518e561b582b0990868ddaca3321f5ac0f7c57e4bf34b86726e874e5ae4476a08f4c74c21fcb4a9ee94ee8572a6fb82afba59f47745e9d51436fc8946294306c5448880df4c41f82a136985e8963070f035906bcc766f9e9125d3ea66cbc1b293ca2ef37a6c581812dd734fad4bd53feef8ac36b393bfd78ba789203a056f9cafcfc91e8e9afc5a64b724f54d18e983996397faa28bf63713d81c18fec214be9a4536400258d08c9f34acb58680d7b7150f23aadd0e10da0d1e7dfe476a2029a700545fe7a1001000000000000000000000000000000000000000000000000000000000000004a007a3bbf651a6efc01fe37e0370c74b18c76e9f5915212a2788af56916a94ab253c75417d6cd6af074f0b874b776a9ebf0a84b90d3bfaa8a785bf84bc1dbba61fca8529a8095566581d0e850ab0c84e41a783866f66318b174fe677844d628c580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cea139807d981ce3de2cc68a5cb30192a703df280bcba7a43f0e7ec31ba5083a6cd60000000000000000000000000000000000000000000000000000000000000c950000000000000000000000000000000000000000000000000000000000000002d1cbb453472425036cef3caa539a0e81c6bd9fd9c3f24de7b38867380a19cafbee367e5c724882cb248462660bc295655c4adf0a5c4e506d0bbf200d4ad992fd000000000000000000000000000000000000000000000000000000000000cea100000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000cea20000000000000000000000000000000000000000000000000000000000000c94000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d6800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020ab4fc7e08d1516f91d284155b64822f7a7209218d6855ddc7a21df7a54cc7d3200000000000000000000000000000000000000000000000000000000000004a0185e65ae41f08d2573dde4132033fcea9bd89945cef1d1490f95a462b969fbb4b090eb5665edab5bd168038779247fef0be93c639b4055537578770fcccf2c7b24ec6c4814a2d7df1fedd5f1dd17a7805fac2da24375dbed726f2ac5dda9de470565de94f494a48971696893aee9e8782b49d8e53cbcb342781dcf8c2482bd88b1db484e1dc529cfe6a3ca825f982502028f2739d993170b4c44b5cb02a4659ced3851b0277d4b6e1fd9db2024a29ec953cf3f9216ba605c7d33fb8403f08763125144cbffe208264650e49dacd0b941bc3833442205bb3e81b373443f9fb6ea3d1a54b6c8a2380148d55359b2823ec915a2d68ada1f1c9dbdfbf0348884bab8b20a243df308485755890eb8c073309dc95d8de61c2e5635c868551c44ca0e240d6ee324a56613ab5a2361cb19654bf628d16d851842b6655ed6e8488332bb16e69cec25ccb4d667e806d6ce861a22aa006f2128bf8b83be85f6492931f40a5d666354e16b9e9a7d40ab956bccc7d666e01ab658ab0d7f155b3ab94ee3a3ff25106fd9455e1a8558ca0b88c41071146fc22376e518032da4275d5151a4ebb4d142068d839359111d96b1947c5c00f30e188bc5ff80446f7a1bf7b4a65038a31d6aa39ab7155c946ee3d53627b9e3ea4fd3c4dbade70ac9a4c06229be1f6ee72e05894d728c5c09e92aa012e55c2668d45ea9856a318bb7121a316dd2d1b5d69093b35fda0156355481834b65630662ff0a5fc7425b0707ab48e798a486a57180f9ede7f9d63bc218bc0fdc38ae33912d3b767e64054f199ee6f51a5fddb95acb23a3df069c80686478db2988afe5e296ba8ad677637eea7cf54b6a21c964ed044de4fadf835d2fdda99e4e6bd04efc0213617481479a99bab16d5f63a3ba03cb33e9e6361ce97763ff5aa4ae7b16504701633e979aa368127a5d3d98f2b4f0563955e597d60a847b9218ced941b7f1aead2b914c24127d76bb5c7ddfcf92789338f7138990d5a9a1190a7076bf23c8b6a8c118b524fd480098ac104b61973c220e0d3c19a6c0a8d3063244fc9e3473bf0928b93d77934716eb52332c3dd45184614e9cb57d0536f0c7e598b67a5216e40fff5ce398dced77721884fb9b54d12a550d176b8a66976ebfd45d7bcfe4a8195fa1f6d31af2374f12b454c7a88ddbbd3e05345c8f5a6d9e41dfbd363630abf7e1265ce7155e72c64df8861521c4257119de3b385be9f3689e2bef572a8bb98d2c499332f8ebc59eec3fa67d42a6797e67d8bf770f329d2ed9c63349d96c5acf005747f5feaf6001fe5989c4e5bd06a45d9a17abb84cc0ac9fcbbc52c97499f7f1c704a7aa96114cbbbf3d44ab74a5c606fb8ba3058e452d8208469d23747c3e741f85c9a9efd0b2289a8c12271c70ed61a41190f61e64b6151ab092844a3ce80f3d471daf39c8f0f274554e7f8654ae2d544e3786cbf878919a8a7ef737603883ede4e710c667f9760d5cd23682daf7093cd588bd2a3980a8f2856489a06d9a07a67d1c582f167b68170e011f2c3cfc18c3c55a7c7fe3d9946c84aee503617419d17ed574c5b02a477a857d1300ad6e5f26a0da49f2fb3282ad61a9510e04ea19495f99dbb72ea9bd3cd7772a4a95732d08076aca1f6205b939877c4785a6a9d63216956dbfb6db53eccfec2eedcb1700000000000000000000000000000000000000000000000000000000000000081f78d05de33bffdf649b225755be3ae579b9fe2c1ae2e73295ee3c1904749320ca25bfe03fb0fedfccfaf1f200f3b4e22a5ddbd0293584456e1a92ea1ec2ff4c4cabe2d0c561af881878ffc013b6571b30b3706d6f277562b8cbfb554fc687df5de7a8c210f658f6e9cb86a281b9678670d5979ec4d51b8afb1026bc13803f67a1e54b4f5f6cd45c6dd2358f4d818b87567cec11ed5e24248ce5900f951252000b0eb10b670b86e81f9436bbf3182bc0f1288e16e3b3f06959785e4f54b23376fdf849f6d2da710f81057cc27c823a69d2cf307ae5c5e4b90f1f9012b15761799267ea908b4ec6d875861e235ee2f18fc1e03d4efa33fa3d2486b940807b31f8000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000167122eb76026540e83a8bae5a06a4c0678f65cb9424e8da84a18fb045e06a399f25aa7010092737eeae54ee05dee6fd6f608a989c6bddb405f914f6350852e54e75c2d32314a4c1da04ca749c8a601667941b5118ff02617388b94d32df5df6976ec31193a180661757261205b80c011000000000452505352906a053b20cb874226c3e4d8e6d51026b29b86153fc14f043796352f563f4d0129723a03000449534d5001013ebf160e4a9178592e46584385a6eaa86d06c7f4d6a1fe9ca8976fbe379286199781e23ba03a6c017ebe6dfd9d23459b90aa76e95d60837ab7ef0177f468bdd8044953544d202202836a000000000441504b43a0940c00000000000013ba45ba08c2f9519d727dbefbc799cc1396a6c03f6438003c2c3950cab05e250561757261010124e68e3825a619966b562cdab9790ebe89bf1df354e14c2e90d6861955aa4b6f7b4f90642726f7a35eba0f2e51720d3f9a21efac1b7f52d8edb8315f7619dc8f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex b/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex index cf691370b..644763152 100644 --- a/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex +++ b/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex @@ -1 +1 @@ -0x00000000000000000000000000000000000000000000000000000000000003a40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000026d4f3896e0cfadcff125976f8af0cb60caf60992ddef1a384e1c76dab0150cab \ No newline at end of file +0x000000000000000000000000000000000000000000000000000000000000ce9e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c93000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c94000000000000000000000000000000000000000000000000000000000000000213ba45ba08c2f9519d727dbefbc799cc1396a6c03f6438003c2c3950cab05e25 \ No newline at end of file diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index 3b7d955fc..10bd22e66 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -73,7 +73,12 @@ pub mod pallet { type WeightInfo: WeightInfo; } + /// Bumped whenever the commitment changes shape, so [`migration`] knows to throw away what + /// was published under the old one. + pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); /// The last commitment published to a header digest, and the set it describes. @@ -229,6 +234,33 @@ impl WeightInfo for () { } } +/// Throw away a commitment computed under an older scheme. +/// +/// A commitment is only recomputed when the membership changes, so a runtime upgrade that changes +/// how keys are hashed would otherwise keep republishing the old value indefinitely, on any chain +/// whose validators happen to stay the same. The stored digest cannot notice: it describes the +/// keys, not the arithmetic applied to them. Clearing the record forces one recomputation and the +/// pallet carries on from there. +pub mod migration { + use super::*; + use frame_support::traits::{Get, GetStorageVersion, OnRuntimeUpgrade}; + + pub struct ClearStaleCommitment(core::marker::PhantomData); + + impl OnRuntimeUpgrade for ClearStaleCommitment { + fn on_runtime_upgrade() -> Weight { + if as GetStorageVersion>::on_chain_storage_version() >= + pallet::STORAGE_VERSION + { + return T::DbWeight::get().reads(1); + } + Published::::kill(); + pallet::STORAGE_VERSION.put::>(); + T::DbWeight::get().reads_writes(1, 2) + } + } +} + /// What to do with the commitment this block. #[derive(Debug, PartialEq, Eq)] pub enum Step { diff --git a/parachain/runtimes/gargantua/src/lib.rs b/parachain/runtimes/gargantua/src/lib.rs index cdd14a37a..edff09cf2 100644 --- a/parachain/runtimes/gargantua/src/lib.rs +++ b/parachain/runtimes/gargantua/src/lib.rs @@ -189,6 +189,7 @@ pub type Migrations = ( pallet_beefy_consensus_proofs::migrations::ClearSp1VkeyHash, pallet_beefy_consensus_proofs::migrations::ClearAcceptedProofHashes, pallet_collator_manager::migrations::MigrateBondsToReserves, + pallet_beefy_apk_digest::migration::ClearStaleCommitment, ); /// Handles converting a weight scalar to a fee value, based on the scale and granularity of the From 8b1f9b1e10b041d26f9391344fd3a225b51c1313 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Mon, 17 Aug 2026 14:28:03 +0100 Subject: [PATCH 32/48] drop the duplicated hash to curve, consensus state and bitlist padding, and split the ecdsa path out of the crate root --- evm/src/consensus/BlsApkBeefy.sol | 42 ++-- evm/src/consensus/BlsHashToCurve.sol | 158 -------------- evm/src/consensus/Types.sol | 25 --- evm/tests/foundry/BlsApkBeefy.t.sol | 6 +- evm/tests/foundry/BlsHashToCurve.t.sol | 79 ------- modules/consensus/beefy/verifier/src/apk.rs | 26 ++- modules/consensus/beefy/verifier/src/ecdsa.rs | 195 ++++++++++++++++++ modules/consensus/beefy/verifier/src/lib.rs | 158 +------------- modules/ismp/clients/beefy/src/consensus.rs | 2 +- 9 files changed, 250 insertions(+), 441 deletions(-) delete mode 100644 evm/src/consensus/BlsHashToCurve.sol delete mode 100644 evm/tests/foundry/BlsHashToCurve.t.sol create mode 100644 modules/consensus/beefy/verifier/src/ecdsa.rs diff --git a/evm/src/consensus/BlsApkBeefy.sol b/evm/src/consensus/BlsApkBeefy.sol index 50973a0ae..3a2d93191 100644 --- a/evm/src/consensus/BlsApkBeefy.sol +++ b/evm/src/consensus/BlsApkBeefy.sol @@ -23,10 +23,10 @@ import {ScaleCodec} from "@polytope-labs/solidity-merkle-trees/src/trie/polkadot import {Codec} from "./Codec.sol"; import { - ApkAuthoritySet, + AuthoritySetCommitment, ApkDigest, BlsApkBeefyConsensusProof, - BlsApkConsensusState, + BeefyConsensusState, BlsApkRelayChainProof, BeefyMmrLeaf, Commitment, @@ -68,7 +68,7 @@ interface IApkProof { * The commitment the proof is checked against does not come from the relay chain's MMR leaf, the * way the keyset root does. Hyperbridge computes it over the relay's next authority set and * publishes it in a header digest, so a client picks it up from a header it has already verified - * and carries it in its consensus state. That is what `ApkAuthoritySet.apkCommitment` holds, and + * and carries it in its consensus state. That is what `AuthoritySetCommitment.root` holds, and * why the state has to be seeded with the starting set's commitment at initialisation. * * Requires Prague for the EIP-2537 precompiles. @@ -116,7 +116,7 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { view returns (bytes memory, IntermediateState[] memory, uint256) { - BlsApkConsensusState memory consensusState = abi.decode(previousState, (BlsApkConsensusState)); + BeefyConsensusState memory consensusState = abi.decode(previousState, (BeefyConsensusState)); (BlsApkRelayChainProof memory relay, ParachainProof memory parachain) = abi.decode(proof, (BlsApkRelayChainProof, ParachainProof)); @@ -125,7 +125,7 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { return (abi.encode(consensusState), new IntermediateState[](0), consensusState.nextAuthoritySet.id); } - (BlsApkConsensusState memory newState, bytes32 headsRoot) = verifyMmrUpdateProof(consensusState, relay); + (BeefyConsensusState memory newState, bytes32 headsRoot) = verifyMmrUpdateProof(consensusState, relay); (IntermediateState[] memory intermediates, ApkDigest memory digest) = verifyParachainHeaderProof(headsRoot, parachain, _digestParaId); @@ -133,19 +133,19 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { // not have keys for yet. Picking it up here is what lets the next update be verified at // all, and is the reason the digest names the *next* set rather than the current one. if ( - digest.setId == newState.nextAuthoritySet.id && newState.nextAuthoritySet.apkCommitment == 0 + digest.setId == newState.nextAuthoritySet.id && newState.nextAuthoritySet.root == bytes32(0) ) { - newState.nextAuthoritySet.apkCommitment = digest.commitment; + newState.nextAuthoritySet.root = bytes32(digest.commitment); } return (abi.encode(newState), intermediates, newState.nextAuthoritySet.id); } /// @dev Verify the signed mmr root, then roll the authority sets forward. - function verifyMmrUpdateProof(BlsApkConsensusState memory trustedState, BlsApkRelayChainProof memory relayProof) + function verifyMmrUpdateProof(BeefyConsensusState memory trustedState, BlsApkRelayChainProof memory relayProof) internal view - returns (BlsApkConsensusState memory, bytes32) + returns (BeefyConsensusState memory, bytes32) { Commitment memory commitment = relayProof.commitment; if ( @@ -156,12 +156,12 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { } bool isCurrent = commitment.validatorSetId == trustedState.currentAuthoritySet.id; - ApkAuthoritySet memory authoritySet = isCurrent ? trustedState.currentAuthoritySet : trustedState.nextAuthoritySet; + AuthoritySetCommitment memory authoritySet = isCurrent ? trustedState.currentAuthoritySet : trustedState.nextAuthoritySet; // A set whose commitment has not been learned from a digest yet cannot be verified against. // Reverting here is deliberate: silently accepting would mean checking the proof against a // zero commitment. - if (authoritySet.apkCommitment == 0) revert MissingApkCommitment(); + if (uint256(authoritySet.root) == 0) revert MissingApkCommitment(); verifySignedByApk(Codec.Encode(commitment), relayProof, authoritySet); @@ -180,10 +180,10 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { trustedState.currentAuthoritySet = trustedState.nextAuthoritySet; // The incoming set's size comes from the mmr leaf, but its APK commitment is not known // until a header digest supplies it, so it starts empty. - trustedState.nextAuthoritySet = ApkAuthoritySet({ + trustedState.nextAuthoritySet = AuthoritySetCommitment({ id: relayProof.latestMmrLeaf.nextAuthoritySet.id, len: relayProof.latestMmrLeaf.nextAuthoritySet.len, - apkCommitment: 0 + root: bytes32(0) }); } trustedState.latestHeight = commitment.blockNumber; @@ -202,7 +202,7 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { function verifySignedByApk( bytes memory encodedCommitment, BlsApkRelayChainProof memory relayProof, - ApkAuthoritySet memory authoritySet + AuthoritySetCommitment memory authoritySet ) internal view { uint256 signed = countSigners(relayProof.bitlist); if (!checkParticipationThreshold(signed, authoritySet.len)) revert SuperMajorityRequired(); @@ -212,7 +212,7 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { // `verify` reverts on failure rather than returning false, so a successful call is the // whole result. Wrapped so the reason surfaces as this contract's error. try _apk.verify( - authoritySet.apkCommitment, + uint256(authoritySet.root), relayProof.bitlist, relayProof.apk, relayProof.apkProof, @@ -226,9 +226,15 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { /// @dev Population count over the bitlist. Fixed cost regardless of how many signed, which is /// the point of the whole scheme. + /// + /// The 1024 slots are packed 250 to a word across the first four and 24 into the last, which + /// is what the circuit decomposes with `ToBinary`. Bits above those ranges are constrained to + /// zero there, so a proof carrying any would not verify; masking them off here keeps the count + /// honest on its own terms rather than relying on that. function countSigners(uint256[5] memory bitlist) internal pure returns (uint256 count) { for (uint256 w = 0; w < 5; w++) { - uint256 word = bitlist[w]; + uint256 width = w == 4 ? 24 : 250; + uint256 word = bitlist[w] & ((uint256(1) << width) - 1); while (word != 0) { word &= word - 1; count++; @@ -243,7 +249,7 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { /// @dev The signed mmr root must attest to the leaf carrying the parachain heads. function verifyMmrLeaf( - BlsApkConsensusState memory trustedState, + BeefyConsensusState memory trustedState, BlsApkRelayChainProof memory relay, bytes32 mmrRoot ) internal pure { @@ -316,5 +322,5 @@ contract BlsApkBeefy is IConsensusV2, ERC165 { /// @dev Only here so the structs appear in the ABI, which is what the Rust bindings are /// generated from. `verify` takes bytes, so without this they would be invisible. - function noOp(BlsApkConsensusState memory s, BlsApkBeefyConsensusProof memory p) external pure {} + function noOp(BeefyConsensusState memory s, BlsApkBeefyConsensusProof memory p) external pure {} } diff --git a/evm/src/consensus/BlsHashToCurve.sol b/evm/src/consensus/BlsHashToCurve.sol deleted file mode 100644 index bebb4b992..000000000 --- a/evm/src/consensus/BlsHashToCurve.sol +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) Polytope Labs Ltd. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -pragma solidity ^0.8.30; - -/** - * @title Hashing a BEEFY commitment onto the BLS12-381 G1 curve. - * @author Polytope Labs (hello@polytope.technology) - * - * @notice Reproduces exactly what substrate's BEEFY signers do, so an aggregate BLS signature can - * be checked on chain. This is the step most likely to be got wrong, because BEEFY does not use - * the IETF ciphersuite string the way a reading of RFC 9380 alone would suggest. - * - * @dev `w3f-bls` builds its hasher with a **one byte domain separation tag of 0x01**, and prepends - * the ciphersuite string to the *message* instead of using it as the tag: - * - * suite = "BLS_SIG_" || "BLS12381" || "G1" || "_XMD:SHA-256_SSWU_RO_" || "NUL_" - * preimage = suite || context || message // context is empty for BEEFY - * point = hash_to_curve(preimage, DST = 0x01) - * - * Using `suite` as the DST, which is what a textbook implementation does, yields a completely - * different point and a silently failing pairing check. `bls_hash_to_curve_vector` in the Rust - * verifier pins a test vector that this library is checked against. - * - * Everything after that composition is standard RFC 9380 and maps onto EIP-2537 precompiles. - * Note BEEFY puts signatures in G1 and public keys in G2, the opposite of the Ethereum - * convention, so an eth2 BLS library does not transfer. - */ -library BlsHashToCurve { - /// @dev EIP-2537 BLS12_G1ADD - address internal constant G1_ADD = address(0x0b); - /// @dev EIP-2537 BLS12_MAP_FP_TO_G1 - address internal constant MAP_FP_TO_G1 = address(0x10); - /// @dev Big-endian modular exponentiation (EIP-198), used to reduce a wide integer mod p. - address internal constant MOD_EXP = address(0x05); - - /// @dev The BLS12-381 base field modulus, 48 bytes big-endian. - bytes internal constant FIELD_MODULUS = - hex"1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab"; - - /// @dev The ciphersuite `w3f-bls` prepends to the message. Not the domain separation tag. - bytes internal constant CIPHER_SUITE = "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; - - /// @dev `DST_prime` = DST || I2OSP(len(DST), 1), with the one byte DST `w3f-bls` uses. - bytes internal constant DST_PRIME = hex"0101"; - - error MapToG1Failed(); - error G1AddFailed(); - error ModExpFailed(); - - /** - * @notice Hash a SCALE-encoded BEEFY commitment onto G1, exactly as its signers did. - * @param commitment the SCALE-encoded commitment - * @return a G1 point, 128 bytes, as EIP-2537 encodes them (x || y, each 64 bytes) - */ - function hashCommitmentToG1(bytes memory commitment) internal view returns (bytes memory) { - return hashToG1(bytes.concat(CIPHER_SUITE, commitment)); - } - - /** - * @notice RFC 9380 `hash_to_curve` for BLS12-381 G1 with the DST `w3f-bls` uses. - * @dev Cofactor clearing is linear, so it makes no difference that MAP_FP_TO_G1 applies it per - * point rather than once after the addition. - */ - function hashToG1(bytes memory preimage) internal view returns (bytes memory) { - (bytes memory u0, bytes memory u1) = hashToField(preimage); - return g1Add(mapToG1(u0), mapToG1(u1)); - } - - /** - * @notice RFC 9380 `hash_to_field` producing the two field elements the map consumes. - * @return u0 and u1, each 64 bytes: a 48 byte big-endian value left padded to 64, which is how - * EIP-2537 wants field elements. - */ - function hashToField(bytes memory preimage) internal view returns (bytes memory u0, bytes memory u1) { - // L = ceil((ceil(log2(p)) + k) / 8) = ceil((381 + 128) / 8) = 64, and we need two of them. - bytes memory uniform = expandMessageXmd(preimage, 128); - - bytes memory wide0 = new bytes(64); - bytes memory wide1 = new bytes(64); - for (uint256 i = 0; i < 64; ++i) { - wide0[i] = uniform[i]; - wide1[i] = uniform[64 + i]; - } - - u0 = toFieldElement(wide0); - u1 = toFieldElement(wide1); - } - - /** - * @notice RFC 9380 section 5.3.1 `expand_message_xmd` with SHA-256. - * @dev Written for `lenInBytes` a multiple of 32 and at most 255 blocks, which covers the only - * caller (128 bytes, so four blocks). - */ - function expandMessageXmd(bytes memory message, uint16 lenInBytes) internal pure returns (bytes memory) { - uint256 ell = (lenInBytes + 31) / 32; - - // msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime - bytes memory zPad = new bytes(64); // SHA-256 block size - bytes memory msgPrime = bytes.concat(zPad, message, bytes2(lenInBytes), hex"00", DST_PRIME); - - bytes32 b0 = sha256(msgPrime); - bytes32 bi = sha256(bytes.concat(b0, hex"01", DST_PRIME)); - - bytes memory out = bytes.concat(bi); - for (uint256 i = 2; i <= ell; ++i) { - bi = sha256(bytes.concat(b0 ^ bi, bytes1(uint8(i)), DST_PRIME)); - out = bytes.concat(out, bi); - } - - return out; - } - - /// @notice Reduce a 64 byte big-endian integer mod p, returned padded to the 64 bytes - /// EIP-2537 expects for a field element. - function toFieldElement(bytes memory wide) internal view returns (bytes memory) { - // modexp(base = wide, exponent = 1, modulus = p) is just `wide mod p`. - bytes memory input = bytes.concat( - bytes32(uint256(64)), // base length - bytes32(uint256(1)), // exponent length - bytes32(uint256(48)), // modulus length - wide, - hex"01", - FIELD_MODULUS - ); - - (bool ok, bytes memory reduced) = MOD_EXP.staticcall(input); - if (!ok || reduced.length != 48) revert ModExpFailed(); - - // EIP-2537 field elements are 64 bytes: 16 zero bytes then the 48 byte value. - return bytes.concat(new bytes(16), reduced); - } - - /// @notice EIP-2537 `MAP_FP_TO_G1`: field element to a G1 point, cofactor already cleared. - function mapToG1(bytes memory fieldElement) internal view returns (bytes memory) { - (bool ok, bytes memory point) = MAP_FP_TO_G1.staticcall(fieldElement); - if (!ok || point.length != 128) revert MapToG1Failed(); - return point; - } - - /// @notice EIP-2537 `G1ADD`. - function g1Add(bytes memory a, bytes memory b) internal view returns (bytes memory) { - (bool ok, bytes memory sum) = G1_ADD.staticcall(bytes.concat(a, b)); - if (!ok || sum.length != 128) revert G1AddFailed(); - return sum; - } -} diff --git a/evm/src/consensus/Types.sol b/evm/src/consensus/Types.sol index 458c4bafb..8a9a3384e 100644 --- a/evm/src/consensus/Types.sol +++ b/evm/src/consensus/Types.sol @@ -187,31 +187,6 @@ struct ApkDigest { uint256 commitment; } -struct ApkAuthoritySet { - /// Id of the set. - uint64 id; - /// Number of validators in the set, for the two-thirds threshold. - uint32 len; - /// Poseidon2 commitment over the set's G1 public keys, as `ApkProof.verify` takes it. - uint256 apkCommitment; -} - -struct BlsApkConsensusState { - /// block number for the latest mmr_root_hash - uint256 latestHeight; - /// Block number that the beefy protocol was activated on the relay chain. - uint256 beefyActivationBlock; - /// authorities for the current round - ApkAuthoritySet currentAuthoritySet; - /// authorities for the next round - ApkAuthoritySet nextAuthoritySet; -} - -// A BEEFY relay chain proof verified by a SNARK over the aggregate public key, rather than by a -// merkle multi-proof of each signer's key. -// -// The saving is that nothing here grows with the number of signers: the bitlist is fixed width and -// the proof is constant size, where the merkle path costs roughly 17k gas per signer. struct BlsApkRelayChainProof { // A commitment to the finalized state Commitment commitment; diff --git a/evm/tests/foundry/BlsApkBeefy.t.sol b/evm/tests/foundry/BlsApkBeefy.t.sol index b4a988354..cee556d16 100644 --- a/evm/tests/foundry/BlsApkBeefy.t.sol +++ b/evm/tests/foundry/BlsApkBeefy.t.sol @@ -5,7 +5,7 @@ import {Test, console} from "forge-std/Test.sol"; import {IntermediateState} from "@hyperbridge/core/interfaces/IConsensusV2.sol"; import {BlsApkBeefy} from "../../src/consensus/BlsApkBeefy.sol"; -import {BlsApkConsensusState} from "../../src/consensus/Types.sol"; +import {BeefyConsensusState} from "../../src/consensus/Types.sol"; import {ApkProof} from "./vendor/ApkProof.sol"; import {PlonkVerifier} from "./vendor/PlonkVerifier.sol"; @@ -47,8 +47,8 @@ contract BlsApkBeefyTest is Test { function test_verify_live_proof() public view { (bytes memory newStateBytes, IntermediateState[] memory intermediates,) = client.verify(_state(), _proof()); - BlsApkConsensusState memory newState = abi.decode(newStateBytes, (BlsApkConsensusState)); - BlsApkConsensusState memory oldState = abi.decode(_state(), (BlsApkConsensusState)); + BeefyConsensusState memory newState = abi.decode(newStateBytes, (BeefyConsensusState)); + BeefyConsensusState memory oldState = abi.decode(_state(), (BeefyConsensusState)); assertGt(newState.latestHeight, oldState.latestHeight, "height should advance"); assertEq(intermediates.length, 1, "should finalize the registered parachain"); diff --git a/evm/tests/foundry/BlsHashToCurve.t.sol b/evm/tests/foundry/BlsHashToCurve.t.sol deleted file mode 100644 index 07c32a419..000000000 --- a/evm/tests/foundry/BlsHashToCurve.t.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.30; - -import {Test} from "forge-std/Test.sol"; -import {BlsHashToCurve} from "../../src/consensus/BlsHashToCurve.sol"; - -/** - * @title Cross-language check of BEEFY's hash-to-curve. - * - * @notice The expected values come from `bls_hash_to_curve_vector` in the Rust verifier, which - * generates them through the very `w3f-bls` code path the relay chain signs with. If these two - * disagree, an on-chain aggregate BLS check would fail against real chain output while looking - * perfectly correct in isolation. - * - * Needs EIP-2537, so run under an EVM version of at least Prague: - * FOUNDRY_PROFILE=bls forge test --match-contract BlsHashToCurveTest -vv - */ -contract BlsHashToCurveTest is Test { - /// The message the Rust vector was generated for. - bytes constant MESSAGE = "beefy-bls-hash-to-curve-vector"; - - /// `u[0]` and `u[1]` from the Rust vector, before the map to the curve. - bytes constant EXPECTED_U0 = - hex"19fa8d7582393438a7bd7ef7e789de283142e027986bc7f5f1919c106071b346a735f1cde455b6d0e9547783b4ffbbc3"; - bytes constant EXPECTED_U1 = - hex"15a0511a246ac447601f9abf8f3c6639328a907e15ba3981923b222f26d5440390996a0a065a227f75c23c552cf37960"; - - /// The resulting G1 point. - bytes constant EXPECTED_X = - hex"0e1caf33eecf4c4d00dc4c2d7dc2f1d9ffef352cdcf50a359caf0c9ddcb49de2a4124773cc45d92901079834fc0743ea"; - bytes constant EXPECTED_Y = - hex"1427e29396b9a044ac8475c5939054e529c70a5c01cdb71e6a845bcb2ad0342dfb148c6860e084fa7a84aa1ea4c54385"; - - /// The ciphersuite is prepended to the message rather than used as the domain separation tag. - function test_ciphersuite_matches_w3f_bls() public pure { - assertEq( - BlsHashToCurve.CIPHER_SUITE, - bytes("BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"), - "ciphersuite drifted from what w3f-bls composes" - ); - // DST_prime = DST || len(DST), and the DST really is the single byte 0x01. - assertEq(BlsHashToCurve.DST_PRIME, hex"0101", "dst is not the one byte 0x01"); - } - - /// `hash_to_field` is the part the contract implements by hand, so it is checked on its own. - function test_hash_to_field_matches_rust() public view { - bytes memory preimage = bytes.concat(BlsHashToCurve.CIPHER_SUITE, MESSAGE); - (bytes memory u0, bytes memory u1) = BlsHashToCurve.hashToField(preimage); - - // Field elements are returned padded to 64 bytes; the value is the trailing 48. - assertEq(_trim(u0), EXPECTED_U0, "u[0] differs from the Rust vector"); - assertEq(_trim(u1), EXPECTED_U1, "u[1] differs from the Rust vector"); - } - - /// The whole pipeline, including the EIP-2537 map and addition. - function test_hash_commitment_to_g1_matches_rust() public view { - bytes memory point = BlsHashToCurve.hashCommitmentToG1(MESSAGE); - assertEq(point.length, 128, "G1 points are 128 bytes"); - - bytes memory x = new bytes(48); - bytes memory y = new bytes(48); - for (uint256 i = 0; i < 48; ++i) { - x[i] = point[16 + i]; - y[i] = point[64 + 16 + i]; - } - - assertEq(x, EXPECTED_X, "point.x differs from the Rust vector"); - assertEq(y, EXPECTED_Y, "point.y differs from the Rust vector"); - } - - /// Strip the 16 bytes of zero padding EIP-2537 puts in front of a field element. - function _trim(bytes memory padded) private pure returns (bytes memory) { - bytes memory out = new bytes(48); - for (uint256 i = 0; i < 48; ++i) { - out[i] = padded[16 + i]; - } - return out; - } -} diff --git a/modules/consensus/beefy/verifier/src/apk.rs b/modules/consensus/beefy/verifier/src/apk.rs index 1ccf51681..9ada1270f 100644 --- a/modules/consensus/beefy/verifier/src/apk.rs +++ b/modules/consensus/beefy/verifier/src/apk.rs @@ -294,8 +294,32 @@ fn challenge(message: &G1Affine, signature: &G1Affine, apk: &G1Affine, apk2: &G2 } /// Population count over the bitlist. Fixed cost regardless of how many signed. +/// The 1024 slots are packed 250 to a word across the first four and 24 into the last, matching +/// the `ToBinary` decomposition in the circuit. Bits above those ranges are constrained to zero +/// there, so a proof carrying any would not verify; masking them off keeps the count honest on its +/// own terms rather than relying on that. pub fn count_signers(bitlist: &[[u8; 32]; APK_BITLIST_WORDS]) -> u32 { - bitlist.iter().flatten().map(|byte| byte.count_ones()).sum() + bitlist + .iter() + .enumerate() + .map(|(word, bytes)| { + let width = if word == APK_BITLIST_WORDS - 1 { 24 } else { 250 }; + // The word is big endian, so the packed bits are the low ones at the end. + bytes + .iter() + .rev() + .enumerate() + .map(|(i, byte)| { + let low = i * 8; + if low >= width { + return 0; + } + let keep = (width - low).min(8); + (byte & (((1u16 << keep) - 1) as u8)).count_ones() + }) + .sum::() + }) + .sum() } /// Two thirds plus one, matching substrate's own rule and the Solidity client. diff --git a/modules/consensus/beefy/verifier/src/ecdsa.rs b/modules/consensus/beefy/verifier/src/ecdsa.rs new file mode 100644 index 000000000..cd16be65f --- /dev/null +++ b/modules/consensus/beefy/verifier/src/ecdsa.rs @@ -0,0 +1,195 @@ +// Copyright (C) Polytope Labs Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Verifying BEEFY finality from per signer ecdsa signatures. +//! +//! Each signer travels with its own signature and a merkle path proving membership of the +//! authority set, so both the proof and the work grow with the number of signers. The aggregate +//! route in [`crate::apk`] is the same protocol with that cost removed. + +use alloc::{string::ToString, vec, vec::Vec}; + +use crate::{ + EcdsaRecover, MMR_ROOT_PAYLOAD_ID, MerkleHasher, error::Error, verify_mmr_leaf, + verify_parachain_headers, +}; +use beefy_verifier_primitives::{ConsensusMessage, ConsensusState, MmrProof, ParachainHeader}; +use codec::Encode; +use ismp::messaging::Keccak256; +use merkle_mountain_range::{ + Error as MmrError, Merge as MmrMerge, MerkleProof as MmrMerkleProof, leaf_index_to_mmr_size, + leaf_index_to_pos, +}; +use polkadot_sdk::{ + sp_consensus_beefy::{Commitment, mmr::MmrLeaf}, + sp_mmr_primitives::LeafProof, +}; +use primitive_types::H256; +use rs_merkle::MerkleProof; + +/// Verify the consensus proof and return the new trusted consensus state and verified parachain +/// headers +pub fn verify_consensus( + trusted_state: ConsensusState, + proof: ConsensusMessage, +) -> Result<(Vec, Vec), Error> { + let (state, heads_root) = verify_mmr_update_proof::(trusted_state, proof.mmr)?; + let verified_headers = verify_parachain_headers::(heads_root, proof.parachain)?; + Ok((state.encode(), verified_headers)) +} + +/// Verifies a new Mmr root update, the relay chain accumulates it's blocks into a merkle mountain +/// range tree which light clients can use as a source for log_2(n) ancestry proofs. This new mmr +/// root hash is signed by the relay chain authority set and we can verify the membership of the +/// authorities that signed this new root using a merkle multi proof and a merkle commitment to the +/// total authorities +pub fn verify_mmr_update_proof( + trusted_state: ConsensusState, + mmr: MmrProof, +) -> Result<(ConsensusState, H256), Error> { + let commitment = &mmr.signed_commitment.commitment; + let preamble = + prepare_update(&trusted_state, commitment, mmr.signed_commitment.signatures.len() as u32)?; + + // The ECDSA half signs the keccak hash of the commitment, and the authority set is committed + // to as the keccak of each signer's Ethereum address, so the signers are identified by + // recovering them rather than by being named in the proof. + let commitment_hash = H::keccak256(&commitment.encode()); + let mut authority_leaves: Vec<[u8; 32]> = Vec::new(); + let mut authority_indices = Vec::new(); + + for sig in mmr.signed_commitment.signatures.iter() { + let uncompressed = H::secp256k1_recover(&commitment_hash.0, &sig.signature) + .map_err(|_| Error::FailedToRecoverPublicKey)?; + + let hashed_uncompressed = H::keccak256(&uncompressed); + + let mut eth_address = [0u8; 20]; + eth_address.copy_from_slice(&hashed_uncompressed.as_ref()[12..]); + + let authority_address_hash = H::keccak256(ð_address); + + authority_leaves.push(authority_address_hash.into()); + authority_indices.push(sig.index as usize); + } + + verify_authority_membership::( + preamble.keyset_commitment, + &mmr.authority_proof, + &authority_indices, + &authority_leaves, + preamble.authority_count, + )?; + + verify_mmr_leaf::(&mmr.latest_mmr_leaf, &mmr.mmr_proof, preamble.mmr_root)?; + + let latest_height = commitment.block_number; + let state = apply_update(trusted_state, &mmr.latest_mmr_leaf, latest_height); + + Ok((state, mmr.latest_mmr_leaf.leaf_extra)) +} + +/// The parts of an update that hold regardless of how the commitment was signed. +struct UpdatePreamble { + /// Commitment to the authority set the signers must belong to. + keyset_commitment: H256, + /// Size of that authority set. + authority_count: u32, + /// MMR root carried in the commitment payload. + mmr_root: H256, +} + +/// Checks staleness, resolves which authority set the commitment claims to be signed under, +/// judges participation against that set alone, and extracts the MMR root from the payload. +fn prepare_update( + trusted_state: &ConsensusState, + commitment: &Commitment, + signer_count: u32, +) -> Result { + if trusted_state.latest_beefy_height >= commitment.block_number { + return Err(Error::StaleHeight { + trusted_height: trusted_state.latest_beefy_height, + current_height: commitment.block_number, + }); + } + + let authority_set = if commitment.validator_set_id == trusted_state.current_authorities.id { + &trusted_state.current_authorities + } else if commitment.validator_set_id == trusted_state.next_authorities.id { + &trusted_state.next_authorities + } else { + return Err(Error::UnknownAuthoritySet { id: commitment.validator_set_id }); + }; + + if !check_participation_threshold(signer_count, authority_set.len) { + return Err(Error::SuperMajorityRequired); + } + + let mmr_root_data = commitment + .payload + .get_raw(&MMR_ROOT_PAYLOAD_ID) + .ok_or(Error::MmrRootHashMissing)?; + + if mmr_root_data.len() != 32 { + return Err(Error::InvalidMmrRootHashLength { len: mmr_root_data.len() }); + } + + Ok(UpdatePreamble { + keyset_commitment: authority_set.keyset_commitment, + authority_count: authority_set.len, + mmr_root: H256::from_slice(mmr_root_data), + }) +} + +/// Proves the signing authorities are members of the committed authority set. +fn verify_authority_membership( + keyset_commitment: H256, + proof: &[[u8; 32]], + indices: &[usize], + leaves: &[[u8; 32]], + authority_count: u32, +) -> Result<(), Error> { + let merkle_proof = MerkleProof::>::new(proof.to_vec()); + + let valid = + merkle_proof.verify(keyset_commitment.into(), indices, leaves, authority_count as usize); + + if !valid { + Err(Error::InvalidAuthoritiesProof)?; + } + + Ok(()) +} + +/// Rotates the tracked authority sets if the leaf announces a newer one, and records the height. +fn apply_update( + mut trusted_state: ConsensusState, + leaf: &MmrLeaf, + latest_height: u32, +) -> ConsensusState { + if leaf.beefy_next_authority_set.id > trusted_state.next_authorities.id { + trusted_state.current_authorities = trusted_state.next_authorities.clone(); + trusted_state.next_authorities = leaf.beefy_next_authority_set.clone(); + } + + trusted_state.latest_beefy_height = latest_height; + + trusted_state +} + +/// Checks for supermajority participation +fn check_participation_threshold(len: u32, total: u32) -> bool { + len >= ((2 * total) / 3) + 1 +} diff --git a/modules/consensus/beefy/verifier/src/lib.rs b/modules/consensus/beefy/verifier/src/lib.rs index de5641add..524e151d0 100644 --- a/modules/consensus/beefy/verifier/src/lib.rs +++ b/modules/consensus/beefy/verifier/src/lib.rs @@ -25,6 +25,7 @@ extern crate alloc; #[cfg(feature = "apk")] pub mod apk; +pub mod ecdsa; pub mod error; pub mod sp1; #[cfg(test)] @@ -51,7 +52,7 @@ use primitive_types::H256; use rs_merkle::{Hasher, MerkleProof}; /// The payload ID for the MMR root hash in a BEEFY commitment -const MMR_ROOT_PAYLOAD_ID: [u8; 2] = *b"mh"; +pub(crate) const MMR_ROOT_PAYLOAD_ID: [u8; 2] = *b"mh"; /// A trait for recovering secp256k1 public keys from ECDSA signatures. /// This allows the verifier to be generic. @@ -92,156 +93,6 @@ impl MmrMerge for KeccakMerge { } } -/// Verify the consensus proof and return the new trusted consensus state and verified parachain -/// headers -pub fn verify_consensus( - trusted_state: ConsensusState, - proof: ConsensusMessage, -) -> Result<(Vec, Vec), Error> { - let (state, heads_root) = verify_mmr_update_proof::(trusted_state, proof.mmr)?; - let verified_headers = verify_parachain_headers::(heads_root, proof.parachain)?; - Ok((state.encode(), verified_headers)) -} - -/// Verifies a new Mmr root update, the relay chain accumulates it's blocks into a merkle mountain -/// range tree which light clients can use as a source for log_2(n) ancestry proofs. This new mmr -/// root hash is signed by the relay chain authority set and we can verify the membership of the -/// authorities that signed this new root using a merkle multi proof and a merkle commitment to the -/// total authorities -pub fn verify_mmr_update_proof( - trusted_state: ConsensusState, - mmr: MmrProof, -) -> Result<(ConsensusState, H256), Error> { - let commitment = &mmr.signed_commitment.commitment; - let preamble = - prepare_update(&trusted_state, commitment, mmr.signed_commitment.signatures.len() as u32)?; - - // The ECDSA half signs the keccak hash of the commitment, and the authority set is committed - // to as the keccak of each signer's Ethereum address, so the signers are identified by - // recovering them rather than by being named in the proof. - let commitment_hash = H::keccak256(&commitment.encode()); - let mut authority_leaves: Vec<[u8; 32]> = Vec::new(); - let mut authority_indices = Vec::new(); - - for sig in mmr.signed_commitment.signatures.iter() { - let uncompressed = H::secp256k1_recover(&commitment_hash.0, &sig.signature) - .map_err(|_| Error::FailedToRecoverPublicKey)?; - - let hashed_uncompressed = H::keccak256(&uncompressed); - - let mut eth_address = [0u8; 20]; - eth_address.copy_from_slice(&hashed_uncompressed.as_ref()[12..]); - - let authority_address_hash = H::keccak256(ð_address); - - authority_leaves.push(authority_address_hash.into()); - authority_indices.push(sig.index as usize); - } - - verify_authority_membership::( - preamble.keyset_commitment, - &mmr.authority_proof, - &authority_indices, - &authority_leaves, - preamble.authority_count, - )?; - - verify_mmr_leaf::(&mmr.latest_mmr_leaf, &mmr.mmr_proof, preamble.mmr_root)?; - - let latest_height = commitment.block_number; - let state = apply_update(trusted_state, &mmr.latest_mmr_leaf, latest_height); - - Ok((state, mmr.latest_mmr_leaf.leaf_extra)) -} - -/// The parts of an update that hold regardless of how the commitment was signed. -struct UpdatePreamble { - /// Commitment to the authority set the signers must belong to. - keyset_commitment: H256, - /// Size of that authority set. - authority_count: u32, - /// MMR root carried in the commitment payload. - mmr_root: H256, -} - -/// Checks staleness, resolves which authority set the commitment claims to be signed under, -/// judges participation against that set alone, and extracts the MMR root from the payload. -fn prepare_update( - trusted_state: &ConsensusState, - commitment: &Commitment, - signer_count: u32, -) -> Result { - if trusted_state.latest_beefy_height >= commitment.block_number { - return Err(Error::StaleHeight { - trusted_height: trusted_state.latest_beefy_height, - current_height: commitment.block_number, - }); - } - - let authority_set = if commitment.validator_set_id == trusted_state.current_authorities.id { - &trusted_state.current_authorities - } else if commitment.validator_set_id == trusted_state.next_authorities.id { - &trusted_state.next_authorities - } else { - return Err(Error::UnknownAuthoritySet { id: commitment.validator_set_id }); - }; - - if !check_participation_threshold(signer_count, authority_set.len) { - return Err(Error::SuperMajorityRequired); - } - - let mmr_root_data = commitment - .payload - .get_raw(&MMR_ROOT_PAYLOAD_ID) - .ok_or(Error::MmrRootHashMissing)?; - - if mmr_root_data.len() != 32 { - return Err(Error::InvalidMmrRootHashLength { len: mmr_root_data.len() }); - } - - Ok(UpdatePreamble { - keyset_commitment: authority_set.keyset_commitment, - authority_count: authority_set.len, - mmr_root: H256::from_slice(mmr_root_data), - }) -} - -/// Proves the signing authorities are members of the committed authority set. -fn verify_authority_membership( - keyset_commitment: H256, - proof: &[[u8; 32]], - indices: &[usize], - leaves: &[[u8; 32]], - authority_count: u32, -) -> Result<(), Error> { - let merkle_proof = MerkleProof::>::new(proof.to_vec()); - - let valid = - merkle_proof.verify(keyset_commitment.into(), indices, leaves, authority_count as usize); - - if !valid { - Err(Error::InvalidAuthoritiesProof)?; - } - - Ok(()) -} - -/// Rotates the tracked authority sets if the leaf announces a newer one, and records the height. -fn apply_update( - mut trusted_state: ConsensusState, - leaf: &MmrLeaf, - latest_height: u32, -) -> ConsensusState { - if leaf.beefy_next_authority_set.id > trusted_state.next_authorities.id { - trusted_state.current_authorities = trusted_state.next_authorities.clone(); - trusted_state.next_authorities = leaf.beefy_next_authority_set.clone(); - } - - trusted_state.latest_beefy_height = latest_height; - - trusted_state -} - /// Verifies the inclusion of parachain headers in the parachain heads root via a merkle multi proof pub fn verify_parachain_headers( heads_root: H256, @@ -311,8 +162,3 @@ pub(crate) fn verify_mmr_leaf( Ok(()) } - -/// Checks for supermajority participation -fn check_participation_threshold(len: u32, total: u32) -> bool { - len >= ((2 * total) / 3) + 1 -} diff --git a/modules/ismp/clients/beefy/src/consensus.rs b/modules/ismp/clients/beefy/src/consensus.rs index bd3542b18..1d9825397 100644 --- a/modules/ismp/clients/beefy/src/consensus.rs +++ b/modules/ismp/clients/beefy/src/consensus.rs @@ -14,7 +14,7 @@ // limitations under the License. use alloc::{boxed::Box, collections::BTreeMap, format, vec, vec::Vec}; -use beefy_verifier::{error::Error as BeefyError, verify_consensus}; +use beefy_verifier::{ecdsa::verify_consensus, error::Error as BeefyError}; #[cfg(feature = "apk")] use beefy_verifier_primitives::PROOF_TYPE_APK; use beefy_verifier_primitives::{ From 37c84602d6fa921f3d62ce26c3d65eff1f9a0d05 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Mon, 17 Aug 2026 18:40:52 +0100 Subject: [PATCH 33/48] call the aggregate client BlsBeefy now the per signer one is gone --- evm/rust/abi/BlsApkBeefy.json | 451 ----------------- evm/rust/abi/BlsBeefy.json | 469 ++++++++++++++++++ evm/rust/src/conversions.rs | 46 +- .../{bls_apk_beefy.rs => bls_beefy.rs} | 8 +- evm/rust/src/generated/mod.rs | 2 +- .../{BlsApkBeefy.sol => BlsBeefy.sol} | 2 +- .../{BlsApkBeefy.t.sol => BlsBeefy.t.sol} | 12 +- modules/consensus/beefy/verifier/src/test.rs | 26 +- .../beefy/verifier/tests/apk_fixture.rs | 6 +- .../src/benchmarking.rs | 4 +- .../pallets/beefy-consensus-proofs/src/lib.rs | 4 +- tesseract/consensus/beefy/apk/src/lib.rs | 20 +- .../consensus/beefy/apk/tests/live_prover.rs | 2 +- tesseract/consensus/beefy/src/host.rs | 7 +- 14 files changed, 541 insertions(+), 518 deletions(-) delete mode 100644 evm/rust/abi/BlsApkBeefy.json create mode 100644 evm/rust/abi/BlsBeefy.json rename evm/rust/src/generated/{bls_apk_beefy.rs => bls_beefy.rs} (86%) rename evm/src/consensus/{BlsApkBeefy.sol => BlsBeefy.sol} (99%) rename evm/tests/foundry/{BlsApkBeefy.t.sol => BlsBeefy.t.sol} (91%) diff --git a/evm/rust/abi/BlsApkBeefy.json b/evm/rust/abi/BlsApkBeefy.json deleted file mode 100644 index 2402d9462..000000000 --- a/evm/rust/abi/BlsApkBeefy.json +++ /dev/null @@ -1,451 +0,0 @@ -[ - { - "type": "constructor", - "inputs": [ - { - "name": "apkProof", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "MMR_ROOT_PAYLOAD_ID", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes2", - "internalType": "bytes2" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "_apk", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract IApkProof" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "noOp", - "inputs": [ - { - "name": "s", - "type": "tuple", - "internalType": "struct BlsApkConsensusState", - "components": [ - { - "name": "latestHeight", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "beefyActivationBlock", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "currentAuthoritySet", - "type": "tuple", - "internalType": "struct ApkAuthoritySet", - "components": [ - { - "name": "id", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "len", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "apkCommitment", - "type": "bytes32", - "internalType": "bytes32" - } - ] - }, - { - "name": "nextAuthoritySet", - "type": "tuple", - "internalType": "struct ApkAuthoritySet", - "components": [ - { - "name": "id", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "len", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "apkCommitment", - "type": "bytes32", - "internalType": "bytes32" - } - ] - } - ] - }, - { - "name": "p", - "type": "tuple", - "internalType": "struct BlsApkBeefyConsensusProof", - "components": [ - { - "name": "relay", - "type": "tuple", - "internalType": "struct BlsApkRelayChainProof", - "components": [ - { - "name": "commitment", - "type": "tuple", - "internalType": "struct Commitment", - "components": [ - { - "name": "payload", - "type": "tuple[]", - "internalType": "struct Payload[]", - "components": [ - { - "name": "id", - "type": "bytes2", - "internalType": "bytes2" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - } - ] - }, - { - "name": "blockNumber", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "validatorSetId", - "type": "uint64", - "internalType": "uint64" - } - ] - }, - { - "name": "bitlist", - "type": "uint256[5]", - "internalType": "uint256[5]" - }, - { - "name": "apk", - "type": "bytes32[3]", - "internalType": "bytes32[3]" - }, - { - "name": "apk2", - "type": "bytes32[6]", - "internalType": "bytes32[6]" - }, - { - "name": "apkProof", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "signature", - "type": "bytes32[3]", - "internalType": "bytes32[3]" - }, - { - "name": "latestMmrLeaf", - "type": "tuple", - "internalType": "struct BeefyMmrLeaf", - "components": [ - { - "name": "version", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "parentNumber", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "parentHash", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "nextAuthoritySet", - "type": "tuple", - "internalType": "struct AuthoritySetCommitment", - "components": [ - { - "name": "id", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "len", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "root", - "type": "bytes32", - "internalType": "bytes32" - } - ] - }, - { - "name": "extra", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "leafIndex", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "mmrProof", - "type": "bytes32[]", - "internalType": "bytes32[]" - } - ] - }, - { - "name": "parachain", - "type": "tuple", - "internalType": "struct ParachainProof", - "components": [ - { - "name": "parachains", - "type": "tuple[]", - "internalType": "struct Parachain[]", - "components": [ - { - "name": "index", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "header", - "type": "bytes", - "internalType": "bytes" - } - ] - }, - { - "name": "proof", - "type": "bytes32[]", - "internalType": "bytes32[]" - }, - { - "name": "leafCount", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ] - } - ], - "outputs": [], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "supportsInterface", - "inputs": [ - { - "name": "interfaceId", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "verify", - "inputs": [ - { - "name": "previousState", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "proof", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [ - { - "name": "", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "", - "type": "tuple[]", - "internalType": "struct IntermediateState[]", - "components": [ - { - "name": "stateMachineId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "height", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "commitment", - "type": "tuple", - "internalType": "struct StateCommitment", - "components": [ - { - "name": "timestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "overlayRoot", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "stateRoot", - "type": "bytes32", - "internalType": "bytes32" - } - ] - } - ] - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "error", - "name": "EmptyLeaves", - "inputs": [] - }, - { - "type": "error", - "name": "EmptyTree", - "inputs": [] - }, - { - "type": "error", - "name": "EmptyTree", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidAggregateProof", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidMmrProof", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidParachainHeaderProof", - "inputs": [] - }, - { - "type": "error", - "name": "LeafIndexOutOfBounds", - "inputs": [] - }, - { - "type": "error", - "name": "MissingApkCommitment", - "inputs": [] - }, - { - "type": "error", - "name": "MmrRootHashMissing", - "inputs": [] - }, - { - "type": "error", - "name": "OutOfBoundsLeaves", - "inputs": [] - }, - { - "type": "error", - "name": "ProofExhausted", - "inputs": [] - }, - { - "type": "error", - "name": "SuperMajorityRequired", - "inputs": [] - }, - { - "type": "error", - "name": "TimestampNotFound", - "inputs": [] - }, - { - "type": "error", - "name": "UnconsumedProof", - "inputs": [] - }, - { - "type": "error", - "name": "UnknownAuthoritySet", - "inputs": [] - }, - { - "type": "error", - "name": "UnsortedLeaves", - "inputs": [] - }, - { - "type": "error", - "name": "UnsortedLeaves", - "inputs": [] - } -] \ No newline at end of file diff --git a/evm/rust/abi/BlsBeefy.json b/evm/rust/abi/BlsBeefy.json new file mode 100644 index 000000000..d8e2c0d12 --- /dev/null +++ b/evm/rust/abi/BlsBeefy.json @@ -0,0 +1,469 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "apkProof", + "type": "address", + "internalType": "address" + }, + { + "name": "digestParaId", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "MMR_ROOT_PAYLOAD_ID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes2", + "internalType": "bytes2" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "_apk", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IApkProof" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "_digestParaId", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "noOp", + "inputs": [ + { + "name": "s", + "type": "tuple", + "internalType": "struct BeefyConsensusState", + "components": [ + { + "name": "latestHeight", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "beefyActivationBlock", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "currentAuthoritySet", + "type": "tuple", + "internalType": "struct AuthoritySetCommitment", + "components": [ + { + "name": "id", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "len", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "root", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "name": "nextAuthoritySet", + "type": "tuple", + "internalType": "struct AuthoritySetCommitment", + "components": [ + { + "name": "id", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "len", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "root", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } + ] + }, + { + "name": "p", + "type": "tuple", + "internalType": "struct BlsApkBeefyConsensusProof", + "components": [ + { + "name": "relay", + "type": "tuple", + "internalType": "struct BlsApkRelayChainProof", + "components": [ + { + "name": "commitment", + "type": "tuple", + "internalType": "struct Commitment", + "components": [ + { + "name": "payload", + "type": "tuple[]", + "internalType": "struct Payload[]", + "components": [ + { + "name": "id", + "type": "bytes2", + "internalType": "bytes2" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "blockNumber", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "validatorSetId", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "name": "bitlist", + "type": "uint256[5]", + "internalType": "uint256[5]" + }, + { + "name": "apk", + "type": "bytes32[3]", + "internalType": "bytes32[3]" + }, + { + "name": "apk2", + "type": "bytes32[6]", + "internalType": "bytes32[6]" + }, + { + "name": "apkProof", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "signature", + "type": "bytes32[3]", + "internalType": "bytes32[3]" + }, + { + "name": "latestMmrLeaf", + "type": "tuple", + "internalType": "struct BeefyMmrLeaf", + "components": [ + { + "name": "version", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "parentNumber", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "parentHash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "nextAuthoritySet", + "type": "tuple", + "internalType": "struct AuthoritySetCommitment", + "components": [ + { + "name": "id", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "len", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "root", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "name": "extra", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "leafIndex", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "mmrProof", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ] + }, + { + "name": "parachain", + "type": "tuple", + "internalType": "struct ParachainProof", + "components": [ + { + "name": "parachains", + "type": "tuple[]", + "internalType": "struct Parachain[]", + "components": [ + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "id", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "header", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "proof", + "type": "bytes32[]", + "internalType": "bytes32[]" + }, + { + "name": "leafCount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ] + } + ], + "outputs": [], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "verify", + "inputs": [ + { + "name": "previousState", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "proof", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "", + "type": "tuple[]", + "internalType": "struct IntermediateState[]", + "components": [ + { + "name": "stateMachineId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "height", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "commitment", + "type": "tuple", + "internalType": "struct StateCommitment", + "components": [ + { + "name": "timestamp", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "overlayRoot", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "stateRoot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } + ] + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "error", + "name": "EmptyLeaves", + "inputs": [] + }, + { + "type": "error", + "name": "EmptyTree", + "inputs": [] + }, + { + "type": "error", + "name": "EmptyTree", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAggregateProof", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidMmrProof", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidParachainHeaderProof", + "inputs": [] + }, + { + "type": "error", + "name": "LeafIndexOutOfBounds", + "inputs": [] + }, + { + "type": "error", + "name": "MissingApkCommitment", + "inputs": [] + }, + { + "type": "error", + "name": "MmrRootHashMissing", + "inputs": [] + }, + { + "type": "error", + "name": "OutOfBoundsLeaves", + "inputs": [] + }, + { + "type": "error", + "name": "ProofExhausted", + "inputs": [] + }, + { + "type": "error", + "name": "SuperMajorityRequired", + "inputs": [] + }, + { + "type": "error", + "name": "TimestampNotFound", + "inputs": [] + }, + { + "type": "error", + "name": "UnconsumedProof", + "inputs": [] + }, + { + "type": "error", + "name": "UnknownAuthoritySet", + "inputs": [] + }, + { + "type": "error", + "name": "UnsortedLeaves", + "inputs": [] + }, + { + "type": "error", + "name": "UnsortedLeaves", + "inputs": [] + } +] \ No newline at end of file diff --git a/evm/rust/src/conversions.rs b/evm/rust/src/conversions.rs index 2d3d0d96a..5921dc390 100644 --- a/evm/rust/src/conversions.rs +++ b/evm/rust/src/conversions.rs @@ -399,16 +399,16 @@ mod beefy { // stay single-sourced above. mod apk_bridge { use super::*; - use crate::bls_apk_beefy::BlsApkBeefy; + use crate::bls_beefy::BlsBeefy; - impl From for Payload { - fn from(value: BlsApkBeefy::Payload) -> Self { + impl From for Payload { + fn from(value: BlsBeefy::Payload) -> Self { Payload { id: value.id, data: value.data } } } - impl From for Commitment { - fn from(value: BlsApkBeefy::Commitment) -> Self { + impl From for Commitment { + fn from(value: BlsBeefy::Commitment) -> Self { Commitment { payload: value.payload.into_iter().map(Into::into).collect(), blockNumber: value.blockNumber, @@ -417,14 +417,14 @@ mod beefy { } } - impl From for AuthoritySetCommitment { - fn from(value: BlsApkBeefy::AuthoritySetCommitment) -> Self { + impl From for AuthoritySetCommitment { + fn from(value: BlsBeefy::AuthoritySetCommitment) -> Self { AuthoritySetCommitment { id: value.id, len: value.len, root: value.root } } } - impl From for BeefyMmrLeaf { - fn from(value: BlsApkBeefy::BeefyMmrLeaf) -> Self { + impl From for BeefyMmrLeaf { + fn from(value: BlsBeefy::BeefyMmrLeaf) -> Self { BeefyMmrLeaf { version: value.version, parentNumber: value.parentNumber, @@ -436,14 +436,14 @@ mod beefy { } } - impl From for Parachain { - fn from(value: BlsApkBeefy::Parachain) -> Self { + impl From for Parachain { + fn from(value: BlsBeefy::Parachain) -> Self { Parachain { index: value.index, id: value.id, header: value.header } } } - impl From for ParachainProof { - fn from(value: BlsApkBeefy::ParachainProof) -> Self { + impl From for ParachainProof { + fn from(value: BlsBeefy::ParachainProof) -> Self { ParachainProof { parachains: value.parachains.into_iter().map(Into::into).collect(), proof: value.proof, @@ -454,13 +454,13 @@ mod beefy { } /// Decoded from calldata a relayer supplied, so every width is checked rather than assumed. - impl TryFrom + impl TryFrom for beefy_verifier_primitives::ApkConsensusMessage { type Error = &'static str; fn try_from( - value: crate::bls_apk_beefy::BlsApkBeefy::BlsApkBeefyConsensusProof, + value: crate::bls_beefy::BlsBeefy::BlsApkBeefyConsensusProof, ) -> Result { let relay = value.relay; let leaf: BeefyMmrLeaf = relay.latestMmrLeaf.into(); @@ -500,18 +500,18 @@ mod beefy { /// to be handed to `initialize_apk_state` in this encoding, since it is otherwise only ever /// learned from a header digest. impl From - for crate::bls_apk_beefy::BlsApkBeefy::BlsApkConsensusState + for crate::bls_beefy::BlsBeefy::BeefyConsensusState { fn from(value: beefy_verifier_primitives::ApkConsensusState) -> Self { let authority_set = |set: beefy_verifier_primitives::ApkAuthoritySet| { - crate::bls_apk_beefy::BlsApkBeefy::ApkAuthoritySet { + crate::bls_beefy::BlsBeefy::AuthoritySetCommitment { id: set.id, len: set.len, - apkCommitment: FixedBytes(set.apk_commitment.0), + root: FixedBytes(set.apk_commitment.0), } }; - crate::bls_apk_beefy::BlsApkBeefy::BlsApkConsensusState { + crate::bls_beefy::BlsBeefy::BeefyConsensusState { latestHeight: value.latest_beefy_height.to_u256(), beefyActivationBlock: value.beefy_activation_block.to_u256(), currentAuthoritySet: authority_set(value.current_authorities), @@ -522,19 +522,19 @@ mod beefy { /// The mmr root is not part of the initial state, since nothing has been proven yet. It is /// filled by the first update that verifies. - impl TryFrom + impl TryFrom for beefy_verifier_primitives::ApkConsensusState { type Error = &'static str; fn try_from( - value: crate::bls_apk_beefy::BlsApkBeefy::BlsApkConsensusState, + value: crate::bls_beefy::BlsBeefy::BeefyConsensusState, ) -> Result { - let authority_set = |set: crate::bls_apk_beefy::BlsApkBeefy::ApkAuthoritySet| { + let authority_set = |set: crate::bls_beefy::BlsBeefy::AuthoritySetCommitment| { beefy_verifier_primitives::ApkAuthoritySet { id: set.id, len: set.len, - apk_commitment: H256(set.apkCommitment.0), + apk_commitment: H256(set.root.0), } }; diff --git a/evm/rust/src/generated/bls_apk_beefy.rs b/evm/rust/src/generated/bls_beefy.rs similarity index 86% rename from evm/rust/src/generated/bls_apk_beefy.rs rename to evm/rust/src/generated/bls_beefy.rs index 135a80e2b..216d4ca5e 100644 --- a/evm/rust/src/generated/bls_apk_beefy.rs +++ b/evm/rust/src/generated/bls_beefy.rs @@ -10,8 +10,8 @@ sol!( #[allow(missing_docs)] #[sol(rpc, ignore_unlinked)] #[derive(Debug, PartialEq, Eq)] - BlsApkBeefy, - "abi/BlsApkBeefy.json" + BlsBeefy, + "abi/BlsBeefy.json" ); #[cfg(not(feature = "std"))] @@ -19,6 +19,6 @@ sol!( #[allow(missing_docs)] #[sol(ignore_unlinked)] #[derive(Debug, PartialEq, Eq)] - BlsApkBeefy, - "abi/BlsApkBeefy.json" + BlsBeefy, + "abi/BlsBeefy.json" ); diff --git a/evm/rust/src/generated/mod.rs b/evm/rust/src/generated/mod.rs index 0f21bfb2c..eeec93e82 100644 --- a/evm/rust/src/generated/mod.rs +++ b/evm/rust/src/generated/mod.rs @@ -10,7 +10,7 @@ //! which is what substrate pallets consume. pub mod bandwidth_manager; -pub mod bls_apk_beefy; +pub mod bls_beefy; pub mod ecdsa_beefy; pub mod erc20; pub mod evm_host; diff --git a/evm/src/consensus/BlsApkBeefy.sol b/evm/src/consensus/BlsBeefy.sol similarity index 99% rename from evm/src/consensus/BlsApkBeefy.sol rename to evm/src/consensus/BlsBeefy.sol index 3a2d93191..ccf7dd918 100644 --- a/evm/src/consensus/BlsApkBeefy.sol +++ b/evm/src/consensus/BlsBeefy.sol @@ -73,7 +73,7 @@ interface IApkProof { * * Requires Prague for the EIP-2537 precompiles. */ -contract BlsApkBeefy is IConsensusV2, ERC165 { +contract BlsBeefy is IConsensusV2, ERC165 { /// The payload id for the mmr root in a BEEFY commitment, "mh" bytes2 public constant MMR_ROOT_PAYLOAD_ID = bytes2("mh"); diff --git a/evm/tests/foundry/BlsApkBeefy.t.sol b/evm/tests/foundry/BlsBeefy.t.sol similarity index 91% rename from evm/tests/foundry/BlsApkBeefy.t.sol rename to evm/tests/foundry/BlsBeefy.t.sol index cee556d16..88e07038d 100644 --- a/evm/tests/foundry/BlsApkBeefy.t.sol +++ b/evm/tests/foundry/BlsBeefy.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.30; import {Test, console} from "forge-std/Test.sol"; import {IntermediateState} from "@hyperbridge/core/interfaces/IConsensusV2.sol"; -import {BlsApkBeefy} from "../../src/consensus/BlsApkBeefy.sol"; +import {BlsBeefy} from "../../src/consensus/BlsBeefy.sol"; import {BeefyConsensusState} from "../../src/consensus/Types.sol"; import {ApkProof} from "./vendor/ApkProof.sol"; import {PlonkVerifier} from "./vendor/PlonkVerifier.sol"; @@ -21,17 +21,17 @@ import {PlonkVerifier} from "./vendor/PlonkVerifier.sol"; * `test_verify_live_proof` because both run the whole client over live data in the same harness. * * Needs EIP-2537: - * FOUNDRY_PROFILE=bls forge test --match-contract BlsApkBeefyTest -vv --gas-report + * FOUNDRY_PROFILE=bls forge test --match-contract BlsBeefyTest -vv --gas-report */ -contract BlsApkBeefyTest is Test { - BlsApkBeefy internal client; +contract BlsBeefyTest is Test { + BlsBeefy internal client; function setUp() public { // False selects the basic ciphersuite, which is what substrate's BEEFY signs with. PoP // would hash the same message to a different point and fail with nothing to explain why. ApkProof apk = new ApkProof(address(new PlonkVerifier())); // The fixture's proof carries para 4009's header, which is where its digest comes from. - client = new BlsApkBeefy(address(apk), 4009); + client = new BlsBeefy(address(apk), 4009); } function _state() internal view returns (bytes memory) { @@ -66,7 +66,7 @@ contract BlsApkBeefyTest is Test { client.verify(state, proof); uint256 used = before - gasleft(); - console.log("BlsApkBeefy.verify gas:", used); + console.log("BlsBeefy.verify gas:", used); } /// Replaying a proof the state has already passed is a no-op rather than a revert, matching the diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index 15729cd64..3693954ff 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -1230,7 +1230,7 @@ async fn bls_apk_live_inputs() { fn bls_apk_live_fixture() { use alloy_primitives::{Bytes, FixedBytes, U256}; use alloy_sol_types::{SolType, SolValue}; - use ismp_abi::bls_apk_beefy::BlsApkBeefy; + use ismp_abi::bls_beefy::BlsBeefy; let dir = std::env::var("APK_FIXTURE_DIR").expect("APK_FIXTURE_DIR must be set"); let read = |name: &str| -> json::Value { @@ -1264,13 +1264,13 @@ fn bls_apk_live_fixture() { let trusted = &inputs["trusted"]; let signing_set_id = u64_of(&inputs["validatorSetId"]); // Only the set that signed needs its commitment seeded; the other is learned from a digest. - let authority_set = |id: u64, len: u64| BlsApkBeefy::ApkAuthoritySet { + let authority_set = |id: u64, len: u64| BlsBeefy::AuthoritySetCommitment { id, len: len as u32, apkCommitment: if id == signing_set_id { apk_commitment } else { FixedBytes::ZERO }, }; - let state = BlsApkBeefy::BlsApkConsensusState { + let state = BlsBeefy::BeefyConsensusState { latestHeight: U256::from(u64_of(&trusted["latestHeight"])), beefyActivationBlock: U256::from(u64_of(&trusted["beefyActivationBlock"])), currentAuthoritySet: authority_set( @@ -1295,9 +1295,9 @@ fn bls_apk_live_fixture() { .expect("five words"); let leaf = &inputs["mmrLeaf"]; - let relay = BlsApkBeefy::BlsApkRelayChainProof { - commitment: BlsApkBeefy::Commitment { - payload: vec![BlsApkBeefy::Payload { + let relay = BlsBeefy::BlsApkRelayChainProof { + commitment: BlsBeefy::Commitment { + payload: vec![BlsBeefy::Payload { id: FixedBytes(*b"mh"), data: Bytes::from(hex_bytes(&inputs["payloadMh"])), }], @@ -1309,11 +1309,11 @@ fn bls_apk_live_fixture() { apk2: words(&inputs["apk2"], 6).try_into().expect("bytes32[6]"), apkProof: Bytes::from(hex_bytes(&snark["apkProof"])), signature: words(&inputs["signature"], 3).try_into().expect("bytes32[3]"), - latestMmrLeaf: BlsApkBeefy::BeefyMmrLeaf { + latestMmrLeaf: BlsBeefy::BeefyMmrLeaf { version: 0, parentNumber: u64_of(&leaf["parentNumber"]) as u32, parentHash: fixed32(&leaf["parentHash"]), - nextAuthoritySet: BlsApkBeefy::AuthoritySetCommitment { + nextAuthoritySet: BlsBeefy::AuthoritySetCommitment { id: u64_of(&leaf["nextAuthoritySetId"]), len: u64_of(&leaf["nextAuthoritySetLen"]) as u32, root: fixed32(&leaf["nextAuthoritySetRoot"]), @@ -1324,12 +1324,12 @@ fn bls_apk_live_fixture() { mmrProof: inputs["mmrProof"].as_array().expect("mmrProof").iter().map(fixed32).collect(), }; - let parachain = BlsApkBeefy::ParachainProof { + let parachain = BlsBeefy::ParachainProof { parachains: inputs["parachains"] .as_array() .expect("parachains") .iter() - .map(|para| BlsApkBeefy::Parachain { + .map(|para| BlsBeefy::Parachain { index: U256::from(u64_of(¶["index"])), id: U256::from(u64_of(¶["id"])), header: Bytes::from(hex_bytes(¶["header"])), @@ -1346,8 +1346,10 @@ fn bls_apk_live_fixture() { // SolValue, not SolType: this has to match `abi.encode(struct)` on the Solidity side. let encoded_state = SolValue::abi_encode(&state); - let encoded_proof = <(BlsApkBeefy::BlsApkRelayChainProof, BlsApkBeefy::ParachainProof) as SolType> - ::abi_encode_params(&(relay.clone(), parachain.clone())); + let encoded_proof = + <(BlsBeefy::BlsApkRelayChainProof, BlsBeefy::ParachainProof) as SolType>::abi_encode_params( + &(relay.clone(), parachain.clone()), + ); let fixtures = std::env::var("APK_FIXTURE_OUT") .unwrap_or_else(|_| "../../../../evm/tests/foundry/fixtures".to_string()); diff --git a/modules/consensus/beefy/verifier/tests/apk_fixture.rs b/modules/consensus/beefy/verifier/tests/apk_fixture.rs index 169f1db90..65abffbd4 100644 --- a/modules/consensus/beefy/verifier/tests/apk_fixture.rs +++ b/modules/consensus/beefy/verifier/tests/apk_fixture.rs @@ -23,7 +23,7 @@ use alloy_sol_types::SolType; use beefy_verifier::apk::{count_signers, verify_apk_consensus}; use beefy_verifier_primitives::{ApkConsensusMessage, ApkConsensusState}; -use ismp_abi::bls_apk_beefy::BlsApkBeefy; +use ismp_abi::bls_beefy::BlsBeefy; use polkadot_sdk::*; use primitive_types::H256; @@ -63,12 +63,12 @@ fn apk_verifier_agrees_with_solidity() { // The state is one abi-encoded struct, the proof is the two the client's `verify` takes as // separate arguments, which is why they decode differently. let trusted: ApkConsensusState = - ::abi_decode(&state_bytes) + ::abi_decode(&state_bytes) .expect("state decodes") .try_into() .expect("state converts"); let proof: ApkConsensusMessage = - ::abi_decode_params(&proof_bytes) + ::abi_decode_params(&proof_bytes) .expect("proof decodes") .try_into() .expect("proof converts"); diff --git a/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs b/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs index 3cfdcbe21..0d13cd25b 100644 --- a/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs +++ b/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs @@ -139,7 +139,7 @@ mod benchmarks { include_bytes!("../../../../evm/tests/foundry/fixtures/apk-verifying-key.bin").to_vec(); let state: beefy_verifier_primitives::ApkConsensusState = - ::abi_decode( + ::abi_decode( &state_bytes, ) .expect("apk state fixture decodes") @@ -148,7 +148,7 @@ mod benchmarks { #[block] { - let proof = ::abi_decode_params( + let proof = ::abi_decode_params( &proof_bytes, ) .expect("apk proof fixture decodes"); diff --git a/modules/pallets/beefy-consensus-proofs/src/lib.rs b/modules/pallets/beefy-consensus-proofs/src/lib.rs index e25841744..e94bb8153 100644 --- a/modules/pallets/beefy-consensus-proofs/src/lib.rs +++ b/modules/pallets/beefy-consensus-proofs/src/lib.rs @@ -346,7 +346,7 @@ pub mod pallet { ::AdminOrigin::ensure_origin(origin)?; let state: beefy_verifier_primitives::ApkConsensusState = - ::abi_decode( + ::abi_decode( &abi_state, ) .map_err(|e| { @@ -955,7 +955,7 @@ pub mod pallet { [&[types::PROOF_TYPE_NAIVE], scale_proof.encode().as_slice()].concat() }, types::PROOF_TYPE_APK => { - let abi_proof = ::abi_decode_params( + let abi_proof = ::abi_decode_params( abi_payload, ) .map_err(|_| Error::::AbiDecodeFailed)?; diff --git a/tesseract/consensus/beefy/apk/src/lib.rs b/tesseract/consensus/beefy/apk/src/lib.rs index 49f5f54d5..91b2dc69b 100644 --- a/tesseract/consensus/beefy/apk/src/lib.rs +++ b/tesseract/consensus/beefy/apk/src/lib.rs @@ -38,7 +38,7 @@ use beefy_prover::bls::{ PairedSignature, }; use beefy_verifier_primitives::{ConsensusState, BLS_G1_SIGNATURE_LEN}; -use ismp_abi::bls_apk_beefy::BlsApkBeefy; +use ismp_abi::bls_beefy::BlsBeefy; mod command; pub use command::CommandProver; @@ -132,7 +132,7 @@ where sp_consensus_beefy::ecdsa_crypto::Signature, >, consensus_state: ConsensusState, - ) -> Result { + ) -> Result { let set_id = signed_commitment.commitment.validator_set_id; if set_id != consensus_state.current_authorities.id && set_id != consensus_state.next_authorities.id @@ -174,9 +174,9 @@ where .clone(); let leaf = &message.mmr.latest_mmr_leaf; - let relay = BlsApkBeefy::BlsApkRelayChainProof { - commitment: BlsApkBeefy::Commitment { - payload: vec![BlsApkBeefy::Payload { + let relay = BlsBeefy::BlsApkRelayChainProof { + commitment: BlsBeefy::Commitment { + payload: vec![BlsBeefy::Payload { id: FixedBytes(*MMR_ROOT_ID), data: Bytes::from(mmr_root), }], @@ -188,7 +188,7 @@ where apk2: aggregate.apk2, apkProof: Bytes::from(proof.proof), signature: aggregate.signature, - latestMmrLeaf: BlsApkBeefy::BeefyMmrLeaf { + latestMmrLeaf: BlsBeefy::BeefyMmrLeaf { // One byte carrying the major version in the top three bits and the minor in the // rest, which is how the reverse conversion in `ismp-abi` reads it back. version: { @@ -197,7 +197,7 @@ where }, parentNumber: leaf.parent_number_and_hash.0, parentHash: FixedBytes(leaf.parent_number_and_hash.1 .0), - nextAuthoritySet: BlsApkBeefy::AuthoritySetCommitment { + nextAuthoritySet: BlsBeefy::AuthoritySetCommitment { id: leaf.beefy_next_authority_set.id, len: leaf.beefy_next_authority_set.len, root: FixedBytes(leaf.beefy_next_authority_set.keyset_commitment.0), @@ -210,12 +210,12 @@ where mmrProof: message.mmr.mmr_proof.items.iter().map(|item| FixedBytes(item.0)).collect(), }; - let parachain = BlsApkBeefy::ParachainProof { + let parachain = BlsBeefy::ParachainProof { parachains: message .parachain .parachains .iter() - .map(|para| BlsApkBeefy::Parachain { + .map(|para| BlsBeefy::Parachain { index: U256::from(para.index), id: U256::from(para.para_id), header: Bytes::from(para.header.clone()), @@ -225,7 +225,7 @@ where leafCount: U256::from(message.parachain.total_leaves), }; - Ok(BlsApkBeefy::BlsApkBeefyConsensusProof { relay, parachain }) + Ok(BlsBeefy::BlsApkBeefyConsensusProof { relay, parachain }) } /// Poseidon2 over the relay's current BEEFY keys at `at`. diff --git a/tesseract/consensus/beefy/apk/tests/live_prover.rs b/tesseract/consensus/beefy/apk/tests/live_prover.rs index e680ff0d9..4e90f3bae 100644 --- a/tesseract/consensus/beefy/apk/tests/live_prover.rs +++ b/tesseract/consensus/beefy/apk/tests/live_prover.rs @@ -250,7 +250,7 @@ async fn writes_initial_state_for_bootstrapping() { }, }; - let abi = ismp_abi::bls_apk_beefy::BlsApkBeefy::BlsApkConsensusState::from(state.clone()); + let abi = ismp_abi::bls_beefy::BlsBeefy::BeefyConsensusState::from(state.clone()); std::fs::write(&out, format!("0x{}", hex::encode(abi.abi_encode()))).expect("write"); println!( "wrote state for sets {} and {} at beefy height {} to {out}", diff --git a/tesseract/consensus/beefy/src/host.rs b/tesseract/consensus/beefy/src/host.rs index 02556dc83..dd744c500 100644 --- a/tesseract/consensus/beefy/src/host.rs +++ b/tesseract/consensus/beefy/src/host.rs @@ -121,7 +121,10 @@ where let apk = matches!(self.prover, Prover::Apk(_)); match (matches!(destination, StateMachine::Evm(_)), apk) { (true, true) => { - let state = ::abi_decode(encoded) + let state = + ::abi_decode( + encoded, + ) .context("Could not abi-decode apk consensus state")?; let state: beefy_verifier_primitives::ApkConsensusState = state.try_into().map_err(|e| anyhow!("{e}"))?; @@ -446,7 +449,7 @@ where current_authorities: authority_set(inner.current_authorities, H256(commitment)), next_authorities: authority_set(inner.next_authorities, H256::zero()), }; - ismp_abi::bls_apk_beefy::BlsApkBeefy::BlsApkConsensusState::from(state).abi_encode() + ismp_abi::bls_beefy::BlsBeefy::BeefyConsensusState::from(state).abi_encode() }, _ => { let state: BeefyConsensusState = prover_state.inner.into(); From 36a76f1d07435202ae110e44f3192a33ba1f63c9 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Mon, 17 Aug 2026 19:37:48 +0100 Subject: [PATCH 34/48] compile the aggregate verifier unconditionally now both runtimes accept those proofs --- modules/ismp/clients/beefy/Cargo.toml | 3 +-- modules/ismp/clients/beefy/src/consensus.rs | 7 ++----- modules/pallets/beefy-consensus-proofs/Cargo.toml | 1 - parachain/runtimes/gargantua/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/modules/ismp/clients/beefy/Cargo.toml b/modules/ismp/clients/beefy/Cargo.toml index 9a69ced37..80e796491 100644 --- a/modules/ismp/clients/beefy/Cargo.toml +++ b/modules/ismp/clients/beefy/Cargo.toml @@ -9,7 +9,7 @@ codec = { workspace = true, features = ["derive"], default-features = false } primitive-types = { workspace = true, default-features = false } ismp = { workspace = true, default-features = false } -beefy-verifier = { workspace = true, default-features = false } +beefy-verifier = { workspace = true, default-features = false, features = ["apk"] } pallet-ismp = { workspace = true, default-features = false } substrate-state-machine = { workspace = true, default-features = false } beefy-verifier-primitives = { workspace = true, default-features = false } @@ -24,7 +24,6 @@ features = [ [features] default = ["std"] -apk = ["beefy-verifier/apk"] std = [ "anyhow/std", "codec/std", diff --git a/modules/ismp/clients/beefy/src/consensus.rs b/modules/ismp/clients/beefy/src/consensus.rs index 1d9825397..9872099af 100644 --- a/modules/ismp/clients/beefy/src/consensus.rs +++ b/modules/ismp/clients/beefy/src/consensus.rs @@ -15,11 +15,9 @@ use alloc::{boxed::Box, collections::BTreeMap, format, vec, vec::Vec}; use beefy_verifier::{ecdsa::verify_consensus, error::Error as BeefyError}; -#[cfg(feature = "apk")] -use beefy_verifier_primitives::PROOF_TYPE_APK; use beefy_verifier_primitives::{ - ConsensusMessage, ConsensusState, MmrProof, PROOF_TYPE_NAIVE, PROOF_TYPE_SP1, ParachainProof, - Sp1BeefyProof, + ConsensusMessage, ConsensusState, MmrProof, PROOF_TYPE_APK, PROOF_TYPE_NAIVE, PROOF_TYPE_SP1, + ParachainProof, Sp1BeefyProof, }; use codec::{Decode, Encode}; use core::marker::PhantomData; @@ -108,7 +106,6 @@ where &vkey, )? }, - #[cfg(feature = "apk")] PROOF_TYPE_APK => { let apk_state: beefy_verifier_primitives::ApkConsensusState = codec::Decode::decode(&mut &trusted_consensus_state[..]) diff --git a/modules/pallets/beefy-consensus-proofs/Cargo.toml b/modules/pallets/beefy-consensus-proofs/Cargo.toml index 0e63b804a..80a04e3d0 100644 --- a/modules/pallets/beefy-consensus-proofs/Cargo.toml +++ b/modules/pallets/beefy-consensus-proofs/Cargo.toml @@ -66,6 +66,5 @@ runtime-benchmarks = [ "polkadot-sdk/runtime-benchmarks", "dep:hex-literal", "beefy-verifier/apk", - "ismp-beefy/apk", ] try-runtime = ["polkadot-sdk/try-runtime"] diff --git a/parachain/runtimes/gargantua/Cargo.toml b/parachain/runtimes/gargantua/Cargo.toml index 654d6cf29..52593c9a4 100644 --- a/parachain/runtimes/gargantua/Cargo.toml +++ b/parachain/runtimes/gargantua/Cargo.toml @@ -34,7 +34,7 @@ ismp-sync-committee = { workspace = true } ismp-bsc = { workspace = true } ismp-parachain = { workspace = true } ismp-grandpa = { workspace = true } -ismp-beefy = { workspace = true, features = ["apk"] } +ismp-beefy = { workspace = true } ismp-parachain-runtime-api = { workspace = true } pallet-ismp-relayer = { workspace = true } pallet-ismp-host-executive = { workspace = true } From 3354b0de2880dbee246c3bd4fd234e34e3075bce Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Mon, 17 Aug 2026 19:50:53 +0100 Subject: [PATCH 35/48] take the apk contracts from the gnark submodule rather than a copy --- .gitmodules | 3 + evm/lib/gnark-apk-proofs | 1 + evm/remappings.txt | 1 + evm/tests/foundry/BlsBeefy.t.sol | 4 +- evm/tests/foundry/vendor/ApkProof.sol | 546 -------------------------- 5 files changed, 7 insertions(+), 548 deletions(-) create mode 160000 evm/lib/gnark-apk-proofs delete mode 100644 evm/tests/foundry/vendor/ApkProof.sol diff --git a/.gitmodules b/.gitmodules index b4cc2e835..d1988de74 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "sdk/packages/simplex/proto/mpcvaultapis"] path = sdk/packages/simplex/proto/mpcvaultapis url = https://github.com/mpcvault/mpcvaultapis.git +[submodule "evm/lib/gnark-apk-proofs"] + path = evm/lib/gnark-apk-proofs + url = https://github.com/polytope-labs/gnark-apk-proofs diff --git a/evm/lib/gnark-apk-proofs b/evm/lib/gnark-apk-proofs new file mode 160000 index 000000000..a341f84a5 --- /dev/null +++ b/evm/lib/gnark-apk-proofs @@ -0,0 +1 @@ +Subproject commit a341f84a5aa672dd3d8f4eb1276446ede1c9671e diff --git a/evm/remappings.txt b/evm/remappings.txt index 016b0a90d..576c9cd69 100644 --- a/evm/remappings.txt +++ b/evm/remappings.txt @@ -4,4 +4,5 @@ @uniswap/=node_modules/@uniswap/ stringutils/=lib/solidity-stringutils/src/ @sp1-contracts/=lib/sp1-contracts/contracts/src/ +@gnark-apk-proofs/=lib/gnark-apk-proofs/solidity/contracts/ forge-std/=node_modules/forge-std/src/ diff --git a/evm/tests/foundry/BlsBeefy.t.sol b/evm/tests/foundry/BlsBeefy.t.sol index 88e07038d..9fe960a8f 100644 --- a/evm/tests/foundry/BlsBeefy.t.sol +++ b/evm/tests/foundry/BlsBeefy.t.sol @@ -6,8 +6,8 @@ import {IntermediateState} from "@hyperbridge/core/interfaces/IConsensusV2.sol"; import {BlsBeefy} from "../../src/consensus/BlsBeefy.sol"; import {BeefyConsensusState} from "../../src/consensus/Types.sol"; -import {ApkProof} from "./vendor/ApkProof.sol"; -import {PlonkVerifier} from "./vendor/PlonkVerifier.sol"; +import {ApkProof} from "@gnark-apk-proofs/ApkProof.sol"; +import {PlonkVerifier} from "@gnark-apk-proofs/PlonkVerifier.sol"; /** * @title BEEFY verified through an aggregate public key proof. diff --git a/evm/tests/foundry/vendor/ApkProof.sol b/evm/tests/foundry/vendor/ApkProof.sol deleted file mode 100644 index 02039424d..000000000 --- a/evm/tests/foundry/vendor/ApkProof.sol +++ /dev/null @@ -1,546 +0,0 @@ -// Copyright 2026 Polytope Labs. -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pragma solidity ^0.8.28; - -import {PlonkVerifier} from "./PlonkVerifier.sol"; - -/** - * @title APK Proof & BLS Aggregate Signature Verifier - * @notice Verifies both APK aggregation proofs (via PLONK) and aggregate BLS - * signatures using the scheme from "Efficient Aggregatable BLS - * Signatures with Chaum-Pedersen Proofs" (https://eprint.iacr.org/2022/1611). - * - * @dev BLS aggregate signature verification equation: - * e(asig + t·apk₁, g₂) = e(H(m) + t·g₁, apk₂) - * Checked as: e(asig + t·apk₁, -g₂) · e(H(m) + t·g₁, apk₂) = 1 - * where t = hash_to_field(H(m) ‖ sig ‖ apk₁ ‖ apk₂) via expand_message_xmd. - * - * BLS12-381 G1 points are passed as `bytes32[3]` (96 bytes total): - * X (48 bytes big-endian) || Y (48 bytes big-endian), matching the - * standard gnark-crypto / EIP-2537 uncompressed G1 format. - * - * G2 points are passed as `bytes32[6]` (192 bytes uncompressed): - * X.c0‖X.c1‖Y.c0‖Y.c1, 48 bytes each. - * - * Requires Prague EVM (Pectra hardfork) for EIP-2537 BLS12-381 precompiles. - * - * Security assumptions and trust boundaries: - * - All elliptic-curve arithmetic (G1ADD, G1MSM, pairing, map-to-G1) is - * delegated to the EIP-2537 precompiles, which are trusted to perform - * on-curve and subgroup validation of their inputs per the EIP. This - * contract therefore performs no separate point validation (finding 50). - * - Verification is stateless and idempotent: it does not track consumed - * proofs, so replay protection (nonce/uniqueness), if required, must be - * enforced by the calling application (finding 53). - * - hashToG1 and the BLS challenge derivation implement expand_message_xmd - * (RFC 9380) with the w3f/bls cipher suite; see the per-function NatSpec - * for the exact DST and parameters (findings 49, 52, 55). - */ -contract ApkProof { - PlonkVerifier public immutable _plonk; - - error G1AddFailed(); - error PlonkVerificationFailed(); - error SignatureVerificationFailed(); - - - // Precompile addresses - uint256 constant PRECOMPILE_MODEXP = 0x05; - uint256 constant PRECOMPILE_BLS12_G1ADD = 0x0b; - uint256 constant PRECOMPILE_BLS12_G1MSM = 0x0c; - uint256 constant PRECOMPILE_BLS12_PAIRING = 0x0f; - uint256 constant PRECOMPILE_BLS12_MAP_FP_TO_G1 = 0x10; - - /** - * Protocol-fixed seed point for APK aggregation. - * - * Derived as hash-to-field(dst="gnark-apk-proofs", msg="apk-seed-coset") - * followed by the RFC 9380 SSWU map + isogeny onto E(Fp), WITHOUT cofactor - * clearing — the point is on the curve but deliberately NOT in the G1 - * subgroup. The circuit's incomplete point addition relies on this: the - * accumulator seed + Sigma(pk_i) stays in the coset seed + G1, disjoint from - * G1, so it can never collide with a public key or reach infinity - * (Ciobotaru et al., eprint 2022/1205, section 5.1). - * - * The G1ADD precompile below accepts it (EIP-2537 ADD checks on-curve only, - * not subgroup); the seed never reaches the pairing precompile, which sees - * only the caller-supplied APK. Mirrors apk.ProtocolSeed() in Go; the - * coordinates are locked by TestProtocolSeedVectors — regenerate both - * together or on-chain verification rejects every proof. - * - * Layout: X = SEED_0 || SEED_1[0:16], Y = SEED_1[16:32] || SEED_2 (48-byte - * big-endian coordinates). - */ - bytes32 constant SEED_0 = 0x19742ffba069554d8cacceb8ed5514b2ecf72cd7372d3414203338f4fd3b3cc7; - bytes32 constant SEED_1 = 0x42fb160f8eb5818422246de186e0814a0e0f5d1199876e646952fb74d39e0b34; - bytes32 constant SEED_2 = 0x042a8d48786adae7e0fccf4b0236c72e82343de94c9d12bf17d22bec9edbbe2b; - - /// BLS signature verification constants - /// - /// BLS12-381 scalar field order r. - uint256 private constant R_MOD = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001; - - /// BLS12-381 G1 generator in EIP-2537 padded format (hi/lo word pairs). - uint256 private constant G1_GEN_X_HI = 0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0f; - uint256 private constant G1_GEN_X_LO = 0xc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb; - uint256 private constant G1_GEN_Y_HI = 0x0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4; - uint256 private constant G1_GEN_Y_LO = 0xfcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1; - - /** - * Negated BLS12-381 G2 generator (-g₂) in EIP-2537 padded format. - * X coordinates are unchanged; Y coordinates are negated (p − Y.c0, p − Y.c1). - * Using -g₂ lets us write the pairing check as: - * e(asig + t·apk₁, -g₂) · e(H(m) + t·g₁, apk₂) = 1 - * avoiding a runtime G1 negation. - */ - uint256 private constant NEG_G2_GEN_X_0_HI = 3045985886519456750490515843806728273; - uint256 private constant NEG_G2_GEN_X_0_LO = - 89961632905173714226157479458612185649920463576279427516307505038263245192632; - uint256 private constant NEG_G2_GEN_X_1_HI = 26419286191256893424348605754143887205; - uint256 private constant NEG_G2_GEN_X_1_LO = - 40446337346877272185227670183527379362551741423616556919902061939448715946878; - uint256 private constant NEG_G2_GEN_Y_0_HI = 17421388336814597573762763446246275004; - uint256 private constant NEG_G2_GEN_Y_0_LO = - 82535940630695547964844822885348920226556672312706312698172214783216175252138; - uint256 private constant NEG_G2_GEN_Y_1_HI = 26554973746327433462396120515077546301; - uint256 private constant NEG_G2_GEN_Y_1_LO = - 69304817850384178235384652711014277219752988873539414788182467642510429663469; - - /** - * w3f/bls cipher suite prefix for message signing, 43 bytes split 32+11. The first 32 bytes - * are common to both schemes; only the trailing tag differs: - * - * "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_" proof of possession - * "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_" basic - * - * The suite is part of the signed preimage, so a verifier has to use the same one the signer - * did. Signing with the basic scheme and verifying with PoP yields a well formed but different - * point, and the pairing simply returns false with nothing to explain why. `w3f_bls` exposes - * both, `Message::new` for basic and `Message::new_assuming_pop` for PoP. Polkadot signs with - * the basic scheme, which is what this hashes with. - */ - uint256 private constant CIPHER_SUITE_FIRST_32 = - 0x424c535f5349475f424c53313233383147315f584d443a5348412d3235365f53; - uint256 private constant CIPHER_SUITE_LAST_11 = 0x5357555f524f5f4e554c5f; - - /// BLS12-381 base field modulus p, split for mstore (32 + 16 bytes). - /// p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab - uint256 private constant BLS_P_FIRST_32 = - 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f624; - uint256 private constant BLS_P_LAST_16 = 0x1eabfffeb153ffffb9feffffffffaaab; - - /** - * @param _verifier The PLONK verifier for the APK circuit. - */ - constructor(address _verifier) { - _plonk = PlonkVerifier(_verifier); - } - - /** - * @notice Verify both APK aggregation proof and aggregate BLS signature in one call. - * @param publicKeysCommitment Poseidon2 hash commitment over all 1024 validator public - * keys: Poseidon2(pk_0.X.limbs || ... || pk_1023.Y.limbs), each Fp coordinate - * decomposed into 6 x 64-bit little-endian limbs (12 limbs per G1 point). - * @param bitlist Participating-validator bitlist (5 field elements); bitlist[0..3] - * encode 250 bits each, bitlist[4] encodes 24 bits. - * @param apk Aggregate public key of participants apk = sum(b_i * pk_i) ∈ G1, - * bytes32[3] (X ‖ Y, 96 bytes). The circuit expects seed + apk; the contract adds - * the seed automatically. - * @param apkProof The serialized PLONK proof bytes. - * @param message H(m) ∈ G1, bytes32[3] (96 bytes). - * @param signature Aggregate signature ∈ G1, bytes32[3] (96 bytes). - * @param apk2 Aggregate public key ∈ G2, bytes32[6] (192 bytes). - */ - function verify( - uint256 publicKeysCommitment, - uint256[5] calldata bitlist, - bytes32[3] calldata apk, - bytes calldata apkProof, - bytes32[3] calldata message, - bytes32[3] calldata signature, - bytes32[6] calldata apk2 - ) external view { - // Verify APK aggregation proof - uint256[18] memory encoded = _encodePublicInputs(publicKeysCommitment, bitlist, apk); - if (!_plonk.Verify(apkProof, encoded)) revert PlonkVerificationFailed(); - - // Verify BLS aggregate signature using the supplied apk - if (!_verifyBls(apk, message, signature, apk2)) revert SignatureVerificationFailed(); - } - - /** - * @notice Hash a message to a BLS12-381 G1 point (w3f/bls compatible). - * Implements hash_to_curve (RFC 9380) with expand_message_xmd (SHA-256), - * DST = 0x01, and the cipher suite prefix - * "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_" prepended internally. - * @param message The raw message to hash. - * @return result Uncompressed G1 point as bytes32[3] (X ‖ Y, 96 bytes). - */ - function hashToG1(bytes memory message) public view returns (bytes32[3] memory result) { - assembly { - let ptr := mload(0x40) - let sha2 := 0x02 - let msgLen := mload(message) - - // ═══════════════════════════════════════════════════════════ - // Phase 1: expand_message_xmd (SHA-256, DST=0x01, 128-byte output) - // DST_prime = 0x01 ‖ 0x01 (DST ‖ I2OSP(1,1)) - // uniform_bytes = b1 ‖ b2 ‖ b3 ‖ b4 - // ═══════════════════════════════════════════════════════════ - - // msg_prime = Z_pad(64) ‖ cipher_suite(43) ‖ msg ‖ I2OSP(128,2) ‖ I2OSP(0,1) ‖ DST_prime - mstore(ptr, 0) // Z_pad[0..31] - mstore(add(ptr, 0x20), 0) // Z_pad[32..63] - mstore(add(ptr, 0x40), CIPHER_SUITE_FIRST_32) // cipher[0..31] - mstore(add(ptr, 0x60), shl(168, CIPHER_SUITE_LAST_11)) // cipher[32..42] - mcopy(add(ptr, 0x6B), add(message, 0x20), msgLen) // message - let pos := add(add(ptr, 0x6B), msgLen) - mstore8(pos, 0x00) // I2OSP(128,2) high - mstore8(add(pos, 1), 0x80) // I2OSP(128,2) low - mstore8(add(pos, 2), 0x00) // I2OSP(0,1) - mstore8(add(pos, 3), 0x01) // DST[0] - mstore8(add(pos, 4), 0x01) // I2OSP(1,1) - let msgPrimeLen := add(0x70, msgLen) // 64+43+msg+3+2 = 112+msg - - // b0 = SHA256(msg_prime) — output just past msg_prime - let b0Out := add(ptr, msgPrimeLen) - if iszero(staticcall(gas(), sha2, ptr, msgPrimeLen, b0Out, 0x20)) { revert(0, 0) } - let b0 := mload(b0Out) - - let hashOut := add(ptr, 0x200) - - // b1 = SHA256(b0 ‖ 0x01 ‖ DST_prime) - // DST_prime = 0x01 0x01 → biHashLen = 32+1+2 = 35 = 0x23 - mstore(ptr, b0) - mstore8(add(ptr, 0x20), 0x01) - mstore8(add(ptr, 0x21), 0x01) // DST[0] - mstore8(add(ptr, 0x22), 0x01) // I2OSP(1,1) - if iszero(staticcall(gas(), sha2, ptr, 0x23, hashOut, 0x20)) { revert(0, 0) } - let b1 := mload(hashOut) - - // b2 = SHA256((b0 ⊕ b1) ‖ 0x02 ‖ DST_prime) - mstore(ptr, xor(b0, b1)) - mstore8(add(ptr, 0x20), 0x02) - if iszero(staticcall(gas(), sha2, ptr, 0x23, hashOut, 0x20)) { revert(0, 0) } - let b2 := mload(hashOut) - - // b3 = SHA256((b0 ⊕ b2) ‖ 0x03 ‖ DST_prime) - mstore(ptr, xor(b0, b2)) - mstore8(add(ptr, 0x20), 0x03) - if iszero(staticcall(gas(), sha2, ptr, 0x23, hashOut, 0x20)) { revert(0, 0) } - let b3 := mload(hashOut) - - // b4 = SHA256((b0 ⊕ b3) ‖ 0x04 ‖ DST_prime) - mstore(ptr, xor(b0, b3)) - mstore8(add(ptr, 0x20), 0x04) - if iszero(staticcall(gas(), sha2, ptr, 0x23, hashOut, 0x20)) { revert(0, 0) } - let b4 := mload(hashOut) - - // ═══════════════════════════════════════════════════════════ - // Phase 2: reduce to field elements via MODEXP (x^1 mod p) - // u0 = (b1‖b2) mod p, u1 = (b3‖b4) mod p - // ═══════════════════════════════════════════════════════════ - - // MODEXP input: Bsize(32) ‖ Esize(32) ‖ Msize(32) ‖ base(64) ‖ exp(1) ‖ mod(48) - mstore(ptr, 64) // Bsize - mstore(add(ptr, 0x20), 1) // Esize - mstore(add(ptr, 0x40), 48) // Msize - mstore(add(ptr, 0x60), b1) // base high - mstore(add(ptr, 0x80), b2) // base low - mstore8(add(ptr, 0xA0), 0x01) // exp = 1 - mstore(add(ptr, 0xA1), BLS_P_FIRST_32) // mod[0..31] - mstore(add(ptr, 0xC1), shl(128, BLS_P_LAST_16)) // mod[32..47] - - // Zero MAP_FP_TO_G1 padding (16 bytes at ptr+0x200) - mstore(add(ptr, 0x200), 0) - - // u0 = (b1‖b2) mod p → ptr+0x210 (48 bytes, forming MAP input at ptr+0x200) - if iszero(staticcall(gas(), PRECOMPILE_MODEXP, ptr, 0xD1, add(ptr, 0x210), 48)) { - revert(0, 0) - } - - // MAP_FP_TO_G1(u0) → Q0 at ptr+0x300 (128 bytes) - if iszero(staticcall(gas(), PRECOMPILE_BLS12_MAP_FP_TO_G1, add(ptr, 0x200), 64, add(ptr, 0x300), 128)) { - revert(0, 0) - } - - // u1 = (b3‖b4) mod p - mstore(add(ptr, 0x60), b3) - mstore(add(ptr, 0x80), b4) - if iszero(staticcall(gas(), PRECOMPILE_MODEXP, ptr, 0xD1, add(ptr, 0x210), 48)) { - revert(0, 0) - } - - // MAP_FP_TO_G1(u1) → Q1 at ptr+0x380 (128 bytes) - if iszero(staticcall(gas(), PRECOMPILE_BLS12_MAP_FP_TO_G1, add(ptr, 0x200), 64, add(ptr, 0x380), 128)) { - revert(0, 0) - } - - // ═══════════════════════════════════════════════════════════ - // Phase 3: G1ADD(Q0, Q1) → H(m) - // ═══════════════════════════════════════════════════════════ - - // Q0‖Q1 contiguous at ptr+0x300 (256 bytes) - if iszero(staticcall(gas(), PRECOMPILE_BLS12_G1ADD, add(ptr, 0x300), 256, add(ptr, 0x300), 128)) { - revert(0, 0) - } - - // Extract raw G1 (96 bytes) from padded format (128 bytes) - // X at ptr+0x310 (48 bytes), Y at ptr+0x350 (48 bytes) - mcopy(result, add(ptr, 0x310), 48) - mcopy(add(result, 48), add(ptr, 0x350), 48) - } - } - - /** - * @dev Derive challenge t and verify the BLS pairing check — all in assembly. - * - * Phase 1 — expand_message_xmd (SHA-256, empty DST, 48-byte output): - * msg_prime = Z_pad(64) ‖ message(96) ‖ sig(96) ‖ apk1(96) ‖ apk2(192) ‖ 0x00300000 - * b0 = SHA256(msg_prime), b1 = SHA256(b0‖0x0100), b2 = SHA256((b0⊕b1)‖0x0200) - * t = (b1·2¹²⁸ + b2>>128) mod r - * - * Phase 2 — pairing check: - * e(sig + t·apk₁, -g₂) · e(msg + t·g₁, apk₂) = 1 - */ - function _verifyBls( - bytes32[3] calldata apk1, - bytes32[3] calldata message, - bytes32[3] calldata signature, - bytes32[6] calldata apk2 - ) internal view returns (bool result) { - assembly { - let ptr := mload(0x40) - let sha2 := 0x02 - - // ═══════════════════════════════════════════════════════════ - // Phase 1: derive challenge t - // ═══════════════════════════════════════════════════════════ - // Build msg_prime at ptr (548 = 0x224 bytes) - mstore(ptr, 0) // Z_pad[0..31] - mstore(add(ptr, 0x20), 0) // Z_pad[32..63] - calldatacopy(add(ptr, 0x40), message, 96) // message - calldatacopy(add(ptr, 0xA0), signature, 96) // signature - calldatacopy(add(ptr, 0x100), apk1, 96) // apk1 - calldatacopy(add(ptr, 0x160), apk2, 192) // apk2 - mstore8(add(ptr, 0x220), 0x00) // I2OSP(48,2) high - mstore8(add(ptr, 0x221), 0x30) // I2OSP(48,2) low - mstore8(add(ptr, 0x222), 0x00) // I2OSP(0,1) - mstore8(add(ptr, 0x223), 0x00) // DST_prime - - // b0 = SHA256(msg_prime) - let hashOut := add(ptr, 0x300) - if iszero(staticcall(gas(), sha2, ptr, 0x224, hashOut, 0x20)) { revert(0, 0) } - let b0 := mload(hashOut) - - // b1 = SHA256(b0 ‖ 0x01 ‖ 0x00) - mstore(ptr, b0) - mstore8(add(ptr, 0x20), 0x01) - mstore8(add(ptr, 0x21), 0x00) - if iszero(staticcall(gas(), sha2, ptr, 0x22, hashOut, 0x20)) { revert(0, 0) } - let b1 := mload(hashOut) - - // b2 = SHA256((b0 ⊕ b1) ‖ 0x02 ‖ 0x00) - mstore(ptr, xor(b0, b1)) - mstore8(add(ptr, 0x20), 0x02) - mstore8(add(ptr, 0x21), 0x00) - if iszero(staticcall(gas(), sha2, ptr, 0x22, hashOut, 0x20)) { revert(0, 0) } - let b2 := mload(hashOut) - - // t = (b1 × 2¹²⁸ + b2>>128) mod r - let t := addmod( - mulmod(b1, 0x100000000000000000000000000000000, R_MOD), - shr(128, b2), - R_MOD - ) - - // ═══════════════════════════════════════════════════════════ - // Phase 2: pairing check - // ═══════════════════════════════════════════════════════════ - - // Write -g₂ into pairing buffer [0x080..0x17F] - mstore(add(ptr, 0x080), NEG_G2_GEN_X_0_HI) - mstore(add(ptr, 0x0A0), NEG_G2_GEN_X_0_LO) - mstore(add(ptr, 0x0C0), NEG_G2_GEN_X_1_HI) - mstore(add(ptr, 0x0E0), NEG_G2_GEN_X_1_LO) - mstore(add(ptr, 0x100), NEG_G2_GEN_Y_0_HI) - mstore(add(ptr, 0x120), NEG_G2_GEN_Y_0_LO) - mstore(add(ptr, 0x140), NEG_G2_GEN_Y_1_HI) - mstore(add(ptr, 0x160), NEG_G2_GEN_Y_1_LO) - - // Pad apk₂ into pairing buffer [0x200..0x2FF] - let apk2Off := apk2 - mstore(add(ptr, 0x200), 0) - calldatacopy(add(ptr, 0x210), apk2Off, 48) - mstore(add(ptr, 0x240), 0) - calldatacopy(add(ptr, 0x250), add(apk2Off, 48), 48) - mstore(add(ptr, 0x280), 0) - calldatacopy(add(ptr, 0x290), add(apk2Off, 96), 48) - mstore(add(ptr, 0x2C0), 0) - calldatacopy(add(ptr, 0x2D0), add(apk2Off, 144), 48) - - // G1MSM(apk₁, t) → t·apk₁ - let msmIn := add(ptr, 0x300) - mstore(msmIn, 0) - calldatacopy(add(msmIn, 0x10), apk1, 48) - mstore(add(msmIn, 0x40), 0) - calldatacopy(add(msmIn, 0x50), add(apk1, 48), 48) - mstore(add(msmIn, 0x80), t) - - let msmOut := add(ptr, 0x3A0) - if iszero(staticcall(gas(), PRECOMPILE_BLS12_G1MSM, msmIn, 0xA0, msmOut, 0x80)) { - revert(0, 0) - } - - // G1ADD(signature, t·apk₁) → lhs at pairing [0x000] - let addIn := add(ptr, 0x420) - mstore(addIn, 0) - calldatacopy(add(addIn, 0x10), signature, 48) - mstore(add(addIn, 0x40), 0) - calldatacopy(add(addIn, 0x50), add(signature, 48), 48) - mcopy(add(addIn, 0x80), msmOut, 0x80) - - if iszero(staticcall(gas(), PRECOMPILE_BLS12_G1ADD, addIn, 0x100, ptr, 0x80)) { - revert(0, 0) - } - - // G1MSM(g₁, t) → t·g₁ - mstore(msmIn, G1_GEN_X_HI) - mstore(add(msmIn, 0x20), G1_GEN_X_LO) - mstore(add(msmIn, 0x40), G1_GEN_Y_HI) - mstore(add(msmIn, 0x60), G1_GEN_Y_LO) - mstore(add(msmIn, 0x80), t) - - if iszero(staticcall(gas(), PRECOMPILE_BLS12_G1MSM, msmIn, 0xA0, msmOut, 0x80)) { - revert(0, 0) - } - - // G1ADD(message, t·g₁) → rhs at pairing [0x180] - mstore(addIn, 0) - calldatacopy(add(addIn, 0x10), message, 48) - mstore(add(addIn, 0x40), 0) - calldatacopy(add(addIn, 0x50), add(message, 48), 48) - mcopy(add(addIn, 0x80), msmOut, 0x80) - - if iszero(staticcall(gas(), PRECOMPILE_BLS12_G1ADD, addIn, 0x100, add(ptr, 0x180), 0x80)) { - revert(0, 0) - } - - // Pairing check — e(lhs, -g₂) · e(rhs, apk₂) = 1 - if iszero(staticcall(gas(), PRECOMPILE_BLS12_PAIRING, ptr, 0x300, add(ptr, 0x300), 0x20)) { - revert(0, 0) - } - - result := mload(add(ptr, 0x300)) - } - } - - /** - * @dev Encode the public inputs into the flat uint256[18] format - * expected by the gnark verifier. - * - * Verifier expects: out[0..4]=bitlist, out[5]=commitment, out[6..17]=apk limbs - */ - function _encodePublicInputs(uint256 publicKeysCommitment, uint256[5] calldata bitlist, bytes32[3] calldata apk) - internal - view - returns (uint256[18] memory out) - { - bytes32 s0 = SEED_0; - bytes32 s1 = SEED_1; - bytes32 s2 = SEED_2; - - assembly { - let mask := 0xFFFFFFFFFFFFFFFF - - // --- Copy bitlist and commitment into out[0..5] --- - calldatacopy(out, bitlist, 160) // bitlist (5*32) → out[0..4] - mstore(add(out, 160), publicKeysCommitment) // commitment → out[5] - - /* - * Build G1ADD input in scratch memory at out + 576. - * out occupies 18*32 = 576 bytes; we use the space after it as scratch. - * - * G1ADD input layout (256 bytes, two padded G1 points): - * [0..127] = seed point (padded EIP-2537 format) - * [128..255] = apk point (padded EIP-2537 format) - */ - let scratch := add(out, 576) - - // Zero the 256-byte G1ADD input region - mstore(scratch, 0) - mstore(add(scratch, 32), 0) - mstore(add(scratch, 64), 0) - mstore(add(scratch, 96), 0) - mstore(add(scratch, 128), 0) - mstore(add(scratch, 160), 0) - mstore(add(scratch, 192), 0) - mstore(add(scratch, 224), 0) - - /* - * Seed point in padded EIP-2537 format (128 bytes): - * [0..15] = zero padding - * [16..47] = s0 (seed X high 32 bytes) - * [48..63] = s1[0:16] (seed X low 16 bytes) - * [64..79] = zero padding between X and Y - * [80..95] = s1[16:32](seed Y high 16 bytes) - * [96..127] = s2 (seed Y low 32 bytes) - */ - mstore(add(scratch, 16), s0) - mstore(add(scratch, 48), s1) - mstore(add(scratch, 64), 0) // zero-pad between X and Y - mstore(add(scratch, 80), shl(128, s1)) - mstore(add(scratch, 96), s2) - - // APK point from calldata (padded) - let apkOff := apk - calldatacopy(add(scratch, 144), apkOff, 48) // APK X - calldatacopy(add(scratch, 208), add(apkOff, 48), 48) // APK Y - - // --- Call G1ADD precompile, output to scratch+256 (128 bytes) --- - let res := add(scratch, 256) - let ok := staticcall(gas(), PRECOMPILE_BLS12_G1ADD, scratch, 256, res, 128) - if iszero(ok) { - mstore(0, 0x55d4cbf9) // G1AddFailed() - revert(28, 4) - } - - /* - * Decompose padded G1 result into 12 x 64-bit limbs → out[6..17]. - * Result layout: [16 zero | X 48 bytes | 16 zero | Y 48 bytes] - * X coordinate: hi at res+16, lo at res+32 - * Y coordinate: hi at res+80, lo at res+96 - */ - let xHi := mload(add(res, 16)) - let xLo := mload(add(res, 32)) - mstore(add(out, 192), and(xLo, mask)) // out[6] - mstore(add(out, 224), and(shr(64, xLo), mask)) // out[7] - mstore(add(out, 256), and(shr(128, xLo), mask)) // out[8] - mstore(add(out, 288), and(shr(192, xLo), mask)) // out[9] - mstore(add(out, 320), and(shr(128, xHi), mask)) // out[10] - mstore(add(out, 352), shr(192, xHi)) // out[11] - - let yHi := mload(add(res, 80)) - let yLo := mload(add(res, 96)) - mstore(add(out, 384), and(yLo, mask)) // out[12] - mstore(add(out, 416), and(shr(64, yLo), mask)) // out[13] - mstore(add(out, 448), and(shr(128, yLo), mask)) // out[14] - mstore(add(out, 480), and(shr(192, yLo), mask)) // out[15] - mstore(add(out, 512), and(shr(128, yHi), mask)) // out[16] - mstore(add(out, 544), shr(192, yHi)) // out[17] - } - } -} From e7acf04f4fc443b748a937f1feb00f448b58d8cc Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Mon, 17 Aug 2026 19:58:38 +0100 Subject: [PATCH 36/48] lock the benchmark's hex literal dependency --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 4ed449124..a11070874 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14738,6 +14738,7 @@ dependencies = [ "beefy-verifier-primitives", "cumulus-pallet-parachain-system", "hex", + "hex-literal 0.4.1", "log", "parity-scale-codec", "polkadot-sdk", From abecb3d6b313a6316ec04720c067372c16c2f262 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Mon, 17 Aug 2026 20:48:11 +0100 Subject: [PATCH 37/48] take the incoming authority set from the header digest and ask the relay for its keys only when the set id moves --- evm/src/consensus/BlsBeefy.sol | 28 +++--- evm/src/consensus/Types.sol | 9 +- evm/tests/foundry/ApkCommitmentDigest.t.sol | 3 +- modules/consensus/beefy/primitives/src/lib.rs | 23 ++++- modules/consensus/beefy/verifier/src/apk.rs | 26 ++---- modules/pallets/beefy-apk-digest/src/lib.rs | 91 +++++++++++++------ parachain/runtimes/gargantua/src/lib.rs | 42 +++++---- 7 files changed, 137 insertions(+), 85 deletions(-) diff --git a/evm/src/consensus/BlsBeefy.sol b/evm/src/consensus/BlsBeefy.sol index ccf7dd918..70faa9337 100644 --- a/evm/src/consensus/BlsBeefy.sol +++ b/evm/src/consensus/BlsBeefy.sol @@ -129,12 +129,18 @@ contract BlsBeefy is IConsensusV2, ERC165 { (IntermediateState[] memory intermediates, ApkDigest memory digest) = verifyParachainHeaderProof(headsRoot, parachain, _digestParaId); - // Forward chaining: a verified header may carry the commitment for a set this client does - // not have keys for yet. Picking it up here is what lets the next update be verified at - // all, and is the reason the digest names the *next* set rather than the current one. - if ( - digest.setId == newState.nextAuthoritySet.id && newState.nextAuthoritySet.root == bytes32(0) - ) { + // Forward chaining: everything this client believes about the incoming set comes from + // here, its id, its size and its commitment together. A header naming the set after the + // one being waited on rolls the sets forward, since the relay has moved on. + if (digest.setId > newState.nextAuthoritySet.id) { + newState.currentAuthoritySet = newState.nextAuthoritySet; + newState.nextAuthoritySet = AuthoritySetCommitment({ + id: digest.setId, + len: digest.len, + root: bytes32(digest.commitment) + }); + } else if (digest.setId == newState.nextAuthoritySet.id && newState.nextAuthoritySet.root == bytes32(0)) { + newState.nextAuthoritySet.len = digest.len; newState.nextAuthoritySet.root = bytes32(digest.commitment); } @@ -176,16 +182,6 @@ contract BlsBeefy is IConsensusV2, ERC165 { verifyMmrLeaf(trustedState, relayProof, mmrRoot); - if (relayProof.latestMmrLeaf.nextAuthoritySet.id > trustedState.nextAuthoritySet.id) { - trustedState.currentAuthoritySet = trustedState.nextAuthoritySet; - // The incoming set's size comes from the mmr leaf, but its APK commitment is not known - // until a header digest supplies it, so it starts empty. - trustedState.nextAuthoritySet = AuthoritySetCommitment({ - id: relayProof.latestMmrLeaf.nextAuthoritySet.id, - len: relayProof.latestMmrLeaf.nextAuthoritySet.len, - root: bytes32(0) - }); - } trustedState.latestHeight = commitment.blockNumber; return (trustedState, relayProof.latestMmrLeaf.extra); diff --git a/evm/src/consensus/Types.sol b/evm/src/consensus/Types.sol index 8a9a3384e..4ad434a32 100644 --- a/evm/src/consensus/Types.sol +++ b/evm/src/consensus/Types.sol @@ -183,6 +183,8 @@ struct BeefyConsensusProof { struct ApkDigest { /// The authority set the commitment describes. uint64 setId; + /// How many validators are in it, which is what the threshold is taken against. + uint32 len; /// Poseidon2 commitment over that set's G1 public keys. uint256 commitment; } @@ -280,7 +282,7 @@ library HeaderImpl { /// MMR leaf, so no further proof is needed: `commitment` can go straight to `ApkProof.verify` /// as `publicKeysCommitment`, and `setId` says which authority set it describes. /// - /// Payload is SCALE: a u64 set id little-endian, then the 32 byte commitment. + /// Payload is SCALE: a u64 set id little-endian, a u32 size, then the 32 byte commitment. /// /// A zero `setId` reads as absent. BEEFY numbers its sets from one, so nothing legitimate /// names set zero, and the caller then has one thing to check rather than two. @@ -292,14 +294,15 @@ library HeaderImpl { bytes memory data = self.digests[j].consensus.data; // Ignore a malformed item rather than reverting: a wrong length means some other // producer wrote under this engine id, and the caller should see "absent", not fail. - if (data.length != 40) continue; + if (data.length != 44) continue; uint64 setId = uint64(ScaleCodec.decodeUint256(Bytes.substr(data, 0, 8))); if (setId == 0) continue; return ApkDigest({ setId: setId, - commitment: uint256(Bytes.toBytes32(Bytes.substr(data, 8))) + len: uint32(ScaleCodec.decodeUint256(Bytes.substr(data, 8, 4))), + commitment: uint256(Bytes.toBytes32(Bytes.substr(data, 12))) }); } } diff --git a/evm/tests/foundry/ApkCommitmentDigest.t.sol b/evm/tests/foundry/ApkCommitmentDigest.t.sol index 785c4dd94..6a9c397ca 100644 --- a/evm/tests/foundry/ApkCommitmentDigest.t.sol +++ b/evm/tests/foundry/ApkCommitmentDigest.t.sol @@ -14,7 +14,7 @@ contract ApkCommitmentDigestTest is Test { using HeaderImpl for Header; bytes constant PAYLOAD_577 = - hex"41020000000000000303030303030303030303030303030303030303030303030303030303030303"; + hex"4102000000000000020000000303030303030303030303030303030303030303030303030303030303030303"; function _header(Digest[] memory digests) internal pure returns (Header memory) { return Header({ @@ -42,6 +42,7 @@ contract ApkCommitmentDigestTest is Test { ApkDigest memory digest = _header(digests).apkCommitment(); assertEq(digest.setId, 577, "set id decoded wrong; check little-endian"); + assertEq(digest.len, 2, "set size decoded wrong"); assertEq(digest.commitment, uint256(0x0303030303030303030303030303030303030303030303030303030303030303)); } diff --git a/modules/consensus/beefy/primitives/src/lib.rs b/modules/consensus/beefy/primitives/src/lib.rs index 865b70021..84954b0c1 100644 --- a/modules/consensus/beefy/primitives/src/lib.rs +++ b/modules/consensus/beefy/primitives/src/lib.rs @@ -199,6 +199,12 @@ pub struct ApkCommitmentDigest { /// The BEEFY validator set the keys belong to, which is the relay's current set id plus one, /// since the commitment describes the next set. pub set_id: u64, + /// How many validators are in that set, which is what the threshold is taken against. + /// + /// Carried here rather than read from the mmr leaf so everything a client believes about the + /// incoming set comes from one authenticated place. Taking the size from the leaf and the + /// commitment from here would leave the two able to describe different sets. + pub len: u32, /// Poseidon2 over the set's G1 keys, padded to the circuit width with the identity point. pub commitment: [u8; 32], } @@ -249,12 +255,21 @@ pub struct ApkConsensusState { } impl ApkConsensusState { - /// Record `commitment` for `set_id` if it names a set this state knows and has no commitment - /// for yet. Called with whatever a verified header carried, so an unknown set is ignored - /// rather than treated as an error. - pub fn learn_commitment(&mut self, set_id: u64, commitment: H256) { + /// Take in what a verified header says about an authority set. + /// + /// A header naming the set after the one this client is waiting for rolls the sets forward: + /// the relay has moved on and the current set is now the one that was next. A header naming a + /// set already held is ignored, so the same digest arriving in several headers changes + /// nothing, and a set already carrying a commitment is never overwritten. + pub fn learn_authority_set(&mut self, set_id: u64, len: u32, commitment: H256) { + if set_id > self.next_authorities.id { + self.current_authorities = self.next_authorities.clone(); + self.next_authorities = ApkAuthoritySet { id: set_id, len, apk_commitment: commitment }; + return; + } for set in [&mut self.next_authorities, &mut self.current_authorities] { if set.id == set_id && set.apk_commitment.is_zero() { + set.len = len; set.apk_commitment = commitment; return; } diff --git a/modules/consensus/beefy/verifier/src/apk.rs b/modules/consensus/beefy/verifier/src/apk.rs index 9ada1270f..3ef515cd9 100644 --- a/modules/consensus/beefy/verifier/src/apk.rs +++ b/modules/consensus/beefy/verifier/src/apk.rs @@ -77,19 +77,21 @@ pub fn verify_apk_consensus( verify_apk_mmr_update_proof::(trusted_state, proof.mmr, verifying_key)?; let headers = crate::verify_parachain_headers::(heads_root, proof.parachain)?; - // Forward chaining: a verified header may carry the commitment for a set this client has no - // keys for yet. Picking it up here is what makes the next update verifiable at all, and is - // why the digest names the next set rather than the current one. + // Forward chaining: everything this client believes about the incoming set comes from here, + // its id, its size and its commitment together. The mmr leaf names a next set too, but says + // nothing about Poseidon2, and taking the size from there and the commitment from here would + // leave the two free to describe different sets. // // Only `digest_para_id`'s headers are read. A proof carries whichever parachains the relay // finalized, and every one of them is proven against the heads root, so a digest from another // parachain is authentic yet says nothing about this relay's authorities. Left unfiltered any // parachain could name the keys this client trusts next. - for header in headers.iter().filter(|header| header.para_id == digest_para_id) { - if let Some(digest) = read_apk_digest(&header.header) { - state.learn_commitment(digest.set_id, H256(digest.commitment)); - break; - } + if let Some(digest) = headers + .iter() + .filter(|header| header.para_id == digest_para_id) + .find_map(|header| read_apk_digest(&header.header)) + { + state.learn_authority_set(digest.set_id, digest.len, H256(digest.commitment)); } Ok((state, headers)) @@ -143,14 +145,6 @@ pub fn verify_apk_mmr_update_proof( crate::verify_mmr_leaf::(&mmr.latest_mmr_leaf, &mmr.mmr_proof, mmr_root)?; - // The leaf names the incoming set and its size, but says nothing about Poseidon2, so its - // commitment starts empty and waits for a digest. - let next = &mmr.latest_mmr_leaf.beefy_next_authority_set; - if next.id > trusted_state.next_authorities.id { - trusted_state.current_authorities = trusted_state.next_authorities.clone(); - trusted_state.next_authorities = - ApkAuthoritySet { id: next.id, len: next.len, apk_commitment: H256::zero() }; - } trusted_state.latest_beefy_height = mmr.commitment.block_number; trusted_state.mmr_root_hash = mmr_root; diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index 10bd22e66..4112a351e 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -81,9 +81,16 @@ pub mod pallet { #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); + /// Whether the next block should carry the relay's authority keys in its proof. + /// + /// Set by the block that sees the set id move, read by [`crate::wants_keys`] when the next + /// block is built. Keeping the keys out of the proof the rest of the time is the point. + #[pallet::storage] + pub type KeysWanted = StorageValue<_, bool, ValueQuery>; + /// The last commitment published to a header digest, and the set it describes. #[pallet::storage] - pub type Published = StorageValue<_, (u64, [u8; 32], [u8; 32]), OptionQuery>; + pub type Published = StorageValue<_, (u64, u32, [u8; 32], [u8; 32]), OptionQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -118,30 +125,52 @@ pub mod pallet { } impl Pallet { - /// Absorb the next chunk, starting or restarting if the set changed, and publish once the - /// whole set is in. + /// Publish this block's digest, committing to a new set when the relay has rotated. + /// + /// The set id is in every proof and costs nothing to read. The keys are only asked for + /// once the id has moved, so the block that notices a rotation records that it needs them + /// and the next one gets them. That keeps 1024 keys out of the relay proof on the ordinary + /// block, which is every block but one or two a session. fn advance() -> Result> { - let keys = Self::relay_beefy_g1_keys()?; - // Read the set id up front even though it is only needed at the end. It is cheap, and - // discovering it missing after hashing a whole set would throw that work away. let set_id = Self::relay_beefy_set_id()?; - let set_digest = sp_io::hashing::blake2_256(&keys.encode()); + // Nothing has moved, so the commitment already computed still describes this set. + if let Some((published, len, _, commitment)) = Published::::get() { + if published == set_id { + Self::deposit_digest(set_id, len, commitment); + return Ok(false); + } + } + + // The id moved. The keys are only in the proof if a previous block asked for them, + // so the first block after a rotation asks and the next one does the work. + let Ok(keys) = Self::relay_beefy_g1_keys() else { + KeysWanted::::put(true); + // The old commitment is still the truth about the old set, and a client files + // commitments by set id, so republishing it under the id it was computed for + // keeps this block's header useful rather than empty. + if let Some((published, len, _, commitment)) = Published::::get() { + Self::deposit_digest(published, len, commitment); + } + return Ok(false); + }; + KeysWanted::::kill(); + + let set_digest = sp_io::hashing::blake2_256(&keys.encode()); + let len = keys.len() as u32; match next_step(Published::::get().as_ref(), set_digest) { - // The keys have not changed, so the commitment stands. It still goes in this - // header, and under the current set id: a session can rotate without changing - // membership, and a client tracks commitments per set id, so the same commitment - // has to be published again under the new one or the client never learns it. + // The membership has not changed, only the id, which is common. Rehashing would + // cost the better part of a second for a commitment already held. Step::Republish(commitment) => { - Self::deposit_digest(set_id, commitment); - Published::::put((set_id, set_digest, commitment)); + Self::deposit_digest(set_id, len, commitment); + Published::::put((set_id, len, set_digest, commitment)); Ok(false) }, Step::Commit => { let commitment = commit(&keys).map_err(|_| Error::::MalformedAuthorityKey)?; - Self::deposit_digest(set_id, commitment); - Published::::put((set_id, set_digest, commitment)); + Self::deposit_digest(set_id, len, commitment); + Published::::put((set_id, len, set_digest, commitment)); Self::deposit_event(Event::CommitmentPublished { set_id, set_digest, @@ -159,8 +188,8 @@ pub mod pallet { /// with no further proof. It goes in every header the set covers rather than only the one /// where the hashing finished, since a verifier only ever sees the single header a proof /// finalizes and cannot choose which. - fn deposit_digest(set_id: u64, commitment: [u8; 32]) { - let payload = ApkCommitmentDigest { set_id, commitment }; + fn deposit_digest(set_id: u64, len: u32, commitment: [u8; 32]) { + let payload = ApkCommitmentDigest { set_id, len, commitment }; frame_system::Pallet::::deposit_log(sp_runtime::DigestItem::Consensus( APK_ENGINE_ID, payload.encode(), @@ -234,6 +263,16 @@ impl WeightInfo for () { } } +/// Whether the next block's relay proof should carry the authority keys. +/// +/// A runtime answers `KeyToIncludeInRelayProof` with this: asking for 1024 keys on every block +/// costs proof size for data that only changes once a session. The block that sees the set id +/// move records that it wants them, and this reports it while the next block is built. Also true +/// before anything has been published, since the first commitment has to come from somewhere. +pub fn wants_keys() -> bool { + KeysWanted::::get() || Published::::get().is_none() +} + /// Throw away a commitment computed under an older scheme. /// /// A commitment is only recomputed when the membership changes, so a runtime upgrade that changes @@ -276,9 +315,9 @@ pub enum Step { /// session rotating without the membership changing, which is common: the commitment is the same, /// so rehashing it would spend the better part of a second for nothing, but it still has to be /// republished under the new set id or a client tracking commitments per set never learns it. -pub fn next_step(published: Option<&(u64, [u8; 32], [u8; 32])>, set_digest: [u8; 32]) -> Step { +pub fn next_step(published: Option<&(u64, u32, [u8; 32], [u8; 32])>, set_digest: [u8; 32]) -> Step { match published { - Some((_, digest, commitment)) if *digest == set_digest => Step::Republish(*commitment), + Some((_, _, digest, commitment)) if *digest == set_digest => Step::Republish(*commitment), _ => Step::Commit, } } @@ -394,7 +433,7 @@ mod tests { #[test] fn keys_already_committed_to_are_republished_rather_than_rehashed() { - let published = (1u64, SET_A, [7u8; 32]); + let published = (1u64, 2u32, SET_A, [7u8; 32]); assert_eq!(next_step(Some(&published), SET_A), Step::Republish([7u8; 32])); } @@ -403,14 +442,14 @@ mod tests { /// already hold, so the stored one is republished under the new set id instead. #[test] fn the_same_keys_under_a_new_set_id_are_not_rehashed() { - let published = (1u64, SET_A, [7u8; 32]); + let published = (1u64, 2u32, SET_A, [7u8; 32]); assert_eq!(next_step(Some(&published), SET_A), Step::Republish([7u8; 32])); } /// Different keys mean the stored commitment says nothing about them. #[test] fn a_new_set_is_committed_to_even_though_another_was_published() { - let published = (1u64, SET_A, [7u8; 32]); + let published = (1u64, 2u32, SET_A, [7u8; 32]); assert_eq!(next_step(Some(&published), SET_B), Step::Commit); } @@ -427,7 +466,7 @@ mod tests { /// normal case: aura and the parachain system both write their own. #[test] fn commitment_is_found_among_other_digest_items() { - let payload = ApkCommitmentDigest { set_id: 577, commitment: [3u8; 32] }; + let payload = ApkCommitmentDigest { set_id: 577, len: 2, commitment: [3u8; 32] }; let digest = sp_runtime::generic::Digest { logs: alloc::vec![ sp_runtime::DigestItem::PreRuntime(*b"aura", alloc::vec![1, 2, 3]), @@ -452,7 +491,7 @@ mod tests { /// the same. This is what the engine id is for. #[test] fn another_engines_consensus_item_is_ignored() { - let payload = ApkCommitmentDigest { set_id: 1, commitment: [9u8; 32] }; + let payload = ApkCommitmentDigest { set_id: 1, len: 2, commitment: [9u8; 32] }; let digest = sp_runtime::generic::Digest { logs: alloc::vec![sp_runtime::DigestItem::Consensus(*b"BEEF", payload.encode())], }; @@ -463,9 +502,9 @@ mod tests { /// out of a header. #[test] fn digest_payload_round_trips() { - let payload = ApkCommitmentDigest { set_id: 42, commitment: [7u8; 32] }; + let payload = ApkCommitmentDigest { set_id: 42, len: 3, commitment: [7u8; 32] }; let encoded = payload.encode(); - assert_eq!(encoded.len(), 8 + 32, "set id then commitment, no padding"); + assert_eq!(encoded.len(), 8 + 4 + 32, "set id, size, then commitment, no padding"); assert_eq!(ApkCommitmentDigest::decode(&mut &encoded[..]).unwrap(), payload); } } diff --git a/parachain/runtimes/gargantua/src/lib.rs b/parachain/runtimes/gargantua/src/lib.rs index edff09cf2..c453cfcf4 100644 --- a/parachain/runtimes/gargantua/src/lib.rs +++ b/parachain/runtimes/gargantua/src/lib.rs @@ -1294,29 +1294,33 @@ impl_runtime_apis! { impl cumulus_primitives_core::KeyToIncludeInRelayProof for Runtime { fn keys_to_prove() -> cumulus_primitives_core::RelayProofRequest { - // The relay chain's BEEFY authority set, `Beefy::Authorities`. The collator only puts - // keys in the relay state proof that were asked for here, and the proof is checked - // against the relay parent's state root by the validators, so reading the set out of - // it needs no trust beyond what a parachain already places in its relay parent. + // The collator only puts keys in the relay state proof that were asked for here, and + // the proof is checked against the relay parent's state root by the validators, so + // reading them out needs no trust beyond what a parachain already places in its relay + // parent. // - // Note this is not `well_known_keys::AUTHORITIES`, which is `Babe::Authorities`; both - // end in twox128("Authorities") but differ in the pallet prefix. - // `Beefy::NextAuthorities` and `Beefy::ValidatorSetId`. The next set rather than the - // current one, so a client verifying a header signed by set N learns the commitment - // for N+1 and can verify the following update. The set id comes along because the - // commitment is not usable without knowing which set it describes. - cumulus_primitives_core::RelayProofRequest { - keys: alloc::vec![ - cumulus_primitives_core::RelayStorageKey::Top( - pallet_beefy_apk_digest::RELAY_BEEFY_NEXT_AUTHORITIES.to_vec() - ), - cumulus_primitives_core::RelayStorageKey::Top( - pallet_beefy_apk_digest::RELAY_BEEFY_VALIDATOR_SET_ID.to_vec() - ), - ], + // The keys wanted are `Beefy::NextAuthorities`, the next set rather than the current + // one so a client verifying a header signed by set N learns the commitment for N+1, + // and `Beefy::ValidatorSetId`, without which the commitment says nothing about which + // set it describes. Note the former is not `well_known_keys::AUTHORITIES`, which is + // `Babe::Authorities`; both end in twox128("Authorities") but differ in the prefix. + // + // The set id goes in every block, since it is one entry and it is what tells the + // pallet a rotation happened. The keys are asked for only when the pallet says it + // needs them, which is the block after a rotation, so the ordinary block does not + // carry a thousand keys it already has a commitment for. + let mut keys = alloc::vec![cumulus_primitives_core::RelayStorageKey::Top( + pallet_beefy_apk_digest::RELAY_BEEFY_VALIDATOR_SET_ID.to_vec() + )]; + if pallet_beefy_apk_digest::wants_keys::() { + keys.push(cumulus_primitives_core::RelayStorageKey::Top( + pallet_beefy_apk_digest::RELAY_BEEFY_NEXT_AUTHORITIES.to_vec(), + )); } + cumulus_primitives_core::RelayProofRequest { keys } } } + } #[cfg(feature = "try-runtime")] impl frame_try_runtime::TryRuntime for Runtime { From 225d695d5f4788d36c454498033665db9e71686c Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Mon, 17 Aug 2026 21:14:23 +0100 Subject: [PATCH 38/48] rebuild the fixtures for the header carried authority set --- evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex | 2 +- evm/tests/foundry/fixtures/bls-apk-beefy-state.hex | 2 +- modules/consensus/beefy/verifier/src/test.rs | 12 ++++++++---- modules/pallets/beefy-apk-digest/src/lib.rs | 6 +++--- parachain/runtimes/gargantua/src/lib.rs | 1 - 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex b/evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex index 5bf08f683..ac01ff318 100644 --- a/evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex +++ b/evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex @@ -1 +1 @@ -0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000478237b877864af98fe7a2d05f4cc0d78eb019d094da6d70c242f95609ee577c42ca62abc23bb3594dc8f5a3516e5c610cbe3f263a5dc33d9d8d0bc631567cf630c4df86e4ad78887f62f46e1e0daa52bd58eee1613cf211518e561b582b0990868ddaca3321f5ac0f7c57e4bf34b86726e874e5ae4476a08f4c74c21fcb4a9ee94ee8572a6fb82afba59f47745e9d51436fc8946294306c5448880df4c41f82a136985e8963070f035906bcc766f9e9125d3ea66cbc1b293ca2ef37a6c581812dd734fad4bd53feef8ac36b393bfd78ba789203a056f9cafcfc91e8e9afc5a64b724f54d18e983996397faa28bf63713d81c18fec214be9a4536400258d08c9f34acb58680d7b7150f23aadd0e10da0d1e7dfe476a2029a700545fe7a1001000000000000000000000000000000000000000000000000000000000000004a007a3bbf651a6efc01fe37e0370c74b18c76e9f5915212a2788af56916a94ab253c75417d6cd6af074f0b874b776a9ebf0a84b90d3bfaa8a785bf84bc1dbba61fca8529a8095566581d0e850ab0c84e41a783866f66318b174fe677844d628c580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cea139807d981ce3de2cc68a5cb30192a703df280bcba7a43f0e7ec31ba5083a6cd60000000000000000000000000000000000000000000000000000000000000c950000000000000000000000000000000000000000000000000000000000000002d1cbb453472425036cef3caa539a0e81c6bd9fd9c3f24de7b38867380a19cafbee367e5c724882cb248462660bc295655c4adf0a5c4e506d0bbf200d4ad992fd000000000000000000000000000000000000000000000000000000000000cea100000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000cea20000000000000000000000000000000000000000000000000000000000000c94000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d6800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020ab4fc7e08d1516f91d284155b64822f7a7209218d6855ddc7a21df7a54cc7d3200000000000000000000000000000000000000000000000000000000000004a0185e65ae41f08d2573dde4132033fcea9bd89945cef1d1490f95a462b969fbb4b090eb5665edab5bd168038779247fef0be93c639b4055537578770fcccf2c7b24ec6c4814a2d7df1fedd5f1dd17a7805fac2da24375dbed726f2ac5dda9de470565de94f494a48971696893aee9e8782b49d8e53cbcb342781dcf8c2482bd88b1db484e1dc529cfe6a3ca825f982502028f2739d993170b4c44b5cb02a4659ced3851b0277d4b6e1fd9db2024a29ec953cf3f9216ba605c7d33fb8403f08763125144cbffe208264650e49dacd0b941bc3833442205bb3e81b373443f9fb6ea3d1a54b6c8a2380148d55359b2823ec915a2d68ada1f1c9dbdfbf0348884bab8b20a243df308485755890eb8c073309dc95d8de61c2e5635c868551c44ca0e240d6ee324a56613ab5a2361cb19654bf628d16d851842b6655ed6e8488332bb16e69cec25ccb4d667e806d6ce861a22aa006f2128bf8b83be85f6492931f40a5d666354e16b9e9a7d40ab956bccc7d666e01ab658ab0d7f155b3ab94ee3a3ff25106fd9455e1a8558ca0b88c41071146fc22376e518032da4275d5151a4ebb4d142068d839359111d96b1947c5c00f30e188bc5ff80446f7a1bf7b4a65038a31d6aa39ab7155c946ee3d53627b9e3ea4fd3c4dbade70ac9a4c06229be1f6ee72e05894d728c5c09e92aa012e55c2668d45ea9856a318bb7121a316dd2d1b5d69093b35fda0156355481834b65630662ff0a5fc7425b0707ab48e798a486a57180f9ede7f9d63bc218bc0fdc38ae33912d3b767e64054f199ee6f51a5fddb95acb23a3df069c80686478db2988afe5e296ba8ad677637eea7cf54b6a21c964ed044de4fadf835d2fdda99e4e6bd04efc0213617481479a99bab16d5f63a3ba03cb33e9e6361ce97763ff5aa4ae7b16504701633e979aa368127a5d3d98f2b4f0563955e597d60a847b9218ced941b7f1aead2b914c24127d76bb5c7ddfcf92789338f7138990d5a9a1190a7076bf23c8b6a8c118b524fd480098ac104b61973c220e0d3c19a6c0a8d3063244fc9e3473bf0928b93d77934716eb52332c3dd45184614e9cb57d0536f0c7e598b67a5216e40fff5ce398dced77721884fb9b54d12a550d176b8a66976ebfd45d7bcfe4a8195fa1f6d31af2374f12b454c7a88ddbbd3e05345c8f5a6d9e41dfbd363630abf7e1265ce7155e72c64df8861521c4257119de3b385be9f3689e2bef572a8bb98d2c499332f8ebc59eec3fa67d42a6797e67d8bf770f329d2ed9c63349d96c5acf005747f5feaf6001fe5989c4e5bd06a45d9a17abb84cc0ac9fcbbc52c97499f7f1c704a7aa96114cbbbf3d44ab74a5c606fb8ba3058e452d8208469d23747c3e741f85c9a9efd0b2289a8c12271c70ed61a41190f61e64b6151ab092844a3ce80f3d471daf39c8f0f274554e7f8654ae2d544e3786cbf878919a8a7ef737603883ede4e710c667f9760d5cd23682daf7093cd588bd2a3980a8f2856489a06d9a07a67d1c582f167b68170e011f2c3cfc18c3c55a7c7fe3d9946c84aee503617419d17ed574c5b02a477a857d1300ad6e5f26a0da49f2fb3282ad61a9510e04ea19495f99dbb72ea9bd3cd7772a4a95732d08076aca1f6205b939877c4785a6a9d63216956dbfb6db53eccfec2eedcb1700000000000000000000000000000000000000000000000000000000000000081f78d05de33bffdf649b225755be3ae579b9fe2c1ae2e73295ee3c1904749320ca25bfe03fb0fedfccfaf1f200f3b4e22a5ddbd0293584456e1a92ea1ec2ff4c4cabe2d0c561af881878ffc013b6571b30b3706d6f277562b8cbfb554fc687df5de7a8c210f658f6e9cb86a281b9678670d5979ec4d51b8afb1026bc13803f67a1e54b4f5f6cd45c6dd2358f4d818b87567cec11ed5e24248ce5900f951252000b0eb10b670b86e81f9436bbf3182bc0f1288e16e3b3f06959785e4f54b23376fdf849f6d2da710f81057cc27c823a69d2cf307ae5c5e4b90f1f9012b15761799267ea908b4ec6d875861e235ee2f18fc1e03d4efa33fa3d2486b940807b31f8000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa900000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000167122eb76026540e83a8bae5a06a4c0678f65cb9424e8da84a18fb045e06a399f25aa7010092737eeae54ee05dee6fd6f608a989c6bddb405f914f6350852e54e75c2d32314a4c1da04ca749c8a601667941b5118ff02617388b94d32df5df6976ec31193a180661757261205b80c011000000000452505352906a053b20cb874226c3e4d8e6d51026b29b86153fc14f043796352f563f4d0129723a03000449534d5001013ebf160e4a9178592e46584385a6eaa86d06c7f4d6a1fe9ca8976fbe379286199781e23ba03a6c017ebe6dfd9d23459b90aa76e95d60837ab7ef0177f468bdd8044953544d202202836a000000000441504b43a0940c00000000000013ba45ba08c2f9519d727dbefbc799cc1396a6c03f6438003c2c3950cab05e250561757261010124e68e3825a619966b562cdab9790ebe89bf1df354e14c2e90d6861955aa4b6f7b4f90642726f7a35eba0f2e51720d3f9a21efac1b7f52d8edb8315f7619dc8f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file +0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000478237b877864af98fe7a2d05f4cc0d78eb019d094da6d70c242f95609ee577c42ca62abc23bb3594dc8f5a3516e5c610cbe3f263a5dc33d9d8d0bc631567cf630c4df86e4ad78887f62f46e1e0daa52bd58eee1613cf211518e561b582b0990868ddaca3321f5ac0f7c57e4bf34b86726e874e5ae4476a08f4c74c21fcb4a9ee94ee8572a6fb82afba59f47745e9d51436fc8946294306c5448880df4c41f82a136985e8963070f035906bcc766f9e9125d3ea66cbc1b293ca2ef37a6c581812dd734fad4bd53feef8ac36b393bfd78ba789203a056f9cafcfc91e8e9afc5a64b724f54d18e983996397faa28bf63713d81c18fec214be9a4536400258d08c9f34acb58680d7b7150f23aadd0e10da0d1e7dfe476a2029a700545fe7a1001000000000000000000000000000000000000000000000000000000000000004a00cfc1b98cbe231e410d38586056db0380342d3924132f5558fca47f114b115ce069141a10715c776f6803816dda0aa3e0b19b325ee82a185ef5444cc550210e36a1a195b179246a74d6bba640579206f732b8204486f05f87794b58271f6854c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ddc0be2e7e8ce0baf653c480d6ed7849a0979b9aa841c21e6b6b0057a3eb44df4d460000000000000000000000000000000000000000000000000000000000000d600000000000000000000000000000000000000000000000000000000000000002d1cbb453472425036cef3caa539a0e81c6bd9fd9c3f24de7b38867380a19cafb5e8a8e4891aeb85777b580c2069c95bdc7a3f5630298d758f1ff6f9726741ebe000000000000000000000000000000000000000000000000000000000000ddc000000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000ddc10000000000000000000000000000000000000000000000000000000000000d5f000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000206d68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000201ef1c9fbd3f1f3a0ef08bab0caad7d04727dd985d5de2ba02e533d8e74fbbcc000000000000000000000000000000000000000000000000000000000000004a0070c46495a760b79d5461897a1aba3fc131f3e4b6fe427beaada6bf1615fae618da38715385099a33ac9ac90cc32b7bf008e3727f6b4efd1b6fded44a5ea4ab9ab001ab8beae65834ac7eb1bb154ce23c723bb4818ea2061fb3f37179d05b3061465de909840a93df73cddc8a01bc353f70137dd88f44d8586ea3bd346ca8606bfff2b143ecf874136e8b2ee6bb7ba1618cdfe20106e73de00e03ed4d77344d391da0990999c9e8e38a0f3c42be960246f7524a32abe6d0e4ddcd7699d29b9ac10b47bf8ca31ed34be97bd48c4b89c3011a85b41e04f2f606d46501991b06176f6b349c338c47168c37663aea1e5d6ff0b25721afc71e6624cdff91299d02d077877bdc08310ad6a3160ab34bbd55472c804096041375ca8474099303443ef5704004f1dd0f00b85667ca370e04e23c51cbcfd8a5adaeb674d363415d72669ecb182a65ebdc40c35daa0efc2f6e6a97309c84232e04e599be9c800717ba8b463c38dd1cf40bf9c8a745e86294e6cdb87757de4966ca2d11cf58c226231cdcfd604e95ec39478bd8d31e20be5bd70a7f0330909ea875c3a9bc4520d4656c5b2a798bf71bbf3ed9e6f002a147d9d2025310d6b1f539b3850bd1f93d3751123c671bdc7f204885a608ba53761c53dcf462042523681e05605bf62f03dbd4798fffd0831602cd7f808faea724b56c4273281ec0f5d12b7efd95d18e182393266c1d1e4ccc119c6d03628d7d162b23c5e58f7129a3f6c044d89674f4406ab51f7cdb030ce47b9d0f540e7150411802535308ac23188c1af4b05400932923e5648dc0b58cca4d61736bba4b64112c17fb1391bbaf97e971c2072eb9a840274a53eb30c39f14d302fc04d60e858130b34a33a123726a11e1ba15cf816a58a1c1dae7ddc41f75a4d6b0abef7d5a650f42758566fea8918096351372a0e5ecffce47840d20bc8ecd0cb520d70a52257bbc0d1e9142eefe3c7e8eab128b7ad82d60cdc75405f01ec4abfa624e6f8a14e781f1bfb76963183b56738595d8680b9cd18ad80cf08ba3614153b1d19669ab8611c5b87c0081711be76021834804a3d0aed23ce364e92535c74093ee74590aa25b31a5f691353d76a34dcefda72842fc8d6fdb097e3e93dc775ce13418fb687985473b87c90b4c7647e990c8d61e6b7924c835acb585f3148ad13a4d266c87d968dbc829ea55c746ade51e7bce0f512cf837b8dba145bc0f01ad88d7b4575fec9e334c4447bef5f1e380a6fa89b999552d741d244dec70ca4b0bc5c032a0f501786f35b1701eb96566e4bf0b9cc08494f16efe35fe84cda2eac02c0404706054140551c1f99e54cb68d6d0ddb6367529fb9e90d68107c3b7376472d65114bb62b7ff166e1d62e57224ffd50ecc0aaca92a2e295c7b94dbf1e008f8da78589f8f11361f71c0759506845a0d34dfd9587de815ab1987d61548a73ded3be5e34221d5e6d23adc92491144e05811c4903a679aa495fce087d6e6e68500c6d0a8596545f602e5f361a06ce737cd48d64a63f54a4aae14b0abf4e8b1b6dd71e74b7f5a97b866dc2326f62ecdc26ef5444f31a32ec0493c8455ea0e2f5a7a28a098bd39d2b1170b20801c3c3e3b426ebe02eaa30b2383fa28392258e75786995c9d7aa79c56385af76a749a327b0921d408881f50db824a200000000000000000000000000000000000000000000000000000000000000081f78d05de33bffdf649b225755be3ae579b9fe2c1ae2e73295ee3c1904749320ca25bfe03fb0fedfccfaf1f200f3b4e22a5ddbd0293584456e1a92ea1ec2ff4cd7eb776484949cc240142bd6508a5464a38c83ff4a633e7626b87468c17ed23803c783cc3af7700fc480092dadd7adeeb06516bd3b4a9b332af85ca9b43520015272786b444bcb3103b6d70a2d86207d873ba3798546f547fd5f7c8e5a2f730023eefd36e7bed259a0c0d81482a73634f7de9e05cfa7ec3a71a0bf51da2ff270e1083a4f191b947f14ae84cde920ef9b9dde8d51ed81e66f60392c46144424d2e4209d7f8db235d524eec7b11cee2e04060c1a6e246cf1e5a26add7604f15271000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fa90000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000016b5191bd2000ca420ee55c8aa61d09e62e33583c55f7e195638168831480525fcf46c60100693a7866d50c3a7048b2b8986cfd0b08cfa08bdec6822ed019afbaef8f8b79f71f3419409caf222892e56b0abc324eb232ec626e000df4cc1bc02bee948e933418066175726120b391c0110000000004525053529059f517784713bb59c71fc8b481358a2e92075ccb6e2827a0b91aebda0891f5f1ee7603000449534d5001013ebf160e4a9178592e46584385a6eaa86d06c7f4d6a1fe9ca8976fbe379286199781e23ba03a6c017ebe6dfd9d23459b90aa76e95d60837ab7ef0177f468bdd8044953544d20326a836a000000000441504b43b05f0d0000000000000200000013ba45ba08c2f9519d727dbefbc799cc1396a6c03f6438003c2c3950cab05e25056175726101019e9bdb3e47a71e2da61b0d7fd1f98794a56c3569d377162c21bbbc184de9c462390cdbbf03a8b3723a1cd5d98772f6d00226ea267209b664a8727b14627c25820000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex b/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex index 644763152..4410479fd 100644 --- a/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex +++ b/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex @@ -1 +1 @@ -0x000000000000000000000000000000000000000000000000000000000000ce9e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c93000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c94000000000000000000000000000000000000000000000000000000000000000213ba45ba08c2f9519d727dbefbc799cc1396a6c03f6438003c2c3950cab05e25 \ No newline at end of file +0x000000000000000000000000000000000000000000000000000000000000ddbd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d5e000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d5f000000000000000000000000000000000000000000000000000000000000000213ba45ba08c2f9519d727dbefbc799cc1396a6c03f6438003c2c3950cab05e25 \ No newline at end of file diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index 3693954ff..dd55b544e 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -38,7 +38,11 @@ use polkadot_sdk::sp_consensus_beefy::{ }; use sp_mmr_primitives::LeafProof; -use crate::{EcdsaRecover, error::Error, verify_consensus, verify_mmr_update_proof}; +use crate::{ + EcdsaRecover, + ecdsa::{verify_consensus, verify_mmr_update_proof}, + error::Error, +}; struct TestHost; @@ -1267,7 +1271,7 @@ fn bls_apk_live_fixture() { let authority_set = |id: u64, len: u64| BlsBeefy::AuthoritySetCommitment { id, len: len as u32, - apkCommitment: if id == signing_set_id { apk_commitment } else { FixedBytes::ZERO }, + root: if id == signing_set_id { apk_commitment } else { FixedBytes::ZERO }, }; let state = BlsBeefy::BeefyConsensusState { @@ -1280,8 +1284,8 @@ fn bls_apk_live_fixture() { nextAuthoritySet: authority_set(u64_of(&trusted["nextId"]), u64_of(&trusted["nextLen"])), }; assert!( - state.currentAuthoritySet.apkCommitment != FixedBytes::ZERO || - state.nextAuthoritySet.apkCommitment != FixedBytes::ZERO, + state.currentAuthoritySet.root != FixedBytes::ZERO || + state.nextAuthoritySet.root != FixedBytes::ZERO, "neither trusted set matches the signing set, so the client would have no commitment", ); diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index 4112a351e..c6c91bbb3 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -73,9 +73,9 @@ pub mod pallet { type WeightInfo: WeightInfo; } - /// Bumped whenever the commitment changes shape, so [`migration`] knows to throw away what - /// was published under the old one. - pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + /// Bumped whenever the commitment or the record around it changes shape, so [`migration`] + /// knows to throw away what was published under the old one. + pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] diff --git a/parachain/runtimes/gargantua/src/lib.rs b/parachain/runtimes/gargantua/src/lib.rs index c453cfcf4..59cd2de82 100644 --- a/parachain/runtimes/gargantua/src/lib.rs +++ b/parachain/runtimes/gargantua/src/lib.rs @@ -1320,7 +1320,6 @@ impl_runtime_apis! { cumulus_primitives_core::RelayProofRequest { keys } } } - } #[cfg(feature = "try-runtime")] impl frame_try_runtime::TryRuntime for Runtime { From 07b1591e3d7e2b06e1cb42d795fc533e73bf0805 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 18 Aug 2026 03:04:24 +0100 Subject: [PATCH 39/48] take the commitment from the circuit's own crate instead of keeping a second implementation --- Cargo.lock | 27 +- Cargo.toml | 2 - .../consensus/beefy/apk-commitment/Cargo.toml | 32 -- .../consensus/beefy/apk-commitment/src/lib.rs | 303 ------------------ modules/consensus/beefy/verifier/Cargo.toml | 3 +- modules/consensus/beefy/verifier/src/test.rs | 22 +- modules/pallets/beefy-apk-digest/Cargo.toml | 13 +- modules/pallets/beefy-apk-digest/src/lib.rs | 9 +- tesseract/consensus/beefy/apk/Cargo.toml | 10 +- tesseract/consensus/beefy/apk/src/lib.rs | 16 +- 10 files changed, 45 insertions(+), 392 deletions(-) delete mode 100644 modules/consensus/beefy/apk-commitment/Cargo.toml delete mode 100644 modules/consensus/beefy/apk-commitment/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a11070874..940982aad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -958,7 +958,6 @@ dependencies = [ "alloy-primitives 1.5.7", "alloy-sol-types 1.5.7", "anyhow", - "apk-commitment", "ark-bls12-381 0.4.0", "ark-ec 0.4.2", "ark-ff 0.4.2", @@ -969,6 +968,7 @@ dependencies = [ "beefy-verifier", "beefy-verifier-primitives", "gnark-apk-prover", + "gnark-plonk-verifier", "hex", "ismp", "ismp-abi", @@ -982,18 +982,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "apk-commitment" -version = "0.1.0" -dependencies = [ - "ark-bls12-381 0.4.0", - "ark-ec 0.4.2", - "ark-ff 0.4.2", - "ark-serialize 0.4.2", - "hex", - "sha3 0.10.8", -] - [[package]] name = "approx" version = "0.5.1" @@ -2975,7 +2963,6 @@ dependencies = [ "alloy-primitives 1.5.7", "alloy-sol-types 1.5.7", "anyhow", - "apk-commitment", "ark-bls12-381 0.4.0", "ark-bls12-381 0.5.0", "ark-ec 0.4.2", @@ -8678,12 +8665,12 @@ dependencies = [ [[package]] name = "gnark-apk-ffi" version = "0.1.0" -source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=48e6aa504994a58fb41465e22eb90b1df6a190f8#48e6aa504994a58fb41465e22eb90b1df6a190f8" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=96df97f5df2da2124be590b254d5049fe99c8908#96df97f5df2da2124be590b254d5049fe99c8908" [[package]] name = "gnark-apk-prover" version = "0.1.0" -source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=48e6aa504994a58fb41465e22eb90b1df6a190f8#48e6aa504994a58fb41465e22eb90b1df6a190f8" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=96df97f5df2da2124be590b254d5049fe99c8908#96df97f5df2da2124be590b254d5049fe99c8908" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", @@ -8696,7 +8683,7 @@ dependencies = [ [[package]] name = "gnark-plonk-verifier" version = "0.1.0" -source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=48e6aa504994a58fb41465e22eb90b1df6a190f8#48e6aa504994a58fb41465e22eb90b1df6a190f8" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=96df97f5df2da2124be590b254d5049fe99c8908#96df97f5df2da2124be590b254d5049fe99c8908" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", @@ -14732,11 +14719,11 @@ dependencies = [ name = "pallet-beefy-apk-digest" version = "0.1.0" dependencies = [ - "apk-commitment", - "ark-bls12-381 0.4.0", - "ark-serialize 0.4.2", + "ark-bls12-381 0.5.0", + "ark-serialize 0.5.0", "beefy-verifier-primitives", "cumulus-pallet-parachain-system", + "gnark-plonk-verifier", "hex", "hex-literal 0.4.1", "log", diff --git a/Cargo.toml b/Cargo.toml index 4ee8126b3..9b751f926 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,6 @@ members = [ "modules/consensus/sync-committee/prover", "modules/consensus/sync-committee/verifier", "modules/consensus/sync-committee/primitives", - "modules/consensus/beefy/apk-commitment", "modules/consensus/beefy/primitives", "modules/consensus/beefy/prover", "modules/consensus/beefy/verifier", @@ -278,7 +277,6 @@ hyperclient = { path = "modules/hyperclient", default-features = false } subxt-utils = { path = "modules/utils/subxt", default-features = false } # consensus provers & verifiers -apk-commitment = { path = "./modules/consensus/beefy/apk-commitment", default-features = false } beefy-verifier-primitives = { version = "0.1.1", path = "./modules/consensus/beefy/primitives", default-features = false } beefy-prover = { path = "./modules/consensus/beefy/prover" } beefy-verifier = { path = "./modules/consensus/beefy/verifier", default-features = false } diff --git a/modules/consensus/beefy/apk-commitment/Cargo.toml b/modules/consensus/beefy/apk-commitment/Cargo.toml deleted file mode 100644 index ef40a41dd..000000000 --- a/modules/consensus/beefy/apk-commitment/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "apk-commitment" -version = "0.1.0" -edition = "2021" -authors = ["Polytope Labs "] -description = "Poseidon2 commitment over a BEEFY validator set's BLS12-381 G1 public keys" -license = "Apache-2.0" -repository = "https://github.com/polytope-labs/hyperbridge" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[dependencies] -# Pinned to 0.4 to match w3f-bls, so the runtime does not carry two arkworks versions. -ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = false } -ark-ec = { version = "0.4.0", default-features = false } -ark-ff = { version = "0.4.0", default-features = false } -ark-serialize = { version = "0.4.0", default-features = false } -sha3 = { version = "0.10", default-features = false } - -[dev-dependencies] -hex = { workspace = true, default-features = true } - -[features] -default = ["std"] -std = [ - "ark-bls12-381/std", - "ark-ec/std", - "ark-ff/std", - "ark-serialize/std", - "sha3/std", -] diff --git a/modules/consensus/beefy/apk-commitment/src/lib.rs b/modules/consensus/beefy/apk-commitment/src/lib.rs deleted file mode 100644 index b330cdbb6..000000000 --- a/modules/consensus/beefy/apk-commitment/src/lib.rs +++ /dev/null @@ -1,303 +0,0 @@ -//! `no_std` Poseidon2 commitment over a validator set's BLS12-381 G1 public keys. -//! -//! A port of `gnark-plonk-verifier`'s `commitment` module so it can run inside a substrate -//! runtime, where hyperbridge has to compute the same value the APK circuit binds to before it can -//! publish it in a header digest. Byte-compatible with the gnark circuit's `PublicKeysCommitment` -//! and with the Go reference `apk.NativePublicKeysCommitment`. -//! -//! Two changes from the upstream module, both forced by `no_std`: -//! -//! - the round keys are derived once per call and threaded through, rather than cached in a -//! `OnceLock`. The derivation is 62 keccaks against 12288 permutations for a full validator set, -//! so it does not show up in the cost. -//! - arkworks 0.4 rather than 0.5, to match `w3f-bls` and keep a single arkworks in the runtime. - -#![cfg_attr(not(feature = "std"), no_std)] - -extern crate alloc; - -use alloc::vec::Vec; - -use ark_bls12_381::{Fq, Fr, G1Affine}; -use ark_ff::{BigInteger, Field, PrimeField, Zero}; -use sha3::{Digest, Keccak256}; - -/// gnark-crypto default Poseidon2 parameters for BLS12-381 in compression mode. -const WIDTH: usize = 2; -const FULL_ROUNDS: usize = 6; -const PARTIAL_ROUNDS: usize = 50; - -/// Equals gnark-crypto's `Parameters.String()` for these parameters. The round keys are a -/// Keccak-256 chain seeded with it, so this string is consensus critical: change a character and -/// every commitment changes. -const SEED: &str = "Poseidon2-BLS12_381[t=2,rF=6,rP=50,d=5]"; - -/// Number of validator slots the APK circuit is fixed to. A shorter set is padded to this with the -/// identity point. -pub const NUM_VALIDATORS: usize = 1024; - -/// Round keys, reproducing gnark-crypto's `Parameters.initRC`: `rnd0 = Keccak(seed)`, -/// `rnd(k+1) = Keccak(rnd k)`, each key taken as `rnd mod r` big-endian. Full rounds carry -/// `WIDTH` keys, partial rounds one, since only lane 0 is keyed. -fn round_keys() -> Vec> { - let half_full = FULL_ROUNDS / 2; - let total = FULL_ROUNDS + PARTIAL_ROUNDS; - let mut rnd: [u8; 32] = Keccak256::digest(SEED.as_bytes()).into(); - let mut keys = Vec::with_capacity(total); - for round in 0..total { - let n = if round < half_full || round >= half_full + PARTIAL_ROUNDS { WIDTH } else { 1 }; - let mut row = Vec::with_capacity(n); - for _ in 0..n { - rnd = Keccak256::digest(rnd).into(); - row.push(Fr::from_be_bytes_mod_order(&rnd)); - } - keys.push(row); - } - keys -} - -/// In-place x^5 S-box. -#[inline] -fn sbox(x: &mut Fr) { - let base = *x; - x.square_in_place(); - x.square_in_place(); - *x *= base; -} - -/// External (full-round) MDS for t=2: `[[2,1],[1,2]]`. -#[inline] -fn mat_mul_external(s: &mut [Fr; WIDTH]) { - let sum = s[0] + s[1]; - s[0] += sum; - s[1] += sum; -} - -/// Internal (partial-round) matrix for t=2: `[[2,1],[1,3]]`. -#[inline] -fn mat_mul_internal(s: &mut [Fr; WIDTH]) { - let sum = s[0] + s[1]; - s[0] += sum; - s[1].double_in_place(); - s[1] += sum; -} - -/// The Poseidon2 permutation on a width-2 state. -fn permutation(state: &mut [Fr; WIDTH], rk: &[Vec]) { - let half_full = FULL_ROUNDS / 2; - let first_full = &rk[..half_full]; - let partial = &rk[half_full..half_full + PARTIAL_ROUNDS]; - let last_full = &rk[half_full + PARTIAL_ROUNDS..]; - - mat_mul_external(state); - for keys in first_full { - for (s, k) in state.iter_mut().zip(keys) { - *s += *k; - } - for s in state.iter_mut() { - sbox(s); - } - mat_mul_external(state); - } - for keys in partial { - state[0] += keys[0]; - sbox(&mut state[0]); - mat_mul_internal(state); - } - for keys in last_full { - for (s, k) in state.iter_mut().zip(keys) { - *s += *k; - } - for s in state.iter_mut() { - sbox(s); - } - mat_mul_external(state); - } -} - -/// 2-to-1 compression with feed-forward on the right input, matching gnark-crypto's -/// `Permutation.Compress`: `right + permutation([left, right])[1]`. -#[inline] -fn compress(left: Fr, right: Fr, rk: &[Vec]) -> Fr { - let mut s = [left, right]; - permutation(&mut s, rk); - right + s[1] -} - -/// Number of 64-bit limbs packed into one `Fr`. Must match `apk.LimbsPerElement` in the circuit. -const LIMBS_PER_ELEMENT: usize = 3; - -/// Decompose a coordinate into six little-endian 64-bit limbs, matching gnark's emulated -/// `BLS12381Fp` layout, and pack them [`LIMBS_PER_ELEMENT`] at a time into `Fr` as -/// `l[0] + l[1]*2^64 + l[2]*2^128`, least significant limb first. -/// -/// Three limbs span at most 192 bits, comfortably inside `Fr`, so the packing never wraps and -/// stays injective. It binds exactly as tightly as absorbing each limb on its own, at a third of -/// the compressions. -#[inline] -fn coord_packed(c: Fq) -> [Fr; 2] { - let limbs = c.into_bigint().0; - core::array::from_fn(|i| { - // The positional sum written out as big endian bytes, most significant limb first. - let mut be = [0u8; 8 * LIMBS_PER_ELEMENT]; - for j in 0..LIMBS_PER_ELEMENT { - let start = 8 * (LIMBS_PER_ELEMENT - 1 - j); - be[start..start + 8].copy_from_slice(&limbs[i * LIMBS_PER_ELEMENT + j].to_be_bytes()); - } - Fr::from_be_bytes_mod_order(&be) - }) -} - -/// The Poseidon2 commitment over `points`, in the circuit's absorption order: per point, the two -/// packed halves of `X` then the two of `Y`, absorbed through a Merkle-Damgard chain with a zero -/// IV. -/// -/// The caller supplies the same list the circuit binds to, which for a validator set means -/// registration order padded to [`NUM_VALIDATORS`] with the identity point. -pub fn public_keys_commitment(points: &[G1Affine]) -> Fr { - let rk = round_keys(); - let mut state = Fr::zero(); - for p in points { - let x = coord_packed(p.x); - let y = coord_packed(p.y); - for block in x.into_iter().chain(y) { - state = compress(state, block, &rk); - } - } - state -} - -/// The commitment as a 32-byte big-endian value, the `uint256 publicKeysCommitment` argument of -/// `ApkProof.verify`. -pub fn public_keys_commitment_bytes(points: &[G1Affine]) -> [u8; 32] { - let mut out = [0u8; 32]; - let be = public_keys_commitment(points).into_bigint().to_bytes_be(); - out[32 - be.len()..].copy_from_slice(&be); - out -} - -/// Pad a validator set to the circuit's fixed width with the identity point. -pub fn padded_to_circuit_width(keys: &[G1Affine]) -> Vec { - let mut points = keys.to_vec(); - points.resize(NUM_VALIDATORS, G1Affine::identity()); - points -} - -/// Resumable form of [`public_keys_commitment`], for absorbing a validator set a chunk at a time. -/// -/// A full set costs roughly 420ms in wasm, which does not fit in one block, but the chain is -/// sequential so it splits cleanly: absorb some points, keep the state, carry on next block. The -/// state is a single field element, so a runtime stores 32 bytes and a cursor between blocks. -/// -/// The caller is responsible for feeding the same points in the same order that -/// [`public_keys_commitment`] would, which for a validator set means registration order padded to -/// [`NUM_VALIDATORS`] with the identity point. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PartialCommitment { - state: Fr, -} - -impl Default for PartialCommitment { - fn default() -> Self { - Self::new() - } -} - -impl PartialCommitment { - /// A fresh chain, matching the zero IV the gnark hasher starts from. - pub fn new() -> Self { - Self { state: Fr::zero() } - } - - /// Absorb the next run of points. Round keys are derived once per call, so callers should - /// prefer fewer, larger chunks over many single-point calls. - pub fn absorb(&mut self, points: &[G1Affine]) { - let rk = round_keys(); - for p in points { - let x = coord_packed(p.x); - let y = coord_packed(p.y); - for block in x.into_iter().chain(y) { - self.state = compress(self.state, block, &rk); - } - } - } - - /// The commitment so far, as the 32-byte big-endian value the contract takes. Only meaningful - /// once every point has been absorbed. - pub fn finish(&self) -> [u8; 32] { - let mut out = [0u8; 32]; - let be = self.state.into_bigint().to_bytes_be(); - out[32 - be.len()..].copy_from_slice(&be); - out - } - - /// Encode the in-progress state for storage between blocks. - pub fn to_bytes(&self) -> [u8; 32] { - self.finish() - } - - /// Restore a state written by [`Self::to_bytes`]. - pub fn from_bytes(bytes: &[u8; 32]) -> Self { - Self { state: Fr::from_be_bytes_mod_order(bytes) } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_ec::AffineRepr; - - /// Ground truth from `circuits/apk/commitment_vectors_test.go`, which locks the Poseidon2 - /// digest over `k * G1::generator()` point sets. If gnark-crypto's parameters ever change - /// these move, and so does every commitment. - const VECTORS: [(usize, &str); 4] = [ - (1, "4df3ca8a29f6b37c04fefb167022ae638df17383caf668b718bf3b65aa320652"), - (2, "20b814b4a4cd0249ffee16a12c0e883eac49a18e91f104e0c777d7de9a797267"), - (3, "1d8d8ce5d1437ebe81c7a10c59d25ec6f53bffb9966d019f460157750a7a1cff"), - (10, "5f9529f2a793ad64450341a6ef732dc1e1b71ddcca7d83f3704ff5e637a4b3bd"), - ]; - - fn k_times_generator(n: usize) -> Vec { - let g = G1Affine::generator(); - (1..=n).map(|k| (g * Fr::from(k as u64)).into()).collect() - } - - #[test] - fn matches_the_gnark_vectors() { - for (n, expected) in VECTORS { - let got = hex::encode(public_keys_commitment_bytes(&k_times_generator(n))); - assert_eq!(got, expected, "commitment diverged from gnark for n={n}"); - } - } - - /// The resumable form must agree with the one-shot form for any split, since a runtime chooses - /// chunk sizes by weight and must not change the result by doing so. - #[test] - fn chunked_absorption_matches_the_one_shot_commitment() { - let points = padded_to_circuit_width(&k_times_generator(3)); - let expected = public_keys_commitment_bytes(&points); - - for chunk in [1usize, 7, 64, 512, NUM_VALIDATORS] { - let mut partial = PartialCommitment::new(); - for run in points.chunks(chunk) { - partial.absorb(run); - // round-trip through storage on every boundary, as a runtime would - partial = PartialCommitment::from_bytes(&partial.to_bytes()); - } - assert_eq!(partial.finish(), expected, "chunk size {chunk} changed the commitment"); - } - } - - #[test] - fn identity_padding_reaches_circuit_width() { - let keys = k_times_generator(2); - let padded = padded_to_circuit_width(&keys); - assert_eq!(padded.len(), NUM_VALIDATORS); - assert!(padded[2..].iter().all(|p| p.is_zero()), "padding is not the identity point"); - assert_ne!( - public_keys_commitment_bytes(&padded), - public_keys_commitment_bytes(&keys), - "padding must change the commitment, else set sizes collide" - ); - } -} diff --git a/modules/consensus/beefy/verifier/Cargo.toml b/modules/consensus/beefy/verifier/Cargo.toml index 80c8cb5a5..10649b76e 100644 --- a/modules/consensus/beefy/verifier/Cargo.toml +++ b/modules/consensus/beefy/verifier/Cargo.toml @@ -31,7 +31,7 @@ ark-serialize = { version = "0.5", default-features = false, optional = true } [dependencies.gnark-plonk-verifier] git = "https://github.com/polytope-labs/gnark-apk-proofs" -rev = "48e6aa504994a58fb41465e22eb90b1df6a190f8" +rev = "96df97f5df2da2124be590b254d5049fe99c8908" default-features = false optional = true @@ -52,7 +52,6 @@ ismp-abi = { workspace = true, default-features = true } # The APK fixture is built in two halves: this crate collects the live BEEFY data, the SNARK is # generated by `gnark-apk-proofs` out of tree, and the two meet over a json file. json = { workspace = true, default-features = true } -apk-commitment = { workspace = true, default-features = true } alloy-primitives = { workspace = true, default-features = true } subxt = { workspace = true, default-features = true } subxt-core = { workspace = true, default-features = true } diff --git a/modules/consensus/beefy/verifier/src/test.rs b/modules/consensus/beefy/verifier/src/test.rs index dd55b544e..3af84fe92 100644 --- a/modules/consensus/beefy/verifier/src/test.rs +++ b/modules/consensus/beefy/verifier/src/test.rs @@ -1149,15 +1149,6 @@ async fn bls_apk_live_inputs() { let aggregate = aggregate_signatures(&signatures).unwrap(); let sig_g1 = G1Affine::deserialize_compressed(&aggregate[..]).expect("aggregate decodes"); - // The same commitment the runtime pallet builds, computed here over the signing set so the - // fixture's consensus state can be seeded with it. - let decompressed = g1_keys - .iter() - .map(|k| G1Affine::deserialize_compressed(&k[..]).expect("G1 key decodes")) - .collect::>(); - let padded = apk_commitment::padded_to_circuit_width(&decompressed); - let apk_commitment_bytes = apk_commitment::public_keys_commitment_bytes(&padded); - let mmr = &message.mmr; let leaf_index = mmr.mmr_proof.leaf_indices.first().copied().unwrap_or_default(); let bundle = json::json!({ @@ -1171,7 +1162,6 @@ async fn bls_apk_live_inputs() { "apk": g1_packed(&apk_g1), "apk2": g2_packed(&apk_g2), "signature": g1_packed(&sig_g1), - "apkCommitment": hex::encode(apk_commitment_bytes), "mmrLeaf": { "parentNumber": mmr.latest_mmr_leaf.parent_number_and_hash.0, "parentHash": hex::encode(mmr.latest_mmr_leaf.parent_number_and_hash.1.0), @@ -1214,7 +1204,6 @@ async fn bls_apk_live_inputs() { mmr.mmr_proof.items.len(), message.parachain.parachains.len(), ); - println!("apk commitment 0x{}", hex::encode(apk_commitment_bytes)); println!("wrote {path}"); assert!( !message.parachain.parachains.is_empty(), @@ -1256,14 +1245,11 @@ fn bls_apk_live_fixture() { raw.chunks(32).map(FixedBytes::<32>::from_slice).collect() }; - // The commitment the proof was generated against has to be the one the client checks it with, - // so take it from the SNARK's own public inputs and require the runtime's version to agree. + // The commitment the proof was generated against is the one the client has to check it with, + // so it comes from the SNARK's own public inputs. That it matches what this workspace would + // compute is not asserted here any more: both would now be `gnark-plonk-verifier`, and the + // implementation is pinned against the go one by that crate's own vectors. let apk_commitment = fixed32(&snark["apkCommitment"]); - assert_eq!( - apk_commitment, - fixed32(&inputs["apkCommitment"]), - "the apk-commitment crate and the circuit disagree about the same validator set", - ); let trusted = &inputs["trusted"]; let signing_set_id = u64_of(&inputs["validatorSetId"]); diff --git a/modules/pallets/beefy-apk-digest/Cargo.toml b/modules/pallets/beefy-apk-digest/Cargo.toml index 075a7d002..90e595acd 100644 --- a/modules/pallets/beefy-apk-digest/Cargo.toml +++ b/modules/pallets/beefy-apk-digest/Cargo.toml @@ -12,15 +12,20 @@ codec = { workspace = true } scale-info = { workspace = true } log = { workspace = true } -apk-commitment = { workspace = true, default-features = false } beefy-verifier-primitives = { workspace = true, default-features = false } cumulus-pallet-parachain-system = { workspace = true, default-features = false } -ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = false } -ark-serialize = { version = "0.4.0", default-features = false } +ark-bls12-381 = { version = "0.5", features = ["curve"], default-features = false } + +ark-serialize = { version = "0.5", default-features = false } hex-literal = { workspace = true, optional = true } +[dependencies.gnark-plonk-verifier] +git = "https://github.com/polytope-labs/gnark-apk-proofs" +rev = "96df97f5df2da2124be590b254d5049fe99c8908" +default-features = false + [dependencies.polkadot-sdk] workspace = true features = ["frame-support", "frame-system", "sp-io", "sp-runtime"] @@ -36,7 +41,7 @@ std = [ "scale-info/std", "log/std", "polkadot-sdk/std", - "apk-commitment/std", + "gnark-plonk-verifier/std", "beefy-verifier-primitives/std", "cumulus-pallet-parachain-system/std", "ark-bls12-381/std", diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index c6c91bbb3..a726c2ff9 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -32,7 +32,6 @@ extern crate alloc; use alloc::vec::Vec; -use apk_commitment::{padded_to_circuit_width, public_keys_commitment_bytes}; use ark_bls12_381::G1Affine; use ark_serialize::CanonicalDeserialize; pub use beefy_verifier_primitives::{ @@ -42,6 +41,7 @@ use beefy_verifier_primitives::{PairedAuthority, BLS_G1_SIGNATURE_LEN}; use codec::{Decode, Encode, MaxEncodedLen}; use cumulus_pallet_parachain_system::RelayChainStateProof; use frame_support::weights::Weight; +use gnark_plonk_verifier::{padded_to_circuit_width, public_keys_commitment_bytes}; use polkadot_sdk::*; use scale_info::TypeInfo; @@ -342,8 +342,8 @@ pub fn commit(keys: &[[u8; BLS_G1_SIGNATURE_LEN]]) -> Result<[u8; 32], Malformed #[cfg(test)] mod tests { use super::*; - use apk_commitment::{padded_to_circuit_width, public_keys_commitment_bytes}; use ark_serialize::CanonicalSerialize; + use gnark_plonk_verifier::{padded_to_circuit_width, public_keys_commitment_bytes}; /// Real G1 halves from a live BLS relay's `Beefy` authorities. const RELAY_KEYS: [&str; 2] = [ @@ -379,8 +379,9 @@ mod tests { /// An empty set is all padding, and must still be well defined. #[test] fn an_empty_set_is_all_padding() { - let all_identity: Vec = - (0..apk_commitment::NUM_VALIDATORS).map(|_| G1Affine::identity()).collect(); + let all_identity: Vec = (0..gnark_plonk_verifier::NUM_VALIDATORS) + .map(|_| G1Affine::identity()) + .collect(); assert_eq!(commit(&[]).unwrap(), public_keys_commitment_bytes(&all_identity)); } diff --git a/tesseract/consensus/beefy/apk/Cargo.toml b/tesseract/consensus/beefy/apk/Cargo.toml index 07d345562..1e694a6b4 100644 --- a/tesseract/consensus/beefy/apk/Cargo.toml +++ b/tesseract/consensus/beefy/apk/Cargo.toml @@ -16,7 +16,6 @@ sp-consensus-beefy = { workspace = true } beefy-prover = { workspace = true, features = ["bls-aggregate"] } beefy-verifier-primitives = { workspace = true, default-features = true } -apk-commitment = { workspace = true, default-features = true } ismp-abi = { workspace = true, default-features = true } ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = false } @@ -25,15 +24,18 @@ ark-ff = { version = "0.4.0", default-features = false } ark-serialize = { version = "0.4.0", default-features = false } json = { workspace = true, default-features = true } +[dependencies.gnark-plonk-verifier] +git = "https://github.com/polytope-labs/gnark-apk-proofs" +rev = "96df97f5df2da2124be590b254d5049fe99c8908" + [dependencies.gnark-apk-prover] git = "https://github.com/polytope-labs/gnark-apk-proofs" -rev = "48e6aa504994a58fb41465e22eb90b1df6a190f8" +rev = "96df97f5df2da2124be590b254d5049fe99c8908" optional = true [dependencies.ark-serialize-05] package = "ark-serialize" version = "0.5" -optional = true [dependencies.tokio] workspace = true @@ -41,7 +43,7 @@ features = ["fs", "process", "rt"] [features] default = [] -local = ["dep:gnark-apk-prover", "dep:ark-serialize-05"] +local = ["dep:gnark-apk-prover"] [dev-dependencies] alloy-sol-types = { workspace = true, default-features = true } diff --git a/tesseract/consensus/beefy/apk/src/lib.rs b/tesseract/consensus/beefy/apk/src/lib.rs index 91b2dc69b..9460643c0 100644 --- a/tesseract/consensus/beefy/apk/src/lib.rs +++ b/tesseract/consensus/beefy/apk/src/lib.rs @@ -318,9 +318,19 @@ struct Aggregate { pub(crate) fn apk_commitment_of( keys: &[[u8; BLS_G1_SIGNATURE_LEN]], ) -> Result<[u8; 32], anyhow::Error> { - let points = keys.iter().map(decompress_g1).collect::, _>>()?; - let padded = apk_commitment::padded_to_circuit_width(&points); - Ok(apk_commitment::public_keys_commitment_bytes(&padded)) + // The commitment is defined by the circuit, so it is computed in the arkworks version the + // circuit's crate speaks rather than the one the rest of this crate uses for the prover. + let points = keys + .iter() + .map(|key| { + ::deserialize_compressed( + &key[..], + ) + .map_err(|_| anyhow!("Malformed G1 point")) + }) + .collect::, anyhow::Error>>()?; + let padded = gnark_plonk_verifier::padded_to_circuit_width(&points); + Ok(gnark_plonk_verifier::public_keys_commitment_bytes(&padded)) } pub(crate) fn decompress_g1(key: &[u8; BLS_G1_SIGNATURE_LEN]) -> Result { From c4420e48dcc17ff500b9bc59b6f77e8744849bb1 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 18 Aug 2026 10:41:09 +0100 Subject: [PATCH 40/48] check the batched pairing refuses a well formed point that is not the signature --- .../beefy/verifier/tests/apk_fixture.rs | 35 +++++++++++++++++++ .../beefy-apk-digest/src/benchmarking.rs | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/modules/consensus/beefy/verifier/tests/apk_fixture.rs b/modules/consensus/beefy/verifier/tests/apk_fixture.rs index 65abffbd4..ab9520ae3 100644 --- a/modules/consensus/beefy/verifier/tests/apk_fixture.rs +++ b/modules/consensus/beefy/verifier/tests/apk_fixture.rs @@ -94,3 +94,38 @@ fn apk_verifier_agrees_with_solidity() { trusted.latest_beefy_height, state.latest_beefy_height ); } + +/// The signature and the binding of `apk` to `apk2` are folded into one pairing with a random +/// challenge, which is cheaper than checking them separately but only sound if it really enforces +/// both. A proof that verifies proves nothing about that: an equation that quietly ignored the +/// signature would still accept every honest proof. +/// +/// The substitute is the aggregate key, which is a real point in G1 and therefore passes the +/// curve and subgroup checks that come first. Only the pairing itself can reject it, so if the +/// update is refused, the signature is genuinely part of the equation. +#[test] +fn the_batched_pairing_enforces_the_signature() { + let decode_hex = |raw: &str| hex::decode(raw.trim().trim_start_matches("0x")).expect("hex"); + let state_bytes = decode_hex(include_str!( + "../../../../../evm/tests/foundry/fixtures/bls-apk-beefy-state.hex" + )); + let proof_bytes = decode_hex(include_str!( + "../../../../../evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex" + )); + let trusted: ApkConsensusState = + ::abi_decode(&state_bytes) + .expect("state decodes") + .try_into() + .expect("state converts"); + let mut proof: ApkConsensusMessage = + ::abi_decode_params(&proof_bytes) + .expect("proof decodes") + .try_into() + .expect("proof converts"); + + proof.mmr.signature = proof.mmr.apk; + assert!( + verify_apk_consensus::(trusted, proof, VERIFYING_KEY, PARA_ID).is_err(), + "a well formed point that is not the signature was accepted", + ); +} diff --git a/modules/pallets/beefy-apk-digest/src/benchmarking.rs b/modules/pallets/beefy-apk-digest/src/benchmarking.rs index d5c318239..15be18fd6 100644 --- a/modules/pallets/beefy-apk-digest/src/benchmarking.rs +++ b/modules/pallets/beefy-apk-digest/src/benchmarking.rs @@ -23,8 +23,8 @@ #![cfg(feature = "runtime-benchmarks")] use super::*; -use apk_commitment::NUM_VALIDATORS; use frame_benchmarking::v2::*; +use gnark_plonk_verifier::NUM_VALIDATORS; use polkadot_sdk::*; /// The G1 half of a real BEEFY key from a live BLS relay, so the decompression inside From 2619500f5622591c5fe3874da07355d0694cc74a Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 18 Aug 2026 11:15:02 +0100 Subject: [PATCH 41/48] pin the gnark dependency to the merged commit --- Cargo.lock | 6 +++--- modules/consensus/beefy/verifier/Cargo.toml | 2 +- modules/pallets/beefy-apk-digest/Cargo.toml | 2 +- tesseract/consensus/beefy/apk/Cargo.toml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 940982aad..40130f703 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8665,12 +8665,12 @@ dependencies = [ [[package]] name = "gnark-apk-ffi" version = "0.1.0" -source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=96df97f5df2da2124be590b254d5049fe99c8908#96df97f5df2da2124be590b254d5049fe99c8908" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=1c15d06d8180906b16201c5206711808b80742ef#1c15d06d8180906b16201c5206711808b80742ef" [[package]] name = "gnark-apk-prover" version = "0.1.0" -source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=96df97f5df2da2124be590b254d5049fe99c8908#96df97f5df2da2124be590b254d5049fe99c8908" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=1c15d06d8180906b16201c5206711808b80742ef#1c15d06d8180906b16201c5206711808b80742ef" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", @@ -8683,7 +8683,7 @@ dependencies = [ [[package]] name = "gnark-plonk-verifier" version = "0.1.0" -source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=96df97f5df2da2124be590b254d5049fe99c8908#96df97f5df2da2124be590b254d5049fe99c8908" +source = "git+https://github.com/polytope-labs/gnark-apk-proofs?rev=1c15d06d8180906b16201c5206711808b80742ef#1c15d06d8180906b16201c5206711808b80742ef" dependencies = [ "ark-bls12-381 0.5.0", "ark-ec 0.5.0", diff --git a/modules/consensus/beefy/verifier/Cargo.toml b/modules/consensus/beefy/verifier/Cargo.toml index 10649b76e..b2fc521cf 100644 --- a/modules/consensus/beefy/verifier/Cargo.toml +++ b/modules/consensus/beefy/verifier/Cargo.toml @@ -31,7 +31,7 @@ ark-serialize = { version = "0.5", default-features = false, optional = true } [dependencies.gnark-plonk-verifier] git = "https://github.com/polytope-labs/gnark-apk-proofs" -rev = "96df97f5df2da2124be590b254d5049fe99c8908" +rev = "1c15d06d8180906b16201c5206711808b80742ef" default-features = false optional = true diff --git a/modules/pallets/beefy-apk-digest/Cargo.toml b/modules/pallets/beefy-apk-digest/Cargo.toml index 90e595acd..29abb0b56 100644 --- a/modules/pallets/beefy-apk-digest/Cargo.toml +++ b/modules/pallets/beefy-apk-digest/Cargo.toml @@ -23,7 +23,7 @@ hex-literal = { workspace = true, optional = true } [dependencies.gnark-plonk-verifier] git = "https://github.com/polytope-labs/gnark-apk-proofs" -rev = "96df97f5df2da2124be590b254d5049fe99c8908" +rev = "1c15d06d8180906b16201c5206711808b80742ef" default-features = false [dependencies.polkadot-sdk] diff --git a/tesseract/consensus/beefy/apk/Cargo.toml b/tesseract/consensus/beefy/apk/Cargo.toml index 1e694a6b4..9ffcdde55 100644 --- a/tesseract/consensus/beefy/apk/Cargo.toml +++ b/tesseract/consensus/beefy/apk/Cargo.toml @@ -26,11 +26,11 @@ json = { workspace = true, default-features = true } [dependencies.gnark-plonk-verifier] git = "https://github.com/polytope-labs/gnark-apk-proofs" -rev = "96df97f5df2da2124be590b254d5049fe99c8908" +rev = "1c15d06d8180906b16201c5206711808b80742ef" [dependencies.gnark-apk-prover] git = "https://github.com/polytope-labs/gnark-apk-proofs" -rev = "96df97f5df2da2124be590b254d5049fe99c8908" +rev = "1c15d06d8180906b16201c5206711808b80742ef" optional = true [dependencies.ark-serialize-05] From 167baa7a6c034731b4a51b27fd68f7006cf1218d Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 18 Aug 2026 12:26:18 +0100 Subject: [PATCH 42/48] hash whenever the relay's set id moves, and hand the caller the sets to assign --- modules/consensus/beefy/primitives/src/lib.rs | 37 ++++--- modules/consensus/beefy/prover/src/bls.rs | 19 ++-- modules/consensus/beefy/verifier/src/apk.rs | 9 +- modules/pallets/beefy-apk-digest/src/lib.rs | 98 +++---------------- 4 files changed, 54 insertions(+), 109 deletions(-) diff --git a/modules/consensus/beefy/primitives/src/lib.rs b/modules/consensus/beefy/primitives/src/lib.rs index 84954b0c1..95089a2bb 100644 --- a/modules/consensus/beefy/primitives/src/lib.rs +++ b/modules/consensus/beefy/primitives/src/lib.rs @@ -255,25 +255,32 @@ pub struct ApkConsensusState { } impl ApkConsensusState { - /// Take in what a verified header says about an authority set. + /// What this state becomes once a verified header names an authority set, if anything. /// - /// A header naming the set after the one this client is waiting for rolls the sets forward: - /// the relay has moved on and the current set is now the one that was next. A header naming a - /// set already held is ignored, so the same digest arriving in several headers changes - /// nothing, and a set already carrying a commitment is never overwritten. - pub fn learn_authority_set(&mut self, set_id: u64, len: u32, commitment: H256) { + /// A header naming the set after the one being waited on rolls the sets forward: the relay has + /// moved on, so the current set is the one that was next. A header naming a set already held + /// fills in its commitment if it has none, which is what a client waiting on the next set is + /// looking for. Anything else, including a set already carrying a commitment, returns `None`, + /// so the same digest arriving in several headers changes nothing. + pub fn with_authority_set( + &self, + set_id: u64, + len: u32, + commitment: H256, + ) -> Option<(ApkAuthoritySet, ApkAuthoritySet)> { + let incoming = ApkAuthoritySet { id: set_id, len, apk_commitment: commitment }; if set_id > self.next_authorities.id { - self.current_authorities = self.next_authorities.clone(); - self.next_authorities = ApkAuthoritySet { id: set_id, len, apk_commitment: commitment }; - return; + return Some((self.next_authorities.clone(), incoming)); } - for set in [&mut self.next_authorities, &mut self.current_authorities] { - if set.id == set_id && set.apk_commitment.is_zero() { - set.len = len; - set.apk_commitment = commitment; - return; - } + if set_id == self.next_authorities.id && self.next_authorities.apk_commitment.is_zero() { + return Some((self.current_authorities.clone(), incoming)); } + if set_id == self.current_authorities.id && + self.current_authorities.apk_commitment.is_zero() + { + return Some((incoming, self.next_authorities.clone())); + } + None } } diff --git a/modules/consensus/beefy/prover/src/bls.rs b/modules/consensus/beefy/prover/src/bls.rs index 6a10900f6..21a0ac6d9 100644 --- a/modules/consensus/beefy/prover/src/bls.rs +++ b/modules/consensus/beefy/prover/src/bls.rs @@ -144,16 +144,23 @@ pub async fn beefy_g1_authorities( pub fn aggregate_signatures( signatures: &[[u8; BLS_G1_SIGNATURE_LEN]], ) -> Result<[u8; BLS_G1_SIGNATURE_LEN], anyhow::Error> { + use ark_ff::Zero; use w3f_bls::{EngineBLS, SerializableToBytes, Signature, TinyBLS381}; - let mut aggregate: Option<::SignatureGroup> = None; - for signature in signatures { - let signature = Signature::::from_bytes(signature) - .map_err(|_| anyhow!("Invalid G1 signature encoding"))?; - aggregate = Some(aggregate.map_or(signature.0, |sum| sum + signature.0)); + // Aggregating nothing would give the identity, which is a well formed point and a meaningless + // signature, so an empty set is refused rather than summed. + if signatures.is_empty() { + Err(anyhow!("No signatures to aggregate"))? } - let aggregate = aggregate.ok_or_else(|| anyhow!("No signatures to aggregate"))?; + let aggregate = signatures.iter().try_fold( + ::SignatureGroup::zero(), + |sum, signature| { + let signature = Signature::::from_bytes(signature) + .map_err(|_| anyhow!("Invalid G1 signature encoding"))?; + Ok::<_, anyhow::Error>(sum + signature.0) + }, + )?; Signature::(aggregate) .to_bytes() .try_into() diff --git a/modules/consensus/beefy/verifier/src/apk.rs b/modules/consensus/beefy/verifier/src/apk.rs index 3ef515cd9..11101cd41 100644 --- a/modules/consensus/beefy/verifier/src/apk.rs +++ b/modules/consensus/beefy/verifier/src/apk.rs @@ -86,12 +86,15 @@ pub fn verify_apk_consensus( // finalized, and every one of them is proven against the heads root, so a digest from another // parachain is authentic yet says nothing about this relay's authorities. Left unfiltered any // parachain could name the keys this client trusts next. - if let Some(digest) = headers + if let Some((current, next)) = headers .iter() .filter(|header| header.para_id == digest_para_id) .find_map(|header| read_apk_digest(&header.header)) - { - state.learn_authority_set(digest.set_id, digest.len, H256(digest.commitment)); + .and_then(|digest| { + state.with_authority_set(digest.set_id, digest.len, H256(digest.commitment)) + }) { + state.current_authorities = current; + state.next_authorities = next; } Ok((state, headers)) diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index a726c2ff9..1e64c7e7a 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -90,13 +90,13 @@ pub mod pallet { /// The last commitment published to a header digest, and the set it describes. #[pallet::storage] - pub type Published = StorageValue<_, (u64, u32, [u8; 32], [u8; 32]), OptionQuery>; + pub type Published = StorageValue<_, (u64, u32, [u8; 32]), OptionQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Committed to a new authority set, and wrote the commitment to this block's header. - CommitmentPublished { set_id: u64, set_digest: [u8; 32], commitment: [u8; 32] }, + CommitmentPublished { set_id: u64, len: u32, commitment: [u8; 32] }, } #[pallet::hooks] @@ -135,7 +135,7 @@ pub mod pallet { let set_id = Self::relay_beefy_set_id()?; // Nothing has moved, so the commitment already computed still describes this set. - if let Some((published, len, _, commitment)) = Published::::get() { + if let Some((published, len, commitment)) = Published::::get() { if published == set_id { Self::deposit_digest(set_id, len, commitment); return Ok(false); @@ -149,36 +149,19 @@ pub mod pallet { // The old commitment is still the truth about the old set, and a client files // commitments by set id, so republishing it under the id it was computed for // keeps this block's header useful rather than empty. - if let Some((published, len, _, commitment)) = Published::::get() { + if let Some((published, len, commitment)) = Published::::get() { Self::deposit_digest(published, len, commitment); } return Ok(false); }; KeysWanted::::kill(); - let set_digest = sp_io::hashing::blake2_256(&keys.encode()); + let commitment = commit(&keys).map_err(|_| Error::::MalformedAuthorityKey)?; let len = keys.len() as u32; - match next_step(Published::::get().as_ref(), set_digest) { - // The membership has not changed, only the id, which is common. Rehashing would - // cost the better part of a second for a commitment already held. - Step::Republish(commitment) => { - Self::deposit_digest(set_id, len, commitment); - Published::::put((set_id, len, set_digest, commitment)); - Ok(false) - }, - Step::Commit => { - let commitment = - commit(&keys).map_err(|_| Error::::MalformedAuthorityKey)?; - Self::deposit_digest(set_id, len, commitment); - Published::::put((set_id, len, set_digest, commitment)); - Self::deposit_event(Event::CommitmentPublished { - set_id, - set_digest, - commitment, - }); - Ok(true) - }, - } + Self::deposit_digest(set_id, len, commitment); + Published::::put((set_id, len, commitment)); + Self::deposit_event(Event::CommitmentPublished { set_id, len, commitment }); + Ok(true) } /// Put the commitment in this block's header. @@ -275,11 +258,9 @@ pub fn wants_keys() -> bool { /// Throw away a commitment computed under an older scheme. /// -/// A commitment is only recomputed when the membership changes, so a runtime upgrade that changes -/// how keys are hashed would otherwise keep republishing the old value indefinitely, on any chain -/// whose validators happen to stay the same. The stored digest cannot notice: it describes the -/// keys, not the arithmetic applied to them. Clearing the record forces one recomputation and the -/// pallet carries on from there. +/// The stored record is republished unchanged until the relay's set id moves, so a runtime upgrade +/// that changes how keys are hashed would otherwise keep publishing the old value for the rest of +/// the session. Clearing it means the next block recomputes instead of waiting for the rotation. pub mod migration { use super::*; use frame_support::traits::{Get, GetStorageVersion, OnRuntimeUpgrade}; @@ -300,28 +281,6 @@ pub mod migration { } } -/// What to do with the commitment this block. -#[derive(Debug, PartialEq, Eq)] -pub enum Step { - /// The keys are unchanged, so republish the commitment already computed for them. - Republish([u8; 32]), - /// Nothing published, or a different set of keys: hash them. - Commit, -} - -/// Decide how to proceed, given what was published last. -/// -/// Kept pure so it can be tested without a mock chain. The case worth being careful about is a -/// session rotating without the membership changing, which is common: the commitment is the same, -/// so rehashing it would spend the better part of a second for nothing, but it still has to be -/// republished under the new set id or a client tracking commitments per set never learns it. -pub fn next_step(published: Option<&(u64, u32, [u8; 32], [u8; 32])>, set_digest: [u8; 32]) -> Step { - match published { - Some((_, _, digest, commitment)) if *digest == set_digest => Step::Republish(*commitment), - _ => Step::Commit, - } -} - /// A key that did not decode as a G1 curve point. #[derive(Debug, PartialEq, Eq)] pub struct MalformedKey; @@ -424,38 +383,7 @@ mod tests { assert!(commit(&[key]).is_ok()); } - const SET_A: [u8; 32] = [0xaa; 32]; - const SET_B: [u8; 32] = [0xbb; 32]; - - #[test] - fn a_fresh_chain_commits() { - assert_eq!(next_step(None, SET_A), Step::Commit); - } - - #[test] - fn keys_already_committed_to_are_republished_rather_than_rehashed() { - let published = (1u64, 2u32, SET_A, [7u8; 32]); - assert_eq!(next_step(Some(&published), SET_A), Step::Republish([7u8; 32])); - } - - /// A session can rotate without the membership changing, which is common on small networks and - /// possible anywhere. Rehashing would cost the better part of a second for a commitment we - /// already hold, so the stored one is republished under the new set id instead. - #[test] - fn the_same_keys_under_a_new_set_id_are_not_rehashed() { - let published = (1u64, 2u32, SET_A, [7u8; 32]); - assert_eq!(next_step(Some(&published), SET_A), Step::Republish([7u8; 32])); - } - - /// Different keys mean the stored commitment says nothing about them. - #[test] - fn a_new_set_is_committed_to_even_though_another_was_published() { - let published = (1u64, 2u32, SET_A, [7u8; 32]); - assert_eq!(next_step(Some(&published), SET_B), Step::Commit); - } - - /// Two different sets must not reach the same commitment, which is the property the set digest - /// is standing in for when deciding whether to rehash. + /// Two different sets must not reach the same commitment. #[test] fn a_different_set_commits_differently() { let first = relay_keys(); From d6bfa416a67f5a3e89df21e966bca80a2ddb0d49 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Tue, 18 Aug 2026 14:43:12 +0100 Subject: [PATCH 43/48] keep one authority set holding both roots so either proof format can advance it --- evm/rust/abi/BlsBeefy.json | 470 +----------------- evm/rust/abi/EcdsaBeefy.json | 2 +- evm/rust/src/conversions.rs | 142 +++--- evm/script/DeployConsensusRouter.s.sol | 4 +- evm/script/DeployHostUpdates.s.sol | 8 +- evm/script/DeployIsmp.s.sol | 5 +- evm/src/consensus/BlsBeefy.sol | 25 +- evm/src/consensus/Codec.sol | 37 ++ evm/src/consensus/EcdsaBeefy.sol | 37 +- evm/src/consensus/SP1Beefy.sol | 22 +- evm/src/consensus/Types.sol | 25 +- evm/tests/foundry/Beefy.sol | 3 +- evm/tests/foundry/SP1BeefyForkTest.sol | 2 +- evm/tests/foundry/SP1BeefyTest.sol | 10 +- .../foundry/fixtures/bls-apk-beefy-state.hex | 2 +- modules/consensus/beefy/primitives/src/lib.rs | 68 ++- modules/consensus/beefy/prover/src/lib.rs | 30 +- modules/consensus/beefy/verifier/src/apk.rs | 31 +- modules/consensus/beefy/verifier/src/ecdsa.rs | 15 +- modules/consensus/beefy/verifier/src/sp1.rs | 14 +- .../beefy/verifier/tests/apk_fixture.rs | 6 +- modules/ismp/clients/beefy/src/consensus.rs | 5 +- .../src/benchmarking.rs | 2 +- .../pallets/beefy-consensus-proofs/src/lib.rs | 4 +- .../beefy-consensus-proofs/src/types.rs | 4 +- 25 files changed, 320 insertions(+), 653 deletions(-) diff --git a/evm/rust/abi/BlsBeefy.json b/evm/rust/abi/BlsBeefy.json index d8e2c0d12..ffebd9143 100644 --- a/evm/rust/abi/BlsBeefy.json +++ b/evm/rust/abi/BlsBeefy.json @@ -1,469 +1 @@ -[ - { - "type": "constructor", - "inputs": [ - { - "name": "apkProof", - "type": "address", - "internalType": "address" - }, - { - "name": "digestParaId", - "type": "uint32", - "internalType": "uint32" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "MMR_ROOT_PAYLOAD_ID", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes2", - "internalType": "bytes2" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "_apk", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract IApkProof" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "_digestParaId", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint32", - "internalType": "uint32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "noOp", - "inputs": [ - { - "name": "s", - "type": "tuple", - "internalType": "struct BeefyConsensusState", - "components": [ - { - "name": "latestHeight", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "beefyActivationBlock", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "currentAuthoritySet", - "type": "tuple", - "internalType": "struct AuthoritySetCommitment", - "components": [ - { - "name": "id", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "len", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "root", - "type": "bytes32", - "internalType": "bytes32" - } - ] - }, - { - "name": "nextAuthoritySet", - "type": "tuple", - "internalType": "struct AuthoritySetCommitment", - "components": [ - { - "name": "id", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "len", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "root", - "type": "bytes32", - "internalType": "bytes32" - } - ] - } - ] - }, - { - "name": "p", - "type": "tuple", - "internalType": "struct BlsApkBeefyConsensusProof", - "components": [ - { - "name": "relay", - "type": "tuple", - "internalType": "struct BlsApkRelayChainProof", - "components": [ - { - "name": "commitment", - "type": "tuple", - "internalType": "struct Commitment", - "components": [ - { - "name": "payload", - "type": "tuple[]", - "internalType": "struct Payload[]", - "components": [ - { - "name": "id", - "type": "bytes2", - "internalType": "bytes2" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - } - ] - }, - { - "name": "blockNumber", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "validatorSetId", - "type": "uint64", - "internalType": "uint64" - } - ] - }, - { - "name": "bitlist", - "type": "uint256[5]", - "internalType": "uint256[5]" - }, - { - "name": "apk", - "type": "bytes32[3]", - "internalType": "bytes32[3]" - }, - { - "name": "apk2", - "type": "bytes32[6]", - "internalType": "bytes32[6]" - }, - { - "name": "apkProof", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "signature", - "type": "bytes32[3]", - "internalType": "bytes32[3]" - }, - { - "name": "latestMmrLeaf", - "type": "tuple", - "internalType": "struct BeefyMmrLeaf", - "components": [ - { - "name": "version", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "parentNumber", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "parentHash", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "nextAuthoritySet", - "type": "tuple", - "internalType": "struct AuthoritySetCommitment", - "components": [ - { - "name": "id", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "len", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "root", - "type": "bytes32", - "internalType": "bytes32" - } - ] - }, - { - "name": "extra", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "leafIndex", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "mmrProof", - "type": "bytes32[]", - "internalType": "bytes32[]" - } - ] - }, - { - "name": "parachain", - "type": "tuple", - "internalType": "struct ParachainProof", - "components": [ - { - "name": "parachains", - "type": "tuple[]", - "internalType": "struct Parachain[]", - "components": [ - { - "name": "index", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "header", - "type": "bytes", - "internalType": "bytes" - } - ] - }, - { - "name": "proof", - "type": "bytes32[]", - "internalType": "bytes32[]" - }, - { - "name": "leafCount", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ] - } - ], - "outputs": [], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "supportsInterface", - "inputs": [ - { - "name": "interfaceId", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "verify", - "inputs": [ - { - "name": "previousState", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "proof", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [ - { - "name": "", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "", - "type": "tuple[]", - "internalType": "struct IntermediateState[]", - "components": [ - { - "name": "stateMachineId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "height", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "commitment", - "type": "tuple", - "internalType": "struct StateCommitment", - "components": [ - { - "name": "timestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "overlayRoot", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "stateRoot", - "type": "bytes32", - "internalType": "bytes32" - } - ] - } - ] - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "error", - "name": "EmptyLeaves", - "inputs": [] - }, - { - "type": "error", - "name": "EmptyTree", - "inputs": [] - }, - { - "type": "error", - "name": "EmptyTree", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidAggregateProof", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidMmrProof", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidParachainHeaderProof", - "inputs": [] - }, - { - "type": "error", - "name": "LeafIndexOutOfBounds", - "inputs": [] - }, - { - "type": "error", - "name": "MissingApkCommitment", - "inputs": [] - }, - { - "type": "error", - "name": "MmrRootHashMissing", - "inputs": [] - }, - { - "type": "error", - "name": "OutOfBoundsLeaves", - "inputs": [] - }, - { - "type": "error", - "name": "ProofExhausted", - "inputs": [] - }, - { - "type": "error", - "name": "SuperMajorityRequired", - "inputs": [] - }, - { - "type": "error", - "name": "TimestampNotFound", - "inputs": [] - }, - { - "type": "error", - "name": "UnconsumedProof", - "inputs": [] - }, - { - "type": "error", - "name": "UnknownAuthoritySet", - "inputs": [] - }, - { - "type": "error", - "name": "UnsortedLeaves", - "inputs": [] - }, - { - "type": "error", - "name": "UnsortedLeaves", - "inputs": [] - } -] \ No newline at end of file +{"abi":[{"type":"constructor","inputs":[{"name":"apkProof","type":"address","internalType":"address"},{"name":"digestParaId","type":"uint32","internalType":"uint32"}],"stateMutability":"nonpayable"},{"type":"function","name":"MMR_ROOT_PAYLOAD_ID","inputs":[],"outputs":[{"name":"","type":"bytes2","internalType":"bytes2"}],"stateMutability":"view"},{"type":"function","name":"_apk","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IApkProof"}],"stateMutability":"view"},{"type":"function","name":"_digestParaId","inputs":[],"outputs":[{"name":"","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"noOp","inputs":[{"name":"s","type":"tuple","internalType":"struct BeefyConsensusState","components":[{"name":"latestHeight","type":"uint256","internalType":"uint256"},{"name":"beefyActivationBlock","type":"uint256","internalType":"uint256"},{"name":"currentAuthoritySet","type":"tuple","internalType":"struct AuthoritySet","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"len","type":"uint256","internalType":"uint256"},{"name":"blsPoseidonHash","type":"uint256","internalType":"uint256"},{"name":"ecdsaMerkleRoot","type":"bytes32","internalType":"bytes32"}]},{"name":"nextAuthoritySet","type":"tuple","internalType":"struct AuthoritySet","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"len","type":"uint256","internalType":"uint256"},{"name":"blsPoseidonHash","type":"uint256","internalType":"uint256"},{"name":"ecdsaMerkleRoot","type":"bytes32","internalType":"bytes32"}]}]},{"name":"p","type":"tuple","internalType":"struct BlsApkBeefyConsensusProof","components":[{"name":"relay","type":"tuple","internalType":"struct BlsApkRelayChainProof","components":[{"name":"commitment","type":"tuple","internalType":"struct Commitment","components":[{"name":"payload","type":"tuple[]","internalType":"struct Payload[]","components":[{"name":"id","type":"bytes2","internalType":"bytes2"},{"name":"data","type":"bytes","internalType":"bytes"}]},{"name":"blockNumber","type":"uint32","internalType":"uint32"},{"name":"validatorSetId","type":"uint64","internalType":"uint64"}]},{"name":"bitlist","type":"uint256[5]","internalType":"uint256[5]"},{"name":"apk","type":"bytes32[3]","internalType":"bytes32[3]"},{"name":"apk2","type":"bytes32[6]","internalType":"bytes32[6]"},{"name":"apkProof","type":"bytes","internalType":"bytes"},{"name":"signature","type":"bytes32[3]","internalType":"bytes32[3]"},{"name":"latestMmrLeaf","type":"tuple","internalType":"struct BeefyMmrLeaf","components":[{"name":"version","type":"uint8","internalType":"uint8"},{"name":"parentNumber","type":"uint32","internalType":"uint32"},{"name":"parentHash","type":"bytes32","internalType":"bytes32"},{"name":"nextAuthoritySet","type":"tuple","internalType":"struct AuthoritySetCommitment","components":[{"name":"id","type":"uint64","internalType":"uint64"},{"name":"len","type":"uint32","internalType":"uint32"},{"name":"root","type":"bytes32","internalType":"bytes32"}]},{"name":"extra","type":"bytes32","internalType":"bytes32"},{"name":"leafIndex","type":"uint256","internalType":"uint256"}]},{"name":"mmrProof","type":"bytes32[]","internalType":"bytes32[]"}]},{"name":"parachain","type":"tuple","internalType":"struct ParachainProof","components":[{"name":"parachains","type":"tuple[]","internalType":"struct Parachain[]","components":[{"name":"index","type":"uint256","internalType":"uint256"},{"name":"id","type":"uint256","internalType":"uint256"},{"name":"header","type":"bytes","internalType":"bytes"}]},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"leafCount","type":"uint256","internalType":"uint256"}]}]}],"outputs":[],"stateMutability":"pure"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"verify","inputs":[{"name":"previousState","type":"bytes","internalType":"bytes"},{"name":"proof","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"},{"name":"","type":"tuple[]","internalType":"struct IntermediateState[]","components":[{"name":"stateMachineId","type":"uint256","internalType":"uint256"},{"name":"height","type":"uint256","internalType":"uint256"},{"name":"commitment","type":"tuple","internalType":"struct StateCommitment","components":[{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"overlayRoot","type":"bytes32","internalType":"bytes32"},{"name":"stateRoot","type":"bytes32","internalType":"bytes32"}]}]},{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"error","name":"EmptyLeaves","inputs":[]},{"type":"error","name":"EmptyTree","inputs":[]},{"type":"error","name":"EmptyTree","inputs":[]},{"type":"error","name":"InvalidAggregateProof","inputs":[]},{"type":"error","name":"InvalidMmrProof","inputs":[]},{"type":"error","name":"InvalidParachainHeaderProof","inputs":[]},{"type":"error","name":"LeafIndexOutOfBounds","inputs":[]},{"type":"error","name":"MissingApkCommitment","inputs":[]},{"type":"error","name":"MmrRootHashMissing","inputs":[]},{"type":"error","name":"OutOfBoundsLeaves","inputs":[]},{"type":"error","name":"ProofExhausted","inputs":[]},{"type":"error","name":"SuperMajorityRequired","inputs":[]},{"type":"error","name":"TimestampNotFound","inputs":[]},{"type":"error","name":"UnconsumedProof","inputs":[]},{"type":"error","name":"UnknownAuthoritySet","inputs":[]},{"type":"error","name":"UnsortedLeaves","inputs":[]},{"type":"error","name":"UnsortedLeaves","inputs":[]}],"bytecode":{"object":"0x60c03461009957601f61324338819003918201601f19168301916001600160401b0383118484101761009d5780849260409485528339810103126100995780516001600160a01b038116919082900361009957602001519063ffffffff821682036100995760805260a05260405161319190816100b282396080518181816108a601526115ab015260a0518181816108f10152610c870152f35b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60806040526004361015610011575f80fd5b5f3560e01c806301ffc9a71461007457806342a947b11461006f578063af8b91d61461006a578063afb5670a14610065578063e455995b146100605763f7e83aee1461005b575f80fd5b610966565b6108d5565b610891565b610871565b6107cf565b346100c85760203660031901126100c85760043563ffffffff60e01b81168091036100c857637bf41d7760e11b81149081156100b7575b50151560805260206080f35b6301ffc9a760e01b149050816100ab565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b608081019081106001600160401b038211176100fb57604052565b6100cc565b606081019081106001600160401b038211176100fb57604052565b604081019081106001600160401b038211176100fb57604052565b60c081019081106001600160401b038211176100fb57604052565b90601f801991011681019081106001600160401b038211176100fb57604052565b6040519061018261010083610151565b565b60405190610182608083610151565b60405190610182604083610151565b60405190610182606083610151565b6040519061018260a083610151565b91908260809103126100c8576040516101d8816100e0565b60608082948035845260208101356020850152604081013560408501520135910152565b906101406003198301126100c857604051610216816100e0565b60606102418294600435845260243560208501526102358160446101c0565b604085015260c46101c0565b910152565b6001600160401b0381116100fb5760051b60200190565b6001600160401b0381116100fb57601f01601f191660200190565b81601f820112156100c85780359061028f8261025d565b9261029d6040519485610151565b828452602083830101116100c857815f926020809301838601378301015290565b359063ffffffff821682036100c857565b35906001600160401b03821682036100c857565b9190916060818403126100c857604051906102fd82610100565b819381356001600160401b0381116100c85782019080601f830112156100c85781359161032983610246565b926103376040519485610151565b80845260208085019160051b830101918383116100c85760208101915b83831061038157505050505060408092610241928552610376602082016102be565b6020860152016102cf565b82356001600160401b0381116100c8578201906040828703601f1901126100c857604051906103af8261011b565b60208301356001600160f01b0319811681036100c85782526040830135916001600160401b0383116100c8576103ed88602080969581960101610278565b83820152815201920191610354565b9080601f830112156100c8576040519161041760a084610151565b829060a081019283116100c857905b8282106104335750505090565b8135815260209182019101610426565b9080601f830112156100c8576040519161045e606084610151565b8290606081019283116100c857905b82821061047a5750505090565b813581526020918201910161046d565b9080601f830112156100c857604051916104a560c084610151565b829060c081019283116100c857905b8282106104c15750505090565b81358152602091820191016104b4565b91908260609103126100c8576040516104e981610100565b60408082946104f7816102cf565b8452610505602082016102be565b60208501520135910152565b919091610100818403126100c8576040519061052c82610136565b819381359160ff831683036100c85761056a60e09260a0948652610552602084016102be565b602087015260408301356040870152606083016104d1565b606085015260c081013560808501520135910152565b9080601f830112156100c857813561059781610246565b926105a56040519485610151565b81845260208085019260051b8201019283116100c857602001905b8282106105cd5750505090565b81358152602091820191016105c0565b919091610380818403126100c8576105f3610172565b9281356001600160401b0381116100c857816106109184016102e3565b845261061f81602084016103fc565b60208501526106318160c08401610443565b604085015261064481610120840161048a565b60608501526101e08201356001600160401b0381116100c85781610669918401610278565b608085015261067c816102008401610443565b60a085015261068f816102608401610511565b60c08501526103608201356001600160401b0381116100c8576106b29201610580565b60e0830152565b9190916060818403126100c857604051906106d382610100565b819381356001600160401b0381116100c857820181601f820112156100c8578035906106fe82610246565b9161070c6040519384610151565b80835260208084019160051b830101918483116100c85760208101915b83831061075a575050505083526020820135916001600160401b0383116100c8576105056040939284938301610580565b82356001600160401b0381116100c8578201906060828803601f1901126100c8576040519061078882610100565b60208301358252604083013560208301526060830135916001600160401b0383116100c8576107bf89602080969581960101610278565b6040820152815201920191610729565b346100c8576101603660031901126100c8576107ea366101fc565b50610144356001600160401b0381116100c857604060031982360301126100c857604051906108188261011b565b80600401356001600160401b0381116100c85761083b90600436918401016105dd565b825260248101356001600160401b0381116100c857602091600461086292369201016106b9565b910152005b5f9103126100c857565b346100c8575f3660031901126100c857604051610dad60f31b8152602090f35b346100c8575f3660031901126100c8576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b346100c8575f3660031901126100c857602060405163ffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b9181601f840112156100c8578235916001600160401b0383116100c857602083818601950101116100c857565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b346100c85760403660031901126100c8576004356001600160401b0381116100c857610996903690600401610915565b602435916001600160401b0383116100c8576109b96109c1933690600401610915565b929091610c29565b91906109d860405192606084526060840190610942565b9282840360208401526020808351958681520192015f945b808610610a0557505082935060408301520390f35b9092602060a06001926040808851805184528581015186850152015180518284015284810151606084015201516080820152019401950194906109f0565b90610140828203126100c857610a889060c060405193610a62856100e0565b8035855260208101356020860152610a7d83604083016101c0565b6040860152016101c0565b606082015290565b9190916040818403126100c85780356001600160401b0381116100c85783610ab99183016105dd565b9260208201356001600160401b0381116100c857610ad792016106b9565b90565b6101829092919260c060606101408301958051845260208101516020850152610b2960408201516040860190606080918051845260208101516020850152604081015160408501520151910152565b0151910190606080918051845260208101516020850152604081015160408501520151910152565b60405190610b5e82610100565b5f6040838281528260208201520152565b60405190610b7e602083610151565b5f80835282815b828110610b9157505050565b602090604051610ba081610100565b5f81525f83820152610bb0610b51565b604082015282828501015201610b85565b90610bcb82610246565b610bd86040519182610151565b8281528092610be9601f1991610246565b01905f5b828110610bf957505050565b602090604051610c0881610100565b5f81525f83820152610c18610b51565b604082015282828501015201610bed565b610c3d90610c469392959495810190610a43565b93810190610a90565b8392919251610c69610c6060208651015163ffffffff1690565b63ffffffff1690565b1115610dd857610d77610cac610c8385610d699697610e9d565b90937f0000000000000000000000000000000000000000000000000000000000000000916110b7565b929095610cc084516001600160401b031690565b9360608301948551906001600160401b03825191168181115f14610d8c575050855160408501525080516001600160401b031691606060c06040610d0b602086015163ffffffff1690565b940151920151015180516001600160401b03858116911603610d7f57604063ffffffff910151925b6001600160401b03610d43610184565b951685521660208401526040830152606082015283525b60405194859160208301610ada565b03601f198101855284610151565b515191929190565b5063ffffffff5f92610d33565b9193501480610dcc575b610da2575b5050610d5a565b6040916020610dba610c608285015163ffffffff1690565b91015201516040845101525f80610d9b565b50604082015115610d96565b506040519150610dfd82610def8560208301610ada565b03601f198101845283610151565b6060610e07610b6f565b9301515191929190565b60405190610e1e826100e0565b5f6060838281528260208201528260408201520152565b60405190610e42826100e0565b815f81525f6020820152610e54610e11565b60408201526060610241610e11565b634e487b7160e01b5f52603260045260245ffd5b805115610e845760200190565b610e63565b8051821015610e845760209160051b010190565b9190610ea7610e35565b5080516040808201805191860180515190926001600160401b0316908114159081611004575b50610ff557519051805190916001600160401b031603610fea575b604081015115610fdb57610f059083610f00846112f6565b611577565b805151915f925f5b818110610f575750508215610f4857610f3f610c60602060c094610f34608097868b6117b4565b015163ffffffff1690565b85520151015190565b6323188e3960e21b5f5260045ffd5b610dad60f31b610f8a610f7d610f6e848851610e89565b51516001600160f01b03191690565b6001600160f01b03191690565b1480610fc2575b610f9e575b600101610f0d565b93506001610fba6020610fb2878751610e89565b5101516116c3565b949050610f96565b50602080610fd1838751610e89565b5101515114610f91565b6334f6ba9f60e01b5f5260045ffd5b506060840151610ee8565b637202e68560e11b5f5260045ffd5b905060608701515114155f610ecd565b604051906110218261011b565b5f6020838281520152565b9061103682610246565b6110436040519182610151565b8281528092611054601f1991610246565b01905f5b82811061106457505050565b60209061106f611014565b82828501015201611058565b805191908290602001825e015f815290565b6040516001600160e01b031990911660208201529190610182908390610def90602483019061107b565b929190926110c3610b51565b93805151916110d18361102c565b926110db81610bc1565b945f5b82811061112a5750506110f3575b5050509190565b61110f92826040602061110b95015191015192611dbd565b1590565b61111b575f80806110ec565b6380b6d5fd60e01b5f5260045ffd5b611135818651610e89565b516040810161114481516119ba565b916111ab602082519201926111a561119e611163865163ffffffff1690565b6001600160e01b03199063ff00ff00600882811b9190911691901c62ff00ff1617601081811b63ffff00001691901c61ffff161760e01b1690565b9151611b06565b9061108d565b602081519101206111ba610193565b91825260208201526111cc848a610e89565b526111d78389610e89565b50805160208301516111e884611b2b565b906111f16101a2565b92835260208301526040820152611208848b610e89565b52611213838a610e89565b506112346112288c516001600160401b031690565b6001600160401b031690565b159081611260575b5061124b575b506001016110de565b600191995061125990611c98565b9890611242565b90505163ffffffff8416145f61123c565b60405190611280602083610151565b5f8252565b610def6112a494936112a461018294604051978895602087019061107b565b9061107b565b610182926112a495946112c9600c94604051988995602087019061107b565b6001600160e01b03199290921682526001600160c01b031916600482015203601319810185520183610151565b90815151611302611271565b905f5b8181106113d757509261131b610ad79394611ee8565b916113d16113476040611338611163602087015163ffffffff1690565b9401516001600160401b031690565b67ffffffffffff000067ff00ff00ff00ff0066ff00ff00ff00ff8360081c169260081b169165ffff0000ffff65ffff0000ff0065ffffffffffff67ffff0000ffff0000861666ff0000ffff000085161760101c16941691161760101b161767ffffffff0000000063ffffffff8260201c169160201b166001600160401b0360c01b911760c01b1690565b926112aa565b9161143c60019161141061141e6113f2610f6e888b51610e89565b6040516001600160f01b031990911660208201529182906022820190565b03601f198101835282610151565b611436602061142e888b51610e89565b510151611b06565b91611285565b9201611305565b906060828203126100c85780601f830112156100c85760405191611468606084610151565b8290606081019283116100c857905b8282106114845750505090565b8151815260209182019101611477565b906020610ad7928181520190610942565b6040513d5f823e3d90fd5b905f905b600382106114c157505050565b60208060019285518152019301910190916114b4565b908152939695949291905f602086015b60058210611561575050509161152f61152361153a9361150f879660c06102009901906114b0565b6102c06101208701526102c0860190610942565b976101408501906114b0565b6101a08301906114b0565b015f905b6006821061154b57505050565b602080600192855181520193019101909161153e565b60208060019285518152019301910190916114e7565b602082019061159761110b61158c8451612065565b6020870151906120cc565b6116b4576040516378b8c33160e11b8152937f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031691606090869081906115e89060048301611494565b0381855afa9485156116af575f9561167b575b50604001519151604084015192608085015195606060a0870151960151843b156100c8575f9661164292604051998a988997889763aa8f580360e01b8952600489016114d7565b03915afa9081611661575b50610182576303c6c1a560e21b5f5260045ffd5b8061166f5f61167593610151565b80610867565b5f61164d565b60409195506116a19060603d6060116116a8575b6116998183610151565b810190611443565b94906115fb565b503d61168f565b6114a5565b633aa90f7f60e21b5f5260045ffd5b60208151106116d3576020015190565b60405162461bcd60e51b8152602060048201526024808201527f42797465733a3a20746f427974657333323a206461746120697320746f20736860448201526337b93a1760e11b6064820152608490fd5b634e487b7160e01b5f52601160045260245ffd5b906001820180921161174657565b611724565b906003820180921161174657565b906002820180921161174657565b9190820180921161174657565b604080519091906117858382610151565b6001815291601f1901825f5b82811061179d57505050565b6020906117a8611014565b82828501015201611791565b61110b916118999360e060c083019261185d611858602061183487516117db815160ff1690565b906117ec8482015163ffffffff1690565b906040810151611820608060608401519301519361181461180b6101b1565b60ff9097168752565b63ffffffff1685880152565b604084015260608301526080820152612186565b8051908201209701518651602001516118529063ffffffff16610c60565b9061227a565b611738565b9460a0611868611774565b9551015190611875610193565b918252602082015261188685610e77565b5261189084610e77565b5001519061228f565b61189f57565b630b92186960e31b5f5260045ffd5b6040519060a082018281106001600160401b038211176100fb5760405260606080835f81525f60208201525f60408201525f838201520152565b604051906118f58261011b565b60606020835f81520152565b6040519061012082018281106001600160401b038211176100fb576040525f6101008382815261192f6118e8565b60208201528260408201526119426118e8565b60608201528260808201526119556118e8565b60a08201528260c0820152606060e08201520152565b9061197582610246565b6119826040519182610151565b8281528092611993601f1991610246565b01905f5b8281106119a357505050565b6020906119ae611901565b82828501015201611997565b6119c26118ae565b506119cb610193565b9081525f60208201526119e56119e08261229b565b6116c3565b906119ef816123da565b6119fb6119e08361229b565b611a076119e08461229b565b91611a11846123da565b611a1a8161196b565b945f5b828110611a4957505050611a2f6101b1565b948552602085015260408401526060830152608082015290565b600190611a5583612548565b60ff611a5f611901565b911680611a8b5750600160c08201525b611a79828a610e89565b52611a848189610e89565b5001611a1d565b60048103611aaf575060016040820152611aa4846125c2565b60608201525b611a6f565b60058103611ad2575060016080820152611ac8846125c2565b60a0820152611a6f565b60068103611af2575060018152611ae8846125c2565b6020820152611a6f565b600803611aaa576001610100820152611a6f565b610ad76112a491611410611b1a8251611ee8565b91604051948593602085019061107b565b611b33610b51565b505f5f925f5f5b6080850180518051831015611c64576040611b5884611b6093610e89565b510151151590565b80611c3c575b611bf3575b611b7b6040611b58848451610e89565b80611bba575b611b8f575b50600101611b3a565b819250611bb260206060611ba860809560019551610e89565b510151015161277e565b929150611b86565b50634953544d60e01b63ffffffff60e01b611bec6060611bdb868651610e89565b510151516001600160e01b03191690565b1614611b81565b95509250611c166119e060206060611c0c878a51610e89565b510151015161269e565b92611c366119e060206060611c2c858b51610e89565b5101510151612718565b95611b6b565b5063049534d560e41b63ffffffff60e01b611c5d6060611bdb868651610e89565b1614611b66565b505050925092908215611c8957611c796101a2565b9283526020830152604082015290565b633eba99d160e21b5f5260045ffd5b611ca0610b51565b5f915b6080810180518051851015611db55761110b6040611b5887611cc494610e89565b611da9576341504b4360e01b611cf0611ce36060611bdb888651610e89565b6001600160e01b03191690565b03611da9576060611d048560209351610e89565b510151015191602c835103611d9c57611d27611228611d22856126b4565b61277e565b936001600160401b03851615611d8857505050611d80611d586119e0611d52610c60611d22866126cb565b93612745565b91611d73611d646101a2565b6001600160401b039095168552565b63ffffffff166020840152565b604082015290565b608092945060019193505b01929050611ca3565b9150916001608091611d93565b50916001608091611d93565b505050919050565b919392908115611e86578451611dd2816127f6565b611ddb826127f6565b916001611de786612c6c565b1b5f805b838210611e065750505050611e0294959650612c9c565b1490565b611e10828c610e89565b51519088821015611e77578215159081611e6c575b50611e5d576001906020611e39848e610e89565b510151611e468489610e89565b52808401611e548488610e89565b52910190611deb565b630647f54960e21b5f5260045ffd5b90508111155f611e25565b630834466160e31b5f5260045ffd5b639136328760e01b5f5260045ffd5b60031981019190821161174657565b5f1981019190821161174657565b602003906020821161174657565b9190820391821161174657565b600190610ad7939260ff60f81b9060f81b168152019061107b565b6040811015611f2c57610ad7611f0a611f046114109360021b90565b60ff1690565b60405160f89190911b6001600160f81b03191660208201529182906021820190565b614000811015611f8b57610ad7611f69611f56611f4f6118586114109560021b90565b61ffff1690565b60ff61ff008260081b169160081c161790565b60405160f09190911b6001600160f01b03191660208201529182906022820190565b634000000081101561200857610ad7611fe6611fb5610c60611fb06114109560021b90565b611759565b63ffffff0062ff00ff62ffffff818460081c1616921660081b161763ffff000061ffff8260101c169160101b161790565b60405160e09190911b6001600160e01b03191660208201529182906024820190565b61141061202d61201a61203293612828565b6040519283916020830160209181520190565b612941565b610ad7612053611f0461204e6120488551611e95565b60021b90565b61174b565b61141060405193849260208401611ecd565b905f915f5b60058110156120c857600481036120c15760185b612094600160ff8460051b86015193161b611ea4565b16805b6120a4575060010161206a565b6120ad81611ea4565b16935f198114611746576001019380612097565b60fa61207e565b5050565b90801591821592836120df575b50505090565b90919250600382029180830460031490151715611746578260011b928304600214171561174657115f80806120d9565b94929196959390966040519788966020880161212a9161107b565b9063ffffffff60e01b1681526004016121429161107b565b916001600160401b0360c01b16825263ffffffff60e01b166008820152600c0161216b9161107b565b6121749161107b565b03601f19810183526101829083610151565b612267612244610def610ad7936121c36121a1825160ff1690565b60405160f89190911b6001600160f81b03191660208201529283906021820190565b610def6121da611163602084015163ffffffff1690565b6121f560408401516040519384916020830160209181520190565b606083015193612275608061221461134788516001600160401b031690565b95612252604061222e61116360208c015163ffffffff1690565b9901516040519a8b916020830160209181520190565b03601f1981018b528a610151565b01516040519889916020830160209181520190565b03601f198101895288610151565b61210f565b80612283575090565b81039081116117465790565b929091611e029261298a565b602081019081516020810180911161174657815151106100c8576020905181835182010191829101116117465760206122d391612bc6565b9080519060208201809211611746575290565b906020820191825182810180911161174657815151106100c857811561233457602090518184518201019182910111611746578161232391612bc6565b918051918201809211611746575290565b505050604051612345602082610151565b5f815290565b60ff60049116019060ff821161174657565b1561236457565b60405162461bcd60e51b815260206004820152602860248201527f756e657870656374656420707265666978206465636f64696e6720436f6d706160448201526731ba1e2ab4b73a1f60c11b6064820152608490fd5b906001600160401b03809116911601906001600160401b03821161174657565b6123e381612548565b60038116806123fd5750610ad7915060021c603f16611f04565b6001810361244257506112289061243c611f04612432612422611f04610ad797612548565b60061b67ffffffffffffffc01690565b9260021c603f1690565b906123ba565b600281036124b85750610c60906124ab60ff8461249c82612465610ad798612548565b958161248b8161247d61247788612548565b97612548565b991660081b63ffffff001690565b911617921660101b63ffff00001690565b17921660181b63ff0000001690565b1760021c633fffffff1690565b6003036124f257610ad79160ff6124de6124d9611d2294603f9060021c1690565b61234b565b16906124ed600883111561235d565b6122e6565b60405162461bcd60e51b815260206004820152601a60248201527f436f64652073686f756c6420626520756e726561636861626c650000000000006044820152606490fd5b908151811015610e84570160200190565b6020810190815160018101809111611746578151511061258e575181516001600160f81b0319916125799190612537565b511660f81c906125898151611738565b905290565b60405162461bcd60e51b815260206004820152600c60248201526b4f7574206f662072616e676560a01b6044820152606490fd5b6125ca6118e8565b50602081019081516004810180911161174657815151106100c85760208151818451820101918291011161174657600461260391612bc6565b918051906004820180921161174657525f915f905b600482106126585750508061262f612635926123da565b906122e6565b612650612640610193565b6001600160e01b03199093168352565b602082015290565b90926001600160f81b031961266d8584612537565b5116908460031b9185830460081486151715611746576001926001600160e01b0319918216901c1617930190612618565b80516020116100c857602080610ad79201612bc6565b80516008116100c85760086020610ad79201612bc6565b8051600c116100c85760288101906020018110611746576004610ad791612bc6565b90815181116100c8578015612708576020610ad79201612bc6565b5050604051612345602082610151565b8051806020116100c857601f198101908111611746576040820191602001821061174657610ad791612bc6565b805180600c116100c857600b19810190811161174657602c820191602001821061174657610ad791612bc6565b8015611746575f190190565b80515f91815b61278d57505090565b90915f19830190838211611746576127a58284612537565b5160f81c91600381901b906001600160fd1b038116036117465760ff8111611746576001901b91828102928184041490151715611746576127ef916127e991611767565b92612772565b9081612784565b9061280082610246565b61280d6040519182610151565b828152809261281e601f1991610246565b0190602036910137565b8060081c9060081b907cff000000ff000000ff000000ff000000ff000000ff000000ff000000ff7dff000000ff000000ff000000ff000000ff000000ff000000ff000000ff007fff000000ff000000ff000000ff000000ff000000ff000000ff000000ff00000084167eff000000ff000000ff000000ff000000ff000000ff000000ff000000ff000084161760101c931691161760101b177bffffffff00000000ffffffff00000000ffffffff00000000ffffffff7fffffffff00000000ffffffff00000000ffffffff00000000ffffffff00000000821660201c911660201b1777ffffffffffffffff0000000000000000ffffffffffffffff8019821660401c911660401b1761293d8160801c9160801b90565b1790565b80515f198101908111611746575b6001600160f81b03196129628284612537565b51166129765761297190612772565b61294f565b6001810180911161174657610ad7916126ed565b9290928215611e86578351938415612bb75760015b858110612b8957506001841480612b7f575b80612b6d575b612b52576129cc6129c785612e08565b6127f6565b946129d5610193565b945f8652602086019687526129e8610193565b925f8452602084019485526129fb6101a2565b905f82526020820193845260408201525f91805b612a9e575b50505051612a8f575190515103612a805781515f190182525b815115612a7057612a3d82613110565b612a4683613110565b90612a52845160010190565b84525f5260205260405f20612a6a8451845190610e89565b52612a2d565b91612a7c915051610e77565b5190565b637227423160e11b5f5260045ffd5b63072afb8760e51b5f5260045ffd5b612aa781612e2e565b92612ac1612ab96001861b8094611ec0565b928392611767565b938685612ace8187612eb3565b92602084015180155f14612b06575050505050508551518551145f03612a145780612b01612afb876130f4565b8a6130d9565b612a0f565b60011480612b4a575b15612b36575050506020612b2d826040612b01940151905190610e89565b5101518a6130d9565b91612b0193916002612afb941b0391612f3c565b508015612b0f565b925090925051612a8057612b67602091610e77565b51015190565b50612b7781610e77565b5151156129b7565b50600185146129b1565b612b938183610e89565b5151612ba7612ba183611ea4565b84610e89565b51511015611e5d5760010161299f565b631a14a47760e31b5f5260045ffd5b919091612bd28361025d565b612bdf6040519182610151565b838152612beb8461025d565b602082019190601f1901368337939091905b6020811015612c3c5780612c1d57505f19905b5182518216911916179052565b612c31612c2c612c3692611eb2565b61312c565b611ea4565b90612c10565b909182518152602081018091116117465791602081018091116117465790601f19810190811115612bfd57611724565b6001811115612c97575f19810190811161174657612c8990612e2e565b600181018091116117465790565b505f90565b9192905f83515b6001612cae86610e77565b5114612df957612cd5612c31856001612ccf612cc98a610e77565b51612e2e565b1b611767565b905f915f915b808310612cfe57505050612cf2612cf89194611738565b60011c90565b92612ca3565b909192612d0b8489610e89565b5182612d1686611738565b1080612ddb575b15612d7c5790612d686001926002612d53612d38898c610e89565b51612d4b612d458b611738565b8d610e89565b51908461313b565b9701965b612d61848b610e89565b5260011c90565b612d72828b610e89565b5201929190612cdb565b8987600183188610612dbe5791600180612db4612d6894612dac8c612da58d9e9d869b9a610e89565b5192610e89565b51908561313b565b9801980196612d57565b612d689150916001612dd288829695610e89565b51970196612d57565b50612dee612de886611738565b8a610e89565b516001821814612d1d565b5050915050612a7c9150610e77565b90815f925b612e145750565b915f19830183811161174657600193169283910192612e0d565b806fffffffffffffffffffffffffffffffff1060071b81811c6001600160401b031060061b1781811c63ffffffff1060051b1781811c61ffff1060041b1781811c60ff1060031b1781811c600f1060021b1781811c60031060011b1790811c6001101790565b60405190612ea182610100565b60606040835f81525f60208201520152565b612ebb612e94565b508051612ece6020830191825190611767565b928251905b848210612f15575b50829350612eee61258992935182611ec0565b908451946040810151612eff6101a2565b9687528360208801526040870152528251611ec0565b90612f24816040860151610e89565b5151821115612f365760010190612ed3565b90612edb565b91906020830151835193612f4f826127f6565b93612f59836127f6565b955f5b84811061308b5750505050935b6001612f7484610e77565b511461307e575f945f905b808210612f93575050848084528452612f69565b9095612f9f8786610e89565b51878784612fac83611738565b1080613060575b1561300757600192612fe583612fdd612d45612fd66130019997612fef97610e89565b5192611738565b51908361313b565b612d61848c610e89565b612ff98289610e89565b520196611759565b90612f7f565b5050845160208601515111156130515760019161303d8392612fe561302c8c8c610e89565b516130368a6130f4565b908361313b565b6130478289610e89565b5201960190612f7f565b63d8f29a1560e01b5f5260045ffd5b5061307361306d83611738565b89610e89565b516001841814612fb3565b93505050612a7c90610e77565b6001906130c46040860160206130ac82516130a68689611767565b90610e89565b5101516130b9848d610e89565b525182850190610e89565b515184016130d2828a610e89565b5201612f5c565b906130ea6020830151835190610e89565b5260018151019052565b6131046020820151825190610e89565b51906001815101905290565b6131206020820151825190610e89565b5181515f190190915290565b601f8111611746576101000a90565b60011661314e575f5260205260405f2090565b905f5260205260405f209056fea2646970667358221220fc0273cbfe6c3a223e89de7e32f88ee15ff63e6a4b718841b56fc84ff6f8192464736f6c634300081e0033","sourceMap":"3245:12033:118:-:0;;;;;;;;;;;;;-1:-1:-1;;3245:12033:118;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;;;;;;;;4610:26;;4646:28;;3245:12033;;;;;;;;4610:26;3245:12033;;;;;;;;;;4646:28;3245:12033;;;;;;;;;;;;-1:-1:-1;3245:12033:118;;;;;;-1:-1:-1;3245:12033:118;;;;;-1:-1:-1;3245:12033:118","linkReferences":{}},"deployedBytecode":{"object":"0x60806040526004361015610011575f80fd5b5f3560e01c806301ffc9a71461007457806342a947b11461006f578063af8b91d61461006a578063afb5670a14610065578063e455995b146100605763f7e83aee1461005b575f80fd5b610966565b6108d5565b610891565b610871565b6107cf565b346100c85760203660031901126100c85760043563ffffffff60e01b81168091036100c857637bf41d7760e11b81149081156100b7575b50151560805260206080f35b6301ffc9a760e01b149050816100ab565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b608081019081106001600160401b038211176100fb57604052565b6100cc565b606081019081106001600160401b038211176100fb57604052565b604081019081106001600160401b038211176100fb57604052565b60c081019081106001600160401b038211176100fb57604052565b90601f801991011681019081106001600160401b038211176100fb57604052565b6040519061018261010083610151565b565b60405190610182608083610151565b60405190610182604083610151565b60405190610182606083610151565b6040519061018260a083610151565b91908260809103126100c8576040516101d8816100e0565b60608082948035845260208101356020850152604081013560408501520135910152565b906101406003198301126100c857604051610216816100e0565b60606102418294600435845260243560208501526102358160446101c0565b604085015260c46101c0565b910152565b6001600160401b0381116100fb5760051b60200190565b6001600160401b0381116100fb57601f01601f191660200190565b81601f820112156100c85780359061028f8261025d565b9261029d6040519485610151565b828452602083830101116100c857815f926020809301838601378301015290565b359063ffffffff821682036100c857565b35906001600160401b03821682036100c857565b9190916060818403126100c857604051906102fd82610100565b819381356001600160401b0381116100c85782019080601f830112156100c85781359161032983610246565b926103376040519485610151565b80845260208085019160051b830101918383116100c85760208101915b83831061038157505050505060408092610241928552610376602082016102be565b6020860152016102cf565b82356001600160401b0381116100c8578201906040828703601f1901126100c857604051906103af8261011b565b60208301356001600160f01b0319811681036100c85782526040830135916001600160401b0383116100c8576103ed88602080969581960101610278565b83820152815201920191610354565b9080601f830112156100c8576040519161041760a084610151565b829060a081019283116100c857905b8282106104335750505090565b8135815260209182019101610426565b9080601f830112156100c8576040519161045e606084610151565b8290606081019283116100c857905b82821061047a5750505090565b813581526020918201910161046d565b9080601f830112156100c857604051916104a560c084610151565b829060c081019283116100c857905b8282106104c15750505090565b81358152602091820191016104b4565b91908260609103126100c8576040516104e981610100565b60408082946104f7816102cf565b8452610505602082016102be565b60208501520135910152565b919091610100818403126100c8576040519061052c82610136565b819381359160ff831683036100c85761056a60e09260a0948652610552602084016102be565b602087015260408301356040870152606083016104d1565b606085015260c081013560808501520135910152565b9080601f830112156100c857813561059781610246565b926105a56040519485610151565b81845260208085019260051b8201019283116100c857602001905b8282106105cd5750505090565b81358152602091820191016105c0565b919091610380818403126100c8576105f3610172565b9281356001600160401b0381116100c857816106109184016102e3565b845261061f81602084016103fc565b60208501526106318160c08401610443565b604085015261064481610120840161048a565b60608501526101e08201356001600160401b0381116100c85781610669918401610278565b608085015261067c816102008401610443565b60a085015261068f816102608401610511565b60c08501526103608201356001600160401b0381116100c8576106b29201610580565b60e0830152565b9190916060818403126100c857604051906106d382610100565b819381356001600160401b0381116100c857820181601f820112156100c8578035906106fe82610246565b9161070c6040519384610151565b80835260208084019160051b830101918483116100c85760208101915b83831061075a575050505083526020820135916001600160401b0383116100c8576105056040939284938301610580565b82356001600160401b0381116100c8578201906060828803601f1901126100c8576040519061078882610100565b60208301358252604083013560208301526060830135916001600160401b0383116100c8576107bf89602080969581960101610278565b6040820152815201920191610729565b346100c8576101603660031901126100c8576107ea366101fc565b50610144356001600160401b0381116100c857604060031982360301126100c857604051906108188261011b565b80600401356001600160401b0381116100c85761083b90600436918401016105dd565b825260248101356001600160401b0381116100c857602091600461086292369201016106b9565b910152005b5f9103126100c857565b346100c8575f3660031901126100c857604051610dad60f31b8152602090f35b346100c8575f3660031901126100c8576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b346100c8575f3660031901126100c857602060405163ffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b9181601f840112156100c8578235916001600160401b0383116100c857602083818601950101116100c857565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b346100c85760403660031901126100c8576004356001600160401b0381116100c857610996903690600401610915565b602435916001600160401b0383116100c8576109b96109c1933690600401610915565b929091610c29565b91906109d860405192606084526060840190610942565b9282840360208401526020808351958681520192015f945b808610610a0557505082935060408301520390f35b9092602060a06001926040808851805184528581015186850152015180518284015284810151606084015201516080820152019401950194906109f0565b90610140828203126100c857610a889060c060405193610a62856100e0565b8035855260208101356020860152610a7d83604083016101c0565b6040860152016101c0565b606082015290565b9190916040818403126100c85780356001600160401b0381116100c85783610ab99183016105dd565b9260208201356001600160401b0381116100c857610ad792016106b9565b90565b6101829092919260c060606101408301958051845260208101516020850152610b2960408201516040860190606080918051845260208101516020850152604081015160408501520151910152565b0151910190606080918051845260208101516020850152604081015160408501520151910152565b60405190610b5e82610100565b5f6040838281528260208201520152565b60405190610b7e602083610151565b5f80835282815b828110610b9157505050565b602090604051610ba081610100565b5f81525f83820152610bb0610b51565b604082015282828501015201610b85565b90610bcb82610246565b610bd86040519182610151565b8281528092610be9601f1991610246565b01905f5b828110610bf957505050565b602090604051610c0881610100565b5f81525f83820152610c18610b51565b604082015282828501015201610bed565b610c3d90610c469392959495810190610a43565b93810190610a90565b8392919251610c69610c6060208651015163ffffffff1690565b63ffffffff1690565b1115610dd857610d77610cac610c8385610d699697610e9d565b90937f0000000000000000000000000000000000000000000000000000000000000000916110b7565b929095610cc084516001600160401b031690565b9360608301948551906001600160401b03825191168181115f14610d8c575050855160408501525080516001600160401b031691606060c06040610d0b602086015163ffffffff1690565b940151920151015180516001600160401b03858116911603610d7f57604063ffffffff910151925b6001600160401b03610d43610184565b951685521660208401526040830152606082015283525b60405194859160208301610ada565b03601f198101855284610151565b515191929190565b5063ffffffff5f92610d33565b9193501480610dcc575b610da2575b5050610d5a565b6040916020610dba610c608285015163ffffffff1690565b91015201516040845101525f80610d9b565b50604082015115610d96565b506040519150610dfd82610def8560208301610ada565b03601f198101845283610151565b6060610e07610b6f565b9301515191929190565b60405190610e1e826100e0565b5f6060838281528260208201528260408201520152565b60405190610e42826100e0565b815f81525f6020820152610e54610e11565b60408201526060610241610e11565b634e487b7160e01b5f52603260045260245ffd5b805115610e845760200190565b610e63565b8051821015610e845760209160051b010190565b9190610ea7610e35565b5080516040808201805191860180515190926001600160401b0316908114159081611004575b50610ff557519051805190916001600160401b031603610fea575b604081015115610fdb57610f059083610f00846112f6565b611577565b805151915f925f5b818110610f575750508215610f4857610f3f610c60602060c094610f34608097868b6117b4565b015163ffffffff1690565b85520151015190565b6323188e3960e21b5f5260045ffd5b610dad60f31b610f8a610f7d610f6e848851610e89565b51516001600160f01b03191690565b6001600160f01b03191690565b1480610fc2575b610f9e575b600101610f0d565b93506001610fba6020610fb2878751610e89565b5101516116c3565b949050610f96565b50602080610fd1838751610e89565b5101515114610f91565b6334f6ba9f60e01b5f5260045ffd5b506060840151610ee8565b637202e68560e11b5f5260045ffd5b905060608701515114155f610ecd565b604051906110218261011b565b5f6020838281520152565b9061103682610246565b6110436040519182610151565b8281528092611054601f1991610246565b01905f5b82811061106457505050565b60209061106f611014565b82828501015201611058565b805191908290602001825e015f815290565b6040516001600160e01b031990911660208201529190610182908390610def90602483019061107b565b929190926110c3610b51565b93805151916110d18361102c565b926110db81610bc1565b945f5b82811061112a5750506110f3575b5050509190565b61110f92826040602061110b95015191015192611dbd565b1590565b61111b575f80806110ec565b6380b6d5fd60e01b5f5260045ffd5b611135818651610e89565b516040810161114481516119ba565b916111ab602082519201926111a561119e611163865163ffffffff1690565b6001600160e01b03199063ff00ff00600882811b9190911691901c62ff00ff1617601081811b63ffff00001691901c61ffff161760e01b1690565b9151611b06565b9061108d565b602081519101206111ba610193565b91825260208201526111cc848a610e89565b526111d78389610e89565b50805160208301516111e884611b2b565b906111f16101a2565b92835260208301526040820152611208848b610e89565b52611213838a610e89565b506112346112288c516001600160401b031690565b6001600160401b031690565b159081611260575b5061124b575b506001016110de565b600191995061125990611c98565b9890611242565b90505163ffffffff8416145f61123c565b60405190611280602083610151565b5f8252565b610def6112a494936112a461018294604051978895602087019061107b565b9061107b565b610182926112a495946112c9600c94604051988995602087019061107b565b6001600160e01b03199290921682526001600160c01b031916600482015203601319810185520183610151565b90815151611302611271565b905f5b8181106113d757509261131b610ad79394611ee8565b916113d16113476040611338611163602087015163ffffffff1690565b9401516001600160401b031690565b67ffffffffffff000067ff00ff00ff00ff0066ff00ff00ff00ff8360081c169260081b169165ffff0000ffff65ffff0000ff0065ffffffffffff67ffff0000ffff0000861666ff0000ffff000085161760101c16941691161760101b161767ffffffff0000000063ffffffff8260201c169160201b166001600160401b0360c01b911760c01b1690565b926112aa565b9161143c60019161141061141e6113f2610f6e888b51610e89565b6040516001600160f01b031990911660208201529182906022820190565b03601f198101835282610151565b611436602061142e888b51610e89565b510151611b06565b91611285565b9201611305565b906060828203126100c85780601f830112156100c85760405191611468606084610151565b8290606081019283116100c857905b8282106114845750505090565b8151815260209182019101611477565b906020610ad7928181520190610942565b6040513d5f823e3d90fd5b905f905b600382106114c157505050565b60208060019285518152019301910190916114b4565b908152939695949291905f602086015b60058210611561575050509161152f61152361153a9361150f879660c06102009901906114b0565b6102c06101208701526102c0860190610942565b976101408501906114b0565b6101a08301906114b0565b015f905b6006821061154b57505050565b602080600192855181520193019101909161153e565b60208060019285518152019301910190916114e7565b602082019061159761110b61158c8451612065565b6020870151906120cc565b6116b4576040516378b8c33160e11b8152937f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031691606090869081906115e89060048301611494565b0381855afa9485156116af575f9561167b575b50604001519151604084015192608085015195606060a0870151960151843b156100c8575f9661164292604051998a988997889763aa8f580360e01b8952600489016114d7565b03915afa9081611661575b50610182576303c6c1a560e21b5f5260045ffd5b8061166f5f61167593610151565b80610867565b5f61164d565b60409195506116a19060603d6060116116a8575b6116998183610151565b810190611443565b94906115fb565b503d61168f565b6114a5565b633aa90f7f60e21b5f5260045ffd5b60208151106116d3576020015190565b60405162461bcd60e51b8152602060048201526024808201527f42797465733a3a20746f427974657333323a206461746120697320746f20736860448201526337b93a1760e11b6064820152608490fd5b634e487b7160e01b5f52601160045260245ffd5b906001820180921161174657565b611724565b906003820180921161174657565b906002820180921161174657565b9190820180921161174657565b604080519091906117858382610151565b6001815291601f1901825f5b82811061179d57505050565b6020906117a8611014565b82828501015201611791565b61110b916118999360e060c083019261185d611858602061183487516117db815160ff1690565b906117ec8482015163ffffffff1690565b906040810151611820608060608401519301519361181461180b6101b1565b60ff9097168752565b63ffffffff1685880152565b604084015260608301526080820152612186565b8051908201209701518651602001516118529063ffffffff16610c60565b9061227a565b611738565b9460a0611868611774565b9551015190611875610193565b918252602082015261188685610e77565b5261189084610e77565b5001519061228f565b61189f57565b630b92186960e31b5f5260045ffd5b6040519060a082018281106001600160401b038211176100fb5760405260606080835f81525f60208201525f60408201525f838201520152565b604051906118f58261011b565b60606020835f81520152565b6040519061012082018281106001600160401b038211176100fb576040525f6101008382815261192f6118e8565b60208201528260408201526119426118e8565b60608201528260808201526119556118e8565b60a08201528260c0820152606060e08201520152565b9061197582610246565b6119826040519182610151565b8281528092611993601f1991610246565b01905f5b8281106119a357505050565b6020906119ae611901565b82828501015201611997565b6119c26118ae565b506119cb610193565b9081525f60208201526119e56119e08261229b565b6116c3565b906119ef816123da565b6119fb6119e08361229b565b611a076119e08461229b565b91611a11846123da565b611a1a8161196b565b945f5b828110611a4957505050611a2f6101b1565b948552602085015260408401526060830152608082015290565b600190611a5583612548565b60ff611a5f611901565b911680611a8b5750600160c08201525b611a79828a610e89565b52611a848189610e89565b5001611a1d565b60048103611aaf575060016040820152611aa4846125c2565b60608201525b611a6f565b60058103611ad2575060016080820152611ac8846125c2565b60a0820152611a6f565b60068103611af2575060018152611ae8846125c2565b6020820152611a6f565b600803611aaa576001610100820152611a6f565b610ad76112a491611410611b1a8251611ee8565b91604051948593602085019061107b565b611b33610b51565b505f5f925f5f5b6080850180518051831015611c64576040611b5884611b6093610e89565b510151151590565b80611c3c575b611bf3575b611b7b6040611b58848451610e89565b80611bba575b611b8f575b50600101611b3a565b819250611bb260206060611ba860809560019551610e89565b510151015161277e565b929150611b86565b50634953544d60e01b63ffffffff60e01b611bec6060611bdb868651610e89565b510151516001600160e01b03191690565b1614611b81565b95509250611c166119e060206060611c0c878a51610e89565b510151015161269e565b92611c366119e060206060611c2c858b51610e89565b5101510151612718565b95611b6b565b5063049534d560e41b63ffffffff60e01b611c5d6060611bdb868651610e89565b1614611b66565b505050925092908215611c8957611c796101a2565b9283526020830152604082015290565b633eba99d160e21b5f5260045ffd5b611ca0610b51565b5f915b6080810180518051851015611db55761110b6040611b5887611cc494610e89565b611da9576341504b4360e01b611cf0611ce36060611bdb888651610e89565b6001600160e01b03191690565b03611da9576060611d048560209351610e89565b510151015191602c835103611d9c57611d27611228611d22856126b4565b61277e565b936001600160401b03851615611d8857505050611d80611d586119e0611d52610c60611d22866126cb565b93612745565b91611d73611d646101a2565b6001600160401b039095168552565b63ffffffff166020840152565b604082015290565b608092945060019193505b01929050611ca3565b9150916001608091611d93565b50916001608091611d93565b505050919050565b919392908115611e86578451611dd2816127f6565b611ddb826127f6565b916001611de786612c6c565b1b5f805b838210611e065750505050611e0294959650612c9c565b1490565b611e10828c610e89565b51519088821015611e77578215159081611e6c575b50611e5d576001906020611e39848e610e89565b510151611e468489610e89565b52808401611e548488610e89565b52910190611deb565b630647f54960e21b5f5260045ffd5b90508111155f611e25565b630834466160e31b5f5260045ffd5b639136328760e01b5f5260045ffd5b60031981019190821161174657565b5f1981019190821161174657565b602003906020821161174657565b9190820391821161174657565b600190610ad7939260ff60f81b9060f81b168152019061107b565b6040811015611f2c57610ad7611f0a611f046114109360021b90565b60ff1690565b60405160f89190911b6001600160f81b03191660208201529182906021820190565b614000811015611f8b57610ad7611f69611f56611f4f6118586114109560021b90565b61ffff1690565b60ff61ff008260081b169160081c161790565b60405160f09190911b6001600160f01b03191660208201529182906022820190565b634000000081101561200857610ad7611fe6611fb5610c60611fb06114109560021b90565b611759565b63ffffff0062ff00ff62ffffff818460081c1616921660081b161763ffff000061ffff8260101c169160101b161790565b60405160e09190911b6001600160e01b03191660208201529182906024820190565b61141061202d61201a61203293612828565b6040519283916020830160209181520190565b612941565b610ad7612053611f0461204e6120488551611e95565b60021b90565b61174b565b61141060405193849260208401611ecd565b905f915f5b60058110156120c857600481036120c15760185b612094600160ff8460051b86015193161b611ea4565b16805b6120a4575060010161206a565b6120ad81611ea4565b16935f198114611746576001019380612097565b60fa61207e565b5050565b90801591821592836120df575b50505090565b90919250600382029180830460031490151715611746578260011b928304600214171561174657115f80806120d9565b94929196959390966040519788966020880161212a9161107b565b9063ffffffff60e01b1681526004016121429161107b565b916001600160401b0360c01b16825263ffffffff60e01b166008820152600c0161216b9161107b565b6121749161107b565b03601f19810183526101829083610151565b612267612244610def610ad7936121c36121a1825160ff1690565b60405160f89190911b6001600160f81b03191660208201529283906021820190565b610def6121da611163602084015163ffffffff1690565b6121f560408401516040519384916020830160209181520190565b606083015193612275608061221461134788516001600160401b031690565b95612252604061222e61116360208c015163ffffffff1690565b9901516040519a8b916020830160209181520190565b03601f1981018b528a610151565b01516040519889916020830160209181520190565b03601f198101895288610151565b61210f565b80612283575090565b81039081116117465790565b929091611e029261298a565b602081019081516020810180911161174657815151106100c8576020905181835182010191829101116117465760206122d391612bc6565b9080519060208201809211611746575290565b906020820191825182810180911161174657815151106100c857811561233457602090518184518201019182910111611746578161232391612bc6565b918051918201809211611746575290565b505050604051612345602082610151565b5f815290565b60ff60049116019060ff821161174657565b1561236457565b60405162461bcd60e51b815260206004820152602860248201527f756e657870656374656420707265666978206465636f64696e6720436f6d706160448201526731ba1e2ab4b73a1f60c11b6064820152608490fd5b906001600160401b03809116911601906001600160401b03821161174657565b6123e381612548565b60038116806123fd5750610ad7915060021c603f16611f04565b6001810361244257506112289061243c611f04612432612422611f04610ad797612548565b60061b67ffffffffffffffc01690565b9260021c603f1690565b906123ba565b600281036124b85750610c60906124ab60ff8461249c82612465610ad798612548565b958161248b8161247d61247788612548565b97612548565b991660081b63ffffff001690565b911617921660101b63ffff00001690565b17921660181b63ff0000001690565b1760021c633fffffff1690565b6003036124f257610ad79160ff6124de6124d9611d2294603f9060021c1690565b61234b565b16906124ed600883111561235d565b6122e6565b60405162461bcd60e51b815260206004820152601a60248201527f436f64652073686f756c6420626520756e726561636861626c650000000000006044820152606490fd5b908151811015610e84570160200190565b6020810190815160018101809111611746578151511061258e575181516001600160f81b0319916125799190612537565b511660f81c906125898151611738565b905290565b60405162461bcd60e51b815260206004820152600c60248201526b4f7574206f662072616e676560a01b6044820152606490fd5b6125ca6118e8565b50602081019081516004810180911161174657815151106100c85760208151818451820101918291011161174657600461260391612bc6565b918051906004820180921161174657525f915f905b600482106126585750508061262f612635926123da565b906122e6565b612650612640610193565b6001600160e01b03199093168352565b602082015290565b90926001600160f81b031961266d8584612537565b5116908460031b9185830460081486151715611746576001926001600160e01b0319918216901c1617930190612618565b80516020116100c857602080610ad79201612bc6565b80516008116100c85760086020610ad79201612bc6565b8051600c116100c85760288101906020018110611746576004610ad791612bc6565b90815181116100c8578015612708576020610ad79201612bc6565b5050604051612345602082610151565b8051806020116100c857601f198101908111611746576040820191602001821061174657610ad791612bc6565b805180600c116100c857600b19810190811161174657602c820191602001821061174657610ad791612bc6565b8015611746575f190190565b80515f91815b61278d57505090565b90915f19830190838211611746576127a58284612537565b5160f81c91600381901b906001600160fd1b038116036117465760ff8111611746576001901b91828102928184041490151715611746576127ef916127e991611767565b92612772565b9081612784565b9061280082610246565b61280d6040519182610151565b828152809261281e601f1991610246565b0190602036910137565b8060081c9060081b907cff000000ff000000ff000000ff000000ff000000ff000000ff000000ff7dff000000ff000000ff000000ff000000ff000000ff000000ff000000ff007fff000000ff000000ff000000ff000000ff000000ff000000ff000000ff00000084167eff000000ff000000ff000000ff000000ff000000ff000000ff000000ff000084161760101c931691161760101b177bffffffff00000000ffffffff00000000ffffffff00000000ffffffff7fffffffff00000000ffffffff00000000ffffffff00000000ffffffff00000000821660201c911660201b1777ffffffffffffffff0000000000000000ffffffffffffffff8019821660401c911660401b1761293d8160801c9160801b90565b1790565b80515f198101908111611746575b6001600160f81b03196129628284612537565b51166129765761297190612772565b61294f565b6001810180911161174657610ad7916126ed565b9290928215611e86578351938415612bb75760015b858110612b8957506001841480612b7f575b80612b6d575b612b52576129cc6129c785612e08565b6127f6565b946129d5610193565b945f8652602086019687526129e8610193565b925f8452602084019485526129fb6101a2565b905f82526020820193845260408201525f91805b612a9e575b50505051612a8f575190515103612a805781515f190182525b815115612a7057612a3d82613110565b612a4683613110565b90612a52845160010190565b84525f5260205260405f20612a6a8451845190610e89565b52612a2d565b91612a7c915051610e77565b5190565b637227423160e11b5f5260045ffd5b63072afb8760e51b5f5260045ffd5b612aa781612e2e565b92612ac1612ab96001861b8094611ec0565b928392611767565b938685612ace8187612eb3565b92602084015180155f14612b06575050505050508551518551145f03612a145780612b01612afb876130f4565b8a6130d9565b612a0f565b60011480612b4a575b15612b36575050506020612b2d826040612b01940151905190610e89565b5101518a6130d9565b91612b0193916002612afb941b0391612f3c565b508015612b0f565b925090925051612a8057612b67602091610e77565b51015190565b50612b7781610e77565b5151156129b7565b50600185146129b1565b612b938183610e89565b5151612ba7612ba183611ea4565b84610e89565b51511015611e5d5760010161299f565b631a14a47760e31b5f5260045ffd5b919091612bd28361025d565b612bdf6040519182610151565b838152612beb8461025d565b602082019190601f1901368337939091905b6020811015612c3c5780612c1d57505f19905b5182518216911916179052565b612c31612c2c612c3692611eb2565b61312c565b611ea4565b90612c10565b909182518152602081018091116117465791602081018091116117465790601f19810190811115612bfd57611724565b6001811115612c97575f19810190811161174657612c8990612e2e565b600181018091116117465790565b505f90565b9192905f83515b6001612cae86610e77565b5114612df957612cd5612c31856001612ccf612cc98a610e77565b51612e2e565b1b611767565b905f915f915b808310612cfe57505050612cf2612cf89194611738565b60011c90565b92612ca3565b909192612d0b8489610e89565b5182612d1686611738565b1080612ddb575b15612d7c5790612d686001926002612d53612d38898c610e89565b51612d4b612d458b611738565b8d610e89565b51908461313b565b9701965b612d61848b610e89565b5260011c90565b612d72828b610e89565b5201929190612cdb565b8987600183188610612dbe5791600180612db4612d6894612dac8c612da58d9e9d869b9a610e89565b5192610e89565b51908561313b565b9801980196612d57565b612d689150916001612dd288829695610e89565b51970196612d57565b50612dee612de886611738565b8a610e89565b516001821814612d1d565b5050915050612a7c9150610e77565b90815f925b612e145750565b915f19830183811161174657600193169283910192612e0d565b806fffffffffffffffffffffffffffffffff1060071b81811c6001600160401b031060061b1781811c63ffffffff1060051b1781811c61ffff1060041b1781811c60ff1060031b1781811c600f1060021b1781811c60031060011b1790811c6001101790565b60405190612ea182610100565b60606040835f81525f60208201520152565b612ebb612e94565b508051612ece6020830191825190611767565b928251905b848210612f15575b50829350612eee61258992935182611ec0565b908451946040810151612eff6101a2565b9687528360208801526040870152528251611ec0565b90612f24816040860151610e89565b5151821115612f365760010190612ed3565b90612edb565b91906020830151835193612f4f826127f6565b93612f59836127f6565b955f5b84811061308b5750505050935b6001612f7484610e77565b511461307e575f945f905b808210612f93575050848084528452612f69565b9095612f9f8786610e89565b51878784612fac83611738565b1080613060575b1561300757600192612fe583612fdd612d45612fd66130019997612fef97610e89565b5192611738565b51908361313b565b612d61848c610e89565b612ff98289610e89565b520196611759565b90612f7f565b5050845160208601515111156130515760019161303d8392612fe561302c8c8c610e89565b516130368a6130f4565b908361313b565b6130478289610e89565b5201960190612f7f565b63d8f29a1560e01b5f5260045ffd5b5061307361306d83611738565b89610e89565b516001841814612fb3565b93505050612a7c90610e77565b6001906130c46040860160206130ac82516130a68689611767565b90610e89565b5101516130b9848d610e89565b525182850190610e89565b515184016130d2828a610e89565b5201612f5c565b906130ea6020830151835190610e89565b5260018151019052565b6131046020820151825190610e89565b51906001815101905290565b6131206020820151825190610e89565b5181515f190190915290565b601f8111611746576101000a90565b60011661314e575f5260205260405f2090565b905f5260205260405f209056fea2646970667358221220fc0273cbfe6c3a223e89de7e32f88ee15ff63e6a4b718841b56fc84ff6f8192464736f6c634300081e0033","sourceMap":"3245:12033:118:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;:::i;:::-;;:::i;:::-;;:::i;:::-;;:::i;:::-;;;;;;-1:-1:-1;;3245:12033:118;;;;;;;;;;;;;;;;-1:-1:-1;;;4803:45:118;;;:85;;;;3245:12033;;;;;;;;;4803:85;-1:-1:-1;;;829:40:49;;-1:-1:-1;4803:85:118;;;3245:12033;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;:::o;:::-;;:::i;:::-;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;:::o;:::-;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;:::o;:::-;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;:::o;:::-;;;;;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;:::o;:::-;;;;;;;;:::i;:::-;:::o;:::-;;;;;;;;:::i;:::-;13648:11;3245:12033;;;13648:11;3245:12033;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;-1:-1:-1;;3245:12033:118;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;:::o;:::-;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;:::o;:::-;-1:-1:-1;;;;;3245:12033:118;;;;;;-1:-1:-1;;3245:12033:118;;;;:::o;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;-1:-1:-1;3245:12033:118;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;:::o;:::-;;;-1:-1:-1;;;;;3245:12033:118;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;-1:-1:-1;;3245:12033:118;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;;3245:12033:118;;;;;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;:::i;:::-;;;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;:::i;:::-;;;;;:::o;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;-1:-1:-1;;3245:12033:118;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;-1:-1:-1;;3245:12033:118;;;;;;;:::i;:::-;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;:::i;:::-;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;:::o;:::-;;;;;;-1:-1:-1;;3245:12033:118;;;;;;-1:-1:-1;;;3245:12033:118;;;;;;;;;;;-1:-1:-1;;3245:12033:118;;;;;;3494:31;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;-1:-1:-1;;3245:12033:118;;;;;;;;3815:37;3245:12033;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;-1:-1:-1;3245:12033:118;;;;;;;;-1:-1:-1;;3245:12033:118;;;;:::o;:::-;;;;;;-1:-1:-1;;3245:12033:118;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;:::i;:::-;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;:::i;:::-;;;;;:::i;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;-1:-1:-1;3245:12033:118;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;:::o;:::-;;;;;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;:::i;:::-;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;:::i;:::-;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;-1:-1:-1;3245:12033:118;;;;;;;;;;;;:::o;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;3245:12033:118;;;;;;;;;:::o;:::-;;;;;;;;:::i;:::-;-1:-1:-1;3245:12033:118;;-1:-1:-1;3245:12033:118;;;;;;:::i;:::-;;;;;;;;;;;;;;4940:2380;5164:48;4940:2380;5306:58;4940:2380;;;;;5164:48;;;;:::i;:::-;5306:58;;;;;:::i;:::-;3245:12033;;;;;5463:59;3245:12033;5494:28;:16;;:28;3245:12033;;;;;;;;;;5463:59;-1:-1:-1;5463:59:118;5459:189;;7247:20;5852:63;5717:43;;7247:20;5717:43;;;:::i;:::-;5901:13;;;5852:63;;:::i;:::-;3245:12033;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;6494:25;;;;;;;3245:12033;-1:-1:-1;;;;;3245:12033:118;;;;6479:43;;;6475:754;6479:43;;;-1:-1:-1;;6569:25:118;;6538:28;;;:56;-1:-1:-1;3245:12033:118;;-1:-1:-1;;;;;3245:12033:118;6706:10;6494:25;6803:19;6538:28;3245:12033;5494:28;6706:10;;3245:12033;;;;;;6751:17;;3245:12033;6803:19;;;:36;;3245:12033;;-1:-1:-1;;;;;3245:12033:118;;;;;6803:55;:152;;6538:28;3245:12033;6881:41;;3245:12033;6803:152;;-1:-1:-1;;;;;3245:12033:118;;:::i;:::-;;;;;;5494:28;6636:334;;3245:12033;6538:28;6636:334;;3245:12033;6494:25;6636:334;;3245:12033;6608:362;;6475:754;3245:12033;;7247:20;;;5494:28;7247:20;;;:::i;:::-;;3245:12033;;7247:20;;;;;;:::i;:::-;7284:25;3245:12033;7239:74;;;4940:2380;:::o;6803:152::-;;3245:12033;;6803:152;;;6475:754;6991:44;;-1:-1:-1;6991:44:118;;:94;;6475:754;6987:242;;6475:754;;;;;6987:242;7201:17;7133:10;5494:28;7101:42;3245:12033;7133:10;;;3245:12033;;;;;7101:42;:29;;3245:12033;7201:17;3245:12033;7201:17;7157:25;;:41;3245:12033;6987:242;;;;6991:94;7039:41;;;;3245:12033;7039:46;6991:94;;5459:189;-1:-1:-1;3245:12033:118;;;-1:-1:-1;5546:26:118;3245:12033;5546:26;;5494:28;5546:26;;;:::i;:::-;;3245:12033;;5546:26;;;;;;:::i;:::-;5602:31;5574:26;;:::i;:::-;5602:31;;;3245:12033;5538:99;;;;:::o;3245:12033::-;;;;;;;:::i;:::-;-1:-1:-1;3245:12033:118;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;:::i;:::-;;-1:-1:-1;3245:12033:118;;-1:-1:-1;3245:12033:118;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;:::i;:::-;;;;;;;;;;;;;;;:::o;7405:1697::-;;;3245:12033;;:::i;:::-;-1:-1:-1;7646:21:118;;7694:25;;;;3245:12033;;7723:32;;;;;3245:12033;7723:32;;-1:-1:-1;;;;;3245:12033:118;7694:64;;;;;;:145;;7405:1697;7677:226;;;3245:12033;7959:32;;3245:12033;;7959:32;;-1:-1:-1;;;;;3245:12033:118;7930:64;8039:76;;;7694:25;8359:28;;3245:12033;8359:33;8355:68;;8490:12;8452:24;;;;;:::i;:::-;8490:12;:::i;:::-;8538:18;;3245:12033;8573:15;3245:12033;8603:13;3245:12033;8618:17;;;;;;8861:21;;;;8857:54;;8981:50;3245:12033;9009:22;9064:24;8962:7;;9064:30;8962:7;;;;:::i;:::-;9009:22;3245:12033;;;;;8981:50;3245:12033;;9064:24;;:30;3245:12033;7405:1697;:::o;8857:54::-;8891:20;;;3245:12033;8891:20;;3245:12033;8891:20;8637:3;3245:12033;;;8660:47;:24;:21;:18;;;:21;:::i;:::-;;3245:12033;-1:-1:-1;;;;;;3245:12033:118;;;8660:24;-1:-1:-1;;;;;;3245:12033:118;;;8660:47;;:90;;;8637:3;8656:182;;8637:3;3245:12033;;8603:13;;8656:182;8796:18;;3245:12033;8780:43;8796:26;:21;:18;;;:21;:::i;:::-;;:26;;8780:43;:::i;:::-;8656:182;;;;;8660:90;8711:18;:26;:18;:21;:18;;;:21;:::i;:::-;;:26;;3245:12033;8711:39;8660:90;;8355:68;8401:22;;;3245:12033;8401:22;;3245:12033;8401:22;8039:76;8086:29;;;;;8039:76;;7677:226;7871:21;;;3245:12033;7871:21;;3245:12033;7871:21;7694:145;7807:29;;;;;;3245:12033;7778:61;;7694:145;;;3245:12033;;;;;;;:::i;:::-;-1:-1:-1;3245:12033:118;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;3245:12033:118;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;-1:-1:-1;;;;;;3245:12033:118;;;;;;;;;;;;;;;;;;;;:::i;13071:1645::-;;;;;3245:12033;;:::i;:::-;13304:16;;;3245:12033;13377:32;;;;:::i;:::-;13462:28;;;;:::i;:::-;13506:13;-1:-1:-1;13521:7:118;;;;;;14483;;14479:190;;13501:968;14679:30;;;;13071:1645;:::o;14479:190::-;14614:6;14559:11;;13648;13804:7;14519:77;14559:11;;;14580:15;;3245:12033;14519:77;;:::i;:::-;14614:6;;3245:12033;14614:6;14610:48;;14479:190;;;;;14610:48;14629:29;;;-1:-1:-1;14629:29:118;;-1:-1:-1;14629:29:118;13530:3;13573:19;:16;;;:19;:::i;:::-;;13648:11;;;13629:31;13648:11;;13629:31;:::i;:::-;3245:12033;13764:87;13804:7;3245:12033;;13804:7;;3245:12033;13815:35;13777:36;13797:15;3245:12033;;;;;;13797:15;-1:-1:-1;;;;;;3245:12033:118;;;;;;;;;;;;;8200:10:65;3245:12033:118;8168:49:65;3245:12033:118;;;;;;;;;;;8266:21:65;3245:12033:118;;;8815:111:65;;13777:36:118;13838:11;;13815:35;:::i;:::-;13764:87;;:::i;:::-;13804:7;3245:12033;;;;13754:98;3245:12033;;:::i;:::-;;;;13804:7;13687:179;;3245:12033;13675:191;;;;:::i;:::-;;;;;;:::i;:::-;;3245:12033;;13804:7;13985:13;;3245:12033;14028:34;;;:::i;:::-;3245:12033;;;:::i;:::-;;;;13804:7;13900:177;;3245:12033;13648:11;13900:177;;3245:12033;13881:196;;;;:::i;:::-;;;;;;:::i;:::-;;14339:17;3245:12033;;;-1:-1:-1;;;;;3245:12033:118;;;;-1:-1:-1;;;;;3245:12033:118;;;14339:17;;:44;;;;13530:3;14335:124;;;13530:3;;3245:12033;;13506:13;;14335:124;3245:12033;14412:32;;;;;;:::i;:::-;14335:124;;;;14339:44;3245:12033;;;;;;14360:23;14339:44;;;3245:12033;;;;;;;;:::i;:::-;;;;:::o;:::-;;;;;;;;;;;;;;;;;;:::i;:::-;;;:::i;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;-1:-1:-1;;;;;;3245:12033:118;;;;;;-1:-1:-1;;;;;;3245:12033:118;;;;;;-1:-1:-1;;3245:12033:118;;;;;;;:::i;1604:684:119:-;;1718:18;;3245:12033:118;;;:::i;:::-;1800:13:119;-1:-1:-1;1815:14:119;;;;;;2093:40;;;2067:214;2093:40;;;:::i;:::-;2188:22;2225:46;3245:12033:118;;2168:43:119;3245:12033:118;1915:42:119;2188:22;;3245:12033:118;;;;;2168:43:119;2245:25;;3245:12033:118;-1:-1:-1;;;;;3245:12033:118;;;;;7788:18:65;3245:12033:118;;;;;;;;;;7937:18:65;7933:22;3245:12033:118;7902:18:65;7898:22;;;;;;3245:12033:118;;;7932:30:65;7933:22;;;;3245:12033:118;;;7896:67:65;3245:12033:118;;;;;;8025:7:65;3245:12033:118;;;-1:-1:-1;;;;;3245:12033:118;;8012:21:65;;3245:12033:118;;;8698:111:65;;2225:46:119;2067:214;;:::i;1831:3::-;1932:18;1860:179;3245:12033:118;1932:18:119;1915:42;;1932:24;:21;:18;;;:21;:::i;:24::-;3245:12033:118;;-1:-1:-1;;;;;;3245:12033:118;;;1915:42:119;;;3245:12033:118;;;;;;;;;1915:42:119;;3245:12033:118;;1915:42:119;;;;;;:::i;:::-;1975:50;1915:42;1998:21;:18;;;:21;:::i;:::-;;:26;;1975:50;:::i;:::-;1860:179;;:::i;:::-;1831:3;3245:12033:118;1800:13:119;;3245:12033:118;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;:::i;:::-;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;9696:912;9914:18;;;;9947:54;9948:53;9901:32;9914:18;;9901:32;:::i;:::-;9914:18;9984:16;;3245:12033;9948:53;;:::i;9947:54::-;9943:90;;3245:12033;;-1:-1:-1;;;10072:32:118;;3245:12033;10072:4;-1:-1:-1;;;;;3245:12033:118;;10072:32;;3245:12033;;;;10072:32;;;;;;:::i;:::-;;;;;;;;;;;-1:-1:-1;10072:32:118;;;9696:912;10322:28;3245:12033;10322:28;3245:12033;10364:18;;3245:12033;10396:14;;;10424:19;;;;;10478:20;10072:32;10478:20;;;;10512:15;;;10297:240;;;;;-1:-1:-1;3245:12033:118;10297:240;3245:12033;;;;;;;;;;;;;10297:240;;10072:32;10297:240;;;:::i;:::-;;;;;;;;;9696:912;-1:-1:-1;10293:309:118;;10568:23;;;-1:-1:-1;10568:23:118;10072:32;-1:-1:-1;10568:23:118;10297:240;;;-1:-1:-1;10297:240:118;;;:::i;:::-;;;:::i;:::-;;;;10072:32;3245:12033;10072:32;;;;;;;;;;;;;;;;:::i;:::-;;;;;:::i;:::-;;;;;;;;;;;;:::i;9943:90::-;10010:23;;;-1:-1:-1;10010:23:118;;-1:-1:-1;10010:23:118;4543:226:58;4650:2;3245:12033:118;;4635:17:58;3245:12033:118;;4650:2:58;4703:60;;4543:226;:::o;3245:12033:118:-;;;-1:-1:-1;;;3245:12033:118;;4650:2:58;3245:12033:118;;;;;;;;;;;;;;-1:-1:-1;;;3245:12033:118;;;;;;;;;;;;;;;;;;;;;12559:1;3245:12033;;;;;;;:::o;:::-;;:::i;:::-;;5093:1:65;3245:12033:118;;;;;;;:::o;:::-;;4835:1:65;3245:12033:118;;;;;;;:::o;:::-;;;;;;;;;;:::o;:::-;;;;;;;;;;;:::i;:::-;12559:1;3245:12033;;;-1:-1:-1;;3245:12033:118;;-1:-1:-1;3245:12033:118;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;;;;11826:1071;12769:75;11826:1071;12858:6;11826:1071;12810:14;12116:19;;;;12478:82;:78;12179:32;12035:403;12116:19;;3245:12033;;;;;;;;12179:32;3245:12033;12179:32;;;3245:12033;;;;;;12245:30;;;;3245:12033;12065:359;12380:25;12315:36;;;;12380:25;;3245:12033;;12065:359;3245:12033;;:::i;:::-;;;;;;;;12065:359;3245:12033;;12065:359;;;3245:12033;;12065:359;12245:30;12065:359;;3245:12033;12315:36;12065:359;;3245:12033;12380:25;12065:359;;3245:12033;12035:403;:::i;:::-;3245:12033;;;;;12012:436;12488:33;;3245:12033;12523:19;;12179:32;12523;3245:12033;12478:78;;3245:12033;;12523:32;3245:12033;12478:78;;;:::i;:::-;:82;:::i;:::-;12614:33;3245:12033;12614:33;;:::i;:::-;12702:19;;:29;3245:12033;;;;:::i;:::-;;;;12179:32;12669:76;;3245:12033;12657:88;;;:::i;:::-;;;;;:::i;:::-;;12810:14;;12769:75;;:::i;12858:6::-;12854:36;;11826:1071::o;12854:36::-;12873:17;;;-1:-1:-1;12873:17:118;;-1:-1:-1;12873:17:118;3245:12033;;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;-1:-1:-1;3245:12033:118;;-1:-1:-1;3245:12033:118;;;;-1:-1:-1;3245:12033:118;;;;-1:-1:-1;3245:12033:118;;;;;;:::o;:::-;;;;;;;:::i;:::-;;;;-1:-1:-1;3245:12033:118;;;;:::o;:::-;;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;-1:-1:-1;3245:12033:118;;;;;;;:::i;:::-;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;3245:12033:118;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;;;;2883:1495:119;3245:12033:118;;:::i;:::-;;;;:::i;:::-;;;;3019:1:119;3000:21;;;3245:12033:118;3052:38:119;3068:21;;;:::i;:::-;3052:38;:::i;:::-;3122:35;;;;:::i;:::-;3187:38;3203:21;;;:::i;3187:38::-;3260;3276:21;;;:::i;3260:38::-;3326:35;;;;:::i;:::-;3397:20;;;:::i;:::-;3433:13;3019:1;3448:10;;;;;;3245:12033:118;;;;;:::i;:::-;;;;3000:21:119;4304:67;;3245:12033:118;;4304:67:119;;3245:12033:118;4304:67:119;;;3245:12033:118;4304:67:119;;;3245:12033:118;2883:1495:119;:::o;3460:3::-;3245:12033:118;3492:21:119;;;;:::i;:::-;3245:12033:118;;;:::i;:::-;;;3565:25:119;;;-1:-1:-1;3245:12033:118;3610:14:119;;;1540:1;3610:21;4257:19;;;;:::i;:::-;;;;;;:::i;:::-;;3245:12033:118;3433:13:119;;3561:683;1361:1;3656:29;;1361:1;;-1:-1:-1;3245:12033:118;;3705:18:119;;1540:1;3767:23;;;:::i;:::-;3748:16;;;:42;3652:592;3561:683;;3652:592;1411:1;3815:24;;1411:1;;-1:-1:-1;3245:12033:118;3859:13:119;;;1540:1;3911:23;;;:::i;:::-;3897:11;;;:37;3561:683;;3811:433;1467:1;3959:30;;1467:1;;-1:-1:-1;3245:12033:118;1540:1:119;;4073:23;;;:::i;:::-;3000:21;4053:17;;:43;3561:683;;3955:289;1540:1;4121:47;3811:433;4117:127;3245:12033:118;4188:34:119;;;1540:1;3561:683;;9049:172:65;9158:56;3245:12033:118;9049:172:65;3245:12033:118;9175:31:65;3245:12033:118;;9175:31:65;:::i;:::-;3245:12033:118;;;9158:56:65;;;;;;3245:12033:118;;:::i;8762:967:123:-;3245:12033:118;;:::i;:::-;;-1:-1:-1;;8920:17:123;-1:-1:-1;;8993:3:123;8972:12;;;;;3245:12033:118;;8968:23:123;;;;;9016:27;:15;;:27;:15;;:::i;:::-;;:27;3245:12033:118;;;;;9016:27:123;:89;;;8993:3;9012:305;;8993:3;9335:27;9016;9335:15;:12;;;:15;:::i;:27::-;:89;;;8993:3;9331:196;;8993:3;-1:-1:-1;3245:12033:118;;8953:13:123;;9331:196;9481:12;;;9456:56;9481:30;:25;:15;8972:12;9481;3245:12033:118;9481:12:123;;:15;:::i;:::-;;:25;;:30;;9456:56;:::i;:::-;9331:196;;;;;9335:89;8237:14;;;;3245:12033:118;;;9366:37:123;:25;:15;:12;;;:15;:::i;:::-;;:25;;3245:12033:118;-1:-1:-1;;;;;;3245:12033:118;;;9366:37:123;3245:12033:118;9366:58:123;9335:89;;9012:305;9164:12;;;;9135:68;9151:51;9164:30;:25;:15;:12;;;:15;:::i;:::-;;:25;;:30;;9151:51;:::i;9135:68::-;9266:12;9237:65;9253:48;9164:30;:25;9266:15;:12;;;:15;:::i;:::-;;:25;;:30;;9253:48;:::i;9237:65::-;9012:305;;;9016:89;3245:12033:118;;;;;;;9047:37:123;:25;:15;:12;;;:15;:::i;:37::-;3245:12033:118;9047:58:123;9016:89;;8968:23;;;;;;;;9575:14;;9571:46;;3245:12033:118;;:::i;:::-;;;;9635:87:123;;;3245:12033:118;9016:27:123;9635:87;;3245:12033:118;8762:967:123;:::o;9571:46::-;9598:19;;;-1:-1:-1;9598:19:123;;-1:-1:-1;9598:19:123;10579:974;3245:12033:118;;:::i;:::-;10697:1:123;10680:867;10725:3;10704:12;;;;;3245:12033:118;;10700:23:123;;;;;10749:27;;:15;;10748:28;10749:15;;:::i;10748:28::-;10744:42;;3245:12033:118;;;10804:58:123;:37;:25;:15;:12;;;:15;:::i;:37::-;-1:-1:-1;;;;;;3245:12033:118;;;10804:58:123;;10800:72;;10804:25;10907:15;:12;:30;:12;;:15;:::i;:::-;;:25;;:30;;3245:12033:118;11162:2:123;3245:12033:118;;11147:17:123;11143:31;;11204:58;11211:50;11236:24;;;:::i;:::-;11211:50;:::i;11204:58::-;3245:12033:118;-1:-1:-1;;;;;3245:12033:118;;11280:10:123;11276:24;;11417;;;11322:214;11481:39;11497:22;11385:58;11392:50;11417:24;;;:::i;11385:58::-;11497:22;;:::i;11481:39::-;3245:12033:118;11322:214:123;3245:12033:118;;:::i;:::-;-1:-1:-1;;;;;3245:12033:118;;;8525:14:123;;;11322:214;3245:12033:118;;10907:30:123;11322:214;;3245:12033:118;;11322:214:123;10749:27;11322:214;;3245:12033:118;11315:221:123;:::o;11276:24::-;10704:12;11292:8;;;3245:12033:118;11292:8:123;;;10685:13;3245:12033:118;10685:13:123;;;;;11143:31;11166:8;;;3245:12033:118;10704:12:123;11166:8;;;10800:72;10864:8;;3245:12033:118;10704:12:123;10864:8;;;10700:23;;;;;;;10579:974::o;1990:238:56:-;;;;;3884:14;;3880:38;;3245:12033:118;;3995:18:56;;;:::i;:::-;4049;;;:::i;:::-;4106:20;4101:1;4106:20;;;:::i;:::-;3245:12033:118;-1:-1:-1;;4179:7:56;;;;;;4577:42;;;;;;;;;;:::i;:::-;2174:47;1990:238;:::o;4168:9::-;4217;;;;:::i;:::-;;3245:12033:118;4250:16:56;;;;;4246:51;;4315:6;;;:26;;;;4168:9;4311:55;;;4101:1;4392:9;:14;:9;;;;:::i;:::-;;:14;3245:12033:118;4380:26:56;;;;:::i;:::-;3245:12033:118;;;;4448:33:56;;;;:::i;:::-;3245:12033:118;;;4168:9:56;;;4311:55;4350:16;;;-1:-1:-1;4350:16:56;;-1:-1:-1;4350:16:56;4315:26;4325:16;;;;;4315:26;;;4246:51;4275:22;;;-1:-1:-1;4275:22:56;;-1:-1:-1;4275:22:56;3880:38;3907:11;;;-1:-1:-1;3907:11:56;;-1:-1:-1;3907:11:56;3245:12033:118;-1:-1:-1;;3245:12033:118;;;;;;;;:::o;:::-;-1:-1:-1;;3245:12033:118;;;;;;;;:::o;:::-;;;;;;;;;:::o;:::-;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;:::i;4484:686:65:-;4577:2;4573:6;;4577:2;;;4602:31;4619:13;4625:6;4602:31;4625:6;3245:12033:118;;;;4625:6:65;3245:12033:118;;;;4619:13:65;4577:2;3245:12033:118;;;;;;-1:-1:-1;;;;;;3245:12033:118;4602:31:65;;;3245:12033:118;;;;;;;;;4569:595:65;4658:7;4654:11;;4658:7;;;4688:51;4705:33;4715:22;4723:12;4724:6;4688:51;4724:6;3245:12033:118;;;;4723:12:65;3245:12033:118;;;;4715:22:65;3245:12033:118;;;8428:1:65;3245:12033:118;;;8428:1:65;3245:12033:118;;8422:19:65;8300:148;;4705:33;4577:2;3245:12033:118;;;;;;-1:-1:-1;;;;;;3245:12033:118;4688:51:65;;;3245:12033:118;;;;;;;;;4650:514:65;4764:7;4760:11;;4764:7;;;4794:51;4811:33;4821:22;4829:12;4830:6;4794:51;4830:6;3245:12033:118;;;;4830:6:65;4829:12;:::i;4821:22::-;3245:12033:118;8200:10:65;3245:12033:118;;;;;;;8195:21:65;8196:14;3245:12033:118;;;8168:49:65;3245:12033:118;;;;;;8279:7:65;3245:12033:118;;;8266:21:65;8046:248;;4811:33;4577:2;3245:12033:118;;;;;;-1:-1:-1;;;;;;3245:12033:118;4794:51:65;;;3245:12033:118;;;;;;;;;4756:408:65;4942:31;;4959:13;4902:85;4959:13;;:::i;:::-;4577:2;3245:12033:118;4942:31:65;;;;;;3245:12033:118;;;;;;;4942:31:65;4902:85;:::i;:::-;5117:36;5065:30;5071:23;5072:17;5073:10;3245:12033:118;;5073:10:65;:::i;:::-;3245:12033:118;;;;5072:17:65;5071:23;:::i;5065:30::-;5117:36;4577:2;3245:12033:118;5117:36:65;;;4942:31;5117:36;;;:::i;11118:367:118:-;;3245:12033;11220:13;3245:12033;11242:3;11239:1;11235:5;;;;;11282:1;11277:6;;11282:1;;11286:2;11277:17;11337:25;11346:1;3245:12033;;11239:1;3245:12033;;;;;;;11337:25;:::i;:::-;11323:40;11377:92;11384:9;;;11242:3;11346:1;3245:12033;11220:13;;11377:92;11421:8;;;:::i;:::-;11413:16;;-1:-1:-1;;3245:12033:118;;;;;;;;11377:92;;11277:17;11291:3;11277:17;;11235:5;;;11118:367::o;11577:156::-;;11691:9;;;;;:35;;;;11577:156;11684:42;;;11577:156;:::o;11691:35::-;3245:12033;;;;11713:1;3245:12033;;;;;;11713:1;3245:12033;;;;;;;;;;;;;11725:1;3245:12033;;;;;11704:22;11691:35;;;;;3245:12033;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;:::i;:::-;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;;;;;;;;;:::i;:::-;;;;:::i;:::-;;;;;;;;;;;;:::i;2339:494:119:-;2788:28;2730:44;2468:30;2442:384;2339:494;2468:30;3245:12033:118;;;;;;;;;;;;;;;-1:-1:-1;;;;;;3245:12033:118;2468:30:119;;;3245:12033:118;;;;;;;;;2468:30:119;2564:33;2512:38;3245:12033:118;2468:30:119;2532:17;;3245:12033:118;;;;;2512:38:119;2564:33;3245:12033:118;2581:15:119;;3245:12033:118;;;2564:33:119;;;2468:30;2564:33;;3245:12033:118;;;;;;;2564:33:119;2631:21;;;;3245:12033:118;2788:28:119;2805:10;2611:45;3245:12033:118;;;-1:-1:-1;;;;;3245:12033:118;;;2611:45:119;2690:25;2730:44;3245:12033:118;2670:46:119;3245:12033:118;2468:30:119;2690:25;;3245:12033:118;;;;;2670:46:119;2747:26;;3245:12033:118;;;2730:44:119;;;2468:30;2730:44;;3245:12033:118;;;;;;;2730:44:119;;3245:12033:118;;2730:44:119;;;;;;:::i;:::-;2805:10;3245:12033:118;;;2788:28:119;;;2468:30;2788:28;;3245:12033:118;;;;;;;2788:28:119;;3245:12033:118;;2788:28:119;;;;;;:::i;:::-;2442:384;:::i;14804:190:118:-;14919:20;;;:68;14804:190;:::o;14919:68::-;3245:12033;;;;;;;14804:190;:::o;3366:228:55:-;;;;3548:39;3366:228;3548:39;:::i;2089:399:58:-;3000:21:119;2216:11:58;;3245:12033:118;;;3000:21:119;3245:12033:118;;;;;;;2237:9:58;;3245:12033:118;-1:-1:-1;3245:12033:118;;3000:21:119;2264:48:58;2351:9;3245:12033:118;;;;;;1938:75:59;;;;3245:12033:118;;;3000:21:119;2392:39:58;;;:::i;:::-;3245:12033:118;;;;3000:21:119;3245:12033:118;;;;;;;;2089:399:58;:::o;:::-;;2216:11;;;3245:12033:118;;;;;;;;;;;2237:9:58;;3245:12033:118;-1:-1:-1;3245:12033:118;;2268:8:58;;2264:48;;2216:11;2351:9;;3245:12033:118;;;;;;1938:75:59;;;;3245:12033:118;;;2392:39:58;;;;:::i;:::-;3245:12033:118;;;;;;;;;;;;2089:399:58;:::o;2264:48::-;3245:12033:118;;;;;;;;;:::i;:::-;-1:-1:-1;3245:12033:118;;2292:9:58;:::o;3245:12033:118:-;;;;;;;;;;;;:::o;:::-;;;;:::o;:::-;;;-1:-1:-1;;;3245:12033:118;;;;;;;;;;;;;;;;;-1:-1:-1;;;3245:12033:118;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;:::o;1205:1510:65:-;1323:20;;;:::i;:::-;3245:12033:118;;;;1453:9:65;;-1:-1:-1;1501:14:65;;-1:-1:-1;3245:12033:118;;;;1509:6:65;3245:12033:118;1449:1238:65;1579:1;1571:9;;1579:1;;1634:20;1782:11;1634:20;1782:11;1787:6;1740:7;1692:13;1634:20;1842:9;1634:20;;:::i;1692:13::-;3245:12033:118;;;;;;1740:7:65;1787:6;3245:12033:118;;;;;;1782:11:65;;;:::i;1567:1120::-;1880:1;1872:9;;1880:1;;1943:20;2275:8;1943:20;2243:16;3245:12033:118;1943:20:65;2194:16;1943:20;;2336:11;1943:20;;:::i;:::-;2013;;2118:15;2013:20;2058;2013;;;:::i;:::-;2058;;:::i;:::-;3245:12033:118;;;;;;;;2118:15:65;3245:12033:118;;2105:29:65;;3245:12033:118;;;;;;;2194:16:65;2188:23;;3245:12033:118;;;;;;;2243:16:65;2237:23;3245:12033:118;;;;;;1868:819:65;2376:1;2368:9;2376:1;;2575:34;2450:6;3245:12033:118;2449:12:65;2450:6;2589:19;2450:6;3245:12033:118;;;;;;;2450:6:65;2449:12;:::i;:::-;3245:12033:118;2503:6:65;2495:59;2508:1;2503:6;;;2495:59;:::i;:::-;2589:19;:::i;2364:323::-;3245:12033:118;;-1:-1:-1;;;2640:36:65;;3245:12033:118;1393:1:65;2640:36;;3245:12033:118;;;;;;;;;;;;;2640:36:65;3245:12033:118;;;;;;;;;;;;;:::o;1564:269:58:-;1649:11;;;3245:12033:118;;;1663:1:58;3245:12033:118;;;;;;;1667:9:58;;3245:12033:118;-1:-1:-1;1645:87:58;;1758:9;3245:12033:118;;-1:-1:-1;;;;;;3245:12033:118;1758:22:58;;3245:12033:118;1758:22:58;:::i;:::-;3245:12033:118;;;;;1791:16:58;3245:12033:118;;1791:16:58;:::i;:::-;3245:12033:118;;1564:269:58;:::o;1645:87::-;3245:12033:118;;-1:-1:-1;;;1699:22:58;;1649:11;1699:22;;;3245:12033:118;;;;;;-1:-1:-1;;;3245:12033:118;;;;1699:22:58;;;4479:308:119;3245:12033:118;;:::i;:::-;;2216:11:58;;;3245:12033:118;;;4620:1:119;3245:12033:118;;;;;;;2237:9:58;;3245:12033:118;-1:-1:-1;3245:12033:118;;2216:11:58;2351:9;;3245:12033:118;;;;;;1938:75:59;;;;3245:12033:118;;;4620:1:119;2392:39:58;;;:::i;:::-;3245:12033:118;;;;4620:1:119;3245:12033:118;;;;;;;;4624:1:119;5413:13:58;4624:1:119;5408:106:58;5428:5;4620:1:119;5428:5:58;;;;4653:35:119;;;;4718:25;4653:35;;:::i;:::-;4718:25;;:::i;:::-;4760:20;3245:12033:118;;:::i;:::-;-1:-1:-1;;;;;;3245:12033:118;;;;;;4760:20:119;2216:11:58;4760:20:119;;3245:12033:118;4479:308:119;:::o;5435:3:58:-;3245:12033:118;;-1:-1:-1;;;;;;5468:16:58;3245:12033:118;;5468:16:58;:::i;:::-;3245:12033:118;;;;;;;;;;5501:1:58;3245:12033:118;;;;;;;;;-1:-1:-1;;;;;;3245:12033:118;;;;;;5454:49:58;5435:3;3245:12033:118;5413:13:58;;;3313:349;3245:12033:118;;9164:30:123;3466:31:58;3245:12033:118;;9164:30:123;3512:8:58;3617:38;3512:8;1938:75:59;3617:38:58;:::i;3313:349::-;3245:12033:118;;11258:1:123;3466:31:58;3245:12033:118;;11258:1:123;1938:75:59;3617:38:58;3512:8;1938:75:59;3617:38:58;:::i;3313:349::-;3245:12033:118;;;3466:31:58;3245:12033:118;;;;;;1938:75:59;;3245:12033:118;-1:-1:-1;3245:12033:118;;11439:1:123;3617:38:58;;;:::i;3313:349::-;;3245:12033:118;;3466:31:58;;3245:12033:118;;3512:8:58;;3508:48;;1938:75:59;3617:38:58;3245:12033:118;1938:75:59;3617:38:58;:::i;3508:48::-;3245:12033:118;;;;;;;;:::i;2744:313:58:-;3245:12033:118;;2876:25:58;9164:30:123;2876:25:58;3245:12033:118;;-1:-1:-1;;3245:12033:118;;;;;;;;;;;9164:30:123;1938:75:59;3245:12033:118;-1:-1:-1;3245:12033:118;;3012:38:58;;;:::i;2744:313::-;3245:12033:118;;2876:25:58;11516:2:123;2876:25:58;3245:12033:118;;-1:-1:-1;;3245:12033:118;;;;;;;;;;;1938:75:59;;3245:12033:118;-1:-1:-1;3245:12033:118;;3012:38:58;;;:::i;3245:12033:118:-;;;;;-1:-1:-1;;3245:12033:118;;:::o;823:320:65:-;3245:12033:118;;;;;961:5:65;;;1123:13;;823:320;:::o;968:3::-;3245:12033:118;;-1:-1:-1;;3245:12033:118;;;;;;;;1051:11:65;;;;:::i;:::-;3245:12033:118;;;;;;;;;-1:-1:-1;;;;;3245:12033:118;;;;;;;;;;1060:1:65;3245:12033:118;;1037:66:65;3245:12033:118;;;;;;;;;;;;;;968:3:65;1012:91;;;;:::i;:::-;968:3;;:::i;:::-;936:23;;;;3245:12033:118;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;:::o;5627:1354:65:-;3245:12033:118;5873:1:65;3245:12033:118;;5873:1:65;3245:12033:118;6063:110:65;6191:86;;6064;;;;;;;3245:12033:118;;6190:110:65;6191:86;;;;3245:12033:118;;6062:239:65;6511:66;6384;6364:86;;3245:12033:118;;6490:110:65;6491:86;3245:12033:118;;6362:239:65;6811:66;6684;;6664:86;;3245:12033:118;;6790:110:65;6791:86;3245:12033:118;;6662:239:65;6965:8;3245:12033:118;;;6965:8:65;3245:12033:118;;;;6965:8:65;6951:23;5627:1354;:::o;6202:380:58:-;3245:12033:118;;-1:-1:-1;;3245:12033:118;;;;;;;6414:3:58;-1:-1:-1;;;;;;6437:7:58;;;;:::i;:::-;3245:12033:118;;6433:86:58;;6414:3;;;:::i;:::-;6382:22;;6433:86;6403:1;3245:12033:118;;;;;;;6546:29:58;;;:::i;4448:2801:55:-;;;;4610:14;;4606:38;;3245:12033:118;;4702:14:55;;;4698:40;;4810:1;4813:13;;;;;;5012:14;4810:1;5012:14;;:32;;;4793:159;5012:56;;;4793:159;5008:169;;5235:35;5249:20;;;:::i;:::-;5235:35;:::i;:::-;3245:12033:118;;;:::i;:::-;;;;;5219:52:55;;;3245:12033:118;;;;;:::i;:::-;;;;;5219:52:55;5313:22;;3245:12033:118;;;;;:::i;:::-;;;;;5219:52:55;5376:34;;3245:12033:118;;;;5376:34:55;;3245:12033:118;;5455:29:55;5494:940;5501:14;;;5494:940;3245:12033:118;;;;6490:52:55;;3245:12033:118;6642:14:55;;3245:12033:118;6622:41:55;6618:71;;3245:12033:118;;-1:-1:-1;;3245:12033:118;;;6763:445:55;3245:12033:118;;6770:21:55;;;6823:20;;;:::i;:::-;6872;;;:::i;:::-;3245:12033:118;6934:18:55;3245:12033:118;;;;;;6934:18:55;3245:12033:118;;;7006:139:55;5219:52;7006:139;3245:12033:118;;7006:139:55;7158:39;:14;;3245:12033:118;;7158:39:55;;:::i;:::-;3245:12033:118;6763:445:55;;6770:21;;7225:17;6770:21;;7225:14;:17;:::i;:::-;3245:12033:118;4448:2801:55;:::o;6618:71::-;5114:17;;;3245:12033:118;6672:17:55;;3245:12033:118;6672:17:55;6490:52;6523:19;;;3245:12033:118;6523:19:55;;3245:12033:118;6523:19:55;5494:940;5548:16;;;:::i;:::-;3245:12033:118;5663:31:55;5625:24;4810:1;3245:12033:118;;5625:24:55;;;:::i;:::-;5663:31;;;;:::i;:::-;5745:42;;;;;;;:::i;:::-;5806:20;5219:52;5806:20;;3245:12033:118;5806:25:55;;5802:622;5806:25;;;5855:14;;;;;;;;3245:12033:118;;;5855:41:55;5851:174;5880:16;5920:5;5880:16;5989;;;;;:::i;:::-;;;:::i;:::-;5494:940;;5802:622;4810:1;6049:25;:40;;;5802:622;6045:379;;;6126:18;;;5219:52;6126:40;:18;3245:12033:118;6126:45:55;:18;;;3245:12033:118;;6126:40:55;;:::i;:::-;;:45;3245:12033:118;6126:45:55;;:::i;6045:379::-;3245:12033:118;6353:55:55;3245:12033:118;;6282:1:55;6353:55;3245:12033:118;;;6353:55:55;;:::i;6049:40::-;6078:11;;;6049:40;;5008:169;3245:12033:118;;;;;;5084:47:55;;5152:9;:14;:9;;:::i;:::-;;:14;3245:12033:118;5145:21:55;:::o;5012:56::-;5048:9;;;;:::i;:::-;;3245:12033:118;5048:20:55;5012:56;;:32;5030:14;4810:1;5030:14;;5012:32;;4798:13;4847:9;;;;:::i;:::-;;3245:12033:118;4866:13:55;4873:5;;;:::i;:::-;4866:13;;:::i;:::-;;3245:12033:118;-1:-1:-1;4847:38:55;4843:67;;4810:1;3245:12033:118;4798:13:55;;4698:40;4725:13;;;3245:12033:118;4725:13:55;;3245:12033:118;4725:13:55;2284:287:59;;;;3245:12033:118;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;-1:-1:-1;;3245:12033:118;;;;2560:3:59;;;3244:193;3251:16;3245:12033:118;3251:16:59;;;;;3494:8;;;-1:-1:-1;;;3245:12033:118;3494:132:59;3636:173;;;;;;;;;;;2284:287::o;3494:132::-;3598:24;3606:15;3598:28;3606:15;;:::i;:::-;3598:24;:::i;:::-;:28;:::i;:::-;3494:132;;;3269:16;3301:65;;;;;;3245:12033:118;;;;;;;;;;;;;;;;;3269:16:59;-1:-1:-1;;3245:12033:118;;;;;;3244:193:59;3245:12033:118;;:::i;7197:131:56:-;7277:1;7272:6;;;7268:20;;-1:-1:-1;;3245:12033:118;;;;;;;7305:12:56;;;:::i;:::-;7277:1;3245:12033:118;;;;;;;7197:131:56;:::o;7268:20::-;7280:8;3245:12033:118;7280:8:56;:::o;5056:1349::-;;;;3245:12033:118;;;5309:1063:56;5332:1;5316:12;;;:::i;:::-;3245:12033:118;5316:17:56;;;5369:45;:41;5381:12;5332:1;5375:19;5381:12;;;:::i;:::-;3245:12033:118;5375:19:56;:::i;:::-;3245:12033:118;5369:41:56;:::i;:45::-;5428:9;3245:12033:118;5457:9:56;3245:12033:118;5452:836:56;5468:7;;;;;;6302;;;6339:16;6338:23;6302:7;6339:16;;:::i;:::-;3245:12033:118;;;;6338:23:56;5309:1063;;;5457:9;5510:12;;;;;;;:::i;:::-;3245:12033:118;5563:5:56;;;;:::i;:::-;:11;:44;;;5457:9;5719:442;;;5786:9;6230:8;5332:1;5786:9;5850:1;5771:40;5786:9;;;;:::i;:::-;3245:12033:118;5797:13:56;5804:5;;;:::i;:::-;5797:13;;:::i;:::-;3245:12033:118;5771:40:56;;;:::i;:::-;3245:12033:118;;5719:442:56;;6179:18;;;;:::i;:::-;3245:12033:118;;;;;6230:8:56;6215:23;;;;:::i;:::-;3245:12033:118;;5457:9:56;;;;;5719:442;5647:7;;5332:1;5647:7;;5646:22;-1:-1:-1;5646:22:56;;5943:9;5332:1;5943:9;5928:35;6230:8;5943:9;5954:8;5943:9;;;;;;;;;:::i;:::-;3245:12033:118;5954:8:56;;:::i;:::-;3245:12033:118;5928:35:56;;;:::i;:::-;3245:12033:118;;;;5878:283:56;5719:442;;5878:283;6230:8;6063:9;;;5332:1;6063:9;;;;;;:::i;:::-;3245:12033:118;;;5878:283:56;5719:442;;5563:44;5588:5;5578:16;5588:5;;;:::i;:::-;5578:16;;:::i;:::-;3245:12033:118;5332:1:56;5599:7;;5578:29;5563:44;;5316:17;;;;;;6389:9;5316:17;;6389:9;:::i;13095:196:55:-;;13147:13;3245:12033:118;13172:113:55;13179:6;;;13095:196;:::o;13172:113::-;3245:12033:118;-1:-1:-1;;3245:12033:118;;;;;;;13210:1:55;13201:10;;3245:12033:118;;;;13172:113:55;;;13353:537;13422:462;;;;;;;;-1:-1:-1;;;;;13422:462:55;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;13353:537;:::o;3245:12033:118:-;;;;;;;:::i;:::-;;;;-1:-1:-1;3245:12033:118;;-1:-1:-1;3245:12033:118;;;;;;:::o;7670:742:55:-;3245:12033:118;;:::i;:::-;;;;7846:33:55;7864:15;;;3245:12033:118;;;7846:33:55;;:::i;:::-;3245:12033:118;;;7935:210:55;7942:15;;;;;;7935:210;3245:12033:118;;;;8175:27:55;8348:28;3245:12033:118;;;8175:27:55;;:::i;:::-;3245:12033:118;;;8287:13:55;7998;8287;;;3245:12033:118;;:::i;:::-;;;;8246:55:55;7864:15;8246:55;;3245:12033:118;7998:13:55;8246:55;;3245:12033:118;;;;8348:28:55;:::i;7935:210::-;7998:13;:24;:13;;;;;:24;:::i;:::-;;3245:12033:118;7978:50:55;;;7974:94;;3245:12033:118;;7935:210:55;;;7974:94;8048:5;;;9445:1927;;;9640:15;;;3245:12033:118;;;9737:21:55;;;;:::i;:::-;9794;;;;:::i;:::-;9830:9;3245:12033:118;9841:10:55;;;;;;10071:20;;;;10182:1157;;3245:12033:118;10189:12:55;;;:::i;:::-;3245:12033:118;10189:17:55;;;3245:12033:118;10248:9:55;3245:12033:118;10272:851:55;10279:7;;;;;;11204:10;;;11228:101;;;;;10182:1157;;10272:851;10320:12;;;;;;:::i;:::-;3245:12033:118;10355:5:55;;;;;;:::i;:::-;:11;:44;;;10272:851;10351:758;;;3245:12033:118;10496:9:55;10481:40;10496:9;10507:13;10514:5;10496:9;10677:6;10496:9;;10561:8;10496:9;;:::i;:::-;3245:12033:118;10514:5:55;;:::i;10507:13::-;3245:12033:118;10481:40:55;;;:::i;:::-;10466:55;;;;:::i;10561:8::-;10543:26;;;;:::i;:::-;3245:12033:118;;10677:6:55;;:::i;:::-;10351:758;10272:851;;10351:758;3245:12033:118;;;;9640:15:55;10801:14;;;3245:12033:118;-1:-1:-1;10781:41:55;10777:70;;3245:12033:118;10899:9:55;10967:8;10899:9;;10884:43;10899:9;;;;:::i;:::-;3245:12033:118;10910:16:55;;;:::i;:::-;10884:43;;;:::i;10967:8::-;10949:26;;;;:::i;:::-;3245:12033:118;;;;10351:758:55;10272:851;;10777:70;10831:16;;;3245:12033:118;10831:16:55;;3245:12033:118;10831:16:55;10355:44;10380:5;10370:16;10380:5;;;:::i;:::-;10370:16;;:::i;:::-;3245:12033:118;;10391:7:55;;10370:29;10355:44;;10189:17;;;;;11356:9;10189:17;11356:9;:::i;9830:::-;3245:12033:118;9880:13:55;9985:25;9880:13;;;9640:15;9880:25;:13;;9894:10;;;;:::i;:::-;9880:25;;:::i;:::-;;:30;3245:12033:118;9868:42:55;;;;:::i;:::-;3245:12033:118;9985:13:55;3245:12033:118;;;9985:25:55;;:::i;:::-;;3245:12033:118;;;9952:64:55;;;;:::i;:::-;3245:12033:118;;9830:9:55;;11444:188;;11527:37;:13;;;;3245:12033:118;;11527:37:55;;:::i;:::-;3245:12033:118;;;;;;;11444:188:55:o;11706:222::-;11808:30;:13;;;;3245:12033:118;;11808:30:55;;:::i;:::-;3245:12033:118;;;;;;;;11706:222:55;:::o;12000:226::-;12106:30;:13;;;;3245:12033:118;;12106:30:55;;:::i;:::-;3245:12033:118;;;-1:-1:-1;;3245:12033:118;;;;;12000:226:55:o;713:2:59:-;;;;;;;;;:::o;6686:471:56:-;6806:1;6800:7;6806:1;;3245:12033:118;6829:141:56;;;;3245:12033:118;6829:141:56;6795:356;6686:471::o;6795:356::-;7000:141;3245:12033:118;7000:141:56;;;;3245:12033:118;7000:141:56;6795:356;6686:471::o","linkReferences":{},"immutableReferences":{"76100":[{"start":2214,"length":32},{"start":5547,"length":32}],"76103":[{"start":2289,"length":32},{"start":3207,"length":32}]}},"methodIdentifiers":{"MMR_ROOT_PAYLOAD_ID()":"af8b91d6","_apk()":"afb5670a","_digestParaId()":"e455995b","noOp((uint256,uint256,(uint256,uint256,uint256,bytes32),(uint256,uint256,uint256,bytes32)),((((bytes2,bytes)[],uint32,uint64),uint256[5],bytes32[3],bytes32[6],bytes,bytes32[3],(uint8,uint32,bytes32,(uint64,uint32,bytes32),bytes32,uint256),bytes32[]),((uint256,uint256,bytes)[],bytes32[],uint256)))":"42a947b1","supportsInterface(bytes4)":"01ffc9a7","verify(bytes,bytes)":"f7e83aee"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"apkProof\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"digestParaId\",\"type\":\"uint32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"EmptyLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyTree\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyTree\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAggregateProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidMmrProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidParachainHeaderProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"LeafIndexOutOfBounds\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MissingApkCommitment\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MmrRootHashMissing\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OutOfBoundsLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ProofExhausted\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SuperMajorityRequired\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"TimestampNotFound\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnconsumedProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnknownAuthoritySet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnsortedLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnsortedLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MMR_ROOT_PAYLOAD_ID\",\"outputs\":[{\"internalType\":\"bytes2\",\"name\":\"\",\"type\":\"bytes2\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"_apk\",\"outputs\":[{\"internalType\":\"contract IApkProof\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"_digestParaId\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"latestHeight\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"beefyActivationBlock\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"len\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"blsPoseidonHash\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"ecdsaMerkleRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct AuthoritySet\",\"name\":\"currentAuthoritySet\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"len\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"blsPoseidonHash\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"ecdsaMerkleRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct AuthoritySet\",\"name\":\"nextAuthoritySet\",\"type\":\"tuple\"}],\"internalType\":\"struct BeefyConsensusState\",\"name\":\"s\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"components\":[{\"components\":[{\"internalType\":\"bytes2\",\"name\":\"id\",\"type\":\"bytes2\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"internalType\":\"struct Payload[]\",\"name\":\"payload\",\"type\":\"tuple[]\"},{\"internalType\":\"uint32\",\"name\":\"blockNumber\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"validatorSetId\",\"type\":\"uint64\"}],\"internalType\":\"struct Commitment\",\"name\":\"commitment\",\"type\":\"tuple\"},{\"internalType\":\"uint256[5]\",\"name\":\"bitlist\",\"type\":\"uint256[5]\"},{\"internalType\":\"bytes32[3]\",\"name\":\"apk\",\"type\":\"bytes32[3]\"},{\"internalType\":\"bytes32[6]\",\"name\":\"apk2\",\"type\":\"bytes32[6]\"},{\"internalType\":\"bytes\",\"name\":\"apkProof\",\"type\":\"bytes\"},{\"internalType\":\"bytes32[3]\",\"name\":\"signature\",\"type\":\"bytes32[3]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"},{\"internalType\":\"uint32\",\"name\":\"parentNumber\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"parentHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"uint64\",\"name\":\"id\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"len\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"internalType\":\"struct AuthoritySetCommitment\",\"name\":\"nextAuthoritySet\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"extra\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"leafIndex\",\"type\":\"uint256\"}],\"internalType\":\"struct BeefyMmrLeaf\",\"name\":\"latestMmrLeaf\",\"type\":\"tuple\"},{\"internalType\":\"bytes32[]\",\"name\":\"mmrProof\",\"type\":\"bytes32[]\"}],\"internalType\":\"struct BlsApkRelayChainProof\",\"name\":\"relay\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"header\",\"type\":\"bytes\"}],\"internalType\":\"struct Parachain[]\",\"name\":\"parachains\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes32[]\",\"name\":\"proof\",\"type\":\"bytes32[]\"},{\"internalType\":\"uint256\",\"name\":\"leafCount\",\"type\":\"uint256\"}],\"internalType\":\"struct ParachainProof\",\"name\":\"parachain\",\"type\":\"tuple\"}],\"internalType\":\"struct BlsApkBeefyConsensusProof\",\"name\":\"p\",\"type\":\"tuple\"}],\"name\":\"noOp\",\"outputs\":[],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"previousState\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"proof\",\"type\":\"bytes\"}],\"name\":\"verify\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"stateMachineId\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"height\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"overlayRoot\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"stateRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct StateCommitment\",\"name\":\"commitment\",\"type\":\"tuple\"}],\"internalType\":\"struct IntermediateState[]\",\"name\":\"\",\"type\":\"tuple[]\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Naming each signer and proving their public key against the authority set's keyset root costs roughly 17k gas per signer, for the G2 addition and the compression needed to rebuild the leaf. At a few hundred validators that dominates everything else. Here a SNARK does that work instead. The prover shows that an aggregate public key corresponds to exactly the validators named in a bitlist, against a commitment to the whole set, and the contract checks one proof and one pairing. Nothing in the calldata or the verification grows with the number of signers. The commitment the proof is checked against does not come from the relay chain's MMR leaf, the way the keyset root does. Hyperbridge computes it over the relay's next authority set and publishes it in a header digest, so a client picks it up from a header it has already verified and carries it in its consensus state. That is what `AuthoritySet.blsPoseidonHash` holds, and why the state has to be seeded with the starting set's commitment at initialisation. Requires Prague for the EIP-2537 precompiles.\",\"kind\":\"dev\",\"methods\":{\"noOp((uint256,uint256,(uint256,uint256,uint256,bytes32),(uint256,uint256,uint256,bytes32)),((((bytes2,bytes)[],uint32,uint64),uint256[5],bytes32[3],bytes32[6],bytes,bytes32[3],(uint8,uint32,bytes32,(uint64,uint32,bytes32),bytes32,uint256),bytes32[]),((uint256,uint256,bytes)[],bytes32[],uint256)))\":{\"details\":\"Only here so the structs appear in the ABI, which is what the Rust bindings are generated from. `verify` takes bytes, so without this they would be invisible.\"},\"supportsInterface(bytes4)\":{\"details\":\"Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section] to learn more about how these ids are created. This function call must use less than 30 000 gas.\"},\"verify(bytes,bytes)\":{\"details\":\"IConsensusV2 entry point.\"}},\"title\":\"BEEFY consensus verified by an aggregate public key proof\",\"version\":1},\"userdoc\":{\"errors\":{\"InvalidAggregateProof()\":[{\"notice\":\"The APK proof or the aggregate signature did not verify.\"}],\"InvalidMmrProof()\":[{\"notice\":\"The mmr leaf was not in the tree the commitment attests to.\"}],\"InvalidParachainHeaderProof()\":[{\"notice\":\"A parachain header was not in the heads root.\"}],\"MissingApkCommitment()\":[{\"notice\":\"The authority set has no APK commitment yet, so no proof against it can be checked.\"}],\"MmrRootHashMissing()\":[{\"notice\":\"The commitment carried no mmr root payload.\"}],\"SuperMajorityRequired()\":[{\"notice\":\"Fewer than two thirds of the set signed.\"}],\"UnknownAuthoritySet()\":[{\"notice\":\"The commitment was signed by a set this client does not know.\"}]},\"kind\":\"user\",\"methods\":{\"MMR_ROOT_PAYLOAD_ID()\":{\"notice\":\"The payload id for the mmr root in a BEEFY commitment, \\\"mh\\\"\"},\"_apk()\":{\"notice\":\"The APK proof verifier, holding the circuit's verifying key.\"},\"_digestParaId()\":{\"notice\":\"The parachain whose header digests carry apk commitments, which is hyperbridge. Every parachain in a proof is proven against the heads root, so a digest from any of them is authentic; only this one's says anything about the relay chain's authorities.\"}},\"notice\":\"Verifies BEEFY finality without touching individual validator keys.\",\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/consensus/BlsBeefy.sol\":\"BlsBeefy\"},\"evmVersion\":\"prague\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@gnark-apk-proofs/=lib/gnark-apk-proofs/solidity/contracts/\",\":@hyperbridge/core/=node_modules/@hyperbridge/core/contracts/\",\":@openzeppelin/=node_modules/@openzeppelin/\",\":@polytope-labs/=node_modules/@polytope-labs/\",\":@sp1-contracts/=lib/sp1-contracts/contracts/src/\",\":@uniswap/=node_modules/@uniswap/\",\":ds-test/=lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=node_modules/forge-std/src/\",\":gnark-apk-proofs/=lib/gnark-apk-proofs/\",\":openzeppelin-contracts/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/\",\":solidity-stringutils/=lib/solidity-stringutils/\",\":sp1-contracts/=lib/sp1-contracts/contracts/\",\":stringutils/=lib/solidity-stringutils/src/\"],\"viaIR\":true},\"sources\":{\"node_modules/@hyperbridge/core/contracts/interfaces/IConsensusV2.sol\":{\"keccak256\":\"0x71dcb5168f8f0f95effac221bdc49e0f662011c1bf86a9369cc0db183b8ac4c3\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://d84388af7b50f5f31110fe3d711930c816432f8c2ebcc7417639a727760544ff\",\"dweb:/ipfs/QmWp89jgUMhCqrGVDphLAEhJm5kPP6q1y9yid3D2a3JFZ9\"]},\"node_modules/@openzeppelin/contracts/utils/introspection/ERC165.sol\":{\"keccak256\":\"0x2d9dc2fe26180f74c11c13663647d38e259e45f95eb88f57b61d2160b0109d3e\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://81233d1f98060113d9922180bb0f14f8335856fe9f339134b09335e9f678c377\",\"dweb:/ipfs/QmWh6R35SarhAn4z2wH8SU456jJSYL2FgucfTFgbHJJN4E\"]},\"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol\":{\"keccak256\":\"0x8891738ffe910f0cf2da09566928589bf5d63f4524dd734fd9cedbac3274dd5c\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://971f954442df5c2ef5b5ebf1eb245d7105d9fbacc7386ee5c796df1d45b21617\",\"dweb:/ipfs/QmadRjHbkicwqwwh61raUEapaVEtaLMcYbQZWs9gUkgj3u\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMountainRange.sol\":{\"keccak256\":\"0x014237038bb77bdf371b50c1268d02bf7eeaf7068c35483d5d0684ca7c30d544\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://beb3ae60094f49716a2b0cfa6229ee27af1778934cf5f2dfc704433fda5a687b\",\"dweb:/ipfs/QmQ339wEFT9X1woHyFTDbbhUCx8ekD9MGLRnhS3PpYp1pm\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMultiProof.sol\":{\"keccak256\":\"0xd4f6e6a9eceaa7d1cdf9684bfe7f3f552adf21dfcf7f1372f7943a4c2f15deb7\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://ec900b3e79ea8ef9d275697937d1984e994d10117aa2be066bd73f6a31aedf10\",\"dweb:/ipfs/QmZQGLiJtbvUSEW5H2HEenzxp58fN43XHRY3SoNTC9Hobg\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Bytes.sol\":{\"keccak256\":\"0xd305383358b93285d8fcee512795487484eadfd7b602df16ff4e9b01afbefec7\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://57c86cd2fe6ab591264c5632d503d77f04748d722e4defa9c2badb238ee1f9bd\",\"dweb:/ipfs/QmTZ6xTcaYkRBshq6YZVMx2kkk94ZtJsRM6xLpC5qWT3oA\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Memory.sol\":{\"keccak256\":\"0x59e3a56caa42c1aac30231173439817d38c7f359e40bd36e9bb418d3f82ceab7\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://e060fed46c6b420624166ea02a326f0f566897941bdc322257e690ae134b8179\",\"dweb:/ipfs/QmbXW8yG2ZntjLMyUEmQGcRZroZj4dVZkBhHxK2PFKfUKB\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Node.sol\":{\"keccak256\":\"0xca611969a68f7fe63dcdc742c9caf9bc1b26495561b68f4676c209279a4576ba\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://17b7ec2cf65d484f0a2c188a7ae33430b015b3a55bf10393167f53839e2dde56\",\"dweb:/ipfs/QmTQbT8J7rBRNHFXSR7S4KbpwCteMrFt8mvrzCD1vYYTwX\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/polkadot/ScaleCodec.sol\":{\"keccak256\":\"0x9ac4df46e68718f7deaaa5b7443778533f53dc0ff3736cc386cf4991099da2aa\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://85d08b00d9173358323105be975ce85b7e206ba52df2d80a14d2581dec1ddd11\",\"dweb:/ipfs/QmQCqVvSdJUqfhLH8TRBGNDGiEzkGduhRacnYhoKcm7e2a\"]},\"src/consensus/BlsBeefy.sol\":{\"keccak256\":\"0x15657e2fd4f84139b1f018b0bb0ed83b8b75cc7259654d061f9f7c59838cf73d\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://85d7bebe51acb750406079fa98fa4b6fa1945afc18a3291926cb80940b1523cc\",\"dweb:/ipfs/QmNSwQ4bxgrp9cEdS8Lp4V82Etc4rFPCd5cYKgUBfggXTb\"]},\"src/consensus/Codec.sol\":{\"keccak256\":\"0x477b62396db5a5c1d89d0388b0724dde80e0a0423b89c07f7b3b5622ff03b5d4\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://6a584c6500ae347c6f9d76d06a96757b024dcb892601c6d9c1a945b9d76811d6\",\"dweb:/ipfs/QmduKRb2Zr9eqeZbi5TFRw5Dyig3XVY3rMvPkxLktLFYrN\"]},\"src/consensus/Types.sol\":{\"keccak256\":\"0xe4b169eefb4afd0f83f09335f4460705fe96e7736314c2030235720cf976e76f\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://4ad510e26689491bcba165fd73c08e1b80302148804e27a184861712d966540b\",\"dweb:/ipfs/QmVuaZ3yB3eBkHRh2DS6Cy1E6B3oige6fDtvCrcYssc3qi\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"address","name":"apkProof","type":"address"},{"internalType":"uint32","name":"digestParaId","type":"uint32"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"type":"error","name":"EmptyLeaves"},{"inputs":[],"type":"error","name":"EmptyTree"},{"inputs":[],"type":"error","name":"EmptyTree"},{"inputs":[],"type":"error","name":"InvalidAggregateProof"},{"inputs":[],"type":"error","name":"InvalidMmrProof"},{"inputs":[],"type":"error","name":"InvalidParachainHeaderProof"},{"inputs":[],"type":"error","name":"LeafIndexOutOfBounds"},{"inputs":[],"type":"error","name":"MissingApkCommitment"},{"inputs":[],"type":"error","name":"MmrRootHashMissing"},{"inputs":[],"type":"error","name":"OutOfBoundsLeaves"},{"inputs":[],"type":"error","name":"ProofExhausted"},{"inputs":[],"type":"error","name":"SuperMajorityRequired"},{"inputs":[],"type":"error","name":"TimestampNotFound"},{"inputs":[],"type":"error","name":"UnconsumedProof"},{"inputs":[],"type":"error","name":"UnknownAuthoritySet"},{"inputs":[],"type":"error","name":"UnsortedLeaves"},{"inputs":[],"type":"error","name":"UnsortedLeaves"},{"inputs":[],"stateMutability":"view","type":"function","name":"MMR_ROOT_PAYLOAD_ID","outputs":[{"internalType":"bytes2","name":"","type":"bytes2"}]},{"inputs":[],"stateMutability":"view","type":"function","name":"_apk","outputs":[{"internalType":"contract IApkProof","name":"","type":"address"}]},{"inputs":[],"stateMutability":"view","type":"function","name":"_digestParaId","outputs":[{"internalType":"uint32","name":"","type":"uint32"}]},{"inputs":[{"internalType":"struct BeefyConsensusState","name":"s","type":"tuple","components":[{"internalType":"uint256","name":"latestHeight","type":"uint256"},{"internalType":"uint256","name":"beefyActivationBlock","type":"uint256"},{"internalType":"struct AuthoritySet","name":"currentAuthoritySet","type":"tuple","components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"len","type":"uint256"},{"internalType":"uint256","name":"blsPoseidonHash","type":"uint256"},{"internalType":"bytes32","name":"ecdsaMerkleRoot","type":"bytes32"}]},{"internalType":"struct AuthoritySet","name":"nextAuthoritySet","type":"tuple","components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"len","type":"uint256"},{"internalType":"uint256","name":"blsPoseidonHash","type":"uint256"},{"internalType":"bytes32","name":"ecdsaMerkleRoot","type":"bytes32"}]}]},{"internalType":"struct BlsApkBeefyConsensusProof","name":"p","type":"tuple","components":[{"internalType":"struct BlsApkRelayChainProof","name":"relay","type":"tuple","components":[{"internalType":"struct Commitment","name":"commitment","type":"tuple","components":[{"internalType":"struct Payload[]","name":"payload","type":"tuple[]","components":[{"internalType":"bytes2","name":"id","type":"bytes2"},{"internalType":"bytes","name":"data","type":"bytes"}]},{"internalType":"uint32","name":"blockNumber","type":"uint32"},{"internalType":"uint64","name":"validatorSetId","type":"uint64"}]},{"internalType":"uint256[5]","name":"bitlist","type":"uint256[5]"},{"internalType":"bytes32[3]","name":"apk","type":"bytes32[3]"},{"internalType":"bytes32[6]","name":"apk2","type":"bytes32[6]"},{"internalType":"bytes","name":"apkProof","type":"bytes"},{"internalType":"bytes32[3]","name":"signature","type":"bytes32[3]"},{"internalType":"struct BeefyMmrLeaf","name":"latestMmrLeaf","type":"tuple","components":[{"internalType":"uint8","name":"version","type":"uint8"},{"internalType":"uint32","name":"parentNumber","type":"uint32"},{"internalType":"bytes32","name":"parentHash","type":"bytes32"},{"internalType":"struct AuthoritySetCommitment","name":"nextAuthoritySet","type":"tuple","components":[{"internalType":"uint64","name":"id","type":"uint64"},{"internalType":"uint32","name":"len","type":"uint32"},{"internalType":"bytes32","name":"root","type":"bytes32"}]},{"internalType":"bytes32","name":"extra","type":"bytes32"},{"internalType":"uint256","name":"leafIndex","type":"uint256"}]},{"internalType":"bytes32[]","name":"mmrProof","type":"bytes32[]"}]},{"internalType":"struct ParachainProof","name":"parachain","type":"tuple","components":[{"internalType":"struct Parachain[]","name":"parachains","type":"tuple[]","components":[{"internalType":"uint256","name":"index","type":"uint256"},{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bytes","name":"header","type":"bytes"}]},{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"},{"internalType":"uint256","name":"leafCount","type":"uint256"}]}]}],"stateMutability":"pure","type":"function","name":"noOp"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"stateMutability":"view","type":"function","name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}]},{"inputs":[{"internalType":"bytes","name":"previousState","type":"bytes"},{"internalType":"bytes","name":"proof","type":"bytes"}],"stateMutability":"view","type":"function","name":"verify","outputs":[{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"struct IntermediateState[]","name":"","type":"tuple[]","components":[{"internalType":"uint256","name":"stateMachineId","type":"uint256"},{"internalType":"uint256","name":"height","type":"uint256"},{"internalType":"struct StateCommitment","name":"commitment","type":"tuple","components":[{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bytes32","name":"overlayRoot","type":"bytes32"},{"internalType":"bytes32","name":"stateRoot","type":"bytes32"}]}]},{"internalType":"uint256","name":"","type":"uint256"}]}],"devdoc":{"kind":"dev","methods":{"noOp((uint256,uint256,(uint256,uint256,uint256,bytes32),(uint256,uint256,uint256,bytes32)),((((bytes2,bytes)[],uint32,uint64),uint256[5],bytes32[3],bytes32[6],bytes,bytes32[3],(uint8,uint32,bytes32,(uint64,uint32,bytes32),bytes32,uint256),bytes32[]),((uint256,uint256,bytes)[],bytes32[],uint256)))":{"details":"Only here so the structs appear in the ABI, which is what the Rust bindings are generated from. `verify` takes bytes, so without this they would be invisible."},"supportsInterface(bytes4)":{"details":"Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section] to learn more about how these ids are created. This function call must use less than 30 000 gas."},"verify(bytes,bytes)":{"details":"IConsensusV2 entry point."}},"version":1},"userdoc":{"kind":"user","methods":{"MMR_ROOT_PAYLOAD_ID()":{"notice":"The payload id for the mmr root in a BEEFY commitment, \"mh\""},"_apk()":{"notice":"The APK proof verifier, holding the circuit's verifying key."},"_digestParaId()":{"notice":"The parachain whose header digests carry apk commitments, which is hyperbridge. Every parachain in a proof is proven against the heads root, so a digest from any of them is authentic; only this one's says anything about the relay chain's authorities."}},"version":1}},"settings":{"remappings":["@gnark-apk-proofs/=lib/gnark-apk-proofs/solidity/contracts/","@hyperbridge/core/=node_modules/@hyperbridge/core/contracts/","@openzeppelin/=node_modules/@openzeppelin/","@polytope-labs/=node_modules/@polytope-labs/","@sp1-contracts/=lib/sp1-contracts/contracts/src/","@uniswap/=node_modules/@uniswap/","ds-test/=lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=node_modules/forge-std/src/","gnark-apk-proofs/=lib/gnark-apk-proofs/","openzeppelin-contracts/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/","solidity-stringutils/=lib/solidity-stringutils/","sp1-contracts/=lib/sp1-contracts/contracts/","stringutils/=lib/solidity-stringutils/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"src/consensus/BlsBeefy.sol":"BlsBeefy"},"evmVersion":"prague","libraries":{},"viaIR":true},"sources":{"node_modules/@hyperbridge/core/contracts/interfaces/IConsensusV2.sol":{"keccak256":"0x71dcb5168f8f0f95effac221bdc49e0f662011c1bf86a9369cc0db183b8ac4c3","urls":["bzz-raw://d84388af7b50f5f31110fe3d711930c816432f8c2ebcc7417639a727760544ff","dweb:/ipfs/QmWp89jgUMhCqrGVDphLAEhJm5kPP6q1y9yid3D2a3JFZ9"],"license":"Apache-2.0"},"node_modules/@openzeppelin/contracts/utils/introspection/ERC165.sol":{"keccak256":"0x2d9dc2fe26180f74c11c13663647d38e259e45f95eb88f57b61d2160b0109d3e","urls":["bzz-raw://81233d1f98060113d9922180bb0f14f8335856fe9f339134b09335e9f678c377","dweb:/ipfs/QmWh6R35SarhAn4z2wH8SU456jJSYL2FgucfTFgbHJJN4E"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol":{"keccak256":"0x8891738ffe910f0cf2da09566928589bf5d63f4524dd734fd9cedbac3274dd5c","urls":["bzz-raw://971f954442df5c2ef5b5ebf1eb245d7105d9fbacc7386ee5c796df1d45b21617","dweb:/ipfs/QmadRjHbkicwqwwh61raUEapaVEtaLMcYbQZWs9gUkgj3u"],"license":"MIT"},"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMountainRange.sol":{"keccak256":"0x014237038bb77bdf371b50c1268d02bf7eeaf7068c35483d5d0684ca7c30d544","urls":["bzz-raw://beb3ae60094f49716a2b0cfa6229ee27af1778934cf5f2dfc704433fda5a687b","dweb:/ipfs/QmQ339wEFT9X1woHyFTDbbhUCx8ekD9MGLRnhS3PpYp1pm"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMultiProof.sol":{"keccak256":"0xd4f6e6a9eceaa7d1cdf9684bfe7f3f552adf21dfcf7f1372f7943a4c2f15deb7","urls":["bzz-raw://ec900b3e79ea8ef9d275697937d1984e994d10117aa2be066bd73f6a31aedf10","dweb:/ipfs/QmZQGLiJtbvUSEW5H2HEenzxp58fN43XHRY3SoNTC9Hobg"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Bytes.sol":{"keccak256":"0xd305383358b93285d8fcee512795487484eadfd7b602df16ff4e9b01afbefec7","urls":["bzz-raw://57c86cd2fe6ab591264c5632d503d77f04748d722e4defa9c2badb238ee1f9bd","dweb:/ipfs/QmTZ6xTcaYkRBshq6YZVMx2kkk94ZtJsRM6xLpC5qWT3oA"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Memory.sol":{"keccak256":"0x59e3a56caa42c1aac30231173439817d38c7f359e40bd36e9bb418d3f82ceab7","urls":["bzz-raw://e060fed46c6b420624166ea02a326f0f566897941bdc322257e690ae134b8179","dweb:/ipfs/QmbXW8yG2ZntjLMyUEmQGcRZroZj4dVZkBhHxK2PFKfUKB"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Node.sol":{"keccak256":"0xca611969a68f7fe63dcdc742c9caf9bc1b26495561b68f4676c209279a4576ba","urls":["bzz-raw://17b7ec2cf65d484f0a2c188a7ae33430b015b3a55bf10393167f53839e2dde56","dweb:/ipfs/QmTQbT8J7rBRNHFXSR7S4KbpwCteMrFt8mvrzCD1vYYTwX"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/polkadot/ScaleCodec.sol":{"keccak256":"0x9ac4df46e68718f7deaaa5b7443778533f53dc0ff3736cc386cf4991099da2aa","urls":["bzz-raw://85d08b00d9173358323105be975ce85b7e206ba52df2d80a14d2581dec1ddd11","dweb:/ipfs/QmQCqVvSdJUqfhLH8TRBGNDGiEzkGduhRacnYhoKcm7e2a"],"license":"Apache-2.0"},"src/consensus/BlsBeefy.sol":{"keccak256":"0x15657e2fd4f84139b1f018b0bb0ed83b8b75cc7259654d061f9f7c59838cf73d","urls":["bzz-raw://85d7bebe51acb750406079fa98fa4b6fa1945afc18a3291926cb80940b1523cc","dweb:/ipfs/QmNSwQ4bxgrp9cEdS8Lp4V82Etc4rFPCd5cYKgUBfggXTb"],"license":"Apache-2.0"},"src/consensus/Codec.sol":{"keccak256":"0x477b62396db5a5c1d89d0388b0724dde80e0a0423b89c07f7b3b5622ff03b5d4","urls":["bzz-raw://6a584c6500ae347c6f9d76d06a96757b024dcb892601c6d9c1a945b9d76811d6","dweb:/ipfs/QmduKRb2Zr9eqeZbi5TFRw5Dyig3XVY3rMvPkxLktLFYrN"],"license":"Apache-2.0"},"src/consensus/Types.sol":{"keccak256":"0xe4b169eefb4afd0f83f09335f4460705fe96e7736314c2030235720cf976e76f","urls":["bzz-raw://4ad510e26689491bcba165fd73c08e1b80302148804e27a184861712d966540b","dweb:/ipfs/QmVuaZ3yB3eBkHRh2DS6Cy1E6B3oige6fDtvCrcYssc3qi"],"license":"Apache-2.0"}},"version":1},"id":118} \ No newline at end of file diff --git a/evm/rust/abi/EcdsaBeefy.json b/evm/rust/abi/EcdsaBeefy.json index c9b48161f..b2270fea4 100644 --- a/evm/rust/abi/EcdsaBeefy.json +++ b/evm/rust/abi/EcdsaBeefy.json @@ -1 +1 @@ -{"abi":[{"type":"function","name":"MMR_ROOT_PAYLOAD_ID","inputs":[],"outputs":[{"name":"","type":"bytes2","internalType":"bytes2"}],"stateMutability":"view"},{"type":"function","name":"noOp","inputs":[{"name":"s","type":"tuple","internalType":"struct BeefyConsensusState","components":[{"name":"latestHeight","type":"uint256","internalType":"uint256"},{"name":"beefyActivationBlock","type":"uint256","internalType":"uint256"},{"name":"currentAuthoritySet","type":"tuple","internalType":"struct AuthoritySetCommitment","components":[{"name":"id","type":"uint64","internalType":"uint64"},{"name":"len","type":"uint32","internalType":"uint32"},{"name":"root","type":"bytes32","internalType":"bytes32"}]},{"name":"nextAuthoritySet","type":"tuple","internalType":"struct AuthoritySetCommitment","components":[{"name":"id","type":"uint64","internalType":"uint64"},{"name":"len","type":"uint32","internalType":"uint32"},{"name":"root","type":"bytes32","internalType":"bytes32"}]}]},{"name":"p","type":"tuple","internalType":"struct BeefyConsensusProof","components":[{"name":"relay","type":"tuple","internalType":"struct RelayChainProof","components":[{"name":"signedCommitment","type":"tuple","internalType":"struct SignedCommitment","components":[{"name":"commitment","type":"tuple","internalType":"struct Commitment","components":[{"name":"payload","type":"tuple[]","internalType":"struct Payload[]","components":[{"name":"id","type":"bytes2","internalType":"bytes2"},{"name":"data","type":"bytes","internalType":"bytes"}]},{"name":"blockNumber","type":"uint32","internalType":"uint32"},{"name":"validatorSetId","type":"uint64","internalType":"uint64"}]},{"name":"votes","type":"tuple[]","internalType":"struct Vote[]","components":[{"name":"signature","type":"bytes","internalType":"bytes"},{"name":"authorityIndex","type":"uint256","internalType":"uint256"}]}]},{"name":"latestMmrLeaf","type":"tuple","internalType":"struct BeefyMmrLeaf","components":[{"name":"version","type":"uint8","internalType":"uint8"},{"name":"parentNumber","type":"uint32","internalType":"uint32"},{"name":"parentHash","type":"bytes32","internalType":"bytes32"},{"name":"nextAuthoritySet","type":"tuple","internalType":"struct AuthoritySetCommitment","components":[{"name":"id","type":"uint64","internalType":"uint64"},{"name":"len","type":"uint32","internalType":"uint32"},{"name":"root","type":"bytes32","internalType":"bytes32"}]},{"name":"extra","type":"bytes32","internalType":"bytes32"},{"name":"leafIndex","type":"uint256","internalType":"uint256"}]},{"name":"mmrProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}]},{"name":"parachain","type":"tuple","internalType":"struct ParachainProof","components":[{"name":"parachains","type":"tuple[]","internalType":"struct Parachain[]","components":[{"name":"index","type":"uint256","internalType":"uint256"},{"name":"id","type":"uint256","internalType":"uint256"},{"name":"header","type":"bytes","internalType":"bytes"}]},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"leafCount","type":"uint256","internalType":"uint256"}]}]}],"outputs":[],"stateMutability":"pure"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"verify","inputs":[{"name":"previousState","type":"bytes","internalType":"bytes"},{"name":"proof","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"},{"name":"","type":"tuple[]","internalType":"struct IntermediateState[]","components":[{"name":"stateMachineId","type":"uint256","internalType":"uint256"},{"name":"height","type":"uint256","internalType":"uint256"},{"name":"commitment","type":"tuple","internalType":"struct StateCommitment","components":[{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"overlayRoot","type":"bytes32","internalType":"bytes32"},{"name":"stateRoot","type":"bytes32","internalType":"bytes32"}]}]},{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"pure"},{"type":"function","name":"verifyConsensus","inputs":[{"name":"encodedState","type":"bytes","internalType":"bytes"},{"name":"encodedProof","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"},{"name":"","type":"tuple[]","internalType":"struct IntermediateState[]","components":[{"name":"stateMachineId","type":"uint256","internalType":"uint256"},{"name":"height","type":"uint256","internalType":"uint256"},{"name":"commitment","type":"tuple","internalType":"struct StateCommitment","components":[{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"overlayRoot","type":"bytes32","internalType":"bytes32"},{"name":"stateRoot","type":"bytes32","internalType":"bytes32"}]}]}],"stateMutability":"pure"},{"type":"error","name":"ECDSAInvalidSignature","inputs":[]},{"type":"error","name":"ECDSAInvalidSignatureLength","inputs":[{"name":"length","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ECDSAInvalidSignatureS","inputs":[{"name":"s","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"EmptyLeaves","inputs":[]},{"type":"error","name":"EmptyTree","inputs":[]},{"type":"error","name":"EmptyTree","inputs":[]},{"type":"error","name":"IllegalGenesisBlock","inputs":[]},{"type":"error","name":"InvalidAuthoritiesProof","inputs":[]},{"type":"error","name":"InvalidMmrProof","inputs":[]},{"type":"error","name":"LeafIndexOutOfBounds","inputs":[]},{"type":"error","name":"MmrRootHashMissing","inputs":[]},{"type":"error","name":"OutOfBoundsLeaves","inputs":[]},{"type":"error","name":"ProofExhausted","inputs":[]},{"type":"error","name":"SuperMajorityRequired","inputs":[]},{"type":"error","name":"TimestampNotFound","inputs":[]},{"type":"error","name":"UnconsumedProof","inputs":[]},{"type":"error","name":"UnknownAuthoritySet","inputs":[]},{"type":"error","name":"UnsortedLeaves","inputs":[]},{"type":"error","name":"UnsortedLeaves","inputs":[]}],"bytecode":{"object":"0x60808060405234601557613455908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c806301ffc9a7146100645780637163f3121461005f5780637d7555981461005a578063af8b91d6146100555763f7e83aee14610050575f80fd5b610aac565b610a31565b610926565b6107e5565b346100d45760203660031901126100d45760043563ffffffff60e01b81168091036100d457630faeaab360e31b81149081156100c3575b81156100b2575b501515608052607f1960a0016080f35b6301ffc9a760e01b149050816100a2565b637bf41d7760e11b8114915061009b565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b606081019081106001600160401b0382111761010757604052565b6100d8565b608081019081106001600160401b0382111761010757604052565b604081019081106001600160401b0382111761010757604052565b60c081019081106001600160401b0382111761010757604052565b90601f801991011681019081106001600160401b0382111761010757604052565b6040519061018d60808361015d565b565b6040519061018d60408361015d565b6040519061018d60608361015d565b6040519061018d60a08361015d565b6001600160401b038116036100d457565b359061018d826101bc565b63ffffffff8116036100d457565b359061018d826101d8565b91908260609103126100d457604051610209816100ec565b60408082948035610219816101bc565b84526020810135610229816101d8565b60208501520135910152565b906101006003198301126100d45760405161024f8161010c565b606061027a82946004358452602435602085015261026e8160446101f1565b604085015260a46101f1565b910152565b9190610100838203126100d457606061027a6040519261029e8461010c565b60a0849680358652602081013560208701526102bd83604083016101f1565b6040870152016101f1565b6001600160401b0381116101075760051b60200190565b6001600160f01b03198116036100d457565b6001600160401b03811161010757601f01601f191660200190565b81601f820112156100d457803590610323826102f1565b92610331604051948561015d565b828452602083830101116100d457815f926020809301838601378301015290565b81601f820112156100d457803590610369826102c8565b92610377604051948561015d565b82845260208085019360051b830101918183116100d45760208101935b8385106103a357505050505090565b84356001600160401b0381116100d45782016040818503601f1901126100d457604051916103d083610127565b6020820135926001600160401b0384116100d4576040836103f888602080988198010161030c565b8352013583820152815201940193610394565b91906040838203126100d4576040519061042482610127565b819380356001600160401b0381116100d45781016060818403126100d4576040519061044f826100ec565b80356001600160401b0381116100d457810184601f820112156100d457803590610478826102c8565b91610486604051938461015d565b80835260208084019160051b830101918783116100d45760208101915b8383106104f657505050506104ce9160409184526104c3602082016101e6565b6020850152016101cd565b604082015283526020810135916001600160401b0383116100d45760209261027a9201610352565b82356001600160401b0381116100d4578201906040828b03601f1901126100d4576040519061052482610127565b6020830135610532816102df565b82526040830135916001600160401b0383116100d45761055a8c60208096958196010161030c565b838201528152019201916104a3565b60ff8116036100d457565b919091610100818403126100d45760a060e06040519261059384610142565b6105cb849682356105a381610569565b865260208301356105b3816101d8565b602087015260408301356040870152606083016101f1565b606085015260c081013560808501520135910152565b9080601f830112156100d45781356105f8816102c8565b92610606604051948561015d565b81845260208085019260051b8201019283116100d457602001905b82821061062e5750505090565b8135815260209182019101610621565b919091610160818403126100d45761065461017e565b9281356001600160401b0381116100d4578161067191840161040b565b84526106808160208401610574565b60208501526101208201356001600160401b0381116100d457816106a59184016105e1565b60408501526101408201356001600160401b0381116100d4576106c892016105e1565b6060830152565b9190916060818403126100d457604051906106e9826100ec565b819381356001600160401b0381116100d457820181601f820112156100d457803590610714826102c8565b91610722604051938461015d565b80835260208084019160051b830101918483116100d45760208101915b838310610770575050505083526020820135916001600160401b0383116100d45761022960409392849383016105e1565b82356001600160401b0381116100d4578201906060828803601f1901126100d4576040519061079e826100ec565b60208301358252604083013560208301526060830135916001600160401b0383116100d4576107d58960208096958196010161030c565b604082015281520192019161073f565b346100d4576101203660031901126100d45761080036610235565b50610104356001600160401b0381116100d457604060031982360301126100d4576040519061082e82610127565b80600401356001600160401b0381116100d457610851906004369184010161063e565b825260248101356001600160401b0381116100d457602091600461087892369201016106cf565b910152005b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b90602080835192838152019201905f5b8181106108be5750505090565b909192602060a0600192604080885180518452858101518685015201518051828401528481015160608401520151608082015201940191019190916108b1565b90916109156109239360408452604084019061087d565b9160208184039101526108a1565b90565b346100d45760403660031901126100d4576004356001600160401b0381116100d45761095690369060040161030c565b6024356001600160401b0381116100d45761097590369060040161030c565b815182019061010083602084019303126100d457610a1e926109e76109ff926109d3610a109560c0604051956109aa8761010c565b60208101518752604081015160208801526109c88360608301610bcf565b604088015201610bcf565b606084015260208082518301019101611050565b6109f292919261018f565b92835260208301526112a3565b92906040519283916020830161111c565b03601f19810183528261015d565b610a2d604051928392836108fe565b0390f35b346100d4575f3660031901126100d457604051610dad60f31b8152602090f35b9181601f840112156100d4578235916001600160401b0383116100d457602083818601950101116100d457565b939291610aa790610a9960409360608852606088019061087d565b9086820360208801526108a1565b930152565b346100d45760403660031901126100d4576004356001600160401b0381116100d457610adc903690600401610a51565b6024356001600160401b0381116100d457610afb903690600401610a51565b9290918101610100828203126100d457610b149161027f565b918101916040828403126100d45781356001600160401b0381116100d45783610b3e91840161063e565b916020810135936001600160401b0385116100d457610b6994610b6192016106cf565b6109f261018f565b90610a2d6001600160401b03610bac606060405194610b9d86610b8f836020830161111c565b03601f19810188528761015d565b0151516001600160401b031690565b1660405193849384610a7e565b519061018d826101bc565b519061018d826101d8565b91908260609103126100d457604051610be7816100ec565b60408082948051610bf7816101bc565b84526020810151610c07816101d8565b60208501520151910152565b81601f820112156100d457805190610c2a826102f1565b92610c38604051948561015d565b828452602083830101116100d457815f9260208093018386015e8301015290565b81601f820112156100d457805190610c70826102c8565b92610c7e604051948561015d565b82845260208085019360051b830101918183116100d45760208101935b838510610caa57505050505090565b84516001600160401b0381116100d45782016040818503601f1901126100d45760405191610cd783610127565b6020820151926001600160401b0384116100d457604083610cff886020809881980101610c13565b8352015183820152815201940193610c9b565b91906040838203126100d45760405190610d2b82610127565b819380516001600160401b0381116100d45781016060818403126100d45760405190610d56826100ec565b80516001600160401b0381116100d457810184601f820112156100d457805190610d7f826102c8565b91610d8d604051938461015d565b80835260208084019160051b830101918783116100d45760208101915b838310610dfd5750505050610dd5916040918452610dca60208201610bc4565b602085015201610bb9565b604082015283526020810151916001600160401b0383116100d45760209261027a9201610c59565b82516001600160401b0381116100d4578201906040828b03601f1901126100d45760405190610e2b82610127565b6020830151610e39816102df565b82526040830151916001600160401b0383116100d457610e618c602080969581960101610c13565b83820152815201920191610daa565b919091610100818403126100d45760a060e060405192610e8f84610142565b610ec784968251610e9f81610569565b86526020830151610eaf816101d8565b60208701526040830151604087015260608301610bcf565b606085015260c081015160808501520151910152565b9080601f830112156100d4578151610ef4816102c8565b92610f02604051948561015d565b81845260208085019260051b8201019283116100d457602001905b828210610f2a5750505090565b8151815260209182019101610f1d565b9190916060818403126100d45760405190610f54826100ec565b819381516001600160401b0381116100d457820181601f820112156100d457805190610f7f826102c8565b91610f8d604051938461015d565b80835260208084019160051b830101918483116100d45760208101915b838310610fdb575050505083526020820151916001600160401b0383116100d457610c076040939284938301610edd565b82516001600160401b0381116100d4578201906060828803601f1901126100d45760405190611009826100ec565b60208301518252604083015160208301526060830151916001600160401b0383116100d45761104089602080969581960101610c13565b6040820152815201920191610faa565b9190916040818403126100d45780516001600160401b0381116100d4578101610160818503126100d45761108261017e565b9080516001600160401b0381116100d4578561109f918301610d12565b82526110ae8560208301610e70565b60208301526101208101516001600160401b0381116100d457856110d3918301610edd565b6040830152610140810151906001600160401b0382116100d4576110f991869101610edd565b60608201529260208201516001600160401b0381116100d4576109239201610f3a565b61018d9092919260a06060610100830195805184526020810151602085015261117060408201516040860190604080916001600160401b03815116845263ffffffff60208201511660208501520151910152565b0151910190604080916001600160401b03815116845263ffffffff60208201511660208501520151910152565b604051906111aa826100ec565b5f6040838281528260208201520152565b604051906111c88261010c565b815f81525f60208201526111da61119d565b6040820152606061027a61119d565b604051906111f860208361015d565b5f80835282815b82811061120b57505050565b60209060405161121a816100ec565b5f81525f8382015261122a61119d565b6040820152828285010152016111ff565b90611245826102c8565b611252604051918261015d565b8281528092611263601f19916102c8565b01905f5b82811061127357505050565b602090604051611282816100ec565b5f81525f8382015261129261119d565b604082015282828501015201611267565b91906112ad6111bb565b50825163ffffffff6020835151510151161115611604578051906112cf6111bb565b508151916020830151519251906112fa6112f16020840163ffffffff90511690565b63ffffffff1690565b93604083019061131182516001600160401b031690565b9160408901926001600160401b0361134161133586516001600160401b0390511690565b6001600160401b031690565b91169081141590816115e1575b506115d257516001600160401b03168251906001600160401b0361137c61133584516001600160401b031690565b9116036115c657905b60208201916113ab6113a76113a16112f1865163ffffffff1690565b846118b4565b1590565b6115b757855151955f965f5b8181106115335750508615611524576113cf906119c3565b60208151910120916113e08161165f565b925f5b8281106114a5575050506040015160608501519251611416936113a793916114109063ffffffff166112f1565b92611b18565b611496576109239460208361142f82966080968c611c30565b01916114486060845101516001600160401b0390511690565b60608a01918251916001600160401b0361146c61133585516001600160401b031690565b911611611485575b5050508752510151910151906116f8565b5260608351015190525f8080611474565b63528bd3ef60e01b5f5260045ffd5b806114b760019260208b510151611633565b5160206114c5825186611b02565b910151906040516114f581610a106020820194856014916bffffffffffffffffffffffff199060601b1681520190565b51902061150061018f565b91825260208201526115128288611633565b5261151d8187611633565b50016113e3565b6323188e3960e21b5f5260045ffd5b610dad60f31b61156661155961154a848751611633565b51516001600160f01b03191690565b6001600160f01b03191690565b148061159e575b61157a575b6001016113b7565b97506001611596602061158e8b8651611633565b5101516118dd565b989050611572565b506020806115ad838651611633565b510151511461156d565b633aa90f7f60e21b5f5260045ffd5b50606088015190611385565b637202e68560e11b5f5260045ffd5b90506115fc61133560608c01516001600160401b0390511690565b14155f61134e565b506109236111e9565b634e487b7160e01b5f52603260045260245ffd5b80511561162e5760200190565b61160d565b805182101561162e5760209160051b010190565b6040519061165482610127565b5f6020838281520152565b90611669826102c8565b611676604051918261015d565b8281528092611687601f19916102c8565b01905f5b82811061169757505050565b6020906116a2611647565b8282850101520161168b565b805191908290602001825e015f815290565b6040516001600160e01b03199091166020820152919061018d9083906116ea9060248301906116ae565b03601f19810184528361015d565b815151916117058361165f565b9161170f8461123b565b935f5b8181106117585750611725575b50505090565b61173d9282604060206113a795015191015192611b18565b611749575f808061171f565b630b92186960e31b5f5260045ffd5b611763818551611633565b519060408201916117748351611e25565b92602084019182511561185557600194816117ee602061182094519201946117e86117e16117a6885163ffffffff1690565b6001600160e01b03199063ff00ff00600882811b9190911691901c62ff00ff1617601081811b63ffff00001691901c61ffff161760e01b1690565b9151611f71565b906116c0565b602081519101206117fd61018f565b918252602082015261180f868c611633565b5261181a858b611633565b50611f96565b9051915161182c61019e565b928352602083015260408201526118438289611633565b5261184e8188611633565b5001611712565b63b4eb9e5160e01b5f5260045ffd5b634e487b7160e01b5f52601160045260245ffd5b906001820180921161188657565b611864565b906003820180921161188657565b906002820180921161188657565b9190820180921161188657565b906001600160ff1b03811681036118865760039060011b04906001820180921161188657101590565b60208151106118ed576020015190565b60405162461bcd60e51b8152602060048201526024808201527f42797465733a3a20746f427974657333323a206461746120697320746f20736860448201526337b93a1760e11b6064820152608490fd5b6040519061194d60208361015d565b5f8252565b6116ea611971949361197161018d9460405197889560208701906116ae565b906116ae565b61018d926119719594611996600c9460405198899560208701906116ae565b6001600160e01b03199290921682526001600160c01b03191660048201520360131981018552018361015d565b908151516119cf61193e565b905f5b818110611aa45750926119e86109239394612156565b91611a9e611a146040611a056117a6602087015163ffffffff1690565b9401516001600160401b031690565b67ffffffffffff000067ff00ff00ff00ff0066ff00ff00ff00ff8360081c169260081b169165ffff0000ffff65ffff0000ff0065ffffffffffff67ffff0000ffff0000861666ff0000ffff000085161760101c16941691161760101b161767ffffffff0000000063ffffffff8260201c169160201b166001600160401b0360c01b911760c01b1690565b92611977565b91611afb600191610a10611add611abf61154a888b51611633565b6040516001600160f01b031990911660208201529182906022820190565b611af56020611aed888b51611633565b510151611f71565b91611952565b92016119d2565b61092391611b0f916122d3565b9092919261232b565b919392908115611be1578451611b2d816123a7565b611b36826123a7565b916001611b4286612c4e565b1b5f805b838210611b615750505050611b5d94959650612c7e565b1490565b611b6b828c611633565b51519088821015611bd2578215159081611bc7575b50611bb8576001906020611b94848e611633565b510151611ba18489611633565b52808401611baf8488611633565b52910190611b46565b630647f54960e21b5f5260045ffd5b90508111155f611b80565b630834466160e31b5f5260045ffd5b639136328760e01b5f5260045ffd5b60408051909190611c01838261015d565b6001815291601f1901825f5b828110611c1957505050565b602090611c24611647565b82828501015201611c0d565b6113a791611d139360406020830192611cd7611cd26020611cae8751611c57815160ff1690565b90611c688482015163ffffffff1690565b9088810151611c9b6080606084015193015193611c8f611c866101ad565b60ff9097168752565b63ffffffff1685880152565b8984015260608301526080820152612450565b805190820120970151865160200151611ccc9063ffffffff166112f1565b90612544565b611878565b9460a0611ce2611bf0565b9551015190611cef61018f565b9182526020820152611d0085611621565b52611d0a84611621565b50015190612559565b61174957565b6040519060a082018281106001600160401b038211176101075760405260606080835f81525f60208201525f60408201525f838201520152565b60405190611d6082610127565b60606020835f81520152565b6040519061012082018281106001600160401b03821117610107576040525f61010083828152611d9a611d53565b6020820152826040820152611dad611d53565b6060820152826080820152611dc0611d53565b60a08201528260c0820152606060e08201520152565b90611de0826102c8565b611ded604051918261015d565b8281528092611dfe601f19916102c8565b01905f5b828110611e0e57505050565b602090611e19611d6c565b82828501015201611e02565b611e2d611d19565b50611e3661018f565b9081525f6020820152611e50611e4b82612565565b6118dd565b90611e5a816126a4565b611e66611e4b83612565565b611e72611e4b84612565565b91611e7c846126a4565b611e8581611dd6565b945f5b828110611eb457505050611e9a6101ad565b948552602085015260408401526060830152608082015290565b600190611ec083612817565b60ff611eca611d6c565b911680611ef65750600160c08201525b611ee4828a611633565b52611eef8189611633565b5001611e88565b60048103611f1a575060016040820152611f0f84612891565b60608201525b611eda565b60058103611f3d575060016080820152611f3384612891565b60a0820152611eda565b60068103611f5d575060018152611f5384612891565b6020820152611eda565b600803611f15576001610100820152611eda565b61092361197191610a10611f858251612156565b9160405194859360208501906116ae565b611f9e61119d565b505f5f925f5f5b60808501805180518310156120cf576040611fc384611fcb93611633565b510151151590565b806120a7575b61205e575b611fe66040611fc3848451611633565b80612025575b611ffa575b50600101611fa5565b81925061201d6020606061201360809560019551611633565b51015101516129e7565b929150611ff1565b50634953544d60e01b63ffffffff60e01b6120576060612046868651611633565b510151516001600160e01b03191690565b1614611fec565b95509250612081611e4b60206060612077878a51611633565b510151015161296d565b926120a1611e4b60206060612097858b51611633565b51015101516129ae565b95611fd6565b5063049534d560e41b63ffffffff60e01b6120c86060612046868651611633565b1614611fd1565b5050509250929082156120f4576120e461019e565b9283526020830152604082015290565b633eba99d160e21b5f5260045ffd5b60031981019190821161188657565b5f1981019190821161188657565b602003906020821161188657565b9190820391821161188657565b600190610923939260ff60f81b9060f81b16815201906116ae565b604081101561219a57610923612178612172610a109360021b90565b60ff1690565b60405160f89190911b6001600160f81b03191660208201529182906021820190565b6140008110156121f9576109236121d76121c46121bd611cd2610a109560021b90565b61ffff1690565b60ff61ff008260081b169160081c161790565b60405160f09190911b6001600160f01b03191660208201529182906022820190565b6340000000811015612276576109236122546122236112f161221e610a109560021b90565b611899565b63ffffff0062ff00ff62ffffff818460081c1616921660081b161763ffff000061ffff8260101c169160101b161790565b60405160e09190911b6001600160e01b03191660208201529182906024820190565b610a1061229b6122886122a093612a5f565b6040519283916020830160209181520190565b612b78565b6109236122c16121726122bc6122b68551612103565b60021b90565b61188b565b610a106040519384926020840161213b565b8151919060418303612303576122fc9250602082015190606060408401519301515f1a90612bc1565b9192909190565b50505f9160029190565b6004111561231757565b634e487b7160e01b5f52602160045260245ffd5b6123348161230d565b8061233d575050565b6123468161230d565b6001810361235d5763f645eedf60e01b5f5260045ffd5b6123668161230d565b60028103612381575063fce698f760e01b5f5260045260245ffd5b8061238d60039261230d565b146123955750565b6335e2f38360e21b5f5260045260245ffd5b906123b1826102c8565b6123be604051918261015d565b82815280926123cf601f19916102c8565b0190602036910137565b9492919695939096604051978896602088016123f4916116ae565b9063ffffffff60e01b16815260040161240c916116ae565b916001600160401b0360c01b16825263ffffffff60e01b166008820152600c01612435916116ae565b61243e916116ae565b03601f198101835261018d908361015d565b61253161250e6116ea6109239361248d61246b825160ff1690565b60405160f89190911b6001600160f81b03191660208201529283906021820190565b6116ea6124a46117a6602084015163ffffffff1690565b6124bf60408401516040519384916020830160209181520190565b60608301519361253f60806124de611a1488516001600160401b031690565b9561251c60406124f86117a660208c015163ffffffff1690565b9901516040519a8b916020830160209181520190565b03601f1981018b528a61015d565b01516040519889916020830160209181520190565b03601f19810189528861015d565b6123d9565b8061254d575090565b81039081116118865790565b929091611b5d92612df3565b602081019081516020810180911161188657815151106100d45760209051818351820101918291011161188657602061259d9161302b565b9080519060208201809211611886575290565b906020820191825182810180911161188657815151106100d45781156125fe5760209051818451820101918291011161188657816125ed9161302b565b918051918201809211611886575290565b50505060405161260f60208261015d565b5f815290565b60ff60049116019060ff821161188657565b1561262e57565b60405162461bcd60e51b815260206004820152602860248201527f756e657870656374656420707265666978206465636f64696e6720436f6d706160448201526731ba1e2ab4b73a1f60c11b6064820152608490fd5b906001600160401b03809116911601906001600160401b03821161188657565b6126ad81612817565b60038116806126c75750610923915060021c603f16612172565b6001810361270c5750611335906127066121726126fc6126ec61217261092397612817565b60061b67ffffffffffffffc01690565b9260021c603f1690565b90612684565b6002810361278257506112f19061277560ff846127668261272f61092398612817565b95816127558161274761274188612817565b97612817565b991660081b63ffffff001690565b911617921660101b63ffff00001690565b17921660181b63ff0000001690565b1760021c633fffffff1690565b6003036127c1576109239160ff6127a86127a36127bc94603f9060021c1690565b612615565b16906127b76008831115612627565b6125b0565b6129e7565b60405162461bcd60e51b815260206004820152601a60248201527f436f64652073686f756c6420626520756e726561636861626c650000000000006044820152606490fd5b90815181101561162e570160200190565b6020810190815160018101809111611886578151511061285d575181516001600160f81b0319916128489190612806565b511660f81c906128588151611878565b905290565b60405162461bcd60e51b815260206004820152600c60248201526b4f7574206f662072616e676560a01b6044820152606490fd5b612899611d53565b50602081019081516004810180911161188657815151106100d4576020815181845182010191829101116118865760046128d29161302b565b918051906004820180921161188657525f915f905b60048210612927575050806128fe612904926126a4565b906125b0565b61291f61290f61018f565b6001600160e01b03199093168352565b602082015290565b90926001600160f81b031961293c8584612806565b5116908460031b9185830460081486151715611886576001926001600160e01b0319918216901c16179301906128e7565b80516020116100d457602080610923920161302b565b90815181116100d457801561299e576020610923920161302b565b505060405161260f60208261015d565b8051806020116100d457601f1981019081116118865760408201916020018210611886576109239161302b565b8015611886575f190190565b80515f91815b6129f657505090565b90915f1983019083821161188657612a0e8284612806565b5160f81c91600381901b906001600160fd1b038116036118865760ff8111611886576001901b9182810292818404149015171561188657612a5891612a52916118a7565b926129db565b90816129ed565b8060081c9060081b907cff000000ff000000ff000000ff000000ff000000ff000000ff000000ff7dff000000ff000000ff000000ff000000ff000000ff000000ff000000ff007fff000000ff000000ff000000ff000000ff000000ff000000ff000000ff00000084167eff000000ff000000ff000000ff000000ff000000ff000000ff000000ff000084161760101c931691161760101b177bffffffff00000000ffffffff00000000ffffffff00000000ffffffff7fffffffff00000000ffffffff00000000ffffffff00000000ffffffff00000000821660201c911660201b1777ffffffffffffffff0000000000000000ffffffffffffffff8019821660401c911660401b17612b748160801c9160801b90565b1790565b80515f198101908111611886575b6001600160f81b0319612b998284612806565b5116612bad57612ba8906129db565b612b86565b600181018091116118865761092391612983565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411612c43579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15612c38575f516001600160a01b03811615612c2e57905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f9160039190565b6001811115612c79575f19810190811161188657612c6b906130cc565b600181018091116118865790565b505f90565b9192905f83515b6001612c9086611621565b5114612de057612cbc612cb7856001612cb1612cab8a611621565b516130cc565b1b6118a7565b612112565b905f915f915b808310612ce557505050612cd9612cdf9194611878565b60011c90565b92612c85565b909192612cf28489611633565b5182612cfd86611878565b1080612dc2575b15612d635790612d4f6001926002612d3a612d1f898c611633565b51612d32612d2c8b611878565b8d611633565b519084613132565b9701965b612d48848b611633565b5260011c90565b612d59828b611633565b5201929190612cc2565b8987600183188610612da55791600180612d9b612d4f94612d938c612d8c8d9e9d869b9a611633565b5192611633565b519085613132565b9801980196612d3e565b612d4f9150916001612db988829695611633565b51970196612d3e565b50612dd5612dcf86611878565b8a611633565b516001821814612d04565b5050915050612def9150611621565b5190565b9290928215611be157835193841561301c5760015b858110612fee57506001841480612fe4575b80612fd2575b612fb757612e35612e3085613152565b6123a7565b94612e3e61018f565b945f865260208601968752612e5161018f565b925f845260208401948552612e6461019e565b905f82526020820193845260408201525f91805b612f03575b50505051612ef4575190515103612ee55781515f190182525b815115612ed957612ea6826133f4565b612eaf836133f4565b90612ebb845160010190565b84525f5260205260405f20612ed38451845190611633565b52612e96565b91612def915051611621565b637227423160e11b5f5260045ffd5b63072afb8760e51b5f5260045ffd5b612f0c816130cc565b92612f26612f1e6001861b809461212e565b9283926118a7565b938685612f338187613197565b92602084015180155f14612f6b575050505050508551518551145f03612e7d5780612f66612f60876133d8565b8a6133bd565b612e78565b60011480612faf575b15612f9b575050506020612f92826040612f66940151905190611633565b5101518a6133bd565b91612f6693916002612f60941b0391613220565b508015612f74565b925090925051612ee557612fcc602091611621565b51015190565b50612fdc81611621565b515115612e20565b5060018514612e1a565b612ff88183611633565b515161300c61300683612112565b84611633565b51511015611bb857600101612e08565b631a14a47760e31b5f5260045ffd5b919091613037836102f1565b613044604051918261015d565b838152613050846102f1565b602082019190601f1901368337939091905b602081101561309c578061308257505f19905b5182518216911916179052565b612cb761309161309692612120565b613410565b90613075565b909182518152602081018091116118865791602081018091116118865790601f1981019081111561306257611864565b806fffffffffffffffffffffffffffffffff1060071b81811c6001600160401b031060061b1781811c63ffffffff1060051b1781811c61ffff1060041b1781811c60ff1060031b1781811c600f1060021b1781811c60031060011b1790811c6001101790565b600116613145575f5260205260405f2090565b905f5260205260405f2090565b90815f925b61315e5750565b915f19830183811161188657600193169283910192613157565b60405190613185826100ec565b60606040835f81525f60208201520152565b61319f613178565b5080516131b260208301918251906118a7565b928251905b8482106131f9575b508293506131d26128589293518261212e565b9084519460408101516131e361019e565b968752836020880152604087015252825161212e565b90613208816040860151611633565b515182111561321a57600101906131b7565b906131bf565b91906020830151835193613233826123a7565b9361323d836123a7565b955f5b84811061336f5750505050935b600161325884611621565b5114613362575f945f905b80821061327757505084808452845261324d565b90956132838786611633565b5187878461329083611878565b1080613344575b156132eb576001926132c9836132c1612d2c6132ba6132e599976132d397611633565b5192611878565b519083613132565b612d48848c611633565b6132dd8289611633565b520196611899565b90613263565b5050845160208601515111156133355760019161332183926132c96133108c8c611633565b5161331a8a6133d8565b9083613132565b61332b8289611633565b5201960190613263565b63d8f29a1560e01b5f5260045ffd5b5061335761335183611878565b89611633565b516001841814613297565b93505050612def90611621565b6001906133a8604086016020613390825161338a86896118a7565b90611633565b51015161339d848d611633565b525182850190611633565b515184016133b6828a611633565b5201613240565b906133ce6020830151835190611633565b5260018151019052565b6133e86020820151825190611633565b51906001815101905290565b6134046020820151825190611633565b5181515f190190915290565b601f8111611886576101000a9056fea2646970667358221220ef5ccb88ede39e8e09e4f5d0e653ecda99ce9449ab4b4e971bbc15048d77a4e864736f6c634300081e0033","sourceMap":"2851:9773:150:-:0;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x60806040526004361015610011575f80fd5b5f3560e01c806301ffc9a7146100645780637163f3121461005f5780637d7555981461005a578063af8b91d6146100555763f7e83aee14610050575f80fd5b610aac565b610a31565b610926565b6107e5565b346100d45760203660031901126100d45760043563ffffffff60e01b81168091036100d457630faeaab360e31b81149081156100c3575b81156100b2575b501515608052607f1960a0016080f35b6301ffc9a760e01b149050816100a2565b637bf41d7760e11b8114915061009b565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b606081019081106001600160401b0382111761010757604052565b6100d8565b608081019081106001600160401b0382111761010757604052565b604081019081106001600160401b0382111761010757604052565b60c081019081106001600160401b0382111761010757604052565b90601f801991011681019081106001600160401b0382111761010757604052565b6040519061018d60808361015d565b565b6040519061018d60408361015d565b6040519061018d60608361015d565b6040519061018d60a08361015d565b6001600160401b038116036100d457565b359061018d826101bc565b63ffffffff8116036100d457565b359061018d826101d8565b91908260609103126100d457604051610209816100ec565b60408082948035610219816101bc565b84526020810135610229816101d8565b60208501520135910152565b906101006003198301126100d45760405161024f8161010c565b606061027a82946004358452602435602085015261026e8160446101f1565b604085015260a46101f1565b910152565b9190610100838203126100d457606061027a6040519261029e8461010c565b60a0849680358652602081013560208701526102bd83604083016101f1565b6040870152016101f1565b6001600160401b0381116101075760051b60200190565b6001600160f01b03198116036100d457565b6001600160401b03811161010757601f01601f191660200190565b81601f820112156100d457803590610323826102f1565b92610331604051948561015d565b828452602083830101116100d457815f926020809301838601378301015290565b81601f820112156100d457803590610369826102c8565b92610377604051948561015d565b82845260208085019360051b830101918183116100d45760208101935b8385106103a357505050505090565b84356001600160401b0381116100d45782016040818503601f1901126100d457604051916103d083610127565b6020820135926001600160401b0384116100d4576040836103f888602080988198010161030c565b8352013583820152815201940193610394565b91906040838203126100d4576040519061042482610127565b819380356001600160401b0381116100d45781016060818403126100d4576040519061044f826100ec565b80356001600160401b0381116100d457810184601f820112156100d457803590610478826102c8565b91610486604051938461015d565b80835260208084019160051b830101918783116100d45760208101915b8383106104f657505050506104ce9160409184526104c3602082016101e6565b6020850152016101cd565b604082015283526020810135916001600160401b0383116100d45760209261027a9201610352565b82356001600160401b0381116100d4578201906040828b03601f1901126100d4576040519061052482610127565b6020830135610532816102df565b82526040830135916001600160401b0383116100d45761055a8c60208096958196010161030c565b838201528152019201916104a3565b60ff8116036100d457565b919091610100818403126100d45760a060e06040519261059384610142565b6105cb849682356105a381610569565b865260208301356105b3816101d8565b602087015260408301356040870152606083016101f1565b606085015260c081013560808501520135910152565b9080601f830112156100d45781356105f8816102c8565b92610606604051948561015d565b81845260208085019260051b8201019283116100d457602001905b82821061062e5750505090565b8135815260209182019101610621565b919091610160818403126100d45761065461017e565b9281356001600160401b0381116100d4578161067191840161040b565b84526106808160208401610574565b60208501526101208201356001600160401b0381116100d457816106a59184016105e1565b60408501526101408201356001600160401b0381116100d4576106c892016105e1565b6060830152565b9190916060818403126100d457604051906106e9826100ec565b819381356001600160401b0381116100d457820181601f820112156100d457803590610714826102c8565b91610722604051938461015d565b80835260208084019160051b830101918483116100d45760208101915b838310610770575050505083526020820135916001600160401b0383116100d45761022960409392849383016105e1565b82356001600160401b0381116100d4578201906060828803601f1901126100d4576040519061079e826100ec565b60208301358252604083013560208301526060830135916001600160401b0383116100d4576107d58960208096958196010161030c565b604082015281520192019161073f565b346100d4576101203660031901126100d45761080036610235565b50610104356001600160401b0381116100d457604060031982360301126100d4576040519061082e82610127565b80600401356001600160401b0381116100d457610851906004369184010161063e565b825260248101356001600160401b0381116100d457602091600461087892369201016106cf565b910152005b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b90602080835192838152019201905f5b8181106108be5750505090565b909192602060a0600192604080885180518452858101518685015201518051828401528481015160608401520151608082015201940191019190916108b1565b90916109156109239360408452604084019061087d565b9160208184039101526108a1565b90565b346100d45760403660031901126100d4576004356001600160401b0381116100d45761095690369060040161030c565b6024356001600160401b0381116100d45761097590369060040161030c565b815182019061010083602084019303126100d457610a1e926109e76109ff926109d3610a109560c0604051956109aa8761010c565b60208101518752604081015160208801526109c88360608301610bcf565b604088015201610bcf565b606084015260208082518301019101611050565b6109f292919261018f565b92835260208301526112a3565b92906040519283916020830161111c565b03601f19810183528261015d565b610a2d604051928392836108fe565b0390f35b346100d4575f3660031901126100d457604051610dad60f31b8152602090f35b9181601f840112156100d4578235916001600160401b0383116100d457602083818601950101116100d457565b939291610aa790610a9960409360608852606088019061087d565b9086820360208801526108a1565b930152565b346100d45760403660031901126100d4576004356001600160401b0381116100d457610adc903690600401610a51565b6024356001600160401b0381116100d457610afb903690600401610a51565b9290918101610100828203126100d457610b149161027f565b918101916040828403126100d45781356001600160401b0381116100d45783610b3e91840161063e565b916020810135936001600160401b0385116100d457610b6994610b6192016106cf565b6109f261018f565b90610a2d6001600160401b03610bac606060405194610b9d86610b8f836020830161111c565b03601f19810188528761015d565b0151516001600160401b031690565b1660405193849384610a7e565b519061018d826101bc565b519061018d826101d8565b91908260609103126100d457604051610be7816100ec565b60408082948051610bf7816101bc565b84526020810151610c07816101d8565b60208501520151910152565b81601f820112156100d457805190610c2a826102f1565b92610c38604051948561015d565b828452602083830101116100d457815f9260208093018386015e8301015290565b81601f820112156100d457805190610c70826102c8565b92610c7e604051948561015d565b82845260208085019360051b830101918183116100d45760208101935b838510610caa57505050505090565b84516001600160401b0381116100d45782016040818503601f1901126100d45760405191610cd783610127565b6020820151926001600160401b0384116100d457604083610cff886020809881980101610c13565b8352015183820152815201940193610c9b565b91906040838203126100d45760405190610d2b82610127565b819380516001600160401b0381116100d45781016060818403126100d45760405190610d56826100ec565b80516001600160401b0381116100d457810184601f820112156100d457805190610d7f826102c8565b91610d8d604051938461015d565b80835260208084019160051b830101918783116100d45760208101915b838310610dfd5750505050610dd5916040918452610dca60208201610bc4565b602085015201610bb9565b604082015283526020810151916001600160401b0383116100d45760209261027a9201610c59565b82516001600160401b0381116100d4578201906040828b03601f1901126100d45760405190610e2b82610127565b6020830151610e39816102df565b82526040830151916001600160401b0383116100d457610e618c602080969581960101610c13565b83820152815201920191610daa565b919091610100818403126100d45760a060e060405192610e8f84610142565b610ec784968251610e9f81610569565b86526020830151610eaf816101d8565b60208701526040830151604087015260608301610bcf565b606085015260c081015160808501520151910152565b9080601f830112156100d4578151610ef4816102c8565b92610f02604051948561015d565b81845260208085019260051b8201019283116100d457602001905b828210610f2a5750505090565b8151815260209182019101610f1d565b9190916060818403126100d45760405190610f54826100ec565b819381516001600160401b0381116100d457820181601f820112156100d457805190610f7f826102c8565b91610f8d604051938461015d565b80835260208084019160051b830101918483116100d45760208101915b838310610fdb575050505083526020820151916001600160401b0383116100d457610c076040939284938301610edd565b82516001600160401b0381116100d4578201906060828803601f1901126100d45760405190611009826100ec565b60208301518252604083015160208301526060830151916001600160401b0383116100d45761104089602080969581960101610c13565b6040820152815201920191610faa565b9190916040818403126100d45780516001600160401b0381116100d4578101610160818503126100d45761108261017e565b9080516001600160401b0381116100d4578561109f918301610d12565b82526110ae8560208301610e70565b60208301526101208101516001600160401b0381116100d457856110d3918301610edd565b6040830152610140810151906001600160401b0382116100d4576110f991869101610edd565b60608201529260208201516001600160401b0381116100d4576109239201610f3a565b61018d9092919260a06060610100830195805184526020810151602085015261117060408201516040860190604080916001600160401b03815116845263ffffffff60208201511660208501520151910152565b0151910190604080916001600160401b03815116845263ffffffff60208201511660208501520151910152565b604051906111aa826100ec565b5f6040838281528260208201520152565b604051906111c88261010c565b815f81525f60208201526111da61119d565b6040820152606061027a61119d565b604051906111f860208361015d565b5f80835282815b82811061120b57505050565b60209060405161121a816100ec565b5f81525f8382015261122a61119d565b6040820152828285010152016111ff565b90611245826102c8565b611252604051918261015d565b8281528092611263601f19916102c8565b01905f5b82811061127357505050565b602090604051611282816100ec565b5f81525f8382015261129261119d565b604082015282828501015201611267565b91906112ad6111bb565b50825163ffffffff6020835151510151161115611604578051906112cf6111bb565b508151916020830151519251906112fa6112f16020840163ffffffff90511690565b63ffffffff1690565b93604083019061131182516001600160401b031690565b9160408901926001600160401b0361134161133586516001600160401b0390511690565b6001600160401b031690565b91169081141590816115e1575b506115d257516001600160401b03168251906001600160401b0361137c61133584516001600160401b031690565b9116036115c657905b60208201916113ab6113a76113a16112f1865163ffffffff1690565b846118b4565b1590565b6115b757855151955f965f5b8181106115335750508615611524576113cf906119c3565b60208151910120916113e08161165f565b925f5b8281106114a5575050506040015160608501519251611416936113a793916114109063ffffffff166112f1565b92611b18565b611496576109239460208361142f82966080968c611c30565b01916114486060845101516001600160401b0390511690565b60608a01918251916001600160401b0361146c61133585516001600160401b031690565b911611611485575b5050508752510151910151906116f8565b5260608351015190525f8080611474565b63528bd3ef60e01b5f5260045ffd5b806114b760019260208b510151611633565b5160206114c5825186611b02565b910151906040516114f581610a106020820194856014916bffffffffffffffffffffffff199060601b1681520190565b51902061150061018f565b91825260208201526115128288611633565b5261151d8187611633565b50016113e3565b6323188e3960e21b5f5260045ffd5b610dad60f31b61156661155961154a848751611633565b51516001600160f01b03191690565b6001600160f01b03191690565b148061159e575b61157a575b6001016113b7565b97506001611596602061158e8b8651611633565b5101516118dd565b989050611572565b506020806115ad838651611633565b510151511461156d565b633aa90f7f60e21b5f5260045ffd5b50606088015190611385565b637202e68560e11b5f5260045ffd5b90506115fc61133560608c01516001600160401b0390511690565b14155f61134e565b506109236111e9565b634e487b7160e01b5f52603260045260245ffd5b80511561162e5760200190565b61160d565b805182101561162e5760209160051b010190565b6040519061165482610127565b5f6020838281520152565b90611669826102c8565b611676604051918261015d565b8281528092611687601f19916102c8565b01905f5b82811061169757505050565b6020906116a2611647565b8282850101520161168b565b805191908290602001825e015f815290565b6040516001600160e01b03199091166020820152919061018d9083906116ea9060248301906116ae565b03601f19810184528361015d565b815151916117058361165f565b9161170f8461123b565b935f5b8181106117585750611725575b50505090565b61173d9282604060206113a795015191015192611b18565b611749575f808061171f565b630b92186960e31b5f5260045ffd5b611763818551611633565b519060408201916117748351611e25565b92602084019182511561185557600194816117ee602061182094519201946117e86117e16117a6885163ffffffff1690565b6001600160e01b03199063ff00ff00600882811b9190911691901c62ff00ff1617601081811b63ffff00001691901c61ffff161760e01b1690565b9151611f71565b906116c0565b602081519101206117fd61018f565b918252602082015261180f868c611633565b5261181a858b611633565b50611f96565b9051915161182c61019e565b928352602083015260408201526118438289611633565b5261184e8188611633565b5001611712565b63b4eb9e5160e01b5f5260045ffd5b634e487b7160e01b5f52601160045260245ffd5b906001820180921161188657565b611864565b906003820180921161188657565b906002820180921161188657565b9190820180921161188657565b906001600160ff1b03811681036118865760039060011b04906001820180921161188657101590565b60208151106118ed576020015190565b60405162461bcd60e51b8152602060048201526024808201527f42797465733a3a20746f427974657333323a206461746120697320746f20736860448201526337b93a1760e11b6064820152608490fd5b6040519061194d60208361015d565b5f8252565b6116ea611971949361197161018d9460405197889560208701906116ae565b906116ae565b61018d926119719594611996600c9460405198899560208701906116ae565b6001600160e01b03199290921682526001600160c01b03191660048201520360131981018552018361015d565b908151516119cf61193e565b905f5b818110611aa45750926119e86109239394612156565b91611a9e611a146040611a056117a6602087015163ffffffff1690565b9401516001600160401b031690565b67ffffffffffff000067ff00ff00ff00ff0066ff00ff00ff00ff8360081c169260081b169165ffff0000ffff65ffff0000ff0065ffffffffffff67ffff0000ffff0000861666ff0000ffff000085161760101c16941691161760101b161767ffffffff0000000063ffffffff8260201c169160201b166001600160401b0360c01b911760c01b1690565b92611977565b91611afb600191610a10611add611abf61154a888b51611633565b6040516001600160f01b031990911660208201529182906022820190565b611af56020611aed888b51611633565b510151611f71565b91611952565b92016119d2565b61092391611b0f916122d3565b9092919261232b565b919392908115611be1578451611b2d816123a7565b611b36826123a7565b916001611b4286612c4e565b1b5f805b838210611b615750505050611b5d94959650612c7e565b1490565b611b6b828c611633565b51519088821015611bd2578215159081611bc7575b50611bb8576001906020611b94848e611633565b510151611ba18489611633565b52808401611baf8488611633565b52910190611b46565b630647f54960e21b5f5260045ffd5b90508111155f611b80565b630834466160e31b5f5260045ffd5b639136328760e01b5f5260045ffd5b60408051909190611c01838261015d565b6001815291601f1901825f5b828110611c1957505050565b602090611c24611647565b82828501015201611c0d565b6113a791611d139360406020830192611cd7611cd26020611cae8751611c57815160ff1690565b90611c688482015163ffffffff1690565b9088810151611c9b6080606084015193015193611c8f611c866101ad565b60ff9097168752565b63ffffffff1685880152565b8984015260608301526080820152612450565b805190820120970151865160200151611ccc9063ffffffff166112f1565b90612544565b611878565b9460a0611ce2611bf0565b9551015190611cef61018f565b9182526020820152611d0085611621565b52611d0a84611621565b50015190612559565b61174957565b6040519060a082018281106001600160401b038211176101075760405260606080835f81525f60208201525f60408201525f838201520152565b60405190611d6082610127565b60606020835f81520152565b6040519061012082018281106001600160401b03821117610107576040525f61010083828152611d9a611d53565b6020820152826040820152611dad611d53565b6060820152826080820152611dc0611d53565b60a08201528260c0820152606060e08201520152565b90611de0826102c8565b611ded604051918261015d565b8281528092611dfe601f19916102c8565b01905f5b828110611e0e57505050565b602090611e19611d6c565b82828501015201611e02565b611e2d611d19565b50611e3661018f565b9081525f6020820152611e50611e4b82612565565b6118dd565b90611e5a816126a4565b611e66611e4b83612565565b611e72611e4b84612565565b91611e7c846126a4565b611e8581611dd6565b945f5b828110611eb457505050611e9a6101ad565b948552602085015260408401526060830152608082015290565b600190611ec083612817565b60ff611eca611d6c565b911680611ef65750600160c08201525b611ee4828a611633565b52611eef8189611633565b5001611e88565b60048103611f1a575060016040820152611f0f84612891565b60608201525b611eda565b60058103611f3d575060016080820152611f3384612891565b60a0820152611eda565b60068103611f5d575060018152611f5384612891565b6020820152611eda565b600803611f15576001610100820152611eda565b61092361197191610a10611f858251612156565b9160405194859360208501906116ae565b611f9e61119d565b505f5f925f5f5b60808501805180518310156120cf576040611fc384611fcb93611633565b510151151590565b806120a7575b61205e575b611fe66040611fc3848451611633565b80612025575b611ffa575b50600101611fa5565b81925061201d6020606061201360809560019551611633565b51015101516129e7565b929150611ff1565b50634953544d60e01b63ffffffff60e01b6120576060612046868651611633565b510151516001600160e01b03191690565b1614611fec565b95509250612081611e4b60206060612077878a51611633565b510151015161296d565b926120a1611e4b60206060612097858b51611633565b51015101516129ae565b95611fd6565b5063049534d560e41b63ffffffff60e01b6120c86060612046868651611633565b1614611fd1565b5050509250929082156120f4576120e461019e565b9283526020830152604082015290565b633eba99d160e21b5f5260045ffd5b60031981019190821161188657565b5f1981019190821161188657565b602003906020821161188657565b9190820391821161188657565b600190610923939260ff60f81b9060f81b16815201906116ae565b604081101561219a57610923612178612172610a109360021b90565b60ff1690565b60405160f89190911b6001600160f81b03191660208201529182906021820190565b6140008110156121f9576109236121d76121c46121bd611cd2610a109560021b90565b61ffff1690565b60ff61ff008260081b169160081c161790565b60405160f09190911b6001600160f01b03191660208201529182906022820190565b6340000000811015612276576109236122546122236112f161221e610a109560021b90565b611899565b63ffffff0062ff00ff62ffffff818460081c1616921660081b161763ffff000061ffff8260101c169160101b161790565b60405160e09190911b6001600160e01b03191660208201529182906024820190565b610a1061229b6122886122a093612a5f565b6040519283916020830160209181520190565b612b78565b6109236122c16121726122bc6122b68551612103565b60021b90565b61188b565b610a106040519384926020840161213b565b8151919060418303612303576122fc9250602082015190606060408401519301515f1a90612bc1565b9192909190565b50505f9160029190565b6004111561231757565b634e487b7160e01b5f52602160045260245ffd5b6123348161230d565b8061233d575050565b6123468161230d565b6001810361235d5763f645eedf60e01b5f5260045ffd5b6123668161230d565b60028103612381575063fce698f760e01b5f5260045260245ffd5b8061238d60039261230d565b146123955750565b6335e2f38360e21b5f5260045260245ffd5b906123b1826102c8565b6123be604051918261015d565b82815280926123cf601f19916102c8565b0190602036910137565b9492919695939096604051978896602088016123f4916116ae565b9063ffffffff60e01b16815260040161240c916116ae565b916001600160401b0360c01b16825263ffffffff60e01b166008820152600c01612435916116ae565b61243e916116ae565b03601f198101835261018d908361015d565b61253161250e6116ea6109239361248d61246b825160ff1690565b60405160f89190911b6001600160f81b03191660208201529283906021820190565b6116ea6124a46117a6602084015163ffffffff1690565b6124bf60408401516040519384916020830160209181520190565b60608301519361253f60806124de611a1488516001600160401b031690565b9561251c60406124f86117a660208c015163ffffffff1690565b9901516040519a8b916020830160209181520190565b03601f1981018b528a61015d565b01516040519889916020830160209181520190565b03601f19810189528861015d565b6123d9565b8061254d575090565b81039081116118865790565b929091611b5d92612df3565b602081019081516020810180911161188657815151106100d45760209051818351820101918291011161188657602061259d9161302b565b9080519060208201809211611886575290565b906020820191825182810180911161188657815151106100d45781156125fe5760209051818451820101918291011161188657816125ed9161302b565b918051918201809211611886575290565b50505060405161260f60208261015d565b5f815290565b60ff60049116019060ff821161188657565b1561262e57565b60405162461bcd60e51b815260206004820152602860248201527f756e657870656374656420707265666978206465636f64696e6720436f6d706160448201526731ba1e2ab4b73a1f60c11b6064820152608490fd5b906001600160401b03809116911601906001600160401b03821161188657565b6126ad81612817565b60038116806126c75750610923915060021c603f16612172565b6001810361270c5750611335906127066121726126fc6126ec61217261092397612817565b60061b67ffffffffffffffc01690565b9260021c603f1690565b90612684565b6002810361278257506112f19061277560ff846127668261272f61092398612817565b95816127558161274761274188612817565b97612817565b991660081b63ffffff001690565b911617921660101b63ffff00001690565b17921660181b63ff0000001690565b1760021c633fffffff1690565b6003036127c1576109239160ff6127a86127a36127bc94603f9060021c1690565b612615565b16906127b76008831115612627565b6125b0565b6129e7565b60405162461bcd60e51b815260206004820152601a60248201527f436f64652073686f756c6420626520756e726561636861626c650000000000006044820152606490fd5b90815181101561162e570160200190565b6020810190815160018101809111611886578151511061285d575181516001600160f81b0319916128489190612806565b511660f81c906128588151611878565b905290565b60405162461bcd60e51b815260206004820152600c60248201526b4f7574206f662072616e676560a01b6044820152606490fd5b612899611d53565b50602081019081516004810180911161188657815151106100d4576020815181845182010191829101116118865760046128d29161302b565b918051906004820180921161188657525f915f905b60048210612927575050806128fe612904926126a4565b906125b0565b61291f61290f61018f565b6001600160e01b03199093168352565b602082015290565b90926001600160f81b031961293c8584612806565b5116908460031b9185830460081486151715611886576001926001600160e01b0319918216901c16179301906128e7565b80516020116100d457602080610923920161302b565b90815181116100d457801561299e576020610923920161302b565b505060405161260f60208261015d565b8051806020116100d457601f1981019081116118865760408201916020018210611886576109239161302b565b8015611886575f190190565b80515f91815b6129f657505090565b90915f1983019083821161188657612a0e8284612806565b5160f81c91600381901b906001600160fd1b038116036118865760ff8111611886576001901b9182810292818404149015171561188657612a5891612a52916118a7565b926129db565b90816129ed565b8060081c9060081b907cff000000ff000000ff000000ff000000ff000000ff000000ff000000ff7dff000000ff000000ff000000ff000000ff000000ff000000ff000000ff007fff000000ff000000ff000000ff000000ff000000ff000000ff000000ff00000084167eff000000ff000000ff000000ff000000ff000000ff000000ff000000ff000084161760101c931691161760101b177bffffffff00000000ffffffff00000000ffffffff00000000ffffffff7fffffffff00000000ffffffff00000000ffffffff00000000ffffffff00000000821660201c911660201b1777ffffffffffffffff0000000000000000ffffffffffffffff8019821660401c911660401b17612b748160801c9160801b90565b1790565b80515f198101908111611886575b6001600160f81b0319612b998284612806565b5116612bad57612ba8906129db565b612b86565b600181018091116118865761092391612983565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411612c43579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15612c38575f516001600160a01b03811615612c2e57905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f9160039190565b6001811115612c79575f19810190811161188657612c6b906130cc565b600181018091116118865790565b505f90565b9192905f83515b6001612c9086611621565b5114612de057612cbc612cb7856001612cb1612cab8a611621565b516130cc565b1b6118a7565b612112565b905f915f915b808310612ce557505050612cd9612cdf9194611878565b60011c90565b92612c85565b909192612cf28489611633565b5182612cfd86611878565b1080612dc2575b15612d635790612d4f6001926002612d3a612d1f898c611633565b51612d32612d2c8b611878565b8d611633565b519084613132565b9701965b612d48848b611633565b5260011c90565b612d59828b611633565b5201929190612cc2565b8987600183188610612da55791600180612d9b612d4f94612d938c612d8c8d9e9d869b9a611633565b5192611633565b519085613132565b9801980196612d3e565b612d4f9150916001612db988829695611633565b51970196612d3e565b50612dd5612dcf86611878565b8a611633565b516001821814612d04565b5050915050612def9150611621565b5190565b9290928215611be157835193841561301c5760015b858110612fee57506001841480612fe4575b80612fd2575b612fb757612e35612e3085613152565b6123a7565b94612e3e61018f565b945f865260208601968752612e5161018f565b925f845260208401948552612e6461019e565b905f82526020820193845260408201525f91805b612f03575b50505051612ef4575190515103612ee55781515f190182525b815115612ed957612ea6826133f4565b612eaf836133f4565b90612ebb845160010190565b84525f5260205260405f20612ed38451845190611633565b52612e96565b91612def915051611621565b637227423160e11b5f5260045ffd5b63072afb8760e51b5f5260045ffd5b612f0c816130cc565b92612f26612f1e6001861b809461212e565b9283926118a7565b938685612f338187613197565b92602084015180155f14612f6b575050505050508551518551145f03612e7d5780612f66612f60876133d8565b8a6133bd565b612e78565b60011480612faf575b15612f9b575050506020612f92826040612f66940151905190611633565b5101518a6133bd565b91612f6693916002612f60941b0391613220565b508015612f74565b925090925051612ee557612fcc602091611621565b51015190565b50612fdc81611621565b515115612e20565b5060018514612e1a565b612ff88183611633565b515161300c61300683612112565b84611633565b51511015611bb857600101612e08565b631a14a47760e31b5f5260045ffd5b919091613037836102f1565b613044604051918261015d565b838152613050846102f1565b602082019190601f1901368337939091905b602081101561309c578061308257505f19905b5182518216911916179052565b612cb761309161309692612120565b613410565b90613075565b909182518152602081018091116118865791602081018091116118865790601f1981019081111561306257611864565b806fffffffffffffffffffffffffffffffff1060071b81811c6001600160401b031060061b1781811c63ffffffff1060051b1781811c61ffff1060041b1781811c60ff1060031b1781811c600f1060021b1781811c60031060011b1790811c6001101790565b600116613145575f5260205260405f2090565b905f5260205260405f2090565b90815f925b61315e5750565b915f19830183811161188657600193169283910192613157565b60405190613185826100ec565b60606040835f81525f60208201520152565b61319f613178565b5080516131b260208301918251906118a7565b928251905b8482106131f9575b508293506131d26128589293518261212e565b9084519460408101516131e361019e565b968752836020880152604087015252825161212e565b90613208816040860151611633565b515182111561321a57600101906131b7565b906131bf565b91906020830151835193613233826123a7565b9361323d836123a7565b955f5b84811061336f5750505050935b600161325884611621565b5114613362575f945f905b80821061327757505084808452845261324d565b90956132838786611633565b5187878461329083611878565b1080613344575b156132eb576001926132c9836132c1612d2c6132ba6132e599976132d397611633565b5192611878565b519083613132565b612d48848c611633565b6132dd8289611633565b520196611899565b90613263565b5050845160208601515111156133355760019161332183926132c96133108c8c611633565b5161331a8a6133d8565b9083613132565b61332b8289611633565b5201960190613263565b63d8f29a1560e01b5f5260045ffd5b5061335761335183611878565b89611633565b516001841814613297565b93505050612def90611621565b6001906133a8604086016020613390825161338a86896118a7565b90611633565b51015161339d848d611633565b525182850190611633565b515184016133b6828a611633565b5201613240565b906133ce6020830151835190611633565b5260018151019052565b6133e86020820151825190611633565b51906001815101905290565b6134046020820151825190611633565b5181515f190190915290565b601f8111611886576101000a9056fea2646970667358221220ef5ccb88ede39e8e09e4f5d0e653ecda99ce9449ab4b4e971bbc15048d77a4e864736f6c634300081e0033","sourceMap":"2851:9773:150:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;:::i;:::-;;:::i;:::-;;:::i;:::-;;;;;;-1:-1:-1;;2851:9773:150;;;;;;;;;;;;;;;;-1:-1:-1;;;3683:43:150;;;:92;;;;2851:9773;3683:144;;;;2851:9773;-1:-1:-1;2851:9773:150;;;;-1:-1:-1;;2851:9773:150;;;;3683:144;-1:-1:-1;;;829:40:62;;-1:-1:-1;3683:144:150;;;:92;-1:-1:-1;;;3730:45:150;;;-1:-1:-1;3683:92:150;;2851:9773;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;:::o;:::-;;:::i;:::-;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;:::o;:::-;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;:::o;:::-;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;:::o;:::-;;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;:::o;:::-;;;;;;;;:::i;:::-;:::o;:::-;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;-1:-1:-1;;;;;2851:9773:150;;;;;:::o;:::-;;;;;;:::i;:::-;;;;;;;:::o;:::-;;;;;;:::i;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;;;;;;;:::o;:::-;;;-1:-1:-1;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;:::o;:::-;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;:::o;:::-;-1:-1:-1;;;;;;2851:9773:150;;;;;:::o;:::-;-1:-1:-1;;;;;2851:9773:150;;;;;;-1:-1:-1;;2851:9773:150;;;;:::o;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;-1:-1:-1;2851:9773:150;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;-1:-1:-1;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;-1:-1:-1;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;;;;:::i;:::-;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;:::i;:::-;;;;;:::o;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;-1:-1:-1;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;-1:-1:-1;;2851:9773:150;;;;;;;:::i;:::-;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;:::i;:::-;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;2851:9773:150;;;;;;;;-1:-1:-1;;2851:9773:150;;;;:::o;:::-;;;;;;;;;;;;;;-1:-1:-1;2851:9773:150;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;:::o;:::-;;;;;;-1:-1:-1;;2851:9773:150;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;:::i;:::-;;;4986:47;;;2851:9773;4986:47;2851:9773;4986:47;;2851:9773;;;;;5374:20;2851:9773;5121:59;5285:70;2851:9773;;5374:20;2851:9773;;;;;;;;:::i;:::-;;4986:47;;2851:9773;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;5121:59;;;;;;:::i;:::-;2851:9773;;;;;:::i;:::-;;;;;5317:37;;2851:9773;5285:70;:::i;:::-;2851:9773;;;;5374:20;;;2851:9773;5374:20;;;:::i;:::-;;2851:9773;;5374:20;;;;;;:::i;:::-;2851:9773;;;;;;;;:::i;:::-;;;;;;;;;;-1:-1:-1;;2851:9773:150;;;;;;-1:-1:-1;;;2851:9773:150;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;:::o;:::-;;;;;;-1:-1:-1;;2851:9773:150;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;:::i;:::-;4220:48;;;;;2851:9773;;;;;;;;;;:::i;:::-;4356:52;;;2851:9773;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;4513:70;2851:9773;;;;;:::i;:::-;;;:::i;4513:70::-;2851:9773;;-1:-1:-1;;;;;4639:28:150;:25;2851:9773;;4602:20;;;;;2851:9773;4602:20;;;:::i;:::-;;2851:9773;;4602:20;;;;;;:::i;:::-;4639:25;;2851:9773;-1:-1:-1;;;;;2851:9773:150;;;4639:28;2851:9773;;;;;;;;:::i;:::-;;;;;;:::i;:::-;;;;;;:::i;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;-1:-1:-1;2851:9773:150;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;-1:-1:-1;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;-1:-1:-1;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;:::i;:::-;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;-1:-1:-1;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;:::i;:::-;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;;;;:::i;:::-;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;:::i;:::-;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;-1:-1:-1;2851:9773:150;;;;;;;;;;;;:::o;:::-;;;;;;;:::i;:::-;;-1:-1:-1;2851:9773:150;;-1:-1:-1;2851:9773:150;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;5983:25;2851:9773;;;;5983:25;2851:9773;;;;;;;;;:::o;:::-;;;;;;;;:::i;:::-;5983:25;2851:9773;;5983:25;2851:9773;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;2851:9773:150;;;;;;;;;:::o;:::-;;;;;;;;:::i;:::-;-1:-1:-1;2851:9773:150;;-1:-1:-1;2851:9773:150;;;;;;:::i;:::-;;;;;;;;;;;;;;5575:827;;;2851:9773;;:::i;:::-;5750:26;2851:9773;;;6012:51;:11;;:28;:39;:51;2851:9773;;-1:-1:-1;5983:80:150;5979:160;;6239:11;;2851:9773;;;:::i;:::-;;7074:27;;:33;6012:51;7074:33;;;2851:9773;7147:38;;:50;7124:73;7147:50;6012:51;7147:50;;2851:9773;;;;;;7147:50;2851:9773;;;;7124:73;7303:25;;;;2851:9773;;;;-1:-1:-1;;;;;2851:9773:150;;;;7332:32;7303:25;7332:32;;;-1:-1:-1;;;;;7303:64:150;7332:35;:32;;-1:-1:-1;;;;;2851:9773:150;;;;;7332:35;-1:-1:-1;;;;;2851:9773:150;;;7303:64;2851:9773;;7303:64;;;;:145;;;;5575:827;7286:226;;;2851:9773;-1:-1:-1;;;;;2851:9773:150;7579:32;;2851:9773;-1:-1:-1;;;;;7550:64:150;2851:9773;;;-1:-1:-1;;;;;2851:9773:150;;;7550:64;2851:9773;;7550:64;7681:87;;;;6012:51;7819:16;;2851:9773;7782:54;7783:53;;2851:9773;;;;;;;7783:53;;;:::i;:::-;7782:54;;2851:9773;7782:54;7778:90;;7903:18;;2851:9773;7938:15;-1:-1:-1;7968:13:150;-1:-1:-1;7983:17:150;;;;;;8226:21;;;;8222:54;;8355:24;;;:::i;:::-;6012:51;2851:9773;;;;8345:35;8435;;;;:::i;:::-;8485:13;-1:-1:-1;8500:10:150;;;;;;-1:-1:-1;;;7303:25:150;8872:17;2851:9773;;8891:16;;;2851:9773;;8953:6;;8843:96;;8872:17;8843:96;;2851:9773;;;;8843:96;;;:::i;8953:6::-;8949:44;;6303:54;9044:7;6012:51;9044:7;;;;9405:30;9044:7;;;:::i;:::-;9066:24;;:44;2851:9773;9066:24;;:41;;-1:-1:-1;;;;;2851:9773:150;;;;;9066:44;2851:9773;9113:29;;;;;2851:9773;-1:-1:-1;;;;;9066:79:150;2851:9773;;;-1:-1:-1;;;;;2851:9773:150;;;9066:79;2851:9773;;9066:79;9062:261;;8480:340;2851:9773;;;;;9405:24;:30;2851:9773;6341:15;;;6303:54;;:::i;9062:261::-;9161:64;2851:9773;9271:24;;:41;;9239:73;;9062:261;;;;;8949:44;8968:25;;;-1:-1:-1;8968:25:150;;-1:-1:-1;8968:25:150;8512:3;8550:27;:36;2851:9773;8550:27;6012:51;8550:27;;:33;;:36;:::i;:::-;;6012:51;8620:45;8650:14;;8620:45;;:::i;:::-;8742:19;;2851:9773;;7303:25;2851:9773;8779:27;;;6012:51;8779:27;;;;2851:9773;;;;;;;;;;;;;8779:27;2851:9773;8769:38;;2851:9773;;:::i;:::-;;;;6012:51;8712:97;;2851:9773;8679:130;;;;:::i;:::-;;;;;;:::i;:::-;;2851:9773;8485:13;;8222:54;8256:20;;;-1:-1:-1;8256:20:150;;-1:-1:-1;8256:20:150;8002:3;2851:9773;;;8025:47;:24;:21;:18;;;:21;:::i;:::-;;2851:9773;-1:-1:-1;;;;;;2851:9773:150;;;8025:24;-1:-1:-1;;;;;;2851:9773:150;;;8025:47;;:90;;;8002:3;8021:182;;8002:3;2851:9773;;7968:13;;8021:182;8161:18;;2851:9773;8145:43;6012:51;8161:21;:18;;;:21;:::i;:::-;;:26;;8145:43;:::i;:::-;8021:182;;;;;8025:90;8076:18;6012:51;8076:18;:21;:18;;;:21;:::i;:::-;;:26;;2851:9773;8076:39;8025:90;;7778;7845:23;;;-1:-1:-1;7845:23:150;;-1:-1:-1;7845:23:150;7681:87;7739:29;2851:9773;7739:29;;;7681:87;;;7286:226;7480:21;;;-1:-1:-1;7480:21:150;;-1:-1:-1;7480:21:150;7303:145;7416:29;;7387:61;7416:32;2851:9773;7416:29;;;-1:-1:-1;;;;;2851:9773:150;;;;;7387:61;;;7303:145;;;5979:160;6101:26;;;:::i;2851:9773::-;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;:::i;:::-;;;;;;;;;;;;;;;:::o;:::-;;;;;;;:::i;:::-;-1:-1:-1;2851:9773:150;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;2851:9773:150;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;-1:-1:-1;;;;;;2851:9773:150;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;:::i;10662:1266::-;10849:16;;2851:9773;10922:32;;;;:::i;:::-;11007:28;;;;:::i;:::-;11051:13;-1:-1:-1;11066:7:150;;;;;;11717;11713:178;;11046:657;11901:20;;;10662:1266;:::o;11713:178::-;11848:6;11793:11;;11193;11223:13;11753:77;11793:11;;;11814:15;;2851:9773;11753:77;;:::i;11848:6::-;11844:36;;11713:178;;;;;11844:36;11863:17;;;-1:-1:-1;11863:17:150;;-1:-1:-1;11863:17:150;11075:3;11118:19;:16;;;:19;:::i;:::-;;11193:11;;;;;11174:31;11193:11;;11174:31;:::i;:::-;11223:13;;;;2851:9773;;;11223:18;11219:52;;2851:9773;;;11375:87;11223:13;11528:24;2851:9773;;11415:7;;2851:9773;11426:35;11388:36;11408:15;2851:9773;;;;;;11408:15;-1:-1:-1;;;;;;2851:9773:150;;;;;;;;;;;;;8200:10:78;2851:9773:150;8168:49:78;2851:9773:150;;;;;;;;;;;8266:21:78;2851:9773:150;;;8815:111:78;;11388:36:150;11449:11;;11426:35;:::i;:::-;11375:87;;:::i;:::-;11223:13;2851:9773;;;;11365:98;2851:9773;;:::i;:::-;;;;11223:13;11298:179;;2851:9773;11286:191;;;;:::i;:::-;;;;;;:::i;:::-;;11528:24;:::i;:::-;2851:9773;;;;;;:::i;:::-;;;;11223:13;11601:91;;2851:9773;11193:11;11601:91;;2851:9773;11566:126;;;;:::i;:::-;;;;;;:::i;:::-;;2851:9773;11051:13;;11219:52;11250:21;;;-1:-1:-1;11250:21:150;;-1:-1:-1;11250:21:150;2851:9773;;;;;;;;;;;;;;10208:1;2851:9773;;;;;;;:::o;:::-;;:::i;:::-;;5093:1:78;2851:9773:150;;;;;;;:::o;:::-;;4835:1:78;2851:9773:150;;;;;;;:::o;:::-;;;;;;;;;;:::o;12327:146::-;;-1:-1:-1;;;;;2851:9773:150;;;;;;12460:1;2851:9773;;;;;;;;;;;;;12438:28;;12327:146;:::o;4543:226:71:-;4650:2;2851:9773:150;;4635:17:71;2851:9773:150;;4650:2:71;4703:60;;4543:226;:::o;2851:9773:150:-;;;-1:-1:-1;;;2851:9773:150;;4650:2:71;2851:9773:150;;;;;;;;;;;;;;-1:-1:-1;;;2851:9773:150;;;;;;;;;;;;;;;:::i;:::-;;;;:::o;:::-;;;;;;;;;;;;;;;;;;:::i;:::-;;;:::i;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;-1:-1:-1;;;;;;2851:9773:150;;;;;;-1:-1:-1;;;;;;2851:9773:150;;;;;;-1:-1:-1;;2851:9773:150;;;;;;;:::i;1570:684:148:-;;1684:18;;2851:9773:150;;;:::i;:::-;1766:13:148;-1:-1:-1;1781:14:148;;;;;;2059:40;;;2033:214;2059:40;;;:::i;:::-;2154:22;2191:46;2851:9773:150;;2134:43:148;2851:9773:150;1881:42:148;2154:22;;2851:9773:150;;;;;2134:43:148;2211:25;;2851:9773:150;-1:-1:-1;;;;;2851:9773:150;;;;;7788:18:78;2851:9773:150;;;;;;;;;;7937:18:78;7933:22;2851:9773:150;7902:18:78;7898:22;;;;;;2851:9773:150;;;7932:30:78;7933:22;;;;2851:9773:150;;;7896:67:78;2851:9773:150;;;;;;8025:7:78;2851:9773:150;;;-1:-1:-1;;;;;2851:9773:150;;8012:21:78;;2851:9773:150;;;8698:111:78;;2191:46:148;2033:214;;:::i;1797:3::-;1898:18;1826:179;2851:9773:150;1898:18:148;1881:42;;1898:24;:21;:18;;;:21;:::i;:24::-;2851:9773:150;;-1:-1:-1;;;;;;2851:9773:150;;;1881:42:148;;;2851:9773:150;;;;;;;;;1881:42:148;1941:50;1881:42;1964:21;:18;;;:21;:::i;:::-;;:26;;1941:50;:::i;:::-;1826:179;;:::i;:::-;1797:3;2851:9773:150;1766:13:148;;3714:255:58;3927:8;3714:255;3871:27;3714:255;3871:27;:::i;:::-;3927:8;;;;;:::i;1990:238:69:-;;;;;3884:14;;3880:38;;2851:9773:150;;3995:18:69;;;:::i;:::-;4049;;;:::i;:::-;4106:20;4101:1;4106:20;;;:::i;:::-;2851:9773:150;-1:-1:-1;;4179:7:69;;;;;;4577:42;;;;;;;;;;:::i;:::-;2174:47;1990:238;:::o;4168:9::-;4217;;;;:::i;:::-;;2851:9773:150;4250:16:69;;;;;4246:51;;4315:6;;;:26;;;;4168:9;4311:55;;;4101:1;4392:9;:14;:9;;;;:::i;:::-;;:14;2851:9773:150;4380:26:69;;;;:::i;:::-;2851:9773:150;;;;4448:33:69;;;;:::i;:::-;2851:9773:150;;;4168:9:69;;;4311:55;4350:16;;;-1:-1:-1;4350:16:69;;-1:-1:-1;4350:16:69;4315:26;4325:16;;;;;4315:26;;;4246:51;4275:22;;;-1:-1:-1;4275:22:69;;-1:-1:-1;4275:22:69;3880:38;3907:11;;;-1:-1:-1;3907:11:69;;-1:-1:-1;3907:11:69;2851:9773:150;;;;;;;;;;;:::i;:::-;10208:1;2851:9773;;;-1:-1:-1;;2851:9773:150;;-1:-1:-1;2851:9773:150;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;;;;9491:1054;10416:75;9491:1054;10506:6;9491:1054;9894:30;9765:19;;;;10127:82;:78;9765:19;9684:403;9765:19;;2851:9773;;;;;;;;9828:32;2851:9773;9828:32;;;2851:9773;;;;;;9894:30;;;;2851:9773;9714:359;10029:25;9964:36;;;;10029:25;;2851:9773;;9714:359;2851:9773;;:::i;:::-;;;;;;;;9714:359;2851:9773;;9714:359;;;2851:9773;;9714:359;;;;2851:9773;9964:36;9714:359;;2851:9773;10029:25;9714:359;;2851:9773;9684:403;:::i;:::-;2851:9773;;;;;9661:436;10137:33;;2851:9773;10172:19;;9765;10172:32;2851:9773;10127:78;;2851:9773;;10172:32;2851:9773;10127:78;;;:::i;:::-;:82;:::i;:::-;10262:33;2851:9773;10262:33;;:::i;:::-;10350:19;;:29;2851:9773;;;;:::i;:::-;;;;9765:19;10317:76;;2851:9773;10305:88;;;:::i;:::-;;;;;:::i;:::-;;10457:14;;10416:75;;:::i;10506:6::-;10502:36;;9491:1054::o;2851:9773::-;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;-1:-1:-1;2851:9773:150;;-1:-1:-1;2851:9773:150;;;;-1:-1:-1;2851:9773:150;;;;-1:-1:-1;2851:9773:150;;;;;;:::o;:::-;;;;;;;:::i;:::-;;;;-1:-1:-1;2851:9773:150;;;;:::o;:::-;;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;-1:-1:-1;2851:9773:150;;;;;;;:::i;:::-;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;2851:9773:150;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;;;;2849:1495:148;2851:9773:150;;:::i;:::-;;;;:::i;:::-;;;;2985:1:148;2966:21;;;2851:9773:150;3018:38:148;3034:21;;;:::i;:::-;3018:38;:::i;:::-;3088:35;;;;:::i;:::-;3153:38;3169:21;;;:::i;3153:38::-;3226;3242:21;;;:::i;3226:38::-;3292:35;;;;:::i;:::-;3363:20;;;:::i;:::-;3399:13;2985:1;3414:10;;;;;;2851:9773:150;;;;;:::i;:::-;;;;2966:21:148;4270:67;;2851:9773:150;;4270:67:148;;2851:9773:150;4270:67:148;;;2851:9773:150;4270:67:148;;;2851:9773:150;2849:1495:148;:::o;3426:3::-;2851:9773:150;3458:21:148;;;;:::i;:::-;2851:9773:150;;;:::i;:::-;;;3531:25:148;;;-1:-1:-1;2851:9773:150;3576:14:148;;;1506:1;3576:21;4223:19;;;;:::i;:::-;;;;;;:::i;:::-;;2851:9773:150;3399:13:148;;3527:683;1327:1;3622:29;;1327:1;;-1:-1:-1;2851:9773:150;;3671:18:148;;1506:1;3733:23;;;:::i;:::-;3714:16;;;:42;3618:592;3527:683;;3618:592;1377:1;3781:24;;1377:1;;-1:-1:-1;2851:9773:150;3825:13:148;;;1506:1;3877:23;;;:::i;:::-;3863:11;;;:37;3527:683;;3777:433;1433:1;3925:30;;1433:1;;-1:-1:-1;2851:9773:150;1506:1:148;;4039:23;;;:::i;:::-;2966:21;4019:17;;:43;3527:683;;3921:289;1506:1;4087:47;3777:433;4083:127;2851:9773:150;4154:34:148;;;1506:1;3527:683;;9049:172:78;9158:56;2851:9773:150;9049:172:78;2851:9773:150;9175:31:78;2851:9773:150;;9175:31:78;:::i;:::-;2851:9773:150;;;9158:56:78;;;;;;2851:9773:150;;:::i;5508:967:152:-;2851:9773:150;;:::i;:::-;;-1:-1:-1;;5666:17:152;-1:-1:-1;;5739:3:152;5718:12;;;;;2851:9773:150;;5714:23:152;;;;;5762:27;:15;;:27;:15;;:::i;:::-;;:27;2851:9773:150;;;;;5762:27:152;:89;;;5739:3;5758:305;;5739:3;6081:27;5762;6081:15;:12;;;:15;:::i;:27::-;:89;;;5739:3;6077:196;;5739:3;-1:-1:-1;2851:9773:150;;5699:13:152;;6077:196;6227:12;;;6202:56;6227:30;:25;:15;5718:12;6227;2851:9773:150;6227:12:152;;:15;:::i;:::-;;:25;;:30;;6202:56;:::i;:::-;6077:196;;;;;6081:89;5133:14;;;;2851:9773:150;;;6112:37:152;:25;:15;:12;;;:15;:::i;:::-;;:25;;2851:9773:150;-1:-1:-1;;;;;;2851:9773:150;;;6112:37:152;2851:9773:150;6112:58:152;6081:89;;5758:305;5910:12;;;;5881:68;5897:51;5910:30;:25;:15;:12;;;:15;:::i;:::-;;:25;;:30;;5897:51;:::i;5881:68::-;6012:12;5983:65;5999:48;5910:30;:25;6012:15;:12;;;:15;:::i;:::-;;:25;;:30;;5999:48;:::i;5983:65::-;5758:305;;;5762:89;2851:9773:150;;;;;;;5793:37:152;:25;:15;:12;;;:15;:::i;:37::-;2851:9773:150;5793:58:152;5762:89;;5714:23;;;;;;;;6321:14;;6317:46;;2851:9773:150;;:::i;:::-;;;;6381:87:152;;;2851:9773:150;5762:27:152;6381:87;;2851:9773:150;5508:967:152;:::o;6317:46::-;6344:19;;;-1:-1:-1;6344:19:152;;-1:-1:-1;6344:19:152;2851:9773:150;-1:-1:-1;;2851:9773:150;;;;;;;;:::o;:::-;-1:-1:-1;;2851:9773:150;;;;;;;;:::o;:::-;;;;;;;;;:::o;:::-;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;:::i;4484:686:78:-;4577:2;4573:6;;4577:2;;;4602:31;4619:13;4625:6;4602:31;4625:6;2851:9773:150;;;;4625:6:78;2851:9773:150;;;;4619:13:78;4577:2;2851:9773:150;;;;;;-1:-1:-1;;;;;;2851:9773:150;4602:31:78;;;2851:9773:150;;;;;;;;;4569:595:78;4658:7;4654:11;;4658:7;;;4688:51;4705:33;4715:22;4723:12;4724:6;4688:51;4724:6;2851:9773:150;;;;4723:12:78;2851:9773:150;;;;4715:22:78;2851:9773:150;;;8428:1:78;2851:9773:150;;;8428:1:78;2851:9773:150;;8422:19:78;8300:148;;4705:33;4577:2;2851:9773:150;;;;;;-1:-1:-1;;;;;;2851:9773:150;4688:51:78;;;2851:9773:150;;;;;;;;;4650:514:78;4764:7;4760:11;;4764:7;;;4794:51;4811:33;4821:22;4829:12;4830:6;4794:51;4830:6;2851:9773:150;;;;4830:6:78;4829:12;:::i;4821:22::-;2851:9773:150;8200:10:78;2851:9773:150;;;;;;;8195:21:78;8196:14;2851:9773:150;;;8168:49:78;2851:9773:150;;;;;;8279:7:78;2851:9773:150;;;8266:21:78;8046:248;;4811:33;4577:2;2851:9773:150;;;;;;-1:-1:-1;;;;;;2851:9773:150;4794:51:78;;;2851:9773:150;;;;;;;;;4756:408:78;4942:31;;4959:13;4902:85;4959:13;;:::i;:::-;4577:2;2851:9773:150;4942:31:78;;;;;;2851:9773:150;;;;;;;4942:31:78;4902:85;:::i;:::-;5117:36;5065:30;5071:23;5072:17;5073:10;2851:9773:150;;5073:10:78;:::i;:::-;2851:9773:150;;;;5072:17:78;5071:23;:::i;5065:30::-;5117:36;4577:2;2851:9773:150;5117:36:78;;;4942:31;5117:36;;;:::i;2129:778:58:-;2851:9773:150;;;2129:778:58;2319:2;2299:22;;2319:2;;2751:25;2535:196;;;;;;;;;;;;;;;-1:-1:-1;2535:196:58;2751:25;;:::i;:::-;2744:32;;;;;:::o;2295:606::-;2807:83;;2823:1;2807:83;2827:35;2807:83;;:::o;2851:9773:150:-;;-1:-1:-1;2851:9773:150;;;:::o;:::-;;;;;;;;;;;;7280:532:58;2851:9773:150;;;:::i;:::-;7366:29:58;;;7411:7;;:::o;7362:444::-;2851:9773:150;;;:::i;:::-;7471:29:58;7462:38;;7471:29;;7523:23;;;7375:20;7523:23;;7375:20;7523:23;7458:348;2851:9773:150;;;:::i;:::-;7576:35:58;7567:44;;7576:35;;7634:46;;;;7375:20;7634:46;7763:32;2851:9773:150;;7375:20:58;7634:46;7563:243;2851:9773:150;;7710:30:58;2851:9773:150;;:::i;:::-;7701:39:58;7697:109;;7563:243;7280:532::o;7697:109::-;7763:32;;;7375:20;7763:32;;2851:9773:150;;7375:20:58;7763:32;2851:9773:150;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;:::i;:::-;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;;;;;;;;;:::i;:::-;;;;:::i;:::-;;;;;;;;;;;;:::i;2305:494:148:-;2754:28;2696:44;2434:30;2408:384;2305:494;2434:30;2851:9773:150;;;;;;;;;;;;;;;-1:-1:-1;;;;;;2851:9773:150;2434:30:148;;;2851:9773:150;;;;;;;;;2434:30:148;2530:33;2478:38;2851:9773:150;2434:30:148;2498:17;;2851:9773:150;;;;;2478:38:148;2530:33;2851:9773:150;2547:15:148;;2851:9773:150;;;2530:33:148;;;2434:30;2530:33;;2851:9773:150;;;;;;;2530:33:148;2597:21;;;;2851:9773:150;2754:28:148;2771:10;2577:45;2851:9773:150;;;-1:-1:-1;;;;;2851:9773:150;;;2577:45:148;2656:25;2696:44;2851:9773:150;2636:46:148;2851:9773:150;2434:30:148;2656:25;;2851:9773:150;;;;;2636:46:148;2713:26;;2851:9773:150;;;2696:44:148;;;2434:30;2696:44;;2851:9773:150;;;;;;;2696:44:148;;2851:9773:150;;2696:44:148;;;;;;:::i;:::-;2771:10;2851:9773:150;;;2754:28:148;;;2434:30;2754:28;;2851:9773:150;;;;;;;2754:28:148;;2851:9773:150;;2754:28:148;;;;;;:::i;:::-;2408:384;:::i;12018:252:150:-;12130:20;;;12166:19;;:::o;12126:138::-;2851:9773;;;;;;;12216:37;:::o;3366:228:68:-;;;;3548:39;3366:228;3548:39;:::i;2089:399:71:-;2966:21:148;2216:11:71;;2851:9773:150;;;2966:21:148;2851:9773:150;;;;;;;2237:9:71;;2851:9773:150;-1:-1:-1;2851:9773:150;;2966:21:148;2264:48:71;2351:9;2851:9773:150;;;;;;1938:75:72;;;;2851:9773:150;;;2966:21:148;2392:39:71;;;:::i;:::-;2851:9773:150;;;;2966:21:148;2851:9773:150;;;;;;;;2089:399:71;:::o;:::-;;2216:11;;;2851:9773:150;;;;;;;;;;;2237:9:71;;2851:9773:150;-1:-1:-1;2851:9773:150;;2268:8:71;;2264:48;;2216:11;2351:9;;2851:9773:150;;;;;;1938:75:72;;;;2851:9773:150;;;2392:39:71;;;;:::i;:::-;2851:9773:150;;;;;;;;;;;;2089:399:71;:::o;2264:48::-;2851:9773:150;;;;;;;;;:::i;:::-;-1:-1:-1;2851:9773:150;;2292:9:71;:::o;2851:9773:150:-;;;;;;;;;;;;:::o;:::-;;;;:::o;:::-;;;-1:-1:-1;;;2851:9773:150;;;;;;;;;;;;;;;;;-1:-1:-1;;;2851:9773:150;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;:::o;1205:1510:78:-;1323:20;;;:::i;:::-;2851:9773:150;;;;1453:9:78;;-1:-1:-1;1501:14:78;;-1:-1:-1;2851:9773:150;;;;1509:6:78;2851:9773:150;1449:1238:78;1579:1;1571:9;;1579:1;;1634:20;1782:11;1634:20;1782:11;1787:6;1740:7;1692:13;1634:20;1842:9;1634:20;;:::i;1692:13::-;2851:9773:150;;;;;;1740:7:78;1787:6;2851:9773:150;;;;;;1782:11:78;;;:::i;1567:1120::-;1880:1;1872:9;;1880:1;;1943:20;2275:8;1943:20;2243:16;2851:9773:150;1943:20:78;2194:16;1943:20;;2336:11;1943:20;;:::i;:::-;2013;;2118:15;2013:20;2058;2013;;;:::i;:::-;2058;;:::i;:::-;2851:9773:150;;;;;;;;2118:15:78;2851:9773:150;;2105:29:78;;2851:9773:150;;;;;;;2194:16:78;2188:23;;2851:9773:150;;;;;;;2243:16:78;2237:23;2851:9773:150;;;;;;1868:819:78;2376:1;2368:9;2376:1;;2575:34;2450:6;2851:9773:150;2449:12:78;2450:6;2589:19;2450:6;2851:9773:150;;;;;;;2450:6:78;2449:12;:::i;:::-;2851:9773:150;2503:6:78;2495:59;2508:1;2503:6;;;2495:59;:::i;:::-;2589:19;:::i;:::-;2575:34;:::i;2364:323::-;2851:9773:150;;-1:-1:-1;;;2640:36:78;;2851:9773:150;1393:1:78;2640:36;;2851:9773:150;;;;;;;;;;;;;2640:36:78;2851:9773:150;;;;;;;;;;;;;:::o;1564:269:71:-;1649:11;;;2851:9773:150;;;1663:1:71;2851:9773:150;;;;;;;1667:9:71;;2851:9773:150;-1:-1:-1;1645:87:71;;1758:9;2851:9773:150;;-1:-1:-1;;;;;;2851:9773:150;1758:22:71;;2851:9773:150;1758:22:71;:::i;:::-;2851:9773:150;;;;;1791:16:71;2851:9773:150;;1791:16:71;:::i;:::-;2851:9773:150;;1564:269:71;:::o;1645:87::-;2851:9773:150;;-1:-1:-1;;;1699:22:71;;1649:11;1699:22;;;2851:9773:150;;;;;;-1:-1:-1;;;2851:9773:150;;;;1699:22:71;;;4445:308:148;2851:9773:150;;:::i;:::-;;2216:11:71;;;2851:9773:150;;;4586:1:148;2851:9773:150;;;;;;;2237:9:71;;2851:9773:150;-1:-1:-1;2851:9773:150;;2216:11:71;2351:9;;2851:9773:150;;;;;;1938:75:72;;;;2851:9773:150;;;4586:1:148;2392:39:71;;;:::i;:::-;2851:9773:150;;;;4586:1:148;2851:9773:150;;;;;;;;4590:1:148;5413:13:71;4590:1:148;5408:106:71;5428:5;4586:1:148;5428:5:71;;;;4619:35:148;;;;4684:25;4619:35;;:::i;:::-;4684:25;;:::i;:::-;4726:20;2851:9773:150;;:::i;:::-;-1:-1:-1;;;;;;2851:9773:150;;;;;;4726:20:148;2216:11:71;4726:20:148;;2851:9773:150;4445:308:148;:::o;5435:3:71:-;2851:9773:150;;-1:-1:-1;;;;;;5468:16:71;2851:9773:150;;5468:16:71;:::i;:::-;2851:9773:150;;;;;;;;;;5501:1:71;2851:9773:150;;;;;;;;;-1:-1:-1;;;;;;2851:9773:150;;;;;;5454:49:71;5435:3;2851:9773:150;5413:13:71;;;3313:349;2851:9773:150;;5910:30:152;3466:31:71;2851:9773:150;;5910:30:152;3512:8:71;3617:38;3512:8;1938:75:72;3617:38:71;:::i;3313:349::-;;2851:9773:150;;3466:31:71;;2851:9773:150;;3512:8:71;;3508:48;;1938:75:72;3617:38:71;2851:9773:150;1938:75:72;3617:38:71;:::i;3508:48::-;2851:9773:150;;;;;;;;:::i;2744:313:71:-;2851:9773:150;;2876:25:71;5910:30:152;2876:25:71;2851:9773:150;;-1:-1:-1;;2851:9773:150;;;;;;;;;;;5910:30:152;1938:75:72;2851:9773:150;-1:-1:-1;2851:9773:150;;3012:38:71;;;:::i;2851:9773:150:-;;;;;-1:-1:-1;;2851:9773:150;;:::o;823:320:78:-;2851:9773:150;;;;;961:5:78;;;1123:13;;823:320;:::o;968:3::-;2851:9773:150;;-1:-1:-1;;2851:9773:150;;;;;;;;1051:11:78;;;;:::i;:::-;2851:9773:150;;;;;;;;;-1:-1:-1;;;;;2851:9773:150;;;;;;;;;;1060:1:78;2851:9773:150;;1037:66:78;2851:9773:150;;;;;;;;;;;;;;968:3:78;1012:91;;;;:::i;:::-;968:3;;:::i;:::-;936:23;;;;5627:1354;2851:9773:150;5873:1:78;2851:9773:150;;5873:1:78;2851:9773:150;6063:110:78;6191:86;;6064;;;;;;;2851:9773:150;;6190:110:78;6191:86;;;;2851:9773:150;;6062:239:78;6511:66;6384;6364:86;;2851:9773:150;;6490:110:78;6491:86;2851:9773:150;;6362:239:78;6811:66;6684;;6664:86;;2851:9773:150;;6790:110:78;6791:86;2851:9773:150;;6662:239:78;6965:8;2851:9773:150;;;6965:8:78;2851:9773:150;;;;6965:8:78;6951:23;5627:1354;:::o;6202:380:71:-;2851:9773:150;;-1:-1:-1;;2851:9773:150;;;;;;;6414:3:71;-1:-1:-1;;;;;;6437:7:71;;;;:::i;:::-;2851:9773:150;;6433:86:71;;6414:3;;;:::i;:::-;6382:22;;6433:86;6403:1;2851:9773:150;;;;;;;6546:29:71;;;:::i;5203:1551:58:-;;;6283:66;6270:79;;6266:164;;2851:9773:150;;;;;;-1:-1:-1;2851:9773:150;;;;;;;;;;;;;;;;;;;6541:24:58;;;;;;;;;-1:-1:-1;6541:24:58;-1:-1:-1;;;;;2851:9773:150;;6579:20:58;6575:113;;6698:49;-1:-1:-1;6698:49:58;-1:-1:-1;5203:1551:58;:::o;6575:113::-;6615:62;-1:-1:-1;6615:62:58;6541:24;6615:62;-1:-1:-1;6615:62:58;:::o;6541:24::-;2851:9773:150;;;;;;;;;6266:164:58;6365:54;;;6381:1;6365:54;6385:30;6365:54;;:::o;7197:131:69:-;7277:1;7272:6;;;7268:20;;-1:-1:-1;;2851:9773:150;;;;;;;7305:12:69;;;:::i;:::-;7277:1;2851:9773:150;;;;;;;7197:131:69;:::o;7268:20::-;7280:8;2851:9773:150;7280:8:69;:::o;5056:1349::-;;;;2851:9773:150;;;5309:1063:69;5332:1;5316:12;;;:::i;:::-;2851:9773:150;5316:17:69;;;5369:45;:41;5381:12;5332:1;5375:19;5381:12;;;:::i;:::-;2851:9773:150;5375:19:69;:::i;:::-;2851:9773:150;5369:41:69;:::i;:::-;:45;:::i;:::-;5428:9;2851:9773:150;5457:9:69;2851:9773:150;5452:836:69;5468:7;;;;;;6302;;;6339:16;6338:23;6302:7;6339:16;;:::i;:::-;2851:9773:150;;;;6338:23:69;5309:1063;;;5457:9;5510:12;;;;;;;:::i;:::-;2851:9773:150;5563:5:69;;;;:::i;:::-;:11;:44;;;5457:9;5719:442;;;5786:9;6230:8;5332:1;5786:9;5850:1;5771:40;5786:9;;;;:::i;:::-;2851:9773:150;5797:13:69;5804:5;;;:::i;:::-;5797:13;;:::i;:::-;2851:9773:150;5771:40:69;;;:::i;:::-;2851:9773:150;;5719:442:69;;6179:18;;;;:::i;:::-;2851:9773:150;;;;;6230:8:69;6215:23;;;;:::i;:::-;2851:9773:150;;5457:9:69;;;;;5719:442;5647:7;;5332:1;5647:7;;5646:22;-1:-1:-1;5646:22:69;;5943:9;5332:1;5943:9;5928:35;6230:8;5943:9;5954:8;5943:9;;;;;;;;;:::i;:::-;2851:9773:150;5954:8:69;;:::i;:::-;2851:9773:150;5928:35:69;;;:::i;:::-;2851:9773:150;;;;5878:283:69;5719:442;;5878:283;6230:8;6063:9;;;5332:1;6063:9;;;;;;:::i;:::-;2851:9773:150;;;5878:283:69;5719:442;;5563:44;5588:5;5578:16;5588:5;;;:::i;:::-;5578:16;;:::i;:::-;2851:9773:150;5332:1:69;5599:7;;5578:29;5563:44;;5316:17;;;;;;6389:9;5316:17;;6389:9;:::i;:::-;2851:9773:150;5056:1349:69;:::o;4448:2801:68:-;;;;4610:14;;4606:38;;2851:9773:150;;4702:14:68;;;4698:40;;4810:1;4813:13;;;;;;5012:14;4810:1;5012:14;;:32;;;4793:159;5012:56;;;4793:159;5008:169;;5235:35;5249:20;;;:::i;:::-;5235:35;:::i;:::-;2851:9773:150;;;:::i;:::-;;;;;5219:52:68;;;2851:9773:150;;;;;:::i;:::-;;;;;5219:52:68;5313:22;;2851:9773:150;;;;;:::i;:::-;;;;;5219:52:68;5376:34;;2851:9773:150;;;;5376:34:68;;2851:9773:150;;5455:29:68;5494:940;5501:14;;;5494:940;2851:9773:150;;;;6490:52:68;;2851:9773:150;6642:14:68;;2851:9773:150;6622:41:68;6618:71;;2851:9773:150;;-1:-1:-1;;2851:9773:150;;;6763:445:68;2851:9773:150;;6770:21:68;;;6823:20;;;:::i;:::-;6872;;;:::i;:::-;2851:9773:150;6934:18:68;2851:9773:150;;;;;;6934:18:68;2851:9773:150;;;7006:139:68;5219:52;7006:139;2851:9773:150;;7006:139:68;7158:39;:14;;2851:9773:150;;7158:39:68;;:::i;:::-;2851:9773:150;6763:445:68;;6770:21;;7225:17;6770:21;;7225:14;:17;:::i;6618:71::-;5114:17;;;2851:9773:150;6672:17:68;;2851:9773:150;6672:17:68;6490:52;6523:19;;;2851:9773:150;6523:19:68;;2851:9773:150;6523:19:68;5494:940;5548:16;;;:::i;:::-;2851:9773:150;5663:31:68;5625:24;4810:1;2851:9773:150;;5625:24:68;;;:::i;:::-;5663:31;;;;:::i;:::-;5745:42;;;;;;;:::i;:::-;5806:20;5219:52;5806:20;;2851:9773:150;5806:25:68;;5802:622;5806:25;;;5855:14;;;;;;;;2851:9773:150;;;5855:41:68;5851:174;5880:16;5920:5;5880:16;5989;;;;;:::i;:::-;;;:::i;:::-;5494:940;;5802:622;4810:1;6049:25;:40;;;5802:622;6045:379;;;6126:18;;;5219:52;6126:40;:18;2851:9773:150;6126:45:68;:18;;;2851:9773:150;;6126:40:68;;:::i;:::-;;:45;2851:9773:150;6126:45:68;;:::i;6045:379::-;2851:9773:150;6353:55:68;2851:9773:150;;6282:1:68;6353:55;2851:9773:150;;;6353:55:68;;:::i;6049:40::-;6078:11;;;6049:40;;5008:169;2851:9773:150;;;;;;5084:47:68;;5152:9;:14;:9;;:::i;:::-;;:14;2851:9773:150;5145:21:68;:::o;5012:56::-;5048:9;;;;:::i;:::-;;2851:9773:150;5048:20:68;5012:56;;:32;5030:14;4810:1;5030:14;;5012:32;;4798:13;4847:9;;;;:::i;:::-;;2851:9773:150;4866:13:68;4873:5;;;:::i;:::-;4866:13;;:::i;:::-;;2851:9773:150;-1:-1:-1;4847:38:68;4843:67;;4810:1;2851:9773:150;4798:13:68;;4698:40;4725:13;;;2851:9773:150;4725:13:68;;2851:9773:150;4725:13:68;2284:287:72;;;;2851:9773:150;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;-1:-1:-1;;2851:9773:150;;;;2560:3:72;;;3244:193;3251:16;2851:9773:150;3251:16:72;;;;;3494:8;;;-1:-1:-1;;;2851:9773:150;3494:132:72;3636:173;;;;;;;;;;;2284:287::o;3494:132::-;3598:24;3606:15;3598:28;3606:15;;:::i;:::-;3598:24;:::i;:28::-;3494:132;;;3269:16;3301:65;;;;;;2851:9773:150;;;;;;;;;;;;;;;;;3269:16:72;-1:-1:-1;;2851:9773:150;;;;;;3244:193:72;2851:9773:150;;:::i;7390:537:69:-;7459:462;;;;;;;;-1:-1:-1;;;;;7459:462:69;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;7390:537;:::o;6686:471::-;6806:1;6800:7;6806:1;;2851:9773:150;6829:141:69;;;;2851:9773:150;6829:141:69;6795:356;6686:471::o;6795:356::-;7000:141;2851:9773:150;7000:141:69;;;;2851:9773:150;7000:141:69;6795:356;6686:471::o;13095:196:68:-;;13147:13;2851:9773:150;13172:113:68;13179:6;;;13095:196;:::o;13172:113::-;2851:9773:150;-1:-1:-1;;2851:9773:150;;;;;;;13210:1:68;13201:10;;2851:9773:150;;;;13172:113:68;;;2851:9773:150;;;;;;;:::i;:::-;;;;-1:-1:-1;2851:9773:150;;-1:-1:-1;2851:9773:150;;;;;;:::o;7670:742:68:-;2851:9773:150;;:::i;:::-;;;;7846:33:68;7864:15;;;2851:9773:150;;;7846:33:68;;:::i;:::-;2851:9773:150;;;7935:210:68;7942:15;;;;;;7935:210;2851:9773:150;;;;8175:27:68;8348:28;2851:9773:150;;;8175:27:68;;:::i;:::-;2851:9773:150;;;8287:13:68;7998;8287;;;2851:9773:150;;:::i;:::-;;;;8246:55:68;7864:15;8246:55;;2851:9773:150;7998:13:68;8246:55;;2851:9773:150;;;;8348:28:68;:::i;7935:210::-;7998:13;:24;:13;;;;;:24;:::i;:::-;;2851:9773:150;7978:50:68;;;7974:94;;2851:9773:150;;7935:210:68;;;7974:94;8048:5;;;9445:1927;;;9640:15;;;2851:9773:150;;;9737:21:68;;;;:::i;:::-;9794;;;;:::i;:::-;9830:9;2851:9773:150;9841:10:68;;;;;;10071:20;;;;10182:1157;;2851:9773:150;10189:12:68;;;:::i;:::-;2851:9773:150;10189:17:68;;;2851:9773:150;10248:9:68;2851:9773:150;10272:851:68;10279:7;;;;;;11204:10;;;11228:101;;;;;10182:1157;;10272:851;10320:12;;;;;;:::i;:::-;2851:9773:150;10355:5:68;;;;;;:::i;:::-;:11;:44;;;10272:851;10351:758;;;2851:9773:150;10496:9:68;10481:40;10496:9;10507:13;10514:5;10496:9;10677:6;10496:9;;10561:8;10496:9;;:::i;:::-;2851:9773:150;10514:5:68;;:::i;10507:13::-;2851:9773:150;10481:40:68;;;:::i;:::-;10466:55;;;;:::i;10561:8::-;10543:26;;;;:::i;:::-;2851:9773:150;;10677:6:68;;:::i;:::-;10351:758;10272:851;;10351:758;2851:9773:150;;;;9640:15:68;10801:14;;;2851:9773:150;-1:-1:-1;10781:41:68;10777:70;;2851:9773:150;10899:9:68;10967:8;10899:9;;10884:43;10899:9;;;;:::i;:::-;2851:9773:150;10910:16:68;;;:::i;:::-;10884:43;;;:::i;10967:8::-;10949:26;;;;:::i;:::-;2851:9773:150;;;;10351:758:68;10272:851;;10777:70;10831:16;;;2851:9773:150;10831:16:68;;2851:9773:150;10831:16:68;10355:44;10380:5;10370:16;10380:5;;;:::i;:::-;10370:16;;:::i;:::-;2851:9773:150;;10391:7:68;;10370:29;10355:44;;10189:17;;;;;11356:9;10189:17;11356:9;:::i;9830:::-;2851:9773:150;9880:13:68;9985:25;9880:13;;;9640:15;9880:25;:13;;9894:10;;;;:::i;:::-;9880:25;;:::i;:::-;;:30;2851:9773:150;9868:42:68;;;;:::i;:::-;2851:9773:150;9985:13:68;2851:9773:150;;;9985:25:68;;:::i;:::-;;2851:9773:150;;;9952:64:68;;;;:::i;:::-;2851:9773:150;;9830:9:68;;11444:188;;11527:37;:13;;;;2851:9773:150;;11527:37:68;;:::i;:::-;2851:9773:150;;;;;;;11444:188:68:o;11706:222::-;11808:30;:13;;;;2851:9773:150;;11808:30:68;;:::i;:::-;2851:9773:150;;;;;;;;11706:222:68;:::o;12000:226::-;12106:30;:13;;;;2851:9773:150;;12106:30:68;;:::i;:::-;2851:9773:150;;;-1:-1:-1;;2851:9773:150;;;;;12000:226:68:o;713:2:72:-;;;;;;;;;:::o","linkReferences":{}},"methodIdentifiers":{"MMR_ROOT_PAYLOAD_ID()":"af8b91d6","noOp((uint256,uint256,(uint64,uint32,bytes32),(uint64,uint32,bytes32)),(((((bytes2,bytes)[],uint32,uint64),(bytes,uint256)[]),(uint8,uint32,bytes32,(uint64,uint32,bytes32),bytes32,uint256),bytes32[],bytes32[]),((uint256,uint256,bytes)[],bytes32[],uint256)))":"7163f312","supportsInterface(bytes4)":"01ffc9a7","verify(bytes,bytes)":"f7e83aee","verifyConsensus(bytes,bytes)":"7d755598"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"name\":\"ECDSAInvalidSignature\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"length\",\"type\":\"uint256\"}],\"name\":\"ECDSAInvalidSignatureLength\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"ECDSAInvalidSignatureS\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyTree\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyTree\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"IllegalGenesisBlock\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAuthoritiesProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidMmrProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"LeafIndexOutOfBounds\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MmrRootHashMissing\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OutOfBoundsLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ProofExhausted\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SuperMajorityRequired\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"TimestampNotFound\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnconsumedProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnknownAuthoritySet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnsortedLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnsortedLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MMR_ROOT_PAYLOAD_ID\",\"outputs\":[{\"internalType\":\"bytes2\",\"name\":\"\",\"type\":\"bytes2\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"latestHeight\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"beefyActivationBlock\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint64\",\"name\":\"id\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"len\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"internalType\":\"struct AuthoritySetCommitment\",\"name\":\"currentAuthoritySet\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint64\",\"name\":\"id\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"len\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"internalType\":\"struct AuthoritySetCommitment\",\"name\":\"nextAuthoritySet\",\"type\":\"tuple\"}],\"internalType\":\"struct BeefyConsensusState\",\"name\":\"s\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"components\":[{\"components\":[{\"components\":[{\"internalType\":\"bytes2\",\"name\":\"id\",\"type\":\"bytes2\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"internalType\":\"struct Payload[]\",\"name\":\"payload\",\"type\":\"tuple[]\"},{\"internalType\":\"uint32\",\"name\":\"blockNumber\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"validatorSetId\",\"type\":\"uint64\"}],\"internalType\":\"struct Commitment\",\"name\":\"commitment\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"authorityIndex\",\"type\":\"uint256\"}],\"internalType\":\"struct Vote[]\",\"name\":\"votes\",\"type\":\"tuple[]\"}],\"internalType\":\"struct SignedCommitment\",\"name\":\"signedCommitment\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"},{\"internalType\":\"uint32\",\"name\":\"parentNumber\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"parentHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"uint64\",\"name\":\"id\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"len\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"internalType\":\"struct AuthoritySetCommitment\",\"name\":\"nextAuthoritySet\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"extra\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"leafIndex\",\"type\":\"uint256\"}],\"internalType\":\"struct BeefyMmrLeaf\",\"name\":\"latestMmrLeaf\",\"type\":\"tuple\"},{\"internalType\":\"bytes32[]\",\"name\":\"mmrProof\",\"type\":\"bytes32[]\"},{\"internalType\":\"bytes32[]\",\"name\":\"proof\",\"type\":\"bytes32[]\"}],\"internalType\":\"struct RelayChainProof\",\"name\":\"relay\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"header\",\"type\":\"bytes\"}],\"internalType\":\"struct Parachain[]\",\"name\":\"parachains\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes32[]\",\"name\":\"proof\",\"type\":\"bytes32[]\"},{\"internalType\":\"uint256\",\"name\":\"leafCount\",\"type\":\"uint256\"}],\"internalType\":\"struct ParachainProof\",\"name\":\"parachain\",\"type\":\"tuple\"}],\"internalType\":\"struct BeefyConsensusProof\",\"name\":\"p\",\"type\":\"tuple\"}],\"name\":\"noOp\",\"outputs\":[],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"previousState\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"proof\",\"type\":\"bytes\"}],\"name\":\"verify\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"stateMachineId\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"height\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"overlayRoot\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"stateRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct StateCommitment\",\"name\":\"commitment\",\"type\":\"tuple\"}],\"internalType\":\"struct IntermediateState[]\",\"name\":\"\",\"type\":\"tuple[]\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"encodedState\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"encodedProof\",\"type\":\"bytes\"}],\"name\":\"verifyConsensus\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"stateMachineId\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"height\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"overlayRoot\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"stateRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct StateCommitment\",\"name\":\"commitment\",\"type\":\"tuple\"}],\"internalType\":\"struct IntermediateState[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"stateMutability\":\"pure\",\"type\":\"function\"}],\"devdoc\":{\"author\":\"Polytope Labs (hello@polytope.technology)\",\"details\":\"The verification flow is: 1. Confirm the commitment's validator set id matches a known authority set. 2. Verify that enough signatures are present to meet the supermajority threshold. 3. Recover signer addresses via ecrecover and verify their membership in the authority set via a merkle multi-proof against the authority set root. 4. Extract the MMR root from the commitment payload and verify the latest MMR leaf inclusion via a merkle mountain range proof. 5. Verify parachain header inclusion in the MMR leaf's parachain heads root. 6. Decode each parachain header to extract finalized state commitments. Stale proofs (commitment block number <= trusted latest height) are treated as no-ops.\",\"errors\":{\"ECDSAInvalidSignature()\":[{\"details\":\"The signature derives the `address(0)`.\"}],\"ECDSAInvalidSignatureLength(uint256)\":[{\"details\":\"The signature has an invalid length.\"}],\"ECDSAInvalidSignatureS(bytes32)\":[{\"details\":\"The signature has an S value that is in the upper half order.\"}]},\"kind\":\"dev\",\"methods\":{\"supportsInterface(bytes4)\":{\"details\":\"See {IERC165-supportsInterface}.\"},\"verify(bytes,bytes)\":{\"details\":\"IConsensusV2 entry point. Decodes the proof, verifies consensus, and returns the updated state along with the latest authority set id.\"},\"verifyConsensus(bytes,bytes)\":{\"details\":\"IConsensus entry point. Decodes the proof and verifies consensus.\"}},\"title\":\"The ECDSA BEEFY Consensus Client.\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"notice\":\"Verifies BEEFY consensus proofs by checking a 2/3+1 supermajority of secp256k1 signatures on-chain, along with merkle multi-proofs of authority set membership. This is the most gas-expensive verifier but requires no off-chain proving infrastructure.\",\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/consensus/EcdsaBeefy.sol\":\"EcdsaBeefy\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@hyperbridge/core/=node_modules/@hyperbridge/core/contracts/\",\":@openzeppelin/=node_modules/@openzeppelin/\",\":@polytope-labs/=node_modules/@polytope-labs/\",\":@sp1-contracts/=lib/sp1-contracts/contracts/src/\",\":@uniswap/=node_modules/@uniswap/\",\":ds-test/=lib/solidity-stringutils/lib/ds-test/src/\",\":erc4626-tests/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=node_modules/forge-std/src/\",\":openzeppelin-contracts/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/\",\":solidity-stringutils/=lib/solidity-stringutils/\",\":sp1-contracts/=lib/sp1-contracts/contracts/\",\":stringutils/=lib/solidity-stringutils/src/\"],\"viaIR\":true},\"sources\":{\"node_modules/@hyperbridge/core/contracts/interfaces/IConsensus.sol\":{\"keccak256\":\"0x56b093b9ca6913da8d12c54beca9ba8d970c1a1a0cf39838c2c341cd4ef23afb\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://6be3495c82c3c5dd95ccc5855289b5e7079a419a9b0d09af843344ac86b6d286\",\"dweb:/ipfs/QmNQZugSBtnVcggYqnTXdcsZV3nLkGTK4VEXCnYX3fGKpV\"]},\"node_modules/@hyperbridge/core/contracts/interfaces/IConsensusV2.sol\":{\"keccak256\":\"0xa680bb1b902d419b862d155c49ae6fd2371e9b8a0a42d67cf07a6b4306300b90\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://df9f403774a0fb637e331e23a599aa1cd3dc2a70baef0ebdd3b7dd4676dab81a\",\"dweb:/ipfs/QmfA14ersZ1D88DkxEaK3krNs7XsCYe5UTmTANKcGB8tE4\"]},\"node_modules/@hyperbridge/core/contracts/libraries/StateMachine.sol\":{\"keccak256\":\"0x860289ae856ea354df5cca2131612da5c15a129fc14ae5ac0d528e0ce7c809c3\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://2ab5a7d6463b634da1e05e2b06818416a6f2d59b5c10815cc74bf26d003c2b95\",\"dweb:/ipfs/QmZ4dpEoDhNefAcKwKBXumLfSHqxhcBctErTzMvvPHXY5G\"]},\"node_modules/@openzeppelin/contracts/utils/Panic.sol\":{\"keccak256\":\"0xf7fe324703a64fc51702311dc51562d5cb1497734f074e4f483bfb6717572d7a\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c6a5ff4f9fd8649b7ee20800b7fa387d3465bd77cf20c2d1068cd5c98e1ed57a\",\"dweb:/ipfs/QmVSaVJf9FXFhdYEYeCEfjMVHrxDh5qL4CGkxdMWpQCrqG\"]},\"node_modules/@openzeppelin/contracts/utils/Strings.sol\":{\"keccak256\":\"0xad148d59f05165f9217d0a9e1ac8f772abb02ea6aaad8a756315c532bf79f9f4\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://15e3599867c2182f5831e9268b274b2ef2047825837df6b4d81c9e89254b093e\",\"dweb:/ipfs/QmZbL7XAYr5RmaNaooPgZRmcDXaudfsYQfYD9y5iAECvpS\"]},\"node_modules/@openzeppelin/contracts/utils/cryptography/ECDSA.sol\":{\"keccak256\":\"0x69f54c02b7d81d505910ec198c11ed4c6a728418a868b906b4a0cf29946fda84\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://8e25e4bdb7ae1f21d23bfee996e22736fc0ab44cfabedac82a757b1edc5623b9\",\"dweb:/ipfs/QmQdWQvB6JCP9ZMbzi8EvQ1PTETqkcTWrbcVurS7DKpa5n\"]},\"node_modules/@openzeppelin/contracts/utils/introspection/ERC165.sol\":{\"keccak256\":\"0x2d9dc2fe26180f74c11c13663647d38e259e45f95eb88f57b61d2160b0109d3e\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://81233d1f98060113d9922180bb0f14f8335856fe9f339134b09335e9f678c377\",\"dweb:/ipfs/QmWh6R35SarhAn4z2wH8SU456jJSYL2FgucfTFgbHJJN4E\"]},\"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol\":{\"keccak256\":\"0x8891738ffe910f0cf2da09566928589bf5d63f4524dd734fd9cedbac3274dd5c\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://971f954442df5c2ef5b5ebf1eb245d7105d9fbacc7386ee5c796df1d45b21617\",\"dweb:/ipfs/QmadRjHbkicwqwwh61raUEapaVEtaLMcYbQZWs9gUkgj3u\"]},\"node_modules/@openzeppelin/contracts/utils/math/Math.sol\":{\"keccak256\":\"0x1225214420c83ebcca88f2ae2b50f053aaa7df7bd684c3e878d334627f2edfc6\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://6c5fab4970634f9ab9a620983dc1c8a30153981a0b1a521666e269d0a11399d3\",\"dweb:/ipfs/QmVRnBC575MESGkEHndjujtR7qub2FzU9RWy9eKLp4hPZB\"]},\"node_modules/@openzeppelin/contracts/utils/math/SafeCast.sol\":{\"keccak256\":\"0x195533c86d0ef72bcc06456a4f66a9b941f38eb403739b00f21fd7c1abd1ae54\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://b1d578337048cad08c1c03041cca5978eff5428aa130c781b271ad9e5566e1f8\",\"dweb:/ipfs/QmPFKL2r9CBsMwmUqqdcFPfHZB2qcs9g1HDrPxzWSxomvy\"]},\"node_modules/@openzeppelin/contracts/utils/math/SignedMath.sol\":{\"keccak256\":\"0xb1970fac7b64e6c09611e6691791e848d5e3fe410fa5899e7df2e0afd77a99e3\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://db5fbb3dddd8b7047465b62575d96231ba8a2774d37fb4737fbf23340fabbb03\",\"dweb:/ipfs/QmVUSvooZKEdEdap619tcJjTLcAuH6QBdZqAzWwnAXZAWJ\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMountainRange.sol\":{\"keccak256\":\"0x014237038bb77bdf371b50c1268d02bf7eeaf7068c35483d5d0684ca7c30d544\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://beb3ae60094f49716a2b0cfa6229ee27af1778934cf5f2dfc704433fda5a687b\",\"dweb:/ipfs/QmQ339wEFT9X1woHyFTDbbhUCx8ekD9MGLRnhS3PpYp1pm\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMultiProof.sol\":{\"keccak256\":\"0xd4f6e6a9eceaa7d1cdf9684bfe7f3f552adf21dfcf7f1372f7943a4c2f15deb7\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://ec900b3e79ea8ef9d275697937d1984e994d10117aa2be066bd73f6a31aedf10\",\"dweb:/ipfs/QmZQGLiJtbvUSEW5H2HEenzxp58fN43XHRY3SoNTC9Hobg\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Bytes.sol\":{\"keccak256\":\"0xd305383358b93285d8fcee512795487484eadfd7b602df16ff4e9b01afbefec7\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://57c86cd2fe6ab591264c5632d503d77f04748d722e4defa9c2badb238ee1f9bd\",\"dweb:/ipfs/QmTZ6xTcaYkRBshq6YZVMx2kkk94ZtJsRM6xLpC5qWT3oA\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Memory.sol\":{\"keccak256\":\"0x59e3a56caa42c1aac30231173439817d38c7f359e40bd36e9bb418d3f82ceab7\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://e060fed46c6b420624166ea02a326f0f566897941bdc322257e690ae134b8179\",\"dweb:/ipfs/QmbXW8yG2ZntjLMyUEmQGcRZroZj4dVZkBhHxK2PFKfUKB\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Node.sol\":{\"keccak256\":\"0xca611969a68f7fe63dcdc742c9caf9bc1b26495561b68f4676c209279a4576ba\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://17b7ec2cf65d484f0a2c188a7ae33430b015b3a55bf10393167f53839e2dde56\",\"dweb:/ipfs/QmTQbT8J7rBRNHFXSR7S4KbpwCteMrFt8mvrzCD1vYYTwX\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/polkadot/ScaleCodec.sol\":{\"keccak256\":\"0x9ac4df46e68718f7deaaa5b7443778533f53dc0ff3736cc386cf4991099da2aa\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://85d08b00d9173358323105be975ce85b7e206ba52df2d80a14d2581dec1ddd11\",\"dweb:/ipfs/QmQCqVvSdJUqfhLH8TRBGNDGiEzkGduhRacnYhoKcm7e2a\"]},\"src/consensus/Codec.sol\":{\"keccak256\":\"0xfbda8d0aef81312f23b53eb34ba4fcd19ab886cbd010a0f100d86271eecee126\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://bc4a4b1a88f702efa6727c368894b8ac13b69769efc81355b71a9a0d7a2efd7b\",\"dweb:/ipfs/QmYwLJgyCr4pPRyyk2oLYMLq5QGRhCSZKLYUEdW6U2SBzx\"]},\"src/consensus/EcdsaBeefy.sol\":{\"keccak256\":\"0x143e08b4250eefd6a9000466df7d5e8ca13f37f19c352be92875bd8bd364cd7b\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://88dc9f7f539608ca28479c3f937a0583d39dcfc698c001b65a478320dd8e1790\",\"dweb:/ipfs/QmaD4DbxtRGCApbQtuovPwBwRt2PZEVrRLck3vFyejkhrH\"]},\"src/consensus/Types.sol\":{\"keccak256\":\"0xcd1866064ae11c71575fefa857a7317d10e18da0c204c09ae42308c4c13a0268\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://c083e149c4607f2cd5b769c1a624bbc5a861e8d5c485dd2ce06a0e23f7e762dd\",\"dweb:/ipfs/QmPMSM8S2sav9t4X2peHJnpTQhowEgEW5phYoiy3LbGAsv\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[{"inputs":[],"type":"error","name":"ECDSAInvalidSignature"},{"inputs":[{"internalType":"uint256","name":"length","type":"uint256"}],"type":"error","name":"ECDSAInvalidSignatureLength"},{"inputs":[{"internalType":"bytes32","name":"s","type":"bytes32"}],"type":"error","name":"ECDSAInvalidSignatureS"},{"inputs":[],"type":"error","name":"EmptyLeaves"},{"inputs":[],"type":"error","name":"EmptyTree"},{"inputs":[],"type":"error","name":"EmptyTree"},{"inputs":[],"type":"error","name":"IllegalGenesisBlock"},{"inputs":[],"type":"error","name":"InvalidAuthoritiesProof"},{"inputs":[],"type":"error","name":"InvalidMmrProof"},{"inputs":[],"type":"error","name":"LeafIndexOutOfBounds"},{"inputs":[],"type":"error","name":"MmrRootHashMissing"},{"inputs":[],"type":"error","name":"OutOfBoundsLeaves"},{"inputs":[],"type":"error","name":"ProofExhausted"},{"inputs":[],"type":"error","name":"SuperMajorityRequired"},{"inputs":[],"type":"error","name":"TimestampNotFound"},{"inputs":[],"type":"error","name":"UnconsumedProof"},{"inputs":[],"type":"error","name":"UnknownAuthoritySet"},{"inputs":[],"type":"error","name":"UnsortedLeaves"},{"inputs":[],"type":"error","name":"UnsortedLeaves"},{"inputs":[],"stateMutability":"view","type":"function","name":"MMR_ROOT_PAYLOAD_ID","outputs":[{"internalType":"bytes2","name":"","type":"bytes2"}]},{"inputs":[{"internalType":"struct BeefyConsensusState","name":"s","type":"tuple","components":[{"internalType":"uint256","name":"latestHeight","type":"uint256"},{"internalType":"uint256","name":"beefyActivationBlock","type":"uint256"},{"internalType":"struct AuthoritySetCommitment","name":"currentAuthoritySet","type":"tuple","components":[{"internalType":"uint64","name":"id","type":"uint64"},{"internalType":"uint32","name":"len","type":"uint32"},{"internalType":"bytes32","name":"root","type":"bytes32"}]},{"internalType":"struct AuthoritySetCommitment","name":"nextAuthoritySet","type":"tuple","components":[{"internalType":"uint64","name":"id","type":"uint64"},{"internalType":"uint32","name":"len","type":"uint32"},{"internalType":"bytes32","name":"root","type":"bytes32"}]}]},{"internalType":"struct BeefyConsensusProof","name":"p","type":"tuple","components":[{"internalType":"struct RelayChainProof","name":"relay","type":"tuple","components":[{"internalType":"struct SignedCommitment","name":"signedCommitment","type":"tuple","components":[{"internalType":"struct Commitment","name":"commitment","type":"tuple","components":[{"internalType":"struct Payload[]","name":"payload","type":"tuple[]","components":[{"internalType":"bytes2","name":"id","type":"bytes2"},{"internalType":"bytes","name":"data","type":"bytes"}]},{"internalType":"uint32","name":"blockNumber","type":"uint32"},{"internalType":"uint64","name":"validatorSetId","type":"uint64"}]},{"internalType":"struct Vote[]","name":"votes","type":"tuple[]","components":[{"internalType":"bytes","name":"signature","type":"bytes"},{"internalType":"uint256","name":"authorityIndex","type":"uint256"}]}]},{"internalType":"struct BeefyMmrLeaf","name":"latestMmrLeaf","type":"tuple","components":[{"internalType":"uint8","name":"version","type":"uint8"},{"internalType":"uint32","name":"parentNumber","type":"uint32"},{"internalType":"bytes32","name":"parentHash","type":"bytes32"},{"internalType":"struct AuthoritySetCommitment","name":"nextAuthoritySet","type":"tuple","components":[{"internalType":"uint64","name":"id","type":"uint64"},{"internalType":"uint32","name":"len","type":"uint32"},{"internalType":"bytes32","name":"root","type":"bytes32"}]},{"internalType":"bytes32","name":"extra","type":"bytes32"},{"internalType":"uint256","name":"leafIndex","type":"uint256"}]},{"internalType":"bytes32[]","name":"mmrProof","type":"bytes32[]"},{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"}]},{"internalType":"struct ParachainProof","name":"parachain","type":"tuple","components":[{"internalType":"struct Parachain[]","name":"parachains","type":"tuple[]","components":[{"internalType":"uint256","name":"index","type":"uint256"},{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bytes","name":"header","type":"bytes"}]},{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"},{"internalType":"uint256","name":"leafCount","type":"uint256"}]}]}],"stateMutability":"pure","type":"function","name":"noOp"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"stateMutability":"view","type":"function","name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}]},{"inputs":[{"internalType":"bytes","name":"previousState","type":"bytes"},{"internalType":"bytes","name":"proof","type":"bytes"}],"stateMutability":"pure","type":"function","name":"verify","outputs":[{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"struct IntermediateState[]","name":"","type":"tuple[]","components":[{"internalType":"uint256","name":"stateMachineId","type":"uint256"},{"internalType":"uint256","name":"height","type":"uint256"},{"internalType":"struct StateCommitment","name":"commitment","type":"tuple","components":[{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bytes32","name":"overlayRoot","type":"bytes32"},{"internalType":"bytes32","name":"stateRoot","type":"bytes32"}]}]},{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"bytes","name":"encodedState","type":"bytes"},{"internalType":"bytes","name":"encodedProof","type":"bytes"}],"stateMutability":"pure","type":"function","name":"verifyConsensus","outputs":[{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"struct IntermediateState[]","name":"","type":"tuple[]","components":[{"internalType":"uint256","name":"stateMachineId","type":"uint256"},{"internalType":"uint256","name":"height","type":"uint256"},{"internalType":"struct StateCommitment","name":"commitment","type":"tuple","components":[{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bytes32","name":"overlayRoot","type":"bytes32"},{"internalType":"bytes32","name":"stateRoot","type":"bytes32"}]}]}]}],"devdoc":{"kind":"dev","methods":{"supportsInterface(bytes4)":{"details":"See {IERC165-supportsInterface}."},"verify(bytes,bytes)":{"details":"IConsensusV2 entry point. Decodes the proof, verifies consensus, and returns the updated state along with the latest authority set id."},"verifyConsensus(bytes,bytes)":{"details":"IConsensus entry point. Decodes the proof and verifies consensus."}},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@hyperbridge/core/=node_modules/@hyperbridge/core/contracts/","@openzeppelin/=node_modules/@openzeppelin/","@polytope-labs/=node_modules/@polytope-labs/","@sp1-contracts/=lib/sp1-contracts/contracts/src/","@uniswap/=node_modules/@uniswap/","ds-test/=lib/solidity-stringutils/lib/ds-test/src/","erc4626-tests/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=node_modules/forge-std/src/","openzeppelin-contracts/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/","solidity-stringutils/=lib/solidity-stringutils/","sp1-contracts/=lib/sp1-contracts/contracts/","stringutils/=lib/solidity-stringutils/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"src/consensus/EcdsaBeefy.sol":"EcdsaBeefy"},"evmVersion":"cancun","libraries":{},"viaIR":true},"sources":{"node_modules/@hyperbridge/core/contracts/interfaces/IConsensus.sol":{"keccak256":"0x56b093b9ca6913da8d12c54beca9ba8d970c1a1a0cf39838c2c341cd4ef23afb","urls":["bzz-raw://6be3495c82c3c5dd95ccc5855289b5e7079a419a9b0d09af843344ac86b6d286","dweb:/ipfs/QmNQZugSBtnVcggYqnTXdcsZV3nLkGTK4VEXCnYX3fGKpV"],"license":"Apache-2.0"},"node_modules/@hyperbridge/core/contracts/interfaces/IConsensusV2.sol":{"keccak256":"0xa680bb1b902d419b862d155c49ae6fd2371e9b8a0a42d67cf07a6b4306300b90","urls":["bzz-raw://df9f403774a0fb637e331e23a599aa1cd3dc2a70baef0ebdd3b7dd4676dab81a","dweb:/ipfs/QmfA14ersZ1D88DkxEaK3krNs7XsCYe5UTmTANKcGB8tE4"],"license":"Apache-2.0"},"node_modules/@hyperbridge/core/contracts/libraries/StateMachine.sol":{"keccak256":"0x860289ae856ea354df5cca2131612da5c15a129fc14ae5ac0d528e0ce7c809c3","urls":["bzz-raw://2ab5a7d6463b634da1e05e2b06818416a6f2d59b5c10815cc74bf26d003c2b95","dweb:/ipfs/QmZ4dpEoDhNefAcKwKBXumLfSHqxhcBctErTzMvvPHXY5G"],"license":"Apache-2.0"},"node_modules/@openzeppelin/contracts/utils/Panic.sol":{"keccak256":"0xf7fe324703a64fc51702311dc51562d5cb1497734f074e4f483bfb6717572d7a","urls":["bzz-raw://c6a5ff4f9fd8649b7ee20800b7fa387d3465bd77cf20c2d1068cd5c98e1ed57a","dweb:/ipfs/QmVSaVJf9FXFhdYEYeCEfjMVHrxDh5qL4CGkxdMWpQCrqG"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/Strings.sol":{"keccak256":"0xad148d59f05165f9217d0a9e1ac8f772abb02ea6aaad8a756315c532bf79f9f4","urls":["bzz-raw://15e3599867c2182f5831e9268b274b2ef2047825837df6b4d81c9e89254b093e","dweb:/ipfs/QmZbL7XAYr5RmaNaooPgZRmcDXaudfsYQfYD9y5iAECvpS"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/cryptography/ECDSA.sol":{"keccak256":"0x69f54c02b7d81d505910ec198c11ed4c6a728418a868b906b4a0cf29946fda84","urls":["bzz-raw://8e25e4bdb7ae1f21d23bfee996e22736fc0ab44cfabedac82a757b1edc5623b9","dweb:/ipfs/QmQdWQvB6JCP9ZMbzi8EvQ1PTETqkcTWrbcVurS7DKpa5n"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/introspection/ERC165.sol":{"keccak256":"0x2d9dc2fe26180f74c11c13663647d38e259e45f95eb88f57b61d2160b0109d3e","urls":["bzz-raw://81233d1f98060113d9922180bb0f14f8335856fe9f339134b09335e9f678c377","dweb:/ipfs/QmWh6R35SarhAn4z2wH8SU456jJSYL2FgucfTFgbHJJN4E"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol":{"keccak256":"0x8891738ffe910f0cf2da09566928589bf5d63f4524dd734fd9cedbac3274dd5c","urls":["bzz-raw://971f954442df5c2ef5b5ebf1eb245d7105d9fbacc7386ee5c796df1d45b21617","dweb:/ipfs/QmadRjHbkicwqwwh61raUEapaVEtaLMcYbQZWs9gUkgj3u"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/math/Math.sol":{"keccak256":"0x1225214420c83ebcca88f2ae2b50f053aaa7df7bd684c3e878d334627f2edfc6","urls":["bzz-raw://6c5fab4970634f9ab9a620983dc1c8a30153981a0b1a521666e269d0a11399d3","dweb:/ipfs/QmVRnBC575MESGkEHndjujtR7qub2FzU9RWy9eKLp4hPZB"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/math/SafeCast.sol":{"keccak256":"0x195533c86d0ef72bcc06456a4f66a9b941f38eb403739b00f21fd7c1abd1ae54","urls":["bzz-raw://b1d578337048cad08c1c03041cca5978eff5428aa130c781b271ad9e5566e1f8","dweb:/ipfs/QmPFKL2r9CBsMwmUqqdcFPfHZB2qcs9g1HDrPxzWSxomvy"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/math/SignedMath.sol":{"keccak256":"0xb1970fac7b64e6c09611e6691791e848d5e3fe410fa5899e7df2e0afd77a99e3","urls":["bzz-raw://db5fbb3dddd8b7047465b62575d96231ba8a2774d37fb4737fbf23340fabbb03","dweb:/ipfs/QmVUSvooZKEdEdap619tcJjTLcAuH6QBdZqAzWwnAXZAWJ"],"license":"MIT"},"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMountainRange.sol":{"keccak256":"0x014237038bb77bdf371b50c1268d02bf7eeaf7068c35483d5d0684ca7c30d544","urls":["bzz-raw://beb3ae60094f49716a2b0cfa6229ee27af1778934cf5f2dfc704433fda5a687b","dweb:/ipfs/QmQ339wEFT9X1woHyFTDbbhUCx8ekD9MGLRnhS3PpYp1pm"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMultiProof.sol":{"keccak256":"0xd4f6e6a9eceaa7d1cdf9684bfe7f3f552adf21dfcf7f1372f7943a4c2f15deb7","urls":["bzz-raw://ec900b3e79ea8ef9d275697937d1984e994d10117aa2be066bd73f6a31aedf10","dweb:/ipfs/QmZQGLiJtbvUSEW5H2HEenzxp58fN43XHRY3SoNTC9Hobg"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Bytes.sol":{"keccak256":"0xd305383358b93285d8fcee512795487484eadfd7b602df16ff4e9b01afbefec7","urls":["bzz-raw://57c86cd2fe6ab591264c5632d503d77f04748d722e4defa9c2badb238ee1f9bd","dweb:/ipfs/QmTZ6xTcaYkRBshq6YZVMx2kkk94ZtJsRM6xLpC5qWT3oA"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Memory.sol":{"keccak256":"0x59e3a56caa42c1aac30231173439817d38c7f359e40bd36e9bb418d3f82ceab7","urls":["bzz-raw://e060fed46c6b420624166ea02a326f0f566897941bdc322257e690ae134b8179","dweb:/ipfs/QmbXW8yG2ZntjLMyUEmQGcRZroZj4dVZkBhHxK2PFKfUKB"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Node.sol":{"keccak256":"0xca611969a68f7fe63dcdc742c9caf9bc1b26495561b68f4676c209279a4576ba","urls":["bzz-raw://17b7ec2cf65d484f0a2c188a7ae33430b015b3a55bf10393167f53839e2dde56","dweb:/ipfs/QmTQbT8J7rBRNHFXSR7S4KbpwCteMrFt8mvrzCD1vYYTwX"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/polkadot/ScaleCodec.sol":{"keccak256":"0x9ac4df46e68718f7deaaa5b7443778533f53dc0ff3736cc386cf4991099da2aa","urls":["bzz-raw://85d08b00d9173358323105be975ce85b7e206ba52df2d80a14d2581dec1ddd11","dweb:/ipfs/QmQCqVvSdJUqfhLH8TRBGNDGiEzkGduhRacnYhoKcm7e2a"],"license":"Apache-2.0"},"src/consensus/Codec.sol":{"keccak256":"0xfbda8d0aef81312f23b53eb34ba4fcd19ab886cbd010a0f100d86271eecee126","urls":["bzz-raw://bc4a4b1a88f702efa6727c368894b8ac13b69769efc81355b71a9a0d7a2efd7b","dweb:/ipfs/QmYwLJgyCr4pPRyyk2oLYMLq5QGRhCSZKLYUEdW6U2SBzx"],"license":"Apache-2.0"},"src/consensus/EcdsaBeefy.sol":{"keccak256":"0x143e08b4250eefd6a9000466df7d5e8ca13f37f19c352be92875bd8bd364cd7b","urls":["bzz-raw://88dc9f7f539608ca28479c3f937a0583d39dcfc698c001b65a478320dd8e1790","dweb:/ipfs/QmaD4DbxtRGCApbQtuovPwBwRt2PZEVrRLck3vFyejkhrH"],"license":"Apache-2.0"},"src/consensus/Types.sol":{"keccak256":"0xcd1866064ae11c71575fefa857a7317d10e18da0c204c09ae42308c4c13a0268","urls":["bzz-raw://c083e149c4607f2cd5b769c1a624bbc5a861e8d5c485dd2ce06a0e23f7e762dd","dweb:/ipfs/QmPMSM8S2sav9t4X2peHJnpTQhowEgEW5phYoiy3LbGAsv"],"license":"Apache-2.0"}},"version":1},"id":150} \ No newline at end of file +{"abi":[{"type":"constructor","inputs":[{"name":"digestParaId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"MMR_ROOT_PAYLOAD_ID","inputs":[],"outputs":[{"name":"","type":"bytes2","internalType":"bytes2"}],"stateMutability":"view"},{"type":"function","name":"_digestParaId","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"noOp","inputs":[{"name":"s","type":"tuple","internalType":"struct BeefyConsensusState","components":[{"name":"latestHeight","type":"uint256","internalType":"uint256"},{"name":"beefyActivationBlock","type":"uint256","internalType":"uint256"},{"name":"currentAuthoritySet","type":"tuple","internalType":"struct AuthoritySet","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"len","type":"uint256","internalType":"uint256"},{"name":"blsPoseidonHash","type":"uint256","internalType":"uint256"},{"name":"ecdsaMerkleRoot","type":"bytes32","internalType":"bytes32"}]},{"name":"nextAuthoritySet","type":"tuple","internalType":"struct AuthoritySet","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"len","type":"uint256","internalType":"uint256"},{"name":"blsPoseidonHash","type":"uint256","internalType":"uint256"},{"name":"ecdsaMerkleRoot","type":"bytes32","internalType":"bytes32"}]}]},{"name":"p","type":"tuple","internalType":"struct BeefyConsensusProof","components":[{"name":"relay","type":"tuple","internalType":"struct RelayChainProof","components":[{"name":"signedCommitment","type":"tuple","internalType":"struct SignedCommitment","components":[{"name":"commitment","type":"tuple","internalType":"struct Commitment","components":[{"name":"payload","type":"tuple[]","internalType":"struct Payload[]","components":[{"name":"id","type":"bytes2","internalType":"bytes2"},{"name":"data","type":"bytes","internalType":"bytes"}]},{"name":"blockNumber","type":"uint32","internalType":"uint32"},{"name":"validatorSetId","type":"uint64","internalType":"uint64"}]},{"name":"votes","type":"tuple[]","internalType":"struct Vote[]","components":[{"name":"signature","type":"bytes","internalType":"bytes"},{"name":"authorityIndex","type":"uint256","internalType":"uint256"}]}]},{"name":"latestMmrLeaf","type":"tuple","internalType":"struct BeefyMmrLeaf","components":[{"name":"version","type":"uint8","internalType":"uint8"},{"name":"parentNumber","type":"uint32","internalType":"uint32"},{"name":"parentHash","type":"bytes32","internalType":"bytes32"},{"name":"nextAuthoritySet","type":"tuple","internalType":"struct AuthoritySetCommitment","components":[{"name":"id","type":"uint64","internalType":"uint64"},{"name":"len","type":"uint32","internalType":"uint32"},{"name":"root","type":"bytes32","internalType":"bytes32"}]},{"name":"extra","type":"bytes32","internalType":"bytes32"},{"name":"leafIndex","type":"uint256","internalType":"uint256"}]},{"name":"mmrProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}]},{"name":"parachain","type":"tuple","internalType":"struct ParachainProof","components":[{"name":"parachains","type":"tuple[]","internalType":"struct Parachain[]","components":[{"name":"index","type":"uint256","internalType":"uint256"},{"name":"id","type":"uint256","internalType":"uint256"},{"name":"header","type":"bytes","internalType":"bytes"}]},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"leafCount","type":"uint256","internalType":"uint256"}]}]}],"outputs":[],"stateMutability":"pure"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"verify","inputs":[{"name":"previousState","type":"bytes","internalType":"bytes"},{"name":"proof","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"},{"name":"","type":"tuple[]","internalType":"struct IntermediateState[]","components":[{"name":"stateMachineId","type":"uint256","internalType":"uint256"},{"name":"height","type":"uint256","internalType":"uint256"},{"name":"commitment","type":"tuple","internalType":"struct StateCommitment","components":[{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"overlayRoot","type":"bytes32","internalType":"bytes32"},{"name":"stateRoot","type":"bytes32","internalType":"bytes32"}]}]},{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"error","name":"ECDSAInvalidSignature","inputs":[]},{"type":"error","name":"ECDSAInvalidSignatureLength","inputs":[{"name":"length","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ECDSAInvalidSignatureS","inputs":[{"name":"s","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"EmptyLeaves","inputs":[]},{"type":"error","name":"EmptyTree","inputs":[]},{"type":"error","name":"EmptyTree","inputs":[]},{"type":"error","name":"IllegalGenesisBlock","inputs":[]},{"type":"error","name":"InvalidAuthoritiesProof","inputs":[]},{"type":"error","name":"InvalidMmrProof","inputs":[]},{"type":"error","name":"LeafIndexOutOfBounds","inputs":[]},{"type":"error","name":"MmrRootHashMissing","inputs":[]},{"type":"error","name":"OutOfBoundsLeaves","inputs":[]},{"type":"error","name":"ProofExhausted","inputs":[]},{"type":"error","name":"SuperMajorityRequired","inputs":[]},{"type":"error","name":"TimestampNotFound","inputs":[]},{"type":"error","name":"UnconsumedProof","inputs":[]},{"type":"error","name":"UnknownAuthoritySet","inputs":[]},{"type":"error","name":"UnsortedLeaves","inputs":[]},{"type":"error","name":"UnsortedLeaves","inputs":[]}],"bytecode":{"object":"0x60a034605e57601f61308f38819003918201601f19168301916001600160401b03831184841017606257808492602094604052833981010312605e5751608052604051613018908161007782396080518181816108640152610c810152f35b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60806040526004361015610011575f80fd5b5f3560e01c806301ffc9a7146100645780639442d9fc1461005f578063af8b91d61461005a578063e455995b146100555763f7e83aee14610050575f80fd5b6108b4565b61084d565b61082d565b610795565b346100b85760203660031901126100b85760043563ffffffff60e01b81168091036100b857637bf41d7760e11b81149081156100a7575b50151560805260206080f35b6301ffc9a760e01b1490508161009b565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b608081019081106001600160401b038211176100eb57604052565b6100bc565b604081019081106001600160401b038211176100eb57604052565b606081019081106001600160401b038211176100eb57604052565b60c081019081106001600160401b038211176100eb57604052565b90601f801991011681019081106001600160401b038211176100eb57604052565b60405190610171608083610141565b565b60405190610171604083610141565b60405190610171606083610141565b6040519061017160a083610141565b91908260809103126100b8576040516101b8816100d0565b60608082948035845260208101356020850152604081013560408501520135910152565b906101406003198301126100b8576040516101f6816100d0565b60606102218294600435845260243560208501526102158160446101a0565b604085015260c46101a0565b910152565b6001600160401b0381116100eb5760051b60200190565b6001600160401b0381116100eb57601f01601f191660200190565b81601f820112156100b85780359061026f8261023d565b9261027d6040519485610141565b828452602083830101116100b857815f926020809301838601378301015290565b359063ffffffff821682036100b857565b35906001600160401b03821682036100b857565b81601f820112156100b8578035906102da82610226565b926102e86040519485610141565b82845260208085019360051b830101918183116100b85760208101935b83851061031457505050505090565b84356001600160401b0381116100b85782016040818503601f1901126100b85760405191610341836100f0565b6020820135926001600160401b0384116100b857604083610369886020809881980101610258565b8352013583820152815201940193610305565b91906040838203126100b85760405190610395826100f0565b819380356001600160401b0381116100b85781016060818403126100b857604051906103c08261010b565b80356001600160401b0381116100b857810184601f820112156100b8578035906103e982610226565b916103f76040519384610141565b80835260208084019160051b830101918783116100b85760208101915b838310610467575050505061043f9160409184526104346020820161029e565b6020850152016102af565b604082015283526020810135916001600160401b0383116100b85760209261022192016102c3565b82356001600160401b0381116100b8578201906040828b03601f1901126100b85760405190610495826100f0565b60208301356001600160f01b0319811681036100b85782526040830135916001600160401b0383116100b8576104d38c602080969581960101610258565b83820152815201920191610414565b91908260609103126100b8576040516104fa8161010b565b6040808294610508816102af565b84526105166020820161029e565b60208501520135910152565b919091610100818403126100b8576040519061053d82610126565b819381359160ff831683036100b85761057b60e09260a09486526105636020840161029e565b602087015260408301356040870152606083016104e2565b606085015260c081013560808501520135910152565b9080601f830112156100b85781356105a881610226565b926105b66040519485610141565b81845260208085019260051b8201019283116100b857602001905b8282106105de5750505090565b81358152602091820191016105d1565b919091610160818403126100b857610604610162565b9281356001600160401b0381116100b8578161062191840161037c565b84526106308160208401610522565b60208501526101208201356001600160401b0381116100b85781610655918401610591565b60408501526101408201356001600160401b0381116100b8576106789201610591565b6060830152565b9190916060818403126100b857604051906106998261010b565b819381356001600160401b0381116100b857820181601f820112156100b8578035906106c482610226565b916106d26040519384610141565b80835260208084019160051b830101918483116100b85760208101915b838310610720575050505083526020820135916001600160401b0383116100b8576105166040939284938301610591565b82356001600160401b0381116100b8578201906060828803601f1901126100b8576040519061074e8261010b565b60208301358252604083013560208301526060830135916001600160401b0383116100b85761078589602080969581960101610258565b60408201528152019201916106ef565b346100b8576101603660031901126100b8576107b0366101dc565b50610144356001600160401b0381116100b857604060031982360301126100b857604051906107de826100f0565b80600401356001600160401b0381116100b85761080190600436918401016105ee565b825260248101356001600160401b0381116100b8576020916004610828923692010161067f565b910152005b346100b8575f3660031901126100b857604051610dad60f31b8152602090f35b346100b8575f3660031901126100b85760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b9181601f840112156100b8578235916001600160401b0383116100b857602083818601950101116100b857565b346100b85760403660031901126100b8576004356001600160401b0381116100b8576108e4903690600401610887565b602435916001600160401b0383116100b85761090761090f933690600401610887565b929091610b95565b919060206040519260608452805191829182606087015201608085015e5f60808285010152601f8019910116820192602060a060808601956080868203018387015284518097520192015f945b80861061097157505082935060408301520390f35b9092602060a060019260408088518051845285810151868501520151805182840152848101516060840152015160808201520194019501949061095c565b90610140828203126100b8576109f49060c0604051936109ce856100d0565b80358552602081013560208601526109e983604083016101a0565b6040860152016101a0565b606082015290565b9190916040818403126100b85780356001600160401b0381116100b85783610a259183016105ee565b9260208201356001600160401b0381116100b857610a43920161067f565b90565b6101719092919260c060606101408301958051845260208101516020850152610a9560408201516040860190606080918051845260208101516020850152604081015160408501520151910152565b0151910190606080918051845260208101516020850152604081015160408501520151910152565b60405190610aca8261010b565b5f6040838281528260208201520152565b60405190610aea602083610141565b5f80835282815b828110610afd57505050565b602090604051610b0c8161010b565b5f81525f83820152610b1c610abd565b604082015282828501015201610af1565b90610b3782610226565b610b446040519182610141565b8281528092610b55601f1991610226565b01905f5b828110610b6557505050565b602090604051610b748161010b565b5f81525f83820152610b84610abd565b604082015282828501015201610b59565b610ba990610bb293929594958101906109af565b938101906109fc565b8392919251610bd6610bcd6020865151015163ffffffff1690565b63ffffffff1690565b1115610cd857610c488160606020610bfd610bf588610c3a999a610e04565b9490946110b6565b970151015180516001600160401b03169360608301948551906001600160401b038251911611610c50575b50505060405194859160208301610a46565b03601f198101855284610141565b515191929190565b604084015281516001600160401b03169163ffffffff6040610ca6610c7c602085015163ffffffff1690565b9451867f000000000000000000000000000000000000000000000000000000000000000091611222565b920151926001600160401b03610cba610162565b951685521660208401526040830152606082015283525f8080610c28565b506040519150610cfd82610cef8560208301610a46565b03601f198101845283610141565b6060610d07610adb565b9301515191929190565b60405190610d1e826100d0565b5f6060838281528260208201528260408201520152565b60405190610d42826100d0565b815f81525f6020820152610d54610d11565b60408201526060610221610d11565b634e487b7160e01b5f52603260045260245ffd5b805115610d845760200190565b610d63565b8051821015610d845760209160051b010190565b60405190610daa826100f0565b5f6020838281520152565b90610dbf82610226565b610dcc6040519182610141565b8281528092610ddd601f1991610226565b01905f5b828110610ded57505050565b602090610df8610d9d565b82828501015201610de1565b9190610e0e610d35565b50805190602082015151915190610e30610bcd6020840163ffffffff90511690565b9260408301610e4681516001600160401b031690565b9060408801916001600160401b03835151911690811415908161106a575b5061105b57519051805190916001600160401b03160361104f57905b6020820191610e97610e938451846112ea565b1590565b61104057845151945f955f5b818110610fbc5750508515610fad57610ebb906113f9565b6020815191012091610ecc81610db5565b925f5b828110610f205750505091610e93916060610ef494015191606086015190519261154e565b610f1157608092610f086020938388611666565b85520151015190565b63528bd3ef60e01b5f5260045ffd5b80610f3260019260208a510151610d89565b516020610f40825186611538565b91015190604051610f7e81610f706020820194856014916bffffffffffffffffffffffff199060601b1681520190565b03601f198101835282610141565b519020610f89610173565b9182526020820152610f9b8288610d89565b52610fa68187610d89565b5001610ecf565b6323188e3960e21b5f5260045ffd5b610dad60f31b610fef610fe2610fd3848751610d89565b51516001600160f01b03191690565b6001600160f01b03191690565b1480611027575b611003575b600101610ea3565b9650600161101f60206110178a8651610d89565b510151611313565b979050610ffb565b50602080611036838651610d89565b5101515114610ff6565b633aa90f7f60e21b5f5260045ffd5b50606086015190610e80565b637202e68560e11b5f5260045ffd5b905060608901515114155f610e64565b805191908290602001825e015f815290565b6040516001600160e01b031990911660208201529190610171908390610cef90602483019061107a565b815151916110c383610db5565b916110cd84610b2d565b935f5b81811061111657506110e3575b50505090565b6110fb928260406020610e939501519101519261154e565b611107575f80806110dd565b630b92186960e31b5f5260045ffd5b611121818551610d89565b51906040820191611132835161185b565b92602084019182511561121357600194816111ac60206111de94519201946111a661119f611164885163ffffffff1690565b6001600160e01b03199063ff00ff00600882811b9190911691901c62ff00ff1617601081811b63ffff00001691901c61ffff161760e01b1690565b91516119a7565b9061108c565b602081519101206111bb610173565b91825260208201526111cd868c610d89565b526111d8858b610d89565b506119cc565b905191516111ea610182565b928352602083015260408201526112018289610d89565b5261120c8188610d89565b50016110d0565b63b4eb9e5160e01b5f5260045ffd5b9190915f915b81518310156112925780602061123e8585610d89565b51015103611288575060406112596112669361126193610d89565b51015161185b565b612507565b906001600160401b03808351169116145f14611283576040015190565b505f90565b9160010191611228565b505050505f90565b634e487b7160e01b5f52601160045260245ffd5b90600182018092116112bc57565b61129a565b90600382018092116112bc57565b90600282018092116112bc57565b919082018092116112bc57565b906001600160ff1b03811681036112bc5760039060011b0490600182018092116112bc57101590565b6020815110611323576020015190565b60405162461bcd60e51b8152602060048201526024808201527f42797465733a3a20746f427974657333323a206461746120697320746f20736860448201526337b93a1760e11b6064820152608490fd5b60405190611383602083610141565b5f8252565b610cef6113a794936113a761017194604051978895602087019061107a565b9061107a565b610171926113a795946113cc600c94604051988995602087019061107a565b6001600160e01b03199290921682526001600160c01b031916600482015203601319810185520183610141565b90815151611405611374565b905f5b8181106114da57509261141e610a439394611b8c565b916114d461144a604061143b611164602087015163ffffffff1690565b9401516001600160401b031690565b67ffffffffffff000067ff00ff00ff00ff0066ff00ff00ff00ff8360081c169260081b169165ffff0000ffff65ffff0000ff0065ffffffffffff67ffff0000ffff0000861666ff0000ffff000085161760101c16941691161760101b161767ffffffff0000000063ffffffff8260201c169160201b166001600160401b0360c01b911760c01b1690565b926113ad565b91611531600191610f706115136114f5610fd3888b51610d89565b6040516001600160f01b031990911660208201529182906022820190565b61152b6020611523888b51610d89565b5101516119a7565b91611388565b9201611408565b610a439161154591611d09565b90929192611d61565b91939290811561161757845161156381611ddd565b61156c82611ddd565b91600161157886612816565b1b5f805b838210611597575050505061159394959650612841565b1490565b6115a1828c610d89565b515190888210156116085782151590816115fd575b506115ee5760019060206115ca848e610d89565b5101516115d78489610d89565b528084016115e58488610d89565b5291019061157c565b630647f54960e21b5f5260045ffd5b90508111155f6115b6565b630834466160e31b5f5260045ffd5b639136328760e01b5f5260045ffd5b604080519091906116378382610141565b6001815291601f1901825f5b82811061164f57505050565b60209061165a610d9d565b82828501015201611643565b610e9391611749936040602083019261170d61170860206116e4875161168d815160ff1690565b9061169e8482015163ffffffff1690565b90888101516116d160806060840151930151936116c56116bc610191565b60ff9097168752565b63ffffffff1685880152565b8984015260608301526080820152611e86565b8051908201209701518651602001516117029063ffffffff16610bcd565b90611f7a565b6112ae565b9460a0611718611626565b9551015190611725610173565b918252602082015261173685610d77565b5261174084610d77565b50015190611f8f565b61110757565b6040519060a082018281106001600160401b038211176100eb5760405260606080835f81525f60208201525f60408201525f838201520152565b60405190611796826100f0565b60606020835f81520152565b6040519061012082018281106001600160401b038211176100eb576040525f610100838281526117d0611789565b60208201528260408201526117e3611789565b60608201528260808201526117f6611789565b60a08201528260c0820152606060e08201520152565b9061181682610226565b6118236040519182610141565b8281528092611834601f1991610226565b01905f5b82811061184457505050565b60209061184f6117a2565b82828501015201611838565b61186361174f565b5061186c610173565b9081525f602082015261188661188182611f9b565b611313565b90611890816120da565b61189c61188183611f9b565b6118a861188184611f9b565b916118b2846120da565b6118bb8161180c565b945f5b8281106118ea575050506118d0610191565b948552602085015260408401526060830152608082015290565b6001906118f683612259565b60ff6119006117a2565b91168061192c5750600160c08201525b61191a828a610d89565b526119258189610d89565b50016118be565b60048103611950575060016040820152611945846122d3565b60608201525b611910565b60058103611973575060016080820152611969846122d3565b60a0820152611910565b60068103611993575060018152611989846122d3565b6020820152611910565b60080361194b576001610100820152611910565b610a436113a791610f706119bb8251611b8c565b91604051948593602085019061107a565b6119d4610abd565b505f5f925f5f5b6080850180518051831015611b055760406119f984611a0193610d89565b510151151590565b80611add575b611a94575b611a1c60406119f9848451610d89565b80611a5b575b611a30575b506001016119db565b819250611a5360206060611a4960809560019551610d89565b510151015161248f565b929150611a27565b50634953544d60e01b63ffffffff60e01b611a8d6060611a7c868651610d89565b510151516001600160e01b03191690565b1614611a22565b95509250611ab761188160206060611aad878a51610d89565b51015101516123af565b92611ad761188160206060611acd858b51610d89565b5101510151612429565b95611a0c565b5063049534d560e41b63ffffffff60e01b611afe6060611a7c868651610d89565b1614611a07565b505050925092908215611b2a57611b1a610182565b9283526020830152604082015290565b633eba99d160e21b5f5260045ffd5b6003198101919082116112bc57565b5f198101919082116112bc57565b60200390602082116112bc57565b919082039182116112bc57565b600190610a43939260ff60f81b9060f81b168152019061107a565b6040811015611bd057610a43611bae611ba8610f709360021b90565b60ff1690565b60405160f89190911b6001600160f81b03191660208201529182906021820190565b614000811015611c2f57610a43611c0d611bfa611bf3611708610f709560021b90565b61ffff1690565b60ff61ff008260081b169160081c161790565b60405160f09190911b6001600160f01b03191660208201529182906022820190565b6340000000811015611cac57610a43611c8a611c59610bcd611c54610f709560021b90565b6112cf565b63ffffff0062ff00ff62ffffff818460081c1616921660081b161763ffff000061ffff8260101c169160101b161790565b60405160e09190911b6001600160e01b03191660208201529182906024820190565b610f70611cd1611cbe611cd693612627565b6040519283916020830160209181520190565b612740565b610a43611cf7611ba8611cf2611cec8551611b39565b60021b90565b6112c1565b610f7060405193849260208401611b71565b8151919060418303611d3957611d329250602082015190606060408401519301515f1a90612789565b9192909190565b50505f9160029190565b60041115611d4d57565b634e487b7160e01b5f52602160045260245ffd5b611d6a81611d43565b80611d73575050565b611d7c81611d43565b60018103611d935763f645eedf60e01b5f5260045ffd5b611d9c81611d43565b60028103611db7575063fce698f760e01b5f5260045260245ffd5b80611dc3600392611d43565b14611dcb5750565b6335e2f38360e21b5f5260045260245ffd5b90611de782610226565b611df46040519182610141565b8281528092611e05601f1991610226565b0190602036910137565b949291969593909660405197889660208801611e2a9161107a565b9063ffffffff60e01b168152600401611e429161107a565b916001600160401b0360c01b16825263ffffffff60e01b166008820152600c01611e6b9161107a565b611e749161107a565b03601f19810183526101719083610141565b611f67611f44610cef610a4393611ec3611ea1825160ff1690565b60405160f89190911b6001600160f81b03191660208201529283906021820190565b610cef611eda611164602084015163ffffffff1690565b611ef560408401516040519384916020830160209181520190565b606083015193611f756080611f1461144a88516001600160401b031690565b95611f526040611f2e61116460208c015163ffffffff1690565b9901516040519a8b916020830160209181520190565b03601f1981018b528a610141565b01516040519889916020830160209181520190565b03601f198101895288610141565b611e0f565b80611f83575090565b81039081116112bc5790565b929091611593926129b6565b60208101908151602081018091116112bc57815151106100b8576020905181835182010191829101116112bc576020611fd391612bee565b90805190602082018092116112bc575290565b90602082019182518281018091116112bc57815151106100b8578115612034576020905181845182010191829101116112bc578161202391612bee565b9180519182018092116112bc575290565b505050604051612045602082610141565b5f815290565b60ff60049116019060ff82116112bc57565b1561206457565b60405162461bcd60e51b815260206004820152602860248201527f756e657870656374656420707265666978206465636f64696e6720436f6d706160448201526731ba1e2ab4b73a1f60c11b6064820152608490fd5b906001600160401b03809116911601906001600160401b0382116112bc57565b6120e381612259565b60038116806120fd5750610a43915060021c603f16611ba8565b6001810361214e57506121429061213c611ba8612132612122611ba8610a4397612259565b60061b67ffffffffffffffc01690565b9260021c603f1690565b906120ba565b6001600160401b031690565b600281036121c45750610bcd906121b760ff846121a882612171610a4398612259565b95816121978161218961218388612259565b97612259565b991660081b63ffffff001690565b911617921660101b63ffff00001690565b17921660181b63ff0000001690565b1760021c633fffffff1690565b60030361220357610a439160ff6121ea6121e56121fe94603f9060021c1690565b61204b565b16906121f9600883111561205d565b611fe6565b61248f565b60405162461bcd60e51b815260206004820152601a60248201527f436f64652073686f756c6420626520756e726561636861626c650000000000006044820152606490fd5b908151811015610d84570160200190565b60208101908151600181018091116112bc578151511061229f575181516001600160f81b03199161228a9190612248565b511660f81c9061229a81516112ae565b905290565b60405162461bcd60e51b815260206004820152600c60248201526b4f7574206f662072616e676560a01b6044820152606490fd5b6122db611789565b5060208101908151600481018091116112bc57815151106100b8576020815181845182010191829101116112bc57600461231491612bee565b91805190600482018092116112bc57525f915f905b6004821061236957505080612340612346926120da565b90611fe6565b612361612351610173565b6001600160e01b03199093168352565b602082015290565b90926001600160f81b031961237e8584612248565b5116908460031b91858304600814861517156112bc576001926001600160e01b0319918216901c1617930190612329565b80516020116100b857602080610a439201612bee565b80516008116100b85760086020610a439201612bee565b8051600c116100b857602881019060200181106112bc576004610a4391612bee565b90815181116100b8578015612419576020610a439201612bee565b5050604051612045602082610141565b8051806020116100b857601f1981019081116112bc57604082019160200182106112bc57610a4391612bee565b805180600c116100b857600b1981019081116112bc57602c82019160200182106112bc57610a4391612bee565b80156112bc575f190190565b80515f91815b61249e57505090565b90915f198301908382116112bc576124b68284612248565b5160f81c91600381901b906001600160fd1b038116036112bc5760ff81116112bc576001901b918281029281840414901517156112bc57612500916124fa916112dd565b92612483565b9081612495565b61250f610abd565b5f915b608081018051805185101561261f57610e9360406119f98761253394610d89565b612613576341504b4360e01b61255f6125526060611a7c888651610d89565b6001600160e01b03191690565b036126135760606125738560209351610d89565b510151015191602c835103612606576125916121426121fe856123c5565b936001600160401b038516156125f2575050506125ea6125c26118816125bc610bcd6121fe866123dc565b93612456565b916125dd6125ce610182565b6001600160401b039095168552565b63ffffffff166020840152565b604082015290565b608092945060019193505b01929050612512565b91509160016080916125fd565b509160016080916125fd565b505050919050565b8060081c9060081b907cff000000ff000000ff000000ff000000ff000000ff000000ff000000ff7dff000000ff000000ff000000ff000000ff000000ff000000ff000000ff007fff000000ff000000ff000000ff000000ff000000ff000000ff000000ff00000084167eff000000ff000000ff000000ff000000ff000000ff000000ff000000ff000084161760101c931691161760101b177bffffffff00000000ffffffff00000000ffffffff00000000ffffffff7fffffffff00000000ffffffff00000000ffffffff00000000ffffffff00000000821660201c911660201b1777ffffffffffffffff0000000000000000ffffffffffffffff8019821660401c911660401b1761273c8160801c9160801b90565b1790565b80515f1981019081116112bc575b6001600160f81b03196127618284612248565b51166127755761277090612483565b61274e565b600181018091116112bc57610a43916123fe565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0841161280b579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15612800575f516001600160a01b038116156127f657905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f9160039190565b6001811115611283575f1981019081116112bc5761283390612c8f565b600181018091116112bc5790565b9192905f83515b600161285386610d77565b51146129a35761287f61287a85600161287461286e8a610d77565b51612c8f565b1b6112dd565b611b48565b905f915f915b8083106128a85750505061289c6128a291946112ae565b60011c90565b92612848565b9091926128b58489610d89565b51826128c0866112ae565b1080612985575b15612926579061291260019260026128fd6128e2898c610d89565b516128f56128ef8b6112ae565b8d610d89565b519084612cf5565b9701965b61290b848b610d89565b5260011c90565b61291c828b610d89565b5201929190612885565b8987600183188610612968579160018061295e612912946129568c61294f8d9e9d869b9a610d89565b5192610d89565b519085612cf5565b9801980196612901565b612912915091600161297c88829695610d89565b51970196612901565b50612998612992866112ae565b8a610d89565b5160018218146128c7565b50509150506129b29150610d77565b5190565b9290928215611617578351938415612bdf5760015b858110612bb157506001841480612ba7575b80612b95575b612b7a576129f86129f385612d15565b611ddd565b94612a01610173565b945f865260208601968752612a14610173565b925f845260208401948552612a27610182565b905f82526020820193845260408201525f91805b612ac6575b50505051612ab7575190515103612aa85781515f190182525b815115612a9c57612a6982612fb7565b612a7283612fb7565b90612a7e845160010190565b84525f5260205260405f20612a968451845190610d89565b52612a59565b916129b2915051610d77565b637227423160e11b5f5260045ffd5b63072afb8760e51b5f5260045ffd5b612acf81612c8f565b92612ae9612ae16001861b8094611b64565b9283926112dd565b938685612af68187612d5a565b92602084015180155f14612b2e575050505050508551518551145f03612a405780612b29612b2387612f9b565b8a612f80565b612a3b565b60011480612b72575b15612b5e575050506020612b55826040612b29940151905190610d89565b5101518a612f80565b91612b2993916002612b23941b0391612de3565b508015612b37565b925090925051612aa857612b8f602091610d77565b51015190565b50612b9f81610d77565b5151156129e3565b50600185146129dd565b612bbb8183610d89565b5151612bcf612bc983611b48565b84610d89565b515110156115ee576001016129cb565b631a14a47760e31b5f5260045ffd5b919091612bfa8361023d565b612c076040519182610141565b838152612c138461023d565b602082019190601f1901368337939091905b6020811015612c5f5780612c4557505f19905b5182518216911916179052565b61287a612c54612c5992611b56565b612fd3565b90612c38565b909182518152602081018091116112bc5791602081018091116112bc5790601f19810190811115612c255761129a565b806fffffffffffffffffffffffffffffffff1060071b81811c6001600160401b031060061b1781811c63ffffffff1060051b1781811c61ffff1060041b1781811c60ff1060031b1781811c600f1060021b1781811c60031060011b1790811c6001101790565b600116612d08575f5260205260405f2090565b905f5260205260405f2090565b90815f925b612d215750565b915f1983018381116112bc57600193169283910192612d1a565b60405190612d488261010b565b60606040835f81525f60208201520152565b612d62612d3b565b508051612d7560208301918251906112dd565b928251905b848210612dbc575b50829350612d9561229a92935182611b64565b908451946040810151612da6610182565b9687528360208801526040870152528251611b64565b90612dcb816040860151610d89565b5151821115612ddd5760010190612d7a565b90612d82565b91906020830151835193612df682611ddd565b93612e0083611ddd565b955f5b848110612f325750505050935b6001612e1b84610d77565b5114612f25575f945f905b808210612e3a575050848084528452612e10565b9095612e468786610d89565b51878784612e53836112ae565b1080612f07575b15612eae57600192612e8c83612e846128ef612e7d612ea89997612e9697610d89565b51926112ae565b519083612cf5565b61290b848c610d89565b612ea08289610d89565b5201966112cf565b90612e26565b505084516020860151511115612ef857600191612ee48392612e8c612ed38c8c610d89565b51612edd8a612f9b565b9083612cf5565b612eee8289610d89565b5201960190612e26565b63d8f29a1560e01b5f5260045ffd5b50612f1a612f14836112ae565b89610d89565b516001841814612e5a565b935050506129b290610d77565b600190612f6b604086016020612f538251612f4d86896112dd565b90610d89565b510151612f60848d610d89565b525182850190610d89565b51518401612f79828a610d89565b5201612e03565b90612f916020830151835190610d89565b5260018151019052565b612fab6020820151825190610d89565b51906001815101905290565b612fc76020820151825190610d89565b5181515f190190915290565b601f81116112bc576101000a9056fea2646970667358221220b54cab27f9309d22218000ba9b81a2beac680cddb6189b628244d20391b8b15264736f6c634300081e0033","sourceMap":"2797:9411:121:-:0;;;;;;;;;;;;;-1:-1:-1;;2797:9411:121;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;;;;;;;;;;;;3220:28;;2797:9411;;;;;;;;3220:28;2797:9411;;;;;;;;;;;;-1:-1:-1;2797:9411:121;;;;;;-1:-1:-1;2797:9411:121;;;;;-1:-1:-1;2797:9411:121","linkReferences":{}},"deployedBytecode":{"object":"0x60806040526004361015610011575f80fd5b5f3560e01c806301ffc9a7146100645780639442d9fc1461005f578063af8b91d61461005a578063e455995b146100555763f7e83aee14610050575f80fd5b6108b4565b61084d565b61082d565b610795565b346100b85760203660031901126100b85760043563ffffffff60e01b81168091036100b857637bf41d7760e11b81149081156100a7575b50151560805260206080f35b6301ffc9a760e01b1490508161009b565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b608081019081106001600160401b038211176100eb57604052565b6100bc565b604081019081106001600160401b038211176100eb57604052565b606081019081106001600160401b038211176100eb57604052565b60c081019081106001600160401b038211176100eb57604052565b90601f801991011681019081106001600160401b038211176100eb57604052565b60405190610171608083610141565b565b60405190610171604083610141565b60405190610171606083610141565b6040519061017160a083610141565b91908260809103126100b8576040516101b8816100d0565b60608082948035845260208101356020850152604081013560408501520135910152565b906101406003198301126100b8576040516101f6816100d0565b60606102218294600435845260243560208501526102158160446101a0565b604085015260c46101a0565b910152565b6001600160401b0381116100eb5760051b60200190565b6001600160401b0381116100eb57601f01601f191660200190565b81601f820112156100b85780359061026f8261023d565b9261027d6040519485610141565b828452602083830101116100b857815f926020809301838601378301015290565b359063ffffffff821682036100b857565b35906001600160401b03821682036100b857565b81601f820112156100b8578035906102da82610226565b926102e86040519485610141565b82845260208085019360051b830101918183116100b85760208101935b83851061031457505050505090565b84356001600160401b0381116100b85782016040818503601f1901126100b85760405191610341836100f0565b6020820135926001600160401b0384116100b857604083610369886020809881980101610258565b8352013583820152815201940193610305565b91906040838203126100b85760405190610395826100f0565b819380356001600160401b0381116100b85781016060818403126100b857604051906103c08261010b565b80356001600160401b0381116100b857810184601f820112156100b8578035906103e982610226565b916103f76040519384610141565b80835260208084019160051b830101918783116100b85760208101915b838310610467575050505061043f9160409184526104346020820161029e565b6020850152016102af565b604082015283526020810135916001600160401b0383116100b85760209261022192016102c3565b82356001600160401b0381116100b8578201906040828b03601f1901126100b85760405190610495826100f0565b60208301356001600160f01b0319811681036100b85782526040830135916001600160401b0383116100b8576104d38c602080969581960101610258565b83820152815201920191610414565b91908260609103126100b8576040516104fa8161010b565b6040808294610508816102af565b84526105166020820161029e565b60208501520135910152565b919091610100818403126100b8576040519061053d82610126565b819381359160ff831683036100b85761057b60e09260a09486526105636020840161029e565b602087015260408301356040870152606083016104e2565b606085015260c081013560808501520135910152565b9080601f830112156100b85781356105a881610226565b926105b66040519485610141565b81845260208085019260051b8201019283116100b857602001905b8282106105de5750505090565b81358152602091820191016105d1565b919091610160818403126100b857610604610162565b9281356001600160401b0381116100b8578161062191840161037c565b84526106308160208401610522565b60208501526101208201356001600160401b0381116100b85781610655918401610591565b60408501526101408201356001600160401b0381116100b8576106789201610591565b6060830152565b9190916060818403126100b857604051906106998261010b565b819381356001600160401b0381116100b857820181601f820112156100b8578035906106c482610226565b916106d26040519384610141565b80835260208084019160051b830101918483116100b85760208101915b838310610720575050505083526020820135916001600160401b0383116100b8576105166040939284938301610591565b82356001600160401b0381116100b8578201906060828803601f1901126100b8576040519061074e8261010b565b60208301358252604083013560208301526060830135916001600160401b0383116100b85761078589602080969581960101610258565b60408201528152019201916106ef565b346100b8576101603660031901126100b8576107b0366101dc565b50610144356001600160401b0381116100b857604060031982360301126100b857604051906107de826100f0565b80600401356001600160401b0381116100b85761080190600436918401016105ee565b825260248101356001600160401b0381116100b8576020916004610828923692010161067f565b910152005b346100b8575f3660031901126100b857604051610dad60f31b8152602090f35b346100b8575f3660031901126100b85760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b9181601f840112156100b8578235916001600160401b0383116100b857602083818601950101116100b857565b346100b85760403660031901126100b8576004356001600160401b0381116100b8576108e4903690600401610887565b602435916001600160401b0383116100b85761090761090f933690600401610887565b929091610b95565b919060206040519260608452805191829182606087015201608085015e5f60808285010152601f8019910116820192602060a060808601956080868203018387015284518097520192015f945b80861061097157505082935060408301520390f35b9092602060a060019260408088518051845285810151868501520151805182840152848101516060840152015160808201520194019501949061095c565b90610140828203126100b8576109f49060c0604051936109ce856100d0565b80358552602081013560208601526109e983604083016101a0565b6040860152016101a0565b606082015290565b9190916040818403126100b85780356001600160401b0381116100b85783610a259183016105ee565b9260208201356001600160401b0381116100b857610a43920161067f565b90565b6101719092919260c060606101408301958051845260208101516020850152610a9560408201516040860190606080918051845260208101516020850152604081015160408501520151910152565b0151910190606080918051845260208101516020850152604081015160408501520151910152565b60405190610aca8261010b565b5f6040838281528260208201520152565b60405190610aea602083610141565b5f80835282815b828110610afd57505050565b602090604051610b0c8161010b565b5f81525f83820152610b1c610abd565b604082015282828501015201610af1565b90610b3782610226565b610b446040519182610141565b8281528092610b55601f1991610226565b01905f5b828110610b6557505050565b602090604051610b748161010b565b5f81525f83820152610b84610abd565b604082015282828501015201610b59565b610ba990610bb293929594958101906109af565b938101906109fc565b8392919251610bd6610bcd6020865151015163ffffffff1690565b63ffffffff1690565b1115610cd857610c488160606020610bfd610bf588610c3a999a610e04565b9490946110b6565b970151015180516001600160401b03169360608301948551906001600160401b038251911611610c50575b50505060405194859160208301610a46565b03601f198101855284610141565b515191929190565b604084015281516001600160401b03169163ffffffff6040610ca6610c7c602085015163ffffffff1690565b9451867f000000000000000000000000000000000000000000000000000000000000000091611222565b920151926001600160401b03610cba610162565b951685521660208401526040830152606082015283525f8080610c28565b506040519150610cfd82610cef8560208301610a46565b03601f198101845283610141565b6060610d07610adb565b9301515191929190565b60405190610d1e826100d0565b5f6060838281528260208201528260408201520152565b60405190610d42826100d0565b815f81525f6020820152610d54610d11565b60408201526060610221610d11565b634e487b7160e01b5f52603260045260245ffd5b805115610d845760200190565b610d63565b8051821015610d845760209160051b010190565b60405190610daa826100f0565b5f6020838281520152565b90610dbf82610226565b610dcc6040519182610141565b8281528092610ddd601f1991610226565b01905f5b828110610ded57505050565b602090610df8610d9d565b82828501015201610de1565b9190610e0e610d35565b50805190602082015151915190610e30610bcd6020840163ffffffff90511690565b9260408301610e4681516001600160401b031690565b9060408801916001600160401b03835151911690811415908161106a575b5061105b57519051805190916001600160401b03160361104f57905b6020820191610e97610e938451846112ea565b1590565b61104057845151945f955f5b818110610fbc5750508515610fad57610ebb906113f9565b6020815191012091610ecc81610db5565b925f5b828110610f205750505091610e93916060610ef494015191606086015190519261154e565b610f1157608092610f086020938388611666565b85520151015190565b63528bd3ef60e01b5f5260045ffd5b80610f3260019260208a510151610d89565b516020610f40825186611538565b91015190604051610f7e81610f706020820194856014916bffffffffffffffffffffffff199060601b1681520190565b03601f198101835282610141565b519020610f89610173565b9182526020820152610f9b8288610d89565b52610fa68187610d89565b5001610ecf565b6323188e3960e21b5f5260045ffd5b610dad60f31b610fef610fe2610fd3848751610d89565b51516001600160f01b03191690565b6001600160f01b03191690565b1480611027575b611003575b600101610ea3565b9650600161101f60206110178a8651610d89565b510151611313565b979050610ffb565b50602080611036838651610d89565b5101515114610ff6565b633aa90f7f60e21b5f5260045ffd5b50606086015190610e80565b637202e68560e11b5f5260045ffd5b905060608901515114155f610e64565b805191908290602001825e015f815290565b6040516001600160e01b031990911660208201529190610171908390610cef90602483019061107a565b815151916110c383610db5565b916110cd84610b2d565b935f5b81811061111657506110e3575b50505090565b6110fb928260406020610e939501519101519261154e565b611107575f80806110dd565b630b92186960e31b5f5260045ffd5b611121818551610d89565b51906040820191611132835161185b565b92602084019182511561121357600194816111ac60206111de94519201946111a661119f611164885163ffffffff1690565b6001600160e01b03199063ff00ff00600882811b9190911691901c62ff00ff1617601081811b63ffff00001691901c61ffff161760e01b1690565b91516119a7565b9061108c565b602081519101206111bb610173565b91825260208201526111cd868c610d89565b526111d8858b610d89565b506119cc565b905191516111ea610182565b928352602083015260408201526112018289610d89565b5261120c8188610d89565b50016110d0565b63b4eb9e5160e01b5f5260045ffd5b9190915f915b81518310156112925780602061123e8585610d89565b51015103611288575060406112596112669361126193610d89565b51015161185b565b612507565b906001600160401b03808351169116145f14611283576040015190565b505f90565b9160010191611228565b505050505f90565b634e487b7160e01b5f52601160045260245ffd5b90600182018092116112bc57565b61129a565b90600382018092116112bc57565b90600282018092116112bc57565b919082018092116112bc57565b906001600160ff1b03811681036112bc5760039060011b0490600182018092116112bc57101590565b6020815110611323576020015190565b60405162461bcd60e51b8152602060048201526024808201527f42797465733a3a20746f427974657333323a206461746120697320746f20736860448201526337b93a1760e11b6064820152608490fd5b60405190611383602083610141565b5f8252565b610cef6113a794936113a761017194604051978895602087019061107a565b9061107a565b610171926113a795946113cc600c94604051988995602087019061107a565b6001600160e01b03199290921682526001600160c01b031916600482015203601319810185520183610141565b90815151611405611374565b905f5b8181106114da57509261141e610a439394611b8c565b916114d461144a604061143b611164602087015163ffffffff1690565b9401516001600160401b031690565b67ffffffffffff000067ff00ff00ff00ff0066ff00ff00ff00ff8360081c169260081b169165ffff0000ffff65ffff0000ff0065ffffffffffff67ffff0000ffff0000861666ff0000ffff000085161760101c16941691161760101b161767ffffffff0000000063ffffffff8260201c169160201b166001600160401b0360c01b911760c01b1690565b926113ad565b91611531600191610f706115136114f5610fd3888b51610d89565b6040516001600160f01b031990911660208201529182906022820190565b61152b6020611523888b51610d89565b5101516119a7565b91611388565b9201611408565b610a439161154591611d09565b90929192611d61565b91939290811561161757845161156381611ddd565b61156c82611ddd565b91600161157886612816565b1b5f805b838210611597575050505061159394959650612841565b1490565b6115a1828c610d89565b515190888210156116085782151590816115fd575b506115ee5760019060206115ca848e610d89565b5101516115d78489610d89565b528084016115e58488610d89565b5291019061157c565b630647f54960e21b5f5260045ffd5b90508111155f6115b6565b630834466160e31b5f5260045ffd5b639136328760e01b5f5260045ffd5b604080519091906116378382610141565b6001815291601f1901825f5b82811061164f57505050565b60209061165a610d9d565b82828501015201611643565b610e9391611749936040602083019261170d61170860206116e4875161168d815160ff1690565b9061169e8482015163ffffffff1690565b90888101516116d160806060840151930151936116c56116bc610191565b60ff9097168752565b63ffffffff1685880152565b8984015260608301526080820152611e86565b8051908201209701518651602001516117029063ffffffff16610bcd565b90611f7a565b6112ae565b9460a0611718611626565b9551015190611725610173565b918252602082015261173685610d77565b5261174084610d77565b50015190611f8f565b61110757565b6040519060a082018281106001600160401b038211176100eb5760405260606080835f81525f60208201525f60408201525f838201520152565b60405190611796826100f0565b60606020835f81520152565b6040519061012082018281106001600160401b038211176100eb576040525f610100838281526117d0611789565b60208201528260408201526117e3611789565b60608201528260808201526117f6611789565b60a08201528260c0820152606060e08201520152565b9061181682610226565b6118236040519182610141565b8281528092611834601f1991610226565b01905f5b82811061184457505050565b60209061184f6117a2565b82828501015201611838565b61186361174f565b5061186c610173565b9081525f602082015261188661188182611f9b565b611313565b90611890816120da565b61189c61188183611f9b565b6118a861188184611f9b565b916118b2846120da565b6118bb8161180c565b945f5b8281106118ea575050506118d0610191565b948552602085015260408401526060830152608082015290565b6001906118f683612259565b60ff6119006117a2565b91168061192c5750600160c08201525b61191a828a610d89565b526119258189610d89565b50016118be565b60048103611950575060016040820152611945846122d3565b60608201525b611910565b60058103611973575060016080820152611969846122d3565b60a0820152611910565b60068103611993575060018152611989846122d3565b6020820152611910565b60080361194b576001610100820152611910565b610a436113a791610f706119bb8251611b8c565b91604051948593602085019061107a565b6119d4610abd565b505f5f925f5f5b6080850180518051831015611b055760406119f984611a0193610d89565b510151151590565b80611add575b611a94575b611a1c60406119f9848451610d89565b80611a5b575b611a30575b506001016119db565b819250611a5360206060611a4960809560019551610d89565b510151015161248f565b929150611a27565b50634953544d60e01b63ffffffff60e01b611a8d6060611a7c868651610d89565b510151516001600160e01b03191690565b1614611a22565b95509250611ab761188160206060611aad878a51610d89565b51015101516123af565b92611ad761188160206060611acd858b51610d89565b5101510151612429565b95611a0c565b5063049534d560e41b63ffffffff60e01b611afe6060611a7c868651610d89565b1614611a07565b505050925092908215611b2a57611b1a610182565b9283526020830152604082015290565b633eba99d160e21b5f5260045ffd5b6003198101919082116112bc57565b5f198101919082116112bc57565b60200390602082116112bc57565b919082039182116112bc57565b600190610a43939260ff60f81b9060f81b168152019061107a565b6040811015611bd057610a43611bae611ba8610f709360021b90565b60ff1690565b60405160f89190911b6001600160f81b03191660208201529182906021820190565b614000811015611c2f57610a43611c0d611bfa611bf3611708610f709560021b90565b61ffff1690565b60ff61ff008260081b169160081c161790565b60405160f09190911b6001600160f01b03191660208201529182906022820190565b6340000000811015611cac57610a43611c8a611c59610bcd611c54610f709560021b90565b6112cf565b63ffffff0062ff00ff62ffffff818460081c1616921660081b161763ffff000061ffff8260101c169160101b161790565b60405160e09190911b6001600160e01b03191660208201529182906024820190565b610f70611cd1611cbe611cd693612627565b6040519283916020830160209181520190565b612740565b610a43611cf7611ba8611cf2611cec8551611b39565b60021b90565b6112c1565b610f7060405193849260208401611b71565b8151919060418303611d3957611d329250602082015190606060408401519301515f1a90612789565b9192909190565b50505f9160029190565b60041115611d4d57565b634e487b7160e01b5f52602160045260245ffd5b611d6a81611d43565b80611d73575050565b611d7c81611d43565b60018103611d935763f645eedf60e01b5f5260045ffd5b611d9c81611d43565b60028103611db7575063fce698f760e01b5f5260045260245ffd5b80611dc3600392611d43565b14611dcb5750565b6335e2f38360e21b5f5260045260245ffd5b90611de782610226565b611df46040519182610141565b8281528092611e05601f1991610226565b0190602036910137565b949291969593909660405197889660208801611e2a9161107a565b9063ffffffff60e01b168152600401611e429161107a565b916001600160401b0360c01b16825263ffffffff60e01b166008820152600c01611e6b9161107a565b611e749161107a565b03601f19810183526101719083610141565b611f67611f44610cef610a4393611ec3611ea1825160ff1690565b60405160f89190911b6001600160f81b03191660208201529283906021820190565b610cef611eda611164602084015163ffffffff1690565b611ef560408401516040519384916020830160209181520190565b606083015193611f756080611f1461144a88516001600160401b031690565b95611f526040611f2e61116460208c015163ffffffff1690565b9901516040519a8b916020830160209181520190565b03601f1981018b528a610141565b01516040519889916020830160209181520190565b03601f198101895288610141565b611e0f565b80611f83575090565b81039081116112bc5790565b929091611593926129b6565b60208101908151602081018091116112bc57815151106100b8576020905181835182010191829101116112bc576020611fd391612bee565b90805190602082018092116112bc575290565b90602082019182518281018091116112bc57815151106100b8578115612034576020905181845182010191829101116112bc578161202391612bee565b9180519182018092116112bc575290565b505050604051612045602082610141565b5f815290565b60ff60049116019060ff82116112bc57565b1561206457565b60405162461bcd60e51b815260206004820152602860248201527f756e657870656374656420707265666978206465636f64696e6720436f6d706160448201526731ba1e2ab4b73a1f60c11b6064820152608490fd5b906001600160401b03809116911601906001600160401b0382116112bc57565b6120e381612259565b60038116806120fd5750610a43915060021c603f16611ba8565b6001810361214e57506121429061213c611ba8612132612122611ba8610a4397612259565b60061b67ffffffffffffffc01690565b9260021c603f1690565b906120ba565b6001600160401b031690565b600281036121c45750610bcd906121b760ff846121a882612171610a4398612259565b95816121978161218961218388612259565b97612259565b991660081b63ffffff001690565b911617921660101b63ffff00001690565b17921660181b63ff0000001690565b1760021c633fffffff1690565b60030361220357610a439160ff6121ea6121e56121fe94603f9060021c1690565b61204b565b16906121f9600883111561205d565b611fe6565b61248f565b60405162461bcd60e51b815260206004820152601a60248201527f436f64652073686f756c6420626520756e726561636861626c650000000000006044820152606490fd5b908151811015610d84570160200190565b60208101908151600181018091116112bc578151511061229f575181516001600160f81b03199161228a9190612248565b511660f81c9061229a81516112ae565b905290565b60405162461bcd60e51b815260206004820152600c60248201526b4f7574206f662072616e676560a01b6044820152606490fd5b6122db611789565b5060208101908151600481018091116112bc57815151106100b8576020815181845182010191829101116112bc57600461231491612bee565b91805190600482018092116112bc57525f915f905b6004821061236957505080612340612346926120da565b90611fe6565b612361612351610173565b6001600160e01b03199093168352565b602082015290565b90926001600160f81b031961237e8584612248565b5116908460031b91858304600814861517156112bc576001926001600160e01b0319918216901c1617930190612329565b80516020116100b857602080610a439201612bee565b80516008116100b85760086020610a439201612bee565b8051600c116100b857602881019060200181106112bc576004610a4391612bee565b90815181116100b8578015612419576020610a439201612bee565b5050604051612045602082610141565b8051806020116100b857601f1981019081116112bc57604082019160200182106112bc57610a4391612bee565b805180600c116100b857600b1981019081116112bc57602c82019160200182106112bc57610a4391612bee565b80156112bc575f190190565b80515f91815b61249e57505090565b90915f198301908382116112bc576124b68284612248565b5160f81c91600381901b906001600160fd1b038116036112bc5760ff81116112bc576001901b918281029281840414901517156112bc57612500916124fa916112dd565b92612483565b9081612495565b61250f610abd565b5f915b608081018051805185101561261f57610e9360406119f98761253394610d89565b612613576341504b4360e01b61255f6125526060611a7c888651610d89565b6001600160e01b03191690565b036126135760606125738560209351610d89565b510151015191602c835103612606576125916121426121fe856123c5565b936001600160401b038516156125f2575050506125ea6125c26118816125bc610bcd6121fe866123dc565b93612456565b916125dd6125ce610182565b6001600160401b039095168552565b63ffffffff166020840152565b604082015290565b608092945060019193505b01929050612512565b91509160016080916125fd565b509160016080916125fd565b505050919050565b8060081c9060081b907cff000000ff000000ff000000ff000000ff000000ff000000ff000000ff7dff000000ff000000ff000000ff000000ff000000ff000000ff000000ff007fff000000ff000000ff000000ff000000ff000000ff000000ff000000ff00000084167eff000000ff000000ff000000ff000000ff000000ff000000ff000000ff000084161760101c931691161760101b177bffffffff00000000ffffffff00000000ffffffff00000000ffffffff7fffffffff00000000ffffffff00000000ffffffff00000000ffffffff00000000821660201c911660201b1777ffffffffffffffff0000000000000000ffffffffffffffff8019821660401c911660401b1761273c8160801c9160801b90565b1790565b80515f1981019081116112bc575b6001600160f81b03196127618284612248565b51166127755761277090612483565b61274e565b600181018091116112bc57610a43916123fe565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0841161280b579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15612800575f516001600160a01b038116156127f657905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f9160039190565b6001811115611283575f1981019081116112bc5761283390612c8f565b600181018091116112bc5790565b9192905f83515b600161285386610d77565b51146129a35761287f61287a85600161287461286e8a610d77565b51612c8f565b1b6112dd565b611b48565b905f915f915b8083106128a85750505061289c6128a291946112ae565b60011c90565b92612848565b9091926128b58489610d89565b51826128c0866112ae565b1080612985575b15612926579061291260019260026128fd6128e2898c610d89565b516128f56128ef8b6112ae565b8d610d89565b519084612cf5565b9701965b61290b848b610d89565b5260011c90565b61291c828b610d89565b5201929190612885565b8987600183188610612968579160018061295e612912946129568c61294f8d9e9d869b9a610d89565b5192610d89565b519085612cf5565b9801980196612901565b612912915091600161297c88829695610d89565b51970196612901565b50612998612992866112ae565b8a610d89565b5160018218146128c7565b50509150506129b29150610d77565b5190565b9290928215611617578351938415612bdf5760015b858110612bb157506001841480612ba7575b80612b95575b612b7a576129f86129f385612d15565b611ddd565b94612a01610173565b945f865260208601968752612a14610173565b925f845260208401948552612a27610182565b905f82526020820193845260408201525f91805b612ac6575b50505051612ab7575190515103612aa85781515f190182525b815115612a9c57612a6982612fb7565b612a7283612fb7565b90612a7e845160010190565b84525f5260205260405f20612a968451845190610d89565b52612a59565b916129b2915051610d77565b637227423160e11b5f5260045ffd5b63072afb8760e51b5f5260045ffd5b612acf81612c8f565b92612ae9612ae16001861b8094611b64565b9283926112dd565b938685612af68187612d5a565b92602084015180155f14612b2e575050505050508551518551145f03612a405780612b29612b2387612f9b565b8a612f80565b612a3b565b60011480612b72575b15612b5e575050506020612b55826040612b29940151905190610d89565b5101518a612f80565b91612b2993916002612b23941b0391612de3565b508015612b37565b925090925051612aa857612b8f602091610d77565b51015190565b50612b9f81610d77565b5151156129e3565b50600185146129dd565b612bbb8183610d89565b5151612bcf612bc983611b48565b84610d89565b515110156115ee576001016129cb565b631a14a47760e31b5f5260045ffd5b919091612bfa8361023d565b612c076040519182610141565b838152612c138461023d565b602082019190601f1901368337939091905b6020811015612c5f5780612c4557505f19905b5182518216911916179052565b61287a612c54612c5992611b56565b612fd3565b90612c38565b909182518152602081018091116112bc5791602081018091116112bc5790601f19810190811115612c255761129a565b806fffffffffffffffffffffffffffffffff1060071b81811c6001600160401b031060061b1781811c63ffffffff1060051b1781811c61ffff1060041b1781811c60ff1060031b1781811c600f1060021b1781811c60031060011b1790811c6001101790565b600116612d08575f5260205260405f2090565b905f5260205260405f2090565b90815f925b612d215750565b915f1983018381116112bc57600193169283910192612d1a565b60405190612d488261010b565b60606040835f81525f60208201520152565b612d62612d3b565b508051612d7560208301918251906112dd565b928251905b848210612dbc575b50829350612d9561229a92935182611b64565b908451946040810151612da6610182565b9687528360208801526040870152528251611b64565b90612dcb816040860151610d89565b5151821115612ddd5760010190612d7a565b90612d82565b91906020830151835193612df682611ddd565b93612e0083611ddd565b955f5b848110612f325750505050935b6001612e1b84610d77565b5114612f25575f945f905b808210612e3a575050848084528452612e10565b9095612e468786610d89565b51878784612e53836112ae565b1080612f07575b15612eae57600192612e8c83612e846128ef612e7d612ea89997612e9697610d89565b51926112ae565b519083612cf5565b61290b848c610d89565b612ea08289610d89565b5201966112cf565b90612e26565b505084516020860151511115612ef857600191612ee48392612e8c612ed38c8c610d89565b51612edd8a612f9b565b9083612cf5565b612eee8289610d89565b5201960190612e26565b63d8f29a1560e01b5f5260045ffd5b50612f1a612f14836112ae565b89610d89565b516001841814612e5a565b935050506129b290610d77565b600190612f6b604086016020612f538251612f4d86896112dd565b90610d89565b510151612f60848d610d89565b525182850190610d89565b51518401612f79828a610d89565b5201612e03565b90612f916020830151835190610d89565b5260018151019052565b612fab6020820151825190610d89565b51906001815101905290565b612fc76020820151825190610d89565b5181515f190190915290565b601f81116112bc576101000a9056fea2646970667358221220b54cab27f9309d22218000ba9b81a2beac680cddb6189b628244d20391b8b15264736f6c634300081e0033","sourceMap":"2797:9411:121:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;:::i;:::-;;:::i;:::-;;:::i;:::-;;;;;;-1:-1:-1;;2797:9411:121;;;;;;;;;;;;;;;;-1:-1:-1;;;3997:45:121;;;:85;;;;2797:9411;;;;;;;;;3997:85;-1:-1:-1;;;829:40:49;;-1:-1:-1;3997:85:121;;;2797:9411;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;:::o;:::-;;:::i;:::-;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;:::o;:::-;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;:::o;:::-;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;:::o;:::-;;;;;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;:::o;:::-;;;;;;;;:::i;:::-;:::o;:::-;7134:25;2797:9411;;;7134:25;2797:9411;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;-1:-1:-1;;2797:9411:121;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;:::o;:::-;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;:::o;:::-;-1:-1:-1;;;;;2797:9411:121;;;;;;-1:-1:-1;;2797:9411:121;;;;:::o;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;-1:-1:-1;2797:9411:121;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;:::o;:::-;;;-1:-1:-1;;;;;2797:9411:121;;;;;;:::o;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;-1:-1:-1;;2797:9411:121;;;;;;;;;;:::i;:::-;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;-1:-1:-1;;2797:9411:121;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;;2797:9411:121;;;;;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;:::i;:::-;;;;;;;;;:::i;:::-;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;:::i;:::-;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;:::i;:::-;;;;;:::o;:::-;;;;;;;;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;-1:-1:-1;;2797:9411:121;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;-1:-1:-1;;2797:9411:121;;;;;;;:::i;:::-;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;:::i;:::-;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;-1:-1:-1;;2797:9411:121;;;;;;-1:-1:-1;;;2797:9411:121;;;;;;;;;;;-1:-1:-1;;2797:9411:121;;;;;;;3131:38;2797:9411;;;;;;;;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;;;:::o;:::-;;;;;;-1:-1:-1;;2797:9411:121;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;:::i;:::-;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;:::i;:::-;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;2797:9411:121;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;2797:9411:121;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;:::o;:::-;;;;;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;:::i;:::-;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;:::i;:::-;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;-1:-1:-1;2797:9411:121;;;;;;;;;;;;:::o;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;2797:9411:121;;;;;;;;;:::o;:::-;;;;;;;;:::i;:::-;-1:-1:-1;2797:9411:121;;-1:-1:-1;2797:9411:121;;;;;;:::i;:::-;;;;;;;;;;;;;;4251:1982;4475:48;4251:1982;4611:52;4251:1982;;;;;4475:48;;;;:::i;:::-;4611:52;;;;;:::i;:::-;2797:9411;;;;;4865:76;2797:9411;4896:45;:22;;:33;:45;2797:9411;;;;;;;;;;4865:76;-1:-1:-1;4865:76:121;4861:206;;6160:20;5135:43;5675:36;4896:45;5231:48;5135:43;;6160:20;5135:43;;;:::i;:::-;5231:48;;;;:::i;:::-;5675:19;;;:36;;2797:9411;;-1:-1:-1;;;;;2797:9411:121;5739:25;5675:36;5739:25;;;;;2797:9411;-1:-1:-1;;;;;2797:9411:121;;;;5725:42;5721:421;;4251:1982;2797:9411;;;;;6160:20;;;4896:45;6160:20;;;:::i;:::-;;2797:9411;;6160:20;;;;;;:::i;:::-;6197:25;2797:9411;6152:74;;;4251:1982;:::o;5721:421::-;5783:28;;;:56;2797:9411;;-1:-1:-1;;;;;2797:9411:121;5950:12;2797:9411;5783:28;5997:71;2797:9411;4896:45;5950:12;;2797:9411;;;;;;6019:20;;6054:13;;5997:71;;:::i;:::-;6103:13;;2797:9411;;-1:-1:-1;;;;;2797:9411:121;;:::i;:::-;;;;;;4896:45;5881:250;;2797:9411;5783:28;5881:250;;2797:9411;5675:36;5881:250;;2797:9411;5853:278;;5721:421;;;;;4861:206;-1:-1:-1;2797:9411:121;;;-1:-1:-1;4965:26:121;2797:9411;4965:26;;4896:45;4965:26;;;:::i;:::-;;2797:9411;;4965:26;;;;;;:::i;:::-;5021:31;4993:26;;:::i;:::-;5021:31;;;2797:9411;4957:99;;;;:::o;2797:9411::-;;;;;;;:::i;:::-;-1:-1:-1;2797:9411:121;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;:::i;:::-;;-1:-1:-1;2797:9411:121;;-1:-1:-1;2797:9411:121;;;;;;:::i;:::-;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;:::i;:::-;;;;;;;;;;;;;;;:::o;:::-;;;;;;;:::i;:::-;-1:-1:-1;2797:9411:121;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;2797:9411:121;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;;;;6684:2343;;;2797:9411;;:::i;:::-;;6905:27;;:33;;;;;2797:9411;6978:38;;:50;6955:73;6978:50;6905:33;6978:50;;2797:9411;;;;;;6955:73;7134:25;;;;2797:9411;;;-1:-1:-1;;;;;2797:9411:121;;;;7163:32;7134:25;7163:32;;;-1:-1:-1;;;;;7163:32:121;;2797:9411;;;7134:64;;;;:145;;;;6684:2343;7117:226;;;2797:9411;7410:32;;2797:9411;;7410:32;;-1:-1:-1;;;;;2797:9411:121;7381:64;7502:87;;;;6905:33;7640:16;;2797:9411;7603:54;7604:53;2797:9411;;7604:53;;:::i;:::-;7603:54;;2797:9411;7603:54;7599:90;;7724:18;;2797:9411;7759:15;2797:9411;7789:13;2797:9411;7804:17;;;;;;8047:21;;;;8043:54;;8176:24;;;:::i;:::-;6905:33;2797:9411;;;;8166:35;8256;;;;:::i;:::-;8306:13;2797:9411;8321:10;;;;;;8706:28;;;;8664:129;8706:28;;8807:6;8706:28;;2797:9411;8736:16;8706:28;8736:16;;;2797:9411;;8664:129;;:::i;8807:6::-;8803:44;;8989:30;8898:7;;6905:33;8898:7;;;;:::i;:::-;2797:9411;;8989:24;;:30;2797:9411;6684:2343;:::o;8803:44::-;8822:25;;;2797:9411;8822:25;;2797:9411;8822:25;8333:3;8371:27;:36;2797:9411;8371:27;6905:33;8371:27;;:33;;:36;:::i;:::-;;6905:33;8441:45;8471:14;;8441:45;;:::i;:::-;8563:19;;2797:9411;;7134:25;2797:9411;8600:27;;;6905:33;8600:27;;;;2797:9411;;;;;;;;;;;;;8600:27;;2797:9411;;8600:27;;;;;;:::i;:::-;2797:9411;8590:38;;2797:9411;;:::i;:::-;;;;6905:33;8533:97;;2797:9411;8500:130;;;;:::i;:::-;;;;;;:::i;:::-;;2797:9411;8306:13;;8043:54;8077:20;;;2797:9411;8077:20;;2797:9411;8077:20;7823:3;2797:9411;;;7846:47;:24;:21;:18;;;:21;:::i;:::-;;2797:9411;-1:-1:-1;;;;;;2797:9411:121;;;7846:24;-1:-1:-1;;;;;;2797:9411:121;;;7846:47;;:90;;;7823:3;7842:182;;7823:3;2797:9411;;7789:13;;7842:182;7982:18;;2797:9411;7966:43;6905:33;7982:21;:18;;;:21;:::i;:::-;;:26;;7966:43;:::i;:::-;7842:182;;;;;7846:90;7897:18;6905:33;7897:18;:21;:18;;;:21;:::i;:::-;;:26;;2797:9411;7897:39;7846:90;;7599;7666:23;;;2797:9411;7666:23;;2797:9411;7666:23;7502:87;7560:29;;;;;7502:87;;;7117:226;7311:21;;;2797:9411;7311:21;;2797:9411;7311:21;7134:145;7247:29;;;;;;2797:9411;7218:61;;7134:145;;;2797:9411;;;;;;;;;;;;;;;;:::o;:::-;;;-1:-1:-1;;;;;;2797:9411:121;;;;;;;;;;;;;;;;;;;;:::i;10246:1266::-;10433:16;;2797:9411;10506:32;;;;:::i;:::-;10591:28;;;;:::i;:::-;10635:13;-1:-1:-1;10650:7:121;;;;;;11301;11297:178;;10630:657;11485:20;;;10246:1266;:::o;11297:178::-;11432:6;11377:11;;10777;10807:13;11337:77;11377:11;;;11398:15;;2797:9411;11337:77;;:::i;11432:6::-;11428:36;;11297:178;;;;;11428:36;11447:17;;;-1:-1:-1;11447:17:121;;-1:-1:-1;11447:17:121;10659:3;10702:19;:16;;;:19;:::i;:::-;;10777:11;;;;;10758:31;10777:11;;10758:31;:::i;:::-;10807:13;;;;2797:9411;;;10807:18;10803:52;;2797:9411;;;10959:87;10807:13;11112:24;2797:9411;;10999:7;;2797:9411;11010:35;10972:36;10992:15;2797:9411;;;;;;10992:15;-1:-1:-1;;;;;;2797:9411:121;;;;;;;;;;;;;8200:10:65;2797:9411:121;8168:49:65;2797:9411:121;;;;;;;;;;;8266:21:65;2797:9411:121;;;8815:111:65;;10972:36:121;11033:11;;11010:35;:::i;:::-;10959:87;;:::i;:::-;10807:13;2797:9411;;;;10949:98;2797:9411;;:::i;:::-;;;;10807:13;10882:179;;2797:9411;10870:191;;;;:::i;:::-;;;;;;:::i;:::-;;11112:24;:::i;:::-;2797:9411;;;;;;:::i;:::-;;;;10807:13;11185:91;;2797:9411;10777:11;11185:91;;2797:9411;11150:126;;;;:::i;:::-;;;;;;:::i;:::-;;2797:9411;10635:13;;10803:52;10834:21;;;-1:-1:-1;10834:21:121;;-1:-1:-1;10834:21:121;7946:368:119;;;;2797:9411:121;8108:182:119;8151:3;2797:9411:121;;8128:21:119;;;;;8174:13;:16;:13;;;;:::i;:::-;;:16;2797:9411:121;8174:32:119;8170:46;;8251:13;:20;:13;8445:36;8251:13;8445:20;8251:13;;:::i;:::-;;:20;;8445;:::i;:::-;:36;:::i;:::-;2797:9411:121;-1:-1:-1;;;;;2797:9411:121;;;;8498:45:119;2797:9411:121;8498:21:119;:45;2797:9411:121;;;8251:20:119;8522:17;2797:9411:121;8230:49:119;:::o;8498:45::-;;2797:9411:121;8230:49:119;:::o;8170:46::-;8208:8;2797:9411:121;;8113:13:119;;;8128:21;;;;;2797:9411:121;7946:368:119;:::o;2797:9411:121:-;;;;;;;;;;;;;;9792:1;2797:9411;;;;;;;:::o;:::-;;:::i;:::-;;5093:1:65;2797:9411:121;;;;;;;:::o;:::-;;4835:1:65;2797:9411:121;;;;;;;:::o;:::-;;;;;;;;;;:::o;11911:146::-;;-1:-1:-1;;;;;2797:9411:121;;;;;;12044:1;2797:9411;;;;;;;;;;;;;12022:28;;11911:146;:::o;4543:226:58:-;4650:2;2797:9411:121;;4635:17:58;2797:9411:121;;4650:2:58;4703:60;;4543:226;:::o;2797:9411:121:-;;;-1:-1:-1;;;2797:9411:121;;4650:2:58;2797:9411:121;;;;;;;;;;;;;;-1:-1:-1;;;2797:9411:121;;;;;;;;;;;;;;;:::i;:::-;;;;:::o;:::-;;;;;;;;;;;;;;;;;;:::i;:::-;;;:::i;:::-;;;;;;;;;;;;;;;;;;;:::i;:::-;-1:-1:-1;;;;;;2797:9411:121;;;;;;-1:-1:-1;;;;;;2797:9411:121;;;;;;-1:-1:-1;;2797:9411:121;;;;;;;:::i;1604:684:119:-;;1718:18;;2797:9411:121;;;:::i;:::-;1800:13:119;-1:-1:-1;1815:14:119;;;;;;2093:40;;;2067:214;2093:40;;;:::i;:::-;2188:22;2225:46;2797:9411:121;;2168:43:119;2797:9411:121;1915:42:119;2188:22;;2797:9411:121;;;;;2168:43:119;2245:25;;2797:9411:121;-1:-1:-1;;;;;2797:9411:121;;;;;7788:18:65;2797:9411:121;;;;;;;;;;7937:18:65;7933:22;2797:9411:121;7902:18:65;7898:22;;;;;;2797:9411:121;;;7932:30:65;7933:22;;;;2797:9411:121;;;7896:67:65;2797:9411:121;;;;;;8025:7:65;2797:9411:121;;;-1:-1:-1;;;;;2797:9411:121;;8012:21:65;;2797:9411:121;;;8698:111:65;;2225:46:119;2067:214;;:::i;1831:3::-;1932:18;1860:179;2797:9411:121;1932:18:119;1915:42;;1932:24;:21;:18;;;:21;:::i;:24::-;2797:9411:121;;-1:-1:-1;;;;;;2797:9411:121;;;1915:42:119;;;2797:9411:121;;;;;;;;;1915:42:119;1975:50;1915:42;1998:21;:18;;;:21;:::i;:::-;;:26;;1975:50;:::i;:::-;1860:179;;:::i;:::-;1831:3;2797:9411:121;1800:13:119;;3714:255:48;3927:8;3714:255;3871:27;3714:255;3871:27;:::i;:::-;3927:8;;;;;:::i;1990:238:56:-;;;;;3884:14;;3880:38;;2797:9411:121;;3995:18:56;;;:::i;:::-;4049;;;:::i;:::-;4106:20;4101:1;4106:20;;;:::i;:::-;2797:9411:121;-1:-1:-1;;4179:7:56;;;;;;4577:42;;;;;;;;;;:::i;:::-;2174:47;1990:238;:::o;4168:9::-;4217;;;;:::i;:::-;;2797:9411:121;4250:16:56;;;;;4246:51;;4315:6;;;:26;;;;4168:9;4311:55;;;4101:1;4392:9;:14;:9;;;;:::i;:::-;;:14;2797:9411:121;4380:26:56;;;;:::i;:::-;2797:9411:121;;;;4448:33:56;;;;:::i;:::-;2797:9411:121;;;4168:9:56;;;4311:55;4350:16;;;-1:-1:-1;4350:16:56;;-1:-1:-1;4350:16:56;4315:26;4325:16;;;;;4315:26;;;4246:51;4275:22;;;-1:-1:-1;4275:22:56;;-1:-1:-1;4275:22:56;3880:38;3907:11;;;-1:-1:-1;3907:11:56;;-1:-1:-1;3907:11:56;2797:9411:121;;;;;;;;;;;:::i;:::-;9792:1;2797:9411;;;-1:-1:-1;;2797:9411:121;;-1:-1:-1;2797:9411:121;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;;;;9075:1054;10000:75;9075:1054;10090:6;9075:1054;9478:30;9349:19;;;;9711:82;:78;9349:19;9268:403;9349:19;;2797:9411;;;;;;;;9412:32;2797:9411;9412:32;;;2797:9411;;;;;;9478:30;;;;2797:9411;9298:359;9613:25;9548:36;;;;9613:25;;2797:9411;;9298:359;2797:9411;;:::i;:::-;;;;;;;;9298:359;2797:9411;;9298:359;;;2797:9411;;9298:359;;;;2797:9411;9548:36;9298:359;;2797:9411;9613:25;9298:359;;2797:9411;9268:403;:::i;:::-;2797:9411;;;;;9245:436;9721:33;;2797:9411;9756:19;;9349;9756:32;2797:9411;9711:78;;2797:9411;;9756:32;2797:9411;9711:78;;;:::i;:::-;:82;:::i;:::-;9846:33;2797:9411;9846:33;;:::i;:::-;9934:19;;:29;2797:9411;;;;:::i;:::-;;;;9349:19;9901:76;;2797:9411;9889:88;;;:::i;:::-;;;;;:::i;:::-;;10041:14;;10000:75;;:::i;10090:6::-;10086:36;;9075:1054::o;2797:9411::-;;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;-1:-1:-1;2797:9411:121;;-1:-1:-1;2797:9411:121;;;;-1:-1:-1;2797:9411:121;;;;-1:-1:-1;2797:9411:121;;;;;;:::o;:::-;;;;;;;:::i;:::-;;;;-1:-1:-1;2797:9411:121;;;;:::o;:::-;;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;-1:-1:-1;2797:9411:121;;;;;;;:::i;:::-;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;-1:-1:-1;2797:9411:121;;;;;;;;;:::o;:::-;;;;;:::i;:::-;;;;;;;;;;2883:1495:119;2797:9411:121;;:::i;:::-;;;;:::i;:::-;;;;3019:1:119;3000:21;;;2797:9411:121;3052:38:119;3068:21;;;:::i;:::-;3052:38;:::i;:::-;3122:35;;;;:::i;:::-;3187:38;3203:21;;;:::i;3187:38::-;3260;3276:21;;;:::i;3260:38::-;3326:35;;;;:::i;:::-;3397:20;;;:::i;:::-;3433:13;3019:1;3448:10;;;;;;2797:9411:121;;;;;:::i;:::-;;;;3000:21:119;4304:67;;2797:9411:121;;4304:67:119;;2797:9411:121;4304:67:119;;;2797:9411:121;4304:67:119;;;2797:9411:121;2883:1495:119;:::o;3460:3::-;2797:9411:121;3492:21:119;;;;:::i;:::-;2797:9411:121;;;:::i;:::-;;;3565:25:119;;;-1:-1:-1;2797:9411:121;3610:14:119;;;1540:1;3610:21;4257:19;;;;:::i;:::-;;;;;;:::i;:::-;;2797:9411:121;3433:13:119;;3561:683;1361:1;3656:29;;1361:1;;-1:-1:-1;2797:9411:121;;3705:18:119;;1540:1;3767:23;;;:::i;:::-;3748:16;;;:42;3652:592;3561:683;;3652:592;1411:1;3815:24;;1411:1;;-1:-1:-1;2797:9411:121;3859:13:119;;;1540:1;3911:23;;;:::i;:::-;3897:11;;;:37;3561:683;;3811:433;1467:1;3959:30;;1467:1;;-1:-1:-1;2797:9411:121;1540:1:119;;4073:23;;;:::i;:::-;3000:21;4053:17;;:43;3561:683;;3955:289;1540:1;4121:47;3811:433;4117:127;2797:9411:121;4188:34:119;;;1540:1;3561:683;;9049:172:65;9158:56;2797:9411:121;9049:172:65;2797:9411:121;9175:31:65;2797:9411:121;;9175:31:65;:::i;:::-;2797:9411:121;;;9158:56:65;;;;;;2797:9411:121;;:::i;8762:967:123:-;2797:9411:121;;:::i;:::-;;-1:-1:-1;;8920:17:123;-1:-1:-1;;8993:3:123;8972:12;;;;;2797:9411:121;;8968:23:123;;;;;9016:27;:15;;:27;:15;;:::i;:::-;;:27;2797:9411:121;;;;;9016:27:123;:89;;;8993:3;9012:305;;8993:3;9335:27;9016;9335:15;:12;;;:15;:::i;:27::-;:89;;;8993:3;9331:196;;8993:3;-1:-1:-1;2797:9411:121;;8953:13:123;;9331:196;9481:12;;;9456:56;9481:30;:25;:15;8972:12;9481;2797:9411:121;9481:12:123;;:15;:::i;:::-;;:25;;:30;;9456:56;:::i;:::-;9331:196;;;;;9335:89;8237:14;;;;2797:9411:121;;;9366:37:123;:25;:15;:12;;;:15;:::i;:::-;;:25;;2797:9411:121;-1:-1:-1;;;;;;2797:9411:121;;;9366:37:123;2797:9411:121;9366:58:123;9335:89;;9012:305;9164:12;;;;9135:68;9151:51;9164:30;:25;:15;:12;;;:15;:::i;:::-;;:25;;:30;;9151:51;:::i;9135:68::-;9266:12;9237:65;9253:48;9164:30;:25;9266:15;:12;;;:15;:::i;:::-;;:25;;:30;;9253:48;:::i;9237:65::-;9012:305;;;9016:89;2797:9411:121;;;;;;;9047:37:123;:25;:15;:12;;;:15;:::i;:37::-;2797:9411:121;9047:58:123;9016:89;;8968:23;;;;;;;;9575:14;;9571:46;;2797:9411:121;;:::i;:::-;;;;9635:87:123;;;2797:9411:121;9016:27:123;9635:87;;2797:9411:121;8762:967:123;:::o;9571:46::-;9598:19;;;-1:-1:-1;9598:19:123;;-1:-1:-1;9598:19:123;2797:9411:121;-1:-1:-1;;2797:9411:121;;;;;;;;:::o;:::-;-1:-1:-1;;2797:9411:121;;;;;;;;:::o;:::-;;;;;;;;;:::o;:::-;;;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;:::i;4484:686:65:-;4577:2;4573:6;;4577:2;;;4602:31;4619:13;4625:6;4602:31;4625:6;2797:9411:121;;;;4625:6:65;2797:9411:121;;;;4619:13:65;4577:2;2797:9411:121;;;;;;-1:-1:-1;;;;;;2797:9411:121;4602:31:65;;;2797:9411:121;;;;;;;;;4569:595:65;4658:7;4654:11;;4658:7;;;4688:51;4705:33;4715:22;4723:12;4724:6;4688:51;4724:6;2797:9411:121;;;;4723:12:65;2797:9411:121;;;;4715:22:65;2797:9411:121;;;8428:1:65;2797:9411:121;;;8428:1:65;2797:9411:121;;8422:19:65;8300:148;;4705:33;4577:2;2797:9411:121;;;;;;-1:-1:-1;;;;;;2797:9411:121;4688:51:65;;;2797:9411:121;;;;;;;;;4650:514:65;4764:7;4760:11;;4764:7;;;4794:51;4811:33;4821:22;4829:12;4830:6;4794:51;4830:6;2797:9411:121;;;;4830:6:65;4829:12;:::i;4821:22::-;2797:9411:121;8200:10:65;2797:9411:121;;;;;;;8195:21:65;8196:14;2797:9411:121;;;8168:49:65;2797:9411:121;;;;;;8279:7:65;2797:9411:121;;;8266:21:65;8046:248;;4811:33;4577:2;2797:9411:121;;;;;;-1:-1:-1;;;;;;2797:9411:121;4794:51:65;;;2797:9411:121;;;;;;;;;4756:408:65;4942:31;;4959:13;4902:85;4959:13;;:::i;:::-;4577:2;2797:9411:121;4942:31:65;;;;;;2797:9411:121;;;;;;;4942:31:65;4902:85;:::i;:::-;5117:36;5065:30;5071:23;5072:17;5073:10;2797:9411:121;;5073:10:65;:::i;:::-;2797:9411:121;;;;5072:17:65;5071:23;:::i;5065:30::-;5117:36;4577:2;2797:9411:121;5117:36:65;;;4942:31;5117:36;;;:::i;2129:778:48:-;2797:9411:121;;;2129:778:48;2319:2;2299:22;;2319:2;;2751:25;2535:196;;;;;;;;;;;;;;;-1:-1:-1;2535:196:48;2751:25;;:::i;:::-;2744:32;;;;;:::o;2295:606::-;2807:83;;2823:1;2807:83;2827:35;2807:83;;:::o;2797:9411:121:-;;-1:-1:-1;2797:9411:121;;;:::o;:::-;;;;;;;;;;;;7280:532:48;2797:9411:121;;;:::i;:::-;7366:29:48;;;7411:7;;:::o;7362:444::-;2797:9411:121;;;:::i;:::-;7471:29:48;7462:38;;7471:29;;7523:23;;;7375:20;7523:23;;7375:20;7523:23;7458:348;2797:9411:121;;;:::i;:::-;7576:35:48;7567:44;;7576:35;;7634:46;;;;7375:20;7634:46;;2797:9411:121;;7375:20:48;7634:46;7563:243;2797:9411:121;;7710:30:48;2797:9411:121;;:::i;:::-;7701:39:48;7697:109;;7563:243;7280:532::o;7697:109::-;7763:32;;;7375:20;7763:32;7634:46;2797:9411:121;;7375:20:48;7763:32;2797:9411:121;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;;;:::i;:::-;;;;;;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;;;;;;;;;;;:::i;:::-;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;;;;;;;;;:::i;:::-;;;;:::i;:::-;;;;;;;;;;;;:::i;2339:494:119:-;2788:28;2730:44;2468:30;2442:384;2339:494;2468:30;2797:9411:121;;;;;;;;;;;;;;;-1:-1:-1;;;;;;2797:9411:121;2468:30:119;;;2797:9411:121;;;;;;;;;2468:30:119;2564:33;2512:38;2797:9411:121;2468:30:119;2532:17;;2797:9411:121;;;;;2512:38:119;2564:33;2797:9411:121;2581:15:119;;2797:9411:121;;;2564:33:119;;;2468:30;2564:33;;2797:9411:121;;;;;;;2564:33:119;2631:21;;;;2797:9411:121;2788:28:119;2805:10;2611:45;2797:9411:121;;;-1:-1:-1;;;;;2797:9411:121;;;2611:45:119;2690:25;2730:44;2797:9411:121;2670:46:119;2797:9411:121;2468:30:119;2690:25;;2797:9411:121;;;;;2670:46:119;2747:26;;2797:9411:121;;;2730:44:119;;;2468:30;2730:44;;2797:9411:121;;;;;;;2730:44:119;;2797:9411:121;;2730:44:119;;;;;;:::i;:::-;2805:10;2797:9411:121;;;2788:28:119;;;2468:30;2788:28;;2797:9411:121;;;;;;;2788:28:119;;2797:9411:121;;2788:28:119;;;;;;:::i;:::-;2442:384;:::i;11602:252:121:-;11714:20;;;11750:19;;:::o;11710:138::-;2797:9411;;;;;;;11800:37;:::o;3366:228:55:-;;;;3548:39;3366:228;3548:39;:::i;2089:399:58:-;3000:21:119;2216:11:58;;2797:9411:121;;;3000:21:119;2797:9411:121;;;;;;;2237:9:58;;2797:9411:121;-1:-1:-1;2797:9411:121;;3000:21:119;2264:48:58;2351:9;2797:9411:121;;;;;;1938:75:59;;;;2797:9411:121;;;3000:21:119;2392:39:58;;;:::i;:::-;2797:9411:121;;;;3000:21:119;2797:9411:121;;;;;;;;2089:399:58;:::o;:::-;;2216:11;;;2797:9411:121;;;;;;;;;;;2237:9:58;;2797:9411:121;-1:-1:-1;2797:9411:121;;2268:8:58;;2264:48;;2216:11;2351:9;;2797:9411:121;;;;;;1938:75:59;;;;2797:9411:121;;;2392:39:58;;;;:::i;:::-;2797:9411:121;;;;;;;;;;;;2089:399:58;:::o;2264:48::-;2797:9411:121;;;;;;;;;:::i;:::-;-1:-1:-1;2797:9411:121;;2292:9:58;:::o;2797:9411:121:-;;;;;;;;;;;;:::o;:::-;;;;:::o;:::-;;;-1:-1:-1;;;2797:9411:121;;;;;;;;;;;;;;;;;-1:-1:-1;;;2797:9411:121;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;:::o;1205:1510:65:-;1323:20;;;:::i;:::-;2797:9411:121;;;;1453:9:65;;-1:-1:-1;1501:14:65;;-1:-1:-1;2797:9411:121;;;;1509:6:65;2797:9411:121;1449:1238:65;1579:1;1571:9;;1579:1;;1634:20;1782:11;1634:20;1782:11;1787:6;1740:7;1692:13;1634:20;1842:9;1634:20;;:::i;1692:13::-;2797:9411:121;;;;;;1740:7:65;1787:6;2797:9411:121;;;;;;1782:11:65;;;:::i;:::-;-1:-1:-1;;;;;2797:9411:121;;;1567:1120:65;1880:1;1872:9;;1880:1;;1943:20;2275:8;1943:20;2243:16;2797:9411:121;1943:20:65;2194:16;1943:20;;2336:11;1943:20;;:::i;:::-;2013;;2118:15;2013:20;2058;2013;;;:::i;:::-;2058;;:::i;:::-;2797:9411:121;;;;;;;;2118:15:65;2797:9411:121;;2105:29:65;;2797:9411:121;;;;;;;2194:16:65;2188:23;;2797:9411:121;;;;;;;2243:16:65;2237:23;2797:9411:121;;;;;;1868:819:65;2376:1;2368:9;2376:1;;2575:34;2450:6;2797:9411:121;2449:12:65;2450:6;2589:19;2450:6;2797:9411:121;;;;;;;2450:6:65;2449:12;:::i;:::-;2797:9411:121;2503:6:65;2495:59;2508:1;2503:6;;;2495:59;:::i;:::-;2589:19;:::i;:::-;2575:34;:::i;2364:323::-;2797:9411:121;;-1:-1:-1;;;2640:36:65;;2797:9411:121;1393:1:65;2640:36;;2797:9411:121;;;;;;;;;;;;;2640:36:65;2797:9411:121;;;;;;;;;;;;;:::o;1564:269:58:-;1649:11;;;2797:9411:121;;;1663:1:58;2797:9411:121;;;;;;;1667:9:58;;2797:9411:121;-1:-1:-1;1645:87:58;;1758:9;2797:9411:121;;-1:-1:-1;;;;;;2797:9411:121;1758:22:58;;2797:9411:121;1758:22:58;:::i;:::-;2797:9411:121;;;;;1791:16:58;2797:9411:121;;1791:16:58;:::i;:::-;2797:9411:121;;1564:269:58;:::o;1645:87::-;2797:9411:121;;-1:-1:-1;;;1699:22:58;;1649:11;1699:22;;;2797:9411:121;;;;;;-1:-1:-1;;;2797:9411:121;;;;1699:22:58;;;4479:308:119;2797:9411:121;;:::i;:::-;;2216:11:58;;;2797:9411:121;;;4620:1:119;2797:9411:121;;;;;;;2237:9:58;;2797:9411:121;-1:-1:-1;2797:9411:121;;2216:11:58;2351:9;;2797:9411:121;;;;;;1938:75:59;;;;2797:9411:121;;;4620:1:119;2392:39:58;;;:::i;:::-;2797:9411:121;;;;4620:1:119;2797:9411:121;;;;;;;;4624:1:119;5413:13:58;4624:1:119;5408:106:58;5428:5;4620:1:119;5428:5:58;;;;4653:35:119;;;;4718:25;4653:35;;:::i;:::-;4718:25;;:::i;:::-;4760:20;2797:9411:121;;:::i;:::-;-1:-1:-1;;;;;;2797:9411:121;;;;;;4760:20:119;2216:11:58;4760:20:119;;2797:9411:121;4479:308:119;:::o;5435:3:58:-;2797:9411:121;;-1:-1:-1;;;;;;5468:16:58;2797:9411:121;;5468:16:58;:::i;:::-;2797:9411:121;;;;;;;;;;5501:1:58;2797:9411:121;;;;;;;;;-1:-1:-1;;;;;;2797:9411:121;;;;;;5454:49:58;5435:3;2797:9411:121;5413:13:58;;;3313:349;2797:9411:121;;9164:30:123;3466:31:58;2797:9411:121;;9164:30:123;3512:8:58;3617:38;3512:8;1938:75:59;3617:38:58;:::i;3313:349::-;2797:9411:121;;11258:1:123;3466:31:58;2797:9411:121;;11258:1:123;1938:75:59;3617:38:58;3512:8;1938:75:59;3617:38:58;:::i;3313:349::-;2797:9411:121;;;3466:31:58;2797:9411:121;;;;;;1938:75:59;;2797:9411:121;-1:-1:-1;2797:9411:121;;11439:1:123;3617:38:58;;;:::i;3313:349::-;;2797:9411:121;;3466:31:58;;2797:9411:121;;3512:8:58;;3508:48;;1938:75:59;3617:38:58;2797:9411:121;1938:75:59;3617:38:58;:::i;3508:48::-;2797:9411:121;;;;;;;;:::i;2744:313:58:-;2797:9411:121;;2876:25:58;9164:30:123;2876:25:58;2797:9411:121;;-1:-1:-1;;2797:9411:121;;;;;;;;;;;9164:30:123;1938:75:59;2797:9411:121;-1:-1:-1;2797:9411:121;;3012:38:58;;;:::i;2744:313::-;2797:9411:121;;2876:25:58;11516:2:123;2876:25:58;2797:9411:121;;-1:-1:-1;;2797:9411:121;;;;;;;;;;;1938:75:59;;2797:9411:121;-1:-1:-1;2797:9411:121;;3012:38:58;;;:::i;2797:9411:121:-;;;;;-1:-1:-1;;2797:9411:121;;:::o;823:320:65:-;2797:9411:121;;;;;961:5:65;;;1123:13;;823:320;:::o;968:3::-;2797:9411:121;;-1:-1:-1;;2797:9411:121;;;;;;;;1051:11:65;;;;:::i;:::-;2797:9411:121;;;;;;;;;-1:-1:-1;;;;;2797:9411:121;;;;;;;;;;1060:1:65;2797:9411:121;;1037:66:65;2797:9411:121;;;;;;;;;;;;;;968:3:65;1012:91;;;;:::i;:::-;968:3;;:::i;:::-;936:23;;;;10579:974:123;2797:9411:121;;:::i;:::-;10697:1:123;10680:867;10725:3;10704:12;;;;;2797:9411:121;;10700:23:123;;;;;10749:27;;:15;;10748:28;10749:15;;:::i;10748:28::-;10744:42;;2797:9411:121;;;10804:58:123;:37;:25;:15;:12;;;:15;:::i;:37::-;-1:-1:-1;;;;;;2797:9411:121;;;10804:58:123;;10800:72;;10804:25;10907:15;:12;:30;:12;;:15;:::i;:::-;;:25;;:30;;2797:9411:121;11162:2:123;2797:9411:121;;11147:17:123;11143:31;;11204:58;11211:50;11236:24;;;:::i;11204:58::-;2797:9411:121;-1:-1:-1;;;;;2797:9411:121;;11280:10:123;11276:24;;11417;;;11322:214;11481:39;11497:22;11385:58;11392:50;11417:24;;;:::i;11385:58::-;11497:22;;:::i;11481:39::-;2797:9411:121;11322:214:123;2797:9411:121;;:::i;:::-;-1:-1:-1;;;;;2797:9411:121;;;8525:14:123;;;11322:214;2797:9411:121;;10907:30:123;11322:214;;2797:9411:121;;11322:214:123;10749:27;11322:214;;2797:9411:121;11315:221:123;:::o;11276:24::-;10704:12;11292:8;;;2797:9411:121;11292:8:123;;;10685:13;2797:9411:121;10685:13:123;;;;;11143:31;11166:8;;;2797:9411:121;10704:12:123;11166:8;;;10800:72;10864:8;;2797:9411:121;10704:12:123;10864:8;;;10700:23;;;;;;;10579:974::o;5627:1354:65:-;2797:9411:121;5873:1:65;2797:9411:121;;5873:1:65;2797:9411:121;6063:110:65;6191:86;;6064;;;;;;;2797:9411:121;;6190:110:65;6191:86;;;;2797:9411:121;;6062:239:65;6511:66;6384;6364:86;;2797:9411:121;;6490:110:65;6491:86;2797:9411:121;;6362:239:65;6811:66;6684;;6664:86;;2797:9411:121;;6790:110:65;6791:86;2797:9411:121;;6662:239:65;6965:8;2797:9411:121;;;6965:8:65;2797:9411:121;;;;6965:8:65;6951:23;5627:1354;:::o;6202:380:58:-;2797:9411:121;;-1:-1:-1;;2797:9411:121;;;;;;;6414:3:58;-1:-1:-1;;;;;;6437:7:58;;;;:::i;:::-;2797:9411:121;;6433:86:58;;6414:3;;;:::i;:::-;6382:22;;6433:86;6403:1;2797:9411:121;;;;;;;6546:29:58;;;:::i;5203:1551:48:-;;;6283:66;6270:79;;6266:164;;2797:9411:121;;;;;;-1:-1:-1;2797:9411:121;;;;;;;;;;;;;;;;;;;6541:24:48;;;;;;;;;-1:-1:-1;6541:24:48;-1:-1:-1;;;;;2797:9411:121;;6579:20:48;6575:113;;6698:49;-1:-1:-1;6698:49:48;-1:-1:-1;5203:1551:48;:::o;6575:113::-;6615:62;-1:-1:-1;6615:62:48;6541:24;6615:62;-1:-1:-1;6615:62:48;:::o;6541:24::-;2797:9411:121;;;;;;;;;6266:164:48;6365:54;;;6381:1;6365:54;6385:30;6365:54;;:::o;7197:131:56:-;7277:1;7272:6;;;7268:20;;-1:-1:-1;;2797:9411:121;;;;;;;7305:12:56;;;:::i;:::-;7277:1;2797:9411:121;;;;;;;7197:131:56;:::o;5056:1349::-;;;;2797:9411:121;;;5309:1063:56;5332:1;5316:12;;;:::i;:::-;2797:9411:121;5316:17:56;;;5369:45;:41;5381:12;5332:1;5375:19;5381:12;;;:::i;:::-;2797:9411:121;5375:19:56;:::i;:::-;2797:9411:121;5369:41:56;:::i;:::-;:45;:::i;:::-;5428:9;2797:9411:121;5457:9:56;2797:9411:121;5452:836:56;5468:7;;;;;;6302;;;6339:16;6338:23;6302:7;6339:16;;:::i;:::-;2797:9411:121;;;;6338:23:56;5309:1063;;;5457:9;5510:12;;;;;;;:::i;:::-;2797:9411:121;5563:5:56;;;;:::i;:::-;:11;:44;;;5457:9;5719:442;;;5786:9;6230:8;5332:1;5786:9;5850:1;5771:40;5786:9;;;;:::i;:::-;2797:9411:121;5797:13:56;5804:5;;;:::i;:::-;5797:13;;:::i;:::-;2797:9411:121;5771:40:56;;;:::i;:::-;2797:9411:121;;5719:442:56;;6179:18;;;;:::i;:::-;2797:9411:121;;;;;6230:8:56;6215:23;;;;:::i;:::-;2797:9411:121;;5457:9:56;;;;;5719:442;5647:7;;5332:1;5647:7;;5646:22;-1:-1:-1;5646:22:56;;5943:9;5332:1;5943:9;5928:35;6230:8;5943:9;5954:8;5943:9;;;;;;;;;:::i;:::-;2797:9411:121;5954:8:56;;:::i;:::-;2797:9411:121;5928:35:56;;;:::i;:::-;2797:9411:121;;;;5878:283:56;5719:442;;5878:283;6230:8;6063:9;;;5332:1;6063:9;;;;;;:::i;:::-;2797:9411:121;;;5878:283:56;5719:442;;5563:44;5588:5;5578:16;5588:5;;;:::i;:::-;5578:16;;:::i;:::-;2797:9411:121;5332:1:56;5599:7;;5578:29;5563:44;;5316:17;;;;;;6389:9;5316:17;;6389:9;:::i;:::-;2797:9411:121;5056:1349:56;:::o;4448:2801:55:-;;;;4610:14;;4606:38;;2797:9411:121;;4702:14:55;;;4698:40;;4810:1;4813:13;;;;;;5012:14;4810:1;5012:14;;:32;;;4793:159;5012:56;;;4793:159;5008:169;;5235:35;5249:20;;;:::i;:::-;5235:35;:::i;:::-;2797:9411:121;;;:::i;:::-;;;;;5219:52:55;;;2797:9411:121;;;;;:::i;:::-;;;;;5219:52:55;5313:22;;2797:9411:121;;;;;:::i;:::-;;;;;5219:52:55;5376:34;;2797:9411:121;;;;5376:34:55;;2797:9411:121;;5455:29:55;5494:940;5501:14;;;5494:940;2797:9411:121;;;;6490:52:55;;2797:9411:121;6642:14:55;;2797:9411:121;6622:41:55;6618:71;;2797:9411:121;;-1:-1:-1;;2797:9411:121;;;6763:445:55;2797:9411:121;;6770:21:55;;;6823:20;;;:::i;:::-;6872;;;:::i;:::-;2797:9411:121;6934:18:55;2797:9411:121;;;;;;6934:18:55;2797:9411:121;;;7006:139:55;5219:52;7006:139;2797:9411:121;;7006:139:55;7158:39;:14;;2797:9411:121;;7158:39:55;;:::i;:::-;2797:9411:121;6763:445:55;;6770:21;;7225:17;6770:21;;7225:14;:17;:::i;6618:71::-;5114:17;;;2797:9411:121;6672:17:55;;2797:9411:121;6672:17:55;6490:52;6523:19;;;2797:9411:121;6523:19:55;;2797:9411:121;6523:19:55;5494:940;5548:16;;;:::i;:::-;2797:9411:121;5663:31:55;5625:24;4810:1;2797:9411:121;;5625:24:55;;;:::i;:::-;5663:31;;;;:::i;:::-;5745:42;;;;;;;:::i;:::-;5806:20;5219:52;5806:20;;2797:9411:121;5806:25:55;;5802:622;5806:25;;;5855:14;;;;;;;;2797:9411:121;;;5855:41:55;5851:174;5880:16;5920:5;5880:16;5989;;;;;:::i;:::-;;;:::i;:::-;5494:940;;5802:622;4810:1;6049:25;:40;;;5802:622;6045:379;;;6126:18;;;5219:52;6126:40;:18;2797:9411:121;6126:45:55;:18;;;2797:9411:121;;6126:40:55;;:::i;:::-;;:45;2797:9411:121;6126:45:55;;:::i;6045:379::-;2797:9411:121;6353:55:55;2797:9411:121;;6282:1:55;6353:55;2797:9411:121;;;6353:55:55;;:::i;6049:40::-;6078:11;;;6049:40;;5008:169;2797:9411:121;;;;;;5084:47:55;;5152:9;:14;:9;;:::i;:::-;;:14;2797:9411:121;5145:21:55;:::o;5012:56::-;5048:9;;;;:::i;:::-;;2797:9411:121;5048:20:55;5012:56;;:32;5030:14;4810:1;5030:14;;5012:32;;4798:13;4847:9;;;;:::i;:::-;;2797:9411:121;4866:13:55;4873:5;;;:::i;:::-;4866:13;;:::i;:::-;;2797:9411:121;-1:-1:-1;4847:38:55;4843:67;;4810:1;2797:9411:121;4798:13:55;;4698:40;4725:13;;;2797:9411:121;4725:13:55;;2797:9411:121;4725:13:55;2284:287:59;;;;2797:9411:121;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;:::i;:::-;;;;;;-1:-1:-1;;2797:9411:121;;;;2560:3:59;;;3244:193;3251:16;2797:9411:121;3251:16:59;;;;;3494:8;;;-1:-1:-1;;;2797:9411:121;3494:132:59;3636:173;;;;;;;;;;;2284:287::o;3494:132::-;3598:24;3606:15;3598:28;3606:15;;:::i;:::-;3598:24;:::i;:28::-;3494:132;;;3269:16;3301:65;;;;;;2797:9411:121;;;;;;;;;;;;;;;;;3269:16:59;-1:-1:-1;;2797:9411:121;;;;;;3244:193:59;2797:9411:121;;:::i;7390:537:56:-;7459:462;;;;;;;;-1:-1:-1;;;;;7459:462:56;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;7390:537;:::o;6686:471::-;6806:1;6800:7;6806:1;;2797:9411:121;6829:141:56;;;;2797:9411:121;6829:141:56;6795:356;6686:471::o;6795:356::-;7000:141;2797:9411:121;7000:141:56;;;;2797:9411:121;7000:141:56;6795:356;6686:471::o;13095:196:55:-;;13147:13;2797:9411:121;13172:113:55;13179:6;;;13095:196;:::o;13172:113::-;2797:9411:121;-1:-1:-1;;2797:9411:121;;;;;;;13210:1:55;13201:10;;2797:9411:121;;;;13172:113:55;;;2797:9411:121;;;;;;;:::i;:::-;;;;-1:-1:-1;2797:9411:121;;-1:-1:-1;2797:9411:121;;;;;;:::o;7670:742:55:-;2797:9411:121;;:::i;:::-;;;;7846:33:55;7864:15;;;2797:9411:121;;;7846:33:55;;:::i;:::-;2797:9411:121;;;7935:210:55;7942:15;;;;;;7935:210;2797:9411:121;;;;8175:27:55;8348:28;2797:9411:121;;;8175:27:55;;:::i;:::-;2797:9411:121;;;8287:13:55;7998;8287;;;2797:9411:121;;:::i;:::-;;;;8246:55:55;7864:15;8246:55;;2797:9411:121;7998:13:55;8246:55;;2797:9411:121;;;;8348:28:55;:::i;7935:210::-;7998:13;:24;:13;;;;;:24;:::i;:::-;;2797:9411:121;7978:50:55;;;7974:94;;2797:9411:121;;7935:210:55;;;7974:94;8048:5;;;9445:1927;;;9640:15;;;2797:9411:121;;;9737:21:55;;;;:::i;:::-;9794;;;;:::i;:::-;9830:9;2797:9411:121;9841:10:55;;;;;;10071:20;;;;10182:1157;;2797:9411:121;10189:12:55;;;:::i;:::-;2797:9411:121;10189:17:55;;;2797:9411:121;10248:9:55;2797:9411:121;10272:851:55;10279:7;;;;;;11204:10;;;11228:101;;;;;10182:1157;;10272:851;10320:12;;;;;;:::i;:::-;2797:9411:121;10355:5:55;;;;;;:::i;:::-;:11;:44;;;10272:851;10351:758;;;2797:9411:121;10496:9:55;10481:40;10496:9;10507:13;10514:5;10496:9;10677:6;10496:9;;10561:8;10496:9;;:::i;:::-;2797:9411:121;10514:5:55;;:::i;10507:13::-;2797:9411:121;10481:40:55;;;:::i;:::-;10466:55;;;;:::i;10561:8::-;10543:26;;;;:::i;:::-;2797:9411:121;;10677:6:55;;:::i;:::-;10351:758;10272:851;;10351:758;2797:9411:121;;;;9640:15:55;10801:14;;;2797:9411:121;-1:-1:-1;10781:41:55;10777:70;;2797:9411:121;10899:9:55;10967:8;10899:9;;10884:43;10899:9;;;;:::i;:::-;2797:9411:121;10910:16:55;;;:::i;:::-;10884:43;;;:::i;10967:8::-;10949:26;;;;:::i;:::-;2797:9411:121;;;;10351:758:55;10272:851;;10777:70;10831:16;;;2797:9411:121;10831:16:55;;2797:9411:121;10831:16:55;10355:44;10380:5;10370:16;10380:5;;;:::i;:::-;10370:16;;:::i;:::-;2797:9411:121;;10391:7:55;;10370:29;10355:44;;10189:17;;;;;11356:9;10189:17;11356:9;:::i;9830:::-;2797:9411:121;9880:13:55;9985:25;9880:13;;;9640:15;9880:25;:13;;9894:10;;;;:::i;:::-;9880:25;;:::i;:::-;;:30;2797:9411:121;9868:42:55;;;;:::i;:::-;2797:9411:121;9985:13:55;2797:9411:121;;;9985:25:55;;:::i;:::-;;2797:9411:121;;;9952:64:55;;;;:::i;:::-;2797:9411:121;;9830:9:55;;11444:188;;11527:37;:13;;;;2797:9411:121;;11527:37:55;;:::i;:::-;2797:9411:121;;;;;;;11444:188:55:o;11706:222::-;11808:30;:13;;;;2797:9411:121;;11808:30:55;;:::i;:::-;2797:9411:121;;;;;;;;11706:222:55;:::o;12000:226::-;12106:30;:13;;;;2797:9411:121;;12106:30:55;;:::i;:::-;2797:9411:121;;;-1:-1:-1;;2797:9411:121;;;;;12000:226:55:o;713:2:59:-;;;;;;;;;:::o","linkReferences":{},"immutableReferences":{"77940":[{"start":2148,"length":32},{"start":3201,"length":32}]}},"methodIdentifiers":{"MMR_ROOT_PAYLOAD_ID()":"af8b91d6","_digestParaId()":"e455995b","noOp((uint256,uint256,(uint256,uint256,uint256,bytes32),(uint256,uint256,uint256,bytes32)),(((((bytes2,bytes)[],uint32,uint64),(bytes,uint256)[]),(uint8,uint32,bytes32,(uint64,uint32,bytes32),bytes32,uint256),bytes32[],bytes32[]),((uint256,uint256,bytes)[],bytes32[],uint256)))":"9442d9fc","supportsInterface(bytes4)":"01ffc9a7","verify(bytes,bytes)":"f7e83aee"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.30+commit.73712a01\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"digestParaId\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"ECDSAInvalidSignature\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"length\",\"type\":\"uint256\"}],\"name\":\"ECDSAInvalidSignatureLength\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"ECDSAInvalidSignatureS\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyTree\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyTree\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"IllegalGenesisBlock\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAuthoritiesProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidMmrProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"LeafIndexOutOfBounds\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MmrRootHashMissing\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OutOfBoundsLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ProofExhausted\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SuperMajorityRequired\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"TimestampNotFound\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnconsumedProof\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnknownAuthoritySet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnsortedLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UnsortedLeaves\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MMR_ROOT_PAYLOAD_ID\",\"outputs\":[{\"internalType\":\"bytes2\",\"name\":\"\",\"type\":\"bytes2\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"_digestParaId\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"latestHeight\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"beefyActivationBlock\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"len\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"blsPoseidonHash\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"ecdsaMerkleRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct AuthoritySet\",\"name\":\"currentAuthoritySet\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"len\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"blsPoseidonHash\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"ecdsaMerkleRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct AuthoritySet\",\"name\":\"nextAuthoritySet\",\"type\":\"tuple\"}],\"internalType\":\"struct BeefyConsensusState\",\"name\":\"s\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"components\":[{\"components\":[{\"components\":[{\"internalType\":\"bytes2\",\"name\":\"id\",\"type\":\"bytes2\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"internalType\":\"struct Payload[]\",\"name\":\"payload\",\"type\":\"tuple[]\"},{\"internalType\":\"uint32\",\"name\":\"blockNumber\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"validatorSetId\",\"type\":\"uint64\"}],\"internalType\":\"struct Commitment\",\"name\":\"commitment\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"authorityIndex\",\"type\":\"uint256\"}],\"internalType\":\"struct Vote[]\",\"name\":\"votes\",\"type\":\"tuple[]\"}],\"internalType\":\"struct SignedCommitment\",\"name\":\"signedCommitment\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"},{\"internalType\":\"uint32\",\"name\":\"parentNumber\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"parentHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"uint64\",\"name\":\"id\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"len\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"internalType\":\"struct AuthoritySetCommitment\",\"name\":\"nextAuthoritySet\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"extra\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"leafIndex\",\"type\":\"uint256\"}],\"internalType\":\"struct BeefyMmrLeaf\",\"name\":\"latestMmrLeaf\",\"type\":\"tuple\"},{\"internalType\":\"bytes32[]\",\"name\":\"mmrProof\",\"type\":\"bytes32[]\"},{\"internalType\":\"bytes32[]\",\"name\":\"proof\",\"type\":\"bytes32[]\"}],\"internalType\":\"struct RelayChainProof\",\"name\":\"relay\",\"type\":\"tuple\"},{\"components\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"header\",\"type\":\"bytes\"}],\"internalType\":\"struct Parachain[]\",\"name\":\"parachains\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes32[]\",\"name\":\"proof\",\"type\":\"bytes32[]\"},{\"internalType\":\"uint256\",\"name\":\"leafCount\",\"type\":\"uint256\"}],\"internalType\":\"struct ParachainProof\",\"name\":\"parachain\",\"type\":\"tuple\"}],\"internalType\":\"struct BeefyConsensusProof\",\"name\":\"p\",\"type\":\"tuple\"}],\"name\":\"noOp\",\"outputs\":[],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceId\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"previousState\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"proof\",\"type\":\"bytes\"}],\"name\":\"verify\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"stateMachineId\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"height\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"overlayRoot\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"stateRoot\",\"type\":\"bytes32\"}],\"internalType\":\"struct StateCommitment\",\"name\":\"commitment\",\"type\":\"tuple\"}],\"internalType\":\"struct IntermediateState[]\",\"name\":\"\",\"type\":\"tuple[]\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}],\"devdoc\":{\"author\":\"Polytope Labs (hello@polytope.technology)\",\"details\":\"The verification flow is: 1. Confirm the commitment's validator set id matches a known authority set. 2. Verify that enough signatures are present to meet the supermajority threshold. 3. Recover signer addresses via ecrecover and verify their membership in the authority set via a merkle multi-proof against the authority set root. 4. Extract the MMR root from the commitment payload and verify the latest MMR leaf inclusion via a merkle mountain range proof. 5. Verify parachain header inclusion in the MMR leaf's parachain heads root. 6. Decode each parachain header to extract finalized state commitments. Stale proofs (commitment block number <= trusted latest height) are treated as no-ops.\",\"errors\":{\"ECDSAInvalidSignature()\":[{\"details\":\"The signature derives the `address(0)`.\"}],\"ECDSAInvalidSignatureLength(uint256)\":[{\"details\":\"The signature has an invalid length.\"}],\"ECDSAInvalidSignatureS(bytes32)\":[{\"details\":\"The signature has an S value that is in the upper half order.\"}]},\"kind\":\"dev\",\"methods\":{\"supportsInterface(bytes4)\":{\"details\":\"See {IERC165-supportsInterface}.\"},\"verify(bytes,bytes)\":{\"details\":\"IConsensusV2 entry point. Decodes the proof, verifies consensus, and returns the updated state along with the latest authority set id.\"}},\"title\":\"The ECDSA BEEFY Consensus Client.\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"_digestParaId()\":{\"notice\":\"The parachain whose header digests carry the bls commitment, which is hyperbridge. Only its headers are read for one, since every parachain in a proof is equally authentic and only this one speaks for the relay's authorities.\"}},\"notice\":\"Verifies BEEFY consensus proofs by checking a 2/3+1 supermajority of secp256k1 signatures on-chain, along with merkle multi-proofs of authority set membership. This is the most gas-expensive verifier but requires no off-chain proving infrastructure.\",\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/consensus/EcdsaBeefy.sol\":\"EcdsaBeefy\"},\"evmVersion\":\"prague\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@gnark-apk-proofs/=lib/gnark-apk-proofs/solidity/contracts/\",\":@hyperbridge/core/=node_modules/@hyperbridge/core/contracts/\",\":@openzeppelin/=node_modules/@openzeppelin/\",\":@polytope-labs/=node_modules/@polytope-labs/\",\":@sp1-contracts/=lib/sp1-contracts/contracts/src/\",\":@uniswap/=node_modules/@uniswap/\",\":ds-test/=lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=node_modules/forge-std/src/\",\":gnark-apk-proofs/=lib/gnark-apk-proofs/\",\":openzeppelin-contracts/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/\",\":solidity-stringutils/=lib/solidity-stringutils/\",\":sp1-contracts/=lib/sp1-contracts/contracts/\",\":stringutils/=lib/solidity-stringutils/src/\"],\"viaIR\":true},\"sources\":{\"node_modules/@hyperbridge/core/contracts/interfaces/IConsensusV2.sol\":{\"keccak256\":\"0x71dcb5168f8f0f95effac221bdc49e0f662011c1bf86a9369cc0db183b8ac4c3\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://d84388af7b50f5f31110fe3d711930c816432f8c2ebcc7417639a727760544ff\",\"dweb:/ipfs/QmWp89jgUMhCqrGVDphLAEhJm5kPP6q1y9yid3D2a3JFZ9\"]},\"node_modules/@hyperbridge/core/contracts/libraries/StateMachine.sol\":{\"keccak256\":\"0x860289ae856ea354df5cca2131612da5c15a129fc14ae5ac0d528e0ce7c809c3\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://2ab5a7d6463b634da1e05e2b06818416a6f2d59b5c10815cc74bf26d003c2b95\",\"dweb:/ipfs/QmZ4dpEoDhNefAcKwKBXumLfSHqxhcBctErTzMvvPHXY5G\"]},\"node_modules/@openzeppelin/contracts/utils/Panic.sol\":{\"keccak256\":\"0xf7fe324703a64fc51702311dc51562d5cb1497734f074e4f483bfb6717572d7a\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c6a5ff4f9fd8649b7ee20800b7fa387d3465bd77cf20c2d1068cd5c98e1ed57a\",\"dweb:/ipfs/QmVSaVJf9FXFhdYEYeCEfjMVHrxDh5qL4CGkxdMWpQCrqG\"]},\"node_modules/@openzeppelin/contracts/utils/Strings.sol\":{\"keccak256\":\"0xad148d59f05165f9217d0a9e1ac8f772abb02ea6aaad8a756315c532bf79f9f4\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://15e3599867c2182f5831e9268b274b2ef2047825837df6b4d81c9e89254b093e\",\"dweb:/ipfs/QmZbL7XAYr5RmaNaooPgZRmcDXaudfsYQfYD9y5iAECvpS\"]},\"node_modules/@openzeppelin/contracts/utils/cryptography/ECDSA.sol\":{\"keccak256\":\"0x69f54c02b7d81d505910ec198c11ed4c6a728418a868b906b4a0cf29946fda84\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://8e25e4bdb7ae1f21d23bfee996e22736fc0ab44cfabedac82a757b1edc5623b9\",\"dweb:/ipfs/QmQdWQvB6JCP9ZMbzi8EvQ1PTETqkcTWrbcVurS7DKpa5n\"]},\"node_modules/@openzeppelin/contracts/utils/introspection/ERC165.sol\":{\"keccak256\":\"0x2d9dc2fe26180f74c11c13663647d38e259e45f95eb88f57b61d2160b0109d3e\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://81233d1f98060113d9922180bb0f14f8335856fe9f339134b09335e9f678c377\",\"dweb:/ipfs/QmWh6R35SarhAn4z2wH8SU456jJSYL2FgucfTFgbHJJN4E\"]},\"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol\":{\"keccak256\":\"0x8891738ffe910f0cf2da09566928589bf5d63f4524dd734fd9cedbac3274dd5c\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://971f954442df5c2ef5b5ebf1eb245d7105d9fbacc7386ee5c796df1d45b21617\",\"dweb:/ipfs/QmadRjHbkicwqwwh61raUEapaVEtaLMcYbQZWs9gUkgj3u\"]},\"node_modules/@openzeppelin/contracts/utils/math/Math.sol\":{\"keccak256\":\"0x1225214420c83ebcca88f2ae2b50f053aaa7df7bd684c3e878d334627f2edfc6\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://6c5fab4970634f9ab9a620983dc1c8a30153981a0b1a521666e269d0a11399d3\",\"dweb:/ipfs/QmVRnBC575MESGkEHndjujtR7qub2FzU9RWy9eKLp4hPZB\"]},\"node_modules/@openzeppelin/contracts/utils/math/SafeCast.sol\":{\"keccak256\":\"0x195533c86d0ef72bcc06456a4f66a9b941f38eb403739b00f21fd7c1abd1ae54\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://b1d578337048cad08c1c03041cca5978eff5428aa130c781b271ad9e5566e1f8\",\"dweb:/ipfs/QmPFKL2r9CBsMwmUqqdcFPfHZB2qcs9g1HDrPxzWSxomvy\"]},\"node_modules/@openzeppelin/contracts/utils/math/SignedMath.sol\":{\"keccak256\":\"0xb1970fac7b64e6c09611e6691791e848d5e3fe410fa5899e7df2e0afd77a99e3\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://db5fbb3dddd8b7047465b62575d96231ba8a2774d37fb4737fbf23340fabbb03\",\"dweb:/ipfs/QmVUSvooZKEdEdap619tcJjTLcAuH6QBdZqAzWwnAXZAWJ\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMountainRange.sol\":{\"keccak256\":\"0x014237038bb77bdf371b50c1268d02bf7eeaf7068c35483d5d0684ca7c30d544\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://beb3ae60094f49716a2b0cfa6229ee27af1778934cf5f2dfc704433fda5a687b\",\"dweb:/ipfs/QmQ339wEFT9X1woHyFTDbbhUCx8ekD9MGLRnhS3PpYp1pm\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMultiProof.sol\":{\"keccak256\":\"0xd4f6e6a9eceaa7d1cdf9684bfe7f3f552adf21dfcf7f1372f7943a4c2f15deb7\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://ec900b3e79ea8ef9d275697937d1984e994d10117aa2be066bd73f6a31aedf10\",\"dweb:/ipfs/QmZQGLiJtbvUSEW5H2HEenzxp58fN43XHRY3SoNTC9Hobg\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Bytes.sol\":{\"keccak256\":\"0xd305383358b93285d8fcee512795487484eadfd7b602df16ff4e9b01afbefec7\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://57c86cd2fe6ab591264c5632d503d77f04748d722e4defa9c2badb238ee1f9bd\",\"dweb:/ipfs/QmTZ6xTcaYkRBshq6YZVMx2kkk94ZtJsRM6xLpC5qWT3oA\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Memory.sol\":{\"keccak256\":\"0x59e3a56caa42c1aac30231173439817d38c7f359e40bd36e9bb418d3f82ceab7\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://e060fed46c6b420624166ea02a326f0f566897941bdc322257e690ae134b8179\",\"dweb:/ipfs/QmbXW8yG2ZntjLMyUEmQGcRZroZj4dVZkBhHxK2PFKfUKB\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Node.sol\":{\"keccak256\":\"0xca611969a68f7fe63dcdc742c9caf9bc1b26495561b68f4676c209279a4576ba\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://17b7ec2cf65d484f0a2c188a7ae33430b015b3a55bf10393167f53839e2dde56\",\"dweb:/ipfs/QmTQbT8J7rBRNHFXSR7S4KbpwCteMrFt8mvrzCD1vYYTwX\"]},\"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/polkadot/ScaleCodec.sol\":{\"keccak256\":\"0x9ac4df46e68718f7deaaa5b7443778533f53dc0ff3736cc386cf4991099da2aa\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://85d08b00d9173358323105be975ce85b7e206ba52df2d80a14d2581dec1ddd11\",\"dweb:/ipfs/QmQCqVvSdJUqfhLH8TRBGNDGiEzkGduhRacnYhoKcm7e2a\"]},\"src/consensus/Codec.sol\":{\"keccak256\":\"0x477b62396db5a5c1d89d0388b0724dde80e0a0423b89c07f7b3b5622ff03b5d4\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://6a584c6500ae347c6f9d76d06a96757b024dcb892601c6d9c1a945b9d76811d6\",\"dweb:/ipfs/QmduKRb2Zr9eqeZbi5TFRw5Dyig3XVY3rMvPkxLktLFYrN\"]},\"src/consensus/EcdsaBeefy.sol\":{\"keccak256\":\"0x1fd6824212d0c42ecfb107d3599ff75d280a956dbaa212707eaa83b6e667aca7\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://0471c92a4bf06528e9826ace0f48c32a17702ecbad93fb8e015123444c002448\",\"dweb:/ipfs/QmaZ47vQbHFHNBzFpupqn66PdVigLYv7P363EmgsyXk2ED\"]},\"src/consensus/Types.sol\":{\"keccak256\":\"0xe4b169eefb4afd0f83f09335f4460705fe96e7736314c2030235720cf976e76f\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://4ad510e26689491bcba165fd73c08e1b80302148804e27a184861712d966540b\",\"dweb:/ipfs/QmVuaZ3yB3eBkHRh2DS6Cy1E6B3oige6fDtvCrcYssc3qi\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.30+commit.73712a01"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"uint256","name":"digestParaId","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"type":"error","name":"ECDSAInvalidSignature"},{"inputs":[{"internalType":"uint256","name":"length","type":"uint256"}],"type":"error","name":"ECDSAInvalidSignatureLength"},{"inputs":[{"internalType":"bytes32","name":"s","type":"bytes32"}],"type":"error","name":"ECDSAInvalidSignatureS"},{"inputs":[],"type":"error","name":"EmptyLeaves"},{"inputs":[],"type":"error","name":"EmptyTree"},{"inputs":[],"type":"error","name":"EmptyTree"},{"inputs":[],"type":"error","name":"IllegalGenesisBlock"},{"inputs":[],"type":"error","name":"InvalidAuthoritiesProof"},{"inputs":[],"type":"error","name":"InvalidMmrProof"},{"inputs":[],"type":"error","name":"LeafIndexOutOfBounds"},{"inputs":[],"type":"error","name":"MmrRootHashMissing"},{"inputs":[],"type":"error","name":"OutOfBoundsLeaves"},{"inputs":[],"type":"error","name":"ProofExhausted"},{"inputs":[],"type":"error","name":"SuperMajorityRequired"},{"inputs":[],"type":"error","name":"TimestampNotFound"},{"inputs":[],"type":"error","name":"UnconsumedProof"},{"inputs":[],"type":"error","name":"UnknownAuthoritySet"},{"inputs":[],"type":"error","name":"UnsortedLeaves"},{"inputs":[],"type":"error","name":"UnsortedLeaves"},{"inputs":[],"stateMutability":"view","type":"function","name":"MMR_ROOT_PAYLOAD_ID","outputs":[{"internalType":"bytes2","name":"","type":"bytes2"}]},{"inputs":[],"stateMutability":"view","type":"function","name":"_digestParaId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"struct BeefyConsensusState","name":"s","type":"tuple","components":[{"internalType":"uint256","name":"latestHeight","type":"uint256"},{"internalType":"uint256","name":"beefyActivationBlock","type":"uint256"},{"internalType":"struct AuthoritySet","name":"currentAuthoritySet","type":"tuple","components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"len","type":"uint256"},{"internalType":"uint256","name":"blsPoseidonHash","type":"uint256"},{"internalType":"bytes32","name":"ecdsaMerkleRoot","type":"bytes32"}]},{"internalType":"struct AuthoritySet","name":"nextAuthoritySet","type":"tuple","components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"uint256","name":"len","type":"uint256"},{"internalType":"uint256","name":"blsPoseidonHash","type":"uint256"},{"internalType":"bytes32","name":"ecdsaMerkleRoot","type":"bytes32"}]}]},{"internalType":"struct BeefyConsensusProof","name":"p","type":"tuple","components":[{"internalType":"struct RelayChainProof","name":"relay","type":"tuple","components":[{"internalType":"struct SignedCommitment","name":"signedCommitment","type":"tuple","components":[{"internalType":"struct Commitment","name":"commitment","type":"tuple","components":[{"internalType":"struct Payload[]","name":"payload","type":"tuple[]","components":[{"internalType":"bytes2","name":"id","type":"bytes2"},{"internalType":"bytes","name":"data","type":"bytes"}]},{"internalType":"uint32","name":"blockNumber","type":"uint32"},{"internalType":"uint64","name":"validatorSetId","type":"uint64"}]},{"internalType":"struct Vote[]","name":"votes","type":"tuple[]","components":[{"internalType":"bytes","name":"signature","type":"bytes"},{"internalType":"uint256","name":"authorityIndex","type":"uint256"}]}]},{"internalType":"struct BeefyMmrLeaf","name":"latestMmrLeaf","type":"tuple","components":[{"internalType":"uint8","name":"version","type":"uint8"},{"internalType":"uint32","name":"parentNumber","type":"uint32"},{"internalType":"bytes32","name":"parentHash","type":"bytes32"},{"internalType":"struct AuthoritySetCommitment","name":"nextAuthoritySet","type":"tuple","components":[{"internalType":"uint64","name":"id","type":"uint64"},{"internalType":"uint32","name":"len","type":"uint32"},{"internalType":"bytes32","name":"root","type":"bytes32"}]},{"internalType":"bytes32","name":"extra","type":"bytes32"},{"internalType":"uint256","name":"leafIndex","type":"uint256"}]},{"internalType":"bytes32[]","name":"mmrProof","type":"bytes32[]"},{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"}]},{"internalType":"struct ParachainProof","name":"parachain","type":"tuple","components":[{"internalType":"struct Parachain[]","name":"parachains","type":"tuple[]","components":[{"internalType":"uint256","name":"index","type":"uint256"},{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bytes","name":"header","type":"bytes"}]},{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"},{"internalType":"uint256","name":"leafCount","type":"uint256"}]}]}],"stateMutability":"pure","type":"function","name":"noOp"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"stateMutability":"view","type":"function","name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}]},{"inputs":[{"internalType":"bytes","name":"previousState","type":"bytes"},{"internalType":"bytes","name":"proof","type":"bytes"}],"stateMutability":"view","type":"function","name":"verify","outputs":[{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"struct IntermediateState[]","name":"","type":"tuple[]","components":[{"internalType":"uint256","name":"stateMachineId","type":"uint256"},{"internalType":"uint256","name":"height","type":"uint256"},{"internalType":"struct StateCommitment","name":"commitment","type":"tuple","components":[{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bytes32","name":"overlayRoot","type":"bytes32"},{"internalType":"bytes32","name":"stateRoot","type":"bytes32"}]}]},{"internalType":"uint256","name":"","type":"uint256"}]}],"devdoc":{"kind":"dev","methods":{"supportsInterface(bytes4)":{"details":"See {IERC165-supportsInterface}."},"verify(bytes,bytes)":{"details":"IConsensusV2 entry point. Decodes the proof, verifies consensus, and returns the updated state along with the latest authority set id."}},"version":1},"userdoc":{"kind":"user","methods":{"_digestParaId()":{"notice":"The parachain whose header digests carry the bls commitment, which is hyperbridge. Only its headers are read for one, since every parachain in a proof is equally authentic and only this one speaks for the relay's authorities."}},"version":1}},"settings":{"remappings":["@gnark-apk-proofs/=lib/gnark-apk-proofs/solidity/contracts/","@hyperbridge/core/=node_modules/@hyperbridge/core/contracts/","@openzeppelin/=node_modules/@openzeppelin/","@polytope-labs/=node_modules/@polytope-labs/","@sp1-contracts/=lib/sp1-contracts/contracts/src/","@uniswap/=node_modules/@uniswap/","ds-test/=lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=node_modules/forge-std/src/","gnark-apk-proofs/=lib/gnark-apk-proofs/","openzeppelin-contracts/=lib/sp1-contracts/contracts/lib/openzeppelin-contracts/","solidity-stringutils/=lib/solidity-stringutils/","sp1-contracts/=lib/sp1-contracts/contracts/","stringutils/=lib/solidity-stringutils/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"src/consensus/EcdsaBeefy.sol":"EcdsaBeefy"},"evmVersion":"prague","libraries":{},"viaIR":true},"sources":{"node_modules/@hyperbridge/core/contracts/interfaces/IConsensusV2.sol":{"keccak256":"0x71dcb5168f8f0f95effac221bdc49e0f662011c1bf86a9369cc0db183b8ac4c3","urls":["bzz-raw://d84388af7b50f5f31110fe3d711930c816432f8c2ebcc7417639a727760544ff","dweb:/ipfs/QmWp89jgUMhCqrGVDphLAEhJm5kPP6q1y9yid3D2a3JFZ9"],"license":"Apache-2.0"},"node_modules/@hyperbridge/core/contracts/libraries/StateMachine.sol":{"keccak256":"0x860289ae856ea354df5cca2131612da5c15a129fc14ae5ac0d528e0ce7c809c3","urls":["bzz-raw://2ab5a7d6463b634da1e05e2b06818416a6f2d59b5c10815cc74bf26d003c2b95","dweb:/ipfs/QmZ4dpEoDhNefAcKwKBXumLfSHqxhcBctErTzMvvPHXY5G"],"license":"Apache-2.0"},"node_modules/@openzeppelin/contracts/utils/Panic.sol":{"keccak256":"0xf7fe324703a64fc51702311dc51562d5cb1497734f074e4f483bfb6717572d7a","urls":["bzz-raw://c6a5ff4f9fd8649b7ee20800b7fa387d3465bd77cf20c2d1068cd5c98e1ed57a","dweb:/ipfs/QmVSaVJf9FXFhdYEYeCEfjMVHrxDh5qL4CGkxdMWpQCrqG"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/Strings.sol":{"keccak256":"0xad148d59f05165f9217d0a9e1ac8f772abb02ea6aaad8a756315c532bf79f9f4","urls":["bzz-raw://15e3599867c2182f5831e9268b274b2ef2047825837df6b4d81c9e89254b093e","dweb:/ipfs/QmZbL7XAYr5RmaNaooPgZRmcDXaudfsYQfYD9y5iAECvpS"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/cryptography/ECDSA.sol":{"keccak256":"0x69f54c02b7d81d505910ec198c11ed4c6a728418a868b906b4a0cf29946fda84","urls":["bzz-raw://8e25e4bdb7ae1f21d23bfee996e22736fc0ab44cfabedac82a757b1edc5623b9","dweb:/ipfs/QmQdWQvB6JCP9ZMbzi8EvQ1PTETqkcTWrbcVurS7DKpa5n"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/introspection/ERC165.sol":{"keccak256":"0x2d9dc2fe26180f74c11c13663647d38e259e45f95eb88f57b61d2160b0109d3e","urls":["bzz-raw://81233d1f98060113d9922180bb0f14f8335856fe9f339134b09335e9f678c377","dweb:/ipfs/QmWh6R35SarhAn4z2wH8SU456jJSYL2FgucfTFgbHJJN4E"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol":{"keccak256":"0x8891738ffe910f0cf2da09566928589bf5d63f4524dd734fd9cedbac3274dd5c","urls":["bzz-raw://971f954442df5c2ef5b5ebf1eb245d7105d9fbacc7386ee5c796df1d45b21617","dweb:/ipfs/QmadRjHbkicwqwwh61raUEapaVEtaLMcYbQZWs9gUkgj3u"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/math/Math.sol":{"keccak256":"0x1225214420c83ebcca88f2ae2b50f053aaa7df7bd684c3e878d334627f2edfc6","urls":["bzz-raw://6c5fab4970634f9ab9a620983dc1c8a30153981a0b1a521666e269d0a11399d3","dweb:/ipfs/QmVRnBC575MESGkEHndjujtR7qub2FzU9RWy9eKLp4hPZB"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/math/SafeCast.sol":{"keccak256":"0x195533c86d0ef72bcc06456a4f66a9b941f38eb403739b00f21fd7c1abd1ae54","urls":["bzz-raw://b1d578337048cad08c1c03041cca5978eff5428aa130c781b271ad9e5566e1f8","dweb:/ipfs/QmPFKL2r9CBsMwmUqqdcFPfHZB2qcs9g1HDrPxzWSxomvy"],"license":"MIT"},"node_modules/@openzeppelin/contracts/utils/math/SignedMath.sol":{"keccak256":"0xb1970fac7b64e6c09611e6691791e848d5e3fe410fa5899e7df2e0afd77a99e3","urls":["bzz-raw://db5fbb3dddd8b7047465b62575d96231ba8a2774d37fb4737fbf23340fabbb03","dweb:/ipfs/QmVUSvooZKEdEdap619tcJjTLcAuH6QBdZqAzWwnAXZAWJ"],"license":"MIT"},"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMountainRange.sol":{"keccak256":"0x014237038bb77bdf371b50c1268d02bf7eeaf7068c35483d5d0684ca7c30d544","urls":["bzz-raw://beb3ae60094f49716a2b0cfa6229ee27af1778934cf5f2dfc704433fda5a687b","dweb:/ipfs/QmQ339wEFT9X1woHyFTDbbhUCx8ekD9MGLRnhS3PpYp1pm"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/MerkleMultiProof.sol":{"keccak256":"0xd4f6e6a9eceaa7d1cdf9684bfe7f3f552adf21dfcf7f1372f7943a4c2f15deb7","urls":["bzz-raw://ec900b3e79ea8ef9d275697937d1984e994d10117aa2be066bd73f6a31aedf10","dweb:/ipfs/QmZQGLiJtbvUSEW5H2HEenzxp58fN43XHRY3SoNTC9Hobg"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Bytes.sol":{"keccak256":"0xd305383358b93285d8fcee512795487484eadfd7b602df16ff4e9b01afbefec7","urls":["bzz-raw://57c86cd2fe6ab591264c5632d503d77f04748d722e4defa9c2badb238ee1f9bd","dweb:/ipfs/QmTZ6xTcaYkRBshq6YZVMx2kkk94ZtJsRM6xLpC5qWT3oA"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Memory.sol":{"keccak256":"0x59e3a56caa42c1aac30231173439817d38c7f359e40bd36e9bb418d3f82ceab7","urls":["bzz-raw://e060fed46c6b420624166ea02a326f0f566897941bdc322257e690ae134b8179","dweb:/ipfs/QmbXW8yG2ZntjLMyUEmQGcRZroZj4dVZkBhHxK2PFKfUKB"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/Node.sol":{"keccak256":"0xca611969a68f7fe63dcdc742c9caf9bc1b26495561b68f4676c209279a4576ba","urls":["bzz-raw://17b7ec2cf65d484f0a2c188a7ae33430b015b3a55bf10393167f53839e2dde56","dweb:/ipfs/QmTQbT8J7rBRNHFXSR7S4KbpwCteMrFt8mvrzCD1vYYTwX"],"license":"Apache-2.0"},"node_modules/@polytope-labs/solidity-merkle-trees/src/trie/polkadot/ScaleCodec.sol":{"keccak256":"0x9ac4df46e68718f7deaaa5b7443778533f53dc0ff3736cc386cf4991099da2aa","urls":["bzz-raw://85d08b00d9173358323105be975ce85b7e206ba52df2d80a14d2581dec1ddd11","dweb:/ipfs/QmQCqVvSdJUqfhLH8TRBGNDGiEzkGduhRacnYhoKcm7e2a"],"license":"Apache-2.0"},"src/consensus/Codec.sol":{"keccak256":"0x477b62396db5a5c1d89d0388b0724dde80e0a0423b89c07f7b3b5622ff03b5d4","urls":["bzz-raw://6a584c6500ae347c6f9d76d06a96757b024dcb892601c6d9c1a945b9d76811d6","dweb:/ipfs/QmduKRb2Zr9eqeZbi5TFRw5Dyig3XVY3rMvPkxLktLFYrN"],"license":"Apache-2.0"},"src/consensus/EcdsaBeefy.sol":{"keccak256":"0x1fd6824212d0c42ecfb107d3599ff75d280a956dbaa212707eaa83b6e667aca7","urls":["bzz-raw://0471c92a4bf06528e9826ace0f48c32a17702ecbad93fb8e015123444c002448","dweb:/ipfs/QmaZ47vQbHFHNBzFpupqn66PdVigLYv7P363EmgsyXk2ED"],"license":"Apache-2.0"},"src/consensus/Types.sol":{"keccak256":"0xe4b169eefb4afd0f83f09335f4460705fe96e7736314c2030235720cf976e76f","urls":["bzz-raw://4ad510e26689491bcba165fd73c08e1b80302148804e27a184861712d966540b","dweb:/ipfs/QmVuaZ3yB3eBkHRh2DS6Cy1E6B3oige6fDtvCrcYssc3qi"],"license":"Apache-2.0"}},"version":1},"id":121} \ No newline at end of file diff --git a/evm/rust/src/conversions.rs b/evm/rust/src/conversions.rs index 5921dc390..1e46eb896 100644 --- a/evm/rust/src/conversions.rs +++ b/evm/rust/src/conversions.rs @@ -47,9 +47,9 @@ mod beefy { use super::ToU256; use crate::{ ecdsa_beefy::Beefy::{ - AuthoritySetCommitment, BeefyConsensusProof, BeefyConsensusState, BeefyMmrLeaf, - Commitment, Parachain, ParachainProof, Payload, RelayChainProof, SignedCommitment, - Vote, + AuthoritySet, AuthoritySetCommitment, BeefyConsensusProof, BeefyConsensusState, + BeefyMmrLeaf, Commitment, Parachain, ParachainProof, Payload, RelayChainProof, + SignedCommitment, Vote, }, sp1_beefy::SP1Beefy::{MiniCommitment, ParachainHeader, PartialBeefyMmrLeaf}, }; @@ -192,6 +192,17 @@ mod beefy { } } + impl From for AuthoritySet { + fn from(value: beefy_verifier_primitives::AuthoritySet) -> Self { + AuthoritySet { + id: value.id.to_u256(), + len: (value.len as u64).to_u256(), + blsPoseidonHash: alloy_primitives::U256::from_be_bytes(value.bls_poseidon_hash.0), + ecdsaMerkleRoot: FixedBytes::from(value.ecdsa_merkle_root.0), + } + } + } + impl From for BeefyConsensusState { fn from(value: ConsensusState) -> Self { BeefyConsensusState { @@ -205,6 +216,13 @@ mod beefy { impl From for ConsensusState { fn from(value: BeefyConsensusState) -> Self { + let authority_set = |set: AuthoritySet| beefy_verifier_primitives::AuthoritySet { + id: set.id.try_into().expect("authority set id out of bounds"), + len: set.len.try_into().expect("authority set length out of bounds"), + bls_poseidon_hash: H256(set.blsPoseidonHash.to_be_bytes()), + ecdsa_merkle_root: H256(set.ecdsaMerkleRoot.0), + }; + ConsensusState { beefy_activation_block: value .beefyActivationBlock @@ -215,59 +233,8 @@ mod beefy { .try_into() .expect("Beefy latest height out of bounds"), mmr_root_hash: Default::default(), - current_authorities: BeefyNextAuthoritySet { - id: value - .currentAuthoritySet - .id - .try_into() - .expect("current authority set id out of bounds"), - len: value - .currentAuthoritySet - .len - .try_into() - .expect("current authority set length out of bounds"), - keyset_commitment: H256(value.currentAuthoritySet.root.0), - }, - next_authorities: BeefyNextAuthoritySet { - id: value - .nextAuthoritySet - .id - .try_into() - .expect("next authority set out of bounds"), - len: value - .nextAuthoritySet - .len - .try_into() - .expect("next authority set length out of bounds"), - keyset_commitment: H256(value.nextAuthoritySet.root.0), - }, - } - } - } - - impl From for sp_consensus_beefy::mmr::MmrLeaf { - fn from(value: PartialBeefyMmrLeaf) -> Self { - let version: u8 = value.version.try_into().expect("mmr leaf version out of bounds"); - sp_consensus_beefy::mmr::MmrLeaf { - version: MmrLeafVersion::new(version >> 5, version & 0b11111), - parent_number_and_hash: ( - value.parentNumber.try_into().expect("parent number out of bounds"), - H256(value.parentHash.0), - ), - beefy_next_authority_set: BeefyNextAuthoritySet { - id: value - .nextAuthoritySet - .id - .try_into() - .expect("next authority set id out of bounds"), - len: value - .nextAuthoritySet - .len - .try_into() - .expect("next authority set len out of bounds"), - keyset_commitment: H256(value.nextAuthoritySet.root.0), - }, - leaf_extra: H256(value.extra.0), + current_authorities: authority_set(value.currentAuthoritySet), + next_authorities: authority_set(value.nextAuthoritySet), } } } @@ -303,6 +270,33 @@ mod beefy { } } + impl From for sp_consensus_beefy::mmr::MmrLeaf { + fn from(value: PartialBeefyMmrLeaf) -> Self { + let version: u8 = value.version.try_into().expect("mmr leaf version out of bounds"); + sp_consensus_beefy::mmr::MmrLeaf { + version: MmrLeafVersion::new(version >> 5, version & 0b11111), + parent_number_and_hash: ( + value.parentNumber.try_into().expect("parent number out of bounds"), + H256(value.parentHash.0), + ), + beefy_next_authority_set: BeefyNextAuthoritySet { + id: value + .nextAuthoritySet + .id + .try_into() + .expect("next authority set id out of bounds"), + len: value + .nextAuthoritySet + .len + .try_into() + .expect("next authority set len out of bounds"), + keyset_commitment: H256(value.nextAuthoritySet.root.0), + }, + leaf_extra: H256(value.extra.0), + } + } + } + impl From for SpMmrLeaf { fn from(value: BeefyMmrLeaf) -> Self { let version: u8 = value.version.try_into().expect("mmr leaf version out of bounds"); @@ -499,15 +493,16 @@ mod beefy { /// The direction tooling needs when bootstrapping a chain: the starting set's commitment has /// to be handed to `initialize_apk_state` in this encoding, since it is otherwise only ever /// learned from a header digest. - impl From + impl From for crate::bls_beefy::BlsBeefy::BeefyConsensusState { - fn from(value: beefy_verifier_primitives::ApkConsensusState) -> Self { - let authority_set = |set: beefy_verifier_primitives::ApkAuthoritySet| { - crate::bls_beefy::BlsBeefy::AuthoritySetCommitment { - id: set.id, - len: set.len, - root: FixedBytes(set.apk_commitment.0), + fn from(value: beefy_verifier_primitives::ConsensusState) -> Self { + let authority_set = |set: beefy_verifier_primitives::AuthoritySet| { + crate::bls_beefy::BlsBeefy::AuthoritySet { + id: set.id.to_u256(), + len: (set.len as u64).to_u256(), + blsPoseidonHash: alloy_primitives::U256::from_be_bytes(set.bls_poseidon_hash.0), + ecdsaMerkleRoot: FixedBytes(set.ecdsa_merkle_root.0), } }; @@ -523,22 +518,23 @@ mod beefy { /// The mmr root is not part of the initial state, since nothing has been proven yet. It is /// filled by the first update that verifies. impl TryFrom - for beefy_verifier_primitives::ApkConsensusState + for beefy_verifier_primitives::ConsensusState { type Error = &'static str; fn try_from( value: crate::bls_beefy::BlsBeefy::BeefyConsensusState, ) -> Result { - let authority_set = |set: crate::bls_beefy::BlsBeefy::AuthoritySetCommitment| { - beefy_verifier_primitives::ApkAuthoritySet { - id: set.id, - len: set.len, - apk_commitment: H256(set.root.0), - } + let authority_set = |set: crate::bls_beefy::BlsBeefy::AuthoritySet| { + Ok::<_, &'static str>(beefy_verifier_primitives::AuthoritySet { + id: set.id.try_into().map_err(|_| "authority set id out of bounds")?, + len: set.len.try_into().map_err(|_| "authority set size out of bounds")?, + bls_poseidon_hash: H256(set.blsPoseidonHash.to_be_bytes()), + ecdsa_merkle_root: H256(set.ecdsaMerkleRoot.0), + }) }; - Ok(beefy_verifier_primitives::ApkConsensusState { + Ok(beefy_verifier_primitives::ConsensusState { latest_beefy_height: value .latestHeight .try_into() @@ -548,8 +544,8 @@ mod beefy { .try_into() .map_err(|_| "beefy activation block out of bounds")?, mmr_root_hash: H256::zero(), - current_authorities: authority_set(value.currentAuthoritySet), - next_authorities: authority_set(value.nextAuthoritySet), + current_authorities: authority_set(value.currentAuthoritySet)?, + next_authorities: authority_set(value.nextAuthoritySet)?, }) } } diff --git a/evm/script/DeployConsensusRouter.s.sol b/evm/script/DeployConsensusRouter.s.sol index ee244cf9e..89b1b338e 100644 --- a/evm/script/DeployConsensusRouter.s.sol +++ b/evm/script/DeployConsensusRouter.s.sol @@ -31,12 +31,14 @@ contract DeployScript is BaseScript { function deploy() internal override { address ecdsaBeefy = config.get("ECDSA_BEEFY").toAddress(); address sp1Verifier = config.get("SP1_VERIFIER").toAddress(); + // Hyperbridge's own para id, whose headers carry the bls commitment every client records. + uint256 hyperbridgeParaId = config.get("HYPERBRIDGE_PARA_ID").toUint256(); // Guard against wiring the router to an address that holds no code on this chain. require(ecdsaBeefy.code.length != 0, "ECDSA_BEEFY has no code on this chain"); require(sp1Verifier.code.length != 0, "SP1_VERIFIER has no code on this chain"); - SP1Beefy sp1Beefy = new SP1Beefy{salt: salt}(ISP1Verifier(sp1Verifier), sp1VerificationKey); + SP1Beefy sp1Beefy = new SP1Beefy{salt: salt}(ISP1Verifier(sp1Verifier), sp1VerificationKey, hyperbridgeParaId); ConsensusRouter consensusRouter = new ConsensusRouter{salt: salt}(IConsensusV2(address(sp1Beefy)), IConsensusV2(ecdsaBeefy)); diff --git a/evm/script/DeployHostUpdates.s.sol b/evm/script/DeployHostUpdates.s.sol index be534425c..854e2cff6 100644 --- a/evm/script/DeployHostUpdates.s.sol +++ b/evm/script/DeployHostUpdates.s.sol @@ -22,10 +22,12 @@ contract DeployScript is BaseScript { /// @notice Main deployment logic - called by BaseScript's run() functions /// @dev This function is called within a broadcast context function deploy() internal override { - // Deploy consensus clients - EcdsaBeefy ecdsaBeefy = new EcdsaBeefy{salt: salt}(); + // Deploy consensus clients. Hyperbridge's own para id is the one whose headers carry the + // bls commitment, which every client records whether or not it checks proofs with it. + uint256 hyperbridgeParaId = config.get("HYPERBRIDGE_PARA_ID").toUint256(); + EcdsaBeefy ecdsaBeefy = new EcdsaBeefy{salt: salt}(hyperbridgeParaId); SP1Verifier verifier = new SP1Verifier{salt: salt}(); - SP1Beefy sp1 = new SP1Beefy{salt: salt}(verifier, sp1VerificationKey); + SP1Beefy sp1 = new SP1Beefy{salt: salt}(verifier, sp1VerificationKey, hyperbridgeParaId); ConsensusRouter consensusClient = new ConsensusRouter{salt: salt}( IConsensusV2(sp1), IConsensusV2(ecdsaBeefy) diff --git a/evm/script/DeployIsmp.s.sol b/evm/script/DeployIsmp.s.sol index b6588e54c..242a91f5f 100644 --- a/evm/script/DeployIsmp.s.sol +++ b/evm/script/DeployIsmp.s.sol @@ -53,9 +53,10 @@ contract DeployScript is BaseScript { // Deploy SP1 ZK consensus client SP1Verifier verifier = new SP1Verifier{salt: salt}(); - SP1Beefy sp1Beefy = new SP1Beefy{salt: salt}(verifier, sp1VerificationKey); + uint256 hyperbridgeParaId = config.get("HYPERBRIDGE_PARA_ID").toUint256(); + SP1Beefy sp1Beefy = new SP1Beefy{salt: salt}(verifier, sp1VerificationKey, hyperbridgeParaId); // Deploy EcdsaBeefy naive consensus client - EcdsaBeefy ecdsaBeefy = new EcdsaBeefy{salt: salt}(); + EcdsaBeefy ecdsaBeefy = new EcdsaBeefy{salt: salt}(hyperbridgeParaId); // Deploy ConsensusRouter wrapping both consensus clients ConsensusRouter consensusRouter = new ConsensusRouter{salt: salt}( IConsensusV2(address(sp1Beefy)), IConsensusV2(address(ecdsaBeefy)) diff --git a/evm/src/consensus/BlsBeefy.sol b/evm/src/consensus/BlsBeefy.sol index 70faa9337..b06226c08 100644 --- a/evm/src/consensus/BlsBeefy.sol +++ b/evm/src/consensus/BlsBeefy.sol @@ -23,6 +23,7 @@ import {ScaleCodec} from "@polytope-labs/solidity-merkle-trees/src/trie/polkadot import {Codec} from "./Codec.sol"; import { + AuthoritySet, AuthoritySetCommitment, ApkDigest, BlsApkBeefyConsensusProof, @@ -68,7 +69,7 @@ interface IApkProof { * The commitment the proof is checked against does not come from the relay chain's MMR leaf, the * way the keyset root does. Hyperbridge computes it over the relay's next authority set and * publishes it in a header digest, so a client picks it up from a header it has already verified - * and carries it in its consensus state. That is what `AuthoritySetCommitment.root` holds, and + * and carries it in its consensus state. That is what `AuthoritySet.blsPoseidonHash` holds, and * why the state has to be seeded with the starting set's commitment at initialisation. * * Requires Prague for the EIP-2537 precompiles. @@ -132,16 +133,22 @@ contract BlsBeefy is IConsensusV2, ERC165 { // Forward chaining: everything this client believes about the incoming set comes from // here, its id, its size and its commitment together. A header naming the set after the // one being waited on rolls the sets forward, since the relay has moved on. + // Both roots are filled in, not just the one this client checks against. The mmr leaf + // names the incoming set's ecdsa root and the digest names its poseidon hash, so a state + // this client advances stays usable by the ecdsa and sp1 clients too. if (digest.setId > newState.nextAuthoritySet.id) { newState.currentAuthoritySet = newState.nextAuthoritySet; - newState.nextAuthoritySet = AuthoritySetCommitment({ + newState.nextAuthoritySet = AuthoritySet({ id: digest.setId, len: digest.len, - root: bytes32(digest.commitment) + blsPoseidonHash: digest.commitment, + ecdsaMerkleRoot: relay.latestMmrLeaf.nextAuthoritySet.id == digest.setId + ? relay.latestMmrLeaf.nextAuthoritySet.root + : bytes32(0) }); - } else if (digest.setId == newState.nextAuthoritySet.id && newState.nextAuthoritySet.root == bytes32(0)) { + } else if (digest.setId == newState.nextAuthoritySet.id && newState.nextAuthoritySet.blsPoseidonHash == 0) { newState.nextAuthoritySet.len = digest.len; - newState.nextAuthoritySet.root = bytes32(digest.commitment); + newState.nextAuthoritySet.blsPoseidonHash = digest.commitment; } return (abi.encode(newState), intermediates, newState.nextAuthoritySet.id); @@ -162,12 +169,12 @@ contract BlsBeefy is IConsensusV2, ERC165 { } bool isCurrent = commitment.validatorSetId == trustedState.currentAuthoritySet.id; - AuthoritySetCommitment memory authoritySet = isCurrent ? trustedState.currentAuthoritySet : trustedState.nextAuthoritySet; + AuthoritySet memory authoritySet = isCurrent ? trustedState.currentAuthoritySet : trustedState.nextAuthoritySet; // A set whose commitment has not been learned from a digest yet cannot be verified against. // Reverting here is deliberate: silently accepting would mean checking the proof against a // zero commitment. - if (uint256(authoritySet.root) == 0) revert MissingApkCommitment(); + if (authoritySet.blsPoseidonHash == 0) revert MissingApkCommitment(); verifySignedByApk(Codec.Encode(commitment), relayProof, authoritySet); @@ -198,7 +205,7 @@ contract BlsBeefy is IConsensusV2, ERC165 { function verifySignedByApk( bytes memory encodedCommitment, BlsApkRelayChainProof memory relayProof, - AuthoritySetCommitment memory authoritySet + AuthoritySet memory authoritySet ) internal view { uint256 signed = countSigners(relayProof.bitlist); if (!checkParticipationThreshold(signed, authoritySet.len)) revert SuperMajorityRequired(); @@ -208,7 +215,7 @@ contract BlsBeefy is IConsensusV2, ERC165 { // `verify` reverts on failure rather than returning false, so a successful call is the // whole result. Wrapped so the reason surfaces as this contract's error. try _apk.verify( - uint256(authoritySet.root), + authoritySet.blsPoseidonHash, relayProof.bitlist, relayProof.apk, relayProof.apkProof, diff --git a/evm/src/consensus/Codec.sol b/evm/src/consensus/Codec.sol index f402fdf66..aea30a06e 100644 --- a/evm/src/consensus/Codec.sol +++ b/evm/src/consensus/Codec.sol @@ -28,6 +28,8 @@ import "./Types.sol"; * and decode SCALE compact unsigned integers. */ library Codec { + using HeaderImpl for Header; + uint8 internal constant DIGEST_ITEM_OTHER = 0; uint8 internal constant DIGEST_ITEM_CONSENSUS = 4; uint8 internal constant DIGEST_ITEM_SEAL = 5; @@ -169,4 +171,39 @@ library Codec { } return (value); } + + /// The commitment hyperbridge published for `setId`, taken from whichever of these headers is + /// hyperbridge's own. Zero when none of them carries one for that set. + /// + /// Every client keeps both roots of an authority set, so a client that has no use for this one + /// still has to record it, or it strands whichever client does. + function blsPoseidonHash(ParachainHeader[] memory headers, uint64 setId, uint256 digestParaId) + internal + pure + returns (uint256) + { + for (uint256 i = 0; i < headers.length; i++) { + if (headers[i].id != digestParaId) continue; + return commitmentFor(headers[i].header, setId); + } + return 0; + } + + /// The same, for the proof shape that carries a merkle index alongside each header. + function blsPoseidonHash(Parachain[] memory parachains, uint64 setId, uint256 digestParaId) + internal + pure + returns (uint256) + { + for (uint256 i = 0; i < parachains.length; i++) { + if (parachains[i].id != digestParaId) continue; + return commitmentFor(parachains[i].header, setId); + } + return 0; + } + + function commitmentFor(bytes memory header, uint64 setId) private pure returns (uint256) { + ApkDigest memory digest = DecodeHeader(header).apkCommitment(); + return digest.setId == setId ? digest.commitment : 0; + } } diff --git a/evm/src/consensus/EcdsaBeefy.sol b/evm/src/consensus/EcdsaBeefy.sol index e0fb7d52c..a99e79ccd 100644 --- a/evm/src/consensus/EcdsaBeefy.sol +++ b/evm/src/consensus/EcdsaBeefy.sol @@ -29,6 +29,7 @@ import {Codec} from "./Codec.sol"; import { Header, HeaderImpl, + AuthoritySet, AuthoritySetCommitment, Vote, RelayChainProof, @@ -63,6 +64,15 @@ import { contract EcdsaBeefy is IConsensusV2, ERC165 { using HeaderImpl for Header; + /// The parachain whose header digests carry the bls commitment, which is hyperbridge. Only its + /// headers are read for one, since every parachain in a proof is equally authentic and only + /// this one speaks for the relay's authorities. + uint256 public immutable _digestParaId; + + constructor(uint256 digestParaId) { + _digestParaId = digestParaId; + } + // The PayloadId for the mmr root. bytes2 public constant MMR_ROOT_PAYLOAD_ID = bytes2("mh"); @@ -95,7 +105,7 @@ contract EcdsaBeefy is IConsensusV2, ERC165 { /// the updated state along with the latest authority set id. function verify(bytes calldata previousState, bytes calldata proof) external - pure + view returns (bytes memory, IntermediateState[] memory, uint256) { BeefyConsensusState memory consensusState = abi.decode(previousState, (BeefyConsensusState)); @@ -110,6 +120,21 @@ contract EcdsaBeefy is IConsensusV2, ERC165 { (BeefyConsensusState memory newState, bytes32 headsRoot) = verifyMmrUpdateProof(consensusState, relay); IntermediateState[] memory intermediates = verifyParachainHeaderProof(headsRoot, parachain); + // Rotating here rather than inside the mmr check, because the bls half of the set comes + // from a hyperbridge header and those are only trustworthy once proven against the heads + // root above. Both halves are recorded even though this client only uses one, so the + // state stays usable by the aggregate client. + AuthoritySetCommitment memory incoming = relay.latestMmrLeaf.nextAuthoritySet; + if (incoming.id > newState.nextAuthoritySet.id) { + newState.currentAuthoritySet = newState.nextAuthoritySet; + newState.nextAuthoritySet = AuthoritySet({ + id: incoming.id, + len: incoming.len, + blsPoseidonHash: Codec.blsPoseidonHash(parachain.parachains, incoming.id, _digestParaId), + ecdsaMerkleRoot: incoming.root + }); + } + return (abi.encode(newState), intermediates, newState.nextAuthoritySet.id); } @@ -135,7 +160,7 @@ contract EcdsaBeefy is IConsensusV2, ERC165 { } bool isCurrentAuthorities = commitment.validatorSetId == trustedState.currentAuthoritySet.id; - AuthoritySetCommitment memory authoritySet = + AuthoritySet memory authoritySet = isCurrentAuthorities ? trustedState.currentAuthoritySet : trustedState.nextAuthoritySet; if (!checkParticipationThreshold(sigLen, authoritySet.len)) revert SuperMajorityRequired(); @@ -158,14 +183,12 @@ contract EcdsaBeefy is IConsensusV2, ERC165 { MerkleMultiProof.Leaf({index: vote.authorityIndex, hash: keccak256(abi.encodePacked(authority))}); } - bool valid = MerkleMultiProof.VerifyProof(authoritySet.root, relayProof.proof, authorities, authoritySet.len); + bool valid = MerkleMultiProof.VerifyProof( + authoritySet.ecdsaMerkleRoot, relayProof.proof, authorities, authoritySet.len + ); if (!valid) revert InvalidAuthoritiesProof(); verifyMmrLeaf(trustedState, relayProof, mmrRoot); - if (relayProof.latestMmrLeaf.nextAuthoritySet.id > trustedState.nextAuthoritySet.id) { - trustedState.currentAuthoritySet = trustedState.nextAuthoritySet; - trustedState.nextAuthoritySet = relayProof.latestMmrLeaf.nextAuthoritySet; - } trustedState.latestHeight = latestHeight; return (trustedState, relayProof.latestMmrLeaf.extra); diff --git a/evm/src/consensus/SP1Beefy.sol b/evm/src/consensus/SP1Beefy.sol index d4266bcda..d25fea4f3 100644 --- a/evm/src/consensus/SP1Beefy.sol +++ b/evm/src/consensus/SP1Beefy.sol @@ -22,6 +22,7 @@ import {Codec} from "./Codec.sol"; import { Header, HeaderImpl, + AuthoritySet, AuthoritySetCommitment, BeefyConsensusState, MiniCommitment, @@ -66,9 +67,15 @@ contract SP1Beefy is IConsensusV2, ERC165 { // Genesis block should not be provided error IllegalGenesisBlock(); - constructor(ISP1Verifier v, bytes32 vk) { + /// The parachain whose header digests carry the bls commitment, which is hyperbridge. Only + /// its headers are read for one, since every parachain in a proof is equally authentic and + /// only this one speaks for the relay's authorities. + uint256 public immutable _digestParaId; + + constructor(ISP1Verifier v, bytes32 vk, uint256 digestParaId) { verifier = v; verificationKey = vk; + _digestParaId = digestParaId; } /** @@ -124,7 +131,7 @@ contract SP1Beefy is IConsensusV2, ERC165 { if (uint256(proof.mmrLeaf.parentNumber) + 1 != commitment.blockNumber) revert StaleMmrLeaf(); - AuthoritySetCommitment memory authority; + AuthoritySet memory authority; if (commitment.validatorSetId == trustedState.nextAuthoritySet.id) { authority = trustedState.nextAuthoritySet; } else if (commitment.validatorSetId == trustedState.currentAuthoritySet.id) { @@ -145,7 +152,7 @@ contract SP1Beefy is IConsensusV2, ERC165 { bytes memory publicInputs = abi.encode( PublicInputs({ authorities_len: authority.len, - authorities_root: authority.root, + authorities_root: authority.ecdsaMerkleRoot, headers: headers, block_number: commitment.blockNumber, leaf_hash: keccak256(Codec.Encode(proof.mmrLeaf)), @@ -169,7 +176,14 @@ contract SP1Beefy is IConsensusV2, ERC165 { if (proof.mmrLeaf.nextAuthoritySet.id > trustedState.nextAuthoritySet.id) { trustedState.currentAuthoritySet = trustedState.nextAuthoritySet; - trustedState.nextAuthoritySet = proof.mmrLeaf.nextAuthoritySet; + trustedState.nextAuthoritySet = AuthoritySet({ + id: proof.mmrLeaf.nextAuthoritySet.id, + len: proof.mmrLeaf.nextAuthoritySet.len, + blsPoseidonHash: Codec.blsPoseidonHash( + proof.headers, proof.mmrLeaf.nextAuthoritySet.id, _digestParaId + ), + ecdsaMerkleRoot: proof.mmrLeaf.nextAuthoritySet.root + }); } trustedState.latestHeight = commitment.blockNumber; diff --git a/evm/src/consensus/Types.sol b/evm/src/consensus/Types.sol index 4ad434a32..cc5800a55 100644 --- a/evm/src/consensus/Types.sol +++ b/evm/src/consensus/Types.sol @@ -85,6 +85,10 @@ struct Commitment { uint64 validatorSetId; } +/// A validator set as the relay chain describes it in an mmr leaf. +/// +/// The field widths are part of how the leaf is hashed into the mmr, so they follow the relay +/// rather than our own preference. [`AuthoritySet`] is the shape a client keeps in its state. struct AuthoritySetCommitment { /// Id of the set. uint64 id; @@ -94,6 +98,23 @@ struct AuthoritySetCommitment { bytes32 root; } +/// A validator set as a client keeps it, holding what every proof format needs to check a +/// signature from that set. +/// +/// One state serves all of them, so a client that advances the set fills in both roots rather than +/// only the one it uses. Leaving the other empty would strand whichever client relies on it until +/// something supplies it again. +struct AuthoritySet { + /// Id of the set. + uint256 id; + /// Number of validators in the set, which the threshold is taken against. + uint256 len; + /// Poseidon2 over the set's G1 keys, which an aggregate proof is checked against. + uint256 blsPoseidonHash; + /// Merkle root over the set's ecdsa keys, which a per signer proof is checked against. + bytes32 ecdsaMerkleRoot; +} + struct BeefyMmrLeaf { uint8 version; uint32 parentNumber; @@ -110,9 +131,9 @@ struct BeefyConsensusState { /// This should be the first block in the merkle-mountain-range tree. uint256 beefyActivationBlock; /// authorities for the current round - AuthoritySetCommitment currentAuthoritySet; + AuthoritySet currentAuthoritySet; /// authorities for the next round - AuthoritySetCommitment nextAuthoritySet; + AuthoritySet nextAuthoritySet; } struct PartialBeefyMmrLeaf { diff --git a/evm/tests/foundry/Beefy.sol b/evm/tests/foundry/Beefy.sol index ba41b03a1..81df94347 100644 --- a/evm/tests/foundry/Beefy.sol +++ b/evm/tests/foundry/Beefy.sol @@ -24,7 +24,8 @@ contract BeefyConsensusClientTest is Test { EcdsaBeefy internal beefy; function setUp() public virtual { - beefy = new EcdsaBeefy(); + // Hyperbridge is para 4009 on the chain these fixtures came from. + beefy = new EcdsaBeefy(4009); } function VerifyV2(bytes calldata trustedConsensusState, bytes calldata proof) diff --git a/evm/tests/foundry/SP1BeefyForkTest.sol b/evm/tests/foundry/SP1BeefyForkTest.sol index 05ee80aaf..a846b18c2 100644 --- a/evm/tests/foundry/SP1BeefyForkTest.sol +++ b/evm/tests/foundry/SP1BeefyForkTest.sol @@ -57,7 +57,7 @@ contract SP1BeefyForkTest is Test { // Reuse the live SP1 verifier from the production deployment, but with the new vkey. ISP1Verifier verifier = SP1Beefy(SP1_BEEFY).verifier(); - SP1Beefy beefy = new SP1Beefy(verifier, VKEY); + SP1Beefy beefy = new SP1Beefy(verifier, VKEY, 4009); (bytes memory newStateEnc, IntermediateState[] memory intermediates, uint256 nextAuthId) = beefy.verify(previousState, proof); diff --git a/evm/tests/foundry/SP1BeefyTest.sol b/evm/tests/foundry/SP1BeefyTest.sol index cfa18bd6b..0f09239bd 100644 --- a/evm/tests/foundry/SP1BeefyTest.sol +++ b/evm/tests/foundry/SP1BeefyTest.sol @@ -27,12 +27,12 @@ contract SP1BeefyTest is Test { function setUp() public virtual { sp1 = new SP1Verifier(); - beefy = new SP1Beefy(sp1, verificationKey); + beefy = new SP1Beefy(sp1, verificationKey, 4009); } function testDecodeConsensusState() public pure { bytes memory encodedState = - hex"0000000000000000000000000000000000000000000000000000000001771a6a00000000000000000000000000000000000000000000000000000000012a5318000000000000000000000000000000000000000000000000000000000000083a00000000000000000000000000000000000000000000000000000000000001f442c444bf993527f25cdeb8cca93b6632fbcacb30cf3e037748e5ca8f39ef9ade000000000000000000000000000000000000000000000000000000000000083b00000000000000000000000000000000000000000000000000000000000001f442c444bf993527f25cdeb8cca93b6632fbcacb30cf3e037748e5ca8f39ef9ade"; + hex"0000000000000000000000000000000000000000000000000000000001771a6a00000000000000000000000000000000000000000000000000000000012a5318000000000000000000000000000000000000000000000000000000000000083a00000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000000042c444bf993527f25cdeb8cca93b6632fbcacb30cf3e037748e5ca8f39ef9ade000000000000000000000000000000000000000000000000000000000000083b00000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000000042c444bf993527f25cdeb8cca93b6632fbcacb30cf3e037748e5ca8f39ef9ade"; BeefyConsensusState memory consensusState = abi.decode(encodedState, (BeefyConsensusState)); @@ -44,14 +44,14 @@ contract SP1BeefyTest is Test { console.log("currentAuthoritySet.len: "); console.log(consensusState.currentAuthoritySet.len); console.log("currentAuthoritySet.root: "); - console.logBytes32(consensusState.currentAuthoritySet.root); + console.logBytes32(consensusState.currentAuthoritySet.ecdsaMerkleRoot); console.log("nextAuthoritySet.id: "); console.log(consensusState.nextAuthoritySet.id); console.log("nextAuthoritySet.len: "); console.log(consensusState.nextAuthoritySet.len); console.log("nextAuthoritySet.root: "); - console.logBytes32(consensusState.nextAuthoritySet.root); + console.logBytes32(consensusState.nextAuthoritySet.ecdsaMerkleRoot); } function skip_testPolkadotVerifier() public view { @@ -79,7 +79,7 @@ contract SP1BeefyTest is Test { // proof was generated against sp1-beefy v1.0.0. The proof is an ABI-encoded SP1BeefyProof // (abi_encode_params output; no outer offset). function sp1BeefyFixture() internal pure returns (bytes memory state, bytes memory proof) { - state = hex"0000000000000000000000000000000000000000000000000000000001df6bd100000000000000000000000000000000000000000000000000000000012a5318000000000000000000000000000000000000000000000000000000000000136a00000000000000000000000000000000000000000000000000000000000002582cd28e2a83ddf10dbcc7da45533a44c70d5bc52be1868649ab8c30f7ec6dc741000000000000000000000000000000000000000000000000000000000000136b00000000000000000000000000000000000000000000000000000000000002582cd28e2a83ddf10dbcc7da45533a44c70d5bc52be1868649ab8c30f7ec6dc741"; + state = hex"0000000000000000000000000000000000000000000000000000000001df6bd100000000000000000000000000000000000000000000000000000000012a5318000000000000000000000000000000000000000000000000000000000000136a000000000000000000000000000000000000000000000000000000000000025800000000000000000000000000000000000000000000000000000000000000002cd28e2a83ddf10dbcc7da45533a44c70d5bc52be1868649ab8c30f7ec6dc741000000000000000000000000000000000000000000000000000000000000136b000000000000000000000000000000000000000000000000000000000000025800000000000000000000000000000000000000000000000000000000000000002cd28e2a83ddf10dbcc7da45533a44c70d5bc52be1868649ab8c30f7ec6dc741"; proof = hex"0000000000000000000000000000000000000000000000000000000001df6bd9000000000000000000000000000000000000000000000000000000000000136a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001df6bd8b06c82d25b39550a06ab64cf89004fce1f913b27190ab108320812295591fa89000000000000000000000000000000000000000000000000000000000000136b00000000000000000000000000000000000000000000000000000000000002582cd28e2a83ddf10dbcc7da45533a44c70d5bc52be1868649ab8c30f7ec6dc741ed96e512661b155ef81e590ca5ad1bacf2ccce06e7e822ca521daa71efb4ff91000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000003608eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000d2700000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000139557ed2657ce1e450327c6006e17e64425bb2154a7e6a55514e3d37fc7fd5d9884697790283bf3e632f74afab019365ed730a6deb0bc3e70bb229635fcf769d28febdf61f520234bde985bfdd18c0b04baf50ddae8f48860b9546184888ce393886265396140661757261209441d70800000000045250535290b43da1ab3f398f7008b0bd1374925ba70102ff77f33c1acce60e98a4e40fb8cf56af7d070449534d500101af5c78d7d0420a25ee6b68dc946d9919da3799b923cff420aa27ab1b646f355794a54ad343c04bc95bb013d63caecc98e97b65edb739cacc2c6e97f7d5aba5c9044953544d20f612176a000000000561757261010162f8da99803bec263b758f801ed06717d9af6178ac74e3c5ecba6e9cf6ab5c33e4daf9b75a18945bc3bb2221e1aceef06b67c87a0c6756872789dffbcd747b810000000000000000000000000000000000000000000000000000000000000000000000000001644388a21c0000000000000000000000000000000000000000000000000000000000000000002f850ee998974d6cc00e50cd0814b098c05bfade466d28573240d057f2535200000000000000000000000000000000000000000000000000000000000000002607774c88245bcad79f2414d5829f9b61771e86fb92366a5d224ba9a42cea9b16e91bc69ca90c8f455e8973ca2d522e460b371e95a8cdd298e00558bb25e39c1046ac2fb71dfc17f57e39ae5309c9522d97cc181e836aa679be1c168e25180b1556c8c21b6537ff21a57ecb73d497301f5fc9fe8f8d312d03720e401684da5e16440efe811bc61f2bfa210171efc4745d1b7461ce5593c8bcd9a9b2f6489f6e0c8cfbf59f1489e3e9a93143084cd57df0bf06cbf1fce9a7098154abcfb984a90fd4708053142c7043ce767492db2f5c0055f8791ef0cfb31173e9cac6ab47600e3953c2efe616bbd960b6048026dd1e0bb8bba4a29f3ccdb5ac21ce3899751300000000000000000000000000000000000000000000000000000000"; } diff --git a/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex b/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex index 4410479fd..1473b463e 100644 --- a/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex +++ b/evm/tests/foundry/fixtures/bls-apk-beefy-state.hex @@ -1 +1 @@ -0x000000000000000000000000000000000000000000000000000000000000ddbd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d5e000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d5f000000000000000000000000000000000000000000000000000000000000000213ba45ba08c2f9519d727dbefbc799cc1396a6c03f6438003c2c3950cab05e25 \ No newline at end of file +0x000000000000000000000000000000000000000000000000000000000000ddbd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d5e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d5f000000000000000000000000000000000000000000000000000000000000000213ba45ba08c2f9519d727dbefbc799cc1396a6c03f6438003c2c3950cab05e250000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/modules/consensus/beefy/primitives/src/lib.rs b/modules/consensus/beefy/primitives/src/lib.rs index 95089a2bb..9a495a1ee 100644 --- a/modules/consensus/beefy/primitives/src/lib.rs +++ b/modules/consensus/beefy/primitives/src/lib.rs @@ -35,9 +35,9 @@ pub struct ConsensusState { /// Latest mmr root hash pub mmr_root_hash: H256, /// Authorities for the current session - pub current_authorities: BeefyAuthoritySet, + pub current_authorities: AuthoritySet, /// Authorities for the next session - pub next_authorities: BeefyAuthoritySet, + pub next_authorities: AuthoritySet, } /// Hash length definition for hashing algorithms used @@ -223,62 +223,58 @@ impl ApkCommitmentDigest { }) } } - -/// An authority set identified by a commitment to its keys rather than a merkle root over them. +/// A validator set as a client keeps it, holding what every proof format needs to check a +/// signature from that set. /// -/// The commitment is Poseidon2 over the validators' G1 keys, which is what the APK circuit binds -/// to. It cannot be read off the relay chain, so it arrives in a header digest and is empty until -/// one has been seen. +/// One state serves all of them, so a client that advances the set fills in both roots rather than +/// only the one it uses. Leaving the other empty would strand whichever client relies on it until +/// something supplies it again. This is not the mmr leaf's authority set, whose shape belongs to +/// the relay chain. #[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode, Default)] -pub struct ApkAuthoritySet { +pub struct AuthoritySet { /// Id of the set pub id: u64, - /// Number of validators in the set + /// Number of validators in the set, which the threshold is taken against pub len: u32, - /// Poseidon2 over the set's G1 keys, zero until a digest supplies it - pub apk_commitment: H256, -} - -/// Consensus state for BEEFY verified through an aggregate public key proof. -#[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode, Default)] -pub struct ApkConsensusState { - /// Latest beefy height - pub latest_beefy_height: u32, - /// Height at which beefy was activated - pub beefy_activation_block: u32, - /// Latest mmr root hash - pub mmr_root_hash: H256, - /// Authorities for the current session - pub current_authorities: ApkAuthoritySet, - /// Authorities for the next session - pub next_authorities: ApkAuthoritySet, + /// Poseidon2 over the set's G1 keys, zero until a header digest supplies it + pub bls_poseidon_hash: H256, + /// Merkle root over the set's ecdsa keys, as the mmr leaf names it + pub ecdsa_merkle_root: H256, } -impl ApkConsensusState { +impl ConsensusState { /// What this state becomes once a verified header names an authority set, if anything. /// /// A header naming the set after the one being waited on rolls the sets forward: the relay has /// moved on, so the current set is the one that was next. A header naming a set already held /// fills in its commitment if it has none, which is what a client waiting on the next set is - /// looking for. Anything else, including a set already carrying a commitment, returns `None`, - /// so the same digest arriving in several headers changes nothing. - pub fn with_authority_set( + /// looking for. Anything else, including a set already carrying one, returns `None`, so the + /// same digest arriving in several headers changes nothing. + pub fn with_bls_commitment( &self, set_id: u64, len: u32, commitment: H256, - ) -> Option<(ApkAuthoritySet, ApkAuthoritySet)> { - let incoming = ApkAuthoritySet { id: set_id, len, apk_commitment: commitment }; + ecdsa_merkle_root: H256, + ) -> Option<(AuthoritySet, AuthoritySet)> { + let incoming = + AuthoritySet { id: set_id, len, bls_poseidon_hash: commitment, ecdsa_merkle_root }; if set_id > self.next_authorities.id { return Some((self.next_authorities.clone(), incoming)); } - if set_id == self.next_authorities.id && self.next_authorities.apk_commitment.is_zero() { - return Some((self.current_authorities.clone(), incoming)); + if set_id == self.next_authorities.id && self.next_authorities.bls_poseidon_hash.is_zero() { + let mut next = self.next_authorities.clone(); + next.len = len; + next.bls_poseidon_hash = commitment; + return Some((self.current_authorities.clone(), next)); } if set_id == self.current_authorities.id && - self.current_authorities.apk_commitment.is_zero() + self.current_authorities.bls_poseidon_hash.is_zero() { - return Some((incoming, self.next_authorities.clone())); + let mut current = self.current_authorities.clone(); + current.len = len; + current.bls_poseidon_hash = commitment; + return Some((current, self.next_authorities.clone())); } None } diff --git a/modules/consensus/beefy/prover/src/lib.rs b/modules/consensus/beefy/prover/src/lib.rs index ffd1c4bc5..ebabcc86e 100644 --- a/modules/consensus/beefy/prover/src/lib.rs +++ b/modules/consensus/beefy/prover/src/lib.rs @@ -152,6 +152,19 @@ fn build_parachain_proof(para_ids: &[u32], heads: &[(u32, Vec)]) -> Parachai ParachainProof { parachains, proof, total_leaves: leaf_count as u32 } } +/// The client side shape of an authority set, built from what the relay chain names. +/// +/// The relay only knows the ecdsa merkle root. The poseidon hash reaches a client through a +/// hyperbridge header digest, so it starts empty here and is filled in once one is seen. +fn from_relay(set: BeefyAuthoritySet) -> beefy_verifier_primitives::AuthoritySet { + beefy_verifier_primitives::AuthoritySet { + id: set.id, + len: set.len, + bls_poseidon_hash: H256::zero(), + ecdsa_merkle_root: set.keyset_commitment, + } +} + impl Prover { /// Construct a beefy client state to be submitted to the counterparty chain pub async fn get_initial_consensus_state( @@ -176,14 +189,15 @@ impl Prover { mmr_root_hash, beefy_activation_block: self.beefy_activation_block, latest_beefy_height: signed_commitment.commitment.block_number as u32, - current_authorities: self - .mmr_leaf_current_authorities(Some(latest_beefy_finalized)) - .await?, - next_authorities: beefy_mmr_leaf_next_authorities( - &self.relay_rpc, - Some(latest_beefy_finalized), - ) - .await?, + // The relay only names the ecdsa root. A client learns the poseidon hash from a + // hyperbridge header digest, so a state starting here has that half empty. + current_authorities: from_relay( + self.mmr_leaf_current_authorities(Some(latest_beefy_finalized)).await?, + ), + next_authorities: from_relay( + beefy_mmr_leaf_next_authorities(&self.relay_rpc, Some(latest_beefy_finalized)) + .await?, + ), }; Ok(client_state) diff --git a/modules/consensus/beefy/verifier/src/apk.rs b/modules/consensus/beefy/verifier/src/apk.rs index 11101cd41..962247a02 100644 --- a/modules/consensus/beefy/verifier/src/apk.rs +++ b/modules/consensus/beefy/verifier/src/apk.rs @@ -29,8 +29,8 @@ use ark_ec::{AffineRepr, CurveGroup, PrimeGroup, pairing::Pairing}; use ark_ff::{BigInteger, One, PrimeField, Zero}; use ark_serialize::CanonicalDeserialize; use beefy_verifier_primitives::{ - APK_BITLIST_WORDS, APK_G1_LEN, APK_G2_LEN, ApkAuthoritySet, ApkCommitmentDigest, - ApkConsensusMessage, ApkConsensusState, ApkMmrProof, ParachainHeader, + APK_BITLIST_WORDS, APK_G1_LEN, APK_G2_LEN, ApkCommitmentDigest, ApkConsensusMessage, + ApkMmrProof, ConsensusState, ParachainHeader, }; use codec::{Decode, Encode}; use polkadot_sdk::*; @@ -68,18 +68,20 @@ const SEED: [u8; APK_G1_LEN] = hex_literal::hex!( /// is, and the headers only once the leaf is. Any commitment picked up from those headers is /// therefore learned from something already proven. pub fn verify_apk_consensus( - trusted_state: ApkConsensusState, + trusted_state: ConsensusState, proof: ApkConsensusMessage, verifying_key: &[u8], digest_para_id: u32, -) -> Result<(ApkConsensusState, Vec), Error> { +) -> Result<(ConsensusState, Vec), Error> { + // The leaf names the incoming set's ecdsa root, which this client has no use for but records + // anyway so a state it advances stays usable by the per signer and sp1 clients. + let ecdsa_merkle_root = proof.mmr.latest_mmr_leaf.beefy_next_authority_set.keyset_commitment; let (mut state, heads_root) = verify_apk_mmr_update_proof::(trusted_state, proof.mmr, verifying_key)?; let headers = crate::verify_parachain_headers::(heads_root, proof.parachain)?; - // Forward chaining: everything this client believes about the incoming set comes from here, - // its id, its size and its commitment together. The mmr leaf names a next set too, but says - // nothing about Poseidon2, and taking the size from there and the commitment from here would + // Forward chaining: the set's id, its size and its commitment all come from here together, + // rather than taking the size from the mmr leaf and the commitment from a digest, which would // leave the two free to describe different sets. // // Only `digest_para_id`'s headers are read. A proof carries whichever parachains the relay @@ -91,7 +93,12 @@ pub fn verify_apk_consensus( .filter(|header| header.para_id == digest_para_id) .find_map(|header| read_apk_digest(&header.header)) .and_then(|digest| { - state.with_authority_set(digest.set_id, digest.len, H256(digest.commitment)) + state.with_bls_commitment( + digest.set_id, + digest.len, + H256(digest.commitment), + ecdsa_merkle_root, + ) }) { state.current_authorities = current; state.next_authorities = next; @@ -102,10 +109,10 @@ pub fn verify_apk_consensus( /// Verify the signed mmr root and roll the authority sets forward. pub fn verify_apk_mmr_update_proof( - mut trusted_state: ApkConsensusState, + mut trusted_state: ConsensusState, mmr: ApkMmrProof, verifying_key: &[u8], -) -> Result<(ApkConsensusState, H256), Error> { +) -> Result<(ConsensusState, H256), Error> { if trusted_state.latest_beefy_height >= mmr.commitment.block_number { return Err(Error::StaleHeight { trusted_height: trusted_state.latest_beefy_height, @@ -125,10 +132,10 @@ pub fn verify_apk_mmr_update_proof( // A set whose commitment has not been learned from a digest yet cannot be verified against. // Refusing is deliberate: proceeding would check the proof against a zero commitment, which // establishes nothing at all. - if authority_set.apk_commitment.is_zero() { + if authority_set.bls_poseidon_hash.is_zero() { return Err(Error::ApkCommitmentMissing { id: set_id }); } - let apk_commitment = authority_set.apk_commitment; + let apk_commitment = authority_set.bls_poseidon_hash; let authority_count = authority_set.len; verify_signed_by_apk( diff --git a/modules/consensus/beefy/verifier/src/ecdsa.rs b/modules/consensus/beefy/verifier/src/ecdsa.rs index cd16be65f..10a62981a 100644 --- a/modules/consensus/beefy/verifier/src/ecdsa.rs +++ b/modules/consensus/beefy/verifier/src/ecdsa.rs @@ -25,7 +25,9 @@ use crate::{ EcdsaRecover, MMR_ROOT_PAYLOAD_ID, MerkleHasher, error::Error, verify_mmr_leaf, verify_parachain_headers, }; -use beefy_verifier_primitives::{ConsensusMessage, ConsensusState, MmrProof, ParachainHeader}; +use beefy_verifier_primitives::{ + AuthoritySet, ConsensusMessage, ConsensusState, MmrProof, ParachainHeader, +}; use codec::Encode; use ismp::messaging::Keccak256; use merkle_mountain_range::{ @@ -147,7 +149,7 @@ fn prepare_update( } Ok(UpdatePreamble { - keyset_commitment: authority_set.keyset_commitment, + keyset_commitment: authority_set.ecdsa_merkle_root, authority_count: authority_set.len, mmr_root: H256::from_slice(mmr_root_data), }) @@ -181,7 +183,14 @@ fn apply_update( ) -> ConsensusState { if leaf.beefy_next_authority_set.id > trusted_state.next_authorities.id { trusted_state.current_authorities = trusted_state.next_authorities.clone(); - trusted_state.next_authorities = leaf.beefy_next_authority_set.clone(); + trusted_state.next_authorities = AuthoritySet { + id: leaf.beefy_next_authority_set.id, + len: leaf.beefy_next_authority_set.len, + // This client cannot compute the poseidon hash, and it only reaches a chain through a + // hyperbridge header digest, so it stays empty until one supplies it. + bls_poseidon_hash: H256::zero(), + ecdsa_merkle_root: leaf.beefy_next_authority_set.keyset_commitment, + }; } trusted_state.latest_beefy_height = latest_height; diff --git a/modules/consensus/beefy/verifier/src/sp1.rs b/modules/consensus/beefy/verifier/src/sp1.rs index a4b8afb13..5de945da7 100644 --- a/modules/consensus/beefy/verifier/src/sp1.rs +++ b/modules/consensus/beefy/verifier/src/sp1.rs @@ -22,9 +22,10 @@ use alloy_sol_types::{ private::{FixedBytes, U256}, sol, }; -use beefy_verifier_primitives::{ConsensusState, ParachainHeader, Sp1BeefyProof}; +use beefy_verifier_primitives::{AuthoritySet, ConsensusState, ParachainHeader, Sp1BeefyProof}; use codec::Encode; use ismp::messaging::Keccak256; +use primitive_types::H256; // Matches `PublicInputs` and `ParachainHeaderHash` in evm/src/consensus/Types.sol sol! { @@ -91,7 +92,7 @@ pub fn verify_sp1_consensus( .collect(); let public_inputs = PublicInputs { - authorities_root: FixedBytes::from(Into::<[u8; 32]>::into(authority.keyset_commitment)), + authorities_root: FixedBytes::from(Into::<[u8; 32]>::into(authority.ecdsa_merkle_root)), authorities_len: U256::from(authority.len), leaf_hash: FixedBytes::from(Into::<[u8; 32]>::into(H::keccak256(&proof.mmr_leaf.encode()))), block_number: U256::from(proof.block_number), @@ -111,7 +112,14 @@ pub fn verify_sp1_consensus( let mut new_state = trusted_state; if proof.mmr_leaf.beefy_next_authority_set.id > new_state.next_authorities.id { new_state.current_authorities = new_state.next_authorities.clone(); - new_state.next_authorities = proof.mmr_leaf.beefy_next_authority_set.clone(); + new_state.next_authorities = AuthoritySet { + id: proof.mmr_leaf.beefy_next_authority_set.id, + len: proof.mmr_leaf.beefy_next_authority_set.len, + // This client cannot compute the poseidon hash, and it only reaches a chain through a + // hyperbridge header digest, so it stays empty until one supplies it. + bls_poseidon_hash: H256::zero(), + ecdsa_merkle_root: proof.mmr_leaf.beefy_next_authority_set.keyset_commitment, + }; } new_state.latest_beefy_height = proof.block_number; diff --git a/modules/consensus/beefy/verifier/tests/apk_fixture.rs b/modules/consensus/beefy/verifier/tests/apk_fixture.rs index ab9520ae3..6c0a1efb7 100644 --- a/modules/consensus/beefy/verifier/tests/apk_fixture.rs +++ b/modules/consensus/beefy/verifier/tests/apk_fixture.rs @@ -22,7 +22,7 @@ use alloy_sol_types::SolType; use beefy_verifier::apk::{count_signers, verify_apk_consensus}; -use beefy_verifier_primitives::{ApkConsensusMessage, ApkConsensusState}; +use beefy_verifier_primitives::{ApkConsensusMessage, ConsensusState}; use ismp_abi::bls_beefy::BlsBeefy; use polkadot_sdk::*; use primitive_types::H256; @@ -62,7 +62,7 @@ fn apk_verifier_agrees_with_solidity() { // The state is one abi-encoded struct, the proof is the two the client's `verify` takes as // separate arguments, which is why they decode differently. - let trusted: ApkConsensusState = + let trusted: ConsensusState = ::abi_decode(&state_bytes) .expect("state decodes") .try_into() @@ -112,7 +112,7 @@ fn the_batched_pairing_enforces_the_signature() { let proof_bytes = decode_hex(include_str!( "../../../../../evm/tests/foundry/fixtures/bls-apk-beefy-proof.hex" )); - let trusted: ApkConsensusState = + let trusted: ConsensusState = ::abi_decode(&state_bytes) .expect("state decodes") .try_into() diff --git a/modules/ismp/clients/beefy/src/consensus.rs b/modules/ismp/clients/beefy/src/consensus.rs index 9872099af..5891214dd 100644 --- a/modules/ismp/clients/beefy/src/consensus.rs +++ b/modules/ismp/clients/beefy/src/consensus.rs @@ -107,14 +107,11 @@ where )? }, PROOF_TYPE_APK => { - let apk_state: beefy_verifier_primitives::ApkConsensusState = - codec::Decode::decode(&mut &trusted_consensus_state[..]) - .map_err(|e| BeefyError::DecodeConsensusState(format!("{e:?}")))?; let apk_proof: beefy_verifier_primitives::ApkConsensusMessage = codec::Decode::decode(&mut &payload[..]) .map_err(|e| BeefyError::DecodeApkProof(format!("{e:?}")))?; let (state, headers) = beefy_verifier::apk::verify_apk_consensus::( - apk_state, + decode_state()?, apk_proof, &C::apk_verifying_key(), C::apk_digest_para_id(), diff --git a/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs b/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs index 0d13cd25b..db03cb228 100644 --- a/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs +++ b/modules/pallets/beefy-consensus-proofs/src/benchmarking.rs @@ -138,7 +138,7 @@ mod benchmarks { let verifying_key = include_bytes!("../../../../evm/tests/foundry/fixtures/apk-verifying-key.bin").to_vec(); - let state: beefy_verifier_primitives::ApkConsensusState = + let state: beefy_verifier_primitives::ConsensusState = ::abi_decode( &state_bytes, ) diff --git a/modules/pallets/beefy-consensus-proofs/src/lib.rs b/modules/pallets/beefy-consensus-proofs/src/lib.rs index e94bb8153..1fcf12353 100644 --- a/modules/pallets/beefy-consensus-proofs/src/lib.rs +++ b/modules/pallets/beefy-consensus-proofs/src/lib.rs @@ -345,7 +345,7 @@ pub mod pallet { pub fn initialize_apk_state(origin: OriginFor, abi_state: Vec) -> DispatchResult { ::AdminOrigin::ensure_origin(origin)?; - let state: beefy_verifier_primitives::ApkConsensusState = + let state: beefy_verifier_primitives::ConsensusState = ::abi_decode( &abi_state, ) @@ -540,7 +540,7 @@ pub mod pallet { /// Only the ids are wanted here, and both shapes have them. fn authority_set_ids(state: &[u8], proof_type: u8) -> Result<(u64, u64), Error> { if proof_type == types::PROOF_TYPE_APK { - let state: beefy_verifier_primitives::ApkConsensusState = + let state: beefy_verifier_primitives::ConsensusState = Decode::decode(&mut &state[..]).map_err(|_| Error::::NotInitialized)?; Ok((state.current_authorities.id, state.next_authorities.id)) } else { diff --git a/modules/pallets/beefy-consensus-proofs/src/types.rs b/modules/pallets/beefy-consensus-proofs/src/types.rs index c936bf831..0695362eb 100644 --- a/modules/pallets/beefy-consensus-proofs/src/types.rs +++ b/modules/pallets/beefy-consensus-proofs/src/types.rs @@ -101,8 +101,8 @@ mod tests { /// cannot accept the rotation until it knows the incoming set's keys. pub fn next_commitment_unknown(state: &[u8], proof_type: u8) -> bool { proof_type == PROOF_TYPE_APK && - beefy_verifier_primitives::ApkConsensusState::decode(&mut &state[..]) - .map(|state| state.next_authorities.apk_commitment.is_zero()) + beefy_verifier_primitives::ConsensusState::decode(&mut &state[..]) + .map(|state| state.next_authorities.bls_poseidon_hash.is_zero()) .unwrap_or(false) } From f794cbcd3ba058cf661200483b9f6fee63139654 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Wed, 19 Aug 2026 00:14:57 +0100 Subject: [PATCH 44/48] measure the commitment on the benchmarking machine --- modules/pallets/beefy-apk-digest/src/lib.rs | 8 ++++---- .../gargantua/src/weights/pallet_beefy_apk_digest.rs | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/pallets/beefy-apk-digest/src/lib.rs b/modules/pallets/beefy-apk-digest/src/lib.rs index 1e64c7e7a..71c62abc8 100644 --- a/modules/pallets/beefy-apk-digest/src/lib.rs +++ b/modules/pallets/beefy-apk-digest/src/lib.rs @@ -237,12 +237,12 @@ pub trait WeightInfo { /// A rough default for tests and for a chain that has not generated its own. /// -/// A full set is around 800ms in wasm, of which roughly four fifths is decompressing the keys -/// rather than hashing them: a key arrives as 48 compressed bytes and recovering `y` needs a -/// square root in the base field. It is only paid when the membership actually changes. +/// A full set is around 580ms in wasm on the benchmarking machine, of which roughly four fifths is +/// decompressing the keys rather than hashing them: a key arrives as 48 compressed bytes and +/// recovering `y` needs a square root in the base field. It is only paid when the relay rotates. impl WeightInfo for () { fn commit() -> Weight { - Weight::from_parts(821_000_000_000, 0).saturating_add(Weight::from_parts(0, 4096)) + Weight::from_parts(588_000_000_000, 0).saturating_add(Weight::from_parts(0, 4096)) } } diff --git a/parachain/runtimes/gargantua/src/weights/pallet_beefy_apk_digest.rs b/parachain/runtimes/gargantua/src/weights/pallet_beefy_apk_digest.rs index f45e1bb2b..9985f9093 100644 --- a/parachain/runtimes/gargantua/src/weights/pallet_beefy_apk_digest.rs +++ b/parachain/runtimes/gargantua/src/weights/pallet_beefy_apk_digest.rs @@ -2,9 +2,9 @@ //! Autogenerated weights for `pallet_beefy_apk_digest` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 -//! DATE: 2026-08-17, STEPS: `2`, REPEAT: `10`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-08-18, STEPS: `2`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `Akinloses-MacBook-Pro.local`, CPU: `` +//! HOSTNAME: `polytope-labs`, CPU: `AMD Ryzen Threadripper PRO 5995WX 64-Cores` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -21,9 +21,9 @@ // --steps // 2 // --repeat -// 10 +// 20 // --output -// parachain/runtimes/gargantua/src/weights/pallet_beefy_apk_digest.rs +// /tmp/apk_digest_weights.rs #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -41,8 +41,8 @@ impl pallet_beefy_apk_digest::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 808_140_000_000 picoseconds. - Weight::from_parts(820_854_000_000, 0) + // Minimum execution time: 584_718_741_000 picoseconds. + Weight::from_parts(587_530_204_000, 0) .saturating_add(Weight::from_parts(0, 0)) } } From de872195143207577f751bb3625b15bfe5c03b5b Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Thu, 20 Aug 2026 13:07:14 +0100 Subject: [PATCH 45/48] let the apk circuit be compiled into the relayer by proving sp1 on a cluster The relayer links sp1's gnark ffi, and the apk circuit brings a go runtime of its own. Two of those in one process do not work: built by different go toolchains the circuit setup spins on a single core forever, and built by the same one the process dies on the first call into either, with the go allocator corrupted. sp1's archive is pulled in by sp1-beefy/local, so a build that proves sp1 on a cluster has room for ours. That cluster feature could never take effect before, because zk-beefy took sp1-beefy with its default features and sp1-beefy defaults to local, which put the archive back whatever was selected. The default is unchanged, sp1 proves locally and the apk prover runs as its own process. Building with sp1-cluster and local instead compiles the circuit in and uses ProverContext directly. A compile error refuses the pair that crashes. --- Cargo.toml | 4 +- tesseract/consensus/admin-relayer/Cargo.toml | 2 +- tesseract/consensus/beefy/Cargo.toml | 6 +- tesseract/consensus/beefy/src/host.rs | 18 +++--- tesseract/consensus/beefy/src/lib.rs | 17 +++++- tesseract/consensus/beefy/src/prover.rs | 60 +++++++++++++++---- .../consensus/beefy/tests/apk_messaging.rs | 8 +-- .../consensus/beefy/tests/mainnet_rotation.rs | 4 +- tesseract/consensus/beefy/zk/Cargo.toml | 2 + tesseract/consensus/beefy/zk/src/lib.rs | 51 ++++++++++++++-- tesseract/consensus/config/Cargo.toml | 10 +++- tesseract/consensus/config/src/lib.rs | 4 +- tesseract/consensus/relayer/Cargo.toml | 10 +++- tesseract/prover/Cargo.toml | 11 +++- tesseract/prover/src/main.rs | 2 +- tesseract/relayer/Cargo.toml | 6 ++ 16 files changed, 166 insertions(+), 49 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9b751f926..a3a880928 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -357,7 +357,7 @@ tesseract-config = { path = "tesseract/messaging/config" } arb-host = { path = "tesseract/consensus/arb-host" } op-host = { path = "tesseract/consensus/op-host" } evm-host = { path = "tesseract/consensus/evm-host" } -tesseract-beefy = { path = "tesseract/consensus/beefy" } +tesseract-beefy = { path = "tesseract/consensus/beefy", default-features = false } tesseract-bsc = { path = "tesseract/consensus/bsc" } tesseract-integration-tests = { path = "tesseract/consensus/integration-tests" } tesseract-sync-committee = { path = "tesseract/consensus/sync-committee" } @@ -367,7 +367,7 @@ tesseract-polygon = { path = "tesseract/consensus/polygon" } tesseract-tendermint = { path = "tesseract/consensus/tendermint" } tesseract-pharos = { path = "tesseract/consensus/pharos" } tesseract-parachain = { path = "tesseract/consensus/parachain" } -tesseract-consensus-config = { path = "tesseract/consensus/config" } +tesseract-consensus-config = { path = "tesseract/consensus/config", default-features = false } tesseract = { path = "tesseract/relayer" } [workspace.dependencies.rs_merkle] diff --git a/tesseract/consensus/admin-relayer/Cargo.toml b/tesseract/consensus/admin-relayer/Cargo.toml index fc3eb2f13..1eec6f685 100644 --- a/tesseract/consensus/admin-relayer/Cargo.toml +++ b/tesseract/consensus/admin-relayer/Cargo.toml @@ -39,7 +39,7 @@ ismp-abi = { workspace = true, default-features = true } beefy-verifier-primitives = { workspace = true } # tesseract -tesseract-beefy = { workspace = true } +tesseract-beefy = { workspace = true, features = ["sp1-local"] } tesseract-evm = { workspace = true } tesseract-primitives = { workspace = true } diff --git a/tesseract/consensus/beefy/Cargo.toml b/tesseract/consensus/beefy/Cargo.toml index 11957edc3..fe6c13d05 100644 --- a/tesseract/consensus/beefy/Cargo.toml +++ b/tesseract/consensus/beefy/Cargo.toml @@ -46,7 +46,7 @@ sp-consensus-beefy = { workspace = true } # tesseract tesseract-substrate = { workspace = true } tesseract-primitives = { workspace = true } -zk-beefy = { path = "zk" } +zk-beefy = { path = "zk", default-features = false } apk-beefy = { path = "apk", default-features = false } rsmq_async = { workspace = true } redis-async = { version = "0.17.1", features = ["with-rustls"] } @@ -56,6 +56,10 @@ workspace = true features = ["sp-runtime"] [features] +default = ["sp1-local"] +sp1-local = ["zk-beefy/local"] +sp1-cluster = ["zk-beefy/cluster"] +local = ["apk-beefy/local"] # a feature that tells the tests to write a new consensus state new-consensus-state = [] diff --git a/tesseract/consensus/beefy/src/host.rs b/tesseract/consensus/beefy/src/host.rs index dd744c500..7ef4481c4 100644 --- a/tesseract/consensus/beefy/src/host.rs +++ b/tesseract/consensus/beefy/src/host.rs @@ -112,7 +112,7 @@ where current_set_id: state.current_authorities.id, next_set_id: state.next_authorities.id, }; - let summarise_apk = |state: beefy_verifier_primitives::ApkConsensusState| StateSummary { + let summarise_apk = |state: beefy_verifier_primitives::ConsensusState| StateSummary { latest_beefy_height: state.latest_beefy_height, current_set_id: state.current_authorities.id, next_set_id: state.next_authorities.id, @@ -126,7 +126,7 @@ where encoded, ) .context("Could not abi-decode apk consensus state")?; - let state: beefy_verifier_primitives::ApkConsensusState = + let state: beefy_verifier_primitives::ConsensusState = state.try_into().map_err(|e| anyhow!("{e}"))?; Ok(summarise_apk(state)) }, @@ -136,7 +136,7 @@ where Ok(summarise(state.into())) }, (false, true) => { - let state = beefy_verifier_primitives::ApkConsensusState::decode(&mut &encoded[..]) + let state = beefy_verifier_primitives::ConsensusState::decode(&mut &encoded[..]) .context("Could not decode apk consensus state")?; Ok(summarise_apk(state)) }, @@ -434,15 +434,11 @@ where let commitment = apk.current_apk_commitment(at).await?; let inner = prover_state.inner.clone(); - let authority_set = |set: sp_consensus_beefy::mmr::BeefyAuthoritySet, - apk_commitment: H256| { - beefy_verifier_primitives::ApkAuthoritySet { - id: set.id, - len: set.len, - apk_commitment, - } + let authority_set = |set: beefy_verifier_primitives::AuthoritySet, + bls_poseidon_hash: H256| { + beefy_verifier_primitives::AuthoritySet { bls_poseidon_hash, ..set } }; - let state = beefy_verifier_primitives::ApkConsensusState { + let state = beefy_verifier_primitives::ConsensusState { latest_beefy_height: inner.latest_beefy_height, beefy_activation_block: inner.beefy_activation_block, mmr_root_hash: inner.mmr_root_hash, diff --git a/tesseract/consensus/beefy/src/lib.rs b/tesseract/consensus/beefy/src/lib.rs index 7f6a71465..6f8d9aeb2 100644 --- a/tesseract/consensus/beefy/src/lib.rs +++ b/tesseract/consensus/beefy/src/lib.rs @@ -12,6 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Both in process provers carry a go runtime, sp1's through gnark's ffi and ours through the apk +// circuit. Two of those in one process corrupt each other's allocator and the binary dies on the +// first call into either, so the combination is refused here rather than at runtime. Prove sp1 on +// a cluster to compile the apk circuit in, or leave the apk circuit in its own process. +#[cfg(all(feature = "local", feature = "sp1-local"))] +compile_error!( + "`local` compiles the apk circuit into this binary, which cannot be done alongside `sp1-local`. \ + Build with `--no-default-features --features sp1-cluster,local`, or drop `local` and let the \ + apk prover run as its own process." +); + /// Log/tracing target for this crate. pub const LOG_TARGET: &str = "consensus-beefy"; @@ -57,7 +68,7 @@ impl BeefyConfig { /// [`BeefyProverConfig::backend`](prover::BeefyProverConfig::backend). pub async fn into_client( self, - ) -> Result, anyhow::Error> + ) -> Result, anyhow::Error> where R: subxt::Config + Send + Sync + Clone, P: subxt::Config> + Send + Sync + Clone, @@ -74,7 +85,7 @@ impl BeefyConfig { .map_err(|_| anyhow!("beefy submission signer account must be 32 bytes"))? .into(); let prover = - Prover::::new(self.prover.clone(), account).await?; + Prover::::new(self.prover.clone(), account).await?; let backend: Arc = match self.prover_config.backend.clone() { backend::ProofBackendConfig::Redis { config } => { @@ -98,7 +109,7 @@ impl BeefyConfig { }, }; - BeefyHost::::new( + BeefyHost::::new( self.host, prover, client, backend, ) .await diff --git a/tesseract/consensus/beefy/src/prover.rs b/tesseract/consensus/beefy/src/prover.rs index 04ac632a1..19ea3834a 100644 --- a/tesseract/consensus/beefy/src/prover.rs +++ b/tesseract/consensus/beefy/src/prover.rs @@ -142,6 +142,10 @@ pub struct ProverConfig { /// falls back to the prover's own default. #[serde(default)] pub apk_srs_dir: Option, + /// Where sp1 proving happens when this build proves on a cluster. Ignored by a build that + /// proves sp1 locally, and required by one that does not. + #[serde(default)] + pub sp1_cluster: Option, } /// The BEEFY prover produces BEEFY consensus proofs using either the naive or zk variety. Consensus @@ -492,7 +496,7 @@ where // of the session instead, which does name the incoming set, and the // rotation goes through on the next tick. if matches!(self.prover, Prover::Apk(_)) && - consensus_state.inner.next_authorities.keyset_commitment.is_zero() + consensus_state.inner.next_authorities.bls_poseidon_hash.is_zero() { let epoch_change_number: u64 = epoch_change_header.number().into(); let from = u64::from(consensus_state.inner.latest_beefy_height) + 1; @@ -587,12 +591,20 @@ where commitment.commitment.block_number; consensus_state.inner.current_authorities = consensus_state.inner.next_authorities.clone(); + let incoming = beefy_prover::relay::beefy_mmr_leaf_next_authorities( + &self.prover.inner().relay_rpc, + Some(epoch_change_block_hash), + ) + .await?; + // The relay names the ecdsa root; the poseidon hash reaches a client + // through a header digest, so it starts empty here. consensus_state.inner.next_authorities = - beefy_prover::relay::beefy_mmr_leaf_next_authorities( - &self.prover.inner().relay_rpc, - Some(epoch_change_block_hash), - ) - .await?; + beefy_verifier_primitives::AuthoritySet { + id: incoming.id, + len: incoming.len, + bls_poseidon_hash: H256::zero(), + ecdsa_merkle_root: incoming.keyset_commitment, + }; tracing::info!( target: crate::LOG_TARGET, "Rotated authority set. Current {}, Next: {}", consensus_state.inner.current_authorities.id, @@ -741,7 +753,7 @@ where } // Implementation for LocalProver -impl Prover +impl Prover where R: subxt::Config, P: subxt::Config, @@ -790,7 +802,7 @@ where let prover = match config.proof_variant { ProofVariant::Sp1 => { - let sp1_prover = zk_beefy::LocalProver::new().await?; + let sp1_prover = zk_beefy::default_prover(config.sp1_cluster.clone()).await?; Prover::Sp1(zk_beefy::Prover::new(prover, sp1_prover, account)) }, ProofVariant::Ecdsa => Prover::Ecdsa(prover, PhantomData), @@ -799,6 +811,22 @@ where .apk_prover_binary .clone() .ok_or_else(|| anyhow!("`apk_prover_binary` is required by the apk variant"))?; + #[cfg(feature = "local")] + let apk_prover: Arc = { + let _ = &binary; + // The circuit is compiled into this process, so there is no binary to talk to + // and the setup happens here rather than in a child. Only a build that proves + // sp1 on a cluster gets this far, since the go runtime the circuit brings + // cannot share a process with the one sp1's gnark ffi links: see the guard in + // this crate's lib.rs. Setup takes a couple of minutes and saturates every + // core, so it stays off the runtime's workers. + let srs_dir = config.apk_srs_dir.clone(); + let local = std::thread::spawn(move || apk_beefy::LocalProver::new(srs_dir)) + .join() + .map_err(|_| anyhow!("apk circuit setup panicked"))??; + Arc::new(local) + }; + #[cfg(not(feature = "local"))] let apk_prover: Arc = if config.apk_prover_one_shot { let work_dir = config .apk_prover_dir @@ -872,8 +900,20 @@ where mmr_root_hash, beefy_activation_block: inner.beefy_activation_block, latest_beefy_height: signed_commitment.commitment.block_number, - current_authorities: current_authority_set.clone(), - next_authorities: next_authority_set.clone(), + // The relay names only the ecdsa root. A client picks the poseidon hash up from a + // hyperbridge header digest, so it starts empty here. + current_authorities: beefy_verifier_primitives::AuthoritySet { + id: current_authority_set.id, + len: current_authority_set.len, + bls_poseidon_hash: H256::zero(), + ecdsa_merkle_root: current_authority_set.keyset_commitment, + }, + next_authorities: beefy_verifier_primitives::AuthoritySet { + id: next_authority_set.id, + len: next_authority_set.len, + bls_poseidon_hash: H256::zero(), + ecdsa_merkle_root: next_authority_set.keyset_commitment, + }, }; Ok(ProverConsensusState { diff --git a/tesseract/consensus/beefy/tests/apk_messaging.rs b/tesseract/consensus/beefy/tests/apk_messaging.rs index ec31658df..258add8a8 100644 --- a/tesseract/consensus/beefy/tests/apk_messaging.rs +++ b/tesseract/consensus/beefy/tests/apk_messaging.rs @@ -200,12 +200,12 @@ async fn a_messaging_proof_is_accepted_as_new_work() -> Result<(), anyhow::Error apk_prover_one_shot: false, apk_srs_dir: None, }; - let prover: Prover = + let prover: Prover = Prover::new(prover_config, Default::default()).await?; let beefy = BeefyProver::< Blake2SubstrateChain, KeccakSubstrateChain, - zk_beefy::LocalProver, + zk_beefy::DefaultProver, dyn ProofBackend, >::new( BeefyProverConfig { @@ -244,8 +244,8 @@ async fn a_messaging_proof_is_accepted_as_new_work() -> Result<(), anyhow::Error "the proof rotated the authority set, so it was not accepted for its messages", ); assert_eq!( - after.inner.next_authorities.keyset_commitment, - before.inner.next_authorities.keyset_commitment, + after.inner.next_authorities.bls_poseidon_hash, + before.inner.next_authorities.bls_poseidon_hash, "the proof taught a commitment, so that is what it could have been accepted for", ); assert!( diff --git a/tesseract/consensus/beefy/tests/mainnet_rotation.rs b/tesseract/consensus/beefy/tests/mainnet_rotation.rs index 902fdef39..d15e6e60a 100644 --- a/tesseract/consensus/beefy/tests/mainnet_rotation.rs +++ b/tesseract/consensus/beefy/tests/mainnet_rotation.rs @@ -207,7 +207,7 @@ async fn rotate_authorities_across_all_chains() -> anyhow::Result<()> { }; // ECDSA proof submitted to the EVM handler, which does not enforce the SP1 committed- // nonce binding, so a zero account is fine here. - let prover: Prover = + let prover: Prover = Prover::new(prover_config, Default::default()).await?; let substrate = SubstrateClient::::new( @@ -241,7 +241,7 @@ async fn rotate_authorities_across_all_chains() -> anyhow::Result<()> { backend: Default::default(), }; - let beefy = BeefyProver::::new( + let beefy = BeefyProver::::new( beefy_config, substrate, prover, diff --git a/tesseract/consensus/beefy/zk/Cargo.toml b/tesseract/consensus/beefy/zk/Cargo.toml index 2d9df3f03..5a1ba9666 100644 --- a/tesseract/consensus/beefy/zk/Cargo.toml +++ b/tesseract/consensus/beefy/zk/Cargo.toml @@ -6,6 +6,7 @@ authors = ["Polytope Labs "] description = "SNARK Circuits for BEEFY consensus proofs written in noir" [dependencies] +serde = { workspace = true, features = ["derive"] } tracing = { workspace = true } tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread"] } rs_merkle = { workspace = true, default-features = true } @@ -27,6 +28,7 @@ hex-literal = "0.4.1" [dependencies.sp1-beefy] git = "https://github.com/polytope-labs/sp1-beefy" tag = "v1.1.0" +default-features = false [dependencies.sp1-beefy-primitives] git = "https://github.com/polytope-labs/sp1-beefy" diff --git a/tesseract/consensus/beefy/zk/src/lib.rs b/tesseract/consensus/beefy/zk/src/lib.rs index e74a0c6e1..028a14af7 100644 --- a/tesseract/consensus/beefy/zk/src/lib.rs +++ b/tesseract/consensus/beefy/zk/src/lib.rs @@ -27,6 +27,45 @@ pub use sp1_beefy::local::LocalProver; #[cfg(test)] mod tests; +/// Which sp1 backend this build proves with. +/// +/// Proving locally links gnark's go runtime into the binary. A process can only hold one of +/// those, and the apk circuit brings its own, so the two in process provers cannot be built +/// together: see the guard in `tesseract-beefy`. Proving on a cluster leaves the go runtime out +/// altogether, which is what makes room for the apk one. +#[cfg(feature = "local")] +pub type DefaultProver = LocalProver; + +/// See [`DefaultProver`]. +#[cfg(all(feature = "cluster", not(feature = "local")))] +pub type DefaultProver = ClusterProver; + +/// Where a cluster build sends its proofs. Unused when proving locally. +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct ClusterConfig { + /// The cluster's grpc api service. + pub rpc: String, + /// Redis, which the cluster reads its artifacts from. + pub redis: String, +} + +/// Builds whichever backend this build was compiled for. +/// +/// Keeping the two constructors behind one call means the callers stay free of feature flags, +/// since only the cluster backend needs to be told where to reach anything. +#[cfg(feature = "local")] +pub async fn default_prover(_cluster: Option) -> Result { + LocalProver::new().await +} + +/// See [`default_prover`]. +#[cfg(all(feature = "cluster", not(feature = "local")))] +pub async fn default_prover(cluster: Option) -> Result { + let cluster = cluster + .ok_or_else(|| anyhow!("`sp1_cluster` is required when sp1 proves on a cluster"))?; + ClusterProver::new(cluster.rpc, cluster.redis).await +} + /// Consensus prover for zk BEEFY. pub struct Prover { pub inner: beefy_prover::Prover, @@ -108,18 +147,18 @@ where let tree = MerkleTree::::from_leaves(&leaf_hashes); // Sanity check: the merkle root of the actual on-chain authorities must equal the - // `keyset_commitment` of the set selected by the commitment's `validator_set_id`. The - // guest verifies authority membership against `authority.keyset_commitment`, so if this + // ecdsa merkle root of the set selected by the commitment's `validator_set_id`. The + // guest verifies authority membership against `authority.ecdsa_merkle_root`, so if this // invariant is broken the proof would fail on-chain. A mismatch here means either the // validator-set selection is wrong or `hash_authority_addresses` has diverged from // `pallet-beefy-mmr`'s eth-address commitment (see `FAILED_BEEFY_TO_ETH_ADDRESS`). let computed_root = tree.root().ok_or_else(|| anyhow!("empty authority set"))?; - if computed_root != authority.keyset_commitment.0 { + if computed_root != authority.ecdsa_merkle_root.0 { Err(anyhow!( - "authority root mismatch for validator set {}: computed 0x{} != keyset_commitment 0x{}", + "authority root mismatch for validator set {}: computed 0x{} != ecdsa merkle root 0x{}", authority.id, hex::encode(computed_root), - hex::encode(authority.keyset_commitment.0), + hex::encode(authority.ecdsa_merkle_root.0), ))? } @@ -153,7 +192,7 @@ where authorities: AuthoritiesProof { len: authority.len, proof: authorities_witness, - root: authority.keyset_commitment.0.into(), + root: authority.ecdsa_merkle_root.0.into(), votes: message .mmr .signed_commitment diff --git a/tesseract/consensus/config/Cargo.toml b/tesseract/consensus/config/Cargo.toml index 802993aae..6fab26277 100644 --- a/tesseract/consensus/config/Cargo.toml +++ b/tesseract/consensus/config/Cargo.toml @@ -22,7 +22,7 @@ arb-host = { workspace = true } evm-host = { path = "../evm-host" } op-host = { workspace = true } pharos-primitives = { workspace = true, default-features = true } -tesseract-beefy = { path = "../beefy" } +tesseract-beefy = { path = "../beefy", default-features = false } tesseract-bsc = { workspace = true } tesseract-evm = { workspace = true } tesseract-grandpa = { workspace = true } @@ -34,8 +34,14 @@ tesseract-substrate = { workspace = true } tesseract-substrate-evm = { workspace = true } tesseract-sync-committee = { workspace = true } tesseract-tendermint = { workspace = true } -zk-beefy = { path = "../beefy/zk" } +zk-beefy = { path = "../beefy/zk", default-features = false } [dependencies.polkadot-sdk] workspace = true features = ["sp-runtime"] + +[features] +default = ["sp1-local"] +sp1-local = ["tesseract-beefy/sp1-local", "zk-beefy/local"] +sp1-cluster = ["tesseract-beefy/sp1-cluster", "zk-beefy/cluster"] +local = ["tesseract-beefy/local"] diff --git a/tesseract/consensus/config/src/lib.rs b/tesseract/consensus/config/src/lib.rs index 9643cfcd3..58c2a6bcf 100644 --- a/tesseract/consensus/config/src/lib.rs +++ b/tesseract/consensus/config/src/lib.rs @@ -112,7 +112,7 @@ pub enum AnyConfig { } pub enum AnyHost { - Beefy(BeefyHost), + Beefy(BeefyHost), Grandpa(GrandpaHost), } @@ -245,7 +245,7 @@ impl HyperbridgeHostConfig { .map_err(|_| anyhow!("beefy submission signer account must be 32 bytes"))? .into(); let prover_instance = - Prover::::new(prover.clone(), account).await?; + Prover::::new(prover.clone(), account).await?; let backend = Arc::new(tesseract_beefy::backend::RedisProofBackend::new(redis).await?); diff --git a/tesseract/consensus/relayer/Cargo.toml b/tesseract/consensus/relayer/Cargo.toml index 8800a40b1..7ff515ace 100644 --- a/tesseract/consensus/relayer/Cargo.toml +++ b/tesseract/consensus/relayer/Cargo.toml @@ -33,9 +33,9 @@ subxt = { workspace = true, default-features = false } sp-core = { workspace = true, features = ["full_crypto"] } subxt-utils = { workspace = true } -tesseract-beefy = { path = "../beefy" } +tesseract-beefy = { path = "../beefy", default-features = false } tesseract-consensus-config = { workspace = true } -zk-beefy = { path = "../beefy/zk" } +zk-beefy = { path = "../beefy/zk", default-features = false } tesseract-bsc = { workspace = true } tesseract-evm = { workspace = true } tesseract-sync-committee = { workspace = true } @@ -56,3 +56,9 @@ pharos-primitives = { workspace = true } [dependencies.polkadot-sdk] workspace = true features = ["sp-runtime", "sc-service"] + +[features] +default = ["sp1-local"] +sp1-local = ["tesseract-beefy/sp1-local", "zk-beefy/local"] +sp1-cluster = ["tesseract-beefy/sp1-cluster", "zk-beefy/cluster"] +local = ["tesseract-beefy/local"] diff --git a/tesseract/prover/Cargo.toml b/tesseract/prover/Cargo.toml index 1f0cb1e72..b1ac1b808 100644 --- a/tesseract/prover/Cargo.toml +++ b/tesseract/prover/Cargo.toml @@ -19,8 +19,15 @@ rustls = { version = "0.23.23", features = ["ring"] } primitive-types = { workspace = true } subxt-utils = { workspace = true } -tesseract-beefy = { workspace = true } +tesseract-beefy = { workspace = true, default-features = false } tesseract-substrate = { workspace = true } tesseract-primitives = { workspace = true } -zk-beefy = { path = "../consensus/beefy/zk" } +zk-beefy = { path = "../consensus/beefy/zk", default-features = false } + +[features] +default = ["sp1-local"] +sp1-local = ["tesseract-beefy/sp1-local", "zk-beefy/local"] +sp1-cluster = ["tesseract-beefy/sp1-cluster", "zk-beefy/cluster"] +# Compile the apk circuit into this binary rather than talking to a prover process. +local = ["tesseract-beefy/local"] diff --git a/tesseract/prover/src/main.rs b/tesseract/prover/src/main.rs index 537ff64bf..9b0a860a6 100644 --- a/tesseract/prover/src/main.rs +++ b/tesseract/prover/src/main.rs @@ -99,7 +99,7 @@ async fn main() -> Result<(), anyhow::Error> { BeefyProver::< Blake2SubstrateChain, KeccakSubstrateChain, - zk_beefy::LocalProver, + zk_beefy::DefaultProver, dyn tesseract_beefy::backend::ProofBackend, >::new(beefy_config, substrate, prover, backend) .await? diff --git a/tesseract/relayer/Cargo.toml b/tesseract/relayer/Cargo.toml index 98af803e4..476a1b60b 100644 --- a/tesseract/relayer/Cargo.toml +++ b/tesseract/relayer/Cargo.toml @@ -50,3 +50,9 @@ features = ["sc-service"] [dev-dependencies] tesseract-primitives = { workspace = true, features = ["testing"] } tempfile = "3.10" + +[features] +default = ["sp1-local"] +sp1-local = ["tesseract-beefy/sp1-local", "tesseract-consensus-config/sp1-local"] +sp1-cluster = ["tesseract-beefy/sp1-cluster", "tesseract-consensus-config/sp1-cluster"] +local = ["tesseract-beefy/local", "tesseract-consensus-config/local"] From b978fc1bb0dd3cf26f09e052824fd47d968fdcf0 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 21 Aug 2026 22:57:31 +0100 Subject: [PATCH 46/48] rotate the apk client from the first justification inside the new session --- tesseract/consensus/beefy/src/prover.rs | 180 ++++++++++++++++++++---- 1 file changed, 153 insertions(+), 27 deletions(-) diff --git a/tesseract/consensus/beefy/src/prover.rs b/tesseract/consensus/beefy/src/prover.rs index 19ea3834a..103633d20 100644 --- a/tesseract/consensus/beefy/src/prover.rs +++ b/tesseract/consensus/beefy/src/prover.rs @@ -126,8 +126,8 @@ pub struct ProverConfig { pub max_rpc_payload_size: Option, /// Query batch size for mmr leaves pub query_batch_size: Option, - /// The `gnark-apk-proofs` prover binary. Only read by the `Apk` variant, which cannot link the - /// prover directly, see `apk_beefy::command`. + /// The `gnark-apk-proofs` prover binary, for a build that has to shell out to it. Ignored by + /// one that compiles the circuit in and proves through it directly, see `apk_beefy::local`. #[serde(default)] pub apk_prover_binary: Option, /// Where a one shot prover exchanges its input and proof files. Ignored when the prover is @@ -363,12 +363,16 @@ where /// still name the set before. Taking the earliest one that does name it, rather than the last /// block of the session, leaves the rest of the session provable, which is what a proof for /// new messages needs. + /// + /// `trusted` is the client's own state, and it bounds the walk: only the two sets it holds can + /// sign anything it will accept. pub async fn teaching_justification( &self, from: u64, until: u64, set_id: u64, para_id: u32, + trusted: &ConsensusState, ) -> anyhow::Result>> { let relay_rpc = self.prover.inner().relay_rpc.clone(); let mut cursor = from; @@ -382,6 +386,15 @@ where } cursor = number + 1; + // Past the sets the client holds is past what it can verify, and every later + // justification is signed by a set further ahead still. Stopping here reports a client + // that has fallen behind its own window as such, rather than handing the proof builder + // a set it will refuse. + let signer = commitment.commitment.validator_set_id; + if signer != trusted.current_authorities.id && signer != trusted.next_authorities.id { + return Ok(None); + } + let Some(hash) = relay_rpc.chain_get_block_hash(Some(number.into())).await? else { continue; }; @@ -493,8 +506,8 @@ where // digest in a parachain header, and the header this rotation carries sits // on the session boundary, where the relay's next set is only just being // queued. Proving finality one block earlier carries a header from the end - // of the session instead, which does name the incoming set, and the - // rotation goes through on the next tick. + // of the session instead, which does name the incoming set, so the client + // holds its commitment by the time the rotation below is proven. if matches!(self.prover, Prover::Apk(_)) && consensus_state.inner.next_authorities.bls_poseidon_hash.is_zero() { @@ -506,6 +519,7 @@ where epoch_change_number, next_set_id, para_id, + &consensus_state.inner, ) .await? else { @@ -553,6 +567,116 @@ where return Ok(()); } + // The rotation arrives through a digest too. A set is only entered when a + // header names the set after it, so the boundary justification cannot + // carry one: its header still names the set being entered, which the + // client already holds. The first justification inside the new session is + // the one that names the set after, and it is signed by the incoming set, + // whose commitment the client learned above. Proving that block rotates + // the client and teaches the following commitment at once, which is why + // steady state costs one proof a session rather than two. + if matches!(self.prover, Prover::Apk(_)) { + let from = u64::from(consensus_state.inner.latest_beefy_height) + 1; + let latest = u64::from(*latest_beefy_header.number()); + let following = next_set_id.saturating_add(1); + let Some(commitment) = self + .teaching_justification( + from, + latest + 1, + following, + para_id, + &consensus_state.inner, + ) + .await? + else { + // Either the parachain has not published the commitment yet, since + // it takes a block or two into the session that queues it, or the + // relay has left the only session this client could still verify, + // in which case it stays here until it is re-seeded. + tracing::info!( + target: crate::LOG_TARGET, + "No justification signed by {} or {} in {from}..{latest} names {following}", + consensus_state.inner.current_authorities.id, + next_set_id, + ); + return Ok(()); + }; + + let finalized_hash = relay_rpc + .chain_get_block_hash(Some( + commitment.commitment.block_number.into(), + )) + .await? + .ok_or_else(|| { + anyhow!( + "no block at {} on the relay", + commitment.commitment.block_number + ) + })?; + let para_header = + query_parachain_header(&relay_rpc, finalized_hash, para_id).await?; + let digest = ApkCommitmentDigest::find_in(¶_header.digest) + .ok_or_else(|| { + anyhow!( + "parachain header at {} lost its apk digest", + commitment.commitment.block_number + ) + })?; + + let consensus_proof = self + .consensus_proof( + commitment.clone(), + consensus_state.inner.clone(), + ) + .await?; + let message = ConsensusProof { + finalized_height: commitment.commitment.block_number, + set_id: next_set_id, + message: ConsensusMessage { + consensus_proof, + consensus_state_id: self.config.consensus_state_id, + signer: H256::random().encode(), + }, + }; + + tracing::info!( + target: crate::LOG_TARGET, + "Proving finality at {} to rotate into {next_set_id}", + commitment.commitment.block_number, + ); + let destinations: Vec = + self.config.state_machines.clone(); + self.backend.send_mandatory_proof(&destinations, message).await?; + + // Mirror what the digest does to the client: the set being entered + // becomes current, and the set the digest names becomes next, already + // carrying the commitment the same header supplied. + let incoming = beefy_prover::relay::beefy_mmr_leaf_next_authorities( + &self.prover.inner().relay_rpc, + Some(finalized_hash), + ) + .await?; + consensus_state.finalized_parachain_height = para_header.number.into(); + consensus_state.inner.latest_beefy_height = + commitment.commitment.block_number; + consensus_state.inner.current_authorities = + consensus_state.inner.next_authorities.clone(); + consensus_state.inner.next_authorities = + beefy_verifier_primitives::AuthoritySet { + id: digest.set_id, + len: digest.len, + bls_poseidon_hash: H256(digest.commitment), + ecdsa_merkle_root: incoming.keyset_commitment, + }; + tracing::info!( + target: crate::LOG_TARGET, "Rotated authority set. Current {}, Next: {}", + consensus_state.inner.current_authorities.id, + consensus_state.inner.next_authorities.id, + ); + self.backend.save_state(&consensus_state).await?; + return Ok(()); + } + let consensus_proof = self .consensus_proof(commitment.clone(), consensus_state.inner.clone()) .await?; @@ -807,19 +931,14 @@ where }, ProofVariant::Ecdsa => Prover::Ecdsa(prover, PhantomData), ProofVariant::Apk => { - let binary = config - .apk_prover_binary - .clone() - .ok_or_else(|| anyhow!("`apk_prover_binary` is required by the apk variant"))?; + // The circuit is compiled into this process, so there is no binary to talk to and + // the setup happens here rather than in a child. Only a build that proves sp1 on + // a cluster gets this far, since the go runtime the circuit brings cannot share a + // process with the one sp1's gnark ffi links: see the guard in this crate's + // lib.rs. Setup takes a couple of minutes and saturates every core, so it stays + // off the runtime's workers. #[cfg(feature = "local")] let apk_prover: Arc = { - let _ = &binary; - // The circuit is compiled into this process, so there is no binary to talk to - // and the setup happens here rather than in a child. Only a build that proves - // sp1 on a cluster gets this far, since the go runtime the circuit brings - // cannot share a process with the one sp1's gnark ffi links: see the guard in - // this crate's lib.rs. Setup takes a couple of minutes and saturates every - // core, so it stays off the runtime's workers. let srs_dir = config.apk_srs_dir.clone(); let local = std::thread::spawn(move || apk_beefy::LocalProver::new(srs_dir)) .join() @@ -827,18 +946,25 @@ where Arc::new(local) }; #[cfg(not(feature = "local"))] - let apk_prover: Arc = if config.apk_prover_one_shot { - let work_dir = config - .apk_prover_dir - .clone() - .unwrap_or_else(|| binary.with_file_name("apk-prover-work")); - Arc::new(apk_beefy::CommandProver::new(binary, work_dir)?) - } else { - // Setup runs here, before any proving, so the first proof is not four minutes - // slower than the rest. - Arc::new( - apk_beefy::ServiceProver::new(binary, config.apk_srs_dir.clone()).await?, - ) + let apk_prover: Arc = { + let binary = + config.apk_prover_binary.clone().ok_or_else(|| { + anyhow!("`apk_prover_binary` is required by a build that proves sp1 locally") + })?; + if config.apk_prover_one_shot { + let work_dir = config + .apk_prover_dir + .clone() + .unwrap_or_else(|| binary.with_file_name("apk-prover-work")); + Arc::new(apk_beefy::CommandProver::new(binary, work_dir)?) + } else { + // Setup runs here, before any proving, so the first proof is not four + // minutes slower than the rest. + Arc::new( + apk_beefy::ServiceProver::new(binary, config.apk_srs_dir.clone()) + .await?, + ) + } }; Prover::Apk(apk_beefy::Prover::new(prover, apk_prover)) }, From 6fb9d76bb2bb7d09ebbb3eafc1e1278339d3e28b Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Fri, 21 Aug 2026 23:57:59 +0100 Subject: [PATCH 47/48] prove apk through the circuit compiled into the binary and delete the subprocess provers --- tesseract/consensus/beefy/apk/src/command.rs | 123 -------------- tesseract/consensus/beefy/apk/src/lib.rs | 10 +- tesseract/consensus/beefy/apk/src/service.rs | 160 ------------------ .../beefy/apk/tests/command_prover.rs | 104 ------------ .../consensus/beefy/apk/tests/live_prover.rs | 73 +++++--- tesseract/consensus/beefy/src/prover.rs | 62 ++----- .../consensus/beefy/tests/apk_messaging.rs | 12 +- .../consensus/beefy/tests/mainnet_rotation.rs | 85 +++++++--- 8 files changed, 126 insertions(+), 503 deletions(-) delete mode 100644 tesseract/consensus/beefy/apk/src/command.rs delete mode 100644 tesseract/consensus/beefy/apk/src/service.rs delete mode 100644 tesseract/consensus/beefy/apk/tests/command_prover.rs diff --git a/tesseract/consensus/beefy/apk/src/command.rs b/tesseract/consensus/beefy/apk/src/command.rs deleted file mode 100644 index 0374e6ad8..000000000 --- a/tesseract/consensus/beefy/apk/src/command.rs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (C) Polytope Labs Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Proving by running `gnark-apk-proofs` as a separate process. -//! -//! Linking the prover into this binary is the obvious thing and it does not work: the circuit -//! setup wedges inside the relayer's process while the identical call completes in a plain binary, -//! in a tokio runtime, and in a tokio runtime with forty live threads. Rather than keep hunting, -//! the prover runs where it is known to work and the two sides meet over json. -//! -//! It also keeps cgo, a Go toolchain and an 800MB reference string out of this build entirely. - -use std::path::PathBuf; - -use alloy_primitives::U256; -use anyhow::{anyhow, Context}; -use ark_ec::AffineRepr; -use ark_ff::{BigInteger, PrimeField}; -use tokio::process::Command; - -use crate::{decompress_g1, ApkProof, ApkProofRequest, ApkProver, BITLIST_WORDS}; - -/// Runs the prover binary, handing it a directory to read inputs from and write the proof to. -/// -/// The binary is expected to read `apk-inputs.json` and write `apk-snark.json`, which is what -/// `gnark-apk-proofs`' `prove_from_json` does. -pub struct CommandProver { - /// The prover executable. - binary: PathBuf, - /// Where the two json files live. Each proof overwrites them, so this is not shared. - work_dir: PathBuf, -} - -impl CommandProver { - /// Build a prover that shells out to `binary`, exchanging files under `work_dir`. - pub fn new(binary: PathBuf, work_dir: PathBuf) -> Result { - std::fs::create_dir_all(&work_dir) - .with_context(|| format!("could not create the prover work directory {work_dir:?}"))?; - Ok(Self { binary, work_dir }) - } -} - -#[async_trait::async_trait] -impl ApkProver for CommandProver { - async fn prove(&self, request: ApkProofRequest) -> Result { - // The circuit takes uncompressed coordinates, so the compressed keys are opened up here - // rather than teaching the prover binary about our encoding. - let keys = request - .keys - .iter() - .map(|key| { - let point = decompress_g1(key)?; - let (x, y) = point.xy().ok_or_else(|| anyhow!("authority key is the identity"))?; - let mut packed = Vec::with_capacity(96); - packed.extend_from_slice(&x.into_bigint().to_bytes_be()); - packed.extend_from_slice(&y.into_bigint().to_bytes_be()); - Ok(hex::encode(packed)) - }) - .collect::, anyhow::Error>>()?; - - let inputs = json::json!({ - "keys": keys, - "participation": request.participation, - }); - let input_path = self.work_dir.join("apk-inputs.json"); - let output_path = self.work_dir.join("apk-snark.json"); - tokio::fs::write(&input_path, json::to_vec(&inputs)?) - .await - .with_context(|| format!("could not write {input_path:?}"))?; - // Left over output would be read back as this proof's if the prover failed silently. - let _ = tokio::fs::remove_file(&output_path).await; - - let status = Command::new(&self.binary) - .arg(&self.work_dir) - .status() - .await - .with_context(|| format!("could not run the prover at {:?}", self.binary))?; - if !status.success() { - Err(anyhow!("the prover exited with {status}"))? - } - - let output: json::Value = json::from_slice( - &tokio::fs::read(&output_path) - .await - .with_context(|| format!("the prover wrote no proof to {output_path:?}"))?, - )?; - - let hex_field = |name: &str| -> Result, anyhow::Error> { - let raw = output[name].as_str().ok_or_else(|| anyhow!("proof has no {name}"))?; - Ok(hex::decode(raw.trim_start_matches("0x"))?) - }; - - let bitlist: [U256; BITLIST_WORDS] = output["bitlist"] - .as_array() - .ok_or_else(|| anyhow!("proof has no bitlist"))? - .iter() - .map(|word| { - let raw = word.as_str().ok_or_else(|| anyhow!("bitlist word is not a string"))?; - Ok(U256::from_be_slice(&hex::decode(raw.trim_start_matches("0x"))?)) - }) - .collect::, anyhow::Error>>()? - .try_into() - .map_err(|_| anyhow!("bitlist is not {BITLIST_WORDS} words"))?; - - let commitment = hex_field("apkCommitment")?; - let apk_commitment = <[u8; 32]>::try_from(commitment.as_slice()) - .map_err(|_| anyhow!("apk commitment is not 32 bytes"))?; - - Ok(ApkProof { proof: hex_field("apkProof")?, bitlist, apk_commitment }) - } -} diff --git a/tesseract/consensus/beefy/apk/src/lib.rs b/tesseract/consensus/beefy/apk/src/lib.rs index 9460643c0..4e3eec878 100644 --- a/tesseract/consensus/beefy/apk/src/lib.rs +++ b/tesseract/consensus/beefy/apk/src/lib.rs @@ -20,8 +20,8 @@ //! SNARK that ties the aggregate to the authority set. //! //! Generating that SNARK needs a Go toolchain through cgo and a large structured reference string, -//! so it sits behind [`ApkProver`] rather than being called directly. Only the binary that -//! actually proves has to take those on. +//! so it sits behind [`ApkProver`] and the `local` feature. A build without the feature assembles +//! everything here except the SNARK, and has no way to produce one. use alloy_primitives::{Bytes, FixedBytes, U256}; use anyhow::anyhow; @@ -40,12 +40,6 @@ use beefy_prover::bls::{ use beefy_verifier_primitives::{ConsensusState, BLS_G1_SIGNATURE_LEN}; use ismp_abi::bls_beefy::BlsBeefy; -mod command; -pub use command::CommandProver; - -mod service; -pub use service::ServiceProver; - #[cfg(feature = "local")] mod local; #[cfg(feature = "local")] diff --git a/tesseract/consensus/beefy/apk/src/service.rs b/tesseract/consensus/beefy/apk/src/service.rs deleted file mode 100644 index ec3fdb408..000000000 --- a/tesseract/consensus/beefy/apk/src/service.rs +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (C) Polytope Labs Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Proving through a prover kept alive between proofs. -//! -//! [`crate::CommandProver`] runs the prover afresh for every proof, and the prover compiles the -//! circuit and generates its keys before it can do anything, about four minutes. Paid once that is -//! setup; paid per proof it is most of the wall clock. This keeps one process alive and talks to it -//! over its stdin and stdout, so the four minutes happen at startup and a proof costs only the two -//! minutes it actually takes. - -use std::{path::PathBuf, process::Stdio}; - -use alloy_primitives::U256; -use anyhow::{anyhow, Context}; -use ark_ec::AffineRepr; -use ark_ff::{BigInteger, PrimeField}; -use tokio::{ - io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - process::{Child, ChildStdin, ChildStdout, Command}, - sync::Mutex, -}; - -use crate::{decompress_g1, ApkProof, ApkProofRequest, ApkProver, BITLIST_WORDS}; - -/// Read until the prover says something in json. -/// -/// It shares stdout with the go library underneath it, which logs its progress there, so anything -/// that is not json is that library talking rather than a reply to us. -async fn read_json_line(stdout: &mut BufReader) -> Result { - loop { - let mut line = String::new(); - if stdout.read_line(&mut line).await? == 0 { - Err(anyhow!("the prover closed its output"))? - } - if let Ok(value) = json::from_str::(line.trim()) { - return Ok(value); - } - } -} - -/// A prover process, and the pipes to talk to it. -struct Session { - /// Kept so the process is killed when this is dropped rather than outliving the relayer. - _child: Child, - stdin: ChildStdin, - stdout: BufReader, -} - -/// Talks to a prover that stays running between proofs. -/// -/// The binary is expected to answer one json request per line with one json response per line, -/// which is what `gnark-apk-proofs`' `prove_serve` does. -pub struct ServiceProver { - /// One proof at a time. The prover is single threaded and the protocol is a line each way, so - /// two callers sharing it would read each other's answers. - session: Mutex, -} - -impl ServiceProver { - /// Start the prover and wait for it to finish its setup. - /// - /// This blocks for as long as the circuit takes to compile, so it belongs in startup rather - /// than on the path of the first proof. - pub async fn new(binary: PathBuf, srs_dir: Option) -> Result { - let mut command = Command::new(&binary); - if let Some(dir) = srs_dir { - command.arg(dir); - } - let mut child = command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .with_context(|| format!("could not start the prover at {binary:?}"))?; - - let stdin = child.stdin.take().ok_or_else(|| anyhow!("prover has no stdin"))?; - let mut stdout = - BufReader::new(child.stdout.take().ok_or_else(|| anyhow!("prover has no stdout"))?); - - // The prover announces itself once the setup is done, so a proof is never sent into a - // process that is still compiling and would look like a hang. - let ready = read_json_line(&mut stdout).await.context("the prover exited during setup")?; - if ready["ready"].as_bool() != Some(true) { - Err(anyhow!("the prover failed to start: {ready}"))? - } - - Ok(Self { session: Mutex::new(Session { _child: child, stdin, stdout }) }) - } -} - -#[async_trait::async_trait] -impl ApkProver for ServiceProver { - async fn prove(&self, request: ApkProofRequest) -> Result { - // The circuit takes uncompressed coordinates, so the compressed keys are opened up here - // rather than teaching the prover about our encoding. - let keys = request - .keys - .iter() - .map(|key| { - let point = decompress_g1(key)?; - let (x, y) = point.xy().ok_or_else(|| anyhow!("authority key is the identity"))?; - let mut packed = Vec::with_capacity(96); - packed.extend_from_slice(&x.into_bigint().to_bytes_be()); - packed.extend_from_slice(&y.into_bigint().to_bytes_be()); - Ok(hex::encode(packed)) - }) - .collect::, anyhow::Error>>()?; - - let mut line = json::to_string(&json::json!({ - "keys": keys, - "participation": request.participation, - }))?; - line.push('\n'); - - let mut session = self.session.lock().await; - session.stdin.write_all(line.as_bytes()).await.context("the prover is gone")?; - session.stdin.flush().await.context("the prover is gone")?; - - let response = read_json_line(&mut session.stdout) - .await - .context("the prover exited while proving")?; - if let Some(error) = response["error"].as_str() { - Err(anyhow!("the prover refused: {error}"))? - } - - let hex_field = |name: &str| -> Result, anyhow::Error> { - let raw = response[name].as_str().ok_or_else(|| anyhow!("proof has no {name}"))?; - Ok(hex::decode(raw.trim_start_matches("0x"))?) - }; - - let bitlist: [U256; BITLIST_WORDS] = response["bitlist"] - .as_array() - .ok_or_else(|| anyhow!("proof has no bitlist"))? - .iter() - .map(|word| { - let raw = word.as_str().ok_or_else(|| anyhow!("bitlist word is not a string"))?; - Ok(U256::from_be_slice(&hex::decode(raw.trim_start_matches("0x"))?)) - }) - .collect::, anyhow::Error>>()? - .try_into() - .map_err(|_| anyhow!("bitlist is not {BITLIST_WORDS} words"))?; - - let apk_commitment = <[u8; 32]>::try_from(hex_field("apkCommitment")?.as_slice()) - .map_err(|_| anyhow!("apk commitment is not 32 bytes"))?; - - Ok(ApkProof { proof: hex_field("apkProof")?, bitlist, apk_commitment }) - } -} diff --git a/tesseract/consensus/beefy/apk/tests/command_prover.rs b/tesseract/consensus/beefy/apk/tests/command_prover.rs deleted file mode 100644 index d9925f2e0..000000000 --- a/tesseract/consensus/beefy/apk/tests/command_prover.rs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (C) Polytope Labs Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! The file contract between this crate and the prover binary. -//! -//! A stub stands in for `gnark-apk-proofs` so these run in a second rather than minutes. What is -//! under test is the handover: what we write, what we read back, and what happens when the prover -//! misbehaves. - -use std::{os::unix::fs::PermissionsExt, path::PathBuf}; - -use apk_beefy::{ApkProofRequest, ApkProver, CommandProver}; -use ark_ec::AffineRepr; -use ark_serialize::CanonicalSerialize; - -/// A key the prover can decompress, which is all this test needs of it. -fn a_key() -> [u8; 48] { - let mut compressed = Vec::new(); - ark_bls12_381::G1Affine::generator() - .serialize_compressed(&mut compressed) - .unwrap(); - compressed.try_into().unwrap() -} - -fn request() -> ApkProofRequest { - ApkProofRequest { keys: vec![a_key(), a_key()], participation: vec![0, 1] } -} - -/// Writes a stub prover into `dir` and returns its path. The body is shell. -fn stub_prover(dir: &PathBuf, body: &str) -> PathBuf { - let path = dir.join("stub-prover"); - std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap(); - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap(); - path -} - -fn work_dir(name: &str) -> PathBuf { - let dir = std::env::temp_dir().join(format!("apk-command-prover-{name}")); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - dir -} - -#[tokio::test] -async fn reads_back_what_the_prover_wrote() { - let dir = work_dir("happy"); - // Echoes a proof shaped like the real one, and proves the inputs arrived by counting the keys. - let prover = stub_prover( - &dir, - r#"test -f "$1/apk-inputs.json" || exit 3 -cat > "$1/apk-snark.json" <<'JSON' -{ - "apkProof": "aabbcc", - "bitlist": ["03", "00", "00", "00", "00"], - "apkCommitment": "1111111111111111111111111111111111111111111111111111111111111111" -} -JSON"#, - ); - - let proof = CommandProver::new(prover, dir.clone()).unwrap().prove(request()).await.unwrap(); - - assert_eq!(proof.proof, vec![0xaa, 0xbb, 0xcc]); - assert_eq!(proof.bitlist[0], alloy_primitives::U256::from(3)); - assert_eq!(proof.apk_commitment, [0x11u8; 32]); - - // The keys reached the prover as uncompressed 96 byte points, which is what it expects. - let written: json::Value = - json::from_slice(&std::fs::read(dir.join("apk-inputs.json")).unwrap()).unwrap(); - assert_eq!(written["keys"].as_array().unwrap().len(), 2); - assert_eq!(written["keys"][0].as_str().unwrap().len(), 192); - assert_eq!(written["participation"].as_array().unwrap().len(), 2); -} - -#[tokio::test] -async fn a_failing_prover_is_an_error() { - let dir = work_dir("failing"); - let prover = stub_prover(&dir, "exit 1"); - - let error = CommandProver::new(prover, dir).unwrap().prove(request()).await.unwrap_err(); - assert!(format!("{error}").contains("exited"), "unexpected error: {error}"); -} - -/// The dangerous case: a prover that fails without writing, leaving the previous proof behind. -#[tokio::test] -async fn stale_output_is_never_read_back() { - let dir = work_dir("stale"); - std::fs::write(dir.join("apk-snark.json"), r#"{"apkProof":"dead"}"#).unwrap(); - let prover = stub_prover(&dir, "exit 0"); - - let error = CommandProver::new(prover, dir).unwrap().prove(request()).await.unwrap_err(); - assert!(format!("{error}").contains("no proof"), "unexpected error: {error}"); -} diff --git a/tesseract/consensus/beefy/apk/tests/live_prover.rs b/tesseract/consensus/beefy/apk/tests/live_prover.rs index 4e90f3bae..de7835416 100644 --- a/tesseract/consensus/beefy/apk/tests/live_prover.rs +++ b/tesseract/consensus/beefy/apk/tests/live_prover.rs @@ -19,27 +19,42 @@ //! closes the other half, that what this crate assembles is something the verifier accepts, which //! is the path a relayer actually takes. //! -//! Needs a relay whose BEEFY authorities hold paired `ecdsa_bls_crypto` keys, the parachain it -//! finalizes, and the prover binary. Takes minutes, since it generates a real SNARK. +//! Needs a relay whose BEEFY authorities hold paired `ecdsa_bls_crypto` keys and the parachain it +//! finalizes. Takes minutes, since it compiles the circuit and generates a real SNARK. //! //! RELAY_WS_URL=ws://127.0.0.1:9979 PARA_WS_URL=ws://127.0.0.1:9981 PARA_ID=4009 \ -//! APK_PROVER_BINARY=~/Documents/polytope/gnark-apk-proofs/target/release/examples/ -//! prove_from_json \ cargo test -p apk-beefy --test live_prover -- --ignored --nocapture +//! cargo test -p apk-beefy --features local --test live_prover -- --ignored --nocapture + +// Both tests build a prover, and the only one there is compiles the circuit in. +#![cfg(feature = "local")] use std::sync::Arc; -use apk_beefy::{CommandProver, Prover}; +use apk_beefy::Prover; use beefy_prover::relay::fetch_latest_beefy_justification; -use beefy_verifier_primitives::{ApkAuthoritySet, ApkConsensusMessage, ApkConsensusState}; +use beefy_verifier_primitives::{ApkConsensusMessage, AuthoritySet, ConsensusState}; use polkadot_sdk::*; use primitive_types::H256; -use subxt::{backend::legacy::LegacyRpcMethods, config::Header as _, PolkadotConfig}; +use subxt::{backend::legacy::LegacyRpcMethods, PolkadotConfig}; const VERIFYING_KEY: &[u8] = include_bytes!("../../../../../evm/tests/foundry/fixtures/apk-verifying-key.bin"); struct TestHost; +/// Seeding reads the chain and never proves, so the slot is filled rather than occupied. +struct NoProver; + +#[async_trait::async_trait] +impl apk_beefy::ApkProver for NoProver { + async fn prove( + &self, + _request: apk_beefy::ApkProofRequest, + ) -> Result { + Err(anyhow::anyhow!("seeding does not prove")) + } +} + impl ismp::messaging::Keccak256 for TestHost { fn keccak256(bytes: &[u8]) -> H256 { sp_io::hashing::keccak_256(bytes).into() @@ -56,8 +71,6 @@ async fn assembles_a_proof_the_verifier_accepts() { .expect("PARA_ID must be set") .parse() .expect("para id is a number"); - let prover_binary = std::env::var("APK_PROVER_BINARY").expect("APK_PROVER_BINARY must be set"); - let (relay_client, relay_rpc_client) = subxt_utils::client::ws_client::(&relay_ws_url, max_rpc_payload_size) .await @@ -81,11 +94,15 @@ async fn assembles_a_proof_the_verifier_accepts() { query_batch_size: Some(100), }; - let work_dir = std::env::temp_dir().join("apk-live-prover"); - let prover = Prover::new( - inner, - Arc::new(CommandProver::new(prover_binary.into(), work_dir).expect("prover")), + // Setup saturates every core for a couple of minutes, so it stays off the runtime. + let srs_dir = std::env::var("APK_SRS_DIR").ok().map(Into::into); + let apk: Arc = Arc::new( + std::thread::spawn(move || apk_beefy::LocalProver::new(srs_dir)) + .join() + .expect("apk circuit setup panicked") + .expect("apk circuit setup"), ); + let prover = Prover::new(inner, apk); let latest: H256 = relay_rpc_client .request("beefy_getFinalizedHead", subxt::ext::subxt_rpcs::rpc_params!()) @@ -135,11 +152,11 @@ async fn assembles_a_proof_the_verifier_accepts() { // is refused rather than checked against zero, so it has to go on whichever set signed. let commitment = H256(prover.current_apk_commitment(latest).await.unwrap()); let signing_set = signed_set_id; - let authority_set = |set: sp_consensus_beefy::mmr::BeefyAuthoritySet| { - let apk_commitment = if set.id == signing_set { commitment } else { H256::zero() }; - ApkAuthoritySet { id: set.id, len: set.len, apk_commitment } + let authority_set = |set: AuthoritySet| AuthoritySet { + bls_poseidon_hash: if set.id == signing_set { commitment } else { H256::zero() }, + ..set }; - let state = ApkConsensusState { + let state = ConsensusState { latest_beefy_height: trusted.latest_beefy_height, beefy_activation_block: trusted.beefy_activation_block, mmr_root_hash: trusted.mmr_root_hash, @@ -147,8 +164,8 @@ async fn assembles_a_proof_the_verifier_accepts() { next_authorities: authority_set(trusted.next_authorities), }; assert!( - state.current_authorities.apk_commitment != H256::zero() || - state.next_authorities.apk_commitment != H256::zero(), + state.current_authorities.bls_poseidon_hash != H256::zero() || + state.next_authorities.bls_poseidon_hash != H256::zero(), "neither trusted set is the one that signed, so the client would have no commitment", ); @@ -157,6 +174,7 @@ async fn assembles_a_proof_the_verifier_accepts() { state.clone(), message, VERIFYING_KEY, + para_id, ) .expect("the verifier accepts what the prover built"); @@ -213,10 +231,7 @@ async fn writes_initial_state_for_bootstrapping() { para_ids: vec![para_id], query_batch_size: Some(100), }; - let prover = Prover::new( - inner, - Arc::new(CommandProver::new("/nonexistent".into(), std::env::temp_dir()).unwrap()), - ); + let prover = Prover::new(inner, Arc::new(NoProver)); let latest: H256 = relay_rpc_client .request("beefy_getFinalizedHead", subxt::ext::subxt_rpcs::rpc_params!()) @@ -234,19 +249,21 @@ async fn writes_initial_state_for_bootstrapping() { Ok("0") => H256::zero(), _ => H256(prover.next_apk_commitment(latest).await.unwrap()), }; - let state = ApkConsensusState { + let state = ConsensusState { latest_beefy_height: trusted.latest_beefy_height, beefy_activation_block: trusted.beefy_activation_block, mmr_root_hash: trusted.mmr_root_hash, - current_authorities: ApkAuthoritySet { + current_authorities: AuthoritySet { id: trusted.current_authorities.id, len: trusted.current_authorities.len, - apk_commitment: current, + bls_poseidon_hash: current, + ecdsa_merkle_root: trusted.current_authorities.ecdsa_merkle_root, }, - next_authorities: ApkAuthoritySet { + next_authorities: AuthoritySet { id: trusted.next_authorities.id, len: trusted.next_authorities.len, - apk_commitment: next, + bls_poseidon_hash: next, + ecdsa_merkle_root: trusted.next_authorities.ecdsa_merkle_root, }, }; diff --git a/tesseract/consensus/beefy/src/prover.rs b/tesseract/consensus/beefy/src/prover.rs index 103633d20..2afab4840 100644 --- a/tesseract/consensus/beefy/src/prover.rs +++ b/tesseract/consensus/beefy/src/prover.rs @@ -126,20 +126,8 @@ pub struct ProverConfig { pub max_rpc_payload_size: Option, /// Query batch size for mmr leaves pub query_batch_size: Option, - /// The `gnark-apk-proofs` prover binary, for a build that has to shell out to it. Ignored by - /// one that compiles the circuit in and proves through it directly, see `apk_beefy::local`. - #[serde(default)] - pub apk_prover_binary: Option, - /// Where a one shot prover exchanges its input and proof files. Ignored when the prover is - /// kept alive, which is the default. - #[serde(default)] - pub apk_prover_dir: Option, - /// Run the prover once per proof instead of keeping it alive. Costs the circuit setup, four - /// minutes, on every proof, so it is only worth it for a prover with no serve mode. - #[serde(default)] - pub apk_prover_one_shot: bool, - /// Where the circuit's structured reference string lives, passed to the prover. Left unset it - /// falls back to the prover's own default. + /// Where the circuit's structured reference string lives. Left unset it falls back to the + /// prover's own default. #[serde(default)] pub apk_srs_dir: Option, /// Where sp1 proving happens when this build proves on a cluster. Ignored by a build that @@ -931,42 +919,26 @@ where }, ProofVariant::Ecdsa => Prover::Ecdsa(prover, PhantomData), ProofVariant::Apk => { - // The circuit is compiled into this process, so there is no binary to talk to and - // the setup happens here rather than in a child. Only a build that proves sp1 on - // a cluster gets this far, since the go runtime the circuit brings cannot share a - // process with the one sp1's gnark ffi links: see the guard in this crate's - // lib.rs. Setup takes a couple of minutes and saturates every core, so it stays - // off the runtime's workers. + // The circuit is only ever proven in process, and it brings a go runtime that + // cannot share one with the runtime sp1's gnark ffi links, so a build proving sp1 + // locally has no way to produce an apk proof: see the guard in this crate's + // lib.rs. + #[cfg(not(feature = "local"))] + return Err(anyhow!( + "the apk variant proves through the circuit compiled into this binary, build \ + with `--no-default-features --features sp1-cluster,local`" + )); + #[cfg(feature = "local")] - let apk_prover: Arc = { + { + // Setup takes a couple of minutes and saturates every core, so it stays off + // the runtime's workers. let srs_dir = config.apk_srs_dir.clone(); let local = std::thread::spawn(move || apk_beefy::LocalProver::new(srs_dir)) .join() .map_err(|_| anyhow!("apk circuit setup panicked"))??; - Arc::new(local) - }; - #[cfg(not(feature = "local"))] - let apk_prover: Arc = { - let binary = - config.apk_prover_binary.clone().ok_or_else(|| { - anyhow!("`apk_prover_binary` is required by a build that proves sp1 locally") - })?; - if config.apk_prover_one_shot { - let work_dir = config - .apk_prover_dir - .clone() - .unwrap_or_else(|| binary.with_file_name("apk-prover-work")); - Arc::new(apk_beefy::CommandProver::new(binary, work_dir)?) - } else { - // Setup runs here, before any proving, so the first proof is not four - // minutes slower than the rest. - Arc::new( - apk_beefy::ServiceProver::new(binary, config.apk_srs_dir.clone()) - .await?, - ) - } - }; - Prover::Apk(apk_beefy::Prover::new(prover, apk_prover)) + Prover::Apk(apk_beefy::Prover::new(prover, Arc::new(local))) + } }, }; diff --git a/tesseract/consensus/beefy/tests/apk_messaging.rs b/tesseract/consensus/beefy/tests/apk_messaging.rs index 258add8a8..8286859ad 100644 --- a/tesseract/consensus/beefy/tests/apk_messaging.rs +++ b/tesseract/consensus/beefy/tests/apk_messaging.rs @@ -27,13 +27,13 @@ //! was learned. If it was not, and the proof was accepted anyway, messages are the only thing left //! it can have been accepted for, which pins the branch without reaching inside the pallet. //! -//! Needs the relay, its parachain, a warm prover binary, and ismp traffic on the parachain so -//! there is something to prove. Takes minutes, since it generates a real SNARK. +//! Needs the relay, its parachain, and ismp traffic on the parachain so there is something to +//! prove. Takes minutes, since it compiles the circuit and generates a real SNARK. //! //! RELAY_WS_URL=ws://127.0.0.1:9979 PARA_WS_URL=ws://127.0.0.1:9981 PARA_ID=4009 \ -//! APK_PROVER_BINARY=~/gnark-apk-proofs/target/release/examples/prove_serve \ //! PARA_SIGNER=0xe5be9a50... \ -//! cargo test -p tesseract-beefy --test apk_messaging -- --ignored --nocapture +//! cargo test -p tesseract-beefy --no-default-features --features sp1-cluster,local \ +//! --test apk_messaging -- --ignored --nocapture use std::sync::Arc; @@ -195,10 +195,8 @@ async fn a_messaging_proof_is_accepted_as_new_work() -> Result<(), anyhow::Error proof_variant: ProofVariant::Apk, max_rpc_payload_size: None, query_batch_size: None, - apk_prover_binary: Some(env("APK_PROVER_BINARY").into()), - apk_prover_dir: None, - apk_prover_one_shot: false, apk_srs_dir: None, + sp1_cluster: None, }; let prover: Prover = Prover::new(prover_config, Default::default()).await?; diff --git a/tesseract/consensus/beefy/tests/mainnet_rotation.rs b/tesseract/consensus/beefy/tests/mainnet_rotation.rs index d15e6e60a..b58ebdde2 100644 --- a/tesseract/consensus/beefy/tests/mainnet_rotation.rs +++ b/tesseract/consensus/beefy/tests/mainnet_rotation.rs @@ -31,12 +31,16 @@ use alloy::{ use alloy_sol_types::SolValue; use anyhow::{anyhow, Context}; use beefy_prover::relay::fetch_latest_beefy_justification; -use ismp_abi::{ecdsa_beefy::BeefyConsensusState, evm_host::EvmHost, handler::handler_v2::HandlerV2}; +use ismp_abi::{ + ecdsa_beefy::BeefyConsensusState, evm_host::EvmHost, handler::handler_v2::HandlerV2, +}; use sp_consensus_beefy::{ecdsa_crypto::Signature, SignedCommitment}; use subxt::{backend::legacy::LegacyRpcMethods, config::Header as _}; use tesseract_beefy::{ backend::{InMemoryProofBackend, ProofBackend}, - prover::{BeefyProver, BeefyProverConfig, Prover, ProverConfig, ProverConsensusState, ProofVariant}, + prover::{ + BeefyProver, BeefyProverConfig, ProofVariant, Prover, ProverConfig, ProverConsensusState, + }, ConsensusState, }; use tesseract_substrate::{ @@ -91,10 +95,14 @@ async fn submit_via_sequencer( ) -> anyhow::Result { // `DynProvider` erases the filler layer, so populate the tx fields explicitly against the // read RPC, sign locally, and push the raw signed tx to the sequencer. - let calldata = - HandlerV2::new(HANDLER, read.clone()).handleConsensus(HOST, proof).calldata().clone(); - let base = - TransactionRequest::default().with_from(from).with_to(HANDLER).with_input(calldata); + let calldata = HandlerV2::new(HANDLER, read.clone()) + .handleConsensus(HOST, proof) + .calldata() + .clone(); + let base = TransactionRequest::default() + .with_from(from) + .with_to(HANDLER) + .with_input(calldata); let chain_id = read.get_chain_id().await.context("get_chain_id")?; let nonce = read.get_transaction_count(from).await.context("get_transaction_count")?; let gas = read.estimate_gas(base.clone()).await.context("estimate_gas")?; @@ -140,7 +148,8 @@ async fn rotate_authorities_across_all_chains() -> anyhow::Result<()> { let from = signer.address(); let wallet = EthereumWallet::from(signer); - let mut chains: Vec<(String, DynProvider, Option)> = Vec::with_capacity(CHAINS.len()); + let mut chains: Vec<(String, DynProvider, Option)> = + Vec::with_capacity(CHAINS.len()); println!("EVM RPC endpoints:"); for (id, name, rpc_env, submit_url) in CHAINS { let url = std::env::var(rpc_env) @@ -181,7 +190,10 @@ async fn rotate_authorities_across_all_chains() -> anyhow::Result<()> { " {name}: height={} current_set={} next_set={}", s.latest_beefy_height, s.current_authorities.id, s.next_authorities.id, ); - if best.as_ref().map_or(true, |(_, b)| s.latest_beefy_height < b.latest_beefy_height) { + if best + .as_ref() + .map_or(true, |(_, b)| s.latest_beefy_height < b.latest_beefy_height) + { best = Some((name.clone(), s)); } } @@ -204,6 +216,8 @@ async fn rotate_authorities_across_all_chains() -> anyhow::Result<()> { proof_variant: ProofVariant::Ecdsa, max_rpc_payload_size: None, query_batch_size: None, + apk_srs_dir: None, + sp1_cluster: None, }; // ECDSA proof submitted to the EVM handler, which does not enforce the SP1 committed- // nonce binding, so a zero account is fine here. @@ -229,10 +243,11 @@ async fn rotate_authorities_across_all_chains() -> anyhow::Result<()> { .await?; // Seed the in-memory backend with the genesis state read from the chains. - let backend: Arc = Arc::new(InMemoryProofBackend::new(ProverConsensusState { - inner: genesis.clone(), - finalized_parachain_height: 0, - })); + let backend: Arc = + Arc::new(InMemoryProofBackend::new(ProverConsensusState { + inner: genesis.clone(), + finalized_parachain_height: 0, + })); let beefy_config = BeefyProverConfig { consensus_state_id: *b"DOT0", @@ -241,12 +256,12 @@ async fn rotate_authorities_across_all_chains() -> anyhow::Result<()> { backend: Default::default(), }; - let beefy = BeefyProver::::new( - beefy_config, - substrate, - prover, - backend, - ) + let beefy = BeefyProver::< + Blake2SubstrateChain, + KeccakSubstrateChain, + zk_beefy::DefaultProver, + dyn ProofBackend, + >::new(beefy_config, substrate, prover, backend) .await?; // A second relay connection for the auxiliary queries the helpers don't expose (resolving @@ -305,15 +320,17 @@ async fn rotate_authorities_across_all_chains() -> anyhow::Result<()> { .chain_get_header(Some(epoch_hash)) .await? .ok_or_else(|| anyhow!("epoch-change header missing"))?; - beefy - .epoch_justification_for(epoch_header.number().into()) - .await? - .ok_or_else(|| anyhow!("no BEEFY justification found for epoch {next_set_id}"))? + beefy.epoch_justification_for(epoch_header.number().into()).await?.ok_or_else( + || anyhow!("no BEEFY justification found for epoch {next_set_id}"), + )? }, None => { // Sets are caught up. Do a final height advance to the live head, then stop. if live_header.number <= anchor.latest_beefy_height { - println!("\nActive chains caught up at height {} set {}", anchor.latest_beefy_height, anchor.current_authorities.id); + println!( + "\nActive chains caught up at height {} set {}", + anchor.latest_beefy_height, anchor.current_authorities.id + ); break; } let head = live_header.hash(); @@ -344,11 +361,17 @@ async fn rotate_authorities_across_all_chains() -> anyhow::Result<()> { // mark the chain skipped, and continue with the rest. Chains with a dedicated submit // endpoint (Arbitrum sequencer) take the raw-tx path; the rest use a normal send. let result: anyhow::Result = match submit { - Some(sequencer) => submit_via_sequencer(read, sequencer, &wallet, from, proof.clone()).await, + Some(sequencer) => + submit_via_sequencer(read, sequencer, &wallet, from, proof.clone()).await, None => { let handler = HandlerV2::new(HANDLER, read.clone()); async { - Ok(handler.handleConsensus(HOST, proof.clone()).send().await?.get_receipt().await?) + Ok(handler + .handleConsensus(HOST, proof.clone()) + .send() + .await? + .get_receipt() + .await?) } .await }, @@ -367,7 +390,10 @@ async fn rotate_authorities_across_all_chains() -> anyhow::Result<()> { advanced_any = true; }, Ok(receipt) => { - println!(" ✗ {name}: reverted (tx {:?}) — skipping this chain.", receipt.transaction_hash); + println!( + " ✗ {name}: reverted (tx {:?}) — skipping this chain.", + receipt.transaction_hash + ); failed.insert(name.clone()); }, Err(e) => { @@ -388,8 +414,11 @@ async fn rotate_authorities_across_all_chains() -> anyhow::Result<()> { } } - let advanced: Vec<&str> = - chains.iter().map(|(n, _, _)| n.as_str()).filter(|n| !failed.contains(*n)).collect(); + let advanced: Vec<&str> = chains + .iter() + .map(|(n, _, _)| n.as_str()) + .filter(|n| !failed.contains(*n)) + .collect(); println!("\nDone — {rotations} rotation(s)."); println!(" advanced: {advanced:?}"); if !failed.is_empty() { From a8cbc83231ece7565efdcacca08e0b4fcc44c685 Mon Sep 17 00:00:00 2001 From: dharjeezy Date: Sun, 23 Aug 2026 20:37:09 +0100 Subject: [PATCH 48/48] point the commitment tests at the merged consensus state type --- .../pallets/beefy-consensus-proofs/src/types.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/modules/pallets/beefy-consensus-proofs/src/types.rs b/modules/pallets/beefy-consensus-proofs/src/types.rs index 0695362eb..86fdf2644 100644 --- a/modules/pallets/beefy-consensus-proofs/src/types.rs +++ b/modules/pallets/beefy-consensus-proofs/src/types.rs @@ -109,21 +109,27 @@ pub fn next_commitment_unknown(state: &[u8], proof_type: u8) -> bool { #[cfg(test)] mod commitment_tests { use super::*; - use beefy_verifier_primitives::{ApkAuthoritySet, ApkConsensusState}; + use beefy_verifier_primitives::{AuthoritySet, ConsensusState}; use codec::Encode; use primitive_types::H256; fn state(next: H256) -> Vec { - ApkConsensusState { + ConsensusState { latest_beefy_height: 100, beefy_activation_block: 0, mmr_root_hash: H256::zero(), - current_authorities: ApkAuthoritySet { + current_authorities: AuthoritySet { id: 7, len: 2, - apk_commitment: H256::repeat_byte(1), + bls_poseidon_hash: H256::repeat_byte(1), + ecdsa_merkle_root: H256::repeat_byte(2), + }, + next_authorities: AuthoritySet { + id: 8, + len: 2, + bls_poseidon_hash: next, + ecdsa_merkle_root: H256::repeat_byte(3), }, - next_authorities: ApkAuthoritySet { id: 8, len: 2, apk_commitment: next }, } .encode() }