Skip to content

Commit

Permalink
Add sign and submit subcommands to sequencer CLI (#1696)
Browse files Browse the repository at this point in the history
## Summary

> [!NOTE]  
> Most of the code is copied from Sam
(#1695) and Jordan's
(#1694) PRs.

Add two new subcommands `sign` and `submit` to the `sequencer`
subcommand.

### `sign`
1. Reads a pbjson formatted
`astria.protocol.transaction.v1.TransactionBody` from a file or `STDIN`
(`file` is a positional argument; `STDIN` is read when providing `-` as
the trailing argument).
2. Signs it with a given private key (`--private-key`).
3. Writes the pbjson formatted
`astria.protocol.transaction.v1.Transaction` to `--output`/`-o`, if
provided, or `STDOUT`.

### `submit`
1. 1. Reads a pbjson formatted
`astria.protocol.transaction.v1.Transaction` from a file or `STDIN`
(`file` is a positional argument; `STDIN` is read when providing `-` as
the trailing argument).
2. Submits it to a sequencer's CometBFT url (`--sequencer-url`)

## Background

We want to be able to test the submitting txs signed via FROST threshold
signing (see #1654) but do not
have a CLI command to submit already signed transactions. The `submit`
command resolves this.

To test the `submit` command it is desirable to have a corresponding
`sign` command which creates a signed `Transaction` from a single
private key.

## Changes
- List changes which were made.

## Testing
1. Run a local sequencer network using `astria-cli-go`
```
just run dev purge all
just run dev init
just run dev run --network local
```
2. Sign a `TransactionBody`:
```
cargo run -p astria-cli -- sequencer sign --private-key 2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90 - <<EOF
{
  "params": {
    "nonce": 0,
    "chainId": "sequencer-test-chain-0"
  },
  "actions": [
    {
      "ibcRelayerChange": {
        "removal": {
          "bech32m": "astria13r24h8mj42sdfqflqyg2fycqf9mdqqzmm2xllj"
        }
      }
    }
  ]
}
EOF
```
3. Submit the signed `Transaction`
```
cargo run -p astria-cli -- sequencer submit --sequencer-url http://127.0.0.1:26657 - <<EOF
{
  "signature": "+hb4bd8kEM8/AQ3wJ2znXcF3Ds1iLZu6OieNOnxY7n1SZsiDr5NQP3lMK4s5134O629XjXhae/FsL+qtbXnBDw==",
  "publicKey": "1b9KP8znF7A4i8wnSevBSK2ZabI/Re4bYF/Vh3hXasQ=",
  "body": {
    "typeUrl": "/astria.protocol.transaction.v1.TransactionBody",
    "value": "ChgSFnNlcXVlbmNlci10ZXN0LWNoYWluLTASNKIDMRIvEi1hc3RyaWExM3IyNGg4bWo0MnNkZnFmbHF5ZzJmeWNxZjltZHFxem1tMnhsbGo="
  }
}
EOF 
```

> [!NOTE]  
> You can also do this is in a single command using `xargs -0`

```
cargo run -p astria-cli -- sequencer sign --private-key 2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90 - <<EOF
{
  "params": {
    "nonce": 3,
    "chainId": "sequencer-test-chain-0"
  },
  "actions": [
    {
      "ibcRelayerChange": {
        "removal": {
          "bech32m": "astria13r24h8mj42sdfqflqyg2fycqf9mdqqzmm2xllj"
        }
      }
    }
  ]
}
EOF | xargs -0 cargo run -p astria-cli -- sequencer submit  --sequencer-url http://127.0.0.1:26657
```

## Metrics
- List out metrics added by PR, delete section if none. 

## Breaking Changelist
- Bulleted list of breaking changes, any notes on migration. Delete
section if none.

## Related Issues
Link any issues that are related, prefer full github links.

closes <!-- list any issues closed here -->

---------

Co-authored-by: Sam Bukowski <[email protected]>
Co-authored-by: Richard Janis Goldschmidt <[email protected]>
Co-authored-by: Fraser Hutchison <[email protected]>
Co-authored-by: Jordan Oroshiba <[email protected]>
  • Loading branch information
5 people authored Oct 23, 2024
1 parent 3e16986 commit 40e86f0
Show file tree
Hide file tree
Showing 18 changed files with 382 additions and 92 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

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

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use astria_core::{
memos,
transaction::v1::Action,
},
Protobuf as _,
};
use astria_eyre::eyre::{
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::time::Duration;
use astria_core::{
primitive::v1::asset,
protocol::bridge::v1::BridgeAccountLastTxHashResponse,
Protobuf as _,
};
use prost::Message as _;
use sequencer_client::{
Expand Down
4 changes: 4 additions & 0 deletions crates/astria-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ readme = "README.md"
repository = "https://github.com/astriaorg/astria"
homepage = "https://astria.org"

[[bin]]
name = "astria-cli"

[dependencies]
color-eyre = "0.6"
clap-stdin = "0.5.1"
# v2.0.0-rc.0 - can be updated once https://github.com/ZcashFoundation/frost/issues/755 is closed
frost-ed25519 = { version = "2.0.0-rc.0", features = [] }
serde_yaml = "0.9.25"
Expand Down
13 changes: 13 additions & 0 deletions crates/astria-cli/src/sequencer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ mod block_height;
mod bridge_lock;
mod ics20_withdrawal;
mod init_bridge_account;
mod sign;
mod submit;
mod sudo;
mod threshold;
mod transfer;
Expand All @@ -31,6 +33,8 @@ impl Command {
SubCommand::Transfer(transfer) => transfer.run().await,
SubCommand::Threshold(threshold) => threshold.run().await,
SubCommand::Ics20Withdrawal(ics20_withdrawal) => ics20_withdrawal.run().await,
SubCommand::Submit(submit) => submit.run().await,
SubCommand::Sign(sign) => sign.run(),
}
}
}
Expand Down Expand Up @@ -59,4 +63,13 @@ enum SubCommand {
Threshold(threshold::Command),
/// Command for withdrawing an ICS20 asset
Ics20Withdrawal(ics20_withdrawal::Command),
/// Submit the signed pbjson formatted Transaction.
Submit(submit::Command),
/// Sign a pbjson formatted TransactionBody to produce a Transaction.
#[expect(
clippy::doc_markdown,
reason = "doc comments are turned into CLI help strings which currently don't use \
backticks"
)]
Sign(sign::Command),
}
99 changes: 99 additions & 0 deletions crates/astria-cli/src/sequencer/sign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use std::{
io::Write,
path::{
Path,
PathBuf,
},
};

use astria_core::{
protocol::transaction::v1::TransactionBody,
Protobuf,
};
use clap_stdin::FileOrStdin;
use color_eyre::eyre::{
self,
WrapErr as _,
};

use crate::utils::signing_key_from_private_key;

#[derive(clap::Args, Debug)]
pub(super) struct Command {
/// The private key of account being sent from
#[arg(long, env = "SEQUENCER_PRIVATE_KEY")]
// TODO: https://github.com/astriaorg/astria/issues/594
// Don't use a plain text private, prefer wrapper like from
// the secrecy crate with specialized `Debug` and `Drop` implementations
// that overwrite the key on drop and don't reveal it when printing.
private_key: String,
/// Target to write the signed transaction in pbjson format (omit to write to STDOUT).
#[arg(long, short)]
output: Option<PathBuf>,
/// Forces an overwrite of `--output` if a file at that location exists.
#[arg(long, short)]
force: bool,
/// The source to read the pbjson formatted astra.protocol.transaction.v1.Transaction (use `-`
/// to pass via STDIN).
input: FileOrStdin,
}

// The goal of the `sign` CLI command is to take in a `TransactionBody` and to sign with a private
// key to create a `Transaction`. This signed `Transaction` should be printed to the console in
// pbjson format.
impl Command {
pub(super) fn run(self) -> eyre::Result<()> {
let key = signing_key_from_private_key(self.private_key.as_str())?;

let filename = self.input.filename().to_string();
let transaction_body = read_transaction_body(self.input)
.wrap_err_with(|| format!("failed to read transaction body from `{filename}`"))?;
let transaction = transaction_body.sign(&key);

serde_json::to_writer(
stdout_or_file(self.output.as_ref(), self.force)
.wrap_err("failed to determine output target")?,
&transaction.to_raw(),
)
.wrap_err("failed to write signed transaction")?;
Ok(())
}
}

fn read_transaction_body(input: FileOrStdin) -> eyre::Result<TransactionBody> {
let wire_body: <TransactionBody as Protobuf>::Raw = serde_json::from_reader(
std::io::BufReader::new(input.into_reader()?),
)
.wrap_err_with(|| {
format!(
"failed to parse input as json `{}`",
TransactionBody::full_name()
)
})?;
TransactionBody::try_from_raw(wire_body).wrap_err("failed to validate transaction body")
}

fn stdout_or_file<P: AsRef<Path>>(
output: Option<P>,
force_overwrite: bool,
) -> eyre::Result<Box<dyn Write>> {
let writer = match output {
Some(path) => {
let file = if force_overwrite {
std::fs::File::options()
.write(true)
.truncate(true)
.open(path)
} else {
std::fs::File::options()
.create_new(true)
.write(true)
.open(path)
}
.wrap_err("failed to open file for writing")?;
Box::new(file) as Box<dyn Write>
}
None => Box::new(std::io::stdout()),
};
Ok(writer)
}
73 changes: 73 additions & 0 deletions crates/astria-cli/src/sequencer/submit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use astria_core::{
self,
protocol::transaction::v1::Transaction,
Protobuf,
};
use astria_sequencer_client::{
HttpClient,
SequencerClientExt as _,
};
use clap_stdin::FileOrStdin;
use color_eyre::eyre::{
self,
ensure,
WrapErr as _,
};

#[derive(clap::Args, Debug)]
pub(super) struct Command {
/// The URL at which the Sequencer node is listening for ABCI commands.
#[arg(
long,
env = "SEQUENCER_URL",
default_value = crate::DEFAULT_SEQUENCER_RPC
)]
sequencer_url: String,
/// The source to read the pbjson formatted astra.protocol.transaction.v1.Transaction (use `-`
/// to pass via STDIN).
input: FileOrStdin,
}

// The 'submit' command takes a 'Transaction' in pbjson form and submits it to the sequencer
impl Command {
pub(super) async fn run(self) -> eyre::Result<()> {
let sequencer_client = HttpClient::new(self.sequencer_url.as_str())
.wrap_err("failed constructing http sequencer client")?;

let filename = self.input.filename().to_string();
let transaction = read_transaction(self.input)
.wrap_err_with(|| format!("to signed transaction from `{filename}`"))?;

let res = sequencer_client
.submit_transaction_sync(transaction)
.await
.wrap_err("failed to submit transaction")?;

ensure!(res.code.is_ok(), "failed to check tx: {}", res.log);

let tx_response = sequencer_client.wait_for_tx_inclusion(res.hash).await;

ensure!(
tx_response.tx_result.code.is_ok(),
"failed to execute tx: {}",
tx_response.tx_result.log
);

println!("Submission completed!");
println!("Included in block: {}", tx_response.height);
Ok(())
}
}

fn read_transaction(input: FileOrStdin) -> eyre::Result<Transaction> {
let wire_body: <Transaction as Protobuf>::Raw = serde_json::from_reader(
std::io::BufReader::new(input.into_reader()?),
)
.wrap_err_with(|| {
format!(
"failed to parse input as json `{}`",
Transaction::full_name()
)
})?;
Transaction::try_from_raw(wire_body).wrap_err("failed to validate transaction body")
}
1 change: 1 addition & 0 deletions crates/astria-composer/src/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use astria_core::{
Transaction,
},
},
Protobuf as _,
};
use astria_eyre::eyre::{
self,
Expand Down
1 change: 1 addition & 0 deletions crates/astria-composer/tests/blackbox/helper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use astria_core::{
abci::AbciErrorCode,
transaction::v1::Transaction,
},
Protobuf as _,
};
use astria_eyre::eyre;
use ethers::prelude::Transaction as EthersTransaction;
Expand Down
1 change: 1 addition & 0 deletions crates/astria-core/src/protocol/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{
block::Deposit,
SequencerBlock,
},
Protobuf as _,
};

#[derive(Default)]
Expand Down
Loading

0 comments on commit 40e86f0

Please sign in to comment.