1use crate::policy::types::ChainType;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use std::collections::BTreeMap;
9
10fn serialize_bytes<S, const N: usize>(bytes: &[u8; N], serializer: S) -> Result<S::Ok, S::Error>
12where
13 S: Serializer,
14{
15 serializer.serialize_str(&hex::encode(bytes))
16}
17
18fn deserialize_bytes<'de, D, const N: usize>(deserializer: D) -> Result<[u8; N], D::Error>
20where
21 D: Deserializer<'de>,
22{
23 let s: String = Deserialize::deserialize(deserializer)?;
24 let s = s.strip_prefix("0x").unwrap_or(&s);
25 let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
26 if bytes.len() != N {
27 return Err(serde::de::Error::custom(format!(
28 "expected {} bytes, got {}",
29 N,
30 bytes.len()
31 )));
32 }
33 let mut arr = [0u8; N];
34 arr.copy_from_slice(&bytes);
35 Ok(arr)
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ValidatorState {
44 #[serde(serialize_with = "serialize_bytes", deserialize_with = "deserialize_bytes")]
48 pub pubkey: [u8; 48],
49
50 #[serde(default)]
54 pub last_signed_block_slot: Option<u64>,
55
56 #[serde(default)]
58 pub block_signing_roots: BTreeMap<u64, [u8; 32]>,
59
60 #[serde(default)]
62 pub highest_source_epoch: u64,
63
64 #[serde(default)]
66 pub highest_target_epoch: u64,
67
68 #[serde(default)]
70 pub attestation_history: AttestationHistory,
71
72 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub chain_state: Option<ChainState>,
77}
78
79impl ValidatorState {
80 pub fn new(pubkey: [u8; 48]) -> Self {
82 Self {
83 pubkey,
84 last_signed_block_slot: None,
85 block_signing_roots: BTreeMap::new(),
86 highest_source_epoch: 0,
87 highest_target_epoch: 0,
88 attestation_history: AttestationHistory::new(),
89 chain_state: None, }
91 }
92
93 pub fn new_ethereum(pubkey: [u8; 48]) -> Self {
95 Self {
96 pubkey,
97 last_signed_block_slot: None,
98 block_signing_roots: BTreeMap::new(),
99 highest_source_epoch: 0,
100 highest_target_epoch: 0,
101 attestation_history: AttestationHistory::new(),
102 chain_state: Some(ChainState::Ethereum(EthereumState::new())),
103 }
104 }
105
106 pub fn new_cosmos(pubkey: [u8; 32]) -> Self {
110 let mut full_pubkey = [0u8; 48];
111 full_pubkey[..32].copy_from_slice(&pubkey);
112
113 Self {
114 pubkey: full_pubkey,
115 last_signed_block_slot: None,
117 block_signing_roots: BTreeMap::new(),
118 highest_source_epoch: 0,
119 highest_target_epoch: 0,
120 attestation_history: AttestationHistory::new(),
121 chain_state: Some(ChainState::Cosmos(CosmosState::new())),
122 }
123 }
124
125 pub fn chain_type(&self) -> ChainType {
127 match &self.chain_state {
128 Some(cs) => cs.chain_type(),
129 None => ChainType::Ethereum, }
131 }
132
133 pub fn ethereum_state(&self) -> Option<EthereumState> {
137 match &self.chain_state {
138 Some(ChainState::Ethereum(state)) => Some(state.clone()),
139 Some(ChainState::Cosmos(_)) => None,
140 None => {
141 Some(EthereumState {
143 last_signed_block_slot: self.last_signed_block_slot,
144 block_signing_roots: self.block_signing_roots.clone(),
145 highest_source_epoch: self.highest_source_epoch,
146 highest_target_epoch: self.highest_target_epoch,
147 attestation_history: self.attestation_history.clone(),
148 })
149 }
150 }
151 }
152
153 pub fn cosmos_state(&self) -> Option<&CosmosState> {
155 match &self.chain_state {
156 Some(ChainState::Cosmos(state)) => Some(state),
157 _ => None,
158 }
159 }
160
161 pub fn cosmos_state_mut(&mut self) -> Option<&mut CosmosState> {
163 match &mut self.chain_state {
164 Some(ChainState::Cosmos(state)) => Some(state),
165 _ => None,
166 }
167 }
168
169 pub fn get_block_signing_root(&self, slot: u64) -> Option<&[u8; 32]> {
175 match &self.chain_state {
176 Some(ChainState::Ethereum(state)) => state.block_signing_roots.get(&slot),
177 Some(ChainState::Cosmos(_)) => None,
178 None => self.block_signing_roots.get(&slot),
179 }
180 }
181
182 pub fn record_block_signing(&mut self, slot: u64, signing_root: [u8; 32]) {
184 match &mut self.chain_state {
185 Some(ChainState::Ethereum(state)) => {
186 state.record_block_signing(slot, signing_root);
187 }
188 Some(ChainState::Cosmos(_)) => {
189 }
191 None => {
192 self.block_signing_roots.insert(slot, signing_root);
194 if self.last_signed_block_slot.is_none_or(|s| slot > s) {
195 self.last_signed_block_slot = Some(slot);
196 }
197 }
198 }
199 }
200
201 pub fn get_attestation_signing_root(
203 &self,
204 source_epoch: u64,
205 target_epoch: u64,
206 ) -> Option<&[u8; 32]> {
207 match &self.chain_state {
208 Some(ChainState::Ethereum(state)) => {
209 state.attestation_history.get_signing_root(source_epoch, target_epoch)
210 }
211 Some(ChainState::Cosmos(_)) => None,
212 None => self.attestation_history.get_signing_root(source_epoch, target_epoch),
213 }
214 }
215
216 pub fn record_attestation_signing(
218 &mut self,
219 source_epoch: u64,
220 target_epoch: u64,
221 signing_root: [u8; 32],
222 ) {
223 match &mut self.chain_state {
224 Some(ChainState::Ethereum(state)) => {
225 state.record_attestation_signing(source_epoch, target_epoch, signing_root);
226 }
227 Some(ChainState::Cosmos(_)) => {
228 }
230 None => {
231 self.attestation_history.record(source_epoch, target_epoch, signing_root);
233 if source_epoch > self.highest_source_epoch {
234 self.highest_source_epoch = source_epoch;
235 }
236 if target_epoch > self.highest_target_epoch {
237 self.highest_target_epoch = target_epoch;
238 }
239 }
240 }
241 }
242
243 pub fn prune(&mut self, min_slot: u64, min_epoch: u64) {
246 match &mut self.chain_state {
247 Some(ChainState::Ethereum(state)) => {
248 state.prune(min_slot, min_epoch);
249 }
250 Some(ChainState::Cosmos(state)) => {
251 state.prune(min_slot as i64);
253 }
254 None => {
255 self.block_signing_roots = self.block_signing_roots.split_off(&min_slot);
257 self.attestation_history.prune(min_epoch);
258 }
259 }
260 }
261}
262
263#[derive(Debug, Clone, Default, Serialize, Deserialize)]
267pub struct AttestationHistory {
268 signed_attestations: BTreeMap<(u64, u64), [u8; 32]>,
271
272 min_target_by_source: BTreeMap<u64, u64>,
275
276 max_target_by_source: BTreeMap<u64, u64>,
279}
280
281impl AttestationHistory {
282 pub fn new() -> Self {
284 Self::default()
285 }
286
287 pub fn get_signing_root(&self, source_epoch: u64, target_epoch: u64) -> Option<&[u8; 32]> {
289 self.signed_attestations.get(&(source_epoch, target_epoch))
290 }
291
292 pub fn iter(&self) -> impl Iterator<Item = ((u64, u64), &[u8; 32])> {
294 self.signed_attestations.iter().map(|(&k, v)| (k, v))
295 }
296
297 pub fn record(&mut self, source_epoch: u64, target_epoch: u64, signing_root: [u8; 32]) {
299 self.signed_attestations
300 .insert((source_epoch, target_epoch), signing_root);
301
302 self.min_target_by_source
304 .entry(source_epoch)
305 .and_modify(|t| {
306 if target_epoch < *t {
307 *t = target_epoch;
308 }
309 })
310 .or_insert(target_epoch);
311
312 self.max_target_by_source
314 .entry(source_epoch)
315 .and_modify(|t| {
316 if target_epoch > *t {
317 *t = target_epoch;
318 }
319 })
320 .or_insert(target_epoch);
321 }
322
323 pub fn get_min_target_for_source_gt(&self, source_epoch: u64) -> Option<u64> {
326 self.min_target_by_source
329 .range((source_epoch + 1)..)
330 .map(|(_, &target)| target)
331 .min()
332 }
333
334 pub fn get_max_target_for_source_lt(&self, source_epoch: u64) -> Option<u64> {
337 self.max_target_by_source
340 .range(..source_epoch)
341 .map(|(_, &target)| target)
342 .max()
343 }
344
345 pub fn prune(&mut self, min_epoch: u64) {
347 self.signed_attestations
348 .retain(|(source, _), _| *source >= min_epoch);
349 self.min_target_by_source
350 .retain(|source, _| *source >= min_epoch);
351 self.max_target_by_source
352 .retain(|source, _| *source >= min_epoch);
353 }
354}
355
356#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
362pub enum CosmosSignedMsgType {
363 Prevote,
365 Precommit,
367 Proposal,
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct CosmosSignedVote {
374 pub block_hash: Option<[u8; 32]>,
376 pub signed_at: u64,
378}
379
380#[derive(Debug, Clone, Default, Serialize, Deserialize)]
384pub struct CosmosState {
385 pub chain_id: Option<String>,
387
388 signed_votes: BTreeMap<(i64, i32, CosmosSignedMsgType), CosmosSignedVote>,
391
392 pub highest_height: i64,
394}
395
396impl CosmosState {
397 pub fn new() -> Self {
399 Self::default()
400 }
401
402 pub fn get_signed_vote(
404 &self,
405 height: i64,
406 round: i32,
407 msg_type: CosmosSignedMsgType,
408 ) -> Option<&CosmosSignedVote> {
409 self.signed_votes.get(&(height, round, msg_type))
410 }
411
412 pub fn record_vote(
414 &mut self,
415 height: i64,
416 round: i32,
417 msg_type: CosmosSignedMsgType,
418 block_hash: Option<[u8; 32]>,
419 ) {
420 let now = std::time::SystemTime::now()
421 .duration_since(std::time::UNIX_EPOCH)
422 .unwrap_or_default()
423 .as_secs();
424
425 self.signed_votes.insert(
426 (height, round, msg_type),
427 CosmosSignedVote {
428 block_hash,
429 signed_at: now,
430 },
431 );
432
433 if height > self.highest_height {
434 self.highest_height = height;
435 }
436 }
437
438 pub fn prune(&mut self, min_height: i64) {
442 self.signed_votes.retain(|(height, _, _), _| *height >= min_height);
443 }
444
445 pub fn len(&self) -> usize {
447 self.signed_votes.len()
448 }
449
450 pub fn is_empty(&self) -> bool {
452 self.signed_votes.is_empty()
453 }
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
462#[serde(tag = "chain_type")]
463pub enum ChainState {
464 Ethereum(EthereumState),
466 Cosmos(CosmosState),
468}
469
470impl ChainState {
471 pub fn chain_type(&self) -> ChainType {
473 match self {
474 ChainState::Ethereum(_) => ChainType::Ethereum,
475 ChainState::Cosmos(_) => ChainType::Cosmos,
476 }
477 }
478
479 pub fn as_ethereum(&self) -> Option<&EthereumState> {
481 match self {
482 ChainState::Ethereum(state) => Some(state),
483 _ => None,
484 }
485 }
486
487 pub fn as_ethereum_mut(&mut self) -> Option<&mut EthereumState> {
489 match self {
490 ChainState::Ethereum(state) => Some(state),
491 _ => None,
492 }
493 }
494
495 pub fn as_cosmos(&self) -> Option<&CosmosState> {
497 match self {
498 ChainState::Cosmos(state) => Some(state),
499 _ => None,
500 }
501 }
502
503 pub fn as_cosmos_mut(&mut self) -> Option<&mut CosmosState> {
505 match self {
506 ChainState::Cosmos(state) => Some(state),
507 _ => None,
508 }
509 }
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize)]
516pub struct EthereumState {
517 pub last_signed_block_slot: Option<u64>,
519
520 pub block_signing_roots: BTreeMap<u64, [u8; 32]>,
523
524 pub highest_source_epoch: u64,
526
527 pub highest_target_epoch: u64,
529
530 pub attestation_history: AttestationHistory,
532}
533
534impl Default for EthereumState {
535 fn default() -> Self {
536 Self::new()
537 }
538}
539
540impl EthereumState {
541 pub fn new() -> Self {
543 Self {
544 last_signed_block_slot: None,
545 block_signing_roots: BTreeMap::new(),
546 highest_source_epoch: 0,
547 highest_target_epoch: 0,
548 attestation_history: AttestationHistory::new(),
549 }
550 }
551
552 pub fn get_block_signing_root(&self, slot: u64) -> Option<&[u8; 32]> {
554 self.block_signing_roots.get(&slot)
555 }
556
557 pub fn record_block_signing(&mut self, slot: u64, signing_root: [u8; 32]) {
559 self.block_signing_roots.insert(slot, signing_root);
560 if self.last_signed_block_slot.is_none_or(|s| slot > s) {
561 self.last_signed_block_slot = Some(slot);
562 }
563 }
564
565 pub fn get_attestation_signing_root(
567 &self,
568 source_epoch: u64,
569 target_epoch: u64,
570 ) -> Option<&[u8; 32]> {
571 self.attestation_history
572 .get_signing_root(source_epoch, target_epoch)
573 }
574
575 pub fn record_attestation_signing(
577 &mut self,
578 source_epoch: u64,
579 target_epoch: u64,
580 signing_root: [u8; 32],
581 ) {
582 self.attestation_history
583 .record(source_epoch, target_epoch, signing_root);
584
585 if source_epoch > self.highest_source_epoch {
586 self.highest_source_epoch = source_epoch;
587 }
588 if target_epoch > self.highest_target_epoch {
589 self.highest_target_epoch = target_epoch;
590 }
591 }
592
593 pub fn prune(&mut self, min_slot: u64, min_epoch: u64) {
595 self.block_signing_roots = self.block_signing_roots.split_off(&min_slot);
597
598 self.attestation_history.prune(min_epoch);
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606
607 fn make_root(val: u8) -> [u8; 32] {
608 let mut root = [0u8; 32];
609 root[0] = val;
610 root
611 }
612
613 #[test]
614 fn test_validator_state_new() {
615 let pubkey = [1u8; 48];
616 let state = ValidatorState::new(pubkey);
617
618 assert_eq!(state.pubkey, pubkey);
619 assert_eq!(state.last_signed_block_slot, None);
620 assert!(state.block_signing_roots.is_empty());
621 assert_eq!(state.highest_source_epoch, 0);
622 assert_eq!(state.highest_target_epoch, 0);
623 }
624
625 #[test]
626 fn test_block_signing() {
627 let mut state = ValidatorState::new([0u8; 48]);
628 let root = make_root(1);
629
630 assert!(state.get_block_signing_root(100).is_none());
631
632 state.record_block_signing(100, root);
633
634 assert_eq!(state.get_block_signing_root(100), Some(&root));
635 assert_eq!(state.last_signed_block_slot, Some(100));
636 }
637
638 #[test]
639 fn test_attestation_signing() {
640 let mut state = ValidatorState::new([0u8; 48]);
641 let root = make_root(1);
642
643 assert!(state.get_attestation_signing_root(10, 11).is_none());
644
645 state.record_attestation_signing(10, 11, root);
646
647 assert_eq!(state.get_attestation_signing_root(10, 11), Some(&root));
648 assert_eq!(state.highest_source_epoch, 10);
649 assert_eq!(state.highest_target_epoch, 11);
650 }
651
652 #[test]
653 fn test_surround_detection_spans() {
654 let mut history = AttestationHistory::new();
655
656 history.record(5, 10, make_root(1));
658
659 assert_eq!(history.get_min_target_for_source_gt(4), Some(10));
662 assert_eq!(history.get_min_target_for_source_gt(5), None);
663
664 assert_eq!(history.get_max_target_for_source_lt(5), None);
666
667 history.record(3, 12, make_root(2));
669
670 assert_eq!(history.get_max_target_for_source_lt(5), Some(12));
672 }
673}