diff --git a/docs/rpc-endpoints.md b/docs/rpc-endpoints.md index 6163f27b75..117d74d0dc 100644 --- a/docs/rpc-endpoints.md +++ b/docs/rpc-endpoints.md @@ -172,6 +172,35 @@ Where data is the hex serialization of the variable value. This endpoint also accepts a querystring parameter `?proof=` which when supplied `0`, will return the JSON object _without_ the `proof` field. +### GET /v2/clarity_marf_value/[Clarity MARF Key] +Attempt to fetch the value of a MARF key. The key is identified with [Clarity MARF Key]. + +Returns JSON data in the form: + +```json +{ + "data": "0x01ce...", + "proof": "0x01ab...", +} +``` + +Where data is the hex serialization of the value. + +### GET /v2/clarity_metadata/[Stacks Address]/[Contract Name]/[Clarity Metadata Key] +Attempt to fetch the metadata of a contract. + The contract is identified with [Stacks Address] and [Contract Name] in the URL path. + The metadata key is identified with [Clarity Metadata Key]. + +Returns JSON data in the form: + +```json +{ + "data": "'{\"contract_identifier\":{...}'", +} +``` + +Where data is the metadata formatted as a JSON string. + ### GET /v2/constant_val/[Stacks Address]/[Contract Name]/[Constant Name] Attempt to fetch a constant from a contract. The contract is identified with [Stacks Address] and [Contract Name] in the URL path. The constant is identified with [Constant Name]. diff --git a/docs/rpc/api/core-node/get-clarity-marf-value.example.json b/docs/rpc/api/core-node/get-clarity-marf-value.example.json new file mode 100644 index 0000000000..d0e233416f --- /dev/null +++ b/docs/rpc/api/core-node/get-clarity-marf-value.example.json @@ -0,0 +1,4 @@ +{ + "data": "0x0a0c000000010a6d6f6e737465722d69640100000000000000000000000000000001", + "proof": "0x123..." +} diff --git a/docs/rpc/api/core-node/get-clarity-marf-value.schema.json b/docs/rpc/api/core-node/get-clarity-marf-value.schema.json new file mode 100644 index 0000000000..ea7e7894fb --- /dev/null +++ b/docs/rpc/api/core-node/get-clarity-marf-value.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Response of get Clarity MARF value request", + "title": "ClarityMARFValueResponse", + "type": "object", + "required": ["data"], + "properties": { + "data": { + "type": "string", + "description": "Hex-encoded string" + }, + "proof": { + "type": "string", + "description": "Hex-encoded string of the MARF proof for the data" + } + } +} diff --git a/docs/rpc/api/core-node/get-clarity-metadata.example.json b/docs/rpc/api/core-node/get-clarity-metadata.example.json new file mode 100644 index 0000000000..5bb4bd5c47 --- /dev/null +++ b/docs/rpc/api/core-node/get-clarity-metadata.example.json @@ -0,0 +1,3 @@ +{ + "data": "'{\"contract_identifier\":{...}, \"private_function_types\":{...}'" +} diff --git a/docs/rpc/api/core-node/get-clarity-metadata.schema.json b/docs/rpc/api/core-node/get-clarity-metadata.schema.json new file mode 100644 index 0000000000..3c0104fa41 --- /dev/null +++ b/docs/rpc/api/core-node/get-clarity-metadata.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Response of get clarity metadata request", + "title": "ClarityMetadataResponse", + "type": "object", + "required": ["data"], + "properties": { + "data": { + "type": "string", + "description": "Metadata value formatted as a JSON string" + } + } +} diff --git a/docs/rpc/openapi.yaml b/docs/rpc/openapi.yaml index f33e0dca73..e77fc31ade 100644 --- a/docs/rpc/openapi.yaml +++ b/docs/rpc/openapi.yaml @@ -519,7 +519,94 @@ paths: description: | The Stacks chain tip to query from. If tip == "latest", the query will be run from the latest known tip (includes unconfirmed state). - If the tip is left unspecified, the stacks chain tip will be selected (only includes confirmed state). + If the tip is left unspecified, the stacks chain tip will be selected (only includes confirmed state). + + /v2/clarity_marf_value/{clarity_marf_key}: + post: + summary: Get the MARF value for a given key + tags: + - Smart Contracts + operationId: get_clarity_marf_value + description: | + Attempt to fetch the value of a MARF key. The key is identified with [Clarity MARF Key]. + + In the response, `data` is the hex serialization of the value. + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: ./api/core-node/get-clarity-marf-value.schema.json + example: + $ref: ./api/core-node/get-clarity-marf-value.example.json + 400: + description: Failed to retrieve MARF key + parameters: + - name: clarity_marf_key + in: path + required: true + description: MARF key + schema: + type: string + - name: proof + in: query + description: Returns object without the proof field when set to 0 + schema: + type: integer + - name: tip + in: query + schema: + type: string + description: The Stacks chain tip to query from. If tip == latest, the query will be run from the latest + known tip (includes unconfirmed state). + + /v2/clarity_metadata/{contract_address}/{contract_name}/{clarity_metadata_key}: + post: + summary: Get the contract metadata for the metadata key + tags: + - Smart Contracts + operationId: get_clarity_metadata_key + description: | + Attempt to fetch the metadata of a contract. The contract is identified with [Stacks Address] and [Contract Name] in the URL path. The metadata key is identified with [Clarity Metadata Key]. + + In the response, `data` is formatted as JSON. + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: ./api/core-node/get-clarity-metadata.schema.json + example: + $ref: ./api/core-node/get-clarity-metadata.example.json + 400: + description: Failed to retrieve constant value from contract + parameters: + - name: contract_address + in: path + required: true + description: Stacks address + schema: + type: string + - name: contract_name + in: path + required: true + description: Contract name + schema: + type: string + - name: clarity_metadata_key + in: path + required: true + description: Metadata key + schema: + type: string + - name: tip + in: query + schema: + type: string + description: The Stacks chain tip to query from. If tip == latest, the query will be run from the latest + known tip (includes unconfirmed state). /v2/constant_val/{contract_address}/{contract_name}/{constant_name}: post: @@ -633,7 +720,7 @@ paths: /v3/blocks/{block_id}: get: - summary: Fetch a Nakamoto block + summary: Fetch a Nakamoto block tags: - Blocks operationId: get_block_v3 @@ -674,7 +761,7 @@ paths: application/json: example: $ref: ./api/core-node/get_tenure_info.json - + /v3/tenures/{block_id}: get: summary: Fetch a sequence of Nakamoto blocks in a tenure diff --git a/stackslib/src/net/api/getclaritymarfvalue.rs b/stackslib/src/net/api/getclaritymarfvalue.rs new file mode 100644 index 0000000000..ff584a0ccf --- /dev/null +++ b/stackslib/src/net/api/getclaritymarfvalue.rs @@ -0,0 +1,232 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 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 clarity::vm::clarity::ClarityConnection; +use clarity::vm::representations::CONTRACT_PRINCIPAL_REGEX_STRING; +use lazy_static::lazy_static; +use regex::{Captures, Regex}; +use stacks_common::types::net::PeerHost; +use stacks_common::util::hash::to_hex; + +use crate::net::http::{ + parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble, + HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, +}; +use crate::net::httpcore::{ + request, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, + StacksHttpRequest, StacksHttpResponse, +}; +use crate::net::{Error as NetError, StacksNodeState, TipRequest}; + +lazy_static! { + static ref CLARITY_NAME_NO_BOUNDARIES_REGEX_STRING: String = + "[a-zA-Z]([a-zA-Z0-9]|[-_!?+<>=/*])*|[-+=/*]|[<>]=?".into(); + static ref MARF_KEY_FOR_TRIP_REGEX_STRING: String = format!( + r"vm::{}::\d+::({})", + *CONTRACT_PRINCIPAL_REGEX_STRING, *CLARITY_NAME_NO_BOUNDARIES_REGEX_STRING, + ); + static ref MARF_KEY_FOR_QUAD_REGEX_STRING: String = + format!(r"{}::[0-9a-fA-F]+", *MARF_KEY_FOR_TRIP_REGEX_STRING,); +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ClarityMarfResponse { + pub data: String, + #[serde(rename = "proof")] + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub marf_proof: Option, +} + +#[derive(Clone)] +pub struct RPCGetClarityMarfRequestHandler { + pub clarity_marf_key: Option, +} +impl RPCGetClarityMarfRequestHandler { + pub fn new() -> Self { + Self { + clarity_marf_key: None, + } + } +} + +/// Decode the HTTP request +impl HttpRequest for RPCGetClarityMarfRequestHandler { + fn verb(&self) -> &'static str { + "GET" + } + + fn path_regex(&self) -> Regex { + Regex::new(&format!( + r"^/v2/clarity/marf/(?P(vm-epoch::epoch-version)|({})|({}))$", + *MARF_KEY_FOR_TRIP_REGEX_STRING, *MARF_KEY_FOR_QUAD_REGEX_STRING + )) + .unwrap() + } + + fn metrics_identifier(&self) -> &str { + "/v2/clarity/marf/:clarity_marf_key" + } + + /// Try to decode this request. + /// There's nothing to load here, so just make sure the request is well-formed. + fn try_parse_request( + &mut self, + preamble: &HttpRequestPreamble, + captures: &Captures, + query: Option<&str>, + _body: &[u8], + ) -> Result { + if preamble.get_content_length() != 0 { + return Err(Error::DecodeError( + "Invalid Http request: expected 0-length body".to_string(), + )); + } + + let marf_key = if let Some(key_str) = captures.name("clarity_marf_key") { + key_str.as_str().to_string() + } else { + return Err(Error::Http(404, "Missing `clarity_marf_key`".to_string())); + }; + + self.clarity_marf_key = Some(marf_key); + + let contents = HttpRequestContents::new().query_string(query); + Ok(contents) + } +} + +/// Handle the HTTP request +impl RPCRequestHandler for RPCGetClarityMarfRequestHandler { + /// Reset internal state + fn restart(&mut self) { + self.clarity_marf_key = None; + } + + /// Make the response + fn try_handle_request( + &mut self, + preamble: HttpRequestPreamble, + contents: HttpRequestContents, + node: &mut StacksNodeState, + ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> { + let clarity_marf_key = self.clarity_marf_key.take().ok_or(NetError::SendError( + "`clarity_marf_key` not set".to_string(), + ))?; + + let tip = match node.load_stacks_chain_tip(&preamble, &contents) { + Ok(tip) => tip, + Err(error_resp) => { + return error_resp.try_into_contents().map_err(NetError::from); + } + }; + + let with_proof = contents.get_with_proof(); + + let data_opt = node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| { + chainstate.maybe_read_only_clarity_tx( + &sortdb.index_handle_at_block(chainstate, &tip)?, + &tip, + |clarity_tx| { + clarity_tx.with_clarity_db_readonly(|clarity_db| { + let (value_hex, marf_proof): (String, _) = if with_proof { + clarity_db + .get_data_with_proof(&clarity_marf_key) + .ok() + .flatten() + .map(|(a, b)| (a, Some(format!("0x{}", to_hex(&b)))))? + } else { + clarity_db + .get_data(&clarity_marf_key) + .ok() + .flatten() + .map(|a| (a, None))? + }; + + let data = format!("0x{}", value_hex); + Some(ClarityMarfResponse { data, marf_proof }) + }) + }, + ) + }); + + let data_resp = match data_opt { + Ok(Some(Some(data))) => data, + Ok(Some(None)) => { + return StacksHttpResponse::new_error( + &preamble, + &HttpNotFound::new("Marf key not found".to_string()), + ) + .try_into_contents() + .map_err(NetError::from); + } + Ok(None) | Err(_) => { + return StacksHttpResponse::new_error( + &preamble, + &HttpNotFound::new("Chain tip not found".to_string()), + ) + .try_into_contents() + .map_err(NetError::from); + } + }; + + let mut preamble = HttpResponsePreamble::ok_json(&preamble); + preamble.set_canonical_stacks_tip_height(Some(node.canonical_stacks_tip_height())); + let body = HttpResponseContents::try_from_json(&data_resp)?; + Ok((preamble, body)) + } +} + +/// Decode the HTTP response +impl HttpResponse for RPCGetClarityMarfRequestHandler { + fn try_parse_response( + &self, + preamble: &HttpResponsePreamble, + body: &[u8], + ) -> Result { + let marf_value: ClarityMarfResponse = parse_json(preamble, body)?; + Ok(HttpResponsePayload::try_from_json(marf_value)?) + } +} + +impl StacksHttpRequest { + pub fn new_getclaritymarf( + host: PeerHost, + clarity_marf_key: String, + tip_req: TipRequest, + with_proof: bool, + ) -> StacksHttpRequest { + StacksHttpRequest::new_for_peer( + host, + "GET".into(), + format!("/v2/clarity/marf/{}", &clarity_marf_key), + HttpRequestContents::new() + .for_tip(tip_req) + .query_arg("proof".into(), if with_proof { "1" } else { "0" }.into()), + ) + .expect("FATAL: failed to construct request from infallible data") + } +} + +impl StacksHttpResponse { + pub fn decode_clarity_marf_response(self) -> Result { + let contents = self.get_http_payload_ok()?; + let contents_json: serde_json::Value = contents.try_into()?; + let resp: ClarityMarfResponse = serde_json::from_value(contents_json) + .map_err(|_e| NetError::DeserializeError("Failed to load from JSON".to_string()))?; + Ok(resp) + } +} diff --git a/stackslib/src/net/api/getclaritymetadata.rs b/stackslib/src/net/api/getclaritymetadata.rs new file mode 100644 index 0000000000..5ef3feee6e --- /dev/null +++ b/stackslib/src/net/api/getclaritymetadata.rs @@ -0,0 +1,233 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 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 clarity::vm::clarity::ClarityConnection; +use clarity::vm::representations::{CONTRACT_NAME_REGEX_STRING, STANDARD_PRINCIPAL_REGEX_STRING}; +use clarity::vm::types::QualifiedContractIdentifier; +use clarity::vm::ContractName; +use lazy_static::lazy_static; +use regex::{Captures, Regex}; +use stacks_common::types::chainstate::StacksAddress; +use stacks_common::types::net::PeerHost; + +use crate::net::http::{ + parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble, + HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, +}; +use crate::net::httpcore::{ + request, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, + StacksHttpRequest, StacksHttpResponse, +}; +use crate::net::{Error as NetError, StacksNodeState, TipRequest}; + +lazy_static! { + static ref CLARITY_NAME_NO_BOUNDARIES_REGEX_STRING: String = + "[a-zA-Z]([a-zA-Z0-9]|[-_!?+<>=/*])*|[-+=/*]|[<>]=?".into(); + static ref METADATA_KEY_REGEX_STRING: String = format!( + r"vm-metadata::\d+::(contract|contract-size|contract-src|contract-data-size|({}))", + *CLARITY_NAME_NO_BOUNDARIES_REGEX_STRING, + ); +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ClarityMetadataResponse { + pub data: String, +} + +#[derive(Clone)] +pub struct RPCGetClarityMetadataRequestHandler { + pub clarity_metadata_key: Option, + pub contract_identifier: Option, +} +impl RPCGetClarityMetadataRequestHandler { + pub fn new() -> Self { + Self { + clarity_metadata_key: None, + contract_identifier: None, + } + } +} + +/// Decode the HTTP request +impl HttpRequest for RPCGetClarityMetadataRequestHandler { + fn verb(&self) -> &'static str { + "GET" + } + + fn path_regex(&self) -> Regex { + Regex::new(&format!( + r"^/v2/clarity/metadata/(?P
{})/(?P{})/(?P(analysis)|({}))$", + *STANDARD_PRINCIPAL_REGEX_STRING, + *CONTRACT_NAME_REGEX_STRING, + *METADATA_KEY_REGEX_STRING + )) + .unwrap() + } + + fn metrics_identifier(&self) -> &str { + "/v2/clarity/metadata/:principal/:contract_name/:clarity_metadata_key" + } + + /// Try to decode this request. + /// There's nothing to load here, so just make sure the request is well-formed. + fn try_parse_request( + &mut self, + preamble: &HttpRequestPreamble, + captures: &Captures, + query: Option<&str>, + _body: &[u8], + ) -> Result { + if preamble.get_content_length() != 0 { + return Err(Error::DecodeError( + "Invalid Http request: expected 0-length body".to_string(), + )); + } + + let contract_identifier = request::get_contract_address(captures, "address", "contract")?; + + let metadata_key = if let Some(key_str) = captures.name("clarity_metadata_key") { + key_str.as_str().to_string() + } else { + return Err(Error::Http( + 404, + "Missing `clarity_metadata_key`".to_string(), + )); + }; + + self.contract_identifier = Some(contract_identifier); + self.clarity_metadata_key = Some(metadata_key); + + let contents = HttpRequestContents::new().query_string(query); + Ok(contents) + } +} + +/// Handle the HTTP request +impl RPCRequestHandler for RPCGetClarityMetadataRequestHandler { + /// Reset internal state + fn restart(&mut self) { + self.contract_identifier = None; + self.clarity_metadata_key = None; + } + + /// Make the response + fn try_handle_request( + &mut self, + preamble: HttpRequestPreamble, + contents: HttpRequestContents, + node: &mut StacksNodeState, + ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> { + let contract_identifier = self.contract_identifier.take().ok_or(NetError::SendError( + "`contract_identifier` not set".to_string(), + ))?; + let clarity_metadata_key = self.clarity_metadata_key.take().ok_or(NetError::SendError( + "`clarity_metadata_key` not set".to_string(), + ))?; + + let tip = match node.load_stacks_chain_tip(&preamble, &contents) { + Ok(tip) => tip, + Err(error_resp) => { + return error_resp.try_into_contents().map_err(NetError::from); + } + }; + + let data_opt = node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| { + chainstate.maybe_read_only_clarity_tx( + &sortdb.index_handle_at_block(chainstate, &tip)?, + &tip, + |clarity_tx| { + clarity_tx.with_clarity_db_readonly(|clarity_db| { + let data = clarity_db + .store + .get_metadata(&contract_identifier, &clarity_metadata_key) + .ok() + .flatten()?; + + Some(ClarityMetadataResponse { data }) + }) + }, + ) + }); + + let data_resp = match data_opt { + Ok(Some(Some(data))) => data, + Ok(Some(None)) => { + return StacksHttpResponse::new_error( + &preamble, + &HttpNotFound::new("Metadata not found".to_string()), + ) + .try_into_contents() + .map_err(NetError::from); + } + Ok(None) | Err(_) => { + return StacksHttpResponse::new_error( + &preamble, + &HttpNotFound::new("Chain tip not found".to_string()), + ) + .try_into_contents() + .map_err(NetError::from); + } + }; + + let mut preamble = HttpResponsePreamble::ok_json(&preamble); + preamble.set_canonical_stacks_tip_height(Some(node.canonical_stacks_tip_height())); + let body = HttpResponseContents::try_from_json(&data_resp)?; + Ok((preamble, body)) + } +} + +/// Decode the HTTP response +impl HttpResponse for RPCGetClarityMetadataRequestHandler { + fn try_parse_response( + &self, + preamble: &HttpResponsePreamble, + body: &[u8], + ) -> Result { + let metadata: ClarityMetadataResponse = parse_json(preamble, body)?; + Ok(HttpResponsePayload::try_from_json(metadata)?) + } +} + +impl StacksHttpRequest { + pub fn new_getclaritymetadata( + host: PeerHost, + contract_addr: StacksAddress, + contract_name: ContractName, + clarity_metadata_key: String, + tip_req: TipRequest, + ) -> StacksHttpRequest { + StacksHttpRequest::new_for_peer( + host, + "GET".into(), + format!( + "/v2/clarity/metadata/{}/{}/{}", + &contract_addr, &contract_name, &clarity_metadata_key + ), + HttpRequestContents::new().for_tip(tip_req), + ) + .expect("FATAL: failed to construct request from infallible data") + } +} + +impl StacksHttpResponse { + pub fn decode_clarity_metadata_response(self) -> Result { + let contents = self.get_http_payload_ok()?; + let contents_json: serde_json::Value = contents.try_into()?; + let resp: ClarityMetadataResponse = serde_json::from_value(contents_json) + .map_err(|_e| NetError::DeserializeError("Failed to load from JSON".to_string()))?; + Ok(resp) + } +} diff --git a/stackslib/src/net/api/mod.rs b/stackslib/src/net/api/mod.rs index d256c15b97..0057f9047d 100644 --- a/stackslib/src/net/api/mod.rs +++ b/stackslib/src/net/api/mod.rs @@ -42,6 +42,8 @@ pub mod getattachment; pub mod getattachmentsinv; pub mod getblock; pub mod getblock_v3; +pub mod getclaritymarfvalue; +pub mod getclaritymetadata; pub mod getconstantval; pub mod getcontractabi; pub mod getcontractsrc; @@ -90,6 +92,8 @@ impl StacksHttp { self.register_rpc_endpoint(getattachmentsinv::RPCGetAttachmentsInvRequestHandler::new()); self.register_rpc_endpoint(getblock::RPCBlocksRequestHandler::new()); self.register_rpc_endpoint(getblock_v3::RPCNakamotoBlockRequestHandler::new()); + self.register_rpc_endpoint(getclaritymarfvalue::RPCGetClarityMarfRequestHandler::new()); + self.register_rpc_endpoint(getclaritymetadata::RPCGetClarityMetadataRequestHandler::new()); self.register_rpc_endpoint(getconstantval::RPCGetConstantValRequestHandler::new()); self.register_rpc_endpoint(getcontractabi::RPCGetContractAbiRequestHandler::new()); self.register_rpc_endpoint(getcontractsrc::RPCGetContractSrcRequestHandler::new()); diff --git a/stackslib/src/net/api/tests/getclaritymarfvalue.rs b/stackslib/src/net/api/tests/getclaritymarfvalue.rs new file mode 100644 index 0000000000..ce342b7442 --- /dev/null +++ b/stackslib/src/net/api/tests/getclaritymarfvalue.rs @@ -0,0 +1,180 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 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::net::{IpAddr, Ipv4Addr, SocketAddr}; + +use clarity::vm::types::{QualifiedContractIdentifier, StacksAddressExtensions}; +use clarity::vm::{ClarityName, ContractName}; +use stacks_common::codec::StacksMessageCodec; +use stacks_common::types::chainstate::StacksAddress; +use stacks_common::types::net::PeerHost; +use stacks_common::types::Address; + +use super::test_rpc; +use crate::net::api::*; +use crate::net::connection::ConnectionOptions; +use crate::net::httpcore::{ + HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp, + StacksHttpRequest, +}; +use crate::net::{ProtocolFamily, TipRequest}; + +#[test] +fn test_try_parse_request() { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); + let mut http = StacksHttp::new(addr.clone(), &ConnectionOptions::default()); + + let vm_key_epoch = "vm-epoch::epoch-version"; + let vm_key_trip = "vm::ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5.counter::1::count"; + let vm_key_quad = "vm::ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5.counter::0::data::1234"; + let valid_keys = [vm_key_epoch, vm_key_trip, vm_key_quad]; + + for key in valid_keys { + let request = StacksHttpRequest::new_getclaritymarf( + addr.into(), + key.to_string(), + TipRequest::SpecificTip(StacksBlockId([0x22; 32])), + true, + ); + assert_eq!( + request.contents().tip_request(), + TipRequest::SpecificTip(StacksBlockId([0x22; 32])) + ); + assert_eq!(request.contents().get_with_proof(), true); + + let bytes = request.try_serialize().unwrap(); + + let (parsed_preamble, offset) = http.read_preamble(&bytes).unwrap(); + let mut handler = getclaritymarfvalue::RPCGetClarityMarfRequestHandler::new(); + let mut parsed_request = http + .handle_try_parse_request( + &mut handler, + &parsed_preamble.expect_request(), + &bytes[offset..], + ) + .unwrap(); + + // parsed request consumes headers that would not be in a constructed request + parsed_request.clear_headers(); + let (preamble, contents) = parsed_request.destruct(); + + // consumed path args + assert_eq!(handler.clarity_marf_key, Some(key.to_string())); + + assert_eq!(&preamble, request.preamble()); + + handler.restart(); + assert!(handler.clarity_marf_key.is_none()); + } +} + +#[test] +fn test_try_make_response() { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); + + let mut requests = vec![]; + + // query existing + let request = StacksHttpRequest::new_getclaritymarf( + addr.into(), + "vm::ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world::1::bar".to_string(), + TipRequest::UseLatestAnchoredTip, + true, + ); + requests.push(request); + + // query existing unconfirmed + let request = StacksHttpRequest::new_getclaritymarf( + addr.into(), + "vm::ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world-unconfirmed::1::bar-unconfirmed" + .to_string(), + TipRequest::UseLatestUnconfirmedTip, + true, + ); + requests.push(request); + + // query non-existant var + let request = StacksHttpRequest::new_getclaritymarf( + addr.into(), + "vm::ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world::1::does-not-exist".to_string(), + TipRequest::UseLatestAnchoredTip, + true, + ); + requests.push(request); + + // query non-existant contract + let request = StacksHttpRequest::new_getclaritymarf( + addr.into(), + "vm::ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.does-not-exist::1::bar".to_string(), + TipRequest::UseLatestAnchoredTip, + true, + ); + requests.push(request); + + let mut responses = test_rpc(function_name!(), requests); + + // existing data + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + assert_eq!( + response.preamble().get_canonical_stacks_tip_height(), + Some(1) + ); + + let resp = response.decode_data_var_response().unwrap(); + assert_eq!(resp.data, "0x0000000000000000000000000000000000"); + assert!(resp.marf_proof.is_some()); + + // unconfirmed data + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + assert_eq!( + response.preamble().get_canonical_stacks_tip_height(), + Some(1) + ); + + let resp = response.decode_data_var_response().unwrap(); + assert_eq!(resp.data, "0x0100000000000000000000000000000001"); + assert!(resp.marf_proof.is_some()); + + // no such var + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + let (preamble, body) = response.destruct(); + assert_eq!(preamble.status_code, 404); + + // no such contract + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + let (preamble, body) = response.destruct(); + assert_eq!(preamble.status_code, 404); +} diff --git a/stackslib/src/net/api/tests/getclaritymetadata.rs b/stackslib/src/net/api/tests/getclaritymetadata.rs new file mode 100644 index 0000000000..3de5949a87 --- /dev/null +++ b/stackslib/src/net/api/tests/getclaritymetadata.rs @@ -0,0 +1,166 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 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::net::{IpAddr, Ipv4Addr, SocketAddr}; + +use clarity::vm::types::{QualifiedContractIdentifier, StacksAddressExtensions}; +use clarity::vm::{ClarityName, ContractName}; +use stacks_common::codec::StacksMessageCodec; +use stacks_common::types::chainstate::StacksAddress; +use stacks_common::types::net::PeerHost; +use stacks_common::types::Address; + +use super::test_rpc; +use crate::net::api::*; +use crate::net::connection::ConnectionOptions; +use crate::net::httpcore::{ + HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp, + StacksHttpRequest, +}; +use crate::net::{ProtocolFamily, TipRequest}; + +#[test] +fn test_try_parse_request() { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); + let mut http = StacksHttp::new(addr.clone(), &ConnectionOptions::default()); + + let request = StacksHttpRequest::new_getclaritymetadata( + addr.into(), + StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(), + "hello-world".try_into().unwrap(), + "vm-metadata::9::contract-size".to_string(), + TipRequest::SpecificTip(StacksBlockId([0x22; 32])), + ); + assert_eq!( + request.contents().tip_request(), + TipRequest::SpecificTip(StacksBlockId([0x22; 32])) + ); + let bytes = request.try_serialize().unwrap(); + + let (parsed_preamble, offset) = http.read_preamble(&bytes).unwrap(); + let mut handler = getclaritymetadata::RPCGetClarityMetadataRequestHandler::new(); + let mut parsed_request = http + .handle_try_parse_request( + &mut handler, + &parsed_preamble.expect_request(), + &bytes[offset..], + ) + .unwrap(); + + // parsed request consumes headers that would not be in a constructed request + parsed_request.clear_headers(); + let (preamble, contents) = parsed_request.destruct(); + + // consumed path args + assert_eq!( + handler.clarity_metadata_key, + Some("vm-metadata::9::contract-size".to_string()) + ); + assert_eq!( + handler.contract_identifier, + Some( + QualifiedContractIdentifier::parse( + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ) + .unwrap() + ) + ); + + assert_eq!(&preamble, request.preamble()); + + handler.restart(); + assert!(handler.clarity_metadata_key.is_none()); +} + +#[test] +fn test_try_parse_request_for_analysis() { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); + let mut http = StacksHttp::new(addr.clone(), &ConnectionOptions::default()); + + let request = StacksHttpRequest::new_getclaritymetadata( + addr.into(), + StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(), + "hello-world".try_into().unwrap(), + "analysis".to_string(), + TipRequest::SpecificTip(StacksBlockId([0x22; 32])), + ); + assert_eq!( + request.contents().tip_request(), + TipRequest::SpecificTip(StacksBlockId([0x22; 32])) + ); + let bytes = request.try_serialize().unwrap(); + + let (parsed_preamble, offset) = http.read_preamble(&bytes).unwrap(); + let mut handler = getclaritymetadata::RPCGetClarityMetadataRequestHandler::new(); + let mut parsed_request = http + .handle_try_parse_request( + &mut handler, + &parsed_preamble.expect_request(), + &bytes[offset..], + ) + .unwrap(); + + // parsed request consumes headers that would not be in a constructed request + parsed_request.clear_headers(); + let (preamble, contents) = parsed_request.destruct(); + + // consumed path args + assert_eq!(handler.clarity_metadata_key, Some("analysis".to_string())); + assert_eq!( + handler.contract_identifier, + Some( + QualifiedContractIdentifier::parse( + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ) + .unwrap() + ) + ); + + assert_eq!(&preamble, request.preamble()); + + handler.restart(); + assert!(handler.clarity_metadata_key.is_none()); +} + +#[test] +fn test_try_make_response() { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); + + let mut requests = vec![]; + + // query existing + let request = StacksHttpRequest::new_getclaritymetadata( + addr.into(), + StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(), + "hello-world".try_into().unwrap(), + "vm-metadata::9::contract-size".to_string(), + TipRequest::UseLatestAnchoredTip, + ); + requests.push(request); + + let mut responses = test_rpc(function_name!(), requests); + + // latest data + let response = responses.remove(0); + + assert_eq!( + response.preamble().get_canonical_stacks_tip_height(), + Some(1) + ); + + let resp = response.decode_clarity_metadata_response().unwrap(); + assert_eq!(resp.data, "1432"); +} diff --git a/stackslib/src/net/api/tests/mod.rs b/stackslib/src/net/api/tests/mod.rs index f0a537d045..72800164f3 100644 --- a/stackslib/src/net/api/tests/mod.rs +++ b/stackslib/src/net/api/tests/mod.rs @@ -60,6 +60,8 @@ mod getattachment; mod getattachmentsinv; mod getblock; mod getblock_v3; +mod getclaritymarfvalue; +mod getclaritymetadata; mod getconstantval; mod getcontractabi; mod getcontractsrc; @@ -117,7 +119,7 @@ const TEST_CONTRACT: &'static str = " (ok 1))) (begin (map-set unit-map { account: 'ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R } { units: 123 })) - + (define-read-only (ro-confirmed) u1) (define-public (do-test) (ok u0))