use crate::error::{ConsensusError, Result};
use crate::validator::ValidatorSet;
use crate::voter::VOTE_FORMAT_VERSION;
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tenzro_crypto::bls::{BlsKeyPair, BlsPublicKey, BlsSignature};
use tenzro_storage::{CF_AUDIT, KvStore};
use tenzro_types::primitives::Address;
const ZK_COSIGN_TAG: &[u8] = b"TENZRO_ZK_COSIGN:";
pub const FRAUD_WINDOW_BLOCKS: u64 = 256;
mod bls_aggregate_serde {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(bytes: &[u8; 96], ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_bytes(bytes)
}
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<[u8; 96], D::Error> {
let v: Vec<u8> = Vec::<u8>::deserialize(de)?;
if v.len() != 96 {
return Err(serde::de::Error::custom(format!(
"zk-quorum bls_aggregate must be exactly 96 bytes, got {}",
v.len()
)));
}
let mut out = [0u8; 96];
out.copy_from_slice(&v);
Ok(out)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZkCommitmentClaim {
pub circuit_id: String,
pub commitment: [u8; 32],
pub proof_locator: String,
}
impl ZkCommitmentClaim {
pub fn cosign_payload(commitment: &[u8; 32]) -> Vec<u8> {
let mut payload = Vec::with_capacity(ZK_COSIGN_TAG.len() + 1 + 32);
payload.extend_from_slice(ZK_COSIGN_TAG);
payload.push(VOTE_FORMAT_VERSION);
payload.extend_from_slice(commitment);
payload
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZkCosign {
pub commitment: [u8; 32],
pub validator: Address,
pub bls_signature: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ZkQuorumCertificate {
pub circuit_id: String,
pub commitment: [u8; 32],
pub voting_power: u128,
#[serde(with = "bls_aggregate_serde")]
pub bls_aggregate: [u8; 96],
pub signer_bitmap: Vec<u8>,
}
impl ZkQuorumCertificate {
pub fn form(
circuit_id: String,
commitment: [u8; 32],
cosigns: &[ZkCosign],
validator_set: &ValidatorSet,
) -> Result<Self> {
let active = validator_set.active_validators();
let n = active.len();
let bitmap_bytes = n.div_ceil(8);
let mut signer_bitmap = vec![0u8; bitmap_bytes];
let mut bls_sigs: Vec<BlsSignature> = Vec::new();
let mut voting_power: u128 = 0;
let normalized = validator_set.normalized_weights();
let mut signed_power: u128 = 0;
let mut seen = vec![false; n];
let payload = ZkCommitmentClaim::cosign_payload(&commitment);
for cosign in cosigns {
if cosign.commitment != commitment {
continue;
}
let Some(idx) = validator_set.index_of(&cosign.validator) else {
continue;
};
if seen[idx] {
continue;
}
let sig = BlsSignature::from_bytes(&cosign.bls_signature).map_err(|e| {
ConsensusError::InvalidSignature(format!(
"zk co-sign from {} carries malformed BLS signature: {e}",
cosign.validator
))
})?;
let pk = BlsPublicKey::from_bytes(&active[idx].bls_public_key).map_err(|e| {
ConsensusError::InvalidSignature(format!(
"validator {} has malformed BLS key: {e}",
cosign.validator
))
})?;
let ok = sig.verify(&pk, &payload).map_err(|e| {
ConsensusError::InvalidSignature(format!("zk co-sign verify raised: {e}"))
})?;
if !ok {
continue;
}
seen[idx] = true;
signer_bitmap[idx / 8] |= 1 << (idx % 8);
bls_sigs.push(sig);
voting_power = voting_power.saturating_add(active[idx].voting_power());
if let Some(w) = normalized.get(idx) {
signed_power = signed_power.saturating_add(*w);
}
}
let quorum_power = validator_set.quorum_voting_power();
if signed_power < quorum_power {
return Err(ConsensusError::InsufficientVotes {
got: signed_power.min(u64::MAX as u128) as u64,
need: quorum_power.min(u64::MAX as u128) as u64,
});
}
let agg = tenzro_crypto::bls::aggregate_signatures(&bls_sigs).map_err(|e| {
ConsensusError::InvalidSignature(format!("zk-quorum BLS aggregation failed: {e}"))
})?;
Ok(Self {
circuit_id,
commitment,
voting_power,
bls_aggregate: agg.to_bytes(),
signer_bitmap,
})
}
pub fn signers(&self, validator_set: &ValidatorSet) -> Vec<Address> {
let active = validator_set.active_validators();
let n = active.len();
let mut out = Vec::new();
#[allow(clippy::needless_range_loop)]
for bit_index in 0..(self.signer_bitmap.len() * 8) {
if bit_index >= n {
break;
}
let byte = self.signer_bitmap[bit_index / 8];
if (byte >> (bit_index % 8)) & 1 == 1 {
out.push(active[bit_index].address);
}
}
out
}
pub fn verify(&self, validator_set: &ValidatorSet) -> Result<()> {
let active = validator_set.active_validators();
let n = active.len();
let expected_bitmap_bytes = n.div_ceil(8);
if self.signer_bitmap.len() != expected_bitmap_bytes {
return Err(ConsensusError::InvalidSignature(format!(
"zk-quorum signer_bitmap length {} does not match expected {} for {} validators",
self.signer_bitmap.len(),
expected_bitmap_bytes,
n
)));
}
let normalized = validator_set.normalized_weights();
let mut signer_pks: Vec<BlsPublicKey> = Vec::new();
let mut total_voting_power: u128 = 0;
let mut signed_power: u128 = 0;
#[allow(clippy::needless_range_loop)]
for bit_index in 0..(expected_bitmap_bytes * 8) {
let byte = self.signer_bitmap[bit_index / 8];
if (byte >> (bit_index % 8)) & 1 == 0 {
continue;
}
if bit_index >= n {
return Err(ConsensusError::InvalidSignature(format!(
"zk-quorum bitmap bit {bit_index} set but active set has {n} validators"
)));
}
let validator = &active[bit_index];
let pk = BlsPublicKey::from_bytes(&validator.bls_public_key).map_err(|e| {
ConsensusError::InvalidSignature(format!(
"zk-quorum signer {} has malformed BLS key: {e}",
validator.address
))
})?;
signer_pks.push(pk);
total_voting_power = total_voting_power.saturating_add(validator.voting_power());
if let Some(w) = normalized.get(bit_index) {
signed_power = signed_power.saturating_add(*w);
}
}
if signer_pks.is_empty() {
return Err(ConsensusError::InvalidSignature(
"zk-quorum bitmap empty — no signers".to_string(),
));
}
let quorum_power = validator_set.quorum_voting_power();
if signed_power < quorum_power {
return Err(ConsensusError::InvalidSignature(format!(
"zk-quorum carries {signed_power} stake-weight from {} signers, below quorum {quorum_power}",
signer_pks.len()
)));
}
if self.voting_power != total_voting_power {
return Err(ConsensusError::InvalidSignature(format!(
"zk-quorum claims voting_power={} but bitmap-tallied power is {total_voting_power}",
self.voting_power
)));
}
let agg_pk = tenzro_crypto::bls::aggregate_public_keys(&signer_pks).map_err(|e| {
ConsensusError::InvalidSignature(format!(
"zk-quorum aggregate public-key reconstruction failed: {e}"
))
})?;
let agg_pk_single = BlsPublicKey::from_bytes(&agg_pk.to_bytes()).map_err(|e| {
ConsensusError::InvalidSignature(format!(
"zk-quorum aggregate public-key round-trip failed: {e}"
))
})?;
let agg_sig = BlsSignature::from_bytes(&self.bls_aggregate).map_err(|e| {
ConsensusError::InvalidSignature(format!(
"zk-quorum bls_aggregate is not a valid signature: {e}"
))
})?;
let payload = ZkCommitmentClaim::cosign_payload(&self.commitment);
let ok = agg_sig.verify(&agg_pk_single, &payload).map_err(|e| {
ConsensusError::InvalidSignature(format!("zk-quorum aggregate verify raised: {e}"))
})?;
if !ok {
return Err(ConsensusError::InvalidSignature(
"zk-quorum aggregate verification rejected the signature".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FraudOutcome {
Unfounded,
Upheld,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AttestedCommitment {
pub certificate: ZkQuorumCertificate,
pub attested_at_height: u64,
pub proof_locator: String,
}
impl AttestedCommitment {
pub fn in_fraud_window(&self, current_height: u64) -> bool {
current_height < self.attested_at_height.saturating_add(FRAUD_WINDOW_BLOCKS)
}
}
pub const ZK_QUORUM_TOPIC: &str = "tenzro/zk-quorum";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ZkQuorumMsg {
Claim {
claim: ZkCommitmentClaim,
cosign: ZkCosign,
},
Cosign {
circuit_id: String,
cosign: ZkCosign,
},
}
pub struct ZkQuorumStore {
bls_key: Arc<BlsKeyPair>,
address: Address,
pending: DashMap<[u8; 32], Vec<ZkCosign>>,
attested: DashMap<[u8; 32], AttestedCommitment>,
storage: Option<Arc<dyn KvStore>>,
}
impl ZkQuorumStore {
fn attested_key(commitment: &[u8; 32]) -> Vec<u8> {
let mut k = b"zk_quorum/attested/".to_vec();
k.extend_from_slice(commitment);
k
}
pub fn new(bls_key: Arc<BlsKeyPair>, address: Address) -> Self {
Self {
bls_key,
address,
pending: DashMap::new(),
attested: DashMap::new(),
storage: None,
}
}
pub fn with_storage(
bls_key: Arc<BlsKeyPair>,
address: Address,
storage: Arc<dyn KvStore>,
) -> Self {
let store = Self {
bls_key,
address,
pending: DashMap::new(),
attested: DashMap::new(),
storage: Some(storage.clone()),
};
store.hydrate();
store
}
pub fn address(&self) -> &Address {
&self.address
}
fn hydrate(&self) {
let Some(storage) = self.storage.as_ref() else {
return;
};
let prefix = b"zk_quorum/attested/";
let Ok(entries) = storage.scan_prefix(CF_AUDIT, prefix) else {
return;
};
for (_key, bytes) in entries {
if let Ok(record) = serde_json::from_slice::<AttestedCommitment>(&bytes) {
self.attested.insert(record.certificate.commitment, record);
}
}
}
fn persist_attested(&self, record: &AttestedCommitment) {
let Some(storage) = self.storage.as_ref() else {
return;
};
if let Ok(bytes) = serde_json::to_vec(record) {
let _ = storage.put(
CF_AUDIT,
&Self::attested_key(&record.certificate.commitment),
&bytes,
);
}
}
fn delete_attested(&self, commitment: &[u8; 32]) {
if let Some(storage) = self.storage.as_ref() {
let _ = storage.delete(CF_AUDIT, &Self::attested_key(commitment));
}
}
pub fn cosign(&self, commitment: [u8; 32]) -> ZkCosign {
let payload = ZkCommitmentClaim::cosign_payload(&commitment);
let sig = self.bls_key.sign(&payload);
ZkCosign {
commitment,
validator: self.address,
bls_signature: sig.to_bytes().to_vec(),
}
}
pub fn record_cosign(
&self,
circuit_id: &str,
cosign: ZkCosign,
validator_set: &ValidatorSet,
) -> Result<Option<ZkQuorumCertificate>> {
let commitment = cosign.commitment;
{
let mut entry = self.pending.entry(commitment).or_default();
if entry.iter().any(|c| c.validator == cosign.validator) {
} else {
entry.push(cosign);
}
}
let cosigns = self
.pending
.get(&commitment)
.map(|e| e.clone())
.unwrap_or_default();
match ZkQuorumCertificate::form(circuit_id.to_string(), commitment, &cosigns, validator_set)
{
Ok(cert) => {
self.pending.remove(&commitment);
Ok(Some(cert))
}
Err(ConsensusError::InsufficientVotes { .. }) => Ok(None),
Err(e) => Err(e),
}
}
pub fn open_fraud_window(
&self,
certificate: ZkQuorumCertificate,
proof_locator: String,
current_height: u64,
) -> AttestedCommitment {
let record = AttestedCommitment {
certificate,
attested_at_height: current_height,
proof_locator,
};
self.persist_attested(&record);
self.attested
.insert(record.certificate.commitment, record.clone());
record
}
pub fn attested(&self, commitment: &[u8; 32]) -> Option<AttestedCommitment> {
self.attested.get(commitment).map(|e| e.clone())
}
pub fn resolve_fraud_proof(
&self,
commitment: &[u8; 32],
current_height: u64,
reverified: bool,
validator_set: &ValidatorSet,
) -> Result<(FraudOutcome, Vec<Address>)> {
let record = self
.attested
.get(commitment)
.map(|e| e.clone())
.ok_or_else(|| {
ConsensusError::Internal(
"fraud proof names a commitment not inside any open fraud window".to_string(),
)
})?;
if !record.in_fraud_window(current_height) {
return Err(ConsensusError::Internal(format!(
"fraud window for commitment closed at height {}",
record
.attested_at_height
.saturating_add(FRAUD_WINDOW_BLOCKS)
)));
}
if reverified {
return Ok((FraudOutcome::Unfounded, Vec::new()));
}
let signers = record.certificate.signers(validator_set);
self.attested.remove(commitment);
self.delete_attested(commitment);
Ok((FraudOutcome::Upheld, signers))
}
pub fn prune_closed_windows(&self, current_height: u64) -> usize {
let expired: Vec<[u8; 32]> = self
.attested
.iter()
.filter(|e| !e.value().in_fraud_window(current_height))
.map(|e| *e.key())
.collect();
for commitment in &expired {
self.attested.remove(commitment);
self.delete_attested(commitment);
}
expired.len()
}
pub fn open_window_count(&self) -> usize {
self.attested.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validator::{ValidatorInfo, ValidatorSet};
use tenzro_crypto::bls::BlsKeyPair;
use tenzro_crypto::pq::MlDsaSigningKey;
use tenzro_crypto::{KeyPair, KeyType};
fn addr(byte: u8) -> Address {
Address::new([byte; 32])
}
fn mk_validator(seed: u8, stake: u128) -> (ValidatorInfo, Arc<BlsKeyPair>) {
let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
let pq = MlDsaSigningKey::generate();
let bls = BlsKeyPair::generate().unwrap();
let info = ValidatorInfo::new(
addr(seed),
kp.public_key().clone(),
pq.verifying_key_bytes().to_vec(),
bls.public_key().to_bytes().to_vec(),
stake,
);
(info, Arc::new(bls))
}
fn commitment_of(seed: u8) -> [u8; 32] {
let mut c = [0u8; 32];
c[0] = seed;
c
}
#[test]
fn certificate_forms_and_verifies_at_quorum() {
let mut infos = Vec::new();
let mut keys = Vec::new();
for i in 0..4u8 {
let (info, key) = mk_validator(i + 1, 100);
infos.push(info);
keys.push(key);
}
let set = ValidatorSet::new(0, infos.clone()).expect("set");
let commitment = commitment_of(7);
let payload = ZkCommitmentClaim::cosign_payload(&commitment);
let mut cosigns = Vec::new();
for i in 0..3usize {
let sig = keys[i].sign(&payload);
cosigns.push(ZkCosign {
commitment,
validator: infos[i].address,
bls_signature: sig.to_bytes().to_vec(),
});
}
let cert = ZkQuorumCertificate::form("inference".to_string(), commitment, &cosigns, &set)
.expect("forms at quorum");
cert.verify(&set).expect("verifies");
assert_eq!(cert.signers(&set).len(), 3);
}
#[test]
fn certificate_rejected_below_quorum() {
let mut infos = Vec::new();
let mut keys = Vec::new();
for i in 0..4u8 {
let (info, key) = mk_validator(i + 1, 100);
infos.push(info);
keys.push(key);
}
let set = ValidatorSet::new(0, infos.clone()).expect("set");
let commitment = commitment_of(9);
let payload = ZkCommitmentClaim::cosign_payload(&commitment);
let mut cosigns = Vec::new();
for i in 0..2usize {
let sig = keys[i].sign(&payload);
cosigns.push(ZkCosign {
commitment,
validator: infos[i].address,
bls_signature: sig.to_bytes().to_vec(),
});
}
let err = ZkQuorumCertificate::form("inference".to_string(), commitment, &cosigns, &set);
assert!(matches!(err, Err(ConsensusError::InsufficientVotes { .. })));
}
#[test]
fn store_forms_cert_when_quorum_reached() {
let mut infos = Vec::new();
let mut keys = Vec::new();
for i in 0..4u8 {
let (info, key) = mk_validator(i + 1, 100);
infos.push(info);
keys.push(key);
}
let set = ValidatorSet::new(0, infos.clone()).expect("set");
let store = ZkQuorumStore::new(keys[0].clone(), infos[0].address);
let commitment = commitment_of(3);
for i in 0..2usize {
let c = ZkCosign {
commitment,
validator: infos[i].address,
bls_signature: keys[i]
.sign(&ZkCommitmentClaim::cosign_payload(&commitment))
.to_bytes()
.to_vec(),
};
let out = store.record_cosign("inference", c, &set).expect("ok");
assert!(out.is_none());
}
let c = ZkCosign {
commitment,
validator: infos[2].address,
bls_signature: keys[2]
.sign(&ZkCommitmentClaim::cosign_payload(&commitment))
.to_bytes()
.to_vec(),
};
let cert = store
.record_cosign("inference", c, &set)
.expect("ok")
.expect("cert formed at quorum");
cert.verify(&set).expect("verifies");
}
#[test]
fn fraud_window_upheld_names_cosigners_and_retracts() {
let mut infos = Vec::new();
let mut keys = Vec::new();
for i in 0..4u8 {
let (info, key) = mk_validator(i + 1, 100);
infos.push(info);
keys.push(key);
}
let set = ValidatorSet::new(0, infos.clone()).expect("set");
let store = ZkQuorumStore::new(keys[0].clone(), infos[0].address);
let commitment = commitment_of(5);
let payload = ZkCommitmentClaim::cosign_payload(&commitment);
let cosigns: Vec<ZkCosign> = (0..3usize)
.map(|i| ZkCosign {
commitment,
validator: infos[i].address,
bls_signature: keys[i].sign(&payload).to_bytes().to_vec(),
})
.collect();
let cert =
ZkQuorumCertificate::form("inference".to_string(), commitment, &cosigns, &set).unwrap();
store.open_fraud_window(cert, "tenzro://blob/deadbeef".to_string(), 1_000);
let (outcome, signers) = store
.resolve_fraud_proof(&commitment, 1_100, false, &set)
.expect("in window");
assert_eq!(outcome, FraudOutcome::Upheld);
assert_eq!(signers.len(), 3);
assert!(store.attested(&commitment).is_none());
}
#[test]
fn fraud_window_unfounded_keeps_commitment() {
let mut infos = Vec::new();
let mut keys = Vec::new();
for i in 0..4u8 {
let (info, key) = mk_validator(i + 1, 100);
infos.push(info);
keys.push(key);
}
let set = ValidatorSet::new(0, infos.clone()).expect("set");
let store = ZkQuorumStore::new(keys[0].clone(), infos[0].address);
let commitment = commitment_of(6);
let payload = ZkCommitmentClaim::cosign_payload(&commitment);
let cosigns: Vec<ZkCosign> = (0..3usize)
.map(|i| ZkCosign {
commitment,
validator: infos[i].address,
bls_signature: keys[i].sign(&payload).to_bytes().to_vec(),
})
.collect();
let cert =
ZkQuorumCertificate::form("inference".to_string(), commitment, &cosigns, &set).unwrap();
store.open_fraud_window(cert, "tenzro://blob/cafe".to_string(), 2_000);
let (outcome, signers) = store
.resolve_fraud_proof(&commitment, 2_050, true, &set)
.expect("in window");
assert_eq!(outcome, FraudOutcome::Unfounded);
assert!(signers.is_empty());
assert!(store.attested(&commitment).is_some());
}
#[test]
fn fraud_proof_past_window_rejected() {
let (info, key) = mk_validator(1, 100);
let set = ValidatorSet::new(0, vec![info.clone()]).ok();
let Some(set) = set else {
return;
};
let store = ZkQuorumStore::new(key.clone(), info.address);
let commitment = commitment_of(8);
let payload = ZkCommitmentClaim::cosign_payload(&commitment);
let cosign = ZkCosign {
commitment,
validator: info.address,
bls_signature: key.sign(&payload).to_bytes().to_vec(),
};
if let Ok(cert) =
ZkQuorumCertificate::form("inference".to_string(), commitment, &[cosign], &set)
{
store.open_fraud_window(cert, "tenzro://blob/aa".to_string(), 100);
let past = 100 + FRAUD_WINDOW_BLOCKS + 1;
let out = store.resolve_fraud_proof(&commitment, past, false, &set);
assert!(out.is_err());
}
}
}