Skip to content

Commit b8881bb

Browse files
Integration test for (new) LoadCanisterSnapshot proposal type. Piggybacks off existing test for TakeCanisterSnapshot.
1 parent cbd88f0 commit b8881bb

File tree

4 files changed

+163
-13
lines changed

4 files changed

+163
-13
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rs/nns/integration_tests/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ BASE_DEPENDENCIES = [
1818
"//rs/nns/governance/api",
1919
"//rs/nns/governance/init",
2020
"//rs/nns/handlers/lifeline/impl:lifeline",
21+
"//rs/nns/handlers/root/interface",
2122
"//rs/nns/sns-wasm",
2223
"//rs/node_rewards/canister/api",
2324
"//rs/registry/canister/api",

rs/nns/integration_tests/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ ic-nns-common = { path = "../common" }
5050
ic-nns-governance = { path = "../governance" }
5151
ic-nns-governance-api = { path = "../governance/api" }
5252
ic-nns-governance-init = { path = "../governance/init" }
53+
ic-nns-handler-root-interface = { path = "../handlers/root/interface" }
5354
ic-node-rewards-canister-api = { path = "../../node_rewards/canister/api" }
5455
ic-sns-root = { path = "../../sns/root" }
5556
ic-sns-swap = { path = "../../sns/swap" }

rs/nns/integration_tests/src/take_canister_snapshot.rs

Lines changed: 160 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
use candid::{CandidType, Encode};
1+
use candid::{Decode, Encode};
22
use ic_base_types::{CanisterId, PrincipalId};
33
use ic_management_canister_types_private::{CanisterSnapshotResponse, ListCanisterSnapshotArgs};
44
use ic_nns_constants::{GOVERNANCE_CANISTER_ID, ROOT_CANISTER_ID};
55
use ic_nns_governance::pb::v1::ProposalStatus;
66
use 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};
1011
use 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;
2020
use 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\nvs.\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

Comments
 (0)