Skip to content

Commit

Permalink
Add SC recursion limit (#4729)
Browse files Browse the repository at this point in the history
* Asc message execution - requery message bytecode after each message execution (#4710)

* Requery bytecode

* cargo fmt

* fix call stack inconsistency (#4709)

* Improve async message checks (#4706)

* Improve async message checks

* Change checks for async messages

* Add unit tests

* Fix ledger change to take into account cancelled message balance change (#4715)

* Take again the speculative changes after async message cancellation

* use .apply() to merge the two LedgerChanges

* Fix: we cannot combine two ledger changes with apply

* avoid cloning the changes

* Remove comment

* Fix async msg same slot (#4718)

* fix open rpc spec (#4716)

* Add eliminated_new_messages in eliminated_msg

---------

Co-authored-by: Modship <[email protected]>

* Add initial code for recursion limit

* Latest runtime

* Run CI on PRs based on mainnet_2_3

* fmt

* Fix config and add UTs

* Update scenarios_mandatories.rs

* Review comments (CI for all branches starting with "mainnet_" + comment)

* Update ci.yml

* Remove manual increment / decrement in interface implementation

* fmt + update sc_runtime + fix warning

* Update test

* Update constants.rs

* Updated execution config for tests

* Updated usize -> u16 for recursion counter and limits

* Update test comments

* Add comments regarding the needs of this limits

* Update sc-runtime branch

---------

Co-authored-by: Modship <[email protected]>
  • Loading branch information
Leo-Besancon and modship authored Oct 16, 2024
1 parent b691160 commit 3833083
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 19 deletions.
32 changes: 16 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions massa-execution-exports/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ pub struct ExecutionConfig {
pub max_execution_traces_slot_limit: usize,
/// Where to dump blocks
pub block_dump_folder_path: PathBuf,
/// Max recursive calls depth in SC
/// Used to limit the recursion_counter value in the context, to avoid stack overflow issues.
pub max_recursive_calls_depth: u16,
/// Runtime condom middleware limits
pub condom_limits: CondomLimits,
}
1 change: 1 addition & 0 deletions massa-execution-exports/src/test_exports/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ impl Default for ExecutionConfig {
broadcast_slot_execution_traces_channel_capacity: 5000,
max_execution_traces_slot_limit: 320,
block_dump_folder_path,
max_recursive_calls_depth: 25,
condom_limits: CondomLimits {
max_exports: Some(100),
max_functions: Some(100),
Expand Down
11 changes: 11 additions & 0 deletions massa-execution-worker/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ pub struct ExecutionContextSnapshot {
/// The gas remaining before the last subexecution.
/// so *excluding* the gas used by the last sc call.
pub gas_remaining_before_subexecution: Option<u64>,

/// recursion counter, incremented for each new nested call
/// This is used to avoid stack overflow issues in the VM (that would crash the node instead of failing the call),
/// by limiting the depth of recursion contracts can have with the max_recursive_calls_depth value.
pub recursion_counter: u16,
}

/// An execution context that needs to be initialized before executing bytecode,
Expand Down Expand Up @@ -179,6 +184,9 @@ pub struct ExecutionContext {
/// The gas remaining before the last subexecution.
/// so *excluding* the gas used by the last sc call.
pub gas_remaining_before_subexecution: Option<u64>,

/// recursion counter, incremented for each new nested call
pub recursion_counter: u16,
}

impl ExecutionContext {
Expand Down Expand Up @@ -244,6 +252,7 @@ impl ExecutionContext {
address_factory: AddressFactory { mip_store },
execution_trail_hash,
gas_remaining_before_subexecution: None,
recursion_counter: 0,
}
}

Expand All @@ -265,6 +274,7 @@ impl ExecutionContext {
event_count: self.events.0.len(),
unsafe_rng: self.unsafe_rng.clone(),
gas_remaining_before_subexecution: self.gas_remaining_before_subexecution,
recursion_counter: self.recursion_counter,
}
}

Expand Down Expand Up @@ -293,6 +303,7 @@ impl ExecutionContext {
self.stack = snapshot.stack;
self.unsafe_rng = snapshot.unsafe_rng;
self.gas_remaining_before_subexecution = snapshot.gas_remaining_before_subexecution;
self.recursion_counter = snapshot.recursion_counter;

// For events, set snapshot delta to error events.
for event in self.events.0.range_mut(snapshot.event_count..) {
Expand Down
23 changes: 23 additions & 0 deletions massa-execution-worker/src/interface_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,29 @@ impl Interface for InterfaceImpl {
Ok(())
}

fn increment_recursion_counter(&self) -> Result<()> {
let mut context = context_guard!(self);

context.recursion_counter += 1;

if context.recursion_counter > self.config.max_recursive_calls_depth {
bail!("recursion depth limit reached");
}

Ok(())
}

fn decrement_recursion_counter(&self) -> Result<()> {
let mut context = context_guard!(self);

match context.recursion_counter.checked_sub(1) {
Some(value) => context.recursion_counter = value,
None => bail!("recursion counter underflow"),
}

Ok(())
}

/// Initialize the call when bytecode calls a function from another bytecode
/// This function transfers the coins passed as parameter,
/// prepares the current execution context by pushing a new element on the top of the call stack,
Expand Down
183 changes: 183 additions & 0 deletions massa-execution-worker/src/tests/scenarios_mandatories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,189 @@ fn test_nested_call_gas_usage() {
);
}

/// Test the recursion depth limit in nested calls using call SC operation
///
/// We call a smart contract that has a nested function call, while setting the max_recursive_calls_depth to 0.
/// We expect the execution of the smart contract call to fail with a message that the recursion depth limit was reached.
#[test]
fn test_nested_call_recursion_limit_reached() {
// setup the period duration
let exec_cfg = ExecutionConfig {
max_recursive_calls_depth: 0, // This limit will be reached
..Default::default()
};

let finalized_waitpoint = WaitPoint::new();
let mut foreign_controllers = ExecutionForeignControllers::new_with_mocks();
selector_boilerplate(&mut foreign_controllers.selector_controller);

foreign_controllers
.ledger_controller
.set_expectations(|ledger_controller| {
ledger_controller
.expect_get_balance()
.returning(move |_| Some(Amount::from_str("100").unwrap()));

ledger_controller
.expect_entry_exists()
.times(2)
.returning(move |_| false);

ledger_controller
.expect_entry_exists()
.times(1)
.returning(move |_| true);
});
let saved_bytecode = expect_finalize_deploy_and_call_blocks(
Slot::new(1, 0),
Some(Slot::new(1, 1)),
finalized_waitpoint.get_trigger_handle(),
&mut foreign_controllers.final_state,
);
final_state_boilerplate(
&mut foreign_controllers.final_state,
foreign_controllers.db.clone(),
&foreign_controllers.selector_controller,
&mut foreign_controllers.ledger_controller,
Some(saved_bytecode),
None,
None,
);
let mut universe = ExecutionTestUniverse::new(foreign_controllers, exec_cfg);

// load bytecodes
universe.deploy_bytecode_block(
&KeyPair::from_str(TEST_SK_1).unwrap(),
Slot::new(1, 0),
include_bytes!("./wasm/nested_call.wasm"),
include_bytes!("./wasm/test.wasm"),
);
finalized_waitpoint.wait();
let address = universe.get_address_sc_deployed(Slot::new(1, 0));

// Call the function test of the smart contract
let operation = ExecutionTestUniverse::create_call_sc_operation(
&KeyPair::from_str(TEST_SK_2).unwrap(),
10000000,
Amount::from_str("0").unwrap(),
Amount::from_str("0").unwrap(),
Address::from_str(&address).unwrap(),
String::from("test"),
address.as_bytes().to_vec(),
)
.unwrap();
universe.call_sc_block(
&KeyPair::from_str(TEST_SK_2).unwrap(),
Slot::new(1, 1),
operation,
);
finalized_waitpoint.wait();

// Get the events of the smart contract execution. We expect the call to have failed, so we check for the error message.
let events = universe
.module_controller
.get_filtered_sc_output_event(EventFilter {
start: Some(Slot::new(1, 1)),
..Default::default()
});
assert!(events.len() >= 2);
//println!("events: {:?}", events);
assert!(events[1].data.contains("recursion depth limit reached"));
}

/// Test the recursion depth limit in nested calls using call SC operation
///
/// We call a smart contract that has a nested function call, while setting the max_recursive_calls_depth to 2.
/// We expect the execution of the smart contract call to succeed as the recursion depth limit was not reached.
#[test]
fn test_nested_call_recursion_limit_not_reached() {
// setup the period duration
let exec_cfg = ExecutionConfig {
max_recursive_calls_depth: 2, // This limit will not be reached
..Default::default()
};

let finalized_waitpoint = WaitPoint::new();
let mut foreign_controllers = ExecutionForeignControllers::new_with_mocks();
selector_boilerplate(&mut foreign_controllers.selector_controller);

foreign_controllers
.ledger_controller
.set_expectations(|ledger_controller| {
ledger_controller
.expect_get_balance()
.returning(move |_| Some(Amount::from_str("100").unwrap()));

ledger_controller
.expect_entry_exists()
.times(2)
.returning(move |_| false);

ledger_controller
.expect_entry_exists()
.times(1)
.returning(move |_| true);
});
let saved_bytecode = expect_finalize_deploy_and_call_blocks(
Slot::new(1, 0),
Some(Slot::new(1, 1)),
finalized_waitpoint.get_trigger_handle(),
&mut foreign_controllers.final_state,
);
final_state_boilerplate(
&mut foreign_controllers.final_state,
foreign_controllers.db.clone(),
&foreign_controllers.selector_controller,
&mut foreign_controllers.ledger_controller,
Some(saved_bytecode),
None,
None,
);
let mut universe = ExecutionTestUniverse::new(foreign_controllers, exec_cfg);

// load bytecodes
universe.deploy_bytecode_block(
&KeyPair::from_str(TEST_SK_1).unwrap(),
Slot::new(1, 0),
include_bytes!("./wasm/nested_call.wasm"),
include_bytes!("./wasm/test.wasm"),
);
finalized_waitpoint.wait();
let address = universe.get_address_sc_deployed(Slot::new(1, 0));

// Call the function test of the smart contract
let operation = ExecutionTestUniverse::create_call_sc_operation(
&KeyPair::from_str(TEST_SK_2).unwrap(),
10000000,
Amount::from_str("0").unwrap(),
Amount::from_str("0").unwrap(),
Address::from_str(&address).unwrap(),
String::from("test"),
address.as_bytes().to_vec(),
)
.unwrap();
universe.call_sc_block(
&KeyPair::from_str(TEST_SK_2).unwrap(),
Slot::new(1, 1),
operation,
);
finalized_waitpoint.wait();

// Get the events. We expect the call to have succeeded, so we check for the length of the events.
// The smart contract emits 4 events in total, (to check gas usage), so we expect at least 4 events,
// and none of them should contain the error message.
let events = universe
.module_controller
.get_filtered_sc_output_event(EventFilter {
start: Some(Slot::new(1, 1)),
..Default::default()
});
assert!(events.len() >= 4);
for event in events.iter() {
assert!(!event.data.contains("recursion depth limit reached"));
}
}

/// Test the ABI get call coins
///
/// Deploy an SC with a method `test` that generate an event saying how many coins he received
Expand Down
Loading

0 comments on commit 3833083

Please sign in to comment.