๐ Trident for Onchain Devs
You know how to write smart contracts. Trident programs look similar but work fundamentally differently. This guide maps your existing mental model -- whether it comes from Solidity, Vyper, Anchor, CosmWasm, or Substrate -- to the zero-knowledge paradigm.
Trident is designed to compile to multiple STARK-based zero-knowledge virtual machines. The primary target today is Triton VM via TASM, but the same source code is designed to target other backends without modification. The result is not a contract deployed on-chain. It is a program that runs locally, produces a cryptographic proof, and lets anyone verify that proof in milliseconds without re-executing anything.
๐ The Paradigm Shift
| Smart Contract (EVM, SVM, CosmWasm) | ZK Program (Trident / Triton VM) |
|---|---|
| Runs on a blockchain VM | Runs locally on the prover's machine |
| State persists between calls | Each proof is standalone -- no persistent state |
| Everyone sees execution (all calldata, storage, logs) | Only the prover sees execution (zero-knowledge) |
| Gas metering at runtime | Proving cost computed at compile time |
| Revert on failure, gas consumed | Assertion failure = no proof generated, nothing consumed |
| Verifier re-executes the transaction | Verifier checks a STARK proof (milliseconds, constant cost) |
| Deployed bytecode lives on-chain | Program is identified by its Tip5 hash |
| Upgradeable via proxy patterns | Config commitments with admin auth; code hash is identity |
| Locked to one VM (EVM, SVM, CairoVM) | Multi-target: same source designed to compile to Triton VM, Miden VM, etc. |
| Security from elliptic curves (secp256k1, ed25519) | Security from hash functions only (quantum-safe) |
The deepest shift: a smart contract is imperative middleware that mutates shared state. A ZK program is a claim about computation -- "I ran this program on these inputs and got these outputs, and here is the proof."
๐ Write Once, Prove Anywhere
Trident compiles the same source to multiple STARK VMs via the --target flag. Programs using only std.* are fully portable. See Multi-Target Compilation for the architecture.
๐ฆ Where's My State?
EVM / Solidity
State lives in 256-bit storage slots. Mappings are keccak256(key . slot).
Every node stores the full state trie. Anyone can read any slot.
// Solidity
mapping(address => uint256) public balances;
function getBalance(address who) view returns (uint256) {
return balances[who];
}
SVM / Anchor
State lives in accounts -- byte buffers owned by programs. You pass accounts into instructions and deserialize them.
CosmWasm
State lives in a key-value store (deps.storage). You read/write with
load/save on typed Item and Map wrappers.
Trident
There are no storage slots, no accounts, no key-value store. State is a Merkle tree commitment -- a single hash (the root) that represents the entire state. The prover knows the full tree; the verifier only sees the root.
To read state, the prover divines (secretly inputs) the leaf data and then authenticates it against the root using a Merkle proof. This is the divine-and-authenticate pattern (see Programming Model for the full treatment):
// Trident โ read an account from the state tree
use vm.io.io
use vm.crypto.hash
let state_root: Digest = vm.io.io.pub_read5() // verifier provides the root
let account_id: Field = vm.io.io.divine() // prover secretly inputs the data
let balance: Field = vm.io.io.divine()
let nonce: Field = vm.io.io.divine()
let auth_hash: Field = vm.io.io.divine()
let lock_until: Field = vm.io.io.divine()
// Hash the leaf and prove it belongs to the tree
let leaf: Digest = vm.crypto.hash.tip5(account_id, balance, nonce,
auth_hash, lock_until,
0, 0, 0, 0, 0)
// Merkle proof authenticates leaf against root
// (the sibling hashes are also divined and verified internally)
The verifier never sees account_id, balance, or any leaf data. It only
sees state_root and the proof that the program executed correctly.
Key insight: In Solidity, state is read from on-chain storage. In Trident, state is claimed by the prover and cryptographically verified.
Side-by-Side: Token Balance Lookup
// Solidity
function balanceOf(address who) view returns (uint256) {
return balances[who]; // storage read
}
// Trident
use vm.io.io
use vm.crypto.hash
fn load_account() -> (Field, Field, Field, Field, Field) {
let id: Field = vm.io.io.divine()
let bal: Field = vm.io.io.divine()
let nonce: Field = vm.io.io.divine()
let auth: Field = vm.io.io.divine()
let lock: Field = vm.io.io.divine()
// prove this data is in the state tree (Merkle proof)
let leaf: Digest = vm.crypto.hash.tip5(id, bal, nonce, auth, lock,
0, 0, 0, 0, 0)
(id, bal, nonce, auth, lock)
}
๐ Where's My msg.sender?
EVM / Solidity
msg.sender is implicit -- injected by the EVM based on the transaction
signature. ECDSA verification happens at the protocol level.
// Solidity
function withdraw() external {
require(msg.sender == owner, "not owner");
// ...
}
SVM / Anchor
The Signer account constraint checks that the transaction was signed by the
corresponding private key. The runtime enforces it before your program runs.
CosmWasm
info.sender is provided by the runtime in the MessageInfo struct.
Trident
There is no msg.sender. There is no implicit identity. Authorization is
explicit: the prover divines a secret and proves knowledge of it by hashing
it and asserting the hash matches an expected value.
// Trident โ authorization via hash preimage
use vm.io.io
use vm.crypto.hash
use vm.core.assert
fn verify_auth(auth_hash: Field) {
let secret: Field = vm.io.io.divine() // prover inputs secret
let computed: Digest = vm.crypto.hash.tip5(secret, 0, 0, 0, 0,
0, 0, 0, 0, 0)
let (h0, _, _, _, _) = computed
vm.core.assert.assert_eq(auth_hash, h0) // must match stored hash
}
The verifier never sees secret. It only knows the proof is valid, which means
someone who knew the preimage of auth_hash ran this program.
This is account abstraction by default. The "secret" can be anything:
- A private key
- A Shamir secret share (threshold multisig)
- A biometric hash
- The output of another ZK proof (recursive verification)
- A hardware security module attestation
There is no privileged key type. No secp256k1, no ed25519. Just hash preimages.
Side-by-Side: Access Control
// Solidity โ Ownable pattern
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setConfig(uint256 val) external onlyOwner {
config = val;
}
// Trident โ admin auth pattern
use vm.io.io
fn update() {
let old_config: Digest = vm.io.io.pub_read5()
let new_config: Digest = vm.io.io.pub_read5()
let admin_auth: Field = vm.io.io.divine() // divine the admin auth hash
// ... divine and verify full config ...
verify_auth(admin_auth) // prove knowledge of admin secret
// ... verify new config commitment ...
}
โฝ Where's My Gas?
EVM / Solidity
Gas is metered per opcode at runtime. You estimate it before sending. Unused gas is refunded. A function that might loop 1000 times costs 1000-iterations of gas even if you are just estimating.
SVM / Anchor
Compute units, similar model. Metered at runtime, capped per transaction.
CosmWasm / Substrate
Gas (CosmWasm) or weight (Substrate). Runtime metering with per-call limits.
Trident
There is no runtime metering. Proving cost is determined by six execution tables in Triton VM:
| Table | What It Measures |
|---|---|
| Processor | Clock cycles (instructions executed) |
| Hash | Hash coprocessor rows (6 per hash / tip5 call) |
| U32 | Range checks, bitwise operations (as_u32, split, &) |
| Op Stack | Operand stack underflow handling |
| RAM | Memory read/write operations |
| Jump Stack | Function call/return, branching overhead |
The tallest table determines the actual STARK proving cost. All tables are padded to the next power of 2. This means:
The power-of-2 cliff: If your tallest table has 1025 rows, it pads to 2048. If it had 1024 rows, it pads to 1024. That one extra instruction doubled your proving cost. This is the single most important cost concept in ZK programming.
Cost is known at compile time because all loops have bounded iteration counts and there is no dynamic dispatch. See How STARK Proofs Work Section 4 for why there are exactly six tables, and the Optimization Guide for cost reduction strategies.
# See the cost breakdown
trident build token.tri --costs
# See which functions are most expensive
trident build token.tri --hotspots
# See per-line cost annotations
trident build token.tri --annotate
# Save and compare costs across changes
trident build token.tri --save-costs before.json
# ... make changes ...
trident build token.tri --compare before.json
Side-by-Side: Cost Estimation
// Solidity โ estimate gas at runtime
uint256 gasStart = gasleft();
doWork();
uint256 gasUsed = gasStart - gasleft();
// You don't know until you run it
# Trident โ cost known before execution
$ trident build token.tri --costs
# Processor: 3,847 rows (padded: 4,096)
# Hash: 2,418 rows (padded: 4,096) <-- dominant
# U32: 312 rows (padded: 4,096)
# Op Stack: 891 rows (padded: 4,096)
# RAM: 604 rows (padded: 4,096)
# Jump Stack: 203 rows (padded: 4,096)
# Padded height: 4,096
๐ Where's My Revert?
EVM / Solidity
require(balance >= amount, "insufficient balance");
revert("something went wrong");
// try/catch for external calls
Revert unwinds state changes but consumes gas up to that point.
SVM / Anchor
require!() macro, or return Err(ErrorCode::...). State changes are rolled
back but compute units are consumed.
CosmWasm
Return Err(ContractError::...). Atomic rollback.
Trident
assert(balance >= amount)
If the assertion fails, the VM halts. No proof is generated. There is no partial execution, no state to roll back (because state was never mutated -- it was proven). There is no gas cost for failure (there is no gas).
No partial failure. Either the entire proof succeeds and every assertion holds, or nothing happens. There is no try/catch because there is nothing to catch -- a failed assertion means the computation is invalid and no proof exists.
// Trident โ range check pattern (balance >= amount)
use vm.core.convert
use vm.core.field
fn assert_non_negative(val: Field) {
let checked: U32 = vm.core.convert.as_u32(val) // fails if val > 2^32 or negative in field
}
let new_balance: Field = vm.core.field.sub(balance, amount)
assert_non_negative(new_balance) // no proof if balance < amount
The as_u32() conversion is how Trident checks that a field element is in a
safe range. If sub(balance, amount) wraps around in the prime field (because
amount > balance), the result is a huge number that fails the U32 range
check.
๐ก Where's My Event?
EVM / Solidity
event Transfer(address indexed from, address indexed to, uint256 amount);
function transfer(address to, uint256 amount) external {
// ...
emit Transfer(msg.sender, to, amount);
}
Events are logged on-chain. Anyone can read them. Indexers watch for them.
Trident
Two kinds of events:
reveal -- open events (like Solidity events). All fields visible to the verifier:
event Transfer {
from: Digest,
to: Digest,
amount: Field,
}
fn pay() {
// ...
reveal Transfer { from: sender, to: receiver, amount: value }
}
seal -- sealed events (no EVM equivalent). Fields are hashed; only the
digest is visible to the verifier. The verifier knows an event happened but
cannot read its contents:
event Nullifier {
account_id: Field,
nonce: Field,
}
fn pay() {
// ...
seal Nullifier { account_id: s_id, nonce: s_nonce }
}
Sealed events are uniquely ZK. They enable privacy-preserving audit trails: the verifier can confirm that a nullifier was emitted (preventing double-spend) without learning which account was involved.
๐ Pattern Translation Table
1. ERC-20 Transfer --> Token Pay Operation
// Solidity
function transfer(address to, uint256 amount) external returns (bool) {
require(balances[msg.sender] >= amount, "insufficient");
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
// Trident (simplified from coin/coin.tri)
use vm.io.io
use vm.core.field
use std.crypto.auth
fn pay() {
let old_root: Digest = vm.io.io.pub_read5()
let new_root: Digest = vm.io.io.pub_read5()
let amount: Field = vm.io.io.pub_read()
// Divine and verify sender account from Merkle tree
let s_bal: Field = vm.io.io.divine()
// ... authenticate against old_root ...
verify_auth(s_auth) // prove ownership
let new_s_bal: Field = vm.core.field.sub(s_bal, amount)
assert_non_negative(new_s_bal) // balance check
// Divine and verify receiver, compute new leaves
let new_r_bal: Field = r_bal + amount
// ... verify new leaves produce new_root ...
seal Nullifier { account_id: s_id, nonce: s_nonce }
reveal SupplyCheck { supply: supply }
}
2. Access Control (Ownable) --> Auth Hash Verification
// Solidity
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
// Trident
use vm.io.io
use vm.crypto.hash
use vm.core.assert
fn verify_auth(auth_hash: Field) {
let secret: Field = vm.io.io.divine()
let computed: Digest = vm.crypto.hash.tip5(secret, 0, 0, 0, 0,
0, 0, 0, 0, 0)
let (h0, _, _, _, _) = computed
vm.core.assert.assert_eq(auth_hash, h0)
}
3. Timelock --> lock_until Field Comparison
// Solidity
require(block.timestamp >= unlockTime, "locked");
// Trident
use vm.io.io
use vm.core.field
let current_time: Field = vm.io.io.pub_read() // verifier provides timestamp
let time_diff: Field = vm.core.field.sub(current_time, lock_until)
assert_non_negative(time_diff) // current_time >= lock_until
4. Mappings --> Merkle Tree Leaves
// Solidity
mapping(address => uint256) public balances;
balances[user] = 100;
uint256 bal = balances[user];
// Trident โ state is a Merkle tree, each "mapping entry" is a leaf
use vm.crypto.hash
let leaf: Digest = vm.crypto.hash.tip5(account_id, balance, nonce, auth, lock,
0, 0, 0, 0, 0)
// Leaf membership proven via Merkle proof against state root
5. Constructor --> Program Constants / Config Commitment
// Solidity
constructor(string memory name_, uint256 supply_) {
name = name_;
totalSupply = supply_;
}
// Trident โ config is a hash commitment, provided as public input
use vm.io.io
let config: Digest = vm.io.io.pub_read5()
// Divine and verify config fields
let admin_auth: Field = vm.io.io.divine()
let mint_auth: Field = vm.io.io.divine()
// ... hash all fields and assert match ...
6. View Functions --> pub_write Outputs
// Solidity
function balanceOf(address who) view returns (uint256) {
return balances[who];
}
// Trident โ prove a value and output it publicly
use vm.io.io
fn balance_proof() {
let root: Digest = vm.io.io.pub_read5()
let bal: Field = vm.io.io.divine()
// ... authenticate bal against root ...
vm.io.io.pub_write(bal) // verifier sees the balance
}
7. require / revert --> assert
// Solidity
require(amount > 0, "zero amount");
require(sender != receiver, "self-transfer");
// Trident
assert(amount > 0)
// Note: no error messages. Either proof exists or it does not.
8. block.timestamp --> pub_read (Public Input from Verifier)
// Solidity
uint256 ts = block.timestamp; // injected by EVM
// Trident
use vm.io.io
let current_time: Field = vm.io.io.pub_read() // verifier provides the timestamp
// The verifier is responsible for providing the correct value.
// The program can authenticate it against a kernel MAST hash
// if running inside Neptune's transaction model.
9. Upgradeable Proxy --> Config Update with Admin Auth
// Solidity (ERC-1967 proxy pattern)
function upgradeTo(address newImpl) external onlyOwner {
_setImplementation(newImpl);
}
// Trident โ config update operation (Op 2 in coin)
use vm.io.io
fn update() {
let old_config: Digest = vm.io.io.pub_read5()
let new_config: Digest = vm.io.io.pub_read5()
// Verify old config and authenticate admin
let old_admin: Field = vm.io.io.divine()
// ... verify old config hash ...
verify_auth(old_admin) // prove admin knowledge
// Verify new config is well-formed
// ... verify new config hash ...
// Setting admin_auth = 0 renounces forever (irreversible)
}
10. Token Mint / Burn --> Supply Accounting with Merkle Tree
// Solidity
function mint(address to, uint256 amount) external onlyMinter {
totalSupply += amount;
balances[to] += amount;
}
// Trident
use vm.io.io
use vm.core.assert
fn mint() {
let old_supply: Field = vm.io.io.pub_read()
let new_supply: Field = vm.io.io.pub_read()
let amount: Field = vm.io.io.pub_read()
verify_auth(cfg_mint_auth) // mint authority required
let expected: Field = old_supply + amount
vm.core.assert.assert_eq(new_supply, expected) // supply accounting
// Update recipient leaf in Merkle tree
let new_r_bal: Field = r_bal + amount
// ... verify new Merkle root ...
reveal SupplyChange { old_supply: old_supply, new_supply: new_supply }
}
๐งฌ What's New (No EVM Equivalent)
These concepts have no direct parallel in smart contract development:
divine() -- Secret Witness Input
The prover inputs data the verifier never sees, then authenticates it (via hashing, Merkle proofs, or range checks). In EVM all calldata is public; in Trident, divine is the default input method. See For Offchain Devs for the full divine-and-authenticate pattern.
seal -- Privacy-Preserving Events
Emit an event where the verifier can confirm it happened but cannot read its contents. Used for nullifiers, private audit trails, and compliance proofs.
seal Nullifier { account_id: s_id, nonce: s_nonce }
// Verifier sees: hash(account_id, nonce) -- not the actual values
Bounded Loops
All iteration must have a compile-time upper bound (for i in 0..n bounded 100). No unbounded recursion, no dynamic dispatch. This is what makes compile-time cost analysis possible. See For Offchain Devs for loop syntax and costing rules.
Cost Annotations
Every Trident function has a deterministic proving cost. The compiler gives you complete visibility:
trident build main.tri --costs # full table breakdown
trident build main.tri --hotspots # top 5 most expensive functions
trident build main.tri --annotate # per-line cost annotations
trident build main.tri --hints # optimization suggestions
No EVM toolchain gives you this level of cost certainty. In Solidity, gas depends on storage state, calldata, and runtime conditions. In Trident, cost is a pure function of the source code.
Recursive Proof Verification
A Trident program can verify another STARK proof inside its own execution, enabling proof composition. See For Offchain Devs for how recursive verification works and its cost profile.
Quantum Safety
All cryptographic security comes from hash functions (Tip5) and FRI commitments -- no elliptic curves. Proofs are quantum-resistant without migration. See How STARK Proofs Work Section 10 for the full argument.
๐ Quick Start for Solidity Devs
1. Install Trident
git clone https://github.com/nicktriton/trident
cd trident
cargo build --release
# Add target/release/trident to your PATH
2. Create a Project and Read the Hello World
trident init my_first_zk
cd my_first_zk
cat main.tri
The default main.tri reads two public inputs, adds them, and writes the
result. Build it:
trident build main.tri -o hello.tasm
trident build main.tri --costs
3. Read the Coin Example
The os/neptune/ directory contains a complete ZK-native coin
with pay, lock, update, mint, and burn operations. See
os/neptune/standards/coin.tri for the implementation and
TSP-1 โ Coin for the design.
4. Build and Check Costs
trident build os/neptune/standards/coin.tri --costs
trident build os/neptune/standards/coin.tri --hotspots
5. Full Walkthrough
Read the Tutorial for a complete step-by-step guide covering types, functions, modules, I/O, hashing, events, testing, and cost analysis.
๐ See Also
- Tutorial -- Step-by-step Trident developer guide
- Language Reference -- Quick lookup: types, operators, builtins, grammar
- Programming Model -- How programs run in Triton VM
- Optimization Guide -- Cost reduction strategies
- Gold Standard Libraries -- Token standards (TSP-1/TSP-2) and capability library
- How STARK Proofs Work -- From execution traces to quantum-safe proofs
- Multi-Target Compilation -- Multi-target architecture: universal core, backend extensions
- For Offchain Devs -- Zero-knowledge from scratch (if you also need the ZK primer)