use rialo_s_pubkey::Pubkey;
use super::{
super::{
attestation_framework::{
Attestation, CertificateAddAttestationError, CertificateAttestationConsistencyError,
},
epoch::{EpochChangeConfig, EpochIdentifier, ValidatorInfo},
test_helpers::{test_keys::*, test_records::create_test_handover_record},
BLAKE3_HASH_SIZE,
},
chain::HandoverChain,
errors::HandoverChainError,
record::HANDOVER_ATTESTATION_INTENT_PREFIX,
*,
};
fn chain_unsigned_genesis_for_test(record: HandoverRecord) -> HandoverChain {
let mut chain = HandoverChain::new();
chain.insert_next_handover(record).unwrap();
chain
}
fn test_validator(seed: u64, stake: u64) -> (ValidatorInfo, crate::ProtocolKeyPair) {
let (authority_key, protocol_keypair, network_key) = create_test_keys_with_keypair(seed);
let protocol_key = protocol_keypair.public();
(
ValidatorInfo {
stake,
consensus_address: "/ip4/127.0.0.1/udp/10100".parse().unwrap(),
state_sync_address: "/ip4/127.0.0.1/udp/20100".parse().unwrap(),
hostname: format!("v-{seed}"),
authority_key,
protocol_key,
network_key,
signing_key: Pubkey::new_unique(),
},
protocol_keypair,
)
}
#[test]
fn test_handover_record_serialization() {
let epoch_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![],
consensus_config: None,
};
let record = HandoverRecord::new(
42,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
epoch_config,
[0xABu8; BLAKE3_HASH_SIZE],
);
let serialized = serde_json::to_string(&record).expect("serialization should succeed");
let deserialized: HandoverRecord =
serde_json::from_str(&serialized).expect("deserialization should succeed");
assert_eq!(deserialized.block_index(), 42);
assert_eq!(deserialized.source_epoch(), EpochIdentifier::new(0));
assert_eq!(deserialized.dest_epoch(), EpochIdentifier::new(1));
assert_eq!(
*deserialized.previous_handover_hash(),
[0xABu8; BLAKE3_HASH_SIZE]
);
}
#[test]
fn test_handover_record_digest_stability() {
let record1 = create_test_handover_record(42, 0, 1, [0xABu8; 32]);
let record2 = record1.clone();
assert_eq!(record1.record_digest(), record2.record_digest());
let record3 = HandoverRecord::new(
43,
record1.source_epoch(),
record1.dest_epoch(),
record1.epoch_config().clone(),
*record1.previous_handover_hash(),
);
assert_ne!(record1.record_digest(), record3.record_digest());
let record4 = HandoverRecord::new(
record1.block_index(),
record1.source_epoch(),
record1.dest_epoch(),
record1.epoch_config().clone(),
[1u8; BLAKE3_HASH_SIZE],
);
assert_ne!(record1.record_digest(), record4.record_digest());
}
#[test]
fn test_handover_record_genesis() {
let epoch_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![],
consensus_config: None,
};
let genesis = HandoverRecord::genesis(EpochIdentifier::new(0), epoch_config.clone());
assert_eq!(genesis.block_index(), 0);
assert_eq!(genesis.source_epoch(), EpochIdentifier::new(0));
assert_eq!(genesis.dest_epoch(), EpochIdentifier::new(0));
assert_eq!(*genesis.previous_handover_hash(), [0u8; BLAKE3_HASH_SIZE]);
assert_eq!(*genesis.epoch_config(), epoch_config);
}
#[test]
fn test_handover_record_is_genesis() {
let epoch_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![],
consensus_config: None,
};
let genesis = HandoverRecord::genesis(EpochIdentifier::new(0), epoch_config.clone());
assert!(genesis.is_genesis());
let handover_0_to_1 = HandoverRecord::new(
100,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
epoch_config.clone(),
[0u8; BLAKE3_HASH_SIZE],
);
assert!(!handover_0_to_1.is_genesis());
let epochs_zero_but_wrong_block = HandoverRecord::new(
200,
EpochIdentifier::new(0),
EpochIdentifier::new(0),
epoch_config.clone(),
[0u8; BLAKE3_HASH_SIZE],
);
assert!(!epochs_zero_but_wrong_block.is_genesis());
let handover_1_to_1 = HandoverRecord::new(
200,
EpochIdentifier::new(1),
EpochIdentifier::new(1),
epoch_config.clone(),
[0u8; BLAKE3_HASH_SIZE],
);
assert!(!handover_1_to_1.is_genesis());
let handover_1_to_2 = HandoverRecord::new(
300,
EpochIdentifier::new(1),
EpochIdentifier::new(2),
epoch_config,
[0u8; BLAKE3_HASH_SIZE],
);
assert!(!handover_1_to_2.is_genesis());
let epoch_config_nonzero = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![],
consensus_config: None,
};
let genesis_shape_wrong_config = HandoverRecord::new(
0,
EpochIdentifier::new(0),
EpochIdentifier::new(0),
epoch_config_nonzero,
[0u8; BLAKE3_HASH_SIZE],
);
assert!(
!genesis_shape_wrong_config.is_genesis(),
"epoch_config must be degenerate (current/new == 0) for genesis"
);
}
#[test]
fn test_handover_record_digest_determinism() {
let a = create_test_handover_record(100, 0, 1, [0xABu8; 32]);
let b = create_test_handover_record(100, 0, 1, [0xABu8; 32]);
assert_eq!(
a.record_digest(),
b.record_digest(),
"identical HandoverRecords must produce identical digests"
);
}
#[test]
fn test_handover_record_digest_field_sensitivity() {
let base = create_test_handover_record(100, 0, 1, [0xABu8; 32]);
let different_index = HandoverRecord::new(
200,
base.source_epoch(),
base.dest_epoch(),
base.epoch_config().clone(),
*base.previous_handover_hash(),
);
assert_ne!(
base.record_digest(),
different_index.record_digest(),
"different block_index must produce different digests"
);
let different_source = HandoverRecord::new(
base.block_index(),
EpochIdentifier::new(5),
base.dest_epoch(),
base.epoch_config().clone(),
*base.previous_handover_hash(),
);
assert_ne!(
base.record_digest(),
different_source.record_digest(),
"different source_epoch must produce different digests"
);
let different_dest = HandoverRecord::new(
base.block_index(),
base.source_epoch(),
EpochIdentifier::new(99),
base.epoch_config().clone(),
*base.previous_handover_hash(),
);
assert_ne!(
base.record_digest(),
different_dest.record_digest(),
"different dest_epoch must produce different digests"
);
let different_prev_hash = HandoverRecord::new(
base.block_index(),
base.source_epoch(),
base.dest_epoch(),
base.epoch_config().clone(),
[0xCDu8; BLAKE3_HASH_SIZE],
);
assert_ne!(
base.record_digest(),
different_prev_hash.record_digest(),
"different previous_handover_hash must produce different digests"
);
}
#[test]
fn test_handover_attestation_intent_prefix_matches_shared_crypto() {
use shared_crypto::intent::{AppId, IntentScope, IntentVersion};
assert_eq!(
HANDOVER_ATTESTATION_INTENT_PREFIX,
[
IntentScope::HandoverAttestation as u8,
IntentVersion::V0 as u8,
AppId::Consensus as u8,
],
"HANDOVER_ATTESTATION_INTENT_PREFIX is out of sync with shared-crypto enums"
);
}
#[test]
fn test_handover_attestation_signing_message_wire_compatible_with_bcs() {
use shared_crypto::intent::{Intent, IntentMessage, IntentScope};
let record = create_test_handover_record(42, 0, 1, [0xABu8; 32]);
let digest = record.record_digest();
let generic_msg = Attestation::<HandoverRecord>::signing_message(&digest);
let intent_msg = IntentMessage::new(
Intent::consensus_app(IntentScope::HandoverAttestation),
digest,
);
let bcs_bytes = bcs::to_bytes(&intent_msg).expect("BCS serialization should succeed");
assert_eq!(
generic_msg.as_slice(),
bcs_bytes.as_slice(),
"signing_message() must be byte-identical to bcs(IntentMessage(HandoverAttestation, digest))"
);
}
#[test]
fn test_handover_attestation_sign_verify_roundtrip() {
let record = create_test_handover_record(100, 0, 1, [0xABu8; 32]);
let (authority_key, protocol_keypair, _) = create_test_keys_with_keypair(800);
let attestation =
Attestation::<HandoverRecord>::sign(&record, &protocol_keypair, &authority_key)
.expect("sign() should succeed");
attestation
.verify_internal_consistency()
.expect("freshly signed HandoverAttestation should verify");
assert_eq!(*attestation.record_digest(), record.record_digest());
assert_eq!(*attestation.authority_key(), authority_key);
assert_eq!(*attestation.protocol_key(), protocol_keypair.public());
}
#[test]
fn test_handover_certificate_validate_attestation_consistency_success() {
let (v0, kp0) = test_validator(810, 50);
let (v1, kp1) = test_validator(811, 50);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone(), v1.clone()],
consensus_config: None,
};
let genesis = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let next_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![],
consensus_config: None,
};
let handover_record = HandoverRecord::new(
100,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
next_config,
genesis.record_digest(),
);
let att1 = Attestation::<HandoverRecord>::sign(&handover_record, &kp0, &v0.authority_key)
.expect("sign() should succeed");
let att2 = Attestation::<HandoverRecord>::sign(&handover_record, &kp1, &v1.authority_key)
.expect("sign() should succeed");
let mut cert = HandoverCertificate::new(handover_record);
let chain = chain_unsigned_genesis_for_test(genesis);
cert.add_attestation(att1, &chain).expect("add att1");
cert.add_attestation(att2, &chain).expect("add att2");
assert!(
cert.validate_attestation_consistency().is_ok(),
"all attestations binding same record should pass"
);
}
#[test]
fn test_handover_certificate_validate_attestation_consistency_mismatch() {
let record = create_test_handover_record(100, 0, 1, [0xABu8; 32]);
let other_record = create_test_handover_record(200, 0, 1, [0xABu8; 32]);
let (auth_key1, kp1, _) = create_test_keys_with_keypair(820);
let (auth_key2, kp2, _) = create_test_keys_with_keypair(821);
let att1 = Attestation::<HandoverRecord>::sign(&record, &kp1, &auth_key1)
.expect("sign() should succeed");
let att2 = Attestation::<HandoverRecord>::sign(&other_record, &kp2, &auth_key2)
.expect("sign() should succeed");
let cert = HandoverCertificate::from_record_and_attestations(record, vec![att1, att2]);
assert_eq!(
cert.validate_attestation_consistency().unwrap_err(),
CertificateAttestationConsistencyError::MismatchedRecordDigest {
cert_type: "HandoverCertificate",
}
);
}
#[test]
fn test_handover_certificate_serialization_roundtrip() {
let (v0, kp0) = test_validator(830, 50);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone()],
consensus_config: None,
};
let genesis = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let next_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![],
consensus_config: None,
};
let record = HandoverRecord::new(
100,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
next_config,
genesis.record_digest(),
);
let att1 = Attestation::<HandoverRecord>::sign(&record, &kp0, &v0.authority_key)
.expect("sign() should succeed");
let mut cert = HandoverCertificate::new(record.clone());
let chain = chain_unsigned_genesis_for_test(genesis);
cert.add_attestation(att1, &chain).expect("add attestation");
let serialized = serde_json::to_string(&cert).expect("serialization should succeed");
let deserialized: HandoverCertificate =
serde_json::from_str(&serialized).expect("deserialization should succeed");
assert_eq!(*deserialized.handover_record(), record);
assert_eq!(deserialized.attestations().len(), 1);
assert!(deserialized.validate_attestation_consistency().is_ok());
}
#[test]
fn test_handover_record_bcs_roundtrip_new_wire_breaking_vs_legacy_handover_chain_digest() {
let record = create_test_handover_record(10, 0, 1, [0xABu8; 32]);
let bytes = bcs::to_bytes(&record).expect("BCS encode HandoverRecord");
let back: HandoverRecord = bcs::from_bytes(&bytes).expect("BCS decode HandoverRecord");
assert_eq!(back, record);
let legacy_unprefixed_handover_digest = {
let mut hasher = blake3::Hasher::new();
hasher.update(&record.block_index().to_le_bytes());
hasher.update(&record.source_epoch().to_le_bytes());
hasher.update(&record.dest_epoch().to_le_bytes());
hasher.update(&record.epoch_config().deterministic_hash());
hasher.update(record.previous_handover_hash());
*hasher.finalize().as_bytes()
};
assert_ne!(
record.record_digest(),
legacy_unprefixed_handover_digest,
"HandoverRecord digest is domain-separated; legacy unprefixed digest is not — no cross-version claim"
);
}
#[test]
fn test_handover_quorum_certificate_bcs_roundtrip() {
let (v0, kp0) = test_validator(900, 50);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone()],
consensus_config: None,
};
let genesis = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let next_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![],
consensus_config: None,
};
let record = HandoverRecord::new(
5,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
next_config,
genesis.record_digest(),
);
let att = Attestation::<HandoverRecord>::sign(&record, &kp0, &v0.authority_key)
.expect("sign handover attestation");
let mut cert = HandoverCertificate::new(record.clone());
let chain = chain_unsigned_genesis_for_test(genesis);
cert.add_attestation(att, &chain).expect("add attestation");
let bytes = bcs::to_bytes(&cert).expect("BCS encode quorum HandoverCertificate");
let back: HandoverCertificate =
bcs::from_bytes(&bytes).expect("BCS decode quorum HandoverCertificate");
assert!(back.validate_attestation_consistency().is_ok());
assert_eq!(*back.handover_record(), record);
}
#[test]
fn test_handover_attestation_bcs_roundtrip() {
let record = create_test_handover_record(7, 1, 2, [0xABu8; 32]);
let (ak, kp, _) = create_test_keys_with_keypair(901);
let att =
Attestation::<HandoverRecord>::sign(&record, &kp, &ak).expect("sign handover attestation");
let bytes = bcs::to_bytes(&att).expect("BCS encode HandoverAttestation");
let back: HandoverAttestation =
bcs::from_bytes(&bytes).expect("BCS decode HandoverAttestation");
back.verify_internal_consistency()
.expect("BCS roundtripped attestation should verify");
}
#[test]
fn test_handover_certificate_add_attestation_success() {
let (v0, kp0) = test_validator(2000, 50);
let (v1, _kp1) = test_validator(2001, 50);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone(), v1],
consensus_config: None,
};
let prev = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let next_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![],
consensus_config: None,
};
let handover_record = HandoverRecord::new(
1,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
next_config,
prev.record_digest(),
);
let att = Attestation::<HandoverRecord>::sign(&handover_record, &kp0, &v0.authority_key)
.expect("sign");
let mut cert = HandoverCertificate::new(handover_record);
let chain = chain_unsigned_genesis_for_test(prev);
assert!(cert.add_attestation(att, &chain).is_ok());
assert_eq!(cert.attestations().len(), 1);
}
#[test]
fn test_handover_certificate_add_attestation_rejects_missing_source_epoch_in_chain() {
let genesis = HandoverRecord::genesis(
EpochIdentifier::new(0),
EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![],
consensus_config: None,
},
);
let chain = chain_unsigned_genesis_for_test(genesis);
let handover_record = create_test_handover_record(2, 5, 6, [0xABu8; 32]);
let (ak, kp, _) = create_test_keys_with_keypair(2010);
let att = Attestation::<HandoverRecord>::sign(&handover_record, &kp, &ak).expect("sign");
let mut cert = HandoverCertificate::new(handover_record);
assert_eq!(
cert.add_attestation(att, &chain).unwrap_err(),
CertificateAddAttestationError::MissingEpochInChain(EpochIdentifier::new(5))
);
}
#[test]
fn test_handover_certificate_add_attestation_rejects_unknown_signer() {
let (v0, _kp0) = test_validator(2100, 50);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0],
consensus_config: None,
};
let prev = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let next_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![],
consensus_config: None,
};
let handover_record = HandoverRecord::new(
1,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
next_config,
prev.record_digest(),
);
let (wrong_auth, wrong_kp, _) = create_test_keys_with_keypair(2101);
let att = Attestation::<HandoverRecord>::sign(&handover_record, &wrong_kp, &wrong_auth)
.expect("sign");
let mut cert = HandoverCertificate::new(handover_record);
let chain = chain_unsigned_genesis_for_test(prev);
assert!(cert.add_attestation(att, &chain).is_err());
}
#[test]
fn test_handover_certificate_add_attestation_rejects_protocol_key_mismatch() {
let (v0, _kp0) = test_validator(2150, 50);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone()],
consensus_config: None,
};
let prev = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let next_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![],
consensus_config: None,
};
let handover_record = HandoverRecord::new(
1,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
next_config,
prev.record_digest(),
);
let (_other_auth, other_kp, _) = create_test_keys_with_keypair(2151);
let att = Attestation::<HandoverRecord>::sign(&handover_record, &other_kp, &v0.authority_key)
.expect("sign");
let mut cert = HandoverCertificate::new(handover_record);
let chain = chain_unsigned_genesis_for_test(prev);
assert_eq!(
cert.add_attestation(att, &chain).unwrap_err(),
CertificateAddAttestationError::ProtocolKeyMismatch
);
}
#[test]
fn test_handover_certificate_add_attestation_rejects_duplicate_authority() {
let (v0, kp0) = test_validator(2200, 50);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone()],
consensus_config: None,
};
let prev = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let next_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![],
consensus_config: None,
};
let handover_record = HandoverRecord::new(
1,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
next_config,
prev.record_digest(),
);
let att = Attestation::<HandoverRecord>::sign(&handover_record, &kp0, &v0.authority_key)
.expect("sign");
let mut cert = HandoverCertificate::new(handover_record.clone());
let chain = chain_unsigned_genesis_for_test(prev.clone());
cert.add_attestation(att.clone(), &chain)
.expect("first add");
assert_eq!(
cert.add_attestation(att, &chain).unwrap_err(),
CertificateAddAttestationError::DuplicateAttestation
);
}
#[test]
fn test_handover_certificate_add_attestation_rejects_internally_inconsistent() {
let (v0, kp0) = test_validator(2250, 50);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone()],
consensus_config: None,
};
let prev = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let next_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![],
consensus_config: None,
};
let handover_record = HandoverRecord::new(
1,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
next_config,
prev.record_digest(),
);
let mut att = Attestation::<HandoverRecord>::sign(&handover_record, &kp0, &v0.authority_key)
.expect("sign");
att.tamper_signature(0, 0xFF);
let mut cert = HandoverCertificate::new(handover_record);
let chain = chain_unsigned_genesis_for_test(prev);
let err = cert.add_attestation(att, &chain).unwrap_err();
assert!(
matches!(err, CertificateAddAttestationError::InvalidAttestation(_)),
"expected InvalidAttestation, got {err:?}"
);
}
#[test]
fn test_handover_certificate_is_complete_stake_threshold() {
let (v0, kp0) = test_validator(2300, 10);
let (v1, kp1) = test_validator(2301, 10);
let (v2, kp2) = test_validator(2302, 10);
let committee = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![v0.clone(), v1.clone(), v2.clone()],
consensus_config: None,
};
let genesis = HandoverRecord::genesis(
EpochIdentifier::new(0),
EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![],
consensus_config: None,
},
);
let previous_record = HandoverRecord::new(
1,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
committee.clone(),
genesis.record_digest(),
);
let mut chain = chain_unsigned_genesis_for_test(genesis);
chain.insert_next_handover(previous_record.clone()).unwrap();
let prev = previous_record.record_digest();
let handover_record = create_test_handover_record(99, 1, 2, prev);
let att0 =
Attestation::<HandoverRecord>::sign(&handover_record, &kp0, &v0.authority_key).unwrap();
let att1 =
Attestation::<HandoverRecord>::sign(&handover_record, &kp1, &v1.authority_key).unwrap();
let mut cert_two = HandoverCertificate::new(handover_record.clone());
cert_two
.add_attestation(att0, &chain)
.expect("add att0 for cert_two");
cert_two
.add_attestation(att1, &chain)
.expect("add att1 for cert_two");
assert!(cert_two.is_complete(&chain), "20 stake > threshold(30)=10");
let att2_only =
Attestation::<HandoverRecord>::sign(&handover_record, &kp2, &v2.authority_key).unwrap();
let mut cert_one = HandoverCertificate::new(handover_record.clone());
cert_one
.add_attestation(att2_only, &chain)
.expect("add att for cert_one");
assert!(
!cert_one.is_complete(&chain),
"10 stake is not > threshold(30)=10"
);
}
#[test]
fn test_genesis_certificate_is_complete_requires_all_committee_members() {
let (v0, kp0) = test_validator(2600, 10);
let (v1, kp1) = test_validator(2601, 10);
let (v2, kp2) = test_validator(2602, 10);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone(), v1.clone(), v2.clone()],
consensus_config: None,
};
let genesis_record = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let chain = chain_unsigned_genesis_for_test(genesis_record.clone());
let mut cert_partial = HandoverCertificate::new(genesis_record.clone());
cert_partial
.add_attestation(
Attestation::<HandoverRecord>::sign(&genesis_record, &kp0, &v0.authority_key).unwrap(),
&chain,
)
.unwrap();
cert_partial
.add_attestation(
Attestation::<HandoverRecord>::sign(&genesis_record, &kp1, &v1.authority_key).unwrap(),
&chain,
)
.unwrap();
assert!(
!cert_partial.is_complete(&chain),
"genesis must not be complete until every committee member has attested"
);
let mut cert_full = HandoverCertificate::new(genesis_record.clone());
cert_full
.add_attestation(
Attestation::<HandoverRecord>::sign(&genesis_record, &kp0, &v0.authority_key).unwrap(),
&chain,
)
.unwrap();
cert_full
.add_attestation(
Attestation::<HandoverRecord>::sign(&genesis_record, &kp1, &v1.authority_key).unwrap(),
&chain,
)
.unwrap();
cert_full
.add_attestation(
Attestation::<HandoverRecord>::sign(&genesis_record, &kp2, &v2.authority_key).unwrap(),
&chain,
)
.unwrap();
assert!(cert_full.is_complete(&chain));
}
#[test]
fn test_handover_certificate_is_complete_genesis_requires_all_committee_members() {
let (v0, kp0) = test_validator(2400, 50);
let (v1, kp1) = test_validator(2401, 50);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone(), v1.clone()],
consensus_config: None,
};
let genesis_record = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let att0 =
Attestation::<HandoverRecord>::sign(&genesis_record, &kp0, &v0.authority_key).unwrap();
let att1 =
Attestation::<HandoverRecord>::sign(&genesis_record, &kp1, &v1.authority_key).unwrap();
let chain = chain_unsigned_genesis_for_test(genesis_record.clone());
let mut cert = HandoverCertificate::new(genesis_record.clone());
cert.add_attestation(att0, &chain).expect("att0");
cert.add_attestation(att1, &chain).expect("att1");
assert!(cert.is_genesis());
assert!(cert.is_complete(&chain));
let mut cert_one = HandoverCertificate::new(genesis_record.clone());
cert_one
.add_attestation(
Attestation::<HandoverRecord>::sign(&genesis_record, &kp0, &v0.authority_key).unwrap(),
&chain,
)
.expect("single att");
assert!(cert_one.is_genesis(), "is_genesis is record shape only");
assert!(
!cert_one.is_complete(&chain),
"genesis quorum requires every committee member"
);
}
#[test]
fn test_handover_certificate_is_genesis_false_when_record_not_genesis_shape() {
let (v0, _kp0) = test_validator(2500, 100);
let cfg = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![v0.clone()],
consensus_config: None,
};
let record = HandoverRecord::new(
10,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
cfg,
[1u8; BLAKE3_HASH_SIZE],
);
let cert = HandoverCertificate::new(record);
assert!(!cert.is_genesis());
}
#[test]
fn test_new_from_genesis_accepts_fully_signed_genesis_certificate() {
let (v0, kp0) = test_validator(2700, 10);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone()],
consensus_config: None,
};
let genesis_record = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let scratch = chain_unsigned_genesis_for_test(genesis_record.clone());
let mut cert = HandoverCertificate::new(genesis_record.clone());
let att =
Attestation::<HandoverRecord>::sign(&genesis_record, &kp0, &v0.authority_key).unwrap();
cert.add_attestation(att, &scratch).unwrap();
let chain = HandoverChain::new_from_genesis(cert).unwrap();
assert_eq!(chain.len(), 1);
assert!(chain[0].is_genesis());
}
#[test]
fn test_new_from_genesis_rejects_partially_signed_multi_validator_genesis() {
let (v0, kp0) = test_validator(2800, 10);
let (v1, _kp1) = test_validator(2801, 10);
let (v2, _kp2) = test_validator(2802, 10);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone(), v1, v2],
consensus_config: None,
};
let genesis_record = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let scratch = chain_unsigned_genesis_for_test(genesis_record.clone());
let mut cert = HandoverCertificate::new(genesis_record.clone());
let att =
Attestation::<HandoverRecord>::sign(&genesis_record, &kp0, &v0.authority_key).unwrap();
cert.add_attestation(att, &scratch).unwrap();
assert_eq!(
HandoverChain::new_from_genesis(cert),
Err(HandoverChainError::IncompleteGenesisCertificate),
"1-of-3 attestations is not unanimous; genesis must be fully attested"
);
}
fn make_record(
block_index: u64,
source_epoch: u64,
dest_epoch: u64,
prev_hash: crate::admin::Blake3Hash,
) -> HandoverRecord {
HandoverRecord::new(
block_index,
EpochIdentifier::from(source_epoch),
EpochIdentifier::from(dest_epoch),
EpochChangeConfig {
current_epoch: EpochIdentifier::from(source_epoch),
new_epoch: EpochIdentifier::from(dest_epoch),
validators: vec![],
consensus_config: None,
},
prev_hash,
)
}
fn make_genesis() -> HandoverRecord {
make_record(0, 0, 0, [0u8; 32])
}
fn chain_push(
chain: &mut HandoverChain,
block_index: u64,
source_epoch: u64,
dest_epoch: u64,
) -> Result<(), HandoverChainError> {
let prev_hash = chain.last().unwrap().handover_record().record_digest();
chain.insert_next_handover(make_record(
block_index,
source_epoch,
dest_epoch,
prev_hash,
))
}
fn assert_chain_epoch_step_invariants(chain: &HandoverChain) {
let certs: Vec<_> = chain.iter().collect();
assert!(!certs.is_empty(), "chain must be non-empty");
let r0 = certs[0].handover_record();
assert!(
r0.is_genesis(),
"first certificate must be canonical genesis 0→0"
);
assert_eq!(r0.source_epoch(), r0.dest_epoch());
assert_eq!(r0.source_epoch(), EpochIdentifier::from(0));
for i in 1..certs.len() {
let prev = certs[i - 1].handover_record();
let cur = certs[i].handover_record();
assert_eq!(
cur.source_epoch(),
prev.dest_epoch(),
"src[i] must equal dest[i-1]"
);
assert_eq!(
cur.dest_epoch().as_u64(),
cur.source_epoch().as_u64() + 1,
"dst[i] must equal src[i] + 1"
);
}
}
#[test]
fn test_handover_chain_only_accepts_genesis_0_to_0_as_first_element() {
assert_eq!(
HandoverChain::new_from_genesis(HandoverCertificate::new(make_record(0, 0, 1, [0u8; 32]))),
Err(HandoverChainError::InvalidGenesisFirstRecord)
);
let degenerate_non_zero = HandoverRecord::new(
0,
EpochIdentifier::new(1),
EpochIdentifier::new(1),
EpochChangeConfig {
current_epoch: EpochIdentifier::new(1),
new_epoch: EpochIdentifier::new(1),
validators: vec![],
consensus_config: None,
},
[0u8; 32],
);
assert!(!degenerate_non_zero.is_genesis());
assert_eq!(
HandoverChain::new_from_genesis(HandoverCertificate::new(degenerate_non_zero)),
Err(HandoverChainError::InvalidGenesisFirstRecord)
);
let _ = chain_unsigned_genesis_for_test(make_genesis());
assert_eq!(
HandoverChain::new_from_genesis(HandoverCertificate::new(make_genesis())),
Err(HandoverChainError::IncompleteGenesisCertificate)
);
let mut empty = HandoverChain::new();
empty.insert_next_handover(make_genesis()).unwrap();
assert_eq!(
HandoverChain::new().insert_next_handover(make_record(100, 0, 1, [0u8; 32])),
Err(HandoverChainError::InvalidGenesisFirstRecord)
);
}
#[test]
fn test_handover_chain_sequential_epoch_links_along_extended_chain() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
chain_push(&mut chain, 1000, 1, 2).unwrap();
chain_push(&mut chain, 2000, 2, 3).unwrap();
assert_chain_epoch_step_invariants(&chain);
}
#[test]
fn test_handover_chain_new() {
let chain = HandoverChain::new();
assert!(chain.is_empty());
assert_eq!(chain.len(), 0);
assert!(chain.last().is_none());
}
#[test]
fn test_handover_chain_from_genesis() {
let genesis = make_genesis();
let chain = chain_unsigned_genesis_for_test(genesis);
assert!(!chain.is_empty());
assert_eq!(chain.len(), 1);
assert_eq!(
chain.last().unwrap().handover_record().dest_epoch(),
EpochIdentifier::from(0)
);
}
#[test]
fn test_certificate_for_epoch() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
assert!(chain
.certificate_for_epoch(EpochIdentifier::from(2))
.unwrap()
.is_none());
assert_eq!(
chain
.certificate_for_epoch(EpochIdentifier::from(0))
.unwrap()
.unwrap()
.handover_record()
.block_index(),
0
);
assert_eq!(
chain
.certificate_for_epoch(EpochIdentifier::from(1))
.unwrap()
.unwrap()
.handover_record()
.block_index(),
500
);
assert_eq!(
chain
.certificate_for_epoch_mut(EpochIdentifier::from(1))
.unwrap()
.unwrap()
.handover_record()
.block_index(),
500
);
}
#[test]
fn test_handover_chain_insert_next_handover() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
chain_push(&mut chain, 1000, 1, 2).unwrap();
assert_eq!(chain.len(), 3);
assert_eq!(
chain.last().unwrap().handover_record().dest_epoch(),
EpochIdentifier::from(2)
);
}
#[test]
fn test_insert_next_handover_rejects_non_ascending_block_index() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
let prev = chain.last().unwrap().handover_record().record_digest();
assert!(chain
.insert_next_handover(make_record(500, 1, 2, prev))
.is_err());
assert!(chain
.insert_next_handover(make_record(400, 1, 2, prev))
.is_err());
chain_push(&mut chain, 501, 1, 2).unwrap();
assert_eq!(chain.len(), 3);
}
#[test]
fn test_insert_next_handover_requires_block_index_strictly_greater_than_previous() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
let prev = chain.last().unwrap().handover_record().record_digest();
assert_eq!(
chain.insert_next_handover(make_record(500, 1, 2, prev)),
Err(HandoverChainError::NonAscendingBlockIndex {
new: 500,
last: 500
})
);
assert_eq!(
chain.insert_next_handover(make_record(499, 1, 2, prev)),
Err(HandoverChainError::NonAscendingBlockIndex {
new: 499,
last: 500
})
);
chain_push(&mut chain, 501, 1, 2).unwrap();
let prev2 = chain.last().unwrap().handover_record().record_digest();
assert_eq!(
chain.insert_next_handover(make_record(501, 2, 3, prev2)),
Err(HandoverChainError::NonAscendingBlockIndex {
new: 501,
last: 501
})
);
chain_push(&mut chain, 2000, 2, 3).unwrap();
assert_eq!(chain.len(), 4);
}
#[test]
fn test_insert_next_handover_requires_next_epoch_rejects_skip_and_regression() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).expect("0→1 is the next epoch after genesis dest 0");
chain_push(&mut chain, 1000, 1, 2).expect("1→2 follows 0→1");
let mut skip = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut skip, 500, 0, 1).unwrap();
let prev = skip.last().unwrap().handover_record().record_digest();
assert_eq!(
skip.insert_next_handover(make_record(1500, 1, 3, prev)),
Err(HandoverChainError::DestEpochSkipped {
last_dest: EpochIdentifier::from(1),
expected: EpochIdentifier::from(2),
got: EpochIdentifier::from(3),
})
);
let mut regress = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut regress, 500, 0, 1).unwrap();
let prev = regress.last().unwrap().handover_record().record_digest();
assert_eq!(
regress.insert_next_handover(make_record(1500, 1, 1, prev)),
Err(HandoverChainError::DestEpochRegression {
last_dest: EpochIdentifier::from(1),
expected: EpochIdentifier::from(2),
got: EpochIdentifier::from(1),
})
);
let mut wrong_source = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut wrong_source, 500, 0, 1).unwrap();
let prev = wrong_source
.last()
.unwrap()
.handover_record()
.record_digest();
assert_eq!(
wrong_source.insert_next_handover(make_record(1500, 0, 2, prev)),
Err(HandoverChainError::SourceEpochMismatch {
last_dest: EpochIdentifier::from(1),
got: EpochIdentifier::from(0),
})
);
}
#[test]
fn test_iter_after_epoch_empty_chain() {
let chain = HandoverChain::new();
assert!(chain
.iter_after_epoch(EpochIdentifier::from(0))
.next()
.is_none());
}
#[test]
fn test_iter_after_epoch_genesis_only() {
let chain = chain_unsigned_genesis_for_test(make_genesis());
assert_eq!(chain.len(), 1);
assert!(chain
.iter_after_epoch(EpochIdentifier::from(0))
.next()
.is_none());
}
#[test]
fn test_iter_after_epoch_matches_dest_epoch_greater_than() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
chain_push(&mut chain, 1000, 1, 2).unwrap();
chain_push(&mut chain, 1500, 2, 3).unwrap();
for after in [0u64, 1, 2, 3] {
let after_e = EpochIdentifier::from(after);
let by_skip: Vec<_> = chain
.iter_after_epoch(after_e)
.map(|c| c.handover_record().dest_epoch())
.collect();
let by_filter: Vec<_> = chain
.iter()
.filter(|c| c.handover_record().dest_epoch() > after_e)
.map(|c| c.handover_record().dest_epoch())
.collect();
assert_eq!(
by_skip, by_filter,
"iter_after_epoch({after}) must match dest_epoch > {after}"
);
}
}
#[test]
fn test_iter_after_epoch_when_after_epoch_past_chain_tip() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
assert!(chain
.iter_after_epoch(EpochIdentifier::from(100))
.next()
.is_none());
}
#[test]
fn test_epoch_for_block_empty_chain() {
let chain = HandoverChain::new();
assert!(chain.epoch_for_block(100).unwrap().is_none());
}
#[test]
fn test_certificate_for_block_empty_chain() {
let chain = HandoverChain::new();
assert!(chain.certificate_for_block(100).unwrap().is_none());
}
#[test]
fn test_epoch_for_block_genesis_only() {
let chain = chain_unsigned_genesis_for_test(make_genesis());
assert!(chain.epoch_for_block(0).unwrap().is_none());
assert!(chain.epoch_for_block(100).unwrap().is_none());
}
#[test]
fn test_certificate_for_block_genesis_only() {
let chain = chain_unsigned_genesis_for_test(make_genesis());
assert!(chain.certificate_for_block(0).unwrap().is_none());
assert!(chain.certificate_for_block(100).unwrap().is_none());
assert!(chain.certificate_for_block(1000).unwrap().is_none());
}
#[test]
fn test_epoch_for_block_single_handover() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
assert!(chain.epoch_for_block(0).unwrap().is_none());
assert!(chain.epoch_for_block(499).unwrap().is_none());
assert_eq!(
chain.epoch_for_block(500).unwrap().unwrap(),
EpochIdentifier::from(1)
);
assert_eq!(
chain.epoch_for_block(750).unwrap().unwrap(),
EpochIdentifier::from(1)
);
}
#[test]
fn test_certificate_for_block_single_handover() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
assert!(chain.certificate_for_block(0).unwrap().is_none());
assert!(chain.certificate_for_block(499).unwrap().is_none());
let h = chain.certificate_for_block(500).unwrap().unwrap();
assert_eq!(h.handover_record().dest_epoch(), EpochIdentifier::from(1));
assert_eq!(h.handover_record().block_index(), 500);
let h = chain.certificate_for_block(750).unwrap().unwrap();
assert_eq!(h.handover_record().dest_epoch(), EpochIdentifier::from(1));
}
#[test]
fn test_epoch_for_block_multiple_handovers() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
chain_push(&mut chain, 1000, 1, 2).unwrap();
chain_push(&mut chain, 1500, 2, 3).unwrap();
assert_eq!(
chain.epoch_for_block(500).unwrap().unwrap(),
EpochIdentifier::from(1)
);
assert_eq!(
chain.epoch_for_block(1500).unwrap().unwrap(),
EpochIdentifier::from(3)
);
}
#[test]
fn test_certificate_for_block_multiple_handovers() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
chain_push(&mut chain, 1000, 1, 2).unwrap();
chain_push(&mut chain, 1500, 2, 3).unwrap();
assert!(chain.certificate_for_block(0).unwrap().is_none());
assert!(chain.certificate_for_block(499).unwrap().is_none());
let h = chain.certificate_for_block(500).unwrap().unwrap();
assert_eq!(h.handover_record().dest_epoch(), EpochIdentifier::from(1));
assert_eq!(h.handover_record().block_index(), 500);
let h = chain.certificate_for_block(999).unwrap().unwrap();
assert_eq!(h.handover_record().dest_epoch(), EpochIdentifier::from(1));
let h = chain.certificate_for_block(1000).unwrap().unwrap();
assert_eq!(h.handover_record().dest_epoch(), EpochIdentifier::from(2));
let h = chain.certificate_for_block(1200).unwrap().unwrap();
assert_eq!(h.handover_record().dest_epoch(), EpochIdentifier::from(2));
let h = chain.certificate_for_block(1500).unwrap().unwrap();
assert_eq!(h.handover_record().dest_epoch(), EpochIdentifier::from(3));
let h = chain.certificate_for_block(10000).unwrap().unwrap();
assert_eq!(h.handover_record().dest_epoch(), EpochIdentifier::from(3));
}
#[test]
fn test_certificate_for_block_matches_epoch_for_block_dest_epoch() {
fn assert_consistent(chain: &HandoverChain, height: u64) {
let by_cert = chain.certificate_for_block(height).unwrap();
let by_epoch = chain.epoch_for_block(height).unwrap();
match (by_cert, by_epoch) {
(Some(c), Some(e)) => {
assert_eq!(c.handover_record().dest_epoch(), e);
}
(None, None) => {}
(a, b) => panic!("mismatch at {height}: {a:?} vs {b:?}"),
}
}
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
chain_push(&mut chain, 1000, 1, 2).unwrap();
for h in [0u64, 100, 499, 500, 750, 999, 1000, 2000] {
assert_consistent(&chain, h);
}
}
#[test]
fn test_certificate_for_block_returns_certificate_for_indexing() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
let cert = chain.certificate_for_block(600).unwrap().unwrap();
assert_eq!(
chain[1].handover_record().record_digest(),
cert.handover_record().record_digest()
);
}
#[test]
fn test_handover_chain_serde_json_roundtrip() {
let mut chain = chain_unsigned_genesis_for_test(make_genesis());
chain_push(&mut chain, 500, 0, 1).unwrap();
let json = serde_json::to_string(&chain).unwrap();
let back: HandoverChain = serde_json::from_str(&json).unwrap();
assert_eq!(chain, back);
}
#[test]
fn test_handover_chain_deserialize_rejects_permuted_certs() {
let genesis = make_genesis();
let genesis_hash = genesis.record_digest();
let g = HandoverCertificate::new(genesis);
let t = HandoverCertificate::new(make_record(500, 0, 1, genesis_hash));
let json = serde_json::to_string(&vec![t, g]).unwrap();
assert!(serde_json::from_str::<HandoverChain>(&json).is_err());
}
#[test]
fn test_handover_chain_try_from_rejects_invalid_order() {
use std::convert::TryFrom;
let genesis = make_genesis();
let genesis_hash = genesis.record_digest();
let g = HandoverCertificate::new(genesis);
let t = HandoverCertificate::new(make_record(500, 0, 1, genesis_hash));
assert!(HandoverChain::try_from(vec![t, g]).is_err());
}
#[test]
fn test_handover_chain_lookups_return_err_when_validate_fails() {
let genesis = make_genesis();
let genesis_hash = genesis.record_digest();
let g = HandoverCertificate::new(genesis);
let t = HandoverCertificate::new(make_record(500, 0, 1, genesis_hash));
let chain = HandoverChain::from_vec_unchecked_for_test(vec![t, g]);
assert!(chain.validate().is_err());
assert!(chain
.certificate_for_epoch(EpochIdentifier::from(1))
.is_err());
assert!(chain.certificate_for_block(600).is_err());
assert!(chain.epoch_for_block(600).is_err());
}
#[cfg(test)]
mod handover_state_sync_trust_boundary_tests {
use rialo_s_pubkey::Pubkey;
use super::{
super::super::{
attestation_framework::Attestation,
epoch::{EpochChangeConfig, EpochIdentifier, ValidatorInfo},
test_helpers::{
test_keys::create_test_keys_with_keypair, test_records::create_test_handover_record,
},
},
chain_unsigned_genesis_for_test, HandoverCertificate, HandoverRecord,
};
use crate::{
consensus_crypto::{AuthorityPublicKey, NetworkPublicKey, ProtocolPublicKey},
Multiaddr,
};
fn attestors_in_source_committee(
cert: &HandoverCertificate,
prev_record: &HandoverRecord,
) -> bool {
if prev_record.dest_epoch() != cert.handover_record().source_epoch() {
return false;
}
let validators = &prev_record.epoch_config().validators;
cert.attestations().iter().all(|a| {
validators
.iter()
.any(|v| v.authority_key == *a.authority_key())
})
}
fn validator_for_keys(
seed: u64,
authority_key: AuthorityPublicKey,
protocol_key: ProtocolPublicKey,
network_key: NetworkPublicKey,
) -> ValidatorInfo {
ValidatorInfo {
stake: 100,
consensus_address: format!("/ip4/127.0.0.1/udp/{}", 30_000 + seed)
.parse::<Multiaddr>()
.expect("valid multiaddr"),
state_sync_address: format!("/ip4/127.0.0.1/udp/{}", 40_000 + seed)
.parse::<Multiaddr>()
.expect("valid multiaddr"),
hostname: format!("handover-trust-validator-{seed}"),
authority_key,
protocol_key,
network_key,
signing_key: Pubkey::new_unique(),
}
}
#[test]
fn parity_handover_valid_signed_certificate_binds_attestors_to_source_committee() {
let (a0, kp0, n0) = create_test_keys_with_keypair(60_001);
let v0 = validator_for_keys(1, a0, kp0.public(), n0);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone()],
consensus_config: None,
};
let prev = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let (a1, kp1, n1) = create_test_keys_with_keypair(60_002);
let v_new = validator_for_keys(2, a1, kp1.public(), n1);
let next_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![v0.clone(), v_new],
consensus_config: None,
};
let handover_record = HandoverRecord::new(
1,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
next_config,
prev.record_digest(),
);
let att = Attestation::<HandoverRecord>::sign(&handover_record, &kp0, &v0.authority_key)
.expect("sign");
let mut cert = HandoverCertificate::new(handover_record);
let chain = chain_unsigned_genesis_for_test(prev.clone());
cert.add_attestation(att, &chain)
.expect("signer must be in previous (source) committee");
assert!(
cert.validate_attestation_consistency().is_ok(),
"internally consistent certificate expected"
);
assert!(
attestors_in_source_committee(&cert, &prev),
"every attestor must be in the previous handover's committee, not only in epoch_config"
);
}
#[test]
fn parity_handover_rejects_signer_from_wrong_previous_record() {
use crate::admin::attestation_framework::CertificateAddAttestationError;
let (a0, kp0, n0) = create_test_keys_with_keypair(60_030);
let v0 = validator_for_keys(30, a0, kp0.public(), n0);
let (a1, kp1, n1) = create_test_keys_with_keypair(60_031);
let v1 = validator_for_keys(31, a1, kp1.public(), n1);
let (a2, kp2, n2) = create_test_keys_with_keypair(60_032);
let v2 = validator_for_keys(32, a2.clone(), kp2.public(), n2);
let genesis = HandoverRecord::genesis(
EpochIdentifier::new(0),
EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone()],
consensus_config: None,
},
);
let mut chain = chain_unsigned_genesis_for_test(genesis.clone());
let h01 = HandoverRecord::new(
100,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![v1.clone()],
consensus_config: None,
},
genesis.record_digest(),
);
chain.insert_next_handover(h01.clone()).unwrap();
let h12 = HandoverRecord::new(
200,
EpochIdentifier::new(1),
EpochIdentifier::new(2),
EpochChangeConfig {
current_epoch: EpochIdentifier::new(1),
new_epoch: EpochIdentifier::new(2),
validators: vec![v2.clone()],
consensus_config: None,
},
h01.record_digest(),
);
chain.insert_next_handover(h12.clone()).unwrap();
let att = Attestation::<HandoverRecord>::sign(&h12, &kp2, &a2).expect("sign");
let mut cert = HandoverCertificate::new(h12.clone());
assert_eq!(
cert.add_attestation(att.clone(), &chain).unwrap_err(),
CertificateAddAttestationError::InvalidAttestor,
"signer from epoch-2 dest committee must be rejected when signing epoch-1→2 handover"
);
let cert_with_att = HandoverCertificate::from_record_and_attestations(h12, vec![att]);
assert!(
!attestors_in_source_committee(&cert_with_att, &h01),
"helper must also reject: v2 is not in h01's committee"
);
}
#[test]
fn parity_handover_genesis_signers_come_from_own_epoch_config() {
use crate::admin::attestation_framework::CertificateAddAttestationError;
let (a0, kp0, n0) = create_test_keys_with_keypair(60_010);
let (a1, kp1, n1) = create_test_keys_with_keypair(60_011);
let v0 = validator_for_keys(10, a0, kp0.public(), n0);
let v1 = validator_for_keys(11, a1, kp1.public(), n1);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone(), v1.clone()],
consensus_config: None,
};
let genesis_record = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
assert!(
genesis_record.is_genesis(),
"precondition: src == dest == 0"
);
assert_eq!(genesis_record.source_epoch(), genesis_record.dest_epoch());
let chain = chain_unsigned_genesis_for_test(genesis_record.clone());
let att0 =
Attestation::<HandoverRecord>::sign(&genesis_record, &kp0, &v0.authority_key).unwrap();
let att1 =
Attestation::<HandoverRecord>::sign(&genesis_record, &kp1, &v1.authority_key).unwrap();
let mut cert = HandoverCertificate::new(genesis_record.clone());
cert.add_attestation(att0, &chain)
.expect("genesis signer from own epoch_config must be accepted");
cert.add_attestation(att1, &chain)
.expect("genesis signer from own epoch_config must be accepted");
assert!(cert.is_genesis());
assert!(
attestors_in_source_committee(&cert, &genesis_record),
"genesis: signing committee IS this record's embedded epoch_config"
);
let (a_out, kp_out, n_out) = create_test_keys_with_keypair(60_012);
let _v_out = validator_for_keys(12, a_out.clone(), kp_out.public(), n_out);
let att_out =
Attestation::<HandoverRecord>::sign(&genesis_record, &kp_out, &a_out).unwrap();
let mut cert2 = HandoverCertificate::new(genesis_record.clone());
assert_eq!(
cert2.add_attestation(att_out, &chain).unwrap_err(),
CertificateAddAttestationError::InvalidAttestor,
"genesis must reject signers not in its own epoch_config"
);
}
#[test]
fn parity_handover_rejects_attestor_not_in_source_committee_even_if_in_epoch_config() {
use crate::admin::attestation_framework::CertificateAddAttestationError;
let (a0, kp0, n0) = create_test_keys_with_keypair(60_020);
let v0 = validator_for_keys(20, a0, kp0.public(), n0);
let genesis_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![v0.clone()],
consensus_config: None,
};
let prev = HandoverRecord::genesis(EpochIdentifier::new(0), genesis_config);
let (a_out, kp_out, n_out) = create_test_keys_with_keypair(60_021);
let v_out = validator_for_keys(21, a_out.clone(), kp_out.public(), n_out);
let next_config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![v0, v_out.clone()],
consensus_config: None,
};
let handover_record = HandoverRecord::new(
1,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
next_config,
prev.record_digest(),
);
let att = Attestation::<HandoverRecord>::sign(&handover_record, &kp_out, &a_out)
.expect("self-consistent attestation from key listed only in new epoch_config");
let chain = chain_unsigned_genesis_for_test(prev.clone());
let mut cert = HandoverCertificate::new(handover_record.clone());
assert_eq!(
cert.add_attestation(att.clone(), &chain).unwrap_err(),
CertificateAddAttestationError::InvalidAttestor,
"add_attestation must reject signer outside previous (source) committee"
);
let cert_with_att =
HandoverCertificate::from_record_and_attestations(handover_record, vec![att]);
assert!(
cert_with_att.validate_attestation_consistency().is_ok(),
"cryptographic self-consistency does not imply source-committee membership"
);
assert!(
!attestors_in_source_committee(&cert_with_att, &prev),
"helper confirms attestor is not in the source (previous) committee"
);
}
#[test]
fn parity_handover_tampered_attestation_fails_signature_verification() {
let record = create_test_handover_record(100, 0, 1, [0u8; 32]);
let (auth, kp, _) = create_test_keys_with_keypair(60_004);
let mut att =
Attestation::<HandoverRecord>::sign(&record, &kp, &auth).expect("sign should succeed");
att.tamper_signature(0, 0xff);
assert!(
att.verify_internal_consistency().is_err(),
"tampered signature bytes must not verify"
);
}
}