use rialo_s_pubkey::Pubkey;
use super::{
super::{
attestation_framework::{
Attestation, CertificateAttestationConsistencyError, ATTESTATION_SIGNING_SIZE,
INTENT_PREFIX_LENGTH,
},
epoch::{EpochChangeConfig, EpochIdentifier, ValidatorInfo},
handover::{HandoverChain, HandoverRecord},
test_helpers::{test_keys::*, test_records::create_test_snapshot_record},
BLAKE3_HASH_SIZE,
},
record::SNAPSHOT_ATTESTATION_INTENT_PREFIX,
*,
};
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,
)
}
fn handover_opening_epoch_1_at(
block_index: u64,
previous_hash: crate::admin::Blake3Hash,
) -> HandoverRecord {
let (v0, _kp0) = test_validator(3100, 10);
let (v1, _kp1) = test_validator(3101, 10);
let (v2, _kp2) = test_validator(3102, 10);
HandoverRecord::new(
block_index,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![v0, v1, v2],
consensus_config: None,
},
previous_hash,
)
}
fn chain_genesis_and_epoch1_opening_at(opening_block: u64) -> HandoverChain {
let genesis = HandoverRecord::genesis(
EpochIdentifier::new(0),
EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![],
consensus_config: None,
},
);
let genesis_hash = genesis.record_digest();
let mut chain = HandoverChain::new();
chain.insert_next_handover(genesis).unwrap();
chain
.insert_next_handover(handover_opening_epoch_1_at(opening_block, genesis_hash))
.unwrap();
chain
}
fn chain_with_epoch2_after_epoch1(
e1_opening_block: u64,
epoch2_opening_block: u64,
) -> HandoverChain {
let mut chain = chain_genesis_and_epoch1_opening_at(e1_opening_block);
let prev_hash = chain.last().unwrap().handover_record().record_digest();
let (v0, _kp0) = test_validator(4000, 10);
let epoch2 = HandoverRecord::new(
epoch2_opening_block,
EpochIdentifier::new(1),
EpochIdentifier::new(2),
EpochChangeConfig {
current_epoch: EpochIdentifier::new(1),
new_epoch: EpochIdentifier::new(2),
validators: vec![v0],
consensus_config: None,
},
prev_hash,
);
chain.insert_next_handover(epoch2).unwrap();
chain
}
#[test]
fn test_snapshot_record_digest_determinism() {
let record_a = create_test_snapshot_record(100, 1);
let record_b = create_test_snapshot_record(100, 1);
assert_eq!(
record_a.record_digest(),
record_b.record_digest(),
"identical SnapshotRecords must produce identical digests"
);
}
#[test]
fn test_snapshot_record_digest_field_sensitivity() {
let base = create_test_snapshot_record(100, 1);
let different_height = SnapshotRecord::new(101, base.epoch(), base.keysvals_hash());
assert_ne!(
base.record_digest(),
different_height.record_digest(),
"different block_height must produce different digests"
);
let different_epoch = SnapshotRecord::new(
base.block_height(),
EpochIdentifier::new(2),
base.keysvals_hash(),
);
assert_ne!(
base.record_digest(),
different_epoch.record_digest(),
"different epoch must produce different digests"
);
let different_keysvals = SnapshotRecord::new(
base.block_height(),
base.epoch(),
[0xCDu8; BLAKE3_HASH_SIZE],
);
assert_ne!(
base.record_digest(),
different_keysvals.record_digest(),
"different keysvals_hash must produce different digests"
);
}
#[test]
fn test_snapshot_attestation_intent_prefix_matches_shared_crypto() {
use shared_crypto::intent::{AppId, IntentScope, IntentVersion};
assert_eq!(
SNAPSHOT_ATTESTATION_INTENT_PREFIX,
[
IntentScope::SnapshotAttestation as u8,
IntentVersion::V0 as u8,
AppId::Consensus as u8,
],
"SNAPSHOT_ATTESTATION_INTENT_PREFIX is out of sync with shared-crypto enums"
);
}
#[test]
fn test_snapshot_attestation_signing_message_format() {
let record = create_test_snapshot_record(42, 1);
let digest = record.record_digest();
let msg = Attestation::<SnapshotRecord>::signing_message(&digest);
assert_eq!(msg.len(), ATTESTATION_SIGNING_SIZE);
assert_eq!(
msg[..INTENT_PREFIX_LENGTH],
SNAPSHOT_ATTESTATION_INTENT_PREFIX
);
assert_eq!(msg[INTENT_PREFIX_LENGTH..], digest);
}
#[test]
fn test_snapshot_attestation_signing_message_wire_compatible_with_bcs() {
use shared_crypto::intent::{Intent, IntentMessage, IntentScope};
let record = create_test_snapshot_record(42, 1);
let digest = record.record_digest();
let generic_msg = Attestation::<SnapshotRecord>::signing_message(&digest);
let intent_msg = IntentMessage::new(
Intent::consensus_app(IntentScope::SnapshotAttestation),
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(SnapshotAttestation, digest))"
);
}
#[test]
fn test_snapshot_attestation_sign_verify_roundtrip() {
let record = create_test_snapshot_record(100, 1);
let (authority_key, protocol_keypair, _) = create_test_keys_with_keypair(700);
let attestation =
Attestation::<SnapshotRecord>::sign(&record, &protocol_keypair, &authority_key)
.expect("sign() should succeed");
attestation
.verify_internal_consistency()
.expect("freshly signed SnapshotAttestation 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_snapshot_attestation_submission_validate_digest_consistency_success() {
let record = create_test_snapshot_record(100, 1);
let (authority_key, protocol_keypair, _) = create_test_keys_with_keypair(710);
let attestation =
Attestation::<SnapshotRecord>::sign(&record, &protocol_keypair, &authority_key)
.expect("sign() should succeed");
let submission = SnapshotAttestationSubmission::new(record, attestation);
assert!(
submission.validate_digest_consistency().is_ok(),
"valid submission should pass digest consistency check"
);
}
#[test]
fn test_snapshot_attestation_submission_validate_digest_consistency_mismatch() {
let record = create_test_snapshot_record(100, 1);
let wrong_record = create_test_snapshot_record(200, 1);
let (authority_key, protocol_keypair, _) = create_test_keys_with_keypair(711);
let attestation =
Attestation::<SnapshotRecord>::sign(&record, &protocol_keypair, &authority_key)
.expect("sign() should succeed");
let submission = SnapshotAttestationSubmission::new(wrong_record, attestation);
assert_eq!(
submission.validate_digest_consistency().unwrap_err(),
SnapshotAttestationSubmissionError::DigestMismatch
);
}
#[test]
fn test_snapshot_certificate_validate_attestation_consistency_success() {
let record = create_test_snapshot_record(100, 1);
let (auth_key1, kp1, _) = create_test_keys_with_keypair(720);
let (auth_key2, kp2, _) = create_test_keys_with_keypair(721);
let att1 = Attestation::<SnapshotRecord>::sign(&record, &kp1, &auth_key1)
.expect("sign() should succeed");
let att2 = Attestation::<SnapshotRecord>::sign(&record, &kp2, &auth_key2)
.expect("sign() should succeed");
let cert = SnapshotCertificate::from_record_and_attestations(record, vec![att1, att2]);
assert!(
cert.validate_attestation_consistency().is_ok(),
"all attestations binding same record should pass"
);
}
#[test]
fn test_snapshot_certificate_validate_attestation_consistency_mismatch() {
let record = create_test_snapshot_record(100, 1);
let other_record = create_test_snapshot_record(200, 1);
let (auth_key1, kp1, _) = create_test_keys_with_keypair(730);
let (auth_key2, kp2, _) = create_test_keys_with_keypair(731);
let att1 = Attestation::<SnapshotRecord>::sign(&record, &kp1, &auth_key1)
.expect("sign() should succeed");
let att2 = Attestation::<SnapshotRecord>::sign(&other_record, &kp2, &auth_key2)
.expect("sign() should succeed");
let cert = SnapshotCertificate::from_record_and_attestations(record, vec![att1, att2]);
assert_eq!(
cert.validate_attestation_consistency().unwrap_err(),
CertificateAttestationConsistencyError::MismatchedRecordDigest {
cert_type: "SnapshotCertificate",
}
);
}
#[test]
fn test_snapshot_certificate_serialization_roundtrip() {
let record = create_test_snapshot_record(100, 1);
let (auth_key1, kp1, _) = create_test_keys_with_keypair(740);
let att1 = Attestation::<SnapshotRecord>::sign(&record, &kp1, &auth_key1)
.expect("sign() should succeed");
let cert = SnapshotCertificate::from_record_and_attestations(record.clone(), vec![att1]);
let serialized = serde_json::to_string(&cert).expect("serialization should succeed");
let deserialized: SnapshotCertificate =
serde_json::from_str(&serialized).expect("deserialization should succeed");
assert_eq!(*deserialized.snapshot_record(), record);
assert_eq!(deserialized.attestations().len(), 1);
assert_eq!(deserialized.block_index(), record.block_height());
assert!(deserialized.validate_attestation_consistency().is_ok());
}
#[test]
fn test_snapshot_certificate_add_attestation_success() {
let chain = chain_genesis_and_epoch1_opening_at(100);
let (v0, kp0) = test_validator(3100, 10);
let snapshot_record =
SnapshotRecord::new(150, EpochIdentifier::new(1), [0xEFu8; BLAKE3_HASH_SIZE]);
let att = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp0, &v0.authority_key)
.expect("sign");
let mut cert = SnapshotCertificate::new(snapshot_record);
assert!(cert.add_attestation(att, &chain).is_ok());
assert_eq!(cert.attestations().len(), 1);
}
#[test]
fn test_snapshot_certificate_add_attestation_rejects_missing_epoch_in_chain() {
let chain = chain_genesis_and_epoch1_opening_at(100);
let snapshot_record = create_test_snapshot_record(150, 2);
let (auth, kp, _) = create_test_keys_with_keypair(3200);
let att = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp, &auth).expect("sign");
let mut cert = SnapshotCertificate::new(snapshot_record);
assert_eq!(
cert.add_attestation(att, &chain).unwrap_err(),
SnapshotCertificateAddAttestationError::MissingEpochInChain(EpochIdentifier::new(2))
);
}
#[test]
fn test_snapshot_certificate_add_attestation_rejects_snapshot_before_epoch_start() {
let chain = chain_genesis_and_epoch1_opening_at(200);
let snapshot_record =
SnapshotRecord::new(199, EpochIdentifier::new(1), [0xEFu8; BLAKE3_HASH_SIZE]);
let (v0, kp0) = test_validator(3100, 10);
let att = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp0, &v0.authority_key)
.expect("sign");
let mut cert = SnapshotCertificate::new(snapshot_record);
assert_eq!(
cert.add_attestation(att, &chain).unwrap_err(),
SnapshotCertificateAddAttestationError::BlockBeforeEpochStart
);
}
#[test]
fn test_snapshot_certificate_add_attestation_rejects_snapshot_after_next_epoch_handover() {
let chain = chain_with_epoch2_after_epoch1(100, 500);
let snapshot_record =
SnapshotRecord::new(501, EpochIdentifier::new(1), [0xEFu8; BLAKE3_HASH_SIZE]);
let (v0, kp0) = test_validator(3100, 10);
let att = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp0, &v0.authority_key)
.expect("sign");
let mut cert = SnapshotCertificate::new(snapshot_record);
assert_eq!(
cert.add_attestation(att, &chain).unwrap_err(),
SnapshotCertificateAddAttestationError::BlockAfterNextEpochHandover {
block_height: 501,
next_epoch_start_block: 500,
}
);
}
#[test]
fn test_snapshot_certificate_add_attestation_accepts_snapshot_at_next_epoch_handover() {
let chain = chain_with_epoch2_after_epoch1(100, 500);
let snapshot_record =
SnapshotRecord::new(500, EpochIdentifier::new(1), [0xEFu8; BLAKE3_HASH_SIZE]);
let (v0, kp0) = test_validator(3100, 10);
let att = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp0, &v0.authority_key)
.expect("sign");
let mut cert = SnapshotCertificate::new(snapshot_record);
assert!(
cert.add_attestation(att, &chain).is_ok(),
"handover block is the last block under the previous epoch's purview"
);
}
#[test]
fn test_snapshot_certificate_is_complete_false_when_out_of_signers_scope() {
let chain = chain_genesis_and_epoch1_opening_at(100);
let snapshot_record =
SnapshotRecord::new(50, EpochIdentifier::new(1), [0xEFu8; BLAKE3_HASH_SIZE]);
let (v0, kp0) = test_validator(3100, 10);
let (v1, kp1) = test_validator(3101, 10);
let (v2, kp2) = test_validator(3102, 10);
let att0 = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp0, &v0.authority_key)
.expect("sign");
let att1 = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp1, &v1.authority_key)
.expect("sign");
let att2 = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp2, &v2.authority_key)
.expect("sign");
let cert =
SnapshotCertificate::from_record_and_attestations(snapshot_record, vec![att0, att1, att2]);
assert!(
!cert.is_complete(&chain),
"quorum must not count when snapshot height is before epoch handover"
);
}
#[test]
fn test_snapshot_certificate_is_complete_stake_threshold_in_scope() {
let chain = chain_genesis_and_epoch1_opening_at(100);
let snapshot_record =
SnapshotRecord::new(150, EpochIdentifier::new(1), [0xEFu8; BLAKE3_HASH_SIZE]);
let (v0, kp0) = test_validator(3100, 10);
let (v1, kp1) = test_validator(3101, 10);
let (v2, kp2) = test_validator(3102, 10);
let att0 = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp0, &v0.authority_key)
.expect("sign");
let att1 = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp1, &v1.authority_key)
.expect("sign");
let mut cert_two = SnapshotCertificate::new(snapshot_record.clone());
cert_two.add_attestation(att0, &chain).unwrap();
cert_two.add_attestation(att1, &chain).unwrap();
assert!(
cert_two.is_complete(&chain),
"20 attested stake exceeds attestation_threshold(total_stake=30) = 10"
);
let att2_only = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp2, &v2.authority_key)
.expect("sign");
let mut cert_one = SnapshotCertificate::new(snapshot_record);
cert_one.add_attestation(att2_only, &chain).unwrap();
assert!(
!cert_one.is_complete(&chain),
"10 attested stake does not exceed attestation_threshold(total_stake=30) = 10"
);
}
#[test]
fn test_snapshot_certificate_is_complete_false_when_total_stake_sum_overflows() {
let (v0, kp0) = test_validator(3200, u64::MAX);
let (v1, kp1) = test_validator(3201, 1u64);
let snapshot_record =
SnapshotRecord::new(150, EpochIdentifier::new(1), [0xEFu8; BLAKE3_HASH_SIZE]);
let att0 = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp0, &v0.authority_key)
.expect("sign");
let att1 = Attestation::<SnapshotRecord>::sign(&snapshot_record, &kp1, &v1.authority_key)
.expect("sign");
let mut chain = HandoverChain::new();
let genesis = HandoverRecord::genesis(
EpochIdentifier::new(0),
EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(0),
validators: vec![],
consensus_config: None,
},
);
let genesis_hash = genesis.record_digest();
chain.insert_next_handover(genesis).unwrap();
chain
.insert_next_handover(HandoverRecord::new(
100,
EpochIdentifier::new(0),
EpochIdentifier::new(1),
EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![v0, v1],
consensus_config: None,
},
genesis_hash,
))
.unwrap();
let mut cert = SnapshotCertificate::new(snapshot_record);
cert.add_attestation(att0, &chain).unwrap();
cert.add_attestation(att1, &chain).unwrap();
assert!(
!cert.is_complete(&chain),
"committee stake sum overflowing u64 must not complete and must not panic"
);
}