diff --git a/Cargo.lock b/Cargo.lock index e1188ea0b..32f6fa339 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index aaff58950..3c9ee8ad0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,5 @@ members = [ "clarity", "stx-genesis", "libstackerdb", + "stacks-signer", "testnet/stacks-node"] diff --git a/stacks-signer/Cargo.toml b/stacks-signer/Cargo.toml new file mode 100644 index 000000000..f3f2d18b7 --- /dev/null +++ b/stacks-signer/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "stacks-signer" +version = "0.0.1" +authors = [ "Jude Nelson " ] +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" } diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs new file mode 100644 index 000000000..0d9156bdb --- /dev/null +++ b/stacks-signer/src/config.rs @@ -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 . + +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 { + 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 { + let data = fs::read_to_string(path) + .map_err(|_| ConfigError::NoSuchConfigFile(path.to_string()))?; + Self::load_from_str(&data) + } +} + +impl TryFrom 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 { + 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 { + RawConfigFile::load_from_str(data)?.try_into() + } + + /// load the config from a file and parse it + pub fn load_from_file(path: &str) -> Result { + let data = fs::read_to_string(path) + .map_err(|_| ConfigError::NoSuchConfigFile(path.to_string()))?; + Self::load_from_str(&data) + } +} diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs new file mode 100644 index 000000000..f00808410 --- /dev/null +++ b/stacks-signer/src/main.rs @@ -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 . + +#[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, + argnames: &[&str], + has_optarg: bool, +) -> Result, 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::>()[0] + ); + process::exit(1); +} + +/// Get -h,--host and -c,--contract +fn parse_host_and_contract(argv: &mut Vec) -> (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) { + 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) { + 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) { + 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) { + 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 = 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))); + } + } +} diff --git a/stacks-signer/src/rpc.rs b/stacks-signer/src/rpc.rs new file mode 100644 index 000000000..6d58229e0 --- /dev/null +++ b/stacks-signer/src/rpc.rs @@ -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 . + +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 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, RPCError>; + fn get_chunks( + &mut self, + slots_and_versions: &[(u32, u32)], + ) -> Result>>, RPCError>; + fn get_latest_chunks(&mut self, slot_ids: &[u32]) -> Result>>, RPCError>; + fn put_chunk(&mut self, chunk: StackerDBChunkData) -> Result; + + /// 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>, 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>, 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, +} + +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(&mut self, todo: F) -> Result + 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, 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 = 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, 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, RPCError> { + let bytes = self.run_http_request( + "GET", + &stackerdb_get_metadata_path(self.stackerdb_contract_id.clone()), + None, + &[], + )?; + let metadata: Vec = 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>>, 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>>, 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 { + 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) + } +} diff --git a/stacks-signer/src/tests/config.rs b/stacks-signer/src/tests/config.rs new file mode 100644 index 000000000..e69de29bb diff --git a/stacks-signer/src/tests/mod.rs b/stacks-signer/src/tests/mod.rs new file mode 100644 index 000000000..3c2539a8e --- /dev/null +++ b/stacks-signer/src/tests/mod.rs @@ -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 . + +pub mod config; +pub mod rpc; diff --git a/stacks-signer/src/tests/rpc.rs b/stacks-signer/src/tests/rpc.rs new file mode 100644 index 000000000..e69de29bb