Begin work on the new RPC for the new node

This commit is contained in:
Luke Parker
2025-11-06 03:08:43 -05:00
parent aff2065c31
commit 1866bb7ae3
20 changed files with 378 additions and 118 deletions

View File

@@ -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 <lukeparker5132@gmail.com>"]
keywords = ["serai"]
keywords = ["serai", "bitcoin"]
edition = "2021"
rust-version = "1.85"

View File

@@ -0,0 +1,3 @@
# serai-client Bitcoin
A client for the Serai network's Bitcoin functionality.

View File

@@ -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};

View File

@@ -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 <lukeparker5132@gmail.com>"]
keywords = ["serai"]
keywords = ["serai", "ethereum"]
edition = "2021"
rust-version = "1.85"

View File

@@ -0,0 +1,3 @@
# serai-client Ethereum
A client for the Serai network's Ethereum functionality.

View File

@@ -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<u8>,
}
/// A contract to deploy, enabling executing arbitrary code.
impl ContractDeployment {
/// Create a new `ContractDeployment`.
pub fn new(gas_limit: u32, code: Vec<u8>) -> Option<Self> {
// 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
}

View File

@@ -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 <lukeparker5132@gmail.com>"]
keywords = ["serai"]
keywords = ["serai", "monero"]
edition = "2021"
rust-version = "1.85"

View File

@@ -0,0 +1,3 @@
# serai-client Monero
A client for the Serai network's Monero functionality.

View File

@@ -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};

View File

@@ -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 <lukeparker5132@gmail.com>"]
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"

View File

@@ -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.

View File

@@ -0,0 +1,3 @@
# serai-client Serai
A client for the Serai network itself.

View File

@@ -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<Option<Vec<Event>>>,
}
impl Clone for TemporalSerai<'_> {
fn clone(&self) -> Self {
Self { serai: self.serai, block: self.block, events: RwLock::new(None) }
}
}
impl Serai {
async fn call<ResponseValue: Default + JsonDeserialize>(
&self,
method: &str,
params: &str,
) -> Result<ResponseValue, RpcError> {
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<ResponseValue: Default + JsonDeserialize> {
result: Option<ResponseValue>,
error: Option<Error>,
}
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::<ResponseValue>::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<Self, RpcError> {
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<serai_abi::Block, RpcError> {
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<Vec<String>, 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
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<Block, Error = BlockchainError>
+ HeaderBackend<Block>
+ BlockBackend<Block>
+ ProvideRuntimeApi<Block>,
>(
client: Arc<C>,
) -> Result<RpcModule<impl 'static + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
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(<Block as sp_runtime::traits::Block>::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)
}

View File

@@ -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<C, P> {
pub id: String,
pub client: Arc<C>,
@@ -38,74 +39,14 @@ pub fn create_full<
>(
deps: FullDeps<C, P>,
) -> Result<RpcModule<()>, Box<dyn std::error::Error + Send + Sync>> {
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::<Vec<_>>();
// 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(<Block as sp_runtime::traits::Block>::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)
}

View File

@@ -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<Block>
+ ProvideRuntimeApi<Block, Api: serai_runtime::SeraiApi<Block>>,
>(
id: String,
client: Arc<C>,
authority_discovery: sc_authority_discovery::Service,
) -> Result<RpcModule<impl 'static + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
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::<Vec<_>>();
// 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)
}