How Pyde Works
A guided tour for the impatient. Read this first to build intuition; then the technical chapters will land.
What's in the name
Pyde (pronounced pied, rhymes with tide). The name carries two senses at once, and both are intentional.
The older sense is tide. A tide is an inescapable, continuous current — it does not ask permission, it does not stop for the night, it does not wait for any single drop to arrive before moving the next. Pyde the network was designed to feel like that. The throughput of a blockchain is rarely about how fast one transaction can land; it is about whether the assembly line ever empties. Pyde's assembly line does not empty. The protocol commits in waves — not poetic waves, literal ones, the way water commits to shore — and the rest state of the system is motion. The factory metaphor that runs through this book is the tide made mechanical.
The surface sense is pied — a casual, phonetic spelling. The name was picked to sit quietly: short, easy to say, easy to type in a hurry, distinctive enough to search for. pyde.network, @pydenet, t.me/pydenet — the rhythm matters when you will type it ten thousand times. It was picked knowing it would mostly be written lowercase, in DMs, by people whose hands are tired.
There is no third sense. No hidden Greek letter, no acronym backing it out, no "Programmable Yield Decentralization Engine" trying to sneak in through the back door. The name is just the name.
The mark
The mark is based on atomic structure — a nucleus and its orbital.
The vertical form is the core. Dense, gravitational, everything pulls toward it. Pyde's architecture is monolithic: consensus and execution unified in one gravitational center.
The circle to its right is in orbit. Independent, in motion, but bound to the core by an invisible force. External chains, bridges, and light clients orbit Pyde freely — verified, not trusted.
The two are separate on purpose. Related but sovereign. The same way a Pyde finality certificate can prove itself anywhere without depending on the chain it came from.
The core is wide at the poles and compressed at the center. Finality under pressure. Stress-tested and held.
No sharp edges. No network imagery. Nothing decorative. The mark looks like a physical law, not a trend.
Grayscale only. Works as a favicon, on a sticker, in metal, as a watermark. Full brand rules live in the Brand Reference.
The mark is the architecture
The atomic reading is not visual flavour. It is the design.
Pyde is the core. Consensus and execution live in one system. State lives where transactions are ordered. The DAG, the JMT, the wasmtime executor — one process, one gravitational well. Most modern chains split these into layers. Pyde does not.
Verification is the binding force. Nothing orbiting the core is trusted. Light clients, bridges, foreign chains, wallets running local previews — they all bind through cryptographic proof. FALCON-signed finality certificates, JMT inclusion proofs, threshold decryption shares. The orbits are mathematical, not political.
Things orbit without merging. A Pyde finality certificate can travel to Ethereum and prove itself there without phoning home. A parachain has its own sub-orbit inside Pyde's well — its own validators, its own state subtree — and stays sovereign. Sovereignty without isolation.
Compression is BFT under pressure. Wave commits run under adversarial conditions. The 85-of-128 quorum, the slashing schedule, the structural MEV resistance — they exist so the core holds when squeezed. Stress-tested.
One core. Many orbits. Bound by physics, not by trust.
Pyde is a factory
Most blockchain explanations start with cryptography and end with consensus, leaving the reader holding a bag of acronyms. We are going to do this differently.
Pyde is a factory. Goods (transactions) arrive at the loading dock from outside. They are sorted, lifted onto a continuously-moving assembly line, and arranged by a series of robotic arms working in parallel. Every few hundred milliseconds, a controlled detonation locks in a batch as final — the bang you feel when the factory floor shakes is a wave commit. After the bang, the audit ledger is stamped, smoke rises from the chimney (eviction, pruning), a receipt is sent out the front door, and the line keeps moving without ever stopping.
The continuous rotation is the throughput. Pyde is not a fast database; it is a deep pipeline.
The Pyde cycle, ~2-3 times a second on commodity hardware. Each pass is one wave commit.
The eleven stages
The full cycle, end-to-end, from a user's keypress to a receipt landing back in their wallet, is eleven stages. Five are happening to your transaction. The other six are happening to other people's transactions concurrently, on the same factory floor, because the line never stops.
Stage 0 — Workshop floor (the user)
A user opens a wallet and asks it to send 100 PYDE to alice.pyde. The wallet quietly does five things before showing a "Sign" button: it resolves the recipient name via JSON-RPC, fetches the sender's account state, fetches any relevant contract bytecode, runs the transaction locally inside a wasmtime sandbox embedded in the wallet itself (Tier 1 client-side preview, see Chapter 17 §17.4b), and shows the user a preview: "This tx will send 100 PYDE, cost ~21,000 gas, leave your balance at 900 PYDE." Only then does the user sign with their FALCON-512 key, and only then does the tx leave their machine.
If the user opted for confidentiality, the wallet also encrypts the payload with the current epoch's threshold pubkey before signing — the recipient and amount become opaque ciphertext that no validator can read until the wave commits.
Stage 1 — Loading dock (RPC ingress)
The transaction lands at any RPC node. RPC nodes are stateless ingress: they hold no validator key, sign nothing, and have no consensus role. They parse the JSON, do a shape check, rate-limit, return the tx hash to the wallet synchronously, and then shovel the transaction into the libp2p Gossipsub mempool topic. From the wallet's perspective the trip is done. In reality it has just begun.
Stage 2 — Sorting room (mempool)
Every node — and especially every committee validator — runs a validation pipeline on each incoming tx: signature verify (FALCON-512, batchable), nonce window check (the tx's nonce must be within sixteen of the sender's last committed nonce), balance sufficiency, gas-limit cap, attribute coherence. Passes go into the local mempool DashMap, organised by gas-price descending. Failures are dropped and the gossip score of the peer that sent it is docked. Encrypted transactions land here too: the envelope is validated, but the payload stays sealed until threshold decryption fires at commit.
Stage 3 — Assembly-line dispatch (batches and vertices)
Inside each of the 128 committee members for this epoch, two things happen continuously. First, every hundred milliseconds or so, the member packs the highest-fee transactions into a Batch (~50-200 txs, ~4 MB cap) and broadcasts it on the /pyde/batches/1.0.0 topic. Second, every round (~150-500 ms, structurally paced — see below), the member emits a Vertex that references ≥85 parent vertices from the previous round, references whichever batches it wants to include, carries piggybacked decryption shares for any encrypted transactions in the subdag, contributes a VRF beacon share, attests to the previous anchor, and is signed by the member's epoch key. Vertices broadcast on /pyde/dag/1.0.0 and form the next floor of the DAG.
The round advances when the member has seen ≥85 vertices from the current round, not when its own timer fires. This is the structural-pacing trick that makes Mysticeti elegant: the floor speed is the median peer speed, not the slowest peer's speed. A single laggard cannot stall the line.
Stage 4 — The foreman picks the lead (anchor selection)
Every K rounds (typically K=3), an anchor is picked, deterministically and verifiably, by all 128 members simultaneously:
anchor_validator_id = VRF(beacon_combined, round, prev_state_root) mod 128
The beacon is the XOR of the prior round's VRF shares (public randomness). The previous state root locks anchor selection to canonical history, so an adversary who reorders the DAG cannot retroactively choose a more favourable anchor. Mod 128 picks which member's vertex at this round wears the crown. Every honest member computes the same answer.
Stage 5 — Big bang (wave commit) 💥
Once the anchor has accumulated ≥85 attestations from later-round vertices (other members' vertices that reach the anchor transitively through parent links), the commit threshold trips. The bang fires.
What the bang does, in three lines:
- BFS subdag walk — starting at the anchor, walk every parent reference recursively. The set of touched vertices is the subdag being committed.
- Canonical sort — order the subdag by (round, author_id, batch_list_order). Every honest member produces the same order.
- Dedupe + flatten — same transaction may appear in multiple batches across multiple members; keep the first appearance. The result is the wave's
ordered_list, a fully deterministic transaction sequence.
That sequence is what gets executed. Before the bang the DAG is ambiguous; after the bang it is fixed. See Chapter 6 §5b–5c for round-vs-wave terminology, missing-vertex handling, and the 5-skip recovery walkthrough.
Stage 6 — Unboxing the sealed crates (threshold decryption)
Encrypted transactions in the ordered list were opaque until now. Each was sealed with a one-time symmetric key encrypted under the epoch's threshold pubkey. Every vertex committed in the wave piggybacked a decryption share for each encrypted tx in the committable subdag. By commit time, ≥85 shares per encrypted tx are already in hand.
For each encrypted transaction: Lagrange interpolation across the shares recovers the decryption key, the payload is decrypted in-memory, and the now-revealed transaction is re-validated (nonce, balance) one final time before execution. If the decrypted transaction is invalid, it is dropped — but the sender still pays a small gas bond from their plaintext balance (anti-spam). The order-then-decrypt design is what gives Pyde its MEV protection: validators cannot front-run, sandwich, or censor based on transaction content, because they could not read it when they ordered it.
Stage 7 — Robotic arms picking and ordering (execution)
The wave's ordered_list enters the Block-STM hybrid scheduler. Transactions with declared access lists go into the certain-parallel set; the scheduler builds a conflict graph and partitions them into independent groups that execute fully in parallel. Transactions without access lists fall into the speculative set; multiple cores run them optimistically, recording every state slot they touch; conflicts trigger deterministic re-execution. Both sets write through a per-wave overlay; at wave commit, the overlay merges in ordered_list order. Parallelism is free as long as the final commit respects the canonical order.
For each transaction, the dispatch looks at the type. Native transactions (Transfer, ValidatorRegister, Stake, Unstake) skip wasmtime entirely — direct calls into native handlers, ~21K gas, no WASM cost. Contract calls and contract deploys enter the wasmtime path: load (or fetch and Cranelift-compile) the contract module from state, instantiate it with a 64 MB linear-memory cap and gas_limit of fuel, invoke the entrypoint, run host functions (sload, sstore, sdelete, log, cross_call) through a per-transaction overlay that snapshots reads and isolates writes. Success merges the overlay into the wave overlay; trap discards it; either way the gas actually consumed is deducted (no refunds in v1, see Chapter 10 §10.1). Cross-contract calls nest overlays recursively so a failed sub-call rolls back cleanly without touching the caller's state.
Stage 8 — Inventory audit (state root computation)
After execution, the wave overlay holds every write and every emitted event. Now the audit stamp goes on. Each (slot_hash, value) write lands in two places: the state_cf flat table (live state, O(1) reads later) and the jmt_cf versioned tree (proofs and state root). JMT internal nodes touched by this wave are recomputed with dual hashes — Blake3 for fast native verification, Poseidon2 for future ZK light clients (see Chapter 4 §4.1b). Events land in three more column families — events_cf (primary, ordered by wave) plus events_by_topic_cf and events_by_contract_cf (indexes for fast filtering) — and the wave commit record carries an events_root (Blake3 Merkle tree over canonical-ordered events) plus a 256-byte events_bloom so light clients can verify event inclusion identically to how they verify state. The new state root, the events root + bloom, the wave commit record, the receipts, and the tx-to-wave mapping all land in a single atomic RocksDB WriteBatch. Either the entire wave commits or none of it does. There is no such thing as a half-committed wave.
Stage 9 — Smoke from the chimney (eviction and pruning) 💨
The DashMap write-back cache layer holds writes from recent waves in memory; reads against hot accounts are near-free here. On every wave boundary, the cache is flushed and LRU eviction trims it back under its size cap. Hot accounts (token contracts, popular pools) stay resident; cold accounts get evicted and next access pays one disk read against state_cf. Pruning policy varies by node tier: archive nodes keep everything; full nodes drop state-tree versions older than ninety days; committee validators keep thirty days. The mempool drops every transaction that just committed and every transaction whose nonce window has now closed.
The smoke rising from the chimney is the eviction. The exhaust is the pruning. The factory shrinks back to a clean working volume ready for the next round.
Stage 10 — Receipt out the front door (back to the user)
The wallet has been holding a WebSocket subscription on the transaction hash since Stage 1. The moment Stage 8's WriteBatch lands, the RPC layer pushes:
{
"tx_hash": "0x...",
"status": "success",
"wave_id": 1234567,
"gas_used": 21000,
"events": [{ "topic": "Transfer", "to": "0xabc...", "amount": "100" }],
"state_root": "0x..."
}
The wallet updates the user's view: "Transferred 100 PYDE to alice.pyde. Confirmed." For light clients (mobile wallets, browser dApps), the same wave commits as a 200-byte header signed by the committee threshold — the light client verifies the threshold signature against the committee pubkeys it already trusts and has now verified the entire wave's integrity without downloading a single transaction. See Chapter 17 §17.3 for the SDK surface and Companion: State Sync for the light-client model.
Stage 11 — The eternal rotation 🔁
Everything you have just read is happening in parallel for different waves. While Stage 7's arms execute wave 1,234,567, round R+1 has already advanced, decryption shares for round R+5's encrypted transactions are piggybacking through the gossip layer, the next anchor is already known by VRF, the mempool is already sorting transactions that will land in wave 1,234,568, and somebody's wallet on the other side of the world is running a Tier-1 preview for a transaction that does not yet exist. The pipeline is deep. The conveyor belts overlap. The big bang fires roughly twice a second.
The continuous rotation is the throughput. No single transaction is faster than on a slower chain — but the assembly line never empties.
What the metaphor catches that the spec sometimes loses
- Pipelining is everything. Stages 1–11 run concurrently for different waves. No stage waits for another stage to finish.
- The bang is real. Wave commit is a discrete moment that locks order. Before the bang the DAG is ambiguous; after the bang it is canonical.
- Smoke is not waste — it is necessary. Eviction and pruning are first-class. Without them the factory chokes on its own inventory.
- The user only sees the loading dock and the receipt window. Everything in between is hidden machinery. The wallet's job is to make the bang feel like an instant click.
Where to read next
If you want the detailed mechanics of any stage:
- Stages 1–2 (ingress, mempool): Chapter 12 — Networking
- Stages 3–5 (DAG, anchor, commit): Chapter 6 — Consensus
- Stage 6 (threshold decryption): Chapter 6 §11 and Chapter 9 — MEV Protection
- Stage 7 (execution, Block-STM, per-tx overlay): Chapter 3 — Execution Layer
- Stage 8 (state model, JMT, dual hash): Chapter 4 — State Model
- Stage 9 (eviction, pruning): Chapter 4 §4.1b and Companion: State Sync
- Stage 10 (wallets, SDKs, RPC): Chapter 17 — Developer Tools
And if you want the deep historical narrative on how Pyde arrived at this design: The Pivot.
Get Started
Pick the path that fits you.
I want to build on Pyde →
You're a developer or technical founder. You want to write a contract, spin up a local devnet, deploy something, integrate with the chain. Start here for the toolchain, the host-function ABI, language-specific examples, and the local-devnet flow.
I want to use Pyde →
You're an end user. You want to hold PYDE, send transactions, run a node, or follow the project's mainnet path. Start here for the wallet story, post-quantum guarantees in plain English, what makes Pyde different from other L1s, and what's available pre-mainnet vs post-mainnet.
Not sure which path? If you're going to type cargo build at any
point, take the developer track. If you only ever interact with Pyde
through a wallet or a dApp, the user track is where you belong.
Both paths converge at the same set of canonical specs in the book — chapters 1–20 and the companion files — when you need to go deep on a specific topic.
Get Started — for Developers
You're here because you want to build something on Pyde. This page is the on-ramp: enough orientation to land you on the right specs, without reproducing them.
What you can build
Pyde supports two contract surfaces:
- Smart contracts — sandboxed WASM modules deployed to the chain. Standard L1 contract development; read Chapter 3 — Execution Layer for the runtime model.
- Parachains — permissionless side-runtimes that share Pyde's finality and validator set, with their own state subtree and an extended ABI for cross-chain messaging + threshold cryptography. Read Chapter 13 — Parachains.
Both compile to WebAssembly. Pyde executes them via wasmtime + Cranelift AOT — deterministic feature subset, per-tx overlay isolation, fuel-metered gas.
What language?
Whatever targets wasm32. Pyde doesn't ship per-language SDKs;
authors compile their .wasm themselves and use the otigen
toolchain to package + deploy it. First-class examples ship for:
- Rust —
cargo build --target wasm32-unknown-unknown --release - AssemblyScript —
npx asc contract.ts -o contract.wasm - Go (TinyGo) —
tinygo build -target wasm-unknown -o contract.wasm - C / C++ —
clang --target=wasm32 -nostdlib -Wl,--no-entry
The chain only sees the bytes. Pick what fits your team.
The five things to read
In order:
- Chapter 1 — Introduction — 10-minute orientation. Why Pyde exists, what it's not.
- Chapter 3 — Execution Layer — the runtime, the per-tx overlay, the determinism contract.
- Host Function ABI v1.0 —
every
pyde::*function your WASM can import. Signatures, semantics, gas costs, error codes. This is the contract the chain stands on. - Chapter 5 — Otigen Toolchain
— how
otigenbuilds, deploys, manages wallets, runs a local console. - Otigen Binary Spec v1.0 — the CLI surface. Every command, every flag.
Bookmark these. The rest of the book (state model, gas, accounts, consensus, networking, parachains, slashing, governance) you read on demand.
The minimum loop (once mainnet ships)
# 1. Scaffold a project
otigen init my-token --lang rust
# 2. Edit src/lib.rs + otigen.toml
# 3. Build (you run cargo; otigen post-processes)
cargo build --target wasm32-unknown-unknown --release
otigen build
# 4. Deploy to devnet / testnet / mainnet
otigen deploy --network devnet
This loop is detailed in
OTIGEN_BINARY_SPEC §3.2.
Pre-mainnet status (today)
Pyde is pre-mainnet. What's already shippable:
- The protocol spec (everything in this book).
- The post-quantum cryptography crate: pyde-crypto.
- The engine workspace's interface layer (MC-0 —
phase-0-foundationtag onpyde-net/engine). - The marketing site you arrived from.
What's in active build-out:
- The engine (execution + consensus + node binary). MC-1 in flight across two parallel streams — see Implementation Plan §3.2.
- The otigen toolchain. MC-1 Stream α — see
pyde-net/otigen.
What you can do right now:
- Read the spec, file issues, propose PIPs.
- Watch the repos.
- Track the roadmap.
Where to ask
- GitHub Discussions — design questions, spec ambiguities.
- Telegram — quick chat, anything that doesn't need a paper trail.
- PIPs — propose a protocol change.
Welcome aboard.
Get Started — for Users
You're not here to write contracts. You want to use Pyde — hold PYDE, send a transaction, run a node, or follow the project's path to mainnet. This page is your map.
What's different about Pyde
Three things, in plain language:
1. It survives the quantum era
Every signature on Pyde uses FALCON-512, a NIST-standardised post-quantum signature scheme. Every encryption uses Kyber-768 (ML-KEM), NIST's post-quantum key-encapsulation scheme.
Translation: when a quantum computer powerful enough to break Bitcoin
- Ethereum signatures shows up, Pyde keeps working. There is no migration window because there's no ECDSA legacy to migrate away from.
Read more: Chapter 8 — Cryptography.
2. Front-running is structurally impossible
On most chains, the order of transactions inside a block is decided by whoever proposes the block — and that ordering is profitable. MEV bots pay validators to insert their trade in front of yours, drain your slippage, and move on.
Pyde encrypts transactions in the mempool with a key only the committee collectively holds. The committee commits to an order before any decryption share is released. By the time anyone can read what's inside a transaction, the ordering is already final. There is no profitable front-run because there's no information to front-run on.
Read more: Chapter 9 — MEV Protection.
3. Your account doesn't die when one key leaks
Native multisig is a protocol feature, not a contract every wallet re-implements. Lose a key, the rest of the keys still control the account. Coming post-mainnet: programmable accounts with spend limits, time locks, social recovery, and per-app session keys that can be revoked at any time.
Read more: Chapter 11 — Account Model.
Honest status (today)
Pyde is pre-mainnet. That means:
| What | When |
|---|---|
| Read the spec | ✅ Now (this book) |
| Open a wallet / acquire PYDE | ❌ Mainnet |
| Send a transaction | ❌ Mainnet (testnet earlier) |
| Run a validator | ❌ Mainnet |
| Run a full node | ❌ Mainnet (devnet earlier) |
| Follow the project | ✅ Now |
The roadmap below tracks the path from "pre-mainnet engineering" to "mainnet live".
What you can do right now
- Read the whitepaper. 30 minutes; covers everything at a digestible depth.
- Follow the roadmap. Five phases (MC-0 → MC-5). MC-0 shipped; MC-1 is in flight. No calendar dates — each phase ships when its bar is met.
- Join Telegram for project chat.
- Follow @pydenet on X for milestone announcements.
- Watch the GitHub org if you want to see the work as it lands.
When mainnet ships
You'll do the things you'd do on any L1, with two structural differences:
- Your address is 32 bytes (
0x+ 64 hex chars). Pyde doesn't truncate addresses the way Ethereum does. You'll see this in any Pyde-native wallet. - Your account survives single-key compromise if you set up native multisig at registration. The wallet UX will surface this as the default for non-trivial balances.
Gas works like Ethereum's EIP-1559 (no priority fees on Pyde — inclusion order isn't biddable), and the chain commits a wave every ~500 ms. Transactions land fast and final.
Where to follow along
- Roadmap — phase-by-phase tracking.
- GitHub org — every repo, every commit.
- Telegram — community chat.
- X (@pydenet) — milestone announcements.
info@pyde.network— formal contact.
Welcome to the pre-mainnet phase. It's the most honest place to be.
Introduction
What is Pyde?
Pyde is a Layer 1 blockchain built greenfield to deliver four properties no chain in production combines today:
- Post-quantum cryptography by default — FALCON-512 signatures, Kyber-768 threshold encryption, Poseidon2 hashing
- MEV resistance by structure — threshold-encrypted mempool + commit-before-reveal ordering + DAG consensus eliminates proposer extraction
- Sub-second finality — Mysticeti-style DAG consensus, ~500ms median finality
- Commodity decentralization — modest hardware for validators not currently on the active committee; equal voting power within the active committee
The execution layer is WebAssembly via wasmtime, with Cranelift ahead-of-time compilation and a hybrid parallel scheduler combining Solana-style declared access lists with Aptos-style Block-STM speculation. Smart contracts can be authored in Rust, AssemblyScript, Go (TinyGo), or C/C++ — whatever language the team already uses — and bundled by the otigen developer toolchain.
Cross-chain interactions — calling functions on other chains, querying oracles, off-chain compute — happen through a permissionless parachain layer (post-mainnet) with operators who stake PYDE and earn gas fees from contracts that call them. No custodial multisigs, no auctioned slots.
The Pivots
Pyde has gone through two clean pivots that materially changed the architecture. Both are documented honestly in the preface (The Pivot) and supported by full historical design records in pivot/.
- Consensus pivot — from an in-house HotStuff variant (whose 400ms tail-latency wedges proved structural rather than tunable) to Mysticeti-style DAG consensus. The HotStuff-era consensus crates are archived; the Mysticeti-based rebuild is in progress.
- Execution pivot — from a custom virtual machine (
pyde-vm), a custom AOT compiler (pyde-aot), and a custom language (Otigen) to WebAssembly via wasmtime. The Otigen name lives on as the developer toolchain (otigen). The original Otigen Book is preserved as a historical artifact.
This book reflects the post-pivot architecture. The work that preceded each pivot is preserved both in code (archive/) and in design documentation (pivot/).
Why a New Layer 1?
The Quantum Problem
Every major Layer 1 in production today — Bitcoin, Ethereum, Solana, Cardano, Polkadot — uses classical cryptography (secp256k1, Ed25519, BLS12-381) vulnerable to Shor's algorithm. NIST's 2024 standardization of FALCON, ML-DSA, and ML-KEM unblocked post-quantum primitives, but retrofitting them into a live chain is a multi-year coordinated migration. Pyde ships PQ at genesis without retrofitting.
The MEV Problem
Maximum Extractable Value has hardened into a multi-billion-dollar tax paid by retail users to validator-builder coalitions. Sandwich attacks, front-running, and proposer extraction are not bugs — they are structural consequences of public mempools and single-proposer block production. Pyde eliminates the structural conditions via threshold encryption + commit-before-reveal + DAG consensus (no single proposer to exploit).
The Decentralization Problem
Chains optimizing for throughput have ended up requiring datacenter-class validator hardware. Chains optimizing for decentralization have ended up with throughput unusable for serious applications. Pyde scales hardware requirements by role — commodity for validators awaiting committee selection, modest professional for validators on the active committee at production targets, datacenter only for aspirational TPS levels.
What's New (Post-Pivot)
- Mysticeti DAG consensus replaces HotStuff. No view changes, no single proposer, sub-second commit latency targeted (implementation in progress)
- WebAssembly execution via wasmtime, with Cranelift AOT. Smart contracts written in Rust, AssemblyScript, Go, or C/C++ — same language ecosystem authors already work in
- Worker / Primary split (Narwhal pattern) for data dissemination separate from consensus
- Hybrid execution scheduler — static access lists for known patterns, Block-STM for dynamic
- JMT state tree (Jellyfish Merkle Tree, radix-16) replaces fixed-depth SMT — with dual Blake3 + Poseidon2 roots so standard light clients and future ZK light clients verify against the same tree
- PIP-2 clustered slot keys + PIP-3 prefetch + PIP-4 write-back cache — three-layer state performance stack
- Encryption opt-in per-tx — MEV protection where needed, no overhead where not
otigendeveloper toolchain — zero-extra-code authoring: write contract logic +otigen.toml, the tool handles everything else- Honest performance targets — 10-30K TPS realistic v1, validated by multi-region performance harness
- Phased mainnet plan — external audit + incentivized testnet before launch (see Roadmap)
Honest Status
This book describes designed architecture, with implementation in various stages:
| Component | Status |
|---|---|
| Architecture design | Complete |
| WASM execution layer (wasmtime + Cranelift) | Design locked 2026-05-21; wasmtime integration next |
| State layer (JMT, dual-hash, PIP-2 clustering) | Single-hash JMT in place; PIP-2/3/4 + dual-hash in progress |
| Mysticeti DAG consensus | Rebuild in progress post-pivot |
Post-quantum cryptography (pyde-crypto) | Functional; threshold-decryption path is research-grade |
| Network protocol (libp2p + QUIC + Gossipsub) | In place; layered peer discovery (no DHT) in flight |
otigen developer toolchain (WASM-era) | Specification complete; scaffold in progress |
| Parachain framework | Designed; implementation deferred to a later phase |
| Performance harness | Not yet built (mandatory before any TPS claim) |
Mainnet ships when the implementation is complete, audited, and validated by an incentivized testnet — no public schedule. See the Roadmap for the sequenced plan.
Performance Targets
Validated by multi-region production-realistic harness (mandatory before any external claim):
| Mode | v1 realistic | v2 stretch | Aspirational |
|---|---|---|---|
| Plaintext TPS (commodity) | 10K-30K | 50K-100K | 500K |
| Encrypted TPS (commodity) | 0.5K-2K | 5K-10K | 50K+ |
| Median finality | ~500ms | ~400ms | ~300ms |
The HotStuff Lesson: the pre-pivot implementation hit ~4K TPS in practice despite higher claimed targets. Pyde now adopts the "claim 1/3 of measured peak" rule — under-promise, over-deliver. No external TPS claim without harness evidence.
Reading Path
This book is the comprehensive technical reference. Different paths for different audiences:
For a researcher / cryptographer:
- Chapter 2: Architecture Overview
- Chapter 6: Consensus (Mysticeti DAG)
- Chapter 8: Cryptography
- Chapter 9: MEV Protection
- Companion: Whitepaper
For an implementer / contributor:
- Chapter 2: Architecture Overview
- Chapter 3: Execution Layer (WASM)
- Chapter 4: State Model
- Chapter 5: Otigen Toolchain
- Chapter 11: Account Model
- Chapter 12: Networking
- Companion: Architecture (Design Doc)
- Preface: The Pivot for context on architectural choices
For a validator operator:
- Chapter 6: Consensus
- Chapter 7: State Sync & Chain Halt
- Chapter 16: Security & Threat Model
- Companions: Validator Lifecycle, Slashing, Chain Halt & Recovery
For an investor / decision-maker:
- This Introduction
- Chapter 14: Tokenomics
- Chapter 19: Launch Strategy
- Companion: Pitch Deck, Tokenomics Detail
For someone doing security review / audit:
- Chapter 16: Security & Threat Model
- Chapter 6: Consensus (safety arguments)
- Chapter 8: Cryptography
- Companions: Threat Model, Failure Scenarios, Network Protocol, Performance Harness
License
Pyde is licensed under Apache-2.0. The full text lives in LICENSE at the root of each Pyde repository. The book content is licensed under CC BY-SA 4.0.
Status
Living document. Updated as the design evolves.
The Pivot
A note on how this book came to describe what it describes, and what changed along the way.
Starting from a question
Pyde began with a simple question that turned out to be much harder than it looked:
Can we build a post-quantum L1 that is actually fast?
Not "fast in the abstract." Not "fast in a research paper." Fast enough that real users would not notice it was post-quantum at all. Fast enough that the security upgrade was free at the point of use.
That question is what this book is about. Everything else — the consensus choice, the execution model, the state layer, the crypto primitives — is downstream of trying to answer it honestly.
This preface is the story of the answers we tried, the answers we kept, and the answers we threw away.
The first instinct: a small, sandboxed VM
The earliest sketches of Pyde leaned on something close to a BPF-style virtual machine. Solana had shown that a tight, sandboxed, register-based VM could run blockchain workloads at speeds that older designs (the EVM in particular) had no path to. The appeal was obvious: instead of inheriting a heavy stack-based VM with decades of cruft, start lean.
The thinking was: a small instruction set, a tight verifier, a fast interpreter or AOT, and crypto-friendly opcodes. Let the rest of the system inherit that lightness.
What we did not appreciate at the time — and what took building to learn — was that the VM is rarely the bottleneck on a blockchain. The bottleneck is consensus tail latency, signature verification, network bandwidth, and disk I/O, in roughly that order. The VM is the part you write last and that matters least. We would learn this the hard way, more than once.
But the BPF idea seeded something useful. It taught us to think in terms of sandboxing as a first-class property, not an afterthought. It taught us that "your own VM" is a commitment to building, maintaining, and securing an entire compiler toolchain — not a one-shot decision. Those lessons stuck. The specific implementation did not.
The HotStuff phase and a 400-millisecond wedge
For the full design record of this era, see hotstuff-consensus-era.
For consensus, we tried HotStuff first. It is the orthodoxy of modern BFT — used by Diem (the version that did not ship), Aptos, several other production chains. The literature is clean. The proofs are tight. The reference implementations are credible.
We picked it up and started integrating it.
For a while, things looked promising. Throughput was reasonable. The committee structure made sense. The pipeline of view changes felt mostly orderly. We started building around it: the mempool, the block production, the early state machine.
And then we ran into a wedge.
Under load, in adversarial conditions — partial network partitions, slow validators, particular orderings of messages — HotStuff's tail latency would balloon. We saw commits taking 400 milliseconds where the median was under 100. That tail was not a curiosity. It meant a real chain running under real conditions would routinely freeze for fractions of a second, and that was unacceptable for the kind of UX we wanted Pyde to enable.
We spent weeks trying to engineer around it. Tuning timeouts. Re-ordering message handlers. Experimenting with leader rotation strategies. Adjusting the view-change protocol. Some of these helped at the margin. None of them got the tail under control.
Eventually the honest read became: HotStuff is not the right base for what we are trying to build. The tail latency is not a tuning problem; it is a structural property of how leader-based BFT handles adversarial conditions. We could keep grinding on it for another year and not get there.
That was the first hard pivot.
We turned to the DAG family — Mysticeti, Narwhal, Blueshark. The DAG approach decouples data availability from ordering, removes the single-leader bottleneck per round, and gives the kind of tail latency profile we needed. Mysticeti specifically had the freshest design and the best throughput numbers in the literature.
We adopted it as the consensus design direction. The implementation is currently in progress — the HotStuff-era consensus crates are archived, and the new Mysticeti-based consensus layer is being built design-first against the post-pivot architecture. The architecture chapters that follow describe Mysticeti as the design Pyde is being built around, not as code that has already shipped.
The HotStuff work was not wasted. Building it taught us what a BFT pipeline really looks like under load. The instinct that "the latency tail is what kills UX" carried forward. But the code itself got archived. We learned, and we moved.
A smaller pivot worth recording: SMT to JMT
Around the same time we were working on the consensus layer, we were also evaluating the state-commitment structure. The clean theoretical answer was a Sparse Merkle Tree — a fixed-depth-256 tree, one of the most studied constructions for accountable state. Beautiful on paper.
Expensive in practice. Every state read or write touches roughly 256 nodes because of the fixed depth. At realistic TPS, that overhead dominates the disk IO budget. The math did not close.
We switched to the Jellyfish Merkle Tree (JMT) — radix-16, path-compressed, production-validated by Diem and Aptos. Same authentication properties (Merkle commitment, inclusion and exclusion proofs), but roughly 5-10 nodes touched per operation instead of 256. The IO budget closes. The chain ships at a realistic TPS instead of an aspirational one.
The SMT lessons did not disappear. They informed the current dual-hash JMT design, where the Poseidon2 path gives us the ZK-proof properties SMTs are known for, while the JMT structure underneath keeps the IO cost manageable. This was a smaller pivot than the consensus and execution ones, but it followed the same pattern: pick the cleanest theoretical answer first, run the numbers, switch to the production-grade variant when the cleanest answer does not survive contact with reality.
Building Otigen the language
For the full design record of this era, see otigen-language-era. The complete language reference, syntax, semantics, and standard library documentation are preserved in the otigen-book (now with a pivot-notice preface).
Around the same time, we made another decision that would also need revisiting later.
We decided to design and build our own smart-contract language.
Looking back, this was not an irrational decision. Given what we knew then, it was rational. The argument went like this: if Pyde is going to be opinionated about consensus, about cryptography, about state — about every layer of the stack — then the smart-contract language should be opinionated too. Otigen would be designed from day one around Pyde's semantics. Encryption-friendly. Threshold-decryption-aware. Nonce-window-native. Tight gas accounting. A clean compilation target for our pVM bytecode.
So we built it.
We built the compiler (otic). We built the bytecode interpreter (pyde-vm). We built the Cranelift-based ahead-of-time compiler (pyde-aot). We built the standard library. We built the developer toolchain (wright). We wrote a book about it (the otigen-book, still preserved as historical reference). We documented opcodes. We designed type semantics. We dogfooded contracts.
Real engineering. Real months of work.
For a while, the bet looked good. Otigen had personality. Its syntax was clean. The pVM was lean. The integration with Pyde's primitives — threshold encryption, access lists, nonce windows — was tighter than any general-purpose VM could match.
What we did not see clearly at the time was that building a smart-contract language is not a one-shot deliverable. It is a permanent commitment to a category of work that competes against everything else the chain needs. A language has to keep up with the host platform (toolchain updates, Cranelift API churn, security advisories). It has to add features real applications need that we did not predict in version one. It has to maintain backwards compatibility, or pay the cost of breaking it. It has to be fuzzed, audited, and hardened against an open adversary. It has to be documented for new developers, supported in IDEs, debuggers, profilers, linters, formatters. It has to be taught.
The deeper question, the one we eventually had to ask honestly, was whether all that ongoing work was paying for the right things. The language was not Pyde's differentiator. Solana's BPF is not why people use Solana. Polkadot's WASM is not why people use Polkadot. Aptos's Move-language is closer to a differentiator, but even there the chain competes on consensus and security, not on Move itself. Smart-contract languages are tools. They matter for developer experience. They do not move the needle on the question Pyde was created to answer — can we build a post-quantum L1 that is actually fast?
The work we were spending on the language was work we were not spending on the answer.
So we ran the honest math.
The honest reckoning
The decision was not made all at once. It accumulated.
There was the moment when a routine Cranelift API update broke the AOT compiler and took two days to chase down. There was the moment when a community developer asked whether they could write contracts in Rust, and we had to say "no, you have to learn Otigen first."
There was the moment when we read another paper on zk-WASM proving and realized that the WASM ecosystem was approaching native ZK execution proofs — work being pushed forward by several research groups in parallel — while a zk-Otigen prover would have to be built from scratch by us, and audited from scratch, and maintained from scratch.
There was the moment when we counted the audit surface honestly. A custom VM means:
- An internal audit of the bytecode interpreter, the AOT compiler, the sandbox boundary, the gas accounting, the trap handling.
- An external audit of all of the above, by a specialist firm willing to learn an instruction set that exists only here.
- Continuous fuzzing of the interpreter and the AOT against adversarial inputs.
- Re-audits whenever the language or the VM evolves.
- The same audit work, repeated, every year, indefinitely.
A WebAssembly runtime means: wasmtime, which is already vetted as production infrastructure by Bytecode Alliance, deployed at scale by Microsoft, Fastly, Shopify, and others. The sandbox has been fuzzed continuously for years. The instruction set is a public standard with academic and industrial scrutiny. We inherit that work at zero engineering cost. Our remaining audit surface shrinks to the host-function ABI and the chain-side integration — a small fraction of what a custom-VM audit would cost.
That was not a marginal saving. That was a reframe of how much engineering capacity Pyde would have to put into proving its own execution layer was safe, on a recurring basis, every year going forward.
And then there was the moment that settled it: we ran the numbers honestly.
The argument for keeping a custom VM had always rested on a quiet assumption — that our custom AOT, hand-tuned for our opcodes, would outperform a general-purpose WASM runtime. The reasoning sounded plausible: bespoke beats generic, surely. We had built pyde-aot carefully. It used Cranelift, the best open-source code generator outside of LLVM. It produced real native machine code. We had spent months on it.
So we looked at wasmtime. And we found out something that changed the whole equation.
Wasmtime also uses Cranelift. The exact same backend. The same code-generation passes. The same register allocator. The same machine-code emitter. The difference between pyde-aot and wasmtime's AOT was not the optimizer — it was the front-end that fed instructions into Cranelift.
And the WASM front-end is the path Cranelift was originally optimized for. WebAssembly is the workload Cranelift was built to serve well. Years of optimization passes, edge cases, calling-convention refinements — all targeted at WASM. Our Otigen front-end, by comparison, was new code touching the same backend. It had not been adversarially fuzzed. It had not benefited from a hundred outside contributors finding obscure miscompiles. It worked, but it was newer, less battle-tested, with smaller margins for the optimizer to extract performance from.
We then ran our own benchmarks instead of guessing. Real measurements on the existing PVM stack, on a developer workstation:
- PVM interpreter, ALU dispatch: ~279 million instructions per second.
- PVM AOT, ALU dispatch: ~2.9 billion instructions per second. A 10× speedup on tight compute loops.
- PVM AOT, DEX swap (constant-product AMM): ~100 million swaps per second, 3.7× faster than interpreted.
- PVM AOT, token transfer (storage-bound): ~243K transfers per second — essentially identical to the interpreter's ~231K. Storage IO dominates; the AOT compute advantage disappears.
- AOT compilation cost: under one millisecond for contracts under 256 instructions.
These numbers are single-thread micro-benchmarks of the execution layer in isolation — one VM, one workload, no consensus, no network, no parallel scheduling. They measure raw VM throughput, not end-to-end TPS. Full-chain TPS is governed by consensus latency, signature verification, network bandwidth, parallel scheduling, and disk IO in addition to VM execution; the realistic v1 target of 10–30K plaintext TPS on commodity hardware reflects all of those layers combined. The numbers above are useful for the VM-vs-VM comparison; they are not the chain's TPS.
Hardware used: Apple M4 Pro, 14 cores, 24 GB RAM, macOS 26.3.1.
Reproduce these numbers yourself: see pivot/03-running-benchmarks.md for the exact commands and the expected output shape.
Those are real numbers, not extrapolations. They tell us several things about where the VM actually matters.
The 10× speedup is on tight ALU loops. Real smart contracts are not tight ALU loops. They are storage reads, storage writes, signature checks, event emissions — workloads where the AOT-versus-interpreter gap collapses to roughly 1× because the bottleneck moves to RocksDB and to cryptographic verification, neither of which the VM can speed up. So the actual workload Pyde runs barely cares which VM compiles it.
When we mapped this against published wasmtime numbers — Cranelift-AOT WASM landing within 80-95% of native speed on compute, the interpreter at 10-30% of native — the comparison sat in the same range as our measurements. The two stacks are not in different leagues. They are in the same league, on the same backend, for the same reasons.
The interpreted comparison told the same story. A WASM interpreter (the fallback path when AOT cache is cold) achieves roughly the same throughput as our PVM interpreter — both sit in that 10-30 percent of native range, because both pay the dispatch cost. There was no meaningful interpreted-vs-interpreted advantage either.
So the speed argument for keeping Otigen quietly disappeared. The custom VM was not faster on the workloads that matter. It was just smaller-team-maintained, less-fuzzed, and lonelier.
What WASM offered was not just a comparable runtime. It was an already-vetted one. Production-deployed at Fastly and Microsoft and Shopify. Continuously fuzzed by an open community. Maintained by an entity that exists to maintain it. And we would pay essentially zero engineering capacity to inherit all of that — no compiler to support, no language to teach, no security maintenance to schedule. We got the speed plus the platform plus the ecosystem, in exchange for retiring a custom stack we had built for reasons that no longer held.
There was the moment when we looked at the surface area a credible v1 requires — consensus correctness, threshold cryptography, state sync, slashing, validator lifecycle, network protocol, parachain framework, audit prep — and realized that an in-house language committed us to maintaining a parallel track of work that competed with all of those for attention. Not because the language was harder than the consensus or the crypto. Because the language was optional in a way the others were not. Every chain ships consensus and crypto and state. Few chains ship their own language. The ones that do (Move, Vyper, Otigen) carry that as a perpetual obligation, and it is rarely the thing that determines whether the chain ships well.
We decided that Pyde would compete on what is actually unique to Pyde — post-quantum consensus, threshold-decrypted mempool, the cryptography stack — and inherit the rest from established WebAssembly tooling.
Pyde's execution layer pivoted to WebAssembly via wasmtime. Authors write contracts in Rust, AssemblyScript, Go, or C — whatever they already know. The compilation target is well-defined, the runtime is battle-tested in production at Fastly and Microsoft and Shopify, the sandboxing is verified by years of fuzzing, the gas-metering is built in, and the ZK-readiness path has actual researchers working on it.
This was not a defeat. It was the right call. The work we did building Otigen taught us what mattered (sandboxing, determinism, gas semantics, tight integration with Pyde primitives) and what did not (a custom syntax we had to teach the world). Everything that mattered carried forward into how we expose Pyde's primitives as WebAssembly host functions. The work was not wasted. The language was retired, but its lessons live in the new architecture.
The Otigen safety goodies are preserved
Worth being explicit about this because it is easy to assume a language-retirement loses the safety properties the language enforced. It does not.
Otigen's design defaults — reentrancy blocked by default, checked arithmetic, typed storage, no tx.origin, compile-time access list inference, the #[view] / #[payable] / #[reentrant] / #[sponsored] / #[constructor] attribute set — are all preserved unchanged in the WASM era. They are now expressed as language-native attributes (Rust #[pyde::view], AssemblyScript @pyde.view, Go //pyde:view, C PYDE_VIEW) that the build tool extracts into the ABI; the runtime applies the same guards it would have applied under Otigen.
Reentrancy is still blocked by default. The reentrancy guard is enforced at the WASM execution layer for every function not marked #[reentrant]. The author who writes nothing is still protected — exactly as in the Otigen era. See Chapter 5: Otigen Toolchain §5.6 for the full attribute surface and per-language declaration syntax.
The mechanism changed (build-time metadata + runtime enforcement instead of language compiler). The author experience and the safety guarantees did not.
What we got from the pivot
Worth naming explicitly, so the trade-offs are visible:
-
An already-vetted execution platform. Wasmtime is production infrastructure at Microsoft, Fastly, Shopify, and many others. The sandbox boundary, the determinism guarantees, the fuel-metered gas, the validation pipeline — all of it has been fuzzed continuously and hardened in adversarial conditions for years. We did not have to build any of it.
-
A dramatically smaller audit surface. A custom VM means auditing the interpreter, the AOT compiler, the sandbox, the gas accounting, the traps, and the language compiler — all from scratch, internally first and then externally, then re-audited as the system evolves. With wasmtime, our audit surface is the host-function ABI and the chain-side integration. Smaller scope, lower cost, faster turnaround, fewer specialists required.
-
Years of compounding maintenance work avoided. No language to keep current. No compiler to keep current. No AOT to keep current. No standard library to maintain. No IDE plugins, no debuggers, no formatters, no linters to write from scratch. The maintenance burden of a custom-language stack is permanent; pivoting away from it returns that capacity to Pyde's actual differentiators.
-
A clean ZK readiness path. zk-WASM is an active research area with multiple groups pushing it toward production. When mature, the provers slot in over our existing wasmtime execution — no re-tooling required on our end. zk-Otigen, by contrast, did not exist and would have been a multi-year side project for us alone.
-
Multi-language support out of the box. Authors write Pyde contracts in Rust, AssemblyScript, Go (via TinyGo), or C/C++. The barrier to entry is "the language you already use," not "the language Pyde wants you to learn." Developer adoption stops being gated by syntax familiarity.
-
A larger ecosystem of tooling. Block explorers, debuggers, profilers, fuzzers, formal verification tools — all exist for WebAssembly. We inherit them. Pyde-specific tooling can layer on top instead of starting from zero.
-
Time savings, measured honestly. The engineering capacity we would have spent maintaining Otigen — language design, compiler bug fixes, AOT bug fixes, standard library work, security advisories, ecosystem support — flows directly into the work Pyde actually competes on: post-quantum consensus, threshold cryptography, state-layer performance, validator lifecycle, parachain framework.
The trade-off we accepted: a small overhead on tight compute loops (which the benchmarks show is negligible for blockchain workloads, where storage IO dominates) and the loss of "Pyde has its own VM" as a marketing line (which was never a real differentiator anyway). For that price we got everything above.
The Otigen name lives on too. The new developer toolchain — the binary that scaffolds projects, generates state bindings, builds WASM artifacts, and deploys them — is called otigen. The same name, repurposed for the role it serves best: making the ergonomics layer feel as opinionated and integrated as the language was meant to be. The original otigen-book is preserved as a historical artifact, a snapshot of an earlier design phase that taught us what we needed to learn.
This is the same posture Rust's cargo takes (named for shipping containers, not for a programming concept) or Foundry's forge and cast take (craft-naming for tools). The name describes the role in the workflow, not the underlying technology.
Where we are now
The architecture that this book describes is the architecture after the pivots:
- Consensus: Mysticeti-style DAG, anchor-every-round, tail-latency-aware.
- Execution: WebAssembly via wasmtime, with Cranelift AOT for hot paths.
- State: Jellyfish Merkle Tree with dual hashing (Blake3 + Poseidon2), PIP-2 clustered slot keys for cache locality, dual roots so we can serve both standard light clients and future ZK light clients from the same tree.
- Cryptography: FALCON for signatures (post-quantum), threshold decryption as an opt-in mempool privacy path, Poseidon2 as our ZK-friendly hash, Blake3 for fast general hashing.
- Developer experience: the
otigenbinary owns the entire authoring lifecycle. Authors write only their contract logic and aotigen.toml. Everything else — language detection, build invocation, state binding generation, ABI emission, deploy-tx submission — is handled by the tool. - Parachains: WASM runtime per parachain, equal-power governance, full upgrade history retention, ENS-style name registration.
Each of these is the result of trying something else first, hitting a wall, and learning what the wall was made of. The book chapters that follow describe each layer in detail. This preface is here so that when you read about Mysticeti instead of HotStuff, or WASM instead of Otigen-the-language, you know that those choices were the outcome of work, not first instincts.
The first instincts were wrong, mostly. The current architecture is what was left after the wrong ones were honestly retired.
What this pivot does not change
It is worth being explicit about what stays the same, because the changes have been substantial and a casual reader could conclude that everything is in flux. It is not.
The core thesis is unchanged: post-quantum from day one, practical performance, decentralized validator set, light-client-verifiable state, opt-in transaction privacy via threshold decryption.
The consensus model is unchanged from the Mysticeti pivot onward: DAG-based, anchor-per-round, equal-power VRF-rotated committee.
The state layer is unchanged from the JMT decision onward: versioned Merkle tree, hash-friendly to both general hashing and ZK provers, PIP-2 clustering for locality.
The cryptography is unchanged: FALCON, Poseidon2, Blake3, threshold decryption via DKG.
The PIPs (Pyde Improvement Proposals) — PIP-2 clustered state keys, PIP-3 scheduler-level prefetch, PIP-4 application-level write-back cache, the dual-hash JMT — all carry forward unchanged. They are layer-agnostic. The execution VM does not affect them.
The pivot is localized. Most of Pyde's design carries through.
What this book is, and is not
This book is the current architecture of Pyde, as honestly as we can describe it. It is updated as design decisions land. The chapters that follow assume the pivots described here have happened; they do not repeatedly say "after the WASM pivot" or "before the consensus change." Read those as historical facts that informed what is described here.
This book is not a marketing document. It does not promise speeds we have not measured. It does not list partnerships that do not exist. It does not paper over the parts of the design that are still hard. Where something is uncertain, we say so. Where we have changed our minds, we say that too.
If you came here looking for a clean, never-pivoted, always-knew-the-answer story — that is not what Pyde is, and not what this book is. Pyde is what happens when someone decides to build a post-quantum L1, runs into every wall the architecture has to offer, and writes down what remained after the dust settled.
For the deep technical material on the earlier iterations — the HotStuff consensus design that preceded Mysticeti, and the Otigen language design that preceded WebAssembly — see the Pivot folder, which includes the design records and a step-by-step guide to running the pivot-era benchmarks on your own machine. The narrative is here; the design records are there.
The book starts now.
Roadmap
Pyde's path from design-complete to mainnet, structured as five phases (MC-0 through MC-5) with three parallel implementation streams in the core phase (MC-1). Each phase ships when its bar is met — no calendar dates.
Coordination details (crate ownership, branching protocol, interface contracts, session handoff prompts for the three streams) live in companion/IMPLEMENTATION_PLAN.md. Read that first if you're implementing.
Legend:
[SEQ]— sequential (must complete before the next phase starts)[PAR]— parallel (can run concurrently with siblings)→— explicit dependencyα/β/γ— owning implementation stream (seeIMPLEMENTATION_PLAN.md§4)
Top-level shape
MC-0 INTERFACE FOUNDATION [SEQ — main session]
│ Create engine repo, lock types + interfaces crates, CI baseline.
│ This is the prerequisite that makes parallelism safe.
▼
MC-1 PROTOCOL CORE [PAR — three streams]
│ Stream α (Toolchain) in pyde-net/otigen
│ Stream β (Execution) in pyde-net/engine on `execution-side` branch
│ Stream γ (Consensus) in pyde-net/engine on `consensus-side` branch
▼
MC-2 INTEGRATION [SEQ — γ owns]
│ Merge β + γ branches; bring up local devnet end-to-end.
▼
MC-3 STATE SYNC + PARACHAIN ACTIVATION [SEQ — β + γ joint]
│ Snapshot machinery, weak-subjectivity, parachain framework live.
▼
MC-4 PERFORMANCE + FAILURE HANDLING [PAR within]
│ Performance harness, chaos drills, soak.
▼
MC-5 VALIDATION + MAINNET LAUNCH [SEQ]
External audits, incentivized testnet, mainnet.
Old MC-1 through MC-7 numbering (pre-2026-05-23) collapses into this shape: old MC-1 + MC-2 → MC-0 + MC-1; old MC-3 → folded into Stream α; old MC-4 → folded into MC-3; old MC-5 → MC-3; old MC-6 → MC-4; old MC-7 → MC-5.
MC-0 — INTERFACE FOUNDATION [SEQ] — main session ✅ shipped
The sequential prerequisite to parallelism. Without MC-0 complete, streams β and γ clash on shared types and interface drift. ~1 day of focused work; the main session owns it.
Tagged phase-0-foundation on main at pyde-net/engine. 92 unit/integration tests pass; cargo clippy --workspace --all-targets -- -D warnings clean; cargo fmt --all -- --check clean.
0.1 Engine repo creation
-
Create
pyde-net/enginerepo on GitHub (fresh; post-pivot) -
Clone locally at
/pyde-net/engine/ -
Initial commit: README + LICENSE (Apache-2.0) +
.gitignore+SECURITY.md+rust-toolchain.toml
0.2 Workspace skeleton
-
Cargo.tomlworkspace with every crate stubbed:types,interfacesaccount,state,tx,wasm-exec,mempool(β-owned)consensus,net,dkg,slashing,node(γ-owned)
-
Each crate stub:
Cargo.toml+src/lib.rswith a placeholder function so the workspace compiles (node also hassrc/main.rsfor thepydebinary)
0.3 types crate (frozen at end of MC-0)
-
Address([u8; 32]) — full Poseidon2, no truncation -
SlotHash,Value(state primitives) -
Balance(u128),Nonce(u64),NonceWindow(16-slot bitmap) -
Txflat envelope +TxTypediscriminant (Ch 11 §11.6 wire format; tag 2 reserved-as-vacant) -
TxHash,Receipt,ReceiptStatus,FeePayer,AccessEntry,AccessType -
StateRoot(dual: Blake3 + Poseidon2) -
EventRecord(withwave_id/tx_index/event_indexprimary key +Vec<Topic>for multi-topic v1) +EventCursorforpyde_getLogspagination -
WaveId(u64),Round(u64),CommitId(= WaveId) -
VertexHash,BatchHash,BatchRef,Vertex(withmember_id+batch_refs+decryption_sharesper Ch 6 §3) +Batch(network gossip type) -
WaveCommitRecord(withanchor_round/prior_anchor_round/events_root/events_bloom/events_count/tx_count/gas_used: u128) -
HardFinalityCertwith 85-of-128 quorum check -
FalconPubkey(897 B fixed),FalconSignature(variable, ≤690 B cap) -
EventsBloom— spec-aligned algorithm: 256 B / 3 hashes /blake3(item)[..8/8..16/16..24]mod 2048 (consumer-side blake3 — leaf-dep invariant preserved) -
ContractAbiper HOST_FN_ABI_SPEC §3.7:pyde_abi_version: u32,contract_type,state_schema_hash,constructor_index/fallback_index/receive_index+EventAbiextension for §14.1 event signatures -
FunctionAttrs(u32 bitfield: VIEW / PAYABLE / REENTRANT / SPONSORED / CONSTRUCTOR / FALLBACK / RECEIVE / ENTRY) -
Error codes from
HOST_FN_ABI_SPEC §4—ERR_*consts + typedErrorCodeenum (i32 wire format; round-trips viaas_i32/from_i32) -
AuthKeys(None / Single / MultiSig / Programmable-reserved at tag0x03) withMAX_MULTISIG_SIGNERS = 16and structural validation - 81 unit + property tests including wire-tag verification and field-order pin tests
0.4 interfaces crate (frozen at end of MC-0)
-
trait StateView— async; balance / nonce_window / slot / code_hash / code / account_type / auth_keys / state_root -
trait StateMutator: StateView— async;commit_wave(wave_id, txs)→WaveCommitRecord,rollback_wave,snapshot→SnapshotHandle -
trait Executor— async;execute_tx(state, tx, gas_limit)+view_call(state, target, data) -
trait MempoolView— async; insert / drain_for_batch / contains / fetch_by_hash / pending_count -
trait NetworkView— async; publish_vertex / publish_batch / fetch_vertex / fetch_batch (libp2p gossip surface) -
trait ConsensusEngine— async; current_round / current_wave / get_finality_cert (read-only observation surface) -
InterfaceError— boundary error enum with retryability classification -
mod mock—MockState/MockExecutor/MockMempool/MockNetwork/MockConsensus, 11 tests each exercising at least one trait method per impl
0.5 CI + branching
-
.github/workflows/ci.ymlrunning fmt + clippy (-D warnings) + test + doc on every PR with target/registry caching -
Long-lived branches created:
execution-side(β),consensus-side(γ) -
Tag
phase-0-foundationonmain
0.6 IMPLEMENTATION_PLAN cross-link
-
pyde-book/src/companion/IMPLEMENTATION_PLAN.mdalready current - Cross-linked from this roadmap
MC-0 BAR: ✅ engine repo exists with all 12 crate stubs compiling; types + interfaces crates fully written and tested (92 tests, all green); CI green; branching protocol established; IMPLEMENTATION_PLAN.md committed.
MC-1 — PROTOCOL CORE [PAR — three streams] → MC-0
The core protocol implementation. Three streams run in parallel: α (toolchain), β (execution), γ (consensus). Each owns disjoint crates per the ownership map in IMPLEMENTATION_PLAN.md §4. The session-handoff prompts for each stream are in IMPLEMENTATION_PLAN.md §7.
MC-1 Stream α — Toolchain [SEQ within α] → MC-0 — repo pyde-net/otigen
Implements OTIGEN_BINARY_SPEC.md.
α.feat — Feature surface (spec §3 + §9 + supporting crates)
-
pyde-net/otigenrepo + Rust workspace -
otigen-toml: config parser + schema validation (spec §4) -
otigen-abi:ContractAbiconstruction + Borsh encoding + custom-section injection viawasm-encoder(spec §6) -
otigen-cli: subcommand framework viaclap(spec §3) -
otigen build: full validation pipeline (spec §3.2 step-by-step) -
otigen-wallet: keystore (Argon2id + AES-256-GCM, single-file multi-account per spec §7.1), FALCON-512 signing, secret-key zeroisation on drop — ported from archivedwrightrepo -
otigen wallet new/import/list/show/delete/password— single-file~/.pyde/keystore.json(override via--keystore), confirmation prompt before destructive ops, NDJSON event stream under--json -
otigen-rpc: JSON-RPC client per Ch 17.4 — syncreqwest::blockingClient+ 15 typed method wrappers (account / call / send / receipt / gas / wave / logs / snapshot), typed error envelope, wiremock-driven e2e tests. WebSocket subscriptions deferred to v2. -
otigen deploy— full §3.3 pipeline (bundle → re-validate → resolve network + wallet → fetch nonce → build canonical tx → FALCON-sign →pyde_sendRawTransaction→ poll receipt).--dry-runfor offline inspection,--no-waitfor fire-and-forget scripts. Wire format (Txenvelope +TxType/FeePayer/AccessTypediscriminant tags + canonical Poseidon2 hash) pinned to Ch 11 §11.6 / §11.8 / §"Transaction hash" on the toolchain side until Stream β'stxcrate lifts beyond its current scaffold. -
otigen upgrade/pause/unpause/kill/inspect -
otigen consoleREPL (spec §3.8) -
otigen verify(spec §3.9) -
Canonical example contracts: Rust ✅, AssemblyScript, Go (TinyGo), C/C++ hello-worlds — Rust shipped + exercised by
tests/hello_rust_e2e.rs; other languages pending
α.qual — Quality bar (production-readiness gate)
Every item below clears before α ships. Documented separately from the feature surface so the gate is unambiguous.
Testing infrastructure
-
Criterion benchmarks for every hot path with baselines committed to
benches/baseline/*.json:otigen-toml: TOML parse + cross-cutting validation ✅ (pyde-net/otigen#6)otigen-abi:ContractAbibuild, Borsh encode/decode round-trip,pyde.abicustom-section inject + extract, validators, full pipeline ✅ (pyde-net/otigen#6)otigen-cli: fullotigen buildpipeline end-to-end — measured via the otigen-abi full_pipeline bench (parse→validate→build→encode→inject = 14.5 µs on the reference machine); the wall-clockotigen buildinvocation is dominated by file I/O, not validator work
-
cargo-fuzztargets with 24h+ cumulative run before α release:otigen-tomlparser (malformed input, deep nesting, huge fields)otigen-abiWASM validator (malformed binaries, edge cases in section structure)otigen-abicustom-section injection (extreme WASM module shapes)
-
Property-test coverage audit: ≥15 proptest groups across
otigen-tomlandotigen-abi(currently ~5) -
Adversarial corpus: 30+ hand-rolled
otigen.tomlfiles undertests/corpus/each verified to pass / fail with the expected diagnostic -
Reproducibility test: two clean builds of the canonical hello-rust example produce byte-identical
contract.wasmandabi.json(modulomanifest.build_timestamp)
CI + supply chain
-
Multi-platform CI matrix:
ubuntu-latestx86_64 + aarch64,macos-latestarm64,windows-latestx86_64 — build / test / clippy / fmt on every PR -
cargo-audit(RustSec advisories) gate on every PR -
cargo-deny(license policy + version policy + duplicate-version checks) gate on every PR -
cargo-machete(unused dep detection) on every PR -
MSRV check: workspace
rust-version = "1.75"enforced in CI on a 1.75 toolchain - cargo-about generated 3rd-party attribution report shipped with every binary release
- Signed binary releases via GitHub Actions: Linux x86_64/aarch64 + macOS arm64 + Windows x86_64 tarballs, sha256sums, sigstore signatures, attached to GitHub Releases
UX completeness
-
--jsonNDJSON output wired across every subcommand per OTIGEN_BINARY_SPEC §10.2 (today only the global flag is parsed; per-event JSON output not yet emitted) -
--verbose/-vvactually emits the documented log levels (today the flag is captured but most commands print fixed output) -
Signal handling:
Ctrl-Cmid-build cleans up partial bundle artifacts -
otigen --versionincludes git-sha + build profile
Spec + documentation
-
Toolchain threat model document at
companion/TOOLCHAIN_THREAT_MODEL.md: 12 threat IDs (T-01 to T-12) covering maliciousotigen.toml, malicious WASM,pyde.abiinjection corruption, substituted.wasm, RPC MITM, keystore tampering, phished password, supply-chain attacks, dependency confusion, build-time code execution, path traversal, tx replay. Coverage table cross-references the roadmap items where each gap is tracked. -
Performance numbers committed in
README.md, Chapter 5 (otigen-toolchain), Chapter 17 (developer tools); baselines on a documented reference machine + how to reproduce ✅ (README in pyde-net/otigen#6; Chapters 5 §5.11 + 17 §17.1 in this PR) -
Architecture chapter (
chapters/05-otigen-toolchain.md) cross-links every public function in the implementation to the spec section it satisfies -
No new
unsafeblocks anywhere in the workspace (verified by grep + CI) -
No
unwrap()/expect()on untrusted-input paths (verified manually + by lint where possible)
α.live — Live tests (blocked on MC-2 devnet)
-
otigen deployagainst a running devnet — end-to-end transaction submission + receipt fetch -
otigen inspectagainst a deployed contract on the devnet -
otigen verifyreproducibility round-trip via the devnet'spyde_getContractCodeRPC - Multi-validator stress: deploy + call from 7 distinct keystore identities concurrently
α BAR (production-ready): every checkbox in α.feat, α.qual, and α.live ticked; CI green on every platform; fuzz targets have run ≥24h cumulative with no surviving crashes; two independent builds of the canonical hello-rust produce byte-identical artifacts; performance baselines committed and tracked on every PR.
α BAR (pre-devnet, demonstrable today as of pyde-net/otigen#5): ✅ — the init → cargo build → otigen build → bundle flow is exercised end-to-end by tests/hello_rust_e2e.rs against the real Rust toolchain. The full BAR adds the α.qual quality gate plus the α.live devnet items.
MC-1 Stream β — Engine Execution [PAR within] → MC-0 — pyde-net/engine branch execution-side
Implements HOST_FN_ABI_SPEC.md (chain side), Chapter 4, PIPs 2/3/4.
Crates owned: account, state, tx, wasm-exec, mempool.
β.1 state crate [SEQ within β] — foundational
- JMT dual-hash (Blake3 + Poseidon2 per node)
-
Two-table architecture:
state_cf(flatslot_hash → value) +jmt_cf(versioned tree) - PIP-2 clustered slot keys (contract-prefix layout)
- PIP-3 wave-level state prefetch (MultiGet against access lists)
- PIP-4 write-back cache (DashMap + warm window + lazy flush)
-
events_cf + events_by_topic_cf + events_by_contract_cf (per
HOST_FN_ABI_SPEC §15.3) - Atomic wave-commit WriteBatch (state + events + wave commit record in one transaction)
- events_root (Blake3 binary Merkle) + events_bloom (256-byte, 3-hash) computation
-
Implement
StateView+StateMutatortraits (frominterfaces) - Snapshot generation (range-proof chunks, manifest)
β.2 account crate [PAR within β]
-
32-byte address derivation (
Poseidon2(falcon_pubkey)) -
AuthKeysenum withSingle,MultiSig,Programmable(Programmable v2-reserved) - 16-slot nonce window
- Name registry as a system contract (ENS-style, unique names)
β.3 tx crate [PAR within β]
-
Native tx types:
Transfer,ValidatorRegister,Stake,Unstake,NameRegister,Multisig,RotateKeys -
WASM tx types:
ContractCall,ContractDeploy - Canonical tx hashing (Blake3 over deterministic encoding)
-
Gas accounting (EIP-1559 base fee; no refunds per
gas-no-refund-v1memory) -
Deploy / upgrade / lifecycle handlers (per
OTIGEN_BINARY_SPEC §8)
β.4 wasm-exec crate [SEQ within β] → β.1
- wasmtime engine config (deterministic feature subset per Ch 3 §3.2)
-
WasmExecutortype -
Module cache: LRU + max-size (1 GB default) + TTL (8 epochs default) (per
HOST_FN_ABI_SPEC §3.6) - Fuel-to-gas mapping (calibrated from spec §10 gas table)
- Per-tx overlay execution model (snapshot-and-rollback; nested for cross-call)
-
Host functions — each independent task:
-
Storage:
sload,sstore,sdelete(with access-list enforcement) -
Balances:
balance,transfer -
Context:
caller,origin,self_address,block_height,wave_id,block_timestamp,chain_id -
Tx context:
tx_hash,tx_value,tx_gas_remaining,calldata_size,calldata_copy -
Events:
emit_event(multi-topic; 1-4 topics; spec §7.5) -
Hashing:
hash_blake3,hash_poseidon2,hash_keccak256 -
Crypto:
falcon_verify -
Cross-call:
cross_call,cross_call_static(FREE; bounded byVIEW_FUEL_CAP),delegate_call -
Halt:
return,revert -
Gas:
consume_gas -
Randomness:
beacon_get -
Parachain extensions (gated):
parachain_storage_read/write/delete,parachain_emit_event,parachain_id,parachain_version,send_xparachain_message,threshold_encrypt,threshold_decrypt
-
Storage:
-
Deploy-time validation (3-layer per
HOST_FN_ABI_SPEC §3.7) -
Attribute application +
pyde.abicustom-section extraction -
Implement
Executortrait (frominterfaces)
β.5 mempool crate [PAR within β] → β.3
- FALCON-512 verify pipeline (batchable)
- Validation rules: chain_id, nonce window, balance, gas bounds, calldata size, attribute coherence
-
Gossip admission (integration with γ's
netcrate viaNetworkViewtrait) - Per-sender rate limit + concurrent cap (DDoS protection)
-
Implement
MempoolViewtrait (frominterfaces)
β BAR: cargo test clean on execution-side branch; mock-based integration tests (using interfaces::mock) pass for state + execution + mempool; can replay a tx end-to-end against the in-memory MockNetwork.
MC-1 Stream γ — Engine Consensus + Network [PAR within] → MC-0 — pyde-net/engine branch consensus-side
Implements Chapter 6, SLASHING.md, VALIDATOR_LIFECYCLE.md, STATE_SYNC.md, CHAIN_HALT.md, NETWORK_PROTOCOL.md.
Crates owned: consensus, net, dkg, slashing, node.
γ.1 consensus crate [SEQ within γ] — foundational
-
Vertexstructure (round, member_id, parent_refs, batch_refs, state_root_sigs, prev_anchor_attestation, decryption_shares, sig) — landed intypescrate at MC-0 -
Local DAG view per validator (
VertexStore: hash + round + slot indexes, equivocation-aware,parking_lot::RwLockguarded) — PR #1 -
Canonical
vertex_hash = Blake3(borsh(vertex_sans_falcon_sig))centralised — PR #1 -
Equivocation flagging on insert (
InsertOutcome::Equivocation { prior_at_slot }; full slashing flow lives in γ.4) -
Vertex production pipeline (
VertexBuilder+Signertrait +select_parentshelper that skips equivocating slots; returns(VertexHash, Vertex)so callers get the dedup key free) — PR #2 -
Vertex validation pipeline (
validate_vertex+Verifiertrait +ValidationConfig; cheapest-first checks: range → batch-dedup → parent quorum → parent-round homogeneity → FALCON sig;MissingParentreturns hash so caller can fetch and retry) — PR #3 -
Round advancement (
RoundTracker: monotonic counter, distinct-member_idquorum check,try_advance/try_advance_to_maxfor state-sync catch-up, equivocator-resistant via distinct-producer counting) — PR #9 -
Anchor selection:
select_anchor(beacon, round, lookback_state_root, committee_size)— Blake3 overbeacon || round.le || state_root.blake3, mod committee. Dual-hash aware (only Blake3 leg mixes in; Poseidon2 reserved for SNARK paths). Uniform at 128 = 2^7 (no rejection sampling needed). — PR #4 - VRF beacon derivation (uses pyde-crypto)
-
Mysticeti 3-stage support check (
check_anchor_support: supporters at R+1 + certifiers at R+2; Committed / Pending / Skipped — Skipped prevents stall on bad proposer) — PR #5 -
BFS subdag walk + canonical sort (
walk_subdag: BFS over parent_vertex_refs, skips already-committed, canonical (round, member_id, hash) order — wire-load-bearing) — PR #7 -
Missing-vertex bookkeeping (
PendingParentsqueue: bounded, idempotent-duplicate, cascade-unblock, exposesmissing_parents()for the network fetch loop). Network-fetch dispatch wired at node-binary level (MC-2). — PR #10 - Anchor-skip handling
- Piggybacked decryption shares (pipeline decryption with consensus)
-
HardFinalityCert generation (
FinalityCertCollector: cached pre-image, duplicate-before-verify, deterministic member_id-sorted finalize, FinalityError taxonomy) — PR #8 -
WaveCommitRecord assembly (
assemble_wave_commit_record: canonical anchor_hash, u32 tx_count overflow check, WaveCommitInputs cross-stream boundary) — PR #7 - Committee management (epoch-bounded; uniform random from eligible stakers)
- Equivocation detection + evidence collection → γ.4 Slashing
-
Implement
ConsensusEnginetrait viaDriver(composed runtime:VertexStore+RoundTracker+PendingParents+ finality history; Arc-shared, fine-grained locks, wave-monotonicity guard, object-safe trait impl) — PR #11
γ.2 net crate [PAR within γ]
- libp2p + QUIC transport (pinned versions)
- Gossipsub topics: vertices, batches, decryption_shares, state_root_sigs, mempool, state_sync, evidence, governance
- Layered peer discovery: hardcoded seeds → DNS → on-chain validator registry → PEX → cache (NO DHT)
- Sentry node pattern (committee primaries behind sentry proxies)
- Peer scoring + multi-layer DDoS protections
- Vertex-fetch protocol (used by γ.1 missing-vertex handling)
- PeerId persistence + known-peers cache for fast restart
-
Implement
NetworkViewtrait (frominterfaces)
γ.3 dkg crate [PAR within γ]
- Pedersen DKG protocol implementation (per epoch)
- PSS resharing (proactive secret sharing across epochs)
-
May import from
pyde-cryptoif helpers land there first
γ.4 slashing crate [PAR within γ] → γ.1
- Validator state machine (registered → active → jailed → unbonding → withdrawn)
- Validator txs: register, unbond, withdraw, rotate-key, unjail
- Operator-identity binding (anti-Sybil; max 3 validators per operator)
- Synced-only committee enforcement
-
10-offense catalog implementation per
SLASHING.md - Slashing escrow + grace period
- Reward distribution (pool-based, stake × uptime)
γ.5 node crate [SEQ within γ] → γ.1 + γ.2 + γ.4 — owned by γ; integration point
-
pydebinary (cli, validator, full-node modes) -
JSON-RPC server (per
HOST_FN_ABI_SPEC §15.4-15.5+ chapter 17 method list) -
consensus_storewithWriteOptions::set_sync(true)(per Ch 16 §16.12) -
panic = "abort"on persist failure - Validator role (FALCON keypair management, attestation, key rotation)
- Persistence: receipts_cf, txs_cf, waves_cf
γ BAR: cargo test clean on consensus-side branch; consensus loop runs end-to-end with MockStateView + MockMempool + MockNetwork; vertex production + anchor selection + commit work in isolation.
MC-2 — INTEGRATION [SEQ] → MC-1 all streams — γ-owned
Merge execution-side and consensus-side branches to main. Bring up a local devnet.
MC-2 spike ✅ shipped (precedes full MC-2)
A single-validator devnet running the real consensus driver end-to-end with stubbed crypto / network / persistence. The "Pyde transfers value, today" demonstration — real Mysticeti 3-stage commit, real BFS subdag walk, real WaveCommitRecord assembly, real HardFinalityCert collection, for a real transfer transaction.
-
DevnetState—StateMutatorimpl with real transfer + fee + nonce-window logic — PR #15 -
DevnetExecutor— pure pre-flightExecutorimpl — PR #16 -
Devnetcomposer +Wallet— full single-validator commit loop — PR #17 -
run_smokescenario + 8 integration tests — PR #18 -
pyde-node devnet --smokeCLI subcommand — PR #19 - README "Try the demo" + bench baseline link — PR #20
Reproduce: cargo run --bin pyde -- devnet --smoke. Full bench baseline: crates/consensus/benches/baseline.md.
Full MC-2 (ahead — needs real β + real γ libs wired)
-
Final merges of β and γ to
main(γ owns this) - Local devnet config (4-7 validators on a single machine, real libp2p networking)
-
End-to-end test flow with real crypto + real persistence + real WASM:
- Author writes contract (with α's otigen)
otigen deployagainst the devnet- Tx submitted, validated by mempool (β), included in vertex (γ)
- Anchor commits, wasmtime executes (β), state updates (β)
- HardFinalityCert formed (γ), receipt queryable via RPC
- Event subscription pushes notifications
- Smoke tests: simple transfer, contract deploy, view call, cross-contract call, event emission, event subscription
MC-2 BAR: local devnet running with sub-second commits and successful end-to-end tx flow. Three smoke contracts deploy and operate correctly. All MC-1 deliverables integrated.
MC-3 — STATE SYNC + PARACHAIN ACTIVATION [SEQ] → MC-2 — β + γ joint
3.1 State sync (γ-led, β co-owns snapshot generation)
-
Snapshot generation (background on archive nodes + volunteer-served)
- Walk JMT at target wave version
- Chunk into ~50MB pieces with range proofs
- Persist chunks; publish SnapshotManifest
-
pyde_getSnapshotManifestRPC handler - Snapshot chunk serving over libp2p streams (parallel from multiple peers)
- Weak-subjectivity checkpoint format (wave_id + dual state_roots + committee threshold sig)
- WS checkpoint distribution
- New-validator sync flow (download manifest, parallel chunk fetch, verify, write, replay tail)
- Sync time target (~40 min for fresh sync)
- Three-tier node model: archive / pruned / light client
3.2 Parachain framework activation (β + γ joint)
- Parachain account structure (versions, balance, config, state_root, owner deposit, status)
-
Parachain ID derivation (
Poseidon2("pyde-parachain:" || name)) - Deploy flow (owner deposit, WASM validation, registry write)
- Upgrade flow (proposal, equal-power voting, scheduled activation)
- Pause / kill (operational lifecycle)
-
State subtree partitioning (
parachain_id[..16]PIP-2 prefix) - Cross-parachain messaging (rate-limited, threshold-signed; γ networking; β host fn)
-
cross_callcallback mechanism (success / error / timeout flows) - Version manifest in wave-commit records (replay correctness)
- Reference parachains: price-feed oracle + confidential-vote parachain
MC-3 BAR: fresh validator can sync to current head in under 1 hour and become committee-eligible. An author deploys a parachain; validators opt in; cross_call from a smart contract to the parachain works with a callback returning a result.
MC-4 — PERFORMANCE + FAILURE HANDLING [PAR within] → MC-2 + MC-3
4.1 Performance harness
Spec: PERFORMANCE_HARNESS.md.
- Workload generators (compute / IO / crypto / mixed-realistic)
- Multi-region topology framework (US-East, EU-West, AP-Southeast)
- Chaos scenarios (validator drops, network partitions, slow disks, equivocating actors)
- Soak-test scheduler (1h / 4h / 24h / 7-day)
- Metrics: TPS, p50/p99/p999 latency, memory, CPU breakdown, gas accounting
- "Claim 1/3 of measured peak" publication discipline
- Per-host-function micro-benchmarks (calibrate gas cost table against real hardware)
- Sequential vs parallel execution scaling tests
4.2 Failure handling drills
Spec: FAILURE_SCENARIOS.md + CHAIN_HALT.md.
- Walk through all 12 catalogued failure scenarios in a testnet
- Soft-stall / hard-halt / emergency-pause drills
- 1-epoch bounded rollback drill
- Validator key compromise + rotation drill
MC-4 BAR: performance numbers published per the harness discipline; failure-handling runbooks battle-tested in a controlled environment.
MC-5 — VALIDATION + MAINNET LAUNCH [SEQ] → MC-4
Spec: Chapter 19 (Launch Strategy).
5.1 External audits (5 specialist tracks)
- Consensus layer (Mysticeti DAG, anchor selection, finality, slashing)
- WASM execution layer (host functions, fuel-to-gas, validation gate, hybrid scheduler)
-
Cryptography (FALCON, Kyber, Blake3, Poseidon2, threshold, PSS) —
pyde-crypto - Networking (libp2p config, gossipsub, peer discovery, sentry pattern, DDoS)
-
otigentoolchain (codegen, ABI extraction, deploy flow, wallet)
5.2 Incentivized testnet
- Reference dApps: DEX, lending market, NFT marketplace
- Funded bug bounty at mainnet tier
- Multi-month soak with real user traffic
- Remediate community-found issues before launch
5.3 Mainnet candidate
- Final genesis configuration
- Initial validator set (≥32 validators, geographically distributed)
- Day-one ecosystem partners (≥3-5 parachains/dApps)
- Token distribution finalized
- Bug bounty scaled to mainnet tier
- Mainnet launch
MC-5 BAR: mainnet live. All MC-0 through MC-4 work integrated, audited, stress-tested, soak-passed.
Beyond V1 [PAR] — post-mainnet research/dev directions
- ZK-aggregated FALCON signatures (the path to dramatic signature-verification throughput gains)
- zk-WASM proven execution
- Cross-chain bridges (Ethereum, Bitcoin, others) with proven-security mechanisms
- Programmable accounts + native session keys — scoped, bounded, revocable dApp delegation. Native at the protocol (vs Ethereum's ERC-4337 retrofit). See Chapter 11 Session keys (v2) and
companion/DESIGN.mdfor the design + v1 reservations the surfaces depend on. - State-expiration policy
- Tier 2/3 wallet preview (heuristics + LLM analysis) per [[ai-wallet-preview-direction]]
V1 reservations that create room for v2 features
V1 ships interfaces; v2 ships implementations. Discipline: don't reach into v2 while v1 is shipping, but reserve the protocol surfaces v2 needs so contracts written today survive the upgrade unchanged.
| v2 feature | v1 reservation | Cost at v1 |
|---|---|---|
| Programmable accounts | AuthKeys::Programmable enum tag 0x03 | Enum variant, unused — ~zero |
| Programmable accounts | Account code_hash + storage_root (unified with contracts) | Already shipped (account/contract account shape unified) |
| Session keys | WASM "policy mode" execution flag | Reserved-but-not-implemented — ~zero |
| Session keys | Multisig signature pipeline | Already shipped (serves multisig + future session-key flows) |
| ZK light clients | Poseidon2 state root + ZK-friendly primitives | Already shipped (dual-hash JMT, no Blake3 in proof-bearing paths) |
| Parachains (further depth) | cross_call host fn, HardFinalityCert primitive, async callback slots | Already shipped (Chapter 13, companion/PARACHAIN_DESIGN.md) |
The discipline: every entry above is something the v1 protocol can ship for ~zero marginal cost, but skipping any one of them would force a hard-fork rewrite when v2 lands. Reserving them now is cheap insurance.
End-to-end flow: user → execution → user
For context on what all this protocol work enables, here's the full E2E flow once all chunks are landed:
1. USER: opens wallet, builds tx (function call, args, gas budget)
2. WALLET: runs local wasmtime preview → shows state changes, gas estimate, events
3. USER: reviews preview, signs (FALCON-512)
4. WALLET: optionally encrypts under committee threshold key (Kyber-768)
5. WALLET → RPC: pyde_sendRawTransaction(signed_tx)
6. RPC: validates ingress (sig, balance, nonce, gas, chain_id)
7. RPC → MEMPOOL WORKER: forwards via libp2p
8. WORKER: adds to pending batch
9. WORKER: seals batch, gossipps to other workers, collects ≥85 certifications
10. WORKER → PRIMARY: certified batch_hash available for inclusion
11. PRIMARY: produces vertex with batch_hash in batch_refs (+ decryption shares if applicable)
12. VERTEX: gossipped via libp2p/gossipsub on pyde/vertices/1 topic
13. DAG: grows; each round adds 128 vertices
14. ANCHOR: deterministically selected via Hash(beacon, round, prev_root) mod 128
15. SUPPORT: round R+2's 85+ vertices transitively reference anchor → 3-stage support
16. COMMIT: subdag walk (BFS-for-set + canonical sort)
17. DECRYPT: batch threshold-decrypt all encrypted txs in subdag (shares already piggybacked)
18. SCHEDULE: hybrid scheduler (static access + Block-STM) partitions for parallel execution
19. EXECUTE: wasmtime runs each tx (per-tx overlays for isolation; success → merge, trap → discard)
20. STATE: changes accumulate in DashMap → JMT update → new state_root
21. SIGN: committee FALCON-signs (wave_id, blake3_root, poseidon2_root)
22. PERSIST: WaveCommitRecord synchronously to disk; vertices/batches/receipts lazily
23. FINALITY: 85+ sigs collected → HardFinalityCert formed
24. USER ← RPC: pyde_getTransactionReceipt(tx_hash) returns success/revert + state changes + gas used
25. USER: sees confirmation in wallet UI
Total wall-clock from step 5 (submit) to step 25 (confirmation visible): ~500ms-1s under normal conditions.
Each step maps to specific chunks in the roadmap. The full path traverses MC-2 (consensus, execution, state, crypto, network, accounts, slashing) end-to-end, with MC-3 (otigen, SDKs, wallet) at the boundaries.
Stream dependency matrix (cross-MC view)
| Item | Owning stream | Depends on | Used by |
|---|---|---|---|
| MC-0 Interface foundation | main session | (none) | All MC-1 streams |
| MC-1 α Toolchain | α | MC-0 + HOST_FN_ABI_SPEC | Contract authors; MC-2 deploy testing |
| MC-1 β.1 State | β | MC-0 | β.4 (wasm-exec); γ.1 (consensus reads state_root); MC-3 state sync |
| MC-1 β.2 Account | β | MC-0 + pyde-crypto | β.3 (tx sender validation); β.4 (host context); γ.4 (validator txs) |
| MC-1 β.3 Tx | β | MC-0 + β.2 + pyde-crypto | β.4 (tx dispatch); β.5 (mempool); γ (consensus orderable items) |
| MC-1 β.4 WASM Execution | β | MC-0 + β.1 + β.2 + β.3 | MC-1 α (pyde.abi consumers); γ (consensus invokes via Executor); MC-3 parachain runtime |
| MC-1 β.5 Mempool | β | MC-0 + β.3 | γ.1 (reads via MempoolView); γ.2 (gossip submission) |
| MC-1 γ.1 Consensus | γ | MC-0 + pyde-crypto | γ.5 (node binary drives consensus); MC-2 integration |
| MC-1 γ.2 Net | γ | MC-0 | γ.1 (gossip transport); β.5 (tx propagation) |
| MC-1 γ.3 DKG | γ | MC-0 + pyde-crypto | γ.1 (threshold decryption keys); β.4 (threshold_encrypt/decrypt) |
| MC-1 γ.4 Slashing + Validator Lifecycle | γ | MC-0 + γ.1 + β.3 | γ.5 (RPC validator endpoints); consensus integrity |
| MC-1 γ.5 Node binary | γ | All β + γ crates via traits | The deployable artifact |
| MC-2 Integration | γ-led | All MC-1 streams done | Devnet & all of MC-3-5 |
| MC-3 State Sync + Parachain | β + γ joint | MC-2 | New validators (sync); parachain authors |
| MC-4 Performance + Failure | shared | MC-2 + MC-3 functional | Mainnet readiness |
| MC-5 Validation + Launch | main | All preceding | Mainnet live |
Operating principle
The bias of this roadmap is honesty over optimism. No chunk ships before its bar is met. No item is checked off until the work behind it is actually done. If something turns out to be wrong, it gets honestly rewritten — including this roadmap.
The work is the work. It ships when it is ready.
Pivot — Historical Design References
This directory preserves Pyde's earlier architectural iterations as first-class historical material. Pyde has gone through two clean pivots that materially changed the protocol design, and the work that preceded each pivot is documented here so it can be studied, learned from, and properly credited.
Read the preface first if you have not already — it is the narrative companion to this directory. The preface tells the story; this directory holds the design records.
Contents
| Document | Era | Status |
|---|---|---|
| 01 — The HotStuff Consensus Era | Pre-Mysticeti consensus design | Retired |
| 02 — The Otigen Language Era | Pre-WASM execution design (custom language + VM + AOT) | Retired |
Each document summarizes what was designed, what was built, what was learned, and where the deep technical material lives (which archived repos, which book, which design docs). The summaries are not re-derivations of the original work — they are pointers + context for reading the originals correctly.
Why this exists
Three reasons:
-
The work is real. Building these systems taught us what mattered and what did not. The current architecture is informed throughout by lessons from these earlier iterations. Pretending the work never happened would be both dishonest and counterproductive — future architects (Pyde or otherwise) can learn from the trade-offs we explored.
-
Honesty is the project's posture. Pyde's design has changed in response to evidence. Documenting the changes openly is the same discipline that made the changes possible. A reader who lands here looking for "why did Pyde stop using X?" deserves a real answer with real material, not a 404.
-
Some of these designs are independently interesting. The Otigen language, the custom register-based VM, the pre-Mysticeti HotStuff integration, the early access-list scheduler — these are not generic patterns. They were thought through carefully. Someone designing a similar system elsewhere may find the trade-offs documented here useful.
How to read what's here
Each document follows the same shape:
- What we built — the design, in summary form.
- Why we built it that way — the constraints and reasoning at the time.
- What we learned — what survived the pivot, intellectually.
- Where the original material lives — links to archived code, archived docs, and the otigen-book for language-specific content.
Read in the order presented (01 then 02). The two pivots happened in sequence; the second was informed by lessons from the first.
Reading order for the whole pivot story
- Preface: The Pivot — the narrative.
- This directory's 01 — HotStuff Era and 02 — Otigen Era — the design records.
- The main book chapters — the current architecture.
- Roadmap — the path forward.
01 — The HotStuff Consensus Era
The first consensus protocol Pyde adopted was an in-house variant of HotStuff. This document summarizes that design, why we chose it, what it taught us, and where the original material lives.
What we built
A linear, pipelined HotStuff variant tuned for Pyde's committee model:
- Three-phase commit pipeline — prepare, pre-commit, commit, decide, with each phase carrying a quorum certificate (QC) from the prior phase.
- Leader-driven block production — one leader per view, leaders rotate per view via deterministic rotation.
- 128-validator committee — the same committee size we still use today (preserved across the pivot).
- 400ms slot timing — target round duration of 400ms, with adaptive timeouts on view changes.
- FALCON-512 quorum certificates — 85-of-128 signatures aggregated into a QC, with the FALCON signature scheme preserved across the pivot.
- Pipelined view changes — to avoid the canonical HotStuff round-trip stall, view changes were pipelined into the steady-state flow.
The architecture lived in a consensus crate inside the engine workspace, alongside the (then-) PVM execution layer and the state crate.
Why we built it that way
HotStuff was the orthodoxy of modern BFT at the time. Used by Diem (Meta's version that did not ship), adopted by Aptos, validated in academic literature, with reference implementations available. The properties looked right for Pyde:
- Linear message complexity (vs PBFT's quadratic).
- Optimistic responsiveness (commits at the speed of the network, not at fixed timeouts).
- Simple safety + liveness proofs.
- Established ecosystem of HotStuff variants to learn from (LibraBFT, AptosBFT, HotStuff-2).
The constraint set Pyde faced — equal-power validators, sub-second commits, geographic-distribution-tolerant — looked like a clean HotStuff fit on paper.
So we built it.
What went wrong
Under load, in adversarial conditions — partial network partitions, slow validators, particular orderings of messages — HotStuff's commit latency tail ballooned. Median commits stayed under 100ms; tail commits ran out to 400ms and beyond. The chain "wedged" intermittently: not formally halted, just unable to deliver low-latency commits when conditions degraded.
We engineered against the tail for weeks. Tuning timeouts, re-ordering message handlers, experimenting with different leader-rotation schedules, adjusting the view-change protocol. Some of these helped at the margin. None of them got the tail under control structurally.
The honest read became: HotStuff's latency tail is not a tuning problem. It is a structural property of leader-based BFT under adversarial conditions. Different parameters give different tail shapes; none of them give a flat tail. We could keep grinding for another year and still ship a chain that wedged.
What we learned
Three lessons survived the pivot intact:
-
Tail latency is the UX killer, not median latency. A chain that commits in 100ms on average but stalls for 400ms in the tail will feel broken to users. The current Mysticeti-based design is specifically chosen for its better tail-latency profile under adversarial conditions, not for its median performance.
-
DAG consensus is structurally different from leader-based BFT, in ways that matter. The single-leader bottleneck in HotStuff is what produces the tail; removing the bottleneck (per-round, every validator can produce a vertex) removes the structural source of the tail.
-
Build to learn, but be willing to throw it away. The HotStuff integration was real engineering work. We did not regret building it — we regretted not pivoting away from it sooner. The retrospective lesson: when the data says "this won't get there," act on it. Do not engineer-around the structural problem.
What survived
Several pieces of the HotStuff-era architecture carried forward into the current Mysticeti-based design without change:
- The 128-validator committee size with 85-quorum threshold.
- The FALCON-512 signature scheme for quorum certificates.
- The equal-power, VRF-rotated committee selection model.
- The general wave abstraction (a periodic commit unit with an associated state root).
- Much of the supporting infrastructure: state layer, mempool admission, transaction types, validator lifecycle.
The pivot was localized to the consensus core. Everything that touched consensus from above or below stayed.
Where the original material lives
- Source code —
archive/crates/consensus/(in the umbrella repo). The HotStuff implementation, including the QC types, view-change protocol, and leader-rotation logic. - Design notes —
archive/crates/consensus/CONSENSUS_INVARIANTS.mddocuments the consensus invariants the HotStuff implementation upheld. - Original whitepaper —
archive/WHITEPAPER.mddescribes the early-architecture vision including HotStuff as the consensus choice. - Pre-pivot engine crates —
archive/crates/more broadly contains the consensus-adjacent crates from this era (mempool integration, transaction processing under HotStuff semantics).
The archive directory is preserved with git history intact. Anyone wanting to study the HotStuff-era implementation can browse it directly or check out the git revision before the consensus pivot.
Reading on
- 02 — The Otigen Language Era — the second pivot, on the execution layer.
- Chapter 6: Consensus (Mysticeti DAG) — the current consensus design.
- Preface: The Pivot — the narrative version of both pivots.
02 — The Otigen Language Era
The second large pivot Pyde went through was the retirement of the custom execution stack — language, VM, AOT, toolchain — in favor of WebAssembly via wasmtime. This document summarizes what we built in the Otigen-language era, why we built it that way, what we learned, and where the original material lives.
What we built
A complete custom execution stack for Pyde, four interlocking components:
Otigen — the language
.oti source files, surface syntax inspired by Rust, semantics tuned for blockchain execution:
- Reentrancy blocked by default; opt-in via the
#[reentrant]attribute. - Checked arithmetic by default; wrapping operations explicit.
- Typed storage via the
storage { ... }block. - No
tx.origin— the language did not expose it. #[view]/#[payable]/#[constructor]function attributes.- Compile-time access-list inference for the parallel scheduler.
- 4-byte function selectors derived from signature hashes.
otic — the compiler
.oti source → PVM bytecode + JSON ABI. Architecturally a four-stage pipeline: lex → parse → resolve → typecheck → safety analysis → bytecode emit. Implemented in Rust as a standalone library + binary.
pyde-vm — the virtual machine
A custom register-based VM:
- 16 × 64-bit general-purpose registers.
- 8 × 256-bit wide registers (for token amounts, hashes, signature components).
- 32-bit fixed-width instruction encoding.
- 62 opcodes covering ALU, memory, storage, crypto, host calls, and control flow.
- Static 4MB memory map with gas-metered page allocation.
- Trap-on-overflow by default.
pyde-aot — the ahead-of-time compiler
PVM bytecode → native x86 / aarch64 machine code, via the Cranelift code generator. Compiled at contract deploy time; the resulting native function was cached forever (contracts were immutable).
wright — the developer toolchain
Project-level CLI (init, build, test, deploy, wallet, console) analogous to Foundry for Solidity. Wrapped the otic compiler with project conventions and a deployment client.
Why we built it that way
The original argument: Pyde was going to be opinionated about every layer (consensus, cryptography, state, MEV protection), so the language layer should be opinionated too. Otigen would be designed from day one around Pyde's semantics — encryption-aware, threshold-decryption-friendly, nonce-window-native, with tight gas accounting and a clean compilation target.
The constraints we wanted the language to address:
- Reentrancy footguns in Solidity contributed to billions of dollars of lost funds historically. Block by default.
- Arithmetic overflow caused the bZx incidents, the YAM rebase bug, others. Check by default.
- Untyped storage in EVM led to slot-collision bugs. Type it.
- tx.origin was a phishing vector. Do not expose it.
- Dynamic dispatch unpredictability broke parallel execution in EVM. Infer access lists at compile time.
These were real problems we wanted addressed structurally, not via developer discipline.
We also believed at the time that a custom VM with a custom instruction set, tightly designed for blockchain operations, would outperform a general-purpose runtime. The PVM's wide-register file was specifically designed for 256-bit token amounts and hash operations.
So we built the whole stack.
What went right
Several pieces of the design worked exactly as intended:
- Otigen's safety defaults caught a class of contract bugs at compile time that would have been runtime failures in EVM.
- Compile-time access-list inference enabled the parallel scheduler to run non-conflicting transactions concurrently, a real performance win.
- The wide-register file was clean for 256-bit operations.
- The AOT compiler produced native code via Cranelift; benchmarks showed 10× speedup on tight ALU loops vs the interpreter.
- The wright toolchain offered a Foundry-quality developer experience.
The engineering work was real. The design was coherent. The team built it carefully.
What went wrong
Two things, accumulating over time:
One — the maintenance commitment was not one-shot
Building a language is a permanent commitment, not a one-time deliverable. Toolchain churn (Cranelift API updates breaking the AOT), feature requests from authors, security advisories, fuzzing, audit prep, documentation, IDE support — all of it had to be sustained continuously. The language was a parallel track of work that competed with the rest of the protocol for attention.
Two — the speed argument did not hold
The case for keeping a custom VM rested on the assumption that custom-AOT would outperform a general-purpose WASM runtime. Investigation showed otherwise:
- Both pyde-aot and wasmtime use the same Cranelift backend.
- WebAssembly is the workload Cranelift was originally optimized for.
- Our Otigen front-end was newer, less battle-tested, less fuzzed.
- Direct benchmarks showed wasmtime-AOT throughput in the same range as pyde-aot.
- On storage-bound workloads (the workload shape that matters for blockchain TPS), the AOT-vs-interpreter advantage collapsed to roughly 1× regardless of which AOT was running.
Measured numbers from the existing PVM stack on commodity hardware:
| Workload | PVM Interpreter | PVM AOT | AOT speedup |
|---|---|---|---|
| ALU dispatch | ~279M instr/sec | ~2.9B instr/sec | 10.4× |
| DEX swap | ~27M swaps/sec | ~100M swaps/sec | 3.7× |
| Token transfer | ~231K tps | ~243K tps | 1.05× (storage-bound) |
Token transfer — the canonical real-world workload — showed no meaningful AOT advantage because RocksDB IO dominates. WASM-AOT sits in the same range as PVM-AOT on the same backend. The custom VM was not faster on the workloads that matter.
What we learned
The lessons that survived the pivot intact, expressed now in the WASM-era architecture:
-
The VM is not the bottleneck. Real blockchain throughput is signature verification + IO + consensus + network bandwidth, in roughly that order. The VM is the fifth contributor. A 10% VM-level slowdown is invisible to TPS.
-
Sandboxing, determinism, gas semantics matter. All three. The WASM execution layer enforces them via wasmtime's feature-flag config, fuel-based metering, and deploy-time validation. The Otigen-era discipline about these properties carried forward.
-
Author safety is a property of host functions, not language syntax. Reentrancy guards, checked arithmetic, type-safe storage access — all of these can be expressed as patterns in the WASM host-function ABI and the binding generators, without requiring authors to learn a new language. The current
otigentoolchain (the binary; same name, new role) emits language-specific bindings that preserve these guarantees in Rust, AssemblyScript, Go, and C. -
Compile-time access lists work, regardless of source language. The current architecture preserves access-list-inferred parallel scheduling; the lists are now produced by the binding generators from the
otigen.tomlstate schema rather than by the Otigen compiler. Same property, different surface. -
A custom language costs more than its benefit returns. The language was not Pyde's differentiator. The work spent on it was work not spent on the post-quantum consensus + crypto + state stack that actually is the differentiator. The pivot redirected that work.
What survived
A lot, in fact:
- The safety properties Otigen aimed for — reentrancy guards, checked arithmetic, typed storage, no
tx.origin— are preserved in the host-function ABI and the binding generators. - The compile-time access-list inference is preserved (now produced by the binding generators from
otigen.toml). - The state model (JMT, PIP-2 clustering, dual-hash) was already architecturally separate from the VM; no changes needed.
- The wave model, gas accounting, threshold encryption, and all the consensus-side properties were preserved without change.
- The otigen name itself — repurposed for the developer toolchain, where it now describes the role of "making the ergonomics layer feel coherent and opinionated."
The pivot was localized to the VM and the language. Everything around it stayed.
Where the original material lives
- The otigen-book — the canonical reference for the Otigen language. Preserved as a published historical artifact at
pyde-net/otigen-bookwith a pivot-notice preface explaining the current status. - otic compiler source —
pyde-net/oticrepo, archived (read-only). - wright toolchain source —
pyde-net/wrightrepo, archived (read-only). - pyde-vm and pyde-aot crate source —
archive/crates/pvm/andarchive/crates/aot/in the umbrella repo, preserved with git history. - Original Otigen-era documentation —
archive/more broadly contains the pre-pivot READMEs, design notes, and benchmark plans. - Benchmark numbers — see the bench files in
archive/crates/pvm/benches/andarchive/crates/aot/benches/. The numbers used in this document and in the preface were captured by running those benchmarks one final time before archival.
Reading on
- 01 — The HotStuff Consensus Era — the first pivot.
- Chapter 3: Execution Layer (WASM) — the current execution model.
- Chapter 5: Otigen Toolchain — the new role for the Otigen name.
- Preface: The Pivot — the narrative version of both pivots.
03 — Running the Pivot-Era Benchmarks
This document is the reproducer for the benchmark numbers cited in the pivot preface and in 02 — The Otigen Language Era.
The benchmarks measure the pre-pivot PVM execution layer (the now-retired pyde-vm interpreter and pyde-aot Cranelift-AOT compiler) in isolation. The benchmark code lives in the archive repository at archive/crates/pvm/benches/ and archive/crates/aot/benches/ (preserved after engine cleanup). You can run it today on any machine that has Rust installed.
The point of running these is not to validate Pyde TPS. The point is to see for yourself the relationship between interpreter throughput, AOT throughput, and storage-bound real-world workloads — the relationship that drove the WASM-pivot decision. The numbers favor WASM because they show that on storage-bound workloads (the ones that determine real chain TPS), the AOT advantage collapses, which means the VM choice does not move the needle.
Reference machine for the numbers in the book
| CPU | Apple M4 Pro |
| Cores | 14 physical / 14 logical |
| RAM | 24 GB |
| OS | macOS 26.3.1 |
| Rust toolchain | stable (any recent stable release works) |
If your machine is faster, you will see higher numbers. If slower, lower. The ratios (AOT-vs-interpreter speedup, storage-bound vs compute-bound) should hold across hardware.
Prerequisites
You need:
- A clone of the
pyde-net/archiverepository (where the retired pre-pivot crates live). - A stable Rust toolchain.
rustup install stableif you do not have it.
That is all. No extra build tools, no test fixtures to download.
Step by step
# 1. Get to the archive workspace.
cd <your-pyde-checkout>/archive
# 2. Run the PVM interpreter benchmark.
cargo bench -p pyde-vm --bench interpreter_bench
Expected output shape:
=== 0236: Interpreter throughput ===
--- ALU dispatch (no memory, no storage) ---
Loop iterations: 100000
Instructions/run: 800005
Runs: 100
Total time: ~270-300ms (depends on CPU)
Throughput: ~280-300 million instructions/sec
Latency: ~3-4 ns/instruction
--- ALU dispatch (with trace recording) ---
Throughput: ~310-340 million instructions/sec
(slightly faster than no-trace by design — see bench comments)
=== 0237: Token transfer execution time ===
--- Token transfer: setup cost ---
Latency: ~2-3 µs/setup
--- Token transfer: execution only ---
Throughput: ~220-240 thousand transfers/sec (execution only)
Latency: ~4-5 µs/transfer
--- Token transfer: full lifecycle ---
Throughput: ~140-160 thousand transfers/sec
Latency: ~6-7 µs/transfer
# 3. Run the AOT-vs-interpreter benchmark.
cargo bench -p pyde-aot --bench aot_bench
Expected output shape:
=== AOT vs Interpreter throughput ===
Interpreter: ~280 million instr/sec
AOT: ~2.9 billion instr/sec
Speedup: ~10x (compute-bound)
=== AOT Token Transfer ===
Interpreter (exec only): ~230 thousand transfers/sec
AOT (exec only): ~240 thousand transfers/sec
Speedup: ~1.0x (storage-bound — this is the point)
=== AOT DEX Swap (constant-product AMM) ===
Interpreter: ~27 million swaps/sec
AOT: ~100 million swaps/sec
Speedup: ~3.7x (mixed compute + state)
=== AOT compilation time ===
4 instructions: ~50 µs
16 instructions: ~100 µs
64 instructions: ~250 µs
256 instructions: ~950 µs
That is everything. Two cargo-bench invocations, two text reports.
What the numbers mean — and what they don't
These are single-thread micro-benchmarks of the execution layer in isolation. There is no consensus running, no network, no parallel scheduling, no real RocksDB IO under sustained write pressure. They measure how fast one VM runs one workload on one thread.
What you should take from them:
- AOT crushes interpreter on tight compute (10× on ALU loops, 3.7× on AMM math). Cranelift is doing real work.
- AOT advantage collapses on storage-bound workloads (token transfer: 1×). This is the workload shape that dominates real blockchain throughput. The VM is not the bottleneck for real applications; storage IO is.
- The interpreter is already fast, around 280 million instructions per second on this hardware. Cold-cache execution paths in production do not have catastrophic latency.
What you should not take from them:
- These are not Pyde's TPS numbers. Full-chain TPS depends on consensus latency, signature verification throughput, network bandwidth, the parallel scheduler, and disk IO in addition to VM execution. The realistic v1 target of 10–30K plaintext TPS on commodity hardware reflects all of those layers combined, not just the VM.
- These do not include parallel execution. Each benchmark above runs one workload on one thread. The production scheduler runs many workloads in parallel via static access lists + Block-STM speculation; that compounds throughput but is measured separately by the full-chain harness, not here.
- These do not separate memory reads from memory writes, or from disk IO. The token-transfer benchmark exercises storage IO end-to-end as a single number; it does not isolate "Sload cost" from "Sstore cost" from "leaf-hash recomputation cost." That level of decomposition is the job of the per-component micro-benchmark suite (in flight; see below) and the full-chain performance harness.
More detailed benchmarks (in flight)
The benchmarks above are deliberately simple — they were enough to drive the pivot decision. A more sophisticated suite is part of the planned performance harness work, covering:
- Per-host-function micro-benchmarks — measuring the cost of each WASM host function (sload, sstore, transfer, threshold_*, hashing primitives, etc.) in isolation, so the gas-cost table can be calibrated against real hardware.
- Sequential vs parallel execution — measuring how the access-list-driven parallel scheduler scales with core count on workloads with various access-conflict ratios.
- Memory read vs memory write vs disk IO — splitting state-layer cost by category, so the JMT + RocksDB + write-back cache (PIP-4) stack can be profiled independently.
- Workload mixes — realistic blends of transfer / token-op / DEX / NFT-mint / encrypted txs, with the realistic-mix fraction tracked over time.
- Multi-region full-chain TPS — the end-to-end measurement with consensus, network, and IO all under load.
Those benchmarks live with the performance harness, not in the engine bench files. See the Performance Harness document for the full testing methodology, what's planned, and the "claim 1/3 of measured peak" discipline that governs how numbers are published.
What you can do with this guide
- Reproduce the pivot-decision numbers on your own hardware — see the ratios for yourself.
- Sanity-check the WASM-pivot reasoning — confirm that storage-bound workloads neutralize the AOT advantage, the empirical observation that drives the "VM choice does not move TPS" claim.
- Establish a baseline for comparing future WASM-execution numbers — once the WASM execution layer ships, equivalent benchmarks can be run against it; the numbers should sit in the same ballpark (within ~10%) per the pivot's expected outcome.
Where the benchmark code lives
| Benchmark | Source |
|---|---|
interpreter_bench | archive/crates/pvm/benches/interpreter_bench.rs |
aot_bench | archive/crates/aot/benches/aot_bench.rs |
| (future) WASM-equivalent benches | wasm-exec/benches/ in the fresh post-pivot engine repo (to be added) |
| (future) host-function micro-benches | same crate |
| (future) full-chain harness | separate repo (planned) |
The benchmark files live in the archive repository under archive/crates/pvm/benches/ and archive/crates/aot/benches/ — preserved with git history intact, runnable indefinitely. When the WASM execution layer ships in the freshly-cut post-pivot engine repo, equivalent benchmarks will be added under wasm-exec/benches/ so the same workload shapes can be measured on the WASM stack for comparison.
Reading on
- Preface: The Pivot — narrative context for these numbers.
- 02 — The Otigen Language Era — the full design record for the system being benchmarked.
- Performance Harness — the multi-layer testing methodology that succeeds these micro-benchmarks.
- Chapter 3: Execution Layer — the WASM execution architecture that replaces what's being measured here.
Architecture Overview
System Architecture
Pyde is a monolithic Layer 1 — consensus, execution, and state in a single binary. Validators and full nodes run the same pyde process; role differentiation is configuration (whether the node stakes, whether it joins the active committee, whether it serves RPC).
┌─────────────────────────────────────────────┐
│ Application Layer │
│ WASM smart contracts, dApps, wallets, RPC │
├─────────────────────────────────────────────┤
│ Execution Layer │
│ WebAssembly (wasmtime + Cranelift AOT), │
│ Block-STM, hybrid access-list scheduler │
├─────────────────────────────────────────────┤
│ State Layer │
│ Jellyfish Merkle Tree (JMT), dual-hash │
│ Blake3 + Poseidon2 per node, PIP-2 clusters │
├─────────────────────────────────────────────┤
│ Consensus Layer │
│ Mysticeti DAG, anchor selection, finality │
├─────────────────────────────────────────────┤
│ Cryptography Layer │
│ FALCON-512, Kyber-768 threshold, DKG │
├─────────────────────────────────────────────┤
│ Network Layer │
│ libp2p + QUIC, Gossipsub, worker/primary │
└─────────────────────────────────────────────┘
Worker / Primary Split (Narwhal Pattern)
Within each validator, the consensus role is split:
- Workers (N processes per validator): handle transaction ingress, build batches of incoming transactions, gossip batches peer-to-peer with other validators' workers
- Primary (one process per validator): handles consensus — produces vertices each round, gathers parent references, signs state roots
This separation decouples high-volume data dissemination from low-volume consensus structure. Transactions travel the network exactly once (via worker gossip); consensus vertices stay tiny (carry only batch hashes by reference).
┌────────────────────────────────────────────────────┐
│ Validator Process │
│ │
│ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ Workers │ │ Primary │ │
│ │ (1 or more) │◄───┤ - Produces vertices │ │
│ │ │ │ - Tracks DAG │ │
│ │ - Tx ingress │ │ - Signs state roots │ │
│ │ - Build │ │ - Runs DKG ceremonies │ │
│ │ batches │ │ - Executes WASM │ │
│ │ - Gossip │ └──────────────────────────┘ │
│ │ batches │ │
│ └──────────────┘ │
└────────────────────────────────────────────────────┘
Workers can be scaled independently of the primary. A validator with high incoming traffic can run 4-8 workers; a quieter validator can run 1.
Consensus: Mysticeti DAG
Pyde's consensus is a Mysticeti-style DAG protocol. Every round (~150ms), each committee member's primary produces exactly one vertex. The vertex contains:
- Batch hashes (data layer references)
- 85+ parent vertex hashes (consensus structure, from prior round)
- State root signatures (attestations on recent commits)
- Anchor attestation (prior round's anchor vertex hash)
- Decryption shares (piggybacked partial decryptions)
- FALCON signature
Vertices form a Directed Acyclic Graph: parents must be strictly from prior rounds. This is purely a consensus structure; transaction data lives in batches referenced by hash.
Each round has a deterministically-selected anchor:
anchor_member = Hash(beacon, round, recent_state_root) mod 128
When the anchor vertex collects sufficient support from later rounds (Mysticeti 3-stage support), a commit fires. ~95% of rounds commit successfully; ~5% skip (next round absorbs the skip).
End-to-end commit latency: ~500ms median.
Execution: WebAssembly + Hybrid Scheduler
After consensus commits a wave (canonical ordered transactions), the execution layer:
- Threshold decryption for encrypted transactions (≥85 partials combined)
- Hybrid scheduler partitions decrypted transactions into parallel groups:
- Static access lists (Solana-style) for functions with compile-time-known accesses, derived from each contract's declared state schema
- Block-STM speculation (Aptos-style) for functions with dynamic accesses
- wasmtime executes WASM modules in canonical order, applying state diffs in parallel where safe. Smart contracts compile from Rust, AssemblyScript, Go, or C/C++ to WASM; runtime is wasmtime with Cranelift AOT and fuel-based gas metering.
- State root computed — dual-hash (Blake3 + Poseidon2) per JMT node
- Committee FALCON-signs state root (piggybacked on next vertices)
- Finality when ≥85 state root signatures collected
State: Jellyfish Merkle Tree
Account state and contract storage are stored in a Jellyfish Merkle Tree (JMT) — radix-16, path-compressed. Compared to a fixed-depth-256 Sparse Merkle Tree:
- ~5-10 nodes touched per state operation (vs ~256)
- Substantial I/O savings at high TPS
- Same authentication properties (Merkle commitment, inclusion / exclusion proofs)
- Production-proven (Diem, Aptos)
State commitment is dual-rooted:
- Blake3 root: fast native verification (committee + validators)
- Poseidon2 root: ZK-circuit-friendly (future light clients, validity proofs)
Cryptography Layer
Three primitives form the cryptographic foundation:
FALCON-512 (Signatures)
NIST FIPS 206 standard. Used for: user tx authorization, vertex production, state root attestations, decryption share authentication. 666-byte signature, ~80μs verification.
Kyber-768 Threshold (Encryption)
NIST FIPS 203 standard with threshold variant. Per-epoch public key from DKG; ≥85 partials decrypt any ciphertext. Enables encrypted-mempool MEV resistance.
Poseidon2 + Blake3 (Hashing)
Hybrid layered: Blake3 for high-volume native paths (JMT internals), Poseidon2 for ZK-bearing paths (state root commitment exposed to future ZK proofs, address derivation, FALCON sig hashing inside ZK circuits).
Network Layer
- Transport: QUIC over UDP (no HOL blocking, TLS 1.3 built-in, mature in Rust via quinn). TCP fallback.
- P2P library: libp2p (Rust) — mature, audited, used by Ethereum/Filecoin/Polkadot
- Peer discovery: layered (hardcoded → DNS → on-chain validator registry → PEX → cache). No DHT.
- Gossip: Gossipsub with per-topic meshes
- DoS protection: 4-layer (connection / message / peer-scoring / application)
- Committee defense: sentry node pattern (Cosmos-style)
Committee NIC requirement at v1's honest throughput target (10-30K plaintext TPS, 0.5-2K encrypted) is ≥500 Mbps. Higher-throughput regimes are post-mainnet scaling work; the v1 number is what mainnet hardware is sized against.
Account Model
Accounts hold:
- nonce (8 bytes)
- balance (16 bytes, u128)
- gas_tank (16 bytes — pre-deposited gas for encrypted submission)
- auth_keys (variable: Single | Multisig | Programmable)
- code_hash (32 bytes, for contracts)
- storage_root (32 bytes, JMT subtree for contract storage)
- key_nonce (4 bytes, FALCON key rotation counter)
Native multisig at v1 — AuthKeys::Multisig(M, [pubkey_1, ..., pubkey_N]) with max 16 signers. Better than Gnosis Safe contract-multisig (Ethereum), which reimplements the same logic with subtle bugs across projects.
Programmable accounts and session keys ship post-mainnet. v1 reserves the Programmable enum variant so contracts written today survive the upgrade without rewriting.
16-slot nonce window — accounts can have up to 16 transactions in-flight out-of-order within the window. Decouples user-level submission from consensus-level execution ordering.
Transaction Lifecycle
1. Wallet constructs tx
2. Wallet → RPC: pyde_estimateAccess(tx) → returns gas_estimate + access_list
3. Wallet attaches access_list to tx
4. Wallet FALCON-signs tx hash
5. (Optional) Wallet encrypts signed_tx + access_list with epoch Kyber PK
6. Wallet submits: pyde_sendRawTransaction or pyde_sendRawEncryptedTransaction
7. RPC node validates wire format, forwards to nearest worker
8. Worker (plaintext) verifies sig, batches, gossips
9. Primary produces vertex, gossips
10. Commit fires (Mysticeti, sub-second target): anchor selected, subdag walked, canonical order emitted
11. (Encrypted) threshold decryption ceremony per encrypted tx (batches contain a mix of plaintext + encrypted txs)
12. wasmtime executes WASM modules in canonical order
13. JMT updates (dual-hash per node), state root signed
14. Finality declared (≥85 state root sigs)
Cross-Chain (Post-Mainnet)
Cross-chain interactions happen through a permissionless parachain layer — operators implement a Pyde-published specification, stake PYDE, follow protocol rules, and earn gas fees from contracts that call them via the cross_call! macro.
The protocol-level surface (cross_call! macro, HardFinalityCert primitive, unified gas model) is settled at v1 genesis. The actual parachain layer ships post-mainnet.
Three-Tier Node Model
| Tier | Stake | Committee Role | Earns |
|---|---|---|---|
| Committee validator | Yes, large | Active (1 of 128) | Activity rewards + pool yield + inflation |
| Non-committee validator | Yes, smaller | Stake-only, waiting selection | Pool yield + inflation |
| RPC node | No | None | Off-chain RPC fees (market-set) |
RPC providers (Infura/Alchemy analog) fit Tier 3 — no stake, no slashing risk.
Key Differentiators
| Ethereum | Solana | Sui | Pyde | |
|---|---|---|---|---|
| Post-Quantum | Migration 5+ years | No plan | No plan | Default at genesis |
| MEV resistance | Auction (PBS) | Proposer extracts | Some via Mysticeti | Structurally impossible |
| Finality | 12-15s | 400ms | 390ms | ~500ms |
| Commodity validator | Possible | No (12+ cores) | No (datacenter) | Yes (any validator awaiting committee selection) |
| Smart contract language | Solidity | Rust/Anchor | Move | Any wasm32 target (Rust, AssemblyScript, Go, C/C++) |
| Account abstraction | Retrofit (ERC-4337) | None native | Limited | Native (v2) |
| Cross-chain | Bridges ($3B+ hacked) | Bridges | Bridges | Permissionless parachain (v2) |
| ZK readiness | Retrofit ongoing | Limited | Limited | Architecture ready (v2) |
Next Chapters
- Chapter 3: Execution Layer — wasmtime runtime, host function ABI, Cranelift AOT, fuel-based gas, determinism boundary
- Chapter 4: State Model — JMT details, dual-hash strategy, PIP-2 clustering
- Chapter 5: Otigen Toolchain — the developer-facing binary (build, deploy, wallet, ABI extraction, per-language attribute declaration)
- Chapter 6: Consensus — full Mysticeti DAG specification
- Chapter 7: State Sync & Chain Halt — operational protocols
- Chapter 8: Cryptography — FALCON, Kyber, Poseidon2, DKG, threshold details
- Chapter 9: MEV Protection — threshold encryption + commit-before-reveal architecture
Chapter 3: Execution Layer
Pyde's execution layer is WebAssembly via wasmtime, with ahead-of-time compilation through Cranelift. Smart contracts and parachains run in sandboxed wasmtime instances, interacting with the chain through a fixed set of host functions that the engine implements in Rust.
This chapter covers the runtime architecture, the host function ABI surface, how compilation and caching work, gas metering, the determinism boundary, and the performance properties of the layer.
For context on why Pyde uses WebAssembly rather than a custom virtual machine, read the preface (The Pivot).
3.1 Why WebAssembly
WebAssembly was designed to be a compilation target: a small, well-specified, sandboxed instruction set that any source language can lower into and any runtime can execute deterministically. For Pyde, this gives us four properties simultaneously, none of which a custom VM could deliver without years of additional work.
-
Universal language support. Authors write contracts in whatever language they already know. Rust is the primary path; AssemblyScript, Go (via TinyGo), and C/C++ (via clang's
--target=wasm32) are first-class alternatives. The chain does not impose a language preference. -
Battle-tested runtime. Wasmtime is maintained by the Bytecode Alliance, used in production at Fastly, Microsoft, and Shopify, continuously fuzzed under adversarial workloads, and audited as a security-critical system. Pyde inherits this hardening at zero engineering cost.
-
Strong sandbox. WebAssembly's linear memory model and structured control flow eliminate entire categories of vulnerabilities (buffer overflows, control-flow hijacks, type confusion). The validation step at module load rejects any malformed binary before it can run. Importing forbidden functions (network, filesystem, threads) is gated at deploy time.
-
ZK-ready path. Active research on zero-knowledge proving of WebAssembly execution (zk-WASM) is converging on practical provers within a multi-year horizon. Pyde's contract bytecode is positioned to benefit from this without re-tooling — when zk-WASM provers mature, they slot in as an attestation layer over execution that has already happened.
The price for these properties: a small overhead on the order of 5-15% relative to a hand-tuned custom VM on tight compute loops, vanishing entirely for storage-bound workloads where the VM is not the bottleneck. The performance section at the end of this chapter quantifies this with real numbers.
3.2 Runtime Architecture
Execution sits inside the wasm-exec crate of the engine workspace. The crate exposes a single WasmExecutor type that owns the wasmtime engine, the compiled-module cache, and the host function bindings. The transaction pipeline calls into WasmExecutor per invocation; the executor handles the rest.
┌────────────────────────────────────────────────────────────┐
│ Engine transaction pipeline │
│ (mempool → access-list scheduler → execution dispatch) │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌───────────────┐
│ WasmExecutor │ ← single per node, owned by node
└──────┬────────┘
│
┌─────────────┼──────────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌─────────────────┐
│ wasmtime │ │ Module │ │ Host functions │
│ Engine │ │ cache │ │ (host_fns.rs) │
│ (Crane- │ │ (per- │ │ — sload │
│ lift) │ │ contract) │ │ — sstore │
└─────────┘ └──────────┘ │ — transfer │
│ — emit_event │
│ — threshold_* │
│ — hash_* │
│ — cross_call │
│ — ... │
└─────────────────┘
│
▼
┌─────────────────┐
│ JMT state, fee │
│ accounting, │
│ event log, etc. │
└─────────────────┘
WasmExecutor responsibilities:
- Hold the wasmtime
Engine(singleton, configured at startup with deterministic feature flags). - Cache compiled
Modules by contract address (compile once, reuse across invocations). - Instantiate per-invocation
Stores with isolated linear memory and the current execution context. - Wire host function calls through the linker.
- Track fuel consumption (gas).
- Handle trap conditions and propagate them as transaction failures.
Engine configuration (set once at node startup):
#![allow(unused)] fn main() { let mut config = wasmtime::Config::new(); config.strategy(wasmtime::Strategy::Cranelift); config.cranelift_opt_level(wasmtime::OptLevel::Speed); config.consume_fuel(true); config.epoch_interruption(true); // Determinism enforcement: config.cranelift_nan_canonicalization(true); config.wasm_threads(false); config.wasm_simd(false); config.wasm_relaxed_simd(false); config.wasm_reference_types(false); config.wasm_bulk_memory(true); // safe, deterministic, useful config.wasm_multi_memory(false); config.wasm_memory64(false); config.wasm_function_references(false); config.wasm_gc(false); config.wasm_component_model(false); // (No WASI imports allowed; not enabled at all.) }
This config produces deterministic execution suitable for consensus: every validator running the same module on the same input produces bit-identical state changes and identical fuel consumption.
3.3 The Host Function ABI
Smart contracts cannot directly access state, signatures, or anything outside their sandbox. They reach the chain through host functions — Rust functions registered with wasmtime's linker that contracts call by name. The full set of host functions is the Host Function ABI, versioned and documented in the canonical Host Function ABI v1.0 Specification.
This section gives the conceptual surface; the spec gives the binary signatures.
Storage:
sload(slot_hash) -> value— read a 32-byte slot.sstore(slot_hash, value)— write a 32-byte slot. Costs increase for new slot allocations.sdelete(slot_hash)— explicitly delete a slot (lower cost thansstore; no refund in v1, per Chapter 10).
Balances and transfers:
balance(addr) -> u128— read an account's PYDE balance.transfer(to_addr, amount)— move PYDE from the caller toto_addr. Fails if insufficient balance.
Execution context:
caller() -> addr— the address that invoked the current call.origin() -> addr— the externally-owned address that initiated the transaction. (Deliberately distinct fromcaller()to avoid thetx.originfootgun from Ethereum.)block_height() -> u64,wave_id() -> u64,block_timestamp() -> u64.chain_id() -> u64.
Events:
emit_event(topic, data)— append a 32-byte topic + opaque bytes payload to the transaction's event log. Each event is buffered in the current overlay (per-tx, per-cross-call); reverted (sub-)calls' events are discarded. At wave commit, all surviving events are committed viaevents_root(Merkle tree) +events_bloomin the wave commit record. Recommended encoding fordatais Borsh; topics are typicallyBlake3(canonical_event_signature). Full storage / indexing / subscription mechanics: see Host Function ABI Spec §15.
Hashing primitives:
hash_keccak256(input) -> hash32— for compatibility with cross-chain interfaces.hash_blake3(input) -> hash32— fast general-purpose hashing.hash_poseidon2(input) -> hash32— ZK-friendly hashing (used in state commitments).
Post-quantum cryptography:
threshold_encrypt(plaintext) -> ciphertext— encrypt a payload under the current committee's threshold key. Available to parachains only.threshold_decrypt(ciphertext) -> plaintext— combine pre-collected committee shares to decrypt. Available to parachains only.falcon_verify(pubkey, message, signature) -> bool— verify a FALCON-512 signature.
Cross-contract calls:
cross_call(target, fn_name, calldata, value, gas_limit, ...)— synchronous call into another contract. Sub-call runs in a nested overlay; merges on success, discards on trap.cross_call_static(target, fn_name, calldata, gas_limit, ...)— view-only sub-call. Free for the caller (only the 50-gas dispatch base charged); bounded by a per-callVIEW_FUEL_CAP(default 10M fuel ≈ 3ms commodity).delegate_call(target, fn_name, calldata, gas_limit, ...)— execute target's code in the caller's storage context.self_address()andcaller()preserve outer-call identity. For proxy / upgradeable patterns.
Randomness:
beacon_get() -> hash32— current wave's committee-derived VRF beacon (XOR of all members' beacon shares). Deterministic across validators, publicly readable.
Gas:
consume_gas(amount)— explicit metering for operations the runtime cannot price automatically (used by binding generators for collection-traversal patterns).
Forbidden by design:
- Network calls (any kind).
- Filesystem access.
- System clock (use
block_timestampinstead — deterministic). - Non-deterministic entropy (use a VRF-based host function when randomness is needed).
- Direct RocksDB access (everything routes through
sload/sstore).
The deploy-time validator rejects any WASM module whose import section references functions outside this allowlist. Hard-enforced.
3.4 Compilation and Caching
The wasmtime engine compiles WebAssembly bytecode to native machine code via Cranelift. Compilation is expensive (tens to hundreds of milliseconds per contract); execution after compilation is fast. The cache strategy makes this acceptable.
Compilation lifecycle:
Deploy time
│
├─ Wasm bytes submitted with deploy transaction
├─ Engine validates bytes (wasmtime::Module::validate)
├─ Engine rejects forbidden imports
├─ Engine compiles bytes via Cranelift → Module
├─ Engine serializes Module to bytes (Module::serialize)
├─ Engine stores both source bytes AND serialized Module in state
└─ Contract is live
Subsequent invocations
│
├─ Engine looks up contract address in module cache
│ ├─ Hit: use cached Module immediately
│ └─ Miss: read serialized Module from state, deserialize, cache it
├─ Engine creates per-invocation Store with execution context
├─ Engine instantiates Module against Store (sub-millisecond)
└─ Engine calls the entry function
Cache properties:
- In-memory cache keyed by contract address.
- LRU-style eviction with a configurable size budget (default ~256 modules resident).
- Serialized modules persist on disk so cold validators warm quickly.
- On contract upgrade, the cache entry is invalidated; the new module is compiled and cached on next use.
Per-contract compilation cost (measured on commodity hardware against PVM-era proxies; WASM-era numbers to be re-measured):
- A simple contract (~100 instructions): ~10ms.
- A medium contract (~1000 instructions): ~50-100ms.
- A large contract (~10000 instructions): ~500ms-1s.
These costs are paid once per contract per node restart, then amortized across all subsequent invocations.
3.5 Gas Metering
Pyde uses wasmtime's fuel mechanism for gas accounting. Fuel is a per-execution budget; every WebAssembly instruction consumes a configurable amount of fuel, and execution traps when fuel reaches zero. Host function calls also consume fuel manually (charged by the host based on operation cost — sstore is heavier than add, for example).
Gas-to-fuel mapping: At node startup, the engine establishes a deterministic mapping from gas units (the chain-level metering unit) to wasmtime fuel units. The mapping accounts for:
- Per-instruction baseline cost (each WASM instruction costs a fixed amount of fuel).
- Per-host-function cost (specific to each host function, defined in the ABI gas table).
- Per-byte storage costs (
sloadreads,sstorewrites, allocation surcharge for new slots). - Per-byte event emission cost.
A transaction declares its gas budget at submission; the engine converts that to fuel and runs the contract with that fuel limit. The fuel actually consumed is converted back to gas for the transaction receipt.
Why fuel and not opcode-counting: Fuel is built into wasmtime's Cranelift backend. Every basic block is instrumented to decrement a fuel counter; when the counter goes negative, execution traps with an out-of-fuel error. The instrumentation is efficient enough not to dominate execution time. Implementing custom opcode-counting on top of wasmtime would be slower and add maintenance burden for no functional gain.
Charging model — no refunds in v1:
The ingress check confirms balance ≥ gas_limit × base_fee, but only gas_used × base_fee is actually debited at execution time. Unused fuel costs the sender nothing — it is never debited and therefore never refunded. Pyde v1 has no operation-level gas refunds either (no sstore_refund, no sdelete refund). See Chapter 10 §10.1 for the full charging pipeline and the EIP-3529 reasoning.
3.5b Per-Transaction Execution Isolation
Every transaction executes against an overlay layered on top of the shared DashMap state cache. The overlay isolates the tx's writes and its emitted events so a revert can throw them away without affecting other txs in the same wave.
Per-tx isolation:
Before tx execution:
tx_overlay: {
state_writes: HashMap<SlotHash, Vec<u8>>,
events: Vec<EventRecord>,
} = empty
During execution:
Reads (state):
1. check tx_overlay.state_writes (any writes this tx made)
2. check dashmap (prior committed-in-this-wave writes from other txs)
3. check state_cf (current persistent state on disk)
Writes (state):
go into tx_overlay.state_writes only (not dashmap yet)
emit_event:
append to tx_overlay.events only
On successful completion:
merge tx_overlay.state_writes into dashmap (marking entries Dirty)
append tx_overlay.events to the wave's canonical events list
generate success receipt
drop tx_overlay (memory freed)
On trap (revert):
discard tx_overlay entirely — state AND events
state unchanged in dashmap
no events emitted to the wave's list
generate revert receipt with reason
sender still pays gas_used × base_fee (see Chapter 10)
Events follow the same merge/discard discipline as state writes. A reverted (sub-)call's events are discarded along with its state writes — the chain never sees events from a path that didn't commit. The wave's final events list (committed via events_root + events_bloom; see Host Function ABI Spec §15) is the topmost overlay's events buffer at wave commit time.
Why no separate undo log: failed writes never landed in shared state. Dropping the overlay throws them away. Simpler than journaled undo.
Nested cross-calls: when tx A calls contract B which calls contract C, each call gets its own overlay layered on top:
A's overlay
↓
B's overlay (reads check B's overlay first, then A's, then dashmap, then state_cf)
↓
C's overlay (reads check C's, then B's, then A's, then dashmap, then state_cf)
Inner call succeeds → merge inner overlay into parent overlay
Inner call traps → drop inner overlay; parent continues
Outer tx traps → drop outer overlay (including all merged inner state)
This is standard transactional-memory layering. wasmtime's host functions are aware of the active overlay and route reads/writes through it.
Memory bounds on the overlay
The overlay can grow during a tx, but is bounded by two factors:
-
Gas budget. Every write into the overlay charges fuel via
sstore. A tx withgas_limit = 10_000_000can write at most ~50K slots (varying by slot size). Author can't write infinitely without paying. -
Linear memory cap. wasmtime's per-instance linear memory is capped (64MB default, configurable per chain release). Even if gas were infinite, the WASM module can't allocate beyond this cap.
Together: a tx can use up to (gas_limit / sstore_cost) × value_size of overlay memory, but capped by linear memory. We don't impose a separate "tx overlay memory cap" — gas + wasmtime config bound it.
3.6 The Determinism Boundary
For consensus to hold, every validator must produce bit-identical state changes when executing the same transaction. This requires deterministic execution at every layer.
Deterministic-by-default in WebAssembly:
- Integer arithmetic (well-specified, no platform-dependent behavior).
- Memory operations (bounds-checked, no undefined behavior).
- Control flow (structured, no goto, no jump tables that vary by platform).
Determinism risks WebAssembly admits, which we disable:
- Floating-point: most operations are deterministic by IEEE-754, but NaN bit patterns can vary. We enable
cranelift_nan_canonicalizationso NaN outputs are canonicalized identically across all validators. - Threads: non-deterministic by definition; we disable the threads proposal.
- SIMD: most SIMD is deterministic, but certain operations (relaxed SIMD) are not. We disable both the SIMD and relaxed-SIMD proposals for now; we may re-enable a deterministic-only SIMD subset in a future version.
- Reference types, GC, function references, component model: complexity surface we don't need yet, disabled.
Determinism risks the runtime introduces, which we control:
- Module compilation may produce different machine code on different platforms (different architectures, different Cranelift versions). We pin the wasmtime version per chain release and require validators to upgrade in coordinated forks. Cached serialized modules are not portable across versions.
- Fuel consumption per host function is defined in the gas table, identical across validators.
What contracts cannot observe:
- Wall-clock time. Use
block_timestamp(deterministic, set by consensus). - True randomness. Use a VRF-derived host function when randomness is required (deterministic per block, unpredictable beforehand).
- The host machine. No CPU info, no OS info, no environment access.
Deploy-time validation: Every contract's WASM is validated at deploy time against the determinism rules. Any module that imports a forbidden function, uses a disabled feature, or fails wasmtime's structural validator is rejected. The validation gate is non-negotiable — it prevents bad code from ever reaching consensus.
3.7 State Access from the Author's Perspective
Host functions are low-level: they take pointers + lengths into WASM linear memory and return raw bytes. Contract authors write the slot derivation themselves in their source language, following the PIP-2 slot layout described in Chapter 4: State Model. The otigen toolchain does NOT generate code; authors write a small helper module (or copy one from a canonical example) that turns ergonomic API calls into the right pyde_storage_read / pyde_storage_write host calls.
The pattern (in Rust):
#![allow(unused)] fn main() { // Author writes (or copies from the canonical example): // 1. Host function imports (one-time declaration): extern "C" { fn pyde_storage_read(slot_hash_ptr: *const u8, slot_hash_len: usize) -> i64; fn pyde_storage_write(slot_hash_ptr: *const u8, slot_hash_len: usize, value_ptr: *const u8, value_len: usize); fn pyde_poseidon2(input_ptr: *const u8, input_len: usize, out_ptr: *mut u8); } // 2. Contract-name prefix, derived once at startup: // (Rust patterns include lazy_static!, OnceCell, const fn — author's choice.) fn contract_addr_prefix() -> &'static [u8; 16] { /* ... */ } // 3. Discriminator constants from otigen.toml [state] section: const BALANCE_DISC: u8 = 0; // matches [state] balance.disc // 4. Slot derivation following PIP-2 layout (address[..16] || hash(disc||key)[..16]): fn balance_slot(addr: &[u8; 32]) -> [u8; 32] { let mut slot = [0u8; 32]; slot[..16].copy_from_slice(contract_addr_prefix()); let mut input = [0u8; 33]; input[0] = BALANCE_DISC; input[1..].copy_from_slice(addr); let mut inner = [0u8; 32]; unsafe { pyde_poseidon2(input.as_ptr(), input.len(), inner.as_mut_ptr()); } slot[16..].copy_from_slice(&inner[..16]); slot } // 5. Ergonomic accessor (author writes this small wrapper): fn read_balance(addr: &[u8; 32]) -> u128 { let slot = balance_slot(addr); let mut value = [0u8; 32]; unsafe { /* call pyde_storage_read, copy into value */ } u128::from_le_bytes(value[..16].try_into().unwrap()) } }
Where the hashing happens:
- The contract-name prefix (
contract_addr_prefix()) is computed once at startup using whatever caching pattern the author's language provides. Rust authors useOnceCell/lazy_static!/ aconst fnif possible. AssemblyScript uses a module-level constant initializer. Go usesinit(). C uses astatic constarray initialized at first call. After the first computation, it's free. - The discriminator (
BALANCE_DISC = 0) is a compile-time constant — never re-hashed. - The dynamic part (the
addrargument) is hashed at runtime — onepyde_poseidon2call per slot reference. That's the irreducible cost.
This is the same end-state as if otigen were generating bindings — same hash count at runtime, same memory layout, same gas profile. The difference: the author owns the code, can inspect it, can audit it, can replace pieces with optimized hot-path versions, and isn't dependent on a chain-team-maintained code generator. The canonical example projects in pyde-net/otigen ship one workable pattern per supported language as a starting point.
The same pattern adapts to AssemblyScript, Go (TinyGo), and C/C++ — each language has its own idioms for module-level constants, lazy initialization, and FFI to host functions. See pyde-net/otigen/examples/ for a working version in each language.
3.8 Performance Characteristics
The honest numbers, measured against PVM-era proxies (WASM-era numbers will replace these as benchmarks are re-run):
Compute-bound workloads (tight ALU loops):
- Wasmtime AOT runs within roughly 80-95% of native code on most workloads. Measured benchmarks on PVM-era code showed AOT throughput around 2.9 billion instructions per second for ALU dispatch; wasmtime-AOT sits in the same range because both use the same Cranelift backend.
- Interpreted execution (cold cache, no AOT yet) runs at roughly 10-30% of native. Pyde's WASM interpreter path is similar in throughput to the previous PVM interpreter measured at ~279 million instructions per second.
Storage-bound workloads (typical real-world smart contracts):
- The AOT-vs-interpreter advantage collapses. Token transfers measured around 231K tps interpreted and 243K tps AOT — essentially identical, because RocksDB IO dominates and neither the interpreter nor the AOT can speed it up.
- This is the workload shape that actually determines blockchain throughput. The VM choice barely affects it.
Module compilation:
- Sub-millisecond for small contracts.
- ~1 second for the largest realistic contracts.
- Paid once per contract per node startup, then cached forever.
End-to-end TPS: The realistic v1 target on commodity validator hardware is 10,000-30,000 plaintext TPS sustained, 500-2,000 encrypted TPS. These numbers come from the full-chain performance harness (consensus + execution + state + network), not from VM microbenchmarks alone. The VM is approximately the fifth-most-important contributor to that number, behind signature verification, network bandwidth, consensus latency, and disk I/O.
The "claim 1/3 of measured peak" discipline applies: published TPS numbers are derived conservatively from sustained measurement under realistic conditions, never from microbenchmark peaks.
3.9 Failure Modes and Traps
When a contract execution fails, it traps. The transaction reverts, no state changes persist, the sender pays gas up to the trap point.
Trap conditions:
- Out of fuel — exceeded the transaction's gas budget.
- Out of bounds — WASM linear memory access outside allocated range.
- Integer overflow (when checked arithmetic is requested by host function gating).
- Forbidden import attempt — caught at deploy, not at runtime; deploy fails instead.
- Stack overflow — wasmtime's configurable stack limit reached.
- Unreachable — the WebAssembly
unreachableinstruction was executed (typically Rust'spanic!()lowers to this). - Host function error —
sstoreto a write-locked slot,transferwith insufficient balance, etc.
Engine-level protections:
- Per-call wall-clock timeout (epoch interruption). Prevents a buggy contract from spinning forever even if fuel accounting is somehow bypassed.
- Per-call linear memory limit (capped well below host memory).
- Per-call stack depth limit.
Trap conditions are reported in transaction receipts as structured error codes, queryable by clients.
3.9b Native Transactions vs WASM Calls
Not every transaction invokes wasmtime. Pyde has a small set of native transaction types that the engine executes directly, without WASM overhead.
Native tx types (no wasmtime invocation)
- Transfer — move PYDE between two accounts; ~21,000 gas; engine handles balance update directly
- ValidatorRegister — stake-account-binding system tx
- ValidatorUnbond — initiate unbonding
- ValidatorRotateKey — FALCON key rotation
- ValidatorUnjail — exit jailed state after grace period
- Multisig — treasury / governance multisig spend
- Slashing — system-emitted from evidence
These all bypass wasmtime and execute as Rust code in the engine. They're cheaper, faster, and don't carry the per-tx WASM instantiation cost.
WASM tx types (wasmtime executes)
- ContractCall — invoke a function on a deployed WASM contract
- ContractDeploy — register new WASM bytes + ABI as a contract
- ParachainCall — invoke a function on a deployed parachain WASM (cross-call routing)
These instantiate the target module via wasmtime, call the entry function, execute under the per-tx overlay, and produce a receipt.
Why split this way
- Performance. Simple transfers don't need a sandbox or fuel metering — they're trivially provable state updates.
- Gas predictability. Native transfers have a fixed gas cost (~21K) known in advance; no fuel-counting needed.
- Common-case optimization. Simple value transfers are the most common tx type on any chain. Avoiding WASM overhead per-transfer materially improves end-to-end TPS for high-volume payment workloads.
WASM contracts that need to move value internally still call pyde_transfer as a host function, which does the same balance-update logic the native transfer does. Authors don't have to choose; the chain serves both paths.
3.10 Contract Lifecycle
Author writes contract → otigen build → .wasm + ABI
│
▼
Author runs otigen deploy
│
├─ Pays registration fee for name (ENS-style, see Account Model chapter)
├─ Pays owner deposit (forfeit on misbehavior)
└─ Submits deploy tx with .wasm bytes
│
▼
Engine validates module (validator, deterministic-features gate, import allowlist)
│
▼
Engine compiles via Cranelift, caches serialized module
│
▼
Engine writes (contract_address → wasm_hash, serialized_module, owner, deposit) to state
│
▼
Contract is live; callable by anyone holding its address or name
Upgrade path mirrors deploy but routes through governance for parachain contracts. Smart contracts (non-parachain) follow a simpler owner-only upgrade flow with grace periods to give users time to verify the new code.
3.11 Where the Code Lives
The WASM execution layer is implemented post-pivot in a fresh engine workspace that does not exist yet. The pre-pivot pvm and aot crates are preserved in pyde-net/archive for historical reference and bench comparison. The table below names the components and their planned crate layout once the fresh engine repo is cut.
| Component | Planned crate / file (post-pivot) |
|---|---|
| WasmExecutor entry point | wasm-exec/src/lib.rs |
| Host function implementations | wasm-exec/src/host_fns.rs |
| Module cache | wasm-exec/src/module_cache.rs |
| Fuel-to-gas mapping | wasm-exec/src/gas_meter.rs |
| Validation gate | wasm-exec/src/validate.rs |
| Deploy-tx processing | tx/src/deploy.rs |
| State binding code generators (per language) | otigen repo (otigen/crates/codegen-*) |
| Host Function ABI specification | companion/HOST_FN_ABI_SPEC.md |
3.12 Open Questions
These are tracked in the roadmap and resolved as the execution layer matures:
- Re-enabling deterministic SIMD. Pyde currently disables SIMD entirely. A deterministic SIMD subset (excluding relaxed operations) would benefit crypto-heavy contracts. Pending implementation work and conservative validation.
- WASM module hash-content-addressing. Two contracts with identical WASM bytes could share a single compiled module entry. Optimization opportunity; not blocking.
- zk-WASM proving integration. When zk-WASM provers reach production quality, slot one in as an optional execution attestation layer. Tracked as a v2/v3 direction in the roadmap.
- Hot-reload of compiled modules across version pins. Currently a wasmtime version bump invalidates the cache; coordinated upgrades are required. Hot-reload research may relax this.
3.13 Reading on
- Chapter 4: State Model — how
sloadandsstorereach the JMT. - Chapter 5: Otigen Toolchain — how authors interact with the execution layer through the developer tool.
- Chapter 6: Consensus — how execution outcomes commit to the chain.
- Chapter 8: Cryptography — what FALCON, Kyber, and Poseidon2 actually do, and how the host functions expose them.
- Preface: The Pivot — why the execution layer is WebAssembly rather than a custom VM.
Chapter 4: State Model
Every blockchain is a replicated state machine. Transactions transform state; consensus ensures every honest node agrees on the result. The quality of the state model decides how fast you commit, how cheap you sync, and how well you parallelize execution.
Pyde stores all state in a Jellyfish Merkle Tree (JMT), persisted in RocksDB, with hybrid hashing: Blake3 on high-volume native paths, Poseidon2 on ZK-bearing paths. The state commitment is dual-rooted — Blake3 for fast native verification by committee and validators, Poseidon2 for future ZK light clients and validity proofs.
The JMT replaces the fixed-depth Sparse Merkle Tree the project initially shipped — a swap made because JMT's radix-16 path compression delivers roughly 40× faster commits. Hybrid hashing was adopted post-pivot once the performance cost of running Poseidon2 over every internal JMT node became clear; Blake3 is ~50× faster on commodity CPUs without sacrificing the ZK-friendly properties where they matter (state root, address derivation, FALCON-sig-hashing inside circuits).
4.1 The Jellyfish Merkle Tree
The JMT is a radix-16 path-compressed Merkle tree. Each internal node has up to 16 children (one per nibble), and runs of single-child nodes are compressed into a single edge labelled with the shared key prefix. Empty subtrees are not materialized.
Why JMT over a fixed-depth Sparse Merkle Tree?
| Property | Fixed-depth SMT (256 levels) | JMT (radix-16, compressed) |
|---|---|---|
| Node hashes per update | 256 | depth-of-key (typ. 8–14) |
| Empty subtree storage | implicit (precomputed) | implicit (no materialize) |
| Update batching | per-key | bulk via update_all |
| Throughput (commits) | baseline | ~40× faster |
| Proof size | fixed (256 sibling hashes) | variable (typ. 8–14) |
| Non-existence proofs | empty leaf hash | path divergence proof |
The headline number — 40× faster commits — was the deciding factor. JMT removes the per-key 256-Poseidon2 cost, replacing it with a path that follows the actual key density in the tree.
The implementation lives in crates/state/src/jmt_store.rs. The persistent
wrapper exposes a small surface:
#![allow(unused)] fn main() { PersistentJMT { fn insert(key: H256, value: Vec<u8>) -> ... fn get(key: H256) -> Option<Vec<u8>> fn update_all(updates: &[(H256, Option<Vec<u8>>)]) -> ... fn root() -> H256 fn delete(key: H256) -> ... fn is_empty() -> bool } }
A HybridJmtHasher adapter implements the jmt::SimpleHasher trait,
delegating internal node hashes to Blake3 (the high-volume path) and
exposing Poseidon2 for state-root and address-derivation paths. The
JMT internals use Blake3; the snapshot manifest and ZK-bearing exports
use Poseidon2. Both roots are computed and signed (Chapter 6).
4.1b Two-Table Architecture: state_cf + jmt_cf
Pyde maintains state in two RocksDB column families, each optimized for a different access pattern:
┌────────────────────────────────────────────────────────────────────────┐
│ state_cf — flat key-value index for live reads │
│ │
│ key = slot_hash (32 bytes, PIP-2 layout) │
│ value = current slot value (raw bytes) │
│ │
│ O(1) point lookup. Updated on every state change. │
│ Used by: live execution path (sload), RPC queries, range scans. │
└────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────┐
│ jmt_cf — versioned tree structure for proofs + state root │
│ │
│ key = NodeKey(version: u64, NibblePath) │
│ value = JmtNode { children_fingerprints[], value_bytes (if leaf) } │
│ │
│ O(depth) walk for proofs. Updated at every wave commit. │
│ Used by: state-root computation, Merkle proofs for light clients, │
│ historical state queries (on archive nodes). │
└────────────────────────────────────────────────────────────────────────┘
Why two tables instead of one:
The JMT alone can serve every read, but each read is O(depth) — typically 6-8 RocksDB gets to walk from root to leaf. For live execution at thousands of TPS, that's too expensive.
state_cf keeps a flat denormalized index of the current value for every slot. A single get returns the value. PIP-2's clustered slot_hash layout keeps state_cf entries spatially clustered by contract, so range scans and multigets stay cheap.
The JMT structure is still maintained alongside, because it's needed for:
- State-root computation: hash up from leaves to root, deterministically, across all validators
- Merkle proofs: light clients verify
(value, proof) → state_rootwithout holding full state - Versioned reads: archive nodes serve historical state by walking older JMT versions
The read path:
fn read_slot(slot_hash) -> Option<Bytes>:
1. dashmap.get(slot_hash) ← PIP-4 in-memory cache (most live reads)
2. state_cf.get(slot_hash) ← ONE disk read (cache miss path)
Total: one disk get, sometimes amortized to zero.
The JMT is not in the live read path. Reads use state_cf. The JMT is reached only for proofs or for state-root computation at commit time.
The write path (at wave commit):
fn commit_wave(dirty_changes: Vec<(SlotHash, Bytes)>):
1. For each (slot_hash, new_value) in dirty_changes:
jmt.update(slot_hash, new_value, new_version)
→ JMT recomputes leaf_hash + internal hashes up the affected path
state_cf.put(slot_hash, new_value)
2. new_state_root = jmt.root_hash(new_version)
3. Both writes happen in a single RocksDB WriteBatch (atomic).
The two tables stay in lockstep. They are never out of sync because every write touches both atomically.
Cost of duplication: roughly 2× storage for the state itself (the leaves' values appear in both state_cf and the JMT's leaf records). This is the trade-off — extra storage in exchange for O(1) live reads while still preserving authenticated proofs.
Retention split:
| Node tier | state_cf | jmt_cf |
|---|---|---|
| Pruned validator | Current state only | Latest version only (older GC'd) |
| Archive node | Current state | All historical versions |
| Light client | None | Just state_root from WaveCommitRecords |
4.1c Events Storage: events_cf + Indexes
State is not the only thing the chain stores. Events emitted via pyde::emit_event (see Chapter 3 §3.3 and Host Function ABI Spec §15) live in three additional column families parallel to state_cf + jmt_cf:
events_cf (primary, ordered by wave)
key: wave_id (8 BE) || tx_index (4 BE) || event_index (4 BE)
value: borsh_encode(EventRecord)
events_by_topic_cf (index)
key: topic (32) || wave_id (8 BE) || tx_index (4 BE) || event_index (4 BE)
value: () -- empty; key carries lookup info
events_by_contract_cf (index)
key: contract_addr (32) || wave_id (8 BE) || tx_index (4 BE) || event_index (4 BE)
value: ()
Atomicity: at every wave commit, the engine writes one RocksDB WriteBatch containing updates to state_cf + jmt_cf + events_cf + events_by_topic_cf + events_by_contract_cf + the wave commit record. Either all five land or none does.
On-chain commitment: each wave commit record carries two summaries of the wave's events:
events_root(Blake3) — binary Merkle tree over canonical-ordered events, suitable for inclusion proofs.events_bloom(256-byte, 2048-bit, 3-hash) — probabilistic summary for cheap "any event matching X in this wave?" checks.
Both are threshold-signed as part of the wave's HardFinalityCert, so light clients verify event inclusion identically to how they verify state.
Retention:
| Node tier | events_cf + indexes |
|---|---|
| Archive node | All events, forever |
| Pruned validator | Last 90 days |
| Committee validator | Last 30 days |
| Light client | None (verifies inclusion proofs against signed events_root) |
Pruning is in lockstep across all three event column families.
For query semantics (pyde_getLogs), subscriptions (pyde_subscribe), and the Borsh-recommended event encoding, see Host Function ABI Spec §14–§15.
4.2 Hybrid Hashing: Blake3 + Poseidon2
Pyde uses two hashes in different layers, chosen for what each is best at:
| Hash | Speed (commodity CPU) | ZK-friendly | Where used |
|---|---|---|---|
| Blake3 | ~3 GB/s | No (huge circuit) | JMT internal nodes, batch hashes, vertex hashes, gossip de-dup, RocksDB keys |
| Poseidon2 | ~60 MB/s | Yes (small circuit) | State root commitment, address derivation, FALCON sig hashing inside ZK circuits, threshold MAC |
The split rule: every hash that lives entirely off-chain or inside a trusted committee-signed structure can be Blake3. Every hash that may be exposed to a future ZK proof (state root, addresses, signature payloads) is Poseidon2.
Poseidon2 (Goldilocks)
Poseidon2 is the algebraic hash used everywhere in Pyde — the JMT, contract
storage-key derivation, transaction hashing, the threshold MAC, the VRF, and
the poseidon2 WASM host function. The parameter set (see Chapter 8 for full
detail):
| Parameter | Value |
|---|---|
| Field | Goldilocks (p = 2^64 - 2^32 + 1) |
| State width | 8 |
| Rate | 4 (256-bit absorb/squeeze) |
| Capacity | 4 |
| External rounds | 8 (4 + 4) |
| Internal rounds | 22 |
| S-box | x^7 |
| Output | 256 bits |
The hash is exposed as three primitives:
| Function | Use |
|---|---|
poseidon2_hash(bytes) | arbitrary input → 256-bit digest |
poseidon2_pair(left, right) | Merkle node hash (order-sensitive by design) |
poseidon2_many(&[Hash256]) | sponge over a variable-length array of hashes |
The _pair form is exposed for compatibility but JMT internal nodes use
Blake3 (blake3_pair); Poseidon2's _hash form is what storage-key
derivation, address derivation, and the poseidon2 WASM host function use; the
_many form is what the threshold scheme uses to combine epoch randomness
shares.
Blake3
Used in the high-volume paths where ZK-friendliness is irrelevant:
- JMT internal node hashes (hybrid-mode hasher)
- Batch hashes referenced from vertices
- Vertex hashes in the DAG
- Gossip message de-duplication keys
- RocksDB cache keys
Blake3 is configured in its default tree-hashing mode with 256-bit output. Native verification of a JMT inclusion proof against the Blake3 state root takes ~5-10 hash operations and completes in microseconds — fast enough that the snapshot manifest verification (Chapter 7) doesn't dominate sync time.
4.3 Account Storage Layout
Every account in crates/account/src/types.rs has a fixed layout:
#![allow(unused)] fn main() { struct Account { address: Address, // 32 bytes (Poseidon2 hash of FALCON pubkey) nonce: u64, // 8 bytes (sliding window base — see Chapter 11) balance: u128, // 16 bytes, in quanta (10^9 quanta = 1 PYDE) code_hash: H256, // 32 bytes (zero for EOAs) storage_root: H256, // 32 bytes (zero for empty contracts) account_type: AccountType,// 1 byte (EOA=0, Contract=1, System=2) auth_keys: AuthKeys, // variable (FALCON pubkey or multisig set) gas_tank: u128, // 16 bytes (sponsored-tx pool) key_nonce: u32, // 4 bytes (rotation counter) } }
Fixed portion: 141 bytes plus the variable auth_keys field.
The address is a 32-byte Poseidon2 hash. Three derivation paths exist:
EOA address = Poseidon2(falcon_public_key_bytes) // 897-byte FALCON pk
CREATE address = Poseidon2(deployer_address || nonce_bytes)
CREATE2 address = Poseidon2(0xFF || deployer_address || salt || code_hash)
The 32-byte length matches the natural Poseidon2 output (4 Goldilocks field elements ≈ 256 bits) and avoids the birthday-bound concerns of 20-byte truncated addresses at chain scale.
4.4 Storage Keys and Slots
Pyde uses a flat storage layout. Account fields and contract storage slots all live in the same JMT, distinguished by discriminator bytes in the key derivation.
The key derivation pattern is:
key = Poseidon2(account_address || discriminator || sub_key)
Some discriminators currently in use (defined in crates/state/src/keys.rs):
| Discriminator | Name | What it keys |
|---|---|---|
| 0x12 | SUPPLY | Total PYDE supply counter |
| 0x13 | TOTAL_BURNED | Cumulative fee burn counter |
| 0x14 | REWARDS_PER_STAKE_UNIT | Lazy-accrual per-stake-unit reward accumulator |
| 0x15 | ACTIVE_STAKE_WEIGHTED_TOTAL | Pool divisor (sum of stake × uptime; excludes exited/slashed) |
| 0x16 | VESTING | Per-account vesting schedule |
| 0x17 | VALIDATOR_SUBSIDY | (total_amount, end_wave) for streaming subsidy |
| 0x18 | AIRDROP_ROOT | Genesis airdrop Merkle root |
| 0x19 | AIRDROP_DEADLINE | Slot height after which sweep is allowed |
| 0x1A | AIRDROP_CLAIMED | Per-leaf-index claim bitmap |
| 0x1B | AIRDROP_EXPECTED_SUM | Genesis pool size invariant |
| 0x1C | MULTISIG_SIGNERS | Treasury multisig signer set (FALCON pks) |
| 0x1D | MULTISIG_THRESHOLD | Required signature count |
| 0x1E | MULTISIG_NONCE | Replay-protection counter for multisig actions |
| 0x1F | EMERGENCY_PAUSE_END_WAVE | End wave_id of an active emergency pause |
This flat scheme means a single Merkle path can prove any state claim — there
is no nested account-trie / storage-trie indirection (the classic
Patricia-trie pattern). One proof, one Poseidon2-walk to the root.
Contract storage layout
The otigen developer toolchain's state binding generator assigns slot
identifiers to storage fields declared in otigen.toml. Each contract
defines its state schema once and gets language-specific bindings that
encode the slot derivation as build-time constants. Single-value fields
lower to:
key = Poseidon2(contract_address, slot_index)
Maps lower to a doubled hash:
key = Poseidon2(contract_address, Poseidon2(slot_index, map_key))
Nested maps add another inner Poseidon2 per nesting level. This is the
machinery that makes self.balances[user_addr] a single Sload opcode in
the compiled bytecode.
4.5 The Block Witness
Pyde's block witness is the data needed to verify and re-execute a block from
scratch given only the previous state root. It lives in
crates/state/src/witness.rs:
#![allow(unused)] fn main() { pub struct BlockWitness { pub entries: Vec<WitnessEntry>, pub proof: SparseMerkleProof, // single batched proof pub pre_state_root: H256, pub post_state_root: H256, // populated by finalize_witness } }
The shape:
entries— every state slot the block touched, with its pre-execution value.proof— a single batched Merkle proof covering all entries againstpre_state_root. JMT supports batch verification, so the proof is asymptotically smaller thanlen(entries)independent paths.pre_state_root— the state root before this block executes (taken from the parent block's header).post_state_root— the state root after execution, set byset_post_state_root()orfinalize_witness()once the block is executed.
Critically, post_state_root is not auto-populated at witness generation
time. The witness is built before execution; the post-root is filled in
afterwards. is_finalized() returns false until that step happens.
The 1 MB witness size cap
A hostile transaction could theoretically force a witness containing millions of entries (e.g., touching deep, sparse storage paths). Pyde caps witness size hard:
#![allow(unused)] fn main() { pub const MAX_WITNESS_SIZE: usize = 1024 * 1024; // 1 MB }
verify_witnesses() rejects any witness exceeding this cap before doing the
work of proof verification. The block as a whole is rejected.
4.6 RocksDB Layout
The JMT and witness logic both persist through RocksDB
(JmtRocksStore in crates/state/src/jmt_store.rs). The key prefixes are:
| Prefix | Meaning |
|---|---|
0x10 | JMT internal nodes |
0x11 | Leaf values |
0x12 | Metadata (version counter, latest root) |
LRU caches sit in front of node and value reads (256k entries each, sized for the working set of an active validator). Compression is LZ4 for the L0–L1 levels and ZSTD for cold levels; the block cache is 512 MB and the memtable pool is 256 MB. These are tuned for the steady-state validator workload, not for peak burst sync.
Writes to consensus-critical state use WriteOptions::set_sync(true) (see
Chapter 6) — JMT updates do not, because the canonical truth is the chain
itself; on restart, a validator can rebuild any missing state from blocks.
4.7 The Block-Application Pipeline
When a block is executed, the state pipeline runs in this order:
- Open a batch against the current JMT.
- Execute each parallel group from the conflict graph (see Chapter 9 for how the access-list scheduler builds groups).
- Within a group, transactions execute sequentially in order; across
groups, in parallel against the same
pre_state_root. - Apply state writes to the batch.
- Distribute fees: 70% to the burn counter (
TOTAL_BURNEDdiscriminator), 20% to the epoch reward pool (distributed at epoch end by stake × uptime), 10% to the treasury account. - Commit the batch with
update_all. The new root ispost_state_root. - Set
witness.post_state_rootand stamp the block header.
The "execute then commit" ordering means the post-root is a function of the exact transaction set, the exact ordering, and the exact starting state — so two honest validators given the same encrypted block always agree on the post-root. Disagreement is a slashing-grade safety violation.
4.8 State Sync
A new node joining the network does not replay every block from genesis — at production TPS, full replay would take longer than the chain has existed. Pyde defines three sync modes (full spec: companion/STATE_SYNC.md, operational summary: Chapter 7):
-
Snapshot sync (default for new full nodes). Download a committee-signed
SnapshotManifest(~5 KB) carrying both Blake3 and Poseidon2 state roots plus chunk references. Verify ≥85 FALCON signatures. Download chunks (~4 MB each) in parallel from peers, verify each against the manifest, reconstruct the JMT, recompute the Blake3 root, compare. Then replay the tail blocks (≤ 8 epochs ≈ 24 hours of tx) to reach the current head. Total time on commodity (100 Mbps): ~40 minutes. -
Light client sync. Headers only + cared-about accounts via JMT inclusion proofs. ~600 KB/year for a typical wallet. Verifies FALCON signatures on the headers it receives.
-
Full sync (archive nodes). Replay every block from genesis. Slowest option; provides full historical state lookup for explorers / indexers.
Chain-of-trust bootstrap. A new node verifies the chain of snapshot
manifests from genesis forward: genesis hardcodes committee_0's pubkeys;
each subsequent epoch-boundary manifest is signed by the prior committee
and contains the next committee's pubkeys.
Weak-subjectivity checkpoints published by the foundation and reputable infrastructure providers let new nodes trust a recent checkpoint and skip the chain-of-trust walk. Beyond a one-epoch rollback window, contradicting a finality checkpoint is impossible without a hard fork.
4.9 What Is NOT in the State
A few things deliberately do not live in the JMT:
-
Receipts. Stored in an in-memory ring buffer (
crates/node/src/receipt_store.rs,MAX_RECEIPT_SLOTS = 10_000). At ~500 ms per commit, this is roughly 80 minutes of recent receipt history. Persistent receipt storage (archive-node mode) is tracked as post-mainnet hardening. -
Mempool contents. Encrypted transactions live in process memory, bounded per sender by the rate-limiting subsystem (10 tx/s, 100 concurrent per sender).
-
Consensus protocol state.
pending_votes,seen_proposals,seen_votes, and pending evidence live in their own RocksDB column under the consensus_store, withset_sync(true)writes — see Chapter 6. -
Finality checkpoints. Stored in the consensus_store with their own key (
FINALITY_CHECKPOINT_KEY), not in the JMT itself.
The line is drawn deliberately: the JMT holds canonical chain state that everyone agrees on. Operational state (consensus liveness, mempool ingress, receipt cache) lives outside the consensus root because it does not need to be globally agreed.
4.10 Summary
| Component | Choice |
|---|---|
| Tree structure | Jellyfish Merkle Tree (radix-16, path-compressed) |
| Internal-node hash | Blake3 (high-volume, native) |
| State root | Dual: Blake3 (native) + Poseidon2 (ZK-bearing) |
| Address-derivation | Poseidon2 (ZK exposure preserved) |
| Storage layout | Flat — single tree, discriminator bytes in keys |
| Address format | 32 bytes, Poseidon2 of the FALCON-512 public key |
| Account record size | 141 bytes fixed + variable auth_keys |
| Storage keying | Poseidon2(addr, slot) for values; doubled for maps |
| Witness format | Single batched JMT proof + entries + pre/post roots |
| Witness size cap | 1 MB (rejected at verification time) |
| Persistence | RocksDB with LRU node and value caches |
| Block-app commit cost | ~40× faster commits than the prior fixed-depth SMT design |
The next chapter covers the developer toolchain (otigen) that sits on top
of this state model — how a contract's [state] declaration in otigen.toml
becomes the slot identifiers the JMT actually sees, via language-specific
state binding generators that pre-compute slot prefix constants at build time.
Chapter 5: Otigen Toolchain
otigen is Pyde's developer toolchain — a single binary that validates the author's WASM build, generates the ABI from otigen.toml, packages the deploy bundle, and handles on-chain lifecycle commands (deploy, upgrade, pause, kill, inspect, wallet, console).
What otigen deliberately does NOT do: it does not compile WASM, it does not generate code, it does not interface with any language's build pipeline. Authors run their own cargo build / asc / tinygo build / clang --target=wasm32 and otigen checks the result. This keeps the toolchain minimal and language-agnostic, and lets authors keep their full native toolchain experience.
The name carries forward from an earlier design phase, when Otigen was Pyde's domain-specific smart-contract language. The language is retired; the name now describes the role it occupies best — the lightweight verifier and packager that makes WebAssembly deployment on Pyde coherent without forcing authors out of their language ecosystems. See The Pivot for the full story.
This chapter covers the toolchain's design, the subcommand surface, the otigen.toml schema, the per-language workflow, build verification, attributes, deploy/upgrade, wallet, and the console.
For the underlying execution layer that contracts run on, read Chapter 3: Execution Layer. For the host functions contracts call, read the Host Function ABI spec.
5.1 Design Principles
The toolchain is built around four principles, each chosen deliberately.
Author owns the build; otigen verifies
otigen does not compile WASM. The author runs their language's native build command (cargo build --target wasm32-unknown-unknown --release, asc assembly/index.ts -O, tinygo build -target wasm-unknown, clang --target=wasm32 -O3) themselves. They get the full diagnostics, the full IDE integration, the full test workflow their language ecosystem provides.
otigen build then verifies the result: confirms the .wasm file exists at the path declared in otigen.toml, validates the WASM module structure, cross-checks that the module imports only allowed host functions and exports every function declared in [functions], and generates the deploy bundle. If anything is missing or wrong, otigen says so; if everything checks out, it prints "ready to deploy."
This keeps the toolchain minimal (no per-language compiler invocation logic to maintain) and respects the author's native toolchain.
Zero extra code in the author's project
A contract project contains only the author's contract logic and an otigen.toml. No bundler files, no glue code, no manifest-handling boilerplate. The author writes what their language requires (a Cargo.toml for Rust, package.json for AssemblyScript, go.mod for Go, Makefile for C/C++) and the contract source itself.
State access and host-function calls go through whatever helper pattern the author or community provides for their language. otigen doesn't ship those helpers, doesn't generate them, doesn't depend on them. It only requires that the resulting .wasm imports the Host Function ABI correctly.
Native test runners
Each language has a mature test framework. The toolchain does not wrap them. Rust authors run cargo test. AssemblyScript authors run npm test. Go authors run go test. C authors use whatever they already use. The toolchain does not impose its own test command.
Attributes and ABI declared in otigen.toml, enforced at runtime
Function attributes (view, payable, reentrant, sponsored, constructor, fallback, receive, entry) and state schema are declared in otigen.toml. otigen build reads them, builds a ContractAbi struct, Borsh-encodes it, and injects it as a WASM custom section named pyde.abi directly into the .wasm artifact the language compiler produced. There is no separate abi.json file at deploy time — the ABI travels with the code as one binary. At runtime, the WASM execution layer extracts the pyde.abi section once, caches the parsed ABI alongside the compiled Module, and applies attribute-driven guards before every call (reentrancy block, view-mode state-write rejection, payable-mode value check, sponsored gas-tank debit, etc.). The WASM module itself does not carry attribute markers — the engine enforces them at the call boundary based on the parsed ABI. Full mechanics: Host Function ABI Spec §3.5–§3.7.
5.2 Subcommand Surface
| Command | Purpose |
|---|---|
otigen init <name> --lang <language> | Scaffold a new project directory from the language template. Populates otigen.toml skeleton and a minimal source file. |
otigen build | Verify + package. Reads otigen.toml, checks the .wasm file exists at the declared path, validates the WASM module (well-formed, imports allowed only), cross-checks declared [functions] exist as WASM exports, generates abi.json from otigen.toml, packages bundle. Prints "ready to deploy" on success. Does NOT compile WASM — the author runs their own language build. |
otigen deploy | Sign and submit a deploy transaction. Registers the contract name (ENS-style), pays the registration fee, pays the owner deposit, transmits the WASM bytes + ABI. |
otigen upgrade | Submit an upgrade proposal. For smart contracts: owner-signed upgrade tx. For parachains: routes through governance (see Chapter 13). |
otigen pause | Pause an operational contract (owner-only, where supported). |
otigen kill | Permanently retire a contract (governance-required for parachains; owner-only for individual contracts where the contract opted into killable). |
otigen inspect <address-or-name> | Read deployed contract state, ABI, version history. |
otigen wallet | Key management. Subcommands: create, import, list, export-pubkey. |
otigen console | REPL against a local or remote Pyde node. |
There is no otigen test. Authors use their language's native test runner.
There is no otigen compile. Authors use their language's native compiler.
5.3 The otigen.toml Schema
A single TOML file declares everything otigen needs to know about the project.
[project]
name = "my_token"
version = "1.0.0"
language = "rust" # one of: rust, assemblyscript, go, c
[build]
wasm_path = "target/wasm32-unknown-unknown/release/my_token.wasm"
# Author runs their own language build to produce this file.
# otigen build verifies it exists, validates it, and packages it.
[contract]
type = "smart_contract" # or "parachain"
description = "A simple PYDE-flavored token contract."
[name_registry]
name = "mytoken" # ENS-style unique name (see Account Model)
extension = "pyde" # reserved for v2; v1 uses flat namespace
[state]
# Declares the contract's state schema. Used for ABI generation,
# explorer indexing, and cross-validation against runtime access patterns.
# Authors derive slot_hash values themselves in their contract code.
balance = { type = "map<address, uint128>", disc = 0 }
nonce = { type = "map<address, uint64>", disc = 1 }
allowances = { type = "map<address, map<address, uint128>>", disc = 6 }
total_supply = { type = "uint128", disc = 7 }
[functions.transfer]
attributes = ["entry", "payable"]
inputs = ["address", "uint128"]
outputs = []
[functions.balance]
attributes = ["entry", "view"]
inputs = ["address"]
outputs = ["uint128"]
[functions.complex_callback]
attributes = ["entry", "reentrant"] # opts INTO reentrancy; default is BLOCKED
inputs = ["bytes"]
[functions.user_signup]
attributes = ["entry", "sponsored"] # gas paid from contract's gas_tank
inputs = ["address"]
[functions.init]
attributes = ["constructor"] # callable only at deploy time
inputs = ["uint128"]
[deploy]
network = "testnet" # or "mainnet" or a named local node
owner_wallet = "alice" # name of the wallet to sign with
deposit = 1000 # in PYDE; forfeited on misbehavior
[gas]
max_per_tx = 10_000_000 # cap; can be raised at deploy time
Schema notes
[project] — basic metadata. language is informational only; it tells humans (and explorers) what language the source is in. otigen does not use this to invoke a compiler.
[build] — the most important field: wasm_path tells otigen build where to find the .wasm file the author produced. If the file is missing, otigen build says so with a clear error and exits.
[contract] — for smart contracts, just descriptive. For parachains, this section grows to include consensus type, validator constraints, slashing preset (see Chapter 13).
[name_registry] — the human-readable name under which the contract will be registered. Names are globally unique (per the ENS-style registry in the account model chapter). Registration costs PYDE; renewal is yearly with a grace period.
[state] — the schema of the contract's storage. Each entry declares a state field name, its type, and its discriminator. Used for: ABI emission (so explorers can decode state), explorer indexing, and as a reference for the author's hand-written slot derivation. otigen does not generate accessor code — the author writes their state access using whatever pattern their language community settles on.
[functions.<name>] — declares each callable function in the contract along with its attributes and signature. Every function the runtime should be able to invoke must have an entry here; otigen build cross-checks that every [functions.X] corresponds to a WASM export named X. Attributes are enforced at runtime by the engine based on what's in the deployed ABI.
[deploy] — settings for otigen deploy. Overridable on the command line.
[gas] — gas cap for the contract. Defaults are usually fine; explicit setting allows larger budgets where needed.
5.4 Per-Language Workflow
Each language has its own template (scaffolded by otigen init) and its own native build command. The author runs the build; then otigen build verifies + packages.
Rust
otigen init my_contract --lang rust
cd my_contract
# Edit src/main.rs with contract logic, declare entries + state in otigen.toml
# Author runs their own build:
cargo build --release --target wasm32-unknown-unknown
# otigen verifies and packages:
otigen build
otigen deploy --network testnet
Scaffolded project tree:
my_contract/
├── otigen.toml # author edits state + functions sections
├── Cargo.toml # pre-configured for wasm32-unknown-unknown
├── .cargo/config.toml # target defaults
├── src/
│ └── main.rs # template with extern host-fn declarations + one example entry
├── tests/
└── .gitignore
otigen build does:
- Read
otigen.toml; confirm[build].wasm_pathpoints to an existing file. - Validate the WASM module (parses cleanly, only imports allowed host functions, only uses allowed WASM features).
- Cross-check: every
[functions.X]has a matching WASM export namedX. - Generate
artifacts/<contract_name>.abi.jsonfrom the[state]and[functions]tables. - Package
artifacts/<contract_name>.bundlecontaining the.wasm+ ABI + deploy metadata. - Print "ready to deploy" with the resolved paths.
AssemblyScript
otigen init my_contract --lang assemblyscript
cd my_contract
# Edit assembly/index.ts, declare entries + state in otigen.toml
npm run asbuild # or: npx asc assembly/index.ts -O --outFile build/my_contract.wasm
otigen build # verify + package
otigen deploy --network testnet
Go (TinyGo)
otigen init my_contract --lang go
cd my_contract
# Edit main.go, declare entries + state in otigen.toml
tinygo build -target wasm-unknown -o build/my_contract.wasm
otigen build # verify + package
otigen deploy --network testnet
C/C++
otigen init my_contract --lang c
cd my_contract
# Edit src/main.c, declare entries + state in otigen.toml
clang --target=wasm32 -O3 -Wl,--no-entry -o build/my_contract.wasm src/main.c
otigen build # verify + package
otigen deploy --network testnet
Why this split
Authors keep their full language toolchain (build errors, IDE integration, dependency management, test runners, fuzzers, profilers — everything). The chain-specific concerns (ABI generation, deploy packaging, on-chain lifecycle) are owned by otigen. The interface between them is the .wasm file + the otigen.toml schema; both are inspectable, neither is generated by the other.
5.5 Build Verification + Packaging
otigen build is purely a validator + packager. It runs in roughly this order:
1. Load otigen.toml; reject if required sections are missing.
2. Resolve [build].wasm_path; reject if the file doesn't exist.
3. Parse the .wasm file; reject if the binary is malformed.
4. Walk the WASM import table; reject any import outside the Host Function ABI allowlist.
5. Walk the WASM export table; cross-check every [functions.X] has a matching export named X.
6. Validate attribute combinations per function (no view+payable, no constructor outside [functions], etc.).
7. Validate state schema: discriminator uniqueness, type validity, map-key types declared.
8. Generate artifacts/<contract_name>.abi.json from [state] + [functions].
9. Package artifacts/<contract_name>.bundle:
- .wasm bytes
- abi.json
- otigen.toml snapshot
- manifest with sha256 hashes
10. Print "ready to deploy" with the bundle path and contract name.
If any step fails, otigen build exits non-zero with a structured error message identifying what's missing or wrong. No partial bundles are written.
How authors do state access (without otigen-generated code)
Because otigen doesn't generate bindings, the author writes their state access using whatever pattern their language community supplies (or just uses raw extern declarations + a small helper module they write themselves). Pyde does not ship per-language SDKs (see no-SDK approach) — the canonical example projects in pyde-net/otigen show one workable pattern per language.
A typical Rust pattern looks like this (the author writes the entire file; otigen never touches it):
#![allow(unused)] fn main() { // src/main.rs (author writes all of this) // Host function imports (declared once, used everywhere): extern "C" { fn pyde_storage_read(slot_hash_ptr: *const u8, slot_hash_len: usize) -> i64; fn pyde_storage_write(slot_hash_ptr: *const u8, slot_hash_len: usize, value_ptr: *const u8, value_len: usize); fn pyde_caller(out_ptr: *mut u8); fn pyde_emit_event(topic_ptr: *const u8, topic_len: usize, data_ptr: *const u8, data_len: usize); fn pyde_poseidon2(input_ptr: *const u8, input_len: usize, out_ptr: *mut u8); } // Slot derivation: author derives slot_hash according to the PIP-2 layout // described in Chapter 4. They can precompute the contract's address prefix // as a `const fn` invocation, or compute on first use and cache. const CONTRACT_NAME: &[u8] = b"mytoken"; const BALANCE_DISC: u8 = 0; // from otigen.toml fn balance_slot(addr: &[u8; 32]) -> [u8; 32] { let mut slot = [0u8; 32]; let contract_prefix = poseidon2_const_prefix(CONTRACT_NAME); // computed at startup; cached slot[..16].copy_from_slice(&contract_prefix[..16]); let mut inner_input = [0u8; 33]; inner_input[0] = BALANCE_DISC; inner_input[1..].copy_from_slice(addr); let mut inner = [0u8; 32]; unsafe { pyde_poseidon2(inner_input.as_ptr(), inner_input.len(), inner.as_mut_ptr()); } slot[16..].copy_from_slice(&inner[..16]); slot } // Entry function — name must match [functions.transfer] in otigen.toml #[no_mangle] pub extern "C" fn transfer(to_ptr: *const u8, amount_lo: u64, amount_hi: u64) -> i32 { // ... read inputs, derive slots, call pyde_storage_read/write, etc. 0 // success } }
The author has total control over how slot derivation is done. They can precompute prefix hashes at startup (Rust lazy_static!, AssemblyScript module-level init, Go init()), keep them in module-level constants, or call pyde_poseidon2 per access. None of this is otigen's concern — otigen just checks that the resulting .wasm is well-formed and matches the declared [functions].
Build-time pre-hashing is the author's responsibility (and easy)
The build-time pre-hashing optimization (computing contract-name prefix once at compile time) is a per-language pattern. In Rust it's a const fn or a lazy_static!. In AssemblyScript it's a top-level constant initializer. In Go it's an init(). In C it's a static const array. The author follows their language's idioms; otigen doesn't get involved.
5.6 Safety Attributes via otigen.toml
Otigen the language had a set of compiler attributes that made common safety properties default and explicit. Every one of those properties carries forward unchanged in the WASM era. Authors declare them in otigen.toml [functions.<name>] attributes = [...]; otigen build includes them in the generated ABI; the runtime enforces them by reading the ABI before invocation and applying the appropriate guards.
The mechanism changed (config-declared metadata enforced at the call boundary instead of compiler-extracted markers in bytecode), but the safety guarantees are identical to the Otigen-language era.
Reentrancy is still blocked by default
This is the most important property to preserve. Every public function gets an automatically generated reentrancy guard. To opt OUT of the guard — for a function that genuinely needs to allow re-entry — add the #[reentrant] attribute.
If you write nothing, you are protected.
The attribute set
| Attribute | Effect |
|---|---|
view | Read-only function. Runtime rejects any state-modifying host call inside it. View calls are FREE (no gas) — see HOST_FN_ABI_SPEC §7.8. |
payable | Function accepts PYDE attached to the call. Non-payable functions reject any attached amount. |
reentrant | Opts INTO allowing reentrancy. Default for every function is reentrancy-blocked. |
constructor | Initialization-only. Callable exactly once, at deploy time. |
sponsored | Gas charged to the contract's gas_tank rather than the caller's balance. Enables gasless UX. |
fallback | Invoked when the call's function selector matches no declared function. At most one per contract. |
receive | Invoked on bare PYDE transfers (no selector, value > 0). At most one per contract. Must also be payable. |
entry | Marks the function as callable from outside the contract (top-level tx or cross_call). Required for any function not marked with another dispatch attribute (constructor/fallback/receive). Internal helpers omit entry and are not exposed in the public selector table. |
For attribute compatibility rules (which combinations are rejected at build + deploy), see HOST_FN_ABI_SPEC §3.5.1.
How attributes are declared
Attributes are declared in otigen.toml, per function. The author writes plain TOML; the source code is whatever they write in their language. No per-language macro syntax is needed and no source-code parsing is required.
[functions.balance]
attributes = ["entry", "view"]
inputs = ["address"]
outputs = ["uint128"]
[functions.deposit]
attributes = ["entry", "payable"]
inputs = []
[functions.complex_callback]
attributes = ["entry", "reentrant"] # opts INTO reentrancy; default is BLOCKED
inputs = ["bytes"]
[functions.user_signup]
attributes = ["entry", "sponsored"] # gas paid by contract's gas_tank
inputs = ["address"]
[functions.init]
attributes = ["constructor"] # callable only at deploy time
inputs = ["uint128"]
The author writes the corresponding WASM exports in their language as normal exported functions. There is no required annotation pattern in source — the function just needs to be exported under the name declared in [functions.<name>]. In Rust, this is #[no_mangle] pub extern "C" fn balance(...). In AssemblyScript, export function balance(...). In Go (TinyGo), //go:wasmexport balance. In C, __attribute__((export_name("balance"))). Standard WASM-export idioms for each language.
What the build tool does with attributes
otigen build validates them (e.g., a function cannot be both view and payable) and writes them into the generated ABI:
{
"functions": [
{
"name": "transfer",
"selector": "0xa9059cbb",
"attributes": ["entry"],
"inputs": [...],
"outputs": [...]
},
{
"name": "balance",
"selector": "0x70a08231",
"attributes": ["entry", "view"],
"inputs": [...],
"outputs": [...]
},
{
"name": "user_signup",
"selector": "0x...",
"attributes": ["entry", "sponsored"],
"inputs": [...],
"outputs": [...]
}
]
}
How the runtime enforces them
The WASM execution layer reads the function's attribute set from the deployed ABI before invocation and applies the appropriate behavior:
| Attribute | Runtime enforcement |
|---|---|
view | Host functions sstore, sdelete, transfer, emit_event trap if called inside a view function. |
payable | If tx.value > 0 and target function is not payable, transaction reverts at dispatch. No state change. |
reentrant | Runtime skips the reentrancy guard for this function. ALL OTHER functions get the guard. |
Not reentrant (default) | On entry, the runtime sets a per-contract reentrancy flag. Any host call that re-enters this contract checks the flag; if set, traps with ReentrancyViolation. On exit, flag is cleared. |
constructor | Callable only by the deploy transaction. Subsequent calls trap. |
sponsored | At dispatch time, the engine debits gas from the contract's gas_tank instead of the caller's balance. If the gas tank is empty, transaction reverts. |
This is identical behavior to Otigen the language. The change is implementation venue: attributes now ride on the ABI declared in otigen.toml rather than on compiler-extracted markers in bytecode. The safety guarantees are the same. The author's per-function declaration moves from source-code annotation to a config file. Both equally explicit; the config form keeps otigen decoupled from per-language source parsing.
Other Otigen design choices preserved
Beyond function attributes, several broader Otigen design choices carry forward as runtime properties of the engine:
| Otigen design choice | How it's preserved in the WASM era |
|---|---|
| Reentrancy off by default | Runtime reentrancy guard for every function not marked reentrant. |
| Checked arithmetic by default | Per-language SDK helper patterns; wrapping ops require explicit opt-in (e.g., Rust's wrapping_add is explicitly named). |
| Typed storage | otigen.toml [state] schema declares types; ABI includes the schema so the runtime + explorers know what each slot is. Authors implement type-safe access in their own code. |
No tx.origin | Host function ABI exposes caller() (direct caller) but no origin(). The Solidity-style phishing footgun is absent. |
| Compile-time access lists | Build tool emits a static access list per function from the declared state schema; the parallel scheduler uses these. |
| 4-byte function selectors | Build tool emits selector = first 4 bytes of Hash(function_signature) in the ABI. |
| Sponsored / gasless transactions | #[sponsored] attribute + gas_tank per contract account, exactly as designed in the Otigen era. |
| Reserved-storage-slot guards | Reentrancy guard uses a reserved slot in the contract's state subtree, never reachable by user-allocated slots. |
The safety floor that Otigen provided is preserved end-to-end. The mechanism is different; the contract author's experience is the same.
5.7 Deploy and Upgrade Flow
Deploy
otigen deploy --network testnet
What happens:
otigenreadsotigen.toml, validates the contract is built (artifacts/<name>.bundleexists and is current).otigenchecks the name registry on-chain: ismytokenavailable? If taken, fail with a clear error.otigenopens the wallet keystore, prompts for password if encrypted, signs the deploy transaction.- The deploy transaction includes:
- Contract name (
mytoken) - Registration fee payment (tiered by name length)
- Owner deposit (forfeit on misbehavior)
- WASM bytes
- ABI JSON
- Initial state values (if any from constructor)
- Contract name (
otigensubmits the transaction to the node.otigenpolls for inclusion, reports the contract address once committed.- Done.
Upgrade
otigen upgrade --network testnet
What happens (smart contract path):
otigenbuilds the new version (same asotigen build).otigensubmits an upgrade transaction signed by the owner key.- The chain applies the upgrade after a grace period (configurable in
otigen.toml; default 100 waves) to give users time to verify. - After grace period: new WASM takes effect, version field increments. The full version history is retained on-chain (see Chapter 13 for parachain upgrade details).
For parachain upgrades, the upgrade flow routes through equal-power validator voting instead of owner-only authorization.
5.8 Wallet Management
The wallet is built into the otigen binary directly — no separate wallet daemon, no external dependency, no extra install step. The implementation is ported forward from the wright toolchain that this binary replaces; the wallet protocol, the keystore format, the file layout, and the subcommand surface are all preserved unchanged.
Why ported from wright
The wright wallet implementation was already production-quality: FALCON-512 keypair generation, AES-256-GCM keystore encryption, Argon2id key derivation from a user passphrase, in-memory key unlock with explicit re-lock. None of that needed to change with the WASM pivot — the wallet's job is to manage FALCON keys and sign transactions, both of which are unchanged across pivots.
So we copy it forward, preserving the format compatibility so wright-era wallet files (~/.pyde/wallets/*.json) can still be loaded by otigen wallet commands.
Subcommand surface
otigen wallet create --name alice
# Generate a new FALCON-512 keypair. Prompts for an encryption passphrase.
# Writes ~/.pyde/wallets/alice.json (encrypted keystore).
otigen wallet import --name bob --from-file ./bob.key
# Import an existing keypair (e.g., from a hardware backup).
otigen wallet import --name carol --pk-hex 0x... --sk-hex 0x...
# Import from raw hex (e.g., from another tool's export).
otigen wallet list
# Show all wallets in ~/.pyde/wallets/, with addresses and last-used timestamps.
otigen wallet export-pubkey alice
# Print the FALCON public key (safe to share; not the signing key).
otigen wallet balance --name alice --network testnet
# Query the live balance for this wallet's address.
otigen wallet remove --name old_wallet
# Delete a wallet keystore (with confirmation prompt).
Keystore format
A wallet is a single JSON file at ~/.pyde/wallets/<name>.json:
{
"name": "alice",
"version": 1,
"address": "0xa1b2c3d4e5f6...",
"falcon_pubkey": "0x...",
"encrypted_secret_key": {
"ciphertext": "0x...", // AES-256-GCM ciphertext of the FALCON private key
"nonce": "0x...", // AES-GCM nonce
"kdf": "argon2id",
"kdf_params": {
"salt": "0x...",
"memory_cost": 65536,
"time_cost": 3,
"parallelism": 4
}
},
"created_at": "...",
"last_used_at": "..."
}
The encrypted private key is decrypted in-memory only when the wallet is unlocked for signing. The plaintext key never touches disk and is zeroized when the wallet locks (explicit otigen wallet lock or process exit).
Signing flow
When otigen deploy, otigen upgrade, or any other subcommand that submits a transaction is invoked with --wallet <name>:
- Read the encrypted keystore from
~/.pyde/wallets/<name>.json. - Prompt for the passphrase (unless
--unlock-with-env PYDE_PASSPHRASEis set, for CI use). - Derive the AES key from the passphrase via Argon2id.
- Decrypt the FALCON private key in memory.
- Construct the transaction, hash it, FALCON-sign with the private key.
- Submit the signed transaction to the network.
- Zeroize the in-memory private key.
External signer protocol
For production deployment workflows requiring hardware signing or multi-party key custody, the toolchain supports an external signer protocol (modeled after ethers.js's external signer interface). Instead of otigen reading the keystore directly, it sends the transaction hash to an external process over a defined IPC protocol; that process returns a FALCON signature.
otigen deploy --network mainnet --external-signer "http://localhost:8765/sign"
This allows integration with:
- Hardware wallets (when FALCON-aware hardware wallets become available).
- HSM-backed signing services.
- Multi-party computation (MPC) signing.
- Air-gapped signing setups.
Native hardware wallet support and HSM integrations are planned post-mainnet; the external signer protocol is the v1 extension point.
Compatibility note
Wallets created with the old wright toolchain (~/.pyde/wallets/*.json written by wright wallet create) are bit-compatible with otigen wallet. You do not need to re-create wallets after upgrading the toolchain. The otigen binary reads, signs with, and writes the same file format.
5.9 The Console
otigen console --network testnet
A REPL against a Pyde node, useful for exploration and debugging:
otigen> wallet alice
Loaded wallet 'alice'. Address: 0xa1b2c3d4e5f6...
otigen> balance 0xa1b2c3d4e5f6
1,000,000 PYDE
otigen> call mytoken total_supply
{ "result": 1000000000 }
otigen> send mytoken transfer 0xdeadbeef... 500
Submitted tx 0x9a3f...
Confirmed in wave 18345.
The console is convenient but not authoritative — for production scripts and CI, use otigen non-interactively or via the SDKs.
5.10 What the Toolchain Does NOT Do
Deliberately omitted:
- Test runner — use the language's native test framework.
- Linter / formatter — use the language's native tooling (
rustfmt,prettier,gofmt,clang-format). - IDE integration — uses the language's standard LSP; no Otigen-specific IDE extension required.
- Documentation generator — use the language's standard (
rustdoc,typedoc, etc.). - Dependency manager — use the language's standard (
cargo,npm,go mod, etc.). - Custom syntax — there is none; the contract is whatever the language allows.
The toolchain wraps deployment-specific concerns. Everything else stays in the language ecosystems the authors already know.
5.11 Performance
The whole toolchain side of the pipeline — parse otigen.toml, validate every cross-cutting rule, walk the compiled .wasm for imports + exports + deterministic-feature compliance, build the canonical ContractAbi, Borsh-encode it, inject the pyde.abi custom section — measures in single-digit microseconds end-to-end. Validation work is essentially free against the file-system overhead of reading the .wasm and writing the four bundle files; a typical otigen build invocation is dominated by I/O (~1–5 ms in practice), not by validator CPU.
Reference numbers on an Apple M-series dev machine (arm64, macOS 15), measured by the criterion benches committed under crates/<crate>/benches/baseline/*.json in the pyde-net/otigen repo. Reproduce with cargo bench -p otigen-toml --bench parse_validate and cargo bench -p otigen-abi --bench abi_pipeline.
| Operation | Median |
|---|---|
selector_of (Blake3 prefix, function-name → 4-byte selector) | 50 ns |
Attributes::from_attributes (3-attribute set) | 1 ns |
from_project_config (build canonical ContractAbi from parsed TOML) | 449 ns |
Borsh encode ContractAbi (3-function contract) | 39 ns |
Borsh decode ContractAbi | 156 ns |
pyde.abi custom-section inject (3-fn realistic WASM) | 494 ns |
pyde.abi custom-section extract | 154 ns |
| WASM import validator (3 imports against the host-fn allowlist) | 196 ns |
WASM export validator (cross-reference vs ContractAbi) | 343 ns |
| WASM deterministic-feature validator (full function-body opcode pass) | 2.3 µs |
otigen.toml parse (canonical spec example, ~50 lines) | 23 µs |
otigen.toml cross-cutting validation pass | 278 ns |
otigen.toml parse + validate (stress: 100 functions + 50 events + 30 state fields) | 488 µs |
| Full in-memory toolchain pipeline (parse → validate → build → encode → inject) | 14.5 µs |
These numbers are tracked from commit pyde-net/otigen#6 forward. Future regressions surface on PRs that run cargo bench --baseline=v1.
The benches are intentionally tight scope — they measure the toolchain-side work, not the chain-side deploy validator (which redoes every check at deploy time per HOST_FN_ABI_SPEC.md §3.7 layer 3) and not the wasmtime AOT compilation step (which happens on the chain at first invocation of a deployed contract, not at otigen build time).
5.12 Reading on
- Chapter 3: Execution Layer — the runtime that contracts compile into.
- Chapter 4: State Model — what
sloadandsstoresee. - Chapter 11: Account Model — the ENS-style name registry that the toolchain registers against.
- Chapter 13: Cross-Chain (Parachains) — parachain-specific deploy and upgrade flows.
- Host Function ABI spec — the binary contract between WASM modules and the engine.
- The Otigen binary design spec — the engineering detail for the toolchain itself (lives in the engine docs).
Consensus: Mysticeti DAG
Note: This chapter reflects the post-May-2026 pivot. The previous HotStuff variant is archived in archive/.
Pyde's consensus is a Mysticeti-style DAG protocol. A committee of 128 validators participates each epoch; every round (~150ms), each member produces exactly one vertex; commits flow continuously at the round rate; finality lands at ~500ms median.
There is no single proposer, no view changes, and no separate prove-then-commit pipeline. Order emerges deterministically from the DAG by every honest validator independently.
1. Why DAG (Why Not HotStuff)
Pyde's previous architecture used a modified pipelined HotStuff with VRF proposer selection. Persistent wedges, head-divergence deadlocks, and view-change cascades motivated a rebuild.
The DAG approach removes the fragile parts:
| Problem in HotStuff | DAG resolution |
|---|---|
| Single proposer bottleneck | No proposer — every member contributes |
| View change protocol complexity | No view changes — eliminated entire failure class |
| Timing-driven slot pipeline | Data-driven rounds advance with quorum, not clock |
| Proposer can censor selectively | 127 honest can include; censorship requires near-unanimous |
| Proposer can extract MEV | No single party reorders; order emerges from DAG |
| Throughput limited by leader bandwidth | Scales with committee size |
| HotStuff bugs cluster in view-change code | DAG doesn't have view-change code |
The same lab/laptop devnet that hit ~4K TPS under pre-pivot HotStuff is the baseline against which DAG performance will be measured. Honest target: 10-30K plaintext TPS, 0.5-2K encrypted TPS, in production-realistic conditions for v1.
2. Worker / Primary Split (Narwhal Pattern)
Each validator runs:
- Workers (1 or more processes): handle high-volume transaction ingress, build batches, gossip batches peer-to-peer
- Primary (1 process per validator): handles consensus — produces vertices, gathers parents, signs state roots
┌──────────────────────────────────────────────────┐
│ Validator │
│ │
│ ┌───────────────┐ ┌────────────────────────┐ │
│ │ Workers │ │ Primary │ │
│ │ (N parallel) │ │ │ │
│ │ │ │ - One vertex / round │ │
│ │ - Tx ingress │◄──┤ - Tracks local DAG │ │
│ │ - Encryption │ │ - Anchor selection │ │
│ │ (if needed) │ │ - State root signing │ │
│ │ - Batches │ │ - DKG participation │ │
│ │ - Gossip │ └────────────────────────┘ │
│ └───────────────┘ │
└──────────────────────────────────────────────────┘
This separation is load-bearing: it lets data flow at network-rate while consensus messages stay small (~few KB).
3. The Vertex
#![allow(unused)] fn main() { struct Vertex { round: u64, member_id: u32, // committee position batch_refs: Vec<BatchHash>, // batches I have, by hash parent_vertex_refs: Vec<VertexHash>, // ≥85 round-(N-1) hashes state_root_sigs: Vec<StateRootSig>, // attestations on recent commits prev_anchor_attestation: VertexHash, // attests prior anchor decryption_shares: Vec<DecryptionShare>, // piggybacked partials falcon_sig: FalconSig, // sig over the vertex } }
Three categories of references in a vertex:
- batch_refs: point to data (batch blobs in worker storage)
- parent_vertex_refs: point to consensus structure (prior round's vertices)
- state_root_sigs + prev_anchor_attestation: point to consensus output (recent commits)
A vertex is dual-role: header (declaring what data I have) AND attestation (acknowledging prior-round work via parent refs). Parent refs ARE the implicit votes — no separate vote messages.
Vertex Size
Compact-encoded (parent refs as bitmap, hash truncation):
- Minimal: ~830 bytes
- Heavy (50 batches + 5 sigs + 85 partials): ~25 KB
- Hard limit: 64 KB
4. Rounds
A round is a layer in the DAG. The round counter is data-driven, not clock-driven:
A member ticks from round N to N+1 the moment they collect ≥85 valid round-N parent vertices in their local DAG view. Slow members lag behind in their counter; the slowest 43 of 128 don't block anyone (128 − 85 = 43 can lag without holding up the rest).
Round 5: [128 vertices, one per member]
↑↑↑↑↑ each refs ≥85 of layer 4 ↑↑↑↑↑
Round 4: [128 vertices]
↑↑↑↑↑ each refs ≥85 of layer 3 ↑↑↑↑↑
Round 3: [128 vertices]
... etc
Parent rule: parents must be strictly from prior round (round_N - 1). No skip edges in v1. This guarantees acyclicity; violations are slashable.
Round rate: ~5-10 rounds/sec depending on network conditions. Faster than 400ms slots while requiring no clock-based timeouts.
5. Anchor Selection
Each round has a deterministically-selected anchor:
anchor_member_id = Hash(beacon, round, recent_state_root) mod 128
Components:
- beacon: epoch-scoped randomness, published in last wave of prior epoch
- round: current round number
- recent_state_root: state root from N=3 rounds ago (limits anchor predictability to ~450ms)
Properties:
- Deterministic — every honest validator computes the same answer
- Unpredictable — depends on state root that wasn't known until recently
- No single proposer authority — anchor doesn't propose, it's just a starting point for the subdag walk
5b. Round vs Wave — terminology
The distinction matters because the two terms diverge under skips:
- Round = a horizontal layer in the DAG. Every committee member produces exactly one vertex per round. Round numbers always advance (~150ms each), never skip.
- Wave = a successful commit. Wave IDs only increment when an anchor commits. Wave IDs are sparse when rounds skip.
If round 5's anchor (Hash(beacon, 5, prev_state_root) mod 128 = validator 88) is missing:
- Round 5 still happens. The other 127 validators produce their round-5 vertices.
- No wave commits for round 5.
- Round 6 happens; its anchor is a different validator (probably).
- If round 6's anchor succeeds, that wave commits the subdag spanning rounds 5 + 6.
Chain cannot get stuck on one missing anchor. Each round picks a new anchor candidate via the formula, so consecutive failures require consecutive bad luck (or multi-validator outage). The protocol only enters hard-halt territory beyond ~20 consecutive failed anchors (per Chain Halt & Recovery).
Skipped rounds in the chain log
Wave commit records carry both the current anchor_round and the prior_anchor_round. Skipped rounds are implicit from the gap:
WaveCommitRecord(wave_id=6, anchor_round=10, prior_anchor_round=Some(9)) → no skips
WaveCommitRecord(wave_id=7, anchor_round=16, prior_anchor_round=Some(10)) → rounds 11-15 skipped (5 consecutive)
No separate skipped_rounds[] table. The gap IS the record.
5c. Missing-Vertex Handling
When a validator needs a vertex it doesn't have (either the anchor or any vertex in the subdag walk), it fetches the vertex from peers via the dedicated consensus channel.
Validator's local processing:
Walking anchor → parent V_a → V_a's parent V_b ...
Hits a missing vertex V_x referenced by some V in the subdag.
Issues fetch request: get_vertex(V_x_hash) to up-to-8 peers in parallel.
Outcome 1 (typical, ~99.9% of cases):
A peer responds within <500ms with V_x.
Validator verifies V_x's FALCON sig, places it in local DAG.
Continues subdag walk.
Wave commits normally.
Outcome 2 (rare):
No peer responds within the structural timeout (~rounds R+3 materialized).
Validator marks this commit as "cannot complete."
Wave does NOT commit; same handling as anchor-skip.
Vertices stay in DAG; next anchor will retry.
Critical principle: validators NEVER assume "the vertex wasn't gossipped" when missing it. They assume "I haven't received it yet" and fetch. Dropping waves on missing vertices would make the chain trivially censurable.
The fetch protocol lives in the network layer (Chapter 12 + companion/NETWORK_PROTOCOL.md) as a libp2p request-response stream, separate from gossipsub. The fetch is fire-and-retry — ask peer A, if no response in 500ms ask peer B, etc.
Skipped-round recovery walkthrough
5 consecutive skips (rounds 11-15), commit at round 16:
Round: 10 11 12 13 14 15 16
Outcome: WAVE 6 skip skip skip skip skip WAVE 7
(vertices still produced;
accumulate in DAG)
↑
subdag walk goes back
to wave 6's frontier
At round 16's commit (wave 7):
Subdag walk from v_r16_anchor:
- v_r16_anchor's parents = 85+ round-15 vertices
- each round-15 vertex's parents = 85+ round-14 vertices
- ...continue back through rounds 14, 13, 12, 11
- round-11 vertices' parents = 85+ round-10 vertices ← STOP
(these were committed in wave 6)
Subdag = vertices from rounds 11, 12, 13, 14, 15, 16
≈ 6 × 128 = 768 vertices total
Execute all txs in canonical order (round ascending → member_id → list_order)
Compute state_root after applying all changes
Compute events_root (Blake3 Merkle tree over canonical-ordered events) + events_bloom
Sign HardFinalityCert(wave_id=7, state_root, events_root, events_bloom)
Wave 7 record: WaveCommitRecord(wave_id=7, anchor_round=16, prior_anchor_round=Some(10),
state_root=..., events_root=..., events_bloom=...,
events_count=..., tx_count=..., gas_used=...)
The wave commit record carries both state and events summaries; both are threshold-signed in the HardFinalityCert so light clients verify event inclusion identically to state. Full structure + indexing mechanics: Host Function ABI Spec §15.2.
Properties:
- Zero tx loss. Every vertex produced eventually commits.
- Bounded latency cost. Each skip adds ~150-300ms to confirmation time.
- No special "catch up" code. The standard subdag-walk handles it. The BFS just walks more rounds when there's a wider gap.
6. Commit
When the anchor vertex collects sufficient support from later rounds (Mysticeti 3-stage support), a commit fires:
1. Anchor selected (deterministic by formula above)
2. Walk anchor's parent_vertex_refs transitively → collect "subdag"
3. Sort subdag deterministically:
- primary key: round number ascending
- secondary key: member_id
- tertiary key: list order within vertex
4. For each vertex in sorted order, dereference batch_refs
5. For each batch, threshold-decrypt (pipelined ceremony — partials already in flight)
6. wasmtime executes decrypted batches in canonical order
7. State root computed (Blake3 + Poseidon2 dual)
8. ≥85 committee FALCON-sign state root (piggybacked on next-round vertices)
9. ≥85 state-root sigs collected → finality declared
Commit Rate
- ~95% of rounds commit successfully in steady state
- ~5% skip (anchor offline or insufficient support); next round absorbs the data
- Average finality: ~500ms median, ~1s p99
No Skip Penalty
When a round skips, its vertices aren't lost — the next round's commit absorbs them via parent-chain traversal. Slow validators just contribute slightly later.
7. Committee
Size & Selection
- 128 active committee members per epoch, selected from the global validator pool
- Selection: uniform random from all validators with stake ≥
MIN_VALIDATOR_STAKE= 10,000 PYDE (single-tier model; no separate committee vs non-committee stake floor) - Anti-Sybil: operator identity binding, max 3 validators per operator
- Epoch length: ~3 hours wall-clock (commit count varies with network conditions — typically ~21,600 commits at the 500 ms median cadence)
# At epoch boundary, derive committee:
eligible = [v for v in all_validators
if v.stake >= MIN_VALIDATOR_STAKE and not v.jailed]
for slot in 0..128:
seed = Hash(beacon, slot)
member = uniform_random_pick(eligible, seed)
committee[slot] = member
eligible.remove(member) # without replacement
Equal Power Within Committee
All 128 members have equal voting weight, equal vertex production rate, equal anchor probability (uniform over members). Stake influences only:
- (a) eligibility (must meet
MIN_VALIDATOR_STAKE= 10,000 PYDE) - (b) proportion of the stake-weighted reward pool (yield distributes by
stake × uptime)
Activity rewards within the committee are contribution-weighted, not stake-weighted.
Why No Stake-Weighted Voting
- Sybil attack mitigated by operator identity cap (not by stake weight)
- Within-committee equality aligns with classical BFT theory
- Reduces plutocracy pressure
- Simpler protocol math (no stake weights in BFT thresholds)
8. BFT Properties
For n=128 validators:
f = ⌊(n-1)/3⌋ = 42(maximum Byzantine)threshold = 2f+1 = 85(quorum for commit / vertex cert / threshold decrypt)
The number 85 appears throughout the protocol:
- Vertex certification (parent refs in next round)
- Commit support
- Threshold decryption shares
- State root signatures
- DKG share threshold
Consistent across the protocol — avoids attack edges from boundary mismatches.
Safety
Holds under any network conditions assuming at most f = 42 Byzantine members (the BFT tolerance ⌊(n-1)/3⌋ with n = 128). Safety property: no two conflicting commits.
Liveness
Holds under partial synchrony (messages eventually delivered, bounded clock skew).
9. Randomness Beacon
Each epoch's beacon is produced by the previous epoch's committee:
1. All 128 members sign known message "epoch_N_beacon" with threshold-share keys
2. ≥85 shares combine into deterministic aggregated signature
3. beacon_N = Hash(aggregated_signature) → 32 bytes
4. Published in last wave of epoch N
Properties:
- Deterministic given the shares
- Unpredictable until ≥85 shares combine (no single party knows it)
- Bias-resistant (shares determined by DKG, can't be cherry-picked)
10. DKG (Distributed Key Generation)
Each epoch transition, the new committee runs DKG to produce a fresh threshold encryption key:
Pedersen DKG, multi-round protocol (~30-60s in background):
Round 1: Each member i picks random secret polynomial f_i(x), degree 84
Round 2: Each member broadcasts public commitments to f_i's coefficients
Round 3: Member i sends f_i(j) to each other member j (encrypted point-to-point)
Round 4: Member j verifies received shares against public commitments,
sums valid shares: s_j = Σ f_i(j) = f(j)
where f(x) = Σ f_i(x) is the combined polynomial
Result:
- Each member j holds s_j = f(j) (private share)
- Public key PK derived from public commitments
- SK = f(0) is NEVER computed
- Threshold = 85 of 128
Mathematical foundation: any 85 points on a degree-84 polynomial uniquely determine it (Lagrange interpolation). 84 points don't.
DKG runs in background during the prior epoch's last minutes. New committee has threshold key ready at epoch start. Plaintext consensus continues during DKG (encryption is optional anyway).
11. Threshold Decryption Ceremony
Encryption is per-transaction, not per-batch. A batch can contain any mix of plaintext and encrypted transactions. The threshold-decryption ceremony runs per encrypted transaction.
After commit fires, for each encrypted transaction across the canonical-ordered subdag:
Each committee member i (during prior rounds — pipelined):
- For each encrypted tx observed in mempool batches:
partial_i = ApplyShare(s_i, ciphertext_of_tx)
(single elliptic-curve op or polynomial multiplication, ~100μs-1ms)
+ FALCON sig over (partial_i, tx_hash)
- Piggyback the partial(s) on next-round vertex (no separate message channel)
At commit time:
- Subdag walk identifies all encrypted txs in the wave
- Collect their partials from the subdag's vertices (decryption_shares field)
- For each encrypted tx:
- Verify each partial's FALCON sig (~80μs per share)
- Once ≥85 valid partials collected: Lagrange interpolation → plaintext
- Batch the combine work: share-application math vectorizes well on SIMD/GPU
- wasmtime executes decrypted (plus already-plaintext) txs in canonical order
Pipelining
Partials are computed as soon as the encrypted tx enters the mempool DAG, before the commit fires. By commit time, partials are typically 80%+ propagated through vertex gossip. Effective post-commit decryption latency: tens of milliseconds.
Scale via batched share-combine
Headroom analysis, not a v1 claim. v1's honest encrypted-TPS target is 0.5-2K. The math below sizes what it would take to push encrypted throughput an order of magnitude further — useful for understanding the scaling lever, not a v1 promise.
At a hypothetical 100K encrypted TPS:
- Per-tx ceremony: 85 partials × ~80μs verify + ~1ms Lagrange = ~8ms CPU work
- 100K txs × 8ms = 800,000 ms of CPU per second total
- Naive sequential: not feasible. But share-combine vectorizes:
- Group partials by ciphertext, combine in parallel across cores
- GPU acceleration on the share-combine path is the realistic post-v1 scale lever
See WHITEPAPER §11 for honest scaling limits.
12. State Root Attestation
After wasmtime execution, each member computes the state root locally (deterministic from input). Members FALCON-sign the state root with explicit hash inclusion:
#![allow(unused)] fn main() { struct StateRootSig { commit_id: u64, state_root_hash: Hash, // explicit — both Blake3 and Poseidon2 signer_id: u32, falcon_sig: FalconSig, // FALCON over (commit_id || root_hash) } }
Sigs piggyback on next-round vertices. Finality requires:
- ≥85 sigs
- All attesting the same root hash
- All FALCON sigs verify
If sigs attest different roots → fork detected → hard halt (see CHAIN_HALT.md).
13. Failure Detection & Halts
Three types of halts:
| Type | Trigger | Authority |
|---|---|---|
| Soft stall | Network / quorum issues | Emergent |
| Hard halt | Contradictory state roots, equivocation cluster, DAG fork | Protocol-detected automatic |
| Emergency halt | Off-chain bug report, active exploit | Governance multisig (7-of-12) |
See CHAIN_HALT.md for full halt + recovery procedures. Rollback is bounded to 1 epoch (~3 hours) — operational flexibility without arbitrary commit reversibility.
14. Slashing
Equivocation, bad state-root signatures, invalid vertices, bad decryption shares, DKG failure, share withholding, extended downtime — all slashable. See SLASHING.md for the full catalog.
Correlated slashing applies a 2× multiplier when many validators offend simultaneously (punishes coordination, protects isolated failures).
15. Recovery Properties
- Single validator offline: other 127 continue normally. Validator catches up via gossip; loses activity rewards.
- 43+ validators offline (at the BFT quorum boundary, 85 active = 2f+1 with no margin): soft stall; downtime slashing PAUSES (partition-aware); resumes when active count returns to 86+ (one above the quorum minimum).
- Network partition: majority-side continues if quorum maintained; minority stalls.
- State root divergence: hard halt; investigation; rollback within 1 epoch; slashing for wrong-root signers.
The chain self-heals from any subset failure that maintains ≥85 functional validators.
16. Comparison
| Property | HotStuff (pre-pivot) | Mysticeti DAG (current) |
|---|---|---|
| Slot/round timing | 400ms clock | Data-driven (~150ms/round) |
| Proposer model | Single per slot (VRF) | None |
| View changes | Yes (cascade-prone) | None |
| Finality | ~1s+ (chained QCs) | ~500ms (per-round) |
| Throughput ceiling | Leader bandwidth | Committee parallelism |
| Censorship resistance | Proposer-dependent | 127-of-128 can include |
| MEV resistance | Proposer + threshold-enc | Structural (no proposer) |
| Liveness under failure | View-change cascades | Graceful (lag, no halt) |
17. Implementation Status
🔴 Mysticeti DAG implementation: not yet built. Pre-pivot HotStuff archived in archive/.
Implementation strategies:
- Option A: Fork Sui's Mysticeti (open source) and adapt to FALCON sigs. Saves substantial consensus engineering — Mysten Labs has spent years getting the algorithm correct.
- Option B: Write from scratch for full control. Larger surface to audit, more bugs to find.
Recommendation: Option A for v1. The work is audit + adaptation for FALCON sigs; correctness of the core algorithm leverages Mysten Labs' existing engineering.
References & Cross-References
- Full design: DESIGN.md §Consensus
- Threat model (consensus threats): THREAT_MODEL.md §Consensus Layer
- Failure scenarios: FAILURE_SCENARIOS.md
- Chain halt: CHAIN_HALT.md
- Slashing: SLASHING.md
- Validator lifecycle: VALIDATOR_LIFECYCLE.md
- Research papers:
- Mysticeti (Babel et al., 2024) —
https://arxiv.org/abs/2310.14821 - Bullshark (Spiegelman et al., 2022)
- Narwhal (Danezis et al., 2021)
- Mysticeti (Babel et al., 2024) —
State Sync & Chain Halt
This chapter covers how new nodes join the network (state sync) and what happens when consensus encounters problems (chain halt + recovery). Both are operational concerns that the design must address explicitly — the HotStuff pre-pivot architecture lacked clear procedures for both, contributing to the wedges that motivated the pivot.
Part 1: State Sync
The Problem
At 30K+ TPS, replaying every block from genesis is infeasible (~10^13 transactions/year). A new node joining the network needs a way to reach current state without full replay.
Three Sync Modes
| Mode | Use Case | Time |
|---|---|---|
| Full sync (genesis replay) | Archive nodes only | Infeasible at high TPS |
| Snapshot sync (default) | Most full nodes, new committee joiners | ~30-60 min on commodity |
| Light client sync | Mobile wallets, browser, dApp backends | Seconds-minutes |
Snapshot Architecture
Decoupled signing and chunk generation:
- Committee signs state root (cheap, every epoch boundary)
- Volunteers generate chunks (heavier, daily cadence)
#![allow(unused)] fn main() { struct SnapshotManifest { epoch: u64, snapshot_state_root_blake3: Hash, snapshot_state_root_poseidon2: Hash, chunk_manifest: Vec<ChunkRef>, current_committee_pubkeys: Vec<FalconPubkey>, // chain-of-trust signatures: Vec<FalconSig>, // ≥85 from prior committee } }
Why dual roots: Blake3 for fast native verification by syncing nodes; Poseidon2 for future ZK light-client compatibility.
Snapshot Cadence
- Committee root signing: every epoch boundary (cheap, ~5 KB manifest)
- Chunk publishing: every 8 epochs (~daily) by volunteer infrastructure
- Tail sync window: up to 24 hours of txs to catch up
Verification Flow
Phase 1: Discover & Verify Manifest
1. Bootstrap from seed peers
2. Discover manifest URLs/hashes from peers
3. Download signed manifest (~5 KB)
4. Verify ≥85 FALCON sigs against trusted committee pubkeys
Phase 2: Download Chunks
5. Discover peers serving snapshot
6. Download chunks in parallel (4 MB each)
7. Verify each chunk_hash against manifest
8. Bad chunks → ban peer, retry
Phase 3: Reconstruct State
9. Apply chunks to JMT
10. Compute Blake3 state root locally
11. Compare to manifest
12. Accept if match
Phase 4: Recent Sync (Tail)
13. Download blocks from snapshot point to current
14. Replay txs against snapshot state
15. Reach current state
Phase 5: Active Operation
16. Subscribe to gossip; begin participation
Chain-of-Trust Bootstrap
A new node verifies the chain of snapshot manifests from genesis:
Genesis block: contains committee_0.pubkeys (hardcoded)
↓
Snapshot at epoch 8: signed by committee 0, contains committee_8.pubkeys
↓
... etc forward, each signed by prior committee
For nodes that prefer speed over trustless verification: weak subjectivity checkpoints are published by foundation + reputable infrastructure providers. New nodes can trust a recent checkpoint and sync from there.
Light Client Mode
For mobile wallets, browser dApps:
- Storage: block headers only + cared-about accounts
- Operations: verify FALCON sigs on headers (~7ms), query accounts via JMT inclusion proofs
- Bandwidth: ~600 KB/year typical wallet usage
Time Estimates (Commodity, 100 Mbps)
Bootstrap from genesis (small): ~5 seconds
Manifest verification (85 FALCON): ~7 ms
Snapshot download (3 GB): ~4 minutes
JMT reconstruction: ~5 minutes
Recent tail sync (8 epochs): ~30 minutes
Total: ~40 minutes
See STATE_SYNC.md for complete protocol details.
Part 2: Chain Halt + Recovery
The HotStuff pre-pivot architecture suffered persistent wedges with no clear halt → investigate → recover procedure. The team patched live, accumulating safety subtleties. Pyde's post-pivot design EXPLICITLY:
- Separates three halt types
- Defines authority + procedure for each
- Builds drills into the operational plan
Three Halt Types
| Type | Trigger | Severity | Authority |
|---|---|---|---|
| Soft stall | Network / quorum issues | Liveness only | Emergent |
| Hard halt | Contradictory state roots, equivocation cluster | Safety risk | Protocol-detected automatic |
| Emergency halt | Critical bug, active exploit, hard-fork prep | High intentional | Governance multisig (7-of-12) |
Detection
Soft stall (automatic):
- No commit > 5 rounds (~5 sec)
- <85 vertices certified
- Active committee count < 86
Hard halt (automatic):
- State root divergence (2+ signed contradictory roots)
- Equivocation cluster (10+ in single epoch)
- DKG output mismatch
- Execution layer critical invariant violation
- DAG fork detected (should be impossible)
Emergency halt (manual):
- Critical bug discovery (off-chain)
- Active exploit
- Hard-fork coordination
What Happens During Halt
| Activity | Soft | Hard | Emergency |
|---|---|---|---|
| Vertex production | Continues (no quorum) | Stops | Stops |
| Commits | Paused | Paused | Paused |
| Tx submission | Queued | Queued | Queued |
| Decryption ceremonies | Paused | Stopped | Stopped |
| Slashing evidence acceptance | Continues | Continues | Continues |
| Gossip | Continues | Continues | Continues |
Key invariant: slashing evidence accepted during halt — attackers cannot escape consequences by triggering a halt.
Recovery Procedures
- Wait it out (soft stalls) — auto-recover
- Software update + replay (hard halts from bugs) — patch, verify, resume
- Rollback (max 1 epoch back, governance authorized) — controversial but bounded
- Hard fork (irreconcilable splits) — coordinated upgrade
- Emergency unhalt (false positives) — multisig releases
Rollback Policy
Bounded operational pragmatism:
- Maximum rollback window: 1 epoch (~3 hours)
- Within window: governance multisig can authorize
- Beyond window: only hard fork (community coordination required)
This is "weak finality with sunset" — operational flexibility for early detection without arbitrary commit reversibility. Industry standard pattern.
Test Plan
Mandatory drills before mainnet:
- Soft stall: deliberately offline 43 validators
- Hard halt: inject state divergence
- Emergency halt: practice multisig coordination
- Rollback: 1-epoch procedure
- Hard fork: coordinated upgrade
Frequency: quarterly in testnet, annually in mainnet. Runbooks per scenario, updated after every drill.
The HotStuff Lesson Applied
HotStuff broke because there was no clear halt procedure — patches accumulated under pressure. Pyde now has:
- Automatic detection of safety violations
- Explicit halt classification
- Pre-rehearsed recovery procedures
- Drill schedule
See CHAIN_HALT.md and FAILURE_SCENARIOS.md for complete operational specs.
References
- Full state sync spec: STATE_SYNC.md
- Full halt spec: CHAIN_HALT.md
- Failure scenarios + drills: FAILURE_SCENARIOS.md
- Validator lifecycle (jail mechanics): VALIDATOR_LIFECYCLE.md
Chapter 8: Cryptography
Pyde's cryptographic stack is post-quantum from genesis. There are no elliptic curves anywhere in the protocol — no secp256k1, no ed25519, no BLS12-381. Every primitive used to authenticate transactions, exchange keys, hash state, or prove randomness is built on lattices or hash functions.
This chapter specifies every primitive with the parameters Pyde actually ships, where they live in the codebase, and how they fit together.
8.1 Design Principles
Three constraints shape every choice:
-
Post-quantum security. Every primitive must resist both classical and known quantum attacks (Shor for factoring/DLP, Grover for brute force). This rules out RSA, ECDSA, EdDSA, BLS, ECDH, and anything else built on elliptic curves or integer factorization.
-
No trusted setup. No ceremony, no toxic waste. Every public parameter is either a NIST standard or a transparent algebraic constant.
-
Hybrid hashing — Blake3 for speed, Poseidon2 for ZK. Bitwise hashes (Blake3) saturate modern CPUs at multi-GB/s and dominate the high-volume native paths (JMT internals, gossip de-dup, batch hashes). Algebraic hashes (Poseidon2) are 30-50× slower in native execution but roughly 1000× cheaper inside an algebraic constraint system (STARK, future ZK validity proof). Pyde uses both: Blake3 where the work is off-chain or committee-signed, Poseidon2 where the hash may be exposed to a ZK circuit (state root, address derivation, signature payloads).
Traditional blockchain crypto stack:
Signatures: ECDSA (secp256k1) broken by quantum
Key exchange: ECDH broken by quantum
Hashing: Keccak-256 quantum-safe; not ZK-native
Randomness: BLS-based VRF broken by quantum
Pyde crypto stack:
Signatures: FALCON-512 lattice (NIST FIPS 206)
Key exchange: Kyber-768 / ML-KEM lattice (NIST FIPS 203)
Hashing: Blake3 + Poseidon2 hybrid: speed + ZK-friendly
Blake3 (Goldilocks-free, ~3 GB/s)
JMT internals, batch hashes, vertex hashes, gossip
Poseidon2 (Goldilocks field, ZK-native)
state root, addresses, MAC, VRF output, RNG mix
Threshold: Shamir over Goldilocks + Kyber + Poseidon2 KDF/MAC
PSS resharing: Lagrange interpolation over Goldilocks
Randomness: Lattice VRF (FALCON-proof + Poseidon2 output)
Symmetric: AES-256-GCM (hardware-accelerated)
The whole stack lives under crates/crypto.
8.2 FALCON-512: Digital Signatures
FALCON (Fast Fourier Lattice-based Compact Signatures over NTRU) is Pyde's
signature scheme. NIST standardized it as part of FIPS 206. Pyde uses the
FALCON-512 parameter set (LOGN = 9, dimension 512).
Why FALCON-512 over Dilithium / SPHINCS+
| Scheme | Pubkey | Signature | Verify time | Notes |
|---|---|---|---|---|
| FALCON-512 | 897 B | 600–900 B | very fast | smallest sigs, lattice (NTRU) |
| Dilithium-2 | 1312 B | 2420 B | fast | larger sigs, module-LWE |
| SPHINCS+-128 | 32 B | 7856 B | slow | hash-based, huge sigs |
A blockchain hashes signatures into every transaction, every consensus vote, and every finality certificate. A 666 B FALCON sig × 128 committee × per-slot finality cert × 10K blocks/hour adds up — Dilithium's 2420 B would inflate that by 3.6×, SPHINCS+ by ~12×. FALCON's compactness is what keeps the bandwidth budget reasonable.
Parameter set
| Parameter | Value |
|---|---|
| Polynomial degree n | 512 |
| Modulus q | 12,289 |
| Public key | 897 bytes |
| Secret key | 1,281 bytes |
| Signature | 600–900 bytes (variable, accepted) |
| Security level | NIST Level 1 (128-bit post-quantum) |
API
crates/crypto/src/falcon.rs exposes:
#![allow(unused)] fn main() { pub fn falcon_keygen() -> (FalconPublicKey, FalconSecretKey); pub fn falcon_sign(sk: &FalconSecretKey, msg: &[u8]) -> FalconSignature; pub fn falcon_verify(pk: &FalconPublicKey, msg: &[u8], sig: &FalconSignature) -> bool; pub fn falcon_batch_verify(items: &[(&FalconPublicKey, &[u8], &FalconSignature)]) -> bool; }
Determinism
Signing is deterministic. The implementation ties the FALCON Gaussian sampler
to a deterministic context derived from the input message, so the same
(secret_key, message) always produces the same signature. This is what
makes the lattice VRF (§8.7) work — the output is a deterministic function of
the inputs.
The domain-separation tag b"pyde-falcon-v1" is mixed into the signing
context to prevent cross-protocol signature reuse.
Where FALCON-512 is used
- Transaction signing — every transaction carries a FALCON-512 sig from the sender's account.
- Vertex production — every DAG vertex is FALCON-signed by its producer.
- State-root attestations — committee members sign
(wave_id, blake3_state_root, poseidon2_state_root)after each commit; ≥ 85 sigs constitute theHardFinalityCert. - Decryption share authentication — threshold partial decryptions are FALCON-signed by their producer.
- PSS resharing contributions — contributors sign their shares.
- P2P peer authentication — the FALCON handshake (
crates/net/src/auth.rs). - VRF proofs — every VRF output is paired with a FALCON proof.
- Slashing evidence — submitters sign their evidence transactions.
Batch verification
falcon_batch_verify checks an array of (pk, msg, sig) triples
sequentially. The current implementation is not algebraically batched —
it returns true only if every individual verification succeeds. Algebraic
batch verification (sharing FFT operations across signatures) is on the
post-mainnet hardening list; the sequential version is correct and meets the
current per-block budget.
8.3 Kyber-768 / ML-KEM: Key Encapsulation
Kyber is Pyde's key encapsulation mechanism. NIST standardized it as ML-KEM under FIPS 203. Pyde uses the Kyber-768 parameter set, NIST Security Level 3.
What is a KEM?
A KEM lets two parties agree on a shared secret without the symmetric key
ever crossing the wire. Alice runs encaps(pk) -> (ciphertext, shared_secret)
and sends the ciphertext to Bob. Bob runs decaps(sk, ciphertext) -> shared_secret. They now share a 32-byte symmetric key, which Pyde uses as
the AES-256-GCM key for the actual payload encryption.
Parameters
| Parameter | Value |
|---|---|
| Module dimension k | 3 |
| Polynomial degree n | 256 |
| Modulus q | 3,329 |
| Public key (encaps key) | 1,184 bytes |
| Secret key (decaps seed) | 64 bytes (full key derived on demand) |
| Ciphertext | 1,088 bytes |
| Shared secret | 32 bytes |
| Security level | NIST Level 3 (192-bit post-quantum) |
API
crates/crypto/src/kyber.rs:
#![allow(unused)] fn main() { pub fn kyber_keygen() -> (KyberPublicKey, KyberSecretKey); pub fn kyber_encapsulate(pk: &KyberPublicKey) -> (KyberCiphertext, SharedSecret); pub fn kyber_decapsulate(sk: &KyberSecretKey, ct: &KyberCiphertext) -> SharedSecret; }
The dependency is ml_kem = "0.3.0-rc.0" — a release-candidate of the NIST
final standard. Upgrading to the stable release once published is tracked as
post-mainnet hardening (task 057 in the mainnet plan).
Where Kyber is used
- P2P transport key exchange. When two nodes establish a libp2p connection, the QUIC handshake uses a hybrid Ed25519 + Kyber key exchange (Ed25519 for the libp2p PeerId, Kyber for forward-secure session keys). See Chapter 12.
- Threshold encryption for the encrypted mempool. The committee's threshold public key is a Kyber-768 key whose secret has been Shamir-split across 128 validators. See §8.5.
8.4 Hashing: Blake3 + Poseidon2
Pyde uses two hash functions, each chosen for a class of paths:
| Function | Speed (native) | ZK cost (constraints) | Used for |
|---|---|---|---|
| Blake3 | ~3 GB/s | ~150k per hash (huge) | JMT internal nodes, batch hashes, vertex hashes, gossip de-dup, RocksDB keys |
| Poseidon2 | ~60 MB/s | ~400 (small) | State root commitment, address derivation, threshold MAC, VRF output, FALCON sig hashing inside ZK circuits, poseidon2 WASM host function |
Blake3
Blake3 is the BLAKE family successor — based on the BLAKE2 compression function arranged as a parallelizable Merkle tree, with hardware acceleration on every modern CPU. Pyde uses Blake3 in its default configuration (256-bit output) for every hash that lives entirely off-chain or inside a trusted committee-signed structure.
Key Pyde-specific uses:
- JMT internal nodes —
blake3_pair(left, right)per Merkle level. At commodity CPU speed, an entire JMT update batch hashes in microseconds. - Batch hashes referenced from vertices — the worker batches transactions and identifies each batch by its Blake3 hash.
- Vertex hashes in the DAG — every consensus vertex is identified by its Blake3 hash.
- Gossip message de-duplication — Gossipsub uses Blake3 to detect duplicate broadcasts.
- RocksDB cache keys — Blake3 fingerprint of (key, version) for the LRU value cache.
Poseidon2: ZK-Friendly Hashing
Poseidon2 is the algebraic hash function used on paths that may be exposed to a ZK circuit, plus a handful of legacy paths kept for compatibility.
Why not Keccak or SHA-256?
Inside an algebraic system (a STARK, an MPC protocol, a future ZK validity proof), bitwise hash functions like Keccak-256 are catastrophically expensive — roughly 150,000 algebraic constraints per Keccak hash compared to a few hundred for Poseidon2. Even though Pyde doesn't ship a STARK at mainnet, the threshold-encryption MAC and the lattice VRF both benefit from a hash that's cheap inside an algebraic field, and the JMT itself amortizes the per-Merkle work better when the hash is field-native.
Construction
Poseidon2 is a sponge construction over a prime field. Pyde uses the
Goldilocks field (p = 2^64 − 2^32 + 1) because:
- Single field elements fit in a 64-bit register.
- Modular reduction is a shift-and-subtract.
- Hardware AES is independent of this field, so we can use both efficiently.
Parameters
| Parameter | Value |
|---|---|
| Field | Goldilocks (p = 2^64 − 2^32 + 1) |
| State width | 8 field elements (≈ 512 bits) |
| Rate | 4 field elements (256-bit absorb) |
| Capacity | 4 field elements |
| External rounds | 8 (4 initial + 4 terminal) |
| Internal rounds | 22 |
| S-box | x^7 (coprime to p − 1) |
| Output size | 4 field elements (256 bits) |
| Security level | 128-bit collision resistance |
(Verified in crates/crypto/src/poseidon2.rs test suite.)
API
#![allow(unused)] fn main() { pub fn poseidon2_hash(data: &[u8]) -> Hash256; pub fn poseidon2_pair(left: Hash256, right: Hash256) -> Hash256; pub fn poseidon2_many(hashes: &[Hash256]) -> Hash256; }
Domain separation is built into the encoding: variable-length inputs are length-prefixed before sponge absorption, and field elements are packed 7 bytes at a time (avoiding values that exceed the Goldilocks modulus).
Where Poseidon2 is used
- State root commitment — the dual-rooted state has a Poseidon2 root alongside the Blake3 root, signed by the committee.
- Account address derivation —
Poseidon2(falcon_pubkey). - CREATE / CREATE2 address derivation —
Poseidon2(deployer || nonce)orPoseidon2(0xFF || deployer || salt || code_hash). - Storage key derivation —
Poseidon2(contract, slot)for single fields, doubled for maps. Encoded as build-time constants by theotigendeveloper toolchain's state binding generator. - Transaction hashing — the canonical tx hash used for replay prevention and the wallet's signing target.
- Threshold MAC —
Poseidon2(0xFF...0xFF || secret || ciphertext). - VRF output —
Poseidon2(domain || fingerprint || input). - Epoch randomness combination —
Poseidon2_many(sorted_shares). poseidon2WASM host function — exposed to user-space contracts via the host-function ABI.
8.5 Threshold Encryption (Mempool MEV Protection)
Threshold encryption is what lets the encrypted mempool work: messages are encrypted such that no single validator (or coalition of < 85) can decrypt, but the active committee acting collectively can.
Construction
The scheme combines three pieces:
- Shamir Secret Sharing over the Goldilocks field — splits a secret into 128 shares of which any 85 reconstruct.
- Kyber-768 KEM — the underlying public-key primitive.
- Poseidon2 as a counter-mode keystream and as the MAC.
Implementation: crates/crypto/src/threshold.rs.
Setup (per epoch)
1. Generate a Kyber-768 keypair: (pk, sk_seed)
2. Split sk_seed into 128 shares using Shamir SSS:
- Random degree-(t-1) polynomial f over Goldilocks where t = 85
- f(0) = sk_seed
- share_i = (i, f(i)) for i in 1..=128
3. Distribute share_i to validator i
4. Publish pk as the committee's threshold public key
Encryption (user wallet)
(ciphertext, shared_secret) = Kyber.Encaps(pk)
keystream = Poseidon2_keystream(shared_secret, message_length)
encrypted_payload = message XOR keystream
mac = Poseidon2(0xFF...0xFF || shared_secret || ciphertext)
wire = (ciphertext, encrypted_payload, mac)
Decryption (committee)
For each ciphertext in the encrypted block:
Each validator i computes a blinded share:
blinded_i = raw_share_i + H(ct_hash || i || elem_idx)
Validator broadcasts blinded share on the consensus channel.
Combiner collects >= 85 shares, unblinds them by subtracting the
same H() values, then Lagrange-interpolates at x=0 to recover the
Kyber decapsulation seed.
Kyber.Decaps(seed, ciphertext) -> shared_secret
Verify MAC; on success, decrypt payload with the keystream.
Share blinding
Each share is blinded with a per-ciphertext, per-element mask
(H(ct_hash || validator_idx || element_idx)) before transmission. This
prevents a validator's share from ciphertext A from being reused against a
different ciphertext B — even if a validator's share leaked, an attacker
couldn't apply it to other blocks. The combiner has the ciphertext and can
unblind during recovery.
Parameters
| Parameter | Value |
|---|---|
| Underlying KEM | Kyber-768 |
| Committee size n | 128 |
| Threshold t | 85 (~2/3, matches BFT quorum) |
| Per-share size | ~256 bytes (blinded) |
| Decryption latency | ~10–15 ms once t shares present |
8.6 PSS — Proactive Secret Sharing and Resharing
The committee rotates each epoch; the threshold public key does not change. PSS is what makes that work — at every epoch boundary the shares are refreshed without anyone learning the underlying secret.
Why PSS
Without PSS, every committee rotation would require a fresh distributed key
generation (DKG), which is O(n^2) interactive and slow. PSS achieves the
same goal with a single round of asynchronous contributions per validator.
Same-committee refresh
Used for routine forward-security refresh:
Each member generates a degree-(t-1) polynomial f_i with f_i(0) = 0.
Each member sends f_i(j) to every other member j.
Each member j updates: new_share_j = old_share_j + Σ f_i(j)
Because every f_i(0) = 0, the underlying secret is unchanged.
But every share is now drawn from a fresh combined polynomial.
The verification check verify_refresh_contribution confirms the first
t evaluations interpolate back to zero — catching contributors who tried
to inject a non-zero free term.
Cross-committee resharing
Used at epoch boundaries when membership changes:
Each old member i with share s_i picks a fresh degree-(new_t - 1)
polynomial g_i with g_i(0) = s_i. They evaluate at the indices of
the new committee and ship the resulting sub-shares.
Each new member j collects threshold contributions, applies a
canonical-subset rule (lowest-from_old_index first), and aggregates:
new_share_j = Σ (lambda_i × g_i(j))
where lambda_i are Lagrange coefficients at x=0 over the OLD indices.
Result: H(0) = original secret; H is the new polynomial; the new
committee sits on H.
The canonical-subset rule is critical. Different new members must
deterministically agree on which t contributions to use, or they end up on
different polynomials. The rule: sort contributions by from_old_index, take
the first t. This is implemented as canonical_resharing_subset() in
crates/crypto/src/threshold.rs.
The aggregation delay
Because the network delivers contributions asynchronously, every new member
waits RESHARE_AGGREGATION_DELAY_ROUNDS = 5 rounds after entering the new
epoch before aggregating. This guarantees that the same canonical set is
visible to every new member when aggregation begins.
Known limitation: no VSS / KZG commitments
The current verify_refresh_contribution and verify_resharing_contribution
detect polynomial inconsistency — if the sub-shares aren't all on the
claimed polynomial, the check fails. They do not detect a malicious
member who consistently presents a polynomial whose constant term is not
their actual share s_i. This would silently cause the new committee to
derive shares of a different secret, and threshold decryption would stop
working at the start of the next epoch.
The mitigation requires Pedersen or KZG commitments on the shares — a substantial crypto upgrade. For mainnet, the assumption is "committee-member compromise is rare," and any such corruption surfaces as a hard decryption failure within the first block of the affected epoch (highly visible). The upgrade is tracked as post-mainnet research.
8.7 Lattice VRF
Pyde's VRF is built on FALCON-512. The construction:
Output (deterministic):
fingerprint = Poseidon2("pyde-vrf-output-v1" || sk_bytes)
output = Poseidon2("pyde-vrf-output-v1" || fingerprint || input)
Proof:
msg = "pyde-vrf-proof-v1" || pk || input || output
proof = falcon_sign(sk, msg)
Verify(pk, input, output, proof):
msg = "pyde-vrf-proof-v1" || pk || input || output
return falcon_verify(pk, msg, proof)
Properties
| Property | Why it holds |
|---|---|
| Deterministic | Output is a Poseidon2 hash of (sk-derived) constants + input |
| Unpredictable | An attacker without sk cannot compute fingerprint |
| Verifiable | Anyone with pk can verify the FALCON sig over the input/output |
| Post-quantum | Inherits FALCON's NTRU-lattice security |
Where the VRF is used
- Anchor selection (indirect). Each round, the canonical anchor is
computed as
Hash(beacon, round, recent_state_root) mod 128(see Chapter 6 §3). The beacon itself is the threshold-aggregated VRF output of the prior epoch's committee — so VRF underpins anchor selection one step removed, not per-round. - Epoch randomness contributions. Each member of the previous epoch's committee contributes a VRF share that, combined with 84 others, seeds the next epoch's beacon.
- Committee selection scoring. At each epoch boundary, every registered
validator gets a VRF score from
epoch_randomness || "committee"; the uniform-random subset of eligible validators chosen by this score forms the next committee.
8.8 Symmetric Encryption: AES-256-GCM
All symmetric encryption uses AES-256-GCM:
- Threshold-encrypted transaction payloads. Once the Kyber KEM gives the wallet a 32-byte shared secret, the payload is encrypted with AES-256-GCM under that secret.
- P2P channel encryption (after the libp2p QUIC handshake — see Chapter 12).
- Wallet keystore encryption (
crates/pyde-rust-sdk/src/wallet.rs).
Properties
- 256-bit key (128-bit post-quantum security against Grover).
- AEAD — authenticated encryption with additional data; tampering is detected.
- AES-NI hardware acceleration on every modern CPU.
8.9 Key Derivation and Address Format
From keypair to address
Master seed (user-provided or random)
-> SHAKE-256 (with domain separator) -> FALCON keygen seed
-> FALCON-512 keygen
|
+-> Public key (897 bytes)
|
+-> Secret key (1281 bytes)
Address derivation:
EOA address = Poseidon2(falcon_public_key) // 32 bytes
CREATE = Poseidon2(deployer_address || nonce)
CREATE2 = Poseidon2(0xFF || deployer || salt || code_hash)
Why 32-byte addresses
Pyde uses 32 bytes (the full Poseidon2 output) instead of Ethereum's 20-byte truncation. Three reasons:
- Birthday-bound margin. A 20-byte address has 80-bit collision resistance. Marginal at chain scale; decisively safer at 128 bits.
- Native output size. Poseidon2 naturally outputs 4 Goldilocks field elements (≈ 256 bits = 32 bytes). Using the full output avoids a truncation step.
- Simpler key derivation. Every key derivation in the protocol produces 32 bytes; addresses match.
Wallet display
Addresses are stored and serialized as raw 32-byte values. Wallets render
them in hex (0xabc...123) or in a Bech32m-style human-readable format with
the pyde1... prefix for safety against typos. The choice of display format
is a wallet-side concern; the protocol doesn't care.
8.10 The Stack at a Glance
+----------------+ +----------------+
| FALCON-512 | | Kyber-768 |
| sigs | | KEM |
+----------------+ +----------------+
| |
v v
tx sigs P2P session keys
vertex sigs threshold pubkey (mempool)
state root attest |
PSS contributions |
| |
+-> Lattice VRF (FALCON sign + Poseidon2 output)
anchor seeding, epoch randomness, committee scoring
+----------------+ +----------------+
| Blake3 | | Poseidon2 |
| (high-volume) | | (Goldilocks) |
+----------------+ +----------------+
| |
v v
JMT internals state root commit,
batch hashes addresses, storage keys,
vertex hashes MAC, VRF output, RNG mix
gossip dedup poseidon2 host function
+----------------+
| AES-256-GCM |
+----------------+
payload AEAD,
wallet keystore
No elliptic curves anywhere. No trusted setup. Every primitive is either a NIST FIPS-standardized scheme (FALCON, ML-KEM, AES) or a widely-studied algebraic construction (Poseidon2, Shamir SSS, PSS).
8.11 Cryptographic Agility
Each primitive is accessed through a small, well-defined module
(crates/crypto/src/falcon.rs, kyber.rs, poseidon2.rs, threshold.rs,
vrf.rs). If a serious break is discovered in any one of them, the
affected module can be replaced through a protocol upgrade without
restructuring the rest of the system.
Because the address format is bound to a hash of the public key (not the key itself), a future migration to a different post-quantum signature scheme would change addresses — but the upgrade path is well-defined: a one-time key-rotation transaction signed by both old and new keys, with the address derivation domain-separated by scheme version.
That migration is not planned. NIST's FIPS standardization is the credible long-term anchor for FALCON and Kyber, and switching from them would only happen if a substantive cryptanalytic break appeared.
Summary
| Primitive | Use | Where |
|---|---|---|
| FALCON-512 | All signatures (txs, vertices, state roots, attestations) | crates/crypto/src/falcon.rs |
| Kyber-768 / ML-KEM | P2P session keys + threshold mempool encryption | crates/crypto/src/kyber.rs |
| Blake3 | High-volume native hashes (JMT, batches, vertices, gossip) | crates/crypto/src/blake3.rs |
| Poseidon2 | ZK-bearing hashes (state root, addresses, MAC, VRF, opcode) | crates/crypto/src/poseidon2.rs |
| Threshold scheme | 85-of-128 mempool decryption (Kyber + Shamir) | crates/crypto/src/threshold.rs |
| PSS (refresh + reshare) | Forward security + cross-committee handoff | crates/crypto/src/threshold.rs |
| Lattice VRF | Anchor seeding, randomness, committee score | crates/crypto/src/vrf.rs |
| AES-256-GCM | Symmetric AEAD (mempool payload, wallet keystore) | (via the aes-gcm crate) |
The next chapter walks through MEV protection end-to-end — how these primitives combine in the DAG commit pipeline to make front-running and sandwich attacks not "discouraged," but unexpressible.
Chapter 9: MEV Protection
Maximal Extractable Value is the single largest structural problem in production blockchain design. On Ethereum it transfers somewhere between $1B and $3B annually from ordinary users into the pockets of searchers, builders, and validators. On Solana the Jito stack is a tip auction by another name. Every "fix" attempted at the application layer ultimately relies on someone who can see your transaction before it lands.
Pyde does not mitigate MEV. It removes the mechanism by which it is expressible. This chapter walks through the four interlocking pieces that make front-running, sandwich attacks, JIT liquidity sniping, and ordering bribery infeasible at the protocol level — not in policy, in physics.
Post-pivot context. The earlier HotStuff design had a single proposer per slot, which was both the source of and the brake on MEV. After the May 2026 pivot to Mysticeti DAG consensus, there is no single proposer to bribe or collude with — each round, every committee member produces a vertex independently, and the canonical order is derived from a deterministically-selected anchor + commit certificate. This makes the MEV story even stronger, but a few details (ordering commitment, mandatory inclusion) have moved from "proposer asserts" to "DAG structurally enforces."
Encryption is optional per-transaction. Users who don't care about front-running (e.g., simple transfers, public DAO votes) can submit plaintext for lower fees and ~0.5-2× higher TPS. Users who do care (swaps, liquidations, arbs) encrypt and pay the threshold-decryption overhead. The protocol supports both.
9.1 The MEV Problem
What MEV looks like
The simplest sandwich attack:
Without MEV protection (Ethereum, Solana):
Mempool:
Alice: Buy 10,000 TOKEN_X at market
Searcher sees Alice's tx and bundles:
Searcher: Buy 5,000 TOKEN_X <- inserted BEFORE Alice
Alice: Buy 10,000 TOKEN_X <- executes at higher price
Searcher: Sell 5,000 TOKEN_X <- inserted AFTER Alice, profits
Result:
Alice pays a worse price.
Searcher pockets the slippage.
Builder/validator extracts a cut via tip or block-bid.
Variants: front-running (just the "Buy before"), back-running (capture an arb the victim's swap creates), JIT liquidity (provide liquidity right before a large swap, withdraw immediately after), liquidation sniping (race other liquidators for a discount).
Why mitigation isn't enough
Every "mitigation" approach in production today shares one defect: at least one party — a builder, a relay, a private-mempool operator — can see your transaction before its position in the block is final.
| Approach | Who still sees the tx |
|---|---|
| PBS / MEV-Boost | Builders + relays |
| Fair ordering | Network observers (latency-exploitable) |
| Batch auctions | Solver (and only fixes one tx type — swaps) |
| Private mempool | Builder still sees |
| Commit-reveal | Adds latency; doesn't address validator games |
As long as anyone can read your transaction before deciding where it goes, MEV extraction is possible.
Pyde's choice: make it information-theoretically impossible for anyone — the proposer, validators, observers — to know what a transaction does until after its position is irrevocably committed.
9.2 The Four Layers
Layer 1: OPTIONAL THRESHOLD-ENCRYPTED MEMPOOL
- Tx payload encrypted with the committee's threshold pubkey (Kyber-768).
- No single party can decrypt; 85 of 128 share-holders required.
- Encryption is opt-in per tx — txs that don't need MEV protection
can be submitted plaintext at lower cost.
- Closes (for encrypted txs): front-running, sandwich, JIT,
liquidation-sniping based on reading mempool contents.
Layer 2: COMMIT-BEFORE-REVEAL ORDERING
- The DAG anchor at round R commits to a canonical subdag ordering
of vertices (and therefore txs) BEFORE decryption shares for that
wave are released.
- The anchor is deterministic from epoch beacon + round; no single
validator chooses it. Decryption shares are piggybacked on
subsequent rounds' vertices and only combine once the subdag is
committed.
- Closes: post-decryption reordering. Because the order is fixed by
the DAG structure before contents are readable, even a colluding
85+ subset cannot rewrite the order after seeing contents.
Layer 3: STRUCTURAL INCLUSION (DAG)
- Every vertex from round R includes references to >= 85 parent
vertices from round R-1. A tx introduced into the DAG via any
honest member's batch is committed once any committed anchor
references the path containing it.
- There is no "proposer" who can selectively omit. Censorship
requires >= 44 validators (the equivocation threshold) to refuse
to reference the tx — a structurally visible attack.
- Closes: single-actor censorship of decryptable txs.
Layer 4: NO TIPS, NO PRIORITY FEES
- The wire format has no field for a tip, priority fee, or out-of-band
payment to any party.
- The fee is exactly gas_used * base_fee.
- Closes: bribery channels for ordering.
Each layer closes attacks the others alone could not. Removing any one re-opens a class of MEV.
9.3 Layer 1 — Threshold-Encrypted Mempool
The wire shape
Plaintext (visible from submission):
from 32 B (Poseidon2 of the FALCON pubkey)
nonce u64
gas_limit u64 (>= 21,000, <= 1.6B gas ceiling)
access_list Vec (state slots the tx will touch)
deadline u64? (wave height after which the tx is invalid)
chain_id u64
signature ~666 B (FALCON-512 over the canonical hash of all fields)
Encrypted (Kyber ciphertext + AES-256-GCM payload):
to 32 B
value u128
calldata Vec<u8>
fee_payer Sender | GasTank | Paymaster(addr)
tx_type Standard | Deploy | Batch | Stake* | Vote*
The committee's threshold public key is a Kyber-768 key whose secret has been Shamir-split across the 128 active validators (see Chapter 8). Any 85 share-holders combine to decrypt; any 84 learn nothing.
What's visible vs what's hidden
| Field | Visible | Why |
|---|---|---|
from | yes | Needed for signature verification + nonce window check |
nonce | yes | Replay protection (must fit the bitmap window) |
gas_limit | yes | Block gas accounting at proposal time |
access_list | yes | Drives parallel scheduling |
deadline | yes | Mempool eviction of expired txs |
chain_id | yes | Cross-chain replay protection |
signature | yes | Validates the whole tx |
to | no | Reveals counterparty |
value | no | Reveals transfer amount |
calldata | no | Reveals function call + arguments + intent |
The access list reveals which state slots the transaction touches, but not what it does to them. An observer can see "this tx touches the DEX contract's reserve slots" but cannot tell whether it's a buy, a sell, a liquidity add, or a fee claim.
Access-list padding (optional)
If a contract's slot pattern is unusually distinctive (rare), the wallet can pad the access list with read-only decoy slots. The decoys cost a small amount of gas (one Sload per slot, ~100 gas each) but flatten the access profile. Most contracts use overlapping slots for many operations, so padding is not needed in practice.
What MEV searchers see in the mempool
Pyde encrypted mempool (what an observer scrapes):
tx_hash | sender | gas_limit | access_list | encrypted
--------+----------+-----------+--------------------+-----------
0xabc...| Alice | 300,000 | [(DEX, [s7, s12])] | 0x8f3a...
0xdef...| Bob | 100,000 | [(NFT, [s1])] | 0x2c7b...
0x123...| Carol | 500,000 | [(DEX, [s7, s12])] | 0x91de...
The observer learns access patterns. They cannot construct an attack bundle because they don't know the swap direction, swap size, slippage tolerance, or token pair.
Anti-spam and per-sender rate limits
To stop a malicious user from flooding the mempool with garbage ciphertexts:
| Limit | Default | Why |
|---|---|---|
DEFAULT_MAX_TX_PER_WINDOW_PER_SENDER | 10 tx / 1 s | Token-bucket burst limit |
DEFAULT_MAX_CONCURRENT_PER_SENDER | 100 in pool | Cap concurrent pending txs |
RATE_WINDOW_MS | 1000 ms | Token-bucket window size |
Each sender has a SenderQuota tracking timestamp deque + concurrent
count; an add() past the limit returns MempoolError::RateLimited.
Ciphertext binding to FALCON pubkey
Each transaction's signature covers a hash that includes the ciphertext hash (Poseidon2 of the encrypted blob). A relay-inflation spammer who takes someone else's ciphertext and resubmits it under a different sender fails signature verification because the legitimate sender's signature binds the ciphertext to the original sender.
This is what makes per-sender rate limits work — every encrypted tx has exactly one valid sender it can attribute to.
9.4 Layer 2 — Commit-Before-Reveal Ordering (DAG)
In the post-pivot DAG architecture, ordering and decryption are structurally separated by the protocol — no proposer "commits" to an ordering, because there is no proposer. Instead:
-
Round R: every committee member produces one vertex with parent refs and batch refs. Encrypted transactions are referenced by batch hash; their plaintext is unknown to everyone (including the vertex producer, who cannot decrypt alone).
-
Round R+1 to R+3: later rounds reference round-R vertices as parents and accumulate Mysticeti's 3-stage support.
-
Anchor commit at round R+3: the deterministic anchor at round R+3 (selected by
Hash(beacon, round, recent_state_root) mod 128) collects sufficient support, and a canonical subdag traversal emits a fixed ordered list of vertices, batches, and transactions. -
Decryption shares released: committee members compute and broadcast decryption shares for the just-committed wave's encrypted transactions, piggybacked on round-R+4 vertices.
-
85 shares combined: any honest node assembles 85 shares per ciphertext, decrypts, and executes in the canonical order.
Round R : vertices produced (encrypted txs referenced by batch hash;
nobody can read contents yet)
Round R+1 : referencing vertices (still encrypted)
Round R+2 : 2-stage support accumulates
Round R+3 : anchor commit fires -> canonical order LOCKED
Round R+4 : decryption shares released -> contents revealed
The critical property: between the moment the anchor commits and the moment shares combine, the ordering is fixed by the DAG structure. There is no actor with both the ability to read contents AND the ability to alter ordering — those capabilities exist in non-overlapping rounds.
Why this is stronger than commit-then-broadcast
Under a single-proposer commit-then-broadcast scheme, you have to trust that the proposer can't both compute shares early AND alter the commitment. Under the DAG, you don't trust anyone: the order is a deterministic function of vertices that were already in the DAG before contents could be read. No commitment signature is needed, because the commitment is the DAG itself.
Implementation
The canonical subdag traversal and ordering emission live in
crates/consensus/src/subdag.rs. The deferred-decryption pipeline lives
in crates/crypto/src/threshold.rs and crates/consensus/src/wave.rs.
9.5 Layer 3 — Structural Inclusion (DAG)
Under HotStuff, a single proposer could selectively omit txs, motivating the local-view mandatory-inclusion check. Under Mysticeti DAG, there is no single proposer — every committee member produces a vertex each round, each vertex references batches from any worker the producer gossiped with, and every committed wave traverses the entire subdag.
For a transaction to be censored, a coalition of ≥ 44 validators
(equivocation threshold = n - 2f = 128 - 84 = 44) must all refuse to
reference the batch containing it. Below that threshold, ≥ 85 honest
vertices reference it and it lands in some committed subdag.
A tx submitted to ANY honest worker is gossiped to all 128 validators.
Each validator's primary produces a vertex referencing batches from
workers it received from. As long as 85+ committee members eventually
reference the batch (directly or transitively via the parent links),
the tx is committed in the next wave.
Censoring requires 44+ validators to coordinate omission — a structurally
visible attack with multiple independent forks of evidence.
Mempool-level mandatory inclusion (residual)
For tighter guarantees on a per-vertex basis, a validator can still skip or down-weight a vertex that omits txs visible in its local mempool view for >= grace_slots. This is defensive, not necessary for safety — the DAG already guarantees inclusion at the wave level. The check catches single-validator censorship attempts before they require coalition.
The audit logic lives in crates/mempool/src/inclusion.rs.
Cryptographic mempool commitments (post-mainnet)
Cryptographically aggregated mempool commitments (every committee member periodically gossips a hash-set of txs they've seen, then the DAG-level inclusion check is against the union) make censorship coalition-bounded even at the round level. This is tracked for post-mainnet hardening; not needed for safety at launch.
9.6 Layer 4 — No Tips, No Priority Fees
Pyde's gas model has no field anywhere in the wire format for a "priority fee" or "tip." Every transaction pays exactly:
fee = gas_used * base_fee
Where base_fee is algorithmically determined by EIP-1559 (target 400M gas,
4× elastic ceiling, ±12.5% per block adjustment). The only sender-controlled
fee parameter is gas_limit, which is a cap (refunded if execution uses
less).
Why this matters
Even if encryption + commitment + mandatory inclusion fully closed the direct ordering attacks, a tip would re-open the bribery channel. A searcher could pay a committee validator out-of-protocol to delay a tx, to position their own tx first, or to censor a competitor's tx. Tips create the economic incentive for all of those attacks; absent tips, no validator gains anything from any of them.
How ordering happens
Under the DAG, ordering is a deterministic function of the committed subdag — not a proposer choice. The subdag traversal at each commit emits transactions in a canonical order derived from vertex round, member id, and batch sequence. No actor — proposer, validator, observer — chooses positions.
For sequential nonce dependencies (a sender submitting txs n, n+1, n+2
in quick succession), the protocol uses the 16-slot bitmap nonce window
(see Chapter 11) — the txs can be included in any order within the window;
gaps are tolerated.
9.7 The End-to-End Walkthrough
A swap from Alice's wallet through the full pipeline:
Step 1 — WALLET (Alice)
- Build tx: to=DEX, calldata=swap(USDC, PYDE, 1000, min_out=950)
- Call pyde_estimateAccess(tx) -> returns gas + access_list
- Encrypt (to, value, calldata, fee_payer, tx_type) with the committee's
Kyber threshold pubkey: ciphertext + AES-256-GCM payload
- Sign the canonical hash with Alice's FALCON-512 secret key
- Submit via pyde_sendRawEncryptedTransaction
Visible to anyone who scrapes the mempool:
Alice sent a tx. It touches DEX slots [reserve_0, reserve_1, alice_bal].
300,000 gas. 0xpyde1abc... signature.
Hidden:
Direction (buy or sell), size, target tokens, slippage tolerance.
Step 2 — INGRESS VALIDATION (any RPC node)
- chain_id, FALCON sig, nonce window, gas-tank balance, gas ceiling,
deadline, access-list dedup, tx size, calldata size -> all pass
- Forward to a nearby worker; worker batches and gossips
Step 3 — DAG VERTEX PRODUCTION (round R)
- Each committee primary produces ONE vertex this round, with:
batch_refs: hashes of batches containing Alice's tx (and others)
parent_vertex_refs: ≥ 85 prior-round vertex hashes
state_root_sigs: attestations on recent commits
decryption_shares: PARTIAL shares for previously-committed waves
FALCON sig
- Nobody can read Alice's tx contents yet — full ciphertext payload.
Step 4 — DAG ANCHOR COMMIT (round R+3, ~500 ms after submission)
- Deterministic anchor at round R+3:
anchor_member = Hash(beacon, R+3, recent_state_root) mod 128
- Anchor collects Mysticeti 3-stage support -> commit fires
- Canonical subdag traversal emits ordered tx list — including Alice's
Step 5 — THRESHOLD DECRYPTION (rounds R+4 to R+5)
- Committee members compute decryption shares for txs in the just-
committed wave, blinded with H(ct_hash || member_idx || elem_idx),
and piggyback shares on their next vertices.
- Any honest node collects ≥ 85 shares per ciphertext, interpolates,
decrypts with AES-256-GCM. ~10-15 ms once 85th share arrives.
Step 6 — EXECUTION (hybrid scheduler)
- For each decrypted tx, build conflict graph from access lists.
- Functions with static access lists: parallel groups (Solana-style).
- Functions with dynamic access: Block-STM speculation (Aptos-style).
- Execute against pre_state_root; new post_state_root.
- Distribute fees: 70% burn, 20% to current epoch's reward pool, 10% treasury.
(Layer 4: no tip is paid because no tip field exists in the wire format.)
Step 7 — STATE ROOT ATTESTATION
- Each committee member FALCON-signs (wave_id, blake3_state_root, poseidon2_state_root).
- Sigs piggyback on subsequent vertices.
- ≥ 85 sigs -> finality. Typically ~500 ms median end-to-end.
Step 8 — RECEIPT
- Receipt available via pyde_getTransactionReceipt:
success, gas_used, logs, fee_paid, fee_payer, block_height
At no point in this flow does any party know transaction contents AND have the ability to influence ordering. That conjunction is what MEV requires; the protocol structurally denies it.
9.8 What Each Attack Vector Requires (And Why It Fails)
Front-running Alice's swap requires:
1. Know Alice's intent <- blocked by encryption (Layer 1)
2. Insert before Alice in this block <- blocked by ordering commitment (Layer 2)
3. Get into the block at all <- blocked by mandatory inclusion + sealed block
4. Have economic motive <- blocked by no-tip rule (Layer 4)
Sandwich attack requires:
1. Know the swap direction <- (1) above
2. Insert before AND after <- (2) and the sealed-block invariant
3. Bribe for specific positioning <- (4) above
Censoring a competitor's tx requires:
- Selectively omitting it <- blocked by mandatory inclusion (Layer 3)
Bribing a committee validator for ordering requires:
- A protocol mechanism to pay them <- doesn't exist (Layer 4)
Each attack requires a conjunction of capabilities. Pyde structurally denies at least one element of every conjunction.
9.9 Edge Cases
Insufficient decryption shares
If fewer than 85 valid shares arrive within ~2 rounds (~300 ms) of a commit, decryption fails for that wave. The DAG continues — subsequent waves are unaffected — but the affected txs remain encrypted and stuck in mempool until either enough shares arrive late OR the user resubmits. Non-responsive committee members are tracked toward the liveness slashing threshold (see Chapter 6).
Liveness assumption: as long as 85+ of 128 validators are honest and
online, decryption succeeds. This is the same f < n/3 assumption that
secures consensus.
Invalid decryption shares
A malicious validator could broadcast a fabricated share. The combiner verifies each share's blinding and the recovery's MAC; an invalid share doesn't poison the recovery (Lagrange interpolation over a sufficient honest subset still works). Detected bad shares are tracked toward slashing for "decryption withholding" (2% per offense).
Garbage ciphertexts
A user could submit a ciphertext that decodes but contains junk. After decryption, the AES-GCM authentication tag fails, the tx is invalid, and gas is consumed (sender pays). The mempool's per-sender rate limit (10 tx/s, 100 concurrent) caps the throughput an attacker can sustain.
Epoch boundary transitions
PSS resharing happens at every epoch boundary. The threshold public key is
unchanged across boundaries — wallets continue encrypting against the same
key. The 5-round aggregation delay
(RESHARE_AGGREGATION_DELAY_ROUNDS) ensures every new committee member
agrees on the same canonical contribution set before the new shares
become live.
Committee-member compromise
If a coalition of 85+ validators colluded, they could decrypt early. Even then, the ordering commitment forces them to commit to ordering before decryption — they can't exploit the early read for sandwiching. They could in principle censor (omit txs from blocks they propose), but that fails the mandatory inclusion check on every honest validator's view. The cost of 85+ collusion is 850,000 PYDE at risk plus the slashing exposure of every participant; the gain is sharply bounded by the structural protections.
9.10 Performance Cost
The MEV protection adds latency primarily in the deferred-decryption path:
| Step | Time (typical) | Where it lives |
|---|---|---|
| DAG anchor commit (waves) | ~500 ms median | crates/consensus/src/wave.rs |
| Threshold share computation | ~5 ms per tx (parallel) | crates/crypto/src/threshold.rs |
| Share gossip + 85-of-N collect | ~50-100 ms | piggybacked on next vertices |
| Recovery + AES decrypt | ~5 ms per tx | crates/crypto/src/threshold.rs |
Encrypted txs reach finality + execution ~600-800 ms median (vs ~500 ms for plaintext). Plaintext-only chains pay zero of this overhead.
Throughput impact. Encrypted txs are ~3-5× slower end-to-end than plaintext because the decryption pipeline serializes (shares must be gathered before the wave can execute). Realistic v1 targets:
- Plaintext: 10-30K TPS on commodity committee hardware
- Encrypted: 0.5-2K TPS on the same hardware
The bandwidth cost is per-share data piggybacked on consensus vertices (~250 KB/validator/wave), well within the 500 Mbps committee NIC budget.
9.11 What's Visible vs Hidden — Recap
+-----------------------------+------+------+
| Field | Plain| Enc. |
+-----------------------------+------+------+
| sender | Y | |
| nonce | Y | |
| gas_limit | Y | |
| access_list | Y | |
| deadline | Y | |
| chain_id | Y | |
| signature | Y | |
| to | | Y |
| value | | Y |
| calldata | | Y |
| fee_payer | | Y |
| tx_type | | Y |
+-----------------------------+------+------+
You see who sends, how much gas they're willing to pay, which slots they touch. You don't see what they're doing.
9.12 What This Doesn't Solve
Honest about the limits:
-
Information leakage from access lists. A sufficiently distinctive access pattern can leak operation type. The mitigation is at the contract-design level (DEXes already share the same slots for buys, sells, and liquidity ops in well-designed code) and the wallet level (optional access-list padding).
-
Out-of-protocol coordination. If a user signs an off-chain message saying "I will swap soon," anyone with that information can act on it. The protocol can't prevent users from leaking their own intent.
-
Long-run statistical profiling. A persistent attacker who watches Alice's access patterns over many transactions could infer her behavior. This is a privacy concern, not an MEV one — Alice's individual transactions are still safe from front-running.
-
Searcher-on-searcher games at the DEX/contract level. If a contract has a built-in tip mechanism (priority gas auctions inside the contract itself), Pyde's protocol-level MEV protection doesn't reach into it.
For mainnet, the in-scope guarantees are: no front-running by any committee member, no sandwich attacks composable through the mempool, no censorship of decryptable txs, and no bribery channel for ordering.
Summary
Pyde's MEV protection is not a feature bolted on to an otherwise standard chain. It is a structural property of the protocol arising from the interaction of four mechanisms:
| Layer | Closes | Lives in |
|---|---|---|
| Optional threshold encryption | Reading tx contents pre-inclusion (opt-in) | crates/crypto/src/threshold.rs |
| Commit-before-reveal (DAG) | Reordering after decryption | crates/consensus/src/wave.rs |
| Structural inclusion (DAG) | Single-actor censorship | crates/consensus/src/dag.rs |
| No tips / priority fees | Bribery for ordering | crates/tx/src/fee.rs |
Each layer addresses an attack the others alone cannot stop. Together, MEV extraction is not "discouraged" — it is unexpressible in the protocol.
v1 scope. Local-view mandatory inclusion is implemented and safe (a defensive backstop on top of structural DAG inclusion). Cryptographically aggregated mempool commitments + on-chain censorship slashing are tracked as post-mainnet hardening.
The next chapter covers the gas and fee model that the no-tip rule sits on top of.
Chapter 16: Security
Security is the substrate on which every other property of Pyde rests. This chapter catalogs the realistic attack surface at mainnet, the concrete defense for each class, the invariants that make the BFT safety argument work, and the operational hygiene that keeps a post-launch network healthy.
The scope of this chapter is the shipped mainnet. Where a defense is on the post-mainnet hardening list rather than live, the chapter says so.
Note. This chapter is the narrative security reference. The canonical catalog — ~50 threats by ID, organized by layer, with mitigation cross-references and acknowledged residual risks — lives in companion/THREAT_MODEL.md. External auditors should start with the threat model and use this chapter for context; readers building intuition should start here and dip into the threat model when they want the full catalog.
16.1 Attack Surface
| Attack class | Severity | Primary defense |
|---|---|---|
| 51% / Byzantine takeover | Critical | BFT f < n/3 with equal-vote committee, Mysticeti DAG safety |
| Long-range attack | High | Weak-subjectivity checkpoints; hard-finality irreversibility |
| Sybil attack | High | Layered: threshold encryption removes attack incentive + operator-identity cap (max 3/operator) + slashing + minimum stake floor |
| Eclipse attack | High | Layered discovery (no DHT) + FALCON peer auth + sentry pattern |
| DDoS (network-level) | Medium | Rate limiting, peer scoring, per-channel size caps, sentry |
| Front-running / MEV | High | Optional threshold encryption + commit-before-reveal DAG (Ch 9) |
| State manipulation | Critical | JMT batched Merkle proofs, deterministic replay, 2 state roots (Blake3+Poseidon2) |
| Quantum attacks | Critical | Entire stack is post-quantum from genesis (Ch 8) |
| Smart contract exploit | High | Default safety attributes (no reentrancy, checked arithmetic) enforced at runtime via the WASM execution layer |
| VM / runtime exploit | Critical | wasmtime sandbox (production-vetted at Microsoft / Fastly / Shopify), deterministic feature subset enforced, deploy-time import validation |
| Consensus persistence loss | Critical | WriteOptions::set_sync(true) + panic-on-persist-failure |
| Replay across chains | High | Mandatory chain_id in every tx hash |
| Treasury drain | Critical | Multisig-only spend + data_digest audit trail |
| Threshold crypto break | Critical | Hard halt + emergency pause + key rotation procedure |
Each of these is covered in more detail below.
16.2 BFT Safety and Liveness
The guarantee
Safety (Mysticeti DAG): no two conflicting subdag commits or state
roots ever achieve finality at the same wave, provided fewer than
f = ⌊(n-1)/3⌋ = 42 committee members are Byzantine. At n = 128,
this is f ≤ 42, threshold 2f + 1 = 85.
Liveness: the DAG advances and produces commits as long as
85 of 128 committee members are honest and online.
Why it holds
Each vertex carries ≥ 85 parent vertex references. An anchor commit at
round R+3 requires Mysticeti 3-stage support — at least 85 round-(R+1)
vertices that reference the anchor as a parent. Two conflicting commits
of contradictory subdags at the same wave would each need 85+ signing
vertices; the total exceeds n = 128, so at least 85 + 85 − 128 = 42
honest members would have had to equivocate (sign in both forks). Under
the Byzantine bound, at most 42 are adversarial. Equivocation is
slashable evidence at 100% of stake, so the cost is total. ∎
State-root divergence (two contradictory Blake3 state roots both signed by 85 members) is detected automatically and triggers a hard halt (Chapter 7 / companion/CHAIN_HALT.md).
What if more than 1/3 are Byzantine
The protocol cannot promise safety above the 1/3 threshold; that's a mathematical limit of BFT consensus. Defenses:
- Raise the cost. 10M PYDE per committee validator × 43 = 430M PYDE at stake minimum for a safety violation, all slashable at 100%. Slashing evidence can be submitted with a 10% finder's fee, creating economic incentive for whistleblowers.
- Weak-subjectivity checkpoints. If an adversary somehow accumulated ≥ 1/3 and started forking, nodes that sync from a recent checkpoint reject the fork outright (§16.3).
- Hard halt on detected divergence. State root divergence (two signed contradictory roots) triggers an automatic chain halt; the network stops producing commits until the divergence is resolved (Chapter 7).
- Social consensus. As with every BFT chain, the final backstop is human coordination: if the chain demonstrably goes off the rails, the honest majority forks away and the broken chain loses social legitimacy.
16.3 Long-Range Attacks and Weak Subjectivity
The attack
An attacker buys (or otherwise acquires) a majority of validator keys that were active at some point in the past. They create a long alternative chain starting from that point — completely different history, potentially different token holders. If a fresh node syncs without any reference point, it cannot distinguish the real chain from the alternative.
The defense: weak-subjectivity checkpoints
When a commit collects ≥ 85 FALCON state-root signatures, the
validator writes a FinalityCheckpoint to the consensus store:
#![allow(unused)] fn main() { struct FinalityCheckpoint { wave_id: u64, blake3_state_root: Hash, poseidon2_state_root: Hash, } }
(Stored under FINALITY_CHECKPOINT_KEY in
crates/node/src/consensus_store.rs.)
A node that's currently synced will refuse to reorg past the latest
checkpoint. FinalityTracker::can_reorg(wave_id) returns false for any
wave at or before the checkpoint's wave_id.
For a cold-syncing node, the protocol doesn't pick a checkpoint on its own — the node's operator provides a trusted recent block hash from a source they trust (the Foundation website, a public explorer, a known good peer). This is called "weak subjectivity" because new nodes must trust something outside pure protocol to anchor their sync.
The human-trust assumption is narrow: all you need is any one honest, recent observation of the chain. Once anchored, the node enforces its own local checkpoint going forward.
Bootstrap peers
The genesis block hash is built into the client binary — no external
trust needed for it. The MAINNET_BOOTSTRAP and TESTNET_BOOTSTRAP lists
(crates/net/src/discovery.rs) provide starting peers, which provide the
current chain state. A new node combines:
- Genesis block hash (hard-coded).
- Recent weak-subjectivity checkpoint (operator-provided).
- Current peer set (from
bootstrap_peers).
—to pin down which chain is real without requiring a full replay from genesis.
16.4 Sybil Resistance
The attack
An adversary creates many validator identities to dominate consensus —
bypassing the f < n/3 bound by simply being the majority of the
active committee.
The defense: layered, not stake-driven
Pyde's Sybil resistance is intentionally not anchored to stake size. The chain's structural MEV resistance removes the primary attack incentive, which lets the stake floor sit at a modest 10,000 PYDE (single tier) and shifts the security burden onto a stack of qualitative defenses. Five layers:
1. Threshold encryption removes the attack incentive. The dominant reason adversaries attack BFT consensus on production chains is MEV extraction — front-running, sandwich attacks, transaction reordering. On Pyde, this attack value is structurally near-zero. Even a Byzantine 1/3 cannot:
- Decrypt encrypted-mempool ciphertexts (requires 85 of 128 shares — see Chapter 8 §8.5);
- Reorder transactions after the DAG anchor commits the canonical order (Chapter 9 §9.4);
- Profitably front-run any opt-in-encrypted transaction.
This collapses the attack-profit equation that drives Ethereum-scale stake floors (32 ETH → ~$80–120K). Pyde does not need to price stake against MEV profits because there are no MEV profits to be made.
2. Operator-identity cap (max 3 validators per operator).
A Byzantine fork needs f + 1 = 43 of 128 committee slots. Under a
3-per-operator cap, that translates to ≥ 15 distinct KYC'd operator
identities — much harder to manufacture than capital. Identity binding
is enforced via the stake-account-to-operator mapping; high-stake
operators face additional KYC verification at registration.
3. Slashing at 100% on safety violations. Equivocation and bad state-root signatures incur full-stake slashing plus permanent ban (see Chapter 14 §14.5 / companion/SLASHING.md). The 10% finder's fee creates an active whistleblower incentive — every honest node has a financial reason to surface attacker evidence within the 21-day freshness window.
4. Hard-halt detection on state-root divergence. Two contradictory signed state roots trigger an automatic chain halt (Chapter 7 §Part 2). Attackers cannot quietly corrupt state — safety violations are loud, visible, and immediately interrupt block production. The 1-epoch bounded rollback policy contains damage to a narrow window.
5. Minimum-stake credibility deposit. The 10K PYDE floor is a credible-commitment deposit, not the load-bearing economic defense. It ensures every validator has some skin in the game and gives the slashing mechanism something to slash. Combined with the operator cap, the lower bound on committed capital for a 43-Byzantine attack is ≥ 15 operators × 3 validators × 10K PYDE = 450K PYDE locked plus the legal and reputational exposure of 15 KYC'd entities. Modest in dollar terms; meaningful in coordination terms.
The honest framing
The single-number "you'd lose $N million in stake to attack" argument that other chains lead with does not apply here. Pyde's claim is different and stronger: the protocol is designed such that there is no profitable attack to fund. Stake economics back this up at the margin. Operator identity binding does the heavy lifting on Sybil specifically. The threshold-encryption property does the work of removing the attack value entirely.
This shifts the trust assumption from "stake is large enough to deter attack" to "operator-identity binding + slashing + structural MEV-resistance jointly make attack unprofitable and detectable." The second is a substantively different argument and worth being explicit about.
Genesis Sybil resistance
The initial 128-validator set is Foundation-curated at genesis (Phase 10 of the launch plan, recruited + validated across 3+ regions). This is a "trusted launch" assumption — not that the Foundation is trusted forever, but that the initial set is diverse and honest. After genesis, committee rotation and permissionless stake-based registration take over.
16.5 Eclipse Attacks
The attack
An adversary surrounds a single target validator with only-adversary peers. The target sees whatever the adversary wants them to see: fake proposals, faked votes, a fake chain. If the adversary can eclipse enough validators, they can force consensus on a fake state (though safety still holds under the 1/3 rule, liveness can be hurt).
The defense
- Peer diversity. The peer manager
(
crates/net/src/peer.rs) caps connections per/24subnet. An adversary would need to control IP addresses across many subnets, not just spin up lots of VMs on one provider. - Layered discovery (no DHT). Pyde explicitly chose not to use a Kademlia DHT (Chapter 12). Discovery is layered: hardcoded seeds, DNS, on-chain validator registry, PEX, local cache. This eliminates the DHT-poisoning eclipse vector — an attacker can't pollute a routing table that doesn't exist.
- FALCON peer authentication (§12.4). After the libp2p connection, peers run a FALCON-signed attestation that binds PeerId to a post-quantum identity. An adversary can't clone a validator's PeerId without their FALCON secret key.
- Sentry node pattern (Chapter 12). Committee validators are reachable only through trusted sentry proxies — their real IPs are not in the public peer set. Eclipsing a committee validator requires compromising the sentry layer, not just the public network.
- Validator-channel filtering. The vertex channel only accepts messages from peers whose attested FALCON pubkey is in the current committee. A non-validator eclipse peer can inject garbage on gossip topics but cannot fake vertex signatures.
What isn't defended (yet)
The current peer-scoring system is deliberately simple (reputation =
messages_received - 10 * invalid_messages). A more sophisticated
gossipsub score with per-topic weights, decay parameters, and gray-listing
is on the post-mainnet hardening list. The current model is enough for
the DDoS-shaped threats at mainnet scale; more sophisticated Eclipse
attacks against one specific validator would show up as anomalous peer
behavior that operators could see in their metrics.
16.6 DDoS Resistance
Connection-level
#![allow(unused)] fn main() { DEFAULT_RATE_LIMIT_PER_IP = 5 conns/sec DEFAULT_MAX_PEERS = 50 DEFAULT_MAX_INBOUND = 30 DEFAULT_MAX_OUTBOUND = 20 }
Per-IP rate limiter throttles new connections; per-subnet limit prevents
one network from hogging peer slots. An attacker flooding an RPC endpoint
bumps against conn_rate_limit_per_ip and saturates at 5 new connections
per second per source address.
Evidence-ingest rate limiting (task 014d)
A non-validator peer can submit evidence messages to validators that then verify them. Naive validators would FALCON-verify every evidence message at ~60 µs each — enough for a flood of invalid evidence to saturate CPU.
The fix: token-bucket rate limit on evidence messages, applied per-peer.
Repeat offenders are dropped after the first failed verification instead
of verifying indefinitely. Lives in crates/net/src/ddos.rs.
Per-channel message size limits
Each gossipsub channel has its own max message size:
| Channel | Max size |
|---|---|
| Vertices | 256 KB |
| Transactions | 128 KB |
| Batches | 4 MB |
| Sync | 16 MB |
| Evidence | 64 KB |
Oversized messages are rejected and the sender takes a reputation hit.
RPC ingress validation (task P7a-3)
Invalid transactions never enter the mempool. The ingress validator
(crates/node/src/rpc.rs::ingress_validate) checks chain_id, FALCON sig,
nonce window, balance, gas bounds, deadline, access-list duplicates, tx
size, calldata size — all before returning Ok or gossipping. Pollution is
isolated to the single ingress node.
Mempool per-sender caps
DEFAULT_MAX_TX_PER_WINDOW_PER_SENDER = 10 (per 1-sec window)
DEFAULT_MAX_CONCURRENT_PER_SENDER = 100 (concurrent txs in pool)
A single spammer cannot flood the mempool. If they try, their per-sender quota blocks further submissions until the window slides.
16.7 Front-Running and MEV
Covered in detail in Chapter 9. The short version:
- Optional threshold-encrypted mempool. Tx payload hidden from everyone until 85-of-128 Kyber shares combine. Users opt in per tx.
- Commit-before-reveal DAG ordering. The DAG anchor commit at round R+3 fixes the canonical order; decryption shares are released only at R+4. No actor has both "can read contents" and "can alter order" at any single round.
- Structural inclusion. No single proposer to censor; censoring a tx requires ≥ 44 colluding committee members.
- No tips. The wire format has no priority-fee field.
Each layer closes attacks the others cannot. Together, MEV is not discouraged — it is structurally unexpressible.
16.8 State Manipulation
The attack
A malicious vertex producer submits a wave-anchor candidate whose claimed post-state root doesn't actually match the result of executing the wave's transactions. Honest validators would incorrectly accept a bogus state.
The defense
Every honest validator executes each committed wave themselves and
FALCON-signs (wave_id, blake3_state_root, poseidon2_state_root). A
malicious vertex producer that claims a wrong root gets 0 honest
state-root sigs; the network cannot reach the 85-sig finality bar.
Two conflicting state claims can't both reach finality (same BFT argument: > 1/3 would have to equivocate). State root divergence is hard-halt detectable (Chapter 7) — the network stops automatically once two contradictory signed roots appear.
JMT Merkle proofs
For light clients that do not execute the wave, the JMT batched proof +
the signed blake3_state_root + the committee's FALCON signatures are
the authentication path. A light client verifies:
HardFinalityCertfor the wave is valid (≥ 85 FALCON sigs).- The JMT proof from
blake3_state_rootto the specific leaf is valid. - The leaf value is what the light client was querying.
The chain of authentication is end-to-end cryptographic. ZK light clients
(post-mainnet) can use the parallel poseidon2_state_root for SNARK-based
verification at much lower cost.
16.9 Quantum Attacks
Every primitive in the protocol is post-quantum:
- FALCON-512 signatures — NTRU lattice, not factoring.
- Kyber-768 / ML-KEM key exchange — lattice, not ECDH.
- Poseidon2 hashing — algebraic, not affected by quantum.
- Lattice VRF — inherits FALCON security.
- AES-256-GCM — symmetric, 128-bit post-quantum security under Grover's algorithm.
The weakest link is Poseidon2's 64-bit post-quantum collision resistance
(Grover halves the exponent). 64-bit collision resistance requires
2^64 quantum hash evaluations, which is far beyond any realistic near-
or mid-term quantum capability. If cryptanalytic advances tighten this,
a hash migration is a standard-shape protocol upgrade.
Pyde has no elliptic-curve crypto anywhere in the protocol. No secp256k1, no ed25519, no BLS12-381. The libp2p transport layer uses ed25519 for PeerId routing only; application-level authentication uses FALCON.
16.10 Smart Contract Safety
The default-safe properties Otigen the language provided are preserved in the WASM era. Mechanism changed; guarantees did not. See Chapter 5 §5.6 for the full attribute surface.
- No reentrancy by default. Every function is guarded by the WASM execution layer; opt out with the
reentrantattribute (language-native:#[pyde::reentrant]/@pyde.reentrant///pyde:reentrant/PYDE_REENTRANT). - Checked arithmetic. Encouraged by per-language SDK helper patterns; wrapping ops require explicit opt-in (e.g., Rust's
wrapping_addis explicitly named). - Typed storage. Declared in
otigen.toml[state]schema; the build tool emits type-safe accessors and the runtime enforces slot-hash uniqueness. - No
tx.origin. The host function ABI exposes onlycaller()(direct caller). The Solidity-style phishing vector is absent. - Access-list enforcement. Slot accesses against slots not declared in the contract's state schema fail at the host-function layer.
These defaults eliminate the most common smart-contract exploit classes at the toolchain + runtime level, not as library choices developers might forget.
The toolchain audit surface
The otigen developer toolchain — specifically its state binding generators and ABI extractor — is part of the audit surface. A codegen bug in a binding generator could emit accessor code that violates declared semantics. Mitigations:
- Unit tests per binding-generator output pattern. Each language target (Rust, AssemblyScript, Go, C/C++) has its own generator with its own test suite covering every accessor shape.
- Property tests for slot-hash determinism across languages — given the same
otigen.toml, all four generators must produce identical runtime slot_hash values for identical inputs. - External audit of the
otigentoolchain before mainnet. - Wasmtime as a trust-minimized dependency. The execution runtime itself is wasmtime, which inherits years of production fuzzing and Bytecode Alliance audit attention — we do not audit a VM we built ourselves.
16.11 WASM Execution Layer Safety
Pyde's execution layer is wasmtime (with Cranelift AOT). The trap surface is wasmtime's, augmented by host-function-specific traps Pyde injects through the ABI.
WASM-native traps
wasmtime traps when the executing module violates its sandbox or its fuel budget. The canonical trap conditions:
OutOfFuel IntegerOverflow IntegerDivisionByZero
MemoryOutOfBounds StackOverflow UndefinedElement
IndirectCallToNull BadSignature UnreachableCodeReached
TableOutOfBounds Interrupt (host-function traps)
Each trap is a clean revert: state writes roll back, gas is consumed up to the trap point (computed from fuel actually consumed), the transaction fails. There is no undefined behavior path. wasmtime's sandbox guarantees structural safety: no buffer overflows, no control-flow hijacks, no type confusion.
Pyde-specific traps via host functions
The host functions add another trap layer for Pyde-specific safety properties:
| Trap | When |
|---|---|
ReentrancyViolation | A cross_call re-enters a non-reentrant function |
AccessListViolation | A slot access targets a slot outside the declared state schema |
ViewFunctionStateModify | A state-modifying host call inside a view-attributed function |
NonPayableValueAttached | tx.value > 0 on a non-payable function |
ConstructorReentrant | An attempt to call a constructor-attributed function post-deploy |
GasTankExhausted | A sponsored function's contract gas tank ran out |
InsufficientBalance | transfer host call when sender balance is below amount |
ForbiddenImport | (deploy-time only) module imports a function outside the ABI allowlist |
Determinism enforcement
wasmtime is configured to reject any module that uses non-deterministic features. The config enforces (at module instantiation and at deploy validation):
cranelift_nan_canonicalization(true)— floating-point NaN bit patterns canonicalized identically across all validatorswasm_threads(false)— no threading (non-deterministic by definition)wasm_simd(false),wasm_relaxed_simd(false)— SIMD disabled until a deterministic-only subset is vettedwasm_reference_types(false),wasm_gc(false),wasm_function_references(false)— complexity surface gated until neededwasm_multi_memory(false),wasm_memory64(false)— explicit memory layout- No WASI imports
A deploy-time validator (crates/wasm-exec/src/validate.rs) re-checks the module's import section against the allowlist and rejects anything that would slip past wasmtime's instantiation check.
Trust-minimization of the runtime
We do not audit wasmtime itself — that work is done continuously by the Bytecode Alliance with years of production fuzzing under adversarial workloads. We pin a tagged wasmtime version per chain release, document the version in the protocol upgrade record, and require validators to upgrade in coordinated forks when we move it. This is a meaningfully smaller audit surface than maintaining a custom VM ourselves would have been (see The Pivot preface for the full reasoning).
16.12 Consensus-State Persistence
The risk. If a validator casts a vote, crashes before the vote is durable, and restarts with a different view, it can double-vote on restart — violating BFT safety.
The defense.
WriteOptions::set_sync(true)on every write to the consensus store (task 014a). A vote is not considered "cast" untilfsyncreturns.panic!+panic = "abort"on any persist failure (task 014b). The process terminates immediately. Continuing after a failed disk write is a BFT-unsafe operation; halting is the correct fail-safe.- Restart recovery reloads
seen_proposals,seen_votes,pending_evidencefrom the consensus store (task 003, 014c).
Microbenchmark (task 014f) confirmed the per-vertex-sig fsync cost is ~25.5 µs on Apple Silicon NVMe — ~39K writes/sec headroom against the ~150 ms round cadence (≥ 1000× margin).
Gradeful drain-and-shutdown on persist failure is a post-mainnet operational polish, not a launch blocker.
16.13 Replay Protection
Cross-chain replay
Every transaction includes chain_id in the canonical hash. A
transaction signed for mainnet cannot be replayed on a testnet; a
testnet tx cannot be submitted to mainnet. Chain IDs:
| Network | chain_id |
|---|---|
| Mainnet | 1 |
| Testnet | TBD |
| Devnet | 31337 |
The chain_id is always enforced; the dev_skip_signature flag only
disables signature verification for chain_id 31337 (devnet), and only
if the config explicitly allows it. On any chain_id other than 31337,
signatures are always required.
Same-chain replay
Each transaction has a nonce that must fit the sender's 16-slot bitmap window (Chapter 11). Once used, the bitmap bit stays set until the window slides past it. A replayed tx hits the bitmap and is rejected.
Multisig replay
Treasury multisig spends include the current MULTISIG_NONCE in the
signing bytes. After a spend, the nonce bumps, so the same signed bytes
cannot be replayed.
Emergency replay
EmergencyPause and EmergencyResume include the current
MULTISIG_NONCE in their signing context. A paused chain that auto-
expires cannot be re-paused by replaying the same signed payload.
16.14 Treasury Security
See Chapter 15 for the governance model. The treasury's on-chain protections:
- Multisig-only spend. No other transaction type drains the treasury account.
- Audit trail.
data_digest = hash(pip_file_contents)ties every spend to a published PIP. - Rotation.
RotateMultisigcan replace the signer set; no single signer is entrenched. - Writeback-clobber protection.
spend.target != tx.from,tx.to == 0x00. Prevents the post-execution pipeline from accidentally overwriting the spend. - Nonce-bound signatures. Each spend bumps
MULTISIG_NONCE; replays fail.
The multisig signer set is a trust assumption. The mitigation is scope: the signers can spend the treasury and rotate themselves; they cannot change consensus rules, supply, or fee distribution.
16.15 Operational Security
Aspects that are not cryptographic but matter at mainnet operation:
- Key management. Validator FALCON secret keys are kept in
hardware-backed storage where possible. Key rotation transactions
(
key_noncebump) exist for compromised-key recovery. - Sentry nodes. Validators typically expose a sentry node for P2P traffic and keep the validator process unreachable directly. This is a deployment concern, not protocol-enforced.
- Monitoring. Every node exposes Prometheus metrics; operators run alerting on consensus participation rate, block inclusion rate, and peer churn.
- Bug bounty. A permanent bug bounty program is part of the community allocation (Chapter 14). The Phase 7 testnet tier has its own bounty; the mainnet tier will be funded at launch.
- Incident response. Phase 10 of the mainnet plan specifies on-call rotation and incident response SOPs. The emergency-pause mechanism gives operational response a real lever during a live exploit.
16.16 Hardening Work In-Flight
Pre-mainnet hardening work tracked in the launch plan (chapter 19):
| Task | Status |
|---|---|
| Clippy/fmt/audit/deny in CI | Hardening track; shipping |
cargo-fuzz on wasm-exec / tx / consensus / RPC / otigen toolchain | 72+ h runs |
| Property tests on pipeline + tokenomics | Initial properties shipped; expanding |
| Witness 1 MB bound validation | Shipped |
Separate MAX_CALLDATA cap | Shipped |
unsafe block invariant docs | Being documented |
unwrap() triage on untrusted paths | Ongoing |
| ml-kem 0.3.0-rc -> stable upgrade | Post-standards-release |
| Persistent receipt store (archive mode) | Post-mainnet |
| Signed-commitment mandatory inclusion | Post-mainnet (Ch 9) |
| Pedersen / KZG commitments for PSS | Post-mainnet |
| Algebraic batch FALCON verify | Post-mainnet |
The honest shape at mainnet: a small, audited, heavily-tested core with a well-scoped set of known future hardening items.
16.17 External Audits
The launch plan schedules five independent external audits before mainnet:
| Audit scope |
|---|
| Consensus layer (Mysticeti DAG, anchor selection, finality, slashing) |
Execution layer (Pyde's host-function ABI, the wasm-exec integration, fuel-to-gas mapping, hybrid scheduler) |
Crypto implementations (FALCON, Kyber, Blake3, Poseidon2, threshold, PSS) — in pyde-crypto polyrepo |
| Networking layer (libp2p config, gossipsub, layered discovery, sentry pattern, DDoS) |
otigen developer toolchain (binding generators, ABI extraction, deploy flow, wallet) |
Note: wasmtime itself is not separately audited — it is a vetted production dependency from the Bytecode Alliance. The Pyde audit focuses on the integration surface (host functions, fuel mapping, validation gate, module cache) and on the toolchain that emits the WASM modules.
Critical + high findings are remediated before mainnet; audit remediations themselves are re-audited. Penetration testing (P2P flooding, RPC DoS, eclipse simulations) runs in parallel.
Summary
| Property / defense | Status at mainnet |
|---|---|
BFT safety f < n/3 | Shipped |
Liveness 85/128 honest + online (2f+1) | Shipped |
| Weak-subjectivity checkpoints | Shipped |
| FALCON peer authentication | Shipped |
| Validator-channel filtering | Shipped |
| Evidence-ingest rate limit | Shipped |
| Per-sender mempool rate limit | Shipped |
| RPC ingress validation | Shipped |
chain_id replay protection | Shipped |
| Multisig-only treasury drain | Shipped |
panic = "abort" on persist failure | Shipped |
| Set-sync(true) consensus writes | Shipped |
| WASM sandbox (wasmtime, production-vetted) | Inherited from wasmtime |
| Deterministic-feature-subset enforcement | Shipped (deploy-time validator) |
| Host-function-level safety traps | Designed; implementation in flight |
| Reentrancy guard (default-on) | Designed; runtime in flight |
| 1 MB witness size cap | Shipped |
| Separate MAX_CALLDATA cap | Shipped |
| Signed mempool commitments | Post-mainnet |
| Pedersen / KZG PSS commitments | Post-mainnet |
| Algebraic batch FALCON verify | Post-mainnet |
| Archive-node receipt store | Post-mainnet |
| External audits (5 specialists) | Pre-mainnet, Phase 8 |
The next chapter covers developer tools: the otigen developer toolchain, the pyde node binary, the Rust and TypeScript SDKs, the WASM crypto bindings, and the JSON-RPC surface.
Chapter 10: Gas and Fee Model
Pyde meters every operation in gas. The economic model on top of gas is EIP-1559 with 4× elastic blocks, deterministic 70/20/10 fee distribution, and no priority fees. There is no tip field, no builder/proposer separation, no bidding war for inclusion order.
This chapter covers the full model: gas costs per opcode, the EIP-1559 base fee math, elastic block sizing, the 70/20/10 split, sponsored transactions through gas tanks, and the calldata/tx size limits.
10.1 Gas Accounting
Pyde uses wasmtime's fuel mechanism for gas metering. At node startup, the engine establishes a deterministic mapping from gas units (the chain-level metering unit) to wasmtime fuel units. Every WebAssembly instruction consumes a configurable amount of fuel; host function calls also consume fuel manually, charged by the host based on operation cost (sstore is heavier than add, for example).
When fuel reaches zero, wasmtime traps the execution with an out-of-fuel error. The transaction reverts; the sender pays gas for all the work done up to the trap point. There is no refund.
#![allow(unused)] fn main() { struct ExecContext { gas_limit: u64, // set by the transaction gas_used: u64, // computed from fuel consumed during execution } }
Charging model: validate up front, deduct after execution
Step 1 — Ingress validation (at RPC):
Check sender.balance ≥ gas_limit × base_fee + value_attached.
If insufficient: REJECT before mempool admission.
Step 2 — Mempool admission + propagation:
No balance changes. Tx flows through workers, batches, vertices.
Step 3 — Execution (at wave commit):
Re-check balance (sender may have spent in prior txs of this wave).
Execute via wasmtime, tracking consumed_fuel.
On completion (success OR trap):
gas_used = fuel_to_gas(consumed_fuel)
charge = gas_used × base_fee
sender.balance -= charge + (value_attached if execution succeeded)
Step 4 — Fee distribution (always 70/20/10):
burn += charge × 0.70
reward_pool += charge × 0.20
treasury += charge × 0.10
No gas refunds in v1
Pyde v1 ships with zero gas refunds. gas_used is what the user pays, always. The sdelete host function is a regular metered operation; it has a lower gas cost than sstore (clearing a slot is less work than writing), but there is no refund applied on top.
The reasoning:
-
Ethereum had to roll back gas refunds via EIP-3529 after gas-token attacks (CHI, GST2) abused refunds to manipulate gas markets at scale. The refund mechanism turned out to be an attack surface, not a feature.
-
Pyde handles state cleanup at the engine layer via PIP-4's write-back cache + state pruning policy, not via user incentives. Storage doesn't accumulate unbounded regardless of whether users explicitly delete. The financial incentive is unnecessary.
-
Simpler accounting. No refund-capping rules, no two-step charge-then-refund logic, no edge cases. Receipts carry one number —
gas_used— and that's the charge.
Why fuel, not opcode counting
Fuel is built into wasmtime's Cranelift backend. Every basic block is instrumented to decrement a fuel counter; when the counter goes negative, execution traps. The instrumentation is efficient enough not to dominate execution time.
Implementing custom opcode-counting on top of wasmtime would be slower and add maintenance burden for no functional gain. The chain-side gas table maps WASM instruction categories and individual host functions to fuel costs; the engine consumes that table at startup and configures wasmtime accordingly.
Why a single dimension
Earlier drafts of this book described a two-dimensional gas model
(exec_cost + prove_cost) intended to price both CPU work and ZK proving
work separately. With ZK proving deferred to post-mainnet, the
proving-cost dimension does not exist at launch and the two-dimensional
model collapses into a single number — the chain-level gas total derived from wasmtime fuel consumption.
Should ZK proving land later, the second dimension can be re-introduced as a separate counter without changing the wire format (transactions already carry only gas_limit).
10.2 EIP-1559 Base Fee
Pyde's base fee adjusts every block by up to 12.5% in either direction based on whether the previous block exceeded or fell below the gas target.
Constants (crates/tx/src/fee.rs)
| Constant | Value | Meaning |
|---|---|---|
GAS_TARGET | 400,000,000 | 50% of the elastic ceiling |
GAS_CEILING | 1,600,000,000 | 4× target — hard block ceiling |
GENESIS_BASE_FEE | 50,000,000,000 quanta | Initial value at genesis |
MIN_BASE_FEE | 1 | Floor — cannot drop to zero |
ADJUSTMENT_DIVISOR | 8 | 1/8 = 12.5% max change per block |
Adjustment formula
#![allow(unused)] fn main() { fn adjust_base_fee(parent_base_fee: u128, parent_gas_used: u64) -> u128 { if parent_gas_used == GAS_TARGET { parent_base_fee } else if parent_gas_used > GAS_TARGET { let delta = parent_gas_used - GAS_TARGET; let bump = parent_base_fee * delta as u128 / GAS_TARGET as u128 / 8; parent_base_fee + bump.max(1) } else { let delta = GAS_TARGET - parent_gas_used; let drop = parent_base_fee * delta as u128 / GAS_TARGET as u128 / 8; (parent_base_fee.saturating_sub(drop)).max(MIN_BASE_FEE) } } }
Properties:
- Proportional adjustment. The change scales with how far the block deviated from target. A block at 75% target produces a smaller bump than one at 100% target.
- Capped at ±12.5% per block. No oracle, no governance vote.
- Bounded below by
MIN_BASE_FEE. Cannot reach zero. - Minimum increase of 1 quanta. Even at very low fees, a busy block bumps the fee at least one quanta.
Convergence at ~500 ms commits
Mysticeti DAG produces a commit every ~500 ms median (Chapter 6).
Each commit is the unit at which the base fee is recomputed (block and
commit are interchangeable here — Pyde collapses both concepts
since the DAG commits at per-commit granularity).
| Scenario | Time to 2× the fee |
|---|---|
| Sustained 100% full commits | ~11 commits (~5.5 s) |
| Sustained 4× full (max) | ~6 commits (~3 s) |
| Sustained empty | half-life ~5 commits (~2.5 s) |
Equilibrium under fluctuating demand sits around 50% of the gas target.
10.3 Elastic Blocks
Pyde blocks have two gas limits:
| Limit | Value (gas) | Role |
|---|---|---|
| Target | 400,000,000 | "Normal" block fullness |
| Hard ceiling (4×) | 1,600,000,000 | Cannot exceed even under congestion |
Block builders can pack up to 4 × GAS_TARGET = 1.6B gas into a single
block. When they exceed the target, the base fee for the next block rises
proportionally.
Gas usage during a congestion spike:
4× ┤ ......................... hard ceiling
│
3× ┤ +-+
│ / \
2× ┤ +---+ +---+
│ / \
target┤----+ +---+---- target line
│ / \
1× ┤ / +-...
│
+---------------------------------------> blocks
spike decay
base fee rises ~2x then settles
Why 4× and not higher
- Validator memory. A 4× block has up to 4× more transactions to buffer, decrypt, and execute. The per-validator memory ceiling caps how high this can safely go on commodity hardware.
- Decryption + voting timing. Threshold decryption shares for a 4× block take longer to combine; the commit timing budget assumes the worst case fits.
- State growth. Larger blocks drive faster state growth. The 4× ceiling bounds worst-case growth by the same factor.
Throughput estimates
At 2 commits/sec (~500 ms commit), GAS_TARGET = 400M, GAS_CEILING = 1.6B:
| Workload | Gas/tx | Theoretical target TPS | Realistic v1 (committee-bound) |
|---|---|---|---|
| Simple transfer | 21,000 | ~38,000 | ~20-30K plaintext / 1-2K encrypted |
| Token transfer (ERC-20) | 65,000 | ~12,300 | ~10-15K plaintext / 0.5-1K encrypted |
| DEX swap | 200,000 | ~4,000 | ~3-4K plaintext / 200-400 encrypted |
Honest v1 numbers. The theoretical numbers above assume committee hardware fully saturates execution. In practice, the v1 targets are 10-30K TPS plaintext, 0.5-2K TPS encrypted on commodity committee hardware (500 Mbps NIC, 32-core, 64 GB). Higher numbers require larger NICs and more cores; see Chapter 19 for the launch-strategy capacity table.
Real numbers depend on workload composition. The performance harness (companion/PERFORMANCE_HARNESS.md) is the only valid source of TPS claims — under the "claim 1/3 of measured peak" rule, the headline number is never the theoretical max.
10.4 No Tips, No Priority Fees
Pyde's transaction format has no priority-fee field. Every transaction pays exactly:
fee = gas_used * base_fee
There is no bidding, no auction, no out-of-protocol payment to any committee validator. The MEV-protection consequences are spelled out in Chapter 9; the gas-economics consequences are:
- Predictable fees. Wallets can quote a single number, not a range.
- No fee market gaming. No need for fee-estimation oracles or multi-priority queues.
- Simpler accounting. The fee distribution is a single division, not a base-vs-tip split.
How does ordering happen, then?
Under the Mysticeti DAG, ordering is a deterministic function of the committed subdag — vertices are produced independently each round, the anchor commit selects a canonical traversal, and transactions emerge in a fixed canonical order. No actor chooses positions; the order is structural (Chapters 6 and 9).
For sequential nonce dependencies, the protocol uses the 16-slot nonce
bitmap window (Chapter 11) — a sender can submit txs n, n+1, n+2
out of order; gaps are tolerated up to the window size.
Legitimate urgency
Use cases that need fast inclusion (liquidations, bridges, time-sensitive trades) have two routes:
- Pre-fund a paymaster's gas tank (sponsored tx — see §10.7) so the user doesn't bottleneck on liquidity.
- Use the deadline field to expire stale txs that were not included quickly, freeing the nonce slot for a fresh attempt.
Neither route bribes anyone for ordering.
10.5 Fee Distribution: 70 / 20 / 10
Every fee splits deterministically:
| Recipient | Share | Where it goes |
|---|---|---|
| Burn | 70% | Increments the on-chain TOTAL_BURNED counter |
| Reward pool | 20% | Pooled across all staked validators (active committee + validators awaiting selection), distributed each epoch by stake × uptime via lazy accrual |
| Treasury | 10% | Credited to the treasury account |
Note: in the pre-pivot HotStuff design the 20% went directly to the slot proposer. Under the DAG there is no single proposer, so the validator share goes to an epoch reward pool indexed by stake and uptime. See Chapter 14 for the per-validator yield math.
Implemented as distribute_fee in crates/tx/src/execution.rs:
#![allow(unused)] fn main() { pub fn distribute_fee(effective_gas: u64, base_fee: u128) -> FeeDistribution { let total_fee = effective_gas as u128 * base_fee; let burned = total_fee * 70 / 100; let reward_pool = total_fee * 20 / 100; let treasury = total_fee - burned - reward_pool; // remainder catches rounding FeeDistribution { burned, reward_pool, treasury } } }
The remainder-to-treasury pattern catches rounding dust so no quanta are lost.
Why not 100% burn?
A 100% burn (Ethereum's EIP-1559 model for the base fee) means validators get nothing from fees and depend entirely on inflation rewards. This works when inflation is generous, but it makes the security budget brittle: as inflation decreases, validator economics become fully dependent on tip volume, which Pyde doesn't have.
The 20% reward-pool share compensates the full staked validator set (both active-committee and validators awaiting selection, per stake × uptime) and ties their compensation to network usage in addition to inflation. Under the DAG there is no single proposer to credit, so the share is pooled and distributed at epoch end. The 10% treasury share funds protocol work via PIP-driven multisig spends (Chapter 15).
Why no prover share?
Earlier drafts of this book had a 70 / 20 / 10 split where the 10% went to provers. Without provers at mainnet, that 10% goes to the treasury. The on-chain math is the same; only the recipient changed.
If ZK proving lands in a future hardfork, the split can be adjusted by governance (a PIP + on-chain multisig action). Until then the treasury gets the 10%.
10.6 Fee Calculation Examples
Simple transfer (21,000 gas)
At GENESIS_BASE_FEE = 50,000,000,000 quanta:
fee = 21,000 * 50,000,000,000
= 1,050,000,000,000,000 quanta
= 1,050,000 micro-PYDE
= 1.05 milli-PYDE
= 0.00105 PYDE
Distribution:
Burn: 735,000,000,000,000 quanta (~0.000735 PYDE)
Validator: 210,000,000,000,000 quanta (~0.000210 PYDE)
Treasury: 105,000,000,000,000 quanta (~0.000105 PYDE)
High-congestion scenario
If sustained demand has driven the base fee 3.5× higher:
base_fee = 175,000,000,000 quanta
fee = 21,000 * 175,000,000,000 = 3,675,000,000,000,000 quanta = 0.003675 PYDE
Burn: 2,572,500 micro-PYDE
Validator: 735,000 micro-PYDE
Treasury: 367,500 micro-PYDE
Low-demand scenario
If sustained empty blocks have driven the base fee to half normal:
base_fee = 25,000,000,000 quanta
fee = 21,000 * 25,000,000,000 = 525,000,000,000,000 quanta = 0.000525 PYDE
The base fee keeps adjusting until the market clears — congestion makes it expensive to spam, low usage makes inclusion cheap.
10.7 Sponsored Transactions
A user with no PYDE balance can still transact if a contract or paymaster account pays the gas. Two mechanisms exist.
Gas tanks
Every account has a gas_tank: u128 field (see Chapter 4 / 11). It's a
balance separate from the account's spendable balance, dedicated to paying
gas on behalf of users.
Anyone can deposit to any account's gas tank:
deposit_gas_tank(target, amount)
Only the account owner can withdraw:
withdraw_gas_tank(target, amount, recipient)
To use a gas tank, a transaction sets:
tx.fee_payer = FeePayer::GasTank
The engine looks up the target contract's gas_tank, debits the fee from
there, and credits the receiver as usual. If the gas tank is empty, the tx
reverts (the sender did not pay).
Paymaster pattern
For more complex sponsorship (eligibility checks, per-user limits), a paymaster contract sits between the user and the target:
tx.fee_payer = FeePayer::Paymaster(paymaster_address)
The engine calls the paymaster's validate_sponsorship(user, target, calldata) -> bool function (gas-bounded — see below). If it returns true,
gas is debited from the paymaster's gas tank.
+----------+ +------------------+ +-----------------+
| User |----->| Paymaster |---->| Target |
| (no $) | | - eligibility | | Contract |
+----------+ | - rate limits | +-----------------+
| - gas tank pays |
+------------------+
Validation gas limit
To stop a paymaster from running an expensive validation function as a DoS
vector, the paymaster's validate_sponsorship has a hard gas cap of
100,000 gas. If validation exceeds that, the tx is rejected. This
prevents an adversarial paymaster from making mempool inclusion expensive
for relays.
Use cases
| Use case | Mechanism |
|---|---|
| Free-to-play games | Game contract's gas tank pays for player moves |
| DeFi onboarding | Protocol pays for first N swaps per user |
| Corporate dApps | Company paymaster covers employee transactions |
| Airdrop claims | Airdrop contract sponsors claim transactions |
| Governance voting | DAO pays gas for governance participation |
10.8 Gas Costs for Common Operations
The full WASM-instruction and host-function gas table is published in the Host Function ABI specification. The headline numbers for the operations that dominate real-world gas usage:
Storage
| Operation | Host function | Gas |
|---|---|---|
| Storage read | sload | 100 (warm) |
| Storage write | sstore | 200 (warm) |
| Storage delete | sdelete | 150 (no refund; cheaper than sstore) |
Crypto
| Operation | Host function | Gas |
|---|---|---|
| Poseidon2 hash | poseidon2 | 1,000 + 6 per 32B chunk |
| Blake3 hash | blake3 | 100 + 1 per 32B chunk |
| Keccak256 hash | keccak256 | 200 + 3 per 32B chunk |
| FALCON-512 verification | falcon_verify | 20,000 |
| Merkle path verification | host fn | 5,000 |
Cross-contract
| Operation | Host function | Gas |
|---|---|---|
| External call | cross_call | 2,500 + callee work |
| Contract deployment | system tx | 32,000 + init code |
Events
| Operation | Host function | Gas |
|---|---|---|
| Emit event | emit_event | 375 + 8 per byte |
WASM execution (per-instruction baseline)
| Category | Fuel cost |
|---|---|
| Arithmetic instructions | 1-3 fuel per op |
| Memory load/store | 5 fuel per op |
| Control flow | 1-2 fuel per op |
| Memory grow | 200 fuel per 64KB page (first touch) |
The build-time state binding generator (see Chapter 5) emits efficient access patterns; for example, a single map lookup expands to one host-function call rather than multiple. The wasmtime-AOT pass then compiles the resulting WASM to native code for execution.
10.9 Validation Limits
The transaction validator
(crates/tx/src/validation.rs) enforces these limits at RPC ingress:
| Limit | Value | Constant |
|---|---|---|
| Min gas limit | 21,000 | MIN_GAS_LIMIT |
| Max gas per block | 1.6B | BLOCK_GAS_MAX |
| Max tx size | 128 KB | MAX_TX_SIZE |
| Max calldata size | 64 KB | MAX_CALLDATA |
MAX_CALLDATA is a separate cap from MAX_TX_SIZE (per the audit
recommendation — task 055 in the mainnet plan). The split prevents an
attacker from building a tx whose calldata fills the entire 128 KB tx
budget and starves the rest of the encoded fields.
A transaction that fails any of these checks is rejected at the RPC node and never enters the mempool — pollution is constrained to that single ingress node.
10.10 Fee Estimation API
pyde_estimateGas runs the transaction in simulation against the current
state and returns the predicted gas consumption.
> pyde_estimateGas
> {
> "from": "0xpyde1abc...",
> "to": "0xpyde1def...",
> "data": "0x...",
> "value": "0x0"
> }
< {
< "gas_estimate": 45200,
< "base_fee": "0x2D79883D2000",
< "estimated_fee": "2260000000000000"
< }
Wallets typically multiply the estimate by ~1.10 to absorb state changes between estimation and inclusion. Because base fee can move at most ±12.5% per block, the inclusion-time fee is bounded relative to the estimation-time fee.
pyde_call runs read-only simulation without state mutation;
pyde_createAccessList produces the access list that should accompany the
transaction. Wallets typically chain these calls automatically:
createAccessList → estimateGas → submit signed tx with the resulting
access list.
10.11 Comparison
| Feature | Ethereum (EIP-1559) | Pyde |
|---|---|---|
| Gas dimensions | 1 | 1 |
| Base fee mechanism | Algorithmic (EIP-1559) | Algorithmic (EIP-1559) |
| Max base-fee change/block | ±12.5% | ±12.5% |
| Priority fee / tip | Yes | No |
| Block elasticity | 2× (15M target / 30M max) | 4× (400M target / 1.6B max) |
| Fee burn | 100% of base fee | 70% of total fee |
| Validator share | Tips only | 20% of total fee (no tip) |
| Treasury share | None | 10% of total fee |
| Native account abstraction | No (ERC-4337 add-on) | Yes (gas tanks + paymaster) |
| Storage rent | None | None (gas pays for the SSTORE) |
| MEV bribery resistance | None (tip-based ordering) | Structural (no tip; encrypted pool) |
10.12 Implementation Notes
Integer arithmetic
All fee calculations use integer arithmetic to avoid floating-point
non-determinism. Quanta are u128 (1 PYDE = 10^9 quanta — note this is
not Ethereum's 10^18 wei scale).
Overflow protection
compute_fee() uses checked_mul to detect overflow. Realistic inputs
(gas_used in millions, base_fee in billions of quanta) fit comfortably
in u128 (max product ≈ 2^60 * 2^40 = 2^100, well below 2^128). The
overflow check guards against pathological encodings.
Base fee in the commit header
Pyde's commit header is the equivalent of Ethereum's block header for fee-market purposes — each commit carries the base fee for transactions executed in that commit:
#![allow(unused)] fn main() { struct CommitHeader { // ... base_fee: u128, // base fee for txs in THIS commit gas_used: u64, // total gas consumed by this commit's txs gas_target: u64, // = GAS_TARGET (always 400M) gas_limit: u64, // = GAS_CEILING (always 1.6B) } }
(The web3-compatibility RPC methods pyde_getBlockByNumber /
pyde_getBlockByHash return a representation of this header, since
external tooling expects "block" terminology.)
The base fee for block N+1 is computed from block N's header by
adjust_base_fee() — every honest node arrives at the same value.
Summary
| Property | Value |
|---|---|
| Gas dimensions | 1 (single counter) |
| Base fee mechanism | EIP-1559, ±12.5% per block adjustment |
| Genesis base fee | 50,000,000,000 quanta |
| Gas target | 400,000,000 (50% of ceiling) |
| Gas ceiling | 1,600,000,000 (4× target — elastic max) |
| Priority fee / tip | None |
| Fee distribution | 70% burn / 20% reward pool / 10% treasury |
| Sponsored transactions | Native (gas_tank field + paymaster pattern) |
| Validation gas cap (paymaster) | 100,000 |
| Max tx size | 128 KB (MAX_TX_SIZE) |
| Max calldata size | 64 KB (MAX_CALLDATA) |
| Min gas limit | 21,000 |
| Storage rent | None |
The next chapter covers the account model the fee model sits on top of — addresses, the nonce window, multisig, and batch transactions.
Chapter 14: Tokenomics
PYDE is the network's native token. It pays for gas, secures consensus through validator staking, and funds protocol work via the treasury. This chapter covers the on-chain mechanics: supply, the inflation schedule, the fee distribution, validator economics, the vesting + airdrop machinery that ships at genesis, and the treasury that funds ongoing protocol work.
Numbers are taken from the actual code constants in crates/tx/src/fee.rs,
crates/slashing/src/lib.rs, and crates/consensus/src/validator.rs — not
from aspirational projections. Where a parameter is set at genesis (as
opposed to hard-coded), the chapter says so.
14.1 Denomination
PYDE has 9 decimals: 1 PYDE = 1,000,000,000 quanta (10^9).
1 quanta = 0.000000001 PYDE
1 micro-PYDE = 1,000 quanta (10^-6 PYDE)
1 milli-PYDE = 1,000,000 quanta (10^-3 PYDE)
1 PYDE = 1,000,000,000 quanta (10^9)
1 kilo-PYDE = 10^12 quanta (10^3 PYDE)
1 mega-PYDE = 10^15 quanta (10^6 PYDE)
All on-chain balances are stored as unsigned 128-bit integers in quanta. This easily covers the genesis supply of 1B PYDE (= 10^18 quanta) without overflow risk, and provides enough precision for micro-transactions.
(Note: Pyde's denomination is not Ethereum's 10^18 wei scale. SDKs expose the correct conversion automatically.)
14.2 Genesis Supply
Genesis total supply: 1,000,000,000 PYDE (1 billion).
#![allow(unused)] fn main() { pub const GENESIS_SUPPLY: u128 = 1_000_000_000 * 1_000_000_000; // = 10^18 quanta }
(crates/tx/src/fee.rs:84)
This is the entire on-chain PYDE in existence at block 0. From block 1 onward, new PYDE enters circulation only via the inflation schedule; no other minting path exists.
Distribution
The genesis allocation is set in the genesis configuration TOML; the on-chain machinery enforces:
- Per-bucket caps — the genesis builder rejects allocations that exceed the per-category caps to prevent oversupply.
- Vesting schedules — most non-validator allocations are subject to on-chain vesting (see §14.6).
- Validator subsidy stream — a portion of the genesis pool is reserved for validator subsidy that streams over a fixed window (§14.4).
- Airdrop pool — genesis seeds an airdrop account with the expected total; claims draw against it; the residual sweeps to the treasury after the deadline.
The exact percentages between buckets (treasury, team vesting, ecosystem, validator subsidy, airdrop) are governance-set parameters in the genesis file rather than protocol constants. The launch genesis is finalized by the Foundation in coordination with the validator set during the mainnet genesis ceremony (Phase 10 of the launch plan).
No supply cap
PYDE has no hard cap. The supply grows by a decreasing inflation rate and shrinks via the 70% fee burn. At target throughput the burn exceeds inflation and the network is net deflationary; at low throughput inflation dominates. The equilibrium depends on usage.
14.3 Inflation Schedule
The inflation rate decreases on a year-by-year schedule:
#![allow(unused)] fn main() { pub const INFLATION_BPS: [u16; 4] = [ 500, // year 1: 5.0% 300, // year 2: 3.0% 200, // year 3: 2.0% 100, // year 4+: 1.0% (terminal) ]; }
(crates/tx/src/fee.rs:92-98, expressed in basis points.)
Year Annual rate
---- -----------
1 5.0%
2 3.0%
3 2.0%
4+ 1.0% (terminal — never decreases further)
The 1% terminal floor exists so validators always have a baseline reward stream regardless of fee volume. At target throughput, fee burn easily exceeds 1% inflation; at lean throughput, inflation keeps validator economics viable.
Per-wave inflation reward
waves_per_year = 63_113_904 (2 commits/sec * 86400 s/day * 365.25 days)
reward_per_wave = GENESIS_SUPPLY * inflation_rate_bps / (10_000 * waves_per_year)
At year 1 (5%):
reward_per_wave = 10^18 quanta * 500 / (10_000 * 63_113_904)
≈ 792,202,572 quanta
≈ 0.792 PYDE per wave
At year 4+ (1%):
reward_per_wave ≈ 158,440,514 quanta ≈ 0.158 PYDE per wave
This per-wave reward credits the reward pool and the treasury at the shares specified by the on-chain reward distribution (see §14.4).
Why a decreasing schedule
- High initial inflation bootstraps validator participation before fee volume exists.
- Decreasing schedule rewards token holders as the network matures — early validators were taking risk that later operators don't.
- Terminal 1% stays low enough that ordinary fee burn at any meaningful usage produces net deflation.
14.4 Fee Distribution: 70 / 20 / 10
Every transaction fee splits deterministically (Chapter 10):
#![allow(unused)] fn main() { pub const FEE_BURN_PCT: u64 = 70; // burned (deflationary) pub const FEE_REWARD_POOL_PCT: u64 = 20; // distributed to stakers pub const FEE_TREASURY_PCT: u64 = 10; // treasury account }
(crates/tx/src/execution.rs:17-20)
The distribute_fee function:
#![allow(unused)] fn main() { pub fn distribute_fee(effective_gas: u64, base_fee: u128) -> FeeDistribution { let total_fee = effective_gas as u128 * base_fee; let burned = total_fee * 70 / 100; let reward_pool = total_fee * 20 / 100; let treasury = total_fee - burned - reward_pool; // remainder catches dust FeeDistribution { burned, reward_pool, treasury } } }
The remainder-to-treasury pattern means rounding dust never disappears.
Where each share goes
- Burn — increments the on-chain
TOTAL_BURNEDcounter under discriminator0x13. Permanently removes PYDE from circulation. - Reward pool — credited to the epoch reward pool account, distributed at epoch end to all staked validators (committee + non-committee) proportional to stake × uptime. Under the DAG there is no single proposer to credit; the pool model spreads rewards across the entire staked validator set.
- Treasury — credited to the treasury account at
Poseidon2("pyde-treasury"). Spent throughMultisigTx(Chapter 15).
Why 70% burn
- High burn pressure. At 10-30K sustained TPS with realistic fee loads, the annual burn exceeds the annual mint within a few years — net deflation.
- MEV resistance. A would-be MEV searcher who used Pyde for extraction would burn 70% of the captured value. Combined with the encrypted-mempool protections (Chapter 9), this further dis-incentivizes attempts.
- Validator share is meaningful but not dominant. 20% pool share is enough to reward staking without making validators primarily fee-driven.
Net inflation analysis
Net inflation = (mint per year) − (burn per year). Illustrative figures at a representative base-fee assumption:
| Avg TPS | Annual fee burn | Year-1 mint (5%) | Net change |
|---|---|---|---|
| 500 | ~5.6M PYDE | 50M | +44.4M (inflationary) |
| 5,000 | ~28M PYDE | 50M | +22M (inflationary) |
| 10,000 | ~45M PYDE | 50M | +5M (near-neutral) |
| 20,000 | ~70M PYDE | 50M | -20M (deflationary) |
| 30,000 | ~105M PYDE | 50M | -55M (strong deflation) |
At v1 realistic targets (10-30K TPS plaintext), the network is near-neutral to deflationary in year 1. At the 1% terminal inflation rate (year 4+), even very low TPS produces net deflation.
14.5 Validator Economics
Single-tier staking
#![allow(unused)] fn main() { pub const MIN_VALIDATOR_STAKE: u128 = 10_000_000_000_000; // 10,000 PYDE }
| Role | Min stake | Committee role | Earns |
|---|---|---|---|
| Validator | 10,000 PYDE | Eligible — uniformly-random selection each epoch picks 128 of the eligible pool | Reward pool share (stake × uptime) + inflation share. When selected to the committee: additional activity-weighted share |
| RPC node | — | None | Off-chain RPC fees only |
Single pool, no tiers. Every validator meeting the 10K PYDE minimum is in the same pool. At each epoch boundary, uniform-random selection picks 128 from the pool to form the active committee for that epoch (see Chapter 6 §7). There is no "committee tier" vs. "non-committee tier" — just one validator role, with committee duty rotating per epoch.
Equal voting in committee. All 128 committee members have equal vote weight regardless of stake. To get additional selection probability, a wealthy staker must register multiple distinct validators with separate FALCON keys and operator identities — and each faces independent slashing exposure plus the per-operator cap (see below).
Why 10K, not higher. Pyde's MEV-extraction attack value is structurally near-zero (threshold encryption + commit-before-reveal ordering eliminate the profit motive that drives Ethereum-scale stake floors). With the attack-incentive removed, stake serves as a credible-commitment deposit against slashable misbehavior rather than as the load-bearing economic defense. Pyde's Sybil resistance is layered (operator-identity cap + slashing + threshold encryption + state-root divergence detection) — see Chapter 16 §16.4 for the full security argument.
The 10K floor matches the spirit of Ethereum's "Lean Consensus" direction (reducing 32 ETH → 4 ETH as fast finality reduces reversibility-window risk) and keeps the modest-hardware-decentralization promise intact: at realistic launch valuations, the bond is accessible without being trivial.
Anti-Sybil: operator-identity cap. Maximum 3 validators per operator identity. An attacker pursuing a Byzantine fork needs 43 committee slots, which translates to ≥ 15 distinct KYC'd operator identities under the cap — meaningfully harder to manufacture than capital alone.
Income sources
A validator's gross income per year:
- Inflation share. A portion of the per-block inflation reward, paid
to the epoch reward pool. Distributed across staked validators
(committee + non-committee) proportional to stake × uptime —
discriminator
0x15tracks the active stake-weighted total used as the denominator. - Fee revenue. 20% of every fee in every committed wave flows to the same epoch reward pool, distributed by the same stake × uptime rule (there is no single proposer in the DAG to credit).
Lazy reward accrual
Rewards do not get pushed to the validator on every block — that would
mean N writes per block. Instead, a global per-stake accumulator
(REWARDS_PER_STAKE_UNIT at discriminator 0x14) tracks the cumulative
yield per unit of staked PYDE × uptime:
On each block:
rewards_per_stake_unit += per_block_reward / total_active_stake_weighted_by_uptime
On ClaimReward (tx type 6):
owed = (current_accumulator - validator.last_claimed_at) * validator.stake * validator.uptime_share
pay owed
validator.last_claimed_at = current_accumulator
ClaimReward is only valid for Active (status 0x00) and Unbonding
(status 0x01) validators; Exited (status 0x02) validators are
explicitly rejected to prevent post-exit accrual leakage.
Validator status lifecycle
#![allow(unused)] fn main() { enum Status { Active = 0x00, Unbonding = 0x01, Exited = 0x02, } }
Transitions:
register -> Active
StakeWithdraw -> Unbonding (30-day countdown)
unbond expires -> Exited (stake returned, removed from pool)
slashed (forced) -> Exited (stake reduced or zero)
Unbonding period
#![allow(unused)] fn main() { pub const UNBONDING_PERIOD_DAYS: u64 = 30; // wall-clock, independent of consensus cadence }
(crates/consensus/src/validator.rs)
A validator who initiates StakeWithdraw (tx type 4) cannot reclaim their
stake until 30 days have passed. The period must exceed the
21-day safety-evidence freshness window so attackers cannot withdraw before
their offense becomes provable.
During the unbonding window:
- Status is
Unbonding. - Stake is locked.
- Validator no longer signs (removed from active committee).
- Pending rewards continue to accrue and can be claimed via
ClaimReward. - Slashing for past offenses still applies — the unbonding window exists precisely so post-exit evidence can still penalize.
After 30 days, an explicit follow-up sweeps the unbonded stake back to
the validator's spendable balance and marks them Exited.
Slashing
Reused from Chapter 6 and companion/SLASHING.md. Penalties scale with stake (percentages of the offender's at-risk stake at the time of offense):
| Offense | Penalty (% of stake) |
|---|---|
| Double signing (safety) | 100% + permanent ban |
| Equivocation (DAG fork at round) | 100% + permanent ban |
| Liveness < 90% per epoch | 1% per epoch |
| Liveness < 50% per epoch | 5% + jail (next epoch) |
| Liveness == 0% per epoch | 10% + forced unbonding |
| Invalid vertex production | 50% (with proof) |
| Decryption withholding | 2% per offense (jail at 3) |
| Sentry exposure violation | 1% (warning escalation) |
Of every slashed amount:
- 10% pays the evidence submitter (
FINDER_FEE_PERCENT). - 90% is burned.
This permissionless evidence-and-burn model means anyone who detects misbehavior is incentivized to submit it, and slashed PYDE is removed from circulation rather than redistributed (preventing perverse "slashing profit" incentives).
Indicative APY
APY = (annual_PYDE_rewards / staked_PYDE) × 100. Rewards distribute by
stake × uptime, so per-token yield is uniform across all validators —
only the absolute PYDE earned scales with stake. Committee participation
adds an activity-weighted bonus, but the base yield is the same.
At year 1, assume 5,000 active validators averaging 100K PYDE staked each (~500M total staked, modest middle ground while supply distributes), 128 selected to the active committee, modest fee volume, 60% of mint flowing to the reward pool:
Inflation share to reward pool (assume 60% of mint):
~30M PYDE / 500M total staked ≈ 6% APY on staked balance
Committee bonus (activity-weighted, 128 of 5000):
marginal additional ~0.5-1% APY during the ~3 hr epoch a validator
is on the committee (and 0 the rest of the time)
Average over a year: small uplift for active operators
Yields vary with how much total stake competes for the pool and where inflation sits on the taper:
| Year | Active validators | Avg stake | Total staked | Inflation | Indicative APY |
|---|---|---|---|---|---|
| 1 | ~1,000 | 100K | 100M | 5.0% | ~30% |
| 2 | ~5,000 | 100K | 500M | 3.0% | ~3.6% |
| 3 | ~10,000 | 100K | 1B (incl. inflation) | 2.0% | ~1.2% |
| 4+ | ~10,000 | 100K | 1B+ | 1.0% | ~0.6% |
Year 1 yields are high by design — bootstrap incentive while the validator set grows from genesis. As more validators come online, the per-token yield compresses naturally. The 1% terminal inflation rate plus the 20% fee-share keeps the steady-state validator economic viable without unbounded dilution.
The exact split between reward pool and treasury inside the inflation mint, and the trajectory of total validator count, are governance parameters; the numbers above are rough sketches, not commitments.
14.6 Vesting
Genesis allocations (team, ecosystem) are subject to on-chain vesting.
#![allow(unused)] fn main() { struct VestingSchedule { start_wave: u64, cliff_waves: u64, duration_waves: u64, total_amount: u128, } }
(crates/tx/src/vesting.rs:29-34, wire format 40 bytes:
start:8 || cliff:8 || duration:8 || total:16 LE)
Unlock curve
wave_id < start + cliff -> unlocked = 0
wave_id >= start + duration -> unlocked = total_amount
otherwise -> unlocked = total_amount * (wave_id - start) / duration
Cliff > duration safeguard
A genesis misconfiguration where cliff > duration would trap funds
forever (the cliff fires before the duration ends, then the duration
"ends" but the cliff still applies). The slice-5.1 audit fix prioritizes
end-of-vesting over cliff:
#![allow(unused)] fn main() { if wave_id >= start + duration { return total_amount; // FULL UNLOCK regardless of cliff } if wave_id < start + cliff { return 0; } // linear interpolation }
Plus genesis validation rejects schedules where cliff > duration.
Validation integration
Every transaction validation reads the sender's vesting schedule and
subtracts vesting.locked_at(current_wave_id) from the account's balance
before checking that the sender can pay gas_limit * base_fee + value. A
sender cannot transfer locked tokens — the protocol enforces it at
ingress.
14.7 Airdrop
Genesis ships an airdrop pool with claims gated by Merkle proof.
State
| Discriminator | Name | Holds |
|---|---|---|
0x18 | AIRDROP_ROOT | Merkle root of the airdrop list |
0x19 | AIRDROP_DEADLINE | Slot height after which sweep is allowed |
0x1A | AIRDROP_CLAIMED | Per-leaf-index claim flag |
0x1B | AIRDROP_EXPECTED_SUM | Genesis pool size (sanity check) |
The airdrop pool account lives at Poseidon2("pyde-airdrop-pool"). At
genesis, the pool is funded with AIRDROP_EXPECTED_SUM (sanity check
against drift between the off-chain Merkle builder and the genesis
balance).
Merkle tree format
Leaf: Poseidon2(0x00 || leaf_index_le8 || address || amount_le16)
Internal: poseidon2_pair(left, right)
Direction bit comes from the leaf_index (prevents sorted-pair attacks where
an attacker could swap left and right siblings to forge a proof).
Claim flow (tx type 7)
data = [leaf_index:8 LE][amount:16 LE][proof_len:1][sibling_0:32]...[sibling_N-1:32]
ClaimAirdrop handler:
1. Check current_wave_id <= AIRDROP_DEADLINE.
2. Check claim hasn't been redeemed (AIRDROP_CLAIMED bit unset).
3. Verify Merkle path against AIRDROP_ROOT.
4. Debit pool by amount; credit claimant.
5. Set the claim bit.
Gas: 30,000 base + 5,000 per Merkle level. Early gas guard rejects if
tx.gas_limit < required_gas before mutating any state — fixed in PR
#212 to prevent under-paid claims from drifting state. Max proof length is
255 levels.
Sweep flow (tx type 8)
After the deadline, anyone can call SweepAirdrop:
SweepAirdrop handler (any sender):
1. Check current_wave_id > AIRDROP_DEADLINE.
2. Move pool's residual balance to the treasury account.
Gas: 40,000 flat. The sweep is permissionless because the funds belong to the protocol — anyone can submit it once the window closes. The early-gas guard pattern applies here too.
14.8 Treasury
The treasury is a system account at Poseidon2("pyde-treasury"). It
accumulates value from three streams:
- Genesis allocation — direct allocation in the genesis config.
- Fee share — 10% of every transaction fee.
- Inflation share — a configurable share of per-block mint.
- Airdrop residual — whatever wasn't claimed by the deadline.
Treasury spending is always through the on-chain MultisigTx (tx
type 9). There is no other path that drains the treasury account
(enforced by the pipeline writeback-clobber protections — see §14.9).
MultisigTx payload
#![allow(unused)] fn main() { struct MultisigSpend { target: Address, value: u128, data_digest: [u8; 32], // hash(pip_file_contents) — audit trail to PIP } }
The data_digest field is the on-chain link to the off-chain PIP
(Pyde Improvement Proposal) document. Anyone auditing the chain can
recover the PIP from its hash, verify the signers approved that exact
spend, and trace the on-chain action back to a published proposal.
Multisig configuration
| Discriminator | Name | Holds |
|---|---|---|
0x1C | MULTISIG_SIGNERS | Length-prefixed array of FALCON pks |
0x1D | MULTISIG_THRESHOLD | Required signature count (u8) |
0x1E | MULTISIG_NONCE | Replay-protection counter |
Max signers: 16 (MAX_MULTISIG_SIGNERS). Each spend bumps
MULTISIG_NONCE so the same signed bytes cannot be replayed.
Wire format (MultisigPayload in crates/tx/src/multisig.rs):
[op_version: 1] [op_body: variable] [sig_count: 1]
[sig_entry_0] ... [sig_entry_N-1]
sig_entry = [signer_index: 1] [sig_len: 2 LE] [falcon_sig: sig_len]
op_version = 0x01 (MULTISIG_VERSION)
Gas: 50,000 base + 50,000 per signature.
Rotating the signer set
RotateMultisig (tx type 10):
#![allow(unused)] fn main() { struct MultisigRotate { new_signer_pks: Vec<Vec<u8>>, // each is a 897-byte FALCON pk new_threshold: u8, } }
Rotation requires the current signer set to authorize. Validation checks: at least one new signer, threshold ≤ new signer count.
Gas: 60,000 base + 50,000 per signature + 10,000 per new signer.
14.9 Emergency Pause
A multisig-authorized circuit breaker that halts all transactions except
EmergencyResume.
Pause (tx type 11)
#![allow(unused)] fn main() { struct EmergencyPausePayload { duration_waves: u64, sigs: Vec<SigEntry>, } }
duration_waves ∈ [1, MAX_PAUSE_DURATION_WAVES]where the cap is 6,500,000 slots (≈ 30 days). Reject zero or excessive durations.- Reject re-pause if the chain is already paused.
- Sets
EMERGENCY_PAUSE_END_WAVE(discriminator0x1F) =current_wave_id + duration_waves. - Bumps multisig nonce.
- Gas: 40,000 base + 50,000 per signature.
Resume (tx type 12)
#![allow(unused)] fn main() { struct EmergencyResumePayload { sigs: Vec<SigEntry>, } }
- Requires the chain to be currently paused.
- Zeros
EMERGENCY_PAUSE_END_WAVE. - Bumps multisig nonce.
- Gas: 40,000 base + 50,000 per signature.
Pause-gate semantics
is_paused(state, current_wave_id) returns true if
current_wave_id < EMERGENCY_PAUSE_END_WAVE. While paused, the pipeline
rejects every transaction type except EmergencyResume before
running validation or charging gas. This means a paused chain cannot be
spammed into draining gas budgets.
The pause auto-expires (current_wave_id >= end_wave) without an explicit
sweep — the gate just stops returning true. This means the worst case for
a runaway pause is the 30-day cap, never indefinite.
Use cases
- Critical bug discovered after audit but before fix is deployed.
- Active exploit being mitigated; pause halts state mutation until a fix ships.
- Coordinated upgrade window (rare; voluntary upgrades are the normal path — see Chapter 18).
The signer set should be picked specifically for crisis response (likely core developers + security team multisig), not the same set that signs treasury spends. This is a configuration decision, not a protocol constraint.
14.10 Writeback Clobber Protection
A subtle pipeline interaction: every transaction's post-execution stage
unconditionally writes the sender's and recipient's account state back to
the JMT. If a MultisigTx handler credits a target that collides with
either tx.from or tx.to, the writeback would overwrite the credit.
The fix:
MultisigTxrejects ifspend.target == tx.from(submitter).MultisigTxrejects iftx.to != Address::ZERO(must not collide with a regular tx target).
Same defenses are applied to RotateMultisig to prevent any signer
collision from clobbering the signer-set update.
14.11 Active-Stake Divisor and Unified Parsing
The pool-share calculation divides by ACTIVE_STAKE_WEIGHTED_TOTAL
(discriminator 0x15) — the sum of stake × uptime_share across every
validator currently in Active status. This diverges from
VALIDATOR_COUNT (the total registered count) once validators exit or
are slashed, and from a flat-per-validator divisor once validators
differ in stake or uptime (the common case across the two staking tiers).
Without this divisor, exited validators would dilute the pool share — even though they're not contributing security. Adjusted on:
StakeWithdraw(validator transitions toUnbonding; their stake weight is removed from the total)Slashof anActivevalidator (stake weight decreases, or removed entirely on jail/exit)- Each block where a validator's
uptime_sharechanges (lazy, indexed by the same accumulator pattern asREWARDS_PER_STAKE_UNIT)
ValidatorEntry parsing is unified through ValidatorEntry::decode() —
the same parser is used by every consensus and tx-handler call site.
Length: 4 + 897 (FALCON pk) + 16 (stake u128) + 1 (status) + 16
(last_claimed_at u128) = 934 bytes.
(This unification fixed a genesis bug where an earlier per-call-site
parser returned None on every genesis validator — surfaced and fixed in
multi-node test #228.)
14.12 Long-Run Equilibrium
The model targets:
| Phase | Net change |
|---|---|
| Year 1–2 | Net mint > burn → modest inflation |
| Year 3–5 | Burn ≈ mint → near-zero net change |
| Year 6+ (terminal) | Burn > mint → mild deflation |
The 1% terminal inflation rate × GENESIS_SUPPLY is around 10M PYDE per
year. Even modest sustained throughput (a few thousand TPS at typical
fee levels) burns more than that. Net deflation is the long-run
expected state.
Summary
| Property | Value |
|---|---|
| Native token | PYDE |
| Decimals | 9 (1 PYDE = 10^9 quanta) |
| Genesis supply | 1,000,000,000 PYDE |
| Supply cap | None (decreasing inflation, fee burn) |
| Inflation schedule | 5% → 3% → 2% → 1% (terminal) |
| Commits per year | ~63,113,904 (2/sec median) |
| Fee distribution | 70% burn / 20% reward pool / 10% treasury |
| Validator stake (min) | 10,000 PYDE (single tier, uniform-random committee selection) |
| Operator-identity cap | 3 validators per operator |
| Unbonding period | 30 days (must exceed 21-day safety evidence freshness) |
| Slashing finder fee | 10% of slashed amount |
| Vesting | On-chain, balance-locked at validation |
| Airdrop | Merkle-proof claim, Sweep after deadline |
| Treasury spend | MultisigTx (type 9) + PIP data_digest audit trail |
| Multisig signers | Up to 16; threshold rotatable via RotateMultisig |
| Multisig threshold (governance) | 7-of-12 typical (set at launch) |
| Emergency pause | EmergencyPause (type 11), max 30 days |
The next chapter covers governance — how PIPs (Pyde Improvement Proposals)
become on-chain MultisigTx actions, and what scope governance has versus
what's hard-coded.
Chapter 15: Governance
Pyde's governance is deliberately minimal at the protocol level. There is no two-chamber voting machine, no plutocratic stake-weighted ballot, no on-chain referendum logic. The model is off-chain Pyde Improvement Proposals (PIPs) + an on-chain treasury multisig, with everything else either hard-coded or operationally driven.
This chapter describes the actual governance design: how proposals form, how rough consensus is reached, what authority the on-chain multisig has, and what falls outside governance entirely.
15.1 Why "Off-Chain PIPs + On-Chain Multisig"
A small number of governance models are well-explored in production blockchains, each with distinct failure modes:
| Model | Common failure mode |
|---|---|
| Stake-weighted token voting | Plutocracy (whales decide, low turnout, capture) |
| Liquid democracy (delegation) | Concentrated delegates, unstable delegation |
| Two-chamber (validators + holders) | Procedural deadlock, complex thresholds |
| Off-chain BIP-style + voluntary upgrade | Real but slow |
| Council multisig | Centralized; depends on signer integrity |
Pyde's choice is closer to the Bitcoin BIP / Ethereum EIP model than to Cosmos-style on-chain governance:
- Proposals are documents, not on-chain ballots. They live in a public
pipsrepo (zarah-s/pips), open to any author, indexed and discussed in the open. - Adoption is via voluntary validator upgrade. When validators running a new client version reach a sufficient share of the active committee, the new behavior takes effect. Validators that don't upgrade continue running the old rules and either follow along (no consensus change) or fork off (consensus-breaking change).
- The on-chain treasury multisig executes spends linked to PIPs. The
MultisigTxpayload carriesdata_digest = hash(pip_file_contents), so every treasury action is on-chain-linked to a published PIP.
The model's core property: no party can drain the treasury, halt the chain, or change the rules without a coordinated, public, auditable process. Drainage requires multisig signers; chain halt requires the emergency multisig; rule changes require validators choosing to run the new code.
15.2 The PIP Process
PIPs are governed by PIP-0001, the founding document that ratifies the PIP system itself. The process at a high level:
PIP lifecycle:
1. Draft Author writes a markdown document (problem, design,
rationale, security considerations).
Creates a PR against zarah-s/pips.
2. Discussion Open discussion on the PR, in forums, etc.
Author iterates.
3. Review PIP receives review from core devs, validators, the
security team. Concerns are addressed in discussion.
4. Acceptance PIP is merged into the pips repo with a final number.
An acceptance signal does not change protocol behavior
by itself — it is documentation of rough consensus.
5. Implementation The PIP is implemented in a code change (PR against
the relevant Pyde repo). The PIP # is referenced.
6. Deployment The new node version ships. Validators choose to run
the new version. Once a sufficient validator share
upgrades, the change takes effect on-chain.
There is no on-chain "yes/no" vote on the PIP itself. The closest thing to a vote is validators choosing to run the new code — a softer but genuine signal.
What gets a PIP
| Change type | PIP needed? |
|---|---|
| Consensus rule change (block format, finality) | Yes |
| Gas cost changes | Yes |
| Fee distribution changes (e.g., 70/20/10 split) | Yes |
| Cryptographic primitive change | Yes |
| New transaction type | Yes |
| New WASM host function | Yes |
| Treasury spend (any size) | Yes (data_digest carries hash) |
| Bootstrap node list update | No (config-driven) |
| Bug-fix release (no protocol change) | No (changelog) |
| Doc updates | No |
What a PIP looks like
A PIP includes (at minimum):
- Problem statement — what is being addressed and why.
- Specification — the exact design / wire format / behavior change.
- Rationale — why this design over alternatives.
- Security considerations — what could go wrong.
- Backwards compatibility — does this require a coordinated upgrade?
- Reference implementation link — code PR(s) that implement it.
PIP-0001 specifies the template in detail.
15.3 Voluntary Validator Upgrade
How a consensus rule change actually takes effect:
1. PIP is accepted; reference implementation merged into Pyde repo.
2. A new node release is cut, including the new behavior.
3. Validators choose whether to upgrade to the new release.
4. If enough validators upgrade simultaneously, the new behavior takes
effect at the activation block (specified in the PIP).
5. Validators on the old release either continue producing the old rules
(forking off if the change is incompatible) or stay in sync (if the
change is opt-in or backward-compatible).
The key word is voluntary. There is no on-chain mechanism to force a validator to upgrade. Validators that reject a change keep running the old rules; if they constitute >1/3 of the active committee, the new behavior cannot reach finality and the change is effectively rejected by the network.
This means the upgrade decision is itself a kind of vote — not measured by token weight, but by validator participation. A controversial change that fails to attract supermajority validator participation simply doesn't land, regardless of how many off-chain signers nominally approved.
Activation parameters
Most consensus changes ship with an activation height — a specific
wave_id at which old nodes will produce waves the new nodes reject
(or vice versa). Validators run the upgrade window with both code paths
available, switching to the new path at the activation wave.
Backward-compatible changes (e.g., new opcodes that no existing contract uses) can ship without coordinated activation — they take effect when an upgraded validator processes the wave, are simply not used by old contracts, and become standard once enough nodes have upgraded.
15.4 The On-Chain Treasury Multisig
The one piece of "governance" that lives on-chain at mainnet is the treasury multisig. This is the mechanism by which approved PIPs that require funding turn into actual PYDE movement.
Configuration
State (recap from Chapter 14):
| Discriminator | Name | Holds |
|---|---|---|
0x1C | MULTISIG_SIGNERS | Length-prefixed array of FALCON pks |
0x1D | MULTISIG_THRESHOLD | Required signature count |
0x1E | MULTISIG_NONCE | Replay protection counter |
Maximum signers: 16. The threshold is t-of-n — requires t valid FALCON
signatures from distinct signers in MULTISIG_SIGNERS.
Suggested initial configuration (set at mainnet genesis): 12 signers, threshold 7, drawn from the Foundation board, core dev leads, validator operator representatives, and independent ecosystem representatives. The emergency-halt multisig is separate (typically a tighter 5-of-7 of core devs + security team for fast crisis response). The exact composition is a launch decision and will be ratified by PIP-0001 + a follow-up PIP.
Spend transaction (MultisigTx = type 9)
#![allow(unused)] fn main() { struct MultisigSpend { target: Address, // recipient value: u128, // PYDE quanta to send data_digest: [u8; 32], // hash(pip_file_contents) } }
The data_digest is the audit trail. Anyone reading the chain sees a
treasury spend (target, value, data_digest); anyone who has the PIP can
hash it and confirm the spend matches. If the digest does not match a
published PIP, that's a public, on-chain anomaly.
Validation enforces:
value > 0target != Address::ZEROtarget != treasury_address(cannot spend to self)target != tx.from(writeback-clobber protection)tx.to == Address::ZEROMULTISIG_NONCEmatches the signed payload (replay protection)- Number of valid signatures from
MULTISIG_SIGNERS≥MULTISIG_THRESHOLD - Each signer index referenced exactly once (no duplicates)
Gas: 50,000 base + 50,000 per signature.
Rotation (RotateMultisig = type 10)
#![allow(unused)] fn main() { struct MultisigRotate { new_signer_pks: Vec<Vec<u8>>, // each is 897-byte FALCON pk new_threshold: u8, } }
The current signer set authorizes the rotation. Validation requires:
new_threshold >= 1new_threshold <= new_signer_pks.len()new_signer_pks.len() <= MAX_MULTISIG_SIGNERS(16)- Same writeback-clobber defenses as
MultisigTx
Gas: 60,000 base + 50,000 per signature + 10,000 per new signer.
Why this isn't "centralized governance"
Critics of multisig-based governance often raise the centralization concern: "a few signers can do anything." The mitigating factors:
- Bounded scope. The multisig can spend the treasury and rotate itself. It cannot change the inflation schedule, the consensus rules, the gas distribution, or any other protocol parameter — those are hard-coded in the validator binary.
- Public, on-chain audit trail. Every spend has a
data_digestlinkable to a PIP. Off-chain spending the treasury is not possible. - Validator override. If the multisig were captured and started spending against published PIPs, validators could refuse to include the spend transactions (or hard-fork them out). Validators retain veto power even over the multisig.
- Rotatable. The signer set can be replaced, also via PIP + multisig action.
A captured multisig is a problem, but a bounded one — it cannot rewrite consensus or change supply.
15.5 Emergency Governance
Pyde has a separate EmergencyPause / EmergencyResume mechanism (also
multisig-authorized) for crisis response. Covered in Chapter 14 §14.9; the
governance-relevant points:
- The emergency multisig signer set is separate from the treasury multisig (the same configuration mechanism, different state slot in a proper deployment).
- Pausing requires the emergency signers; resuming requires the same.
- Pause is auto-expiring at
MAX_PAUSE_DURATION_WAVES(~30 days). A paused chain cannot stay paused indefinitely without a fresh authorization.
The recommended emergency signer set: core developers + security team, with a much lower threshold than the treasury multisig (so a quick response is possible during a live exploit). The exact configuration is a mainnet-launch decision.
15.6 What Is NOT Governable
Hard-coded protocol constants that cannot be changed by any on-chain action — only by a PIP + new validator binary release + voluntary validator upgrade:
| Constant | Where |
|---|---|
| DAG round period (~150 ms) | crates/consensus/src/round.rs |
| Commit cadence (~500 ms median) | crates/consensus/src/wave.rs |
| Committee size (128) | crates/consensus/src/committee.rs |
| Quorum / threshold (85) | crates/consensus/src/quorum.rs |
| Equivocation threshold (44) | crates/consensus/src/quorum.rs |
| Validator min stake (10,000 PYDE) | crates/tx/src/pipeline.rs (will move to shared crate post-consensus-rebuild) |
| Operator-identity cap (3 / operator) | crates/tx/src/pipeline.rs |
| Unbonding period (30 days) | crates/consensus/src/validator.rs |
| Inflation schedule | crates/tx/src/fee.rs |
| Fee split (70/20/10) | crates/tx/src/execution.rs |
| Gas target / ceiling | crates/tx/src/fee.rs |
MAX_TX_SIZE (128 KB) | crates/tx/src/validation.rs |
MAX_CALLDATA (64 KB) | crates/tx/src/validation.rs |
MAX_BATCH_SIZE (4 MB) | crates/mempool/src/batch.rs |
| Cryptographic primitives | pyde-crypto polyrepo (FALCON, Kyber, Blake3, Poseidon2) |
| WASM host function ABI | crates/wasm-exec/src/host_fns.rs + Host Function ABI spec doc |
Changing any of these requires a code release. Validators choose whether to run it.
15.7 What Falls Through the Gaps
Some operational concerns sit outside both the PIP process and the multisig:
| Concern | Handled by |
|---|---|
| Bootstrap node list | Config — operators ship their own |
| Block explorer | Foundation operates a public one |
| RPC endpoints | Multiple operators run them |
| Indexing / data products | Ecosystem builds them |
| Wallet integrations | Ecosystem partnerships |
| Marketing / branding | Foundation |
| Conference sponsorships | Treasury via PIP-driven multisig |
| Bug bounty payments | Treasury via PIP-driven multisig |
These are not "governance" in any rigorous sense. They are operational choices that the Foundation, validators, and ecosystem participants make independently.
15.8 Comparison with Other Networks
| Property | Pyde | Ethereum | Cosmos / Tendermint | Polkadot |
|---|---|---|---|---|
| Protocol-rule change | PIP + voluntary upgrade | EIP + voluntary upgrade | On-chain governance vote | Council + referenda |
| Treasury spend | On-chain multisig + PIP | Foundation grants | On-chain governance | On-chain treasury / Council |
| Emergency halt | Multisig pause | None at protocol layer | None at protocol layer | Sudo (pre-removal) |
| Token voting | None | None at protocol layer | Stake-weighted | Stake-weighted |
| Validator-only signal | Voluntary upgrade | Voluntary upgrade | On-chain | Council inclusion |
| Off-chain coordination doc | PIP | EIP | Forum + on-chain proposal | OpenGov / Forum |
| Constitutional parameters | All of them, hard-coded | Hard-coded | Some on-chain | Some on-chain |
The Pyde model is closest to Ethereum's: heavy reliance on off-chain proposals and voluntary validator upgrades, with a small on-chain mechanism (in our case, the treasury multisig) for the parts that genuinely need on-chain authorization.
15.9 Why No Stake-Weighted Voting?
Stake-weighted voting is the most common form of on-chain governance, and the design Pyde explicitly rejected. Three reasons:
- Plutocracy. A stake-weighted vote concentrates power in whoever holds the most tokens. PYDE distribution at any point in time is a snapshot — there's no reason to think it tracks anything beyond who bought early.
- Low turnout. Most token holders don't vote. The few who do gain outsized influence.
- Vote-buying. Active markets exist for vote delegation in stake-weighted systems. Treasury-spend votes can be auctioned off.
The PIP-and-voluntary-upgrade model removes the "vote weight" question entirely. There is no quantum of governance influence that can be purchased. There is only:
- Anyone can write a PIP.
- Validators can choose to run the resulting code (or not).
- Multisig signers can authorize PIP-linked treasury spends (or not).
Each piece is a clear, narrow authority. None of them aggregate into "control of the protocol."
15.10 Future Direction
Possible post-mainnet additions to governance, none on the critical path:
- Validator signal mechanism. A way for validators to publicly signal support or opposition for a PIP before activation, increasing process transparency. Pure off-chain or a thin on-chain log.
- Quadratic / conviction voting for treasury allocation. A sub-process for ecosystem grant allocation that gives some weighted input to ecosystem participants without becoming token-weighted control.
- Optional on-chain PIP registry. A storage-discriminator (
PIP_REGISTRY?) that mirrors the off-chain PIP repo so on-chain readers can resolve adata_digestwithout needing the off-chain repo.
None of these change the fundamental shape: the multisig is bounded, the PIP process is open, and validators decide what code they run.
Summary
| Component | Status at mainnet |
|---|---|
| PIP process | Off-chain, in zarah-s/pips |
| PIP authority | Documents intent; not protocol law |
| Validator upgrade | Voluntary; per-release |
| Treasury multisig | On-chain, MultisigTx (type 9) |
| Multisig rotation | On-chain, RotateMultisig (type 10) |
| Multisig signer cap | 16 |
MultisigTx PIP linkage | data_digest = hash(pip_file) on-chain |
| Emergency pause | On-chain, EmergencyPause (type 11) |
| Pause max window | ~30 days (auto-expiring) |
| On-chain stake-weighted voting | None |
| Hard-coded protocol constants | All of them — change via code release |
The next chapter covers security — the threat model, slashing detail, and the weak-subjectivity defenses that protect against long-range attacks.
Chapter 11: Account Model
The account model is the data structure at the center of every blockchain. It decides how users are identified, how balances are tracked, how authorization happens, and how concurrent transactions interact.
Pyde's account model is built on three ideas:
- Post-quantum from genesis. Addresses are derived from FALCON-512 public keys. There is no ECDSA legacy to migrate away from.
- Nonce window, not sequential. Each account gets a 16-slot nonce bitmap window — multiple in-flight txs without head-of-line blocking.
- Native account abstraction. Multisig, batch transactions, and paymaster sponsorship are protocol features, not application-layer add-ons.
This chapter covers the account record, address derivation, nonce mechanics, multisig configuration, batch transactions, and the transaction wire format.
11.1 Account Structure
Every account in crates/account/src/types.rs:
#![allow(unused)] fn main() { struct Account { address: Address, // 32 bytes (Poseidon2 hash of FALCON pk) nonce: u64, // 8 B -- low end of the 16-slot window balance: u128, // 16 B -- spendable balance, in quanta code_hash: H256, // 32 B -- 0x00..00 for EOAs storage_root: H256, // 32 B -- 0x00..00 for empty contracts account_type: AccountType,// 1 B -- EOA=0, Contract=1, System=2 auth_keys: AuthKeys, // variable -- see §11.7 gas_tank: u128, // 16 B -- sponsored-tx pool key_nonce: u32, // 4 B -- key-rotation counter } }
Fixed-portion size: 141 bytes plus the variable auth_keys field. The
encoding is little-endian, dense; the JMT stores the serialized blob as
the leaf value.
| Field | Mutability |
|---|---|
address | immutable after account creation |
nonce | per-tx (window slides forward) |
balance | per-tx |
code_hash | set once at deploy; never changes |
storage_root | every block that mutates the contract |
account_type | immutable |
auth_keys | rotatable (increments key_nonce) |
gas_tank | deposit by anyone; withdraw by owner |
key_nonce | increments on key rotation |
The "spendable" balance is what's available after deducting any vesting
locks (Chapter 14). The vesting subsystem reads the on-chain
VestingSchedule for the account and subtracts the locked portion before
checking balance during validation.
11.2 Address Derivation
All Pyde addresses are 32-byte Poseidon2 hashes. The derivation depends on how the account is created.
EOA
EOA address = Poseidon2(falcon_public_key_bytes)
The input is the raw 897-byte FALCON-512 public key. The output is 32 bytes of Poseidon2 over the Goldilocks field — the natural output size, no truncation.
CREATE (deploy from a deployer's nonce)
CREATE address = Poseidon2(deployer_address || nonce_bytes)
The deployer's address and the deployer's current nonce — the same scheme as Ethereum, but Poseidon2 instead of Keccak.
CREATE2 (deterministic deploy with a salt)
CREATE2 address = Poseidon2(0xFF || deployer_address || salt || code_hash)
The leading 0xFF is a domain separator that distinguishes CREATE2 outputs
from CREATE outputs (so two different derivation inputs can never collide).
Why 32 bytes (not 20)
A 20-byte address provides 80-bit collision resistance, which is marginal at chain scale. Pyde uses the full 32 bytes — 128-bit collision resistance — which matches the natural Poseidon2 output. There is no storage cost worth saving by truncating.
11.3 Account Types
#![allow(unused)] fn main() { enum AccountType { EOA = 0x00, Contract = 0x01, System = 0x02, } }
EOA
The standard user account. Has a single FALCON pubkey (or a multisig set)
in auth_keys. No code, no storage. Balance and nonce live directly in the
account record.
Contract
Has deployed WASM bytecode (code_hash != 0) and optionally a storage trie
(storage_root != 0). Cannot directly initiate transactions — only
respond to calls. May have a non-empty gas_tank to sponsor user calls
into it.
System
Pre-existing accounts at deterministic addresses for protocol-level
operations (treasury, airdrop pool, validator entries). Their addresses are
typically Poseidon2("pyde-treasury") or similar — not derived from any
public key. They are seeded at genesis and only mutated by specific
transaction handlers (e.g. the treasury balance moves only via MultisigTx
spend or fee-split crediting).
11.4 Nonce Bitmap Window
Sequential nonces (Ethereum's model) cause head-of-line blocking: if a tx at nonce 5 is stuck (e.g., dependent on a state change that hasn't happened), all higher nonces from the same sender are blocked behind it.
Pyde uses a 16-slot bitmap window:
#![allow(unused)] fn main() { pub const WINDOW_SIZE: u64 = 16; struct NonceState { base: u64, // lowest unused nonce used: u16, // bitmap: bit i = nonce (base + i) used } }
A transaction can use any nonce in [base, base + 15]. The bitmap tracks
which slots are filled. When the lowest bit becomes set, the window slides
forward past every consecutive used slot.
#![allow(unused)] fn main() { fn use_nonce(state: &mut NonceState, n: u64) -> Result<(), Error> { if n < state.base || n >= state.base + 16 { return Err(NonceOutOfWindow); } let offset = (n - state.base) as u16; let bit = 1 << offset; if state.used & bit != 0 { return Err(NonceAlreadyUsed); } state.used |= bit; while state.used & 1 == 1 { // slide window past contiguous used state.base += 1; state.used >>= 1; } Ok(()) } }
Worked example
Initial: base=100, used=0b0000000000000000 window [100..115]
Submit tx with nonce=103:
base=100, used=0b0000000000001000 100,101,102 still available
Submit tx with nonce=100:
(slide) base=101, used=0b0000000000000100 window [101..116]
Submit tx with nonce=101:
(slide past 101 and 102 -- 103 is set)
base=102, used=0b0000000000000010 window [102..117]
Submit tx with nonce=102:
base=104, used=0b0000000000000000 window [104..119]
Properties
| Property | Outcome |
|---|---|
| Concurrent submissions | Up to 16 in-flight from one sender |
| Stuck-tx tolerance | A stuck nonce N doesn't block N+1, N+2, ... |
| Replay protection | Each (account, nonce) usable exactly once |
| Cancellation | Submit a different tx with the same nonce |
| Compact state | 10 bytes of nonce state per account |
Limit
If a power user genuinely needs more than 16 in-flight, they use multiple accounts. In practice, even high-frequency market makers rarely exceed 16 pending — at ~500ms median commit and v1 target TPS, the queue drains in a handful of waves.
11.5 Authorization: AuthKeys
Each account stores a auth_keys field that determines who is allowed to
sign for it:
#![allow(unused)] fn main() { enum AuthKeys { None, // tag 0x00 Single(Vec<u8>), // tag 0x01 — FALCON pk MultiSig { keys: Vec<Vec<u8>>, threshold: u32 }, // tag 0x02 — max 16 signers Programmable, // tag 0x03 — RESERVED v2 } }
| Variant | Status | Used for |
|---|---|---|
None | v1 | System accounts, contracts that have no admin |
Single | v1 | Standard EOA — one FALCON-512 public key (~897 bytes) |
MultiSig | v1 | Native multi-signature — set of keys + threshold (max 16) |
Programmable | v2 reserved | Contract-defined auth logic (session keys, social recovery, biometric, etc.) — discriminant is reserved at v1 so contracts written today survive the v2 upgrade without rewriting |
Why native multisig at v1. Gnosis Safe's contract-based multisig on
Ethereum has been re-implemented dozens of times across projects with
subtle bugs in each. Pyde standardizes the simple t-of-n case as a
protocol primitive, so wallets and contracts can rely on a single audited
implementation. Weighted multisig and exotic schemes still live at the
contract layer.
The Programmable reservation. Reserving 0x03 at v1 means contracts
that today reference AuthKeys::Programmable (as a future-proofing hint)
won't break at the v2 upgrade — the discriminant is allocated. Session
keys, social recovery, and biometric auth are post-mainnet features. See
Session keys (v2) below for the design and what v1 reserves to make
that work.
A Single EOA uses one FALCON pubkey for all transactions. A MultiSig
account requires threshold-of-N signatures to authorize.
Key rotation
Both Single and MultiSig are mutable. A key rotation transaction signed
by the current auth_keys updates the field and increments
key_nonce by 1. The increment invalidates any in-flight transaction
signed under the old key — they will fail signature verification on
inclusion.
The address itself never changes. Storing addresses in contracts (for balances, allowances, ACLs) remains valid across any number of key rotations.
Why no native key-recovery
Pyde does not ship a built-in social-recovery scheme. The intended pattern
for high-value accounts is MultiSig with guardian keys:
keys: [
Owner (weight 3 if you implement weighted multisig in a contract),
Guardian_1 (weight 1),
Guardian_2 (weight 1),
Guardian_3 (weight 1),
]
threshold: 3
Normal: Owner signs alone (weight 3 == threshold).
Recovery: Three guardians together (1+1+1 = 3) authorize a key rotation.
The base MultiSig variant in AuthKeys provides equal-weight t-of-n.
Weighted variants live at the contract layer (a deployed multisig contract
that owns the EOA via key rotation).
Session keys (v2)
A session key is a temporary, scope-limited key the user authorizes a dApp (or an agent) to act with on their behalf — for a bounded time, against a bounded set of contracts, with a bounded spend cap. The user signs once. The dApp signs many times, within the declared scope, without ever holding the user's main key.
This is the UX layer most consumer crypto applications have been missing. Pyde ships native session-key support at v2 (paired with programmable accounts). Ethereum is retrofitting the same idea via ERC-4337; Pyde gets it at the protocol layer.
Use cases:
- Gaming. Sign once at session start; play 200 in-game actions without per-action wallet popups.
- AI agents. Delegate "trade at most 100 PYDE/day on this DEX until next Friday" without handing over the master key.
- Consumer apps. Recurring subscriptions, micro-transactions, real-time DeFi positions.
- Embedded wallets. Passkey-style flows where the user's main key never leaves a secure enclave.
How it works (v2):
#![allow(unused)] fn main() { struct SessionKey { pubkey: FalconPubkey, // the delegated key scope: SessionScope, // what it can do expires_at: WaveId, // when it stops working revoked: bool, // owner-flippable kill switch } struct SessionScope { contracts: Vec<Address>, // allow-list of callable contracts methods: Vec<Selector>, // optional method allow-list (empty = all) max_spend: u128, // hard cap on cumulative PYDE outflow spent_so_far: u128, // running counter, updated at commit } }
At authorization time, for any tx submitted under a session key, the protocol checks:
- Signature. FALCON-verify against
SessionKey.pubkey. - Liveness.
expires_at > current_waveandrevoked == false. - Scope. Target contract is in
scope.contracts; ifscope.methodsis non-empty, the called selector is in it. - Spend cap.
spent_so_far + tx.value ≤ max_spend.
All four must pass. On commit, spent_so_far is incremented atomically.
The account's main auth_keys is untouched — session keys are an
additional authorization path, not a replacement.
Revocation. A RevokeSessionKey tx signed by the account's main
auth_keys flips revoked = true. The session is invalid from the next
wave onward.
Why v2, not v1. Session keys are a specific policy expressed in the
AuthKeys::Programmable variant. They need the policy engine that
programmable accounts ship with. Both move together at v2.
What v1 reserves to make this work:
| v1 surface | Why it matters for v2 session keys |
|---|---|
AuthKeys::Programmable enum variant (tag 0x03) | The authorization model session keys plug into |
Account code_hash + storage_root fields | Programmable accounts use the same shape as contracts |
| WASM "policy mode" execution flag (reserved) | Session-key checks run in a restricted-state-access mode |
| Multisig signature pipeline | Same verification path serves session-key + multisig flows |
These reservations cost nothing at v1 (the enum variant is unused, the policy-mode flag is reserved-but-not-implemented). v2 ships session keys without breaking any account-touching contract written for v1.
11.6 Transaction Wire Format
A transaction in crates/tx/src/types.rs:
#![allow(unused)] fn main() { struct Transaction { from: Address, // 32 B to: Address, // 32 B (Address::ZERO for deploy) value: u128, // 16 B (in quanta) data: Vec<u8>, // calldata or initcode gas_limit: u64, // 8 B nonce: u64, // 8 B (in [base, base+15]) signature: FalconSig, // ~666 B fee_payer: FeePayer, // tag + optional address (1-33 B) access_list: Vec<AccessEntry>, deadline: Option<u64>, // 0 or 8 B chain_id: u64, // 8 B tx_type: TransactionType,// 1 B (see §11.8) } }
fee_payer
#![allow(unused)] fn main() { enum FeePayer { Sender, // pays from their own balance (default) GasTank, // gas paid from the target contract's gas_tank Paymaster(Address), // gas paid by named paymaster (calls validator) } }
See Chapter 10 for sponsorship semantics.
access_list
#![allow(unused)] fn main() { struct AccessEntry { address: Address, storage_keys: Vec<U256>, access_type: AccessType, // Read | ReadWrite } }
The access list drives parallel execution (Chapter 9). Wallets generate it
automatically by simulating the transaction (pyde_createAccessList) and
attach it to the signed transaction. If the actual on-chain execution
touches a slot not in the access list, the transaction reverts cleanly with
AccessListViolation.
deadline
A wave_id after which the tx becomes invalid. If included before
deadline it executes normally; if not, it is dropped from mempools and
the nonce slot frees up. Recommended values:
| Use case | Deadline (waves after submission) | Wall time |
|---|---|---|
| DEX swap | +20 | ~10 sec |
| Token transfer | +120 | ~60 sec |
| Mint | +600 | ~5 min |
| Governance vote | +28,800 | ~4 hr |
| No urgency | None | indefinite |
Transaction hash
Computed via Poseidon2 over the canonical encoding of all fields. The signature is over this hash:
tx_hash = Poseidon2(
chain_id || from || to || value || Poseidon2(data) || gas_limit || nonce ||
fee_payer_tag || Poseidon2(access_list) || deadline || tx_type
)
data and access_list are pre-hashed to keep the outer Poseidon2 input
size bounded.
Typical sizes
A simple transfer (no calldata, no access list, no deadline) is roughly
780 bytes — dominated by the FALCON-512 signature. A complex tx with a
populated access list and several KB of calldata can reach the 128 KB
MAX_TX_SIZE.
11.7 Multisig Treasury Spend
Beyond per-account multisig (where auth_keys = MultiSig{...}), Pyde has a
treasury-level multisig for protocol-funded actions. This is what
moves PYDE out of the treasury account when a PIP is approved.
The mechanism uses two new transaction types:
| Type ID | Name | Purpose |
|---|---|---|
| 9 | MultisigTx | Treasury spend: debit treasury, credit target |
| 10 | RotateMultisig | Rotate the signer set + threshold |
The current signer set and threshold live in state under the discriminators
MULTISIG_SIGNERS / MULTISIG_THRESHOLD; replay is prevented by
MULTISIG_NONCE. See Chapter 15 for the governance flow that produces
these signatures.
The handler enforces:
value > 0target != Address::ZEROtarget != treasury_addresstarget != tx.from(prevents pipeline-writeback clobber)tx.to == Address::ZERO(must not collide with a regular tx target)
The signature count + threshold check happens against the on-chain signer
set. A successful spend bumps MULTISIG_NONCE so the same signed payload
cannot be replayed.
11.8 Transaction Types
The TransactionType enum (in crates/tx/src/types.rs) currently has 13
variants. Tag 2 is intentionally vacant — Batch was prototyped pre-mainnet but removed before launch (the dispatch arm was a 21k-gas no-op and never wired to real semantics; keeping the gap means a forged tx_type = 2 fails decode rather than silently aliasing to another type).
| ID | Name | What it does |
|---|---|---|
| 0 | Standard | Value transfer or contract call |
| 1 | Deploy | Contract deployment (to == Address::ZERO, data == initcode) |
| 3 | StakeDeposit | Lock ≥ MIN_VALIDATOR_STAKE (10,000 PYDE) and register as validator (data = FALCON pubkey 897 B). Single-tier — any validator meeting the floor is eligible for the per-epoch uniform-random committee selection (see Chapter 14 §14.5). |
| 4 | StakeWithdraw | Begin 30-day unbonding |
| 5 | Slash | Submit double-sign evidence (data = serialized evidence) |
| 6 | ClaimReward | Claim accrued staking yield from the pool |
| 7 | ClaimAirdrop | Claim genesis airdrop with Merkle proof |
| 8 | SweepAirdrop | Move unclaimed airdrop residue to treasury (post-deadline) |
| 9 | MultisigTx | Treasury spend with multisig signatures |
| 10 | RotateMultisig | Rotate multisig signer set + threshold |
| 11 | EmergencyPause | Halt block production (multisig-signed) |
| 12 | EmergencyResume | Resume normal processing (multisig-signed, clears pause) |
| 13 | RegisterPubkey | First-time pubkey registration for a funded-but-unregistered account. No signature, no gas, no value — proof of pubkey ownership is the address-derivation check (only the keypair holder can produce a pubkey that hashes to a given address). Allowed only when balance > 0 and auth_keys == AuthKeys::None. After execution, auth_keys = AuthKeys::Single(tx.data) and the account can sign normal txs. |
Each handler in crates/tx/src/pipeline.rs validates the type-specific
payload, applies the state effect, and runs through the same fee
distribution + post-execution writeback. Unknown discriminators are
rejected at validation.
11.9 Batch Transactions (removed pre-mainnet)
Multi-operation batch transactions were prototyped under tag 2 but
removed before launch. The dispatch arm was a 21k-gas no-op never wired
to real semantics, and ABI-level multi-call patterns (a contract that
takes a Vec<(Address, u128, bytes)> and dispatches internally) cover
the same use cases without protocol-level complexity. Tag 2 remains
reserved (decodes to None) so a forged transaction with tx_type = 2
fails decode rather than silently aliasing to another variant.
If multi-op atomicity becomes a documented need post-mainnet, a future PIP can re-introduce the variant at the next unused tag with a real implementation.
11.10 Contract Code and Storage
Deployment
#![allow(unused)] fn main() { Transaction { from: deployer, to: Address::ZERO, // signals deployment value: ..., data: init_bytecode, // executed once at deploy gas_limit: ..., nonce: ..., tx_type: TransactionType::Deploy, ... } }
wasmtime instantiates init_bytecode against a fresh context. The init code's
return value is stored as the contract's runtime bytecode. The deployed
contract address is Poseidon2(deployer || nonce) (see §11.2). The
code_hash is set to Poseidon2(runtime_bytecode).
After deployment, the code_hash is immutable. Upgradeability is
handled at the application layer with the proxy pattern:
+-----------+ DELEGATECALL +------------------+
| Proxy | ---------------------------> | Implementation |
| (fixed) | | (v1, v2, v3) |
| storage: | proxy uses its own storage | no storage of |
| current_ | but executes the impl's code | its own |
| impl | +------------------+
+-----------+
The proxy's address never changes; upgrading is a single state write to
current_impl in the proxy's storage.
Storage schema
The otigen toolchain's build-time storage layout (Chapter 5) and the JMT key derivation
(Chapter 4) together produce a fully typed storage model. There is no
"random raw 256-bit slot" — every storage access is keyed against the
contract address with a discriminator that came from a typed declaration.
11.11 Account State in the JMT
Accounts and their storage all live in the same JMT. A single Merkle path from the JMT root proves any claim about any account.
To prove "Alice's balance at wave W equals X":
- Show the JMT path from the wave-W state root (in the commit header) to Alice's account leaf.
- Decode the account record; read the
balancefield.
There is no separate "account trie" + "storage trie" indirection. One root, one path, one proof.
Light clients use this property to verify state without storing the full chain — they need block headers and on-demand JMT proofs from full nodes.
11.12 Worked Lifecycle: Sponsored Token Transfer
Step 1 — Wallet builds tx
from: 0xpyde1abc... (Alice)
to: 0xpyde1def... (DEX contract)
value: 0
data: swap(USDC, PYDE, 1000)
gas_limit: 150,000
nonce: 42 (within Alice's nonce window)
fee_payer: GasTank <- DEX contract pays
deadline: block 2,000,025 (10 sec from now)
chain_id: 1
tx_type: Standard
signature: FALCON-512(Alice's sk, hash of all fields)
Step 2 — RPC ingress
- chain_id matches
- FALCON sig verifies against Alice's auth_keys
- nonce 42 is in [40, 55]
- DEX.gas_tank >= 150_000 * base_fee
- deadline > current_wave_id
- access_list dedup OK
- tx size + calldata size within limits
-> ENQUEUE on gossip channel BEFORE returning Ok
Step 3 — Mempool propagation
Encrypted payload reaches every node's mempool via gossipsub.
Step 4 — DAG vertex production (round R)
Tx referenced by batch hash in worker batch; each committee member's
primary references the batch in its round-R vertex.
Step 5 — Commit (round R+3, ~500 ms after submission)
Deterministic anchor commits the subdag; canonical order emitted.
Step 6 — Threshold decryption (rounds R+4 to R+5)
85+ Kyber shares -> shared_secret -> AES decrypt payload.
Step 7 — Execution (hybrid scheduler)
- Gas charged from DEX.gas_tank (FeePayer::GasTank); accounted via wasmtime fuel.
- Conflict graph built from access list; parallel groups execute.
- DEX swap logic runs: SLOAD reserves, SLOAD/SSTORE Alice's USDC,
transfer PYDE to Alice.
- Total gas used: 87,400.
Step 8 — Fee distribution
total_fee = 87,400 * base_fee
burn: 70%
reward pool: 20% (distributed at epoch end across stakers)
treasury: 10%
Debited from DEX.gas_tank.
Step 9 — State writeback
Alice's USDC balance updated, PYDE balance updated, DEX gas_tank
debited. Alice's nonce 42 marked used; window slides if 40, 41 also used.
Step 10 — Finality (state root attestation, ~500 ms median end-to-end)
85+ FALCON state-root sigs piggybacked on subsequent vertices.
Step 11 — Receipt
pyde_getTransactionReceipt returns success, gas_used, logs, fee_paid.
Summary
| Property | Value |
|---|---|
| Address size | 32 bytes (Poseidon2 hash, no truncation) |
| Address derivation | EOA from FALCON pk; CREATE / CREATE2 from deployer |
| Account types | EOA, Contract, System |
| Auth schemes | None, Single FALCON pk, MultiSig{keys, threshold} |
| Address mutability | Immutable across key rotations |
| Nonce window | 16 slots (bitmap), sliding base |
| Native account abstraction | Yes (fee_payer = GasTank / Paymaster(addr)) |
| Multisig per-account | Yes (via AuthKeys::MultiSig) |
| Multisig treasury | Yes (MultisigTx = type 9) |
| Batch transactions | Removed pre-mainnet (tag 2 reserved-as-vacant) |
| Transaction types | 13 active (Standard, Deploy, Stake*, Slash, Claim*, Sweep*, Multisig*, Emergency*, RegisterPubkey) |
| Validation gas cap | 100,000 for paymaster validation |
The next chapter covers the networking layer that ferries all these transactions between nodes — libp2p, QUIC, the four gossipsub channels, and the FALCON peer-attestation handshake.
Chapter 12: Networking
Pyde's P2P network sits on libp2p over QUIC, with purpose-specific gossipsub channels and an application-layer FALCON-512 handshake that binds each peer's libp2p PeerId to a post-quantum identity. Peer discovery uses a layered approach (no Kademlia DHT) — hardcoded seeds, then DNS, then the on-chain validator registry, then PEX. This was a deliberate post-pivot choice: a DHT for a 128-member committee is more attack surface than it is value.
Worker / Primary split (Narwhal pattern). Within each validator, transactions and consensus traffic are decoupled. Workers gossip transaction batches peer-to-peer (high-volume data dissemination); primaries gossip vertices (low-volume consensus structure). A vertex carries batch hashes by reference, never full payloads.
The encryption story is layered — libp2p's standard Ed25519/X25519 handles peer routing, FALCON does the heavy lifting at the application layer where quantum-safety matters. Committee defense uses the sentry node pattern (Cosmos-style): committee validators are reachable only through sentry proxies, never expose their committee identity to public peers.
12.1 Transport: libp2p over QUIC
Why libp2p
libp2p is the modular networking stack used by Ethereum 2.0, Filecoin, and Polkadot. It gives Pyde:
- Pluggable transport (Pyde uses QUIC).
- Multistream protocol negotiation per stream.
- Built-in Kademlia DHT and gossipsub implementations.
- Peer identity via PeerId.
Why QUIC
| Property | TCP + Yamux/mplex | QUIC |
|---|---|---|
| Connection setup | 1-3 RTT (TCP + TLS) | 0-1 RTT (integrated TLS) |
| Head-of-line blocking | yes (all streams share) | no (per-stream flow control) |
| Multiplexing | userspace (Yamux) | native (kernel-assisted) |
| Connection migration | not supported | supported (connection IDs) |
| Mandatory encryption | optional (TLS) | always (TLS 1.3 in handshake) |
Per-stream independence matters most when block propagation (large) and consensus votes (latency-critical) share the same QUIC connection. A single lost packet on the block stream does not stall the vote stream.
The libp2p config is set up in crates/net/src/node.rs via
SwarmBuilder::with_quic().
Identity at the libp2p layer
libp2p PeerIds in Pyde are derived from Ed25519 / X25519 keys — the libp2p default. The choice is intentional: libp2p's PeerId routing, Kademlia DHT lookups, and QUIC handshake all assume one of the supported key types. Replacing the libp2p layer's identity with FALCON would require a custom libp2p fork.
The post-quantum identity layer sits one step higher: every consensus and validator-channel message is signed with FALCON-512, and the application-level peer handshake (§12.4) binds the libp2p PeerId to a FALCON public key. Pyde's threat model treats the libp2p layer as fungible peer routing; the cryptographic claims that matter — vote authenticity, finality cert verification, evidence verification — all sit on FALCON.
12.2 The Four Channels
Different traffic has different latency and throughput profiles. Mixing them on one gossip topic forces the worst-case scheduling on every message type. Pyde splits traffic into four channels, each tuned for its workload.
+---------------------------------------------------------------+
| Pyde Node |
| |
| +-------------+ +-------------+ +-------------+ +----------+ |
| | Consensus | | Transactions| | Blocks | | Sync | |
| | gossip | | gossip | | gossip | | req/resp | |
| +------+------+ +------+------+ +------+------+ +----+-----+ |
| | | | | |
| +------+---------------+---------------+-------------+------+ |
| | libp2p / gossipsub | |
| +------+----------------------------------------------------+ |
| | |
| +------+----------------------------------------------------+ |
| | QUIC transport | |
| +-----------------------------------------------------------+ |
+---------------------------------------------------------------+
| Topic | Participants | Size limit | What it carries |
|---|---|---|---|
pyde/vertices/1 | Committee primaries | 256 KB | DAG vertices (batch refs + parent refs + state-root sigs + decryption shares + FALCON sig) |
pyde/transactions/1 | All nodes | 128 KB | User transactions (plaintext or encrypted) |
pyde/batches/1 | Workers + primaries | 4 MB | Worker batches (hard cap; preserves modest-hardware claim) |
pyde/sync/1 | All nodes (req/resp) | 16 MB | Snapshot chunks (4 MB typical), historical vertices |
pyde/evidence/1 | Validators | 64 KB | Slashing evidence (double-sign, equivocation, etc.) |
Validator-only vertex channel
Non-validator peers are dropped from the pyde/vertices/1 and
pyde/evidence/1 topics. The check
(ChannelAccess::validator_only() in crates/net/src/channels.rs) refuses
to forward messages from peers whose FALCON-attested pubkey is not in the
current committee set. A non-validator that subscribes to the topic gets
ValidationResult::Reject on every publish.
This matters: the vertex channel carries committee FALCON sigs, piggybacked decryption shares, and state-root attestations. A malicious non-validator that could flood the channel could DoS the commit pipeline. The validator-only filter prevents this by construction.
Per-channel size limits
The validator (crates/net/src/channels.rs) checks the message size
against the per-channel cap before forwarding. Oversized messages are
rejected and the originating peer takes a reputation penalty.
12.3 Gossipsub Configuration
crates/net/src/node.rs configures gossipsub:
| Parameter | Value | Why |
|---|---|---|
validation_mode | Permissive | Auto-forward; see throughput note |
heartbeat_interval | 150 ms | Matches DAG round cadence; amortizes mesh maintenance without blocking round progress |
mesh_n | 8 | Mesh size per node |
mesh_n_low | 4 | Trigger mesh expansion |
mesh_n_high | 12 | Trigger mesh pruning |
gossip_lazy | 8 | Number of IHAVE peers |
history_length | 6 | Recent message-id buffer (heartbeats) |
history_gossip | 3 | Size of the IHAVE batch |
duplicate_cache_time | 60 s | Dedup window — handles small-net jitter |
flood_publish | true | Initial publish reaches all mesh peers |
max_transmit_size | 1 MB | Per-message cap (channels override) |
The Permissive + flood_publish change
Strict gossipsub validation requires the application layer to call
report_message_validation_result for every message before it gets
forwarded. Earlier Pyde code didn't do this on every path — the result was
that, on a small (4-validator) testnet, transactions only reached the
direct peer of the submitting node. They never propagated through the
mesh.
The fix (commit 2018b17) was twofold:
- Switch to
ValidationMode::Permissive, which auto-forwards a message once the basic structural check passes. - Set
flood_publish = trueso the initial publish from a node reaches all of its mesh peers immediately, not just a random subset.
The combination raised sustained TPS from ~1K to ~4K on the same testnet
hardware. There is also a paired change in the block executor that skips
redundant per-tx FALCON verification when the block-level batched verify
already passed (block_sigs_pre_verified flag in BlockContext) —
roughly 70% reduction in block-execution CPU.
12.4 FALCON P2P Handshake
After a libp2p connection is established, the two peers run a FALCON attestation exchange to bind the libp2p PeerId to a post-quantum identity.
#![allow(unused)] fn main() { // crates/net/src/auth.rs struct PydeAuthReq { nonce: [u8; 32] } struct PydeAuthResp { falcon_pubkey: Vec<u8>, // ~897 bytes signature: Vec<u8>, // FALCON over (nonce || responder_peer_id_bytes) } }
Flow
A (initiator) B (responder)
| |
| --- PydeAuthReq(nonce) ------->|
| |
| | sign msg = nonce || responder_peer_id_bytes
| | with B's FALCON sk
| |
| <-- PydeAuthResp(pk, sig) -----|
| |
| verify(pk, msg, sig) |
| record (peer_id -> pk) |
| |
verify_auth_resp(req, resp, peer_id) parses the pubkey, reconstructs the
attestation message, and runs falcon_verify. On success, the binding
(peer_id -> falcon_pubkey) is recorded in the local PeerManager.
Outcome
#![allow(unused)] fn main() { enum AuthOutcome { NoPendingNonce, // attempt to respond with no outstanding req VerifyFailed, // FALCON sig invalid RebindRejected, // peer tried to bind a different pubkey StoredAsValidator, // pubkey is in current committee_keys StoredAsNonValidator, // pubkey is not in committee } }
A RebindRejected is suspicious — once a PeerId is bound to a FALCON
pubkey, attempts to re-bind it are denied (a PeerId switching pubkeys mid
session is either a bug or an attack).
Validator-channel filtering uses this binding
Every gossipsub message on pyde/consensus/1 is checked against the
attested pubkey of the publishing peer. Non-validators (no committee
membership) get their messages dropped before any heavyweight verification
runs. This is the cheap front-line filter that keeps consensus traffic
clean.
12.5 Peer Discovery (Layered, No DHT)
Pyde does not use a Kademlia DHT. The pre-pivot design did, until we audited the security profile: a DHT for a 128-member committee gives an attacker a controllable lookup surface (Sybil flooding of routing tables, eclipse via DHT poisoning) without offering value the committee couldn't get from simpler mechanisms.
Discovery proceeds in five layers, each falling back to the next:
1. Hardcoded bootstrap seeds (chain spec ships ~10 well-known IPs)
2. DNS seed lookup (TXT records at seed.pyde.network)
3. On-chain validator registry (each validator's PeerId+addr on-chain)
4. Peer exchange (PEX) (peers gossip their connected-peer list)
5. Local cache (recently-seen-good peers persisted)
Bootstrap
The chain spec ships hardcoded bootstrap seeds + the DNS seed name. At startup the node dials seeds in parallel, performs FALCON handshakes, and queries each seed's connected-peer list (PEX) to expand the candidate set.
# in pyde.toml
[network]
bootstrap_seeds = [
"/dns4/seed1.pyde.network/udp/30303/quic-v1/p2p/12D3Koo...",
"/dns4/seed2.pyde.network/udp/30303/quic-v1/p2p/12D3Koo...",
]
dns_seed = "seed.pyde.network"
On-chain validator registry
Each committee validator's (falcon_pubkey, peer_id, multiaddr) is on
chain in the validator-registry account, updated when a validator joins
the committee. A new node fetching the genesis block (or any later state
snapshot) has the complete committee directory — no DHT lookup required.
Peer exchange (PEX)
Once connected, peers periodically gossip a short list of other peers
they're currently connected to. PEX uses a small dedicated request/response
protocol (/pyde/pex/1) — not the gossipsub channels — to avoid mixing
discovery traffic with consensus.
Why this is enough
- 128 committee members is small enough that the on-chain registry is the entire ground truth. No DHT-style scalability is needed.
- Sentry node pattern (next section) hides committee identities from public peers anyway — the committee discovery layer is private.
- Layered fallback means no single point of failure: seeds, DNS, on-chain, PEX, cache.
What's stored in the layered cache
| Layer | Persistence | Trust model |
|---|---|---|
| Hardcoded seeds | binary | Chain-spec trusted |
| DNS records | DNS TTL | DNS operator trusted |
| On-chain registry | JMT | Consensus-finalized |
| PEX cache | LRU 1024 | Peer-attested only |
| Local good-peer cache | disk LRU 100 | Empirically known good |
12.6 Connection Limits and Rate Limiting
crates/net/src/config.rs defaults:
| Constant | Default | Meaning |
|---|---|---|
DEFAULT_PORT | 30303 | Default UDP listen port |
DEFAULT_MAX_PEERS | 50 | Total connected peers |
DEFAULT_MAX_INBOUND | 30 | Max inbound connections |
DEFAULT_MAX_OUTBOUND | 20 | Max outbound connections |
DEFAULT_RATE_LIMIT_PER_IP | 5 / sec | Inbound connect rate per IP |
DEFAULT_IDLE_TIMEOUT | 60 s | Drop idle connections after |
The peer manager (crates/net/src/peer.rs) tracks these per-IP
counters; can_accept() enforces them.
Token-bucket rate limits
The DDoS subsystem (crates/net/src/ddos.rs) implements per-peer
token-bucket rate limiting:
#![allow(unused)] fn main() { RateLimiter { max_tokens: f64, refill_rate: f64, // tokens / sec current: f64, last_refill: Instant, } }
Evidence ingest, in particular, is rate-limited (per the post-Phase-1
audit hardening: task 014d). Without the limit, a non-validator peer
could spam garbage-sig evidence at ~60 µs of FALCON verify each — enough
to consume validator CPU at scale. With the limit, repeat offenders are
dropped after the first failure.
Per-subnet limits
SubnetLimiter (also in crates/net/src/ddos.rs) tracks /24 subnets and
caps connections per subnet, preventing a single network operator from
monopolizing peer slots.
12.7 Peer Reputation
Each PeerInfo (crates/net/src/peer.rs) tracks:
#![allow(unused)] fn main() { struct PeerInfo { peer_id: PeerId, falcon_pubkey: Option<Vec<u8>>, // post-handshake binding role: PeerRole, // Validator / FullNode / Light / Unknown messages_received: u64, invalid_messages: u64, last_seen: Instant, } }
A simple reputation score:
reputation = messages_received - (invalid_messages * 10)
Peers with strongly negative reputation are dropped and rate-limited. The
scoring is deliberately simple — Pyde does not currently ship a
sophisticated gossip score (no peer_score_thresholds), trusting the
combination of validator-channel filtering, FALCON binding, and
token-bucket rate limits to handle the major attack vectors.
A more sophisticated scoring mechanism (decay weights, per-topic scores, gray-listing) is on the post-mainnet hardening list.
12.8 NAT Traversal
Pyde leans on libp2p's standard NAT-traversal tools:
- AutoNAT detects whether the local node is reachable.
- DCUtR (Direct Connection Upgrade through Relay) coordinates QUIC hole-punching between nodes behind cone NATs.
- Relay nodes forward traffic for nodes behind symmetric NATs that can't be hole-punched.
- UPnP / PCP automatic port mapping on supportive home routers.
A node with nat_status = SymmetricNat will rely on relays; a Public
node accepts inbound directly. This is standard libp2p mechanics; Pyde
does not modify the underlying behavior.
12.9 Bandwidth Profile
At a steady-state v1 target of 10-30K plaintext TPS (~80 KB average batches, ~500 ms median commit cadence):
| Channel | Inbound | Outbound |
|---|---|---|
| Transactions | ~3 MB/s | ~3 MB/s |
| Batches | ~1 MB/s | ~1 MB/s |
| Consensus (validator) | ~0.3 MB/s | ~0.3 MB/s |
| Sync (serving) | ~2 MB/s | ~2 MB/s |
| DHT / discovery | ~0.1 MB/s | ~0.1 MB/s |
| Validator total | ~6 MB/s | ~6 MB/s |
| Full node total | ~4 MB/s | ~4 MB/s |
Recommended links:
| Role | Bandwidth | Connections |
|---|---|---|
| Validator | 100+ Mbps symmetric | 50–100 |
| Full node | 100 Mbps symmetric | 30–60 |
| Light client | 1 Mbps | 3–5 |
These are well within commodity hosting tiers — no datacenter requirement.
Bandwidth reductions
- Transaction batching within gossipsub (configurable batch + 50 ms flush window).
- Compact blocks for large block bodies — short tx IDs (6 bytes of Poseidon2 hash) instead of full tx hashes (32 bytes).
- LZ4 / Snappy compression on gossip payloads (~60% reduction on transaction batches).
- Mesh dedup cache —
duplicate_cache_time = 60 sprevents the same message from being forwarded multiple times.
12.10 Network Initialization Sequence
On `pyde run`:
1. Load config (TOML); apply CLI overrides.
2. Initialize logging.
3. Create or load validator identity (FALCON keypair if validator).
4. Open RocksDB state store; apply genesis if empty.
5. Attach the consensus_store (restore seen_proposals / votes / evidence).
6. Generate libp2p keypair (Ed25519); derive PeerId.
7. Bind QUIC listener on configured port (default 30303).
8. Connect to bootstrap peers.
9. Run Kademlia FIND_NODE(self) to populate routing table.
10. Subscribe to gossipsub topics.
11. If validator role:
a. Announce committee membership on DHT (validator:{epoch} key).
b. Run FALCON handshake with discovered validators.
c. Start the consensus loop.
12. Start RPC server (HTTP + WebSocket).
13. Start metrics endpoint (Prometheus, default port 9090).
12.11 Metrics
Every node exposes a Prometheus endpoint with at minimum:
| Metric | Type | Meaning |
|---|---|---|
pyde_peers_connected | gauge | Total connected peers |
pyde_peers_by_role | gauge | Validators / full / unknown |
pyde_gossip_messages_received | counter | Messages received per topic |
pyde_gossip_messages_sent | counter | Messages sent per topic |
pyde_bandwidth_inbound_bytes | counter | Total inbound bytes |
pyde_bandwidth_outbound_bytes | counter | Total outbound bytes |
pyde_block_propagation_time_ms | histo | Time from propose to receipt |
pyde_consensus_msg_latency_ms | histo | Round-trip on consensus channel |
pyde_dht_routing_table_size | gauge | Kademlia routing table entries |
pyde_falcon_handshakes_completed | counter | Successful peer handshakes |
pyde_falcon_handshakes_failed | counter | Verification failures |
These feed into the docker/grafana dashboards that ship with the repo.
12.12 Sentry Node Pattern (Committee Defense)
Committee validators have stake at risk and produce vertices on a tight ~500ms cadence — losing connectivity for a few rounds risks liveness penalties. To insulate them from direct attack, Pyde supports the sentry node pattern (Cosmos-style):
Internet
|
v
+----------+ +----------+ +----------+
| Sentry 1 | | Sentry 2 | | Sentry 3 | (public-facing, no stake)
+----+-----+ +----+-----+ +----+-----+
| | |
+-------------+--------------+
| (private VPN/cloud network)
v
+-------------+
| Committee | (hidden, never directly addressable)
| Validator |
+-------------+
- Committee validator only accepts QUIC connections from its known sentry IPs. Public peers never know its IP.
- Sentry nodes are full nodes that route traffic to the validator. They run no stake; if attacked, they're disposable.
- PEX-suppressed — the committee validator does not gossip its address via PEX, so its IP doesn't leak through the discovery layer.
The pattern is supported in pyde.toml:
[network]
sentry_mode = true # for committee validators
allowed_inbound_peers = [
"/ip4/10.0.1.5/udp/30303/quic-v1/p2p/12D3Koo...", # sentry 1
"/ip4/10.0.1.6/udp/30303/quic-v1/p2p/12D3Koo...", # sentry 2
]
suppress_pex_advertisement = true
Non-committee validators and full nodes typically don't bother with sentries.
12.13 What's Out of Scope for Mainnet
Honest about what is not in the network layer at launch:
- Witness delivery to provers. The chain doesn't have provers, so
there's no
pyde/witnesses/1channel. - Erasure coding for vertex propagation. The current implementation fans out vertices via gossipsub. Reed-Solomon erasure coding for very large vertices is on the post-mainnet improvements list.
- Algebraic FALCON batch verification. Implemented as sequential loop for v1; algebraic batching (sharing FFT work across signatures) is post-mainnet hardening.
Summary
| Component | Choice |
|---|---|
| Transport | libp2p over QUIC (TCP fallback) |
| libp2p identity | Ed25519 (PeerId routing only) |
| Application identity | FALCON-512 (vertex sigs, attestations, evidence) |
| Channels | 5 — vertices / transactions / batches / sync / evidence |
| Validator channel filter | FALCON pubkey ∈ current committee |
| Gossipsub mode | Permissive + flood_publish = true |
| Heartbeat | 150 ms (matches DAG round cadence) |
| Mesh size | 8 (low 4, high 12) |
| Peer handshake | FALCON-512 attestation; binds peer_id → falcon_pk |
| Discovery | Layered: seeds → DNS → on-chain registry → PEX → cache (no DHT) |
| Committee defense | Sentry node pattern (Cosmos-style) |
| Connection limits | 50 total / 30 inbound / 20 outbound (defaults) |
| Rate limit (per IP) | 5 / sec (defaults) |
| Symmetric encryption | TLS 1.3 inside QUIC |
| Bandwidth (committee) | 500 Mbps @ 30K TPS, scales w/ TPS (Ch 19) |
The next chapter covers the cross-chain and parachain story — what's in scope for mainnet, what isn't, and what the SDK direction looks like.
Chapter 13: Parachains and Cross-Chain
This chapter covers two distinct (and sometimes conflated) topics:
- Pyde's parachain framework — the v1 mechanism for app-specific execution contexts that run as WebAssembly modules with their own state subtrees, their own governance, and their own validator sets opting in from Pyde's main committee.
- Cross-chain bridges to other L1s — the post-mainnet path to interoperability with Ethereum, Bitcoin, and other chains.
These are different things. A Pyde parachain is an on-chain WASM module with extra privileges (its own state space, cross-parachain messaging, threshold-crypto access). A cross-chain bridge is infrastructure that ferries proofs between Pyde and a foreign chain.
For parachains: the framework ships at v1 — the on-chain registry, governance, lifecycle, and execution environment are all part of mainnet. Authors write parachain logic in any wasm32-target language (Rust, AssemblyScript, Go, C/C++) and deploy via the otigen toolchain. The full design is in memory/parachain-v1-design and the upcoming PPIPs (Pyde Parachain Improvement Proposals).
For cross-chain bridges: the surface ships at v1; the implementation ships post-mainnet. The cross_call host function, the HardFinalityCert primitive, and the unified gas model are all available at genesis so contracts can be written today against the interface. The actual cross-chain transports (FALCON-in-EVM verifier, light-client contracts, relay infrastructure) ship after mainnet stability is proven.
13.1 What Mainnet Ships
At mainnet, Pyde does not ship:
- A native bridge to any other chain (no Ethereum bridge, no Bitcoin bridge, no IBC channel).
- Native cross-chain message passing to foreign L1s at the protocol level
(the
cross_callinterface exists; the transports do not). - Slot auctions or Polkadot-style shared-security parachains. Pyde's parachain model is different — see §13.5.
What it does ship:
- A sovereign L1 with the full execution model (WASM contracts via wasmtime, encrypted mempool, FALCON-quorum finality, JMT state).
- The parachain framework (registry, governance, lifecycle, execution environment) — see §13.5 below.
- Hard-finality certificates suitable for use as cross-chain proof inputs by any future bridge contract.
- An architecture that leaves room for cross-chain bridges as post-mainnet extensions.
The reasoning: bridges are the largest historical source of catastrophic loss in the cryptocurrency ecosystem. Shipping a bridge at mainnet without months of audit and incentivized testnet exposure would be irresponsible relative to the launch timeline. A bridge added later, against a stable chain with proven liveness, is a much smaller surface to audit.
13.2 Why Cross-Chain Is Hard
Cross-chain communication boils down to one question: how does Chain B verify that something happened on Chain A? Three families of answers exist:
| Approach | Trust assumption |
|---|---|
| Trusted relay | A multisig or enclave attests to events |
| Light-client proof | Chain B verifies Chain A's consensus signatures directly |
| Validity proof (ZK) | Chain B verifies a SNARK/STARK of Chain A's execution |
Trusted relays are the cheapest to build and the worst by every other metric — every major bridge exploit (Wormhole, Ronin, Nomad, Multichain) hit a trusted-relay design. Light-client proofs require Chain B to implement Chain A's signature verification on-chain, which is expensive but honest. Validity proofs are the strongest model and the most complex to implement.
For Pyde's eventual bridges, the design constraints are:
- No new trusted parties. No multisig "guardians" sit between Pyde and the counterparty chain.
- Light-client verification. The counterparty chain runs a Pyde light client (FALCON verification + finality cert) that proves "block N was hard-finalized on Pyde."
- Symmetric or asymmetric, but always verifiable. If the counterparty chain has its own light-client logic implementable on Pyde, the bridge is symmetric. If not (e.g., Bitcoin), Pyde verifies the counterparty one direction only.
None of this work is on the mainnet critical path.
13.3 Hard-Finality Certificates as Bridge Inputs
The one piece of cross-chain infrastructure mainnet does ship — implicitly — is the hard-finality certificate (Chapter 6):
#![allow(unused)] fn main() { struct HardFinalityCert { wave_id: u64, blake3_state_root: Hash, poseidon2_state_root: Hash, voter_bitmap: u128, // 128-bit bitmap signatures: Vec<FalconSignature>, // ≥ 85 } }
This certificate, signed by ≥ 2f+1 = 85 of the active committee, is exactly the input a future light-client bridge needs:
- A counterparty bridge contract holds the active committee's FALCON public keys (refreshed at epoch boundaries).
- To accept a Pyde-side event, the bridge requires:
- A
HardFinalityCertfor the commit that included the event. - A Merkle proof from the wave's
blake3_state_root(native) orposeidon2_state_root(ZK-circuit-friendly) to the event's storage slot.
- A
- Verification is
(85 × FALCON_verify) + (one Merkle path verification), feasible on any chain with a reasonable VM.
The committee size of 128 caps the per-cert verification cost at ≥ 85 FALCON verifies. At ~1 ms per verify on commodity hardware, that's ~85 ms of counterparty execution per accepted Pyde event — non-trivial but not catastrophic.
13.4 The cross_call Host Function
Cross-context invocation in Pyde is exposed as a WASM host function:
#![allow(unused)] fn main() { // From the WASM contract author's perspective (Rust example): let result = pyde::cross_call( target_address, // contract or parachain address "request_price", // function name &args, // serialized arguments CallbackSpec { success_method: "on_price_received", error_method: "on_price_failed", max_callback_gas: 100_000, timeout_waves: 100, }, )?; }
The same primitive serves three call shapes:
- Smart contract → smart contract (same chain, fully working at v1). Synchronous if both contracts are in the same wave; asynchronous via callback if execution spans waves.
- Smart contract → parachain (working at v1 once parachain framework is live). Asynchronous; the parachain's committee processes the call and submits a callback transaction with the result.
- Smart contract → foreign L1 (interface available at v1; transport ships post-mainnet). Until the cross-chain transport lands, this returns
NotYetSupportedat runtime — but contract code written againstcross_callto a foreign target compiles and deploys today, ready for when the transport ships.
The host function signature is part of the v1 Host Function ABI specification and is stable at genesis. Contracts written today against the v1 interface continue to work as additional transports come online.
Callback context preserved
Every cross_call carries enough context that the callback can reconstruct what happened:
callback_id(unique per call)original_caller(address that initiated the original transaction)original_fn(function that issued the cross_call)original_args_hash(hash of original args; full args retrievable from the chain log)issued_at_wave(when the call was issued)target(who was called)
On result (success, error, or timeout), the callback handler receives both the result payload and the context. Full audit trail is always preserved.
13.5 The Parachain Framework (v1)
Pyde's parachain framework is not a Polkadot-style slot-auction model and is not a separate operator network running off-chain. It is an on-chain execution mechanism for app-specific WebAssembly modules with extra capabilities relative to ordinary smart contracts.
The distinction matters because the "parachain" word is overloaded in the L1 ecosystem. In Pyde:
- Smart contracts are WASM modules with the standard host-function ABI. They share Pyde's state space, follow Pyde's transaction lifecycle, are scheduled by Pyde's main executor.
- Parachains are WASM modules with an extended host-function allowlist (cross-parachain messaging, threshold-crypto access, governance hooks) and their own state subtree partitioned by
parachain_id[..16]under PIP-2 clustering. They have their own validator committees (subsets of the main Pyde committee that opt in), their own consensus instance (chosen from a preset menu at deploy time), and their own upgrade governance (equal-power voting among their validators).
What ships at v1
The full framework: registration, deployment, lifecycle, upgrade governance, state partitioning, cross-parachain messaging, version history retention, and the host-function ABI surface that parachain WASM is built against.
What v1 does not include (deferred to v2 or later):
- A maintained per-language SDK (per the no-SDK approach: authors compile their own WASM in any wasm32-target language using the published Host Function ABI; canonical example projects are provided as starting points, but there is no per-language SDK to maintain).
- ZK-aggregated signature verification for parachain committees (the path to massively higher throughput; v2/v3 work).
Parachain deployment
Authors deploy a parachain the same way they deploy a smart contract — via the otigen toolchain:
otigen init my_parachain --lang rust --type parachain
# ... author writes parachain logic in src/main.rs ...
# ... declares state schema, consensus preset, slashing preset in otigen.toml ...
otigen build
otigen deploy --network testnet --name "chainlink"
otigen.toml for a parachain extends the smart-contract schema with parachain-specific fields:
[contract]
type = "parachain"
[parachain]
consensus_preset = "simple_bft" # or "threshold" or "optimistic"
min_validators = 7
quorum_threshold = "2/3"
[slashing]
preset = "standard" # minimal / standard / strict
[hosts]
allowed = [
"storage_read", "storage_write", "emit_event",
"send_xparachain_message", "threshold_decrypt",
# ... full parachain-extension allowlist
]
Parachain governance
Parachain upgrades go through equal-power voting among the parachain's validators (one validator, one vote — NOT stake-weighted). Configurable quorum, configurable threshold, with a default 2/3 supermajority. Owner-only emergency pause and kill are available for operational lifecycle. Governance can claw back squatted names via PPIP if the dispute warrants.
Full upgrade history is retained on-chain forever. Every transaction receipt records (parachain_id, parachain_version, wasm_hash) so historical replay can fetch the exact WASM binary that originally executed each tx.
Cross-parachain messaging
Parachains can call each other via the send_xparachain_message host function. Rate-limited, threshold-signed (the calling parachain's committee signs the outgoing message; the receiving parachain's committee verifies it), and routed through Pyde's main consensus as regular transactions. The full mechanism is documented in the upcoming PPIPs.
Why this model rather than slot auctions
Slot auctions (Polkadot-style) concentrate parachain rights in deep-pocketed operators, creating political and centralization risk. Pyde's parachain model is closer to "deploy a contract that happens to have its own state space and validator committee" — anyone can deploy, costs are predictable (ENS-style name registration + owner deposit), and economic alignment is via stake and slashing rather than auction proceeds.
13.6 Native Bridges (Post-Mainnet)
Beyond a parachain SDK, the longer-term direction includes purpose-built bridges to specific chains.
| Direction | Mechanism | Difficulty |
|---|---|---|
| Pyde → Ethereum | Ethereum contract verifies Pyde finality certs | Moderate (FALCON in EVM) |
| Ethereum → Pyde | Pyde contract verifies Ethereum execution proofs | Moderate (Merkle Patricia) |
| Pyde → Bitcoin | SPV-style proofs of Bitcoin finality | Hard (PoW finality is probabilistic) |
| Pyde → other PoS L1s | Each side verifies the other's signature scheme | Variable |
The Ethereum bridge is the most concrete near-term target post-mainnet. The work splits into:
- An Ethereum-side contract that verifies FALCON signatures and
HardFinalityCertstructures. FALCON-512 verification in EVM is non-trivial (algebraic operations over a 12,289-mod ring) but not fundamentally blocked. - A Pyde-side contract that verifies Ethereum execution proofs (Merkle Patricia paths). This part is straightforward — WASM contracts on Pyde can implement Patricia path verification just as Solidity contracts can.
- A relay process that ferries finality certs and execution proofs between the two chains. The relay is permissionless — anyone can run it, and anyone can verify the outputs.
No mainnet timeline commitment exists. The bridge is contingent on:
- Pyde mainnet stability (Phase 9 + Phase 10 of the launch plan).
- Independent audit of the FALCON-in-EVM verifier (probably the most novel piece of crypto code in the bridge stack).
- A specific use case that justifies the bridge (e.g., bringing Ethereum-issued stablecoins to Pyde at scale).
13.7 What WASM Contracts Can Do Today (No Bridge)
A few cross-chain-adjacent things are still possible at the application layer without any protocol-level bridge:
Off-chain oracle pattern
A contract on Pyde that needs an external value (e.g., an asset price) can:
- Define an
oracle: Addressstorage field. - Allow only that address to write to a
pricesmap. - The "oracle" is an off-chain process running by some trusted operator (or a multisig) that submits update transactions.
This is not a bridge. It is a trusted off-chain feed. But it works, and it unlocks DeFi applications without waiting for a bridge.
Mirror tokens
A token contract on Pyde can represent off-chain assets (USDC, ETH-pegged) by trusting a multisig minter. This is the same trust model as wrapped tokens on every other chain — appropriate when the operator is sufficiently trusted (e.g., a regulated custodian) but not appropriate as a default bridge.
Light-client deployments
If a developer wants to verify Ethereum events on Pyde today, they can deploy an Ethereum-light-client WASM contract that consumes Ethereum block headers (relayed by an off-chain process) and verifies execution proofs against them. The verification work is done by the contract; the relay is just data ferrying.
This is the right pattern, even if the relay is operationally trusted — the verification is on-chain and trustless.
13.8 Parachain Economics
A common question: what does PYDE pay for in a parachain world?
PYDE is the gas token across the platform. Every parachain operation that touches state, emits events, sends cross-parachain messages, or consumes execution gas is metered in PYDE via wasmtime fuel — exactly the same as smart-contract operations. Authors pay registration fees + owner deposits in PYDE at deploy time. Validators of a parachain earn PYDE rewards via the standard inflation distribution, weighted by their committee membership and uptime.
Parachain authors can layer their own internal token economies on top (e.g., a DEX parachain might mint LP tokens; a DAO parachain might mint governance tokens) — but those are application-layer concerns, not protocol-level mechanics. The protocol charges PYDE; what the parachain charges its users is its own decision.
This keeps the gas accounting simple: one token, one fuel mechanism, uniform across smart contracts and parachains.
13.9 What the Roadmap Looks Like
| Stage | Cross-chain capability |
|---|---|
| Mainnet (v1) | Parachain framework live (WASM-based); cross_call host function available; HardFinalityCert format stable |
| Post-mainnet — Stage 1 | First production parachains deployed (DEX, oracle, etc.) |
| Post-mainnet — Stage 2 | First Ethereum bridge (FALCON-verifier on EVM + Pyde-side Patricia verifier) |
| Post-mainnet — Stage 3 | Multi-chain bridges (additional foreign L1s) |
| Post-mainnet — Stage 4 | ZK-aggregated FALCON signatures (reduces bridge verification cost dramatically) |
| Post-mainnet — Stage 5 | zk-WASM proven execution (where research is heading) |
These are directional. Each stage is gated on the maturity of the previous stage and on credible auditor capacity, not on a calendar.
Summary
| Capability | At mainnet? | Post-mainnet plan? |
|---|---|---|
| Sovereign L1 | Yes | — |
| Hard-finality certificate (cert format) | Yes | Used by future bridges |
| Parachain framework (WASM-based) | Yes | Production parachains roll in over time |
| Cross-parachain messaging | Yes (with framework) | Optimizations + ZK aggregation |
cross_call host function (interface) | Yes | Foreign-chain transports wired post-mainnet |
| Smart-contract → smart-contract calls | Yes (working) | Performance optimizations |
| Smart-contract → parachain calls | Yes (with framework) | — |
| Smart-contract → foreign L1 calls | Interface only, returns NotYetSupported | Wired when bridges ship |
| Native bridge to Ethereum | No | Yes (FALCON-in-EVM) |
| Native bridge to Bitcoin | No | Maybe (SPV proofs) |
| Off-chain oracle / multisig mints | Possible at app layer | Same as today |
| Light-client contracts (Ethereum) | Possible at app layer | Easier with bridge |
Pyde at launch is a sovereign network with a working parachain framework, designed not to depend on cross-chain bridges. Sovereign assets, sovereign users, sovereign apps, sovereign parachains. Foreign-chain bridge work begins once that base is provably stable.
The next chapter covers the PYDE token: supply, inflation, distribution, fee mechanics, and staking economics.
Chapter 17: Developer Tools
Pyde's developer toolchain is the set of command-line programs, SDKs, and RPC endpoints that let people write, test, deploy, and interact with contracts. This chapter is the reference survey — what exists, what it does, where to find it.
For deep documentation on the primary developer-facing tool (the otigen binary), see Chapter 5: Otigen Toolchain. This chapter does not duplicate that material; it summarizes and points outward.
What's in scope
otigen— the developer toolchain binary. Handles project scaffolding, builds in any supported language, state binding generation, deployments, wallet management, REPL access. The single tool most contract developers use day-to-day.pyde— the node binary (validator + full node) with JSON-RPC, libp2p networking, and the WASM execution layer.pyde-rust-sdk— the Rust client SDK for talking to a Pyde node programmatically.pyde-ts-sdk— the TypeScript / JavaScript client SDK.pyde-crypto-wasm— WASM bindings exposing post-quantum cryptography (FALCON signing, Kyber encryption, Poseidon2/Blake3 hashing) to browser and Node.js environments.
What's not in scope at launch (tracked later)
- A dedicated Pyde block explorer frontend (the backend indexer is on the roadmap; the UI is community ecosystem work).
- A proprietary IDE. Standard editors with the language's standard tooling (rust-analyzer for Rust, the AssemblyScript LSP, gopls for Go, clangd for C/C++) are the intended path. No Pyde-specific IDE.
- Per-language testing wrappers. Contract authors use their language's native test runner (
cargo test,npm test,go test,clang+ your test framework of choice).
17.1 otigen — the developer toolchain
The Foundry / Hardhat / Cargo-equivalent for Pyde. Replaces the earlier wright toolchain that targeted the now-retired Otigen smart-contract language.
otigen is language-agnostic: the same binary handles projects authored in Rust, AssemblyScript, Go (via TinyGo), or C/C++. Authors declare their language in otigen.toml; otigen invokes the correct compiler with the correct WASM target and packages the resulting artifact for deployment.
Subcommand summary
otigen init <name> --lang <language> Scaffold a new project from the language template
otigen build Build the WASM module + ABI + bundle artifact
otigen deploy Sign and submit a deploy transaction
otigen upgrade Submit an upgrade proposal
otigen pause / kill Operational lifecycle (where supported)
otigen inspect <address-or-name> Read deployed contract state, ABI, version history
otigen wallet Wallet management subcommands
otigen console REPL against a local or remote node
There is no otigen test. Authors use their language's native test runner.
Performance — what to expect from otigen build
The whole otigen build validation + packaging pipeline runs in single-digit microseconds of CPU work for a typical contract (parse otigen.toml, validate every cross-cutting rule, walk the compiled .wasm for imports + exports + deterministic-feature compliance, build the canonical ContractAbi, Borsh-encode, inject the pyde.abi custom section). Wall-clock invocations are dominated by file I/O — reading the .wasm + writing the four bundle files — which lands in the 1–5 ms range on commodity hardware. Validator work is essentially free against that.
The full in-memory pipeline measures ~14.5 µs on an Apple M-series reference machine. Per-step numbers (Blake3 selector derivation, Borsh encode, custom-section injection, WASM-feature validation) are in Chapter 5 §5.11 with a reproduction recipe via cargo bench. Baselines are committed under crates/<crate>/benches/baseline/ in the pyde-net/otigen repo; future regressions surface on every PR that runs cargo bench --baseline=v1.
For the full reference — otigen.toml schema, per-language workflows, state binding generation, deploy/upgrade flow internals, performance numbers — see Chapter 5.
17.2 pyde — the node binary
The node binary that any full node or validator runs. Contains:
- The Mysticeti DAG consensus layer
- The WASM execution layer (wasmtime + Cranelift AOT)
- The JMT state layer (with PIP-2 clustering, dual-hash, PIP-3 prefetch, PIP-4 write-back cache)
- The libp2p + QUIC + Gossipsub network layer
- A JSON-RPC server for client interaction
- Validator-mode flags for committee participation, stake management, key rotation
pyde init Initialize a new node (creates keystore, config)
pyde start Run the node (validator if configured for committee participation, full node otherwise)
pyde config show Print effective config
pyde keys rotate Rotate FALCON keypair (validator-only)
pyde admin <subcommand> Operational commands (drain, halt, recover)
A full operational reference for validators is published as a separate document (see Validator Operating Guide, post-public-testnet).
17.3 SDKs
Two first-class language SDKs at launch, with the WASM crypto bindings as a third-party-friendly bridge.
pyde-rust-sdk
Idiomatic Rust client for Pyde nodes. Use cases:
- Backend services interacting with Pyde from Rust applications.
- Scripted deployment + interaction (alternative to
otigen's deploy/send commands when scripting in Rust). - Tools building on top of Pyde (indexers, monitoring, custom validators).
Surface area:
- Transaction construction + signing
- RPC client (JSON-RPC over HTTP and WebSocket)
- Streaming subscriptions (new blocks, account changes, event filters)
- ABI encoding/decoding helpers
- Wallet integration (load keys from
~/.pyde/wallets/, hardware wallets via external signer protocol)
pyde-ts-sdk
TypeScript / JavaScript SDK. Ships at ethers-equivalent maturity from day one (lessons from EVM tooling baked in).
Surface area:
- Same primitives as
pyde-rust-sdkbut idiomatic TS - Browser-friendly via tree-shaking + WASM crypto bridge
- Type-safe ABI generation from
abi.jsonartifacts - React hooks for common patterns (account, balance, contract calls)
- Wallet adapter pattern for browser-wallet integration
pyde-crypto-wasm
WASM bindings exposing post-quantum cryptography to JavaScript. Used internally by pyde-ts-sdk, also usable directly by any project that needs PQ crypto in a browser or Node.js environment.
Surface area:
- FALCON-512 keypair generation, sign, verify
- Kyber-768 encryption / decryption
- Threshold-encryption shares (where used by client-side encrypted-tx submission)
- Poseidon2 and Blake3 hashing
17.4 JSON-RPC
The node exposes a JSON-RPC interface over HTTP and WebSocket. Method surface includes:
- Standard query methods —
pyde_getAccount,pyde_getBalance,pyde_getNonce,pyde_getContractCode,pyde_getContractState,pyde_resolveName - Transaction submission —
pyde_sendRawTransaction,pyde_sendRawEncryptedTransaction,pyde_estimateAccess - View-function calls —
pyde_call(contract, fn, calldata)— free, off-chain execution against current state; no tx, no gas, no consensus. Mirrors EVM'seth_call. Bounded by RPC-layer rate limits + per-call instruction cap. - Subscription methods (WebSocket) —
pyde_subscribe:newHeads— wave commits as they finalizeaccountChanges— state changes to a specific accountlogs— events matching an AND+OR filter (topic OR-list + optional contract); at-least-once delivery; each event carries(wave_id, tx_index, event_index)cursor for dedup;pyde_resubscribe({from: cursor})resumes after disconnect. Full mechanics: Host Function ABI Spec §15.5.
- Historical event queries —
pyde_getLogs({from_wave, to_wave, topics, contract, cursor, limit})— 5,000-wave cap per request, cursor pagination, ascending wave order. Per-wave bloom filter prefilters; three RocksDB indexes resolve exact matches. Full spec: Host Function ABI Spec §15.4. - Snapshot queries —
pyde_getSnapshotManifest(wave_id)(light-client state sync) - Gas / fee estimation —
pyde_estimateGas,pyde_getBaseFee - Wave + state-root queries (for light clients) —
pyde_getWave,pyde_getHardFinalityCert - Validator-specific methods — committee status, attestations, under an authorized-only namespace
The full method catalog is published as the JSON-RPC reference (lives alongside the node binary documentation).
17.4b Client-Side wasmtime + Wallet Preview Tiers
Pyde's TS and Rust SDKs embed wasmtime directly, so wallets can simulate transactions locally before signing. This unlocks honest pre-sign safety information without server-side round trips.
Tier 1 — Deterministic local preview (v1 mainnet)
The default. Wallets ship with:
- Gas estimation — run the tx against current state locally; count consumed fuel; show user the expected gas cost
- Access list inference — speculatively execute; record every sload/sstore call's slot_hash; attach the inferred access list to the tx so the chain's parallel scheduler can use it
- View function execution —
view-attributed functions execute locally, fetching state via RPC for any cache misses; no tx submitted, no gas - Dry-run preview — show the user "this tx will spend X PYDE, transfer Y tokens to address Z, emit Transfer event, leave your balance at W"
- Known-pattern decoding — recognize standard ABI patterns (transfer, approve, etc.) and surface them in plain language
The user clicks Sign only after seeing exactly what the tx does in this moment.
Wallet UX flow (Tier 1):
User constructs tx in wallet
↓
Wallet fetches contract WASM + relevant state via RPC
↓
Wallet runs wasmtime locally with the tx
↓
Wallet displays preview:
"Calling Token.transfer(to=0xabc..., amount=100 PYDE)
This tx will:
- Send 100 PYDE from you (0xYOU) to 0xabc...
- Your balance after: 900 PYDE
- Emit event: Transfer(from=0xYOU, to=0xabc..., amount=100)
- Cost: ~25,000 gas (~0.001 PYDE)
[Sign] [Cancel]"
↓
User signs (FALCON-512) → tx submitted
Tier 2 — Reputation + heuristics (v2 direction)
Layers on top of Tier 1. Doesn't require AI — just curated data + pattern matching:
- Flag contracts on known-malicious lists (Blockaid, Pyde-community-maintained registries)
- Flag "approve unfamiliar contract for max amount" patterns
- Cross-reference with audit databases (was this contract audited? by whom?)
- Surface community reputation scores
Tier 3 — LLM-augmented analysis (v3+ direction)
LLM reads contract WASM (or decompiled source) to summarize behavior, identify common risk patterns:
- approve+drain combos
- hidden auth modifiers
- timelocked backdoors
- liquidity-rug constructions
Rates confidence: "looks like a standard DEX trade" vs "matches wallet-drainer pattern X." Surfaces a graded warning to the user.
By the time Pyde mainnet matures, third-party services (Blockaid, Pocket Universe, etc.) will likely offer this as an API. Pyde wallets can integrate.
Honest v1 framing
The marketing claim Pyde v1 can make:
Pyde wallets show you the immediate effects of every transaction before you sign — including exact state changes, events emitted, and gas cost. You see what your authorization does in this moment. Deeper analysis (downstream authorization implications, contract backdoors, signed-message replays) requires reading the contract code or using third-party safety tools.
Honest, defensible, materially better than EVM wallet UX without overpromising.
What Tier 1 cannot detect
Worth being explicit about:
- Approval-then-drain patterns. The approval looks innocuous (just a state write). The drain happens in a future tx that the malicious contract submits using that approval.
- Time-locked backdoors. Contract logic that activates after N waves.
- Signed-message replay. Signing arbitrary EIP-712-style messages off-chain that can be replayed.
These are application-layer risks. Tier 2/3 (when shipped) address them. v1 documents them honestly so users know to use third-party tools for those classes of analysis.
17.5 What changed at the pivots
For readers coming from the pre-pivot world, the developer tooling has changed substantially:
| Pre-pivot (Otigen-language era) | Post-pivot (current) |
|---|---|
otic — Otigen compiler | Retired; archived |
wright — project CLI | Retired; archived. Role taken by the new otigen binary |
.oti source files | Replaced by author's language of choice (.rs, .ts, .go, .c) |
| PVM bytecode artifacts | Replaced by WASM .wasm artifacts |
| Otigen-specific tests | Replaced by author's language's native test runner |
pyde.toml config | Replaced by otigen.toml config with state schema declaration |
The otigen name survives, repurposed for the developer toolchain. See The Pivot for the full narrative, and pivot/02-otigen-language-era.md for the design record of the retired language.
17.6 Where everything lives
| Tool | Repo |
|---|---|
otigen developer toolchain | pyde-net/otigen |
pyde node binary + engine | pyde-net/engine |
pyde-rust-sdk | pyde-net/pyde-rust-sdk |
pyde-ts-sdk | pyde-net/pyde-ts-sdk |
pyde-crypto-wasm | pyde-net/crypto-wasm |
Archived otic compiler | pyde-net/otic (archived) |
Archived wright toolchain | pyde-net/wright (archived) |
| The Otigen Book (historical) | pyde-net/otigen-book (preserved as historical artifact) |
17.7 Reading on
- Chapter 5: Otigen Toolchain — the deep reference for the
otigenbinary. - Chapter 3: Execution Layer — the WASM runtime that compiled contracts execute under.
- Chapter 11: Account Model — the name registry the toolchain interacts with.
- Chapter 18: Protocol Upgrades — how contract and protocol upgrades flow.
- Preface: The Pivot — narrative on why the toolchain looks the way it does.
Chapter 18: Protocol Upgrades
Pyde's upgrade model is the same one Bitcoin and Ethereum use: a public specification (PIPs), a reference implementation, and voluntary validator upgrade. Validators choose whether to run a new release. There is no on-chain governance switch that flips protocol rules without that choice.
This chapter covers the upgrade process end to end: the PIP linkage, the validator upgrade flow, hard-fork vs soft-fork distinctions, the emergency pause as a separate mechanism, and the patterns for in-flight state migrations.
18.1 Upgrade Categories
Different changes require different process weight.
| Category | Example | Process required |
|---|---|---|
| Operational config update | Bootstrap node list, log level | Operator-side; no PIP |
| Bug fix (no protocol change) | Memory leak, RPC parse bug | Code release; PIP not required |
| Backward-compatible feature | New opcode unused by existing contracts | PIP + voluntary upgrade; no fork |
| Backward-incompatible (hard fork) | Gas cost change, new tx type semantics | PIP + activation block + coordinated upgrade |
| Cryptographic primitive change | Hash migration | PIP + multi-version overlap window |
| Treasury action | Grant payout, audit funding | PIP + on-chain MultisigTx |
| Emergency response | Active exploit | EmergencyPause (multisig); fix; resume |
Each path has its own velocity. A bug fix can ship in days; a hash migration takes months and dedicated audit time.
18.2 The Voluntary Upgrade Flow
For a typical hard-fork-grade change (e.g., adjusting a gas constant):
Step 1 — PIP draft
Author writes the PIP, opens PR against zarah-s/pips, defines:
- the change
- the rationale
- the activation block height (or activation epoch)
- the test plan
- backward compatibility implications
Step 2 — Discussion + acceptance
Open review by core devs, validators, security team.
PIP merges into pips repo with a final number.
Step 3 — Implementation
Code change merged into the Pyde repo, referencing the PIP #.
Ships in the next node release (e.g. v0.5.0).
Step 4 — Release announcement
The release notes name the activation block.
Typical activation window: weeks to months out, to give validators
time to upgrade.
Step 5 — Validator upgrade
Each validator operator updates their binary. They can do this as
early as they want; the new code is dormant until activation block.
Step 6 — Activation
At the named activation block, every node running the new release
starts using the new rule. Nodes still on the old release either:
- Fork off (if the change is consensus-incompatible).
- Stay in sync (if the change is backward-compatible).
Step 7 — Stable state
After enough time, the upgrade is "settled" — old releases are
deprecated, the network runs the new rule.
There is no on-chain "yes/no" vote. The closest signal is what fraction of the active committee runs the new code at activation. If less than 2f+1 (85 of 128) validators upgrade, the new rule cannot reach finality and the change is effectively rejected by the network.
This is governance through validator opt-in. It is slow and conservative by design.
18.3 Hard Fork vs Soft Fork
Hard fork (consensus-incompatible)
A hard fork is a change that nodes running the old rules cannot accept under any circumstances — e.g., a new gas cost, a new transaction type the old code doesn't recognize, a change to the encryption scheme.
For a hard fork:
- Activation block must be set well in advance.
- Validator coordination is required: at least 2f+1 must be on the new release at activation.
- Validators that don't upgrade fork off; their chain is the legacy version.
- Hard forks should be rare and well-justified.
Soft fork (backward-compatible)
A soft fork tightens the rules — old nodes still accept the new rules (they're a subset of what the old node would accept), but new nodes won't accept blocks that violate the new rules.
For a soft fork:
- Activation can be more gradual; majority of validators on the new release is enough to enforce the new rule.
- Old validators stay in sync; they just don't enforce the new constraint themselves.
- Soft forks are the preferred path when possible.
Simple non-fork
Changes that don't alter consensus semantics — e.g., a new RPC method, a performance optimization, a logging fix — ship in regular releases without any activation block. Operators upgrade at their own pace.
18.4 What Can and Can't Be Changed
Hard-coded (require code release + PIP)
Per Chapter 15:
| Constant | Where |
|---|---|
| DAG round period (~150 ms) | crates/consensus/src/round.rs |
| Commit target (~500 ms) | crates/consensus/src/wave.rs |
| Committee size (128) | crates/consensus/src/committee.rs |
| Quorum / threshold (85) | crates/consensus/src/quorum.rs |
| Equivocation threshold (44) | crates/consensus/src/quorum.rs |
| Validator min stake (10,000 PYDE) | crates/tx/src/pipeline.rs (will move to shared crate post-consensus-rebuild) |
| Operator-identity cap (3 / operator) | crates/tx/src/pipeline.rs |
| Unbonding period (30 days) | crates/consensus/src/validator.rs |
| Inflation schedule | crates/tx/src/fee.rs |
| Fee split (70/20/10) | crates/tx/src/execution.rs |
| Gas target / ceiling | crates/tx/src/fee.rs |
| Tx / calldata size limits | crates/tx/src/validation.rs |
| Max batch size (4 MB) | crates/mempool/src/batch.rs |
| Cryptographic primitives | pyde-crypto polyrepo (FALCON, Kyber, Blake3, Poseidon2) |
| WASM host function ABI | crates/wasm-exec/src/host_fns.rs + Host Function ABI spec doc |
Changing any of these requires a release + voluntary upgrade.
On-chain (multisig-controlled)
| Item | Mechanism |
|---|---|
| Treasury spend | MultisigTx (type 9) |
| Multisig signer set | RotateMultisig (type 10) |
| Emergency pause | EmergencyPause (type 11) |
| Resume from pause | EmergencyResume (type 12) |
These are bounded actions — drain treasury (with PIP linkage), rotate signers, halt for ≤ 30 days, resume. They cannot change protocol rules.
Operator-side
| Item | Lives in |
|---|---|
| Bootstrap peer list | pyde.toml [network] bootstrap_peers |
| RPC endpoint config | pyde.toml [rpc] |
| Log level / format | pyde.toml [logging] |
| Metrics port | pyde.toml [metrics] |
| Datadir location | pyde.toml [node] datadir |
Operators control these per-node; they don't require coordination.
18.5 Emergency Pause as Crisis Response
EmergencyPause (type 11) is not a normal upgrade mechanism — it's a
crisis-response tool. The signer set should be specifically chosen for
crisis response (core devs + security team), with a low threshold so a
quick response is possible.
Workflow during a live exploit:
t=0 Active exploit detected
t+5min Security team confirms; emergency multisig assembles signatures
t+10min EmergencyPause (duration: e.g., 24 hours) submitted on-chain
t+20min Pause takes effect; chain halts non-Resume txs
t+1-24h Fix developed, code-reviewed, audited, released
t+24h EmergencyResume submitted; chain resumes
t+24h Validator operators upgrade to the patched release
The 30-day max pause window (MAX_PAUSE_DURATION_WAVES) is a hard
ceiling — no extension mechanism. If an issue genuinely requires longer
than 30 days to fix, the chain restarts via genesis adjustment plus
voluntary validator upgrade — a much heavier process designed for the
"this can't be fixed in one pause window" case.
18.6 State Migration Patterns
Changes that affect on-chain state require a migration plan. Three common patterns:
Pattern 1: Lazy migration
The old format remains valid; new code accepts both and writes the new format on first touch.
Example: adding a new field to the Account struct.
- Old encoding: 141 bytes
- New encoding: 145 bytes (4 extra bytes for new_field)
- Migration: nodes accept both; on any update to an account, write the
new format.
- Eventually all touched accounts are in the new format. Old untouched
accounts stay in the old format until something writes them.
This works for additive changes that don't break existing readers.
Pattern 2: Activation-block migration
A specific block height where the format flips. Before that block, old format; after, new format.
Example: changing the canonical hash function (hypothetical).
- Activation block N.
- Pre-N: all hashes are Poseidon2.
- Post-N: all hashes are NewHash2.
- Old data continues to be read with Poseidon2 (matches its block height);
new data uses NewHash2.
- State proofs for pre-N data remain valid against pre-N state roots;
post-N data uses post-N state roots.
This is heavyweight. It requires careful protocol-version tracking on every state read.
Pattern 3: Migration transaction
The migration is a tx that anyone can submit; it transforms specific state in place.
Example: per-account vesting schedule format change.
- PIP defines the migration transaction format.
- During an upgrade window, anyone can submit MigrateVesting(account)
transactions that re-write the schedule in the new format.
- After a deadline, the old format is no longer accepted.
Useful when the migration is per-account or per-asset and can be done gradually.
18.7 Versioning Discipline
The Pyde release cadence is release-based, not block-based — releases
ship when ready, not on a fixed schedule. Each release has a semver-style
version (e.g., 0.4.2).
| Component | Version source |
|---|---|
| Node binary | pyde --version |
otigen developer toolchain | otigen --version |
pyde-rust-sdk crate | Cargo.toml version |
pyde-crypto-wasm pkg | package.json |
| Host Function ABI version | embedded in the artifact |
| Contract ABI version | embedded in the artifact |
The binary embeds the wire-format version (EVIDENCE_VERSION = 1 for
slashing evidence, MULTISIG_VERSION = 0x01 for multisig payloads). If
either is bumped, that's a hard fork — the deserializer rejects unknown
versions.
18.8 Coordinating an Upgrade
The day-of-upgrade checklist for a hard fork:
T-30 days: PIP merged, release tagged, activation block announced.
T-14 days: Foundation publishes "validator upgrade tracker" — counts how
many of the active committee have signaled the new release.
T-7 days: If <80% of active committee on the new release, postpone the
activation block via a follow-up PIP.
T-1 day: Final reminder.
T-0: Activation block. New rule takes effect.
T+1 hour: Foundation confirms chain is producing under the new rule.
T+1 week: Old releases marked deprecated.
The "80% signaling threshold" is a social norm, not a protocol enforced threshold. The protocol-enforced threshold is 2f+1 = 85 of 128 — but shipping at exactly 85 is brittle: a single validator going offline mid-flight drops the network below quorum. 80%+ as a social coordination target gives margin above the protocol minimum.
Validator signaling
There is no explicit on-chain signal of "validator X has upgraded." The signaling happens out-of-band:
- Validators announce their version via the
identifylibp2p protocol. - Foundation operates a tracker that polls validators and reports aggregate version distribution.
- Validators may also announce intent in PIP discussion threads.
A more formal on-chain signal (e.g., embedding the running release in each committee member's vertex attestation) is on the post-mainnet improvement list.
18.9 Comparison: Pyde vs Other Upgrade Models
| Property | Pyde | Ethereum | Tezos / Cosmos |
|---|---|---|---|
| Off-chain proposal | PIP | EIP | TIP / CIP |
| On-chain governance vote | None | None at protocol level | Yes (stake-weighted) |
| Validator upgrade | Voluntary | Voluntary | On-chain "self-amendment" |
| Hard-fork coordination | Activation block + social | Activation block + social | Voted on-chain |
| Treasury action | On-chain multisig + PIP | Foundation grants | On-chain (Tezos), proposal (Cosmos) |
| Emergency halt | Multisig pause | None | Sometimes (social fork only) |
Pyde's model is closer to Ethereum / Bitcoin than to Tezos / Cosmos. The trade-off: slower to react than on-chain governance, but no plutocratic-vote attack surface.
18.10 Honest About Limitations
- No on-chain validator-upgrade signal. Coordinated activation depends on out-of-band tracking. A future PIP could add an opt-in signaling-via-vote-payload mechanism.
- No automatic rollback. If a hard fork ships with a critical bug discovered post-activation, recovery requires another release + another upgrade. The emergency pause buys time but doesn't undo state changes.
- Manual genesis adjustment for catastrophic-recovery scenarios is documented but never operationally tested at scale. (The mainnet plan's Phase 9 incentivized testnet is the place where this kind of recovery could be rehearsed.)
- No validator slashing for "voted for the wrong fork." Validators can signal whatever they want; only protocol-level misbehavior (double signing, equivocation, etc.) is slashed.
Summary
| Property | Status at mainnet |
|---|---|
| Upgrade model | PIP + voluntary validator upgrade |
| Hard fork mechanism | Activation block + coordinated upgrade |
| Soft fork mechanism | Same; old nodes stay in sync |
| Treasury action | On-chain MultisigTx + PIP linkage |
| Emergency response | EmergencyPause (≤30 days, auto-expiring) |
| State migration patterns | Lazy / activation-block / migration tx |
| Wire-format versions | EVIDENCE_VERSION, MULTISIG_VERSION (bumped on layout change) |
| On-chain validator-upgrade signal | None (out-of-band tracking) |
| Automatic rollback | None (re-release path) |
The next chapter covers the launch strategy — the ten-phase mainnet plan, the testnet milestones, and the audit + incentivized testnet requirements before mainnet genesis.
Chapter 19: Launch Strategy
This chapter is the road from "code in a repo" to "live mainnet." The full sequenced plan lives in the Roadmap; this chapter covers the principles and the shape, not the calendar.
There are no specific launch dates in this document. Phasing is honest; calendar commitments are not made.
19.1 Launch Philosophy
Three principles that shape every phase:
-
Audit before stake. Every line of consensus, cryptography, execution, and state-layer code goes through external audit before any user has serious skin in the game. The audit is not a formality.
-
Testnet exposure before mainnet. A multi-month incentivized testnet with reference contracts and external developers must run cleanly before any genesis ceremony. Real network conditions catch issues no simulation does.
-
Voluntary launch. No one is forced onto Pyde mainnet. The genesis validator set is recruited and validated; users opt in by deploying contracts and bridging value.
The plan is conservative on purpose. A delayed launch is recoverable; a botched launch is not. Bridge exploits and broken consensus hard-forks have ended chains.
19.2 The Shape of the Path
The roadmap groups work into sequenced phases. They are not strictly linear — many items run in parallel within a phase — but each phase has a bar that gates the next.
Summary, in order:
| Phase | Bar |
|---|---|
| Pivot foundations | Documentation, repo cleanup, foundational design specs |
| Engine cleanup | Pre-pivot crates removed from active workspace; archived for reference |
| WASM execution hardening | Single-language end-to-end (contracts deploy, execute, modify state, state verifiable) |
| Multi-language + parachain framework | All supported languages working; parachain governance + lifecycle complete |
| Public testnet | Multi-region committee, external developers building real contracts |
| Audit + stress + bug bounty | External audit complete; all critical findings resolved; stress testing passed |
| Mainnet candidate | Final build; validator set committed; genesis configuration locked |
The deliverables and exit criteria for each phase are in the Roadmap, enumerated to the smallest actionable unit. This chapter does not duplicate them.
19.3 What ships at mainnet vs after
Pyde mainnet ships with:
- Post-quantum cryptography: FALCON signatures, Kyber threshold encryption, Poseidon2 + Blake3 hashing.
- Mysticeti DAG consensus with sub-second median commit and 85-of-128 FALCON quorum certificates.
- WASM execution via wasmtime + Cranelift AOT, with the host-function ABI v1.0.
- JMT state with dual-hash (Blake3 + Poseidon2) per node, PIP-2 clustered keys, PIP-3 prefetch, PIP-4 write-back cache.
- libp2p + QUIC + Gossipsub networking with bootstrap-based peer discovery (no DHT).
- Native multisig accounts; ENS-style name registration for contracts and parachains.
- The
otigendeveloper toolchain with Rust, AssemblyScript, Go (TinyGo), and C/C++ support. - The Rust and TypeScript SDKs.
Mainnet does not ship with:
- Programmable accounts (post-mainnet —
Programmableenum variant reserved at v1 so contracts written today survive). - Native session keys (post-mainnet, paired with programmable accounts).
- Live parachain operator network (designed for v1, implementation in a later phase; the interfaces ship at v1 so the design forward-commits).
- ZK-aggregated FALCON signatures (the path to substantially higher signature-verification throughput; v2/v3 work).
- zk-WASM proven execution (research-stage; integrated when the upstream provers reach production quality).
- Cross-chain bridges to other L1s (post-mainnet, only with proven security models).
This split is intentional. v1 ships the properties that justify Pyde's existence — post-quantum security, MEV resistance, sub-second finality, commodity-hardware decentralization, multi-language WASM contracts. Everything else is sequenced honestly and shipped when ready.
19.4 The "claim 1/3" Discipline
A discipline carried forward from the consensus pivot:
No external TPS claim is published until the performance harness exists, has been run on production-realistic conditions, and the methodology is reproducible by third parties. Published headline numbers are 1/3 of measured peak — never burst, never microbenchmark, never single-machine if multi-region is the relevant scope.
The earlier consensus implementation hit roughly 4K TPS in lab tests despite higher claimed targets. The discipline above prevents that gap from recurring. Realistic v1 targets — 10-30K plaintext TPS, 500-2K encrypted TPS, on commodity validator hardware — come from this discipline.
See the Performance Harness companion document for the testing methodology.
19.5 What carries forward from the pivots
For context (see The Pivot for the full story):
- HotStuff-era consensus work — properties, lessons, and invariants carry forward; the code is archived and the consensus layer is being rebuilt around Mysticeti.
- Otigen-era execution work — the safety properties (reentrancy guards, checked arithmetic, typed storage, no
tx.origin, compile-time access-list inference) carry forward as patterns in the WASM host-function ABI and the binding generators; the language and custom VM are retired.
Both pivots reset the critical path for the affected layer but did not invalidate the work on adjacent layers (state, accounts, transactions, tokenomics, vesting, multisig, all preserved across both pivots).
19.6 Reading on
- Roadmap — the canonical sequenced plan with all phase deliverables.
- Preface: The Pivot — context on both architectural pivots.
- Performance Harness — testing methodology.
- Chapter 16: Security — threat model and audit scope.
- Chapter 6: Consensus — the consensus design that ships at mainnet.
Pyde Technical Design
Version 0.1
This is the comprehensive technical design document for Pyde. For high-level pitch, see WHITEPAPER.md. For operational specs, see the individual documents linked below.
Table of Contents
- Layered Architecture
- Consensus: Mysticeti DAG
- Cryptography
- Execution Layer
- State Layer
- Account Model
- Transaction Lifecycle
- Encryption & MEV Resistance
- Network Protocol
- Performance Targets
- Implementation Status
Layered Architecture
Pyde is a monolithic blockchain (consensus + execution + state in single binary) with these layers:
┌─────────────────────────────────────────────┐
│ Application │
│ WASM smart contracts, dApps, wallets, RPC │
├─────────────────────────────────────────────┤
│ Execution │
│ WebAssembly (wasmtime + Cranelift AOT), Block-STM, │
│ hybrid access-list scheduler │
├─────────────────────────────────────────────┤
│ State │
│ Jellyfish Merkle Tree (JMT) │
│ Hybrid: Blake3 native + Poseidon2 for ZK │
├─────────────────────────────────────────────┤
│ Consensus │
│ Mysticeti DAG, anchor selection, finality │
├─────────────────────────────────────────────┤
│ Cryptography │
│ FALCON-512, Kyber-768 threshold, DKG │
├─────────────────────────────────────────────┤
│ Network │
│ libp2p + QUIC, Gossipsub, worker/primary │
└─────────────────────────────────────────────┘
Consensus: Mysticeti DAG
Algorithm Choice
Pyde uses Mysticeti-style DAG consensus (Mysten Labs' production protocol on Sui). Chosen over Bullshark for faster commit latency (~390ms vs ~1s) and better liveness under validator failures.
Why DAG over HotStuff:
- No single-proposer bottleneck — every committee member contributes vertices continuously
- No view changes — eliminates the bug class that caused Pyde's pre-pivot wedges
- Censorship resistance — 127 honest members can include any transaction; censorship requires near-unanimous collusion
- Throughput scales with committee size, not constrained by one proposer's bandwidth
- Threshold-decryption integrates naturally at the order-commit boundary
Worker / Primary Split (Narwhal Pattern)
Each validator runs:
- Workers (N per validator): handle tx ingress, build batches, gossip batches peer-to-peer
- Primary (one per validator): handles consensus — produces vertices, gathers parents, signs state roots
Transactions travel the network exactly once (via worker gossip). Consensus vertices stay tiny (carry only batch hashes by reference).
Vertex Structure
#![allow(unused)] fn main() { struct Vertex { round: u64, member_id: u32, // validator address as u32 internally batch_refs: Vec<BatchHash>, // hashes of batches I have parent_vertex_refs: Vec<VertexHash>, // ≥85 round-(N-1) vertex hashes state_root_sigs: Vec<StateRootSig>, // attestations on recent commits prev_anchor_attestation: VertexHash, // attests prior round's anchor decryption_shares: Vec<DecryptionShare>, // piggybacked partials falcon_sig: FalconSig, // sig over the vertex } }
Each vertex is dual-role: header (declaring what data I have) AND attestation (acknowledging prior-round vertices via parent_vertex_refs). Parent references are implicit "votes" — no separate vote messages.
Rounds & Anchors
A round is a layer in the DAG, advancing when a member collects ≥85 parent vertices.
Each round has a deterministically-selected anchor:
anchor_member_id = Hash(beacon, round, recent_state_root) mod 128
The recent_state_root lookback (N=3 rounds) limits anchor predictability to ~450ms (down from a full epoch).
A commit fires when the anchor vertex has sufficient support (Mysticeti 3-stage support). Multiple commits can be in flight; ~95% of rounds commit successfully.
Commit
1. Anchor selected by deterministic rule
2. Anchor's subdag walked via parent_vertex_refs (transitive)
3. Subdag sorted: (round, member_id, list_order)
4. Batches dereferenced from each vertex
5. Threshold decryption ceremony runs (pipelined — partials pre-broadcast)
6. ≥85 partials combine per batch → plaintexts revealed
7. wasmtime executes in canonical order
8. State root computed (Blake3 + Poseidon2 dual)
9. ≥85 committee FALCON-sign state root (piggybacked on next vertices)
10. Finality declared
End-to-end latency: ~500ms median for plaintext, ~700ms for encrypted (decryption ceremony adds ~200ms within wave budget).
Committee
- Size: 128 validators per epoch
- Selection: uniform random from all validators with stake ≥
MIN_VALIDATOR_STAKE(10,000 PYDE). Single tier — every staked validator meeting the floor is in the same pool - Anti-Sybil: operator identity binding, max 3 validators per operator
- Equal power: all 128 have equal voting weight, vertex production rate, anchor probability
- Stake influence: only on eligibility + flat 30% pool yield share. Activity rewards within committee are contribution-weighted, not stake-weighted.
- Epoch length: ~3 hours (measured in wall-clock, not in round count, so it's stable across consensus-cadence changes)
- DKG ceremony: runs in background during prior epoch's last minutes
Safety & Liveness
- Safety: Mysticeti BFT — holds under any network with at most
f = 42Byzantine members (BFT tolerance⌊(n-1)/3⌋for n = 128) - Liveness: holds under partial synchrony
- Recovery: explicit halt detection + investigation + recovery (see CHAIN_HALT.md)
- Rollback: bounded to 1 epoch (3 hours) via governance multisig; beyond that, only hard fork
Cryptography
Signatures: FALCON-512
NIST FIPS 206 standard. Used for:
- User transaction authorization
- Validator vertex production
- Committee state-root attestations
- Decryption share authentication
Properties:
- Public key: 897 bytes
- Signature: 666 bytes
- Verification: ~80μs commodity CPU
- Post-quantum secure (lattice-based)
Threshold Encryption: Kyber-768
NIST FIPS 203 standard, with threshold variant.
- Public key: 1184 bytes (one per epoch, shared across all encrypters)
- Ciphertext overhead: ~1088 bytes + plaintext size
- Decryption: requires ≥85 of 128 partial decryptions combined via Lagrange interpolation
Critical invariant: commit-before-reveal. Consensus orders encrypted transactions before any decryption shares are released. Decryption happens after ordering is final.
v1 risk: production-grade threshold variants of lattice schemes (Kyber threshold) are research-stage. Pyde v1 may ship with classical-crypto threshold (ElGamal-style) as transitional measure, migrating to threshold Kyber when audited implementations mature. This is the single largest cryptographic engineering risk in the design.
Hash Functions: Hybrid Layered Strategy
| Use case | Hash | Why |
|---|---|---|
| JMT internal nodes | Blake3 | ~30× faster than Poseidon2 on CPU |
| State root (published) | Both | Blake3 native verification + Poseidon2 for ZK |
| Transaction hashes | Blake3 (ciphertext), Poseidon2 (plaintext canonical) | Per use |
| Address derivation | Poseidon2 | Used in sig-verify ZK circuits |
| FALCON sig hashing | Poseidon2 | Inside ZK aggregation circuit |
| Vertex hashes | Blake3 | Small volume, no ZK |
Random Beacon
Each epoch's beacon is produced by the previous epoch's committee:
- All 128 members sign a known message
"epoch_N_beacon"with threshold-share keys - ≥85 shares combine into deterministic aggregated signature
beacon_N = Hash(aggregated_signature)→ 32 bytes randomness- Published in last wave of epoch N
Properties: deterministic given shares, unpredictable until reveal, bias-resistant.
DKG (Distributed Key Generation)
Pedersen DKG, multi-round protocol (~30-60s runtime):
Round 1: Each member generates random secret polynomial f_i(x), degree 84
Round 2: Each member broadcasts public commitments to coefficients
Round 3: Member i sends f_i(j) to each other member j (encrypted)
Round 4: Member j verifies received values, sums s_j = Σ f_i(j) = f(j)
where f(x) = Σ f_i(x) is the combined polynomial
Result: Each member j holds s_j = f(j) (private share)
SK = f(0) is never computed; PK derivable from public commitments
Threshold = 85
Mathematical foundation: any 85 points on a degree-84 polynomial uniquely determine it (Lagrange interpolation), enabling 85+ members to perform partial decryptions that combine without anyone reconstructing SK.
Partial Decryption Math
Given ciphertext (c1, c2) where c1 = g^r, c2 = m · PK^r:
Each member i: partial_i = c1^(s_i)
Combine via Lagrange (any subset S of 85):
combined = Π_{i in S} partial_i^(λ_i)
= c1^(SK)
= PK^r
Decrypt: m = c2 / combined
SK is never assembled. Each member's s_i is reusable across many ciphertexts within the epoch.
Execution Layer
WASM Execution Layer (wasmtime)
WebAssembly via wasmtime, with Cranelift ahead-of-time compilation:
- WebAssembly Core Specification as the instruction set (industry-standard, externally maintained)
- Deterministic feature subset enforced (NaN canonicalization on; threads, non-deterministic SIMD, reference types, GC, multi-memory, memory64, WASI all disabled)
- Fuel-based gas metering (wasmtime's built-in mechanism)
- Per-contract module compilation cache (Cranelift AOT artifacts persisted)
- Deploy-time validator rejects modules with forbidden imports or non-deterministic features
- Host-function ABI is the only chain-side surface contracts can reach
Determinism is load-bearing. Same input transactions must produce byte-identical state transitions across all validators (consensus safety) and feasible ZK circuits (future validity proofs). wasmtime's determinism config + deploy-time validator together provide this guarantee.
Smart Contract Authoring
Contracts are authored in any wasm32-target language (Rust, AssemblyScript, Go via TinyGo, C/C++). The otigen developer toolchain handles the lifecycle: project scaffolding (otigen init), build with state binding generation (otigen build), deploy (otigen deploy), upgrade governance, wallet management.
Pyde safety attributes (preserved from Otigen-language era):
- Reentrancy off by default (opt-out via
reentrantattribute) - Checked arithmetic (wrapping ops require explicit opt-in)
- Typed storage via
[state]schema inotigen.toml - No
tx.origin(host function ABI exposes onlycaller) - View / payable / reentrant / sponsored / constructor attributes
- Compile-time static access list inference (from declared state schema)
- 4-byte function selectors
Build output: .wasm artifact + JSON ABI + deploy bundle.
Hybrid Parallel Scheduler
Combines two parallel-execution paradigms:
Static access lists (Solana-style): for functions where access can be inferred at compile time, the scheduler partitions transactions into parallel groups by their declared access sets. Deterministic, no speculation overhead.
Block-STM (Aptos-style): for functions with dynamic access patterns, transactions execute optimistically with read/write set tracking; conflicts trigger re-execution in canonical order.
The hybrid: Build-time state binding generator emits both declared_access_set (static) and dynamic_access_regions (runtime). Runtime scheduler uses static info for partition planning, falls back to Block-STM for dynamic regions.
Pyde-specific opportunity: controls compiler, runtime, language, and protocol — enabling this hybrid where most chains commit to one approach.
Preflight Execution
Users request access list + gas estimate via RPC before signing:
Client → pyde_estimateAccess(tx)
→ RPC runs a wasmtime preflight (dry-run against current state)
→ Returns: { gas_estimate, access_list }
Client attaches access_list to tx, signs
State staleness handled by treating access list as a hint — scheduler verifies at runtime, falls back to Block-STM on mismatch.
State Layer
Jellyfish Merkle Tree (JMT)
Radix-16, path-compressed Merkle tree (Diem/Aptos lineage):
- ~5–10 nodes per state operation (vs SMT's ~256)
- Substantial I/O savings at high TPS
- Standard authentication properties (commitment, inclusion/exclusion proofs)
State Root Commitment
Dual-rooted:
- Blake3 root: fast native verification (used by validators)
- Poseidon2 root: ZK-circuit-friendly (future light clients, validity proofs)
Both computed at each commit, both signed by committee.
State Pruning
| Node type | State retention |
|---|---|
| Archive node | All historical state |
| Full node (default) | Last 90 days |
| Committee validator | Last 30 days |
| Light client | Headers + cared-about accounts |
See STATE_SYNC.md for sync protocol details.
Account Model
Account State
#![allow(unused)] fn main() { struct Account { nonce: u64, balance: u128, gas_tank: u128, // pre-deposited for encrypted tx submission auth_keys: AuthKeys, // Single | Multisig | Programmable code_hash: Hash, // for contract accounts storage_root: Hash, // for contract storage (JMT subtree) key_nonce: u32, // FALCON key rotation counter } enum AuthKeys { Single(FalconPubkey), Multisig(M, Vec<FalconPubkey>), // M-of-N, max 16 Programmable, // reserved for v2 } }
Nonce Window
Pyde uses a 16-slot sliding nonce window instead of strict sequential nonces:
#![allow(unused)] fn main() { struct NonceState { base: u64, // lowest unused nonce used: u16, // 16-bit bitmap of consumed slots in [base, base+15] } }
Allows up to 16 concurrent in-flight transactions per account, out-of-order within the window. Standard EVM-style nonces force head-of-line blocking; Pyde's window decouples submission ordering from execution ordering.
Native Multisig (v1)
AuthKeys::Multisig(M, [pubkey_1, ..., pubkey_N]) requires M valid FALCON signatures over the tx hash. Max 16 signers. Used for treasuries, DAOs, exchange custody.
Significantly safer than contract-based multisig (Gnosis Safe model on Ethereum), which reimplements the same logic across projects with subtle bugs.
Programmable Accounts (v2)
Reserved enum variant at v1. When v2 ships:
- Account has signing keys AND attached WASM policy module
- Policy runs on every authorization, can implement: spend limits, time locks, allow-listed recipients, social recovery, tiered authorization, AI agent delegation
- Same fields as contracts (
code_hash+storage_root) - WASM "policy mode" — restricted state access during validation
Session Keys (v2)
Scoped, bounded, revocable delegation. The user authorizes a session key once; the dApp (or agent) signs many transactions on the user's behalf within the declared scope.
Type:
#![allow(unused)] fn main() { struct SessionKey { pubkey: FalconPubkey, scope: SessionScope, expires_at: WaveId, revoked: bool, } struct SessionScope { contracts: Vec<Address>, methods: Vec<Selector>, // optional; empty = all methods on allowed contracts max_spend: u128, spent_so_far: u128, // mutable, updated at tx commit } }
Registry. Session keys are stored under the account's programmable-policy state subtree. The slot_hash clusters with the account under PIP-2 so lookups during authorization are local. New keys are added by RegisterSessionKey txs signed under the main auth_keys; existing keys are revoked by RevokeSessionKey txs.
Authorization-time check (pseudocode):
fn authorize_session_tx(tx) -> Result<(), AuthError> {
let sk = lookup_session_key(tx.session_key_id)?;
// 1. Signature
verify_falcon(sk.pubkey, tx.hash, tx.session_sig)?;
// 2. Liveness
require(current_wave < sk.expires_at, KeyExpired);
require(!sk.revoked, KeyRevoked);
// 3. Scope
require(sk.scope.contracts.contains(&tx.to), OutsideContractScope);
if !sk.scope.methods.is_empty() {
require(sk.scope.methods.contains(&tx.selector), OutsideMethodScope);
}
// 4. Spend cap
let new_spent = sk.scope.spent_so_far + tx.value;
require(new_spent <= sk.scope.max_spend, ExceedsSpendCap);
// On commit:
// sk.scope.spent_so_far = new_spent;
Ok(())
}
Use cases:
- Gaming — sign once, play many actions.
- AI agents — bounded delegation (e.g., "trade at most 100 PYDE/day on this DEX until next Friday").
- Consumer apps — subscriptions, micro-transactions.
- Embedded wallets — passkey-style flows where the main key never leaves a secure enclave.
Limits.
- Maximum 32 active session keys per account (anti-squat).
max_spendis monotonic — increasing it requires a new key, not a mutation.expires_atcannot exceedcurrent_wave + MAX_SESSION_WAVES(default: ~30 days at 500ms/wave = ~5.18M waves).
v1 reservations: AuthKeys::Programmable enum tag 0x03, account code_hash + storage_root fields, WASM policy-mode execution flag, multisig signature pipeline. All present at genesis; only the policy engine and session-key registry need to be added at v2.
Threat-model entries for session keys live in companion/THREAT_MODEL.md §Authorization Layer (added v0.2).
Transaction Lifecycle
Plaintext Transaction
User wallet:
1. Construct unsigned tx (sender, recipient, amount, nonce, gas, payload, deadline)
2. RPC pyde_estimateAccess(tx) → returns gas_estimate + access_list
3. Attach access_list to tx
4. FALCON-sign tx hash
5. Submit: pyde_sendRawTransaction(signed_tx)
RPC node:
6. Verify wire format, size, chain_id
7. Forward to nearest validator worker
Worker:
8. Verify FALCON sig at ingress
9. Verify nonce within window, balance, gas
10. Batch with other txs
11. Gossip batch to peer workers
Primary (every ~150ms):
12. Produce vertex referencing batches + parents
13. Gossip vertex
14. Peer primaries cert via next-round parent refs
Commit (per round, ~390ms median):
15. Anchor selected; subdag walked; canonical order emitted
16. wasmtime executes in canonical order:
- Nonce window check (state may have changed)
- Balance check
- Access list verification (vs runtime)
- Hybrid scheduler partitions txs into parallel groups
- Execute, apply state diffs
17. JMT updated, state root computed (Blake3 + Poseidon2)
18. Committee FALCON-signs state root, ≥85 collected
19. Finality declared
Encrypted Transaction
Same as above, with:
- Step 4.5: After FALCON-sign, Kyber-encrypt signed_tx with epoch PK
- Step 5: pyde_sendRawEncryptedTransaction(encrypted_blob)
- Worker step 8: cannot verify sig (encrypted) — only verify wire format
- Commit step 15.5: threshold decryption ceremony — ≥85 partials combine per encrypted tx (batches contain a mix of plaintext + encrypted txs) → plaintexts revealed. Share-combine is batched across the wave for amortised cost.
- Then wasmtime step 16 includes first sig verification
Encryption & MEV Resistance
Three structural defenses, layered:
Layer 1: Threshold Encryption
Users encrypt time-sensitive transactions (DEX swaps, NFT mints, liquidations) before submission. Encrypted blob is opaque — even committee members cannot decrypt alone.
Layer 2: Commit-Before-Reveal
Consensus orders encrypted transactions before decryption shares are released. By the time content is revealed, ordering is fixed and irreversible.
Layer 3: No Proposer
Pyde's DAG consensus has no single party empowered to choose which transactions enter a commit or in what order. The canonical order emerges deterministically from the DAG; no member can selectively reorder, exclude, or front-run.
Combined effect: sandwich attacks, front-running, proposer extraction are structurally impossible — not policed, not auctioned, not made more efficient, but eliminated.
Encryption is Optional
Per-tx choice via envelope:
pyde_sendRawTransaction— unencrypted, fast path, no MEV protectionpyde_sendRawEncryptedTransaction— encrypted, MEV-resistant, costs more gas
Wallets default to "auto" — encrypt time-sensitive, skip for simple transfers.
Encryption bandwidth cost: ~70% reduction if 80% of txs are unencrypted simple transfers (typical mix).
Network Protocol Summary
See NETWORK_PROTOCOL.md for full details.
Key choices:
- Transport: QUIC over UDP (no HOL blocking, built-in TLS 1.3)
- Library: libp2p (Rust) — mature, audited
- Peer discovery: layered (hardcoded → DNS → on-chain registry → PEX → cache); no DHT
- Gossip: Gossipsub with per-topic meshes
- DoS: 4-layer (connection/message/peer-scoring/application)
- Committee defense: sentry node pattern (Cosmos-style)
Performance Targets
Honest Targets
Validated by multi-region production-realistic harness (see PERFORMANCE_HARNESS.md):
| Metric | Realistic v1 | Stretch v2 | Aspirational |
|---|---|---|---|
| Plaintext TPS (commodity) | 10K-30K | 50K-100K | 500K |
| Encrypted TPS (commodity CPU) | 500-2K | 5K-10K | 50K+ (GPU) |
| Median finality | ~500ms | ~400ms | ~300ms |
| Committee NIC requirement (at TPS) | 500 Mbps | 1 Gbps | 10 Gbps |
"Claim 1/3 of Bench" Rule
- Harness measures: X TPS sustained
- Public claim: X/3 TPS
- Aspirational: X with "production validation pending"
No external TPS claim without harness evidence.
Hardware Tiers
| Role | Hardware |
|---|---|
| Light client | Mobile / browser |
| Full node / RPC | 8c / 16GB / 500GB / 100 Mbps |
| Non-committee validator | 8c / 16GB / 500GB / 100-250 Mbps |
| Committee at 30K TPS (v1 realistic) | 8-16c / 32GB / 1TB SSD / 500 Mbps |
| Committee at 100K TPS (v2 stretch) | 16c / 32GB / 2TB SSD / 1 Gbps |
| Committee at 500K TPS (aspirational, GPU-class) | 32c / 64GB / 4TB SSD / 10 Gbps |
Modest hardware applies to any validator awaiting committee selection at all levels. Active-committee hardware scales with throughput target. The 500K row is aspirational and tied to GPU-acceleration / batch-decryption research advances per the honest performance targets above.
Implementation Status
This documentation reflects designed architecture, not shipped implementation:
| Component | Status |
|---|---|
| Architecture design | ✅ Complete |
| WASM execution layer (wasmtime + Cranelift AOT) | 🟡 Foundation in place; integration in progress; programmable-accounts hooks + hybrid scheduler integration pending |
| State layer (JMT) | 🟡 In place, needs hybrid hashing |
| Consensus (Mysticeti DAG) | 🔴 Not yet — rebuild post-pivot |
| Threshold cryptography | 🔴 Research-grade (PQ threshold is bleeding-edge) |
| Network protocol (libp2p) | 🟡 Existing in archive, needs migration |
| Performance harness | 🔴 Not yet built |
| Slashing + lifecycle | 🟡 Partial in archive |
| State sync | 🟡 Partial design |
| Documentation | 🟡 This is the current state |
Mainnet ships when the work above is complete and the external audit passes. No public schedule.
Highest-risk piece: post-quantum threshold cryptography. Research-stage, may require classical-crypto transitional v1 with migration to PQ threshold in v2 as standards mature.
Cross-References
| Topic | Document |
|---|---|
| Threats & adversaries | THREAT_MODEL.md |
| Operational failures | FAILURE_SCENARIOS.md |
| Halt + recovery procedures | CHAIN_HALT.md |
| Slashing rules | SLASHING.md |
| Validator state machine | VALIDATOR_LIFECYCLE.md |
| State sync protocol | STATE_SYNC.md |
| Network protocol | NETWORK_PROTOCOL.md |
| Performance harness | PERFORMANCE_HARNESS.md |
| Token economics | TOKENOMICS.md |
Document version: 0.1
License: See repository root
Pyde: A Post-Quantum, MEV-Resistant Layer 1 with DAG Consensus
Version 0.2 — May 2026 Pyde Network · Apache-2.0
Abstract
Pyde is a Layer 1 blockchain built greenfield to ship four properties as defaults from genesis:
- Post-quantum cryptography — FALCON-512 signatures, Kyber-768 threshold encryption, Poseidon2 + Blake3 hybrid hashing. No pre-quantum primitive on any consensus or account path.
- MEV resistance — threshold-encrypted mempool + commit-before-reveal ordering + DAG consensus. Sandwich attacks, front-running, and proposer extraction are not policed or auctioned; they are structurally impossible.
- Sub-second finality — Mysticeti-style DAG consensus, ~500 ms median commit finality, an 85-of-128 FALCON quorum certificate.
- Commodity-hardware decentralization — full nodes and validators awaiting committee selection run on 8 cores / 16 GB RAM. Validators on the active committee at production throughput require a 500 Mbps – 1 Gbps NIC; every committee seat carries one vote regardless of stake.
The execution layer is WebAssembly via wasmtime (with Cranelift AOT) and a hybrid parallel scheduler that combines static access lists (Solana-style) with optimistic Block-STM speculation (Aptos-style). Smart contracts are authored in Rust, AssemblyScript, Go (TinyGo), or C/C++ — any wasm32-target language — with Pyde safety attributes (reentrancy off by default, checked arithmetic, typed storage, no tx.origin, compile-time access-list inference) preserved as language-native attributes and enforced at runtime. The otigen developer toolchain handles project scaffolding, build, state binding generation, and deployment. Cross-chain interactions are served by the parachain framework (v1) and post-mainnet bridge contracts gated by HardFinalityCert — a FALCON quorum certificate verifiable on any chain.
This document presents the current design following a May 2026 architectural pivot from an in-house HotStuff variant (whose persistent wedges and stalls at 400 ms slot timing motivated a clean rebuild) to a DAG-based consensus inspired by Narwhal, Bullshark, and Mysticeti. The pivot scoped the chain to its execution and cryptography layers first; the consensus layer is being rebuilt design-first against the new foundation.
Realistic v1 mainnet throughput targets, validated by a multi-region performance harness, are 10 K–30 K plaintext TPS sustained and 500–2 K encrypted TPS on commodity validator hardware. Aspirational long-term targets (with GPU acceleration, batch threshold decryption, and protocol upgrades) reach 500 K TPS; those are not v1 commitments.
1. The Problem
Four open architectural debts run across the production L1 set today, each of them protocol-level rather than application-level, and each easier to ship at genesis than to migrate into a chain that has been running without it.
Quantum vulnerability. Every major L1 in production — Bitcoin, Ethereum, Solana, Cardano, Polkadot, Aptos, Sui — secures its consensus and account paths with classical cryptography (secp256k1, Ed25519, BLS12-381) that falls to Shor's algorithm on a cryptographically-relevant quantum computer. NIST's 2024 standardization of FALCON, ML-DSA, and ML-KEM unblocked the post-quantum primitives, but retrofitting them into a live chain with trillions of dollars at risk and deployed contracts hard-coded against pre-quantum key formats is a multi-year coordinated migration. The chains have not been blind to the problem; the constraint is the shape of the migration, not the seriousness of the response.
MEV extraction. Maximum Extractable Value has hardened into a multi-billion-dollar tax paid by retail users to validator-builder coalitions. Sandwich attacks, front-running, and proposer extraction are not bugs to be patched — they are structural consequences of public mempools combined with single-proposer block production. The incumbent response has been to make the MEV market more efficient (proposer-builder separation on Ethereum, Jito on Solana). The alternative — removing the information asymmetry at the protocol level — is harder to retrofit because builder economics are now baked into the validator revenue model.
Throughput at finality. Chains optimizing hardest for throughput have made validation a premium-hosting business. A Solana validator at production performance requires 12+ cores and 256+ GB RAM. Chains optimizing for decentralization have ended up with throughput unusable for serious applications. The combination — sub-second hard finality at retail-scale throughput on commodity hardware — is the category no production chain occupies cleanly today.
Centralization at scale. Validation, smart-contract deployment, and cross-chain interaction have all converged toward gated infrastructure: data-center validators, custodial bridge multisigs, oracle networks run by small operator coalitions, app-chain slots auctioned to well-capitalized parachain teams. Each is a coherent local response to the constraint set the chain in question faced; the cumulative cost lands on the user.
These four problems are not independent items. They converge in time. NIST's 2024 standardization matured the cryptographic primitives at the same moment that MEV literature converted into quantified user-cost numbers, at the same moment that Solana's hardware creep made the decentralization cost visible, at the same moment that L2 sequencer trust assumptions started attracting public scrutiny. The architecture that wins the next decade does not have to be the one that won the last one. No chain in production today provides all four properties as defaults. Pyde is the chain built to occupy that position.
2. Four Axioms
Every design choice in this document follows from four axioms.
Axiom 1 — Post-quantum cryptography is the default. No application-layer signature, encryption, or hash in Pyde uses pre-quantum primitives. FALCON-512 signs every consensus vote, every transaction, every validator key registration. Kyber-768 / ML-KEM encrypts every encrypted-mempool transaction. Poseidon2 (Goldilocks field) hashes ZK-bearing commitments; Blake3 hashes the high-volume native paths where ZK-friendliness is not in scope. Ed25519 appears only in libp2p's noise transport for peer routing — a quantum attacker who breaks Ed25519 learns the network topology but cannot forge a vertex, decrypt a transaction, or compromise an account.
The trade-off is signature size: 666 bytes for FALCON-512 versus 64 bytes for Ed25519, 1,088 bytes per Kyber-768 ciphertext versus negligible plaintext overhead. Pyde absorbs that cost in the layers that matter and avoids it everywhere it does not (e.g., gossip-level message authentication uses Blake3 + libp2p noise).
Axiom 2 — MEV is a protocol bug. No committee validator must be able to read, reorder, or selectively include unconfirmed transactions. This is a security property, not a market-design problem. Pyde achieves it with three interlocking mechanisms (Section 8 has the details):
- Transactions can be encrypted under a Kyber-768 threshold public key held jointly by the 128-validator committee — no fewer than 85 of 128 shares can decrypt.
- The committee commits to a canonical order at the DAG anchor before any decryption share is released. The order is fixed by the time content is visible.
- There is no single proposer. Order emerges deterministically from the DAG by every honest validator independently; no committee member can reorder, exclude, or front-run.
The combination removes the surface MEV extraction needs to exist on. Encryption is opt-in per transaction; simple transfers go plaintext for lower fees, MEV-sensitive operations (DEX swaps, NFT mints, liquidations) opt into encryption.
Axiom 3 — Throughput requires parallel execution in a single binary. Consensus and execution share a single process. The execution layer is a WebAssembly execution (wasmtime + Cranelift AOT) with a hybrid parallel scheduler: static access lists for functions with compile-time-known accesses, Block-STM speculation for dynamic accesses. The choice is monolithic over modular: every cross-layer boundary is a trust boundary and a latency cost; for an L1 whose target is high-throughput low-latency MEV-free execution, coherence is worth more than heterogeneity. Cross-chain interoperability is added back as a separate permissionless parachain layer above the coherent base, not as a structural premise that fragments the chain at genesis.
Axiom 4 — Decentralization is the protocol's burden, not the user's. Validators run on commodity hardware. Every committee member has exactly one vote regardless of stake — the validator bond is anti-Sybil cost, not a power multiplier. Cross-chain infrastructure is permissionless: any operator who stakes PYDE and runs a Pyde-published spec joins the parachain operator set, no auctioned slots, no gatekeeping team. The cost of participating in Pyde — running a node, validating, building a parachain — is a function of will and a small fixed bond, not access to data-center capital or auction proceeds.
3. The May 2026 Pivot
Pyde's earlier architecture used an in-house pipelined HotStuff variant with VRF proposer selection at 400 ms slot timing. Repeated wedges — head-divergence deadlocks, view-change cascades, quorum starvation under network jitter — were being addressed by accumulating patches rather than fundamental changes. The team made a clean break: remove the entire consensus, mempool, and networking layers from the active workspace and rebuild against a foundation with a smaller protocol surface and simpler safety arguments.
Post-pivot:
- The active engine workspace contains six execution-layer crates:
crypto,pvm,aot,state,account,tx. Nothing else. - Consensus, mempool, networking, slashing, and the node binary have been moved to a
archive/archive for reference. - The next consensus layer is being designed against the lessons of HotStuff failure: no view changes, no single-proposer bottleneck, data-driven round advancement, structural censorship resistance.
The decision: Mysticeti-style DAG consensus, with FALCON-bound vertex production and threshold-decryption ceremonies pipelined into the commit boundary. The remainder of this document describes the post-pivot design.
4. Architecture
Pyde is a monolithic Layer 1 chain — consensus, execution, and state in a single binary — with a layered protocol structure.
┌─────────────────────────────────────────────┐
│ Application Layer │
│ WASM smart contracts, dApps, wallets, RPC │
├─────────────────────────────────────────────┤
│ Execution Layer │
│ WebAssembly (wasmtime + Cranelift AOT), hybrid scheduler │
│ (static access lists + Block-STM) │
├─────────────────────────────────────────────┤
│ State Layer │
│ Jellyfish Merkle Tree (JMT), hybrid hashing │
│ (Blake3 native + Poseidon2 ZK-bearing) │
├─────────────────────────────────────────────┤
│ Consensus Layer │
│ Mysticeti DAG, anchor selection, wave │
│ commit (rebuild in progress) │
├─────────────────────────────────────────────┤
│ Cryptography Layer │
│ FALCON-512 sigs, Kyber-768 threshold, DKG, │
│ threshold decryption, VRF │
├─────────────────────────────────────────────┤
│ Network Layer │
│ libp2p + QUIC, Gossipsub, worker / primary │
│ split (Narwhal pattern) │
└─────────────────────────────────────────────┘
Three operational tiers run the same binary; role differentiation is configuration:
| Tier | Stake | Committee role | Earns |
|---|---|---|---|
| Validator | 10K PYDE min (single tier) | Eligible for uniform-random committee selection each epoch | Reward-pool share (stake × uptime) + inflation share + activity-weighted bonus while on active committee |
| RPC node / full node | None | None | Off-chain RPC fees (market-set) |
5. Cryptography
5.1 FALCON-512 Signatures
Every transaction, vertex, and state-root attestation is signed with FALCON-512 (NIST FIPS 206). Properties:
- Signature size: ~666 bytes (variable, hard cap 1,280 bytes). Public key: 897 bytes.
- Verification: ~80 µs on commodity x86_64 / ARM64.
- No post-quantum BLS analog has matured, so consensus quorum certificates are the union of N FALCON signatures over a
voter_bitmaprather than a single aggregated signature. The mainnet bandwidth budget (500 Mbps – 1 Gbps NIC at the relevant TPS tier) is sized to absorb the QC size.
5.2 Kyber-768 Threshold Encryption
Pyde's encrypted mempool uses Kyber-768 (NIST FIPS 203) with a threshold variant. At each epoch the 128 committee members run a Distributed Key Generation ceremony producing one public key PK and 128 shares s_i. The threshold is 2f + 1 = 85 of 128 — the same quorum that gates commit and finality.
Transactions can optionally be encrypted under PK before submission. Decryption requires 85 committee members to compute partial decryptions and combine them by Lagrange interpolation. No coalition of fewer than 85 can decrypt anything — the unique secret only exists in distributed form.
Critical invariant — commit-before-reveal. Consensus commits to an order at the DAG anchor before any decryption share is released. By the time content is revealed, the order is fixed and irreversible. This is what eliminates MEV at the protocol layer.
5.3 Hybrid Hashing: Blake3 + Poseidon2
| Use | Hash | Reason |
|---|---|---|
| JMT internal nodes (high volume) | Blake3 | ~30× faster than Poseidon2 on CPU; not in ZK circuits |
| Published state root (per commit) | Both (Blake3 native + Poseidon2 ZK) | Native verification fast; ZK validity proofs future-compatible |
| Transaction hashes — ciphertext | Blake3 | Gossip / dedup, not in ZK |
| Transaction hashes — plaintext canonical | Poseidon2 | Inside sig-verify ZK circuits |
| Address derivation | Poseidon2 | Poseidon2(falcon_pk) exposed to sig-verify circuits |
| FALCON sig payload hashing | Poseidon2 | Inside ZK aggregation |
Poseidon2 over the Goldilocks field is the algebraic hash everywhere a future ZK proof would need to re-derive the value in-circuit; Blake3 is the high-throughput native primitive where ZK exposure is not in scope.
5.4 Randomness Beacon
Each epoch's beacon is produced by the previous epoch's committee via a threshold-signature ceremony on a known message. ≥ 85 shares combine into a deterministic aggregated signature; the hash of the signature is the beacon, 32 bytes. The beacon seeds:
- Per-round anchor selection:
anchor_member_id = Hash(beacon, round, recent_state_root) mod 128 - Next epoch's committee VRF picks
- Other protocol randomness
The recent_state_root term reduces anchor predictability from a full epoch (~ 3 hours) to a few rounds (~ 450 ms).
6. Consensus: Mysticeti-Style DAG
6.1 Why DAG (Why Not HotStuff)
The pre-pivot HotStuff variant exhibited persistent wedges and view-change cascades under realistic network conditions. The DAG approach removes the fragile parts:
| Problem in HotStuff | DAG resolution |
|---|---|
| Single proposer bottleneck | No proposer — every member contributes vertices each round |
| View-change protocol complexity | No view changes — eliminated an entire failure class |
| Timing-driven slot pipeline | Data-driven rounds advance with quorum, not clock |
| Proposer can selectively censor | 127 honest members can include any tx; censorship requires near-unanimous collusion |
| Throughput limited by leader bandwidth | Throughput scales with committee size |
The DAG also integrates cleanly with threshold decryption: the commit boundary is the natural place to run the decryption ceremony, with partial shares piggybacked on vertices in the rounds leading up to it.
6.2 Worker / Primary Split (Narwhal Pattern)
Each validator runs two roles:
- Workers (N processes): handle transaction ingress, build batches, gossip batches to peer workers.
- Primary (one process): handles consensus — produces vertices each round, gathers parent references, signs state roots, runs the DKG.
Transactions traverse the network once via worker gossip; consensus vertices stay tiny because they carry only batch hashes by reference (Section 6.3).
6.3 The Vertex
Each round, every committee member's primary produces exactly one vertex:
#![allow(unused)] fn main() { struct Vertex { round: u64, member_id: u32, batch_refs: Vec<BatchHash>, // hashes of batches I have parent_vertex_refs: Vec<VertexHash>, // ≥ 85 round-(N-1) hashes state_root_sigs: Vec<StateRootSig>, // attestations on recent commits prev_anchor_attestation: VertexHash, // attestation of prior anchor decryption_shares: Vec<DecryptionShare>, // piggybacked partials falcon_sig: FalconSig, // sig over the vertex } }
Parents must come strictly from the prior round (no skip edges in v1). The DAG is a consensus structure; transaction data lives in batches stored at the worker layer, referenced by hash.
Vertex size: typically ~ 830 bytes minimal, ~ 25 KB heavy (50 batches + 5 state-root sigs + 85 decryption-share partials); hard cap 64 KB.
6.4 Rounds, Anchor, and Commit
Rounds are data-driven: a member ticks from round N to N + 1 once it collects ≥ 85 valid round-N parents (the slowest 43 can lag without blocking anyone). Round rate: ~ 5–10 rounds / sec depending on network conditions.
Each round has a deterministically-selected anchor:
anchor_member_id = Hash(beacon, round, recent_state_root) mod 128
When the anchor vertex collects sufficient Mysticeti 3-stage support from later rounds, a commit fires:
- Anchor's subdag is collected by walking
parent_vertex_refstransitively. - The subdag is sorted deterministically:
(round, member_id, list_order). - Batches referenced by each vertex are dereferenced.
- For each encrypted transaction within those batches (encryption is per-tx, not per-batch), the threshold decryption ceremony runs (pipelined — partial shares are already in flight by commit time via the
decryption_sharesfield of vertices observed during the prior rounds). - wasmtime executes decrypted transactions in canonical order.
- State root is computed (Blake3 + Poseidon2 dual), FALCON-signed by ≥ 85 committee members.
- Finality is declared once ≥ 85 state-root signatures converge.
Median end-to-end finality target: ~ 500 ms. Validated by performance harness pre-publication.
6.5 Committee
128 validators per epoch, drawn from the global validator pool:
- Selection: uniform random from all validators with stake ≥
MIN_VALIDATOR_STAKE(10,000 PYDE). Single tier — no separate committee/non-committee stake floors. - Anti-Sybil: operator identity binding, max 3 validators per operator.
- Equal power: every committee member has equal voting weight, equal vertex production rate, equal anchor probability. Stake influences only (a) eligibility and (b) the proportion of the flat 30 % stake-pool yield share. Activity rewards are contribution-weighted, not stake-weighted.
- Epoch length: ~ 3 hours wall-clock (round count varies with network conditions).
- DKG: runs in the background during the prior epoch's last minutes; the new committee has the threshold key ready by epoch start.
6.6 BFT Properties
For n = 128: f = ⌊(n − 1) / 3⌋ = 42 is the maximum tolerable Byzantine count; the quorum threshold is 2f + 1 = 85. This single number appears throughout the protocol (vertex certification, commit support, threshold decryption, state-root sigs, DKG output) — consistency across uses avoids attack edges from boundary mismatches.
Safety holds under any network conditions assuming at most f = 42 Byzantine members. Liveness holds under partial synchrony.
6.7 Halts and Recovery
When safety appears at risk (e.g., contradictory state-root sigs), the protocol auto-halts. Three halt classes:
| Class | Trigger | Authority |
|---|---|---|
| Soft stall | Network / quorum slack | Emergent (auto-recovers) |
| Hard halt | Contradictory state roots, equivocation cluster, DAG fork | Protocol-detected automatic |
| Emergency halt | Off-chain bug report, active exploit, hard-fork prep | Governance multisig (7-of-12) |
Rollback is bounded to one epoch (~ 3 hours); within that window governance can authorize rollback to a prior consistent state. Beyond an epoch, only a coordinated hard fork is possible. This is the "weak finality with sunset" pattern — operational flexibility for early detection without arbitrary commit reversibility.
7. Execution: WebAssembly, Hybrid Scheduling
7.1 The WASM Execution Environment
Pyde executes smart contracts under wasmtime, the Bytecode Alliance's production WebAssembly runtime (in use at Microsoft, Fastly, Shopify):
- WebAssembly Core Specification: linear memory, structured control flow, validated bytecode, no syscalls
- Cranelift AOT compilation inside wasmtime — every module is compiled to native machine code at deploy time and cached; subsequent invocations re-use the compiled artifact, no JIT, no runtime recompile
- Fuel-based gas metering — every WASM instruction decrements a fuel counter at the basic-block level; when fuel hits zero, wasmtime traps and the transaction reverts
- Per-instance sandbox — each transaction runs in its own wasmtime instance with bounded linear memory (default 64 MB cap); the host (validator) decides which host functions are importable
- Host Function ABI for all chain interaction — storage (
sload/sstore/sdelete), balance and transfers, crypto (Blake3, Poseidon2, Keccak256, FALCON verify, threshold encrypt/decrypt), events, cross-contract and cross-chain calls - The retired Otigen language and custom
pyde-vmregister-based VM are preserved in the historical pivot record only; the active execution layer is wasmtime end-to-end
Determinism is load-bearing: the same WASM module with the same inputs and host-fn responses must produce byte-identical state transitions across all 128 committee members (consensus state-root agreement) and inside future ZK validity proofs over execution. Non-deterministic WASM features (threads, SIMD timing, floating-point environment) are disabled at module-validation time.
7.2 Smart Contract Authoring
Smart contracts are authored in any wasm32-target language (Rust, AssemblyScript, Go via TinyGo, C/C++). The otigen developer toolchain reads a otigen.toml, generates state bindings with pre-computed slot constants, invokes the correct language compiler, and produces a .wasm artifact plus JSON ABI:
- 30 keywords; storage maps, structs, enums, variable-length
Vec,String - 4-byte function selectors (EVM-compatible dispatch)
- Reentrancy guards (
#[reentrant]), checked arithmetic by default, custom errors and events #[view]/#[payable]/#[reentrant]function attributes- Compile-time static access-list inference: for each function, the compiler emits the set of storage slots it provably touches, plus regions where access depends on runtime values
- Block context is
block.anchor(the wave's canonical anchor vertex), notblock.proposer— Pyde's DAG has no single proposer, so contracts that depended onblock.proposeron other chains do not have an analog here
7.3 Hybrid Parallel Scheduler
Two parallel-execution philosophies, used together:
- Static access lists (Solana-style): for functions where access is inferable at compile time, the scheduler partitions transactions into parallel groups by their declared access sets. Deterministic, no speculation overhead.
- Block-STM speculation (Aptos-style): for functions with dynamic access patterns, transactions execute optimistically. Read / write sets are tracked at runtime; conflicts trigger re-execution in canonical order.
The Build-time state binding generator emits both declared_access_set (static) and dynamic_access_regions (runtime). The runtime scheduler uses the static information for partition planning and falls back to Block-STM for dynamic regions. Pyde controls compiler, runtime, and language — making this hybrid feasible where most chains commit to one approach.
Preflight at user submission time (via pyde_estimateAccess RPC) returns a runtime-observed access list, which the wallet attaches to the transaction. The scheduler treats access lists as hints, verifying at runtime and falling back to speculation on mismatch — safe by construction.
7.4 Transaction Lifecycle
Wallet:
1. Construct tx (sender, recipient, amount, payload, ...)
2. RPC pyde_estimateAccess → returns gas_estimate + access_list
3. Attach access_list to tx
4. FALCON-sign tx hash
5. (Optional) Encrypt signed tx + access_list with epoch PK
6. Submit to RPC
Worker:
7. Validate wire format
8. (Plaintext) verify FALCON sig at ingress
9. Batch with other txs
10. Gossip batch to peer workers
Primary:
11. Produce vertex referencing available batches
12. Gossip vertex; peers attest as parents in next round
Commit (~500 ms median):
13. Anchor selected; subdag walked; canonical order emitted
14. (Encrypted) threshold-decrypt batches
15. wasmtime executes in canonical order
16. State root computed, signed by ≥ 85 committee members
17. Finality declared on 85 state-root sigs
End-to-end latency: ~ 500 ms median for plaintext, ~ 700 ms for encrypted (adds the decryption ceremony to the commit budget).
8. State: Jellyfish Merkle Tree
State is stored in a Jellyfish Merkle Tree (radix-16, path-compressed), persisted in RocksDB. Compared to a fixed-depth-256 Sparse Merkle Tree:
- ~ 5–10 nodes touched per state operation (vs ~ 256)
- Substantial I/O savings at high TPS
- Same authentication properties (Merkle commitment, inclusion / exclusion proofs)
- Production-proven (Diem, Aptos)
State commitment is dual-rooted at every commit: Blake3 for fast native verification by committee and validators, Poseidon2 for future ZK light clients and validity proofs. Both roots are signed by ≥ 85 committee members.
The block witness — every state slot touched by a wave plus a single batched JMT proof against the pre-state root — has a hard 1 MB cap, rejected at verification before any proof work runs.
9. MEV Resistance
Three structural defenses, layered:
Layer 1 — Threshold encryption. Users can encrypt transactions before submission. The encrypted blob is opaque even to committee members. Mempool sees only encrypted bytes; attackers cannot observe content to position around.
Layer 2 — Commit-before-reveal. Consensus orders encrypted transactions at the DAG anchor before any decryption share is released. By the time content is revealed, the order is fixed and irreversible.
Layer 3 — No proposer. Pyde's DAG consensus has no single party empowered to choose which transactions enter a commit or in what order. The canonical order emerges deterministically from the DAG; no committee member can selectively reorder, exclude, or front-run.
The combination eliminates the structural conditions for sandwich attacks, front-running, and proposer extraction. MEV is not policed or auctioned — it is structurally impossible at the protocol layer.
Encryption is opt-in. Simple transfers go plaintext for lower gas; MEV-sensitive operations opt in via pyde_sendRawEncryptedTransaction. Encryption adds ~ 200 ms to end-to-end latency (the threshold decryption ceremony).
10. Network Protocol
- Transport: QUIC over UDP, with TCP fallback. No head-of-line blocking, built-in TLS 1.3.
- P2P library: libp2p (Rust). Audited, used by Ethereum / Filecoin / Polkadot.
- Node identity: Ed25519 keypair (separate from validator FALCON key, rotatable).
- Peer discovery: layered (hardcoded seeds → DNS → on-chain validator registry → PEX → persistent cache). No DHT.
- Gossip: Gossipsub with per-topic meshes (
vertices,batches,decryption_shares,state_root_sigs,mempool,state_sync). Message size limits per type, enforced at parse time. - DoS defense: four layers — connection (IP / ASN caps), message (rate limits per type), peer scoring (misbehavior accumulates, decays with good behavior), application (gas tank prepayment for encrypted submission).
- Committee defense: sentry-node pattern (Cosmos-style) to insulate committee primaries from direct internet exposure.
Committee NIC requirement at v1's honest throughput target (10–30 K plaintext TPS, 0.5–2 K encrypted) is ≥500 Mbps. Higher-throughput regimes (1 Gbps, 10 Gbps) appear in §12.1 below labeled as Stretch / Aspirational, not v1.
11. Cross-Chain: The Parachain Layer (Post-Mainnet)
Cross-chain interactions in Pyde — calling functions on other chains, querying oracles, requesting off-chain compute, indexing on-chain data — happen through a parachain layer of permissionless decentralized infrastructure providers. A parachain is not a sovereign app-chain. It is an open-source implementation of a Pyde-published specification, run by operators who stake PYDE, follow protocol-defined rules, and earn gas fees from contracts that call them.
11.1 The cross_call! Macro
#![allow(unused)] fn main() { cross_call!( target_chain = "ethereum", contract = "0x...", function = "balanceOf", args = [...], callback = "handle_balance_response", ); }
The macro is asynchronous. The originating transaction marks the call pending and emits an event; the actual cross-chain or oracle work happens off-chain at the parachain operator set; the result arrives in a separate callback transaction.
11.2 HardFinalityCert
A FALCON quorum certificate over (wave_id, blake3_state_root, poseidon2_state_root), signed by ≥ 85 of the active committee. Verification on any counterparty chain: 85 FALCON-512 verifies (~ 85 ms) plus a Merkle path — feasible on any chain with a reasonable VM. The cert's stability across the chain's lifetime is what makes parachains feasible without further protocol changes after mainnet.
11.3 Architecture vs Implementation
The protocol-level surface (the cross_call! macro, HardFinalityCert, unified gas model) is settled at genesis. The actual parachain layer — specification, reference implementations, operator economics, bridges to Ethereum / Cosmos / Solana — ships post-mainnet. The mainnet cross_call! initially returns a runtime "not yet supported"; contracts written today work without rewriting when parachains activate.
12. Performance
12.1 Honest Targets
Realistic v1 mainnet throughput, validated by a multi-region production-realistic harness:
| Mode | Realistic v1 | Stretch v1 | Aspirational |
|---|---|---|---|
| Plaintext TPS (sustained, commodity) | 10 K – 30 K | 50 K – 100 K | 500 K |
| Encrypted TPS (sustained, commodity) | 0.5 K – 2 K | 5 K – 10 K | 50 K + (GPU) |
| Median commit finality | ~ 500 ms | ~ 400 ms | ~ 300 ms |
| Committee NIC at sustained TPS | 500 Mbps | 1 Gbps | 10 Gbps |
These numbers will be revised based on actual harness output and adjusted using the "claim one-third of measured peak" rule.
12.2 Hardware Tiers
| Role | Hardware |
|---|---|
| Light client | Mobile / browser |
| Full node / RPC | 8c / 16 GB / 500 GB NVMe / 100 Mbps |
| Non-committee validator | 8c / 16 GB / 500 GB / 100 – 250 Mbps |
| Committee validator (v1 realistic, 30 K TPS) | 8 – 16c / 32 GB / 1 TB SSD / 500 Mbps |
| Committee validator (Stretch v2, 100 K TPS) | 16c / 32 GB / 2 TB SSD / 1 Gbps |
| Committee validator (Aspirational, 500 K TPS, GPU-class) | 32c / 64 GB / 4 TB SSD / 10 Gbps |
The commodity-hardware promise applies layered: full nodes and validators awaiting committee selection stay on a developer workstation at every TPS level. The first committee row is the v1 hardware Pyde is sized against; the higher rows are post-mainnet scaling targets, not v1 commitments.
12.3 Methodology
Pyde's pre-pivot in-house HotStuff implementation measured ~ 4 K TPS in practice — well below the original 12,500 TPS design target it claimed. The lesson: lab benchmarks ≠ production. Pyde's performance discipline going forward:
- Multi-region testing mandatory. Localhost devnet numbers do not count.
- Production-realistic workload mix. Not synthetic transfer-only; realistic ratios of transfers / AMM swaps / NFT mints / contract calls.
- Continuous soak. 4-hour minimum for any TPS claim that ships externally.
- One-third rule. External claims are one-third of measured peak.
- Public dashboard. Rolling 30-day metrics, visible.
No TPS claim is published externally without harness evidence. This is non-negotiable, and it is the most important lesson absorbed from the pre-pivot reset.
13. Economics
13.1 Token
- Total genesis supply: 1,000,000,000 PYDE
- Decimals: 9 (1 PYDE = 10⁹ quanta)
- Inflation schedule: 5 % year 1, decreasing to 3 % / 2 % / 1 %, fixed at 1 % thereafter
13.2 Validator Bonds
| Tier | Minimum stake | Role |
|---|---|---|
| Validator | 10,000 PYDE (single tier) | Eligible for uniform-random committee selection; 128 of the pool serve each epoch |
Anti-Sybil cap: max 3 validators per operator (identity-bound). Bonding: 1 epoch before active. Unbonding: 30 days (must exceed the 21-day safety-evidence freshness window). Slashing applies during both bonded and unbonding states — preventing attack-then-exit.
13.3 Fee Model
EIP-1559 base fee with elastic 4 × blocks; no priority tips. Priority would re-introduce the information asymmetry the encrypted mempool eliminates, so it is structurally excluded rather than zeroed by policy. Every transaction pays exactly gas_used × base_fee — wallets quote a single number, not a range.
Each transaction's base fee splits deterministically:
- 70 % burned (deflationary pressure)
- 10 % to treasury (multisig-controlled, PIP-reviewed)
- 20 % to the reward pool, distributed at epoch end:
- 70 % of the pool, activity-weighted across the active committee (vertices certified, batches included, decryption shares submitted, anchor selections)
- 30 % of the pool, flat across all staked validators (active-committee + awaiting-selection), distributed by stake × uptime
13.4 Indicative APY
Per-token yield is uniform across all validators (single tier; rewards distribute by stake × uptime). The activity-weighted committee bonus is layered on top during the ~3-hr epoch a validator is on the active committee. Year-1 yields are high while the validator pool is small and inflation is at the 5 % rate; the rate compresses as the pool grows and inflation tapers to the 1 % terminal floor.
13.5 Net Inflation
Net inflation = mint − burn. At sustained moderate usage (10 K – 30 K TPS plaintext with realistic fee loads), the annual burn exceeds annual mint within a few years; the chain becomes net deflationary. At low usage, slight inflation maintains the validator security budget. At very high usage, deflationary pressure may eventually require parameter adjustment via governance.
14. Governance
Pyde's governance is off-chain. Protocol changes proceed via Pyde Improvement Proposals (PIPs) — public, versioned, ratified by social consensus, modeled on Bitcoin's BIPs and Ethereum's EIPs. Validators upgrade voluntarily; hard forks happen by social agreement; the chain that retains 67 % + stake is the legitimate continuation.
On-chain governance is restricted to two surfaces: treasury spending and emergency operations, both gated by an M-of-N FALCON multisig (7-of-12 recommended) with a 30-day-bounded emergency-pause primitive.
Two-chamber on-chain governance was evaluated and explicitly rejected. Protocol upgrade should require coordinated human decision, not stake-weighted voting that incumbents can capture. The pattern of governance attacks across stake-weighted systems is the empirical case against this design.
15. Slashing
Pyde's slashing magnitudes are industry-aligned, with correlated-offense multipliers and an evidence-submitter reward for safety violations:
| Offense | First instance | Max (correlation / repeat) |
|---|---|---|
| Equivocation | 10 % | 50 % |
| Bad state-root signature | 10 % | 50 % |
| Bad anchor attestation | 5 % | 20 % |
| Invalid vertex | 5 % | 30 % |
| Bad decryption share | 5 % | 30 % |
| DKG failure | 2 % | 10 % |
| Share withholding (per round) | 0.1 % | 5 % / epoch |
| Extended downtime (per round) | 0.05 % | 10 % / epoch |
| Bad batch attestation | 2 % | 5 % |
Coordinated safety offenses apply a 2 × multiplier. Reporter receives 10 % of safety-slash distributions; the remainder is burned. A 24-hour slashing escrow window allows governance to void false positives. Jail escalation runs 24 h → 7 d → permanent on repeat liveness offenses.
16. Comparison to Other L1s
16.1 Comparison Matrix
| Axis | Pyde | Ethereum (L1) | Solana | Aptos | Sui | Polkadot | Cosmos | Avalanche |
|---|---|---|---|---|---|---|---|---|
| Post-quantum signatures (default) | Yes (FALCON-512) | Roadmap | Roadmap | Roadmap | Roadmap | Roadmap | Roadmap | Roadmap |
| Encrypted mempool (default) | Yes (Kyber-768 threshold) | No (PBS auction) | No (Jito auction) | No | No | No | Proposals in IBC track | No |
| Sandwich-attack prevention | Structural | Partial (PBS) | Partial (Jito) | Partial | Partial | N/A (relay-chain) | Partial | Partial |
| Sustained throughput target | 10 K – 30 K v1 (harness-validated) | ~ 15 TPS L1 | High peak; outage history | Lab high | Lab high | Variable (per parachain) | Variable (per zone) | High (per subnet) |
| Hard-finality time | ~ 500 ms (DAG commit) | ~ 12 min | Probabilistic (~ 13 s) | < 1 s | < 1 s | ~ 12 – 60 s | ~ 6 s | ~ 1 s |
| Validator hardware | 8c / 16 GB / 500 GB / 100 Mbps (awaiting committee) | Modest | 12 + cores / 256 + GB | Modest | Modest | Modest (validator tier) | Modest (per zone) | Modest |
| Equal validator voting | Yes (1 = 1) | Stake-weighted | Stake-weighted | Stake-weighted | Stake-weighted | Stake-weighted | Stake-weighted | Stake-weighted |
| Permissionless cross-chain infra layer | Roadmap (parachain spec, PYDE-staked, unified gas) | L2s (per-L2 sequencer); third-party oracles | Third-party (Pyth / Switchboard) | No | No | App-chains via auctions | IBC zone-to-zone (no integrated infra) | Subnet model (sovereign, not infra) |
Each chain in this matrix is competently engineered by serious teams. The differences are choices, not capability gaps. The matrix exists to make the choices visible, not to imply a ranking.
16.2 What Pyde Owes the Field
Pyde does not invent every wheel. The chain stands on a foundation the rest of the industry built — and the strategic claim is not that other chains are wrong, but that the time has come to integrate the field's best ideas into a single greenfield design.
- Bitcoin invented the field. Public chain, hard rules, minimal trust assumptions — the social model everything in this document presupposes.
- Ethereum invented the programmable blockchain and shaped most of the design vocabulary the field still uses: smart contracts, EVM execution semantics, the EIP process, EIP-1559, account-abstraction roadmap, the entire MEV literature. Pyde adopts the EIP-1559 base-fee + elastic-block design (with the priority-tip removal the encrypted mempool enables) and the EIP-style off-chain governance workflow.
- Solana proved at scale that a monolithic-binary L1 with parallel execution can deliver retail throughput, and that consensus and execution sharing one process is operationally viable. Pyde's monolithic architecture, access-list-driven scheduler, and sub-second-finality commitment are the same family of design choices Solana legitimized in production. Solana's stability work — mempool overload mitigations, consensus liveness fixes, gossipsub tuning — is the production reference for what hardening a high-throughput chain looks like.
- Aptos contributed the Jellyfish Merkle Tree that Pyde adopts as its state structure, and Block-STM as one of the field's two leading approaches to the parallelism problem. Pyde's hybrid scheduler folds Block-STM in alongside static access lists.
- Sui introduced the object-centric model as one of the cleanest expressions of ownership encoded in the transaction structure. Pyde's scheduler operates against declared access lists rather than encoded ownership, but Sui's work established that parallelism is a function of transaction format, not just scheduler implementation. Mysticeti — the consensus Pyde adopts post-pivot — was developed by the Mysten Labs team behind Sui.
- Narwhal and Bullshark (Sonnino, Spiegelman, et al.) established the worker / primary split and the DAG-based mempool design Pyde's consensus directly builds on.
- Polkadot pioneered pluggable consensus (BABE / GRANDPA) and parachain architecture as a first-class concept. Pyde's parachain layer applies pluggable-consensus thinking to a different scope — decentralized infrastructure providers, not sovereign app-chains.
- Cosmos built IBC, the most rigorous cross-chain protocol shipped to date, and the principle that cross-chain interaction should be cryptographically verifiable rather than custodially trusted. Pyde's
HardFinalityCert-based bridge primitive sits in IBC's intellectual lineage. - Avalanche demonstrated that subnet-style horizontal scaling is operationally tractable and that Snowman / Avalanche consensus can deliver sub-second finality at production scale.
- Chainlink built the production reference for decentralized oracle networks — the operator-set staking model, deviation-tolerance attestations, off-chain-to-on-chain data bridges. Pyde's parachain layer is Chainlink-style decentralized infrastructure integrated natively into an L1's gas model.
- Filecoin and the libp2p / IPFS ecosystem produced the modular networking stack Pyde uses as transport. Pyde's net-layer crate is integration work over a stack the field built.
- Cardano, Tezos, Algorand, Mina, Aleo, Diem, the Move language team, the entire ZK-rollup research community, Flashbots, the Rust async-runtime ecosystem, NIST, IETF — all shaped the design space. The list is not exhaustive.
Where Pyde diverges — post-quantum-from-genesis, encrypted-mempool-by-default, equal voting, commodity hardware, the permissionless parachain layer with unified gas — is where the bet sits. Every chain in the comparison was built for the era it was built for. Pyde is the only chain in the table that needs no migration to ship all four properties.
17. Open Problems
The design is complete in the senses that matter; the engineering risk is concentrated in a few specific places that this document calls out explicitly rather than hides.
17.1 Threshold Post-Quantum Cryptography
Production-grade threshold variants of Kyber are research-stage. Pyde v1 may ship with a classical-crypto threshold scheme (ElGamal-style over a high-prime field, used only inside the threshold-decryption ceremony) as a transitional measure, migrating to threshold Kyber when audited implementations mature. This is the single largest cryptographic engineering risk in the design. It is being actively researched, not skipped.
17.2 Batch Threshold Decryption
Per-ciphertext threshold decryption scales poorly at very high TPS (~ 50 K – 100 K encrypted TPS ceiling on commodity hardware before per-ceremony cost dominates). Batch decryption schemes — where one threshold ceremony decrypts multiple ciphertexts amortized — are research-stage. Pyde v2 will adopt one once standardization matures; v1 ships the per-ceremony scheme with GPU-acceleration headroom characterized by the performance harness.
17.3 ZK Light Clients
The hybrid-hashing strategy (Poseidon2 on ZK-bearing paths) keeps zero-knowledge proof options open. Post-mainnet, ZK-validated state proofs would enable succinct light clients (kilobytes of proof, full security). Specific SNARK system choice (Plonky3, SP1, Halo2, RISC Zero) is deferred until the consensus rebuild lands and the per-circuit cost can be measured against real protocol structures.
17.4 Programmable Accounts and Session Keys
Native multisig ships at v1. Programmable accounts (sandboxed WASM policy modules expressing spend limits, time locks, allow-listed recipients, tiered authorization, recovery flows) and session keys (epoch-bounded, scope-limited dApp delegation without per-action wallet popups) ship post-mainnet.
A session key is bounded by four parameters: an allow-list of contracts, an optional allow-list of method selectors, a hard spend cap, and an expiry wave. Each authorization checks the FALCON signature, the liveness flags, the scope, and the cumulative spend — all four must pass. Revocation is a single tx signed by the account's main auth_keys and takes effect at the next wave commit. Ethereum is retrofitting the same idea via ERC-4337; Pyde gets it at the protocol layer.
The AuthKeys enum reserves the Programmable variant (tag 0x03) at genesis. The account code_hash + storage_root shape and the multisig signature pipeline are also v1 surfaces that v2 reuses, so contracts written today survive the upgrade without rewriting. The full mechanism is documented in Chapter 11 Session keys (v2) and companion/DESIGN.md.
17.5 Parachain Layer
The protocol-level cross-chain primitives (cross_call!, HardFinalityCert) ship at genesis with mainnet stubs. The full parachain layer — specification, reference implementations, operator economics, bridges to Ethereum / Cosmos / Solana — ships post-mainnet.
18. Path to Mainnet
This document is the technical specification of the post-pivot design. The engineering between specification and mainnet is the work ahead, in execution order:
- Mysticeti DAG implementation. Adapt the open-source Mysticeti reference for FALCON-bound signatures and Pyde's threshold-decryption integration; rebuild the consensus, mempool, and node crates against the new foundation.
- Performance harness build-out. Multi-region production-realistic infrastructure; workload generators for the four target tx-mixes; chaos / failure injection; soak schedule. Pre-mainnet test slate is mandatory before any external TPS claim.
- External audit programme. Multi-track, specialist firms across consensus, the WASM execution layer integration (host-function ABI, fuel-to-gas mapping, deploy-time validator), post-quantum cryptography, networking, and the
otigendeveloper toolchain. Remediate all critical and high findings; re-audit the remediation. The wasmtime runtime itself is a vetted production dependency from the Bytecode Alliance and is not separately audited. - Incentivized testnet. Reference dApps (DEX, lending market, NFT marketplace); fully-funded bug bounty at mainnet-tier scale; multi-month soak; remediate community-found issues before launch.
- 128-validator genesis. Recruit operators with documented hardware benchmarks and incentivized-testnet participation. Geo-distribute across 3 + regions. Coordinate validator DKG for the threshold pubkey. Sign the genesis block. Publish the chain hash.
There is no public schedule. Mainnet ships when the audit programme passes and the incentivized testnet validates the throughput target on production-realistic infrastructure — not before.
19. Conclusion
Pyde represents a chain built around the architectural requirements of the next decade: post-quantum security, MEV resistance, sub-second finality, and commodity-hardware decentralization for users and infrastructure. The pivot from in-house HotStuff to Mysticeti-style DAG consensus reflects an explicit commitment to designing from a clean foundation rather than patching accumulated technical debt.
The design is complete; the implementation is the work ahead. This is not a chain that ships in six months. It is a chain that aims to occupy a category — post-quantum, MEV-free, commodity-validated — that no production chain occupies cleanly today. The strategic window for that occupancy is open and time-bound.
Document version: 0.2
Status: Living document
License: Apache-2.0 — see LICENSE at the repository root
Pyde Validator Lifecycle
Version 0.1
This document specifies the validator state machine, operations, parameters, and anti-Sybil mechanisms.
State Machine
[NOT REGISTERED]
↓ register_validator(stake ≥ MIN_VALIDATOR_STAKE, falcon_pubkey, threshold_key)
↓ (single tier; MIN_VALIDATOR_STAKE = 10,000 PYDE)
[PENDING ACTIVATION] (1 epoch bonding period)
↓ next epoch boundary
[ACTIVE - WAITING] ←──────┐
↓ VRF selects │ epoch ends (not re-selected)
[COMMITTEE - ACTIVE] ──────┘
↓ request_unbond()
[UNBONDING] (30 days)
↓ 30 days elapsed
[WITHDRAWABLE]
↓ withdraw()
[NOT REGISTERED]
Side states (from any active state):
→ [SLASHED] (stake reduced; forced unbond if < min stake)
→ [JAILED] (excluded from committee; unjail required)
Parameters
| Parameter | Value | Notes |
|---|---|---|
MIN_VALIDATOR_STAKE | 10,000 PYDE | Single-tier minimum; any validator meeting this threshold enters the eligible pool for uniform-random committee selection |
MAX_VALIDATORS_PER_OPERATOR (cap) | 3 | Anti-Sybil; enforced on operator identity, not stake |
BONDING_PERIOD | 1 epoch (~3 hours) | Time from registration to active eligibility |
UNBONDING_PERIOD | 30 days | Long enough for safety evidence to surface |
EVIDENCE_FRESHNESS_SAFETY | 21 days | Must be < unbonding period |
EVIDENCE_FRESHNESS_LIVENESS | 1 epoch | Real-time only |
KEY_ROTATION_INTERVAL | Max once per epoch | Prevents rotation abuse |
JAIL_PERIOD_1ST | 24 hours | First jail |
JAIL_PERIOD_2ND | 7 days | Within 30 days of first |
JAIL_3RD | Permanent | 3rd jail = permanent removal |
UNJAIL_FEE | 10 PYDE | Anti-griefing |
SLASHING_ESCROW | 24 hours | Dispute window before slash finalizes |
NEW_VALIDATOR_GRACE_EPOCHS | 1 | 50% reduced slashing in first epoch |
Pseudocode convention. Where this document writes
MIN_STAKEin pseudocode below, it refers toMIN_VALIDATOR_STAKE(10,000 PYDE) — the single-tier minimum.
State Details
[NOT REGISTERED]
Default state. Account is a user wallet, not a validator.
[PENDING ACTIVATION]
Registered with stake, waiting to become eligible.
- Triggered by:
register_validator(stake, falcon_pubkey, threshold_verify_key, operator_identity) - Stake is locked
- Earns nothing during pending
- Auto-transitions to ACTIVE-WAITING at next epoch boundary
[ACTIVE - WAITING]
In the pool, eligible for VRF selection into committee.
- Conditions: stake ≥ MIN_STAKE AND not jailed AND grace period passed
- Earns: flat 30% pool yield (proportional to stake)
- Selected randomly for committee at each epoch boundary
- Cannot be slashed for liveness (no committee duties)
- Can still be slashed for safety (e.g., late-submitted equivocation evidence)
[COMMITTEE - ACTIVE]
Selected for current epoch as one of 128 active members.
- Duties: vertex production, decryption shares, DKG participation, state-root signing
- Earns: activity-weighted share of 70% committee pool + flat 30% pool yield + inflation share
- Subject to full slashing (safety + liveness)
- Loops back to ACTIVE-WAITING at next epoch boundary unless re-selected
[UNBONDING]
Exiting voluntarily.
- Triggered by:
request_unbond() - Stake locked for 30 days
- Cannot be selected for committee
- Cannot earn rewards
- Can still be slashed for offenses within freshness window
- Auto-transitions to WITHDRAWABLE after 30 days
[WITHDRAWABLE]
Stake unlocked, claim available.
- Triggered after 30-day unbonding completes
- User calls
withdraw()to claim remaining stake (after any slashing) - Transitions to NOT REGISTERED
- Frees operator slot for new validator registration
[SLASHED] (Modifier)
- Stake reduced by slash amount
- If remaining stake < MIN_STAKE → forced unbonding
- 24-hour slashing escrow before distribution applied
- See SLASHING.md for full slashing details
[JAILED] (Modifier)
- Excluded from committee at next epoch boundary
- Cannot be selected during jail period
- Stake still locked (not unbonding)
- Requires
unjail()transaction to rejoin pool - Escalates: 24h → 7d → permanent
Operations
Register Validator
#![allow(unused)] fn main() { fn register_validator( stake: u64, falcon_pubkey: FalconPubkey, threshold_verify_key: ThresholdVerifyKey, operator_identity: Address, // anti-Sybil binding ) -> ValidatorId // Preconditions: // - stake >= MIN_STAKE // - operator_identity has < MAX_VALIDATORS_PER_OPERATOR validators // - sender has sufficient balance // // Effects: // - Transfer stake to bonded escrow // - Set state = PENDING_ACTIVATION // - Activation epoch = current_epoch + 1 // - Emit ValidatorRegistered event }
Request Unbond
#![allow(unused)] fn main() { fn request_unbond(validator_id: ValidatorId) -> UnbondingClaim // Preconditions: // - Caller is validator's stake account // - State is ACTIVE-WAITING or COMMITTEE-ACTIVE // - If COMMITTEE-ACTIVE: complete current epoch first // // Effects: // - Set state = UNBONDING // - withdrawable_at = current_time + UNBONDING_PERIOD // - Emit ValidatorUnbonding event }
Withdraw
#![allow(unused)] fn main() { fn withdraw(validator_id: ValidatorId) -> u64 // Preconditions: // - Caller is validator's stake account // - State is WITHDRAWABLE // - No unresolved slashing escrow // // Effects: // - Compute remaining stake (after any slashing) // - Transfer to operator account // - Set state = NOT_REGISTERED // - Free up operator slot // - Emit ValidatorWithdrawn event }
Rotate Keys
#![allow(unused)] fn main() { fn rotate_keys( validator_id: ValidatorId, new_falcon_pubkey: FalconPubkey, new_threshold_verify_key: ThresholdVerifyKey, ) -> Result // Preconditions: // - Caller is validator's stake account // - Last rotation > KEY_ROTATION_INTERVAL ago // - State is ACTIVE-WAITING (not in committee — disruption risk) // // Effects: // - Update pubkeys in account state // - Effective at next epoch boundary // - Old pubkey kept for VERIFY ONLY during 1-epoch grace // - Emit KeyRotated event }
Unjail
#![allow(unused)] fn main() { fn unjail(validator_id: ValidatorId) -> Result // Preconditions: // - State is JAILED // - Time since jail >= jail_period_for_this_offense // - Pays UNJAIL_FEE // - Remaining stake >= MIN_STAKE // - Not 3rd jail (permanent) // // Effects: // - Set state = ACTIVE-WAITING // - Eligible for next committee selection // - Emit ValidatorUnjailed event }
Anti-Sybil: Multiple Validators per Operator
Identity binding via operator_identity field:
- Default: same address as stake account (1:1 binding)
- Optional: multiple validators per operator if registered under same identity
- Cap:
MAX_VALIDATORS_PER_OPERATOR = 3
Why Cap?
- Sybil amplification: without a cap, a rich operator could run dozens of validators under different keys and dominate committee selection
- Cap forces multi-operator diversity — a 43-Byzantine fork requires ≥ 15 distinct KYC'd operator identities
- 3 still allows operational diversity (HA pair + standby, or three-region geographic distribution)
Optional Stronger Anti-Sybil (Post-Mainnet PIP)
Escalating bond for additional validators registered under the same operator identity:
| Validator slot | Required stake |
|---|---|
| 1st | 10,000 PYDE |
| 2nd | 10,000 PYDE |
| 3rd | 20,000 PYDE |
Reduces ROI on heavy concentration. Tracked as post-mainnet hardening; not in scope for v1.
Committee Selection (Each Epoch)
# At end of epoch N, derive committee for epoch N+1:
eligible = [v for v in all_validators if v.stake >= MIN_STAKE
and not v.jailed
and v.grace_period_passed]
for slot in 0..128:
seed = Hash(beacon || slot)
member = uniform_random_pick(eligible, seed)
committee[slot] = member
eligible.remove(member) # without replacement
Selection is uniform random within eligible pool. Stake influences only:
- Probability of being eligible (must meet MIN_STAKE)
- Proportion of flat 30% stake-pool yield
Stake does NOT influence committee selection probability. Equal probability among eligible validators.
Edge Cases
1. Slashed below MIN_STAKE
- Validator forced into UNBONDING state
- 30-day countdown starts
- Cannot be re-selected during unbonding
- After unbonding, can re-register with fresh stake
2. Operator wants more validators
- Register new validator under same
operator_identity - Allowed up to
MAX_VALIDATORS_PER_OPERATOR - Each requires separate
MIN_STAKE
3. Mid-Epoch Hardware Upgrade
- Key rotation requires ACTIVE-WAITING state
- P2P endpoint updates allowed any time (cosmetic)
- For key compromise: emergency rotation allowed any time (with higher fee + audit)
4. Operator Goes Bankrupt / Disappears
- Accumulates downtime slashing over ~3 epochs
- Eventually slashed below MIN_STAKE → forced unbond
- 30-day timer starts
- Stake withdrawable by operator's stake account after 30 days
- No "abandoned validator" cleanup needed; lifecycle handles it
References
- Slashing details: see SLASHING.md
- Committee selection (full algorithm): see WHITEPAPER.md §5.5
- Network protocol (peer addresses): see NETWORK_PROTOCOL.md
Document version: 0.1
License: See repository root
Pyde Slashing Rules
Version 0.1
This document specifies all slashable offenses, detection mechanisms, slash amounts, evidence flow, jail mechanics, and the interaction with the validator lifecycle.
Numbers below are starting points. Final numbers require economic modeling pre-mainnet and may be adjusted via PIP.
Principles
- Safety vs Liveness distinction — different severity, detection, and slash amounts
- Correlated slashing for safety — coordinated attacks lose more
- Permissionless evidence — anyone can submit cryptographic evidence; reporter reward incentivizes monitoring
- Bounded slashing — per-epoch caps prevent stacking attacks
Offense Catalog
Safety Offenses (Severe, Cryptographic Evidence)
| # | Offense | First instance | Max (correlation/repeat) | Jail | Distribution |
|---|---|---|---|---|---|
| 1 | Equivocation (vertex) — two different vertices for same (round, member_id) | 10% | 50% | 1 epoch | 50% burn / 30% treasury / 20% reporter |
| 2 | Bad state-root signature — two contradictory state roots for same commit | 10% | 50% | 1 epoch | Same as above |
| 3 | Bad anchor attestation — vertex's prev_anchor_attestation contradicts 85+ honest majority | 5% | 20% | 1 epoch | Same as above |
| 4 | Invalid vertex structure — parent refs out of order, refs to non-existent batches | 5% | 30% | 1 epoch | 100% burn |
| 5 | Bad decryption share — partial that provably doesn't combine correctly | 5% | 30% | 1 epoch | 50% burn / 30% treasury / 20% reporter |
Liveness Offenses (Auto-Detected, Graduated)
| # | Offense | Per-event | Per-epoch cap | Jail | Distribution |
|---|---|---|---|---|---|
| 6 | DKG participation failure — invalid or missing shares during DKG | 2% | 10% | Until next epoch | 100% burn |
| 7 | Share withholding — no decryption share when expected | 0.1%/round missed | 5%/epoch | After 100 consecutive missed | 100% burn |
| 8 | Extended downtime — no vertices produced for N consecutive rounds | 0.05%/round | 10%/epoch | After 5% reached | 100% burn |
| 9 | Bad batch attestation — worker gossips batch with invalid txs | 2% | 5% | None (warning) | 100% burn |
Future / Deferred
| # | Offense | Status |
|---|---|---|
| 10 | Censorship (provable, off-chain coordination) | v2 (requires cryptographic censorship commitments) |
Correlation Multiplier (Safety Offenses Only)
To punish coordinated attacks and protect isolated failures:
correlation_multiplier = 1 + (other_offenders_this_epoch / max_byzantine)
= 1 + (k / 42) for n=128
Caps at 2× to avoid disproportionate punishment in bug scenarios.
| Other offenders | Multiplier | Effective slash (equivocation 10%) |
|---|---|---|
| 1 | 1.02× | 10.2% |
| 10 | 1.24× | 12.4% |
| 42 (max byzantine) | 2.0× | 20% |
| 43+ | 2.0× (cap) | 20% |
Combined with repeat-offense escalation, a coordinated 43-attack can hit the maximum 50% slash within an epoch.
Slash Math (Percentages of Offender's Stake)
All percentages apply to the offender's current stake at the time of offense. Pyde uses a single staking tier:
- Validators: minimum
MIN_VALIDATOR_STAKE= 10,000 PYDE - Operator-identity cap: 3 validators per operator (anti-Sybil)
- Real-world bonds will be higher than the minimum (rational operators stake more to absorb minor liveness penalties without falling below the floor)
Equivocation (10% × correlation × repeat) — minimum 10K PYDE bond:
1st instance, alone: 1,000 PYDE
1st instance, 42 others: 2,000 PYDE (2× correlation cap)
2nd instance, 42 others: 4,000 PYDE
Capped at 50%: 5,000 PYDE (full burn at the bond floor)
Downtime (0.05%/round) — minimum 10K PYDE bond, when serving on the active committee:
10 rounds missed: 5 PYDE
100 rounds missed: 50 PYDE (5% — also triggers jail)
At 10% epoch cap: 1,000 PYDE
Liveness penalties apply only while a validator is on the active committee for the epoch. Validators awaiting selection have no per-round liveness obligation (they can still be slashed for safety offenses with freshness-window evidence).
Evidence Submission
Permissionless: any node can submit evidence.
#![allow(unused)] fn main() { struct Evidence { offense_type: OffenseType, offender_id: ValidatorId, epoch: u64, proof: CryptographicProof, reporter_id: Option<Address>, // for reward distribution } // Submission as a regular transaction (paid gas) fn submit_evidence(evidence: Evidence) -> Result<()> { // Engine verifies cryptographic proof // If valid: // - Stake slashed from offender (subject to 24h escrow) // - Distribution applied (burn / treasury / reporter) // - Jail status set if applicable // - Event emitted for indexing } }
Evidence Freshness Window
- Safety offenses: 21 days
- Liveness offenses: 1 epoch (real-time only)
- DKG failures: 1 epoch (same as ceremony)
Outside the window: cannot slash. Evidence becomes historical record but no enforcement.
Reporter Cooldown
- Same reporter address: max 5 evidence transactions per epoch
- Limits griefing (malicious reporter spamming invalid evidence)
Jail Mechanics
When a validator is jailed:
- Removed from committee at next epoch boundary
- Cannot rejoin until
unjail()transaction executed - Unjail requirements:
- Time elapsed ≥ jail period
- Pays unjail fee (10 PYDE — anti-griefing)
- Remaining stake ≥ minimum bond for the validator's tier (MIN_VALIDATOR_STAKE = 10K PYDE — single tier)
Escalating Jail Periods
- 1st jail: 24 hours
- 2nd jail within 30 days: 7 days
- 3rd jail: permanent removal (kicked out of validator set)
Slashing Escrow (24-Hour Dispute Window)
To handle false-positive slashes:
Stake state machine:
bonded → slashed_frozen → slashed_finalized
(24h)
During the 24-hour escrow:
- Slashed stake is in "frozen" state (not yet destroyed)
- Governance multisig can void or reduce the slash
- After 24h with no dispute: slash finalizes (distribution applied)
This protects against bugs in slashing logic or contested circumstances (e.g., network partition that fooled detection).
New Validator Grace Period
A validator in their first epoch has 50% reduced slashing on all offenses. Encourages experimentation with new operational setups; bad actors can't hide forever (just one epoch).
Unbonding Interaction
Critical: unbonding must exceed evidence freshness to prevent attack-then-exit.
Unbonding period: 30 days
Safety evidence freshness: 21 days
30 > 21 → prevents attacker withdrawing before evidence is submitted
State machine:
bonded → (request_unbond) → unbonding (30d) → withdrawable
↓
still slashable during unbonding
Slashing applies during BOTH bonded and unbonding states. After withdrawal (past 30 days): cannot slash.
Edge Cases
1. Network Partition
If >43 validators go offline simultaneously due to network split:
- Downtime slashing PAUSES (auto-detected by protocol — committee active count < 85 → liveness mode)
- Resumes once active count ≥ 85
- Prevents punishing the 85+ honest majority while 43+ are partitioned
2. Key Compromise
Validator's key stolen, attacker double-signs:
- Slashing applies (your responsibility as key holder)
- Mitigations: HSM, key rotation, multisig validators (v2)
- No insurance pool (avoid moral hazard)
3. Chain Halt
If chain halts entirely:
- No automatic slashing during halt
- Manual investigation post-recovery
- Specific validators slashed only with cryptographic evidence
4. Hard Fork
If chain hard-forks:
- Slashing state migrates with the chain
- "Wrong-fork" validators on minority chain don't auto-slash (separate chains, separate state)
Sanity Check
At the bond floor, total committee bond: 128 × 10K = 1.28M PYDE. In practice operators stake more to absorb minor penalties without falling below the floor — realistic total committee bond depends on actual operator behavior post-launch.
Max single-event slash at floor (42 offenders × equivocation × 2× correlation):
42 × 10K × 10% × 2.0 = 84K PYDE (= 6.5% of total committee bond at floor)
Max correlated attack across epoch (42 offenders × 5 events × 2× correlation, capped at 50%):
42 × 10K × 50% = 210K PYDE (= 16.4% of total committee bond at floor)
These dollar numbers are intentionally not the load-bearing deterrent. Pyde's security argument (Chapter 16 §16.4) is that threshold encryption removes the attack-profit motive entirely — there is no MEV-extraction revenue to recoup. Stake serves as a credible-commitment deposit against slashable misbehavior plus the input the slashing mechanism has to slash. The operator-identity cap, KYC binding, and slashing-with- finder's-fee do the heavy lifting on Sybil resistance.
Implementation Notes
Slashing is implemented as system transactions handled by the engine:
#![allow(unused)] fn main() { // At evidence submission: pvm.execute_system_tx(SystemTx::SubmitEvidence(evidence)); // At slashing escrow expiry (24h after slash): pvm.execute_system_tx(SystemTx::FinalizeSlash(slash_id)); // At unjail request: pvm.execute_system_tx(SystemTx::Unjail(validator_id)); }
All slashing state is part of validator account state, indexed by validator_id.
References
- Validator lifecycle: see VALIDATOR_LIFECYCLE.md
- Threat catalog (cross-reference): see THREAT_MODEL.md
- Chain halt + recovery: see CHAIN_HALT.md
Document version: 0.1 License: See repository root
Pyde State Sync Protocol
Version 0.1
How new nodes join the network at any point in time. At 30K+ TPS, replaying from genesis is infeasible — snapshot sync is the default.
Sync Modes
| Mode | Use Case | Time |
|---|---|---|
| Full sync (genesis replay) | Archive nodes only | Infeasible at high TPS |
| Snapshot sync (default) | Most full nodes, new committee joiners | ~30-60 min on commodity |
| Light client sync | Mobile wallets, browser, dApp backends | Seconds-minutes |
Snapshot Architecture
Key separation:
- Committee signs state root (cheap, every epoch boundary)
- Volunteers generate chunks (heavier, daily-ish cadence)
This drops committee disk I/O burden. Manifest is small and committee-signed; chunks are large and content-verifiable.
Snapshot Manifest
#![allow(unused)] fn main() { struct SnapshotManifest { epoch: u64, snapshot_state_root_blake3: Hash, snapshot_state_root_poseidon2: Hash, chunk_manifest: Vec<ChunkRef>, current_committee_pubkeys: Vec<FalconPubkey>, // chain-of-trust signatures: Vec<FalconSig>, // ≥85 from prior epoch's committee } struct ChunkRef { chunk_index: u32, chunk_size: u32, chunk_hash: Hash, // Blake3 chunk_path: String, // P2P routing hint } }
Why Dual Roots
- Blake3: fast native verification
- Poseidon2: future ZK light-client compatibility
Both computed at snapshot time, both signed by committee.
Snapshot Cadence
- Committee root signing: every epoch boundary (cheap)
- Chunk publishing: every 8 epochs (~daily) by volunteer infrastructure providers
- Tail sync window: up to 24 hours of txs to catch up
Snapshot Size Projections
| Component | v1 mainnet | 5-year projection |
|---|---|---|
| Account state (~10M accounts × ~150B) | 150 MB – 1.5 GB | 5-10 GB |
| Contract storage (~5× accounts × 64B) | 500 MB – 3 GB | 20 GB |
| Contract code (~50K contracts × 50KB) | ~2.5 GB | 20 GB |
| Total | ~1-3 GB | ~50 GB |
Chunk Format and Merkle Range Proofs
Each snapshot chunk is a self-contained, independently-verifiable bundle of JMT nodes. A chunk's authenticity is proven by walking its nodes' hashes up to the committee-signed state root, using fringe siblings carried in the chunk.
#![allow(unused)] fn main() { struct Chunk { chunk_id: u32, // Contiguous range of jmt_cf entries (internal nodes + leaves) covered by this chunk. nodes: Vec<(NodeKey, NodeContents)>, // The slot_hash → value pairs for leaves in this chunk's range. // (Used to populate state_cf at the new validator.) leaves: Vec<(SlotHash, ValueBytes)>, // Merkle range proof — the sibling hashes along the path from the chunk's // bottom layer up to the global state_root. Needed to verify the chunk // independently of other chunks. fringe_siblings: Vec<(NibblePath, Hash)>, } }
Why fringe siblings
The chunk doesn't contain the entire JMT — that would be every other chunk too. It contains some contiguous portion (e.g., "all nodes whose NibblePath starts with 3a"). To prove that portion is part of the canonical state at the snapshot's version, the chunk must include the sibling hashes along the boundary.
Conceptual example:
Suppose the JMT looks like:
ROOT
/ \
h_3 h_5
/ \ \
... ... leaf at 0x5b22...
A chunk covers leaves under "3a..." prefix. It contains:
- All internal nodes under "3a"
- All leaves under "3a"
- Fringe sibling: h_5 (sibling of h_3 at root level)
- Any other siblings along the path from the "3a" subtree to root
The chunk does NOT include leaves under "5..." prefix; only their hash on the way up.
Verification per chunk
For each chunk received:
1. For each leaf in chunk.leaves:
compute leaf_hash = Hash(slot_hash || value || metadata)
2. Reconstruct internal-node hashes within the chunk's subtree using its
internal-node entries (NodeContents include children's fingerprints).
3. Walk up from the chunk's local root using fringe_siblings at each level:
current_hash = chunk_local_root_hash
for (sibling_path, sibling_hash) in fringe_siblings:
combine_hashes(current_hash, sibling_hash, sibling_path)
4. Final hash MUST equal trusted state_root (from the committee-signed manifest).
5. If yes: chunk is authentic. Write its (NodeKey, NodeContents) pairs into
local jmt_cf, and its (slot_hash, value) pairs into local state_cf.
6. If no: discard. Request the chunk from a different peer (the source was malicious
or corrupted). The bad peer is penalized via peer scoring.
Properties
- Each chunk is independently verifiable. Lose one chunk, request from another peer; no cascading failure.
- The fringe siblings are small (~few hundred bytes per chunk) — they don't materially inflate chunk size.
- The proof is non-interactive — chunk + fringe siblings is enough; no back-and-forth needed.
- Standard cryptographic primitive — Aptos's JMT uses this; Ethereum's MPT has similar range-proof support. Not novel.
Snapshot manifest RPC handler
RPC method: pyde_getSnapshotManifest(wave_id)
→ Returns SnapshotManifest for that wave's snapshot, or NotAvailable.
Behind the scenes:
1. waves_cf.get(wave_id) → WaveCommitRecord → look up jmt version
2. snapshots_cf.get(version) → SnapshotManifest if pre-generated, else None
3. If None: optionally generate on-demand (expensive; archive only)
4. Return manifest
Snapshot generation (background, archive nodes):
- Triggered every N waves (e.g., every epoch)
- Walk jmt_cf at target version, group nodes into ~50MB chunks with key-range partitions
- Compute range proofs (fringe siblings) for each chunk
- Store chunks + manifest in snapshots_cf
- Manifest published with committee threshold sig
Verification Flow
Phase 1: Discover & Verify Manifest
1. Bootstrap from seed peers
2. Discover manifest URLs/hashes from peers
3. Download signed manifest (~5 KB)
4. Verify ≥85 FALCON sigs against trusted committee pubkeys
Phase 2: Download Chunks
5. Discover peers serving snapshot
6. Download chunks in parallel (4 MB each)
7. Verify each chunk_hash against manifest
8. Bad chunks → ban peer, retry from another
Phase 3: Reconstruct State
9. Apply chunks to JMT
10. Compute Blake3 state root locally
11. Compare to manifest.snapshot_state_root_blake3
12. If match: snapshot valid, accept
Phase 4: Recent Sync (Tail)
13. Download blocks from snapshot point to current
14. Replay txs against snapshot state
15. Reach current state, exit sync mode
Phase 5: Active Operation
16. Subscribe to gossip
17. Begin normal participation
Bootstrap from Genesis: Chain-of-Trust
A new node doesn't yet know which committee pubkeys to trust. Solved via genesis chain:
Genesis block: contains committee_0.pubkeys (hardcoded by founders)
↓
Snapshot at epoch 8: signed by committee 0, contains committee_8.pubkeys
↓
Snapshot at epoch 16: signed by committee 8, contains committee_16.pubkeys
↓
... etc forward
New node verifies the chain by:
- Downloading genesis (~5 MB, includes committee_0 pubkeys)
- Downloading intermediate manifests (~5 KB each, hundreds at scale)
- Verifying chain forward: each manifest signed by prior committee
- Accepting current snapshot if chain-of-trust holds
Weak Subjectivity Checkpoints (Optional)
For nodes that don't want full chain-of-trust verification:
- Foundation and reputable infra providers publish "trusted recent checkpoints"
- Signed by their own keys (not committee)
- Assert: "we've verified the chain up to epoch X, root = Y"
- Distributed via known infrastructure (HTTPS, signed websites)
- Updated weekly
New node options:
- Purist: full chain-of-trust from genesis (long but trustless)
- Pragmatist: trust a recent checkpoint, sync from there (fast)
Both produce same security guarantees from the trusted point forward.
Light Client Mode
Doesn't download full state. For mobile wallets, browser dApps, embedded clients.
Storage
- Block headers only (no full blocks)
- Recent committee pubkeys
- Own account state + recent transactions
- JMT proofs for accounts user cares about
Operations
- Verify new block headers via FALCON sigs (~85 verifies, ~6.8ms)
- Query specific accounts: ask full node for
{balance, JMT inclusion proof} - Verify proof against latest signed state root
- Submit transactions: same as regular RPC
Bandwidth
~600 KB/year for typical wallet usage (8 epochs/day × 365 days × ~200 bytes per epoch boundary header).
Incremental Sync (Delta Snapshots)
For nodes with a recent snapshot:
Have: Snapshot at epoch E
Want: Snapshot at epoch E + 8
Delta snapshot:
- Changed accounts since epoch E
- Changed storage slots since E
- New contracts deployed since E
- Signed by committee at E + 8
Apply delta to existing local state → updated snapshot
Saves bandwidth: typical delta is 10-50 MB vs full 3 GB.
Storage / Pruning Policy
| Node type | State retention | Block retention |
|---|---|---|
| Archive node | All historical state | All blocks since genesis |
| Full node (default) | State for last 90 days | Blocks for last 30 days |
| Committee validator | State for last 30 days | Blocks for last 8 epochs |
| Light client | Headers + cared-about accounts | Headers only |
Tunable per-node. Archive nodes earn slightly higher RPC fees for serving historical queries.
Failure Modes & Recovery
| Failure | Detection | Recovery |
|---|---|---|
| All peers serve bad data | Manifest sig fails | Try more peers, ban liars |
| Snapshot corruption mid-download | Chunk hash mismatch | Ban peer, retry chunk from another |
| Manifest signed by wrong committee | Sig verify fails | Reject manifest, find another |
| Network outage during sync | Connection dropped | Resume from last verified chunk |
| Snapshot too old (> evidence window) | Sig set might be slashed | Use newer snapshot |
Time Estimates (commodity hardware, 100 Mbps)
Bootstrap from genesis (small): ~5 seconds
Manifest verification (85 FALCON): ~7 ms
Snapshot download (3 GB at 100 Mbps): ~4 minutes
JMT reconstruction: ~5 minutes
Recent tail sync (8 epochs of txs): ~30 minutes
Total: ~40 minutes
For comparison: Ethereum snap sync 4-24 hours, Cosmos statesync 1-3 hours.
State Growth (v2 Concern)
5-year projection of ~50 GB is optimistic. Solana shows ~80 GB after 4 years despite aggressive engineering.
Future mitigations (defer to v2):
- Account expiration (Aptos pattern): accounts not touched in N years get archived
- Storage rent (Solana pattern): accounts pay rent to stay active
- Stateless validators (Ethereum research): validators use state proofs
References
- Hash strategy: see WHITEPAPER.md §4.3
- Light client (more detail): see WHITEPAPER.md §7
- Network bandwidth: see NETWORK_PROTOCOL.md
Document version: 0.1
License: See repository root
Pyde Chain Halt + Recovery Procedures
Version 0.1
The HotStuff lesson made operational: explicit halt detection → investigation → recovery procedures. No live-patching under pressure.
Three Halt Types
| Type | Trigger | Severity | Authority | Recovery |
|---|---|---|---|---|
| Soft stall | Network/quorum issues | Liveness only | Emergent (any node detects) | Wait (auto-resume) |
| Hard halt | Detected inconsistency (state root divergence, equivocation cluster) | Safety risk | Protocol-detected automatic | Manual investigation |
| Emergency halt | Critical bug, active exploit, hard-fork prep | High intentional | Governance multisig (7-of-12) | Per-incident, max 30 days |
Detection Mechanisms
Soft Stall (Automatic)
- No commit for > 5 rounds (~1s expected, so 5s threshold)
- <85 vertices certified for last K rounds
- Active committee count drops below safety threshold (86)
Response: Validators enter "stall mode" — produce vertices, wait for quorum. Mempool keeps accepting txs (queued). Auto-recover when conditions improve.
Hard Halt (Automatic)
- State root divergence detected (2+ signed contradictory roots for same commit)
- Equivocation cluster (10+ validators in single epoch)
- DKG output mismatch
- Execution layer critical invariant violation
- DAG fork detected (impossible per protocol, indicates bug)
Response: All validators stop producing vertices. All commits halted. Halt event broadcast. Forensic state preserved. Manual intervention required.
Emergency Halt (Manual)
- Critical bug discovered (off-chain, e.g., security researcher)
- Active exploit being mitigated
- Hard-fork coordination needed
- State recovery from previous incident
Response: Governance multisig signs HaltMessage with timestamp + reason. Halt activated for max 30 days (constitutional limit).
What Happens During Halt
| Activity | Soft Stall | Hard Halt | Emergency Halt |
|---|---|---|---|
| Vertex production | Continues (no quorum) | Stops | Stops |
| Commits | Paused | Paused | Paused |
| Tx submission | Accepted, queued | Accepted, queued | Accepted, queued |
| Decryption ceremonies | Paused | Stopped | Stopped |
| DKG ceremonies | Continues unless triggered | Stopped | Stopped |
| State queries | Continue | Continue (forensic) | Continue |
| Slashing evidence acceptance | Continues | Continues | Continues |
| Gossip | Continues | Continues | Continues |
Key invariant: slashing evidence accepted during halt. Attackers cannot escape consequences by triggering a halt.
Investigation Procedure (Hard / Emergency)
Phase 1: Triage (within 1 hour)
- Confirm halt type + trigger
- Identify affected commits / validators
- Snapshot forensic state (preserve)
- Public incident report (initial)
Phase 2: Root Cause Analysis (within 6-24 hours)
- Bug / attack / infrastructure failure?
- Determine scope of impact
- Coordinate with validator operators
- Develop fix or recovery plan
Phase 3: Recovery Plan (within 24-72 hours)
- Propose recovery strategy
- Validate plan with multisig + community
- Coordinate validator updates if needed
- Schedule resume timing
Recovery Procedures (5 Paths)
1. Wait It Out (Soft Stalls)
- Network/validator issues resolve naturally
- 85+ validators come back online
- Quorum forms, commits resume
- No intervention needed
- Typical: <30 minutes; >1 hour escalates
2. Software Update + Replay (Hard Halts from Bugs)
- Identify the deterministic bug causing state divergence
- Patch validator software
- Validators verify they're at consistent state
- Coordinate restart from last verified commit
- Replay txs from mempool
3. Rollback (Controversial, Severe Bugs)
- Roll back to last "clean" commit (max 1 epoch back — 3 hours)
- Discard commits after rollback point
- Re-execute affected txs
- Apply slashing to bad actors
- Limited window prevents catastrophic finality violations
4. Hard Fork (Irreconcilable Issues)
- Manual coordination via governance multisig
- Agreement on canonical state
- All validators update software
- Resume from agreed genesis-of-new-fork state
- Old chain abandoned
5. Emergency Unhalt (False-Positive Halts)
- Investigation reveals no actual issue
- Multisig releases halt
- Resume normally
Rollback Policy
Bounded operational pragmatism:
Maximum rollback window: 1 epoch (~3 hours)
Within window: governance multisig can authorize rollback
Beyond window: only hard fork (community coordination required)
Philosophy: weak finality with a sunset.
- Within 1 epoch: finality is "almost certain but reversible via emergency"
- After 1 epoch: finality is "irreversible without coordinated hard fork"
This is industry standard pattern (Solana de facto, Ethereum has emergency rollback procedures).
State Reconciliation After Rollback
1. All validators agree on rollback target (commit C)
2. Validators roll back state to C
3. Commits after C are discarded
4. Txs in those commits returned to mempool (if still valid)
5. Slashing applied to validators who produced bad-state-root sigs
6. Software updates applied if needed
7. Resume normal operation from C
8. New canonical fork is the post-rollback chain
Specific Scenario Playbooks
Scenario A: State Root Divergence in Commit N
- Detection: 2+ validators signed contradictory roots for commit N
- Action: hard halt automatic
- Investigation: which validators? what tx caused? bug or attack?
- Recovery: identify cause, patch validators, rollback to N-1, resume
- Slashing: validators with wrong root get bad-state-root-sig slash (10%+)
Scenario B: 43+ Committee Offline Simultaneously
- Detection: <85 quorum cannot form
- Action: soft stall
- Investigation: coordinated (attack) or correlated (datacenter outage)?
- Recovery: correlated → wait; coordinated → governance emergency halt to remove
- Slashing: extended downtime + possibly coordination evidence
Scenario C: Critical Bug Discovered (Off-Chain)
- Detection: human report to foundation
- Action: emergency halt via multisig
- Investigation: assess exploit, develop patch
- Recovery: coordinate validator update, resume after patch
- Slashing: none (no on-chain evidence)
Scenario D: DKG Ceremony Failed (Multiple Times)
- Detection: round 4 fails >3 consecutive
- Action: partial halt (encryption disabled for epoch)
- Investigation: which members not contributing? bug or attack?
- Recovery: rotate problematic members + retry DKG, OR continue without encryption
- Slashing: DKG-failure for non-participants
Scenario E: Detected DAG Fork
- Detection: contradictory subdags after commit
- Action: hard halt (this should be impossible per protocol)
- Investigation: deep protocol bug
- Recovery: hard fork to canonical chain, coordinate community
- Slashing: equivocation slashing for forking actors
Communication & Coordination
Halt detected → On-chain "ChainHalted" event emitted
↓
Validator dashboards display halt status
↓
Foundation publishes incident page (initial within 1 hour)
↓
Coordination channels active:
- Discord/Telegram: real-time
- Validator email list: critical comms
- Twitter/X: public status
↓
Resolution proposed
↓
Multisig signs ResumeMessage when ready
↓
On-chain "ChainResumed" event
↓
Public post-mortem within 7 days
Re-Entry After Halt
1. Multisig signals resume (or auto-resume for soft stalls)
2. Validators verify they're at consistent state
3. Mempool processes queued txs (validity re-checked against current state)
4. Commits resume normal cadence
5. Slashing evidence from halt period processed
6. System returns to normal operation
Test Plan / Drills
Mandatory before mainnet:
- Soft stall drills: deliberately offline 43 validators, verify recovery
- Hard halt drills: inject state divergence, verify detection + flow
- Emergency halt drills: practice multisig coordination
- Rollback drills: practice 1-epoch rollback procedure
- Hard fork drills: practice coordinated upgrade
Frequency: quarterly in testnet, annually in mainnet.
Documentation: runbooks for each scenario; updated after every drill.
The HotStuff Lesson Applied
HotStuff broke under wedges/stalls because there was no clear halt → investigate → recover procedure. The team patched live, accumulating safety subtleties.
Pyde's design EXPLICITLY:
- Separates the three halt types
- Defines authority + procedure for each
- Builds drills into the operational plan
This is the lesson learned from the pivot.
References
- Threat model: see THREAT_MODEL.md
- Failure scenarios (operational walk-through): see FAILURE_SCENARIOS.md
- Slashing: see SLASHING.md
Document version: 0.1
License: See repository root
Pyde Threat Model
Version 0.1
This is the canonical threat model for Pyde. It catalogs ~50 threats across 7 layers, maps each to its mitigation in the protocol design, and acknowledges residual risks.
This is a living document. Update on new threats discovered, protocol changes, and quarterly review.
Companion to Chapter 16. Chapter 16: Security is the narrative defense reference — it walks the same ground in essay form, explains why each defense was chosen, and is intended for readers building intuition. This document is the catalog: every threat carries an ID, severity, detection signal, and mitigation reference. External auditors should treat this document as the entry point; bug reporters should reference threat IDs from this catalog.
1. Scope & Assets
In Scope (Protocol Responsibility)
- User funds (PYDE balances + staked amounts)
- State integrity (no fork, no double-spend)
- Transaction ordering integrity (no proposer-MEV)
- Encryption invariants (commit-before-reveal)
- Validator stake (fair slashing)
- Privacy of encrypted transaction contents
- Liveness (chain progress)
- Cross-chain finality (HardFinalityCert correctness)
Out of Scope (User / Operational Responsibility)
- User wallet compromise (private key custody is the user's)
- Smart contract bugs in user-deployed WASM contracts (audit + safety features mitigate, but protocol doesn't enforce)
- RPC provider failures (orthogonal infrastructure)
- Single-node hardware failures (operator responsibility, mitigated by redundancy)
- Social engineering of multisig holders (organizational responsibility)
- Future quantum compute attacks on archived encrypted transactions (no defense possible)
- Application-layer DDoS (dApp choosing weak rate limits)
Asset Value Classification
| Asset | Value | Loss impact |
|---|---|---|
| User funds | Critical | Direct financial loss to users |
| State integrity | Critical | Chain becomes untrustworthy |
| MEV resistance | Critical | Core value proposition |
| Validator stake | High | Slashing must be fair |
| Liveness | High | Chain stops being useful |
| Privacy | High | Encryption promise violated |
| Cross-chain integrity | High | Bridges hacks have caused $3B+ historical losses |
2. Adversary Model
Adversary Types
| Type | Motivation | Resources | Likelihood |
|---|---|---|---|
| MEV bot operator | Profit | Modest infrastructure, deep mempool knowledge | High |
| Economic actor | Profit (large) | Significant capital, can stake | Medium |
| Coordinated cartel | Combined economic gain | Large stake + infrastructure | Medium |
| State adversary | Geopolitical, censorship | Nation-state resources, BGP control | Low but high-impact |
| Insider (validator) | Profit, sabotage | Has stake, share, software access | Low but high-impact |
| Cryptographic adversary | Research or destruction | Mathematician + compute | Low |
| Quantum adversary | Long-term destruction | Future quantum computer | Very low (decade+) |
| Network adversary | Disruption | ISP / BGP position | Low |
| Software supply chain | Various | Dependency access | Medium |
| Social attacker | Various | Social skills | Medium |
Adversary Capabilities
Default network adversary (Dolev-Yao):
- ✅ Observe public messages
- ✅ Delay, reorder, drop, duplicate messages
- ✅ Spoof network packets
- ❌ Cannot forge FALCON signatures
- ❌ Cannot decrypt without ≥85 shares
- ❌ Cannot find hash collisions in Blake3 or Poseidon2
Insider validator (single):
- ✅ Has one FALCON private key
- ✅ Has one threshold decryption share
s_i - ✅ Has validator software access
- ❌ Cannot reconstruct shared SK alone
- ❌ Cannot forge other validators' signatures
- ❌ Cannot violate determinism alone (constrained by protocol rules)
Coordinated insiders (≤42 validators, below BFT threshold):
- ✅ Can collectively decrypt nothing (need 85)
- ✅ Can equivocate (each commits slashable offense)
- ✅ Can collude on transactions (but ordering is deterministic)
- ❌ Cannot violate safety (need 85+ for any commit)
- ❌ Cannot censor (other 86+ can include any transaction)
Coordinated insiders (≥85 validators, above BFT threshold):
- ✅ Can decrypt encrypted transactions
- ✅ Can commit to invalid states (others detect and halt)
- ✅ Can censor
- ✅ Can fork the chain
- This is the "BFT broken" scenario — out of normal protocol scope. Residual risk.
3. Trust Assumptions
Cryptographic
- FALCON-512 is EUF-CMA secure (NIST standard)
- Kyber-768 is IND-CCA2 secure (NIST FIPS 203)
- Blake3 and Poseidon2 are collision-resistant
- DKG produces a valid threshold key under honest majority
- Random beacon is unpredictable until reveal
Network
- Partially synchronous: messages eventually delivered (no permanent partition)
- Clock skew bounded (~5 seconds maximum)
- At least one honest path exists between any two honest nodes
Validator Behavior
- ≥85 of 128 committee members are honest (BFT supermajority)
- Honest nodes follow the protocol; slashing punishes deviation
- Validator software is correctly implemented (defense via formal methods + audits)
Operational
- Genesis ceremony participants are honest
- Hardcoded seed nodes are operated honestly
- DNS infrastructure is reliable
- Foundation multisig members are not compromised (>4 of 7 honest for 7-of-12 threshold)
4. Threat Catalog
Consensus Layer
| ID | Threat | Severity | Detection | Mitigation |
|---|---|---|---|---|
| T-CONS-1 | Equivocation (validator signs contradictory messages) | High | Cryptographic evidence | Equivocation slashing 10-50% |
| T-CONS-2 | Long-range attack (rewrite history) | Medium | State root signatures, finality | Bounded rollback (1 epoch), weak-subjectivity checkpoints |
| T-CONS-3 | Bad state-root signing | High | Contradictory roots for same commit | Bad-state-root slashing 10%, correlation multiplier |
| T-CONS-4 | Anchor predictability exploitation | Medium | Public beacon analysis | Lookback state-root randomness |
| T-CONS-5 | Adaptive corruption (mid-epoch) | Medium | Liveness slashing | Epoch boundary commitment, slashing accumulation |
| T-CONS-6 | Slashing race (withdraw before slash applies) | High | Unbonding period | Unbonding (30d) > evidence freshness (21d) |
| T-CONS-7 | DAG cycle / invalid parent refs | Critical | Structural validation | Auto-reject vertex, slash producer |
| T-CONS-8 | Coordinated proposer attack | High | DAG has no proposer | Structurally impossible |
Cryptographic Layer
| ID | Threat | Severity | Detection | Mitigation |
|---|---|---|---|---|
| T-CRYPT-1 | FALCON key compromise (single validator) | Medium | Anomaly detection | Key rotation, HSM recommended |
| T-CRYPT-2 | Kyber threshold compromise (≥85) | Critical | DKG output | Honest BFT majority assumption; per-epoch refresh |
| T-CRYPT-3 | Hash collision (Blake3 / Poseidon2) | Very low | Cryptanalysis | Standardized primitives, dual hash strategy |
| T-CRYPT-4 | Threshold decryption side-channel | Low | Audit | Constant-time implementation |
| T-CRYPT-5 | DKG manipulation (force bad key) | Medium | DKG validation | Pedersen DKG with public commitments, slashing |
| T-CRYPT-6 | Random beacon bias | Medium | Output analysis | Threshold-sig beacon (no single party controls) |
| T-CRYPT-7 | Future quantum on archived encrypted txs | Long-term | N/A | Out of scope; PQ primitives best available |
MEV / Economic Layer
| ID | Threat | Severity | Detection | Mitigation |
|---|---|---|---|---|
| T-MEV-1 | Front-running via early decryption | High | N/A | Commit-before-reveal invariant enforced |
| T-MEV-2 | Sandwich attacks | High | N/A | Plaintexts hidden until order committed |
| T-MEV-3 | Liquidation racing | Medium | N/A | Mitigated by encryption + commit-before-reveal |
| T-MEV-4 | Time-bandit attacks | High | Finality | Bounded rollback, slashing |
| T-MEV-5 | Validator-builder collusion | Medium | N/A | No proposer-builder separation; DAG eliminates surface |
| T-MEV-6 | Stake concentration → control 43+ committee | High | Public stake state | Anti-Sybil (operator identity cap), stake cap |
| T-MEV-7 | Bribery of committee for ordering | Medium | Behavior analysis | Equal-power voting + slashing makes bribery expensive |
| T-MEV-8 | Censorship (selective exclusion) | High | Detection hard | 127 others can include; censorship requires near-unanimous |
Network Layer
| ID | Threat | Severity | Detection | Mitigation |
|---|---|---|---|---|
| T-NET-1 | Eclipse attack (isolate target) | Medium | Peer diversity analysis | Anti-eclipse: diverse IPs/ASNs, persistent peers |
| T-NET-2 | DDoS on committee validator | High | Traffic analysis | Sentry node pattern, rate limits, peer scoring |
| T-NET-3 | BGP hijack / route manipulation | Low (rare) | Out-of-band | Out of scope (network responsibility) |
| T-NET-4 | Sybil on peer discovery | Medium | IP/ASN concentration | Layered discovery (not DHT), peer score |
| T-NET-5 | Message flooding / spam | Medium | Rate limits | Per-peer rate limiting, gas tank requirement |
| T-NET-6 | Network partition (deliberate or accidental) | Medium | Quorum detection | Partition-aware slashing pause; halt detection |
Economic / Governance Layer
| ID | Threat | Severity | Detection | Mitigation |
|---|---|---|---|---|
| T-ECON-1 | Stake concentration (rich operator, many cheap validators) | High | On-chain analysis | Operator identity binding, max 3 per operator |
| T-ECON-2 | Validator collusion (43+ coordinated offline DoS) | High | Quorum detection | Slashing + partition handling |
| T-ECON-3 | Treasury attacks (governance capture) | Medium | Public proposals | Off-chain governance, transparent PIP process |
| T-ECON-4 | Multisig compromise (emergency halt abuse) | High | Multi-key threshold | 7-of-12 multisig, slashable malicious unhalt |
| T-ECON-5 | Token price collapse → slashing economics broken | Medium | Market data | Numbers tunable, treasury can adjust |
Software / Implementation Layer
| ID | Threat | Severity | Detection | Mitigation |
|---|---|---|---|---|
| T-SW-1 | WASM execution non-determinism bug | Critical | State root divergence | Extensive testing, formal verification, halt detection |
| T-SW-2 | Toolchain binding-generator bug | High | Contract test failures | Per-language generator audits, fuzz testing across all four targets |
| T-SW-3 | FALCON sig side-channel | Low | Timing analysis | Constant-time implementation |
| T-SW-4 | Memory corruption (buffer overflow) | High | Rust borrow checker, audits | Use safe Rust, audit unsafe blocks |
| T-SW-5 | Cryptographic library bug | High | Audits | Use well-audited libraries (RustCrypto) |
| T-SW-6 | State corruption (disk errors) | Medium | Snapshot verification | JMT root recomputation, peer cross-verification |
Authorization Layer (v2 — session keys + programmable accounts)
Session keys ship at v2. The threats below are catalogued now so the v2 implementation lands against a known surface. Until v2, the AuthKeys::Programmable variant is reserved-but-disabled — these threats are inactive at v1.
| ID | Threat | Severity | Detection | Mitigation |
|---|---|---|---|---|
| T-AUTH-1 | Session-key theft (compromised dApp leaks key) | Medium | User notification; on-chain anomaly (unusual spend pattern within scope) | Limited blast radius via scope (contracts + methods + spend cap + expiry); user can revoke instantly with a single signed tx; main auth_keys untouched |
| T-AUTH-2 | Revoked-key replay (attacker submits tx signed by previously-revoked session key) | Low | Authorization-time revoked check | Revocation is on-chain state; tx rejected at validation with KeyRevoked |
| T-AUTH-3 | Scope expansion via mutable storage manipulation | High | Policy WASM audit | Policy WASM runs in restricted-state mode; cannot modify own scope without main-key signature on a RegisterSessionKey/UpdateScope tx |
| T-AUTH-4 | Session-key squatting (creating many keys to flood storage) | Low | Per-account session-key count | Hard limit (32 active session keys per account); spent storage refunded on revocation |
| T-AUTH-5 | spent_so_far overflow attack | Low | u128 arithmetic checks at authorization | Saturating addition + max_spend ≤ u128::MAX / 2 registration check |
| T-AUTH-6 | Expired-key acceptance (clock skew at wave boundary) | Low | Authorization-time expires_at check | Wave is the authoritative clock; no off-chain time source enters the check |
Social Layer
| ID | Threat | Severity | Detection | Mitigation |
|---|---|---|---|---|
| T-SOC-1 | Phishing of operators / multisig | High | Out-of-band | Operator training, HSM, multisig for high-value ops |
| T-SOC-2 | Misinformation during incident | Medium | Multiple channels | Foundation as authoritative source, clear comms protocol |
| T-SOC-3 | Insider threat (developer / foundation) | Medium | Code review, multisig | Multi-sig deployments, public PIP review |
| T-SOC-4 | Supply chain attack on dependencies | High | Cargo.lock audit | Reproducible builds, dependency review |
5. Mitigation Cross-Reference
| Mitigation | Specification |
|---|---|
| BFT 85/128 quorum + DAG consensus | See WHITEPAPER §5 |
| Slashing | See SLASHING.md |
| Threshold encryption + commit-before-reveal | See WHITEPAPER §4, §8 |
| Anti-Sybil (operator identity binding) | See VALIDATOR_LIFECYCLE.md |
| State sync verification (chain-of-trust) | See STATE_SYNC.md |
| Chain halt + recovery procedures | See CHAIN_HALT.md |
| Network defenses (DoS, eclipse) | See NETWORK_PROTOCOL.md |
| Performance harness validates resilience | See PERFORMANCE_HARNESS.md |
| Equal-power committee | See WHITEPAPER §5.5 |
| Honest throughput claims | See WHITEPAPER §11 |
6. Residual Risks (Acknowledged, Not Fully Mitigated)
These are risks Pyde cannot fully eliminate:
-
Coordinated 85+ validator collusion — out of BFT scope. If 85+ collude, safety can be violated. Mitigation: economic disincentives + stake distribution + operator identity cap.
-
Quantum compute breaking PQ primitives in <10 years — not currently feasible to defend; PQ choice is the best available.
-
Smart contract bugs in user-deployed WASM contracts — out of protocol scope. Mitigation: Pyde safety attributes (reentrancy off by default, checked arithmetic) preserved in the WASM era + recommended user audits.
-
Single-validator key compromise — validator loses ≤1 vote of influence. Mitigation: key rotation, HSM, multisig validator (v2 feature).
-
Foundation multisig compromise — 7+ of 12 hostile = emergency halt abuse. Mitigation: diverse multisig members, public visibility, slashable malicious unhalt.
-
Network-level adversary (BGP, ISP) — out of protocol scope. Mitigation: encourage geographic + provider diversity.
-
Genesis trust — initial committee, hardcoded seeds, hardcoded committee pubkeys all require founder trust. Unavoidable at chain launch.
7. Update Procedure
The threat model is a living document:
- Update triggers:
- New threats discovered (research, incidents, audits)
- Protocol changes (new features → new attack surfaces)
- Quarterly review (mandatory)
- Format for new threat entry:
- T-XXX-N: <name> - Severity: <Critical / High / Medium / Low> - Discovered: <date / source> - Detection: <how detected> - Mitigation: <how addressed or "residual risk"> - Reference: <design doc section> - Each major update increments the version number.
8. For Auditors
This document is the entry point for external security review. Auditors should:
- Verify the threat catalog is complete (no missing categories)
- Verify each mitigation is actually implemented (trace to code)
- Verify residual risks are acceptable for the asset values
- Verify trust assumptions are reasonable for production
- Test selected scenarios (especially from FAILURE_SCENARIOS.md)
Document version: 0.1
License: See repository root
Pyde Failure Scenarios
Version 0.1
Operational walk-throughs of failure modes. Complements THREAT_MODEL.md (what attacks exist) with step-by-step recovery procedures.
General Incident Response Timeline
T+0:00 Detection (auto or manual)
T+0:05 On-call notified
T+0:15 Triage call initiated
T+0:30 Initial incident page published
T+1:00 Root cause investigation begins
T+6:00 Recovery plan proposed
T+24:00 Recovery executed (straightforward cases)
T+72:00 Resolution + initial post-mortem
T+7d Full public post-mortem published
T+30d Drill that scenario in testnet
Communication Protocol
- Authoritative source: foundation incident page + Discord #incidents
- Status page: pyde.network/status (always updated)
- Validator coordination: private email list + dedicated Discord channel
- Public: Twitter/X status updates every 30 min during active incident
The 12 Scenarios
Scenario 1: Single Validator Offline (Hardware Failure)
- Trigger: Validator's server crashes (disk, power, etc.)
- Detection: Auto-detected within 2 rounds (no vertex from validator)
- Initial Response: None needed — other 127 continue normally
- Investigation: Operator diagnoses (off-chain)
- Recovery: Operator replaces hardware, runs state sync, resumes
- Time to Recovery: 4-24 hours
- Slashing: Downtime accumulates (~0.05%/round)
- Drill Frequency: Quarterly
Scenario 2: Validator Key Compromise
- Trigger: Operator's key stolen (phishing, server intrusion)
- Detection: Unusual signing patterns OR operator reports
- Initial Response:
- Operator: rotate to new key immediately
- Foundation: investigate scope
- Other validators: monitor for collusion
- Investigation: Forensic analysis, attribution if possible
- Recovery: Key rotation, possibly fresh validator slot if old one slashed
- Time to Recovery: 1-7 days
- Slashing: Whatever the attacker did with the key
- Lessons: HSM strongly recommended; key rotation procedures documented
- Drill Frequency: Annual paper drill
Scenario 3: Network Partition (30% Split for 1 Hour)
- Trigger: BGP routing issue, undersea cable cut, ISP outage
- Detection:
- Active committee count drops below 85 (quorum threshold = 2f+1, f=42)
- Soft stall triggered automatically
- Downtime slashing PAUSES (partition-aware)
- Initial Response:
- Validators in majority partition: keep producing vertices
- Validators in minority: cannot reach quorum, stall
- No coordination needed (automatic handling)
- Investigation: Root cause analysis (network team)
- Recovery:
- Network heals
- Minority validators rejoin gossip
- DAG resynchronizes
- Slashing resumes
- Time to Recovery: Hours (depends on network)
- Slashing: None during partition (partition-aware pause)
- Drill Frequency: Quarterly (simulate in testnet)
Scenario 4: State Root Divergence Detected
- Trigger: Bug in WASM execution layer or non-determinism
- Detection: Auto — 2+ validators sign contradictory state roots for same commit → hard halt
- Initial Response:
- All validators halt
- Forensic state preserved
- Incident page published
- Investigation:
- Identify which validators signed which root
- Determine which root is "correct"
- Identify bug causing divergence
- 6-24 hours
- Recovery:
- Patch the bug
- Validators update software
- Roll back to last consistent commit (within 1-epoch window)
- Resume from rolled-back state
- Slash validators who signed wrong roots
- Time to Recovery: 24-72 hours
- Slashing: Bad-state-root-sig (~10%) to validators on wrong fork
- Lessons: WASM execution determinism testing must improve; add new test cases
- Drill Frequency: Quarterly (inject in testnet)
Scenario 5: DKG Ceremony Fails Repeatedly
- Trigger:
- Several committee members go offline mid-DKG
- DKG round 3 messages don't reach validators
- Bug in DKG implementation
- Detection: DKG round 4 verification fails for >3 consecutive attempts → partial halt (encryption disabled for this epoch)
- Initial Response:
- Identify which members not contributing valid shares
- Decide: retry vs. continue without encryption
- Investigation:
- Per-member: offline, buggy, or malicious?
- Network issues vs. software bug
- Recovery (options):
- A: Retry DKG with backup committee members
- B: Continue without encryption for this epoch
- C: Replace problematic members from the validators-awaiting-selection pool
- Time to Recovery: Same epoch (~3 hours) or next epoch
- Slashing: DKG-failure for non-contributors (~5%)
- Drill Frequency: Annual
Scenario 6: Critical Execution Layer Bug (Off-Chain Disclosure)
- Trigger: Security researcher reports vulnerability via responsible disclosure
- Detection: Email to
security@pyde.network - Initial Response:
- Within 1 hour: foundation reviews + confirms severity
- If critical + active exploit risk: emergency halt via multisig
- If critical + no immediate risk: 24-72 hour disclosure window
- Investigation:
- Reproduce the bug
- Develop patch
- Test patch
- Coordinate validator updates
- Recovery:
- All validators update software simultaneously
- Coordinated restart if needed
- Public disclosure + acknowledgment + bounty payment
- Time to Recovery: 24-72 hours
- Slashing: None (no on-chain offense)
- Lessons: Strong bug bounty program; clear disclosure policy
- Drill Frequency: Annual paper drill
Scenario 7: Active Exploit Being Used
- Trigger: Foundation observes attacker draining funds
- Detection: On-chain monitoring tools, validator reports
- Initial Response: Emergency halt within 15 minutes via multisig
- Investigation:
- Identify exploit mechanism (fast)
- Calculate scope of damage
- Identify attacker addresses if possible
- Recovery:
- Patch the exploit
- Validator update
- Rollback if within 1-epoch window (controversial)
- OR resume without rollback (user funds lost)
- Compensation plan from treasury if available
- Time to Recovery: 24-72 hours
- Slashing: None (off-chain attack)
- Lessons: Better monitoring; multisig response speed critical
- Drill Frequency: Annual simulated
Scenario 8: Foundation Multisig Key Lost / Compromised
- Trigger: Holder loses key (HW failure) OR key stolen
- Detection: Holder reports loss OR unusual multisig activity observed
- Initial Response:
- Lost: holder coordinates with other multisig members for replacement
- Stolen: investigate scope, secure remaining keys
- Investigation: Verify identity of remaining holders; forensic if stolen
- Recovery:
- Replace lost/compromised key via multisig vote
- May need genesis-update if all keys at risk
- Update on-chain multisig configuration
- Time to Recovery: Days to weeks
- Slashing: None (operational)
- Lessons: Diverse holders, geographic distribution, HSM
- Drill Frequency: Annual paper drill
Scenario 9: Major Cloud Provider Outage (AWS us-east-1)
- Trigger: Cloud provider region outage
- Detection: 30-60% of validators in that region go offline
- Initial Response: Validators outside affected region continue if quorum maintained
- Investigation: Identify cause (provider's issue, not Pyde's)
- Recovery:
- Cloud provider recovers
- Validators come back online
- Network catches up
- Slashing PAUSED during partition
- Time to Recovery: Hours (depends on provider)
- Slashing: None (partition-aware)
- Lessons: Validator diversity matters; encourage multi-provider, multi-region
- Drill Frequency: Quarterly multi-region resilience test
Scenario 10: Coordinated 43-Validator Attack
- Trigger: 43 validators coordinate to attack (offline or equivocate)
- Detection: Real-time monitoring shows coordinated behavior
- Initial Response:
- 43 offline: stall (auto), need governance to remove if persistent
- 43 equivocating: massive slashing events
- Investigation: Identify coordinator; collect cryptographic evidence
- Recovery:
- 43 offline: emergency halt + governance removal
- 43 equivocating: slash all 43 (correlation multiplier = 2× → full bond)
- Network resumes with remaining 85+ honest
- Time to Recovery: 24-72 hours
- Slashing: Up to 100% × 43 validators (correlation max)
- Lessons: This is the BFT boundary; design defends but at cost
- Drill Frequency: Annual paper-only (too disruptive for testnet)
Scenario 11: Memory Leak Causing Rolling Restarts
- Trigger: Bug causes validator memory to grow unbounded
- Detection:
- Operator notices RSS growing
- Performance dashboards show abnormal memory
- OOM crashes
- Initial Response:
- Identify affected validators
- Restart affected (each)
- Investigation:
- Heap profiling
- Identify leaked structure
- Patch the bug
- Recovery:
- Software update
- Rolling restart (not simultaneous)
- Time to Recovery: Hours to days
- Slashing: Downtime for extended restarts
- Lessons: Better memory profiling, soak testing
- Drill Frequency: Continuous (every soak test)
Scenario 12: Genesis State Inconsistency Discovered
- Trigger: After mainnet launch, discrepancy found in genesis state
- Detection: Foundation review, validator report
- Initial Response:
- Determine if functional or cosmetic
- If functional: emergency halt
- Investigation:
- Identify cause (founder error, hardcoded discrepancy)
- Calculate impact
- Recovery:
- Cosmetic: file a note, no action
- Functional: hard fork required (re-genesis or state correction)
- Time to Recovery: Days to weeks (hard fork is coordination-heavy)
- Slashing: None (genesis issue)
- Lessons: Genesis review must be thorough; multiple parties verify
- Drill Frequency: Pre-launch paper review only (irreversible post-launch)
Generalized Lessons
| Pattern | Recommendation |
|---|---|
| Multiple validators affected together | Encourage geographic + provider + ISP diversity |
| Operational mistakes | HSM, multisig for critical ops, runbooks |
| Software bugs | Bug bounty, formal verification, extensive testing |
| Network issues | Partition-aware slashing, sentry nodes, diverse routes |
| Time to recovery | Pre-rehearsed drills > improvising under pressure |
Runbook Library Structure
Each scenario should have a written runbook:
runbooks/
├── 01-validator-offline-single.md
├── 02-validator-key-compromise.md
├── 03-network-partition.md
├── 04-state-root-divergence.md
├── 05-dkg-failure.md
├── 06-pvm-bug-disclosed.md
├── 07-active-exploit.md
├── 08-multisig-key-event.md
├── 09-cloud-provider-outage.md
├── 10-coordinated-attack.md
├── 11-memory-leak.md
├── 12-genesis-discrepancy.md
└── README.md (decision tree → which runbook)
Each runbook contains: trigger conditions, detection criteria, step-by-step response (commands to run, calls to make), recovery procedures, escalation paths, communication templates, post-incident checklist.
Drill Schedule
| Drill | Frequency | Format |
|---|---|---|
| Validator restart | Quarterly | Live (testnet) |
| Network partition | Quarterly | Live (testnet) |
| State root divergence | Quarterly | Live (testnet, injection) |
| DKG failure | Annual | Live (testnet) |
| Active exploit | Annual | Simulated |
| Coordinated attack | Annual | Paper only |
| Key compromise | Annual | Paper only |
| Multisig key event | Annual | Paper only |
| Genesis discrepancy | Pre-launch only | Paper review |
| Cloud outage | Quarterly | Live (testnet, region isolation) |
Track every drill: time-to-detect, time-to-respond, time-to-recover. Improve runbooks based on observed gaps.
Integration with Other Documents
- Threat model: see THREAT_MODEL.md for the "what could attack us"
- Chain halt: see CHAIN_HALT.md for halt mechanics
- Performance harness: see PERFORMANCE_HARNESS.md for chaos testing infrastructure
- Slashing: see SLASHING.md for slashing details
Document version: 0.1
License: See repository root
Pyde Network Protocol
Version 0.1
Transport, peer discovery, gossip, message types, DoS protections, and committee defense patterns.
Transport & P2P Library
| Choice | Rationale |
|---|---|
| Transport: QUIC (over UDP) | No HOL blocking, built-in TLS 1.3, mature in Rust (quinn) |
| Fallback: TCP | Compatibility for restrictive networks |
| Library: libp2p (Rust) | Mature, audited, used by Ethereum/Filecoin/Polkadot |
| Node ID: Ed25519 keypair (separate from validator FALCON) | Stable network identity, rotatable without affecting validator status |
Peer Discovery: Layered Bootstrap
Layer 1: Hardcoded seeds (5-10 stable, foundation-operated)
↓
Layer 2: DNS seeds (~10 more peer addresses)
↓
Layer 3: Validator registry (on-chain — committee members publish addresses)
↓
Layer 4: Peer Exchange (PEX) — peers tell each other about peers
↓
Layer 5: Persistent peer set (preserved across restarts)
No DHT
Kademlia DHT (used by IPFS, Filecoin) is for content discovery. Pyde is a chain — peers are limited and known. DHT adds complexity without benefit.
Why layered > DHT for Pyde:
- ✅ Peer identity is on-chain (validator FALCON-bound)
- ✅ Sybil cost is real (
MIN_VALIDATOR_STAKE= 10K PYDE + operator-identity cap of 3 per operator) - ✅ Far simpler (~1K LOC vs ~10K LOC for DHT)
- ✅ Faster discovery (single-hop vs multi-hop)
- ✅ Smaller audit surface
Comparable approaches: Bitcoin, Cosmos, Solana all use layered (no DHT). Ethereum uses both but primarily layered.
Bootstrap Sequence (First Launch)
1. Try hardcoded seeds first (5-10 stable, foundation-operated)
2. Resolve DNS seeds (~10 more peer addresses)
3. Query validator registry on-chain (all staked validators — active committee + awaiting selection)
4. Establish connections to N peers (default N=20)
5. Run PEX to discover more peers
6. Persist successful peers to disk for next startup
Connection Management
| Parameter | Default | Notes |
|---|---|---|
MAX_CONNECTIONS | 200 | Tunable |
MIN_OUTBOUND_CONNECTIONS | 8 | Tunable |
MAX_CONNECTIONS_PER_IP | 5 | Tunable |
MAX_CONNECTIONS_PER_ASN | 50 | Anti-clustering |
INBOUND_CONNECTION_LIMIT | 100 | Tunable |
CONNECTION_TIMEOUT | 10s | Tunable |
HANDSHAKE_TIMEOUT | 5s | Not tunable (security) |
Per-Role Recommendations
- Committee validators: 30-50 active peers (reliability + low-latency)
- Full nodes: 10-20 active peers (default)
- Light clients: 3-5 active peers
Churn Handling
- Lost connection → reconnect with backoff (1s, 5s, 30s, 5min, 30min)
- Persistent failure → demote from "preferred" list
- Misbehaving → ban with TTL (1h, 6h, 24h, permanent)
Message Types & Hard Size Limits
| Type | Priority | Typical | Hard Limit |
|---|---|---|---|
| Ping / Pong | Low | 16B | 64B |
| PeerExchange | Low | 1KB | 8KB |
| VertexAnnouncement | High | 40B | 64B |
| VertexRequest | High | 32B | 64B |
| VertexData | High | 4KB | 64KB |
| BatchAnnouncement | Med | 40B | 64B |
| BatchRequest | Med | 32B | 64B |
| BatchData | Med | 50-200KB | 4MB |
| DecryptionShare | High | 1KB | 2KB |
| StateRootSig | High | 738B | 1KB |
| TxSubmission (plain) | Med | 500B | 8KB |
| TxSubmission (encrypted) | Med | 1.5KB | 8KB |
| ManifestRequest | Low | 32B | 64B |
| ManifestData | Low | 5KB | 64KB |
| ChunkData (state sync) | Low | 4MB | 4MB |
Enforcement Pattern
#![allow(unused)] fn main() { trait Message { const MAX_SIZE: usize; fn validate_size(len: usize) -> Result<()>; } // At parse time: // 1. Read message type tag (1 byte) // 2. Read payload length (4 bytes) // 3. CHECK against max_size BEFORE allocating buffer // 4. If too large: reject + peer score penalty (+5 points) // 5. If OK: read payload, deserialize, process }
Memory safety, DoS resistance, predictability, audit-friendliness all depend on explicit limits.
BatchData Sizing
| Hard Limit | Modest hardware fit | Max TPS support |
|---|---|---|
| 2 MB | Strongest | ~30K |
| 4 MB (chosen) | Strong | ~100K |
| 8 MB | Mixed | ~200K |
| 16 MB | Aspirational | ~500K |
4 MB hard limit balances modest-hardware committee promise (≥500 Mbps NIC sufficient for v1's 10-30K plaintext TPS target, with headroom in the batch size for post-mainnet scaling) with realistic burst scenarios (NFT mints up to ~2000 encrypted txs in one batch). The "Max TPS support" column above is a theoretical ceiling implied by the batch limit; the v1 honest target is much lower (see honest throughput reset).
For batches >4 MB: chunked transfer (BatchAnnouncement → multiple BatchChunk messages of 4 MB each).
Gossip Protocol: Gossipsub
Pyde uses libp2p's Gossipsub for message propagation. Industry standard.
How It Works
- Each node maintains "meshes" per topic (subscribed peers, default 6-8)
- Messages flood through the mesh first
- Lazy push: message IDs (8 bytes) sent more broadly; full message pulled on demand
- Heartbeat every second prunes / repairs mesh
Pyde Topics
| Topic | Subscribers |
|---|---|
pyde/vertices/<epoch> | All committee + full nodes |
pyde/batches/<shard> | All committee workers + RPC nodes |
pyde/decryption_shares/<commit> | All committee |
pyde/state_root_sigs/<commit> | All committee + full + light |
pyde/mempool/plain | All validators + RPC nodes |
pyde/mempool/encrypted | All validators + RPC nodes |
pyde/state_sync/manifests | Sync-mode nodes |
Parameters (Battle-Tested Defaults)
- Mesh size D = 8 (target peers in mesh)
- Fanout = 6 (peers for non-mesh delivery)
- Heartbeat interval = 1s
- Message TTL = 60s
DoS Protections (Multi-Layer)
Layer 1: Connection-Level
- Max connections per IP/ASN (already specified)
- Token bucket per connection
- Slow-loris protection (handshake timeout)
- Reject obviously malformed traffic at OS level (iptables hints to ops)
Layer 2: Message-Level Rate Limits
| Limit | Default | Per |
|---|---|---|
| Vertex announcements | 10/s | Per peer |
| Vertex data requests | 20/s | Per peer |
| Batch announcements | 100/s | Per peer |
| Batch data requests | 50/s | Per peer |
| Tx submissions | 100/s | Per peer (lower for unknown) |
| State sync requests | 10/min | Per peer |
| PEX requests | 1/min | Per peer |
Exceeding rate → drop messages silently. Repeated exceedance → ban.
Layer 3: Peer Scoring
#![allow(unused)] fn main() { struct PeerScore { successful_messages: u64, failed_messages: u64, invalid_messages: u64, avg_latency_ms: u32, bandwidth_used: u64, misbehavior_points: i32, last_misbehavior: Timestamp, } }
Misbehavior point assignments:
- Invalid sig: +10 points
- Malformed message: +5 points
- Duplicate spam: +2 points
- Slow / timeout: +1 point
Thresholds:
- 50 points → throttle (reduce priority, drop low-prio messages)
- 100 points → temp ban (1 hour)
- 200 points → longer ban (24 hours)
- 500 points → permanent ban
Points decay over time (1 point per hour) — rewards good behavior over time.
Layer 4: Application-Level
- Tx submission rate limit per sender address
- Gas tank prepayment (legacy
gas_tankfield) — pay-as-you-go for ingress - Resource caps on processing (CPU, memory per operation)
Bandwidth Prioritization (When Constrained)
Priority queue (top = highest):
1. State root sigs (consensus finality)
2. Vertex broadcasts (consensus structure)
3. Decryption shares (encrypted tx finality)
4. Batch announcements + small data
5. Tx submissions (mempool)
6. State sync chunks (background)
7. PEX, ping/pong (low frequency)
Per-peer bandwidth caps prevent any single peer from monopolizing.
Committee members can configure higher priority for vertex/share traffic.
Sentry Node Pattern (for Committee Validators)
DoS-vulnerable validators (committee members) should NOT expose to the public internet. Standard pattern:
Public Internet
↓
Sentry Node 1, 2, 3 (public-facing)
↓ (private network)
Committee Validator (NOT internet-exposed)
Sentries:
- Run by same operator (or trusted relays)
- Filter incoming traffic
- Forward only valid messages to validator
- Absorb DDoS attacks
Cost: 2-3× infrastructure per validator. Standard practice. Cosmos chains all use this.
Network Identity & Validator Binding
Three layers of identity:
- Network ID (Ed25519): used by libp2p for connection-level identity. Rotatable.
- Validator FALCON pubkey: consensus identity, registered on-chain. Rotatable per epoch.
- Operator stake account: ownership, slashing target. Stable.
Binding: validator's FALCON pubkey is signed by their stake account.
Publishing committee network IDs (in account state) for active epoch enables direct peer connections; mapping cleared after epoch ends to limit DoS targeting outside committee duty.
Anti-Eclipse Protections
Eclipse attack: adversary surrounds a node with malicious peers, controls their view of the network.
Defenses:
- Maintain peers from diverse IPs / ASNs
- Persistent peers (preserve across restarts)
- Random peer rotation (drop oldest every N hours)
- Mandatory connections to "well-known" peers (foundation, reputable infra) — optional
State Sync Network Behavior
State sync chunks are large (4 MB). Special handling:
- Lower priority than consensus traffic
- Dedicated bandwidth budget (e.g., max 20% of available)
- Peers can opt-out of being state sync sources
- Sync nodes maintain separate connection pool for chunk fetching
Connection Diagram
[Light Client]
(3-5 peers)
|
↓ State queries via libp2p
|
[Full Node / RPC]
(10-20 peers)
|
↓ Gossip vertices, batches
|
[Public Sentry Nodes]
(filtering)
|
↓ Filtered traffic only
|
[Committee Validator]
(30-50 peers, private mesh)
References
- Transport details: see WHITEPAPER.md §9
- Performance impact: see PERFORMANCE_HARNESS.md
- Threat model (network threats): see THREAT_MODEL.md §4 Network Layer
Document version: 0.1
License: See repository root
Pyde Performance Harness
Version 0.1
The gate before any external TPS claim. This is testing infrastructure that protects against the HotStuff trap: claimed numbers production cannot reproduce.
Why
Pyde's pre-pivot HotStuff implementation hit ~4K TPS in practice despite claims of higher. The lesson: lab benchmarks ≠ production. Performance harness is what prevents repeat.
All Pyde TPS claims must come from harness output, never from microbenchmarks or local devnet measurements.
Goals
- Reproducibly measure end-to-end performance under realistic conditions
- Detect regressions automatically on code changes
- Validate claims before they're published externally
- Find limits before they bite in production
- Generate audit trail of "this is how we know X is true"
Architecture
pyde-bench/
├── topology/ # Network topology configurations
│ ├── single_region.toml (8-16 validators, same DC)
│ ├── multi_region.toml (3 regions, geographic distribution)
│ └── production_sim.toml (full 128 validators, 3+ regions)
├── workloads/ # Workload generators
│ ├── transfers.rs (simple PYDE transfers)
│ ├── contract_calls.rs (WASM contract interactions)
│ ├── encrypted_swaps.rs (Kyber-encrypted, MEV-sensitive)
│ ├── nft_mint_burst.rs (burst pattern simulation)
│ └── mixed.rs (realistic distribution)
├── metrics/ # Metrics collection + reporting
│ ├── collector.rs (per-validator scraping)
│ ├── prometheus.rs (export to Prometheus/Grafana)
│ └── reporter.rs (markdown/HTML reports)
├── chaos/ # Chaos engineering
│ ├── validator_kill.rs (random validator restarts)
│ ├── network_partition.rs (split-brain testing)
│ ├── slow_peer.rs (latency injection)
│ └── adversarial.rs (bad-actor behaviors)
├── soak/ # Long-duration test runners
└── reports/ # Output formats
Test Topologies
| Topology | Validators | Regions | Use |
|---|---|---|---|
| Local devnet | 4 | 1 (localhost) | Smoke tests, dev iteration |
| Single-region testnet | 16 | 1 (single datacenter) | Component testing |
| Multi-region testnet | 16-32 | 3 (US, EU, APAC) | Realistic perf testing |
| Production-sim | 128 | 4+ (global) | Pre-mainnet validation |
Multi-region requirement is critical. Pre-pivot HotStuff testing was likely localhost or single-DC. Real conditions include:
- 50-200ms RTT between regions
- 1-3% packet loss occasionally
- Bandwidth variation
- Time clock skew
Cloud provider matrix for production-sim:
- AWS (us-east-1, eu-west-1, ap-southeast-1)
- GCP (us-central, europe-west, asia-east)
- Hetzner / Vultr / OVH (cost-optimized)
- Mix providers for cross-provider scenarios
Workload Generators
#![allow(unused)] fn main() { trait Workload { fn generate_tx(&mut self, ctx: &Context) -> Tx; fn target_tps(&self) -> u64; fn distribution(&self) -> &Distribution; } }
Concrete workloads:
- TransferWorkload: simple A→B transfers; baseline
- ContractWorkload: realistic WASM contract interactions
- EncryptedSwapWorkload: ~80% encrypted (worst-case for decryption)
- NFTMintBurstWorkload: 0 → 100K → 0 TPS in 60s
- MixedWorkload: 70% transfers / 15% contracts / 10% encrypted / 5% complex
Workload realism:
- Real FALCON sig generation (not pre-computed)
- Real Kyber encryption (not pre-computed)
- Variable tx sizes (not all minimum)
- Account hot-spotting (some accounts get more traffic — tests parallel execution)
Metrics Collected (Continuous)
TPS Metrics
tps_sustained— average over last 60stps_burst— peak sustained over 10stps_pending— txs in mempool / queued
Latency Metrics (Percentiles p50, p90, p99, p99.9)
tx_submission_to_finality— end-to-endtx_in_batch_latency— submit → in batchbatch_to_vertex_latency— batch → referenced by vertexvertex_to_commit_latency— vertex → commitcommit_to_execution_latency— commit → wasmtime executeddecryption_ceremony_latency— start partial → ≥85 received
Consensus Metrics
round_advance_rate— rounds/sec per validatorvertex_certification_rate— % of vertices that get 85+ certscommit_success_rate— % of rounds where commit firesanchor_selection_success_rate— % of anchors that have valid vertex
Resource Utilization (Per Validator)
cpu_usage_pct— total CPUcpu_per_subsystem— consensus / wasmtime / network / IOmemory_resident_mb/memory_heap_mbdisk_read_iops/disk_write_iops/disk_used_gbnetwork_in_mbps/network_out_mbpsopen_file_descriptors/tcp_connections
State Metrics
jmt_depth_max/jmt_depth_avgstate_root_compute_ms(per commit)state_growth_per_hour_mb
Network Metrics
peer_countpeer_score_distributionmessages_per_second(by type)bandwidth_per_message_typefailed_message_rate
Validator-Specific
slashing_events_per_epochdkg_ceremony_time_msepoch_transition_time_ms
Soak Test Schedule
| Test | Duration | Frequency |
|---|---|---|
| Smoke | 5 min | Every commit (CI) |
| Short soak | 1 hour | Daily |
| Standard soak | 4 hours | Weekly |
| Extended soak | 24 hours | Pre-release |
| Pre-launch soak | 7 days | Before mainnet only |
Pass criteria for soak:
- TPS within 5% of starting value over 4 hours
- p99 latency within 20% of starting value
- Memory growth < 100 MB/hour (excluding state)
- No consensus stalls > 5 seconds
- No new "halt" events (other than scripted chaos)
Chaos Scenarios
#![allow(unused)] fn main() { trait ChaosScenario { fn name(&self) -> &str; fn execute(&self, network: &mut TestNetwork) -> ChaosResult; } }
- ValidatorRestart: random validator restarts every 5 min
- NetworkPartition: split 30% of validators for 5 min
- SlowPeer: inject 500ms latency on some peers
- BadActor: validator equivocates, sends bad sigs, attacks
- BandwidthConstraint: cap one validator at 100 Mbps
- ClockSkew: skew validator clocks by up to 5s
Mandatory Pre-Mainnet Tests
All must pass with publishable evidence before any TPS claim:
| Test | Pass Criteria |
|---|---|
| Steady-state 30K TPS | 4 hours @ 30K, p99 <1s, no stalls |
| Burst 100K TPS | 60s burst absorbed, queue drains in 5 min |
| Validator restart loop | 24h with restarts every 5 min, no stall |
| Network partition | 30% partition for 5 min, both recover, no fork |
| DKG under load | Epoch transition at 30K TPS, no commit stall |
| State sync under load | New node joins at 30K TPS, syncs in <1 hour |
| Slashing under load | Equivocation slashed within 1 epoch |
| 7-day soak | 10K TPS for 7 days, no memory leak, no drift |
| Encrypted tx mix | 30% encrypted at 30K TPS, decrypt latency <500ms |
| Modest hardware | Single committee validator on 1 Gbps, 8c/16GB |
Honest Reporting Discipline
Adopting the "claim 1/3 of measured peak" rule:
- Harness measures: X TPS sustained
- Public claim: X/3 TPS
- Aspirational: X with "production validation pending"
Publication format:
"Pyde sustained 30,000 TPS over a 4-hour test on a 16-validator multi-region testnet (US-East, EU-West, AP-Southeast), with median finality of 480ms and p99 of 950ms. Workload: 70% transfers, 15% contract calls, 10% encrypted, 5% complex. Test methodology and raw data available at
pyde.network/perf/{run-id}."
Specific numbers, methodology referenced, reproducible. NOT "Pyde supports 500K TPS" with no caveats.
Public Dashboard Structure
pyde.network/perf
├── Current Metrics
│ ├── Sustained TPS (last 7 days)
│ ├── p50, p99 latency
│ ├── Validator count + uptime
│ └── Test network conditions
├── Soak History
│ ├── 4h, 24h, 7d soak results
│ ├── Pass/fail per scenario
│ └── Regression trend lines
└── Methodology
├── Test topology
├── Workload composition
├── Hardware specs
└── How to reproduce
Build Effort
| Component | Effort |
|---|---|
| Basic harness skeleton + workload generators | ~2 weeks |
| Multi-region deployment automation | ~1 week |
| Metrics collection + Prometheus integration | ~1 week |
| Chaos testing scenarios | ~2 weeks |
| Long-duration soak runners | ~1 week |
| Reporting + dashboard | ~1 week |
| Total minimum viable harness | ~8 weeks of focused engineering |
In practice, with competing priorities across the rest of the protocol, this sequences across a multi-month window rather than running back-to-back.
Cloud Cost
- 16-validator multi-region testnet: ~$300/month sustained
- Pre-mainnet 128-validator production-sim: ~$2500/month
- Run as needed; don't keep production-sim running continuously
The Key Principle
Build harness BEFORE making any TPS claims externally. The harness IS the evidence. Without it, claims are aspirational. With it, claims are defensible.
This is the HotStuff lesson. Don't skip.
References
- Honest throughput targets: see WHITEPAPER.md §11
- Chaos integration with failure scenarios: see FAILURE_SCENARIOS.md
Document version: 0.1
License: See repository root
Pyde Parachain Design
Version 0.1
This is the canonical design specification for Pyde's parachain framework. Chapter 13 is the narrative overview; this document is the deeper mechanics, the design rationale, and the surface that future PPIPs (Pyde Parachain Improvement Proposals) extend.
1. Scope and framing
A Pyde parachain is an on-chain WASM module with an extended host-function allowlist, a private state subtree, and its own validator committee selected from the main Pyde committee. It is not a slot-auction model (Polkadot-style), not a separate operator network running off-chain, and not a cross-chain bridge to a foreign L1.
The word "parachain" is overloaded in the L1 ecosystem. In Pyde:
| Term | Meaning |
|---|---|
| Smart contract | A WASM module deployed via otigen that shares Pyde's general state space and runs on the main executor. |
| Parachain | A WASM module deployed via otigen with type = "parachain", granted: (a) its own state subtree partitioned under PIP-2 clustering by parachain_id[..16], (b) extended host-function access (cross-parachain messaging, threshold-crypto access, governance hooks), (c) its own validator committee (a subset of Pyde's main committee that opts in at deploy time), and (d) its own upgrade governance. |
| Cross-chain bridge | Infrastructure that ferries proofs between Pyde and a foreign L1 (Ethereum, Bitcoin). Out of scope here — see Chapter 13 §13.2-§13.3, §13.6. |
The parachain framework ships at v1: registration, deployment, lifecycle, upgrade governance, state partitioning, cross-parachain messaging, version history retention, and the host-function ABI surface are all part of mainnet.
2. Why this model
Three design choices distinguish Pyde's parachains from the alternatives:
- No slot auctions. Slot auctions concentrate parachain rights in deep-pocketed operators, creating political and centralization risk. Pyde parachains are deployed by name registration (ENS-style, see §4) with predictable costs.
- Equal-power validator voting. Each registered parachain validator gets one vote on upgrades, NOT stake-weighted (see §7). This is consistent with Pyde's "uniform random + min stake, no stake weighting" committee philosophy and prevents large-stake validators from dominating parachain decisions.
- No maintained per-language SDK. Pyde provides the Host Function ABI specification, a bundling CLI (
otigen), and canonical example projects. Authors compile their own WASM in any wasm32-target language and declare host imports manually. See §11.
3. Architecture overview
A parachain at v1 consists of:
┌─────────────────────────────────────────────────────────────────┐
│ Parachain account (on-chain) │
│ │
│ parachain_id: [u8; 32] (derived from name; see §4) │
│ name: String ("chainlink", "uniswap", etc.) │
│ owner: Address │
│ current_version: u32 │
│ versions: Vec<ParachainVersionRecord> (full history) │
│ state_root: [u8; 32] (subtree root) │
│ config: ParachainConfig │
│ status: Active | Paused | Killed │
└─────────────────────────────────────────────────────────────────┘
│
│ partitions
▼
┌─────────────────────────────────────────────────────────────────┐
│ Parachain state subtree (PIP-2 clustered under jmt_cf) │
│ │
│ slot_hash format: │
│ parachain_id[..16] || Hash(slot_namespace || ...)[..16] │
│ │
│ → entire parachain's state lives in a contiguous JMT subtree │
│ → snapshot, range scan, cross-parachain proof all efficient │
└─────────────────────────────────────────────────────────────────┘
│
│ managed by
▼
┌─────────────────────────────────────────────────────────────────┐
│ Parachain validator committee │
│ │
│ - Subset of Pyde's main 128-validator committee │
│ - Opted in at deploy time (or at upgrade) │
│ - Configurable size: min 7, default 21 │
│ - Equal-power voting (1 validator = 1 vote) │
│ - Per-parachain consensus preset (simple_bft / threshold / opt) │
└─────────────────────────────────────────────────────────────────┘
│
│ executes
▼
┌─────────────────────────────────────────────────────────────────┐
│ Parachain WASM (wasmtime, Cranelift AOT) │
│ │
│ - Imports: only functions from the parachain ABI allowlist │
│ (validated at deploy time) │
│ - Linear memory: 64 MB cap │
│ - Fuel: derived from tx.gas_limit │
│ - Deterministic feature subset (no threads, no SIMD floats, …) │
└─────────────────────────────────────────────────────────────────┘
4. Parachain ID derivation
parachain_id = Poseidon2("pyde-parachain:" || name_bytes)
Names are globally unique, ENS-style. 1-32 chars, single-letter allowed. First-come-first-served at registration with yearly renewal + grace period (see Chapter 11 for the full naming model).
Why prefix the hash with "pyde-parachain:" — to keep the parachain namespace disjoint from the contract namespace and the account namespace. A contract named chainlink and a parachain named chainlink would otherwise collide on Poseidon2(name). The prefix forces them into different parachain_id and contract_address values even when their human-readable names are identical.
Why 32 bytes for the full ID — see [memory: address-naming-collision]. Pyde uses full 32-byte addresses everywhere (no truncation). The first 16 bytes are used by PIP-2 clustering (§5); the full 32 bytes are the canonical identifier in receipts, events, and cross-parachain messages.
Collision risk: with 2^128 possible 16-byte clustering prefixes, the birthday bound is ~2^64 names before a clustering collision becomes likely. Pyde additionally enforces uniqueness at registration time — the on-chain name registry rejects any name whose Poseidon2 hash matches an existing parachain's. PIP-2 collision risk is effectively zero.
5. State partitioning (PIP-2)
All of a parachain's state lives in a contiguous JMT subtree. The slot_hash format:
slot_hash[0..16] = parachain_id[..16] (clustering prefix)
slot_hash[16..32] = Hash(slot_namespace || key)[..16]
Where slot_namespace is the parachain's internal namespace (e.g., "balances", "orders", "config") and key is the slot-specific key bytes.
Benefits inherited from PIP-2:
- Snapshot efficiency. Snapshotting a single parachain is a contiguous JMT subtree walk. No filtering, no global scan.
- Range scan efficiency. RocksDB's clustered key layout means the parachain's data lives in adjacent SST blocks. Hot parachains stay hot in the block cache.
- Per-parachain state-root. The subtree's root hash is naturally available; light clients can verify proofs against per-parachain roots without verifying the global root.
- Cross-parachain proofs. Parachain A can include a JMT inclusion proof from parachain B's state in its own state transitions — the verifier only needs B's subtree root, not B's full state.
The clustering applies recursively: within a parachain's namespace, the slot_namespace prefix further clusters related keys (all balances together, all orders together).
6. Lifecycle
REGISTERING
│
owner submits │ RegisterParachainTx
deploy fee + │ with name + WASM + config
owner deposit │
▼
ACTIVE
/ \
/ \ governance vote
owner / \ to upgrade
pause ▼ ▼
PAUSED → UPGRADING
│ │
│ owner │ new version activates
│ unpause │ at wave N + grace_period
│ │
▼ ▼
ACTIVE ACTIVE (new version)
kill (owner-only, irreversible)
│
▼
KILLED
6.1 Registration
RegisterParachainTx {
name: String // 1-32 chars
initial_wasm: WasmBytes // ≤ 4 MB
config: ParachainConfig
owner: Address
validator_set: Vec<ValidatorPubkey> // opt-in committee members
deploy_fee_paid: u128
owner_deposit: u128
}
Validations at registration:
- Name is well-formed (1-32 chars, alphanumeric + hyphens).
- Name is not already registered (uniqueness check via registry).
- WASM module is well-formed and instantiable under Pyde's deterministic wasmtime config.
- WASM imports only functions in the parachain ABI allowlist (§11).
validator_set⊆ current main committee; size ≥config.min_validators.- Owner has paid the deploy fee + has the owner deposit available.
- Config is internally consistent (e.g., quorum_threshold ≤ validator_set.len()).
On success: parachain_id is derived (§4), the parachain account is initialized with version 0 (the initial WASM), state subtree root is set to empty (Poseidon2 of empty tree), status is Active.
6.2 Upgrade
UpgradeParachainTx {
parachain_id: [u8; 32]
new_wasm: WasmBytes
new_config: ParachainConfig
proposal_id: ProposalId
vote_certs: Vec<FalconSig> // ≥ quorum from §7
threshold_sig: ThresholdSig // parachain committee threshold-signed
}
See §7 for the governance vote that produces these certs. On successful submission:
- The transaction includes the upgrade in the next wave's commit.
- A
ParachainVersionRecordis appended toversionswithactivated_at_wave = current_wave + grace_period(default 100 waves ≈ 50s at 500ms/wave). - The parachain's
current_versionis bumped at the activation wave. - ALL parachain peers + relay nodes simultaneously swap the wasmtime
Moduleinstance. Old instance is discarded, new active. Module is pre-compiled and cached so the swap is sub-millisecond. - First N waves post-activation: nodes verify their local execution matches consensus. Mismatch = halt + alert (indicates corrupted upgrade or compile-time variation).
6.3 Pause / Unpause (owner-only)
Owner can pause the parachain via PauseParachainTx. While paused:
- New transactions targeting the parachain are rejected at ingress.
- Existing in-flight transactions complete normally.
- State is preserved; the subtree continues to exist.
Owner can resume via UnpauseParachainTx. No governance vote needed for pause/unpause — this is operational lifecycle, not a protocol-level decision.
6.4 Kill (owner-only, irreversible)
KillParachainTx marks the parachain Killed. After kill:
- New transactions are rejected.
- The owner deposit is returned to the owner (minus a cleanup fee).
- The parachain's state subtree is retained on-chain for
STATE_RETENTION_WAVES(default ~1 year), then pruned by archive nodes. - The name remains in the registry but cannot be re-registered for
NAME_REUSE_GRACE(default 1 year) to prevent confusion.
6.5 Version history retention — never discarded
#![allow(unused)] fn main() { pub struct ParachainAccount { pub name: String, pub parachain_id: [u8; 32], pub current_version: u32, pub versions: Vec<ParachainVersionRecord>, // FULL HISTORY, ordered pub balance: u128, pub config: ParachainConfig, pub state_root: [u8; 32], pub owner: Address, pub status: ParachainStatus, } pub struct ParachainVersionRecord { pub version: u32, pub wasm_hash: [u8; 32], pub wasm_blob_ref: ContentAddress, pub config_snapshot: ParachainConfig, pub activated_at_wave: WaveId, pub deactivated_at_wave: Option<WaveId>, pub upgrade_proposal_id: ProposalId, pub upgrade_vote_certs: Vec<FalconSig>, pub upgrade_committee_threshold_sig: ThresholdSig, } }
Storage tiering: the last 5 versions store WASM bytes on-chain. Older versions store only wasm_hash + wasm_blob_ref pointing to off-chain content-addressed storage (IPFS-like). Metadata (hashes, configs, signatures) stays on-chain forever. Authors are expected to maintain off-chain mirrors of historical builds; archive nodes also pin them.
Why retain forever: every parachain-touching tx receipt includes (parachain_id, parachain_version, wasm_hash). Wave-commit records include a manifest of parachain versions active during that wave. Replay nodes (during state sync verification, slashing-evidence replay, or historical queries) use these to fetch the exact WASM binary that originally executed each tx. Discarding history would make replay impossible.
7. Governance: equal-power voting
Parachain validators: one validator, one vote
Quorum: configurable per parachain (default 2/3 of validators must vote)
Threshold: 2/3 of voters say YES to pass
This is NOT stake-weighted. Each registered parachain validator gets exactly one vote on upgrade proposals, regardless of their stake size. The rationale (which mirrors Pyde's main-committee philosophy):
- Stake-weighting concentrates governance power in deep-pocketed validators.
- Equal-power voting is consistent with the anti-plutocracy stance baked into committee selection (see WHITEPAPER §5.5).
- Coalitions form on merit and operational reliability, not capital.
The vote flow:
- Proposal submission. Anyone can submit an
UpgradeProposalTxcontaining the new WASM + new config. The proposal enters aPendingstate with a public discussion period (default: 7 days). - Voting window. Each parachain validator can submit a
VoteTxwith{proposal_id, vote: yes|no|abstain, sig: FalconSig}. Voting is open for the configured window (default: 3 days after the discussion period). - Tally. After the voting window closes, vote certs are collected. If quorum (2/3 of validators must vote) is met and threshold (2/3 of voters say YES) is hit, the proposal advances to
Approved. - Threshold ceremony. The parachain's validator committee runs a threshold-signing ceremony over the proposal hash. The output is the
upgrade_committee_threshold_sigthat goes into the version record. - Activation. An
UpgradeParachainTxincludes the vote certs + threshold sig + new WASM + scheduled activation wave. After the grace period, the upgrade activates as described in §6.2.
If quorum is not met or threshold is not hit, the proposal is Rejected and cannot be re-submitted unchanged for PROPOSAL_COOLDOWN (default: 30 days).
8. Capability model (host-function allowlist)
Parachain WASM is sandboxed; host functions are the only escape. Pyde exposes a fixed allowlist:
EXPOSED (parachain ABI):
storage:
parachain_storage_read(key_ptr, key_len, out_ptr, out_len_ptr) -> i32
parachain_storage_write(key_ptr, key_len, val_ptr, val_len) -> i32
parachain_storage_delete(key_ptr, key_len) -> i32
events:
parachain_emit_event(topic_ptr, topic_len, data_ptr, data_len) -> i32
context:
parachain_get_caller(out_ptr) -> i32
parachain_get_block_height() -> u64
parachain_get_wave_id() -> u64
parachain_get_parachain_id(out_ptr) -> i32
cross-parachain messaging (rate-limited):
parachain_send_xparachain_message(target_id_ptr, msg_ptr, msg_len, callback_spec_ptr) -> i32
threshold crypto (optional):
threshold_decrypt(ciphertext_ptr, ciphertext_len, out_ptr, out_len_ptr) -> i32
threshold_encrypt(plaintext_ptr, plaintext_len, out_ptr, out_len_ptr) -> i32
hashing primitives:
hash_keccak256(in_ptr, in_len, out_ptr) -> i32
hash_blake3(in_ptr, in_len, out_ptr) -> i32
hash_poseidon2(in_ptr, in_len, out_ptr) -> i32
explicit gas metering:
consume_gas(units: u64) -> i32
EXPLICITLY FORBIDDEN:
network calls (any kind) — non-deterministic
file/disk access — non-deterministic + capability escape
system clock — non-deterministic; use get_block_height instead
non-deterministic entropy — non-deterministic; use VRF beacon via host fn
direct RocksDB access — must route through parachain_storage_*
WASM threads — non-deterministic by definition
non-deterministic SIMD / float ops — determinism risk
WASI — not allowed (whole interface forbidden)
Deploy-time validation rejects any .wasm whose imports reference functions outside the allowlist. Hard-enforced — there is no opt-out.
9. Cross-parachain messaging
Parachains call each other via parachain_send_xparachain_message. Mechanics:
send_xparachain_message(
target_id: [u8; 32], // target parachain
msg: bytes, // payload (parachain-defined format)
callback_spec: {
callback_fn: String, // function on the calling parachain
max_callback_gas: u64,
timeout_waves: u64, // give up after this many waves
}
) -> XCallId
The flow:
- Send. Calling parachain's WASM invokes the host fn. A
XCallMessageis recorded in the calling parachain's outgoing-queue state. The current wave's commit records the outgoing message. - Threshold sig. The calling parachain's validator committee threshold-signs the outgoing message (deferred to the next wave's vertex piggybacking; one threshold sig per outgoing message).
- Route. Pyde's main consensus relays the message: every wave commit, the engine scans all outgoing-queue diffs and produces
XCallDeliveryTxtransactions targeting the destination parachain. - Verify on receive. The target parachain's validator committee verifies the incoming threshold sig against the source committee's pubkeys (which it knows from the on-chain registry). On verify failure, the message is dropped + logged (no callback fires).
- Execute. On verify success, the target parachain's WASM is invoked with the message payload as input. The target executes, may emit events, may write state.
- Callback. A return value (or timeout) is recorded in the target's outgoing-queue, routed back to the original caller, and that caller's
callback_fnis invoked with the result + the callback context.
Rate limit: each parachain has a configurable budget of outgoing messages per wave (default: 64). Exceeding the budget causes the host fn to trap with XCallRateLimited.
Callback context is preserved across the round-trip:
callback_id unique per call
original_caller address that initiated the original tx
original_fn function that issued the cross-call
original_args_hash hash of original args (full args retrievable from chain log)
issued_at_wave when the call was issued
target_id which parachain was called
This is the same callback context model as cross_call (Chapter 13 §13.4), just specialized for parachain-to-parachain.
10. No-SDK approach
Pyde does not ship a maintained per-language SDK for parachain development. The rationale (locked in 2026-05-21 session):
- A solo-founder's bandwidth cannot maintain language-specific SDKs alongside the core protocol — that is months of work per year per language.
- The WASM ecosystem already has mature toolchains for Rust, AssemblyScript, Go (TinyGo), C/C++, Zig.
- Per-language SDKs create version-skew between SDK and ABI; better to have a single ABI doc that languages adapt to (and that the language-community can wrap on their own time).
- Ethereum's ecosystem has 50+ community Web3 libraries — none "official." Healthy decentralized tooling emerges this way.
What Pyde provides:
- Host Function ABI Specification — a ~10-page document covering names, signatures, memory layout conventions, gas cost table per host function, ABI versioning rules.
otigen parachainCLI:bundle: package.wasm+parachain.tomlinto a deploy artifact.submit: sign and send the deploy tx.upgrade: replace WASM bytes via governance flow.pause/unpause/kill.
- On-chain parachain registry — single source of truth for config + WASM bytes + version history.
- Hardcoded bootstrap nodes — peer discovery; no DHT (see Network Protocol).
- Slashing preset menu — minimal / standard / strict; authors pick at deploy time.
- Canonical example parachains (NOT maintained SDKs — just starter projects authors can copy and modify):
hello-world-parachain(Rust)hello-world-parachain(AssemblyScript)hello-world-parachain(Go/TinyGo)
What authors provide:
- Their compiled
.wasm(any wasm32-target language). - A
parachain.tomlconfig file declaring state schema, consensus preset, slashing preset, allowed host imports. - Manual
extern "C"(or language-equivalent) import declarations for host functions they call.
11. ZK-readiness path baked in
Authors are instructed (in the ABI doc) to use the deterministic WASM subset:
- No floats outside canonical NaN.
- No threads.
- No non-deterministic SIMD.
- No mutable globals (only immutable globals or per-instance memory).
This keeps WASM bytecode amenable to future zk-WASM proving (~2-3 years out per current research trajectory). Authors who comply now will be ZK-ready by default later. Non-deterministic features are already blocked by Pyde's wasmtime config (deploy validator rejects them), so compliance is automatic.
12. Slashing presets
Parachains pick from a three-tier menu at deploy time:
| Preset | Equivocation | Bad state root | Liveness (offline) |
|---|---|---|---|
minimal | 5% | 5% | 0.5%/epoch |
standard | 25% | 10% | 1%/epoch |
strict | 50% | 25% | 2%/epoch |
The preset applies to that parachain's validator committee only — not to those validators' main-committee stake. Main-committee slashing (see SLASHING.md) is separate and additive.
Why a preset menu rather than free parameters: small parachain teams should not have to make slashing-economics decisions. The presets are sane defaults chosen by Pyde's economic model. If a parachain wants custom slashing, they can submit a PPIP to add a new preset; the existing three should cover 95% of use cases.
13. Parachain economics
PYDE is the gas token across the platform. Every parachain operation that touches state, emits events, sends cross-parachain messages, or consumes execution gas is metered in PYDE via wasmtime fuel — exactly the same as smart-contract operations. Authors pay registration fees + owner deposits in PYDE at deploy time. Validators of a parachain earn PYDE rewards via the standard inflation distribution, weighted by their committee membership and uptime.
Parachain authors can layer their own internal token economies on top (e.g., a DEX parachain might mint LP tokens; a DAO parachain might mint governance tokens) — but those are application-layer concerns, not protocol-level mechanics. The protocol charges PYDE; what the parachain charges its users is its own decision.
This keeps the gas accounting simple: one token, one fuel mechanism, uniform across smart contracts and parachains.
14. Failure modes
| Failure | Detection | Recovery |
|---|---|---|
| Parachain WASM enters infinite loop | Fuel exhausted → trap | Tx fails; gas charged; state rolled back |
| Cross-parachain message verify fails | Target committee rejects | Message dropped + logged; no callback fires |
| Cross-parachain message timeout | timeout_waves exceeded | Callback fires with XCallTimeout error |
| Parachain committee falls below quorum | Wave-commit fails for parachain txs | Parachain enters LimpMode; only no-state txs land until quorum restored |
| Bad WASM upgrade (deterministic divergence) | First N post-activation waves see local-vs-consensus mismatch | Hard halt + alert; manual emergency rollback via main governance |
| State subtree corruption | JMT root mismatch on snapshot verification | Cross-verify with peers; re-sync the parachain's subtree from snapshot |
| Name registry race (two parties register same name simultaneously) | Atomic registry check rejects later one | First confirmed at wave-commit wins; later one refunded |
15. v2 directions
Tracked but explicitly deferred to v2 or later:
- ZK-aggregated FALCON signature verification for parachain committees — the path to massively higher throughput. ~95% of the prerequisite work (dual-hash JMT, Poseidon2 state root) is done at v1; the aggregation circuit + verifier is v2 work.
- Adaptive validator-set rotation per parachain — currently the validator set is fixed at deploy and changes via governance. v2 may allow continuous rotation based on uptime / stake.
- Multi-WASM execution within one parachain — currently one parachain = one WASM module. v2 could allow modular parachains with hot-swappable components.
- First-class light-client parachain bootstrap — currently new parachain validators sync the full subtree. v2 could ship per-parachain light-client mode for resource-constrained validators.
16. References
- Narrative overview: Chapter 13
- Account model + naming: Chapter 11
- State model + PIP-2 clustering: Chapter 4
- Execution layer + WASM: Chapter 3
- Slashing: SLASHING.md
- Threat model: THREAT_MODEL.md
- Network protocol: NETWORK_PROTOCOL.md
Document version: 0.1
License: See repository root
Pyde Host Function ABI Specification
Version: v1.0 (draft) Status: Authoritative for v1 mainnet. Subject to revision until mainnet genesis; frozen at v1 launch and only extended in backwards-compatible ways thereafter.
This document is the canonical specification of the Host Function ABI — the surface a WebAssembly contract or parachain uses to interact with the Pyde chain. The execution layer (wasm-exec) is the implementation of this spec. The otigen toolchain validates contracts against this spec at build and deploy time. Independent auditors verify the implementation matches the spec.
If the wasm-exec implementation and this document disagree, this document is authoritative. Implementation bugs are bugs in wasm-exec, not in the spec.
For the conceptual surface and rationale, see Chapter 3 — Execution Layer. For parachain-only extensions, see Chapter 13 — Parachains and companion/PARACHAIN_DESIGN.md.
1. Scope
This spec defines:
- The WASM import module name under which host functions are registered
- The signature of every host function (parameters, returns)
- The semantics of every host function (what it does, what it returns, when it traps)
- The gas cost of every host function (fuel charged per call)
- The error codes returned by every host function
- The memory layout conventions for passing data across the WASM ⇄ host boundary
- The forbidden imports list — functions a deployed module is rejected for importing
- The ABI versioning rules that govern how this spec evolves post-v1
This spec does not define:
- The WASM core instruction set (that is the WebAssembly Core Specification)
- The wasmtime runtime configuration (see Chapter 3 §3.2)
- The toolchain mechanics for declaring host imports in source language (see Chapter 5 — Otigen Toolchain)
- The fuel-to-gas mapping internals (see Chapter 10 §10.1)
2. ABI versioning
2.1 Version field
Every deployed contract declares an ABI version at deploy time. The version is recorded on-chain in the contract's account record. The engine refuses to execute a contract whose declared ABI is newer than the engine's supported ABI.
pyde_abi_version: u32 // semver-packed: high 16 = major, low 16 = minor
Example: 0x0001_0000 = ABI v1.0.
2.2 Compatibility rules
-
Major version bump (v1 → v2) — breaking change. Not permitted post-mainnet. If a future protocol upgrade fundamentally re-shapes the ABI, it ships as v2 alongside v1; the engine supports both forever; old contracts continue to execute under v1 semantics. Major bumps cost the network a hard fork.
-
Minor version bump (v1.0 → v1.1) — backwards-compatible addition. New host functions may be added. Existing function signatures, semantics, gas costs, and error codes are frozen. Old contracts continue to execute without re-deployment.
-
No deprecation, no removal. Once a function is in the ABI, it exists forever at the same signature with the same semantics. This is a one-way ratchet, identical in spirit to Ethereum's opcode discipline.
-
Engine support is monotonic. An engine running ABI v1.7 supports every contract deployed against v1.0 through v1.7. It refuses contracts declaring v1.8 or higher.
2.3 What does not count as a breaking change
- Bug fixes in the engine's implementation that bring observed behavior into compliance with this spec
- Performance improvements that do not change observable semantics
- Changes to internal data layouts that don't affect WASM-visible byte order
- Adding new gas-cost-zero diagnostics (debug logs, traces) under a
#[cfg(debug)]gate
3. WASM import module + calling conventions
3.1 Import module name
All host functions are registered under the WASM module name pyde. A contract imports functions like:
(import "pyde" "sload" (func (param i32 i32) (result i32)))
(import "pyde" "sstore" (func (param i32 i32) (result i32)))
(import "pyde" "emit_event" (func (param i32 i32 i32 i32) (result i32)))
Parachain-only host functions are also registered under pyde; they are gated at deploy time by the validator rejecting them for non-parachain contracts (§9.2).
3.2 Pointer + length convention
Pyde host functions pass data across the WASM ⇄ host boundary using i32 byte-pointers into WASM linear memory plus i32 lengths for variable-length data. The conventions are:
| Pattern | Use |
|---|---|
ptr: i32, len: i32 | Caller-allocated input buffer of known length |
ptr: i32 (no length) | Caller-allocated input buffer of fixed length (e.g., 32-byte hash, 32-byte address, 16-byte u128) |
out_ptr: i32 (no length) | Caller-allocated output buffer of fixed length; host writes exactly that many bytes |
out_ptr: i32, out_len_ptr: i32 | Caller-allocated output buffer + a separate i32 pointer where the host writes the actual length used |
All multi-byte integers are little-endian (matching WASM linear memory's native byte order).
Fixed sizes used by the ABI:
| Type | Size (bytes) |
|---|---|
| Address | 32 |
| Slot hash | 32 |
| Hash output (Blake3, Poseidon2, Keccak256) | 32 |
| u128 (balance, value, amount) | 16 |
| u64 (block height, wave id, chain id, timestamp) | 8 |
| u32 (gas, length, counter) | 4 |
3.3 Return values
Every host function returns an i32 result code:
0— success- Positive non-zero — currently unused; reserved for future warning/info codes
- Negative — error (see §4)
Functions that conceptually return data (e.g., balance()) write the data to a caller-provided output pointer and return the i32 result code. Functions that conceptually return a small scalar (e.g., block_height()) return the scalar directly via WASM's normal return mechanism (e.g., -> i64).
Convention summary:
| Return shape | Function category |
|---|---|
-> i32 (error code only) | Mutating ops without return data (sstore, transfer) |
-> i32 + writes to out_ptr | Returns fixed-size data (sload, caller, balance) |
-> i32 + writes to out_ptr + out_len_ptr | Returns variable-size data (calldata_copy, parachain_storage_read) |
-> i64 | Returns a single u64/i64 scalar (block_height, wave_id) |
(never returns) | Halt operations (return, revert) trap to end execution |
3.4 Memory safety
A host function that receives a pointer + length must validate that the range [ptr, ptr + len) lies entirely within the WASM module's linear memory. Out-of-bounds access traps with MemoryOutOfBounds. This is enforced by the engine; contracts cannot escape the sandbox by passing a malicious pointer.
Maximum linear memory size: 64 MB (hard cap, see Chapter 3 §3.5b). Any read or write past 64 MB traps regardless of pointer value.
3.5 Function attributes
WebAssembly itself has no concept of view/payable/reentrant/etc. — those are chain-level constraints applied at the engine ⇄ WASM boundary. The otigen toolchain reads attributes from otigen.toml and embeds them as a WASM custom section (§3.7) for the engine to consume at runtime.
The attribute set:
| Attribute | Meaning | Enforced by |
|---|---|---|
view | Function must not modify state, transfer value, or emit events | Engine sets view_mode flag on HostState; sstore/sdelete/transfer/emit_event return ERR_FORBIDDEN while flag is set |
payable | Function accepts attached PYDE value (tx.value > 0). Non-payable functions reject value transfers | Engine checks attribute before call; returns ERR_VALUE_TRANSFER_NOT_PAYABLE if value > 0 and attribute absent |
reentrant | Function opts in to being called while already on the call stack. Default is non-reentrant | Engine tracks (contract_addr, fn_name) active set; rejects re-entry of non-reentrant fn with ERR_REENTRANCY_BLOCKED |
sponsored | Gas costs charged to the contract's gas tank instead of the caller | Engine routes gas accounting to contract's tank balance before invocation |
constructor | Callable only at contract deploy time. Subsequent calls are rejected | Deploy validator allows; engine rejects post-deploy with ERR_CONSTRUCTOR_REENTRANT (re-using the reentrancy code is incorrect; treat constructor lockout as a distinct conceptual error category in implementation) |
fallback | Invoked when a call's function selector matches no declared function. At most one per contract. Function signature: (calldata_ptr: i32, calldata_len: i32) -> i32. Default if absent: unmatched selector returns ERR_INVALID_FUNCTION_NAME | Engine dispatches to fallback after selector-table miss |
receive | Invoked on bare PYDE transfers (no selector, value > 0). At most one per contract. Function takes no arguments. Must also be payable (otherwise it would reject the value it's meant to accept). Default if absent: bare value transfers return ERR_VALUE_TRANSFER_NOT_PAYABLE | Engine dispatches to receive on bare-value tx |
entry | Declares the function is callable from outside the contract (top-level tx or cross_call). Required for any function not marked with another dispatch attribute (constructor, fallback, receive). Internal helpers omit this and are not exposed | Deploy validator strips non-entry non-dispatch fns from the public selector table |
Storage: the attribute bitfield is part of the pyde.abi custom section (§3.7), not the WASM bytecode. The same .wasm would behave identically regardless of attributes — the engine wraps every call with attribute-driven pre-checks.
3.5.1 Attribute compatibility rules
Some combinations are nonsensical or unsafe. The build (otigen build) and the deploy validator BOTH check these. Defense in depth: an author might hand-edit the pyde.abi section to bypass the build check, but the deploy validator catches it.
| Combination | Status | Reason |
|---|---|---|
view + payable | ❌ Rejected | View = no state changes; payable = receives value (state change) |
view + constructor | ❌ Rejected | Constructors initialise state; view can't |
view + reentrant | ❌ Rejected | Views are inherently reentrant (they make no state changes there's no guard to opt out of); the attribute is meaningless on a view |
view + sponsored | ❌ Rejected | Views are FREE (§7.8); sponsoring zero gas is meaningless |
view + fallback | ❌ Rejected | Fallback is the catch-all dispatch; restricting it to read-only is a footgun — authors expect to be able to do anything in a fallback |
view + receive | ❌ Rejected | Receive accepts value; view can't accept value |
payable + constructor | ✅ Allowed | Constructors can initialise with funds |
payable + reentrant | ⚠️ Warning, allowed | DAO-attack pattern. Build emits warning; deploy accepts |
payable + fallback | ✅ Allowed | Generic handler that also accepts value |
constructor + reentrant | ❌ Rejected | Constructors are deploy-only; can't be re-entered |
constructor + sponsored | ❌ Rejected | No gas tank exists at deploy time |
constructor + fallback | ❌ Rejected | Distinct call shapes; constructor is deploy-time, fallback is run-time |
constructor + receive | ❌ Rejected | Same; distinct dispatch contexts |
sponsored + reentrant | ⚠️ Warning, allowed | DAO-attack pattern (contract pays gas for its own re-entry) |
fallback + receive | ❌ Rejected | Distinct triggers (selector-miss vs bare-value); can't be the same handler |
receive + payable | ✅ Required | Receive without payable is a no-op contradiction |
receive + reentrant | ❌ Rejected | Recursive receive is meaningless and dangerous |
3.5.2 Per-call dispatch flow
When the engine invokes a function (top-level tx or cross_call):
1. Look up fn_name in cached ContractAbi
if not found:
if FALLBACK fn exists: dispatch to fallback
else if bare value transfer && RECEIVE fn exists: dispatch to receive
else: return ERR_INVALID_FUNCTION_NAME
2. Read attribute bitfield + access list
3. Apply pre-checks (constructor lockout, payable, reentrancy, sponsored,
view-mode flag, access list install)
4. Apply value transfer (if value > 0 and payable)
5. Push per-tx overlay (nested for cross_call)
6. Invoke WASM function body via wasmtime
7. On return: merge or discard overlay; pop call stack; charge gas
The host-side reference implementation of this dispatch wrapper is the subject of §12.6 + §13.
3.6 Module cache
After the engine compiles a contract's WASM (via Cranelift AOT, see Chapter 3), the compiled wasmtime::Module is large in memory (typically ~2–10× the input WASM size) but expensive to re-derive. Pyde caches it.
ModuleCache (in-memory, per node):
Key: contract_address ([u8; 32])
Value: CachedModule {
compiled: wasmtime::Module, // post-AOT
parsed_abi: ContractAbi, // extracted from pyde.abi custom section
last_used: WaveId, // updated on every invocation
size_bytes: usize, // estimated memory footprint
}
Eviction policy:
- LRU by `last_used` wave
- Hard size cap: MODULE_CACHE_MAX_BYTES (default 1 GB; node-configurable)
- TTL: drop entries with last_used < (current_wave - MODULE_CACHE_TTL_WAVES)
Default TTL: 8 epochs ≈ ~1 day on commodity hardware
- On cache miss: fetch raw .wasm from state_cf, compile via Cranelift,
extract pyde.abi custom section, install entry, return
Properties:
- Hot contracts stay resident → near-zero invocation overhead after first call
- Cold contracts evict → bounded memory footprint
- First-call latency for a cold contract: ~50–200 ms (Cranelift AOT pass)
- Subsequent calls within cache window: ~few μs (cache lookup) + actual exec
- Mirrors the dashmap state cache's design pattern: max size + LRU + TTL
This is conceptually identical to the PIP-4 write-back state cache at the state layer: in-memory hot-path, bounded size, transparent eviction. Cold contracts pay one disk-read + one AOT-compile on revival; hot contracts skip both.
3.7 The pyde.abi custom section
Pyde does not store ABI metadata as separate on-chain state. Instead, the .wasm carries its ABI inside a WebAssembly custom section (a standard WASM binary feature) named pyde.abi. The chain stores only the .wasm bytes; the section travels with the code.
Layout:
.wasm file contains:
[WASM header]
[Type section, Import, Function, Memory, Global, Export, Code, ...]
[Custom section: name="pyde.abi", contents=BORSH(ContractAbi)]
The ContractAbi struct (Borsh-encoded):
#![allow(unused)] fn main() { struct ContractAbi { pyde_abi_version: u32, // semver-packed, must match engine's supported contract_type: ContractType, // Contract | Parachain functions: Vec<FunctionAbi>, state_schema_hash: [u8; 32], // Blake3 of the canonical state schema constructor_index: Option<u32>, // index into functions of the constructor, if any fallback_index: Option<u32>, // index into functions of the fallback, if any receive_index: Option<u32>, // index into functions of the receive, if any } struct FunctionAbi { name: String, // matches the exported WASM function name selector: [u8; 4], // first 4 bytes of Blake3(name) — for dispatch attributes: u32, // bitfield (see §3.5) access_list: Vec<AccessListEntry>, // declared slot patterns } bitflags! { struct Attributes: u32 { const VIEW = 1 << 0; const PAYABLE = 1 << 1; const REENTRANT = 1 << 2; const SPONSORED = 1 << 3; const CONSTRUCTOR = 1 << 4; const FALLBACK = 1 << 5; const RECEIVE = 1 << 6; const ENTRY = 1 << 7; } } }
Build-time: otigen build reads otigen.toml, builds this struct, Borsh-encodes it, and uses a WASM custom-section writer (e.g., the wasm-encoder crate) to inject the section into the .wasm file produced by the language compiler. The code section is untouched; only the metadata appendix is added.
Deploy-time: the deploy validator extracts and parses the pyde.abi section and runs a three-layer validation pipeline (the build-time check is best-effort author ergonomics; the deploy-time re-check is the chain-facing defense; the runtime is the definitive guarantee):
- Schema check — version compatibility (
pyde_abi_version≤ engine's max supported), well-formed Borsh decoding, every required field present. - Cross-reference check — every
FunctionAbi.namematches a WASM(export "name" (func ...)); every WASM-exported function (other than internal helpers — TBD how to mark) appears infunctions[*]. No drift between declarations and code. - Attribute compatibility check — every function's
attributesbitfield is a legal combination per §3.5.1. At most oneFALLBACK, at most oneRECEIVE,RECEIVEimpliesPAYABLE, etc. - Static call-graph check (view enforcement) — for each function with the
VIEWattribute, build the call graph from its body. Walk every transitively-reachable function. If any reachable function importspyde::sstore,pyde::sdelete,pyde::transfer,pyde::emit_event,pyde::parachain_storage_write,pyde::parachain_storage_delete, orpyde::parachain_emit_event, REJECT the deploy withDeployRejected: ViewMutatesState(<fn_name>, <mutating_import>). Indirect calls (call_indirect) are conservatively treated as potentially-anything; a view that usescall_indirectis rejected unless every possible target is also statically provable to be view-safe. - Static access-list check (best-effort) — for each function with a declared access list, scan all statically-resolvable
pyde::sload/sstorecall sites; verify the slot pattern matches the declared list. Dynamic slot computation can't be checked statically — runtime enforcement (Layer 3, below) is the actual guarantee.
On any check failure: deploy is rejected with a specific error code identifying the failing step. On success: the entire .wasm (with custom section intact) is stored in state_cf at the contract's code slot.
Runtime (Layer 3 — the definitive guarantee):
The static checks above are best-effort and cannot catch everything (indirect calls, computed slot hashes, transitive-through-table calls). The runtime is the actual enforcement boundary:
- The engine sets
host_state.view_mode = truebefore invoking aVIEWfunction.host_sstore,host_sdelete,host_transfer,host_emit_event, and the parachain mutating variants all check the flag and returnERR_FORBIDDENif set. A view function that tries to mutate state at runtime traps; the calling tx reverts; the chain is protected. - The engine installs the declared
access_listinhost_state.access_listbefore invoking.host_sload/host_sstorecheck membership; reject withERR_ACCESS_LIST_VIOLATIONon miss. - The engine maintains the active call stack and rejects re-entry into non-
reentrantfunctions.
The chain is therefore safe even if a malicious author hand-crafts a .wasm that bypasses the deploy validator's static checks (e.g., via cleverly-constructed call_indirect patterns) — the runtime catches mutations at the point of attempt. The cost of a bypass attempt is paid by the attacker (gas burned up to the trap, tx reverts, no harm done).
Runtime: the engine loads the .wasm into wasmtime, extracts and parses the pyde.abi section once, caches the parsed ContractAbi alongside the compiled module in the ModuleCache (§3.6). All subsequent invocations of the contract read attributes from this in-memory cache. There is no per-call disk read for ABI metadata.
Wallets and indexers: fetch the .wasm via the RPC pyde_getContractCode(addr) method, parse the pyde.abi custom section client-side (SDKs ship a small helper), and have the full ABI without an extra round trip.
One artifact, one source of truth.
4. Error codes
Negative i32 values returned by host functions. Each function lists which codes it can return; this is the master table.
| Code | Symbol | Meaning |
|---|---|---|
-1 | ERR_INVALID_INPUT | Malformed input bytes (e.g., non-32-byte hash, non-canonical encoding) |
-2 | ERR_NOT_FOUND | Reserved. Storage reads return zero values on missing slots (see sload, balance, parachain_storage_read). Currently only used as a sub-call failure indicator in some cross_call paths. Do not introduce new uses without ABI council review. |
-3 | ERR_INSUFFICIENT_BALANCE | Caller balance too low for the requested operation |
-4 | ERR_OUT_OF_GAS | Gas budget exhausted (typically a trap, but returned here for consume_gas) |
-5 | ERR_FORBIDDEN | Operation not permitted in this context (e.g., sstore from a view function) |
-6 | ERR_ACCESS_LIST_VIOLATION | Accessed slot not in declared access list |
-7 | ERR_OUTPUT_BUFFER_TOO_SMALL | Caller's output buffer was smaller than required |
-8 | ERR_INVALID_ADDRESS | Address format invalid (e.g., 32-byte all-zero, reserved sentinel) |
-9 | ERR_REENTRANCY_BLOCKED | Cross-call would re-enter a non-reentrant function |
-10 | ERR_CROSS_CALL_FAILED | Sub-call trapped or returned non-zero error code |
-11 | ERR_CROSS_CALL_OUT_OF_GAS | Sub-call exhausted forwarded gas |
-12 | ERR_VALUE_TRANSFER_NOT_PAYABLE | Attempted transfer to a function not marked payable |
-13 | ERR_INVALID_FUNCTION_NAME | cross_call target function does not exist |
-14 | ERR_XCALL_RATE_LIMITED | Parachain cross-message budget exceeded for this wave (parachain only) |
-15 | ERR_PARACHAIN_ONLY | Function callable only from parachain context |
-16 | ERR_CIPHERTEXT_INVALID | Threshold-decryption input malformed |
-17 | ERR_SIGNATURE_INVALID | FALCON signature verification failed |
-100 | ERR_INTERNAL | Engine-side bug or unexpected state. Should never occur in a correct implementation; surfaces as a trap in practice. Document for completeness. |
Critical failures (MemoryOutOfBounds, StackOverflow, OutOfFuel, IntegerDivideByZero, UnreachableCodeReached, host-fn-invariant violations) trap. Traps are unrecoverable; the transaction reverts; gas is consumed up to the trap point.
5. Gas metering
Every host function call consumes a fixed base gas cost plus, for variable-length inputs, a per-byte cost. Gas is charged before the host function's work begins. If charging would exceed the contract's remaining gas, the host function traps with OutOfFuel and does not execute.
Gas costs are listed inline with each function and are summarized in the Gas Table at §10. Values in this spec are canonical; the engine's crates/wasm-exec/src/gas_table.rs is the implementation of this table.
The fuel-to-gas mapping is documented in Chapter 10 §10.1. For purposes of this spec, gas = fuel (1:1 at the wasmtime boundary).
5.1 No refunds
Per Chapter 10 §10.1, Pyde v1 has zero gas refunds. sdelete is cheaper than sstore but does not refund. No host function returns gas to the caller.
5.2 Dynamic gas via consume_gas
Contracts that perform off-fuel work (e.g., synchronous loops bounded by external data) can charge gas explicitly via consume_gas(amount). This is metered identically to host-function gas.
6. Determinism rules
A correct Pyde host function call must produce bit-identical results on every honest validator. The following are forbidden in host function implementations:
- Wall-clock time (
std::time::Instant::now(),SystemTime::now()) - Floating-point operations outside the WASM canonical NaN regime
- Non-deterministic RNG (use
beacon_getfor chain-derived randomness) - File system access
- Network calls
- Threading or any concurrency primitive observable to the contract
- Memory allocation patterns that depend on system state (engine uses a fixed-size arena per call)
Host functions that appear to depend on time (block_timestamp) actually return chain-state-derived values that are deterministic across validators. Same for beacon_get.
The wasmtime configuration (see Chapter 3 §3.2) enforces WASM-side determinism (canonical NaN, no threads, no SIMD, no relaxed-SIMD, no bulk-memory non-determinism, no GC). Host-side determinism is the spec's contract; implementations that violate it are bugs.
7. Core host functions
All functions below are available to every deployed module (contracts + parachains). The pyde:: prefix in WAT examples corresponds to the (import "pyde" "<name>" ...) form.
7.1 Storage
sload
pyde::sload(slot_ptr: i32, value_out_ptr: i32) -> i32
slot_ptr — pointer to 32-byte slot hash
value_out_ptr — pointer to 32-byte buffer where the value is written
Returns: 0 on success,
ERR_ACCESS_LIST_VIOLATION if slot is outside the declared access list.
Gas: 200 base. (Cache-warm reads from the dashmap layer cost the same gas;
gas is paid against the worst-case disk-fetch cost.)
Semantics: a slot that was never written (or was sdeleted) reads back as 32 zero
bytes — NOT an error. This matches EVM's storage model: empty and zero are
indistinguishable, contracts that need to track "set vs unset" must use a separate
flag slot. The only failure modes are gas exhaustion (traps) and access-list
violation (returns the error code).
sstore
pyde::sstore(slot_ptr: i32, value_ptr: i32) -> i32
slot_ptr — pointer to 32-byte slot hash
value_ptr — pointer to 32-byte value to write
Returns: 0 on success, ERR_FORBIDDEN if called from a view function,
ERR_ACCESS_LIST_VIOLATION if slot is outside the declared access list.
Gas: 5,000 base. (Same cost for new and overwrite; no cold/warm distinction in v1.)
sdelete
pyde::sdelete(slot_ptr: i32) -> i32
slot_ptr — pointer to 32-byte slot hash
Returns: 0 on success (even if slot did not exist), ERR_FORBIDDEN if called from a
view function, ERR_ACCESS_LIST_VIOLATION if slot is outside the access list.
Gas: 150 base. (Cheaper than sstore — clearing a slot is less work than writing it.
No refund applied; the user pays gas_used regardless.)
7.2 Account & balance
balance
pyde::balance(addr_ptr: i32, balance_out_ptr: i32) -> i32
addr_ptr — pointer to 32-byte address
balance_out_ptr — pointer to 16-byte buffer where the u128 balance is written (LE)
Returns: 0 on success, ERR_INVALID_ADDRESS if address malformed.
Gas: 100 base.
Semantics: an address that has never been funded reads back as balance = 0 — NOT
an error. Querying a non-existent account is a normal operation. ERR_INVALID_ADDRESS
fires only for structurally-bad addresses (e.g., reserved sentinel values).
transfer
pyde::transfer(to_ptr: i32, amount_ptr: i32) -> i32
to_ptr — pointer to 32-byte recipient address
amount_ptr — pointer to 16-byte u128 amount (LE)
Returns: 0 on success, ERR_INSUFFICIENT_BALANCE if caller balance < amount,
ERR_INVALID_ADDRESS if recipient malformed,
ERR_FORBIDDEN if called from a view function.
Gas: 7,000 base.
7.3 Execution context
All context functions return chain-state-derived values that are bit-identical across validators.
caller
pyde::caller(addr_out_ptr: i32) -> i32
addr_out_ptr — pointer to 32-byte buffer
Returns: 0 always (caller always exists).
Gas: 5 base.
Semantics: returns the immediate caller's address. For top-level transactions,
caller == origin == the externally-owned account that signed the tx.
For nested cross-calls, caller is the contract that issued the cross_call.
origin
pyde::origin(addr_out_ptr: i32) -> i32
addr_out_ptr — pointer to 32-byte buffer
Returns: 0 always.
Gas: 5 base.
Semantics: returns the externally-owned account that signed the original transaction,
regardless of cross-call nesting depth. Deliberately distinct from caller() to avoid
the tx.origin phishing footgun from Ethereum (origin should rarely be checked for
authorization).
self_address
pyde::self_address(addr_out_ptr: i32) -> i32
addr_out_ptr — pointer to 32-byte buffer
Returns: 0 always.
Gas: 5 base.
Semantics: returns the address of the currently-executing contract or parachain.
block_height
pyde::block_height() -> i64
Returns: the current block height as a u64 (Pyde collapses "block" and "wave" —
this is the same value as wave_id()).
Gas: 2 base.
wave_id
pyde::wave_id() -> i64
Returns: the current wave id as a u64. Identical to block_height() in v1.
Gas: 2 base.
block_timestamp
pyde::block_timestamp() -> i64
Returns: the canonical timestamp of the wave being committed, in seconds since Unix epoch.
This value is committee-attested and identical across all validators.
Gas: 2 base.
chain_id
pyde::chain_id() -> i64
Returns: the chain identifier (1 = mainnet, 31337 = devnet, others TBD).
Gas: 2 base.
7.4 Transaction context
tx_hash
pyde::tx_hash(hash_out_ptr: i32) -> i32
hash_out_ptr — pointer to 32-byte buffer
Returns: 0 always; writes the current transaction's Blake3 hash.
Gas: 5 base.
tx_value
pyde::tx_value(value_out_ptr: i32) -> i32
value_out_ptr — pointer to 16-byte buffer (u128, LE)
Returns: 0 always; writes the PYDE value attached to the current call.
For non-payable functions this is always zero; for payable functions, it is the
amount passed in by the caller (top-level tx.value or cross_call's value argument).
Gas: 5 base.
tx_gas_remaining
pyde::tx_gas_remaining() -> i64
Returns: remaining gas (fuel) in the current call frame.
Gas: 2 base.
calldata_size
pyde::calldata_size() -> i32
Returns: total length in bytes of the calldata buffer for the current invocation.
Gas: 2 base.
calldata_copy
pyde::calldata_copy(offset: i32, len: i32, out_ptr: i32) -> i32
offset — byte offset into the calldata buffer
len — number of bytes to copy
out_ptr — pointer to len-sized buffer
Returns: 0 on success, ERR_INVALID_INPUT if (offset + len) exceeds calldata_size().
Gas: 8 base + 1 per byte copied.
7.5 Events
emit_event
pyde::emit_event(
topics_ptr: i32, — pointer to (topics_count × 32) bytes of topic data
topics_count: i32, — number of topics; must be 1 ≤ topics_count ≤ 4
data_ptr: i32,
data_len: i32,
) -> i32
topics_ptr — pointer to topics_count consecutive 32-byte topic values
topics_count — 1 to 4 inclusive; topic[0] is conventionally Blake3(signature)
data_ptr, len — variable-length non-indexed event payload
Returns: 0 on success,
ERR_FORBIDDEN if called from a view function,
ERR_INVALID_INPUT if topics_count < 1 or topics_count > 4,
ERR_INVALID_INPUT if data_len > MAX_EVENT_DATA_SIZE.
Gas: 100 base + 50 × topics_count + 8 per data byte.
(Each topic adds 32 bytes of state-commitment cost; 50 gas per topic
covers the bloom-set + per-topic index write.)
Semantics:
Appends an event record to the current overlay's events buffer. Topic
semantics follow the §14.1 convention:
- topic[0] = Blake3(canonical_event_signature). Identifies the event type;
this is what subscribers and indexers match on as the primary filter.
- topic[1..topics_count] = indexed field values, in declaration order.
Each indexed field's value occupies one 32-byte topic slot. Authors
declare which fields are indexed in otigen.toml (§14.1).
At wave commit (§15), the events buffer flushes atomically with state:
- One row to events_cf (primary, keyed by (wave_id, tx_index, event_index))
- topics_count rows to events_by_topic_cf (one per topic value)
- One row to events_by_contract_cf (keyed by contract_addr)
- Every topic + the contract_addr is added to the wave's events_bloom
- The event participates in the wave's events_root Merkle tree
Events from a reverted (sub-)call are discarded along with the overlay;
the chain never sees events from a path that did not commit.
7.6 Hashing primitives
All three accept variable-length input and write a 32-byte output.
hash_blake3
pyde::hash_blake3(in_ptr: i32, in_len: i32, out_ptr: i32) -> i32
Returns: 0 always.
Gas: 15 base + 3 per word (8 bytes), rounded up.
hash_poseidon2
pyde::hash_poseidon2(in_ptr: i32, in_len: i32, out_ptr: i32) -> i32
Returns: 0 always.
Gas: 100 base + 30 per word (8 bytes), rounded up.
Notes: ZK-friendly hash; significantly more expensive than Blake3 in native execution.
Use where ZK-circuit-friendly output is required (state-root commitments, address
derivation). Use Blake3 everywhere else.
hash_keccak256
pyde::hash_keccak256(in_ptr: i32, in_len: i32, out_ptr: i32) -> i32
Returns: 0 always.
Gas: 30 base + 6 per word (8 bytes), rounded up.
Notes: provided for cross-chain interoperability. Pyde's native hashes are Blake3
(performance path) and Poseidon2 (ZK path). Keccak256 is for verifying Ethereum-style
inputs (Merkle Patricia proofs, etc.).
7.7 Post-quantum cryptography
falcon_verify
pyde::falcon_verify(
pk_ptr: i32, — pointer to ~897-byte FALCON-512 public key
msg_ptr: i32, msg_len: i32,
sig_ptr: i32, sig_len: i32
) -> i32
Returns: 0 if signature is valid, ERR_SIGNATURE_INVALID otherwise.
Gas: 50,000 base. (Reflects the ~80μs cost on commodity x86_64 commodity hardware.)
7.8 Cross-contract calls
cross_call
pyde::cross_call(
target_ptr: i32, — pointer to 32-byte target contract address
fn_name_ptr: i32, fn_name_len: i32,— UTF-8 function name to invoke
calldata_ptr: i32, calldata_len: i32,
value_ptr: i32, — pointer to 16-byte u128 value to attach (0 = no transfer)
gas_limit: i64, — gas budget for the sub-call
return_data_out_ptr: i32,
return_data_out_len_ptr: i32 — pointer to i32 written with actual return length
) -> i32
Returns: 0 on success; sub-call's negative error code on failure;
ERR_CROSS_CALL_FAILED if sub-call trapped;
ERR_CROSS_CALL_OUT_OF_GAS if sub-call exhausted forwarded gas;
ERR_REENTRANCY_BLOCKED if target function is non-`reentrant` and caller would
re-enter it;
ERR_INVALID_FUNCTION_NAME if target function does not exist;
ERR_VALUE_TRANSFER_NOT_PAYABLE if value > 0 and target is non-`payable`.
Gas: 1,000 base + 8 per byte of calldata + sub-call's actual gas_used.
Semantics: synchronous call to another contract within the same wave. The sub-call
runs in a nested per-tx overlay (see [Chapter 3 §3.5b](../chapters/03-virtual-machine.md)).
On sub-call success: the overlay merges into the parent on cross_call return.
On sub-call trap or non-zero error: the overlay is discarded; parent state untouched.
Caller's remaining gas is decremented by sub-call's actual gas_used regardless of outcome.
cross_call_static
pyde::cross_call_static(
target_ptr: i32,
fn_name_ptr: i32, fn_name_len: i32,
calldata_ptr: i32, calldata_len: i32,
gas_limit: i64,
return_data_out_ptr: i32,
return_data_out_len_ptr: i32
) -> i32
Returns: as above, but target must be a `view`-attributed function (returns
ERR_FORBIDDEN otherwise).
Gas: 50 base for the dispatch (caller pays). Sub-call execution itself is FREE
to the caller — see "View calls are free" below.
Semantics: view-only variant. Sub-call may not modify state, emit events, or
transfer value. Useful for safe queries across contracts.
View calls are free:
- Off-chain via RPC pyde_call(contract, fn, calldata): completely free; no
tx, no consensus, no gas accounting.
- On-chain via this host fn: ALSO free for the caller. The dispatch base
cost (50 gas) covers setup; the sub-call's actual execution does not
debit the caller's remaining gas.
- View functions cannot mutate state, so the chain doesn't need to charge
for them as an economic incentive — the rationale for charging state-
mutating ops doesn't apply.
Bounding mechanism (DoS prevention):
- Each cross_call_static invocation initialises its wasmtime instance with
a per-call FUEL CAP, default VIEW_FUEL_CAP = 10_000_000 (~3ms commodity).
- Configurable per node operator (NodeConfig.view_fuel_cap).
- If the view exhausts the cap: trap with OutOfFuel; cross_call_static
returns ERR_CROSS_CALL_OUT_OF_GAS to caller; caller's actual gas budget
is NOT debited for the sub-call's work.
- The cap exists purely to bound per-call wall-clock time so a malicious
contract can't burn unbounded validator CPU via view spam.
delegate_call
pyde::delegate_call(
target_ptr: i32, — pointer to 32-byte target contract address
(whose CODE will run)
fn_name_ptr: i32, fn_name_len: i32,
calldata_ptr: i32, calldata_len: i32,
gas_limit: i64,
return_data_out_ptr: i32,
return_data_out_len_ptr: i32
) -> i32
Returns: 0 on success; sub-call's negative error code on failure;
ERR_CROSS_CALL_FAILED if sub-call trapped;
ERR_CROSS_CALL_OUT_OF_GAS if sub-call exhausted forwarded gas;
ERR_INVALID_FUNCTION_NAME if target function does not exist;
ERR_REENTRANCY_BLOCKED if (caller_addr, target_fn) is already on the call stack
and target_fn is not `reentrant`.
Gas: 1,200 base + 8 per byte of calldata + sub-call gas_used.
(Slightly higher base than cross_call because the engine must keep the caller's
overlay active rather than push a fresh one.)
Semantics: execute target contract's CODE in the CALLER'S STORAGE CONTEXT.
Concretely:
- Loads target's WASM + parsed ABI
- Invokes target's named function, but with the engine's HostState configured
so that:
* sload/sstore hit the caller's slots (NOT the target's)
* self_address() returns the caller's address (NOT the target's)
* caller() returns the original caller of the OUTER function
* origin() unchanged (still tx originator)
* tx_value() unchanged (still the value attached to the outer call)
- Access list enforcement is against the CALLER'S declared list (not the
target's) — the target's code may try to access slots the caller hasn't
declared, which fails with ERR_ACCESS_LIST_VIOLATION
- No value transfer happens (delegate_call doesn't move PYDE — the called
code operates on the caller's balance directly)
- Reentrancy guard applies to (caller_addr, target_fn_name)
Use cases:
- Upgradeable contracts: proxy contract holds state; delegate_call to an
implementation contract for logic. Upgrade = swap which implementation
address the proxy delegates to.
- Libraries: shared logic deployed once; per-caller state via delegate_call.
Risks for authors:
- Target's code can corrupt caller's storage if their slot layouts differ.
- Target's code can transfer caller's funds (self_address is the caller).
- This is the same risk model as EVM's delegatecall; the v1 spec does not
add any structural guardrails beyond access-list enforcement. Authors are
expected to use delegate_call only with target contracts they fully trust.
7.9 Halt operations
return
pyde::return(data_ptr: i32, data_len: i32) -> (never returns)
Sets the current call frame's return data and exits successfully. The data is
visible to the caller via cross_call's return_data_out_ptr.
Gas: 0 base (the trap exits the call frame).
revert
pyde::revert(reason_ptr: i32, reason_len: i32) -> (never returns)
Reverts the current call frame. All state changes since the call started are
discarded (the per-tx overlay is dropped). The reason bytes are made available
to the caller as the failure payload.
Gas: 0 base.
7.10 Explicit gas metering
consume_gas
pyde::consume_gas(amount: i64) -> i32
Returns: 0 on success, ERR_OUT_OF_GAS if amount exceeds remaining gas (and the
function traps with OutOfFuel — the i32 return is for documentation only).
Gas: 2 base + amount (so `consume_gas(N)` total cost is N+2).
Use case: contracts that perform off-fuel work (synchronous loops bounded by
external data, expensive computations charged against the user's gas budget) call
consume_gas explicitly to make the charge visible.
7.11 VRF beacon
beacon_get
pyde::beacon_get(out_ptr: i32) -> i32
out_ptr — pointer to 32-byte buffer
Returns: 0 always; writes the current wave's committee-derived VRF beacon
(XOR of all members' beacon shares from the prior anchor round).
Gas: 50 base.
Semantics: deterministic, public randomness, identical across all validators. Use as
a chain-derived random source. Note that the beacon is *publicly predictable* within a
wave — adversaries cannot bias it, but they *can* observe it. Use threshold encryption
if you need adversary-private randomness.
8. Parachain-only host functions
These functions are available only to modules deployed with type = "parachain". The deploy-time validator rejects any non-parachain module that imports any function in this section. Attempting to call a parachain function from a non-parachain context (theoretically impossible after deploy validation, surfaces as an engine bug) returns ERR_PARACHAIN_ONLY.
For the parachain design rationale, see companion/PARACHAIN_DESIGN.md.
8.1 Parachain storage
parachain_storage_read
pyde::parachain_storage_read(
key_ptr: i32, key_len: i32,
value_out_ptr: i32,
value_out_len_ptr: i32
) -> i32
Returns: 0 on success,
ERR_OUTPUT_BUFFER_TOO_SMALL if the value exists but caller's buffer is too small.
Gas: 250 base + 1 per byte returned.
Semantics: read from this parachain's state subtree (PIP-2 clustered under
parachain_id[..16]). Variable-length keys + variable-length values, unlike the
core sload's fixed 32-byte interface. A key that was never written returns
success with *out_len_ptr written as 0 — NOT an error. Callers check the written
length to distinguish "empty value" from "value too large for my buffer."
parachain_storage_write
pyde::parachain_storage_write(
key_ptr: i32, key_len: i32,
value_ptr: i32, value_len: i32
) -> i32
Returns: 0 on success, ERR_FORBIDDEN if called from a view function.
Gas: 5,500 base + 10 per byte stored.
parachain_storage_delete
pyde::parachain_storage_delete(key_ptr: i32, key_len: i32) -> i32
Returns: 0 on success (even if key did not exist), ERR_FORBIDDEN if view fn.
Gas: 250 base.
8.2 Parachain context
parachain_id
pyde::parachain_id(out_ptr: i32) -> i32
out_ptr — pointer to 32-byte buffer
Returns: 0 always; writes this parachain's ID (Poseidon2 of "pyde-parachain:" || name).
Gas: 5 base.
parachain_version
pyde::parachain_version() -> i32
Returns: the current parachain's active version (u32).
Gas: 5 base.
8.3 Parachain events
parachain_emit_event
pyde::parachain_emit_event(
topics_ptr: i32,
topics_count: i32, — 1 to 4 inclusive; topic[0] = Blake3(signature)
data_ptr: i32,
data_len: i32,
) -> i32
Returns: 0 on success,
ERR_FORBIDDEN if view fn,
ERR_INVALID_INPUT if topics_count out of range or data oversized.
Gas: 100 base + 50 × topics_count + 8 per data byte.
Semantics: identical to the core emit_event (§7.5) including multi-topic
support and the indexed-field convention. The event is filed under the
parachain's own event-stream namespace (the contract_addr field of the
EventRecord carries the parachain_id) so subscribers can filter for a
specific parachain's events. Same storage layout and indexing as core
events (§15.3).
8.4 Cross-parachain messaging
send_xparachain_message
pyde::send_xparachain_message(
target_id_ptr: i32, — pointer to 32-byte destination parachain ID
msg_ptr: i32, msg_len: i32, — opaque payload
callback_fn_name_ptr: i32, callback_fn_name_len: i32, — function on this parachain
max_callback_gas: i64,
timeout_waves: i64 — give up after this many waves
) -> i64
Returns: positive XCallId (u64) on success;
negative error code: ERR_XCALL_RATE_LIMITED if budget exceeded,
ERR_INVALID_INPUT if target_id is malformed, ERR_INVALID_FUNCTION_NAME if
callback function does not exist on this parachain.
Gas: 10,000 base + 8 per byte of msg_len.
Semantics: queue an asynchronous message to the target parachain. The calling
parachain's committee threshold-signs the message; the target parachain's committee
verifies and dispatches. Result (or timeout) arrives later as a callback transaction
that invokes the named callback_fn on this parachain. See PARACHAIN_DESIGN §9 for
the full flow.
Rate limit: 64 outgoing messages per wave per parachain by default
(parachain-configurable).
8.5 Threshold cryptography
These are exposed to parachains for application-level confidentiality use cases (blinded auctions, sealed-bid markets, MEV-protected DEX matching at parachain layer).
threshold_encrypt
pyde::threshold_encrypt(
plaintext_ptr: i32, plaintext_len: i32,
ciphertext_out_ptr: i32,
ciphertext_out_len_ptr: i32
) -> i32
Returns: 0 on success, ERR_OUTPUT_BUFFER_TOO_SMALL if buffer insufficient.
Gas: 80,000 base + 100 per byte.
Semantics: encrypt under the current epoch's threshold public key. Result is a
Kyber-768 KEM envelope + ChaCha20-Poly1305 ciphertext. Decryption requires ≥85
shares (combined by the chain at appropriate ceremony points).
threshold_decrypt
pyde::threshold_decrypt(
ciphertext_ptr: i32, ciphertext_len: i32,
plaintext_out_ptr: i32,
plaintext_out_len_ptr: i32
) -> i32
Returns: 0 on success, ERR_CIPHERTEXT_INVALID if malformed,
ERR_FORBIDDEN if the calling parachain has not yet hit a wave where the
committee has combined shares for this ciphertext.
Gas: 100,000 base + 50 per byte.
Semantics: decrypt a ciphertext for which the committee has already executed the
threshold-decryption ceremony. The combined plaintext is materialized into the
output buffer. This is parachain-only because cross-parachain ceremony coordination
requires the parachain-specific committee infrastructure.
9. Forbidden imports
9.1 Hard-rejected at deploy time
The deploy validator rejects any module whose WASM import section references any of the following. Attempting to deploy such a module returns DeployRejected: ForbiddenImport(<name>).
| Module | Function | Reason |
|---|---|---|
wasi_snapshot_preview1 | (any) | File I/O, system clock, env vars — non-deterministic |
wasi_unstable | (any) | Same |
wasi:* | (any) | Same |
env | (any) | Generic env-namespace functions out of scope for Pyde ABI |
pyde | functions not in this spec | Future-proofing; rejects modules built against an unreleased ABI version |
| Any other module name | (any) | Single permitted namespace is pyde. |
9.2 Parachain functions called from non-parachain modules
If a non-parachain module imports a function from §8, the deploy validator rejects the deployment with DeployRejected: ParachainOnly(<name>). The eligible-import set is determined by the contract's declared type in otigen.toml.
9.3 WASM features rejected at instantiation time
The wasmtime config (see Chapter 3 §3.2) rejects modules that use:
- Threads (
wasm_threads) - SIMD (
wasm_simd,wasm_relaxed_simd) - Reference types (
wasm_reference_types) - GC (
wasm_gc) - Function references (
wasm_function_references) - Multiple memories (
wasm_multi_memory) - Memory64 (
wasm_memory64) - Component model (
wasm_component_model)
These cannot be opted into per-contract. They are network-wide forbidden.
10. Gas table
Authoritative gas costs for every host function. This table is the source of truth; if the engine implementation diverges, the engine is wrong.
| Function | Base gas | Per-byte / per-word | Notes |
|---|---|---|---|
sload | 200 | — | Same gas hot/cold |
sstore | 5,000 | — | Same gas new/overwrite |
sdelete | 150 | — | No refund |
balance | 100 | — | |
transfer | 7,000 | — | |
caller, origin, self_address | 5 | — | |
block_height, wave_id, block_timestamp, chain_id | 2 | — | |
tx_hash | 5 | — | |
tx_value | 5 | — | |
tx_gas_remaining | 2 | — | |
calldata_size | 2 | — | |
calldata_copy | 8 | 1 / byte | |
emit_event | 100 | + 50 / topic + 8 / data byte | 1 to 4 topics; topic[0] conventionally signature hash |
hash_blake3 | 15 | 3 / word (8 bytes) | |
hash_poseidon2 | 100 | 30 / word | ZK-friendly, expensive |
hash_keccak256 | 30 | 6 / word | EVM-compat |
falcon_verify | 50,000 | — | ~80μs commodity |
cross_call | 1,000 | 8 / byte calldata + sub-call gas | |
cross_call_static | 50 | — | Sub-call execution is FREE; caller pays only the dispatch base. Sub-call bounded by VIEW_FUEL_CAP (default 10M instructions ≈ 3ms) |
delegate_call | 1,200 | 8 / byte calldata + sub-call gas | Caller's storage context |
return | 0 | — | Halt op |
revert | 0 | — | Halt op |
consume_gas | 2 | + amount | Pure manual metering |
beacon_get | 50 | — | |
parachain_storage_read | 250 | 1 / byte returned | Parachain only |
parachain_storage_write | 5,500 | 10 / byte | Parachain only |
parachain_storage_delete | 250 | — | Parachain only |
parachain_id | 5 | — | Parachain only |
parachain_version | 5 | — | Parachain only |
parachain_emit_event | 100 | + 50 / topic + 8 / data byte | Parachain only; same multi-topic surface as core emit_event |
send_xparachain_message | 10,000 | 8 / byte | Parachain only |
threshold_encrypt | 80,000 | 100 / byte | Parachain only |
threshold_decrypt | 100,000 | 50 / byte | Parachain only |
Per-word = per-8-bytes, rounded up. Per-byte = per-1-byte, no rounding.
These values are initial calibration, set against representative benchmarks for commodity validator hardware. The benchmark harness (see companion/PERFORMANCE_HARNESS.md) is the authority for production calibration; pre-mainnet sweeps may revise these numbers up or down by ≤2× without changing the ABI version (gas tables are an implementation detail, not part of the binary signature).
11. Native (non-WASM) transaction types
Several transaction types bypass the WASM execution layer entirely and run as native handlers in the engine. These do not use the Host Function ABI — they are listed here for completeness so contract authors understand which operations are "free of WASM overhead":
| Transaction type | Cost | Path |
|---|---|---|
Transfer (account-to-account) | ~21,000 gas | Native handler; no wasmtime instantiation |
ValidatorRegister | Native | |
ValidatorUnbond | Native | |
Stake / Unstake | Native | |
RotateKeys (account key rotation) | Native | |
NameRegister (system contract) | Native (via system contract) |
See Chapter 3 §3.9b for the dispatch logic.
12. Invoking host functions from contract code
This section explains the WASM imports mechanism with concrete language examples — the most-asked question from contract authors.
12.1 What an import declaration actually is
A WebAssembly module's binary format includes an import section listing every external function the module needs. Each entry pairs a (module_name, function_name) with a function type signature. The module body never includes the implementation; it just declares "I'll call this — somebody provide it at instantiation time."
Pyde reserves the module name pyde for all host functions. A contract that declares an import like:
(import "pyde" "sload" (func (param i32 i32) (result i32)))
is saying: "Give me a function named sload from module pyde, taking (i32, i32) and returning i32." At instantiation time, wasmtime walks the import section and looks each one up in a host-provided Linker. If the entry exists, the contract's call is wired to the host's Rust implementation. If not, instantiation fails — and the deploy validator rejects the contract before it ever reaches a node.
12.2 Rust contract — declaring imports
#![allow(unused)] fn main() { // All host functions go under module "pyde" #[link(wasm_import_module = "pyde")] extern "C" { fn sload(slot_ptr: u32, value_out_ptr: u32) -> i32; fn sstore(slot_ptr: u32, value_ptr: u32) -> i32; fn caller(addr_out_ptr: u32) -> i32; fn emit_event( topic_ptr: u32, topic_len: u32, data_ptr: u32, data_len: u32, ) -> i32; fn hash_blake3(in_ptr: u32, in_len: u32, out_ptr: u32) -> i32; } #[no_mangle] pub extern "C" fn store_and_read() -> i32 { let slot = [0x42u8; 32]; let value_in = [0xAAu8; 32]; let mut value_out = [0u8; 32]; unsafe { sstore(slot.as_ptr() as u32, value_in.as_ptr() as u32); sload(slot.as_ptr() as u32, value_out.as_mut_ptr() as u32) } } }
Compile with cargo build --target wasm32-unknown-unknown --release. Inspect with wasm-objdump -x:
Import[5]:
- func[0] sig=2 <pyde.sload>
- func[1] sig=2 <pyde.sstore>
- func[2] sig=3 <pyde.caller>
- func[3] sig=4 <pyde.emit_event>
- func[4] sig=5 <pyde.hash_blake3>
No Pyde library dependency. No code generation. Just extern declarations and the attribute that targets the pyde import namespace.
12.3 AssemblyScript contract — same imports
// AssemblyScript uses @external decorators
@external("pyde", "sload")
declare function sload(slotPtr: usize, valueOutPtr: usize): i32;
@external("pyde", "sstore")
declare function sstore(slotPtr: usize, valuePtr: usize): i32;
@external("pyde", "caller")
declare function caller(addrOutPtr: usize): i32;
@external("pyde", "emit_event")
declare function emit_event(
topicPtr: usize, topicLen: usize,
dataPtr: usize, dataLen: usize
): i32;
@external("pyde", "hash_blake3")
declare function hash_blake3(inPtr: usize, inLen: usize, outPtr: usize): i32;
export function store_and_read(): i32 {
const slot = new ArrayBuffer(32);
const valueIn = new ArrayBuffer(32);
const valueOut = new ArrayBuffer(32);
// Fill slot with 0x42, valueIn with 0xAA
const slotPtr = changetype<usize>(slot);
const valueInPtr = changetype<usize>(valueIn);
for (let i: i32 = 0; i < 32; i++) {
store<u8>(slotPtr + i, 0x42);
store<u8>(valueInPtr + i, 0xAA);
}
sstore(slotPtr, valueInPtr);
return sload(slotPtr, changetype<usize>(valueOut));
}
Compile with npx asc store_and_read.ts -o store_and_read.wasm --target release. Resulting WASM has the same import structure. The runtime can't tell which language produced it.
12.4 Go (TinyGo) contract — same imports
//go:wasmimport pyde sload
func sload(slotPtr uint32, valueOutPtr uint32) int32
//go:wasmimport pyde sstore
func sstore(slotPtr uint32, valuePtr uint32) int32
//go:wasmimport pyde emit_event
func emit_event(topicPtr, topicLen, dataPtr, dataLen uint32) int32
//go:export store_and_read
func StoreAndRead() int32 {
slot := [32]byte{}
for i := range slot { slot[i] = 0x42 }
valueIn := [32]byte{}
for i := range valueIn { valueIn[i] = 0xAA }
var valueOut [32]byte
slotPtr := uint32(uintptr(unsafe.Pointer(&slot[0])))
valueInPtr := uint32(uintptr(unsafe.Pointer(&valueIn[0])))
valueOutPtr := uint32(uintptr(unsafe.Pointer(&valueOut[0])))
sstore(slotPtr, valueInPtr)
return sload(slotPtr, valueOutPtr)
}
Compile with tinygo build -target=wasm-unknown -o store_and_read.wasm. Same WASM output shape.
12.5 C / C++ contract — same imports
__attribute__((import_module("pyde"), import_name("sload")))
extern int32_t sload(int32_t slot_ptr, int32_t value_out_ptr);
__attribute__((import_module("pyde"), import_name("sstore")))
extern int32_t sstore(int32_t slot_ptr, int32_t value_ptr);
__attribute__((import_module("pyde"), import_name("emit_event")))
extern int32_t emit_event(int32_t topic_ptr, int32_t topic_len,
int32_t data_ptr, int32_t data_len);
__attribute__((export_name("store_and_read")))
int32_t store_and_read(void) {
uint8_t slot[32]; for (int i = 0; i < 32; i++) slot[i] = 0x42;
uint8_t value_in[32]; for (int i = 0; i < 32; i++) value_in[i] = 0xAA;
uint8_t value_out[32];
sstore((int32_t)(uintptr_t)slot, (int32_t)(uintptr_t)value_in);
return sload((int32_t)(uintptr_t)slot, (int32_t)(uintptr_t)value_out);
}
Compile with clang --target=wasm32 -nostdlib -Wl,--no-entry -o store_and_read.wasm store_and_read.c. Same WASM output shape.
12.6 Host side — how the engine handles invocations
In Pyde's wasm-exec Rust crate, every function in this spec is registered with wasmtime's Linker at engine startup. When a contract is instantiated, wasmtime walks the contract's import section and binds each one to its registered handler:
#![allow(unused)] fn main() { // Engine startup — once per node lifetime pub fn build_linker(engine: &wasmtime::Engine) -> Linker<HostState> { let mut linker = Linker::new(engine); // Register every host function from §7 and §8 linker.func_wrap("pyde", "sload", host_sload).unwrap(); linker.func_wrap("pyde", "sstore", host_sstore).unwrap(); linker.func_wrap("pyde", "caller", host_caller).unwrap(); linker.func_wrap("pyde", "emit_event", host_emit_event).unwrap(); linker.func_wrap("pyde", "hash_blake3", host_hash_blake3).unwrap(); // ... 30+ more linker } // sload implementation (correct version — missing slot returns zeros, no error) fn host_sload( mut caller: Caller<'_, HostState>, slot_ptr: i32, value_out_ptr: i32, ) -> i32 { // 1. Charge gas FIRST (before any work) if caller.consume_fuel(SLOAD_GAS_COST).is_err() { return ERR_OUT_OF_GAS; // documentation; wasmtime traps with OutOfFuel } // 2. Get the contract's exported linear memory let memory = match caller.get_export("memory") { Some(wasmtime::Extern::Memory(m)) => m, _ => return ERR_INTERNAL, }; // 3. Read the slot hash from WASM memory (bounds-checked by wasmtime) let mut slot_bytes = [0u8; 32]; if memory.read(&caller, slot_ptr as usize, &mut slot_bytes).is_err() { return ERR_INVALID_INPUT; } // 4. Access-list check if !caller.data().access_list.contains(&slot_bytes) { return ERR_ACCESS_LIST_VIOLATION; } // 5. Look up the value — default to 32 zero bytes if never written let value_bytes = caller.data().state_get(&slot_bytes).unwrap_or([0u8; 32]); // 6. Write back to WASM memory (bounds-checked) if memory.write(&mut caller, value_out_ptr as usize, &value_bytes).is_err() { return ERR_INVALID_INPUT; } 0 // success } }
The flow when a contract executes sload(slot_ptr, value_out_ptr):
Contract WASM (any language) wasm-exec (Rust)
───────────────────────────── ───────────────────────────────
[author's compiled WASM] [engine startup, once per node]
(import "pyde" "sload" ...) linker.func_wrap("pyde", "sload",
host_sload)
↓
[at instantiation] [wasmtime walks contract's
import section, binds each
import to a linker entry]
↓
[at execution] [contract's `sload` stub now
sload(slot_ptr, value_out_ptr) points to host_sload]
│
▼
[wasmtime traps into Rust] ──────→ host_sload(caller, slot_ptr, value_out_ptr)
│
├─ charge gas via consume_fuel
├─ read 32 bytes from WASM memory at slot_ptr
├─ access-list check
├─ state_get(slot_bytes).unwrap_or([0; 32])
├─ write 32 bytes back at value_out_ptr
▼
← return i32 return 0
│
▼
[contract resumes execution
with sload's return value]
13. Cross-contract call mechanics
cross_call is the most complex host function. This section spells out the exact flow when contract A calls contract B.
13.1 The 12-step flow
When A invokes cross_call(B_addr, "fn_name", calldata, value, gas_limit, return_data_out_ptr, return_data_out_len_ptr):
- Wasmtime traps into
host_cross_callwith all arguments. - Charge A's gas:
1,000 base + 8 × calldata_len + (gas_limit reserved). If A's remaining budget is insufficient, trap A withOutOfFuel. - Validate target B: state-lookup
B_addr; must have a non-emptycode_hash. If not, returnERR_CROSS_CALL_FAILED. - Validate function name: lookup
"fn_name"in B's deployed ABI metadata (cached at deploy time). If not found, returnERR_INVALID_FUNCTION_NAME. - Reentrancy check: walk the current call stack of
(contract, fn)pairs. If(B_addr, "fn_name")is already on the stack AND"fn_name"is not#[reentrant], returnERR_REENTRANCY_BLOCKED. - Payable check: if
value > 0and"fn_name"is not#[payable], returnERR_VALUE_TRANSFER_NOT_PAYABLE. - Push a new overlay onto the per-tx overlay stack. Call it
overlay_B. Reads from B'ssloadwalk:overlay_B → overlay_A → dashmap → state_cf. Writes from B'ssstorego tooverlay_Bonly. - Create a new wasmtime Store + Instance for B with: fresh linear memory (B cannot see A's memory directly); fuel =
gas_limit; the sameLinker(so B has the same host functions available);HostStatepointing tooverlay_Band the active call stack with B pushed on. - Copy calldata from A's memory into B's memory at a host-chosen offset (typically the start of B's memory's calldata region).
- Apply value transfer: if
value > 0, atomically debit A's balance and credit B's byvalue. This happens before B's code runs so B's firsttx_value()call sees the right amount. - Invoke B's entry function with calldata. B's WASM executes in isolation — its
sload/sstoreoperate onoverlay_B; its owncross_callwould push another overlay on top. - On B's exit, handle the outcome:
- Success (B returned normally): merge
overlay_Bintooverlay_A; copy return data from B's memory into A's memory atreturn_data_out_ptr; write actual length atreturn_data_out_len_ptr; consume B's actual fuel from A's remaining budget; return0to A. - Trap (B hit OutOfFuel, MemoryOutOfBounds, reverted, etc.): discard
overlay_Bentirely; revert the value transfer from step 10; consume B's actual fuel from A's remaining; returnERR_CROSS_CALL_FAILEDto A. - OutOfFuel specifically: same as trap, but return
ERR_CROSS_CALL_OUT_OF_GASto distinguish.
- Success (B returned normally): merge
13.2 The overlay stack
The per-tx overlay stack is the load-bearing data structure here:
At depth 0 (top of stack — what B's writes go to):
overlay_B = HashMap<SlotHash, Value> (initially empty)
At depth 1:
overlay_A = HashMap<SlotHash, Value> (A's pending writes from before cross_call)
At depth 2:
wave overlay = HashMap<SlotHash, Value> (writes from prior committed txs in this wave)
At depth 3:
dashmap (write-back cache, hot recent state)
At depth 4:
state_cf (canonical disk-backed state)
At depth 5:
jmt_cf (versioned tree; only for state-root computation)
Reads walk top-down until a value is found. Writes always go to the top of the stack. Merge on success copies overlay_B's entries into overlay_A. Discard on trap drops overlay_B.
This is the same nesting pattern at every depth: a tx that issues a cross_call becomes one frame deeper; that sub-call issuing another cross_call becomes one deeper still.
13.3 Memory isolation
A and B have completely separate WASM linear memories. They cannot see each other's memory. The only communication channels are:
- A → B: the calldata bytes copied at step 9
- B → A: the return data copied at step 12 (success path)
- A ↔ B (shared): state, but only through the overlay stack — there is no shared memory region
This means a malicious B cannot read A's stack, A's locals, A's other variables. The sandbox is per-instance.
13.4 Stack depth cap
To prevent runaway recursion (e.g., a contract that calls itself unboundedly through different addresses), the call stack has a hard depth limit. Default: 1024 frames. Exceeding it returns ERR_CROSS_CALL_FAILED from the offending cross_call invocation.
13.5 Gas accounting
- Reservation: A pre-charges
gas_limitfrom its remaining budget at step 2 (the host function refuses to start the sub-call if A can't afford the reservation). - Forwarding: B receives a fresh fuel counter of
gas_limit. - Consumption: After B exits, A's budget is debited by B's actual fuel consumed (which may be less than
gas_limit). - No refund: any unused portion of
gas_limitis not returned to A (consistent with the no-refund policy). A consumed gas it didn't end up using — that's the tradeoff for the simpler accounting model. Authors are advised to sizegas_limitcarefully.
13.6 Why cross_call_static exists
cross_call_static is the read-only variant. It enforces:
- Target function must be marked
#[view]— if not, returnsERR_FORBIDDEN. - Sub-call cannot mutate state, emit events, or transfer value (the view-mode flag in the overlay rejects writes).
- No new overlay is needed (no writes possible); reads walk the existing stack.
This is cheaper (no overlay push/merge) and safer (no reentrancy risk — view functions can't change anything observable).
14. Event encoding convention
Each event carries 1 to 4 topics (each 32 bytes) plus an opaque data payload. The chain stores both verbatim. For wallets, indexers, and SDKs to decode events consistently, Pyde defines a canonical convention for both.
14.1 Topics
Topics are how events are indexed and filtered on-chain. Each event has 1 to 4 topics. By convention:
topic[0]is alwaysBlake3(canonical_event_signature). This is the event-type identifier — what subscribers and indexers match on as the primary filter.topic[1..topics_count]are indexed-field values, in author-declared order.
Authors mark fields as indexed in otigen.toml:
[events.Transfer]
signature = "Transfer(address,address,uint128)"
fields = [
{ name = "from", type = "address", indexed = true },
{ name = "to", type = "address", indexed = true },
{ name = "amount", type = "uint128" }, # not indexed → goes in data
]
Up to 3 fields can be indexed (giving a total of 4 topics — signature plus 3 — matching EVM's LOG4 limit).
Topic value encoding
How each indexed-field value becomes a 32-byte topic:
| Field type | Encoding rule |
|---|---|
address ([u8; 32]) | Stored as-is (already 32 bytes) |
uint64, int64 | Left-padded to 32 bytes (zeros in MSB) |
uint128, int128 | Left-padded to 32 bytes |
bool | Left-padded to 32 bytes (0x00...00 or 0x00...01) |
[u8; N] where N ≤ 32 | Left-padded to 32 bytes |
string | Blake3(utf8_bytes) |
bytes (Vec<u8>) | Blake3(bytes) |
T[] (Vec<T>) | Blake3(borsh_encode(value)) |
struct { ... } | Blake3(borsh_encode(value)) |
enum { ... } | Blake3(borsh_encode(value)) |
Rule: fixed-size ≤32 bytes get stored as-is (padded); variable-size or >32 bytes get hashed. Matches EVM's indexed semantics.
Canonical signature string
The signature string drives topic[0]. Type names mirror Solidity's for familiarity:
| Pyde type | Signature token |
|---|---|
[u8; 32] (address) | address |
u64 | uint64 |
u128 | uint128 |
i64 | int64 |
bool | bool |
String (UTF-8) | string |
Vec<u8> | bytes |
Vec<T> | T[] |
[T; N] | T[N] |
enum X { ... } | enum |
| Custom struct | tuple (with field types in parens; rare) |
Examples:
"Transfer(address,address,uint128)"
"Approval(address,address,uint128,uint64)"
"OrderFilled(address,string,uint128,uint64[],enum)"
The signature string is not stored on chain — only Blake3(signature) is, as topic[0]. Indexers and SDKs maintain a registry of signatures they care about and hash them locally to match against event topics. The pyde.abi custom section of the deployed contract carries the full signature for any explorer that wants to render the event with field names.
14.2 Data
14.2 Data
The data field is the event payload as bytes. The chain stores it verbatim — encoding is the author's choice.
Borsh is the recommended encoding. Pyde's toolchain, SDKs, indexers, wallets, and example contracts all assume Borsh by default; choosing it gets you out-of-the-box decoding everywhere. otigen ships Borsh helpers as part of the canonical project templates. pyde-rust-sdk and pyde-ts-sdk ship Borsh decoders that match topics to signature registries and auto-deserialize. Block explorers built on these SDKs render Borsh-encoded events without any per-contract integration.
Authors picking a different encoding (raw bytes for tiny events, Protobuf for cross-team contracts, custom format for niche cases) are free to do so — the chain doesn't care — but they take on the integration burden: SDK consumers need custom decoders, wallet previews can't auto-render the event, indexers need per-contract logic.
Borsh chosen as the recommended default over alternatives:
- vs JSON: smaller (no whitespace, no field names in the wire format), deterministic byte ordering, no integer-precision issues
- vs Protobuf: simpler, no schema-evolution complexity, language-agnostic implementations more uniform, no
.prototoolchain dependency - vs SCALE: better Rust-ecosystem support, simpler grammar
- vs EVM ABI encoding: simpler, more compact, no padding-to-32-bytes overhead, no special handling for dynamic-length fields
- vs MsgPack/CBOR: deterministic by construction (canonical encoding), no implementation-defined behaviors
Borsh is supported in: Rust (borsh crate), TypeScript (@dao-xyz/borsh-ts, borsh-js), AssemblyScript (community as-borsh), Go (github.com/near/borsh-go), C (community), Python (borsh-construct). Pyde's recommendation tracks this ecosystem; if a language gains a high-quality Borsh implementation, contracts in that language get first-class event support without Pyde shipping bindings.
14.3 Example: Rust emitter (with indexed fields)
The author declares the event in otigen.toml (per §14.1). The SDK generates a typed emit helper. The author's code stays clean:
#![allow(unused)] fn main() { use pyde_contract::events; // Inside a contract function: events::Transfer { from: caller_address, to: recipient, amount: 100u128, }.emit(); }
Behind the scenes, the SDK helper (generated from otigen.toml) builds the call:
#![allow(unused)] fn main() { // Generated by SDK from otigen.toml — author doesn't write this impl Transfer { pub fn emit(self) -> i32 { // 1. Build topics let mut topics = [0u8; 4 * 32]; // topic[0] = Blake3(signature) — precomputed constant topics[0..32].copy_from_slice(&TRANSFER_SIGNATURE_HASH); // topic[1] = padded(from) — address is already 32 bytes topics[32..64].copy_from_slice(&self.from); // topic[2] = padded(to) topics[64..96].copy_from_slice(&self.to); // No topic[3] — we only have 2 indexed fields. // 2. Borsh-encode non-indexed fields (just amount) let data = borsh::to_vec(&self.amount).unwrap(); // 3. Call the host function unsafe { emit_event( topics.as_ptr() as u32, 3, // topics_count = 3 data.as_ptr() as u32, data.len() as u32, ) } } } // Precomputed at otigen build time: const TRANSFER_SIGNATURE_HASH: [u8; 32] = blake3_const(b"Transfer(address,address,uint128)"); }
For events without indexed fields, the SDK emits with topics_count = 1 (just the signature hash) and Borsh-encodes all fields into data.
14.4 Example: TypeScript decoder (in pyde-ts-sdk, with indexed fields)
import { deserialize } from "@dao-xyz/borsh-ts";
import { blake3 } from "@noble/hashes/blake3";
// Borsh schema only needs the NON-indexed fields:
class TransferEventData {
amount: bigint; // u128
}
const transferTopic = blake3("Transfer(address,address,uint128)");
for await (const event of subscription) {
// Match by signature hash at topic[0]
if (!uint8ArrayEqual(event.topics[0], transferTopic)) continue;
// Indexed fields come from topics[1..]:
const from = event.topics[1]; // 32-byte address (no padding for addresses)
const to = event.topics[2];
// Non-indexed fields come from Borsh-decoded data:
const { amount } = deserialize(event.data, TransferEventData);
console.log(`Transfer from ${hex(from)} to ${hex(to)} amount ${amount}`);
}
A wallet or explorer that doesn't statically know the event type can still decode it dynamically:
- Fetch the contract's
.wasmviapyde_getContractCode(addr) - Parse the
pyde.abicustom section to find the event matchingtopics[0] - The ABI declares which fields are indexed (→ pair them with
topics[1..]) and which are not (→ Borsh-decode them fromdata) - Render the typed event with field names and values
14.5 Authors are free to use a different encoding
The data field is opaque to the chain. An author who has reason to use a custom encoding (raw bytes for ultra-simple events, Protobuf for cross-team consistency, etc.) is free to do so. The cost: SDK consumers must write custom decoders for those events; standard wallet preview / explorer tooling won't auto-decode them.
The recommendation stands: use Borsh unless you have a specific reason not to.
15. Event storage, indexing, and subscriptions
This section specifies how events emitted via pyde::emit_event (§7.5) and pyde::parachain_emit_event (§8.3) are committed on-chain, stored at each node, indexed for query, and delivered to real-time subscribers.
15.1 Per-overlay buffering during execution
Each per-tx overlay (see §3 of Chapter 3) maintains its own ordered events buffer alongside its state writes. Calls to emit_event append to the current top-of-stack overlay's buffer.
On overlay merge (success): parent.events.extend(child.events)
On overlay discard (revert): child.events dropped along with state writes
This means: events from a reverted (sub-)call are not committed. A top-level tx that reverts emits zero events. A cross_call'd sub-call that traps loses its events when its overlay is discarded; if the parent then succeeds, only the parent's pre-call events plus its post-call events (if any) survive.
The wave's final events list = the topmost overlay's events buffer at wave commit time, with positions assigned as (wave_id, tx_index, event_index) in canonical order.
15.2 On-chain commitment
Every wave commit record includes both an events_root (deterministic Merkle commitment) and an events_bloom (probabilistic summary).
#![allow(unused)] fn main() { struct WaveCommitRecord { wave_id: u64, anchor_hash: VertexHash, state_root: (Blake3Hash, Poseidon2Hash), // unchanged from Ch 4 events_root: Blake3Hash, // NEW: see §15.2.1 events_bloom: [u8; 256], // NEW: 2048-bit, see §15.2.2 included_txs: Vec<TxHash>, tx_count: u32, events_count: u32, // total events in this wave gas_used: u128, } }
The wave commit record is what the committee threshold-signs as part of the HardFinalityCert. events_root and events_bloom therefore inherit consensus-level integrity.
15.2.1 events_root
A binary Merkle tree over the wave's events in canonical order:
leaf_i = Blake3(borsh_encode(EventRecord_i))
node = Blake3(left || right)
events_root = top of tree (padded with zero-leaves to next power of two)
For a wave with zero events:
events_root = [0u8; 32] (sentinel — no events to commit)
Light client inclusion proof: to prove "event E was emitted in wave W", a light client needs:
- The wave's
HardFinalityCertcontaining the signedevents_root. - The
EventRecorditself. - A Merkle proof from the event's leaf position to the root (log₂(events_count) hashes).
Proof verification: recompute the leaf hash, walk the proof to reconstruct the root, compare against the cert's events_root. If equal, the event is provably committed to that wave.
Cost per event ~32-byte hash; cost per wave ~few hundred μs (events_count is typically thousands at most, not millions). Negligible compared to wave-commit fixed costs.
Future ZK extension: v2 may add a events_root_poseidon2 parallel field for ZK-circuit-friendly proofs, mirroring the dual-hash state-root pattern (Chapter 4 §4.1b). v1 ships Blake3 only.
15.2.2 events_bloom
A 256-byte (2048-bit) bloom filter over the wave's events. Used for cheap "did any event matching X happen in wave W?" queries without fetching the event list.
For each event in the wave:
for each topic in event.topics: // 1 to 4 topics per event
insert(bloom, topic)
insert(bloom, event.contract_addr) // 32-byte contract address
insert(bloom, item):
h1 = blake3(item)[..8] mod 2048
h2 = blake3(item)[8..16] mod 2048
h3 = blake3(item)[16..24] mod 2048
bloom.set_bit(h1)
bloom.set_bit(h2)
bloom.set_bit(h3)
Three hash functions, 2048-bit filter. Expected false-positive rate at typical wave loads:
| Events per wave | False-positive rate |
|---|---|
| 100 | ~0.001 % |
| 1,000 | ~1 % |
| 5,000 | ~17 % |
| 10,000 | ~52 % |
At v1 honest throughput (~10-30K TPS plaintext, most txs not emitting events), a typical wave has <2,000 events and the bloom is highly selective. At peak load it becomes less useful but never lies (no false negatives). Historical query (§15.4) uses the bloom as a pre-filter and the indexes for exact matches.
15.3 Per-node storage layout
Three RocksDB column families. Big-endian numeric encoding throughout so RocksDB's lexicographic iterator order matches numeric order.
events_cf (primary store)
key: wave_id (8 BE) || tx_index (4 BE) || event_index (4 BE)
value: borsh_encode(EventRecord)
EventRecord {
wave_id: u64,
tx_index: u32,
event_index: u32,
contract_addr: [u8; 32],
topics: Vec<[u8; 32]>, // 1 to 4 topics; topic[0] = signature hash
data: Vec<u8>,
}
events_by_topic_cf (index)
key: topic (32) || wave_id (8 BE) || tx_index (4 BE) || event_index (4 BE)
value: () // empty — the key contains all the lookup info
Prefix scan with topic_X → all events whose ANY topic equals X, in wave order.
An event with N topics writes N rows to this CF (one per topic value).
events_by_contract_cf (index)
key: contract_addr (32) || wave_id (8 BE) || tx_index (4 BE) || event_index (4 BE)
value: ()
Prefix scan with contract_X → all events from that contract, in wave order.
Atomicity: on every wave commit, the engine writes one RocksDB WriteBatch containing all three CFs' updates plus the wave commit record. Atomic: either all three indexes update together or none does.
Write cost per event: 1 + topics_count + 1 RocksDB puts — one primary, one per topic, one contract index. At sustained ~2,000 events/wave with an average of ~2 topics each, that's ~8,000 puts/wave, which RocksDB handles in single-digit ms with the existing PIP-4 write-back cache architecture (Chapter 4).
15.4 Historical query
JSON-RPC method pyde_getLogs(filter):
#![allow(unused)] fn main() { struct GetLogsRequest { from_wave: u64, // inclusive to_wave: u64, // inclusive; capped: to_wave - from_wave ≤ 5,000 topics: [Option<Vec<[u8;32]>>; 4], // positional filter; index i matches event.topics[i]. // Some(list) at position i: event's i-th topic must be IN the list // None at position i: any value at that position (or absent) contract: Option<[u8; 32]>, // None = any contract cursor: Option<EventCursor>, // continuation from prior page; None = start fresh limit: u32, // max events to return; default 100, max 1,000 } struct EventCursor { wave_id: u64, tx_index: u32, event_index: u32, } struct GetLogsResponse { events: Vec<EventRecord>, next_cursor: Option<EventCursor>, // None = exhausted; Some = call again with this cursor } }
Filter semantics (positional, EVM-style):
match(event, filter) =
(filter.contract == None OR event.contract_addr == filter.contract) AND
(filter.from_wave == None OR event.wave_id >= filter.from_wave) AND
for each position i in 0..4:
if filter.topics[i] == None: skip (any value matches)
else if event.topics.len() <= i: NOT a match (event missing this position)
else: event.topics[i] must be IN filter.topics[i] (OR-list within a position)
Examples:
# "All Transfer events":
filter.topics = [Some([Blake3("Transfer(address,address,uint128)")]), None, None, None]
# "All Transfer events FROM address 0xAB...CD":
filter.topics = [
Some([Blake3("Transfer(...)")]),
Some([padded(0xAB...CD)]),
None,
None,
]
# "Either Transfer OR Approval from contract X":
filter.topics = [
Some([Blake3("Transfer(...)"), Blake3("Approval(...)")]),
None, None, None,
]
filter.contract = Some(contract_X)
Query plan:
- Validate the request:
to_wave - from_wave ≤ 5,000; per-position list size ≤ 8;limit ≤ 1,000. - Wave-level bloom prefilter: for each wave in
[from_wave, to_wave], load the wave's commit record and test theevents_bloomagainst every concrete value in the filter (any positional topic OR the contract). Drop waves with no bloom hit. - Per-wave exact lookup: for surviving waves, pick the most selective filter element to drive the scan:
- If a specific position has a single topic value: scan
events_by_topic_cffor that value, then post-filter results against the remaining positional constraints + contract. - If no topic but contract is set: scan
events_by_contract_cfprefixcontract || wave_id, then post-filter against topic positions. - If multiple values at one position: scan each, merge sorted union.
- If a specific position has a single topic value: scan
- Stream results in canonical order until
limitis reached, buildingnext_cursorto point to the next event past the limit. - Return the page + cursor.
Subsequent pages: client calls pyde_getLogs again with the same filter and the returned cursor. Server resumes scanning past the cursor.
Ordering is wave-ascending only in v1. Descending order is a v2 minor bump if needed.
15.5 Real-time subscription
JSON-RPC method pyde_subscribe({method: "logs", filter}) over WebSocket:
#![allow(unused)] fn main() { struct LogSubscription { topics: [Option<Vec<[u8;32]>>; 4], // positional filter (same shape as pyde_getLogs) contract: Option<[u8; 32]>, from: Option<EventCursor>, // for resume-on-reconnect; None = live from now } }
Engine behavior:
- On subscribe: add
(subscription_id, LogSubscription)to in-memory registry; iffromis provided, replay from disk via the historical-query machinery until caught up to the current wave, then transition to live. - On every wave commit (after the wave's events land in disk): for each active subscription, walk the wave's events, match against the filter, push matches as
LogEventNotificationrecords over the WebSocket. - On disconnect: drop subscription from registry. Subscriber must
pyde_subscribeagain on reconnect (withfromcursor if it wants to resume from a specific position).
#![allow(unused)] fn main() { struct LogEventNotification { subscription_id: SubscriptionId, event: EventRecord, // includes (wave_id, tx_index, event_index) for dedup } }
Delivery guarantees:
- Post-commit only. Subscribers receive events only after the event's wave has committed. No "pending event" notifications.
- Canonical order. Events arrive in
(wave_id, tx_index, event_index)order. Subscribers can dedupe by cursor since each event carries its position. - At-least-once. If the WebSocket disconnects mid-push, the subscriber must reconnect and use
fromcursor to resume from a known-processed position. The engine does not track which events a specific subscriber acknowledged; subscribers reconcile via cursor.
Filter syntax (positional, EVM-style): identical to pyde_getLogs (§15.4). Per-position topic constraints are AND'd; within each position, multiple values are OR'd; the contract filter is AND'd on top.
This covers EVM-equivalent filtering ("Transfer events from address X to anyone", "Approval OR Transfer events on token Y", etc.) and gives indexers parity with what they're used to.
15.6 Retention
Events follow the same retention tiering as state (Chapter 4):
| Node tier | Events retention |
|---|---|
| Archive | Forever |
| Full node | Last 90 days |
| Committee validator | Last 30 days |
| Light client | No primary storage; verifies inclusion proofs against signed events_root |
Pruning: at every epoch boundary, the engine sweeps events_cf, events_by_topic_cf, and events_by_contract_cf together, removing entries with wave_id < (current_wave - retention_waves). Lockstep — never partial. The wave commit records themselves are retained per the wave-commit retention policy (longer than events; needed for chain-of-trust during state sync).
15.7 Light client model
A light client doesn't store events. It can:
- Verify a specific event exists: given an
EventRecord(fetched from any full node) plus the wave'sHardFinalityCertplus a Merkle proof toevents_root, verify the event is committed to a finalised wave. - Probabilistically check existence: given just the wave's
HardFinalityCert, checkevents_bloomfor a topic/contract match. False-positive rate per §15.2.2. - Subscribe to live events: connect to a full node's
pyde_subscribe. Trust the node's stream (or verify each event with an inclusion proof for high-stakes cases).
15.8 Cross-parachain event isolation
Events from parachain_emit_event (§8.3) are recorded with the parachain's parachain_id in their contract_addr field (parachains and contracts share the address space; see PARACHAIN_DESIGN.md §4). Subscribers filter on contract_addr = parachain_id to listen for a specific parachain's events.
No separate parachain-events column family — they share the same events_cf / events_by_topic_cf / events_by_contract_cf machinery as ordinary contract events. The bloom filter aggregates both. The Merkle root commits to both. Parachain events are queryable identically.
15.9 Implementation notes for wasm-exec
Reference flow for the engine implementation (pseudocode):
#![allow(unused)] fn main() { // During tx execution fn host_emit_event( mut caller: Caller<'_, HostState>, topics_ptr: i32, topics_count: i32, data_ptr: i32, data_len: i32, ) -> i32 { // 1. Validate + gas if topics_count < 1 || topics_count > 4 { return ERR_INVALID_INPUT; } if data_len > MAX_EVENT_DATA_SIZE { return ERR_INVALID_INPUT; } let gas = EMIT_EVENT_BASE_GAS + 50 * topics_count as u64 + 8 * data_len as u64; if caller.consume_fuel(gas).is_err() { return ERR_OUT_OF_GAS; } if caller.data().view_mode { return ERR_FORBIDDEN; } // 2. Read topics + data from WASM memory let memory = /* get exported memory */; let total_topic_bytes = (topics_count as usize) * 32; let mut topics_buf = vec![0u8; total_topic_bytes]; memory.read(&caller, topics_ptr as usize, &mut topics_buf)?; let topics: Vec<[u8; 32]> = topics_buf .chunks_exact(32) .map(|c| { let mut t = [0u8; 32]; t.copy_from_slice(c); t }) .collect(); let mut data = vec![0u8; data_len as usize]; memory.read(&caller, data_ptr as usize, &mut data)?; // 3. Append to the current overlay's events buffer let event = EventRecord { wave_id: caller.data().current_wave, tx_index: caller.data().tx_index, event_index: caller.data().overlay_top().events.len() as u32, contract_addr: caller.data().self_address, topics, data, }; caller.data_mut().overlay_top_mut().events.push(event); 0 } // At wave commit fn finalize_wave_events(wave: &mut WaveCommit) { let all_events = wave.collect_committed_events(); // walks committed overlays wave.events_count = all_events.len() as u32; // Build bloom — every topic + contract_addr of every event let mut bloom = [0u8; 256]; for e in &all_events { for topic in &e.topics { bloom_insert(&mut bloom, topic); } bloom_insert(&mut bloom, &e.contract_addr); } wave.events_bloom = bloom; // Build Merkle root over canonical-ordered events let leaves: Vec<Blake3Hash> = all_events.iter() .map(|e| blake3_hash(&borsh::to_vec(e).unwrap())) .collect(); wave.events_root = merkle_root_blake3(&leaves); // Write to disk (atomic batch with state + wave commit) let mut batch = WriteBatch::new(); for e in all_events { let primary_key = (e.wave_id, e.tx_index, e.event_index).encode_be(); batch.put_cf(events_cf, primary_key, borsh::to_vec(&e).unwrap()); // One row per topic in events_by_topic_cf for topic in &e.topics { let topic_key = (topic, e.wave_id, e.tx_index, e.event_index).encode_be(); batch.put_cf(events_by_topic_cf, topic_key, &[]); } let contract_key = (e.contract_addr, e.wave_id, e.tx_index, e.event_index).encode_be(); batch.put_cf(events_by_contract_cf, contract_key, &[]); } db.write(batch).expect("atomic events write"); // Notify subscribers (positional filter match per §15.5) for (sub_id, sub) in subscription_registry.iter() { for e in &wave.events { if matches(e, &sub.filter) { websocket_push(sub_id, LogEventNotification { subscription_id: sub_id, event: e.clone() }); } } } } }
15.10 Open items deferred to v2
- Address-list filters. v1 supports one contract per subscription. v2 could allow
contracts: Vec<Address>(OR-list of contracts). - Descending wave queries. v1 returns events ascending only. v2 could add
direction: Ascending | Descending. - events_root_poseidon2. ZK-friendly parallel root for the events tree, mirroring the dual-hash state-root pattern. v2 work; not on v1 critical path.
- Indexed wildcards / set matching on contract. v1 contract filter is a single optional address. v2 could allow set membership and contract-name pattern matching.
Note: multi-topic native (up to 4 topics per event with EVM-style indexed-field marking) ships at v1 — see §14.1 for the encoding and §15.3-§15.5 for storage / query / subscription.
16. Conformance test surface
A conformance test suite — implementation of which is post-mainnet hardening work — must exercise every function in §7 and §8 with:
- Valid inputs returning expected outputs
- Each error code's trigger condition
- Each gas cost (charged before execution begins)
- Memory bounds at the WASM limits (0, 1, 64 MB - 1, 64 MB boundary)
- Each forbidden-import case at deploy time
- Determinism: run the same input on 128 simulated validators; outputs must match bit-for-bit
The conformance test suite ships in the post-pivot engine repo under wasm-exec/tests/conformance/. It is run as part of CI on every wasm-exec commit and as a gate on protocol upgrades that touch this spec.
17. Evolution & deprecation policy
17.1 Adding a new function (minor version bump)
- PIP describing the new function: signature, semantics, gas cost, error codes, use case.
- PIP review + acceptance per Chapter 15 — Governance.
- Engine implements the function under a
pyde_abi_v1_<N+1>feature gate. - New function is callable only by modules declaring
pyde_abi_version >= 1.(N+1). - Modules built against earlier versions continue executing unchanged.
17.2 Changing existing function semantics (NOT permitted)
Existing function semantics, gas costs, and error codes are frozen at v1.0 mainnet. Any change requires a v2.0 major bump, which is a hard fork.
If a v1.x function is discovered to have an implementation bug that diverges from this spec, the engine is patched to match the spec. If a v1.x function is discovered to have a spec bug (the spec itself is wrong), the spec is amended, the engine is patched to match the corrected spec, and the change is documented in the Migration Notes as a clarification (not a new function and not a major bump).
17.3 Reserving for v2
Functions known to be useful but requiring substantial design work (e.g., a streaming I/O abstraction, an account-abstraction policy invocation primitive, session-key authorization hooks) are not added to v1. They are tracked on the Roadmap under "Beyond V1" and ship as part of v2 when ready.
17.4 Per-language SDK alignment
Pyde does not ship per-language SDKs (see PARACHAIN_DESIGN §10). Community-maintained Rust, AssemblyScript, Go (TinyGo), and C/C++ bindings against this spec are encouraged. Each binding library is responsible for translating this spec's WAT signatures into idiomatic language-native function declarations; the canonical example projects shipped with otigen demonstrate the expected wrapping for each language.
18. References
- Chapter 3 — Execution Layer — conceptual overview, wasmtime config, per-tx overlay model
- Chapter 5 — Otigen Toolchain — how authors declare host imports in their language of choice
- Chapter 10 — Gas and Fee Model — fuel-to-gas mapping, EIP-1559, no-refund policy
- Chapter 13 — Parachains — parachain framework overview
- companion/PARACHAIN_DESIGN.md — full parachain design + ABI extension rationale
- companion/PERFORMANCE_HARNESS.md — gas-table calibration authority
- companion/THREAT_MODEL.md — security review of every host function
- WebAssembly Core Specification — the WASM ISA itself
- wasmtime documentation — the runtime Pyde uses
Document version: 0.1 (draft for v1 mainnet)
License: See repository root
Pyde Otigen Toolchain Binary Specification
Version: v1.0 (draft) Status: Authoritative for v1 mainnet. Subject to revision until mainnet genesis; frozen at v1 launch and only extended in backwards-compatible ways thereafter.
This document is the canonical specification of the otigen developer toolchain binary — the command-line program contract authors use to scaffold projects, drive language-specific builds, validate against the chain ABI, sign and submit deploys, manage wallets, and interact with running networks.
Where HOST_FN_ABI_SPEC.md defines the binary surface between the WASM execution layer and contract code, this document defines the surface between the author and the chain.
If the implementation and this document disagree, this document is authoritative. Implementation bugs are bugs in otigen, not in the spec.
For the narrative overview, see Chapter 5 — Otigen Toolchain.
1. Scope
This spec defines:
- The subcommand catalog — every
otigen X Y Zcommand, its flags, semantics, and exit codes - The
otigen.tomlschema — every key, type, default, and validation rule - The per-language build pipeline — exactly how
otigeninvokes Rust / AssemblyScript / Go / C compilers - The
pyde.abicustom-section injection — howotigenintegrates ABI metadata into the WASM output - The wallet integration — keystore format, FALCON signing pipeline, key rotation
- The deploy / upgrade / lifecycle flow — what transactions
otigensubmits and how - The artifact format — the deploy bundle structure (
.wasm+ manifest) - The network configuration — RPC endpoints, chain IDs, default gas
- The CI / scripting interface — JSON output mode, exit codes
- The versioning rules —
otigenbinary version vs chain ABI version compatibility
This spec does not define:
- The Host Function ABI (see HOST_FN_ABI_SPEC.md)
- Language compiler internals (those belong to upstream — rustc, asc, TinyGo, clang)
- The chain's transaction wire format (see Chapter 11 — Account Model)
- Per-language SDKs —
otigenis not an SDK; it's a build harness (see PARACHAIN_DESIGN §10 for the no-SDK rationale)
2. What otigen is and isn't
Is:
- A build harness: it invokes the language compiler the author already has installed, then post-processes the output WASM.
- A deploy client: it signs, submits, and tracks lifecycle transactions against a Pyde network.
- A wallet: it manages FALCON-512 keypairs in an encrypted keystore.
- A REPL: it offers an interactive shell for querying state, calling contracts, and debugging.
Is NOT:
- A language compiler.
otigendoes not parse Rust / AssemblyScript / Go / C. It calls the language's own compiler. - A language-specific SDK. There are no first-party Rust, TypeScript, AssemblyScript, etc. bindings shipped by
otigen. Author writesexterndeclarations against the Host Function ABI themselves; canonical example projects show the idiom. - An IDE. Authors use their language's standard IDE tooling (rust-analyzer, AssemblyScript LSP, gopls, clangd).
otigenis invoked from the command line or from a project'snpm run/cargo runscript. - A test runner. Authors use their language's native test command (
cargo test,npm test,go test).
3. Subcommand catalog
otigen <subcommand> [subsubcommand] [args] [flags]
All subcommands accept the global flags:
| Flag | Effect |
|---|---|
-v, --verbose | Verbose logging (also -vv for debug) |
-q, --quiet | Suppress non-error output |
--json | Output structured JSON (for CI / scripting) |
--network <name> | Override the default network (default: read from otigen.toml → [network.default]) |
--keystore <path> | Override the default keystore location (default: ~/.pyde/keystore.json) |
--config <path> | Override the default config path (default: ./otigen.toml) |
-h, --help | Show subcommand help |
3.1 otigen init
Scaffold a new project from a language template.
otigen init <name> --lang <rust|as|go|c> [--type <contract|parachain>] [--dir <path>]
| Arg | Required | Description |
|---|---|---|
<name> | yes | Project name. Used for the contract/parachain identity and the directory. |
--lang | yes | Target language: rust, as (AssemblyScript), go (TinyGo), or c (clang/wasm32). |
--type | no | contract (default) or parachain. Selects the appropriate scaffold. |
--dir | no | Target directory (default: ./<name>). |
Side effects:
- Creates
<dir>/. - Writes
<dir>/otigen.tomlfrom the language template (see §4 for schema). - Writes
<dir>/src/containing a hello-world contract withextern "C"declarations for one host function and one exported function. - Writes language-specific config (e.g.,
Cargo.tomlfor Rust,package.jsonfor AS,go.modfor Go). - Writes
.gitignoreexcludingtarget/,node_modules/,build/.
Exit codes: 0 on success, 1 if <dir> already exists, 2 if the language is unknown.
3.2 otigen build
Verify + package. Does not invoke the language compiler — that is the author's responsibility (run cargo build first, etc.).
otigen build [--release|--debug] [--out <path>]
| Flag | Default | Description |
|---|---|---|
--release | (default) | Validate against release-build expectations |
--debug | off | Allow debug-build artifacts (useful for local dev) |
--out | ./artifacts/ | Output directory for the deploy bundle |
Pipeline:
- Read
otigen.toml. Validate schema (§4). Validate attribute combinations per HOST_FN_ABI_SPEC §3.5.1. - Locate the compiled
.wasmat the path declared in[contract.lang.output]. - Validate the WASM:
- Well-formed binary (passes
wasmparserround-trip). - Every WASM import declares module
pyde(noenv, nowasi:*). - Every imported function name is in the HOST_FN_ABI_SPEC allowlist (and for non-parachain types, no parachain-only fn imports).
- Every function declared in
[functions.X]has a matching WASM export. - Every WASM export (other than internal helpers) is declared in
[functions.X]. - WASM features used are in the deterministic subset (no threads, no SIMD, etc.).
- Well-formed binary (passes
- Static call-graph view check. For each
viewfunction, build the transitive call graph from its body. If any reachable function importspyde::sstore/sdelete/transfer/emit_event/parachain_storage_write/parachain_storage_delete/parachain_emit_event, reject withBuildRejected: ViewMutatesState(<fn_name>, <mutating_import>). - Build
ContractAbistruct fromotigen.toml:- For each
[functions.X]: extract attributes, compute selector =Blake3(fn_name)[..4], copy access list. - For each
[events.X]: extract field list, computetopic_signature_hash = Blake3(canonical_signature), mark indexed fields. - Compute
state_schema_hash = Blake3(canonical_state_schema_bytes).
- For each
- Borsh-encode the
ContractAbi. - Inject the encoded ABI as a WASM custom section named
pyde.abi, using thewasm-encoderRust crate. The code section is untouched. - Write the bundle to
<out>/<contract_name>.bundle/:contract.wasm(with thepyde.abicustom section embedded)otigen.toml(verbatim copy)abi.json(human-readable mirror of the ABI for tooling)manifest.json(hashes, build timestamp, otigen version, target network)
Exit codes: 0 on success, 1 on validation failure, 2 if the .wasm was not found at the expected path.
3.3 otigen deploy
Sign and submit a deploy transaction.
otigen deploy [--network <name>] [--from <addr-or-keyname>] [--bundle <path>] [--dry-run]
| Flag | Default | Description |
|---|---|---|
--network | from otigen.toml | Target network |
--from | from otigen.toml | Deploying address or named key |
--bundle | ./artifacts/<name>.bundle/ | Path to the deploy bundle |
--dry-run | off | Validate + simulate only; do not submit |
Pipeline:
- Load bundle. Re-validate WASM + ABI consistency (defense in depth).
- Construct a
DeployTx:DeployTx { sender, name, // contract/parachain name wasm_bytes, // the .wasm with embedded pyde.abi contract_type, // Contract or Parachain initial_state_input, // calldata for the constructor (if any) nonce, gas_limit, gas_price, } - Compute canonical tx hash. FALCON-sign with the sender's key (prompts for keystore password unless cached).
- Submit via
pyde_sendRawTransaction. Print the tx hash. - (Optional) Wait for inclusion: poll
pyde_getTransactionReceiptuntil included. Report success / revert.
Exit codes: 0 on inclusion + success, 1 on validation failure, 2 on network error, 3 on revert.
3.4 otigen upgrade
Replace a contract's WASM via the upgrade flow.
otigen upgrade <name-or-address> [--network <name>] [--bundle <path>]
For contracts: submits an UpgradeContractTx signed by the contract owner.
For parachains: requires governance certs collected separately (per PARACHAIN_DESIGN §6.2). otigen upgrade --parachain runs the full vote flow if [parachain.governance.auto_collect] is true; otherwise the author submits the proposal, gathers votes externally, and runs otigen upgrade --finalize <proposal-id> to submit the activation tx.
3.5 otigen pause / otigen unpause / otigen kill
Operational lifecycle.
otigen pause <name-or-address> [--from <key>]
otigen unpause <name-or-address> [--from <key>]
otigen kill <name-or-address> [--from <key>] [--yes]
pause: owner-only. SubmitsPauseContractTx. Reversible.unpause: owner-only. SubmitsUnpauseContractTx.kill: owner-only, irreversible. Requires--yesto confirm. SubmitsKillContractTx.
3.6 otigen inspect
Read contract / parachain state and metadata.
otigen inspect <name-or-address> [--at-wave <wave_id>] [--field <name>]
Outputs:
- Contract type, name, owner, current version, total versions
- ABI summary (functions, events, state schema)
- Code hash, WASM size, deployment wave
- If
--field <name>is given: the current value of that storage field (uses ABI for type-safe decoding) - If
--at-waveis given: state as of that historical wave (archive nodes only)
3.7 otigen wallet
Wallet subcommands.
otigen wallet new [--name <label>] # generate a new FALCON keypair
otigen wallet list # list keys in keystore
otigen wallet show <name> # show address + public-key fingerprint
otigen wallet rotate <name> # initiate key rotation (submits KeyRotationTx)
otigen wallet import <path> # import an external keystore entry
otigen wallet export <name> # export a keystore entry (prompts for password)
otigen wallet password <name> # change a keystore entry's password
otigen wallet sign <name> <hex-message> # sign arbitrary bytes (for advanced use)
Keystore format: see §6.
3.8 otigen console
Interactive REPL against a Pyde node.
otigen console [--network <name>] [--from <key>]
REPL commands:
call <addr> <fn> <args...>— invoke a view function (free, off-chain)tx <addr> <fn> <args...>— submit a state-changing txevents <addr> [--topic <hash>] [--from <wave>]— query event historybalance <addr>— query balancestate <addr> <field>— query a state field (type-safe via ABI)subscribe <addr> --logs --topic <hash>— open a live event subscriptionhelp,exit
The console caches the contract ABI on first contact so subsequent calls are type-checked locally.
3.9 otigen verify
Verify that a published contract's bundled artifact matches its on-chain deployment.
otigen verify <name-or-address> [--bundle <path>]
Compares the local bundle's WASM bytes against the chain's stored bytes. Useful for confirming reproducible builds: if two builders run otigen build from the same source and toolchain versions, they should produce byte-identical bundles.
Exit codes: 0 on match, 1 on mismatch (with a diff summary).
4. otigen.toml schema
The canonical config file. Lives at the project root.
4.1 Top-level tables
[contract]
name = "my-token" # required; lowercase + hyphens (ENS-style; see §4.2)
version = "1.0.0" # required; semver
description = "Example token" # optional
type = "contract" # "contract" (default) or "parachain"
[contract.lang]
language = "rust" # required; rust | as | go | c
output = "target/wasm32-unknown-unknown/release/my_token.wasm" # required; Rust crate name uses snake_case (cargo convention), so the .wasm filename uses underscores even though the Pyde contract name uses hyphens
[contract.lang.toolchain]
rust_channel = "stable" # for rust
rust_toolchain = "1.75.0" # pinned toolchain
asc_version = "0.27.0" # for AS
tinygo_version = "0.30.0" # for go
clang_version = "17" # for c
[deploy]
gas_limit = 10_000_000 # default per-deploy gas budget
gas_price = "auto" # "auto" = use current base_fee; or fixed quanta
owner_deposit = 1000 # PYDE locked at deploy time (parachain only)
[wallet]
default_keystore = "~/.pyde/keystore.json"
default_account = "deployer" # name of the keystore entry to use by default
[network.default]
name = "testnet"
[network.mainnet]
rpc_url = "https://rpc.pyde.network"
chain_id = 1
explorer_url = "https://explorer.pyde.network"
[network.testnet]
rpc_url = "https://rpc-testnet.pyde.network"
chain_id = 2
explorer_url = "https://explorer-testnet.pyde.network"
[network.devnet]
rpc_url = "http://localhost:9933"
chain_id = 31337
[state]
# State schema; each entry declares a top-level state field name and its type.
schema = [
{ name = "owner", type = "address" },
{ name = "total_supply", type = "uint128" },
{ name = "balances", type = "mapping(address -> uint128)" },
]
[functions.transfer]
attributes = ["entry", "payable"]
inputs = ["address", "uint128"]
outputs = ["bool"]
access_list = [
"balances[caller()]", # informational; runtime computes hashes
"balances[args.0]",
]
[functions.balance_of]
attributes = ["entry", "view"]
inputs = ["address"]
outputs = ["uint128"]
access_list = ["balances[args.0]"]
[functions.init]
attributes = ["constructor"]
inputs = ["uint128"]
[events.Transfer]
signature = "Transfer(address,address,uint128)"
fields = [
{ name = "from", type = "address", indexed = true },
{ name = "to", type = "address", indexed = true },
{ name = "amount", type = "uint128" },
]
[events.Approval]
signature = "Approval(address,address,uint128)"
fields = [
{ name = "owner", type = "address", indexed = true },
{ name = "spender", type = "address", indexed = true },
{ name = "amount", type = "uint128" },
]
4.2 [contract] keys
| Key | Type | Required | Default | Validation |
|---|---|---|---|---|
name | string | ✅ | — | 1-32 chars, lowercase alphanumeric + -; matches ENS-style naming (see Chapter 11) |
version | string | ✅ | — | semver |
description | string | ❌ | empty | ≤ 200 chars |
type | enum | ❌ | "contract" | "contract" or "parachain" |
4.3 [contract.lang] keys
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
language | enum | ✅ | — | "rust", "as", "go", "c" |
output | path | ✅ | — | Path (relative to project root) where the language compiler writes the .wasm |
The [contract.lang.toolchain] subtable holds language-specific version pins. otigen build does not invoke the compiler — it only validates that the output .wasm exists. But it records the declared toolchain in the bundle manifest for reproducibility.
4.4 [functions.<name>] keys
| Key | Type | Required | Default | Validation |
|---|---|---|---|---|
attributes | array of strings | ✅ | — | Any subset of view, payable, reentrant, sponsored, constructor, fallback, receive, entry. Subject to compatibility rules per HOST_FN_ABI_SPEC §3.5.1 |
inputs | array of strings | ❌ | [] | Parameter types in declaration order |
outputs | array of strings | ❌ | [] | Return types in declaration order |
access_list | array of strings | ❌ | [] | Informational state slot patterns; the runtime computes the actual hashes |
A function declared in [functions.X] must have a matching WASM export named X. The reverse must also hold (no orphan exports), unless the export name starts with _ (internal helper convention).
4.5 [events.<name>] keys
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
signature | string | ✅ | — | Canonical signature string (Solidity-style), e.g. "Transfer(address,address,uint128)". Must match the field types in declaration order. |
fields | array of tables | ✅ | — | Field metadata (name, type, indexed flag). See HOST_FN_ABI_SPEC §14.1. |
Each field entry:
| Key | Type | Required | Default |
|---|---|---|---|
name | string | ✅ | — |
type | string | ✅ | — |
indexed | bool | ❌ | false |
Rules (validated at otigen build):
- Up to 3 fields can be
indexed(so total topics, includingtopic[0]= signature hash, ≤ 4 — matches EVM LOG4). - The
signaturestring must, when parsed, yield exactly the field types in order.otigen buildcross-checks. - Event names are unique within a contract.
4.6 [state] table
Declares the contract's storage schema. The toolchain doesn't generate accessor code (that's the author's job per the no-SDK approach) — but the schema is embedded in the bundle and used for type-safe inspection (otigen inspect --field), for explorer UI rendering, and for the state_schema_hash value in the deployed ABI.
schema is an ordered array of { name, type } entries. Types follow the Solidity-token convention used in event signatures (§4.5).
4.7 [deploy] table
| Key | Type | Default | Notes |
|---|---|---|---|
gas_limit | u64 | 10_000_000 | Default gas budget for deploy/upgrade txs |
gas_price | string or u128 | "auto" | "auto" reads chain's current base_fee; explicit value is in quanta per gas |
owner_deposit | u128 | 0 | PYDE locked at deploy (parachain only; refunded on kill) |
4.8 [wallet] table
| Key | Type | Default |
|---|---|---|
default_keystore | path | ~/.pyde/keystore.json |
default_account | string | — |
4.9 [network.X] tables
Multiple networks can be declared. [network.default] names which is used when --network is not specified.
| Key | Type | Required | Notes |
|---|---|---|---|
rpc_url | URL | ✅ | JSON-RPC endpoint |
chain_id | u64 | ✅ | Per the HOST_FN_ABI_SPEC chain_id table |
explorer_url | URL | ❌ | For convenient link generation in console output |
[network.default] has only a name field that selects one of the other [network.*] tables as the default.
4.10 [parachain] table (parachain only)
For type = "parachain":
[parachain]
consensus_preset = "simple_bft" # or "threshold" or "optimistic"
min_validators = 7
quorum_threshold = "2/3"
[parachain.governance]
voting_period_days = 3
proposal_cooldown_days = 30
auto_collect = false # if true, `otigen upgrade` runs the full vote flow
[parachain.slashing]
preset = "standard" # minimal / standard / strict
See PARACHAIN_DESIGN for the semantics of each preset.
5. Per-language build pipeline
otigen does not invoke the language compiler. The author runs the language's own build command first (e.g., cargo build --target wasm32-unknown-unknown --release); otigen build then picks up the resulting .wasm and post-processes it.
This separation keeps otigen simple: it doesn't need to track language toolchain versions, manage compiler flags, or replicate package-manager behavior. The language ecosystem owns the build; otigen owns the chain-specific packaging.
The expected build commands per language (documented in canonical example projects):
| Language | Command | Output |
|---|---|---|
| Rust | cargo build --target wasm32-unknown-unknown --release | target/wasm32-unknown-unknown/release/<name>.wasm |
| AssemblyScript | npx asc src/main.ts -o build/contract.wasm --target release | build/contract.wasm |
| Go (TinyGo) | tinygo build -target=wasm-unknown -o build/contract.wasm | build/contract.wasm |
| C / C++ | clang --target=wasm32 -nostdlib -Wl,--no-entry -o build/contract.wasm src/*.c | build/contract.wasm |
The path in [contract.lang.output] tells otigen build where to find the .wasm. If absent, the build fails with BuildRejected: WasmNotFound(<expected_path>).
5.1 Toolchain pinning
[contract.lang.toolchain] declares which compiler version the contract was built against. otigen build does not enforce this (it doesn't invoke the compiler) but it records the values in the bundle manifest. otigen verify uses these values to detect cross-toolchain drift.
6. pyde.abi custom-section injection
The mechanism by which otigen build integrates ABI metadata into the WASM artifact.
6.1 What gets embedded
A ContractAbi struct, Borsh-encoded.
The canonical shape is defined in HOST_FN_ABI_SPEC.md §3.7 — every byte the chain side reads at deploy time. The struct is deliberately lean: only what the chain's dispatch wrapper needs at runtime (per-function name + selector + attribute bitfield + access list, plus the schema hash + dispatch indices).
For reference, repeated here:
#![allow(unused)] fn main() { struct ContractAbi { pyde_abi_version: u32, // monotonic; matches engine's supported ABI version contract_type: ContractType, // Contract | Parachain functions: Vec<FunctionAbi>, state_schema_hash: [u8; 32], // Blake3 of canonical state-schema bytes constructor_index: Option<u32>, fallback_index: Option<u32>, receive_index: Option<u32>, } struct FunctionAbi { name: String, selector: [u8; 4], // = Blake3(name)[..4] attributes: u32, // bitfield (see HOST_FN_ABI_SPEC §3.5) access_list: Vec<String>, // declared state-slot access patterns } }
The lean shape is intentional. Two design decisions follow from it:
-
Events are not embedded in
pyde.abi. Event metadata (signature, indexed fields, topic-hash derivation) is a runtime convention: contracts callhost_emit_event(topics, data)and the chain stores topics + data verbatim. Wallets and indexers reconstruct event semantics from the event signature alone (the canonical encoding of which is documented in HOST_FN_ABI_SPEC §14.1). The bundle'sotigen.toml(shipped alongsidecontract.wasmper §9) carries the[events.X]declarations for tooling that wants the full picture. -
Function
inputs/outputsare not embedded either. The chain dispatches by selector — it does not need typed parameter or return-value metadata to invoke a function. Wallets that want to construct calldata from typed arguments read the bundle'sotigen.toml(or its richerabi.jsonmirror, per §9.3) which retains the[functions.X]inputs/outputslists.
If the implementation and this document disagree on the byte shape, HOST_FN_ABI_SPEC.md §3.7 is authoritative.
6.2 Injection mechanism
otigen build uses the wasm-encoder Rust crate (or equivalent) to inject a custom section into the .wasm:
#![allow(unused)] fn main() { use wasm_encoder::{CustomSection, Module}; let mut module = Module::new(); // ... copy all sections from the input WASM ... module.section(&CustomSection { name: "pyde.abi", data: borsh::to_vec(&contract_abi)?, }); let final_wasm: Vec<u8> = module.finish(); }
The code section is untouched — otigen does not modify a single executable byte. Only a new metadata section is appended.
6.3 Verification
On deploy, the chain's deploy validator parses the pyde.abi custom section and re-runs every check from otigen build §3.2 step 3-5 against the actual WASM bytes. This is defense in depth: a malicious author could hand-edit the pyde.abi section to bypass the build check, but the deploy validator would catch it.
See HOST_FN_ABI_SPEC §3.7 for the chain side of this contract.
7. Wallet integration
7.1 Keystore format
JSON file (default location ~/.pyde/keystore.json). One file holds multiple accounts:
{
"version": 1,
"accounts": {
"deployer": {
"address": "0xabcd...",
"pubkey": "...base64 FALCON-512 pubkey (~897 bytes)...",
"ciphertext": "...AES-256-GCM ciphertext of the FALCON secret key...",
"salt": "...random 16 bytes for Argon2id...",
"nonce": "...random 12 bytes for AES-GCM...",
"kdf": {
"name": "argon2id",
"memory_kb": 65536,
"iterations": 3,
"parallelism": 4
}
}
}
}
Decryption: key = Argon2id(password, salt, kdf_params); secret_key = AES-256-GCM-Decrypt(ciphertext, key, nonce).
7.2 Key generation
otigen wallet new runs:
- Generate a fresh FALCON-512 keypair via
pyde-crypto. - Prompt the user for a password.
- Derive
key = Argon2id(password, random_16_byte_salt, kdf_params). - Encrypt the secret key:
ciphertext = AES-256-GCM-Encrypt(secret_key, key, random_12_byte_nonce). - Compute the address:
addr = Poseidon2(falcon_public_key_bytes)(full 32 bytes, no truncation). Matches Chapter 11 §11.2 and theaddress-naming-collisionlocked-in derivation — every EOA on Pyde isPoseidon2(falcon_public_key_bytes). The input is the raw 897-byte FALCON-512 public key; the output is the full 32-byte Poseidon2 hash. - Append the entry to the keystore.
7.3 Signing pipeline
For every tx-submitting subcommand (deploy, upgrade, etc.):
- Build the canonical tx bytes per the chain's tx format (Chapter 11).
- Compute
tx_hash = Blake3(canonical_tx_bytes). - Load the keystore entry. Prompt for password (or use cached if
--cache-passwordwas passed). - Decrypt the secret key (§7.1).
signature = FALCON-512-Sign(tx_hash, secret_key).- Attach the signature + pubkey to the tx.
- Submit via JSON-RPC.
The decrypted secret key is held in memory only for the duration of the signing operation, then zeroized.
7.4 Hardware-wallet bridge
Out of scope for v1. The keystore is software-only.
Post-v1, a WalletBackend trait will allow hardware wallets (Ledger / Trezor / dedicated FALCON HSM devices) to be plugged in behind the same API. The [wallet] table will gain a backend = "hardware-ledger" | "hardware-trezor" | "software" field.
8. Deploy, upgrade, and lifecycle flow
8.1 Deploy transaction
DeployContractTx {
sender: [u8; 32],
name: String, // contract name (registered in name registry)
wasm_bytes: Vec<u8>, // .wasm with embedded pyde.abi
contract_type: ContractType,
init_calldata: Vec<u8>, // calldata for the constructor (if any)
deploy_fee: u128,
nonce: u64,
gas_limit: u64,
gas_price: u128,
sig: FalconSignature,
pubkey: FalconPubkey,
}
Chain handling on DeployContractTx:
- FALCON-verify the signature.
- Validate nonce, balance for
deploy_fee + gas_limit × gas_price. - Parse the
pyde.abicustom section fromwasm_bytesand validate (per HOST_FN_ABI_SPEC §3.7). - Register the contract name. Compute the contract address (see Chapter 11).
- Store
wasm_bytesin state at the contract's code slot. - If a constructor is declared, instantiate the WASM and invoke the constructor with
init_calldata. - Emit a
ContractDeployedevent.
8.2 Upgrade transaction
For contracts (single-signer):
UpgradeContractTx {
sender: [u8; 32], // must be the contract owner
contract_addr: [u8; 32],
new_wasm: Vec<u8>,
nonce, gas_limit, gas_price, sig, pubkey,
}
Chain validates owner authorization, re-runs ABI parsing/validation against new_wasm, stores it, and bumps current_version.
For parachains: the upgrade requires governance certs (per PARACHAIN_DESIGN §6.2). The full proposal → vote → finalize flow is documented there.
8.3 Pause / Unpause / Kill transactions
All owner-only. Submitted as simple txs (PauseContractTx, UnpauseContractTx, KillContractTx). No special governance required.
9. Artifact format
9.1 The deploy bundle
otigen build produces a directory:
./artifacts/<contract_name>.bundle/
contract.wasm # WASM binary with embedded pyde.abi custom section
otigen.toml # verbatim copy of the source config
abi.json # human-readable ABI mirror
manifest.json # build metadata
9.2 manifest.json
{
"version": 1,
"name": "my-token",
"contract_type": "contract",
"build_timestamp": "2026-05-23T16:42:00Z",
"otigen_version": "1.0.0",
"pyde_abi_version": "1.0.0",
"target_chain_id": 1,
"wasm_hash_blake3": "0xabcd...",
"wasm_size_bytes": 152384,
"wasm_size_bytes_uncompressed": 152384,
"pyde_abi_hash_blake3": "0x1234...",
"language": "rust",
"language_toolchain": {
"rust_channel": "stable",
"rust_toolchain": "1.75.0"
}
}
9.3 abi.json
The same ContractAbi data structure as the embedded pyde.abi custom section, but serialized as JSON for human inspection and IDE / explorer tooling. Authoritative source is the embedded custom section; abi.json is a mirror.
9.4 Reproducibility
Two builders running otigen build from the same:
- Source code
otigen.toml- Language toolchain version
otigenversion
should produce byte-identical contract.wasm and manifest.json (modulo build_timestamp). otigen verify exists to confirm this property.
10. Diagnostics and CI mode
10.1 Verbose mode
-v shows informational logs (which file is being read, which step is running). -vv adds debug-level logs (HTTP requests, key derivation timings, etc.).
10.2 JSON output mode
--json causes every subcommand to emit one JSON object per logical event, one per line (NDJSON-style):
{"event": "build_start", "name": "my_token", "ts": "2026-05-23T16:42:00Z"}
{"event": "validation_passed", "checks": ["wasm_well_formed", "imports_allowed", "abi_consistent"]}
{"event": "abi_injected", "bytes_added": 1840}
{"event": "bundle_written", "path": "./artifacts/my_token.bundle/"}
{"event": "build_success", "duration_ms": 248}
CI / scripting consumers parse this stream. Human readers see a friendlier format by default (omit --json).
10.3 Exit codes
Standardized across all subcommands:
| Code | Meaning |
|---|---|
0 | Success |
1 | Validation / logic failure (bad config, ABI inconsistency, etc.) |
2 | Resource failure (file not found, network unreachable, etc.) |
3 | Transaction failure (revert, gas exhausted, sub-call failed) |
4 | Wallet failure (bad password, missing keystore entry, etc.) |
5 | Authorization failure (signing party not authorized) |
64 | Unhandled internal error (should not occur in a correct implementation; report as a bug) |
10.4 Error message format
Errors include a structured prefix for easy parsing:
otigen [ERROR] BuildRejected: ViewMutatesState
function: transfer
reason: reachable via call graph from `do_transfer_internal`
mutating: pyde::sstore at offset 0x4a2
see: HOST_FN_ABI_SPEC.md §3.7 step 4
11. Versioning and compatibility
11.1 otigen binary version
otigen itself follows semver (MAJOR.MINOR.PATCH):
- MAJOR: breaking CLI / config-schema changes
- MINOR: new subcommands, new flags, new schema fields (backwards-compatible)
- PATCH: bug fixes
11.2 ABI compatibility
otigen emits a pyde_abi_version field in the bundle. The chain refuses to accept a deploy whose declared ABI is newer than the chain's supported ABI. See HOST_FN_ABI_SPEC §2.
Cross-version matrix:
| otigen | chain ABI | Compatible? |
|---|---|---|
| 1.0.x | 1.0 | ✅ |
| 1.1.x | 1.0 | ✅ (otigen down-targets to 1.0 if pyde_abi_version = "1.0.0" in otigen.toml) |
| 1.0.x | 1.1 | ✅ (chain supports older modules) |
| 2.0.x | 1.x | ⚠️ otigen 2.x defaults to ABI v2.0; users can --target-abi 1.x to downgrade |
11.3 Schema migration
When otigen introduces a new otigen.toml key in a minor version, existing configs continue to work (the new key is optional with a sensible default). otigen init produces the latest schema.
When otigen introduces a required new key, that's a MAJOR bump; otigen migrate exists to upgrade old configs.
12. References
- Chapter 5 — Otigen Toolchain — narrative overview
- Chapter 17 — Developer Tools — what tools authors use day-to-day
- HOST_FN_ABI_SPEC.md — the chain-facing ABI this toolchain builds against
- PARACHAIN_DESIGN.md — parachain-specific concerns (no-SDK rationale, governance, etc.)
- Chapter 11 — Account Model — address derivation, tx wire format
wasm-encodercrate — the WASM section-writerotigenuses
Document version: 0.1 (draft for v1 mainnet)
License: See repository root
Pyde Implementation Plan
Version 0.1 — written 2026-05-23 after design phase completion.
This document is the coordination artifact for implementing Pyde. The design phase is done (the rest of the book + the locked specs in this companion/ directory define the protocol). This document defines:
- Who builds what — three parallel work streams, with strict crate ownership
- In what order — five sequential phases (MC-0 through MC-5)
- Against what specs — every stream points at its canonical authoritative doc
- How to avoid clashes — interface contracts frozen at Phase 0; branching protocol; coordination rules
If this document and any other artifact disagree on implementation logistics (who owns what, branching rules), this document wins. If this document and a design spec (HOST_FN_ABI_SPEC, etc.) disagree on protocol semantics, the design spec wins.
For the roadmap with checklist-level tracking, see roadmap.md. For the design philosophy ("v1 ships interfaces, v2 ships implementations") see the memory entry v2_roadmap_and_room.
1. The three-session model
Pyde's v1 implementation is structured as three parallel work streams, each owning its own clear scope. The streams are designed to be independently parallelizable — the only synchronization point is at integration time (MC-2).
| Stream | Codename | Repository | Primary spec | What it builds |
|---|---|---|---|---|
| α | Toolchain | pyde-net/otigen (new) | OTIGEN_BINARY_SPEC.md | The otigen developer-tool binary: build, deploy, wallet, console |
| β | Execution | pyde-net/engine (new), branch execution-side | HOST_FN_ABI_SPEC.md, Chapter 4, PIPs 2/3/4 | The WASM execution layer: state, account, tx, mempool, wasm-exec |
| γ | Consensus | pyde-net/engine, branch consensus-side | Chapter 6, SLASHING.md, VALIDATOR_LIFECYCLE.md, STATE_SYNC.md, CHAIN_HALT.md, NETWORK_PROTOCOL.md | Consensus + networking + node binary |
Each stream is meant to be assignable to a single Claude session or a single human contributor and run independently for weeks at a time without coordination beyond the locked interface contracts (§4).
2. Five-phase execution timeline
MC-0 — INTERFACE FOUNDATION [SEQ — me]
│
▼
MC-1 — PROTOCOL CORE [PAR — three sessions]
│ ├─ Stream α (toolchain)
│ ├─ Stream β (execution)
│ └─ Stream γ (consensus)
│
▼
MC-2 — INTEGRATION [SEQ]
│ Merge β + γ branches; bring up local devnet
│
▼
MC-3 — STATE SYNC + PARACHAIN ACTIVATION [SEQ]
│
▼
MC-4 — PERFORMANCE + FAILURE HANDLING [PAR within]
│
▼
MC-5 — VALIDATION + MAINNET LAUNCH [SEQ]
Phase summaries in §3 below. Detailed checklists per phase live in roadmap.md.
3. Phase plan
3.1 MC-0 — Interface Foundation (sequential, ~1 day)
The prerequisite to safe parallelism. Without MC-0 complete, the three streams clash on shared types and interface drift.
Deliverables:
- Fresh
pyde-net/enginerepo created on GitHub + cloned locally. - Cargo workspace skeleton with stubs for every crate listed in §5.
typescrate fully written. Every type used across crate boundaries lives here, frozen at end of MC-0. Includes:Address,SlotHash,Value,Balance,Nonce,Tx,TxHash,Receipt,StateRoot(Blake3 + Poseidon2),EventRecord,WaveId,Round,VertexHash,Vertex,WaveCommitRecord,HardFinalityCert,FalconPubkey,FalconSignature, error codes per HOST_FN_ABI_SPEC §4.interfacescrate fully written. The cross-crate traits that decouple β and γ:trait StateView— read-only state access (used by mempool validation, view-call execution)trait StateMutator— apply a wave's worth of writes atomicallytrait Executor— invoke a tx (called by consensus when committing a wave)trait MempoolView— what consensus reads from the mempooltrait NetworkView— gossipsub send/recv abstractiontrait ConsensusEngine— the consensus loop the node binary drives- Each trait ships with a mock implementation so β and γ can write tests in isolation.
- CI baseline —
.github/workflows/ci.ymlrunscargo build,cargo test,cargo clippy --workspace -- -D warnings,cargo fmt --all -- --checkon every PR. - Branching protocol documented (§6).
- Initial commit tagged
phase-0-foundation.
Owner: main session (current context). The user does not need to spin up parallel sessions until MC-0 ships.
Bar to advance to MC-1: phase-0-foundation tag landed on main; CI green; types and interfaces crates pass their own unit tests; all crate stubs compile.
3.2 MC-1 — Protocol Core (parallel, three streams)
The three streams (α, β, γ) work concurrently against the locked Phase 0 foundation.
Stream α — Toolchain (pyde-net/otigen repo)
Implements OTIGEN_BINARY_SPEC.md end-to-end. Independent of engine internals — only depends on the locked Host Function ABI spec to validate WASM modules. Specific deliverables in §3.2 of the spec; first milestone is otigen build working against the canonical Rust hello-world contract.
Crates (in pyde-net/otigen workspace):
otigen-cli— the binaryotigen-toml— config parser + schema validationotigen-abi—pyde.abicustom-section construction + injection (viawasm-encoder)otigen-rpc— JSON-RPC clientotigen-wallet— keystore (Argon2id + AES-256-GCM) + FALCON-512 signing- (later)
otigen-console— REPL
External dependencies:
pyde-crypto(sibling polyrepo) — FALCON, Argon2id, AES-GCM, Borshwasmparser,wasm-encoder(Bytecode Alliance) — WASM inspection + custom-section writingclap— CLI frameworkserde,toml— config parsingreqwest,tokio-tungstenite— HTTP + WebSocket
Stream β — Engine Execution (pyde-net/engine, branch execution-side)
Crates owned:
account— 32-byte addresses,AuthKeysenum (withProgrammablev2 reservation), 16-slot nonce window, name-registry interfacestate— JMT dual-hash,state_cf+jmt_cf+events_cf+events_by_topic_cf+events_by_contract_cf, PIP-2 clustered keys, PIP-3 prefetch, PIP-4 write-back cache, snapshot generationtx— transaction types (Transfer, ContractCall, ContractDeploy, ValidatorRegister, Multisig, etc.), canonical hashing, gas accounting, deploy/upgrade/lifecycle handlerswasm-exec— wasmtime engine config (deterministic feature subset),WasmExecutor, every host function from HOST_FN_ABI_SPEC §7-§8, module cache, fuel-to-gas mapping, per-tx overlaymempool— FALCON-verify pipeline, validation rules, gossip admission, gas-bond logic
Spec map:
HOST_FN_ABI_SPEC— every host function this stream implements- Chapter 4 — state model + dual-hash JMT
- PIPs 2, 3, 4 — state optimizations
- Chapter 11 — account model, tx wire format
- Chapter 10 — gas + fee model
- Chapter 3 — execution layer architecture, per-tx overlay
Stream γ — Engine Consensus + Networking (pyde-net/engine, branch consensus-side)
Crates owned:
consensus— Mysticeti DAG, vertex/round/anchor/wave logic, BFS subdag walk, slashing evidence collection, equivocation detection, missing-vertex fetchnet— libp2p + QUIC + Gossipsub, peer discovery (layered, no DHT), sentry-node pattern, vertex-fetch protocoldkg— Pedersen DKG protocol (or import frompyde-cryptoif it lands there first)slashing— validator state machine, the 10-offense catalog, slashing escrow, jail mechanics, reward distributionnode— the binary, JSON-RPC server, validator role,consensus_storewithset_sync(true), persistence
Spec map:
- Chapter 6 — Mysticeti DAG consensus
SLASHING.md— full 10-offense catalogVALIDATOR_LIFECYCLE.md— registration, bonding, unbonding, jailSTATE_SYNC.md— snapshot mechanics, chain-of-trustCHAIN_HALT.md— halt detection, recovery pathsNETWORK_PROTOCOL.md— libp2p config, topics, peer scoring- Chapter 12 — networking
- Chapter 16 — security (cross-references throughout)
MC-1 BAR: Each of α / β / γ runs cargo build && cargo test clean on their branch. The β + γ branches build and link against the frozen types + interfaces crates. Mock-based integration tests pass within each stream.
3.3 MC-2 — Integration (sequential)
Merge β and γ branches to main. Bring up a local devnet (4-7 validators on a single machine) producing sub-second commits with end-to-end tx flow:
- Author writes a contract (with α's otigen), builds locally, deploys via
otigen deploy. - Tx submitted to RPC, validated by mempool (β), batched, gossipped (γ), included in vertex (γ).
- Anchor commits, subdag walks (γ), wasmtime executes (β).
- State updates (β), state_root signed (γ),
HardFinalityCertformed (γ). - Receipt queryable via RPC; event subscription pushes notifications.
Coordinated by the main session. Both β and γ contributors review the merge PRs. Owner of the integration milestone: γ (since node crate lives there).
MC-2 BAR: Local devnet running end-to-end. All MC-1 deliverables integrated. Performance is correct (functional), not yet measured (that's MC-4).
3.4 MC-3 — State Sync + Parachain Activation (sequential)
Add the two protocol-level extensions that depend on MC-2 being functional:
- State sync — snapshot generation, weak-subjectivity checkpoints, fresh-validator flow. Spec:
STATE_SYNC.md. - Parachain framework activation — parachain registry, deployment + lifecycle, cross-parachain messaging, governance flow. Spec:
PARACHAIN_DESIGN.md.
Owner: shared between β and γ as the changes touch both sides. Coordinated by the main session.
3.5 MC-4 — Performance + Failure Handling (parallel within)
- Performance harness build-out — multi-region workload generation, soak testing, "claim 1/3 of measured peak" discipline. Spec:
PERFORMANCE_HARNESS.md. - Chaos / failure injection — failure-scenarios catalog walkthroughs (
FAILURE_SCENARIOS.md). - Chain halt recovery drills —
CHAIN_HALT.mdplaybooks executed in test environments.
3.6 MC-5 — Validation + Mainnet Launch (sequential)
- Five external audits (consensus, execution layer, cryptography, networking, otigen toolchain).
- Incentivized testnet (multi-month soak with reference dApps + bug bounty at mainnet tier).
- 128-validator genesis ceremony.
- Mainnet launch.
Spec map: Chapter 19 (Launch Strategy).
Mainnet ships when the validation work passes — not before, not on a calendar.
4. Crate ownership map
The load-bearing table of this document. Every crate has exactly one owning stream. No co-ownership.
pyde-net/engine (one repo, β and γ collaborate via branches)
| Crate | Owner | Branch | Depends on |
|---|---|---|---|
types | MC-0 (frozen) | main | (none — leaf crate) |
interfaces | MC-0 (frozen) | main | types |
account | β | execution-side | types, pyde-crypto |
state | β | execution-side | types, interfaces |
tx | β | execution-side | types, account, state, pyde-crypto |
wasm-exec | β | execution-side | types, interfaces, state, account, tx |
mempool | β | execution-side | types, interfaces, account, tx |
consensus | γ | consensus-side | types, interfaces, pyde-crypto |
net | γ | consensus-side | types, interfaces |
dkg | γ | consensus-side | types, pyde-crypto |
slashing | γ | consensus-side | types, interfaces |
node | γ | consensus-side | (all of the above) |
pyde-net/otigen (separate repo, α owns entirely)
| Crate | Owner | Depends on |
|---|---|---|
otigen-cli | α | all otigen-* crates below |
otigen-toml | α | serde, toml |
otigen-abi | α | wasmparser, wasm-encoder, borsh |
otigen-rpc | α | reqwest, tokio-tungstenite |
otigen-wallet | α | pyde-crypto |
pyde-net/pyde-crypto (existing polyrepo)
Already in place. Both engine streams + α import from it. Out of scope for new implementation work in MC-1 — only additions (DKG, PSS) added as needed.
Top-level files in pyde-net/engine
| File | Owner | Notes |
|---|---|---|
Cargo.toml (workspace) | MC-0 initially; stream adds its own dep entries | Avoid editing other streams' sections |
README.md | γ | Stream γ owns the binary so it owns documentation |
.github/workflows/ci.yml | MC-0 initially; both streams may extend their respective test stages | |
LICENSE, SECURITY.md, .gitignore | MC-0 initially | Edits via coordinated PR |
5. Interface contracts (high-level)
The traits in engine/crates/interfaces/src/lib.rs. Frozen at end of MC-0. Changes after that require a coordinated PR from both β and γ + main session approval.
#![allow(unused)] fn main() { // engine/crates/interfaces/src/lib.rs (sketch — full impl in MC-0) use pyde_engine_types::{ Address, SlotHash, Value, Balance, Tx, TxHash, Receipt, StateRoot, EventRecord, WaveId, WaveCommitRecord, Vertex, VertexHash, HardFinalityCert, }; /// Read-only state access. Implemented by `state::StateStore`. /// Used by `mempool` for validation, `wasm-exec` for sload, RPC for queries. pub trait StateView { fn get_slot(&self, slot: &SlotHash) -> Option<Value>; fn get_balance(&self, addr: &Address) -> Balance; fn get_nonce(&self, addr: &Address) -> u64; fn get_code_hash(&self, addr: &Address) -> Option<[u8; 32]>; fn state_root(&self) -> StateRoot; } /// Wave-level state mutation. Implemented by `state::StateStore`. /// Used by `consensus` to apply a committed wave's writes. pub trait StateMutator: StateView { fn begin_wave(&mut self, wave_id: WaveId); fn execute_tx(&mut self, tx: &Tx) -> Receipt; fn finalize_wave(&mut self) -> WaveCommitRecord; } /// Tx invocation. Implemented by `wasm-exec::WasmExecutor`. /// Used by `consensus` to execute committed txs. pub trait Executor { fn execute(&mut self, tx: &Tx, state: &mut dyn StateMutator) -> Receipt; } /// Mempool query. Implemented by `mempool::Mempool`. /// Used by `consensus` to pull txs into batches. pub trait MempoolView { fn drain_for_batch(&mut self, max_bytes: usize) -> Vec<Tx>; fn insert(&mut self, tx: Tx) -> Result<TxHash, MempoolError>; fn contains(&self, hash: &TxHash) -> bool; } /// Network gossip. Implemented by `net::Network`. /// Used by `consensus` for vertex / batch / share dissemination. #[async_trait] pub trait NetworkView: Send + Sync { async fn publish_vertex(&self, vertex: Vertex); async fn publish_batch(&self, batch: Batch); fn subscribe_vertices(&self) -> Receiver<Vertex>; fn subscribe_batches(&self) -> Receiver<Batch>; fn fetch_vertex(&self, hash: VertexHash) -> Future<Option<Vertex>>; } /// The consensus loop. Implemented by `consensus::ConsensusEngine`. /// Driven by `node` binary. #[async_trait] pub trait ConsensusEngine: Send { async fn run( &mut self, state: &mut dyn StateMutator, executor: &mut dyn Executor, mempool: &mut dyn MempoolView, network: &dyn NetworkView, ); } }
Each trait ships with a mock implementation in interfaces/src/mock.rs so each stream can write isolated tests:
#![allow(unused)] fn main() { // interfaces/src/mock.rs pub struct MockStateView { /* HashMap-backed */ } pub struct MockMempool { /* VecDeque-backed */ } pub struct MockNetwork { /* channel-backed */ } // ... etc. }
6. Branching + coordination protocol
6.1 Branching
main ← integration branch; both streams merge here
├── execution-side ← stream β's long-lived branch
└── consensus-side ← stream γ's long-lived branch
- Each stream merges to
mainweekly minimum (more often is fine). - Each merge is a PR with CI green; one reviewer (the other stream's session or zarah).
- After every weekly merge, each stream rebases its branch onto the latest
main.
6.2 Tagged checkpoints
phase-0-foundation— end of MC-0phase-1-α-milestone-N— α stream milestonesphase-1-β-milestone-N— β stream milestonesphase-1-γ-milestone-N— γ stream milestonesphase-2-integration-bar— local devnet running end-to-endphase-3-state-sync-live,phase-3-parachain-activationphase-4-perf-harness-baseline,phase-4-chaos-drills-passedphase-5-audit-N-passed,phase-5-mainnet-launch
6.3 Coordination rules
- No edits to
typesorinterfacescrates after MC-0 without a coordinated PR signed off by both other streams. - Crate ownership is exclusive. β does not touch γ's crates; γ does not touch β's. If a need arises, raise it as an issue first, agree on which side owns the change, then PR.
- Shared dependencies update via coordinated PR. Bumping wasmtime, libp2p, etc. is a top-level PR reviewed by both streams.
- Conflicts on
mainthat bisect crate ownership get reverted; original committer rebases.
6.4 Communication
- GitHub issues on
pyde-net/engineandpyde-net/otigenfor design questions, blocking dependencies, interface clarifications. - Spec ambiguity? Update the relevant spec in
pyde-net/pyde-bookvia PR. Both streams reference the updated spec. - Cross-stream blocker? Tag both streams' owning agents in an issue.
7. Session handoff prompts (paste-ready)
The three prompts below are designed to be self-contained — each prompt initializes a new Claude session with full context to start work on its assigned stream.
7.1 Stream α — Toolchain session prompt
# Pyde Session α — Otigen Toolchain Implementation
You're joining the Pyde Layer 1 blockchain project. Three parallel
implementation streams are running concurrently; you own Stream α
(the developer toolchain).
## What Pyde is
Post-quantum L1 (FALCON-512 sigs, Kyber-768 threshold encryption,
Poseidon2+Blake3 hashing). Mysticeti DAG consensus with 128/85 quorum
and sub-second commits. WASM execution via wasmtime. MEV-resistant
by structure. Pre-mainnet, solo-founder-led (zarah). Workspace at
/Users/victorsamuel/Documents/zarah/systems/rust/pyde-net/.
## Your stream
Implement the `otigen` developer toolchain binary in a fresh repo
`pyde-net/otigen`. The toolchain:
- Reads `otigen.toml` configs
- Validates compiled `.wasm` artifacts against the Host Function ABI
- Injects a `pyde.abi` custom section into the WASM
- Signs and submits deploy / upgrade / lifecycle transactions
- Manages FALCON-512 keystores
- Offers an interactive REPL
## Authoritative specs
In priority order:
1. `pyde-book/src/companion/OTIGEN_BINARY_SPEC.md` — your canonical
spec (819 lines, 12 sections). Every command, every config key,
every validation rule.
2. `pyde-book/src/companion/HOST_FN_ABI_SPEC.md` — the chain-facing
ABI you validate WASM modules against.
3. `pyde-book/src/companion/IMPLEMENTATION_PLAN.md` — coordination
doc; defines your scope + how to coordinate with streams β and γ.
4. `pyde-book/src/companion/PARACHAIN_DESIGN.md` — parachain-specific
extension surface (parachain deploy + cross-parachain messaging).
5. `pyde-book/src/chapters/05-otigen-toolchain.md` — narrative
overview (lighter; specs above are canonical).
6. `pyde-book/src/chapters/11-account-model.md` — transaction wire
format, address derivation.
## Constraints
- **No AI attribution anywhere** (commits, code, PRs). Work reads
as zarah's own.
- **No per-language SDK shipping with otigen** — by design (see
PARACHAIN_DESIGN.md §10). Canonical example contracts only.
- **otigen does NOT invoke language compilers.** Author runs
`cargo build` / `npx asc` / etc. themselves; otigen post-processes
the resulting `.wasm`.
- **Apache-2.0 license**, clippy clean, fmt applied, no `unwrap()`
on untrusted paths.
- **The `pyde.abi` custom section is the canonical ABI** — chain
stores only the `.wasm`; the section travels with the code.
## Setup
1. Check `/pyde-net/otigen/` exists locally and on
`github.com/pyde-net/otigen`. Create both if not.
2. Initialize a Rust workspace.
3. Sub-crates: `otigen-cli`, `otigen-toml`, `otigen-abi`,
`otigen-rpc`, `otigen-wallet`. (Names suggested; adjust if you
have a better structure.)
4. Depend on `pyde-crypto` (sibling polyrepo) for FALCON-512,
Argon2id, AES-256-GCM, Borsh.
## First milestone
`otigen.toml` parsing + the `otigen build` validation pipeline
(spec §4 + §3.2). This is the foundation everything else builds on:
1. Parse `otigen.toml` with full schema validation (use `serde` + `toml`).
2. Locate the compiled `.wasm` at the declared path.
3. Walk the WASM via `wasmparser` and run every check in spec §3.2.
4. Build the `ContractAbi` struct from parsed config + WASM exports.
5. Borsh-encode + inject `pyde.abi` custom section via `wasm-encoder`.
6. Write the deploy bundle to `./artifacts/<name>.bundle/`.
7. Test against a canonical example Rust hello-world contract.
## Coordination
- You're independent of streams β and γ; only common dependency is
the locked HOST_FN_ABI_SPEC.
- Open issues on `pyde-net/otigen` for spec ambiguity; ping zarah.
- When you reach `otigen deploy`, you'll need a devnet to test
against. By then streams β + γ should have one running.
## First action
Read OTIGEN_BINARY_SPEC.md end-to-end. Read chapter 5 for context.
Verify the workspace setup. Begin first-milestone work.
7.2 Stream β — Engine Execution session prompt
# Pyde Session β — Engine Execution Layer
You're joining the Pyde Layer 1 blockchain project. Three parallel
implementation streams are running concurrently; you own Stream β
(the execution layer of the engine).
## What Pyde is
Post-quantum L1 (FALCON-512 sigs, Kyber-768 threshold encryption,
Poseidon2+Blake3 hashing). Mysticeti DAG consensus with 128/85 quorum
and sub-second commits. WASM execution via wasmtime. MEV-resistant
by structure. Pre-mainnet, solo-founder-led (zarah). Workspace at
/Users/victorsamuel/Documents/zarah/systems/rust/pyde-net/.
## Your stream
Implement the execution side of `pyde-net/engine`. Crates you own:
- `account` — 32-byte addresses, AuthKeys enum, 16-slot nonce window
- `state` — JMT dual-hash, state_cf + jmt_cf + events_cf×3,
PIPs 2/3/4 (clustered keys, prefetch, write-back cache),
snapshot generation
- `tx` — transaction types, canonical hashing, gas accounting,
deploy/upgrade/lifecycle handlers
- `wasm-exec` — wasmtime config, every host function from
HOST_FN_ABI_SPEC §7-§8, module cache, fuel-to-gas mapping,
per-tx overlay execution model
- `mempool` — FALCON verify, validation rules, gossip admission
You work on branch `execution-side` of `pyde-net/engine`.
Stream γ (consensus side) works on branch `consensus-side` in the
same repo. **Do not touch γ's crates** (`consensus`, `net`, `dkg`,
`slashing`, `node`). Communicate cross-stream needs via GitHub
issues; do not edit interfaces or shared types unilaterally.
## Authoritative specs
In priority order:
1. `pyde-book/src/companion/HOST_FN_ABI_SPEC.md` — every host
function you implement. 2,154 lines, 18 sections. The chain side
of the WASM ⇄ chain boundary.
2. `pyde-book/src/companion/IMPLEMENTATION_PLAN.md` — coordination
doc (this stream's scope, crate ownership, branching protocol,
interface contracts).
3. `pyde-book/src/chapters/04-state-model.md` — JMT, two-table
architecture, events_cf, PIP-2/3/4.
4. `pyde-book/src/chapters/03-virtual-machine.md` — execution
layer architecture, per-tx overlay, native vs WASM tx types.
5. `pyde-book/src/chapters/11-account-model.md` — account types,
address derivation, tx wire format, nonce window.
6. `pyde-book/src/chapters/10-gas-and-fee-model.md` — gas
accounting, no-refund policy, EIP-1559 base fee.
7. `pyde-net/pips/pip-0002` (clustered keys), `pip-0003` (prefetch),
`pip-0004` (write-back cache).
## Constraints
- **No AI attribution anywhere** (commits, code, PRs).
- **Apache-2.0 license**, clippy clean, fmt applied, no `unwrap()`
on untrusted-input paths.
- Use the frozen `types` and `interfaces` crates (in MC-0) — do
NOT change them without a coordinated PR.
- `mempool` is yours; consensus reads from it via the
`MempoolView` trait. Do not let γ touch your crates.
- `wasm-exec` implements the host functions; the engine
registers them with wasmtime's `Linker`. Authoritative gas
costs in spec §10. Authoritative validation rules in spec §3.7.
## Setup
1. `pyde-net/engine` repo exists post-MC-0; clone it locally.
2. Check out the `execution-side` branch.
3. Verify the workspace skeleton with stub crates compiles.
## First milestone
Implement the `account` + `state` crates with full functionality
(no WASM execution yet — that comes next). Key deliverables:
- Address derivation: `Poseidon2(falcon_pubkey)` → 32-byte address.
- `AuthKeys` enum with `Single`, `MultiSig`, `Programmable`
variants (Programmable v2-reserved per ch 11 §11.5).
- 16-slot nonce window per account.
- JMT dual-hash (Blake3 + Poseidon2) state tree.
- Two-table architecture (`state_cf` + `jmt_cf`).
- Atomic WriteBatch commits.
- Implement the `StateView` and `StateMutator` traits from the
`interfaces` crate.
- Test against the mock implementations in interfaces/.
Once `account` + `state` are solid, move to `tx`, then `wasm-exec`,
then `mempool`.
## Coordination
- Open issues on `pyde-net/engine` for design questions.
- Merge to `main` weekly minimum after CI green + reviewer LGTM.
- Tag milestones: `phase-1-β-milestone-N` (1 = state, 2 = wasm-exec
basics, 3 = full host fn catalog, etc.).
- Cross-stream blockers: tag both stream agents in the issue.
## First action
Read HOST_FN_ABI_SPEC.md end-to-end. Read IMPLEMENTATION_PLAN.md.
Read chapters 03, 04, 11, 10. Verify branch + workspace state.
Begin with `state` crate (foundational; everything else builds on it).
7.3 Stream γ — Engine Consensus + Network session prompt
# Pyde Session γ — Engine Consensus + Network Layer
You're joining the Pyde Layer 1 blockchain project. Three parallel
implementation streams are running concurrently; you own Stream γ
(the consensus + network + node binary side of the engine).
## What Pyde is
Post-quantum L1 (FALCON-512 sigs, Kyber-768 threshold encryption,
Poseidon2+Blake3 hashing). Mysticeti DAG consensus with 128/85 quorum
and sub-second commits. WASM execution via wasmtime. MEV-resistant
by structure. Pre-mainnet, solo-founder-led (zarah). Workspace at
/Users/victorsamuel/Documents/zarah/systems/rust/pyde-net/.
## Your stream
Implement the consensus + networking side of `pyde-net/engine`.
Crates you own:
- `consensus` — Mysticeti DAG, vertex/anchor/wave logic, BFS subdag
walk, slashing evidence collection, equivocation detection,
missing-vertex fetch, threshold-decryption coordination
- `net` — libp2p + QUIC + Gossipsub, peer discovery (layered, no
DHT), sentry-node pattern, vertex-fetch protocol
- `dkg` — Pedersen DKG protocol (or thin wrapper if it lands in
pyde-crypto first)
- `slashing` — validator state machine, 10-offense catalog,
slashing escrow, jail mechanics, reward distribution
- `node` — the binary, JSON-RPC server, validator role,
`consensus_store` with `set_sync(true)`, persistence,
`panic = "abort"` on persist failure
You work on branch `consensus-side` of `pyde-net/engine`.
Stream β (execution side) works on branch `execution-side` in the
same repo. **Do not touch β's crates** (`account`, `state`, `tx`,
`wasm-exec`, `mempool`). Read from them via the locked
`interfaces` traits. Communicate cross-stream needs via GitHub
issues; do not edit interfaces or shared types unilaterally.
You own the `node` crate — it wires everything together at
integration time (MC-2).
## Authoritative specs
In priority order:
1. `pyde-book/src/companion/IMPLEMENTATION_PLAN.md` — coordination
doc (your scope, crate ownership, branching protocol, interface
contracts).
2. `pyde-book/src/chapters/06-consensus.md` — Mysticeti DAG,
anchor selection, wave commit, BFS subdag walk, threshold
decryption ceremony, HardFinalityCert.
3. `pyde-book/src/companion/SLASHING.md` — full 10-offense catalog.
4. `pyde-book/src/companion/VALIDATOR_LIFECYCLE.md` —
registration, bonding, unbonding, jail mechanics, key rotation.
5. `pyde-book/src/companion/STATE_SYNC.md` — snapshot mechanics,
chain-of-trust, weak-subjectivity checkpoints.
6. `pyde-book/src/companion/CHAIN_HALT.md` — halt detection, 5
recovery paths, bounded rollback.
7. `pyde-book/src/companion/NETWORK_PROTOCOL.md` — libp2p config,
Gossipsub topics, peer scoring, sentry pattern.
8. `pyde-book/src/chapters/12-networking.md` — networking detail.
9. `pyde-book/src/chapters/08-cryptography.md` — DKG, threshold
decryption, VRF (your consumer; pyde-crypto is the impl).
10. `pyde-book/src/companion/THREAT_MODEL.md` — security context.
## Constraints
- **No AI attribution anywhere** (commits, code, PRs).
- **Apache-2.0 license**, clippy clean, fmt applied, no `unwrap()`
on untrusted-input paths.
- Use the frozen `types` and `interfaces` crates from MC-0 — do
NOT change them without a coordinated PR.
- `consensus` reads txs from `mempool` via `MempoolView` (β owns
mempool). It invokes execution via `Executor` trait (β owns
wasm-exec). Don't reach into β's crates directly.
- All consensus-store writes use `WriteOptions::set_sync(true)`
per Chapter 16 §16.12. Persist failure = `panic = "abort"`.
## Setup
1. `pyde-net/engine` repo exists post-MC-0; clone it locally.
2. Check out the `consensus-side` branch.
3. Verify the workspace skeleton with stub crates compiles.
## First milestone
Implement the `consensus` crate with the Mysticeti DAG core:
- Vertex structure (round, member_id, parent_refs, batch_refs,
state_root_sigs, decryption_shares, prev_anchor_attestation, sig).
- Local DAG view (in-memory graph with vertex insertion + lookup).
- Round advancement (peer-attestation triggered, data-driven —
NOT clock-driven).
- Anchor selection: `Hash(beacon, round, prev_state_root) mod 128`.
- BFS subdag walk + canonical sort (round asc, member_id asc,
batch_list_order).
- Missing-vertex fetch protocol (async pull from peers).
- Anchor-skip handling (when anchor vertex absent).
- Test in isolation using `MockStateView`, `MockMempool`,
`MockNetwork` from the `interfaces` crate.
Then move to `net` (libp2p + Gossipsub topics), then `slashing`,
then wire it all up in `node`.
## Coordination
- Open issues on `pyde-net/engine` for design questions.
- Merge to `main` weekly minimum after CI green + reviewer LGTM.
- Tag milestones: `phase-1-γ-milestone-N` (1 = consensus core,
2 = network, 3 = slashing + lifecycle, 4 = node binary).
- Cross-stream blockers: tag both stream agents in the issue.
- You own integration: when MC-2 begins, you drive the merge +
devnet bring-up.
## First action
Read IMPLEMENTATION_PLAN.md. Read chapter 6 end-to-end (the spec
is dense and the BFS / anchor / threshold-decryption mechanics
are subtle). Read SLASHING.md + VALIDATOR_LIFECYCLE.md +
CHAIN_HALT.md. Verify branch + workspace state. Begin with
`consensus` crate (foundational for everything else in γ).
8. Risks + mitigations
| Risk | Severity | Mitigation |
|---|---|---|
Interface drift during MC-1 — β or γ realizes a needed change to interfaces mid-implementation | High | Both sides write tests against the locked traits early. If a change is genuinely required, both sides + main session co-sign the PR. |
types crate creep — new fields added ad-hoc as implementation reveals needs | High | All type additions are PRs against types crate, reviewed by both other streams. Pre-MC-1 we lock the "v1 type set" via thorough walk-through. |
| One stream lags substantially | Medium | Weekly merges to main make lag visible early. If γ lags, β still ships; integration happens when both are ready. No artificial gating. |
| Spec ambiguity blocks implementation | Medium | Open a PR against the relevant spec in pyde-net/pyde-book; both streams read updated spec from there. Treat spec as the contract. |
| Cross-stream blocker not surfaced | Medium | GitHub issue tags both stream agents; weekly merge reviews catch silent blockers. |
| Integration (MC-2) bigger than expected | Medium | γ owns the node crate from day one — eliminates a "who integrates" question. β provides clean trait implementations + tests that γ wires in. |
| Stream α blocked waiting on devnet | Low | α first milestone (otigen build) needs no chain; second milestone (otigen deploy) is when chain matters. By then β+γ should have devnet running. If not, α can mock-deploy against a stub RPC. |
9. Glossary of agreements
Quick reference for things the implementation must hold to:
typescrate is FROZEN at end of MC-0. No additions without coordinated PR.interfacescrate is FROZEN at end of MC-0. Same rule.- No co-ownership of crates. Each crate has one owning stream. Period.
- Weekly merges minimum. No long-lived branches diverging silently.
- No AI attribution. Anywhere. Per
no_ai_attributionmemory. - Apache-2.0 + clippy-clean + no untrusted
unwrap(). CI enforces. - Specs are authoritative. When code and spec disagree, the spec is right; either fix the code or update the spec via PR.
- Multi-topic events native at v1. Not a v2 deferral (per recent locked decision).
- View calls are free (RPC pyde_call AND on-chain cross_call_static). Bounded by
VIEW_FUEL_CAP. - Gas refunds: zero in v1. No exceptions.
10. References
- HOST_FN_ABI_SPEC.md — chain-facing ABI
- OTIGEN_BINARY_SPEC.md — toolchain spec
- PARACHAIN_DESIGN.md — parachain framework
- STATE_SYNC.md, CHAIN_HALT.md, SLASHING.md, VALIDATOR_LIFECYCLE.md, NETWORK_PROTOCOL.md, THREAT_MODEL.md, FAILURE_SCENARIOS.md, PERFORMANCE_HARNESS.md — operational specs
- roadmap.md — phase-by-phase checklist tracking
- The 20 book chapters + 4 PIPs — full design
Document version: 0.1 (draft for v1 mainnet)
License: Apache-2.0 + CC BY-SA 4.0 (per repository root)
Pyde Tokenomics
The PYDE token is the native asset of the Pyde blockchain. It is used for: gas payment, validator staking, governance signaling, and parachain operator bonds.
Total Supply & Genesis
- Total genesis supply: 1,000,000,000 PYDE
- Decimal places: 9 (1 PYDE = 10^9 quanta — see Chapter 14 for the full denomination ladder)
- Smallest unit: 1 quanta = 10^-9 PYDE
Initial Distribution (v1)
| Allocation | Amount | % | Vesting |
|---|---|---|---|
| Validator rewards pool | 200,000,000 | 20% | Released proportionally over 4 years via inflation |
| Treasury (multisig-controlled) | 150,000,000 | 15% | Released via governance proposals |
| Ecosystem grants | 100,000,000 | 10% | 4-year cliff for grantees |
| Public sale | 200,000,000 | 20% | Released at genesis to public buyers |
| Founders & early contributors | 150,000,000 | 15% | 4-year vesting, 1-year cliff |
| Investors | 200,000,000 | 20% | 4-year vesting, 1-year cliff |
Numbers above are illustrative starting points; final distribution requires legal review and stakeholder negotiation.
Inflation Schedule
| Year | Inflation rate | New PYDE minted |
|---|---|---|
| 1 | 5% | 50M |
| 2 | 3% | ~30M (compounding) |
| 3 | 2% | ~21M |
| 4+ | 1% (fixed) | ~10M/year thereafter |
Rationale: front-loaded inflation rewards early validators; fixed 1% tail provides long-term security budget without unbounded dilution.
Inflation accrues to the reward pool, distributed per the same rule as the fee share (see below).
Fee Model (EIP-1559 Style)
Every transaction has:
- Base fee: dynamically adjusted per block (EIP-1559 mechanism, target 50% block utilization)
- No priority tip. The encrypted mempool eliminates the information asymmetry that priority fees price. Priority would re-introduce ordering exploitation.
- Combined gas: for
cross_call!invocations (post-mainnet), Pyde-side + parachain-side gas billed in one transaction
Block Elasticity
- Target gas limit per block: 400M gas
- Maximum (4× elastic): 1.6B gas
- Base fee adjusts up when blocks are >50% full, down when <50%
- Adjustment factor: ±12.5% per block (EIP-1559 standard)
Per-Transaction Fee Flow
For every transaction's base fee:
100% of base_fee
├── 70% burned (deflationary pressure)
├── 10% to treasury (multisig-controlled)
└── 20% to the reward pool
├── 70% activity-weighted across active committee (= 14% of total)
│ • Vertices certified by ≥85 peers
│ • Batches included in committed waves (× tx count)
│ • Decryption shares submitted on time
│ • Anchor selections (uptime-correlated)
└── 30% flat across full stake pool (= 6% of total)
(every staked validator earns the base; activity bonus is layered on for those currently on the committee)
Plus inflation issuance (also flowing into the reward pool) distributed by the same rule.
Validator Staking
Bond Requirements
Single-tier staking:
- Minimum: 10,000 PYDE (
MIN_VALIDATOR_STAKE) — any validator meeting this threshold enters the pool from which the 128-member active committee is uniformly randomly selected each epoch - Maximum validators per operator: 3 (anti-Sybil cap, enforced on operator identity)
- Bonding period: 1 epoch (~3 hours) before active
- Unbonding period: 30 days (must exceed the 21-day safety evidence freshness window)
There is no separate "committee tier" with a higher floor. Pyde relies on threshold encryption + operator-identity cap + slashing for Sybil resistance, not on stake-size economics (see Chapter 16 §16.4 for the full security argument).
Staking Yield Estimate
Assume:
- 50% of supply staked → 500M PYDE
- Year 1 inflation: 50M PYDE → distributed to validators
- Activity rewards from fees: scales with chain usage
Estimated yield year 1:
Inflation share: 50M / 500M = 10%
Fee share: depends on chain activity
At low utilization: ~10-12% APY
At moderate utilization (target): ~12-15% APY
At high utilization: ~15-20% APY
Specific yields depend on actual network activity. Numbers above are illustrative; actual yields will be observable post-launch.
Active-Committee vs Awaiting-Selection Earnings
Every staked validator earns from the same pool; the difference is in the activity-weighted bonus while serving on the active committee.
| Status | Earnings Source |
|---|---|
| Validator on active committee | Base stake × uptime share of reward pool + activity-weighted committee bonus (vertices certified, batches included, anchor selections) + inflation share |
| Validator awaiting selection | Base stake × uptime share of reward pool + inflation share (no committee bonus until selected) |
Committee participation is per-epoch; over time, every validator qualifying for the pool will rotate onto the active committee proportionally and accrue activity bonuses then.
Slashing Economics
Slashing penalties (see SLASHING.md for full catalog):
| Offense | First instance | Max |
|---|---|---|
| Equivocation | 10% | 50% (correlation/repeat) |
| Bad state-root | 10% | 50% |
| Downtime | 0.05%/round | 10%/epoch |
Distribution of slashed amounts (safety offenses):
- 50% burned (irrecoverable, hurts attacker economics)
- 30% to treasury
- 20% to reporter (incentivizes monitoring)
Distribution of slashed amounts (liveness offenses):
- 100% burned (no reporter incentive needed; protocol auto-detects)
Treasury
The treasury accrues from:
- 10% of all transaction base fees
- Treasury portion of slashing (30% from safety offenses)
- Inflation allocation (if any portion designated)
Treasury spending is gated by M-of-N FALCON multisig (7-of-12 recommended) and is restricted to:
- Public goods grants (developer tools, audits, infra)
- Bug bounty payouts
- Emergency response (rare)
- Other purposes ratified by PIP (Pyde Improvement Proposal)
The treasury cannot be unilaterally drained — public PIPs + multisig threshold + 30-day-bounded emergency pause provide checks.
Parachain Operator Economics (Post-Mainnet)
Parachain operators stake PYDE as their bond and earn from the combined gas of every cross_call! invocation. The split is:
- 70% to parachain operator(s) providing the cross-chain service
- 20% to the Pyde-side reward pool (for executing the originating transaction)
- 10% burned (consistent with main fee model)
Parachain operators face their own slashing for misbehavior (incorrect responses, downtime), creating staked-honesty guarantees comparable to validators.
Token Velocity & Use
PYDE is intended to be used for transactions, staking, and bond, not held purely as speculative store-of-value. Mechanisms to encourage utility:
- Gas burn (70%): every transaction reduces supply, creating deflationary pressure when network usage is high
- Validator bond locking: 10K PYDE per validator slot, locked during operation
- Treasury spending: continually deploys PYDE into the ecosystem
- No priority tips: removes the speculative auction layer that creates token-velocity drag
Long-Term Sustainability
Post year-4, supply economics are:
- Inflation: ~1% per year (~10M PYDE)
- Burn rate: depends on usage; at 30K TPS sustained with mixed workload, estimated ~30-100M PYDE/year burned
At sustained moderate usage, the chain is net deflationary (burn > inflation). At low usage, slight inflation maintains validator security budget. At very high usage, deflationary pressure may eventually require fee structure adjustments (governance decision).
Open Questions
- Initial distribution percentages: above are illustrative; final allocations need legal + stakeholder negotiation.
- Investor terms: lockup, vesting, and post-vesting governance rights are open design questions.
- Treasury governance specifics: which categories of spending require which multisig thresholds — to be detailed in governance PIP.
- Parachain reward split: 70/20/10 above is starting point; may adjust based on operator economics post-mainnet.
References
- Fee flow: see WHITEPAPER.md §12
- Slashing details: see SLASHING.md
- Validator lifecycle: see VALIDATOR_LIFECYCLE.md
Version 0.1
Pyde Brand Reference
Version 0.1 · canonical brand guidance for the Pyde wordmark, glyph, and visual system.
This page is the source of truth for anyone (designers, contributors, integrators, ecosystem teams) using the Pyde name or mark. If you are about to make a logo lockup, a poster, a sticker, or a third-party landing page, read this first.
For the story behind the name and the mark, see How Pyde Works → What's in a name.
1. The name
Pyde — pronounced pied (rhymes with tide).
| Form | Use |
|---|---|
Pyde | Default, sentence-case. Use this almost everywhere — headings, prose, marketing copy. |
pyde | Lowercase in URLs, handles, file names, code identifiers (pyde-net, pyde.network). The X handle is @pydenet (the available short form). |
PYDE | Uppercase only when referring to the token / unit of account (100 PYDE, gas paid in PYDE). |
PYDE NETWORK (all caps) | Not used. Only in occasional design-led headings if it serves a layout, never in body copy. |
Do not:
- Write
pYde,PyDe,pydE, or any other mixed-case treatment. - Translate the name. It is
Pydein every language. - Spell it out as an acronym (
Programmable Yield Decentralized Engineand other backronyms). The name is not an acronym. Drop it from any third-party marketing that suggests it is.
Sentence patterns:
✓ Pyde is a sovereign L1.
✓ Send 100 PYDE to alice.pyde.
✓ pyde.network/docs
✗ The PYDE Network announces…
✗ pYde launches mainnet
2. The mark (glyph)
The mark is based on atomic structure — a nucleus and its orbital. Not a trend, not network imagery, not decorative. It looks like a physical law.
Anatomy:
- The vertical form is the core. Dense, gravitational, everything pulls toward it. Pyde is monolithic — consensus and execution in one place. Wide at the poles, compressed at the center: finality under pressure. Stress-tested and held.
- The circle to the right is in orbit. Independent, in motion, but bound to the core by an invisible force. External chains, bridges, light clients, portable finality certificates — they orbit. They are verified, not trusted.
- The two are separate on purpose. Related but sovereign. The composition is asymmetric — the orbital sits to the upper-right. Do not mirror, balance, or duplicate it. The core is fixed; the orbital can be anywhere.
Geometry:
- The orbital's diameter is about
0.40×the core's widest width. - The orbital's centre sits about
0.55×the core's height from the top, offset right by about0.85×the core's widest radius.
Guidance, not pixel rules. To recreate, eyeball against assets/logo.png.
3. Lockups
| Lockup | Use |
|---|---|
| Mark alone | Favicons, app icons, profile pictures, social avatars, watermarks, very small footprints. Default for any context under 32×32 px. |
| Mark + wordmark, horizontal | Website headers, presentation cover slides, partnership materials. Wordmark sits to the right of the mark, baseline-aligned to the mark's vertical centre. Space between mark and wordmark = 1.0× the mark's widest radius. |
| Mark + wordmark, vertical | Posters, stickers, merchandise. Wordmark sits below the mark, centered. Vertical space = 0.6× mark height. |
Clear space: the mark must always have clear space around it equal to 0.5× the mark's widest radius. No other graphic element (text, image, border) intrudes into this clear space.
Minimum size: the mark must not be rendered below 16×16 px. At sizes under 32×32 px, do not pair it with the wordmark.
4. Colour
The Pyde palette is black and white, with shades. Nothing more.
Restrained, calm, subtle — the visual posture matches the technical posture. The brand is meant to feel like a physical law: present, quiet, not asking for attention. Color noise doesn't belong here. The protocol is the product, not the palette.
| Token | Hex | Role |
|---|---|---|
--pyde-ink | #0d1117 | Primary dark — backgrounds, dark-theme surfaces, default body text in light mode. |
--pyde-shadow | #2a2f36 | Dark elevated — elevated surfaces on dark backgrounds, code-block fills. |
--pyde-mist | #7a8590 | Mid-gray — muted labels, captions, dividers. |
--pyde-veil | #e1e4ea | Light elevated — soft surfaces on light backgrounds, subtle dividers. |
--pyde-paper | #f7f8fa | Primary light — backgrounds in light mode, default body text in dark mode. |
These five grayscale tokens carry the entire brand. No accent palette. Color is not part of the brand.
Mark colouring rules:
- The mark is grayscale. The canonical rendering is the gradient version in
assets/logo.png. - A solid-black or solid-white version of the mark is acceptable for monochrome contexts (engraving, single-colour print, dark-on-light printing).
- Never recolor the mark. Not for theming, not for events, not for partnerships, not for special occasions.
Why grayscale only:
- The brand should feel like a physical law: present, calm, derived not designed.
- The mark is grayscale (a nucleus and its orbital, see §2). The palette mirrors that posture.
- A restrained palette stands out in a sea of colorful chains. Discipline reads as confidence.
- Color is reserved for one purpose only: the existing factory illustration (see §6), which predates this discipline and serves as a didactic diagram, not as brand surface.
5. Typography
Pyde uses system fonts. No custom typeface ships with the brand.
| Context | Font stack |
|---|---|
| Body, UI, code | -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif |
| Monospace (code, hashes, addresses) | ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace |
Why system fonts:
- Zero load time, perfect cross-platform rendering.
- Accessibility-first: respects the user's font-size preferences.
- No licensing complications for downstream community use.
- Consistent with Pyde's minimalism — the protocol is the product, not the typography.
When the brand needs a "voice" beyond system fonts (a presentation cover, a marketing illustration), use Inter (Bold or Black weight) for the wordmark only. Body type stays system.
6. The factory illustration system
The factory metaphor (see How Pyde Works) is a teaching illustration, not a brand surface. The animated SVG at src/assets/factory-loop.svg predates the grayscale-only discipline (§4) and retains its original color cues — droplets for transactions, an amber flash for wave commit, a green pillar for state, a gold lock for threshold encryption — as didactic shortcuts that help readers visualize Pyde mechanics at a glance.
These colors live in the animation only. They are not brand tokens. New illustrations default to the §4 grayscale palette; if differentiation beyond gray is needed, prefer pattern, opacity, line weight, or texture over color.
When making a new illustration:
- Use the §4 grayscale tokens.
- Lines are
1.4pxfor structural elements,0.6pxfor fine detail. - Corners are slightly rounded (
rx="2"is the default). - Animations loop on a 3-second cycle (matching the factory loop's tempo).
Diagrams that explain Pyde mechanics may reuse the factory's visual vocabulary in grayscale: droplets for transactions, boxes for vertices, pillars for state, smoke for eviction, a flash for wave commit.
7. Voice and tone
This is the brand's voice — apply it to any official copy or external comms.
| Quality | Means |
|---|---|
| Direct | Short sentences. Active voice. Avoid "we are excited to announce." Just announce. |
| Honest | Numbers are real numbers. "10–30K plaintext TPS on commodity hardware" is a Pyde-voice sentence; "Pyde achieves limitless throughput" is not. |
| Specific | "Mysticeti DAG with 128-validator committee, FALCON-512 signatures" beats "next-generation consensus." Name the thing. |
| Unpretentious | No "L1 of L1s," no "ushering in a new era." If a competitor would write it, don't. |
| Curious | When something is hard or undecided, say so. The audience is technical; treating them as adults builds trust. |
Examples:
✓ Pyde commits in waves on a 128-validator Mysticeti DAG. Encrypted transactions stay sealed until the wave commits.
✓ v1 ships realistic numbers, not aspirational ones. ~10–30K plaintext TPS on commodity hardware.
✗ Pyde is the world's first post-quantum, MEV-resistant, infinitely scalable Web3 platform of the future.
✗ We are revolutionizing how the world thinks about blockchain.
8. Asset inventory
The canonical brand asset directory is pyde-book/src/assets/ (also mirrored at pyde-book/assets/).
| File | Purpose |
|---|---|
logo.png | Canonical full-colour grayscale mark, 500×500 px. Default for digital use. |
factory-loop.svg | Animated illustration of the Pyde transaction lifecycle. The brand's visual vocabulary defined in motion. |
Pending (post-launch designer handoff):
logo.svg— vector source of the mark (currently only PNG exists).wordmark.svg— vector wordmark in Inter Bold.mark-monochrome.svg— single-colour version for engraving, single-colour print.social-card-default.png— Open Graph + Twitter Card default image.presentation-template.pptx/.key— slide deck template with the brand applied.
9. Third-party use
Community projects, ecosystem partners, and individuals are welcome to use the Pyde mark and name to refer to Pyde, subject to these rules:
Allowed without permission:
- Use the name "Pyde" to describe Pyde, in factual reference (news articles, tutorials, code documentation, third-party tooling that integrates with Pyde).
- Display the mark to indicate compatibility or integration ("works with Pyde," "deploys on Pyde").
- Reuse
assets/logo.pngandassets/factory-loop.svgat original aspect ratios.
Not allowed without permission:
- Use the name "Pyde" in your product name (
PydeWallet,PydeDeFi) in a way that implies official endorsement. - Modify the mark (recolour, distort, add elements, reshape).
- Use the mark on merchandise sold for profit at scale.
- Imply official affiliation with the Pyde Foundation when none exists.
For anything ambiguous, default to asking. There is no formal trademark registration at v1; the goodwill is community-held.
10. This document evolves
The brand is young. This document is a snapshot, not a contract. As Pyde matures and a dedicated designer joins, expect:
- A formal logo grid + construction guidance.
- A full type system (likely a custom display face for the wordmark).
- A motion-design spec beyond the factory loop.
- A photography / illustration direction for marketing.
When that work lands, this document gets revised and the version number bumps.
Document version: 0.1
License: See repository root
Pyde — Pitch Deck
The Problem
Every major Layer 1 in production today has at least one structural problem the rest of the decade will require fixing:
| Post-Quantum Secure | MEV-Free | Sub-second Final | Commodity Decentralized | |
|---|---|---|---|---|
| Ethereum | ❌ secp256k1/BLS | ❌ multi-billion $ extracted | ❌ ~12s | ⚠️ Tier-2 chains fragment |
| Solana | ❌ Ed25519 | ❌ proposer extracts | ✅ ~400ms | ❌ 12+cores/256GB+ |
| Bitcoin | ❌ secp256k1 | ✅ (slow blocks) | ❌ 60+ min | ✅ |
| Aptos / Sui | ❌ BLS12-381 | ⚠️ partial | ✅ ~400ms | ⚠️ medium |
No chain in production today is post-quantum + MEV-free + sub-second + commodity-validated.
The migration to PQ cryptography on existing chains is a multi-year coordinated upgrade — trillions of dollars of value, entrenched wallets, entrenched contracts. The chain built greenfield with PQ as default ships those properties at genesis without retrofitting.
The Solution: Pyde
A Layer 1 chain built from scratch with all four properties as defaults:
-
Post-Quantum cryptography by default
- FALCON-512 signatures (NIST-standardized) for every transaction
- Kyber-768 threshold encryption for the mempool
- Poseidon2 hashing where ZK matters
-
MEV resistance by structure, not policy
- Threshold-encrypted mempool: content invisible until ordered
- Commit-before-reveal: ordering finalized before decryption
- DAG consensus: no single proposer to exploit
-
Sub-second finality via DAG consensus
- Mysticeti-style DAG (Sui's production consensus)
- ~500ms median finality target
- No view changes (eliminates HotStuff's bug class)
-
Commodity hardware decentralization
- Full nodes / RPC: 8c/16GB/100Mbps (laptop-class)
- Equal-power voting within the committee
- Operator identity binding (no Sybil amplification)
Differentiation
| Ethereum | Solana | Sui | Pyde | |
|---|---|---|---|---|
| Post-Quantum | Migration path 5+ years out | No plan | No plan | Default at genesis |
| MEV | Auction (PBS) | Extracted by proposers | Some via Mysticeti | Structurally impossible |
| Finality | 12-15s | 400ms | 390ms | ~500ms |
| Commodity validator | Possible | No (12+ cores) | No (datacenter) | Yes (any validator awaiting committee selection) |
| Smart contract language | Solidity | Rust/Anchor | Move | Any wasm32-target language (Rust/AS/Go/C) with Pyde safety attributes preserved |
| Account abstraction | Retrofit (ERC-4337) | None native | Limited | Native (v2) |
| Cross-chain | Bridges (hacked $3B+) | Bridges | Bridges | Permissionless parachain layer (v2) |
| ZK readiness | Retrofit ongoing | Limited | Limited | Architecture ready (v2) |
Architecture Highlights
- Consensus: Mysticeti DAG with 128-validator committee, FALCON sigs, Kyber threshold encryption integrated at the order boundary
- Execution: WebAssembly via wasmtime, with Cranelift AOT and hybrid Block-STM + access list scheduling
- State: Jellyfish Merkle Tree (JMT) with dual Blake3 + Poseidon2 root commitment
- Language: Pyde safety attributes (reentrancy off by default, checked arithmetic, typed storage, no tx.origin, compile-time access lists) preserved as language-native attributes across Rust, AssemblyScript, Go, C
- Networking: libp2p + QUIC + Gossipsub
- Cross-chain: Permissionless parachain layer (post-mainnet) — open spec, multiple implementations, on-chain rules + slashing
Honest Status
Design: complete. 27+ design documents covering every subsystem.
Implementation: in progress.
- Execution layer (WASM via wasmtime, JMT with dual-hash and PIP-2 clustering): functional, needs extensions
- Cryptography (FALCON, Kyber base): in place; threshold variant in research
- Consensus (Mysticeti DAG): rebuild post-pivot
- Network (libp2p migration): planned
Mainnet ships when the work above is complete and the external audit passes. No public schedule.
Performance Targets (Honest)
Validated by multi-region production-realistic harness (mandatory before any external claim):
| Mode | v1 realistic | v2 stretch | Aspirational |
|---|---|---|---|
| Plaintext TPS (commodity) | 10–30K | 50–100K | 500K |
| Encrypted TPS (commodity) | 0.5–2K | 5–10K | 50K |
| Median finality | ~500ms | ~400ms | ~300ms |
Aspirational targets require GPU acceleration or batch-decryption research advances. Pyde will publish only numbers validated by the production-realistic harness, not microbenchmarks.
The Pivot Story
Pyde's earlier in-house HotStuff consensus suffered persistent wedges, stalls, and view-change cascades at 400ms slot timing. After patching accumulated technical debt without resolving the underlying protocol design problems, the team made a clean break in May 2026:
- Removed: consensus, mempool, networking from active workspace (archived as
archive/) - Replaced with: Mysticeti-style DAG consensus rebuild
- Refocused: execution layer + cryptography first, consensus rebuild on solid foundation
This is the chain built deliberately, not the chain rushed to mainnet. The pivot reflects an explicit commitment to safety over speed-to-market.
The Ask
Pyde is currently a solo project, with vision-first development → publish → community → stabilize → audit → mainnet.
Looking for:
- Cryptography collaborators — particularly post-quantum threshold encryption (the hardest piece)
- Consensus reviewers — Mysticeti DAG specialists for safety/liveness analysis
- Audit budget — $500K–$1M projected for v1 mainnet audit
- Grant funding — Ethereum Foundation, NIST, Polkadot for threshold PQ research
- Early ecosystem builders — wallets, block explorers, dApp developers willing to build on a pre-mainnet testnet
Contact
- Website: pyde.network
- Repository: github.com/pyde-net
- X: @pydenet
- Telegram: t.me/pydenet
- Email: info@pyde.network
- Documents: see the Companion Specifications section of The Pyde Book (Whitepaper, Threat Model, Performance Harness, Parachain Design, Brand, and more)
This is not vaporware. This is the architecture of the chain that exists for the next decade.
Version 0.1
Chapter 20: Appendix
Reference material from across the book in one place: the glossary, the constants tables, the discriminator registry, the JSON-RPC method index, and the post-mainnet roadmap.
A. Glossary
| Term | Definition |
|---|---|
| Pyde | The post-quantum L1 blockchain. Name of the protocol, the network, and the binary. |
| PYDE | The native token. 1 PYDE = 10^9 quanta. |
| otigen | Pyde's developer toolchain (the binary). Scaffolds projects, builds WASM artifacts, deploys, manages wallets. Name carried forward from the retired Otigen language. |
| WASM | WebAssembly. Pyde's execution layer; smart contracts and parachains compile to WASM and execute under wasmtime. |
| wasmtime | The WebAssembly runtime used by Pyde. Bytecode Alliance project, production-vetted at Microsoft / Fastly / Shopify. |
| Host Function ABI | The stable interface contracts use to interact with chain state (sload, sstore, transfer, threshold crypto, hashing, cross_call, etc.). See the Host Function ABI spec. |
| Cranelift | The code generator used by wasmtime for ahead-of-time WASM-to-native compilation. |
| Otigen (retired) | Was Pyde's domain-specific smart-contract language. Retired in the WASM pivot; see The Pivot preface. The Otigen Book is preserved as a historical artifact. |
| JMT | Jellyfish Merkle Tree. The state commitment structure (radix-16, path-compressed). |
| Blake3 | Fast bitwise hash. Used for JMT internals, batch hashes, vertex hashes, gossip de-dup. |
| Poseidon2 | Algebraic hash over the Goldilocks field. State root commit, addresses, MAC, VRF, ZK-bearing paths. |
| FALCON-512 | NIST FIPS 206 post-quantum signature scheme. ~666-byte sigs, 897-byte pks. |
| Kyber-768 | NIST FIPS 203 post-quantum KEM. P2P session keys and threshold mempool. |
| Threshold encryption | Mempool encryption such that any 85 of 128 committee members combine to decrypt. |
| PSS | Proactive Secret Sharing — refresh key shares without changing the public key. |
| DKG | Distributed Key Generation. Pedersen DKG ceremony each epoch for threshold pubkey. |
| VRF | Verifiable Random Function. Lattice-based; built from FALCON + Poseidon2. |
| Mysticeti | The DAG-based consensus protocol Pyde uses (post-May-2026 pivot, formerly HotStuff). |
| DAG | Directed Acyclic Graph. Every round, each committee member produces a vertex; parents must be strictly prior rounds. |
| Vertex | A committee member's per-round output: batch refs + parent refs + state-root sigs + decryption shares + FALCON sig. |
| Round | A ~150 ms DAG cycle. Each member produces one vertex per round. |
| Wave | The Mysticeti commit unit. Anchor at round R+3 commits the subdag rooted at round R. |
| Anchor | Deterministically-selected committee member whose round-R vertex commits the wave. Hash(beacon, round, recent_state_root) mod 128. |
| Worker / Primary | Narwhal pattern: workers gossip tx batches, primary produces vertices and runs consensus. |
| HardFinalityCert | ≥ 85 FALCON sigs over (wave_id, blake3_state_root, poseidon2_state_root). |
| Committee | The 128 active validators per epoch. Equal vote weight; uniform random selection. |
| Epoch | ~3 hours of waves. PSS resharing fires at epoch boundary. |
| Validator | Node staking ≥ MIN_VALIDATOR_STAKE (10,000 PYDE). Single tier — uniform-random committee selection picks 128 from the eligible pool each epoch. |
| Full node | Node that executes waves and serves RPC, but does not stake. |
| MEV | Maximal Extractable Value. The MEV class is structurally closed in Pyde. |
| Encrypted mempool | Optional Kyber-encrypted submission. Decryption deferred until after DAG anchor commit. |
| Commit-before-reveal | DAG anchor commits canonical ordering before threshold-decryption shares are released. |
| Hybrid scheduler | Execution model: static access lists (Solana-style) + Block-STM speculation (Aptos-style). |
| Sentry node | Public-facing proxy in front of a committee validator. Hides validator's real IP. |
| Treasury | The system account at Poseidon2("pyde-treasury"). Spent via on-chain multisig. |
| PIP | Pyde Improvement Proposal. Off-chain documents that drive code changes. |
| Multisig signers | The on-chain set authorized to spend the treasury (MULTISIG_SIGNERS). |
| Emergency pause | Multisig-authorized halt of non-Resume txs; max 30 days, auto-expiring. |
| Hard halt | Automatic chain halt on detected safety violation (state root divergence, equivocation cluster). |
| Weak-subjectivity checkpoint | Hard-finalized commit (wave_id + state_root + committee FALCON sigs) that a fresh node trusts to anchor sync. |
| Quanta | Smallest PYDE denomination. 1 PYDE = 10^9 quanta. |
| Access list | Per-tx declaration of state slots the tx will read or write. |
| Nonce window | 16-slot bitmap of in-flight nonces per account. |
| Gas tank | Per-account dedicated balance for sponsoring user transactions. |
| Paymaster | A contract that pays gas on behalf of a user, with custom validation logic. |
| Parachain operator | Permissionless v2 actor who stakes PYDE, fulfills cross_call! to other chains, earns gas fees. |
B. Network Constants
| Constant | Value | Where |
|---|---|---|
ROUND_PERIOD_MS | 150 (DAG round cadence) | consensus/round.rs |
COMMIT_TARGET_MS | 500 (median commit) | consensus/commit.rs |
EPOCH_LENGTH | ~3 hours of waves | consensus/epoch.rs |
COMMITTEE_SIZE (mainnet) | 128 | consensus/committee.rs |
THRESHOLD (2f+1) | 85 | consensus/quorum.rs |
EQUIVOCATION_THRESHOLD (n-2f) | 44 | consensus/quorum.rs |
RANDOMNESS_THRESHOLD | 85 (sorted before combine) | consensus/epoch_randomness.rs |
RESHARE_AGGREGATION_DELAY_WAVES | 5 | crypto/threshold.rs / validator |
MIN_VALIDATOR_STAKE | 10,000 PYDE | tx/pipeline.rs (single tier) |
MAX_VALIDATORS_PER_OPERATOR | 3 | tx/pipeline.rs (anti-Sybil cap) |
UNBONDING_PERIOD | 30 days | consensus/validator.rs |
FINDER_FEE_PERCENT | 10 | slashing/lib.rs |
EVIDENCE_VERSION | 1 | slashing/lib.rs |
MULTISIG_VERSION | 0x01 | tx/multisig.rs |
MAX_MULTISIG_SIGNERS | 16 | tx/multisig.rs |
MAX_PAUSE_DURATION_WAVES | ~30 days of waves | tx/pipeline.rs |
MAX_BATCH_SIZE | 4 MB | mempool/batch.rs |
C. Gas / Fee Constants
| Constant | Value | Where |
|---|---|---|
GAS_TARGET | 400,000,000 | tx/fee.rs |
GAS_CEILING | 1,600,000,000 (4× target) | tx/fee.rs |
GENESIS_BASE_FEE | 50,000,000,000 quanta | tx/fee.rs |
MIN_BASE_FEE | 1 | tx/fee.rs |
ADJUSTMENT_DIVISOR | 8 (1/8 = 12.5% per block) | tx/fee.rs |
FEE_BURN_PCT | 70 | tx/execution.rs |
FEE_REWARD_POOL_PCT | 20 | tx/execution.rs |
FEE_TREASURY_PCT | 10 | tx/execution.rs |
MIN_GAS_LIMIT | 21,000 | tx/validation.rs |
MAX_TX_SIZE | 128 KB | tx/validation.rs |
MAX_CALLDATA | 64 KB | tx/validation.rs |
WAVES_PER_YEAR | 63,113,904 (2/sec) | tx/fee.rs |
INFLATION_BPS | [500, 300, 200, 100] | tx/fee.rs |
GENESIS_SUPPLY | 10^18 quanta (1B PYDE) | tx/fee.rs |
D. Mempool Constants
| Constant | Value | Where |
|---|---|---|
DEFAULT_MAX_TX_PER_WINDOW_PER_SENDER | 10 | mempool/pool.rs |
DEFAULT_MAX_CONCURRENT_PER_SENDER | 100 | mempool/pool.rs |
RATE_WINDOW_MS | 1000 | mempool/pool.rs |
WINDOW_SIZE (nonce bitmap) | 16 | account/nonce.rs |
MAX_RECEIPT_SLOTS | 10,000 | node/receipt_store.rs |
E. WASM Execution Constants
| Constant | Value | Meaning |
|---|---|---|
| Initial linear memory | 1 MB | Default WASM linear memory per instantiation |
| Max linear memory | 64 MB | Capped by the engine to bound resource use |
| Stack depth limit | Configurable | wasmtime-enforced; rejects modules exceeding cap |
PAGE_ALLOC_GAS | 200 fuel/64KB | Fuel per WASM memory.grow page |
| Default fuel per gas unit | (calibrated) | Established at node startup from the gas table |
MODULE_CACHE_MAX_BYTES | 1 GB (default) | LRU + size-cap + TTL on compiled Module + parsed ABI; per-node tunable. See HOST_FN_ABI_SPEC §3.6 |
MODULE_CACHE_TTL_WAVES | 8 epochs (~1 day) | Cache entries unused longer than this are evicted |
VIEW_FUEL_CAP | 10,000,000 | Per-call wasmtime fuel cap for cross_call_static views (≈ 3 ms commodity). View calls are free; this bounds wall-clock. |
Note: PVM-era constants (4 MB address space, 16+8 register file, 62 opcodes) are retired. WASM's instruction set is the WebAssembly Core Specification; the host-function ABI is defined in companion/HOST_FN_ABI_SPEC.md.
F. Network / Discovery Constants
| Constant | Default | Where |
|---|---|---|
DEFAULT_PORT | 30303 | net/config.rs |
DEFAULT_MAX_PEERS | 50 | net/config.rs |
DEFAULT_MAX_INBOUND | 30 | net/config.rs |
DEFAULT_MAX_OUTBOUND | 20 | net/config.rs |
DEFAULT_RATE_LIMIT_PER_IP | 5 / sec | net/config.rs |
DEFAULT_IDLE_TIMEOUT | 60 s | net/config.rs |
| Gossipsub mesh_n | 8 | net/node.rs |
| Gossipsub heartbeat | 150 ms (DAG round) | net/node.rs |
MAINNET_SEEDS | (set at launch) | net/discovery.rs |
TESTNET_SEEDS | (set at launch) | net/discovery.rs |
MAINNET_DNS_SEED | seed.pyde.network | net/discovery.rs |
G. State Discriminators
Used in Poseidon2(addr || discriminator || sub_key) for storage keys.
Defined in crates/state/src/keys.rs.
| Discriminator | Name | Holds |
|---|---|---|
| 0x12 | SUPPLY | Total PYDE supply counter |
| 0x13 | TOTAL_BURNED | Cumulative fee burn counter |
| 0x14 | REWARDS_PER_STAKE_UNIT | Lazy-accrual per-stake-unit reward accumulator |
| 0x15 | ACTIVE_STAKE_WEIGHTED_TOTAL | Pool divisor (sum of stake × uptime; excludes exited / slashed) |
| 0x16 | VESTING | Per-account vesting schedule (40 bytes) |
| 0x17 | VALIDATOR_SUBSIDY | (total_amount, end_wave) streaming subsidy |
| 0x18 | AIRDROP_ROOT | Genesis airdrop Merkle root |
| 0x19 | AIRDROP_DEADLINE | wave_id after which sweep is allowed |
| 0x1A | AIRDROP_CLAIMED | Per-leaf-index claim bitmap |
| 0x1B | AIRDROP_EXPECTED_SUM | Genesis pool size invariant |
| 0x1C | MULTISIG_SIGNERS | Treasury multisig signer set (FALCON pks) |
| 0x1D | MULTISIG_THRESHOLD | Required signature count |
| 0x1E | MULTISIG_NONCE | Replay-protection counter for multisig |
| 0x1F | EMERGENCY_PAUSE_END_WAVE | End wave_id of an active emergency pause |
H. Transaction Type Registry
Defined in crates/tx/src/types.rs.
Tag 2 is intentionally vacant — Batch was prototyped pre-mainnet and
removed before launch (see Chapter 11 §11.9). A forged tx_type = 2
fails decode.
| ID | Name | Purpose |
|---|---|---|
| 0 | Standard | Value transfer or contract call |
| 1 | Deploy | Contract deployment |
| 3 | StakeDeposit | Lock ≥ 10,000 PYDE and register validator (single tier, uniform-random committee selection per epoch) |
| 4 | StakeWithdraw | Begin 30-day unbonding |
| 5 | Slash | Submit double-sign evidence |
| 6 | ClaimReward | Claim accrued staking yield from the pool |
| 7 | ClaimAirdrop | Claim genesis airdrop with Merkle proof |
| 8 | SweepAirdrop | Move unclaimed airdrop residue to treasury (post-deadline) |
| 9 | MultisigTx | Treasury spend with multisig signatures |
| 10 | RotateMultisig | Rotate multisig signer set + threshold |
| 11 | EmergencyPause | Halt block production (multisig-signed) |
| 12 | EmergencyResume | Resume normal processing |
| 13 | RegisterPubkey | First-time pubkey binding for a funded-but-unregistered account (no sig, no gas; proof is address-derivation) |
I. WASM Host Function Surface (Summary)
Pyde's execution layer is WebAssembly. The WASM instruction set itself is the WebAssembly Core Specification — defined and maintained externally, not by Pyde. What Pyde defines is the Host Function ABI: the chain-side surface that contracts call to interact with state, accounts, crypto, events, and other chain primitives.
The full Host Function ABI specification (signatures, memory layout conventions, gas cost table, versioning rules, parachain-extension allowlist, forbidden imports) lives at companion/HOST_FN_ABI_SPEC.md. The high-level surface, organized by category:
Storage
sload, sstore, sdelete
Balances and transfers
balance, transfer
Execution context
caller, origin, block_height, wave_id, block_timestamp, chain_id
Events
emit_event
Hashing primitives
keccak256, blake3, poseidon2
Post-quantum cryptography
threshold_encrypt, threshold_decrypt_share, falcon_verify
Cross-contract / cross-parachain
cross_call
Gas accounting
consume_gas
Parachain-extension host functions (parachain-only)
send_xparachain_message, get_committee_info, additional governance hooks (full list in the Host Function ABI spec).
Forbidden imports (enforced at deploy)
Network calls, filesystem access, system clock, non-deterministic entropy, WASM threads, non-deterministic SIMD.
The deploy-time validator rejects any WASM module whose import section references functions outside this allowlist.
J. JSON-RPC Method Index
Full reference in Chapter 17. The methods, prefixed pyde_:
| Method | Returns |
|---|---|
pyde_getBalance | balance (quanta string) |
pyde_getTransactionCount | nonce (u64) |
pyde_getCode | hex bytecode |
pyde_getStorageAt | hex value |
pyde_chainId | hex chain_id |
pyde_blockNumber | hex head wave_id |
pyde_gasPrice | base fee (quanta) |
pyde_stateRoot | current state root |
pyde_syncing | sync status object |
pyde_getValidators | validators with status + stake |
pyde_getBlockByNumber | BlockHeader |
pyde_getBlockByHash | BlockHeader |
pyde_getTransactionReceipt | receipt with logs + fee breakdown |
pyde_getLogs | matching logs |
pyde_mempoolSize | pending tx count |
pyde_sendRawTransaction | tx hash |
pyde_sendTransaction | (dev only) tx hash |
pyde_sendEncryptedTransaction | tx hash |
pyde_call | view-function return data (FREE off-chain) |
pyde_estimateGas | gas estimate |
pyde_createAccessList | inferred access list |
pyde_getHardFinalityCert | committee-signed cert for a wave (incl. state_root + events_root + events_bloom) |
pyde_getSnapshotManifest | snapshot manifest for state sync |
pyde_resolveName | name → address registry lookup |
WebSocket subscriptions (via pyde_subscribe({method, ...})): newHeads
(wave commits), accountChanges, logs (events with AND+OR topic / contract
filter; at-least-once delivery with cursor for dedup). pyde_resubscribe({from: cursor})
resumes a logs stream after disconnect. Full mechanics:
HOST_FN_ABI_SPEC §15.5.
K. Cryptographic Primitives Summary
| Purpose | Primitive | Sizes |
|---|---|---|
| Digital signatures | FALCON-512 (NIST FIPS 206) | pk 897 B, sk 1281 B, sig ~666 B |
| Key encapsulation | Kyber-768 / ML-KEM (FIPS 203) | pk 1184 B, sk seed 64 B, ct 1088 B |
| High-volume hashing | Blake3 | 256-bit output, ~3 GB/s native |
| ZK-bearing hashing | Poseidon2 over Goldilocks | 256-bit output, ~400 constraints/hash |
| Threshold encryption | Shamir SSS + Kyber + Poseidon2 | 85-of-128, ~250 B per share |
| PSS resharing | Lagrange interpolation over Goldilocks | preserves underlying secret |
| DKG | Pedersen DKG over Kyber-768 | per-epoch threshold pubkey |
| VRF | FALCON-proof + Poseidon2 output | inherits FALCON security |
| Symmetric AEAD | AES-256-GCM (hardware-accelerated) | 32-byte key, 16-byte tag |
| Address | Poseidon2(falcon_pubkey) | 32 bytes |
No elliptic curves anywhere in the protocol.
L. Post-Mainnet Roadmap
Items explicitly out of scope for the launch network, with the rough priority each is tracked at:
| Item | Priority | Notes |
|---|---|---|
| Persistent receipt store (archive-node mode) | High | Task 058. Needed for production explorers. |
| ML-KEM upgrade from 0.3.0-rc to stable | High | Task 057. Once NIST stable releases. |
| Algebraic batch FALCON verification | High | Per-block verification cost reduction. |
| Signed-mempool commitments + censorship slashing | High | Replaces local-view mandatory inclusion. |
| Pedersen / KZG commitments for PSS resharing | High | Closes the malicious-contributor edge case. |
| Graceful drain-and-shutdown on persist failure | Medium | Task 014e. Operational polish. |
| Two-dimensional gas (exec + prove) | Medium | Depends on ZK proving landing. |
| Off-chain Merkle builder CLI for airdrop ops | Medium | Operator tooling, ~150 LOC. |
| Mempool-level filter during emergency pause | Low | Cleaner than gate-check at admission. |
| Sentry-node validator hiding | Low | Operational pattern, not protocol. |
| Sophisticated peer scoring | Medium | Multi-topic + decay parameters. |
| Fancy version-signaling on-chain | Low | Currently out-of-band. |
| ZK validity proofs (STARK proving) | Research | Major redesign; restores prover economics. |
| Native Ethereum bridge | High | FALCON-in-EVM verifier + Patricia verifier as a Pyde WASM contract. |
| Native Bitcoin bridge | Medium | SPV-style proofs; PoW finality is probabilistic. |
| Parachain SDK (Rust / Go / C++) | Medium | Sovereign chains sharing Pyde security. |
| TypeScript SDK | Medium | WASM bridge available now; dedicated TS later. |
| Native browser wallet | Low | Ecosystem; WASM exposes primitives. |
| Block-explorer frontend | High | Backend in Phase 7; UI is ecosystem. |
The list is the project's tracked future work, not a commitment timeline. Each item moves on PIP merit, audit capacity, and ecosystem demand.
M. Key References in the Codebase
For readers diving into the source. The pre-pivot crates listed below
(crypto, state, account, slashing, tx, consensus, networking, mempool,
node) live in the pyde-net/archive
repository, preserved with full git history. The post-pivot WASM
execution layer crate (wasm-exec) is to be implemented in a
freshly-cut workspace when the WASM-era engine repo is bootstrapped —
the row below is forward-looking. Paths are relative to whichever
workspace the file ends up in (archive workspace for pre-pivot rows,
the future post-pivot workspace for wasm-exec).
| Subsystem | Key files |
|---|---|
| Crypto stack | crates/crypto/src/{falcon,kyber,poseidon2,threshold,vrf}.rs |
| State commitment | crates/state/src/jmt_store.rs, witness.rs, keys.rs |
| Account record | crates/account/src/{types,address,nonce}.rs |
| Slashing constants | crates/slashing/src/lib.rs |
| TX types + pipeline | crates/tx/src/{types,validation,pipeline,fee,execution}.rs |
| Multisig / governance | crates/tx/src/multisig.rs, crates/tx/src/vesting.rs |
| Airdrop | crates/tx/src/airdrop.rs |
| Consensus | crates/consensus/src/{dag,vertex,wave,anchor,subdag,validator,finality,slashing,epoch_randomness,committee,quorum,round}.rs |
| Networking | crates/net/src/{node,channels,auth,peer,ddos,discovery,config}.rs |
| Mempool | crates/mempool/src/{pool,block_builder,inclusion,encrypted}.rs |
| Node binary + RPC | crates/node/src/{main,cli,rpc,validator,consensus_store,receipt_store}.rs |
| WASM execution layer (to be implemented) | wasm-exec/src/{lib,host_fns,module_cache,gas_meter,validate}.rs (post-pivot) |
otigen developer toolchain | pyde-net/otigen (separate repo): subcommand framework, otigen.toml schema, language detection, state binding generators (Rust/AS/Go/C), deploy flow, wallet |
| Rust SDK | crates/pyde-rust-sdk/src/{lib,client,wallet,contract,signer,abi,types,ws}.rs |
| WASM crypto | crates/pyde-crypto-wasm/src/lib.rs |
Launch plan, hardening status, and the phased route to mainnet: chapter 19 (Launch Strategy).
N. Where the Numbers Came From
The key headline figures, with their sources:
| Claim | Source |
|---|---|
| ~150 ms DAG round period | ROUND_PERIOD_MS in consensus/round.rs |
| ~500 ms median commit | COMMIT_TARGET_MS in consensus/commit.rs |
| v1 plaintext TPS: 10-30K | Performance harness measurement, "claim 1/3 of measured peak" rule (companion/PERFORMANCE_HARNESS.md) |
| v1 encrypted TPS: 0.5-2K | Same harness; threshold-decryption serial cost |
| 70 / 20 / 10 fee split | FEE_BURN_PCT etc in tx/execution.rs |
| 5% → 1% inflation schedule | INFLATION_BPS in tx/fee.rs |
| 10,000 PYDE validator min stake | MIN_VALIDATOR_STAKE in tx/pipeline.rs (single tier) |
| 3 max validators per operator | MAX_VALIDATORS_PER_OPERATOR in tx/pipeline.rs (anti-Sybil) |
| 30-day unbonding | UNBONDING_PERIOD in consensus/validator.rs |
| 16-slot nonce window | WINDOW_SIZE in account/nonce.rs |
| 128 KB tx / 64 KB calldata caps | MAX_TX_SIZE, MAX_CALLDATA in tx/validation.rs |
| 4 MB batch hard cap | MAX_BATCH_SIZE in mempool/batch.rs |
| 1 MB witness cap | MAX_WITNESS_SIZE in state/witness.rs |
| WASM host function ABI v1.0 | wasm-exec/src/host_fns.rs (post-pivot) + companion/HOST_FN_ABI_SPEC.md |
| wasmtime + Cranelift AOT | Pinned wasmtime version in Cargo.toml |
| Module cache size | MODULE_CACHE_SIZE in wasm-exec/src/module_cache.rs (post-pivot) |
| Committee 128, threshold 85 | COMMITTEE_SIZE, THRESHOLD in consensus/quorum.rs |
| 85-of-128 threshold for decryption | RANDOMNESS_THRESHOLD (and equivalent for Kyber) |
O. License and Contribution
The Pyde codebase is licensed under Apache 2.0 (workspace-wide, in
Cargo.toml). Contributions go through the PR process at
github.com/zarah-s/.... Substantive protocol changes go through a PIP
first (see Chapter 15).
This book is part of the project repository. Corrections and additions are welcomed via PR.
End Notes
Pyde is a sovereign post-quantum L1. Mainnet ships:
- No elliptic curves — FALCON-512, Kyber-768, Blake3, Poseidon2, lattice VRF.
- DAG consensus, no proposers — Mysticeti-style; each round every committee member produces a vertex; canonical order is structural.
- Hybrid execution scheduler — static access lists + Block-STM speculation.
- Optional threshold encryption — opt in per-tx for MEV protection; plaintext supported at lower cost.
- No tip mechanism — fees are exactly
gas_used × base_fee. - No on-chain stake-weighted vote — governance is PIPs + on-chain multisig.
- No bridge at v1 —
cross_call!macro stable; parachain operator layer ships post-mainnet. - Structural MEV protection — commit-before-reveal + DAG ordering + no tips = unexpressible MEV.
Everything that doesn't ship at mainnet is tracked, scoped, and prioritized for post-launch work. Honesty about what's in vs out is the single biggest difference between this book and earlier drafts.
The next thing to read isn't a separate file — it's chapter 19 (Launch Strategy), where the phased work-in-flight to mainnet lives. The Companion Specifications section of this book holds the full technical specs (Whitepaper, Design, Threat Model, Performance Harness, Parachain Design, Brand, and more).
Migration Notes (May 2026 Pivot)
This page is the migration log between the pre-pivot Pyde architecture (in-house HotStuff consensus) and the post-pivot architecture (Mysticeti DAG consensus + hybrid hashing + optional encryption). The book itself has been rewritten in place; this page exists as a single reference for what changed and why, useful for readers who came in mid-flight or who need to reconcile against pre-pivot artifacts.
The Pivot, In One Page
Before (HotStuff variant):
- Single-proposer-per-slot BFT consensus with 400 ms slot timing.
- View-change protocol for proposer failures.
- Encrypted mempool with proposer-asserted ordering commitment.
- Validators each stake a fixed 10K PYDE; equal vote weight.
- Sparse Merkle Tree (256-deep) for state.
- Poseidon2 hashing everywhere.
- Targeted 12.5K TPS sustained / 50K peak as headline.
After (Mysticeti DAG):
- DAG consensus — every round every committee member produces one vertex.
- No proposers, no view changes. Anchor selection is deterministic.
- Optional threshold encryption per-tx (plaintext or encrypted).
- Single-tier staking — 10,000 PYDE minimum, uniform-random committee selection per epoch, operator-identity cap (3 per operator).
- Jellyfish Merkle Tree (radix-16, path-compressed).
- Hybrid hashing — Blake3 (high-volume native) + Poseidon2 (ZK-bearing).
- v1 honest target: 10-30K plaintext / 0.5-2K encrypted TPS on commodity committee hardware, per the "claim 1/3 of measured peak" rule.
Why the Pivot
The HotStuff variant accumulated wedges, head-divergence deadlocks, and view-change cascades that resisted patching. Lab measurements peaked at ~4K TPS (full launch tests never ran). Repeated incidents under simple multi-node tests suggested the issue was structural, not implementation. The team chose a clean break: remove the consensus, mempool, and networking layers from the workspace; rebuild with the Mysticeti DAG protocol that Sui has been running in production since 2024.
Component-by-Component Diff
| Component | Pre-pivot | Post-pivot |
|---|---|---|
| Consensus | HotStuff variant, 1 proposer/slot | Mysticeti DAG, 128 vertices/round |
| Slot timing | 400 ms slot | ~150 ms round, ~500 ms median commit |
| Ordering | Proposer-asserted ordering commitment | Structural via committed subdag |
| Validator architecture | Monolithic | Worker (tx batching) + Primary (consensus) |
| Mempool | Always-encrypted | Optional encryption per-tx |
| State tree | Fixed-depth Sparse Merkle Tree | Jellyfish Merkle Tree (radix-16, path-compressed) |
| Hashing | Poseidon2 everywhere | Blake3 (native) + Poseidon2 (ZK-bearing) |
| State root | Single Poseidon2 root | Dual: Blake3 + Poseidon2 |
| Execution | Static access lists only | Hybrid: static + Block-STM speculation |
| Staking model | Single 10K PYDE | Single 10K PYDE (unchanged; an interim mid-pivot draft of the book proposed 10M/100K tiers — that was an error; flat-tier with operator-cap was the actual decision) |
| Reward distribution | Direct proposer share (20%) | Epoch reward pool (20%, distributed by stake×uptime) |
| Peer discovery | Kademlia DHT | Layered (seeds → DNS → on-chain registry → PEX → cache) |
| Committee defense | Operational sentry pattern only | Sentry pattern with protocol support |
| Cross-chain | Stub cross_call! | cross_call! + parachain operator network (v2) |
| Account abstraction | Single + Multisig | Single + Multisig (max 16) + Programmable (v2 reserved) |
What Stayed the Same
- FALCON-512 signatures everywhere. Untouched.
- Kyber-768 threshold encryption primitive. Untouched (now opt-in per-tx instead of mandatory).
- 70/20/10 fee split. Recipient of 20% changed (proposer → reward pool) but the percentages held.
- 16-slot nonce window per account. Untouched.
- Gas tank + paymaster sponsored-tx model. Untouched.
- Treasury multisig + emergency pause governance model. Untouched (multisig threshold raised to 7-of-12 typical).
Reading Order if You Knew the Pre-Pivot Book
If you're returning to the book after the pivot, the chapters that changed most are:
- Chapter 6 (Consensus) — full rewrite; HotStuff → Mysticeti DAG.
- Chapter 7 (State Sync & Chain Halt) — new chapter, operational procedures absent in pre-pivot.
- Chapter 9 (MEV Protection) — restructured for DAG ordering.
- Chapter 4 (State Model) — hybrid hashing, dual state roots.
- Chapter 8 (Cryptography) — Blake3 added; Poseidon2 scope narrowed.
- Chapter 12 (Networking) — DHT removed; layered discovery + sentry.
- Chapter 14 (Tokenomics) — single-tier staking (10K PYDE min, uniform-random committee selection, operator-identity cap), reward pool, updated inflation math.
- Chapter 19 (Launch Strategy) — timeline reset post-pivot.
- Chapter 20 (Appendix) — glossary, constants, post-mainnet roadmap updated.
Chapters that changed less:
- Chapter 3 (Execution Layer) — full rewrite for WebAssembly via wasmtime (post-pivot).
- Chapter 5 (Otigen Toolchain) — full rewrite as the developer toolchain (the binary; name carried forward from the retired language).
- Chapter 10 (Gas/Fee) — commit-cadence + honest TPS numbers.
- Chapter 11 (Account Model) — reserved
ProgrammableAuthKeys variant for v2. - Chapter 13 (Cross-Chain) — parachain layer framed as permissionless operator network (v2), not auctioned slots.
- Chapter 16 (Security) — DAG safety argument replaces HotStuff one; attack surface table updated.
- Chapters 15, 17, 18 — minor parameter / API updates.
Honest Status
Designed architecture, not shipped implementation. This book describes the post-pivot target architecture. Implementation status:
| Component | Status |
|---|---|
| Architecture design | ✅ Complete |
| WASM execution layer (wasmtime + Cranelift AOT) | 🟡 Foundation in place; integration in progress |
| State (JMT) | 🟡 In place, needs hybrid hashing wired |
| Mysticeti DAG consensus | 🔴 Not yet — rebuild post-pivot |
| Threshold cryptography | 🔴 Research-grade (PQ threshold is bleeding edge) |
| Network protocol | 🟡 Existing, needs libp2p+QUIC migration |
| Performance harness | 🔴 Not yet built |
The performance harness is the bottleneck on credible TPS claims. No external number leaves this project without harness evidence under the "claim 1/3 of measured peak" rule.
Related Docs
- Full technical design: companion/DESIGN.md
- Whitepaper: companion/WHITEPAPER.md
- Threat model: companion/THREAT_MODEL.md
- Failure scenarios: companion/FAILURE_SCENARIOS.md
- Performance harness spec: companion/PERFORMANCE_HARNESS.md
- Mainnet plan: Launch Strategy