use super::{
NnsGovernanceMaturityModulation, NnsGovernanceProposalId, NnsGovernanceRewardEvent,
NnsGovernanceSourceProvenance,
tests::sample_economics,
wire::{
GetMaturityModulationResponse, GetMetricsResult, GovernanceCachedMetrics, GovernanceError,
},
};
use crate::nns::MAINNET_GOVERNANCE_CANISTER_ID;
use ic_host_artifacts::{
wasm::{ExportKind, InspectionLimits, inspect},
wasm_text::{CompileLimits, compile},
};
use ic_platform::candid::{DecodeLimits, contract::Principal};
use ic_testkit::pic::{
CanisterInstallExt, InstallSpec, PocketIc, PocketIcBuilder, PocketIcStartupConfig, RejectCode,
update_candid_args,
};
use ic_testkit::pocket_ic::Time;
use serde_json::Value;
use std::{collections::BTreeSet, env, fmt::Write, fs, time::Duration};
const REPORT_REPLY_LIMITS: DecodeLimits = DecodeLimits {
reply_bytes: 2 * 1024 * 1024,
decoding_quota: 64_000_000,
skipping_quota: 100_000,
type_entries: 4_096,
header_bytes: 64 * 1024,
};
#[test]
#[ignore = "requires explicit Testkit server preparation and compiled probe; run make canister-test"]
fn compiled_governance_reports_preserve_values_provenance_and_failures() {
let path = env::var_os("IC_QUERY_GOVERNANCE_PROBE_WASM")
.expect("select the compiled probe with IC_QUERY_GOVERNANCE_PROBE_WASM");
let probe_wasm = fs::read(path).expect("read compiled Governance probe");
let owned = PocketIcStartupConfig::from_env(Duration::from_secs(60))
.expect("explicit Testkit server")
.build_instance(
PocketIcBuilder::new()
.with_nns_subnet()
.with_application_subnet(),
)
.expect("start Testkit instance");
let pic = &owned.instance;
pic.set_time(Time::from_nanos_since_unix_epoch(1_780_531_200_000_000_000));
let governance = Principal::from_text(MAINNET_GOVERNANCE_CANISTER_ID).unwrap();
pic.create_canister_with_id(None, None, governance)
.expect("fixed NNS Governance principal");
pic.add_cycles(governance, 2_000_000_000_000);
let mut replies = governance_replies();
pic.install_canister(
governance,
governance_wasm(&replies),
ic_platform::candid::contract::encode_args(()).unwrap(),
None,
);
let probe = pic
.try_create_and_install(InstallSpec {
wasm: probe_wasm,
init_bytes: ic_platform::candid::contract::encode_args(()).unwrap(),
cycles: 2_000_000_000_000,
install_sender: None,
label: Some("Query Governance probe".into()),
})
.expect("install compiled Query probe");
for args in [
b"not candid".to_vec(),
ic_platform::candid::contract::encode_args((42_u64,)).unwrap(),
ic_platform::candid::contract::encode_args(("economics", vec![(); 100_000])).unwrap(),
] {
let rejection = pic
.update_call(probe, Principal::anonymous(), "report", args)
.expect_err("invalid or over-budget entry arguments must trap");
assert_eq!(rejection.reject_code, RejectCode::CanisterError);
}
let (reply,): (String,) = update_candid_args(
pic,
probe,
Principal::anonymous(),
"report",
("economics", 7_u64),
REPORT_REPLY_LIMITS,
)
.expect("entrypoint preserves bounded skipping of unused arguments");
assert_eq!(
serde_json::from_str::<Value>(&reply).unwrap()["status"],
"ok"
);
assert_reports(pic, probe, governance);
replies[1].1 =
ic_platform::candid::contract::encode_one(GetMetricsResult::Err(GovernanceError {
error_type: 7,
error_message: "fixture governance failure".into(),
}))
.unwrap();
pic.reinstall_canister(
governance,
governance_wasm(&replies),
ic_platform::candid::contract::encode_args(()).unwrap(),
None,
)
.unwrap();
let reply = report(pic, probe, "metrics");
assert_eq!(reply["status"], "error");
let error = reply["error"].as_str().unwrap();
assert!(error.contains("code 7"));
assert!(error.contains("fixture governance failure"));
replies[0].1 = b"not candid".to_vec();
pic.reinstall_canister(
governance,
governance_wasm(&replies),
ic_platform::candid::contract::encode_args(()).unwrap(),
None,
)
.unwrap();
let reply = report(pic, probe, "economics");
assert_eq!(reply["status"], "error");
assert!(reply["error"].as_str().unwrap().contains("decode Candid"));
pic.uninstall_canister(governance, None).unwrap();
let reply = report(pic, probe, "economics");
assert_eq!(reply["status"], "error");
assert!(
reply["error"]
.as_str()
.unwrap()
.contains("was rejected with code")
);
}
fn assert_reports(pic: &PocketIc, probe: Principal, governance: Principal) {
let expected_source =
serde_json::to_value(NnsGovernanceSourceProvenance::ReplicatedInterCanisterCall {
collector_canister_id: probe.to_text(),
})
.unwrap();
for kind in [
"economics",
"metrics",
"reward_event",
"maturity_modulation",
] {
let reply = report(pic, probe, kind);
assert_eq!(reply["status"], "ok", "{reply}");
let report = &reply["report"];
assert_eq!(report["schema_version"], 1);
assert_eq!(report["network"], "ic");
assert_eq!(report["governance_canister_id"], governance.to_text());
assert_eq!(report["source"], expected_source);
match kind {
"economics" => assert_eq!(report["economics"]["neuron_minimum_stake_e8s"], 100_000_000),
"metrics" => {
assert_eq!(report["metrics"]["total_supply_icp"], 42);
assert_eq!(
report["metrics"]["not_dissolving_neurons_e8s_buckets"][0]["key"],
7
);
assert_eq!(
report["metrics"]["not_dissolving_neurons_e8s_buckets"][0]["value"],
12.5
);
}
"reward_event" => assert_eq!(report["reward_event"]["settled_proposals"][0]["id"], 7),
"maturity_modulation" => assert_eq!(
report["maturity_modulation"]["current_value_permyriad"],
-125
),
_ => unreachable!(),
}
}
}
fn report(pic: &PocketIc, probe: Principal, kind: &str) -> Value {
let (reply,): (String,) = update_candid_args(
pic,
probe,
Principal::anonymous(),
"report",
(kind.to_string(),),
REPORT_REPLY_LIMITS,
)
.expect("execute compiled report entry point");
serde_json::from_str(&reply).expect("probe JSON report")
}
fn governance_replies() -> Vec<(&'static str, Vec<u8>)> {
let metrics = GetMetricsResult::Ok(Box::new(GovernanceCachedMetrics {
total_supply_icp: 42,
not_dissolving_neurons_e8s_buckets: vec![(7, 12.5)],
timestamp_seconds: 1_780_531_200,
..Default::default()
}));
let reward = NnsGovernanceRewardEvent {
rounds_since_last_distribution: Some(1),
day_after_genesis: 2,
actual_timestamp_seconds: 1_780_531_200,
total_available_e8s_equivalent: 4,
latest_round_available_e8s_equivalent: Some(5),
distributed_e8s_equivalent: 6,
settled_proposals: vec![NnsGovernanceProposalId { id: 7 }],
};
let maturity = GetMaturityModulationResponse {
maturity_modulation: Some(NnsGovernanceMaturityModulation {
current_value_permyriad: Some(-125),
updated_at_timestamp_seconds: Some(1_780_531_200),
}),
};
vec![
(
"get_network_economics_parameters",
ic_platform::candid::contract::encode_one(sample_economics()).unwrap(),
),
(
"get_metrics",
ic_platform::candid::contract::encode_one(metrics).unwrap(),
),
(
"get_latest_reward_event",
ic_platform::candid::contract::encode_one(reward).unwrap(),
),
(
"get_maturity_modulation",
ic_platform::candid::contract::encode_one(maturity).unwrap(),
),
]
}
fn governance_wasm(replies: &[(&str, Vec<u8>)]) -> Vec<u8> {
let bytes = replies.iter().map(|(_, bytes)| bytes.len()).sum::<usize>();
let mut module = format!(
r#"(module
(import "ic0" "msg_reply_data_append" (func $append (param i32 i32)))
(import "ic0" "msg_reply" (func $reply))
(memory (export "memory") {})"#,
bytes.div_ceil(65_536).max(1)
);
let mut offset = 0;
for (method, reply) in replies {
write!(module, "(data (i32.const {offset}) \"").unwrap();
for byte in reply {
write!(module, "\\{byte:02x}").unwrap();
}
write!(module, "\") (func (export \"canister_update {method}\") i32.const {offset} i32.const {} call $append call $reply)", reply.len()).unwrap();
offset += reply.len();
}
module.push(')');
compile(
&module,
CompileLimits {
source_bytes: 1024 * 1024,
module_bytes: 1024 * 1024,
},
)
.expect("fixture Governance module")
}
#[test]
fn governance_fixture_exports_the_configured_update_methods() {
let replies = governance_replies();
let module = governance_wasm(&replies);
let facts = inspect(
&module,
InspectionLimits {
module_bytes: 1024 * 1024,
sections: 100,
exports: 100,
custom_sections: 10,
},
)
.unwrap();
assert_eq!(facts.exports["memory"].kind, ExportKind::Memory);
let actual: BTreeSet<_> = facts
.exports
.iter()
.filter(|(_, export)| export.kind == ExportKind::Function)
.map(|(name, _)| (*name).to_string())
.collect();
let expected = replies
.iter()
.map(|(name, _)| format!("canister_update {name}"))
.collect();
assert_eq!(actual, expected);
}