From 1866bb7ae3f557f4cccc6a987b3001d6a99bde02 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Thu, 6 Nov 2025 03:08:43 -0500 Subject: [PATCH] Begin work on the new RPC for the new node --- .github/workflows/tests.yml | 1 + Cargo.toml | 1 + substrate/client/bitcoin/Cargo.toml | 4 +- substrate/client/bitcoin/README.md | 3 + substrate/client/bitcoin/src/lib.rs | 5 +- substrate/client/ethereum/Cargo.toml | 4 +- substrate/client/ethereum/README.md | 3 + substrate/client/ethereum/src/lib.rs | 10 +- substrate/client/monero/Cargo.toml | 4 +- substrate/client/monero/README.md | 3 + substrate/client/monero/src/lib.rs | 4 + substrate/client/serai/Cargo.toml | 29 +++++ substrate/client/serai/LICENSE | 21 ++++ substrate/client/serai/README.md | 3 + substrate/client/serai/src/lib.rs | 140 ++++++++++++++++++++++ substrate/client/src/lib.rs | 15 ++- substrate/client/src/networks.rs | 8 -- substrate/node/src/rpc/blockchain.rs | 52 ++++++++ substrate/node/src/{rpc.rs => rpc/mod.rs} | 100 ++-------------- substrate/node/src/rpc/p2p_validators.rs | 86 +++++++++++++ 20 files changed, 378 insertions(+), 118 deletions(-) create mode 100644 substrate/client/bitcoin/README.md create mode 100644 substrate/client/ethereum/README.md create mode 100644 substrate/client/monero/README.md create mode 100644 substrate/client/serai/Cargo.toml create mode 100644 substrate/client/serai/LICENSE create mode 100644 substrate/client/serai/README.md create mode 100644 substrate/client/serai/src/lib.rs delete mode 100644 substrate/client/src/networks.rs create mode 100644 substrate/node/src/rpc/blockchain.rs rename substrate/node/src/{rpc.rs => rpc/mod.rs} (56%) create mode 100644 substrate/node/src/rpc/p2p_validators.rs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e88e7bab..90a96450 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -109,4 +109,5 @@ jobs: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-bitcoin GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-ethereum GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-monero + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-serai GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client diff --git a/Cargo.toml b/Cargo.toml index c55577f4..7351e594 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ members = [ "substrate/client/bitcoin", "substrate/client/ethereum", "substrate/client/monero", + "substrate/client/serai", "substrate/client", "orchestration", diff --git a/substrate/client/bitcoin/Cargo.toml b/substrate/client/bitcoin/Cargo.toml index be79745e..b432397c 100644 --- a/substrate/client/bitcoin/Cargo.toml +++ b/substrate/client/bitcoin/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "serai-client-bitcoin" version = "0.1.0" -description = "Bitcoin client library for the Serai network" +description = "A client for the Serai network's Bitcoin functionality" license = "MIT" repository = "https://github.com/serai-dex/serai/tree/develop/substrate/client/bitcoin" authors = ["Luke Parker "] -keywords = ["serai"] +keywords = ["serai", "bitcoin"] edition = "2021" rust-version = "1.85" diff --git a/substrate/client/bitcoin/README.md b/substrate/client/bitcoin/README.md new file mode 100644 index 00000000..8105baab --- /dev/null +++ b/substrate/client/bitcoin/README.md @@ -0,0 +1,3 @@ +# serai-client Bitcoin + +A client for the Serai network's Bitcoin functionality. diff --git a/substrate/client/bitcoin/src/lib.rs b/substrate/client/bitcoin/src/lib.rs index f3c476ba..2654b9fc 100644 --- a/substrate/client/bitcoin/src/lib.rs +++ b/substrate/client/bitcoin/src/lib.rs @@ -1,4 +1,7 @@ -#![no_std] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] use core::{str::FromStr, fmt}; diff --git a/substrate/client/ethereum/Cargo.toml b/substrate/client/ethereum/Cargo.toml index afa9314c..35fdba80 100644 --- a/substrate/client/ethereum/Cargo.toml +++ b/substrate/client/ethereum/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "serai-client-ethereum" version = "0.1.0" -description = "Ethereum client library for the Serai network" +description = "A client for the Serai network's Ethereum functionality" license = "MIT" repository = "https://github.com/serai-dex/serai/tree/develop/substrate/client/ethereum" authors = ["Luke Parker "] -keywords = ["serai"] +keywords = ["serai", "ethereum"] edition = "2021" rust-version = "1.85" diff --git a/substrate/client/ethereum/README.md b/substrate/client/ethereum/README.md new file mode 100644 index 00000000..0a50a549 --- /dev/null +++ b/substrate/client/ethereum/README.md @@ -0,0 +1,3 @@ +# serai-client Ethereum + +A client for the Serai network's Ethereum functionality. diff --git a/substrate/client/ethereum/src/lib.rs b/substrate/client/ethereum/src/lib.rs index 8615b4b3..e0adff2d 100644 --- a/substrate/client/ethereum/src/lib.rs +++ b/substrate/client/ethereum/src/lib.rs @@ -1,4 +1,7 @@ -#![no_std] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] use core::str::FromStr; use std_shims::{vec::Vec, io::Read}; @@ -12,6 +15,7 @@ use serai_primitives::address::ExternalAddress; /// Payments to an address with a gas limit which exceed this value will be dropped entirely. pub const ADDRESS_GAS_LIMIT: u32 = 950_000; +/// A contract to deploy, enabling executing arbitrary code. #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub struct ContractDeployment { /// The gas limit to use for this contract's execution. @@ -26,8 +30,8 @@ pub struct ContractDeployment { code: Vec, } -/// A contract to deploy, enabling executing arbitrary code. impl ContractDeployment { + /// Create a new `ContractDeployment`. pub fn new(gas_limit: u32, code: Vec) -> Option { // Check the gas limit is less the address gas limit if gas_limit > ADDRESS_GAS_LIMIT { @@ -44,9 +48,11 @@ impl ContractDeployment { Some(Self { gas_limit, code }) } + /// The gas limit to use when deploying (and executing) this contract. pub fn gas_limit(&self) -> u32 { self.gas_limit } + /// The code for the contract to deploy. pub fn code(&self) -> &[u8] { &self.code } diff --git a/substrate/client/monero/Cargo.toml b/substrate/client/monero/Cargo.toml index db922dc0..5789b12c 100644 --- a/substrate/client/monero/Cargo.toml +++ b/substrate/client/monero/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "serai-client-monero" version = "0.1.0" -description = "Monero client library for the Serai network" +description = "A client for the Serai network's Monero functionality" license = "MIT" repository = "https://github.com/serai-dex/serai/tree/develop/substrate/client/monero" authors = ["Luke Parker "] -keywords = ["serai"] +keywords = ["serai", "monero"] edition = "2021" rust-version = "1.85" diff --git a/substrate/client/monero/README.md b/substrate/client/monero/README.md new file mode 100644 index 00000000..c2c069a2 --- /dev/null +++ b/substrate/client/monero/README.md @@ -0,0 +1,3 @@ +# serai-client Monero + +A client for the Serai network's Monero functionality. diff --git a/substrate/client/monero/src/lib.rs b/substrate/client/monero/src/lib.rs index 23e950d5..1f47d977 100644 --- a/substrate/client/monero/src/lib.rs +++ b/substrate/client/monero/src/lib.rs @@ -1,3 +1,7 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + use core::{str::FromStr, fmt}; use dalek_ff_group::{EdwardsPoint, Ed25519}; diff --git a/substrate/client/serai/Cargo.toml b/substrate/client/serai/Cargo.toml new file mode 100644 index 00000000..d16f0131 --- /dev/null +++ b/substrate/client/serai/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "serai-client-serai" +version = "0.1.0" +description = "A client for the Serai network itself" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/substrate/client/serai" +authors = ["Luke Parker "] +keywords = ["serai"] +edition = "2021" +rust-version = "1.85" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true + +[dependencies] +thiserror = { version = "2", default-features = false, features = ["std"] } +core-json-traits = { version = "0.4", default-features = false, features = ["alloc"] } +core-json-derive = { version = "0.4", default-features = false } +simple-request = { path = "../../../common/request", version = "0.3" } + +hex = { version = "0.4", default-features = false, features = ["alloc"] } +borsh = { version = "1", default-features = false, features = ["std"] } +serai-abi = { path = "../../abi", version = "0.1" } + +async-lock = "3" diff --git a/substrate/client/serai/LICENSE b/substrate/client/serai/LICENSE new file mode 100644 index 00000000..32ff304a --- /dev/null +++ b/substrate/client/serai/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2025 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/substrate/client/serai/README.md b/substrate/client/serai/README.md new file mode 100644 index 00000000..62f5bf59 --- /dev/null +++ b/substrate/client/serai/README.md @@ -0,0 +1,3 @@ +# serai-client Serai + +A client for the Serai network itself. diff --git a/substrate/client/serai/src/lib.rs b/substrate/client/serai/src/lib.rs new file mode 100644 index 00000000..597cec5f --- /dev/null +++ b/substrate/client/serai/src/lib.rs @@ -0,0 +1,140 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + +use std::io::Read; + +use thiserror::Error; +use core_json_traits::{JsonDeserialize, JsonStructure}; +use core_json_derive::JsonDeserialize; +use simple_request::{hyper, Request, TokioClient}; + +use borsh::BorshDeserialize; +pub use serai_abi as abi; +use abi::{primitives::network_id::ExternalNetworkId, Event}; + +use async_lock::RwLock; + +/// An error from the RPC. +#[derive(Debug, Error)] +pub enum RpcError { + /// An internal error occured. + #[error("internal error: {0}")] + InternalError(String), + /// A failure with the connection occurred. + #[error("failed to communicate with serai")] + ConnectionError, + /// The node provided an invalid response. + #[error("node is faulty: {0}")] + InvalidNode(String), + /// The response contained an error. + #[error("error in response: {0}")] + ErrorInResponse(String), +} + +/// An RPC client to a Serai node. +#[derive(Clone)] +pub struct Serai { + url: String, + client: TokioClient, +} + +/// An RPC client to a Serai node, scoped to a specific block. +pub struct TemporalSerai<'a> { + serai: &'a Serai, + block: [u8; 32], + events: RwLock>>, +} +impl Clone for TemporalSerai<'_> { + fn clone(&self) -> Self { + Self { serai: self.serai, block: self.block, events: RwLock::new(None) } + } +} + +impl Serai { + async fn call( + &self, + method: &str, + params: &str, + ) -> Result { + let request = + format!(r#"{{ "jsonrpc": "2.0", "id": 0, "method": {method}, "params": {params} }}"#); + let request = hyper::Request::post(&self.url) + .header("Content-Type", "application/json") + .body(request.as_bytes().to_vec().into()) + .map_err(|e| RpcError::InternalError(format!("{e:?}")))?; + + #[derive(Default, JsonDeserialize)] + pub struct Error { + message: String, + } + #[derive(Default, JsonDeserialize)] + struct Response { + result: Option, + error: Option, + } + + let mut response_reader = self + .client + .request(request) + .await + .map_err(|_| RpcError::ConnectionError)? + .body() + .await + .map_err(|_| RpcError::ConnectionError)?; + let mut response_vec = Vec::with_capacity(1024); + response_reader.read_to_end(&mut response_vec).map_err(|_| { + RpcError::InternalError("couldn't read response from `simple-request` into `Vec`".to_string()) + })?; + + // TODO: Map `std::io::Read` into `core_json::Read` with an adapter + let response = Response::::deserialize_structure::< + _, + core_json_traits::ConstStack<32>, + >(response_vec.as_slice()) + .map_err(|e| RpcError::InvalidNode(format!("{e:?}")))?; + match response { + Response { result: Some(result), error: None } => Ok(result), + Response { result: None, error: Some(error) } => { + Err(RpcError::ErrorInResponse(error.message)) + } + Response { result: Some(_), error: Some(_) } | Response { result: None, error: None } => { + Err(RpcError::InvalidNode( + "node didn't exclusively provide either `result` or `error`".to_string(), + )) + } + } + } + + /// Create a new RPC client. + pub fn new(url: String) -> Result { + let client = TokioClient::with_connection_pool().map_err(|_| RpcError::ConnectionError)?; + Ok(Serai { url, client }) + } + + /// Fetch a block from the Serai blockchain. + pub async fn block(&self, hash: [u8; 32]) -> Result { + let bin: String = self.call("serai_block", &format!("[{}]", hex::encode(hash))).await?; + serai_abi::Block::deserialize( + &mut hex::decode(&bin) + .map_err(|_| RpcError::InvalidNode("node returned non-hex-encoded block".to_string()))? + .as_slice(), + ) + .map_err(|_| RpcError::InvalidNode("node returned invalid block".to_string())) + } + + /// Return the P2P addresses for the validators of the specified network. + pub async fn p2p_validators(&self, network: ExternalNetworkId) -> Result, RpcError> { + self + .call( + "p2p_validators", + match network { + ExternalNetworkId::Bitcoin => "[bitcoin]", + ExternalNetworkId::Ethereum => "[ethereum]", + ExternalNetworkId::Monero => "[monero]", + _ => Err(RpcError::InternalError("unrecognized external network ID".to_string()))?, + }, + ) + .await + } +} diff --git a/substrate/client/src/lib.rs b/substrate/client/src/lib.rs index fc0f902e..f0ee54d0 100644 --- a/substrate/client/src/lib.rs +++ b/substrate/client/src/lib.rs @@ -1,13 +1,12 @@ -#[cfg(feature = "networks")] -pub mod networks; +#[cfg(feature = "bitcoin")] +pub use serai_client_bitcoin as bitcoin; +#[cfg(feature = "ethereum")] +pub mod serai_client_ethereum as ethereum; +#[cfg(feature = "monero")] +pub mod serai_client_monero as monero; #[cfg(feature = "serai")] -mod serai; -#[cfg(feature = "serai")] -pub use serai::*; - -#[cfg(not(feature = "serai"))] -pub use serai_abi::primitives; +pub use serai_client_serai as serai; #[cfg(test)] mod tests; diff --git a/substrate/client/src/networks.rs b/substrate/client/src/networks.rs deleted file mode 100644 index a142997e..00000000 --- a/substrate/client/src/networks.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[cfg(feature = "bitcoin")] -pub use serai_client_bitcoin; - -#[cfg(feature = "ethereum")] -pub mod serai_client_ethereum; - -#[cfg(feature = "monero")] -pub mod serai_client_monero; diff --git a/substrate/node/src/rpc/blockchain.rs b/substrate/node/src/rpc/blockchain.rs new file mode 100644 index 00000000..e189d62e --- /dev/null +++ b/substrate/node/src/rpc/blockchain.rs @@ -0,0 +1,52 @@ +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_block_builder::BlockBuilder; +use sp_api::ProvideRuntimeApi; + +use serai_abi::{primitives::prelude::*, SubstrateBlock as Block}; +use serai_runtime::*; + +use jsonrpsee::RpcModule; + +use sc_client_api::BlockBackend; + +pub(crate) fn module< + C: 'static + + Send + + Sync + + HeaderMetadata + + HeaderBackend + + BlockBackend + + ProvideRuntimeApi, +>( + client: Arc, +) -> Result, Box> { + let mut module = RpcModule::new(client); + module.register_async_method("serai_block", |params, client, _ext| async move { + let [block_hash]: [String; 1] = params.parse()?; + 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, + )); + }; + let Some(block) = client.block(block_hash).ok().flatten() else { + return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -2, + "couldn't find requested block", + Option::<()>::None, + )); + }; + Ok(hex::encode(block.block.encode())) + })?; + Ok(module) +} diff --git a/substrate/node/src/rpc.rs b/substrate/node/src/rpc/mod.rs similarity index 56% rename from substrate/node/src/rpc.rs rename to substrate/node/src/rpc/mod.rs index 832fb77a..cc856784 100644 --- a/substrate/node/src/rpc.rs +++ b/substrate/node/src/rpc/mod.rs @@ -1,5 +1,3 @@ -#![expect(unused_imports)] - use std::{sync::Arc, ops::Deref, collections::HashSet}; use rand_core::{RngCore, OsRng}; @@ -19,6 +17,9 @@ use jsonrpsee::RpcModule; use sc_client_api::BlockBackend; use sc_transaction_pool_api::TransactionPool; +mod blockchain; +mod p2p_validators; + pub struct FullDeps { pub id: String, pub client: Arc, @@ -38,74 +39,14 @@ pub fn create_full< >( deps: FullDeps, ) -> Result, Box> { - let mut module = RpcModule::new(()); let FullDeps { id, client, pool, authority_discovery } = deps; + let mut root = RpcModule::new(()); + root.merge(blockchain::module(client.clone())?)?; if let Some(authority_discovery) = authority_discovery { - let mut authority_discovery_module = - RpcModule::new((id, client.clone(), RwLock::new(authority_discovery))); - authority_discovery_module.register_async_method( - "p2p_validators", - |params, context, _ext| async move { - let [network]: [String; 1] = params.parse()?; - let network = match network.to_lowercase().as_str() { - "serai" => NetworkId::Serai, - "bitcoin" => ExternalNetworkId::Bitcoin.into(), - "ethereum" => ExternalNetworkId::Ethereum.into(), - "monero" => ExternalNetworkId::Monero.into(), - _ => Err(jsonrpsee::types::error::ErrorObjectOwned::owned( - -1, - "network to fetch the `p2p_validators` of was unrecognized".to_string(), - Option::<()>::None, - ))?, - }; - let (id, client, authority_discovery) = &*context; - let latest_block = client.info().best_hash; - - let validators = client.runtime_api().validators(latest_block, network).map_err(|_| { - jsonrpsee::types::error::ErrorObjectOwned::owned( - -2, - format!( - "couldn't get validators from the latest block, which is likely a fatal bug. {}", - "please report this at https://github.com/serai-dex/serai", - ), - Option::<()>::None, - ) - }); - let validators = match validators { - Ok(validators) => validators, - Err(e) => return Err(e), - }; - // Always return the protocol's bootnodes - let mut all_p2p_addresses = crate::chain_spec::bootnode_multiaddrs(id); - // Additionally returns validators found over the DHT - for validator in validators { - let mut returned_addresses = authority_discovery - .write() - .await - .get_addresses_by_authority_id(validator.into()) - .await - .unwrap_or_else(HashSet::new) - .into_iter() - .collect::>(); - // Randomly select an address - // There should be one, there may be two if their IP address changed, and more should only - // occur if they have multiple proxies/an IP address changing frequently/some issue - // preventing consistent self-identification - // It isn't beneficial to use multiple addresses for a single peer here - if !returned_addresses.is_empty() { - all_p2p_addresses.push( - returned_addresses - .remove(usize::try_from(OsRng.next_u64() >> 32).unwrap() % returned_addresses.len()) - .into(), - ); - } - } - Ok(all_p2p_addresses) - }, - )?; - module.merge(authority_discovery_module)?; + root.merge(p2p_validators::module(id, client, authority_discovery)?)?; } + Ok(root) /* TODO use ciphersuite::{GroupIo, WithPreferredHash}; @@ -205,31 +146,4 @@ pub fn create_full< module.merge(serai_json_module)?; */ - - let mut block_bin_module = RpcModule::new(client); - block_bin_module.register_async_method("serai_block", |params, client, _ext| async move { - let [block_hash]: [String; 1] = params.parse()?; - 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, - )); - }; - let Some(block) = client.block(block_hash).ok().flatten() else { - return Err(jsonrpsee::types::error::ErrorObjectOwned::owned( - -1, - "couldn't find requested block", - Option::<()>::None, - )); - }; - Ok(hex::encode(block.block.encode())) - })?; - module.merge(block_bin_module)?; - - Ok(module) } diff --git a/substrate/node/src/rpc/p2p_validators.rs b/substrate/node/src/rpc/p2p_validators.rs new file mode 100644 index 00000000..f07037d4 --- /dev/null +++ b/substrate/node/src/rpc/p2p_validators.rs @@ -0,0 +1,86 @@ +use std::{sync::Arc, ops::Deref, collections::HashSet}; + +use rand_core::{RngCore, OsRng}; + +use sp_core::Encode; +use sp_blockchain::{Error as BlockchainError, HeaderBackend}; +use sp_api::ProvideRuntimeApi; + +use serai_abi::{primitives::prelude::*, SubstrateBlock as Block}; +use serai_runtime::*; + +use tokio::sync::RwLock; + +use jsonrpsee::RpcModule; + +pub(crate) fn module< + C: 'static + + Send + + Sync + + HeaderBackend + + ProvideRuntimeApi>, +>( + id: String, + client: Arc, + authority_discovery: sc_authority_discovery::Service, +) -> Result, Box> { + let mut module = RpcModule::new((id, client, RwLock::new(authority_discovery))); + module.register_async_method("p2p_validators", |params, context, _ext| async move { + let [network]: [String; 1] = params.parse()?; + let network = match network.to_lowercase().as_str() { + "serai" => NetworkId::Serai, + "bitcoin" => ExternalNetworkId::Bitcoin.into(), + "ethereum" => ExternalNetworkId::Ethereum.into(), + "monero" => ExternalNetworkId::Monero.into(), + _ => Err(jsonrpsee::types::error::ErrorObjectOwned::owned( + -1, + "network to fetch the `p2p_validators` of was unrecognized".to_string(), + Option::<()>::None, + ))?, + }; + let (id, client, authority_discovery) = &*context; + let latest_block = client.info().best_hash; + + let validators = client.runtime_api().validators(latest_block, network).map_err(|_| { + jsonrpsee::types::error::ErrorObjectOwned::owned( + -2, + format!( + "couldn't get validators from the latest block, which is likely a fatal bug. {}", + "please report this at https://github.com/serai-dex/serai", + ), + Option::<()>::None, + ) + }); + let validators = match validators { + Ok(validators) => validators, + Err(e) => return Err(e), + }; + // Always return the protocol's bootnodes + let mut all_p2p_addresses = crate::chain_spec::bootnode_multiaddrs(id); + // Additionally returns validators found over the DHT + for validator in validators { + let mut returned_addresses = authority_discovery + .write() + .await + .get_addresses_by_authority_id(validator.into()) + .await + .unwrap_or_else(HashSet::new) + .into_iter() + .collect::>(); + // Randomly select an address + // There should be one, there may be two if their IP address changed, and more should only + // occur if they have multiple proxies/an IP address changing frequently/some issue + // preventing consistent self-identification + // It isn't beneficial to use multiple addresses for a single peer here + if !returned_addresses.is_empty() { + all_p2p_addresses.push( + returned_addresses + .remove(usize::try_from(OsRng.next_u64() >> 32).unwrap() % returned_addresses.len()) + .into(), + ); + } + } + Ok(all_p2p_addresses) + })?; + Ok(module) +}