From d4de57c3a7456c64ba68b54069b7e646feb4f7cc Mon Sep 17 00:00:00 2001 From: Alex Tupper Date: Tue, 11 Feb 2025 10:47:48 +0000 Subject: [PATCH 1/2] feat(api): allow the client to specify a type of CostTracker when calling callreadonly, choosing from 'mid_block' (default; current) and 'free' --- stackslib/src/net/api/callreadonly.rs | 59 +++++++++++++-------- stackslib/src/net/api/tests/callreadonly.rs | 47 +++++++++++++++- stackslib/src/net/httpcore.rs | 40 ++++++++++++++ stackslib/src/net/tests/httpcore.rs | 43 ++++++++++++++- 4 files changed, 162 insertions(+), 27 deletions(-) diff --git a/stackslib/src/net/api/callreadonly.rs b/stackslib/src/net/api/callreadonly.rs index 150ed1ca1e..7de5d3265e 100644 --- a/stackslib/src/net/api/callreadonly.rs +++ b/stackslib/src/net/api/callreadonly.rs @@ -48,8 +48,8 @@ use crate::net::http::{ HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, HttpServerError, }; use crate::net::httpcore::{ - request, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp, - StacksHttpRequest, StacksHttpResponse, + request, CostTracker, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, + StacksHttp, StacksHttpRequest, StacksHttpResponse, }; use crate::net::p2p::PeerNetwork; use crate::net::{Error as NetError, StacksNodeState, TipRequest}; @@ -202,6 +202,8 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler { } }; + let cost_tracker = contents.get_cost_tracker(); + let contract_identifier = self .contract_identifier .take() @@ -230,24 +232,31 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler { let mainnet = chainstate.mainnet; let chain_id = chainstate.chain_id; - let mut cost_limit = self.read_only_call_limit.clone(); - cost_limit.write_length = 0; - cost_limit.write_count = 0; chainstate.maybe_read_only_clarity_tx( &sortdb.index_handle_at_block(chainstate, &tip)?, &tip, |clarity_tx| { - let epoch = clarity_tx.get_epoch(); - let cost_track = clarity_tx - .with_clarity_db_readonly(|clarity_db| { - LimitedCostTracker::new_mid_block( - mainnet, chain_id, cost_limit, clarity_db, epoch, - ) - }) - .map_err(|_| { - ClarityRuntimeError::from(InterpreterError::CostContractLoadFailure) - })?; + let cost_track = match cost_tracker { + CostTracker::Free => LimitedCostTracker::Free, + CostTracker::MidBlock => { + let epoch = clarity_tx.get_epoch(); + let mut cost_limit = self.read_only_call_limit.clone(); + cost_limit.write_length = 0; + cost_limit.write_count = 0; + clarity_tx + .with_clarity_db_readonly(|clarity_db| { + LimitedCostTracker::new_mid_block( + mainnet, chain_id, cost_limit, clarity_db, epoch, + ) + }) + .map_err(|_| { + ClarityRuntimeError::from( + InterpreterError::CostContractLoadFailure, + ) + })? + } + }; let clarity_version = clarity_tx .with_analysis_db_readonly(|analysis_db| { @@ -355,6 +364,7 @@ impl StacksHttpRequest { function_name: ClarityName, function_args: Vec, tip_req: TipRequest, + cost_tracker: CostTracker, ) -> StacksHttpRequest { StacksHttpRequest::new_for_peer( host, @@ -363,14 +373,17 @@ impl StacksHttpRequest { "/v2/contracts/call-read/{}/{}/{}", &contract_addr, &contract_name, &function_name ), - HttpRequestContents::new().for_tip(tip_req).payload_json( - serde_json::to_value(CallReadOnlyRequestBody { - sender: sender.to_string(), - sponsor: sponsor.map(|s| s.to_string()), - arguments: function_args.into_iter().map(|v| v.to_string()).collect(), - }) - .expect("FATAL: failed to encode infallible data"), - ), + HttpRequestContents::new() + .for_tip(tip_req) + .query_arg("cost_tracker".to_string(), cost_tracker.to_string()) + .payload_json( + serde_json::to_value(CallReadOnlyRequestBody { + sender: sender.to_string(), + sponsor: sponsor.map(|s| s.to_string()), + arguments: function_args.into_iter().map(|v| v.to_string()).collect(), + }) + .expect("FATAL: failed to encode infallible data"), + ), ) .expect("FATAL: failed to construct request from infallible data") } diff --git a/stackslib/src/net/api/tests/callreadonly.rs b/stackslib/src/net/api/tests/callreadonly.rs index 577d2e0b12..75aeb1439b 100644 --- a/stackslib/src/net/api/tests/callreadonly.rs +++ b/stackslib/src/net/api/tests/callreadonly.rs @@ -28,8 +28,8 @@ use crate::core::BLOCK_LIMIT_MAINNET_21; use crate::net::api::*; use crate::net::connection::ConnectionOptions; use crate::net::httpcore::{ - HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp, - StacksHttpRequest, + CostTracker, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, + StacksHttp, StacksHttpRequest, }; use crate::net::{ProtocolFamily, TipRequest}; @@ -49,6 +49,7 @@ fn test_try_parse_request() { "ro-test".try_into().unwrap(), vec![], TipRequest::SpecificTip(StacksBlockId([0x22; 32])), + CostTracker::MidBlock, ); assert_eq!( request.contents().tip_request(), @@ -121,6 +122,7 @@ fn test_try_make_response() { "ro-confirmed".try_into().unwrap(), vec![], TipRequest::UseLatestAnchoredTip, + CostTracker::MidBlock, ); requests.push(request); @@ -136,6 +138,7 @@ fn test_try_make_response() { "ro-test".try_into().unwrap(), vec![], TipRequest::UseLatestUnconfirmedTip, + CostTracker::MidBlock, ); requests.push(request); @@ -151,6 +154,7 @@ fn test_try_make_response() { "does-not-exist".try_into().unwrap(), vec![], TipRequest::UseLatestUnconfirmedTip, + CostTracker::MidBlock, ); requests.push(request); @@ -166,6 +170,7 @@ fn test_try_make_response() { "ro-test".try_into().unwrap(), vec![], TipRequest::UseLatestUnconfirmedTip, + CostTracker::MidBlock, ); requests.push(request); @@ -181,6 +186,23 @@ fn test_try_make_response() { "ro-confirmed".try_into().unwrap(), vec![], TipRequest::SpecificTip(StacksBlockId([0x11; 32])), + CostTracker::MidBlock, + ); + requests.push(request); + + // query confirmed tip with free cost tracker + let request = StacksHttpRequest::new_callreadonlyfunction( + addr.into(), + StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(), + "hello-world".try_into().unwrap(), + StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R") + .unwrap() + .to_account_principal(), + None, + "ro-confirmed".try_into().unwrap(), + vec![], + TipRequest::UseLatestAnchoredTip, + CostTracker::Free, ); requests.push(request); @@ -270,4 +292,25 @@ fn test_try_make_response() { let (preamble, payload) = response.destruct(); assert_eq!(preamble.status_code, 404); + + // confirmed tip with free cost tracker (same test conditions as confirmed tip) + 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_call_readonly_response().unwrap(); + + assert!(resp.okay); + assert!(resp.result.is_some()); + assert!(resp.cause.is_none()); + + // u1 + assert_eq!(resp.result.unwrap(), "0x0100000000000000000000000000000001"); } diff --git a/stackslib/src/net/httpcore.rs b/stackslib/src/net/httpcore.rs index a38f35c005..8c78c1d099 100644 --- a/stackslib/src/net/httpcore.rs +++ b/stackslib/src/net/httpcore.rs @@ -102,6 +102,36 @@ impl From<&str> for TipRequest { } } +/// All representations of the `cost_tracker=` query parameter value; +/// Each value corresponds to a different choice of construction of a cost tracker +/// (implementing clarity::vm::costs::CostTracker). +#[derive(Debug, Clone, PartialEq)] +pub enum CostTracker { + /// Use LimitedCostTracker::Free + Free, + /// Use LimitedCostTracker:new_mid_block + MidBlock, +} + +impl fmt::Display for CostTracker { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Free => write!(f, "free"), + Self::MidBlock => write!(f, "mid_block"), + } + } +} + +impl From<&str> for CostTracker { + fn from(s: &str) -> CostTracker { + match s { + "free" => CostTracker::Free, + "mid_block" => CostTracker::MidBlock, + _ => CostTracker::MidBlock, + } + } +} + /// Extension to HttpRequestPreamble to give it awareness of Stacks-specific fields pub trait HttpPreambleExtensions { /// Set the node's canonical Stacks chain tip @@ -303,6 +333,8 @@ pub trait HttpRequestContentsExtensions { fn for_tip(self, tip_req: TipRequest) -> Self; /// Identify the tip request fn tip_request(&self) -> TipRequest; + /// Identify the cost tracker (e.g. when executing a contract) + fn get_cost_tracker(&self) -> CostTracker; /// Determine if we should return a MARF proof fn get_with_proof(&self) -> bool; } @@ -331,6 +363,14 @@ impl HttpRequestContentsExtensions for HttpRequestContents { .unwrap_or(TipRequest::UseLatestAnchoredTip) } + // Get the cost_tracker= query parameter value + fn get_cost_tracker(&self) -> CostTracker { + self.get_query_args() + .get("cost_tracker") + .map(|cost_tracker| cost_tracker.as_str().into()) + .unwrap_or(CostTracker::MidBlock) + } + /// Get the proof= query parameter value fn get_with_proof(&self) -> bool { let proof_value = self diff --git a/stackslib/src/net/tests/httpcore.rs b/stackslib/src/net/tests/httpcore.rs index 8372398533..303b15bcd9 100644 --- a/stackslib/src/net/tests/httpcore.rs +++ b/stackslib/src/net/tests/httpcore.rs @@ -44,8 +44,8 @@ use crate::net::http::{ HttpResponsePreamble, HttpVersion, HTTP_PREAMBLE_MAX_NUM_HEADERS, }; use crate::net::httpcore::{ - send_http_request, HttpPreambleExtensions, HttpRequestContentsExtensions, StacksHttp, - StacksHttpMessage, StacksHttpPreamble, StacksHttpRequest, StacksHttpResponse, + send_http_request, CostTracker, HttpPreambleExtensions, HttpRequestContentsExtensions, + StacksHttp, StacksHttpMessage, StacksHttpPreamble, StacksHttpRequest, StacksHttpResponse, }; use crate::net::rpc::ConversationHttp; use crate::net::{ProtocolFamily, TipRequest}; @@ -1009,6 +1009,45 @@ fn test_http_parse_proof_tip_query() { assert_eq!(tip_req, TipRequest::UseLatestAnchoredTip); } +#[test] +fn test_http_parse_cost_tracker_query() { + // parses free cost tracker + match HttpRequestContents::new() + .query_string(Some("cost_tracker=free")) + .get_cost_tracker() + { + CostTracker::Free => {} + _ => panic!(), + } + + // parses mid_block cost tracker + match HttpRequestContents::new() + .query_string(Some("cost_tracker=mid_block")) + .get_cost_tracker() + { + CostTracker::MidBlock => {} + _ => panic!(), + } + + // defaults to mid block for malformed cost tracker + match HttpRequestContents::new() + .query_string(Some("cost_tracker=bad")) + .get_cost_tracker() + { + CostTracker::MidBlock => {} + _ => panic!(), + } + + // defaults to mid block for missing cost tracker + match HttpRequestContents::new() + .query_string(Some("cost_tracker=")) + .get_cost_tracker() + { + CostTracker::MidBlock => {} + _ => panic!(), + } +} + #[test] fn test_http_parse_proof_request_query() { let query_txt = ""; From 3bec0aca4b99ab26c807d32c16aa20eb8bbd8165 Mon Sep 17 00:00:00 2001 From: Alex Tupper Date: Tue, 11 Feb 2025 11:02:50 +0000 Subject: [PATCH 2/2] docs(api): add cost_tracker query parameter to callreadonly openapi spec --- docs/rpc/openapi.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/rpc/openapi.yaml b/docs/rpc/openapi.yaml index d82494ca36..85f76571d0 100644 --- a/docs/rpc/openapi.yaml +++ b/docs/rpc/openapi.yaml @@ -229,6 +229,16 @@ 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). required: false + - name: cost_tracker + in: query + schema: + type: string + enum: + - mid_block + - free + default: mid_block + description: The type of CostTracker to use when executing the contract. + required: false requestBody: description: map of arguments and the simulated tx-sender where sender is either a Contract identifier or a normal Stacks address, and arguments is an array of hex serialized Clarity values. required: true