Skip to main content

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.

caller-basic.sr9
// 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:

caller-patterns.sr9
// 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
};
}
PatternDescription
shared({caller}) funcBind caller directly for contracts and body code
shared func with Runtime.callerId()Read current runtime caller in the body
shared funcIgnore caller entirely
shared query({caller}) funcBind 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:

caller-owner-check.sr9
// 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:

  1. Any caller of withdraw must be the current owner
  2. Only the owner can transfer ownership
  3. Public functions like deposit accept 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:

caller-role-set.sr9
// 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:

caller-in-contract.sr9
// 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 method
  • requires owner == ?caller - Logical proof fact that should match the entry guard
  • ensures result == caller - Return value equals the caller
  • ensures 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:

caller-actor-class.sr9
// 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's Principal for contracts
  • Caller identity enables access control via entry_requires owner == ?caller plus a matching logical requires
  • 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