Skip to content

Commit

Permalink
feat: given a MARF key constructed client side, retrieve clarity value
Browse files Browse the repository at this point in the history
  • Loading branch information
lgalabru authored and hugocaillard committed Jul 23, 2024
1 parent f160aaf commit 26cf0da
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 0 deletions.
234 changes: 234 additions & 0 deletions stackslib/src/net/api/getclaritymarfvalue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// 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::io::{Read, Write};

use clarity::vm::ast::parser::v1::CLARITY_NAME_REGEX;
use clarity::vm::clarity::ClarityConnection;
use clarity::vm::costs::LimitedCostTracker;
use clarity::vm::database::{ClarityDatabase, STXBalance, StoreType};
use clarity::vm::representations::{
CONTRACT_NAME_REGEX_STRING, PRINCIPAL_DATA_REGEX_STRING, STANDARD_PRINCIPAL_REGEX_STRING,
};
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, StandardPrincipalData};
use clarity::vm::{ClarityName, ClarityVersion, ContractName};
use regex::{Captures, Regex};
use stacks_common::types::chainstate::{StacksAddress, StacksBlockId};
use stacks_common::types::net::PeerHost;
use stacks_common::types::Address;
use stacks_common::util::hash::{to_hex, Sha256Sum};

use crate::burnchains::Burnchain;
use crate::chainstate::burn::db::sortdb::SortitionDB;
use crate::chainstate::stacks::db::StacksChainState;
use crate::chainstate::stacks::Error as ChainError;
use crate::core::mempool::MemPoolDB;
use crate::net::http::{
parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble,
HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, HttpServerError,
};
use crate::net::httpcore::{
request, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp,
StacksHttpRequest, StacksHttpResponse,
};
use crate::net::p2p::PeerNetwork;
use crate::net::{Error as NetError, StacksNodeState, TipRequest};
use crate::util_lib::boot::boot_code_id;
use crate::util_lib::db::Error as DBError;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClarityMarfValueResponse {
pub data: String,
#[serde(rename = "proof")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub marf_proof: Option<String>,
}

#[derive(Clone)]
pub struct RPCGetClarityMarfValueRequestHandler {
pub clarity_marf_key: Option<String>,
}
impl RPCGetClarityMarfValueRequestHandler {
pub fn new() -> Self {
Self {
clarity_marf_key: None,
}
}
}

/// Decode the HTTP request
impl HttpRequest for RPCGetClarityMarfValueRequestHandler {
fn verb(&self) -> &'static str {
"GET"
}

fn path_regex(&self) -> Regex {
// TODO: write regex validating the following patterns
// format!("vm::{}::{}::{}", contract_identifier, data as u8, var_name)
// format!("vm::{}::{}::{}::{}", contract_identifier, data as u8, var_name, key_value)
// format!("vm-metadata::{}::{}", data as u8, var_name)
// format!("vm-epoch::epoch-version")

Regex::new(&format!("^/v2/clarity_marf_value/(?<clarity_marf_key>.*)$",)).unwrap()
}

fn metrics_identifier(&self) -> &str {
"/v2/clarity_marf_value/: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<HttpRequestContents, Error> {
if preamble.get_content_length() != 0 {
return Err(Error::DecodeError(
"Invalid Http request: expected 0-length body".to_string(),
));
}

let marf_key = request::get_marf_key(captures, "clarity_marf_key")?;

self.clarity_marf_key = Some(marf_key);

let contents = HttpRequestContents::new().query_string(query);
Ok(contents)
}
}

/// Handle the HTTP request
impl RPCRequestHandler for RPCGetClarityMarfValueRequestHandler {
/// 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_conn(), &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(ClarityMarfValueResponse { data, marf_proof })
})
})
});

let data_resp = match data_opt {
Ok(Some(Some(data))) => data,
Ok(Some(None)) => {
return StacksHttpResponse::new_error(
&preamble,
&HttpNotFound::new("Data var 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 RPCGetClarityMarfValueRequestHandler {
fn try_parse_response(
&self,
preamble: &HttpResponsePreamble,
body: &[u8],
) -> Result<HttpResponsePayload, Error> {
let marf_value: ClarityMarfValueResponse = parse_json(preamble, body)?;
Ok(HttpResponsePayload::try_from_json(marf_value)?)
}
}

impl StacksHttpRequest {
/// Make a new request for a data var
pub fn new_getclaritymarfvalue(
host: PeerHost,
clarity_marf_key: String,
tip_req: TipRequest,
with_proof: bool,
) -> StacksHttpRequest {
StacksHttpRequest::new_for_peer(
host,
"GET".into(),
format!("/v2/clarity_marf_value/{}", &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_value_response(self) -> Result<ClarityMarfValueResponse, NetError> {
let contents = self.get_http_payload_ok()?;
let contents_json: serde_json::Value = contents.try_into()?;
let resp: ClarityMarfValueResponse = serde_json::from_value(contents_json)
.map_err(|_e| NetError::DeserializeError("Failed to load from JSON".to_string()))?;
Ok(resp)
}
}
4 changes: 4 additions & 0 deletions stackslib/src/net/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub mod getattachment;
pub mod getattachmentsinv;
pub mod getblock;
pub mod getblock_v3;
pub mod getclaritymarfvalue;
pub mod getconstantval;
pub mod getcontractabi;
pub mod getcontractsrc;
Expand Down Expand Up @@ -90,6 +91,9 @@ 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::RPCGetClarityMarfValueRequestHandler::new(),
);
self.register_rpc_endpoint(getconstantval::RPCGetConstantValRequestHandler::new());
self.register_rpc_endpoint(getcontractabi::RPCGetContractAbiRequestHandler::new());
self.register_rpc_endpoint(getcontractsrc::RPCGetContractSrcRequestHandler::new());
Expand Down
11 changes: 11 additions & 0 deletions stackslib/src/net/httpcore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,17 @@ pub mod request {
Ok(txid)
}

/// Get and parse a MARF key from a path's captures, given the name of the regex field.
pub fn get_marf_key(captures: &Captures, key: &str) -> Result<String, HttpError> {
let marf_key = if let Some(marf_key_str) = captures.name(key) {
marf_key_str.as_str().to_string()
} else {
return Err(HttpError::Http(404, format!("Missing `{}`", key)));
};

Ok(marf_key)
}

/// Get and parse a Clarity name from a path's captures, given the name of the regex field.
pub fn get_clarity_name(captures: &Captures, key: &str) -> Result<ClarityName, HttpError> {
let clarity_name = if let Some(name_str) = captures.name(key) {
Expand Down

0 comments on commit 26cf0da

Please sign in to comment.