mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-01-12 22:43:42 +08:00
feat: prototype stacks-signer daemon (just a CLI front-end to stackerdb for now)
This commit is contained in:
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -2692,6 +2692,25 @@ dependencies = [
|
||||
"warp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stacks-signer"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"clarity",
|
||||
"libstackerdb",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_stacker",
|
||||
"sha2 0.10.6",
|
||||
"slog",
|
||||
"slog-json",
|
||||
"slog-term",
|
||||
"stacks-common",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stackslib"
|
||||
version = "0.0.1"
|
||||
|
||||
@@ -7,4 +7,5 @@ members = [
|
||||
"clarity",
|
||||
"stx-genesis",
|
||||
"libstackerdb",
|
||||
"stacks-signer",
|
||||
"testnet/stacks-node"]
|
||||
|
||||
42
stacks-signer/Cargo.toml
Normal file
42
stacks-signer/Cargo.toml
Normal file
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "stacks-signer"
|
||||
version = "0.0.1"
|
||||
authors = [ "Jude Nelson <jude@stacks.org>" ]
|
||||
license = "GPLv3"
|
||||
homepage = "https://github.com/blockstack/stacks-blockchain"
|
||||
repository = "https://github.com/blockstack/stacks-blockchain"
|
||||
description = "Stacker signer binary"
|
||||
keywords = [ "stacks", "stx", "bitcoin", "crypto", "blockstack", "decentralized", "dapps", "blockchain" ]
|
||||
readme = "README.md"
|
||||
resolver = "2"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "stacks-signer"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = "1"
|
||||
serde_derive = "1"
|
||||
serde_stacker = "0.1"
|
||||
stacks-common = { path = "../stacks-common" }
|
||||
clarity = { path = "../clarity" }
|
||||
libstackerdb = { path = "../libstackerdb" }
|
||||
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 }
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1.0"
|
||||
features = ["arbitrary_precision", "unbounded_depth"]
|
||||
|
||||
[dependencies.secp256k1]
|
||||
version = "0.24.3"
|
||||
features = ["serde", "recovery"]
|
||||
|
||||
[target.'cfg(all(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64"), not(target_env = "msvc")))'.dependencies]
|
||||
sha2 = { version = "0.10", features = ["asm"] }
|
||||
|
||||
[target.'cfg(any(not(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64")), target_env = "msvc"))'.dependencies]
|
||||
sha2 = { version = "0.10" }
|
||||
136
stacks-signer/src/config.rs
Normal file
136
stacks-signer/src/config.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
|
||||
// Copyright (C) 2020-2023 Stacks Open Internet Foundation
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// 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;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
NoSuchConfigFile(String),
|
||||
ParseError(String),
|
||||
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 {
|
||||
/// endpoint to the stacks node
|
||||
pub node_host: SocketAddr,
|
||||
/// smart contract that controls the target stackerdb
|
||||
pub stackerdb_contract_id: QualifiedContractIdentifier,
|
||||
}
|
||||
|
||||
/// Internal struct for loading up the config file
|
||||
#[derive(Deserialize)]
|
||||
struct RawConfigFile {
|
||||
/// endpoint to stacks node
|
||||
pub node_host: String,
|
||||
/// contract identifier
|
||||
pub stackerdb_contract_id: String,
|
||||
}
|
||||
|
||||
impl RawConfigFile {
|
||||
/// load the config from a string
|
||||
pub fn load_from_str(data: &str) -> Result<RawConfigFile, 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)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RawConfigFile> for ConfigFile {
|
||||
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> {
|
||||
let node_host = raw_data
|
||||
.node_host
|
||||
.clone()
|
||||
.to_socket_addrs()
|
||||
.map_err(|_| {
|
||||
ConfigError::BadField("node_host".to_string(), raw_data.node_host.clone())
|
||||
})?
|
||||
.next()
|
||||
.ok_or(ConfigError::BadField(
|
||||
"node_host".to_string(),
|
||||
raw_data.node_host.clone(),
|
||||
))?;
|
||||
|
||||
let stackerdb_contract_id =
|
||||
QualifiedContractIdentifier::parse(&raw_data.stackerdb_contract_id).map_err(|_| {
|
||||
ConfigError::BadField(
|
||||
"stackerdb_contract_id".to_string(),
|
||||
raw_data.stackerdb_contract_id,
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(ConfigFile {
|
||||
node_host,
|
||||
stackerdb_contract_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
/// load the config from a string and parse it
|
||||
pub fn load_from_str(data: &str) -> Result<ConfigFile, 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)
|
||||
}
|
||||
}
|
||||
296
stacks-signer/src/main.rs
Normal file
296
stacks-signer/src/main.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
|
||||
// Copyright (C) 2020-2023 Stacks Open Internet Foundation
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#[macro_use(o, slog_log, slog_trace, slog_debug, slog_info, slog_warn, slog_error)]
|
||||
extern crate slog;
|
||||
|
||||
#[macro_use]
|
||||
extern crate stacks_common;
|
||||
|
||||
extern crate clarity;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate toml;
|
||||
|
||||
mod config;
|
||||
mod rpc;
|
||||
|
||||
use crate::rpc::SignerSession;
|
||||
use crate::rpc::StackerDBSession;
|
||||
use std::env;
|
||||
use std::io;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::SocketAddr;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::process;
|
||||
|
||||
use clarity::vm::types::QualifiedContractIdentifier;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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 chunk_opt = session.get_chunk(slot_id, slot_version).unwrap();
|
||||
if let Some(chunk) = chunk_opt {
|
||||
io::stdout().write(&chunk).unwrap();
|
||||
}
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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"));
|
||||
}
|
||||
|
||||
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] == "-" {
|
||||
let mut buf = vec![];
|
||||
io::stdin().read_to_end(&mut buf).unwrap();
|
||||
buf
|
||||
} else {
|
||||
argv[5].as_bytes().to_vec()
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
"get-latest-chunk" => {
|
||||
handle_get_latest_chunk(argv);
|
||||
}
|
||||
"list-chunks" => {
|
||||
handle_list_chunks(argv);
|
||||
}
|
||||
"put-chunk" => {
|
||||
handle_put_chunk(argv);
|
||||
}
|
||||
_ => {
|
||||
usage(Some(&format!("Unrecognized subcommand '{}'", &subcommand)));
|
||||
}
|
||||
}
|
||||
}
|
||||
392
stacks-signer/src/rpc.rs
Normal file
392
stacks-signer/src/rpc.rs
Normal file
@@ -0,0 +1,392 @@
|
||||
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
|
||||
// Copyright (C) 2020-2023 Stacks Open Internet Foundation
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// 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::collections::HashMap;
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::SocketAddr;
|
||||
use std::net::TcpStream;
|
||||
use std::str;
|
||||
|
||||
use libstackerdb::{
|
||||
stackerdb_get_chunk_path, stackerdb_get_metadata_path, stackerdb_post_chunk_path, SlotMetadata,
|
||||
StackerDBChunkAckData, StackerDBChunkData,
|
||||
};
|
||||
|
||||
use clarity::vm::types::QualifiedContractIdentifier;
|
||||
|
||||
use stacks_common::codec::MAX_MESSAGE_LEN;
|
||||
use stacks_common::deps_common::httparse;
|
||||
use stacks_common::util::chunked_encoding::*;
|
||||
|
||||
use serde_json;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RPCError {
|
||||
IO(io::Error),
|
||||
Deserialize(String),
|
||||
NotConnected,
|
||||
MalformedResponse(String),
|
||||
HttpError(u32),
|
||||
}
|
||||
|
||||
impl fmt::Display for RPCError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
RPCError::IO(ref s) => fmt::Display::fmt(s, f),
|
||||
RPCError::Deserialize(ref s) => fmt::Display::fmt(s, f),
|
||||
RPCError::HttpError(ref s) => {
|
||||
write!(f, "HTTP code {}", s)
|
||||
}
|
||||
RPCError::MalformedResponse(ref s) => {
|
||||
write!(f, "Malformed response: {}", s)
|
||||
}
|
||||
RPCError::NotConnected => {
|
||||
write!(f, "Not connected")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for RPCError {
|
||||
fn cause(&self) -> Option<&dyn error::Error> {
|
||||
match *self {
|
||||
RPCError::IO(ref s) => Some(s),
|
||||
RPCError::Deserialize(..) => None,
|
||||
RPCError::HttpError(..) => None,
|
||||
RPCError::MalformedResponse(..) => None,
|
||||
RPCError::NotConnected => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for RPCError {
|
||||
fn from(e: io::Error) -> RPCError {
|
||||
RPCError::IO(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SignerSession {
|
||||
fn connect(
|
||||
&mut self,
|
||||
host: SocketAddr,
|
||||
stackerdb_contract_id: QualifiedContractIdentifier,
|
||||
) -> Result<(), RPCError>;
|
||||
fn list_chunks(&mut self) -> Result<Vec<SlotMetadata>, RPCError>;
|
||||
fn get_chunks(
|
||||
&mut self,
|
||||
slots_and_versions: &[(u32, u32)],
|
||||
) -> Result<Vec<Option<Vec<u8>>>, RPCError>;
|
||||
fn get_latest_chunks(&mut self, slot_ids: &[u32]) -> Result<Vec<Option<Vec<u8>>>, RPCError>;
|
||||
fn put_chunk(&mut self, chunk: StackerDBChunkData) -> Result<StackerDBChunkAckData, RPCError>;
|
||||
|
||||
/// Get a single chunk with the given version
|
||||
/// Returns Ok(Some(..)) if the chunk exists
|
||||
/// Returns Ok(None) if the chunk with the given version does not exist
|
||||
/// Returns Err(..) on transport error
|
||||
fn get_chunk(&mut self, slot_id: u32, version: u32) -> Result<Option<Vec<u8>>, RPCError> {
|
||||
Ok(self.get_chunks(&[(slot_id, version)])?[0].clone())
|
||||
}
|
||||
|
||||
/// Get a single latest chunk.
|
||||
/// Returns Ok(Some(..)) if the slot exists
|
||||
/// Returns Ok(None) if not
|
||||
/// Returns Err(..) on transport error
|
||||
fn get_latest_chunk(&mut self, slot_id: u32) -> Result<Option<Vec<u8>>, RPCError> {
|
||||
Ok(self.get_latest_chunks(&[(slot_id)])?[0].clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// signer session for a stackerdb instance
|
||||
pub struct StackerDBSession {
|
||||
/// host we're talking to
|
||||
pub host: SocketAddr,
|
||||
/// contract we're talking to
|
||||
pub stackerdb_contract_id: QualifiedContractIdentifier,
|
||||
/// connection to the replica
|
||||
sock: Option<TcpStream>,
|
||||
}
|
||||
|
||||
impl StackerDBSession {
|
||||
/// instantiate but don't connect
|
||||
pub fn new(
|
||||
host: SocketAddr,
|
||||
stackerdb_contract_id: QualifiedContractIdentifier,
|
||||
) -> StackerDBSession {
|
||||
StackerDBSession {
|
||||
host,
|
||||
stackerdb_contract_id,
|
||||
sock: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// connect or reconnect to the node
|
||||
fn connect_or_reconnect(&mut self) -> Result<(), RPCError> {
|
||||
debug!("connect to {}", &self.host);
|
||||
self.sock = Some(TcpStream::connect(self.host.clone())?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Do something with the connected socket
|
||||
fn with_socket<F, R>(&mut self, todo: F) -> Result<R, RPCError>
|
||||
where
|
||||
F: FnOnce(&mut StackerDBSession, &mut TcpStream) -> R,
|
||||
{
|
||||
if self.sock.is_none() {
|
||||
self.connect_or_reconnect()?;
|
||||
}
|
||||
|
||||
let mut sock = if let Some(s) = self.sock.take() {
|
||||
s
|
||||
} else {
|
||||
return Err(RPCError::NotConnected);
|
||||
};
|
||||
|
||||
let res = todo(self, &mut sock);
|
||||
|
||||
self.sock = Some(sock);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Decode the HTTP payload into its headers and body.
|
||||
/// Return the offset into payload where the body starts, and a table of headers.
|
||||
fn decode_http_response(payload: &[u8]) -> Result<(HashMap<String, String>, usize), RPCError> {
|
||||
// realistically, there won't be more than 32 headers
|
||||
let mut headers_buf = [httparse::EMPTY_HEADER; 32];
|
||||
let mut resp = httparse::Response::new(&mut headers_buf);
|
||||
|
||||
// consume respuest
|
||||
let (headers, body_offset) = if let Ok(httparse::Status::Complete(body_offset)) =
|
||||
resp.parse(payload)
|
||||
{
|
||||
if let Some(code) = resp.code {
|
||||
if code != 200 {
|
||||
return Err(RPCError::HttpError(code.into()));
|
||||
}
|
||||
} else {
|
||||
return Err(RPCError::MalformedResponse(
|
||||
"No HTTP status code returned".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(version) = resp.version {
|
||||
if version != 0 && version != 1 {
|
||||
return Err(RPCError::MalformedResponse(format!(
|
||||
"Unrecognized HTTP code {}",
|
||||
version
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
return Err(RPCError::MalformedResponse(
|
||||
"No HTTP version given".to_string(),
|
||||
));
|
||||
}
|
||||
let mut headers: HashMap<String, String> = HashMap::new();
|
||||
for i in 0..resp.headers.len() {
|
||||
let value = String::from_utf8(resp.headers[i].value.to_vec()).map_err(|_e| {
|
||||
RPCError::MalformedResponse("Invalid HTTP header value: not utf-8".to_string())
|
||||
})?;
|
||||
if !value.is_ascii() {
|
||||
return Err(RPCError::MalformedResponse(
|
||||
"Invalid HTTP response: header value is not ASCII-US".to_string(),
|
||||
));
|
||||
}
|
||||
if value.len() > 4096 {
|
||||
return Err(RPCError::MalformedResponse(format!(
|
||||
"Invalid HTTP response: header value is too big"
|
||||
)));
|
||||
}
|
||||
|
||||
let key = resp.headers[i].name.to_string().to_lowercase();
|
||||
if headers.contains_key(&key) {
|
||||
return Err(RPCError::MalformedResponse(format!(
|
||||
"Invalid HTTP respuest: duplicate header \"{}\"",
|
||||
key
|
||||
)));
|
||||
}
|
||||
headers.insert(key, value);
|
||||
}
|
||||
(headers, body_offset)
|
||||
} else {
|
||||
return Err(RPCError::Deserialize(
|
||||
"Failed to decode HTTP headers".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
Ok((headers, body_offset))
|
||||
}
|
||||
|
||||
/// send an HTTP RPC request and receive a reply.
|
||||
/// Return the HTTP reply, decoded if it was chunked
|
||||
fn run_http_request(
|
||||
&mut self,
|
||||
verb: &str,
|
||||
path: &str,
|
||||
content_type: Option<&str>,
|
||||
payload: &[u8],
|
||||
) -> Result<Vec<u8>, RPCError> {
|
||||
self.with_socket(|session, sock| {
|
||||
let content_length_hdr = if payload.len() > 0 {
|
||||
format!("Content-Length: {}\r\n", payload.len())
|
||||
}
|
||||
else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let req_txt = if let Some(content_type) = content_type {
|
||||
format!(
|
||||
"{} {} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\nContent-Type: {}\r\n{}User-Agent: stacks-signer/0.1\r\nAccept: */*\r\n\r\n",
|
||||
verb, path, format!("{}", &session.host), content_type, content_length_hdr
|
||||
)
|
||||
}
|
||||
else {
|
||||
format!(
|
||||
"{} {} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n{}User-Agent: stacks-signer/0.1\r\nAccept: */*\r\n\r\n",
|
||||
verb, path, format!("{}", &session.host), content_length_hdr
|
||||
)
|
||||
};
|
||||
debug!("HTTP request\n{}", &req_txt);
|
||||
|
||||
sock.write_all(req_txt.as_bytes())?;
|
||||
sock.write_all(payload)?;
|
||||
|
||||
let mut buf = vec![];
|
||||
|
||||
sock.read_to_end(&mut buf)?;
|
||||
|
||||
let (headers, body_offset) = Self::decode_http_response(&buf)?;
|
||||
if body_offset >= buf.len() {
|
||||
// no body
|
||||
debug!("No HTTP body");
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// let chunked = Self::is_http_chunked(&buf[0..body_offset])?;
|
||||
let chunked = if let Some(val) = headers.get("transfer-encoding") {
|
||||
val == "chunked"
|
||||
}
|
||||
else {
|
||||
false
|
||||
};
|
||||
|
||||
let body = if chunked {
|
||||
// chunked encoding
|
||||
debug!("HTTP response is chunked, at offset {}", body_offset);
|
||||
let ptr = &mut &buf[body_offset..];
|
||||
let mut fd = HttpChunkedTransferReader::from_reader(ptr, MAX_MESSAGE_LEN.into());
|
||||
let mut decoded_body = vec![];
|
||||
fd.read_to_end(&mut decoded_body)?;
|
||||
decoded_body
|
||||
}
|
||||
else {
|
||||
// body is just as-is
|
||||
debug!("HTTP response is raw, at offset {}", body_offset);
|
||||
buf[body_offset..].to_vec()
|
||||
};
|
||||
|
||||
Ok(body)
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
impl SignerSession for StackerDBSession {
|
||||
/// connect to the replica
|
||||
fn connect(
|
||||
&mut self,
|
||||
host: SocketAddr,
|
||||
stackerdb_contract_id: QualifiedContractIdentifier,
|
||||
) -> Result<(), RPCError> {
|
||||
self.host = host;
|
||||
self.stackerdb_contract_id = stackerdb_contract_id;
|
||||
self.connect_or_reconnect()
|
||||
}
|
||||
|
||||
/// query the replica for a list of chunks
|
||||
fn list_chunks(&mut self) -> Result<Vec<SlotMetadata>, RPCError> {
|
||||
let bytes = self.run_http_request(
|
||||
"GET",
|
||||
&stackerdb_get_metadata_path(self.stackerdb_contract_id.clone()),
|
||||
None,
|
||||
&[],
|
||||
)?;
|
||||
let metadata: Vec<SlotMetadata> = serde_json::from_slice(&bytes)
|
||||
.map_err(|e| RPCError::Deserialize(format!("{:?}", &e)))?;
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// query the replica for zero or more chunks
|
||||
fn get_chunks(
|
||||
&mut self,
|
||||
slots_and_versions: &[(u32, u32)],
|
||||
) -> Result<Vec<Option<Vec<u8>>>, RPCError> {
|
||||
let mut payloads = vec![];
|
||||
for (slot_id, slot_version) in slots_and_versions.iter() {
|
||||
let path = stackerdb_get_chunk_path(
|
||||
self.stackerdb_contract_id.clone(),
|
||||
*slot_id,
|
||||
Some(*slot_version),
|
||||
);
|
||||
let chunk = match self.run_http_request("GET", &path, None, &[]) {
|
||||
Ok(body_bytes) => Some(body_bytes),
|
||||
Err(RPCError::HttpError(code)) => {
|
||||
if code != 404 {
|
||||
return Err(RPCError::HttpError(code));
|
||||
}
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
payloads.push(chunk);
|
||||
}
|
||||
Ok(payloads)
|
||||
}
|
||||
|
||||
/// query the replica for zero or more latest chunks
|
||||
fn get_latest_chunks(&mut self, slot_ids: &[u32]) -> Result<Vec<Option<Vec<u8>>>, RPCError> {
|
||||
let mut payloads = vec![];
|
||||
for slot_id in slot_ids.iter() {
|
||||
let path = stackerdb_get_chunk_path(self.stackerdb_contract_id.clone(), *slot_id, None);
|
||||
let chunk = match self.run_http_request("GET", &path, None, &[]) {
|
||||
Ok(body_bytes) => Some(body_bytes),
|
||||
Err(RPCError::HttpError(code)) => {
|
||||
if code != 404 {
|
||||
return Err(RPCError::HttpError(code));
|
||||
}
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
payloads.push(chunk);
|
||||
}
|
||||
Ok(payloads)
|
||||
}
|
||||
|
||||
/// upload a chunk
|
||||
fn put_chunk(&mut self, chunk: StackerDBChunkData) -> Result<StackerDBChunkAckData, RPCError> {
|
||||
let body =
|
||||
serde_json::to_vec(&chunk).map_err(|e| RPCError::Deserialize(format!("{:?}", &e)))?;
|
||||
let path = stackerdb_post_chunk_path(self.stackerdb_contract_id.clone());
|
||||
let resp_bytes = self.run_http_request("POST", &path, Some("application/json"), &body)?;
|
||||
let ack: StackerDBChunkAckData = serde_json::from_slice(&resp_bytes)
|
||||
.map_err(|e| RPCError::Deserialize(format!("{:?}", &e)))?;
|
||||
Ok(ack)
|
||||
}
|
||||
}
|
||||
0
stacks-signer/src/tests/config.rs
Normal file
0
stacks-signer/src/tests/config.rs
Normal file
18
stacks-signer/src/tests/mod.rs
Normal file
18
stacks-signer/src/tests/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
|
||||
// Copyright (C) 2020-2023 Stacks Open Internet Foundation
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
pub mod config;
|
||||
pub mod rpc;
|
||||
0
stacks-signer/src/tests/rpc.rs
Normal file
0
stacks-signer/src/tests/rpc.rs
Normal file
Reference in New Issue
Block a user