// Package blockchain — equivocation evidence verification for SLASH txs. // // "Equivocation" = a validator signing two different consensus messages // at the same height+view+phase, each endorsing a different block hash. // PBFT safety depends on validators NOT doing this; a malicious validator // that equivocates can split honest nodes into disagreeing majorities. // // The SLASH tx embeds an EquivocationEvidence payload carrying both // conflicting messages. Any node (not just the victim) can submit it; // on-chain verification is purely cryptographic — no "trust me" from the // submitter. If the evidence is valid, the offender's stake is burned and // they're removed from the validator set. package blockchain import ( "bytes" "crypto/ed25519" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" ) // EquivocationEvidence is embedded (as JSON bytes) in SlashPayload.Evidence // when Reason == "equivocation". Two distinct consensus messages from the // same validator at the same consensus position prove they are trying to // fork the chain. type EquivocationEvidence struct { A *ConsensusMsg `json:"a"` B *ConsensusMsg `json:"b"` } // ValidateEquivocation verifies that the two messages constitute genuine // equivocation evidence against `offender`. Returns nil on success; // errors are returned with enough detail for the applyTx caller to log // why a slash was rejected. // // Rules: // - Both messages must be signed by `offender` (From = offender, // signature verifies against the offender's Ed25519 pubkey). // - Same Type (MsgPrepare or MsgCommit — we don't slash for equivocating // on PrePrepare since leaders can legitimately re-propose). // - Same View, same SeqNum — equivocation is about the same consensus // round. // - Distinct BlockHash — otherwise the two messages are identical and // not actually contradictory. // - Both sigs verify against the offender's pubkey. func ValidateEquivocation(offender string, ev *EquivocationEvidence) error { if ev == nil || ev.A == nil || ev.B == nil { return fmt.Errorf("equivocation: missing message(s)") } if ev.A.From != offender || ev.B.From != offender { return fmt.Errorf("equivocation: messages not from offender %s", offender[:8]) } // Only PREPARE / COMMIT equivocation is slashable. PRE-PREPARE double- // proposals are expected during view changes — the protocol tolerates // them. if ev.A.Type != ev.B.Type { return fmt.Errorf("equivocation: messages are different types (%v vs %v)", ev.A.Type, ev.B.Type) } if ev.A.Type != MsgPrepare && ev.A.Type != MsgCommit { return fmt.Errorf("equivocation: only PREPARE/COMMIT are slashable (got %v)", ev.A.Type) } if ev.A.View != ev.B.View { return fmt.Errorf("equivocation: different views (%d vs %d)", ev.A.View, ev.B.View) } if ev.A.SeqNum != ev.B.SeqNum { return fmt.Errorf("equivocation: different seqnums (%d vs %d)", ev.A.SeqNum, ev.B.SeqNum) } if bytes.Equal(ev.A.BlockHash, ev.B.BlockHash) { return fmt.Errorf("equivocation: messages endorse the same block") } // Decode pubkey + verify both signatures over the canonical bytes. pubBytes, err := hex.DecodeString(offender) if err != nil || len(pubBytes) != ed25519.PublicKeySize { return fmt.Errorf("equivocation: bad offender pubkey") } pub := ed25519.PublicKey(pubBytes) if !ed25519.Verify(pub, consensusMsgSignBytes(ev.A), ev.A.Signature) { return fmt.Errorf("equivocation: signature A does not verify") } if !ed25519.Verify(pub, consensusMsgSignBytes(ev.B), ev.B.Signature) { return fmt.Errorf("equivocation: signature B does not verify") } return nil } // consensusMsgSignBytes MUST match consensus/pbft.go:msgSignBytes exactly. // We duplicate it here (instead of importing consensus) to keep the // blockchain package free of a consensus dependency — consensus already // imports blockchain for types. func consensusMsgSignBytes(msg *ConsensusMsg) []byte { tmp := *msg tmp.Signature = nil tmp.Block = nil data, _ := json.Marshal(tmp) h := sha256.Sum256(data) return h[:] }