use serde::{Deserialize, Serialize};
pub const BLAKE3_HASH_SIZE: usize = 32;
pub type Blake3Hash = [u8; BLAKE3_HASH_SIZE];
pub const BLS12381_PUBLIC_KEY_SIZE: usize = 96;
pub const ED25519_PUBLIC_KEY_SIZE: usize = 32;
pub const ED25519_SIGNATURE_SIZE: usize = 64;
const READY_MESSAGE_INTENT_PREFIX: [u8; 3] = [14, 0, 2];
const INTENT_PREFIX_LEN: usize = 3;
pub const READY_MESSAGE_SIGNING_SIZE: usize = INTENT_PREFIX_LEN + 8 + BLAKE3_HASH_SIZE;
#[macro_use]
mod attestation_framework;
pub mod epoch;
mod handover;
mod snapshot;
#[cfg(test)]
mod test_helpers;
pub use attestation_framework::{
attestation_threshold, validate_attestation_consistency, AttestableRecord, Attestation,
AttestationError, Certificate, CertificateAddAttestationError,
CertificateAttestationConsistencyError, CommitteeAttestedRecord, RecordDigest,
ATTESTATION_THRESHOLD_DENOMINATOR, ATTESTATION_THRESHOLD_NUMERATOR,
};
pub use epoch::*;
pub use handover::{
HandoverAttestation, HandoverCertificate, HandoverCertificateAddAttestationError,
HandoverChain, HandoverChainError, HandoverRecord,
};
pub use snapshot::{
SnapshotAttestation, SnapshotAttestationSubmission, SnapshotAttestationSubmissionError,
SnapshotCertificate, SnapshotCertificateAddAttestationError, SnapshotRecord,
};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum AdminTransaction {
EpochChange(Box<EpochChangeConfig>),
Ready(Box<ValidatorReadyMessage>),
Handover(Box<HandoverRecord>),
SnapshotAttestationSubmission(Box<SnapshotAttestationSubmission>),
}
#[cfg(test)]
mod tests {
use fastcrypto::{ed25519, traits::KeyPair};
use rand::SeedableRng;
use rialo_s_pubkey::Pubkey;
use super::{
test_helpers::{
test_keys::create_test_keys_with_keypair, test_records::create_test_snapshot_record,
},
*,
};
use crate::consensus_crypto::{AuthorityPublicKey, NetworkPublicKey, ProtocolPublicKey};
const KEYGEN_SEED_BASE: u64 = 42;
fn create_test_keys(seed: u64) -> (AuthorityPublicKey, ProtocolPublicKey, NetworkPublicKey) {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let authority_key = AuthorityPublicKey::new(
fastcrypto::bls12381::min_sig::BLS12381KeyPair::generate(&mut rng)
.public()
.clone(),
);
let protocol_key =
ProtocolPublicKey::new(ed25519::Ed25519KeyPair::generate(&mut rng).public().clone());
let network_key =
NetworkPublicKey::new(ed25519::Ed25519KeyPair::generate(&mut rng).public().clone());
(authority_key, protocol_key, network_key)
}
#[test]
fn test_epoch_change_config_serialization() {
let (authority_key, protocol_key, network_key) = create_test_keys(KEYGEN_SEED_BASE);
let config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![ValidatorInfo {
stake: 1000,
consensus_address: "/ip4/127.0.0.1/udp/10100".parse().unwrap(),
state_sync_address: "/ip4/127.0.0.1/udp/20100".parse().unwrap(),
hostname: "validator-0".to_string(),
authority_key,
protocol_key,
network_key,
signing_key: Pubkey::new_unique(),
}],
consensus_config: None,
};
let serialized = serde_json::to_string(&config).expect("serialization should succeed");
let deserialized: EpochChangeConfig =
serde_json::from_str(&serialized).expect("deserialization should succeed");
assert_eq!(deserialized.current_epoch, config.current_epoch);
assert_eq!(deserialized.new_epoch, config.new_epoch);
assert_eq!(deserialized.validators.len(), 1);
assert_eq!(deserialized.validators[0].stake, 1000);
}
#[test]
fn test_admin_transaction_serialization() {
let config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(4),
new_epoch: EpochIdentifier::new(5),
validators: vec![],
consensus_config: Some(EpochConsensusConfig {
num_leaders_per_round: Some(2),
..Default::default()
}),
};
let admin_tx = AdminTransaction::EpochChange(Box::new(config));
let serialized = serde_json::to_string(&admin_tx).expect("serialization should succeed");
let deserialized: AdminTransaction =
serde_json::from_str(&serialized).expect("deserialization should succeed");
match deserialized {
AdminTransaction::EpochChange(cfg) => {
assert_eq!(cfg.new_epoch, EpochIdentifier::new(5));
assert!(cfg.consensus_config.is_some());
}
_ => panic!("Expected EpochChange"),
}
}
#[test]
fn test_validator_ready_message_serialization() {
let (authority_key, protocol_key, _) = create_test_keys(KEYGEN_SEED_BASE + 1);
let epoch_config_hash = [0xABu8; BLAKE3_HASH_SIZE];
let ready_msg = ValidatorReadyMessage {
current_epoch: EpochIdentifier::new(0),
epoch_config_hash,
authority_key: authority_key.clone(),
protocol_key: protocol_key.clone(),
signature: [2u8; ED25519_SIGNATURE_SIZE],
};
let serialized = serde_json::to_string(&ready_msg).expect("serialization should succeed");
let deserialized: ValidatorReadyMessage =
serde_json::from_str(&serialized).expect("deserialization should succeed");
assert_eq!(deserialized.current_epoch, EpochIdentifier::new(0));
assert_eq!(deserialized.epoch_config_hash, epoch_config_hash);
assert_eq!(deserialized.authority_key, authority_key);
assert_eq!(deserialized.protocol_key, protocol_key);
assert_eq!(deserialized.signature, ready_msg.signature);
}
#[test]
fn test_validator_ready_message_bytes_delegates_to_signing_message() {
let (authority_key, protocol_key, _) = create_test_keys(KEYGEN_SEED_BASE + 2);
let epoch_config_hash = [0xABu8; BLAKE3_HASH_SIZE];
let ready_msg = ValidatorReadyMessage {
current_epoch: EpochIdentifier::new(5),
epoch_config_hash,
authority_key,
protocol_key,
signature: [2u8; ED25519_SIGNATURE_SIZE],
};
assert_eq!(
ready_msg.message_bytes(),
ValidatorReadyMessage::signing_message(EpochIdentifier::new(5), &epoch_config_hash)
);
}
#[test]
fn test_ready_message_signing_message_wire_format() {
let epoch = EpochIdentifier::new(42);
let epoch_config_hash = [0xBBu8; BLAKE3_HASH_SIZE];
let msg = ValidatorReadyMessage::signing_message(epoch, &epoch_config_hash);
assert_eq!(msg.len(), 43);
assert_eq!(msg[0..3], [14, 0, 2]);
assert_eq!(msg[3..11], 42u64.to_le_bytes());
assert_eq!(msg[11..43], epoch_config_hash);
let msg2 =
ValidatorReadyMessage::signing_message(EpochIdentifier::new(0), &epoch_config_hash);
assert_ne!(msg, msg2);
assert_eq!(msg2[0..3], [14, 0, 2]);
}
#[test]
fn test_ready_message_intent_prefix_matches_shared_crypto() {
use shared_crypto::intent::{AppId, IntentScope, IntentVersion, INTENT_PREFIX_LENGTH};
assert_eq!(
super::READY_MESSAGE_INTENT_PREFIX,
[
IntentScope::ValidatorReady as u8,
IntentVersion::V0 as u8,
AppId::Consensus as u8,
],
"READY_MESSAGE_INTENT_PREFIX is out of sync with shared-crypto enums"
);
assert_eq!(
super::INTENT_PREFIX_LEN,
INTENT_PREFIX_LENGTH,
"INTENT_PREFIX_LEN is out of sync with shared_crypto::intent::INTENT_PREFIX_LENGTH"
);
}
#[test]
fn test_admin_transaction_ready_serialization() {
let (authority_key, protocol_key, _) = create_test_keys(KEYGEN_SEED_BASE + 3);
let epoch_config_hash = [0xABu8; BLAKE3_HASH_SIZE];
let ready_msg = ValidatorReadyMessage {
current_epoch: EpochIdentifier::new(0),
epoch_config_hash,
authority_key,
protocol_key,
signature: [2u8; ED25519_SIGNATURE_SIZE],
};
let admin_tx = AdminTransaction::Ready(Box::new(ready_msg));
let serialized = serde_json::to_string(&admin_tx).expect("serialization should succeed");
let deserialized: AdminTransaction =
serde_json::from_str(&serialized).expect("deserialization should succeed");
match deserialized {
AdminTransaction::Ready(msg) => {
assert_eq!(msg.current_epoch, EpochIdentifier::new(0));
assert_eq!(msg.epoch_config_hash, epoch_config_hash);
}
_ => panic!("Expected Ready"),
}
}
#[test]
fn test_admin_transaction_handover_serialization() {
let epoch_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),
epoch_config,
[0u8; BLAKE3_HASH_SIZE],
);
let admin_tx = AdminTransaction::Handover(Box::new(record));
let serialized = serde_json::to_string(&admin_tx).expect("serialization should succeed");
let deserialized: AdminTransaction =
serde_json::from_str(&serialized).expect("deserialization should succeed");
match deserialized {
AdminTransaction::Handover(r) => {
assert_eq!(r.block_index(), 100);
assert_eq!(r.source_epoch(), EpochIdentifier::new(0));
assert_eq!(r.dest_epoch(), EpochIdentifier::new(1));
}
_ => panic!("Expected Handover"),
}
}
#[test]
fn test_epoch_change_config_deterministic_hash() {
let (authority_key, protocol_key, network_key) = create_test_keys(KEYGEN_SEED_BASE + 4);
let config1 = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![ValidatorInfo {
stake: 1000,
consensus_address: "/ip4/127.0.0.1/udp/10100".parse().unwrap(),
state_sync_address: "/ip4/127.0.0.1/udp/20100".parse().unwrap(),
hostname: "validator-0".to_string(),
authority_key: authority_key.clone(),
protocol_key: protocol_key.clone(),
network_key: network_key.clone(),
signing_key: Pubkey::new_from_array([3u8; 32]),
}],
consensus_config: None,
};
let config2 = config1.clone();
assert_eq!(config1.deterministic_hash(), config2.deterministic_hash());
let config3 = EpochChangeConfig {
new_epoch: EpochIdentifier::new(2),
..config1.clone()
};
assert_ne!(config1.deterministic_hash(), config3.deterministic_hash());
let config4 = EpochChangeConfig {
validators: vec![ValidatorInfo {
stake: 2000, consensus_address: "/ip4/127.0.0.1/udp/10100".parse().unwrap(),
state_sync_address: "/ip4/127.0.0.1/udp/20100".parse().unwrap(),
hostname: "validator-0".to_string(),
authority_key,
protocol_key,
network_key,
signing_key: Pubkey::new_from_array([3u8; 32]),
}],
..config1.clone()
};
assert_ne!(config1.deterministic_hash(), config4.deterministic_hash());
let config5 = EpochChangeConfig {
consensus_config: Some(EpochConsensusConfig {
num_leaders_per_round: Some(2),
..Default::default()
}),
..config1.clone()
};
assert_ne!(config1.deterministic_hash(), config5.deterministic_hash());
}
#[test]
fn test_deterministic_hash_uses_canonical_multiaddr_wire_format() {
let (authority_key, protocol_key, network_key) = create_test_keys(KEYGEN_SEED_BASE + 5);
let config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(0),
new_epoch: EpochIdentifier::new(1),
validators: vec![ValidatorInfo {
stake: 1000,
consensus_address: "/ip4/127.0.0.1/udp/10100".parse().unwrap(),
state_sync_address: "/ip4/127.0.0.1/udp/20100".parse().unwrap(),
hostname: "validator-0".to_string(),
authority_key: authority_key.clone(),
protocol_key: protocol_key.clone(),
network_key: network_key.clone(),
signing_key: Pubkey::new_from_array([3u8; 32]),
}],
consensus_config: None,
};
let mut hasher = blake3::Hasher::new();
hasher.update(&0u64.to_le_bytes()); hasher.update(&1u64.to_le_bytes()); hasher.update(&1u64.to_le_bytes());
hasher.update(&1000u64.to_le_bytes());
let consensus_addr: crate::multiaddr::Multiaddr =
"/ip4/127.0.0.1/udp/10100".parse().unwrap();
let consensus_wire = consensus_addr.to_vec();
hasher.update(&(consensus_wire.len() as u64).to_le_bytes());
hasher.update(&consensus_wire);
let state_sync_addr: crate::multiaddr::Multiaddr =
"/ip4/127.0.0.1/udp/20100".parse().unwrap();
let state_sync_wire = state_sync_addr.to_vec();
hasher.update(&(state_sync_wire.len() as u64).to_le_bytes());
hasher.update(&state_sync_wire);
let hostname = "validator-0";
hasher.update(&(hostname.len() as u64).to_le_bytes());
hasher.update(hostname.as_bytes());
hasher.update(&authority_key.to_bytes());
hasher.update(&protocol_key.to_bytes());
hasher.update(&network_key.to_bytes());
hasher.update(Pubkey::new_from_array([3u8; 32]).as_ref());
hasher.update(&[0u8]);
let expected_hash = *hasher.finalize().as_bytes();
assert_eq!(
config.deterministic_hash(),
expected_hash,
"deterministic_hash() must use Multiaddr::to_vec() canonical wire-format bytes, \
not to_string() display representation"
);
assert_eq!(config.deterministic_hash(), config.deterministic_hash());
}
#[test]
fn test_multiaddr_to_string_differs_from_to_vec() {
let addr: crate::multiaddr::Multiaddr = "/ip4/127.0.0.1/udp/10100".parse().unwrap();
let string_bytes = addr.to_string().into_bytes();
let wire_bytes = addr.to_vec();
assert_ne!(
string_bytes, wire_bytes,
"to_string() bytes and to_vec() wire-format bytes must differ — \
using the wrong one for hashing would silently break consensus"
);
}
#[test]
fn test_admin_transaction_snapshot_attestation_submission_serialization() {
let record = create_test_snapshot_record(999, 1);
let (authority_key, protocol_keypair, _) = create_test_keys_with_keypair(62);
let attestation =
Attestation::<SnapshotRecord>::sign(&record, &protocol_keypair, &authority_key)
.expect("sign() should succeed");
let submission = SnapshotAttestationSubmission::new(record.clone(), attestation);
let admin_tx =
AdminTransaction::SnapshotAttestationSubmission(Box::new(submission.clone()));
let serialized = serde_json::to_string(&admin_tx).expect("serialization should succeed");
let deserialized: AdminTransaction =
serde_json::from_str(&serialized).expect("deserialization should succeed");
match deserialized {
AdminTransaction::SnapshotAttestationSubmission(sub) => {
assert_eq!(sub.snapshot_record(), &record);
assert!(sub.validate_digest_consistency().is_ok());
}
_ => panic!("Expected SnapshotAttestationSubmission"),
}
}
#[test]
fn test_admin_transaction_epoch_change_bcs_roundtrip() {
let config = EpochChangeConfig {
current_epoch: EpochIdentifier::new(4),
new_epoch: EpochIdentifier::new(5),
validators: vec![],
consensus_config: Some(EpochConsensusConfig {
num_leaders_per_round: Some(2),
..Default::default()
}),
};
let admin_tx = AdminTransaction::EpochChange(Box::new(config));
let bytes = bcs::to_bytes(&admin_tx).expect("BCS serialization should succeed");
let deserialized: AdminTransaction =
bcs::from_bytes(&bytes).expect("BCS deserialization should succeed");
match deserialized {
AdminTransaction::EpochChange(cfg) => {
assert_eq!(cfg.new_epoch, EpochIdentifier::new(5));
}
_ => panic!("Expected EpochChange"),
}
}
#[test]
fn test_admin_transaction_handover_bcs_roundtrip() {
let epoch_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),
epoch_config,
[0u8; BLAKE3_HASH_SIZE],
);
let admin_tx = AdminTransaction::Handover(Box::new(record));
let bytes = bcs::to_bytes(&admin_tx).expect("BCS serialization should succeed");
let deserialized: AdminTransaction =
bcs::from_bytes(&bytes).expect("BCS deserialization should succeed");
match deserialized {
AdminTransaction::Handover(r) => {
assert_eq!(r.block_index(), 100);
assert_eq!(r.dest_epoch(), EpochIdentifier::new(1));
}
_ => panic!("Expected Handover"),
}
}
#[test]
fn test_admin_transaction_snapshot_attestation_submission_bcs_roundtrip() {
let record = create_test_snapshot_record(500, 2);
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");
let submission = SnapshotAttestationSubmission::new(record.clone(), attestation);
let admin_tx = AdminTransaction::SnapshotAttestationSubmission(Box::new(submission));
let bytes = bcs::to_bytes(&admin_tx).expect("BCS serialization should succeed");
let deserialized: AdminTransaction =
bcs::from_bytes(&bytes).expect("BCS deserialization should succeed");
match deserialized {
AdminTransaction::SnapshotAttestationSubmission(sub) => {
assert_eq!(sub.snapshot_record(), &record);
assert!(sub.validate_digest_consistency().is_ok());
}
_ => panic!("Expected SnapshotAttestationSubmission"),
}
}
}