From f9e3d1b142f59c06545031ff0d5b0e261116df32 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Fri, 14 Nov 2025 03:35:38 -0500 Subject: [PATCH] Expand validator sets API with the rest of the events and some getters We could've added a storage API, and fetched fields that way, except we want the storage to be opaque. That meant we needed to add the RPC routes to the node, which also simplifies other people writing RPC code and fetching these fields. Then the node could've used the storage API, except a lot of the storage in validator-sets is marked opaque and to only be read via functions, so extending the runtime made the most sense. --- substrate/client/serai/src/lib.rs | 21 ++- substrate/client/serai/src/validator_sets.rs | 100 +++++++++++- .../client/serai/tests/validator_sets.rs | 22 ++- substrate/node/src/rpc/blockchain.rs | 49 +----- substrate/node/src/rpc/mod.rs | 3 + substrate/node/src/rpc/utils.rs | 53 +++++++ substrate/node/src/rpc/validator_sets.rs | 143 ++++++++++++++++++ substrate/runtime/src/lib.rs | 21 ++- substrate/runtime/src/wasm/mod.rs | 17 ++- substrate/validator-sets/src/keys.rs | 7 + substrate/validator-sets/src/lib.rs | 8 + 11 files changed, 387 insertions(+), 57 deletions(-) create mode 100644 substrate/node/src/rpc/utils.rs create mode 100644 substrate/node/src/rpc/validator_sets.rs diff --git a/substrate/client/serai/src/lib.rs b/substrate/client/serai/src/lib.rs index 8b766ffe..6add4c60 100644 --- a/substrate/client/serai/src/lib.rs +++ b/substrate/client/serai/src/lib.rs @@ -125,12 +125,12 @@ impl Serai { /// Fetch the latest finalized block number. pub async fn latest_finalized_block_number(&self) -> Result { - self.call("serai_latestFinalizedBlockNumber", "[]").await + self.call("blockchain/latest_finalized_block_number", "[]").await } /// Fetch if a block is finalized. pub async fn finalized(&self, block: BlockHash) -> Result { - self.call("serai_isFinalized", &format!(r#"["{block}"]"#)).await + self.call("blockchain/is_finalized", &format!(r#"{{ "block": "{block}" }}"#)).await } async fn block_internal( @@ -147,12 +147,14 @@ impl Serai { /// Fetch a block from the Serai blockchain. pub async fn block(&self, block: BlockHash) -> Result { - Self::block_internal(self.call("serai_block", &format!(r#"["{block}"]"#))).await + Self::block_internal(self.call("blockchain/block", &format!(r#"{{ "block": "{block}" }}"#))) + .await } /// Fetch a block from the Serai blockchain by its number. pub async fn block_by_number(&self, block: u64) -> Result { - Self::block_internal(self.call("serai_block", &format!("[{block}]"))).await + Self::block_internal(self.call("blockchain/block", &format!(r#"{{ "block": "{block}" }}"#))) + .await } /// Scope this RPC client to the state as of a specific block. @@ -183,6 +185,14 @@ impl Serai { } impl<'a> TemporalSerai<'a> { + async fn call( + &self, + method: &str, + params: &str, + ) -> 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(_)`. @@ -195,8 +205,7 @@ impl<'a> TemporalSerai<'a> { if events_mut.is_none() { *events_mut = Some( self - .serai - .call::>("serai_events", &format!(r#"["{}"]"#, self.block)) + .call::>("blockchain/events", "") .await? .into_iter() .map(|event| { diff --git a/substrate/client/serai/src/validator_sets.rs b/substrate/client/serai/src/validator_sets.rs index 2bd0a77c..cf7fd5e2 100644 --- a/substrate/client/serai/src/validator_sets.rs +++ b/substrate/client/serai/src/validator_sets.rs @@ -1,6 +1,27 @@ -pub use serai_abi::validator_sets::Event; +use borsh::BorshDeserialize; + +pub use serai_abi::{ + primitives::{ + crypto::KeyPair, + network_id::{ExternalNetworkId, NetworkId}, + validator_sets::{Session, ExternalValidatorSet, ValidatorSet}, + balance::Amount, + }, + validator_sets::Event, +}; + use crate::{RpcError, TemporalSerai}; +fn rpc_network(network: impl Into) -> Result<&'static str, RpcError> { + Ok(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. #[derive(Clone)] pub struct ValidatorSets<'a>(pub(super) &'a TemporalSerai<'a>); @@ -36,6 +57,18 @@ impl<'a> ValidatorSets<'a> { ) } + /// 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(), + ) + } + /// The `AcceptedHandover` events from the validator sets module. pub async fn accepted_handover_events(&self) -> Result, RpcError> { Ok( @@ -47,4 +80,69 @@ impl<'a> ValidatorSets<'a> { .collect(), ) } + + /// 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(), + ) + } + + /// The current session for the specified network. + pub async fn current_session(&self, network: NetworkId) -> Result, RpcError> { + Ok( + self + .0 + .call::>( + "validator-sets/session", + &format!(r#" "network": {} "#, rpc_network(network)?), + ) + .await? + .map(Session), + ) + } + + /// The stake for the current validators for 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 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#" "set": {{ "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())) + } } diff --git a/substrate/client/serai/tests/validator_sets.rs b/substrate/client/serai/tests/validator_sets.rs index a7458681..32397438 100644 --- a/substrate/client/serai/tests/validator_sets.rs +++ b/substrate/client/serai/tests/validator_sets.rs @@ -1,7 +1,8 @@ use serai_abi::{ primitives::{ network_id::{ExternalNetworkId, NetworkId}, - validator_sets::{Session, ValidatorSet}, + balance::Amount, + validator_sets::{Session, ExternalValidatorSet, ValidatorSet}, }, validator_sets::Event, }; @@ -125,7 +126,24 @@ async fn validator_sets() { ); } - println!("Finished `serai-client/blockchain` test"); + { + let serai = + serai.as_of(serai.block_by_number(0).await.unwrap().header.hash()).await.unwrap(); + let serai = serai.validator_sets(); + for network in NetworkId::all() { + assert_eq!(serai.current_session(network).await.unwrap(), Some(Session(0))); + assert_eq!(serai.current_stake(network).await.unwrap(), Some(Amount(0))); + match network { + NetworkId::Serai => {} + NetworkId::External(network) => assert_eq!( + serai.keys(ExternalValidatorSet { network, session: Session(0) }).await.unwrap(), + None + ), + } + } + } + + println!("Finished `serai-client/validator_sets` test"); }) .await; } diff --git a/substrate/node/src/rpc/blockchain.rs b/substrate/node/src/rpc/blockchain.rs index a2d7a9af..2a76ae3b 100644 --- a/substrate/node/src/rpc/blockchain.rs +++ b/substrate/node/src/rpc/blockchain.rs @@ -15,46 +15,7 @@ use serai_runtime::SeraiApi; use jsonrpsee::RpcModule; -fn block_hash< - C: HeaderMetadata - + HeaderBackend - + BlockBackend - + ProvideRuntimeApi, ->( - client: &C, - params: &jsonrpsee::types::params::Params, -) -> Result<::Hash, jsonrpsee::types::error::ErrorObjectOwned> { - Ok(if let Ok(block_hash) = params.sequence().next::() { - let Some(block_hash) = hex::decode(&block_hash).ok().and_then(|bytes| { - <[u8; 32]>::try_from(bytes.as_slice()) - .map(::Hash::from) - .ok() - }) else { - return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( - -1, - "requested block hash wasn't a valid hash", - Option::<()>::None, - )); - }; - block_hash - } else { - let Ok(block_number) = params.sequence().next::() else { - return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( - -1, - "requested block wasn't a valid hash nor number", - Option::<()>::None, - )); - }; - let Ok(Some(block_hash)) = client.block_hash(block_number) else { - return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( - -2, - "couldn't find requested block's hash", - Option::<()>::None, - )); - }; - block_hash - }) -} +use super::utils::block_hash; pub(crate) fn module< C: 'static @@ -69,11 +30,11 @@ pub(crate) fn module< ) -> Result, Box> { let mut module = RpcModule::new(client); - module.register_method("serai_latestFinalizedBlockNumber", |_params, client, _ext| { + module.register_method("blockchain/latest_finalized_block_number", |_params, client, _ext| { client.info().finalized_number }); - module.register_method("serai_isFinalized", |params, client, _ext| { + module.register_method("blockchain/is_finalized", |params, client, _ext| { let block_hash = block_hash(&**client, ¶ms)?; let finalized = client.info().finalized_number; let Ok(Some(number)) = client.number(block_hash) else { @@ -96,7 +57,7 @@ pub(crate) fn module< ) })?; - module.register_method("serai_block", |params, client, _ext| { + module.register_method("blockchain/block", |params, client, _ext| { let block_hash = block_hash(&**client, ¶ms)?; let Ok(Some(block)) = client.block(block_hash) else { return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( @@ -109,7 +70,7 @@ pub(crate) fn module< Ok(hex::encode(borsh::to_vec(&serai_abi::Block::from(block.block)).unwrap())) })?; - module.register_method("serai_events", |params, client, _ext| { + module.register_method("blockchain/events", |params, client, _ext| { let block_hash = block_hash(&**client, ¶ms)?; let Ok(events) = client.runtime_api().events(block_hash) else { return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( diff --git a/substrate/node/src/rpc/mod.rs b/substrate/node/src/rpc/mod.rs index d9131edb..dd7dd5b7 100644 --- a/substrate/node/src/rpc/mod.rs +++ b/substrate/node/src/rpc/mod.rs @@ -16,7 +16,9 @@ use jsonrpsee::RpcModule; use sc_client_api::BlockBackend; use sc_transaction_pool_api::TransactionPool; +mod utils; mod blockchain; +mod validator_sets; mod p2p_validators; pub struct FullDeps { @@ -42,6 +44,7 @@ pub fn create_full< let mut root = RpcModule::new(()); root.merge(blockchain::module(client.clone())?)?; + root.merge(validator_sets::module(client.clone()))?; if let Some(authority_discovery) = authority_discovery { root.merge(p2p_validators::module(id, client, authority_discovery)?)?; } diff --git a/substrate/node/src/rpc/utils.rs b/substrate/node/src/rpc/utils.rs new file mode 100644 index 00000000..df14308b --- /dev/null +++ b/substrate/node/src/rpc/utils.rs @@ -0,0 +1,53 @@ +use sp_blockchain::{Error as BlockchainError, HeaderMetadata, HeaderBackend}; +use sc_client_api::BlockBackend; + +use serai_abi::{primitives::prelude::*, SubstrateBlock as Block}; + +pub(super) fn block_hash< + C: HeaderMetadata + HeaderBackend + BlockBackend, +>( + client: &C, + params: &jsonrpsee::types::params::Params, +) -> Result<::Hash, jsonrpsee::types::error::ErrorObjectOwned> { + #[derive(sp_core::serde::Deserialize)] + #[serde(crate = "sp_core::serde")] + struct BlockByHash { + block: String, + }; + #[derive(sp_core::serde::Deserialize)] + #[serde(crate = "sp_core::serde")] + struct BlockByNumber { + block: u64, + }; + + Ok(if let Ok(block_hash) = params.parse::() { + let Some(block_hash) = hex::decode(&block_hash.block).ok().and_then(|bytes| { + <[u8; 32]>::try_from(bytes.as_slice()) + .map(::Hash::from) + .ok() + }) else { + return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -1, + "requested block hash wasn't a valid hash", + Option::<()>::None, + )); + }; + block_hash + } else { + let Ok(block_number) = params.parse::() else { + return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -1, + "requested block wasn't a valid hash nor number", + Option::<()>::None, + )); + }; + let Ok(Some(block_hash)) = client.block_hash(block_number.block) else { + return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -2, + "couldn't find requested block's hash", + Option::<()>::None, + )); + }; + block_hash + }) +} diff --git a/substrate/node/src/rpc/validator_sets.rs b/substrate/node/src/rpc/validator_sets.rs new file mode 100644 index 00000000..e3385513 --- /dev/null +++ b/substrate/node/src/rpc/validator_sets.rs @@ -0,0 +1,143 @@ +use std::{sync::Arc, ops::Deref, collections::HashSet}; + +use rand_core::{RngCore, OsRng}; + +use sp_core::Encode; +use sp_blockchain::{Error as BlockchainError, HeaderMetadata, HeaderBackend}; +use sp_consensus::BlockStatus; +use sp_block_builder::BlockBuilder; +use sp_api::ProvideRuntimeApi; +use sc_client_api::BlockBackend; + +use serai_abi::{primitives::prelude::*, SubstrateBlock as Block}; + +use serai_runtime::SeraiApi; + +use jsonrpsee::RpcModule; + +use super::utils::block_hash; + +pub(super) fn network( + params: &jsonrpsee::types::params::Params, +) -> Result { + #[derive(sp_core::serde::Deserialize)] + #[serde(crate = "sp_core::serde")] + struct Network { + network: String, + } + + let Ok(network) = params.parse::() else { + return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -1, + r#"missing `string` "network" field"#, + Option::<()>::None, + )); + }; + + Ok(match network.network.to_lowercase().as_str() { + "serai" => NetworkId::Serai, + "bitcoin" => NetworkId::External(ExternalNetworkId::Bitcoin), + "ethereum" => NetworkId::External(ExternalNetworkId::Ethereum), + "monero" => NetworkId::External(ExternalNetworkId::Monero), + _ => Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -1, + "unrecognized network requested", + Option::<()>::None, + ))?, + }) +} + +pub(super) fn set( + params: &jsonrpsee::types::params::Params, +) -> Result { + #[derive(sp_core::serde::Deserialize)] + #[serde(crate = "sp_core::serde")] + struct Set { + network: String, + session: u32, + } + + let Ok(set) = params.parse::() else { + return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -1, + r#"missing `object` "set" field"#, + Option::<()>::None, + )); + }; + + let network = match set.network.to_lowercase().as_str() { + "serai" => NetworkId::Serai, + "bitcoin" => NetworkId::External(ExternalNetworkId::Bitcoin), + "ethereum" => NetworkId::External(ExternalNetworkId::Ethereum), + "monero" => NetworkId::External(ExternalNetworkId::Monero), + _ => Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -1, + "unrecognized network requested", + Option::<()>::None, + ))?, + }; + + Ok(ValidatorSet { network, session: Session(set.session) }) +} + +pub(crate) fn module< + C: 'static + + Send + + Sync + + HeaderMetadata + + HeaderBackend + + BlockBackend + + ProvideRuntimeApi>, +>( + client: Arc, +) -> RpcModule { + let mut module = RpcModule::new(client); + + module.register_method("validator-sets/current_session", |params, client, _ext| { + let block_hash = block_hash(&**client, ¶ms)?; + let network = network(¶ms)?; + let Ok(session) = client.runtime_api().current_session(block_hash, network) else { + return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -2, + "couldn't fetch the session for the requested network", + Option::<()>::None, + )); + }; + Ok(session.map(|session| session.0)) + }); + + module.register_method("validator-sets/current_stake", |params, client, _ext| { + let block_hash = block_hash(&**client, ¶ms)?; + let network = network(¶ms)?; + let Ok(stake) = client.runtime_api().current_stake(block_hash, network) else { + return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -2, + "couldn't fetch the total allocated stake for the requested network", + Option::<()>::None, + )); + }; + Ok(stake.map(|stake| stake.0)) + }); + + module.register_method("validator-sets/keys", |params, client, _ext| { + let block_hash = block_hash(&**client, ¶ms)?; + let set = set(¶ms)?; + let Ok(set) = ExternalValidatorSet::try_from(set) else { + return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -1, + "requested keys for a non-extenral validator set", + Option::<()>::None, + )); + }; + let Ok(key_pair) = client.runtime_api().keys(block_hash, set) else { + return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -2, + "couldn't fetch the keys for the requested validator set", + Option::<()>::None, + )); + }; + Ok(hex::encode(borsh::to_vec(&key_pair).unwrap())) + }); + + module +} diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index e2cd527c..c27df819 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -6,9 +6,10 @@ extern crate alloc; use alloc::vec::Vec; use serai_abi::{ primitives::{ - crypto::{Public, SignedEmbeddedEllipticCurveKeys}, + crypto::{Public, SignedEmbeddedEllipticCurveKeys, KeyPair}, network_id::NetworkId, - balance::Balance, + validator_sets::{Session, ExternalValidatorSet, ValidatorSet}, + balance::{Amount, Balance}, }, Event, }; @@ -34,7 +35,10 @@ sp_api::decl_runtime_apis! { } pub trait SeraiApi { fn events() -> Vec>; - fn validators(network_id: NetworkId) -> Vec; + fn validators(network: NetworkId) -> Vec; + fn current_session(network: NetworkId) -> Option; + fn current_stake(network: NetworkId) -> Option; + fn keys(set: ExternalValidatorSet) -> Option; } } @@ -44,6 +48,8 @@ mod apis { use alloc::borrow::Cow; use serai_abi::{SubstrateHeader as Header, SubstrateBlock as Block}; + use super::*; + #[sp_version::runtime_version] pub const VERSION: sp_version::RuntimeVersion = sp_version::RuntimeVersion { spec_name: Cow::Borrowed("serai"), @@ -181,6 +187,15 @@ mod apis { ) -> Vec { unimplemented!("runtime is only implemented when WASM") } + fn current_session(network: NetworkId) -> Option { + unimplemented!("runtime is only implemented when WASM") + } + fn current_stake(network: NetworkId) -> Option { + unimplemented!("runtime is only implemented when WASM") + } + fn keys(set: ExternalValidatorSet) -> Option { + unimplemented!("runtime is only implemented when WASM") + } } } } diff --git a/substrate/runtime/src/wasm/mod.rs b/substrate/runtime/src/wasm/mod.rs index 05a7dc9f..976e99e7 100644 --- a/substrate/runtime/src/wasm/mod.rs +++ b/substrate/runtime/src/wasm/mod.rs @@ -9,7 +9,7 @@ use serai_abi::{ primitives::{ network_id::{ExternalNetworkId, NetworkId}, balance::{Amount, ExternalBalance}, - validator_sets::ValidatorSet, + validator_sets::{Session, ExternalValidatorSet, ValidatorSet}, address::SeraiAddress, }, SubstrateHeader as Header, SubstrateBlock, @@ -522,6 +522,21 @@ sp_api::impl_runtime_apis! { .map(|validator| validator.0.into()) .collect() } + fn current_session(network: NetworkId) -> Option { + ValidatorSets::current_session(network) + } + fn current_stake(network: NetworkId) -> Option { + ValidatorSets::stake_for_current_validator_set(network) + } + fn keys(set: ExternalValidatorSet) -> Option { + ValidatorSets::oraclization_key(set) + .and_then(|oraclization_key| { + ValidatorSets::external_key(set) + .map(|external_key| { + serai_abi::primitives::crypto::KeyPair(oraclization_key.into(), external_key) + }) + }) + } } } diff --git a/substrate/validator-sets/src/keys.rs b/substrate/validator-sets/src/keys.rs index d5f22dc6..d885d94f 100644 --- a/substrate/validator-sets/src/keys.rs +++ b/substrate/validator-sets/src/keys.rs @@ -33,6 +33,9 @@ pub(crate) trait Keys { /// The oraclization key for a validator set. fn oraclization_key(set: ExternalValidatorSet) -> Option; + + /// The external key for a validator set. + fn external_key(set: ExternalValidatorSet) -> Option; } impl Keys for S { @@ -53,4 +56,8 @@ impl Keys for S { fn oraclization_key(set: ExternalValidatorSet) -> Option { S::OraclizationKeys::get(set) } + + fn external_key(set: ExternalValidatorSet) -> Option { + S::ExternalKeys::get(set) + } } diff --git a/substrate/validator-sets/src/lib.rs b/substrate/validator-sets/src/lib.rs index 2582e2fe..d0d726e8 100644 --- a/substrate/validator-sets/src/lib.rs +++ b/substrate/validator-sets/src/lib.rs @@ -307,6 +307,14 @@ mod pallet { Abstractions::::selected_validators(set) } + pub fn oraclization_key(set: ExternalValidatorSet) -> Option { + Abstractions::::oraclization_key(set) + } + + pub fn external_key(set: ExternalValidatorSet) -> Option { + Abstractions::::external_key(set) + } + /* TODO pub fn distribute_block_rewards( network: NetworkId,