Caller Identity
Shared functions can access the Principal of their caller for authorization, ownership checks, and audit trails.
Overview
Every call to a shared function on the Internet Computer carries the identity of
the caller as a Principal. In verified code, prefer shared({caller}) when
the caller must appear in contracts, or Runtime.callerId() when the body only
needs the current runtime caller.
// Basic caller identity access
import Runtime "mo:core/Runtime";
persistent actor {
// Destructure to get caller directly
public shared({caller}) func whoAmI() : async Principal
ensures result == caller;
{
caller
};
// Runtime helper for the current caller
public shared func whoAmIRuntime() : async Principal
ensures result == Runtime.callerId();
{
Runtime.callerId()
};
}
The shared({caller}) pattern binds the caller directly as a method value.
Binding Patterns
Use these verified patterns for caller-sensitive code:
// Different ways to bind caller identity
import Runtime "mo:core/Runtime";
persistent actor {
// Pattern 1: Destructure caller directly for contracts and body code
public shared({caller}) func pattern1() : async Principal
ensures result == caller;
{
caller
};
// Pattern 2: Ignore the message pattern and use the runtime helper
public shared func pattern2() : async Principal
ensures result == Runtime.callerId();
{
Runtime.callerId()
};
// Pattern 3: Ignore caller entirely
public shared func pattern3() : async () {
// Caller not needed
};
// Pattern 4: Query with caller
public shared query({caller}) func queryWithCaller() : async Principal {
caller
};
}
| Pattern | Description |
|---|---|
shared({caller}) func | Bind caller directly for contracts and body code |
shared func with Runtime.callerId() | Read current runtime caller in the body |
shared func | Ignore caller entirely |
shared query({caller}) func | Bind caller for a query response |
The {caller} destructuring pattern is the most common choice when caller
identity is part of the method contract.
Access Control
The primary use of caller identity is access control - restricting who can execute certain operations.
Owner Pattern
The simplest access control pattern uses a single owner:
// Owner-only access control pattern
persistent actor {
var owner : ?Principal = null;
var balance : Nat = 0;
// First caller claims ownership
public shared({caller}) func claimOwnership() : async ()
modifies owner
entry_requires owner == null;
requires owner == null;
ensures owner == ?caller;
{
owner := ?caller;
};
// Only owner can withdraw
public shared({caller}) func withdraw(amount : Nat) : async ()
modifies balance
reads owner
entry_requires owner == ?caller and amount <= balance;
requires owner == ?caller;
requires amount <= balance;
ensures balance == old(balance) - amount;
{
balance -= amount;
};
// Anyone can deposit
public func deposit(amount : Nat) : async ()
modifies balance
ensures balance == old(balance) + amount;
{
balance += amount;
};
// Only owner can transfer ownership
public shared({caller}) func transferOwnership(newOwner : Principal) : async ()
modifies owner
entry_requires owner == ?caller;
requires owner == ?caller;
ensures owner == ?newOwner;
{
owner := ?newOwner;
};
}
For exported actor methods, the runtime guard is the entry_requires clause.
The matching logical requires clause gives the verifier the same fact inside
the method body. Together, entry_requires owner == ?caller and
requires owner == ?caller ensure only the owner can call protected
functions. The verifier proves that:
- Any caller of
withdrawmust be the current owner - Only the owner can transfer ownership
- Public functions like
depositaccept any caller
Role-Based Access
For multiple authorized users, keep the deployable role state in runtime fields or runtime collections, then mirror it with ghost state only when you need a proof model:
// Role-based access control with caller identity
persistent actor {
var data : Nat = 0;
var primaryAdmin : ?Principal = null;
var secondaryAdmin : ?Principal = null;
// First caller becomes the primary admin
public shared({caller}) func claimPrimaryAdmin() : async ()
modifies primaryAdmin
entry_requires primaryAdmin == null;
requires primaryAdmin == null;
ensures primaryAdmin == ?caller;
{
primaryAdmin := ?caller;
};
// Only admins can modify data
public shared({caller}) func setData(value : Nat) : async ()
modifies data
reads primaryAdmin, secondaryAdmin
entry_requires primaryAdmin == ?caller or secondaryAdmin == ?caller;
requires primaryAdmin == ?caller or secondaryAdmin == ?caller;
ensures data == value;
{
data := value;
};
// Only the primary admin can rotate the secondary admin
public shared({caller}) func setSecondaryAdmin(newAdmin : Principal) : async ()
modifies secondaryAdmin
reads primaryAdmin
entry_requires primaryAdmin == ?caller;
requires primaryAdmin == ?caller;
ensures secondaryAdmin == ?newAdmin;
{
secondaryAdmin := ?newAdmin;
};
// Anyone can read data
public func getData() : async Nat
reads data
ensures result == data;
{
data
};
}
Ghost sets are proof-only and cannot be the source of runtime authorization.
Use entry_requires over runtime role state for exported access checks, and mirror the runtime state with ghost variables only for proofs.
Using Caller in Contracts
Caller identity can appear in requires and ensures clauses:
// Using caller in verification contracts
persistent actor {
var owner : ?Principal = null;
var value : Nat = 0;
public shared({caller}) func claimOwnership() : async ()
modifies owner
entry_requires owner == null;
requires owner == null;
ensures owner == ?caller;
{
owner := ?caller;
};
// Caller in requires clause for access control
public shared({caller}) func ownerOnly() : async Nat
reads value, owner
entry_requires owner == ?caller;
requires owner == ?caller;
ensures result == value;
{
value
};
// Caller equality in ensures clause
public shared({caller}) func returnCaller() : async Principal
ensures result == caller;
{
caller
};
// Conditional postcondition based on caller
public shared({caller}) func getValueIfOwner() : async ?Nat
reads value, owner
ensures owner == ?caller ==> result == ?value;
ensures owner != ?caller ==> result == null;
{
if (owner == ?caller) {
?value
} else {
null
}
};
}
Contract usage:
entry_requires owner == ?caller- Runtime entry guard for an exported methodrequires owner == ?caller- Logical proof fact that should match the entry guardensures result == caller- Return value equals the callerensures owner == ?caller ==> ...- Conditional postcondition based on caller
The caller is stable within a function call, so you can reference it in both preconditions and postconditions. Across an await, the bound caller value remains the same but actor fields must be reasoned about through invariants and explicit post-await checks.
Actor Classes
For verified actor classes, pass the owner principal explicitly through a shared constructor parameter:
// Configuring an owner at actor creation
persistent actor class Wallet(initialOwner : Principal) = this {
// Capture the owner from an explicit constructor parameter
transient let owner : ?Principal = ?initialOwner;
transient var balance : Nat = 0;
public func deposit(amount : Nat) : async ()
modifies balance
ensures balance == old(balance) + amount;
{
balance += amount;
};
public shared({caller}) func withdraw(amount : Nat) : async ()
modifies balance
reads owner
entry_requires owner == ?caller and amount <= balance;
requires owner == ?caller;
requires amount <= balance;
ensures balance == old(balance) - amount;
{
balance -= amount;
};
public func getBalance() : async Nat
reads balance
ensures result == balance;
{
balance
};
}
This pattern is useful for:
- Setting the initial owner
- Recording the creator's identity
- Establishing trust relationships at deployment
Query Functions
Query functions can also access caller identity:
public shared query({caller}) func queryWithCaller() : async Principal {
caller
}
However, query responses are not consensus-certified by default. Use update functions for security-critical authorization decisions or pair query data with certified responses when clients must rely on it.
Verification Model
In the Viper verification backend, Principal is modeled as an opaque value where equality is the main proof operation available to ordinary code. This means:
You can verify:
- Equality checks:
owner == ?caller - Runtime role checks over deployable state
- Passing principals through functions unchanged
You cannot verify:
- Principal string formatting or parsing
- Relationships between principals (hierarchy, delegation)
- Properties of anonymous or system principals
Caller Stability
The caller remains stable within a single function invocation:
public shared({caller}) func example() : async Principal
ensures result == caller;
{
let c1 = caller;
// ... some code ...
let c2 = caller;
assert c1 == c2; // Verified: caller doesn't change mid-call
c2
};
The bound caller value remains stable across await because it is an entry
message value. Runtime.callerId() is modeled as current call-context state and
should not be used as a cross-await stable fact. Actor state may still change due
to interleaved calls.
Common Patterns
Check-Then-Act
public shared({caller}) func restrictedAction() : async ()
reads owner
entry_requires owner == ?caller;
requires owner == ?caller;
{
// Safe: only owner reaches here
doSomethingPrivileged();
};
Dynamic Role Lookup
public shared({caller}) func roleBasedAction(role : Text) : async ()
reads admin, user
entry_requires (role == "admin" and admin == ?caller)
or (role == "user" and user == ?caller);
requires (role == "admin" and admin == ?caller)
or (role == "user" and user == ?caller);
{
// Caller has appropriate role
};
Return Caller for Logging
public shared({caller}) func auditedAction() : async Principal
ensures result == caller;
{
// Perform action, return caller for audit trail
caller
};
Common Mistakes
Forgetting shared Keyword
// Wrong: no bound caller value
public func noCallerAccess() : async () {
// caller is not defined here
};
// Correct: shared grants caller access
public shared({caller}) func hasCallerAccess() : async () {
let _ = caller;
};
Using Caller After Await
public shared({caller}) func risky() : async () {
let entryCaller = caller;
try { await someExternalCall() } catch (_) {};
// entryCaller is still valid
// but actor state may have changed
// invariants are re-assumed, not proven from pre-await state
};
The caller identity itself remains stable, but reasoning about state changes requires invariants.
Trusting Query Responses
// Less suitable for critical decisions: query response is not consensus-certified
public shared query({caller}) func queryCheck() : async Bool {
owner == ?caller
};
// More secure: update function for critical checks
public shared({caller}) func updateCheck() : async Bool {
owner == ?caller
};
Do not let clients treat an uncertified query response as an authorization decision that changes value or state. Use update methods for decisions that matter.
Summary
- Use
shared({caller})to bind the caller'sPrincipalfor contracts - Caller identity enables access control via
entry_requires owner == ?callerplus a matching logicalrequires - Ghost sets (
Set<Principal>) support proof models, but runtime authorization must use runtime state - Actor classes can capture an owner principal from a constructor parameter
- The caller is stable within a function but state may change across
await - Query responses are not consensus-certified by default; use update functions for critical authorization
- Principal is modeled opaquely - ordinary authorization proofs should rely on equality and runtime state
Related Topics
- Entry requires for runtime caller guards
- Access control patterns for owner and role proofs
- Async await for cross-await caller and state reasoning
- Runtime module for
Runtime.callerId()