Skip to content

Commit 830707b

Browse files
committed
added implicit max_execution_time when executin readonly calls in cost-free mode
1 parent 28fc444 commit 830707b

File tree

6 files changed

+266
-20
lines changed

6 files changed

+266
-20
lines changed

stackslib/src/net/api/callreadonly.rs

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
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::time::Duration;
18+
1719
use clarity::vm::analysis::CheckErrors;
1820
use clarity::vm::ast::parser::v1::CLARITY_NAME_REGEX;
1921
use clarity::vm::clarity::ClarityConnection;
20-
use clarity::vm::costs::{ExecutionCost, LimitedCostTracker};
22+
use clarity::vm::costs::{CostErrors, ExecutionCost, LimitedCostTracker};
23+
use clarity::vm::errors::Error as ClarityRuntimeError;
2124
use clarity::vm::errors::Error::Unchecked;
22-
use clarity::vm::errors::{Error as ClarityRuntimeError, InterpreterError};
2325
use clarity::vm::representations::{CONTRACT_NAME_REGEX_STRING, STANDARD_PRINCIPAL_REGEX_STRING};
2426
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier};
2527
use clarity::vm::{ClarityName, ContractName, SymbolicExpression, Value};
@@ -68,10 +70,16 @@ pub struct RPCCallReadOnlyRequestHandler {
6870
pub sender: Option<PrincipalData>,
6971
pub sponsor: Option<PrincipalData>,
7072
pub arguments: Option<Vec<Value>>,
73+
74+
read_only_max_execution_time: Duration,
7175
}
7276

7377
impl RPCCallReadOnlyRequestHandler {
74-
pub fn new(maximum_call_argument_size: u32, read_only_call_limit: ExecutionCost) -> Self {
78+
pub fn new(
79+
maximum_call_argument_size: u32,
80+
read_only_call_limit: ExecutionCost,
81+
read_only_max_execution_time: Duration,
82+
) -> Self {
7583
Self {
7684
maximum_call_argument_size,
7785
read_only_call_limit,
@@ -80,6 +88,7 @@ impl RPCCallReadOnlyRequestHandler {
8088
sender: None,
8189
sponsor: None,
8290
arguments: None,
91+
read_only_max_execution_time,
8392
}
8493
}
8594
}
@@ -184,6 +193,12 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler {
184193
}
185194
};
186195

196+
let cost_tracker = contents
197+
.get_query_args()
198+
.get("cost_tracker")
199+
.map(|cost_tracker| cost_tracker.as_str().into())
200+
.unwrap_or(CostTrackerRequest::Limited);
201+
187202
let contract_identifier = self
188203
.contract_identifier
189204
.take()
@@ -216,20 +231,27 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler {
216231
cost_limit.write_length = 0;
217232
cost_limit.write_count = 0;
218233

234+
let mut enforce_max_execution_time = false;
235+
219236
chainstate.maybe_read_only_clarity_tx(
220237
&sortdb.index_handle_at_block(chainstate, &tip)?,
221238
&tip,
222239
|clarity_tx| {
223240
let epoch = clarity_tx.get_epoch();
224241
let cost_track = clarity_tx
225-
.with_clarity_db_readonly(|clarity_db| {
226-
LimitedCostTracker::new_mid_block(
242+
.with_clarity_db_readonly(|clarity_db| match cost_tracker {
243+
CostTrackerRequest::Limited => LimitedCostTracker::new_mid_block(
227244
mainnet, chain_id, cost_limit, clarity_db, epoch,
228-
)
245+
),
246+
CostTrackerRequest::Free => {
247+
enforce_max_execution_time = true;
248+
Ok(LimitedCostTracker::new_free())
249+
}
250+
CostTrackerRequest::Invalid => {
251+
Err(CostErrors::Expect("Invalid cost tracker".into()))
252+
}
229253
})
230-
.map_err(|_| {
231-
ClarityRuntimeError::from(InterpreterError::CostContractLoadFailure)
232-
})?;
254+
.map_err(|e| ClarityRuntimeError::from(e))?;
233255

234256
let clarity_version = clarity_tx
235257
.with_analysis_db_readonly(|analysis_db| {
@@ -250,6 +272,13 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler {
250272
sponsor,
251273
cost_track,
252274
|env| {
275+
// cost tracking in read only calls is meamingful mainly from a security point of view
276+
// for this reason we enforce max_execution_time when cost tracking is disabled/free
277+
if enforce_max_execution_time {
278+
env.global_context
279+
.set_max_execution_time(self.read_only_max_execution_time);
280+
}
281+
253282
// we want to execute any function as long as no actual writes are made as
254283
// opposed to be limited to purely calling `define-read-only` functions,
255284
// so use `read_only = false`. This broadens the number of functions that
@@ -326,6 +355,38 @@ impl HttpResponse for RPCCallReadOnlyRequestHandler {
326355
}
327356
}
328357

358+
/// All representations of the `cost_tracker=` query parameter value
359+
#[derive(Debug, Clone, PartialEq)]
360+
pub enum CostTrackerRequest {
361+
Limited,
362+
Free,
363+
Invalid,
364+
}
365+
366+
impl CostTrackerRequest {}
367+
368+
impl std::fmt::Display for CostTrackerRequest {
369+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
370+
match self {
371+
Self::Limited => write!(f, "limited"),
372+
Self::Free => write!(f, "free"),
373+
Self::Invalid => write!(f, "invalid"),
374+
}
375+
}
376+
}
377+
378+
impl From<&str> for CostTrackerRequest {
379+
fn from(s: &str) -> CostTrackerRequest {
380+
if s == "limited" || s == "" {
381+
CostTrackerRequest::Limited
382+
} else if s == "free" {
383+
CostTrackerRequest::Free
384+
} else {
385+
CostTrackerRequest::Invalid
386+
}
387+
}
388+
}
389+
329390
impl StacksHttpRequest {
330391
/// Make a new request to run a read-only function
331392
pub fn new_callreadonlyfunction(
@@ -337,6 +398,7 @@ impl StacksHttpRequest {
337398
function_name: ClarityName,
338399
function_args: Vec<Value>,
339400
tip_req: TipRequest,
401+
cost_tracker: CostTrackerRequest,
340402
) -> StacksHttpRequest {
341403
StacksHttpRequest::new_for_peer(
342404
host,
@@ -345,14 +407,17 @@ impl StacksHttpRequest {
345407
"/v2/contracts/call-read/{}/{}/{}",
346408
&contract_addr, &contract_name, &function_name
347409
),
348-
HttpRequestContents::new().for_tip(tip_req).payload_json(
349-
serde_json::to_value(CallReadOnlyRequestBody {
350-
sender: sender.to_string(),
351-
sponsor: sponsor.map(|s| s.to_string()),
352-
arguments: function_args.into_iter().map(|v| v.to_string()).collect(),
353-
})
354-
.expect("FATAL: failed to encode infallible data"),
355-
),
410+
HttpRequestContents::new()
411+
.for_tip(tip_req)
412+
.query_arg("cost_tracker".to_string(), cost_tracker.to_string())
413+
.payload_json(
414+
serde_json::to_value(CallReadOnlyRequestBody {
415+
sender: sender.to_string(),
416+
sponsor: sponsor.map(|s| s.to_string()),
417+
arguments: function_args.into_iter().map(|v| v.to_string()).collect(),
418+
})
419+
.expect("FATAL: failed to encode infallible data"),
420+
),
356421
)
357422
.expect("FATAL: failed to construct request from infallible data")
358423
}

stackslib/src/net/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ impl StacksHttp {
7373
self.register_rpc_endpoint(callreadonly::RPCCallReadOnlyRequestHandler::new(
7474
self.maximum_call_argument_size,
7575
self.read_only_call_limit.clone(),
76+
self.read_only_max_execution_time,
7677
));
7778
self.register_rpc_endpoint(getaccount::RPCGetAccountRequestHandler::new());
7879
self.register_rpc_endpoint(getattachment::RPCGetAttachmentRequestHandler::new());

stackslib/src/net/api/tests/callreadonly.rs

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
18+
use std::time::Duration;
1819

1920
use clarity::types::chainstate::StacksBlockId;
2021
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, StacksAddressExtensions};
2122
use stacks_common::types::chainstate::StacksAddress;
2223
use stacks_common::types::Address;
2324

24-
use super::test_rpc;
25+
use super::{test_rpc, test_rpc_with_config};
2526
use crate::core::BLOCK_LIMIT_MAINNET_21;
27+
use crate::net::api::callreadonly::CostTrackerRequest;
2628
use crate::net::api::*;
2729
use crate::net::connection::ConnectionOptions;
2830
use crate::net::httpcore::{
@@ -47,6 +49,7 @@ fn test_try_parse_request() {
4749
"ro-test".try_into().unwrap(),
4850
vec![],
4951
TipRequest::SpecificTip(StacksBlockId([0x22; 32])),
52+
CostTrackerRequest::Limited,
5053
);
5154
assert_eq!(
5255
request.contents().tip_request(),
@@ -58,8 +61,11 @@ fn test_try_parse_request() {
5861
debug!("Request:\n{}\n", std::str::from_utf8(&bytes).unwrap());
5962

6063
let (parsed_preamble, offset) = http.read_preamble(&bytes).unwrap();
61-
let mut handler =
62-
callreadonly::RPCCallReadOnlyRequestHandler::new(4096, BLOCK_LIMIT_MAINNET_21);
64+
let mut handler = callreadonly::RPCCallReadOnlyRequestHandler::new(
65+
4096,
66+
BLOCK_LIMIT_MAINNET_21,
67+
Duration::from_secs(30),
68+
);
6369
let mut parsed_request = http
6470
.handle_try_parse_request(
6571
&mut handler,
@@ -119,6 +125,7 @@ fn test_try_make_response() {
119125
"ro-confirmed".try_into().unwrap(),
120126
vec![],
121127
TipRequest::UseLatestAnchoredTip,
128+
CostTrackerRequest::Limited,
122129
);
123130
requests.push(request);
124131

@@ -134,6 +141,7 @@ fn test_try_make_response() {
134141
"ro-test".try_into().unwrap(),
135142
vec![],
136143
TipRequest::UseLatestUnconfirmedTip,
144+
CostTrackerRequest::Limited,
137145
);
138146
requests.push(request);
139147

@@ -149,6 +157,7 @@ fn test_try_make_response() {
149157
"does-not-exist".try_into().unwrap(),
150158
vec![],
151159
TipRequest::UseLatestUnconfirmedTip,
160+
CostTrackerRequest::Limited,
152161
);
153162
requests.push(request);
154163

@@ -164,6 +173,7 @@ fn test_try_make_response() {
164173
"ro-test".try_into().unwrap(),
165174
vec![],
166175
TipRequest::UseLatestUnconfirmedTip,
176+
CostTrackerRequest::Limited,
167177
);
168178
requests.push(request);
169179

@@ -179,6 +189,7 @@ fn test_try_make_response() {
179189
"ro-confirmed".try_into().unwrap(),
180190
vec![],
181191
TipRequest::SpecificTip(StacksBlockId([0x11; 32])),
192+
CostTrackerRequest::Limited,
182193
);
183194
requests.push(request);
184195

@@ -269,3 +280,119 @@ fn test_try_make_response() {
269280
let (preamble, payload) = response.destruct();
270281
assert_eq!(preamble.status_code, 404);
271282
}
283+
284+
#[test]
285+
fn test_try_make_response_free_cost_tracker() {
286+
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333);
287+
288+
let mut requests = vec![];
289+
290+
// query confirmed tip
291+
let request = StacksHttpRequest::new_callreadonlyfunction(
292+
addr.into(),
293+
StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(),
294+
"hello-world".try_into().unwrap(),
295+
StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R")
296+
.unwrap()
297+
.to_account_principal(),
298+
None,
299+
"ro-confirmed".try_into().unwrap(),
300+
vec![],
301+
TipRequest::UseLatestAnchoredTip,
302+
CostTrackerRequest::Free,
303+
);
304+
requests.push(request);
305+
306+
let mut responses = test_rpc_with_config(
307+
function_name!(),
308+
requests,
309+
|peer_1_config| {
310+
peer_1_config
311+
.connection_opts
312+
.read_only_max_execution_time_secs = 0
313+
},
314+
|peer_2_config| {
315+
peer_2_config
316+
.connection_opts
317+
.read_only_max_execution_time_secs = 0
318+
},
319+
);
320+
321+
// confirmed tip
322+
let response = responses.remove(0);
323+
debug!(
324+
"Response:\n{}\n",
325+
std::str::from_utf8(&response.try_serialize().unwrap()).unwrap()
326+
);
327+
328+
assert_eq!(
329+
response.preamble().get_canonical_stacks_tip_height(),
330+
Some(1)
331+
);
332+
333+
let resp = response.decode_call_readonly_response().unwrap();
334+
335+
assert!(!resp.okay);
336+
assert!(resp.result.is_none());
337+
assert!(resp.cause.is_some());
338+
339+
assert_eq!(resp.cause.unwrap(), "Unchecked(ExecutionTimeExpired)");
340+
}
341+
342+
#[test]
343+
fn test_try_make_response_invalid_cost_tracker() {
344+
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333);
345+
346+
let mut requests = vec![];
347+
348+
// query confirmed tip
349+
let request = StacksHttpRequest::new_callreadonlyfunction(
350+
addr.into(),
351+
StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(),
352+
"hello-world".try_into().unwrap(),
353+
StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R")
354+
.unwrap()
355+
.to_account_principal(),
356+
None,
357+
"ro-confirmed".try_into().unwrap(),
358+
vec![],
359+
TipRequest::UseLatestAnchoredTip,
360+
CostTrackerRequest::Invalid,
361+
);
362+
requests.push(request);
363+
364+
let mut responses = test_rpc_with_config(
365+
function_name!(),
366+
requests,
367+
|peer_1_config| {
368+
peer_1_config
369+
.connection_opts
370+
.read_only_max_execution_time_secs = 0
371+
},
372+
|peer_2_config| {
373+
peer_2_config
374+
.connection_opts
375+
.read_only_max_execution_time_secs = 0
376+
},
377+
);
378+
379+
// confirmed tip
380+
let response = responses.remove(0);
381+
debug!(
382+
"Response:\n{}\n",
383+
std::str::from_utf8(&response.try_serialize().unwrap()).unwrap()
384+
);
385+
386+
assert_eq!(
387+
response.preamble().get_canonical_stacks_tip_height(),
388+
Some(1)
389+
);
390+
391+
let resp = response.decode_call_readonly_response().unwrap();
392+
393+
assert!(!resp.okay);
394+
assert!(resp.result.is_none());
395+
assert!(resp.cause.is_some());
396+
397+
assert_eq!(resp.cause.unwrap(), "Interpreter(Expect(\"Interpreter failure during cost calculation: Invalid cost tracker\"))");
398+
}

0 commit comments

Comments
 (0)