Consensus
Reputation-gated BFT for withdrawal approval and compute verification, with on-chain slashing.
Consensus
Paraloom uses Byzantine fault-tolerant consensus among a small validator cohort to verify zk-SNARK withdrawal proofs and compute job results. Validators are verify-only — they do not produce proofs. Voting weight is gated by on-chain reputation, and bad behaviour is recorded as on-chain slashing evidence.
Parameters
| Default | Source | |
|---|---|---|
| Threshold | 7 of 10 | configurable per network in Settings |
| Verification window | tunable | per-message timeout |
| Heartbeat interval | 5 s | coordinator HA |
| Min validator stake | 1 SOL | enforced on-chain (MIN_VALIDATOR_STAKE) |
| Initial reputation | 1000 | new validators start neutral |
The threshold lives in consensus::ConsensusThreshold; the on-chain stake minimum lives in programs/paraloom/src/lib.rs:
pub const MIN_VALIDATOR_STAKE: u64 = 1_000_000_000; // 1 SOLTwo consensus jobs, one cohort
The same BFT cohort handles both:
| Use | Threshold | What's voted on |
|---|---|---|
| Withdrawal approval | 7 / 10 (default) | Validity of a Groth16 withdrawal proof |
| Compute result agreement | 7 / 10 (configurable, lower for stable subsets) | Hash equality of WASM execution outputs |
Withdrawal flow lives in src/consensus/withdrawal.rs; compute consensus uses the same WithdrawalVerificationCoordinator-style aggregation in src/compute/distribution.rs.
Reputation gating
Withdrawal voting is gated by reputation — a validator below the gate threshold cannot vote on withdrawals. New validators start at neutral; reputation moves with observed behaviour.
The reputation tracker (ReputationTracker in src/consensus/reputation.rs) updates per round:
| Event | Reputation delta |
|---|---|
| Correct vote on agreed outcome | + |
| Vote in line with majority on slashable evidence | + |
| Disagreement with majority (no equivocation) | small − |
| Equivocation (conflicting signed votes at same height) | recorded as slashing evidence |
| Persistent unavailability (N missed heartbeats over a window) | recorded as slashing evidence |
The exact deltas are tunable per network and intentionally not advertised as a fixed table — they're consensus parameters, not protocol invariants.
Slashing evidence
Two kinds of evidence are recorded in protocol:
Equivocation
Two valid signatures from the same validator on conflicting messages at the same consensus height. Cryptographic proof of fault — slashable on-chain.
Persistent unavailability
A validator that misses heartbeats / votes for longer than a configurable window without proper unregistration. Aggregated evidence is signed by the cohort and submitted on-chain.
Evidence catalog and tracker live in src/consensus/slashing.rs. Slashing is executed by the slash_validator instruction in the Anchor program — stake is transferred from the validator's PDA to the bridge vault, and the times_slashed counter increments.
// programs/paraloom/src/lib.rs (extract)
pub fn slash_validator(
ctx: Context<SlashValidator>,
validator: Pubkey,
slash_percentage: u8, // 1-100
) -> Result<()> {
let slash_amount = (validator_account.stake_amount as u128
* slash_percentage as u128 / 100) as u64;
validator_account.stake_amount =
validator_account.stake_amount.saturating_sub(slash_amount);
validator_account.times_slashed += 1;
// … transfer slash_amount from validator account to bridge_vault …
}Withdrawal flow (from a vote's perspective)
The chain re-verifies the proof and enforces nullifier uniqueness independently of the cohort — even a fully malicious 10/10 cohort cannot approve an invalid withdrawal. The cohort can only delay or refuse.
Coordinator role
A single coordinator drives round mechanics (collecting votes, computing the threshold, submitting the on-chain tx). It is not a single trust point — its decisions are still verified by the cohort and the on-chain program. But it is a single coordination point. Active/passive HA ensures a primary crash doesn't pause the network: details in Coordinator HA.
Configuration
[consensus]
threshold = 7 # of total_validators
total_validators = 10
heartbeat_secs = 5
# Reputation gate — validators below this are not counted
reputation_gate = 500 # of 10000
[network]
# bootstrap peers are read from on-chain validator registry — no manual seeds neededTests
End-to-end consensus scenarios — happy path, equivocation evidence, persistent unavailability, gating — live in tests/consensus_integration_test.rs. The reputation gating + slashing work landed in v0.4.0 (issue #62).