canister-security
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCanister Security
Canister 安全指南
What This Is
内容概述
Security patterns for IC canisters in Motoko and Rust. The async messaging model creates TOCTOU (time-of-check-time-of-use) vulnerabilities where state changes between calls. is NOT a reliable security boundary. Anyone on the internet can burn your cycles by sending update calls. This skill provides copy-paste correct patterns for access control, reentrancy prevention, async safety, and callback trap handling.
awaitcanister_inspect_message本文介绍适用于IC Canister的Motoko和Rust语言安全模式。IC的异步消息模型会引发TOCTOU(检查时间与使用时间不一致)漏洞,即调用期间状态可能发生变更。并非可靠的安全边界,互联网上的任何人都可以通过发送更新调用消耗你的Cycle。本文提供可直接复制粘贴的正确实现模式,涵盖访问控制、重入防护、异步安全以及回调陷阱处理等场景。
awaitcanister_inspect_messagePrerequisites
前置条件
- For Motoko: package manager,
mopsin mops.tomlcore = "2.0.0" - For Rust: ,
ic-cdk = "0.19"candid = "0.10"
- 对于Motoko:需安装包管理器,且在mops.toml中配置
mopscore = "2.0.0" - 对于Rust:需安装和
ic-cdk = "0.19"依赖candid = "0.10"
Security Pitfalls
安全陷阱
-
Relying onfor access control. This hook runs on a single replica without full consensus. A malicious boundary node can bypass it by forwarding the message anyway. It is also never called for inter-canister calls, query calls, or management canister calls. Always duplicate access checks inside every update method. Use
canister_inspect_messageonly as a cycle-saving optimization, never as a security boundary.inspect_message -
Forgetting to reject the anonymous principal. Every endpoint that requires authentication must check that the caller is not the anonymous principal (). In Motoko use
2vxsx-fae, in Rust comparePrincipal.isAnonymous(caller). Without this, unauthenticated callers can invoke protected methods — and if the canister uses the caller principal as an identity key (e.g., for balances), the anonymous principal becomes a shared identity anyone can use.msg_caller() != Principal::anonymous() -
Reading state before an async call and assuming it's unchanged after (TOCTOU). When your canisters an inter-canister call, other messages can interleave and mutate state. This is one of the most critical sources of DeFi exploits on IC. Use per-caller locking (CallerGuard pattern) to prevent concurrent operations. For financial operations, also consider the saga pattern (deduct before
await, compensate on failure) — but implementing it correctly is complex due to edge cases like callback traps and call timeouts where the outcome is ambiguous.await -
Trapping in. If
pre_upgradetraps (e.g., serializing too much data exceeds the instruction limit), the canister becomes permanently non-upgradeable. Avoid storing large data structures in the heap that must be serialized during upgrade. In Rust, usepre_upgradefor direct stable memory access. In Motoko, theic-stable-structuresdeclaration stores allpersistent actorandletvariables automatically in stable memory — no manual serialization needed.var -
Not monitoring cycles balance. Every canister has a defaultof 2,592,000 seconds (~30 days). When cycles drop below the threshold reserve, the canister freezes (rejects all update calls). When cycles reach zero, the canister is uninstalled — its code and memory are removed, though the canister ID and controllers survive. The real pitfall is not actively monitoring and topping up cycles. For production canisters holding valuable state, increase the freezing threshold and set up automated monitoring.
freezing_thresholdbash# Check current settings (mainnet) icp canister settings show backend -e ic # Increase freezing threshold for high-value canisters icp canister settings update backend --freezing-threshold 7776000 -e ic # 90 days -
Single controller with no backup. If you lose the controller identity's private key, the canister becomes unupgradeable forever. There is no recovery mechanism. Always add a backup controller or governance canister:bash
icp canister settings update backend --add-controller <backup-principal> -e icWhen deploying, ask the developer if they have a backup controller principal to add. -
Callingin production.
fetchRootKey()fetches the root public key from the replica and trusts whatever it returns. On mainnet, the root key is hardcoded into the agent — callingfetchRootKey()there allows a man-in-the-middle to substitute a different key, breaking all verification. Only callfetchRootKey()in local development, guarded by an environment check. For frontends served by asset canisters, the root key is provided automatically.fetchRootKey() -
Exposing admin methods without guards. Every update method is callable by anyone on the internet. Admin methods (migration, config, minting) must explicitly check the caller against an allowlist. There is no built-in role system — you must implement it yourself. Always include admin revocation — missing revocation is a common source of bugs.
-
Storing secrets in canister state. Canister memory on standard application subnets is readable by node operators. Never store private keys, API secrets, or passwords in canister state. For on-chain secret management, use vetKD (threshold key derivation).
-
Allowing unbounded user-controlled storage. If users can store data without limits, an attacker can fill the 4 GiB Wasm heap or stable memory, bricking the canister. Always enforce per-user storage quotas and validate input sizes.
-
Trapping in a callback after state mutation. If your canister mutates state before an inter-canister call and the callback traps, the pre-call mutations persist but the callback's mutations are rolled back. A malicious callee can exploit this to skip security-critical actions like debiting an account. Structure code so that critical state mutations happen before the async boundary and are correctly rolled back if a failure or trap occurs. Use(Motoko) or
try/finallyguards (Rust) to ensure cleanup always runs. Keep cleanup code minimal — trapping in cleanup recreates the problem. Consider usingDropfor rollback logic and journaling for crash-safe state transitions.call_on_cleanup -
Unbounded wait calls preventing upgrades. If your canister makes a call to an untrustworthy or buggy callee that never responds, the canister cannot be stopped (and therefore cannot be upgraded) while awaiting outstanding responses. Use bounded wait calls (timeouts) to ensure calls complete in bounded time regardless of callee behavior.
-
依赖实现访问控制:该钩子仅在单个副本上运行,不经过完整共识。恶意边界节点可以通过直接转发消息绕过它。此外,跨Canister调用、查询调用或管理Canister调用不会触发该钩子。必须在每个更新方法内部重复实现访问检查。仅将
canister_inspect_message作为节省Cycle的优化手段,绝不能将其作为安全边界。inspect_message -
忘记拒绝匿名主体:所有需要身份验证的端点必须检查调用者是否为匿名主体()。在Motoko中使用
2vxsx-fae,在Rust中比较Principal.isAnonymous(caller)。如果缺少此检查,未认证的调用者可以调用受保护的方法——如果Canister使用调用者主体作为身份密钥(例如用于余额管理),匿名主体将成为任何人都可以使用的共享身份。msg_caller() != Principal::anonymous() -
异步调用前读取状态并假设其在调用后保持不变(TOCTOU):当Canister跨Canister调用时,其他消息可能会插入并修改状态。这是IC上DeFi项目遭受攻击的最关键原因之一。使用按调用者锁定的机制(CallerGuard模式)防止并发操作。对于金融操作,还可以考虑Saga模式(在
await前扣除金额,失败时补偿)——但由于回调陷阱和调用超时等边缘情况(结果不明确),正确实现该模式非常复杂。await -
在中触发陷阱:如果
pre_upgrade触发陷阱(例如序列化过多数据超出指令限制),Canister将永久无法升级。避免在堆中存储需要在升级时序列化的大型数据结构。在Rust中,使用pre_upgrade直接访问稳定内存。在Motoko中,ic-stable-structures声明会自动将所有persistent actor和let变量存储在稳定内存中——无需手动序列化。var -
未监控Cycle余额:每个Canister的默认冻结阈值为2,592,000秒(约30天)。当Cycle低于阈值储备时,Canister会冻结(拒绝所有更新调用)。当Cycle耗尽时,Canister将被卸载——其代码和内存会被删除,但Canister ID和控制器会保留。真正的陷阱是未主动监控和补充Cycle。对于存储有价值状态的生产级Canister,应提高冻结阈值并设置自动化监控。bash
# 检查当前设置(主网) icp canister settings show backend -e ic # 为高价值Canister提高冻结阈值 icp canister settings update backend --freezing-threshold 7776000 -e ic # 90天 -
单个控制器且无备份:如果丢失控制器身份的私钥,Canister将永远无法升级,且没有恢复机制。始终添加备份控制器或治理Canister:bash
icp canister settings update backend --add-controller <backup-principal> -e ic部署时,务必询问开发人员是否有要添加的备份控制器主体。 -
在生产环境中调用:
fetchRootKey()从副本获取根公钥并信任返回的任何内容。在主网上,根密钥已硬编码到Agent中——在此处调用fetchRootKey()会允许中间人替换不同的密钥,破坏所有验证。仅在本地开发环境中调用fetchRootKey(),并通过环境检查进行防护。对于由资产Canister提供服务的前端,根密钥会自动提供。fetchRootKey() -
暴露无防护的管理员方法:每个更新方法都可以被互联网上的任何人调用。管理员方法(迁移、配置、铸造等)必须显式检查调用者是否在允许列表中。IC没有内置的角色系统——你必须自行实现。始终包含管理员撤销功能——缺少撤销功能是常见的漏洞来源。
-
在Canister状态中存储机密:标准应用子网中的Canister内存可由节点运营商读取。切勿将私钥、API密钥或密码存储在Canister状态中。对于链上机密管理,请使用vetKD(阈值密钥派生)。
-
允许无限制的用户控制存储:如果用户可以无限制地存储数据,攻击者可能会填满4 GiB的Wasm堆或稳定内存,导致Canister无法使用。始终实施按用户划分的存储配额并验证输入大小。
-
状态变更后在回调中触发陷阱:如果Canister在跨Canister调用前修改了状态,而回调触发陷阱,调用前的变更会保留,但回调的变更会回滚。恶意被调用者可以利用这一点跳过诸如账户扣款等安全关键操作。代码结构应确保关键状态变更发生在异步边界之前,并且在失败或陷阱发生时能正确回滚。使用Motoko的或Rust的
try/finally防护确保清理操作始终运行。保持清理代码最小化——清理时触发陷阱会重现问题。可以考虑使用Drop实现回滚逻辑,并使用日志确保状态转换的崩溃安全性。call_on_cleanup -
无限制的等待调用阻止升级:如果Canister调用不可信或有bug的被调用者且对方永远不响应,Canister在等待未完成的响应时无法停止(因此无法升级)。使用有界等待调用(超时)确保无论被调用者行为如何,调用都能在限定时间内完成。
How It Works
工作原理
IC Security Model
IC安全模型
- Update calls go through consensus — all nodes on a subnet execute the code and must agree on the result. Standard application subnets have 13 nodes; system and fiduciary subnets have more (28+). This makes update calls tamper-proof but slower (~2s).
- Query calls run on a single replica — fast (~200ms) but the replica can return incorrect or malicious results. Replica-signed queries provide partial mitigation (the responding replica signs the response), but for full trust, use certified data or update calls for security-critical reads.
- Inter-canister calls are async messages. Between sending a request and receiving the response, your canister can process other messages. State may change under you (see TOCTOU pitfall above).
- State rollback on trap. If a message execution traps, all its state changes are rolled back. For inter-canister calls, the first execution (before ) and the callback (after
await) are separate messages — a trap in the callback rolls back only the callback's changes, while the first execution's changes persist. This is why cleanup logic (like releasing locks) must go in cleanup context (await/finally), not regular callback code.Drop
- 更新调用经过共识——子网中的所有节点执行代码并必须就结果达成一致。标准应用子网有13个节点;系统和信托子网有更多节点(28+)。这使得更新调用防篡改但速度较慢(约2秒)。
- 查询调用在单个副本上运行——速度快(约200毫秒)但副本可能返回不正确或恶意的结果。副本签名查询提供部分缓解(响应的副本会对响应签名),但为了完全信任,对于安全关键的读取操作应使用认证数据或更新调用。
- 跨Canister调用是异步消息。在发送请求和接收响应之间,Canister可以处理其他消息。状态可能会在你不知情的情况下发生变化(参见上述TOCTOU陷阱)。
- 陷阱时的状态回滚:如果消息执行触发陷阱,其所有状态变更都会回滚。对于跨Canister调用,第一次执行(之前)和回调(
await之后)是单独的消息——回调中的陷阱仅回滚回调的变更,而第一次执行的变更会保留。这就是为什么清理逻辑(如释放锁)必须放在清理上下文(await/finally)中,而不是常规的回调代码中。Drop
Implementation
实现示例
Motoko
Motoko语言
Access control
访问控制
Uses the pattern to capture the deployer atomically — no separate call, no front-running risk.
shared(msg)init()motoko
import Principal "mo:core/Principal";
import Set "mo:core/pure/Set";
import Runtime "mo:core/Runtime";
shared(msg) persistent actor class MyCanister() {
// --- Authorization state ---
// transient: recomputed on each install/upgrade from msg.caller (the controller)
transient let owner = msg.caller;
var admins : Set.Set<Principal> = Set.empty();
// --- Guards ---
func requireAuthenticated(caller : Principal) {
if (Principal.isAnonymous(caller)) {
Runtime.trap("anonymous caller not allowed");
};
};
func requireOwner(caller : Principal) {
requireAuthenticated(caller);
if (caller != owner) {
Runtime.trap("caller is not the owner");
};
};
func requireAdmin(caller : Principal) {
requireAuthenticated(caller);
if (caller != owner and not Set.contains(admins, Principal.compare, caller)) {
Runtime.trap("caller is not an admin");
};
};
// --- Admin management ---
public shared ({ caller }) func addAdmin(newAdmin : Principal) : async () {
requireOwner(caller);
admins := Set.add(admins, Principal.compare, newAdmin);
};
public shared ({ caller }) func removeAdmin(admin : Principal) : async () {
requireOwner(caller);
admins := Set.remove(admins, Principal.compare, admin);
};
// --- Endpoints ---
public shared ({ caller }) func publicAction() : async Text {
requireAuthenticated(caller);
"ok";
};
public shared ({ caller }) func adminAction() : async () {
requireAdmin(caller);
// ... protected logic
};
};使用模式原子性地捕获部署者——无需单独的调用,避免抢先攻击风险。
shared(msg)init()motoko
import Principal "mo:core/Principal";
import Set "mo:core/pure/Set";
import Runtime "mo:core/Runtime";
shared(msg) persistent actor class MyCanister() {
// --- 授权状态 ---
// 瞬态:每次安装/升级时从msg.caller(控制器)重新计算
transient let owner = msg.caller;
var admins : Set.Set<Principal> = Set.empty();
// --- 防护函数 ---
func requireAuthenticated(caller : Principal) {
if (Principal.isAnonymous(caller)) {
Runtime.trap("anonymous caller not allowed");
};
};
func requireOwner(caller : Principal) {
requireAuthenticated(caller);
if (caller != owner) {
Runtime.trap("caller is not the owner");
};
};
func requireAdmin(caller : Principal) {
requireAuthenticated(caller);
if (caller != owner and not Set.contains(admins, Principal.compare, caller)) {
Runtime.trap("caller is not an admin");
};
};
// --- 管理员管理 ---
public shared ({ caller }) func addAdmin(newAdmin : Principal) : async () {
requireOwner(caller);
admins := Set.add(admins, Principal.compare, newAdmin);
};
public shared ({ caller }) func removeAdmin(admin : Principal) : async () {
requireOwner(caller);
admins := Set.remove(admins, Principal.compare, admin);
};
// --- 端点方法 ---
public shared ({ caller }) func publicAction() : async Text {
requireAuthenticated(caller);
"ok";
};
public shared ({ caller }) func adminAction() : async () {
requireAdmin(caller);
// ... 受保护的逻辑
};
};Reentrancy prevention (CallerGuard pattern)
重入防护(CallerGuard模式)
Per-caller locking prevents a second call from the same caller while the first is awaiting a response. The guard must be released in the block — if the callback traps, state changes are rolled back, but runs in cleanup context where state changes persist.
finallycatchfinallymotoko
import Map "mo:core/Map";
import Principal "mo:core/Principal";
import Error "mo:core/Error";
import Result "mo:core/Result";
// Inside the persistent actor class { ... }
// otherCanister is application-specific — replace with your canister reference.
let pendingRequests = Map.empty<Principal, Bool>();
func acquireGuard(principal : Principal) : Result.Result<(), Text> {
if (Map.get(pendingRequests, Principal.compare, principal) != null) {
return #err("already processing a request for this caller");
};
Map.add(pendingRequests, Principal.compare, principal, true);
#ok;
};
func releaseGuard(principal : Principal) {
ignore Map.delete(pendingRequests, Principal.compare, principal);
};
public shared ({ caller }) func doSomethingAsync() : async Result.Result<Text, Text> {
requireAuthenticated(caller);
// 1. Acquire per-caller lock — rejects concurrent calls from same principal
switch (acquireGuard(caller)) {
case (#err(msg)) { return #err(msg) };
case (#ok) {};
};
// 2. Make inter-canister call
try {
let result = await otherCanister.someMethod();
#ok(result)
} catch (e) {
#err("call failed: " # Error.message(e))
} finally {
// Runs in cleanup context even if the callback traps — changes here persist.
releaseGuard(caller);
};
};按调用者锁定可防止同一调用者在第一次调用等待响应时发起第二次调用。防护必须在块中释放——如果回调触发陷阱,中的状态变更会回滚,但在清理上下文中运行,此处的状态变更会保留。
finallycatchfinallymotoko
import Map "mo:core/Map";
import Principal "mo:core/Principal";
import Error "mo:core/Error";
import Result "mo:core/Result";
// 在persistent actor类内部 { ... }
// otherCanister为应用特定的Canister引用——替换为你的Canister引用。
let pendingRequests = Map.empty<Principal, Bool>();
func acquireGuard(principal : Principal) : Result.Result<(), Text> {
if (Map.get(pendingRequests, Principal.compare, principal) != null) {
return #err("already processing a request for this caller");
};
Map.add(pendingRequests, Principal.compare, principal, true);
#ok;
};
func releaseGuard(principal : Principal) {
ignore Map.delete(pendingRequests, Principal.compare, principal);
};
public shared ({ caller }) func doSomethingAsync() : async Result.Result<Text, Text> {
requireAuthenticated(caller);
// 1. 获取按调用者锁定——拒绝来自同一主体的并发调用
switch (acquireGuard(caller)) {
case (#err(msg)) { return #err(msg) };
case (#ok) {};
};
// 2. 发起跨Canister调用
try {
let result = await otherCanister.someMethod();
#ok(result)
} catch (e) {
#err("call failed: " # Error.message(e))
} finally {
// 即使回调触发陷阱也会在清理上下文中运行——此处的变更会保留。
releaseGuard(caller);
};
};inspect_message (cycle optimization only)
inspect_message(仅用于Cycle优化)
motoko
// Inside persistent actor { ... }
// Method variants must match your public methods
system func inspect(
{
caller : Principal;
msg : {
#adminAction : () -> ();
#addAdmin : () -> Principal;
#removeAdmin : () -> Principal;
#publicAction : () -> ();
#doSomethingAsync : () -> ();
}
}
) : Bool {
switch (msg) {
// Admin methods: reject anonymous to save cycles on Candid decoding
case (#adminAction _) { not Principal.isAnonymous(caller) };
case (#addAdmin _) { not Principal.isAnonymous(caller) };
case (#removeAdmin _) { not Principal.isAnonymous(caller) };
case (#doSomethingAsync _) { not Principal.isAnonymous(caller) };
// Public methods: accept all
case (_) { true };
};
};motoko
// 在persistent actor内部 { ... }
// 方法变体必须与你的公共方法匹配
system func inspect(
{
caller : Principal;
msg : {
#adminAction : () -> ();
#addAdmin : () -> Principal;
#removeAdmin : () -> Principal;
#publicAction : () -> ();
#doSomethingAsync : () -> ();
}
}
) : Bool {
switch (msg) {
// 管理员方法:拒绝匿名调用以节省Candid解码的Cycle
case (#adminAction _) { not Principal.isAnonymous(caller) };
case (#addAdmin _) { not Principal.isAnonymous(caller) };
case (#removeAdmin _) { not Principal.isAnonymous(caller) };
case (#doSomethingAsync _) { not Principal.isAnonymous(caller) };
// 公共方法:接受所有调用
case (_) { true };
};
};Rust
Rust语言
Access control (using CDK guard pattern)
访问控制(使用CDK防护模式)
The attribute runs a check before the method body. If the guard returns , the call is rejected before any method code executes. This is more robust than calling guard functions inside the method — you cannot forget to add it.
guardErrrust
use ic_cdk::{init, update};
use ic_cdk::api::msg_caller;
use candid::Principal;
use std::cell::RefCell;
thread_local! {
static OWNER: RefCell<Principal> = RefCell::new(Principal::anonymous());
static ADMINS: RefCell<Vec<Principal>> = RefCell::new(vec![]);
}
// --- Guards (for #[update(guard = "...")] attribute) ---
// Must return Result<(), String>. Err rejects the call.
fn require_authenticated() -> Result<(), String> {
if msg_caller() == Principal::anonymous() {
return Err("anonymous caller not allowed".to_string());
}
Ok(())
}
fn require_owner() -> Result<(), String> {
require_authenticated()?;
OWNER.with(|o| {
if msg_caller() != *o.borrow() {
return Err("caller is not the owner".to_string());
}
Ok(())
})
}
fn require_admin() -> Result<(), String> {
require_authenticated()?;
let caller = msg_caller();
let is_authorized = OWNER.with(|o| caller == *o.borrow())
|| ADMINS.with(|a| a.borrow().contains(&caller));
if !is_authorized {
return Err("caller is not an admin".to_string());
}
Ok(())
}
// --- Init ---
#[init]
fn init(owner: Principal) {
OWNER.with(|o| *o.borrow_mut() = owner);
}
// --- Endpoints ---
#[update(guard = "require_authenticated")]
fn public_action() -> String {
"ok".to_string()
}
#[update(guard = "require_admin")]
fn admin_action() {
// ... protected logic — guard already validated caller
}
#[update(guard = "require_owner")]
fn add_admin(new_admin: Principal) {
ADMINS.with(|a| a.borrow_mut().push(new_admin));
}
#[update(guard = "require_owner")]
fn remove_admin(admin: Principal) {
ADMINS.with(|a| a.borrow_mut().retain(|p| p != &admin));
}
ic_cdk::export_candid!();guardErrrust
use ic_cdk::{init, update};
use ic_cdk::api::msg_caller;
use candid::Principal;
use std::cell::RefCell;
thread_local! {
static OWNER: RefCell<Principal> = RefCell::new(Principal::anonymous());
static ADMINS: RefCell<Vec<Principal>> = RefCell::new(vec![]);
}
// --- 防护函数(用于#[update(guard = "...")]属性) ---
// 必须返回Result<(), String>。Err会拒绝调用。
fn require_authenticated() -> Result<(), String> {
if msg_caller() == Principal::anonymous() {
return Err("anonymous caller not allowed".to_string());
}
Ok(())
}
fn require_owner() -> Result<(), String> {
require_authenticated()?;
OWNER.with(|o| {
if msg_caller() != *o.borrow() {
return Err("caller is not the owner".to_string());
}
Ok(())
})
}
fn require_admin() -> Result<(), String> {
require_authenticated()?;
let caller = msg_caller();
let is_authorized = OWNER.with(|o| caller == *o.borrow())
|| ADMINS.with(|a| a.borrow().contains(&caller));
if !is_authorized {
return Err("caller is not an admin".to_string());
}
Ok(())
}
// --- 初始化 ---
#[init]
fn init(owner: Principal) {
OWNER.with(|o| *o.borrow_mut() = owner);
}
// --- 端点方法 ---
#[update(guard = "require_authenticated")]
fn public_action() -> String {
"ok".to_string()
}
#[update(guard = "require_admin")]
fn admin_action() {
// ... 受保护的逻辑 —— 防护已验证调用者
}
#[update(guard = "require_owner")]
fn add_admin(new_admin: Principal) {
ADMINS.with(|a| a.borrow_mut().push(new_admin));
}
#[update(guard = "require_owner")]
fn remove_admin(admin: Principal) {
ADMINS.with(|a| a.borrow_mut().retain(|p| p != &admin));
}
ic_cdk::export_candid!();Reentrancy prevention (CallerGuard pattern)
重入防护(CallerGuard模式)
CallerGuardDroplet _ = CallerGuard::new(caller)?rust
use std::cell::RefCell;
use std::collections::BTreeSet;
use candid::Principal;
use ic_cdk::update;
use ic_cdk::api::msg_caller;
use ic_cdk::call::Call;
// other_canister_id is application-specific — replace with your canister reference.
thread_local! {
static PENDING: RefCell<BTreeSet<Principal>> = RefCell::new(BTreeSet::new());
}
struct CallerGuard {
principal: Principal,
}
impl CallerGuard {
fn new(principal: Principal) -> Result<Self, String> {
PENDING.with(|p| {
if !p.borrow_mut().insert(principal) {
return Err("already processing a request for this caller".to_string());
}
Ok(Self { principal })
})
}
}
impl Drop for CallerGuard {
fn drop(&mut self) {
PENDING.with(|p| {
p.borrow_mut().remove(&self.principal);
});
}
}
#[update]
async fn do_something_async() -> Result<String, String> {
let caller = msg_caller();
if caller == Principal::anonymous() {
return Err("anonymous caller not allowed".to_string());
}
// Acquire per-caller lock — rejects concurrent calls from same principal.
// Drop releases lock even if callback traps.
let _guard = CallerGuard::new(caller)?;
// Make inter-canister call
let response = Call::bounded_wait(other_canister_id(), "some_method")
.await
.map_err(|e| format!("call failed: {:?}", e))?;
let result: String = response.candid()
.map_err(|e| format!("decode failed: {:?}", e))?;
Ok(result)
// _guard dropped here → lock released
}CallerGuardDroplet _ = CallerGuard::new(caller)?rust
use std::cell::RefCell;
use std::collections::BTreeSet;
use candid::Principal;
use ic_cdk::update;
use ic_cdk::api::msg_caller;
use ic_cdk::call::Call;
// other_canister_id为应用特定的Canister引用——替换为你的Canister引用。
thread_local! {
static PENDING: RefCell<BTreeSet<Principal>> = RefCell::new(BTreeSet::new());
}
struct CallerGuard {
principal: Principal,
}
impl CallerGuard {
fn new(principal: Principal) -> Result<Self, String> {
PENDING.with(|p| {
if !p.borrow_mut().insert(principal) {
return Err("already processing a request for this caller".to_string());
}
Ok(Self { principal })
})
}
}
impl Drop for CallerGuard {
fn drop(&mut self) {
PENDING.with(|p| {
p.borrow_mut().remove(&self.principal);
});
}
}
#[update]
async fn do_something_async() -> Result<String, String> {
let caller = msg_caller();
if caller == Principal::anonymous() {
return Err("anonymous caller not allowed".to_string());
}
// 获取按调用者锁定——拒绝来自同一主体的并发调用。
// 即使回调触发陷阱,Drop也会释放锁。
let _guard = CallerGuard::new(caller)?;
// 发起跨Canister调用
let response = Call::bounded_wait(other_canister_id(), "some_method")
.await
.map_err(|e| format!("call failed: {:?}", e))?;
let result: String = response.candid()
.map_err(|e| format!("decode failed: {:?}", e))?;
Ok(result)
// _guard在此处被丢弃 → 锁释放
}inspect_message (cycle optimization only)
inspect_message(仅用于Cycle优化)
rust
use ic_cdk::api::{accept_message, msg_caller, msg_method_name};
use candid::Principal;
/// Pre-filter to reduce cycle waste from spam.
/// Runs on ONE node. Can be bypassed. NOT a security check.
/// Always duplicate real access control inside each method or via guard attribute.
#[ic_cdk::inspect_message]
fn inspect_message() {
let method = msg_method_name();
match method.as_str() {
// Admin methods: only accept from non-anonymous callers
"admin_action" | "add_admin" | "remove_admin" | "do_something_async" => {
if msg_caller() != Principal::anonymous() {
accept_message();
}
// Silently reject anonymous — saves cycles on Candid decoding
}
// Public methods: accept all
_ => accept_message(),
}
}rust
use ic_cdk::api::{accept_message, msg_caller, msg_method_name};
use candid::Principal;
/// 预过滤以减少垃圾消息的Cycle浪费。
/// 在单个节点上运行。可以被绕过。不是安全检查。
/// 始终在每个方法内部或通过guard属性重复实现真正的访问控制。
#[ic_cdk::inspect_message]
fn inspect_message() {
let method = msg_method_name();
match method.as_str() {
// 管理员方法:仅接受非匿名调用者
"admin_action" | "add_admin" | "remove_admin" | "do_something_async" => {
if msg_caller() != Principal::anonymous() {
accept_message();
}
// 静默拒绝匿名调用——节省Candid解码的Cycle
}
// 公共方法:接受所有调用
_ => accept_message(),
}
}