mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Add method to fetch a block's events to the RPC
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use core::future::Future;
|
||||
use core::{ops::Deref, convert::AsRef, future::Future};
|
||||
use std::{sync::Arc, io::Read};
|
||||
|
||||
use thiserror::Error;
|
||||
@@ -19,6 +19,10 @@ use abi::{
|
||||
|
||||
use async_lock::RwLock;
|
||||
|
||||
/// RPC client functionality for the validator sets module.
|
||||
pub mod validator_sets;
|
||||
use validator_sets::*;
|
||||
|
||||
/// An error from the RPC.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RpcError {
|
||||
@@ -177,3 +181,45 @@ impl Serai {
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TemporalSerai<'a> {
|
||||
/// Fetch the events for this block.
|
||||
///
|
||||
/// The returned `Option` will always be `Some(_)`.
|
||||
async fn events(&self) -> Result<async_lock::RwLockReadGuard<'_, Option<Vec<Event>>>, 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
|
||||
.serai
|
||||
.call::<Vec<String>>("serai_events", &format!(r#"["{}"]"#, self.block))
|
||||
.await?
|
||||
.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::<Result<_, _>>()?,
|
||||
);
|
||||
}
|
||||
}
|
||||
events = self.events.read().await;
|
||||
}
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
/// Scope to the validator sets module.
|
||||
pub fn validator_sets(&self) -> ValidatorSets<'_> {
|
||||
ValidatorSets(self)
|
||||
}
|
||||
}
|
||||
|
||||
50
substrate/client/serai/src/validator_sets.rs
Normal file
50
substrate/client/serai/src/validator_sets.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
pub use serai_abi::validator_sets::Event;
|
||||
use crate::{RpcError, TemporalSerai};
|
||||
|
||||
/// A `TemporalSerai` scoped to the validator sets module.
|
||||
#[derive(Clone)]
|
||||
pub struct ValidatorSets<'a>(pub(super) &'a TemporalSerai<'a>);
|
||||
|
||||
impl<'a> ValidatorSets<'a> {
|
||||
/// The events from the validator sets module.
|
||||
pub async fn events(&self) -> Result<Vec<Event>, RpcError> {
|
||||
Ok(
|
||||
self
|
||||
.0
|
||||
.events()
|
||||
.await?
|
||||
.as_ref()
|
||||
.expect("`TemporalSerai::events` returned None")
|
||||
.iter()
|
||||
.filter_map(|event| match event {
|
||||
serai_abi::Event::ValidatorSets(event) => Some(event.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The `SetDecided` events from the validator sets module.
|
||||
pub async fn set_decided_events(&self) -> Result<Vec<Event>, RpcError> {
|
||||
Ok(
|
||||
self
|
||||
.events()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|event| matches!(event, Event::SetDecided { .. }))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The `AcceptedHandover` events from the validator sets module.
|
||||
pub async fn accepted_handover_events(&self) -> Result<Vec<Event>, RpcError> {
|
||||
Ok(
|
||||
self
|
||||
.events()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|event| matches!(event, Event::AcceptedHandover { .. }))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,18 @@ async fn blockchain() {
|
||||
.run_async(async |ops| {
|
||||
let serai = serai_substrate_tests::rpc(&ops, handle).await;
|
||||
|
||||
'outer: {
|
||||
for _ in 0 .. (5 * 10) {
|
||||
tokio::time::sleep(core::time::Duration::from_secs(6)).await;
|
||||
|
||||
let latest_finalized = serai.latest_finalized_block_number().await.unwrap();
|
||||
if latest_finalized > 0 {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
panic!("finalized block remained the genesis block for over five minutes");
|
||||
};
|
||||
|
||||
// Check the sanity of fetching a block
|
||||
let test_finalized_block = |number| {
|
||||
let serai = &serai;
|
||||
|
||||
131
substrate/client/serai/tests/validator_sets.rs
Normal file
131
substrate/client/serai/tests/validator_sets.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use serai_abi::{
|
||||
primitives::{
|
||||
network_id::{ExternalNetworkId, NetworkId},
|
||||
validator_sets::{Session, ValidatorSet},
|
||||
},
|
||||
validator_sets::Event,
|
||||
};
|
||||
|
||||
use serai_client_serai::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn validator_sets() {
|
||||
let mut test = dockertest::DockerTest::new();
|
||||
let (composition, handle) = serai_substrate_tests::composition(
|
||||
"alice",
|
||||
serai_docker_tests::fresh_logs_folder(true, "serai-client/validator_sets"),
|
||||
);
|
||||
test.provide_container(
|
||||
composition
|
||||
.replace_cmd(
|
||||
["serai-node", "--unsafe-rpc-external", "--rpc-cors", "all", "--dev"]
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect(),
|
||||
)
|
||||
.replace_env([("RUST_LOG".to_string(), "runtime=debug".to_string())].into()),
|
||||
);
|
||||
|
||||
test
|
||||
.run_async(async |ops| {
|
||||
let serai = serai_substrate_tests::rpc(&ops, handle).await;
|
||||
|
||||
'outer: {
|
||||
for _ in 0 .. (5 * 10) {
|
||||
tokio::time::sleep(core::time::Duration::from_secs(6)).await;
|
||||
|
||||
let latest_finalized = serai.latest_finalized_block_number().await.unwrap();
|
||||
if latest_finalized > 0 {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
panic!("finalized block remained the genesis block for over five minutes");
|
||||
};
|
||||
|
||||
// The genesis block should have the expected events
|
||||
{
|
||||
{
|
||||
let mut events = serai
|
||||
.as_of(serai.block_by_number(0).await.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) },
|
||||
},
|
||||
Event::SetDecided {
|
||||
set: ValidatorSet { network: NetworkId::Serai, session: Session(1) },
|
||||
},
|
||||
Event::SetDecided {
|
||||
set: ValidatorSet {
|
||||
network: NetworkId::External(ExternalNetworkId::Bitcoin),
|
||||
session: Session(0),
|
||||
},
|
||||
},
|
||||
Event::SetDecided {
|
||||
set: ValidatorSet {
|
||||
network: NetworkId::External(ExternalNetworkId::Ethereum),
|
||||
session: Session(0),
|
||||
},
|
||||
},
|
||||
Event::SetDecided {
|
||||
set: ValidatorSet {
|
||||
network: NetworkId::External(ExternalNetworkId::Monero),
|
||||
session: Session(0),
|
||||
},
|
||||
},
|
||||
];
|
||||
expected.sort_by_key(|event| borsh::to_vec(event).unwrap());
|
||||
assert_eq!(events, expected);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
serai
|
||||
.as_of(serai.block_by_number(0).await.unwrap().header.hash())
|
||||
.await
|
||||
.unwrap()
|
||||
.validator_sets()
|
||||
.accepted_handover_events()
|
||||
.await
|
||||
.unwrap(),
|
||||
vec![Event::AcceptedHandover {
|
||||
set: ValidatorSet { network: NetworkId::Serai, session: Session(0) }
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
// The next block should not have these events
|
||||
{
|
||||
assert_eq!(
|
||||
serai
|
||||
.as_of(serai.block_by_number(1).await.unwrap().header.hash())
|
||||
.await
|
||||
.unwrap()
|
||||
.validator_sets()
|
||||
.set_decided_events()
|
||||
.await
|
||||
.unwrap(),
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(
|
||||
serai
|
||||
.as_of(serai.block_by_number(1).await.unwrap().header.hash())
|
||||
.await
|
||||
.unwrap()
|
||||
.validator_sets()
|
||||
.accepted_handover_events()
|
||||
.await
|
||||
.unwrap(),
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
|
||||
println!("Finished `serai-client/blockchain` test");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
@@ -28,18 +28,6 @@ impl crate::Config<CoinsInstance> for Test {
|
||||
type AllowMint = crate::AlwaysAllowMint;
|
||||
}
|
||||
|
||||
impl TryFrom<RuntimeEvent> for serai_abi::Event {
|
||||
type Error = ();
|
||||
fn try_from(event: RuntimeEvent) -> Result<serai_abi::Event, ()> {
|
||||
match event {
|
||||
RuntimeEvent::Core(serai_core_pallet::Event::Event(event)) => {
|
||||
Ok(serai_abi::Event::deserialize_reader(&mut event.as_slice()).unwrap())
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_test_ext() -> sp_io::TestExternalities {
|
||||
let mut storage = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ fn mint() {
|
||||
// test events
|
||||
let mint_events = Core::events()
|
||||
.iter()
|
||||
.map(|event| borsh::from_slice::<serai_abi::Event>(event.as_slice()).unwrap())
|
||||
.filter_map(|event| {
|
||||
if let serai_abi::Event::Coins(e) = &event {
|
||||
if matches!(e, CoinsEvent::Mint { .. }) {
|
||||
@@ -82,6 +83,7 @@ fn burn_with_instruction() {
|
||||
|
||||
let burn_events = Core::events()
|
||||
.iter()
|
||||
.map(|event| borsh::from_slice::<serai_abi::Event>(event.as_slice()).unwrap())
|
||||
.filter_map(|event| {
|
||||
if let serai_abi::Event::Coins(e) = &event {
|
||||
if matches!(e, CoinsEvent::BurnWithInstruction { .. }) {
|
||||
|
||||
@@ -132,15 +132,16 @@ pub mod pallet {
|
||||
|
||||
/// Fetch all of Serai's events.
|
||||
///
|
||||
/// This MUST only be used for testing purposes.
|
||||
#[cfg(any(feature = "std", feature = "runtime-benchmarks", test))]
|
||||
pub fn events() -> Vec<serai_abi::Event>
|
||||
/// This MUST NOT be called during a transaction/block's execution.
|
||||
pub fn events() -> Vec<Vec<u8>>
|
||||
where
|
||||
serai_abi::Event: TryFrom<T::RuntimeEvent>,
|
||||
T::RuntimeEvent: TryInto<Event<T>>,
|
||||
{
|
||||
frame_system::Pallet::<T>::events()
|
||||
.into_iter()
|
||||
.filter_map(|e| serai_abi::Event::try_from(e.event).ok())
|
||||
frame_system::Pallet::<T>::read_events_no_consensus()
|
||||
.filter_map(|e| match e.event.try_into() {
|
||||
Ok(Event::Event(bytes)) => Some(bytes),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,27 +11,20 @@ use sc_client_api::BlockBackend;
|
||||
|
||||
use serai_abi::{primitives::prelude::*, SubstrateBlock as Block};
|
||||
|
||||
use serai_runtime::SeraiApi;
|
||||
|
||||
use jsonrpsee::RpcModule;
|
||||
|
||||
pub(crate) fn module<
|
||||
C: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ HeaderMetadata<Block, Error = BlockchainError>
|
||||
fn block_hash<
|
||||
C: 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_method("serai_latestFinalizedBlockNumber", |_params, client, _ext| {
|
||||
client.info().finalized_number
|
||||
});
|
||||
|
||||
module.register_method("serai_isFinalized", |params, client, _ext| {
|
||||
let [block_hash]: [String; 1] = params.parse()?;
|
||||
client: &C,
|
||||
params: &jsonrpsee::types::params::Params,
|
||||
) -> Result<<Block as sp_runtime::traits::Block>::Hash, jsonrpsee::types::error::ErrorObjectOwned> {
|
||||
Ok(if let Ok(block_hash) = params.sequence().next::<String>() {
|
||||
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)
|
||||
@@ -43,6 +36,45 @@ pub(crate) fn module<
|
||||
Option::<()>::None,
|
||||
));
|
||||
};
|
||||
block_hash
|
||||
} else {
|
||||
let Ok(block_number) = params.sequence().next::<u64>() 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
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn module<
|
||||
C: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ HeaderMetadata<Block, Error = BlockchainError>
|
||||
+ HeaderBackend<Block>
|
||||
+ BlockBackend<Block>
|
||||
+ ProvideRuntimeApi<Block, Api: SeraiApi<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_method("serai_latestFinalizedBlockNumber", |_params, client, _ext| {
|
||||
client.info().finalized_number
|
||||
});
|
||||
|
||||
module.register_method("serai_isFinalized", |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 {
|
||||
return Err(jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||
@@ -65,37 +97,7 @@ pub(crate) fn module<
|
||||
})?;
|
||||
|
||||
module.register_method("serai_block", |params, client, _ext| {
|
||||
let block_hash = if let Ok([block_hash]) = params.parse::<[String; 1]>() {
|
||||
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,
|
||||
));
|
||||
};
|
||||
block_hash
|
||||
} else {
|
||||
let Ok([block_number]) = params.parse::<[u64; 1]>() 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
|
||||
};
|
||||
|
||||
let block_hash = block_hash(&**client, ¶ms)?;
|
||||
let Ok(Some(block)) = client.block(block_hash) else {
|
||||
return Err(jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||
-2,
|
||||
@@ -107,5 +109,17 @@ 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| {
|
||||
let block_hash = block_hash(&**client, ¶ms)?;
|
||||
let Ok(events) = client.runtime_api().events(block_hash) else {
|
||||
return Err(jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||
-2,
|
||||
"couldn't fetch the events for the requested block",
|
||||
Option::<()>::None,
|
||||
));
|
||||
};
|
||||
Ok(events.into_iter().map(hex::encode).collect::<Vec<String>>())
|
||||
})?;
|
||||
|
||||
Ok(module)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use serai_abi::primitives::{
|
||||
crypto::{Public, SignedEmbeddedEllipticCurveKeys},
|
||||
network_id::NetworkId,
|
||||
balance::Balance,
|
||||
use serai_abi::{
|
||||
primitives::{
|
||||
crypto::{Public, SignedEmbeddedEllipticCurveKeys},
|
||||
network_id::NetworkId,
|
||||
balance::Balance,
|
||||
},
|
||||
Event,
|
||||
};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
@@ -30,6 +33,7 @@ sp_api::decl_runtime_apis! {
|
||||
fn build(genesis: GenesisConfig);
|
||||
}
|
||||
pub trait SeraiApi {
|
||||
fn events() -> Vec<Vec<u8>>;
|
||||
fn validators(network_id: NetworkId) -> Vec<Public>;
|
||||
}
|
||||
}
|
||||
@@ -169,6 +173,9 @@ mod apis {
|
||||
}
|
||||
|
||||
impl crate::SeraiApi<Block> for Runtime {
|
||||
fn events() -> Vec<Vec<u8>> {
|
||||
unimplemented!("runtime is only implemented when WASM")
|
||||
}
|
||||
fn validators(
|
||||
network: serai_abi::primitives::network_id::NetworkId
|
||||
) -> Vec<serai_abi::primitives::crypto::Public> {
|
||||
|
||||
@@ -505,6 +505,9 @@ sp_api::impl_runtime_apis! {
|
||||
}
|
||||
|
||||
impl crate::SeraiApi<Block> for Runtime {
|
||||
fn events() -> Vec<Vec<u8>> {
|
||||
Core::events()
|
||||
}
|
||||
fn validators(network: NetworkId) -> Vec<serai_abi::primitives::crypto::Public> {
|
||||
// Returning the latest-decided, not latest and active, means the active set
|
||||
// may fail to peer find if there isn't sufficient overlap. If a large amount reboot,
|
||||
|
||||
Reference in New Issue
Block a user