Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions rs/embedders/src/wasm_utils/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,16 @@ fn get_valid_system_apis_common(
},
)],
),
(
"is_subnet_admin",
vec![(
API_VERSION_IC0,
FunctionSignature {
param_types: vec![I, I],
return_type: vec![DataType::I32],
},
)],
),
(
"in_replicated_execution",
vec![(
Expand Down
13 changes: 13 additions & 0 deletions rs/embedders/src/wasmtime_embedder/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,19 @@ pub fn syscalls<
})
.unwrap();

linker
.func_wrap("ic0", "is_subnet_admin", {
move |mut caller: Caller<'_, StoreData>, src: I, size: I| {
let src: usize = src.try_into().expect("Failed to convert I to usize");
let size: usize = size.try_into().expect("Failed to convert I to usize");
charge_for_cpu_and_mem(&mut caller, overhead::IS_SUBNET_ADMIN, size)?;
with_memory_and_system_api(&mut caller, |system_api, memory| {
system_api.ic0_is_subnet_admin(src, size, memory)
})
}
})
.unwrap();

linker
.func_wrap("ic0", "in_replicated_execution", {
move |mut caller: Caller<'_, StoreData>| {
Expand Down
26 changes: 26 additions & 0 deletions rs/embedders/src/wasmtime_embedder/system_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4164,6 +4164,32 @@ impl SystemApi for SystemApiImpl {
result
}

fn ic0_is_subnet_admin(
&self,
src: usize,
size: usize,
heap: &mut Heap<'_>,
) -> HypervisorResult<u32> {
let msg_bytes = heap.get("ic0.is_subnet_admin", src, size)?;
let result = PrincipalId::try_from(msg_bytes)
.map(|principal_id| {
self.sandbox_safe_system_state
.is_subnet_admin(&principal_id)
.into()
})
.map_err(|e| HypervisorError::InvalidPrincipalId(PrincipalIdBlobParseError(e.0)));

trace_syscall!(
self,
IsSubnetAdmin,
src,
size,
summarize(heap, src, size),
result
);
result
}

/// Sets `timeout_seconds` to the provided value if not yet set, making this a best-effort call.
/// The timeout is bounded from above by `MAX_CALL_TIMEOUT_SECONDS`.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ use ic_nns_constants::CYCLES_MINTING_CANISTER_ID;
use ic_registry_subnet_type::SubnetType;
use ic_replicated_state::canister_state::execution_state::WasmExecutionMode;
use ic_replicated_state::{
CallOrigin, NetworkTopology, OutputRequest, SystemState, canister_state::DEFAULT_QUEUE_CAPACITY,
CallOrigin, NetworkTopology, OutputRequest, SystemState,
canister_state::DEFAULT_QUEUE_CAPACITY, metadata_state::can_have_subnet_admins,
};
use ic_types::canister_log::CanisterLogMetrics;
use ic_types::{
Expand Down Expand Up @@ -1234,6 +1235,19 @@ impl SandboxSafeSystemState {
self.controllers.contains(principal_id)
}

/// Whether `principal_id` is a subnet admin of the subnet this canister
/// runs on. Admins count only on subnets that can have them, as in
/// `ReplicatedState::get_own_subnet_admins`.
pub fn is_subnet_admin(&self, principal_id: &PrincipalId) -> bool {
self.network_topology
.subnets()
.get(&self.get_subnet_id())
.is_some_and(|subnet| {
can_have_subnet_admins(subnet.subnet_type, subnet.cost_schedule)
&& subnet.subnet_admins.contains(principal_id)
})
}

/// Checks the cycles balance against the freezing threshold with the new
/// memory usage if that's needed for the given API type.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub mod overhead {
pub const DEBUG_PRINT: NumInstructions = NumInstructions::new(100);
pub const GLOBAL_TIMER_SET: NumInstructions = NumInstructions::new(500);
pub const IS_CONTROLLER: NumInstructions = NumInstructions::new(1_000);
pub const IS_SUBNET_ADMIN: NumInstructions = NumInstructions::new(1_000);
pub const IN_REPLICATED_EXECUTION: NumInstructions = NumInstructions::new(500);
pub const MSG_ARG_DATA_COPY: NumInstructions = NumInstructions::new(500);
pub const MSG_ARG_DATA_SIZE: NumInstructions = NumInstructions::new(500);
Expand Down
27 changes: 27 additions & 0 deletions rs/embedders/tests/system_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ fn is_supported(api_type: SystemApiCallId, context: &str) -> bool {
SystemApiCallId::GlobalTimerSet => vec!["I", "G", "U", "Ry", "Rt", "C", "T"],
SystemApiCallId::PerformanceCounter => vec!["*", "s"],
SystemApiCallId::IsController => vec!["*", "s"],
SystemApiCallId::IsSubnetAdmin => vec!["*", "s"],
SystemApiCallId::InReplicatedExecution => vec!["*", "s"],
SystemApiCallId::CostCall => vec!["*", "s"],
SystemApiCallId::CostCreateCanister => vec!["*", "s"],
Expand Down Expand Up @@ -887,6 +888,16 @@ fn api_availability_test(
context,
);
}
SystemApiCallId::IsSubnetAdmin => {
assert_api_availability(
|api| api.ic0_is_subnet_admin(0, 0, &mut Heap::unchecked(&mut [42; 128])),
api_type,
&system_state,
cycles_account_manager,
api_type_enum,
context,
);
}
SystemApiCallId::InReplicatedExecution => {
assert_api_availability(
|api| api.ic0_in_replicated_execution(),
Expand Down Expand Up @@ -2056,6 +2067,22 @@ fn ic0_is_controller_invalid_principal_id() {
));
}

#[test]
fn ic0_is_subnet_admin_invalid_principal_id() {
let api = get_system_api(
ApiTypeBuilder::build_update_api(),
&SystemStateBuilder::default().build(),
CyclesAccountManagerBuilder::new().build(),
);
let mut admin = [0_u8; 70];
assert!(matches!(
api.ic0_is_subnet_admin(0, admin.len(), &mut Heap::unchecked(&mut admin)),
Err(HypervisorError::InvalidPrincipalId(
PrincipalIdBlobParseError(..)
))
));
}

#[test]
fn test_ic0_cycles_burn() {
let initial_cycles = Cycles::new(5_000_000_000_000);
Expand Down
3 changes: 3 additions & 0 deletions rs/embedders/tests/wasmtime_embedder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3101,6 +3101,8 @@ fn wasm64_import_system_api_functions() {

(import "ic0" "is_controller"
(func $ic0_is_controller (param i64) (param i64) (result i32)))
(import "ic0" "is_subnet_admin"
(func $ic0_is_subnet_admin (param i64) (param i64) (result i32)))

(import "ic0" "subnet_self_size"
(func $ic0_subnet_self_size (result i64)))
Expand Down Expand Up @@ -3149,6 +3151,7 @@ fn wasm64_import_system_api_functions() {
(call $ic0_certified_data_set (i64.const 0) (i64.const 5))
(call $ic0_data_certificate_copy (i64.const 4096) (i64.const 0) (call $ic0_data_certificate_size))
(drop (call $ic0_is_controller (i64.const 0) (i64.const 5)))
(drop (call $ic0_is_subnet_admin (i64.const 0) (i64.const 5)))

)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1801,6 +1801,7 @@ fn query_cache_future_proof_test() {
| SystemApiCallId::GlobalTimerSet
| SystemApiCallId::InReplicatedExecution
| SystemApiCallId::IsController
| SystemApiCallId::IsSubnetAdmin
| SystemApiCallId::MintCycles128
| SystemApiCallId::MsgArgDataCopy
| SystemApiCallId::MsgArgDataSize
Expand Down
117 changes: 117 additions & 0 deletions rs/execution_environment/tests/hypervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11125,6 +11125,123 @@ fn ic0_is_controller() {
);
}

// Replies with `ic0.is_subnet_admin` of the principal passed as the argument,
// as a little-endian u32, from an update and from a query.
const IS_SUBNET_ADMIN_WAT: &str = r#"
(module
(import "ic0" "is_subnet_admin"
(func $is_subnet_admin (param i32 i32) (result i32)))
(import "ic0" "msg_arg_data_size"
(func $msg_arg_data_size (result i32)))
(import "ic0" "msg_arg_data_copy"
(func $msg_arg_data_copy (param i32 i32 i32)))
(import "ic0" "msg_reply" (func $msg_reply))
(import "ic0" "msg_reply_data_append"
(func $msg_reply_data_append (param i32 i32)))
(func $answer
;; heap[100..] = the principal
(call $msg_arg_data_copy (i32.const 100) (i32.const 0) (call $msg_arg_data_size))
;; heap[0..4] = is_subnet_admin(heap[100..])
(i32.store (i32.const 0)
(call $is_subnet_admin (i32.const 100) (call $msg_arg_data_size)))
(call $msg_reply_data_append (i32.const 0) (i32.const 4))
(call $msg_reply)
)
(func (export "canister_update update") (call $answer))
(func (export "canister_query query") (call $answer))
(memory 1 1)
)"#;

fn is_subnet_admin(
test: &mut ExecutionTest,
canister_id: CanisterId,
principal: PrincipalId,
) -> u32 {
let update = get_reply(test.ingress(canister_id, "update", principal.as_slice().to_vec()));
let query =
get_reply(test.non_replicated_query(canister_id, "query", principal.as_slice().to_vec()));
assert_eq!(update, query);
u32::from_le_bytes(update.try_into().unwrap())
}

#[test]
fn ic0_is_subnet_admin_on_a_cloud_engine() {
let admin = user_test_id(100).get();
let mut test = ExecutionTestBuilder::new()
.with_subnet_type(SubnetType::CloudEngine)
.with_cost_schedule(CanisterCyclesCostSchedule::Free)
.with_subnet_admins(vec![admin])
.build();
let canister_id = test.canister_from_wat(IS_SUBNET_ADMIN_WAT).unwrap();
let controller = test.user_id().get();
assert!(
!test
.canister_state(canister_id)
.system_state
.controllers
.contains(&admin)
);

assert_eq!(is_subnet_admin(&mut test, canister_id, admin), 1);
// A controller is not a subnet admin by being a controller.
assert_eq!(is_subnet_admin(&mut test, canister_id, controller), 0);
assert_eq!(
is_subnet_admin(&mut test, canister_id, user_test_id(101).get()),
0
);
}

#[test]
fn ic0_is_subnet_admin_on_a_rented_subnet() {
let admin = user_test_id(100).get();
let mut test = ExecutionTestBuilder::new()
.with_subnet_type(SubnetType::Application)
.with_cost_schedule(CanisterCyclesCostSchedule::Free)
.with_subnet_admins(vec![admin])
.build();
let canister_id = test.canister_from_wat(IS_SUBNET_ADMIN_WAT).unwrap();

assert_eq!(is_subnet_admin(&mut test, canister_id, admin), 1);
assert_eq!(
is_subnet_admin(&mut test, canister_id, user_test_id(101).get()),
0
);
}

// Admins in the topology of a subnet that cannot have subnet admins are
// ignored, as they are by the management canister.
#[test]
fn ic0_is_subnet_admin_returns_0_where_subnet_admins_do_not_apply() {
let admin = user_test_id(100).get();
let mut test = ExecutionTestBuilder::new()
.with_subnet_type(SubnetType::Application)
.with_cost_schedule(CanisterCyclesCostSchedule::Normal)
.with_subnet_admins(vec![admin])
.build();
let canister_id = test.canister_from_wat(IS_SUBNET_ADMIN_WAT).unwrap();

assert_eq!(is_subnet_admin(&mut test, canister_id, admin), 0);
}

#[test]
fn ic0_is_subnet_admin_traps_on_an_invalid_principal() {
let mut test = ExecutionTestBuilder::new()
.with_subnet_type(SubnetType::CloudEngine)
.with_cost_schedule(CanisterCyclesCostSchedule::Free)
.with_subnet_admins(vec![user_test_id(100).get()])
.build();
let canister_id = test.canister_from_wat(IS_SUBNET_ADMIN_WAT).unwrap();

let err = test
.ingress(canister_id, "update", vec![42; 30])
.unwrap_err();
assert_eq!(ErrorCode::CanisterContractViolation, err.code());
assert!(
err.description()
.contains("Canister provided invalid principal id.")
);
}

#[test]
fn ic0_time_constant_within_single_message() {
let mut test = ExecutionTestBuilder::new().build();
Expand Down
15 changes: 15 additions & 0 deletions rs/interfaces/src/execution_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ pub enum SystemApiCallId {
InReplicatedExecution,
/// Tracker for `ic0.is_controller()`
IsController,
/// Tracker for `ic0.is_subnet_admin()`
IsSubnetAdmin,
/// Tracker for `ic0.mint_cycles128()`
MintCycles128,
/// Tracker for `ic0.msg_arg_data_copy()`
Expand Down Expand Up @@ -1300,6 +1302,19 @@ pub trait SystemApi {
heap: &mut Heap<'_>,
) -> HypervisorResult<u32>;

/// Checks whether the principal identified by src/size is a subnet admin
/// of the subnet the canister runs on. If yes, then a value of 1 is
/// returned, otherwise a 0 is returned. On a subnet that cannot have
/// subnet admins, it returns 0. It can be called multiple times.
///
/// This system call traps if src+size exceeds the size of the WebAssembly memory.
fn ic0_is_subnet_admin(
&self,
src: usize,
size: usize,
heap: &mut Heap<'_>,
) -> HypervisorResult<u32>;

/// If run in replicated execution (i.e. an update call or a certified
/// query), returns 1.
/// If run in non-replicated execution (i.e. query),
Expand Down
Loading