๐ŸŒ‰ 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

Local Graph