Actions, Phases & Proofs
Every pool transaction is a batch of client actions. Actions are grouped into phases with a fixed ordering - a transaction may skip phases, but must never go backwards:
| Phase | Action | Effect on temp balance |
|---|---|---|
| 0 | SetViewingKey |
- |
| 1 | OpenChannel |
- |
| 2 | OpenSubchannel |
- |
| 3 | Deposit |
+ amount |
| 4 | UseNote |
+ note amount |
| 5 | CreateEncNote / CreateOpenNote |
− amount |
| 6 | Withdraw |
− amount |
| 7 | InvokeExternal (at most once) |
- |
The fixed ordering removes state-machine ambiguity: there is exactly one way to
encode a given semantic operation, which closes whole classes of ordering bugs.
InvokeExternal is the composability hook - it calls an anonymizer contract
(escrow, DEX adapter, lending) at most once per transaction.
The balance invariant
Within one transaction, the contract tracks a temporary balance per token:
inflows (Deposit, UseNote) add, outflows (CreateNote, Withdraw)
subtract. Two rules are enforced:
- The balance may never go negative at any point.
- Every token's balance must end at exactly zero.
fn subtract_balance(ref self: TokenBalances, token: ContractAddress, amount: u128) {
let (entry, current_balance) = self.entry(key: token.into());
let new_value = current_balance
.checked_sub(amount)
.expect(errors::NEGATIVE_INTERMEDIATE_BALANCE);
self = entry.finalize(:new_value);
}
fn assert_valid(self: SquashedTokenBalances) {
for (_token, _, balance) in self.into_entries() {
assert(balance.is_zero(), errors::FINAL_BALANCE_MUST_BE_ZERO);
}
}
No value is created, destroyed, or left unaccounted - a transfer of 30 from a 100-note must create outputs totaling 100 (30 to the recipient, 70 change).
From actions to an on-chain transaction
client builds actions
│
▼
virtual Starknet execution ← anchored to a recent block's state
(dedicated virtual OS, snapshot; unsupported syscalls fail
compiles client → server actions) here, at proving time
│
▼
Stwo proof generation ← ~29 s (12-core / 46 GiB; machine-dependent)
│
▼
submit tx (directly or via paymaster)
│
▼
sequencer-level verification ← proof checked before execution;
│ contract validates proof facts
▼
apply_actions: storage writes, token transfers, events
The proof is generated over a virtual Starknet execution environment: the
transaction runs against an anchored state snapshot of a recent block, using
the same Cairo logic the chain uses. That gives native storage semantics and
account-abstraction signature checks (is_valid_signature) for free.
On submission, the contract checks the proof facts before applying anything:
- the proof came from the expected program variant (
VIRTUAL_SNOS) - the anchor block is recent - within
proof_validity_blocksof the tip - the proven message hash matches the submitted actions exactly
Stale proofs expire; mismatched actions are rejected; only then are storage writes, ERC-20 transfers and events applied atomically.
Next: the escrowed key that makes all of this auditable - Compliance & Auditing.