From 6b19780c7b6989aa311c5bfabd4a74dfb22acb45 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Tue, 18 Nov 2025 20:50:32 -0500 Subject: [PATCH] Remove historical state access from Serai Resolves https://github.com/serai-dex/serai/issues/694. --- Cargo.lock | 29 +- coordinator/cosign/src/intend.rs | 137 ++++++-- coordinator/cosign/src/lib.rs | 67 +--- coordinator/p2p/libp2p/src/validators.rs | 9 +- coordinator/src/db.rs | 1 - coordinator/substrate/Cargo.toml | 1 - coordinator/substrate/src/canonical.rs | 22 +- coordinator/substrate/src/ephemeral.rs | 119 ++++--- .../substrate/src/publish_slash_report.rs | 3 +- coordinator/substrate/src/set_keys.rs | 4 +- message-queue/src/main.rs | 1 - substrate/abi/src/validator_sets.rs | 6 +- substrate/client/serai/Cargo.toml | 2 - substrate/client/serai/src/coins.rs | 73 +---- substrate/client/serai/src/in_instructions.rs | 40 +-- substrate/client/serai/src/lib.rs | 153 ++++----- substrate/client/serai/src/validator_sets.rs | 310 ++++++++---------- substrate/client/serai/tests/blockchain.rs | 6 +- .../client/serai/tests/validator_sets.rs | 126 ++++--- substrate/primitives/src/network_id.rs | 1 - .../src/embedded_elliptic_curve_keys.rs | 8 +- substrate/validator-sets/src/lib.rs | 35 +- 22 files changed, 531 insertions(+), 622 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e68352ab..15165b46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,9 +92,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-chains" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaa9ea039a6f9304b4a593d780b1f23e1ae183acdee938b11b38795acacc9f1" +checksum = "3ef6e7627b842406f449f83ae1a437a01cd244bc246d66f102cee9c0435ce10d" dependencies = [ "alloy-primitives", "num_enum", @@ -979,17 +979,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "async-lock" -version = "3.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -1636,9 +1625,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8" dependencies = [ "clap_builder", "clap_derive", @@ -1646,9 +1635,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1" dependencies = [ "anstream", "anstyle", @@ -6750,9 +6739,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "resolv-conf" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "revm" @@ -8334,7 +8323,6 @@ dependencies = [ name = "serai-client-serai" version = "0.1.0" dependencies = [ - "async-lock", "bitvec", "blake2 0.11.0-rc.3", "borsh", @@ -8451,7 +8439,6 @@ dependencies = [ "serai-db", "serai-processor-messages", "serai-task", - "tokio", ] [[package]] diff --git a/coordinator/cosign/src/intend.rs b/coordinator/cosign/src/intend.rs index 90935b1d..dbe5bbfa 100644 --- a/coordinator/cosign/src/intend.rs +++ b/coordinator/cosign/src/intend.rs @@ -4,11 +4,18 @@ use std::{sync::Arc, collections::HashMap}; use blake2::{Digest, Blake2b256}; use serai_client_serai::{ - abi::primitives::{ - balance::Amount, validator_sets::ExternalValidatorSet, address::SeraiAddress, - merkle::IncrementalUnbalancedMerkleTree, + abi::{ + primitives::{ + network_id::{ExternalNetworkId, NetworkId}, + balance::Amount, + crypto::Public, + validator_sets::{Session, ExternalValidatorSet}, + address::SeraiAddress, + merkle::IncrementalUnbalancedMerkleTree, + }, + validator_sets::Event, }, - Serai, + Serai, Events, }; use serai_db::*; @@ -16,10 +23,20 @@ use serai_task::ContinuallyRan; use crate::*; +#[derive(BorshSerialize, BorshDeserialize)] +struct Set { + session: Session, + key: Public, + stake: Amount, +} + create_db!( CosignIntend { ScanCosignFrom: () -> u64, BuildsUpon: () -> IncrementalUnbalancedMerkleTree, + Stakes: (network: ExternalNetworkId, validator: SeraiAddress) -> Amount, + Validators: (set: ExternalValidatorSet) -> Vec, + LatestSet: (network: ExternalNetworkId) -> Set, } ); @@ -40,23 +57,38 @@ db_channel! { async fn block_has_events_justifying_a_cosign( serai: &Serai, block_number: u64, -) -> Result<(Block, HasEvents), String> { +) -> Result<(Block, Events, HasEvents), String> { let block = serai .block_by_number(block_number) .await .map_err(|e| format!("{e:?}"))? .ok_or_else(|| "couldn't get block which should've been finalized".to_string())?; - let serai = serai.as_of(block.header.hash()).await.map_err(|e| format!("{e:?}"))?; + let events = serai.events(block.header.hash()).await.map_err(|e| format!("{e:?}"))?; - if !serai.validator_sets().set_keys_events().await.map_err(|e| format!("{e:?}"))?.is_empty() { - return Ok((block, HasEvents::Notable)); + if events.validator_sets().set_keys_events().next().is_some() { + return Ok((block, events, HasEvents::Notable)); } - if !serai.coins().burn_with_instruction_events().await.map_err(|e| format!("{e:?}"))?.is_empty() { - return Ok((block, HasEvents::NonNotable)); + if events.coins().burn_with_instruction_events().next().is_some() { + return Ok((block, events, HasEvents::NonNotable)); } - Ok((block, HasEvents::No)) + Ok((block, events, HasEvents::No)) +} + +// Fetch the `ExternalValidatorSet`s, and their associated keys, used for cosigning as of this +// block. +fn cosigning_sets(getter: &impl Get) -> Vec<(ExternalValidatorSet, Public, Amount)> { + let mut sets = vec![]; + for network in ExternalNetworkId::all() { + let Some(Set { session, key, stake }) = LatestSet::get(getter, network) else { + // If this network doesn't have usable keys, move on + continue; + }; + + sets.push((ExternalValidatorSet { network, session }, key, stake)); + } + sets } /// A task to determine which blocks we should intend to cosign. @@ -77,7 +109,7 @@ impl ContinuallyRan for CosignIntendTask { for block_number in start_block_number ..= latest_block_number { let mut txn = self.db.txn(); - let (block, mut has_events) = + let (block, events, mut has_events) = block_has_events_justifying_a_cosign(&self.serai, block_number) .await .map_err(|e| format!("{e:?}"))?; @@ -105,32 +137,75 @@ impl ContinuallyRan for CosignIntendTask { ); BuildsUpon::set(&mut txn, &builds_upon); + // Update the stakes + for event in events.validator_sets().allocation_events() { + let Event::Allocation { validator, network, amount } = event else { + panic!("event from `allocation_events` wasn't `Event::Allocation`") + }; + let Ok(network) = ExternalNetworkId::try_from(*network) else { continue }; + let existing = Stakes::get(&txn, network, *validator).unwrap_or(Amount(0)); + Stakes::set(&mut txn, network, *validator, &Amount(existing.0 + amount.0)); + } + for event in events.validator_sets().deallocation_events() { + let Event::Deallocation { validator, network, amount, timeline: _ } = event else { + panic!("event from `deallocation_events` wasn't `Event::Deallocation`") + }; + let Ok(network) = ExternalNetworkId::try_from(*network) else { continue }; + let existing = Stakes::get(&txn, network, *validator).unwrap_or(Amount(0)); + Stakes::set(&mut txn, network, *validator, &Amount(existing.0 - amount.0)); + } + + // Handle decided sets + for event in events.validator_sets().set_decided_events() { + let Event::SetDecided { set, validators } = event else { + panic!("event from `set_decided_events` wasn't `Event::SetDecided`") + }; + + let Ok(set) = ExternalValidatorSet::try_from(*set) else { continue }; + Validators::set( + &mut txn, + set, + &validators.iter().map(|(validator, _key_shares)| *validator).collect(), + ); + } + + // Handle declarations of the latest set + for event in events.validator_sets().set_keys_events() { + let Event::SetKeys { set, key_pair } = event else { + panic!("event from `set_keys_events` wasn't `Event::SetKeys`") + }; + let mut stake = 0; + for validator in + Validators::take(&mut txn, *set).expect("set which wasn't decided set keys") + { + stake += Stakes::get(&txn, set.network, validator).unwrap_or(Amount(0)).0; + } + LatestSet::set( + &mut txn, + set.network, + &Set { session: set.session, key: key_pair.0, stake: Amount(stake) }, + ); + } + let global_session_for_this_block = LatestGlobalSessionIntended::get(&txn); // If this is notable, it creates a new global session, which we index into the database // now if has_events == HasEvents::Notable { - let serai = self.serai.as_of(block_hash).await.map_err(|e| format!("{e:?}"))?; - let sets_and_keys = cosigning_sets(&serai).await?; - let global_session = - GlobalSession::id(sets_and_keys.iter().map(|(set, _key)| *set).collect()); + let sets_and_keys_and_stakes = cosigning_sets(&txn); + let global_session = GlobalSession::id( + sets_and_keys_and_stakes.iter().map(|(set, _key, _stake)| *set).collect(), + ); - let mut sets = Vec::with_capacity(sets_and_keys.len()); - let mut keys = HashMap::with_capacity(sets_and_keys.len()); - let mut stakes = HashMap::with_capacity(sets_and_keys.len()); + let mut sets = Vec::with_capacity(sets_and_keys_and_stakes.len()); + let mut keys = HashMap::with_capacity(sets_and_keys_and_stakes.len()); + let mut stakes = HashMap::with_capacity(sets_and_keys_and_stakes.len()); let mut total_stake = 0; - for (set, key) in &sets_and_keys { - sets.push(*set); - keys.insert(set.network, SeraiAddress::from(*key)); - let stake = serai - .validator_sets() - .current_stake(set.network.into()) - .await - .map_err(|e| format!("{e:?}"))? - .unwrap_or(Amount(0)) - .0; - stakes.insert(set.network, stake); - total_stake += stake; + for (set, key, stake) in sets_and_keys_and_stakes { + sets.push(set); + keys.insert(set.network, key); + stakes.insert(set.network, stake.0); + total_stake += stake.0; } if total_stake == 0 { Err(format!("cosigning sets for block #{block_number} had 0 stake in total"))?; diff --git a/coordinator/cosign/src/lib.rs b/coordinator/cosign/src/lib.rs index b89f6b3f..781e0c27 100644 --- a/coordinator/cosign/src/lib.rs +++ b/coordinator/cosign/src/lib.rs @@ -20,7 +20,7 @@ use serai_client_serai::{ }, Block, }, - Serai, TemporalSerai, + Serai, State, }; use serai_db::*; @@ -59,7 +59,7 @@ use delay::LatestCosignedBlockNumber; pub(crate) struct GlobalSession { pub(crate) start_block_number: u64, pub(crate) sets: Vec, - pub(crate) keys: HashMap, + pub(crate) keys: HashMap, pub(crate) stakes: HashMap, pub(crate) total_stake: u64, } @@ -121,60 +121,6 @@ create_db! { } } -/// Fetch the keys used for cosigning by a specific network. -async fn keys_for_network( - serai: &TemporalSerai<'_>, - network: ExternalNetworkId, -) -> Result, String> { - let Some(latest_session) = - serai.validator_sets().current_session(network.into()).await.map_err(|e| format!("{e:?}"))? - else { - // If this network hasn't had a session declared, move on - return Ok(None); - }; - - // Get the keys for the latest session - if let Some(keys) = serai - .validator_sets() - .keys(ExternalValidatorSet { network, session: latest_session }) - .await - .map_err(|e| format!("{e:?}"))? - { - return Ok(Some((latest_session, keys))); - } - - // If the latest session has yet to set keys, use the prior session - if let Some(prior_session) = latest_session.0.checked_sub(1).map(Session) { - if let Some(keys) = serai - .validator_sets() - .keys(ExternalValidatorSet { network, session: prior_session }) - .await - .map_err(|e| format!("{e:?}"))? - { - return Ok(Some((prior_session, keys))); - } - } - - Ok(None) -} - -/// Fetch the `ExternalValidatorSet`s, and their associated keys, used for cosigning as of this -/// block. -async fn cosigning_sets( - serai: &TemporalSerai<'_>, -) -> Result, String> { - let mut sets = vec![]; - for network in ExternalNetworkId::all() { - let Some((session, keys)) = keys_for_network(serai, network).await? else { - // If this network doesn't have usable keys, move on - continue; - }; - - sets.push((ExternalValidatorSet { network, session }, keys.0)); - } - Ok(sets) -} - /// An object usable to request notable cosigns for a block. pub trait RequestNotableCosigns: 'static + Send { /// The error type which may be encountered when requesting notable cosigns. @@ -379,13 +325,8 @@ impl Cosigning { // Check the cosign's signature { - let key = Public::from({ - let Some(key) = global_session.keys.get(&network) else { - Err(IntakeCosignError::NonParticipatingNetwork)? - }; - *key - }); - + let key = + *global_session.keys.get(&network).ok_or(IntakeCosignError::NonParticipatingNetwork)?; if !signed_cosign.verify_signature(key) { Err(IntakeCosignError::InvalidSignature)?; } diff --git a/coordinator/p2p/libp2p/src/validators.rs b/coordinator/p2p/libp2p/src/validators.rs index eafd259c..d55709b4 100644 --- a/coordinator/p2p/libp2p/src/validators.rs +++ b/coordinator/p2p/libp2p/src/validators.rs @@ -60,8 +60,7 @@ impl Validators { Besides, we can't connect to historical validators, only the current validators. */ - let temporal_serai = serai.borrow().as_of_latest_finalized_block().await?; - let temporal_serai = temporal_serai.validator_sets(); + let serai = serai.borrow().state().await?; let mut session_changes = vec![]; { @@ -70,9 +69,9 @@ impl Validators { let mut futures = FuturesUnordered::new(); for network in ExternalNetworkId::all() { let sessions = sessions.borrow(); - let temporal_serai = temporal_serai.borrow(); + let serai = serai.borrow(); futures.push(async move { - let session = match temporal_serai.current_session(network.into()).await { + let session = match serai.current_session(network.into()).await { Ok(Some(session)) => session, Ok(None) => return Ok(None), Err(e) => return Err(e), @@ -81,7 +80,7 @@ impl Validators { if sessions.get(&network) == Some(&session) { Ok(None) } else { - match temporal_serai.current_validators(network.into()).await { + match serai.current_validators(network.into()).await { Ok(Some(validators)) => Ok(Some(( network, session, diff --git a/coordinator/src/db.rs b/coordinator/src/db.rs index 6f53cf52..87cedcd0 100644 --- a/coordinator/src/db.rs +++ b/coordinator/src/db.rs @@ -50,7 +50,6 @@ fn tributary_db_folder(set: ExternalValidatorSet) -> String { ExternalNetworkId::Bitcoin => "Bitcoin", ExternalNetworkId::Ethereum => "Ethereum", ExternalNetworkId::Monero => "Monero", - _ => panic!("unrecognized `ExternalNetworkId`"), }; format!("{root_path}/tributary-{network}-{}", set.session.0) } diff --git a/coordinator/substrate/Cargo.toml b/coordinator/substrate/Cargo.toml index 15579f33..02743e8a 100644 --- a/coordinator/substrate/Cargo.toml +++ b/coordinator/substrate/Cargo.toml @@ -29,7 +29,6 @@ serai-client-serai = { path = "../../substrate/client/serai", default-features = log = { version = "0.4", default-features = false, features = ["std"] } futures = { version = "0.3", default-features = false, features = ["std"] } -tokio = { version = "1", default-features = false } serai-db = { path = "../../common/db", version = "0.1.1" } serai-task = { path = "../../common/task", version = "0.1" } diff --git a/coordinator/substrate/src/canonical.rs b/coordinator/substrate/src/canonical.rs index 28e6d7fc..10c1ecdb 100644 --- a/coordinator/substrate/src/canonical.rs +++ b/coordinator/substrate/src/canonical.rs @@ -73,21 +73,13 @@ impl ContinuallyRan for CanonicalEventStream { } Err(serai_cosign::Faulted) => return Err("cosigning process faulted".to_string()), }; - let temporal_serai = serai.as_of(block_hash).await.map_err(|e| format!("{e}"))?; - let temporal_serai_validators = temporal_serai.validator_sets(); - let temporal_serai_instructions = temporal_serai.in_instructions(); - let temporal_serai_coins = temporal_serai.coins(); - - let (block, set_keys_events, slash_report_events, batch_events, burn_events) = - tokio::try_join!( - serai.block(block_hash), - temporal_serai_validators.set_keys_events(), - temporal_serai_validators.slash_report_events(), - temporal_serai_instructions.batch_events(), - temporal_serai_coins.burn_with_instruction_events(), - ) - .map_err(|e| format!("{e:?}"))?; - let Some(block) = block else { + let events = serai.events(block_hash).await.map_err(|e| format!("{e}"))?; + let set_keys_events = events.validator_sets().set_keys_events().cloned().collect(); + let slash_report_events = + events.validator_sets().slash_report_events().cloned().collect(); + let batch_events = events.in_instructions().batch_events().cloned().collect(); + let burn_events = events.coins().burn_with_instruction_events().cloned().collect(); + let Some(block) = serai.block(block_hash).await.map_err(|e| format!("{e:?}"))? else { Err(format!("Serai node didn't have cosigned block #{block_number}"))? }; diff --git a/coordinator/substrate/src/ephemeral.rs b/coordinator/substrate/src/ephemeral.rs index 30a13021..643742ec 100644 --- a/coordinator/substrate/src/ephemeral.rs +++ b/coordinator/substrate/src/ephemeral.rs @@ -6,7 +6,7 @@ use futures::stream::{StreamExt, FuturesOrdered}; use serai_client_serai::{ abi::primitives::{ BlockHash, - crypto::EmbeddedEllipticCurveKeys, + crypto::EmbeddedEllipticCurveKeys as EmbeddedEllipticCurveKeysStruct, network_id::ExternalNetworkId, validator_sets::{KeyShares, ExternalValidatorSet}, address::SeraiAddress, @@ -24,6 +24,10 @@ use crate::NewSetInformation; create_db!( CoordinatorSubstrateEphemeral { NextBlock: () -> u64, + EmbeddedEllipticCurveKeys: ( + network: ExternalNetworkId, + validator: SeraiAddress + ) -> EmbeddedEllipticCurveKeysStruct, } ); @@ -56,6 +60,7 @@ impl ContinuallyRan for EphemeralEventStream { struct EphemeralEvents { block_hash: BlockHash, time: u64, + embedded_elliptic_curve_keys_events: Vec, set_decided_events: Vec, accepted_handover_events: Vec, } @@ -76,15 +81,17 @@ impl ContinuallyRan for EphemeralEventStream { Err(serai_cosign::Faulted) => return Err("cosigning process faulted".to_string()), }; - let temporal_serai = serai.as_of(block_hash).await.map_err(|e| format!("{e}"))?; - let temporal_serai_validators = temporal_serai.validator_sets(); - let (block, set_decided_events, accepted_handover_events) = tokio::try_join!( - serai.block(block_hash), - temporal_serai_validators.set_decided_events(), - temporal_serai_validators.accepted_handover_events(), - ) - .map_err(|e| format!("{e:?}"))?; - let Some(block) = block else { + let events = serai.events(block_hash).await.map_err(|e| format!("{e}"))?; + let embedded_elliptic_curve_keys_events = events + .validator_sets() + .set_embedded_elliptic_curve_keys_events() + .cloned() + .collect::>(); + let set_decided_events = + events.validator_sets().set_decided_events().cloned().collect::>(); + let accepted_handover_events = + events.validator_sets().accepted_handover_events().cloned().collect::>(); + let Some(block) = serai.block(block_hash).await.map_err(|e| format!("{e:?}"))? else { Err(format!("Serai node didn't have cosigned block #{block_number}"))? }; @@ -92,7 +99,13 @@ impl ContinuallyRan for EphemeralEventStream { let time = block.header.unix_time_in_millis() / 1000; Ok(( block_number, - EphemeralEvents { block_hash, time, set_decided_events, accepted_handover_events }, + EphemeralEvents { + block_hash, + time, + embedded_elliptic_curve_keys_events, + set_decided_events, + accepted_handover_events, + }, )) } } @@ -123,30 +136,39 @@ impl ContinuallyRan for EphemeralEventStream { let mut txn = self.db.txn(); + for event in block.embedded_elliptic_curve_keys_events { + let serai_client_serai::abi::validator_sets::Event::SetEmbeddedEllipticCurveKeys { + validator, + keys, + } = &event + else { + panic!( + "{}: {event:?}", + "`SetEmbeddedEllipticCurveKeys` event wasn't a `SetEmbeddedEllipticCurveKeys` event" + ); + }; + + EmbeddedEllipticCurveKeys::set(&mut txn, keys.network(), *validator, keys); + } + for set_decided in block.set_decided_events { let serai_client_serai::abi::validator_sets::Event::SetDecided { set, validators } = &set_decided else { panic!("`SetDecided` event wasn't a `SetDecided` event: {set_decided:?}"); }; + // We only coordinate over external networks let Ok(set) = ExternalValidatorSet::try_from(*set) else { continue }; - - let serai = self.serai.as_of(block.block_hash).await.map_err(|e| format!("{e}"))?; - let serai = serai.validator_sets(); let validators = - validators.iter().map(|(validator, weight)| (*validator, weight)).collect::>(); + validators.iter().map(|(validator, weight)| (*validator, weight.0)).collect::>(); + let in_set = validators.iter().any(|(validator, _)| *validator == self.validator); if in_set { if u16::try_from(validators.len()).is_err() { Err("more than u16::MAX validators sent")?; } - let validators = validators - .into_iter() - .map(|(validator, weight)| (validator, weight.0)) - .collect::>(); - // Do the summation in u32 so we don't risk a u16 overflow let total_weight = validators.iter().map(|(_, weight)| u32::from(*weight)).sum::(); if total_weight > u32::from(KeyShares::MAX_PER_SET) { @@ -159,50 +181,26 @@ impl ContinuallyRan for EphemeralEventStream { .expect("value smaller than `u16` constant but doesn't fit in `u16`"); // Fetch all of the validators' embedded elliptic curve keys - let mut embedded_elliptic_curve_keys = FuturesOrdered::new(); - for (validator, _) in &validators { - let validator = *validator; - // try_join doesn't return a future so we need to wrap it in this additional async - // block - embedded_elliptic_curve_keys.push_back({ - let serai = serai.clone(); - async move { - match serai.embedded_elliptic_curve_keys(validator, set.network).await { - Ok(Some(keys)) => Ok(Some(( - validator, - match keys { - EmbeddedEllipticCurveKeys::Bitcoin(substrate, external) => { - assert_eq!(set.network, ExternalNetworkId::Bitcoin); - (substrate, external.as_slice().to_vec()) - } - EmbeddedEllipticCurveKeys::Ethereum(substrate, external) => { - assert_eq!(set.network, ExternalNetworkId::Ethereum); - (substrate, external.as_slice().to_vec()) - } - EmbeddedEllipticCurveKeys::Monero(substrate) => { - assert_eq!(set.network, ExternalNetworkId::Monero); - (substrate, substrate.as_slice().to_vec()) - } - }, - ))), - Ok(None) => Ok(None), - Err(e) => Err(e), - } - } - }); - } - let mut evrf_public_keys = Vec::with_capacity(usize::from(total_weight)); for (validator, weight) in &validators { - let Some((future_validator, (substrate_embedded_key, external_embedded_key))) = - embedded_elliptic_curve_keys.next().await.unwrap().map_err(|e| format!("{e:?}"))? - else { - Err("`SetDecided` with validator missing an embedded key".to_string())? + let keys = match EmbeddedEllipticCurveKeys::get(&txn, set.network, *validator) + .expect("selected validator lacked embedded elliptic curve keys") + { + EmbeddedEllipticCurveKeysStruct::Bitcoin(substrate, external) => { + assert_eq!(set.network, ExternalNetworkId::Bitcoin); + (substrate, external.to_vec()) + } + EmbeddedEllipticCurveKeysStruct::Ethereum(substrate, external) => { + assert_eq!(set.network, ExternalNetworkId::Ethereum); + (substrate, external.to_vec()) + } + EmbeddedEllipticCurveKeysStruct::Monero(substrate) => { + assert_eq!(set.network, ExternalNetworkId::Monero); + (substrate, substrate.to_vec()) + } }; - assert_eq!(*validator, future_validator); - for _ in 0 .. *weight { - evrf_public_keys.push((substrate_embedded_key, external_embedded_key.clone())); + evrf_public_keys.push(keys.clone()); } } @@ -213,6 +211,7 @@ impl ContinuallyRan for EphemeralEventStream { // TODO: This should be inlined into the Processor's key gen code // It's legacy from when we removed participants from the key gen threshold: ((total_weight * 2) / 3) + 1, + // TODO: Why are `validators` and `evrf_public_keys` two separate fields? validators, evrf_public_keys, participant_indexes: Default::default(), diff --git a/coordinator/substrate/src/publish_slash_report.rs b/coordinator/substrate/src/publish_slash_report.rs index f038fafd..7e7ef531 100644 --- a/coordinator/substrate/src/publish_slash_report.rs +++ b/coordinator/substrate/src/publish_slash_report.rs @@ -36,8 +36,7 @@ impl PublishSlashReportTask { // This uses the latest finalized block, not the latest cosigned block, which should be // fine as in the worst case, the only impact is no longer attempting TX publication - let serai = self.serai.as_of_latest_finalized_block().await.map_err(|e| format!("{e:?}"))?; - let serai = serai.validator_sets(); + let serai = self.serai.state().await.map_err(|e| format!("{e:?}"))?; let session_after_slash_report = Session(session.0 + 1); let current_session = serai.current_session(network.into()).await.map_err(|e| format!("{e:?}"))?; diff --git a/coordinator/substrate/src/set_keys.rs b/coordinator/substrate/src/set_keys.rs index 37afbb81..15c9e1ca 100644 --- a/coordinator/substrate/src/set_keys.rs +++ b/coordinator/substrate/src/set_keys.rs @@ -40,9 +40,7 @@ impl ContinuallyRan for SetKeysTask { // This uses the latest finalized block, not the latest cosigned block, which should be // fine as in the worst case, the only impact is no longer attempting TX publication - let serai = - self.serai.as_of_latest_finalized_block().await.map_err(|e| format!("{e:?}"))?; - let serai = serai.validator_sets(); + let serai = self.serai.state().await.map_err(|e| format!("{e:?}"))?; let current_session = serai.current_session(network.into()).await.map_err(|e| format!("{e:?}"))?; let current_session = current_session.map(|session| session.0); diff --git a/message-queue/src/main.rs b/message-queue/src/main.rs index 5d2de1f8..776c747f 100644 --- a/message-queue/src/main.rs +++ b/message-queue/src/main.rs @@ -219,7 +219,6 @@ async fn main() { ExternalNetworkId::Bitcoin => "BITCOIN_KEY", ExternalNetworkId::Ethereum => "ETHEREUM_KEY", ExternalNetworkId::Monero => "MONERO_KEY", - _ => panic!("unrecognized network"), }) else { continue; }; diff --git a/substrate/abi/src/validator_sets.rs b/substrate/abi/src/validator_sets.rs index 40b30e16..9f7e54d4 100644 --- a/substrate/abi/src/validator_sets.rs +++ b/substrate/abi/src/validator_sets.rs @@ -3,7 +3,7 @@ use alloc::vec::Vec; use borsh::{BorshSerialize, BorshDeserialize}; use serai_primitives::{ - crypto::{SignedEmbeddedEllipticCurveKeys, KeyPair, Signature}, + crypto::{EmbeddedEllipticCurveKeys, SignedEmbeddedEllipticCurveKeys, KeyPair, Signature}, address::SeraiAddress, balance::Amount, network_id::*, @@ -125,8 +125,8 @@ pub enum Event { SetEmbeddedEllipticCurveKeys { /// The validator which set their keys. validator: SeraiAddress, - /// The network which they set embedded elliptic curve keys for. - network: ExternalNetworkId, + /// The embedded elliptic curve keys they set. + keys: EmbeddedEllipticCurveKeys, }, /// A validator's allocation to a network has increased. Allocation { diff --git a/substrate/client/serai/Cargo.toml b/substrate/client/serai/Cargo.toml index 52fa4efa..b368a84a 100644 --- a/substrate/client/serai/Cargo.toml +++ b/substrate/client/serai/Cargo.toml @@ -28,8 +28,6 @@ borsh = { version = "1", default-features = false, features = ["std"] } bitvec = { version = "1", default-features = false, features = ["alloc", "std"] } serai-abi = { path = "../../abi", version = "0.1", default-features = false, features = ["std"] } -async-lock = "3" - [dev-dependencies] blake2 = { version = "0.11.0-rc.3", default-features = false } sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84" } diff --git a/substrate/client/serai/src/coins.rs b/substrate/client/serai/src/coins.rs index dcd72fd5..ae68dcbe 100644 --- a/substrate/client/serai/src/coins.rs +++ b/substrate/client/serai/src/coins.rs @@ -1,76 +1,37 @@ pub use serai_abi::coins::Event; -use crate::{RpcError, TemporalSerai}; +use crate::{RpcError, Events}; -/// A `TemporalSerai` scoped to the coins module. +/// An `Events` scoped to the coins module. #[derive(Clone)] -pub struct Coins<'a>(pub(super) &'a TemporalSerai<'a>); +pub struct Coins(pub(super) Events); -impl<'a> Coins<'a> { +impl Coins { /// The events from the coins module. - pub async fn events(&self) -> Result, RpcError> { - Ok( - self - .0 - .events_borrowed() - .await? - .as_ref() - .expect("`TemporalSerai::events` returned None") - .iter() - .flat_map(IntoIterator::into_iter) - .filter_map(|event| match event { - serai_abi::Event::Coins(event) => Some(event.clone()), - _ => None, - }) - .collect(), - ) + pub fn events(&self) -> impl Iterator { + self.0.events().flatten().filter_map(|event| match event { + serai_abi::Event::Coins(event) => Some(event), + _ => None, + }) } /// The `Mint` events from the coins module. - pub async fn mint_events(&self) -> Result, RpcError> { - Ok( - self - .events() - .await? - .into_iter() - .filter(|event| matches!(event, Event::Mint { .. })) - .collect(), - ) + pub fn mint_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::Mint { .. })) } /// The `Transfer` events from the coins module. - pub async fn transfer_events(&self) -> Result, RpcError> { - Ok( - self - .events() - .await? - .into_iter() - .filter(|event| matches!(event, Event::Transfer { .. })) - .collect(), - ) + pub fn transfer_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::Transfer { .. })) } /// The `Burn` events from the coins module. - pub async fn burn_events(&self) -> Result, RpcError> { - Ok( - self - .events() - .await? - .into_iter() - .filter(|event| matches!(event, Event::Burn { .. })) - .collect(), - ) + pub fn burn_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::Burn { .. })) } /// The `BurnWithInstruction` events from the coins module. - pub async fn burn_with_instruction_events(&self) -> Result, RpcError> { - Ok( - self - .events() - .await? - .into_iter() - .filter(|event| matches!(event, Event::BurnWithInstruction { .. })) - .collect(), - ) + pub fn burn_with_instruction_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::BurnWithInstruction { .. })) } } diff --git a/substrate/client/serai/src/in_instructions.rs b/substrate/client/serai/src/in_instructions.rs index ae603290..d0780c53 100644 --- a/substrate/client/serai/src/in_instructions.rs +++ b/substrate/client/serai/src/in_instructions.rs @@ -4,42 +4,24 @@ pub use serai_abi::{ UnsignedCall, Transaction, }; -use crate::{RpcError, TemporalSerai}; +use crate::{RpcError, Events}; -/// A `TemporalSerai` scoped to the in instructions module. +/// An `Events` scoped to the in instructions module. #[derive(Clone)] -pub struct InInstructions<'serai>(pub(super) &'serai TemporalSerai<'serai>); +pub struct InInstructions(pub(super) Events); -impl<'serai> InInstructions<'serai> { +impl InInstructions { /// The events from the in instructions module. - pub async fn events(&self) -> Result, RpcError> { - Ok( - self - .0 - .events_borrowed() - .await? - .as_ref() - .expect("`TemporalSerai::events` returned None") - .iter() - .flat_map(IntoIterator::into_iter) - .filter_map(|event| match event { - serai_abi::Event::InInstructions(event) => Some(event.clone()), - _ => None, - }) - .collect(), - ) + fn events(&self) -> impl Iterator { + self.0.events().flatten().filter_map(|event| match event { + serai_abi::Event::InInstructions(event) => Some(event), + _ => None, + }) } /// The `Batch` events from the in instructions module. - pub async fn batch_events(&self) -> Result, RpcError> { - Ok( - self - .events() - .await? - .into_iter() - .filter(|event| matches!(event, Event::Batch { .. })) - .collect(), - ) + pub fn batch_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::Batch { .. })) } /// Create a transaction to execute a batch. diff --git a/substrate/client/serai/src/lib.rs b/substrate/client/serai/src/lib.rs index 1e8f0bdf..0b205a7e 100644 --- a/substrate/client/serai/src/lib.rs +++ b/substrate/client/serai/src/lib.rs @@ -17,8 +17,6 @@ use abi::{ Transaction, Block, Event, }; -use async_lock::RwLock; - mod coins; pub use coins::Coins; @@ -55,16 +53,18 @@ pub struct Serai { client: TokioClient, } -/// An RPC client to a Serai node, scoped to a specific block. -/// -/// Upon any request being made for the events emitted by this block, the entire list of events -/// from this block will be cached within this. This allows future calls for events to be done -/// cheaply. +/// The events from a specific block. #[derive(Clone)] -pub struct TemporalSerai<'serai> { +pub struct Events { + // These are cached within an `Arc` for cheap `Clone`s + events: Arc>>, +} + +/// An RPC client to a Serai node, scoped to a specific block. +#[derive(Clone)] +pub struct State<'serai> { serai: &'serai Serai, block: BlockHash, - events: Arc>>>>, } impl Serai { @@ -175,28 +175,41 @@ impl Serai { .await } - /// Scope this RPC client to the state as of a specific block. - /// - /// This will yield an error if the block chosen isn't finalized. This ensures, given an honest - /// node, that this scope will be available for the lifetime of this object. - pub async fn as_of(&self, block: BlockHash) -> Result, RpcError> { - if !self.finalized(block).await? { - Err(RpcError::NotFinalized)?; - } - Ok(TemporalSerai { serai: self, block, events: Arc::new(RwLock::new(None)) }) + /// Fetch the events of a specific block. + pub async fn events(&self, block: BlockHash) -> Result { + Ok(Events { + events: Arc::new( + self + .call::>>("blockchain/events", &format!(r#"{{ "block": "{block}" }}"#)) + .await? + .into_iter() + .map(|events_per_tx| { + events_per_tx + .into_iter() + .map(|event| { + Event::deserialize( + &mut hex::decode(&event) + .map_err(|_| { + RpcError::InvalidNode("node returned non-hex-encoded event".to_string()) + })? + .as_slice(), + ) + .map_err(|_| RpcError::InvalidNode("node returned invalid event".to_string())) + }) + .collect::, _>>() + }) + .collect::, _>>()?, + ), + }) } /// Scope this RPC client to the state as of the latest finalized block. - pub async fn as_of_latest_finalized_block(&self) -> Result, RpcError> { + pub async fn state(&self) -> Result, RpcError> { let block = self .block_by_number(self.latest_finalized_block_number().await?) .await? .ok_or_else(|| RpcError::InvalidNode("couldn't fetch latest finalized block".to_string()))?; - Ok(TemporalSerai { - serai: self, - block: block.header.hash(), - events: Arc::new(RwLock::new(None)), - }) + Ok(State { serai: self, block: block.header.hash() }) } /// Return the P2P addresses for the validators of the specified network. @@ -208,14 +221,38 @@ impl Serai { ExternalNetworkId::Bitcoin => r#"["bitcoin"]"#, ExternalNetworkId::Ethereum => r#"["ethereum"]"#, ExternalNetworkId::Monero => r#"["monero"]"#, - _ => Err(RpcError::InternalError("unrecognized external network ID".to_string()))?, }, ) .await } } -impl<'serai> TemporalSerai<'serai> { +impl Events { + /// The events within this container. + /// + /// This will yield the events for each transaction within the block, including the implicit + /// transactions at the start and end of each block, within an outer container. + pub fn events(&self) -> impl Iterator> { + self.events.iter() + } + + /// Scope to the coins module. + pub fn coins(&self) -> Coins { + Coins(self.clone()) + } + + /// Scope to the validator sets module. + pub fn validator_sets(&self) -> ValidatorSets { + ValidatorSets(self.clone()) + } + + /// Scope to the in instructions module. + pub fn in_instructions(&self) -> InInstructions { + InInstructions(self.clone()) + } +} + +impl<'serai> State<'serai> { async fn call( &self, method: &str, @@ -223,68 +260,4 @@ impl<'serai> TemporalSerai<'serai> { ) -> Result { self.serai.call(method, &format!(r#"{{ "block": "{}" {params} }}"#, self.block)).await } - - /// Fetch the events for this block. - /// - /// The returned `Option` will always be `Some(_)`. - async fn events_borrowed( - &self, - ) -> Result>>>, RpcError> { - let mut events = self.events.read().await; - if events.is_none() { - drop(events); - { - let mut events_mut = self.events.write().await; - if events_mut.is_none() { - *events_mut = Some( - self - .call::>>("blockchain/events", "") - .await? - .into_iter() - .map(|events_per_tx| { - events_per_tx - .into_iter() - .map(|event| { - Event::deserialize( - &mut hex::decode(&event) - .map_err(|_| { - RpcError::InvalidNode("node returned non-hex-encoded event".to_string()) - })? - .as_slice(), - ) - .map_err(|_| RpcError::InvalidNode("node returned invalid event".to_string())) - }) - .collect::, _>>() - }) - .collect::, _>>()?, - ); - } - } - events = self.events.read().await; - } - Ok(events) - } - - /// Fetch the events for this block. - /// - /// These will be grouped by the transactions which emitted them, including the inherent - /// transactions at the start and end of every block. - pub async fn events(&self) -> Result>, RpcError> { - Ok(self.events_borrowed().await?.clone().expect("`TemporalSerai::events` returned None")) - } - - /// Scope to the coins module. - pub fn coins(&self) -> Coins<'_> { - Coins(self) - } - - /// Scope to the validator sets module. - pub fn validator_sets(&self) -> ValidatorSets<'_> { - ValidatorSets(self) - } - - /// Scope to the in instructions module. - pub fn in_instructions(&self) -> InInstructions<'_> { - InInstructions(self) - } } diff --git a/substrate/client/serai/src/validator_sets.rs b/substrate/client/serai/src/validator_sets.rs index e4162b92..5f863cb0 100644 --- a/substrate/client/serai/src/validator_sets.rs +++ b/substrate/client/serai/src/validator_sets.rs @@ -14,203 +14,68 @@ pub use serai_abi::{ UnsignedCall, Transaction, }; -use crate::{RpcError, TemporalSerai}; +use crate::{RpcError, Events, State}; -fn rpc_network(network: impl Into) -> Result<&'static str, RpcError> { - Ok(match network.into() { +fn rpc_network(network: impl Into) -> &'static str { + match network.into() { NetworkId::Serai => r#""serai""#, NetworkId::External(ExternalNetworkId::Bitcoin) => r#""bitcoin""#, NetworkId::External(ExternalNetworkId::Ethereum) => r#""ethereum""#, NetworkId::External(ExternalNetworkId::Monero) => r#""monero""#, - _ => Err(RpcError::InternalError("unrecognized network ID".to_string()))?, - }) + } } -/// A `TemporalSerai` scoped to the validator sets module. +/// An `Events` scoped to the validator sets module. #[derive(Clone)] -pub struct ValidatorSets<'serai>(pub(super) &'serai TemporalSerai<'serai>); +pub struct ValidatorSets(pub(super) Events); -impl<'serai> ValidatorSets<'serai> { +impl ValidatorSets { /// The events from the validator sets module. - pub async fn events(&self) -> Result, RpcError> { - Ok( - self - .0 - .events_borrowed() - .await? - .as_ref() - .expect("`TemporalSerai::events` returned None") - .iter() - .flat_map(IntoIterator::into_iter) - .filter_map(|event| match event { - serai_abi::Event::ValidatorSets(event) => Some(event.clone()), - _ => None, - }) - .collect(), - ) + pub fn events(&self) -> impl Iterator { + self.0.events().flatten().filter_map(|event| match event { + serai_abi::Event::ValidatorSets(event) => Some(event), + _ => None, + }) } /// The `SetDecided` events from the validator sets module. - pub async fn set_decided_events(&self) -> Result, RpcError> { - Ok( - self - .events() - .await? - .into_iter() - .filter(|event| matches!(event, Event::SetDecided { .. })) - .collect(), - ) + pub fn set_decided_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::SetDecided { .. })) } /// The `SetKeys` events from the validator sets module. - pub async fn set_keys_events(&self) -> Result, RpcError> { - Ok( - self - .events() - .await? - .into_iter() - .filter(|event| matches!(event, Event::SetKeys { .. })) - .collect(), - ) + pub fn set_keys_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::SetKeys { .. })) } /// The `AcceptedHandover` events from the validator sets module. - pub async fn accepted_handover_events(&self) -> Result, RpcError> { - Ok( - self - .events() - .await? - .into_iter() - .filter(|event| matches!(event, Event::AcceptedHandover { .. })) - .collect(), - ) + pub fn accepted_handover_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::AcceptedHandover { .. })) } /// The `SlashReport` events from the validator sets module. - pub async fn slash_report_events(&self) -> Result, RpcError> { - Ok( - self - .events() - .await? - .into_iter() - .filter(|event| matches!(event, Event::SlashReport { .. })) - .collect(), - ) + pub fn slash_report_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::SlashReport { .. })) } - /// The current session for the specified network. - pub async fn current_session(&self, network: NetworkId) -> Result, RpcError> { - Ok( - self - .0 - .call::>( - "validator-sets/current_session", - &format!(r#", "network": {} "#, rpc_network(network)?), - ) - .await? - .map(Session), - ) + /// The `SetEmbeddedEllipticCurveKeys` events from the validator sets module. + pub fn set_embedded_elliptic_curve_keys_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::SetEmbeddedEllipticCurveKeys { .. })) } - /// The stake for the current validators for the specified network. - pub async fn current_stake(&self, network: NetworkId) -> Result, RpcError> { - Ok( - self - .0 - .call::>( - "validator-sets/current_stake", - &format!(r#", "network": {} "#, rpc_network(network)?), - ) - .await? - .map(Amount), - ) + /// The `Allocation` events from the validator sets module. + pub fn allocation_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::Allocation { .. })) } - /// The keys for the specified validator set. - pub async fn keys(&self, set: ExternalValidatorSet) -> Result, RpcError> { - let Some(key_pair) = self - .0 - .call::>( - "validator-sets/keys", - &format!(r#", "network": {}, "session": {} "#, rpc_network(set.network)?, set.session.0), - ) - .await? - else { - return Ok(None); - }; - KeyPair::deserialize( - &mut hex::decode(key_pair) - .map_err(|_| RpcError::InvalidNode("validator set's keys weren't valid hex".to_string()))? - .as_slice(), - ) - .map(Some) - .map_err(|_| RpcError::InvalidNode("validator set's keys weren't a valid key pair".to_string())) + /// The `Deallocation` events from the validator sets module. + pub fn deallocation_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::Deallocation { .. })) } - /// The current validators for the specified network. - pub async fn current_validators( - &self, - network: NetworkId, - ) -> Result>, RpcError> { - self - .0 - .call::>>( - "validator-sets/current_validators", - &format!(r#", "network": {} "#, rpc_network(network)?), - ) - .await? - .map(|validators| { - validators - .into_iter() - .map(|addr| { - SeraiAddress::from_str(&addr) - .map_err(|_| RpcError::InvalidNode("validator's address was invalid".to_string())) - }) - .collect() - }) - .transpose() - } - - /// If the prior validators for this network is still expected to publish a slash report. - pub async fn pending_slash_report(&self, network: ExternalNetworkId) -> Result { - self - .0 - .call( - "validator-sets/pending_slash_report", - &format!(r#", "network": {} "#, rpc_network(network)?), - ) - .await - } - - /// The key on an embedded elliptic curve for the specified validator. - pub async fn embedded_elliptic_curve_keys( - &self, - validator: SeraiAddress, - network: ExternalNetworkId, - ) -> Result, RpcError> { - let Some(keys) = self - .0 - .call::>( - "validator-sets/embedded_elliptic_curve_keys", - &format!(r#", "validator": {validator}, "network": {} "#, rpc_network(network)?), - ) - .await? - else { - return Ok(None); - }; - EmbeddedEllipticCurveKeys::deserialize( - &mut hex::decode(keys) - .map_err(|_| { - RpcError::InvalidNode( - "validator's embedded elliptic curve keys weren't valid hex".to_string(), - ) - })? - .as_slice(), - ) - .map(Some) - .map_err(|_| { - RpcError::InvalidNode("validator's embedded elliptic curve keys weren't valid".to_string()) - }) + /// The `DelayedDeallocationClaimed` events from the validator sets module. + pub fn delayed_deallocation_claimed_events(&self) -> impl Iterator { + self.events().filter(|event| matches!(event, Event::DelayedDeallocationClaimed { .. })) } /// Create a transaction to set a validator set's keys. @@ -247,3 +112,114 @@ impl<'serai> ValidatorSets<'serai> { } } } + +impl<'serai> State<'serai> { + /// The current session for the specified network. + pub async fn current_session(&self, network: NetworkId) -> Result, RpcError> { + Ok( + self + .call::>( + "validator-sets/current_session", + &format!(r#", "network": {} "#, rpc_network(network)), + ) + .await? + .map(Session), + ) + } + + /// The stake for the current validators for the specified network. + pub async fn current_stake(&self, network: NetworkId) -> Result, RpcError> { + Ok( + self + .call::>( + "validator-sets/current_stake", + &format!(r#", "network": {} "#, rpc_network(network)), + ) + .await? + .map(Amount), + ) + } + + /// The keys for the specified validator set. + pub async fn keys(&self, set: ExternalValidatorSet) -> Result, RpcError> { + let Some(key_pair) = self + .call::>( + "validator-sets/keys", + &format!(r#", "network": {}, "session": {} "#, rpc_network(set.network), set.session.0), + ) + .await? + else { + return Ok(None); + }; + KeyPair::deserialize( + &mut hex::decode(key_pair) + .map_err(|_| RpcError::InvalidNode("validator set's keys weren't valid hex".to_string()))? + .as_slice(), + ) + .map(Some) + .map_err(|_| RpcError::InvalidNode("validator set's keys weren't a valid key pair".to_string())) + } + + /// The current validators for the specified network. + pub async fn current_validators( + &self, + network: NetworkId, + ) -> Result>, RpcError> { + self + .call::>>( + "validator-sets/current_validators", + &format!(r#", "network": {} "#, rpc_network(network)), + ) + .await? + .map(|validators| { + validators + .into_iter() + .map(|addr| { + SeraiAddress::from_str(&addr) + .map_err(|_| RpcError::InvalidNode("validator's address was invalid".to_string())) + }) + .collect() + }) + .transpose() + } + + /// If the prior validators for this network is still expected to publish a slash report. + pub async fn pending_slash_report(&self, network: ExternalNetworkId) -> Result { + self + .call( + "validator-sets/pending_slash_report", + &format!(r#", "network": {} "#, rpc_network(network)), + ) + .await + } + + /// The key on an embedded elliptic curve for the specified validator. + pub async fn embedded_elliptic_curve_keys( + &self, + validator: SeraiAddress, + network: ExternalNetworkId, + ) -> Result, RpcError> { + let Some(keys) = self + .call::>( + "validator-sets/embedded_elliptic_curve_keys", + &format!(r#", "validator": "{validator}", "network": {} "#, rpc_network(network)), + ) + .await? + else { + return Ok(None); + }; + EmbeddedEllipticCurveKeys::deserialize( + &mut hex::decode(keys) + .map_err(|_| { + RpcError::InvalidNode( + "validator's embedded elliptic curve keys weren't valid hex".to_string(), + ) + })? + .as_slice(), + ) + .map(Some) + .map_err(|_| { + RpcError::InvalidNode("validator's embedded elliptic curve keys weren't valid".to_string()) + }) + } +} diff --git a/substrate/client/serai/tests/blockchain.rs b/substrate/client/serai/tests/blockchain.rs index 8c9b614d..8cb80b70 100644 --- a/substrate/client/serai/tests/blockchain.rs +++ b/substrate/client/serai/tests/blockchain.rs @@ -133,7 +133,11 @@ async fn blockchain() { .chain(block.transactions.iter().map(serai_abi::Transaction::hash)) .chain(core::iter::once(end_transaction)); - let events = serai.as_of(block.header.hash()).await.unwrap().events().await.unwrap(); + let events = serai.events(block.header.hash()).await.unwrap(); + let events = events + .events() + .map(|iter| iter.into_iter().cloned().collect::>()) + .collect::>(); assert_eq!(events.len(), 2 + block.transactions.len()); let mut transaction_leaves = vec![]; diff --git a/substrate/client/serai/tests/validator_sets.rs b/substrate/client/serai/tests/validator_sets.rs index 522f3e45..454c0f6e 100644 --- a/substrate/client/serai/tests/validator_sets.rs +++ b/substrate/client/serai/tests/validator_sets.rs @@ -3,6 +3,7 @@ use serai_abi::{ address::SeraiAddress, network_id::{ExternalNetworkId, NetworkId}, balance::Amount, + crypto::EmbeddedEllipticCurveKeys, validator_sets::{Session, ExternalValidatorSet, ValidatorSet, KeyShares}, }, validator_sets::Event, @@ -44,67 +45,94 @@ async fn validator_sets() { panic!("finalized block remained the genesis block for over five minutes"); }; + // The genesis block should have the expected events { use sp_core::{Pair as _, sr25519::Pair}; let genesis_validators = vec![( SeraiAddress::from(Pair::from_string("//Alice", None).unwrap().public()), KeyShares(1), )]; - // The genesis block should have the expected events + { - let mut events = serai - .as_of(serai.block_by_number(0).await.unwrap().unwrap().header.hash()) + let events = serai + .events(serai.block_by_number(0).await.unwrap().unwrap().header.hash()) .await .unwrap() .validator_sets() - .set_decided_events() - .await - .unwrap(); - events.sort_by_key(|event| borsh::to_vec(event).unwrap()); - let mut expected = vec![ - Event::SetDecided { - set: ValidatorSet { network: NetworkId::Serai, session: Session(0) }, - validators: genesis_validators.clone(), - }, - Event::SetDecided { - set: ValidatorSet { network: NetworkId::Serai, session: Session(1) }, - validators: genesis_validators.clone(), - }, - Event::SetDecided { - set: ValidatorSet { - network: NetworkId::External(ExternalNetworkId::Bitcoin), - session: Session(0), + .set_embedded_elliptic_curve_keys_events() + .cloned() + .collect::>(); + assert_eq!(events.len(), ExternalNetworkId::all().collect::>().len()); + + let state = serai.state().await.unwrap(); + for (event, network) in events.into_iter().zip(ExternalNetworkId::all()) { + assert_eq!( + event, + Event::SetEmbeddedEllipticCurveKeys { + validator: genesis_validators[0].0, + keys: state + .embedded_elliptic_curve_keys(genesis_validators[0].0, network) + .await + .unwrap() + .unwrap(), + } + ); + } + } + + { + assert_eq!( + serai + .events(serai.block_by_number(0).await.unwrap().unwrap().header.hash()) + .await + .unwrap() + .validator_sets() + .set_decided_events() + .cloned() + .collect::>(), + vec![ + Event::SetDecided { + set: ValidatorSet { network: NetworkId::Serai, session: Session(0) }, + validators: genesis_validators.clone(), }, - validators: genesis_validators.clone(), - }, - Event::SetDecided { - set: ValidatorSet { - network: NetworkId::External(ExternalNetworkId::Ethereum), - session: Session(0), + Event::SetDecided { + set: ValidatorSet { + network: NetworkId::External(ExternalNetworkId::Bitcoin), + session: Session(0) + }, + validators: genesis_validators.clone(), }, - validators: genesis_validators.clone(), - }, - Event::SetDecided { - set: ValidatorSet { - network: NetworkId::External(ExternalNetworkId::Monero), - session: Session(0), + Event::SetDecided { + set: ValidatorSet { + network: NetworkId::External(ExternalNetworkId::Ethereum), + session: Session(0) + }, + validators: genesis_validators.clone(), }, - validators: genesis_validators.clone(), - }, - ]; - expected.sort_by_key(|event| borsh::to_vec(event).unwrap()); - assert_eq!(events, expected); + Event::SetDecided { + set: ValidatorSet { + network: NetworkId::External(ExternalNetworkId::Monero), + session: Session(0) + }, + validators: genesis_validators.clone(), + }, + Event::SetDecided { + set: ValidatorSet { network: NetworkId::Serai, session: Session(1) }, + validators: genesis_validators.clone(), + }, + ] + ); } assert_eq!( serai - .as_of(serai.block_by_number(0).await.unwrap().unwrap().header.hash()) + .events(serai.block_by_number(0).await.unwrap().unwrap().header.hash()) .await .unwrap() .validator_sets() .accepted_handover_events() - .await - .unwrap(), + .cloned() + .collect::>(), vec![Event::AcceptedHandover { set: ValidatorSet { network: NetworkId::Serai, session: Session(0) } }] @@ -115,34 +143,30 @@ async fn validator_sets() { { assert_eq!( serai - .as_of(serai.block_by_number(1).await.unwrap().unwrap().header.hash()) + .events(serai.block_by_number(1).await.unwrap().unwrap().header.hash()) .await .unwrap() .validator_sets() .set_decided_events() - .await - .unwrap(), + .cloned() + .collect::>(), vec![], ); assert_eq!( serai - .as_of(serai.block_by_number(1).await.unwrap().unwrap().header.hash()) + .events(serai.block_by_number(1).await.unwrap().unwrap().header.hash()) .await .unwrap() .validator_sets() .accepted_handover_events() - .await - .unwrap(), + .cloned() + .collect::>(), vec![], ); } { - let serai = serai - .as_of(serai.block_by_number(0).await.unwrap().unwrap().header.hash()) - .await - .unwrap(); - let serai = serai.validator_sets(); + let serai = serai.state().await.unwrap(); for network in NetworkId::all() { match network { NetworkId::Serai => { diff --git a/substrate/primitives/src/network_id.rs b/substrate/primitives/src/network_id.rs index 414dce63..e5362494 100644 --- a/substrate/primitives/src/network_id.rs +++ b/substrate/primitives/src/network_id.rs @@ -30,7 +30,6 @@ pub enum EmbeddedEllipticCurve { feature = "non_canonical_scale_derivations", derive(scale::Encode, scale::Decode, scale::MaxEncodedLen, scale::DecodeWithMemTracking) )] -#[non_exhaustive] pub enum ExternalNetworkId { /// The Bitcoin network. Bitcoin = 1, diff --git a/substrate/validator-sets/src/embedded_elliptic_curve_keys.rs b/substrate/validator-sets/src/embedded_elliptic_curve_keys.rs index 22f68b0d..0ffaab86 100644 --- a/substrate/validator-sets/src/embedded_elliptic_curve_keys.rs +++ b/substrate/validator-sets/src/embedded_elliptic_curve_keys.rs @@ -27,7 +27,7 @@ pub(crate) trait EmbeddedEllipticCurveKeys { fn set_embedded_elliptic_curve_keys( validator: Public, keys: SignedEmbeddedEllipticCurveKeys, - ) -> Result<(), ()>; + ) -> Result; /// Get a validator's embedded elliptic curve keys, for an external network. fn embedded_elliptic_curve_keys( @@ -45,10 +45,10 @@ impl EmbeddedEllipticCurveKeys for S { fn set_embedded_elliptic_curve_keys( validator: Public, keys: SignedEmbeddedEllipticCurveKeys, - ) -> Result<(), ()> { + ) -> Result { let keys = keys.verify(validator.into()).ok_or(())?; - S::EmbeddedEllipticCurveKeys::set(keys.network(), validator, Some(keys)); - Ok(()) + S::EmbeddedEllipticCurveKeys::insert(keys.network(), validator, keys); + Ok(keys) } /// Get a validator's embedded elliptic curve keys, for an external network. diff --git a/substrate/validator-sets/src/lib.rs b/substrate/validator-sets/src/lib.rs index 54178f37..559d6bbe 100644 --- a/substrate/validator-sets/src/lib.rs +++ b/substrate/validator-sets/src/lib.rs @@ -193,11 +193,8 @@ mod pallet { for (participant, keys) in &self.participants { for (network, keys) in ExternalNetworkId::all().zip(keys.iter().cloned()) { assert_eq!(network, keys.network()); - as crate::EmbeddedEllipticCurveKeys>::set_embedded_elliptic_curve_keys( - *participant, - keys, - ) - .expect("genesis embedded elliptic curve keys weren't valid"); + Pallet::::set_embedded_elliptic_curve_keys_internal(*participant, keys) + .expect("genesis embedded elliptic curve keys weren't valid"); } } for network in NetworkId::all() { @@ -331,6 +328,23 @@ mod pallet { ) } + fn set_embedded_elliptic_curve_keys_internal( + validator: Public, + keys: SignedEmbeddedEllipticCurveKeys, + ) -> DispatchResult { + let network = keys.network(); + let keys = + as crate::EmbeddedEllipticCurveKeys>::set_embedded_elliptic_curve_keys( + validator, keys, + ) + .map_err(|()| Error::::InvalidEmbeddedEllipticCurveKeys)?; + Core::::emit_event(Event::SetEmbeddedEllipticCurveKeys { + validator: validator.into(), + keys, + }); + Ok(()) + } + /* TODO pub fn distribute_block_rewards( network: NetworkId, @@ -492,16 +506,7 @@ mod pallet { keys: SignedEmbeddedEllipticCurveKeys, ) -> DispatchResult { let validator = ensure_signed(origin)?; - let network = keys.network(); - as crate::EmbeddedEllipticCurveKeys>::set_embedded_elliptic_curve_keys( - validator, keys, - ) - .map_err(|()| Error::::InvalidEmbeddedEllipticCurveKeys)?; - Core::::emit_event(Event::SetEmbeddedEllipticCurveKeys { - validator: validator.into(), - network, - }); - Ok(()) + Self::set_embedded_elliptic_curve_keys_internal(validator, keys) } #[pallet::call_index(3)]