executor/
vm.rs

1use consensus_types::{block::Block, block_data::BlockType, vote::Vote};
2use diem_crypto::hash::CryptoHash;
3use diem_logger::{error as diem_error, prelude::*};
4use diem_state_view::StateView;
5use diem_types::{
6    account_address::{from_consensus_public_key, AccountAddress},
7    block_info::PivotBlockDecision,
8    contract_event::ContractEvent,
9    epoch_state::EpochState,
10    on_chain_config::new_epoch_event_key,
11    term_state::pos_state_config::{PosStateConfigTrait, POS_STATE_CONFIG},
12    transaction::{
13        authenticator::TransactionAuthenticator, ConflictSignature,
14        DisputePayload, ElectionPayload, RegisterPayload, RetirePayload,
15        SignatureCheckedTransaction, SignedTransaction, Transaction,
16        TransactionOutput, TransactionPayload, TransactionStatus,
17        UpdateVotingPowerPayload,
18    },
19    validator_verifier::ValidatorVerifier,
20    vm_status::{KeptVMStatus, StatusCode, VMStatus},
21};
22
23/// A VM for Conflux PoS chain.
24pub struct PosVM;
25
26impl PosVM {
27    /// Executes a block of transactions and returns output for each one of
28    /// them.
29    pub fn execute_block(
30        transactions: Vec<Transaction>, state_view: &dyn StateView,
31        catch_up_mode: bool,
32    ) -> Result<Vec<TransactionOutput>, VMStatus> {
33        let mut vm_outputs = Vec::new();
34        for transaction in transactions {
35            let output = match transaction {
36                Transaction::BlockMetadata(_) => {
37                    Self::process_block_metadata(state_view)?
38                }
39                Transaction::UserTransaction(trans) => {
40                    let tx = Self::check_signature_for_user_tx(trans)?;
41                    let spec = Spec { catch_up_mode };
42                    Self::process_user_transaction(state_view, &tx, &spec)?
43                }
44                Transaction::GenesisTransaction(events) => {
45                    Self::process_genesis_transaction(events)?
46                }
47            };
48            vm_outputs.push(output);
49        }
50
51        Ok(vm_outputs)
52    }
53}
54
55impl PosVM {
56    fn process_block_metadata(
57        state_view: &dyn StateView,
58    ) -> Result<TransactionOutput, VMStatus> {
59        let mut events = state_view.pos_state().get_unlock_events();
60        diem_debug!("get_unlock_events: {}", events.len());
61
62        let next_view = state_view.pos_state().current_view() + 1;
63        let (term, view_in_term) = POS_STATE_CONFIG.get_term_view(next_view);
64
65        // TODO(lpl): Simplify.
66        if view_in_term == 0 {
67            let (validator_verifier, vrf_seed) =
68                state_view.pos_state().get_committee_at(term).map_err(|e| {
69                    diem_warn!("get_new_committee error: {:?}", e);
70                    VMStatus::Error(StatusCode::CFX_INVALID_TX)
71                })?;
72            let epoch = term + 1;
73            let validator_bytes = bcs::to_bytes(&EpochState::new(
74                epoch,
75                validator_verifier,
76                vrf_seed,
77            ))
78            .unwrap();
79            let contract_event =
80                ContractEvent::new(new_epoch_event_key(), validator_bytes);
81            events.push(contract_event);
82        }
83        Ok(Self::gen_output(events))
84    }
85
86    fn check_signature_for_user_tx(
87        trans: SignedTransaction,
88    ) -> Result<SignatureCheckedTransaction, VMStatus> {
89        // TODO(lpl): Parallel verification.
90        trans.check_signature().map_err(|e| {
91            diem_trace!("invalid transactions signature: e={:?}", e);
92            VMStatus::Error(StatusCode::INVALID_SIGNATURE)
93        })
94    }
95
96    fn process_user_transaction(
97        state_view: &dyn StateView, tx: &SignatureCheckedTransaction,
98        spec: &Spec,
99    ) -> Result<TransactionOutput, VMStatus> {
100        let events = match tx.payload() {
101            TransactionPayload::Election(election_payload) => {
102                election_payload.execute(state_view, tx, spec)?
103            }
104            TransactionPayload::Retire(retire_payload) => {
105                retire_payload.execute(state_view, tx, spec)?
106            }
107            TransactionPayload::PivotDecision(pivot_decision) => {
108                pivot_decision.execute(state_view, tx, spec)?
109            }
110            TransactionPayload::Register(register) => {
111                register.execute(state_view, tx, spec)?
112            }
113            TransactionPayload::UpdateVotingPower(update) => {
114                update.execute(state_view, tx, spec)?
115            }
116            TransactionPayload::Dispute(dispute) => {
117                dispute.execute(state_view, tx, spec)?
118            }
119            _ => return Err(VMStatus::Error(StatusCode::CFX_UNEXPECTED_TX)),
120        };
121
122        Ok(Self::gen_output(events))
123    }
124
125    fn process_genesis_transaction(
126        events: Vec<ContractEvent>,
127    ) -> Result<TransactionOutput, VMStatus> {
128        Ok(Self::gen_output(events))
129    }
130
131    fn gen_output(events: Vec<ContractEvent>) -> TransactionOutput {
132        let status = TransactionStatus::Keep(KeptVMStatus::Executed);
133        TransactionOutput::new(events, 0, status)
134    }
135}
136
137pub struct Spec {
138    pub catch_up_mode: bool,
139}
140
141pub trait ExecutableBuiltinTx {
142    fn execute(
143        &self, state_view: &dyn StateView, tx: &SignatureCheckedTransaction,
144        spec: &Spec,
145    ) -> Result<Vec<ContractEvent>, VMStatus>;
146}
147
148impl ExecutableBuiltinTx for ElectionPayload {
149    fn execute(
150        &self, state_view: &dyn StateView, _tx: &SignatureCheckedTransaction,
151        spec: &Spec,
152    ) -> Result<Vec<ContractEvent>, VMStatus> {
153        if !spec.catch_up_mode {
154            state_view
155                .pos_state()
156                .validate_election(self)
157                .map_err(|e| {
158                    diem_error!("election tx error: {:?}", e);
159                    VMStatus::Error(StatusCode::CFX_INVALID_TX)
160                })?;
161        }
162        Ok(vec![self.to_event()])
163    }
164}
165
166impl ExecutableBuiltinTx for PivotBlockDecision {
167    fn execute(
168        &self, state_view: &dyn StateView, tx: &SignatureCheckedTransaction,
169        spec: &Spec,
170    ) -> Result<Vec<ContractEvent>, VMStatus> {
171        if !spec.catch_up_mode {
172            let authenticator = tx.authenticator();
173            let signature = match authenticator {
174                TransactionAuthenticator::MultiBLS { signature } => {
175                    Ok(signature)
176                }
177                _ => Err(VMStatus::Error(StatusCode::CFX_INVALID_TX)),
178            }?;
179            state_view
180                .pos_state()
181                .validate_pivot_decision(self, signature)
182                .map_err(|e| {
183                    diem_error!("pivot decision tx error: {:?}", e);
184                    VMStatus::Error(StatusCode::CFX_INVALID_TX)
185                })?;
186        }
187        Ok(vec![self.to_event()])
188    }
189}
190
191impl ExecutableBuiltinTx for DisputePayload {
192    fn execute(
193        &self, state_view: &dyn StateView, _tx: &SignatureCheckedTransaction,
194        _spec: &Spec,
195    ) -> Result<Vec<ContractEvent>, VMStatus> {
196        let view = state_view.pos_state().current_view();
197        let offense_epoch = verify_dispute(self, view)
198            .ok_or(VMStatus::Error(StatusCode::CFX_INVALID_TX))?;
199        state_view
200            .pos_state()
201            .validate_dispute(self, offense_epoch)
202            .map_err(|e| {
203                diem_error!("dispute tx error: {:?}", e);
204                VMStatus::Error(StatusCode::CFX_INVALID_TX)
205            })?;
206        Ok(vec![
207            if POS_STATE_CONFIG.cip173_active(view) {
208                self.to_event_v2(offense_epoch)
209            } else {
210                self.to_event()
211            },
212        ])
213    }
214}
215
216macro_rules! impl_builtin_tx_by_gen_events {
217    ( $($name:ident),*  ) => {
218        $(impl ExecutableBuiltinTx for $name {
219            fn execute(&self, _state_view: &dyn StateView,_tx: &SignatureCheckedTransaction,  _spec: &Spec) -> Result<Vec<ContractEvent>, VMStatus> {
220                Ok(vec![self.to_event()])
221            }
222        })*
223    }
224}
225
226// Transactions which just generate events without other process
227impl_builtin_tx_by_gen_events!(
228    RegisterPayload,
229    RetirePayload,
230    UpdateVotingPowerPayload
231);
232
233/// Verify the block is a `Proposal` signed by `address`. The embedded QC is
234/// not checked: its committee signers are unknown to the single-target dispute
235/// verifier.
236fn verify_dispute_proposal(
237    block: &Block, address: AccountAddress, verifier: &ValidatorVerifier,
238) -> bool {
239    match block.block_data().block_type() {
240        BlockType::Proposal { author, .. } => {
241            if *author != address {
242                diem_trace!("Dispute proposal authored by another validator");
243                return false;
244            }
245            match block.signature() {
246                Some(signature) => verifier
247                    .verify(*author, block.block_data(), signature)
248                    .is_ok(),
249                None => {
250                    diem_trace!("Dispute proposal missing proposer signature");
251                    false
252                }
253            }
254        }
255        _ => {
256            diem_trace!("Dispute proposal is not a Proposal block");
257            false
258        }
259    }
260}
261
262/// The epoch the offence claims, or `None` if the evidence is invalid.
263pub fn verify_dispute(dispute: &DisputePayload, view: u64) -> Option<u64> {
264    let computed_address =
265        from_consensus_public_key(&dispute.bls_pub_key, &dispute.vrf_pub_key);
266    if dispute.address != computed_address {
267        diem_trace!("Incorrect address and public keys");
268        return None;
269    }
270    let enforce_conflict = POS_STATE_CONFIG.cip173_active(view);
271    match &dispute.conflicting_votes {
272        ConflictSignature::Proposal((proposal_byte1, proposal_byte2)) => {
273            let proposal1: Block =
274                match bcs::from_bytes(proposal_byte1.as_slice()) {
275                    Ok(proposal) => proposal,
276                    Err(e) => {
277                        diem_trace!("1st proposal encoding error: {:?}", e);
278                        return None;
279                    }
280                };
281            let proposal2: Block =
282                match bcs::from_bytes(proposal_byte2.as_slice()) {
283                    Ok(proposal) => proposal,
284                    Err(e) => {
285                        diem_trace!("2nd proposal encoding error: {:?}", e);
286                        return None;
287                    }
288                };
289            if (proposal1.block_data().epoch()
290                != proposal2.block_data().epoch())
291                || (proposal1.block_data().round()
292                    != proposal2.block_data().round())
293            {
294                diem_trace!("Two proposals are from different rounds");
295                return None;
296            }
297            let temp_verifier = ValidatorVerifier::new_single(
298                dispute.address,
299                dispute.bls_pub_key.clone(),
300                Some(dispute.vrf_pub_key.clone()),
301            );
302            if enforce_conflict {
303                if !verify_dispute_proposal(
304                    &proposal1,
305                    dispute.address,
306                    &temp_verifier,
307                ) || !verify_dispute_proposal(
308                    &proposal2,
309                    dispute.address,
310                    &temp_verifier,
311                ) {
312                    return None;
313                }
314                // `id()` is the hash of `block_data` only (excludes
315                // signature/vrf).
316                if proposal1.id() == proposal2.id() {
317                    diem_trace!("Two proposals are identical");
318                    return None;
319                }
320            } else if proposal1.validate_signature(&temp_verifier).is_err()
321                || proposal2.validate_signature(&temp_verifier).is_err()
322            {
323                return None;
324            }
325            return Some(proposal1.block_data().epoch());
326        }
327        ConflictSignature::Vote((vote_byte1, vote_byte2)) => {
328            let vote1: Vote = match bcs::from_bytes(vote_byte1.as_slice()) {
329                Ok(vote) => vote,
330                Err(e) => {
331                    diem_trace!("1st vote encoding error: {:?}", e);
332                    return None;
333                }
334            };
335            let vote2: Vote = match bcs::from_bytes(vote_byte2.as_slice()) {
336                Ok(vote) => vote,
337                Err(e) => {
338                    diem_trace!("2nd vote encoding error: {:?}", e);
339                    return None;
340                }
341            };
342            if (vote1.vote_data().proposed().epoch()
343                != vote2.vote_data().proposed().epoch())
344                || (vote1.vote_data().proposed().round()
345                    != vote2.vote_data().proposed().round())
346            {
347                diem_trace!("Two votes are from different rounds");
348                return None;
349            }
350            // `new_single` already forces this as a side effect of holding one
351            // member; stated outright, a wider verifier cannot silently let
352            // A's equivocation convict B.
353            if enforce_conflict
354                && (vote1.author() != dispute.address
355                    || vote2.author() != dispute.address)
356            {
357                diem_trace!("Dispute vote authored by another validator");
358                return None;
359            }
360            let temp_verifier = ValidatorVerifier::new_single(
361                dispute.address,
362                dispute.bls_pub_key.clone(),
363                Some(dispute.vrf_pub_key.clone()),
364            );
365            if vote1.verify(&temp_verifier).is_err()
366                || vote2.verify(&temp_verifier).is_err()
367            {
368                diem_trace!("dispute vote verification error: vote1_r={:?} vote2_r={:?}", vote1.verify(&temp_verifier), vote2.verify(&temp_verifier));
369                return None;
370            }
371            // Compare `LedgerInfo` by hash, not serialized bytes: the optional
372            // `timeout_signature` is not part of the `LedgerInfo`.
373            if enforce_conflict
374                && vote1.ledger_info().hash() == vote2.ledger_info().hash()
375            {
376                diem_trace!("Two votes share the same ledger info");
377                return None;
378            }
379            return Some(vote1.vote_data().proposed().epoch());
380        }
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    //! One `#[test]` runs every case, since `POS_STATE_CONFIG` is set-once and
387    //! a second `install_config()` would panic; its body lists them in order.
388    //! Cases state their expectation as `accepted`, `rejected`, `gated`
389    //! (accepted before `cip173_transition_view` and refused after — what the
390    //! fix exists for) or `enabled` (the reverse). All four assert on both
391    //! sides of the transition. A genuine equivocation per branch controls that
392    //! the fixtures are valid; identity cases put the bad item in either
393    //! operand slot, since a bad first item short-circuits the second;
394    //! dedup cases pair items whose bytes differ while the statement the
395    //! target signed stays the same, or is merely re-encoded.
396
397    use super::verify_dispute;
398    use consensus_types::{
399        block::Block, block_data::BlockData, quorum_cert::QuorumCert,
400        vote::Vote, vote_data::VoteData,
401    };
402    use diem_crypto::{hash::CryptoHash, HashValue, ValidCryptoMaterial};
403    use diem_types::{
404        account_address::{from_consensus_public_key, AccountAddress},
405        block_info::BlockInfo,
406        ledger_info::{LedgerInfo, LedgerInfoWithSignatures},
407        term_state::pos_state_config::{PosStateConfig, POS_STATE_CONFIG},
408        transaction::{ConflictSignature, DisputePayload},
409        validator_config::{
410            ConsensusPublicKey, ConsensusSignature, ConsensusVRFProof,
411            ConsensusVRFPublicKey,
412        },
413        validator_signer::ValidatorSigner,
414    };
415    use std::{collections::BTreeMap, convert::TryFrom};
416
417    /// Must start a term — `PosStateConfig::new` rejects anything else.
418    const TRANSITION: u64 = 2 * 60;
419    const BEFORE: u64 = 0;
420
421    /// Every piece of evidence below shares this epoch/round, so the epoch and
422    /// round comparisons never short-circuit ahead of the check under test.
423    const EPOCH: u64 = 1;
424    const ROUND: u64 = 2;
425    const TIMESTAMP: u64 = 1000;
426
427    #[track_caller]
428    fn accepted(evidence: &DisputePayload) {
429        assert_eq!(verify_dispute(evidence, BEFORE), Some(EPOCH));
430        assert_eq!(verify_dispute(evidence, TRANSITION), Some(EPOCH));
431    }
432
433    #[track_caller]
434    fn gated(evidence: &DisputePayload) {
435        assert_eq!(verify_dispute(evidence, BEFORE), Some(EPOCH));
436        assert_eq!(verify_dispute(evidence, TRANSITION), None);
437    }
438
439    /// Evidence the legacy path rejected and CIP-173 accepts: real proposal
440    /// equivocation, which the QC check made unusable before the gate.
441    #[track_caller]
442    fn enabled(evidence: &DisputePayload) {
443        assert_eq!(verify_dispute(evidence, BEFORE), None);
444        assert_eq!(verify_dispute(evidence, TRANSITION), Some(EPOCH));
445    }
446
447    #[track_caller]
448    fn rejected(evidence: &DisputePayload) {
449        assert_eq!(verify_dispute(evidence, BEFORE), None);
450        assert_eq!(verify_dispute(evidence, TRANSITION), None);
451    }
452
453    /// The last argument is `cip173_transition_view`; `M` leaves every other
454    /// transition inert.
455    fn install_config() {
456        const M: u64 = u64::MAX;
457        POS_STATE_CONFIG
458            .set(PosStateConfig::new(
459                60, 1, 1, 0, 0, M, 0, 0, M, M, M, 0, 0, 60, M, M, TRANSITION,
460            ))
461            .expect("POS_STATE_CONFIG already set");
462    }
463
464    /// The validator a dispute accuses. `address` stays derived from the two
465    /// public keys because `verify_dispute` recomputes and compares it.
466    struct Target {
467        address: AccountAddress,
468        bls: ConsensusPublicKey,
469        vrf: ConsensusVRFPublicKey,
470    }
471
472    impl Target {
473        fn of(signer: &ValidatorSigner) -> Self {
474            let bls = signer.public_key();
475            let vrf = signer.vrf_public_key().unwrap();
476            let address = from_consensus_public_key(&bls, &vrf);
477            Self { address, bls, vrf }
478        }
479
480        fn dispute(&self, conflict: ConflictSignature) -> DisputePayload {
481            DisputePayload {
482                address: self.address,
483                bls_pub_key: self.bls.clone(),
484                vrf_pub_key: self.vrf.clone(),
485                conflicting_votes: conflict,
486            }
487        }
488
489        fn votes(&self, v1: &Vote, v2: &Vote) -> DisputePayload {
490            self.dispute(ConflictSignature::Vote((
491                bcs::to_bytes(v1).unwrap(),
492                bcs::to_bytes(v2).unwrap(),
493            )))
494        }
495
496        fn blocks(&self, b1: &Block, b2: &Block) -> DisputePayload {
497            self.raw_blocks(
498                bcs::to_bytes(b1).unwrap(),
499                bcs::to_bytes(b2).unwrap(),
500            )
501        }
502
503        fn raw_blocks(&self, b1: Vec<u8>, b2: Vec<u8>) -> DisputePayload {
504            self.dispute(ConflictSignature::Proposal((b1, b2)))
505        }
506    }
507
508    fn block_info(round: u64, id: HashValue) -> BlockInfo {
509        BlockInfo::new(EPOCH, round, id, HashValue::zero(), 0, 0, None, None)
510    }
511
512    /// Round 0 makes `QuorumCert::verify` short-circuit, so the QC never needs
513    /// real signatures.
514    fn genesis_qc(parent: u8) -> QuorumCert {
515        let bi = block_info(0, HashValue::new([parent; 32]));
516        let vote_data = VoteData::new(bi.clone(), bi.clone());
517        let li = LedgerInfo::new(bi, vote_data.hash());
518        QuorumCert::new(
519            vote_data,
520            LedgerInfoWithSignatures::new(li, BTreeMap::new()),
521        )
522    }
523
524    /// A QC certifying a live round and signed by someone outside the dispute's
525    /// single-target verifier — the shape real evidence carries. Unlike
526    /// [`genesis_qc`] this one makes `QuorumCert::verify` check signatures.
527    fn live_qc(signer: &ValidatorSigner, parent: u8) -> QuorumCert {
528        let certified = block_info(ROUND - 1, HashValue::new([parent; 32]));
529        let vote_data =
530            VoteData::new(certified.clone(), block_info(0, HashValue::zero()));
531        let li = LedgerInfo::new(certified, vote_data.hash());
532        let mut signatures = BTreeMap::new();
533        signatures.insert(Target::of(signer).address, signer.sign(&li));
534        QuorumCert::new(
535            vote_data,
536            LedgerInfoWithSignatures::new(li, signatures),
537        )
538    }
539
540    /// Evidence varies only in the parent it builds on — never in the proposed
541    /// block or the timestamp — so a dedup keyed on either of those instead of
542    /// on the whole signed statement cannot survive the positive controls.
543    fn make_vote(
544        signer: &ValidatorSigner, author: AccountAddress, parent: u8,
545    ) -> Vote {
546        let vote_data = VoteData::new(
547            block_info(ROUND, HashValue::new([0xAA; 32])),
548            block_info(ROUND - 1, HashValue::new([parent; 32])),
549        );
550        let li = LedgerInfo::new(BlockInfo::empty(), HashValue::zero());
551        Vote::new(vote_data, author, li, signer)
552    }
553
554    fn proposal_data(author: AccountAddress, parent: u8) -> BlockData {
555        proposal_data_with_qc(author, genesis_qc(parent))
556    }
557
558    fn proposal_data_with_qc(
559        author: AccountAddress, qc: QuorumCert,
560    ) -> BlockData {
561        BlockData::new_proposal(vec![], author, ROUND, TIMESTAMP, qc)
562    }
563
564    fn make_proposal(
565        signer: &ValidatorSigner, author: AccountAddress, parent: u8,
566    ) -> Block {
567        Block::new_proposal_from_block_data(
568            proposal_data(author, parent),
569            signer,
570        )
571    }
572
573    /// No constructor yields a `Proposal` block with a missing or
574    /// non-canonically encoded signature, so those are built as wire bytes;
575    /// the call site asserts this matches a real `Block` encoding.
576    fn proposal_wire_bytes(
577        data: &BlockData, signature: Option<&[u8]>,
578    ) -> Vec<u8> {
579        bcs::to_bytes(&(
580            data,
581            signature.map(<[u8]>::to_vec),
582            None::<(u64, ConsensusVRFProof)>,
583        ))
584        .unwrap()
585    }
586
587    /// Compressed G2 is the 96-byte x coordinate flagged with `0x80`, plus
588    /// `0x20` when y is the larger root; try both rather than recompute it.
589    fn compressed_signature_bytes(signature: &ConsensusSignature) -> Vec<u8> {
590        let uncompressed = ValidCryptoMaterial::to_bytes(signature);
591        for sort_flag in &[0x00u8, 0x20u8] {
592            let mut candidate = uncompressed[..96].to_vec();
593            candidate[0] |= 0x80 | sort_flag;
594            if let Ok(decoded) =
595                ConsensusSignature::try_from(candidate.as_slice())
596            {
597                if decoded == *signature {
598                    return candidate;
599                }
600            }
601        }
602        panic!("no compressed encoding decodes back to the same signature");
603    }
604
605    /// `verify_dispute` never checks the VRF proof, so any bytes work.
606    fn dummy_vrf_proof() -> ConsensusVRFProof {
607        ConsensusVRFProof::try_from(&[][..]).unwrap()
608    }
609
610    #[test]
611    fn verify_dispute_conflict_gating() {
612        install_config();
613
614        let signer = ValidatorSigner::random([7u8; 32]);
615        let other = ValidatorSigner::random([9u8; 32]);
616        let target = Target::of(&signer);
617
618        vote_conflict_gating(&signer, &target);
619        proposal_conflict_gating(&signer, &target);
620        vote_identity_is_bound_to_the_accused(&signer, &other, &target);
621        proposal_identity_is_bound_to_the_accused(&signer, &other, &target);
622        unsigned_proposal_is_not_evidence(&signer, &target);
623        fields_outside_block_data_cannot_forge_a_conflict(&signer, &target);
624        live_qc_evidence_only_works_after_the_gate(&signer, &other, &target);
625    }
626
627    fn vote_conflict_gating(signer: &ValidatorSigner, target: &Target) {
628        let va = make_vote(signer, target.address, 1);
629        let vb = make_vote(signer, target.address, 2);
630        // Equivocation over one proposed block: the key is the signed
631        // `LedgerInfo`, not the proposed id.
632        assert_eq!(
633            va.vote_data().proposed().id(),
634            vb.vote_data().proposed().id()
635        );
636        assert_ne!(va.ledger_info().hash(), vb.ledger_info().hash());
637        accepted(&target.votes(&va, &vb));
638        gated(&target.votes(&va, &va));
639
640        // Adding the timeout signature changes the bytes but not the
641        // `LedgerInfo` it signs, so byte inequality is not conflict.
642        let mut timeout = va.clone();
643        timeout.add_timeout_signature(signer.sign(&timeout.timeout()));
644        assert_ne!(
645            bcs::to_bytes(&va).unwrap(),
646            bcs::to_bytes(&timeout).unwrap()
647        );
648        assert_eq!(va.ledger_info().hash(), timeout.ledger_info().hash());
649        gated(&target.votes(&va, &timeout));
650    }
651
652    fn proposal_conflict_gating(signer: &ValidatorSigner, target: &Target) {
653        let pa = make_proposal(signer, target.address, 1);
654        let pb = make_proposal(signer, target.address, 2);
655        // Equivocation at one timestamp: the key is the whole `block_data`.
656        assert_eq!(pa.timestamp_usecs(), pb.timestamp_usecs());
657        assert_ne!(pa.id(), pb.id());
658        accepted(&target.blocks(&pa, &pb));
659        gated(&target.blocks(&pa, &pa));
660
661        // NIL blocks carry no proposer signature at all.
662        let na = Block::new_nil(ROUND, genesis_qc(1));
663        let nb = Block::new_nil(ROUND, genesis_qc(2));
664        gated(&target.blocks(&na, &nb));
665    }
666
667    fn vote_identity_is_bound_to_the_accused(
668        signer: &ValidatorSigner, other: &ValidatorSigner, target: &Target,
669    ) {
670        let impostor = Target::of(other).address;
671        let genuine = make_vote(signer, target.address, 1);
672        // Another validator's own equivocation, replayed against the target.
673        let foreign_a = make_vote(other, impostor, 1);
674        let foreign_b = make_vote(other, impostor, 2);
675        // `author` is the target but the signature is the impostor's.
676        let forged = make_vote(other, target.address, 2);
677        let misnamed = make_vote(signer, impostor, 2);
678
679        rejected(&target.votes(&foreign_a, &foreign_b));
680        rejected(&target.votes(&genuine, &foreign_b));
681        rejected(&target.votes(&forged, &genuine));
682        rejected(&target.votes(&genuine, &forged));
683        rejected(&target.votes(&misnamed, &genuine));
684        rejected(&target.votes(&genuine, &misnamed));
685    }
686
687    fn proposal_identity_is_bound_to_the_accused(
688        signer: &ValidatorSigner, other: &ValidatorSigner, target: &Target,
689    ) {
690        let impostor = Target::of(other).address;
691        let genuine = make_proposal(signer, target.address, 1);
692        // Another validator's own equivocation, replayed against the target.
693        let foreign_a = make_proposal(other, impostor, 1);
694        let foreign_b = make_proposal(other, impostor, 2);
695        // `author` is the target but the signature is the impostor's.
696        let forged = make_proposal(other, target.address, 2);
697        // Signed by the target but naming someone else: only the author
698        // comparison rejects this one.
699        let misnamed = make_proposal(signer, impostor, 2);
700
701        rejected(&target.blocks(&foreign_a, &foreign_b));
702        rejected(&target.blocks(&genuine, &foreign_b));
703        rejected(&target.blocks(&forged, &genuine));
704        rejected(&target.blocks(&genuine, &forged));
705        rejected(&target.blocks(&misnamed, &genuine));
706        rejected(&target.blocks(&genuine, &misnamed));
707    }
708
709    fn unsigned_proposal_is_not_evidence(
710        signer: &ValidatorSigner, target: &Target,
711    ) {
712        let unsigned =
713            proposal_wire_bytes(&proposal_data(target.address, 2), None);
714        let decoded: Block = bcs::from_bytes(&unsigned).unwrap();
715        assert!(decoded.signature().is_none());
716        assert_eq!(decoded.author(), Some(target.address));
717
718        // A different parent gives the genuine companion a different id, so the
719        // missing signature is the only thing left to reject the pair.
720        let genuine =
721            bcs::to_bytes(&make_proposal(signer, target.address, 1)).unwrap();
722        rejected(&target.raw_blocks(unsigned.clone(), genuine.clone()));
723        rejected(&target.raw_blocks(genuine, unsigned));
724    }
725
726    /// `signature` and `vrf_nonce_and_proof` sit on `Block`, outside the
727    /// signed `block_data`, so re-encoding either forges a second block
728    /// without the target's key: a G2 point has both a 96-byte compressed and
729    /// a 192-byte uncompressed form and `g2_from_slice` accepts either, while
730    /// serialization always emits the uncompressed one.
731    fn fields_outside_block_data_cannot_forge_a_conflict(
732        signer: &ValidatorSigner, target: &Target,
733    ) {
734        let data = proposal_data(target.address, 1);
735        let signature = signer.sign(&data);
736        let copy = |vrf: Option<(u64, ConsensusVRFProof)>| {
737            Block::new_proposal_from_block_data_and_signature(
738                data.clone(),
739                signature.clone(),
740                vrf,
741            )
742        };
743        let plain = copy(None);
744        let with_proof = copy(Some((1, dummy_vrf_proof())));
745        let other_nonce = copy(Some((2, dummy_vrf_proof())));
746
747        // Every copy carries the target's own valid signature, so all the
748        // identity checks pass and only the equal ids reject them.
749        assert_ne!(
750            bcs::to_bytes(&plain).unwrap(),
751            bcs::to_bytes(&with_proof).unwrap()
752        );
753        assert_eq!(plain.id(), with_proof.id());
754        assert_eq!(with_proof.id(), other_nonce.id());
755        gated(&target.blocks(&plain, &with_proof));
756        gated(&target.blocks(&with_proof, &other_nonce));
757
758        // The signature variant needs raw wire bytes; no constructor emits a
759        // non-canonical encoding. The uncompressed hand-built form must equal
760        // the real one, or the compressed one would test a different encoding.
761        let canonical = ValidCryptoMaterial::to_bytes(&signature);
762        let compressed = compressed_signature_bytes(&signature);
763        assert_ne!(canonical, compressed);
764        let uncompressed = proposal_wire_bytes(&data, Some(&canonical));
765        assert_eq!(uncompressed, bcs::to_bytes(&plain).unwrap());
766        let alternative = proposal_wire_bytes(&data, Some(&compressed));
767        let decoded: Block = bcs::from_bytes(&alternative).unwrap();
768        assert_eq!(decoded.id(), plain.id());
769        gated(&target.raw_blocks(uncompressed, alternative));
770    }
771
772    /// Real proposal evidence carries a committee-signed QC, and the legacy
773    /// path verified it against the single-target verifier, so genuine
774    /// proposal equivocation was unusable until the gate stopped looking at
775    /// the QC. Every other proposal case keeps a round-0 QC, which
776    /// `QuorumCert::verify` short-circuits, precisely so that their own checks
777    /// stay the operative ones on both sides of the transition.
778    fn live_qc_evidence_only_works_after_the_gate(
779        signer: &ValidatorSigner, other: &ValidatorSigner, target: &Target,
780    ) {
781        let proposal = |parent: u8| {
782            Block::new_proposal_from_block_data(
783                proposal_data_with_qc(target.address, live_qc(other, parent)),
784                signer,
785            )
786        };
787        let pa = proposal(1);
788        let pb = proposal(2);
789        assert_ne!(pa.id(), pb.id());
790        enabled(&target.blocks(&pa, &pb));
791    }
792}