diff --git a/Cargo.lock b/Cargo.lock index 512e69ca..0391747a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1469,7 +1469,6 @@ dependencies = [ "anstyle", "clap_lex", "strsim", - "terminal_size", ] [[package]] @@ -13470,6 +13469,7 @@ dependencies = [ "async-trait", "clap", "futures", + "hex", "jsonrpsee 0.20.3", "sha2 0.10.8", "subxt", @@ -13600,16 +13600,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "terminal_size" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" -dependencies = [ - "rustix 0.38.24", - "windows-sys 0.48.0", -] - [[package]] name = "termtree" version = "0.4.1" diff --git a/sugondat-nmt/src/ns.rs b/sugondat-nmt/src/ns.rs index 759c3f81..7e5eb680 100644 --- a/sugondat-nmt/src/ns.rs +++ b/sugondat-nmt/src/ns.rs @@ -1,5 +1,8 @@ use crate::NS_ID_SIZE; +use core::fmt; +/// The namespace. A blob is submitted into a namespace. A namespace is a 4 byte vector. +/// The convention is that the namespace id is a 4-byte little-endian integer. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Namespace(u32); @@ -10,6 +13,7 @@ impl Namespace { Self(namespace_id) } + /// Returns a namespace with the given namespace id. pub fn with_namespace_id(namespace_id: u32) -> Self { Self(namespace_id) } @@ -33,3 +37,16 @@ impl Namespace { namespace_id } } + + +impl fmt::Display for Namespace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Print the namespace as a 4-byte hex string. We don't use `hex` crate here to avoid + // extra dependencies. + write!(f, "0x")?; + for byte in self.to_raw_bytes().iter() { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} diff --git a/sugondat-shim/Cargo.toml b/sugondat-shim/Cargo.toml index a888de2f..d133806a 100644 --- a/sugondat-shim/Cargo.toml +++ b/sugondat-shim/Cargo.toml @@ -6,8 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +sugondat-nmt = { path = "../sugondat-nmt", features = ["serde"] } +sugondat-subxt = { path = "../sugondat-subxt" } +sugondat-shim-common-sovereign = { path = "common/sovereign", features = ["server"] } + anyhow = "1.0.75" -clap = { version = "4.4.8", features = ["derive", "env", "wrap_help"] } +clap = { version = "4.4.8", features = ["derive", "env"] } futures = "0.3.29" jsonrpsee = { version = "0.20.3", features = ["ws-client", "server"] } tracing = "0.1.40" @@ -17,7 +21,4 @@ async-trait = "0.1.74" subxt = { version = "0.32.1" } subxt-signer = {version = "0.32.1", features = ["subxt"] } sha2 = "0.10.8" - -sugondat-nmt = { path = "../sugondat-nmt", features = ["serde"] } -sugondat-subxt = { path = "../sugondat-subxt" } -sugondat-shim-common-sovereign = { path = "common/sovereign", features = ["server"] } +hex = "0.4.3" diff --git a/sugondat-shim/src/cli.rs b/sugondat-shim/src/cli.rs index 1dc7499f..ae775194 100644 --- a/sugondat-shim/src/cli.rs +++ b/sugondat-shim/src/cli.rs @@ -1,22 +1,45 @@ -use crate::serve; -use anyhow::bail; use clap::{Parser, Subcommand}; -use tracing_subscriber::fmt; -use tracing_subscriber::prelude::*; + +// NOTE: +// +// The architecture of the CLI may seem contrived, but here are some reasons for it: +// +// - We want to push the parameters into the subcommands, instead of having them on the more general +// structs. Specifially, we want to avoid +// +// sugondat-shim -p 10 serve --node-url=... +// +// because the user will have to remember where each flag must be (e.g. here -p before the +// subcommand, but --node-url after the subcommand). Besides, it also looks clunky. +// +// - We want to have the CLI definition not to be scatered all over the codebase. Therefore it is +// defined in a single file. +// +// - We use modules to group the CLI definitions for each subcommand, instead of prefixing and +// dealing with lots of types like `ServeParams`, `QueryParams`, `QuerySubmitParams`, etc. +// +// This approach is more verbose, but it is also more explicit and easier to understand. +// Verbosiness is OK here, because we reserve the entire file for the CLI definitions +// anyway. +// +// When adding a new subcommand or parameter, try to follow the same patterns as the existing +// ones. Ensure that the flags are consistent with the other subcommands, that the help +// messages are present and clear, etc. + +const ENV_SUGONDAT_SHIM_PORT: &str = "SUGONDAT_SHIM_PORT"; +const ENV_SUGONDAT_NAMESPACE: &str = "SUGONDAT_NAMESPACE"; +const ENV_SUGONDAT_NODE_URL: &str = "SUGONDAT_NODE_URL"; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] -struct Cli { +pub struct Cli { #[command(subcommand)] - command: Commands, + pub command: Commands, } -/// Common parameters for all subcommands. -/// -/// It's not declared on the `Cli` struct with `clap(flatten)` because of how the syntax -/// `sugondat-shim -p 10 serve --node-url` looks unintuitive. +/// Common parameters for the adapter subcommands. #[derive(clap::Args, Debug)] -pub struct CommonParams { +pub struct AdapterServerParams { /// The address on which the shim should listen for incoming connections from the rollup nodes. #[clap(short, long, default_value = "127.0.0.1", group = "listen")] pub address: String, @@ -25,7 +48,7 @@ pub struct CommonParams { #[clap( short, long, - env = "SUGONDAT_SHIM_PORT", + env = ENV_SUGONDAT_SHIM_PORT, default_value = "10995", group = "listen" )] @@ -33,7 +56,15 @@ pub struct CommonParams { // TODO: e.g. --submit-key, prometheus stuff, enabled adapters, etc. } -impl CommonParams { +/// Common parameters for that commands that connect to the sugondat-node. +#[derive(clap::Args, Debug)] +pub struct SugondatRpcParams { + /// The address of the sugondat-node to connect to. + #[clap(long, default_value = "ws://localhost:9944", env = ENV_SUGONDAT_NODE_URL)] + pub node_url: String, +} + +impl AdapterServerParams { /// Whether the sovereign adapter should be enabled. pub fn enable_sovereign(&self) -> bool { true @@ -41,30 +72,75 @@ impl CommonParams { } #[derive(Subcommand, Debug)] -enum Commands { +pub enum Commands { + /// Connect to the sugondat node and serve requests from the rollup nodes. Serve(serve::Params), + /// Serve requests from the rollup nodes by simulating the DA layer. Simulate, + /// Allows running queries locally. Useful for debugging. + Query(query::Params), } -pub async fn run() -> anyhow::Result<()> { - init_logging()?; - let cli = Cli::parse(); - match cli.command { - Commands::Serve(params) => serve::run(params).await?, - Commands::Simulate => { - bail!("simulate subcommand not yet implemented") - } +pub mod serve { + //! CLI definition for the `serve` subcommand. + + use super::{AdapterServerParams, SugondatRpcParams}; + use clap::Args; + + #[derive(Debug, Args)] + pub struct Params { + #[clap(flatten)] + pub rpc: SugondatRpcParams, + + #[clap(flatten)] + pub adapter: AdapterServerParams, } - Ok(()) } -fn init_logging() -> anyhow::Result<()> { - let filter = tracing_subscriber::EnvFilter::builder() - .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) - .from_env_lossy(); - tracing_subscriber::registry() - .with(fmt::layer()) - .with(filter) - .try_init()?; - Ok(()) +pub mod query { + //! CLI definition for the `query` subcommand. + + // TODO: I envision several subcommands here. For example: + // - query block — returns the information about a block and header. + // - query blob - returns the blob for a given key. The key here is the same sense as + // described here https://github.com/thrumdev/sugondat/issues/9#issuecomment-1814005570. + + use super::{SugondatRpcParams, ENV_SUGONDAT_NAMESPACE}; + use clap::{Args, Subcommand}; + + #[derive(Debug, Args)] + pub struct Params { + #[command(subcommand)] + pub command: Commands, + } + + #[derive(Subcommand, Debug)] + pub enum Commands { + /// Submits the given blob into a namespace. + Submit(submit::Params), + } + + pub mod submit { + //! CLI definition for the `query submit` subcommand. + + use super::{SugondatRpcParams, ENV_SUGONDAT_NAMESPACE}; + use clap::Args; + + #[derive(Debug, Args)] + pub struct Params { + #[clap(flatten)] + pub rpc: SugondatRpcParams, + + /// The namespace to submit the blob into. + /// + /// The namespace can be specified either as a 4-byte vector, or as an unsigned 32-bit + /// integer. To distinguish between the two, the byte vector must be prefixed with + /// `0x`. + #[clap(long, short, env = ENV_SUGONDAT_NAMESPACE)] + pub namespace: String, + + /// The file path of the blob to submit. Pass `-` to read from stdin. + pub blob_path: String, + } + } } diff --git a/sugondat-shim/src/cmd/mod.rs b/sugondat-shim/src/cmd/mod.rs new file mode 100644 index 00000000..61cad3bc --- /dev/null +++ b/sugondat-shim/src/cmd/mod.rs @@ -0,0 +1,31 @@ +use crate::cli::{Cli, Commands}; +use clap::Parser; + +pub mod query; +pub mod serve; + +pub async fn dispatch() -> anyhow::Result<()> { + init_logging()?; + let cli = Cli::parse(); + match cli.command { + Commands::Serve(params) => serve::run(params).await?, + Commands::Simulate => { + anyhow::bail!("simulate subcommand not yet implemented") + } + Commands::Query(params) => query::run(params).await?, + } + Ok(()) +} + +fn init_logging() -> anyhow::Result<()> { + use tracing_subscriber::fmt; + use tracing_subscriber::prelude::*; + let filter = tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) + .from_env_lossy(); + tracing_subscriber::registry() + .with(fmt::layer()) + .with(filter) + .try_init()?; + Ok(()) +} diff --git a/sugondat-shim/src/cmd/query/mod.rs b/sugondat-shim/src/cmd/query/mod.rs new file mode 100644 index 00000000..940d79a9 --- /dev/null +++ b/sugondat-shim/src/cmd/query/mod.rs @@ -0,0 +1,19 @@ +use crate::{ + cli::query::{Commands, Params}, + sugondat_rpc, +}; + +mod submit; + +pub async fn run(params: Params) -> anyhow::Result<()> { + match params.command { + Commands::Submit(params) => submit::run(params).await?, + } + Ok(()) +} + +async fn connect_rpc( + conn_params: crate::cli::SugondatRpcParams, +) -> anyhow::Result { + sugondat_rpc::Client::new(conn_params.node_url).await +} diff --git a/sugondat-shim/src/cmd/query/submit.rs b/sugondat-shim/src/cmd/query/submit.rs new file mode 100644 index 00000000..4a9d799f --- /dev/null +++ b/sugondat-shim/src/cmd/query/submit.rs @@ -0,0 +1,56 @@ +use anyhow::Context; + +use super::connect_rpc; +use crate::cli::query::submit::Params; + +pub async fn run(params: Params) -> anyhow::Result<()> { + let Params { + blob_path, + namespace, + rpc, + } = params; + let blob = read_blob(&blob_path) + .with_context(|| format!("cannot read blob file path '{}'", blob_path))?; + let namespace = read_namespace(&namespace)?; + let client = connect_rpc(rpc).await?; + tracing::info!("submitting blob to namespace {}", namespace); + let block_hash = client.submit_blob(blob, namespace).await?; + tracing::info!("submitted blob to block hash 0x{}", hex::encode(block_hash)); + Ok(()) +} + +/// Reads a blob from either a file or stdin. +fn read_blob(path: &str) -> anyhow::Result> { + use std::io::Read; + let mut blob = Vec::new(); + if path == "-" { + tracing::debug!("reading blob contents from stdin"); + std::io::stdin().read_to_end(&mut blob)?; + } else { + std::fs::File::open(path)?.read_to_end(&mut blob)?; + } + Ok(blob) +} + +/// Reads the namespace from a given namespace specifier. +/// +/// The original namespace format is a 4-byte vector. so we support both the original format and +/// a more human-readable format, which is an unsigned 32-bit integer. To distinguish between the +/// two, the byte vector must be prefixed with `0x`. +/// +/// The integer is interpreted as little-endian. +fn read_namespace(namespace: &str) -> anyhow::Result { + if namespace.starts_with("0x") { + let namespace = namespace.trim_start_matches("0x"); + let namespace = hex::decode(namespace)?; + let namespace: [u8; 4] = namespace.try_into().map_err(|e: Vec| { + anyhow::anyhow!("namespace must be 4 bytes long, but was {}", e.len()) + })?; + Ok(sugondat_nmt::Namespace::from_raw_bytes(namespace)) + } else { + let namespace_id = namespace + .parse::() + .with_context(|| format!("cannot parse namespace id '{}'", namespace))?; + Ok(sugondat_nmt::Namespace::with_namespace_id(namespace_id)) + } +} diff --git a/sugondat-shim/src/serve.rs b/sugondat-shim/src/cmd/serve.rs similarity index 50% rename from sugondat-shim/src/serve.rs rename to sugondat-shim/src/cmd/serve.rs index 01cbd5b3..74ceb1a6 100644 --- a/sugondat-shim/src/serve.rs +++ b/sugondat-shim/src/cmd/serve.rs @@ -1,28 +1,22 @@ -use crate::{adapters::sovereign::SovereignAdapter, cli::CommonParams, sugondat_rpc::Client}; -use clap::Args; +use crate::{ + adapters::sovereign::SovereignAdapter, + cli::{serve::Params, AdapterServerParams}, + sugondat_rpc::Client, +}; + use jsonrpsee::{server::Server, Methods}; use sugondat_shim_common_sovereign::SovereignRPCServer; use tracing::{debug, info}; -#[derive(Debug, Args)] -pub struct Params { - /// The address of the sugondat-node to connect to. - #[clap(long, default_value = "ws://localhost:9944")] - node_url: String, - - #[clap(flatten)] - common: CommonParams, -} - pub async fn run(params: Params) -> anyhow::Result<()> { info!( "starting sugondat-shim server on {}:{}", - params.common.address, params.common.port + params.adapter.address, params.adapter.port ); - let listen_on = (params.common.address.as_str(), params.common.port); + let listen_on = (params.adapter.address.as_str(), params.adapter.port); let server = Server::builder().build(listen_on).await?; - let client = connect_client(¶ms.node_url).await?; - let handle = server.start(init_adapters(client, ¶ms.common)); + let client = connect_client(¶ms.rpc.node_url).await?; + let handle = server.start(init_adapters(client, ¶ms.adapter)); handle.stopped().await; Ok(()) } @@ -32,9 +26,9 @@ async fn connect_client(url: &str) -> anyhow::Result { Ok(client) } -fn init_adapters(client: Client, common: &CommonParams) -> Methods { +fn init_adapters(client: Client, adapter: &AdapterServerParams) -> Methods { let mut methods = Methods::new(); - if common.enable_sovereign() { + if adapter.enable_sovereign() { debug!("enabling sovereign adapter"); let adapter = SovereignAdapter::new(client.clone()); methods.merge(adapter.into_rpc()).unwrap(); diff --git a/sugondat-shim/src/main.rs b/sugondat-shim/src/main.rs index e38fba30..6adfaef0 100644 --- a/sugondat-shim/src/main.rs +++ b/sugondat-shim/src/main.rs @@ -1,9 +1,9 @@ mod adapters; mod cli; -mod serve; +mod cmd; mod sugondat_rpc; #[tokio::main] async fn main() -> anyhow::Result<()> { - cli::run().await + cmd::dispatch().await } diff --git a/sugondat-shim/src/sugondat_rpc.rs b/sugondat-shim/src/sugondat_rpc.rs index bacdb927..712fee66 100644 --- a/sugondat-shim/src/sugondat_rpc.rs +++ b/sugondat-shim/src/sugondat_rpc.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use subxt::{backend::rpc::RpcClient, rpc_params, utils::H256, OnlineClient}; use sugondat_nmt::Namespace; use sugondat_subxt::{ @@ -78,11 +79,13 @@ impl Client { }) } + /// Submit a blob with the given namespace. Returns a block hash in which the extrinsic was + /// included. pub async fn submit_blob( &self, blob: Vec, namespace: sugondat_nmt::Namespace, - ) -> anyhow::Result<()> { + ) -> anyhow::Result<[u8; 32]> { use subxt_signer::sr25519::dev; let namespace_id = namespace.namespace_id(); @@ -91,14 +94,21 @@ impl Client { .submit_blob(namespace_id, BoundedVec(blob)); let from = dev::alice(); - let _events = self + let signed = self .subxt .tx() - .sign_and_submit_then_watch_default(&extrinsic, &from) - .await? + .create_signed(&extrinsic, &from, Default::default()) + .await + .with_context(|| format!("failed to validate or sign extrinsic with dev key pair"))?; + + let events = signed + .submit_and_watch() + .await + .with_context(|| format!("failed to submit extrinsic"))? .wait_for_finalized_success() .await?; - Ok(()) + let block_hash = events.block_hash(); + Ok(block_hash.0) } }