Skip to content

Commit 09213ed

Browse files
committed
shim: add blob submit CLI tool
1 parent 435dc42 commit 09213ed

File tree

10 files changed

+250
-68
lines changed

10 files changed

+250
-68
lines changed

Cargo.lock

Lines changed: 1 addition & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sugondat-nmt/src/ns.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use crate::NS_ID_SIZE;
2+
use core::fmt;
23

4+
/// The namespace. A blob is submitted into a namespace. A namespace is a 4 byte vector.
5+
/// The convention is that the namespace id is a 4-byte little-endian integer.
36
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
47
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58
pub struct Namespace(u32);
@@ -10,6 +13,7 @@ impl Namespace {
1013
Self(namespace_id)
1114
}
1215

16+
/// Returns a namespace with the given namespace id.
1317
pub fn with_namespace_id(namespace_id: u32) -> Self {
1418
Self(namespace_id)
1519
}
@@ -33,3 +37,16 @@ impl Namespace {
3337
namespace_id
3438
}
3539
}
40+
41+
42+
impl fmt::Display for Namespace {
43+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44+
// Print the namespace as a 4-byte hex string. We don't use `hex` crate here to avoid
45+
// extra dependencies.
46+
write!(f, "0x")?;
47+
for byte in self.to_raw_bytes().iter() {
48+
write!(f, "{:02x}", byte)?;
49+
}
50+
Ok(())
51+
}
52+
}

sugondat-shim/Cargo.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ edition = "2021"
66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9+
sugondat-nmt = { path = "../sugondat-nmt", features = ["serde"] }
10+
sugondat-subxt = { path = "../sugondat-subxt" }
11+
sugondat-shim-common-sovereign = { path = "common/sovereign", features = ["server"] }
12+
913
anyhow = "1.0.75"
10-
clap = { version = "4.4.8", features = ["derive", "env", "wrap_help"] }
14+
clap = { version = "4.4.8", features = ["derive", "env"] }
1115
futures = "0.3.29"
1216
jsonrpsee = { version = "0.20.3", features = ["ws-client", "server"] }
1317
tracing = "0.1.40"
@@ -17,7 +21,4 @@ async-trait = "0.1.74"
1721
subxt = { version = "0.32.1" }
1822
subxt-signer = {version = "0.32.1", features = ["subxt"] }
1923
sha2 = "0.10.8"
20-
21-
sugondat-nmt = { path = "../sugondat-nmt", features = ["serde"] }
22-
sugondat-subxt = { path = "../sugondat-subxt" }
23-
sugondat-shim-common-sovereign = { path = "common/sovereign", features = ["server"] }
24+
hex = "0.4.3"

sugondat-shim/src/cli.rs

Lines changed: 100 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
1-
use crate::cmd::serve;
2-
use anyhow::bail;
31
use clap::{Parser, Subcommand};
4-
use tracing_subscriber::fmt;
5-
use tracing_subscriber::prelude::*;
2+
3+
// NOTE:
4+
//
5+
// The architecture of the CLI may seem contrived, but here are some reasons for it:
6+
//
7+
// - We want to push the parameters into the subcommands, instead of having them on the more general
8+
// structs. Specifially, we want to avoid
9+
//
10+
// sugondat-shim -p 10 serve --node-url=...
11+
//
12+
// because the user will have to remember where each flag must be (e.g. here -p before the
13+
// subcommand, but --node-url after the subcommand). Besides, it also looks clunky.
14+
//
15+
// - We want to have the CLI definition not to be scatered all over the codebase. Therefore it is
16+
// defined in a single file.
17+
//
18+
// - We use modules to group the CLI definitions for each subcommand, instead of prefixing and
19+
// dealing with lots of types like `ServeParams`, `QueryParams`, `QuerySubmitParams`, etc.
20+
//
21+
// This approach is more verbose, but it is also more explicit and easier to understand.
22+
// Verbosiness is OK here, because we reserve the entire file for the CLI definitions
23+
// anyway.
24+
//
25+
// When adding a new subcommand or parameter, try to follow the same patterns as the existing
26+
// ones. Ensure that the flags are consistent with the other subcommands, that the help
27+
// messages are present and clear, etc.
28+
29+
const ENV_SUGONDAT_SHIM_PORT: &str = "SUGONDAT_SHIM_PORT";
30+
const ENV_SUGONDAT_NAMESPACE: &str = "SUGONDAT_NAMESPACE";
31+
const ENV_SUGONDAT_NODE_URL: &str = "SUGONDAT_NODE_URL";
632

733
#[derive(Parser, Debug)]
834
#[command(author, version, about, long_about = None)]
9-
struct Cli {
35+
pub struct Cli {
1036
#[command(subcommand)]
11-
command: Commands,
37+
pub command: Commands,
1238
}
1339

14-
/// The environment variable used to override the default port to listen to when serving or to
15-
/// connect to when running RPCs.
16-
const SUGONDAT_SHIM_PORT_ENV: &str = "SUGONDAT_SHIM_PORT";
17-
1840
/// Common parameters for the adapter subcommands.
19-
///
20-
/// It's not declared on the `Cli` struct with `clap(flatten)` because of how the syntax
21-
/// `sugondat-shim -p 10 serve --node-url` looks unintuitive.
2241
#[derive(clap::Args, Debug)]
2342
pub struct AdapterServerParams {
2443
/// The address on which the shim should listen for incoming connections from the rollup nodes.
@@ -29,14 +48,22 @@ pub struct AdapterServerParams {
2948
#[clap(
3049
short,
3150
long,
32-
env = SUGONDAT_SHIM_PORT_ENV,
51+
env = ENV_SUGONDAT_SHIM_PORT,
3352
default_value = "10995",
3453
group = "listen"
3554
)]
3655
pub port: u16,
3756
// TODO: e.g. --submit-key, prometheus stuff, enabled adapters, etc.
3857
}
3958

59+
/// Common parameters for that commands that connect to the sugondat-node.
60+
#[derive(clap::Args, Debug)]
61+
pub struct SugondatRpcParams {
62+
/// The address of the sugondat-node to connect to.
63+
#[clap(long, default_value = "ws://localhost:9944", env = ENV_SUGONDAT_NODE_URL)]
64+
pub node_url: String,
65+
}
66+
4067
impl AdapterServerParams {
4168
/// Whether the sovereign adapter should be enabled.
4269
pub fn enable_sovereign(&self) -> bool {
@@ -45,30 +72,70 @@ impl AdapterServerParams {
4572
}
4673

4774
#[derive(Subcommand, Debug)]
48-
enum Commands {
75+
pub enum Commands {
76+
/// Connect to the sugondat node and serve requests from the rollup nodes.
4977
Serve(serve::Params),
78+
/// Serve requests from the rollup nodes by simulating the DA layer.
5079
Simulate,
80+
/// Allows running queries locally. Useful for debugging.
81+
Query(query::Params),
5182
}
5283

53-
pub async fn run() -> anyhow::Result<()> {
54-
init_logging()?;
55-
let cli = Cli::parse();
56-
match cli.command {
57-
Commands::Serve(params) => serve::run(params).await?,
58-
Commands::Simulate => {
59-
bail!("simulate subcommand not yet implemented")
60-
}
84+
pub mod serve {
85+
//! CLI definition for the `serve` subcommand.
86+
87+
use super::{AdapterServerParams, SugondatRpcParams};
88+
use clap::Args;
89+
90+
#[derive(Debug, Args)]
91+
pub struct Params {
92+
#[clap(flatten)]
93+
pub rpc: SugondatRpcParams,
94+
95+
#[clap(flatten)]
96+
pub adapter: AdapterServerParams,
6197
}
62-
Ok(())
6398
}
6499

65-
fn init_logging() -> anyhow::Result<()> {
66-
let filter = tracing_subscriber::EnvFilter::builder()
67-
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
68-
.from_env_lossy();
69-
tracing_subscriber::registry()
70-
.with(fmt::layer())
71-
.with(filter)
72-
.try_init()?;
73-
Ok(())
100+
pub mod query {
101+
//! CLI definition for the `query` subcommand.
102+
103+
use super::{SugondatRpcParams, ENV_SUGONDAT_NAMESPACE};
104+
use clap::{Args, Subcommand};
105+
106+
#[derive(Debug, Args)]
107+
pub struct Params {
108+
#[command(subcommand)]
109+
pub command: Commands,
110+
}
111+
112+
#[derive(Subcommand, Debug)]
113+
pub enum Commands {
114+
/// Submits the given blob into a namespace.
115+
Submit(submit::Params),
116+
}
117+
118+
pub mod submit {
119+
//! CLI definition for the `query submit` subcommand.
120+
121+
use super::{SugondatRpcParams, ENV_SUGONDAT_NAMESPACE};
122+
use clap::Args;
123+
124+
#[derive(Debug, Args)]
125+
pub struct Params {
126+
#[clap(flatten)]
127+
pub rpc: SugondatRpcParams,
128+
129+
/// The namespace to submit the blob into.
130+
///
131+
/// The namespace can be specified either as a 4-byte vector, or as an unsigned 32-bit
132+
/// integer. To distinguish between the two, the byte vector must be prefixed with
133+
/// `0x`.
134+
#[clap(long, short, env = ENV_SUGONDAT_NAMESPACE)]
135+
pub namespace: String,
136+
137+
/// The file path of the blob to submit. Pass `-` to read from stdin.
138+
pub blob_path: String,
139+
}
140+
}
74141
}

sugondat-shim/src/cmd/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,31 @@
1+
use crate::cli::{Cli, Commands};
2+
use clap::Parser;
3+
4+
pub mod query;
15
pub mod serve;
6+
7+
pub async fn dispatch() -> anyhow::Result<()> {
8+
init_logging()?;
9+
let cli = Cli::parse();
10+
match cli.command {
11+
Commands::Serve(params) => serve::run(params).await?,
12+
Commands::Simulate => {
13+
anyhow::bail!("simulate subcommand not yet implemented")
14+
}
15+
Commands::Query(params) => query::run(params).await?,
16+
}
17+
Ok(())
18+
}
19+
20+
fn init_logging() -> anyhow::Result<()> {
21+
use tracing_subscriber::fmt;
22+
use tracing_subscriber::prelude::*;
23+
let filter = tracing_subscriber::EnvFilter::builder()
24+
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
25+
.from_env_lossy();
26+
tracing_subscriber::registry()
27+
.with(fmt::layer())
28+
.with(filter)
29+
.try_init()?;
30+
Ok(())
31+
}

sugondat-shim/src/cmd/query/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use crate::{
2+
cli::query::{Commands, Params},
3+
sugondat_rpc,
4+
};
5+
6+
mod submit;
7+
8+
pub async fn run(params: Params) -> anyhow::Result<()> {
9+
match params.command {
10+
Commands::Submit(params) => submit::run(params).await?,
11+
}
12+
Ok(())
13+
}
14+
15+
async fn connect_rpc(
16+
conn_params: crate::cli::SugondatRpcParams,
17+
) -> anyhow::Result<sugondat_rpc::Client> {
18+
sugondat_rpc::Client::new(conn_params.node_url).await
19+
}

sugondat-shim/src/cmd/query/submit.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use anyhow::Context;
2+
3+
use super::connect_rpc;
4+
use crate::cli::query::submit::Params;
5+
6+
pub async fn run(params: Params) -> anyhow::Result<()> {
7+
let Params {
8+
blob_path,
9+
namespace,
10+
rpc,
11+
} = params;
12+
let blob = read_blob(&blob_path)
13+
.with_context(|| format!("cannot read blob file path '{}'", blob_path))?;
14+
let namespace = read_namespace(&namespace)?;
15+
let client = connect_rpc(rpc).await?;
16+
tracing::info!("submitting blob to namespace {}", namespace);
17+
let block_hash = client.submit_blob(blob, namespace).await?;
18+
tracing::info!("submitted blob to block hash 0x{}", hex::encode(block_hash));
19+
Ok(())
20+
}
21+
22+
/// Reads a blob from either a file or stdin.
23+
fn read_blob(path: &str) -> anyhow::Result<Vec<u8>> {
24+
use std::io::Read;
25+
let mut blob = Vec::new();
26+
if path == "-" {
27+
tracing::debug!("reading blob contents from stdin");
28+
std::io::stdin().read_to_end(&mut blob)?;
29+
} else {
30+
std::fs::File::open(path)?.read_to_end(&mut blob)?;
31+
}
32+
Ok(blob)
33+
}
34+
35+
/// Reads the namespace from a given namespace specifier.
36+
///
37+
/// The original namespace format is a 4-byte vector. so we support both the original format and
38+
/// a more human-readable format, which is an unsigned 32-bit integer. To distinguish between the
39+
/// two, the byte vector must be prefixed with `0x`.
40+
///
41+
/// The integer is interpreted as little-endian.
42+
fn read_namespace(namespace: &str) -> anyhow::Result<sugondat_nmt::Namespace> {
43+
if namespace.starts_with("0x") {
44+
let namespace = namespace.trim_start_matches("0x");
45+
let namespace = hex::decode(namespace)?;
46+
let namespace: [u8; 4] = namespace.try_into().map_err(|e: Vec<u8>| {
47+
anyhow::anyhow!("namespace must be 4 bytes long, but was {}", e.len())
48+
})?;
49+
Ok(sugondat_nmt::Namespace::from_raw_bytes(namespace))
50+
} else {
51+
let namespace_id = namespace
52+
.parse::<u32>()
53+
.with_context(|| format!("cannot parse namespace id '{}'", namespace))?;
54+
Ok(sugondat_nmt::Namespace::with_namespace_id(namespace_id))
55+
}
56+
}

0 commit comments

Comments
 (0)