Skip to content
Open
151 changes: 151 additions & 0 deletions rs/nervous_system/integration_tests/tests/batch_proposal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use candid::Principal;
use ic_base_types::PrincipalId;
use ic_crypto_sha2::Sha256;
use ic_nervous_system_common_test_utils::wasm_helpers::SMALLEST_VALID_WASM_BYTES;
use ic_nervous_system_integration_tests::pocket_ic_helpers::{NnsInstaller, nns};
use ic_nns_constants::ROOT_CANISTER_ID;
use ic_nns_governance_api::{
BatchRequest, CreateCanisterAndInstallCodeRequest, MakeProposalRequest, ProposalActionRequest,
SuccessfulProposalExecutionValue, WasmModule,
};
use pocket_ic::PocketIcBuilder;

/// Verifies that a Batch proposal containing two CreateCanisterAndInstallCode
/// sub-actions is executed successfully and that each sub-action produces a
/// distinct canister ID with the expected WASM installed.
#[tokio::test]
async fn test_batch_of_two_create_canister_and_install_code_proposals() {
// Step 1: Prepare the world.

// Step 1.1: Boot up an IC.
let pocket_ic = PocketIcBuilder::new()
.with_nns_subnet()
// Required by NnsInstaller, but otherwise, not actually used.
.with_sns_subnet()
.build_async()
.await;

// Step 1.2: Install NNS.
let mut nns_installer = NnsInstaller::default();
nns_installer.with_current_nns_canister_versions();
// Required to enable the Batch proposal feature flag.
nns_installer.with_test_governance_canister();
nns_installer.install(&pocket_ic).await;

// Step 2: Call the code under test.

// Step 2.1: Gather ingredients for proposal.

// The subnet where the canisters will be created.
let topology = pocket_ic.topology().await;
let host_subnet_id = PrincipalId::from(topology.get_nns().unwrap());

// Code that will be installed into the new canisters.
let wasm_a: Vec<u8> = SMALLEST_VALID_WASM_BYTES.to_vec();
let wasm_b: Vec<u8> = {
let mut w = wasm_a.clone();
// Append a valid custom section (id=0, size=2, name_len=0, data=[0x42]).
w.extend_from_slice(&[0x00, 0x02, 0x00, 0x42]);
w
};
// Used later for verification.
let hashes = [
Sha256::hash(&wasm_a).to_vec(),
Sha256::hash(&wasm_b).to_vec(),
];

// Step 2.2: Assemble the kernel of the proposal, the Batch itself.
let actions = [wasm_a, wasm_b]
.into_iter()
.map(|wasm: Vec<u8>| -> ProposalActionRequest {
let result = CreateCanisterAndInstallCodeRequest {
host_subnet_id: Some(host_subnet_id),
wasm_module: Some(WasmModule::Inlined(wasm)),
canister_settings: None,
install_arg: None,
};

ProposalActionRequest::CreateCanisterAndInstallCode(result)
})
.collect();
let batch = BatchRequest {
actions: Some(actions),
};

// Step 2.3: Execute the proposal.
let proposal_info = nns::governance::propose_and_wait(
&pocket_ic,
MakeProposalRequest {
title: Some("Create two canisters (using Batch)".to_string()),
summary: "".to_string(),
url: "".to_string(),
action: Some(ProposalActionRequest::Batch(batch)),
},
)
.await
.unwrap();

// Step 3: Verify results.

// Step 3.1: Proposal executed successfully.
assert!(
proposal_info.executed_timestamp_seconds > 0,
"{:#?}",
proposal_info,
);
assert_eq!(proposal_info.failure_reason, None, "{:#?}", proposal_info,);

// Step 3.2: New canister IDs (in success_value).
let batch_ok = match proposal_info.success_value {
Some(SuccessfulProposalExecutionValue::Batch(ok)) => ok,
wrong => panic!("Expected Batch success_value, got: {wrong:#?}"),
};
assert_eq!(batch_ok.sub_results.len(), 2, "{batch_ok:#?}");
let new_canister_ids = batch_ok
.sub_results
.into_iter()
.map(|sub_result| {
// Unwrap sub_result.
let sub_result = match sub_result.unwrap() {
SuccessfulProposalExecutionValue::CreateCanisterAndInstallCode(ok) => ok,
wrong => {
panic!("Sub-result 0: expected CreateCanisterAndInstallCode, got {wrong:#?}")
}
};

sub_result.canister_id.unwrap()
})
.collect::<Vec<_>>();
let canister_id_a = new_canister_ids[0];
let canister_id_b = new_canister_ids[1];
assert_ne!(
canister_id_a, canister_id_b,
"Both sub-actions must produce distinct canister IDs"
);

// Step 3.3: Canisters created in specified subnet.
for (i, canister_id) in new_canister_ids.iter().enumerate() {
let canister_subnet = pocket_ic
.get_subnet(Principal::from(*canister_id))
.await
.unwrap();

assert_eq!(PrincipalId::from(canister_subnet), host_subnet_id, "{i}");
}

// Step 3.4: Each canister has the expected WASM installed.
for (i, (canister_id, expected_module_hash)) in
new_canister_ids.iter().zip(hashes.iter()).enumerate()
{
// Observe module hash.
let controller = Principal::from(PrincipalId::from(ROOT_CANISTER_ID));
let observed_module_hash = pocket_ic
.canister_status(Principal::from(*canister_id), Some(controller))
.await
.unwrap()
.module_hash
.unwrap();

assert_eq!(observed_module_hash, *expected_module_hash, "{i}");
}
}
45 changes: 45 additions & 0 deletions rs/nns/governance/api/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,8 @@ pub mod proposal {
LoadCanisterSnapshot(super::LoadCanisterSnapshot),
/// Create a canister in a (possibly non-NNS) subnet and install code into it.
CreateCanisterAndInstallCode(super::CreateCanisterAndInstallCode),
/// Multiple actions. Executed sequentially. Stops on first failure.
Batch(super::Batch),
}
}
/// Empty message to use in oneof fields that represent empty
Expand Down Expand Up @@ -1451,6 +1453,7 @@ pub enum ProposalActionRequest {
TakeCanisterSnapshot(TakeCanisterSnapshot),
LoadCanisterSnapshot(LoadCanisterSnapshot),
CreateCanisterAndInstallCode(CreateCanisterAndInstallCodeRequest),
Batch(BatchRequest),
}

#[derive(
Expand Down Expand Up @@ -2066,6 +2069,7 @@ pub struct ProposalInfo {
pub enum SuccessfulProposalExecutionValue {
CreateCanisterAndInstallCode(CreateCanisterAndInstallCodeOk),
TakeCanisterSnapshot(TakeCanisterSnapshotOk),
Batch(BatchOk),
}

/// The result of a successful CreateCanisterAndInstallCode proposal execution.
Expand All @@ -2080,6 +2084,14 @@ pub struct TakeCanisterSnapshotOk {
pub snapshot_id: Vec<u8>,
}

/// The result of successfully executing a Batch proposal.
/// Each element corresponds to the same-indexed sub-action in the batch.
/// Sub-actions that produce no value have `None`.
#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Clone, PartialEq, Debug)]
pub struct BatchOk {
pub sub_results: Vec<Option<SuccessfulProposalExecutionValue>>,
}

impl From<CreateCanisterAndInstallCodeOk> for SuccessfulProposalExecutionValue {
fn from(ok: CreateCanisterAndInstallCodeOk) -> Self {
Self::CreateCanisterAndInstallCode(ok)
Expand All @@ -2092,6 +2104,12 @@ impl From<TakeCanisterSnapshotOk> for SuccessfulProposalExecutionValue {
}
}

impl From<BatchOk> for SuccessfulProposalExecutionValue {
fn from(ok: BatchOk) -> Self {
Self::Batch(ok)
}
}

/// Network economics contains the parameters for several operations related
/// to the economy of the network. When submitting a NetworkEconomics proposal
/// default values (0) are considered unchanged, so a valid proposal only needs
Expand Down Expand Up @@ -2919,6 +2937,33 @@ pub struct CreateCanisterAndInstallCodeRequest {
pub install_arg: Option<Vec<u8>>,
}

/// Analogous to BatchRequest, but used in responses.
#[derive(
candid::CandidType, candid::Deserialize, serde::Serialize, Clone, PartialEq, Debug, Default,
)]
pub struct Batch {
pub actions: Option<Vec<proposal::Action>>,
}

/// Take multiple actions, sequentially, and stop if any step fails.
///
/// Topic: All actions must have the same topic. This has the same topic
/// as the constituents.
#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Clone, PartialEq, Debug)]
pub struct BatchRequest {
pub actions: Option<Vec<ProposalActionRequest>>,
// A couple of ideas for possible future enhancements (no promises though):
//
// 1. Let user choose a different behavior on error. Currently,
// we support "just stop", but you can imagine people might
// want "keep going".
//
// 2. Let user choose parallel execution. Currently, we only
// support sequential.
//
// Each of these would probably be involve adding an variant/enum field.
}

/// This represents the whole NNS governance system. It contains all
/// information about the NNS governance system that must be kept
/// across upgrades of the NNS governance system.
Expand Down
15 changes: 15 additions & 0 deletions rs/nns/governance/canister/governance.did
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Action = variant {
TakeCanisterSnapshot : TakeCanisterSnapshot;
LoadCanisterSnapshot : LoadCanisterSnapshot;
CreateCanisterAndInstallCode : CreateCanisterAndInstallCode;
Batch : Batch;
};

type AddHotKey = record {
Expand Down Expand Up @@ -255,9 +256,14 @@ type TakeCanisterSnapshotOk = record {
snapshot_id : blob;
};

type BatchOk = record {
sub_results : vec opt SuccessfulProposalExecutionValue;
};

type SuccessfulProposalExecutionValue = variant {
CreateCanisterAndInstallCode : CreateCanisterAndInstallCodeOk;
TakeCanisterSnapshot : TakeCanisterSnapshotOk;
Batch : BatchOk;
};

type CreateServiceNervousSystem = record {
Expand Down Expand Up @@ -1083,6 +1089,15 @@ type ProposalActionRequest = variant {
TakeCanisterSnapshot : TakeCanisterSnapshot;
LoadCanisterSnapshot : LoadCanisterSnapshot;
CreateCanisterAndInstallCode : CreateCanisterAndInstallCodeRequest;
Batch : BatchRequest;
};

type Batch = record {
actions : opt vec Action;
};

type BatchRequest = record {
actions : opt vec ProposalActionRequest;
};

// Creates a rented subnet from a rental request (in the Subnet Rental
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -723,9 +723,28 @@ message Proposal {
LoadCanisterSnapshot load_canister_snapshot = 33;
// Create a canister in a (possibly non-NNS) subnet and install code into it.
CreateCanisterAndInstallCode create_canister_and_install_code = 34;
// Execute multiple proposal actions sequentially. Stops at first failure.
Batch batch = 35;
}
}

// Multiple actions to execute sequentially. Stop on first failure.
//
// No recursion, i.e. Batch cannot contain batch.
//
// # Topic
//
// All constituent actions must have the same topic.
//
// This has the topic of its constituents.
message Batch {
// The steps to take.
// Proposal includes some fields that are superfluous in this context
// (title, summary, etc). The only part we use here is the `action` oneof.
// The others will be left blank.
repeated Proposal actions = 1;
}

// Take a canister snapshot.
message TakeCanisterSnapshot {
// The canister being snapshotted.
Expand Down Expand Up @@ -1262,6 +1281,7 @@ message SuccessfulProposalExecutionValue {
oneof proposal_type {
CreateCanisterAndInstallCodeOk create_canister_and_install_code = 1;
TakeCanisterSnapshotOk take_canister_snapshot = 2;
BatchOk batch = 3;
}
}

Expand All @@ -1273,6 +1293,14 @@ message TakeCanisterSnapshotOk {
bytes snapshot_id = 1;
}

// The result of successfully executing a Batch proposal.
// Each element corresponds to the same-indexed sub-action in the batch.
// Sub-actions that produce no value have an empty SuccessfulProposalExecutionValue
// (i.e. proposal_type is not set).
message BatchOk {
repeated SuccessfulProposalExecutionValue sub_results = 1;
}

// This structure contains data for settling the Neurons' Fund participation in an SNS token swap.
message NeuronsFundData {
// Initial Neurons' Fund reserves computed at the time of execution of the proposal through which
Expand Down
Loading
Loading