Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ escargot = "0.5.15"
futures = "0.3"
ic-vetkd-utils = { git = "https://github.com/dfinity/ic", rev = "95231520" }
lazy_static = "1.5.0"
pocket-ic = { git = "https://github.com/dfinity/ic", tag = "release-2026-03-02_11-09-base" }
pocket-ic = { git = "https://github.com/dfinity/ic", rev = "719ff387aab462ce5759c4c20c30de959fe9538c" } # 2026-04-15 https://github.com/dfinity/ic/pull/9845
prost = "0.14.3"
prost-build = "0.14.3"
reqwest = "0.13.2"
Expand Down
17 changes: 17 additions & 0 deletions e2e-tests/src/bin/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ fn call_msg_reply() {
msg_reply(vec![42]);
}

/// Returns the caller info data bytes provided in the `sender_info`.
/// Returns empty bytes if no `sender_info` was provided.
#[unsafe(export_name = "canister_update call_msg_caller_info_data")]
fn call_msg_caller_info_data() {
msg_reply(msg_caller_info_data());
}

/// Returns the caller info signer as raw principal bytes.
/// Returns empty bytes if no `sender_info` was provided.
#[unsafe(export_name = "canister_update call_msg_caller_info_signer")]
fn call_msg_caller_info_signer() {
let signer_bytes = msg_caller_info_signer()
.map(|p| p.as_slice().to_vec())
.unwrap_or_default();
msg_reply(signer_bytes);
}

#[unsafe(export_name = "canister_update call_msg_reject")]
fn call_msg_reject() {
msg_reject("e2e test reject");
Expand Down
43 changes: 40 additions & 3 deletions e2e-tests/tests/api.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use candid::Principal;
use ic_cdk_management_canister::{CanisterSettings, EnvironmentVariable, UpdateSettingsArgs};
use pocket_ic::ErrorCode;
use pocket_ic::{ErrorCode, common::rest::RawSenderInfo};

mod test_utilities;
use test_utilities::{cargo_build_canister, pic_base, update};

#[test]
fn call_api() {
let wasm = cargo_build_canister("api");
// with_ii_subnet is required for testing the ic0.cost_sign_with_* API with pre-defined key name.
let pic = pic_base().with_ii_subnet().build();
// with_test_threshold_keys_subnet is required for testing the ic0.cost_sign_with_* API with pre-defined key name.
let pic = pic_base().with_test_threshold_keys_subnet().build();
let canister_id = pic.create_canister();
pic.add_cycles(canister_id, 100_000_000_000_000);
pic.install_canister(canister_id, wasm, vec![], None);
Expand All @@ -25,6 +25,43 @@ fn call_api() {
// Unlike the other entry points, `call_msg_dealine_caller` was implemented with the `#[update]` macro.
// So we use the update method which assumes candid
let _: () = update(&pic, canister_id, "call_msg_deadline_caller", ()).unwrap();
let info_data = b"test_caller_info_data";
let sender_info = RawSenderInfo {
info: info_data.to_vec(),
signer: canister_id.as_slice().to_vec(),
};
// With sender_info: data should equal the info bytes that were sent.
Comment thread
lwshang marked this conversation as resolved.
let res = pic
.update_call_with_sender_info(
canister_id,
sender,
"call_msg_caller_info_data",
vec![],
sender_info.clone(),
)
.unwrap();
assert_eq!(res, info_data);
// Without sender_info: data should be empty.
let res = pic
.update_call(canister_id, sender, "call_msg_caller_info_data", vec![])
.unwrap();
assert!(res.is_empty());
// With sender_info: signer should be the canister_id principal bytes.
let res = pic
.update_call_with_sender_info(
canister_id,
sender,
"call_msg_caller_info_signer",
Comment thread
aterga marked this conversation as resolved.
vec![],
sender_info,
)
.unwrap();
assert_eq!(res, canister_id.as_slice());
// Without sender_info: signer should be None, returning empty bytes.
let res = pic
.update_call(canister_id, sender, "call_msg_caller_info_signer", vec![])
.unwrap();
assert!(res.is_empty());
// `msg_reject_code` and `msg_reject_msg` can't be tested here.
// They are invoked in the reply/reject callback of inter-canister calls.
// So the `call.rs` test covers them.
Expand Down
6 changes: 4 additions & 2 deletions e2e-tests/tests/management_canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ use test_utilities::{cargo_build_canister, pic_base, update};
#[test]
fn test_management_canister() {
let wasm = cargo_build_canister("management_canister");
// Setup pocket-ic with an II subnet which is required by the "provisional" test.
let pic = pic_base().with_ii_subnet().build();
let pic = pic_base()
.with_ii_subnet() // II subnet is required by the "provisional" test.
.with_test_threshold_keys_subnet() // threshold keys subnet is required for testing ecdsa/schnorr signing APIs with pre-defined key name.
.build();

let canister_id = pic.create_canister();
let subnet_id = pic.get_subnet(canister_id).unwrap();
Expand Down
22 changes: 14 additions & 8 deletions e2e-tests/tests/test_utilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,30 +114,36 @@ fn check_pocket_ic_server() -> PathBuf {
.iter()
.find(|m| m.name.as_ref() == "ic-cdk-e2e-tests")
.expect("ic-cdk-e2e-tests not found in Cargo.toml");
let pocket_ic_tag = e2e_tests_package
let source_repr = &e2e_tests_package
.dependencies
.iter()
.find(|d| d.name == "pocket-ic")
.expect("pocket-ic not found in Cargo.toml")
.source
.as_ref()
.expect("pocket-ic source not found in Cargo.toml")
.repr
.split_once("tag=")
.expect("`tag=` not found in pocket-ic source")
.1;
.repr;
// Source URL is e.g. `git+https://...?rev=<hash>#<hash>` or `git+https://...?tag=<tag>#<hash>`.
// Extract the value after `rev=` or `tag=`, stopping at `#` or `&`.
let pocket_ic_ref = if let Some((_, rest)) = source_repr.split_once("rev=") {
rest.split_once(['#', '&']).map_or(rest, |(v, _)| v)
} else if let Some((_, rest)) = source_repr.split_once("tag=") {
rest.split_once(['#', '&']).map_or(rest, |(v, _)| v)
} else {
panic!("neither `rev=` nor `tag=` found in pocket-ic source: {source_repr}")
};
let target_dir = metadata.target_directory;
let artifact_dir = target_dir.join("e2e-tests-artifacts");
let tag_path = artifact_dir.join("pocket-ic-tag");
let server_binary_path = artifact_dir.join("pocket-ic");
if let Ok(tag) = std::fs::read_to_string(&tag_path)
&& tag == pocket_ic_tag
if let Ok(cached) = std::fs::read_to_string(&tag_path)
&& cached == pocket_ic_ref
&& server_binary_path.exists()
{
return server_binary_path.into();
}
panic!(
"pocket-ic server not found or tag mismatch, please run `scripts/download_pocket_ic_server.sh` in the project root"
"pocket-ic server not found or version mismatch, please run `scripts/download_pocket_ic_server.sh` in the project root"
);
}

Expand Down
35 changes: 16 additions & 19 deletions ic-cdk-timers/src/global_timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,31 +108,28 @@ fn do_timer(
interval,
concurrent_calls,
..
} => {
if *concurrent_calls >= 5 {
ic0::debug_print(
format!(
"[ic-cdk-timers] canister_global_timer: \
too many concurrent calls for single timer ({}), \
rescheduling for next possible execution time",
concurrent_calls
)
.as_bytes(),
);
// Reschedule based on `now`, not `timer_scheduled_time`,
// intentionally skipping intermediate executions.
reschedule_timer(timers, task_id, now, *interval);
return true; // skip
}
} if *concurrent_calls >= 5 => {
ic0::debug_print(
format!(
"[ic-cdk-timers] canister_global_timer: \
too many concurrent calls for single timer ({}), \
rescheduling for next possible execution time",
concurrent_calls
)
.as_bytes(),
);
// Reschedule based on `now`, not `timer_scheduled_time`,
// intentionally skipping intermediate executions.
reschedule_timer(timers, task_id, now, *interval);
true // skip
}
// 3. Check serial timer is available
Task::RepeatedSerialBusy { interval } => {
reschedule_timer(timers, task_id, timer_scheduled_time, *interval);
return true; // skip
true // skip
}
_ => (),
_ => false, // do not skip
}
false // do not skip
});
if skip {
return ControlFlow::Continue(());
Expand Down
4 changes: 4 additions & 0 deletions ic-cdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

### Added

- Added `msg_caller_info_data` and `msg_caller_info_signer` to `ic_cdk::api`, exposing the caller's identity attribute data and the signing canister ID respectively.

## [0.20.0] - 2026-03-05

### Changed
Expand Down
26 changes: 26 additions & 0 deletions ic-cdk/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,32 @@ pub fn msg_caller() -> Principal {
Principal::try_from(&buf).unwrap()
}

/// Gets auxiliary data about the caller as provided by the canister with which the caller's identity is associated.
///
/// This only returns non-empty data if the caller is a self-authenticating principal authenticated
/// by canister signatures (e.g. Internet Identity). Returns empty bytes when the caller is another canister.
pub fn msg_caller_info_data() -> Vec<u8> {
let len = ic0::msg_caller_info_data_size();
let mut buf = vec![0u8; len];
ic0::msg_caller_info_data_copy(&mut buf, 0);
buf
}

/// Gets the canister ID of the canister that provided the caller's canister signature.
///
/// Returns `None` if the caller is not a self-authenticating principal authenticated by canister
/// signatures (e.g. when the caller is another canister or no sender info was provided).
pub fn msg_caller_info_signer() -> Option<Principal> {
let len = ic0::msg_caller_info_signer_size();
if len == 0 {
return None;
}
let mut buf = vec![0u8; len];
ic0::msg_caller_info_signer_copy(&mut buf, 0);
// Trust that the system always returns a valid principal when non-empty.
Some(Principal::try_from(&buf).expect("msg_caller_info_signer must be a valid principal"))
}

/// Returns the reject code, if the current function is invoked as a reject callback.
pub fn msg_reject_code() -> u32 {
ic0::msg_reject_code()
Expand Down
4 changes: 4 additions & 0 deletions ic0/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

### Added

- Added `msg_caller_info_data_size`, `msg_caller_info_data_copy`, `msg_caller_info_signer_size`, and `msg_caller_info_signer_copy` API bindings.

## [1.0.1] - 2025-09-11

### Added
Expand Down
4 changes: 4 additions & 0 deletions ic0/ic0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
ic0.msg_arg_data_copy : (dst : I, offset : I, size : I) -> (); // I U RQ NRQ CQ Ry CRy F
ic0.msg_caller_size : () -> I; // *
ic0.msg_caller_copy : (dst : I, offset : I, size : I) -> (); // *
ic0.msg_caller_info_data_size : () -> I; // U RQ NRQ CQ Ry Rt CRy CRt C CC F
ic0.msg_caller_info_data_copy : (dst : I, offset : I, size : I) -> (); // U RQ NRQ CQ Ry Rt CRy CRt C CC F
ic0.msg_caller_info_signer_size : () -> I; // U RQ NRQ CQ Ry Rt CRy CRt C CC F
ic0.msg_caller_info_signer_copy : (dst : I, offset : I, size : I) -> (); // U RQ NRQ CQ Ry Rt CRy CRt C CC F
ic0.msg_reject_code : () -> i32; // Ry Rt CRy CRt
ic0.msg_reject_msg_size : () -> I ; // Rt CRt
ic0.msg_reject_msg_copy : (dst : I, offset : I, size : I) -> (); // Rt CRt
Expand Down
8 changes: 8 additions & 0 deletions ic0/manual_safety_comments.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ ic0.msg_caller_size : () -> I;
Always safe to call
ic0.msg_caller_copy : (dst : I, offset : I, size : I) -> (); // *
`dst` must be a pointer to a writable sequence of bytes with size `size`. The `offset` parameter does not affect safety.
ic0.msg_caller_info_data_size : () -> I; // U RQ NRQ CQ Ry Rt CRy CRt C CC F
Always safe to call
ic0.msg_caller_info_data_copy : (dst : I, offset : I, size : I) -> (); // U RQ NRQ CQ Ry Rt CRy CRt C CC F
`dst` must be a pointer to a writable sequence of bytes with size `size`. The `offset` parameter does not affect safety.
ic0.msg_caller_info_signer_size : () -> I; // U RQ NRQ CQ Ry Rt CRy CRt C CC F
Always safe to call
ic0.msg_caller_info_signer_copy : (dst : I, offset : I, size : I) -> (); // U RQ NRQ CQ Ry Rt CRy CRt C CC F
`dst` must be a pointer to a writable sequence of bytes with size `size`. The `offset` parameter does not affect safety.
ic0.msg_reject_code : () -> i32; // Ry Rt CRy CRt
Always safe to call
ic0.msg_reject_msg_size : () -> I ; // Rt CRt
Expand Down
46 changes: 46 additions & 0 deletions ic0/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,52 @@ pub fn msg_caller_copy_uninit(dst: &mut [MaybeUninit<u8>], offset: usize) {
unsafe { sys::msg_caller_copy(dst.as_mut_ptr() as usize, offset, dst.len()) }
}

#[inline]
pub fn msg_caller_info_data_size() -> usize {
// SAFETY: ic0.msg_caller_info_data_size is always safe to call
unsafe { sys::msg_caller_info_data_size() }
}

#[inline]
pub fn msg_caller_info_data_copy(dst: &mut [u8], offset: usize) {
// SAFETY: dst is a writable sequence of bytes and therefore safe to pass as ptr and len to ic0.msg_caller_info_data_copy
// The offset parameter does not affect safety
unsafe { sys::msg_caller_info_data_copy(dst.as_mut_ptr() as usize, offset, dst.len()) }
}

/// # Safety
///
/// This function will fully initialize `dst` (or trap if it cannot).
#[inline]
pub fn msg_caller_info_data_copy_uninit(dst: &mut [MaybeUninit<u8>], offset: usize) {
// SAFETY: dst is a writable sequence of bytes and therefore safe to pass as ptr and len to ic0.msg_caller_info_data_copy
// The offset parameter does not affect safety
unsafe { sys::msg_caller_info_data_copy(dst.as_mut_ptr() as usize, offset, dst.len()) }
}

#[inline]
pub fn msg_caller_info_signer_size() -> usize {
// SAFETY: ic0.msg_caller_info_signer_size is always safe to call
unsafe { sys::msg_caller_info_signer_size() }
}

#[inline]
pub fn msg_caller_info_signer_copy(dst: &mut [u8], offset: usize) {
// SAFETY: dst is a writable sequence of bytes and therefore safe to pass as ptr and len to ic0.msg_caller_info_signer_copy
// The offset parameter does not affect safety
unsafe { sys::msg_caller_info_signer_copy(dst.as_mut_ptr() as usize, offset, dst.len()) }
}

/// # Safety
///
/// This function will fully initialize `dst` (or trap if it cannot).
#[inline]
pub fn msg_caller_info_signer_copy_uninit(dst: &mut [MaybeUninit<u8>], offset: usize) {
// SAFETY: dst is a writable sequence of bytes and therefore safe to pass as ptr and len to ic0.msg_caller_info_signer_copy
// The offset parameter does not affect safety
unsafe { sys::msg_caller_info_signer_copy(dst.as_mut_ptr() as usize, offset, dst.len()) }
}

#[inline]
pub fn msg_reject_code() -> u32 {
// SAFETY: ic0.msg_reject_code is always safe to call
Expand Down
24 changes: 24 additions & 0 deletions ic0/src/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ unsafe extern "C" {
#[doc = "# Safety\n\n`dst` must be a pointer to a writable sequence of bytes with size `size`. The `offset` parameter does not affect safety."]
pub fn msg_caller_copy(dst: usize, offset: usize, size: usize);
#[doc = "# Safety\n\nAlways safe to call"]
pub fn msg_caller_info_data_size() -> usize;
#[doc = "# Safety\n\n`dst` must be a pointer to a writable sequence of bytes with size `size`. The `offset` parameter does not affect safety."]
pub fn msg_caller_info_data_copy(dst: usize, offset: usize, size: usize);
#[doc = "# Safety\n\nAlways safe to call"]
pub fn msg_caller_info_signer_size() -> usize;
#[doc = "# Safety\n\n`dst` must be a pointer to a writable sequence of bytes with size `size`. The `offset` parameter does not affect safety."]
pub fn msg_caller_info_signer_copy(dst: usize, offset: usize, size: usize);
#[doc = "# Safety\n\nAlways safe to call"]
pub fn msg_reject_code() -> u32;
#[doc = "# Safety\n\nAlways safe to call"]
pub fn msg_reject_msg_size() -> usize;
Expand Down Expand Up @@ -164,6 +172,22 @@ mod non_wasm {
panic!("msg_caller_copy should only be called inside canisters.");
}
#[doc = "# Safety\n\nAlways safe to call"]
pub unsafe fn msg_caller_info_data_size() -> usize {
panic!("msg_caller_info_data_size should only be called inside canisters.");
}
#[doc = "# Safety\n\n`dst` must be a pointer to a writable sequence of bytes with size `size`. The `offset` parameter does not affect safety."]
pub unsafe fn msg_caller_info_data_copy(dst: usize, offset: usize, size: usize) {
panic!("msg_caller_info_data_copy should only be called inside canisters.");
}
#[doc = "# Safety\n\nAlways safe to call"]
pub unsafe fn msg_caller_info_signer_size() -> usize {
panic!("msg_caller_info_signer_size should only be called inside canisters.");
}
#[doc = "# Safety\n\n`dst` must be a pointer to a writable sequence of bytes with size `size`. The `offset` parameter does not affect safety."]
pub unsafe fn msg_caller_info_signer_copy(dst: usize, offset: usize, size: usize) {
panic!("msg_caller_info_signer_copy should only be called inside canisters.");
}
#[doc = "# Safety\n\nAlways safe to call"]
pub unsafe fn msg_reject_code() -> u32 {
panic!("msg_reject_code should only be called inside canisters.");
}
Expand Down
Loading
Loading