diff --git a/stackslib/src/net/http.rs b/stackslib/src/net/http.rs
index 116813cd6..87b68a30a 100644
--- a/stackslib/src/net/http.rs
+++ b/stackslib/src/net/http.rs
@@ -1,21 +1,18 @@
-/*
- copyright: (c) 2013-2020 by Blockstack PBC, a public benefit corporation.
-
- This file is part of Blockstack.
-
- Blockstack is free software. You may redistribute 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.
-
- Blockstack is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY, including without 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 Blockstack. If not, see .
-*/
+// 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, HashSet};
use std::convert::TryFrom;
@@ -44,7 +41,10 @@ use crate::chainstate::stacks::{
};
use crate::deps::httparse;
use crate::net::atlas::Attachment;
+use crate::net::stackerdb::STACKERDB_MAX_CHUNK_SIZE;
use crate::net::ClientError;
+use crate::net::ContractId;
+use crate::net::ContractIdExtension;
use crate::net::Error as net_error;
use crate::net::Error::ClarityError;
use crate::net::ExtendedStacksHeader;
@@ -62,6 +62,7 @@ use crate::net::NeighborAddress;
use crate::net::PeerAddress;
use crate::net::PeerHost;
use crate::net::ProtocolFamily;
+use crate::net::StackerDBChunkData;
use crate::net::StacksHttpMessage;
use crate::net::StacksHttpPreamble;
use crate::net::UnconfirmedTransactionResponse;
@@ -163,6 +164,26 @@ lazy_static! {
Regex::new(r#"^/v2/attachments/([0-9a-f]{40})$"#).unwrap();
static ref PATH_POST_MEMPOOL_QUERY: Regex =
Regex::new(r#"^/v2/mempool/query$"#).unwrap();
+ static ref PATH_GET_STACKERDB_METADATA: Regex =
+ Regex::new(&format!(
+ r#"^/v2/stackerdb/(?P
{})/(?P{})$"#,
+ *STANDARD_PRINCIPAL_REGEX_STRING, *CONTRACT_NAME_REGEX_STRING
+ )).unwrap();
+ static ref PATH_GET_STACKERDB_CHUNK: Regex =
+ Regex::new(&format!(
+ r#"^/v2/stackerdb/(?P{})/(?P{})/([0-9]+)$"#,
+ *STANDARD_PRINCIPAL_REGEX_STRING, *CONTRACT_NAME_REGEX_STRING
+ )).unwrap();
+ static ref PATH_GET_STACKERDB_VERSIONED_CHUNK: Regex =
+ Regex::new(&format!(
+ r#"^/v2/stackerdb/(?P{})/(?P{})/([0-9]+)/([0-9]+)$"#,
+ *STANDARD_PRINCIPAL_REGEX_STRING, *CONTRACT_NAME_REGEX_STRING
+ )).unwrap();
+ static ref PATH_POST_STACKERDB_CHUNK: Regex =
+ Regex::new(&format!(
+ r#"/v2/stackerdb/(?P{})/(?P{})/chunks$"#,
+ *STANDARD_PRINCIPAL_REGEX_STRING, *CONTRACT_NAME_REGEX_STRING
+ )).unwrap();
static ref PATH_OPTIONS_WILDCARD: Regex = Regex::new("^/v2/.{0,4096}$").unwrap();
}
@@ -1612,6 +1633,26 @@ impl HttpRequestType {
&PATH_POST_MEMPOOL_QUERY,
&HttpRequestType::parse_post_mempool_query,
),
+ (
+ "GET",
+ &PATH_GET_STACKERDB_METADATA,
+ &HttpRequestType::parse_get_stackerdb_metadata,
+ ),
+ (
+ "GET",
+ &PATH_GET_STACKERDB_CHUNK,
+ &HttpRequestType::parse_get_stackerdb_chunk,
+ ),
+ (
+ "GET",
+ &PATH_GET_STACKERDB_VERSIONED_CHUNK,
+ &HttpRequestType::parse_get_stackerdb_versioned_chunk,
+ ),
+ (
+ "POST",
+ &PATH_POST_STACKERDB_CHUNK,
+ &HttpRequestType::parse_post_stackerdb_chunk,
+ ),
];
// use url::Url to parse path and query string
@@ -2425,7 +2466,8 @@ impl HttpRequestType {
preamble: &HttpRequestPreamble,
fd: &mut R,
) -> Result {
- let body: PostTransactionRequestBody = serde_json::from_reader(fd)
+ let mut bound_fd = BoundReader::from_reader(fd, preamble.get_content_length() as u64);
+ let body: PostTransactionRequestBody = serde_json::from_reader(&mut bound_fd)
.map_err(|_e| net_error::DeserializeError("Failed to parse body".into()))?;
let tx = {
@@ -2710,6 +2752,155 @@ impl HttpRequestType {
))
}
+ fn parse_get_stackerdb_metadata(
+ _protocol: &mut StacksHttp,
+ preamble: &HttpRequestPreamble,
+ regex: &Captures,
+ _query: Option<&str>,
+ _fd: &mut R,
+ ) -> Result {
+ if preamble.get_content_length() != 0 {
+ return Err(net_error::DeserializeError(
+ "Invalid Http request: expected 0-length body".to_string(),
+ ));
+ }
+
+ HttpRequestType::parse_get_contract_arguments(preamble, regex).map(
+ |(preamble, addr, name)| {
+ let contract_id = ContractId::from_parts(addr, name);
+ HttpRequestType::GetStackerDBMetadata(preamble, contract_id)
+ },
+ )
+ }
+
+ fn parse_get_stackerdb_chunk(
+ _protocol: &mut StacksHttp,
+ preamble: &HttpRequestPreamble,
+ regex: &Captures,
+ _query: Option<&str>,
+ _fd: &mut R,
+ ) -> Result {
+ if preamble.get_content_length() != 0 {
+ return Err(net_error::DeserializeError(
+ "Invalid Http request: expected 0-length body".to_string(),
+ ));
+ }
+
+ let slot_id: u32 = regex
+ .get(6)
+ .ok_or(net_error::DeserializeError(
+ "Failed to match slot ID".to_string(),
+ ))?
+ .as_str()
+ .parse()
+ .map_err(|_| net_error::DeserializeError("Failed to decode slot ID".to_string()))?;
+
+ HttpRequestType::parse_get_contract_arguments(preamble, regex).map(
+ |(preamble, addr, name)| {
+ let contract_id = ContractId::from_parts(addr, name);
+ HttpRequestType::GetStackerDBChunk(preamble, contract_id, slot_id, None)
+ },
+ )
+ }
+
+ fn parse_get_stackerdb_versioned_chunk(
+ _protocol: &mut StacksHttp,
+ preamble: &HttpRequestPreamble,
+ regex: &Captures,
+ _query: Option<&str>,
+ _fd: &mut R,
+ ) -> Result {
+ if preamble.get_content_length() != 0 {
+ return Err(net_error::DeserializeError(
+ "Invalid Http request: expected 0-length body".to_string(),
+ ));
+ }
+
+ let slot_id: u32 = regex
+ .get(6)
+ .ok_or(net_error::DeserializeError(
+ "Failed to match slot ID".to_string(),
+ ))?
+ .as_str()
+ .parse()
+ .map_err(|_| net_error::DeserializeError("Failed to decode slot ID".to_string()))?;
+
+ let version: u32 = regex
+ .get(7)
+ .ok_or(net_error::DeserializeError(
+ "Failed to match slot version".to_string(),
+ ))?
+ .as_str()
+ .parse()
+ .map_err(|_| {
+ net_error::DeserializeError("Failed to decode slot version".to_string())
+ })?;
+
+ HttpRequestType::parse_get_contract_arguments(preamble, regex).map(
+ |(preamble, addr, name)| {
+ let contract_id = ContractId::from_parts(addr, name);
+ HttpRequestType::GetStackerDBChunk(preamble, contract_id, slot_id, Some(version))
+ },
+ )
+ }
+
+ fn parse_post_stackerdb_chunk(
+ _protocol: &mut StacksHttp,
+ preamble: &HttpRequestPreamble,
+ regex: &Captures,
+ _query: Option<&str>,
+ fd: &mut R,
+ ) -> Result {
+ if preamble.get_content_length() == 0 {
+ return Err(net_error::DeserializeError(
+ "Invalid Http request: expected non-zero-length body for PostStackerDBChunk"
+ .to_string(),
+ ));
+ }
+
+ if preamble.get_content_length() > MAX_PAYLOAD_LEN {
+ return Err(net_error::DeserializeError(
+ "Invalid Http request: PostStackerDBChunk body is too big".to_string(),
+ ));
+ }
+
+ // content-type must be given, and must be application/json
+ match preamble.content_type {
+ None => {
+ return Err(net_error::DeserializeError(
+ "Missing Content-Type for stackerdb chunk".to_string(),
+ ));
+ }
+ Some(ref c) => {
+ if *c != HttpContentType::JSON {
+ return Err(net_error::DeserializeError(
+ "Wrong Content-Type for stackerdb; expected application/json".to_string(),
+ ));
+ }
+ }
+ };
+
+ let contract_addr = StacksAddress::from_string(®ex["address"]).ok_or_else(|| {
+ net_error::DeserializeError("Failed to parse contract address".into())
+ })?;
+ let contract_name = ContractName::try_from(regex["contract"].to_string())
+ .map_err(|_e| net_error::DeserializeError("Failed to parse contract name".into()))?;
+
+ let contract_id = ContractId::from_parts(contract_addr, contract_name);
+
+ let mut bound_fd = BoundReader::from_reader(fd, preamble.get_content_length() as u64);
+ let chunk_data: StackerDBChunkData =
+ serde_json::from_reader(&mut bound_fd).map_err(|_e| {
+ net_error::DeserializeError("Failed to parse StackerDB chunk body".into())
+ })?;
+
+ Ok(HttpRequestType::PostStackerDBChunk(
+ HttpRequestMetadata::from_preamble(preamble),
+ contract_id,
+ chunk_data,
+ ))
+ }
+
fn parse_options_preflight(
_protocol: &mut StacksHttp,
preamble: &HttpRequestPreamble,
@@ -2751,6 +2942,9 @@ impl HttpRequestType {
HttpRequestType::GetAttachment(ref md, ..) => md,
HttpRequestType::MemPoolQuery(ref md, ..) => md,
HttpRequestType::FeeRateEstimate(ref md, _, _) => md,
+ HttpRequestType::GetStackerDBMetadata(ref md, ..) => md,
+ HttpRequestType::GetStackerDBChunk(ref md, ..) => md,
+ HttpRequestType::PostStackerDBChunk(ref md, ..) => md,
HttpRequestType::ClientError(ref md, ..) => md,
}
}
@@ -2783,6 +2977,9 @@ impl HttpRequestType {
HttpRequestType::GetAttachment(ref mut md, ..) => md,
HttpRequestType::MemPoolQuery(ref mut md, ..) => md,
HttpRequestType::FeeRateEstimate(ref mut md, _, _) => md,
+ HttpRequestType::GetStackerDBMetadata(ref mut md, ..) => md,
+ HttpRequestType::GetStackerDBChunk(ref mut md, ..) => md,
+ HttpRequestType::PostStackerDBChunk(ref mut md, ..) => md,
HttpRequestType::ClientError(ref mut md, ..) => md,
}
}
@@ -2965,6 +3162,36 @@ impl HttpRequestType {
}
None => "/v2/mempool/query".to_string(),
},
+ HttpRequestType::GetStackerDBMetadata(_, contract_id) => format!(
+ "/v2/stackerdb/{}/{}",
+ &contract_id.address(),
+ &contract_id.name()
+ ),
+ HttpRequestType::GetStackerDBChunk(_, contract_id, slot_id, slot_version_opt) => {
+ if let Some(version) = slot_version_opt {
+ format!(
+ "/v2/stackerdb/{}/{}/{}/{}",
+ &contract_id.address(),
+ &contract_id.name(),
+ slot_id,
+ version
+ )
+ } else {
+ format!(
+ "/v2/stackerdb/{}/{}/{}",
+ &contract_id.address(),
+ &contract_id.name(),
+ slot_id
+ )
+ }
+ }
+ HttpRequestType::PostStackerDBChunk(_, contract_id, ..) => {
+ format!(
+ "/v2/stackerdb/{}/{}/chunks",
+ &contract_id.address(),
+ &contract_id.name()
+ )
+ }
HttpRequestType::FeeRateEstimate(_, _, _) => self.get_path().to_string(),
HttpRequestType::ClientError(_md, e) => match e {
ClientError::NotFound(path) => path.to_string(),
@@ -3008,6 +3235,13 @@ impl HttpRequestType {
HttpRequestType::GetIsTraitImplemented(..) => "/v2/traits/:principal/:contract_name",
HttpRequestType::MemPoolQuery(..) => "/v2/mempool/query",
HttpRequestType::FeeRateEstimate(_, _, _) => "/v2/fees/transaction",
+ HttpRequestType::GetStackerDBMetadata(..) => "/v2/stackerdb/:principal/:contract_name",
+ HttpRequestType::GetStackerDBChunk(..) => {
+ "/v2/stackerdb/:principal/:contract_name/:slot_id(/:slot_version)?"
+ }
+ HttpRequestType::PostStackerDBChunk(..) => {
+ "/v2/stackerdb/:principal/:contract_name/chunks"
+ }
HttpRequestType::OptionsPreflight(..) | HttpRequestType::ClientError(..) => "/",
}
}
@@ -3181,6 +3415,28 @@ impl HttpRequestType {
fd.write_all(&request_body_bytes)
.map_err(net_error::WriteError)?;
}
+ HttpRequestType::PostStackerDBChunk(md, _, request) => {
+ let mut request_body_bytes = vec![];
+ serde_json::to_writer(&mut request_body_bytes, request).map_err(|e| {
+ net_error::SerializeError(format!(
+ "Failed to serialize StackerDB POST chunk to JSON: {:?}",
+ &e
+ ))
+ })?;
+ HttpRequestPreamble::new_serialized(
+ fd,
+ &md.version,
+ "POST",
+ &self.request_path(),
+ &md.peer,
+ md.keep_alive,
+ Some(request_body_bytes.len() as u32),
+ Some(&HttpContentType::JSON),
+ |fd| stacks_height_headers(fd, md),
+ )?;
+ fd.write_all(&request_body_bytes)
+ .map_err(net_error::WriteError)?;
+ }
other_type => {
let md = other_type.metadata();
let request_path = other_type.request_path();
@@ -3491,6 +3747,22 @@ impl HttpResponseType {
&PATH_POST_MEMPOOL_QUERY,
&HttpResponseType::parse_post_mempool_query,
),
+ (
+ &PATH_GET_STACKERDB_METADATA,
+ &HttpResponseType::parse_get_stackerdb_metadata,
+ ),
+ (
+ &PATH_GET_STACKERDB_CHUNK,
+ &HttpResponseType::parse_get_stackerdb_chunk,
+ ),
+ (
+ &PATH_GET_STACKERDB_VERSIONED_CHUNK,
+ &HttpResponseType::parse_get_stackerdb_chunk,
+ ),
+ (
+ &PATH_POST_STACKERDB_CHUNK,
+ &HttpResponseType::parse_stackerdb_chunk_response,
+ ),
];
// use url::Url to parse path and query string
@@ -4041,6 +4313,55 @@ impl HttpResponseType {
))
}
+ fn parse_get_stackerdb_metadata(
+ _protocol: &mut StacksHttp,
+ request_version: HttpVersion,
+ preamble: &HttpResponsePreamble,
+ fd: &mut R,
+ len_hint: Option,
+ ) -> Result {
+ let slot_metadata =
+ HttpResponseType::parse_json(preamble, fd, len_hint, MAX_MESSAGE_LEN as u64)?;
+ Ok(HttpResponseType::StackerDBMetadata(
+ HttpResponseMetadata::from_preamble(request_version, preamble),
+ slot_metadata,
+ ))
+ }
+
+ fn parse_get_stackerdb_chunk(
+ _protocol: &mut StacksHttp,
+ request_version: HttpVersion,
+ preamble: &HttpResponsePreamble,
+ fd: &mut R,
+ len_hint: Option,
+ ) -> Result {
+ let chunk = HttpResponseType::parse_bytestream(
+ preamble,
+ fd,
+ len_hint,
+ STACKERDB_MAX_CHUNK_SIZE as u64,
+ )?;
+ Ok(HttpResponseType::StackerDBChunk(
+ HttpResponseMetadata::from_preamble(request_version, preamble),
+ chunk,
+ ))
+ }
+
+ fn parse_stackerdb_chunk_response(
+ _protocol: &mut StacksHttp,
+ request_version: HttpVersion,
+ preamble: &HttpResponsePreamble,
+ fd: &mut R,
+ len_hint: Option,
+ ) -> Result {
+ let slot_ack =
+ HttpResponseType::parse_json(preamble, fd, len_hint, MAX_MESSAGE_LEN as u64)?;
+ Ok(HttpResponseType::StackerDBChunkAck(
+ HttpResponseMetadata::from_preamble(request_version, preamble),
+ slot_ack,
+ ))
+ }
+
fn error_reason(code: u16) -> &'static str {
match code {
400 => "Bad Request",
@@ -4105,6 +4426,9 @@ impl HttpResponseType {
HttpResponseType::MemPoolTxs(ref md, ..) => md,
HttpResponseType::OptionsPreflight(ref md) => md,
HttpResponseType::TransactionFeeEstimation(ref md, _) => md,
+ HttpResponseType::StackerDBMetadata(ref md, ..) => md,
+ HttpResponseType::StackerDBChunk(ref md, ..) => md,
+ HttpResponseType::StackerDBChunkAck(ref md, ..) => md,
// errors
HttpResponseType::BadRequestJSON(ref md, _) => md,
HttpResponseType::BadRequest(ref md, _) => md,
@@ -4406,6 +4730,42 @@ impl HttpResponseType {
None => HttpResponseType::send_bytestream(protocol, md, fd, txs),
}?;
}
+ HttpResponseType::StackerDBMetadata(ref md, ref slot_metadata) => {
+ HttpResponsePreamble::new_serialized(
+ fd,
+ 200,
+ "OK",
+ md.content_length.clone(),
+ &HttpContentType::JSON,
+ md.request_id,
+ |ref mut fd| keep_alive_headers(fd, md),
+ )?;
+ HttpResponseType::send_json(protocol, md, fd, slot_metadata)?;
+ }
+ HttpResponseType::StackerDBChunk(ref md, ref chunk, ..) => {
+ HttpResponsePreamble::new_serialized(
+ fd,
+ 200,
+ "OK",
+ md.content_length.clone(),
+ &HttpContentType::Bytes,
+ md.request_id,
+ |ref mut fd| keep_alive_headers(fd, md),
+ )?;
+ HttpResponseType::send_bytestream(protocol, md, fd, chunk)?;
+ }
+ HttpResponseType::StackerDBChunkAck(ref md, ref ack_data) => {
+ HttpResponsePreamble::new_serialized(
+ fd,
+ 200,
+ "OK",
+ md.content_length.clone(),
+ &HttpContentType::JSON,
+ md.request_id,
+ |ref mut fd| keep_alive_headers(fd, md),
+ )?;
+ HttpResponseType::send_json(protocol, md, fd, ack_data)?;
+ }
HttpResponseType::OptionsPreflight(ref md) => {
HttpResponsePreamble::new_serialized(
fd,
@@ -4525,6 +4885,9 @@ impl MessageSequence for StacksHttpMessage {
HttpRequestType::GetAttachmentsInv(..) => "HTTP(GetAttachmentsInv)",
HttpRequestType::MemPoolQuery(..) => "HTTP(MemPoolQuery)",
HttpRequestType::OptionsPreflight(..) => "HTTP(OptionsPreflight)",
+ HttpRequestType::GetStackerDBMetadata(..) => "HTTP(GetStackerDBMetadata)",
+ HttpRequestType::GetStackerDBChunk(..) => "HTTP(GetStackerDBChunk)",
+ HttpRequestType::PostStackerDBChunk(..) => "HTTP(PostStackerDBChunk)",
HttpRequestType::ClientError(..) => "HTTP(ClientError)",
HttpRequestType::FeeRateEstimate(_, _, _) => "HTTP(FeeRateEstimate)",
},
@@ -4555,6 +4918,9 @@ impl MessageSequence for StacksHttpMessage {
HttpResponseType::UnconfirmedTransaction(_, _) => "HTTP(UnconfirmedTransaction)",
HttpResponseType::MemPoolTxStream(..) => "HTTP(MemPoolTxStream)",
HttpResponseType::MemPoolTxs(..) => "HTTP(MemPoolTxs)",
+ HttpResponseType::StackerDBMetadata(..) => "HTTP(StackerDBMetadata)",
+ HttpResponseType::StackerDBChunk(..) => "HTTP(StackerDBChunk)",
+ HttpResponseType::StackerDBChunkAck(..) => "HTTP(StackerDBChunkAck)",
HttpResponseType::OptionsPreflight(_) => "HTTP(OptionsPreflight)",
HttpResponseType::BadRequestJSON(..) | HttpResponseType::BadRequest(..) => {
"HTTP(400)"
@@ -6216,6 +6582,7 @@ mod test {
)
.unwrap(),
authenticated: true,
+ stackerdbs: Some(vec![]),
},
RPCNeighbor {
network_id: 3,
@@ -6230,6 +6597,7 @@ mod test {
)
.unwrap(),
authenticated: false,
+ stackerdbs: Some(vec![]),
},
],
inbound: vec![],