use nklave_core::state::validator::ValidatorState;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Eip3076Interchange {
pub metadata: Eip3076Metadata,
pub data: Vec<Eip3076ValidatorData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Eip3076Metadata {
pub interchange_format_version: String,
pub genesis_validators_root: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Eip3076ValidatorData {
pub pubkey: String,
pub signed_blocks: Vec<Eip3076SignedBlock>,
pub signed_attestations: Vec<Eip3076SignedAttestation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Eip3076SignedBlock {
pub slot: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub signing_root: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Eip3076SignedAttestation {
pub source_epoch: String,
pub target_epoch: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub signing_root: Option<String>,
}
impl Eip3076Interchange {
pub const VERSION: &'static str = "5";
pub fn from_validators(
genesis_validators_root: [u8; 32],
validators: &[&ValidatorState],
) -> Self {
let data = validators
.iter()
.map(|v| {
let pubkey = format!("0x{}", hex::encode(v.pubkey));
let signed_blocks: Vec<_> = v
.block_signing_roots
.iter()
.map(|(slot, root)| Eip3076SignedBlock {
slot: slot.to_string(),
signing_root: Some(format!("0x{}", hex::encode(root))),
})
.collect();
let signed_attestations: Vec<_> = v
.attestation_history
.iter()
.map(|((source, target), root)| Eip3076SignedAttestation {
source_epoch: source.to_string(),
target_epoch: target.to_string(),
signing_root: Some(format!("0x{}", hex::encode(root))),
})
.collect();
Eip3076ValidatorData {
pubkey,
signed_blocks,
signed_attestations,
}
})
.collect();
Self {
metadata: Eip3076Metadata {
interchange_format_version: Self::VERSION.to_string(),
genesis_validators_root: format!("0x{}", hex::encode(genesis_validators_root)),
},
data,
}
}
pub fn import(path: impl AsRef<Path>) -> Result<Self, Eip3076Error> {
let file = File::open(path).map_err(|e| Eip3076Error::Io(e.to_string()))?;
let reader = BufReader::new(file);
serde_json::from_reader(reader).map_err(|e| Eip3076Error::Parse(e.to_string()))
}
pub fn export(&self, path: impl AsRef<Path>) -> Result<(), Eip3076Error> {
let file = File::create(path).map_err(|e| Eip3076Error::Io(e.to_string()))?;
let writer = BufWriter::new(file);
serde_json::to_writer_pretty(writer, self)
.map_err(|e| Eip3076Error::Serialize(e.to_string()))
}
pub fn genesis_validators_root(&self) -> Result<[u8; 32], Eip3076Error> {
parse_hex_bytes(&self.metadata.genesis_validators_root)
}
pub fn apply_to_validators(
&self,
validators: &mut std::collections::HashMap<[u8; 48], ValidatorState>,
) -> Result<(), Eip3076Error> {
for validator_data in &self.data {
let pubkey: [u8; 48] = parse_hex_bytes(&validator_data.pubkey)?;
let state = validators
.entry(pubkey)
.or_insert_with(|| ValidatorState::new(pubkey));
for block in &validator_data.signed_blocks {
let slot: u64 = block
.slot
.parse()
.map_err(|_| Eip3076Error::Parse("Invalid slot".to_string()))?;
let signing_root: [u8; 32] = if let Some(ref root) = block.signing_root {
parse_hex_bytes(root)?
} else {
[0u8; 32]
};
state.record_block_signing(slot, signing_root);
}
for attestation in &validator_data.signed_attestations {
let source_epoch: u64 = attestation
.source_epoch
.parse()
.map_err(|_| Eip3076Error::Parse("Invalid source_epoch".to_string()))?;
let target_epoch: u64 = attestation
.target_epoch
.parse()
.map_err(|_| Eip3076Error::Parse("Invalid target_epoch".to_string()))?;
let signing_root: [u8; 32] = if let Some(ref root) = attestation.signing_root {
parse_hex_bytes(root)?
} else {
[0u8; 32]
};
state.record_attestation_signing(source_epoch, target_epoch, signing_root);
}
}
Ok(())
}
}
fn parse_hex_bytes<const N: usize>(s: &str) -> Result<[u8; N], Eip3076Error> {
let s = s.strip_prefix("0x").unwrap_or(s);
let bytes = hex::decode(s).map_err(|e| Eip3076Error::Parse(e.to_string()))?;
if bytes.len() != N {
return Err(Eip3076Error::Parse(format!(
"Expected {} bytes, got {}",
N,
bytes.len()
)));
}
let mut arr = [0u8; N];
arr.copy_from_slice(&bytes);
Ok(arr)
}
#[derive(Debug, Error)]
pub enum Eip3076Error {
#[error("I/O error: {0}")]
Io(String),
#[error("Parse error: {0}")]
Parse(String),
#[error("Serialization error: {0}")]
Serialize(String),
#[error("Genesis validators root mismatch")]
GenesisRootMismatch,
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use tempfile::TempDir;
#[test]
fn test_eip3076_roundtrip() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("slashing_protection.json");
let mut state = ValidatorState::new([1u8; 48]);
state.record_block_signing(100, [2u8; 32]);
state.record_attestation_signing(10, 11, [3u8; 32]);
let interchange =
Eip3076Interchange::from_validators([0u8; 32], &[&state]);
interchange.export(&path).unwrap();
let loaded = Eip3076Interchange::import(&path).unwrap();
assert_eq!(loaded.data.len(), 1);
assert_eq!(loaded.data[0].signed_blocks.len(), 1);
assert_eq!(loaded.data[0].signed_attestations.len(), 1);
}
#[test]
fn test_apply_to_validators() {
let mut state = ValidatorState::new([1u8; 48]);
state.record_block_signing(100, [2u8; 32]);
let interchange =
Eip3076Interchange::from_validators([0u8; 32], &[&state]);
let mut validators = HashMap::new();
interchange.apply_to_validators(&mut validators).unwrap();
assert!(validators.contains_key(&[1u8; 48]));
let loaded_state = validators.get(&[1u8; 48]).unwrap();
assert!(loaded_state.get_block_signing_root(100).is_some());
}
}