@@ -23,7 +23,7 @@ use ic_test_utilities_metrics::fetch_int_counter;
2323use ic_types:: ingress:: { IngressState , IngressStatus , WasmResult } ;
2424use ic_types:: messages:: MessageId ;
2525use ic_types:: { CanisterId , ComputeAllocation , MemoryAllocation , NumBytes , NumInstructions } ;
26- use ic_types_cycles:: { Cycles , CyclesUseCase , NominalCycles } ;
26+ use ic_types_cycles:: { CompoundCycles , Cycles , CyclesUseCase , Instructions , NominalCycles } ;
2727use ic_types_test_utils:: ids:: { canister_test_id, subnet_test_id, user_test_id} ;
2828use ic_universal_canister:: { UNIVERSAL_CANISTER_WASM , call_args, wasm} ;
2929use maplit:: btreemap;
@@ -139,19 +139,38 @@ fn consumed_cycles_for_instructions(
139139 . unwrap_or_default ( )
140140}
141141
142- /// Analogously to `dts_resume_fails_due_to_cycles_decrease` for calls,
143- /// replicated queries, callbacks, and tasks, resuming a paused `install_code`
144- /// whose canister lost cycles while it was paused fails instead of replaying the
145- /// recorded steps on a balance that can no longer cover them. The failed
146- /// execution is charged for exactly the instructions it had already executed,
147- /// including those of the paused Wasm execution.
148- #[ test]
149- fn dts_install_code_resume_fails_due_to_cycles_decrease ( ) {
150- const INSTRUCTION_LIMIT : u64 = 50_000_000 ;
151- const SLICE_INSTRUCTION_LIMIT : u64 = 132_000 ;
142+ /// The instruction limits of the DTS `install_code` tests that change the cycles
143+ /// balance of the canister while its execution is paused.
144+ const DTS_INSTALL_CODE_INSTRUCTION_LIMIT : u64 = 50_000_000 ;
145+ const DTS_INSTALL_CODE_SLICE_INSTRUCTION_LIMIT : u64 = 132_000 ;
146+
147+ /// A canister with a paused `install_code` execution, along with a snapshot of
148+ /// the accounting counters of that canister taken before the execution started.
149+ ///
150+ /// Shared by the tests that decrease and increase the cycles balance of the
151+ /// canister while its `install_code` execution is paused.
152+ struct PausedInstallCode {
153+ test : ExecutionTest ,
154+ canister_id : CanisterId ,
155+ /// The ingress message of the `install_code` subnet message.
156+ ingress_id : MessageId ,
157+ /// The cycles balance before the execution cycles were prepaid.
158+ original_balance : Cycles ,
159+ /// The cycles consumed for instructions.
160+ original_consumed_cycles : NominalCycles ,
161+ /// The instructions executed by all the slices of the canister.
162+ original_executed_instructions : NumInstructions ,
163+ /// The accumulated cost of the instructions executed by the canister.
164+ original_execution_cost : CompoundCycles < Instructions > ,
165+ }
166+
167+ /// Starts an `install_code` execution that pauses after its first slice: that
168+ /// slice compiles the Wasm module and executes its `(start)` function, which
169+ /// together exceed the slice instruction limit.
170+ fn install_code_paused_after_first_slice ( ) -> PausedInstallCode {
152171 let mut test = ExecutionTestBuilder :: new ( )
153- . with_install_code_instruction_limit ( INSTRUCTION_LIMIT )
154- . with_install_code_slice_instruction_limit ( SLICE_INSTRUCTION_LIMIT )
172+ . with_install_code_instruction_limit ( DTS_INSTALL_CODE_INSTRUCTION_LIMIT )
173+ . with_install_code_slice_instruction_limit ( DTS_INSTALL_CODE_SLICE_INSTRUCTION_LIMIT )
155174 . with_create_execution_state_base_cost ( 0 )
156175 . with_manual_execution ( )
157176 . build ( ) ;
@@ -169,6 +188,7 @@ fn dts_install_code_resume_fails_due_to_cycles_decrease() {
169188 let original_balance = test. canister_state ( canister_id) . system_state . balance ( ) ;
170189 let original_consumed_cycles = consumed_cycles_for_instructions ( & test, canister_id) ;
171190 let original_executed_instructions = test. canister_executed_instructions ( canister_id) ;
191+ let original_execution_cost = test. canister_execution_cost ( canister_id) ;
172192
173193 let ingress_id = test. dts_install_code ( payload) ;
174194
@@ -187,13 +207,42 @@ fn dts_install_code_resume_fails_due_to_cycles_decrease() {
187207 - test
188208 . cycles_account_manager( )
189209 . execution_cost(
190- NumInstructions :: from( INSTRUCTION_LIMIT ) ,
210+ NumInstructions :: from( DTS_INSTALL_CODE_INSTRUCTION_LIMIT ) ,
191211 test. get_own_subnet_cycles_config( ) ,
192212 WASM_EXECUTION_MODE ,
193213 )
194214 . real( ) ,
195215 ) ;
196216
217+ PausedInstallCode {
218+ test,
219+ canister_id,
220+ ingress_id,
221+ original_balance,
222+ original_consumed_cycles,
223+ original_executed_instructions,
224+ original_execution_cost,
225+ }
226+ }
227+
228+ /// Analogously to `dts_resume_fails_due_to_cycles_decrease` for calls,
229+ /// replicated queries, callbacks, and tasks, resuming a paused `install_code`
230+ /// whose canister lost cycles while it was paused fails instead of replaying the
231+ /// recorded steps on a balance that can no longer cover them. The failed
232+ /// execution is charged for exactly the instructions it had already executed,
233+ /// including those of the paused Wasm execution.
234+ #[ test]
235+ fn dts_install_code_resume_fails_due_to_cycles_decrease ( ) {
236+ let PausedInstallCode {
237+ mut test,
238+ canister_id,
239+ ingress_id,
240+ original_balance,
241+ original_consumed_cycles,
242+ original_executed_instructions,
243+ ..
244+ } = install_code_paused_after_first_slice ( ) ;
245+
197246 // Decrease the cycles balance of the clean canister.
198247 test. canister_state_mut ( canister_id)
199248 . system_state
@@ -224,7 +273,10 @@ fn dts_install_code_resume_fails_due_to_cycles_decrease() {
224273 // more than the slice instruction limit.
225274 let executed_instructions =
226275 test. canister_executed_instructions ( canister_id) - original_executed_instructions;
227- assert_gt ! ( executed_instructions. get( ) , SLICE_INSTRUCTION_LIMIT ) ;
276+ assert_gt ! (
277+ executed_instructions. get( ) ,
278+ DTS_INSTALL_CODE_SLICE_INSTRUCTION_LIMIT
279+ ) ;
228280
229281 // The canister is charged exactly the cost of those instructions, including
230282 // the instructions of the paused Wasm execution: the rest of the prepaid
@@ -245,6 +297,62 @@ fn dts_install_code_resume_fails_due_to_cycles_decrease() {
245297 ) ;
246298}
247299
300+ /// Counterpart of `dts_install_code_resume_fails_due_to_cycles_decrease`: while
301+ /// resuming a paused `install_code` whose canister lost cycles fails, an
302+ /// increase of the cycles balance while the execution is paused is tolerated and
303+ /// the additional cycles are not lost when the execution completes.
304+ #[ test]
305+ fn dts_install_code_resume_succeeds_after_cycles_increase ( ) {
306+ const CYCLES_ADDED_WHILE_PAUSED : Cycles = Cycles :: new ( 1_234_567_890 ) ;
307+
308+ let PausedInstallCode {
309+ mut test,
310+ canister_id,
311+ ingress_id,
312+ original_balance,
313+ original_executed_instructions,
314+ original_execution_cost,
315+ ..
316+ } = install_code_paused_after_first_slice ( ) ;
317+
318+ // Increase the cycles balance of the clean canister.
319+ test. canister_state_mut ( canister_id)
320+ . system_state
321+ . add_cycles ( CYCLES_ADDED_WHILE_PAUSED ) ;
322+
323+ // The remaining slices resume the paused execution, which completes.
324+ while test. canister_state ( canister_id) . next_execution ( ) == NextExecution :: ContinueInstallCode {
325+ test. execute_slice ( canister_id) ;
326+ }
327+ assert_eq ! (
328+ test. canister_state( canister_id) . next_execution( ) ,
329+ NextExecution :: None
330+ ) ;
331+
332+ let result = check_ingress_status ( test. ingress_status ( & ingress_id) ) . unwrap ( ) ;
333+ assert_eq ! ( result, WasmResult :: Reply ( EmptyBlob . encode( ) ) ) ;
334+
335+ // The code has been installed.
336+ assert ! ( test. canister_state( canister_id) . execution_state. is_some( ) ) ;
337+
338+ // The execution spanned multiple slices.
339+ let executed_instructions =
340+ test. canister_executed_instructions ( canister_id) - original_executed_instructions;
341+ assert_gt ! (
342+ executed_instructions. get( ) ,
343+ DTS_INSTALL_CODE_SLICE_INSTRUCTION_LIMIT
344+ ) ;
345+
346+ // The canister is charged exactly the cost of the executed instructions: the
347+ // cycles added while the execution was paused are not lost.
348+ assert_eq ! (
349+ test. canister_state( canister_id) . system_state. balance( ) ,
350+ original_balance
351+ - ( test. canister_execution_cost( canister_id) - original_execution_cost) . real( )
352+ + CYCLES_ADDED_WHILE_PAUSED
353+ ) ;
354+ }
355+
248356#[ test]
249357fn dts_abort_works_in_install_code ( ) {
250358 const INSTRUCTION_LIMIT : u64 = 50_000_000 ;
0 commit comments