Tenzro Testnet is live. Get testnet TNZO

Slashing & Equivocation

Tenzro Network enforces validator honesty through on-chain slashing: if a validator signs two conflicting votes at the same consensus height, the equivocation detector in the consensus layer constructs cryptographic evidence and invokes the staking layer's slashing callback, which reduces the validator's stake by 10%. This page documents how that end-to-end pipeline works.

What is Equivocation?

In BFT consensus, equivocation means a validator signs two different messages for the same consensus position. The two canonical failure modes are:

  • Double vote — the validator signs a PREPARE (or COMMIT) vote for block A at height H, then signs a PREPARE (or COMMIT) vote for block B at the same height H
  • Double proposal — the leader proposes two different blocks at the same height, attempting to fork the chain

Either is a protocol-level offense. In an honest run of the protocol, a validator only ever votes once per (height, view) tuple. Equivocation is direct evidence of a Byzantine fault — no legitimate software bug, network partition, or race condition can cause it.

Detection in the VoteCollector

The consensus layer's VoteCollector is where votes arrive during PREPARE and COMMIT phases. Every incoming vote passes through the EquivocationDetector, which indexes votes by (validator, height, view) and checks whether the same validator has previously signed a vote with a different block hash at the same position. If it has, the detector constructs an EquivocationEvidence containing both conflicting votes and their signatures:

// A validator signed two conflicting votes at the same height
EquivocationEvidence {
  validator: 0xvalidator_address...,
  height: 1207,
  view: 10,
  vote_a: PreparedVote {
    block_hash: 0xabc123...,
    signature: 0xsig_a...,
  },
  vote_b: PreparedVote {
    block_hash: 0xdef456...,
    signature: 0xsig_b...,
  },
  detected_at: "2026-04-07T14:23:45Z",
}

Because the evidence carries both signatures, it's self-verifying: any observer can independently check that both signatures come from the same validator public key and that they sign different block hashes. This keeps the slashing decision decentralized — no trusted third party is required to accept the evidence.

SlashingCallback Trait

The consensus crate doesn't know about staking — it operates at the BFT protocol layer, not the economic layer. To bridge the two, consensus exposes a SlashingCallback trait that the node wires to the staking manager at startup. When equivocation is detected, consensus invokes the callback, which in turn calls into StakingManager::slash():

/// Trait that bridges consensus evidence to the staking layer
pub trait SlashingCallback: Send + Sync {
    async fn slash(
        &self,
        validator: &Address,
        evidence: EquivocationEvidence,
        penalty_bps: u32,
    ) -> Result<SlashReceipt, SlashingError>;
}

/// Implementation wired at node startup
pub struct StakingSlashingCallback {
    staking_manager: Arc<StakingManager>,
}

#[async_trait]
impl SlashingCallback for StakingSlashingCallback {
    async fn slash(
        &self,
        validator: &Address,
        evidence: EquivocationEvidence,
        penalty_bps: u32,
    ) -> Result<SlashReceipt, SlashingError> {
        // penalty_bps = 1000 (10%)
        let staked = self.staking_manager.get_stake(validator)?;
        let penalty = (staked * penalty_bps as u128) / 10_000;

        self.staking_manager.slash(validator, penalty).await?;
        self.staking_manager.record_evidence(evidence).await?;

        Ok(SlashReceipt {
            validator: *validator,
            amount_slashed: penalty,
            remaining_stake: staked - penalty,
            epoch: self.staking_manager.current_epoch(),
        })
    }
}

This clean separation means consensus can run in isolation for testing (with a no-op callback), and the staking layer can be swapped without touching consensus code. The StakingSlashingCallback implementation lives in tenzro-node and is instantiated once at startup.

End-to-End Pipeline

Putting it all together, here's the full flow from a conflicting vote arriving at the VoteCollector to the validator losing stake:

VoteCollector::insert_vote(vote)
   |
   v
EquivocationDetector::check(validator, height, view, vote)
   |
   |  (already have a vote from this validator at same height/view
   |   with a different block hash?)
   |
   +-- NO  --> store vote, return Ok
   |
   +-- YES --> construct EquivocationEvidence
                 |
                 v
              SlashingCallback::slash(validator, evidence, 1000)
                 |
                 v
              StakingManager::slash(validator, 10% of stake)
                 |
                 v
              - Validator stake reduced
              - Evidence persisted to CF_SLASHING
              - Next epoch: removed from validator set
              - Evidence emitted as block metadata

The pipeline is synchronous and deterministic. Every node that receives both conflicting votes will independently detect the equivocation and produce the same EquivocationEvidence. The staking mutation is applied atomically as part of the block that includes the evidence.

Slashing Parameters

  • Penalty rate: 10% of total staked balance (1000 basis points)
  • Scope: Applied to the validator's own stake (delegators are unaffected at the consensus layer; delegation slashing is enforced separately at the staking layer)
  • Timing: Applied immediately on evidence commit; validator set update happens at next epoch boundary
  • Evidence storage: Written to the CF_SLASHING column family in RocksDB for auditability and replay

Validator Set Update at Epoch Boundary

Slashed validators are removed from the active validator set at the next epoch transition if their remaining stake falls below the minimum threshold. Until then, they remain in the set with reduced stake (and therefore reduced weight in leader selection):

Epoch 5 -> Epoch 6 transition
  1. Collect all slashing evidence from epoch 5
  2. Apply each slash via StakingSlashingCallback
  3. Recompute validator set (top N stakers after slashing)
  4. Validators below minimum stake threshold removed
  5. Epoch 6 validator set locked
  6. New leader sequence computed with TEE-weighting

Defending Against False Slashing

Because the EquivocationEvidencecarries both raw signatures, it is self-verifying — any observer can check the signatures independently. A malicious node cannot fabricate evidence without forging a valid signature from the target validator's key, which reduces to forging Ed25519 or Secp256k1 signatures (not computationally feasible).

Additionally, the evidence must be committed within a block produced by the honest majority, so an isolated Byzantine node cannot slash anyone unilaterally. Slashing requires consensus on the evidence, which requires 2f + 1 honest validators.

Recovering from a Slash

A slashed validator is not permanently banned. After the slash is applied:

  • The validator can top up its stake to meet the minimum threshold
  • At the next epoch, if the validator is in the top N by stake, it rejoins the active set
  • Evidence is retained in CF_SLASHING for historical accountability
  • Repeated equivocation results in repeated slashing; there is no cap on how many times a validator can be slashed

The design assumes that validators who equivocate are either misconfigured (e.g., running multiple instances with the same key) or actively malicious. In either case, repeated slashing until they either fix their setup or run out of stake is the correct response.

Related Resources

  • Consensus Protocol — where votes are collected and PREPARE/COMMIT certificates are built
  • Token Economics — staking, rewards, and the TNZO supply model
  • Governance — how slashing parameters can be changed through on-chain proposals