Skip to content

shim: query CLI #34

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions sugondat-nmt/src/ns.rs
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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)
}
Expand All @@ -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(())
}
}
11 changes: 6 additions & 5 deletions sugondat-shim/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
140 changes: 108 additions & 32 deletions sugondat-shim/src/cli.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,46 +48,99 @@ pub struct CommonParams {
#[clap(
short,
long,
env = "SUGONDAT_SHIM_PORT",
env = ENV_SUGONDAT_SHIM_PORT,
default_value = "10995",
group = "listen"
)]
pub port: u16,
// 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
}
}

#[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 <block_hash/number> — returns the information about a block and header.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are query parameters for the DA layer block / number, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep! generally shim doesn't know anything about rollup blocks. Only DA blocks and blobs.

// - query blob <id> - 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,
}
}
}
31 changes: 31 additions & 0 deletions sugondat-shim/src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
19 changes: 19 additions & 0 deletions sugondat-shim/src/cmd/query/mod.rs
Original file line number Diff line number Diff line change
@@ -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> {
sugondat_rpc::Client::new(conn_params.node_url).await
}
56 changes: 56 additions & 0 deletions sugondat-shim/src/cmd/query/submit.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>> {
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<sugondat_nmt::Namespace> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another possible implementation would be:

fn read_namespace(namespace: &str) -> anyhow::Result<sugondat_nmt::Namespace> {

    if let Some(hex) = namespace.strip_prefix("0x") {
        let namespace = hex::decode(hex)?;
        let namespace: [u8; 4] = namespace.try_into().map_err(|e: Vec<u8>| {
            anyhow::anyhow!("namespace must be 4 bytes long, but was {}", e.len())
        })?;
        Ok(sugondat_nmt::Namespace::from_raw_bytes(namespace))
    }

    let namespace_id = namespace
        .parse::<u32>()
        .with_context(|| format!("cannot parse namespace id '{}'", namespace))?;
    Ok(sugondat_nmt::Namespace::with_namespace_id(namespace_id))
}

or cleaner but technically more computation required because the from_str_radix would perform an useless translation to u32

fn read_namespace(mut namespace: &str) -> anyhow::Result<sugondat_nmt::Namespace> {

    let namespace_id = match namespace.strip_prefix("0x") {
        Some(hex) => u32::from_str_radix(hex, 16).with_context(|| format!("cannot parse namespace id '{}'", namespace))?,
        None => namespace.parse::<u32>().with_context(|| format!("cannot parse namespace id '{}'", namespace))?
    };

    Ok(sugondat_nmt::Namespace::with_namespace_id(namespace_id))
}

(didn't test the error handling)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, this is a funny one.

I specifically went for the implementation that you see here to disallow passing 0x0x0011, because those functions are not equivalent: one removes the occurance once and the other removes all occurances. However, I mixed them up and did the opposite what I inteded.

That said, I don't think this matters much and it's OK either way. Feel free to file a PR with the fix. I actually prefer the first option: for one, it's not unlikely that we change the size of the namespace #12 . Note that you it seems you are missing a return though.

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<u8>| {
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::<u32>()
.with_context(|| format!("cannot parse namespace id '{}'", namespace))?;
Ok(sugondat_nmt::Namespace::with_namespace_id(namespace_id))
}
}
Loading