Chore/stacks signer cli cleanup (#3885)

* Cleanup documentation and CLI using clap and errors using thiserror

Signed-off-by: Jacinta Ferrant <jacinta@trustmachines.co>

* Do not enable host, contract, or private key to be specified if using the config option

Signed-off-by: Jacinta Ferrant <jacinta@trustmachines.co>

* Fix typo in comment

Signed-off-by: Jacinta Ferrant <jacinta@trustmachines.co>

---------

Signed-off-by: Jacinta Ferrant <jacinta@trustmachines.co>
This commit is contained in:
jferrant
2023-08-31 10:39:21 -04:00
committed by GitHub
parent 8a3ed3a3ba
commit b24391412e
5 changed files with 491 additions and 307 deletions

213
Cargo.lock generated
View File

@@ -101,6 +101,54 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
[[package]]
name = "anstyle-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "anstyle-wincon"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
dependencies = [
"anstyle",
"windows-sys 0.48.0",
]
[[package]]
name = "anyhow"
version = "1.0.69"
@@ -460,6 +508,47 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "clap"
version = "4.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c8d502cbaec4595d2e7d5f61e318f05417bd2b66fdc3809498f0d3fdf0bea27"
dependencies = [
"clap_builder",
"clap_derive",
"once_cell",
]
[[package]]
name = "clap_builder"
version = "4.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5891c7bc0edb3e1c2204fc5e94009affabeb1821c9e5fdc3959536c5c0bb984d"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.12",
]
[[package]]
name = "clap_lex"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
[[package]]
name = "clarity"
version = "0.0.1"
@@ -502,6 +591,12 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "concurrent-queue"
version = "2.1.0"
@@ -572,7 +667,7 @@ checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f"
dependencies = [
"atty",
"cast",
"clap",
"clap 2.34.0",
"criterion-plot",
"csv",
"itertools",
@@ -1173,6 +1268,12 @@ dependencies = [
"http",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.1.19"
@@ -2717,6 +2818,7 @@ dependencies = [
name = "stacks-signer"
version = "0.0.1"
dependencies = [
"clap 4.4.1",
"clarity",
"libsigner",
"libstackerdb",
@@ -2730,6 +2832,7 @@ dependencies = [
"slog-json",
"slog-term",
"stacks-common",
"thiserror",
"toml",
]
@@ -2842,6 +2945,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "stx-genesis"
version = "0.1.0"
@@ -3248,6 +3357,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "value-bag"
version = "1.0.0-alpha.9"
@@ -3490,13 +3605,13 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
@@ -3505,7 +3620,16 @@ version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
@@ -3514,13 +3638,28 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
@@ -3529,42 +3668,84 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winreg"
version = "0.10.1"

View File

@@ -16,17 +16,19 @@ name = "stacks-signer"
path = "src/main.rs"
[dependencies]
clarity = { path = "../clarity" }
clap = { version = "4.1.1", features = ["derive", "env"] }
libsigner = { path = "../libsigner" }
libstackerdb = { path = "../libstackerdb" }
serde = "1"
serde_derive = "1"
serde_stacker = "0.1"
stacks-common = { path = "../stacks-common" }
clarity = { path = "../clarity" }
libstackerdb = { path = "../libstackerdb" }
libsigner = { path = "../libsigner" }
toml = "0.5.6"
slog = { version = "2.5.2", features = [ "max_level_trace" ] }
slog-term = "2.6.0"
slog-json = { version = "2.3.0", optional = true }
slog-term = "2.6.0"
stacks-common = { path = "../stacks-common" }
thiserror = "1.0"
toml = "0.5.6"
[dependencies.serde_json]
version = "1.0"

94
stacks-signer/README.md Normal file
View File

@@ -0,0 +1,94 @@
# stacks-signer: Stacks Signer CLI
stacks-signer is a command-line interface (CLI) for executing DKG (Distributed Key Generation) rounds, signing transactions and blocks, and more within the Stacks blockchain ecosystem. This tool provides various subcommands to interact with the StackerDB, perform cryptographic operations, and manage configurations.
## Installation
To use stacks-signer, you need to build and install the Rust program. You can do this by following these steps:
1. **Clone the Repository**: Clone the stacks-signer repository from [GitHub](https://github.com/blockstack/stacks-blockchain).
```bash
git clone https://github.com/blockstack/stacks-blockchain.git
```
2. **Build the Program**: Change to the stacks-signer directory and build the program using `cargo`.
```bash
cd stacks-signer
cargo build --release
```
3. **Run the Program**: You can now run the stacks-signer CLI.
```bash
./target/release/stacks-signer --help
```
### Configuration
You can provide configuration options such as the host, contract, and private key using a TOML file. Use the `--config` option to specify the path to the configuration file. Alternatively, you can provide the necessary options directly in the command line.
```bash
./stacks-signer --config <config_file>
```
- `--config`: Path to the TOML configuration file.
## Usage
The stacks-signer CLI provides the following subcommands:
### `get-chunk`
Retrieve a chunk from the StackerDB instance.
```bash
./stacks-signer --config <config_file> get-chunk --slot_id <slot_id> --slot_version <slot_version>
```
- `--host`: The stacks node host to connect to. Required if not using the --config option.
- `--contract`: The contract ID of the StackerDB instance. Required if not using the --config option.
- `--slot_id`: The slot ID to get.
- `--slot_version`: The slot version to get.
### `get-latest-chunk`
Retrieve the latest chunk from the StackerDB instance.
```bash
./stacks-signer --config <config_file> get-latest-chunk --slot_id <slot_id>
```
- `--host`: The stacks node host to connect to. Required if not using the --config option.
- `--contract`: The contract ID of the StackerDB instance. Required if not using the --config option.
- `--slot_id`: The slot ID to get.
### `list-chunks`
List chunks from the StackerDB instance.
```bash
./stacks-signer --config <config_file> list-chunks
```
- `--host`: The stacks node host to connect to. Required if not using the --config option.
- `--contract`: The contract ID of the StackerDB instance. Required if not using the --config option.
### `put-chunk`
Upload a chunk to the StackerDB instance.
```bash
./stacks-signer --config <config_file> put-chunk --slot_id <slot_id> --slot_version <slot_version> [--data <data>]
```
- `--host`: The stacks node host to connect to. Required if not using the --config option.
- `--contract`: The contract ID of the StackerDB instance. Required if not using the --config option.
- `--slot_id`: The slot ID to get.
- `--slot_version`: The slot version to get.
- `--data`: The data to upload. If you wish to pipe data using STDIN, use with '-'.
## License
This program is open-source software released under the terms of the GNU General Public License (GPL). You should have received a copy of the GNU General Public License along with this program.

View File

@@ -14,85 +14,80 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use std::convert::TryFrom;
use std::error;
use std::fmt;
use std::fs;
use std::net::{SocketAddr, ToSocketAddrs};
use serde::Deserialize;
use toml;
use clarity::vm::types::QualifiedContractIdentifier;
use serde::Deserialize;
use stacks_common::types::chainstate::StacksPrivateKey;
use std::{
convert::TryFrom,
fs,
net::{SocketAddr, ToSocketAddrs},
path::PathBuf,
};
#[derive(Debug)]
#[derive(thiserror::Error, Debug)]
/// An error occurred parsing the provided configuration
pub enum ConfigError {
NoSuchConfigFile(String),
/// Error occurred reading config file
#[error("{0}")]
InvalidConfig(String),
/// An error occurred parsing the TOML data
#[error("{0}")]
ParseError(String),
/// A field was malformed
#[error("identifier={0}, value={1}")]
BadField(String, String),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ConfigError::NoSuchConfigFile(ref s) => fmt::Display::fmt(s, f),
ConfigError::ParseError(ref s) => fmt::Display::fmt(s, f),
ConfigError::BadField(ref f1, ref f2) => {
write!(f, "identifier={}, value={}", f1, f2)
}
}
}
}
impl error::Error for ConfigError {
fn cause(&self) -> Option<&dyn error::Error> {
match *self {
ConfigError::NoSuchConfigFile(..) => None,
ConfigError::ParseError(..) => None,
ConfigError::BadField(..) => None,
}
}
}
pub struct ConfigFile {
/// The parsed configuration for the signer
pub struct Config {
/// endpoint to the stacks node
pub node_host: SocketAddr,
/// smart contract that controls the target stackerdb
pub stackerdb_contract_id: QualifiedContractIdentifier,
/// the private key used to sign blocks, chunks, and transactions
pub private_key: StacksPrivateKey,
}
/// Internal struct for loading up the config file
#[derive(Deserialize)]
#[derive(Deserialize, Debug)]
struct RawConfigFile {
/// endpoint to stacks node
pub node_host: String,
/// contract identifier
pub stackerdb_contract_id: String,
/// the private key used to sign blocks, chunks, and transactions in hexademical format
pub private_key: String,
}
impl RawConfigFile {
/// load the config from a string
pub fn load_from_str(data: &str) -> Result<RawConfigFile, ConfigError> {
pub fn load_from_str(data: &str) -> Result<Self, ConfigError> {
let config: RawConfigFile =
toml::from_str(data).map_err(|e| ConfigError::ParseError(format!("{:?}", &e)))?;
Ok(config)
}
/// load the config from a file
pub fn load_from_file(path: &str) -> Result<RawConfigFile, ConfigError> {
let data = fs::read_to_string(path)
.map_err(|_| ConfigError::NoSuchConfigFile(path.to_string()))?;
Self::load_from_str(&data)
/// load the config from a file and parse it
pub fn load_from_file(path: &str) -> Result<Self, ConfigError> {
Self::try_from(&PathBuf::from(path))
}
}
impl TryFrom<RawConfigFile> for ConfigFile {
impl TryFrom<&PathBuf> for RawConfigFile {
type Error = ConfigError;
fn try_from(path: &PathBuf) -> Result<Self, Self::Error> {
RawConfigFile::load_from_str(&fs::read_to_string(path).map_err(|e| {
ConfigError::InvalidConfig(format!("failed to read config file: {:?}", &e))
})?)
}
}
impl TryFrom<RawConfigFile> for Config {
type Error = ConfigError;
/// Attempt to decode the raw config file's primitive types into our types.
/// NOTE: network access is required for this to work
fn try_from(raw_data: RawConfigFile) -> Result<ConfigFile, Self::Error> {
fn try_from(raw_data: RawConfigFile) -> Result<Self, Self::Error> {
let node_host = raw_data
.node_host
.clone()
@@ -114,23 +109,33 @@ impl TryFrom<RawConfigFile> for ConfigFile {
)
})?;
Ok(ConfigFile {
let private_key = StacksPrivateKey::from_hex(&raw_data.private_key)
.map_err(|_| ConfigError::BadField("private_key".to_string(), raw_data.private_key))?;
Ok(Self {
node_host,
stackerdb_contract_id,
private_key,
})
}
}
impl ConfigFile {
impl TryFrom<&PathBuf> for Config {
type Error = ConfigError;
fn try_from(path: &PathBuf) -> Result<Self, ConfigError> {
let config_file = RawConfigFile::try_from(path)?;
Self::try_from(config_file)
}
}
impl Config {
/// load the config from a string and parse it
pub fn load_from_str(data: &str) -> Result<ConfigFile, ConfigError> {
pub fn load_from_str(data: &str) -> Result<Self, ConfigError> {
RawConfigFile::load_from_str(data)?.try_into()
}
/// load the config from a file and parse it
pub fn load_from_file(path: &str) -> Result<ConfigFile, ConfigError> {
let data = fs::read_to_string(path)
.map_err(|_| ConfigError::NoSuchConfigFile(path.to_string()))?;
Self::load_from_str(&data)
pub fn load_from_file(path: &str) -> Result<Self, ConfigError> {
Self::try_from(&PathBuf::from(path))
}
}

View File

@@ -1,3 +1,8 @@
//! # stacks-signer: Stacks signer binary for executing DKG rounds, signing transactions and blocks, and more.
//!
//! Usage documentation can be found in the [README]("https://github.com/blockstack/stacks-blockchain/stacks-signer/README.md).
//!
//!
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
// Copyright (C) 2020-2023 Stacks Open Internet Foundation
//
@@ -13,7 +18,6 @@
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
extern crate slog;
extern crate stacks_common;
@@ -24,13 +28,14 @@ extern crate toml;
mod config;
use crate::config::Config;
use clap::Parser;
use libsigner::{SignerSession, StackerDBSession};
use std::env;
use std::io;
use std::io::{Read, Write};
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
use std::process;
use std::{
io::{self, Read, Write},
net::SocketAddr,
path::PathBuf,
};
use clarity::vm::types::QualifiedContractIdentifier;
@@ -38,254 +43,151 @@ use stacks_common::types::chainstate::StacksPrivateKey;
use libstackerdb::StackerDBChunkData;
/// Consume one argument from `args`, which may go by multiple names in `argnames`.
/// If it has an argument (`has_optarg`), then return it.
///
/// Returns Ok(Some(arg)) if this argument was passed and it has argument `arg`
/// Returns Ok(Some("")) if this argument was passed but `has_optarg` is false
/// Returns Ok(None) if this argument is not present
/// Returns Err(..) if an argument was expected but not found.
fn consume_arg(
args: &mut Vec<String>,
argnames: &[&str],
has_optarg: bool,
) -> Result<Option<String>, String> {
if let Some(ref switch) = args
.iter()
.find(|ref arg| argnames.iter().find(|ref argname| argname == arg).is_some())
{
let idx = args
.iter()
.position(|ref arg| arg == switch)
.expect("BUG: did not find the thing that was just found");
let argval = if has_optarg {
// following argument is the argument value
if idx + 1 < args.len() {
Some(args[idx + 1].clone())
} else {
// invalid usage -- expected argument
return Err(format!("Expected argument for {}", argnames.join(",")));
}
} else {
// only care about presence of this option
Some("".to_string())
};
args.remove(idx);
if has_optarg {
// also clear the argument
args.remove(idx);
}
Ok(argval)
} else {
// not found
Ok(None)
}
#[derive(Parser, Debug)]
#[command(author, version, about)]
/// The CLI arguments for the stacks signer
pub struct Cli {
/// Path to config file
#[arg(long, value_name = "FILE")]
config: Option<PathBuf>,
/// The Stacks node to connect to
#[clap(long, required_unless_present = "config", conflicts_with = "config")]
host: Option<SocketAddr>,
/// The stacker-db contract to use
#[arg(short, long, value_parser = parse_contract, required_unless_present = "config", conflicts_with = "config")]
contract: Option<QualifiedContractIdentifier>,
/// The Stacks private key to use in hexademical format
#[arg(short, long, value_parser = parse_private_key, required_unless_present = "config", conflicts_with = "config")]
private_key: Option<StacksPrivateKey>,
/// Subcommand action to take
#[command(subcommand)]
pub command: Command,
}
/// Print an error message, usage, and exit
fn usage(err_msg: Option<&str>) {
if let Some(err_msg) = err_msg {
eprintln!("{}", err_msg);
}
eprintln!(
"Usage: {} subcommand [args]",
&env::args().collect::<Vec<_>>()[0]
);
process::exit(1);
/// Subcommands for the stacks signer binary
#[derive(clap::Subcommand, Debug)]
pub enum Command {
/// Get a chunk from the stacker-db instance
GetChunk(GetChunkArgs),
/// Get the latest chunk from the stacker-db instance
GetLatestChunk(GetLatestChunkArgs),
/// List chunks from the stacker-db instance
ListChunks,
/// Upload a chunk to the stacker-db instance
PutChunk(PutChunkArgs),
}
/// Get -h,--host and -c,--contract
fn parse_host_and_contract(argv: &mut Vec<String>) -> (SocketAddr, QualifiedContractIdentifier) {
let host_opt = match consume_arg(argv, &["-h", "--host"], true) {
Ok(x) => x,
Err(msg) => {
usage(Some(&msg));
unreachable!()
}
};
let contract_opt = match consume_arg(argv, &["-c", "--contract"], true) {
Ok(x) => x,
Err(msg) => {
usage(Some(&msg));
unreachable!()
}
};
let host = match host_opt {
Some(host) => match host.to_socket_addrs() {
Ok(mut iter) => match iter.next() {
Some(host) => host,
None => {
usage(Some("No hosts resolved"));
unreachable!()
}
},
Err(..) => {
usage(Some("Failed to resolve host"));
unreachable!()
}
},
None => {
usage(Some("Need -h,--host"));
unreachable!()
}
};
let contract = match contract_opt {
Some(host) => match QualifiedContractIdentifier::parse(&host) {
Ok(qcid) => qcid,
Err(..) => {
usage(Some("Invalid contract ID"));
unreachable!()
}
},
None => {
usage(Some("Need -c,--contract"));
unreachable!()
}
};
(host, contract)
/// Arguments for the get-chunk command
#[derive(Parser, Debug, Clone)]
pub struct GetChunkArgs {
/// The slot ID to get
#[arg(long)]
slot_id: u32,
/// The slot version to get
#[arg(long)]
slot_version: u32,
}
/// Handle the get-chunk subcommand
fn handle_get_chunk(mut argv: Vec<String>) {
let (host, contract) = parse_host_and_contract(&mut argv);
if argv.len() < 4 {
usage(Some("Expected slot_id and slot_version"));
}
let slot_id: u32 = match argv[2].parse() {
Ok(x) => x,
Err(..) => {
usage(Some("Expected u32 for slot ID"));
unreachable!()
}
};
let slot_version: u32 = match argv[3].parse() {
Ok(x) => x,
Err(..) => {
usage(Some("Expected u32 for slot version"));
unreachable!()
}
};
let mut session = StackerDBSession::new(host.clone(), contract.clone());
session.connect(host, contract).unwrap();
let mut chunk_opt = session.get_chunk(slot_id, slot_version).unwrap();
if let Some(chunk) = chunk_opt.take() {
io::stdout().write(&chunk).unwrap();
}
process::exit(0);
/// Arguments for the get-latest-chunk command
#[derive(Parser, Debug, Clone)]
pub struct GetLatestChunkArgs {
/// The slot ID to get
#[arg(long)]
slot_id: u32,
}
/// Handle the get-latest-chunk subcommand
fn handle_get_latest_chunk(mut argv: Vec<String>) {
let (host, contract) = parse_host_and_contract(&mut argv);
if argv.len() < 3 {
usage(Some("Expected slot_id"));
}
let slot_id: u32 = match argv[2].parse() {
Ok(x) => x,
Err(..) => {
usage(Some("Expected u32 for slot ID"));
unreachable!()
}
};
let mut session = StackerDBSession::new(host.clone(), contract.clone());
session.connect(host, contract).unwrap();
let chunk_opt = session.get_latest_chunk(slot_id).unwrap();
if let Some(chunk) = chunk_opt {
io::stdout().write(&chunk).unwrap();
}
process::exit(0);
#[derive(Parser, Debug, Clone)]
/// Arguments for the put-chunk command
pub struct PutChunkArgs {
/// The slot ID to get
#[arg(long)]
slot_id: u32,
/// The slot version to get
#[arg(long)]
slot_version: u32,
/// The data to upload
#[arg(required = false, value_parser = parse_data)]
data: Vec<u8>,
}
/// Handle listing chunks
fn handle_list_chunks(mut argv: Vec<String>) {
let (host, contract) = parse_host_and_contract(&mut argv);
let mut session = StackerDBSession::new(host.clone(), contract.clone());
session.connect(host, contract).unwrap();
let chunk_list = session.list_chunks().unwrap();
println!("{}", serde_json::to_string(&chunk_list).unwrap());
process::exit(0);
/// Parse the contract ID
fn parse_contract(contract: &str) -> Result<QualifiedContractIdentifier, String> {
QualifiedContractIdentifier::parse(contract).map_err(|e| format!("Invalid contract: {}", e))
}
/// Handle uploading a chunk
fn handle_put_chunk(mut argv: Vec<String>) {
let (host, contract) = parse_host_and_contract(&mut argv);
if argv.len() < 6 {
usage(Some("Expected slot_id, slot_version, private_key, data"));
}
/// Parse the hexadecimal Stacks private key
fn parse_private_key(private_key: &str) -> Result<StacksPrivateKey, String> {
StacksPrivateKey::from_hex(private_key).map_err(|e| format!("Invalid private key: {}", e))
}
let slot_id: u32 = match argv[2].parse() {
Ok(x) => x,
Err(..) => {
usage(Some("Expected u32 for slot ID"));
unreachable!()
}
};
let slot_version: u32 = match argv[3].parse() {
Ok(x) => x,
Err(..) => {
usage(Some("Expected u32 for slot version"));
unreachable!()
}
};
let privk = match StacksPrivateKey::from_hex(&argv[4]) {
Ok(x) => x,
Err(..) => {
usage(Some("Failed to parse private key"));
unreachable!()
}
};
let data = if argv[5] == "-" {
/// Parse the input data
fn parse_data(data: &str) -> Result<Vec<u8>, String> {
let data = if data == "-" {
// Parse the data from stdin
let mut buf = vec![];
io::stdin().read_to_end(&mut buf).unwrap();
buf
} else {
argv[5].as_bytes().to_vec()
data.as_bytes().to_vec()
};
Ok(data)
}
/// Create a new stacker db session
fn stackerdb_session(host: SocketAddr, contract: QualifiedContractIdentifier) -> StackerDBSession {
let mut session = StackerDBSession::new(host, contract.clone());
session.connect(host, contract).unwrap();
session
}
/// Write the chunk to stdout
fn write_chunk_to_stdout(chunk_opt: Option<Vec<u8>>) {
if let Some(chunk) = chunk_opt.as_ref() {
let bytes = io::stdout().write(chunk).unwrap();
if bytes < chunk.len() {
print!(
"Failed to write complete chunk to stdout. Missing {} bytes",
chunk.len() - bytes
);
}
}
}
fn main() {
let cli = Cli::parse();
let (host, contract, private_key) = if let Some(config) = cli.config {
let config = Config::try_from(&config).unwrap();
(
config.node_host,
config.stackerdb_contract_id,
config.private_key,
)
} else {
(
cli.host.unwrap(),
cli.contract.unwrap(),
cli.private_key.unwrap(),
)
};
let mut chunk = StackerDBChunkData::new(slot_id, slot_version, data);
chunk.sign(&privk).unwrap();
let mut session = StackerDBSession::new(host.clone(), contract.clone());
session.connect(host, contract).unwrap();
let chunk_ack = session.put_chunk(chunk).unwrap();
println!("{}", serde_json::to_string(&chunk_ack).unwrap());
process::exit(0);
}
fn main() {
let argv: Vec<String> = env::args().collect();
if argv.len() < 2 {
usage(Some("No subcommand given"));
}
let subcommand = argv[1].clone();
match subcommand.as_str() {
"get-chunk" => {
handle_get_chunk(argv);
let mut session = stackerdb_session(host, contract);
match cli.command {
Command::GetChunk(args) => {
let chunk_opt = session.get_chunk(args.slot_id, args.slot_version).unwrap();
write_chunk_to_stdout(chunk_opt);
}
"get-latest-chunk" => {
handle_get_latest_chunk(argv);
Command::GetLatestChunk(args) => {
let chunk_opt = session.get_latest_chunk(args.slot_id).unwrap();
write_chunk_to_stdout(chunk_opt);
}
"list-chunks" => {
handle_list_chunks(argv);
Command::ListChunks => {
let chunk_list = session.list_chunks().unwrap();
println!("{}", serde_json::to_string(&chunk_list).unwrap());
}
"put-chunk" => {
handle_put_chunk(argv);
}
_ => {
usage(Some(&format!("Unrecognized subcommand '{}'", &subcommand)));
Command::PutChunk(args) => {
let mut chunk = StackerDBChunkData::new(args.slot_id, args.slot_version, args.data);
chunk.sign(&private_key).unwrap();
let chunk_ack = session.put_chunk(chunk).unwrap();
println!("{}", serde_json::to_string(&chunk_ack).unwrap());
}
}
}