How Pyde Works

The Pyde mark

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.

Pyde factory loop animation: transactions flowing in as droplets, batches forming, DAG floors rising, wave commit flash, state pillars stamped, smoke wisps rising, repeat.

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:

  1. BFS subdag walk — starting at the anchor, walk every parent reference recursively. The set of touched vertices is the subdag being committed.
  2. Canonical sort — order the subdag by (round, author_id, batch_list_order). Every honest member produces the same order.
  3. 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.

If you want the detailed mechanics of any stage:

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:

  1. Smart contracts — sandboxed WASM modules deployed to the chain. Standard L1 contract development; read Chapter 3 — Execution Layer for the runtime model.
  2. 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:

  • Rustcargo build --target wasm32-unknown-unknown --release
  • AssemblyScriptnpx 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:

  1. Chapter 1 — Introduction — 10-minute orientation. Why Pyde exists, what it's not.
  2. Chapter 3 — Execution Layer — the runtime, the per-tx overlay, the determinism contract.
  3. 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.
  4. Chapter 5 — Otigen Toolchain — how otigen builds, deploys, manages wallets, runs a local console.
  5. 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-foundation tag on pyde-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:

WhatWhen
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

  1. Read the whitepaper. 30 minutes; covers everything at a digestible depth.
  2. 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.
  3. Join Telegram for project chat.
  4. Follow @pydenet on X for milestone announcements.
  5. 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:

  1. Post-quantum cryptography by default — FALCON-512 signatures, Kyber-768 threshold encryption, Poseidon2 hashing
  2. MEV resistance by structure — threshold-encrypted mempool + commit-before-reveal ordering + DAG consensus eliminates proposer extraction
  3. Sub-second finality — Mysticeti-style DAG consensus, ~500ms median finality
  4. 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
  • otigen developer 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:

ComponentStatus
Architecture designComplete
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 consensusRebuild 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 frameworkDesigned; implementation deferred to a later phase
Performance harnessNot 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):

Modev1 realisticv2 stretchAspirational
Plaintext TPS (commodity)10K-30K50K-100K500K
Encrypted TPS (commodity)0.5K-2K5K-10K50K+
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:

  1. Chapter 2: Architecture Overview
  2. Chapter 6: Consensus (Mysticeti DAG)
  3. Chapter 8: Cryptography
  4. Chapter 9: MEV Protection
  5. Companion: Whitepaper

For an implementer / contributor:

  1. Chapter 2: Architecture Overview
  2. Chapter 3: Execution Layer (WASM)
  3. Chapter 4: State Model
  4. Chapter 5: Otigen Toolchain
  5. Chapter 11: Account Model
  6. Chapter 12: Networking
  7. Companion: Architecture (Design Doc)
  8. Preface: The Pivot for context on architectural choices

For a validator operator:

  1. Chapter 6: Consensus
  2. Chapter 7: State Sync & Chain Halt
  3. Chapter 16: Security & Threat Model
  4. Companions: Validator Lifecycle, Slashing, Chain Halt & Recovery

For an investor / decision-maker:

  1. This Introduction
  2. Chapter 14: Tokenomics
  3. Chapter 19: Launch Strategy
  4. Companion: Pitch Deck, Tokenomics Detail

For someone doing security review / audit:

  1. Chapter 16: Security & Threat Model
  2. Chapter 6: Consensus (safety arguments)
  3. Chapter 8: Cryptography
  4. 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 otigen binary owns the entire authoring lifecycle. Authors write only their contract logic and a otigen.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 (see IMPLEMENTATION_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/engine repo 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.toml workspace with every crate stubbed:
    • types, interfaces
    • account, state, tx, wasm-exec, mempool (β-owned)
    • consensus, net, dkg, slashing, node (γ-owned)
  • Each crate stub: Cargo.toml + src/lib.rs with a placeholder function so the workspace compiles (node also has src/main.rs for the pyde binary)

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)
  • Tx flat envelope + TxType discriminant (Ch 11 §11.6 wire format; tag 2 reserved-as-vacant)
  • TxHash, Receipt, ReceiptStatus, FeePayer, AccessEntry, AccessType
  • StateRoot (dual: Blake3 + Poseidon2)
  • EventRecord (with wave_id / tx_index / event_index primary key + Vec<Topic> for multi-topic v1) + EventCursor for pyde_getLogs pagination
  • WaveId (u64), Round (u64), CommitId (= WaveId)
  • VertexHash, BatchHash, BatchRef, Vertex (with member_id + batch_refs + decryption_shares per Ch 6 §3) + Batch (network gossip type)
  • WaveCommitRecord (with anchor_round / prior_anchor_round / events_root / events_bloom / events_count / tx_count / gas_used: u128)
  • HardFinalityCert with 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)
  • ContractAbi per HOST_FN_ABI_SPEC §3.7: pyde_abi_version: u32, contract_type, state_schema_hash, constructor_index / fallback_index / receive_index + EventAbi extension for §14.1 event signatures
  • FunctionAttrs (u32 bitfield: VIEW / PAYABLE / REENTRANT / SPONSORED / CONSTRUCTOR / FALLBACK / RECEIVE / ENTRY)
  • Error codes from HOST_FN_ABI_SPEC §4ERR_* consts + typed ErrorCode enum (i32 wire format; round-trips via as_i32 / from_i32)
  • AuthKeys (None / Single / MultiSig / Programmable-reserved at tag 0x03) with MAX_MULTISIG_SIGNERS = 16 and 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, snapshotSnapshotHandle
  • 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 mockMockState / MockExecutor / MockMempool / MockNetwork / MockConsensus, 11 tests each exercising at least one trait method per impl

0.5 CI + branching

  • .github/workflows/ci.yml running fmt + clippy (-D warnings) + test + doc on every PR with target/registry caching
  • Long-lived branches created: execution-side (β), consensus-side (γ)
  • Tag phase-0-foundation on main
  • pyde-book/src/companion/IMPLEMENTATION_PLAN.md already 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/otigen repo + Rust workspace
  • otigen-toml: config parser + schema validation (spec §4)
  • otigen-abi: ContractAbi construction + Borsh encoding + custom-section injection via wasm-encoder (spec §6)
  • otigen-cli: subcommand framework via clap (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 archived wright repo
  • 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 — sync reqwest::blocking Client + 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-run for offline inspection, --no-wait for fire-and-forget scripts. Wire format (Tx envelope + TxType / FeePayer / AccessType discriminant tags + canonical Poseidon2 hash) pinned to Ch 11 §11.6 / §11.8 / §"Transaction hash" on the toolchain side until Stream β's tx crate lifts beyond its current scaffold.
  • otigen upgrade / pause / unpause / kill / inspect
  • otigen console REPL (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: ContractAbi build, Borsh encode/decode round-trip, pyde.abi custom-section inject + extract, validators, full pipeline ✅ (pyde-net/otigen#6)
    • otigen-cli: full otigen build pipeline 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-clock otigen build invocation is dominated by file I/O, not validator work
  • cargo-fuzz targets with 24h+ cumulative run before α release:
    • otigen-toml parser (malformed input, deep nesting, huge fields)
    • otigen-abi WASM validator (malformed binaries, edge cases in section structure)
    • otigen-abi custom-section injection (extreme WASM module shapes)
  • Property-test coverage audit: ≥15 proptest groups across otigen-toml and otigen-abi (currently ~5)
  • Adversarial corpus: 30+ hand-rolled otigen.toml files under tests/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.wasm and abi.json (modulo manifest.build_timestamp)

CI + supply chain

  • Multi-platform CI matrix: ubuntu-latest x86_64 + aarch64, macos-latest arm64, windows-latest x86_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

  • --json NDJSON 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 / -vv actually emits the documented log levels (today the flag is captured but most commands print fixed output)
  • Signal handling: Ctrl-C mid-build cleans up partial bundle artifacts
  • otigen --version includes 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 malicious otigen.toml, malicious WASM, pyde.abi injection 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 unsafe blocks 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 deploy against a running devnet — end-to-end transaction submission + receipt fetch
  • otigen inspect against a deployed contract on the devnet
  • otigen verify reproducibility round-trip via the devnet's pyde_getContractCode RPC
  • 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-0pyde-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 (flat slot_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 + StateMutator traits (from interfaces)
  • Snapshot generation (range-proof chunks, manifest)

β.2 account crate [PAR within β]

  • 32-byte address derivation (Poseidon2(falcon_pubkey))
  • AuthKeys enum with Single, 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-v1 memory)
  • 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)
  • WasmExecutor type
  • 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 by VIEW_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
  • Deploy-time validation (3-layer per HOST_FN_ABI_SPEC §3.7)
  • Attribute application + pyde.abi custom-section extraction
  • Implement Executor trait (from interfaces)

β.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 net crate via NetworkView trait)
  • Per-sender rate limit + concurrent cap (DDoS protection)
  • Implement MempoolView trait (from interfaces)

β 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-0pyde-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

  • Vertex structure (round, member_id, parent_refs, batch_refs, state_root_sigs, prev_anchor_attestation, decryption_shares, sig) — landed in types crate at MC-0
  • Local DAG view per validator (VertexStore: hash + round + slot indexes, equivocation-aware, parking_lot::RwLock guarded) — 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 + Signer trait + select_parents helper that skips equivocating slots; returns (VertexHash, Vertex) so callers get the dedup key free) — PR #2
  • Vertex validation pipeline (validate_vertex + Verifier trait + ValidationConfig; cheapest-first checks: range → batch-dedup → parent quorum → parent-round homogeneity → FALCON sig; MissingParent returns hash so caller can fetch and retry) — PR #3
  • Round advancement (RoundTracker: monotonic counter, distinct-member_id quorum check, try_advance / try_advance_to_max for state-sync catch-up, equivocator-resistant via distinct-producer counting) — PR #9
  • Anchor selection: select_anchor(beacon, round, lookback_state_root, committee_size) — Blake3 over beacon || 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 (PendingParents queue: bounded, idempotent-duplicate, cascade-unblock, exposes missing_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 ConsensusEngine trait via Driver (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 NetworkView trait (from interfaces)

γ.3 dkg crate [PAR within γ]

  • Pedersen DKG protocol implementation (per epoch)
  • PSS resharing (proactive secret sharing across epochs)
  • May import from pyde-crypto if 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

  • pyde binary (cli, validator, full-node modes)
  • JSON-RPC server (per HOST_FN_ABI_SPEC §15.4-15.5 + chapter 17 method list)
  • consensus_store with WriteOptions::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.

  • DevnetStateStateMutator impl with real transfer + fee + nonce-window logic — PR #15
  • DevnetExecutor — pure pre-flight Executor impl — PR #16
  • Devnet composer + Wallet — full single-validator commit loop — PR #17
  • run_smoke scenario + 8 integration tests — PR #18
  • pyde-node devnet --smoke CLI 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 deploy against 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_getSnapshotManifest RPC 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_call callback 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)
  • otigen toolchain (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.md for 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 featurev1 reservationCost at v1
Programmable accountsAuthKeys::Programmable enum tag 0x03Enum variant, unused — ~zero
Programmable accountsAccount code_hash + storage_root (unified with contracts)Already shipped (account/contract account shape unified)
Session keysWASM "policy mode" execution flagReserved-but-not-implemented — ~zero
Session keysMultisig signature pipelineAlready shipped (serves multisig + future session-key flows)
ZK light clientsPoseidon2 state root + ZK-friendly primitivesAlready shipped (dual-hash JMT, no Blake3 in proof-bearing paths)
Parachains (further depth)cross_call host fn, HardFinalityCert primitive, async callback slotsAlready 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)

ItemOwning streamDepends onUsed by
MC-0 Interface foundationmain session(none)All MC-1 streams
MC-1 α ToolchainαMC-0 + HOST_FN_ABI_SPECContract 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 + β.3MC-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 traitsThe deployable artifact
MC-2 Integrationγ-ledAll MC-1 streams doneDevnet & all of MC-3-5
MC-3 State Sync + Parachainβ + γ jointMC-2New validators (sync); parachain authors
MC-4 Performance + FailuresharedMC-2 + MC-3 functionalMainnet readiness
MC-5 Validation + LaunchmainAll precedingMainnet 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

DocumentEraStatus
01 — The HotStuff Consensus EraPre-Mysticeti consensus designRetired
02 — The Otigen Language EraPre-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:

  1. 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.

  2. 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.

  3. 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

  1. Preface: The Pivot — the narrative.
  2. This directory's 01 — HotStuff Era and 02 — Otigen Era — the design records.
  3. The main book chapters — the current architecture.
  4. 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:

  1. 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.

  2. 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.

  3. 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 codearchive/crates/consensus/ (in the umbrella repo). The HotStuff implementation, including the QC types, view-change protocol, and leader-rotation logic.
  • Design notesarchive/crates/consensus/CONSENSUS_INVARIANTS.md documents the consensus invariants the HotStuff implementation upheld.
  • Original whitepaperarchive/WHITEPAPER.md describes the early-architecture vision including HotStuff as the consensus choice.
  • Pre-pivot engine cratesarchive/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 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:

WorkloadPVM InterpreterPVM AOTAOT speedup
ALU dispatch~279M instr/sec~2.9B instr/sec10.4×
DEX swap~27M swaps/sec~100M swaps/sec3.7×
Token transfer~231K tps~243K tps1.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:

  1. 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.

  2. 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.

  3. 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 otigen toolchain (the binary; same name, new role) emits language-specific bindings that preserve these guarantees in Rust, AssemblyScript, Go, and C.

  4. 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.toml state schema rather than by the Otigen compiler. Same property, different surface.

  5. 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-book with a pivot-notice preface explaining the current status.
  • otic compiler sourcepyde-net/otic repo, archived (read-only).
  • wright toolchain sourcepyde-net/wright repo, archived (read-only).
  • pyde-vm and pyde-aot crate sourcearchive/crates/pvm/ and archive/crates/aot/ in the umbrella repo, preserved with git history.
  • Original Otigen-era documentationarchive/ more broadly contains the pre-pivot READMEs, design notes, and benchmark plans.
  • Benchmark numbers — see the bench files in archive/crates/pvm/benches/ and archive/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

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

CPUApple M4 Pro
Cores14 physical / 14 logical
RAM24 GB
OSmacOS 26.3.1
Rust toolchainstable (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/archive repository (where the retired pre-pivot crates live).
  • A stable Rust toolchain. rustup install stable if 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

BenchmarkSource
interpreter_bencharchive/crates/pvm/benches/interpreter_bench.rs
aot_bencharchive/crates/aot/benches/aot_bench.rs
(future) WASM-equivalent bencheswasm-exec/benches/ in the fresh post-pivot engine repo (to be added)
(future) host-function micro-benchessame crate
(future) full-chain harnessseparate 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

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:

  1. Threshold decryption for encrypted transactions (≥85 partials combined)
  2. 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
  3. 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.
  4. State root computed — dual-hash (Blake3 + Poseidon2) per JMT node
  5. Committee FALCON-signs state root (piggybacked on next vertices)
  6. 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

TierStakeCommittee RoleEarns
Committee validatorYes, largeActive (1 of 128)Activity rewards + pool yield + inflation
Non-committee validatorYes, smallerStake-only, waiting selectionPool yield + inflation
RPC nodeNoNoneOff-chain RPC fees (market-set)

RPC providers (Infura/Alchemy analog) fit Tier 3 — no stake, no slashing risk.

Key Differentiators

EthereumSolanaSuiPyde
Post-QuantumMigration 5+ yearsNo planNo planDefault at genesis
MEV resistanceAuction (PBS)Proposer extractsSome via MysticetiStructurally impossible
Finality12-15s400ms390ms~500ms
Commodity validatorPossibleNo (12+ cores)No (datacenter)Yes (any validator awaiting committee selection)
Smart contract languageSolidityRust/AnchorMoveAny wasm32 target (Rust, AssemblyScript, Go, C/C++)
Account abstractionRetrofit (ERC-4337)None nativeLimitedNative (v2)
Cross-chainBridges ($3B+ hacked)BridgesBridgesPermissionless parachain (v2)
ZK readinessRetrofit ongoingLimitedLimitedArchitecture 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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 than sstore; 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 to to_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 from caller() to avoid the tx.origin footgun 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 via events_root (Merkle tree) + events_bloom in the wave commit record. Recommended encoding for data is Borsh; topics are typically Blake3(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-call VIEW_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() and caller() 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_timestamp instead — 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 (sload reads, sstore writes, 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:

  1. Gas budget. Every write into the overlay charges fuel via sstore. A tx with gas_limit = 10_000_000 can write at most ~50K slots (varying by slot size). Author can't write infinitely without paying.

  2. 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_canonicalization so 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 use OnceCell / lazy_static! / a const fn if possible. AssemblyScript uses a module-level constant initializer. Go uses init(). C uses a static const array 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 addr argument) is hashed at runtime — one pyde_poseidon2 call 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 unreachable instruction was executed (typically Rust's panic!() lowers to this).
  • Host function errorsstore to a write-locked slot, transfer with 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.

ComponentPlanned crate / file (post-pivot)
WasmExecutor entry pointwasm-exec/src/lib.rs
Host function implementationswasm-exec/src/host_fns.rs
Module cachewasm-exec/src/module_cache.rs
Fuel-to-gas mappingwasm-exec/src/gas_meter.rs
Validation gatewasm-exec/src/validate.rs
Deploy-tx processingtx/src/deploy.rs
State binding code generators (per language)otigen repo (otigen/crates/codegen-*)
Host Function ABI specificationcompanion/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

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?

PropertyFixed-depth SMT (256 levels)JMT (radix-16, compressed)
Node hashes per update256depth-of-key (typ. 8–14)
Empty subtree storageimplicit (precomputed)implicit (no materialize)
Update batchingper-keybulk via update_all
Throughput (commits)baseline~40× faster
Proof sizefixed (256 sibling hashes)variable (typ. 8–14)
Non-existence proofsempty leaf hashpath 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_root without 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 tierstate_cfjmt_cf
Pruned validatorCurrent state onlyLatest version only (older GC'd)
Archive nodeCurrent stateAll historical versions
Light clientNoneJust 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 tierevents_cf + indexes
Archive nodeAll events, forever
Pruned validatorLast 90 days
Committee validatorLast 30 days
Light clientNone (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:

HashSpeed (commodity CPU)ZK-friendlyWhere used
Blake3~3 GB/sNo (huge circuit)JMT internal nodes, batch hashes, vertex hashes, gossip de-dup, RocksDB keys
Poseidon2~60 MB/sYes (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):

ParameterValue
FieldGoldilocks (p = 2^64 - 2^32 + 1)
State width8
Rate4 (256-bit absorb/squeeze)
Capacity4
External rounds8 (4 + 4)
Internal rounds22
S-boxx^7
Output256 bits

The hash is exposed as three primitives:

FunctionUse
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):

DiscriminatorNameWhat it keys
0x12SUPPLYTotal PYDE supply counter
0x13TOTAL_BURNEDCumulative fee burn counter
0x14REWARDS_PER_STAKE_UNITLazy-accrual per-stake-unit reward accumulator
0x15ACTIVE_STAKE_WEIGHTED_TOTALPool divisor (sum of stake × uptime; excludes exited/slashed)
0x16VESTINGPer-account vesting schedule
0x17VALIDATOR_SUBSIDY(total_amount, end_wave) for streaming subsidy
0x18AIRDROP_ROOTGenesis airdrop Merkle root
0x19AIRDROP_DEADLINESlot height after which sweep is allowed
0x1AAIRDROP_CLAIMEDPer-leaf-index claim bitmap
0x1BAIRDROP_EXPECTED_SUMGenesis pool size invariant
0x1CMULTISIG_SIGNERSTreasury multisig signer set (FALCON pks)
0x1DMULTISIG_THRESHOLDRequired signature count
0x1EMULTISIG_NONCEReplay-protection counter for multisig actions
0x1FEMERGENCY_PAUSE_END_WAVEEnd 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 against pre_state_root. JMT supports batch verification, so the proof is asymptotically smaller than len(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 by set_post_state_root() or finalize_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:

PrefixMeaning
0x10JMT internal nodes
0x11Leaf values
0x12Metadata (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:

  1. Open a batch against the current JMT.
  2. Execute each parallel group from the conflict graph (see Chapter 9 for how the access-list scheduler builds groups).
  3. Within a group, transactions execute sequentially in order; across groups, in parallel against the same pre_state_root.
  4. Apply state writes to the batch.
  5. Distribute fees: 70% to the burn counter (TOTAL_BURNED discriminator), 20% to the epoch reward pool (distributed at epoch end by stake × uptime), 10% to the treasury account.
  6. Commit the batch with update_all. The new root is post_state_root.
  7. Set witness.post_state_root and 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):

  1. 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.

  2. 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.

  3. 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, with set_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

ComponentChoice
Tree structureJellyfish Merkle Tree (radix-16, path-compressed)
Internal-node hashBlake3 (high-volume, native)
State rootDual: Blake3 (native) + Poseidon2 (ZK-bearing)
Address-derivationPoseidon2 (ZK exposure preserved)
Storage layoutFlat — single tree, discriminator bytes in keys
Address format32 bytes, Poseidon2 of the FALCON-512 public key
Account record size141 bytes fixed + variable auth_keys
Storage keyingPoseidon2(addr, slot) for values; doubled for maps
Witness formatSingle batched JMT proof + entries + pre/post roots
Witness size cap1 MB (rejected at verification time)
PersistenceRocksDB 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

CommandPurpose
otigen init <name> --lang <language>Scaffold a new project directory from the language template. Populates otigen.toml skeleton and a minimal source file.
otigen buildVerify + 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 deploySign 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 upgradeSubmit an upgrade proposal. For smart contracts: owner-signed upgrade tx. For parachains: routes through governance (see Chapter 13).
otigen pausePause an operational contract (owner-only, where supported).
otigen killPermanently 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 walletKey management. Subcommands: create, import, list, export-pubkey.
otigen consoleREPL 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_path points 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 named X.
  • Generate artifacts/<contract_name>.abi.json from the [state] and [functions] tables.
  • Package artifacts/<contract_name>.bundle containing 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

AttributeEffect
viewRead-only function. Runtime rejects any state-modifying host call inside it. View calls are FREE (no gas) — see HOST_FN_ABI_SPEC §7.8.
payableFunction accepts PYDE attached to the call. Non-payable functions reject any attached amount.
reentrantOpts INTO allowing reentrancy. Default for every function is reentrancy-blocked.
constructorInitialization-only. Callable exactly once, at deploy time.
sponsoredGas charged to the contract's gas_tank rather than the caller's balance. Enables gasless UX.
fallbackInvoked when the call's function selector matches no declared function. At most one per contract.
receiveInvoked on bare PYDE transfers (no selector, value > 0). At most one per contract. Must also be payable.
entryMarks 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:

AttributeRuntime enforcement
viewHost functions sstore, sdelete, transfer, emit_event trap if called inside a view function.
payableIf tx.value > 0 and target function is not payable, transaction reverts at dispatch. No state change.
reentrantRuntime 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.
constructorCallable only by the deploy transaction. Subsequent calls trap.
sponsoredAt 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 choiceHow it's preserved in the WASM era
Reentrancy off by defaultRuntime reentrancy guard for every function not marked reentrant.
Checked arithmetic by defaultPer-language SDK helper patterns; wrapping ops require explicit opt-in (e.g., Rust's wrapping_add is explicitly named).
Typed storageotigen.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.originHost function ABI exposes caller() (direct caller) but no origin(). The Solidity-style phishing footgun is absent.
Compile-time access listsBuild tool emits a static access list per function from the declared state schema; the parallel scheduler uses these.
4-byte function selectorsBuild 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 guardsReentrancy 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:

  1. otigen reads otigen.toml, validates the contract is built (artifacts/<name>.bundle exists and is current).
  2. otigen checks the name registry on-chain: is mytoken available? If taken, fail with a clear error.
  3. otigen opens the wallet keystore, prompts for password if encrypted, signs the deploy transaction.
  4. 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)
  5. otigen submits the transaction to the node.
  6. otigen polls for inclusion, reports the contract address once committed.
  7. Done.

Upgrade

otigen upgrade --network testnet

What happens (smart contract path):

  1. otigen builds the new version (same as otigen build).
  2. otigen submits an upgrade transaction signed by the owner key.
  3. The chain applies the upgrade after a grace period (configurable in otigen.toml; default 100 waves) to give users time to verify.
  4. 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>:

  1. Read the encrypted keystore from ~/.pyde/wallets/<name>.json.
  2. Prompt for the passphrase (unless --unlock-with-env PYDE_PASSPHRASE is set, for CI use).
  3. Derive the AES key from the passphrase via Argon2id.
  4. Decrypt the FALCON private key in memory.
  5. Construct the transaction, hash it, FALCON-sign with the private key.
  6. Submit the signed transaction to the network.
  7. 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.

OperationMedian
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 ContractAbi156 ns
pyde.abi custom-section inject (3-fn realistic WASM)494 ns
pyde.abi custom-section extract154 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 pass278 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

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 HotStuffDAG resolution
Single proposer bottleneckNo proposer — every member contributes
View change protocol complexityNo view changes — eliminated entire failure class
Timing-driven slot pipelineData-driven rounds advance with quorum, not clock
Proposer can censor selectively127 honest can include; censorship requires near-unanimous
Proposer can extract MEVNo single party reorders; order emerges from DAG
Throughput limited by leader bandwidthScales with committee size
HotStuff bugs cluster in view-change codeDAG 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:

TypeTriggerAuthority
Soft stallNetwork / quorum issuesEmergent
Hard haltContradictory state roots, equivocation cluster, DAG forkProtocol-detected automatic
Emergency haltOff-chain bug report, active exploitGovernance 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

PropertyHotStuff (pre-pivot)Mysticeti DAG (current)
Slot/round timing400ms clockData-driven (~150ms/round)
Proposer modelSingle per slot (VRF)None
View changesYes (cascade-prone)None
Finality~1s+ (chained QCs)~500ms (per-round)
Throughput ceilingLeader bandwidthCommittee parallelism
Censorship resistanceProposer-dependent127-of-128 can include
MEV resistanceProposer + threshold-encStructural (no proposer)
Liveness under failureView-change cascadesGraceful (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

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

ModeUse CaseTime
Full sync (genesis replay)Archive nodes onlyInfeasible at high TPS
Snapshot sync (default)Most full nodes, new committee joiners~30-60 min on commodity
Light client syncMobile wallets, browser, dApp backendsSeconds-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

TypeTriggerSeverityAuthority
Soft stallNetwork / quorum issuesLiveness onlyEmergent
Hard haltContradictory state roots, equivocation clusterSafety riskProtocol-detected automatic
Emergency haltCritical bug, active exploit, hard-fork prepHigh intentionalGovernance 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

ActivitySoftHardEmergency
Vertex productionContinues (no quorum)StopsStops
CommitsPausedPausedPaused
Tx submissionQueuedQueuedQueued
Decryption ceremoniesPausedStoppedStopped
Slashing evidence acceptanceContinuesContinuesContinues
GossipContinuesContinuesContinues

Key invariant: slashing evidence accepted during halt — attackers cannot escape consequences by triggering a halt.

Recovery Procedures

  1. Wait it out (soft stalls) — auto-recover
  2. Software update + replay (hard halts from bugs) — patch, verify, resume
  3. Rollback (max 1 epoch back, governance authorized) — controversial but bounded
  4. Hard fork (irreconcilable splits) — coordinated upgrade
  5. 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:

  1. Soft stall: deliberately offline 43 validators
  2. Hard halt: inject state divergence
  3. Emergency halt: practice multisig coordination
  4. Rollback: 1-epoch procedure
  5. 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

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:

  1. 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.

  2. No trusted setup. No ceremony, no toxic waste. Every public parameter is either a NIST standard or a transparent algebraic constant.

  3. 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+

SchemePubkeySignatureVerify timeNotes
FALCON-512897 B600–900 Bvery fastsmallest sigs, lattice (NTRU)
Dilithium-21312 B2420 Bfastlarger sigs, module-LWE
SPHINCS+-12832 B7856 Bslowhash-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

ParameterValue
Polynomial degree n512
Modulus q12,289
Public key897 bytes
Secret key1,281 bytes
Signature600–900 bytes (variable, accepted)
Security levelNIST 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

  1. Transaction signing — every transaction carries a FALCON-512 sig from the sender's account.
  2. Vertex production — every DAG vertex is FALCON-signed by its producer.
  3. State-root attestations — committee members sign (wave_id, blake3_state_root, poseidon2_state_root) after each commit; ≥ 85 sigs constitute the HardFinalityCert.
  4. Decryption share authentication — threshold partial decryptions are FALCON-signed by their producer.
  5. PSS resharing contributions — contributors sign their shares.
  6. P2P peer authentication — the FALCON handshake (crates/net/src/auth.rs).
  7. VRF proofs — every VRF output is paired with a FALCON proof.
  8. 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

ParameterValue
Module dimension k3
Polynomial degree n256
Modulus q3,329
Public key (encaps key)1,184 bytes
Secret key (decaps seed)64 bytes (full key derived on demand)
Ciphertext1,088 bytes
Shared secret32 bytes
Security levelNIST 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

  1. 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.
  2. 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:

FunctionSpeed (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 nodesblake3_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

ParameterValue
FieldGoldilocks (p = 2^64 − 2^32 + 1)
State width8 field elements (≈ 512 bits)
Rate4 field elements (256-bit absorb)
Capacity4 field elements
External rounds8 (4 initial + 4 terminal)
Internal rounds22
S-boxx^7 (coprime to p − 1)
Output size4 field elements (256 bits)
Security level128-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

  1. State root commitment — the dual-rooted state has a Poseidon2 root alongside the Blake3 root, signed by the committee.
  2. Account address derivationPoseidon2(falcon_pubkey).
  3. CREATE / CREATE2 address derivationPoseidon2(deployer || nonce) or Poseidon2(0xFF || deployer || salt || code_hash).
  4. Storage key derivationPoseidon2(contract, slot) for single fields, doubled for maps. Encoded as build-time constants by the otigen developer toolchain's state binding generator.
  5. Transaction hashing — the canonical tx hash used for replay prevention and the wallet's signing target.
  6. Threshold MACPoseidon2(0xFF...0xFF || secret || ciphertext).
  7. VRF outputPoseidon2(domain || fingerprint || input).
  8. Epoch randomness combinationPoseidon2_many(sorted_shares).
  9. poseidon2 WASM 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:

  1. Shamir Secret Sharing over the Goldilocks field — splits a secret into 128 shares of which any 85 reconstruct.
  2. Kyber-768 KEM — the underlying public-key primitive.
  3. 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

ParameterValue
Underlying KEMKyber-768
Committee size n128
Threshold t85 (~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

PropertyWhy it holds
DeterministicOutput is a Poseidon2 hash of (sk-derived) constants + input
UnpredictableAn attacker without sk cannot compute fingerprint
VerifiableAnyone with pk can verify the FALCON sig over the input/output
Post-quantumInherits FALCON's NTRU-lattice security

Where the VRF is used

  1. 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.
  2. 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.
  3. 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:

  1. 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.
  2. P2P channel encryption (after the libp2p QUIC handshake — see Chapter 12).
  3. 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:

  1. Birthday-bound margin. A 20-byte address has 80-bit collision resistance. Marginal at chain scale; decisively safer at 128 bits.
  2. Native output size. Poseidon2 naturally outputs 4 Goldilocks field elements (≈ 256 bits = 32 bytes). Using the full output avoids a truncation step.
  3. 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

PrimitiveUseWhere
FALCON-512All signatures (txs, vertices, state roots, attestations)crates/crypto/src/falcon.rs
Kyber-768 / ML-KEMP2P session keys + threshold mempool encryptioncrates/crypto/src/kyber.rs
Blake3High-volume native hashes (JMT, batches, vertices, gossip)crates/crypto/src/blake3.rs
Poseidon2ZK-bearing hashes (state root, addresses, MAC, VRF, opcode)crates/crypto/src/poseidon2.rs
Threshold scheme85-of-128 mempool decryption (Kyber + Shamir)crates/crypto/src/threshold.rs
PSS (refresh + reshare)Forward security + cross-committee handoffcrates/crypto/src/threshold.rs
Lattice VRFAnchor seeding, randomness, committee scorecrates/crypto/src/vrf.rs
AES-256-GCMSymmetric 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.

ApproachWho still sees the tx
PBS / MEV-BoostBuilders + relays
Fair orderingNetwork observers (latency-exploitable)
Batch auctionsSolver (and only fixes one tx type — swaps)
Private mempoolBuilder still sees
Commit-revealAdds 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

FieldVisibleWhy
fromyesNeeded for signature verification + nonce window check
nonceyesReplay protection (must fit the bitmap window)
gas_limityesBlock gas accounting at proposal time
access_listyesDrives parallel scheduling
deadlineyesMempool eviction of expired txs
chain_idyesCross-chain replay protection
signatureyesValidates the whole tx
tonoReveals counterparty
valuenoReveals transfer amount
calldatanoReveals 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:

LimitDefaultWhy
DEFAULT_MAX_TX_PER_WINDOW_PER_SENDER10 tx / 1 sToken-bucket burst limit
DEFAULT_MAX_CONCURRENT_PER_SENDER100 in poolCap concurrent pending txs
RATE_WINDOW_MS1000 msToken-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:

  1. 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).

  2. Round R+1 to R+3: later rounds reference round-R vertices as parents and accumulate Mysticeti's 3-stage support.

  3. 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.

  4. Decryption shares released: committee members compute and broadcast decryption shares for the just-committed wave's encrypted transactions, piggybacked on round-R+4 vertices.

  5. 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:

StepTime (typical)Where it lives
DAG anchor commit (waves)~500 ms mediancrates/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 mspiggybacked on next vertices
Recovery + AES decrypt~5 ms per txcrates/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:

LayerClosesLives in
Optional threshold encryptionReading tx contents pre-inclusion (opt-in)crates/crypto/src/threshold.rs
Commit-before-reveal (DAG)Reordering after decryptioncrates/consensus/src/wave.rs
Structural inclusion (DAG)Single-actor censorshipcrates/consensus/src/dag.rs
No tips / priority feesBribery for orderingcrates/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 classSeverityPrimary defense
51% / Byzantine takeoverCriticalBFT f < n/3 with equal-vote committee, Mysticeti DAG safety
Long-range attackHighWeak-subjectivity checkpoints; hard-finality irreversibility
Sybil attackHighLayered: threshold encryption removes attack incentive + operator-identity cap (max 3/operator) + slashing + minimum stake floor
Eclipse attackHighLayered discovery (no DHT) + FALCON peer auth + sentry pattern
DDoS (network-level)MediumRate limiting, peer scoring, per-channel size caps, sentry
Front-running / MEVHighOptional threshold encryption + commit-before-reveal DAG (Ch 9)
State manipulationCriticalJMT batched Merkle proofs, deterministic replay, 2 state roots (Blake3+Poseidon2)
Quantum attacksCriticalEntire stack is post-quantum from genesis (Ch 8)
Smart contract exploitHighDefault safety attributes (no reentrancy, checked arithmetic) enforced at runtime via the WASM execution layer
VM / runtime exploitCriticalwasmtime sandbox (production-vetted at Microsoft / Fastly / Shopify), deterministic feature subset enforced, deploy-time import validation
Consensus persistence lossCriticalWriteOptions::set_sync(true) + panic-on-persist-failure
Replay across chainsHighMandatory chain_id in every tx hash
Treasury drainCriticalMultisig-only spend + data_digest audit trail
Threshold crypto breakCriticalHard 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:

  1. 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.
  2. 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).
  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).
  4. 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:

  1. Genesis block hash (hard-coded).
  2. Recent weak-subjectivity checkpoint (operator-provided).
  3. 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

  1. Peer diversity. The peer manager (crates/net/src/peer.rs) caps connections per /24 subnet. An adversary would need to control IP addresses across many subnets, not just spin up lots of VMs on one provider.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

ChannelMax size
Vertices256 KB
Transactions128 KB
Batches4 MB
Sync16 MB
Evidence64 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:

  1. HardFinalityCert for the wave is valid (≥ 85 FALCON sigs).
  2. The JMT proof from blake3_state_root to the specific leaf is valid.
  3. 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 reentrant attribute (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_add is 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 only caller() (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 otigen toolchain 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:

TrapWhen
ReentrancyViolationA cross_call re-enters a non-reentrant function
AccessListViolationA slot access targets a slot outside the declared state schema
ViewFunctionStateModifyA state-modifying host call inside a view-attributed function
NonPayableValueAttachedtx.value > 0 on a non-payable function
ConstructorReentrantAn attempt to call a constructor-attributed function post-deploy
GasTankExhaustedA sponsored function's contract gas tank ran out
InsufficientBalancetransfer 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 validators
  • wasm_threads(false) — no threading (non-deterministic by definition)
  • wasm_simd(false), wasm_relaxed_simd(false) — SIMD disabled until a deterministic-only subset is vetted
  • wasm_reference_types(false), wasm_gc(false), wasm_function_references(false) — complexity surface gated until needed
  • wasm_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.

  1. WriteOptions::set_sync(true) on every write to the consensus store (task 014a). A vote is not considered "cast" until fsync returns.
  2. 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.
  3. Restart recovery reloads seen_proposals, seen_votes, pending_evidence from 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:

Networkchain_id
Mainnet1
TestnetTBD
Devnet31337

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:

  1. Multisig-only spend. No other transaction type drains the treasury account.
  2. Audit trail. data_digest = hash(pip_file_contents) ties every spend to a published PIP.
  3. Rotation. RotateMultisig can replace the signer set; no single signer is entrenched.
  4. Writeback-clobber protection. spend.target != tx.from, tx.to == 0x00. Prevents the post-execution pipeline from accidentally overwriting the spend.
  5. 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_nonce bump) 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):

TaskStatus
Clippy/fmt/audit/deny in CIHardening track; shipping
cargo-fuzz on wasm-exec / tx / consensus / RPC / otigen toolchain72+ h runs
Property tests on pipeline + tokenomicsInitial properties shipped; expanding
Witness 1 MB bound validationShipped
Separate MAX_CALLDATA capShipped
unsafe block invariant docsBeing documented
unwrap() triage on untrusted pathsOngoing
ml-kem 0.3.0-rc -> stable upgradePost-standards-release
Persistent receipt store (archive mode)Post-mainnet
Signed-commitment mandatory inclusionPost-mainnet (Ch 9)
Pedersen / KZG commitments for PSSPost-mainnet
Algebraic batch FALCON verifyPost-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 / defenseStatus at mainnet
BFT safety f < n/3Shipped
Liveness 85/128 honest + online (2f+1)Shipped
Weak-subjectivity checkpointsShipped
FALCON peer authenticationShipped
Validator-channel filteringShipped
Evidence-ingest rate limitShipped
Per-sender mempool rate limitShipped
RPC ingress validationShipped
chain_id replay protectionShipped
Multisig-only treasury drainShipped
panic = "abort" on persist failureShipped
Set-sync(true) consensus writesShipped
WASM sandbox (wasmtime, production-vetted)Inherited from wasmtime
Deterministic-feature-subset enforcementShipped (deploy-time validator)
Host-function-level safety trapsDesigned; implementation in flight
Reentrancy guard (default-on)Designed; runtime in flight
1 MB witness size capShipped
Separate MAX_CALLDATA capShipped
Signed mempool commitmentsPost-mainnet
Pedersen / KZG PSS commitmentsPost-mainnet
Algebraic batch FALCON verifyPost-mainnet
Archive-node receipt storePost-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:

  1. 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.

  2. 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.

  3. 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)

ConstantValueMeaning
GAS_TARGET400,000,00050% of the elastic ceiling
GAS_CEILING1,600,000,0004× target — hard block ceiling
GENESIS_BASE_FEE50,000,000,000 quantaInitial value at genesis
MIN_BASE_FEE1Floor — cannot drop to zero
ADJUSTMENT_DIVISOR81/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).

ScenarioTime to 2× the fee
Sustained 100% full commits~11 commits (~5.5 s)
Sustained 4× full (max)~6 commits (~3 s)
Sustained emptyhalf-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:

LimitValue (gas)Role
Target400,000,000"Normal" block fullness
Hard ceiling (4×)1,600,000,000Cannot 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:

WorkloadGas/txTheoretical target TPSRealistic v1 (committee-bound)
Simple transfer21,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 swap200,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:

RecipientShareWhere it goes
Burn70%Increments the on-chain TOTAL_BURNED counter
Reward pool20%Pooled across all staked validators (active committee + validators awaiting selection), distributed each epoch by stake × uptime via lazy accrual
Treasury10%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 caseMechanism
Free-to-play gamesGame contract's gas tank pays for player moves
DeFi onboardingProtocol pays for first N swaps per user
Corporate dAppsCompany paymaster covers employee transactions
Airdrop claimsAirdrop contract sponsors claim transactions
Governance votingDAO 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

OperationHost functionGas
Storage readsload100 (warm)
Storage writesstore200 (warm)
Storage deletesdelete150 (no refund; cheaper than sstore)

Crypto

OperationHost functionGas
Poseidon2 hashposeidon21,000 + 6 per 32B chunk
Blake3 hashblake3100 + 1 per 32B chunk
Keccak256 hashkeccak256200 + 3 per 32B chunk
FALCON-512 verificationfalcon_verify20,000
Merkle path verificationhost fn5,000

Cross-contract

OperationHost functionGas
External callcross_call2,500 + callee work
Contract deploymentsystem tx32,000 + init code

Events

OperationHost functionGas
Emit eventemit_event375 + 8 per byte

WASM execution (per-instruction baseline)

CategoryFuel cost
Arithmetic instructions1-3 fuel per op
Memory load/store5 fuel per op
Control flow1-2 fuel per op
Memory grow200 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:

LimitValueConstant
Min gas limit21,000MIN_GAS_LIMIT
Max gas per block1.6BBLOCK_GAS_MAX
Max tx size128 KBMAX_TX_SIZE
Max calldata size64 KBMAX_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: createAccessListestimateGas → submit signed tx with the resulting access list.


10.11 Comparison

FeatureEthereum (EIP-1559)Pyde
Gas dimensions11
Base fee mechanismAlgorithmic (EIP-1559)Algorithmic (EIP-1559)
Max base-fee change/block±12.5%±12.5%
Priority fee / tipYesNo
Block elasticity2× (15M target / 30M max)4× (400M target / 1.6B max)
Fee burn100% of base fee70% of total fee
Validator shareTips only20% of total fee (no tip)
Treasury shareNone10% of total fee
Native account abstractionNo (ERC-4337 add-on)Yes (gas tanks + paymaster)
Storage rentNoneNone (gas pays for the SSTORE)
MEV bribery resistanceNone (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

PropertyValue
Gas dimensions1 (single counter)
Base fee mechanismEIP-1559, ±12.5% per block adjustment
Genesis base fee50,000,000,000 quanta
Gas target400,000,000 (50% of ceiling)
Gas ceiling1,600,000,000 (4× target — elastic max)
Priority fee / tipNone
Fee distribution70% burn / 20% reward pool / 10% treasury
Sponsored transactionsNative (gas_tank field + paymaster pattern)
Validation gas cap (paymaster)100,000
Max tx size128 KB (MAX_TX_SIZE)
Max calldata size64 KB (MAX_CALLDATA)
Min gas limit21,000
Storage rentNone

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_BURNED counter under discriminator 0x13. 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 through MultisigTx (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 TPSAnnual fee burnYear-1 mint (5%)Net change
500~5.6M PYDE50M+44.4M (inflationary)
5,000~28M PYDE50M+22M (inflationary)
10,000~45M PYDE50M+5M (near-neutral)
20,000~70M PYDE50M-20M (deflationary)
30,000~105M PYDE50M-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
}
RoleMin stakeCommittee roleEarns
Validator10,000 PYDEEligible — uniformly-random selection each epoch picks 128 of the eligible poolReward pool share (stake × uptime) + inflation share. When selected to the committee: additional activity-weighted share
RPC nodeNoneOff-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:

  1. 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 0x15 tracks the active stake-weighted total used as the denominator.
  2. 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):

OffensePenalty (% of stake)
Double signing (safety)100% + permanent ban
Equivocation (DAG fork at round)100% + permanent ban
Liveness < 90% per epoch1% per epoch
Liveness < 50% per epoch5% + jail (next epoch)
Liveness == 0% per epoch10% + forced unbonding
Invalid vertex production50% (with proof)
Decryption withholding2% per offense (jail at 3)
Sentry exposure violation1% (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:

YearActive validatorsAvg stakeTotal stakedInflationIndicative APY
1~1,000100K100M5.0%~30%
2~5,000100K500M3.0%~3.6%
3~10,000100K1B (incl. inflation)2.0%~1.2%
4+~10,000100K1B+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

DiscriminatorNameHolds
0x18AIRDROP_ROOTMerkle root of the airdrop list
0x19AIRDROP_DEADLINESlot height after which sweep is allowed
0x1AAIRDROP_CLAIMEDPer-leaf-index claim flag
0x1BAIRDROP_EXPECTED_SUMGenesis 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:

  1. Genesis allocation — direct allocation in the genesis config.
  2. Fee share — 10% of every transaction fee.
  3. Inflation share — a configurable share of per-block mint.
  4. 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

DiscriminatorNameHolds
0x1CMULTISIG_SIGNERSLength-prefixed array of FALCON pks
0x1DMULTISIG_THRESHOLDRequired signature count (u8)
0x1EMULTISIG_NONCEReplay-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 (discriminator 0x1F) = 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:

  • MultisigTx rejects if spend.target == tx.from (submitter).
  • MultisigTx rejects if tx.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 to Unbonding; their stake weight is removed from the total)
  • Slash of an Active validator (stake weight decreases, or removed entirely on jail/exit)
  • Each block where a validator's uptime_share changes (lazy, indexed by the same accumulator pattern as REWARDS_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:

PhaseNet change
Year 1–2Net mint > burn → modest inflation
Year 3–5Burn ≈ 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

PropertyValue
Native tokenPYDE
Decimals9 (1 PYDE = 10^9 quanta)
Genesis supply1,000,000,000 PYDE
Supply capNone (decreasing inflation, fee burn)
Inflation schedule5% → 3% → 2% → 1% (terminal)
Commits per year~63,113,904 (2/sec median)
Fee distribution70% burn / 20% reward pool / 10% treasury
Validator stake (min)10,000 PYDE (single tier, uniform-random committee selection)
Operator-identity cap3 validators per operator
Unbonding period30 days (must exceed 21-day safety evidence freshness)
Slashing finder fee10% of slashed amount
VestingOn-chain, balance-locked at validation
AirdropMerkle-proof claim, Sweep after deadline
Treasury spendMultisigTx (type 9) + PIP data_digest audit trail
Multisig signersUp to 16; threshold rotatable via RotateMultisig
Multisig threshold (governance)7-of-12 typical (set at launch)
Emergency pauseEmergencyPause (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:

ModelCommon failure mode
Stake-weighted token votingPlutocracy (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 upgradeReal but slow
Council multisigCentralized; depends on signer integrity

Pyde's choice is closer to the Bitcoin BIP / Ethereum EIP model than to Cosmos-style on-chain governance:

  1. Proposals are documents, not on-chain ballots. They live in a public pips repo (zarah-s/pips), open to any author, indexed and discussed in the open.
  2. 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).
  3. The on-chain treasury multisig executes spends linked to PIPs. The MultisigTx payload carries data_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 typePIP needed?
Consensus rule change (block format, finality)Yes
Gas cost changesYes
Fee distribution changes (e.g., 70/20/10 split)Yes
Cryptographic primitive changeYes
New transaction typeYes
New WASM host functionYes
Treasury spend (any size)Yes (data_digest carries hash)
Bootstrap node list updateNo (config-driven)
Bug-fix release (no protocol change)No (changelog)
Doc updatesNo

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):

DiscriminatorNameHolds
0x1CMULTISIG_SIGNERSLength-prefixed array of FALCON pks
0x1DMULTISIG_THRESHOLDRequired signature count
0x1EMULTISIG_NONCEReplay 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 > 0
  • target != Address::ZERO
  • target != treasury_address (cannot spend to self)
  • target != tx.from (writeback-clobber protection)
  • tx.to == Address::ZERO
  • MULTISIG_NONCE matches the signed payload (replay protection)
  • Number of valid signatures from MULTISIG_SIGNERSMULTISIG_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 >= 1
  • new_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:

  1. 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.
  2. Public, on-chain audit trail. Every spend has a data_digest linkable to a PIP. Off-chain spending the treasury is not possible.
  3. 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.
  4. 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:

ConstantWhere
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 schedulecrates/tx/src/fee.rs
Fee split (70/20/10)crates/tx/src/execution.rs
Gas target / ceilingcrates/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 primitivespyde-crypto polyrepo (FALCON, Kyber, Blake3, Poseidon2)
WASM host function ABIcrates/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:

ConcernHandled by
Bootstrap node listConfig — operators ship their own
Block explorerFoundation operates a public one
RPC endpointsMultiple operators run them
Indexing / data productsEcosystem builds them
Wallet integrationsEcosystem partnerships
Marketing / brandingFoundation
Conference sponsorshipsTreasury via PIP-driven multisig
Bug bounty paymentsTreasury 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

PropertyPydeEthereumCosmos / TendermintPolkadot
Protocol-rule changePIP + voluntary upgradeEIP + voluntary upgradeOn-chain governance voteCouncil + referenda
Treasury spendOn-chain multisig + PIPFoundation grantsOn-chain governanceOn-chain treasury / Council
Emergency haltMultisig pauseNone at protocol layerNone at protocol layerSudo (pre-removal)
Token votingNoneNone at protocol layerStake-weightedStake-weighted
Validator-only signalVoluntary upgradeVoluntary upgradeOn-chainCouncil inclusion
Off-chain coordination docPIPEIPForum + on-chain proposalOpenGov / Forum
Constitutional parametersAll of them, hard-codedHard-codedSome on-chainSome 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:

  1. 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.
  2. Low turnout. Most token holders don't vote. The few who do gain outsized influence.
  3. 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 a data_digest without 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

ComponentStatus at mainnet
PIP processOff-chain, in zarah-s/pips
PIP authorityDocuments intent; not protocol law
Validator upgradeVoluntary; per-release
Treasury multisigOn-chain, MultisigTx (type 9)
Multisig rotationOn-chain, RotateMultisig (type 10)
Multisig signer cap16
MultisigTx PIP linkagedata_digest = hash(pip_file) on-chain
Emergency pauseOn-chain, EmergencyPause (type 11)
Pause max window~30 days (auto-expiring)
On-chain stake-weighted votingNone
Hard-coded protocol constantsAll 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:

  1. Post-quantum from genesis. Addresses are derived from FALCON-512 public keys. There is no ECDSA legacy to migrate away from.
  2. Nonce window, not sequential. Each account gets a 16-slot nonce bitmap window — multiple in-flight txs without head-of-line blocking.
  3. 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.

FieldMutability
addressimmutable after account creation
nonceper-tx (window slides forward)
balanceper-tx
code_hashset once at deploy; never changes
storage_rootevery block that mutates the contract
account_typeimmutable
auth_keysrotatable (increments key_nonce)
gas_tankdeposit by anyone; withdraw by owner
key_nonceincrements 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

PropertyOutcome
Concurrent submissionsUp to 16 in-flight from one sender
Stuck-tx toleranceA stuck nonce N doesn't block N+1, N+2, ...
Replay protectionEach (account, nonce) usable exactly once
CancellationSubmit a different tx with the same nonce
Compact state10 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
}
}
VariantStatusUsed for
Nonev1System accounts, contracts that have no admin
Singlev1Standard EOA — one FALCON-512 public key (~897 bytes)
MultiSigv1Native multi-signature — set of keys + threshold (max 16)
Programmablev2 reservedContract-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:

  1. Signature. FALCON-verify against SessionKey.pubkey.
  2. Liveness. expires_at > current_wave and revoked == false.
  3. Scope. Target contract is in scope.contracts; if scope.methods is non-empty, the called selector is in it.
  4. 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 surfaceWhy it matters for v2 session keys
AuthKeys::Programmable enum variant (tag 0x03)The authorization model session keys plug into
Account code_hash + storage_root fieldsProgrammable 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 pipelineSame 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 caseDeadline (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 urgencyNoneindefinite

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 IDNamePurpose
9MultisigTxTreasury spend: debit treasury, credit target
10RotateMultisigRotate 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 > 0
  • target != Address::ZERO
  • target != treasury_address
  • target != 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).

IDNameWhat it does
0StandardValue transfer or contract call
1DeployContract deployment (to == Address::ZERO, data == initcode)
3StakeDepositLock ≥ 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).
4StakeWithdrawBegin 30-day unbonding
5SlashSubmit double-sign evidence (data = serialized evidence)
6ClaimRewardClaim accrued staking yield from the pool
7ClaimAirdropClaim genesis airdrop with Merkle proof
8SweepAirdropMove unclaimed airdrop residue to treasury (post-deadline)
9MultisigTxTreasury spend with multisig signatures
10RotateMultisigRotate multisig signer set + threshold
11EmergencyPauseHalt block production (multisig-signed)
12EmergencyResumeResume normal processing (multisig-signed, clears pause)
13RegisterPubkeyFirst-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":

  1. Show the JMT path from the wave-W state root (in the commit header) to Alice's account leaf.
  2. Decode the account record; read the balance field.

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

PropertyValue
Address size32 bytes (Poseidon2 hash, no truncation)
Address derivationEOA from FALCON pk; CREATE / CREATE2 from deployer
Account typesEOA, Contract, System
Auth schemesNone, Single FALCON pk, MultiSig{keys, threshold}
Address mutabilityImmutable across key rotations
Nonce window16 slots (bitmap), sliding base
Native account abstractionYes (fee_payer = GasTank / Paymaster(addr))
Multisig per-accountYes (via AuthKeys::MultiSig)
Multisig treasuryYes (MultisigTx = type 9)
Batch transactionsRemoved pre-mainnet (tag 2 reserved-as-vacant)
Transaction types13 active (Standard, Deploy, Stake*, Slash, Claim*, Sweep*, Multisig*, Emergency*, RegisterPubkey)
Validation gas cap100,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

PropertyTCP + Yamux/mplexQUIC
Connection setup1-3 RTT (TCP + TLS)0-1 RTT (integrated TLS)
Head-of-line blockingyes (all streams share)no (per-stream flow control)
Multiplexinguserspace (Yamux)native (kernel-assisted)
Connection migrationnot supportedsupported (connection IDs)
Mandatory encryptionoptional (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                      |  |
|  +-----------------------------------------------------------+  |
+---------------------------------------------------------------+
TopicParticipantsSize limitWhat it carries
pyde/vertices/1Committee primaries256 KBDAG vertices (batch refs + parent refs + state-root sigs + decryption shares + FALCON sig)
pyde/transactions/1All nodes128 KBUser transactions (plaintext or encrypted)
pyde/batches/1Workers + primaries4 MBWorker batches (hard cap; preserves modest-hardware claim)
pyde/sync/1All nodes (req/resp)16 MBSnapshot chunks (4 MB typical), historical vertices
pyde/evidence/1Validators64 KBSlashing 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:

ParameterValueWhy
validation_modePermissiveAuto-forward; see throughput note
heartbeat_interval150 msMatches DAG round cadence; amortizes mesh maintenance without blocking round progress
mesh_n8Mesh size per node
mesh_n_low4Trigger mesh expansion
mesh_n_high12Trigger mesh pruning
gossip_lazy8Number of IHAVE peers
history_length6Recent message-id buffer (heartbeats)
history_gossip3Size of the IHAVE batch
duplicate_cache_time60 sDedup window — handles small-net jitter
flood_publishtrueInitial publish reaches all mesh peers
max_transmit_size1 MBPer-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:

  1. Switch to ValidationMode::Permissive, which auto-forwards a message once the basic structural check passes.
  2. Set flood_publish = true so 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

LayerPersistenceTrust model
Hardcoded seedsbinaryChain-spec trusted
DNS recordsDNS TTLDNS operator trusted
On-chain registryJMTConsensus-finalized
PEX cacheLRU 1024Peer-attested only
Local good-peer cachedisk LRU 100Empirically known good

12.6 Connection Limits and Rate Limiting

crates/net/src/config.rs defaults:

ConstantDefaultMeaning
DEFAULT_PORT30303Default UDP listen port
DEFAULT_MAX_PEERS50Total connected peers
DEFAULT_MAX_INBOUND30Max inbound connections
DEFAULT_MAX_OUTBOUND20Max outbound connections
DEFAULT_RATE_LIMIT_PER_IP5 / secInbound connect rate per IP
DEFAULT_IDLE_TIMEOUT60 sDrop 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:

  1. AutoNAT detects whether the local node is reachable.
  2. DCUtR (Direct Connection Upgrade through Relay) coordinates QUIC hole-punching between nodes behind cone NATs.
  3. Relay nodes forward traffic for nodes behind symmetric NATs that can't be hole-punched.
  4. 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):

ChannelInboundOutbound
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:

RoleBandwidthConnections
Validator100+ Mbps symmetric50–100
Full node100 Mbps symmetric30–60
Light client1 Mbps3–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 cacheduplicate_cache_time = 60 s prevents 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:

MetricTypeMeaning
pyde_peers_connectedgaugeTotal connected peers
pyde_peers_by_rolegaugeValidators / full / unknown
pyde_gossip_messages_receivedcounterMessages received per topic
pyde_gossip_messages_sentcounterMessages sent per topic
pyde_bandwidth_inbound_bytescounterTotal inbound bytes
pyde_bandwidth_outbound_bytescounterTotal outbound bytes
pyde_block_propagation_time_mshistoTime from propose to receipt
pyde_consensus_msg_latency_mshistoRound-trip on consensus channel
pyde_dht_routing_table_sizegaugeKademlia routing table entries
pyde_falcon_handshakes_completedcounterSuccessful peer handshakes
pyde_falcon_handshakes_failedcounterVerification 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/1 channel.
  • 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

ComponentChoice
Transportlibp2p over QUIC (TCP fallback)
libp2p identityEd25519 (PeerId routing only)
Application identityFALCON-512 (vertex sigs, attestations, evidence)
Channels5 — vertices / transactions / batches / sync / evidence
Validator channel filterFALCON pubkey ∈ current committee
Gossipsub modePermissive + flood_publish = true
Heartbeat150 ms (matches DAG round cadence)
Mesh size8 (low 4, high 12)
Peer handshakeFALCON-512 attestation; binds peer_id → falcon_pk
DiscoveryLayered: seeds → DNS → on-chain registry → PEX → cache (no DHT)
Committee defenseSentry node pattern (Cosmos-style)
Connection limits50 total / 30 inbound / 20 outbound (defaults)
Rate limit (per IP)5 / sec (defaults)
Symmetric encryptionTLS 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:

  1. 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.
  2. 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_call interface 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:

ApproachTrust assumption
Trusted relayA multisig or enclave attests to events
Light-client proofChain 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:

  1. No new trusted parties. No multisig "guardians" sit between Pyde and the counterparty chain.
  2. Light-client verification. The counterparty chain runs a Pyde light client (FALCON verification + finality cert) that proves "block N was hard-finalized on Pyde."
  3. 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:
    1. A HardFinalityCert for the commit that included the event.
    2. A Merkle proof from the wave's blake3_state_root (native) or poseidon2_state_root (ZK-circuit-friendly) to the event's storage slot.
  • 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:

  1. 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.
  2. 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.
  3. Smart contract → foreign L1 (interface available at v1; transport ships post-mainnet). Until the cross-chain transport lands, this returns NotYetSupported at runtime — but contract code written against cross_call to 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.

DirectionMechanismDifficulty
Pyde → EthereumEthereum contract verifies Pyde finality certsModerate (FALCON in EVM)
Ethereum → PydePyde contract verifies Ethereum execution proofsModerate (Merkle Patricia)
Pyde → BitcoinSPV-style proofs of Bitcoin finalityHard (PoW finality is probabilistic)
Pyde → other PoS L1sEach side verifies the other's signature schemeVariable

The Ethereum bridge is the most concrete near-term target post-mainnet. The work splits into:

  1. An Ethereum-side contract that verifies FALCON signatures and HardFinalityCert structures. FALCON-512 verification in EVM is non-trivial (algebraic operations over a 12,289-mod ring) but not fundamentally blocked.
  2. 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.
  3. 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:

  1. Define an oracle: Address storage field.
  2. Allow only that address to write to a prices map.
  3. 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

StageCross-chain capability
Mainnet (v1)Parachain framework live (WASM-based); cross_call host function available; HardFinalityCert format stable
Post-mainnet — Stage 1First production parachains deployed (DEX, oracle, etc.)
Post-mainnet — Stage 2First Ethereum bridge (FALCON-verifier on EVM + Pyde-side Patricia verifier)
Post-mainnet — Stage 3Multi-chain bridges (additional foreign L1s)
Post-mainnet — Stage 4ZK-aggregated FALCON signatures (reduces bridge verification cost dramatically)
Post-mainnet — Stage 5zk-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

CapabilityAt mainnet?Post-mainnet plan?
Sovereign L1Yes
Hard-finality certificate (cert format)YesUsed by future bridges
Parachain framework (WASM-based)YesProduction parachains roll in over time
Cross-parachain messagingYes (with framework)Optimizations + ZK aggregation
cross_call host function (interface)YesForeign-chain transports wired post-mainnet
Smart-contract → smart-contract callsYes (working)Performance optimizations
Smart-contract → parachain callsYes (with framework)
Smart-contract → foreign L1 callsInterface only, returns NotYetSupportedWired when bridges ship
Native bridge to EthereumNoYes (FALCON-in-EVM)
Native bridge to BitcoinNoMaybe (SPV proofs)
Off-chain oracle / multisig mintsPossible at app layerSame as today
Light-client contracts (Ethereum)Possible at app layerEasier 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-sdk but idiomatic TS
  • Browser-friendly via tree-shaking + WASM crypto bridge
  • Type-safe ABI generation from abi.json artifacts
  • 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's eth_call. Bounded by RPC-layer rate limits + per-call instruction cap.
  • Subscription methods (WebSocket) — pyde_subscribe:
    • newHeads — wave commits as they finalize
    • accountChanges — state changes to a specific account
    • logs — 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 executionview-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 compilerRetired; archived
wright — project CLIRetired; archived. Role taken by the new otigen binary
.oti source filesReplaced by author's language of choice (.rs, .ts, .go, .c)
PVM bytecode artifactsReplaced by WASM .wasm artifacts
Otigen-specific testsReplaced by author's language's native test runner
pyde.toml configReplaced 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

ToolRepo
otigen developer toolchainpyde-net/otigen
pyde node binary + enginepyde-net/engine
pyde-rust-sdkpyde-net/pyde-rust-sdk
pyde-ts-sdkpyde-net/pyde-ts-sdk
pyde-crypto-wasmpyde-net/crypto-wasm
Archived otic compilerpyde-net/otic (archived)
Archived wright toolchainpyde-net/wright (archived)
The Otigen Book (historical)pyde-net/otigen-book (preserved as historical artifact)

17.7 Reading on

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.

CategoryExampleProcess required
Operational config updateBootstrap node list, log levelOperator-side; no PIP
Bug fix (no protocol change)Memory leak, RPC parse bugCode release; PIP not required
Backward-compatible featureNew opcode unused by existing contractsPIP + voluntary upgrade; no fork
Backward-incompatible (hard fork)Gas cost change, new tx type semanticsPIP + activation block + coordinated upgrade
Cryptographic primitive changeHash migrationPIP + multi-version overlap window
Treasury actionGrant payout, audit fundingPIP + on-chain MultisigTx
Emergency responseActive exploitEmergencyPause (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:

ConstantWhere
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 schedulecrates/tx/src/fee.rs
Fee split (70/20/10)crates/tx/src/execution.rs
Gas target / ceilingcrates/tx/src/fee.rs
Tx / calldata size limitscrates/tx/src/validation.rs
Max batch size (4 MB)crates/mempool/src/batch.rs
Cryptographic primitivespyde-crypto polyrepo (FALCON, Kyber, Blake3, Poseidon2)
WASM host function ABIcrates/wasm-exec/src/host_fns.rs + Host Function ABI spec doc

Changing any of these requires a release + voluntary upgrade.

On-chain (multisig-controlled)

ItemMechanism
Treasury spendMultisigTx (type 9)
Multisig signer setRotateMultisig (type 10)
Emergency pauseEmergencyPause (type 11)
Resume from pauseEmergencyResume (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

ItemLives in
Bootstrap peer listpyde.toml [network] bootstrap_peers
RPC endpoint configpyde.toml [rpc]
Log level / formatpyde.toml [logging]
Metrics portpyde.toml [metrics]
Datadir locationpyde.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).

ComponentVersion source
Node binarypyde --version
otigen developer toolchainotigen --version
pyde-rust-sdk crateCargo.toml version
pyde-crypto-wasm pkgpackage.json
Host Function ABI versionembedded in the artifact
Contract ABI versionembedded 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 identify libp2p 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

PropertyPydeEthereumTezos / Cosmos
Off-chain proposalPIPEIPTIP / CIP
On-chain governance voteNoneNone at protocol levelYes (stake-weighted)
Validator upgradeVoluntaryVoluntaryOn-chain "self-amendment"
Hard-fork coordinationActivation block + socialActivation block + socialVoted on-chain
Treasury actionOn-chain multisig + PIPFoundation grantsOn-chain (Tezos), proposal (Cosmos)
Emergency haltMultisig pauseNoneSometimes (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

PropertyStatus at mainnet
Upgrade modelPIP + voluntary validator upgrade
Hard fork mechanismActivation block + coordinated upgrade
Soft fork mechanismSame; old nodes stay in sync
Treasury actionOn-chain MultisigTx + PIP linkage
Emergency responseEmergencyPause (≤30 days, auto-expiring)
State migration patternsLazy / activation-block / migration tx
Wire-format versionsEVIDENCE_VERSION, MULTISIG_VERSION (bumped on layout change)
On-chain validator-upgrade signalNone (out-of-band tracking)
Automatic rollbackNone (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:

  1. 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.

  2. 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.

  3. 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:

PhaseBar
Pivot foundationsDocumentation, repo cleanup, foundational design specs
Engine cleanupPre-pivot crates removed from active workspace; archived for reference
WASM execution hardeningSingle-language end-to-end (contracts deploy, execute, modify state, state verifiable)
Multi-language + parachain frameworkAll supported languages working; parachain governance + lifecycle complete
Public testnetMulti-region committee, external developers building real contracts
Audit + stress + bug bountyExternal audit complete; all critical findings resolved; stress testing passed
Mainnet candidateFinal 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 otigen developer toolchain with Rust, AssemblyScript, Go (TinyGo), and C/C++ support.
  • The Rust and TypeScript SDKs.

Mainnet does not ship with:

  • Programmable accounts (post-mainnet — Programmable enum 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

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

  1. Layered Architecture
  2. Consensus: Mysticeti DAG
  3. Cryptography
  4. Execution Layer
  5. State Layer
  6. Account Model
  7. Transaction Lifecycle
  8. Encryption & MEV Resistance
  9. Network Protocol
  10. Performance Targets
  11. 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 = 42 Byzantine 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 caseHashWhy
JMT internal nodesBlake3~30× faster than Poseidon2 on CPU
State root (published)BothBlake3 native verification + Poseidon2 for ZK
Transaction hashesBlake3 (ciphertext), Poseidon2 (plaintext canonical)Per use
Address derivationPoseidon2Used in sig-verify ZK circuits
FALCON sig hashingPoseidon2Inside ZK aggregation circuit
Vertex hashesBlake3Small volume, no ZK

Random Beacon

Each epoch's beacon is produced by the previous epoch's committee:

  1. All 128 members sign a 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 randomness
  4. 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 reentrant attribute)
  • Checked arithmetic (wrapping ops require explicit opt-in)
  • Typed storage via [state] schema in otigen.toml
  • No tx.origin (host function ABI exposes only caller)
  • 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 typeState retention
Archive nodeAll historical state
Full node (default)Last 90 days
Committee validatorLast 30 days
Light clientHeaders + 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_spend is monotonic — increasing it requires a new key, not a mutation.
  • expires_at cannot exceed current_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 protection
  • pyde_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):

MetricRealistic v1Stretch v2Aspirational
Plaintext TPS (commodity)10K-30K50K-100K500K
Encrypted TPS (commodity CPU)500-2K5K-10K50K+ (GPU)
Median finality~500ms~400ms~300ms
Committee NIC requirement (at TPS)500 Mbps1 Gbps10 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

RoleHardware
Light clientMobile / browser
Full node / RPC8c / 16GB / 500GB / 100 Mbps
Non-committee validator8c / 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:

ComponentStatus
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

TopicDocument
Threats & adversariesTHREAT_MODEL.md
Operational failuresFAILURE_SCENARIOS.md
Halt + recovery proceduresCHAIN_HALT.md
Slashing rulesSLASHING.md
Validator state machineVALIDATOR_LIFECYCLE.md
State sync protocolSTATE_SYNC.md
Network protocolNETWORK_PROTOCOL.md
Performance harnessPERFORMANCE_HARNESS.md
Token economicsTOKENOMICS.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:

  1. Post-quantum cryptography — FALCON-512 signatures, Kyber-768 threshold encryption, Poseidon2 + Blake3 hybrid hashing. No pre-quantum primitive on any consensus or account path.
  2. 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.
  3. Sub-second finality — Mysticeti-style DAG consensus, ~500 ms median commit finality, an 85-of-128 FALCON quorum certificate.
  4. 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):

  1. 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.
  2. 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.
  3. 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:

TierStakeCommittee roleEarns
Validator10K PYDE min (single tier)Eligible for uniform-random committee selection each epochReward-pool share (stake × uptime) + inflation share + activity-weighted bonus while on active committee
RPC node / full nodeNoneNoneOff-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_bitmap rather 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

UseHashReason
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 — ciphertextBlake3Gossip / dedup, not in ZK
Transaction hashes — plaintext canonicalPoseidon2Inside sig-verify ZK circuits
Address derivationPoseidon2Poseidon2(falcon_pk) exposed to sig-verify circuits
FALCON sig payload hashingPoseidon2Inside 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 HotStuffDAG resolution
Single proposer bottleneckNo proposer — every member contributes vertices each round
View-change protocol complexityNo view changes — eliminated an entire failure class
Timing-driven slot pipelineData-driven rounds advance with quorum, not clock
Proposer can selectively censor127 honest members can include any tx; censorship requires near-unanimous collusion
Throughput limited by leader bandwidthThroughput 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:

  1. Anchor's subdag is collected by walking parent_vertex_refs transitively.
  2. The subdag is sorted deterministically: (round, member_id, list_order).
  3. Batches referenced by each vertex are dereferenced.
  4. 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_shares field of vertices observed during the prior rounds).
  5. wasmtime executes decrypted transactions in canonical order.
  6. State root is computed (Blake3 + Poseidon2 dual), FALCON-signed by ≥ 85 committee members.
  7. 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:

ClassTriggerAuthority
Soft stallNetwork / quorum slackEmergent (auto-recovers)
Hard haltContradictory state roots, equivocation cluster, DAG forkProtocol-detected automatic
Emergency haltOff-chain bug report, active exploit, hard-fork prepGovernance 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-vm register-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), not block.proposer — Pyde's DAG has no single proposer, so contracts that depended on block.proposer on 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:

ModeRealistic v1Stretch v1Aspirational
Plaintext TPS (sustained, commodity)10 K – 30 K50 K – 100 K500 K
Encrypted TPS (sustained, commodity)0.5 K – 2 K5 K – 10 K50 K + (GPU)
Median commit finality~ 500 ms~ 400 ms~ 300 ms
Committee NIC at sustained TPS500 Mbps1 Gbps10 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

RoleHardware
Light clientMobile / browser
Full node / RPC8c / 16 GB / 500 GB NVMe / 100 Mbps
Non-committee validator8c / 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

TierMinimum stakeRole
Validator10,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:

OffenseFirst instanceMax (correlation / repeat)
Equivocation10 %50 %
Bad state-root signature10 %50 %
Bad anchor attestation5 %20 %
Invalid vertex5 %30 %
Bad decryption share5 %30 %
DKG failure2 %10 %
Share withholding (per round)0.1 %5 % / epoch
Extended downtime (per round)0.05 %10 % / epoch
Bad batch attestation2 %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

AxisPydeEthereum (L1)SolanaAptosSuiPolkadotCosmosAvalanche
Post-quantum signatures (default)Yes (FALCON-512)RoadmapRoadmapRoadmapRoadmapRoadmapRoadmapRoadmap
Encrypted mempool (default)Yes (Kyber-768 threshold)No (PBS auction)No (Jito auction)NoNoNoProposals in IBC trackNo
Sandwich-attack preventionStructuralPartial (PBS)Partial (Jito)PartialPartialN/A (relay-chain)PartialPartial
Sustained throughput target10 K – 30 K v1 (harness-validated)~ 15 TPS L1High peak; outage historyLab highLab highVariable (per parachain)Variable (per zone)High (per subnet)
Hard-finality time~ 500 ms (DAG commit)~ 12 minProbabilistic (~ 13 s)< 1 s< 1 s~ 12 – 60 s~ 6 s~ 1 s
Validator hardware8c / 16 GB / 500 GB / 100 Mbps (awaiting committee)Modest12 + cores / 256 + GBModestModestModest (validator tier)Modest (per zone)Modest
Equal validator votingYes (1 = 1)Stake-weightedStake-weightedStake-weightedStake-weightedStake-weightedStake-weightedStake-weighted
Permissionless cross-chain infra layerRoadmap (parachain spec, PYDE-staked, unified gas)L2s (per-L2 sequencer); third-party oraclesThird-party (Pyth / Switchboard)NoNoApp-chains via auctionsIBC 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:

  1. 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.
  2. 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.
  3. 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 otigen developer 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.
  4. 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.
  5. 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

ParameterValueNotes
MIN_VALIDATOR_STAKE10,000 PYDESingle-tier minimum; any validator meeting this threshold enters the eligible pool for uniform-random committee selection
MAX_VALIDATORS_PER_OPERATOR (cap)3Anti-Sybil; enforced on operator identity, not stake
BONDING_PERIOD1 epoch (~3 hours)Time from registration to active eligibility
UNBONDING_PERIOD30 daysLong enough for safety evidence to surface
EVIDENCE_FRESHNESS_SAFETY21 daysMust be < unbonding period
EVIDENCE_FRESHNESS_LIVENESS1 epochReal-time only
KEY_ROTATION_INTERVALMax once per epochPrevents rotation abuse
JAIL_PERIOD_1ST24 hoursFirst jail
JAIL_PERIOD_2ND7 daysWithin 30 days of first
JAIL_3RDPermanent3rd jail = permanent removal
UNJAIL_FEE10 PYDEAnti-griefing
SLASHING_ESCROW24 hoursDispute window before slash finalizes
NEW_VALIDATOR_GRACE_EPOCHS150% reduced slashing in first epoch

Pseudocode convention. Where this document writes MIN_STAKE in pseudocode below, it refers to MIN_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 slotRequired stake
1st10,000 PYDE
2nd10,000 PYDE
3rd20,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


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

  1. Safety vs Liveness distinction — different severity, detection, and slash amounts
  2. Correlated slashing for safety — coordinated attacks lose more
  3. Permissionless evidence — anyone can submit cryptographic evidence; reporter reward incentivizes monitoring
  4. Bounded slashing — per-epoch caps prevent stacking attacks

Offense Catalog

Safety Offenses (Severe, Cryptographic Evidence)

#OffenseFirst instanceMax (correlation/repeat)JailDistribution
1Equivocation (vertex) — two different vertices for same (round, member_id)10%50%1 epoch50% burn / 30% treasury / 20% reporter
2Bad state-root signature — two contradictory state roots for same commit10%50%1 epochSame as above
3Bad anchor attestation — vertex's prev_anchor_attestation contradicts 85+ honest majority5%20%1 epochSame as above
4Invalid vertex structure — parent refs out of order, refs to non-existent batches5%30%1 epoch100% burn
5Bad decryption share — partial that provably doesn't combine correctly5%30%1 epoch50% burn / 30% treasury / 20% reporter

Liveness Offenses (Auto-Detected, Graduated)

#OffensePer-eventPer-epoch capJailDistribution
6DKG participation failure — invalid or missing shares during DKG2%10%Until next epoch100% burn
7Share withholding — no decryption share when expected0.1%/round missed5%/epochAfter 100 consecutive missed100% burn
8Extended downtime — no vertices produced for N consecutive rounds0.05%/round10%/epochAfter 5% reached100% burn
9Bad batch attestation — worker gossips batch with invalid txs2%5%None (warning)100% burn

Future / Deferred

#OffenseStatus
10Censorship (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 to avoid disproportionate punishment in bug scenarios.

Other offendersMultiplierEffective slash (equivocation 10%)
11.02×10.2%
101.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


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

ModeUse CaseTime
Full sync (genesis replay)Archive nodes onlyInfeasible at high TPS
Snapshot sync (default)Most full nodes, new committee joiners~30-60 min on commodity
Light client syncMobile wallets, browser, dApp backendsSeconds-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

Componentv1 mainnet5-year projection
Account state (~10M accounts × ~150B)150 MB – 1.5 GB5-10 GB
Contract storage (~5× accounts × 64B)500 MB – 3 GB20 GB
Contract code (~50K contracts × 50KB)~2.5 GB20 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:

  1. Downloading genesis (~5 MB, includes committee_0 pubkeys)
  2. Downloading intermediate manifests (~5 KB each, hundreds at scale)
  3. Verifying chain forward: each manifest signed by prior committee
  4. 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 typeState retentionBlock retention
Archive nodeAll historical stateAll blocks since genesis
Full node (default)State for last 90 daysBlocks for last 30 days
Committee validatorState for last 30 daysBlocks for last 8 epochs
Light clientHeaders + cared-about accountsHeaders only

Tunable per-node. Archive nodes earn slightly higher RPC fees for serving historical queries.

Failure Modes & Recovery

FailureDetectionRecovery
All peers serve bad dataManifest sig failsTry more peers, ban liars
Snapshot corruption mid-downloadChunk hash mismatchBan peer, retry chunk from another
Manifest signed by wrong committeeSig verify failsReject manifest, find another
Network outage during syncConnection droppedResume from last verified chunk
Snapshot too old (> evidence window)Sig set might be slashedUse 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


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

TypeTriggerSeverityAuthorityRecovery
Soft stallNetwork/quorum issuesLiveness onlyEmergent (any node detects)Wait (auto-resume)
Hard haltDetected inconsistency (state root divergence, equivocation cluster)Safety riskProtocol-detected automaticManual investigation
Emergency haltCritical bug, active exploit, hard-fork prepHigh intentionalGovernance 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

ActivitySoft StallHard HaltEmergency Halt
Vertex productionContinues (no quorum)StopsStops
CommitsPausedPausedPaused
Tx submissionAccepted, queuedAccepted, queuedAccepted, queued
Decryption ceremoniesPausedStoppedStopped
DKG ceremoniesContinues unless triggeredStoppedStopped
State queriesContinueContinue (forensic)Continue
Slashing evidence acceptanceContinuesContinuesContinues
GossipContinuesContinuesContinues

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:

  1. Soft stall drills: deliberately offline 43 validators, verify recovery
  2. Hard halt drills: inject state divergence, verify detection + flow
  3. Emergency halt drills: practice multisig coordination
  4. Rollback drills: practice 1-epoch rollback procedure
  5. 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


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

AssetValueLoss impact
User fundsCriticalDirect financial loss to users
State integrityCriticalChain becomes untrustworthy
MEV resistanceCriticalCore value proposition
Validator stakeHighSlashing must be fair
LivenessHighChain stops being useful
PrivacyHighEncryption promise violated
Cross-chain integrityHighBridges hacks have caused $3B+ historical losses

2. Adversary Model

Adversary Types

TypeMotivationResourcesLikelihood
MEV bot operatorProfitModest infrastructure, deep mempool knowledgeHigh
Economic actorProfit (large)Significant capital, can stakeMedium
Coordinated cartelCombined economic gainLarge stake + infrastructureMedium
State adversaryGeopolitical, censorshipNation-state resources, BGP controlLow but high-impact
Insider (validator)Profit, sabotageHas stake, share, software accessLow but high-impact
Cryptographic adversaryResearch or destructionMathematician + computeLow
Quantum adversaryLong-term destructionFuture quantum computerVery low (decade+)
Network adversaryDisruptionISP / BGP positionLow
Software supply chainVariousDependency accessMedium
Social attackerVariousSocial skillsMedium

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

IDThreatSeverityDetectionMitigation
T-CONS-1Equivocation (validator signs contradictory messages)HighCryptographic evidenceEquivocation slashing 10-50%
T-CONS-2Long-range attack (rewrite history)MediumState root signatures, finalityBounded rollback (1 epoch), weak-subjectivity checkpoints
T-CONS-3Bad state-root signingHighContradictory roots for same commitBad-state-root slashing 10%, correlation multiplier
T-CONS-4Anchor predictability exploitationMediumPublic beacon analysisLookback state-root randomness
T-CONS-5Adaptive corruption (mid-epoch)MediumLiveness slashingEpoch boundary commitment, slashing accumulation
T-CONS-6Slashing race (withdraw before slash applies)HighUnbonding periodUnbonding (30d) > evidence freshness (21d)
T-CONS-7DAG cycle / invalid parent refsCriticalStructural validationAuto-reject vertex, slash producer
T-CONS-8Coordinated proposer attackHighDAG has no proposerStructurally impossible

Cryptographic Layer

IDThreatSeverityDetectionMitigation
T-CRYPT-1FALCON key compromise (single validator)MediumAnomaly detectionKey rotation, HSM recommended
T-CRYPT-2Kyber threshold compromise (≥85)CriticalDKG outputHonest BFT majority assumption; per-epoch refresh
T-CRYPT-3Hash collision (Blake3 / Poseidon2)Very lowCryptanalysisStandardized primitives, dual hash strategy
T-CRYPT-4Threshold decryption side-channelLowAuditConstant-time implementation
T-CRYPT-5DKG manipulation (force bad key)MediumDKG validationPedersen DKG with public commitments, slashing
T-CRYPT-6Random beacon biasMediumOutput analysisThreshold-sig beacon (no single party controls)
T-CRYPT-7Future quantum on archived encrypted txsLong-termN/AOut of scope; PQ primitives best available

MEV / Economic Layer

IDThreatSeverityDetectionMitigation
T-MEV-1Front-running via early decryptionHighN/ACommit-before-reveal invariant enforced
T-MEV-2Sandwich attacksHighN/APlaintexts hidden until order committed
T-MEV-3Liquidation racingMediumN/AMitigated by encryption + commit-before-reveal
T-MEV-4Time-bandit attacksHighFinalityBounded rollback, slashing
T-MEV-5Validator-builder collusionMediumN/ANo proposer-builder separation; DAG eliminates surface
T-MEV-6Stake concentration → control 43+ committeeHighPublic stake stateAnti-Sybil (operator identity cap), stake cap
T-MEV-7Bribery of committee for orderingMediumBehavior analysisEqual-power voting + slashing makes bribery expensive
T-MEV-8Censorship (selective exclusion)HighDetection hard127 others can include; censorship requires near-unanimous

Network Layer

IDThreatSeverityDetectionMitigation
T-NET-1Eclipse attack (isolate target)MediumPeer diversity analysisAnti-eclipse: diverse IPs/ASNs, persistent peers
T-NET-2DDoS on committee validatorHighTraffic analysisSentry node pattern, rate limits, peer scoring
T-NET-3BGP hijack / route manipulationLow (rare)Out-of-bandOut of scope (network responsibility)
T-NET-4Sybil on peer discoveryMediumIP/ASN concentrationLayered discovery (not DHT), peer score
T-NET-5Message flooding / spamMediumRate limitsPer-peer rate limiting, gas tank requirement
T-NET-6Network partition (deliberate or accidental)MediumQuorum detectionPartition-aware slashing pause; halt detection

Economic / Governance Layer

IDThreatSeverityDetectionMitigation
T-ECON-1Stake concentration (rich operator, many cheap validators)HighOn-chain analysisOperator identity binding, max 3 per operator
T-ECON-2Validator collusion (43+ coordinated offline DoS)HighQuorum detectionSlashing + partition handling
T-ECON-3Treasury attacks (governance capture)MediumPublic proposalsOff-chain governance, transparent PIP process
T-ECON-4Multisig compromise (emergency halt abuse)HighMulti-key threshold7-of-12 multisig, slashable malicious unhalt
T-ECON-5Token price collapse → slashing economics brokenMediumMarket dataNumbers tunable, treasury can adjust

Software / Implementation Layer

IDThreatSeverityDetectionMitigation
T-SW-1WASM execution non-determinism bugCriticalState root divergenceExtensive testing, formal verification, halt detection
T-SW-2Toolchain binding-generator bugHighContract test failuresPer-language generator audits, fuzz testing across all four targets
T-SW-3FALCON sig side-channelLowTiming analysisConstant-time implementation
T-SW-4Memory corruption (buffer overflow)HighRust borrow checker, auditsUse safe Rust, audit unsafe blocks
T-SW-5Cryptographic library bugHighAuditsUse well-audited libraries (RustCrypto)
T-SW-6State corruption (disk errors)MediumSnapshot verificationJMT 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.

IDThreatSeverityDetectionMitigation
T-AUTH-1Session-key theft (compromised dApp leaks key)MediumUser 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-2Revoked-key replay (attacker submits tx signed by previously-revoked session key)LowAuthorization-time revoked checkRevocation is on-chain state; tx rejected at validation with KeyRevoked
T-AUTH-3Scope expansion via mutable storage manipulationHighPolicy WASM auditPolicy WASM runs in restricted-state mode; cannot modify own scope without main-key signature on a RegisterSessionKey/UpdateScope tx
T-AUTH-4Session-key squatting (creating many keys to flood storage)LowPer-account session-key countHard limit (32 active session keys per account); spent storage refunded on revocation
T-AUTH-5spent_so_far overflow attackLowu128 arithmetic checks at authorizationSaturating addition + max_spend ≤ u128::MAX / 2 registration check
T-AUTH-6Expired-key acceptance (clock skew at wave boundary)LowAuthorization-time expires_at checkWave is the authoritative clock; no off-chain time source enters the check

Social Layer

IDThreatSeverityDetectionMitigation
T-SOC-1Phishing of operators / multisigHighOut-of-bandOperator training, HSM, multisig for high-value ops
T-SOC-2Misinformation during incidentMediumMultiple channelsFoundation as authoritative source, clear comms protocol
T-SOC-3Insider threat (developer / foundation)MediumCode review, multisigMulti-sig deployments, public PIP review
T-SOC-4Supply chain attack on dependenciesHighCargo.lock auditReproducible builds, dependency review

5. Mitigation Cross-Reference

MitigationSpecification
BFT 85/128 quorum + DAG consensusSee WHITEPAPER §5
SlashingSee SLASHING.md
Threshold encryption + commit-before-revealSee 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 proceduresSee CHAIN_HALT.md
Network defenses (DoS, eclipse)See NETWORK_PROTOCOL.md
Performance harness validates resilienceSee PERFORMANCE_HARNESS.md
Equal-power committeeSee WHITEPAPER §5.5
Honest throughput claimsSee WHITEPAPER §11

6. Residual Risks (Acknowledged, Not Fully Mitigated)

These are risks Pyde cannot fully eliminate:

  1. Coordinated 85+ validator collusion — out of BFT scope. If 85+ collude, safety can be violated. Mitigation: economic disincentives + stake distribution + operator identity cap.

  2. Quantum compute breaking PQ primitives in <10 years — not currently feasible to defend; PQ choice is the best available.

  3. 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.

  4. Single-validator key compromise — validator loses ≤1 vote of influence. Mitigation: key rotation, HSM, multisig validator (v2 feature).

  5. Foundation multisig compromise — 7+ of 12 hostile = emergency halt abuse. Mitigation: diverse multisig members, public visibility, slashable malicious unhalt.

  6. Network-level adversary (BGP, ISP) — out of protocol scope. Mitigation: encourage geographic + provider diversity.

  7. 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:

  1. Verify the threat catalog is complete (no missing categories)
  2. Verify each mitigation is actually implemented (trace to code)
  3. Verify residual risks are acceptable for the asset values
  4. Verify trust assumptions are reasonable for production
  5. 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

PatternRecommendation
Multiple validators affected togetherEncourage geographic + provider + ISP diversity
Operational mistakesHSM, multisig for critical ops, runbooks
Software bugsBug bounty, formal verification, extensive testing
Network issuesPartition-aware slashing, sentry nodes, diverse routes
Time to recoveryPre-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

DrillFrequencyFormat
Validator restartQuarterlyLive (testnet)
Network partitionQuarterlyLive (testnet)
State root divergenceQuarterlyLive (testnet, injection)
DKG failureAnnualLive (testnet)
Active exploitAnnualSimulated
Coordinated attackAnnualPaper only
Key compromiseAnnualPaper only
Multisig key eventAnnualPaper only
Genesis discrepancyPre-launch onlyPaper review
Cloud outageQuarterlyLive (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


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

ChoiceRationale
Transport: QUIC (over UDP)No HOL blocking, built-in TLS 1.3, mature in Rust (quinn)
Fallback: TCPCompatibility 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

ParameterDefaultNotes
MAX_CONNECTIONS200Tunable
MIN_OUTBOUND_CONNECTIONS8Tunable
MAX_CONNECTIONS_PER_IP5Tunable
MAX_CONNECTIONS_PER_ASN50Anti-clustering
INBOUND_CONNECTION_LIMIT100Tunable
CONNECTION_TIMEOUT10sTunable
HANDSHAKE_TIMEOUT5sNot 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

TypePriorityTypicalHard Limit
Ping / PongLow16B64B
PeerExchangeLow1KB8KB
VertexAnnouncementHigh40B64B
VertexRequestHigh32B64B
VertexDataHigh4KB64KB
BatchAnnouncementMed40B64B
BatchRequestMed32B64B
BatchDataMed50-200KB4MB
DecryptionShareHigh1KB2KB
StateRootSigHigh738B1KB
TxSubmission (plain)Med500B8KB
TxSubmission (encrypted)Med1.5KB8KB
ManifestRequestLow32B64B
ManifestDataLow5KB64KB
ChunkData (state sync)Low4MB4MB

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 LimitModest hardware fitMax TPS support
2 MBStrongest~30K
4 MB (chosen)Strong~100K
8 MBMixed~200K
16 MBAspirational~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

  1. Each node maintains "meshes" per topic (subscribed peers, default 6-8)
  2. Messages flood through the mesh first
  3. Lazy push: message IDs (8 bytes) sent more broadly; full message pulled on demand
  4. Heartbeat every second prunes / repairs mesh

Pyde Topics

TopicSubscribers
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/plainAll validators + RPC nodes
pyde/mempool/encryptedAll validators + RPC nodes
pyde/state_sync/manifestsSync-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

LimitDefaultPer
Vertex announcements10/sPer peer
Vertex data requests20/sPer peer
Batch announcements100/sPer peer
Batch data requests50/sPer peer
Tx submissions100/sPer peer (lower for unknown)
State sync requests10/minPer peer
PEX requests1/minPer 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_tank field) — 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:

  1. Network ID (Ed25519): used by libp2p for connection-level identity. Rotatable.
  2. Validator FALCON pubkey: consensus identity, registered on-chain. Rotatable per epoch.
  3. 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


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

  1. Reproducibly measure end-to-end performance under realistic conditions
  2. Detect regressions automatically on code changes
  3. Validate claims before they're published externally
  4. Find limits before they bite in production
  5. 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

TopologyValidatorsRegionsUse
Local devnet41 (localhost)Smoke tests, dev iteration
Single-region testnet161 (single datacenter)Component testing
Multi-region testnet16-323 (US, EU, APAC)Realistic perf testing
Production-sim1284+ (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 60s
  • tps_burst — peak sustained over 10s
  • tps_pending — txs in mempool / queued

Latency Metrics (Percentiles p50, p90, p99, p99.9)

  • tx_submission_to_finality — end-to-end
  • tx_in_batch_latency — submit → in batch
  • batch_to_vertex_latency — batch → referenced by vertex
  • vertex_to_commit_latency — vertex → commit
  • commit_to_execution_latency — commit → wasmtime executed
  • decryption_ceremony_latency — start partial → ≥85 received

Consensus Metrics

  • round_advance_rate — rounds/sec per validator
  • vertex_certification_rate — % of vertices that get 85+ certs
  • commit_success_rate — % of rounds where commit fires
  • anchor_selection_success_rate — % of anchors that have valid vertex

Resource Utilization (Per Validator)

  • cpu_usage_pct — total CPU
  • cpu_per_subsystem — consensus / wasmtime / network / IO
  • memory_resident_mb / memory_heap_mb
  • disk_read_iops / disk_write_iops / disk_used_gb
  • network_in_mbps / network_out_mbps
  • open_file_descriptors / tcp_connections

State Metrics

  • jmt_depth_max / jmt_depth_avg
  • state_root_compute_ms (per commit)
  • state_growth_per_hour_mb

Network Metrics

  • peer_count
  • peer_score_distribution
  • messages_per_second (by type)
  • bandwidth_per_message_type
  • failed_message_rate

Validator-Specific

  • slashing_events_per_epoch
  • dkg_ceremony_time_ms
  • epoch_transition_time_ms

Soak Test Schedule

TestDurationFrequency
Smoke5 minEvery commit (CI)
Short soak1 hourDaily
Standard soak4 hoursWeekly
Extended soak24 hoursPre-release
Pre-launch soak7 daysBefore 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:

TestPass Criteria
Steady-state 30K TPS4 hours @ 30K, p99 <1s, no stalls
Burst 100K TPS60s burst absorbed, queue drains in 5 min
Validator restart loop24h with restarts every 5 min, no stall
Network partition30% partition for 5 min, both recover, no fork
DKG under loadEpoch transition at 30K TPS, no commit stall
State sync under loadNew node joins at 30K TPS, syncs in <1 hour
Slashing under loadEquivocation slashed within 1 epoch
7-day soak10K TPS for 7 days, no memory leak, no drift
Encrypted tx mix30% encrypted at 30K TPS, decrypt latency <500ms
Modest hardwareSingle 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

ComponentEffort
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


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:

TermMeaning
Smart contractA WASM module deployed via otigen that shares Pyde's general state space and runs on the main executor.
ParachainA 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 bridgeInfrastructure 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:

  1. 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.
  2. 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.
  3. 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:

  1. Name is well-formed (1-32 chars, alphanumeric + hyphens).
  2. Name is not already registered (uniqueness check via registry).
  3. WASM module is well-formed and instantiable under Pyde's deterministic wasmtime config.
  4. WASM imports only functions in the parachain ABI allowlist (§11).
  5. validator_set ⊆ current main committee; size ≥ config.min_validators.
  6. Owner has paid the deploy fee + has the owner deposit available.
  7. 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:

  1. The transaction includes the upgrade in the next wave's commit.
  2. A ParachainVersionRecord is appended to versions with activated_at_wave = current_wave + grace_period (default 100 waves ≈ 50s at 500ms/wave).
  3. The parachain's current_version is bumped at the activation wave.
  4. ALL parachain peers + relay nodes simultaneously swap the wasmtime Module instance. Old instance is discarded, new active. Module is pre-compiled and cached so the swap is sub-millisecond.
  5. 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:

  1. Proposal submission. Anyone can submit an UpgradeProposalTx containing the new WASM + new config. The proposal enters a Pending state with a public discussion period (default: 7 days).
  2. Voting window. Each parachain validator can submit a VoteTx with {proposal_id, vote: yes|no|abstain, sig: FalconSig}. Voting is open for the configured window (default: 3 days after the discussion period).
  3. 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.
  4. Threshold ceremony. The parachain's validator committee runs a threshold-signing ceremony over the proposal hash. The output is the upgrade_committee_threshold_sig that goes into the version record.
  5. Activation. An UpgradeParachainTx includes 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:

  1. Send. Calling parachain's WASM invokes the host fn. A XCallMessage is recorded in the calling parachain's outgoing-queue state. The current wave's commit records the outgoing message.
  2. 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).
  3. Route. Pyde's main consensus relays the message: every wave commit, the engine scans all outgoing-queue diffs and produces XCallDeliveryTx transactions targeting the destination parachain.
  4. 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).
  5. 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.
  6. 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_fn is 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:

  1. Host Function ABI Specification — a ~10-page document covering names, signatures, memory layout conventions, gas cost table per host function, ABI versioning rules.
  2. otigen parachain CLI:
    • bundle: package .wasm + parachain.toml into a deploy artifact.
    • submit: sign and send the deploy tx.
    • upgrade: replace WASM bytes via governance flow.
    • pause / unpause / kill.
  3. On-chain parachain registry — single source of truth for config + WASM bytes + version history.
  4. Hardcoded bootstrap nodes — peer discovery; no DHT (see Network Protocol).
  5. Slashing preset menu — minimal / standard / strict; authors pick at deploy time.
  6. 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.toml config 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:

PresetEquivocationBad state rootLiveness (offline)
minimal5%5%0.5%/epoch
standard25%10%1%/epoch
strict50%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

FailureDetectionRecovery
Parachain WASM enters infinite loopFuel exhausted → trapTx fails; gas charged; state rolled back
Cross-parachain message verify failsTarget committee rejectsMessage dropped + logged; no callback fires
Cross-parachain message timeouttimeout_waves exceededCallback fires with XCallTimeout error
Parachain committee falls below quorumWave-commit fails for parachain txsParachain enters LimpMode; only no-state txs land until quorum restored
Bad WASM upgrade (deterministic divergence)First N post-activation waves see local-vs-consensus mismatchHard halt + alert; manual emergency rollback via main governance
State subtree corruptionJMT root mismatch on snapshot verificationCross-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 oneFirst 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


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:


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:

PatternUse
ptr: i32, len: i32Caller-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: i32Caller-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:

TypeSize (bytes)
Address32
Slot hash32
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 shapeFunction category
-> i32 (error code only)Mutating ops without return data (sstore, transfer)
-> i32 + writes to out_ptrReturns fixed-size data (sload, caller, balance)
-> i32 + writes to out_ptr + out_len_ptrReturns variable-size data (calldata_copy, parachain_storage_read)
-> i64Returns 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:

AttributeMeaningEnforced by
viewFunction must not modify state, transfer value, or emit eventsEngine sets view_mode flag on HostState; sstore/sdelete/transfer/emit_event return ERR_FORBIDDEN while flag is set
payableFunction accepts attached PYDE value (tx.value > 0). Non-payable functions reject value transfersEngine checks attribute before call; returns ERR_VALUE_TRANSFER_NOT_PAYABLE if value > 0 and attribute absent
reentrantFunction opts in to being called while already on the call stack. Default is non-reentrantEngine tracks (contract_addr, fn_name) active set; rejects re-entry of non-reentrant fn with ERR_REENTRANCY_BLOCKED
sponsoredGas costs charged to the contract's gas tank instead of the callerEngine routes gas accounting to contract's tank balance before invocation
constructorCallable only at contract deploy time. Subsequent calls are rejectedDeploy 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)
fallbackInvoked 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_NAMEEngine dispatches to fallback after selector-table miss
receiveInvoked 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_PAYABLEEngine dispatches to receive on bare-value tx
entryDeclares 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 exposedDeploy 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.

CombinationStatusReason
view + payable❌ RejectedView = no state changes; payable = receives value (state change)
view + constructor❌ RejectedConstructors initialise state; view can't
view + reentrant❌ RejectedViews 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❌ RejectedViews are FREE (§7.8); sponsoring zero gas is meaningless
view + fallback❌ RejectedFallback 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❌ RejectedReceive accepts value; view can't accept value
payable + constructor✅ AllowedConstructors can initialise with funds
payable + reentrant⚠️ Warning, allowedDAO-attack pattern. Build emits warning; deploy accepts
payable + fallback✅ AllowedGeneric handler that also accepts value
constructor + reentrant❌ RejectedConstructors are deploy-only; can't be re-entered
constructor + sponsored❌ RejectedNo gas tank exists at deploy time
constructor + fallback❌ RejectedDistinct call shapes; constructor is deploy-time, fallback is run-time
constructor + receive❌ RejectedSame; distinct dispatch contexts
sponsored + reentrant⚠️ Warning, allowedDAO-attack pattern (contract pays gas for its own re-entry)
fallback + receive❌ RejectedDistinct triggers (selector-miss vs bare-value); can't be the same handler
receive + payable✅ RequiredReceive without payable is a no-op contradiction
receive + reentrant❌ RejectedRecursive 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):

  1. Schema check — version compatibility (pyde_abi_version ≤ engine's max supported), well-formed Borsh decoding, every required field present.
  2. Cross-reference check — every FunctionAbi.name matches a WASM (export "name" (func ...)); every WASM-exported function (other than internal helpers — TBD how to mark) appears in functions[*]. No drift between declarations and code.
  3. Attribute compatibility check — every function's attributes bitfield is a legal combination per §3.5.1. At most one FALLBACK, at most one RECEIVE, RECEIVE implies PAYABLE, etc.
  4. Static call-graph check (view enforcement) — for each function with the VIEW attribute, build the call graph from its body. Walk every transitively-reachable function. If any reachable function imports pyde::sstore, pyde::sdelete, pyde::transfer, pyde::emit_event, pyde::parachain_storage_write, pyde::parachain_storage_delete, or pyde::parachain_emit_event, REJECT the deploy with DeployRejected: ViewMutatesState(<fn_name>, <mutating_import>). Indirect calls (call_indirect) are conservatively treated as potentially-anything; a view that uses call_indirect is rejected unless every possible target is also statically provable to be view-safe.
  5. Static access-list check (best-effort) — for each function with a declared access list, scan all statically-resolvable pyde::sload/sstore call 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 = true before invoking a VIEW function. host_sstore, host_sdelete, host_transfer, host_emit_event, and the parachain mutating variants all check the flag and return ERR_FORBIDDEN if 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_list in host_state.access_list before invoking. host_sload/host_sstore check membership; reject with ERR_ACCESS_LIST_VIOLATION on miss.
  • The engine maintains the active call stack and rejects re-entry into non-reentrant functions.

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.

CodeSymbolMeaning
-1ERR_INVALID_INPUTMalformed input bytes (e.g., non-32-byte hash, non-canonical encoding)
-2ERR_NOT_FOUNDReserved. 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.
-3ERR_INSUFFICIENT_BALANCECaller balance too low for the requested operation
-4ERR_OUT_OF_GASGas budget exhausted (typically a trap, but returned here for consume_gas)
-5ERR_FORBIDDENOperation not permitted in this context (e.g., sstore from a view function)
-6ERR_ACCESS_LIST_VIOLATIONAccessed slot not in declared access list
-7ERR_OUTPUT_BUFFER_TOO_SMALLCaller's output buffer was smaller than required
-8ERR_INVALID_ADDRESSAddress format invalid (e.g., 32-byte all-zero, reserved sentinel)
-9ERR_REENTRANCY_BLOCKEDCross-call would re-enter a non-reentrant function
-10ERR_CROSS_CALL_FAILEDSub-call trapped or returned non-zero error code
-11ERR_CROSS_CALL_OUT_OF_GASSub-call exhausted forwarded gas
-12ERR_VALUE_TRANSFER_NOT_PAYABLEAttempted transfer to a function not marked payable
-13ERR_INVALID_FUNCTION_NAMEcross_call target function does not exist
-14ERR_XCALL_RATE_LIMITEDParachain cross-message budget exceeded for this wave (parachain only)
-15ERR_PARACHAIN_ONLYFunction callable only from parachain context
-16ERR_CIPHERTEXT_INVALIDThreshold-decryption input malformed
-17ERR_SIGNATURE_INVALIDFALCON signature verification failed
-100ERR_INTERNALEngine-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_get for 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>).

ModuleFunctionReason
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
pydefunctions not in this specFuture-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.

FunctionBase gasPer-byte / per-wordNotes
sload200Same gas hot/cold
sstore5,000Same gas new/overwrite
sdelete150No refund
balance100
transfer7,000
caller, origin, self_address5
block_height, wave_id, block_timestamp, chain_id2
tx_hash5
tx_value5
tx_gas_remaining2
calldata_size2
calldata_copy81 / byte
emit_event100+ 50 / topic + 8 / data byte1 to 4 topics; topic[0] conventionally signature hash
hash_blake3153 / word (8 bytes)
hash_poseidon210030 / wordZK-friendly, expensive
hash_keccak256306 / wordEVM-compat
falcon_verify50,000~80μs commodity
cross_call1,0008 / byte calldata + sub-call gas
cross_call_static50Sub-call execution is FREE; caller pays only the dispatch base. Sub-call bounded by VIEW_FUEL_CAP (default 10M instructions ≈ 3ms)
delegate_call1,2008 / byte calldata + sub-call gasCaller's storage context
return0Halt op
revert0Halt op
consume_gas2+ amountPure manual metering
beacon_get50
parachain_storage_read2501 / byte returnedParachain only
parachain_storage_write5,50010 / byteParachain only
parachain_storage_delete250Parachain only
parachain_id5Parachain only
parachain_version5Parachain only
parachain_emit_event100+ 50 / topic + 8 / data byteParachain only; same multi-topic surface as core emit_event
send_xparachain_message10,0008 / byteParachain only
threshold_encrypt80,000100 / byteParachain only
threshold_decrypt100,00050 / byteParachain 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 typeCostPath
Transfer (account-to-account)~21,000 gasNative handler; no wasmtime instantiation
ValidatorRegisterNative
ValidatorUnbondNative
Stake / UnstakeNative
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):

  1. Wasmtime traps into host_cross_call with all arguments.
  2. Charge A's gas: 1,000 base + 8 × calldata_len + (gas_limit reserved). If A's remaining budget is insufficient, trap A with OutOfFuel.
  3. Validate target B: state-lookup B_addr; must have a non-empty code_hash. If not, return ERR_CROSS_CALL_FAILED.
  4. Validate function name: lookup "fn_name" in B's deployed ABI metadata (cached at deploy time). If not found, return ERR_INVALID_FUNCTION_NAME.
  5. 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], return ERR_REENTRANCY_BLOCKED.
  6. Payable check: if value > 0 and "fn_name" is not #[payable], return ERR_VALUE_TRANSFER_NOT_PAYABLE.
  7. Push a new overlay onto the per-tx overlay stack. Call it overlay_B. Reads from B's sload walk: overlay_B → overlay_A → dashmap → state_cf. Writes from B's sstore go to overlay_B only.
  8. Create a new wasmtime Store + Instance for B with: fresh linear memory (B cannot see A's memory directly); fuel = gas_limit; the same Linker (so B has the same host functions available); HostState pointing to overlay_B and the active call stack with B pushed on.
  9. 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).
  10. Apply value transfer: if value > 0, atomically debit A's balance and credit B's by value. This happens before B's code runs so B's first tx_value() call sees the right amount.
  11. Invoke B's entry function with calldata. B's WASM executes in isolation — its sload/sstore operate on overlay_B; its own cross_call would push another overlay on top.
  12. On B's exit, handle the outcome:
    • Success (B returned normally): merge overlay_B into overlay_A; copy return data from B's memory into A's memory at return_data_out_ptr; write actual length at return_data_out_len_ptr; consume B's actual fuel from A's remaining budget; return 0 to A.
    • Trap (B hit OutOfFuel, MemoryOutOfBounds, reverted, etc.): discard overlay_B entirely; revert the value transfer from step 10; consume B's actual fuel from A's remaining; return ERR_CROSS_CALL_FAILED to A.
    • OutOfFuel specifically: same as trap, but return ERR_CROSS_CALL_OUT_OF_GAS to distinguish.

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_limit from 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_limit is 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 size gas_limit carefully.

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, returns ERR_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 always Blake3(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 typeEncoding rule
address ([u8; 32])Stored as-is (already 32 bytes)
uint64, int64Left-padded to 32 bytes (zeros in MSB)
uint128, int128Left-padded to 32 bytes
boolLeft-padded to 32 bytes (0x00...00 or 0x00...01)
[u8; N] where N ≤ 32Left-padded to 32 bytes
stringBlake3(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 typeSignature token
[u8; 32] (address)address
u64uint64
u128uint128
i64int64
boolbool
String (UTF-8)string
Vec<u8>bytes
Vec<T>T[]
[T; N]T[N]
enum X { ... }enum
Custom structtuple (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 .proto toolchain 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:

  1. Fetch the contract's .wasm via pyde_getContractCode(addr)
  2. Parse the pyde.abi custom section to find the event matching topics[0]
  3. The ABI declares which fields are indexed (→ pair them with topics[1..]) and which are not (→ Borsh-decode them from data)
  4. 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:

  1. The wave's HardFinalityCert containing the signed events_root.
  2. The EventRecord itself.
  3. 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 waveFalse-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:

  1. Validate the request: to_wave - from_wave ≤ 5,000; per-position list size ≤ 8; limit ≤ 1,000.
  2. Wave-level bloom prefilter: for each wave in [from_wave, to_wave], load the wave's commit record and test the events_bloom against every concrete value in the filter (any positional topic OR the contract). Drop waves with no bloom hit.
  3. 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_cf for that value, then post-filter results against the remaining positional constraints + contract.
    • If no topic but contract is set: scan events_by_contract_cf prefix contract || wave_id, then post-filter against topic positions.
    • If multiple values at one position: scan each, merge sorted union.
  4. Stream results in canonical order until limit is reached, building next_cursor to point to the next event past the limit.
  5. 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; if from is 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 LogEventNotification records over the WebSocket.
  • On disconnect: drop subscription from registry. Subscriber must pyde_subscribe again on reconnect (with from cursor 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 from cursor 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 tierEvents retention
ArchiveForever
Full nodeLast 90 days
Committee validatorLast 30 days
Light clientNo 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's HardFinalityCert plus a Merkle proof to events_root, verify the event is committed to a finalised wave.
  • Probabilistically check existence: given just the wave's HardFinalityCert, check events_bloom for 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)

  1. PIP describing the new function: signature, semantics, gas cost, error codes, use case.
  2. PIP review + acceptance per Chapter 15 — Governance.
  3. Engine implements the function under a pyde_abi_v1_<N+1> feature gate.
  4. New function is callable only by modules declaring pyde_abi_version >= 1.(N+1).
  5. 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


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 Z command, its flags, semantics, and exit codes
  • The otigen.toml schema — every key, type, default, and validation rule
  • The per-language build pipeline — exactly how otigen invokes Rust / AssemblyScript / Go / C compilers
  • The pyde.abi custom-section injection — how otigen integrates ABI metadata into the WASM output
  • The wallet integration — keystore format, FALCON signing pipeline, key rotation
  • The deploy / upgrade / lifecycle flow — what transactions otigen submits 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 rulesotigen binary version vs chain ABI version compatibility

This spec does not define:


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. otigen does 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 writes extern declarations 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). otigen is invoked from the command line or from a project's npm run / cargo run script.
  • 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:

FlagEffect
-v, --verboseVerbose logging (also -vv for debug)
-q, --quietSuppress non-error output
--jsonOutput 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, --helpShow 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>]
ArgRequiredDescription
<name>yesProject name. Used for the contract/parachain identity and the directory.
--langyesTarget language: rust, as (AssemblyScript), go (TinyGo), or c (clang/wasm32).
--typenocontract (default) or parachain. Selects the appropriate scaffold.
--dirnoTarget directory (default: ./<name>).

Side effects:

  1. Creates <dir>/.
  2. Writes <dir>/otigen.toml from the language template (see §4 for schema).
  3. Writes <dir>/src/ containing a hello-world contract with extern "C" declarations for one host function and one exported function.
  4. Writes language-specific config (e.g., Cargo.toml for Rust, package.json for AS, go.mod for Go).
  5. Writes .gitignore excluding target/, 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>]
FlagDefaultDescription
--release(default)Validate against release-build expectations
--debugoffAllow debug-build artifacts (useful for local dev)
--out./artifacts/Output directory for the deploy bundle

Pipeline:

  1. Read otigen.toml. Validate schema (§4). Validate attribute combinations per HOST_FN_ABI_SPEC §3.5.1.
  2. Locate the compiled .wasm at the path declared in [contract.lang.output].
  3. Validate the WASM:
    • Well-formed binary (passes wasmparser round-trip).
    • Every WASM import declares module pyde (no env, no wasi:*).
    • 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.).
  4. Static call-graph view check. For each view function, build the transitive call graph from its body. If any reachable function imports pyde::sstore / sdelete / transfer / emit_event / parachain_storage_write / parachain_storage_delete / parachain_emit_event, reject with BuildRejected: ViewMutatesState(<fn_name>, <mutating_import>).
  5. Build ContractAbi struct from otigen.toml:
    • For each [functions.X]: extract attributes, compute selector = Blake3(fn_name)[..4], copy access list.
    • For each [events.X]: extract field list, compute topic_signature_hash = Blake3(canonical_signature), mark indexed fields.
    • Compute state_schema_hash = Blake3(canonical_state_schema_bytes).
  6. Borsh-encode the ContractAbi.
  7. Inject the encoded ABI as a WASM custom section named pyde.abi, using the wasm-encoder Rust crate. The code section is untouched.
  8. Write the bundle to <out>/<contract_name>.bundle/:
    • contract.wasm (with the pyde.abi custom 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]
FlagDefaultDescription
--networkfrom otigen.tomlTarget network
--fromfrom otigen.tomlDeploying address or named key
--bundle./artifacts/<name>.bundle/Path to the deploy bundle
--dry-runoffValidate + simulate only; do not submit

Pipeline:

  1. Load bundle. Re-validate WASM + ABI consistency (defense in depth).
  2. 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,
    }
    
  3. Compute canonical tx hash. FALCON-sign with the sender's key (prompts for keystore password unless cached).
  4. Submit via pyde_sendRawTransaction. Print the tx hash.
  5. (Optional) Wait for inclusion: poll pyde_getTransactionReceipt until 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. Submits PauseContractTx. Reversible.
  • unpause: owner-only. Submits UnpauseContractTx.
  • kill: owner-only, irreversible. Requires --yes to confirm. Submits KillContractTx.

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-wave is 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 tx
  • events <addr> [--topic <hash>] [--from <wave>] — query event history
  • balance <addr> — query balance
  • state <addr> <field> — query a state field (type-safe via ABI)
  • subscribe <addr> --logs --topic <hash> — open a live event subscription
  • help, 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

KeyTypeRequiredDefaultValidation
namestring1-32 chars, lowercase alphanumeric + -; matches ENS-style naming (see Chapter 11)
versionstringsemver
descriptionstringempty≤ 200 chars
typeenum"contract""contract" or "parachain"

4.3 [contract.lang] keys

KeyTypeRequiredDefaultNotes
languageenum"rust", "as", "go", "c"
outputpathPath (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

KeyTypeRequiredDefaultValidation
attributesarray of stringsAny subset of view, payable, reentrant, sponsored, constructor, fallback, receive, entry. Subject to compatibility rules per HOST_FN_ABI_SPEC §3.5.1
inputsarray of strings[]Parameter types in declaration order
outputsarray of strings[]Return types in declaration order
access_listarray 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

KeyTypeRequiredDefaultNotes
signaturestringCanonical signature string (Solidity-style), e.g. "Transfer(address,address,uint128)". Must match the field types in declaration order.
fieldsarray of tablesField metadata (name, type, indexed flag). See HOST_FN_ABI_SPEC §14.1.

Each field entry:

KeyTypeRequiredDefault
namestring
typestring
indexedboolfalse

Rules (validated at otigen build):

  • Up to 3 fields can be indexed (so total topics, including topic[0] = signature hash, ≤ 4 — matches EVM LOG4).
  • The signature string must, when parsed, yield exactly the field types in order. otigen build cross-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

KeyTypeDefaultNotes
gas_limitu6410_000_000Default gas budget for deploy/upgrade txs
gas_pricestring or u128"auto""auto" reads chain's current base_fee; explicit value is in quanta per gas
owner_depositu1280PYDE locked at deploy (parachain only; refunded on kill)

4.8 [wallet] table

KeyTypeDefault
default_keystorepath~/.pyde/keystore.json
default_accountstring

4.9 [network.X] tables

Multiple networks can be declared. [network.default] names which is used when --network is not specified.

KeyTypeRequiredNotes
rpc_urlURLJSON-RPC endpoint
chain_idu64Per the HOST_FN_ABI_SPEC chain_id table
explorer_urlURLFor 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):

LanguageCommandOutput
Rustcargo build --target wasm32-unknown-unknown --releasetarget/wasm32-unknown-unknown/release/<name>.wasm
AssemblyScriptnpx asc src/main.ts -o build/contract.wasm --target releasebuild/contract.wasm
Go (TinyGo)tinygo build -target=wasm-unknown -o build/contract.wasmbuild/contract.wasm
C / C++clang --target=wasm32 -nostdlib -Wl,--no-entry -o build/contract.wasm src/*.cbuild/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 call host_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's otigen.toml (shipped alongside contract.wasm per §9) carries the [events.X] declarations for tooling that wants the full picture.

  • Function inputs / outputs are 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's otigen.toml (or its richer abi.json mirror, per §9.3) which retains the [functions.X] inputs / outputs lists.

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:

  1. Generate a fresh FALCON-512 keypair via pyde-crypto.
  2. Prompt the user for a password.
  3. Derive key = Argon2id(password, random_16_byte_salt, kdf_params).
  4. Encrypt the secret key: ciphertext = AES-256-GCM-Encrypt(secret_key, key, random_12_byte_nonce).
  5. Compute the address: addr = Poseidon2(falcon_public_key_bytes) (full 32 bytes, no truncation). Matches Chapter 11 §11.2 and the address-naming-collision locked-in derivation — every EOA on Pyde is Poseidon2(falcon_public_key_bytes). The input is the raw 897-byte FALCON-512 public key; the output is the full 32-byte Poseidon2 hash.
  6. Append the entry to the keystore.

7.3 Signing pipeline

For every tx-submitting subcommand (deploy, upgrade, etc.):

  1. Build the canonical tx bytes per the chain's tx format (Chapter 11).
  2. Compute tx_hash = Blake3(canonical_tx_bytes).
  3. Load the keystore entry. Prompt for password (or use cached if --cache-password was passed).
  4. Decrypt the secret key (§7.1).
  5. signature = FALCON-512-Sign(tx_hash, secret_key).
  6. Attach the signature + pubkey to the tx.
  7. 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:

  1. FALCON-verify the signature.
  2. Validate nonce, balance for deploy_fee + gas_limit × gas_price.
  3. Parse the pyde.abi custom section from wasm_bytes and validate (per HOST_FN_ABI_SPEC §3.7).
  4. Register the contract name. Compute the contract address (see Chapter 11).
  5. Store wasm_bytes in state at the contract's code slot.
  6. If a constructor is declared, instantiate the WASM and invoke the constructor with init_calldata.
  7. Emit a ContractDeployed event.

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
  • otigen version

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:

CodeMeaning
0Success
1Validation / logic failure (bad config, ABI inconsistency, etc.)
2Resource failure (file not found, network unreachable, etc.)
3Transaction failure (revert, gas exhausted, sub-call failed)
4Wallet failure (bad password, missing keystore entry, etc.)
5Authorization failure (signing party not authorized)
64Unhandled 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:

otigenchain ABICompatible?
1.0.x1.0
1.1.x1.0✅ (otigen down-targets to 1.0 if pyde_abi_version = "1.0.0" in otigen.toml)
1.0.x1.1✅ (chain supports older modules)
2.0.x1.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


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).

StreamCodenameRepositoryPrimary specWhat it builds
αToolchainpyde-net/otigen (new)OTIGEN_BINARY_SPEC.mdThe otigen developer-tool binary: build, deploy, wallet, console
βExecutionpyde-net/engine (new), branch execution-sideHOST_FN_ABI_SPEC.md, Chapter 4, PIPs 2/3/4The WASM execution layer: state, account, tx, mempool, wasm-exec
γConsensuspyde-net/engine, branch consensus-sideChapter 6, SLASHING.md, VALIDATOR_LIFECYCLE.md, STATE_SYNC.md, CHAIN_HALT.md, NETWORK_PROTOCOL.mdConsensus + 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:

  1. Fresh pyde-net/engine repo created on GitHub + cloned locally.
  2. Cargo workspace skeleton with stubs for every crate listed in §5.
  3. types crate 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.
  4. interfaces crate 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 atomically
    • trait Executor — invoke a tx (called by consensus when committing a wave)
    • trait MempoolView — what consensus reads from the mempool
    • trait NetworkView — gossipsub send/recv abstraction
    • trait ConsensusEngine — the consensus loop the node binary drives
    • Each trait ships with a mock implementation so β and γ can write tests in isolation.
  5. CI baseline.github/workflows/ci.yml runs cargo build, cargo test, cargo clippy --workspace -- -D warnings, cargo fmt --all -- --check on every PR.
  6. Branching protocol documented (§6).
  7. 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 binary
  • otigen-toml — config parser + schema validation
  • otigen-abipyde.abi custom-section construction + injection (via wasm-encoder)
  • otigen-rpc — JSON-RPC client
  • otigen-wallet — keystore (Argon2id + AES-256-GCM) + FALCON-512 signing
  • (later) otigen-console — REPL

External dependencies:

  • pyde-crypto (sibling polyrepo) — FALCON, Argon2id, AES-GCM, Borsh
  • wasmparser, wasm-encoder (Bytecode Alliance) — WASM inspection + custom-section writing
  • clap — CLI framework
  • serde, toml — config parsing
  • reqwest, tokio-tungstenite — HTTP + WebSocket

Stream β — Engine Execution (pyde-net/engine, branch execution-side)

Crates owned:

  • account — 32-byte addresses, AuthKeys enum (with Programmable v2 reservation), 16-slot nonce window, name-registry interface
  • state — 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 generation
  • tx — transaction types (Transfer, ContractCall, ContractDeploy, ValidatorRegister, Multisig, etc.), canonical hashing, gas accounting, deploy/upgrade/lifecycle handlers
  • wasm-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 overlay
  • mempool — 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 fetch
  • net — libp2p + QUIC + Gossipsub, peer discovery (layered, no DHT), sentry-node pattern, vertex-fetch protocol
  • dkg — Pedersen DKG protocol (or import from pyde-crypto if it lands there first)
  • slashing — validator state machine, the 10-offense catalog, slashing escrow, jail mechanics, reward distribution
  • node — the binary, JSON-RPC server, validator role, consensus_store with set_sync(true), persistence

Spec map:

  • Chapter 6 — Mysticeti DAG consensus
  • SLASHING.md — full 10-offense catalog
  • VALIDATOR_LIFECYCLE.md — registration, bonding, unbonding, jail
  • STATE_SYNC.md — snapshot mechanics, chain-of-trust
  • CHAIN_HALT.md — halt detection, recovery paths
  • NETWORK_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:

  1. Author writes a contract (with α's otigen), builds locally, deploys via otigen deploy.
  2. Tx submitted to RPC, validated by mempool (β), batched, gossipped (γ), included in vertex (γ).
  3. Anchor commits, subdag walks (γ), wasmtime executes (β).
  4. State updates (β), state_root signed (γ), HardFinalityCert formed (γ).
  5. 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 drillsCHAIN_HALT.md playbooks 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)

CrateOwnerBranchDepends on
typesMC-0 (frozen)main(none — leaf crate)
interfacesMC-0 (frozen)maintypes
accountβexecution-sidetypes, pyde-crypto
stateβexecution-sidetypes, interfaces
txβexecution-sidetypes, account, state, pyde-crypto
wasm-execβexecution-sidetypes, interfaces, state, account, tx
mempoolβexecution-sidetypes, interfaces, account, tx
consensusγconsensus-sidetypes, interfaces, pyde-crypto
netγconsensus-sidetypes, interfaces
dkgγconsensus-sidetypes, pyde-crypto
slashingγconsensus-sidetypes, interfaces
nodeγconsensus-side(all of the above)

pyde-net/otigen (separate repo, α owns entirely)

CrateOwnerDepends 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

FileOwnerNotes
Cargo.toml (workspace)MC-0 initially; stream adds its own dep entriesAvoid editing other streams' sections
README.mdγStream γ owns the binary so it owns documentation
.github/workflows/ci.ymlMC-0 initially; both streams may extend their respective test stages
LICENSE, SECURITY.md, .gitignoreMC-0 initiallyEdits 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 main weekly 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-0
  • phase-1-α-milestone-N — α stream milestones
  • phase-1-β-milestone-N — β stream milestones
  • phase-1-γ-milestone-N — γ stream milestones
  • phase-2-integration-bar — local devnet running end-to-end
  • phase-3-state-sync-live, phase-3-parachain-activation
  • phase-4-perf-harness-baseline, phase-4-chaos-drills-passed
  • phase-5-audit-N-passed, phase-5-mainnet-launch

6.3 Coordination rules

  • No edits to types or interfaces crates 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 main that bisect crate ownership get reverted; original committer rebases.

6.4 Communication

  • GitHub issues on pyde-net/engine and pyde-net/otigen for design questions, blocking dependencies, interface clarifications.
  • Spec ambiguity? Update the relevant spec in pyde-net/pyde-book via 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

RiskSeverityMitigation
Interface drift during MC-1 — β or γ realizes a needed change to interfaces mid-implementationHighBoth 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 needsHighAll 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 substantiallyMediumWeekly merges to main make lag visible early. If γ lags, β still ships; integration happens when both are ready. No artificial gating.
Spec ambiguity blocks implementationMediumOpen 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 surfacedMediumGitHub issue tags both stream agents; weekly merge reviews catch silent blockers.
Integration (MC-2) bigger than expectedMediumγ owns the node crate from day one — eliminates a "who integrates" question. β provides clean trait implementations + tests that γ wires in.
Stream α blocked waiting on devnetLowα 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:

  • types crate is FROZEN at end of MC-0. No additions without coordinated PR.
  • interfaces crate 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_attribution memory.
  • 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


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)

AllocationAmount%Vesting
Validator rewards pool200,000,00020%Released proportionally over 4 years via inflation
Treasury (multisig-controlled)150,000,00015%Released via governance proposals
Ecosystem grants100,000,00010%4-year cliff for grantees
Public sale200,000,00020%Released at genesis to public buyers
Founders & early contributors150,000,00015%4-year vesting, 1-year cliff
Investors200,000,00020%4-year vesting, 1-year cliff

Numbers above are illustrative starting points; final distribution requires legal review and stakeholder negotiation.

Inflation Schedule

YearInflation rateNew PYDE minted
15%50M
23%~30M (compounding)
32%~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.

StatusEarnings Source
Validator on active committeeBase stake × uptime share of reward pool + activity-weighted committee bonus (vertices certified, batches included, anchor selections) + inflation share
Validator awaiting selectionBase 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):

OffenseFirst instanceMax
Equivocation10%50% (correlation/repeat)
Bad state-root10%50%
Downtime0.05%/round10%/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:

  1. Gas burn (70%): every transaction reduces supply, creating deflationary pressure when network usage is high
  2. Validator bond locking: 10K PYDE per validator slot, locked during operation
  3. Treasury spending: continually deploys PYDE into the ecosystem
  4. 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

  1. Initial distribution percentages: above are illustrative; final allocations need legal + stakeholder negotiation.
  2. Investor terms: lockup, vesting, and post-vesting governance rights are open design questions.
  3. Treasury governance specifics: which categories of spending require which multisig thresholds — to be detailed in governance PIP.
  4. Parachain reward split: 70/20/10 above is starting point; may adjust based on operator economics post-mainnet.

References


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).

FormUse
PydeDefault, sentence-case. Use this almost everywhere — headings, prose, marketing copy.
pydeLowercase in URLs, handles, file names, code identifiers (pyde-net, pyde.network). The X handle is @pydenet (the available short form).
PYDEUppercase 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 Pyde in every language.
  • Spell it out as an acronym (Programmable Yield Decentralized Engine and 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.

The Pyde mark

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 about 0.85× the core's widest radius.

Guidance, not pixel rules. To recreate, eyeball against assets/logo.png.


3. Lockups

LockupUse
Mark aloneFavicons, app icons, profile pictures, social avatars, watermarks, very small footprints. Default for any context under 32×32 px.
Mark + wordmark, horizontalWebsite 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, verticalPosters, 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.

TokenHexRole
--pyde-ink#0d1117Primary dark — backgrounds, dark-theme surfaces, default body text in light mode.
--pyde-shadow#2a2f36Dark elevated — elevated surfaces on dark backgrounds, code-block fills.
--pyde-mist#7a8590Mid-gray — muted labels, captions, dividers.
--pyde-veil#e1e4eaLight elevated — soft surfaces on light backgrounds, subtle dividers.
--pyde-paper#f7f8faPrimary 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.

ContextFont 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.4px for structural elements, 0.6px for 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.

QualityMeans
DirectShort sentences. Active voice. Avoid "we are excited to announce." Just announce.
HonestNumbers 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.
UnpretentiousNo "L1 of L1s," no "ushering in a new era." If a competitor would write it, don't.
CuriousWhen 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/).

FilePurpose
logo.pngCanonical full-colour grayscale mark, 500×500 px. Default for digital use.
factory-loop.svgAnimated 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.png and assets/factory-loop.svg at 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 SecureMEV-FreeSub-second FinalCommodity 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:

  1. 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
  2. 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
  3. 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)
  4. Commodity hardware decentralization

    • Full nodes / RPC: 8c/16GB/100Mbps (laptop-class)
    • Equal-power voting within the committee
    • Operator identity binding (no Sybil amplification)

Differentiation

EthereumSolanaSuiPyde
Post-QuantumMigration path 5+ years outNo planNo planDefault at genesis
MEVAuction (PBS)Extracted by proposersSome via MysticetiStructurally impossible
Finality12-15s400ms390ms~500ms
Commodity validatorPossibleNo (12+ cores)No (datacenter)Yes (any validator awaiting committee selection)
Smart contract languageSolidityRust/AnchorMoveAny wasm32-target language (Rust/AS/Go/C) with Pyde safety attributes preserved
Account abstractionRetrofit (ERC-4337)None nativeLimitedNative (v2)
Cross-chainBridges (hacked $3B+)BridgesBridgesPermissionless parachain layer (v2)
ZK readinessRetrofit ongoingLimitedLimitedArchitecture 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):

Modev1 realisticv2 stretchAspirational
Plaintext TPS (commodity)10–30K50–100K500K
Encrypted TPS (commodity)0.5–2K5–10K50K
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:

  1. Cryptography collaborators — particularly post-quantum threshold encryption (the hardest piece)
  2. Consensus reviewers — Mysticeti DAG specialists for safety/liveness analysis
  3. Audit budget — $500K–$1M projected for v1 mainnet audit
  4. Grant funding — Ethereum Foundation, NIST, Polkadot for threshold PQ research
  5. Early ecosystem builders — wallets, block explorers, dApp developers willing to build on a pre-mainnet testnet

Contact


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

TermDefinition
PydeThe post-quantum L1 blockchain. Name of the protocol, the network, and the binary.
PYDEThe native token. 1 PYDE = 10^9 quanta.
otigenPyde's developer toolchain (the binary). Scaffolds projects, builds WASM artifacts, deploys, manages wallets. Name carried forward from the retired Otigen language.
WASMWebAssembly. Pyde's execution layer; smart contracts and parachains compile to WASM and execute under wasmtime.
wasmtimeThe WebAssembly runtime used by Pyde. Bytecode Alliance project, production-vetted at Microsoft / Fastly / Shopify.
Host Function ABIThe stable interface contracts use to interact with chain state (sload, sstore, transfer, threshold crypto, hashing, cross_call, etc.). See the Host Function ABI spec.
CraneliftThe 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.
JMTJellyfish Merkle Tree. The state commitment structure (radix-16, path-compressed).
Blake3Fast bitwise hash. Used for JMT internals, batch hashes, vertex hashes, gossip de-dup.
Poseidon2Algebraic hash over the Goldilocks field. State root commit, addresses, MAC, VRF, ZK-bearing paths.
FALCON-512NIST FIPS 206 post-quantum signature scheme. ~666-byte sigs, 897-byte pks.
Kyber-768NIST FIPS 203 post-quantum KEM. P2P session keys and threshold mempool.
Threshold encryptionMempool encryption such that any 85 of 128 committee members combine to decrypt.
PSSProactive Secret Sharing — refresh key shares without changing the public key.
DKGDistributed Key Generation. Pedersen DKG ceremony each epoch for threshold pubkey.
VRFVerifiable Random Function. Lattice-based; built from FALCON + Poseidon2.
MysticetiThe DAG-based consensus protocol Pyde uses (post-May-2026 pivot, formerly HotStuff).
DAGDirected Acyclic Graph. Every round, each committee member produces a vertex; parents must be strictly prior rounds.
VertexA committee member's per-round output: batch refs + parent refs + state-root sigs + decryption shares + FALCON sig.
RoundA ~150 ms DAG cycle. Each member produces one vertex per round.
WaveThe Mysticeti commit unit. Anchor at round R+3 commits the subdag rooted at round R.
AnchorDeterministically-selected committee member whose round-R vertex commits the wave. Hash(beacon, round, recent_state_root) mod 128.
Worker / PrimaryNarwhal pattern: workers gossip tx batches, primary produces vertices and runs consensus.
HardFinalityCert≥ 85 FALCON sigs over (wave_id, blake3_state_root, poseidon2_state_root).
CommitteeThe 128 active validators per epoch. Equal vote weight; uniform random selection.
Epoch~3 hours of waves. PSS resharing fires at epoch boundary.
ValidatorNode staking ≥ MIN_VALIDATOR_STAKE (10,000 PYDE). Single tier — uniform-random committee selection picks 128 from the eligible pool each epoch.
Full nodeNode that executes waves and serves RPC, but does not stake.
MEVMaximal Extractable Value. The MEV class is structurally closed in Pyde.
Encrypted mempoolOptional Kyber-encrypted submission. Decryption deferred until after DAG anchor commit.
Commit-before-revealDAG anchor commits canonical ordering before threshold-decryption shares are released.
Hybrid schedulerExecution model: static access lists (Solana-style) + Block-STM speculation (Aptos-style).
Sentry nodePublic-facing proxy in front of a committee validator. Hides validator's real IP.
TreasuryThe system account at Poseidon2("pyde-treasury"). Spent via on-chain multisig.
PIPPyde Improvement Proposal. Off-chain documents that drive code changes.
Multisig signersThe on-chain set authorized to spend the treasury (MULTISIG_SIGNERS).
Emergency pauseMultisig-authorized halt of non-Resume txs; max 30 days, auto-expiring.
Hard haltAutomatic chain halt on detected safety violation (state root divergence, equivocation cluster).
Weak-subjectivity checkpointHard-finalized commit (wave_id + state_root + committee FALCON sigs) that a fresh node trusts to anchor sync.
QuantaSmallest PYDE denomination. 1 PYDE = 10^9 quanta.
Access listPer-tx declaration of state slots the tx will read or write.
Nonce window16-slot bitmap of in-flight nonces per account.
Gas tankPer-account dedicated balance for sponsoring user transactions.
PaymasterA contract that pays gas on behalf of a user, with custom validation logic.
Parachain operatorPermissionless v2 actor who stakes PYDE, fulfills cross_call! to other chains, earns gas fees.

B. Network Constants

ConstantValueWhere
ROUND_PERIOD_MS150 (DAG round cadence)consensus/round.rs
COMMIT_TARGET_MS500 (median commit)consensus/commit.rs
EPOCH_LENGTH~3 hours of wavesconsensus/epoch.rs
COMMITTEE_SIZE (mainnet)128consensus/committee.rs
THRESHOLD (2f+1)85consensus/quorum.rs
EQUIVOCATION_THRESHOLD (n-2f)44consensus/quorum.rs
RANDOMNESS_THRESHOLD85 (sorted before combine)consensus/epoch_randomness.rs
RESHARE_AGGREGATION_DELAY_WAVES5crypto/threshold.rs / validator
MIN_VALIDATOR_STAKE10,000 PYDEtx/pipeline.rs (single tier)
MAX_VALIDATORS_PER_OPERATOR3tx/pipeline.rs (anti-Sybil cap)
UNBONDING_PERIOD30 daysconsensus/validator.rs
FINDER_FEE_PERCENT10slashing/lib.rs
EVIDENCE_VERSION1slashing/lib.rs
MULTISIG_VERSION0x01tx/multisig.rs
MAX_MULTISIG_SIGNERS16tx/multisig.rs
MAX_PAUSE_DURATION_WAVES~30 days of wavestx/pipeline.rs
MAX_BATCH_SIZE4 MBmempool/batch.rs

C. Gas / Fee Constants

ConstantValueWhere
GAS_TARGET400,000,000tx/fee.rs
GAS_CEILING1,600,000,000 (4× target)tx/fee.rs
GENESIS_BASE_FEE50,000,000,000 quantatx/fee.rs
MIN_BASE_FEE1tx/fee.rs
ADJUSTMENT_DIVISOR8 (1/8 = 12.5% per block)tx/fee.rs
FEE_BURN_PCT70tx/execution.rs
FEE_REWARD_POOL_PCT20tx/execution.rs
FEE_TREASURY_PCT10tx/execution.rs
MIN_GAS_LIMIT21,000tx/validation.rs
MAX_TX_SIZE128 KBtx/validation.rs
MAX_CALLDATA64 KBtx/validation.rs
WAVES_PER_YEAR63,113,904 (2/sec)tx/fee.rs
INFLATION_BPS[500, 300, 200, 100]tx/fee.rs
GENESIS_SUPPLY10^18 quanta (1B PYDE)tx/fee.rs

D. Mempool Constants

ConstantValueWhere
DEFAULT_MAX_TX_PER_WINDOW_PER_SENDER10mempool/pool.rs
DEFAULT_MAX_CONCURRENT_PER_SENDER100mempool/pool.rs
RATE_WINDOW_MS1000mempool/pool.rs
WINDOW_SIZE (nonce bitmap)16account/nonce.rs
MAX_RECEIPT_SLOTS10,000node/receipt_store.rs

E. WASM Execution Constants

ConstantValueMeaning
Initial linear memory1 MBDefault WASM linear memory per instantiation
Max linear memory64 MBCapped by the engine to bound resource use
Stack depth limitConfigurablewasmtime-enforced; rejects modules exceeding cap
PAGE_ALLOC_GAS200 fuel/64KBFuel per WASM memory.grow page
Default fuel per gas unit(calibrated)Established at node startup from the gas table
MODULE_CACHE_MAX_BYTES1 GB (default)LRU + size-cap + TTL on compiled Module + parsed ABI; per-node tunable. See HOST_FN_ABI_SPEC §3.6
MODULE_CACHE_TTL_WAVES8 epochs (~1 day)Cache entries unused longer than this are evicted
VIEW_FUEL_CAP10,000,000Per-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

ConstantDefaultWhere
DEFAULT_PORT30303net/config.rs
DEFAULT_MAX_PEERS50net/config.rs
DEFAULT_MAX_INBOUND30net/config.rs
DEFAULT_MAX_OUTBOUND20net/config.rs
DEFAULT_RATE_LIMIT_PER_IP5 / secnet/config.rs
DEFAULT_IDLE_TIMEOUT60 snet/config.rs
Gossipsub mesh_n8net/node.rs
Gossipsub heartbeat150 ms (DAG round)net/node.rs
MAINNET_SEEDS(set at launch)net/discovery.rs
TESTNET_SEEDS(set at launch)net/discovery.rs
MAINNET_DNS_SEEDseed.pyde.networknet/discovery.rs

G. State Discriminators

Used in Poseidon2(addr || discriminator || sub_key) for storage keys. Defined in crates/state/src/keys.rs.

DiscriminatorNameHolds
0x12SUPPLYTotal PYDE supply counter
0x13TOTAL_BURNEDCumulative fee burn counter
0x14REWARDS_PER_STAKE_UNITLazy-accrual per-stake-unit reward accumulator
0x15ACTIVE_STAKE_WEIGHTED_TOTALPool divisor (sum of stake × uptime; excludes exited / slashed)
0x16VESTINGPer-account vesting schedule (40 bytes)
0x17VALIDATOR_SUBSIDY(total_amount, end_wave) streaming subsidy
0x18AIRDROP_ROOTGenesis airdrop Merkle root
0x19AIRDROP_DEADLINEwave_id after which sweep is allowed
0x1AAIRDROP_CLAIMEDPer-leaf-index claim bitmap
0x1BAIRDROP_EXPECTED_SUMGenesis pool size invariant
0x1CMULTISIG_SIGNERSTreasury multisig signer set (FALCON pks)
0x1DMULTISIG_THRESHOLDRequired signature count
0x1EMULTISIG_NONCEReplay-protection counter for multisig
0x1FEMERGENCY_PAUSE_END_WAVEEnd 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.

IDNamePurpose
0StandardValue transfer or contract call
1DeployContract deployment
3StakeDepositLock ≥ 10,000 PYDE and register validator (single tier, uniform-random committee selection per epoch)
4StakeWithdrawBegin 30-day unbonding
5SlashSubmit double-sign evidence
6ClaimRewardClaim accrued staking yield from the pool
7ClaimAirdropClaim genesis airdrop with Merkle proof
8SweepAirdropMove unclaimed airdrop residue to treasury (post-deadline)
9MultisigTxTreasury spend with multisig signatures
10RotateMultisigRotate multisig signer set + threshold
11EmergencyPauseHalt block production (multisig-signed)
12EmergencyResumeResume normal processing
13RegisterPubkeyFirst-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_:

MethodReturns
pyde_getBalancebalance (quanta string)
pyde_getTransactionCountnonce (u64)
pyde_getCodehex bytecode
pyde_getStorageAthex value
pyde_chainIdhex chain_id
pyde_blockNumberhex head wave_id
pyde_gasPricebase fee (quanta)
pyde_stateRootcurrent state root
pyde_syncingsync status object
pyde_getValidatorsvalidators with status + stake
pyde_getBlockByNumberBlockHeader
pyde_getBlockByHashBlockHeader
pyde_getTransactionReceiptreceipt with logs + fee breakdown
pyde_getLogsmatching logs
pyde_mempoolSizepending tx count
pyde_sendRawTransactiontx hash
pyde_sendTransaction(dev only) tx hash
pyde_sendEncryptedTransactiontx hash
pyde_callview-function return data (FREE off-chain)
pyde_estimateGasgas estimate
pyde_createAccessListinferred access list
pyde_getHardFinalityCertcommittee-signed cert for a wave (incl. state_root + events_root + events_bloom)
pyde_getSnapshotManifestsnapshot manifest for state sync
pyde_resolveNamename → 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

PurposePrimitiveSizes
Digital signaturesFALCON-512 (NIST FIPS 206)pk 897 B, sk 1281 B, sig ~666 B
Key encapsulationKyber-768 / ML-KEM (FIPS 203)pk 1184 B, sk seed 64 B, ct 1088 B
High-volume hashingBlake3256-bit output, ~3 GB/s native
ZK-bearing hashingPoseidon2 over Goldilocks256-bit output, ~400 constraints/hash
Threshold encryptionShamir SSS + Kyber + Poseidon285-of-128, ~250 B per share
PSS resharingLagrange interpolation over Goldilockspreserves underlying secret
DKGPedersen DKG over Kyber-768per-epoch threshold pubkey
VRFFALCON-proof + Poseidon2 outputinherits FALCON security
Symmetric AEADAES-256-GCM (hardware-accelerated)32-byte key, 16-byte tag
AddressPoseidon2(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:

ItemPriorityNotes
Persistent receipt store (archive-node mode)HighTask 058. Needed for production explorers.
ML-KEM upgrade from 0.3.0-rc to stableHighTask 057. Once NIST stable releases.
Algebraic batch FALCON verificationHighPer-block verification cost reduction.
Signed-mempool commitments + censorship slashingHighReplaces local-view mandatory inclusion.
Pedersen / KZG commitments for PSS resharingHighCloses the malicious-contributor edge case.
Graceful drain-and-shutdown on persist failureMediumTask 014e. Operational polish.
Two-dimensional gas (exec + prove)MediumDepends on ZK proving landing.
Off-chain Merkle builder CLI for airdrop opsMediumOperator tooling, ~150 LOC.
Mempool-level filter during emergency pauseLowCleaner than gate-check at admission.
Sentry-node validator hidingLowOperational pattern, not protocol.
Sophisticated peer scoringMediumMulti-topic + decay parameters.
Fancy version-signaling on-chainLowCurrently out-of-band.
ZK validity proofs (STARK proving)ResearchMajor redesign; restores prover economics.
Native Ethereum bridgeHighFALCON-in-EVM verifier + Patricia verifier as a Pyde WASM contract.
Native Bitcoin bridgeMediumSPV-style proofs; PoW finality is probabilistic.
Parachain SDK (Rust / Go / C++)MediumSovereign chains sharing Pyde security.
TypeScript SDKMediumWASM bridge available now; dedicated TS later.
Native browser walletLowEcosystem; WASM exposes primitives.
Block-explorer frontendHighBackend 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).

SubsystemKey files
Crypto stackcrates/crypto/src/{falcon,kyber,poseidon2,threshold,vrf}.rs
State commitmentcrates/state/src/jmt_store.rs, witness.rs, keys.rs
Account recordcrates/account/src/{types,address,nonce}.rs
Slashing constantscrates/slashing/src/lib.rs
TX types + pipelinecrates/tx/src/{types,validation,pipeline,fee,execution}.rs
Multisig / governancecrates/tx/src/multisig.rs, crates/tx/src/vesting.rs
Airdropcrates/tx/src/airdrop.rs
Consensuscrates/consensus/src/{dag,vertex,wave,anchor,subdag,validator,finality,slashing,epoch_randomness,committee,quorum,round}.rs
Networkingcrates/net/src/{node,channels,auth,peer,ddos,discovery,config}.rs
Mempoolcrates/mempool/src/{pool,block_builder,inclusion,encrypted}.rs
Node binary + RPCcrates/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 toolchainpyde-net/otigen (separate repo): subcommand framework, otigen.toml schema, language detection, state binding generators (Rust/AS/Go/C), deploy flow, wallet
Rust SDKcrates/pyde-rust-sdk/src/{lib,client,wallet,contract,signer,abi,types,ws}.rs
WASM cryptocrates/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:

ClaimSource
~150 ms DAG round periodROUND_PERIOD_MS in consensus/round.rs
~500 ms median commitCOMMIT_TARGET_MS in consensus/commit.rs
v1 plaintext TPS: 10-30KPerformance harness measurement, "claim 1/3 of measured peak" rule (companion/PERFORMANCE_HARNESS.md)
v1 encrypted TPS: 0.5-2KSame harness; threshold-decryption serial cost
70 / 20 / 10 fee splitFEE_BURN_PCT etc in tx/execution.rs
5% → 1% inflation scheduleINFLATION_BPS in tx/fee.rs
10,000 PYDE validator min stakeMIN_VALIDATOR_STAKE in tx/pipeline.rs (single tier)
3 max validators per operatorMAX_VALIDATORS_PER_OPERATOR in tx/pipeline.rs (anti-Sybil)
30-day unbondingUNBONDING_PERIOD in consensus/validator.rs
16-slot nonce windowWINDOW_SIZE in account/nonce.rs
128 KB tx / 64 KB calldata capsMAX_TX_SIZE, MAX_CALLDATA in tx/validation.rs
4 MB batch hard capMAX_BATCH_SIZE in mempool/batch.rs
1 MB witness capMAX_WITNESS_SIZE in state/witness.rs
WASM host function ABI v1.0wasm-exec/src/host_fns.rs (post-pivot) + companion/HOST_FN_ABI_SPEC.md
wasmtime + Cranelift AOTPinned wasmtime version in Cargo.toml
Module cache sizeMODULE_CACHE_SIZE in wasm-exec/src/module_cache.rs (post-pivot)
Committee 128, threshold 85COMMITTEE_SIZE, THRESHOLD in consensus/quorum.rs
85-of-128 threshold for decryptionRANDOMNESS_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 v1cross_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

ComponentPre-pivotPost-pivot
ConsensusHotStuff variant, 1 proposer/slotMysticeti DAG, 128 vertices/round
Slot timing400 ms slot~150 ms round, ~500 ms median commit
OrderingProposer-asserted ordering commitmentStructural via committed subdag
Validator architectureMonolithicWorker (tx batching) + Primary (consensus)
MempoolAlways-encryptedOptional encryption per-tx
State treeFixed-depth Sparse Merkle TreeJellyfish Merkle Tree (radix-16, path-compressed)
HashingPoseidon2 everywhereBlake3 (native) + Poseidon2 (ZK-bearing)
State rootSingle Poseidon2 rootDual: Blake3 + Poseidon2
ExecutionStatic access lists onlyHybrid: static + Block-STM speculation
Staking modelSingle 10K PYDESingle 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 distributionDirect proposer share (20%)Epoch reward pool (20%, distributed by stake×uptime)
Peer discoveryKademlia DHTLayered (seeds → DNS → on-chain registry → PEX → cache)
Committee defenseOperational sentry pattern onlySentry pattern with protocol support
Cross-chainStub cross_call!cross_call! + parachain operator network (v2)
Account abstractionSingle + MultisigSingle + 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:

  1. Chapter 6 (Consensus) — full rewrite; HotStuff → Mysticeti DAG.
  2. Chapter 7 (State Sync & Chain Halt) — new chapter, operational procedures absent in pre-pivot.
  3. Chapter 9 (MEV Protection) — restructured for DAG ordering.
  4. Chapter 4 (State Model) — hybrid hashing, dual state roots.
  5. Chapter 8 (Cryptography) — Blake3 added; Poseidon2 scope narrowed.
  6. Chapter 12 (Networking) — DHT removed; layered discovery + sentry.
  7. Chapter 14 (Tokenomics) — single-tier staking (10K PYDE min, uniform-random committee selection, operator-identity cap), reward pool, updated inflation math.
  8. Chapter 19 (Launch Strategy) — timeline reset post-pivot.
  9. 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 Programmable AuthKeys 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:

ComponentStatus
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.