Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add a cost_tracker option to callreadonly rpc specifying the CostTracker to use for evaluation #5828

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/rpc/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 36 additions & 23 deletions stackslib/src/net/api/callreadonly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -202,6 +202,8 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler {
}
};

let cost_tracker = contents.get_cost_tracker();

let contract_identifier = self
.contract_identifier
.take()
Expand Down Expand Up @@ -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| {
Expand Down Expand Up @@ -355,6 +364,7 @@ impl StacksHttpRequest {
function_name: ClarityName,
function_args: Vec<Value>,
tip_req: TipRequest,
cost_tracker: CostTracker,
) -> StacksHttpRequest {
StacksHttpRequest::new_for_peer(
host,
Expand All @@ -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")
}
Expand Down
47 changes: 45 additions & 2 deletions stackslib/src/net/api/tests/callreadonly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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(),
Expand Down Expand Up @@ -121,6 +122,7 @@ fn test_try_make_response() {
"ro-confirmed".try_into().unwrap(),
vec![],
TipRequest::UseLatestAnchoredTip,
CostTracker::MidBlock,
);
requests.push(request);

Expand All @@ -136,6 +138,7 @@ fn test_try_make_response() {
"ro-test".try_into().unwrap(),
vec![],
TipRequest::UseLatestUnconfirmedTip,
CostTracker::MidBlock,
);
requests.push(request);

Expand All @@ -151,6 +154,7 @@ fn test_try_make_response() {
"does-not-exist".try_into().unwrap(),
vec![],
TipRequest::UseLatestUnconfirmedTip,
CostTracker::MidBlock,
);
requests.push(request);

Expand All @@ -166,6 +170,7 @@ fn test_try_make_response() {
"ro-test".try_into().unwrap(),
vec![],
TipRequest::UseLatestUnconfirmedTip,
CostTracker::MidBlock,
);
requests.push(request);

Expand All @@ -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);

Expand Down Expand Up @@ -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");
}
40 changes: 40 additions & 0 deletions stackslib/src/net/httpcore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down
43 changes: 41 additions & 2 deletions stackslib/src/net/tests/httpcore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 = "";
Expand Down