@@ -69,6 +69,7 @@ const RESERVED_PROTOCOL_IDENTIFIERS: &[&str] = &[
6969 "system_emission" ,
7070 "system_rewards_pool" ,
7171 "system_ping_commitment" ,
72+ "system_slashing" , // EquivocationProof sender — block-construction only, never gossiped
7273] ;
7374
7475/// Validate whether the `tx.from` value matches one of the three accepted
@@ -242,6 +243,21 @@ pub mod gas_limits {
242243/// Transaction hash type
243244pub type TxHash = String ;
244245
246+ /// One side of an equivocation proof: the per-block signable header fields of a
247+ /// microblock (height + producer are shared across both sides, kept on the TX).
248+ /// Carries enough to reconstruct the exact `Block_Sig_v23.1` signing digest and
249+ /// re-verify the producer's Dilithium3 signature on-chain — no trust in the reporter.
250+ #[ derive( Debug , Clone , Serialize , Deserialize , PartialEq ) ]
251+ pub struct EquivocationHeader {
252+ pub timestamp : u64 ,
253+ pub merkle_root : [ u8 ; 32 ] ,
254+ pub previous_hash : [ u8 ; 32 ] ,
255+ pub state_root : [ u8 ; 32 ] ,
256+ pub vrf_output : Option < [ u8 ; 32 ] > ,
257+ pub timeout_round : u64 ,
258+ pub signature : Vec < u8 > ,
259+ }
260+
245261/// Transaction types
246262#[ derive( Debug , Clone , Serialize , Deserialize , PartialEq ) ]
247263pub enum TransactionType {
@@ -251,7 +267,41 @@ pub enum TransactionType {
251267 to : String ,
252268 amount : u64 ,
253269 } ,
254-
270+
271+ /// Cryptographic proof that `offender` signed two DIFFERENT microblocks at the
272+ /// same `height` — provable equivocation. Both `EquivocationHeader.signature`s are
273+ /// the offender's Dilithium3 sigs over the `Block_Sig_v23.1` digest of their fields;
274+ /// unforgeable. Verified on-chain against the offender's registry PK and applied
275+ /// deterministically in the reputation fold (offender → reputation 0 + ban). No
276+ /// balance effect. Fail-safe: an invalid/forged proof simply fails verification.
277+ EquivocationProof {
278+ offender : String ,
279+ height : u64 ,
280+ block_a : EquivocationHeader ,
281+ block_b : EquivocationHeader ,
282+ } ,
283+
284+ /// Cryptographic proof that `offender` signed two DIFFERENT checkpoint votes at the SAME
285+ /// consensus round `index` — provable BFT vote equivocation (accountable safety: a
286+ /// committee member double-voting is what an attacker would do to violate finality
287+ /// safety). Both signatures are the offender's consensus-key sigs over the canonical
288+ /// `QNET_BFT2_VOTE:<hex(checkpoint_hash)>` message; unforgeable. Verified on-chain against
289+ /// the offender's registry PK and applied in the reputation fold (offender → ban). No
290+ /// balance effect. Fail-safe: an invalid/forged proof simply fails verification.
291+ VoteEquivocationProof {
292+ offender : String ,
293+ /// bincode of BOTH conflicting checkpoints (qnet_consensus Checkpoint). SOUNDNESS:
294+ /// the vote signature covers ONLY the checkpoint hash, NOT the round, so the full
295+ /// preimages are REQUIRED — the fold re-derives each hash and reads each round `index`,
296+ /// then bans ONLY if `index_a == index_b` (a same-round double-vote) and the hashes
297+ /// differ and both sigs verify. Carrying only hashes would let a forger pair two honest
298+ /// votes from DIFFERENT rounds and falsely slash an honest node.
299+ checkpoint_a : Vec < u8 > ,
300+ signature_a : Vec < u8 > ,
301+ checkpoint_b : Vec < u8 > ,
302+ signature_b : Vec < u8 > ,
303+ } ,
304+
255305 /// Token swap via DEX smart contract
256306 /// Fee: standard gas fee goes directly to block producer (v3.18: Pool 2 removed)
257307 Swap {
@@ -810,6 +860,8 @@ impl Transaction {
810860 | TransactionType :: LightNodeEligibilityBitmap { .. }
811861 | TransactionType :: RewardDistribution
812862 | TransactionType :: KeyRotation { .. }
863+ | TransactionType :: EquivocationProof { .. }
864+ | TransactionType :: VoteEquivocationProof { .. }
813865 )
814866 }
815867
@@ -936,6 +988,9 @@ impl Transaction {
936988 // gas costs to deter quantum-readiness adoption. Rate-limited via
937989 // per-account nonce monotonicity (one upgrade per account, ever).
938990 TransactionType :: SetPQRequirement { } => 0 ,
991+ // Equivocation slashing proofs: system TX, free (no gas, no balance effect).
992+ TransactionType :: EquivocationProof { .. } => 0 ,
993+ TransactionType :: VoteEquivocationProof { .. } => 0 ,
939994 }
940995 }
941996
@@ -1521,6 +1576,35 @@ impl Transaction {
15211576 // because it inspects fields on the parent Transaction struct.
15221577 // Empty sender is already caught at the top of `validate()`.
15231578 }
1579+ TransactionType :: EquivocationProof { offender, block_a, block_b, .. } => {
1580+ // Structural check only — the cryptographic verification (Dilithium3 over
1581+ // the Block_Sig_v23.1 digest against the offender's registry PK) runs at the
1582+ // integration layer, which holds the consensus PK registry.
1583+ if offender. is_empty ( ) {
1584+ return Err ( "[REJECT][TX] equivocation_proof empty_offender" . to_string ( ) ) ;
1585+ }
1586+ if block_a == block_b {
1587+ return Err ( "[REJECT][TX] equivocation_proof identical_blocks" . to_string ( ) ) ;
1588+ }
1589+ if block_a. signature . is_empty ( ) || block_b. signature . is_empty ( ) {
1590+ return Err ( "[REJECT][TX] equivocation_proof missing_signature" . to_string ( ) ) ;
1591+ }
1592+ }
1593+ TransactionType :: VoteEquivocationProof { offender, checkpoint_a, signature_a, checkpoint_b, signature_b } => {
1594+ // Structural check only — the cryptographic + same-round verification (deserialize
1595+ // both checkpoints, index_a == index_b, hashes differ, both consensus-key sigs over
1596+ // QNET_BFT2_VOTE:<hex(hash)> valid vs the offender's registry PK) runs at the
1597+ // integration layer, which holds the consensus PK registry + the Checkpoint type.
1598+ if offender. is_empty ( ) {
1599+ return Err ( "[REJECT][TX] vote_equivocation_proof empty_offender" . to_string ( ) ) ;
1600+ }
1601+ if checkpoint_a == checkpoint_b {
1602+ return Err ( "[REJECT][TX] vote_equivocation_proof identical_checkpoints" . to_string ( ) ) ;
1603+ }
1604+ if signature_a. is_empty ( ) || signature_b. is_empty ( ) {
1605+ return Err ( "[REJECT][TX] vote_equivocation_proof missing_signature" . to_string ( ) ) ;
1606+ }
1607+ }
15241608 }
15251609
15261610 Ok ( ( ) )
@@ -2713,11 +2797,16 @@ impl Transaction {
27132797 }
27142798 }
27152799 }
2800+ // Equivocation slashing proofs: no account-state effect. The penalty
2801+ // (offender → reputation 0 + ban) is applied deterministically in the
2802+ // reputation fold from the committed proof, not in account state.
2803+ TransactionType :: EquivocationProof { .. } => { }
2804+ TransactionType :: VoteEquivocationProof { .. } => { }
27162805 }
27172806
27182807 Ok ( ( ) )
27192808 }
2720-
2809+
27212810 /// Check if transaction qualifies for instant local finalization
27222811 pub fn can_be_locally_finalized ( & self , config : & LocalFinalizationConfig ) -> bool {
27232812 // Small amount transactions get instant finalization
0 commit comments