Skip to content

Commit 931f315

Browse files
committed
feat: rpc endpoint to retrieve clarity metadata
1 parent 26cf0da commit 931f315

File tree

8 files changed

+618
-55
lines changed

8 files changed

+618
-55
lines changed

clarity/src/vm/representations.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ lazy_static! {
6464
Regex::new(format!("^{}$|^__transient$", CONTRACT_NAME_REGEX_STRING.as_str()).as_str())
6565
.unwrap()
6666
};
67+
pub static ref MARF_KEY_FOR_TRIP_REGEX_STRING: String = format!(
68+
r"vm::{}::\d+::.*",
69+
*CONTRACT_PRINCIPAL_REGEX_STRING,
70+
);
71+
pub static ref MARF_KEY_FOR_QUAD_REGEX_STRING: String = format!(
72+
r"{}::.*",
73+
*MARF_KEY_FOR_TRIP_REGEX_STRING,
74+
);
75+
pub static ref METADATA_KEY_REGEX_STRING: String = format!(
76+
r"vm-metadata::\d+::.*",
77+
78+
);
6779
}
6880

6981
guarded_string!(

stackslib/src/net/api/getclaritymarfvalue.rs

Lines changed: 37 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2-
// Copyright (C) 2020-2023 Stacks Open Internet Foundation
2+
// Copyright (C) 2020-2024 Stacks Open Internet Foundation
33
//
44
// This program is free software: you can redistribute it and/or modify
55
// it under the terms of the GNU General Public License as published by
@@ -14,40 +14,23 @@
1414
// You should have received a copy of the GNU General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17-
use std::io::{Read, Write};
18-
19-
use clarity::vm::ast::parser::v1::CLARITY_NAME_REGEX;
2017
use clarity::vm::clarity::ClarityConnection;
21-
use clarity::vm::costs::LimitedCostTracker;
22-
use clarity::vm::database::{ClarityDatabase, STXBalance, StoreType};
2318
use clarity::vm::representations::{
24-
CONTRACT_NAME_REGEX_STRING, PRINCIPAL_DATA_REGEX_STRING, STANDARD_PRINCIPAL_REGEX_STRING,
19+
MARF_KEY_FOR_QUAD_REGEX_STRING, MARF_KEY_FOR_TRIP_REGEX_STRING,
2520
};
26-
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, StandardPrincipalData};
27-
use clarity::vm::{ClarityName, ClarityVersion, ContractName};
2821
use regex::{Captures, Regex};
29-
use stacks_common::types::chainstate::{StacksAddress, StacksBlockId};
3022
use stacks_common::types::net::PeerHost;
31-
use stacks_common::types::Address;
32-
use stacks_common::util::hash::{to_hex, Sha256Sum};
33-
34-
use crate::burnchains::Burnchain;
35-
use crate::chainstate::burn::db::sortdb::SortitionDB;
36-
use crate::chainstate::stacks::db::StacksChainState;
37-
use crate::chainstate::stacks::Error as ChainError;
38-
use crate::core::mempool::MemPoolDB;
23+
use stacks_common::util::hash::to_hex;
24+
3925
use crate::net::http::{
4026
parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble,
41-
HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, HttpServerError,
27+
HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble,
4228
};
4329
use crate::net::httpcore::{
44-
request, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp,
30+
request, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler,
4531
StacksHttpRequest, StacksHttpResponse,
4632
};
47-
use crate::net::p2p::PeerNetwork;
4833
use crate::net::{Error as NetError, StacksNodeState, TipRequest};
49-
use crate::util_lib::boot::boot_code_id;
50-
use crate::util_lib::db::Error as DBError;
5134

5235
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5336
pub struct ClarityMarfValueResponse {
@@ -77,13 +60,11 @@ impl HttpRequest for RPCGetClarityMarfValueRequestHandler {
7760
}
7861

7962
fn path_regex(&self) -> Regex {
80-
// TODO: write regex validating the following patterns
81-
// format!("vm::{}::{}::{}", contract_identifier, data as u8, var_name)
82-
// format!("vm::{}::{}::{}::{}", contract_identifier, data as u8, var_name, key_value)
83-
// format!("vm-metadata::{}::{}", data as u8, var_name)
84-
// format!("vm-epoch::epoch-version")
85-
86-
Regex::new(&format!("^/v2/clarity_marf_value/(?<clarity_marf_key>.*)$",)).unwrap()
63+
Regex::new(&format!(
64+
r"^/v2/clarity_marf_value/(?P<clarity_marf_key>(vm-epoch::epoch-version)|({})|({}))$",
65+
*MARF_KEY_FOR_TRIP_REGEX_STRING, *MARF_KEY_FOR_QUAD_REGEX_STRING
66+
))
67+
.unwrap()
8768
}
8869

8970
fn metrics_identifier(&self) -> &str {
@@ -105,7 +86,7 @@ impl HttpRequest for RPCGetClarityMarfValueRequestHandler {
10586
));
10687
}
10788

108-
let marf_key = request::get_marf_key(captures, "clarity_marf_key")?;
89+
let marf_key = request::get_key(captures, "clarity_marf_key")?;
10990

11091
self.clarity_marf_key = Some(marf_key);
11192

@@ -142,34 +123,38 @@ impl RPCRequestHandler for RPCGetClarityMarfValueRequestHandler {
142123
let with_proof = contents.get_with_proof();
143124

144125
let data_opt = node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| {
145-
chainstate.maybe_read_only_clarity_tx(&sortdb.index_conn(), &tip, |clarity_tx| {
146-
clarity_tx.with_clarity_db_readonly(|clarity_db| {
147-
let (value_hex, marf_proof): (String, _) = if with_proof {
148-
clarity_db
149-
.get_data_with_proof(&clarity_marf_key)
150-
.ok()
151-
.flatten()
152-
.map(|(a, b)| (a, Some(format!("0x{}", to_hex(&b)))))?
153-
} else {
154-
clarity_db
155-
.get_data(&clarity_marf_key)
156-
.ok()
157-
.flatten()
158-
.map(|a| (a, None))?
159-
};
160-
161-
let data = format!("0x{}", value_hex);
162-
Some(ClarityMarfValueResponse { data, marf_proof })
163-
})
164-
})
126+
chainstate.maybe_read_only_clarity_tx(
127+
&sortdb.index_handle_at_block(chainstate, &tip)?,
128+
&tip,
129+
|clarity_tx| {
130+
clarity_tx.with_clarity_db_readonly(|clarity_db| {
131+
let (value_hex, marf_proof): (String, _) = if with_proof {
132+
clarity_db
133+
.get_data_with_proof(&clarity_marf_key)
134+
.ok()
135+
.flatten()
136+
.map(|(a, b)| (a, Some(format!("0x{}", to_hex(&b)))))?
137+
} else {
138+
clarity_db
139+
.get_data(&clarity_marf_key)
140+
.ok()
141+
.flatten()
142+
.map(|a| (a, None))?
143+
};
144+
145+
let data = format!("0x{}", value_hex);
146+
Some(ClarityMarfValueResponse { data, marf_proof })
147+
})
148+
},
149+
)
165150
});
166151

167152
let data_resp = match data_opt {
168153
Ok(Some(Some(data))) => data,
169154
Ok(Some(None)) => {
170155
return StacksHttpResponse::new_error(
171156
&preamble,
172-
&HttpNotFound::new("Data var not found".to_string()),
157+
&HttpNotFound::new("Marf key not found".to_string()),
173158
)
174159
.try_into_contents()
175160
.map_err(NetError::from);
@@ -204,7 +189,6 @@ impl HttpResponse for RPCGetClarityMarfValueRequestHandler {
204189
}
205190

206191
impl StacksHttpRequest {
207-
/// Make a new request for a data var
208192
pub fn new_getclaritymarfvalue(
209193
host: PeerHost,
210194
clarity_marf_key: String,
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2+
// Copyright (C) 2020-2024 Stacks Open Internet Foundation
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
use clarity::vm::clarity::ClarityConnection;
18+
use clarity::vm::representations::{
19+
CONTRACT_NAME_REGEX_STRING, METADATA_KEY_REGEX_STRING, STANDARD_PRINCIPAL_REGEX_STRING,
20+
};
21+
use clarity::vm::types::QualifiedContractIdentifier;
22+
use clarity::vm::ContractName;
23+
use regex::{Captures, Regex};
24+
use stacks_common::types::chainstate::StacksAddress;
25+
use stacks_common::types::net::PeerHost;
26+
27+
use crate::net::http::{
28+
parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble,
29+
HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble,
30+
};
31+
use crate::net::httpcore::{
32+
request, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler,
33+
StacksHttpRequest, StacksHttpResponse,
34+
};
35+
use crate::net::{Error as NetError, StacksNodeState, TipRequest};
36+
37+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38+
pub struct ClarityMetadataResponse {
39+
pub data: String,
40+
}
41+
42+
#[derive(Clone)]
43+
pub struct RPCGetClarityMetadataRequestHandler {
44+
pub clarity_metadata_key: Option<String>,
45+
pub contract_identifier: Option<QualifiedContractIdentifier>,
46+
}
47+
impl RPCGetClarityMetadataRequestHandler {
48+
pub fn new() -> Self {
49+
Self {
50+
clarity_metadata_key: None,
51+
contract_identifier: None,
52+
}
53+
}
54+
}
55+
56+
/// Decode the HTTP request
57+
impl HttpRequest for RPCGetClarityMetadataRequestHandler {
58+
fn verb(&self) -> &'static str {
59+
"GET"
60+
}
61+
62+
fn path_regex(&self) -> Regex {
63+
Regex::new(&format!(
64+
r"^/v2/clarity_metadata/(?P<address>{})/(?P<contract>{})/(?P<clarity_metadata_key>(analysis)|({}))$",
65+
*STANDARD_PRINCIPAL_REGEX_STRING,
66+
*CONTRACT_NAME_REGEX_STRING,
67+
*METADATA_KEY_REGEX_STRING
68+
))
69+
.unwrap()
70+
}
71+
72+
fn metrics_identifier(&self) -> &str {
73+
"/v2/clarity_metadata/:principal/:contract_name/:clarity_metadata_key"
74+
}
75+
76+
/// Try to decode this request.
77+
/// There's nothing to load here, so just make sure the request is well-formed.
78+
fn try_parse_request(
79+
&mut self,
80+
preamble: &HttpRequestPreamble,
81+
captures: &Captures,
82+
query: Option<&str>,
83+
_body: &[u8],
84+
) -> Result<HttpRequestContents, Error> {
85+
if preamble.get_content_length() != 0 {
86+
return Err(Error::DecodeError(
87+
"Invalid Http request: expected 0-length body".to_string(),
88+
));
89+
}
90+
91+
let contract_identifier = request::get_contract_address(captures, "address", "contract")?;
92+
let metadata_key = request::get_key(captures, "clarity_metadata_key")?;
93+
94+
self.contract_identifier = Some(contract_identifier);
95+
self.clarity_metadata_key = Some(metadata_key);
96+
97+
let contents = HttpRequestContents::new().query_string(query);
98+
Ok(contents)
99+
}
100+
}
101+
102+
/// Handle the HTTP request
103+
impl RPCRequestHandler for RPCGetClarityMetadataRequestHandler {
104+
/// Reset internal state
105+
fn restart(&mut self) {
106+
self.contract_identifier = None;
107+
self.clarity_metadata_key = None;
108+
}
109+
110+
/// Make the response
111+
fn try_handle_request(
112+
&mut self,
113+
preamble: HttpRequestPreamble,
114+
contents: HttpRequestContents,
115+
node: &mut StacksNodeState,
116+
) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
117+
let contract_identifier = self.contract_identifier.take().ok_or(NetError::SendError(
118+
"`contract_identifier` not set".to_string(),
119+
))?;
120+
let clarity_metadata_key = self.clarity_metadata_key.take().ok_or(NetError::SendError(
121+
"`clarity_metadata_key` not set".to_string(),
122+
))?;
123+
124+
let tip = match node.load_stacks_chain_tip(&preamble, &contents) {
125+
Ok(tip) => tip,
126+
Err(error_resp) => {
127+
return error_resp.try_into_contents().map_err(NetError::from);
128+
}
129+
};
130+
131+
let data_opt = node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| {
132+
chainstate.maybe_read_only_clarity_tx(
133+
&sortdb.index_handle_at_block(chainstate, &tip)?,
134+
&tip,
135+
|clarity_tx| {
136+
clarity_tx.with_clarity_db_readonly(|clarity_db| {
137+
let data = clarity_db
138+
.store
139+
.get_metadata(&contract_identifier, &clarity_metadata_key)
140+
.ok()
141+
.flatten()?;
142+
143+
Some(ClarityMetadataResponse { data })
144+
})
145+
},
146+
)
147+
});
148+
149+
let data_resp = match data_opt {
150+
Ok(Some(Some(data))) => data,
151+
Ok(Some(None)) => {
152+
return StacksHttpResponse::new_error(
153+
&preamble,
154+
&HttpNotFound::new("Metadata not found".to_string()),
155+
)
156+
.try_into_contents()
157+
.map_err(NetError::from);
158+
}
159+
Ok(None) | Err(_) => {
160+
return StacksHttpResponse::new_error(
161+
&preamble,
162+
&HttpNotFound::new("Chain tip not found".to_string()),
163+
)
164+
.try_into_contents()
165+
.map_err(NetError::from);
166+
}
167+
};
168+
169+
let mut preamble = HttpResponsePreamble::ok_json(&preamble);
170+
preamble.set_canonical_stacks_tip_height(Some(node.canonical_stacks_tip_height()));
171+
let body = HttpResponseContents::try_from_json(&data_resp)?;
172+
Ok((preamble, body))
173+
}
174+
}
175+
176+
/// Decode the HTTP response
177+
impl HttpResponse for RPCGetClarityMetadataRequestHandler {
178+
fn try_parse_response(
179+
&self,
180+
preamble: &HttpResponsePreamble,
181+
body: &[u8],
182+
) -> Result<HttpResponsePayload, Error> {
183+
let metadata: ClarityMetadataResponse = parse_json(preamble, body)?;
184+
Ok(HttpResponsePayload::try_from_json(metadata)?)
185+
}
186+
}
187+
188+
impl StacksHttpRequest {
189+
pub fn new_getclaritymetadata(
190+
host: PeerHost,
191+
contract_addr: StacksAddress,
192+
contract_name: ContractName,
193+
clarity_metadata_key: String,
194+
tip_req: TipRequest,
195+
) -> StacksHttpRequest {
196+
StacksHttpRequest::new_for_peer(
197+
host,
198+
"GET".into(),
199+
format!(
200+
"/v2/clarity_metadata/{}/{}/{}",
201+
&contract_addr, &contract_name, &clarity_metadata_key
202+
),
203+
HttpRequestContents::new().for_tip(tip_req),
204+
)
205+
.expect("FATAL: failed to construct request from infallible data")
206+
}
207+
}
208+
209+
impl StacksHttpResponse {
210+
pub fn decode_clarity_metadata_response(self) -> Result<ClarityMetadataResponse, NetError> {
211+
let contents = self.get_http_payload_ok()?;
212+
let contents_json: serde_json::Value = contents.try_into()?;
213+
let resp: ClarityMetadataResponse = serde_json::from_value(contents_json)
214+
.map_err(|_e| NetError::DeserializeError("Failed to load from JSON".to_string()))?;
215+
Ok(resp)
216+
}
217+
}

0 commit comments

Comments
 (0)