feat: prototype stacks-signer daemon (just a CLI front-end to stackerdb for now)

This commit is contained in:
Jude Nelson
2023-08-18 16:15:45 -04:00
parent de927bda9d
commit 8b22d19d2b
9 changed files with 904 additions and 0 deletions

19
Cargo.lock generated
View File

@@ -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"

View File

@@ -7,4 +7,5 @@ members = [
"clarity",
"stx-genesis",
"libstackerdb",
"stacks-signer",
"testnet/stacks-node"]

42
stacks-signer/Cargo.toml Normal file
View 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
View 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
View 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
View 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)
}
}

View File

View 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;

View File