Skip to content

Commit f07e989

Browse files
feat(nns): Define and use ValidExecuteNnsFunction (#7724)
Every time an ExecuteNnsFunction is used, it most likely needs to be validated (e.g. nns function id is valid and is not obsolete). A `ValidExecuteNnsFunction` type consolidates the validation logic and make it impossible to represent an invalid one with this type. This will also help with transforming into a self-describing proposal action.
1 parent 193b9fd commit f07e989

File tree

12 files changed

+738
-539
lines changed

12 files changed

+738
-539
lines changed

rs/nns/governance/src/canister_state.rs

Lines changed: 102 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
use crate::decoder_config;
2-
use crate::governance::{
3-
Environment, Governance, HeapGrowthPotential, RandomnessGenerator, RngError,
1+
use crate::{
2+
decoder_config,
3+
governance::{Environment, Governance, HeapGrowthPotential, RandomnessGenerator, RngError},
4+
pb::v1::{GovernanceError, governance_error::ErrorType},
5+
proposals::execute_nns_function::{ValidExecuteNnsFunction, ValidNnsFunction},
46
};
7+
58
use async_trait::async_trait;
69
use candid::{Decode, Encode};
710
use ic_base_types::CanisterId;
@@ -187,15 +190,8 @@ impl Environment for CanisterEnv {
187190
fn execute_nns_function(
188191
&self,
189192
proposal_id: u64,
190-
update: &crate::pb::v1::ExecuteNnsFunction,
193+
execute_nns_function: &ValidExecuteNnsFunction,
191194
) -> Result<(), crate::pb::v1::GovernanceError> {
192-
// use internal types, as this API is used in core
193-
use crate::pb::v1::{GovernanceError, NnsFunction, governance_error::ErrorType};
194-
195-
let mt = NnsFunction::try_from(update.nns_function).map_err(|_|
196-
// No update type specified.
197-
GovernanceError::new(ErrorType::PreconditionFailed))?;
198-
199195
let reply = move || {
200196
governance_mut().set_proposal_execution_status(proposal_id, Ok(()));
201197
};
@@ -227,22 +223,19 @@ impl Environment for CanisterEnv {
227223
)),
228224
);
229225
};
230-
let (canister_id, method) = mt.canister_and_function()?;
231-
let method = method.to_owned();
226+
let (canister_id, method) = execute_nns_function.nns_function.canister_and_function();
232227
let proposal_timestamp_seconds = governance()
233228
.get_proposal_data(ProposalId(proposal_id))
234229
.map(|data| data.proposal_timestamp_seconds)
235230
.ok_or(GovernanceError::new(ErrorType::PreconditionFailed))?;
236231
let effective_payload = get_effective_payload(
237-
mt,
238-
update.payload.clone(),
232+
execute_nns_function,
239233
proposal_id,
240234
proposal_timestamp_seconds,
241235
)?;
242236

243237
ic_cdk::futures::spawn_017_compat(async move {
244-
match CdkRuntime::call_bytes_with_cleanup(canister_id, &method, &effective_payload)
245-
.await
238+
match CdkRuntime::call_bytes_with_cleanup(canister_id, method, &effective_payload).await
246239
{
247240
Ok(_) => reply(),
248241
Err(e) => reject(e),
@@ -281,130 +274,127 @@ impl Environment for CanisterEnv {
281274
}
282275
// Processes the payload received and transforms it into a form the intended canister expects.
283276
// The arguments `proposal_id` is used by AddSnsWasm proposals.
284-
// `_proposal_timestamp_seconds` will be used in the future by subnet rental NNS proposals.
277+
// `proposal_timestamp_seconds` is used by subnet rental NNS proposals.
278+
// TODO(NNS1-4272): move this into `impl ValidExecuteNnsFunction`.
285279
fn get_effective_payload(
286-
mt: crate::pb::v1::NnsFunction,
287-
payload: Vec<u8>,
280+
execute_nns_function: &ValidExecuteNnsFunction,
288281
proposal_id: u64,
289282
proposal_timestamp_seconds: u64,
290283
) -> Result<Vec<u8>, crate::pb::v1::GovernanceError> {
291-
use crate::pb::v1::{GovernanceError, NnsFunction, governance_error::ErrorType};
284+
use crate::pb::v1::{GovernanceError, governance_error::ErrorType};
292285

293286
const BITCOIN_SET_CONFIG_METHOD_NAME: &str = "set_config";
294287
const BITCOIN_MAINNET_CANISTER_ID: &str = "ghsi2-tqaaa-aaaan-aaaca-cai";
295288
const BITCOIN_TESTNET_CANISTER_ID: &str = "g4xu7-jiaaa-aaaan-aaaaq-cai";
296289

297-
match mt {
298-
NnsFunction::BitcoinSetConfig => {
290+
let ValidExecuteNnsFunction {
291+
nns_function,
292+
payload,
293+
} = execute_nns_function;
294+
295+
match nns_function {
296+
ValidNnsFunction::BitcoinSetConfig => {
299297
// Decode the payload to get the network.
300-
let payload = match Decode!([decoder_config()]; &payload, BitcoinSetConfigProposal) {
301-
Ok(payload) => payload,
302-
Err(_) => {
303-
return Err(GovernanceError::new_with_message(ErrorType::InvalidProposal, "Payload must be a valid BitcoinSetConfigProposal."));
304-
}
305-
};
298+
let decoded_payload = Decode!([decoder_config()]; payload, BitcoinSetConfigProposal)
299+
.map_err(|_| {
300+
GovernanceError::new_with_message(
301+
ErrorType::InvalidProposal,
302+
"Unable to decode BitcoinSetConfigProposal proposal: {e}",
303+
)
304+
})?;
306305

307306
// Convert it to a call canister payload.
308-
let canister_id = CanisterId::from_str(match payload.network {
307+
let canister_id = CanisterId::from_str(match decoded_payload.network {
309308
BitcoinNetwork::Mainnet => BITCOIN_MAINNET_CANISTER_ID,
310309
BitcoinNetwork::Testnet => BITCOIN_TESTNET_CANISTER_ID,
311-
}).expect("bitcoin canister id must be valid.");
310+
})
311+
.expect("bitcoin canister id must be valid.");
312312

313313
let encoded_payload = Encode!(&CallCanisterRequest {
314314
canister_id,
315315
method_name: BITCOIN_SET_CONFIG_METHOD_NAME.to_string(),
316-
payload: payload.payload
316+
payload: decoded_payload.payload
317317
})
318-
.unwrap();
318+
.unwrap();
319319

320320
Ok(encoded_payload)
321321
}
322-
NnsFunction::SubnetRentalRequest => {
322+
ValidNnsFunction::SubnetRentalRequest => {
323323
// Decode the payload to `SubnetRentalRequest`.
324-
let payload = match Decode!([decoder_config()]; &payload, SubnetRentalRequest) {
325-
Ok(payload) => payload,
326-
Err(_) => {
327-
return Err(GovernanceError::new_with_message(ErrorType::InvalidProposal, "Payload must be a valid SubnetRentalRequest."));
328-
}
329-
};
324+
let decoded_payload = Decode!([decoder_config()]; payload, SubnetRentalRequest)
325+
.map_err(|_| {
326+
GovernanceError::new_with_message(
327+
ErrorType::InvalidProposal,
328+
"Unable to decode SubnetRentalRequest proposal: {e}",
329+
)
330+
})?;
330331

331332
// Convert the payload to `SubnetRentalProposalPayload`.
332333
let SubnetRentalRequest {
333334
user,
334335
rental_condition_id,
335-
} = payload;
336+
} = decoded_payload;
336337
let proposal_creation_time_seconds = proposal_timestamp_seconds;
337338
let encoded_payload = Encode!(&SubnetRentalProposalPayload {
338339
user,
339340
rental_condition_id,
340341
proposal_id,
341342
proposal_creation_time_seconds,
342-
}).unwrap();
343+
})
344+
.unwrap();
343345

344346
Ok(encoded_payload)
345347
}
346348

347-
| NnsFunction::AddSnsWasm => {
348-
let payload = add_proposal_id_to_add_wasm_request(&payload, proposal_id)?;
349+
ValidNnsFunction::AddSnsWasm => {
350+
let transformed_payload = add_proposal_id_to_add_wasm_request(payload, proposal_id)?;
349351

350-
Ok(payload)
352+
Ok(transformed_payload)
351353
}
352354

353355
// NOTE: Methods are listed explicitly as opposed to using the `_` wildcard so
354356
// that adding a new function causes a compile error here, ensuring that the developer
355357
// makes an explicit decision on how the payload is handled.
356-
NnsFunction::Unspecified
357-
| NnsFunction::UpdateElectedHostosVersions
358-
| NnsFunction::UpdateNodesHostosVersion
359-
| NnsFunction::ReviseElectedHostosVersions
360-
| NnsFunction::DeployHostosToSomeNodes
361-
| NnsFunction::AssignNoid
362-
| NnsFunction::CreateSubnet
363-
| NnsFunction::AddNodeToSubnet
364-
| NnsFunction::RemoveNodesFromSubnet
365-
| NnsFunction::ChangeSubnetMembership
366-
| NnsFunction::NnsCanisterInstall
367-
| NnsFunction::NnsCanisterUpgrade
368-
| NnsFunction::NnsRootUpgrade
369-
| NnsFunction::HardResetNnsRootToVersion
370-
| NnsFunction::RecoverSubnet
371-
| NnsFunction::BlessReplicaVersion
372-
| NnsFunction::RetireReplicaVersion
373-
| NnsFunction::ReviseElectedGuestosVersions
374-
| NnsFunction::UpdateNodeOperatorConfig
375-
| NnsFunction::DeployGuestosToAllSubnetNodes
376-
| NnsFunction::UpdateConfigOfSubnet
377-
| NnsFunction::IcpXdrConversionRate
378-
| NnsFunction::ClearProvisionalWhitelist
379-
| NnsFunction::SetAuthorizedSubnetworks
380-
| NnsFunction::SetFirewallConfig
381-
| NnsFunction::AddFirewallRules
382-
| NnsFunction::RemoveFirewallRules
383-
| NnsFunction::UpdateFirewallRules
384-
| NnsFunction::StopOrStartNnsCanister
385-
| NnsFunction::RemoveNodes
386-
| NnsFunction::UninstallCode
387-
| NnsFunction::UpdateNodeRewardsTable
388-
| NnsFunction::AddOrRemoveDataCenters
389-
| NnsFunction::UpdateUnassignedNodesConfig // obsolete
390-
| NnsFunction::RemoveNodeOperators
391-
| NnsFunction::RerouteCanisterRanges
392-
| NnsFunction::PrepareCanisterMigration
393-
| NnsFunction::CompleteCanisterMigration
394-
| NnsFunction::UpdateSubnetType
395-
| NnsFunction::ChangeSubnetTypeAssignment
396-
| NnsFunction::UpdateAllowedPrincipals
397-
| NnsFunction::UpdateSnsWasmSnsSubnetIds
398-
| NnsFunction::InsertSnsWasmUpgradePathEntries
399-
| NnsFunction::AddApiBoundaryNodes
400-
| NnsFunction::RemoveApiBoundaryNodes
401-
| NnsFunction::UpdateApiBoundaryNodesVersion // obsolete
402-
| NnsFunction::DeployGuestosToAllUnassignedNodes
403-
| NnsFunction::UpdateSshReadonlyAccessForAllUnassignedNodes
404-
| NnsFunction::DeployGuestosToSomeApiBoundaryNodes
405-
| NnsFunction::PauseCanisterMigrations
406-
| NnsFunction::UnpauseCanisterMigrations
407-
| NnsFunction::SetSubnetOperationalLevel => Ok(payload),
358+
ValidNnsFunction::CreateSubnet
359+
| ValidNnsFunction::AddNodeToSubnet
360+
| ValidNnsFunction::NnsCanisterInstall
361+
| ValidNnsFunction::RecoverSubnet
362+
| ValidNnsFunction::UpdateConfigOfSubnet
363+
| ValidNnsFunction::AssignNoid
364+
| ValidNnsFunction::DeployGuestosToAllSubnetNodes
365+
| ValidNnsFunction::ClearProvisionalWhitelist
366+
| ValidNnsFunction::RemoveNodesFromSubnet
367+
| ValidNnsFunction::SetAuthorizedSubnetworks
368+
| ValidNnsFunction::SetFirewallConfig
369+
| ValidNnsFunction::UpdateNodeOperatorConfig
370+
| ValidNnsFunction::RemoveNodes
371+
| ValidNnsFunction::UninstallCode
372+
| ValidNnsFunction::UpdateNodeRewardsTable
373+
| ValidNnsFunction::AddOrRemoveDataCenters
374+
| ValidNnsFunction::RemoveNodeOperators
375+
| ValidNnsFunction::RerouteCanisterRanges
376+
| ValidNnsFunction::AddFirewallRules
377+
| ValidNnsFunction::RemoveFirewallRules
378+
| ValidNnsFunction::UpdateFirewallRules
379+
| ValidNnsFunction::PrepareCanisterMigration
380+
| ValidNnsFunction::CompleteCanisterMigration
381+
| ValidNnsFunction::ChangeSubnetMembership
382+
| ValidNnsFunction::UpdateSubnetType
383+
| ValidNnsFunction::ChangeSubnetTypeAssignment
384+
| ValidNnsFunction::UpdateSnsWasmSnsSubnetIds
385+
| ValidNnsFunction::InsertSnsWasmUpgradePathEntries
386+
| ValidNnsFunction::ReviseElectedGuestosVersions
387+
| ValidNnsFunction::HardResetNnsRootToVersion
388+
| ValidNnsFunction::AddApiBoundaryNodes
389+
| ValidNnsFunction::RemoveApiBoundaryNodes
390+
| ValidNnsFunction::DeployGuestosToSomeApiBoundaryNodes
391+
| ValidNnsFunction::DeployGuestosToAllUnassignedNodes
392+
| ValidNnsFunction::UpdateSshReadonlyAccessForAllUnassignedNodes
393+
| ValidNnsFunction::ReviseElectedHostosVersions
394+
| ValidNnsFunction::DeployHostosToSomeNodes
395+
| ValidNnsFunction::PauseCanisterMigrations
396+
| ValidNnsFunction::UnpauseCanisterMigrations
397+
| ValidNnsFunction::SetSubnetOperationalLevel => Ok(payload.clone()),
408398
}
409399
}
410400

@@ -446,7 +436,7 @@ fn add_proposal_id_to_add_wasm_request(
446436
#[cfg(test)]
447437
mod tests {
448438
use super::*;
449-
use candid::{Decode, Encode};
439+
450440
#[test]
451441
fn test_set_time_warp() {
452442
let environment = CanisterEnv::new();
@@ -461,7 +451,6 @@ mod tests {
461451

462452
#[test]
463453
fn test_get_effective_payload_sets_proposal_id_for_add_wasm() {
464-
let mt = crate::pb::v1::NnsFunction::AddSnsWasm;
465454
let proposal_id = 42;
466455
let wasm = vec![1, 2, 3];
467456
let canister_type = 3;
@@ -477,7 +466,13 @@ mod tests {
477466
})
478467
.unwrap();
479468

480-
let effective_payload = get_effective_payload(mt, payload, proposal_id, 0).unwrap();
469+
let execute_nns_function = ValidExecuteNnsFunction {
470+
nns_function: ValidNnsFunction::AddSnsWasm,
471+
payload,
472+
};
473+
474+
let effective_payload =
475+
get_effective_payload(&execute_nns_function, proposal_id, 0).unwrap();
481476

482477
let decoded = Decode!(&effective_payload, AddWasmRequest).unwrap();
483478
assert_eq!(
@@ -496,7 +491,6 @@ mod tests {
496491

497492
#[test]
498493
fn test_get_effective_payload_overrides_proposal_id_for_add_wasm() {
499-
let mt = crate::pb::v1::NnsFunction::AddSnsWasm;
500494
let proposal_id = 42;
501495
let payload = Encode!(&AddWasmRequest {
502496
wasm: Some(SnsWasm {
@@ -507,7 +501,13 @@ mod tests {
507501
})
508502
.unwrap();
509503

510-
let effective_payload = get_effective_payload(mt, payload, proposal_id, 0).unwrap();
504+
let execute_nns_function = ValidExecuteNnsFunction {
505+
nns_function: ValidNnsFunction::AddSnsWasm,
506+
payload,
507+
};
508+
509+
let effective_payload =
510+
get_effective_payload(&execute_nns_function, proposal_id, 0).unwrap();
511511

512512
let decoded = Decode!(&effective_payload, AddWasmRequest).unwrap();
513513
assert_eq!(decoded.wasm.unwrap().proposal_id.unwrap(), proposal_id);

0 commit comments

Comments
 (0)