Update the block RPCs to return null when missing, not an error

Promotes clarity.
This commit is contained in:
Luke Parker
2025-11-15 16:12:39 -05:00
parent 3cfbd9add7
commit fce26eaee1
6 changed files with 58 additions and 38 deletions

View File

@@ -134,25 +134,29 @@ impl Serai {
} }
async fn block_internal( async fn block_internal(
block: impl Future<Output = Result<String, RpcError>>, block: impl Future<Output = Result<Option<String>, RpcError>>,
) -> Result<Block, RpcError> { ) -> Result<Option<Block>, RpcError> {
let bin = block.await?; let bin = block.await?;
Block::deserialize( bin
&mut hex::decode(&bin) .map(|bin| {
.map_err(|_| RpcError::InvalidNode("node returned non-hex-encoded block".to_string()))? Block::deserialize(
.as_slice(), &mut hex::decode(&bin)
) .map_err(|_| RpcError::InvalidNode("node returned non-hex-encoded block".to_string()))?
.map_err(|_| RpcError::InvalidNode("node returned invalid block".to_string())) .as_slice(),
)
.map_err(|_| RpcError::InvalidNode("node returned invalid block".to_string()))
})
.transpose()
} }
/// Fetch a block from the Serai blockchain. /// Fetch a block from the Serai blockchain.
pub async fn block(&self, block: BlockHash) -> Result<Block, RpcError> { pub async fn block(&self, block: BlockHash) -> Result<Option<Block>, RpcError> {
Self::block_internal(self.call("blockchain/block", &format!(r#"{{ "block": "{block}" }}"#))) Self::block_internal(self.call("blockchain/block", &format!(r#"{{ "block": "{block}" }}"#)))
.await .await
} }
/// Fetch a block from the Serai blockchain by its number. /// Fetch a block from the Serai blockchain by its number.
pub async fn block_by_number(&self, block: u64) -> Result<Block, RpcError> { pub async fn block_by_number(&self, block: u64) -> Result<Option<Block>, RpcError> {
Self::block_internal(self.call("blockchain/block", &format!(r#"{{ "block": {block} }}"#))).await Self::block_internal(self.call("blockchain/block", &format!(r#"{{ "block": {block} }}"#))).await
} }

View File

@@ -49,8 +49,8 @@ async fn blockchain() {
let test_finalized_block = |number| { let test_finalized_block = |number| {
let serai = &serai; let serai = &serai;
async move { async move {
let block = serai.block_by_number(number).await.unwrap(); let block = serai.block_by_number(number).await.unwrap().unwrap();
assert_eq!(serai.block(block.header.hash()).await.unwrap(), block); assert_eq!(serai.block(block.header.hash()).await.unwrap().unwrap(), block);
assert!(serai.finalized(block.header.hash()).await.unwrap()); assert!(serai.finalized(block.header.hash()).await.unwrap());
} }
}; };
@@ -70,7 +70,7 @@ async fn blockchain() {
continue; continue;
}; };
// Check if it's considered finalized // Check if it's considered finalized
let considered_finalized = serai.finalized(block.header.hash()).await.unwrap(); let considered_finalized = serai.finalized(block.unwrap().header.hash()).await.unwrap();
// Ensure the finalized block is the same, meaning this block didn't become finalized as // Ensure the finalized block is the same, meaning this block didn't become finalized as
// we made these RPC requests // we made these RPC requests
if latest_finalized != serai.latest_finalized_block_number().await.unwrap() { if latest_finalized != serai.latest_finalized_block_number().await.unwrap() {
@@ -108,7 +108,7 @@ async fn blockchain() {
let mut observed_consensus_commitments = HashSet::new(); let mut observed_consensus_commitments = HashSet::new();
let mut tagged_block_hashes = vec![]; let mut tagged_block_hashes = vec![];
for i in 0 ..= last_block_number { for i in 0 ..= last_block_number {
let block = serai.block_by_number(i).await.unwrap(); let block = serai.block_by_number(i).await.unwrap().unwrap();
assert_eq!(block.header.number(), i); assert_eq!(block.header.number(), i);

View File

@@ -53,7 +53,7 @@ async fn validator_sets() {
// The genesis block should have the expected events // The genesis block should have the expected events
{ {
let mut events = serai let mut events = serai
.as_of(serai.block_by_number(0).await.unwrap().header.hash()) .as_of(serai.block_by_number(0).await.unwrap().unwrap().header.hash())
.await .await
.unwrap() .unwrap()
.validator_sets() .validator_sets()
@@ -98,7 +98,7 @@ async fn validator_sets() {
assert_eq!( assert_eq!(
serai serai
.as_of(serai.block_by_number(0).await.unwrap().header.hash()) .as_of(serai.block_by_number(0).await.unwrap().unwrap().header.hash())
.await .await
.unwrap() .unwrap()
.validator_sets() .validator_sets()
@@ -115,7 +115,7 @@ async fn validator_sets() {
{ {
assert_eq!( assert_eq!(
serai serai
.as_of(serai.block_by_number(1).await.unwrap().header.hash()) .as_of(serai.block_by_number(1).await.unwrap().unwrap().header.hash())
.await .await
.unwrap() .unwrap()
.validator_sets() .validator_sets()
@@ -126,7 +126,7 @@ async fn validator_sets() {
); );
assert_eq!( assert_eq!(
serai serai
.as_of(serai.block_by_number(1).await.unwrap().header.hash()) .as_of(serai.block_by_number(1).await.unwrap().unwrap().header.hash())
.await .await
.unwrap() .unwrap()
.validator_sets() .validator_sets()
@@ -138,8 +138,10 @@ async fn validator_sets() {
} }
{ {
let serai = let serai = serai
serai.as_of(serai.block_by_number(0).await.unwrap().header.hash()).await.unwrap(); .as_of(serai.block_by_number(0).await.unwrap().unwrap().header.hash())
.await
.unwrap();
let serai = serai.validator_sets(); let serai = serai.validator_sets();
for network in NetworkId::all() { for network in NetworkId::all() {
match network { match network {

View File

@@ -37,10 +37,12 @@ pub(crate) fn module<
module.register_method( module.register_method(
"blockchain/is_finalized", "blockchain/is_finalized",
|params, client, _ext| -> Result<_, Error> { |params, client, _ext| -> Result<_, Error> {
let block_hash = block_hash(&**client, &params)?; let Some(block_hash) = block_hash(&**client, &params)? else {
return Ok(false);
};
let finalized = client.info().finalized_number; let finalized = client.info().finalized_number;
let Ok(Some(number)) = client.number(block_hash) else { let Ok(Some(number)) = client.number(block_hash) else {
Err(Error::Missing("failed to fetch block's number"))? return Ok(false);
}; };
let Ok(status) = client.block_status(block_hash) else { let Ok(status) = client.block_status(block_hash) else {
Err(Error::Internal("failed to fetch block's status"))? Err(Error::Internal("failed to fetch block's status"))?
@@ -53,18 +55,22 @@ pub(crate) fn module<
)?; )?;
module.register_method("blockchain/block", |params, client, _ext| -> Result<_, Error> { module.register_method("blockchain/block", |params, client, _ext| -> Result<_, Error> {
let block_hash = block_hash(&**client, &params)?; let Some(block_hash) = block_hash(&**client, &params)? else {
return Ok(None);
};
let Ok(Some(block)) = client.block(block_hash) else { let Ok(Some(block)) = client.block(block_hash) else {
Err(Error::Missing("couldn't find requested block"))? return Ok(None);
}; };
Ok(hex::encode(borsh::to_vec(&serai_abi::Block::from(block.block)).unwrap())) Ok(Some(hex::encode(borsh::to_vec(&serai_abi::Block::from(block.block)).unwrap())))
})?; })?;
module.register_method("blockchain/events", |params, client, _ext| -> Result<_, Error> { module.register_method("blockchain/events", |params, client, _ext| -> Result<_, Error> {
let block_hash = block_hash(&**client, &params)?; let Some(block_hash) = block_hash(&**client, &params)? else {
Err(Error::InvalidStateReference)?
};
let Ok(events) = client.runtime_api().events(block_hash) else { let Ok(events) = client.runtime_api().events(block_hash) else {
Err(Error::Missing("couldn't fetch the events for the requested block"))? Err(Error::InvalidStateReference)?
}; };
Ok( Ok(
events events

View File

@@ -6,7 +6,7 @@ use serai_abi::{primitives::prelude::*, SubstrateBlock as Block};
pub(super) enum Error { pub(super) enum Error {
Internal(&'static str), Internal(&'static str),
InvalidRequest(&'static str), InvalidRequest(&'static str),
Missing(&'static str), InvalidStateReference,
} }
impl From<Error> for jsonrpsee::types::error::ErrorObjectOwned { impl From<Error> for jsonrpsee::types::error::ErrorObjectOwned {
@@ -18,9 +18,11 @@ impl From<Error> for jsonrpsee::types::error::ErrorObjectOwned {
Error::InvalidRequest(str) => { Error::InvalidRequest(str) => {
jsonrpsee::types::error::ErrorObjectOwned::owned(-2, str, Option::<()>::None) jsonrpsee::types::error::ErrorObjectOwned::owned(-2, str, Option::<()>::None)
} }
Error::Missing(str) => { Error::InvalidStateReference => jsonrpsee::types::error::ErrorObjectOwned::owned(
jsonrpsee::types::error::ErrorObjectOwned::owned(-3, str, Option::<()>::None) -4,
} "the block used as the reference was not locally held",
Option::<()>::None,
),
} }
} }
} }
@@ -30,7 +32,7 @@ pub(super) fn block_hash<
>( >(
client: &C, client: &C,
params: &jsonrpsee::types::params::Params, params: &jsonrpsee::types::params::Params,
) -> Result<<Block as sp_runtime::traits::Block>::Hash, Error> { ) -> Result<Option<<Block as sp_runtime::traits::Block>::Hash>, Error> {
#[derive(sp_core::serde::Deserialize)] #[derive(sp_core::serde::Deserialize)]
#[serde(crate = "sp_core::serde")] #[serde(crate = "sp_core::serde")]
struct BlockByHash { struct BlockByHash {
@@ -50,13 +52,13 @@ pub(super) fn block_hash<
}) else { }) else {
return Err(Error::InvalidRequest("requested block hash wasn't a valid hash")); return Err(Error::InvalidRequest("requested block hash wasn't a valid hash"));
}; };
block_hash Some(block_hash)
} else { } else {
let Ok(block_number) = params.parse::<BlockByNumber>() else { let Ok(block_number) = params.parse::<BlockByNumber>() else {
return Err(Error::InvalidRequest("requested block wasn't a valid hash nor number")); return Err(Error::InvalidRequest("requested block wasn't a valid hash nor number"));
}; };
let Ok(Some(block_hash)) = client.block_hash(block_number.block) else { let Ok(block_hash) = client.block_hash(block_number.block) else {
return Err(Error::Missing("no block hash for that block number")); return Err(Error::Internal("couldn't fetch block hash for block number"));
}; };
block_hash block_hash
}) })

View File

@@ -72,7 +72,9 @@ pub(crate) fn module<
module.register_method( module.register_method(
"validator-sets/current_session", "validator-sets/current_session",
|params, client, _ext| -> Result<_, Error> { |params, client, _ext| -> Result<_, Error> {
let block_hash = block_hash(&**client, &params)?; let Some(block_hash) = block_hash(&**client, &params)? else {
Err(Error::InvalidStateReference)?
};
let network = network(&params)?; let network = network(&params)?;
let Ok(session) = client.runtime_api().current_session(block_hash, network) else { let Ok(session) = client.runtime_api().current_session(block_hash, network) else {
Err(Error::Internal("couldn't fetch the session for the requested network"))? Err(Error::Internal("couldn't fetch the session for the requested network"))?
@@ -84,7 +86,9 @@ pub(crate) fn module<
module.register_method( module.register_method(
"validator-sets/current_stake", "validator-sets/current_stake",
|params, client, _ext| -> Result<_, Error> { |params, client, _ext| -> Result<_, Error> {
let block_hash = block_hash(&**client, &params)?; let Some(block_hash) = block_hash(&**client, &params)? else {
Err(Error::InvalidStateReference)?
};
let network = network(&params)?; let network = network(&params)?;
let Ok(stake) = client.runtime_api().current_stake(block_hash, network) else { let Ok(stake) = client.runtime_api().current_stake(block_hash, network) else {
Err(Error::Internal("couldn't fetch the total allocated stake for the requested network"))? Err(Error::Internal("couldn't fetch the total allocated stake for the requested network"))?
@@ -94,7 +98,9 @@ pub(crate) fn module<
); );
module.register_method("validator-sets/keys", |params, client, _ext| -> Result<_, Error> { module.register_method("validator-sets/keys", |params, client, _ext| -> Result<_, Error> {
let block_hash = block_hash(&**client, &params)?; let Some(block_hash) = block_hash(&**client, &params)? else {
Err(Error::InvalidStateReference)?
};
let set = set(&params)?; let set = set(&params)?;
let Ok(set) = ExternalValidatorSet::try_from(set) else { let Ok(set) = ExternalValidatorSet::try_from(set) else {
Err(Error::InvalidRequest("requested keys for a non-external validator set"))? Err(Error::InvalidRequest("requested keys for a non-external validator set"))?