Add method to fetch a block's events to the RPC

This commit is contained in:
Luke Parker
2025-11-13 04:50:54 -05:00
parent 367a5769e8
commit 509bd58f4e
10 changed files with 324 additions and 70 deletions

View File

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

View 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(),
)
}
}

View File

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

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