1- use candid:: { CandidType , Encode } ;
1+ use candid:: { Decode , Encode } ;
22use ic_base_types:: { CanisterId , PrincipalId } ;
33use ic_management_canister_types_private:: { CanisterSnapshotResponse , ListCanisterSnapshotArgs } ;
44use ic_nns_constants:: { GOVERNANCE_CANISTER_ID , ROOT_CANISTER_ID } ;
55use ic_nns_governance:: pb:: v1:: ProposalStatus ;
66use ic_nns_governance_api:: {
7- ExecuteNnsFunction , MakeProposalRequest , NnsFunction , ProposalActionRequest ,
8- manage_neuron_response:: Command ,
7+ ExecuteNnsFunction , MakeProposalRequest , Motion , NnsFunction , ProposalActionRequest ,
8+ ProposalInfo , manage_neuron_response:: Command ,
99} ;
10+ use ic_nns_handler_root_interface:: { LoadCanisterSnapshotRequest , TakeCanisterSnapshotRequest } ;
1011use ic_nns_test_utils:: {
1112 common:: NnsInitPayloadsBuilder ,
1213 neuron_helpers:: get_neuron_1,
@@ -16,19 +17,10 @@ use ic_nns_test_utils::{
1617 update_with_sender,
1718 } ,
1819} ;
19- use serde:: Deserialize ;
2020use std:: time:: { Duration , SystemTime } ;
2121
22- // Defined in ic_nns_handler_root_interface, but redefined here to avoid extra dependencies
23- // for the test target if not already present.
24- #[ derive( Clone , Eq , PartialEq , Hash , Debug , CandidType , Deserialize ) ]
25- pub struct TakeCanisterSnapshotRequest {
26- pub canister_id : PrincipalId ,
27- pub replace_snapshot : Option < Vec < u8 > > ,
28- }
29-
3022#[ test]
31- fn test_take_canister_snapshot ( ) {
23+ fn test_take_and_load_canister_snapshot ( ) {
3224 // Step 1: Prepare the world: Set up the NNS canisters and get the neuron.
3325
3426 let state_machine = state_machine_builder_for_nns_tests ( ) . build ( ) ;
@@ -196,4 +188,159 @@ fn test_take_canister_snapshot() {
196188 first_snapshot. snapshot_id( ) ,
197189 "{second_snapshot:#?}\n \n vs.\n \n {first_snapshot:#?}"
198190 ) ;
191+
192+ // Step 1C: Prepare the world for LoadCanisterSnapshot. This consists of
193+ // submitting a "marker" (Motion) proposal. It will get blown away by the
194+ // LoadCanisterSnapshot proposal, because it is being created after the
195+ // snapshot loaded by the LoadCanisterSnapshot proposal.
196+ let make_marker_response = nns_governance_make_proposal (
197+ & state_machine,
198+ neuron. principal_id ,
199+ neuron. neuron_id ,
200+ & MakeProposalRequest {
201+ title : Some ( "Marker Proposal" . to_string ( ) ) ,
202+ summary : "This is a marker proposal." . to_string ( ) ,
203+ url : "https://forum.dfinity.org/marker-proposal" . to_string ( ) ,
204+ action : Some ( ProposalActionRequest :: Motion ( Motion {
205+ motion_text : "This proposal should disappear after snapshot load" . to_string ( ) ,
206+ } ) ) ,
207+ } ,
208+ ) ;
209+ let marker_proposal_id = match make_marker_response. command . as_ref ( ) . unwrap ( ) {
210+ Command :: MakeProposal ( response) => response. proposal_id . unwrap ( ) ,
211+ _ => panic ! ( "{make_marker_response:#?}" ) ,
212+ } ;
213+ nns_wait_for_proposal_execution ( & state_machine, marker_proposal_id. id ) ;
214+
215+ // Verify marker exists. After loading the snapshot, this won't work anymore.
216+ let _marker_info: ProposalInfo =
217+ nns_governance_get_proposal_info_as_anonymous ( & state_machine, marker_proposal_id. id ) ;
218+
219+ // Step 2C: Run the code under test by passing a LoadCanisterSnapshot
220+ // proposal. (As is often the case in tests, the proposal passes right away
221+ // due to the proposal being made by a neuron with overwhelming voting
222+ // power.)
223+
224+ // Step 2C.1: Assemble MakeProposalRequest.
225+ let payload = Encode ! ( & LoadCanisterSnapshotRequest {
226+ canister_id: target_canister_id. get( ) ,
227+ // Remember, this snapshot (second_snapshot) was taken BEFORE the marker
228+ // proposal.
229+ snapshot_id: second_snapshot. snapshot_id( ) . to_vec( ) ,
230+ } )
231+ . unwrap ( ) ;
232+ let action = ProposalActionRequest :: ExecuteNnsFunction ( ExecuteNnsFunction {
233+ nns_function : NnsFunction :: LoadCanisterSnapshot as i32 ,
234+ payload,
235+ } ) ;
236+ let make_proposal_request = MakeProposalRequest {
237+ title : Some ( "Restore Governance Canister to Snapshot 2" . to_string ( ) ) ,
238+ summary : r#"This will clobber the "marker" motion proposal."# . to_string ( ) ,
239+ url : "https://forum.dfinity.org/restore-governance-canister-to-snapshot-2" . to_string ( ) ,
240+ action : Some ( action) ,
241+ } ;
242+
243+ // Step 2C.2: Submit the proposal.
244+ let make_proposal_response = nns_governance_make_proposal (
245+ & state_machine,
246+ neuron. principal_id ,
247+ neuron. neuron_id ,
248+ & make_proposal_request,
249+ ) ;
250+ let load_proposal_id = match make_proposal_response. command . as_ref ( ) . unwrap ( ) {
251+ Command :: MakeProposal ( response) => response. proposal_id . unwrap ( ) ,
252+ _ => panic ! ( "{make_proposal_response:#?}" ) ,
253+ } ;
254+
255+ // Step 3C: Verify LoadCanisterSnapshot execution.
256+
257+ // Step 3C.1: Poll until the LoadCanisterSnapshot proposal vanishes (or it
258+ // is marked as fail). If LoadCanisterSnapshot proposals work correctly,
259+ // then the LoadCanisterSnapshot proposal itself would disappear, because
260+ // that proposal itself is not in the (Governance canister) snapshot.
261+ let mut done = false ;
262+ for _ in 0 ..50 {
263+ // Fetch the LoadCanisterSnapshot proposal.
264+ let response_bytes = state_machine
265+ . execute_ingress_as (
266+ PrincipalId :: new_anonymous ( ) ,
267+ GOVERNANCE_CANISTER_ID ,
268+ "get_proposal_info" ,
269+ Encode ! ( & load_proposal_id. id) . unwrap ( ) ,
270+ )
271+ . unwrap ( ) ;
272+ let result = match response_bytes {
273+ ic_types:: ingress:: WasmResult :: Reply ( bytes) => bytes,
274+ ic_types:: ingress:: WasmResult :: Reject ( reason) => {
275+ panic ! ( "get_proposal_info rejected: {reason}" )
276+ }
277+ } ;
278+ let proposal_info: Option < ProposalInfo > =
279+ candid:: Decode !( & result, Option <ProposalInfo >) . unwrap ( ) ;
280+
281+ // If the proposal is suddenly missing, that's actually a sign that it
282+ // worked. In any case, it means we can now proceed with the rest of
283+ // verification.
284+ if proposal_info. is_none ( ) {
285+ println ! (
286+ "As expected, the LoadCanisterSnapshot proposal vanished \
287+ (as a result of its own execution!).",
288+ ) ;
289+ done = true ;
290+ break ;
291+ }
292+
293+ // Exit early if proposal execution failed, since this is a terminal
294+ // state. This is "just" an optimization in that this whole test would
295+ // fail even if we deleted this chunk.
296+ let status = ProposalStatus :: try_from ( proposal_info. unwrap ( ) . status ) ;
297+ if status == Ok ( ProposalStatus :: Failed ) {
298+ panic ! ( "Load Snapshot Proposal failed execution!" ) ;
299+ }
300+
301+ // Sleep before polling again.
302+ state_machine. advance_time ( Duration :: from_secs ( 10 ) ) ;
303+ state_machine. tick ( ) ;
304+ }
305+ assert ! (
306+ done,
307+ "Timeout waiting for Load Snapshot Proposal to vanish \
308+ (as a result of correct execution).",
309+ ) ;
310+
311+ // Step 3C.2: Verify that the MARKER (motion) proposal has (also) been blown
312+ // away (not just the LoadCanisterSnapshot proposal).
313+ let response_bytes = state_machine
314+ . execute_ingress_as (
315+ PrincipalId :: new_anonymous ( ) ,
316+ GOVERNANCE_CANISTER_ID ,
317+ "get_proposal_info" ,
318+ Encode ! ( & marker_proposal_id. id) . unwrap ( ) ,
319+ )
320+ . unwrap ( ) ;
321+ let result = match response_bytes {
322+ ic_types:: ingress:: WasmResult :: Reply ( bytes) => bytes,
323+ ic_types:: ingress:: WasmResult :: Reject ( reason) => {
324+ panic ! ( "get_proposal_info rejected: {reason}" )
325+ }
326+ } ;
327+ let final_marker_proposal_status: Option < ProposalInfo > =
328+ candid:: Decode !( & result, Option <ProposalInfo >) . unwrap ( ) ;
329+ assert_eq ! (
330+ final_marker_proposal_status, None ,
331+ "Marker proposal {} should have been wiped out by snapshot load, \
332+ but it still exists: {:#?}",
333+ marker_proposal_id. id, final_marker_proposal_status
334+ ) ;
335+
336+ // Step 3C.3: Verify that the first proposal is still there (albeit moot,
337+ // since the second proposal clobbered the snapshot created by the first
338+ // proposal.)
339+ let first_proposal_info =
340+ nns_governance_get_proposal_info_as_anonymous ( & state_machine, first_proposal_id. id ) ;
341+ assert_eq ! (
342+ ProposalStatus :: try_from( first_proposal_info. status) ,
343+ Ok ( ProposalStatus :: Executed ) ,
344+ "First proposal should still exist and be executed: {first_proposal_info:#?}" ,
345+ ) ;
199346}
0 commit comments