Skip to content

Commit

Permalink
test: Add tests to check that changes are not applied if replicated q…
Browse files Browse the repository at this point in the history
…uery execution traps (#3669)
  • Loading branch information
dsarlis authored Jan 29, 2025
1 parent e6b3156 commit a99f598
Showing 1 changed file with 122 additions and 0 deletions.
122 changes: 122 additions & 0 deletions rs/execution_environment/src/execution_environment/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2830,6 +2830,91 @@ fn replicated_query_can_accept_cycles() {
);
}

#[test]
fn replicated_query_does_not_accept_cycles_on_trap() {
let mut test = ExecutionTestBuilder::new().with_manual_execution().build();
let initial_cycles = Cycles::new(1_000_000_000_000);
let a_id = test.universal_canister_with_cycles(initial_cycles).unwrap();
let b_id = test.universal_canister_with_cycles(initial_cycles).unwrap();
let transferred_cycles = Cycles::from(1_000_000u128);

// Canister B attempts to accept cycles in a replicated query. Should succeed.
let b_callback = wasm()
.accept_cycles(transferred_cycles)
.message_payload()
.append_and_reply()
.trap()
.build();

let a_payload = wasm()
.call_with_cycles(
b_id,
"query",
call_args().other_side(b_callback.clone()),
transferred_cycles,
)
.build();

let (message_id, _) = test.ingress_raw(a_id, "update", a_payload);

test.execute_message(a_id);
test.induct_messages();
test.execute_message(b_id);

let system_state = &mut test.canister_state_mut(b_id).system_state;

assert_eq!(1, system_state.queues().output_queues_len());
assert_eq!(1, system_state.queues().output_queues_message_count());

let message = system_state
.queues_mut()
.clone()
.pop_canister_output(&a_id)
.unwrap();

let reject_message = if let RequestOrResponse::Response(msg) = message {
assert_eq!(msg.originator, a_id);
assert_eq!(msg.respondent, b_id);
assert_eq!(msg.refund, transferred_cycles);
if let Payload::Reject(context) = msg.response_payload.clone() {
context.message().clone()
} else {
panic!("unexpected response: {:?}", msg);
}
} else {
panic!("unexpected message popped: {:?}", message);
};

test.induct_messages();
test.execute_message(a_id);

let ingress_state = test.ingress_state(&message_id);

if let IngressState::Completed(wasm_result) = ingress_state {
match wasm_result {
WasmResult::Reject(_) => (),
WasmResult::Reply(_) => panic!("expected reject"),
}
} else {
panic!("unexpected ingress state {:?}", ingress_state);
};

// Canister A does not lose `transferred_cycles` since B trapped after accepting.
assert_eq!(
test.canister_state(a_id).system_state.balance(),
initial_cycles
- test.canister_execution_cost(a_id)
- test.call_fee("query", &b_callback)
- test.reject_fee(reject_message)
);

// Canister B does not get any cycles.
assert_eq!(
test.canister_state(b_id).system_state.balance(),
initial_cycles - test.canister_execution_cost(b_id)
);
}

#[test]
fn replicated_query_can_burn_cycles() {
let mut test = ExecutionTestBuilder::new().with_manual_execution().build();
Expand Down Expand Up @@ -2868,6 +2953,43 @@ fn replicated_query_can_burn_cycles() {
assert_eq!(burned_cycles, NominalCycles::from(cycles_to_burn));
}

#[test]
fn replicated_query_does_not_burn_cycles_on_trap() {
let mut test = ExecutionTestBuilder::new().with_manual_execution().build();
let initial_cycles = Cycles::new(1_000_000_000_000);
let canister_id = test.universal_canister_with_cycles(initial_cycles).unwrap();
let cycles_to_burn = Cycles::new(10_000_000u128);

let payload = wasm().cycles_burn128(cycles_to_burn).reply().trap().build();
let (message_id, _) = test.ingress_raw(canister_id, "query", payload);
test.execute_message(canister_id);

let ingress_state = test.ingress_state(&message_id);
if let IngressState::Failed(user_error) = ingress_state {
assert_eq!(user_error.code(), ErrorCode::CanisterCalledTrap);
assert!(user_error
.description()
.contains("Canister called `ic0.trap`"),);
} else {
panic!("unexpected ingress state {:?}", ingress_state);
};

// Canister A only loses cycles due to executing but not `cycles_to_burn` (since it trapped)...
assert_eq!(
test.canister_state(canister_id).system_state.balance(),
initial_cycles - test.canister_execution_cost(canister_id)
);

// ...and no burned cycles are accounted for in the canister's metrics.
assert!(test
.canister_state(canister_id)
.system_state
.canister_metrics
.get_consumed_cycles_by_use_cases()
.get(&CyclesUseCase::BurnedCycles)
.is_none());
}

#[test]
fn test_consumed_cycles_by_use_case_with_refund() {
let mut test = ExecutionTestBuilder::new().with_manual_execution().build();
Expand Down

0 comments on commit a99f598

Please sign in to comment.