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

Add initial code for recursion limit #4729

Merged
merged 24 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
04ed29c
Asc message execution - requery message bytecode after each message e…
Leo-Besancon Jun 18, 2024
52814d4
fix call stack inconsistency (#4709)
Leo-Besancon Jun 21, 2024
b64e360
Improve async message checks (#4706)
Leo-Besancon Jun 25, 2024
86e1d02
Fix ledger change to take into account cancelled message balance chan…
Leo-Besancon Jul 2, 2024
880eec7
Fix async msg same slot (#4718)
Leo-Besancon Jul 2, 2024
e25f021
Add initial code for recursion limit
Leo-Besancon Jul 16, 2024
9c13a14
Latest runtime
Leo-Besancon Jul 16, 2024
c0f3a0a
Run CI on PRs based on mainnet_2_3
Leo-Besancon Jul 16, 2024
57bf59d
fmt
Leo-Besancon Jul 16, 2024
ad071b5
Fix config and add UTs
Leo-Besancon Jul 16, 2024
fb20aec
Update scenarios_mandatories.rs
Leo-Besancon Jul 16, 2024
7c7c32f
Review comments (CI for all branches starting with "mainnet_" + comment)
Leo-Besancon Jul 17, 2024
2cc4a17
Update ci.yml
Leo-Besancon Jul 17, 2024
766f044
Remove manual increment / decrement in interface implementation
Leo-Besancon Aug 5, 2024
aa77944
Merge branch 'mainnet_2_3' into add_recursion_counter
Leo-Besancon Oct 15, 2024
3f37c1d
fmt + update sc_runtime + fix warning
Leo-Besancon Oct 15, 2024
e12ee3c
Merge branch 'mainnet_2_3' into add_recursion_counter
Leo-Besancon Oct 15, 2024
8504ced
Update test
Leo-Besancon Oct 15, 2024
e36e31d
Update constants.rs
Leo-Besancon Oct 15, 2024
869c46a
Updated execution config for tests
Leo-Besancon Oct 16, 2024
0069fd5
Updated usize -> u16 for recursion counter and limits
Leo-Besancon Oct 16, 2024
8c5ff99
Update test comments
Leo-Besancon Oct 16, 2024
d351f76
Add comments regarding the needs of this limits
Leo-Besancon Oct 16, 2024
b3cf306
Update sc-runtime branch
Leo-Besancon Oct 16, 2024
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
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
Loading