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,