Skip to content

Commit bb197bf

Browse files
authored
fault_proving(global_roots): GraphQL API for serving state roots (#2742)
1 parent 8661fbf commit bb197bf

File tree

12 files changed

+647
-4
lines changed

12 files changed

+647
-4
lines changed

.changes/added/2742.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added API crate for merkle root service.

Cargo.lock

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ members = [
1414
"crates/fuel-gas-price-algorithm",
1515
"crates/keygen",
1616
"crates/metrics",
17+
"crates/proof_system/global_merkle_root/api",
1718
"crates/proof_system/global_merkle_root/service",
1819
"crates/proof_system/global_merkle_root/storage",
1920
"crates/services",
@@ -106,6 +107,10 @@ fuel-vm-private = { version = "0.59.2", package = "fuel-vm", default-features =
106107

107108
# Common dependencies
108109
anyhow = "1.0"
110+
async-graphql = { version = "7.0.11", features = [
111+
"graphiql",
112+
"tracing",
113+
], default-features = false }
109114
async-trait = "0.1"
110115
aws-sdk-kms = "1.37"
111116
cynic = { version = "3.1.0", features = ["http-reqwest"] }

crates/fuel-core/Cargo.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@ version = { workspace = true }
1212

1313
[dependencies]
1414
anyhow = { workspace = true }
15-
async-graphql = { version = "7.0.11", features = [
16-
"graphiql",
17-
"tracing",
18-
], default-features = false }
15+
async-graphql = { workspace = true }
1916
async-graphql-value = "7.0.11"
2017
async-trait = { workspace = true }
2118
axum = { workspace = true }
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[package]
2+
authors = { workspace = true }
3+
categories = ["cryptography::cryptocurrencies"]
4+
description = "GraphQL API for the global merkle root service."
5+
edition = { workspace = true }
6+
homepage = { workspace = true }
7+
keywords = ["blockchain", "cryptocurrencies", "fuel-client", "fuel-core"]
8+
license = { workspace = true }
9+
name = "fuel-core-global-merkle-root-api"
10+
repository = { workspace = true }
11+
version = { workspace = true }
12+
13+
[dependencies]
14+
anyhow = { workspace = true }
15+
async-graphql = { workspace = true }
16+
async-trait = { workspace = true }
17+
axum = { workspace = true }
18+
derive_more = { workspace = true }
19+
fuel-core-services = { workspace = true }
20+
fuel-core-storage = { workspace = true, features = ["alloc"] }
21+
fuel-core-types = { workspace = true, default-features = false, features = [
22+
"serde",
23+
"alloc",
24+
] }
25+
hex = { workspace = true }
26+
hyper = { workspace = true }
27+
tokio = { workspace = true }
28+
tracing = { workspace = true }
29+
30+
[dev-dependencies]
31+
fuel-core-storage = { workspace = true, features = ["std", "test-helpers"] }
32+
fuel-core-types = { workspace = true, default-features = false, features = [
33+
"serde",
34+
"random",
35+
"test-helpers",
36+
] }
37+
reqwest = { workspace = true }
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//! The GraphQL API of the state root service
2+
3+
#![deny(clippy::arithmetic_side_effects)]
4+
#![deny(clippy::cast_possible_truncation)]
5+
#![deny(unused_crate_dependencies)]
6+
#![deny(missing_docs)]
7+
#![deny(warnings)]
8+
9+
/// Port definitions
10+
pub mod ports;
11+
12+
/// API service definition
13+
pub mod service;
14+
15+
/// GraphQL schema definition
16+
pub mod schema;
17+
18+
#[cfg(test)]
19+
pub mod tests;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use fuel_core_storage::Error as StorageError;
2+
use fuel_core_types::{
3+
fuel_tx::Bytes32,
4+
fuel_types::BlockHeight,
5+
};
6+
7+
/// Represents the ability to retrieve a state root at a given block height
8+
pub trait GetStateRoot {
9+
/// Get the state root at the given height
10+
fn state_root_at(&self, height: BlockHeight)
11+
-> Result<Option<Bytes32>, StorageError>;
12+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use std::{
2+
array::TryFromSliceError,
3+
fmt::Display,
4+
str::FromStr,
5+
};
6+
7+
use async_graphql::{
8+
EmptyMutation,
9+
EmptySubscription,
10+
InputValueError,
11+
ScalarType,
12+
};
13+
use hex::FromHexError;
14+
15+
use crate::ports;
16+
17+
/// The schema of the state root service.
18+
pub type Schema<Storage> =
19+
async_graphql::Schema<Query<Storage>, EmptyMutation, EmptySubscription>;
20+
21+
/// The query type of the schema.
22+
pub struct Query<Storage> {
23+
storage: Storage,
24+
}
25+
26+
impl<Storage> Query<Storage> {
27+
/// Create a new query object.
28+
pub fn new(storage: Storage) -> Self {
29+
Self { storage }
30+
}
31+
}
32+
33+
#[async_graphql::Object]
34+
impl<Storage> Query<Storage>
35+
where
36+
Storage: ports::GetStateRoot + Send + Sync,
37+
{
38+
async fn state_root(
39+
&self,
40+
height: BlockHeight,
41+
) -> async_graphql::Result<Option<MerkleRoot>> {
42+
let state_root = self.storage.state_root_at(height.0.into())?;
43+
44+
Ok(state_root.map(|root| MerkleRoot(*root)))
45+
}
46+
}
47+
48+
/// GraphQL scalar type for block height.
49+
#[derive(
50+
Clone,
51+
Copy,
52+
Debug,
53+
PartialEq,
54+
Eq,
55+
PartialOrd,
56+
Ord,
57+
derive_more::FromStr,
58+
derive_more::Display,
59+
)]
60+
pub struct BlockHeight(u32);
61+
62+
/// GraphQL scalar type for the merkle root.
63+
#[derive(Clone, Copy, Debug)]
64+
pub struct MerkleRoot([u8; 32]);
65+
66+
impl Display for MerkleRoot {
67+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68+
write!(f, "0x{}", hex::encode(self.0))
69+
}
70+
}
71+
72+
impl FromStr for MerkleRoot {
73+
type Err = MerkleRootParseError;
74+
75+
fn from_str(s: &str) -> Result<Self, Self::Err> {
76+
let bytes = hex::decode(s.trim_start_matches("0x"))?;
77+
Ok(MerkleRoot(bytes.as_slice().try_into()?))
78+
}
79+
}
80+
81+
/// Error type for the merkle root parsing.
82+
#[derive(Clone, Debug, derive_more::Display, derive_more::From, derive_more::Error)]
83+
pub enum MerkleRootParseError {
84+
/// The merkle root isn't valid hex.
85+
DecodeFailure(FromHexError),
86+
/// The merkle root has the wrong number of bytes.
87+
WrongLength(TryFromSliceError),
88+
}
89+
90+
macro_rules! impl_scalar_type {
91+
($type:ty) => {
92+
#[async_graphql::Scalar]
93+
impl ScalarType for $type {
94+
fn parse(
95+
value: async_graphql::Value,
96+
) -> async_graphql::InputValueResult<Self> {
97+
let async_graphql::Value::String(text) = value else {
98+
return Err(InputValueError::expected_type(value))
99+
};
100+
101+
text.parse().map_err(InputValueError::custom)
102+
}
103+
104+
fn to_value(&self) -> async_graphql::Value {
105+
async_graphql::Value::String(self.to_string())
106+
}
107+
}
108+
};
109+
}
110+
111+
impl_scalar_type!(MerkleRoot);
112+
impl_scalar_type!(BlockHeight);

0 commit comments

Comments
 (0)