use crate::policy::types::ChainType;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
fn serialize_bytes<S, const N: usize>(bytes: &[u8; N], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&hex::encode(bytes))
}
fn deserialize_bytes<'de, D, const N: usize>(deserializer: D) -> Result<[u8; N], D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
let s = s.strip_prefix("0x").unwrap_or(&s);
let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
if bytes.len() != N {
return Err(serde::de::Error::custom(format!(
"expected {} bytes, got {}",
N,
bytes.len()
)));
}
let mut arr = [0u8; N];
arr.copy_from_slice(&bytes);
Ok(arr)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidatorState {
#[serde(serialize_with = "serialize_bytes", deserialize_with = "deserialize_bytes")]
pub pubkey: [u8; 48],
#[serde(default)]
pub last_signed_block_slot: Option<u64>,
#[serde(default)]
pub block_signing_roots: BTreeMap<u64, [u8; 32]>,
#[serde(default)]
pub highest_source_epoch: u64,
#[serde(default)]
pub highest_target_epoch: u64,
#[serde(default)]
pub attestation_history: AttestationHistory,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chain_state: Option<ChainState>,
}
impl ValidatorState {
pub fn new(pubkey: [u8; 48]) -> Self {
Self {
pubkey,
last_signed_block_slot: None,
block_signing_roots: BTreeMap::new(),
highest_source_epoch: 0,
highest_target_epoch: 0,
attestation_history: AttestationHistory::new(),
chain_state: None, }
}
pub fn new_ethereum(pubkey: [u8; 48]) -> Self {
Self {
pubkey,
last_signed_block_slot: None,
block_signing_roots: BTreeMap::new(),
highest_source_epoch: 0,
highest_target_epoch: 0,
attestation_history: AttestationHistory::new(),
chain_state: Some(ChainState::Ethereum(EthereumState::new())),
}
}
pub fn new_cosmos(pubkey: [u8; 32]) -> Self {
let mut full_pubkey = [0u8; 48];
full_pubkey[..32].copy_from_slice(&pubkey);
Self {
pubkey: full_pubkey,
last_signed_block_slot: None,
block_signing_roots: BTreeMap::new(),
highest_source_epoch: 0,
highest_target_epoch: 0,
attestation_history: AttestationHistory::new(),
chain_state: Some(ChainState::Cosmos(CosmosState::new())),
}
}
pub fn chain_type(&self) -> ChainType {
match &self.chain_state {
Some(cs) => cs.chain_type(),
None => ChainType::Ethereum, }
}
pub fn ethereum_state(&self) -> Option<EthereumState> {
match &self.chain_state {
Some(ChainState::Ethereum(state)) => Some(state.clone()),
Some(ChainState::Cosmos(_)) => None,
None => {
Some(EthereumState {
last_signed_block_slot: self.last_signed_block_slot,
block_signing_roots: self.block_signing_roots.clone(),
highest_source_epoch: self.highest_source_epoch,
highest_target_epoch: self.highest_target_epoch,
attestation_history: self.attestation_history.clone(),
})
}
}
}
pub fn cosmos_state(&self) -> Option<&CosmosState> {
match &self.chain_state {
Some(ChainState::Cosmos(state)) => Some(state),
_ => None,
}
}
pub fn cosmos_state_mut(&mut self) -> Option<&mut CosmosState> {
match &mut self.chain_state {
Some(ChainState::Cosmos(state)) => Some(state),
_ => None,
}
}
pub fn get_block_signing_root(&self, slot: u64) -> Option<&[u8; 32]> {
match &self.chain_state {
Some(ChainState::Ethereum(state)) => state.block_signing_roots.get(&slot),
Some(ChainState::Cosmos(_)) => None,
None => self.block_signing_roots.get(&slot),
}
}
pub fn record_block_signing(&mut self, slot: u64, signing_root: [u8; 32]) {
match &mut self.chain_state {
Some(ChainState::Ethereum(state)) => {
state.record_block_signing(slot, signing_root);
}
Some(ChainState::Cosmos(_)) => {
}
None => {
self.block_signing_roots.insert(slot, signing_root);
if self.last_signed_block_slot.is_none_or(|s| slot > s) {
self.last_signed_block_slot = Some(slot);
}
}
}
}
pub fn get_attestation_signing_root(
&self,
source_epoch: u64,
target_epoch: u64,
) -> Option<&[u8; 32]> {
match &self.chain_state {
Some(ChainState::Ethereum(state)) => {
state.attestation_history.get_signing_root(source_epoch, target_epoch)
}
Some(ChainState::Cosmos(_)) => None,
None => self.attestation_history.get_signing_root(source_epoch, target_epoch),
}
}
pub fn record_attestation_signing(
&mut self,
source_epoch: u64,
target_epoch: u64,
signing_root: [u8; 32],
) {
match &mut self.chain_state {
Some(ChainState::Ethereum(state)) => {
state.record_attestation_signing(source_epoch, target_epoch, signing_root);
}
Some(ChainState::Cosmos(_)) => {
}
None => {
self.attestation_history.record(source_epoch, target_epoch, signing_root);
if source_epoch > self.highest_source_epoch {
self.highest_source_epoch = source_epoch;
}
if target_epoch > self.highest_target_epoch {
self.highest_target_epoch = target_epoch;
}
}
}
}
pub fn prune(&mut self, min_slot: u64, min_epoch: u64) {
match &mut self.chain_state {
Some(ChainState::Ethereum(state)) => {
state.prune(min_slot, min_epoch);
}
Some(ChainState::Cosmos(state)) => {
state.prune(min_slot as i64);
}
None => {
self.block_signing_roots = self.block_signing_roots.split_off(&min_slot);
self.attestation_history.prune(min_epoch);
}
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AttestationHistory {
signed_attestations: BTreeMap<(u64, u64), [u8; 32]>,
min_target_by_source: BTreeMap<u64, u64>,
max_target_by_source: BTreeMap<u64, u64>,
}
impl AttestationHistory {
pub fn new() -> Self {
Self::default()
}
pub fn get_signing_root(&self, source_epoch: u64, target_epoch: u64) -> Option<&[u8; 32]> {
self.signed_attestations.get(&(source_epoch, target_epoch))
}
pub fn iter(&self) -> impl Iterator<Item = ((u64, u64), &[u8; 32])> {
self.signed_attestations.iter().map(|(&k, v)| (k, v))
}
pub fn record(&mut self, source_epoch: u64, target_epoch: u64, signing_root: [u8; 32]) {
self.signed_attestations
.insert((source_epoch, target_epoch), signing_root);
self.min_target_by_source
.entry(source_epoch)
.and_modify(|t| {
if target_epoch < *t {
*t = target_epoch;
}
})
.or_insert(target_epoch);
self.max_target_by_source
.entry(source_epoch)
.and_modify(|t| {
if target_epoch > *t {
*t = target_epoch;
}
})
.or_insert(target_epoch);
}
pub fn get_min_target_for_source_gt(&self, source_epoch: u64) -> Option<u64> {
self.min_target_by_source
.range((source_epoch + 1)..)
.map(|(_, &target)| target)
.min()
}
pub fn get_max_target_for_source_lt(&self, source_epoch: u64) -> Option<u64> {
self.max_target_by_source
.range(..source_epoch)
.map(|(_, &target)| target)
.max()
}
pub fn prune(&mut self, min_epoch: u64) {
self.signed_attestations
.retain(|(source, _), _| *source >= min_epoch);
self.min_target_by_source
.retain(|source, _| *source >= min_epoch);
self.max_target_by_source
.retain(|source, _| *source >= min_epoch);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum CosmosSignedMsgType {
Prevote,
Precommit,
Proposal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CosmosSignedVote {
pub block_hash: Option<[u8; 32]>,
pub signed_at: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CosmosState {
pub chain_id: Option<String>,
signed_votes: BTreeMap<(i64, i32, CosmosSignedMsgType), CosmosSignedVote>,
pub highest_height: i64,
}
impl CosmosState {
pub fn new() -> Self {
Self::default()
}
pub fn get_signed_vote(
&self,
height: i64,
round: i32,
msg_type: CosmosSignedMsgType,
) -> Option<&CosmosSignedVote> {
self.signed_votes.get(&(height, round, msg_type))
}
pub fn record_vote(
&mut self,
height: i64,
round: i32,
msg_type: CosmosSignedMsgType,
block_hash: Option<[u8; 32]>,
) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.signed_votes.insert(
(height, round, msg_type),
CosmosSignedVote {
block_hash,
signed_at: now,
},
);
if height > self.highest_height {
self.highest_height = height;
}
}
pub fn prune(&mut self, min_height: i64) {
self.signed_votes.retain(|(height, _, _), _| *height >= min_height);
}
pub fn len(&self) -> usize {
self.signed_votes.len()
}
pub fn is_empty(&self) -> bool {
self.signed_votes.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "chain_type")]
pub enum ChainState {
Ethereum(EthereumState),
Cosmos(CosmosState),
}
impl ChainState {
pub fn chain_type(&self) -> ChainType {
match self {
ChainState::Ethereum(_) => ChainType::Ethereum,
ChainState::Cosmos(_) => ChainType::Cosmos,
}
}
pub fn as_ethereum(&self) -> Option<&EthereumState> {
match self {
ChainState::Ethereum(state) => Some(state),
_ => None,
}
}
pub fn as_ethereum_mut(&mut self) -> Option<&mut EthereumState> {
match self {
ChainState::Ethereum(state) => Some(state),
_ => None,
}
}
pub fn as_cosmos(&self) -> Option<&CosmosState> {
match self {
ChainState::Cosmos(state) => Some(state),
_ => None,
}
}
pub fn as_cosmos_mut(&mut self) -> Option<&mut CosmosState> {
match self {
ChainState::Cosmos(state) => Some(state),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EthereumState {
pub last_signed_block_slot: Option<u64>,
pub block_signing_roots: BTreeMap<u64, [u8; 32]>,
pub highest_source_epoch: u64,
pub highest_target_epoch: u64,
pub attestation_history: AttestationHistory,
}
impl Default for EthereumState {
fn default() -> Self {
Self::new()
}
}
impl EthereumState {
pub fn new() -> Self {
Self {
last_signed_block_slot: None,
block_signing_roots: BTreeMap::new(),
highest_source_epoch: 0,
highest_target_epoch: 0,
attestation_history: AttestationHistory::new(),
}
}
pub fn get_block_signing_root(&self, slot: u64) -> Option<&[u8; 32]> {
self.block_signing_roots.get(&slot)
}
pub fn record_block_signing(&mut self, slot: u64, signing_root: [u8; 32]) {
self.block_signing_roots.insert(slot, signing_root);
if self.last_signed_block_slot.is_none_or(|s| slot > s) {
self.last_signed_block_slot = Some(slot);
}
}
pub fn get_attestation_signing_root(
&self,
source_epoch: u64,
target_epoch: u64,
) -> Option<&[u8; 32]> {
self.attestation_history
.get_signing_root(source_epoch, target_epoch)
}
pub fn record_attestation_signing(
&mut self,
source_epoch: u64,
target_epoch: u64,
signing_root: [u8; 32],
) {
self.attestation_history
.record(source_epoch, target_epoch, signing_root);
if source_epoch > self.highest_source_epoch {
self.highest_source_epoch = source_epoch;
}
if target_epoch > self.highest_target_epoch {
self.highest_target_epoch = target_epoch;
}
}
pub fn prune(&mut self, min_slot: u64, min_epoch: u64) {
self.block_signing_roots = self.block_signing_roots.split_off(&min_slot);
self.attestation_history.prune(min_epoch);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_root(val: u8) -> [u8; 32] {
let mut root = [0u8; 32];
root[0] = val;
root
}
#[test]
fn test_validator_state_new() {
let pubkey = [1u8; 48];
let state = ValidatorState::new(pubkey);
assert_eq!(state.pubkey, pubkey);
assert_eq!(state.last_signed_block_slot, None);
assert!(state.block_signing_roots.is_empty());
assert_eq!(state.highest_source_epoch, 0);
assert_eq!(state.highest_target_epoch, 0);
}
#[test]
fn test_block_signing() {
let mut state = ValidatorState::new([0u8; 48]);
let root = make_root(1);
assert!(state.get_block_signing_root(100).is_none());
state.record_block_signing(100, root);
assert_eq!(state.get_block_signing_root(100), Some(&root));
assert_eq!(state.last_signed_block_slot, Some(100));
}
#[test]
fn test_attestation_signing() {
let mut state = ValidatorState::new([0u8; 48]);
let root = make_root(1);
assert!(state.get_attestation_signing_root(10, 11).is_none());
state.record_attestation_signing(10, 11, root);
assert_eq!(state.get_attestation_signing_root(10, 11), Some(&root));
assert_eq!(state.highest_source_epoch, 10);
assert_eq!(state.highest_target_epoch, 11);
}
#[test]
fn test_surround_detection_spans() {
let mut history = AttestationHistory::new();
history.record(5, 10, make_root(1));
assert_eq!(history.get_min_target_for_source_gt(4), Some(10));
assert_eq!(history.get_min_target_for_source_gt(5), None);
assert_eq!(history.get_max_target_for_source_lt(5), None);
history.record(3, 12, make_root(2));
assert_eq!(history.get_max_target_for_source_lt(5), Some(12));
}
}