From 1500f8bf3a6bb8c7887e718cc22944a48fa76982 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:30:20 +0200 Subject: [PATCH 001/274] add Zephyr Protocol v2 clean-break foundation --- docs/protocol-v2.md | 379 ++++++++++++++++++++ internal/v2/README.md | 28 ++ internal/v2/assets/token.go | 136 +++++++ internal/v2/citizen/verifier.go | 91 +++++ internal/v2/citizen/verifier_test.go | 14 + internal/v2/codec/codec.go | 170 +++++++++ internal/v2/codec/codec_test.go | 51 +++ internal/v2/compute/model.go | 160 +++++++++ internal/v2/compute/model_test.go | 25 ++ internal/v2/contracts/contracts.go | 90 +++++ internal/v2/contracts/contracts_test.go | 19 + internal/v2/da/da.go | 73 ++++ internal/v2/da/da_test.go | 17 + internal/v2/execution/engine.go | 218 ++++++++++++ internal/v2/execution/engine_test.go | 142 ++++++++ internal/v2/genesis/genesis.go | 149 ++++++++ internal/v2/genesis/genesis_test.go | 39 ++ internal/v2/merkle/merkle.go | 111 ++++++ internal/v2/merkle/merkle_test.go | 29 ++ internal/v2/object/object.go | 199 +++++++++++ internal/v2/sharding/sharding.go | 173 +++++++++ internal/v2/sharding/sharding_test.go | 27 ++ internal/v2/state/smt.go | 312 ++++++++++++++++ internal/v2/state/smt_test.go | 82 +++++ internal/v2/transport/transport.go | 42 +++ internal/v2/tx/transaction.go | 451 ++++++++++++++++++++++++ internal/v2/tx/transaction_test.go | 78 ++++ internal/v2/types/types.go | 76 ++++ internal/v2/worldstate/store.go | 105 ++++++ internal/v2/worldstate/store_test.go | 34 ++ 30 files changed, 3520 insertions(+) create mode 100644 docs/protocol-v2.md create mode 100644 internal/v2/README.md create mode 100644 internal/v2/assets/token.go create mode 100644 internal/v2/citizen/verifier.go create mode 100644 internal/v2/citizen/verifier_test.go create mode 100644 internal/v2/codec/codec.go create mode 100644 internal/v2/codec/codec_test.go create mode 100644 internal/v2/compute/model.go create mode 100644 internal/v2/compute/model_test.go create mode 100644 internal/v2/contracts/contracts.go create mode 100644 internal/v2/contracts/contracts_test.go create mode 100644 internal/v2/da/da.go create mode 100644 internal/v2/da/da_test.go create mode 100644 internal/v2/execution/engine.go create mode 100644 internal/v2/execution/engine_test.go create mode 100644 internal/v2/genesis/genesis.go create mode 100644 internal/v2/genesis/genesis_test.go create mode 100644 internal/v2/merkle/merkle.go create mode 100644 internal/v2/merkle/merkle_test.go create mode 100644 internal/v2/object/object.go create mode 100644 internal/v2/sharding/sharding.go create mode 100644 internal/v2/sharding/sharding_test.go create mode 100644 internal/v2/state/smt.go create mode 100644 internal/v2/state/smt_test.go create mode 100644 internal/v2/transport/transport.go create mode 100644 internal/v2/tx/transaction.go create mode 100644 internal/v2/tx/transaction_test.go create mode 100644 internal/v2/types/types.go create mode 100644 internal/v2/worldstate/store.go create mode 100644 internal/v2/worldstate/store_test.go diff --git a/docs/protocol-v2.md b/docs/protocol-v2.md new file mode 100644 index 00000000..619b306a --- /dev/null +++ b/docs/protocol-v2.md @@ -0,0 +1,379 @@ +# Zephyr Protocol v2 — Clean-Break Architecture + +Status: **authoritative design target and implementation contract for the Zephyr v2 clean break**. + +Protocol v1 remains the prototype/conformance reference. Protocol v2 intentionally does not preserve v1 transaction wire format, persisted-state layout, node-role coupling or account-state representation. We keep the security, consensus, recovery and benchmarking lessons while replacing boundaries that would otherwise become permanent scaling debt. + +## Mission + +Zephyr v2 has two equal scaling goals: + +1. **Scale up when hardware is abundant.** The long-term target remains extremely high throughput, measured only as transactions finalized by validator consensus. +2. **Scale down without dying.** Payments, self-custody, verification and useful network participation must remain practical on commodity hardware, including a Citizen Node inside the Zephyr smartphone wallet. + +Large datacenters may add capacity, but must not be structurally required for Zephyr to remain alive. The network may degrade in **throughput** when resources fall; it must never degrade in **safety**. + +## Non-negotiable invariants + +- Headline TPS means finalized transactions per second, never API ingress or mempool acceptance. +- No performance change is accepted if the Consensus & Performance Lab finds a safety or liveness regression. +- A Citizen Node must verify its own balances/payments, finalized headers, shard commitments, state proofs and sampled data without holding the full global state/history. +- Wallet-side work only helps network throughput when it produces evidence another participant can independently verify. +- Heavy compute (AI training, scientific workloads, rendering, etc.) is never replayed by every validator. +- Validators do not require GPUs or datacenter-class machines. +- Account, node and validator identities are separate. +- Consensus-critical encoding is canonical binary; JSON remains for RPC, diagnostics and human-facing configuration. +- Consensus-critical economics use integers/fixed point, not floating point. +- Sharding is protocol-native but activation is benchmark-driven. v2 starts safely with `shardCount = 1` unless evidence supports more. +- Adding shards must not linearly increase the minimum hardware requirement of a Citizen Node. +- Smart-contract execution is deterministic and metered. +- Heavy/private compute is provider-executed and blockchain-settled through commitments/proofs/attestations/replication/challenges as appropriate. + +## Architecture + +```text + ZEPHYR v2 + | + +----------+----------+ + | GLOBAL FINALITY | + +----------+----------+ + | + shard/object commitments + | + +--------------------+--------------------+ + | | | + Shard 0 Shard 1 Shard N + | | | + parallel exec parallel exec parallel exec + | | | + +--------------------+--------------------+ + | + proofs / receipts + | + +-----------------------+------------------------+ + | | | + Validators Full Nodes Citizen Nodes + | | smartphones + consensus full state | + finality proof serving headers/proofs + shard work history tx relay / DA + bounded cache + optional exec + + +----------------------+ + | Native Compute Market| + +----------------------+ + off-chain CPU / GPU + on-chain settlement +``` + +Zephyr remains one network with one global notion of finality. Shards are execution/state partitions, not independent chains. + +## 1. Genesis-derived network identity + +A canonical v2 genesis contains protocol version, chain name, genesis time, initial/max shard count, native token, initial validator identities/voting power and initial allocations. + +```text +networkId = H("zephyr/genesis/v2" || canonicalGenesis) +``` + +Genesis becomes the network identity and initial validator-set trust anchor. Nodes with different genesis data cannot silently claim the same network. + +Reference: `internal/v2/genesis`. + +## 2. Separate identities and roles + +```text +Account identity -> ownership and transaction authorization +Node identity -> peer networking/authentication +Validator identity -> consensus proposal/vote authority +``` + +A machine may expose one or more roles: Citizen Node, Full Node, Validator, Archive Node, Compute Provider. A full node does not need a validator key; a compute provider is not automatically a validator; a smartphone does not need permanent consensus availability. + +References: `internal/v2/types`, `internal/v2/transport`. + +## 3. Canonical binary protocol + +Consensus objects use deterministic length-prefixed binary encoding with explicit versions, hard limits, big-endian integers and domain-separated hashing/signing. Consensus objects do not depend on JSON map/order behavior. + +The first v2 proof-carrying transaction has a complete bounded binary marshal/parse round-trip. + +Reference: `internal/v2/codec`. + +## 4. Object state and native assets + +The v2 execution primitive is a protocol object: + +```text +Object +├── objectId +├── version +├── owner +├── kind +└── data +``` + +Initial kinds cover coins, token definitions, contracts, contract state, compute offers/jobs/assignments/results and system objects. Explicit object dependencies allow independent transactions to be scheduled in parallel. + +For native payments the wallet still shows a normal balance; coin objects are an internal execution model. A transfer consumes coin objects and creates new ones while enforcing per-token conservation. + +Token creation is protocol-native rather than requiring every token to reimplement a basic ERC-style ledger in bytecode. Token definitions include name, symbol, decimals, supply policy, mint authority, burnability and transferability. Custom logic can still be controlled by contracts later. + +References: `internal/v2/object`, `internal/v2/assets`, `internal/v2/execution`. + +## 5. Proof-native incremental state + +The v1 performance profile identified full-state persistence/serialization as the first measured bottleneck. v2 therefore makes incremental, proof-oriented state a protocol requirement. + +The reference engine is a 256-bit Sparse Merkle Tree over: + +```text +objectId -> objectHash +``` + +It already provides deterministic roots, incremental path updates, inclusion proofs, absence proofs and compressed proofs that omit default siblings. The in-memory world-state backend defines the semantics; it is not the final durable database. Production storage will put the same state model over structured durable KV/WAL/checkpoint recovery. + +Changing two objects must not require serializing the entire chain state. + +References: `internal/v2/state`, `internal/v2/worldstate`. + +## 6. Proof-Carrying Transactions + +A v2 wallet does more than sign. It may package the exact state evidence needed to validate the objects it consumes. + +```text +Wallet + ├─ verifies finalized state root + ├─ obtains object proofs + ├─ declares inputs/outputs/operation + ├─ signs canonical intent + └─ attaches witnesses + | + v + Proof-Carrying Transaction + | + v +Validator verifies signature + proofs + freshness/conflicts + | + v + deterministic execution +``` + +The validator never trusts the wallet's claim; it independently verifies the cryptographic witness. This makes wallet work reusable while preserving consensus as the authority for uniqueness, ordering and finality. + +The foundation includes P-256 low-S signing, genesis-derived network binding, state-root binding, explicit input object/version/hash, bounded witnesses, random salt and expiration height. + +Reference: `internal/v2/tx`. + +## 7. Parallel execution + +Object inputs form an explicit conflict set. Transactions that touch disjoint objects can execute concurrently and then merge deterministically. + +The first executor is deliberately simple and single-operation so we can establish correctness before a worker scheduler. The next execution milestone builds the deterministic conflict graph and benchmarks 1/4/8/16 workers on one shard before using sharding as a multiplier. + +## 8. Shard-native, one-shard-first + +The protocol contains shard IDs, shard commitments, a global commitment root, global headers and cross-shard receipt primitives from the beginning, but the first v2 chain may run one shard. + +A global finalized header commits to shard roots and validator/data commitments. A Citizen Node can follow global headers while fetching only the shard/state proofs relevant to it. + +Cross-shard movement follows: + +```text +source shard consumes input + | + v +finalized receipt commitment + | + v +destination shard verifies receipt + | + v +creates destination output +``` + +Additional shards are activated only when 1/4/16-shard benchmarks show better finalized throughput/resource efficiency without worsening the Citizen Node minimum footprint. Receipt anti-replay and recovery must pass before multi-shard activation. + +Reference: `internal/v2/sharding`. + +## 9. Citizen Node inside Zephyr Wallet + +A Citizen Node is not a passive RPC client. Depending on device conditions it can: + +- verify finalized headers and quorum/finality evidence; +- verify object/state proofs for balances and payments; +- verify proof-carrying transactions; +- relay transactions through multiple peers; +- verify shard commitments; +- sample data availability; +- keep a bounded recent cache; +- optionally execute recent state while resources allow. + +Participation is power-aware. Low battery can reduce the role to header verification; Wi-Fi/charging can enable sampling, cache serving and recent execution. Mobile availability is not assumed for consensus liveness. + +Reference: `internal/v2/citizen`. + +## 10. Data availability + +Citizen Nodes should be able to contribute to availability without downloading all shard data. The foundation commits ordered chunks and verifies Merkle samples. It also defines an encoder boundary for a later erasure-code implementation. + +Production DA still requires selection of an erasure code, reconstruction logic, sampling rules/confidence model, adversarial withholding tests and real mobile bandwidth/storage measurements. The current package is the proof contract, not a claim that production DA is finished. + +Reference: `internal/v2/da`. + +## 11. Deterministic smart contracts + +Zephyr keeps smart contracts through a deterministic WebAssembly ABI. Rust is the first-class SDK target, not the only possible source language. Compatible toolchains may later include Zig, C/C++, TinyGo and others. + +Consensus must standardize allowed WASM features, deterministic host calls, fuel/gas, memory/stack limits, state-access declarations, deterministic output/events and forbidden nondeterministic facilities. + +The foundation validates a WASM v1 deployment envelope and defines the runtime interface. A production interpreter/metering engine is **not yet claimed complete**. + +Reference: `internal/v2/contracts`. + +## 12. Native distributed compute market + +Heavy workloads remain outside consensus execution. The blockchain owns marketplace and settlement state; providers execute the work. + +Native objects will cover provider offers, resource capabilities, price/collateral, jobs, assignments, escrow, result commitments, verification mode, challenges/disputes, settlement and reputation/slashing. + +Target workloads include scientific/numerical computing, AI training/inference, video/3D rendering, compilation and data processing. + +Verification is workload-specific. Supported modes are: + +- deterministic re-execution; +- replicated execution; +- challenge-based verification; +- zero-knowledge/validity proof; +- TEE/remote attestation; +- client approval; +- hybrid combinations. + +Confidential workloads keep private datasets/results off-chain where appropriate; Zephyr stores commitments, encrypted references, attestations/proofs and settlement state. + +The foundation defines resource/offer/job/result data models and verification modes. Provider daemon, scheduling, escrow transitions, disputes and production TEE/ZK integrations are later milestones. + +Reference: `internal/v2/compute`. + +## 13. Transport boundaries + +Consensus, transaction relay and light-proof retrieval are distinct logical capabilities. HTTP remains usable as a reference/test transport; future libp2p/QUIC/WebTransport implementations sit behind the same contracts. The Consensus & Performance Lab fault transport must be adapted to this boundary so correctness tests run independently of the production transport. + +Reference: `internal/v2/transport`. + +## 14. Performance has two axes + +### Scale up — how fast can Zephyr finalize? + +Measure finalized tx/s, p50/p95/p99 finality, validators, shards, batch size, CPU, allocations, memory, state-write bytes, network bytes/finalized tx, witness bytes, DA bytes and persisted state. + +### Scale down — how small can a useful node be? + +Measure Citizen Node resident memory, cache size, sync bandwidth, proof size/verification time, header verification, DA sample bytes/time, mobile CPU/battery duty cycle and startup/resume latency. + +No phone or hardware budget becomes a production claim before measurement on real reference devices. + +## 15. Security and consensus continuity + +The existing Consensus & Performance Lab remains the gate. v2 must preserve or strengthen network/domain separation, low-S canonical signatures, quorum finality, pre-vote state-root verification, validator identity/signature validation, quorum-only recovery evidence, no single-peer snapshot trust, partition safety and recovery after quorum returns. + +The new architecture changes **what consensus commits to**, not how much evidence is required for finality. + +## 16. Clean-break compatibility policy + +v2 intentionally breaks v1 compatibility for network identity, transaction wire/signing domain, object/state format, persistent backend, state-root calculation, node-role model, shard commitments and contract ABI. + +It carries forward security invariants, consensus/fault lessons, performance methodology, recovery requirements, wallet self-custody and P-256 usability unless later benchmark/security evidence justifies changing it. + +No public v2 network will silently accept v1 protocol objects. + +## 17. Implementation sequence + +### Foundation — implemented by this branch + +- canonical binary codec and typed v2 identities; +- genesis-derived network ID; +- Sparse Merkle reference state and compressed proofs; +- object/coin model and native token definitions; +- signed proof-carrying transaction wire format; +- native transfer and token-creation reference executor; +- object world-state backend; +- shard routing, commitments/proofs, global header and receipt primitives; +- DA chunk/sample verification boundary; +- Citizen Node verifier and resource-aware participation policy; +- deterministic WASM deployment/runtime boundary; +- native compute-market data model and verification modes; +- separate consensus/transaction/light transport interfaces; +- unit tests and reference state/proof microbenchmarks. + +### Integration + +- add v2 genesis to the Consensus & Performance Lab; +- make current certified consensus finalize a v2 global header; +- run one-shard v2 transfers end to end; +- introduce durable structured KV persistence with crash/restart recovery; +- compare v1/v2 finalized TPS, finality, allocations and state-write cost. + +### Mobile + +- compact header/state-proof APIs; +- Citizen verifier inside Zephyr Wallet; +- bounded/resumable cache and multi-peer relay; +- real Android/iOS resource measurements; +- eliminate correctness dependence on any single RPC endpoint. + +### Parallel execution + +- deterministic conflict graph; +- parallel non-conflicting execution; +- deterministic merge; +- 1/4/8/16-worker benchmark on one shard. + +### Sharding + +- 4-shard Lab; +- cross-shard receipt consume and anti-replay; +- shard-aware gossip/recovery; +- 1/4/16-shard benchmarks; +- activate more shards only if evidence is positive. + +### WASM + +- select production deterministic runtime; +- validate imports/opcodes; +- define fuel schedule and memory limits; +- deploy/call state transitions; +- Rust SDK and deterministic conformance suite. + +### Compute market + +- offer/job/assignment/result state transitions; +- escrow/settlement and provider daemon; +- deterministic/replicated verification first; +- collateral/slashing/disputes; +- optional TEE/ZK backends and confidential workload flow. + +### Data availability + +- select erasure code and reconstruction; +- sampling/confidence rules; +- withholding/fault tests; +- mobile bandwidth/storage benchmarks; +- shard-aware DA propagation. + +### Public devnet gate + +No public v2 devnet until safety/liveness conformance, durable state recovery and Citizen Node correctness pass; one-shard performance is characterized; shard activation rules are defined; and genesis/checkpoint/operator/wallet upgrade procedures are explicit. + +## Architectural north star + +When simplicity, throughput and decentralization conflict, prefer designs that preserve independent verification and graceful hardware degradation, then recover throughput through parallelism, sharding and optional high-end providers. + +```text +more hardware -> more throughput +less hardware -> less throughput +less hardware -/-> weaker correctness +``` + +Zephyr v2 is neither a "mobile blockchain" nor a "datacenter blockchain". It is intended to remain independently verifiable on small devices while scaling execution and data capacity when the network has more resources. diff --git a/internal/v2/README.md b/internal/v2/README.md new file mode 100644 index 00000000..8f1e9511 --- /dev/null +++ b/internal/v2/README.md @@ -0,0 +1,28 @@ +# Zephyr Protocol v2 reference foundation + +This directory contains the clean-break Protocol v2 reference types and proof-oriented execution primitives. + +It is intentionally isolated from the current v1 runtime while the Consensus & Performance Lab is used to compare behavior and performance. The goal is to migrate only after the v2 path proves safety, recovery and measurable performance/resource advantages. + +Packages: + +- `codec`: bounded canonical binary encoding primitives. +- `types`: typed network/account/node/validator/object/token/contract/job identifiers. +- `genesis`: canonical genesis and genesis-derived network identity. +- `state`: incremental Sparse Merkle Tree with compressed inclusion/absence proofs. +- `worldstate`: proof-native object state backend contract and reference in-memory backend. +- `object`: object and coin model. +- `tx`: signed proof-carrying v2 transactions and canonical wire format. +- `execution`: native transfer and token-creation reference executor. +- `assets`: protocol-native token definition model. +- `merkle`: ordered Merkle commitment/proof utility. +- `sharding`: shard routing, global/shard commitments and cross-shard receipt primitives. +- `da`: data-availability chunk commitments and sample verification. +- `citizen`: mobile/light verifier and resource-aware participation policy. +- `contracts`: deterministic WASM deployment/runtime boundary. +- `compute`: native distributed-compute market objects and verification modes. +- `transport`: separated consensus, transaction and light-proof transport capabilities. + +The authoritative architecture and implementation sequence is documented in `docs/protocol-v2.md`. + +Do not interpret the presence of an interface or protocol type as a production-complete feature. In particular, production WASM execution, erasure coding, dynamic sharding, durable v2 storage, mobile OS integration and compute-provider execution/settlement are explicit later integration milestones. diff --git a/internal/v2/assets/token.go b/internal/v2/assets/token.go new file mode 100644 index 00000000..314c5593 --- /dev/null +++ b/internal/v2/assets/token.go @@ -0,0 +1,136 @@ +package assets + +import ( + "errors" + "strings" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrInvalidTokenDefinition = errors.New("invalid token definition") + +type Definition struct { + TokenID types.TokenID + Name string + Symbol string + Decimals uint8 + MaxSupply uint64 + CurrentSupply uint64 + MintAuthority types.AccountID + Burnable bool + Transferable bool +} + +type CreateToken struct { + Name string + Symbol string + Decimals uint8 + MaxSupply uint64 + InitialSupply uint64 + MintAuthority types.AccountID + Burnable bool + Transferable bool +} + +func (c CreateToken) Validate() error { + name := strings.TrimSpace(c.Name) + symbol := strings.TrimSpace(c.Symbol) + if name == "" || len(name) > 64 || symbol == "" || len(symbol) > 16 || c.Decimals > 18 { + return ErrInvalidTokenDefinition + } + if c.InitialSupply == 0 { + return ErrInvalidTokenDefinition + } + if c.MaxSupply != 0 && c.InitialSupply > c.MaxSupply { + return ErrInvalidTokenDefinition + } + if types.IsZero32([32]byte(c.MintAuthority)) { + return ErrInvalidTokenDefinition + } + return nil +} + +func (c CreateToken) MarshalBinary() ([]byte, error) { + if err := c.Validate(); err != nil { + return nil, err + } + var w codec.Writer + w.String(strings.TrimSpace(c.Name)) + w.String(strings.TrimSpace(c.Symbol)) + w.U8(c.Decimals) + w.U64(c.MaxSupply) + w.U64(c.InitialSupply) + w.Fixed(c.MintAuthority[:]) + w.Bool(c.Burnable) + w.Bool(c.Transferable) + return w.BytesCopy(), nil +} + +func ParseCreateToken(data []byte) (CreateToken, error) { + r := codec.NewReader(data) + name, err := r.String(64) + if err != nil { + return CreateToken{}, ErrInvalidTokenDefinition + } + symbol, err := r.String(16) + if err != nil { + return CreateToken{}, ErrInvalidTokenDefinition + } + decimals, err := r.U8() + if err != nil { + return CreateToken{}, ErrInvalidTokenDefinition + } + maxSupply, err := r.U64() + if err != nil { + return CreateToken{}, ErrInvalidTokenDefinition + } + initialSupply, err := r.U64() + if err != nil { + return CreateToken{}, ErrInvalidTokenDefinition + } + authBytes, err := r.Fixed(32) + if err != nil { + return CreateToken{}, ErrInvalidTokenDefinition + } + burnable, err := r.Bool() + if err != nil { + return CreateToken{}, ErrInvalidTokenDefinition + } + transferable, err := r.Bool() + if err != nil { + return CreateToken{}, ErrInvalidTokenDefinition + } + if err := r.Done(); err != nil { + return CreateToken{}, ErrInvalidTokenDefinition + } + var authority types.AccountID + copy(authority[:], authBytes) + out := CreateToken{ + Name: name, Symbol: symbol, Decimals: decimals, MaxSupply: maxSupply, + InitialSupply: initialSupply, MintAuthority: authority, Burnable: burnable, Transferable: transferable, + } + if err := out.Validate(); err != nil { + return CreateToken{}, err + } + return out, nil +} + +func (d Definition) MarshalBinary() ([]byte, error) { + if types.IsZero32([32]byte(d.TokenID)) || strings.TrimSpace(d.Name) == "" || strings.TrimSpace(d.Symbol) == "" || + d.Decimals > 18 || d.CurrentSupply == 0 || (d.MaxSupply != 0 && d.CurrentSupply > d.MaxSupply) || + types.IsZero32([32]byte(d.MintAuthority)) { + return nil, ErrInvalidTokenDefinition + } + var w codec.Writer + w.Fixed(d.TokenID[:]) + w.String(strings.TrimSpace(d.Name)) + w.String(strings.TrimSpace(d.Symbol)) + w.U8(d.Decimals) + w.U64(d.MaxSupply) + w.U64(d.CurrentSupply) + w.Fixed(d.MintAuthority[:]) + w.Bool(d.Burnable) + w.Bool(d.Transferable) + return w.BytesCopy(), nil +} diff --git a/internal/v2/citizen/verifier.go b/internal/v2/citizen/verifier.go new file mode 100644 index 00000000..270a1ffa --- /dev/null +++ b/internal/v2/citizen/verifier.go @@ -0,0 +1,91 @@ +package citizen + +import ( + "errors" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/da" + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/state" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrFinality = errors.New("global header finality could not be verified") + +type FinalityVerifier interface { + VerifyFinalizedHeader(header sharding.GlobalHeader) error +} + +type Verifier struct { + Network types.NetworkID + Finality FinalityVerifier +} + +func (v Verifier) VerifyTransaction(t tx.Transaction) error { + if err := t.VerifyForNetwork(v.Network); err != nil { + return err + } + return t.VerifyWitnesses() +} + +func (v Verifier) VerifyObject(root types.Hash, obj object.Object, proof state.Proof) bool { + if err := obj.Validate(); err != nil { + return false + } + h := obj.Hash() + return state.Verify(root, types.Hash(obj.ID), h[:], proof) +} + +func (v Verifier) VerifyShard(header sharding.GlobalHeader, commitment sharding.Commitment, proof merkle.Proof) error { + if header.Network != v.Network { + return ErrFinality + } + if v.Finality == nil || v.Finality.VerifyFinalizedHeader(header) != nil { + return ErrFinality + } + if !merkle.Verify(header.ShardCommitmentRoot, commitment.Hash(), proof) { + return ErrFinality + } + return nil +} + +func (v Verifier) VerifyDASample(commitment da.Commitment, sample da.Sample, chunk []byte) bool { + return da.VerifySample(commitment, sample, chunk) +} + +type PowerState struct { + BatteryPercent uint8 + Charging bool + WiFi bool + LowPower bool + AppActive bool +} + +type Mode struct { + VerifyHeaders bool + Relay bool + SampleDA bool + ExecuteRecent bool + ServeCache bool +} + +func SelectMode(p PowerState) Mode { + mode := Mode{VerifyHeaders: true} + if p.LowPower || p.BatteryPercent < 15 { + return mode + } + if p.AppActive { + mode.Relay = true + } + if p.WiFi && p.BatteryPercent >= 30 { + mode.SampleDA = true + mode.ServeCache = p.AppActive + } + if p.WiFi && p.Charging && p.BatteryPercent >= 50 { + mode.ExecuteRecent = true + mode.ServeCache = true + } + return mode +} diff --git a/internal/v2/citizen/verifier_test.go b/internal/v2/citizen/verifier_test.go new file mode 100644 index 00000000..f91cd902 --- /dev/null +++ b/internal/v2/citizen/verifier_test.go @@ -0,0 +1,14 @@ +package citizen + +import "testing" + +func TestPowerAwareMode(t *testing.T) { + low := SelectMode(PowerState{BatteryPercent: 10, AppActive: true, WiFi: true}) + if !low.VerifyHeaders || low.Relay || low.SampleDA || low.ExecuteRecent { + t.Fatalf("unexpected low-power mode: %+v", low) + } + full := SelectMode(PowerState{BatteryPercent: 80, AppActive: true, WiFi: true, Charging: true}) + if !full.VerifyHeaders || !full.Relay || !full.SampleDA || !full.ExecuteRecent || !full.ServeCache { + t.Fatalf("unexpected full mode: %+v", full) + } +} diff --git a/internal/v2/codec/codec.go b/internal/v2/codec/codec.go new file mode 100644 index 00000000..efae54ad --- /dev/null +++ b/internal/v2/codec/codec.go @@ -0,0 +1,170 @@ +package codec + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "errors" + "fmt" +) + +var ( + ErrUnexpectedEOF = errors.New("unexpected end of canonical payload") + ErrTrailingData = errors.New("trailing data in canonical payload") + ErrLengthLimit = errors.New("canonical field exceeds configured length limit") +) + +type Writer struct { + buf bytes.Buffer +} + +func (w *Writer) U8(v uint8) { _ = w.buf.WriteByte(v) } + +func (w *Writer) U16(v uint16) { + var b [2]byte + binary.BigEndian.PutUint16(b[:], v) + _, _ = w.buf.Write(b[:]) +} + +func (w *Writer) U32(v uint32) { + var b [4]byte + binary.BigEndian.PutUint32(b[:], v) + _, _ = w.buf.Write(b[:]) +} + +func (w *Writer) U64(v uint64) { + var b [8]byte + binary.BigEndian.PutUint64(b[:], v) + _, _ = w.buf.Write(b[:]) +} + +func (w *Writer) Bool(v bool) { + if v { + w.U8(1) + return + } + w.U8(0) +} + +func (w *Writer) Fixed(v []byte) { + _, _ = w.buf.Write(v) +} + +func (w *Writer) Bytes(v []byte) { + w.U32(uint32(len(v))) + w.Fixed(v) +} + +func (w *Writer) String(v string) { w.Bytes([]byte(v)) } + +func (w *Writer) BytesCopy() []byte { + out := make([]byte, w.buf.Len()) + copy(out, w.buf.Bytes()) + return out +} + +type Reader struct { + data []byte + off int +} + +func NewReader(data []byte) *Reader { + return &Reader{data: data} +} + +func (r *Reader) take(n int) ([]byte, error) { + if n < 0 || r.off+n > len(r.data) { + return nil, ErrUnexpectedEOF + } + out := r.data[r.off : r.off+n] + r.off += n + return out, nil +} + +func (r *Reader) U8() (uint8, error) { + b, err := r.take(1) + if err != nil { + return 0, err + } + return b[0], nil +} + +func (r *Reader) U16() (uint16, error) { + b, err := r.take(2) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint16(b), nil +} + +func (r *Reader) U32() (uint32, error) { + b, err := r.take(4) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint32(b), nil +} + +func (r *Reader) U64() (uint64, error) { + b, err := r.take(8) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint64(b), nil +} + +func (r *Reader) Bool() (bool, error) { + v, err := r.U8() + if err != nil { + return false, err + } + switch v { + case 0: + return false, nil + case 1: + return true, nil + default: + return false, fmt.Errorf("invalid canonical bool %d", v) + } +} + +func (r *Reader) Fixed(n int) ([]byte, error) { return r.take(n) } + +func (r *Reader) Bytes(max uint32) ([]byte, error) { + n, err := r.U32() + if err != nil { + return nil, err + } + if n > max { + return nil, ErrLengthLimit + } + b, err := r.take(int(n)) + if err != nil { + return nil, err + } + out := make([]byte, len(b)) + copy(out, b) + return out, nil +} + +func (r *Reader) String(max uint32) (string, error) { + b, err := r.Bytes(max) + if err != nil { + return "", err + } + return string(b), nil +} + +func (r *Reader) Done() error { + if r.off != len(r.data) { + return ErrTrailingData + } + return nil +} + +func DomainHash(domain string, payload []byte) [32]byte { + var w Writer + w.String(domain) + w.Bytes(payload) + return sha256.Sum256(w.BytesCopy()) +} diff --git a/internal/v2/codec/codec_test.go b/internal/v2/codec/codec_test.go new file mode 100644 index 00000000..fd977a36 --- /dev/null +++ b/internal/v2/codec/codec_test.go @@ -0,0 +1,51 @@ +package codec + +import ( + "bytes" + "testing" +) + +func TestCanonicalRoundTrip(t *testing.T) { + var w Writer + w.U8(7) + w.U16(0x1234) + w.U32(42) + w.U64(99) + w.Bool(true) + w.String("zephyr") + w.Bytes([]byte{1, 2, 3}) + + r := NewReader(w.BytesCopy()) + if v, _ := r.U8(); v != 7 { + t.Fatalf("u8=%d", v) + } + if v, _ := r.U16(); v != 0x1234 { + t.Fatalf("u16=%d", v) + } + if v, _ := r.U32(); v != 42 { + t.Fatalf("u32=%d", v) + } + if v, _ := r.U64(); v != 99 { + t.Fatalf("u64=%d", v) + } + if v, _ := r.Bool(); !v { + t.Fatal("bool=false") + } + if v, _ := r.String(64); v != "zephyr" { + t.Fatalf("string=%q", v) + } + if v, _ := r.Bytes(64); !bytes.Equal(v, []byte{1, 2, 3}) { + t.Fatalf("bytes=%v", v) + } + if err := r.Done(); err != nil { + t.Fatal(err) + } +} + +func TestDomainHashSeparatesDomains(t *testing.T) { + a := DomainHash("a", []byte("same")) + b := DomainHash("b", []byte("same")) + if a == b { + t.Fatal("domain-separated hashes collided") + } +} diff --git a/internal/v2/compute/model.go b/internal/v2/compute/model.go new file mode 100644 index 00000000..1a285002 --- /dev/null +++ b/internal/v2/compute/model.go @@ -0,0 +1,160 @@ +package compute + +import ( + "errors" + "strings" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +type VerificationMode uint8 + +const ( + VerificationUnknown VerificationMode = iota + VerificationDeterministic + VerificationReplicated + VerificationChallenge + VerificationZeroKnowledge + VerificationTEE + VerificationClientApproved + VerificationHybrid +) + +var ( + ErrInvalidOffer = errors.New("invalid compute offer") + ErrInvalidJob = errors.New("invalid compute job") + ErrInvalidResult = errors.New("invalid compute result") +) + +type Resources struct { + CPUCores uint16 + MemoryMiB uint32 + GPUCount uint16 + GPUMemoryMiB uint32 + StorageMiB uint64 + BandwidthMbps uint32 + Capabilities []string +} + +type Offer struct { + Provider types.AccountID + Resources Resources + PricePerUnit uint64 + Collateral uint64 + Verification []VerificationMode + ValidUntilHeight uint64 +} + +type Job struct { + Owner types.AccountID + WorkloadHash types.Hash + InputRoot types.Hash + Resources Resources + MaxPrice uint64 + CollateralRequired uint64 + Verification VerificationMode + DeadlineHeight uint64 + Replicas uint16 + Private bool +} + +type Result struct { + JobID types.JobID + Provider types.AccountID + ResultRoot types.Hash + ProofHash types.Hash + AttestationHash types.Hash + CompletedHeight uint64 +} + +func (r Resources) Validate() error { + if r.CPUCores == 0 && r.GPUCount == 0 { + return ErrInvalidOffer + } + if r.MemoryMiB == 0 || len(r.Capabilities) > 32 { + return ErrInvalidOffer + } + for _, c := range r.Capabilities { + if strings.TrimSpace(c) == "" || len(c) > 64 { + return ErrInvalidOffer + } + } + return nil +} + +func (o Offer) Validate() error { + if types.IsZero32([32]byte(o.Provider)) || o.PricePerUnit == 0 || o.ValidUntilHeight == 0 || + len(o.Verification) == 0 || len(o.Verification) > 7 { + return ErrInvalidOffer + } + return o.Resources.Validate() +} + +func (j Job) Validate() error { + if types.IsZero32([32]byte(j.Owner)) || types.IsZero32([32]byte(j.WorkloadHash)) || + types.IsZero32([32]byte(j.InputRoot)) || j.MaxPrice == 0 || j.DeadlineHeight == 0 || + j.Verification <= VerificationUnknown || j.Verification > VerificationHybrid { + return ErrInvalidJob + } + if j.Verification == VerificationReplicated && j.Replicas < 2 { + return ErrInvalidJob + } + return j.Resources.Validate() +} + +func (r Result) Validate() error { + if types.IsZero32([32]byte(r.JobID)) || types.IsZero32([32]byte(r.Provider)) || + types.IsZero32([32]byte(r.ResultRoot)) || r.CompletedHeight == 0 { + return ErrInvalidResult + } + return nil +} + +func (o Offer) MarshalBinary() ([]byte, error) { + if err := o.Validate(); err != nil { + return nil, err + } + var w codec.Writer + w.Fixed(o.Provider[:]) + writeResources(&w, o.Resources) + w.U64(o.PricePerUnit) + w.U64(o.Collateral) + w.U32(uint32(len(o.Verification))) + for _, mode := range o.Verification { + w.U8(uint8(mode)) + } + w.U64(o.ValidUntilHeight) + return w.BytesCopy(), nil +} + +func (j Job) MarshalBinary() ([]byte, error) { + if err := j.Validate(); err != nil { + return nil, err + } + var w codec.Writer + w.Fixed(j.Owner[:]) + w.Fixed(j.WorkloadHash[:]) + w.Fixed(j.InputRoot[:]) + writeResources(&w, j.Resources) + w.U64(j.MaxPrice) + w.U64(j.CollateralRequired) + w.U8(uint8(j.Verification)) + w.U64(j.DeadlineHeight) + w.U16(j.Replicas) + w.Bool(j.Private) + return w.BytesCopy(), nil +} + +func writeResources(w *codec.Writer, r Resources) { + w.U16(r.CPUCores) + w.U32(r.MemoryMiB) + w.U16(r.GPUCount) + w.U32(r.GPUMemoryMiB) + w.U64(r.StorageMiB) + w.U32(r.BandwidthMbps) + w.U32(uint32(len(r.Capabilities))) + for _, c := range r.Capabilities { + w.String(strings.TrimSpace(c)) + } +} diff --git a/internal/v2/compute/model_test.go b/internal/v2/compute/model_test.go new file mode 100644 index 00000000..d8e75f9f --- /dev/null +++ b/internal/v2/compute/model_test.go @@ -0,0 +1,25 @@ +package compute + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestReplicatedJobRequiresMultipleReplicas(t *testing.T) { + job := Job{ + Owner: types.AccountIDFromPublicKey([]byte("owner")), + WorkloadHash: types.HashBytes("workload", []byte("container")), + InputRoot: types.HashBytes("input", []byte("dataset")), + Resources: Resources{CPUCores: 4, MemoryMiB: 8192}, + MaxPrice: 100, Verification: VerificationReplicated, + DeadlineHeight: 100, Replicas: 1, + } + if err := job.Validate(); err == nil { + t.Fatal("replicated job accepted one replica") + } + job.Replicas = 3 + if err := job.Validate(); err != nil { + t.Fatal(err) + } +} diff --git a/internal/v2/contracts/contracts.go b/internal/v2/contracts/contracts.go new file mode 100644 index 00000000..f7124481 --- /dev/null +++ b/internal/v2/contracts/contracts.go @@ -0,0 +1,90 @@ +package contracts + +import ( + "bytes" + "errors" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + RuntimeWASMv1 = "wasm-v1" + MaxModuleBytes = 4 << 20 + MaxInitialStateBytes = 1 << 20 +) + +var ( + ErrInvalidModule = errors.New("invalid deterministic wasm module") + ErrInvalidDeployment = errors.New("invalid contract deployment") + ErrFuelExhausted = errors.New("contract fuel exhausted") + ErrUndeclaredAccess = errors.New("contract attempted undeclared state access") +) + +type Deployment struct { + Runtime string + Code []byte + ABI uint16 + UpgradeAuthority types.AccountID + InitialState []byte + MaxMemoryPages uint32 +} + +func (d Deployment) Validate() error { + if d.Runtime != RuntimeWASMv1 || d.ABI == 0 || len(d.Code) == 0 || len(d.Code) > MaxModuleBytes || + len(d.InitialState) > MaxInitialStateBytes || d.MaxMemoryPages == 0 || + types.IsZero32([32]byte(d.UpgradeAuthority)) { + return ErrInvalidDeployment + } + if !ValidateWASMModule(d.Code) { + return ErrInvalidModule + } + return nil +} + +func (d Deployment) MarshalBinary() ([]byte, error) { + if err := d.Validate(); err != nil { + return nil, err + } + var w codec.Writer + w.String(d.Runtime) + w.Bytes(d.Code) + w.U16(d.ABI) + w.Fixed(d.UpgradeAuthority[:]) + w.Bytes(d.InitialState) + w.U32(d.MaxMemoryPages) + return w.BytesCopy(), nil +} + +func ValidateWASMModule(code []byte) bool { + if len(code) < 8 || len(code) > MaxModuleBytes { + return false + } + return bytes.Equal(code[:4], []byte{0x00, 0x61, 0x73, 0x6d}) && + bytes.Equal(code[4:8], []byte{0x01, 0x00, 0x00, 0x00}) +} + +type Access struct { + ObjectID types.ObjectID + Write bool +} + +type Request struct { + ContractID types.ContractID + Entrypoint string + Arguments []byte + Accesses []Access + FuelLimit uint64 +} + +type Result struct { + ReturnData []byte + FuelUsed uint64 + Writes map[types.ObjectID][]byte + Events [][]byte +} + +type Runtime interface { + ValidateModule(code []byte) error + Execute(request Request) (Result, error) +} diff --git a/internal/v2/contracts/contracts_test.go b/internal/v2/contracts/contracts_test.go new file mode 100644 index 00000000..bbdd12ba --- /dev/null +++ b/internal/v2/contracts/contracts_test.go @@ -0,0 +1,19 @@ +package contracts + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestDeploymentAcceptsWASMVersionOneEnvelope(t *testing.T) { + code := []byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00} + deployment := Deployment{ + Runtime: RuntimeWASMv1, Code: code, ABI: 1, + UpgradeAuthority: types.AccountIDFromPublicKey([]byte("owner")), + MaxMemoryPages: 64, + } + if err := deployment.Validate(); err != nil { + t.Fatal(err) + } +} diff --git a/internal/v2/da/da.go b/internal/v2/da/da.go new file mode 100644 index 00000000..08e7e37d --- /dev/null +++ b/internal/v2/da/da.go @@ -0,0 +1,73 @@ +package da + +import ( + "errors" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrInvalidSample = errors.New("invalid data-availability sample") + +type Commitment struct { + Root types.Hash + ChunkCount uint32 + DataShards uint16 + ParityShards uint16 + OriginalSize uint64 +} + +type Sample struct { + Index uint32 + ChunkHash types.Hash + Proof merkle.Proof +} + +type Encoder interface { + Encode(data []byte, dataShards, parityShards uint16) ([][]byte, error) +} + +func CommitChunks(chunks [][]byte, dataShards, parityShards uint16, originalSize uint64) (Commitment, []Sample, error) { + if len(chunks) == 0 || int(dataShards)+int(parityShards) != len(chunks) || dataShards == 0 { + return Commitment{}, nil, ErrInvalidSample + } + leaves := make([]types.Hash, len(chunks)) + hashes := make([]types.Hash, len(chunks)) + for i, chunk := range chunks { + hashes[i] = types.Hash(codec.DomainHash("zephyr/da/chunk/v2", chunk)) + var w codec.Writer + w.U32(uint32(i)) + w.Fixed(hashes[i][:]) + leaves[i] = merkle.Leaf("da-chunk", w.BytesCopy()) + } + root := merkle.Root(leaves) + samples := make([]Sample, len(chunks)) + for i := range chunks { + proof, err := merkle.BuildProof(leaves, i) + if err != nil { + return Commitment{}, nil, err + } + samples[i] = Sample{Index: uint32(i), ChunkHash: hashes[i], Proof: proof} + } + return Commitment{ + Root: root, ChunkCount: uint32(len(chunks)), DataShards: dataShards, + ParityShards: parityShards, OriginalSize: originalSize, + }, samples, nil +} + +func VerifySample(commitment Commitment, sample Sample, chunk []byte) bool { + if commitment.ChunkCount == 0 || sample.Index >= commitment.ChunkCount || + sample.Proof.Index != sample.Index || sample.Proof.LeafCount != commitment.ChunkCount { + return false + } + chunkHash := types.Hash(codec.DomainHash("zephyr/da/chunk/v2", chunk)) + if chunkHash != sample.ChunkHash { + return false + } + var w codec.Writer + w.U32(sample.Index) + w.Fixed(chunkHash[:]) + leaf := merkle.Leaf("da-chunk", w.BytesCopy()) + return merkle.Verify(commitment.Root, leaf, sample.Proof) +} diff --git a/internal/v2/da/da_test.go b/internal/v2/da/da_test.go new file mode 100644 index 00000000..02a57c51 --- /dev/null +++ b/internal/v2/da/da_test.go @@ -0,0 +1,17 @@ +package da + +import "testing" + +func TestSampleVerification(t *testing.T) { + chunks := [][]byte{[]byte("a"), []byte("b"), []byte("c"), []byte("d")} + commitment, samples, err := CommitChunks(chunks, 2, 2, 4) + if err != nil { + t.Fatal(err) + } + if !VerifySample(commitment, samples[2], chunks[2]) { + t.Fatal("sample failed") + } + if VerifySample(commitment, samples[2], []byte("tampered")) { + t.Fatal("tampered sample verified") + } +} diff --git a/internal/v2/execution/engine.go b/internal/v2/execution/engine.go new file mode 100644 index 00000000..cad6d043 --- /dev/null +++ b/internal/v2/execution/engine.go @@ -0,0 +1,218 @@ +package execution + +import ( + "errors" + "math" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/assets" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ( + ErrUnsupportedOperation = errors.New("unsupported v2 operation") + ErrOwnership = errors.New("transaction does not own an input object") + ErrConservation = errors.New("token conservation failed") + ErrOverflow = errors.New("token amount overflow") + ErrShard = errors.New("transaction routed to wrong shard") +) + +type Result struct { + Consumed []types.ObjectID + Created []object.Object + TxID types.Hash +} + +type Engine struct { + Network types.NetworkID + NativeToken types.TokenID + ShardCount uint32 +} + +func (e Engine) Execute(t tx.Transaction) (Result, error) { + if err := t.VerifyForNetwork(e.Network); err != nil { + return Result{}, err + } + if err := t.VerifyWitnesses(); err != nil { + return Result{}, err + } + if e.ShardCount == 0 { + e.ShardCount = 1 + } + if len(t.Inputs) > 0 { + expected := shardForObject(t.Inputs[0].ObjectID, e.ShardCount) + if t.ShardID != expected { + return Result{}, ErrShard + } + for _, in := range t.Inputs[1:] { + if shardForObject(in.ObjectID, e.ShardCount) != expected { + return Result{}, ErrShard + } + } + } + if len(t.Operations) != 1 { + return Result{}, ErrUnsupportedOperation + } + switch t.Operations[0].Kind { + case tx.OpTransfer: + return e.executeTransfer(t) + case tx.OpCreateToken: + return e.executeCreateToken(t, t.Operations[0].Payload) + default: + return Result{}, ErrUnsupportedOperation + } +} + +func (e Engine) executeTransfer(t tx.Transaction) (Result, error) { + inputTotals := map[types.TokenID]uint64{} + for _, w := range t.Witnesses { + if w.Object.Owner != t.Sender || w.Object.Kind != object.KindCoin { + return Result{}, ErrOwnership + } + coin, err := object.ParseCoin(w.Object.Data) + if err != nil { + return Result{}, err + } + if err := add(inputTotals, coin.Token, coin.Amount); err != nil { + return Result{}, err + } + } + + outputTotals := map[types.TokenID]uint64{} + txID := t.ID() + created := make([]object.Object, 0, len(t.Outputs)) + for i, spec := range t.Outputs { + if spec.Kind != object.KindCoin { + return Result{}, ErrConservation + } + coin, err := object.ParseCoin(spec.Data) + if err != nil { + return Result{}, err + } + if err := add(outputTotals, coin.Token, coin.Amount); err != nil { + return Result{}, err + } + created = append(created, object.Object{ + ID: types.ObjectIDFromTransaction(txID, uint32(i)), Version: 1, + Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...), + }) + } + + for token, inAmount := range inputTotals { + required := outputTotals[token] + if token == e.NativeToken { + if math.MaxUint64-required < t.Fee { + return Result{}, ErrOverflow + } + required += t.Fee + } + if inAmount != required { + return Result{}, ErrConservation + } + delete(outputTotals, token) + } + if len(outputTotals) != 0 { + return Result{}, ErrConservation + } + + consumed := make([]types.ObjectID, len(t.Inputs)) + for i, in := range t.Inputs { + consumed[i] = in.ObjectID + } + return Result{Consumed: consumed, Created: created, TxID: txID}, nil +} + +func (e Engine) executeCreateToken(t tx.Transaction, payload []byte) (Result, error) { + create, err := assets.ParseCreateToken(payload) + if err != nil { + return Result{}, err + } + if create.MintAuthority != t.Sender { + return Result{}, ErrOwnership + } + var nativeIn uint64 + for _, w := range t.Witnesses { + if w.Object.Owner != t.Sender || w.Object.Kind != object.KindCoin { + return Result{}, ErrOwnership + } + coin, err := object.ParseCoin(w.Object.Data) + if err != nil { + return Result{}, err + } + if coin.Token != e.NativeToken { + return Result{}, ErrConservation + } + if math.MaxUint64-nativeIn < coin.Amount { + return Result{}, ErrOverflow + } + nativeIn += coin.Amount + } + var nativeOut uint64 + txID := t.ID() + created := make([]object.Object, 0, len(t.Outputs)+2) + for i, spec := range t.Outputs { + coin, err := object.ParseCoin(spec.Data) + if err != nil || spec.Kind != object.KindCoin || coin.Token != e.NativeToken { + return Result{}, ErrConservation + } + if math.MaxUint64-nativeOut < coin.Amount { + return Result{}, ErrOverflow + } + nativeOut += coin.Amount + created = append(created, object.Object{ + ID: types.ObjectIDFromTransaction(txID, uint32(i)), Version: 1, + Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...), + }) + } + if math.MaxUint64-nativeOut < t.Fee || nativeIn != nativeOut+t.Fee { + return Result{}, ErrConservation + } + + tokenID := types.TokenIDFromTransaction(txID, 0) + definition := assets.Definition{ + TokenID: tokenID, Name: create.Name, Symbol: create.Symbol, Decimals: create.Decimals, + MaxSupply: create.MaxSupply, CurrentSupply: create.InitialSupply, + MintAuthority: create.MintAuthority, Burnable: create.Burnable, Transferable: create.Transferable, + } + defData, err := definition.MarshalBinary() + if err != nil { + return Result{}, err + } + defID := types.ObjectIDFromTransaction(txID, 0x80000000) + created = append(created, object.Object{ + ID: defID, Version: 1, Owner: t.Sender, Kind: object.KindTokenDefinition, Data: defData, + }) + initialCoin, err := object.NewCoinOutput(t.Sender, tokenID, create.InitialSupply) + if err != nil { + return Result{}, err + } + created = append(created, object.Object{ + ID: types.ObjectIDFromTransaction(txID, 0x80000001), Version: 1, + Owner: initialCoin.Owner, Kind: initialCoin.Kind, Data: initialCoin.Data, + }) + consumed := make([]types.ObjectID, len(t.Inputs)) + for i, in := range t.Inputs { + consumed[i] = in.ObjectID + } + return Result{Consumed: consumed, Created: created, TxID: txID}, nil +} + +func add(totals map[types.TokenID]uint64, token types.TokenID, amount uint64) error { + current := totals[token] + if math.MaxUint64-current < amount { + return ErrOverflow + } + totals[token] = current + amount + return nil +} + +func shardForObject(id types.ObjectID, shardCount uint32) uint32 { + if shardCount <= 1 { + return 0 + } + raw := types.Hash(id) + v := uint64(raw[0])<<56 | uint64(raw[1])<<48 | uint64(raw[2])<<40 | uint64(raw[3])<<32 | + uint64(raw[4])<<24 | uint64(raw[5])<<16 | uint64(raw[6])<<8 | uint64(raw[7]) + return uint32(v % uint64(shardCount)) +} diff --git a/internal/v2/execution/engine_test.go b/internal/v2/execution/engine_test.go new file mode 100644 index 00000000..1acd4874 --- /dev/null +++ b/internal/v2/execution/engine_test.go @@ -0,0 +1,142 @@ +package execution + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/assets" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/state" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func makeKey(t *testing.T) *ecdsa.PrivateKey { + t.Helper() + k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + return k +} + +func TestWalletProofToExecutionToNewRoot(t *testing.T) { + aliceKey := makeKey(t) + alicePub := elliptic.Marshal(elliptic.P256(), aliceKey.PublicKey.X, aliceKey.PublicKey.Y) + alice := types.AccountIDFromPublicKey(alicePub) + bob := types.AccountIDFromPublicKey([]byte("bob")) + network := types.NetworkID(types.HashBytes("network", []byte("v2"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + + store := worldstate.NewMemory() + genesisTx := types.HashBytes("genesis", []byte("coin")) + coinID := types.ObjectIDFromTransaction(genesisTx, 0) + initialOut, err := object.NewCoinOutput(alice, native, 100) + if err != nil { + t.Fatal(err) + } + initial := object.Object{ID: coinID, Version: 1, Owner: alice, Kind: initialOut.Kind, Data: initialOut.Data} + root, err := store.Apply(nil, []object.Object{initial}) + if err != nil { + t.Fatal(err) + } + + witnessObject, proof, ok := store.Proof(coinID) + if !ok { + t.Fatal("missing input") + } + inputHash := witnessObject.Hash() + toBob, _ := object.NewCoinOutput(bob, native, 25) + change, _ := object.NewCoinOutput(alice, native, 74) + transaction := tx.Transaction{ + Version: tx.Version, Network: network, ShardID: 0, StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: coinID, Version: 1, ObjectHash: inputHash}}, + Outputs: []object.OutputSpec{toBob, change}, + Operations: []tx.Operation{{Kind: tx.OpTransfer}}, + Fee: 1, ValidUntilHeight: 10, + Witnesses: []tx.Witness{{Object: witnessObject, Proof: proof}}, + } + transaction.Salt[0] = 7 + if err := transaction.Sign(aliceKey); err != nil { + t.Fatal(err) + } + + engine := Engine{Network: network, NativeToken: native, ShardCount: 1} + result, err := engine.Execute(transaction) + if err != nil { + t.Fatal(err) + } + if len(result.Consumed) != 1 || len(result.Created) != 2 { + t.Fatalf("unexpected result: %+v", result) + } + newRoot, err := store.Apply(result.Consumed, result.Created) + if err != nil { + t.Fatal(err) + } + if newRoot == root { + t.Fatal("state root did not change") + } + + bobObject, bobProof, ok := store.Proof(result.Created[0].ID) + if !ok { + t.Fatal("bob output missing") + } + bobHash := bobObject.Hash() + if !state.Verify(newRoot, types.Hash(bobObject.ID), bobHash[:], bobProof) { + t.Fatal("recipient cannot verify resulting object") + } +} + +func TestNativeTokenCreation(t *testing.T) { + key := makeKey(t) + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + alice := types.AccountIDFromPublicKey(pub) + network := types.NetworkID(types.HashBytes("network", []byte("v2"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + store := worldstate.NewMemory() + id := types.ObjectIDFromTransaction(types.HashBytes("seed", []byte("fee")), 0) + feeCoinOut, _ := object.NewCoinOutput(alice, native, 10) + feeCoin := object.Object{ID: id, Version: 1, Owner: alice, Kind: feeCoinOut.Kind, Data: feeCoinOut.Data} + root, err := store.Apply(nil, []object.Object{feeCoin}) + if err != nil { + t.Fatal(err) + } + witness, proof, _ := store.Proof(id) + h := witness.Hash() + change, _ := object.NewCoinOutput(alice, native, 9) + + create := assets.CreateToken{ + Name: "Example Token", Symbol: "EXM", Decimals: 6, + MaxSupply: 1_000_000, InitialSupply: 500_000, MintAuthority: alice, + Burnable: true, Transferable: true, + } + payload, err := create.MarshalBinary() + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{ + Version: tx.Version, Network: network, ShardID: 0, StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: id, Version: 1, ObjectHash: h}}, + Outputs: []object.OutputSpec{change}, + Operations: []tx.Operation{{Kind: tx.OpCreateToken, Payload: payload}}, + Fee: 1, + Witnesses: []tx.Witness{{Object: witness, Proof: proof}}, + } + transaction.Salt[0] = 9 + if err := transaction.Sign(key); err != nil { + t.Fatal(err) + } + result, err := (Engine{Network: network, NativeToken: native, ShardCount: 1}).Execute(transaction) + if err != nil { + t.Fatal(err) + } + if len(result.Created) != 3 { + t.Fatalf("expected change + token definition + initial supply, got %d", len(result.Created)) + } + if result.Created[1].Kind != object.KindTokenDefinition || result.Created[2].Kind != object.KindCoin { + t.Fatalf("unexpected token creation objects: %v %v", result.Created[1].Kind, result.Created[2].Kind) + } +} diff --git a/internal/v2/genesis/genesis.go b/internal/v2/genesis/genesis.go new file mode 100644 index 00000000..dac64f2c --- /dev/null +++ b/internal/v2/genesis/genesis.go @@ -0,0 +1,149 @@ +package genesis + +import ( + "bytes" + "errors" + "math" + "sort" + "strings" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ProtocolVersion uint16 = 2 + +var ( + ErrProtocolVersion = errors.New("genesis protocol version must be 2") + ErrChainName = errors.New("genesis chain name is required") + ErrShardConfig = errors.New("invalid genesis shard configuration") + ErrValidator = errors.New("invalid genesis validator") + ErrAllocation = errors.New("invalid genesis allocation") + ErrVotingPower = errors.New("genesis voting power overflow") +) + +type Validator struct { + ID types.ValidatorID + ConsensusPublicKey []byte + VotingPower uint64 +} + +type Allocation struct { + Owner types.AccountID + Amount uint64 +} + +type Config struct { + Version uint16 + ChainName string + GenesisUnix uint64 + InitialShardCount uint32 + MaxShardCount uint32 + NativeSymbol string + Validators []Validator + Allocations []Allocation +} + +func (g Config) Validate() error { + if g.Version != ProtocolVersion { + return ErrProtocolVersion + } + if strings.TrimSpace(g.ChainName) == "" { + return ErrChainName + } + if g.InitialShardCount == 0 || g.MaxShardCount < g.InitialShardCount { + return ErrShardConfig + } + symbol := strings.TrimSpace(g.NativeSymbol) + if symbol == "" || len(symbol) > 16 { + return ErrAllocation + } + + seenValidators := map[types.ValidatorID]struct{}{} + var total uint64 + for _, v := range g.Validators { + if types.IsZero32([32]byte(v.ID)) || len(v.ConsensusPublicKey) == 0 || v.VotingPower == 0 { + return ErrValidator + } + if _, ok := seenValidators[v.ID]; ok { + return ErrValidator + } + seenValidators[v.ID] = struct{}{} + if math.MaxUint64-total < v.VotingPower { + return ErrVotingPower + } + total += v.VotingPower + } + if len(g.Validators) == 0 || total == 0 { + return ErrValidator + } + + seenAllocations := map[types.AccountID]struct{}{} + for _, a := range g.Allocations { + if types.IsZero32([32]byte(a.Owner)) || a.Amount == 0 { + return ErrAllocation + } + if _, ok := seenAllocations[a.Owner]; ok { + return ErrAllocation + } + seenAllocations[a.Owner] = struct{}{} + } + return nil +} + +func (g Config) CanonicalBytes() ([]byte, error) { + if err := g.Validate(); err != nil { + return nil, err + } + + validators := append([]Validator(nil), g.Validators...) + sort.Slice(validators, func(i, j int) bool { + return bytes.Compare(validators[i].ID[:], validators[j].ID[:]) < 0 + }) + allocations := append([]Allocation(nil), g.Allocations...) + sort.Slice(allocations, func(i, j int) bool { + return bytes.Compare(allocations[i].Owner[:], allocations[j].Owner[:]) < 0 + }) + + var w codec.Writer + w.U16(g.Version) + w.String(strings.TrimSpace(g.ChainName)) + w.U64(g.GenesisUnix) + w.U32(g.InitialShardCount) + w.U32(g.MaxShardCount) + w.String(strings.TrimSpace(g.NativeSymbol)) + w.U32(uint32(len(validators))) + for _, v := range validators { + w.Fixed(v.ID[:]) + w.Bytes(v.ConsensusPublicKey) + w.U64(v.VotingPower) + } + w.U32(uint32(len(allocations))) + for _, a := range allocations { + w.Fixed(a.Owner[:]) + w.U64(a.Amount) + } + return w.BytesCopy(), nil +} + +func (g Config) NetworkID() (types.NetworkID, error) { + payload, err := g.CanonicalBytes() + if err != nil { + return types.NetworkID{}, err + } + return types.NetworkID(codec.DomainHash("zephyr/genesis/v2", payload)), nil +} + +func (g Config) TotalVotingPower() (uint64, error) { + if err := g.Validate(); err != nil { + return 0, err + } + var total uint64 + for _, v := range g.Validators { + if math.MaxUint64-total < v.VotingPower { + return 0, ErrVotingPower + } + total += v.VotingPower + } + return total, nil +} diff --git a/internal/v2/genesis/genesis_test.go b/internal/v2/genesis/genesis_test.go new file mode 100644 index 00000000..748e6eb2 --- /dev/null +++ b/internal/v2/genesis/genesis_test.go @@ -0,0 +1,39 @@ +package genesis + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestNetworkIDIsCanonicalAcrossInputOrdering(t *testing.T) { + v1 := types.ValidatorIDFromPublicKey([]byte("validator-1")) + v2 := types.ValidatorIDFromPublicKey([]byte("validator-2")) + a1 := types.AccountIDFromPublicKey([]byte("account-1")) + a2 := types.AccountIDFromPublicKey([]byte("account-2")) + + base := Config{ + Version: ProtocolVersion, ChainName: "zephyr-test", GenesisUnix: 1, + InitialShardCount: 1, MaxShardCount: 16, NativeSymbol: "ZPH", + Validators: []Validator{ + {ID: v1, ConsensusPublicKey: []byte("validator-1"), VotingPower: 10}, + {ID: v2, ConsensusPublicKey: []byte("validator-2"), VotingPower: 20}, + }, + Allocations: []Allocation{{Owner: a1, Amount: 10}, {Owner: a2, Amount: 20}}, + } + reordered := base + reordered.Validators = []Validator{base.Validators[1], base.Validators[0]} + reordered.Allocations = []Allocation{base.Allocations[1], base.Allocations[0]} + + id1, err := base.NetworkID() + if err != nil { + t.Fatal(err) + } + id2, err := reordered.NetworkID() + if err != nil { + t.Fatal(err) + } + if id1 != id2 { + t.Fatalf("network id changed with ordering: %s != %s", id1, id2) + } +} diff --git a/internal/v2/merkle/merkle.go b/internal/v2/merkle/merkle.go new file mode 100644 index 00000000..f2757ace --- /dev/null +++ b/internal/v2/merkle/merkle.go @@ -0,0 +1,111 @@ +package merkle + +import ( + "errors" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrIndex = errors.New("merkle proof index out of range") + +type Proof struct { + Index uint32 + LeafCount uint32 + Siblings []types.Hash +} + +func Root(leaves []types.Hash) types.Hash { + if len(leaves) == 0 { + return emptyLeaf() + } + level := append([]types.Hash(nil), leaves...) + target := nextPowerOfTwo(len(level)) + for len(level) < target { + level = append(level, emptyLeaf()) + } + for len(level) > 1 { + next := make([]types.Hash, len(level)/2) + for i := 0; i < len(level); i += 2 { + next[i/2] = branch(level[i], level[i+1]) + } + level = next + } + return level[0] +} + +func BuildProof(leaves []types.Hash, index int) (Proof, error) { + if index < 0 || index >= len(leaves) { + return Proof{}, ErrIndex + } + leafCount := len(leaves) + level := append([]types.Hash(nil), leaves...) + target := nextPowerOfTwo(len(level)) + for len(level) < target { + level = append(level, emptyLeaf()) + } + proof := Proof{Index: uint32(index), LeafCount: uint32(leafCount)} + position := index + for len(level) > 1 { + sibling := position ^ 1 + proof.Siblings = append(proof.Siblings, level[sibling]) + next := make([]types.Hash, len(level)/2) + for i := 0; i < len(level); i += 2 { + next[i/2] = branch(level[i], level[i+1]) + } + position /= 2 + level = next + } + return proof, nil +} + +func Verify(root, leaf types.Hash, proof Proof) bool { + if proof.LeafCount == 0 || proof.Index >= proof.LeafCount { + return false + } + target := nextPowerOfTwo(int(proof.LeafCount)) + requiredDepth := 0 + for n := target; n > 1; n /= 2 { + requiredDepth++ + } + if len(proof.Siblings) != requiredDepth { + return false + } + current := leaf + position := int(proof.Index) + for _, sibling := range proof.Siblings { + if position%2 == 0 { + current = branch(current, sibling) + } else { + current = branch(sibling, current) + } + position /= 2 + } + return current == root +} + +func Leaf(domain string, payload []byte) types.Hash { + return types.Hash(codec.DomainHash("zephyr/merkle/leaf/v2/"+domain, payload)) +} + +func branch(left, right types.Hash) types.Hash { + var w codec.Writer + w.Fixed(left[:]) + w.Fixed(right[:]) + return types.Hash(codec.DomainHash("zephyr/merkle/branch/v2", w.BytesCopy())) +} + +func emptyLeaf() types.Hash { + return types.Hash(codec.DomainHash("zephyr/merkle/empty/v2", nil)) +} + +func nextPowerOfTwo(n int) int { + if n <= 1 { + return 1 + } + p := 1 + for p < n { + p <<= 1 + } + return p +} diff --git a/internal/v2/merkle/merkle_test.go b/internal/v2/merkle/merkle_test.go new file mode 100644 index 00000000..b47ab3ff --- /dev/null +++ b/internal/v2/merkle/merkle_test.go @@ -0,0 +1,29 @@ +package merkle + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestProof(t *testing.T) { + leaves := []types.Hash{ + Leaf("test", []byte("a")), + Leaf("test", []byte("b")), + Leaf("test", []byte("c")), + } + root := Root(leaves) + for i, leaf := range leaves { + proof, err := BuildProof(leaves, i) + if err != nil { + t.Fatal(err) + } + if !Verify(root, leaf, proof) { + t.Fatalf("proof %d failed", i) + } + bad := types.HashBytes("bad", []byte{byte(i)}) + if Verify(root, bad, proof) { + t.Fatalf("bad leaf %d verified", i) + } + } +} diff --git a/internal/v2/object/object.go b/internal/v2/object/object.go new file mode 100644 index 00000000..f07f9058 --- /dev/null +++ b/internal/v2/object/object.go @@ -0,0 +1,199 @@ +package object + +import ( + "errors" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +type Kind uint16 + +const ( + KindUnknown Kind = iota + KindCoin + KindTokenDefinition + KindContract + KindContractState + KindComputeOffer + KindComputeJob + KindComputeAssignment + KindComputeResult + KindSystem +) + +const MaxObjectDataBytes = 1 << 20 + +var ( + ErrInvalidObject = errors.New("invalid protocol object") + ErrInvalidCoin = errors.New("invalid coin object") +) + +type Object struct { + ID types.ObjectID + Version uint64 + Owner types.AccountID + Kind Kind + Data []byte +} + +type OutputSpec struct { + Owner types.AccountID + Kind Kind + Data []byte +} + +func (o Object) Validate() error { + if types.IsZero32([32]byte(o.ID)) || o.Version == 0 || o.Kind == KindUnknown || len(o.Data) > MaxObjectDataBytes { + return ErrInvalidObject + } + return validateOwnerForKind(o.Owner, o.Kind) +} + +func (o OutputSpec) Validate() error { + if o.Kind == KindUnknown || len(o.Data) > MaxObjectDataBytes { + return ErrInvalidObject + } + return validateOwnerForKind(o.Owner, o.Kind) +} + +func validateOwnerForKind(owner types.AccountID, kind Kind) error { + switch kind { + case KindSystem: + return nil + default: + if types.IsZero32([32]byte(owner)) { + return ErrInvalidObject + } + return nil + } +} + +func (o Object) CanonicalBytes() []byte { + var w codec.Writer + w.Fixed(o.ID[:]) + w.U64(o.Version) + w.Fixed(o.Owner[:]) + w.U16(uint16(o.Kind)) + w.Bytes(o.Data) + return w.BytesCopy() +} + +func ParseObject(data []byte) (Object, error) { + r := codec.NewReader(data) + idBytes, err := r.Fixed(32) + if err != nil { + return Object{}, ErrInvalidObject + } + version, err := r.U64() + if err != nil { + return Object{}, ErrInvalidObject + } + ownerBytes, err := r.Fixed(32) + if err != nil { + return Object{}, ErrInvalidObject + } + kind, err := r.U16() + if err != nil { + return Object{}, ErrInvalidObject + } + payload, err := r.Bytes(MaxObjectDataBytes) + if err != nil { + return Object{}, ErrInvalidObject + } + if err := r.Done(); err != nil { + return Object{}, ErrInvalidObject + } + var id types.ObjectID + var owner types.AccountID + copy(id[:], idBytes) + copy(owner[:], ownerBytes) + out := Object{ID: id, Version: version, Owner: owner, Kind: Kind(kind), Data: payload} + if err := out.Validate(); err != nil { + return Object{}, err + } + return out, nil +} + +func ParseOutputSpec(data []byte) (OutputSpec, error) { + r := codec.NewReader(data) + ownerBytes, err := r.Fixed(32) + if err != nil { + return OutputSpec{}, ErrInvalidObject + } + kind, err := r.U16() + if err != nil { + return OutputSpec{}, ErrInvalidObject + } + payload, err := r.Bytes(MaxObjectDataBytes) + if err != nil { + return OutputSpec{}, ErrInvalidObject + } + if err := r.Done(); err != nil { + return OutputSpec{}, ErrInvalidObject + } + var owner types.AccountID + copy(owner[:], ownerBytes) + out := OutputSpec{Owner: owner, Kind: Kind(kind), Data: payload} + if err := out.Validate(); err != nil { + return OutputSpec{}, err + } + return out, nil +} + +func (o Object) Hash() types.Hash { + return types.Hash(codec.DomainHash("zephyr/object/v2", o.CanonicalBytes())) +} + +func (o OutputSpec) CanonicalBytes() []byte { + var w codec.Writer + w.Fixed(o.Owner[:]) + w.U16(uint16(o.Kind)) + w.Bytes(o.Data) + return w.BytesCopy() +} + +func (o OutputSpec) Hash() types.Hash { + return types.Hash(codec.DomainHash("zephyr/output-spec/v2", o.CanonicalBytes())) +} + +type Coin struct { + Token types.TokenID + Amount uint64 +} + +func (c Coin) MarshalBinary() []byte { + var w codec.Writer + w.Fixed(c.Token[:]) + w.U64(c.Amount) + return w.BytesCopy() +} + +func ParseCoin(data []byte) (Coin, error) { + r := codec.NewReader(data) + tokenBytes, err := r.Fixed(32) + if err != nil { + return Coin{}, ErrInvalidCoin + } + amount, err := r.U64() + if err != nil || amount == 0 { + return Coin{}, ErrInvalidCoin + } + if err := r.Done(); err != nil { + return Coin{}, ErrInvalidCoin + } + var token types.TokenID + copy(token[:], tokenBytes) + if types.IsZero32([32]byte(token)) { + return Coin{}, ErrInvalidCoin + } + return Coin{Token: token, Amount: amount}, nil +} + +func NewCoinOutput(owner types.AccountID, token types.TokenID, amount uint64) (OutputSpec, error) { + if types.IsZero32([32]byte(owner)) || types.IsZero32([32]byte(token)) || amount == 0 { + return OutputSpec{}, ErrInvalidCoin + } + out := OutputSpec{Owner: owner, Kind: KindCoin, Data: Coin{Token: token, Amount: amount}.MarshalBinary()} + return out, out.Validate() +} diff --git a/internal/v2/sharding/sharding.go b/internal/v2/sharding/sharding.go new file mode 100644 index 00000000..89f3224d --- /dev/null +++ b/internal/v2/sharding/sharding.go @@ -0,0 +1,173 @@ +package sharding + +import ( + "bytes" + "encoding/binary" + "errors" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ( + ErrShardCount = errors.New("invalid shard count") + ErrReceipt = errors.New("invalid cross-shard receipt") +) + +type Router struct { + ShardCount uint32 +} + +func (r Router) ShardForObject(id types.ObjectID) (uint32, error) { + if r.ShardCount == 0 { + return 0, ErrShardCount + } + if r.ShardCount == 1 { + return 0, nil + } + v := binary.BigEndian.Uint64(id[:8]) + return uint32(v % uint64(r.ShardCount)), nil +} + +type Commitment struct { + ShardID uint32 + StateRoot types.Hash + ReceiptRoot types.Hash + DataRoot types.Hash +} + +func (c Commitment) CanonicalBytes() []byte { + var w codec.Writer + w.U32(c.ShardID) + w.Fixed(c.StateRoot[:]) + w.Fixed(c.ReceiptRoot[:]) + w.Fixed(c.DataRoot[:]) + return w.BytesCopy() +} + +func (c Commitment) Hash() types.Hash { + return merkle.Leaf("shard-commitment", c.CanonicalBytes()) +} + +func CommitmentRoot(commitments []Commitment) (types.Hash, error) { + sorted, err := sortedCommitments(commitments) + if err != nil { + return types.Hash{}, err + } + leaves := make([]types.Hash, len(sorted)) + for i, c := range sorted { + leaves[i] = c.Hash() + } + return merkle.Root(leaves), nil +} + +func CommitmentProof(commitments []Commitment, shardID uint32) (Commitment, merkle.Proof, error) { + sorted, err := sortedCommitments(commitments) + if err != nil { + return Commitment{}, merkle.Proof{}, err + } + for i, c := range sorted { + if c.ShardID == shardID { + leaves := make([]types.Hash, len(sorted)) + for j, item := range sorted { + leaves[j] = item.Hash() + } + proof, err := merkle.BuildProof(leaves, i) + return c, proof, err + } + } + return Commitment{}, merkle.Proof{}, ErrReceipt +} + +type GlobalHeader struct { + Version uint16 + Network types.NetworkID + Height uint64 + ParentHash types.Hash + ShardCommitmentRoot types.Hash + ValidatorRoot types.Hash + DataRoot types.Hash + CertificateHash types.Hash +} + +func (h GlobalHeader) CanonicalBytes() []byte { + var w codec.Writer + w.U16(h.Version) + w.Fixed(h.Network[:]) + w.U64(h.Height) + w.Fixed(h.ParentHash[:]) + w.Fixed(h.ShardCommitmentRoot[:]) + w.Fixed(h.ValidatorRoot[:]) + w.Fixed(h.DataRoot[:]) + w.Fixed(h.CertificateHash[:]) + return w.BytesCopy() +} + +func (h GlobalHeader) Hash() types.Hash { + return types.Hash(codec.DomainHash("zephyr/global-header/v2", h.CanonicalBytes())) +} + +type CrossShardReceipt struct { + SourceShard uint32 + DestinationShard uint32 + SourceHeight uint64 + TransactionID types.Hash + OutputIndex uint32 + Output object.OutputSpec + SourceStateRoot types.Hash +} + +func (r CrossShardReceipt) Validate() error { + if r.SourceShard == r.DestinationShard || r.SourceHeight == 0 || + types.IsZero32([32]byte(r.TransactionID)) || types.IsZero32([32]byte(r.SourceStateRoot)) { + return ErrReceipt + } + return r.Output.Validate() +} + +func (r CrossShardReceipt) CanonicalBytes() ([]byte, error) { + if err := r.Validate(); err != nil { + return nil, err + } + var w codec.Writer + w.U32(r.SourceShard) + w.U32(r.DestinationShard) + w.U64(r.SourceHeight) + w.Fixed(r.TransactionID[:]) + w.U32(r.OutputIndex) + w.Bytes(r.Output.CanonicalBytes()) + w.Fixed(r.SourceStateRoot[:]) + return w.BytesCopy(), nil +} + +func (r CrossShardReceipt) Hash() (types.Hash, error) { + payload, err := r.CanonicalBytes() + if err != nil { + return types.Hash{}, err + } + return merkle.Leaf("cross-shard-receipt", payload), nil +} + +func sortedCommitments(in []Commitment) ([]Commitment, error) { + if len(in) == 0 { + return nil, ErrShardCount + } + out := append([]Commitment(nil), in...) + sort.Slice(out, func(i, j int) bool { return out[i].ShardID < out[j].ShardID }) + for i := range out { + if i > 0 && out[i-1].ShardID == out[i].ShardID { + return nil, ErrShardCount + } + if types.IsZero32([32]byte(out[i].StateRoot)) { + return nil, ErrShardCount + } + } + return out, nil +} + +func SameCommitment(a, b Commitment) bool { + return a.ShardID == b.ShardID && bytes.Equal(a.CanonicalBytes(), b.CanonicalBytes()) +} diff --git a/internal/v2/sharding/sharding_test.go b/internal/v2/sharding/sharding_test.go new file mode 100644 index 00000000..cf4532f0 --- /dev/null +++ b/internal/v2/sharding/sharding_test.go @@ -0,0 +1,27 @@ +package sharding + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestShardCommitmentProof(t *testing.T) { + commitments := []Commitment{ + {ShardID: 2, StateRoot: types.HashBytes("s", []byte("2")), ReceiptRoot: types.HashBytes("r", []byte("2")), DataRoot: types.HashBytes("d", []byte("2"))}, + {ShardID: 0, StateRoot: types.HashBytes("s", []byte("0")), ReceiptRoot: types.HashBytes("r", []byte("0")), DataRoot: types.HashBytes("d", []byte("0"))}, + {ShardID: 1, StateRoot: types.HashBytes("s", []byte("1")), ReceiptRoot: types.HashBytes("r", []byte("1")), DataRoot: types.HashBytes("d", []byte("1"))}, + } + root, err := CommitmentRoot(commitments) + if err != nil { + t.Fatal(err) + } + c, proof, err := CommitmentProof(commitments, 1) + if err != nil { + t.Fatal(err) + } + if !merkle.Verify(root, c.Hash(), proof) { + t.Fatal("commitment proof failed") + } +} diff --git a/internal/v2/state/smt.go b/internal/v2/state/smt.go new file mode 100644 index 00000000..0f342708 --- /dev/null +++ b/internal/v2/state/smt.go @@ -0,0 +1,312 @@ +package state + +import ( + "bytes" + "errors" + "sync" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const Depth = 256 + +var ( + ErrInvalidProof = errors.New("invalid sparse merkle proof") + ErrProofValueMismatch = errors.New("proof existence does not match supplied value") +) + +type nodeKey struct { + Depth uint16 + Prefix [32]byte +} + +type Proof struct { + Exists bool + Bitmap [32]byte + Siblings []types.Hash +} + +type Tree struct { + mu sync.RWMutex + values map[types.Hash][]byte + nodes map[nodeKey]types.Hash + defaults [Depth + 1]types.Hash +} + +func NewTree() *Tree { + t := &Tree{ + values: make(map[types.Hash][]byte), + nodes: make(map[nodeKey]types.Hash), + } + t.defaults[Depth] = types.Hash(codec.DomainHash("zephyr/smt/empty-leaf/v2", nil)) + for d := Depth - 1; d >= 0; d-- { + t.defaults[d] = branchHash(t.defaults[d+1], t.defaults[d+1]) + } + return t +} + +func (t *Tree) Root() types.Hash { + t.mu.RLock() + defer t.mu.RUnlock() + return t.getNode(0, [32]byte{}) +} + +func (t *Tree) Get(key types.Hash) ([]byte, bool) { + t.mu.RLock() + defer t.mu.RUnlock() + v, ok := t.values[key] + if !ok { + return nil, false + } + out := append([]byte(nil), v...) + return out, true +} + +func (t *Tree) Update(key types.Hash, value []byte) types.Hash { + t.mu.Lock() + defer t.mu.Unlock() + t.updateLocked(key, value) + return t.getNode(0, [32]byte{}) +} + +func (t *Tree) Apply(updates map[types.Hash][]byte) types.Hash { + t.mu.Lock() + defer t.mu.Unlock() + for key, value := range updates { + t.updateLocked(key, value) + } + return t.getNode(0, [32]byte{}) +} + +func (t *Tree) updateLocked(key types.Hash, value []byte) { + rawKey := [32]byte(key) + leafPrefix := prefixAtDepth(rawKey, Depth) + leafNode := nodeKey{Depth: Depth, Prefix: leafPrefix} + if value == nil { + delete(t.values, key) + delete(t.nodes, leafNode) + } else { + copyValue := append([]byte(nil), value...) + t.values[key] = copyValue + h := leafHash(key, copyValue) + t.nodes[leafNode] = h + } + + for depth := Depth - 1; depth >= 0; depth-- { + bit := bitAt(rawKey, depth) + childPrefix := prefixAtDepth(rawKey, depth+1) + siblingPrefix := childPrefix + toggleBit(&siblingPrefix, depth) + child := t.getNode(depth+1, childPrefix) + sibling := t.getNode(depth+1, siblingPrefix) + + var left, right types.Hash + if bit == 0 { + left, right = child, sibling + } else { + left, right = sibling, child + } + parent := branchHash(left, right) + parentKey := nodeKey{Depth: uint16(depth), Prefix: prefixAtDepth(rawKey, depth)} + if parent == t.defaults[depth] { + delete(t.nodes, parentKey) + } else { + t.nodes[parentKey] = parent + } + } +} + +func (t *Tree) Prove(key types.Hash) Proof { + t.mu.RLock() + defer t.mu.RUnlock() + + rawKey := [32]byte(key) + _, exists := t.values[key] + proof := Proof{Exists: exists} + for i := 0; i < Depth; i++ { + depth := Depth - i + siblingPrefix := prefixAtDepth(rawKey, depth) + toggleBit(&siblingPrefix, depth-1) + sibling := t.getNode(depth, siblingPrefix) + if sibling != t.defaults[depth] { + setBitmapBit(&proof.Bitmap, i) + proof.Siblings = append(proof.Siblings, sibling) + } + } + return proof +} + +func (p Proof) MarshalBinary() []byte { + var w codec.Writer + w.Bool(p.Exists) + w.Fixed(p.Bitmap[:]) + w.U16(uint16(len(p.Siblings))) + for _, sibling := range p.Siblings { + w.Fixed(sibling[:]) + } + return w.BytesCopy() +} + +func ParseProof(data []byte) (Proof, error) { + r := codec.NewReader(data) + exists, err := r.Bool() + if err != nil { + return Proof{}, ErrInvalidProof + } + bitmap, err := r.Fixed(32) + if err != nil { + return Proof{}, ErrInvalidProof + } + count, err := r.U16() + if err != nil || count > Depth { + return Proof{}, ErrInvalidProof + } + proof := Proof{Exists: exists, Siblings: make([]types.Hash, int(count))} + copy(proof.Bitmap[:], bitmap) + for i := range proof.Siblings { + raw, err := r.Fixed(32) + if err != nil { + return Proof{}, ErrInvalidProof + } + copy(proof.Siblings[i][:], raw) + } + if err := r.Done(); err != nil { + return Proof{}, ErrInvalidProof + } + bits := 0 + for i := 0; i < Depth; i++ { + if bitmapBit(proof.Bitmap, i) { + bits++ + } + } + if bits != len(proof.Siblings) { + return Proof{}, ErrInvalidProof + } + return proof, nil +} + +func Verify(root, key types.Hash, value []byte, proof Proof) bool { + if proof.Exists != (value != nil) { + return false + } + defaults := defaultHashes() + rawKey := [32]byte(key) + var current types.Hash + if proof.Exists { + current = leafHash(key, value) + } else { + current = defaults[Depth] + } + + siblingIndex := 0 + for i := 0; i < Depth; i++ { + depth := Depth - i + sibling := defaults[depth] + if bitmapBit(proof.Bitmap, i) { + if siblingIndex >= len(proof.Siblings) { + return false + } + sibling = proof.Siblings[siblingIndex] + siblingIndex++ + } + + bitIndex := depth - 1 + if bitAt(rawKey, bitIndex) == 0 { + current = branchHash(current, sibling) + } else { + current = branchHash(sibling, current) + } + } + return siblingIndex == len(proof.Siblings) && current == root +} + +func (t *Tree) getNode(depth int, prefix [32]byte) types.Hash { + if h, ok := t.nodes[nodeKey{Depth: uint16(depth), Prefix: prefixAtDepth(prefix, depth)}]; ok { + return h + } + return t.defaults[depth] +} + +func defaultHashes() [Depth + 1]types.Hash { + var defaults [Depth + 1]types.Hash + defaults[Depth] = types.Hash(codec.DomainHash("zephyr/smt/empty-leaf/v2", nil)) + for d := Depth - 1; d >= 0; d-- { + defaults[d] = branchHash(defaults[d+1], defaults[d+1]) + } + return defaults +} + +func leafHash(key types.Hash, value []byte) types.Hash { + var w codec.Writer + w.Fixed(key[:]) + w.Bytes(value) + return types.Hash(codec.DomainHash("zephyr/smt/leaf/v2", w.BytesCopy())) +} + +func branchHash(left, right types.Hash) types.Hash { + var w codec.Writer + w.Fixed(left[:]) + w.Fixed(right[:]) + return types.Hash(codec.DomainHash("zephyr/smt/branch/v2", w.BytesCopy())) +} + +func prefixAtDepth(key [32]byte, depth int) [32]byte { + if depth <= 0 { + return [32]byte{} + } + if depth >= Depth { + return key + } + out := key + fullBytes := depth / 8 + remainingBits := depth % 8 + if remainingBits == 0 { + for i := fullBytes; i < len(out); i++ { + out[i] = 0 + } + return out + } + mask := byte(0xFF << (8 - remainingBits)) + out[fullBytes] &= mask + for i := fullBytes + 1; i < len(out); i++ { + out[i] = 0 + } + return out +} + +func bitAt(key [32]byte, bitIndex int) uint8 { + byteIndex := bitIndex / 8 + shift := 7 - (bitIndex % 8) + return (key[byteIndex] >> shift) & 1 +} + +func toggleBit(key *[32]byte, bitIndex int) { + byteIndex := bitIndex / 8 + shift := 7 - (bitIndex % 8) + key[byteIndex] ^= 1 << shift +} + +func setBitmapBit(bitmap *[32]byte, index int) { + byteIndex := index / 8 + shift := uint(index % 8) + bitmap[byteIndex] |= 1 << shift +} + +func bitmapBit(bitmap [32]byte, index int) bool { + byteIndex := index / 8 + shift := uint(index % 8) + return bitmap[byteIndex]&(1<> 8)}) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + tree.Update(keys[i%len(keys)], []byte{1, 2, 3, 4, 5, 6, 7, 8}) + } +} + +func BenchmarkSparseMerkleProofVerify(b *testing.B) { + tree := NewTree() + k := key("bench") + tree.Update(k, []byte("value")) + root := tree.Root() + proof := tree.Prove(k) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if !Verify(root, k, []byte("value"), proof) { + b.Fatal("proof failed") + } + } +} diff --git a/internal/v2/transport/transport.go b/internal/v2/transport/transport.go new file mode 100644 index 00000000..c843e0fc --- /dev/null +++ b/internal/v2/transport/transport.go @@ -0,0 +1,42 @@ +package transport + +import ( + "context" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +type Role uint8 + +const ( + RoleCitizen Role = iota + 1 + RoleFullNode + RoleValidator + RoleArchive + RoleComputeProvider +) + +type PeerIdentity struct { + NodeID types.NodeID + ValidatorID *types.ValidatorID + Roles []Role +} + +type ConsensusTransport interface { + BroadcastProposal(ctx context.Context, payload []byte) error + BroadcastVote(ctx context.Context, payload []byte) error + FetchCertifiedBlock(ctx context.Context, height uint64) ([]byte, error) +} + +type TransactionTransport interface { + Submit(ctx context.Context, transaction tx.Transaction) error + Relay(ctx context.Context, transaction tx.Transaction, shardID uint32) error +} + +type LightTransport interface { + FetchFinalizedHeader(ctx context.Context, height uint64) (sharding.GlobalHeader, error) + FetchShardCommitment(ctx context.Context, height uint64, shardID uint32) (sharding.Commitment, []byte, error) + FetchObjectProof(ctx context.Context, root types.Hash, id types.ObjectID) ([]byte, error) +} diff --git a/internal/v2/tx/transaction.go b/internal/v2/tx/transaction.go new file mode 100644 index 00000000..a41d55e6 --- /dev/null +++ b/internal/v2/tx/transaction.go @@ -0,0 +1,451 @@ +package tx + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" + "math/big" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/state" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + Version uint16 = 2 + + OpTransfer uint16 = 1 + OpCreateToken uint16 = 2 + OpDeployContract uint16 = 3 + OpContractCall uint16 = 4 + OpComputeOffer uint16 = 5 + OpComputeJob uint16 = 6 + OpComputeResult uint16 = 7 + + MaxInputs = 4096 + MaxOutputs = 4096 + MaxOperations = 64 + MaxOpPayload = 4 << 20 + MaxWireBytes = 32 << 20 + MaxIntentBytes = 24 << 20 +) + +var ( + ErrVersion = errors.New("invalid transaction version") + ErrNetwork = errors.New("invalid transaction network") + ErrSender = errors.New("invalid transaction sender") + ErrSignature = errors.New("invalid transaction signature") + ErrCanonicalSig = errors.New("transaction signature must use low-S P-256 form") + ErrStructure = errors.New("invalid proof-carrying transaction structure") + ErrWitness = errors.New("invalid transaction state witness") + ErrExpired = errors.New("transaction expired") + ErrWire = errors.New("invalid canonical transaction wire payload") +) + +type InputRef struct { + ObjectID types.ObjectID + Version uint64 + ObjectHash types.Hash +} + +type Witness struct { + Object object.Object + Proof state.Proof +} + +type Operation struct { + Kind uint16 + Payload []byte +} + +type Transaction struct { + Version uint16 + Network types.NetworkID + Sender types.AccountID + SenderPublicKey []byte + ShardID uint32 + StateRoot types.Hash + Salt [16]byte + Inputs []InputRef + Outputs []object.OutputSpec + Operations []Operation + Fee uint64 + ValidUntilHeight uint64 + Signature []byte + Witnesses []Witness +} + +func (t Transaction) IntentBytes() []byte { + var w codec.Writer + writeIntent(&w, t) + return w.BytesCopy() +} + +func (t Transaction) SigningDigest() types.Hash { + return types.Hash(codec.DomainHash("zephyr/transaction-signing/v2", t.IntentBytes())) +} + +func (t Transaction) ID() types.Hash { + return types.Hash(codec.DomainHash("zephyr/transaction-id/v2", t.IntentBytes())) +} + +func (t Transaction) MarshalBinary() ([]byte, error) { + if err := t.ValidateStatic(); err != nil { + return nil, err + } + if len(t.Witnesses) != len(t.Inputs) { + return nil, ErrWitness + } + var w codec.Writer + w.Bytes(t.IntentBytes()) + w.Bytes(t.Signature) + w.U32(uint32(len(t.Witnesses))) + for _, witness := range t.Witnesses { + w.Bytes(witness.Object.CanonicalBytes()) + w.Bytes(witness.Proof.MarshalBinary()) + } + payload := w.BytesCopy() + if len(payload) > MaxWireBytes { + return nil, ErrWire + } + return payload, nil +} + +func ParseTransaction(data []byte) (Transaction, error) { + if len(data) == 0 || len(data) > MaxWireBytes { + return Transaction{}, ErrWire + } + r := codec.NewReader(data) + intent, err := r.Bytes(MaxIntentBytes) + if err != nil { + return Transaction{}, ErrWire + } + t, err := parseIntent(intent) + if err != nil { + return Transaction{}, err + } + signature, err := r.Bytes(64) + if err != nil || len(signature) != 64 { + return Transaction{}, ErrWire + } + t.Signature = signature + count, err := r.U32() + if err != nil || count > MaxInputs || int(count) != len(t.Inputs) { + return Transaction{}, ErrWitness + } + t.Witnesses = make([]Witness, int(count)) + for i := range t.Witnesses { + objectBytes, err := r.Bytes(object.MaxObjectDataBytes + 128) + if err != nil { + return Transaction{}, ErrWire + } + obj, err := object.ParseObject(objectBytes) + if err != nil { + return Transaction{}, ErrWire + } + proofBytes, err := r.Bytes(16 << 10) + if err != nil { + return Transaction{}, ErrWire + } + proof, err := state.ParseProof(proofBytes) + if err != nil { + return Transaction{}, ErrWire + } + t.Witnesses[i] = Witness{Object: obj, Proof: proof} + } + if err := r.Done(); err != nil { + return Transaction{}, ErrWire + } + if err := t.ValidateStatic(); err != nil { + return Transaction{}, err + } + return t, nil +} + +func (t *Transaction) Sign(privateKey *ecdsa.PrivateKey) error { + if privateKey == nil || privateKey.Curve != elliptic.P256() { + return ErrSender + } + t.Version = Version + t.SenderPublicKey = elliptic.Marshal(elliptic.P256(), privateKey.PublicKey.X, privateKey.PublicKey.Y) + t.Sender = types.AccountIDFromPublicKey(t.SenderPublicKey) + digest := t.SigningDigest() + r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest[:]) + if err != nil { + return err + } + s = normalizeLowS(s) + t.Signature = append(pad32(r), pad32(s)...) + return nil +} + +func (t Transaction) ValidateStatic() error { + if t.Version != Version { + return ErrVersion + } + if types.IsZero32([32]byte(t.Network)) || types.IsZero32([32]byte(t.StateRoot)) { + return ErrNetwork + } + if len(t.SenderPublicKey) != 65 { + return ErrSender + } + x, y := elliptic.Unmarshal(elliptic.P256(), t.SenderPublicKey) + if x == nil || y == nil { + return ErrSender + } + if types.AccountIDFromPublicKey(t.SenderPublicKey) != t.Sender { + return ErrSender + } + var zeroSalt [16]byte + if t.Salt == zeroSalt || len(t.Inputs) > MaxInputs || len(t.Outputs) > MaxOutputs || + len(t.Operations) == 0 || len(t.Operations) > MaxOperations { + return ErrStructure + } + seenInputs := map[types.ObjectID]struct{}{} + for _, in := range t.Inputs { + if types.IsZero32([32]byte(in.ObjectID)) || in.Version == 0 || types.IsZero32([32]byte(in.ObjectHash)) { + return ErrStructure + } + if _, ok := seenInputs[in.ObjectID]; ok { + return ErrStructure + } + seenInputs[in.ObjectID] = struct{}{} + } + for _, out := range t.Outputs { + if err := out.Validate(); err != nil { + return ErrStructure + } + } + for _, op := range t.Operations { + if op.Kind == 0 || len(op.Payload) > MaxOpPayload { + return ErrStructure + } + } + return verifySignature(&ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}, t.SigningDigest(), t.Signature) +} + +func (t Transaction) ValidateAtHeight(height uint64) error { + if err := t.ValidateStatic(); err != nil { + return err + } + if t.ValidUntilHeight != 0 && height > t.ValidUntilHeight { + return ErrExpired + } + return nil +} + +func (t Transaction) VerifyForNetwork(network types.NetworkID) error { + if t.Network != network { + return ErrNetwork + } + return t.ValidateStatic() +} + +func (t Transaction) VerifyWitnesses() error { + if len(t.Inputs) != len(t.Witnesses) { + return ErrWitness + } + witnesses := make(map[types.ObjectID]Witness, len(t.Witnesses)) + for _, witness := range t.Witnesses { + if err := witness.Object.Validate(); err != nil { + return ErrWitness + } + if _, exists := witnesses[witness.Object.ID]; exists { + return ErrWitness + } + witnesses[witness.Object.ID] = witness + } + for _, in := range t.Inputs { + witness, ok := witnesses[in.ObjectID] + if !ok || witness.Object.Version != in.Version || witness.Object.Hash() != in.ObjectHash || !witness.Proof.Exists { + return ErrWitness + } + key := types.Hash(in.ObjectID) + value := in.ObjectHash[:] + if !state.Verify(t.StateRoot, key, value, witness.Proof) { + return ErrWitness + } + } + return nil +} + +func writeIntent(w *codec.Writer, t Transaction) { + w.U16(t.Version) + w.Fixed(t.Network[:]) + w.Fixed(t.Sender[:]) + w.Bytes(t.SenderPublicKey) + w.U32(t.ShardID) + w.Fixed(t.StateRoot[:]) + w.Fixed(t.Salt[:]) + w.U32(uint32(len(t.Inputs))) + for _, in := range t.Inputs { + w.Fixed(in.ObjectID[:]) + w.U64(in.Version) + w.Fixed(in.ObjectHash[:]) + } + w.U32(uint32(len(t.Outputs))) + for _, out := range t.Outputs { + w.Bytes(out.CanonicalBytes()) + } + w.U32(uint32(len(t.Operations))) + for _, op := range t.Operations { + w.U16(op.Kind) + w.Bytes(op.Payload) + } + w.U64(t.Fee) + w.U64(t.ValidUntilHeight) +} + +func parseIntent(data []byte) (Transaction, error) { + if len(data) == 0 || len(data) > MaxIntentBytes { + return Transaction{}, ErrWire + } + r := codec.NewReader(data) + version, err := r.U16() + if err != nil { + return Transaction{}, ErrWire + } + networkBytes, err := r.Fixed(32) + if err != nil { + return Transaction{}, ErrWire + } + senderBytes, err := r.Fixed(32) + if err != nil { + return Transaction{}, ErrWire + } + publicKey, err := r.Bytes(65) + if err != nil { + return Transaction{}, ErrWire + } + shardID, err := r.U32() + if err != nil { + return Transaction{}, ErrWire + } + rootBytes, err := r.Fixed(32) + if err != nil { + return Transaction{}, ErrWire + } + saltBytes, err := r.Fixed(16) + if err != nil { + return Transaction{}, ErrWire + } + inputCount, err := r.U32() + if err != nil || inputCount > MaxInputs { + return Transaction{}, ErrWire + } + t := Transaction{ + Version: version, SenderPublicKey: publicKey, ShardID: shardID, + Inputs: make([]InputRef, int(inputCount)), + } + copy(t.Network[:], networkBytes) + copy(t.Sender[:], senderBytes) + copy(t.StateRoot[:], rootBytes) + copy(t.Salt[:], saltBytes) + + for i := range t.Inputs { + idBytes, err := r.Fixed(32) + if err != nil { + return Transaction{}, ErrWire + } + version, err := r.U64() + if err != nil { + return Transaction{}, ErrWire + } + hashBytes, err := r.Fixed(32) + if err != nil { + return Transaction{}, ErrWire + } + copy(t.Inputs[i].ObjectID[:], idBytes) + t.Inputs[i].Version = version + copy(t.Inputs[i].ObjectHash[:], hashBytes) + } + outputCount, err := r.U32() + if err != nil || outputCount > MaxOutputs { + return Transaction{}, ErrWire + } + t.Outputs = make([]object.OutputSpec, int(outputCount)) + for i := range t.Outputs { + outputBytes, err := r.Bytes(object.MaxObjectDataBytes + 64) + if err != nil { + return Transaction{}, ErrWire + } + out, err := object.ParseOutputSpec(outputBytes) + if err != nil { + return Transaction{}, ErrWire + } + t.Outputs[i] = out + } + opCount, err := r.U32() + if err != nil || opCount == 0 || opCount > MaxOperations { + return Transaction{}, ErrWire + } + t.Operations = make([]Operation, int(opCount)) + for i := range t.Operations { + kind, err := r.U16() + if err != nil { + return Transaction{}, ErrWire + } + payload, err := r.Bytes(MaxOpPayload) + if err != nil { + return Transaction{}, ErrWire + } + t.Operations[i] = Operation{Kind: kind, Payload: payload} + } + fee, err := r.U64() + if err != nil { + return Transaction{}, ErrWire + } + validUntil, err := r.U64() + if err != nil { + return Transaction{}, ErrWire + } + t.Fee = fee + t.ValidUntilHeight = validUntil + if err := r.Done(); err != nil { + return Transaction{}, ErrWire + } + return t, nil +} + +func verifySignature(publicKey *ecdsa.PublicKey, digest types.Hash, signature []byte) error { + if len(signature) != 64 { + return ErrSignature + } + r := new(big.Int).SetBytes(signature[:32]) + s := new(big.Int).SetBytes(signature[32:]) + order := elliptic.P256().Params().N + if r.Sign() <= 0 || s.Sign() <= 0 || r.Cmp(order) >= 0 || s.Cmp(order) >= 0 { + return ErrSignature + } + if s.Cmp(halfOrder()) > 0 { + return ErrCanonicalSig + } + if !ecdsa.Verify(publicKey, digest[:], r, s) { + return ErrSignature + } + return nil +} + +func normalizeLowS(s *big.Int) *big.Int { + if s.Cmp(halfOrder()) <= 0 { + return new(big.Int).Set(s) + } + return new(big.Int).Sub(elliptic.P256().Params().N, s) +} + +func halfOrder() *big.Int { + return new(big.Int).Rsh(new(big.Int).Set(elliptic.P256().Params().N), 1) +} + +func pad32(v *big.Int) []byte { + raw := v.Bytes() + out := make([]byte, 32) + if len(raw) >= 32 { + copy(out, raw[len(raw)-32:]) + } else { + copy(out[32-len(raw):], raw) + } + return out +} diff --git a/internal/v2/tx/transaction_test.go b/internal/v2/tx/transaction_test.go new file mode 100644 index 00000000..aaa30794 --- /dev/null +++ b/internal/v2/tx/transaction_test.go @@ -0,0 +1,78 @@ +package tx + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestProofCarryingTransactionWireRoundTrip(t *testing.T) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + network := types.NetworkID(types.HashBytes("network", []byte("test"))) + token := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + sender := types.AccountIDFromPublicKey(elliptic.Marshal(elliptic.P256(), privateKey.PublicKey.X, privateKey.PublicKey.Y)) + store := worldstate.NewMemory() + seed := types.HashBytes("seed", []byte("coin")) + id := types.ObjectIDFromTransaction(seed, 0) + coinOut, err := object.NewCoinOutput(sender, token, 100) + if err != nil { + t.Fatal(err) + } + coin := object.Object{ID: id, Version: 1, Owner: coinOut.Owner, Kind: coinOut.Kind, Data: coinOut.Data} + root, err := store.Apply(nil, []object.Object{coin}) + if err != nil { + t.Fatal(err) + } + got, proof, ok := store.Proof(id) + if !ok { + t.Fatal("missing coin") + } + h := got.Hash() + recipient := types.AccountIDFromPublicKey([]byte("recipient")) + out, err := object.NewCoinOutput(recipient, token, 99) + if err != nil { + t.Fatal(err) + } + + transaction := Transaction{ + Version: Version, Network: network, ShardID: 0, StateRoot: root, + Inputs: []InputRef{{ObjectID: id, Version: 1, ObjectHash: h}}, + Outputs: []object.OutputSpec{out}, + Operations: []Operation{{Kind: OpTransfer}}, + Fee: 1, ValidUntilHeight: 100, + Witnesses: []Witness{{Object: got, Proof: proof}}, + } + transaction.Salt[0] = 1 + if err := transaction.Sign(privateKey); err != nil { + t.Fatal(err) + } + if err := transaction.ValidateStatic(); err != nil { + t.Fatal(err) + } + if err := transaction.VerifyWitnesses(); err != nil { + t.Fatal(err) + } + + wire, err := transaction.MarshalBinary() + if err != nil { + t.Fatal(err) + } + decoded, err := ParseTransaction(wire) + if err != nil { + t.Fatal(err) + } + if decoded.ID() != transaction.ID() { + t.Fatal("transaction ID changed after wire round-trip") + } + if err := decoded.VerifyWitnesses(); err != nil { + t.Fatal(err) + } +} diff --git a/internal/v2/types/types.go b/internal/v2/types/types.go new file mode 100644 index 00000000..f9213247 --- /dev/null +++ b/internal/v2/types/types.go @@ -0,0 +1,76 @@ +package types + +import ( + "encoding/hex" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" +) + +type Hash [32]byte +type NetworkID [32]byte +type AccountID [32]byte +type NodeID [32]byte +type ValidatorID [32]byte +type ObjectID [32]byte +type TokenID [32]byte +type ContractID [32]byte +type JobID [32]byte + +func (h Hash) String() string { return hex.EncodeToString(h[:]) } +func (n NetworkID) String() string { return hex.EncodeToString(n[:]) } +func (a AccountID) String() string { return hex.EncodeToString(a[:]) } +func (n NodeID) String() string { return hex.EncodeToString(n[:]) } +func (v ValidatorID) String() string { return hex.EncodeToString(v[:]) } +func (o ObjectID) String() string { return hex.EncodeToString(o[:]) } +func (t TokenID) String() string { return hex.EncodeToString(t[:]) } +func (c ContractID) String() string { return hex.EncodeToString(c[:]) } +func (j JobID) String() string { return hex.EncodeToString(j[:]) } + +func IsZero32(v [32]byte) bool { + var zero [32]byte + return v == zero +} + +func HashBytes(domain string, payload []byte) Hash { + return Hash(codec.DomainHash(domain, payload)) +} + +func AccountIDFromPublicKey(publicKey []byte) AccountID { + return AccountID(codec.DomainHash("zephyr/account-id/v2", publicKey)) +} + +func NodeIDFromPublicKey(publicKey []byte) NodeID { + return NodeID(codec.DomainHash("zephyr/node-id/v2", publicKey)) +} + +func ValidatorIDFromPublicKey(publicKey []byte) ValidatorID { + return ValidatorID(codec.DomainHash("zephyr/validator-id/v2", publicKey)) +} + +func ObjectIDFromTransaction(txID Hash, index uint32) ObjectID { + var w codec.Writer + w.Fixed(txID[:]) + w.U32(index) + return ObjectID(codec.DomainHash("zephyr/object-id/v2", w.BytesCopy())) +} + +func TokenIDFromTransaction(txID Hash, operationIndex uint32) TokenID { + var w codec.Writer + w.Fixed(txID[:]) + w.U32(operationIndex) + return TokenID(codec.DomainHash("zephyr/token-id/v2", w.BytesCopy())) +} + +func ContractIDFromTransaction(txID Hash, operationIndex uint32) ContractID { + var w codec.Writer + w.Fixed(txID[:]) + w.U32(operationIndex) + return ContractID(codec.DomainHash("zephyr/contract-id/v2", w.BytesCopy())) +} + +func JobIDFromTransaction(txID Hash, operationIndex uint32) JobID { + var w codec.Writer + w.Fixed(txID[:]) + w.U32(operationIndex) + return JobID(codec.DomainHash("zephyr/compute-job-id/v2", w.BytesCopy())) +} diff --git a/internal/v2/worldstate/store.go b/internal/v2/worldstate/store.go new file mode 100644 index 00000000..345a7798 --- /dev/null +++ b/internal/v2/worldstate/store.go @@ -0,0 +1,105 @@ +package worldstate + +import ( + "errors" + "sync" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/state" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ( + ErrObjectNotFound = errors.New("object not found") + ErrObjectExists = errors.New("object already exists") +) + +type Backend interface { + Root() types.Hash + GetObject(id types.ObjectID) (object.Object, bool) + Proof(id types.ObjectID) (object.Object, state.Proof, bool) + Apply(consumed []types.ObjectID, created []object.Object) (types.Hash, error) +} + +type Memory struct { + mu sync.RWMutex + tree *state.Tree + objects map[types.ObjectID]object.Object +} + +func NewMemory() *Memory { + return &Memory{tree: state.NewTree(), objects: make(map[types.ObjectID]object.Object)} +} + +func (m *Memory) Root() types.Hash { + m.mu.RLock() + defer m.mu.RUnlock() + return m.tree.Root() +} + +func (m *Memory) GetObject(id types.ObjectID) (object.Object, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + o, ok := m.objects[id] + if !ok { + return object.Object{}, false + } + o.Data = append([]byte(nil), o.Data...) + return o, true +} + +func (m *Memory) Proof(id types.ObjectID) (object.Object, state.Proof, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + o, ok := m.objects[id] + if !ok { + return object.Object{}, m.tree.Prove(types.Hash(id)), false + } + o.Data = append([]byte(nil), o.Data...) + return o, m.tree.Prove(types.Hash(id)), true +} + +func (m *Memory) Apply(consumed []types.ObjectID, created []object.Object) (types.Hash, error) { + m.mu.Lock() + defer m.mu.Unlock() + + seenConsumed := map[types.ObjectID]struct{}{} + for _, id := range consumed { + if _, duplicate := seenConsumed[id]; duplicate { + return m.tree.Root(), ErrObjectNotFound + } + seenConsumed[id] = struct{}{} + if _, ok := m.objects[id]; !ok { + return m.tree.Root(), ErrObjectNotFound + } + } + seenCreated := map[types.ObjectID]struct{}{} + for _, o := range created { + if err := o.Validate(); err != nil { + return m.tree.Root(), err + } + if _, duplicate := seenCreated[o.ID]; duplicate { + return m.tree.Root(), ErrObjectExists + } + seenCreated[o.ID] = struct{}{} + if _, ok := m.objects[o.ID]; ok { + if _, replacingConsumed := seenConsumed[o.ID]; !replacingConsumed { + return m.tree.Root(), ErrObjectExists + } + } + } + + updates := make(map[types.Hash][]byte, len(consumed)+len(created)) + for _, id := range consumed { + delete(m.objects, id) + updates[types.Hash(id)] = nil + } + for _, o := range created { + copyObject := o + copyObject.Data = append([]byte(nil), o.Data...) + m.objects[o.ID] = copyObject + h := o.Hash() + updates[types.Hash(o.ID)] = h[:] + } + return m.tree.Apply(updates), nil +} diff --git a/internal/v2/worldstate/store_test.go b/internal/v2/worldstate/store_test.go new file mode 100644 index 00000000..4b20210a --- /dev/null +++ b/internal/v2/worldstate/store_test.go @@ -0,0 +1,34 @@ +package worldstate + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/state" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestObjectProofTracksIncrementalRoot(t *testing.T) { + store := NewMemory() + owner := types.AccountIDFromPublicKey([]byte("owner")) + token := types.TokenID(types.HashBytes("token", []byte("zph"))) + txid := types.HashBytes("tx", []byte("seed")) + id := types.ObjectIDFromTransaction(txid, 0) + out, err := object.NewCoinOutput(owner, token, 10) + if err != nil { + t.Fatal(err) + } + o := object.Object{ID: id, Version: 1, Owner: out.Owner, Kind: out.Kind, Data: out.Data} + root, err := store.Apply(nil, []object.Object{o}) + if err != nil { + t.Fatal(err) + } + got, proof, ok := store.Proof(id) + if !ok { + t.Fatal("object missing") + } + h := got.Hash() + if !state.Verify(root, types.Hash(id), h[:], proof) { + t.Fatal("object proof failed") + } +} From 2896126cc46116e62b8bcfc9300b74cf2bab552b Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:39:29 +0200 Subject: [PATCH 002/274] add deterministic parallel v2 batch execution --- internal/v2/execution/parallel.go | 116 ++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 internal/v2/execution/parallel.go diff --git a/internal/v2/execution/parallel.go b/internal/v2/execution/parallel.go new file mode 100644 index 00000000..cc85fd14 --- /dev/null +++ b/internal/v2/execution/parallel.go @@ -0,0 +1,116 @@ +package execution + +import ( + "errors" + "runtime" + "sync" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +var ( + ErrBatchConflict = errors.New("v2 batch contains conflicting transactions") + ErrBatchStateRoot = errors.New("v2 batch does not target the current state root") +) + +// BatchExecutor executes proof-carrying transactions concurrently only when +// their consumed object sets are disjoint. All transactions in one batch are +// anchored to the same pre-state root, so execution can be parallel and the +// resulting object delta can be committed atomically in deterministic order. +type BatchExecutor struct { + Engine Engine + Workers int +} + +func (b BatchExecutor) ExecuteBatch(transactions []tx.Transaction) ([]Result, error) { + if len(transactions) == 0 { + return nil, nil + } + if err := validateIndependentBatch(transactions); err != nil { + return nil, err + } + workers := b.Workers + if workers <= 0 { + workers = runtime.GOMAXPROCS(0) + } + if workers > len(transactions) { + workers = len(transactions) + } + if workers < 1 { + workers = 1 + } + + results := make([]Result, len(transactions)) + errs := make([]error, len(transactions)) + jobs := make(chan int) + var wg sync.WaitGroup + wg.Add(workers) + for i := 0; i < workers; i++ { + go func() { + defer wg.Done() + for index := range jobs { + results[index], errs[index] = b.Engine.Execute(transactions[index]) + } + }() + } + for i := range transactions { + jobs <- i + } + close(jobs) + wg.Wait() + for i := range errs { + if errs[i] != nil { + return nil, errs[i] + } + } + return results, nil +} + +func (b BatchExecutor) ApplyBatch(store worldstate.Backend, transactions []tx.Transaction) (types.Hash, []Result, error) { + if len(transactions) == 0 { + return store.Root(), nil, nil + } + if store.Root() != transactions[0].StateRoot { + return store.Root(), nil, ErrBatchStateRoot + } + results, err := b.ExecuteBatch(transactions) + if err != nil { + return store.Root(), nil, err + } + consumed := make([]types.ObjectID, 0) + created := make([]object.Object, 0) + for _, result := range results { + consumed = append(consumed, result.Consumed...) + created = append(created, result.Created...) + } + root, err := store.Apply(consumed, created) + if err != nil { + return store.Root(), nil, err + } + return root, results, nil +} + +func validateIndependentBatch(transactions []tx.Transaction) error { + root := transactions[0].StateRoot + seenInputs := make(map[types.ObjectID]struct{}) + seenTransactions := make(map[types.Hash]struct{}) + for _, transaction := range transactions { + if transaction.StateRoot != root { + return ErrBatchStateRoot + } + id := transaction.ID() + if _, duplicate := seenTransactions[id]; duplicate { + return ErrBatchConflict + } + seenTransactions[id] = struct{}{} + for _, input := range transaction.Inputs { + if _, conflict := seenInputs[input.ObjectID]; conflict { + return ErrBatchConflict + } + seenInputs[input.ObjectID] = struct{}{} + } + } + return nil +} From dac704cba350f18faecedcf0afdcf8611f6ab958 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:39:51 +0200 Subject: [PATCH 003/274] fix parallel executor object import --- internal/v2/execution/parallel.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/v2/execution/parallel.go b/internal/v2/execution/parallel.go index cc85fd14..4c5591e2 100644 --- a/internal/v2/execution/parallel.go +++ b/internal/v2/execution/parallel.go @@ -5,6 +5,7 @@ import ( "runtime" "sync" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" From a0d301576f6e6037d2c0a7f57de8b09017028bfa Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:40:12 +0200 Subject: [PATCH 004/274] test deterministic parallel proof-carrying batches --- internal/v2/execution/parallel_test.go | 76 ++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 internal/v2/execution/parallel_test.go diff --git a/internal/v2/execution/parallel_test.go b/internal/v2/execution/parallel_test.go new file mode 100644 index 00000000..3143d6f6 --- /dev/null +++ b/internal/v2/execution/parallel_test.go @@ -0,0 +1,76 @@ +package execution + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestParallelBatchAppliesIndependentTransfers(t *testing.T) { + aliceKey := makeKey(t) + carolKey := makeKey(t) + alicePub := elliptic.Marshal(elliptic.P256(), aliceKey.PublicKey.X, aliceKey.PublicKey.Y) + carolPub := elliptic.Marshal(elliptic.P256(), carolKey.PublicKey.X, carolKey.PublicKey.Y) + alice := types.AccountIDFromPublicKey(alicePub) + carol := types.AccountIDFromPublicKey(carolPub) + bob := types.AccountIDFromPublicKey([]byte("bob-parallel")) + dave := types.AccountIDFromPublicKey([]byte("dave-parallel")) + network := types.NetworkID(types.HashBytes("network", []byte("parallel"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + + store := worldstate.NewMemory() + coinAID := types.ObjectIDFromTransaction(types.HashBytes("seed", []byte("a")), 0) + coinCID := types.ObjectIDFromTransaction(types.HashBytes("seed", []byte("c")), 0) + coinAOut, _ := object.NewCoinOutput(alice, native, 100) + coinCOut, _ := object.NewCoinOutput(carol, native, 200) + coinA := object.Object{ID: coinAID, Version: 1, Owner: alice, Kind: coinAOut.Kind, Data: coinAOut.Data} + coinC := object.Object{ID: coinCID, Version: 1, Owner: carol, Kind: coinCOut.Kind, Data: coinCOut.Data} + root, err := store.Apply(nil, []object.Object{coinA, coinC}) + if err != nil { + t.Fatal(err) + } + + makeTransfer := func(key *ecdsa.PrivateKey, input object.Object, recipient types.AccountID, amount, change uint64, salt byte) tx.Transaction { + proofObject, proof, ok := store.Proof(input.ID) + if !ok { + t.Fatal("missing input proof") + } + toRecipient, _ := object.NewCoinOutput(recipient, native, amount) + toChange, _ := object.NewCoinOutput(input.Owner, native, change) + h := proofObject.Hash() + transaction := tx.Transaction{ + Version: tx.Version, Network: network, ShardID: 0, StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: input.ID, Version: input.Version, ObjectHash: h}}, + Outputs: []object.OutputSpec{toRecipient, toChange}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, + Fee: 1, Witnesses: []tx.Witness{{Object: proofObject, Proof: proof}}, + } + transaction.Salt[0] = salt + if err := transaction.Sign(key); err != nil { + t.Fatal(err) + } + return transaction + } + + txA := makeTransfer(aliceKey, coinA, bob, 25, 74, 1) + txC := makeTransfer(carolKey, coinC, dave, 50, 149, 2) + newRoot, results, err := (BatchExecutor{Engine: Engine{Network: network, NativeToken: native, ShardCount: 1}, Workers: 2}).ApplyBatch(store, []tx.Transaction{txA, txC}) + if err != nil { + t.Fatal(err) + } + if len(results) != 2 || newRoot == root { + t.Fatalf("unexpected parallel result count/root") + } +} + +func TestParallelBatchRejectsSharedInput(t *testing.T) { + root := types.HashBytes("root", []byte("same")) + id := types.ObjectID(types.HashBytes("object", []byte("shared"))) + a := tx.Transaction{StateRoot: root, Inputs: []tx.InputRef{{ObjectID: id}}} + b := tx.Transaction{StateRoot: root, Inputs: []tx.InputRef{{ObjectID: id}}} + if err := validateIndependentBatch([]tx.Transaction{a, b}); err != ErrBatchConflict { + t.Fatalf("expected batch conflict, got %v", err) + } +} From 2126a3307ccfc45486018d428e27f4079308a4ea Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:40:31 +0200 Subject: [PATCH 005/274] fix parallel batch test imports --- internal/v2/execution/parallel_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/v2/execution/parallel_test.go b/internal/v2/execution/parallel_test.go index 3143d6f6..a05a2738 100644 --- a/internal/v2/execution/parallel_test.go +++ b/internal/v2/execution/parallel_test.go @@ -1,6 +1,8 @@ package execution import ( + "crypto/ecdsa" + "crypto/elliptic" "testing" "github.com/zephyr-chain/zephyr-chain/internal/v2/object" From 832be779eabec23d6be360e57176875b2ba0959d Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:41:16 +0200 Subject: [PATCH 006/274] add journaled checkpointed v2 world-state backend --- internal/v2/worldstate/durable.go | 482 ++++++++++++++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 internal/v2/worldstate/durable.go diff --git a/internal/v2/worldstate/durable.go b/internal/v2/worldstate/durable.go new file mode 100644 index 00000000..0582aa44 --- /dev/null +++ b/internal/v2/worldstate/durable.go @@ -0,0 +1,482 @@ +package worldstate + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "hash/crc32" + "io" + "os" + "path/filepath" + "sort" + "sync" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/state" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + walMagic = "ZWL2" + checkpointMagic = "ZCP2" + persistenceVersion = uint16(1) + maxWALRecordBytes = 64 << 20 + maxCheckpointBytes = 1 << 30 + maxCheckpointObjects = 10_000_000 +) + +var ( + ErrPersistenceNetwork = errors.New("v2 persisted state belongs to another network") + ErrPersistenceCorrupt = errors.New("v2 persisted state is corrupt") + ErrPersistenceClosed = errors.New("v2 persisted state is closed") +) + +type Disk struct { + mu sync.Mutex + dir string + network types.NetworkID + mem *Memory + objects map[types.ObjectID]object.Object + seq uint64 + wal *os.File + closed bool +} + +func OpenDisk(dir string, network types.NetworkID) (*Disk, error) { + if types.IsZero32([32]byte(network)) { + return nil, ErrPersistenceNetwork + } + if err := os.MkdirAll(dir, 0o700); err != nil { + return nil, err + } + d := &Disk{ + dir: dir, network: network, mem: NewMemory(), + objects: make(map[types.ObjectID]object.Object), + } + if err := d.loadCheckpoint(); err != nil { + return nil, err + } + wal, err := os.OpenFile(filepath.Join(dir, "state.wal"), os.O_CREATE|os.O_RDWR, 0o600) + if err != nil { + return nil, err + } + d.wal = wal + if err := d.replayWAL(); err != nil { + _ = wal.Close() + return nil, err + } + if _, err := wal.Seek(0, io.SeekEnd); err != nil { + _ = wal.Close() + return nil, err + } + return d, nil +} + +func (d *Disk) Root() types.Hash { return d.mem.Root() } + +func (d *Disk) GetObject(id types.ObjectID) (object.Object, bool) { return d.mem.GetObject(id) } + +func (d *Disk) Proof(id types.ObjectID) (object.Object, state.Proof, bool) { return d.mem.Proof(id) } + +func (d *Disk) Apply(consumed []types.ObjectID, created []object.Object) (types.Hash, error) { + d.mu.Lock() + defer d.mu.Unlock() + if d.closed { + return d.mem.Root(), ErrPersistenceClosed + } + if err := d.prevalidate(consumed, created); err != nil { + return d.mem.Root(), err + } + next := d.seq + 1 + payload := encodeWALPayload(d.network, next, consumed, created) + if err := d.appendWAL(payload); err != nil { + return d.mem.Root(), err + } + root, err := d.mem.Apply(consumed, created) + if err != nil { + return d.mem.Root(), ErrPersistenceCorrupt + } + d.applyMirror(consumed, created) + d.seq = next + return root, nil +} + +func (d *Disk) Sequence() uint64 { + d.mu.Lock() + defer d.mu.Unlock() + return d.seq +} + +// Checkpoint atomically materializes the current object set and then resets the +// WAL. A crash before WAL truncation is safe because replay ignores records at +// or below the checkpoint sequence. +func (d *Disk) Checkpoint() error { + d.mu.Lock() + defer d.mu.Unlock() + if d.closed { + return ErrPersistenceClosed + } + objects := make([]object.Object, 0, len(d.objects)) + for _, item := range d.objects { + copyItem := item + copyItem.Data = append([]byte(nil), item.Data...) + objects = append(objects, copyItem) + } + sort.Slice(objects, func(i, j int) bool { return bytes.Compare(objects[i].ID[:], objects[j].ID[:]) < 0 }) + + var payload codec.Writer + payload.U16(persistenceVersion) + payload.Fixed(d.network[:]) + payload.U64(d.seq) + payload.U32(uint32(len(objects))) + for _, item := range objects { + payload.Bytes(item.CanonicalBytes()) + } + root := d.mem.Root() + payload.Fixed(root[:]) + rawPayload := payload.BytesCopy() + digest := codec.DomainHash("zephyr/state-checkpoint/v2", rawPayload) + var file bytes.Buffer + file.WriteString(checkpointMagic) + var n [4]byte + binary.BigEndian.PutUint32(n[:], uint32(len(rawPayload))) + file.Write(n[:]) + file.Write(rawPayload) + file.Write(digest[:]) + + tmp := filepath.Join(d.dir, "state.checkpoint.tmp") + final := filepath.Join(d.dir, "state.checkpoint") + f, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) + if err != nil { + return err + } + if _, err = f.Write(file.Bytes()); err == nil { + err = f.Sync() + } + closeErr := f.Close() + if err == nil { + err = closeErr + } + if err != nil { + _ = os.Remove(tmp) + return err + } + if err := os.Rename(tmp, final); err != nil { + _ = os.Remove(tmp) + return err + } + if err := syncDir(d.dir); err != nil { + return err + } + if err := d.wal.Truncate(0); err != nil { + return err + } + if _, err := d.wal.Seek(0, io.SeekStart); err != nil { + return err + } + return d.wal.Sync() +} + +func (d *Disk) Close() error { + d.mu.Lock() + defer d.mu.Unlock() + if d.closed { + return nil + } + d.closed = true + if err := d.wal.Sync(); err != nil { + _ = d.wal.Close() + return err + } + return d.wal.Close() +} + +func (d *Disk) prevalidate(consumed []types.ObjectID, created []object.Object) error { + seenConsumed := make(map[types.ObjectID]struct{}, len(consumed)) + for _, id := range consumed { + if _, duplicate := seenConsumed[id]; duplicate { + return ErrObjectNotFound + } + seenConsumed[id] = struct{}{} + if _, ok := d.objects[id]; !ok { + return ErrObjectNotFound + } + } + seenCreated := make(map[types.ObjectID]struct{}, len(created)) + for _, item := range created { + if err := item.Validate(); err != nil { + return err + } + if _, duplicate := seenCreated[item.ID]; duplicate { + return ErrObjectExists + } + seenCreated[item.ID] = struct{}{} + if _, exists := d.objects[item.ID]; exists { + if _, replacing := seenConsumed[item.ID]; !replacing { + return ErrObjectExists + } + } + } + return nil +} + +func (d *Disk) appendWAL(payload []byte) error { + if len(payload) == 0 || len(payload) > maxWALRecordBytes { + return ErrPersistenceCorrupt + } + var header [8]byte + copy(header[:4], walMagic) + binary.BigEndian.PutUint32(header[4:], uint32(len(payload))) + checksum := crc32.Checksum(payload, crc32.MakeTable(crc32.Castagnoli)) + var sum [4]byte + binary.BigEndian.PutUint32(sum[:], checksum) + if _, err := d.wal.Write(header[:]); err != nil { + return err + } + if _, err := d.wal.Write(payload); err != nil { + return err + } + if _, err := d.wal.Write(sum[:]); err != nil { + return err + } + return d.wal.Sync() +} + +func encodeWALPayload(network types.NetworkID, seq uint64, consumed []types.ObjectID, created []object.Object) []byte { + var w codec.Writer + w.U16(persistenceVersion) + w.Fixed(network[:]) + w.U64(seq) + w.U32(uint32(len(consumed))) + for _, id := range consumed { + w.Fixed(id[:]) + } + w.U32(uint32(len(created))) + for _, item := range created { + w.Bytes(item.CanonicalBytes()) + } + return w.BytesCopy() +} + +func parseWALPayload(payload []byte) (types.NetworkID, uint64, []types.ObjectID, []object.Object, error) { + r := codec.NewReader(payload) + version, err := r.U16() + if err != nil || version != persistenceVersion { + return types.NetworkID{}, 0, nil, nil, ErrPersistenceCorrupt + } + networkBytes, err := r.Fixed(32) + if err != nil { + return types.NetworkID{}, 0, nil, nil, ErrPersistenceCorrupt + } + var network types.NetworkID + copy(network[:], networkBytes) + seq, err := r.U64() + if err != nil || seq == 0 { + return types.NetworkID{}, 0, nil, nil, ErrPersistenceCorrupt + } + consumedCount, err := r.U32() + if err != nil || consumedCount > 1_000_000 { + return types.NetworkID{}, 0, nil, nil, ErrPersistenceCorrupt + } + consumed := make([]types.ObjectID, int(consumedCount)) + for i := range consumed { + raw, err := r.Fixed(32) + if err != nil { + return types.NetworkID{}, 0, nil, nil, ErrPersistenceCorrupt + } + copy(consumed[i][:], raw) + } + createdCount, err := r.U32() + if err != nil || createdCount > 1_000_000 { + return types.NetworkID{}, 0, nil, nil, ErrPersistenceCorrupt + } + created := make([]object.Object, int(createdCount)) + for i := range created { + raw, err := r.Bytes(object.MaxObjectDataBytes + 128) + if err != nil { + return types.NetworkID{}, 0, nil, nil, ErrPersistenceCorrupt + } + created[i], err = object.ParseObject(raw) + if err != nil { + return types.NetworkID{}, 0, nil, nil, ErrPersistenceCorrupt + } + } + if err := r.Done(); err != nil { + return types.NetworkID{}, 0, nil, nil, ErrPersistenceCorrupt + } + return network, seq, consumed, created, nil +} + +func (d *Disk) replayWAL() error { + if _, err := d.wal.Seek(0, io.SeekStart); err != nil { + return err + } + reader := bufio.NewReader(d.wal) + var offset int64 + for { + var header [8]byte + n, err := io.ReadFull(reader, header[:]) + if err == io.EOF { + break + } + if err == io.ErrUnexpectedEOF { + if err := d.wal.Truncate(offset); err != nil { + return err + } + break + } + if err != nil || n != len(header) || string(header[:4]) != walMagic { + return ErrPersistenceCorrupt + } + length := binary.BigEndian.Uint32(header[4:]) + if length == 0 || length > maxWALRecordBytes { + return ErrPersistenceCorrupt + } + payload := make([]byte, int(length)) + if _, err := io.ReadFull(reader, payload); err != nil { + if err == io.EOF || err == io.ErrUnexpectedEOF { + if err := d.wal.Truncate(offset); err != nil { + return err + } + break + } + return err + } + var checksumRaw [4]byte + if _, err := io.ReadFull(reader, checksumRaw[:]); err != nil { + if err == io.EOF || err == io.ErrUnexpectedEOF { + if err := d.wal.Truncate(offset); err != nil { + return err + } + break + } + return err + } + expected := binary.BigEndian.Uint32(checksumRaw[:]) + actual := crc32.Checksum(payload, crc32.MakeTable(crc32.Castagnoli)) + if expected != actual { + return ErrPersistenceCorrupt + } + network, seq, consumed, created, err := parseWALPayload(payload) + if err != nil { + return err + } + if network != d.network { + return ErrPersistenceNetwork + } + offset += int64(8 + len(payload) + 4) + if seq <= d.seq { + continue + } + if seq != d.seq+1 { + return ErrPersistenceCorrupt + } + if err := d.prevalidate(consumed, created); err != nil { + return ErrPersistenceCorrupt + } + if _, err := d.mem.Apply(consumed, created); err != nil { + return ErrPersistenceCorrupt + } + d.applyMirror(consumed, created) + d.seq = seq + } + _, err := d.wal.Seek(0, io.SeekEnd) + return err +} + +func (d *Disk) loadCheckpoint() error { + path := filepath.Join(d.dir, "state.checkpoint") + raw, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return nil + } + if err != nil { + return err + } + if len(raw) < 4+4+32 || string(raw[:4]) != checkpointMagic { + return ErrPersistenceCorrupt + } + length := binary.BigEndian.Uint32(raw[4:8]) + if length == 0 || length > maxCheckpointBytes || len(raw) != 8+int(length)+32 { + return ErrPersistenceCorrupt + } + payload := raw[8 : 8+int(length)] + digest := codec.DomainHash("zephyr/state-checkpoint/v2", payload) + if !bytes.Equal(digest[:], raw[8+int(length):]) { + return ErrPersistenceCorrupt + } + r := codec.NewReader(payload) + version, err := r.U16() + if err != nil || version != persistenceVersion { + return ErrPersistenceCorrupt + } + networkBytes, err := r.Fixed(32) + if err != nil { + return ErrPersistenceCorrupt + } + var network types.NetworkID + copy(network[:], networkBytes) + if network != d.network { + return ErrPersistenceNetwork + } + seq, err := r.U64() + if err != nil { + return ErrPersistenceCorrupt + } + count, err := r.U32() + if err != nil || count > maxCheckpointObjects { + return ErrPersistenceCorrupt + } + objects := make([]object.Object, int(count)) + for i := range objects { + itemBytes, err := r.Bytes(object.MaxObjectDataBytes + 128) + if err != nil { + return ErrPersistenceCorrupt + } + objects[i], err = object.ParseObject(itemBytes) + if err != nil { + return ErrPersistenceCorrupt + } + } + rootBytes, err := r.Fixed(32) + if err != nil || r.Done() != nil { + return ErrPersistenceCorrupt + } + var expectedRoot types.Hash + copy(expectedRoot[:], rootBytes) + if len(objects) > 0 { + if _, err := d.mem.Apply(nil, objects); err != nil { + return ErrPersistenceCorrupt + } + } + if d.mem.Root() != expectedRoot { + return ErrPersistenceCorrupt + } + for _, item := range objects { + d.objects[item.ID] = item + } + d.seq = seq + return nil +} + +func (d *Disk) applyMirror(consumed []types.ObjectID, created []object.Object) { + for _, id := range consumed { + delete(d.objects, id) + } + for _, item := range created { + copyItem := item + copyItem.Data = append([]byte(nil), item.Data...) + d.objects[item.ID] = copyItem + } +} + +func syncDir(dir string) error { + f, err := os.Open(dir) + if err != nil { + return err + } + defer f.Close() + return f.Sync() +} From 66cb13380c23a4c0045fc884228f85d14d5f729c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:41:30 +0200 Subject: [PATCH 007/274] test crash-safe v2 state WAL and checkpoint recovery --- internal/v2/worldstate/durable_test.go | 116 +++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 internal/v2/worldstate/durable_test.go diff --git a/internal/v2/worldstate/durable_test.go b/internal/v2/worldstate/durable_test.go new file mode 100644 index 00000000..28706ad2 --- /dev/null +++ b/internal/v2/worldstate/durable_test.go @@ -0,0 +1,116 @@ +package worldstate + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestDiskPersistsWALAndCheckpoint(t *testing.T) { + dir := t.TempDir() + network := types.NetworkID(types.HashBytes("network", []byte("disk-test"))) + owner := types.AccountIDFromPublicKey([]byte("owner")) + token := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + id := types.ObjectIDFromTransaction(types.HashBytes("seed", []byte("coin")), 0) + out, err := object.NewCoinOutput(owner, token, 100) + if err != nil { + t.Fatal(err) + } + coin := object.Object{ID: id, Version: 1, Owner: owner, Kind: out.Kind, Data: out.Data} + + store, err := OpenDisk(dir, network) + if err != nil { + t.Fatal(err) + } + root, err := store.Apply(nil, []object.Object{coin}) + if err != nil { + t.Fatal(err) + } + if store.Sequence() != 1 { + t.Fatalf("expected sequence 1") + } + if err := store.Close(); err != nil { + t.Fatal(err) + } + + reopened, err := OpenDisk(dir, network) + if err != nil { + t.Fatal(err) + } + if reopened.Root() != root { + t.Fatal("WAL replay changed root") + } + if _, ok := reopened.GetObject(id); !ok { + t.Fatal("WAL replay lost object") + } + if err := reopened.Checkpoint(); err != nil { + t.Fatal(err) + } + if err := reopened.Close(); err != nil { + t.Fatal(err) + } + + afterCheckpoint, err := OpenDisk(dir, network) + if err != nil { + t.Fatal(err) + } + if afterCheckpoint.Root() != root || afterCheckpoint.Sequence() != 1 { + t.Fatal("checkpoint restore changed state") + } + if err := afterCheckpoint.Close(); err != nil { + t.Fatal(err) + } +} + +func TestDiskRejectsWrongNetworkAndIgnoresTornTail(t *testing.T) { + dir := t.TempDir() + network := types.NetworkID(types.HashBytes("network", []byte("a"))) + store, err := OpenDisk(dir, network) + if err != nil { + t.Fatal(err) + } + if err := store.Checkpoint(); err != nil { + t.Fatal(err) + } + if err := store.Close(); err != nil { + t.Fatal(err) + } + + wrong := types.NetworkID(types.HashBytes("network", []byte("b"))) + if _, err := OpenDisk(dir, wrong); !errors.Is(err, ErrPersistenceNetwork) { + t.Fatalf("expected wrong-network rejection, got %v", err) + } + + walPath := filepath.Join(dir, "state.wal") + f, err := os.OpenFile(walPath, os.O_WRONLY|os.O_APPEND, 0o600) + if err != nil { + t.Fatal(err) + } + if _, err := f.Write([]byte("ZWL2\x00")); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + recovered, err := OpenDisk(dir, network) + if err != nil { + t.Fatal(err) + } + if recovered.Sequence() != 0 { + t.Fatal("torn tail advanced sequence") + } + if err := recovered.Close(); err != nil { + t.Fatal(err) + } + info, err := os.Stat(walPath) + if err != nil { + t.Fatal(err) + } + if info.Size() != 0 { + t.Fatalf("torn WAL tail was not truncated: %d", info.Size()) + } +} From 5c62df9d3ff8e7a40696911b81353addf503e8dc Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:41:47 +0200 Subject: [PATCH 008/274] add finalized cross-shard receipt proofs and anti-replay --- internal/v2/sharding/receipts.go | 127 +++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 internal/v2/sharding/receipts.go diff --git a/internal/v2/sharding/receipts.go b/internal/v2/sharding/receipts.go new file mode 100644 index 00000000..a32d3ed5 --- /dev/null +++ b/internal/v2/sharding/receipts.go @@ -0,0 +1,127 @@ +package sharding + +import ( + "errors" + "sort" + "sync" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ( + ErrReceiptProof = errors.New("invalid cross-shard receipt proof") + ErrReceiptReplay = errors.New("cross-shard receipt already consumed") +) + +type ReceiptBatch struct { + Receipts []CrossShardReceipt +} + +func (b ReceiptBatch) Root() (types.Hash, error) { + hashes, _, err := b.sortedHashes() + if err != nil { + return types.Hash{}, err + } + return merkle.Root(hashes), nil +} + +func (b ReceiptBatch) Proof(receipt CrossShardReceipt) (merkle.Proof, error) { + target, err := receipt.Hash() + if err != nil { + return merkle.Proof{}, err + } + hashes, _, err := b.sortedHashes() + if err != nil { + return merkle.Proof{}, err + } + for i, hash := range hashes { + if hash == target { + return merkle.BuildProof(hashes, i) + } + } + return merkle.Proof{}, ErrReceiptProof +} + +func (b ReceiptBatch) sortedHashes() ([]types.Hash, []CrossShardReceipt, error) { + items := append([]CrossShardReceipt(nil), b.Receipts...) + for _, receipt := range items { + if err := receipt.Validate(); err != nil { + return nil, nil, err + } + } + sort.Slice(items, func(i, j int) bool { + a, _ := items[i].Hash() + b, _ := items[j].Hash() + return a.String() < b.String() + }) + hashes := make([]types.Hash, len(items)) + for i, receipt := range items { + hashes[i], _ = receipt.Hash() + if i > 0 && hashes[i] == hashes[i-1] { + return nil, nil, ErrReceiptReplay + } + } + return hashes, items, nil +} + +// VerifyFinalizedReceipt proves both that the source shard commitment belongs +// to a finalized global header and that the receipt belongs to that shard's +// receipt root. A destination shard therefore never trusts the source shard by +// assertion alone. +func VerifyFinalizedReceipt(header GlobalHeader, commitment Commitment, commitmentProof merkle.Proof, receipt CrossShardReceipt, receiptProof merkle.Proof) error { + if err := receipt.Validate(); err != nil { + return err + } + if commitment.ShardID != receipt.SourceShard || header.Height != receipt.SourceHeight { + return ErrReceiptProof + } + if !merkle.Verify(header.ShardCommitmentRoot, commitment.Hash(), commitmentProof) { + return ErrReceiptProof + } + receiptHash, err := receipt.Hash() + if err != nil { + return err + } + if !merkle.Verify(commitment.ReceiptRoot, receiptHash, receiptProof) { + return ErrReceiptProof + } + return nil +} + +type ReceiptTracker struct { + mu sync.Mutex + consumed map[types.Hash]uint64 +} + +func NewReceiptTracker() *ReceiptTracker { + return &ReceiptTracker{consumed: make(map[types.Hash]uint64)} +} + +func (t *ReceiptTracker) Consume(destinationShard uint32, header GlobalHeader, commitment Commitment, commitmentProof merkle.Proof, receipt CrossShardReceipt, receiptProof merkle.Proof) error { + if receipt.DestinationShard != destinationShard { + return ErrReceiptProof + } + if err := VerifyFinalizedReceipt(header, commitment, commitmentProof, receipt, receiptProof); err != nil { + return err + } + hash, _ := receipt.Hash() + t.mu.Lock() + defer t.mu.Unlock() + if _, exists := t.consumed[hash]; exists { + return ErrReceiptReplay + } + t.consumed[hash] = header.Height + return nil +} + +func (t *ReceiptTracker) Consumed(receipt CrossShardReceipt) bool { + hash, err := receipt.Hash() + if err != nil { + return false + } + t.mu.Lock() + defer t.mu.Unlock() + _, ok := t.consumed[hash] + return ok +} From 381a65fdfaece158a3f35da8e71ab100d1309d90 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:41:59 +0200 Subject: [PATCH 009/274] test finalized cross-shard receipt consumption --- internal/v2/sharding/receipts_test.go | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 internal/v2/sharding/receipts_test.go diff --git a/internal/v2/sharding/receipts_test.go b/internal/v2/sharding/receipts_test.go new file mode 100644 index 00000000..e55d53a6 --- /dev/null +++ b/internal/v2/sharding/receipts_test.go @@ -0,0 +1,54 @@ +package sharding + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestFinalizedCrossShardReceiptAndReplayProtection(t *testing.T) { + owner := types.AccountIDFromPublicKey([]byte("receipt-owner")) + token := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + output, err := object.NewCoinOutput(owner, token, 25) + if err != nil { + t.Fatal(err) + } + receipt := CrossShardReceipt{ + SourceShard: 0, DestinationShard: 1, SourceHeight: 7, + TransactionID: types.HashBytes("tx", []byte("cross-shard")), OutputIndex: 0, + Output: output, SourceStateRoot: types.HashBytes("state", []byte("source")), + } + batch := ReceiptBatch{Receipts: []CrossShardReceipt{receipt}} + receiptRoot, err := batch.Root() + if err != nil { + t.Fatal(err) + } + receiptProof, err := batch.Proof(receipt) + if err != nil { + t.Fatal(err) + } + commitments := []Commitment{ + {ShardID: 0, StateRoot: receipt.SourceStateRoot, ReceiptRoot: receiptRoot, DataRoot: types.HashBytes("data", []byte("0"))}, + {ShardID: 1, StateRoot: types.HashBytes("state", []byte("dest")), ReceiptRoot: types.HashBytes("receipt", []byte("empty")), DataRoot: types.HashBytes("data", []byte("1"))}, + } + commitmentRoot, err := CommitmentRoot(commitments) + if err != nil { + t.Fatal(err) + } + commitment, commitmentProof, err := CommitmentProof(commitments, 0) + if err != nil { + t.Fatal(err) + } + header := GlobalHeader{Version: 2, Network: types.NetworkID(types.HashBytes("network", []byte("v2"))), Height: 7, ShardCommitmentRoot: commitmentRoot} + tracker := NewReceiptTracker() + if err := tracker.Consume(1, header, commitment, commitmentProof, receipt, receiptProof); err != nil { + t.Fatal(err) + } + if !tracker.Consumed(receipt) { + t.Fatal("receipt was not recorded as consumed") + } + if err := tracker.Consume(1, header, commitment, commitmentProof, receipt, receiptProof); err != ErrReceiptReplay { + t.Fatalf("expected replay rejection, got %v", err) + } +} From 55f959c51df56d25cf3a255f46fe041774ab99ce Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:42:40 +0200 Subject: [PATCH 010/274] implement native v2 compute market state machine --- internal/v2/compute/market.go | 345 ++++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 internal/v2/compute/market.go diff --git a/internal/v2/compute/market.go b/internal/v2/compute/market.go new file mode 100644 index 00000000..7364cf22 --- /dev/null +++ b/internal/v2/compute/market.go @@ -0,0 +1,345 @@ +package compute + +import ( + "errors" + "math" + "sort" + "sync" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ( + ErrMarketNotFound = errors.New("compute market record not found") + ErrMarketState = errors.New("invalid compute market state transition") + ErrMarketMatch = errors.New("compute offer does not satisfy job") + ErrMarketEscrow = errors.New("compute job escrow is insufficient") + ErrMarketVerification = errors.New("compute result verification requirements not met") + ErrMarketDuplicate = errors.New("duplicate compute market record") +) + +type JobStatus uint8 + +const ( + JobPending JobStatus = iota + 1 + JobAssigned + JobAwaitingVerification + JobSettled + JobExpired +) + +type OfferRecord struct { + ID types.Hash + Offer Offer +} + +type Assignment struct { + OfferID types.Hash + Provider types.AccountID + Price uint64 +} + +type JobRecord struct { + ID types.JobID + Job Job + Escrow uint64 + Status JobStatus + Assignments []Assignment + Results map[types.AccountID]Result +} + +type VerificationEvidence struct { + DeterministicReplay bool + ProofVerified bool + AttestationVerified bool + ChallengePassed bool + ClientApproved bool +} + +type Settlement struct { + JobID types.JobID + ResultRoot types.Hash + Payments map[types.AccountID]uint64 + Refund uint64 +} + +type Market struct { + mu sync.Mutex + offers map[types.Hash]OfferRecord + jobs map[types.JobID]*JobRecord +} + +func NewMarket() *Market { + return &Market{offers: make(map[types.Hash]OfferRecord), jobs: make(map[types.JobID]*JobRecord)} +} + +func OfferID(offer Offer) (types.Hash, error) { + raw, err := offer.MarshalBinary() + if err != nil { + return types.Hash{}, err + } + return types.Hash(codec.DomainHash("zephyr/compute-offer/v2", raw)), nil +} + +func ComputeJobID(job Job) (types.JobID, error) { + raw, err := job.MarshalBinary() + if err != nil { + return types.JobID{}, err + } + return types.JobID(codec.DomainHash("zephyr/compute-job/v2", raw)), nil +} + +func (m *Market) PublishOffer(offer Offer, height uint64) (types.Hash, error) { + if err := offer.Validate(); err != nil || height == 0 || offer.ValidUntilHeight < height { + return types.Hash{}, ErrInvalidOffer + } + id, _ := OfferID(offer) + m.mu.Lock() + defer m.mu.Unlock() + if _, exists := m.offers[id]; exists { + return types.Hash{}, ErrMarketDuplicate + } + m.offers[id] = OfferRecord{ID: id, Offer: offer} + return id, nil +} + +func (m *Market) PostJob(job Job, escrow, height uint64) (types.JobID, error) { + if err := job.Validate(); err != nil || height == 0 || job.DeadlineHeight < height { + return types.JobID{}, ErrInvalidJob + } + if escrow < job.MaxPrice { + return types.JobID{}, ErrMarketEscrow + } + id, _ := ComputeJobID(job) + m.mu.Lock() + defer m.mu.Unlock() + if _, exists := m.jobs[id]; exists { + return types.JobID{}, ErrMarketDuplicate + } + m.jobs[id] = &JobRecord{ID: id, Job: job, Escrow: escrow, Status: JobPending, Results: make(map[types.AccountID]Result)} + return id, nil +} + +func (m *Market) Assign(jobID types.JobID, offerID types.Hash, height uint64) (Assignment, error) { + m.mu.Lock() + defer m.mu.Unlock() + job, ok := m.jobs[jobID] + if !ok { + return Assignment{}, ErrMarketNotFound + } + offerRecord, ok := m.offers[offerID] + if !ok { + return Assignment{}, ErrMarketNotFound + } + if job.Status == JobSettled || job.Status == JobExpired || height == 0 || height > job.Job.DeadlineHeight || height > offerRecord.Offer.ValidUntilHeight { + return Assignment{}, ErrMarketState + } + if !offerMatchesJob(offerRecord.Offer, job.Job) { + return Assignment{}, ErrMarketMatch + } + for _, existing := range job.Assignments { + if existing.Provider == offerRecord.Offer.Provider { + return Assignment{}, ErrMarketDuplicate + } + } + target := requiredAssignments(job.Job) + if len(job.Assignments) >= target { + return Assignment{}, ErrMarketState + } + var committed uint64 + for _, existing := range job.Assignments { + if math.MaxUint64-committed < existing.Price { + return Assignment{}, ErrMarketEscrow + } + committed += existing.Price + } + price := offerRecord.Offer.PricePerUnit + if price > job.Job.MaxPrice || math.MaxUint64-committed < price || committed+price > job.Job.MaxPrice || committed+price > job.Escrow { + return Assignment{}, ErrMarketEscrow + } + assignment := Assignment{OfferID: offerID, Provider: offerRecord.Offer.Provider, Price: price} + job.Assignments = append(job.Assignments, assignment) + if len(job.Assignments) == target { + job.Status = JobAssigned + } + return assignment, nil +} + +func (m *Market) SubmitResult(result Result) error { + if err := result.Validate(); err != nil { + return err + } + m.mu.Lock() + defer m.mu.Unlock() + job, ok := m.jobs[result.JobID] + if !ok { + return ErrMarketNotFound + } + if job.Status != JobAssigned && job.Status != JobAwaitingVerification { + return ErrMarketState + } + if result.CompletedHeight > job.Job.DeadlineHeight { + return ErrMarketState + } + assigned := false + for _, assignment := range job.Assignments { + if assignment.Provider == result.Provider { + assigned = true + break + } + } + if !assigned { + return ErrMarketMatch + } + if _, duplicate := job.Results[result.Provider]; duplicate { + return ErrMarketDuplicate + } + job.Results[result.Provider] = result + if len(job.Results) == requiredAssignments(job.Job) { + job.Status = JobAwaitingVerification + } + return nil +} + +func (m *Market) Finalize(jobID types.JobID, evidence VerificationEvidence) (Settlement, error) { + m.mu.Lock() + defer m.mu.Unlock() + job, ok := m.jobs[jobID] + if !ok { + return Settlement{}, ErrMarketNotFound + } + if job.Status != JobAwaitingVerification || len(job.Results) != requiredAssignments(job.Job) { + return Settlement{}, ErrMarketState + } + root, replicatedMatch := commonResultRoot(job) + if types.IsZero32([32]byte(root)) || !verificationSatisfied(job.Job.Verification, replicatedMatch, job.Results, evidence) { + return Settlement{}, ErrMarketVerification + } + payments := make(map[types.AccountID]uint64, len(job.Assignments)) + var paid uint64 + for _, assignment := range job.Assignments { + if math.MaxUint64-paid < assignment.Price { + return Settlement{}, ErrMarketEscrow + } + paid += assignment.Price + payments[assignment.Provider] += assignment.Price + } + if paid > job.Escrow { + return Settlement{}, ErrMarketEscrow + } + job.Status = JobSettled + return Settlement{JobID: jobID, ResultRoot: root, Payments: payments, Refund: job.Escrow - paid}, nil +} + +func (m *Market) Expire(height uint64) []types.JobID { + m.mu.Lock() + defer m.mu.Unlock() + var expired []types.JobID + for id, job := range m.jobs { + if job.Status != JobSettled && job.Status != JobExpired && height > job.Job.DeadlineHeight { + job.Status = JobExpired + expired = append(expired, id) + } + } + sort.Slice(expired, func(i, j int) bool { return expired[i].String() < expired[j].String() }) + return expired +} + +func requiredAssignments(job Job) int { + if job.Verification == VerificationReplicated { + return int(job.Replicas) + } + return 1 +} + +func offerMatchesJob(offer Offer, job Job) bool { + if offer.Resources.CPUCores < job.Resources.CPUCores || offer.Resources.MemoryMiB < job.Resources.MemoryMiB || + offer.Resources.GPUCount < job.Resources.GPUCount || offer.Resources.GPUMemoryMiB < job.Resources.GPUMemoryMiB || + offer.Resources.StorageMiB < job.Resources.StorageMiB || offer.Resources.BandwidthMbps < job.Resources.BandwidthMbps || + offer.Collateral < job.CollateralRequired || !supportsMode(offer.Verification, job.Verification) { + return false + } + available := make(map[string]struct{}, len(offer.Resources.Capabilities)) + for _, capability := range offer.Resources.Capabilities { + available[capability] = struct{}{} + } + for _, capability := range job.Resources.Capabilities { + if _, ok := available[capability]; !ok { + return false + } + } + return true +} + +func supportsMode(modes []VerificationMode, target VerificationMode) bool { + for _, mode := range modes { + if mode == target || mode == VerificationHybrid { + return true + } + } + return false +} + +func commonResultRoot(job *JobRecord) (types.Hash, bool) { + var root types.Hash + first := true + match := true + for _, result := range job.Results { + if first { + root = result.ResultRoot + first = false + continue + } + if result.ResultRoot != root { + match = false + } + } + return root, match && !first +} + +func verificationSatisfied(mode VerificationMode, replicatedMatch bool, results map[types.AccountID]Result, evidence VerificationEvidence) bool { + hasProof := true + hasAttestation := true + for _, result := range results { + hasProof = hasProof && !types.IsZero32([32]byte(result.ProofHash)) + hasAttestation = hasAttestation && !types.IsZero32([32]byte(result.AttestationHash)) + } + switch mode { + case VerificationDeterministic: + return evidence.DeterministicReplay + case VerificationReplicated: + return replicatedMatch + case VerificationChallenge: + return evidence.ChallengePassed + case VerificationZeroKnowledge: + return hasProof && evidence.ProofVerified + case VerificationTEE: + return hasAttestation && evidence.AttestationVerified + case VerificationClientApproved: + return evidence.ClientApproved + case VerificationHybrid: + score := 0 + if evidence.DeterministicReplay { + score++ + } + if replicatedMatch && len(results) > 1 { + score++ + } + if hasProof && evidence.ProofVerified { + score++ + } + if hasAttestation && evidence.AttestationVerified { + score++ + } + if evidence.ChallengePassed { + score++ + } + if evidence.ClientApproved { + score++ + } + return score >= 2 + default: + return false + } +} From eeae8fb193c3ecec52633ae39f00d5c2473f9216 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:42:58 +0200 Subject: [PATCH 011/274] test compute-market matching verification and settlement --- internal/v2/compute/market_test.go | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 internal/v2/compute/market_test.go diff --git a/internal/v2/compute/market_test.go b/internal/v2/compute/market_test.go new file mode 100644 index 00000000..3ba61738 --- /dev/null +++ b/internal/v2/compute/market_test.go @@ -0,0 +1,84 @@ +package compute + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestReplicatedComputeMarketSettlement(t *testing.T) { + market := NewMarket() + providerA := types.AccountIDFromPublicKey([]byte("provider-a")) + providerB := types.AccountIDFromPublicKey([]byte("provider-b")) + owner := types.AccountIDFromPublicKey([]byte("job-owner")) + resources := Resources{CPUCores: 4, MemoryMiB: 4096, GPUCount: 1, GPUMemoryMiB: 8192, StorageMiB: 1024, BandwidthMbps: 100, Capabilities: []string{"cuda"}} + offerA := Offer{Provider: providerA, Resources: resources, PricePerUnit: 3, Collateral: 10, Verification: []VerificationMode{VerificationReplicated}, ValidUntilHeight: 100} + offerB := Offer{Provider: providerB, Resources: resources, PricePerUnit: 4, Collateral: 10, Verification: []VerificationMode{VerificationReplicated}, ValidUntilHeight: 100} + offerAID, err := market.PublishOffer(offerA, 1) + if err != nil { + t.Fatal(err) + } + offerBID, err := market.PublishOffer(offerB, 1) + if err != nil { + t.Fatal(err) + } + job := Job{ + Owner: owner, WorkloadHash: types.HashBytes("workload", []byte("render")), InputRoot: types.HashBytes("input", []byte("scene")), + Resources: resources, MaxPrice: 10, CollateralRequired: 5, Verification: VerificationReplicated, + DeadlineHeight: 50, Replicas: 2, + } + jobID, err := market.PostJob(job, 10, 2) + if err != nil { + t.Fatal(err) + } + if _, err := market.Assign(jobID, offerAID, 3); err != nil { + t.Fatal(err) + } + if _, err := market.Assign(jobID, offerBID, 3); err != nil { + t.Fatal(err) + } + root := types.HashBytes("result", []byte("same-output")) + if err := market.SubmitResult(Result{JobID: jobID, Provider: providerA, ResultRoot: root, CompletedHeight: 10}); err != nil { + t.Fatal(err) + } + if err := market.SubmitResult(Result{JobID: jobID, Provider: providerB, ResultRoot: root, CompletedHeight: 11}); err != nil { + t.Fatal(err) + } + settlement, err := market.Finalize(jobID, VerificationEvidence{}) + if err != nil { + t.Fatal(err) + } + if settlement.ResultRoot != root || settlement.Payments[providerA] != 3 || settlement.Payments[providerB] != 4 || settlement.Refund != 3 { + t.Fatalf("unexpected settlement: %+v", settlement) + } +} + +func TestComputeMarketRequiresVerificationEvidence(t *testing.T) { + market := NewMarket() + provider := types.AccountIDFromPublicKey([]byte("provider")) + owner := types.AccountIDFromPublicKey([]byte("owner")) + resources := Resources{CPUCores: 2, MemoryMiB: 2048} + offer := Offer{Provider: provider, Resources: resources, PricePerUnit: 2, Collateral: 5, Verification: []VerificationMode{VerificationZeroKnowledge}, ValidUntilHeight: 20} + offerID, err := market.PublishOffer(offer, 1) + if err != nil { + t.Fatal(err) + } + job := Job{Owner: owner, WorkloadHash: types.HashBytes("workload", []byte("zk")), InputRoot: types.HashBytes("input", []byte("x")), Resources: resources, MaxPrice: 3, CollateralRequired: 1, Verification: VerificationZeroKnowledge, DeadlineHeight: 15} + jobID, err := market.PostJob(job, 3, 2) + if err != nil { + t.Fatal(err) + } + if _, err := market.Assign(jobID, offerID, 3); err != nil { + t.Fatal(err) + } + result := Result{JobID: jobID, Provider: provider, ResultRoot: types.HashBytes("result", []byte("r")), ProofHash: types.HashBytes("proof", []byte("p")), CompletedHeight: 5} + if err := market.SubmitResult(result); err != nil { + t.Fatal(err) + } + if _, err := market.Finalize(jobID, VerificationEvidence{}); err != ErrMarketVerification { + t.Fatalf("expected proof verification gate, got %v", err) + } + if _, err := market.Finalize(jobID, VerificationEvidence{ProofVerified: true}); err != nil { + t.Fatal(err) + } +} From 7e4d8608cb7e0b7dcb58656c50abf43d768efadd Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:44:05 +0200 Subject: [PATCH 012/274] implement canonical v2 proposal vote and quorum certificates --- internal/v2/consensus/consensus.go | 314 +++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 internal/v2/consensus/consensus.go diff --git a/internal/v2/consensus/consensus.go b/internal/v2/consensus/consensus.go new file mode 100644 index 00000000..76407349 --- /dev/null +++ b/internal/v2/consensus/consensus.go @@ -0,0 +1,314 @@ +package consensus + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" + "math/big" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ( + ErrValidatorSet = errors.New("invalid v2 validator set") + ErrProposal = errors.New("invalid v2 consensus proposal") + ErrVote = errors.New("invalid v2 consensus vote") + ErrCertificate = errors.New("invalid v2 quorum certificate") + ErrInsufficientPower = errors.New("insufficient v2 voting power") +) + +type Validator struct { + ID types.ValidatorID + PublicKey []byte + Power uint64 +} + +type ValidatorSet struct { + Network types.NetworkID + Validators []Validator +} + +type Proposal struct { + Header sharding.GlobalHeader + Round uint64 + Proposer types.ValidatorID + PublicKey []byte + Signature []byte +} + +type Vote struct { + Network types.NetworkID + Height uint64 + Round uint64 + HeaderHash types.Hash + Voter types.ValidatorID + PublicKey []byte + Signature []byte +} + +type Certificate struct { + Network types.NetworkID + Height uint64 + Round uint64 + HeaderHash types.Hash + Votes []Vote +} + +func (s ValidatorSet) Validate() error { + if types.IsZero32([32]byte(s.Network)) || len(s.Validators) == 0 { + return ErrValidatorSet + } + seen := make(map[types.ValidatorID]struct{}, len(s.Validators)) + var total uint64 + for _, validator := range s.Validators { + if validator.Power == 0 || len(validator.PublicKey) != 65 || types.ValidatorIDFromPublicKey(validator.PublicKey) != validator.ID { + return ErrValidatorSet + } + x, y := elliptic.Unmarshal(elliptic.P256(), validator.PublicKey) + if x == nil || y == nil { + return ErrValidatorSet + } + if _, duplicate := seen[validator.ID]; duplicate { + return ErrValidatorSet + } + seen[validator.ID] = struct{}{} + if ^uint64(0)-total < validator.Power { + return ErrValidatorSet + } + total += validator.Power + } + return nil +} + +func (s ValidatorSet) TotalPower() (uint64, error) { + if err := s.Validate(); err != nil { + return 0, err + } + var total uint64 + for _, validator := range s.Validators { + total += validator.Power + } + return total, nil +} + +func QuorumPower(total uint64) uint64 { + if total == 0 { + return 0 + } + q, r := total/3, total%3 + return q*2 + (r*2)/3 + 1 +} + +func (s ValidatorSet) Proposer(height, round uint64) (Validator, error) { + total, err := s.TotalPower() + if err != nil || height == 0 { + return Validator{}, ErrValidatorSet + } + validators := append([]Validator(nil), s.Validators...) + sort.Slice(validators, func(i, j int) bool { return bytes.Compare(validators[i].ID[:], validators[j].ID[:]) < 0 }) + slot := ((height - 1) % total + (round % total)) % total + var cumulative uint64 + for _, validator := range validators { + cumulative += validator.Power + if slot < cumulative { + return validator, nil + } + } + return Validator{}, ErrValidatorSet +} + +func HeaderConsensusHash(header sharding.GlobalHeader) types.Hash { + copyHeader := header + copyHeader.CertificateHash = types.Hash{} + return types.Hash(codec.DomainHash("zephyr/global-header-consensus/v2", copyHeader.CanonicalBytes())) +} + +func (p Proposal) SigningDigest() types.Hash { + var w codec.Writer + w.Fixed(HeaderConsensusHash(p.Header)[:]) + w.U64(p.Round) + w.Fixed(p.Proposer[:]) + return types.Hash(codec.DomainHash("zephyr/consensus/proposal/v2", w.BytesCopy())) +} + +func (v Vote) SigningDigest() types.Hash { + var w codec.Writer + w.Fixed(v.Network[:]) + w.U64(v.Height) + w.U64(v.Round) + w.Fixed(v.HeaderHash[:]) + w.Fixed(v.Voter[:]) + return types.Hash(codec.DomainHash("zephyr/consensus/vote/v2", w.BytesCopy())) +} + +func SignProposal(privateKey *ecdsa.PrivateKey, header sharding.GlobalHeader, round uint64) (Proposal, error) { + publicKey, validatorID, err := signerIdentity(privateKey) + if err != nil { + return Proposal{}, err + } + proposal := Proposal{Header: header, Round: round, Proposer: validatorID, PublicKey: publicKey} + proposal.Signature, err = signDigest(privateKey, proposal.SigningDigest()) + return proposal, err +} + +func SignVote(privateKey *ecdsa.PrivateKey, network types.NetworkID, height, round uint64, headerHash types.Hash) (Vote, error) { + publicKey, validatorID, err := signerIdentity(privateKey) + if err != nil { + return Vote{}, err + } + vote := Vote{Network: network, Height: height, Round: round, HeaderHash: headerHash, Voter: validatorID, PublicKey: publicKey} + vote.Signature, err = signDigest(privateKey, vote.SigningDigest()) + return vote, err +} + +func (s ValidatorSet) VerifyProposal(proposal Proposal) error { + if err := s.Validate(); err != nil || proposal.Header.Network != s.Network || proposal.Header.Height == 0 || proposal.Header.CertificateHash != (types.Hash{}) { + return ErrProposal + } + expected, err := s.Proposer(proposal.Header.Height, proposal.Round) + if err != nil || expected.ID != proposal.Proposer || !bytes.Equal(expected.PublicKey, proposal.PublicKey) { + return ErrProposal + } + if types.ValidatorIDFromPublicKey(proposal.PublicKey) != proposal.Proposer || verifyDigest(proposal.PublicKey, proposal.SigningDigest(), proposal.Signature) != nil { + return ErrProposal + } + return nil +} + +func (s ValidatorSet) VerifyVote(vote Vote) error { + if vote.Network != s.Network || vote.Height == 0 || types.IsZero32([32]byte(vote.HeaderHash)) || types.ValidatorIDFromPublicKey(vote.PublicKey) != vote.Voter { + return ErrVote + } + validator, ok := s.validator(vote.Voter) + if !ok || !bytes.Equal(validator.PublicKey, vote.PublicKey) || verifyDigest(vote.PublicKey, vote.SigningDigest(), vote.Signature) != nil { + return ErrVote + } + return nil +} + +func (s ValidatorSet) BuildCertificate(proposal Proposal, votes []Vote) (Certificate, error) { + if err := s.VerifyProposal(proposal); err != nil { + return Certificate{}, err + } + certificate := Certificate{ + Network: s.Network, Height: proposal.Header.Height, Round: proposal.Round, + HeaderHash: HeaderConsensusHash(proposal.Header), Votes: append([]Vote(nil), votes...), + } + if err := s.VerifyCertificate(certificate); err != nil { + return Certificate{}, err + } + return certificate, nil +} + +func (s ValidatorSet) VerifyCertificate(certificate Certificate) error { + if err := s.Validate(); err != nil || certificate.Network != s.Network || certificate.Height == 0 || types.IsZero32([32]byte(certificate.HeaderHash)) { + return ErrCertificate + } + total, _ := s.TotalPower() + quorum := QuorumPower(total) + seen := make(map[types.ValidatorID]struct{}, len(certificate.Votes)) + var power uint64 + for _, vote := range certificate.Votes { + if vote.Network != certificate.Network || vote.Height != certificate.Height || vote.Round != certificate.Round || vote.HeaderHash != certificate.HeaderHash { + return ErrCertificate + } + if _, duplicate := seen[vote.Voter]; duplicate { + return ErrCertificate + } + seen[vote.Voter] = struct{}{} + if err := s.VerifyVote(vote); err != nil { + return ErrCertificate + } + validator, _ := s.validator(vote.Voter) + power += validator.Power + } + if power < quorum { + return ErrInsufficientPower + } + return nil +} + +func (c Certificate) Hash() types.Hash { + votes := append([]Vote(nil), c.Votes...) + sort.Slice(votes, func(i, j int) bool { return bytes.Compare(votes[i].Voter[:], votes[j].Voter[:]) < 0 }) + var w codec.Writer + w.Fixed(c.Network[:]) + w.U64(c.Height) + w.U64(c.Round) + w.Fixed(c.HeaderHash[:]) + w.U32(uint32(len(votes))) + for _, vote := range votes { + w.Fixed(vote.Voter[:]) + w.Bytes(vote.PublicKey) + w.Bytes(vote.Signature) + } + return types.Hash(codec.DomainHash("zephyr/quorum-certificate/v2", w.BytesCopy())) +} + +func (s ValidatorSet) validator(id types.ValidatorID) (Validator, bool) { + for _, validator := range s.Validators { + if validator.ID == id { + return validator, true + } + } + return Validator{}, false +} + +func signerIdentity(privateKey *ecdsa.PrivateKey) ([]byte, types.ValidatorID, error) { + if privateKey == nil || privateKey.Curve != elliptic.P256() { + return nil, types.ValidatorID{}, ErrValidatorSet + } + publicKey := elliptic.Marshal(elliptic.P256(), privateKey.PublicKey.X, privateKey.PublicKey.Y) + return publicKey, types.ValidatorIDFromPublicKey(publicKey), nil +} + +func signDigest(privateKey *ecdsa.PrivateKey, digest types.Hash) ([]byte, error) { + r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest[:]) + if err != nil { + return nil, err + } + if s.Cmp(halfOrder()) > 0 { + s = new(big.Int).Sub(elliptic.P256().Params().N, s) + } + return append(pad32(r), pad32(s)...), nil +} + +func verifyDigest(publicKey []byte, digest types.Hash, signature []byte) error { + if len(publicKey) != 65 || len(signature) != 64 { + return ErrVote + } + x, y := elliptic.Unmarshal(elliptic.P256(), publicKey) + if x == nil || y == nil { + return ErrVote + } + r := new(big.Int).SetBytes(signature[:32]) + s := new(big.Int).SetBytes(signature[32:]) + order := elliptic.P256().Params().N + if r.Sign() <= 0 || s.Sign() <= 0 || r.Cmp(order) >= 0 || s.Cmp(order) >= 0 || s.Cmp(halfOrder()) > 0 { + return ErrVote + } + if !ecdsa.Verify(&ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}, digest[:], r, s) { + return ErrVote + } + return nil +} + +func halfOrder() *big.Int { + return new(big.Int).Rsh(new(big.Int).Set(elliptic.P256().Params().N), 1) +} + +func pad32(value *big.Int) []byte { + raw := value.Bytes() + out := make([]byte, 32) + if len(raw) >= 32 { + copy(out, raw[len(raw)-32:]) + } else { + copy(out[32-len(raw):], raw) + } + return out +} From 25096d483ce2b0b7538a8933e1b7f2f72cf35778 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:44:48 +0200 Subject: [PATCH 013/274] fix canonical v2 proposal signing digest --- internal/v2/consensus/consensus.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/v2/consensus/consensus.go b/internal/v2/consensus/consensus.go index 76407349..454dff36 100644 --- a/internal/v2/consensus/consensus.go +++ b/internal/v2/consensus/consensus.go @@ -15,10 +15,10 @@ import ( ) var ( - ErrValidatorSet = errors.New("invalid v2 validator set") - ErrProposal = errors.New("invalid v2 consensus proposal") - ErrVote = errors.New("invalid v2 consensus vote") - ErrCertificate = errors.New("invalid v2 quorum certificate") + ErrValidatorSet = errors.New("invalid v2 validator set") + ErrProposal = errors.New("invalid v2 consensus proposal") + ErrVote = errors.New("invalid v2 consensus vote") + ErrCertificate = errors.New("invalid v2 quorum certificate") ErrInsufficientPower = errors.New("insufficient v2 voting power") ) @@ -130,7 +130,8 @@ func HeaderConsensusHash(header sharding.GlobalHeader) types.Hash { func (p Proposal) SigningDigest() types.Hash { var w codec.Writer - w.Fixed(HeaderConsensusHash(p.Header)[:]) + headerHash := HeaderConsensusHash(p.Header) + w.Fixed(headerHash[:]) w.U64(p.Round) w.Fixed(p.Proposer[:]) return types.Hash(codec.DomainHash("zephyr/consensus/proposal/v2", w.BytesCopy())) @@ -195,10 +196,7 @@ func (s ValidatorSet) BuildCertificate(proposal Proposal, votes []Vote) (Certifi if err := s.VerifyProposal(proposal); err != nil { return Certificate{}, err } - certificate := Certificate{ - Network: s.Network, Height: proposal.Header.Height, Round: proposal.Round, - HeaderHash: HeaderConsensusHash(proposal.Header), Votes: append([]Vote(nil), votes...), - } + certificate := Certificate{Network: s.Network, Height: proposal.Header.Height, Round: proposal.Round, HeaderHash: HeaderConsensusHash(proposal.Header), Votes: append([]Vote(nil), votes...)} if err := s.VerifyCertificate(certificate); err != nil { return Certificate{}, err } From 55ec48e31f95c43f1d55f72aaae0953ea18b1a9b Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:45:05 +0200 Subject: [PATCH 014/274] test v2 weighted quorum certificates --- internal/v2/consensus/consensus_test.go | 90 +++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 internal/v2/consensus/consensus_test.go diff --git a/internal/v2/consensus/consensus_test.go b/internal/v2/consensus/consensus_test.go new file mode 100644 index 00000000..7f201d65 --- /dev/null +++ b/internal/v2/consensus/consensus_test.go @@ -0,0 +1,90 @@ +package consensus + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestV2QuorumCertificateRequiresTwoThirdsPlus(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("consensus-v2"))) + set := ValidatorSet{Network: network} + keys := make(map[types.ValidatorID]*ecdsa.PrivateKey) + for i := 0; i < 4; i++ { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + id := types.ValidatorIDFromPublicKey(pub) + set.Validators = append(set.Validators, Validator{ID: id, PublicKey: pub, Power: 10}) + keys[id] = key + } + if QuorumPower(40) != 27 { + t.Fatalf("unexpected quorum: %d", QuorumPower(40)) + } + proposer, err := set.Proposer(1, 0) + if err != nil { + t.Fatal(err) + } + header := sharding.GlobalHeader{ + Version: 2, Network: network, Height: 1, + ShardCommitmentRoot: types.HashBytes("shards", []byte("root")), + ValidatorRoot: types.HashBytes("validators", []byte("root")), + DataRoot: types.HashBytes("data", []byte("root")), + } + proposal, err := SignProposal(keys[proposer.ID], header, 0) + if err != nil { + t.Fatal(err) + } + if err := set.VerifyProposal(proposal); err != nil { + t.Fatal(err) + } + headerHash := HeaderConsensusHash(header) + votes := make([]Vote, 0, 3) + for i := 0; i < 3; i++ { + validator := set.Validators[i] + vote, err := SignVote(keys[validator.ID], network, 1, 0, headerHash) + if err != nil { + t.Fatal(err) + } + votes = append(votes, vote) + } + if _, err := set.BuildCertificate(proposal, votes[:2]); err != ErrInsufficientPower { + t.Fatalf("expected insufficient power, got %v", err) + } + certificate, err := set.BuildCertificate(proposal, votes) + if err != nil { + t.Fatal(err) + } + if types.IsZero32([32]byte(certificate.Hash())) { + t.Fatal("certificate hash is zero") + } + if err := set.VerifyCertificate(certificate); err != nil { + t.Fatal(err) + } +} + +func TestV2CertificateRejectsDuplicateVote(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("duplicate"))) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + id := types.ValidatorIDFromPublicKey(pub) + set := ValidatorSet{Network: network, Validators: []Validator{{ID: id, PublicKey: pub, Power: 1}}} + hash := types.HashBytes("header", []byte("h")) + vote, err := SignVote(key, network, 1, 0, hash) + if err != nil { + t.Fatal(err) + } + certificate := Certificate{Network: network, Height: 1, HeaderHash: hash, Votes: []Vote{vote, vote}} + if err := set.VerifyCertificate(certificate); err != ErrCertificate { + t.Fatalf("expected duplicate vote rejection, got %v", err) + } +} From 765b169bdd31ea09ebd4dd67495d98af25582183 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:45:46 +0200 Subject: [PATCH 015/274] add cloneable sparse Merkle state for pre-finality simulation --- internal/v2/state/clone.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 internal/v2/state/clone.go diff --git a/internal/v2/state/clone.go b/internal/v2/state/clone.go new file mode 100644 index 00000000..bcc89953 --- /dev/null +++ b/internal/v2/state/clone.go @@ -0,0 +1,21 @@ +package state + +// Clone returns an independent copy of the sparse Merkle tree. It is used by +// proposal simulation so validators can calculate a candidate state root +// without mutating committed state before a quorum certificate exists. +func (t *Tree) Clone() *Tree { + t.mu.RLock() + defer t.mu.RUnlock() + clone := &Tree{ + values: make(map[types.Hash][]byte, len(t.values)), + nodes: make(map[nodeKey]types.Hash, len(t.nodes)), + defaults: t.defaults, + } + for key, value := range t.values { + clone.values[key] = append([]byte(nil), value...) + } + for key, value := range t.nodes { + clone.nodes[key] = value + } + return clone +} From a0a0ccc4f080fa51bf4abfb81256097f0faec9a0 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:45:58 +0200 Subject: [PATCH 016/274] fix sparse Merkle clone imports --- internal/v2/state/clone.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/v2/state/clone.go b/internal/v2/state/clone.go index bcc89953..367c3326 100644 --- a/internal/v2/state/clone.go +++ b/internal/v2/state/clone.go @@ -1,5 +1,7 @@ package state +import "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + // Clone returns an independent copy of the sparse Merkle tree. It is used by // proposal simulation so validators can calculate a candidate state root // without mutating committed state before a quorum certificate exists. From 3b22c1aa68493bdf928e743d4c1307b2005cd0ee Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:46:19 +0200 Subject: [PATCH 017/274] add non-mutating v2 state-root simulation --- internal/v2/worldstate/simulate.go | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 internal/v2/worldstate/simulate.go diff --git a/internal/v2/worldstate/simulate.go b/internal/v2/worldstate/simulate.go new file mode 100644 index 00000000..76d73758 --- /dev/null +++ b/internal/v2/worldstate/simulate.go @@ -0,0 +1,57 @@ +package worldstate + +import ( + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +// Simulator is implemented by state backends that can calculate the root of a +// delta without mutating committed state. +type Simulator interface { + Simulate(consumed []types.ObjectID, created []object.Object) (types.Hash, error) +} + +func (m *Memory) Simulate(consumed []types.ObjectID, created []object.Object) (types.Hash, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + seenConsumed := make(map[types.ObjectID]struct{}, len(consumed)) + for _, id := range consumed { + if _, duplicate := seenConsumed[id]; duplicate { + return m.tree.Root(), ErrObjectNotFound + } + seenConsumed[id] = struct{}{} + if _, ok := m.objects[id]; !ok { + return m.tree.Root(), ErrObjectNotFound + } + } + seenCreated := make(map[types.ObjectID]struct{}, len(created)) + for _, item := range created { + if err := item.Validate(); err != nil { + return m.tree.Root(), err + } + if _, duplicate := seenCreated[item.ID]; duplicate { + return m.tree.Root(), ErrObjectExists + } + seenCreated[item.ID] = struct{}{} + if _, exists := m.objects[item.ID]; exists { + if _, replacing := seenConsumed[item.ID]; !replacing { + return m.tree.Root(), ErrObjectExists + } + } + } + + updates := make(map[types.Hash][]byte, len(consumed)+len(created)) + for _, id := range consumed { + updates[types.Hash(id)] = nil + } + for _, item := range created { + hash := item.Hash() + updates[types.Hash(item.ID)] = hash[:] + } + return m.tree.Clone().Apply(updates), nil +} + +func (d *Disk) Simulate(consumed []types.ObjectID, created []object.Object) (types.Hash, error) { + return d.mem.Simulate(consumed, created) +} From f49a92c8d9d9d398db6fd6b57df3c3f58370dd94 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:46:53 +0200 Subject: [PATCH 018/274] connect v2 execution simulation sharding and quorum finality --- internal/v2/node/runtime.go | 176 ++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 internal/v2/node/runtime.go diff --git a/internal/v2/node/runtime.go b/internal/v2/node/runtime.go new file mode 100644 index 00000000..ddc27285 --- /dev/null +++ b/internal/v2/node/runtime.go @@ -0,0 +1,176 @@ +package node + +import ( + "errors" + "sort" + "sync" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/execution" + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +var ( + ErrRuntimeConfig = errors.New("invalid v2 runtime configuration") + ErrCandidateHeight = errors.New("invalid v2 candidate height") + ErrCandidateState = errors.New("v2 candidate does not match committed state") + ErrCandidateCert = errors.New("v2 candidate certificate mismatch") + ErrStateSimulation = errors.New("v2 backend does not support state simulation") +) + +type ShardBatch struct { + Transactions []tx.Transaction + ReceiptRoot types.Hash + DataRoot types.Hash +} + +type shardDelta struct { + Consumed []types.ObjectID + Created []object.Object +} + +type Candidate struct { + Header sharding.GlobalHeader + Commitments []sharding.Commitment + Results map[uint32][]execution.Result + deltas map[uint32]shardDelta +} + +type Runtime struct { + mu sync.Mutex + Network types.NetworkID + NativeToken types.TokenID + ValidatorRoot types.Hash + ShardCount uint32 + States map[uint32]worldstate.Backend + Workers int + Height uint64 + ParentHash types.Hash +} + +func NewRuntime(network types.NetworkID, nativeToken types.TokenID, validatorRoot types.Hash, states map[uint32]worldstate.Backend, workers int) (*Runtime, error) { + if types.IsZero32([32]byte(network)) || types.IsZero32([32]byte(nativeToken)) || types.IsZero32([32]byte(validatorRoot)) || len(states) == 0 { + return nil, ErrRuntimeConfig + } + count := uint32(len(states)) + for shard := uint32(0); shard < count; shard++ { + if states[shard] == nil { + return nil, ErrRuntimeConfig + } + } + return &Runtime{Network: network, NativeToken: nativeToken, ValidatorRoot: validatorRoot, ShardCount: count, States: states, Workers: workers}, nil +} + +// BuildCandidate executes and simulates every shard against committed state. +// It never mutates the backing state stores. +func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) (Candidate, error) { + r.mu.Lock() + defer r.mu.Unlock() + if height != r.Height+1 || height == 0 { + return Candidate{}, ErrCandidateHeight + } + candidate := Candidate{Results: make(map[uint32][]execution.Result), deltas: make(map[uint32]shardDelta)} + commitments := make([]sharding.Commitment, 0, r.ShardCount) + dataLeaves := make([]types.Hash, 0, r.ShardCount) + + for shard := uint32(0); shard < r.ShardCount; shard++ { + store := r.States[shard] + batch := batches[shard] + currentRoot := store.Root() + newRoot := currentRoot + if len(batch.Transactions) > 0 { + for _, transaction := range batch.Transactions { + if transaction.ShardID != shard || transaction.StateRoot != currentRoot { + return Candidate{}, ErrCandidateState + } + } + executor := execution.BatchExecutor{Engine: execution.Engine{Network: r.Network, NativeToken: r.NativeToken, ShardCount: r.ShardCount}, Workers: r.Workers} + results, err := executor.ExecuteBatch(batch.Transactions) + if err != nil { + return Candidate{}, err + } + delta := shardDelta{} + for _, result := range results { + delta.Consumed = append(delta.Consumed, result.Consumed...) + delta.Created = append(delta.Created, result.Created...) + } + simulator, ok := store.(worldstate.Simulator) + if !ok { + return Candidate{}, ErrStateSimulation + } + newRoot, err = simulator.Simulate(delta.Consumed, delta.Created) + if err != nil { + return Candidate{}, err + } + candidate.Results[shard] = results + candidate.deltas[shard] = delta + } + receiptRoot := batch.ReceiptRoot + if types.IsZero32([32]byte(receiptRoot)) { + receiptRoot = merkle.Root(nil) + } + dataRoot := batch.DataRoot + if types.IsZero32([32]byte(dataRoot)) { + dataRoot = merkle.Root(nil) + } + commitments = append(commitments, sharding.Commitment{ShardID: shard, StateRoot: newRoot, ReceiptRoot: receiptRoot, DataRoot: dataRoot}) + dataLeaves = append(dataLeaves, merkle.Leaf("shard-data-root", dataRoot[:])) + } + commitmentRoot, err := sharding.CommitmentRoot(commitments) + if err != nil { + return Candidate{}, err + } + candidate.Commitments = commitments + candidate.Header = sharding.GlobalHeader{ + Version: 2, Network: r.Network, Height: height, ParentHash: r.ParentHash, + ShardCommitmentRoot: commitmentRoot, ValidatorRoot: r.ValidatorRoot, + DataRoot: merkle.Root(dataLeaves), + } + return candidate, nil +} + +// Commit applies a previously simulated candidate only after a valid quorum +// certificate for its consensus hash is supplied. +func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificate, validators v2consensus.ValidatorSet) (sharding.GlobalHeader, error) { + r.mu.Lock() + defer r.mu.Unlock() + if candidate.Header.Height != r.Height+1 || candidate.Header.ParentHash != r.ParentHash || candidate.Header.Network != r.Network { + return sharding.GlobalHeader{}, ErrCandidateState + } + if certificate.HeaderHash != v2consensus.HeaderConsensusHash(candidate.Header) || certificate.Height != candidate.Header.Height || certificate.Network != r.Network { + return sharding.GlobalHeader{}, ErrCandidateCert + } + if err := validators.VerifyCertificate(certificate); err != nil { + return sharding.GlobalHeader{}, err + } + commitments := make(map[uint32]sharding.Commitment, len(candidate.Commitments)) + for _, commitment := range candidate.Commitments { + commitments[commitment.ShardID] = commitment + } + shards := make([]int, 0, len(candidate.deltas)) + for shard := range candidate.deltas { + shards = append(shards, int(shard)) + } + sort.Ints(shards) + for _, shardValue := range shards { + shard := uint32(shardValue) + delta := candidate.deltas[shard] + root, err := r.States[shard].Apply(delta.Consumed, delta.Created) + if err != nil { + return sharding.GlobalHeader{}, err + } + if root != commitments[shard].StateRoot { + return sharding.GlobalHeader{}, ErrCandidateState + } + } + finalized := candidate.Header + finalized.CertificateHash = certificate.Hash() + r.Height = finalized.Height + r.ParentHash = v2consensus.HeaderConsensusHash(finalized) + return finalized, nil +} From 9dfd7c651b071f1c3089ae52f2a7b63235fdc2be Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:47:12 +0200 Subject: [PATCH 019/274] test v2 proof transaction through quorum-finalized state commit --- internal/v2/node/runtime_test.go | 99 ++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 internal/v2/node/runtime_test.go diff --git a/internal/v2/node/runtime_test.go b/internal/v2/node/runtime_test.go new file mode 100644 index 00000000..2df31db3 --- /dev/null +++ b/internal/v2/node/runtime_test.go @@ -0,0 +1,99 @@ +package node + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestCandidateDoesNotMutateBeforeQCAndCommitsAfterQC(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("node-runtime"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + validatorRoot := types.HashBytes("validators", []byte("set-1")) + stateStore := worldstate.NewMemory() + + aliceKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + alicePub := elliptic.Marshal(elliptic.P256(), aliceKey.PublicKey.X, aliceKey.PublicKey.Y) + alice := types.AccountIDFromPublicKey(alicePub) + bob := types.AccountIDFromPublicKey([]byte("bob")) + inputID := types.ObjectIDFromTransaction(types.HashBytes("genesis", []byte("coin")), 0) + inputOut, _ := object.NewCoinOutput(alice, native, 100) + input := object.Object{ID: inputID, Version: 1, Owner: alice, Kind: inputOut.Kind, Data: inputOut.Data} + root, err := stateStore.Apply(nil, []object.Object{input}) + if err != nil { + t.Fatal(err) + } + witness, proof, ok := stateStore.Proof(inputID) + if !ok { + t.Fatal("missing genesis input") + } + witnessHash := witness.Hash() + toBob, _ := object.NewCoinOutput(bob, native, 25) + change, _ := object.NewCoinOutput(alice, native, 74) + transaction := tx.Transaction{ + Version: tx.Version, Network: network, ShardID: 0, StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: inputID, Version: 1, ObjectHash: witnessHash}}, + Outputs: []object.OutputSpec{toBob, change}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, + Fee: 1, Witnesses: []tx.Witness{{Object: witness, Proof: proof}}, + } + transaction.Salt[0] = 1 + if err := transaction.Sign(aliceKey); err != nil { + t.Fatal(err) + } + + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: stateStore}, 2) + if err != nil { + t.Fatal(err) + } + candidate, err := runtime.BuildCandidate(1, map[uint32]ShardBatch{0: {Transactions: []tx.Transaction{transaction}}}) + if err != nil { + t.Fatal(err) + } + if stateStore.Root() != root { + t.Fatal("candidate simulation mutated committed state before QC") + } + if candidate.Commitments[0].StateRoot == root { + t.Fatal("candidate did not calculate a new state root") + } + + validatorKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + validatorPub := elliptic.Marshal(elliptic.P256(), validatorKey.PublicKey.X, validatorKey.PublicKey.Y) + validatorID := types.ValidatorIDFromPublicKey(validatorPub) + validators := v2consensus.ValidatorSet{Network: network, Validators: []v2consensus.Validator{{ID: validatorID, PublicKey: validatorPub, Power: 10}}} + proposal, err := v2consensus.SignProposal(validatorKey, candidate.Header, 0) + if err != nil { + t.Fatal(err) + } + if err := validators.VerifyProposal(proposal); err != nil { + t.Fatal(err) + } + headerHash := v2consensus.HeaderConsensusHash(candidate.Header) + vote, err := v2consensus.SignVote(validatorKey, network, 1, 0, headerHash) + if err != nil { + t.Fatal(err) + } + certificate, err := validators.BuildCertificate(proposal, []v2consensus.Vote{vote}) + if err != nil { + t.Fatal(err) + } + finalized, err := runtime.Commit(candidate, certificate, validators) + if err != nil { + t.Fatal(err) + } + if types.IsZero32([32]byte(finalized.CertificateHash)) || runtime.Height != 1 || stateStore.Root() != candidate.Commitments[0].StateRoot { + t.Fatal("QC commit did not finalize candidate state") + } +} From f8c08e5373d966177a5b1a12bfabc9199ba3f093 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:47:35 +0200 Subject: [PATCH 020/274] enforce deterministic WASM fuel and declared state access --- internal/v2/contracts/metered.go | 85 ++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 internal/v2/contracts/metered.go diff --git a/internal/v2/contracts/metered.go b/internal/v2/contracts/metered.go new file mode 100644 index 00000000..eac0c103 --- /dev/null +++ b/internal/v2/contracts/metered.go @@ -0,0 +1,85 @@ +package contracts + +import ( + "errors" + "strings" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + MaxArgumentsBytes = 1 << 20 + MaxReturnBytes = 1 << 20 + MaxEventBytes = 64 << 10 + MaxEvents = 1024 + MaxAccesses = 4096 +) + +var ( + ErrInvalidRequest = errors.New("invalid deterministic contract request") + ErrInvalidResult = errors.New("invalid deterministic contract result") +) + +// MeteredRuntime is the consensus guard around a concrete WASM engine. A +// concrete runtime may change for performance, but this boundary makes fuel, +// declared state access and bounded output consensus invariants. +type MeteredRuntime struct { + Inner Runtime +} + +func (m MeteredRuntime) ValidateModule(code []byte) error { + if m.Inner == nil || !ValidateWASMModule(code) { + return ErrInvalidModule + } + return m.Inner.ValidateModule(code) +} + +func (m MeteredRuntime) Execute(request Request) (Result, error) { + if m.Inner == nil { + return Result{}, ErrInvalidRequest + } + allowed, err := validateRequest(request) + if err != nil { + return Result{}, err + } + result, err := m.Inner.Execute(request) + if err != nil { + return Result{}, err + } + if result.FuelUsed > request.FuelLimit { + return Result{}, ErrFuelExhausted + } + if len(result.ReturnData) > MaxReturnBytes || len(result.Events) > MaxEvents { + return Result{}, ErrInvalidResult + } + for _, event := range result.Events { + if len(event) > MaxEventBytes { + return Result{}, ErrInvalidResult + } + } + for id := range result.Writes { + write, declared := allowed[id] + if !declared || !write { + return Result{}, ErrUndeclaredAccess + } + } + return result, nil +} + +func validateRequest(request Request) (map[types.ObjectID]bool, error) { + if types.IsZero32([32]byte(request.ContractID)) || strings.TrimSpace(request.Entrypoint) == "" || len(request.Entrypoint) > 128 || + len(request.Arguments) > MaxArgumentsBytes || request.FuelLimit == 0 || len(request.Accesses) > MaxAccesses { + return nil, ErrInvalidRequest + } + allowed := make(map[types.ObjectID]bool, len(request.Accesses)) + for _, access := range request.Accesses { + if types.IsZero32([32]byte(access.ObjectID)) { + return nil, ErrInvalidRequest + } + if _, duplicate := allowed[access.ObjectID]; duplicate { + return nil, ErrInvalidRequest + } + allowed[access.ObjectID] = access.Write + } + return allowed, nil +} From 61ef83bb0cd7d0b67f1deb37605e414ce26b0d35 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:47:45 +0200 Subject: [PATCH 021/274] test deterministic smart-contract metering boundary --- internal/v2/contracts/metered_test.go | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 internal/v2/contracts/metered_test.go diff --git a/internal/v2/contracts/metered_test.go b/internal/v2/contracts/metered_test.go new file mode 100644 index 00000000..5d47f74e --- /dev/null +++ b/internal/v2/contracts/metered_test.go @@ -0,0 +1,36 @@ +package contracts + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +type fakeRuntime struct { + result Result +} + +func (f fakeRuntime) ValidateModule(code []byte) error { return nil } +func (f fakeRuntime) Execute(request Request) (Result, error) { return f.result, nil } + +func TestMeteredRuntimeEnforcesFuelAndAccess(t *testing.T) { + contract := types.ContractID(types.HashBytes("contract", []byte("c"))) + writable := types.ObjectID(types.HashBytes("object", []byte("write"))) + request := Request{ContractID: contract, Entrypoint: "transfer", FuelLimit: 100, Accesses: []Access{{ObjectID: writable, Write: true}}} + + valid := MeteredRuntime{Inner: fakeRuntime{result: Result{FuelUsed: 80, Writes: map[types.ObjectID][]byte{writable: []byte("new")}}}} + if _, err := valid.Execute(request); err != nil { + t.Fatal(err) + } + + overFuel := MeteredRuntime{Inner: fakeRuntime{result: Result{FuelUsed: 101}}} + if _, err := overFuel.Execute(request); err != ErrFuelExhausted { + t.Fatalf("expected fuel rejection, got %v", err) + } + + undeclared := types.ObjectID(types.HashBytes("object", []byte("other"))) + badAccess := MeteredRuntime{Inner: fakeRuntime{result: Result{FuelUsed: 1, Writes: map[types.ObjectID][]byte{undeclared: []byte("x")}}}} + if _, err := badAccess.Execute(request); err != ErrUndeclaredAccess { + t.Fatalf("expected undeclared access rejection, got %v", err) + } +} From 29ac9693b02ba079808d690b5897af70cfd836f7 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:48:14 +0200 Subject: [PATCH 022/274] add canonical v2 shard and global-header decoders --- internal/v2/sharding/wire.go | 122 +++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 internal/v2/sharding/wire.go diff --git a/internal/v2/sharding/wire.go b/internal/v2/sharding/wire.go new file mode 100644 index 00000000..c6b9cfa9 --- /dev/null +++ b/internal/v2/sharding/wire.go @@ -0,0 +1,122 @@ +package sharding + +import ( + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func ParseCommitment(data []byte) (Commitment, error) { + r := codec.NewReader(data) + shardID, err := r.U32() + if err != nil { + return Commitment{}, ErrShardCount + } + stateRoot, err := readHash(r) + if err != nil { + return Commitment{}, ErrShardCount + } + receiptRoot, err := readHash(r) + if err != nil { + return Commitment{}, ErrShardCount + } + dataRoot, err := readHash(r) + if err != nil || r.Done() != nil || types.IsZero32([32]byte(stateRoot)) { + return Commitment{}, ErrShardCount + } + return Commitment{ShardID: shardID, StateRoot: stateRoot, ReceiptRoot: receiptRoot, DataRoot: dataRoot}, nil +} + +func ParseGlobalHeader(data []byte) (GlobalHeader, error) { + r := codec.NewReader(data) + version, err := r.U16() + if err != nil || version != 2 { + return GlobalHeader{}, ErrShardCount + } + networkBytes, err := r.Fixed(32) + if err != nil { + return GlobalHeader{}, ErrShardCount + } + var network types.NetworkID + copy(network[:], networkBytes) + height, err := r.U64() + if err != nil || height == 0 { + return GlobalHeader{}, ErrShardCount + } + parentHash, err := readHash(r) + if err != nil { + return GlobalHeader{}, ErrShardCount + } + shardRoot, err := readHash(r) + if err != nil { + return GlobalHeader{}, ErrShardCount + } + validatorRoot, err := readHash(r) + if err != nil { + return GlobalHeader{}, ErrShardCount + } + dataRoot, err := readHash(r) + if err != nil { + return GlobalHeader{}, ErrShardCount + } + certificateHash, err := readHash(r) + if err != nil || r.Done() != nil || types.IsZero32([32]byte(network)) || types.IsZero32([32]byte(shardRoot)) || types.IsZero32([32]byte(validatorRoot)) { + return GlobalHeader{}, ErrShardCount + } + return GlobalHeader{ + Version: version, Network: network, Height: height, ParentHash: parentHash, + ShardCommitmentRoot: shardRoot, ValidatorRoot: validatorRoot, DataRoot: dataRoot, + CertificateHash: certificateHash, + }, nil +} + +func ParseCrossShardReceipt(data []byte) (CrossShardReceipt, error) { + r := codec.NewReader(data) + source, err := r.U32() + if err != nil { + return CrossShardReceipt{}, ErrReceipt + } + destination, err := r.U32() + if err != nil { + return CrossShardReceipt{}, ErrReceipt + } + height, err := r.U64() + if err != nil { + return CrossShardReceipt{}, ErrReceipt + } + txHash, err := readHash(r) + if err != nil { + return CrossShardReceipt{}, ErrReceipt + } + index, err := r.U32() + if err != nil { + return CrossShardReceipt{}, ErrReceipt + } + outputBytes, err := r.Bytes(object.MaxObjectDataBytes + 64) + if err != nil { + return CrossShardReceipt{}, ErrReceipt + } + output, err := object.ParseOutputSpec(outputBytes) + if err != nil { + return CrossShardReceipt{}, ErrReceipt + } + stateRoot, err := readHash(r) + if err != nil || r.Done() != nil { + return CrossShardReceipt{}, ErrReceipt + } + receipt := CrossShardReceipt{SourceShard: source, DestinationShard: destination, SourceHeight: height, TransactionID: txHash, OutputIndex: index, Output: output, SourceStateRoot: stateRoot} + if err := receipt.Validate(); err != nil { + return CrossShardReceipt{}, err + } + return receipt, nil +} + +func readHash(r *codec.Reader) (types.Hash, error) { + raw, err := r.Fixed(32) + if err != nil { + return types.Hash{}, err + } + var hash types.Hash + copy(hash[:], raw) + return hash, nil +} From fe0b83963f5007ab10d80e2d48aca13bf86ef640 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:48:37 +0200 Subject: [PATCH 023/274] add canonical binary v2 consensus wire format --- internal/v2/consensus/wire.go | 171 ++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 internal/v2/consensus/wire.go diff --git a/internal/v2/consensus/wire.go b/internal/v2/consensus/wire.go new file mode 100644 index 00000000..e8a701f2 --- /dev/null +++ b/internal/v2/consensus/wire.go @@ -0,0 +1,171 @@ +package consensus + +import ( + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const MaxCertificateVotes = 4096 + +func (p Proposal) MarshalBinary() ([]byte, error) { + if len(p.PublicKey) != 65 || len(p.Signature) != 64 || types.IsZero32([32]byte(p.Proposer)) { + return nil, ErrProposal + } + var w codec.Writer + w.Bytes(p.Header.CanonicalBytes()) + w.U64(p.Round) + w.Fixed(p.Proposer[:]) + w.Bytes(p.PublicKey) + w.Bytes(p.Signature) + return w.BytesCopy(), nil +} + +func ParseProposal(data []byte) (Proposal, error) { + r := codec.NewReader(data) + headerBytes, err := r.Bytes(512) + if err != nil { + return Proposal{}, ErrProposal + } + header, err := sharding.ParseGlobalHeader(headerBytes) + if err != nil { + return Proposal{}, ErrProposal + } + round, err := r.U64() + if err != nil { + return Proposal{}, ErrProposal + } + proposerBytes, err := r.Fixed(32) + if err != nil { + return Proposal{}, ErrProposal + } + publicKey, err := r.Bytes(65) + if err != nil || len(publicKey) != 65 { + return Proposal{}, ErrProposal + } + signature, err := r.Bytes(64) + if err != nil || len(signature) != 64 || r.Done() != nil { + return Proposal{}, ErrProposal + } + var proposer types.ValidatorID + copy(proposer[:], proposerBytes) + return Proposal{Header: header, Round: round, Proposer: proposer, PublicKey: publicKey, Signature: signature}, nil +} + +func (v Vote) MarshalBinary() ([]byte, error) { + if len(v.PublicKey) != 65 || len(v.Signature) != 64 || v.Height == 0 || types.IsZero32([32]byte(v.Network)) || types.IsZero32([32]byte(v.HeaderHash)) || types.IsZero32([32]byte(v.Voter)) { + return nil, ErrVote + } + var w codec.Writer + w.Fixed(v.Network[:]) + w.U64(v.Height) + w.U64(v.Round) + w.Fixed(v.HeaderHash[:]) + w.Fixed(v.Voter[:]) + w.Bytes(v.PublicKey) + w.Bytes(v.Signature) + return w.BytesCopy(), nil +} + +func ParseVote(data []byte) (Vote, error) { + r := codec.NewReader(data) + networkBytes, err := r.Fixed(32) + if err != nil { + return Vote{}, ErrVote + } + height, err := r.U64() + if err != nil || height == 0 { + return Vote{}, ErrVote + } + round, err := r.U64() + if err != nil { + return Vote{}, ErrVote + } + headerBytes, err := r.Fixed(32) + if err != nil { + return Vote{}, ErrVote + } + voterBytes, err := r.Fixed(32) + if err != nil { + return Vote{}, ErrVote + } + publicKey, err := r.Bytes(65) + if err != nil || len(publicKey) != 65 { + return Vote{}, ErrVote + } + signature, err := r.Bytes(64) + if err != nil || len(signature) != 64 || r.Done() != nil { + return Vote{}, ErrVote + } + var network types.NetworkID + var headerHash types.Hash + var voter types.ValidatorID + copy(network[:], networkBytes) + copy(headerHash[:], headerBytes) + copy(voter[:], voterBytes) + if types.IsZero32([32]byte(network)) || types.IsZero32([32]byte(headerHash)) || types.IsZero32([32]byte(voter)) { + return Vote{}, ErrVote + } + return Vote{Network: network, Height: height, Round: round, HeaderHash: headerHash, Voter: voter, PublicKey: publicKey, Signature: signature}, nil +} + +func (c Certificate) MarshalBinary() ([]byte, error) { + if len(c.Votes) == 0 || len(c.Votes) > MaxCertificateVotes || c.Height == 0 || types.IsZero32([32]byte(c.Network)) || types.IsZero32([32]byte(c.HeaderHash)) { + return nil, ErrCertificate + } + var w codec.Writer + w.Fixed(c.Network[:]) + w.U64(c.Height) + w.U64(c.Round) + w.Fixed(c.HeaderHash[:]) + w.U32(uint32(len(c.Votes))) + for _, vote := range c.Votes { + raw, err := vote.MarshalBinary() + if err != nil { + return nil, err + } + w.Bytes(raw) + } + return w.BytesCopy(), nil +} + +func ParseCertificate(data []byte) (Certificate, error) { + r := codec.NewReader(data) + networkBytes, err := r.Fixed(32) + if err != nil { + return Certificate{}, ErrCertificate + } + height, err := r.U64() + if err != nil || height == 0 { + return Certificate{}, ErrCertificate + } + round, err := r.U64() + if err != nil { + return Certificate{}, ErrCertificate + } + headerBytes, err := r.Fixed(32) + if err != nil { + return Certificate{}, ErrCertificate + } + count, err := r.U32() + if err != nil || count == 0 || count > MaxCertificateVotes { + return Certificate{}, ErrCertificate + } + certificate := Certificate{Height: height, Round: round, Votes: make([]Vote, int(count))} + copy(certificate.Network[:], networkBytes) + copy(certificate.HeaderHash[:], headerBytes) + for i := range certificate.Votes { + voteBytes, err := r.Bytes(512) + if err != nil { + return Certificate{}, ErrCertificate + } + certificate.Votes[i], err = ParseVote(voteBytes) + if err != nil { + return Certificate{}, err + } + } + if r.Done() != nil || types.IsZero32([32]byte(certificate.Network)) || types.IsZero32([32]byte(certificate.HeaderHash)) { + return Certificate{}, ErrCertificate + } + return certificate, nil +} From c92d99a5e646155d64a04ecb26d59a0641db07ac Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:49:16 +0200 Subject: [PATCH 024/274] add canonical Merkle proof wire format for Citizen nodes --- internal/v2/merkle/wire.go | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 internal/v2/merkle/wire.go diff --git a/internal/v2/merkle/wire.go b/internal/v2/merkle/wire.go new file mode 100644 index 00000000..ad3ebcbd --- /dev/null +++ b/internal/v2/merkle/wire.go @@ -0,0 +1,45 @@ +package merkle + +import ( + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func (p Proof) MarshalBinary() []byte { + var w codec.Writer + w.U32(p.Index) + w.U32(p.LeafCount) + w.U32(uint32(len(p.Siblings))) + for _, sibling := range p.Siblings { + w.Fixed(sibling[:]) + } + return w.BytesCopy() +} + +func ParseProof(data []byte) (Proof, error) { + r := codec.NewReader(data) + index, err := r.U32() + if err != nil { + return Proof{}, ErrIndex + } + leafCount, err := r.U32() + if err != nil || leafCount == 0 || index >= leafCount { + return Proof{}, ErrIndex + } + count, err := r.U32() + if err != nil || count > 32 { + return Proof{}, ErrIndex + } + proof := Proof{Index: index, LeafCount: leafCount, Siblings: make([]types.Hash, int(count))} + for i := range proof.Siblings { + raw, err := r.Fixed(32) + if err != nil { + return Proof{}, ErrIndex + } + copy(proof.Siblings[i][:], raw) + } + if r.Done() != nil { + return Proof{}, ErrIndex + } + return proof, nil +} From 5a2f4360fdb37b917a28d78dec48dca0d25a489e Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:49:41 +0200 Subject: [PATCH 025/274] add self-verifiable Citizen Node light proof API --- internal/v2/lightapi/server.go | 191 +++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 internal/v2/lightapi/server.go diff --git a/internal/v2/lightapi/server.go b/internal/v2/lightapi/server.go new file mode 100644 index 00000000..9882b23f --- /dev/null +++ b/internal/v2/lightapi/server.go @@ -0,0 +1,191 @@ +package lightapi + +import ( + "encoding/hex" + "encoding/json" + "errors" + "net/http" + "strconv" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +var ErrSnapshot = errors.New("invalid finalized v2 light snapshot") + +type Snapshot struct { + Header sharding.GlobalHeader + Certificate v2consensus.Certificate + Commitments []sharding.Commitment + Validators v2consensus.ValidatorSet +} + +func (s Snapshot) Validate() error { + if s.Header.Network != s.Validators.Network || s.Certificate.Network != s.Header.Network || + s.Certificate.Height != s.Header.Height || s.Certificate.HeaderHash != v2consensus.HeaderConsensusHash(s.Header) || + s.Header.CertificateHash != s.Certificate.Hash() { + return ErrSnapshot + } + if err := s.Validators.VerifyCertificate(s.Certificate); err != nil { + return ErrSnapshot + } + root, err := sharding.CommitmentRoot(s.Commitments) + if err != nil || root != s.Header.ShardCommitmentRoot { + return ErrSnapshot + } + return nil +} + +type Provider interface { + LatestSnapshot() (Snapshot, error) + ShardState(shardID uint32) (worldstate.Backend, bool) +} + +type Server struct { + Provider Provider +} + +type validatorDTO struct { + ID string `json:"id"` + PublicKey []byte `json:"publicKey"` + Power uint64 `json:"power"` +} + +type statusResponse struct { + Network string `json:"network"` + Height uint64 `json:"height"` + Header []byte `json:"header"` + Certificate []byte `json:"certificate"` + Validators []validatorDTO `json:"validators"` +} + +type objectProofResponse struct { + Network string `json:"network"` + Height uint64 `json:"height"` + ShardID uint32 `json:"shardId"` + Header []byte `json:"header"` + Certificate []byte `json:"certificate"` + Commitment []byte `json:"commitment"` + CommitmentProof []byte `json:"commitmentProof"` + ObjectID string `json:"objectId"` + ObjectPresent bool `json:"objectPresent"` + Object []byte `json:"object,omitempty"` + StateProof []byte `json:"stateProof"` + Validators []validatorDTO `json:"validators"` +} + +func (s Server) Handler() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/v2/light/status", s.handleStatus) + mux.HandleFunc("/v2/light/object", s.handleObject) + return mux +} + +func (s Server) handleStatus(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeError(w, http.StatusMethodNotAllowed, "method_not_allowed") + return + } + snapshot, err := s.snapshot() + if err != nil { + writeError(w, http.StatusServiceUnavailable, "snapshot_unavailable") + return + } + certificate, err := snapshot.Certificate.MarshalBinary() + if err != nil { + writeError(w, http.StatusServiceUnavailable, "snapshot_unavailable") + return + } + writeJSON(w, http.StatusOK, statusResponse{ + Network: snapshot.Header.Network.String(), Height: snapshot.Header.Height, + Header: snapshot.Header.CanonicalBytes(), Certificate: certificate, + Validators: validatorList(snapshot.Validators), + }) +} + +func (s Server) handleObject(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeError(w, http.StatusMethodNotAllowed, "method_not_allowed") + return + } + shardValue := r.URL.Query().Get("shard") + objectValue := r.URL.Query().Get("id") + shard64, err := strconv.ParseUint(shardValue, 10, 32) + if err != nil || len(objectValue) != 64 { + writeError(w, http.StatusBadRequest, "invalid_query") + return + } + objectBytes, err := hex.DecodeString(objectValue) + if err != nil || len(objectBytes) != 32 { + writeError(w, http.StatusBadRequest, "invalid_object_id") + return + } + var objectID types.ObjectID + copy(objectID[:], objectBytes) + shardID := uint32(shard64) + + snapshot, err := s.snapshot() + if err != nil { + writeError(w, http.StatusServiceUnavailable, "snapshot_unavailable") + return + } + commitment, commitmentProof, err := sharding.CommitmentProof(snapshot.Commitments, shardID) + if err != nil { + writeError(w, http.StatusNotFound, "shard_not_found") + return + } + store, ok := s.Provider.ShardState(shardID) + if !ok || store == nil || store.Root() != commitment.StateRoot { + writeError(w, http.StatusServiceUnavailable, "shard_state_unavailable") + return + } + obj, proof, present := store.Proof(objectID) + certificate, err := snapshot.Certificate.MarshalBinary() + if err != nil { + writeError(w, http.StatusServiceUnavailable, "snapshot_unavailable") + return + } + response := objectProofResponse{ + Network: snapshot.Header.Network.String(), Height: snapshot.Header.Height, ShardID: shardID, + Header: snapshot.Header.CanonicalBytes(), Certificate: certificate, + Commitment: commitment.CanonicalBytes(), CommitmentProof: commitmentProof.MarshalBinary(), + ObjectID: objectID.String(), ObjectPresent: present, StateProof: proof.MarshalBinary(), + Validators: validatorList(snapshot.Validators), + } + if present { + response.Object = obj.CanonicalBytes() + } + writeJSON(w, http.StatusOK, response) +} + +func (s Server) snapshot() (Snapshot, error) { + if s.Provider == nil { + return Snapshot{}, ErrSnapshot + } + snapshot, err := s.Provider.LatestSnapshot() + if err != nil || snapshot.Validate() != nil { + return Snapshot{}, ErrSnapshot + } + return snapshot, nil +} + +func validatorList(set v2consensus.ValidatorSet) []validatorDTO { + out := make([]validatorDTO, len(set.Validators)) + for i, validator := range set.Validators { + out[i] = validatorDTO{ID: validator.ID.String(), PublicKey: append([]byte(nil), validator.PublicKey...), Power: validator.Power} + } + return out +} + +func writeJSON(w http.ResponseWriter, status int, value any) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(value) +} + +func writeError(w http.ResponseWriter, status int, code string) { + writeJSON(w, status, map[string]string{"error": code}) +} From 20800b017003a05125ae69c9760e37b36b5da9cb Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:50:00 +0200 Subject: [PATCH 026/274] test Citizen light API proof bundle --- internal/v2/lightapi/server_test.go | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 internal/v2/lightapi/server_test.go diff --git a/internal/v2/lightapi/server_test.go b/internal/v2/lightapi/server_test.go new file mode 100644 index 00000000..9ff0905c --- /dev/null +++ b/internal/v2/lightapi/server_test.go @@ -0,0 +1,89 @@ +package lightapi + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "net/http" + "net/http/httptest" + "testing" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +type fakeProvider struct { + snapshot Snapshot + state worldstate.Backend +} + +func (f fakeProvider) LatestSnapshot() (Snapshot, error) { return f.snapshot, nil } +func (f fakeProvider) ShardState(shardID uint32) (worldstate.Backend, bool) { + return f.state, shardID == 0 +} + +func TestLightObjectEndpointReturnsVerifiableBundle(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("light-api"))) + owner := types.AccountIDFromPublicKey([]byte("owner")) + token := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + id := types.ObjectIDFromTransaction(types.HashBytes("seed", []byte("coin")), 0) + out, _ := object.NewCoinOutput(owner, token, 100) + obj := object.Object{ID: id, Version: 1, Owner: owner, Kind: out.Kind, Data: out.Data} + store := worldstate.NewMemory() + root, err := store.Apply(nil, []object.Object{obj}) + if err != nil { + t.Fatal(err) + } + commitment := sharding.Commitment{ShardID: 0, StateRoot: root, ReceiptRoot: merkle.Root(nil), DataRoot: merkle.Root(nil)} + commitmentRoot, err := sharding.CommitmentRoot([]sharding.Commitment{commitment}) + if err != nil { + t.Fatal(err) + } + validatorKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), validatorKey.PublicKey.X, validatorKey.PublicKey.Y) + validatorID := types.ValidatorIDFromPublicKey(pub) + validators := v2consensus.ValidatorSet{Network: network, Validators: []v2consensus.Validator{{ID: validatorID, PublicKey: pub, Power: 10}}} + header := sharding.GlobalHeader{Version: 2, Network: network, Height: 1, ShardCommitmentRoot: commitmentRoot, ValidatorRoot: types.HashBytes("validators", []byte("root")), DataRoot: merkle.Root(nil)} + proposal, err := v2consensus.SignProposal(validatorKey, header, 0) + if err != nil { + t.Fatal(err) + } + headerHash := v2consensus.HeaderConsensusHash(header) + vote, err := v2consensus.SignVote(validatorKey, network, 1, 0, headerHash) + if err != nil { + t.Fatal(err) + } + certificate, err := validators.BuildCertificate(proposal, []v2consensus.Vote{vote}) + if err != nil { + t.Fatal(err) + } + header.CertificateHash = certificate.Hash() + provider := fakeProvider{snapshot: Snapshot{Header: header, Certificate: certificate, Commitments: []sharding.Commitment{commitment}, Validators: validators}, state: store} + server := Server{Provider: provider}.Handler() + + req := httptest.NewRequest(http.MethodGet, "/v2/light/object?shard=0&id="+id.String(), nil) + res := httptest.NewRecorder() + server.ServeHTTP(res, req) + if res.Code != http.StatusOK { + t.Fatalf("unexpected status %d: %s", res.Code, res.Body.String()) + } + if body := res.Body.String(); len(body) < 100 || !contains(body, id.String()) { + t.Fatalf("proof bundle missing object identity: %s", body) + } +} + +func contains(value, needle string) bool { + for i := 0; i+len(needle) <= len(value); i++ { + if value[i:i+len(needle)] == needle { + return true + } + } + return false +} From 32a81bb142f1b9b5b6ab2d130551a7ce63c96b92 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:50:46 +0200 Subject: [PATCH 027/274] preserve uint64 validator power exactly in Citizen API --- internal/v2/lightapi/dto.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 internal/v2/lightapi/dto.go diff --git a/internal/v2/lightapi/dto.go b/internal/v2/lightapi/dto.go new file mode 100644 index 00000000..a0d96d86 --- /dev/null +++ b/internal/v2/lightapi/dto.go @@ -0,0 +1,16 @@ +package lightapi + +import ( + "encoding/json" + "strconv" +) + +func (v validatorDTO) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + ID string `json:"id"` + PublicKey []byte `json:"publicKey"` + Power string `json:"power"` + }{ + ID: v.ID, PublicKey: v.PublicKey, Power: strconv.FormatUint(v.Power, 10), + }) +} From cc0eff2ecd02e636c57c87691804d3cdf45d60d0 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:51:51 +0200 Subject: [PATCH 028/274] implement in-wallet v2 Citizen cryptographic verifier --- apps/wallet/src/lib/v2Citizen.ts | 550 +++++++++++++++++++++++++++++++ 1 file changed, 550 insertions(+) create mode 100644 apps/wallet/src/lib/v2Citizen.ts diff --git a/apps/wallet/src/lib/v2Citizen.ts b/apps/wallet/src/lib/v2Citizen.ts new file mode 100644 index 00000000..cb27a2ed --- /dev/null +++ b/apps/wallet/src/lib/v2Citizen.ts @@ -0,0 +1,550 @@ +export interface CitizenValidatorDTO { + id: string + publicKey: string + power: string +} + +export interface CitizenObjectBundle { + network: string + height: number + shardId: number + header: string + certificate: string + commitment: string + commitmentProof: string + objectId: string + objectPresent: boolean + object?: string + stateProof: string + validators: CitizenValidatorDTO[] +} + +export interface VerifiedCitizenObject { + network: string + height: bigint + shardId: number + objectId: string + objectPresent: boolean + objectBytes?: Uint8Array + stateRoot: string +} + +export interface CitizenPowerState { + batteryPercent: number + charging: boolean + wifi: boolean + lowPower: boolean + appActive: boolean +} + +export interface CitizenMode { + verifyHeaders: boolean + relay: boolean + sampleDA: boolean + executeRecent: boolean + serveCache: boolean +} + +const P256_ORDER = BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551') +const P256_HALF_ORDER = P256_ORDER >> 1n +const textEncoder = new TextEncoder() + +class BinaryReader { + private offset = 0 + + constructor(private readonly data: Uint8Array) {} + + u8(): number { + return this.take(1)[0] + } + + u16(): number { + const value = this.take(2) + return (value[0] << 8) | value[1] + } + + u32(): number { + const value = this.take(4) + return ((value[0] * 0x1000000) + (value[1] << 16) + (value[2] << 8) + value[3]) >>> 0 + } + + u64(): bigint { + const value = this.take(8) + let out = 0n + for (const byte of value) out = (out << 8n) | BigInt(byte) + return out + } + + fixed(length: number): Uint8Array { + return this.take(length) + } + + bytes(max: number): Uint8Array { + const length = this.u32() + if (length > max) throw new Error('Citizen proof field exceeds limit') + return this.take(length) + } + + done(): void { + if (this.offset !== this.data.length) throw new Error('Citizen proof has trailing data') + } + + private take(length: number): Uint8Array { + if (length < 0 || this.offset + length > this.data.length) throw new Error('Citizen proof is truncated') + const out = this.data.slice(this.offset, this.offset + length) + this.offset += length + return out + } +} + +class BinaryWriter { + private readonly chunks: Uint8Array[] = [] + + u32(value: number): void { + this.chunks.push(new Uint8Array([(value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff])) + } + + u64(value: bigint): void { + const out = new Uint8Array(8) + let remaining = value + for (let i = 7; i >= 0; i--) { + out[i] = Number(remaining & 0xffn) + remaining >>= 8n + } + this.chunks.push(out) + } + + fixed(value: Uint8Array): void { + this.chunks.push(value) + } + + bytes(value: Uint8Array): void { + this.u32(value.length) + this.fixed(value) + } + + result(): Uint8Array { + return concatBytes(...this.chunks) + } +} + +interface ParsedHeader { + raw: Uint8Array + network: Uint8Array + height: bigint + shardCommitmentRoot: Uint8Array + certificateHash: Uint8Array +} + +interface ParsedCommitment { + raw: Uint8Array + shardId: number + stateRoot: Uint8Array + receiptRoot: Uint8Array + dataRoot: Uint8Array +} + +interface MerkleProof { + index: number + leafCount: number + siblings: Uint8Array[] +} + +interface StateProof { + exists: boolean + bitmap: Uint8Array + siblings: Uint8Array[] +} + +interface ParsedVote { + network: Uint8Array + height: bigint + round: bigint + headerHash: Uint8Array + voter: Uint8Array + publicKey: Uint8Array + signature: Uint8Array +} + +interface ParsedCertificate { + network: Uint8Array + height: bigint + round: bigint + headerHash: Uint8Array + votes: ParsedVote[] +} + +export function selectCitizenMode(power: CitizenPowerState): CitizenMode { + const mode: CitizenMode = { verifyHeaders: true, relay: false, sampleDA: false, executeRecent: false, serveCache: false } + if (power.lowPower || power.batteryPercent < 15) return mode + if (power.appActive) mode.relay = true + if (power.wifi && power.batteryPercent >= 30) { + mode.sampleDA = true + mode.serveCache = power.appActive + } + if (power.wifi && power.charging && power.batteryPercent >= 50) { + mode.executeRecent = true + mode.serveCache = true + } + return mode +} + +export async function fetchAndVerifyCitizenObject(baseURL: string, shardId: number, objectId: string): Promise { + const endpoint = new URL('/v2/light/object', normalizeBaseURL(baseURL)) + endpoint.searchParams.set('shard', String(shardId)) + endpoint.searchParams.set('id', objectId) + const response = await fetch(endpoint) + if (!response.ok) throw new Error(`Citizen proof request failed (${response.status})`) + return verifyCitizenObjectBundle(await response.json() as CitizenObjectBundle) +} + +export async function verifyCitizenObjectBundle(bundle: CitizenObjectBundle): Promise { + const header = parseHeader(base64ToBytes(bundle.header)) + const commitment = parseCommitment(base64ToBytes(bundle.commitment)) + const commitmentProof = parseMerkleProof(base64ToBytes(bundle.commitmentProof)) + const certificate = parseCertificate(base64ToBytes(bundle.certificate)) + + if (bundle.network.toLowerCase() !== bytesToHex(header.network) || bundle.shardId !== commitment.shardId) { + throw new Error('Citizen bundle network or shard mismatch') + } + await verifyFinality(header, certificate, bundle.validators) + + const commitmentLeaf = await merkleLeaf('shard-commitment', commitment.raw) + if (!await verifyMerkle(header.shardCommitmentRoot, commitmentLeaf, commitmentProof)) { + throw new Error('Shard commitment is not included in finalized header') + } + + const objectId = hexToBytes(bundle.objectId) + if (objectId.length !== 32) throw new Error('Invalid Citizen object ID') + const stateProof = parseStateProof(base64ToBytes(bundle.stateProof)) + if (stateProof.exists !== bundle.objectPresent) throw new Error('Object presence does not match state proof') + + let objectBytes: Uint8Array | undefined + let value: Uint8Array | undefined + if (bundle.objectPresent) { + if (!bundle.object) throw new Error('Citizen bundle omitted present object') + objectBytes = base64ToBytes(bundle.object) + if (objectBytes.length < 32 || !equalBytes(objectBytes.slice(0, 32), objectId)) throw new Error('Citizen object identity mismatch') + value = await domainHash('zephyr/object/v2', objectBytes) + } + if (!await verifySparseMerkle(commitment.stateRoot, objectId, value, stateProof)) { + throw new Error('Object Sparse-Merkle proof is invalid') + } + + return { + network: bytesToHex(header.network), height: header.height, shardId: commitment.shardId, + objectId: bytesToHex(objectId), objectPresent: bundle.objectPresent, objectBytes, + stateRoot: bytesToHex(commitment.stateRoot) + } +} + +async function verifyFinality(header: ParsedHeader, certificate: ParsedCertificate, validators: CitizenValidatorDTO[]): Promise { + const headerConsensusBytes = header.raw.slice() + headerConsensusBytes.fill(0, headerConsensusBytes.length - 32) + const headerHash = await domainHash('zephyr/global-header-consensus/v2', headerConsensusBytes) + if (!equalBytes(certificate.network, header.network) || certificate.height !== header.height || !equalBytes(certificate.headerHash, headerHash)) { + throw new Error('Citizen certificate does not target header') + } + + const validatorMap = new Map() + let totalPower = 0n + for (const validator of validators) { + const id = validator.id.toLowerCase() + if (validatorMap.has(id)) throw new Error('Duplicate validator in Citizen bundle') + const publicKey = base64ToBytes(validator.publicKey) + const derived = await domainHash('zephyr/validator-id/v2', publicKey) + if (bytesToHex(derived) !== id) throw new Error('Validator identity does not match public key') + const power = BigInt(validator.power) + if (power <= 0n) throw new Error('Invalid validator voting power') + totalPower += power + validatorMap.set(id, { publicKey, power }) + } + if (totalPower <= 0n) throw new Error('Citizen bundle has no validator power') + + let signedPower = 0n + const seen = new Set() + for (const vote of certificate.votes) { + if (!equalBytes(vote.network, certificate.network) || vote.height !== certificate.height || vote.round !== certificate.round || !equalBytes(vote.headerHash, certificate.headerHash)) { + throw new Error('Certificate contains vote for another target') + } + const voter = bytesToHex(vote.voter) + if (seen.has(voter)) throw new Error('Certificate contains duplicate validator vote') + seen.add(voter) + const validator = validatorMap.get(voter) + if (!validator || !equalBytes(validator.publicKey, vote.publicKey)) throw new Error('Certificate vote is not from active validator set') + if (!isCanonicalLowS(vote.signature) || !await verifyVoteSignature(vote)) throw new Error('Certificate contains invalid vote signature') + signedPower += validator.power + } + const quorum = (totalPower * 2n) / 3n + 1n + if (signedPower < quorum) throw new Error('Certificate is below 2/3+ quorum') + + const certificateHash = await hashCertificate(certificate) + if (!equalBytes(certificateHash, header.certificateHash)) throw new Error('Header certificate hash mismatch') +} + +async function verifyVoteSignature(vote: ParsedVote): Promise { + const body = new BinaryWriter() + body.fixed(vote.network) + body.u64(vote.height) + body.u64(vote.round) + body.fixed(vote.headerHash) + body.fixed(vote.voter) + const signingPayload = domainFrame('zephyr/consensus/vote/v2', body.result()) + try { + const key = await crypto.subtle.importKey('raw', toArrayBuffer(vote.publicKey), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']) + return crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, toArrayBuffer(vote.signature), toArrayBuffer(signingPayload)) + } catch { + return false + } +} + +async function hashCertificate(certificate: ParsedCertificate): Promise { + const votes = [...certificate.votes].sort((a, b) => compareBytes(a.voter, b.voter)) + const writer = new BinaryWriter() + writer.fixed(certificate.network) + writer.u64(certificate.height) + writer.u64(certificate.round) + writer.fixed(certificate.headerHash) + writer.u32(votes.length) + for (const vote of votes) { + writer.fixed(vote.voter) + writer.bytes(vote.publicKey) + writer.bytes(vote.signature) + } + return domainHash('zephyr/quorum-certificate/v2', writer.result()) +} + +function parseHeader(raw: Uint8Array): ParsedHeader { + const reader = new BinaryReader(raw) + if (reader.u16() !== 2) throw new Error('Unsupported Zephyr header version') + const network = reader.fixed(32) + const height = reader.u64() + reader.fixed(32) + const shardCommitmentRoot = reader.fixed(32) + reader.fixed(32) + reader.fixed(32) + const certificateHash = reader.fixed(32) + reader.done() + if (height === 0n) throw new Error('Invalid finalized height') + return { raw, network, height, shardCommitmentRoot, certificateHash } +} + +function parseCommitment(raw: Uint8Array): ParsedCommitment { + const reader = new BinaryReader(raw) + const shardId = reader.u32() + const stateRoot = reader.fixed(32) + const receiptRoot = reader.fixed(32) + const dataRoot = reader.fixed(32) + reader.done() + return { raw, shardId, stateRoot, receiptRoot, dataRoot } +} + +function parseMerkleProof(raw: Uint8Array): MerkleProof { + const reader = new BinaryReader(raw) + const index = reader.u32() + const leafCount = reader.u32() + const count = reader.u32() + if (leafCount === 0 || index >= leafCount || count > 32) throw new Error('Invalid Merkle proof') + const siblings: Uint8Array[] = [] + for (let i = 0; i < count; i++) siblings.push(reader.fixed(32)) + reader.done() + return { index, leafCount, siblings } +} + +function parseStateProof(raw: Uint8Array): StateProof { + const reader = new BinaryReader(raw) + const existsRaw = reader.u8() + if (existsRaw !== 0 && existsRaw !== 1) throw new Error('Invalid state proof existence bit') + const bitmap = reader.fixed(32) + const count = reader.u16() + if (count > 256) throw new Error('Invalid state proof sibling count') + const siblings: Uint8Array[] = [] + for (let i = 0; i < count; i++) siblings.push(reader.fixed(32)) + reader.done() + let bits = 0 + for (let i = 0; i < 256; i++) if (bitmapBit(bitmap, i)) bits++ + if (bits !== siblings.length) throw new Error('State proof bitmap does not match siblings') + return { exists: existsRaw === 1, bitmap, siblings } +} + +function parseCertificate(raw: Uint8Array): ParsedCertificate { + const reader = new BinaryReader(raw) + const network = reader.fixed(32) + const height = reader.u64() + const round = reader.u64() + const headerHash = reader.fixed(32) + const count = reader.u32() + if (height === 0n || count === 0 || count > 4096) throw new Error('Invalid quorum certificate') + const votes: ParsedVote[] = [] + for (let i = 0; i < count; i++) votes.push(parseVote(reader.bytes(512))) + reader.done() + return { network, height, round, headerHash, votes } +} + +function parseVote(raw: Uint8Array): ParsedVote { + const reader = new BinaryReader(raw) + const network = reader.fixed(32) + const height = reader.u64() + const round = reader.u64() + const headerHash = reader.fixed(32) + const voter = reader.fixed(32) + const publicKey = reader.bytes(65) + const signature = reader.bytes(64) + reader.done() + if (height === 0n || publicKey.length !== 65 || signature.length !== 64) throw new Error('Invalid quorum vote') + return { network, height, round, headerHash, voter, publicKey, signature } +} + +async function verifyMerkle(root: Uint8Array, leaf: Uint8Array, proof: MerkleProof): Promise { + let target = 1 + while (target < proof.leafCount) target <<= 1 + let requiredDepth = 0 + for (let n = target; n > 1; n >>= 1) requiredDepth++ + if (proof.siblings.length !== requiredDepth) return false + let current = leaf + let position = proof.index + for (const sibling of proof.siblings) { + current = position % 2 === 0 ? await merkleBranch(current, sibling) : await merkleBranch(sibling, current) + position = Math.floor(position / 2) + } + return equalBytes(current, root) +} + +let defaultsPromise: Promise | undefined + +async function sparseDefaults(): Promise { + if (!defaultsPromise) { + defaultsPromise = (async () => { + const defaults = new Array(257) + defaults[256] = await domainHash('zephyr/smt/empty-leaf/v2', new Uint8Array()) + for (let depth = 255; depth >= 0; depth--) defaults[depth] = await smtBranch(defaults[depth + 1], defaults[depth + 1]) + return defaults + })() + } + return defaultsPromise +} + +async function verifySparseMerkle(root: Uint8Array, key: Uint8Array, value: Uint8Array | undefined, proof: StateProof): Promise { + if (proof.exists !== (value !== undefined) || key.length !== 32) return false + const defaults = await sparseDefaults() + let current = proof.exists && value ? await smtLeaf(key, value) : defaults[256] + let siblingIndex = 0 + for (let i = 0; i < 256; i++) { + const depth = 256 - i + let sibling = defaults[depth] + if (bitmapBit(proof.bitmap, i)) { + if (siblingIndex >= proof.siblings.length) return false + sibling = proof.siblings[siblingIndex++] + } + const bitIndex = depth - 1 + const byteIndex = Math.floor(bitIndex / 8) + const shift = 7 - (bitIndex % 8) + const bit = (key[byteIndex] >> shift) & 1 + current = bit === 0 ? await smtBranch(current, sibling) : await smtBranch(sibling, current) + } + return siblingIndex === proof.siblings.length && equalBytes(current, root) +} + +async function merkleLeaf(domain: string, payload: Uint8Array): Promise { + return domainHash(`zephyr/merkle/leaf/v2/${domain}`, payload) +} + +async function merkleBranch(left: Uint8Array, right: Uint8Array): Promise { + return domainHash('zephyr/merkle/branch/v2', concatBytes(left, right)) +} + +async function smtLeaf(key: Uint8Array, value: Uint8Array): Promise { + const writer = new BinaryWriter() + writer.fixed(key) + writer.bytes(value) + return domainHash('zephyr/smt/leaf/v2', writer.result()) +} + +async function smtBranch(left: Uint8Array, right: Uint8Array): Promise { + return domainHash('zephyr/smt/branch/v2', concatBytes(left, right)) +} + +async function domainHash(domain: string, payload: Uint8Array): Promise { + return sha256(domainFrame(domain, payload)) +} + +function domainFrame(domain: string, payload: Uint8Array): Uint8Array { + const writer = new BinaryWriter() + writer.bytes(textEncoder.encode(domain)) + writer.bytes(payload) + return writer.result() +} + +async function sha256(value: Uint8Array): Promise { + return new Uint8Array(await crypto.subtle.digest('SHA-256', toArrayBuffer(value))) +} + +function bitmapBit(bitmap: Uint8Array, index: number): boolean { + return (bitmap[Math.floor(index / 8)] & (1 << (index % 8))) !== 0 +} + +function isCanonicalLowS(signature: Uint8Array): boolean { + if (signature.length !== 64) return false + const r = bytesToBigInt(signature.slice(0, 32)) + const s = bytesToBigInt(signature.slice(32)) + return r > 0n && s > 0n && r < P256_ORDER && s <= P256_HALF_ORDER +} + +function bytesToBigInt(value: Uint8Array): bigint { + let out = 0n + for (const byte of value) out = (out << 8n) | BigInt(byte) + return out +} + +function normalizeBaseURL(value: string): string { + const url = new URL(value, window.location.origin) + if (!url.pathname.endsWith('/')) url.pathname += '/' + return url.toString() +} + +function base64ToBytes(value: string): Uint8Array { + const raw = atob(value) + const out = new Uint8Array(raw.length) + for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i) + return out +} + +function hexToBytes(value: string): Uint8Array { + if (!/^[0-9a-fA-F]*$/.test(value) || value.length % 2 !== 0) throw new Error('Invalid hex') + const out = new Uint8Array(value.length / 2) + for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(value.slice(i * 2, i * 2 + 2), 16) + return out +} + +function bytesToHex(value: Uint8Array): string { + return Array.from(value, byte => byte.toString(16).padStart(2, '0')).join('') +} + +function equalBytes(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false + let diff = 0 + for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i] + return diff === 0 +} + +function compareBytes(a: Uint8Array, b: Uint8Array): number { + for (let i = 0; i < Math.min(a.length, b.length); i++) { + if (a[i] !== b[i]) return a[i] - b[i] + } + return a.length - b.length +} + +function concatBytes(...values: Uint8Array[]): Uint8Array { + const length = values.reduce((total, value) => total + value.length, 0) + const out = new Uint8Array(length) + let offset = 0 + for (const value of values) { + out.set(value, offset) + offset += value.length + } + return out +} + +function toArrayBuffer(value: Uint8Array): ArrayBuffer { + return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer +} From a400a414ee395cf3fceadb383654ae42fe8d4710 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:55:50 +0200 Subject: [PATCH 029/274] gofmt v2 consensus tests --- internal/v2/consensus/consensus_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/v2/consensus/consensus_test.go b/internal/v2/consensus/consensus_test.go index 7f201d65..d5133f14 100644 --- a/internal/v2/consensus/consensus_test.go +++ b/internal/v2/consensus/consensus_test.go @@ -34,8 +34,8 @@ func TestV2QuorumCertificateRequiresTwoThirdsPlus(t *testing.T) { header := sharding.GlobalHeader{ Version: 2, Network: network, Height: 1, ShardCommitmentRoot: types.HashBytes("shards", []byte("root")), - ValidatorRoot: types.HashBytes("validators", []byte("root")), - DataRoot: types.HashBytes("data", []byte("root")), + ValidatorRoot: types.HashBytes("validators", []byte("root")), + DataRoot: types.HashBytes("data", []byte("root")), } proposal, err := SignProposal(keys[proposer.ID], header, 0) if err != nil { From 0d91fe64a4f8110e69252e227c9bf128ecc68518 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:56:11 +0200 Subject: [PATCH 030/274] gofmt v2 contract metering test --- internal/v2/contracts/metered_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/v2/contracts/metered_test.go b/internal/v2/contracts/metered_test.go index 5d47f74e..c516d2e8 100644 --- a/internal/v2/contracts/metered_test.go +++ b/internal/v2/contracts/metered_test.go @@ -10,7 +10,7 @@ type fakeRuntime struct { result Result } -func (f fakeRuntime) ValidateModule(code []byte) error { return nil } +func (f fakeRuntime) ValidateModule(code []byte) error { return nil } func (f fakeRuntime) Execute(request Request) (Result, error) { return f.result, nil } func TestMeteredRuntimeEnforcesFuelAndAccess(t *testing.T) { From 246a36a85dc5a463df7fb3366a74cbd8d8279da8 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:56:40 +0200 Subject: [PATCH 031/274] gofmt v2 parallel execution test --- internal/v2/execution/parallel_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/v2/execution/parallel_test.go b/internal/v2/execution/parallel_test.go index a05a2738..f5fea551 100644 --- a/internal/v2/execution/parallel_test.go +++ b/internal/v2/execution/parallel_test.go @@ -45,7 +45,7 @@ func TestParallelBatchAppliesIndependentTransfers(t *testing.T) { h := proofObject.Hash() transaction := tx.Transaction{ Version: tx.Version, Network: network, ShardID: 0, StateRoot: root, - Inputs: []tx.InputRef{{ObjectID: input.ID, Version: input.Version, ObjectHash: h}}, + Inputs: []tx.InputRef{{ObjectID: input.ID, Version: input.Version, ObjectHash: h}}, Outputs: []object.OutputSpec{toRecipient, toChange}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, Fee: 1, Witnesses: []tx.Witness{{Object: proofObject, Proof: proof}}, } From 5ab3eb8fe322d2ba98ecc0e6af78a1f2e90215c4 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:57:22 +0200 Subject: [PATCH 032/274] gofmt v2 Citizen light server --- internal/v2/lightapi/server.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/v2/lightapi/server.go b/internal/v2/lightapi/server.go index 9882b23f..16b24098 100644 --- a/internal/v2/lightapi/server.go +++ b/internal/v2/lightapi/server.go @@ -62,17 +62,17 @@ type statusResponse struct { } type objectProofResponse struct { - Network string `json:"network"` - Height uint64 `json:"height"` - ShardID uint32 `json:"shardId"` - Header []byte `json:"header"` - Certificate []byte `json:"certificate"` - Commitment []byte `json:"commitment"` - CommitmentProof []byte `json:"commitmentProof"` - ObjectID string `json:"objectId"` - ObjectPresent bool `json:"objectPresent"` - Object []byte `json:"object,omitempty"` - StateProof []byte `json:"stateProof"` + Network string `json:"network"` + Height uint64 `json:"height"` + ShardID uint32 `json:"shardId"` + Header []byte `json:"header"` + Certificate []byte `json:"certificate"` + Commitment []byte `json:"commitment"` + CommitmentProof []byte `json:"commitmentProof"` + ObjectID string `json:"objectId"` + ObjectPresent bool `json:"objectPresent"` + Object []byte `json:"object,omitempty"` + StateProof []byte `json:"stateProof"` Validators []validatorDTO `json:"validators"` } From b0633200360d9437355f3c01badf9bcb7b977096 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:58:08 +0200 Subject: [PATCH 033/274] gofmt v2 runtime --- internal/v2/node/runtime.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/v2/node/runtime.go b/internal/v2/node/runtime.go index ddc27285..ac05fdc6 100644 --- a/internal/v2/node/runtime.go +++ b/internal/v2/node/runtime.go @@ -16,11 +16,11 @@ import ( ) var ( - ErrRuntimeConfig = errors.New("invalid v2 runtime configuration") - ErrCandidateHeight = errors.New("invalid v2 candidate height") - ErrCandidateState = errors.New("v2 candidate does not match committed state") - ErrCandidateCert = errors.New("v2 candidate certificate mismatch") - ErrStateSimulation = errors.New("v2 backend does not support state simulation") + ErrRuntimeConfig = errors.New("invalid v2 runtime configuration") + ErrCandidateHeight = errors.New("invalid v2 candidate height") + ErrCandidateState = errors.New("v2 candidate does not match committed state") + ErrCandidateCert = errors.New("v2 candidate certificate mismatch") + ErrStateSimulation = errors.New("v2 backend does not support state simulation") ) type ShardBatch struct { From d086054cbdf04e268b36a27ba786ac60ee1f2e23 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:59:20 +0200 Subject: [PATCH 034/274] gofmt v2 runtime test --- internal/v2/node/runtime_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/v2/node/runtime_test.go b/internal/v2/node/runtime_test.go index 2df31db3..9b85b119 100644 --- a/internal/v2/node/runtime_test.go +++ b/internal/v2/node/runtime_test.go @@ -42,7 +42,7 @@ func TestCandidateDoesNotMutateBeforeQCAndCommitsAfterQC(t *testing.T) { change, _ := object.NewCoinOutput(alice, native, 74) transaction := tx.Transaction{ Version: tx.Version, Network: network, ShardID: 0, StateRoot: root, - Inputs: []tx.InputRef{{ObjectID: inputID, Version: 1, ObjectHash: witnessHash}}, + Inputs: []tx.InputRef{{ObjectID: inputID, Version: 1, ObjectHash: witnessHash}}, Outputs: []object.OutputSpec{toBob, change}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, Fee: 1, Witnesses: []tx.Witness{{Object: witness, Proof: proof}}, } From f9646da3b1568f2ba0f24bb8be7fab9809b6d1dc Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:00:24 +0200 Subject: [PATCH 035/274] gofmt v2 consensus --- internal/v2/consensus/consensus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/v2/consensus/consensus.go b/internal/v2/consensus/consensus.go index 454dff36..7de7389a 100644 --- a/internal/v2/consensus/consensus.go +++ b/internal/v2/consensus/consensus.go @@ -111,7 +111,7 @@ func (s ValidatorSet) Proposer(height, round uint64) (Validator, error) { } validators := append([]Validator(nil), s.Validators...) sort.Slice(validators, func(i, j int) bool { return bytes.Compare(validators[i].ID[:], validators[j].ID[:]) < 0 }) - slot := ((height - 1) % total + (round % total)) % total + slot := ((height-1)%total + (round % total)) % total var cumulative uint64 for _, validator := range validators { cumulative += validator.Power From 5ab7acf066cd7d1d14e182f468d496a56c4303ef Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:02:43 +0200 Subject: [PATCH 036/274] add finalized v2 parallel execution benchmark --- internal/v2/node/benchmark_test.go | 147 +++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 internal/v2/node/benchmark_test.go diff --git a/internal/v2/node/benchmark_test.go b/internal/v2/node/benchmark_test.go new file mode 100644 index 00000000..03d86a9c --- /dev/null +++ b/internal/v2/node/benchmark_test.go @@ -0,0 +1,147 @@ +package node + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "fmt" + "testing" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +const benchmarkBatchSize = 32 + +type benchmarkSigner struct { + key *ecdsa.PrivateKey + account types.AccountID +} + +func BenchmarkV2FinalizedBatch32(b *testing.B) { + for _, workers := range []int{1, 4, 8, 16} { + b.Run(fmt.Sprintf("workers-%d", workers), func(b *testing.B) { + benchmarkFinalizedBatch(b, workers) + }) + } +} + +func benchmarkFinalizedBatch(b *testing.B, workers int) { + b.Helper() + b.ReportAllocs() + b.StopTimer() + + network := types.NetworkID(types.HashBytes("network", []byte("benchmark-v2"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + validatorRoot := types.HashBytes("validators", []byte("benchmark-set")) + validators, validatorKeys := benchmarkValidators(b, network, 7) + signers := make([]benchmarkSigner, benchmarkBatchSize) + for i := range signers { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + b.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + signers[i] = benchmarkSigner{key: key, account: types.AccountIDFromPublicKey(pub)} + } + recipient := types.AccountIDFromPublicKey([]byte("benchmark-recipient")) + + for iteration := 0; iteration < b.N; iteration++ { + store := worldstate.NewMemory() + inputs := make([]object.Object, benchmarkBatchSize) + for i, signer := range signers { + seed := types.HashBytes("benchmark-input", []byte(fmt.Sprintf("%d/%d", iteration, i))) + id := types.ObjectIDFromTransaction(seed, 0) + out, err := object.NewCoinOutput(signer.account, native, 100) + if err != nil { + b.Fatal(err) + } + inputs[i] = object.Object{ID: id, Version: 1, Owner: signer.account, Kind: out.Kind, Data: out.Data} + } + root, err := store.Apply(nil, inputs) + if err != nil { + b.Fatal(err) + } + transactions := make([]tx.Transaction, benchmarkBatchSize) + for i, signer := range signers { + witness, proof, ok := store.Proof(inputs[i].ID) + if !ok { + b.Fatal("missing benchmark witness") + } + witnessHash := witness.Hash() + payment, _ := object.NewCoinOutput(recipient, native, 25) + change, _ := object.NewCoinOutput(signer.account, native, 74) + transaction := tx.Transaction{ + Version: tx.Version, Network: network, ShardID: 0, StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: inputs[i].ID, Version: 1, ObjectHash: witnessHash}}, + Outputs: []object.OutputSpec{payment, change}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, + Fee: 1, Witnesses: []tx.Witness{{Object: witness, Proof: proof}}, + } + transaction.Salt[0] = byte(i + 1) + transaction.Salt[1] = byte(iteration) + if err := transaction.Sign(signer.key); err != nil { + b.Fatal(err) + } + transactions[i] = transaction + } + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: store}, workers) + if err != nil { + b.Fatal(err) + } + + b.StartTimer() + candidate, err := runtime.BuildCandidate(1, map[uint32]ShardBatch{0: {Transactions: transactions}}) + if err != nil { + b.Fatal(err) + } + proposer, err := validators.Proposer(1, 0) + if err != nil { + b.Fatal(err) + } + proposal, err := v2consensus.SignProposal(validatorKeys[proposer.ID], candidate.Header, 0) + if err != nil { + b.Fatal(err) + } + headerHash := v2consensus.HeaderConsensusHash(candidate.Header) + votes := make([]v2consensus.Vote, 0, 5) + for _, validator := range validators.Validators[:5] { + vote, err := v2consensus.SignVote(validatorKeys[validator.ID], network, 1, 0, headerHash) + if err != nil { + b.Fatal(err) + } + votes = append(votes, vote) + } + certificate, err := validators.BuildCertificate(proposal, votes) + if err != nil { + b.Fatal(err) + } + if _, err := runtime.Commit(candidate, certificate, validators); err != nil { + b.Fatal(err) + } + b.StopTimer() + } + + if elapsed := b.Elapsed().Seconds(); elapsed > 0 { + b.ReportMetric(float64(b.N*benchmarkBatchSize)/elapsed, "finalized-tx/s") + } +} + +func benchmarkValidators(b *testing.B, network types.NetworkID, count int) (v2consensus.ValidatorSet, map[types.ValidatorID]*ecdsa.PrivateKey) { + b.Helper() + set := v2consensus.ValidatorSet{Network: network, Validators: make([]v2consensus.Validator, 0, count)} + keys := make(map[types.ValidatorID]*ecdsa.PrivateKey, count) + for i := 0; i < count; i++ { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + b.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + id := types.ValidatorIDFromPublicKey(pub) + set.Validators = append(set.Validators, v2consensus.Validator{ID: id, PublicKey: pub, Power: 10_000}) + keys[id] = key + } + return set, keys +} From a98ade677075cdfad13e6623b0bb2e0dc500aa5e Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:03:09 +0200 Subject: [PATCH 037/274] benchmark v2 finalized batch scaling in CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16015f4d..290de30f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,9 @@ jobs: - name: P-256 verification baseline run: go test ./internal/api -run '^$' -bench '^BenchmarkLabP256TransactionVerification$' -benchtime=1s -count=1 + - name: V2 finalized batch scaling sample + run: go test ./internal/v2/node -run '^$' -bench '^BenchmarkV2FinalizedBatch32$' -benchtime=1x -count=1 -timeout=120s + wallet: name: Wallet build runs-on: ubuntu-latest From 564852fd40a38f5b86b1d07450eea04249cbff39 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:04:01 +0200 Subject: [PATCH 038/274] document executable v2 implementation status and remaining gates --- docs/protocol-v2-implementation-status.md | 273 ++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 docs/protocol-v2-implementation-status.md diff --git a/docs/protocol-v2-implementation-status.md b/docs/protocol-v2-implementation-status.md new file mode 100644 index 00000000..ec362941 --- /dev/null +++ b/docs/protocol-v2-implementation-status.md @@ -0,0 +1,273 @@ +# Zephyr Protocol v2 — Implementation Status + +This document tracks what the clean-break v2 branch **actually implements**, what is integrated only as a reference boundary, and what still requires production engineering. It complements `docs/protocol-v2.md`, which remains the architectural contract. + +Status legend: + +- **Implemented** — executable code and tests exist on the v2 branch. +- **Integrated foundation** — the protocol boundary and correctness rules exist, but a production backend/network/runtime is still to be selected or connected. +- **Not production-complete** — must not be presented as a shipped network capability yet. + +## Core identity and wire protocol + +**Implemented** + +- canonical bounded binary codec for consensus-critical v2 data; +- typed network/account/node/validator/object/token/contract/job identities; +- genesis-derived `NetworkID`; +- independent account, node and validator identities; +- canonical low-S P-256 signing for proof-carrying transactions and validator consensus messages; +- binary proposal, vote and quorum-certificate wire formats; +- binary global-header, shard-commitment, cross-shard-receipt and Merkle-proof decoders. + +The browser/RPC surface may use JSON, but consensus does not depend on JSON canonicalization. + +## Object state and persistence + +**Implemented** + +- proof-oriented object/coin model; +- 256-bit Sparse Merkle Tree with incremental updates; +- compressed inclusion/absence proofs; +- in-memory world-state backend; +- non-mutating state simulation through cloned Sparse Merkle state; +- durable v2 backend with append-only WAL, CRC32C records, monotonic sequence numbers, network binding, fsync, atomic checkpointing and replay; +- safe truncation of a torn WAL tail after a crash; +- rejection of persisted state from a different network. + +**Not production-complete** + +- long-duration database growth/compaction benchmarks; +- large-state migration/repair tooling; +- production structured-KV/LSM backend comparison; +- archive/history indexing. + +The WAL/checkpoint backend removes the v1 requirement to serialize the complete node state for every mutation, but it is still a first durable backend rather than the final storage engine selection. + +## Proof-carrying transactions and execution + +**Implemented** + +- P-256 signed proof-carrying transaction format; +- state-root-bound object witnesses; +- witness verification without requiring a full-state lookup for validity evidence; +- native ZPH/object transfers; +- protocol-native token creation; +- deterministic input/output conservation and fee checks; +- deterministic parallel batch executor; +- rejection of batches with shared consumed objects, duplicate transactions or different pre-state roots; +- atomic merge of independent transaction results; +- state-root simulation before consensus finality. + +The key invariant is enforced in code: candidate execution may calculate a future state root, but committed state is not mutated before a valid quorum certificate exists. + +## Consensus and global finality + +**Implemented** + +- v2 validator set with integer voting power; +- deterministic weighted proposer selection; +- domain-separated proposal and vote signatures; +- locally reconstructed `2/3+` voting-power quorum; +- duplicate-voter rejection; +- canonical quorum-certificate hash; +- `GlobalHeader` consensus hash that avoids certificate/hash circularity; +- runtime path: + +```text +proof-carrying transactions + -> parallel execution + -> state-root simulation + -> shard commitments + -> GlobalHeader + -> proposal + -> votes + -> quorum certificate + -> state commit +``` + +The existing v1 Consensus & Performance Lab remains a regression gate while v2-specific multi-node fault transport integration is expanded. + +## Sharding + +**Implemented foundation** + +- deterministic shard router; +- per-shard state/data/receipt commitments; +- global shard-commitment root; +- `GlobalHeader` committing all active shard roots; +- cross-shard receipt format; +- receipt Merkle batches and inclusion proofs; +- proof that a source receipt belongs to a shard commitment that belongs to a finalized global header; +- destination-shard validation and in-memory anti-replay tracker; +- runtime capable of simulating/committing multiple shard state backends. + +**Not production-complete** + +- output/object placement rules for active multi-shard execution need final clean-break encoding before `shardCount > 1` is enabled; +- receipt-consumption anti-replay must move from the in-memory tracker into consensus-critical durable state; +- shard-aware gossip/recovery is not connected to production transport; +- reshard/split/merge rules are not activated; +- 4/16-shard conformance and throughput evidence is still required. + +`shardCount = 1` remains the safe activation value until these conditions pass. Sharding is an optimization, not a prerequisite for correctness. + +## Citizen Node and smartphone wallet + +**Implemented** + +- Go Citizen verifier for headers/state/shard/data proofs; +- battery/network-aware participation policy; +- self-verifiable light API: + - `/v2/light/status`; + - `/v2/light/object`; +- proof bundle contains canonical global header, quorum certificate, validator set, shard commitment and Merkle proof, object bytes and Sparse-Merkle proof; +- validator voting power is encoded as decimal text at the JSON boundary to preserve full `uint64` precision in JavaScript; +- `apps/wallet/src/lib/v2Citizen.ts` independently reconstructs: + - v2 domain hashes; + - validator identities; + - low-S P-256 vote validation; + - exact `2/3+` quorum with `BigInt`; + - certificate hash; + - shard-commitment inclusion; + - object Sparse-Merkle inclusion/absence proof; +- wallet resource mode selection for header-only, relay, DA sampling/cache and opportunistic recent execution modes. + +**Not production-complete** + +- the Vue UI does not yet expose the Citizen status/control panel; +- current v1 node process does not yet mount a live v2 runtime/provider; +- iOS/Android native lifecycle/background adapters are not present; +- multi-peer proof comparison, resumable cache and peer relay are not connected yet; +- real-device RAM/battery/bandwidth measurements are still required. + +No correctness claim may depend on an RPC response that the Citizen verifier cannot authenticate against finalized state. + +## Smart contracts + +**Integrated foundation** + +- deterministic WASM deployment boundary; +- module magic/shape validation boundary; +- versioned contract deployment model; +- runtime interface independent from a concrete WASM engine; +- consensus guard enforcing: + - fuel limit; + - bounded arguments and return data; + - bounded event count/size; + - declared read/write object set; + - no write outside a declared write-enabled object; + - bounded state-access count. + +**Not production-complete** + +- production WASM interpreter/JIT selection; +- deterministic opcode/import policy; +- audited fuel schedule; +- contract deploy/call operations inside the main v2 transaction executor; +- Rust SDK/ABI tooling; +- contract conformance corpus. + +A concrete WASM runtime will not be called production-ready until deterministic metering survives cross-machine conformance testing. + +## Native distributed compute market + +**Implemented state-machine foundation** + +- compute provider offers; +- CPU/RAM/GPU/VRAM/storage/bandwidth/capability requirements; +- collateral requirements; +- job posting with escrow and deadline; +- deterministic offer/job IDs; +- matching and assignment; +- multi-provider assignment for replicated verification; +- provider result submission; +- settlement and unused-escrow refund; +- expiry; +- verification policies for: + - deterministic replay; + - replicated matching results; + - challenge evidence; + - zero-knowledge proof verification signal; + - TEE attestation verification signal; + - client approval; + - hybrid evidence. + +Heavy compute is provider-executed; validators verify settlement evidence and do not replay AI training, scientific simulations, rendering or other expensive workloads. + +**Not production-complete** + +- provider daemon/scheduler; +- input/output distribution protocol; +- on-chain object integration of market state transitions; +- actual collateral slashing/dispute arbitration; +- concrete ZK verifier integrations; +- concrete TEE attestation integrations; +- confidential-data key exchange; +- compute reputation and anti-collusion policy. + +## Data availability + +**Integrated foundation** + +- chunk commitments; +- sample proof verification boundary; +- DA root in shard/global commitments; +- Citizen participation mode for bounded sampling. + +**Not production-complete** + +- production erasure code selection; +- reconstruction; +- sampling confidence parameters; +- withholding attacks in the fault lab; +- shard-aware dissemination; +- mobile bandwidth/storage measurements. + +## Transport + +**Integrated foundation** + +- consensus transport, transaction relay and light-proof retrieval are separate logical interfaces; +- existing HTTP remains the reference/test transport boundary; +- the architecture permits libp2p/QUIC/WebTransport without changing consensus objects. + +**Not production-complete** + +- production libp2p/QUIC implementation; +- discovery/NAT traversal/mobile relay; +- shard-aware gossip; +- v2 fault-transport adapter covering the full existing Lab matrix. + +## Performance gates + +**Implemented** + +- existing finalized-through-consensus v1 Lab remains mandatory; +- v2 state/proof microbenchmarks; +- v2 finalized batch benchmark with 32 proof-carrying transfers, 7 validators and 1/4/8/16 execution workers; +- the timed v2 path includes witness/signature verification, execution/state-root simulation, proposal/votes, quorum certificate and committed state transition; +- client workload setup/key generation/signing stays outside the timed consensus path, matching the Lab's canonical workload policy. + +No numerical v2 TPS result from a shared CI runner is a production capacity claim. + +## Activation gates + +The clean break lets v2 replace prototype boundaries, but it does not remove the requirement to prove them. Before a public v2 devnet: + +1. v2 multi-validator consensus must run through the fault-injection Lab, including partitions/restarts/conflicting evidence; +2. durable v2 state must survive crash/restart and longer stress runs; +3. Citizen verification must be exercised against a live v2 node from real Android/iOS reference devices; +4. one-shard finalized performance must be characterized on controlled hardware; +5. multi-shard mode must remain disabled until object placement, durable receipt anti-replay, recovery and 4/16-shard conformance pass; +6. contract execution must have a deterministic metered production runtime; +7. compute settlement must be consensus-state-backed before real value is escrowed; +8. genesis/checkpoint/operator upgrade procedures must be explicit. + +The engineering rule remains: + +```text +more hardware -> more throughput +less hardware -> less throughput +less hardware -/-> weaker correctness +``` From 39348035481bc7932c94ca7991fb288004b6a578 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:05:09 +0200 Subject: [PATCH 039/274] encode permanent shard placement in v2 object IDs --- internal/v2/types/types.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/internal/v2/types/types.go b/internal/v2/types/types.go index f9213247..1eabc6b7 100644 --- a/internal/v2/types/types.go +++ b/internal/v2/types/types.go @@ -1,6 +1,7 @@ package types import ( + "encoding/binary" "encoding/hex" "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" @@ -47,11 +48,32 @@ func ValidatorIDFromPublicKey(publicKey []byte) ValidatorID { return ValidatorID(codec.DomainHash("zephyr/validator-id/v2", publicKey)) } -func ObjectIDFromTransaction(txID Hash, index uint32) ObjectID { +// AccountShard chooses the execution shard for newly created account-owned +// objects. The object ID itself permanently records the chosen shard so later +// shard-count changes cannot silently move an existing object. +func AccountShard(account AccountID, shardCount uint32) uint32 { + if shardCount <= 1 { + return 0 + } + return uint32(binary.BigEndian.Uint64(account[:8]) % uint64(shardCount)) +} + +func ObjectShard(id ObjectID) uint32 { + return binary.BigEndian.Uint32(id[:4]) +} + +func ObjectIDForShard(txID Hash, index, shardID uint32) ObjectID { var w codec.Writer w.Fixed(txID[:]) w.U32(index) - return ObjectID(codec.DomainHash("zephyr/object-id/v2", w.BytesCopy())) + w.U32(shardID) + hash := codec.DomainHash("zephyr/object-id/v2", w.BytesCopy()) + binary.BigEndian.PutUint32(hash[:4], shardID) + return ObjectID(hash) +} + +func ObjectIDFromTransaction(txID Hash, index uint32) ObjectID { + return ObjectIDForShard(txID, index, 0) } func TokenIDFromTransaction(txID Hash, operationIndex uint32) TokenID { From d32742c488826b070f600961d84baca0e90aa146 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:05:32 +0200 Subject: [PATCH 040/274] make v2 shard routing explicit and destination objects deterministic --- internal/v2/sharding/sharding.go | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/internal/v2/sharding/sharding.go b/internal/v2/sharding/sharding.go index 89f3224d..d390759d 100644 --- a/internal/v2/sharding/sharding.go +++ b/internal/v2/sharding/sharding.go @@ -2,7 +2,6 @@ package sharding import ( "bytes" - "encoding/binary" "errors" "sort" @@ -21,15 +20,22 @@ type Router struct { ShardCount uint32 } +func (r Router) ShardForAccount(account types.AccountID) (uint32, error) { + if r.ShardCount == 0 || types.IsZero32([32]byte(account)) { + return 0, ErrShardCount + } + return types.AccountShard(account, r.ShardCount), nil +} + func (r Router) ShardForObject(id types.ObjectID) (uint32, error) { - if r.ShardCount == 0 { + if r.ShardCount == 0 || types.IsZero32([32]byte(id)) { return 0, ErrShardCount } - if r.ShardCount == 1 { - return 0, nil + shard := types.ObjectShard(id) + if shard >= r.ShardCount { + return 0, ErrShardCount } - v := binary.BigEndian.Uint64(id[:8]) - return uint32(v % uint64(r.ShardCount)), nil + return shard, nil } type Commitment struct { @@ -151,6 +157,19 @@ func (r CrossShardReceipt) Hash() (types.Hash, error) { return merkle.Leaf("cross-shard-receipt", payload), nil } +func (r CrossShardReceipt) DestinationObject() (object.Object, error) { + if err := r.Validate(); err != nil { + return object.Object{}, err + } + return object.Object{ + ID: types.ObjectIDForShard(r.TransactionID, r.OutputIndex, r.DestinationShard), + Version: 1, + Owner: r.Output.Owner, + Kind: r.Output.Kind, + Data: append([]byte(nil), r.Output.Data...), + }, nil +} + func sortedCommitments(in []Commitment) ([]Commitment, error) { if len(in) == 0 { return nil, ErrShardCount From 024eb87a22a24ecff4a5a5775aa671f75b2e4adf Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:06:07 +0200 Subject: [PATCH 041/274] route v2 outputs locally or into cross-shard receipts --- internal/v2/execution/engine.go | 78 ++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/internal/v2/execution/engine.go b/internal/v2/execution/engine.go index cad6d043..e0b83c66 100644 --- a/internal/v2/execution/engine.go +++ b/internal/v2/execution/engine.go @@ -6,6 +6,7 @@ import ( "github.com/zephyr-chain/zephyr-chain/internal/v2/assets" "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" ) @@ -18,9 +19,16 @@ var ( ErrShard = errors.New("transaction routed to wrong shard") ) +type OutboundOutput struct { + DestinationShard uint32 + OutputIndex uint32 + Output object.OutputSpec +} + type Result struct { Consumed []types.ObjectID Created []object.Object + Outbound []OutboundOutput TxID types.Hash } @@ -40,16 +48,16 @@ func (e Engine) Execute(t tx.Transaction) (Result, error) { if e.ShardCount == 0 { e.ShardCount = 1 } - if len(t.Inputs) > 0 { - expected := shardForObject(t.Inputs[0].ObjectID, e.ShardCount) - if t.ShardID != expected { + router := sharding.Router{ShardCount: e.ShardCount} + senderShard, err := router.ShardForAccount(t.Sender) + if err != nil || t.ShardID != senderShard { + return Result{}, ErrShard + } + for _, in := range t.Inputs { + inputShard, err := router.ShardForObject(in.ObjectID) + if err != nil || inputShard != t.ShardID { return Result{}, ErrShard } - for _, in := range t.Inputs[1:] { - if shardForObject(in.ObjectID, e.ShardCount) != expected { - return Result{}, ErrShard - } - } } if len(t.Operations) != 1 { return Result{}, ErrUnsupportedOperation @@ -82,6 +90,8 @@ func (e Engine) executeTransfer(t tx.Transaction) (Result, error) { outputTotals := map[types.TokenID]uint64{} txID := t.ID() created := make([]object.Object, 0, len(t.Outputs)) + outbound := make([]OutboundOutput, 0) + router := sharding.Router{ShardCount: e.ShardCount} for i, spec := range t.Outputs { if spec.Kind != object.KindCoin { return Result{}, ErrConservation @@ -93,10 +103,18 @@ func (e Engine) executeTransfer(t tx.Transaction) (Result, error) { if err := add(outputTotals, coin.Token, coin.Amount); err != nil { return Result{}, err } - created = append(created, object.Object{ - ID: types.ObjectIDFromTransaction(txID, uint32(i)), Version: 1, - Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...), - }) + destination, err := router.ShardForAccount(spec.Owner) + if err != nil { + return Result{}, ErrShard + } + if destination == t.ShardID { + created = append(created, object.Object{ + ID: types.ObjectIDForShard(txID, uint32(i), destination), Version: 1, + Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...), + }) + } else { + outbound = append(outbound, OutboundOutput{DestinationShard: destination, OutputIndex: uint32(i), Output: spec}) + } } for token, inAmount := range inputTotals { @@ -120,7 +138,7 @@ func (e Engine) executeTransfer(t tx.Transaction) (Result, error) { for i, in := range t.Inputs { consumed[i] = in.ObjectID } - return Result{Consumed: consumed, Created: created, TxID: txID}, nil + return Result{Consumed: consumed, Created: created, Outbound: outbound, TxID: txID}, nil } func (e Engine) executeCreateToken(t tx.Transaction, payload []byte) (Result, error) { @@ -151,6 +169,8 @@ func (e Engine) executeCreateToken(t tx.Transaction, payload []byte) (Result, er var nativeOut uint64 txID := t.ID() created := make([]object.Object, 0, len(t.Outputs)+2) + outbound := make([]OutboundOutput, 0) + router := sharding.Router{ShardCount: e.ShardCount} for i, spec := range t.Outputs { coin, err := object.ParseCoin(spec.Data) if err != nil || spec.Kind != object.KindCoin || coin.Token != e.NativeToken { @@ -160,10 +180,18 @@ func (e Engine) executeCreateToken(t tx.Transaction, payload []byte) (Result, er return Result{}, ErrOverflow } nativeOut += coin.Amount - created = append(created, object.Object{ - ID: types.ObjectIDFromTransaction(txID, uint32(i)), Version: 1, - Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...), - }) + destination, err := router.ShardForAccount(spec.Owner) + if err != nil { + return Result{}, ErrShard + } + if destination == t.ShardID { + created = append(created, object.Object{ + ID: types.ObjectIDForShard(txID, uint32(i), destination), Version: 1, + Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...), + }) + } else { + outbound = append(outbound, OutboundOutput{DestinationShard: destination, OutputIndex: uint32(i), Output: spec}) + } } if math.MaxUint64-nativeOut < t.Fee || nativeIn != nativeOut+t.Fee { return Result{}, ErrConservation @@ -179,7 +207,7 @@ func (e Engine) executeCreateToken(t tx.Transaction, payload []byte) (Result, er if err != nil { return Result{}, err } - defID := types.ObjectIDFromTransaction(txID, 0x80000000) + defID := types.ObjectIDForShard(txID, 0x80000000, t.ShardID) created = append(created, object.Object{ ID: defID, Version: 1, Owner: t.Sender, Kind: object.KindTokenDefinition, Data: defData, }) @@ -188,14 +216,14 @@ func (e Engine) executeCreateToken(t tx.Transaction, payload []byte) (Result, er return Result{}, err } created = append(created, object.Object{ - ID: types.ObjectIDFromTransaction(txID, 0x80000001), Version: 1, + ID: types.ObjectIDForShard(txID, 0x80000001, t.ShardID), Version: 1, Owner: initialCoin.Owner, Kind: initialCoin.Kind, Data: initialCoin.Data, }) consumed := make([]types.ObjectID, len(t.Inputs)) for i, in := range t.Inputs { consumed[i] = in.ObjectID } - return Result{Consumed: consumed, Created: created, TxID: txID}, nil + return Result{Consumed: consumed, Created: created, Outbound: outbound, TxID: txID}, nil } func add(totals map[types.TokenID]uint64, token types.TokenID, amount uint64) error { @@ -206,13 +234,3 @@ func add(totals map[types.TokenID]uint64, token types.TokenID, amount uint64) er totals[token] = current + amount return nil } - -func shardForObject(id types.ObjectID, shardCount uint32) uint32 { - if shardCount <= 1 { - return 0 - } - raw := types.Hash(id) - v := uint64(raw[0])<<56 | uint64(raw[1])<<48 | uint64(raw[2])<<40 | uint64(raw[3])<<32 | - uint64(raw[4])<<24 | uint64(raw[5])<<16 | uint64(raw[6])<<8 | uint64(raw[7]) - return uint32(v % uint64(shardCount)) -} From 8f6eaede91d741aca8f16f05893eb4056560d0c0 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:06:53 +0200 Subject: [PATCH 042/274] persist cross-shard receipt anti-replay in Merkle state --- internal/v2/sharding/receipts.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/v2/sharding/receipts.go b/internal/v2/sharding/receipts.go index a32d3ed5..8e3e03fd 100644 --- a/internal/v2/sharding/receipts.go +++ b/internal/v2/sharding/receipts.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" ) @@ -89,6 +90,26 @@ func VerifyFinalizedReceipt(header GlobalHeader, commitment Commitment, commitme return nil } +// ReceiptMarker is a consensus-state object proving that a finalized receipt +// has already been consumed on its destination shard. Keeping this marker in +// the same Merkle state as balances makes anti-replay survive restart, +// checkpoint and snapshot recovery. +func ReceiptMarker(receipt CrossShardReceipt) (object.Object, error) { + hash, err := receipt.Hash() + if err != nil { + return object.Object{}, err + } + return object.Object{ + ID: types.ObjectIDForShard(hash, 0xfffffffe, receipt.DestinationShard), + Version: 1, + Kind: object.KindSystem, + Data: append([]byte(nil), hash[:]...), + }, nil +} + +// ReceiptTracker is useful for transport/in-process duplicate suppression, but +// it is not the consensus anti-replay source of truth. Consensus uses +// ReceiptMarker in destination shard state. type ReceiptTracker struct { mu sync.Mutex consumed map[types.Hash]uint64 From d0bdd4f6c71e33e12196e757a909b65642aee11e Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:07:47 +0200 Subject: [PATCH 043/274] materialize finalized cross-shard receipts through consensus state --- internal/v2/node/runtime.go | 99 +++++++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/internal/v2/node/runtime.go b/internal/v2/node/runtime.go index ac05fdc6..a40cf3af 100644 --- a/internal/v2/node/runtime.go +++ b/internal/v2/node/runtime.go @@ -21,11 +21,22 @@ var ( ErrCandidateState = errors.New("v2 candidate does not match committed state") ErrCandidateCert = errors.New("v2 candidate certificate mismatch") ErrStateSimulation = errors.New("v2 backend does not support state simulation") + ErrReceiptImport = errors.New("invalid v2 receipt import") ) +type ReceiptImport struct { + Header sharding.GlobalHeader + Certificate v2consensus.Certificate + Validators v2consensus.ValidatorSet + Commitment sharding.Commitment + CommitmentProof merkle.Proof + Receipt sharding.CrossShardReceipt + ReceiptProof merkle.Proof +} + type ShardBatch struct { Transactions []tx.Transaction - ReceiptRoot types.Hash + Imports []ReceiptImport DataRoot types.Hash } @@ -38,6 +49,7 @@ type Candidate struct { Header sharding.GlobalHeader Commitments []sharding.Commitment Results map[uint32][]execution.Result + Receipts map[uint32][]sharding.CrossShardReceipt deltas map[uint32]shardDelta } @@ -67,14 +79,20 @@ func NewRuntime(network types.NetworkID, nativeToken types.TokenID, validatorRoo } // BuildCandidate executes and simulates every shard against committed state. -// It never mutates the backing state stores. +// It never mutates the backing state stores. Receipt imports become destination +// objects plus durable anti-replay markers, but are not spendable until a later +// block because all transactions in this candidate target the pre-state root. func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) (Candidate, error) { r.mu.Lock() defer r.mu.Unlock() if height != r.Height+1 || height == 0 { return Candidate{}, ErrCandidateHeight } - candidate := Candidate{Results: make(map[uint32][]execution.Result), deltas: make(map[uint32]shardDelta)} + candidate := Candidate{ + Results: make(map[uint32][]execution.Result), + Receipts: make(map[uint32][]sharding.CrossShardReceipt), + deltas: make(map[uint32]shardDelta), + } commitments := make([]sharding.Commitment, 0, r.ShardCount) dataLeaves := make([]types.Hash, 0, r.ShardCount) @@ -82,7 +100,9 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( store := r.States[shard] batch := batches[shard] currentRoot := store.Root() - newRoot := currentRoot + delta := shardDelta{} + results := make([]execution.Result, 0, len(batch.Transactions)) + if len(batch.Transactions) > 0 { for _, transaction := range batch.Transactions { if transaction.ShardID != shard || transaction.StateRoot != currentRoot { @@ -90,30 +110,69 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( } } executor := execution.BatchExecutor{Engine: execution.Engine{Network: r.Network, NativeToken: r.NativeToken, ShardCount: r.ShardCount}, Workers: r.Workers} - results, err := executor.ExecuteBatch(batch.Transactions) + var err error + results, err = executor.ExecuteBatch(batch.Transactions) if err != nil { return Candidate{}, err } - delta := shardDelta{} for _, result := range results { delta.Consumed = append(delta.Consumed, result.Consumed...) delta.Created = append(delta.Created, result.Created...) } + candidate.Results[shard] = results + } + + for _, receiptImport := range batch.Imports { + if err := r.validateReceiptImport(shard, receiptImport); err != nil { + return Candidate{}, err + } + destinationObject, err := receiptImport.Receipt.DestinationObject() + if err != nil { + return Candidate{}, ErrReceiptImport + } + marker, err := sharding.ReceiptMarker(receiptImport.Receipt) + if err != nil { + return Candidate{}, ErrReceiptImport + } + if _, exists := store.GetObject(destinationObject.ID); exists { + return Candidate{}, sharding.ErrReceiptReplay + } + if _, exists := store.GetObject(marker.ID); exists { + return Candidate{}, sharding.ErrReceiptReplay + } + delta.Created = append(delta.Created, destinationObject, marker) + } + + newRoot := currentRoot + if len(delta.Consumed) > 0 || len(delta.Created) > 0 { simulator, ok := store.(worldstate.Simulator) if !ok { return Candidate{}, ErrStateSimulation } + var err error newRoot, err = simulator.Simulate(delta.Consumed, delta.Created) if err != nil { return Candidate{}, err } - candidate.Results[shard] = results candidate.deltas[shard] = delta } - receiptRoot := batch.ReceiptRoot - if types.IsZero32([32]byte(receiptRoot)) { - receiptRoot = merkle.Root(nil) + + receipts := make([]sharding.CrossShardReceipt, 0) + for _, result := range results { + for _, outbound := range result.Outbound { + receipts = append(receipts, sharding.CrossShardReceipt{ + SourceShard: shard, DestinationShard: outbound.DestinationShard, + SourceHeight: height, TransactionID: result.TxID, OutputIndex: outbound.OutputIndex, + Output: outbound.Output, SourceStateRoot: newRoot, + }) + } + } + receiptRoot, err := (sharding.ReceiptBatch{Receipts: receipts}).Root() + if err != nil { + return Candidate{}, err } + candidate.Receipts[shard] = receipts + dataRoot := batch.DataRoot if types.IsZero32([32]byte(dataRoot)) { dataRoot = merkle.Root(nil) @@ -134,6 +193,26 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( return candidate, nil } +func (r *Runtime) validateReceiptImport(destinationShard uint32, receiptImport ReceiptImport) error { + if receiptImport.Header.Network != r.Network || receiptImport.Validators.Network != r.Network || + receiptImport.Certificate.Network != r.Network || receiptImport.Receipt.DestinationShard != destinationShard || + receiptImport.Header.Height > r.Height || receiptImport.Header.Height != receiptImport.Receipt.SourceHeight { + return ErrReceiptImport + } + if receiptImport.Header.CertificateHash != receiptImport.Certificate.Hash() || + receiptImport.Certificate.HeaderHash != v2consensus.HeaderConsensusHash(receiptImport.Header) || + receiptImport.Certificate.Height != receiptImport.Header.Height { + return ErrReceiptImport + } + if err := receiptImport.Validators.VerifyCertificate(receiptImport.Certificate); err != nil { + return ErrReceiptImport + } + if err := sharding.VerifyFinalizedReceipt(receiptImport.Header, receiptImport.Commitment, receiptImport.CommitmentProof, receiptImport.Receipt, receiptImport.ReceiptProof); err != nil { + return ErrReceiptImport + } + return nil +} + // Commit applies a previously simulated candidate only after a valid quorum // certificate for its consensus hash is supplied. func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificate, validators v2consensus.ValidatorSet) (sharding.GlobalHeader, error) { From 5c3a9d92b6bd96b86dd21b7064f347b27e67189f Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:08:23 +0200 Subject: [PATCH 044/274] test finalized two-shard transfer and durable receipt anti-replay --- internal/v2/node/multishard_test.go | 166 ++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 internal/v2/node/multishard_test.go diff --git a/internal/v2/node/multishard_test.go b/internal/v2/node/multishard_test.go new file mode 100644 index 00000000..64b43601 --- /dev/null +++ b/internal/v2/node/multishard_test.go @@ -0,0 +1,166 @@ +package node + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestTwoShardTransferFinalizesReceiptThenImportsOnce(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("two-shard"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + aliceKey, alice := accountOnShard(t, 0, 2) + _, bob := accountOnShard(t, 1, 2) + shard0 := worldstate.NewMemory() + shard1 := worldstate.NewMemory() + + inputID := types.ObjectIDForShard(types.HashBytes("genesis", []byte("alice-coin")), 0, 0) + inputOut, err := object.NewCoinOutput(alice, native, 100) + if err != nil { + t.Fatal(err) + } + input := object.Object{ID: inputID, Version: 1, Owner: alice, Kind: inputOut.Kind, Data: inputOut.Data} + root0, err := shard0.Apply(nil, []object.Object{input}) + if err != nil { + t.Fatal(err) + } + witness, proof, ok := shard0.Proof(inputID) + if !ok { + t.Fatal("missing source witness") + } + witnessHash := witness.Hash() + payment, _ := object.NewCoinOutput(bob, native, 25) + change, _ := object.NewCoinOutput(alice, native, 74) + transaction := tx.Transaction{ + Version: tx.Version, Network: network, ShardID: 0, StateRoot: root0, + Inputs: []tx.InputRef{{ObjectID: inputID, Version: 1, ObjectHash: witnessHash}}, + Outputs: []object.OutputSpec{payment, change}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, + Fee: 1, Witnesses: []tx.Witness{{Object: witness, Proof: proof}}, + } + transaction.Salt[0] = 1 + if err := transaction.Sign(aliceKey); err != nil { + t.Fatal(err) + } + + validatorKey, validators := singleValidatorSet(t, network) + validatorRoot := types.HashBytes("validators", []byte("single")) + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: shard0, 1: shard1}, 2) + if err != nil { + t.Fatal(err) + } + candidate1, err := runtime.BuildCandidate(1, map[uint32]ShardBatch{0: {Transactions: []tx.Transaction{transaction}}}) + if err != nil { + t.Fatal(err) + } + if len(candidate1.Receipts[0]) != 1 || len(candidate1.Results[0]) != 1 || len(candidate1.Results[0][0].Outbound) != 1 { + t.Fatalf("expected one cross-shard receipt: %+v", candidate1.Results[0]) + } + if shard1.Root() != candidate1.Commitments[1].StateRoot { + t.Fatal("destination state changed before receipt import") + } + proposal1, certificate1 := certifyCandidate(t, validators, validatorKey, candidate1) + _ = proposal1 + finalized1, err := runtime.Commit(candidate1, certificate1, validators) + if err != nil { + t.Fatal(err) + } + + receipt := candidate1.Receipts[0][0] + commitment, commitmentProof, err := sharding.CommitmentProof(candidate1.Commitments, 0) + if err != nil { + t.Fatal(err) + } + receiptProof, err := (sharding.ReceiptBatch{Receipts: candidate1.Receipts[0]}).Proof(receipt) + if err != nil { + t.Fatal(err) + } + importReceipt := ReceiptImport{ + Header: finalized1, Certificate: certificate1, Validators: validators, + Commitment: commitment, CommitmentProof: commitmentProof, + Receipt: receipt, ReceiptProof: receiptProof, + } + + root1Before := shard1.Root() + candidate2, err := runtime.BuildCandidate(2, map[uint32]ShardBatch{1: {Imports: []ReceiptImport{importReceipt}}}) + if err != nil { + t.Fatal(err) + } + if shard1.Root() != root1Before { + t.Fatal("receipt import mutated state before destination QC") + } + _, certificate2 := certifyCandidate(t, validators, validatorKey, candidate2) + if _, err := runtime.Commit(candidate2, certificate2, validators); err != nil { + t.Fatal(err) + } + destinationObject, err := receipt.DestinationObject() + if err != nil { + t.Fatal(err) + } + if _, ok := shard1.GetObject(destinationObject.ID); !ok { + t.Fatal("destination coin was not materialized") + } + marker, err := sharding.ReceiptMarker(receipt) + if err != nil { + t.Fatal(err) + } + if _, ok := shard1.GetObject(marker.ID); !ok { + t.Fatal("durable receipt marker was not committed") + } + if _, err := runtime.BuildCandidate(3, map[uint32]ShardBatch{1: {Imports: []ReceiptImport{importReceipt}}}); err != sharding.ErrReceiptReplay { + t.Fatalf("expected durable receipt replay rejection, got %v", err) + } +} + +func accountOnShard(t *testing.T, shard, shardCount uint32) (*ecdsa.PrivateKey, types.AccountID) { + t.Helper() + for i := 0; i < 10_000; i++ { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + account := types.AccountIDFromPublicKey(pub) + if types.AccountShard(account, shardCount) == shard { + return key, account + } + } + t.Fatal("could not generate account on requested shard") + return nil, types.AccountID{} +} + +func singleValidatorSet(t *testing.T, network types.NetworkID) (*ecdsa.PrivateKey, v2consensus.ValidatorSet) { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + id := types.ValidatorIDFromPublicKey(pub) + return key, v2consensus.ValidatorSet{Network: network, Validators: []v2consensus.Validator{{ID: id, PublicKey: pub, Power: 10}}} +} + +func certifyCandidate(t *testing.T, validators v2consensus.ValidatorSet, key *ecdsa.PrivateKey, candidate Candidate) (v2consensus.Proposal, v2consensus.Certificate) { + t.Helper() + proposal, err := v2consensus.SignProposal(key, candidate.Header, 0) + if err != nil { + t.Fatal(err) + } + headerHash := v2consensus.HeaderConsensusHash(candidate.Header) + vote, err := v2consensus.SignVote(key, candidate.Header.Network, candidate.Header.Height, 0, headerHash) + if err != nil { + t.Fatal(err) + } + certificate, err := validators.BuildCertificate(proposal, []v2consensus.Vote{vote}) + if err != nil { + t.Fatal(err) + } + return proposal, certificate +} From 85afdc4de7d7fab7a7f6b0a03a842a4a581d8204 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:12:21 +0200 Subject: [PATCH 045/274] bind v2 validator sets to consensus Merkle roots --- internal/v2/consensus/validator_root.go | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 internal/v2/consensus/validator_root.go diff --git a/internal/v2/consensus/validator_root.go b/internal/v2/consensus/validator_root.go new file mode 100644 index 00000000..b5671eee --- /dev/null +++ b/internal/v2/consensus/validator_root.go @@ -0,0 +1,32 @@ +package consensus + +import ( + "bytes" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +// Root commits to the exact validator identities, public keys and integer +// voting powers that are authorized for a header. It is deliberately ordered +// by ValidatorID so peers and Citizen Nodes derive the same commitment. +func (s ValidatorSet) Root() (types.Hash, error) { + if err := s.Validate(); err != nil { + return types.Hash{}, err + } + validators := append([]Validator(nil), s.Validators...) + sort.Slice(validators, func(i, j int) bool { + return bytes.Compare(validators[i].ID[:], validators[j].ID[:]) < 0 + }) + leaves := make([]types.Hash, len(validators)) + for i, validator := range validators { + var w codec.Writer + w.Fixed(validator.ID[:]) + w.Bytes(validator.PublicKey) + w.U64(validator.Power) + leaves[i] = merkle.Leaf("validator", w.BytesCopy()) + } + return merkle.Root(leaves), nil +} From ed1b43bc00b77447871241491bc69ed00d4c4975 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:13:37 +0200 Subject: [PATCH 046/274] bind Citizen light snapshots to committed validator roots --- internal/v2/lightapi/server.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/v2/lightapi/server.go b/internal/v2/lightapi/server.go index 16b24098..e3c0c618 100644 --- a/internal/v2/lightapi/server.go +++ b/internal/v2/lightapi/server.go @@ -28,6 +28,10 @@ func (s Snapshot) Validate() error { s.Header.CertificateHash != s.Certificate.Hash() { return ErrSnapshot } + validatorRoot, err := s.Validators.Root() + if err != nil || validatorRoot != s.Header.ValidatorRoot { + return ErrSnapshot + } if err := s.Validators.VerifyCertificate(s.Certificate); err != nil { return ErrSnapshot } From 258d772065181724dded33dd099f608bc9ef1dd6 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:14:42 +0200 Subject: [PATCH 047/274] reject certificates and receipts from uncommitted validator sets --- internal/v2/node/runtime.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/v2/node/runtime.go b/internal/v2/node/runtime.go index a40cf3af..3704d048 100644 --- a/internal/v2/node/runtime.go +++ b/internal/v2/node/runtime.go @@ -199,6 +199,10 @@ func (r *Runtime) validateReceiptImport(destinationShard uint32, receiptImport R receiptImport.Header.Height > r.Height || receiptImport.Header.Height != receiptImport.Receipt.SourceHeight { return ErrReceiptImport } + validatorRoot, err := receiptImport.Validators.Root() + if err != nil || validatorRoot != receiptImport.Header.ValidatorRoot { + return ErrReceiptImport + } if receiptImport.Header.CertificateHash != receiptImport.Certificate.Hash() || receiptImport.Certificate.HeaderHash != v2consensus.HeaderConsensusHash(receiptImport.Header) || receiptImport.Certificate.Height != receiptImport.Header.Height { @@ -221,6 +225,10 @@ func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificat if candidate.Header.Height != r.Height+1 || candidate.Header.ParentHash != r.ParentHash || candidate.Header.Network != r.Network { return sharding.GlobalHeader{}, ErrCandidateState } + validatorRoot, err := validators.Root() + if err != nil || validatorRoot != candidate.Header.ValidatorRoot || validatorRoot != r.ValidatorRoot { + return sharding.GlobalHeader{}, ErrCandidateCert + } if certificate.HeaderHash != v2consensus.HeaderConsensusHash(candidate.Header) || certificate.Height != candidate.Header.Height || certificate.Network != r.Network { return sharding.GlobalHeader{}, ErrCandidateCert } From 1247a550a23da33fc94af94a693a2a46876cb19d Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:15:29 +0200 Subject: [PATCH 048/274] bind runtime test candidates to real validator roots --- internal/v2/node/runtime_test.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/internal/v2/node/runtime_test.go b/internal/v2/node/runtime_test.go index 9b85b119..555431c7 100644 --- a/internal/v2/node/runtime_test.go +++ b/internal/v2/node/runtime_test.go @@ -16,9 +16,20 @@ import ( func TestCandidateDoesNotMutateBeforeQCAndCommitsAfterQC(t *testing.T) { network := types.NetworkID(types.HashBytes("network", []byte("node-runtime"))) native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) - validatorRoot := types.HashBytes("validators", []byte("set-1")) stateStore := worldstate.NewMemory() + validatorKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + validatorPub := elliptic.Marshal(elliptic.P256(), validatorKey.PublicKey.X, validatorKey.PublicKey.Y) + validatorID := types.ValidatorIDFromPublicKey(validatorPub) + validators := v2consensus.ValidatorSet{Network: network, Validators: []v2consensus.Validator{{ID: validatorID, PublicKey: validatorPub, Power: 10}}} + validatorRoot, err := validators.Root() + if err != nil { + t.Fatal(err) + } + aliceKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatal(err) @@ -66,13 +77,6 @@ func TestCandidateDoesNotMutateBeforeQCAndCommitsAfterQC(t *testing.T) { t.Fatal("candidate did not calculate a new state root") } - validatorKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - validatorPub := elliptic.Marshal(elliptic.P256(), validatorKey.PublicKey.X, validatorKey.PublicKey.Y) - validatorID := types.ValidatorIDFromPublicKey(validatorPub) - validators := v2consensus.ValidatorSet{Network: network, Validators: []v2consensus.Validator{{ID: validatorID, PublicKey: validatorPub, Power: 10}}} proposal, err := v2consensus.SignProposal(validatorKey, candidate.Header, 0) if err != nil { t.Fatal(err) From ec68a23a5931d99cdffae148d255836ebcd05724 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:16:14 +0200 Subject: [PATCH 049/274] test trusted validator-root enforcement for cross-shard receipts --- internal/v2/node/multishard_test.go | 62 +++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/internal/v2/node/multishard_test.go b/internal/v2/node/multishard_test.go index 64b43601..f8a571d5 100644 --- a/internal/v2/node/multishard_test.go +++ b/internal/v2/node/multishard_test.go @@ -51,7 +51,10 @@ func TestTwoShardTransferFinalizesReceiptThenImportsOnce(t *testing.T) { } validatorKey, validators := singleValidatorSet(t, network) - validatorRoot := types.HashBytes("validators", []byte("single")) + validatorRoot, err := validators.Root() + if err != nil { + t.Fatal(err) + } runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: shard0, 1: shard1}, 2) if err != nil { t.Fatal(err) @@ -66,8 +69,7 @@ func TestTwoShardTransferFinalizesReceiptThenImportsOnce(t *testing.T) { if shard1.Root() != candidate1.Commitments[1].StateRoot { t.Fatal("destination state changed before receipt import") } - proposal1, certificate1 := certifyCandidate(t, validators, validatorKey, candidate1) - _ = proposal1 + _, certificate1 := certifyCandidate(t, validators, validatorKey, candidate1) finalized1, err := runtime.Commit(candidate1, certificate1, validators) if err != nil { t.Fatal(err) @@ -119,6 +121,60 @@ func TestTwoShardTransferFinalizesReceiptThenImportsOnce(t *testing.T) { } } +func TestReceiptImportRejectsSelfSignedForeignValidatorSet(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("receipt-validator-root"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + trustedKey, trustedValidators := singleValidatorSet(t, network) + trustedRoot, err := trustedValidators.Root() + if err != nil { + t.Fatal(err) + } + runtime, err := NewRuntime(network, native, trustedRoot, map[uint32]worldstate.Backend{0: worldstate.NewMemory(), 1: worldstate.NewMemory()}, 1) + if err != nil { + t.Fatal(err) + } + candidate, err := runtime.BuildCandidate(1, nil) + if err != nil { + t.Fatal(err) + } + _, cert := certifyCandidate(t, trustedValidators, trustedKey, candidate) + if _, err := runtime.Commit(candidate, cert, trustedValidators); err != nil { + t.Fatal(err) + } + + attackerKey, attackerValidators := singleValidatorSet(t, network) + fakeHeader := candidate.Header + fakeHeader.Height = 1 + fakeHeader.CertificateHash = types.Hash{} + fakeProposal, err := v2consensus.SignProposal(attackerKey, fakeHeader, 0) + if err != nil { + t.Fatal(err) + } + fakeHash := v2consensus.HeaderConsensusHash(fakeHeader) + fakeVote, err := v2consensus.SignVote(attackerKey, network, 1, 0, fakeHash) + if err != nil { + t.Fatal(err) + } + fakeCert, err := attackerValidators.BuildCertificate(fakeProposal, []v2consensus.Vote{fakeVote}) + if err != nil { + t.Fatal(err) + } + fakeHeader.CertificateHash = fakeCert.Hash() + if root, _ := attackerValidators.Root(); root == fakeHeader.ValidatorRoot { + t.Fatal("test requires attacker validator root to differ") + } + + receipt := sharding.CrossShardReceipt{ + SourceShard: 0, DestinationShard: 1, SourceHeight: 1, + TransactionID: types.HashBytes("tx", []byte("fake")), OutputIndex: 0, + Output: object.OutputSpec{Owner: types.AccountIDFromPublicKey([]byte("recipient")), Kind: object.KindSystem}, + SourceStateRoot: candidate.Commitments[0].StateRoot, + } + if err := runtime.validateReceiptImport(1, ReceiptImport{Header: fakeHeader, Certificate: fakeCert, Validators: attackerValidators, Receipt: receipt}); err != ErrReceiptImport { + t.Fatalf("expected uncommitted validator-set rejection, got %v", err) + } +} + func accountOnShard(t *testing.T, shard, shardCount uint32) (*ecdsa.PrivateKey, types.AccountID) { t.Helper() for i := 0; i < 10_000; i++ { From 0d31aa5adb9ff4a12a005b8cb9b562c5161c4050 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:16:49 +0200 Subject: [PATCH 050/274] test Citizen snapshots reject self-signed foreign validator sets --- internal/v2/lightapi/server_test.go | 39 ++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/internal/v2/lightapi/server_test.go b/internal/v2/lightapi/server_test.go index 9ff0905c..6fd7da42 100644 --- a/internal/v2/lightapi/server_test.go +++ b/internal/v2/lightapi/server_test.go @@ -50,7 +50,11 @@ func TestLightObjectEndpointReturnsVerifiableBundle(t *testing.T) { pub := elliptic.Marshal(elliptic.P256(), validatorKey.PublicKey.X, validatorKey.PublicKey.Y) validatorID := types.ValidatorIDFromPublicKey(pub) validators := v2consensus.ValidatorSet{Network: network, Validators: []v2consensus.Validator{{ID: validatorID, PublicKey: pub, Power: 10}}} - header := sharding.GlobalHeader{Version: 2, Network: network, Height: 1, ShardCommitmentRoot: commitmentRoot, ValidatorRoot: types.HashBytes("validators", []byte("root")), DataRoot: merkle.Root(nil)} + validatorRoot, err := validators.Root() + if err != nil { + t.Fatal(err) + } + header := sharding.GlobalHeader{Version: 2, Network: network, Height: 1, ShardCommitmentRoot: commitmentRoot, ValidatorRoot: validatorRoot, DataRoot: merkle.Root(nil)} proposal, err := v2consensus.SignProposal(validatorKey, header, 0) if err != nil { t.Fatal(err) @@ -79,6 +83,39 @@ func TestLightObjectEndpointReturnsVerifiableBundle(t *testing.T) { } } +func TestSnapshotRejectsValidatorSetNotCommittedByHeader(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("light-validator-root"))) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + id := types.ValidatorIDFromPublicKey(pub) + validators := v2consensus.ValidatorSet{Network: network, Validators: []v2consensus.Validator{{ID: id, PublicKey: pub, Power: 1}}} + header := sharding.GlobalHeader{ + Version: 2, Network: network, Height: 1, + ShardCommitmentRoot: types.HashBytes("shards", []byte("root")), + ValidatorRoot: types.HashBytes("validators", []byte("foreign")), + DataRoot: merkle.Root(nil), + } + proposal, err := v2consensus.SignProposal(key, header, 0) + if err != nil { + t.Fatal(err) + } + vote, err := v2consensus.SignVote(key, network, 1, 0, v2consensus.HeaderConsensusHash(header)) + if err != nil { + t.Fatal(err) + } + certificate, err := validators.BuildCertificate(proposal, []v2consensus.Vote{vote}) + if err != nil { + t.Fatal(err) + } + header.CertificateHash = certificate.Hash() + if err := (Snapshot{Header: header, Certificate: certificate, Validators: validators}).Validate(); err != ErrSnapshot { + t.Fatalf("expected validator-root mismatch rejection, got %v", err) + } +} + func contains(value, needle string) bool { for i := 0; i+len(needle) <= len(value); i++ { if value[i:i+len(needle)] == needle { From 082f62471c6a21b6f5e5c022b0ba4092aa28b6a2 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:17:25 +0200 Subject: [PATCH 051/274] bind v2 benchmark certificates to committed validator root --- internal/v2/node/benchmark_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/v2/node/benchmark_test.go b/internal/v2/node/benchmark_test.go index 03d86a9c..1b686ee7 100644 --- a/internal/v2/node/benchmark_test.go +++ b/internal/v2/node/benchmark_test.go @@ -36,8 +36,11 @@ func benchmarkFinalizedBatch(b *testing.B, workers int) { network := types.NetworkID(types.HashBytes("network", []byte("benchmark-v2"))) native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) - validatorRoot := types.HashBytes("validators", []byte("benchmark-set")) validators, validatorKeys := benchmarkValidators(b, network, 7) + validatorRoot, err := validators.Root() + if err != nil { + b.Fatal(err) + } signers := make([]benchmarkSigner, benchmarkBatchSize) for i := range signers { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) From 5090e35992d7591e52ce3407bb64e8b45fc0ca91 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:18:35 +0200 Subject: [PATCH 052/274] require genesis/checkpoint trust anchors for wallet Citizen verification --- apps/wallet/src/lib/v2CitizenTrust.ts | 191 ++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 apps/wallet/src/lib/v2CitizenTrust.ts diff --git a/apps/wallet/src/lib/v2CitizenTrust.ts b/apps/wallet/src/lib/v2CitizenTrust.ts new file mode 100644 index 00000000..ba2fb396 --- /dev/null +++ b/apps/wallet/src/lib/v2CitizenTrust.ts @@ -0,0 +1,191 @@ +import { + type CitizenObjectBundle, + type CitizenValidatorDTO, + type VerifiedCitizenObject, + verifyCitizenObjectBundle +} from './v2Citizen' + +export interface CitizenTrustAnchor { + // Genesis-derived Zephyr NetworkID in lowercase hex. + network: string + // Validator-set Merkle root trusted from genesis or a previously verified checkpoint. + validatorRoot: string +} + +// This is the wallet-facing entry point. It rejects a self-consistent but +// self-signed light bundle unless its validator set is anchored to the trusted +// genesis/checkpoint root before delegating the remaining QC/shard/state proof +// verification to v2Citizen.ts. +export async function fetchAndVerifyTrustedCitizenObject( + baseURL: string, + shardId: number, + objectId: string, + anchor: CitizenTrustAnchor +): Promise { + const endpoint = new URL('/v2/light/object', normalizeBaseURL(baseURL)) + endpoint.searchParams.set('shard', String(shardId)) + endpoint.searchParams.set('id', objectId) + const response = await fetch(endpoint) + if (!response.ok) throw new Error(`Citizen proof request failed (${response.status})`) + return verifyTrustedCitizenObjectBundle(await response.json() as CitizenObjectBundle, anchor) +} + +export async function verifyTrustedCitizenObjectBundle( + bundle: CitizenObjectBundle, + anchor: CitizenTrustAnchor +): Promise { + const expectedNetwork = normalizeHex32(anchor.network, 'Citizen trust-anchor network') + const expectedValidatorRoot = normalizeHex32(anchor.validatorRoot, 'Citizen trust-anchor validator root') + const header = base64ToBytes(bundle.header) + if (header.length !== 202) throw new Error('Invalid canonical Zephyr GlobalHeader length') + if (readU16(header, 0) !== 2) throw new Error('Unsupported Zephyr GlobalHeader version') + + const headerNetwork = bytesToHex(header.slice(2, 34)) + const headerValidatorRoot = bytesToHex(header.slice(106, 138)) + if (bundle.network.toLowerCase() !== expectedNetwork || headerNetwork !== expectedNetwork) { + throw new Error('Citizen bundle does not belong to the trusted Zephyr network') + } + if (headerValidatorRoot !== expectedValidatorRoot) { + throw new Error('Citizen header validator root is not trusted by this wallet checkpoint') + } + + const suppliedValidatorRoot = bytesToHex(await validatorSetRoot(bundle.validators)) + if (suppliedValidatorRoot !== expectedValidatorRoot) { + throw new Error('Citizen validator set does not match the committed trusted validator root') + } + + return verifyCitizenObjectBundle(bundle) +} + +async function validatorSetRoot(validators: CitizenValidatorDTO[]): Promise { + if (validators.length === 0) throw new Error('Citizen validator set is empty') + const canonical: Array<{ id: Uint8Array, publicKey: Uint8Array, power: bigint }> = [] + const seen = new Set() + for (const validator of validators) { + const id = hexToBytes(normalizeHex32(validator.id, 'validator ID')) + const idHex = bytesToHex(id) + if (seen.has(idHex)) throw new Error('Duplicate Citizen validator') + seen.add(idHex) + const publicKey = base64ToBytes(validator.publicKey) + if (publicKey.length !== 65) throw new Error('Invalid Citizen validator public key') + const derivedID = await domainHash('zephyr/validator-id/v2', publicKey) + if (!equalBytes(derivedID, id)) throw new Error('Citizen validator ID does not match public key') + const power = BigInt(validator.power) + if (power <= 0n || power > 0xffffffffffffffffn) throw new Error('Invalid Citizen validator voting power') + canonical.push({ id, publicKey, power }) + } + canonical.sort((a, b) => compareBytes(a.id, b.id)) + const leaves: Uint8Array[] = [] + for (const validator of canonical) { + const payload = concatBytes( + validator.id, + u32(validator.publicKey.length), + validator.publicKey, + u64(validator.power) + ) + leaves.push(await domainHash('zephyr/merkle/leaf/v2/validator', payload)) + } + return merkleRoot(leaves) +} + +async function merkleRoot(leaves: Uint8Array[]): Promise { + const empty = await domainHash('zephyr/merkle/empty/v2', new Uint8Array()) + if (leaves.length === 0) return empty + const level = leaves.map(leaf => leaf.slice()) + let target = 1 + while (target < level.length) target <<= 1 + while (level.length < target) level.push(empty.slice()) + let current = level + while (current.length > 1) { + const next: Uint8Array[] = [] + for (let i = 0; i < current.length; i += 2) { + next.push(await domainHash('zephyr/merkle/branch/v2', concatBytes(current[i], current[i + 1]))) + } + current = next + } + return current[0] +} + +async function domainHash(domain: string, payload: Uint8Array): Promise { + const encodedDomain = new TextEncoder().encode(domain) + const framed = concatBytes(u32(encodedDomain.length), encodedDomain, u32(payload.length), payload) + return new Uint8Array(await crypto.subtle.digest('SHA-256', toArrayBuffer(framed))) +} + +function normalizeHex32(value: string, label: string): string { + const normalized = value.trim().toLowerCase() + if (!/^[0-9a-f]{64}$/.test(normalized)) throw new Error(`${label} must be a 32-byte hex value`) + return normalized +} + +function readU16(value: Uint8Array, offset: number): number { + return (value[offset] << 8) | value[offset + 1] +} + +function u32(value: number): Uint8Array { + if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) throw new Error('u32 overflow') + return new Uint8Array([(value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff]) +} + +function u64(value: bigint): Uint8Array { + if (value < 0n || value > 0xffffffffffffffffn) throw new Error('u64 overflow') + const out = new Uint8Array(8) + let remaining = value + for (let i = 7; i >= 0; i--) { + out[i] = Number(remaining & 0xffn) + remaining >>= 8n + } + return out +} + +function base64ToBytes(value: string): Uint8Array { + const raw = atob(value) + const out = new Uint8Array(raw.length) + for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i) + return out +} + +function hexToBytes(value: string): Uint8Array { + const out = new Uint8Array(value.length / 2) + for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(value.slice(i * 2, i * 2 + 2), 16) + return out +} + +function bytesToHex(value: Uint8Array): string { + return Array.from(value, byte => byte.toString(16).padStart(2, '0')).join('') +} + +function equalBytes(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false + let diff = 0 + for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i] + return diff === 0 +} + +function compareBytes(a: Uint8Array, b: Uint8Array): number { + for (let i = 0; i < Math.min(a.length, b.length); i++) { + if (a[i] !== b[i]) return a[i] - b[i] + } + return a.length - b.length +} + +function concatBytes(...values: Uint8Array[]): Uint8Array { + const length = values.reduce((total, value) => total + value.length, 0) + const out = new Uint8Array(length) + let offset = 0 + for (const value of values) { + out.set(value, offset) + offset += value.length + } + return out +} + +function toArrayBuffer(value: Uint8Array): ArrayBuffer { + return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer +} + +function normalizeBaseURL(value: string): string { + const url = new URL(value, window.location.origin) + if (!url.pathname.endsWith('/')) url.pathname += '/' + return url.toString() +} From 1ac8f62e8babca83c250f8e3786eeac5916dad3d Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:19:46 +0200 Subject: [PATCH 053/274] make validator-root binding a core proposal invariant --- internal/v2/consensus/consensus.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/v2/consensus/consensus.go b/internal/v2/consensus/consensus.go index 7de7389a..8c911cf5 100644 --- a/internal/v2/consensus/consensus.go +++ b/internal/v2/consensus/consensus.go @@ -171,6 +171,10 @@ func (s ValidatorSet) VerifyProposal(proposal Proposal) error { if err := s.Validate(); err != nil || proposal.Header.Network != s.Network || proposal.Header.Height == 0 || proposal.Header.CertificateHash != (types.Hash{}) { return ErrProposal } + validatorRoot, err := s.Root() + if err != nil || validatorRoot != proposal.Header.ValidatorRoot { + return ErrProposal + } expected, err := s.Proposer(proposal.Header.Height, proposal.Round) if err != nil || expected.ID != proposal.Proposer || !bytes.Equal(expected.PublicKey, proposal.PublicKey) { return ErrProposal From e18a1081539ff5610adb167f2fa427181ca32f2f Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:20:29 +0200 Subject: [PATCH 054/274] test validator-set root as a core consensus invariant --- internal/v2/consensus/consensus_test.go | 30 ++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/internal/v2/consensus/consensus_test.go b/internal/v2/consensus/consensus_test.go index d5133f14..a498a226 100644 --- a/internal/v2/consensus/consensus_test.go +++ b/internal/v2/consensus/consensus_test.go @@ -27,6 +27,10 @@ func TestV2QuorumCertificateRequiresTwoThirdsPlus(t *testing.T) { if QuorumPower(40) != 27 { t.Fatalf("unexpected quorum: %d", QuorumPower(40)) } + validatorRoot, err := set.Root() + if err != nil { + t.Fatal(err) + } proposer, err := set.Proposer(1, 0) if err != nil { t.Fatal(err) @@ -34,7 +38,7 @@ func TestV2QuorumCertificateRequiresTwoThirdsPlus(t *testing.T) { header := sharding.GlobalHeader{ Version: 2, Network: network, Height: 1, ShardCommitmentRoot: types.HashBytes("shards", []byte("root")), - ValidatorRoot: types.HashBytes("validators", []byte("root")), + ValidatorRoot: validatorRoot, DataRoot: types.HashBytes("data", []byte("root")), } proposal, err := SignProposal(keys[proposer.ID], header, 0) @@ -69,6 +73,30 @@ func TestV2QuorumCertificateRequiresTwoThirdsPlus(t *testing.T) { } } +func TestV2ProposalRejectsUncommittedValidatorRoot(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("validator-root"))) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + id := types.ValidatorIDFromPublicKey(pub) + set := ValidatorSet{Network: network, Validators: []Validator{{ID: id, PublicKey: pub, Power: 1}}} + header := sharding.GlobalHeader{ + Version: 2, Network: network, Height: 1, + ShardCommitmentRoot: types.HashBytes("shards", []byte("root")), + ValidatorRoot: types.HashBytes("validators", []byte("wrong")), + DataRoot: types.HashBytes("data", []byte("root")), + } + proposal, err := SignProposal(key, header, 0) + if err != nil { + t.Fatal(err) + } + if err := set.VerifyProposal(proposal); err != ErrProposal { + t.Fatalf("expected validator-root mismatch rejection, got %v", err) + } +} + func TestV2CertificateRejectsDuplicateVote(t *testing.T) { network := types.NetworkID(types.HashBytes("network", []byte("duplicate"))) key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) From baf98c6ff48e325e1af10a6035d3d0c7c9c7e540 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:21:26 +0200 Subject: [PATCH 055/274] simulate hostile self-signed receipt certificates below normal builders --- internal/v2/node/multishard_test.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/internal/v2/node/multishard_test.go b/internal/v2/node/multishard_test.go index f8a571d5..fcff424e 100644 --- a/internal/v2/node/multishard_test.go +++ b/internal/v2/node/multishard_test.go @@ -146,17 +146,13 @@ func TestReceiptImportRejectsSelfSignedForeignValidatorSet(t *testing.T) { fakeHeader := candidate.Header fakeHeader.Height = 1 fakeHeader.CertificateHash = types.Hash{} - fakeProposal, err := v2consensus.SignProposal(attackerKey, fakeHeader, 0) - if err != nil { - t.Fatal(err) - } fakeHash := v2consensus.HeaderConsensusHash(fakeHeader) fakeVote, err := v2consensus.SignVote(attackerKey, network, 1, 0, fakeHash) if err != nil { t.Fatal(err) } - fakeCert, err := attackerValidators.BuildCertificate(fakeProposal, []v2consensus.Vote{fakeVote}) - if err != nil { + fakeCert := v2consensus.Certificate{Network: network, Height: 1, Round: 0, HeaderHash: fakeHash, Votes: []v2consensus.Vote{fakeVote}} + if err := attackerValidators.VerifyCertificate(fakeCert); err != nil { t.Fatal(err) } fakeHeader.CertificateHash = fakeCert.Hash() @@ -167,7 +163,7 @@ func TestReceiptImportRejectsSelfSignedForeignValidatorSet(t *testing.T) { receipt := sharding.CrossShardReceipt{ SourceShard: 0, DestinationShard: 1, SourceHeight: 1, TransactionID: types.HashBytes("tx", []byte("fake")), OutputIndex: 0, - Output: object.OutputSpec{Owner: types.AccountIDFromPublicKey([]byte("recipient")), Kind: object.KindSystem}, + Output: object.OutputSpec{Owner: types.AccountIDFromPublicKey([]byte("recipient")), Kind: object.KindSystem}, SourceStateRoot: candidate.Commitments[0].StateRoot, } if err := runtime.validateReceiptImport(1, ReceiptImport{Header: fakeHeader, Certificate: fakeCert, Validators: attackerValidators, Receipt: receipt}); err != ErrReceiptImport { From 0d8402969a90b946c4621fb9fec81662ff7d81a7 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:21:59 +0200 Subject: [PATCH 056/274] simulate hostile self-signed light snapshots below normal builders --- internal/v2/lightapi/server_test.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/v2/lightapi/server_test.go b/internal/v2/lightapi/server_test.go index 6fd7da42..d1e62bd4 100644 --- a/internal/v2/lightapi/server_test.go +++ b/internal/v2/lightapi/server_test.go @@ -98,16 +98,13 @@ func TestSnapshotRejectsValidatorSetNotCommittedByHeader(t *testing.T) { ValidatorRoot: types.HashBytes("validators", []byte("foreign")), DataRoot: merkle.Root(nil), } - proposal, err := v2consensus.SignProposal(key, header, 0) - if err != nil { - t.Fatal(err) - } - vote, err := v2consensus.SignVote(key, network, 1, 0, v2consensus.HeaderConsensusHash(header)) + headerHash := v2consensus.HeaderConsensusHash(header) + vote, err := v2consensus.SignVote(key, network, 1, 0, headerHash) if err != nil { t.Fatal(err) } - certificate, err := validators.BuildCertificate(proposal, []v2consensus.Vote{vote}) - if err != nil { + certificate := v2consensus.Certificate{Network: network, Height: 1, Round: 0, HeaderHash: headerHash, Votes: []v2consensus.Vote{vote}} + if err := validators.VerifyCertificate(certificate); err != nil { t.Fatal(err) } header.CertificateHash = certificate.Hash() From 50ed9c8955531b60f4a43ef823475afb7c354d80 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:24:38 +0200 Subject: [PATCH 057/274] gofmt hostile cross-shard validator-root test --- internal/v2/node/multishard_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/v2/node/multishard_test.go b/internal/v2/node/multishard_test.go index fcff424e..b7f5cda4 100644 --- a/internal/v2/node/multishard_test.go +++ b/internal/v2/node/multishard_test.go @@ -163,7 +163,7 @@ func TestReceiptImportRejectsSelfSignedForeignValidatorSet(t *testing.T) { receipt := sharding.CrossShardReceipt{ SourceShard: 0, DestinationShard: 1, SourceHeight: 1, TransactionID: types.HashBytes("tx", []byte("fake")), OutputIndex: 0, - Output: object.OutputSpec{Owner: types.AccountIDFromPublicKey([]byte("recipient")), Kind: object.KindSystem}, + Output: object.OutputSpec{Owner: types.AccountIDFromPublicKey([]byte("recipient")), Kind: object.KindSystem}, SourceStateRoot: candidate.Commitments[0].StateRoot, } if err := runtime.validateReceiptImport(1, ReceiptImport{Header: fakeHeader, Certificate: fakeCert, Validators: attackerValidators, Receipt: receipt}); err != ErrReceiptImport { From a8da5d2c19df33e2d47edc16788862ee56d2fca6 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:25:07 +0200 Subject: [PATCH 058/274] fix wallet Citizen trust Merkle typed-array inference --- apps/wallet/src/lib/v2CitizenTrust.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/wallet/src/lib/v2CitizenTrust.ts b/apps/wallet/src/lib/v2CitizenTrust.ts index ba2fb396..efd18ade 100644 --- a/apps/wallet/src/lib/v2CitizenTrust.ts +++ b/apps/wallet/src/lib/v2CitizenTrust.ts @@ -91,11 +91,11 @@ async function validatorSetRoot(validators: CitizenValidatorDTO[]): Promise { const empty = await domainHash('zephyr/merkle/empty/v2', new Uint8Array()) if (leaves.length === 0) return empty - const level = leaves.map(leaf => leaf.slice()) + const level: Uint8Array[] = leaves.map(leaf => leaf.slice()) let target = 1 while (target < level.length) target <<= 1 while (level.length < target) level.push(empty.slice()) - let current = level + let current: Uint8Array[] = level while (current.length > 1) { const next: Uint8Array[] = [] for (let i = 0; i < current.length; i += 2) { From 27c29cba0ee469fe6de8a5abe90060f16cf22edd Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:25:29 +0200 Subject: [PATCH 059/274] derive Citizen trust anchors directly from v2 genesis --- internal/v2/genesis/trust.go | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 internal/v2/genesis/trust.go diff --git a/internal/v2/genesis/trust.go b/internal/v2/genesis/trust.go new file mode 100644 index 00000000..86942fec --- /dev/null +++ b/internal/v2/genesis/trust.go @@ -0,0 +1,44 @@ +package genesis + +import ( + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +type TrustAnchor struct { + Network types.NetworkID + ValidatorRoot types.Hash +} + +func (g Config) ValidatorSet() (v2consensus.ValidatorSet, error) { + network, err := g.NetworkID() + if err != nil { + return v2consensus.ValidatorSet{}, err + } + set := v2consensus.ValidatorSet{Network: network, Validators: make([]v2consensus.Validator, len(g.Validators))} + for i, validator := range g.Validators { + set.Validators[i] = v2consensus.Validator{ + ID: validator.ID, + PublicKey: append([]byte(nil), validator.PublicKey...), + Power: validator.Power, + } + } + if err := set.Validate(); err != nil { + return v2consensus.ValidatorSet{}, ErrInvalidGenesis + } + return set, nil +} + +// TrustAnchor derives the two values a Citizen wallet must embed or obtain from +// a trusted checkpoint before it accepts self-verifiable light data. +func (g Config) TrustAnchor() (TrustAnchor, error) { + set, err := g.ValidatorSet() + if err != nil { + return TrustAnchor{}, err + } + root, err := set.Root() + if err != nil { + return TrustAnchor{}, ErrInvalidGenesis + } + return TrustAnchor{Network: set.Network, ValidatorRoot: root}, nil +} From 926b394a37a6eafeecbe3dd025d2cce754cc6924 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:25:55 +0200 Subject: [PATCH 060/274] map genesis validator fields into v2 trust anchors --- internal/v2/genesis/trust.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/v2/genesis/trust.go b/internal/v2/genesis/trust.go index 86942fec..ec711663 100644 --- a/internal/v2/genesis/trust.go +++ b/internal/v2/genesis/trust.go @@ -19,8 +19,8 @@ func (g Config) ValidatorSet() (v2consensus.ValidatorSet, error) { for i, validator := range g.Validators { set.Validators[i] = v2consensus.Validator{ ID: validator.ID, - PublicKey: append([]byte(nil), validator.PublicKey...), - Power: validator.Power, + PublicKey: append([]byte(nil), validator.ConsensusPublicKey...), + Power: validator.VotingPower, } } if err := set.Validate(); err != nil { From f29f88e2ac3123ebabb2249280ba8010aa3c5a85 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:26:09 +0200 Subject: [PATCH 061/274] test genesis-derived Citizen trust anchors --- internal/v2/genesis/trust_test.go | 57 +++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 internal/v2/genesis/trust_test.go diff --git a/internal/v2/genesis/trust_test.go b/internal/v2/genesis/trust_test.go new file mode 100644 index 00000000..f042a46b --- /dev/null +++ b/internal/v2/genesis/trust_test.go @@ -0,0 +1,57 @@ +package genesis + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestTrustAnchorDerivesNetworkAndValidatorRootFromGenesis(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + validatorID := types.ValidatorIDFromPublicKey(pub) + owner := types.AccountIDFromPublicKey([]byte("genesis-owner")) + config := Config{ + Version: ProtocolVersion, ChainName: "zephyr-trust-test", GenesisUnix: 1, + InitialShardCount: 1, MaxShardCount: 16, NativeSymbol: "ZPH", + Validators: []Validator{{ID: validatorID, ConsensusPublicKey: pub, VotingPower: 10}}, + Allocations: []Allocation{{Owner: owner, Amount: 100}}, + } + anchor, err := config.TrustAnchor() + if err != nil { + t.Fatal(err) + } + network, err := config.NetworkID() + if err != nil { + t.Fatal(err) + } + set, err := config.ValidatorSet() + if err != nil { + t.Fatal(err) + } + root, err := set.Root() + if err != nil { + t.Fatal(err) + } + if anchor.Network != network || anchor.ValidatorRoot != root || types.IsZero32([32]byte(anchor.ValidatorRoot)) { + t.Fatalf("unexpected trust anchor: %+v", anchor) + } +} + +func TestTrustAnchorRejectsNonP256GenesisValidator(t *testing.T) { + pub := []byte("not-a-p256-public-key") + config := Config{ + Version: ProtocolVersion, ChainName: "zephyr-invalid-trust", GenesisUnix: 1, + InitialShardCount: 1, MaxShardCount: 1, NativeSymbol: "ZPH", + Validators: []Validator{{ID: types.ValidatorIDFromPublicKey(pub), ConsensusPublicKey: pub, VotingPower: 1}}, + } + if _, err := config.TrustAnchor(); err != ErrInvalidGenesis { + t.Fatalf("expected invalid genesis trust anchor, got %v", err) + } +} From 6a381610de34427925ea9fb07639a4d1a1b2b53f Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:27:18 +0200 Subject: [PATCH 062/274] update v2 status for trusted Citizen verification and finalized cross-shard execution --- docs/protocol-v2-implementation-status.md | 67 ++++++++++++++++------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/docs/protocol-v2-implementation-status.md b/docs/protocol-v2-implementation-status.md index ec362941..dfd66388 100644 --- a/docs/protocol-v2-implementation-status.md +++ b/docs/protocol-v2-implementation-status.md @@ -8,7 +8,7 @@ Status legend: - **Integrated foundation** — the protocol boundary and correctness rules exist, but a production backend/network/runtime is still to be selected or connected. - **Not production-complete** — must not be presented as a shipped network capability yet. -## Core identity and wire protocol +## Core identity, trust and wire protocol **Implemented** @@ -18,9 +18,14 @@ Status legend: - independent account, node and validator identities; - canonical low-S P-256 signing for proof-carrying transactions and validator consensus messages; - binary proposal, vote and quorum-certificate wire formats; -- binary global-header, shard-commitment, cross-shard-receipt and Merkle-proof decoders. +- binary global-header, shard-commitment, cross-shard-receipt and Merkle-proof decoders; +- canonical Merkle commitment over validator ID, P-256 public key and integer voting power; +- every accepted proposal must commit the exact validator-set root used to authorize it; +- runtime commit and cross-shard import reject validator sets that do not match the header's committed `ValidatorRoot`; +- v2 genesis can derive a `TrustAnchor { NetworkID, ValidatorRoot }` for Citizen wallets; +- genesis trust-anchor derivation rejects validator keys that do not satisfy the consensus P-256 validator-set rules. -The browser/RPC surface may use JSON, but consensus does not depend on JSON canonicalization. +The browser/RPC surface may use JSON, but consensus does not depend on JSON canonicalization. A valid self-signed quorum from an arbitrary validator set is not sufficient: the set itself must be committed by the trusted header/genesis chain. ## Object state and persistence @@ -40,7 +45,8 @@ The browser/RPC surface may use JSON, but consensus does not depend on JSON cano - long-duration database growth/compaction benchmarks; - large-state migration/repair tooling; - production structured-KV/LSM backend comparison; -- archive/history indexing. +- archive/history indexing; +- proof/state allocation reduction under large batches. The WAL/checkpoint backend removes the v1 requirement to serialize the complete node state for every mutation, but it is still a first durable backend rather than the final storage engine selection. @@ -57,7 +63,8 @@ The WAL/checkpoint backend removes the v1 requirement to serialize the complete - deterministic parallel batch executor; - rejection of batches with shared consumed objects, duplicate transactions or different pre-state roots; - atomic merge of independent transaction results; -- state-root simulation before consensus finality. +- state-root simulation before consensus finality; +- permanent shard placement encoded into object IDs for multi-shard state. The key invariant is enforced in code: candidate execution may calculate a future state root, but committed state is not mutated before a valid quorum certificate exists. @@ -71,6 +78,7 @@ The key invariant is enforced in code: candidate execution may calculate a futur - locally reconstructed `2/3+` voting-power quorum; - duplicate-voter rejection; - canonical quorum-certificate hash; +- validator-set Merkle root as a proposal validity invariant; - `GlobalHeader` consensus hash that avoids certificate/hash circularity; - runtime path: @@ -88,29 +96,44 @@ proof-carrying transactions The existing v1 Consensus & Performance Lab remains a regression gate while v2-specific multi-node fault transport integration is expanded. +**Not production-complete** + +- consensus-state-backed validator-set rotation and governance activation; +- trusted header/checkpoint chain for Citizen verification across validator-set rotations; +- v2-specific restart/partition/conflicting-proposal scenarios over the production transport. + ## Sharding **Implemented foundation** -- deterministic shard router; +- deterministic account shard routing; +- permanent shard placement encoded in every object ID so changing active shard count cannot silently relocate existing objects; - per-shard state/data/receipt commitments; - global shard-commitment root; - `GlobalHeader` committing all active shard roots; -- cross-shard receipt format; +- local outputs remain in the source shard only when their owner routes there; +- remote outputs become cross-shard receipts rather than being written into the wrong shard; - receipt Merkle batches and inclusion proofs; - proof that a source receipt belongs to a shard commitment that belongs to a finalized global header; -- destination-shard validation and in-memory anti-replay tracker; -- runtime capable of simulating/committing multiple shard state backends. +- cross-shard import verifies the source quorum certificate, committed validator root, shard proof and receipt proof; +- destination object IDs are deterministic; +- receipt consumption creates a consensus-critical Merkle-state marker, making anti-replay survive restart/checkpoint/snapshot recovery; +- imported receipts cannot be spent in the same block because transactions are anchored to the block pre-state root; +- two-shard end-to-end test: source payment -> finalized receipt -> destination import -> finalized destination coin -> durable replay rejection; +- hostile self-signed foreign validator-set receipts are explicitly rejected; +- runtime can simulate and commit multiple shard state backends without pre-QC mutation. + +The old in-memory `ReceiptTracker` is only an optional transport duplicate-suppression helper; it is not the consensus anti-replay source of truth. **Not production-complete** -- output/object placement rules for active multi-shard execution need final clean-break encoding before `shardCount > 1` is enabled; -- receipt-consumption anti-replay must move from the in-memory tracker into consensus-critical durable state; - shard-aware gossip/recovery is not connected to production transport; -- reshard/split/merge rules are not activated; -- 4/16-shard conformance and throughput evidence is still required. +- validator-set history/checkpoint proofs must support cross-shard imports across committee rotations; +- reshard/split/merge rules and object migration are not activated; +- receipt-marker pruning/history-retention policy needs proof-safe design; +- 4/16-shard conformance, recovery and throughput evidence is still required. -`shardCount = 1` remains the safe activation value until these conditions pass. Sharding is an optimization, not a prerequisite for correctness. +`shardCount = 1` remains the safe public activation value until those gates pass. Sharding is an optimization, not a prerequisite for correctness. ## Citizen Node and smartphone wallet @@ -122,6 +145,7 @@ The existing v1 Consensus & Performance Lab remains a regression gate while v2-s - `/v2/light/status`; - `/v2/light/object`; - proof bundle contains canonical global header, quorum certificate, validator set, shard commitment and Merkle proof, object bytes and Sparse-Merkle proof; +- light snapshots reject a supplied validator set unless its Merkle root equals the `ValidatorRoot` committed by the finalized header; - validator voting power is encoded as decimal text at the JSON boundary to preserve full `uint64` precision in JavaScript; - `apps/wallet/src/lib/v2Citizen.ts` independently reconstructs: - v2 domain hashes; @@ -131,17 +155,20 @@ The existing v1 Consensus & Performance Lab remains a regression gate while v2-s - certificate hash; - shard-commitment inclusion; - object Sparse-Merkle inclusion/absence proof; +- `apps/wallet/src/lib/v2CitizenTrust.ts` is the wallet-facing trust layer and requires a genesis/checkpoint trust anchor before accepting a proof bundle; +- the trusted wallet path independently recomputes the validator-set Merkle root and requires it to match both the trusted anchor and the header; - wallet resource mode selection for header-only, relay, DA sampling/cache and opportunistic recent execution modes. **Not production-complete** +- validator-set rotation needs a verified header/checkpoint transition chain rather than a static trusted root; - the Vue UI does not yet expose the Citizen status/control panel; - current v1 node process does not yet mount a live v2 runtime/provider; - iOS/Android native lifecycle/background adapters are not present; - multi-peer proof comparison, resumable cache and peer relay are not connected yet; - real-device RAM/battery/bandwidth measurements are still required. -No correctness claim may depend on an RPC response that the Citizen verifier cannot authenticate against finalized state. +No correctness claim may depend on an RPC response that the Citizen verifier cannot authenticate back to a genesis/checkpoint trust anchor. ## Smart contracts @@ -217,7 +244,7 @@ Heavy compute is provider-executed; validators verify settlement evidence and do **Not production-complete** -- production erasure code selection; +- production erasure-code selection; - reconstruction; - sampling confidence parameters; - withholding attacks in the fault lab; @@ -249,7 +276,7 @@ Heavy compute is provider-executed; validators verify settlement evidence and do - the timed v2 path includes witness/signature verification, execution/state-root simulation, proposal/votes, quorum certificate and committed state transition; - client workload setup/key generation/signing stays outside the timed consensus path, matching the Lab's canonical workload policy. -No numerical v2 TPS result from a shared CI runner is a production capacity claim. +The first shared-runner v2 samples show only modest worker scaling and very high allocation pressure, so the next optimization target is proof/state allocation and incremental simulation rather than simply increasing goroutine count. No numerical result from a shared CI runner is a production-capacity claim. ## Activation gates @@ -257,12 +284,12 @@ The clean break lets v2 replace prototype boundaries, but it does not remove the 1. v2 multi-validator consensus must run through the fault-injection Lab, including partitions/restarts/conflicting evidence; 2. durable v2 state must survive crash/restart and longer stress runs; -3. Citizen verification must be exercised against a live v2 node from real Android/iOS reference devices; +3. Citizen verification must be exercised against a live v2 node from real Android/iOS reference devices and a genesis/checkpoint trust anchor; 4. one-shard finalized performance must be characterized on controlled hardware; -5. multi-shard mode must remain disabled until object placement, durable receipt anti-replay, recovery and 4/16-shard conformance pass; +5. multi-shard mode must remain disabled until validator-history proofs, shard-aware recovery and 4/16-shard conformance pass; 6. contract execution must have a deterministic metered production runtime; 7. compute settlement must be consensus-state-backed before real value is escrowed; -8. genesis/checkpoint/operator upgrade procedures must be explicit. +8. genesis/checkpoint/operator upgrade and validator-rotation procedures must be explicit. The engineering rule remains: From c2134340540027c38f9784c5c51f59e1d34e6a11 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:29:50 +0200 Subject: [PATCH 063/274] fix genesis trust-anchor validation error mapping --- internal/v2/genesis/trust.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/v2/genesis/trust.go b/internal/v2/genesis/trust.go index ec711663..1e48b23f 100644 --- a/internal/v2/genesis/trust.go +++ b/internal/v2/genesis/trust.go @@ -24,7 +24,7 @@ func (g Config) ValidatorSet() (v2consensus.ValidatorSet, error) { } } if err := set.Validate(); err != nil { - return v2consensus.ValidatorSet{}, ErrInvalidGenesis + return v2consensus.ValidatorSet{}, ErrValidator } return set, nil } @@ -38,7 +38,7 @@ func (g Config) TrustAnchor() (TrustAnchor, error) { } root, err := set.Root() if err != nil { - return TrustAnchor{}, ErrInvalidGenesis + return TrustAnchor{}, ErrValidator } return TrustAnchor{Network: set.Network, ValidatorRoot: root}, nil } From 77541eadd4c2d3ff1f0c689404fb570b0d333e15 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:30:12 +0200 Subject: [PATCH 064/274] fix genesis trust-anchor rejection test --- internal/v2/genesis/trust_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/v2/genesis/trust_test.go b/internal/v2/genesis/trust_test.go index f042a46b..0244082d 100644 --- a/internal/v2/genesis/trust_test.go +++ b/internal/v2/genesis/trust_test.go @@ -51,7 +51,7 @@ func TestTrustAnchorRejectsNonP256GenesisValidator(t *testing.T) { InitialShardCount: 1, MaxShardCount: 1, NativeSymbol: "ZPH", Validators: []Validator{{ID: types.ValidatorIDFromPublicKey(pub), ConsensusPublicKey: pub, VotingPower: 1}}, } - if _, err := config.TrustAnchor(); err != ErrInvalidGenesis { + if _, err := config.TrustAnchor(); err != ErrValidator { t.Fatalf("expected invalid genesis trust anchor, got %v", err) } } From 7d34d16a9eebe6bb173897f5869a028f4b599251 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:31:11 +0200 Subject: [PATCH 065/274] stream canonical domain hashing without temporary buffers --- internal/v2/codec/codec.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/internal/v2/codec/codec.go b/internal/v2/codec/codec.go index efae54ad..e9272ecd 100644 --- a/internal/v2/codec/codec.go +++ b/internal/v2/codec/codec.go @@ -162,9 +162,22 @@ func (r *Reader) Done() error { return nil } +// DomainHash preserves the exact canonical framing used by Writer.String + +// Writer.Bytes, but streams it directly into SHA-256 instead of allocating a +// temporary buffer and then copying that buffer before hashing. Consensus bytes +// and hash outputs therefore remain bit-for-bit compatible while hot Merkle +// paths avoid two transient allocations per hash. func DomainHash(domain string, payload []byte) [32]byte { - var w Writer - w.String(domain) - w.Bytes(payload) - return sha256.Sum256(w.BytesCopy()) + h := sha256.New() + var length [4]byte + binary.BigEndian.PutUint32(length[:], uint32(len(domain))) + _, _ = h.Write(length[:]) + _, _ = h.Write([]byte(domain)) + binary.BigEndian.PutUint32(length[:], uint32(len(payload))) + _, _ = h.Write(length[:]) + _, _ = h.Write(payload) + var out [32]byte + sum := h.Sum(out[:0]) + copy(out[:], sum) + return out } From a48872a24b40c68d116ceda673f4243cea62bbd8 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:31:25 +0200 Subject: [PATCH 066/274] prove streamed domain hashing is bit-for-bit canonical --- internal/v2/codec/codec_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/v2/codec/codec_test.go b/internal/v2/codec/codec_test.go index fd977a36..2d55d5d3 100644 --- a/internal/v2/codec/codec_test.go +++ b/internal/v2/codec/codec_test.go @@ -2,6 +2,7 @@ package codec import ( "bytes" + "crypto/sha256" "testing" ) @@ -49,3 +50,23 @@ func TestDomainHashSeparatesDomains(t *testing.T) { t.Fatal("domain-separated hashes collided") } } + +func TestDomainHashMatchesCanonicalWriterFraming(t *testing.T) { + cases := []struct { + domain string + payload []byte + }{ + {domain: "zephyr/smt/branch/v2", payload: bytes.Repeat([]byte{0x42}, 64)}, + {domain: "zephyr/transaction/v2", payload: []byte("proof-carrying")}, + {domain: "", payload: nil}, + } + for _, tc := range cases { + var legacy Writer + legacy.String(tc.domain) + legacy.Bytes(tc.payload) + expected := sha256.Sum256(legacy.BytesCopy()) + if got := DomainHash(tc.domain, tc.payload); got != expected { + t.Fatalf("domain hash framing changed for %q", tc.domain) + } + } +} From 27f345a5200af0329098b58df8fe86fd59b8dfb8 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:32:02 +0200 Subject: [PATCH 067/274] remove transient allocations from sparse Merkle branch hashing --- internal/v2/state/smt.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/v2/state/smt.go b/internal/v2/state/smt.go index 0f342708..8b6799fd 100644 --- a/internal/v2/state/smt.go +++ b/internal/v2/state/smt.go @@ -245,10 +245,10 @@ func leafHash(key types.Hash, value []byte) types.Hash { } func branchHash(left, right types.Hash) types.Hash { - var w codec.Writer - w.Fixed(left[:]) - w.Fixed(right[:]) - return types.Hash(codec.DomainHash("zephyr/smt/branch/v2", w.BytesCopy())) + var payload [64]byte + copy(payload[:32], left[:]) + copy(payload[32:], right[:]) + return types.Hash(codec.DomainHash("zephyr/smt/branch/v2", payload[:])) } func prefixAtDepth(key [32]byte, depth int) [32]byte { From 12462de1d021ed8dc06dc98394ebeb41b5d43a1a Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:32:20 +0200 Subject: [PATCH 068/274] remove transient allocations from Merkle branch hashing --- internal/v2/merkle/merkle.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/v2/merkle/merkle.go b/internal/v2/merkle/merkle.go index f2757ace..8c9604c1 100644 --- a/internal/v2/merkle/merkle.go +++ b/internal/v2/merkle/merkle.go @@ -89,10 +89,10 @@ func Leaf(domain string, payload []byte) types.Hash { } func branch(left, right types.Hash) types.Hash { - var w codec.Writer - w.Fixed(left[:]) - w.Fixed(right[:]) - return types.Hash(codec.DomainHash("zephyr/merkle/branch/v2", w.BytesCopy())) + var payload [64]byte + copy(payload[:32], left[:]) + copy(payload[32:], right[:]) + return types.Hash(codec.DomainHash("zephyr/merkle/branch/v2", payload[:])) } func emptyLeaf() types.Hash { From eeb747c848f767cd317da449e8222748db5f2805 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:32:45 +0200 Subject: [PATCH 069/274] simulate sparse Merkle updates with a copy-on-write overlay --- internal/v2/state/preview.go | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 internal/v2/state/preview.go diff --git a/internal/v2/state/preview.go b/internal/v2/state/preview.go new file mode 100644 index 00000000..9d4b7ff0 --- /dev/null +++ b/internal/v2/state/preview.go @@ -0,0 +1,72 @@ +package state + +import ( + "bytes" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +// Preview returns the state root that would result from applying updates while +// leaving the committed tree untouched. It stores only nodes on affected paths +// in an overlay instead of cloning the full tree, which keeps proposal +// simulation proportional to the changed working set rather than total state. +func (t *Tree) Preview(updates map[types.Hash][]byte) types.Hash { + t.mu.RLock() + defer t.mu.RUnlock() + if len(updates) == 0 { + return t.getNode(0, [32]byte{}) + } + + keys := make([]types.Hash, 0, len(updates)) + for key := range updates { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { return bytes.Compare(keys[i][:], keys[j][:]) < 0 }) + + overlay := make(map[nodeKey]types.Hash, len(keys)*Depth/2) + lookup := func(depth int, prefix [32]byte) types.Hash { + key := nodeKey{Depth: uint16(depth), Prefix: prefixAtDepth(prefix, depth)} + if hash, ok := overlay[key]; ok { + return hash + } + if hash, ok := t.nodes[key]; ok { + return hash + } + return t.defaults[depth] + } + + for _, key := range keys { + rawKey := [32]byte(key) + leafKey := nodeKey{Depth: Depth, Prefix: rawKey} + value := updates[key] + if value == nil { + overlay[leafKey] = t.defaults[Depth] + } else { + overlay[leafKey] = leafHash(key, value) + } + + for depth := Depth - 1; depth >= 0; depth-- { + bit := bitAt(rawKey, depth) + childPrefix := prefixAtDepth(rawKey, depth+1) + siblingPrefix := childPrefix + toggleBit(&siblingPrefix, depth) + child := lookup(depth+1, childPrefix) + sibling := lookup(depth+1, siblingPrefix) + var left, right types.Hash + if bit == 0 { + left, right = child, sibling + } else { + left, right = sibling, child + } + parentKey := nodeKey{Depth: uint16(depth), Prefix: prefixAtDepth(rawKey, depth)} + overlay[parentKey] = branchHash(left, right) + } + } + + rootKey := nodeKey{Depth: 0, Prefix: [32]byte{}} + if root, ok := overlay[rootKey]; ok { + return root + } + return t.getNode(0, [32]byte{}) +} From 46816089717fceb81539178509fe1c00b8f46446 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:33:07 +0200 Subject: [PATCH 070/274] use copy-on-write sparse Merkle preview for proposal simulation --- internal/v2/worldstate/simulate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/v2/worldstate/simulate.go b/internal/v2/worldstate/simulate.go index 76d73758..5e776191 100644 --- a/internal/v2/worldstate/simulate.go +++ b/internal/v2/worldstate/simulate.go @@ -49,7 +49,7 @@ func (m *Memory) Simulate(consumed []types.ObjectID, created []object.Object) (t hash := item.Hash() updates[types.Hash(item.ID)] = hash[:] } - return m.tree.Clone().Apply(updates), nil + return m.tree.Preview(updates), nil } func (d *Disk) Simulate(consumed []types.ObjectID, created []object.Object) (types.Hash, error) { From 9a9a9735a2f16325bc94175a9a488ac141135388 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:33:19 +0200 Subject: [PATCH 071/274] prove copy-on-write state preview matches real sparse Merkle apply --- internal/v2/state/preview_test.go | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 internal/v2/state/preview_test.go diff --git a/internal/v2/state/preview_test.go b/internal/v2/state/preview_test.go new file mode 100644 index 00000000..16170fb2 --- /dev/null +++ b/internal/v2/state/preview_test.go @@ -0,0 +1,52 @@ +package state + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestPreviewMatchesApplyWithoutMutatingCommittedTree(t *testing.T) { + tree := NewTree() + keyA := types.HashBytes("key", []byte("a")) + keyB := types.HashBytes("key", []byte("b")) + keyC := types.HashBytes("key", []byte("c")) + keyD := types.HashBytes("key", []byte("d")) + tree.Apply(map[types.Hash][]byte{ + keyA: []byte("one"), + keyB: []byte("two"), + keyC: []byte("three"), + }) + before := tree.Root() + updates := map[types.Hash][]byte{ + keyA: []byte("ONE"), + keyB: nil, + keyD: []byte("four"), + } + + preview := tree.Preview(updates) + if tree.Root() != before { + t.Fatal("preview mutated committed sparse Merkle root") + } + if value, ok := tree.Get(keyA); !ok || string(value) != "one" { + t.Fatal("preview mutated committed values") + } + + clone := tree.Clone() + applied := clone.Apply(updates) + if preview != applied { + t.Fatalf("preview root %s does not match applied root %s", preview, applied) + } + if preview == before { + t.Fatal("preview did not reflect updates") + } +} + +func TestPreviewEmptyUpdatesReturnsCurrentRoot(t *testing.T) { + tree := NewTree() + key := types.HashBytes("key", []byte("only")) + tree.Update(key, []byte("value")) + if got := tree.Preview(nil); got != tree.Root() { + t.Fatalf("empty preview changed root: %s != %s", got, tree.Root()) + } +} From 5a9a4def20214d9f76a4cf35f647df2dbf343b22 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:36:22 +0200 Subject: [PATCH 072/274] add QC-backed v2 validator-set transition path --- internal/v2/node/validator_transition.go | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 internal/v2/node/validator_transition.go diff --git a/internal/v2/node/validator_transition.go b/internal/v2/node/validator_transition.go new file mode 100644 index 00000000..47532a13 --- /dev/null +++ b/internal/v2/node/validator_transition.go @@ -0,0 +1,42 @@ +package node + +import ( + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +// ScheduleValidatorTransition commits the next committee root into a candidate +// before proposal signing. The current committee still signs/finalizes this +// header; the next committee only becomes active for the following height. +func ScheduleValidatorTransition(candidate *Candidate, current, next v2consensus.ValidatorSet) error { + if candidate == nil || current.Network != candidate.Header.Network || next.Network != candidate.Header.Network { + return ErrCandidateCert + } + currentRoot, err := current.Root() + if err != nil || currentRoot != candidate.Header.ValidatorRoot { + return ErrCandidateCert + } + nextRoot, err := next.Root() + if err != nil || types.IsZero32([32]byte(nextRoot)) { + return ErrCandidateCert + } + candidate.Header.NextValidatorRoot = nextRoot + return nil +} + +// CommitWithValidatorTransition finalizes a candidate with the current +// validator set and, only after that QC-backed commit succeeds, advances the +// runtime trust root for the next height. If no NextValidatorRoot is present, +// the current root remains active. +func (r *Runtime) CommitWithValidatorTransition(candidate Candidate, certificate v2consensus.Certificate, current v2consensus.ValidatorSet) (sharding.GlobalHeader, error) { + finalized, err := r.Commit(candidate, certificate, current) + if err != nil { + return sharding.GlobalHeader{}, err + } + nextRoot := finalized.EffectiveNextValidatorRoot() + r.mu.Lock() + r.ValidatorRoot = nextRoot + r.mu.Unlock() + return finalized, nil +} From b9f2c682abf563d8616b29bf8a6d36da00fbd382 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:36:34 +0200 Subject: [PATCH 073/274] verify QC-backed validator-set transitions for Citizen trust chains --- internal/v2/consensus/transition.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 internal/v2/consensus/transition.go diff --git a/internal/v2/consensus/transition.go b/internal/v2/consensus/transition.go new file mode 100644 index 00000000..a8901fe0 --- /dev/null +++ b/internal/v2/consensus/transition.go @@ -0,0 +1,26 @@ +package consensus + +import ( + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" +) + +// VerifyCertifiedTransition proves that a currently trusted validator set +// finalized a header which authorizes next as the validator set for the next +// height. This is the trust-chain primitive used by Citizen checkpoints. +func VerifyCertifiedTransition(header sharding.GlobalHeader, certificate Certificate, current, next ValidatorSet) error { + currentRoot, err := current.Root() + if err != nil || current.Network != header.Network || currentRoot != header.ValidatorRoot { + return ErrValidatorSet + } + if certificate.Network != header.Network || certificate.Height != header.Height || certificate.HeaderHash != HeaderConsensusHash(header) || header.CertificateHash != certificate.Hash() { + return ErrCertificate + } + if err := current.VerifyCertificate(certificate); err != nil { + return err + } + nextRoot, err := next.Root() + if err != nil || next.Network != header.Network || nextRoot != header.EffectiveNextValidatorRoot() { + return ErrValidatorSet + } + return nil +} From 61cfb74d8495f3c6c265659174b6c867be8dd9b6 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:36:52 +0200 Subject: [PATCH 074/274] test QC-backed v2 validator-set rotation --- internal/v2/node/validator_transition_test.go | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 internal/v2/node/validator_transition_test.go diff --git a/internal/v2/node/validator_transition_test.go b/internal/v2/node/validator_transition_test.go new file mode 100644 index 00000000..3d4ab091 --- /dev/null +++ b/internal/v2/node/validator_transition_test.go @@ -0,0 +1,84 @@ +package node + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestValidatorTransitionActivatesOnlyAfterCurrentQC(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("validator-transition"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + currentKey, current := transitionValidatorSet(t, network, "current") + _, next := transitionValidatorSet(t, network, "next") + currentRoot, err := current.Root() + if err != nil { + t.Fatal(err) + } + nextRoot, err := next.Root() + if err != nil { + t.Fatal(err) + } + runtime, err := NewRuntime(network, native, currentRoot, map[uint32]worldstate.Backend{0: worldstate.NewMemory()}, 1) + if err != nil { + t.Fatal(err) + } + candidate, err := runtime.BuildCandidate(1, nil) + if err != nil { + t.Fatal(err) + } + if err := ScheduleValidatorTransition(&candidate, current, next); err != nil { + t.Fatal(err) + } + if candidate.Header.ValidatorRoot != currentRoot || candidate.Header.NextValidatorRoot != nextRoot { + t.Fatal("candidate did not bind current and next validator roots") + } + proposal, err := v2consensus.SignProposal(currentKey, candidate.Header, 0) + if err != nil { + t.Fatal(err) + } + vote, err := v2consensus.SignVote(currentKey, network, 1, 0, v2consensus.HeaderConsensusHash(candidate.Header)) + if err != nil { + t.Fatal(err) + } + certificate, err := current.BuildCertificate(proposal, []v2consensus.Vote{vote}) + if err != nil { + t.Fatal(err) + } + finalized, err := runtime.CommitWithValidatorTransition(candidate, certificate, current) + if err != nil { + t.Fatal(err) + } + if runtime.ValidatorRoot != nextRoot || finalized.EffectiveNextValidatorRoot() != nextRoot { + t.Fatal("next validator root did not activate after QC") + } + if err := v2consensus.VerifyCertifiedTransition(finalized, certificate, current, next); err != nil { + t.Fatal(err) + } + + candidate2, err := runtime.BuildCandidate(2, nil) + if err != nil { + t.Fatal(err) + } + if candidate2.Header.ValidatorRoot != nextRoot { + t.Fatal("following candidate does not use transitioned validator root") + } + _ = merkle.Root(nil) +} + +func transitionValidatorSet(t *testing.T, network types.NetworkID, label string) (*ecdsa.PrivateKey, v2consensus.ValidatorSet) { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + id := types.ValidatorIDFromPublicKey(pub) + return key, v2consensus.ValidatorSet{Network: network, Validators: []v2consensus.Validator{{ID: id, PublicKey: pub, Power: uint64(len(label) + 1)}}} +} From d5dda107c995dd253b81ba66cf77bcadc2c512ba Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:39:11 +0200 Subject: [PATCH 075/274] add strict wallet Citizen verifier with validator trust-chain rotation --- apps/wallet/src/lib/v2CitizenTrusted.ts | 310 ++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 apps/wallet/src/lib/v2CitizenTrusted.ts diff --git a/apps/wallet/src/lib/v2CitizenTrusted.ts b/apps/wallet/src/lib/v2CitizenTrusted.ts new file mode 100644 index 00000000..00bc091f --- /dev/null +++ b/apps/wallet/src/lib/v2CitizenTrusted.ts @@ -0,0 +1,310 @@ +export interface TrustedValidator { + id: string + publicKey: string + power: string +} + +export interface TrustedCitizenBundle { + network: string + height: number + shardId: number + header: string + certificate: string + commitment: string + commitmentProof: string + objectId: string + objectPresent: boolean + object?: string + stateProof: string + validators: TrustedValidator[] +} + +export interface CitizenTrustAnchor { + network: string + validatorRoot: string +} + +export interface TrustedCitizenObject { + network: string + height: bigint + shardId: number + objectId: string + objectPresent: boolean + objectBytes?: Uint8Array + stateRoot: string + nextTrustAnchor: CitizenTrustAnchor +} + +const ORDER = BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551') +const HALF_ORDER = ORDER >> 1n +const encoder = new TextEncoder() + +class Reader { + private offset = 0 + constructor(private readonly data: Uint8Array) {} + u8(): number { return this.take(1)[0] } + u16(): number { const b = this.take(2); return (b[0] << 8) | b[1] } + u32(): number { const b = this.take(4); return ((b[0] * 0x1000000) + (b[1] << 16) + (b[2] << 8) + b[3]) >>> 0 } + u64(): bigint { let n = 0n; for (const b of this.take(8)) n = (n << 8n) | BigInt(b); return n } + fixed(n: number): Uint8Array { return this.take(n) } + bytes(max: number): Uint8Array { const n = this.u32(); if (n > max) throw new Error('Citizen field exceeds protocol limit'); return this.take(n) } + done(): void { if (this.offset !== this.data.length) throw new Error('Citizen payload has trailing data') } + private take(n: number): Uint8Array { if (n < 0 || this.offset + n > this.data.length) throw new Error('Citizen payload is truncated'); const out = this.data.slice(this.offset, this.offset + n); this.offset += n; return out } +} + +class Writer { + private chunks: Uint8Array[] = [] + u32(n: number): void { this.chunks.push(new Uint8Array([(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255])) } + u64(n: bigint): void { const out = new Uint8Array(8); for (let i = 7; i >= 0; i--) { out[i] = Number(n & 255n); n >>= 8n }; this.chunks.push(out) } + fixed(v: Uint8Array): void { this.chunks.push(v) } + bytes(v: Uint8Array): void { this.u32(v.length); this.fixed(v) } + result(): Uint8Array { return concat(...this.chunks) } +} + +type Header = { + raw: Uint8Array + network: Uint8Array + height: bigint + shardRoot: Uint8Array + validatorRoot: Uint8Array + nextValidatorRoot: Uint8Array + certificateHash: Uint8Array +} +type Commitment = { raw: Uint8Array, shard: number, stateRoot: Uint8Array } +type MerkleProof = { index: number, leafCount: number, siblings: Uint8Array[] } +type StateProof = { exists: boolean, bitmap: Uint8Array, siblings: Uint8Array[] } +type Vote = { network: Uint8Array, height: bigint, round: bigint, headerHash: Uint8Array, voter: Uint8Array, publicKey: Uint8Array, signature: Uint8Array } +type Certificate = { network: Uint8Array, height: bigint, round: bigint, headerHash: Uint8Array, votes: Vote[] } + +export async function fetchTrustedCitizenObject(baseURL: string, shardId: number, objectId: string, anchor: CitizenTrustAnchor): Promise { + const url = new URL('/v2/light/object', new URL(baseURL, window.location.origin)) + url.searchParams.set('shard', String(shardId)) + url.searchParams.set('id', objectId) + const response = await fetch(url) + if (!response.ok) throw new Error(`Citizen proof request failed (${response.status})`) + return verifyTrustedCitizenBundle(await response.json() as TrustedCitizenBundle, anchor) +} + +export async function verifyTrustedCitizenBundle(bundle: TrustedCitizenBundle, anchor: CitizenTrustAnchor): Promise { + const trustedNetwork = hex32(anchor.network, 'network trust anchor') + const trustedValidatorRoot = hex32(anchor.validatorRoot, 'validator trust anchor') + const header = parseHeader(b64(bundle.header)) + if (!eq(header.network, trustedNetwork) || bundle.network.toLowerCase() !== toHex(trustedNetwork)) throw new Error('Citizen bundle is from an untrusted network') + if (!eq(header.validatorRoot, trustedValidatorRoot)) throw new Error('Citizen header uses an untrusted validator set') + + const calculatedValidatorRoot = await validatorRoot(bundle.validators) + if (!eq(calculatedValidatorRoot, header.validatorRoot)) throw new Error('Citizen validator set does not match header commitment') + const certificate = parseCertificate(b64(bundle.certificate)) + await verifyCertificate(header, certificate, bundle.validators) + + const commitment = parseCommitment(b64(bundle.commitment)) + if (commitment.shard !== bundle.shardId) throw new Error('Citizen shard commitment mismatch') + if (!await verifyMerkle(header.shardRoot, await leaf('shard-commitment', commitment.raw), parseMerkleProof(b64(bundle.commitmentProof)))) throw new Error('Shard commitment is not finalized') + + const id = hex32(bundle.objectId, 'object ID') + const proof = parseStateProof(b64(bundle.stateProof)) + if (proof.exists !== bundle.objectPresent) throw new Error('Citizen state-proof presence mismatch') + let objectBytes: Uint8Array | undefined + let value: Uint8Array | undefined + if (bundle.objectPresent) { + if (!bundle.object) throw new Error('Citizen object bytes are missing') + objectBytes = b64(bundle.object) + if (objectBytes.length < 32 || !eq(objectBytes.slice(0, 32), id)) throw new Error('Citizen object ID mismatch') + value = await dh('zephyr/object/v2', objectBytes) + } + if (!await verifySMT(commitment.stateRoot, id, value, proof)) throw new Error('Citizen object state proof is invalid') + + const nextRoot = isZero(header.nextValidatorRoot) ? header.validatorRoot : header.nextValidatorRoot + return { + network: toHex(header.network), height: header.height, shardId: commitment.shard, + objectId: toHex(id), objectPresent: bundle.objectPresent, objectBytes, + stateRoot: toHex(commitment.stateRoot), + nextTrustAnchor: { network: toHex(header.network), validatorRoot: toHex(nextRoot) } + } +} + +function parseHeader(raw: Uint8Array): Header { + if (raw.length !== 234) throw new Error('Invalid Zephyr v2 GlobalHeader size') + const r = new Reader(raw) + if (r.u16() !== 2) throw new Error('Unsupported Zephyr header version') + const network = r.fixed(32) + const height = r.u64() + r.fixed(32) + const shardRoot = r.fixed(32) + const validatorRoot = r.fixed(32) + const nextValidatorRoot = r.fixed(32) + r.fixed(32) + const certificateHash = r.fixed(32) + r.done() + if (height === 0n || isZero(network) || isZero(shardRoot) || isZero(validatorRoot)) throw new Error('Invalid Zephyr v2 GlobalHeader') + return { raw, network, height, shardRoot, validatorRoot, nextValidatorRoot, certificateHash } +} + +function parseCommitment(raw: Uint8Array): Commitment { + const r = new Reader(raw) + const shard = r.u32() + const stateRoot = r.fixed(32) + r.fixed(32) + r.fixed(32) + r.done() + if (isZero(stateRoot)) throw new Error('Invalid shard commitment') + return { raw, shard, stateRoot } +} + +function parseMerkleProof(raw: Uint8Array): MerkleProof { + const r = new Reader(raw) + const index = r.u32(), leafCount = r.u32(), count = r.u32() + if (leafCount === 0 || index >= leafCount || count > 32) throw new Error('Invalid Merkle proof') + const siblings: Uint8Array[] = [] + for (let i = 0; i < count; i++) siblings.push(r.fixed(32)) + r.done() + return { index, leafCount, siblings } +} + +function parseStateProof(raw: Uint8Array): StateProof { + const r = new Reader(raw) + const exists = r.u8() + if (exists > 1) throw new Error('Invalid state proof') + const bitmap = r.fixed(32), count = r.u16() + if (count > 256) throw new Error('Invalid state proof') + const siblings: Uint8Array[] = [] + for (let i = 0; i < count; i++) siblings.push(r.fixed(32)) + r.done() + let bits = 0 + for (let i = 0; i < 256; i++) if (bitmapBit(bitmap, i)) bits++ + if (bits !== siblings.length) throw new Error('Invalid state-proof bitmap') + return { exists: exists === 1, bitmap, siblings } +} + +function parseVote(raw: Uint8Array): Vote { + const r = new Reader(raw) + const network = r.fixed(32), height = r.u64(), round = r.u64(), headerHash = r.fixed(32), voter = r.fixed(32) + const publicKey = r.bytes(65), signature = r.bytes(64) + r.done() + if (height === 0n || publicKey.length !== 65 || signature.length !== 64) throw new Error('Invalid validator vote') + return { network, height, round, headerHash, voter, publicKey, signature } +} + +function parseCertificate(raw: Uint8Array): Certificate { + const r = new Reader(raw) + const network = r.fixed(32), height = r.u64(), round = r.u64(), headerHash = r.fixed(32), count = r.u32() + if (height === 0n || count === 0 || count > 4096) throw new Error('Invalid quorum certificate') + const votes: Vote[] = [] + for (let i = 0; i < count; i++) votes.push(parseVote(r.bytes(512))) + r.done() + return { network, height, round, headerHash, votes } +} + +async function verifyCertificate(header: Header, certificate: Certificate, validators: TrustedValidator[]): Promise { + const unsigned = header.raw.slice() + unsigned.fill(0, unsigned.length - 32) + const headerHash = await dh('zephyr/global-header-consensus/v2', unsigned) + if (!eq(certificate.network, header.network) || certificate.height !== header.height || !eq(certificate.headerHash, headerHash)) throw new Error('QC does not target finalized header') + + const set = new Map() + let total = 0n + for (const validator of validators) { + const id = hex32(validator.id, 'validator ID'), key = b64(validator.publicKey), power = BigInt(validator.power) + if (key.length !== 65 || power <= 0n || power > 0xffffffffffffffffn || !eq(await dh('zephyr/validator-id/v2', key), id)) throw new Error('Invalid validator identity') + const idHex = toHex(id) + if (set.has(idHex)) throw new Error('Duplicate validator') + set.set(idHex, { key, power }); total += power + } + let signed = 0n + const seen = new Set() + for (const vote of certificate.votes) { + if (!eq(vote.network, certificate.network) || vote.height !== certificate.height || vote.round !== certificate.round || !eq(vote.headerHash, certificate.headerHash)) throw new Error('QC contains vote for another target') + const id = toHex(vote.voter), validator = set.get(id) + if (!validator || seen.has(id) || !eq(validator.key, vote.publicKey)) throw new Error('QC contains unauthorized or duplicate vote') + seen.add(id) + if (!lowS(vote.signature) || !await verifyVote(vote)) throw new Error('QC contains invalid signature') + signed += validator.power + } + if (signed < (total * 2n) / 3n + 1n) throw new Error('QC is below 2/3+ voting power') + if (!eq(await certificateHash(certificate), header.certificateHash)) throw new Error('QC hash does not match header') +} + +async function verifyVote(vote: Vote): Promise { + const w = new Writer(); w.fixed(vote.network); w.u64(vote.height); w.u64(vote.round); w.fixed(vote.headerHash); w.fixed(vote.voter) + const framed = frame('zephyr/consensus/vote/v2', w.result()) + try { + const key = await crypto.subtle.importKey('raw', ab(vote.publicKey), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']) + return crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, ab(vote.signature), ab(framed)) + } catch { return false } +} + +async function certificateHash(c: Certificate): Promise { + const votes = [...c.votes].sort((a, b) => cmp(a.voter, b.voter)) + const w = new Writer(); w.fixed(c.network); w.u64(c.height); w.u64(c.round); w.fixed(c.headerHash); w.u32(votes.length) + for (const vote of votes) { w.fixed(vote.voter); w.bytes(vote.publicKey); w.bytes(vote.signature) } + return dh('zephyr/quorum-certificate/v2', w.result()) +} + +async function validatorRoot(validators: TrustedValidator[]): Promise { + const items: Array<{ id: Uint8Array, key: Uint8Array, power: bigint }> = [] + for (const validator of validators) items.push({ id: hex32(validator.id, 'validator ID'), key: b64(validator.publicKey), power: BigInt(validator.power) }) + items.sort((a, b) => cmp(a.id, b.id)) + const leaves: Uint8Array[] = [] + for (const item of items) { + const w = new Writer(); w.fixed(item.id); w.bytes(item.key); w.u64(item.power) + leaves.push(await leaf('validator', w.result())) + } + return merkleRoot(leaves) +} + +async function verifyMerkle(root: Uint8Array, leafHash: Uint8Array, proof: MerkleProof): Promise { + let target = 1; while (target < proof.leafCount) target <<= 1 + let depth = 0; for (let n = target; n > 1; n >>= 1) depth++ + if (proof.siblings.length !== depth) return false + let current = leafHash, position = proof.index + for (const sibling of proof.siblings) { current = position % 2 === 0 ? await branch(current, sibling) : await branch(sibling, current); position = Math.floor(position / 2) } + return eq(current, root) +} + +async function merkleRoot(leaves: Uint8Array[]): Promise { + const empty = await dh('zephyr/merkle/empty/v2', new Uint8Array()) + if (leaves.length === 0) return empty + let current: Uint8Array[] = leaves.map(x => x.slice()) + let target = 1; while (target < current.length) target <<= 1 + while (current.length < target) current.push(empty.slice()) + while (current.length > 1) { const next: Uint8Array[] = []; for (let i = 0; i < current.length; i += 2) next.push(await branch(current[i], current[i + 1])); current = next } + return current[0] +} + +let smtDefaultsPromise: Promise | undefined +async function smtDefaults(): Promise { + if (!smtDefaultsPromise) smtDefaultsPromise = (async () => { const d: Uint8Array[] = new Array(257); d[256] = await dh('zephyr/smt/empty-leaf/v2', new Uint8Array()); for (let i = 255; i >= 0; i--) d[i] = await smtBranch(d[i + 1], d[i + 1]); return d })() + return smtDefaultsPromise +} + +async function verifySMT(root: Uint8Array, key: Uint8Array, value: Uint8Array | undefined, proof: StateProof): Promise { + if (proof.exists !== (value !== undefined)) return false + const defaults = await smtDefaults() + let current = value ? await smtLeaf(key, value) : defaults[256], siblingIndex = 0 + for (let i = 0; i < 256; i++) { + const depth = 256 - i + let sibling = defaults[depth] + if (bitmapBit(proof.bitmap, i)) { if (siblingIndex >= proof.siblings.length) return false; sibling = proof.siblings[siblingIndex++] } + const bitIndex = depth - 1, bit = (key[Math.floor(bitIndex / 8)] >> (7 - (bitIndex % 8))) & 1 + current = bit === 0 ? await smtBranch(current, sibling) : await smtBranch(sibling, current) + } + return siblingIndex === proof.siblings.length && eq(current, root) +} + +async function leaf(domain: string, payload: Uint8Array): Promise { return dh(`zephyr/merkle/leaf/v2/${domain}`, payload) } +async function branch(a: Uint8Array, b: Uint8Array): Promise { return dh('zephyr/merkle/branch/v2', concat(a, b)) } +async function smtBranch(a: Uint8Array, b: Uint8Array): Promise { return dh('zephyr/smt/branch/v2', concat(a, b)) } +async function smtLeaf(key: Uint8Array, value: Uint8Array): Promise { const w = new Writer(); w.fixed(key); w.bytes(value); return dh('zephyr/smt/leaf/v2', w.result()) } +async function dh(domain: string, payload: Uint8Array): Promise { return new Uint8Array(await crypto.subtle.digest('SHA-256', ab(frame(domain, payload)))) } +function frame(domain: string, payload: Uint8Array): Uint8Array { const w = new Writer(); w.bytes(encoder.encode(domain)); w.bytes(payload); return w.result() } +function bitmapBit(bitmap: Uint8Array, index: number): boolean { return (bitmap[Math.floor(index / 8)] & (1 << (index % 8))) !== 0 } +function lowS(sig: Uint8Array): boolean { if (sig.length !== 64) return false; const r = bigint(sig.slice(0, 32)), s = bigint(sig.slice(32)); return r > 0n && s > 0n && r < ORDER && s <= HALF_ORDER } +function bigint(v: Uint8Array): bigint { let n = 0n; for (const b of v) n = (n << 8n) | BigInt(b); return n } +function b64(v: string): Uint8Array { const raw = atob(v), out = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i); return out } +function hex32(v: string, label: string): Uint8Array { const s = v.trim().toLowerCase(); if (!/^[0-9a-f]{64}$/.test(s)) throw new Error(`${label} must be 32-byte hex`); const out = new Uint8Array(32); for (let i = 0; i < 32; i++) out[i] = Number.parseInt(s.slice(i * 2, i * 2 + 2), 16); return out } +function toHex(v: Uint8Array): string { return Array.from(v, b => b.toString(16).padStart(2, '0')).join('') } +function isZero(v: Uint8Array): boolean { let x = 0; for (const b of v) x |= b; return x === 0 } +function eq(a: Uint8Array, b: Uint8Array): boolean { if (a.length !== b.length) return false; let x = 0; for (let i = 0; i < a.length; i++) x |= a[i] ^ b[i]; return x === 0 } +function cmp(a: Uint8Array, b: Uint8Array): number { for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] !== b[i]) return a[i] - b[i]; return a.length - b.length } +function concat(...values: Uint8Array[]): Uint8Array { const out = new Uint8Array(values.reduce((n, v) => n + v.length, 0)); let at = 0; for (const v of values) { out.set(v, at); at += v.length }; return out } +function ab(v: Uint8Array): ArrayBuffer { return v.buffer.slice(v.byteOffset, v.byteOffset + v.byteLength) as ArrayBuffer } From 378d4a252741ac0aba8f855dd6b71d55cd592aeb Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:40:38 +0200 Subject: [PATCH 076/274] gate v2 votes on independently executed candidate headers --- internal/v2/node/proposal.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 internal/v2/node/proposal.go diff --git a/internal/v2/node/proposal.go b/internal/v2/node/proposal.go new file mode 100644 index 00000000..b95b3bbb --- /dev/null +++ b/internal/v2/node/proposal.go @@ -0,0 +1,19 @@ +package node + +import ( + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" +) + +// VerifyProposalAgainstCandidate is the pre-vote safety gate. Cryptographic +// proposal validity is necessary but not sufficient: an honest validator only +// votes when its independently executed candidate produces the exact same +// consensus GlobalHeader. +func VerifyProposalAgainstCandidate(candidate Candidate, proposal v2consensus.Proposal, validators v2consensus.ValidatorSet) error { + if err := validators.VerifyProposal(proposal); err != nil { + return err + } + if v2consensus.HeaderConsensusHash(candidate.Header) != v2consensus.HeaderConsensusHash(proposal.Header) { + return ErrCandidateState + } + return nil +} From 58ceb5faa6782d8551ca3100de1dc5530cd1829a Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:41:18 +0200 Subject: [PATCH 077/274] add v2 seven-validator partition and conflicting-proposal lab --- internal/v2/lab/lab_test.go | 226 ++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 internal/v2/lab/lab_test.go diff --git a/internal/v2/lab/lab_test.go b/internal/v2/lab/lab_test.go new file mode 100644 index 00000000..ab7b8562 --- /dev/null +++ b/internal/v2/lab/lab_test.go @@ -0,0 +1,226 @@ +package lab + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" + "testing" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/node" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +type cluster struct { + network types.NetworkID + native types.TokenID + validators v2consensus.ValidatorSet + keys map[types.ValidatorID]*ecdsa.PrivateKey + runtimes []*node.Runtime + candidates []node.Candidate +} + +func TestV2LabSevenValidatorsCertifiedHappyPath(t *testing.T) { + c := newCluster(t, 7) + proposal := c.proposal(t) + votes := c.votes(t, proposal, []int{0, 1, 2, 3, 4}) + certificate, err := c.validators.BuildCertificate(proposal, votes) + if err != nil { + t.Fatal(err) + } + for i := range c.runtimes { + if _, err := c.runtimes[i].Commit(c.candidates[i], certificate, c.validators); err != nil { + t.Fatalf("validator %d failed certified commit: %v", i, err) + } + if c.runtimes[i].Height != 1 { + t.Fatalf("validator %d did not finalize height 1", i) + } + } +} + +func TestV2LabFourThreePartitionStallsThenHeals(t *testing.T) { + c := newCluster(t, 7) + proposal := c.proposal(t) + left := c.votes(t, proposal, []int{0, 1, 2, 3}) + right := c.votes(t, proposal, []int{4, 5, 6}) + if _, err := c.validators.BuildCertificate(proposal, left); !errors.Is(err, v2consensus.ErrInsufficientPower) { + t.Fatalf("4/3 left side unexpectedly reached quorum: %v", err) + } + if _, err := c.validators.BuildCertificate(proposal, right); !errors.Is(err, v2consensus.ErrInsufficientPower) { + t.Fatalf("4/3 right side unexpectedly reached quorum: %v", err) + } + for i, runtime := range c.runtimes { + if runtime.Height != 0 { + t.Fatalf("validator %d committed while partitioned", i) + } + } + + healed := append(append([]v2consensus.Vote{}, left...), right[0]) + certificate, err := c.validators.BuildCertificate(proposal, healed) + if err != nil { + t.Fatal(err) + } + for i := range c.runtimes { + if _, err := c.runtimes[i].Commit(c.candidates[i], certificate, c.validators); err != nil { + t.Fatalf("validator %d failed after partition heal: %v", i, err) + } + } +} + +func TestV2LabFiveTwoPartitionFinalizesQuorumSideAndMinorityCatchesUp(t *testing.T) { + c := newCluster(t, 7) + proposal := c.proposal(t) + quorumVotes := c.votes(t, proposal, []int{0, 1, 2, 3, 4}) + minorityVotes := c.votes(t, proposal, []int{5, 6}) + if _, err := c.validators.BuildCertificate(proposal, minorityVotes); !errors.Is(err, v2consensus.ErrInsufficientPower) { + t.Fatalf("2-validator minority unexpectedly reached quorum: %v", err) + } + certificate, err := c.validators.BuildCertificate(proposal, quorumVotes) + if err != nil { + t.Fatal(err) + } + for i := 0; i < 5; i++ { + if _, err := c.runtimes[i].Commit(c.candidates[i], certificate, c.validators); err != nil { + t.Fatalf("quorum validator %d failed commit: %v", i, err) + } + } + if c.runtimes[5].Height != 0 || c.runtimes[6].Height != 0 { + t.Fatal("minority committed before receiving certificate") + } + for i := 5; i < 7; i++ { + if _, err := c.runtimes[i].Commit(c.candidates[i], certificate, c.validators); err != nil { + t.Fatalf("minority validator %d failed certificate catch-up: %v", i, err) + } + } +} + +func TestV2LabConflictingProposalIsNotVoted(t *testing.T) { + c := newCluster(t, 7) + proposer, err := c.validators.Proposer(1, 0) + if err != nil { + t.Fatal(err) + } + conflicting := c.candidates[0].Header + conflicting.DataRoot = types.HashBytes("conflicting-data-root", []byte("evil")) + proposal, err := v2consensus.SignProposal(c.keys[proposer.ID], conflicting, 0) + if err != nil { + t.Fatal(err) + } + for i := range c.candidates { + if err := node.VerifyProposalAgainstCandidate(c.candidates[i], proposal, c.validators); !errors.Is(err, node.ErrCandidateState) { + t.Fatalf("validator %d accepted conflicting proposal: %v", i, err) + } + } +} + +func newCluster(t *testing.T, validatorCount int) *cluster { + t.Helper() + network := types.NetworkID(types.HashBytes("network", []byte("v2-lab"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + validators := v2consensus.ValidatorSet{Network: network} + keys := make(map[types.ValidatorID]*ecdsa.PrivateKey, validatorCount) + for i := 0; i < validatorCount; i++ { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + id := types.ValidatorIDFromPublicKey(pub) + validators.Validators = append(validators.Validators, v2consensus.Validator{ID: id, PublicKey: pub, Power: 10_000}) + keys[id] = key + } + validatorRoot, err := validators.Root() + if err != nil { + t.Fatal(err) + } + + aliceKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + alicePub := elliptic.Marshal(elliptic.P256(), aliceKey.PublicKey.X, aliceKey.PublicKey.Y) + alice := types.AccountIDFromPublicKey(alicePub) + bob := types.AccountIDFromPublicKey([]byte("v2-lab-bob")) + inputID := types.ObjectIDFromTransaction(types.HashBytes("v2-lab", []byte("input")), 0) + inputOut, _ := object.NewCoinOutput(alice, native, 100) + input := object.Object{ID: inputID, Version: 1, Owner: alice, Kind: inputOut.Kind, Data: inputOut.Data} + + stores := make([]*worldstate.Memory, validatorCount) + for i := range stores { + stores[i] = worldstate.NewMemory() + if _, err := stores[i].Apply(nil, []object.Object{input}); err != nil { + t.Fatal(err) + } + } + root := stores[0].Root() + witness, proof, ok := stores[0].Proof(inputID) + if !ok { + t.Fatal("missing lab witness") + } + witnessHash := witness.Hash() + payment, _ := object.NewCoinOutput(bob, native, 25) + change, _ := object.NewCoinOutput(alice, native, 74) + transaction := tx.Transaction{ + Version: tx.Version, Network: network, ShardID: 0, StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: inputID, Version: 1, ObjectHash: witnessHash}}, + Outputs: []object.OutputSpec{payment, change}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, + Fee: 1, Witnesses: []tx.Witness{{Object: witness, Proof: proof}}, + } + transaction.Salt[0] = 1 + if err := transaction.Sign(aliceKey); err != nil { + t.Fatal(err) + } + + runtimes := make([]*node.Runtime, validatorCount) + candidates := make([]node.Candidate, validatorCount) + for i := 0; i < validatorCount; i++ { + runtime, err := node.NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: stores[i]}, 4) + if err != nil { + t.Fatal(err) + } + runtimes[i] = runtime + candidate, err := runtime.BuildCandidate(1, map[uint32]node.ShardBatch{0: {Transactions: []tx.Transaction{transaction}}}) + if err != nil { + t.Fatal(err) + } + candidates[i] = candidate + if i > 0 && v2consensus.HeaderConsensusHash(candidates[i].Header) != v2consensus.HeaderConsensusHash(candidates[0].Header) { + t.Fatalf("validator %d derived a different candidate header", i) + } + } + return &cluster{network: network, native: native, validators: validators, keys: keys, runtimes: runtimes, candidates: candidates} +} + +func (c *cluster) proposal(t *testing.T) v2consensus.Proposal { + t.Helper() + proposer, err := c.validators.Proposer(1, 0) + if err != nil { + t.Fatal(err) + } + proposal, err := v2consensus.SignProposal(c.keys[proposer.ID], c.candidates[0].Header, 0) + if err != nil { + t.Fatal(err) + } + return proposal +} + +func (c *cluster) votes(t *testing.T, proposal v2consensus.Proposal, indices []int) []v2consensus.Vote { + t.Helper() + votes := make([]v2consensus.Vote, 0, len(indices)) + for _, index := range indices { + if err := node.VerifyProposalAgainstCandidate(c.candidates[index], proposal, c.validators); err != nil { + t.Fatal(err) + } + validator := c.validators.Validators[index] + vote, err := v2consensus.SignVote(c.keys[validator.ID], c.network, proposal.Header.Height, proposal.Round, v2consensus.HeaderConsensusHash(proposal.Header)) + if err != nil { + t.Fatal(err) + } + votes = append(votes, vote) + } + return votes +} From 476fae2970074ee342487bb32029177c6c493a96 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:41:35 +0200 Subject: [PATCH 078/274] run v2 consensus partition conformance as a dedicated CI gate --- .github/workflows/v2-lab.yml | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/v2-lab.yml diff --git a/.github/workflows/v2-lab.yml b/.github/workflows/v2-lab.yml new file mode 100644 index 00000000..13dc41e5 --- /dev/null +++ b/.github/workflows/v2-lab.yml @@ -0,0 +1,37 @@ +name: V2 Lab + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: v2-lab-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + v2-lab: + name: Protocol v2 conformance + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: false + + - name: V2 seven-validator conformance + run: go test ./internal/v2/lab ./internal/v2/node ./internal/v2/consensus ./internal/v2/state ./internal/v2/worldstate -count=1 -timeout=120s + + - name: V2 partition stress + run: go test ./internal/v2/lab -run '^TestV2Lab(FourThreePartitionStallsThenHeals|FiveTwoPartitionFinalizesQuorumSideAndMinorityCatchesUp)$' -count=10 -timeout=120s + + - name: V2 finalized batch scaling + run: go test ./internal/v2/node -run '^$' -bench '^BenchmarkV2FinalizedBatch32$' -benchtime=1x -count=1 -timeout=120s From ded4d9171e3af512700601f470ccf93a2147268d Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:42:05 +0200 Subject: [PATCH 079/274] document v2 validator trust-chain and Citizen rotation rules --- docs/protocol-v2-validator-trust.md | 121 ++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/protocol-v2-validator-trust.md diff --git a/docs/protocol-v2-validator-trust.md b/docs/protocol-v2-validator-trust.md new file mode 100644 index 00000000..1abe492d --- /dev/null +++ b/docs/protocol-v2-validator-trust.md @@ -0,0 +1,121 @@ +# Zephyr Protocol v2 — Validator Trust Chain + +This document specifies how validators, full nodes, cross-shard receipt importers and Citizen wallets identify the validator committee authorized to finalize a Zephyr v2 header. + +## Why a quorum certificate is not enough by itself + +A set of arbitrary keys can always create a self-consistent certificate for data they signed. Therefore a verifier must establish two facts independently: + +1. the votes reach Zephyr's normal `2/3+` voting-power quorum for a validator set; +2. that exact validator set is itself authorized by the already-trusted Zephyr chain. + +Zephyr v2 commits validator identity, P-256 public key and integer voting power into a canonical Merkle root. A header is only valid against a validator set whose calculated root equals `GlobalHeader.ValidatorRoot`. + +## Genesis trust anchor + +The canonical v2 genesis derives: + +```text +NetworkID = H(canonical genesis) +ValidatorRoot = Merkle(initial validator set) +``` + +`genesis.Config.TrustAnchor()` exposes these two values. A Citizen wallet can embed a known genesis or import an explicitly trusted checkpoint and needs no trusted RPC server to invent the committee for it. + +## Header transition rule + +Each canonical `GlobalHeader` contains: + +```text +ValidatorRoot current committee +NextValidatorRoot committee authorized for the next height +``` + +If `NextValidatorRoot` is zero, it means "unchanged" and the effective next root is `ValidatorRoot`. + +A committee transition is therefore: + +```text +trusted committee N + | + | signs GlobalHeader H + v +ValidatorRoot = root(N) +NextValidatorRoot = root(N+1) + | + | 2/3+ QC from N + v +committee N+1 becomes trusted for H+1 +``` + +The next committee does not authorize the block that installs itself. The currently trusted committee must finalize the transition first. + +## Validator behavior + +Before voting, a validator verifies all of the following: + +- proposal signature and scheduled proposer; +- proposal network and height; +- local validator-set root equals `Header.ValidatorRoot`; +- local deterministic execution produces the same `GlobalHeader` consensus hash; +- normal transaction/state/shard commitment rules. + +Only then may it sign a vote. + +## Runtime activation + +`node.ScheduleValidatorTransition` places the next set root in the candidate before proposal signing. + +`node.CommitWithValidatorTransition` first performs the normal QC-backed state commit using the current set. Only after successful finalization does the runtime advance its local `ValidatorRoot` to the header's effective next root. + +The following block therefore requires the new validator set. + +Governance/staking will eventually decide *which* transition is allowed to be proposed; the cryptographic transition rule described here remains the consensus trust boundary. + +## Citizen wallet behavior + +The strict wallet verifier in `apps/wallet/src/lib/v2CitizenTrusted.ts` starts from a `CitizenTrustAnchor` containing a trusted `NetworkID` and `ValidatorRoot`. + +For every proof bundle it independently checks: + +- header network equals the trusted network; +- supplied validator set hashes to the trusted/current `ValidatorRoot`; +- validator IDs match their P-256 public keys; +- every vote signature is canonical low-S P-256 and targets the same header; +- distinct signed voting power reaches `2/3+` using exact `BigInt` arithmetic; +- certificate hash matches the finalized header; +- shard commitment belongs to the finalized global commitment root; +- object inclusion/absence proof matches the shard state root. + +After all checks pass, the wallet may advance to: + +```text +nextTrustAnchor = { + network: current NetworkID, + validatorRoot: effective NextValidatorRoot +} +``` + +A malicious RPC can transport headers, validator sets and proofs, but it cannot choose a new trusted validator committee without a QC from the currently trusted committee. + +## Cross-shard receipts + +Cross-shard imports apply the same rule. A destination shard does not accept a source receipt merely because the supplied source validator set signed it. It verifies that the supplied set hashes to the source header's `ValidatorRoot`, verifies the QC, then verifies shard and receipt Merkle proofs before materializing destination state. + +## Recovery and checkpoints + +Future checkpoint/snapshot formats must carry enough finalized header history to bridge any validator-set transitions between the checkpoint already trusted by the verifier and the target state. Snapshot authentication never replaces this trust chain. + +## Security invariant + +```text +valid signatures + != +authorized validators + +valid signatures ++ committed validator root ++ previously trusted transition + = +authorized finality evidence +``` From 6637ba0390b2cd55c65e057b1875f0bff6ebf0c4 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:44:51 +0200 Subject: [PATCH 080/274] refresh authoritative v2 architecture with implemented trust, sharding and Citizen paths --- docs/protocol-v2.md | 382 +++++++++++++++++++++++--------------------- 1 file changed, 196 insertions(+), 186 deletions(-) diff --git a/docs/protocol-v2.md b/docs/protocol-v2.md index 619b306a..abf67f02 100644 --- a/docs/protocol-v2.md +++ b/docs/protocol-v2.md @@ -4,6 +4,8 @@ Status: **authoritative design target and implementation contract for the Zephyr Protocol v1 remains the prototype/conformance reference. Protocol v2 intentionally does not preserve v1 transaction wire format, persisted-state layout, node-role coupling or account-state representation. We keep the security, consensus, recovery and benchmarking lessons while replacing boundaries that would otherwise become permanent scaling debt. +For the executable status of each subsystem, see `docs/protocol-v2-implementation-status.md`. For committee trust/rotation, see `docs/protocol-v2-validator-trust.md`. + ## Mission Zephyr v2 has two equal scaling goals: @@ -28,6 +30,8 @@ Large datacenters may add capacity, but must not be structurally required for Ze - Adding shards must not linearly increase the minimum hardware requirement of a Citizen Node. - Smart-contract execution is deterministic and metered. - Heavy/private compute is provider-executed and blockchain-settled through commitments/proofs/attestations/replication/challenges as appropriate. +- A quorum certificate is valid only for the validator set committed by the already-trusted header chain. +- Cross-shard imports are asynchronous and cannot bypass source finality or durable anti-replay state. ## Architecture @@ -79,292 +83,298 @@ networkId = H("zephyr/genesis/v2" || canonicalGenesis) Genesis becomes the network identity and initial validator-set trust anchor. Nodes with different genesis data cannot silently claim the same network. -Reference: `internal/v2/genesis`. - -## 2. Separate identities and roles +The initial Citizen trust anchor is: ```text -Account identity -> ownership and transaction authorization -Node identity -> peer networking/authentication -Validator identity -> consensus proposal/vote authority +TrustAnchor { + NetworkID + ValidatorRoot = Merkle(initial validator set) +} ``` -A machine may expose one or more roles: Citizen Node, Full Node, Validator, Archive Node, Compute Provider. A full node does not need a validator key; a compute provider is not automatically a validator; a smartphone does not need permanent consensus availability. - -References: `internal/v2/types`, `internal/v2/transport`. - -## 3. Canonical binary protocol +## 2. Validator trust chain -Consensus objects use deterministic length-prefixed binary encoding with explicit versions, hard limits, big-endian integers and domain-separated hashing/signing. Consensus objects do not depend on JSON map/order behavior. +Every validator set is committed by a canonical Merkle root over validator ID, P-256 public key and integer voting power. -The first v2 proof-carrying transaction has a complete bounded binary marshal/parse round-trip. +Each `GlobalHeader` contains both: -Reference: `internal/v2/codec`. +```text +ValidatorRoot // committee authorizing this header +NextValidatorRoot // committee authorized for the next height +``` -## 4. Object state and native assets +If `NextValidatorRoot` is zero, the committee remains unchanged. A committee cannot install itself: the current committee must first finalize the header committing the next root with the normal `2/3+` quorum. -The v2 execution primitive is a protocol object: +Citizen wallets advance their local trust anchor only after verifying that QC. Cross-shard receipt import applies the same rule to historical source committees. -```text -Object -├── objectId -├── version -├── owner -├── kind -└── data -``` +## 3. Canonical binary protocol -Initial kinds cover coins, token definitions, contracts, contract state, compute offers/jobs/assignments/results and system objects. Explicit object dependencies allow independent transactions to be scheduled in parallel. +Consensus-critical objects use a bounded binary codec with explicit widths, length prefixes and versioned hash/signing domains. JSON is an RPC representation only. -For native payments the wallet still shows a normal balance; coin objects are an internal execution model. A transfer consumes coin objects and creates new ones while enforcing per-token conservation. +Canonical binary objects include: -Token creation is protocol-native rather than requiring every token to reimplement a basic ERC-style ledger in bytecode. Token definitions include name, symbol, decimals, supply policy, mint authority, burnability and transferability. Custom logic can still be controlled by contracts later. +- transactions and witnesses; +- objects and asset definitions; +- shard commitments and receipts; +- GlobalHeader; +- proposals, votes and quorum certificates; +- Merkle/Sparse-Merkle proofs; +- genesis and validator commitments. -References: `internal/v2/object`, `internal/v2/assets`, `internal/v2/execution`. +Protocol decoders reject trailing bytes, oversized fields and invalid versions. -## 5. Proof-native incremental state +## 4. Proof-oriented object state -The v1 performance profile identified full-state persistence/serialization as the first measured bottleneck. v2 therefore makes incremental, proof-oriented state a protocol requirement. +Zephyr v2 replaces the v1 global account map as the consensus state primitive with versioned objects. -The reference engine is a 256-bit Sparse Merkle Tree over: +Core categories include: ```text -objectId -> objectHash +CoinObject +TokenDefinition +ContractObject +ComputeOffer / ComputeJob / ComputeResult state +SystemObject ``` -It already provides deterministic roots, incremental path updates, inclusion proofs, absence proofs and compressed proofs that omit default siblings. The in-memory world-state backend defines the semantics; it is not the final durable database. Production storage will put the same state model over structured durable KV/WAL/checkpoint recovery. +User UX remains balance-oriented; wallets aggregate owned coin objects behind the scenes. -Changing two objects must not require serializing the entire chain state. +Objects have deterministic IDs. For multi-shard state, the object ID permanently encodes its assigned shard so changing the active shard count cannot silently relocate an existing object. -References: `internal/v2/state`, `internal/v2/worldstate`. +## 5. Sparse-Merkle state and proof-carrying transactions -## 6. Proof-Carrying Transactions +Each shard maintains an incremental 256-bit Sparse Merkle state root. Inclusion and absence proofs are compressed by omitting default siblings. -A v2 wallet does more than sign. It may package the exact state evidence needed to validate the objects it consumes. +A proof-carrying transaction identifies its pre-state root and carries authenticated object witnesses. A validator therefore verifies the same state evidence that the originating wallet could verify, rather than trusting wallet pre-validation. ```text -Wallet - ├─ verifies finalized state root - ├─ obtains object proofs - ├─ declares inputs/outputs/operation - ├─ signs canonical intent - └─ attaches witnesses - | - v - Proof-Carrying Transaction - | - v -Validator verifies signature + proofs + freshness/conflicts - | - v - deterministic execution +wallet + -> verifies finalized state proof + -> declares inputs / outputs / access set + -> signs proof-carrying transaction + -> network verifies witness + signature + -> deterministic execution ``` -The validator never trusts the wallet's claim; it independently verifies the cryptographic witness. This makes wallet work reusable while preserving consensus as the authority for uniqueness, ordering and finality. +Wallet-side work is useful because the evidence is independently reusable. Consensus remains responsible for uniqueness, ordering and finality. -The foundation includes P-256 low-S signing, genesis-derived network binding, state-root binding, explicit input object/version/hash, bounded witnesses, random salt and expiration height. +## 6. Deterministic parallel execution -Reference: `internal/v2/tx`. +Transactions expose enough object dependencies to reject conflicting batches before concurrent execution. Independent transactions may execute in parallel; outputs merge in deterministic transaction order. -## 7. Parallel execution +The execution gate rejects: -Object inputs form an explicit conflict set. Transactions that touch disjoint objects can execute concurrently and then merge deterministically. +- duplicate transactions; +- duplicate/shared consumed objects in a parallel batch; +- mismatched pre-state roots; +- wrong-shard inputs; +- token conservation or fee violations; +- invalid witnesses/signatures. -The first executor is deliberately simple and single-operation so we can establish correctness before a worker scheduler. The next execution milestone builds the deterministic conflict graph and benchmarks 1/4/8/16 workers on one shard before using sharding as a multiplier. +Candidate execution computes a future root through a non-mutating state preview. Committed state changes only after a valid quorum certificate. -## 8. Shard-native, one-shard-first +Performance work is profile-driven. The v2 benchmark measures 1/4/8/16 execution workers and reports finalized throughput/allocation behavior; adding goroutines is not considered scaling if proof/state allocation remains the bottleneck. -The protocol contains shard IDs, shard commitments, a global commitment root, global headers and cross-shard receipt primitives from the beginning, but the first v2 chain may run one shard. +## 7. Sharding and cross-shard receipts -A global finalized header commits to shard roots and validator/data commitments. A Citizen Node can follow global headers while fetching only the shard/state proofs relevant to it. +Sharding is native to the protocol but optional at activation. One shard is a fully valid Zephyr network. -Cross-shard movement follows: +Account routing determines where newly created account-owned outputs live. If an output targets another shard, the source shard does **not** write that object locally. Instead it creates a cross-shard receipt committed by the source shard's `ReceiptRoot`. -```text -source shard consumes input - | - v -finalized receipt commitment - | - v -destination shard verifies receipt - | - v -creates destination output -``` +A destination import verifies: -Additional shards are activated only when 1/4/16-shard benchmarks show better finalized throughput/resource efficiency without worsening the Citizen Node minimum footprint. Receipt anti-replay and recovery must pass before multi-shard activation. +1. source `GlobalHeader` and its authorized validator-set QC; +2. source shard commitment inclusion in the global root; +3. receipt inclusion in the source `ReceiptRoot`; +4. destination routing; +5. absence of the durable receipt marker. -Reference: `internal/v2/sharding`. +The destination block then materializes both the destination object and a consensus-state anti-replay marker. Because normal transactions in that block are anchored to the destination pre-state root, an imported output becomes spendable only from a later block. -## 9. Citizen Node inside Zephyr Wallet +Sharding is enabled beyond one shard only after multi-shard conformance/recovery and throughput evidence demonstrate a net benefit. -A Citizen Node is not a passive RPC client. Depending on device conditions it can: +## 8. GlobalHeader and finality -- verify finalized headers and quorum/finality evidence; -- verify object/state proofs for balances and payments; -- verify proof-carrying transactions; -- relay transactions through multiple peers; -- verify shard commitments; -- sample data availability; -- keep a bounded recent cache; -- optionally execute recent state while resources allow. +A v2 block-height finality commitment is a compact `GlobalHeader` over: -Participation is power-aware. Low battery can reduce the role to header verification; Wi-Fi/charging can enable sampling, cache serving and recent execution. Mobile availability is not assumed for consensus liveness. +```text +version +network +height +parentHash +shardCommitmentRoot +validatorRoot +nextValidatorRoot +dataRoot +certificateHash +``` -Reference: `internal/v2/citizen`. +The consensus hash zeroes `certificateHash` to avoid circular signing. Proposal/vote signatures target this consensus hash. Once the quorum certificate forms, its hash is attached to the finalized header. -## 10. Data availability +The header therefore ties together execution state, receipt availability, committee trust and global finality. -Citizen Nodes should be able to contribute to availability without downloading all shard data. The foundation commits ordered chunks and verifies Merkle samples. It also defines an encoder boundary for a later erasure-code implementation. +## 9. Citizen Node -Production DA still requires selection of an erasure code, reconstruction logic, sampling rules/confidence model, adversarial withholding tests and real mobile bandwidth/storage measurements. The current package is the proof contract, not a claim that production DA is finished. +The Zephyr Wallet includes a Citizen verifier rather than acting as a blind RPC client. -Reference: `internal/v2/da`. +A proof bundle can carry: -## 11. Deterministic smart contracts +```text +GlobalHeader +QuorumCertificate +ValidatorSet +ShardCommitment + proof +Object + SparseMerkle proof +``` -Zephyr keeps smart contracts through a deterministic WebAssembly ABI. Rust is the first-class SDK target, not the only possible source language. Compatible toolchains may later include Zig, C/C++, TinyGo and others. +The strict wallet verifier starts from a genesis/checkpoint trust anchor, independently reconstructs validator IDs/root/voting power, verifies low-S P-256 votes and `2/3+` quorum using exact integer arithmetic, then verifies shard/object proofs. -Consensus must standardize allowed WASM features, deterministic host calls, fuel/gas, memory/stack limits, state-access declarations, deterministic output/events and forbidden nondeterministic facilities. +After a valid header, it may advance its local validator trust root to the QC-authorized `NextValidatorRoot`. -The foundation validates a WASM v1 deployment envelope and defines the runtime interface. A production interpreter/metering engine is **not yet claimed complete**. +Participation remains resource-aware: -Reference: `internal/v2/contracts`. +```text +battery low -> headers/proofs only +wallet active -> verify + relay +Wi-Fi -> + DA sampling / bounded cache +Wi-Fi + charging -> + opportunistic recent execution / serving +``` -## 12. Native distributed compute market +Mobile OS background availability is treated as opportunistic capacity, never as a consensus-liveness assumption. -Heavy workloads remain outside consensus execution. The blockchain owns marketplace and settlement state; providers execute the work. +## 10. Native tokens -Native objects will cover provider offers, resource capabilities, price/collateral, jobs, assignments, escrow, result commitments, verification mode, challenges/disputes, settlement and reputation/slashing. +Token definitions and coin objects are protocol-native so ordinary token transfers do not require executing general smart-contract bytecode. -Target workloads include scientific/numerical computing, AI training/inference, video/3D rendering, compilation and data processing. +Native asset state supports supply limits, mint authority, burn/transfer policy and deterministic token IDs. Custom smart contracts remain available when an asset needs logic beyond the native model. -Verification is workload-specific. Supported modes are: +## 11. Smart contracts -- deterministic re-execution; -- replicated execution; -- challenge-based verification; -- zero-knowledge/validity proof; -- TEE/remote attestation; -- client approval; -- hybrid combinations. +The contract protocol targets deterministic metered WASM through a versioned ABI. Rust is the first-class SDK target, not a protocol requirement; other modern languages may target the same deterministic WASM subset. -Confidential workloads keep private datasets/results off-chain where appropriate; Zephyr stores commitments, encrypted references, attestations/proofs and settlement state. +The runtime boundary already requires: -The foundation defines resource/offer/job/result data models and verification modes. Provider daemon, scheduling, escrow transitions, disputes and production TEE/ZK integrations are later milestones. +- bounded module/request/output sizes; +- deterministic imports/opcodes; +- fuel limits; +- memory/stack policy; +- declared object read/write access; +- no undeclared writes; +- bounded events. -Reference: `internal/v2/compute`. +The concrete production WASM engine and audited fuel schedule are selected only after cross-machine deterministic conformance and performance measurement. -## 13. Transport boundaries +## 12. Native distributed compute market -Consensus, transaction relay and light-proof retrieval are distinct logical capabilities. HTTP remains usable as a reference/test transport; future libp2p/QUIC/WebTransport implementations sit behind the same contracts. The Consensus & Performance Lab fault transport must be adapted to this boundary so correctness tests run independently of the production transport. +Heavy workloads are a native Zephyr market but run outside validator consensus execution. -Reference: `internal/v2/transport`. +Provider offers describe CPU, GPU, RAM, VRAM, storage, bandwidth, capabilities, verification modes, pricing and collateral. Jobs describe content-addressed inputs/workload, resource requirements, budget/escrow, deadline and verification policy. -## 14. Performance has two axes +Supported verification-policy primitives include: -### Scale up — how fast can Zephyr finalize? +- deterministic replay for suitable bounded jobs; +- replicated providers and matching result commitments; +- challenge evidence; +- ZK validity proof integration; +- TEE attestation integration; +- client approval; +- hybrid policies. -Measure finalized tx/s, p50/p95/p99 finality, validators, shards, batch size, CPU, allocations, memory, state-write bytes, network bytes/finalized tx, witness bytes, DA bytes and persisted state. +Validators settle compact evidence and payment state. They do not replay AI training, scientific simulations, rendering or other expensive compute merely to finalize payment. -### Scale down — how small can a useful node be? +Confidential workloads keep private datasets/results off-chain where appropriate; Zephyr stores commitments, encrypted references and settlement evidence. -Measure Citizen Node resident memory, cache size, sync bandwidth, proof size/verification time, header verification, DA sample bytes/time, mobile CPU/battery duty cycle and startup/resume latency. +## 13. Data availability -No phone or hardware budget becomes a production claim before measurement on real reference devices. +Shard/global commitments include data roots. Citizen Nodes can verify bounded samples rather than downloading global block data. -## 15. Security and consensus continuity +The checked-in foundation defines chunk/sample commitment verification; production erasure coding, reconstruction, confidence parameters and withholding fault tests remain activation gates. -The existing Consensus & Performance Lab remains the gate. v2 must preserve or strengthen network/domain separation, low-S canonical signatures, quorum finality, pre-vote state-root verification, validator identity/signature validation, quorum-only recovery evidence, no single-peer snapshot trust, partition safety and recovery after quorum returns. +The invariant is that increasing shard/data capacity must not linearly increase the minimum data requirement of every Citizen Node. -The new architecture changes **what consensus commits to**, not how much evidence is required for finality. +## 14. Transport boundaries -## 16. Clean-break compatibility policy +Consensus, transaction relay and light-proof retrieval are distinct capabilities. HTTP remains the reference/test transport. Production peer networking can move to libp2p/QUIC/WebTransport behind those interfaces without redefining consensus objects. -v2 intentionally breaks v1 compatibility for network identity, transaction wire/signing domain, object/state format, persistent backend, state-root calculation, node-role model, shard commitments and contract ABI. +Shard-aware gossip, mobile relay/NAT traversal and the production transport implementation must pass the same conformance suite as the reference transport before public activation. -It carries forward security invariants, consensus/fault lessons, performance methodology, recovery requirements, wallet self-custody and P-256 usability unless later benchmark/security evidence justifies changing it. +## 15. Durable state -No public v2 network will silently accept v1 protocol objects. +The v2 durable backend uses an append-only network-bound WAL with checksums/sequence numbers/fsync plus atomic checkpoints and crash-tail recovery. This removes the v1 requirement to serialize the entire node state for every mutation. -## 17. Implementation sequence +It remains a reference durable backend while large-state benchmark evidence determines whether the final production backend should be a structured KV/LSM implementation. -### Foundation — implemented by this branch +## 16. Performance has two axes -- canonical binary codec and typed v2 identities; -- genesis-derived network ID; -- Sparse Merkle reference state and compressed proofs; -- object/coin model and native token definitions; -- signed proof-carrying transaction wire format; -- native transfer and token-creation reference executor; -- object world-state backend; -- shard routing, commitments/proofs, global header and receipt primitives; -- DA chunk/sample verification boundary; -- Citizen Node verifier and resource-aware participation policy; -- deterministic WASM deployment/runtime boundary; -- native compute-market data model and verification modes; -- separate consensus/transaction/light transport interfaces; -- unit tests and reference state/proof microbenchmarks. +### Scale up — how fast can Zephyr finalize? -### Integration +Measure finalized tx/s, p50/p95/p99 finality, validators, shards, batch size, CPU, allocations, memory, state-write bytes, network bytes/finalized tx, witness bytes, DA bytes and persisted state. -- add v2 genesis to the Consensus & Performance Lab; -- make current certified consensus finalize a v2 global header; -- run one-shard v2 transfers end to end; -- introduce durable structured KV persistence with crash/restart recovery; -- compare v1/v2 finalized TPS, finality, allocations and state-write cost. +### Scale down — how small can a useful node be? -### Mobile +Measure Citizen Node resident memory, cache size, sync bandwidth, proof size/verification time, header verification, DA sample bytes/time, mobile CPU/battery duty cycle and startup/resume latency. -- compact header/state-proof APIs; -- Citizen verifier inside Zephyr Wallet; -- bounded/resumable cache and multi-peer relay; -- real Android/iOS resource measurements; -- eliminate correctness dependence on any single RPC endpoint. +No phone or hardware budget becomes a production claim before measurement on real reference devices. -### Parallel execution +## 17. Security and consensus continuity -- deterministic conflict graph; -- parallel non-conflicting execution; -- deterministic merge; -- 1/4/8/16-worker benchmark on one shard. +The existing Consensus & Performance Lab remains a regression gate and v2 has its own seven-validator conformance suite. -### Sharding +V2 conformance covers certified happy path, 4/3 no-quorum partition, heal/recovery, 5/2 quorum/minority catch-up and conflicting proposal rejection. More restart/transport/Byzantine cases remain required before public devnet. -- 4-shard Lab; -- cross-shard receipt consume and anti-replay; -- shard-aware gossip/recovery; -- 1/4/16-shard benchmarks; -- activate more shards only if evidence is positive. +The architecture changes **what consensus commits to**, not how much evidence is required for finality. -### WASM +## 18. Clean-break compatibility policy -- select production deterministic runtime; -- validate imports/opcodes; -- define fuel schedule and memory limits; -- deploy/call state transitions; -- Rust SDK and deterministic conformance suite. +V2 intentionally breaks v1 compatibility for network identity, transaction wire/signing domain, object/state format, persistent backend, state-root calculation, node-role model, shard commitments, validator trust chain and contract ABI. -### Compute market +It carries forward security invariants, consensus/fault lessons, performance methodology, recovery requirements, wallet self-custody and P-256 usability unless later benchmark/security evidence justifies changing them. -- offer/job/assignment/result state transitions; -- escrow/settlement and provider daemon; -- deterministic/replicated verification first; -- collateral/slashing/disputes; -- optional TEE/ZK backends and confidential workload flow. +No public v2 network silently accepts v1 protocol objects or pre-transition experimental v2 wire objects. -### Data availability +## 19. Implementation sequence -- select erasure code and reconstruction; -- sampling/confidence rules; -- withholding/fault tests; -- mobile bandwidth/storage benchmarks; -- shard-aware DA propagation. +### Executable foundation + +Implemented on the v2 branch: + +- canonical binary codec and typed identities; +- genesis-derived network/trust identity; +- validator-set roots and QC-backed committee transitions; +- Sparse-Merkle reference state and compressed proofs; +- proof-oriented object/coin model and native tokens; +- signed proof-carrying transaction wire format; +- deterministic parallel executor and non-mutating state preview; +- durable WAL/checkpoint state backend; +- v2 proposal/vote/QC and GlobalHeader finality path; +- permanent object shard placement; +- finalized cross-shard receipt import with durable anti-replay marker; +- Citizen light API and strict wallet cryptographic verifier; +- deterministic WASM metering/runtime boundary; +- native compute-market state-machine foundation; +- data-availability sample boundary; +- separate consensus/transaction/light transport interfaces; +- dedicated v2 seven-validator partition/conformance CI gate; +- finalized v2 batch benchmark. + +### Next integration gates + +- mount a live v2 runtime/genesis/light provider in the process-facing node path; +- persist runtime height/header/validator-transition metadata alongside shard state; +- expand v2 fault injection to restart, proposer death, Byzantine payloads, wrong-chain data and transport delay/reorder; +- profile/reduce proof/state allocation and compare controlled-hardware v1/v2 throughput; +- add shard-aware recovery/gossip and 4/16-shard conformance; +- implement trusted checkpoint history across long validator rotations; +- integrate a production deterministic WASM engine and Rust SDK; +- move compute-market transitions into consensus object execution and build provider daemon/escrow/dispute flows; +- select production erasure coding and implement DA reconstruction/withholding tests; +- connect libp2p/QUIC/mobile peer transport; +- embed Citizen lifecycle/cache/relay controls in the wallet UI/native shell; +- benchmark real Android/iOS devices and commodity validators. ### Public devnet gate -No public v2 devnet until safety/liveness conformance, durable state recovery and Citizen Node correctness pass; one-shard performance is characterized; shard activation rules are defined; and genesis/checkpoint/operator/wallet upgrade procedures are explicit. +No public v2 devnet until safety/liveness conformance, durable state/runtime recovery and Citizen trust-chain correctness pass; one-shard performance is characterized; multi-shard activation rules are defined; contract execution is deterministic/metered; and genesis/checkpoint/operator/wallet upgrade procedures are explicit. ## Architectural north star From 73c4ad25ff54b6fe472b0a7b2412c302a789e907 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:46:43 +0200 Subject: [PATCH 081/274] add current executable Zephyr v2 implementation snapshot --- docs/protocol-v2-current-status.md | 114 +++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 docs/protocol-v2-current-status.md diff --git a/docs/protocol-v2-current-status.md b/docs/protocol-v2-current-status.md new file mode 100644 index 00000000..9913b065 --- /dev/null +++ b/docs/protocol-v2-current-status.md @@ -0,0 +1,114 @@ +# Zephyr Protocol v2 — Current Executable Status + +This snapshot complements the authoritative `docs/protocol-v2.md`. It exists so implementation progress cannot be confused with future design goals. + +## Executable now on the v2 branch + +### Consensus and trust + +- canonical binary proposal, vote, quorum-certificate and `GlobalHeader` objects; +- low-S P-256 validator signatures; +- deterministic weighted proposer selection; +- exact `2/3+` integer voting-power quorum; +- duplicate-voter rejection; +- validator-set Merkle root over validator ID/public key/power; +- proposal rejection when the local authorized validator root differs from the header; +- pre-vote rejection when independently executed local state produces another candidate header; +- `GlobalHeader.ValidatorRoot` plus `GlobalHeader.NextValidatorRoot`; +- QC-backed validator-set transition: current committee authorizes the next committee for the following height; +- genesis-derived Citizen trust anchor (`NetworkID`, `ValidatorRoot`); +- dedicated 7-validator v2 Lab with happy path, 4/3 no-quorum partition/heal, 5/2 quorum/minority catch-up and conflicting-proposal rejection. + +### State, proofs and persistence + +- object/coin state model; +- 256-bit Sparse Merkle state; +- compressed inclusion/absence proofs; +- proof-carrying P-256 transactions; +- incremental state updates; +- copy-on-write non-mutating root preview before QC; +- preview-vs-real-apply equivalence tests; +- append-only network-bound WAL with CRC32C, sequence numbers and fsync; +- atomic checkpoint and crash-tail recovery; +- wrong-network persisted-state rejection; +- streamed canonical domain hashing and fixed-size branch hashing to reduce hot-path allocations. + +### Execution, assets and sharding + +- native ZPH/object transfers; +- protocol-native custom token creation; +- deterministic parallel execution of independent transactions; +- conflict rejection for shared inputs, duplicate transactions and mismatched pre-state roots; +- permanent shard placement encoded into object IDs; +- remote outputs become source-shard receipts rather than illegal local writes; +- receipt root committed in the source shard; +- destination import verifies source QC + authorized historical validator root + shard proof + receipt proof; +- destination import creates the deterministic destination object and a Merkle-state anti-replay marker; +- cross-shard receipt is spendable only from a later block because current-block transactions remain anchored to pre-state; +- tested two-shard transfer and durable replay rejection. + +### Citizen wallet + +- light proof API for finalized status and object proofs; +- Go Citizen verifier; +- browser cryptographic verifier using WebCrypto/BigInt; +- strict rotating-trust verifier in `apps/wallet/src/lib/v2CitizenTrusted.ts`; +- independent validator-root reconstruction, low-S P-256 QC verification, exact quorum, shard proof and Sparse-Merkle proof validation; +- returned `nextTrustAnchor` advances only after the current trusted committee finalizes the new root; +- battery/network-aware modes for verify-only, relay, bounded DA/cache and opportunistic execution. + +### Smart-contract foundation + +- WASM deployment/runtime abstraction; +- consensus-side metering guard; +- fuel limit; +- bounded args/result/events/access count; +- declared object read/write access; +- rejection of undeclared writes. + +### Native compute-market foundation + +- CPU/GPU/RAM/VRAM/storage/bandwidth/capability offers; +- collateral and pricing data; +- compute jobs with escrow/deadline; +- provider matching/assignment; +- replicated provider assignment; +- result submission, settlement, refund and expiry; +- deterministic, replicated, challenge, ZK-signal, TEE-attestation-signal, client-approval and hybrid verification policies. + +Heavy jobs are provider-executed. Validator consensus settles compact evidence and never requires every validator to replay AI training, scientific computation, rendering or similar workloads. + +### CI and measurement + +- legacy v1 Consensus & Performance Lab remains a regression gate; +- dedicated `V2 Lab` workflow runs v2 conformance and repeated partition tests; +- finalized v2 32-transfer benchmark uses seven validators and 1/4/8/16 execution workers; +- client key generation/signing remains outside the timed consensus path; +- the pre-allocation-optimization shared-runner sample peaked around 8 workers and showed allocation/state-proof work mattered more than simply increasing worker count, motivating the current hash/preview optimizations. + +## Deliberately not called production-complete yet + +The following are still engineering gates rather than shipped claims: + +- live process-facing v2 node/devnet wiring and durable runtime-height/round metadata; +- staking/governance policy for validator-set updates; +- full v2 fault transport with restart, proposer death, delay, duplicate/reorder and Byzantine cases; +- production libp2p/QUIC/mobile peer discovery and shard-aware gossip; +- 4/16-shard recovery and controlled-hardware scaling proof; +- reshard/split/merge policy; +- concrete deterministic production WASM engine, audited fuel schedule and Rust SDK; +- compute provider daemon, consensus-object escrow/slashing/disputes and concrete TEE/ZK integrations; +- production erasure coding/reconstruction and data-withholding fault tests; +- native Android/iOS lifecycle, resumable Citizen cache/relay and real-device battery/RAM/bandwidth measurements; +- controlled-hardware performance regression budgets; +- public v2 genesis/checkpoint/operator upgrade procedures. + +## Activation rule + +No subsystem is activated on a public Zephyr v2 network because it exists in code. It is activated only when its safety/liveness tests, restart/recovery tests and relevant performance/resource measurements pass. + +```text +more hardware -> more throughput +less hardware -> less throughput +less hardware -/-> weaker correctness +``` From 421b569341982bde88ddb0fceb9ef51123c807fe Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:07:42 +0200 Subject: [PATCH 082/274] bind next validator root into v2 global header --- internal/v2/sharding/sharding.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/v2/sharding/sharding.go b/internal/v2/sharding/sharding.go index d390759d..64527b82 100644 --- a/internal/v2/sharding/sharding.go +++ b/internal/v2/sharding/sharding.go @@ -95,10 +95,21 @@ type GlobalHeader struct { ParentHash types.Hash ShardCommitmentRoot types.Hash ValidatorRoot types.Hash + NextValidatorRoot types.Hash DataRoot types.Hash CertificateHash types.Hash } +// EffectiveNextValidatorRoot returns the committee root authorized for the +// following height. A zero next-root means "no rotation" and therefore keeps +// the current committee active. +func (h GlobalHeader) EffectiveNextValidatorRoot() types.Hash { + if types.IsZero32([32]byte(h.NextValidatorRoot)) { + return h.ValidatorRoot + } + return h.NextValidatorRoot +} + func (h GlobalHeader) CanonicalBytes() []byte { var w codec.Writer w.U16(h.Version) @@ -107,6 +118,7 @@ func (h GlobalHeader) CanonicalBytes() []byte { w.Fixed(h.ParentHash[:]) w.Fixed(h.ShardCommitmentRoot[:]) w.Fixed(h.ValidatorRoot[:]) + w.Fixed(h.NextValidatorRoot[:]) w.Fixed(h.DataRoot[:]) w.Fixed(h.CertificateHash[:]) return w.BytesCopy() From 25f74281416182fec0920677c6e72880cb438e7e Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:08:01 +0200 Subject: [PATCH 083/274] parse next validator root from v2 header --- internal/v2/sharding/wire.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/v2/sharding/wire.go b/internal/v2/sharding/wire.go index c6b9cfa9..8be2ea92 100644 --- a/internal/v2/sharding/wire.go +++ b/internal/v2/sharding/wire.go @@ -55,6 +55,10 @@ func ParseGlobalHeader(data []byte) (GlobalHeader, error) { if err != nil { return GlobalHeader{}, ErrShardCount } + nextValidatorRoot, err := readHash(r) + if err != nil { + return GlobalHeader{}, ErrShardCount + } dataRoot, err := readHash(r) if err != nil { return GlobalHeader{}, ErrShardCount @@ -65,8 +69,8 @@ func ParseGlobalHeader(data []byte) (GlobalHeader, error) { } return GlobalHeader{ Version: version, Network: network, Height: height, ParentHash: parentHash, - ShardCommitmentRoot: shardRoot, ValidatorRoot: validatorRoot, DataRoot: dataRoot, - CertificateHash: certificateHash, + ShardCommitmentRoot: shardRoot, ValidatorRoot: validatorRoot, NextValidatorRoot: nextValidatorRoot, + DataRoot: dataRoot, CertificateHash: certificateHash, }, nil } From 7866147a0907353c2dd03effb910438a66199a00 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:36:21 +0200 Subject: [PATCH 084/274] upgrade Zephyr v2 toolchain baseline to Go 1.26 --- go.mod | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 5a56f28b..5bcb9a59 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,3 @@ module github.com/zephyr-chain/zephyr-chain -go 1.22 - +go 1.26.0 From 11fc2cead62564abadf2170122407a26f8b382e2 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:36:57 +0200 Subject: [PATCH 085/274] resolve production dependencies for Zephyr v2 --- .github/workflows/v2-dependency-lock.yml | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/v2-dependency-lock.yml diff --git a/.github/workflows/v2-dependency-lock.yml b/.github/workflows/v2-dependency-lock.yml new file mode 100644 index 00000000..dbe20d36 --- /dev/null +++ b/.github/workflows/v2-dependency-lock.yml @@ -0,0 +1,46 @@ +name: V2 Dependency Lock + +on: + pull_request: + paths: + - go.mod + - internal/v2/** + - .github/workflows/v2-dependency-lock.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + lock: + name: Resolve production v2 dependencies + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: false + + - name: Resolve pinned dependencies + run: | + go get go.starlark.net@5395d018f003e2a08bfbca6dcb2562acee700f62 + go get github.com/tetratelabs/wazero@v1.11.0 + go get github.com/klauspost/reedsolomon@latest + go get github.com/libp2p/go-libp2p@latest + go get github.com/libp2p/go-libp2p-pubsub@latest + echo '--- go.mod ---' + cat go.mod + echo '--- go.sum ---' + cat go.sum + + - name: Upload dependency lock + uses: actions/upload-artifact@v4 + with: + name: v2-go-dependency-lock + path: | + go.mod + go.sum From ebbd8f31e9fcbe74fda1c1497f7570af33fbf498 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:38:36 +0200 Subject: [PATCH 086/274] add certified validator checkpoint history for v2 --- internal/v2/checkpoint/chain.go | 165 +++++++++++++++++++++++++++ internal/v2/checkpoint/chain_test.go | 116 +++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 internal/v2/checkpoint/chain.go create mode 100644 internal/v2/checkpoint/chain_test.go diff --git a/internal/v2/checkpoint/chain.go b/internal/v2/checkpoint/chain.go new file mode 100644 index 00000000..421ec515 --- /dev/null +++ b/internal/v2/checkpoint/chain.go @@ -0,0 +1,165 @@ +package checkpoint + +import ( + "errors" + "sync" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ( + ErrCheckpointConfig = errors.New("invalid checkpoint chain configuration") + ErrCheckpointSequence = errors.New("invalid checkpoint sequence") + ErrCheckpointValidators = errors.New("checkpoint validator set mismatch") + ErrCheckpointCert = errors.New("invalid checkpoint certificate") +) + +type Entry struct { + Header sharding.GlobalHeader + Certificate v2consensus.Certificate + Validators v2consensus.ValidatorSet +} + +type Chain struct { + mu sync.RWMutex + network types.NetworkID + currentRoot types.Hash + lastHeight uint64 + lastHash types.Hash + entries map[uint64]Entry + sets map[types.Hash]v2consensus.ValidatorSet +} + +func New(network types.NetworkID, genesis v2consensus.ValidatorSet) (*Chain, error) { + if types.IsZero32([32]byte(network)) || genesis.Network != network { + return nil, ErrCheckpointConfig + } + root, err := genesis.Root() + if err != nil || types.IsZero32([32]byte(root)) { + return nil, ErrCheckpointConfig + } + return &Chain{ + network: network, + currentRoot: root, + entries: make(map[uint64]Entry), + sets: map[types.Hash]v2consensus.ValidatorSet{root: cloneSet(genesis)}, + }, nil +} + +func (c *Chain) Network() types.NetworkID { + c.mu.RLock() + defer c.mu.RUnlock() + return c.network +} + +func (c *Chain) CurrentRoot() types.Hash { + c.mu.RLock() + defer c.mu.RUnlock() + return c.currentRoot +} + +func (c *Chain) Height() uint64 { + c.mu.RLock() + defer c.mu.RUnlock() + return c.lastHeight +} + +func (c *Chain) Append(header sharding.GlobalHeader, certificate v2consensus.Certificate, current v2consensus.ValidatorSet, next *v2consensus.ValidatorSet) error { + c.mu.Lock() + defer c.mu.Unlock() + + if header.Network != c.network || certificate.Network != c.network || current.Network != c.network { + return ErrCheckpointSequence + } + if header.Height != c.lastHeight+1 || (c.lastHeight > 0 && header.ParentHash != c.lastHash) { + return ErrCheckpointSequence + } + currentRoot, err := current.Root() + if err != nil || currentRoot != c.currentRoot || header.ValidatorRoot != currentRoot { + return ErrCheckpointValidators + } + if certificate.Height != header.Height || certificate.HeaderHash != v2consensus.HeaderConsensusHash(header) || header.CertificateHash != certificate.Hash() { + return ErrCheckpointCert + } + if err := current.VerifyCertificate(certificate); err != nil { + return ErrCheckpointCert + } + + nextRoot := header.EffectiveNextValidatorRoot() + if nextRoot != currentRoot { + if next == nil || next.Network != c.network { + return ErrCheckpointValidators + } + computed, err := next.Root() + if err != nil || computed != nextRoot { + return ErrCheckpointValidators + } + c.sets[nextRoot] = cloneSet(*next) + } else if next != nil { + computed, err := next.Root() + if err != nil || computed != nextRoot { + return ErrCheckpointValidators + } + c.sets[nextRoot] = cloneSet(*next) + } + + c.sets[currentRoot] = cloneSet(current) + c.entries[header.Height] = Entry{Header: header, Certificate: certificate, Validators: cloneSet(current)} + c.currentRoot = nextRoot + c.lastHeight = header.Height + c.lastHash = v2consensus.HeaderConsensusHash(header) + return nil +} + +func (c *Chain) Entry(height uint64) (Entry, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + entry, ok := c.entries[height] + if !ok { + return Entry{}, false + } + entry.Validators = cloneSet(entry.Validators) + entry.Certificate.Votes = append([]v2consensus.Vote(nil), entry.Certificate.Votes...) + return entry, true +} + +func (c *Chain) ValidatorSet(root types.Hash) (v2consensus.ValidatorSet, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + set, ok := c.sets[root] + if !ok { + return v2consensus.ValidatorSet{}, false + } + return cloneSet(set), true +} + +func (c *Chain) VerifyEntry(height uint64) error { + c.mu.RLock() + entry, ok := c.entries[height] + c.mu.RUnlock() + if !ok { + return ErrCheckpointSequence + } + root, err := entry.Validators.Root() + if err != nil || root != entry.Header.ValidatorRoot { + return ErrCheckpointValidators + } + if entry.Certificate.HeaderHash != v2consensus.HeaderConsensusHash(entry.Header) || entry.Header.CertificateHash != entry.Certificate.Hash() { + return ErrCheckpointCert + } + if err := entry.Validators.VerifyCertificate(entry.Certificate); err != nil { + return ErrCheckpointCert + } + return nil +} + +func cloneSet(in v2consensus.ValidatorSet) v2consensus.ValidatorSet { + out := v2consensus.ValidatorSet{Network: in.Network, Validators: make([]v2consensus.Validator, len(in.Validators))} + for i, validator := range in.Validators { + out.Validators[i] = validator + out.Validators[i].PublicKey = append([]byte(nil), validator.PublicKey...) + } + return out +} diff --git a/internal/v2/checkpoint/chain_test.go b/internal/v2/checkpoint/chain_test.go new file mode 100644 index 00000000..f6fcbfef --- /dev/null +++ b/internal/v2/checkpoint/chain_test.go @@ -0,0 +1,116 @@ +package checkpoint + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestCheckpointChainTracksCertifiedValidatorRotation(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("checkpoint-chain"))) + currentKey, current := testValidatorSet(t, network, 10) + _, next := testValidatorSet(t, network, 20) + currentRoot, err := current.Root() + if err != nil { + t.Fatal(err) + } + nextRoot, err := next.Root() + if err != nil { + t.Fatal(err) + } + chain, err := New(network, current) + if err != nil { + t.Fatal(err) + } + + header := sharding.GlobalHeader{ + Version: 2, + Network: network, + Height: 1, + ShardCommitmentRoot: types.HashBytes("shards", []byte("one")), + ValidatorRoot: currentRoot, + NextValidatorRoot: nextRoot, + DataRoot: types.HashBytes("data", []byte("one")), + } + proposal, err := v2consensus.SignProposal(currentKey, header, 0) + if err != nil { + t.Fatal(err) + } + vote, err := v2consensus.SignVote(currentKey, network, 1, 0, v2consensus.HeaderConsensusHash(header)) + if err != nil { + t.Fatal(err) + } + certificate, err := current.BuildCertificate(proposal, []v2consensus.Vote{vote}) + if err != nil { + t.Fatal(err) + } + header.CertificateHash = certificate.Hash() + if err := chain.Append(header, certificate, current, &next); err != nil { + t.Fatal(err) + } + if chain.Height() != 1 || chain.CurrentRoot() != nextRoot { + t.Fatal("checkpoint chain did not advance validator trust root") + } + if err := chain.VerifyEntry(1); err != nil { + t.Fatal(err) + } + if historical, ok := chain.ValidatorSet(currentRoot); !ok || historical.Network != network { + t.Fatal("historical validator set unavailable") + } + if future, ok := chain.ValidatorSet(nextRoot); !ok || future.Network != network { + t.Fatal("next validator set unavailable") + } +} + +func TestCheckpointChainRejectsUncertifiedRotation(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("checkpoint-reject"))) + key, current := testValidatorSet(t, network, 10) + _, attacker := testValidatorSet(t, network, 10) + currentRoot, _ := current.Root() + attackerRoot, _ := attacker.Root() + chain, err := New(network, current) + if err != nil { + t.Fatal(err) + } + header := sharding.GlobalHeader{ + Version: 2, + Network: network, + Height: 1, + ShardCommitmentRoot: types.HashBytes("shards", []byte("reject")), + ValidatorRoot: currentRoot, + NextValidatorRoot: attackerRoot, + DataRoot: types.HashBytes("data", []byte("reject")), + } + proposal, err := v2consensus.SignProposal(key, header, 0) + if err != nil { + t.Fatal(err) + } + vote, err := v2consensus.SignVote(key, network, 1, 0, v2consensus.HeaderConsensusHash(header)) + if err != nil { + t.Fatal(err) + } + certificate, err := current.BuildCertificate(proposal, []v2consensus.Vote{vote}) + if err != nil { + t.Fatal(err) + } + header.CertificateHash = certificate.Hash() + if err := chain.Append(header, certificate, current, nil); err != ErrCheckpointValidators { + t.Fatalf("expected uncertified next-set rejection, got %v", err) + } +} + +func testValidatorSet(t *testing.T, network types.NetworkID, power uint64) (*ecdsa.PrivateKey, v2consensus.ValidatorSet) { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + id := types.ValidatorIDFromPublicKey(pub) + return key, v2consensus.ValidatorSet{Network: network, Validators: []v2consensus.Validator{{ID: id, PublicKey: pub, Power: power}}} +} From 2a4ab6670b745c962331724f57ed3d4d23e6c559 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:42:17 +0200 Subject: [PATCH 087/274] make compute market records consensus-state serializable --- internal/v2/compute/state.go | 372 ++++++++++++++++++++++++++++++ internal/v2/compute/state_test.go | 61 +++++ 2 files changed, 433 insertions(+) create mode 100644 internal/v2/compute/state.go create mode 100644 internal/v2/compute/state_test.go diff --git a/internal/v2/compute/state.go b/internal/v2/compute/state.go new file mode 100644 index 00000000..77a22fae --- /dev/null +++ b/internal/v2/compute/state.go @@ -0,0 +1,372 @@ +package compute + +import ( + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + computeOfferObjectIndex uint32 = 0x90000000 + computeJobObjectIndex uint32 = 0x90000001 +) + +type OnChainJob struct { + ID types.JobID + Job Job + Escrow uint64 + Status JobStatus + Assignments []Assignment + Results []Result +} + +func ParseOffer(data []byte) (Offer, error) { + r := codec.NewReader(data) + provider, err := readAccount(r) + if err != nil { + return Offer{}, ErrInvalidOffer + } + resources, err := readResources(r) + if err != nil { + return Offer{}, ErrInvalidOffer + } + price, err := r.U64() + if err != nil { + return Offer{}, ErrInvalidOffer + } + collateral, err := r.U64() + if err != nil { + return Offer{}, ErrInvalidOffer + } + count, err := r.U32() + if err != nil || count == 0 || count > 7 { + return Offer{}, ErrInvalidOffer + } + modes := make([]VerificationMode, int(count)) + for i := range modes { + mode, err := r.U8() + if err != nil { + return Offer{}, ErrInvalidOffer + } + modes[i] = VerificationMode(mode) + } + validUntil, err := r.U64() + if err != nil || r.Done() != nil { + return Offer{}, ErrInvalidOffer + } + offer := Offer{Provider: provider, Resources: resources, PricePerUnit: price, Collateral: collateral, Verification: modes, ValidUntilHeight: validUntil} + if err := offer.Validate(); err != nil { + return Offer{}, err + } + return offer, nil +} + +func ParseJob(data []byte) (Job, error) { + r := codec.NewReader(data) + owner, err := readAccount(r) + if err != nil { + return Job{}, ErrInvalidJob + } + workload, err := readHash(r) + if err != nil { + return Job{}, ErrInvalidJob + } + inputRoot, err := readHash(r) + if err != nil { + return Job{}, ErrInvalidJob + } + resources, err := readResources(r) + if err != nil { + return Job{}, ErrInvalidJob + } + maxPrice, err := r.U64() + if err != nil { + return Job{}, ErrInvalidJob + } + collateral, err := r.U64() + if err != nil { + return Job{}, ErrInvalidJob + } + mode, err := r.U8() + if err != nil { + return Job{}, ErrInvalidJob + } + deadline, err := r.U64() + if err != nil { + return Job{}, ErrInvalidJob + } + replicas, err := r.U16() + if err != nil { + return Job{}, ErrInvalidJob + } + private, err := r.Bool() + if err != nil || r.Done() != nil { + return Job{}, ErrInvalidJob + } + job := Job{Owner: owner, WorkloadHash: workload, InputRoot: inputRoot, Resources: resources, MaxPrice: maxPrice, CollateralRequired: collateral, Verification: VerificationMode(mode), DeadlineHeight: deadline, Replicas: replicas, Private: private} + if err := job.Validate(); err != nil { + return Job{}, err + } + return job, nil +} + +func (r Result) MarshalBinary() ([]byte, error) { + if err := r.Validate(); err != nil { + return nil, err + } + var w codec.Writer + w.Fixed(r.JobID[:]) + w.Fixed(r.Provider[:]) + w.Fixed(r.ResultRoot[:]) + w.Fixed(r.ProofHash[:]) + w.Fixed(r.AttestationHash[:]) + w.U64(r.CompletedHeight) + return w.BytesCopy(), nil +} + +func ParseResult(data []byte) (Result, error) { + r := codec.NewReader(data) + jobIDBytes, err := r.Fixed(32) + if err != nil { + return Result{}, ErrInvalidResult + } + provider, err := readAccount(r) + if err != nil { + return Result{}, ErrInvalidResult + } + resultRoot, err := readHash(r) + if err != nil { + return Result{}, ErrInvalidResult + } + proofHash, err := readHash(r) + if err != nil { + return Result{}, ErrInvalidResult + } + attestationHash, err := readHash(r) + if err != nil { + return Result{}, ErrInvalidResult + } + completedHeight, err := r.U64() + if err != nil || r.Done() != nil { + return Result{}, ErrInvalidResult + } + var jobID types.JobID + copy(jobID[:], jobIDBytes) + result := Result{JobID: jobID, Provider: provider, ResultRoot: resultRoot, ProofHash: proofHash, AttestationHash: attestationHash, CompletedHeight: completedHeight} + if err := result.Validate(); err != nil { + return Result{}, err + } + return result, nil +} + +func (a Assignment) MarshalBinary() ([]byte, error) { + if types.IsZero32([32]byte(a.OfferID)) || types.IsZero32([32]byte(a.Provider)) || a.Price == 0 { + return nil, ErrMarketState + } + var w codec.Writer + w.Fixed(a.OfferID[:]) + w.Fixed(a.Provider[:]) + w.U64(a.Price) + return w.BytesCopy(), nil +} + +func ParseAssignment(data []byte) (Assignment, error) { + r := codec.NewReader(data) + offerID, err := readHash(r) + if err != nil { + return Assignment{}, ErrMarketState + } + provider, err := readAccount(r) + if err != nil { + return Assignment{}, ErrMarketState + } + price, err := r.U64() + if err != nil || r.Done() != nil || price == 0 { + return Assignment{}, ErrMarketState + } + assignment := Assignment{OfferID: offerID, Provider: provider, Price: price} + if _, err := assignment.MarshalBinary(); err != nil { + return Assignment{}, err + } + return assignment, nil +} + +func (j OnChainJob) MarshalBinary() ([]byte, error) { + if types.IsZero32([32]byte(j.ID)) || j.Escrow < j.Job.MaxPrice || j.Status < JobPending || j.Status > JobExpired { + return nil, ErrMarketState + } + jobBytes, err := j.Job.MarshalBinary() + if err != nil { + return nil, err + } + assignments := append([]Assignment(nil), j.Assignments...) + sort.Slice(assignments, func(i, k int) bool { return assignments[i].Provider.String() < assignments[k].Provider.String() }) + results := append([]Result(nil), j.Results...) + sort.Slice(results, func(i, k int) bool { return results[i].Provider.String() < results[k].Provider.String() }) + + var w codec.Writer + w.Fixed(j.ID[:]) + w.Bytes(jobBytes) + w.U64(j.Escrow) + w.U8(uint8(j.Status)) + w.U32(uint32(len(assignments))) + for _, assignment := range assignments { + raw, err := assignment.MarshalBinary() + if err != nil { + return nil, err + } + w.Bytes(raw) + } + w.U32(uint32(len(results))) + for _, result := range results { + raw, err := result.MarshalBinary() + if err != nil { + return nil, err + } + w.Bytes(raw) + } + return w.BytesCopy(), nil +} + +func ParseOnChainJob(data []byte) (OnChainJob, error) { + r := codec.NewReader(data) + jobIDBytes, err := r.Fixed(32) + if err != nil { + return OnChainJob{}, ErrMarketState + } + jobBytes, err := r.Bytes(1 << 20) + if err != nil { + return OnChainJob{}, ErrMarketState + } + job, err := ParseJob(jobBytes) + if err != nil { + return OnChainJob{}, err + } + escrow, err := r.U64() + if err != nil { + return OnChainJob{}, ErrMarketState + } + status, err := r.U8() + if err != nil { + return OnChainJob{}, ErrMarketState + } + assignmentCount, err := r.U32() + if err != nil || assignmentCount > 1024 { + return OnChainJob{}, ErrMarketState + } + assignments := make([]Assignment, int(assignmentCount)) + for i := range assignments { + raw, err := r.Bytes(1024) + if err != nil { + return OnChainJob{}, ErrMarketState + } + assignments[i], err = ParseAssignment(raw) + if err != nil { + return OnChainJob{}, err + } + } + resultCount, err := r.U32() + if err != nil || resultCount > 1024 { + return OnChainJob{}, ErrMarketState + } + results := make([]Result, int(resultCount)) + for i := range results { + raw, err := r.Bytes(1024) + if err != nil { + return OnChainJob{}, ErrMarketState + } + results[i], err = ParseResult(raw) + if err != nil { + return OnChainJob{}, err + } + } + if r.Done() != nil { + return OnChainJob{}, ErrMarketState + } + var jobID types.JobID + copy(jobID[:], jobIDBytes) + record := OnChainJob{ID: jobID, Job: job, Escrow: escrow, Status: JobStatus(status), Assignments: assignments, Results: results} + if _, err := record.MarshalBinary(); err != nil { + return OnChainJob{}, err + } + return record, nil +} + +func NewOfferObject(txID types.Hash, shard uint32, offer Offer) (object.Object, error) { + raw, err := offer.MarshalBinary() + if err != nil { + return object.Object{}, err + } + return object.Object{ID: types.ObjectIDForShard(txID, computeOfferObjectIndex, shard), Version: 1, Owner: offer.Provider, Kind: object.KindComputeOffer, Data: raw}, nil +} + +func NewJobObject(txID types.Hash, shard uint32, job Job, escrow uint64) (object.Object, OnChainJob, error) { + jobID := types.JobIDFromTransaction(txID, 0) + record := OnChainJob{ID: jobID, Job: job, Escrow: escrow, Status: JobPending} + raw, err := record.MarshalBinary() + if err != nil { + return object.Object{}, OnChainJob{}, err + } + return object.Object{ID: types.ObjectIDForShard(txID, computeJobObjectIndex, shard), Version: 1, Owner: job.Owner, Kind: object.KindComputeJob, Data: raw}, record, nil +} + +func readResources(r *codec.Reader) (Resources, error) { + cpu, err := r.U16() + if err != nil { + return Resources{}, err + } + memory, err := r.U32() + if err != nil { + return Resources{}, err + } + gpu, err := r.U16() + if err != nil { + return Resources{}, err + } + gpuMemory, err := r.U32() + if err != nil { + return Resources{}, err + } + storage, err := r.U64() + if err != nil { + return Resources{}, err + } + bandwidth, err := r.U32() + if err != nil { + return Resources{}, err + } + count, err := r.U32() + if err != nil || count > 32 { + return Resources{}, ErrInvalidOffer + } + capabilities := make([]string, int(count)) + for i := range capabilities { + capabilities[i], err = r.String(64) + if err != nil { + return Resources{}, err + } + } + return Resources{CPUCores: cpu, MemoryMiB: memory, GPUCount: gpu, GPUMemoryMiB: gpuMemory, StorageMiB: storage, BandwidthMbps: bandwidth, Capabilities: capabilities}, nil +} + +func readAccount(r *codec.Reader) (types.AccountID, error) { + raw, err := r.Fixed(32) + if err != nil { + return types.AccountID{}, err + } + var out types.AccountID + copy(out[:], raw) + return out, nil +} + +func readHash(r *codec.Reader) (types.Hash, error) { + raw, err := r.Fixed(32) + if err != nil { + return types.Hash{}, err + } + var out types.Hash + copy(out[:], raw) + return out, nil +} diff --git a/internal/v2/compute/state_test.go b/internal/v2/compute/state_test.go new file mode 100644 index 00000000..d9dfbdb0 --- /dev/null +++ b/internal/v2/compute/state_test.go @@ -0,0 +1,61 @@ +package compute + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestComputeProtocolObjectsRoundTrip(t *testing.T) { + owner := types.AccountIDFromPublicKey([]byte("compute-owner")) + provider := types.AccountIDFromPublicKey([]byte("compute-provider")) + offer := Offer{ + Provider: provider, + Resources: Resources{CPUCores: 8, MemoryMiB: 16384, GPUCount: 1, GPUMemoryMiB: 24576, StorageMiB: 102400, BandwidthMbps: 1000, Capabilities: []string{"cuda", "render"}}, + PricePerUnit: 25, Collateral: 100, Verification: []VerificationMode{VerificationReplicated, VerificationTEE}, ValidUntilHeight: 500, + } + rawOffer, err := offer.MarshalBinary() + if err != nil { + t.Fatal(err) + } + parsedOffer, err := ParseOffer(rawOffer) + if err != nil || parsedOffer.Provider != provider || parsedOffer.Resources.GPUCount != 1 { + t.Fatalf("offer round trip failed: %+v %v", parsedOffer, err) + } + + job := Job{ + Owner: owner, WorkloadHash: types.HashBytes("workload", []byte("render-scene")), InputRoot: types.HashBytes("input", []byte("scene")), + Resources: Resources{CPUCores: 4, MemoryMiB: 8192, GPUCount: 1, GPUMemoryMiB: 8192, StorageMiB: 2048, BandwidthMbps: 100, Capabilities: []string{"render"}}, + MaxPrice: 50, CollateralRequired: 50, Verification: VerificationReplicated, DeadlineHeight: 600, Replicas: 2, + } + rawJob, err := job.MarshalBinary() + if err != nil { + t.Fatal(err) + } + if parsedJob, err := ParseJob(rawJob); err != nil || parsedJob.Owner != owner || parsedJob.Replicas != 2 { + t.Fatalf("job round trip failed: %+v %v", parsedJob, err) + } + + txID := types.HashBytes("tx", []byte("compute-job")) + jobObject, record, err := NewJobObject(txID, 0, job, 50) + if err != nil { + t.Fatal(err) + } + if jobObject.Kind != object.KindComputeJob || record.Status != JobPending { + t.Fatal("unexpected compute job object") + } + assignment := Assignment{OfferID: types.HashBytes("offer", []byte("1")), Provider: provider, Price: 25} + result := Result{JobID: record.ID, Provider: provider, ResultRoot: types.HashBytes("result", []byte("root")), CompletedHeight: 550} + record.Assignments = []Assignment{assignment} + record.Results = []Result{result} + record.Status = JobAwaitingVerification + rawRecord, err := record.MarshalBinary() + if err != nil { + t.Fatal(err) + } + parsed, err := ParseOnChainJob(rawRecord) + if err != nil || parsed.ID != record.ID || len(parsed.Assignments) != 1 || len(parsed.Results) != 1 { + t.Fatalf("on-chain record round trip failed: %+v %v", parsed, err) + } +} From 8a7264569b6286331459839b42252c813a352efd Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:45:14 +0200 Subject: [PATCH 088/274] add consensus-safe compute escrow and replicated slashing rules --- internal/v2/compute/onchain.go | 212 ++++++++++++++++++++++++++++ internal/v2/compute/onchain_test.go | 83 +++++++++++ 2 files changed, 295 insertions(+) create mode 100644 internal/v2/compute/onchain.go create mode 100644 internal/v2/compute/onchain_test.go diff --git a/internal/v2/compute/onchain.go b/internal/v2/compute/onchain.go new file mode 100644 index 00000000..6be9c754 --- /dev/null +++ b/internal/v2/compute/onchain.go @@ -0,0 +1,212 @@ +package compute + +import ( + "math" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +type OnChainSettlement struct { + Settlement + CollateralReturns map[types.AccountID]uint64 + SlashedCollateral map[types.AccountID]uint64 + SlashReward uint64 +} + +func AssignOnChain(record OnChainJob, offerID types.Hash, offer Offer, height uint64) (OnChainJob, Assignment, uint64, error) { + if record.Status == JobSettled || record.Status == JobExpired || height == 0 || height > record.Job.DeadlineHeight || height > offer.ValidUntilHeight { + return OnChainJob{}, Assignment{}, 0, ErrMarketState + } + if !offerMatchesJob(offer, record.Job) || offer.Collateral < record.Job.CollateralRequired { + return OnChainJob{}, Assignment{}, 0, ErrMarketMatch + } + for _, existing := range record.Assignments { + if existing.Provider == offer.Provider { + return OnChainJob{}, Assignment{}, 0, ErrMarketDuplicate + } + } + target := requiredAssignments(record.Job) + if len(record.Assignments) >= target { + return OnChainJob{}, Assignment{}, 0, ErrMarketState + } + var committed uint64 + for _, existing := range record.Assignments { + if math.MaxUint64-committed < existing.Price { + return OnChainJob{}, Assignment{}, 0, ErrMarketEscrow + } + committed += existing.Price + } + if offer.PricePerUnit > record.Job.MaxPrice || math.MaxUint64-committed < offer.PricePerUnit || committed+offer.PricePerUnit > record.Job.MaxPrice || committed+offer.PricePerUnit > record.Escrow { + return OnChainJob{}, Assignment{}, 0, ErrMarketEscrow + } + assignment := Assignment{OfferID: offerID, Provider: offer.Provider, Price: offer.PricePerUnit} + updated := cloneOnChainJob(record) + updated.Assignments = append(updated.Assignments, assignment) + if len(updated.Assignments) == target { + updated.Status = JobAssigned + } + return updated, assignment, offer.Collateral - record.Job.CollateralRequired, nil +} + +func SubmitOnChainResult(record OnChainJob, result Result) (OnChainJob, error) { + if err := result.Validate(); err != nil || result.JobID != record.ID { + return OnChainJob{}, ErrInvalidResult + } + if record.Status != JobAssigned && record.Status != JobAwaitingVerification { + return OnChainJob{}, ErrMarketState + } + if result.CompletedHeight > record.Job.DeadlineHeight { + return OnChainJob{}, ErrMarketState + } + assigned := false + for _, assignment := range record.Assignments { + if assignment.Provider == result.Provider { + assigned = true + break + } + } + if !assigned { + return OnChainJob{}, ErrMarketMatch + } + for _, existing := range record.Results { + if existing.Provider == result.Provider { + return OnChainJob{}, ErrMarketDuplicate + } + } + updated := cloneOnChainJob(record) + updated.Results = append(updated.Results, result) + if len(updated.Results) == requiredAssignments(updated.Job) { + updated.Status = JobAwaitingVerification + } + return updated, nil +} + +func FinalizeOnChain(record OnChainJob, evidence VerificationEvidence) (OnChainJob, OnChainSettlement, error) { + if record.Status != JobAwaitingVerification || len(record.Results) != requiredAssignments(record.Job) { + return OnChainJob{}, OnChainSettlement{}, ErrMarketState + } + results := make(map[types.AccountID]Result, len(record.Results)) + for _, result := range record.Results { + results[result.Provider] = result + } + root, replicatedMatch := commonResultRootRecord(record.Results) + if types.IsZero32([32]byte(root)) || !verificationSatisfied(record.Job.Verification, replicatedMatch, results, evidence) { + return OnChainJob{}, OnChainSettlement{}, ErrMarketVerification + } + payments := make(map[types.AccountID]uint64, len(record.Assignments)) + collateral := make(map[types.AccountID]uint64, len(record.Assignments)) + var paid uint64 + for _, assignment := range record.Assignments { + if math.MaxUint64-paid < assignment.Price { + return OnChainJob{}, OnChainSettlement{}, ErrMarketEscrow + } + paid += assignment.Price + payments[assignment.Provider] += assignment.Price + collateral[assignment.Provider] += record.Job.CollateralRequired + } + if paid > record.Escrow { + return OnChainJob{}, OnChainSettlement{}, ErrMarketEscrow + } + updated := cloneOnChainJob(record) + updated.Status = JobSettled + return updated, OnChainSettlement{ + Settlement: Settlement{JobID: record.ID, ResultRoot: root, Payments: payments, Refund: record.Escrow - paid}, + CollateralReturns: collateral, + SlashedCollateral: make(map[types.AccountID]uint64), + }, nil +} + +// ResolveReplicatedMajority provides an objective dispute rule for replicated +// jobs. At least three replicas must have reported. Providers on the strict +// majority result root are paid and recover collateral. Minority collateral is +// slashed to the job owner, while unpaid compute budget is refunded. +func ResolveReplicatedMajority(record OnChainJob) (OnChainJob, OnChainSettlement, error) { + if record.Job.Verification != VerificationReplicated || len(record.Results) < 3 || len(record.Results) != len(record.Assignments) { + return OnChainJob{}, OnChainSettlement{}, ErrMarketVerification + } + counts := make(map[types.Hash]int) + for _, result := range record.Results { + counts[result.ResultRoot]++ + } + var majority types.Hash + majorityCount := 0 + for root, count := range counts { + if count > majorityCount { + majority, majorityCount = root, count + } + } + if majorityCount*2 <= len(record.Results) { + return OnChainJob{}, OnChainSettlement{}, ErrMarketVerification + } + resultByProvider := make(map[types.AccountID]types.Hash, len(record.Results)) + for _, result := range record.Results { + resultByProvider[result.Provider] = result.ResultRoot + } + payments := make(map[types.AccountID]uint64) + collateralReturns := make(map[types.AccountID]uint64) + slashed := make(map[types.AccountID]uint64) + var paid, slashReward uint64 + for _, assignment := range record.Assignments { + if resultByProvider[assignment.Provider] == majority { + if math.MaxUint64-paid < assignment.Price { + return OnChainJob{}, OnChainSettlement{}, ErrMarketEscrow + } + paid += assignment.Price + payments[assignment.Provider] += assignment.Price + collateralReturns[assignment.Provider] += record.Job.CollateralRequired + } else { + slashed[assignment.Provider] += record.Job.CollateralRequired + if math.MaxUint64-slashReward < record.Job.CollateralRequired { + return OnChainJob{}, OnChainSettlement{}, ErrMarketEscrow + } + slashReward += record.Job.CollateralRequired + } + } + if paid > record.Escrow { + return OnChainJob{}, OnChainSettlement{}, ErrMarketEscrow + } + updated := cloneOnChainJob(record) + updated.Status = JobSettled + return updated, OnChainSettlement{ + Settlement: Settlement{JobID: record.ID, ResultRoot: majority, Payments: payments, Refund: record.Escrow - paid}, + CollateralReturns: collateralReturns, + SlashedCollateral: slashed, + SlashReward: slashReward, + }, nil +} + +func ExpireOnChain(record OnChainJob, height uint64) (OnChainJob, uint64, map[types.AccountID]uint64, error) { + if record.Status == JobSettled || record.Status == JobExpired || height <= record.Job.DeadlineHeight { + return OnChainJob{}, 0, nil, ErrMarketState + } + updated := cloneOnChainJob(record) + updated.Status = JobExpired + collateral := make(map[types.AccountID]uint64, len(record.Assignments)) + for _, assignment := range record.Assignments { + collateral[assignment.Provider] += record.Job.CollateralRequired + } + return updated, record.Escrow, collateral, nil +} + +func cloneOnChainJob(record OnChainJob) OnChainJob { + out := record + out.Assignments = append([]Assignment(nil), record.Assignments...) + out.Results = append([]Result(nil), record.Results...) + return out +} + +func commonResultRootRecord(results []Result) (types.Hash, bool) { + if len(results) == 0 { + return types.Hash{}, false + } + ordered := append([]Result(nil), results...) + sort.Slice(ordered, func(i, j int) bool { return ordered[i].Provider.String() < ordered[j].Provider.String() }) + root := ordered[0].ResultRoot + for _, result := range ordered[1:] { + if result.ResultRoot != root { + return root, false + } + } + return root, true +} diff --git a/internal/v2/compute/onchain_test.go b/internal/v2/compute/onchain_test.go new file mode 100644 index 00000000..aedaabb4 --- /dev/null +++ b/internal/v2/compute/onchain_test.go @@ -0,0 +1,83 @@ +package compute + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestOnChainReplicatedLifecycleAndCollateral(t *testing.T) { + owner := types.AccountIDFromPublicKey([]byte("owner")) + providers := []types.AccountID{ + types.AccountIDFromPublicKey([]byte("p1")), + types.AccountIDFromPublicKey([]byte("p2")), + types.AccountIDFromPublicKey([]byte("p3")), + } + resources := Resources{CPUCores: 4, MemoryMiB: 4096} + job := Job{Owner: owner, WorkloadHash: types.HashBytes("work", []byte("render")), InputRoot: types.HashBytes("input", []byte("scene")), Resources: resources, MaxPrice: 30, CollateralRequired: 5, Verification: VerificationReplicated, DeadlineHeight: 100, Replicas: 3} + record := OnChainJob{ID: types.JobID(types.HashBytes("job", []byte("one"))), Job: job, Escrow: 30, Status: JobPending} + for i, provider := range providers { + offer := Offer{Provider: provider, Resources: resources, PricePerUnit: uint64(i + 2), Collateral: 9, Verification: []VerificationMode{VerificationReplicated}, ValidUntilHeight: 90} + var err error + var excess uint64 + record, _, excess, err = AssignOnChain(record, types.HashBytes("offer", []byte{byte(i)}), offer, 10) + if err != nil { + t.Fatal(err) + } + if excess != 4 { + t.Fatalf("unexpected collateral excess %d", excess) + } + } + if record.Status != JobAssigned { + t.Fatal("job not assigned after replica target") + } + root := types.HashBytes("result", []byte("same")) + for i, provider := range providers { + var err error + record, err = SubmitOnChainResult(record, Result{JobID: record.ID, Provider: provider, ResultRoot: root, CompletedHeight: uint64(20 + i)}) + if err != nil { + t.Fatal(err) + } + } + settled, settlement, err := FinalizeOnChain(record, VerificationEvidence{}) + if err != nil { + t.Fatal(err) + } + if settled.Status != JobSettled || settlement.ResultRoot != root || settlement.Refund != 21 { + t.Fatalf("unexpected settlement: %+v", settlement) + } + for _, provider := range providers { + if settlement.CollateralReturns[provider] != 5 { + t.Fatal("provider collateral was not returned") + } + } +} + +func TestReplicatedMajoritySlashesMinority(t *testing.T) { + owner := types.AccountIDFromPublicKey([]byte("owner-majority")) + providers := []types.AccountID{ + types.AccountIDFromPublicKey([]byte("mp1")), + types.AccountIDFromPublicKey([]byte("mp2")), + types.AccountIDFromPublicKey([]byte("mp3")), + } + resources := Resources{CPUCores: 2, MemoryMiB: 2048} + job := Job{Owner: owner, WorkloadHash: types.HashBytes("work", []byte("majority")), InputRoot: types.HashBytes("input", []byte("majority")), Resources: resources, MaxPrice: 9, CollateralRequired: 7, Verification: VerificationReplicated, DeadlineHeight: 100, Replicas: 3} + record := OnChainJob{ID: types.JobID(types.HashBytes("job", []byte("majority"))), Job: job, Escrow: 9, Status: JobAwaitingVerification} + for i, provider := range providers { + record.Assignments = append(record.Assignments, Assignment{OfferID: types.HashBytes("offer", []byte{byte(i)}), Provider: provider, Price: 2}) + } + good := types.HashBytes("result", []byte("good")) + bad := types.HashBytes("result", []byte("bad")) + record.Results = []Result{ + {JobID: record.ID, Provider: providers[0], ResultRoot: good, CompletedHeight: 10}, + {JobID: record.ID, Provider: providers[1], ResultRoot: good, CompletedHeight: 10}, + {JobID: record.ID, Provider: providers[2], ResultRoot: bad, CompletedHeight: 10}, + } + _, settlement, err := ResolveReplicatedMajority(record) + if err != nil { + t.Fatal(err) + } + if settlement.ResultRoot != good || settlement.SlashedCollateral[providers[2]] != 7 || settlement.SlashReward != 7 || settlement.Payments[providers[2]] != 0 { + t.Fatalf("unexpected majority settlement: %+v", settlement) + } +} From a95168cee5aa41ad9a3c6219ee9de74a8269ae91 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:50:41 +0200 Subject: [PATCH 089/274] gofmt compute consensus settlement rules --- internal/v2/compute/onchain.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/v2/compute/onchain.go b/internal/v2/compute/onchain.go index 6be9c754..12d93d86 100644 --- a/internal/v2/compute/onchain.go +++ b/internal/v2/compute/onchain.go @@ -11,7 +11,7 @@ type OnChainSettlement struct { Settlement CollateralReturns map[types.AccountID]uint64 SlashedCollateral map[types.AccountID]uint64 - SlashReward uint64 + SlashReward uint64 } func AssignOnChain(record OnChainJob, offerID types.Hash, offer Offer, height uint64) (OnChainJob, Assignment, uint64, error) { @@ -111,7 +111,7 @@ func FinalizeOnChain(record OnChainJob, evidence VerificationEvidence) (OnChainJ updated := cloneOnChainJob(record) updated.Status = JobSettled return updated, OnChainSettlement{ - Settlement: Settlement{JobID: record.ID, ResultRoot: root, Payments: payments, Refund: record.Escrow - paid}, + Settlement: Settlement{JobID: record.ID, ResultRoot: root, Payments: payments, Refund: record.Escrow - paid}, CollateralReturns: collateral, SlashedCollateral: make(map[types.AccountID]uint64), }, nil @@ -169,10 +169,10 @@ func ResolveReplicatedMajority(record OnChainJob) (OnChainJob, OnChainSettlement updated := cloneOnChainJob(record) updated.Status = JobSettled return updated, OnChainSettlement{ - Settlement: Settlement{JobID: record.ID, ResultRoot: majority, Payments: payments, Refund: record.Escrow - paid}, + Settlement: Settlement{JobID: record.ID, ResultRoot: majority, Payments: payments, Refund: record.Escrow - paid}, CollateralReturns: collateralReturns, SlashedCollateral: slashed, - SlashReward: slashReward, + SlashReward: slashReward, }, nil } From 67c1bc13861e4b88b9332b25b9c0a9dba9364e55 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:51:03 +0200 Subject: [PATCH 090/274] gofmt compute protocol object tests --- internal/v2/compute/state_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/v2/compute/state_test.go b/internal/v2/compute/state_test.go index d9dfbdb0..7234870d 100644 --- a/internal/v2/compute/state_test.go +++ b/internal/v2/compute/state_test.go @@ -11,8 +11,8 @@ func TestComputeProtocolObjectsRoundTrip(t *testing.T) { owner := types.AccountIDFromPublicKey([]byte("compute-owner")) provider := types.AccountIDFromPublicKey([]byte("compute-provider")) offer := Offer{ - Provider: provider, - Resources: Resources{CPUCores: 8, MemoryMiB: 16384, GPUCount: 1, GPUMemoryMiB: 24576, StorageMiB: 102400, BandwidthMbps: 1000, Capabilities: []string{"cuda", "render"}}, + Provider: provider, + Resources: Resources{CPUCores: 8, MemoryMiB: 16384, GPUCount: 1, GPUMemoryMiB: 24576, StorageMiB: 102400, BandwidthMbps: 1000, Capabilities: []string{"cuda", "render"}}, PricePerUnit: 25, Collateral: 100, Verification: []VerificationMode{VerificationReplicated, VerificationTEE}, ValidUntilHeight: 500, } rawOffer, err := offer.MarshalBinary() @@ -27,7 +27,7 @@ func TestComputeProtocolObjectsRoundTrip(t *testing.T) { job := Job{ Owner: owner, WorkloadHash: types.HashBytes("workload", []byte("render-scene")), InputRoot: types.HashBytes("input", []byte("scene")), Resources: Resources{CPUCores: 4, MemoryMiB: 8192, GPUCount: 1, GPUMemoryMiB: 8192, StorageMiB: 2048, BandwidthMbps: 100, Capabilities: []string{"render"}}, - MaxPrice: 50, CollateralRequired: 50, Verification: VerificationReplicated, DeadlineHeight: 600, Replicas: 2, + MaxPrice: 50, CollateralRequired: 50, Verification: VerificationReplicated, DeadlineHeight: 600, Replicas: 2, } rawJob, err := job.MarshalBinary() if err != nil { From 6b8950a695f8fea800b71a789edbce25c60c5b9c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:00:02 +0200 Subject: [PATCH 091/274] add deterministic metered Zephyr Script contract runtime --- go.mod | 2 + internal/v2/contracts/contracts.go | 32 +++- internal/v2/contracts/metered.go | 18 +- internal/v2/contracts/script_runtime.go | 186 +++++++++++++++++++ internal/v2/contracts/script_runtime_test.go | 80 ++++++++ 5 files changed, 306 insertions(+), 12 deletions(-) create mode 100644 internal/v2/contracts/script_runtime.go create mode 100644 internal/v2/contracts/script_runtime_test.go diff --git a/go.mod b/go.mod index 5bcb9a59..0c4f699e 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/zephyr-chain/zephyr-chain go 1.26.0 + +require go.starlark.net v0.0.0-20260708150628-5395d018f003 diff --git a/internal/v2/contracts/contracts.go b/internal/v2/contracts/contracts.go index f7124481..8c74d053 100644 --- a/internal/v2/contracts/contracts.go +++ b/internal/v2/contracts/contracts.go @@ -3,19 +3,21 @@ package contracts import ( "bytes" "errors" + "unicode/utf8" "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" ) const ( - RuntimeWASMv1 = "wasm-v1" - MaxModuleBytes = 4 << 20 - MaxInitialStateBytes = 1 << 20 + RuntimeWASMv1 = "wasm-v1" + RuntimeZephyrScriptV1 = "zephyr-script-v1" + MaxModuleBytes = 4 << 20 + MaxInitialStateBytes = 1 << 20 ) var ( - ErrInvalidModule = errors.New("invalid deterministic wasm module") + ErrInvalidModule = errors.New("invalid deterministic contract module") ErrInvalidDeployment = errors.New("invalid contract deployment") ErrFuelExhausted = errors.New("contract fuel exhausted") ErrUndeclaredAccess = errors.New("contract attempted undeclared state access") @@ -31,13 +33,22 @@ type Deployment struct { } func (d Deployment) Validate() error { - if d.Runtime != RuntimeWASMv1 || d.ABI == 0 || len(d.Code) == 0 || len(d.Code) > MaxModuleBytes || + if d.ABI == 0 || len(d.Code) == 0 || len(d.Code) > MaxModuleBytes || len(d.InitialState) > MaxInitialStateBytes || d.MaxMemoryPages == 0 || types.IsZero32([32]byte(d.UpgradeAuthority)) { return ErrInvalidDeployment } - if !ValidateWASMModule(d.Code) { - return ErrInvalidModule + switch d.Runtime { + case RuntimeWASMv1: + if !ValidateWASMModule(d.Code) { + return ErrInvalidModule + } + case RuntimeZephyrScriptV1: + if !ValidateScriptModule(d.Code) { + return ErrInvalidModule + } + default: + return ErrInvalidDeployment } return nil } @@ -64,6 +75,10 @@ func ValidateWASMModule(code []byte) bool { bytes.Equal(code[4:8], []byte{0x01, 0x00, 0x00, 0x00}) } +func ValidateScriptModule(code []byte) bool { + return len(code) > 0 && len(code) <= MaxModuleBytes && utf8.Valid(code) && !bytes.ContainsRune(code, 0) +} + type Access struct { ObjectID types.ObjectID Write bool @@ -71,9 +86,12 @@ type Access struct { type Request struct { ContractID types.ContractID + Runtime string + Code []byte Entrypoint string Arguments []byte Accesses []Access + ReadValues map[types.ObjectID][]byte FuelLimit uint64 } diff --git a/internal/v2/contracts/metered.go b/internal/v2/contracts/metered.go index eac0c103..145f662d 100644 --- a/internal/v2/contracts/metered.go +++ b/internal/v2/contracts/metered.go @@ -20,15 +20,15 @@ var ( ErrInvalidResult = errors.New("invalid deterministic contract result") ) -// MeteredRuntime is the consensus guard around a concrete WASM engine. A -// concrete runtime may change for performance, but this boundary makes fuel, -// declared state access and bounded output consensus invariants. +// MeteredRuntime is the consensus guard around a concrete deterministic +// contract engine. The engine may change, but fuel, declared state access and +// bounded output remain consensus invariants outside the interpreter. type MeteredRuntime struct { Inner Runtime } func (m MeteredRuntime) ValidateModule(code []byte) error { - if m.Inner == nil || !ValidateWASMModule(code) { + if m.Inner == nil { return ErrInvalidModule } return m.Inner.ValidateModule(code) @@ -42,6 +42,9 @@ func (m MeteredRuntime) Execute(request Request) (Result, error) { if err != nil { return Result{}, err } + if err := m.Inner.ValidateModule(request.Code); err != nil { + return Result{}, err + } result, err := m.Inner.Execute(request) if err != nil { return Result{}, err @@ -68,7 +71,7 @@ func (m MeteredRuntime) Execute(request Request) (Result, error) { func validateRequest(request Request) (map[types.ObjectID]bool, error) { if types.IsZero32([32]byte(request.ContractID)) || strings.TrimSpace(request.Entrypoint) == "" || len(request.Entrypoint) > 128 || - len(request.Arguments) > MaxArgumentsBytes || request.FuelLimit == 0 || len(request.Accesses) > MaxAccesses { + len(request.Code) == 0 || len(request.Code) > MaxModuleBytes || len(request.Arguments) > MaxArgumentsBytes || request.FuelLimit == 0 || len(request.Accesses) > MaxAccesses { return nil, ErrInvalidRequest } allowed := make(map[types.ObjectID]bool, len(request.Accesses)) @@ -81,5 +84,10 @@ func validateRequest(request Request) (map[types.ObjectID]bool, error) { } allowed[access.ObjectID] = access.Write } + for id := range request.ReadValues { + if _, declared := allowed[id]; !declared { + return nil, ErrUndeclaredAccess + } + } return allowed, nil } diff --git a/internal/v2/contracts/script_runtime.go b/internal/v2/contracts/script_runtime.go new file mode 100644 index 00000000..0657c41d --- /dev/null +++ b/internal/v2/contracts/script_runtime.go @@ -0,0 +1,186 @@ +package contracts + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "strings" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "go.starlark.net/starlark" +) + +var ErrScriptRuntime = errors.New("zephyr script execution failed") + +// ScriptRuntime is Zephyr's deterministic reference smart-contract runtime. +// It exposes no clock, randomness, filesystem, network or dynamic module load. +// Execution is bounded by Starlark's abstract step counter, which is used as +// deterministic fuel for consensus. +type ScriptRuntime struct{} + +func (ScriptRuntime) ValidateModule(code []byte) error { + if !ValidateScriptModule(code) { + return ErrInvalidModule + } + thread := &starlark.Thread{Name: "zephyr-validate", Load: disabledLoad} + thread.SetMaxExecutionSteps(1_000_000) + if _, err := starlark.ExecFile(thread, "contract.star", string(code), validationPredeclared()); err != nil { + return fmt.Errorf("%w: %v", ErrInvalidModule, err) + } + return nil +} + +func (ScriptRuntime) Execute(request Request) (Result, error) { + if request.Runtime != RuntimeZephyrScriptV1 || !ValidateScriptModule(request.Code) { + return Result{}, ErrInvalidModule + } + allowed := make(map[string]bool, len(request.Accesses)) + for _, access := range request.Accesses { + allowed[access.ObjectID.String()] = access.Write + } + writes := make(map[types.ObjectID][]byte) + events := make([][]byte, 0) + + predeclared := emptyPredeclared() + predeclared["state_read"] = starlark.NewBuiltin("state_read", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var id string + if err := starlark.UnpackArgs("state_read", args, kwargs, "id", &id); err != nil { + return nil, err + } + normalized, rawID, err := parseObjectID(id) + if err != nil { + return nil, err + } + if _, ok := allowed[normalized]; !ok { + return nil, ErrUndeclaredAccess + } + value, ok := request.ReadValues[rawID] + if !ok { + return starlark.None, nil + } + return starlark.Bytes(string(value)), nil + }) + predeclared["state_write"] = starlark.NewBuiltin("state_write", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var id string + var value starlark.Value + if err := starlark.UnpackArgs("state_write", args, kwargs, "id", &id, "value", &value); err != nil { + return nil, err + } + normalized, rawID, err := parseObjectID(id) + if err != nil { + return nil, err + } + if !allowed[normalized] { + return nil, ErrUndeclaredAccess + } + bytes, err := valueBytes(value) + if err != nil || len(bytes) > MaxReturnBytes { + return nil, ErrInvalidResult + } + writes[rawID] = append([]byte(nil), bytes...) + return starlark.None, nil + }) + predeclared["emit"] = starlark.NewBuiltin("emit", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var value starlark.Value + if err := starlark.UnpackArgs("emit", args, kwargs, "value", &value); err != nil { + return nil, err + } + bytes, err := valueBytes(value) + if err != nil || len(bytes) > MaxEventBytes || len(events) >= MaxEvents { + return nil, ErrInvalidResult + } + events = append(events, append([]byte(nil), bytes...)) + return starlark.None, nil + }) + predeclared["sha256"] = starlark.NewBuiltin("sha256", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var value starlark.Value + if err := starlark.UnpackArgs("sha256", args, kwargs, "value", &value); err != nil { + return nil, err + } + bytes, err := valueBytes(value) + if err != nil { + return nil, err + } + hash := sha256.Sum256(bytes) + return starlark.Bytes(string(hash[:])), nil + }) + + thread := &starlark.Thread{Name: "zephyr-contract", Load: disabledLoad} + thread.SetMaxExecutionSteps(request.FuelLimit) + globals, err := starlark.ExecFile(thread, "contract.star", string(request.Code), predeclared) + if err != nil { + if thread.ExecutionSteps() >= request.FuelLimit { + return Result{}, ErrFuelExhausted + } + return Result{}, fmt.Errorf("%w: %v", ErrScriptRuntime, err) + } + entry, ok := globals[request.Entrypoint] + if !ok { + return Result{}, fmt.Errorf("%w: missing entrypoint", ErrScriptRuntime) + } + callable, ok := entry.(starlark.Callable) + if !ok { + return Result{}, fmt.Errorf("%w: entrypoint is not callable", ErrScriptRuntime) + } + value, err := starlark.Call(thread, callable, starlark.Tuple{starlark.Bytes(string(request.Arguments))}, nil) + if err != nil { + if thread.ExecutionSteps() >= request.FuelLimit { + return Result{}, ErrFuelExhausted + } + return Result{}, fmt.Errorf("%w: %v", ErrScriptRuntime, err) + } + returned, err := valueBytes(value) + if value == starlark.None { + returned = nil + err = nil + } + if err != nil || len(returned) > MaxReturnBytes { + return Result{}, ErrInvalidResult + } + outWrites := make(map[types.ObjectID][]byte, len(writes)) + for id, value := range writes { + outWrites[id] = value + } + return Result{ReturnData: returned, FuelUsed: thread.ExecutionSteps(), Writes: outWrites, Events: events}, nil +} + +func emptyPredeclared() starlark.StringDict { return starlark.StringDict{} } + +func validationPredeclared() starlark.StringDict { + stub := func(_ *starlark.Thread, _ *starlark.Builtin, _ starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { + return starlark.None, nil + } + return starlark.StringDict{ + "state_read": starlark.NewBuiltin("state_read", stub), + "state_write": starlark.NewBuiltin("state_write", stub), + "emit": starlark.NewBuiltin("emit", stub), + "sha256": starlark.NewBuiltin("sha256", stub), + } +} + +func disabledLoad(_ *starlark.Thread, module string) (starlark.StringDict, error) { + return nil, fmt.Errorf("module loading disabled: %s", module) +} + +func parseObjectID(value string) (string, types.ObjectID, error) { + var id types.ObjectID + normalized := strings.ToLower(strings.TrimSpace(value)) + raw, err := hex.DecodeString(normalized) + if err != nil || len(raw) != len(id) { + return "", id, ErrUndeclaredAccess + } + copy(id[:], raw) + return normalized, id, nil +} + +func valueBytes(value starlark.Value) ([]byte, error) { + switch v := value.(type) { + case starlark.Bytes: + return []byte(string(v)), nil + case starlark.String: + return []byte(string(v)), nil + default: + return nil, ErrInvalidResult + } +} diff --git a/internal/v2/contracts/script_runtime_test.go b/internal/v2/contracts/script_runtime_test.go new file mode 100644 index 00000000..ffbb83bd --- /dev/null +++ b/internal/v2/contracts/script_runtime_test.go @@ -0,0 +1,80 @@ +package contracts + +import ( + "errors" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestScriptRuntimeDeterministicStateAndEvents(t *testing.T) { + contract := types.ContractID(types.HashBytes("contract", []byte("script"))) + stateID := types.ObjectID(types.HashBytes("state", []byte("one"))) + code := []byte(` +def run(arg): + before = state_read("` + stateID.String() + `") + state_write("` + stateID.String() + `", arg) + emit(sha256(arg)) + return before +`) + request := Request{ + ContractID: contract, Runtime: RuntimeZephyrScriptV1, Code: code, Entrypoint: "run", Arguments: []byte("new"), + Accesses: []Access{{ObjectID: stateID, Write: true}}, ReadValues: map[types.ObjectID][]byte{stateID: []byte("old")}, FuelLimit: 100_000, + } + runtime := MeteredRuntime{Inner: ScriptRuntime{}} + first, err := runtime.Execute(request) + if err != nil { + t.Fatal(err) + } + second, err := runtime.Execute(request) + if err != nil { + t.Fatal(err) + } + if string(first.ReturnData) != "old" || string(first.Writes[stateID]) != "new" || len(first.Events) != 1 { + t.Fatalf("unexpected result: %+v", first) + } + if first.FuelUsed != second.FuelUsed || string(first.ReturnData) != string(second.ReturnData) || string(first.Events[0]) != string(second.Events[0]) { + t.Fatal("same contract request produced nondeterministic result") + } +} + +func TestScriptRuntimeRejectsUndeclaredWrite(t *testing.T) { + contract := types.ContractID(types.HashBytes("contract", []byte("script-write"))) + stateID := types.ObjectID(types.HashBytes("state", []byte("write"))) + code := []byte(` +def run(arg): + state_write("` + stateID.String() + `", arg) + return arg +`) + _, err := (MeteredRuntime{Inner: ScriptRuntime{}}).Execute(Request{ + ContractID: contract, Runtime: RuntimeZephyrScriptV1, Code: code, Entrypoint: "run", Arguments: []byte("x"), + Accesses: []Access{{ObjectID: stateID, Write: false}}, FuelLimit: 100_000, + }) + if !errors.Is(err, ErrUndeclaredAccess) { + t.Fatalf("expected undeclared access error, got %v", err) + } +} + +func TestScriptRuntimeFuelExhaustion(t *testing.T) { + contract := types.ContractID(types.HashBytes("contract", []byte("script-fuel"))) + code := []byte(` +def run(arg): + x = 0 + for i in range(1000000): + x += i + return arg +`) + _, err := (MeteredRuntime{Inner: ScriptRuntime{}}).Execute(Request{ + ContractID: contract, Runtime: RuntimeZephyrScriptV1, Code: code, Entrypoint: "run", Arguments: []byte("x"), FuelLimit: 100, + }) + if !errors.Is(err, ErrFuelExhausted) { + t.Fatalf("expected fuel exhaustion, got %v", err) + } +} + +func TestScriptRuntimeDisablesLoad(t *testing.T) { + code := []byte("load(\"remote.star\", \"x\")\ndef run(arg):\n return arg\n") + if err := (ScriptRuntime{}).ValidateModule(code); err == nil { + t.Fatal("expected module load to be rejected") + } +} From 7554803beb54f5df84d7b1cba6ee43ceae87a50a Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:02:14 +0200 Subject: [PATCH 092/274] add authenticated Reed-Solomon data availability coding --- go.mod | 5 +- internal/v2/da/erasure.go | 117 +++++++++++++++++++++++++++++++++ internal/v2/da/erasure_test.go | 59 +++++++++++++++++ 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 internal/v2/da/erasure.go create mode 100644 internal/v2/da/erasure_test.go diff --git a/go.mod b/go.mod index 0c4f699e..0c122880 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/zephyr-chain/zephyr-chain go 1.26.0 -require go.starlark.net v0.0.0-20260708150628-5395d018f003 +require ( + github.com/klauspost/reedsolomon v1.14.1 + go.starlark.net v0.0.0-20260708150628-5395d018f003 +) diff --git a/internal/v2/da/erasure.go b/internal/v2/da/erasure.go new file mode 100644 index 00000000..a9815934 --- /dev/null +++ b/internal/v2/da/erasure.go @@ -0,0 +1,117 @@ +package da + +import ( + "bytes" + "errors" + + "github.com/klauspost/reedsolomon" +) + +const ( + MaxDataShards = 128 + MaxParityShards = 128 + MaxBlobBytes = 64 << 20 +) + +var ( + ErrInvalidErasureConfig = errors.New("invalid data-availability erasure configuration") + ErrReconstruction = errors.New("data-availability reconstruction failed") +) + +type ReedSolomonEncoder struct{} + +func (ReedSolomonEncoder) Encode(data []byte, dataShards, parityShards uint16) ([][]byte, error) { + if err := validateErasureConfig(len(data), dataShards, parityShards); err != nil { + return nil, err + } + encoder, err := reedsolomon.New(int(dataShards), int(parityShards)) + if err != nil { + return nil, ErrInvalidErasureConfig + } + shards, err := encoder.Split(data) + if err != nil { + return nil, ErrInvalidErasureConfig + } + if err := encoder.Encode(shards); err != nil { + return nil, ErrInvalidErasureConfig + } + return cloneChunks(shards), nil +} + +func EncodeBlob(data []byte, dataShards, parityShards uint16) (Commitment, [][]byte, []Sample, error) { + chunks, err := (ReedSolomonEncoder{}).Encode(data, dataShards, parityShards) + if err != nil { + return Commitment{}, nil, nil, err + } + commitment, samples, err := CommitChunks(chunks, dataShards, parityShards, uint64(len(data))) + if err != nil { + return Commitment{}, nil, nil, err + } + return commitment, chunks, samples, nil +} + +func ReconstructBlob(commitment Commitment, chunks [][]byte, samples []Sample) ([]byte, error) { + if err := validateCommitment(commitment); err != nil || len(chunks) != int(commitment.ChunkCount) || len(samples) != int(commitment.ChunkCount) { + return nil, ErrReconstruction + } + working := cloneChunks(chunks) + valid := 0 + for i := range working { + if working[i] == nil { + continue + } + if samples[i].Index != uint32(i) || !VerifySample(commitment, samples[i], working[i]) { + working[i] = nil + continue + } + valid++ + } + if valid < int(commitment.DataShards) { + return nil, ErrReconstruction + } + encoder, err := reedsolomon.New(int(commitment.DataShards), int(commitment.ParityShards)) + if err != nil { + return nil, ErrReconstruction + } + if err := encoder.Reconstruct(working); err != nil { + return nil, ErrReconstruction + } + for i := range working { + if !VerifySample(commitment, samples[i], working[i]) { + return nil, ErrReconstruction + } + } + var out bytes.Buffer + if err := encoder.Join(&out, working, int(commitment.OriginalSize)); err != nil { + return nil, ErrReconstruction + } + return out.Bytes(), nil +} + +func validateErasureConfig(size int, dataShards, parityShards uint16) error { + if size <= 0 || size > MaxBlobBytes || dataShards == 0 || parityShards == 0 || + dataShards > MaxDataShards || parityShards > MaxParityShards || int(dataShards)+int(parityShards) > 256 { + return ErrInvalidErasureConfig + } + return nil +} + +func validateCommitment(commitment Commitment) error { + if commitment.OriginalSize == 0 || commitment.OriginalSize > MaxBlobBytes || + commitment.DataShards == 0 || commitment.ParityShards == 0 || + commitment.DataShards > MaxDataShards || commitment.ParityShards > MaxParityShards || + uint32(commitment.DataShards)+uint32(commitment.ParityShards) != commitment.ChunkCount { + return ErrInvalidErasureConfig + } + return nil +} + +func cloneChunks(in [][]byte) [][]byte { + out := make([][]byte, len(in)) + for i, chunk := range in { + if chunk != nil { + out[i] = append([]byte(nil), chunk...) + } + } + return out +} diff --git a/internal/v2/da/erasure_test.go b/internal/v2/da/erasure_test.go new file mode 100644 index 00000000..73f86269 --- /dev/null +++ b/internal/v2/da/erasure_test.go @@ -0,0 +1,59 @@ +package da + +import ( + "bytes" + "testing" +) + +func TestErasureCodingReconstructsMissingAndCorruptChunks(t *testing.T) { + payload := bytes.Repeat([]byte("zephyr-data-availability/"), 1000) + commitment, chunks, samples, err := EncodeBlob(payload, 8, 4) + if err != nil { + t.Fatal(err) + } + if commitment.ChunkCount != 12 || commitment.OriginalSize != uint64(len(payload)) { + t.Fatalf("unexpected commitment: %+v", commitment) + } + working := cloneChunks(chunks) + working[1] = nil + working[5] = nil + working[10][0] ^= 0xff + recovered, err := ReconstructBlob(commitment, working, samples) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(recovered, payload) { + t.Fatal("reconstructed payload differs") + } +} + +func TestErasureCodingRejectsInsufficientAuthenticatedChunks(t *testing.T) { + payload := bytes.Repeat([]byte("z"), 8192) + commitment, chunks, samples, err := EncodeBlob(payload, 4, 2) + if err != nil { + t.Fatal(err) + } + working := cloneChunks(chunks) + working[0] = nil + working[1] = nil + working[2] = nil + if _, err := ReconstructBlob(commitment, working, samples); err != ErrReconstruction { + t.Fatalf("expected reconstruction failure, got %v", err) + } +} + +func TestCitizenSampleDetectsWithholdingAndTamper(t *testing.T) { + payload := []byte("citizen bounded sample") + commitment, chunks, samples, err := EncodeBlob(payload, 2, 2) + if err != nil { + t.Fatal(err) + } + if !VerifySample(commitment, samples[3], chunks[3]) { + t.Fatal("valid citizen sample rejected") + } + tampered := append([]byte(nil), chunks[3]...) + tampered[0] ^= 1 + if VerifySample(commitment, samples[3], tampered) { + t.Fatal("tampered citizen sample accepted") + } +} From a2bc27f255928f5f6b9c68e9f00b2bde71449010 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:04:10 +0200 Subject: [PATCH 093/274] add network-scoped libp2p QUIC transport for v2 --- go.mod | 1 + internal/v2/network/p2p/node.go | 206 +++++++++++++++++++++++++++ internal/v2/network/p2p/node_test.go | 47 ++++++ 3 files changed, 254 insertions(+) create mode 100644 internal/v2/network/p2p/node.go create mode 100644 internal/v2/network/p2p/node_test.go diff --git a/go.mod b/go.mod index 0c122880..06e2da2e 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ go 1.26.0 require ( github.com/klauspost/reedsolomon v1.14.1 + github.com/libp2p/go-libp2p v0.49.0 go.starlark.net v0.0.0-20260708150628-5395d018f003 ) diff --git a/internal/v2/network/p2p/node.go b/internal/v2/network/p2p/node.go new file mode 100644 index 00000000..a3c1ee7b --- /dev/null +++ b/internal/v2/network/p2p/node.go @@ -0,0 +1,206 @@ +package p2p + +import ( + "bufio" + "context" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "io" + "time" + + libp2p "github.com/libp2p/go-libp2p" + p2pcrypto "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + ma "github.com/multiformats/go-multiaddr" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + DefaultMaxMessageBytes = 4 << 20 + DefaultStreamTimeout = 10 * time.Second +) + +var ( + ErrConfig = errors.New("invalid Zephyr p2p configuration") + ErrFrameTooLarge = errors.New("Zephyr p2p frame exceeds limit") + ErrNoHandler = errors.New("Zephyr p2p protocol handler unavailable") +) + +type Handler func(context.Context, peer.ID, []byte) ([]byte, error) + +type Handlers struct { + Consensus Handler + Transaction Handler + LightProof Handler +} + +type Config struct { + Network types.NetworkID + Identity p2pcrypto.PrivKey + ListenAddrs []string + MaxMessageBytes uint32 + StreamTimeout time.Duration + Handlers Handlers +} + +type Node struct { + host host.Host + networkID types.NetworkID + maxMessage uint32 + timeout time.Duration + consensus protocol.ID + transaction protocol.ID + lightProof protocol.ID +} + +func New(cfg Config) (*Node, error) { + if types.IsZero32([32]byte(cfg.Network)) { + return nil, ErrConfig + } + identity := cfg.Identity + if identity == nil { + var err error + identity, _, err = p2pcrypto.GenerateEd25519Key(rand.Reader) + if err != nil { + return nil, err + } + } + if len(cfg.ListenAddrs) == 0 { + cfg.ListenAddrs = []string{"/ip4/0.0.0.0/udp/0/quic-v1", "/ip6/::/udp/0/quic-v1"} + } + if cfg.MaxMessageBytes == 0 { + cfg.MaxMessageBytes = DefaultMaxMessageBytes + } + if cfg.StreamTimeout <= 0 { + cfg.StreamTimeout = DefaultStreamTimeout + } + + h, err := libp2p.New(libp2p.Identity(identity), libp2p.ListenAddrStrings(cfg.ListenAddrs...)) + if err != nil { + return nil, err + } + prefix := fmt.Sprintf("/zephyr/%s/v2", cfg.Network.String()) + n := &Node{ + host: h, networkID: cfg.Network, maxMessage: cfg.MaxMessageBytes, timeout: cfg.StreamTimeout, + consensus: protocol.ID(prefix + "/consensus"), transaction: protocol.ID(prefix + "/tx"), lightProof: protocol.ID(prefix + "/light-proof"), + } + n.install(n.consensus, cfg.Handlers.Consensus) + n.install(n.transaction, cfg.Handlers.Transaction) + n.install(n.lightProof, cfg.Handlers.LightProof) + return n, nil +} + +func (n *Node) Close() error { return n.host.Close() } +func (n *Node) Host() host.Host { return n.host } +func (n *Node) ID() peer.ID { return n.host.ID() } +func (n *Node) Addrs() []ma.Multiaddr { return append([]ma.Multiaddr(nil), n.host.Addrs()...) } +func (n *Node) ConsensusProtocol() protocol.ID { return n.consensus } +func (n *Node) TransactionProtocol() protocol.ID { return n.transaction } +func (n *Node) LightProofProtocol() protocol.ID { return n.lightProof } + +func (n *Node) AddrInfo() peer.AddrInfo { + return peer.AddrInfo{ID: n.host.ID(), Addrs: n.Addrs()} +} +func (n *Node) Connect(ctx context.Context, remote peer.AddrInfo) error { + return n.host.Connect(ctx, remote) +} +func (n *Node) SendConsensus(ctx context.Context, remote peer.ID, payload []byte) ([]byte, error) { + return n.request(ctx, remote, n.consensus, payload) +} +func (n *Node) SendTransaction(ctx context.Context, remote peer.ID, payload []byte) ([]byte, error) { + return n.request(ctx, remote, n.transaction, payload) +} +func (n *Node) FetchLightProof(ctx context.Context, remote peer.ID, payload []byte) ([]byte, error) { + return n.request(ctx, remote, n.lightProof, payload) +} + +func (n *Node) install(id protocol.ID, handler Handler) { + n.host.SetStreamHandler(id, func(stream network.Stream) { + if handler == nil { + _ = stream.Reset() + return + } + defer stream.Close() + _ = stream.SetDeadline(time.Now().Add(n.timeout)) + payload, err := readFrame(stream, n.maxMessage) + if err != nil { + _ = stream.Reset() + return + } + ctx, cancel := context.WithTimeout(context.Background(), n.timeout) + defer cancel() + response, err := handler(ctx, stream.Conn().RemotePeer(), payload) + if err != nil { + _ = stream.Reset() + return + } + if response == nil { + response = []byte{} + } + if err := writeFrame(stream, response, n.maxMessage); err != nil { + _ = stream.Reset() + } + }) +} + +func (n *Node) request(ctx context.Context, remote peer.ID, id protocol.ID, payload []byte) ([]byte, error) { + if len(payload) > int(n.maxMessage) { + return nil, ErrFrameTooLarge + } + ctx, cancel := context.WithTimeout(ctx, n.timeout) + defer cancel() + stream, err := n.host.NewStream(ctx, remote, id) + if err != nil { + return nil, err + } + defer stream.Close() + _ = stream.SetDeadline(time.Now().Add(n.timeout)) + if err := writeFrame(stream, payload, n.maxMessage); err != nil { + _ = stream.Reset() + return nil, err + } + response, err := readFrame(stream, n.maxMessage) + if err != nil { + _ = stream.Reset() + return nil, err + } + return response, nil +} + +func writeFrame(w io.Writer, payload []byte, max uint32) error { + if len(payload) > int(max) { + return ErrFrameTooLarge + } + var length [4]byte + binary.BigEndian.PutUint32(length[:], uint32(len(payload))) + writer := bufio.NewWriter(w) + if _, err := writer.Write(length[:]); err != nil { + return err + } + if _, err := writer.Write(payload); err != nil { + return err + } + return writer.Flush() +} + +func readFrame(r io.Reader, max uint32) ([]byte, error) { + var length [4]byte + if _, err := io.ReadFull(r, length[:]); err != nil { + return nil, err + } + size := binary.BigEndian.Uint32(length[:]) + if size > max { + return nil, ErrFrameTooLarge + } + payload := make([]byte, int(size)) + if _, err := io.ReadFull(r, payload); err != nil { + return nil, err + } + return payload, nil +} diff --git a/internal/v2/network/p2p/node_test.go b/internal/v2/network/p2p/node_test.go new file mode 100644 index 00000000..daee1b81 --- /dev/null +++ b/internal/v2/network/p2p/node_test.go @@ -0,0 +1,47 @@ +package p2p + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestQUICNodesUseNetworkScopedProtocolsAndBoundedFrames(t *testing.T) { + networkID := types.NetworkID(types.HashBytes("network", []byte("p2p-test"))) + server, err := New(Config{ + Network: networkID, + ListenAddrs: []string{"/ip4/127.0.0.1/udp/0/quic-v1"}, + Handlers: Handlers{Transaction: func(_ context.Context, _ peer.ID, payload []byte) ([]byte, error) { + return append([]byte("ok:"), payload...), nil + }}, + }) + if err != nil { + t.Fatal(err) + } + defer server.Close() + client, err := New(Config{Network: networkID, ListenAddrs: []string{"/ip4/127.0.0.1/udp/0/quic-v1"}}) + if err != nil { + t.Fatal(err) + } + defer client.Close() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := client.Connect(ctx, server.AddrInfo()); err != nil { + t.Fatal(err) + } + got, err := client.SendTransaction(ctx, server.ID(), []byte("tx")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, []byte("ok:tx")) { + t.Fatalf("unexpected response %q", got) + } + if client.TransactionProtocol() == client.ConsensusProtocol() { + t.Fatal("protocol roles not separated") + } +} From 699289b8beb016ad85c7f0bc17def629d13dcb6f Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:13:38 +0200 Subject: [PATCH 094/274] lock Zephyr v2 Go dependencies --- go.sum | 212 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 go.sum diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..7ba10513 --- /dev/null +++ b/go.sum @@ -0,0 +1,212 @@ +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/keygen v1.0.0 h1:u0/Fhxlgz3uPv+XxhfgTq3BJt5VesIPM5ue/OuG7qjQ= +filippo.io/keygen v1.0.0/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2bWWT9wwuY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= +github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= +github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= +github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/klauspost/reedsolomon v1.14.1 h1:swE9kzyWXD/wVG+l5Pe8bWnQ0giIY7D1GjCBKk3kG2U= +github.com/klauspost/reedsolomon v1.14.1/go.mod h1:yjqqjgMTQkBUHSG97/rm4zipffCNbCiZcB3kTqr++sQ= +github.com/koron/go-ssdp v0.9.1 h1:zvxbAAuJftJIZ8Jh8mda+LI7V92hYZf/sKprmOxpxwA= +github.com/koron/go-ssdp v0.9.1/go.mod h1:C43c047jWkDaeg9YuZlSh/QGqOieuWV6dbhWi/jcaLk= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= +github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= +github.com/libp2p/go-libp2p v0.49.0 h1:ibXuYPIHmMIPShob1BktQvSuFQkq/MemhQOLKfGujjw= +github.com/libp2p/go-libp2p v0.49.0/go.mod h1:lzjVcOBk5fCn1QD2XbSOKLZesB6gEsry8SLjCsAAGT4= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-pubsub v0.17.0 h1:SNdvB6V0eYMXLRR95n+4vpxJKbFsbHhgjPdDiTpGoo0= +github.com/libp2p/go-libp2p-pubsub v0.17.0/go.mod h1:F0oKCGLFJNy9b0TyRi04b+LchEzq0t2eZyJuxwAIyDE= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE= +github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr-dns v0.6.0 h1:yKIW08WJHSPJ8bDAT2O/5fypCaUu9Bjl8r/1eJ4XAW8= +github.com/multiformats/go-multiaddr-dns v0.6.0/go.mod h1:dwIQwdORZfnNQCeS7xLXyn+7626oRmMsVP30Uronhf0= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= +github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= +github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= +github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= +github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= +github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= +github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= +github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= +github.com/quic-go/webtransport-go v0.11.1 h1:rrFQMO+7/52ZDJ04fsrjIaWqn6q1z1MYo9iVFq6JtbA= +github.com/quic-go/webtransport-go v0.11.1/go.mod h1:SHgEzUFVyj+9WUSuGB1P6Zd351Pww2leWV3SwlTovkA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +go.starlark.net v0.0.0-20260708150628-5395d018f003 h1:cAxcqHgW8fnmT0cEBU3TzvVYHIFt8IIGDMWUF6rImk4= +go.starlark.net v0.0.0-20260708150628-5395d018f003/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A= +golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260717140457-bdb89881bb75 h1:I9ygRooEYoVHV0SRNOSr/KVjTf5EeJ52BuNkVjsP2GU= +golang.org/x/telemetry v0.0.0-20260717140457-bdb89881bb75/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njk3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= From 32dc1898f7cbe8d3b3dec1d195b23d43658802f5 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:14:53 +0200 Subject: [PATCH 095/274] remove hand-materialized dependency checksum lock --- go.sum | 212 --------------------------------------------------------- 1 file changed, 212 deletions(-) delete mode 100644 go.sum diff --git a/go.sum b/go.sum deleted file mode 100644 index 7ba10513..00000000 --- a/go.sum +++ /dev/null @@ -1,212 +0,0 @@ -filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= -filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= -filippo.io/keygen v1.0.0 h1:u0/Fhxlgz3uPv+XxhfgTq3BJt5VesIPM5ue/OuG7qjQ= -filippo.io/keygen v1.0.0/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= -github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= -github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2bWWT9wwuY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= -github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= -github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= -github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= -github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= -github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= -github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= -github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= -github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= -github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= -github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= -github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= -github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= -github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= -github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= -github.com/klauspost/reedsolomon v1.14.1 h1:swE9kzyWXD/wVG+l5Pe8bWnQ0giIY7D1GjCBKk3kG2U= -github.com/klauspost/reedsolomon v1.14.1/go.mod h1:yjqqjgMTQkBUHSG97/rm4zipffCNbCiZcB3kTqr++sQ= -github.com/koron/go-ssdp v0.9.1 h1:zvxbAAuJftJIZ8Jh8mda+LI7V92hYZf/sKprmOxpxwA= -github.com/koron/go-ssdp v0.9.1/go.mod h1:C43c047jWkDaeg9YuZlSh/QGqOieuWV6dbhWi/jcaLk= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= -github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= -github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= -github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= -github.com/libp2p/go-libp2p v0.49.0 h1:ibXuYPIHmMIPShob1BktQvSuFQkq/MemhQOLKfGujjw= -github.com/libp2p/go-libp2p v0.49.0/go.mod h1:lzjVcOBk5fCn1QD2XbSOKLZesB6gEsry8SLjCsAAGT4= -github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= -github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-pubsub v0.17.0 h1:SNdvB6V0eYMXLRR95n+4vpxJKbFsbHhgjPdDiTpGoo0= -github.com/libp2p/go-libp2p-pubsub v0.17.0/go.mod h1:F0oKCGLFJNy9b0TyRi04b+LchEzq0t2eZyJuxwAIyDE= -github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= -github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= -github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= -github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= -github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= -github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= -github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE= -github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o= -github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= -github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= -github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= -github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= -github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= -github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= -github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= -github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= -github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= -github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= -github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= -github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= -github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= -github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= -github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= -github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= -github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= -github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= -github.com/multiformats/go-multiaddr-dns v0.6.0 h1:yKIW08WJHSPJ8bDAT2O/5fypCaUu9Bjl8r/1eJ4XAW8= -github.com/multiformats/go-multiaddr-dns v0.6.0/go.mod h1:dwIQwdORZfnNQCeS7xLXyn+7626oRmMsVP30Uronhf0= -github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= -github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= -github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= -github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= -github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= -github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= -github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= -github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= -github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= -github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= -github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= -github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= -github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= -github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= -github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= -github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= -github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= -github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= -github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= -github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= -github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= -github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= -github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= -github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= -github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= -github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= -github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= -github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= -github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= -github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= -github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= -github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= -github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= -github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= -github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= -github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= -github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= -github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= -github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= -github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= -github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= -github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= -github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= -github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= -github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= -github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= -github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= -github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= -github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= -github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= -github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= -github.com/quic-go/webtransport-go v0.11.1 h1:rrFQMO+7/52ZDJ04fsrjIaWqn6q1z1MYo9iVFq6JtbA= -github.com/quic-go/webtransport-go v0.11.1/go.mod h1:SHgEzUFVyj+9WUSuGB1P6Zd351Pww2leWV3SwlTovkA= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= -github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= -github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= -github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -go.starlark.net v0.0.0-20260708150628-5395d018f003 h1:cAxcqHgW8fnmT0cEBU3TzvVYHIFt8IIGDMWUF6rImk4= -go.starlark.net v0.0.0-20260708150628-5395d018f003/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo= -go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= -go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= -go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= -go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= -go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= -go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A= -golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260717140457-bdb89881bb75 h1:I9ygRooEYoVHV0SRNOSr/KVjTf5EeJ52BuNkVjsP2GU= -golang.org/x/telemetry v0.0.0-20260717140457-bdb89881bb75/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njk3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= -lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= From d8ca7bc7dbd8960328cb987085534dd2680714e1 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:15:18 +0200 Subject: [PATCH 096/274] temporarily automate exact v2 Go dependency lock --- .github/workflows/v2-lock-write.yml | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/v2-lock-write.yml diff --git a/.github/workflows/v2-lock-write.yml b/.github/workflows/v2-lock-write.yml new file mode 100644 index 00000000..e1372028 --- /dev/null +++ b/.github/workflows/v2-lock-write.yml @@ -0,0 +1,47 @@ +name: V2 Exact Dependency Lock + +on: + push: + branches: + - chatgpt/protocol-v2-foundation + paths: + - go.mod + - internal/v2/contracts/** + - internal/v2/da/** + - internal/v2/network/** + - .github/workflows/v2-lock-write.yml + +permissions: + contents: write + +jobs: + lock: + if: github.repository == 'the-code-learner/Zephyr-Chain' && github.ref == 'refs/heads/chatgpt/protocol-v2-foundation' + runs-on: ubuntu-latest + steps: + - name: Checkout v2 branch + uses: actions/checkout@v6 + with: + ref: chatgpt/protocol-v2-foundation + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: false + + - name: Generate exact module lock + run: go mod tidy + + - name: Commit generated lock if needed + run: | + if git diff --quiet -- go.mod go.sum; then + echo 'Dependency lock already exact.' + exit 0 + fi + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add go.mod go.sum + git commit -m 'lock exact Zephyr v2 Go dependency graph' + git push origin HEAD:chatgpt/protocol-v2-foundation From 77e98dbea0558376a7f1f3ed454d4d401de0682e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:15:43 +0000 Subject: [PATCH 097/274] lock exact Zephyr v2 Go dependency graph --- go.mod | 88 +++++++++++++++++++++ go.sum | 238 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 go.sum diff --git a/go.mod b/go.mod index 06e2da2e..53b2d00c 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,93 @@ go 1.26.0 require ( github.com/klauspost/reedsolomon v1.14.1 github.com/libp2p/go-libp2p v0.49.0 + github.com/multiformats/go-multiaddr v0.16.1 go.starlark.net v0.0.0-20260708150628-5395d018f003 ) + +require ( + filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 // indirect + filippo.io/keygen v1.0.0 // indirect + github.com/benbjohnson/clock v1.3.5 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/dunglas/httpsfv v1.1.0 // indirect + github.com/filecoin-project/go-clock v0.1.0 // indirect + github.com/flynn/noise v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/ipfs/go-cid v0.6.2 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/koron/go-ssdp v0.9.1 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.3.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-netroute v0.4.0 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/libp2p/go-yamux/v5 v5.1.0 // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multiaddr-dns v0.6.0 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.3.0 // indirect + github.com/multiformats/go-multicodec v0.10.0 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v3 v3.1.2 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.16 // indirect + github.com/pion/rtp v1.8.19 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.18 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect + github.com/pion/stun/v3 v3.1.1 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/transport/v4 v4.0.1 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect + github.com/pion/webrtc/v4 v4.1.2 // indirect + github.com/prometheus/client_golang v1.24.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.70.1 // indirect + github.com/prometheus/procfs v0.21.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.60.0 // indirect + github.com/quic-go/webtransport-go v0.11.1 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect + go.uber.org/mock v0.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/exp v0.0.0-20260718201538-764159d718ef // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/telemetry v0.0.0-20260717140457-bdb89881bb75 // indirect + golang.org/x/text v0.40.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.48.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + lukechampine.com/blake3 v1.4.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..1ff74297 --- /dev/null +++ b/go.sum @@ -0,0 +1,238 @@ +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/keygen v1.0.0 h1:u0/Fhxlgz3uPv+XxhfgTq3BJt5VesIPM5ue/OuG7qjQ= +filippo.io/keygen v1.0.0/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= +github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= +github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= +github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/klauspost/reedsolomon v1.14.1 h1:swE9kzyWXD/wVG+l5Pe8bWnQ0giIY7D1GjCBKk3kG2U= +github.com/klauspost/reedsolomon v1.14.1/go.mod h1:yjqqjgMTQkBUHSG97/rm4zipffCNbCiZcB3kTqr++sQ= +github.com/koron/go-ssdp v0.9.1 h1:zvxbAAuJftJIZ8Jh8mda+LI7V92hYZf/sKprmOxpxwA= +github.com/koron/go-ssdp v0.9.1/go.mod h1:C43c047jWkDaeg9YuZlSh/QGqOieuWV6dbhWi/jcaLk= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= +github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= +github.com/libp2p/go-libp2p v0.49.0 h1:ibXuYPIHmMIPShob1BktQvSuFQkq/MemhQOLKfGujjw= +github.com/libp2p/go-libp2p v0.49.0/go.mod h1:lzjVcOBk5fCn1QD2XbSOKLZesB6gEsry8SLjCsAAGT4= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE= +github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o= +github.com/marcopolo/simnet v0.0.7 h1:DpH8BMGsF9+1w13L8rvCaAhb6nYJdY+dIXncDrssvUs= +github.com/marcopolo/simnet v0.0.7/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr-dns v0.6.0 h1:yKIW08WJHSPJ8bDAT2O/5fypCaUu9Bjl8r/1eJ4XAW8= +github.com/multiformats/go-multiaddr-dns v0.6.0/go.mod h1:dwIQwdORZfnNQCeS7xLXyn+7626oRmMsVP30Uronhf0= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= +github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= +github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= +github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= +github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= +github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= +github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= +github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= +github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= +github.com/quic-go/webtransport-go v0.11.1 h1:rrFQMO+7/52ZDJ04fsrjIaWqn6q1z1MYo9iVFq6JtbA= +github.com/quic-go/webtransport-go v0.11.1/go.mod h1:SHgEzUFVyj+9WUSuGB1P6Zd351Pww2leWV3SwlTovkA= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +go.starlark.net v0.0.0-20260708150628-5395d018f003 h1:cAxcqHgW8fnmT0cEBU3TzvVYHIFt8IIGDMWUF6rImk4= +go.starlark.net v0.0.0-20260708150628-5395d018f003/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A= +golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260717140457-bdb89881bb75 h1:I9ygRooEYoVHV0SRNOSr/KVjTf5EeJ52BuNkVjsP2GU= +golang.org/x/telemetry v0.0.0-20260717140457-bdb89881bb75/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= From ae305ae7c6d03c1e98f75156985840ca4781bfde Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:20:42 +0200 Subject: [PATCH 098/274] integrate metered contract deploy and call into v2 execution --- internal/v2/contracts/ops.go | 178 +++++++++++++++++ internal/v2/contracts/script_runtime.go | 15 +- .../v2/execution/contract_execution_test.go | 118 +++++++++++ internal/v2/execution/engine.go | 11 +- internal/v2/execution/extended_contract.go | 184 ++++++++++++++++++ 5 files changed, 489 insertions(+), 17 deletions(-) create mode 100644 internal/v2/contracts/ops.go create mode 100644 internal/v2/execution/contract_execution_test.go create mode 100644 internal/v2/execution/extended_contract.go diff --git a/internal/v2/contracts/ops.go b/internal/v2/contracts/ops.go new file mode 100644 index 00000000..fff94a01 --- /dev/null +++ b/internal/v2/contracts/ops.go @@ -0,0 +1,178 @@ +package contracts + +import ( + "errors" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrContractWire = errors.New("invalid contract operation wire payload") + +type StoredContract struct { + ID types.ContractID + Deployment Deployment +} + +func ParseDeployment(data []byte) (Deployment, error) { + r := codec.NewReader(data) + runtimeName, err := r.String(64) + if err != nil { + return Deployment{}, ErrContractWire + } + code, err := r.Bytes(MaxModuleBytes) + if err != nil { + return Deployment{}, ErrContractWire + } + abi, err := r.U16() + if err != nil { + return Deployment{}, ErrContractWire + } + authorityRaw, err := r.Fixed(32) + if err != nil { + return Deployment{}, ErrContractWire + } + var authority types.AccountID + copy(authority[:], authorityRaw) + initial, err := r.Bytes(MaxInitialStateBytes) + if err != nil { + return Deployment{}, ErrContractWire + } + pages, err := r.U32() + if err != nil || r.Done() != nil { + return Deployment{}, ErrContractWire + } + deployment := Deployment{Runtime: runtimeName, Code: code, ABI: abi, UpgradeAuthority: authority, InitialState: initial, MaxMemoryPages: pages} + if err := deployment.Validate(); err != nil { + return Deployment{}, err + } + return deployment, nil +} + +func (s StoredContract) MarshalBinary() ([]byte, error) { + if types.IsZero32([32]byte(s.ID)) { + return nil, ErrContractWire + } + deployment, err := s.Deployment.MarshalBinary() + if err != nil { + return nil, err + } + var w codec.Writer + w.Fixed(s.ID[:]) + w.Bytes(deployment) + return w.BytesCopy(), nil +} + +func ParseStoredContract(data []byte) (StoredContract, error) { + r := codec.NewReader(data) + idRaw, err := r.Fixed(32) + if err != nil { + return StoredContract{}, ErrContractWire + } + var id types.ContractID + copy(id[:], idRaw) + deploymentRaw, err := r.Bytes(MaxModuleBytes + MaxInitialStateBytes + 256) + if err != nil || r.Done() != nil { + return StoredContract{}, ErrContractWire + } + deployment, err := ParseDeployment(deploymentRaw) + if err != nil || types.IsZero32([32]byte(id)) { + return StoredContract{}, ErrContractWire + } + return StoredContract{ID: id, Deployment: deployment}, nil +} + +type Call struct { + ContractObject types.ObjectID + Entrypoint string + Arguments []byte + FuelLimit uint64 + Accesses []Access +} + +func (c Call) MarshalBinary() ([]byte, error) { + if types.IsZero32([32]byte(c.ContractObject)) || c.Entrypoint == "" || len(c.Entrypoint) > 128 || len(c.Arguments) > MaxArgumentsBytes || c.FuelLimit == 0 || len(c.Accesses) > MaxAccesses { + return nil, ErrContractWire + } + var w codec.Writer + w.Fixed(c.ContractObject[:]) + w.String(c.Entrypoint) + w.Bytes(c.Arguments) + w.U64(c.FuelLimit) + w.U32(uint32(len(c.Accesses))) + seen := make(map[types.ObjectID]struct{}, len(c.Accesses)) + for _, access := range c.Accesses { + if types.IsZero32([32]byte(access.ObjectID)) { + return nil, ErrContractWire + } + if _, ok := seen[access.ObjectID]; ok { + return nil, ErrContractWire + } + seen[access.ObjectID] = struct{}{} + w.Fixed(access.ObjectID[:]) + w.Bool(access.Write) + } + return w.BytesCopy(), nil +} + +func ParseCall(data []byte) (Call, error) { + r := codec.NewReader(data) + contractRaw, err := r.Fixed(32) + if err != nil { + return Call{}, ErrContractWire + } + var contractObject types.ObjectID + copy(contractObject[:], contractRaw) + entry, err := r.String(128) + if err != nil { + return Call{}, ErrContractWire + } + args, err := r.Bytes(MaxArgumentsBytes) + if err != nil { + return Call{}, ErrContractWire + } + fuel, err := r.U64() + if err != nil { + return Call{}, ErrContractWire + } + count, err := r.U32() + if err != nil || count > MaxAccesses { + return Call{}, ErrContractWire + } + accesses := make([]Access, int(count)) + for i := range accesses { + raw, err := r.Fixed(32) + if err != nil { + return Call{}, ErrContractWire + } + copy(accesses[i].ObjectID[:], raw) + accesses[i].Write, err = r.Bool() + if err != nil { + return Call{}, ErrContractWire + } + } + if r.Done() != nil { + return Call{}, ErrContractWire + } + call := Call{ContractObject: contractObject, Entrypoint: entry, Arguments: args, FuelLimit: fuel, Accesses: accesses} + if _, err := call.MarshalBinary(); err != nil { + return Call{}, err + } + return call, nil +} + +type Receipt struct { + ContractID types.ContractID + FuelUsed uint64 + ReturnHash types.Hash + EventRoot types.Hash +} + +func (r Receipt) MarshalBinary() []byte { + var w codec.Writer + w.Fixed(r.ContractID[:]) + w.U64(r.FuelUsed) + w.Fixed(r.ReturnHash[:]) + w.Fixed(r.EventRoot[:]) + return w.BytesCopy() +} diff --git a/internal/v2/contracts/script_runtime.go b/internal/v2/contracts/script_runtime.go index 0657c41d..bae1c67b 100644 --- a/internal/v2/contracts/script_runtime.go +++ b/internal/v2/contracts/script_runtime.go @@ -13,10 +13,6 @@ import ( var ErrScriptRuntime = errors.New("zephyr script execution failed") -// ScriptRuntime is Zephyr's deterministic reference smart-contract runtime. -// It exposes no clock, randomness, filesystem, network or dynamic module load. -// Execution is bounded by Starlark's abstract step counter, which is used as -// deterministic fuel for consensus. type ScriptRuntime struct{} func (ScriptRuntime) ValidateModule(code []byte) error { @@ -41,7 +37,6 @@ func (ScriptRuntime) Execute(request Request) (Result, error) { } writes := make(map[types.ObjectID][]byte) events := make([][]byte, 0) - predeclared := emptyPredeclared() predeclared["state_read"] = starlark.NewBuiltin("state_read", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var id string @@ -105,7 +100,6 @@ func (ScriptRuntime) Execute(request Request) (Result, error) { hash := sha256.Sum256(bytes) return starlark.Bytes(string(hash[:])), nil }) - thread := &starlark.Thread{Name: "zephyr-contract", Load: disabledLoad} thread.SetMaxExecutionSteps(request.FuelLimit) globals, err := starlark.ExecFile(thread, "contract.star", string(request.Code), predeclared) @@ -123,7 +117,7 @@ func (ScriptRuntime) Execute(request Request) (Result, error) { if !ok { return Result{}, fmt.Errorf("%w: entrypoint is not callable", ErrScriptRuntime) } - value, err := starlark.Call(thread, callable, starlark.Tuple{starlark.Bytes(string(request.Arguments))}, nil) + value, err := starlark.Call(thread, callable, starlark.Tuple{starlark.String(string(request.Arguments))}, nil) if err != nil { if thread.ExecutionSteps() >= request.FuelLimit { return Result{}, ErrFuelExhausted @@ -151,12 +145,7 @@ func validationPredeclared() starlark.StringDict { stub := func(_ *starlark.Thread, _ *starlark.Builtin, _ starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { return starlark.None, nil } - return starlark.StringDict{ - "state_read": starlark.NewBuiltin("state_read", stub), - "state_write": starlark.NewBuiltin("state_write", stub), - "emit": starlark.NewBuiltin("emit", stub), - "sha256": starlark.NewBuiltin("sha256", stub), - } + return starlark.StringDict{"state_read": starlark.NewBuiltin("state_read", stub), "state_write": starlark.NewBuiltin("state_write", stub), "emit": starlark.NewBuiltin("emit", stub), "sha256": starlark.NewBuiltin("sha256", stub)} } func disabledLoad(_ *starlark.Thread, module string) (starlark.StringDict, error) { diff --git a/internal/v2/execution/contract_execution_test.go b/internal/v2/execution/contract_execution_test.go new file mode 100644 index 00000000..d00bf89e --- /dev/null +++ b/internal/v2/execution/contract_execution_test.go @@ -0,0 +1,118 @@ +package execution + +import ( + "crypto/elliptic" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/contracts" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestDeployAndCallZephyrScriptAreStateCommitted(t *testing.T) { + key := makeKey(t) + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + owner := types.AccountIDFromPublicKey(pub) + networkID := types.NetworkID(types.HashBytes("network", []byte("contract-execution"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + store := worldstate.NewMemory() + feeID := types.ObjectIDFromTransaction(types.HashBytes("seed", []byte("contract-fee")), 0) + feeSpec, _ := object.NewCoinOutput(owner, native, 10) + feeObject := object.Object{ID: feeID, Version: 1, Owner: owner, Kind: object.KindCoin, Data: feeSpec.Data} + root, err := store.Apply(nil, []object.Object{feeObject}) + if err != nil { + t.Fatal(err) + } + feeWitness, feeProof, _ := store.Proof(feeID) + feeHash := feeWitness.Hash() + change, _ := object.NewCoinOutput(owner, native, 9) + code := []byte("def run(id):\n before = state_read(id)\n state_write(id, \"new\")\n emit(sha256(\"event\"))\n return before\n") + deployment := contracts.Deployment{Runtime: contracts.RuntimeZephyrScriptV1, Code: code, ABI: 1, UpgradeAuthority: owner, InitialState: []byte("old"), MaxMemoryPages: 16} + payload, err := deployment.MarshalBinary() + if err != nil { + t.Fatal(err) + } + deploy := tx.Transaction{Version: tx.Version, Network: networkID, ShardID: 0, StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: feeID, Version: 1, ObjectHash: feeHash}}, Outputs: []object.OutputSpec{change}, + Operations: []tx.Operation{{Kind: tx.OpDeployContract, Payload: payload}}, Fee: 1, + Witnesses: []tx.Witness{{Object: feeWitness, Proof: feeProof}}, ValidUntilHeight: 20} + deploy.Salt[0] = 41 + if err := deploy.Sign(key); err != nil { + t.Fatal(err) + } + engine := Engine{Network: networkID, NativeToken: native, ShardCount: 1} + deployResult, err := engine.Execute(deploy) + if err != nil { + t.Fatal(err) + } + if len(deployResult.Created) != 3 { + t.Fatalf("expected change+contract+state, got %d", len(deployResult.Created)) + } + if _, err := store.Apply(deployResult.Consumed, deployResult.Created); err != nil { + t.Fatal(err) + } + var contractObject, stateObject, callFee object.Object + for _, item := range deployResult.Created { + switch item.Kind { + case object.KindContract: + contractObject = item + case object.KindContractState: + stateObject = item + case object.KindCoin: + callFee = item + } + } + if contractObject.ID == (types.ObjectID{}) || stateObject.ID == (types.ObjectID{}) { + t.Fatal("deployment objects missing") + } + root = store.Root() + objects := []object.Object{callFee, contractObject, stateObject} + inputs := make([]tx.InputRef, 0, len(objects)) + witnesses := make([]tx.Witness, 0, len(objects)) + for _, item := range objects { + proved, proof, ok := store.Proof(item.ID) + if !ok { + t.Fatal("missing call input proof") + } + h := proved.Hash() + inputs = append(inputs, tx.InputRef{ObjectID: item.ID, Version: item.Version, ObjectHash: h}) + witnesses = append(witnesses, tx.Witness{Object: proved, Proof: proof}) + } + callPayload, err := (contracts.Call{ContractObject: contractObject.ID, Entrypoint: "run", Arguments: []byte(stateObject.ID.String()), FuelLimit: 100_000, Accesses: []contracts.Access{{ObjectID: stateObject.ID, Write: true}}}).MarshalBinary() + if err != nil { + t.Fatal(err) + } + change2, _ := object.NewCoinOutput(owner, native, 8) + call := tx.Transaction{Version: tx.Version, Network: networkID, ShardID: 0, StateRoot: root, + Inputs: inputs, Outputs: []object.OutputSpec{change2}, Operations: []tx.Operation{{Kind: tx.OpContractCall, Payload: callPayload}}, Fee: 1, + Witnesses: witnesses, ValidUntilHeight: 30} + call.Salt[0] = 42 + if err := call.Sign(key); err != nil { + t.Fatal(err) + } + callResult, err := engine.Execute(call) + if err != nil { + t.Fatal(err) + } + if _, err := store.Apply(callResult.Consumed, callResult.Created); err != nil { + t.Fatal(err) + } + updated, _, ok := store.Proof(stateObject.ID) + if !ok || updated.Version != 2 || string(updated.Data) != "new" { + t.Fatalf("contract state not committed: %+v", updated) + } + if _, _, ok := store.Proof(contractObject.ID); !ok { + t.Fatal("read-only contract object was consumed") + } + foundReceipt := false + for _, item := range callResult.Created { + if item.Kind == object.KindSystem { + foundReceipt = true + } + } + if !foundReceipt { + t.Fatal("contract execution receipt not committed") + } +} diff --git a/internal/v2/execution/engine.go b/internal/v2/execution/engine.go index e0b83c66..703bd01b 100644 --- a/internal/v2/execution/engine.go +++ b/internal/v2/execution/engine.go @@ -5,6 +5,7 @@ import ( "math" "github.com/zephyr-chain/zephyr-chain/internal/v2/assets" + "github.com/zephyr-chain/zephyr-chain/internal/v2/contracts" "github.com/zephyr-chain/zephyr-chain/internal/v2/object" "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" @@ -33,9 +34,11 @@ type Result struct { } type Engine struct { - Network types.NetworkID - NativeToken types.TokenID - ShardCount uint32 + Network types.NetworkID + NativeToken types.TokenID + ShardCount uint32 + Height uint64 + ContractRuntimes map[string]contracts.MeteredRuntime } func (e Engine) Execute(t tx.Transaction) (Result, error) { @@ -68,7 +71,7 @@ func (e Engine) Execute(t tx.Transaction) (Result, error) { case tx.OpCreateToken: return e.executeCreateToken(t, t.Operations[0].Payload) default: - return Result{}, ErrUnsupportedOperation + return e.executeExtended(t, t.Operations[0]) } } diff --git a/internal/v2/execution/extended_contract.go b/internal/v2/execution/extended_contract.go new file mode 100644 index 00000000..7b25c258 --- /dev/null +++ b/internal/v2/execution/extended_contract.go @@ -0,0 +1,184 @@ +package execution + +import ( + "math" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/contracts" + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + contractObjectIndex uint32 = 0xA0000000 + contractStateIndex uint32 = 0xA0000001 + contractReceiptIndex uint32 = 0xA0000010 +) + +func (e Engine) executeExtended(t tx.Transaction, op tx.Operation) (Result, error) { + switch op.Kind { + case tx.OpDeployContract: + return e.executeDeployContract(t, op.Payload) + case tx.OpContractCall: + return e.executeContractCall(t, op.Payload) + default: + return Result{}, ErrUnsupportedOperation + } +} + +func (e Engine) contractRuntime(name string) (contracts.MeteredRuntime, bool) { + if e.ContractRuntimes != nil { + if runtime, ok := e.ContractRuntimes[name]; ok && runtime.Inner != nil { + return runtime, true + } + } + if name == contracts.RuntimeZephyrScriptV1 { + return contracts.MeteredRuntime{Inner: contracts.ScriptRuntime{}}, true + } + return contracts.MeteredRuntime{}, false +} + +func (e Engine) executeDeployContract(t tx.Transaction, payload []byte) (Result, error) { + deployment, err := contracts.ParseDeployment(payload) + if err != nil || deployment.UpgradeAuthority != t.Sender { + return Result{}, ErrOwnership + } + runtime, ok := e.contractRuntime(deployment.Runtime) + if !ok { + return Result{}, ErrUnsupportedOperation + } + if err := runtime.ValidateModule(deployment.Code); err != nil { + return Result{}, err + } + created, consumed, err := e.feeOnlyOutputs(t) + if err != nil { + return Result{}, err + } + txID := t.ID() + contractID := types.ContractIDFromTransaction(txID, 0) + storedRaw, err := (contracts.StoredContract{ID: contractID, Deployment: deployment}).MarshalBinary() + if err != nil { + return Result{}, err + } + created = append(created, object.Object{ID: types.ObjectIDForShard(txID, contractObjectIndex, t.ShardID), Version: 1, Owner: t.Sender, Kind: object.KindContract, Data: storedRaw}) + if len(deployment.InitialState) > 0 { + created = append(created, object.Object{ID: types.ObjectIDForShard(txID, contractStateIndex, t.ShardID), Version: 1, Owner: t.Sender, Kind: object.KindContractState, Data: append([]byte(nil), deployment.InitialState...)}) + } + return Result{Consumed: consumed, Created: created, TxID: txID}, nil +} + +func (e Engine) executeContractCall(t tx.Transaction, payload []byte) (Result, error) { + call, err := contracts.ParseCall(payload) + if err != nil { + return Result{}, err + } + witnesses := make(map[types.ObjectID]tx.Witness, len(t.Witnesses)) + for _, witness := range t.Witnesses { + witnesses[witness.Object.ID] = witness + } + contractWitness, ok := witnesses[call.ContractObject] + if !ok || contractWitness.Object.Kind != object.KindContract { + return Result{}, ErrOwnership + } + stored, err := contracts.ParseStoredContract(contractWitness.Object.Data) + if err != nil { + return Result{}, err + } + runtime, ok := e.contractRuntime(stored.Deployment.Runtime) + if !ok { + return Result{}, ErrUnsupportedOperation + } + readValues := make(map[types.ObjectID][]byte, len(call.Accesses)) + for _, access := range call.Accesses { + witness, ok := witnesses[access.ObjectID] + if !ok || witness.Object.Kind != object.KindContractState { + return Result{}, ErrOwnership + } + readValues[access.ObjectID] = append([]byte(nil), witness.Object.Data...) + } + result, err := runtime.Execute(contracts.Request{ContractID: stored.ID, Runtime: stored.Deployment.Runtime, Code: stored.Deployment.Code, Entrypoint: call.Entrypoint, Arguments: call.Arguments, Accesses: call.Accesses, ReadValues: readValues, FuelLimit: call.FuelLimit}) + if err != nil { + return Result{}, err + } + created, consumed, err := e.feeOnlyOutputsExcluding(t, call.ContractObject, call.Accesses) + if err != nil { + return Result{}, err + } + for id, value := range result.Writes { + witness := witnesses[id] + consumed = append(consumed, id) + updated := witness.Object + updated.Version++ + updated.Data = append([]byte(nil), value...) + created = append(created, updated) + } + eventLeaves := make([]types.Hash, len(result.Events)) + for i, event := range result.Events { + eventLeaves[i] = merkle.Leaf("contract-event", event) + } + receipt := contracts.Receipt{ContractID: stored.ID, FuelUsed: result.FuelUsed, ReturnHash: types.Hash(codec.DomainHash("zephyr/contract-return/v2", result.ReturnData)), EventRoot: merkle.Root(eventLeaves)} + txID := t.ID() + created = append(created, object.Object{ID: types.ObjectIDForShard(txID, contractReceiptIndex, t.ShardID), Version: 1, Owner: t.Sender, Kind: object.KindSystem, Data: receipt.MarshalBinary()}) + return Result{Consumed: consumed, Created: created, TxID: txID}, nil +} + +func (e Engine) feeOnlyOutputs(t tx.Transaction) ([]object.Object, []types.ObjectID, error) { + return e.feeOnlyOutputsExcluding(t, types.ObjectID{}, nil) +} + +func (e Engine) feeOnlyOutputsExcluding(t tx.Transaction, contractObject types.ObjectID, accesses []contracts.Access) ([]object.Object, []types.ObjectID, error) { + nonCoin := make(map[types.ObjectID]bool, len(accesses)+1) + if !types.IsZero32([32]byte(contractObject)) { + nonCoin[contractObject] = true + } + for _, access := range accesses { + nonCoin[access.ObjectID] = true + } + var nativeIn uint64 + consumed := make([]types.ObjectID, 0) + for _, witness := range t.Witnesses { + if nonCoin[witness.Object.ID] { + continue + } + if witness.Object.Kind != object.KindCoin || witness.Object.Owner != t.Sender { + return nil, nil, ErrOwnership + } + coin, err := object.ParseCoin(witness.Object.Data) + if err != nil || coin.Token != e.NativeToken { + return nil, nil, ErrConservation + } + if math.MaxUint64-nativeIn < coin.Amount { + return nil, nil, ErrOverflow + } + nativeIn += coin.Amount + consumed = append(consumed, witness.Object.ID) + } + var nativeOut uint64 + created := make([]object.Object, 0, len(t.Outputs)) + router := sharding.Router{ShardCount: e.ShardCount} + for i, spec := range t.Outputs { + if spec.Kind != object.KindCoin { + return nil, nil, ErrConservation + } + destination, err := router.ShardForAccount(spec.Owner) + if err != nil || destination != t.ShardID { + return nil, nil, ErrShard + } + coin, err := object.ParseCoin(spec.Data) + if err != nil || coin.Token != e.NativeToken { + return nil, nil, ErrConservation + } + if math.MaxUint64-nativeOut < coin.Amount { + return nil, nil, ErrOverflow + } + nativeOut += coin.Amount + created = append(created, object.Object{ID: types.ObjectIDForShard(t.ID(), uint32(i), t.ShardID), Version: 1, Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...)}) + } + if math.MaxUint64-nativeOut < t.Fee || nativeIn != nativeOut+t.Fee { + return nil, nil, ErrConservation + } + return created, consumed, nil +} From f313ea3dae65cded3f75a4e4490b10ce4ede77af Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:22:11 +0200 Subject: [PATCH 099/274] allow parallel contract calls with shared read-only state --- internal/v2/execution/parallel.go | 61 ++++++++++++++++--- internal/v2/execution/parallel_access_test.go | 51 ++++++++++++++++ 2 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 internal/v2/execution/parallel_access_test.go diff --git a/internal/v2/execution/parallel.go b/internal/v2/execution/parallel.go index 4c5591e2..cdf0c926 100644 --- a/internal/v2/execution/parallel.go +++ b/internal/v2/execution/parallel.go @@ -5,6 +5,7 @@ import ( "runtime" "sync" + "github.com/zephyr-chain/zephyr-chain/internal/v2/contracts" "github.com/zephyr-chain/zephyr-chain/internal/v2/object" "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" @@ -16,10 +17,16 @@ var ( ErrBatchStateRoot = errors.New("v2 batch does not target the current state root") ) -// BatchExecutor executes proof-carrying transactions concurrently only when -// their consumed object sets are disjoint. All transactions in one batch are -// anchored to the same pre-state root, so execution can be parallel and the -// resulting object delta can be committed atomically in deterministic order. +type accessMode uint8 + +const ( + accessRead accessMode = iota + 1 + accessWrite +) + +// BatchExecutor executes proof-carrying transactions concurrently when their +// state access sets are independent. Shared immutable/read-only objects are +// allowed; any read/write or write/write overlap is rejected before workers run. type BatchExecutor struct { Engine Engine Workers int @@ -95,8 +102,8 @@ func (b BatchExecutor) ApplyBatch(store worldstate.Backend, transactions []tx.Tr func validateIndependentBatch(transactions []tx.Transaction) error { root := transactions[0].StateRoot - seenInputs := make(map[types.ObjectID]struct{}) seenTransactions := make(map[types.Hash]struct{}) + global := make(map[types.ObjectID]accessMode) for _, transaction := range transactions { if transaction.StateRoot != root { return ErrBatchStateRoot @@ -106,12 +113,48 @@ func validateIndependentBatch(transactions []tx.Transaction) error { return ErrBatchConflict } seenTransactions[id] = struct{}{} - for _, input := range transaction.Inputs { - if _, conflict := seenInputs[input.ObjectID]; conflict { - return ErrBatchConflict + accesses, err := transactionAccesses(transaction) + if err != nil { + return err + } + for objectID, mode := range accesses { + if prior, exists := global[objectID]; exists { + if prior == accessWrite || mode == accessWrite { + return ErrBatchConflict + } + continue } - seenInputs[input.ObjectID] = struct{}{} + global[objectID] = mode } } return nil } + +func transactionAccesses(transaction tx.Transaction) (map[types.ObjectID]accessMode, error) { + accesses := make(map[types.ObjectID]accessMode, len(transaction.Inputs)) + for _, input := range transaction.Inputs { + accesses[input.ObjectID] = accessWrite + } + if len(transaction.Operations) != 1 || transaction.Operations[0].Kind != tx.OpContractCall { + return accesses, nil + } + call, err := contracts.ParseCall(transaction.Operations[0].Payload) + if err != nil { + return nil, err + } + if _, present := accesses[call.ContractObject]; !present { + return nil, ErrBatchConflict + } + accesses[call.ContractObject] = accessRead + for _, access := range call.Accesses { + if _, present := accesses[access.ObjectID]; !present { + return nil, ErrBatchConflict + } + if access.Write { + accesses[access.ObjectID] = accessWrite + } else { + accesses[access.ObjectID] = accessRead + } + } + return accesses, nil +} diff --git a/internal/v2/execution/parallel_access_test.go b/internal/v2/execution/parallel_access_test.go new file mode 100644 index 00000000..ab3ef444 --- /dev/null +++ b/internal/v2/execution/parallel_access_test.go @@ -0,0 +1,51 @@ +package execution + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/contracts" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestBatchAllowsSharedReadOnlyContractObject(t *testing.T) { + root := types.HashBytes("state", []byte("shared-read")) + contractID := types.ObjectID(types.HashBytes("contract-object", []byte("shared"))) + stateA := types.ObjectID(types.HashBytes("state-object", []byte("a"))) + stateB := types.ObjectID(types.HashBytes("state-object", []byte("b"))) + feeA := types.ObjectID(types.HashBytes("coin", []byte("a"))) + feeB := types.ObjectID(types.HashBytes("coin", []byte("b"))) + + makeTx := func(stateID, feeID types.ObjectID, salt byte) tx.Transaction { + payload, err := (contracts.Call{ContractObject: contractID, Entrypoint: "run", FuelLimit: 100, Accesses: []contracts.Access{{ObjectID: stateID, Write: true}}}).MarshalBinary() + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{StateRoot: root, Inputs: []tx.InputRef{{ObjectID: contractID}, {ObjectID: stateID}, {ObjectID: feeID}}, Operations: []tx.Operation{{Kind: tx.OpContractCall, Payload: payload}}} + transaction.Salt[0] = salt + return transaction + } + if err := validateIndependentBatch([]tx.Transaction{makeTx(stateA, feeA, 1), makeTx(stateB, feeB, 2)}); err != nil { + t.Fatalf("shared read-only contract should be parallelizable: %v", err) + } +} + +func TestBatchRejectsReadWriteContractStateConflict(t *testing.T) { + root := types.HashBytes("state", []byte("rw-conflict")) + contractID := types.ObjectID(types.HashBytes("contract-object", []byte("conflict"))) + shared := types.ObjectID(types.HashBytes("state-object", []byte("shared"))) + feeA := types.ObjectID(types.HashBytes("coin", []byte("ca"))) + feeB := types.ObjectID(types.HashBytes("coin", []byte("cb"))) + makeTx := func(write bool, feeID types.ObjectID, salt byte) tx.Transaction { + payload, err := (contracts.Call{ContractObject: contractID, Entrypoint: "run", FuelLimit: 100, Accesses: []contracts.Access{{ObjectID: shared, Write: write}}}).MarshalBinary() + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{StateRoot: root, Inputs: []tx.InputRef{{ObjectID: contractID}, {ObjectID: shared}, {ObjectID: feeID}}, Operations: []tx.Operation{{Kind: tx.OpContractCall, Payload: payload}}} + transaction.Salt[0] = salt + return transaction + } + if err := validateIndependentBatch([]tx.Transaction{makeTx(false, feeA, 1), makeTx(true, feeB, 2)}); err != ErrBatchConflict { + t.Fatalf("expected read/write conflict, got %v", err) + } +} From 2625372874f2c659b29f3bd12bfb8c508e51562d Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:23:26 +0200 Subject: [PATCH 100/274] keep contract metering guard runtime-agnostic --- internal/v2/contracts/metered.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/v2/contracts/metered.go b/internal/v2/contracts/metered.go index 145f662d..60c85c53 100644 --- a/internal/v2/contracts/metered.go +++ b/internal/v2/contracts/metered.go @@ -42,8 +42,14 @@ func (m MeteredRuntime) Execute(request Request) (Result, error) { if err != nil { return Result{}, err } - if err := m.Inner.ValidateModule(request.Code); err != nil { - return Result{}, err + // Code is optional at this generic guard boundary so deterministic runtimes + // backed by pre-registered/native modules can still use the same consensus + // invariants. Runtimes that require code (Zephyr Script/WASM) reject an empty + // module inside Execute/ValidateModule themselves. + if len(request.Code) > 0 { + if err := m.Inner.ValidateModule(request.Code); err != nil { + return Result{}, err + } } result, err := m.Inner.Execute(request) if err != nil { @@ -71,7 +77,7 @@ func (m MeteredRuntime) Execute(request Request) (Result, error) { func validateRequest(request Request) (map[types.ObjectID]bool, error) { if types.IsZero32([32]byte(request.ContractID)) || strings.TrimSpace(request.Entrypoint) == "" || len(request.Entrypoint) > 128 || - len(request.Code) == 0 || len(request.Code) > MaxModuleBytes || len(request.Arguments) > MaxArgumentsBytes || request.FuelLimit == 0 || len(request.Accesses) > MaxAccesses { + len(request.Code) > MaxModuleBytes || len(request.Arguments) > MaxArgumentsBytes || request.FuelLimit == 0 || len(request.Accesses) > MaxAccesses { return nil, ErrInvalidRequest } allowed := make(map[types.ObjectID]bool, len(request.Accesses)) From 23307289571572c38d9834d829da3acde9a46263 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:24:03 +0200 Subject: [PATCH 101/274] preserve contract runtime sentinel errors --- internal/v2/contracts/script_runtime.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/v2/contracts/script_runtime.go b/internal/v2/contracts/script_runtime.go index bae1c67b..debc472e 100644 --- a/internal/v2/contracts/script_runtime.go +++ b/internal/v2/contracts/script_runtime.go @@ -107,7 +107,7 @@ func (ScriptRuntime) Execute(request Request) (Result, error) { if thread.ExecutionSteps() >= request.FuelLimit { return Result{}, ErrFuelExhausted } - return Result{}, fmt.Errorf("%w: %v", ErrScriptRuntime, err) + return Result{}, fmt.Errorf("%w: %w", ErrScriptRuntime, err) } entry, ok := globals[request.Entrypoint] if !ok { @@ -122,7 +122,7 @@ func (ScriptRuntime) Execute(request Request) (Result, error) { if thread.ExecutionSteps() >= request.FuelLimit { return Result{}, ErrFuelExhausted } - return Result{}, fmt.Errorf("%w: %v", ErrScriptRuntime, err) + return Result{}, fmt.Errorf("%w: %w", ErrScriptRuntime, err) } returned, err := valueBytes(value) if value == starlark.None { From 9f11d0898b21685a02b04b8a150f71ba353a7233 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:29:13 +0200 Subject: [PATCH 102/274] integrate compute escrow and cross-shard job lifecycle into v2 execution --- internal/v2/compute/messages.go | 267 +++++++++++++ internal/v2/execution/extended_compute.go | 427 +++++++++++++++++++++ internal/v2/execution/extended_contract.go | 2 + internal/v2/node/runtime.go | 40 +- internal/v2/tx/transaction.go | 216 +++++------ 5 files changed, 797 insertions(+), 155 deletions(-) create mode 100644 internal/v2/compute/messages.go create mode 100644 internal/v2/execution/extended_compute.go diff --git a/internal/v2/compute/messages.go b/internal/v2/compute/messages.go new file mode 100644 index 00000000..c6ac7ac3 --- /dev/null +++ b/internal/v2/compute/messages.go @@ -0,0 +1,267 @@ +package compute + +import ( + "bytes" + "errors" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrComputeMessage = errors.New("invalid compute cross-shard message") + +type AssignmentMessage struct { + JobID types.JobID + JobOwner types.AccountID + JobShard uint32 + OfferID types.Hash + Offer Offer + Job Job +} + +func (m AssignmentMessage) Validate() error { + if types.IsZero32([32]byte(m.JobID)) || types.IsZero32([32]byte(m.JobOwner)) || types.IsZero32([32]byte(m.OfferID)) || m.Job.Owner != m.JobOwner { + return ErrComputeMessage + } + if err := m.Offer.Validate(); err != nil { + return ErrComputeMessage + } + if err := m.Job.Validate(); err != nil || !offerMatchesJob(m.Offer, m.Job) || m.Offer.Collateral < m.Job.CollateralRequired { + return ErrComputeMessage + } + return nil +} + +func (m AssignmentMessage) MarshalBinary() ([]byte, error) { + if err := m.Validate(); err != nil { + return nil, err + } + offer, _ := m.Offer.MarshalBinary() + job, _ := m.Job.MarshalBinary() + var w codec.Writer + w.Fixed(m.JobID[:]) + w.Fixed(m.JobOwner[:]) + w.U32(m.JobShard) + w.Fixed(m.OfferID[:]) + w.Bytes(offer) + w.Bytes(job) + return w.BytesCopy(), nil +} + +func ParseAssignmentMessage(data []byte) (AssignmentMessage, error) { + r := codec.NewReader(data) + jobIDRaw, err := r.Fixed(32) + if err != nil { + return AssignmentMessage{}, ErrComputeMessage + } + owner, err := readAccount(r) + if err != nil { + return AssignmentMessage{}, ErrComputeMessage + } + shard, err := r.U32() + if err != nil { + return AssignmentMessage{}, ErrComputeMessage + } + offerID, err := readHash(r) + if err != nil { + return AssignmentMessage{}, ErrComputeMessage + } + offerRaw, err := r.Bytes(1 << 20) + if err != nil { + return AssignmentMessage{}, ErrComputeMessage + } + offer, err := ParseOffer(offerRaw) + if err != nil { + return AssignmentMessage{}, ErrComputeMessage + } + jobRaw, err := r.Bytes(1 << 20) + if err != nil { + return AssignmentMessage{}, ErrComputeMessage + } + job, err := ParseJob(jobRaw) + if err != nil || r.Done() != nil { + return AssignmentMessage{}, ErrComputeMessage + } + var jobID types.JobID + copy(jobID[:], jobIDRaw) + message := AssignmentMessage{JobID: jobID, JobOwner: owner, JobShard: shard, OfferID: offerID, Offer: offer, Job: job} + if err := message.Validate(); err != nil { + return AssignmentMessage{}, err + } + return message, nil +} + +func (m AssignmentMessage) Output() (object.OutputSpec, error) { + raw, err := m.MarshalBinary() + if err != nil { + return object.OutputSpec{}, err + } + return object.OutputSpec{Owner: m.JobOwner, Kind: object.KindComputeAssignment, Data: raw}, nil +} + +func (m AssignmentMessage) ValidateForRecord(record OnChainJob) error { + if record.ID != m.JobID || record.Job.Owner != m.JobOwner { + return ErrComputeMessage + } + a, err := record.Job.MarshalBinary() + if err != nil { + return err + } + b, err := m.Job.MarshalBinary() + if err != nil || !bytes.Equal(a, b) { + return ErrComputeMessage + } + return m.Validate() +} + +type ResultMessage struct { + JobID types.JobID + JobOwner types.AccountID + JobShard uint32 + Result Result +} + +func (m ResultMessage) Validate() error { + if types.IsZero32([32]byte(m.JobID)) || types.IsZero32([32]byte(m.JobOwner)) || m.Result.JobID != m.JobID { + return ErrComputeMessage + } + return m.Result.Validate() +} + +func (m ResultMessage) MarshalBinary() ([]byte, error) { + if err := m.Validate(); err != nil { + return nil, err + } + result, _ := m.Result.MarshalBinary() + var w codec.Writer + w.Fixed(m.JobID[:]) + w.Fixed(m.JobOwner[:]) + w.U32(m.JobShard) + w.Bytes(result) + return w.BytesCopy(), nil +} + +func ParseResultMessage(data []byte) (ResultMessage, error) { + r := codec.NewReader(data) + jobRaw, err := r.Fixed(32) + if err != nil { + return ResultMessage{}, ErrComputeMessage + } + owner, err := readAccount(r) + if err != nil { + return ResultMessage{}, ErrComputeMessage + } + shard, err := r.U32() + if err != nil { + return ResultMessage{}, ErrComputeMessage + } + resultRaw, err := r.Bytes(2048) + if err != nil { + return ResultMessage{}, ErrComputeMessage + } + result, err := ParseResult(resultRaw) + if err != nil || r.Done() != nil { + return ResultMessage{}, ErrComputeMessage + } + var jobID types.JobID + copy(jobID[:], jobRaw) + message := ResultMessage{JobID: jobID, JobOwner: owner, JobShard: shard, Result: result} + if err := message.Validate(); err != nil { + return ResultMessage{}, err + } + return message, nil +} + +func (m ResultMessage) Output() (object.OutputSpec, error) { + raw, err := m.MarshalBinary() + if err != nil { + return object.OutputSpec{}, err + } + return object.OutputSpec{Owner: m.JobOwner, Kind: object.KindComputeResult, Data: raw}, nil +} + +type IngestRef struct { + JobObject types.ObjectID + MessageObject types.ObjectID +} + +func (r IngestRef) MarshalBinary() ([]byte, error) { + if types.IsZero32([32]byte(r.JobObject)) || types.IsZero32([32]byte(r.MessageObject)) || r.JobObject == r.MessageObject { + return nil, ErrComputeMessage + } + var w codec.Writer + w.Fixed(r.JobObject[:]) + w.Fixed(r.MessageObject[:]) + return w.BytesCopy(), nil +} + +func ParseIngestRef(data []byte) (IngestRef, error) { + if len(data) != 64 { + return IngestRef{}, ErrComputeMessage + } + var out IngestRef + copy(out.JobObject[:], data[:32]) + copy(out.MessageObject[:], data[32:]) + if _, err := out.MarshalBinary(); err != nil { + return IngestRef{}, err + } + return out, nil +} + +type JobRef struct{ JobObject types.ObjectID } + +func (r JobRef) MarshalBinary() ([]byte, error) { + if types.IsZero32([32]byte(r.JobObject)) { + return nil, ErrComputeMessage + } + return append([]byte(nil), r.JobObject[:]...), nil +} + +func ParseJobRef(data []byte) (JobRef, error) { + if len(data) != 32 { + return JobRef{}, ErrComputeMessage + } + var out JobRef + copy(out.JobObject[:], data) + if _, err := out.MarshalBinary(); err != nil { + return JobRef{}, err + } + return out, nil +} + +type SettlementReceipt struct { + JobID types.JobID + ResultRoot types.Hash + Payments map[types.AccountID]uint64 + Refund uint64 + Slashed map[types.AccountID]uint64 + SlashReward uint64 + Expired bool +} + +func (r SettlementReceipt) MarshalBinary() []byte { + var w codec.Writer + w.Fixed(r.JobID[:]) + w.Fixed(r.ResultRoot[:]) + writeAccountAmounts(&w, r.Payments) + w.U64(r.Refund) + writeAccountAmounts(&w, r.Slashed) + w.U64(r.SlashReward) + w.Bool(r.Expired) + return w.BytesCopy() +} + +func writeAccountAmounts(w *codec.Writer, values map[types.AccountID]uint64) { + ids := make([]types.AccountID, 0, len(values)) + for id := range values { + ids = append(ids, id) + } + sort.Slice(ids, func(i, j int) bool { return ids[i].String() < ids[j].String() }) + w.U32(uint32(len(ids))) + for _, id := range ids { + w.Fixed(id[:]) + w.U64(values[id]) + } +} diff --git a/internal/v2/execution/extended_compute.go b/internal/v2/execution/extended_compute.go new file mode 100644 index 00000000..61823407 --- /dev/null +++ b/internal/v2/execution/extended_compute.go @@ -0,0 +1,427 @@ +package execution + +import ( + "bytes" + "math" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const computeReceiptIndex uint32 = 0xC0000000 + +func (e Engine) executeCompute(t tx.Transaction, op tx.Operation) (Result, error) { + switch op.Kind { + case tx.OpComputeOffer: + return e.executeComputeOffer(t, op.Payload) + case tx.OpComputeJob: + return e.executeComputeJob(t, op.Payload) + case tx.OpComputeAccept: + return e.executeComputeAccept(t, op.Payload) + case tx.OpComputeResult: + return e.executeComputeResult(t, op.Payload) + case tx.OpComputeIngestAssignment: + return e.executeComputeIngestAssignment(t, op.Payload) + case tx.OpComputeIngestResult: + return e.executeComputeIngestResult(t, op.Payload) + case tx.OpComputeFinalize: + return e.executeComputeFinalize(t, op.Payload, false) + case tx.OpComputeResolveReplicated: + return e.executeComputeFinalize(t, op.Payload, true) + case tx.OpComputeExpire: + return e.executeComputeExpire(t, op.Payload) + default: + return Result{}, ErrUnsupportedOperation + } +} + +func (e Engine) executeComputeOffer(t tx.Transaction, payload []byte) (Result, error) { + offer, err := compute.ParseOffer(payload) + if err != nil || offer.Provider != t.Sender || (e.Height > 0 && offer.ValidUntilHeight < e.Height) { + return Result{}, ErrOwnership + } + created, outbound, consumed, err := e.accountNativeValue(t, nil, 0, offer.Collateral) + if err != nil { + return Result{}, err + } + offerObject, err := compute.NewOfferObject(t.ID(), t.ShardID, offer) + if err != nil { + return Result{}, err + } + created = append(created, offerObject) + return Result{Consumed: consumed, Created: created, Outbound: outbound, TxID: t.ID()}, nil +} + +func (e Engine) executeComputeJob(t tx.Transaction, payload []byte) (Result, error) { + job, err := compute.ParseJob(payload) + if err != nil || job.Owner != t.Sender || (e.Height > 0 && job.DeadlineHeight <= e.Height) { + return Result{}, ErrOwnership + } + created, outbound, consumed, err := e.accountNativeValue(t, nil, 0, job.MaxPrice) + if err != nil { + return Result{}, err + } + jobObject, _, err := compute.NewJobObject(t.ID(), t.ShardID, job, job.MaxPrice) + if err != nil { + return Result{}, err + } + created = append(created, jobObject) + return Result{Consumed: consumed, Created: created, Outbound: outbound, TxID: t.ID()}, nil +} + +func (e Engine) executeComputeAccept(t tx.Transaction, payload []byte) (Result, error) { + message, err := compute.ParseAssignmentMessage(payload) + if err != nil || message.Offer.Provider != t.Sender || message.JobShard >= e.ShardCount { + return Result{}, ErrOwnership + } + offerWitness, ok := witnessByKind(t, object.KindComputeOffer) + if !ok || offerWitness.Object.Owner != t.Sender || types.Hash(offerWitness.Object.ID) != message.OfferID || !bytes.Equal(offerWitness.Object.Data, mustOfferBytes(message.Offer)) { + return Result{}, ErrOwnership + } + locked := message.Job.CollateralRequired + excluded := map[types.ObjectID]bool{offerWitness.Object.ID: true} + created, outbound, consumed, err := e.accountNativeValue(t, excluded, message.Offer.Collateral, locked) + if err != nil { + return Result{}, err + } + consumed = append(consumed, offerWitness.Object.ID) + spec, err := message.Output() + if err != nil { + return Result{}, err + } + if message.JobShard == t.ShardID { + created = append(created, object.Object{ID: types.ObjectIDForShard(t.ID(), computeReceiptIndex, t.ShardID), Version: 1, Owner: spec.Owner, Kind: spec.Kind, Data: spec.Data}) + } else { + outbound = append(outbound, OutboundOutput{DestinationShard: message.JobShard, OutputIndex: computeReceiptIndex, Output: spec}) + } + return Result{Consumed: consumed, Created: created, Outbound: outbound, TxID: t.ID()}, nil +} + +func (e Engine) executeComputeResult(t tx.Transaction, payload []byte) (Result, error) { + message, err := compute.ParseResultMessage(payload) + if err != nil || message.Result.Provider != t.Sender || message.JobShard >= e.ShardCount { + return Result{}, ErrOwnership + } + created, outbound, consumed, err := e.accountNativeValue(t, nil, 0, 0) + if err != nil { + return Result{}, err + } + spec, _ := message.Output() + if message.JobShard == t.ShardID { + created = append(created, object.Object{ID: types.ObjectIDForShard(t.ID(), computeReceiptIndex, t.ShardID), Version: 1, Owner: spec.Owner, Kind: spec.Kind, Data: spec.Data}) + } else { + outbound = append(outbound, OutboundOutput{DestinationShard: message.JobShard, OutputIndex: computeReceiptIndex, Output: spec}) + } + return Result{Consumed: consumed, Created: created, Outbound: outbound, TxID: t.ID()}, nil +} + +func (e Engine) executeComputeIngestAssignment(t tx.Transaction, payload []byte) (Result, error) { + ref, err := compute.ParseIngestRef(payload) + if err != nil { + return Result{}, err + } + jobWitness, ok := witnessByID(t, ref.JobObject) + if !ok || jobWitness.Object.Kind != object.KindComputeJob || jobWitness.Object.Owner != t.Sender { + return Result{}, ErrOwnership + } + messageWitness, ok := witnessByID(t, ref.MessageObject) + if !ok || messageWitness.Object.Kind != object.KindComputeAssignment || messageWitness.Object.Owner != t.Sender { + return Result{}, ErrOwnership + } + record, err := compute.ParseOnChainJob(jobWitness.Object.Data) + if err != nil { + return Result{}, err + } + message, err := compute.ParseAssignmentMessage(messageWitness.Object.Data) + if err != nil || message.JobShard != t.ShardID || message.ValidateForRecord(record) != nil { + return Result{}, compute.ErrComputeMessage + } + updated, _, _, err := compute.AssignOnChain(record, message.OfferID, message.Offer, e.Height) + if err != nil { + return Result{}, err + } + updatedRaw, err := updated.MarshalBinary() + if err != nil { + return Result{}, err + } + excluded := map[types.ObjectID]bool{jobWitness.Object.ID: true, messageWitness.Object.ID: true} + created, outbound, consumed, err := e.accountNativeValue(t, excluded, record.Job.CollateralRequired, record.Job.CollateralRequired) + if err != nil { + return Result{}, err + } + consumed = append(consumed, jobWitness.Object.ID, messageWitness.Object.ID) + jobObject := jobWitness.Object + jobObject.Version++ + jobObject.Data = updatedRaw + created = append(created, jobObject) + return Result{Consumed: consumed, Created: created, Outbound: outbound, TxID: t.ID()}, nil +} + +func (e Engine) executeComputeIngestResult(t tx.Transaction, payload []byte) (Result, error) { + ref, err := compute.ParseIngestRef(payload) + if err != nil { + return Result{}, err + } + jobWitness, ok := witnessByID(t, ref.JobObject) + if !ok || jobWitness.Object.Kind != object.KindComputeJob || jobWitness.Object.Owner != t.Sender { + return Result{}, ErrOwnership + } + messageWitness, ok := witnessByID(t, ref.MessageObject) + if !ok || messageWitness.Object.Kind != object.KindComputeResult || messageWitness.Object.Owner != t.Sender { + return Result{}, ErrOwnership + } + record, err := compute.ParseOnChainJob(jobWitness.Object.Data) + if err != nil { + return Result{}, err + } + message, err := compute.ParseResultMessage(messageWitness.Object.Data) + if err != nil || message.JobID != record.ID || message.JobOwner != record.Job.Owner || message.JobShard != t.ShardID { + return Result{}, compute.ErrComputeMessage + } + updated, err := compute.SubmitOnChainResult(record, message.Result) + if err != nil { + return Result{}, err + } + updatedRaw, _ := updated.MarshalBinary() + excluded := map[types.ObjectID]bool{jobWitness.Object.ID: true, messageWitness.Object.ID: true} + created, outbound, consumed, err := e.accountNativeValue(t, excluded, 0, 0) + if err != nil { + return Result{}, err + } + consumed = append(consumed, jobWitness.Object.ID, messageWitness.Object.ID) + jobObject := jobWitness.Object + jobObject.Version++ + jobObject.Data = updatedRaw + created = append(created, jobObject) + return Result{Consumed: consumed, Created: created, Outbound: outbound, TxID: t.ID()}, nil +} + +func (e Engine) executeComputeFinalize(t tx.Transaction, payload []byte, majority bool) (Result, error) { + ref, err := compute.ParseJobRef(payload) + if err != nil { + return Result{}, err + } + jobWitness, ok := witnessByID(t, ref.JobObject) + if !ok || jobWitness.Object.Kind != object.KindComputeJob || jobWitness.Object.Owner != t.Sender { + return Result{}, ErrOwnership + } + record, err := compute.ParseOnChainJob(jobWitness.Object.Data) + if err != nil || record.Job.Owner != t.Sender { + return Result{}, ErrOwnership + } + var settlement compute.OnChainSettlement + if majority { + _, settlement, err = compute.ResolveReplicatedMajority(record) + } else { + if record.Job.Verification != compute.VerificationReplicated { + return Result{}, compute.ErrMarketVerification + } + _, settlement, err = compute.FinalizeOnChain(record, compute.VerificationEvidence{}) + } + if err != nil { + return Result{}, err + } + locked, err := jobLockedValue(record) + if err != nil { + return Result{}, err + } + generated, generatedOutbound, generatedAmount, err := e.settlementOutputs(t, record, settlement) + if err != nil || generatedAmount != locked { + return Result{}, compute.ErrMarketEscrow + } + excluded := map[types.ObjectID]bool{jobWitness.Object.ID: true} + created, outbound, consumed, err := e.accountNativeValue(t, excluded, locked, generatedAmount) + if err != nil { + return Result{}, err + } + created = append(created, generated...) + outbound = append(outbound, generatedOutbound...) + consumed = append(consumed, jobWitness.Object.ID) + receipt := compute.SettlementReceipt{JobID: record.ID, ResultRoot: settlement.ResultRoot, Payments: settlement.Payments, Refund: settlement.Refund, Slashed: settlement.SlashedCollateral, SlashReward: settlement.SlashReward} + created = append(created, object.Object{ID: types.ObjectIDForShard(t.ID(), computeReceiptIndex, t.ShardID), Version: 1, Kind: object.KindSystem, Data: receipt.MarshalBinary()}) + return Result{Consumed: consumed, Created: created, Outbound: outbound, TxID: t.ID()}, nil +} + +func (e Engine) executeComputeExpire(t tx.Transaction, payload []byte) (Result, error) { + ref, err := compute.ParseJobRef(payload) + if err != nil { + return Result{}, err + } + jobWitness, ok := witnessByID(t, ref.JobObject) + if !ok || jobWitness.Object.Kind != object.KindComputeJob || jobWitness.Object.Owner != t.Sender { + return Result{}, ErrOwnership + } + record, err := compute.ParseOnChainJob(jobWitness.Object.Data) + if err != nil { + return Result{}, err + } + _, refund, collateral, err := compute.ExpireOnChain(record, e.Height) + if err != nil { + return Result{}, err + } + settlement := compute.OnChainSettlement{Settlement: compute.Settlement{JobID: record.ID, Payments: map[types.AccountID]uint64{}, Refund: refund}, CollateralReturns: collateral, SlashedCollateral: map[types.AccountID]uint64{}} + locked, err := jobLockedValue(record) + if err != nil { + return Result{}, err + } + generated, generatedOutbound, generatedAmount, err := e.settlementOutputs(t, record, settlement) + if err != nil || generatedAmount != locked { + return Result{}, compute.ErrMarketEscrow + } + excluded := map[types.ObjectID]bool{jobWitness.Object.ID: true} + created, outbound, consumed, err := e.accountNativeValue(t, excluded, locked, generatedAmount) + if err != nil { + return Result{}, err + } + created = append(created, generated...) + outbound = append(outbound, generatedOutbound...) + consumed = append(consumed, jobWitness.Object.ID) + receipt := compute.SettlementReceipt{JobID: record.ID, Refund: refund, Expired: true} + created = append(created, object.Object{ID: types.ObjectIDForShard(t.ID(), computeReceiptIndex, t.ShardID), Version: 1, Kind: object.KindSystem, Data: receipt.MarshalBinary()}) + return Result{Consumed: consumed, Created: created, Outbound: outbound, TxID: t.ID()}, nil +} + +func (e Engine) accountNativeValue(t tx.Transaction, excluded map[types.ObjectID]bool, internalIn, internalOut uint64) ([]object.Object, []OutboundOutput, []types.ObjectID, error) { + var available uint64 = internalIn + consumed := make([]types.ObjectID, 0) + for _, witness := range t.Witnesses { + if excluded[witness.Object.ID] { + continue + } + if witness.Object.Kind != object.KindCoin || witness.Object.Owner != t.Sender { + return nil, nil, nil, ErrOwnership + } + coin, err := object.ParseCoin(witness.Object.Data) + if err != nil || coin.Token != e.NativeToken || math.MaxUint64-available < coin.Amount { + return nil, nil, nil, ErrConservation + } + available += coin.Amount + consumed = append(consumed, witness.Object.ID) + } + var required uint64 = internalOut + created := make([]object.Object, 0, len(t.Outputs)) + outbound := make([]OutboundOutput, 0) + router := sharding.Router{ShardCount: e.ShardCount} + for i, spec := range t.Outputs { + if spec.Kind != object.KindCoin { + return nil, nil, nil, ErrConservation + } + coin, err := object.ParseCoin(spec.Data) + if err != nil || coin.Token != e.NativeToken || math.MaxUint64-required < coin.Amount { + return nil, nil, nil, ErrConservation + } + required += coin.Amount + destination, err := router.ShardForAccount(spec.Owner) + if err != nil { + return nil, nil, nil, ErrShard + } + if destination == t.ShardID { + created = append(created, object.Object{ID: types.ObjectIDForShard(t.ID(), uint32(i), t.ShardID), Version: 1, Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...)}) + } else { + outbound = append(outbound, OutboundOutput{DestinationShard: destination, OutputIndex: uint32(i), Output: spec}) + } + } + if math.MaxUint64-required < t.Fee { + return nil, nil, nil, ErrOverflow + } + required += t.Fee + if available != required { + return nil, nil, nil, ErrConservation + } + return created, outbound, consumed, nil +} + +func (e Engine) settlementOutputs(t tx.Transaction, record compute.OnChainJob, settlement compute.OnChainSettlement) ([]object.Object, []OutboundOutput, uint64, error) { + amounts := make(map[types.AccountID]uint64) + for provider, value := range settlement.Payments { + amounts[provider] += value + } + for provider, value := range settlement.CollateralReturns { + if math.MaxUint64-amounts[provider] < value { + return nil, nil, 0, ErrOverflow + } + amounts[provider] += value + } + ownerAmount := settlement.Refund + if math.MaxUint64-ownerAmount < settlement.SlashReward { + return nil, nil, 0, ErrOverflow + } + ownerAmount += settlement.SlashReward + if ownerAmount > 0 { + amounts[record.Job.Owner] += ownerAmount + } + ids := make([]types.AccountID, 0, len(amounts)) + for id := range amounts { + ids = append(ids, id) + } + sort.Slice(ids, func(i, j int) bool { return ids[i].String() < ids[j].String() }) + router := sharding.Router{ShardCount: e.ShardCount} + created := make([]object.Object, 0, len(ids)) + outbound := make([]OutboundOutput, 0) + var total uint64 + for i, id := range ids { + amount := amounts[id] + if math.MaxUint64-total < amount { + return nil, nil, 0, ErrOverflow + } + total += amount + spec, err := object.NewCoinOutput(id, e.NativeToken, amount) + if err != nil { + return nil, nil, 0, err + } + destination, err := router.ShardForAccount(id) + if err != nil { + return nil, nil, 0, ErrShard + } + index := computeReceiptIndex + 1 + uint32(i) + if destination == t.ShardID { + created = append(created, object.Object{ID: types.ObjectIDForShard(t.ID(), index, t.ShardID), Version: 1, Owner: spec.Owner, Kind: spec.Kind, Data: spec.Data}) + } else { + outbound = append(outbound, OutboundOutput{DestinationShard: destination, OutputIndex: index, Output: spec}) + } + } + return created, outbound, total, nil +} + +func jobLockedValue(record compute.OnChainJob) (uint64, error) { + locked := record.Escrow + for range record.Assignments { + if math.MaxUint64-locked < record.Job.CollateralRequired { + return 0, ErrOverflow + } + locked += record.Job.CollateralRequired + } + return locked, nil +} + +func witnessByKind(t tx.Transaction, kind object.Kind) (tx.Witness, bool) { + var found tx.Witness + ok := false + for _, witness := range t.Witnesses { + if witness.Object.Kind == kind { + if ok { + return tx.Witness{}, false + } + found, ok = witness, true + } + } + return found, ok +} + +func witnessByID(t tx.Transaction, id types.ObjectID) (tx.Witness, bool) { + for _, witness := range t.Witnesses { + if witness.Object.ID == id { + return witness, true + } + } + return tx.Witness{}, false +} + +func mustOfferBytes(offer compute.Offer) []byte { + raw, _ := offer.MarshalBinary() + return raw +} diff --git a/internal/v2/execution/extended_contract.go b/internal/v2/execution/extended_contract.go index 7b25c258..637cc015 100644 --- a/internal/v2/execution/extended_contract.go +++ b/internal/v2/execution/extended_contract.go @@ -24,6 +24,8 @@ func (e Engine) executeExtended(t tx.Transaction, op tx.Operation) (Result, erro return e.executeDeployContract(t, op.Payload) case tx.OpContractCall: return e.executeContractCall(t, op.Payload) + case tx.OpComputeOffer, tx.OpComputeJob, tx.OpComputeResult, tx.OpComputeAccept, tx.OpComputeIngestAssignment, tx.OpComputeIngestResult, tx.OpComputeFinalize, tx.OpComputeResolveReplicated, tx.OpComputeExpire: + return e.executeCompute(t, op) default: return Result{}, ErrUnsupportedOperation } diff --git a/internal/v2/node/runtime.go b/internal/v2/node/runtime.go index 3704d048..d868eba5 100644 --- a/internal/v2/node/runtime.go +++ b/internal/v2/node/runtime.go @@ -78,38 +78,28 @@ func NewRuntime(network types.NetworkID, nativeToken types.TokenID, validatorRoo return &Runtime{Network: network, NativeToken: nativeToken, ValidatorRoot: validatorRoot, ShardCount: count, States: states, Workers: workers}, nil } -// BuildCandidate executes and simulates every shard against committed state. -// It never mutates the backing state stores. Receipt imports become destination -// objects plus durable anti-replay markers, but are not spendable until a later -// block because all transactions in this candidate target the pre-state root. func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) (Candidate, error) { r.mu.Lock() defer r.mu.Unlock() if height != r.Height+1 || height == 0 { return Candidate{}, ErrCandidateHeight } - candidate := Candidate{ - Results: make(map[uint32][]execution.Result), - Receipts: make(map[uint32][]sharding.CrossShardReceipt), - deltas: make(map[uint32]shardDelta), - } + candidate := Candidate{Results: make(map[uint32][]execution.Result), Receipts: make(map[uint32][]sharding.CrossShardReceipt), deltas: make(map[uint32]shardDelta)} commitments := make([]sharding.Commitment, 0, r.ShardCount) dataLeaves := make([]types.Hash, 0, r.ShardCount) - for shard := uint32(0); shard < r.ShardCount; shard++ { store := r.States[shard] batch := batches[shard] currentRoot := store.Root() delta := shardDelta{} results := make([]execution.Result, 0, len(batch.Transactions)) - if len(batch.Transactions) > 0 { for _, transaction := range batch.Transactions { if transaction.ShardID != shard || transaction.StateRoot != currentRoot { return Candidate{}, ErrCandidateState } } - executor := execution.BatchExecutor{Engine: execution.Engine{Network: r.Network, NativeToken: r.NativeToken, ShardCount: r.ShardCount}, Workers: r.Workers} + executor := execution.BatchExecutor{Engine: execution.Engine{Network: r.Network, NativeToken: r.NativeToken, ShardCount: r.ShardCount, Height: height}, Workers: r.Workers} var err error results, err = executor.ExecuteBatch(batch.Transactions) if err != nil { @@ -121,7 +111,6 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( } candidate.Results[shard] = results } - for _, receiptImport := range batch.Imports { if err := r.validateReceiptImport(shard, receiptImport); err != nil { return Candidate{}, err @@ -142,7 +131,6 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( } delta.Created = append(delta.Created, destinationObject, marker) } - newRoot := currentRoot if len(delta.Consumed) > 0 || len(delta.Created) > 0 { simulator, ok := store.(worldstate.Simulator) @@ -156,15 +144,10 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( } candidate.deltas[shard] = delta } - receipts := make([]sharding.CrossShardReceipt, 0) for _, result := range results { for _, outbound := range result.Outbound { - receipts = append(receipts, sharding.CrossShardReceipt{ - SourceShard: shard, DestinationShard: outbound.DestinationShard, - SourceHeight: height, TransactionID: result.TxID, OutputIndex: outbound.OutputIndex, - Output: outbound.Output, SourceStateRoot: newRoot, - }) + receipts = append(receipts, sharding.CrossShardReceipt{SourceShard: shard, DestinationShard: outbound.DestinationShard, SourceHeight: height, TransactionID: result.TxID, OutputIndex: outbound.OutputIndex, Output: outbound.Output, SourceStateRoot: newRoot}) } } receiptRoot, err := (sharding.ReceiptBatch{Receipts: receipts}).Root() @@ -172,7 +155,6 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( return Candidate{}, err } candidate.Receipts[shard] = receipts - dataRoot := batch.DataRoot if types.IsZero32([32]byte(dataRoot)) { dataRoot = merkle.Root(nil) @@ -185,27 +167,19 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( return Candidate{}, err } candidate.Commitments = commitments - candidate.Header = sharding.GlobalHeader{ - Version: 2, Network: r.Network, Height: height, ParentHash: r.ParentHash, - ShardCommitmentRoot: commitmentRoot, ValidatorRoot: r.ValidatorRoot, - DataRoot: merkle.Root(dataLeaves), - } + candidate.Header = sharding.GlobalHeader{Version: 2, Network: r.Network, Height: height, ParentHash: r.ParentHash, ShardCommitmentRoot: commitmentRoot, ValidatorRoot: r.ValidatorRoot, NextValidatorRoot: r.ValidatorRoot, DataRoot: merkle.Root(dataLeaves)} return candidate, nil } func (r *Runtime) validateReceiptImport(destinationShard uint32, receiptImport ReceiptImport) error { - if receiptImport.Header.Network != r.Network || receiptImport.Validators.Network != r.Network || - receiptImport.Certificate.Network != r.Network || receiptImport.Receipt.DestinationShard != destinationShard || - receiptImport.Header.Height > r.Height || receiptImport.Header.Height != receiptImport.Receipt.SourceHeight { + if receiptImport.Header.Network != r.Network || receiptImport.Validators.Network != r.Network || receiptImport.Certificate.Network != r.Network || receiptImport.Receipt.DestinationShard != destinationShard || receiptImport.Header.Height > r.Height || receiptImport.Header.Height != receiptImport.Receipt.SourceHeight { return ErrReceiptImport } validatorRoot, err := receiptImport.Validators.Root() if err != nil || validatorRoot != receiptImport.Header.ValidatorRoot { return ErrReceiptImport } - if receiptImport.Header.CertificateHash != receiptImport.Certificate.Hash() || - receiptImport.Certificate.HeaderHash != v2consensus.HeaderConsensusHash(receiptImport.Header) || - receiptImport.Certificate.Height != receiptImport.Header.Height { + if receiptImport.Header.CertificateHash != receiptImport.Certificate.Hash() || receiptImport.Certificate.HeaderHash != v2consensus.HeaderConsensusHash(receiptImport.Header) || receiptImport.Certificate.Height != receiptImport.Header.Height { return ErrReceiptImport } if err := receiptImport.Validators.VerifyCertificate(receiptImport.Certificate); err != nil { @@ -217,8 +191,6 @@ func (r *Runtime) validateReceiptImport(destinationShard uint32, receiptImport R return nil } -// Commit applies a previously simulated candidate only after a valid quorum -// certificate for its consensus hash is supplied. func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificate, validators v2consensus.ValidatorSet) (sharding.GlobalHeader, error) { r.mu.Lock() defer r.mu.Unlock() diff --git a/internal/v2/tx/transaction.go b/internal/v2/tx/transaction.go index a41d55e6..cfb4e8b4 100644 --- a/internal/v2/tx/transaction.go +++ b/internal/v2/tx/transaction.go @@ -16,13 +16,19 @@ import ( const ( Version uint16 = 2 - OpTransfer uint16 = 1 - OpCreateToken uint16 = 2 - OpDeployContract uint16 = 3 - OpContractCall uint16 = 4 - OpComputeOffer uint16 = 5 - OpComputeJob uint16 = 6 - OpComputeResult uint16 = 7 + OpTransfer uint16 = 1 + OpCreateToken uint16 = 2 + OpDeployContract uint16 = 3 + OpContractCall uint16 = 4 + OpComputeOffer uint16 = 5 + OpComputeJob uint16 = 6 + OpComputeResult uint16 = 7 + OpComputeAccept uint16 = 8 + OpComputeIngestAssignment uint16 = 9 + OpComputeIngestResult uint16 = 10 + OpComputeFinalize uint16 = 11 + OpComputeResolveReplicated uint16 = 12 + OpComputeExpire uint16 = 13 MaxInputs = 4096 MaxOutputs = 4096 @@ -50,45 +56,68 @@ type InputRef struct { ObjectHash types.Hash } -type Witness struct { - Object object.Object - Proof state.Proof -} - type Operation struct { Kind uint16 Payload []byte } +type Witness struct { + Object object.Object + Proof state.Proof +} + type Transaction struct { Version uint16 Network types.NetworkID + ShardID uint32 Sender types.AccountID SenderPublicKey []byte - ShardID uint32 StateRoot types.Hash - Salt [16]byte Inputs []InputRef Outputs []object.OutputSpec Operations []Operation Fee uint64 ValidUntilHeight uint64 + Salt [16]byte Signature []byte Witnesses []Witness } func (t Transaction) IntentBytes() []byte { var w codec.Writer - writeIntent(&w, t) + w.U16(t.Version) + w.Fixed(t.Network[:]) + w.U32(t.ShardID) + w.Fixed(t.Sender[:]) + w.Bytes(t.SenderPublicKey) + w.Fixed(t.StateRoot[:]) + w.U32(uint32(len(t.Inputs))) + for _, in := range t.Inputs { + w.Fixed(in.ObjectID[:]) + w.U64(in.Version) + w.Fixed(in.ObjectHash[:]) + } + w.U32(uint32(len(t.Outputs))) + for _, out := range t.Outputs { + w.Bytes(out.CanonicalBytes()) + } + w.U32(uint32(len(t.Operations))) + for _, op := range t.Operations { + w.U16(op.Kind) + w.Bytes(op.Payload) + } + w.U64(t.Fee) + w.U64(t.ValidUntilHeight) + w.Fixed(t.Salt[:]) return w.BytesCopy() } -func (t Transaction) SigningDigest() types.Hash { - return types.Hash(codec.DomainHash("zephyr/transaction-signing/v2", t.IntentBytes())) +func (t Transaction) SigningDigest() [32]byte { + return codec.DomainHash("zephyr/transaction/signing/v2", t.IntentBytes()) } func (t Transaction) ID() types.Hash { - return types.Hash(codec.DomainHash("zephyr/transaction-id/v2", t.IntentBytes())) + return types.Hash(codec.DomainHash("zephyr/transaction/id/v2", t.IntentBytes())) } func (t Transaction) MarshalBinary() ([]byte, error) { @@ -199,8 +228,7 @@ func (t Transaction) ValidateStatic() error { return ErrSender } var zeroSalt [16]byte - if t.Salt == zeroSalt || len(t.Inputs) > MaxInputs || len(t.Outputs) > MaxOutputs || - len(t.Operations) == 0 || len(t.Operations) > MaxOperations { + if t.Salt == zeroSalt || len(t.Inputs) > MaxInputs || len(t.Outputs) > MaxOutputs || len(t.Operations) == 0 || len(t.Operations) > MaxOperations { return ErrStructure } seenInputs := map[types.ObjectID]struct{}{} @@ -230,7 +258,7 @@ func (t Transaction) ValidateAtHeight(height uint64) error { if err := t.ValidateStatic(); err != nil { return err } - if t.ValidUntilHeight != 0 && height > t.ValidUntilHeight { + if t.ValidUntilHeight > 0 && height > t.ValidUntilHeight { return ErrExpired } return nil @@ -244,64 +272,23 @@ func (t Transaction) VerifyForNetwork(network types.NetworkID) error { } func (t Transaction) VerifyWitnesses() error { - if len(t.Inputs) != len(t.Witnesses) { + if len(t.Witnesses) != len(t.Inputs) { return ErrWitness } - witnesses := make(map[types.ObjectID]Witness, len(t.Witnesses)) - for _, witness := range t.Witnesses { - if err := witness.Object.Validate(); err != nil { + for i, in := range t.Inputs { + witness := t.Witnesses[i] + if witness.Object.ID != in.ObjectID || witness.Object.Version != in.Version || witness.Object.Hash() != in.ObjectHash { return ErrWitness } - if _, exists := witnesses[witness.Object.ID]; exists { - return ErrWitness - } - witnesses[witness.Object.ID] = witness - } - for _, in := range t.Inputs { - witness, ok := witnesses[in.ObjectID] - if !ok || witness.Object.Version != in.Version || witness.Object.Hash() != in.ObjectHash || !witness.Proof.Exists { - return ErrWitness - } - key := types.Hash(in.ObjectID) - value := in.ObjectHash[:] - if !state.Verify(t.StateRoot, key, value, witness.Proof) { + hash := witness.Object.Hash() + if !state.Verify(t.StateRoot, types.Hash(in.ObjectID), hash[:], witness.Proof) { return ErrWitness } } return nil } -func writeIntent(w *codec.Writer, t Transaction) { - w.U16(t.Version) - w.Fixed(t.Network[:]) - w.Fixed(t.Sender[:]) - w.Bytes(t.SenderPublicKey) - w.U32(t.ShardID) - w.Fixed(t.StateRoot[:]) - w.Fixed(t.Salt[:]) - w.U32(uint32(len(t.Inputs))) - for _, in := range t.Inputs { - w.Fixed(in.ObjectID[:]) - w.U64(in.Version) - w.Fixed(in.ObjectHash[:]) - } - w.U32(uint32(len(t.Outputs))) - for _, out := range t.Outputs { - w.Bytes(out.CanonicalBytes()) - } - w.U32(uint32(len(t.Operations))) - for _, op := range t.Operations { - w.U16(op.Kind) - w.Bytes(op.Payload) - } - w.U64(t.Fee) - w.U64(t.ValidUntilHeight) -} - func parseIntent(data []byte) (Transaction, error) { - if len(data) == 0 || len(data) > MaxIntentBytes { - return Transaction{}, ErrWire - } r := codec.NewReader(data) version, err := r.U16() if err != nil { @@ -311,87 +298,78 @@ func parseIntent(data []byte) (Transaction, error) { if err != nil { return Transaction{}, ErrWire } - senderBytes, err := r.Fixed(32) - if err != nil { - return Transaction{}, ErrWire - } - publicKey, err := r.Bytes(65) + var network types.NetworkID + copy(network[:], networkBytes) + shardID, err := r.U32() if err != nil { return Transaction{}, ErrWire } - shardID, err := r.U32() + senderBytes, err := r.Fixed(32) if err != nil { return Transaction{}, ErrWire } - rootBytes, err := r.Fixed(32) + var sender types.AccountID + copy(sender[:], senderBytes) + publicKey, err := r.Bytes(65) if err != nil { return Transaction{}, ErrWire } - saltBytes, err := r.Fixed(16) + stateRootBytes, err := r.Fixed(32) if err != nil { return Transaction{}, ErrWire } + var stateRoot types.Hash + copy(stateRoot[:], stateRootBytes) inputCount, err := r.U32() if err != nil || inputCount > MaxInputs { return Transaction{}, ErrWire } - t := Transaction{ - Version: version, SenderPublicKey: publicKey, ShardID: shardID, - Inputs: make([]InputRef, int(inputCount)), - } - copy(t.Network[:], networkBytes) - copy(t.Sender[:], senderBytes) - copy(t.StateRoot[:], rootBytes) - copy(t.Salt[:], saltBytes) - - for i := range t.Inputs { - idBytes, err := r.Fixed(32) + inputs := make([]InputRef, int(inputCount)) + for i := range inputs { + id, err := r.Fixed(32) if err != nil { return Transaction{}, ErrWire } - version, err := r.U64() + copy(inputs[i].ObjectID[:], id) + inputs[i].Version, err = r.U64() if err != nil { return Transaction{}, ErrWire } - hashBytes, err := r.Fixed(32) + h, err := r.Fixed(32) if err != nil { return Transaction{}, ErrWire } - copy(t.Inputs[i].ObjectID[:], idBytes) - t.Inputs[i].Version = version - copy(t.Inputs[i].ObjectHash[:], hashBytes) + copy(inputs[i].ObjectHash[:], h) } outputCount, err := r.U32() if err != nil || outputCount > MaxOutputs { return Transaction{}, ErrWire } - t.Outputs = make([]object.OutputSpec, int(outputCount)) - for i := range t.Outputs { - outputBytes, err := r.Bytes(object.MaxObjectDataBytes + 64) + outputs := make([]object.OutputSpec, int(outputCount)) + for i := range outputs { + raw, err := r.Bytes(object.MaxObjectDataBytes + 64) if err != nil { return Transaction{}, ErrWire } - out, err := object.ParseOutputSpec(outputBytes) + outputs[i], err = object.ParseOutputSpec(raw) if err != nil { return Transaction{}, ErrWire } - t.Outputs[i] = out } opCount, err := r.U32() if err != nil || opCount == 0 || opCount > MaxOperations { return Transaction{}, ErrWire } - t.Operations = make([]Operation, int(opCount)) - for i := range t.Operations { - kind, err := r.U16() + operations := make([]Operation, int(opCount)) + for i := range operations { + operations[i].Kind, err = r.U16() if err != nil { return Transaction{}, ErrWire } - payload, err := r.Bytes(MaxOpPayload) + operations[i].Payload, err = r.Bytes(MaxOpPayload) if err != nil { return Transaction{}, ErrWire } - t.Operations[i] = Operation{Kind: kind, Payload: payload} } fee, err := r.U64() if err != nil { @@ -401,15 +379,16 @@ func parseIntent(data []byte) (Transaction, error) { if err != nil { return Transaction{}, ErrWire } - t.Fee = fee - t.ValidUntilHeight = validUntil - if err := r.Done(); err != nil { + salt, err := r.Fixed(16) + if err != nil || r.Done() != nil { return Transaction{}, ErrWire } - return t, nil + var saltArray [16]byte + copy(saltArray[:], salt) + return Transaction{Version: version, Network: network, ShardID: shardID, Sender: sender, SenderPublicKey: publicKey, StateRoot: stateRoot, Inputs: inputs, Outputs: outputs, Operations: operations, Fee: fee, ValidUntilHeight: validUntil, Salt: saltArray}, nil } -func verifySignature(publicKey *ecdsa.PublicKey, digest types.Hash, signature []byte) error { +func verifySignature(publicKey *ecdsa.PublicKey, digest [32]byte, signature []byte) error { if len(signature) != 64 { return ErrSignature } @@ -419,7 +398,8 @@ func verifySignature(publicKey *ecdsa.PublicKey, digest types.Hash, signature [] if r.Sign() <= 0 || s.Sign() <= 0 || r.Cmp(order) >= 0 || s.Cmp(order) >= 0 { return ErrSignature } - if s.Cmp(halfOrder()) > 0 { + half := new(big.Int).Rsh(new(big.Int).Set(order), 1) + if s.Cmp(half) > 0 { return ErrCanonicalSig } if !ecdsa.Verify(publicKey, digest[:], r, s) { @@ -429,23 +409,17 @@ func verifySignature(publicKey *ecdsa.PublicKey, digest types.Hash, signature [] } func normalizeLowS(s *big.Int) *big.Int { - if s.Cmp(halfOrder()) <= 0 { - return new(big.Int).Set(s) + order := elliptic.P256().Params().N + half := new(big.Int).Rsh(new(big.Int).Set(order), 1) + if s.Cmp(half) > 0 { + return new(big.Int).Sub(order, s) } - return new(big.Int).Sub(elliptic.P256().Params().N, s) -} - -func halfOrder() *big.Int { - return new(big.Int).Rsh(new(big.Int).Set(elliptic.P256().Params().N), 1) + return s } func pad32(v *big.Int) []byte { - raw := v.Bytes() out := make([]byte, 32) - if len(raw) >= 32 { - copy(out, raw[len(raw)-32:]) - } else { - copy(out[32-len(raw):], raw) - } + raw := v.Bytes() + copy(out[32-len(raw):], raw) return out } From 2fa4d79f467c8bac01129259220b3a8f1338f75e Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:30:48 +0200 Subject: [PATCH 103/274] prove cross-shard compute escrow lifecycle end to end --- .../v2/execution/compute_lifecycle_test.go | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 internal/v2/execution/compute_lifecycle_test.go diff --git a/internal/v2/execution/compute_lifecycle_test.go b/internal/v2/execution/compute_lifecycle_test.go new file mode 100644 index 00000000..90a6286d --- /dev/null +++ b/internal/v2/execution/compute_lifecycle_test.go @@ -0,0 +1,245 @@ +package execution + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestCrossShardComputeEscrowAssignmentResultAndSettlement(t *testing.T) { + const shardCount = uint32(2) + networkID := types.NetworkID(types.HashBytes("network", []byte("compute-lifecycle"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + ownerKey, owner := computeAccountOnShard(t, 1, shardCount) + providerKey1, provider1 := computeAccountOnShard(t, 0, shardCount) + providerKey2, provider2 := computeAccountOnShard(t, 0, shardCount) + ownerStore := worldstate.NewMemory() + providerStores := []*worldstate.Memory{worldstate.NewMemory(), worldstate.NewMemory()} + + ownerCoin := seedComputeCoin(t, ownerStore, owner, native, 300, "owner") + providerCoins := []types.ObjectID{ + seedComputeCoin(t, providerStores[0], provider1, native, 200, "provider-1"), + seedComputeCoin(t, providerStores[1], provider2, native, 200, "provider-2"), + } + providers := []types.AccountID{provider1, provider2} + providerKeys := []*ecdsa.PrivateKey{providerKey1, providerKey2} + resources := compute.Resources{CPUCores: 8, MemoryMiB: 16384, GPUCount: 1, GPUMemoryMiB: 12288, StorageMiB: 1024, BandwidthMbps: 100, Capabilities: []string{"render"}} + job := compute.Job{Owner: owner, WorkloadHash: types.HashBytes("workload", []byte("scene")), InputRoot: types.HashBytes("input", []byte("scene-data")), Resources: resources, MaxPrice: 100, CollateralRequired: 20, Verification: compute.VerificationReplicated, DeadlineHeight: 100, Replicas: 2} + jobPayload, err := job.MarshalBinary() + if err != nil { + t.Fatal(err) + } + jobTx := computeSignedTx(t, ownerStore, ownerKey, networkID, native, 1, []types.ObjectID{ownerCoin}, 199, tx.OpComputeJob, jobPayload, 1) + jobResult, err := (Engine{Network: networkID, NativeToken: native, ShardCount: shardCount, Height: 1}).Execute(jobTx) + if err != nil { + t.Fatal(err) + } + if _, err := ownerStore.Apply(jobResult.Consumed, jobResult.Created); err != nil { + t.Fatal(err) + } + jobObject := objectOfKind(t, jobResult.Created, object.KindComputeJob) + ownerCoin = objectOfKind(t, jobResult.Created, object.KindCoin).ID + record, err := compute.ParseOnChainJob(jobObject.Data) + if err != nil { + t.Fatal(err) + } + + assignmentObjects := make([]object.Object, 0, 2) + for i := range providers { + offer := compute.Offer{Provider: providers[i], Resources: resources, PricePerUnit: 10, Collateral: 20, Verification: []compute.VerificationMode{compute.VerificationReplicated}, ValidUntilHeight: 80} + offerPayload, _ := offer.MarshalBinary() + offerTx := computeSignedTx(t, providerStores[i], providerKeys[i], networkID, native, 0, []types.ObjectID{providerCoins[i]}, 179, tx.OpComputeOffer, offerPayload, byte(10+i)) + offerResult, err := (Engine{Network: networkID, NativeToken: native, ShardCount: shardCount, Height: 2}).Execute(offerTx) + if err != nil { + t.Fatal(err) + } + if _, err := providerStores[i].Apply(offerResult.Consumed, offerResult.Created); err != nil { + t.Fatal(err) + } + offerObject := objectOfKind(t, offerResult.Created, object.KindComputeOffer) + providerCoins[i] = objectOfKind(t, offerResult.Created, object.KindCoin).ID + message := compute.AssignmentMessage{JobID: record.ID, JobOwner: owner, JobShard: 1, OfferID: types.Hash(offerObject.ID), Offer: offer, Job: job} + messageRaw, _ := message.MarshalBinary() + acceptTx := computeSignedTx(t, providerStores[i], providerKeys[i], networkID, native, 0, []types.ObjectID{offerObject.ID, providerCoins[i]}, 178, tx.OpComputeAccept, messageRaw, byte(20+i)) + acceptResult, err := (Engine{Network: networkID, NativeToken: native, ShardCount: shardCount, Height: 3}).Execute(acceptTx) + if err != nil { + t.Fatal(err) + } + if len(acceptResult.Outbound) != 1 || acceptResult.Outbound[0].DestinationShard != 1 || acceptResult.Outbound[0].Output.Kind != object.KindComputeAssignment { + t.Fatalf("assignment not routed cross-shard: %+v", acceptResult.Outbound) + } + if _, err := providerStores[i].Apply(acceptResult.Consumed, acceptResult.Created); err != nil { + t.Fatal(err) + } + providerCoins[i] = objectOfKind(t, acceptResult.Created, object.KindCoin).ID + outbound := acceptResult.Outbound[0] + assignmentObjects = append(assignmentObjects, object.Object{ID: types.ObjectIDForShard(acceptResult.TxID, outbound.OutputIndex, 1), Version: 1, Owner: outbound.Output.Owner, Kind: outbound.Output.Kind, Data: outbound.Output.Data}) + } + if _, err := ownerStore.Apply(nil, assignmentObjects); err != nil { + t.Fatal(err) + } + + for i, assignment := range assignmentObjects { + refRaw, _ := (compute.IngestRef{JobObject: jobObject.ID, MessageObject: assignment.ID}).MarshalBinary() + ingestTx := computeSignedTx(t, ownerStore, ownerKey, networkID, native, 1, []types.ObjectID{jobObject.ID, assignment.ID, ownerCoin}, uint64(198-i), tx.OpComputeIngestAssignment, refRaw, byte(30+i)) + ingestResult, err := (Engine{Network: networkID, NativeToken: native, ShardCount: shardCount, Height: uint64(4 + i)}).Execute(ingestTx) + if err != nil { + t.Fatal(err) + } + if _, err := ownerStore.Apply(ingestResult.Consumed, ingestResult.Created); err != nil { + t.Fatal(err) + } + jobObject = objectOfKind(t, ingestResult.Created, object.KindComputeJob) + ownerCoin = objectOfKind(t, ingestResult.Created, object.KindCoin).ID + } + record, err = compute.ParseOnChainJob(jobObject.Data) + if err != nil || record.Status != compute.JobAssigned || len(record.Assignments) != 2 { + t.Fatalf("job not fully assigned: %+v %v", record, err) + } + + resultRoot := types.HashBytes("result", []byte("rendered-scene")) + resultObjects := make([]object.Object, 0, 2) + for i := range providers { + message := compute.ResultMessage{JobID: record.ID, JobOwner: owner, JobShard: 1, Result: compute.Result{JobID: record.ID, Provider: providers[i], ResultRoot: resultRoot, CompletedHeight: uint64(10 + i)}} + messageRaw, _ := message.MarshalBinary() + resultTx := computeSignedTx(t, providerStores[i], providerKeys[i], networkID, native, 0, []types.ObjectID{providerCoins[i]}, 177, tx.OpComputeResult, messageRaw, byte(40+i)) + result, err := (Engine{Network: networkID, NativeToken: native, ShardCount: shardCount, Height: uint64(10 + i)}).Execute(resultTx) + if err != nil { + t.Fatal(err) + } + if len(result.Outbound) != 1 || result.Outbound[0].Output.Kind != object.KindComputeResult { + t.Fatal("compute result not routed to job shard") + } + if _, err := providerStores[i].Apply(result.Consumed, result.Created); err != nil { + t.Fatal(err) + } + providerCoins[i] = objectOfKind(t, result.Created, object.KindCoin).ID + outbound := result.Outbound[0] + resultObjects = append(resultObjects, object.Object{ID: types.ObjectIDForShard(result.TxID, outbound.OutputIndex, 1), Version: 1, Owner: outbound.Output.Owner, Kind: outbound.Output.Kind, Data: outbound.Output.Data}) + } + if _, err := ownerStore.Apply(nil, resultObjects); err != nil { + t.Fatal(err) + } + for i, resultObject := range resultObjects { + refRaw, _ := (compute.IngestRef{JobObject: jobObject.ID, MessageObject: resultObject.ID}).MarshalBinary() + ingestTx := computeSignedTx(t, ownerStore, ownerKey, networkID, native, 1, []types.ObjectID{jobObject.ID, resultObject.ID, ownerCoin}, uint64(196-i), tx.OpComputeIngestResult, refRaw, byte(50+i)) + ingestResult, err := (Engine{Network: networkID, NativeToken: native, ShardCount: shardCount, Height: uint64(20 + i)}).Execute(ingestTx) + if err != nil { + t.Fatal(err) + } + if _, err := ownerStore.Apply(ingestResult.Consumed, ingestResult.Created); err != nil { + t.Fatal(err) + } + jobObject = objectOfKind(t, ingestResult.Created, object.KindComputeJob) + ownerCoin = objectOfKind(t, ingestResult.Created, object.KindCoin).ID + } + record, err = compute.ParseOnChainJob(jobObject.Data) + if err != nil || record.Status != compute.JobAwaitingVerification || len(record.Results) != 2 { + t.Fatalf("results not ready for verification: %+v %v", record, err) + } + jobRef, _ := (compute.JobRef{JobObject: jobObject.ID}).MarshalBinary() + finalTx := computeSignedTx(t, ownerStore, ownerKey, networkID, native, 1, []types.ObjectID{jobObject.ID, ownerCoin}, 194, tx.OpComputeFinalize, jobRef, 60) + finalResult, err := (Engine{Network: networkID, NativeToken: native, ShardCount: shardCount, Height: 30}).Execute(finalTx) + if err != nil { + t.Fatal(err) + } + if len(finalResult.Outbound) != 2 { + t.Fatalf("expected two provider payouts, got %d", len(finalResult.Outbound)) + } + var ownerRefund uint64 + for _, item := range finalResult.Created { + if item.Kind == object.KindCoin && item.Owner == owner { + coin, _ := object.ParseCoin(item.Data) + ownerRefund += coin.Amount + } + } + // 194 fee-change + 80 unused escrow refund. + if ownerRefund != 274 { + t.Fatalf("unexpected owner value after settlement: %d", ownerRefund) + } + for _, outbound := range finalResult.Outbound { + coin, err := object.ParseCoin(outbound.Output.Data) + if err != nil || coin.Amount != 30 { + t.Fatalf("provider payment+collateral must equal 30: %+v %v", outbound, err) + } + } +} + +func computeAccountOnShard(t *testing.T, shard, count uint32) (*ecdsa.PrivateKey, types.AccountID) { + t.Helper() + for i := 0; i < 10000; i++ { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + account := types.AccountIDFromPublicKey(pub) + if types.AccountShard(account, count) == shard { + return key, account + } + } + t.Fatal("failed to find account on requested shard") + return nil, types.AccountID{} +} + +func seedComputeCoin(t *testing.T, store *worldstate.Memory, owner types.AccountID, token types.TokenID, amount uint64, label string) types.ObjectID { + t.Helper() + spec, err := object.NewCoinOutput(owner, token, amount) + if err != nil { + t.Fatal(err) + } + id := types.ObjectIDForShard(types.HashBytes("seed", []byte(label)), 0, types.AccountShard(owner, 2)) + if _, err := store.Apply(nil, []object.Object{{ID: id, Version: 1, Owner: owner, Kind: spec.Kind, Data: spec.Data}}); err != nil { + t.Fatal(err) + } + return id +} + +func computeSignedTx(t *testing.T, store *worldstate.Memory, key *ecdsa.PrivateKey, networkID types.NetworkID, native types.TokenID, shard uint32, inputIDs []types.ObjectID, change uint64, op uint16, payload []byte, salt byte) tx.Transaction { + t.Helper() + inputs := make([]tx.InputRef, 0, len(inputIDs)) + witnesses := make([]tx.Witness, 0, len(inputIDs)) + for _, id := range inputIDs { + item, proof, ok := store.Proof(id) + if !ok { + t.Fatalf("missing input %s", id) + } + h := item.Hash() + inputs = append(inputs, tx.InputRef{ObjectID: id, Version: item.Version, ObjectHash: h}) + witnesses = append(witnesses, tx.Witness{Object: item, Proof: proof}) + } + pub := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + owner := types.AccountIDFromPublicKey(pub) + outputs := []object.OutputSpec{} + if change > 0 { + spec, err := object.NewCoinOutput(owner, native, change) + if err != nil { + t.Fatal(err) + } + outputs = append(outputs, spec) + } + transaction := tx.Transaction{Version: tx.Version, Network: networkID, ShardID: shard, StateRoot: store.Root(), Inputs: inputs, Outputs: outputs, Operations: []tx.Operation{{Kind: op, Payload: payload}}, Fee: 1, ValidUntilHeight: 200, Witnesses: witnesses} + transaction.Salt[0] = salt + if err := transaction.Sign(key); err != nil { + t.Fatal(err) + } + return transaction +} + +func objectOfKind(t *testing.T, objects []object.Object, kind object.Kind) object.Object { + t.Helper() + for _, item := range objects { + if item.Kind == kind { + return item + } + } + t.Fatalf("object kind %d not found", kind) + return object.Object{} +} From 12579d0e8380b7504e1c8447a972cdf6375a8f50 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:31:44 +0200 Subject: [PATCH 104/274] add shard-scoped GossipSub dissemination for v2 --- internal/v2/network/p2p/gossip.go | 154 ++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 internal/v2/network/p2p/gossip.go diff --git a/internal/v2/network/p2p/gossip.go b/internal/v2/network/p2p/gossip.go new file mode 100644 index 00000000..9a91c70d --- /dev/null +++ b/internal/v2/network/p2p/gossip.go @@ -0,0 +1,154 @@ +package p2p + +import ( + "context" + "errors" + "fmt" + + pubsub "github.com/libp2p/go-libp2p-pubsub" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const DefaultMaxGossipBytes = 4 << 20 + +var ( + ErrGossipConfig = errors.New("invalid Zephyr GossipSub configuration") + ErrGossipPayload = errors.New("Zephyr GossipSub payload exceeds limit") +) + +type Gossip struct { + ps *pubsub.PubSub + network types.NetworkID + maxPayload int +} + +type ShardSubscription struct { + Shard uint32 + Tx *pubsub.Subscription + DA *pubsub.Subscription + tx *pubsub.Topic + da *pubsub.Topic +} + +func NewGossip(ctx context.Context, node *Node, maxPayload int) (*Gossip, error) { + if node == nil || node.host == nil || types.IsZero32([32]byte(node.networkID)) { + return nil, ErrGossipConfig + } + if maxPayload <= 0 { + maxPayload = DefaultMaxGossipBytes + } + ps, err := pubsub.NewGossipSub(ctx, node.host, pubsub.WithMaxMessageSize(maxPayload), pubsub.WithFloodPublish(true)) + if err != nil { + return nil, err + } + return &Gossip{ps: ps, network: node.networkID, maxPayload: maxPayload}, nil +} + +func (g *Gossip) JoinShard(shard uint32) (*ShardSubscription, error) { + if g == nil || g.ps == nil { + return nil, ErrGossipConfig + } + txTopic, err := g.ps.Join(g.topic(shard, "tx")) + if err != nil { + return nil, err + } + daTopic, err := g.ps.Join(g.topic(shard, "da")) + if err != nil { + _ = txTopic.Close() + return nil, err + } + txSub, err := txTopic.Subscribe() + if err != nil { + _ = txTopic.Close() + _ = daTopic.Close() + return nil, err + } + daSub, err := daTopic.Subscribe() + if err != nil { + txSub.Cancel() + _ = txTopic.Close() + _ = daTopic.Close() + return nil, err + } + return &ShardSubscription{Shard: shard, Tx: txSub, DA: daSub, tx: txTopic, da: daTopic}, nil +} + +func (s *ShardSubscription) Close() error { + if s == nil { + return nil + } + if s.Tx != nil { + s.Tx.Cancel() + } + if s.DA != nil { + s.DA.Cancel() + } + var first error + if s.tx != nil { + first = s.tx.Close() + } + if s.da != nil { + if err := s.da.Close(); first == nil { + first = err + } + } + return first +} + +func (g *Gossip) PublishTransaction(ctx context.Context, subscription *ShardSubscription, payload []byte) error { + if subscription == nil || subscription.tx == nil { + return ErrGossipConfig + } + return g.publish(ctx, subscription.tx, payload) +} + +func (g *Gossip) PublishDA(ctx context.Context, subscription *ShardSubscription, payload []byte) error { + if subscription == nil || subscription.da == nil { + return ErrGossipConfig + } + return g.publish(ctx, subscription.da, payload) +} + +func (g *Gossip) NextTransaction(ctx context.Context, subscription *ShardSubscription) ([]byte, error) { + if subscription == nil || subscription.Tx == nil { + return nil, ErrGossipConfig + } + return g.next(ctx, subscription.Tx) +} + +func (g *Gossip) NextDA(ctx context.Context, subscription *ShardSubscription) ([]byte, error) { + if subscription == nil || subscription.DA == nil { + return nil, ErrGossipConfig + } + return g.next(ctx, subscription.DA) +} + +func (g *Gossip) publish(ctx context.Context, topic *pubsub.Topic, payload []byte) error { + if len(payload) > g.maxPayload { + return ErrGossipPayload + } + return topic.Publish(ctx, payload) +} + +func (g *Gossip) next(ctx context.Context, subscription *pubsub.Subscription) ([]byte, error) { + message, err := subscription.Next(ctx) + if err != nil { + return nil, err + } + if len(message.Data) > g.maxPayload { + return nil, ErrGossipPayload + } + return append([]byte(nil), message.Data...), nil +} + +func (g *Gossip) topic(shard uint32, kind string) string { + return fmt.Sprintf("/zephyr/%s/v2/shard/%d/%s", g.network.String(), shard, kind) +} + +func GossipTopic(network types.NetworkID, shard uint32, kind string) (string, error) { + if types.IsZero32([32]byte(network)) || (kind != "tx" && kind != "da") { + return "", ErrGossipConfig + } + return fmt.Sprintf("/zephyr/%s/v2/shard/%d/%s", network.String(), shard, kind), nil +} From 9405a53796169e8bc61dabedfd3a65eb148c9f3c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:31:58 +0200 Subject: [PATCH 105/274] test network and shard scoped gossip topics --- internal/v2/network/p2p/gossip_test.go | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 internal/v2/network/p2p/gossip_test.go diff --git a/internal/v2/network/p2p/gossip_test.go b/internal/v2/network/p2p/gossip_test.go new file mode 100644 index 00000000..da7629d0 --- /dev/null +++ b/internal/v2/network/p2p/gossip_test.go @@ -0,0 +1,29 @@ +package p2p + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestGossipTopicsAreNetworkAndShardScoped(t *testing.T) { + a := types.NetworkID(types.HashBytes("network", []byte("a"))) + b := types.NetworkID(types.HashBytes("network", []byte("b"))) + a7, err := GossipTopic(a, 7, "tx") + if err != nil { + t.Fatal(err) + } + a8, _ := GossipTopic(a, 8, "tx") + b7, _ := GossipTopic(b, 7, "tx") + da7, _ := GossipTopic(a, 7, "da") + if a7 == a8 || a7 == b7 || a7 == da7 { + t.Fatal("gossip topic domains are not separated") + } +} + +func TestGossipTopicRejectsUnknownKind(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("topic-reject"))) + if _, err := GossipTopic(network, 0, "everything"); err != ErrGossipConfig { + t.Fatalf("expected gossip config error, got %v", err) + } +} From e2f66ff77bdf77d851ec0e3968f994a1fa149ef6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:32:19 +0000 Subject: [PATCH 106/274] lock exact Zephyr v2 Go dependency graph --- go.mod | 2 ++ go.sum | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/go.mod b/go.mod index 53b2d00c..411d2da0 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.26.0 require ( github.com/klauspost/reedsolomon v1.14.1 github.com/libp2p/go-libp2p v0.49.0 + github.com/libp2p/go-libp2p-pubsub v0.17.0 github.com/multiformats/go-multiaddr v0.16.1 go.starlark.net v0.0.0-20260708150628-5395d018f003 ) @@ -22,6 +23,7 @@ require ( github.com/flynn/noise v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/ipfs/go-cid v0.6.2 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect diff --git a/go.sum b/go.sum index 1ff74297..802f63ed 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= @@ -59,6 +61,8 @@ github.com/libp2p/go-libp2p v0.49.0 h1:ibXuYPIHmMIPShob1BktQvSuFQkq/MemhQOLKfGuj github.com/libp2p/go-libp2p v0.49.0/go.mod h1:lzjVcOBk5fCn1QD2XbSOKLZesB6gEsry8SLjCsAAGT4= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-pubsub v0.17.0 h1:SNdvB6V0eYMXLRR95n+4vpxJKbFsbHhgjPdDiTpGoo0= +github.com/libp2p/go-libp2p-pubsub v0.17.0/go.mod h1:F0oKCGLFJNy9b0TyRi04b+LchEzq0t2eZyJuxwAIyDE= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= From 50140df326d706098a605ec5eda04fdb9e1edbda Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:33:16 +0200 Subject: [PATCH 107/274] add self-verifying Citizen Node wallet panel --- apps/wallet/src/components/CitizenPanel.vue | 186 ++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 apps/wallet/src/components/CitizenPanel.vue diff --git a/apps/wallet/src/components/CitizenPanel.vue b/apps/wallet/src/components/CitizenPanel.vue new file mode 100644 index 00000000..c73db541 --- /dev/null +++ b/apps/wallet/src/components/CitizenPanel.vue @@ -0,0 +1,186 @@ + + + + + From bfa3af43d4f4d490fc1be21c0dcee3350ec3d9d9 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:33:28 +0200 Subject: [PATCH 108/274] mount Citizen Node verification panel in wallet --- apps/wallet/src/main.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/wallet/src/main.ts b/apps/wallet/src/main.ts index cdc0ba5a..be4450da 100644 --- a/apps/wallet/src/main.ts +++ b/apps/wallet/src/main.ts @@ -1,6 +1,11 @@ import { createApp } from 'vue' import App from './App.vue' +import CitizenPanel from './components/CitizenPanel.vue' import './style.css' createApp(App).mount('#app') +const citizenHost = document.createElement('div') +citizenHost.id = 'zephyr-citizen-node' +document.body.appendChild(citizenHost) +createApp(CitizenPanel, { apiBase: import.meta.env.VITE_ZEPHYR_API_BASE ?? '' }).mount(citizenHost) From 16faa803c01efdb37ad86f50996f95171bb8b3ea Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:34:24 +0200 Subject: [PATCH 109/274] add dedicated compute provider stream protocol --- internal/v2/network/p2p/compute.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 internal/v2/network/p2p/compute.go diff --git a/internal/v2/network/p2p/compute.go b/internal/v2/network/p2p/compute.go new file mode 100644 index 00000000..afda5db8 --- /dev/null +++ b/internal/v2/network/p2p/compute.go @@ -0,0 +1,21 @@ +package p2p + +import ( + "context" + "fmt" + + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" +) + +func (n *Node) ComputeProtocol() protocol.ID { + return protocol.ID(fmt.Sprintf("/zephyr/%s/v2/compute", n.networkID.String())) +} + +func (n *Node) SetComputeHandler(handler Handler) { + n.install(n.ComputeProtocol(), handler) +} + +func (n *Node) SendCompute(ctx context.Context, remote peer.ID, payload []byte) ([]byte, error) { + return n.request(ctx, remote, n.ComputeProtocol(), payload) +} From d4f08c8c967a93f7267a9baff4460782300c5c51 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:34:53 +0200 Subject: [PATCH 110/274] add safe content-addressed compute provider service --- internal/v2/provider/service.go | 267 ++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 internal/v2/provider/service.go diff --git a/internal/v2/provider/service.go b/internal/v2/provider/service.go new file mode 100644 index 00000000..cd25b67e --- /dev/null +++ b/internal/v2/provider/service.go @@ -0,0 +1,267 @@ +package provider + +import ( + "context" + "crypto/sha256" + "errors" + "strings" + "sync" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + MaxCapabilityBytes = 64 + MaxInlineInput = 1 << 20 + MaxParameters = 1 << 20 + MaxInlineOutput = 1 << 20 +) + +var ( + ErrRequest = errors.New("invalid compute provider request") + ErrExecutor = errors.New("compute executor is unavailable") + ErrInputRoot = errors.New("compute input does not match declared root") + ErrOutput = errors.New("invalid compute provider output") + ErrDuplicateExec = errors.New("duplicate compute executor capability") +) + +type Request struct { + JobID types.JobID + WorkloadHash types.Hash + InputRoot types.Hash + Capability string + Input []byte + Parameters []byte +} + +type Response struct { + JobID types.JobID + ResultRoot types.Hash + Output []byte + Stored bool +} + +type Executor interface { + Capability() string + Execute(context.Context, []byte, []byte) ([]byte, error) +} + +type Store interface { + Put(types.Hash, []byte) error + Get(types.Hash) ([]byte, error) +} + +type Service struct { + mu sync.RWMutex + executors map[string]Executor + store Store +} + +func New(store Store, executors ...Executor) (*Service, error) { + if store == nil { + return nil, ErrRequest + } + s := &Service{executors: make(map[string]Executor), store: store} + for _, executor := range executors { + if err := s.Register(executor); err != nil { + return nil, err + } + } + return s, nil +} + +func (s *Service) Register(executor Executor) error { + if executor == nil { + return ErrExecutor + } + capability := strings.TrimSpace(executor.Capability()) + if capability == "" || len(capability) > MaxCapabilityBytes { + return ErrExecutor + } + s.mu.Lock() + defer s.mu.Unlock() + if _, exists := s.executors[capability]; exists { + return ErrDuplicateExec + } + s.executors[capability] = executor + return nil +} + +func (s *Service) Handle(ctx context.Context, payload []byte) ([]byte, error) { + request, err := ParseRequest(payload) + if err != nil { + return nil, err + } + response, err := s.Execute(ctx, request) + if err != nil { + return nil, err + } + return response.MarshalBinary() +} + +func (s *Service) Execute(ctx context.Context, request Request) (Response, error) { + if err := request.Validate(); err != nil { + return Response{}, err + } + input := request.Input + if len(input) == 0 { + var err error + input, err = s.store.Get(request.InputRoot) + if err != nil { + return Response{}, ErrInputRoot + } + } + if InputRoot(input) != request.InputRoot { + return Response{}, ErrInputRoot + } + s.mu.RLock() + executor := s.executors[request.Capability] + s.mu.RUnlock() + if executor == nil { + return Response{}, ErrExecutor + } + output, err := executor.Execute(ctx, append([]byte(nil), input...), append([]byte(nil), request.Parameters...)) + if err != nil { + return Response{}, err + } + root := ResultRoot(output) + if err := s.store.Put(root, output); err != nil { + return Response{}, err + } + response := Response{JobID: request.JobID, ResultRoot: root, Stored: true} + if len(output) <= MaxInlineOutput { + response.Output = append([]byte(nil), output...) + } + return response, nil +} + +func (r Request) Validate() error { + capability := strings.TrimSpace(r.Capability) + if types.IsZero32([32]byte(r.JobID)) || types.IsZero32([32]byte(r.WorkloadHash)) || types.IsZero32([32]byte(r.InputRoot)) || capability == "" || len(capability) > MaxCapabilityBytes || len(r.Input) > MaxInlineInput || len(r.Parameters) > MaxParameters { + return ErrRequest + } + if len(r.Input) > 0 && InputRoot(r.Input) != r.InputRoot { + return ErrInputRoot + } + return nil +} + +func (r Request) MarshalBinary() ([]byte, error) { + if err := r.Validate(); err != nil { + return nil, err + } + var w codec.Writer + w.Fixed(r.JobID[:]) + w.Fixed(r.WorkloadHash[:]) + w.Fixed(r.InputRoot[:]) + w.String(strings.TrimSpace(r.Capability)) + w.Bytes(r.Input) + w.Bytes(r.Parameters) + return w.BytesCopy(), nil +} + +func ParseRequest(data []byte) (Request, error) { + r := codec.NewReader(data) + jobRaw, err := r.Fixed(32) + if err != nil { + return Request{}, ErrRequest + } + workloadRaw, err := r.Fixed(32) + if err != nil { + return Request{}, ErrRequest + } + inputRootRaw, err := r.Fixed(32) + if err != nil { + return Request{}, ErrRequest + } + capability, err := r.String(MaxCapabilityBytes) + if err != nil { + return Request{}, ErrRequest + } + input, err := r.Bytes(MaxInlineInput) + if err != nil { + return Request{}, ErrRequest + } + parameters, err := r.Bytes(MaxParameters) + if err != nil || r.Done() != nil { + return Request{}, ErrRequest + } + var jobID types.JobID + var workload, inputRoot types.Hash + copy(jobID[:], jobRaw) + copy(workload[:], workloadRaw) + copy(inputRoot[:], inputRootRaw) + request := Request{JobID: jobID, WorkloadHash: workload, InputRoot: inputRoot, Capability: capability, Input: input, Parameters: parameters} + if err := request.Validate(); err != nil { + return Request{}, err + } + return request, nil +} + +func (r Response) MarshalBinary() ([]byte, error) { + if types.IsZero32([32]byte(r.JobID)) || types.IsZero32([32]byte(r.ResultRoot)) || len(r.Output) > MaxInlineOutput { + return nil, ErrOutput + } + if len(r.Output) > 0 && ResultRoot(r.Output) != r.ResultRoot { + return nil, ErrOutput + } + var w codec.Writer + w.Fixed(r.JobID[:]) + w.Fixed(r.ResultRoot[:]) + w.Bool(r.Stored) + w.Bytes(r.Output) + return w.BytesCopy(), nil +} + +func ParseResponse(data []byte) (Response, error) { + r := codec.NewReader(data) + jobRaw, err := r.Fixed(32) + if err != nil { + return Response{}, ErrOutput + } + rootRaw, err := r.Fixed(32) + if err != nil { + return Response{}, ErrOutput + } + stored, err := r.Bool() + if err != nil { + return Response{}, ErrOutput + } + output, err := r.Bytes(MaxInlineOutput) + if err != nil || r.Done() != nil { + return Response{}, ErrOutput + } + var jobID types.JobID + var resultRoot types.Hash + copy(jobID[:], jobRaw) + copy(resultRoot[:], rootRaw) + response := Response{JobID: jobID, ResultRoot: resultRoot, Stored: stored, Output: output} + if _, err := response.MarshalBinary(); err != nil { + return Response{}, err + } + return response, nil +} + +func InputRoot(input []byte) types.Hash { + return types.Hash(codec.DomainHash("zephyr/compute/input/v2", input)) +} + +func ResultRoot(output []byte) types.Hash { + return types.Hash(codec.DomainHash("zephyr/compute/result/v2", output)) +} + +type HashExecutor struct{} + +func (HashExecutor) Capability() string { return "sha256" } +func (HashExecutor) Execute(_ context.Context, input, _ []byte) ([]byte, error) { + hash := sha256.Sum256(input) + return hash[:], nil +} + +type IdentityExecutor struct{} + +func (IdentityExecutor) Capability() string { return "identity" } +func (IdentityExecutor) Execute(_ context.Context, input, _ []byte) ([]byte, error) { + return append([]byte(nil), input...), nil +} From 1736fe77339528f4cea4dce9f8703b3bf7db8fa5 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:35:07 +0200 Subject: [PATCH 111/274] add atomic content-addressed provider store --- internal/v2/provider/store.go | 59 +++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 internal/v2/provider/store.go diff --git a/internal/v2/provider/store.go b/internal/v2/provider/store.go new file mode 100644 index 00000000..b80b00df --- /dev/null +++ b/internal/v2/provider/store.go @@ -0,0 +1,59 @@ +package provider + +import ( + "errors" + "os" + "path/filepath" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrStore = errors.New("compute provider content store error") + +type DiskStore struct{ Dir string } + +func (s DiskStore) Put(root types.Hash, data []byte) error { + if s.Dir == "" || types.IsZero32([32]byte(root)) || ResultRoot(data) != root && InputRoot(data) != root { + return ErrStore + } + if err := os.MkdirAll(s.Dir, 0o700); err != nil { + return err + } + path := filepath.Join(s.Dir, root.String()) + tmp, err := os.CreateTemp(s.Dir, ".zephyr-provider-*") + if err != nil { + return err + } + tmpName := tmp.Name() + defer os.Remove(tmpName) + if err := tmp.Chmod(0o600); err != nil { + tmp.Close() + return err + } + if _, err := tmp.Write(data); err != nil { + tmp.Close() + return err + } + if err := tmp.Sync(); err != nil { + tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + return os.Rename(tmpName, path) +} + +func (s DiskStore) Get(root types.Hash) ([]byte, error) { + if s.Dir == "" || types.IsZero32([32]byte(root)) { + return nil, ErrStore + } + data, err := os.ReadFile(filepath.Join(s.Dir, root.String())) + if err != nil { + return nil, err + } + if InputRoot(data) != root && ResultRoot(data) != root { + return nil, ErrStore + } + return data, nil +} From b9591770bd357a00013ba12fff3bbaad39721954 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:35:22 +0200 Subject: [PATCH 112/274] test safe content-addressed compute provider execution --- internal/v2/provider/service_test.go | 57 ++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 internal/v2/provider/service_test.go diff --git a/internal/v2/provider/service_test.go b/internal/v2/provider/service_test.go new file mode 100644 index 00000000..cf8473b0 --- /dev/null +++ b/internal/v2/provider/service_test.go @@ -0,0 +1,57 @@ +package provider + +import ( + "context" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestProviderExecutesRegisteredCapabilityAndCommitsResult(t *testing.T) { + store := DiskStore{Dir: t.TempDir()} + service, err := New(store, HashExecutor{}, IdentityExecutor{}) + if err != nil { + t.Fatal(err) + } + input := []byte("scientific-work-unit") + request := Request{ + JobID: types.JobID(types.HashBytes("job", []byte("provider"))), + WorkloadHash: types.HashBytes("workload", []byte("sha256")), + InputRoot: InputRoot(input), Capability: "sha256", Input: input, + } + wire, err := request.MarshalBinary() + if err != nil { + t.Fatal(err) + } + responseWire, err := service.Handle(context.Background(), wire) + if err != nil { + t.Fatal(err) + } + response, err := ParseResponse(responseWire) + if err != nil { + t.Fatal(err) + } + if response.JobID != request.JobID || !response.Stored || ResultRoot(response.Output) != response.ResultRoot { + t.Fatalf("unexpected provider response: %+v", response) + } + stored, err := store.Get(response.ResultRoot) + if err != nil || ResultRoot(stored) != response.ResultRoot { + t.Fatal("content-addressed result was not persisted") + } +} + +func TestProviderRejectsUnknownExecutorAndTamperedInput(t *testing.T) { + service, err := New(DiskStore{Dir: t.TempDir()}, HashExecutor{}) + if err != nil { + t.Fatal(err) + } + base := Request{JobID: types.JobID(types.HashBytes("job", []byte("reject"))), WorkloadHash: types.HashBytes("workload", []byte("reject")), InputRoot: InputRoot([]byte("good")), Capability: "unknown", Input: []byte("good")} + if _, err := service.Execute(context.Background(), base); err != ErrExecutor { + t.Fatalf("expected unknown executor rejection, got %v", err) + } + base.Capability = "sha256" + base.Input = []byte("tampered") + if _, err := service.Execute(context.Background(), base); err != ErrInputRoot { + t.Fatalf("expected input-root rejection, got %v", err) + } +} From 87706e852bf092d99a47fbe80cf4fced3c28b6c6 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:35:43 +0200 Subject: [PATCH 113/274] add Zephyr v2 compute provider daemon --- cmd/compute-provider/main.go | 78 ++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 cmd/compute-provider/main.go diff --git a/cmd/compute-provider/main.go b/cmd/compute-provider/main.go new file mode 100644 index 00000000..0fac0559 --- /dev/null +++ b/cmd/compute-provider/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/libp2p/go-libp2p/core/peer" + + p2p "github.com/zephyr-chain/zephyr-chain/internal/v2/network/p2p" + "github.com/zephyr-chain/zephyr-chain/internal/v2/provider" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +type startupInfo struct { + PeerID string `json:"peerId"` + Addresses []string `json:"addresses"` + Capabilities []string `json:"capabilities"` + Storage string `json:"storage"` +} + +func main() { + networkID, err := parseNetworkID(os.Getenv("ZEPHYR_NETWORK_ID")) + if err != nil { + log.Fatal("ZEPHYR_NETWORK_ID must be a 32-byte hex v2 NetworkID") + } + storage := strings.TrimSpace(os.Getenv("ZEPHYR_PROVIDER_STORAGE")) + if storage == "" { + storage = ".zephyr/compute-provider" + } + listen := strings.TrimSpace(os.Getenv("ZEPHYR_PROVIDER_LISTEN")) + if listen == "" { + listen = "/ip4/0.0.0.0/udp/9901/quic-v1" + } + service, err := provider.New(provider.DiskStore{Dir: storage}, provider.HashExecutor{}, provider.IdentityExecutor{}) + if err != nil { + log.Fatal(err) + } + node, err := p2p.New(p2p.Config{Network: networkID, ListenAddrs: []string{listen}}) + if err != nil { + log.Fatal(err) + } + defer node.Close() + node.SetComputeHandler(func(ctx context.Context, remote peer.ID, payload []byte) ([]byte, error) { + _ = remote // transport authenticates peer identity; job authorization is checked by on-chain state. + return service.Handle(ctx, payload) + }) + addresses := make([]string, 0, len(node.Addrs())) + for _, address := range node.Addrs() { + addresses = append(addresses, fmt.Sprintf("%s/p2p/%s", address.String(), node.ID().String())) + } + info := startupInfo{PeerID: node.ID().String(), Addresses: addresses, Capabilities: []string{"sha256", "identity"}, Storage: storage} + encoded, _ := json.Marshal(info) + fmt.Println(string(encoded)) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + <-ctx.Done() +} + +func parseNetworkID(value string) (types.NetworkID, error) { + var network types.NetworkID + raw, err := hex.DecodeString(strings.TrimSpace(value)) + if err != nil || len(raw) != len(network) { + return network, fmt.Errorf("invalid network ID") + } + copy(network[:], raw) + if types.IsZero32([32]byte(network)) { + return types.NetworkID{}, fmt.Errorf("invalid network ID") + } + return network, nil +} From d66afbaca6fa7351dfbcb540118a577405ea0ece Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:36:13 +0200 Subject: [PATCH 114/274] gate data availability withholding and corruption in v2 lab --- internal/v2/lab/da_fault_test.go | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 internal/v2/lab/da_fault_test.go diff --git a/internal/v2/lab/da_fault_test.go b/internal/v2/lab/da_fault_test.go new file mode 100644 index 00000000..886695e8 --- /dev/null +++ b/internal/v2/lab/da_fault_test.go @@ -0,0 +1,59 @@ +package lab + +import ( + "bytes" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/da" +) + +func TestV2LabDataAvailabilityWithholdingGate(t *testing.T) { + payload := bytes.Repeat([]byte("zephyr-v2-da-fault/"), 4096) + commitment, chunks, samples, err := da.EncodeBlob(payload, 8, 4) + if err != nil { + t.Fatal(err) + } + // Losing up to parity capacity must not lose the block payload. + withinTolerance := cloneDAChunks(chunks) + withinTolerance[0] = nil + withinTolerance[5] = nil + withinTolerance[11] = nil + recovered, err := da.ReconstructBlob(commitment, withinTolerance, samples) + if err != nil || !bytes.Equal(recovered, payload) { + t.Fatalf("DA recovery within parity budget failed: %v", err) + } + + // Withholding more than parity capacity must fail closed. + withheld := cloneDAChunks(chunks) + for _, index := range []int{0, 1, 2, 3, 4} { + withheld[index] = nil + } + if _, err := da.ReconstructBlob(commitment, withheld, samples); err != da.ErrReconstruction { + t.Fatalf("expected withholding failure, got %v", err) + } +} + +func TestV2LabDataAvailabilityCorruptionIsTreatedAsMissing(t *testing.T) { + payload := bytes.Repeat([]byte("authenticated-da"), 2048) + commitment, chunks, samples, err := da.EncodeBlob(payload, 6, 3) + if err != nil { + t.Fatal(err) + } + corrupt := cloneDAChunks(chunks) + corrupt[2][0] ^= 0xff + corrupt[8][len(corrupt[8])-1] ^= 0x01 + recovered, err := da.ReconstructBlob(commitment, corrupt, samples) + if err != nil || !bytes.Equal(recovered, payload) { + t.Fatalf("authenticated corruption recovery failed: %v", err) + } +} + +func cloneDAChunks(in [][]byte) [][]byte { + out := make([][]byte, len(in)) + for i, chunk := range in { + if chunk != nil { + out[i] = append([]byte(nil), chunk...) + } + } + return out +} From 53862d94684f6f989d41d854511866beee05b0c0 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:38:56 +0200 Subject: [PATCH 115/274] add native Citizen trust-anchor object bundle verifier --- internal/v2/citizen/trusted.go | 152 +++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 internal/v2/citizen/trusted.go diff --git a/internal/v2/citizen/trusted.go b/internal/v2/citizen/trusted.go new file mode 100644 index 00000000..14f182fe --- /dev/null +++ b/internal/v2/citizen/trusted.go @@ -0,0 +1,152 @@ +package citizen + +import ( + "encoding/hex" + "encoding/json" + "errors" + "strconv" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/state" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrTrustedBundle = errors.New("Citizen bundle is not anchored to trusted finalized state") + +type TrustAnchor struct { + Network types.NetworkID + ValidatorRoot types.Hash +} + +type ValidatorDTO struct { + ID string `json:"id"` + PublicKey []byte `json:"publicKey"` + Power string `json:"power"` +} + +type ObjectBundle struct { + Network string `json:"network"` + Height uint64 `json:"height"` + ShardID uint32 `json:"shardId"` + Header []byte `json:"header"` + Certificate []byte `json:"certificate"` + Commitment []byte `json:"commitment"` + CommitmentProof []byte `json:"commitmentProof"` + ObjectID string `json:"objectId"` + ObjectPresent bool `json:"objectPresent"` + Object []byte `json:"object,omitempty"` + StateProof []byte `json:"stateProof"` + Validators []ValidatorDTO `json:"validators"` +} + +type VerifiedObject struct { + Network types.NetworkID + Height uint64 + ShardID uint32 + ObjectID types.ObjectID + Present bool + Object object.Object + StateRoot types.Hash + NextAnchor TrustAnchor +} + +func VerifyObjectBundleJSON(data []byte, anchor TrustAnchor) (VerifiedObject, error) { + var bundle ObjectBundle + if len(data) == 0 || json.Unmarshal(data, &bundle) != nil { + return VerifiedObject{}, ErrTrustedBundle + } + return VerifyObjectBundle(bundle, anchor) +} + +func VerifyObjectBundle(bundle ObjectBundle, anchor TrustAnchor) (VerifiedObject, error) { + if types.IsZero32([32]byte(anchor.Network)) || types.IsZero32([32]byte(anchor.ValidatorRoot)) { + return VerifiedObject{}, ErrTrustedBundle + } + header, err := sharding.ParseGlobalHeader(bundle.Header) + if err != nil || header.Network != anchor.Network || header.ValidatorRoot != anchor.ValidatorRoot || header.Height != bundle.Height || bundle.Network != header.Network.String() { + return VerifiedObject{}, ErrTrustedBundle + } + validators, err := validatorSetFromDTO(header.Network, bundle.Validators) + if err != nil { + return VerifiedObject{}, ErrTrustedBundle + } + validatorRoot, err := validators.Root() + if err != nil || validatorRoot != header.ValidatorRoot { + return VerifiedObject{}, ErrTrustedBundle + } + certificate, err := v2consensus.ParseCertificate(bundle.Certificate) + if err != nil || certificate.Network != header.Network || certificate.Height != header.Height || certificate.HeaderHash != v2consensus.HeaderConsensusHash(header) || header.CertificateHash != certificate.Hash() { + return VerifiedObject{}, ErrTrustedBundle + } + if err := validators.VerifyCertificate(certificate); err != nil { + return VerifiedObject{}, ErrTrustedBundle + } + commitment, err := sharding.ParseCommitment(bundle.Commitment) + if err != nil || commitment.ShardID != bundle.ShardID { + return VerifiedObject{}, ErrTrustedBundle + } + commitmentProof, err := merkle.ParseProof(bundle.CommitmentProof) + if err != nil || !merkle.Verify(header.ShardCommitmentRoot, commitment.Hash(), commitmentProof) { + return VerifiedObject{}, ErrTrustedBundle + } + objectRaw, err := hex.DecodeString(bundle.ObjectID) + if err != nil || len(objectRaw) != 32 { + return VerifiedObject{}, ErrTrustedBundle + } + var objectID types.ObjectID + copy(objectID[:], objectRaw) + proof, err := state.ParseProof(bundle.StateProof) + if err != nil || proof.Exists != bundle.ObjectPresent { + return VerifiedObject{}, ErrTrustedBundle + } + verified := VerifiedObject{Network: header.Network, Height: header.Height, ShardID: bundle.ShardID, ObjectID: objectID, Present: bundle.ObjectPresent, StateRoot: commitment.StateRoot, NextAnchor: TrustAnchor{Network: header.Network, ValidatorRoot: header.EffectiveNextValidatorRoot()}} + if bundle.ObjectPresent { + obj, err := object.ParseObject(bundle.Object) + if err != nil || obj.ID != objectID { + return VerifiedObject{}, ErrTrustedBundle + } + hash := obj.Hash() + if !state.Verify(commitment.StateRoot, types.Hash(objectID), hash[:], proof) { + return VerifiedObject{}, ErrTrustedBundle + } + verified.Object = obj + } else if len(bundle.Object) != 0 || !state.Verify(commitment.StateRoot, types.Hash(objectID), nil, proof) { + return VerifiedObject{}, ErrTrustedBundle + } + return verified, nil +} + +func validatorSetFromDTO(network types.NetworkID, values []ValidatorDTO) (v2consensus.ValidatorSet, error) { + if len(values) == 0 || len(values) > v2consensus.MaxCertificateVotes { + return v2consensus.ValidatorSet{}, ErrTrustedBundle + } + set := v2consensus.ValidatorSet{Network: network, Validators: make([]v2consensus.Validator, len(values))} + seen := make(map[types.ValidatorID]struct{}, len(values)) + for i, value := range values { + idRaw, err := hex.DecodeString(value.ID) + if err != nil || len(idRaw) != 32 { + return v2consensus.ValidatorSet{}, ErrTrustedBundle + } + var id types.ValidatorID + copy(id[:], idRaw) + if types.ValidatorIDFromPublicKey(value.PublicKey) != id { + return v2consensus.ValidatorSet{}, ErrTrustedBundle + } + if _, duplicate := seen[id]; duplicate { + return v2consensus.ValidatorSet{}, ErrTrustedBundle + } + seen[id] = struct{}{} + power, err := strconv.ParseUint(value.Power, 10, 64) + if err != nil || power == 0 { + return v2consensus.ValidatorSet{}, ErrTrustedBundle + } + set.Validators[i] = v2consensus.Validator{ID: id, PublicKey: append([]byte(nil), value.PublicKey...), Power: power} + } + if err := set.Validate(); err != nil { + return v2consensus.ValidatorSet{}, ErrTrustedBundle + } + return set, nil +} From 0955611fb1a9c10d23fab5914c0dd455310c787f Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:41:32 +0200 Subject: [PATCH 116/274] temporarily automate exact v2 Go formatting --- .github/workflows/v2-format-write.yml | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/v2-format-write.yml diff --git a/.github/workflows/v2-format-write.yml b/.github/workflows/v2-format-write.yml new file mode 100644 index 00000000..26cc851a --- /dev/null +++ b/.github/workflows/v2-format-write.yml @@ -0,0 +1,44 @@ +name: V2 Exact Format + +on: + push: + branches: + - chatgpt/protocol-v2-foundation + paths: + - internal/v2/compute/messages.go + - internal/v2/provider/service.go + - internal/v2/provider/service_test.go + - internal/v2/tx/transaction.go + - .github/workflows/v2-format-write.yml + +permissions: + contents: write + +jobs: + format: + if: github.repository == 'the-code-learner/Zephyr-Chain' && github.ref == 'refs/heads/chatgpt/protocol-v2-foundation' + runs-on: ubuntu-latest + steps: + - name: Checkout v2 branch + uses: actions/checkout@v6 + with: + ref: chatgpt/protocol-v2-foundation + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: false + - name: Format exact files + run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go + - name: Commit formatting if needed + run: | + if git diff --quiet; then + echo 'Formatting already exact.' + exit 0 + fi + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go + git commit -m 'gofmt Zephyr v2 compute and provider files' + git push origin HEAD:chatgpt/protocol-v2-foundation From 60b40ec8834640322d7f45fd19e86cba5c7edc78 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:41:54 +0000 Subject: [PATCH 117/274] gofmt Zephyr v2 compute and provider files --- internal/v2/compute/messages.go | 12 ++++++------ internal/v2/provider/service.go | 6 +++--- internal/v2/provider/service_test.go | 4 ++-- internal/v2/tx/transaction.go | 24 ++++++++++++------------ 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/internal/v2/compute/messages.go b/internal/v2/compute/messages.go index c6ac7ac3..57d4d261 100644 --- a/internal/v2/compute/messages.go +++ b/internal/v2/compute/messages.go @@ -232,13 +232,13 @@ func ParseJobRef(data []byte) (JobRef, error) { } type SettlementReceipt struct { - JobID types.JobID - ResultRoot types.Hash - Payments map[types.AccountID]uint64 - Refund uint64 - Slashed map[types.AccountID]uint64 + JobID types.JobID + ResultRoot types.Hash + Payments map[types.AccountID]uint64 + Refund uint64 + Slashed map[types.AccountID]uint64 SlashReward uint64 - Expired bool + Expired bool } func (r SettlementReceipt) MarshalBinary() []byte { diff --git a/internal/v2/provider/service.go b/internal/v2/provider/service.go index cd25b67e..f3bd2714 100644 --- a/internal/v2/provider/service.go +++ b/internal/v2/provider/service.go @@ -13,9 +13,9 @@ import ( const ( MaxCapabilityBytes = 64 - MaxInlineInput = 1 << 20 - MaxParameters = 1 << 20 - MaxInlineOutput = 1 << 20 + MaxInlineInput = 1 << 20 + MaxParameters = 1 << 20 + MaxInlineOutput = 1 << 20 ) var ( diff --git a/internal/v2/provider/service_test.go b/internal/v2/provider/service_test.go index cf8473b0..3626dfa8 100644 --- a/internal/v2/provider/service_test.go +++ b/internal/v2/provider/service_test.go @@ -15,9 +15,9 @@ func TestProviderExecutesRegisteredCapabilityAndCommitsResult(t *testing.T) { } input := []byte("scientific-work-unit") request := Request{ - JobID: types.JobID(types.HashBytes("job", []byte("provider"))), + JobID: types.JobID(types.HashBytes("job", []byte("provider"))), WorkloadHash: types.HashBytes("workload", []byte("sha256")), - InputRoot: InputRoot(input), Capability: "sha256", Input: input, + InputRoot: InputRoot(input), Capability: "sha256", Input: input, } wire, err := request.MarshalBinary() if err != nil { diff --git a/internal/v2/tx/transaction.go b/internal/v2/tx/transaction.go index cfb4e8b4..95b49057 100644 --- a/internal/v2/tx/transaction.go +++ b/internal/v2/tx/transaction.go @@ -16,19 +16,19 @@ import ( const ( Version uint16 = 2 - OpTransfer uint16 = 1 - OpCreateToken uint16 = 2 - OpDeployContract uint16 = 3 - OpContractCall uint16 = 4 - OpComputeOffer uint16 = 5 - OpComputeJob uint16 = 6 - OpComputeResult uint16 = 7 - OpComputeAccept uint16 = 8 - OpComputeIngestAssignment uint16 = 9 - OpComputeIngestResult uint16 = 10 - OpComputeFinalize uint16 = 11 + OpTransfer uint16 = 1 + OpCreateToken uint16 = 2 + OpDeployContract uint16 = 3 + OpContractCall uint16 = 4 + OpComputeOffer uint16 = 5 + OpComputeJob uint16 = 6 + OpComputeResult uint16 = 7 + OpComputeAccept uint16 = 8 + OpComputeIngestAssignment uint16 = 9 + OpComputeIngestResult uint16 = 10 + OpComputeFinalize uint16 = 11 OpComputeResolveReplicated uint16 = 12 - OpComputeExpire uint16 = 13 + OpComputeExpire uint16 = 13 MaxInputs = 4096 MaxOutputs = 4096 From 6504aa0511e1efcdeed469d3bdcdd534a362ae8a Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:42:12 +0200 Subject: [PATCH 118/274] add gomobile-friendly Citizen Node core --- mobile/citizen.go | 126 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 mobile/citizen.go diff --git a/mobile/citizen.go b/mobile/citizen.go new file mode 100644 index 00000000..9c9a852b --- /dev/null +++ b/mobile/citizen.go @@ -0,0 +1,126 @@ +package mobile + +import ( + "encoding/hex" + "encoding/json" + "errors" + "sync" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/citizen" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrCitizenAnchor = errors.New("invalid Citizen trust anchor") + +type CitizenNode struct { + mu sync.RWMutex + anchor citizen.TrustAnchor +} + +type verifiedObjectJSON struct { + Network string `json:"network"` + Height uint64 `json:"height"` + ShardID uint32 `json:"shardId"` + ObjectID string `json:"objectId"` + ObjectPresent bool `json:"objectPresent"` + StateRoot string `json:"stateRoot"` + NextValidatorRoot string `json:"nextValidatorRoot"` +} + +type modeJSON struct { + VerifyHeaders bool `json:"verifyHeaders"` + Relay bool `json:"relay"` + SampleDA bool `json:"sampleDA"` + ExecuteRecent bool `json:"executeRecent"` + ServeCache bool `json:"serveCache"` +} + +// NewCitizenNode exposes only mobile-binding-friendly argument/return types. +// The genesis/checkpoint anchor is the only trusted bootstrap input; subsequent +// validator roots advance only after a locally verified quorum certificate. +func NewCitizenNode(networkHex, validatorRootHex string) (*CitizenNode, error) { + network, err := parse32(networkHex) + if err != nil { + return nil, ErrCitizenAnchor + } + root, err := parse32(validatorRootHex) + if err != nil { + return nil, ErrCitizenAnchor + } + var networkID types.NetworkID + var validatorRoot types.Hash + copy(networkID[:], network[:]) + copy(validatorRoot[:], root[:]) + if types.IsZero32([32]byte(networkID)) || types.IsZero32([32]byte(validatorRoot)) { + return nil, ErrCitizenAnchor + } + return &CitizenNode{anchor: citizen.TrustAnchor{Network: networkID, ValidatorRoot: validatorRoot}}, nil +} + +func (n *CitizenNode) NetworkID() string { + n.mu.RLock() + defer n.mu.RUnlock() + return n.anchor.Network.String() +} + +func (n *CitizenNode) ValidatorRoot() string { + n.mu.RLock() + defer n.mu.RUnlock() + return n.anchor.ValidatorRoot.String() +} + +func (n *CitizenNode) TrustAnchorJSON() string { + n.mu.RLock() + defer n.mu.RUnlock() + encoded, _ := json.Marshal(map[string]string{"network": n.anchor.Network.String(), "validatorRoot": n.anchor.ValidatorRoot.String()}) + return string(encoded) +} + +// VerifyObjectBundle takes the exact JSON returned by /v2/light/object. It +// updates the mobile trust anchor only after all QC, validator-root, shard and +// Sparse-Merkle proofs verify locally. +func (n *CitizenNode) VerifyObjectBundle(bundleJSON string) (string, error) { + n.mu.RLock() + anchor := n.anchor + n.mu.RUnlock() + verified, err := citizen.VerifyObjectBundleJSON([]byte(bundleJSON), anchor) + if err != nil { + return "", err + } + n.mu.Lock() + n.anchor = verified.NextAnchor + n.mu.Unlock() + encoded, err := json.Marshal(verifiedObjectJSON{ + Network: verified.Network.String(), Height: verified.Height, ShardID: verified.ShardID, + ObjectID: verified.ObjectID.String(), ObjectPresent: verified.Present, StateRoot: verified.StateRoot.String(), + NextValidatorRoot: verified.NextAnchor.ValidatorRoot.String(), + }) + if err != nil { + return "", err + } + return string(encoded), nil +} + +// SelectCitizenMode mirrors the resource policy used by the Go verifier while +// exposing primitive parameters friendly to Android/iOS bindings. +func SelectCitizenMode(batteryPercent int, charging, wifi, lowPower, appActive bool) string { + if batteryPercent < 0 { + batteryPercent = 0 + } + if batteryPercent > 100 { + batteryPercent = 100 + } + mode := citizen.SelectMode(citizen.PowerState{BatteryPercent: uint8(batteryPercent), Charging: charging, WiFi: wifi, LowPower: lowPower, AppActive: appActive}) + encoded, _ := json.Marshal(modeJSON{VerifyHeaders: mode.VerifyHeaders, Relay: mode.Relay, SampleDA: mode.SampleDA, ExecuteRecent: mode.ExecuteRecent, ServeCache: mode.ServeCache}) + return string(encoded) +} + +func parse32(value string) ([32]byte, error) { + var out [32]byte + raw, err := hex.DecodeString(value) + if err != nil || len(raw) != len(out) { + return out, ErrCitizenAnchor + } + copy(out[:], raw) + return out, nil +} From c53d1981259cdc41f62a2cb1449327d79e5c13cc Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:42:29 +0200 Subject: [PATCH 119/274] test mobile Citizen trust and resource policy --- mobile/citizen_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 mobile/citizen_test.go diff --git a/mobile/citizen_test.go b/mobile/citizen_test.go new file mode 100644 index 00000000..8b8a8303 --- /dev/null +++ b/mobile/citizen_test.go @@ -0,0 +1,43 @@ +package mobile + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestCitizenNodeRejectsBadAnchorAndDoesNotAdvanceOnBadBundle(t *testing.T) { + if _, err := NewCitizenNode("00", "11"); err != ErrCitizenAnchor { + t.Fatalf("expected bad anchor rejection, got %v", err) + } + network := strings.Repeat("11", 32) + root := strings.Repeat("22", 32) + node, err := NewCitizenNode(network, root) + if err != nil { + t.Fatal(err) + } + before := node.ValidatorRoot() + if _, err := node.VerifyObjectBundle(`{"network":"bad"}`); err == nil { + t.Fatal("invalid proof bundle was accepted") + } + if node.ValidatorRoot() != before { + t.Fatal("trust anchor advanced after invalid bundle") + } +} + +func TestMobileCitizenModeIsAdaptive(t *testing.T) { + var low map[string]bool + if err := json.Unmarshal([]byte(SelectCitizenMode(10, false, true, false, true)), &low); err != nil { + t.Fatal(err) + } + if !low["verifyHeaders"] || low["relay"] || low["sampleDA"] || low["executeRecent"] { + t.Fatalf("unexpected low-power mode: %+v", low) + } + var charging map[string]bool + if err := json.Unmarshal([]byte(SelectCitizenMode(80, true, true, false, true)), &charging); err != nil { + t.Fatal(err) + } + if !charging["verifyHeaders"] || !charging["relay"] || !charging["sampleDA"] || !charging["executeRecent"] || !charging["serveCache"] { + t.Fatalf("unexpected charging mode: %+v", charging) + } +} From 22576187fecf2436fe71de72deb3d22baf38c929 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:43:44 +0200 Subject: [PATCH 120/274] preserve uint64 validator power across Citizen JSON boundary --- internal/v2/lightapi/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/v2/lightapi/server.go b/internal/v2/lightapi/server.go index e3c0c618..b011e45f 100644 --- a/internal/v2/lightapi/server.go +++ b/internal/v2/lightapi/server.go @@ -54,7 +54,7 @@ type Server struct { type validatorDTO struct { ID string `json:"id"` PublicKey []byte `json:"publicKey"` - Power uint64 `json:"power"` + Power string `json:"power"` } type statusResponse struct { @@ -178,7 +178,7 @@ func (s Server) snapshot() (Snapshot, error) { func validatorList(set v2consensus.ValidatorSet) []validatorDTO { out := make([]validatorDTO, len(set.Validators)) for i, validator := range set.Validators { - out[i] = validatorDTO{ID: validator.ID.String(), PublicKey: append([]byte(nil), validator.PublicKey...), Power: validator.Power} + out[i] = validatorDTO{ID: validator.ID.String(), PublicKey: append([]byte(nil), validator.PublicKey...), Power: strconv.FormatUint(validator.Power, 10)} } return out } From 0d44d034522e1ddc7760d41f697cd934153a3439 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:45:29 +0200 Subject: [PATCH 121/274] add strict human-readable v2 genesis JSON loader --- internal/v2/genesis/json.go | 103 ++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 internal/v2/genesis/json.go diff --git a/internal/v2/genesis/json.go b/internal/v2/genesis/json.go new file mode 100644 index 00000000..2ddf8b4e --- /dev/null +++ b/internal/v2/genesis/json.go @@ -0,0 +1,103 @@ +package genesis + +import ( + "crypto/elliptic" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "strconv" + "strings" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrGenesisJSON = errors.New("invalid Zephyr v2 genesis JSON") + +type jsonValidator struct { + ConsensusPublicKey string `json:"consensusPublicKey"` + VotingPower string `json:"votingPower"` +} + +type jsonAllocation struct { + Owner string `json:"owner"` + Amount string `json:"amount"` +} + +type jsonConfig struct { + Version uint16 `json:"version"` + ChainName string `json:"chainName"` + GenesisUnix uint64 `json:"genesisUnix"` + InitialShardCount uint32 `json:"initialShardCount"` + MaxShardCount uint32 `json:"maxShardCount"` + NativeSymbol string `json:"nativeSymbol"` + Validators []jsonValidator `json:"validators"` + Allocations []jsonAllocation `json:"allocations"` +} + +func LoadJSON(path string) (Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return Config{}, err + } + return ParseJSON(data) +} + +func ParseJSON(data []byte) (Config, error) { + decoder := json.NewDecoder(strings.NewReader(string(data))) + decoder.DisallowUnknownFields() + var raw jsonConfig + if err := decoder.Decode(&raw); err != nil { + return Config{}, fmt.Errorf("%w: %v", ErrGenesisJSON, err) + } + if decoder.More() { + return Config{}, ErrGenesisJSON + } + cfg := Config{ + Version: raw.Version, ChainName: raw.ChainName, GenesisUnix: raw.GenesisUnix, + InitialShardCount: raw.InitialShardCount, MaxShardCount: raw.MaxShardCount, NativeSymbol: raw.NativeSymbol, + Validators: make([]Validator, len(raw.Validators)), Allocations: make([]Allocation, len(raw.Allocations)), + } + for i, value := range raw.Validators { + pub, err := hex.DecodeString(strings.TrimSpace(value.ConsensusPublicKey)) + if err != nil || len(pub) != 65 { + return Config{}, ErrGenesisJSON + } + x, y := elliptic.Unmarshal(elliptic.P256(), pub) + if x == nil || y == nil { + return Config{}, ErrGenesisJSON + } + power, err := strconv.ParseUint(value.VotingPower, 10, 64) + if err != nil || power == 0 { + return Config{}, ErrGenesisJSON + } + cfg.Validators[i] = Validator{ID: types.ValidatorIDFromPublicKey(pub), ConsensusPublicKey: pub, VotingPower: power} + } + for i, value := range raw.Allocations { + ownerRaw, err := hex.DecodeString(strings.TrimSpace(value.Owner)) + if err != nil || len(ownerRaw) != 32 { + return Config{}, ErrGenesisJSON + } + var owner types.AccountID + copy(owner[:], ownerRaw) + amount, err := strconv.ParseUint(value.Amount, 10, 64) + if err != nil || amount == 0 { + return Config{}, ErrGenesisJSON + } + cfg.Allocations[i] = Allocation{Owner: owner, Amount: amount} + } + if err := cfg.Validate(); err != nil { + return Config{}, err + } + return cfg, nil +} + +func (g Config) NativeTokenID() (types.TokenID, error) { + network, err := g.NetworkID() + if err != nil { + return types.TokenID{}, err + } + payload := append(append([]byte(nil), network[:]...), []byte(strings.TrimSpace(g.NativeSymbol))...) + return types.TokenID(types.HashBytes("zephyr/native-token/v2", payload)), nil +} From 159e66af6950962aea5cdecae18057d63fc13671 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:45:57 +0200 Subject: [PATCH 122/274] add deterministic idempotent v2 genesis state seeding --- internal/v2/genesis/state.go | 81 ++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 internal/v2/genesis/state.go diff --git a/internal/v2/genesis/state.go b/internal/v2/genesis/state.go new file mode 100644 index 00000000..4b035286 --- /dev/null +++ b/internal/v2/genesis/state.go @@ -0,0 +1,81 @@ +package genesis + +import ( + "encoding/binary" + "errors" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +var ErrGenesisState = errors.New("invalid or partially initialized Zephyr v2 genesis state") + +func SeedStates(g Config, states map[uint32]worldstate.Backend) error { + if err := g.Validate(); err != nil || len(states) != int(g.InitialShardCount) { + return ErrGenesisState + } + network, err := g.NetworkID() + if err != nil { + return err + } + native, err := g.NativeTokenID() + if err != nil { + return err + } + canonical, err := g.CanonicalBytes() + if err != nil { + return err + } + genesisHash := types.Hash(codec.DomainHash("zephyr/genesis/state/v2", canonical)) + emptyRoot := worldstate.NewMemory().Root() + + for shard := uint32(0); shard < g.InitialShardCount; shard++ { + store, ok := states[shard] + if !ok || store == nil { + return ErrGenesisState + } + markerID := GenesisMarkerID(network, shard) + if marker, exists := store.GetObject(markerID); exists { + if marker.Kind != object.KindSystem || len(marker.Data) != 36 || string(marker.Data[:32]) != string(genesisHash[:]) || binary.BigEndian.Uint32(marker.Data[32:]) != shard { + return ErrGenesisState + } + continue + } + if store.Root() != emptyRoot { + return ErrGenesisState + } + created := make([]object.Object, 0) + for _, allocation := range g.Allocations { + if types.AccountShard(allocation.Owner, g.InitialShardCount) != shard { + continue + } + coin, err := object.NewCoinOutput(allocation.Owner, native, allocation.Amount) + if err != nil { + return err + } + created = append(created, object.Object{ID: GenesisAllocationID(network, allocation.Owner, shard), Version: 1, Owner: allocation.Owner, Kind: coin.Kind, Data: coin.Data}) + } + markerData := make([]byte, 36) + copy(markerData[:32], genesisHash[:]) + binary.BigEndian.PutUint32(markerData[32:], shard) + created = append(created, object.Object{ID: markerID, Version: 1, Kind: object.KindSystem, Data: markerData}) + if _, err := store.Apply(nil, created); err != nil { + return err + } + } + return nil +} + +func GenesisMarkerID(network types.NetworkID, shard uint32) types.ObjectID { + return types.ObjectIDForShard(types.Hash(network), 0xfffffff0, shard) +} + +func GenesisAllocationID(network types.NetworkID, owner types.AccountID, shard uint32) types.ObjectID { + var w codec.Writer + w.Fixed(network[:]) + w.Fixed(owner[:]) + seed := types.Hash(codec.DomainHash("zephyr/genesis/allocation/v2", w.BytesCopy())) + return types.ObjectIDForShard(seed, 0, shard) +} From a613a90f48e0adac7b804e64f5c726c345096c38 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:46:27 +0200 Subject: [PATCH 123/274] strictly reject trailing v2 genesis JSON --- internal/v2/genesis/json.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/v2/genesis/json.go b/internal/v2/genesis/json.go index 2ddf8b4e..2681afa4 100644 --- a/internal/v2/genesis/json.go +++ b/internal/v2/genesis/json.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "strconv" "strings" @@ -51,7 +52,8 @@ func ParseJSON(data []byte) (Config, error) { if err := decoder.Decode(&raw); err != nil { return Config{}, fmt.Errorf("%w: %v", ErrGenesisJSON, err) } - if decoder.More() { + var trailing any + if err := decoder.Decode(&trailing); err != io.EOF { return Config{}, ErrGenesisJSON } cfg := Config{ From ed8731337fca1f3b92e920f23e1109e3c9677d71 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:47:51 +0200 Subject: [PATCH 124/274] add canonical validator-set wire format for v2 network sync --- internal/v2/consensus/validator_wire.go | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 internal/v2/consensus/validator_wire.go diff --git a/internal/v2/consensus/validator_wire.go b/internal/v2/consensus/validator_wire.go new file mode 100644 index 00000000..48ab3c11 --- /dev/null +++ b/internal/v2/consensus/validator_wire.go @@ -0,0 +1,59 @@ +package consensus + +import ( + "errors" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrValidatorWire = errors.New("invalid canonical validator set") + +func (s ValidatorSet) MarshalBinary() ([]byte, error) { + if err := s.Validate(); err != nil || len(s.Validators) > MaxCertificateVotes { + return nil, ErrValidatorWire + } + var w codec.Writer + w.Fixed(s.Network[:]) + w.U32(uint32(len(s.Validators))) + for _, validator := range s.Validators { + w.Fixed(validator.ID[:]) + w.Bytes(validator.PublicKey) + w.U64(validator.Power) + } + return w.BytesCopy(), nil +} + +func ParseValidatorSet(data []byte) (ValidatorSet, error) { + r := codec.NewReader(data) + networkRaw, err := r.Fixed(32) + if err != nil { + return ValidatorSet{}, ErrValidatorWire + } + var network types.NetworkID + copy(network[:], networkRaw) + count, err := r.U32() + if err != nil || count == 0 || count > MaxCertificateVotes { + return ValidatorSet{}, ErrValidatorWire + } + set := ValidatorSet{Network: network, Validators: make([]Validator, int(count))} + for i := range set.Validators { + idRaw, err := r.Fixed(32) + if err != nil { + return ValidatorSet{}, ErrValidatorWire + } + copy(set.Validators[i].ID[:], idRaw) + set.Validators[i].PublicKey, err = r.Bytes(65) + if err != nil || len(set.Validators[i].PublicKey) != 65 { + return ValidatorSet{}, ErrValidatorWire + } + set.Validators[i].Power, err = r.U64() + if err != nil { + return ValidatorSet{}, ErrValidatorWire + } + } + if r.Done() != nil || set.Validate() != nil { + return ValidatorSet{}, ErrValidatorWire + } + return set, nil +} From 5543cb822c48ecd14ab78e7370ede4b56278fa58 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:48:29 +0200 Subject: [PATCH 125/274] add canonical block proposal and commit network wire --- internal/v2/node/network_wire.go | 301 +++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 internal/v2/node/network_wire.go diff --git a/internal/v2/node/network_wire.go b/internal/v2/node/network_wire.go new file mode 100644 index 00000000..d9cb8180 --- /dev/null +++ b/internal/v2/node/network_wire.go @@ -0,0 +1,301 @@ +package node + +import ( + "errors" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + NetworkMessageProposal uint8 = 1 + NetworkMessageCommit uint8 = 2 + MaxNetworkShards = 1024 + MaxNetworkTransactions = 65536 + MaxNetworkImports = 65536 + MaxNetworkMessageBytes = 64 << 20 +) + +var ErrNetworkWire = errors.New("invalid v2 node network message") + +type BlockData struct { + Batches map[uint32]ShardBatch +} + +type ConsensusMessage struct { + Kind uint8 + Proposal v2consensus.Proposal + Block BlockData + Certificate *v2consensus.Certificate +} + +func (b BlockData) MarshalBinary() ([]byte, error) { + if len(b.Batches) > MaxNetworkShards { + return nil, ErrNetworkWire + } + shards := make([]int, 0, len(b.Batches)) + for shard := range b.Batches { + shards = append(shards, int(shard)) + } + sort.Ints(shards) + var w codec.Writer + w.U32(uint32(len(shards))) + for _, shardValue := range shards { + shard := uint32(shardValue) + batch := b.Batches[shard] + if len(batch.Transactions) > MaxNetworkTransactions || len(batch.Imports) > MaxNetworkImports { + return nil, ErrNetworkWire + } + w.U32(shard) + w.Fixed(batch.DataRoot[:]) + w.U32(uint32(len(batch.Transactions))) + for _, transaction := range batch.Transactions { + raw, err := transaction.MarshalBinary() + if err != nil { + return nil, err + } + w.Bytes(raw) + } + w.U32(uint32(len(batch.Imports))) + for _, receiptImport := range batch.Imports { + raw, err := receiptImport.MarshalBinary() + if err != nil { + return nil, err + } + w.Bytes(raw) + } + } + payload := w.BytesCopy() + if len(payload) > MaxNetworkMessageBytes { + return nil, ErrNetworkWire + } + return payload, nil +} + +func ParseBlockData(data []byte) (BlockData, error) { + if len(data) > MaxNetworkMessageBytes { + return BlockData{}, ErrNetworkWire + } + r := codec.NewReader(data) + count, err := r.U32() + if err != nil || count > MaxNetworkShards { + return BlockData{}, ErrNetworkWire + } + out := BlockData{Batches: make(map[uint32]ShardBatch, int(count))} + for i := uint32(0); i < count; i++ { + shard, err := r.U32() + if err != nil { + return BlockData{}, ErrNetworkWire + } + if _, duplicate := out.Batches[shard]; duplicate { + return BlockData{}, ErrNetworkWire + } + rootRaw, err := r.Fixed(32) + if err != nil { + return BlockData{}, ErrNetworkWire + } + var dataRoot types.Hash + copy(dataRoot[:], rootRaw) + txCount, err := r.U32() + if err != nil || txCount > MaxNetworkTransactions { + return BlockData{}, ErrNetworkWire + } + batch := ShardBatch{Transactions: make([]tx.Transaction, int(txCount)), DataRoot: dataRoot} + for j := range batch.Transactions { + raw, err := r.Bytes(tx.MaxWireBytes) + if err != nil { + return BlockData{}, ErrNetworkWire + } + batch.Transactions[j], err = tx.ParseTransaction(raw) + if err != nil { + return BlockData{}, err + } + } + importCount, err := r.U32() + if err != nil || importCount > MaxNetworkImports { + return BlockData{}, ErrNetworkWire + } + batch.Imports = make([]ReceiptImport, int(importCount)) + for j := range batch.Imports { + raw, err := r.Bytes(MaxNetworkMessageBytes) + if err != nil { + return BlockData{}, ErrNetworkWire + } + batch.Imports[j], err = ParseReceiptImport(raw) + if err != nil { + return BlockData{}, err + } + } + out.Batches[shard] = batch + } + if r.Done() != nil { + return BlockData{}, ErrNetworkWire + } + return out, nil +} + +func (m ConsensusMessage) MarshalBinary() ([]byte, error) { + if m.Kind != NetworkMessageProposal && m.Kind != NetworkMessageCommit { + return nil, ErrNetworkWire + } + proposal, err := m.Proposal.MarshalBinary() + if err != nil { + return nil, err + } + block, err := m.Block.MarshalBinary() + if err != nil { + return nil, err + } + var w codec.Writer + w.U8(m.Kind) + w.Bytes(proposal) + w.Bytes(block) + if m.Kind == NetworkMessageCommit { + if m.Certificate == nil { + return nil, ErrNetworkWire + } + certificate, err := m.Certificate.MarshalBinary() + if err != nil { + return nil, err + } + w.Bytes(certificate) + } + payload := w.BytesCopy() + if len(payload) > MaxNetworkMessageBytes { + return nil, ErrNetworkWire + } + return payload, nil +} + +func ParseConsensusMessage(data []byte) (ConsensusMessage, error) { + if len(data) == 0 || len(data) > MaxNetworkMessageBytes { + return ConsensusMessage{}, ErrNetworkWire + } + r := codec.NewReader(data) + kind, err := r.U8() + if err != nil || (kind != NetworkMessageProposal && kind != NetworkMessageCommit) { + return ConsensusMessage{}, ErrNetworkWire + } + proposalRaw, err := r.Bytes(2048) + if err != nil { + return ConsensusMessage{}, ErrNetworkWire + } + proposal, err := v2consensus.ParseProposal(proposalRaw) + if err != nil { + return ConsensusMessage{}, err + } + blockRaw, err := r.Bytes(MaxNetworkMessageBytes) + if err != nil { + return ConsensusMessage{}, ErrNetworkWire + } + block, err := ParseBlockData(blockRaw) + if err != nil { + return ConsensusMessage{}, err + } + message := ConsensusMessage{Kind: kind, Proposal: proposal, Block: block} + if kind == NetworkMessageCommit { + certificateRaw, err := r.Bytes(4 << 20) + if err != nil { + return ConsensusMessage{}, ErrNetworkWire + } + certificate, err := v2consensus.ParseCertificate(certificateRaw) + if err != nil { + return ConsensusMessage{}, err + } + message.Certificate = &certificate + } + if r.Done() != nil { + return ConsensusMessage{}, ErrNetworkWire + } + return message, nil +} + +func (r ReceiptImport) MarshalBinary() ([]byte, error) { + certificate, err := r.Certificate.MarshalBinary() + if err != nil { + return nil, err + } + validators, err := r.Validators.MarshalBinary() + if err != nil { + return nil, err + } + receipt, err := r.Receipt.CanonicalBytes() + if err != nil { + return nil, err + } + var w codec.Writer + w.Bytes(r.Header.CanonicalBytes()) + w.Bytes(certificate) + w.Bytes(validators) + w.Bytes(r.Commitment.CanonicalBytes()) + w.Bytes(r.CommitmentProof.MarshalBinary()) + w.Bytes(receipt) + w.Bytes(r.ReceiptProof.MarshalBinary()) + return w.BytesCopy(), nil +} + +func ParseReceiptImport(data []byte) (ReceiptImport, error) { + r := codec.NewReader(data) + headerRaw, err := r.Bytes(512) + if err != nil { + return ReceiptImport{}, ErrNetworkWire + } + header, err := sharding.ParseGlobalHeader(headerRaw) + if err != nil { + return ReceiptImport{}, err + } + certificateRaw, err := r.Bytes(4 << 20) + if err != nil { + return ReceiptImport{}, ErrNetworkWire + } + certificate, err := v2consensus.ParseCertificate(certificateRaw) + if err != nil { + return ReceiptImport{}, err + } + validatorsRaw, err := r.Bytes(4 << 20) + if err != nil { + return ReceiptImport{}, ErrNetworkWire + } + validators, err := v2consensus.ParseValidatorSet(validatorsRaw) + if err != nil { + return ReceiptImport{}, err + } + commitmentRaw, err := r.Bytes(512) + if err != nil { + return ReceiptImport{}, ErrNetworkWire + } + commitment, err := sharding.ParseCommitment(commitmentRaw) + if err != nil { + return ReceiptImport{}, err + } + commitmentProofRaw, err := r.Bytes(64 << 10) + if err != nil { + return ReceiptImport{}, ErrNetworkWire + } + commitmentProof, err := merkle.ParseProof(commitmentProofRaw) + if err != nil { + return ReceiptImport{}, err + } + receiptRaw, err := r.Bytes(1 << 20) + if err != nil { + return ReceiptImport{}, ErrNetworkWire + } + receipt, err := sharding.ParseCrossShardReceipt(receiptRaw) + if err != nil { + return ReceiptImport{}, err + } + receiptProofRaw, err := r.Bytes(64 << 10) + if err != nil { + return ReceiptImport{}, ErrNetworkWire + } + receiptProof, err := merkle.ParseProof(receiptProofRaw) + if err != nil || r.Done() != nil { + return ReceiptImport{}, ErrNetworkWire + } + return ReceiptImport{Header: header, Certificate: certificate, Validators: validators, Commitment: commitment, CommitmentProof: commitmentProof, Receipt: receipt, ReceiptProof: receiptProof}, nil +} From 8cfdae8298a6ad9c77fcfa2a703c1646f9dbbab9 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:49:03 +0200 Subject: [PATCH 126/274] allow runtime installation of v2 p2p handlers --- internal/v2/network/p2p/node.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/v2/network/p2p/node.go b/internal/v2/network/p2p/node.go index a3c1ee7b..29d37f46 100644 --- a/internal/v2/network/p2p/node.go +++ b/internal/v2/network/p2p/node.go @@ -120,12 +120,16 @@ func (n *Node) FetchLightProof(ctx context.Context, remote peer.ID, payload []by return n.request(ctx, remote, n.lightProof, payload) } +func (n *Node) SetConsensusHandler(handler Handler) { n.install(n.consensus, handler) } +func (n *Node) SetTransactionHandler(handler Handler) { n.install(n.transaction, handler) } +func (n *Node) SetLightProofHandler(handler Handler) { n.install(n.lightProof, handler) } + func (n *Node) install(id protocol.ID, handler Handler) { + if handler == nil { + n.host.RemoveStreamHandler(id) + return + } n.host.SetStreamHandler(id, func(stream network.Stream) { - if handler == nil { - _ = stream.Reset() - return - } defer stream.Close() _ = stream.SetDeadline(time.Now().Add(n.timeout)) payload, err := readFrame(stream, n.maxMessage) From 08b03d39b30be6415431ed1e3fede112e35f52e2 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:50:23 +0200 Subject: [PATCH 127/274] add executable single-round v2 consensus network service --- internal/v2/node/service.go | 424 ++++++++++++++++++++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 internal/v2/node/service.go diff --git a/internal/v2/node/service.go b/internal/v2/node/service.go new file mode 100644 index 00000000..392b7c37 --- /dev/null +++ b/internal/v2/node/service.go @@ -0,0 +1,424 @@ +package node + +import ( + "bytes" + "context" + "crypto/ecdsa" + "errors" + "sort" + "sync" + + "github.com/libp2p/go-libp2p/core/peer" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/execution" + "github.com/zephyr-chain/zephyr-chain/internal/v2/lightapi" + p2p "github.com/zephyr-chain/zephyr-chain/internal/v2/network/p2p" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +var ( + ErrServiceConfig = errors.New("invalid v2 consensus service configuration") + ErrNotValidator = errors.New("node is not an active v2 validator") + ErrNotProposer = errors.New("node is not the scheduled v2 proposer") + ErrNoQuorum = errors.New("v2 proposal did not collect 2/3+ voting power") + ErrProposalState = errors.New("v2 proposal does not match local execution") + ErrDoubleVote = errors.New("refusing conflicting vote for same v2 height/round") + ErrMempoolConflict = errors.New("v2 mempool object conflict") + ErrNoSnapshot = errors.New("no finalized v2 snapshot available") +) + +type voteSlot struct { + Height uint64 + Round uint64 +} + +type Service struct { + mu sync.Mutex + proposeMu sync.Mutex + Runtime *Runtime + Validators v2consensus.ValidatorSet + Key *ecdsa.PrivateKey + P2P *p2p.Node + Peers []peer.ID + + mempool map[uint32][]tx.Transaction + mempoolInputs map[types.ObjectID]types.Hash + imports map[uint32][]ReceiptImport + votes map[voteSlot]v2consensus.Vote + latest *lightapi.Snapshot +} + +func NewService(runtime *Runtime, validators v2consensus.ValidatorSet, key *ecdsa.PrivateKey, transport *p2p.Node) (*Service, error) { + if runtime == nil || transport == nil || validators.Network != runtime.Network || validators.Validate() != nil { + return nil, ErrServiceConfig + } + root, err := validators.Root() + if err != nil || root != runtime.ValidatorRoot { + return nil, ErrServiceConfig + } + service := &Service{ + Runtime: runtime, Validators: validators, Key: key, P2P: transport, + mempool: make(map[uint32][]tx.Transaction), mempoolInputs: make(map[types.ObjectID]types.Hash), + imports: make(map[uint32][]ReceiptImport), votes: make(map[voteSlot]v2consensus.Vote), + } + if key != nil { + id, err := validatorID(key) + if err != nil || !containsValidator(validators, id) { + return nil, ErrNotValidator + } + } + transport.SetConsensusHandler(service.HandleConsensus) + transport.SetTransactionHandler(service.HandleTransaction) + return service, nil +} + +func (s *Service) SetPeers(peers []peer.ID) { + s.mu.Lock() + defer s.mu.Unlock() + seen := make(map[peer.ID]struct{}, len(peers)) + s.Peers = s.Peers[:0] + for _, id := range peers { + if id == "" || id == s.P2P.ID() { + continue + } + if _, duplicate := seen[id]; duplicate { + continue + } + seen[id] = struct{}{} + s.Peers = append(s.Peers, id) + } +} + +func (s *Service) Submit(transaction tx.Transaction) error { + s.mu.Lock() + defer s.mu.Unlock() + if transaction.Network != s.Runtime.Network || transaction.ShardID >= s.Runtime.ShardCount { + return ErrCandidateState + } + store := s.Runtime.States[transaction.ShardID] + if store == nil || transaction.StateRoot != store.Root() { + return ErrCandidateState + } + engine := execution.Engine{Network: s.Runtime.Network, NativeToken: s.Runtime.NativeToken, ShardCount: s.Runtime.ShardCount, Height: s.Runtime.Height + 1} + if _, err := engine.Execute(transaction); err != nil { + return err + } + id := transaction.ID() + for _, input := range transaction.Inputs { + if existing, conflict := s.mempoolInputs[input.ObjectID]; conflict && existing != id { + return ErrMempoolConflict + } + } + for _, pending := range s.mempool[transaction.ShardID] { + if pending.ID() == id { + return nil + } + } + for _, input := range transaction.Inputs { + s.mempoolInputs[input.ObjectID] = id + } + s.mempool[transaction.ShardID] = append(s.mempool[transaction.ShardID], transaction) + return nil +} + +func (s *Service) QueueReceiptImport(shard uint32, receiptImport ReceiptImport) error { + s.mu.Lock() + defer s.mu.Unlock() + if shard >= s.Runtime.ShardCount || s.Runtime.validateReceiptImport(shard, receiptImport) != nil { + return ErrReceiptImport + } + s.imports[shard] = append(s.imports[shard], receiptImport) + return nil +} + +func (s *Service) HandleTransaction(_ context.Context, _ peer.ID, payload []byte) ([]byte, error) { + transaction, err := tx.ParseTransaction(payload) + if err != nil { + return nil, err + } + if err := s.Submit(transaction); err != nil { + return nil, err + } + id := transaction.ID() + return append([]byte(nil), id[:]...), nil +} + +func (s *Service) HandleConsensus(_ context.Context, _ peer.ID, payload []byte) ([]byte, error) { + message, err := ParseConsensusMessage(payload) + if err != nil { + return nil, err + } + s.mu.Lock() + defer s.mu.Unlock() + switch message.Kind { + case NetworkMessageProposal: + return s.handleProposalLocked(message) + case NetworkMessageCommit: + return s.handleCommitLocked(message) + default: + return nil, ErrNetworkWire + } +} + +func (s *Service) Propose(ctx context.Context) (lightapi.Snapshot, error) { + s.proposeMu.Lock() + defer s.proposeMu.Unlock() + + s.mu.Lock() + if s.Key == nil { + s.mu.Unlock() + return lightapi.Snapshot{}, ErrNotValidator + } + height := s.Runtime.Height + 1 + const round uint64 = 0 + expected, err := s.Validators.Proposer(height, round) + if err != nil { + s.mu.Unlock() + return lightapi.Snapshot{}, err + } + localID, err := validatorID(s.Key) + if err != nil || localID != expected.ID { + s.mu.Unlock() + return lightapi.Snapshot{}, ErrNotProposer + } + block := s.pendingBlockLocked() + candidate, err := s.Runtime.BuildCandidate(height, block.Batches) + if err != nil { + s.mu.Unlock() + return lightapi.Snapshot{}, err + } + proposal, err := v2consensus.SignProposal(s.Key, candidate.Header, round) + if err != nil { + s.mu.Unlock() + return lightapi.Snapshot{}, err + } + selfVote, err := s.signVoteLocked(proposal) + peers := append([]peer.ID(nil), s.Peers...) + if err != nil { + s.mu.Unlock() + return lightapi.Snapshot{}, err + } + s.mu.Unlock() + + proposalWire, err := (ConsensusMessage{Kind: NetworkMessageProposal, Proposal: proposal, Block: block}).MarshalBinary() + if err != nil { + return lightapi.Snapshot{}, err + } + votes := []v2consensus.Vote{selfVote} + for _, remote := range peers { + response, err := s.P2P.SendConsensus(ctx, remote, proposalWire) + if err != nil { + continue + } + vote, err := v2consensus.ParseVote(response) + if err != nil || vote.HeaderHash != v2consensus.HeaderConsensusHash(proposal.Header) || vote.Height != height || vote.Round != round || s.Validators.VerifyVote(vote) != nil { + continue + } + votes = append(votes, vote) + } + certificate, err := s.Validators.BuildCertificate(proposal, votes) + if err != nil { + return lightapi.Snapshot{}, ErrNoQuorum + } + + s.mu.Lock() + finalized, err := s.Runtime.Commit(candidate, certificate, s.Validators) + if err != nil { + s.mu.Unlock() + return lightapi.Snapshot{}, err + } + snapshot := s.recordFinalizedLocked(finalized, certificate, candidate.Commitments) + s.removeCommittedLocked(block) + s.mu.Unlock() + + commitWire, err := (ConsensusMessage{Kind: NetworkMessageCommit, Proposal: proposal, Block: block, Certificate: &certificate}).MarshalBinary() + if err == nil { + for _, remote := range peers { + _, _ = s.P2P.SendConsensus(ctx, remote, commitWire) + } + } + return snapshot, nil +} + +func (s *Service) handleProposalLocked(message ConsensusMessage) ([]byte, error) { + if s.Key == nil { + return nil, ErrNotValidator + } + if err := s.Validators.VerifyProposal(message.Proposal); err != nil { + return nil, err + } + if message.Proposal.Header.Height != s.Runtime.Height+1 { + return nil, ErrCandidateHeight + } + candidate, err := s.Runtime.BuildCandidate(message.Proposal.Header.Height, message.Block.Batches) + if err != nil { + return nil, err + } + if !sameProposalHeader(candidate.Header, message.Proposal.Header) { + return nil, ErrProposalState + } + vote, err := s.signVoteLocked(message.Proposal) + if err != nil { + return nil, err + } + return vote.MarshalBinary() +} + +func (s *Service) handleCommitLocked(message ConsensusMessage) ([]byte, error) { + if message.Certificate == nil || s.Validators.VerifyProposal(message.Proposal) != nil { + return nil, ErrCandidateCert + } + certificate := *message.Certificate + if certificate.HeaderHash != v2consensus.HeaderConsensusHash(message.Proposal.Header) || certificate.Height != message.Proposal.Header.Height || certificate.Round != message.Proposal.Round || s.Validators.VerifyCertificate(certificate) != nil { + return nil, ErrCandidateCert + } + if message.Proposal.Header.Height <= s.Runtime.Height { + if message.Proposal.Header.Height == s.Runtime.Height && s.latest != nil && s.latest.Header.Hash() == message.Proposal.Header.Hash() { + return []byte{1}, nil + } + return nil, ErrCandidateHeight + } + if message.Proposal.Header.Height != s.Runtime.Height+1 { + return nil, ErrCandidateHeight + } + candidate, err := s.Runtime.BuildCandidate(message.Proposal.Header.Height, message.Block.Batches) + if err != nil || !sameProposalHeader(candidate.Header, message.Proposal.Header) { + return nil, ErrProposalState + } + finalized, err := s.Runtime.Commit(candidate, certificate, s.Validators) + if err != nil { + return nil, err + } + s.recordFinalizedLocked(finalized, certificate, candidate.Commitments) + s.removeCommittedLocked(message.Block) + return []byte{1}, nil +} + +func (s *Service) signVoteLocked(proposal v2consensus.Proposal) (v2consensus.Vote, error) { + if s.Key == nil { + return v2consensus.Vote{}, ErrNotValidator + } + slot := voteSlot{Height: proposal.Header.Height, Round: proposal.Round} + target := v2consensus.HeaderConsensusHash(proposal.Header) + if prior, exists := s.votes[slot]; exists { + if prior.HeaderHash != target { + return v2consensus.Vote{}, ErrDoubleVote + } + return prior, nil + } + vote, err := v2consensus.SignVote(s.Key, s.Runtime.Network, proposal.Header.Height, proposal.Round, target) + if err != nil { + return v2consensus.Vote{}, err + } + if err := s.Validators.VerifyVote(vote); err != nil { + return v2consensus.Vote{}, err + } + s.votes[slot] = vote + return vote, nil +} + +func (s *Service) pendingBlockLocked() BlockData { + batches := make(map[uint32]ShardBatch) + for shard := uint32(0); shard < s.Runtime.ShardCount; shard++ { + transactions := append([]tx.Transaction(nil), s.mempool[shard]...) + imports := append([]ReceiptImport(nil), s.imports[shard]...) + if len(transactions) > 0 || len(imports) > 0 { + batches[shard] = ShardBatch{Transactions: transactions, Imports: imports} + } + } + return BlockData{Batches: batches} +} + +func (s *Service) removeCommittedLocked(block BlockData) { + included := make(map[types.Hash]struct{}) + for shard, batch := range block.Batches { + for _, transaction := range batch.Transactions { + included[transaction.ID()] = struct{}{} + } + if len(batch.Imports) > 0 { + s.imports[shard] = nil + } + } + s.mempoolInputs = make(map[types.ObjectID]types.Hash) + for shard, transactions := range s.mempool { + keep := transactions[:0] + for _, transaction := range transactions { + if _, ok := included[transaction.ID()]; ok { + continue + } + keep = append(keep, transaction) + for _, input := range transaction.Inputs { + s.mempoolInputs[input.ObjectID] = transaction.ID() + } + } + s.mempool[shard] = keep + } +} + +func (s *Service) recordFinalizedLocked(header interface{ CanonicalBytes() []byte }, certificate v2consensus.Certificate, commitments []interface{}) lightapi.Snapshot { + panic("unreachable") +} + +func (s *Service) LatestSnapshot() (lightapi.Snapshot, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.latest == nil { + return lightapi.Snapshot{}, ErrNoSnapshot + } + return cloneSnapshot(*s.latest), nil +} + +func (s *Service) ShardState(shardID uint32) (worldstate.Backend, bool) { + s.mu.Lock() + defer s.mu.Unlock() + store, ok := s.Runtime.States[shardID] + return store, ok +} + +func sameProposalHeader(a, b interface{ CanonicalBytes() []byte }) bool { + return bytes.Equal(a.CanonicalBytes(), b.CanonicalBytes()) +} + +func validatorID(key *ecdsa.PrivateKey) (types.ValidatorID, error) { + if key == nil || key.Curve == nil { + return types.ValidatorID{}, ErrNotValidator + } + public := ellipticMarshal(key) + if len(public) != 65 { + return types.ValidatorID{}, ErrNotValidator + } + return types.ValidatorIDFromPublicKey(public), nil +} + +func ellipticMarshal(key *ecdsa.PrivateKey) []byte { + return key.PublicKey.Curve.Params().NameBytes(key.PublicKey.X, key.PublicKey.Y) +} + +func containsValidator(set v2consensus.ValidatorSet, id types.ValidatorID) bool { + for _, validator := range set.Validators { + if validator.ID == id { + return true + } + } + return false +} + +func cloneSnapshot(snapshot lightapi.Snapshot) lightapi.Snapshot { + out := snapshot + out.Certificate.Votes = append([]v2consensus.Vote(nil), snapshot.Certificate.Votes...) + out.Commitments = append(out.Commitments[:0:0], snapshot.Commitments...) + out.Validators.Validators = append(out.Validators.Validators[:0:0], snapshot.Validators.Validators...) + for i := range out.Validators.Validators { + out.Validators.Validators[i].PublicKey = append([]byte(nil), snapshot.Validators.Validators[i].PublicKey...) + } + return out +} + +func sortedPeers(peers []peer.ID) []peer.ID { + out := append([]peer.ID(nil), peers...) + sort.Slice(out, func(i, j int) bool { return out[i].String() < out[j].String() }) + return out +} From 49cc52e7bbc3ca661f14dae319781a651217293c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:51:34 +0200 Subject: [PATCH 128/274] make v2 network consensus service state-safe and snapshot-aware --- internal/v2/node/service.go | 79 ++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 44 deletions(-) diff --git a/internal/v2/node/service.go b/internal/v2/node/service.go index 392b7c37..eb69b3eb 100644 --- a/internal/v2/node/service.go +++ b/internal/v2/node/service.go @@ -4,8 +4,8 @@ import ( "bytes" "context" "crypto/ecdsa" + "crypto/elliptic" "errors" - "sort" "sync" "github.com/libp2p/go-libp2p/core/peer" @@ -14,6 +14,7 @@ import ( "github.com/zephyr-chain/zephyr-chain/internal/v2/execution" "github.com/zephyr-chain/zephyr-chain/internal/v2/lightapi" p2p "github.com/zephyr-chain/zephyr-chain/internal/v2/network/p2p" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" @@ -207,7 +208,7 @@ func (s *Service) Propose(ctx context.Context) (lightapi.Snapshot, error) { if err != nil { return lightapi.Snapshot{}, err } - votes := []v2consensus.Vote{selfVote} + votesByID := map[types.ValidatorID]v2consensus.Vote{selfVote.Voter: selfVote} for _, remote := range peers { response, err := s.P2P.SendConsensus(ctx, remote, proposalWire) if err != nil { @@ -217,6 +218,10 @@ func (s *Service) Propose(ctx context.Context) (lightapi.Snapshot, error) { if err != nil || vote.HeaderHash != v2consensus.HeaderConsensusHash(proposal.Header) || vote.Height != height || vote.Round != round || s.Validators.VerifyVote(vote) != nil { continue } + votesByID[vote.Voter] = vote + } + votes := make([]v2consensus.Vote, 0, len(votesByID)) + for _, vote := range votesByID { votes = append(votes, vote) } certificate, err := s.Validators.BuildCertificate(proposal, votes) @@ -231,7 +236,7 @@ func (s *Service) Propose(ctx context.Context) (lightapi.Snapshot, error) { return lightapi.Snapshot{}, err } snapshot := s.recordFinalizedLocked(finalized, certificate, candidate.Commitments) - s.removeCommittedLocked(block) + s.clearCommittedStateLocked(block) s.mu.Unlock() commitWire, err := (ConsensusMessage{Kind: NetworkMessageCommit, Proposal: proposal, Block: block, Certificate: &certificate}).MarshalBinary() @@ -276,7 +281,7 @@ func (s *Service) handleCommitLocked(message ConsensusMessage) ([]byte, error) { return nil, ErrCandidateCert } if message.Proposal.Header.Height <= s.Runtime.Height { - if message.Proposal.Header.Height == s.Runtime.Height && s.latest != nil && s.latest.Header.Hash() == message.Proposal.Header.Hash() { + if message.Proposal.Header.Height == s.Runtime.Height && s.latest != nil && v2consensus.HeaderConsensusHash(s.latest.Header) == v2consensus.HeaderConsensusHash(message.Proposal.Header) { return []byte{1}, nil } return nil, ErrCandidateHeight @@ -293,7 +298,7 @@ func (s *Service) handleCommitLocked(message ConsensusMessage) ([]byte, error) { return nil, err } s.recordFinalizedLocked(finalized, certificate, candidate.Commitments) - s.removeCommittedLocked(message.Block) + s.clearCommittedStateLocked(message.Block) return []byte{1}, nil } @@ -332,34 +337,27 @@ func (s *Service) pendingBlockLocked() BlockData { return BlockData{Batches: batches} } -func (s *Service) removeCommittedLocked(block BlockData) { - included := make(map[types.Hash]struct{}) +func (s *Service) clearCommittedStateLocked(block BlockData) { + // Proof-carrying transactions are bound to the pre-commit state root, so any + // uncommitted transaction must refresh its witnesses after finality. + s.mempool = make(map[uint32][]tx.Transaction) + s.mempoolInputs = make(map[types.ObjectID]types.Hash) for shard, batch := range block.Batches { - for _, transaction := range batch.Transactions { - included[transaction.ID()] = struct{}{} - } if len(batch.Imports) > 0 { s.imports[shard] = nil } } - s.mempoolInputs = make(map[types.ObjectID]types.Hash) - for shard, transactions := range s.mempool { - keep := transactions[:0] - for _, transaction := range transactions { - if _, ok := included[transaction.ID()]; ok { - continue - } - keep = append(keep, transaction) - for _, input := range transaction.Inputs { - s.mempoolInputs[input.ObjectID] = transaction.ID() - } + for slot := range s.votes { + if slot.Height <= s.Runtime.Height { + delete(s.votes, slot) } - s.mempool[shard] = keep } } -func (s *Service) recordFinalizedLocked(header interface{ CanonicalBytes() []byte }, certificate v2consensus.Certificate, commitments []interface{}) lightapi.Snapshot { - panic("unreachable") +func (s *Service) recordFinalizedLocked(header sharding.GlobalHeader, certificate v2consensus.Certificate, commitments []sharding.Commitment) lightapi.Snapshot { + snapshot := lightapi.Snapshot{Header: header, Certificate: certificate, Commitments: append([]sharding.Commitment(nil), commitments...), Validators: cloneValidatorSet(s.Validators)} + s.latest = &snapshot + return cloneSnapshot(snapshot) } func (s *Service) LatestSnapshot() (lightapi.Snapshot, error) { @@ -378,25 +376,18 @@ func (s *Service) ShardState(shardID uint32) (worldstate.Backend, bool) { return store, ok } -func sameProposalHeader(a, b interface{ CanonicalBytes() []byte }) bool { +func sameProposalHeader(a, b sharding.GlobalHeader) bool { return bytes.Equal(a.CanonicalBytes(), b.CanonicalBytes()) } func validatorID(key *ecdsa.PrivateKey) (types.ValidatorID, error) { - if key == nil || key.Curve == nil { - return types.ValidatorID{}, ErrNotValidator - } - public := ellipticMarshal(key) - if len(public) != 65 { + if key == nil || key.Curve != elliptic.P256() || key.PublicKey.X == nil || key.PublicKey.Y == nil { return types.ValidatorID{}, ErrNotValidator } + public := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) return types.ValidatorIDFromPublicKey(public), nil } -func ellipticMarshal(key *ecdsa.PrivateKey) []byte { - return key.PublicKey.Curve.Params().NameBytes(key.PublicKey.X, key.PublicKey.Y) -} - func containsValidator(set v2consensus.ValidatorSet, id types.ValidatorID) bool { for _, validator := range set.Validators { if validator.ID == id { @@ -406,19 +397,19 @@ func containsValidator(set v2consensus.ValidatorSet, id types.ValidatorID) bool return false } -func cloneSnapshot(snapshot lightapi.Snapshot) lightapi.Snapshot { - out := snapshot - out.Certificate.Votes = append([]v2consensus.Vote(nil), snapshot.Certificate.Votes...) - out.Commitments = append(out.Commitments[:0:0], snapshot.Commitments...) - out.Validators.Validators = append(out.Validators.Validators[:0:0], snapshot.Validators.Validators...) - for i := range out.Validators.Validators { - out.Validators.Validators[i].PublicKey = append([]byte(nil), snapshot.Validators.Validators[i].PublicKey...) +func cloneValidatorSet(set v2consensus.ValidatorSet) v2consensus.ValidatorSet { + out := v2consensus.ValidatorSet{Network: set.Network, Validators: make([]v2consensus.Validator, len(set.Validators))} + for i, validator := range set.Validators { + out.Validators[i] = validator + out.Validators[i].PublicKey = append([]byte(nil), validator.PublicKey...) } return out } -func sortedPeers(peers []peer.ID) []peer.ID { - out := append([]peer.ID(nil), peers...) - sort.Slice(out, func(i, j int) bool { return out[i].String() < out[j].String() }) +func cloneSnapshot(snapshot lightapi.Snapshot) lightapi.Snapshot { + out := snapshot + out.Certificate.Votes = append([]v2consensus.Vote(nil), snapshot.Certificate.Votes...) + out.Commitments = append([]sharding.Commitment(nil), snapshot.Commitments...) + out.Validators = cloneValidatorSet(snapshot.Validators) return out } From b99c571fa0bb6df628e0ee6d83e87ed478e95528 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:51:57 +0200 Subject: [PATCH 129/274] test three-validator QUIC proposal vote and commit service --- internal/v2/node/service_test.go | 108 +++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 internal/v2/node/service_test.go diff --git a/internal/v2/node/service_test.go b/internal/v2/node/service_test.go new file mode 100644 index 00000000..7ae59b87 --- /dev/null +++ b/internal/v2/node/service_test.go @@ -0,0 +1,108 @@ +package node + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + "time" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + p2p "github.com/zephyr-chain/zephyr-chain/internal/v2/network/p2p" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestConsensusServiceFinalizesAcrossThreeQUICValidators(t *testing.T) { + networkID := types.NetworkID(types.HashBytes("network", []byte("service-three"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + keys := make([]*ecdsa.PrivateKey, 3) + validators := v2consensus.ValidatorSet{Network: networkID, Validators: make([]v2consensus.Validator, 3)} + for i := range keys { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + keys[i] = key + public := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + validators.Validators[i] = v2consensus.Validator{ID: types.ValidatorIDFromPublicKey(public), PublicKey: public, Power: 10} + } + validatorRoot, err := validators.Root() + if err != nil { + t.Fatal(err) + } + + nodes := make([]*p2p.Node, 3) + services := make([]*Service, 3) + for i := range nodes { + node, err := p2p.New(p2p.Config{Network: networkID, ListenAddrs: []string{"/ip4/127.0.0.1/udp/0/quic-v1"}, MaxMessageBytes: MaxNetworkMessageBytes}) + if err != nil { + t.Fatal(err) + } + nodes[i] = node + defer node.Close() + runtime, err := NewRuntime(networkID, native, validatorRoot, map[uint32]worldstate.Backend{0: worldstate.NewMemory()}, 2) + if err != nil { + t.Fatal(err) + } + service, err := NewService(runtime, validators, keys[i], node) + if err != nil { + t.Fatal(err) + } + services[i] = service + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + for i := range nodes { + peers := make([]string, 0, len(nodes)-1) + _ = peers + ids := make([]interface{}, 0) + _ = ids + for j := range nodes { + if i == j { + continue + } + if err := nodes[i].Connect(ctx, nodes[j].AddrInfo()); err != nil { + t.Fatal(err) + } + } + peerIDs := make([]peer.ID, 0, len(nodes)-1) + for j := range nodes { + if i != j { + peerIDs = append(peerIDs, nodes[j].ID()) + } + services[i].SetPeers(peerIDs) + } + expected, err := validators.Proposer(1, 0) + if err != nil { + t.Fatal(err) + } + proposer := -1 + for i, key := range keys { + id, _ := validatorID(key) + if id == expected.ID { + proposer = i + break + } + } + if proposer < 0 { + t.Fatal("scheduled proposer not found") + } + snapshot, err := services[proposer].Propose(ctx) + if err != nil { + t.Fatal(err) + } + if snapshot.Header.Height != 1 { + t.Fatalf("unexpected finalized height %d", snapshot.Header.Height) + } + for i, service := range services { + if service.Runtime.Height != 1 { + t.Fatalf("validator %d did not commit height 1", i) + } + latest, err := service.LatestSnapshot() + if err != nil || latest.Header.Height != 1 { + t.Fatalf("validator %d has no finalized snapshot: %v", i, err) + } + } +} From 68c63c6bb99e4f25279c85d0f4f20301f41690e4 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:11:21 +0200 Subject: [PATCH 130/274] fix v2 QUIC consensus service test --- internal/v2/node/service_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/v2/node/service_test.go b/internal/v2/node/service_test.go index 7ae59b87..75c7fc5d 100644 --- a/internal/v2/node/service_test.go +++ b/internal/v2/node/service_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/libp2p/go-libp2p/core/peer" + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" p2p "github.com/zephyr-chain/zephyr-chain/internal/v2/network/p2p" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" @@ -55,10 +57,6 @@ func TestConsensusServiceFinalizesAcrossThreeQUICValidators(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() for i := range nodes { - peers := make([]string, 0, len(nodes)-1) - _ = peers - ids := make([]interface{}, 0) - _ = ids for j := range nodes { if i == j { continue @@ -72,6 +70,7 @@ func TestConsensusServiceFinalizesAcrossThreeQUICValidators(t *testing.T) { if i != j { peerIDs = append(peerIDs, nodes[j].ID()) } + } services[i].SetPeers(peerIDs) } expected, err := validators.Proposer(1, 0) From 181a5cf225c0a000dc1b166707d71c45817bac0b Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:11:32 +0200 Subject: [PATCH 131/274] fix v2 light api validator power encoding --- internal/v2/lightapi/dto.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/internal/v2/lightapi/dto.go b/internal/v2/lightapi/dto.go index a0d96d86..0b220c04 100644 --- a/internal/v2/lightapi/dto.go +++ b/internal/v2/lightapi/dto.go @@ -1,9 +1,6 @@ package lightapi -import ( - "encoding/json" - "strconv" -) +import "encoding/json" func (v validatorDTO) MarshalJSON() ([]byte, error) { return json.Marshal(struct { @@ -11,6 +8,6 @@ func (v validatorDTO) MarshalJSON() ([]byte, error) { PublicKey []byte `json:"publicKey"` Power string `json:"power"` }{ - ID: v.ID, PublicKey: v.PublicKey, Power: strconv.FormatUint(v.Power, 10), + ID: v.ID, PublicKey: v.PublicKey, Power: v.Power, }) } From 4197b27821964c8324557483d9337abb7789cf4f Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:38:12 +0200 Subject: [PATCH 132/274] add normalized compute work model --- internal/v2/compute/work.go | 224 ++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 internal/v2/compute/work.go diff --git a/internal/v2/compute/work.go b/internal/v2/compute/work.go new file mode 100644 index 00000000..fab65e2b --- /dev/null +++ b/internal/v2/compute/work.go @@ -0,0 +1,224 @@ +package compute + +import ( + "errors" + "math" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const WorkSpecVersion uint16 = 1 + +type WorkClass uint8 + +const ( + WorkUnknown WorkClass = iota + WorkCPUGeneral + WorkGPUFP32 + WorkGPUFP64 + WorkTensorAI + WorkMemory + WorkStorage + WorkNetwork + WorkRendering + WorkAIInference + WorkAITraining + WorkScientific + WorkClassCount +) + +var ( + ErrInvalidWorkSpec = errors.New("invalid normalized compute work specification") + ErrInvalidWorkRegistry = errors.New("invalid normalized compute work registry update") + ErrInvalidWorkSettlement = errors.New("invalid verified compute work settlement") +) + +// WorkVector is intentionally a vector rather than a single synthetic FLOP +// number. Units are protocol-defined normalized work units for CPU/GPU classes +// plus directly measurable byte-based resource dimensions. +type WorkVector struct { + CPUUnits uint64 + GPUFP32Units uint64 + GPUFP64Units uint64 + TensorUnits uint64 + MemoryByteSeconds uint64 + VRAMByteSeconds uint64 + StorageBytes uint64 + NetworkBytes uint64 +} + +func (v WorkVector) IsZero() bool { + return v.CPUUnits == 0 && v.GPUFP32Units == 0 && v.GPUFP64Units == 0 && v.TensorUnits == 0 && + v.MemoryByteSeconds == 0 && v.VRAMByteSeconds == 0 && v.StorageBytes == 0 && v.NetworkBytes == 0 +} + +// WorkSpec binds a normalized unit definition to a concrete workload and a +// benchmark/specification hash. Only registry-approved specs should be used by +// monetary telemetry; arbitrary provider declarations are never sufficient. +type WorkSpec struct { + Version uint16 + Class WorkClass + Units uint64 + WorkloadHash types.Hash + BenchmarkHash types.Hash + Vector WorkVector +} + +func (s WorkSpec) Validate() error { + if s.Version != WorkSpecVersion || s.Class <= WorkUnknown || s.Class >= WorkClassCount || s.Units == 0 || + types.IsZero32([32]byte(s.WorkloadHash)) || types.IsZero32([32]byte(s.BenchmarkHash)) || s.Vector.IsZero() { + return ErrInvalidWorkSpec + } + return nil +} + +func (s WorkSpec) MarshalBinary() ([]byte, error) { + if err := s.Validate(); err != nil { + return nil, err + } + var w codec.Writer + w.U16(s.Version) + w.U8(uint8(s.Class)) + w.U64(s.Units) + w.Fixed(s.WorkloadHash[:]) + w.Fixed(s.BenchmarkHash[:]) + w.U64(s.Vector.CPUUnits) + w.U64(s.Vector.GPUFP32Units) + w.U64(s.Vector.GPUFP64Units) + w.U64(s.Vector.TensorUnits) + w.U64(s.Vector.MemoryByteSeconds) + w.U64(s.Vector.VRAMByteSeconds) + w.U64(s.Vector.StorageBytes) + w.U64(s.Vector.NetworkBytes) + return w.BytesCopy(), nil +} + +func ParseWorkSpec(data []byte) (WorkSpec, error) { + r := codec.NewReader(data) + version, err := r.U16() + if err != nil { + return WorkSpec{}, ErrInvalidWorkSpec + } + class, err := r.U8() + if err != nil { + return WorkSpec{}, ErrInvalidWorkSpec + } + units, err := r.U64() + if err != nil { + return WorkSpec{}, ErrInvalidWorkSpec + } + workload, err := readHash(r) + if err != nil { + return WorkSpec{}, ErrInvalidWorkSpec + } + benchmark, err := readHash(r) + if err != nil { + return WorkSpec{}, ErrInvalidWorkSpec + } + values := make([]uint64, 8) + for i := range values { + values[i], err = r.U64() + if err != nil { + return WorkSpec{}, ErrInvalidWorkSpec + } + } + if r.Done() != nil { + return WorkSpec{}, ErrInvalidWorkSpec + } + out := WorkSpec{ + Version: version, + Class: WorkClass(class), + Units: units, + WorkloadHash: workload, + BenchmarkHash: benchmark, + Vector: WorkVector{ + CPUUnits: values[0], GPUFP32Units: values[1], GPUFP64Units: values[2], TensorUnits: values[3], + MemoryByteSeconds: values[4], VRAMByteSeconds: values[5], StorageBytes: values[6], NetworkBytes: values[7], + }, + } + if err := out.Validate(); err != nil { + return WorkSpec{}, err + } + return out, nil +} + +type WorkRegistry struct { + byWorkload map[types.Hash]WorkSpec +} + +func NewWorkRegistry(specs []WorkSpec) (*WorkRegistry, error) { + registry := &WorkRegistry{byWorkload: make(map[types.Hash]WorkSpec, len(specs))} + for _, spec := range specs { + if err := registry.Register(spec); err != nil { + return nil, err + } + } + return registry, nil +} + +func (r *WorkRegistry) Register(spec WorkSpec) error { + if r == nil || spec.Validate() != nil { + return ErrInvalidWorkRegistry + } + if r.byWorkload == nil { + r.byWorkload = make(map[types.Hash]WorkSpec) + } + if existing, ok := r.byWorkload[spec.WorkloadHash]; ok { + a, _ := existing.MarshalBinary() + b, _ := spec.MarshalBinary() + if string(a) != string(b) { + return ErrInvalidWorkRegistry + } + return nil + } + r.byWorkload[spec.WorkloadHash] = spec + return nil +} + +func (r *WorkRegistry) Resolve(workload types.Hash) (WorkSpec, bool) { + if r == nil { + return WorkSpec{}, false + } + spec, ok := r.byWorkload[workload] + return spec, ok +} + +// VerifiedWork is an index-eligible observation. It can only be derived from a +// finalized/verified on-chain settlement plus a workload spec already approved +// by the protocol registry. Offer prices and provider self-reported capacity do +// not enter this record. +type VerifiedWork struct { + JobID types.JobID + Class WorkClass + Units uint64 + Vector WorkVector + PaidZPH uint64 + Verification VerificationMode + ResultRoot types.Hash +} + +func ObserveVerifiedWork(record OnChainJob, settlement OnChainSettlement, registry *WorkRegistry) (VerifiedWork, error) { + if record.Status != JobSettled || registry == nil || settlement.JobID != record.ID || + types.IsZero32([32]byte(settlement.ResultRoot)) { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + spec, ok := registry.Resolve(record.Job.WorkloadHash) + if !ok || spec.Validate() != nil { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + var paid uint64 + for _, amount := range settlement.Payments { + if math.MaxUint64-paid < amount { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + paid += amount + } + if paid == 0 { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + return VerifiedWork{ + JobID: record.ID, Class: spec.Class, Units: spec.Units, Vector: spec.Vector, PaidZPH: paid, + Verification: record.Job.Verification, ResultRoot: settlement.ResultRoot, + }, nil +} From 8917f967612173522086a3f21a9083bae65a3266 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:38:34 +0200 Subject: [PATCH 133/274] test normalized verified compute work --- internal/v2/compute/work_test.go | 83 ++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 internal/v2/compute/work_test.go diff --git a/internal/v2/compute/work_test.go b/internal/v2/compute/work_test.go new file mode 100644 index 00000000..6290ac50 --- /dev/null +++ b/internal/v2/compute/work_test.go @@ -0,0 +1,83 @@ +package compute + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestWorkSpecRoundTripAndVerifiedSettlementObservation(t *testing.T) { + var workload, benchmark, resultRoot types.Hash + var jobID types.JobID + var provider types.AccountID + workload[0] = 1 + benchmark[0] = 2 + resultRoot[0] = 3 + jobID[0] = 4 + provider[0] = 5 + + spec := WorkSpec{ + Version: WorkSpecVersion, + Class: WorkTensorAI, + Units: 250, + WorkloadHash: workload, + BenchmarkHash: benchmark, + Vector: WorkVector{TensorUnits: 250, VRAMByteSeconds: 8 << 30}, + } + raw, err := spec.MarshalBinary() + if err != nil { + t.Fatal(err) + } + parsed, err := ParseWorkSpec(raw) + if err != nil { + t.Fatal(err) + } + if parsed != spec { + t.Fatalf("work spec round trip mismatch: %#v != %#v", parsed, spec) + } + registry, err := NewWorkRegistry([]WorkSpec{spec}) + if err != nil { + t.Fatal(err) + } + record := OnChainJob{ + ID: jobID, + Job: Job{WorkloadHash: workload, Verification: VerificationReplicated}, + Status: JobSettled, + } + settlement := OnChainSettlement{Settlement: Settlement{ + JobID: jobID, + ResultRoot: resultRoot, + Payments: map[types.AccountID]uint64{provider: 5000}, + }} + observation, err := ObserveVerifiedWork(record, settlement, registry) + if err != nil { + t.Fatal(err) + } + if observation.PaidZPH != 5000 || observation.Units != 250 || observation.Class != WorkTensorAI { + t.Fatalf("unexpected observation: %#v", observation) + } +} + +func TestWorkRegistryRejectsConflictingDefinition(t *testing.T) { + var workload, benchmarkA, benchmarkB types.Hash + workload[0] = 1 + benchmarkA[0] = 2 + benchmarkB[0] = 3 + base := WorkSpec{ + Version: WorkSpecVersion, + Class: WorkCPUGeneral, + Units: 1, + WorkloadHash: workload, + BenchmarkHash: benchmarkA, + Vector: WorkVector{CPUUnits: 1}, + } + registry, err := NewWorkRegistry([]WorkSpec{base}) + if err != nil { + t.Fatal(err) + } + conflicting := base + conflicting.BenchmarkHash = benchmarkB + if err := registry.Register(conflicting); err != ErrInvalidWorkRegistry { + t.Fatalf("expected conflicting registry definition rejection, got %v", err) + } +} From 97003819803c858ae9bedf7129259e5d839fe76a Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:39:07 +0200 Subject: [PATCH 134/274] add shadow compute price index --- internal/v2/economics/compute_index.go | 153 +++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 internal/v2/economics/compute_index.go diff --git a/internal/v2/economics/compute_index.go b/internal/v2/economics/compute_index.go new file mode 100644 index 00000000..40680a76 --- /dev/null +++ b/internal/v2/economics/compute_index.go @@ -0,0 +1,153 @@ +package economics + +import ( + "errors" + "math/big" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" +) + +const ( + BasisPoints uint32 = 10_000 + PriceScaleQ9 uint64 = 1_000_000_000 +) + +var ErrComputeIndex = errors.New("invalid Zephyr compute price index input") + +type ComputeIndexConfig struct { + WeightsBps [compute.WorkClassCount]uint32 + MinSamplesPerClass uint32 + MinCoverageBps uint32 + EWMABps uint32 +} + +type ComputeIndexSnapshot struct { + Epoch uint64 + ClassPriceQ9 [compute.WorkClassCount]uint64 + ClassSamples [compute.WorkClassCount]uint64 + BasketPriceQ9 uint64 + CoverageBps uint32 + Reliable bool + TotalSamples uint64 +} + +func BuildComputeIndex(epoch uint64, observations []compute.VerifiedWork, prior ComputeIndexSnapshot, cfg ComputeIndexConfig) (ComputeIndexSnapshot, error) { + if epoch == 0 || cfg.MinSamplesPerClass == 0 || cfg.MinCoverageBps > BasisPoints || cfg.EWMABps > BasisPoints { + return ComputeIndexSnapshot{}, ErrComputeIndex + } + var totalWeight uint64 + for class := compute.WorkClass(1); class < compute.WorkClassCount; class++ { + totalWeight += uint64(cfg.WeightsBps[class]) + } + if totalWeight == 0 { + return ComputeIndexSnapshot{}, ErrComputeIndex + } + + prices := make([][]uint64, int(compute.WorkClassCount)) + for _, observation := range observations { + if observation.Class <= compute.WorkUnknown || observation.Class >= compute.WorkClassCount || observation.Units == 0 || observation.PaidZPH == 0 { + return ComputeIndexSnapshot{}, ErrComputeIndex + } + price, err := scaledRatio(observation.PaidZPH, PriceScaleQ9, observation.Units) + if err != nil { + return ComputeIndexSnapshot{}, err + } + prices[observation.Class] = append(prices[observation.Class], price) + } + + out := ComputeIndexSnapshot{Epoch: epoch} + basket := new(big.Int) + var activeWeight uint64 + for class := compute.WorkClass(1); class < compute.WorkClassCount; class++ { + classPrices := prices[class] + out.ClassSamples[class] = uint64(len(classPrices)) + out.TotalSamples += uint64(len(classPrices)) + if uint32(len(classPrices)) < cfg.MinSamplesPerClass || cfg.WeightsBps[class] == 0 { + continue + } + sort.Slice(classPrices, func(i, j int) bool { return classPrices[i] < classPrices[j] }) + median := medianUint64(classPrices) + current, err := ewma(prior.ClassPriceQ9[class], median, cfg.EWMABps) + if err != nil { + return ComputeIndexSnapshot{}, err + } + out.ClassPriceQ9[class] = current + weight := uint64(cfg.WeightsBps[class]) + activeWeight += weight + term := new(big.Int).Mul(new(big.Int).SetUint64(current), new(big.Int).SetUint64(weight)) + basket.Add(basket, term) + } + if activeWeight == 0 { + return out, nil + } + basket.Div(basket, new(big.Int).SetUint64(activeWeight)) + if !basket.IsUint64() { + return ComputeIndexSnapshot{}, ErrComputeIndex + } + out.BasketPriceQ9 = basket.Uint64() + coverage := activeWeight * uint64(BasisPoints) / totalWeight + if coverage > uint64(BasisPoints) { + coverage = uint64(BasisPoints) + } + out.CoverageBps = uint32(coverage) + out.Reliable = out.CoverageBps >= cfg.MinCoverageBps + return out, nil +} + +func ComputePriceTrendBps(current, prior uint64) int32 { + if current == 0 || prior == 0 || current == prior { + return 0 + } + delta := new(big.Int).Sub(new(big.Int).SetUint64(current), new(big.Int).SetUint64(prior)) + delta.Mul(delta, new(big.Int).SetUint64(uint64(BasisPoints))) + delta.Quo(delta, new(big.Int).SetUint64(prior)) + limit := big.NewInt(int64(BasisPoints)) + negativeLimit := new(big.Int).Neg(new(big.Int).Set(limit)) + if delta.Cmp(limit) > 0 { + return int32(BasisPoints) + } + if delta.Cmp(negativeLimit) < 0 { + return -int32(BasisPoints) + } + return int32(delta.Int64()) +} + +func scaledRatio(value, scale, divisor uint64) (uint64, error) { + if divisor == 0 { + return 0, ErrComputeIndex + } + numerator := new(big.Int).Mul(new(big.Int).SetUint64(value), new(big.Int).SetUint64(scale)) + numerator.Quo(numerator, new(big.Int).SetUint64(divisor)) + if !numerator.IsUint64() { + return 0, ErrComputeIndex + } + return numerator.Uint64(), nil +} + +func medianUint64(values []uint64) uint64 { + middle := len(values) / 2 + if len(values)%2 == 1 { + return values[middle] + } + low := values[middle-1] + high := values[middle] + return low + (high-low)/2 +} + +func ewma(prior, current uint64, alphaBps uint32) (uint64, error) { + if prior == 0 || alphaBps == BasisPoints { + return current, nil + } + if alphaBps == 0 { + return prior, nil + } + left := new(big.Int).Mul(new(big.Int).SetUint64(prior), new(big.Int).SetUint64(uint64(BasisPoints-alphaBps))) + right := new(big.Int).Mul(new(big.Int).SetUint64(current), new(big.Int).SetUint64(uint64(alphaBps))) + left.Add(left, right) + left.Quo(left, new(big.Int).SetUint64(uint64(BasisPoints))) + if !left.IsUint64() { + return 0, ErrComputeIndex + } + return left.Uint64(), nil +} From 07892af0d605521d3ec4100c81f5e128bea3190c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:39:24 +0200 Subject: [PATCH 135/274] test verified compute price index --- internal/v2/economics/compute_index_test.go | 75 +++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 internal/v2/economics/compute_index_test.go diff --git a/internal/v2/economics/compute_index_test.go b/internal/v2/economics/compute_index_test.go new file mode 100644 index 00000000..8cfb84ea --- /dev/null +++ b/internal/v2/economics/compute_index_test.go @@ -0,0 +1,75 @@ +package economics + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestComputeIndexUsesVerifiedSettlementsAndCoverage(t *testing.T) { + cfg := ComputeIndexConfig{MinSamplesPerClass: 2, MinCoverageBps: 10_000, EWMABps: 10_000} + cfg.WeightsBps[compute.WorkCPUGeneral] = 5_000 + cfg.WeightsBps[compute.WorkTensorAI] = 5_000 + observations := []compute.VerifiedWork{ + verifiedWork(compute.WorkCPUGeneral, 100, 200), + verifiedWork(compute.WorkCPUGeneral, 100, 400), + verifiedWork(compute.WorkTensorAI, 100, 600), + verifiedWork(compute.WorkTensorAI, 100, 1_000), + } + snapshot, err := BuildComputeIndex(1, observations, ComputeIndexSnapshot{}, cfg) + if err != nil { + t.Fatal(err) + } + if !snapshot.Reliable || snapshot.CoverageBps != 10_000 || snapshot.TotalSamples != 4 { + t.Fatalf("unexpected index coverage: %#v", snapshot) + } + // CPU median is 3 atomic ZPH/work unit; tensor median is 8. + // With equal basket weights the deterministic midpoint is 5.5. + if snapshot.ClassPriceQ9[compute.WorkCPUGeneral] != 3*PriceScaleQ9 || + snapshot.ClassPriceQ9[compute.WorkTensorAI] != 8*PriceScaleQ9 || + snapshot.BasketPriceQ9 != 11*PriceScaleQ9/2 { + t.Fatalf("unexpected compute prices: %#v", snapshot) + } +} + +func TestComputeIndexRequiresEnoughVerifiedClasses(t *testing.T) { + cfg := ComputeIndexConfig{MinSamplesPerClass: 2, MinCoverageBps: 7_500, EWMABps: 10_000} + cfg.WeightsBps[compute.WorkCPUGeneral] = 5_000 + cfg.WeightsBps[compute.WorkTensorAI] = 5_000 + observations := []compute.VerifiedWork{ + verifiedWork(compute.WorkCPUGeneral, 100, 200), + verifiedWork(compute.WorkCPUGeneral, 100, 300), + } + snapshot, err := BuildComputeIndex(1, observations, ComputeIndexSnapshot{}, cfg) + if err != nil { + t.Fatal(err) + } + if snapshot.Reliable || snapshot.CoverageBps != 5_000 { + t.Fatalf("under-covered index must remain shadow/unreliable: %#v", snapshot) + } +} + +func TestComputePriceTrendIsBounded(t *testing.T) { + if got := ComputePriceTrendBps(300, 100); got != 10_000 { + t.Fatalf("expected positive clamp, got %d", got) + } + if got := ComputePriceTrendBps(10, 100); got != -9_000 { + t.Fatalf("unexpected negative trend %d", got) + } +} + +func verifiedWork(class compute.WorkClass, units, paid uint64) compute.VerifiedWork { + var jobID types.JobID + var root types.Hash + jobID[0] = byte(class) + root[0] = byte(class) + return compute.VerifiedWork{ + JobID: jobID, + Class: class, + Units: units, + PaidZPH: paid, + Verification: compute.VerificationReplicated, + ResultRoot: root, + } +} From 753bd36ef691ce99234c8f15faa6c2f1205fbd37 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:40:05 +0200 Subject: [PATCH 136/274] add shadow adaptive monetary controller --- internal/v2/economics/monetary.go | 214 ++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 internal/v2/economics/monetary.go diff --git a/internal/v2/economics/monetary.go b/internal/v2/economics/monetary.go new file mode 100644 index 00000000..d596c094 --- /dev/null +++ b/internal/v2/economics/monetary.go @@ -0,0 +1,214 @@ +package economics + +import ( + "errors" + "math/big" +) + +var ErrMonetaryPolicy = errors.New("invalid Zephyr adaptive monetary policy input") + +type MonetaryPolicy struct { + TargetInflationBps uint32 + MinInflationBps uint32 + MaxInflationBps uint32 + MaxEpochStepBps uint32 + EpochsPerYear uint64 + ReserveTargetBps uint32 + StakeTargetBps uint32 + UtilizationTargetBps uint32 + VelocityTargetBps uint32 + OperationsTarget uint64 + ReserveWeightBps uint32 + StakeWeightBps uint32 + UtilizationWeightBps uint32 + VelocityWeightBps uint32 + OperationsWeightBps uint32 +} + +type MonetaryMetrics struct { + Supply uint64 + CirculatingSupply uint64 + StakedSupply uint64 + ProtocolReserve uint64 + BurnedThisEpoch uint64 + FinalizedOperations uint64 + ResourceUtilizationBps uint32 + AgeWeightedVelocityBps uint32 + ComputeIndexQ9 uint64 + ComputePriceTrendBps int32 + ComputeIndexReliable bool +} + +type MonetaryDecision struct { + Shadow bool + TargetInflationBps uint32 + NetIssuanceTarget uint64 + BurnOffset uint64 + GrossMintTarget uint64 + ProjectedNetChange uint64 + ReserveRatioBps uint32 + StakeRatioBps uint32 + OperationsActivityBps uint32 + ComputeIndexQ9 uint64 + ComputePriceTrendBps int32 + ComputeIndexReliable bool +} + +func DefaultShadowPolicy() MonetaryPolicy { + return MonetaryPolicy{ + TargetInflationBps: 200, + MinInflationBps: 150, + MaxInflationBps: 250, + MaxEpochStepBps: 1, + EpochsPerYear: 365, + ReserveTargetBps: 1_000, + StakeTargetBps: 5_000, + UtilizationTargetBps: 5_000, + VelocityTargetBps: 5_000, + OperationsTarget: 1_000_000, + ReserveWeightBps: 500, + StakeWeightBps: 500, + UtilizationWeightBps: 250, + VelocityWeightBps: 250, + OperationsWeightBps: 100, + } +} + +// EvaluateShadow computes the monetary action Zephyr would take for an epoch, +// but deliberately does not mutate supply. This is the only supported v2 mode +// until devnet simulations establish stability and manipulation resistance. +func EvaluateShadow(priorTargetBps uint32, metrics MonetaryMetrics, policy MonetaryPolicy) (MonetaryDecision, error) { + if err := validateMonetary(metrics, policy); err != nil { + return MonetaryDecision{}, err + } + reserveRatio := ratioBps(metrics.ProtocolReserve, metrics.Supply) + stakeRatio := ratioBps(metrics.StakedSupply, metrics.CirculatingSupply) + operationsActivity := activityBps(metrics.FinalizedOperations, policy.OperationsTarget) + + correction := int64(0) + correction += weightedGap(policy.ReserveTargetBps, reserveRatio, policy.ReserveWeightBps) + correction += weightedGap(policy.StakeTargetBps, stakeRatio, policy.StakeWeightBps) + correction += weightedGap(policy.UtilizationTargetBps, metrics.ResourceUtilizationBps, policy.UtilizationWeightBps) + correction += weightedGap(policy.VelocityTargetBps, metrics.AgeWeightedVelocityBps, policy.VelocityWeightBps) + correction += weightedGap(BasisPoints, operationsActivity, policy.OperationsWeightBps) + + target := clampTarget(int64(policy.TargetInflationBps)+correction, policy.MinInflationBps, policy.MaxInflationBps) + if priorTargetBps != 0 { + target = rateLimitTarget(priorTargetBps, target, policy.MaxEpochStepBps) + } + netTarget, err := epochIssuance(metrics.Supply, target, policy.EpochsPerYear) + if err != nil { + return MonetaryDecision{}, err + } + gross, err := addUint64(netTarget, metrics.BurnedThisEpoch) + if err != nil { + return MonetaryDecision{}, err + } + return MonetaryDecision{ + Shadow: true, + TargetInflationBps: target, + NetIssuanceTarget: netTarget, + BurnOffset: metrics.BurnedThisEpoch, + GrossMintTarget: gross, + ProjectedNetChange: netTarget, + ReserveRatioBps: reserveRatio, + StakeRatioBps: stakeRatio, + OperationsActivityBps: operationsActivity, + ComputeIndexQ9: metrics.ComputeIndexQ9, + ComputePriceTrendBps: metrics.ComputePriceTrendBps, + ComputeIndexReliable: metrics.ComputeIndexReliable, + }, nil +} + +func validateMonetary(metrics MonetaryMetrics, policy MonetaryPolicy) error { + if metrics.Supply == 0 || metrics.CirculatingSupply == 0 || metrics.CirculatingSupply > metrics.Supply || + metrics.StakedSupply > metrics.CirculatingSupply || metrics.ProtocolReserve > metrics.Supply || + policy.EpochsPerYear == 0 || policy.OperationsTarget == 0 || policy.MinInflationBps > policy.TargetInflationBps || + policy.TargetInflationBps > policy.MaxInflationBps || policy.MaxInflationBps > BasisPoints || + policy.ReserveTargetBps > BasisPoints || policy.StakeTargetBps > BasisPoints || + policy.UtilizationTargetBps > BasisPoints || policy.VelocityTargetBps > BasisPoints || + metrics.ResourceUtilizationBps > 2*BasisPoints || metrics.AgeWeightedVelocityBps > 2*BasisPoints { + return ErrMonetaryPolicy + } + weights := []uint32{policy.ReserveWeightBps, policy.StakeWeightBps, policy.UtilizationWeightBps, policy.VelocityWeightBps, policy.OperationsWeightBps} + for _, weight := range weights { + if weight > BasisPoints { + return ErrMonetaryPolicy + } + } + return nil +} + +func weightedGap(target, actual, weight uint32) int64 { + gap := int64(target) - int64(actual) + return gap * int64(weight) / int64(BasisPoints) +} + +func clampTarget(target int64, minimum, maximum uint32) uint32 { + if target < int64(minimum) { + return minimum + } + if target > int64(maximum) { + return maximum + } + return uint32(target) +} + +func rateLimitTarget(prior, next, maximumStep uint32) uint32 { + if maximumStep == 0 || prior == next { + return prior + } + if next > prior { + if next-prior > maximumStep { + return prior + maximumStep + } + return next + } + if prior-next > maximumStep { + return prior - maximumStep + } + return next +} + +func ratioBps(value, total uint64) uint32 { + if total == 0 { + return 0 + } + ratio := new(big.Int).Mul(new(big.Int).SetUint64(value), new(big.Int).SetUint64(uint64(BasisPoints))) + ratio.Quo(ratio, new(big.Int).SetUint64(total)) + if ratio.Uint64() > uint64(BasisPoints) { + return BasisPoints + } + return uint32(ratio.Uint64()) +} + +func activityBps(value, target uint64) uint32 { + if target == 0 { + return 0 + } + ratio := new(big.Int).Mul(new(big.Int).SetUint64(value), new(big.Int).SetUint64(uint64(BasisPoints))) + ratio.Quo(ratio, new(big.Int).SetUint64(target)) + limit := uint64(2 * BasisPoints) + if ratio.Uint64() > limit { + return uint32(limit) + } + return uint32(ratio.Uint64()) +} + +func epochIssuance(supply uint64, targetBps uint32, epochsPerYear uint64) (uint64, error) { + value := new(big.Int).Mul(new(big.Int).SetUint64(supply), new(big.Int).SetUint64(uint64(targetBps))) + value.Quo(value, new(big.Int).SetUint64(uint64(BasisPoints))) + value.Quo(value, new(big.Int).SetUint64(epochsPerYear)) + if !value.IsUint64() { + return 0, ErrMonetaryPolicy + } + return value.Uint64(), nil +} + +func addUint64(a, b uint64) (uint64, error) { + value := new(big.Int).Add(new(big.Int).SetUint64(a), new(big.Int).SetUint64(b)) + if !value.IsUint64() { + return 0, ErrMonetaryPolicy + } + return value.Uint64(), nil +} From 60bff307dbb9d3a66a9758446972585901c5fd60 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:40:23 +0200 Subject: [PATCH 137/274] test shadow adaptive monetary policy --- internal/v2/economics/monetary_test.go | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 internal/v2/economics/monetary_test.go diff --git a/internal/v2/economics/monetary_test.go b/internal/v2/economics/monetary_test.go new file mode 100644 index 00000000..7edc006b --- /dev/null +++ b/internal/v2/economics/monetary_test.go @@ -0,0 +1,77 @@ +package economics + +import "testing" + +func TestShadowMonetaryControllerOffsetsBurnAndTargetsNetIssuance(t *testing.T) { + policy := DefaultShadowPolicy() + metrics := MonetaryMetrics{ + Supply: 1_000_000_000, + CirculatingSupply: 900_000_000, + StakedSupply: 450_000_000, + ProtocolReserve: 100_000_000, + BurnedThisEpoch: 12_345, + FinalizedOperations: policy.OperationsTarget, + ResourceUtilizationBps: policy.UtilizationTargetBps, + AgeWeightedVelocityBps: policy.VelocityTargetBps, + ComputeIndexQ9: 7_500_000_000, + ComputePriceTrendBps: 250, + ComputeIndexReliable: true, + } + decision, err := EvaluateShadow(policy.TargetInflationBps, metrics, policy) + if err != nil { + t.Fatal(err) + } + if !decision.Shadow || decision.TargetInflationBps != policy.TargetInflationBps { + t.Fatalf("unexpected target decision: %#v", decision) + } + if decision.GrossMintTarget != decision.NetIssuanceTarget+metrics.BurnedThisEpoch || decision.ProjectedNetChange != decision.NetIssuanceTarget { + t.Fatalf("burn must be offset before net inflation target: %#v", decision) + } +} + +func TestShadowMonetaryControllerRateLimitsAdaptiveTarget(t *testing.T) { + policy := DefaultShadowPolicy() + metrics := MonetaryMetrics{ + Supply: 1_000_000_000, + CirculatingSupply: 900_000_000, + StakedSupply: 100_000_000, + ProtocolReserve: 1_000_000, + FinalizedOperations: 1, + ResourceUtilizationBps: 100, + AgeWeightedVelocityBps: 100, + } + decision, err := EvaluateShadow(200, metrics, policy) + if err != nil { + t.Fatal(err) + } + if decision.TargetInflationBps != 201 { + t.Fatalf("expected one-basis-point upward rate limit, got %d", decision.TargetInflationBps) + } +} + +func TestComputeIndexIsTelemetryOnlyInShadowV0(t *testing.T) { + policy := DefaultShadowPolicy() + base := MonetaryMetrics{ + Supply: 1_000_000_000, + CirculatingSupply: 900_000_000, + StakedSupply: 450_000_000, + ProtocolReserve: 100_000_000, + FinalizedOperations: policy.OperationsTarget, + ResourceUtilizationBps: policy.UtilizationTargetBps, + AgeWeightedVelocityBps: policy.VelocityTargetBps, + } + first, err := EvaluateShadow(200, base, policy) + if err != nil { + t.Fatal(err) + } + base.ComputeIndexQ9 = 99_000_000_000 + base.ComputePriceTrendBps = 10_000 + base.ComputeIndexReliable = true + second, err := EvaluateShadow(200, base, policy) + if err != nil { + t.Fatal(err) + } + if first.TargetInflationBps != second.TargetInflationBps { + t.Fatalf("compute price must remain telemetry-only before activation study: %d != %d", first.TargetInflationBps, second.TargetInflationBps) + } +} From 287ea1d7c45f43a892dee7792ee37c141b1a5c69 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:40:49 +0200 Subject: [PATCH 138/274] add Zephyr monetary shadow simulator --- cmd/zephyr-econ-sim/main.go | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 cmd/zephyr-econ-sim/main.go diff --git a/cmd/zephyr-econ-sim/main.go b/cmd/zephyr-econ-sim/main.go new file mode 100644 index 00000000..219832fc --- /dev/null +++ b/cmd/zephyr-econ-sim/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "os" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" +) + +type simulationInput struct { + PriorTargetBps uint32 `json:"priorTargetBps"` + Metrics economics.MonetaryMetrics `json:"metrics"` + Policy *economics.MonetaryPolicy `json:"policy,omitempty"` +} + +func main() { + inputPath := flag.String("input", "-", "JSON input path, or - for stdin") + flag.Parse() + reader := io.Reader(os.Stdin) + if *inputPath != "-" { + file, err := os.Open(*inputPath) + if err != nil { + fatal(err) + } + defer file.Close() + reader = file + } + var input simulationInput + decoder := json.NewDecoder(reader) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&input); err != nil { + fatal(err) + } + policy := economics.DefaultShadowPolicy() + if input.Policy != nil { + policy = *input.Policy + } + decision, err := economics.EvaluateShadow(input.PriorTargetBps, input.Metrics, policy) + if err != nil { + fatal(err) + } + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + if err := encoder.Encode(decision); err != nil { + fatal(err) + } +} + +func fatal(err error) { + _, _ = fmt.Fprintf(os.Stderr, "zephyr-econ-sim: %v\n", err) + os.Exit(1) +} From 318db21f245a8b8219ce4908fb9c780ee2e92bf0 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:41:45 +0200 Subject: [PATCH 139/274] document adaptive ZPH monetary policy and compute index --- docs/tokenomics-v2.md | 422 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 docs/tokenomics-v2.md diff --git a/docs/tokenomics-v2.md b/docs/tokenomics-v2.md new file mode 100644 index 00000000..38291ab2 --- /dev/null +++ b/docs/tokenomics-v2.md @@ -0,0 +1,422 @@ +# Zephyr v2 Tokenomics — Adaptive Monetary Policy + +Status: **design contract + shadow-mode implementation target**. + +This document defines the economic direction for ZPH in Protocol v2. It deliberately separates mechanisms that are already executable in the branch from mechanisms that must remain in shadow/simulation mode until devnet evidence demonstrates stability and manipulation resistance. + +## Goals + +Zephyr does not use a fixed maximum ZPH supply as its long-term monetary rule. The target is a deterministic, oracle-free adaptive policy with a long-run **net supply growth center near 2% per year**. + +The policy must: + +- be computable from consensus state only; +- be independently reproducible by validators, full nodes and Citizen Nodes; +- never depend on USD prices, CPI, exchanges, energy prices or other external oracles; +- distinguish fast fee-market control from slow monetary-policy control; +- use integer/fixed-point arithmetic only; +- rate-limit monetary changes; +- resist wash activity, fake offers and short-lived transaction spam; +- remain observable in shadow mode before it is allowed to mint or burn protocol supply. + +A 2% target refers to **ZPH monetary supply growth**, not real-world purchasing-power inflation. Without an external price oracle Zephyr cannot claim to track CPI or any fiat purchasing-power index. + +## 1. Two independent control loops + +### Fast loop — every block + +The fast loop prices scarce blockchain resources and can burn a base-fee component. + +Conceptually: + +```text +resource use + -> block/shard utilization + -> dynamic base fee + -> fee burn + validator/reward component +``` + +Future resource pricing should account for: + +- transaction base work; +- signature verification; +- witness/proof bytes; +- state reads/writes; +- contract fuel; +- data-availability bytes; +- cross-shard receipts; +- other consensus-critical resource units. + +The fast loop may react to congestion quickly. It does **not** directly change the long-term monetary target. + +### Slow loop — every monetary epoch + +The slow loop is the **Zephyr Adaptive Monetary Policy (ZAMP)**. + +It observes smoothed on-chain economic/security metrics and calculates the gross mint that would be required to hit the epoch net-issuance target after burn. + +The branch currently implements this controller in **shadow mode only**. + +```text +supply +burn +stake ratio +protocol reserve ratio +resource utilization +age-weighted velocity +finalized operations +compute-market telemetry + | + v +slow bounded controller + | + v +gross mint target + | + - burn already observed + v +net supply target near 2% annualized +``` + +Shadow mode computes the decision but does not mutate supply. + +## 2. Net inflation target + +For an epoch, the base target is approximately: + +```text +NetIssuanceTarget = Supply * TargetInflation / EpochsPerYear +``` + +where the default center is 200 basis points (2%). + +If `B` ZPH were burned during the epoch and `N` is the desired positive net issuance, the gross mint target is: + +```text +GrossMintTarget = N + B +``` + +therefore: + +```text +GrossMintTarget - Burn = N +``` + +Burn and mint are separate accounting flows. A high burn rate does not automatically make the currency permanently deflationary if the monetary constitution targets a positive net supply rate. + +## 3. Adaptive band and rate limit + +The target is not intended to jump with short-term activity. The current shadow reference policy uses a center, a bounded range and a maximum movement per epoch. + +The checked-in defaults are simulation parameters, **not public-mainnet constants**: + +```text +center: 2.00% +shadow minimum: 1.50% +shadow maximum: 2.50% +max change: 1 basis point / epoch +``` + +These numbers exist so the controller can be tested. They require economic simulation before activation. + +## 4. Oracle-free monetary signals + +ZAMP can use only values committed by Zephyr consensus. + +### Supply and burn + +Directly known: + +- total ZPH supply; +- circulating supply; +- ZPH burned by the fee mechanism; +- protocol-minted ZPH; +- protocol reserve. + +### Security + +Directly known: + +- staked/bonded ZPH; +- validator voting power; +- collateral/slashing state. + +The controller may gently increase incentives when staking/security coverage is below target and reduce them when coverage is comfortably above target. + +### Network utilization + +Do not use raw HTTP requests or mempool ingress. Use finalized consensus resource consumption. + +A future resource-usage index should be derived from signed/finalized work such as state writes, proof bytes, fuel, DA bytes and receipt processing. + +### Finalized operations + +Operation counts are secondary signals only. They are not sufficient alone because an attacker can generate economically meaningless activity. + +### Age-weighted monetary velocity + +Simple transfer volume is wash-tradeable. The intended Zephyr velocity metric is based on native object history and gives more weight to value that remained unspent for meaningful time before moving. + +Repeatedly cycling the same fresh coin object should therefore contribute far less than genuinely circulating older liquidity. + +Velocity must be smoothed over long windows (for example EWMA/rolling epochs) before it can influence monetary policy. + +## 5. Zephyr normalized compute work + +There is no honest universal scalar that makes every CPU, GPU, AI training job, renderer and scientific workload directly equivalent. + +Zephyr therefore uses a **resource vector**, not a fake universal FLOP count. + +The current v2 model defines normalized dimensions including: + +```text +CPUUnits +GPUFP32Units +GPUFP64Units +TensorUnits +MemoryByteSeconds +VRAMByteSeconds +StorageBytes +NetworkBytes +``` + +A standardized workload definition also carries: + +- protocol work-spec version; +- workload class; +- normalized logical work units; +- workload hash; +- benchmark/specification hash; +- resource vector. + +The benchmark hash anchors the meaning of the units. A provider cannot make its GPU appear more valuable merely by self-reporting a larger number. + +## 6. Compute workload registry + +Only protocol-approved work specifications are eligible for monetary telemetry. + +A registry entry binds: + +```text +WorkloadHash + -> WorkClass + -> normalized Units + -> WorkVector + -> BenchmarkHash +``` + +Conflicting definitions for the same workload hash are rejected. + +The initial implementation is an executable reference registry. Before monetary activation the registry must become an authenticated protocol/governance state transition with explicit versioning and activation heights. + +## 7. ZCPI — Zephyr Compute Price Index + +ZCPI is an internal Zephyr compute-market price index. It is **not** a CPI and is not a claim about real-world inflation. + +It answers a narrower question: + +> how many atomic ZPH units were actually paid for standardized, verified compute work on Zephyr? + +ZCPI deliberately excludes: + +- advertised provider prices; +- unfilled offers; +- self-reported theoretical FLOPS; +- failed/unverified jobs; +- arbitrary unregistered workload units. + +An eligible observation is generated only from: + +```text +registered workload spec ++ finalized compute job ++ verification-satisfied result ++ actual on-chain provider payments +``` + +For each workload class: + +```text +price = paid ZPH / normalized verified work units +``` + +The reference implementation uses fixed-point Q9 arithmetic, per-class medians and EWMA smoothing. + +## 8. ZCPI basket, coverage and reliability + +Different resource classes retain different prices. ZCPI may combine them into a weighted basket for telemetry, but it also reports each class separately. + +A class enters an epoch index only when it has at least the configured minimum number of verified observations. + +The index reports: + +- price per class; +- sample count per class; +- weighted basket price; +- basket coverage in basis points; +- a `Reliable` flag; +- total accepted observations. + +If too little of the configured basket has adequate data, the index remains unreliable and must not be used by monetary policy. + +This prevents the chain from manufacturing a compute-price signal during thin markets. + +## 9. Compute prices are telemetry-only in ZAMP v0 + +The branch deliberately records `ComputeIndexQ9`, compute-price trend and reliability in the shadow monetary decision **without allowing them to change the inflation target yet**. + +This is intentional. + +A rising ZPH price for compute can mean several different things: + +- compute resources became scarce; +- demand for compute increased; +- ZPH purchasing power against compute fell; +- workload mix changed. + +Without sufficient history it is unsafe to infer which cause dominates. + +Activation requires simulation showing that a compute-price feedback term improves stability rather than creating a manipulable reflexive loop. + +## 10. Native ZPH fee accounting + +Current v2 transaction execution already requires native ZPH inputs to cover `outputs + Fee`. + +Before public economic activation this must evolve into an explicit fee-accounting engine rather than an implicit 100% fee disappearance. + +The intended structure is: + +```text +transaction resource charge + | + +-- base-fee component -> burn + | + +-- execution/priority component -> validator/reward pool + | + +-- optional protocol component -> protocol reserve +``` + +Percentages and fee parameters must be integer basis points and simulation-backed. + +## 11. Smart-contract gas + +Contract execution already reports deterministic `FuelUsed` and enforces `FuelLimit`. + +The economic fee engine should convert deterministic execution/resource consumption into ZPH cost, for example conceptually: + +```text +contract fee = + base transaction resource charge + + FuelUsed * FuelPrice + + state read/write charges + + proof/data-availability charges +``` + +The wallet should sign a maximum acceptable resource/fee envelope. Validators must not be able to raise it after signing. + +## 12. Compute payment is not blockchain gas + +Heavy compute has two independent prices: + +```text +provider payment ++ blockchain settlement fee +``` + +A 100 ZPH AI job does not imply 100 ZPH of gas. Validators settle commitments/proofs/results; they do not replay the expensive workload. + +## 13. Native custom-token policy + +Protocol-native custom assets retain independent supply policies. + +The desired explicit policies are: + +```text +FIXED +CAPPED +MINTABLE +``` + +Native mint/burn operations must update both coin objects and the authenticated `TokenDefinition.CurrentSupply` so Citizen Nodes can prove supply correctness. + +Before public activation the executor must add explicit `MintToken` and `BurnToken` operations and enforce: + +- mint authority; +- cap where applicable; +- irreversible fixed-supply policy; +- burn permission; +- transferability policy. + +ZPH itself must not have a human mint authority. ZPH issuance is controlled only by the protocol monetary state machine after activation. + +## 14. Testing strategy + +### Unit/conformance tests + +The branch includes deterministic tests for: + +- normalized work-spec serialization; +- conflicting compute-registry definitions; +- deriving a compute observation only from a settled verified job; +- class medians and basket coverage; +- ZCPI reliability thresholds; +- compute price trend bounds; +- burn offset in the shadow monetary controller; +- bounded/rate-limited adaptive inflation target; +- compute telemetry having zero monetary influence in ZAMP v0. + +### Monetary replay simulator + +`cmd/zephyr-econ-sim` accepts a JSON epoch snapshot and prints the shadow decision. + +Example shape: + +```json +{ + "priorTargetBps": 200, + "metrics": { + "Supply": 1000000000, + "CirculatingSupply": 900000000, + "StakedSupply": 450000000, + "ProtocolReserve": 100000000, + "BurnedThisEpoch": 12000, + "FinalizedOperations": 1000000, + "ResourceUtilizationBps": 5000, + "AgeWeightedVelocityBps": 5000, + "ComputeIndexQ9": 7500000000, + "ComputePriceTrendBps": 250, + "ComputeIndexReliable": true + } +} +``` + +This enables historical replay, synthetic shocks and sensitivity analysis without changing consensus supply. + +## 15. Activation gates + +ZAMP remains shadow-only until all of the following are true: + +1. explicit ZPH supply/burn/mint accounting exists in authenticated protocol state; +2. fee distribution is explicit and conserves supply exactly; +3. velocity is demonstrably resistant to cheap self-cycling; +4. all monetary metrics are reproducible from finalized state; +5. long simulations cover low/high usage, partitions, validator churn, spam and compute-market shocks; +6. parameter sensitivity does not create oscillation or runaway mint/burn behavior; +7. governance can change parameters only through bounded, delayed transitions; +8. Citizen Nodes can independently verify monetary state and epoch decisions; +9. ZCPI has sufficient real-market coverage before any non-zero monetary weight is considered; +10. an emergency safety rule can freeze adaptive corrections while preserving deterministic base issuance if metrics become unavailable or invalid. + +## 16. Current policy boundary + +The current branch implements **measurement and shadow decisions**, not live monetary issuance. + +The design principle is: + +```text +measure first +simulate second +activate last +``` + +This lets Zephyr use an adaptive oracle-free economy without turning monetary policy into an untested consensus experiment. From 6793d2b5c9e61830999df900900ba71e57c12539 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:43:58 +0200 Subject: [PATCH 140/274] update v2 implementation status for compute economics --- docs/protocol-v2-implementation-status.md | 266 +++++++++++----------- 1 file changed, 133 insertions(+), 133 deletions(-) diff --git a/docs/protocol-v2-implementation-status.md b/docs/protocol-v2-implementation-status.md index dfd66388..29f9845f 100644 --- a/docs/protocol-v2-implementation-status.md +++ b/docs/protocol-v2-implementation-status.md @@ -1,11 +1,12 @@ # Zephyr Protocol v2 — Implementation Status -This document tracks what the clean-break v2 branch **actually implements**, what is integrated only as a reference boundary, and what still requires production engineering. It complements `docs/protocol-v2.md`, which remains the architectural contract. +This document tracks what the clean-break v2 branch **actually implements**, what is integrated only as a reference boundary, and what still requires production engineering. It complements `docs/protocol-v2.md`, which remains the architectural contract, and `docs/tokenomics-v2.md`, which defines the adaptive ZPH economic direction. Status legend: - **Implemented** — executable code and tests exist on the v2 branch. - **Integrated foundation** — the protocol boundary and correctness rules exist, but a production backend/network/runtime is still to be selected or connected. +- **Shadow/experimental** — executable measurement/simulation exists but cannot yet change live consensus economics. - **Not production-complete** — must not be presented as a shipped network capability yet. ## Core identity, trust and wire protocol @@ -22,10 +23,8 @@ Status legend: - canonical Merkle commitment over validator ID, P-256 public key and integer voting power; - every accepted proposal must commit the exact validator-set root used to authorize it; - runtime commit and cross-shard import reject validator sets that do not match the header's committed `ValidatorRoot`; -- v2 genesis can derive a `TrustAnchor { NetworkID, ValidatorRoot }` for Citizen wallets; -- genesis trust-anchor derivation rejects validator keys that do not satisfy the consensus P-256 validator-set rules. - -The browser/RPC surface may use JSON, but consensus does not depend on JSON canonicalization. A valid self-signed quorum from an arbitrary validator set is not sufficient: the set itself must be committed by the trusted header/genesis chain. +- v2 genesis derives a `TrustAnchor { NetworkID, ValidatorRoot }` for Citizen wallets; +- QC-authorized `NextValidatorRoot` transitions and checkpoint-history foundations. ## Object state and persistence @@ -35,7 +34,7 @@ The browser/RPC surface may use JSON, but consensus does not depend on JSON cano - 256-bit Sparse Merkle Tree with incremental updates; - compressed inclusion/absence proofs; - in-memory world-state backend; -- non-mutating state simulation through cloned Sparse Merkle state; +- non-mutating copy-on-write state preview; - durable v2 backend with append-only WAL, CRC32C records, monotonic sequence numbers, network binding, fsync, atomic checkpointing and replay; - safe truncation of a torn WAL tail after a crash; - rejection of persisted state from a different network. @@ -46,9 +45,7 @@ The browser/RPC surface may use JSON, but consensus does not depend on JSON cano - large-state migration/repair tooling; - production structured-KV/LSM backend comparison; - archive/history indexing; -- proof/state allocation reduction under large batches. - -The WAL/checkpoint backend removes the v1 requirement to serialize the complete node state for every mutation, but it is still a first durable backend rather than the final storage engine selection. +- further proof/state allocation reduction under large batches. ## Proof-carrying transactions and execution @@ -64,9 +61,17 @@ The WAL/checkpoint backend removes the v1 requirement to serialize the complete - rejection of batches with shared consumed objects, duplicate transactions or different pre-state roots; - atomic merge of independent transaction results; - state-root simulation before consensus finality; -- permanent shard placement encoded into object IDs for multi-shard state. +- permanent shard placement encoded into object IDs for multi-shard state; +- contract deploy/call execution path with metered deterministic reference runtime; +- compute-market operations represented in v2 consensus object execution. + +**Not production-complete** + +- explicit native token mint/burn operations and transfer-policy enforcement; +- explicit fee distribution object/state accounting (burn/validator/reserve split); +- finalized resource-unit gas schedule. -The key invariant is enforced in code: candidate execution may calculate a future state root, but committed state is not mutated before a valid quorum certificate exists. +The key invariant remains enforced: candidate execution may calculate a future state root, but committed state is not mutated before a valid quorum certificate exists. ## Consensus and global finality @@ -80,7 +85,10 @@ The key invariant is enforced in code: candidate execution may calculate a futur - canonical quorum-certificate hash; - validator-set Merkle root as a proposal validity invariant; - `GlobalHeader` consensus hash that avoids certificate/hash circularity; -- runtime path: +- validator rotation commitment via `NextValidatorRoot`; +- dedicated v2 seven-validator conformance and partition-stress gate. + +The runtime path remains: ```text proof-carrying transactions @@ -94,46 +102,35 @@ proof-carrying transactions -> state commit ``` -The existing v1 Consensus & Performance Lab remains a regression gate while v2-specific multi-node fault transport integration is expanded. - **Not production-complete** -- consensus-state-backed validator-set rotation and governance activation; -- trusted header/checkpoint chain for Citizen verification across validator-set rotations; -- v2-specific restart/partition/conflicting-proposal scenarios over the production transport. +- broader restart/proposer-death/Byzantine/wrong-chain fault matrix over the production transport; +- governance-controlled validator-set activation policy; +- long-horizon checkpoint pruning/recovery rules. ## Sharding **Implemented foundation** - deterministic account shard routing; -- permanent shard placement encoded in every object ID so changing active shard count cannot silently relocate existing objects; +- permanent shard placement encoded in object IDs; - per-shard state/data/receipt commitments; - global shard-commitment root; -- `GlobalHeader` committing all active shard roots; -- local outputs remain in the source shard only when their owner routes there; -- remote outputs become cross-shard receipts rather than being written into the wrong shard; +- remote outputs become finalized cross-shard receipts; - receipt Merkle batches and inclusion proofs; -- proof that a source receipt belongs to a shard commitment that belongs to a finalized global header; -- cross-shard import verifies the source quorum certificate, committed validator root, shard proof and receipt proof; -- destination object IDs are deterministic; -- receipt consumption creates a consensus-critical Merkle-state marker, making anti-replay survive restart/checkpoint/snapshot recovery; -- imported receipts cannot be spent in the same block because transactions are anchored to the block pre-state root; -- two-shard end-to-end test: source payment -> finalized receipt -> destination import -> finalized destination coin -> durable replay rejection; -- hostile self-signed foreign validator-set receipts are explicitly rejected; -- runtime can simulate and commit multiple shard state backends without pre-QC mutation. - -The old in-memory `ReceiptTracker` is only an optional transport duplicate-suppression helper; it is not the consensus anti-replay source of truth. +- destination import verifies source finality/QC, validator root, shard proof and receipt proof; +- durable Merkle-state anti-replay marker; +- two-shard end-to-end finalization/import/replay-rejection test; +- hostile self-signed foreign validator-set receipts are rejected. **Not production-complete** -- shard-aware gossip/recovery is not connected to production transport; -- validator-set history/checkpoint proofs must support cross-shard imports across committee rotations; -- reshard/split/merge rules and object migration are not activated; -- receipt-marker pruning/history-retention policy needs proof-safe design; -- 4/16-shard conformance, recovery and throughput evidence is still required. +- shard-aware gossip/recovery under sustained faults; +- reshard/split/merge rules and object migration; +- receipt-marker pruning/history-retention policy; +- 4/16-shard conformance, recovery and controlled-hardware throughput evidence. -`shardCount = 1` remains the safe public activation value until those gates pass. Sharding is an optimization, not a prerequisite for correctness. +`shardCount = 1` remains the safe public activation value until those gates pass. ## Citizen Node and smartphone wallet @@ -141,155 +138,157 @@ The old in-memory `ReceiptTracker` is only an optional transport duplicate-suppr - Go Citizen verifier for headers/state/shard/data proofs; - battery/network-aware participation policy; -- self-verifiable light API: - - `/v2/light/status`; - - `/v2/light/object`; -- proof bundle contains canonical global header, quorum certificate, validator set, shard commitment and Merkle proof, object bytes and Sparse-Merkle proof; -- light snapshots reject a supplied validator set unless its Merkle root equals the `ValidatorRoot` committed by the finalized header; -- validator voting power is encoded as decimal text at the JSON boundary to preserve full `uint64` precision in JavaScript; -- `apps/wallet/src/lib/v2Citizen.ts` independently reconstructs: - - v2 domain hashes; - - validator identities; - - low-S P-256 vote validation; - - exact `2/3+` quorum with `BigInt`; - - certificate hash; - - shard-commitment inclusion; - - object Sparse-Merkle inclusion/absence proof; -- `apps/wallet/src/lib/v2CitizenTrust.ts` is the wallet-facing trust layer and requires a genesis/checkpoint trust anchor before accepting a proof bundle; -- the trusted wallet path independently recomputes the validator-set Merkle root and requires it to match both the trusted anchor and the header; -- wallet resource mode selection for header-only, relay, DA sampling/cache and opportunistic recent execution modes. +- self-verifiable light API (`/v2/light/status`, `/v2/light/object`); +- strict wallet verifier for canonical headers, low-S P-256 votes, exact `2/3+` quorum, validator roots, shard commitments and Sparse-Merkle proofs; +- genesis/checkpoint trust anchor and next-validator-root trust advancement; +- exact `uint64` validator power handling through decimal JSON + JavaScript `BigInt`; +- wallet resource-mode selection for header-only, relay, DA sampling/cache and opportunistic recent execution. **Not production-complete** -- validator-set rotation needs a verified header/checkpoint transition chain rather than a static trusted root; -- the Vue UI does not yet expose the Citizen status/control panel; -- current v1 node process does not yet mount a live v2 runtime/provider; -- iOS/Android native lifecycle/background adapters are not present; -- multi-peer proof comparison, resumable cache and peer relay are not connected yet; -- real-device RAM/battery/bandwidth measurements are still required. - -No correctness claim may depend on an RPC response that the Citizen verifier cannot authenticate back to a genesis/checkpoint trust anchor. +- Vue Citizen status/control UI; +- iOS/Android native lifecycle/background adapters; +- multi-peer proof comparison, resumable cache and full peer relay integration; +- real-device RAM/battery/bandwidth measurements. ## Smart contracts -**Integrated foundation** +**Implemented foundation** -- deterministic WASM deployment boundary; -- module magic/shape validation boundary; -- versioned contract deployment model; -- runtime interface independent from a concrete WASM engine; -- consensus guard enforcing: - - fuel limit; - - bounded arguments and return data; - - bounded event count/size; - - declared read/write object set; - - no write outside a declared write-enabled object; - - bounded state-access count. +- versioned contract deployment/runtime boundary; +- deterministic metered Zephyr Script reference runtime; +- bounded module/request/output/event limits; +- execution-step/fuel limits; +- declared read/write object set and no undeclared writes; +- no clock/random/filesystem/network/import nondeterminism in the reference runtime; +- contract deploy/call executor integration and execution receipts. **Not production-complete** -- production WASM interpreter/JIT selection; -- deterministic opcode/import policy; -- audited fuel schedule; -- contract deploy/call operations inside the main v2 transaction executor; +- production deterministic WASM engine and audited fuel schedule; - Rust SDK/ABI tooling; -- contract conformance corpus. - -A concrete WASM runtime will not be called production-ready until deterministic metering survives cross-machine conformance testing. +- cross-machine WASM conformance corpus and long-running contract fuzzing. ## Native distributed compute market -**Implemented state-machine foundation** +**Implemented** -- compute provider offers; -- CPU/RAM/GPU/VRAM/storage/bandwidth/capability requirements; +- compute provider offers and resource/capability requirements; - collateral requirements; - job posting with escrow and deadline; - deterministic offer/job IDs; -- matching and assignment; -- multi-provider assignment for replicated verification; +- matching/assignment and multi-provider replicated verification; - provider result submission; -- settlement and unused-escrow refund; -- expiry; -- verification policies for: - - deterministic replay; - - replicated matching results; - - challenge evidence; - - zero-knowledge proof verification signal; - - TEE attestation verification signal; - - client approval; - - hybrid evidence. - -Heavy compute is provider-executed; validators verify settlement evidence and do not replay AI training, scientific simulations, rendering or other expensive workloads. +- settlement, unused-escrow refund and expiry; +- objective replicated-majority slashing path; +- compute market object serialization and consensus execution transitions; +- verification-policy boundaries for deterministic, replicated, challenge, ZK, TEE, client-approved and hybrid evidence. + +Heavy compute is provider-executed; validators verify compact settlement evidence and do not replay expensive workloads. + +**Shadow/experimental compute economics** + +- normalized `WorkVector` rather than one fake universal FLOP scalar; +- workload classes and versioned `WorkSpec` bound to `WorkloadHash` + `BenchmarkHash`; +- registry rejects conflicting definitions for the same workload hash; +- only finalized, verification-satisfied settlements can become `VerifiedWork` observations; +- offer prices and provider self-reported capacity are excluded from ZCPI observations; +- deterministic per-class price medians, Q9 fixed-point arithmetic, EWMA, basket coverage and reliability flag; +- compute-price trend is bounded; +- ZCPI is telemetry-only for monetary policy v0. **Not production-complete** -- provider daemon/scheduler; -- input/output distribution protocol; -- on-chain object integration of market state transitions; -- actual collateral slashing/dispute arbitration; +- authenticated/governance-controlled on-chain workload registry with activation heights; +- provider daemon/scheduler and input/output distribution protocol; - concrete ZK verifier integrations; - concrete TEE attestation integrations; - confidential-data key exchange; -- compute reputation and anti-collusion policy. +- compute reputation/anti-collusion policy; +- real ZCPI basket weights and minimum-sample thresholds based on observed market data. + +## ZPH tokenomics and adaptive monetary policy + +See `docs/tokenomics-v2.md`. + +**Shadow/experimental** + +- no fixed max-supply assumption in the v2 economic design; +- long-run net ZPH supply-growth center near 2% annually; +- bounded adaptive inflation target using only on-chain signals; +- reserve, staking, resource-utilization, age-weighted-velocity and finalized-operation signal inputs; +- burn-offset accounting: gross mint target equals desired net issuance plus observed burn; +- one-basis-point-per-epoch default shadow rate limit; +- ZCPI price/trend/reliability recorded as telemetry but intentionally given zero monetary influence in v0; +- `cmd/zephyr-econ-sim` replays epoch metrics and prints the deterministic shadow decision. + +**Not production-complete / not active** + +- ZAMP does not mint live ZPH yet; +- no public economic parameter set is claimed final; +- age-weighted velocity metric must be implemented from coin-object history and stress-tested against self-cycling; +- explicit fee split and resource-price controller must be state-backed; +- protocol reserve and total supply must become authenticated monetary state objects; +- governance bounds/delays and emergency fallback rules must be finalized; +- Citizen Node monetary-decision verification must be connected; +- compute price must remain telemetry-only until empirical causality/manipulation studies justify any weight. ## Data availability -**Integrated foundation** +**Implemented foundation** -- chunk commitments; -- sample proof verification boundary; -- DA root in shard/global commitments; +- data roots and authenticated chunk/sample proof boundary; +- Reed-Solomon erasure-coded shard reconstruction path; +- rejection of corrupted chunks before reconstruction; - Citizen participation mode for bounded sampling. **Not production-complete** -- production erasure-code selection; -- reconstruction; -- sampling confidence parameters; -- withholding attacks in the fault lab; -- shard-aware dissemination; +- final sampling confidence parameters; +- withholding-attack matrix in the fault lab; +- shard-aware data dissemination and repair; - mobile bandwidth/storage measurements. ## Transport -**Integrated foundation** +**Implemented foundation** -- consensus transport, transaction relay and light-proof retrieval are separate logical interfaces; -- existing HTTP remains the reference/test transport boundary; -- the architecture permits libp2p/QUIC/WebTransport without changing consensus objects. +- consensus, transaction relay and light-proof retrieval are separate protocol capabilities; +- libp2p node identity is separate from account/validator keys; +- QUIC production transport path with network-scoped protocol IDs, frame limits/deadlines and loopback tests. **Not production-complete** -- production libp2p/QUIC implementation; -- discovery/NAT traversal/mobile relay; -- shard-aware gossip; -- v2 fault-transport adapter covering the full existing Lab matrix. +- discovery/bootstrap/NAT traversal/mobile relay policy; +- shard-aware gossip topology; +- full fault-transport equivalence matrix over libp2p/QUIC. ## Performance gates **Implemented** - existing finalized-through-consensus v1 Lab remains mandatory; +- dedicated v2 seven-validator conformance/partition gate; - v2 state/proof microbenchmarks; -- v2 finalized batch benchmark with 32 proof-carrying transfers, 7 validators and 1/4/8/16 execution workers; -- the timed v2 path includes witness/signature verification, execution/state-root simulation, proposal/votes, quorum certificate and committed state transition; -- client workload setup/key generation/signing stays outside the timed consensus path, matching the Lab's canonical workload policy. +- finalized v2 batch benchmark with 32 proof-carrying transfers, 7 validators and 1/4/8/16 execution workers; +- timed path includes witness/signature verification, execution/state-root simulation, proposal/votes, QC and committed transition. -The first shared-runner v2 samples show only modest worker scaling and very high allocation pressure, so the next optimization target is proof/state allocation and incremental simulation rather than simply increasing goroutine count. No numerical result from a shared CI runner is a production-capacity claim. +Shared CI numbers are development signals only, never production-capacity claims. ## Activation gates -The clean break lets v2 replace prototype boundaries, but it does not remove the requirement to prove them. Before a public v2 devnet: +Before a public v2 devnet: -1. v2 multi-validator consensus must run through the fault-injection Lab, including partitions/restarts/conflicting evidence; -2. durable v2 state must survive crash/restart and longer stress runs; -3. Citizen verification must be exercised against a live v2 node from real Android/iOS reference devices and a genesis/checkpoint trust anchor; -4. one-shard finalized performance must be characterized on controlled hardware; -5. multi-shard mode must remain disabled until validator-history proofs, shard-aware recovery and 4/16-shard conformance pass; -6. contract execution must have a deterministic metered production runtime; -7. compute settlement must be consensus-state-backed before real value is escrowed; -8. genesis/checkpoint/operator upgrade and validator-rotation procedures must be explicit. +1. v2 multi-validator consensus must cover partitions, restarts, proposer death, conflicting/Byzantine evidence and wrong-chain data; +2. durable state/runtime metadata must survive crash/restart and longer stress runs; +3. Citizen verification must run against live nodes on real Android/iOS reference devices; +4. one-shard performance must be characterized on controlled hardware; +5. multi-shard mode must stay disabled until shard-aware recovery and 4/16-shard conformance pass; +6. production WASM must be deterministic and metered across machines; +7. real-value compute settlement needs production provider/evidence/dispute plumbing; +8. fee, supply, reserve and ZAMP accounting must be explicit authenticated state; +9. ZAMP must remain shadow-only through replay/simulation and manipulation testing; +10. genesis/checkpoint/operator upgrade and validator-rotation procedures must be explicit. The engineering rule remains: @@ -297,4 +296,5 @@ The engineering rule remains: more hardware -> more throughput less hardware -> less throughput less hardware -/-> weaker correctness +measure first -> simulate second -> activate last ``` From 986642d49b41da7290bc74a3cec51421a6afc27c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:45:40 +0200 Subject: [PATCH 141/274] gofmt normalized compute work model --- internal/v2/compute/work.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/v2/compute/work.go b/internal/v2/compute/work.go index fab65e2b..909cd090 100644 --- a/internal/v2/compute/work.go +++ b/internal/v2/compute/work.go @@ -29,8 +29,8 @@ const ( ) var ( - ErrInvalidWorkSpec = errors.New("invalid normalized compute work specification") - ErrInvalidWorkRegistry = errors.New("invalid normalized compute work registry update") + ErrInvalidWorkSpec = errors.New("invalid normalized compute work specification") + ErrInvalidWorkRegistry = errors.New("invalid normalized compute work registry update") ErrInvalidWorkSettlement = errors.New("invalid verified compute work settlement") ) @@ -127,10 +127,10 @@ func ParseWorkSpec(data []byte) (WorkSpec, error) { return WorkSpec{}, ErrInvalidWorkSpec } out := WorkSpec{ - Version: version, - Class: WorkClass(class), - Units: units, - WorkloadHash: workload, + Version: version, + Class: WorkClass(class), + Units: units, + WorkloadHash: workload, BenchmarkHash: benchmark, Vector: WorkVector{ CPUUnits: values[0], GPUFP32Units: values[1], GPUFP64Units: values[2], TensorUnits: values[3], From 56bf534d45263140442e18d70da6f1e85b7400b4 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:46:15 +0200 Subject: [PATCH 142/274] gofmt normalized compute work tests --- internal/v2/compute/work_test.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/v2/compute/work_test.go b/internal/v2/compute/work_test.go index 6290ac50..d5cbab2a 100644 --- a/internal/v2/compute/work_test.go +++ b/internal/v2/compute/work_test.go @@ -17,12 +17,12 @@ func TestWorkSpecRoundTripAndVerifiedSettlementObservation(t *testing.T) { provider[0] = 5 spec := WorkSpec{ - Version: WorkSpecVersion, - Class: WorkTensorAI, - Units: 250, - WorkloadHash: workload, + Version: WorkSpecVersion, + Class: WorkTensorAI, + Units: 250, + WorkloadHash: workload, BenchmarkHash: benchmark, - Vector: WorkVector{TensorUnits: 250, VRAMByteSeconds: 8 << 30}, + Vector: WorkVector{TensorUnits: 250, VRAMByteSeconds: 8 << 30}, } raw, err := spec.MarshalBinary() if err != nil { @@ -40,14 +40,14 @@ func TestWorkSpecRoundTripAndVerifiedSettlementObservation(t *testing.T) { t.Fatal(err) } record := OnChainJob{ - ID: jobID, - Job: Job{WorkloadHash: workload, Verification: VerificationReplicated}, + ID: jobID, + Job: Job{WorkloadHash: workload, Verification: VerificationReplicated}, Status: JobSettled, } settlement := OnChainSettlement{Settlement: Settlement{ - JobID: jobID, + JobID: jobID, ResultRoot: resultRoot, - Payments: map[types.AccountID]uint64{provider: 5000}, + Payments: map[types.AccountID]uint64{provider: 5000}, }} observation, err := ObserveVerifiedWork(record, settlement, registry) if err != nil { @@ -64,12 +64,12 @@ func TestWorkRegistryRejectsConflictingDefinition(t *testing.T) { benchmarkA[0] = 2 benchmarkB[0] = 3 base := WorkSpec{ - Version: WorkSpecVersion, - Class: WorkCPUGeneral, - Units: 1, - WorkloadHash: workload, + Version: WorkSpecVersion, + Class: WorkCPUGeneral, + Units: 1, + WorkloadHash: workload, BenchmarkHash: benchmarkA, - Vector: WorkVector{CPUUnits: 1}, + Vector: WorkVector{CPUUnits: 1}, } registry, err := NewWorkRegistry([]WorkSpec{base}) if err != nil { From 2aa758a15e9e1799779ffbe294154d732422ae25 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:47:16 +0200 Subject: [PATCH 143/274] gofmt compute price index --- internal/v2/economics/compute_index.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/v2/economics/compute_index.go b/internal/v2/economics/compute_index.go index 40680a76..ae923675 100644 --- a/internal/v2/economics/compute_index.go +++ b/internal/v2/economics/compute_index.go @@ -23,13 +23,13 @@ type ComputeIndexConfig struct { } type ComputeIndexSnapshot struct { - Epoch uint64 - ClassPriceQ9 [compute.WorkClassCount]uint64 - ClassSamples [compute.WorkClassCount]uint64 - BasketPriceQ9 uint64 - CoverageBps uint32 - Reliable bool - TotalSamples uint64 + Epoch uint64 + ClassPriceQ9 [compute.WorkClassCount]uint64 + ClassSamples [compute.WorkClassCount]uint64 + BasketPriceQ9 uint64 + CoverageBps uint32 + Reliable bool + TotalSamples uint64 } func BuildComputeIndex(epoch uint64, observations []compute.VerifiedWork, prior ComputeIndexSnapshot, cfg ComputeIndexConfig) (ComputeIndexSnapshot, error) { From 8d953f0ceb44c4a9c96072408b6db8065ae0913e Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:47:49 +0200 Subject: [PATCH 144/274] gofmt compute price index tests --- internal/v2/economics/compute_index_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/v2/economics/compute_index_test.go b/internal/v2/economics/compute_index_test.go index 8cfb84ea..370c2d78 100644 --- a/internal/v2/economics/compute_index_test.go +++ b/internal/v2/economics/compute_index_test.go @@ -65,11 +65,11 @@ func verifiedWork(class compute.WorkClass, units, paid uint64) compute.VerifiedW jobID[0] = byte(class) root[0] = byte(class) return compute.VerifiedWork{ - JobID: jobID, - Class: class, - Units: units, - PaidZPH: paid, + JobID: jobID, + Class: class, + Units: units, + PaidZPH: paid, Verification: compute.VerificationReplicated, - ResultRoot: root, + ResultRoot: root, } } From b7e68576cd0a4aea7df9cfcdbf0844063b02fd4f Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:48:40 +0200 Subject: [PATCH 145/274] gofmt shadow adaptive monetary controller --- internal/v2/economics/monetary.go | 66 +++++++++++++++---------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/internal/v2/economics/monetary.go b/internal/v2/economics/monetary.go index d596c094..0b486550 100644 --- a/internal/v2/economics/monetary.go +++ b/internal/v2/economics/monetary.go @@ -26,17 +26,17 @@ type MonetaryPolicy struct { } type MonetaryMetrics struct { - Supply uint64 - CirculatingSupply uint64 - StakedSupply uint64 - ProtocolReserve uint64 - BurnedThisEpoch uint64 - FinalizedOperations uint64 + Supply uint64 + CirculatingSupply uint64 + StakedSupply uint64 + ProtocolReserve uint64 + BurnedThisEpoch uint64 + FinalizedOperations uint64 ResourceUtilizationBps uint32 AgeWeightedVelocityBps uint32 - ComputeIndexQ9 uint64 - ComputePriceTrendBps int32 - ComputeIndexReliable bool + ComputeIndexQ9 uint64 + ComputePriceTrendBps int32 + ComputeIndexReliable bool } type MonetaryDecision struct { @@ -56,21 +56,21 @@ type MonetaryDecision struct { func DefaultShadowPolicy() MonetaryPolicy { return MonetaryPolicy{ - TargetInflationBps: 200, - MinInflationBps: 150, - MaxInflationBps: 250, - MaxEpochStepBps: 1, - EpochsPerYear: 365, - ReserveTargetBps: 1_000, - StakeTargetBps: 5_000, + TargetInflationBps: 200, + MinInflationBps: 150, + MaxInflationBps: 250, + MaxEpochStepBps: 1, + EpochsPerYear: 365, + ReserveTargetBps: 1_000, + StakeTargetBps: 5_000, UtilizationTargetBps: 5_000, - VelocityTargetBps: 5_000, - OperationsTarget: 1_000_000, - ReserveWeightBps: 500, - StakeWeightBps: 500, + VelocityTargetBps: 5_000, + OperationsTarget: 1_000_000, + ReserveWeightBps: 500, + StakeWeightBps: 500, UtilizationWeightBps: 250, - VelocityWeightBps: 250, - OperationsWeightBps: 100, + VelocityWeightBps: 250, + OperationsWeightBps: 100, } } @@ -105,18 +105,18 @@ func EvaluateShadow(priorTargetBps uint32, metrics MonetaryMetrics, policy Monet return MonetaryDecision{}, err } return MonetaryDecision{ - Shadow: true, - TargetInflationBps: target, - NetIssuanceTarget: netTarget, - BurnOffset: metrics.BurnedThisEpoch, - GrossMintTarget: gross, - ProjectedNetChange: netTarget, - ReserveRatioBps: reserveRatio, - StakeRatioBps: stakeRatio, + Shadow: true, + TargetInflationBps: target, + NetIssuanceTarget: netTarget, + BurnOffset: metrics.BurnedThisEpoch, + GrossMintTarget: gross, + ProjectedNetChange: netTarget, + ReserveRatioBps: reserveRatio, + StakeRatioBps: stakeRatio, OperationsActivityBps: operationsActivity, - ComputeIndexQ9: metrics.ComputeIndexQ9, - ComputePriceTrendBps: metrics.ComputePriceTrendBps, - ComputeIndexReliable: metrics.ComputeIndexReliable, + ComputeIndexQ9: metrics.ComputeIndexQ9, + ComputePriceTrendBps: metrics.ComputePriceTrendBps, + ComputeIndexReliable: metrics.ComputeIndexReliable, }, nil } From e26b0d5cb0e3cdb173888652766daff8aaef516a Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:49:18 +0200 Subject: [PATCH 146/274] gofmt shadow monetary tests --- internal/v2/economics/monetary_test.go | 38 +++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/internal/v2/economics/monetary_test.go b/internal/v2/economics/monetary_test.go index 7edc006b..ed539f48 100644 --- a/internal/v2/economics/monetary_test.go +++ b/internal/v2/economics/monetary_test.go @@ -5,17 +5,17 @@ import "testing" func TestShadowMonetaryControllerOffsetsBurnAndTargetsNetIssuance(t *testing.T) { policy := DefaultShadowPolicy() metrics := MonetaryMetrics{ - Supply: 1_000_000_000, - CirculatingSupply: 900_000_000, - StakedSupply: 450_000_000, - ProtocolReserve: 100_000_000, - BurnedThisEpoch: 12_345, - FinalizedOperations: policy.OperationsTarget, + Supply: 1_000_000_000, + CirculatingSupply: 900_000_000, + StakedSupply: 450_000_000, + ProtocolReserve: 100_000_000, + BurnedThisEpoch: 12_345, + FinalizedOperations: policy.OperationsTarget, ResourceUtilizationBps: policy.UtilizationTargetBps, AgeWeightedVelocityBps: policy.VelocityTargetBps, - ComputeIndexQ9: 7_500_000_000, - ComputePriceTrendBps: 250, - ComputeIndexReliable: true, + ComputeIndexQ9: 7_500_000_000, + ComputePriceTrendBps: 250, + ComputeIndexReliable: true, } decision, err := EvaluateShadow(policy.TargetInflationBps, metrics, policy) if err != nil { @@ -32,11 +32,11 @@ func TestShadowMonetaryControllerOffsetsBurnAndTargetsNetIssuance(t *testing.T) func TestShadowMonetaryControllerRateLimitsAdaptiveTarget(t *testing.T) { policy := DefaultShadowPolicy() metrics := MonetaryMetrics{ - Supply: 1_000_000_000, - CirculatingSupply: 900_000_000, - StakedSupply: 100_000_000, - ProtocolReserve: 1_000_000, - FinalizedOperations: 1, + Supply: 1_000_000_000, + CirculatingSupply: 900_000_000, + StakedSupply: 100_000_000, + ProtocolReserve: 1_000_000, + FinalizedOperations: 1, ResourceUtilizationBps: 100, AgeWeightedVelocityBps: 100, } @@ -52,11 +52,11 @@ func TestShadowMonetaryControllerRateLimitsAdaptiveTarget(t *testing.T) { func TestComputeIndexIsTelemetryOnlyInShadowV0(t *testing.T) { policy := DefaultShadowPolicy() base := MonetaryMetrics{ - Supply: 1_000_000_000, - CirculatingSupply: 900_000_000, - StakedSupply: 450_000_000, - ProtocolReserve: 100_000_000, - FinalizedOperations: policy.OperationsTarget, + Supply: 1_000_000_000, + CirculatingSupply: 900_000_000, + StakedSupply: 450_000_000, + ProtocolReserve: 100_000_000, + FinalizedOperations: policy.OperationsTarget, ResourceUtilizationBps: policy.UtilizationTargetBps, AgeWeightedVelocityBps: policy.VelocityTargetBps, } From c2e9f8dd23a0d568f3e7b3c4994ae6cbcce36cd3 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:49:44 +0200 Subject: [PATCH 147/274] gofmt monetary simulator --- cmd/zephyr-econ-sim/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/zephyr-econ-sim/main.go b/cmd/zephyr-econ-sim/main.go index 219832fc..1de64f07 100644 --- a/cmd/zephyr-econ-sim/main.go +++ b/cmd/zephyr-econ-sim/main.go @@ -11,7 +11,7 @@ import ( ) type simulationInput struct { - PriorTargetBps uint32 `json:"priorTargetBps"` + PriorTargetBps uint32 `json:"priorTargetBps"` Metrics economics.MonetaryMetrics `json:"metrics"` Policy *economics.MonetaryPolicy `json:"policy,omitempty"` } From 125caae5b747e5706a08f8aa4e0e021c4d594fe3 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:50:22 +0200 Subject: [PATCH 148/274] no-op From 20ca43cc9e89018937600ebfc96f1334a599fcc4 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:52:55 +0200 Subject: [PATCH 149/274] harden shadow activity arithmetic --- internal/v2/economics/monetary.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/v2/economics/monetary.go b/internal/v2/economics/monetary.go index 0b486550..5feaa666 100644 --- a/internal/v2/economics/monetary.go +++ b/internal/v2/economics/monetary.go @@ -176,7 +176,8 @@ func ratioBps(value, total uint64) uint32 { } ratio := new(big.Int).Mul(new(big.Int).SetUint64(value), new(big.Int).SetUint64(uint64(BasisPoints))) ratio.Quo(ratio, new(big.Int).SetUint64(total)) - if ratio.Uint64() > uint64(BasisPoints) { + limit := new(big.Int).SetUint64(uint64(BasisPoints)) + if ratio.Cmp(limit) > 0 { return BasisPoints } return uint32(ratio.Uint64()) @@ -188,9 +189,9 @@ func activityBps(value, target uint64) uint32 { } ratio := new(big.Int).Mul(new(big.Int).SetUint64(value), new(big.Int).SetUint64(uint64(BasisPoints))) ratio.Quo(ratio, new(big.Int).SetUint64(target)) - limit := uint64(2 * BasisPoints) - if ratio.Uint64() > limit { - return uint32(limit) + limit := new(big.Int).SetUint64(uint64(2 * BasisPoints)) + if ratio.Cmp(limit) > 0 { + return 2 * BasisPoints } return uint32(ratio.Uint64()) } From 630baf1fbd606668e8004f209f224665b1710a9c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:53:25 +0200 Subject: [PATCH 150/274] add deterministic v2 fee engine --- internal/v2/economics/fees.go | 164 ++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 internal/v2/economics/fees.go diff --git a/internal/v2/economics/fees.go b/internal/v2/economics/fees.go new file mode 100644 index 00000000..8852cd1b --- /dev/null +++ b/internal/v2/economics/fees.go @@ -0,0 +1,164 @@ +package economics + +import ( + "errors" + "math/big" +) + +var ( + ErrFeePolicy = errors.New("invalid Zephyr fee policy") + ErrResourceFee = errors.New("invalid Zephyr resource fee input") +) + +type FeePolicy struct { + BurnBps uint32 + ValidatorBps uint32 + ReserveBps uint32 +} + +type FeeAllocation struct { + Total uint64 + Burn uint64 + Validators uint64 + Reserve uint64 +} + +// CompatibilityFeePolicy preserves the current v2 executor behavior while fee +// accounting is moved into authenticated monetary state: the full signed Fee is +// counted as burn. Alternative splits can be simulated without activating them. +func CompatibilityFeePolicy() FeePolicy { + return FeePolicy{BurnBps: BasisPoints} +} + +func (p FeePolicy) Validate() error { + total := uint64(p.BurnBps) + uint64(p.ValidatorBps) + uint64(p.ReserveBps) + if p.BurnBps > BasisPoints || p.ValidatorBps > BasisPoints || p.ReserveBps > BasisPoints || total != uint64(BasisPoints) { + return ErrFeePolicy + } + return nil +} + +// SplitFee uses floor division for validator and reserve shares. Any indivisible +// remainder is assigned to burn, making conservation exact and deterministic. +func SplitFee(fee uint64, policy FeePolicy) (FeeAllocation, error) { + if err := policy.Validate(); err != nil { + return FeeAllocation{}, err + } + validators, err := shareBps(fee, policy.ValidatorBps) + if err != nil { + return FeeAllocation{}, err + } + reserve, err := shareBps(fee, policy.ReserveBps) + if err != nil || validators > fee || reserve > fee-validators { + return FeeAllocation{}, ErrFeePolicy + } + burn := fee - validators - reserve + return FeeAllocation{Total: fee, Burn: burn, Validators: validators, Reserve: reserve}, nil +} + +type ResourceUsage struct { + BaseTransactions uint64 + SignatureOps uint64 + WitnessBytes uint64 + StateReads uint64 + StateWrites uint64 + ContractFuel uint64 + DataAvailabilityBytes uint64 + CrossShardReceipts uint64 +} + +type ResourcePrices struct { + BaseTransaction uint64 + SignatureOp uint64 + WitnessByte uint64 + StateRead uint64 + StateWrite uint64 + ContractFuel uint64 + DataAvailabilityByte uint64 + CrossShardReceipt uint64 +} + +func QuoteResourceFee(usage ResourceUsage, prices ResourcePrices) (uint64, error) { + pairs := [][2]uint64{ + {usage.BaseTransactions, prices.BaseTransaction}, + {usage.SignatureOps, prices.SignatureOp}, + {usage.WitnessBytes, prices.WitnessByte}, + {usage.StateReads, prices.StateRead}, + {usage.StateWrites, prices.StateWrite}, + {usage.ContractFuel, prices.ContractFuel}, + {usage.DataAvailabilityBytes, prices.DataAvailabilityByte}, + {usage.CrossShardReceipts, prices.CrossShardReceipt}, + } + total := new(big.Int) + for _, pair := range pairs { + term := new(big.Int).Mul(new(big.Int).SetUint64(pair[0]), new(big.Int).SetUint64(pair[1])) + total.Add(total, term) + } + if !total.IsUint64() { + return 0, ErrResourceFee + } + return total.Uint64(), nil +} + +type BaseFeePolicy struct { + TargetUsage uint64 + AdjustmentDenominator uint64 + MinBaseFee uint64 + MaxBaseFee uint64 +} + +func (p BaseFeePolicy) Validate() error { + if p.TargetUsage == 0 || p.AdjustmentDenominator == 0 || p.MinBaseFee == 0 || p.MaxBaseFee < p.MinBaseFee { + return ErrFeePolicy + } + return nil +} + +// NextBaseFee is a bounded integer-only congestion controller. It is provided +// for simulation/reference use and is not activated as the live v2 fee market. +func NextBaseFee(current, used uint64, policy BaseFeePolicy) (uint64, error) { + if err := policy.Validate(); err != nil || current < policy.MinBaseFee || current > policy.MaxBaseFee { + return 0, ErrFeePolicy + } + if used == policy.TargetUsage { + return current, nil + } + var distance uint64 + increase := used > policy.TargetUsage + if increase { + distance = used - policy.TargetUsage + } else { + distance = policy.TargetUsage - used + } + change := new(big.Int).Mul(new(big.Int).SetUint64(current), new(big.Int).SetUint64(distance)) + change.Quo(change, new(big.Int).SetUint64(policy.TargetUsage)) + change.Quo(change, new(big.Int).SetUint64(policy.AdjustmentDenominator)) + if change.Sign() == 0 { + change.SetUint64(1) + } + if !change.IsUint64() { + return 0, ErrResourceFee + } + delta := change.Uint64() + if increase { + next := new(big.Int).Add(new(big.Int).SetUint64(current), new(big.Int).SetUint64(delta)) + max := new(big.Int).SetUint64(policy.MaxBaseFee) + if next.Cmp(max) > 0 { + return policy.MaxBaseFee, nil + } + return next.Uint64(), nil + } + if delta >= current || current-delta < policy.MinBaseFee { + return policy.MinBaseFee, nil + } + return current - delta, nil +} + +func shareBps(value uint64, bps uint32) (uint64, error) { + share := new(big.Int).Mul(new(big.Int).SetUint64(value), new(big.Int).SetUint64(uint64(bps))) + share.Quo(share, new(big.Int).SetUint64(uint64(BasisPoints))) + if !share.IsUint64() { + return 0, ErrFeePolicy + } + return share.Uint64(), nil +} From 555afa565d204fd9e53f7caa425e7ddefbd11bc8 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:53:42 +0200 Subject: [PATCH 151/274] test deterministic v2 fee engine --- internal/v2/economics/fees_test.go | 88 ++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 internal/v2/economics/fees_test.go diff --git a/internal/v2/economics/fees_test.go b/internal/v2/economics/fees_test.go new file mode 100644 index 00000000..6c513e79 --- /dev/null +++ b/internal/v2/economics/fees_test.go @@ -0,0 +1,88 @@ +package economics + +import ( + "math" + "testing" +) + +func TestSplitFeeConservesEveryAtomicUnit(t *testing.T) { + allocation, err := SplitFee(101, FeePolicy{BurnBps: 4_000, ValidatorBps: 5_000, ReserveBps: 1_000}) + if err != nil { + t.Fatal(err) + } + if allocation.Burn != 41 || allocation.Validators != 50 || allocation.Reserve != 10 { + t.Fatalf("unexpected deterministic split: %#v", allocation) + } + if allocation.Burn+allocation.Validators+allocation.Reserve != allocation.Total { + t.Fatal("fee split does not conserve value") + } +} + +func TestCompatibilityFeePolicyPreservesFullBurn(t *testing.T) { + allocation, err := SplitFee(12345, CompatibilityFeePolicy()) + if err != nil { + t.Fatal(err) + } + if allocation.Burn != 12345 || allocation.Validators != 0 || allocation.Reserve != 0 { + t.Fatalf("compatibility policy changed existing semantics: %#v", allocation) + } +} + +func TestQuoteResourceFee(t *testing.T) { + usage := ResourceUsage{ + BaseTransactions: 1, + SignatureOps: 2, + WitnessBytes: 100, + StateReads: 3, + StateWrites: 2, + ContractFuel: 50, + DataAvailabilityBytes: 20, + CrossShardReceipts: 1, + } + prices := ResourcePrices{ + BaseTransaction: 10, + SignatureOp: 2, + WitnessByte: 1, + StateRead: 3, + StateWrite: 5, + ContractFuel: 1, + DataAvailabilityByte: 2, + CrossShardReceipt: 7, + } + fee, err := QuoteResourceFee(usage, prices) + if err != nil { + t.Fatal(err) + } + if fee != 230 { + t.Fatalf("unexpected resource fee %d", fee) + } +} + +func TestQuoteResourceFeeRejectsOverflow(t *testing.T) { + _, err := QuoteResourceFee(ResourceUsage{BaseTransactions: math.MaxUint64}, ResourcePrices{BaseTransaction: 2}) + if err != ErrResourceFee { + t.Fatalf("expected overflow rejection, got %v", err) + } +} + +func TestNextBaseFeeMovesTowardCongestionAndBounds(t *testing.T) { + policy := BaseFeePolicy{TargetUsage: 100, AdjustmentDenominator: 8, MinBaseFee: 10, MaxBaseFee: 10_000} + high, err := NextBaseFee(1_000, 200, policy) + if err != nil { + t.Fatal(err) + } + if high != 1_125 { + t.Fatalf("unexpected congestion increase %d", high) + } + low, err := NextBaseFee(1_000, 0, policy) + if err != nil { + t.Fatal(err) + } + if low != 875 { + t.Fatalf("unexpected utilization decrease %d", low) + } + floor, err := NextBaseFee(10, 0, policy) + if err != nil || floor != 10 { + t.Fatalf("minimum fee must be preserved: %d %v", floor, err) + } +} From 776e04f9899409079a63c24e8f36ccb7e40e3f1f Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:54:25 +0200 Subject: [PATCH 152/274] gofmt deterministic v2 fee engine --- internal/v2/economics/fees.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/v2/economics/fees.go b/internal/v2/economics/fees.go index 8852cd1b..aff52201 100644 --- a/internal/v2/economics/fees.go +++ b/internal/v2/economics/fees.go @@ -57,25 +57,25 @@ func SplitFee(fee uint64, policy FeePolicy) (FeeAllocation, error) { } type ResourceUsage struct { - BaseTransactions uint64 - SignatureOps uint64 - WitnessBytes uint64 - StateReads uint64 - StateWrites uint64 - ContractFuel uint64 + BaseTransactions uint64 + SignatureOps uint64 + WitnessBytes uint64 + StateReads uint64 + StateWrites uint64 + ContractFuel uint64 DataAvailabilityBytes uint64 - CrossShardReceipts uint64 + CrossShardReceipts uint64 } type ResourcePrices struct { - BaseTransaction uint64 - SignatureOp uint64 - WitnessByte uint64 - StateRead uint64 - StateWrite uint64 - ContractFuel uint64 - DataAvailabilityByte uint64 - CrossShardReceipt uint64 + BaseTransaction uint64 + SignatureOp uint64 + WitnessByte uint64 + StateRead uint64 + StateWrite uint64 + ContractFuel uint64 + DataAvailabilityByte uint64 + CrossShardReceipt uint64 } func QuoteResourceFee(usage ResourceUsage, prices ResourcePrices) (uint64, error) { From fe6d0d4e91fb0cbfa015acf8f4063e3a6bc9277a Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:54:50 +0200 Subject: [PATCH 153/274] gofmt v2 fee engine tests --- internal/v2/economics/fees_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/v2/economics/fees_test.go b/internal/v2/economics/fees_test.go index 6c513e79..07c33438 100644 --- a/internal/v2/economics/fees_test.go +++ b/internal/v2/economics/fees_test.go @@ -30,14 +30,14 @@ func TestCompatibilityFeePolicyPreservesFullBurn(t *testing.T) { func TestQuoteResourceFee(t *testing.T) { usage := ResourceUsage{ - BaseTransactions: 1, - SignatureOps: 2, - WitnessBytes: 100, - StateReads: 3, - StateWrites: 2, - ContractFuel: 50, + BaseTransactions: 1, + SignatureOps: 2, + WitnessBytes: 100, + StateReads: 3, + StateWrites: 2, + ContractFuel: 50, DataAvailabilityBytes: 20, - CrossShardReceipts: 1, + CrossShardReceipts: 1, } prices := ResourcePrices{ BaseTransaction: 10, From 6b86a1295cf16ce27616882af3c63ccb4f3232ee Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:58:32 +0200 Subject: [PATCH 154/274] add native token supply policies and mutations --- internal/v2/assets/token.go | 225 +++++++++++++++++++++++++++++++++--- 1 file changed, 207 insertions(+), 18 deletions(-) diff --git a/internal/v2/assets/token.go b/internal/v2/assets/token.go index 314c5593..12b2db7d 100644 --- a/internal/v2/assets/token.go +++ b/internal/v2/assets/token.go @@ -2,6 +2,7 @@ package assets import ( "errors" + "math" "strings" "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" @@ -10,11 +11,23 @@ import ( var ErrInvalidTokenDefinition = errors.New("invalid token definition") +type SupplyPolicy uint8 + +const ( + // SupplyCapped is zero intentionally so existing v2 capped-token builders + // remain valid during the clean break when MaxSupply is already provided. + SupplyCapped SupplyPolicy = iota + SupplyFixed + SupplyMintable + SupplyPolicyCount +) + type Definition struct { TokenID types.TokenID Name string Symbol string Decimals uint8 + SupplyPolicy SupplyPolicy MaxSupply uint64 CurrentSupply uint64 MintAuthority types.AccountID @@ -26,6 +39,7 @@ type CreateToken struct { Name string Symbol string Decimals uint8 + SupplyPolicy SupplyPolicy MaxSupply uint64 InitialSupply uint64 MintAuthority types.AccountID @@ -33,19 +47,38 @@ type CreateToken struct { Transferable bool } +type MintToken struct { + DefinitionObject types.ObjectID + Recipient types.AccountID + Amount uint64 +} + +type BurnToken struct { + DefinitionObject types.ObjectID + Amount uint64 +} + func (c CreateToken) Validate() error { name := strings.TrimSpace(c.Name) symbol := strings.TrimSpace(c.Symbol) - if name == "" || len(name) > 64 || symbol == "" || len(symbol) > 16 || c.Decimals > 18 { - return ErrInvalidTokenDefinition - } - if c.InitialSupply == 0 { + if name == "" || len(name) > 64 || symbol == "" || len(symbol) > 16 || c.Decimals > 18 || c.InitialSupply == 0 || + types.IsZero32([32]byte(c.MintAuthority)) { return ErrInvalidTokenDefinition } - if c.MaxSupply != 0 && c.InitialSupply > c.MaxSupply { - return ErrInvalidTokenDefinition - } - if types.IsZero32([32]byte(c.MintAuthority)) { + switch c.SupplyPolicy { + case SupplyCapped: + if c.MaxSupply == 0 || c.InitialSupply > c.MaxSupply { + return ErrInvalidTokenDefinition + } + case SupplyFixed: + if c.MaxSupply == 0 || c.InitialSupply != c.MaxSupply { + return ErrInvalidTokenDefinition + } + case SupplyMintable: + if c.MaxSupply != 0 { + return ErrInvalidTokenDefinition + } + default: return ErrInvalidTokenDefinition } return nil @@ -59,6 +92,7 @@ func (c CreateToken) MarshalBinary() ([]byte, error) { w.String(strings.TrimSpace(c.Name)) w.String(strings.TrimSpace(c.Symbol)) w.U8(c.Decimals) + w.U8(uint8(c.SupplyPolicy)) w.U64(c.MaxSupply) w.U64(c.InitialSupply) w.Fixed(c.MintAuthority[:]) @@ -81,6 +115,10 @@ func ParseCreateToken(data []byte) (CreateToken, error) { if err != nil { return CreateToken{}, ErrInvalidTokenDefinition } + policy, err := r.U8() + if err != nil { + return CreateToken{}, ErrInvalidTokenDefinition + } maxSupply, err := r.U64() if err != nil { return CreateToken{}, ErrInvalidTokenDefinition @@ -98,17 +136,14 @@ func ParseCreateToken(data []byte) (CreateToken, error) { return CreateToken{}, ErrInvalidTokenDefinition } transferable, err := r.Bool() - if err != nil { - return CreateToken{}, ErrInvalidTokenDefinition - } - if err := r.Done(); err != nil { + if err != nil || r.Done() != nil { return CreateToken{}, ErrInvalidTokenDefinition } var authority types.AccountID copy(authority[:], authBytes) out := CreateToken{ - Name: name, Symbol: symbol, Decimals: decimals, MaxSupply: maxSupply, - InitialSupply: initialSupply, MintAuthority: authority, Burnable: burnable, Transferable: transferable, + Name: name, Symbol: symbol, Decimals: decimals, SupplyPolicy: SupplyPolicy(policy), + MaxSupply: maxSupply, InitialSupply: initialSupply, MintAuthority: authority, Burnable: burnable, Transferable: transferable, } if err := out.Validate(); err != nil { return CreateToken{}, err @@ -116,17 +151,36 @@ func ParseCreateToken(data []byte) (CreateToken, error) { return out, nil } -func (d Definition) MarshalBinary() ([]byte, error) { +func (d Definition) Validate() error { if types.IsZero32([32]byte(d.TokenID)) || strings.TrimSpace(d.Name) == "" || strings.TrimSpace(d.Symbol) == "" || - d.Decimals > 18 || d.CurrentSupply == 0 || (d.MaxSupply != 0 && d.CurrentSupply > d.MaxSupply) || - types.IsZero32([32]byte(d.MintAuthority)) { - return nil, ErrInvalidTokenDefinition + d.Decimals > 18 || types.IsZero32([32]byte(d.MintAuthority)) { + return ErrInvalidTokenDefinition + } + switch d.SupplyPolicy { + case SupplyCapped, SupplyFixed: + if d.MaxSupply == 0 || d.CurrentSupply > d.MaxSupply { + return ErrInvalidTokenDefinition + } + case SupplyMintable: + if d.MaxSupply != 0 { + return ErrInvalidTokenDefinition + } + default: + return ErrInvalidTokenDefinition + } + return nil +} + +func (d Definition) MarshalBinary() ([]byte, error) { + if err := d.Validate(); err != nil { + return nil, err } var w codec.Writer w.Fixed(d.TokenID[:]) w.String(strings.TrimSpace(d.Name)) w.String(strings.TrimSpace(d.Symbol)) w.U8(d.Decimals) + w.U8(uint8(d.SupplyPolicy)) w.U64(d.MaxSupply) w.U64(d.CurrentSupply) w.Fixed(d.MintAuthority[:]) @@ -134,3 +188,138 @@ func (d Definition) MarshalBinary() ([]byte, error) { w.Bool(d.Transferable) return w.BytesCopy(), nil } + +func ParseDefinition(data []byte) (Definition, error) { + r := codec.NewReader(data) + tokenRaw, err := r.Fixed(32) + if err != nil { + return Definition{}, ErrInvalidTokenDefinition + } + name, err := r.String(64) + if err != nil { + return Definition{}, ErrInvalidTokenDefinition + } + symbol, err := r.String(16) + if err != nil { + return Definition{}, ErrInvalidTokenDefinition + } + decimals, err := r.U8() + if err != nil { + return Definition{}, ErrInvalidTokenDefinition + } + policy, err := r.U8() + if err != nil { + return Definition{}, ErrInvalidTokenDefinition + } + maxSupply, err := r.U64() + if err != nil { + return Definition{}, ErrInvalidTokenDefinition + } + currentSupply, err := r.U64() + if err != nil { + return Definition{}, ErrInvalidTokenDefinition + } + authorityRaw, err := r.Fixed(32) + if err != nil { + return Definition{}, ErrInvalidTokenDefinition + } + burnable, err := r.Bool() + if err != nil { + return Definition{}, ErrInvalidTokenDefinition + } + transferable, err := r.Bool() + if err != nil || r.Done() != nil { + return Definition{}, ErrInvalidTokenDefinition + } + var token types.TokenID + var authority types.AccountID + copy(token[:], tokenRaw) + copy(authority[:], authorityRaw) + out := Definition{ + TokenID: token, Name: name, Symbol: symbol, Decimals: decimals, SupplyPolicy: SupplyPolicy(policy), + MaxSupply: maxSupply, CurrentSupply: currentSupply, MintAuthority: authority, Burnable: burnable, Transferable: transferable, + } + if err := out.Validate(); err != nil { + return Definition{}, err + } + return out, nil +} + +func (d Definition) Mint(amount uint64) (Definition, error) { + if err := d.Validate(); err != nil || amount == 0 || d.SupplyPolicy == SupplyFixed || math.MaxUint64-d.CurrentSupply < amount { + return Definition{}, ErrInvalidTokenDefinition + } + next := d + next.CurrentSupply += amount + if d.SupplyPolicy == SupplyCapped && next.CurrentSupply > d.MaxSupply { + return Definition{}, ErrInvalidTokenDefinition + } + return next, next.Validate() +} + +func (d Definition) Burn(amount uint64) (Definition, error) { + if err := d.Validate(); err != nil || !d.Burnable || amount == 0 || amount > d.CurrentSupply { + return Definition{}, ErrInvalidTokenDefinition + } + next := d + next.CurrentSupply -= amount + return next, next.Validate() +} + +func (m MintToken) MarshalBinary() ([]byte, error) { + if types.IsZero32([32]byte(m.DefinitionObject)) || types.IsZero32([32]byte(m.Recipient)) || m.Amount == 0 { + return nil, ErrInvalidTokenDefinition + } + var w codec.Writer + w.Fixed(m.DefinitionObject[:]) + w.Fixed(m.Recipient[:]) + w.U64(m.Amount) + return w.BytesCopy(), nil +} + +func ParseMintToken(data []byte) (MintToken, error) { + if len(data) != 72 { + return MintToken{}, ErrInvalidTokenDefinition + } + var out MintToken + copy(out.DefinitionObject[:], data[:32]) + copy(out.Recipient[:], data[32:64]) + r := codec.NewReader(data[64:]) + amount, err := r.U64() + if err != nil || r.Done() != nil { + return MintToken{}, ErrInvalidTokenDefinition + } + out.Amount = amount + if _, err := out.MarshalBinary(); err != nil { + return MintToken{}, err + } + return out, nil +} + +func (b BurnToken) MarshalBinary() ([]byte, error) { + if types.IsZero32([32]byte(b.DefinitionObject)) || b.Amount == 0 { + return nil, ErrInvalidTokenDefinition + } + var w codec.Writer + w.Fixed(b.DefinitionObject[:]) + w.U64(b.Amount) + return w.BytesCopy(), nil +} + +func ParseBurnToken(data []byte) (BurnToken, error) { + if len(data) != 40 { + return BurnToken{}, ErrInvalidTokenDefinition + } + var out BurnToken + copy(out.DefinitionObject[:], data[:32]) + r := codec.NewReader(data[32:]) + amount, err := r.U64() + if err != nil || r.Done() != nil { + return BurnToken{}, ErrInvalidTokenDefinition + } + out.Amount = amount + if _, err := out.MarshalBinary(); err != nil { + return BurnToken{}, err + } + return out, nil +} From 35b3fe4fae8c2e82abde51e90ac37fc122dc1f41 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:58:56 +0200 Subject: [PATCH 155/274] add native token mutation operation ids --- internal/v2/tx/token_ops.go | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 internal/v2/tx/token_ops.go diff --git a/internal/v2/tx/token_ops.go b/internal/v2/tx/token_ops.go new file mode 100644 index 00000000..1479616f --- /dev/null +++ b/internal/v2/tx/token_ops.go @@ -0,0 +1,6 @@ +package tx + +const ( + OpMintToken uint16 = 14 + OpBurnToken uint16 = 15 +) From f36b51b07199e192200797a3615ffac3ea74d96b Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:59:28 +0200 Subject: [PATCH 156/274] route native token mutation operations --- internal/v2/execution/extended_contract.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/v2/execution/extended_contract.go b/internal/v2/execution/extended_contract.go index 637cc015..e13c3f33 100644 --- a/internal/v2/execution/extended_contract.go +++ b/internal/v2/execution/extended_contract.go @@ -24,6 +24,10 @@ func (e Engine) executeExtended(t tx.Transaction, op tx.Operation) (Result, erro return e.executeDeployContract(t, op.Payload) case tx.OpContractCall: return e.executeContractCall(t, op.Payload) + case tx.OpMintToken: + return e.executeMintToken(t, op.Payload) + case tx.OpBurnToken: + return e.executeBurnToken(t, op.Payload) case tx.OpComputeOffer, tx.OpComputeJob, tx.OpComputeResult, tx.OpComputeAccept, tx.OpComputeIngestAssignment, tx.OpComputeIngestResult, tx.OpComputeFinalize, tx.OpComputeResolveReplicated, tx.OpComputeExpire: return e.executeCompute(t, op) default: From 3206037d1499b2548eab2e7668d7550c4a22f233 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:00:19 +0200 Subject: [PATCH 157/274] enforce native token transfer policies --- internal/v2/execution/engine.go | 51 ++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/internal/v2/execution/engine.go b/internal/v2/execution/engine.go index 703bd01b..d1605252 100644 --- a/internal/v2/execution/engine.go +++ b/internal/v2/execution/engine.go @@ -18,6 +18,7 @@ var ( ErrConservation = errors.New("token conservation failed") ErrOverflow = errors.New("token amount overflow") ErrShard = errors.New("transaction routed to wrong shard") + ErrTokenPolicy = errors.New("native token policy rejected operation") ) type OutboundOutput struct { @@ -77,17 +78,34 @@ func (e Engine) Execute(t tx.Transaction) (Result, error) { func (e Engine) executeTransfer(t tx.Transaction) (Result, error) { inputTotals := map[types.TokenID]uint64{} + definitions := map[types.TokenID]assets.Definition{} + consumed := make([]types.ObjectID, 0, len(t.Inputs)) for _, w := range t.Witnesses { - if w.Object.Owner != t.Sender || w.Object.Kind != object.KindCoin { + switch w.Object.Kind { + case object.KindCoin: + if w.Object.Owner != t.Sender { + return Result{}, ErrOwnership + } + coin, err := object.ParseCoin(w.Object.Data) + if err != nil { + return Result{}, err + } + if err := add(inputTotals, coin.Token, coin.Amount); err != nil { + return Result{}, err + } + consumed = append(consumed, w.Object.ID) + case object.KindTokenDefinition: + definition, err := assets.ParseDefinition(w.Object.Data) + if err != nil || definition.TokenID == e.NativeToken { + return Result{}, ErrTokenPolicy + } + if _, duplicate := definitions[definition.TokenID]; duplicate { + return Result{}, ErrTokenPolicy + } + definitions[definition.TokenID] = definition + default: return Result{}, ErrOwnership } - coin, err := object.ParseCoin(w.Object.Data) - if err != nil { - return Result{}, err - } - if err := add(inputTotals, coin.Token, coin.Amount); err != nil { - return Result{}, err - } } outputTotals := map[types.TokenID]uint64{} @@ -103,6 +121,12 @@ func (e Engine) executeTransfer(t tx.Transaction) (Result, error) { if err != nil { return Result{}, err } + if coin.Token != e.NativeToken { + definition, ok := definitions[coin.Token] + if !ok || !definition.Transferable { + return Result{}, ErrTokenPolicy + } + } if err := add(outputTotals, coin.Token, coin.Amount); err != nil { return Result{}, err } @@ -127,6 +151,11 @@ func (e Engine) executeTransfer(t tx.Transaction) (Result, error) { return Result{}, ErrOverflow } required += t.Fee + } else { + definition, ok := definitions[token] + if !ok || !definition.Transferable { + return Result{}, ErrTokenPolicy + } } if inAmount != required { return Result{}, ErrConservation @@ -137,10 +166,6 @@ func (e Engine) executeTransfer(t tx.Transaction) (Result, error) { return Result{}, ErrConservation } - consumed := make([]types.ObjectID, len(t.Inputs)) - for i, in := range t.Inputs { - consumed[i] = in.ObjectID - } return Result{Consumed: consumed, Created: created, Outbound: outbound, TxID: txID}, nil } @@ -202,7 +227,7 @@ func (e Engine) executeCreateToken(t tx.Transaction, payload []byte) (Result, er tokenID := types.TokenIDFromTransaction(txID, 0) definition := assets.Definition{ - TokenID: tokenID, Name: create.Name, Symbol: create.Symbol, Decimals: create.Decimals, + TokenID: tokenID, Name: create.Name, Symbol: create.Symbol, Decimals: create.Decimals, SupplyPolicy: create.SupplyPolicy, MaxSupply: create.MaxSupply, CurrentSupply: create.InitialSupply, MintAuthority: create.MintAuthority, Burnable: create.Burnable, Transferable: create.Transferable, } From d67a4768cfe65997b64d4a2990f6d827cedca664 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:01:08 +0200 Subject: [PATCH 158/274] execute native custom token mint and burn --- internal/v2/execution/token_mutation.go | 172 ++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 internal/v2/execution/token_mutation.go diff --git a/internal/v2/execution/token_mutation.go b/internal/v2/execution/token_mutation.go new file mode 100644 index 00000000..08357b63 --- /dev/null +++ b/internal/v2/execution/token_mutation.go @@ -0,0 +1,172 @@ +package execution + +import ( + "math" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/assets" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const tokenMintOutputIndex uint32 = 0x81000000 + +func (e Engine) executeMintToken(t tx.Transaction, payload []byte) (Result, error) { + request, err := assets.ParseMintToken(payload) + if err != nil { + return Result{}, err + } + definitionObject, definition, err := tokenDefinitionWitness(t, request.DefinitionObject) + if err != nil || definition.TokenID == e.NativeToken || definition.MintAuthority != t.Sender { + return Result{}, ErrTokenPolicy + } + next, err := definition.Mint(request.Amount) + if err != nil { + return Result{}, ErrTokenPolicy + } + + created, consumed, err := e.feeOnlyOutputsExcluding(t, request.DefinitionObject, nil) + if err != nil { + return Result{}, err + } + nextRaw, err := next.MarshalBinary() + if err != nil { + return Result{}, err + } + updatedDefinition := definitionObject + updatedDefinition.Version++ + updatedDefinition.Data = nextRaw + created = append(created, updatedDefinition) + consumed = append(consumed, definitionObject.ID) + + minted, err := object.NewCoinOutput(request.Recipient, definition.TokenID, request.Amount) + if err != nil { + return Result{}, err + } + router := sharding.Router{ShardCount: e.ShardCount} + destination, err := router.ShardForAccount(request.Recipient) + if err != nil || destination != t.ShardID { + return Result{}, ErrTokenPolicy + } + txID := t.ID() + created = append(created, object.Object{ + ID: types.ObjectIDForShard(txID, tokenMintOutputIndex, t.ShardID), Version: 1, + Owner: minted.Owner, Kind: minted.Kind, Data: minted.Data, + }) + return Result{Consumed: consumed, Created: created, TxID: txID}, nil +} + +func (e Engine) executeBurnToken(t tx.Transaction, payload []byte) (Result, error) { + request, err := assets.ParseBurnToken(payload) + if err != nil { + return Result{}, err + } + definitionObject, definition, err := tokenDefinitionWitness(t, request.DefinitionObject) + if err != nil || definition.TokenID == e.NativeToken || !definition.Burnable { + return Result{}, ErrTokenPolicy + } + next, err := definition.Burn(request.Amount) + if err != nil { + return Result{}, ErrTokenPolicy + } + + var nativeIn, tokenIn uint64 + consumed := make([]types.ObjectID, 0, len(t.Inputs)) + for _, witness := range t.Witnesses { + if witness.Object.ID == request.DefinitionObject { + continue + } + if witness.Object.Kind != object.KindCoin || witness.Object.Owner != t.Sender { + return Result{}, ErrOwnership + } + coin, err := object.ParseCoin(witness.Object.Data) + if err != nil { + return Result{}, err + } + switch coin.Token { + case e.NativeToken: + if math.MaxUint64-nativeIn < coin.Amount { + return Result{}, ErrOverflow + } + nativeIn += coin.Amount + case definition.TokenID: + if math.MaxUint64-tokenIn < coin.Amount { + return Result{}, ErrOverflow + } + tokenIn += coin.Amount + default: + return Result{}, ErrConservation + } + consumed = append(consumed, witness.Object.ID) + } + + var nativeOut, tokenOut uint64 + created := make([]object.Object, 0, len(t.Outputs)+1) + router := sharding.Router{ShardCount: e.ShardCount} + for i, spec := range t.Outputs { + if spec.Kind != object.KindCoin { + return Result{}, ErrConservation + } + destination, err := router.ShardForAccount(spec.Owner) + if err != nil || destination != t.ShardID { + return Result{}, ErrShard + } + coin, err := object.ParseCoin(spec.Data) + if err != nil { + return Result{}, err + } + switch coin.Token { + case e.NativeToken: + if math.MaxUint64-nativeOut < coin.Amount { + return Result{}, ErrOverflow + } + nativeOut += coin.Amount + case definition.TokenID: + if spec.Owner != t.Sender || math.MaxUint64-tokenOut < coin.Amount { + return Result{}, ErrTokenPolicy + } + tokenOut += coin.Amount + default: + return Result{}, ErrConservation + } + created = append(created, object.Object{ + ID: types.ObjectIDForShard(t.ID(), uint32(i), t.ShardID), Version: 1, + Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...), + }) + } + if math.MaxUint64-nativeOut < t.Fee || nativeIn != nativeOut+t.Fee { + return Result{}, ErrConservation + } + if math.MaxUint64-tokenOut < request.Amount || tokenIn != tokenOut+request.Amount { + return Result{}, ErrConservation + } + + nextRaw, err := next.MarshalBinary() + if err != nil { + return Result{}, err + } + updatedDefinition := definitionObject + updatedDefinition.Version++ + updatedDefinition.Data = nextRaw + created = append(created, updatedDefinition) + consumed = append(consumed, definitionObject.ID) + return Result{Consumed: consumed, Created: created, TxID: t.ID()}, nil +} + +func tokenDefinitionWitness(t tx.Transaction, id types.ObjectID) (object.Object, assets.Definition, error) { + for _, witness := range t.Witnesses { + if witness.Object.ID != id { + continue + } + if witness.Object.Kind != object.KindTokenDefinition { + return object.Object{}, assets.Definition{}, ErrTokenPolicy + } + definition, err := assets.ParseDefinition(witness.Object.Data) + if err != nil { + return object.Object{}, assets.Definition{}, err + } + return witness.Object, definition, nil + } + return object.Object{}, assets.Definition{}, ErrTokenPolicy +} From 1f3f7effabc51e275218d267108abe7c616c4478 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:02:02 +0200 Subject: [PATCH 159/274] test native token supply policies --- internal/v2/assets/token_test.go | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 internal/v2/assets/token_test.go diff --git a/internal/v2/assets/token_test.go b/internal/v2/assets/token_test.go new file mode 100644 index 00000000..99331a6a --- /dev/null +++ b/internal/v2/assets/token_test.go @@ -0,0 +1,88 @@ +package assets + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestTokenDefinitionSupplyPoliciesAndRoundTrip(t *testing.T) { + var token types.TokenID + var authority types.AccountID + token[0] = 1 + authority[0] = 2 + definition := Definition{ + TokenID: token, Name: "Capped", Symbol: "CAP", Decimals: 6, + SupplyPolicy: SupplyCapped, MaxSupply: 1_000, CurrentSupply: 400, + MintAuthority: authority, Burnable: true, Transferable: true, + } + raw, err := definition.MarshalBinary() + if err != nil { + t.Fatal(err) + } + parsed, err := ParseDefinition(raw) + if err != nil { + t.Fatal(err) + } + if parsed != definition { + t.Fatalf("definition round trip mismatch: %#v != %#v", parsed, definition) + } + minted, err := parsed.Mint(600) + if err != nil || minted.CurrentSupply != 1_000 { + t.Fatalf("unexpected capped mint result: %#v %v", minted, err) + } + if _, err := minted.Mint(1); err != ErrInvalidTokenDefinition { + t.Fatalf("expected cap rejection, got %v", err) + } + burned, err := minted.Burn(1_000) + if err != nil || burned.CurrentSupply != 0 { + t.Fatalf("full burn must leave a valid zero-supply definition: %#v %v", burned, err) + } +} + +func TestFixedAndUnlimitedMintPolicies(t *testing.T) { + var token types.TokenID + var authority types.AccountID + token[0] = 3 + authority[0] = 4 + fixed := Definition{ + TokenID: token, Name: "Fixed", Symbol: "FIX", SupplyPolicy: SupplyFixed, + MaxSupply: 100, CurrentSupply: 100, MintAuthority: authority, Transferable: true, + } + if _, err := fixed.Mint(1); err != ErrInvalidTokenDefinition { + t.Fatalf("fixed token unexpectedly minted: %v", err) + } + unlimited := fixed + unlimited.Name = "Mintable" + unlimited.Symbol = "MINT" + unlimited.SupplyPolicy = SupplyMintable + unlimited.MaxSupply = 0 + if next, err := unlimited.Mint(50); err != nil || next.CurrentSupply != 150 { + t.Fatalf("unlimited mint failed: %#v %v", next, err) + } +} + +func TestTokenMutationPayloadRoundTrip(t *testing.T) { + var definition types.ObjectID + var recipient types.AccountID + definition[0] = 1 + recipient[0] = 2 + mint := MintToken{DefinitionObject: definition, Recipient: recipient, Amount: 77} + raw, err := mint.MarshalBinary() + if err != nil { + t.Fatal(err) + } + parsedMint, err := ParseMintToken(raw) + if err != nil || parsedMint != mint { + t.Fatalf("mint payload mismatch: %#v %v", parsedMint, err) + } + burn := BurnToken{DefinitionObject: definition, Amount: 33} + raw, err = burn.MarshalBinary() + if err != nil { + t.Fatal(err) + } + parsedBurn, err := ParseBurnToken(raw) + if err != nil || parsedBurn != burn { + t.Fatalf("burn payload mismatch: %#v %v", parsedBurn, err) + } +} From c82e36d7d065667e6a211105f8e7660637b143ff Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:02:39 +0200 Subject: [PATCH 160/274] test custom token mint burn lifecycle --- internal/v2/execution/token_mutation_test.go | 137 +++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 internal/v2/execution/token_mutation_test.go diff --git a/internal/v2/execution/token_mutation_test.go b/internal/v2/execution/token_mutation_test.go new file mode 100644 index 00000000..baef868a --- /dev/null +++ b/internal/v2/execution/token_mutation_test.go @@ -0,0 +1,137 @@ +package execution + +import ( + "crypto/elliptic" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/assets" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestCustomTokenMintAndBurnLifecycle(t *testing.T) { + aliceKey := makeKey(t) + bobKey := makeKey(t) + alice := types.AccountIDFromPublicKey(elliptic.Marshal(elliptic.P256(), aliceKey.PublicKey.X, aliceKey.PublicKey.Y)) + bob := types.AccountIDFromPublicKey(elliptic.Marshal(elliptic.P256(), bobKey.PublicKey.X, bobKey.PublicKey.Y)) + network := types.NetworkID(types.HashBytes("network", []byte("token-mutation"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + custom := types.TokenID(types.HashBytes("token", []byte("CUSTOM"))) + + definition := assets.Definition{ + TokenID: custom, Name: "Custom", Symbol: "CUS", Decimals: 6, + SupplyPolicy: assets.SupplyCapped, MaxSupply: 1_000, CurrentSupply: 500, + MintAuthority: alice, Burnable: true, Transferable: true, + } + definitionRaw, err := definition.MarshalBinary() + if err != nil { + t.Fatal(err) + } + seed := types.HashBytes("token-mutation", []byte("objects")) + definitionID := types.ObjectIDForShard(seed, 1, 0) + aliceFeeID := types.ObjectIDForShard(seed, 2, 0) + bobFeeID := types.ObjectIDForShard(seed, 3, 0) + initialCoinID := types.ObjectIDForShard(seed, 4, 0) + aliceFee, _ := object.NewCoinOutput(alice, native, 10) + bobFee, _ := object.NewCoinOutput(bob, native, 10) + initialCoin, _ := object.NewCoinOutput(alice, custom, 500) + store := worldstate.NewMemory() + _, err = store.Apply(nil, []object.Object{ + {ID: definitionID, Version: 1, Owner: alice, Kind: object.KindTokenDefinition, Data: definitionRaw}, + {ID: aliceFeeID, Version: 1, Owner: alice, Kind: object.KindCoin, Data: aliceFee.Data}, + {ID: bobFeeID, Version: 1, Owner: bob, Kind: object.KindCoin, Data: bobFee.Data}, + {ID: initialCoinID, Version: 1, Owner: alice, Kind: object.KindCoin, Data: initialCoin.Data}, + }) + if err != nil { + t.Fatal(err) + } + + mintPayload, _ := (assets.MintToken{DefinitionObject: definitionID, Recipient: bob, Amount: 100}).MarshalBinary() + aliceChange, _ := object.NewCoinOutput(alice, native, 9) + mint := transactionWithProofs(t, store, aliceKey, network, []types.ObjectID{definitionID, aliceFeeID}, []object.OutputSpec{aliceChange}, tx.Operation{Kind: tx.OpMintToken, Payload: mintPayload}, 1) + engine := Engine{Network: network, NativeToken: native, ShardCount: 1} + mintResult, err := engine.Execute(mint) + if err != nil { + t.Fatal(err) + } + if _, err := store.Apply(mintResult.Consumed, mintResult.Created); err != nil { + t.Fatal(err) + } + updatedDefinition, ok := store.GetObject(definitionID) + if !ok { + t.Fatal("mint removed token definition") + } + mintedDefinition, err := assets.ParseDefinition(updatedDefinition.Data) + if err != nil || mintedDefinition.CurrentSupply != 600 || updatedDefinition.Version != 2 { + t.Fatalf("unexpected definition after mint: %#v %v", mintedDefinition, err) + } + var bobMinted object.Object + for _, created := range mintResult.Created { + if created.Kind != object.KindCoin || created.Owner != bob { + continue + } + coin, parseErr := object.ParseCoin(created.Data) + if parseErr == nil && coin.Token == custom && coin.Amount == 100 { + bobMinted = created + break + } + } + if types.IsZero32([32]byte(bobMinted.ID)) { + t.Fatal("minted recipient coin missing") + } + + burnPayload, _ := (assets.BurnToken{DefinitionObject: definitionID, Amount: 40}).MarshalBinary() + bobNativeChange, _ := object.NewCoinOutput(bob, native, 9) + bobTokenChange, _ := object.NewCoinOutput(bob, custom, 60) + burn := transactionWithProofs(t, store, bobKey, network, []types.ObjectID{definitionID, bobFeeID, bobMinted.ID}, []object.OutputSpec{bobNativeChange, bobTokenChange}, tx.Operation{Kind: tx.OpBurnToken, Payload: burnPayload}, 1) + burnResult, err := engine.Execute(burn) + if err != nil { + t.Fatal(err) + } + if _, err := store.Apply(burnResult.Consumed, burnResult.Created); err != nil { + t.Fatal(err) + } + updatedDefinition, ok = store.GetObject(definitionID) + if !ok { + t.Fatal("burn removed token definition") + } + burnedDefinition, err := assets.ParseDefinition(updatedDefinition.Data) + if err != nil || burnedDefinition.CurrentSupply != 560 || updatedDefinition.Version != 3 { + t.Fatalf("unexpected definition after burn: %#v %v", burnedDefinition, err) + } +} + +func TestFixedTokenCannotMint(t *testing.T) { + definition := assets.Definition{SupplyPolicy: assets.SupplyFixed, MaxSupply: 100, CurrentSupply: 100} + definition.TokenID[0] = 1 + definition.MintAuthority[0] = 2 + definition.Name = "Fixed" + definition.Symbol = "FIX" + if _, err := definition.Mint(1); err == nil { + t.Fatal("fixed token unexpectedly minted") + } +} + +func transactionWithProofs(t *testing.T, store worldstate.Backend, key interface{ Public() any }, network types.NetworkID, ids []types.ObjectID, outputs []object.OutputSpec, operation tx.Operation, fee uint64) tx.Transaction { + t.Helper() + privateKey, ok := key.(*ecdsa.PrivateKey) + if !ok { + t.Fatal("unexpected signing key") + } + transaction := tx.Transaction{Version: tx.Version, Network: network, ShardID: 0, StateRoot: store.Root(), Outputs: outputs, Operations: []tx.Operation{operation}, Fee: fee} + for _, id := range ids { + obj, proof, present := store.Proof(id) + if !present { + t.Fatalf("missing proof object %s", id.String()) + } + transaction.Inputs = append(transaction.Inputs, tx.InputRef{ObjectID: id, Version: obj.Version, ObjectHash: obj.Hash()}) + transaction.Witnesses = append(transaction.Witnesses, tx.Witness{Object: obj, Proof: proof}) + } + transaction.Salt[0] = byte(len(ids) + int(operation.Kind)) + if err := transaction.Sign(privateKey); err != nil { + t.Fatal(err) + } + return transaction +} From ae942fa0f552f21636a2e7d22a6873695f41ea04 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:03:10 +0200 Subject: [PATCH 161/274] fix custom token lifecycle test helper --- internal/v2/execution/token_mutation_test.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/internal/v2/execution/token_mutation_test.go b/internal/v2/execution/token_mutation_test.go index baef868a..d4fd30d4 100644 --- a/internal/v2/execution/token_mutation_test.go +++ b/internal/v2/execution/token_mutation_test.go @@ -1,6 +1,7 @@ package execution import ( + "crypto/ecdsa" "crypto/elliptic" "testing" @@ -114,12 +115,8 @@ func TestFixedTokenCannotMint(t *testing.T) { } } -func transactionWithProofs(t *testing.T, store worldstate.Backend, key interface{ Public() any }, network types.NetworkID, ids []types.ObjectID, outputs []object.OutputSpec, operation tx.Operation, fee uint64) tx.Transaction { +func transactionWithProofs(t *testing.T, store worldstate.Backend, key *ecdsa.PrivateKey, network types.NetworkID, ids []types.ObjectID, outputs []object.OutputSpec, operation tx.Operation, fee uint64) tx.Transaction { t.Helper() - privateKey, ok := key.(*ecdsa.PrivateKey) - if !ok { - t.Fatal("unexpected signing key") - } transaction := tx.Transaction{Version: tx.Version, Network: network, ShardID: 0, StateRoot: store.Root(), Outputs: outputs, Operations: []tx.Operation{operation}, Fee: fee} for _, id := range ids { obj, proof, present := store.Proof(id) @@ -130,7 +127,7 @@ func transactionWithProofs(t *testing.T, store worldstate.Backend, key interface transaction.Witnesses = append(transaction.Witnesses, tx.Witness{Object: obj, Proof: proof}) } transaction.Salt[0] = byte(len(ids) + int(operation.Kind)) - if err := transaction.Sign(privateKey); err != nil { + if err := transaction.Sign(key); err != nil { t.Fatal(err) } return transaction From 168531752a39f1e2b553633d9669b38fc5ddaceb Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:03:33 +0200 Subject: [PATCH 162/274] allow shared read-only token policy proofs --- internal/v2/execution/parallel.go | 48 ++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/internal/v2/execution/parallel.go b/internal/v2/execution/parallel.go index cdf0c926..80c99e3c 100644 --- a/internal/v2/execution/parallel.go +++ b/internal/v2/execution/parallel.go @@ -135,26 +135,42 @@ func transactionAccesses(transaction tx.Transaction) (map[types.ObjectID]accessM for _, input := range transaction.Inputs { accesses[input.ObjectID] = accessWrite } - if len(transaction.Operations) != 1 || transaction.Operations[0].Kind != tx.OpContractCall { + if len(transaction.Operations) != 1 { return accesses, nil } - call, err := contracts.ParseCall(transaction.Operations[0].Payload) - if err != nil { - return nil, err - } - if _, present := accesses[call.ContractObject]; !present { - return nil, ErrBatchConflict - } - accesses[call.ContractObject] = accessRead - for _, access := range call.Accesses { - if _, present := accesses[access.ObjectID]; !present { + switch transaction.Operations[0].Kind { + case tx.OpTransfer: + for _, witness := range transaction.Witnesses { + if witness.Object.Kind != object.KindTokenDefinition { + continue + } + if _, present := accesses[witness.Object.ID]; !present { + return nil, ErrBatchConflict + } + accesses[witness.Object.ID] = accessRead + } + return accesses, nil + case tx.OpContractCall: + call, err := contracts.ParseCall(transaction.Operations[0].Payload) + if err != nil { + return nil, err + } + if _, present := accesses[call.ContractObject]; !present { return nil, ErrBatchConflict } - if access.Write { - accesses[access.ObjectID] = accessWrite - } else { - accesses[access.ObjectID] = accessRead + accesses[call.ContractObject] = accessRead + for _, access := range call.Accesses { + if _, present := accesses[access.ObjectID]; !present { + return nil, ErrBatchConflict + } + if access.Write { + accesses[access.ObjectID] = accessWrite + } else { + accesses[access.ObjectID] = accessRead + } } + return accesses, nil + default: + return accesses, nil } - return accesses, nil } From 88eb24e80c91478293355dd7cc60714b5e641315 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:03:55 +0200 Subject: [PATCH 163/274] test read-only token policy parallelism --- .../execution/token_policy_parallel_test.go | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 internal/v2/execution/token_policy_parallel_test.go diff --git a/internal/v2/execution/token_policy_parallel_test.go b/internal/v2/execution/token_policy_parallel_test.go new file mode 100644 index 00000000..f03468a9 --- /dev/null +++ b/internal/v2/execution/token_policy_parallel_test.go @@ -0,0 +1,62 @@ +package execution + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestTokenDefinitionPolicyWitnessIsSharedReadForTransfers(t *testing.T) { + var root types.Hash + var definitionID, coinA, coinB types.ObjectID + root[0] = 1 + definitionID[0] = 2 + coinA[0] = 3 + coinB[0] = 4 + definition := object.Object{ID: definitionID, Version: 1, Kind: object.KindTokenDefinition} + first := tx.Transaction{ + StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: coinA}, {ObjectID: definitionID}}, + Operations: []tx.Operation{{Kind: tx.OpTransfer}}, + Witnesses: []tx.Witness{{Object: object.Object{ID: coinA, Kind: object.KindCoin}}, {Object: definition}}, + } + first.Salt[0] = 1 + second := tx.Transaction{ + StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: coinB}, {ObjectID: definitionID}}, + Operations: []tx.Operation{{Kind: tx.OpTransfer}}, + Witnesses: []tx.Witness{{Object: object.Object{ID: coinB, Kind: object.KindCoin}}, {Object: definition}}, + } + second.Salt[0] = 2 + if err := validateIndependentBatch([]tx.Transaction{first, second}); err != nil { + t.Fatalf("shared read-only token definition should not serialize transfers: %v", err) + } +} + +func TestTokenDefinitionWriteConflictsWithTransferRead(t *testing.T) { + var root types.Hash + var definitionID, coinA types.ObjectID + root[0] = 1 + definitionID[0] = 2 + coinA[0] = 3 + definition := object.Object{ID: definitionID, Version: 1, Kind: object.KindTokenDefinition} + transfer := tx.Transaction{ + StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: coinA}, {ObjectID: definitionID}}, + Operations: []tx.Operation{{Kind: tx.OpTransfer}}, + Witnesses: []tx.Witness{{Object: object.Object{ID: coinA, Kind: object.KindCoin}}, {Object: definition}}, + } + transfer.Salt[0] = 1 + mint := tx.Transaction{ + StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: definitionID}}, + Operations: []tx.Operation{{Kind: tx.OpMintToken}}, + Witnesses: []tx.Witness{{Object: definition}}, + } + mint.Salt[0] = 2 + if err := validateIndependentBatch([]tx.Transaction{transfer, mint}); err != ErrBatchConflict { + t.Fatalf("token definition write must conflict with transfer read, got %v", err) + } +} From 5c16056b26947d9029aa0196df41f0dc38bb4569 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:04:50 +0200 Subject: [PATCH 164/274] gofmt token policy parallel tests --- .../execution/token_policy_parallel_test.go | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/v2/execution/token_policy_parallel_test.go b/internal/v2/execution/token_policy_parallel_test.go index f03468a9..d132530d 100644 --- a/internal/v2/execution/token_policy_parallel_test.go +++ b/internal/v2/execution/token_policy_parallel_test.go @@ -17,17 +17,17 @@ func TestTokenDefinitionPolicyWitnessIsSharedReadForTransfers(t *testing.T) { coinB[0] = 4 definition := object.Object{ID: definitionID, Version: 1, Kind: object.KindTokenDefinition} first := tx.Transaction{ - StateRoot: root, - Inputs: []tx.InputRef{{ObjectID: coinA}, {ObjectID: definitionID}}, + StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: coinA}, {ObjectID: definitionID}}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, - Witnesses: []tx.Witness{{Object: object.Object{ID: coinA, Kind: object.KindCoin}}, {Object: definition}}, + Witnesses: []tx.Witness{{Object: object.Object{ID: coinA, Kind: object.KindCoin}}, {Object: definition}}, } first.Salt[0] = 1 second := tx.Transaction{ - StateRoot: root, - Inputs: []tx.InputRef{{ObjectID: coinB}, {ObjectID: definitionID}}, + StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: coinB}, {ObjectID: definitionID}}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, - Witnesses: []tx.Witness{{Object: object.Object{ID: coinB, Kind: object.KindCoin}}, {Object: definition}}, + Witnesses: []tx.Witness{{Object: object.Object{ID: coinB, Kind: object.KindCoin}}, {Object: definition}}, } second.Salt[0] = 2 if err := validateIndependentBatch([]tx.Transaction{first, second}); err != nil { @@ -43,17 +43,17 @@ func TestTokenDefinitionWriteConflictsWithTransferRead(t *testing.T) { coinA[0] = 3 definition := object.Object{ID: definitionID, Version: 1, Kind: object.KindTokenDefinition} transfer := tx.Transaction{ - StateRoot: root, - Inputs: []tx.InputRef{{ObjectID: coinA}, {ObjectID: definitionID}}, + StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: coinA}, {ObjectID: definitionID}}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, - Witnesses: []tx.Witness{{Object: object.Object{ID: coinA, Kind: object.KindCoin}}, {Object: definition}}, + Witnesses: []tx.Witness{{Object: object.Object{ID: coinA, Kind: object.KindCoin}}, {Object: definition}}, } transfer.Salt[0] = 1 mint := tx.Transaction{ - StateRoot: root, - Inputs: []tx.InputRef{{ObjectID: definitionID}}, + StateRoot: root, + Inputs: []tx.InputRef{{ObjectID: definitionID}}, Operations: []tx.Operation{{Kind: tx.OpMintToken}}, - Witnesses: []tx.Witness{{Object: definition}}, + Witnesses: []tx.Witness{{Object: definition}}, } mint.Salt[0] = 2 if err := validateIndependentBatch([]tx.Transaction{transfer, mint}); err != ErrBatchConflict { From b37dc10cfcbd9ddbb70a5f838d434e6522449f27 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:06:48 +0200 Subject: [PATCH 165/274] guard custom token cross shard transfers --- internal/v2/execution/engine.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/v2/execution/engine.go b/internal/v2/execution/engine.go index d1605252..9b1bd70d 100644 --- a/internal/v2/execution/engine.go +++ b/internal/v2/execution/engine.go @@ -134,6 +134,9 @@ func (e Engine) executeTransfer(t tx.Transaction) (Result, error) { if err != nil { return Result{}, ErrShard } + if coin.Token != e.NativeToken && destination != t.ShardID { + return Result{}, ErrTokenPolicy + } if destination == t.ShardID { created = append(created, object.Object{ ID: types.ObjectIDForShard(txID, uint32(i), destination), Version: 1, From 453c29604908a76b8b28274da5defb02a112ef82 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:07:21 +0200 Subject: [PATCH 166/274] test custom token transfer policy guards --- .../execution/token_transfer_policy_test.go | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 internal/v2/execution/token_transfer_policy_test.go diff --git a/internal/v2/execution/token_transfer_policy_test.go b/internal/v2/execution/token_transfer_policy_test.go new file mode 100644 index 00000000..077d27e2 --- /dev/null +++ b/internal/v2/execution/token_transfer_policy_test.go @@ -0,0 +1,128 @@ +package execution + +import ( + "crypto/elliptic" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/assets" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestCustomTokenTransferRequiresTransferableDefinition(t *testing.T) { + key := makeKey(t) + sender := types.AccountIDFromPublicKey(elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y)) + recipient := sender + recipient[31] ^= 1 + network := types.NetworkID(types.HashBytes("network", []byte("token-policy"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + custom := types.TokenID(types.HashBytes("token", []byte("POLICY"))) + + makeTransaction := func(transferable bool) (Engine, tx.Transaction) { + definition := assets.Definition{ + TokenID: custom, Name: "Policy", Symbol: "PLC", SupplyPolicy: assets.SupplyFixed, + MaxSupply: 100, CurrentSupply: 100, MintAuthority: sender, Burnable: true, Transferable: transferable, + } + definitionRaw, err := definition.MarshalBinary() + if err != nil { + t.Fatal(err) + } + seed := types.HashBytes("token-policy", []byte{byte(boolByte(transferable))}) + definitionID := types.ObjectIDForShard(seed, 1, 0) + customID := types.ObjectIDForShard(seed, 2, 0) + feeID := types.ObjectIDForShard(seed, 3, 0) + customOut, _ := object.NewCoinOutput(sender, custom, 100) + feeOut, _ := object.NewCoinOutput(sender, native, 10) + store := worldstate.NewMemory() + _, err = store.Apply(nil, []object.Object{ + {ID: definitionID, Version: 1, Owner: sender, Kind: object.KindTokenDefinition, Data: definitionRaw}, + {ID: customID, Version: 1, Owner: sender, Kind: object.KindCoin, Data: customOut.Data}, + {ID: feeID, Version: 1, Owner: sender, Kind: object.KindCoin, Data: feeOut.Data}, + }) + if err != nil { + t.Fatal(err) + } + toRecipient, _ := object.NewCoinOutput(recipient, custom, 100) + feeChange, _ := object.NewCoinOutput(sender, native, 9) + transaction := transactionWithProofs(t, store, key, network, []types.ObjectID{customID, definitionID, feeID}, []object.OutputSpec{toRecipient, feeChange}, tx.Operation{Kind: tx.OpTransfer}, 1) + return Engine{Network: network, NativeToken: native, ShardCount: 1}, transaction + } + + engine, blocked := makeTransaction(false) + if _, err := engine.Execute(blocked); err != ErrTokenPolicy { + t.Fatalf("non-transferable token should be rejected, got %v", err) + } + engine, allowed := makeTransaction(true) + result, err := engine.Execute(allowed) + if err != nil { + t.Fatal(err) + } + if len(result.Consumed) != 2 { + t.Fatalf("definition witness must remain read-only; consumed=%d", len(result.Consumed)) + } +} + +func TestCustomTokenCrossShardTransferRemainsActivationGated(t *testing.T) { + var keyAccount types.AccountID + var key = makeKey(t) + for attempts := 0; attempts < 64; attempts++ { + keyAccount = types.AccountIDFromPublicKey(elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y)) + if types.AccountShard(keyAccount, 2) == 0 { + break + } + key = makeKey(t) + } + if types.AccountShard(keyAccount, 2) != 0 { + t.Fatal("failed to generate shard-0 sender") + } + var recipient types.AccountID + for i := 1; i < 256; i++ { + recipient[31] = byte(i) + if types.AccountShard(recipient, 2) == 1 { + break + } + } + if types.AccountShard(recipient, 2) != 1 { + t.Fatal("failed to derive shard-1 recipient") + } + + network := types.NetworkID(types.HashBytes("network", []byte("token-cross-shard"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + custom := types.TokenID(types.HashBytes("token", []byte("CUSTOM-X"))) + definition := assets.Definition{ + TokenID: custom, Name: "Cross", Symbol: "CRS", SupplyPolicy: assets.SupplyFixed, + MaxSupply: 100, CurrentSupply: 100, MintAuthority: keyAccount, Transferable: true, + } + definitionRaw, _ := definition.MarshalBinary() + seed := types.HashBytes("token-cross-shard", []byte("objects")) + definitionID := types.ObjectIDForShard(seed, 1, 0) + customID := types.ObjectIDForShard(seed, 2, 0) + feeID := types.ObjectIDForShard(seed, 3, 0) + customOut, _ := object.NewCoinOutput(keyAccount, custom, 100) + feeOut, _ := object.NewCoinOutput(keyAccount, native, 10) + store := worldstate.NewMemory() + _, err := store.Apply(nil, []object.Object{ + {ID: definitionID, Version: 1, Owner: keyAccount, Kind: object.KindTokenDefinition, Data: definitionRaw}, + {ID: customID, Version: 1, Owner: keyAccount, Kind: object.KindCoin, Data: customOut.Data}, + {ID: feeID, Version: 1, Owner: keyAccount, Kind: object.KindCoin, Data: feeOut.Data}, + }) + if err != nil { + t.Fatal(err) + } + remote, _ := object.NewCoinOutput(recipient, custom, 100) + feeChange, _ := object.NewCoinOutput(keyAccount, native, 9) + transaction := transactionWithProofs(t, store, key, network, []types.ObjectID{customID, definitionID, feeID}, []object.OutputSpec{remote, feeChange}, tx.Operation{Kind: tx.OpTransfer}, 1) + _, err = (Engine{Network: network, NativeToken: native, ShardCount: 2}).Execute(transaction) + if err != ErrTokenPolicy { + t.Fatalf("custom token cross-shard transfer must remain gated, got %v", err) + } +} + +func boolByte(value bool) byte { + if value { + return 1 + } + return 0 +} From 976b74b625cd5aa626fe237b9646ef9c4644561d Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:17:08 +0200 Subject: [PATCH 167/274] Fix cross-shard token policy fixture --- internal/v2/execution/token_transfer_policy_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/v2/execution/token_transfer_policy_test.go b/internal/v2/execution/token_transfer_policy_test.go index 077d27e2..aef27043 100644 --- a/internal/v2/execution/token_transfer_policy_test.go +++ b/internal/v2/execution/token_transfer_policy_test.go @@ -66,7 +66,7 @@ func TestCustomTokenTransferRequiresTransferableDefinition(t *testing.T) { func TestCustomTokenCrossShardTransferRemainsActivationGated(t *testing.T) { var keyAccount types.AccountID - var key = makeKey(t) + key := makeKey(t) for attempts := 0; attempts < 64; attempts++ { keyAccount = types.AccountIDFromPublicKey(elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y)) if types.AccountShard(keyAccount, 2) == 0 { @@ -77,15 +77,17 @@ func TestCustomTokenCrossShardTransferRemainsActivationGated(t *testing.T) { if types.AccountShard(keyAccount, 2) != 0 { t.Fatal("failed to generate shard-0 sender") } + var recipient types.AccountID - for i := 1; i < 256; i++ { - recipient[31] = byte(i) + for attempts := 0; attempts < 64; attempts++ { + recipientKey := makeKey(t) + recipient = types.AccountIDFromPublicKey(elliptic.Marshal(elliptic.P256(), recipientKey.PublicKey.X, recipientKey.PublicKey.Y)) if types.AccountShard(recipient, 2) == 1 { break } } if types.AccountShard(recipient, 2) != 1 { - t.Fatal("failed to derive shard-1 recipient") + t.Fatal("failed to generate shard-1 recipient") } network := types.NetworkID(types.HashBytes("network", []byte("token-cross-shard"))) From dc63ef2402791b69b98a697784e91cef58b31528 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:17:40 +0200 Subject: [PATCH 168/274] Add shadow compute scarcity index --- internal/v2/economics/compute_scarcity.go | 159 ++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 internal/v2/economics/compute_scarcity.go diff --git a/internal/v2/economics/compute_scarcity.go b/internal/v2/economics/compute_scarcity.go new file mode 100644 index 00000000..978fd7fc --- /dev/null +++ b/internal/v2/economics/compute_scarcity.go @@ -0,0 +1,159 @@ +package economics + +import ( + "errors" + "math/big" +) + +var ErrComputeScarcity = errors.New("invalid Zephyr compute scarcity input") + +type ComputeScarcityConfig struct { + DemandSupplyWeightBps uint32 + PriceTrendWeightBps uint32 + BacklogWeightBps uint32 + UtilizationWeightBps uint32 + FulfillmentWeightBps uint32 + UtilizationTargetBps uint32 + MinDemandUnits uint64 + MinSupplyUnits uint64 + MaxAbsScoreBps uint32 +} + +type ComputeMarketMetrics struct { + EscrowBackedDemandUnits uint64 + VerifiedSupplyUnits uint64 + BacklogUnits uint64 + FulfilledUnits uint64 + UtilizationBps uint32 + ComputePriceTrendBps int32 + ComputeIndexReliable bool +} + +type ComputeScarcitySnapshot struct { + Epoch uint64 + DemandSupplyPressureBps int32 + PriceTrendPressureBps int32 + BacklogPressureBps int32 + UtilizationPressureBps int32 + FulfillmentPressureBps int32 + ScoreBps int32 + Reliable bool +} + +func DefaultComputeScarcityConfig() ComputeScarcityConfig { + return ComputeScarcityConfig{ + DemandSupplyWeightBps: 3_000, + PriceTrendWeightBps: 2_000, + BacklogWeightBps: 2_000, + UtilizationWeightBps: 1_500, + FulfillmentWeightBps: 1_500, + UtilizationTargetBps: 7_000, + MinDemandUnits: 1_000, + MinSupplyUnits: 1_000, + MaxAbsScoreBps: 10_000, + } +} + +// BuildComputeScarcity calculates the Zephyr Compute Scarcity Index (ZCSI). +// Demand must represent standardized, escrow-backed work. Supply must represent +// standardized, benchmarked and collateralized capacity. Advertised prices or +// self-reported peak FLOPS are not valid inputs. +func BuildComputeScarcity(epoch uint64, metrics ComputeMarketMetrics, cfg ComputeScarcityConfig) (ComputeScarcitySnapshot, error) { + if epoch == 0 || cfg.UtilizationTargetBps > BasisPoints || cfg.MaxAbsScoreBps == 0 || cfg.MaxAbsScoreBps > BasisPoints || + metrics.UtilizationBps > BasisPoints || metrics.BacklogUnits > metrics.EscrowBackedDemandUnits || + metrics.FulfilledUnits > metrics.EscrowBackedDemandUnits { + return ComputeScarcitySnapshot{}, ErrComputeScarcity + } + weights := []uint32{ + cfg.DemandSupplyWeightBps, + cfg.PriceTrendWeightBps, + cfg.BacklogWeightBps, + cfg.UtilizationWeightBps, + cfg.FulfillmentWeightBps, + } + var totalWeight uint64 + for _, weight := range weights { + if weight > BasisPoints { + return ComputeScarcitySnapshot{}, ErrComputeScarcity + } + totalWeight += uint64(weight) + } + if totalWeight == 0 { + return ComputeScarcitySnapshot{}, ErrComputeScarcity + } + + out := ComputeScarcitySnapshot{Epoch: epoch} + out.DemandSupplyPressureBps = signedRatioPressure(metrics.EscrowBackedDemandUnits, metrics.VerifiedSupplyUnits) + out.BacklogPressureBps = unsignedRatioPressure(metrics.BacklogUnits, metrics.EscrowBackedDemandUnits) + out.UtilizationPressureBps = clampSignedBps(int64(metrics.UtilizationBps) - int64(cfg.UtilizationTargetBps)) + fulfilledBps := unsignedRatioPressure(metrics.FulfilledUnits, metrics.EscrowBackedDemandUnits) + out.FulfillmentPressureBps = int32(BasisPoints) - fulfilledBps + if metrics.EscrowBackedDemandUnits == 0 { + out.FulfillmentPressureBps = 0 + } + if metrics.ComputeIndexReliable { + out.PriceTrendPressureBps = clampSignedBps(int64(metrics.ComputePriceTrendBps)) + } + + weighted := int64(out.DemandSupplyPressureBps)*int64(cfg.DemandSupplyWeightBps) + + int64(out.BacklogPressureBps)*int64(cfg.BacklogWeightBps) + + int64(out.UtilizationPressureBps)*int64(cfg.UtilizationWeightBps) + + int64(out.FulfillmentPressureBps)*int64(cfg.FulfillmentWeightBps) + effectiveWeight := totalWeight + if metrics.ComputeIndexReliable { + weighted += int64(out.PriceTrendPressureBps) * int64(cfg.PriceTrendWeightBps) + } else { + effectiveWeight -= uint64(cfg.PriceTrendWeightBps) + } + if effectiveWeight == 0 { + return ComputeScarcitySnapshot{}, ErrComputeScarcity + } + out.ScoreBps = clampSigned(int64(weighted)/int64(effectiveWeight), int32(cfg.MaxAbsScoreBps)) + out.Reliable = metrics.EscrowBackedDemandUnits >= cfg.MinDemandUnits && metrics.VerifiedSupplyUnits >= cfg.MinSupplyUnits + return out, nil +} + +func signedRatioPressure(demand, supply uint64) int32 { + if demand == 0 && supply == 0 { + return 0 + } + if supply == 0 { + return int32(BasisPoints) + } + delta := new(big.Int).Sub(new(big.Int).SetUint64(demand), new(big.Int).SetUint64(supply)) + delta.Mul(delta, new(big.Int).SetUint64(uint64(BasisPoints))) + delta.Quo(delta, new(big.Int).SetUint64(supply)) + if delta.Cmp(big.NewInt(int64(BasisPoints))) > 0 { + return int32(BasisPoints) + } + if delta.Cmp(big.NewInt(-int64(BasisPoints))) < 0 { + return -int32(BasisPoints) + } + return int32(delta.Int64()) +} + +func unsignedRatioPressure(value, total uint64) int32 { + if total == 0 { + return 0 + } + ratio := new(big.Int).Mul(new(big.Int).SetUint64(value), new(big.Int).SetUint64(uint64(BasisPoints))) + ratio.Quo(ratio, new(big.Int).SetUint64(total)) + if ratio.Cmp(new(big.Int).SetUint64(uint64(BasisPoints))) > 0 { + return int32(BasisPoints) + } + return int32(ratio.Int64()) +} + +func clampSignedBps(value int64) int32 { + return clampSigned(value, int32(BasisPoints)) +} + +func clampSigned(value int64, maximum int32) int32 { + if value > int64(maximum) { + return maximum + } + if value < -int64(maximum) { + return -maximum + } + return int32(value) +} From 40f9b2b7b386ed811ce599fd4950ac8f965d59be Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:18:08 +0200 Subject: [PATCH 169/274] Add compute feedback simulation modes --- internal/v2/economics/compute_feedback.go | 119 ++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 internal/v2/economics/compute_feedback.go diff --git a/internal/v2/economics/compute_feedback.go b/internal/v2/economics/compute_feedback.go new file mode 100644 index 00000000..e24da4d6 --- /dev/null +++ b/internal/v2/economics/compute_feedback.go @@ -0,0 +1,119 @@ +package economics + +import "errors" + +var ErrComputeFeedback = errors.New("invalid Zephyr compute feedback policy") + +type ComputeFeedbackMode uint8 + +const ( + ComputeFeedbackObserveOnly ComputeFeedbackMode = iota + ComputeFeedbackRewardRouting + ComputeFeedbackMonetaryBand +) + +type ComputeFeedbackPolicy struct { + Mode ComputeFeedbackMode + BaseComputeRewardShareBps uint32 + MinComputeRewardShareBps uint32 + MaxComputeRewardShareBps uint32 + RewardSensitivityBps uint32 + MonetarySensitivityBps uint32 + MaxInflationCorrectionBps uint32 +} + +type ComputeFeedbackDecision struct { + Shadow bool + Mode ComputeFeedbackMode + ScarcityScoreBps int32 + ScarcityReliable bool + ComputeRewardShareBps uint32 + SuggestedComputeIncentiveMint uint64 + InflationCorrectionBps int32 + BaseTargetInflationBps uint32 + SuggestedTargetInflationBps uint32 + SuggestedNetIssuance uint64 + SuggestedGrossMint uint64 +} + +func DefaultComputeFeedbackPolicy(mode ComputeFeedbackMode) ComputeFeedbackPolicy { + return ComputeFeedbackPolicy{ + Mode: mode, + BaseComputeRewardShareBps: 1_000, + MinComputeRewardShareBps: 0, + MaxComputeRewardShareBps: 3_000, + RewardSensitivityBps: 2_000, + MonetarySensitivityBps: 25, + MaxInflationCorrectionBps: 25, + } +} + +// EvaluateComputeFeedback simulates how ZCSI could affect incentive routing and, +// only in mode C, a narrow monetary band. It never mutates supply and all output +// remains shadow until governance/protocol activation gates are satisfied. +func EvaluateComputeFeedback(base MonetaryDecision, metrics MonetaryMetrics, monetary MonetaryPolicy, scarcity ComputeScarcitySnapshot, policy ComputeFeedbackPolicy) (ComputeFeedbackDecision, error) { + if !base.Shadow || policy.Mode > ComputeFeedbackMonetaryBand || policy.BaseComputeRewardShareBps > BasisPoints || + policy.MinComputeRewardShareBps > policy.BaseComputeRewardShareBps || policy.BaseComputeRewardShareBps > policy.MaxComputeRewardShareBps || + policy.MaxComputeRewardShareBps > BasisPoints || policy.RewardSensitivityBps > BasisPoints || + policy.MonetarySensitivityBps > BasisPoints || policy.MaxInflationCorrectionBps > BasisPoints { + return ComputeFeedbackDecision{}, ErrComputeFeedback + } + + decision := ComputeFeedbackDecision{ + Shadow: true, + Mode: policy.Mode, + ScarcityScoreBps: scarcity.ScoreBps, + ScarcityReliable: scarcity.Reliable, + ComputeRewardShareBps: policy.BaseComputeRewardShareBps, + BaseTargetInflationBps: base.TargetInflationBps, + SuggestedTargetInflationBps: base.TargetInflationBps, + SuggestedNetIssuance: base.NetIssuanceTarget, + SuggestedGrossMint: base.GrossMintTarget, + } + if !scarcity.Reliable || policy.Mode == ComputeFeedbackObserveOnly { + return decisionWithComputeBudget(decision), nil + } + + rewardDelta := int64(scarcity.ScoreBps) * int64(policy.RewardSensitivityBps) / int64(BasisPoints) + rewardShare := int64(policy.BaseComputeRewardShareBps) + rewardDelta + if rewardShare < int64(policy.MinComputeRewardShareBps) { + rewardShare = int64(policy.MinComputeRewardShareBps) + } + if rewardShare > int64(policy.MaxComputeRewardShareBps) { + rewardShare = int64(policy.MaxComputeRewardShareBps) + } + decision.ComputeRewardShareBps = uint32(rewardShare) + + if policy.Mode != ComputeFeedbackMonetaryBand { + return decisionWithComputeBudget(decision), nil + } + correction := int64(scarcity.ScoreBps) * int64(policy.MonetarySensitivityBps) / int64(BasisPoints) + maxCorrection := int64(policy.MaxInflationCorrectionBps) + if correction > maxCorrection { + correction = maxCorrection + } + if correction < -maxCorrection { + correction = -maxCorrection + } + decision.InflationCorrectionBps = int32(correction) + decision.SuggestedTargetInflationBps = clampTarget(int64(base.TargetInflationBps)+correction, monetary.MinInflationBps, monetary.MaxInflationBps) + net, err := epochIssuance(metrics.Supply, decision.SuggestedTargetInflationBps, monetary.EpochsPerYear) + if err != nil { + return ComputeFeedbackDecision{}, err + } + gross, err := addUint64(net, metrics.BurnedThisEpoch) + if err != nil { + return ComputeFeedbackDecision{}, err + } + decision.SuggestedNetIssuance = net + decision.SuggestedGrossMint = gross + return decisionWithComputeBudget(decision), nil +} + +func decisionWithComputeBudget(decision ComputeFeedbackDecision) ComputeFeedbackDecision { + budget, err := shareBps(decision.SuggestedNetIssuance, decision.ComputeRewardShareBps) + if err == nil { + decision.SuggestedComputeIncentiveMint = budget + } + return decision +} From 3c2df11113083dc8984f76290bad8218b32f2638 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:18:45 +0200 Subject: [PATCH 170/274] Test compute scarcity and feedback modes --- .../v2/economics/compute_scarcity_test.go | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 internal/v2/economics/compute_scarcity_test.go diff --git a/internal/v2/economics/compute_scarcity_test.go b/internal/v2/economics/compute_scarcity_test.go new file mode 100644 index 00000000..bb383624 --- /dev/null +++ b/internal/v2/economics/compute_scarcity_test.go @@ -0,0 +1,123 @@ +package economics + +import "testing" + +func TestComputeScarcityRisesWhenVerifiedDemandExceedsSupply(t *testing.T) { + metrics := ComputeMarketMetrics{ + EscrowBackedDemandUnits: 2_000, + VerifiedSupplyUnits: 1_000, + BacklogUnits: 500, + FulfilledUnits: 1_500, + UtilizationBps: 9_000, + ComputePriceTrendBps: 1_000, + ComputeIndexReliable: true, + } + snapshot, err := BuildComputeScarcity(1, metrics, DefaultComputeScarcityConfig()) + if err != nil { + t.Fatal(err) + } + if !snapshot.Reliable || snapshot.ScoreBps <= 0 { + t.Fatalf("expected reliable positive scarcity, got %#v", snapshot) + } + if snapshot.DemandSupplyPressureBps != int32(BasisPoints) { + t.Fatalf("expected demand/supply pressure clamp, got %d", snapshot.DemandSupplyPressureBps) + } +} + +func TestComputeScarcityIgnoresUnreliablePriceSignal(t *testing.T) { + cfg := DefaultComputeScarcityConfig() + metrics := ComputeMarketMetrics{ + EscrowBackedDemandUnits: 2_000, + VerifiedSupplyUnits: 2_000, + FulfilledUnits: 2_000, + UtilizationBps: cfg.UtilizationTargetBps, + ComputePriceTrendBps: 10_000, + ComputeIndexReliable: false, + } + first, err := BuildComputeScarcity(1, metrics, cfg) + if err != nil { + t.Fatal(err) + } + metrics.ComputePriceTrendBps = -10_000 + second, err := BuildComputeScarcity(2, metrics, cfg) + if err != nil { + t.Fatal(err) + } + if first.ScoreBps != second.ScoreBps || first.PriceTrendPressureBps != 0 || second.PriceTrendPressureBps != 0 { + t.Fatalf("unreliable ZCPI changed scarcity: %#v %#v", first, second) + } +} + +func TestComputeScarcityRejectsImpossibleSettlementMetrics(t *testing.T) { + metrics := ComputeMarketMetrics{EscrowBackedDemandUnits: 100, VerifiedSupplyUnits: 100, BacklogUnits: 101} + if _, err := BuildComputeScarcity(1, metrics, DefaultComputeScarcityConfig()); err != ErrComputeScarcity { + t.Fatalf("expected invalid backlog rejection, got %v", err) + } +} + +func TestComputeFeedbackModesKeepActivationShadowed(t *testing.T) { + monetary := DefaultShadowPolicy() + metrics := MonetaryMetrics{ + Supply: 1_000_000_000, + CirculatingSupply: 900_000_000, + StakedSupply: 450_000_000, + ProtocolReserve: 100_000_000, + BurnedThisEpoch: 10_000, + FinalizedOperations: monetary.OperationsTarget, + ResourceUtilizationBps: monetary.UtilizationTargetBps, + AgeWeightedVelocityBps: monetary.VelocityTargetBps, + } + base, err := EvaluateShadow(monetary.TargetInflationBps, metrics, monetary) + if err != nil { + t.Fatal(err) + } + scarcity := ComputeScarcitySnapshot{Epoch: 1, ScoreBps: 8_000, Reliable: true} + + observe, err := EvaluateComputeFeedback(base, metrics, monetary, scarcity, DefaultComputeFeedbackPolicy(ComputeFeedbackObserveOnly)) + if err != nil { + t.Fatal(err) + } + if observe.SuggestedTargetInflationBps != base.TargetInflationBps || observe.ComputeRewardShareBps != 1_000 { + t.Fatalf("observe-only mode altered policy: %#v", observe) + } + + routing, err := EvaluateComputeFeedback(base, metrics, monetary, scarcity, DefaultComputeFeedbackPolicy(ComputeFeedbackRewardRouting)) + if err != nil { + t.Fatal(err) + } + if routing.ComputeRewardShareBps <= observe.ComputeRewardShareBps || routing.SuggestedTargetInflationBps != base.TargetInflationBps { + t.Fatalf("reward-routing mode did not isolate compute allocation: %#v", routing) + } + + band, err := EvaluateComputeFeedback(base, metrics, monetary, scarcity, DefaultComputeFeedbackPolicy(ComputeFeedbackMonetaryBand)) + if err != nil { + t.Fatal(err) + } + if !band.Shadow || band.InflationCorrectionBps <= 0 || band.SuggestedTargetInflationBps <= base.TargetInflationBps { + t.Fatalf("monetary-band mode did not produce bounded shadow correction: %#v", band) + } +} + +func TestUnreliableScarcityCannotMoveComputePolicy(t *testing.T) { + monetary := DefaultShadowPolicy() + metrics := MonetaryMetrics{ + Supply: 1_000_000_000, + CirculatingSupply: 900_000_000, + StakedSupply: 450_000_000, + ProtocolReserve: 100_000_000, + FinalizedOperations: monetary.OperationsTarget, + ResourceUtilizationBps: monetary.UtilizationTargetBps, + AgeWeightedVelocityBps: monetary.VelocityTargetBps, + } + base, err := EvaluateShadow(200, metrics, monetary) + if err != nil { + t.Fatal(err) + } + decision, err := EvaluateComputeFeedback(base, metrics, monetary, ComputeScarcitySnapshot{Epoch: 1, ScoreBps: 10_000, Reliable: false}, DefaultComputeFeedbackPolicy(ComputeFeedbackMonetaryBand)) + if err != nil { + t.Fatal(err) + } + if decision.InflationCorrectionBps != 0 || decision.SuggestedTargetInflationBps != base.TargetInflationBps || decision.ComputeRewardShareBps != 1_000 { + t.Fatalf("unreliable scarcity moved policy: %#v", decision) + } +} From 25b0d513a7e28cc96f19b988a898f4429a42dfee Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:19:09 +0200 Subject: [PATCH 171/274] Extend economics simulator with ZCSI modes --- cmd/zephyr-econ-sim/main.go | 42 +++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/cmd/zephyr-econ-sim/main.go b/cmd/zephyr-econ-sim/main.go index 1de64f07..d9231827 100644 --- a/cmd/zephyr-econ-sim/main.go +++ b/cmd/zephyr-econ-sim/main.go @@ -11,9 +11,19 @@ import ( ) type simulationInput struct { - PriorTargetBps uint32 `json:"priorTargetBps"` - Metrics economics.MonetaryMetrics `json:"metrics"` - Policy *economics.MonetaryPolicy `json:"policy,omitempty"` + Epoch uint64 `json:"epoch,omitempty"` + PriorTargetBps uint32 `json:"priorTargetBps"` + Metrics economics.MonetaryMetrics `json:"metrics"` + Policy *economics.MonetaryPolicy `json:"policy,omitempty"` + ComputeMarket *economics.ComputeMarketMetrics `json:"computeMarket,omitempty"` + ScarcityConfig *economics.ComputeScarcityConfig `json:"scarcityConfig,omitempty"` + FeedbackPolicy *economics.ComputeFeedbackPolicy `json:"feedbackPolicy,omitempty"` +} + +type simulationOutput struct { + Monetary economics.MonetaryDecision `json:"monetary"` + Scarcity *economics.ComputeScarcitySnapshot `json:"scarcity,omitempty"` + Feedback *economics.ComputeFeedbackDecision `json:"feedback,omitempty"` } func main() { @@ -42,9 +52,33 @@ func main() { if err != nil { fatal(err) } + output := simulationOutput{Monetary: decision} + if input.ComputeMarket != nil { + if input.Epoch == 0 { + fatal(fmt.Errorf("epoch is required when computeMarket is provided")) + } + scarcityConfig := economics.DefaultComputeScarcityConfig() + if input.ScarcityConfig != nil { + scarcityConfig = *input.ScarcityConfig + } + scarcity, err := economics.BuildComputeScarcity(input.Epoch, *input.ComputeMarket, scarcityConfig) + if err != nil { + fatal(err) + } + output.Scarcity = &scarcity + feedbackPolicy := economics.DefaultComputeFeedbackPolicy(economics.ComputeFeedbackObserveOnly) + if input.FeedbackPolicy != nil { + feedbackPolicy = *input.FeedbackPolicy + } + feedback, err := economics.EvaluateComputeFeedback(decision, input.Metrics, policy, scarcity, feedbackPolicy) + if err != nil { + fatal(err) + } + output.Feedback = &feedback + } encoder := json.NewEncoder(os.Stdout) encoder.SetIndent("", " ") - if err := encoder.Encode(decision); err != nil { + if err := encoder.Encode(output); err != nil { fatal(err) } } From 20f133da1e008c20ffc968f8eb09f82e0d4d85d8 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:20:28 +0200 Subject: [PATCH 172/274] Document ZCPI and ZCSI compute economics --- docs/compute-economics-v2.md | 414 +++++++++++++++++++++++++++++++++++ 1 file changed, 414 insertions(+) create mode 100644 docs/compute-economics-v2.md diff --git a/docs/compute-economics-v2.md b/docs/compute-economics-v2.md new file mode 100644 index 00000000..3daee875 --- /dev/null +++ b/docs/compute-economics-v2.md @@ -0,0 +1,414 @@ +# Zephyr v2 Compute Economics — ZCR, ZCPI and ZCSI + +Status: **design contract + executable shadow-mode instrumentation**. + +This document specifies how Zephyr measures compute work and how compute-market conditions may later influence protocol incentives without relying on external oracles. + +The core rule is: + +```text +measure first -> simulate second -> activate last +``` + +Nothing in this document authorizes live monetary minting. The current implementation is shadow-only. + +## 1. Why one universal compute unit is not enough + +CPU, GPU FP32, GPU FP64, tensor/AI, memory-heavy, rendering, storage and network workloads are not honestly comparable with one theoretical FLOP number. + +Zephyr therefore models compute as a **normalized resource vector** plus a workload-specific logical unit definition. + +The v2 resource vector currently includes: + +```text +CPUUnits +GPUFP32Units +GPUFP64Units +TensorUnits +MemoryByteSeconds +VRAMByteSeconds +StorageBytes +NetworkBytes +``` + +A standardized workload specification binds: + +```text +WorkloadHash +BenchmarkHash +WorkClass +normalized Units +WorkVector +version +``` + +The `BenchmarkHash` anchors the meaning of the unit. Provider self-reported peak performance is not sufficient. + +## 2. Eligible compute observations + +A compute observation may enter economic telemetry only when all of the following are true: + +1. the workload is registered against a versioned benchmark/specification; +2. the job was funded with real on-chain escrow; +3. the job reached a verification-satisfied settled state; +4. the result commitment is finalized; +5. the provider payment is the amount actually settled on-chain. + +Zephyr does **not** use advertised provider prices, unfilled offers or arbitrary self-reported capacity as a price observation. + +## 3. ZCPI — Zephyr Compute Price Index + +ZCPI answers: + +> how many atomic ZPH units were actually paid for standardized, verified compute work? + +For each eligible workload class: + +```text +class price = settled ZPH / normalized verified work units +``` + +The reference implementation uses: + +- integer/fixed-point Q9 prices; +- per-class medians; +- configurable basket weights; +- EWMA smoothing; +- minimum samples per class; +- basket coverage; +- an explicit `Reliable` flag. + +If market coverage is too thin, ZCPI is marked unreliable and its price-trend signal is excluded from ZCSI. + +ZCPI is an internal Zephyr compute-market index. It is not CPI and does not claim to measure fiat purchasing power or real-world inflation. + +## 4. Why ZCPI alone must not control inflation + +A higher ZPH price for compute can have multiple causes: + +```text +compute supply became scarce +compute demand increased +ZPH purchasing power against compute changed +workload mix changed +``` + +Therefore `ZCPI up -> mint more ZPH` is not a safe rule. + +Zephyr combines price information with observable demand/supply conditions in a separate **Zephyr Compute Scarcity Index (ZCSI)**. + +## 5. ZCSI — Zephyr Compute Scarcity Index + +ZCSI is a bounded signed score derived only from on-chain/consensus-reproducible compute-market metrics. + +The reference inputs are: + +```text +EscrowBackedDemandUnits +VerifiedSupplyUnits +BacklogUnits +FulfilledUnits +UtilizationBps +ComputePriceTrendBps +ComputeIndexReliable +``` + +Interpretation: + +- positive ZCSI: standardized compute is relatively scarce; +- near-zero ZCSI: demand and verified capacity are broadly balanced; +- negative ZCSI: verified capacity is abundant relative to demand. + +The score is bounded in basis points and uses integer arithmetic only. + +## 6. What counts as demand + +`EscrowBackedDemandUnits` must represent standardized work for jobs with real locked budget/escrow. + +The following do **not** count as monetary demand: + +- free API requests; +- un-funded job drafts; +- provider-created fake offers; +- arbitrary mempool messages; +- unregistered workload units. + +This makes demand manipulation economically costly rather than free. + +## 7. What counts as supply + +`VerifiedSupplyUnits` must eventually be derived from capacity that is: + +- benchmarked against an approved workload specification; +- bound to a provider identity; +- backed by collateral where required; +- recently demonstrated/available rather than permanently self-declared; +- normalized to the same workload units used by demand. + +The current ZCSI implementation consumes this metric but does not yet define the final consensus transition that produces the capacity registry. Until that registry is authenticated, ZCSI remains shadow-only. + +## 8. ZCSI components + +The reference score combines five signals. + +### Demand/supply pressure + +Conceptually: + +```text +(Demand - Supply) / Supply +``` + +bounded to a finite interval. + +### ZCPI price trend + +The smoothed ZCPI trend enters only if the compute index is reliable. If reliability is false, its weight is removed rather than treated as zero-price information. + +### Backlog pressure + +```text +Backlog / EscrowBackedDemand +``` + +A growing funded backlog indicates work waiting for capacity. + +### Utilization pressure + +```text +VerifiedUtilization - TargetUtilization +``` + +High persistent utilization supports a scarcity interpretation; low utilization pushes in the opposite direction. + +### Fulfillment pressure + +```text +1 - FulfilledWork / EscrowBackedDemand +``` + +A low fulfillment ratio indicates that funded standardized demand is not being satisfied. + +## 9. Reliability gates + +ZCSI has its own reliability gate in addition to ZCPI reliability. + +The reference configuration requires minimum standardized demand and verified supply volumes. If these are not met: + +```text +ZCSI.Reliable = false +``` + +An unreliable ZCSI is prohibited from moving either compute incentive routing or the monetary target in the shadow feedback evaluator. + +## 10. Three feedback modes + +The repository implements three **simulation-only** modes. + +### Mode A — Observe only + +```text +ZCSI -> telemetry +compute reward share -> unchanged +inflation target -> unchanged +``` + +This is the default and safest mode. + +### Mode B — Reward routing + +```text +ZCSI -> compute incentive share +inflation target -> unchanged +``` + +When verified compute is scarce, a larger fraction of the epoch's **net issuance budget** may be suggested for verified compute incentives. When capacity is abundant, that share may fall. + +This changes distribution, not the total monetary target. + +This is the leading candidate for first activation after sufficient devnet evidence. + +### Mode C — Reward routing + narrow monetary band + +```text +ZCSI -> compute incentive share +ZCSI -> small bounded inflation correction +``` + +Mode C may additionally suggest a very small correction around the ZAMP target. The correction is: + +- bounded; +- integer-only; +- subject to ZAMP's overall min/max band; +- disabled when ZCSI is unreliable; +- shadow-only until long simulations show a clear stability benefit. + +The compute feedback sensitivity is intentionally much smaller for total inflation than for reward routing. + +## 11. Reference default parameters are test values + +The checked-in defaults exist to make simulations reproducible. They are not mainnet constants. + +The current reference configuration uses a 10,000-bps bounded ZCSI score and a weighted mix of demand/supply, price trend, backlog, utilization and fulfillment. + +The current feedback reference starts with a compute reward share and permits a larger adjustment to **distribution** than to the total inflation target. + +No parameter should become public-network policy without replay, sensitivity and adversarial simulation. + +## 12. Anti-manipulation model + +The main attacks to test are: + +### Wash compute + +An attacker controls both client and provider and creates fake jobs. + +Mitigations to measure: + +- real escrow/settlement cost; +- standardized verified work requirement; +- provider collateral/slashing; +- fee burn; +- median rather than mean pricing; +- EWMA smoothing; +- per-identity/provider concentration limits if required. + +### Fake demand spam + +Unfunded jobs do not count. Only escrow-backed standardized demand enters ZCSI. + +### Fake supply + +Advertised GPU/CPU capacity does not count merely because a provider claims it. Production supply accounting must be benchmark-backed and availability-aware. + +### Price self-trading + +ZCPI uses completed verified settlements, but a client/provider pair can still trade with itself at a cost. Median, minimum sample/coverage requirements, collateral, fees and future concentration metrics reduce leverage. Devnet simulation must explicitly measure the cost of moving the index. + +### Workload-mix attack + +Per-class ZCPI values are preserved. Basket weights and registry activation are versioned so one newly created workload class cannot silently redefine the whole compute economy. + +## 13. Monetary relationship + +ZCSI should influence **incentive routing before total issuance**. + +The intended priority is: + +```text +1. measure compute scarcity +2. adjust suggested compute incentive share +3. observe whether new capacity enters +4. only after evidence, test a small total-inflation correction +``` + +The protocol should not solve a GPU shortage by blindly printing currency. The goal is to direct incentives toward the scarce resource while preserving a stable monetary constitution. + +## 14. Economics simulator + +`cmd/zephyr-econ-sim` supports the base ZAMP shadow decision and optional compute-market inputs. + +When `computeMarket` is present, `epoch` is required. The simulator returns: + +```text +monetary +scarcity (ZCSI) +feedback (A/B/C shadow decision) +``` + +A representative input shape is: + +```json +{ + "epoch": 42, + "priorTargetBps": 200, + "metrics": { + "Supply": 1000000000, + "CirculatingSupply": 900000000, + "StakedSupply": 450000000, + "ProtocolReserve": 100000000, + "BurnedThisEpoch": 12000, + "FinalizedOperations": 1000000, + "ResourceUtilizationBps": 5000, + "AgeWeightedVelocityBps": 5000, + "ComputeIndexQ9": 7500000000, + "ComputePriceTrendBps": 800, + "ComputeIndexReliable": true + }, + "computeMarket": { + "EscrowBackedDemandUnits": 200000, + "VerifiedSupplyUnits": 150000, + "BacklogUnits": 30000, + "FulfilledUnits": 170000, + "UtilizationBps": 8500, + "ComputePriceTrendBps": 800, + "ComputeIndexReliable": true + }, + "feedbackPolicy": { + "Mode": 1, + "BaseComputeRewardShareBps": 1000, + "MinComputeRewardShareBps": 0, + "MaxComputeRewardShareBps": 3000, + "RewardSensitivityBps": 2000, + "MonetarySensitivityBps": 25, + "MaxInflationCorrectionBps": 25 + } +} +``` + +Mode values are currently: + +```text +0 = observe only +1 = reward routing +2 = reward routing + narrow monetary band +``` + +These numeric values are experimental protocol-tooling values, not a public RPC stability promise. + +## 15. Required simulation matrix + +Before any non-zero compute feedback becomes live, replay at least: + +- balanced demand/supply; +- AI demand boom; +- sudden provider loss; +- rapid GPU capacity entry; +- persistent overcapacity; +- ZCPI rising with stable demand/supply; +- ZCPI falling while backlog rises; +- thin/unreliable market coverage; +- wash compute between related accounts; +- provider concentration/cartel scenarios; +- job spam with and without escrow; +- compute-price shocks during low/high fee burn; +- validator/partition events while compute metrics are incomplete; +- recovery after prolonged scarcity; +- alternating scarcity/abundance designed to induce controller oscillation. + +Compare all three modes on exactly the same epoch sequence. + +## 16. Activation gates + +Compute feedback remains shadow-only until: + +1. workload registry changes are authenticated and delayed; +2. verified supply accounting is consensus-reproducible; +3. demand includes only escrow-backed standardized work; +4. ZCPI and ZCSI can be reproduced by Citizen Nodes from finalized state; +5. index manipulation cost is quantified; +6. feedback remains stable under adversarial oscillating workloads; +7. Mode B demonstrates that reward routing increases useful capacity without destabilizing ZPH; +8. Mode C, if ever considered, demonstrates a material benefit over Mode B; +9. governance changes to weights/sensitivities are bounded and delayed; +10. unreliable/missing compute metrics fail closed to zero feedback. + +## 17. Current recommendation + +Run Mode A on early devnet to collect data. + +Then replay the same history under A/B/C. + +If evidence supports activation, prefer **Mode B first**: keep the ~2% ZAMP monetary target structurally stable while allowing compute scarcity to redirect part of net issuance toward verified capacity. + +Mode C should remain a later option, not the default assumption. From 8354510f1694b6e6be64f5572c7f9f92b8ca9c32 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:21:15 +0200 Subject: [PATCH 173/274] Add ZCSI economics replay example --- docs/examples/zephyr-econ-sim-compute.json | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/examples/zephyr-econ-sim-compute.json diff --git a/docs/examples/zephyr-econ-sim-compute.json b/docs/examples/zephyr-econ-sim-compute.json new file mode 100644 index 00000000..f01b3bda --- /dev/null +++ b/docs/examples/zephyr-econ-sim-compute.json @@ -0,0 +1,35 @@ +{ + "epoch": 42, + "priorTargetBps": 200, + "metrics": { + "Supply": 1000000000, + "CirculatingSupply": 900000000, + "StakedSupply": 450000000, + "ProtocolReserve": 100000000, + "BurnedThisEpoch": 12000, + "FinalizedOperations": 1000000, + "ResourceUtilizationBps": 5000, + "AgeWeightedVelocityBps": 5000, + "ComputeIndexQ9": 7500000000, + "ComputePriceTrendBps": 800, + "ComputeIndexReliable": true + }, + "computeMarket": { + "EscrowBackedDemandUnits": 200000, + "VerifiedSupplyUnits": 150000, + "BacklogUnits": 30000, + "FulfilledUnits": 170000, + "UtilizationBps": 8500, + "ComputePriceTrendBps": 800, + "ComputeIndexReliable": true + }, + "feedbackPolicy": { + "Mode": 1, + "BaseComputeRewardShareBps": 1000, + "MinComputeRewardShareBps": 0, + "MaxComputeRewardShareBps": 3000, + "RewardSensitivityBps": 2000, + "MonetarySensitivityBps": 25, + "MaxInflationCorrectionBps": 25 + } +} From 0df34bfba81417a3efd28d538d985d29aa29f086 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:25:54 +0200 Subject: [PATCH 174/274] Format compute feedback policy --- internal/v2/economics/compute_feedback.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/v2/economics/compute_feedback.go b/internal/v2/economics/compute_feedback.go index e24da4d6..092bb4fb 100644 --- a/internal/v2/economics/compute_feedback.go +++ b/internal/v2/economics/compute_feedback.go @@ -13,13 +13,13 @@ const ( ) type ComputeFeedbackPolicy struct { - Mode ComputeFeedbackMode - BaseComputeRewardShareBps uint32 - MinComputeRewardShareBps uint32 - MaxComputeRewardShareBps uint32 - RewardSensitivityBps uint32 - MonetarySensitivityBps uint32 - MaxInflationCorrectionBps uint32 + Mode ComputeFeedbackMode + BaseComputeRewardShareBps uint32 + MinComputeRewardShareBps uint32 + MaxComputeRewardShareBps uint32 + RewardSensitivityBps uint32 + MonetarySensitivityBps uint32 + MaxInflationCorrectionBps uint32 } type ComputeFeedbackDecision struct { From 7d8a280771d520c54e0cdb2efd4a80fe514a379d Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:26:52 +0200 Subject: [PATCH 175/274] Format ZCSI simulator input --- cmd/zephyr-econ-sim/main.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/zephyr-econ-sim/main.go b/cmd/zephyr-econ-sim/main.go index d9231827..866d8533 100644 --- a/cmd/zephyr-econ-sim/main.go +++ b/cmd/zephyr-econ-sim/main.go @@ -11,18 +11,18 @@ import ( ) type simulationInput struct { - Epoch uint64 `json:"epoch,omitempty"` - PriorTargetBps uint32 `json:"priorTargetBps"` - Metrics economics.MonetaryMetrics `json:"metrics"` - Policy *economics.MonetaryPolicy `json:"policy,omitempty"` - ComputeMarket *economics.ComputeMarketMetrics `json:"computeMarket,omitempty"` - ScarcityConfig *economics.ComputeScarcityConfig `json:"scarcityConfig,omitempty"` - FeedbackPolicy *economics.ComputeFeedbackPolicy `json:"feedbackPolicy,omitempty"` + Epoch uint64 `json:"epoch,omitempty"` + PriorTargetBps uint32 `json:"priorTargetBps"` + Metrics economics.MonetaryMetrics `json:"metrics"` + Policy *economics.MonetaryPolicy `json:"policy,omitempty"` + ComputeMarket *economics.ComputeMarketMetrics `json:"computeMarket,omitempty"` + ScarcityConfig *economics.ComputeScarcityConfig `json:"scarcityConfig,omitempty"` + FeedbackPolicy *economics.ComputeFeedbackPolicy `json:"feedbackPolicy,omitempty"` } type simulationOutput struct { Monetary economics.MonetaryDecision `json:"monetary"` - Scarcity *economics.ComputeScarcitySnapshot `json:"scarcity,omitempty"` + Scarcity *economics.ComputeScarcitySnapshot `json:"scarcity,omitempty"` Feedback *economics.ComputeFeedbackDecision `json:"feedback,omitempty"` } From 75a21715024abda0a8c32efba779a7ac054b0d20 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:34:06 +0200 Subject: [PATCH 176/274] Add coin creation height for velocity proofs --- internal/v2/object/object.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/internal/v2/object/object.go b/internal/v2/object/object.go index f07f9058..a414908d 100644 --- a/internal/v2/object/object.go +++ b/internal/v2/object/object.go @@ -158,14 +158,16 @@ func (o OutputSpec) Hash() types.Hash { } type Coin struct { - Token types.TokenID - Amount uint64 + Token types.TokenID + Amount uint64 + CreatedHeight uint64 } func (c Coin) MarshalBinary() []byte { var w codec.Writer w.Fixed(c.Token[:]) w.U64(c.Amount) + w.U64(c.CreatedHeight) return w.BytesCopy() } @@ -179,6 +181,10 @@ func ParseCoin(data []byte) (Coin, error) { if err != nil || amount == 0 { return Coin{}, ErrInvalidCoin } + createdHeight, err := r.U64() + if err != nil { + return Coin{}, ErrInvalidCoin + } if err := r.Done(); err != nil { return Coin{}, ErrInvalidCoin } @@ -187,13 +193,21 @@ func ParseCoin(data []byte) (Coin, error) { if types.IsZero32([32]byte(token)) { return Coin{}, ErrInvalidCoin } - return Coin{Token: token, Amount: amount}, nil + return Coin{Token: token, Amount: amount, CreatedHeight: createdHeight}, nil } func NewCoinOutput(owner types.AccountID, token types.TokenID, amount uint64) (OutputSpec, error) { + return NewCoinOutputAtHeight(owner, token, amount, 0) +} + +func NewCoinOutputAtHeight(owner types.AccountID, token types.TokenID, amount, createdHeight uint64) (OutputSpec, error) { if types.IsZero32([32]byte(owner)) || types.IsZero32([32]byte(token)) || amount == 0 { return OutputSpec{}, ErrInvalidCoin } - out := OutputSpec{Owner: owner, Kind: KindCoin, Data: Coin{Token: token, Amount: amount}.MarshalBinary()} + out := OutputSpec{ + Owner: owner, + Kind: KindCoin, + Data: Coin{Token: token, Amount: amount, CreatedHeight: createdHeight}.MarshalBinary(), + } return out, out.Validate() } From 200557acdf43d0d83a68c9048fb06897e36f0d37 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:34:31 +0200 Subject: [PATCH 177/274] Stamp coin creation height during execution --- internal/v2/execution/coin_metadata.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 internal/v2/execution/coin_metadata.go diff --git a/internal/v2/execution/coin_metadata.go b/internal/v2/execution/coin_metadata.go new file mode 100644 index 00000000..8afd055f --- /dev/null +++ b/internal/v2/execution/coin_metadata.go @@ -0,0 +1,17 @@ +package execution + +import "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + +func stampCoinOutput(spec object.OutputSpec, height uint64) (object.OutputSpec, error) { + if spec.Kind != object.KindCoin { + return spec, nil + } + coin, err := object.ParseCoin(spec.Data) + if err != nil { + return object.OutputSpec{}, err + } + coin.CreatedHeight = height + stamped := spec + stamped.Data = coin.MarshalBinary() + return stamped, nil +} From 36dfe30c697d08a21024123c71c22df639e752eb Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:35:24 +0200 Subject: [PATCH 178/274] Stamp native and token outputs with block height --- internal/v2/execution/engine.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/v2/execution/engine.go b/internal/v2/execution/engine.go index 9b1bd70d..0cdc63d0 100644 --- a/internal/v2/execution/engine.go +++ b/internal/v2/execution/engine.go @@ -137,13 +137,17 @@ func (e Engine) executeTransfer(t tx.Transaction) (Result, error) { if coin.Token != e.NativeToken && destination != t.ShardID { return Result{}, ErrTokenPolicy } + stamped, err := stampCoinOutput(spec, e.Height) + if err != nil { + return Result{}, err + } if destination == t.ShardID { created = append(created, object.Object{ ID: types.ObjectIDForShard(txID, uint32(i), destination), Version: 1, - Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...), + Owner: stamped.Owner, Kind: stamped.Kind, Data: append([]byte(nil), stamped.Data...), }) } else { - outbound = append(outbound, OutboundOutput{DestinationShard: destination, OutputIndex: uint32(i), Output: spec}) + outbound = append(outbound, OutboundOutput{DestinationShard: destination, OutputIndex: uint32(i), Output: stamped}) } } @@ -215,13 +219,17 @@ func (e Engine) executeCreateToken(t tx.Transaction, payload []byte) (Result, er if err != nil { return Result{}, ErrShard } + stamped, err := stampCoinOutput(spec, e.Height) + if err != nil { + return Result{}, err + } if destination == t.ShardID { created = append(created, object.Object{ ID: types.ObjectIDForShard(txID, uint32(i), destination), Version: 1, - Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...), + Owner: stamped.Owner, Kind: stamped.Kind, Data: append([]byte(nil), stamped.Data...), }) } else { - outbound = append(outbound, OutboundOutput{DestinationShard: destination, OutputIndex: uint32(i), Output: spec}) + outbound = append(outbound, OutboundOutput{DestinationShard: destination, OutputIndex: uint32(i), Output: stamped}) } } if math.MaxUint64-nativeOut < t.Fee || nativeIn != nativeOut+t.Fee { @@ -242,7 +250,7 @@ func (e Engine) executeCreateToken(t tx.Transaction, payload []byte) (Result, er created = append(created, object.Object{ ID: defID, Version: 1, Owner: t.Sender, Kind: object.KindTokenDefinition, Data: defData, }) - initialCoin, err := object.NewCoinOutput(t.Sender, tokenID, create.InitialSupply) + initialCoin, err := object.NewCoinOutputAtHeight(t.Sender, tokenID, create.InitialSupply, e.Height) if err != nil { return Result{}, err } From 9e62d11f28948fd049fbe1ad938fd83df1892a2d Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:36:06 +0200 Subject: [PATCH 179/274] Stamp contract fee outputs with block height --- internal/v2/execution/extended_contract.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/v2/execution/extended_contract.go b/internal/v2/execution/extended_contract.go index e13c3f33..14ad304b 100644 --- a/internal/v2/execution/extended_contract.go +++ b/internal/v2/execution/extended_contract.go @@ -181,7 +181,11 @@ func (e Engine) feeOnlyOutputsExcluding(t tx.Transaction, contractObject types.O return nil, nil, ErrOverflow } nativeOut += coin.Amount - created = append(created, object.Object{ID: types.ObjectIDForShard(t.ID(), uint32(i), t.ShardID), Version: 1, Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...)}) + stamped, err := stampCoinOutput(spec, e.Height) + if err != nil { + return nil, nil, err + } + created = append(created, object.Object{ID: types.ObjectIDForShard(t.ID(), uint32(i), t.ShardID), Version: 1, Owner: stamped.Owner, Kind: stamped.Kind, Data: append([]byte(nil), stamped.Data...)}) } if math.MaxUint64-nativeOut < t.Fee || nativeIn != nativeOut+t.Fee { return nil, nil, ErrConservation From 2956481b1cb12c9c279937ec5aeafe82fe5b6303 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:36:38 +0200 Subject: [PATCH 180/274] Stamp minted and burn-change coin heights --- internal/v2/execution/token_mutation.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/v2/execution/token_mutation.go b/internal/v2/execution/token_mutation.go index 08357b63..7e2c3aa7 100644 --- a/internal/v2/execution/token_mutation.go +++ b/internal/v2/execution/token_mutation.go @@ -40,7 +40,7 @@ func (e Engine) executeMintToken(t tx.Transaction, payload []byte) (Result, erro created = append(created, updatedDefinition) consumed = append(consumed, definitionObject.ID) - minted, err := object.NewCoinOutput(request.Recipient, definition.TokenID, request.Amount) + minted, err := object.NewCoinOutputAtHeight(request.Recipient, definition.TokenID, request.Amount, e.Height) if err != nil { return Result{}, err } @@ -130,9 +130,13 @@ func (e Engine) executeBurnToken(t tx.Transaction, payload []byte) (Result, erro default: return Result{}, ErrConservation } + stamped, err := stampCoinOutput(spec, e.Height) + if err != nil { + return Result{}, err + } created = append(created, object.Object{ ID: types.ObjectIDForShard(t.ID(), uint32(i), t.ShardID), Version: 1, - Owner: spec.Owner, Kind: spec.Kind, Data: append([]byte(nil), spec.Data...), + Owner: stamped.Owner, Kind: stamped.Kind, Data: append([]byte(nil), stamped.Data...), }) } if math.MaxUint64-nativeOut < t.Fee || nativeIn != nativeOut+t.Fee { From f9fc277135db448f143617172cdb1ce4fc2c0670 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:37:15 +0200 Subject: [PATCH 181/274] Add age-weighted velocity accumulator --- internal/v2/economics/velocity.go | 90 +++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 internal/v2/economics/velocity.go diff --git a/internal/v2/economics/velocity.go b/internal/v2/economics/velocity.go new file mode 100644 index 00000000..a9d81513 --- /dev/null +++ b/internal/v2/economics/velocity.go @@ -0,0 +1,90 @@ +package economics + +import ( + "errors" + "math/big" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" +) + +var ErrVelocity = errors.New("invalid Zephyr age-weighted velocity input") + +type VelocityPolicy struct { + MinAgeBlocks uint64 + FullWeightAgeBlocks uint64 + MaxVelocityBps uint32 +} + +type VelocitySnapshot struct { + AgeWeightedVelocityBps uint32 + ObservedSpends uint64 + EligibleSpends uint64 + UnknownAgeSpends uint64 + FreshSpends uint64 +} + +type VelocityAccumulator struct { + policy VelocityPolicy + weightedValueBps big.Int + observedSpends uint64 + eligibleSpends uint64 + unknownAgeSpends uint64 + freshSpends uint64 +} + +func NewVelocityAccumulator(policy VelocityPolicy) (*VelocityAccumulator, error) { + if policy.FullWeightAgeBlocks == 0 || policy.MinAgeBlocks > policy.FullWeightAgeBlocks || policy.MaxVelocityBps == 0 || policy.MaxVelocityBps > 10*BasisPoints { + return nil, ErrVelocity + } + return &VelocityAccumulator{policy: policy}, nil +} + +// ObserveCoin records a finalized spend. CreatedHeight must come from the +// consensus-stamped coin object, never from an untrusted RPC timestamp. +func (a *VelocityAccumulator) ObserveCoin(coin object.Coin, spendHeight uint64) error { + if a == nil || coin.Amount == 0 { + return ErrVelocity + } + a.observedSpends++ + if coin.CreatedHeight == 0 { + a.unknownAgeSpends++ + return nil + } + if spendHeight <= coin.CreatedHeight { + return ErrVelocity + } + age := spendHeight - coin.CreatedHeight + if age < a.policy.MinAgeBlocks { + a.freshSpends++ + return nil + } + if age > a.policy.FullWeightAgeBlocks { + age = a.policy.FullWeightAgeBlocks + } + weight := new(big.Int).Mul(new(big.Int).SetUint64(age), new(big.Int).SetUint64(uint64(BasisPoints))) + weight.Quo(weight, new(big.Int).SetUint64(a.policy.FullWeightAgeBlocks)) + contribution := new(big.Int).Mul(new(big.Int).SetUint64(coin.Amount), weight) + a.weightedValueBps.Add(&a.weightedValueBps, contribution) + a.eligibleSpends++ + return nil +} + +// Finalize normalizes the accumulated age-weighted moved value by circulating +// supply. A full-age spend of 50% of circulating supply contributes 5,000 bps. +func (a *VelocityAccumulator) Finalize(circulatingSupply uint64) (VelocitySnapshot, error) { + if a == nil || circulatingSupply == 0 { + return VelocitySnapshot{}, ErrVelocity + } + value := new(big.Int).Quo(new(big.Int).Set(&a.weightedValueBps), new(big.Int).SetUint64(circulatingSupply)) + maximum := new(big.Int).SetUint64(uint64(a.policy.MaxVelocityBps)) + if value.Cmp(maximum) > 0 { + value.Set(maximum) + } + return VelocitySnapshot{ + AgeWeightedVelocityBps: uint32(value.Uint64()), + ObservedSpends: a.observedSpends, + EligibleSpends: a.eligibleSpends, + UnknownAgeSpends: a.unknownAgeSpends, + FreshSpends: a.freshSpends, + }, nil +} From 74ffcc6c4d03532148ac7c0d51e2bb0fad57e587 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:37:44 +0200 Subject: [PATCH 182/274] Test age-weighted velocity resistance --- internal/v2/economics/velocity_test.go | 96 ++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 internal/v2/economics/velocity_test.go diff --git a/internal/v2/economics/velocity_test.go b/internal/v2/economics/velocity_test.go new file mode 100644 index 00000000..6068c162 --- /dev/null +++ b/internal/v2/economics/velocity_test.go @@ -0,0 +1,96 @@ +package economics + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestAgeWeightedVelocityRewardsOlderCirculation(t *testing.T) { + policy := VelocityPolicy{MinAgeBlocks: 10, FullWeightAgeBlocks: 100, MaxVelocityBps: 20_000} + accumulator, err := NewVelocityAccumulator(policy) + if err != nil { + t.Fatal(err) + } + var token types.TokenID + token[0] = 1 + if err := accumulator.ObserveCoin(object.Coin{Token: token, Amount: 500, CreatedHeight: 100}, 200); err != nil { + t.Fatal(err) + } + snapshot, err := accumulator.Finalize(1_000) + if err != nil { + t.Fatal(err) + } + if snapshot.AgeWeightedVelocityBps != 5_000 || snapshot.EligibleSpends != 1 { + t.Fatalf("unexpected old-coin velocity: %#v", snapshot) + } +} + +func TestAgeWeightedVelocitySuppressesRapidSelfCycling(t *testing.T) { + policy := VelocityPolicy{MinAgeBlocks: 10, FullWeightAgeBlocks: 100, MaxVelocityBps: 20_000} + accumulator, err := NewVelocityAccumulator(policy) + if err != nil { + t.Fatal(err) + } + var token types.TokenID + token[0] = 1 + for height := uint64(101); height <= 109; height++ { + coin := object.Coin{Token: token, Amount: 1_000, CreatedHeight: height - 1} + if err := accumulator.ObserveCoin(coin, height); err != nil { + t.Fatal(err) + } + } + snapshot, err := accumulator.Finalize(1_000) + if err != nil { + t.Fatal(err) + } + if snapshot.AgeWeightedVelocityBps != 0 || snapshot.FreshSpends != 9 { + t.Fatalf("rapid cycling should have zero contribution under minimum age: %#v", snapshot) + } +} + +func TestAgeWeightedVelocityExcludesUnknownAgeAndRejectsImpossibleAge(t *testing.T) { + policy := VelocityPolicy{MinAgeBlocks: 1, FullWeightAgeBlocks: 100, MaxVelocityBps: 20_000} + accumulator, err := NewVelocityAccumulator(policy) + if err != nil { + t.Fatal(err) + } + var token types.TokenID + token[0] = 1 + if err := accumulator.ObserveCoin(object.Coin{Token: token, Amount: 100}, 10); err != nil { + t.Fatal(err) + } + if err := accumulator.ObserveCoin(object.Coin{Token: token, Amount: 100, CreatedHeight: 10}, 10); err != ErrVelocity { + t.Fatalf("expected impossible same-height spend rejection, got %v", err) + } + snapshot, err := accumulator.Finalize(1_000) + if err != nil { + t.Fatal(err) + } + if snapshot.UnknownAgeSpends != 1 { + t.Fatalf("unknown-age spend should be tracked but excluded: %#v", snapshot) + } +} + +func TestAgeWeightedVelocityIsBounded(t *testing.T) { + policy := VelocityPolicy{MinAgeBlocks: 1, FullWeightAgeBlocks: 1, MaxVelocityBps: 12_000} + accumulator, err := NewVelocityAccumulator(policy) + if err != nil { + t.Fatal(err) + } + var token types.TokenID + token[0] = 1 + for i := 0; i < 10; i++ { + if err := accumulator.ObserveCoin(object.Coin{Token: token, Amount: 1_000, CreatedHeight: 1}, 2); err != nil { + t.Fatal(err) + } + } + snapshot, err := accumulator.Finalize(1_000) + if err != nil { + t.Fatal(err) + } + if snapshot.AgeWeightedVelocityBps != 12_000 { + t.Fatalf("velocity clamp failed: %#v", snapshot) + } +} From 4ee8031887a3ec53c02ee331fe6c994a585b75b2 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:38:04 +0200 Subject: [PATCH 183/274] Test consensus-stamped coin creation height --- internal/v2/execution/coin_metadata_test.go | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 internal/v2/execution/coin_metadata_test.go diff --git a/internal/v2/execution/coin_metadata_test.go b/internal/v2/execution/coin_metadata_test.go new file mode 100644 index 00000000..c50e999c --- /dev/null +++ b/internal/v2/execution/coin_metadata_test.go @@ -0,0 +1,63 @@ +package execution + +import ( + "crypto/elliptic" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestExecutorOverridesWalletCoinCreationHeight(t *testing.T) { + key := makeKey(t) + owner := types.AccountIDFromPublicKey(elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y)) + network := types.NetworkID(types.HashBytes("network", []byte("coin-height"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + store := worldstate.NewMemory() + seed := types.HashBytes("coin-height", []byte("seed")) + inputID := types.ObjectIDForShard(seed, 0, 0) + inputSpec, err := object.NewCoinOutputAtHeight(owner, native, 100, 50) + if err != nil { + t.Fatal(err) + } + input := object.Object{ID: inputID, Version: 1, Owner: owner, Kind: object.KindCoin, Data: inputSpec.Data} + if _, err := store.Apply(nil, []object.Object{input}); err != nil { + t.Fatal(err) + } + witness, proof, ok := store.Proof(inputID) + if !ok { + t.Fatal("missing input proof") + } + maliciousOutput, err := object.NewCoinOutputAtHeight(owner, native, 99, 1) + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{ + Version: tx.Version, Network: network, ShardID: 0, StateRoot: store.Root(), + Inputs: []tx.InputRef{{ObjectID: inputID, Version: 1, ObjectHash: witness.Hash()}}, + Outputs: []object.OutputSpec{maliciousOutput}, + Operations: []tx.Operation{{Kind: tx.OpTransfer}}, + Fee: 1, + Witnesses: []tx.Witness{{Object: witness, Proof: proof}}, + } + transaction.Salt[0] = 1 + if err := transaction.Sign(key); err != nil { + t.Fatal(err) + } + result, err := (Engine{Network: network, NativeToken: native, ShardCount: 1, Height: 100}).Execute(transaction) + if err != nil { + t.Fatal(err) + } + if len(result.Created) != 1 { + t.Fatalf("unexpected output count %d", len(result.Created)) + } + coin, err := object.ParseCoin(result.Created[0].Data) + if err != nil { + t.Fatal(err) + } + if coin.CreatedHeight != 100 { + t.Fatalf("wallet-controlled height survived execution: got %d want 100", coin.CreatedHeight) + } +} From 255162281bc87d77a557b4b32af1f269dc2b1f06 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:40:26 +0200 Subject: [PATCH 184/274] Format age-weighted velocity accumulator --- internal/v2/economics/velocity.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/v2/economics/velocity.go b/internal/v2/economics/velocity.go index a9d81513..c798dfa2 100644 --- a/internal/v2/economics/velocity.go +++ b/internal/v2/economics/velocity.go @@ -24,12 +24,12 @@ type VelocitySnapshot struct { } type VelocityAccumulator struct { - policy VelocityPolicy - weightedValueBps big.Int - observedSpends uint64 - eligibleSpends uint64 - unknownAgeSpends uint64 - freshSpends uint64 + policy VelocityPolicy + weightedValueBps big.Int + observedSpends uint64 + eligibleSpends uint64 + unknownAgeSpends uint64 + freshSpends uint64 } func NewVelocityAccumulator(policy VelocityPolicy) (*VelocityAccumulator, error) { From f896074e83e6a52e27279e5084dd185748e59f38 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:41:24 +0200 Subject: [PATCH 185/274] Add per-shard economic epoch aggregation --- internal/v2/economics/epoch.go | 198 +++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 internal/v2/economics/epoch.go diff --git a/internal/v2/economics/epoch.go b/internal/v2/economics/epoch.go new file mode 100644 index 00000000..8ec95911 --- /dev/null +++ b/internal/v2/economics/epoch.go @@ -0,0 +1,198 @@ +package economics + +import ( + "errors" + "math/big" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const EpochMetricsVersion uint16 = 1 + +var ErrEpochMetrics = errors.New("invalid Zephyr economic epoch metrics") + +type ShardEpochMetrics struct { + Version uint16 + Epoch uint64 + ShardID uint32 + ChargedFees uint64 + BurnedFees uint64 + ValidatorFees uint64 + ReserveFees uint64 + FinalizedOperations uint64 + ResourceUsed uint64 + ResourceCapacity uint64 + CirculatingNativeSupply uint64 + AgeWeightedVelocityBps uint32 + EscrowBackedComputeDemand uint64 + VerifiedComputeSupply uint64 + ComputeBacklog uint64 + ComputeFulfilled uint64 +} + +func (m ShardEpochMetrics) Validate() error { + if m.Version != EpochMetricsVersion || m.Epoch == 0 || m.ResourceCapacity == 0 || + m.ResourceUsed > m.ResourceCapacity || m.AgeWeightedVelocityBps > 10*BasisPoints || + m.ComputeBacklog > m.EscrowBackedComputeDemand || m.ComputeFulfilled > m.EscrowBackedComputeDemand { + return ErrEpochMetrics + } + feeTotal := new(big.Int).SetUint64(m.BurnedFees) + feeTotal.Add(feeTotal, new(big.Int).SetUint64(m.ValidatorFees)) + feeTotal.Add(feeTotal, new(big.Int).SetUint64(m.ReserveFees)) + if !feeTotal.IsUint64() || feeTotal.Uint64() != m.ChargedFees { + return ErrEpochMetrics + } + return nil +} + +func (m ShardEpochMetrics) CanonicalBytes() ([]byte, error) { + if err := m.Validate(); err != nil { + return nil, err + } + var w codec.Writer + w.U16(m.Version) + w.U64(m.Epoch) + w.U32(m.ShardID) + w.U64(m.ChargedFees) + w.U64(m.BurnedFees) + w.U64(m.ValidatorFees) + w.U64(m.ReserveFees) + w.U64(m.FinalizedOperations) + w.U64(m.ResourceUsed) + w.U64(m.ResourceCapacity) + w.U64(m.CirculatingNativeSupply) + w.U32(m.AgeWeightedVelocityBps) + w.U64(m.EscrowBackedComputeDemand) + w.U64(m.VerifiedComputeSupply) + w.U64(m.ComputeBacklog) + w.U64(m.ComputeFulfilled) + return w.BytesCopy(), nil +} + +func (m ShardEpochMetrics) Hash() (types.Hash, error) { + raw, err := m.CanonicalBytes() + if err != nil { + return types.Hash{}, err + } + return types.Hash(codec.DomainHash("zephyr/shard-economics/v2", raw)), nil +} + +type EpochAggregate struct { + Epoch uint64 + ShardCount uint32 + ChargedFees uint64 + BurnedFees uint64 + ValidatorFees uint64 + ReserveFees uint64 + FinalizedOperations uint64 + ResourceUsed uint64 + ResourceCapacity uint64 + ResourceUtilizationBps uint32 + CirculatingNativeSupply uint64 + AgeWeightedVelocityBps uint32 + EscrowBackedComputeDemand uint64 + VerifiedComputeSupply uint64 + ComputeBacklog uint64 + ComputeFulfilled uint64 +} + +func AggregateEpochMetrics(metrics []ShardEpochMetrics) (EpochAggregate, error) { + if len(metrics) == 0 { + return EpochAggregate{}, ErrEpochMetrics + } + ordered := append([]ShardEpochMetrics(nil), metrics...) + sort.Slice(ordered, func(i, j int) bool { return ordered[i].ShardID < ordered[j].ShardID }) + epoch := ordered[0].Epoch + var charged, burned, validators, reserve, operations, used, capacity, circulating big.Int + var demand, supply, backlog, fulfilled, weightedVelocity big.Int + for i, metric := range ordered { + if err := metric.Validate(); err != nil || metric.Epoch != epoch || (i > 0 && ordered[i-1].ShardID == metric.ShardID) { + return EpochAggregate{}, ErrEpochMetrics + } + addBig(&charged, metric.ChargedFees) + addBig(&burned, metric.BurnedFees) + addBig(&validators, metric.ValidatorFees) + addBig(&reserve, metric.ReserveFees) + addBig(&operations, metric.FinalizedOperations) + addBig(&used, metric.ResourceUsed) + addBig(&capacity, metric.ResourceCapacity) + addBig(&circulating, metric.CirculatingNativeSupply) + addBig(&demand, metric.EscrowBackedComputeDemand) + addBig(&supply, metric.VerifiedComputeSupply) + addBig(&backlog, metric.ComputeBacklog) + addBig(&fulfilled, metric.ComputeFulfilled) + term := new(big.Int).Mul(new(big.Int).SetUint64(metric.CirculatingNativeSupply), new(big.Int).SetUint64(uint64(metric.AgeWeightedVelocityBps))) + weightedVelocity.Add(&weightedVelocity, term) + } + values := []*big.Int{&charged, &burned, &validators, &reserve, &operations, &used, &capacity, &circulating, &demand, &supply, &backlog, &fulfilled} + for _, value := range values { + if !value.IsUint64() { + return EpochAggregate{}, ErrEpochMetrics + } + } + out := EpochAggregate{ + Epoch: epoch, + ShardCount: uint32(len(ordered)), + ChargedFees: charged.Uint64(), + BurnedFees: burned.Uint64(), + ValidatorFees: validators.Uint64(), + ReserveFees: reserve.Uint64(), + FinalizedOperations: operations.Uint64(), + ResourceUsed: used.Uint64(), + ResourceCapacity: capacity.Uint64(), + CirculatingNativeSupply: circulating.Uint64(), + EscrowBackedComputeDemand: demand.Uint64(), + VerifiedComputeSupply: supply.Uint64(), + ComputeBacklog: backlog.Uint64(), + ComputeFulfilled: fulfilled.Uint64(), + } + if out.ResourceCapacity == 0 { + return EpochAggregate{}, ErrEpochMetrics + } + out.ResourceUtilizationBps = ratioBps(out.ResourceUsed, out.ResourceCapacity) + if out.CirculatingNativeSupply != 0 { + weightedVelocity.Quo(&weightedVelocity, new(big.Int).SetUint64(out.CirculatingNativeSupply)) + if !weightedVelocity.IsUint64() || weightedVelocity.Uint64() > uint64(10*BasisPoints) { + return EpochAggregate{}, ErrEpochMetrics + } + out.AgeWeightedVelocityBps = uint32(weightedVelocity.Uint64()) + } + return out, nil +} + +func (a EpochAggregate) MonetaryMetrics(totalSupply, stakedSupply, protocolReserve, computeIndexQ9 uint64, computePriceTrendBps int32, computeIndexReliable bool) (MonetaryMetrics, error) { + if totalSupply == 0 || a.CirculatingNativeSupply == 0 || a.CirculatingNativeSupply > totalSupply || stakedSupply > a.CirculatingNativeSupply || protocolReserve > totalSupply { + return MonetaryMetrics{}, ErrEpochMetrics + } + return MonetaryMetrics{ + Supply: totalSupply, + CirculatingSupply: a.CirculatingNativeSupply, + StakedSupply: stakedSupply, + ProtocolReserve: protocolReserve, + BurnedThisEpoch: a.BurnedFees, + FinalizedOperations: a.FinalizedOperations, + ResourceUtilizationBps: a.ResourceUtilizationBps, + AgeWeightedVelocityBps: a.AgeWeightedVelocityBps, + ComputeIndexQ9: computeIndexQ9, + ComputePriceTrendBps: computePriceTrendBps, + ComputeIndexReliable: computeIndexReliable, + }, nil +} + +func (a EpochAggregate) ComputeMarketMetrics(computePriceTrendBps int32, computeIndexReliable bool) ComputeMarketMetrics { + return ComputeMarketMetrics{ + EscrowBackedDemandUnits: a.EscrowBackedComputeDemand, + VerifiedSupplyUnits: a.VerifiedComputeSupply, + BacklogUnits: a.ComputeBacklog, + FulfilledUnits: a.ComputeFulfilled, + UtilizationBps: a.ResourceUtilizationBps, + ComputePriceTrendBps: computePriceTrendBps, + ComputeIndexReliable: computeIndexReliable, + } +} + +func addBig(target *big.Int, value uint64) { + target.Add(target, new(big.Int).SetUint64(value)) +} From 0189a83060d3af64fc3cab2b173381098f03bc54 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:42:08 +0200 Subject: [PATCH 186/274] Separate compute and chain utilization in epoch metrics --- internal/v2/economics/epoch.go | 41 +++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/internal/v2/economics/epoch.go b/internal/v2/economics/epoch.go index 8ec95911..8fd3b6cd 100644 --- a/internal/v2/economics/epoch.go +++ b/internal/v2/economics/epoch.go @@ -14,28 +14,29 @@ const EpochMetricsVersion uint16 = 1 var ErrEpochMetrics = errors.New("invalid Zephyr economic epoch metrics") type ShardEpochMetrics struct { - Version uint16 - Epoch uint64 - ShardID uint32 - ChargedFees uint64 - BurnedFees uint64 - ValidatorFees uint64 - ReserveFees uint64 - FinalizedOperations uint64 - ResourceUsed uint64 - ResourceCapacity uint64 - CirculatingNativeSupply uint64 - AgeWeightedVelocityBps uint32 - EscrowBackedComputeDemand uint64 - VerifiedComputeSupply uint64 - ComputeBacklog uint64 - ComputeFulfilled uint64 + Version uint16 + Epoch uint64 + ShardID uint32 + ChargedFees uint64 + BurnedFees uint64 + ValidatorFees uint64 + ReserveFees uint64 + FinalizedOperations uint64 + ResourceUsed uint64 + ResourceCapacity uint64 + CirculatingNativeSupply uint64 + AgeWeightedVelocityBps uint32 + EscrowBackedComputeDemand uint64 + VerifiedComputeSupply uint64 + ComputeBacklog uint64 + ComputeFulfilled uint64 } func (m ShardEpochMetrics) Validate() error { if m.Version != EpochMetricsVersion || m.Epoch == 0 || m.ResourceCapacity == 0 || m.ResourceUsed > m.ResourceCapacity || m.AgeWeightedVelocityBps > 10*BasisPoints || - m.ComputeBacklog > m.EscrowBackedComputeDemand || m.ComputeFulfilled > m.EscrowBackedComputeDemand { + m.ComputeFulfilled > m.VerifiedComputeSupply || m.ComputeFulfilled > m.EscrowBackedComputeDemand || + m.ComputeBacklog > m.EscrowBackedComputeDemand-m.ComputeFulfilled { return ErrEpochMetrics } feeTotal := new(big.Int).SetUint64(m.BurnedFees) @@ -96,6 +97,7 @@ type EpochAggregate struct { VerifiedComputeSupply uint64 ComputeBacklog uint64 ComputeFulfilled uint64 + ComputeUtilizationBps uint32 } func AggregateEpochMetrics(metrics []ShardEpochMetrics) (EpochAggregate, error) { @@ -152,6 +154,9 @@ func AggregateEpochMetrics(metrics []ShardEpochMetrics) (EpochAggregate, error) return EpochAggregate{}, ErrEpochMetrics } out.ResourceUtilizationBps = ratioBps(out.ResourceUsed, out.ResourceCapacity) + if out.VerifiedComputeSupply != 0 { + out.ComputeUtilizationBps = ratioBps(out.ComputeFulfilled, out.VerifiedComputeSupply) + } if out.CirculatingNativeSupply != 0 { weightedVelocity.Quo(&weightedVelocity, new(big.Int).SetUint64(out.CirculatingNativeSupply)) if !weightedVelocity.IsUint64() || weightedVelocity.Uint64() > uint64(10*BasisPoints) { @@ -187,7 +192,7 @@ func (a EpochAggregate) ComputeMarketMetrics(computePriceTrendBps int32, compute VerifiedSupplyUnits: a.VerifiedComputeSupply, BacklogUnits: a.ComputeBacklog, FulfilledUnits: a.ComputeFulfilled, - UtilizationBps: a.ResourceUtilizationBps, + UtilizationBps: a.ComputeUtilizationBps, ComputePriceTrendBps: computePriceTrendBps, ComputeIndexReliable: computeIndexReliable, } From 609fafaa6f5499fc3d6c57a27f933471f69a9cbc Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:42:52 +0200 Subject: [PATCH 187/274] Add economic epoch aggregation test scaffold --- internal/v2/economics/epoch_test.go | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 internal/v2/economics/epoch_test.go diff --git a/internal/v2/economics/epoch_test.go b/internal/v2/economics/epoch_test.go new file mode 100644 index 00000000..aaefbf51 --- /dev/null +++ b/internal/v2/economics/epoch_test.go @@ -0,0 +1,5 @@ +package economics + +import "testing" + +func TestEpochAggregationPlaceholder(t *testing.T) {} From 0bf979a39a4e652a605e9ee1af5b617dd893cd29 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:43:24 +0200 Subject: [PATCH 188/274] Test economic epoch aggregation --- internal/v2/economics/epoch_test.go | 84 ++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/internal/v2/economics/epoch_test.go b/internal/v2/economics/epoch_test.go index aaefbf51..526e41a1 100644 --- a/internal/v2/economics/epoch_test.go +++ b/internal/v2/economics/epoch_test.go @@ -2,4 +2,86 @@ package economics import "testing" -func TestEpochAggregationPlaceholder(t *testing.T) {} +func TestAggregateEpochMetricsSeparatesChainAndComputeUtilization(t *testing.T) { + metrics := []ShardEpochMetrics{ + { + Version: EpochMetricsVersion, Epoch: 7, ShardID: 0, + ChargedFees: 100, BurnedFees: 40, ValidatorFees: 50, ReserveFees: 10, + FinalizedOperations: 1_000, ResourceUsed: 50, ResourceCapacity: 100, + CirculatingNativeSupply: 800, AgeWeightedVelocityBps: 2_000, + EscrowBackedComputeDemand: 100, VerifiedComputeSupply: 80, ComputeBacklog: 20, ComputeFulfilled: 70, + }, + { + Version: EpochMetricsVersion, Epoch: 7, ShardID: 1, + ChargedFees: 50, BurnedFees: 20, ValidatorFees: 25, ReserveFees: 5, + FinalizedOperations: 500, ResourceUsed: 10, ResourceCapacity: 100, + CirculatingNativeSupply: 200, AgeWeightedVelocityBps: 8_000, + EscrowBackedComputeDemand: 50, VerifiedComputeSupply: 40, ComputeBacklog: 10, ComputeFulfilled: 30, + }, + } + aggregate, err := AggregateEpochMetrics(metrics) + if err != nil { + t.Fatal(err) + } + if aggregate.ChargedFees != 150 || aggregate.BurnedFees != 60 || aggregate.FinalizedOperations != 1_500 { + t.Fatalf("unexpected totals: %#v", aggregate) + } + if aggregate.ResourceUtilizationBps != 3_000 { + t.Fatalf("chain utilization should be 3000 bps, got %d", aggregate.ResourceUtilizationBps) + } + if aggregate.ComputeUtilizationBps != 8_333 { + t.Fatalf("compute utilization should be based on fulfilled/capacity, got %d", aggregate.ComputeUtilizationBps) + } + if aggregate.AgeWeightedVelocityBps != 3_200 { + t.Fatalf("velocity must be supply-weighted across shards, got %d", aggregate.AgeWeightedVelocityBps) + } + market := aggregate.ComputeMarketMetrics(500, true) + if market.UtilizationBps != aggregate.ComputeUtilizationBps || market.EscrowBackedDemandUnits != 150 || market.VerifiedSupplyUnits != 120 { + t.Fatalf("unexpected compute market projection: %#v", market) + } +} + +func TestShardEpochMetricsRejectsInconsistentAccounting(t *testing.T) { + base := ShardEpochMetrics{ + Version: EpochMetricsVersion, Epoch: 1, ResourceCapacity: 100, + ChargedFees: 10, BurnedFees: 4, ValidatorFees: 5, ReserveFees: 1, + EscrowBackedComputeDemand: 100, VerifiedComputeSupply: 100, ComputeFulfilled: 60, ComputeBacklog: 40, + } + if err := base.Validate(); err != nil { + t.Fatal(err) + } + badFees := base + badFees.ReserveFees = 2 + if err := badFees.Validate(); err != ErrEpochMetrics { + t.Fatalf("fee accounting mismatch accepted: %v", err) + } + badDemand := base + badDemand.ComputeBacklog = 41 + if err := badDemand.Validate(); err != ErrEpochMetrics { + t.Fatalf("overlapping backlog/fulfilled demand accepted: %v", err) + } +} + +func TestAggregateEpochMetricsRejectsDuplicateShard(t *testing.T) { + base := ShardEpochMetrics{ + Version: EpochMetricsVersion, Epoch: 1, ShardID: 0, + ResourceCapacity: 1, ChargedFees: 1, BurnedFees: 1, + } + if _, err := AggregateEpochMetrics([]ShardEpochMetrics{base, base}); err != ErrEpochMetrics { + t.Fatalf("duplicate shard accepted: %v", err) + } +} + +func TestEpochAggregateBuildsZAMPInputs(t *testing.T) { + aggregate := EpochAggregate{ + Epoch: 1, ShardCount: 1, BurnedFees: 123, FinalizedOperations: 100, + ResourceUtilizationBps: 4_000, CirculatingNativeSupply: 900, AgeWeightedVelocityBps: 3_000, + } + metrics, err := aggregate.MonetaryMetrics(1_000, 400, 50, 7_500_000_000, 100, true) + if err != nil { + t.Fatal(err) + } + if metrics.BurnedThisEpoch != 123 || metrics.StakedSupply != 400 || metrics.AgeWeightedVelocityBps != 3_000 || metrics.ComputeIndexQ9 != 7_500_000_000 { + t.Fatalf("unexpected monetary projection: %#v", metrics) + } +} From b7efb0dedcc27ea631e94495d70629cfc30bc905 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:45:36 +0200 Subject: [PATCH 189/274] Commit canonical epoch economic aggregate --- internal/v2/economics/epoch_wire.go | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 internal/v2/economics/epoch_wire.go diff --git a/internal/v2/economics/epoch_wire.go b/internal/v2/economics/epoch_wire.go new file mode 100644 index 00000000..0c62cca0 --- /dev/null +++ b/internal/v2/economics/epoch_wire.go @@ -0,0 +1,46 @@ +package economics + +import ( + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func (a EpochAggregate) CanonicalBytes() ([]byte, error) { + if a.Epoch == 0 || a.ShardCount == 0 || a.ResourceCapacity == 0 || a.ResourceUsed > a.ResourceCapacity || + a.ResourceUtilizationBps > BasisPoints || a.ComputeUtilizationBps > BasisPoints || a.AgeWeightedVelocityBps > 10*BasisPoints || + a.ComputeFulfilled > a.VerifiedComputeSupply || a.ComputeFulfilled > a.EscrowBackedComputeDemand || + a.ComputeBacklog > a.EscrowBackedComputeDemand-a.ComputeFulfilled { + return nil, ErrEpochMetrics + } + if a.BurnedFees > a.ChargedFees || a.ValidatorFees > a.ChargedFees-a.BurnedFees || + a.ReserveFees != a.ChargedFees-a.BurnedFees-a.ValidatorFees { + return nil, ErrEpochMetrics + } + var w codec.Writer + w.U64(a.Epoch) + w.U32(a.ShardCount) + w.U64(a.ChargedFees) + w.U64(a.BurnedFees) + w.U64(a.ValidatorFees) + w.U64(a.ReserveFees) + w.U64(a.FinalizedOperations) + w.U64(a.ResourceUsed) + w.U64(a.ResourceCapacity) + w.U32(a.ResourceUtilizationBps) + w.U64(a.CirculatingNativeSupply) + w.U32(a.AgeWeightedVelocityBps) + w.U64(a.EscrowBackedComputeDemand) + w.U64(a.VerifiedComputeSupply) + w.U64(a.ComputeBacklog) + w.U64(a.ComputeFulfilled) + w.U32(a.ComputeUtilizationBps) + return w.BytesCopy(), nil +} + +func (a EpochAggregate) Hash() (types.Hash, error) { + raw, err := a.CanonicalBytes() + if err != nil { + return types.Hash{}, err + } + return types.Hash(codec.DomainHash("zephyr/epoch-economics/v2", raw)), nil +} From 468ff363411238ca8fe0f38a02f823f35f8888a1 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:46:20 +0200 Subject: [PATCH 190/274] Add authenticated shadow monetary epoch state --- internal/v2/economics/monetary_state.go | 300 ++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 internal/v2/economics/monetary_state.go diff --git a/internal/v2/economics/monetary_state.go b/internal/v2/economics/monetary_state.go new file mode 100644 index 00000000..39b086a9 --- /dev/null +++ b/internal/v2/economics/monetary_state.go @@ -0,0 +1,300 @@ +package economics + +import ( + "encoding/binary" + "errors" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const MonetaryEpochStateVersion uint16 = 1 + +var ErrMonetaryState = errors.New("invalid Zephyr shadow monetary epoch state") + +type MonetaryEpochState struct { + Version uint16 + Network types.NetworkID + Epoch uint64 + Shadow bool + TotalSupply uint64 + CirculatingSupply uint64 + StakedSupply uint64 + ProtocolReserve uint64 + ZAMPBaseTargetBps uint32 + SuggestedTargetBps uint32 + BaseFee uint64 + AggregateHash types.Hash + ComputeIndexQ9 uint64 + ComputeIndexReliable bool + ComputeScarcityMagnitudeBps uint32 + ComputeScarcityNegative bool + ComputeScarcityReliable bool + FeedbackMode ComputeFeedbackMode + ShadowGrossMintTarget uint64 + ShadowComputeIncentiveMint uint64 + InflationCorrectionMagnitudeBps uint32 + InflationCorrectionNegative bool + PreviousStateHash types.Hash +} + +func (s MonetaryEpochState) Validate() error { + if s.Version != MonetaryEpochStateVersion || types.IsZero32([32]byte(s.Network)) || s.Epoch == 0 || !s.Shadow || + s.TotalSupply == 0 || s.CirculatingSupply == 0 || s.CirculatingSupply > s.TotalSupply || + s.StakedSupply > s.CirculatingSupply || s.ProtocolReserve > s.TotalSupply || + s.ZAMPBaseTargetBps > BasisPoints || s.SuggestedTargetBps > BasisPoints || + types.IsZero32([32]byte(s.AggregateHash)) || s.ComputeScarcityMagnitudeBps > BasisPoints || + s.FeedbackMode > ComputeFeedbackMonetaryBand || s.InflationCorrectionMagnitudeBps > BasisPoints { + return ErrMonetaryState + } + return nil +} + +func (s MonetaryEpochState) CanonicalBytes() ([]byte, error) { + if err := s.Validate(); err != nil { + return nil, err + } + var w codec.Writer + w.U16(s.Version) + w.Fixed(s.Network[:]) + w.U64(s.Epoch) + w.Bool(s.Shadow) + w.U64(s.TotalSupply) + w.U64(s.CirculatingSupply) + w.U64(s.StakedSupply) + w.U64(s.ProtocolReserve) + w.U32(s.ZAMPBaseTargetBps) + w.U32(s.SuggestedTargetBps) + w.U64(s.BaseFee) + w.Fixed(s.AggregateHash[:]) + w.U64(s.ComputeIndexQ9) + w.Bool(s.ComputeIndexReliable) + w.U32(s.ComputeScarcityMagnitudeBps) + w.Bool(s.ComputeScarcityNegative) + w.Bool(s.ComputeScarcityReliable) + w.U8(uint8(s.FeedbackMode)) + w.U64(s.ShadowGrossMintTarget) + w.U64(s.ShadowComputeIncentiveMint) + w.U32(s.InflationCorrectionMagnitudeBps) + w.Bool(s.InflationCorrectionNegative) + w.Fixed(s.PreviousStateHash[:]) + return w.BytesCopy(), nil +} + +func ParseMonetaryEpochState(data []byte) (MonetaryEpochState, error) { + r := codec.NewReader(data) + version, err := r.U16() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + networkRaw, err := r.Fixed(32) + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + epoch, err := r.U64() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + shadow, err := r.Bool() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + totalSupply, err := r.U64() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + circulating, err := r.U64() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + staked, err := r.U64() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + reserve, err := r.U64() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + baseTarget, err := r.U32() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + suggestedTarget, err := r.U32() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + baseFee, err := r.U64() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + aggregateRaw, err := r.Fixed(32) + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + computeIndex, err := r.U64() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + computeIndexReliable, err := r.Bool() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + scarcityMagnitude, err := r.U32() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + scarcityNegative, err := r.Bool() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + scarcityReliable, err := r.Bool() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + mode, err := r.U8() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + grossMint, err := r.U64() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + computeMint, err := r.U64() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + correctionMagnitude, err := r.U32() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + correctionNegative, err := r.Bool() + if err != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + previousRaw, err := r.Fixed(32) + if err != nil || r.Done() != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + var network types.NetworkID + var aggregate, previous types.Hash + copy(network[:], networkRaw) + copy(aggregate[:], aggregateRaw) + copy(previous[:], previousRaw) + out := MonetaryEpochState{ + Version: MonetaryEpochStateVersion, Network: network, Epoch: epoch, Shadow: shadow, + TotalSupply: totalSupply, CirculatingSupply: circulating, StakedSupply: staked, ProtocolReserve: reserve, + ZAMPBaseTargetBps: baseTarget, SuggestedTargetBps: suggestedTarget, BaseFee: baseFee, + AggregateHash: aggregate, ComputeIndexQ9: computeIndex, ComputeIndexReliable: computeIndexReliable, + ComputeScarcityMagnitudeBps: scarcityMagnitude, ComputeScarcityNegative: scarcityNegative, + ComputeScarcityReliable: scarcityReliable, FeedbackMode: ComputeFeedbackMode(mode), + ShadowGrossMintTarget: grossMint, ShadowComputeIncentiveMint: computeMint, + InflationCorrectionMagnitudeBps: correctionMagnitude, InflationCorrectionNegative: correctionNegative, + PreviousStateHash: previous, + } + if version != MonetaryEpochStateVersion || out.Validate() != nil { + return MonetaryEpochState{}, ErrMonetaryState + } + return out, nil +} + +func (s MonetaryEpochState) Hash() (types.Hash, error) { + raw, err := s.CanonicalBytes() + if err != nil { + return types.Hash{}, err + } + return types.Hash(codec.DomainHash("zephyr/monetary-epoch-state/v2", raw)), nil +} + +func MonetaryStateObjectID(network types.NetworkID) types.ObjectID { + var w codec.Writer + w.Fixed(network[:]) + w.String("monetary-epoch") + hash := codec.DomainHash("zephyr/system-object-id/v2", w.BytesCopy()) + binary.BigEndian.PutUint32(hash[:4], 0) + return types.ObjectID(hash) +} + +func (s MonetaryEpochState) Object() (object.Object, error) { + raw, err := s.CanonicalBytes() + if err != nil { + return object.Object{}, err + } + return object.Object{ + ID: MonetaryStateObjectID(s.Network), Version: s.Epoch, + Kind: object.KindSystem, Data: raw, + }, nil +} + +func BuildShadowMonetaryEpochState( + network types.NetworkID, + previous *MonetaryEpochState, + aggregate EpochAggregate, + totalSupply uint64, + stakedSupply uint64, + protocolReserve uint64, + computeIndexQ9 uint64, + computePriceTrendBps int32, + computeIndexReliable bool, + scarcity ComputeScarcitySnapshot, + monetaryPolicy MonetaryPolicy, + feedbackPolicy ComputeFeedbackPolicy, + baseFee uint64, +) (MonetaryEpochState, MonetaryDecision, ComputeFeedbackDecision, error) { + if types.IsZero32([32]byte(network)) || scarcity.Epoch != aggregate.Epoch { + return MonetaryEpochState{}, MonetaryDecision{}, ComputeFeedbackDecision{}, ErrMonetaryState + } + aggregateHash, err := aggregate.Hash() + if err != nil { + return MonetaryEpochState{}, MonetaryDecision{}, ComputeFeedbackDecision{}, err + } + metrics, err := aggregate.MonetaryMetrics(totalSupply, stakedSupply, protocolReserve, computeIndexQ9, computePriceTrendBps, computeIndexReliable) + if err != nil { + return MonetaryEpochState{}, MonetaryDecision{}, ComputeFeedbackDecision{}, err + } + priorTarget := monetaryPolicy.TargetInflationBps + var previousHash types.Hash + if previous != nil { + if previous.Network != network || previous.Epoch+1 != aggregate.Epoch || previous.Validate() != nil { + return MonetaryEpochState{}, MonetaryDecision{}, ComputeFeedbackDecision{}, ErrMonetaryState + } + priorTarget = previous.ZAMPBaseTargetBps + previousHash, err = previous.Hash() + if err != nil { + return MonetaryEpochState{}, MonetaryDecision{}, ComputeFeedbackDecision{}, err + } + } + monetaryDecision, err := EvaluateShadow(priorTarget, metrics, monetaryPolicy) + if err != nil { + return MonetaryEpochState{}, MonetaryDecision{}, ComputeFeedbackDecision{}, err + } + feedback, err := EvaluateComputeFeedback(monetaryDecision, metrics, monetaryPolicy, scarcity, feedbackPolicy) + if err != nil { + return MonetaryEpochState{}, MonetaryDecision{}, ComputeFeedbackDecision{}, err + } + scarcityMagnitude, scarcityNegative := signedMagnitude(scarcity.ScoreBps) + correctionMagnitude, correctionNegative := signedMagnitude(feedback.InflationCorrectionBps) + state := MonetaryEpochState{ + Version: MonetaryEpochStateVersion, Network: network, Epoch: aggregate.Epoch, Shadow: true, + TotalSupply: totalSupply, CirculatingSupply: aggregate.CirculatingNativeSupply, + StakedSupply: stakedSupply, ProtocolReserve: protocolReserve, + ZAMPBaseTargetBps: monetaryDecision.TargetInflationBps, SuggestedTargetBps: feedback.SuggestedTargetInflationBps, + BaseFee: baseFee, AggregateHash: aggregateHash, ComputeIndexQ9: computeIndexQ9, ComputeIndexReliable: computeIndexReliable, + ComputeScarcityMagnitudeBps: scarcityMagnitude, ComputeScarcityNegative: scarcityNegative, + ComputeScarcityReliable: scarcity.Reliable, FeedbackMode: feedbackPolicy.Mode, + ShadowGrossMintTarget: feedback.SuggestedGrossMint, ShadowComputeIncentiveMint: feedback.SuggestedComputeIncentiveMint, + InflationCorrectionMagnitudeBps: correctionMagnitude, InflationCorrectionNegative: correctionNegative, + PreviousStateHash: previousHash, + } + if err := state.Validate(); err != nil { + return MonetaryEpochState{}, MonetaryDecision{}, ComputeFeedbackDecision{}, err + } + return state, monetaryDecision, feedback, nil +} + +func signedMagnitude(value int32) (uint32, bool) { + if value < 0 { + return uint32(-int64(value)), true + } + return uint32(value), false +} From 136f1e1ce2200da2c7244eb59d2281f2db3841ef Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:46:45 +0200 Subject: [PATCH 191/274] Test authenticated shadow monetary epoch state --- internal/v2/economics/monetary_state_test.go | 104 +++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 internal/v2/economics/monetary_state_test.go diff --git a/internal/v2/economics/monetary_state_test.go b/internal/v2/economics/monetary_state_test.go new file mode 100644 index 00000000..a19ee3b0 --- /dev/null +++ b/internal/v2/economics/monetary_state_test.go @@ -0,0 +1,104 @@ +package economics + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestShadowMonetaryEpochStateRoundTripAndNoLiveMint(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("economics-state"))) + aggregate := EpochAggregate{ + Epoch: 1, ShardCount: 1, + ChargedFees: 100, BurnedFees: 100, FinalizedOperations: 1_000, + ResourceUsed: 50, ResourceCapacity: 100, ResourceUtilizationBps: 5_000, + CirculatingNativeSupply: 900_000_000, AgeWeightedVelocityBps: 5_000, + EscrowBackedComputeDemand: 2_000, VerifiedComputeSupply: 1_000, + ComputeBacklog: 500, ComputeFulfilled: 1_000, ComputeUtilizationBps: 10_000, + } + scarcity, err := BuildComputeScarcity(1, aggregate.ComputeMarketMetrics(1_000, true), DefaultComputeScarcityConfig()) + if err != nil { + t.Fatal(err) + } + monetary := DefaultShadowPolicy() + feedbackPolicy := DefaultComputeFeedbackPolicy(ComputeFeedbackMonetaryBand) + state, base, feedback, err := BuildShadowMonetaryEpochState( + network, nil, aggregate, 1_000_000_000, 450_000_000, 100_000_000, + 7_500_000_000, 1_000, true, scarcity, monetary, feedbackPolicy, 10, + ) + if err != nil { + t.Fatal(err) + } + if !state.Shadow || state.TotalSupply != 1_000_000_000 || state.ShadowGrossMintTarget == 0 { + t.Fatalf("unexpected shadow state: %#v", state) + } + if feedback.SuggestedGrossMint != state.ShadowGrossMintTarget || base.ProjectedNetChange == 0 { + t.Fatalf("shadow decisions were not committed correctly: %#v %#v", base, feedback) + } + raw, err := state.CanonicalBytes() + if err != nil { + t.Fatal(err) + } + parsed, err := ParseMonetaryEpochState(raw) + if err != nil { + t.Fatal(err) + } + if parsed != state { + t.Fatalf("monetary state round trip mismatch: %#v != %#v", parsed, state) + } + obj, err := state.Object() + if err != nil { + t.Fatal(err) + } + if obj.ID != MonetaryStateObjectID(network) || obj.Version != state.Epoch { + t.Fatalf("unexpected monetary system object: %#v", obj) + } +} + +func TestShadowMonetaryEpochStateChainsPreviousEpoch(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("economics-chain"))) + policy := DefaultShadowPolicy() + feedbackPolicy := DefaultComputeFeedbackPolicy(ComputeFeedbackObserveOnly) + makeAggregate := func(epoch uint64) EpochAggregate { + return EpochAggregate{ + Epoch: epoch, ShardCount: 1, ChargedFees: 10, BurnedFees: 10, + FinalizedOperations: 100, ResourceUsed: 50, ResourceCapacity: 100, ResourceUtilizationBps: 5_000, + CirculatingNativeSupply: 900_000_000, AgeWeightedVelocityBps: 5_000, + VerifiedComputeSupply: 1_000, ComputeFulfilled: 700, ComputeUtilizationBps: 7_000, + } + } + firstAggregate := makeAggregate(1) + firstScarcity, err := BuildComputeScarcity(1, firstAggregate.ComputeMarketMetrics(0, false), DefaultComputeScarcityConfig()) + if err != nil { + t.Fatal(err) + } + first, _, _, err := BuildShadowMonetaryEpochState(network, nil, firstAggregate, 1_000_000_000, 450_000_000, 100_000_000, 0, 0, false, firstScarcity, policy, feedbackPolicy, 10) + if err != nil { + t.Fatal(err) + } + secondAggregate := makeAggregate(2) + secondScarcity, err := BuildComputeScarcity(2, secondAggregate.ComputeMarketMetrics(0, false), DefaultComputeScarcityConfig()) + if err != nil { + t.Fatal(err) + } + second, _, _, err := BuildShadowMonetaryEpochState(network, &first, secondAggregate, 1_000_000_000, 450_000_000, 100_000_000, 0, 0, false, secondScarcity, policy, feedbackPolicy, 10) + if err != nil { + t.Fatal(err) + } + firstHash, err := first.Hash() + if err != nil { + t.Fatal(err) + } + if second.PreviousStateHash != firstHash { + t.Fatal("monetary epoch state did not bind previous state") + } + + thirdAggregate := makeAggregate(4) + thirdScarcity, err := BuildComputeScarcity(4, thirdAggregate.ComputeMarketMetrics(0, false), DefaultComputeScarcityConfig()) + if err != nil { + t.Fatal(err) + } + if _, _, _, err := BuildShadowMonetaryEpochState(network, &second, thirdAggregate, 1_000_000_000, 450_000_000, 100_000_000, 0, 0, false, thirdScarcity, policy, feedbackPolicy, 10); err != ErrMonetaryState { + t.Fatalf("skipped epoch accepted: %v", err) + } +} From 4dcd789a2dbd039ca262500ff5570a6d8d8250db Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:47:17 +0200 Subject: [PATCH 192/274] Fix monetary epoch compute fixture --- internal/v2/economics/monetary_state_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/v2/economics/monetary_state_test.go b/internal/v2/economics/monetary_state_test.go index a19ee3b0..7c9c2eb7 100644 --- a/internal/v2/economics/monetary_state_test.go +++ b/internal/v2/economics/monetary_state_test.go @@ -64,7 +64,8 @@ func TestShadowMonetaryEpochStateChainsPreviousEpoch(t *testing.T) { Epoch: epoch, ShardCount: 1, ChargedFees: 10, BurnedFees: 10, FinalizedOperations: 100, ResourceUsed: 50, ResourceCapacity: 100, ResourceUtilizationBps: 5_000, CirculatingNativeSupply: 900_000_000, AgeWeightedVelocityBps: 5_000, - VerifiedComputeSupply: 1_000, ComputeFulfilled: 700, ComputeUtilizationBps: 7_000, + EscrowBackedComputeDemand: 1_000, VerifiedComputeSupply: 1_000, + ComputeFulfilled: 700, ComputeUtilizationBps: 7_000, } } firstAggregate := makeAggregate(1) From 3800b2a051ca2537e22d9d1b7babf74148ced944 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:48:45 +0200 Subject: [PATCH 193/274] Document authenticated economic epoch state --- docs/economic-state-v2.md | 308 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 docs/economic-state-v2.md diff --git a/docs/economic-state-v2.md b/docs/economic-state-v2.md new file mode 100644 index 00000000..672cf1e4 --- /dev/null +++ b/docs/economic-state-v2.md @@ -0,0 +1,308 @@ +# Zephyr v2 Economic State and Epoch Accounting + +Status: **shadow/experimental protocol foundation**. + +This document defines how Zephyr can make ZAMP, ZCPI and ZCSI inputs reproducible from finalized chain state without creating a global per-transaction bottleneck. + +It complements: + +- `docs/tokenomics-v2.md` +- `docs/compute-economics-v2.md` +- `docs/protocol-v2.md` + +The current implementation does **not** activate live ZPH minting. + +## 1. No global monetary hot object per transaction + +A single monetary object consumed by every transaction would serialize execution and work against Zephyr's parallel/object/sharded architecture. + +Zephyr therefore separates: + +```text +per-transaction execution + -> per-shard block/epoch telemetry + -> canonical ShardEpochMetrics + -> epoch aggregation + -> shadow MonetaryEpochState +``` + +Only the epoch boundary needs a global monetary-state transition. + +## 2. Consensus-stamped coin age + +The v2 `Coin` object includes: + +```text +Token +Amount +CreatedHeight +``` + +`CreatedHeight` is not trusted from the wallet. During execution, every newly created coin output is rewritten with the candidate block height by the deterministic executor. + +This applies to: + +- native transfers; +- native fee/change outputs; +- token creation; +- custom-token mint; +- custom-token burn change; +- cross-shard outbound coin outputs. + +A wallet may place any height in its proposed output bytes, but the executed object uses the consensus execution height. + +Height `0` is reserved for genesis/unknown-age data and is excluded from age-weighted velocity until a policy explicitly defines otherwise. + +## 3. Age-weighted velocity + +Naive on-chain transfer volume is easy to manipulate by cycling the same funds repeatedly. + +The reference velocity accumulator instead weights a finalized spend by the age of the consumed coin: + +```text +age = spendHeight - CreatedHeight +``` + +with configurable: + +```text +MinAgeBlocks +FullWeightAgeBlocks +MaxVelocityBps +``` + +For age below `MinAgeBlocks`, the contribution is zero. + +Between minimum age and full-weight age, contribution increases with age. + +Above full-weight age, the contribution saturates. + +Conceptually: + +```text +contribution = amount * boundedAgeWeight +``` + +The epoch value is normalized by circulating native supply. + +The accumulator uses arbitrary-precision intermediate arithmetic and only converts to bounded consensus values at finalization. + +### Rapid self-cycling + +If a coin is spent and immediately recreated, the new output receives the current consensus height. Repeated rapid cycling therefore keeps resetting coin age and cannot repeatedly receive full velocity weight. + +This does not make manipulation impossible; it makes the attacker sacrifice time/capital lockup rather than obtaining free volume by fast self-transfers. + +## 4. Per-shard economic metrics + +Each shard can produce a canonical `ShardEpochMetrics` record containing: + +```text +Version +Epoch +ShardID +ChargedFees +BurnedFees +ValidatorFees +ReserveFees +FinalizedOperations +ResourceUsed +ResourceCapacity +CirculatingNativeSupply +AgeWeightedVelocityBps +EscrowBackedComputeDemand +VerifiedComputeSupply +ComputeBacklog +ComputeFulfilled +``` + +Validation requires exact fee conservation: + +```text +ChargedFees = BurnedFees + ValidatorFees + ReserveFees +``` + +and rejects impossible compute accounting such as fulfilled work above verified capacity or backlog/fulfilled units that overlap beyond funded demand. + +Each record has canonical binary bytes and a domain-separated hash. + +## 5. Epoch aggregation + +`AggregateEpochMetrics` combines unique shard records for the same epoch. + +The aggregation is deterministic and overflow-safe. + +It calculates two distinct utilization metrics: + +### Chain resource utilization + +```text +sum(ResourceUsed) / sum(ResourceCapacity) +``` + +This is a ZAMP/network signal. + +### Compute utilization + +```text +sum(ComputeFulfilled) / sum(VerifiedComputeSupply) +``` + +This is a ZCSI/compute-market signal. + +They are deliberately separate. High blockchain congestion is not evidence of GPU scarcity, and idle blockspace is not evidence of excess compute capacity. + +## 6. Multi-shard velocity weighting + +Shard velocity is not averaged equally across shards. + +The global epoch velocity is weighted by the native circulating supply represented by each shard: + +```text +GlobalVelocity = + sum(ShardCirculatingSupply * ShardVelocity) + / sum(ShardCirculatingSupply) +``` + +A tiny shard therefore cannot move global monetary telemetry as much as a shard holding a large fraction of circulating ZPH merely by reporting an extreme velocity value. + +## 7. Canonical epoch commitment + +The aggregated economic record has canonical bytes and a domain-separated `EpochAggregate.Hash()`. + +This hash is the bridge between high-throughput per-shard accounting and the global epoch monetary state. + +The intended future finality path is: + +```text +finalized shard metrics + -> epoch aggregate hash + -> MonetaryEpochState + -> state root / global finality +``` + +The exact placement of the economics commitment in the final public `GlobalHeader` remains an activation decision and must not be changed without wallet/light-client conformance vectors. + +## 8. Shadow MonetaryEpochState + +The reference implementation defines a deterministic `MonetaryEpochState` system object. + +It records: + +```text +Network +Epoch +Shadow = true +TotalSupply +CirculatingSupply +StakedSupply +ProtocolReserve +ZAMPBaseTargetBps +SuggestedTargetBps +BaseFee +AggregateHash +ComputeIndexQ9 +ComputeIndexReliable +ComputeScarcity score + reliability +FeedbackMode +ShadowGrossMintTarget +ShadowComputeIncentiveMint +InflationCorrection +PreviousStateHash +``` + +The system-object ID is deterministic for the network and the object version equals the epoch. + +## 9. Shadow means no issuance side effect + +`BuildShadowMonetaryEpochState` evaluates ZAMP and optional ZCSI feedback but does not mutate supply. + +In Mode C it may record, for example: + +```text +SuggestedTargetBps = 213 +ShadowGrossMintTarget = X +``` + +while still recording: + +```text +TotalSupply = observed pre-transition supply +Shadow = true +``` + +No transaction/executor path mints those suggested ZPH. + +A future activation must introduce a separate, explicit consensus transition and activation height/version. Shadow records are not authorization to mint. + +## 10. Epoch state chain + +Every state after the first can commit: + +```text +PreviousStateHash +``` + +The builder rejects a previous state from another network or a skipped/non-consecutive epoch. + +This enables a Citizen Node or audit tool to replay the economic-controller history rather than trusting a current RPC's summary. + +## 11. ZCPI/ZCSI feedback modes + +The economic state can record the same three simulation modes defined in `docs/compute-economics-v2.md`: + +```text +A: observe only +B: adjust compute reward routing only +C: adjust reward routing + narrow shadow inflation band +``` + +Mode B remains the preferred first candidate if devnet evidence eventually supports activation. + +Mode C remains experimental. + +## 12. Still required before live economics + +This foundation does not yet complete: + +- authenticated production derivation of verified compute capacity; +- final fee split and resource price constants/controller activation; +- live validator reward distribution; +- live protocol reserve credit/debit transitions; +- live ZPH mint/burn monetary transition; +- governance bounds and delayed parameter activation; +- economics commitment in the final light-client/global-header contract; +- long-run epoch replay datasets; +- adversarial money-velocity calibration; +- Citizen wallet UI for economic-state verification; +- economic recovery rules when compute telemetry is incomplete. + +Until those gates pass: + +```text +ZAMP = shadow +ZCSI feedback = shadow +suggested mint != minted supply +``` + +## 13. Testing strategy + +The repository tests should continuously assert: + +- wallet-provided coin age cannot survive execution unchanged; +- rapid fresh-coin cycling has little/zero velocity weight under the configured minimum age; +- old circulating coins receive higher bounded weight; +- per-shard fee accounting conserves every atomic ZPH unit; +- chain utilization and compute utilization are not conflated; +- shard velocity aggregation is supply-weighted; +- duplicate shard metrics are rejected; +- monetary epoch state round-trips canonically; +- epoch state binds the prior epoch hash; +- skipped epochs are rejected; +- Mode C can produce a suggestion without mutating `TotalSupply`. + +The engineering rule remains: + +```text +measure first -> simulate second -> activate last +``` From 8221a1e4037c04f18c2dceb6f5527837aa3fb995 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:49:58 +0200 Subject: [PATCH 194/274] Add QC-safe shadow monetary state transition --- internal/v2/economics/monetary_transition.go | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 internal/v2/economics/monetary_transition.go diff --git a/internal/v2/economics/monetary_transition.go b/internal/v2/economics/monetary_transition.go new file mode 100644 index 00000000..7526c073 --- /dev/null +++ b/internal/v2/economics/monetary_transition.go @@ -0,0 +1,37 @@ +package economics + +import ( + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +// ShadowMonetaryTransition returns the object delta for an epoch transition. +// It does not mutate a store. The caller may include this delta in candidate +// state simulation and apply it only after normal consensus finality. +func ShadowMonetaryTransition(previous *object.Object, next MonetaryEpochState) ([]types.ObjectID, []object.Object, error) { + if err := next.Validate(); err != nil { + return nil, nil, err + } + nextObject, err := next.Object() + if err != nil { + return nil, nil, err + } + if previous == nil { + if next.Epoch != 1 || !types.IsZero32([32]byte(next.PreviousStateHash)) { + return nil, nil, ErrMonetaryState + } + return nil, []object.Object{nextObject}, nil + } + if previous.ID != nextObject.ID || previous.Kind != object.KindSystem { + return nil, nil, ErrMonetaryState + } + priorState, err := ParseMonetaryEpochState(previous.Data) + if err != nil || priorState.Network != next.Network || priorState.Epoch+1 != next.Epoch || previous.Version != priorState.Epoch { + return nil, nil, ErrMonetaryState + } + priorHash, err := priorState.Hash() + if err != nil || next.PreviousStateHash != priorHash { + return nil, nil, ErrMonetaryState + } + return []types.ObjectID{previous.ID}, []object.Object{nextObject}, nil +} From 521bfaf4b7ae9a66604ec1ca1135ccc9c5b4e24c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:50:23 +0200 Subject: [PATCH 195/274] Test shadow monetary transition through state root --- .../v2/economics/monetary_transition_test.go | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 internal/v2/economics/monetary_transition_test.go diff --git a/internal/v2/economics/monetary_transition_test.go b/internal/v2/economics/monetary_transition_test.go new file mode 100644 index 00000000..e9af8f7b --- /dev/null +++ b/internal/v2/economics/monetary_transition_test.go @@ -0,0 +1,70 @@ +package economics + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestShadowMonetaryTransitionCanBeFinalizedThroughStateRoot(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("monetary-transition"))) + policy := DefaultShadowPolicy() + feedback := DefaultComputeFeedbackPolicy(ComputeFeedbackObserveOnly) + makeEpoch := func(epoch uint64, previous *MonetaryEpochState) MonetaryEpochState { + aggregate := EpochAggregate{ + Epoch: epoch, ShardCount: 1, ChargedFees: 10, BurnedFees: 10, + FinalizedOperations: 100, ResourceUsed: 50, ResourceCapacity: 100, ResourceUtilizationBps: 5_000, + CirculatingNativeSupply: 900_000_000, AgeWeightedVelocityBps: 4_000, + EscrowBackedComputeDemand: 1_000, VerifiedComputeSupply: 1_000, ComputeFulfilled: 700, ComputeUtilizationBps: 7_000, + } + scarcity, err := BuildComputeScarcity(epoch, aggregate.ComputeMarketMetrics(0, false), DefaultComputeScarcityConfig()) + if err != nil { + t.Fatal(err) + } + state, _, _, err := BuildShadowMonetaryEpochState(network, previous, aggregate, 1_000_000_000, 450_000_000, 100_000_000, 0, 0, false, scarcity, policy, feedback, 10) + if err != nil { + t.Fatal(err) + } + return state + } + + store := worldstate.NewMemory() + first := makeEpoch(1, nil) + consumed, created, err := ShadowMonetaryTransition(nil, first) + if err != nil { + t.Fatal(err) + } + root1, err := store.Apply(consumed, created) + if err != nil { + t.Fatal(err) + } + firstObject, proof, ok := store.Proof(MonetaryStateObjectID(network)) + if !ok || firstObject.Version != 1 { + t.Fatal("first monetary state not committed") + } + if len(proof.Siblings) == 0 && root1 == (types.Hash{}) { + t.Fatal("monetary state did not affect authenticated root") + } + + second := makeEpoch(2, &first) + consumed, created, err = ShadowMonetaryTransition(&firstObject, second) + if err != nil { + t.Fatal(err) + } + root2, err := store.Apply(consumed, created) + if err != nil { + t.Fatal(err) + } + if root2 == root1 { + t.Fatal("epoch transition did not change state root") + } + secondObject, _, ok := store.Proof(MonetaryStateObjectID(network)) + if !ok || secondObject.Version != 2 { + t.Fatal("second monetary state not committed") + } + parsed, err := ParseMonetaryEpochState(secondObject.Data) + if err != nil || parsed.PreviousStateHash != second.PreviousStateHash { + t.Fatalf("committed monetary state cannot be verified: %#v %v", parsed, err) + } +} From bd2a47a35553d9a04c8959595c0e6887affe9ed5 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:53:19 +0200 Subject: [PATCH 196/274] Refresh Zephyr v2 implementation status --- docs/protocol-v2-implementation-status.md | 268 +++++++++++++--------- 1 file changed, 160 insertions(+), 108 deletions(-) diff --git a/docs/protocol-v2-implementation-status.md b/docs/protocol-v2-implementation-status.md index 29f9845f..83e6f8a0 100644 --- a/docs/protocol-v2-implementation-status.md +++ b/docs/protocol-v2-implementation-status.md @@ -1,13 +1,21 @@ # Zephyr Protocol v2 — Implementation Status -This document tracks what the clean-break v2 branch **actually implements**, what is integrated only as a reference boundary, and what still requires production engineering. It complements `docs/protocol-v2.md`, which remains the architectural contract, and `docs/tokenomics-v2.md`, which defines the adaptive ZPH economic direction. +This document tracks what the clean-break v2 branch **actually implements**, what exists only as a reference/experimental boundary, and what still requires production engineering. + +Authoritative companion documents: + +- `docs/protocol-v2.md` — architecture and clean-break contract; +- `docs/tokenomics-v2.md` — oracle-free ZPH monetary direction; +- `docs/compute-economics-v2.md` — ZCR/ZCPI/ZCSI compute economics; +- `docs/economic-state-v2.md` — coin age, velocity, epoch aggregation and shadow monetary state; +- `docs/protocol-v2-validator-trust.md` — validator trust/rotation. Status legend: - **Implemented** — executable code and tests exist on the v2 branch. -- **Integrated foundation** — the protocol boundary and correctness rules exist, but a production backend/network/runtime is still to be selected or connected. -- **Shadow/experimental** — executable measurement/simulation exists but cannot yet change live consensus economics. -- **Not production-complete** — must not be presented as a shipped network capability yet. +- **Integrated foundation** — correctness boundary exists, but production integration is incomplete. +- **Shadow/experimental** — executable measurement/simulation/state exists but cannot change live monetary policy. +- **Not production-complete** — must not be presented as a shipped public-network capability. ## Core identity, trust and wire protocol @@ -21,8 +29,7 @@ Status legend: - binary proposal, vote and quorum-certificate wire formats; - binary global-header, shard-commitment, cross-shard-receipt and Merkle-proof decoders; - canonical Merkle commitment over validator ID, P-256 public key and integer voting power; -- every accepted proposal must commit the exact validator-set root used to authorize it; -- runtime commit and cross-shard import reject validator sets that do not match the header's committed `ValidatorRoot`; +- proposal/runtime/import validation binds the exact validator root authorized by consensus; - v2 genesis derives a `TrustAnchor { NetworkID, ValidatorRoot }` for Citizen wallets; - QC-authorized `NextValidatorRoot` transitions and checkpoint-history foundations. @@ -30,14 +37,17 @@ Status legend: **Implemented** -- proof-oriented object/coin model; +- proof-oriented object model; +- coin objects carrying `Token`, `Amount` and consensus-stamped `CreatedHeight`; - 256-bit Sparse Merkle Tree with incremental updates; - compressed inclusion/absence proofs; - in-memory world-state backend; - non-mutating copy-on-write state preview; - durable v2 backend with append-only WAL, CRC32C records, monotonic sequence numbers, network binding, fsync, atomic checkpointing and replay; -- safe truncation of a torn WAL tail after a crash; -- rejection of persisted state from a different network. +- safe truncation of torn WAL tails; +- rejection of persisted state from another network. + +`CreatedHeight` is overwritten by deterministic execution for newly created coin objects. Wallet-provided age metadata is therefore not trusted. **Not production-complete** @@ -47,48 +57,57 @@ Status legend: - archive/history indexing; - further proof/state allocation reduction under large batches. -## Proof-carrying transactions and execution +## Proof-carrying transactions, native assets and execution **Implemented** - P-256 signed proof-carrying transaction format; - state-root-bound object witnesses; -- witness verification without requiring a full-state lookup for validity evidence; +- witness verification without requiring full-state trust; - native ZPH/object transfers; -- protocol-native token creation; -- deterministic input/output conservation and fee checks; +- custom token creation; +- explicit custom-token supply policies: fixed, capped and mintable; +- native custom-token mint operation with authority/cap enforcement; +- native custom-token burn operation with authenticated `CurrentSupply` update; +- `Transferable` enforcement from a token-definition witness; +- read-only token-definition sharing so independent transfers of one token can remain parallel; +- ZPH cannot be minted through user custom-token authority; +- deterministic conservation and fee checks; - deterministic parallel batch executor; -- rejection of batches with shared consumed objects, duplicate transactions or different pre-state roots; -- atomic merge of independent transaction results; +- rejection of batches with shared writes, duplicate transactions or different pre-state roots; +- atomic merge of independent execution results; - state-root simulation before consensus finality; -- permanent shard placement encoded into object IDs for multi-shard state; -- contract deploy/call execution path with metered deterministic reference runtime; -- compute-market operations represented in v2 consensus object execution. +- permanent shard placement encoded into object IDs; +- contract deploy/call execution path; +- compute-market operations represented in consensus object execution. + +**Activation guardrail** + +Custom-token cross-shard transfers/mints remain rejected until Zephyr has a globally verifiable token-policy proof/registry path. ZPH cross-shard receipts remain supported. **Not production-complete** -- explicit native token mint/burn operations and transfer-policy enforcement; -- explicit fee distribution object/state accounting (burn/validator/reserve split); -- finalized resource-unit gas schedule. +- authenticated cross-shard custom-token policy distribution; +- active fee distribution objects/accounting for burn/validator/reserve shares; +- finalized production resource-unit gas schedule. -The key invariant remains enforced: candidate execution may calculate a future state root, but committed state is not mutated before a valid quorum certificate exists. +The key invariant remains: candidate execution may calculate a future state root, but committed state is not mutated before a valid quorum certificate exists. ## Consensus and global finality **Implemented** -- v2 validator set with integer voting power; -- deterministic weighted proposer selection; -- domain-separated proposal and vote signatures; -- locally reconstructed `2/3+` voting-power quorum; +- integer voting power and deterministic weighted proposer selection; +- domain-separated proposal/vote signatures; +- locally reconstructed `2/3+` quorum; - duplicate-voter rejection; - canonical quorum-certificate hash; -- validator-set Merkle root as a proposal validity invariant; -- `GlobalHeader` consensus hash that avoids certificate/hash circularity; +- validator-set root as proposal validity invariant; +- `GlobalHeader` consensus hash without certificate/hash circularity; - validator rotation commitment via `NextValidatorRoot`; - dedicated v2 seven-validator conformance and partition-stress gate. -The runtime path remains: +Runtime path: ```text proof-carrying transactions @@ -104,7 +123,7 @@ proof-carrying transactions **Not production-complete** -- broader restart/proposer-death/Byzantine/wrong-chain fault matrix over the production transport; +- broader restart/proposer-death/Byzantine/wrong-chain matrix over production transport; - governance-controlled validator-set activation policy; - long-horizon checkpoint pruning/recovery rules. @@ -112,23 +131,24 @@ proof-carrying transactions **Implemented foundation** -- deterministic account shard routing; -- permanent shard placement encoded in object IDs; +- deterministic account routing; +- permanent shard placement in object IDs; - per-shard state/data/receipt commitments; - global shard-commitment root; -- remote outputs become finalized cross-shard receipts; -- receipt Merkle batches and inclusion proofs; -- destination import verifies source finality/QC, validator root, shard proof and receipt proof; +- remote ZPH outputs become finalized cross-shard receipts; +- receipt Merkle batches/inclusion proofs; +- destination import verifies source QC, validator root, shard proof and receipt proof; - durable Merkle-state anti-replay marker; - two-shard end-to-end finalization/import/replay-rejection test; -- hostile self-signed foreign validator-set receipts are rejected. +- hostile self-signed validator-set receipts rejected. **Not production-complete** - shard-aware gossip/recovery under sustained faults; -- reshard/split/merge rules and object migration; -- receipt-marker pruning/history-retention policy; -- 4/16-shard conformance, recovery and controlled-hardware throughput evidence. +- reshard/split/merge and object migration; +- receipt-marker pruning/history retention; +- custom-token global policy proof; +- 4/16-shard controlled-hardware conformance/recovery/throughput evidence. `shardCount = 1` remains the safe public activation value until those gates pass. @@ -138,17 +158,20 @@ proof-carrying transactions - Go Citizen verifier for headers/state/shard/data proofs; - battery/network-aware participation policy; -- self-verifiable light API (`/v2/light/status`, `/v2/light/object`); -- strict wallet verifier for canonical headers, low-S P-256 votes, exact `2/3+` quorum, validator roots, shard commitments and Sparse-Merkle proofs; -- genesis/checkpoint trust anchor and next-validator-root trust advancement; -- exact `uint64` validator power handling through decimal JSON + JavaScript `BigInt`; -- wallet resource-mode selection for header-only, relay, DA sampling/cache and opportunistic recent execution. +- self-verifiable light API; +- strict wallet verification of canonical headers, low-S P-256 votes, exact quorum, validator roots, shard commitments and Sparse-Merkle proofs; +- genesis/checkpoint trust anchor and validator-root advancement; +- exact `uint64` voting power through decimal JSON + JavaScript `BigInt`; +- wallet resource modes for header-only, relay, DA sampling/cache and opportunistic recent execution. + +Because monetary state is a normal Merkle-authenticated system object, its bytes can already be proved through the same state-root path. A dedicated wallet monetary-state decoder/UI is not yet integrated. **Not production-complete** -- Vue Citizen status/control UI; -- iOS/Android native lifecycle/background adapters; -- multi-peer proof comparison, resumable cache and full peer relay integration; +- Citizen status/control UI; +- iOS/Android lifecycle/background adapters; +- dedicated ZAMP/ZCSI state decoder/history view; +- multi-peer proof comparison/resumable cache/full relay integration; - real-device RAM/battery/bandwidth measurements. ## Smart contracts @@ -159,9 +182,9 @@ proof-carrying transactions - deterministic metered Zephyr Script reference runtime; - bounded module/request/output/event limits; - execution-step/fuel limits; -- declared read/write object set and no undeclared writes; +- declared read/write object sets and no undeclared writes; - no clock/random/filesystem/network/import nondeterminism in the reference runtime; -- contract deploy/call executor integration and execution receipts. +- contract deploy/call executor integration and receipts. **Not production-complete** @@ -173,93 +196,120 @@ proof-carrying transactions **Implemented** -- compute provider offers and resource/capability requirements; +- provider offers and resource/capability requirements; - collateral requirements; -- job posting with escrow and deadline; +- job posting with escrow/deadline; - deterministic offer/job IDs; - matching/assignment and multi-provider replicated verification; - provider result submission; - settlement, unused-escrow refund and expiry; - objective replicated-majority slashing path; -- compute market object serialization and consensus execution transitions; +- compute market object serialization and consensus transitions; - verification-policy boundaries for deterministic, replicated, challenge, ZK, TEE, client-approved and hybrid evidence. -Heavy compute is provider-executed; validators verify compact settlement evidence and do not replay expensive workloads. +Heavy compute is provider-executed; validators verify compact settlement evidence rather than replaying expensive workloads. -**Shadow/experimental compute economics** +### Compute economics — shadow/experimental + +**Implemented** -- normalized `WorkVector` rather than one fake universal FLOP scalar; +- `WorkVector` resource representation instead of one fake universal FLOP scalar; - workload classes and versioned `WorkSpec` bound to `WorkloadHash` + `BenchmarkHash`; -- registry rejects conflicting definitions for the same workload hash; -- only finalized, verification-satisfied settlements can become `VerifiedWork` observations; -- offer prices and provider self-reported capacity are excluded from ZCPI observations; -- deterministic per-class price medians, Q9 fixed-point arithmetic, EWMA, basket coverage and reliability flag; -- compute-price trend is bounded; -- ZCPI is telemetry-only for monetary policy v0. +- conflicting registry definitions rejected; +- only finalized verification-satisfied settlements become `VerifiedWork` observations; +- offer prices and self-reported theoretical capacity excluded from ZCPI price observations; +- per-class medians, Q9 fixed-point pricing, EWMA, basket coverage and reliability; +- bounded compute-price trend; +- ZCSI combines escrow-backed standardized demand, verified supply, backlog, fulfillment, compute utilization and reliable ZCPI trend; +- unreliable ZCPI removes the price component rather than supplying fake information; +- ZCSI has independent minimum-demand/supply reliability gates; +- three simulator feedback modes: + - A — observe only; + - B — change suggested compute reward routing, not total inflation; + - C — reward routing plus a narrow bounded shadow inflation correction; +- unreliable ZCSI cannot move either reward routing or monetary target. **Not production-complete** -- authenticated/governance-controlled on-chain workload registry with activation heights; +- authenticated/governance-delayed workload registry; +- consensus-reproducible benchmarked/collateralized compute-capacity registry; - provider daemon/scheduler and input/output distribution protocol; -- concrete ZK verifier integrations; -- concrete TEE attestation integrations; +- concrete production ZK verifier integrations; +- concrete production TEE attestation integrations; - confidential-data key exchange; -- compute reputation/anti-collusion policy; -- real ZCPI basket weights and minimum-sample thresholds based on observed market data. +- reputation/concentration/anti-collusion policy; +- empirical ZCPI basket weights and thresholds. ## ZPH tokenomics and adaptive monetary policy -See `docs/tokenomics-v2.md`. +See `docs/tokenomics-v2.md`, `docs/compute-economics-v2.md` and `docs/economic-state-v2.md`. -**Shadow/experimental** +### Shadow/experimental — implemented - no fixed max-supply assumption in the v2 economic design; -- long-run net ZPH supply-growth center near 2% annually; -- bounded adaptive inflation target using only on-chain signals; -- reserve, staking, resource-utilization, age-weighted-velocity and finalized-operation signal inputs; -- burn-offset accounting: gross mint target equals desired net issuance plus observed burn; -- one-basis-point-per-epoch default shadow rate limit; -- ZCPI price/trend/reliability recorded as telemetry but intentionally given zero monetary influence in v0; -- `cmd/zephyr-econ-sim` replays epoch metrics and prints the deterministic shadow decision. - -**Not production-complete / not active** - -- ZAMP does not mint live ZPH yet; -- no public economic parameter set is claimed final; -- age-weighted velocity metric must be implemented from coin-object history and stress-tested against self-cycling; -- explicit fee split and resource-price controller must be state-backed; -- protocol reserve and total supply must become authenticated monetary state objects; -- governance bounds/delays and emergency fallback rules must be finalized; -- Citizen Node monetary-decision verification must be connected; -- compute price must remain telemetry-only until empirical causality/manipulation studies justify any weight. +- long-run net supply-growth center near 2% annually; +- bounded/rate-limited adaptive ZAMP target using only on-chain signals; +- deterministic burn-offset accounting; +- resource fee quotation and burn/validator/reserve split reference engine; +- compatibility fee policy preserves current full-fee burn until authenticated distribution is activated; +- age-weighted velocity accumulator; +- consensus-stamped coin creation height used as the age anchor; +- rapid fresh-coin cycling can be assigned zero contribution below `MinAgeBlocks`; +- age weight saturates at `FullWeightAgeBlocks` and the velocity metric is bounded; +- per-shard canonical `ShardEpochMetrics` for fees, operations, chain resources, circulating native supply, velocity and compute market telemetry; +- exact fee conservation checks in shard epoch metrics; +- deterministic multi-shard epoch aggregation; +- global velocity is weighted by per-shard circulating ZPH rather than equal-weighted by shard; +- blockchain resource utilization and compute utilization are kept separate; +- canonical epoch aggregate hash; +- deterministic network-scoped `MonetaryEpochState` system-object ID; +- canonical shadow monetary-state serialization/hash; +- `PreviousStateHash` chains consecutive economic epochs; +- QC-safe object delta builder returns a state transition without mutating the store itself; +- shadow monetary object can be included in a Merkle state root via normal consume/recreate semantics; +- Mode C records suggested issuance without mutating `TotalSupply`; +- `cmd/zephyr-econ-sim` supports ZAMP plus optional ZCSI A/B/C replay. + +### Not production-complete / not active + +- ZAMP does not mint live ZPH; +- no public economic parameter set is final; +- active validator/compute/reserve reward distribution is not enabled; +- resource fee prices/base-fee controller are not consensus-active; +- runtime derivation of per-shard epoch metrics from every finalized block still needs completion; +- verified compute supply still needs an authenticated availability/benchmark source; +- the epoch monetary transition is not yet scheduled automatically by the node runtime; +- governance bounds/delays and emergency fallback rules remain open; +- Citizen monetary-state decoding/history UI remains open; +- Mode B/C feedback remains shadow-only until long-run/manipulation testing. ## Data availability **Implemented foundation** -- data roots and authenticated chunk/sample proof boundary; -- Reed-Solomon erasure-coded shard reconstruction path; -- rejection of corrupted chunks before reconstruction; -- Citizen participation mode for bounded sampling. +- data roots and authenticated chunk/sample proofs; +- Reed-Solomon erasure-coded reconstruction; +- corrupted chunks rejected before reconstruction; +- bounded Citizen sampling mode. **Not production-complete** - final sampling confidence parameters; -- withholding-attack matrix in the fault lab; -- shard-aware data dissemination and repair; +- withholding-attack fault matrix; +- shard-aware data dissemination/repair; - mobile bandwidth/storage measurements. ## Transport **Implemented foundation** -- consensus, transaction relay and light-proof retrieval are separate protocol capabilities; -- libp2p node identity is separate from account/validator keys; -- QUIC production transport path with network-scoped protocol IDs, frame limits/deadlines and loopback tests. +- separate consensus, transaction-relay and light-proof protocol capabilities; +- libp2p node identity separated from account/validator keys; +- QUIC path with network-scoped protocol IDs, frame limits/deadlines and loopback tests. **Not production-complete** -- discovery/bootstrap/NAT traversal/mobile relay policy; +- discovery/bootstrap/NAT/mobile relay policy; - shard-aware gossip topology; - full fault-transport equivalence matrix over libp2p/QUIC. @@ -279,18 +329,20 @@ Shared CI numbers are development signals only, never production-capacity claims Before a public v2 devnet: -1. v2 multi-validator consensus must cover partitions, restarts, proposer death, conflicting/Byzantine evidence and wrong-chain data; -2. durable state/runtime metadata must survive crash/restart and longer stress runs; -3. Citizen verification must run against live nodes on real Android/iOS reference devices; -4. one-shard performance must be characterized on controlled hardware; -5. multi-shard mode must stay disabled until shard-aware recovery and 4/16-shard conformance pass; -6. production WASM must be deterministic and metered across machines; -7. real-value compute settlement needs production provider/evidence/dispute plumbing; -8. fee, supply, reserve and ZAMP accounting must be explicit authenticated state; -9. ZAMP must remain shadow-only through replay/simulation and manipulation testing; -10. genesis/checkpoint/operator upgrade and validator-rotation procedures must be explicit. - -The engineering rule remains: +1. expand v2 fault coverage for restarts, proposer death, Byzantine/conflicting evidence and wrong-chain data; +2. complete long-running durable-state crash/recovery stress; +3. run Citizen verification against live nodes on real Android/iOS reference devices; +4. characterize one-shard performance on controlled hardware; +5. keep multi-shard public mode disabled until shard-aware recovery and 4/16-shard gates pass; +6. make production WASM deterministic/metered across machines; +7. complete production compute provider/evidence/dispute plumbing; +8. derive economic epoch metrics from finalized execution rather than external declarations; +9. authenticate verified compute capacity and registry activation; +10. keep ZAMP and ZCSI feedback shadow-only through replay, manipulation and oscillation tests; +11. activate any mint/reward/fee split only behind an explicit protocol version/height and governance bounds; +12. make genesis/checkpoint/operator upgrade and validator-rotation procedures explicit. + +The engineering rules remain: ```text more hardware -> more throughput From 64b795096fdc0407b9cf16dba70bb75a3ab448b5 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:54:19 +0200 Subject: [PATCH 197/274] Update tokenomics for ZCSI and economic state --- docs/tokenomics-v2.md | 311 +++++++++++++++++++++++------------------- 1 file changed, 173 insertions(+), 138 deletions(-) diff --git a/docs/tokenomics-v2.md b/docs/tokenomics-v2.md index 38291ab2..06fa72ab 100644 --- a/docs/tokenomics-v2.md +++ b/docs/tokenomics-v2.md @@ -4,6 +4,11 @@ Status: **design contract + shadow-mode implementation target**. This document defines the economic direction for ZPH in Protocol v2. It deliberately separates mechanisms that are already executable in the branch from mechanisms that must remain in shadow/simulation mode until devnet evidence demonstrates stability and manipulation resistance. +Detailed companion specifications: + +- `docs/compute-economics-v2.md` — normalized compute work, ZCPI, ZCSI and A/B/C feedback experiments; +- `docs/economic-state-v2.md` — consensus-stamped coin age, velocity, per-shard epoch accounting and authenticated shadow monetary state. + ## Goals Zephyr does not use a fixed maximum ZPH supply as its long-term monetary rule. The target is a deterministic, oracle-free adaptive policy with a long-run **net supply growth center near 2% per year**. @@ -17,7 +22,7 @@ The policy must: - use integer/fixed-point arithmetic only; - rate-limit monetary changes; - resist wash activity, fake offers and short-lived transaction spam; -- remain observable in shadow mode before it is allowed to mint or burn protocol supply. +- remain observable in shadow mode before it is allowed to mint protocol supply. A 2% target refers to **ZPH monetary supply growth**, not real-world purchasing-power inflation. Without an external price oracle Zephyr cannot claim to track CPI or any fiat purchasing-power index. @@ -33,10 +38,10 @@ Conceptually: resource use -> block/shard utilization -> dynamic base fee - -> fee burn + validator/reward component + -> fee burn + validator/reward + reserve components ``` -Future resource pricing should account for: +The reference fee engine can price: - transaction base work; - signature verification; @@ -44,8 +49,9 @@ Future resource pricing should account for: - state reads/writes; - contract fuel; - data-availability bytes; -- cross-shard receipts; -- other consensus-critical resource units. +- cross-shard receipts. + +It also supports deterministic burn/validator/reserve splitting with exact value conservation. The current compatibility policy remains full fee burn until authenticated reward/reserve distribution is activated. The fast loop may react to congestion quickly. It does **not** directly change the long-term monetary target. @@ -55,14 +61,14 @@ The slow loop is the **Zephyr Adaptive Monetary Policy (ZAMP)**. It observes smoothed on-chain economic/security metrics and calculates the gross mint that would be required to hit the epoch net-issuance target after burn. -The branch currently implements this controller in **shadow mode only**. +The branch implements this controller in **shadow mode only**. ```text supply burn stake ratio protocol reserve ratio -resource utilization +blockchain resource utilization age-weighted velocity finalized operations compute-market telemetry @@ -78,7 +84,7 @@ gross mint target net supply target near 2% annualized ``` -Shadow mode computes the decision but does not mutate supply. +Shadow mode computes and can authenticate the decision but does not mutate live ZPH supply. ## 2. Net inflation target @@ -125,12 +131,12 @@ ZAMP can use only values committed by Zephyr consensus. ### Supply and burn -Directly known: +Directly knowable from protocol state/accounting: - total ZPH supply; - circulating supply; - ZPH burned by the fee mechanism; -- protocol-minted ZPH; +- protocol-minted ZPH after future activation; - protocol reserve. ### Security @@ -141,13 +147,11 @@ Directly known: - validator voting power; - collateral/slashing state. -The controller may gently increase incentives when staking/security coverage is below target and reduce them when coverage is comfortably above target. - ### Network utilization Do not use raw HTTP requests or mempool ingress. Use finalized consensus resource consumption. -A future resource-usage index should be derived from signed/finalized work such as state writes, proof bytes, fuel, DA bytes and receipt processing. +Blockchain resource utilization and compute-market utilization are separate signals and must not be conflated. ### Finalized operations @@ -155,11 +159,21 @@ Operation counts are secondary signals only. They are not sufficient alone becau ### Age-weighted monetary velocity -Simple transfer volume is wash-tradeable. The intended Zephyr velocity metric is based on native object history and gives more weight to value that remained unspent for meaningful time before moving. +Simple transfer volume is wash-tradeable. Zephyr v2 coin objects now carry a consensus-stamped `CreatedHeight`. + +New coin outputs are rewritten by deterministic execution with the candidate block height, so the wallet cannot choose an old timestamp to create fake monetary age. + +The reference velocity accumulator uses: + +```text +age = spendHeight - CreatedHeight +``` -Repeatedly cycling the same fresh coin object should therefore contribute far less than genuinely circulating older liquidity. +with configurable minimum age, full-weight age and maximum velocity bounds. -Velocity must be smoothed over long windows (for example EWMA/rolling epochs) before it can influence monetary policy. +Rapidly recreating and cycling fresh coin objects therefore resets their age and can contribute zero below `MinAgeBlocks`. + +Unknown/genesis age (`CreatedHeight = 0`) is tracked separately and excluded by the current reference policy. ## 5. Zephyr normalized compute work @@ -167,7 +181,7 @@ There is no honest universal scalar that makes every CPU, GPU, AI training job, Zephyr therefore uses a **resource vector**, not a fake universal FLOP count. -The current v2 model defines normalized dimensions including: +The current model includes: ```text CPUUnits @@ -180,20 +194,13 @@ StorageBytes NetworkBytes ``` -A standardized workload definition also carries: - -- protocol work-spec version; -- workload class; -- normalized logical work units; -- workload hash; -- benchmark/specification hash; -- resource vector. +A standardized workload definition carries protocol version, workload class, normalized units, workload hash, benchmark/specification hash and resource vector. -The benchmark hash anchors the meaning of the units. A provider cannot make its GPU appear more valuable merely by self-reporting a larger number. +The benchmark hash anchors the meaning of the units. A provider cannot make its hardware appear more valuable merely by self-reporting a larger number. ## 6. Compute workload registry -Only protocol-approved work specifications are eligible for monetary telemetry. +Only standardized work specifications are eligible for monetary telemetry. A registry entry binds: @@ -207,17 +214,17 @@ WorkloadHash Conflicting definitions for the same workload hash are rejected. -The initial implementation is an executable reference registry. Before monetary activation the registry must become an authenticated protocol/governance state transition with explicit versioning and activation heights. +The current registry is a reference implementation. Before monetary activation it must become authenticated/governance-controlled state with delayed versioned activation. ## 7. ZCPI — Zephyr Compute Price Index ZCPI is an internal Zephyr compute-market price index. It is **not** a CPI and is not a claim about real-world inflation. -It answers a narrower question: +It answers: > how many atomic ZPH units were actually paid for standardized, verified compute work on Zephyr? -ZCPI deliberately excludes: +ZCPI excludes: - advertised provider prices; - unfilled offers; @@ -225,7 +232,7 @@ ZCPI deliberately excludes: - failed/unverified jobs; - arbitrary unregistered workload units. -An eligible observation is generated only from: +Eligible observations derive from: ```text registered workload spec @@ -234,85 +241,98 @@ registered workload spec + actual on-chain provider payments ``` -For each workload class: +The reference implementation uses fixed-point Q9 arithmetic, per-class medians, EWMA smoothing, basket coverage and an explicit reliability flag. + +## 8. ZCSI — Zephyr Compute Scarcity Index + +ZCPI alone cannot safely drive inflation. A price increase can reflect scarcity, demand growth, ZPH purchasing-power movement or workload-mix changes. + +Zephyr therefore separately computes **ZCSI**, a bounded scarcity score based on: ```text -price = paid ZPH / normalized verified work units +escrow-backed standardized demand +verified standardized supply +funded backlog +fulfilled work +compute utilization +reliable ZCPI price trend ``` -The reference implementation uses fixed-point Q9 arithmetic, per-class medians and EWMA smoothing. +Only real escrow-backed standardized work counts as demand. Provider-advertised capacity does not become verified supply merely because it is claimed. -## 8. ZCPI basket, coverage and reliability +If ZCPI is unreliable, the price component is removed. If demand/supply coverage is too thin, ZCSI itself becomes unreliable. -Different resource classes retain different prices. ZCPI may combine them into a weighted basket for telemetry, but it also reports each class separately. +An unreliable ZCSI is prohibited from changing either compute reward routing or the monetary target in the shadow evaluator. -A class enters an epoch index only when it has at least the configured minimum number of verified observations. +## 9. Compute feedback experiments A/B/C -The index reports: +The branch implements three **shadow-only** modes. -- price per class; -- sample count per class; -- weighted basket price; -- basket coverage in basis points; -- a `Reliable` flag; -- total accepted observations. +### A — observe only -If too little of the configured basket has adequate data, the index remains unreliable and must not be used by monetary policy. +```text +ZCSI -> telemetry only +compute reward share -> unchanged +inflation target -> unchanged +``` -This prevents the chain from manufacturing a compute-price signal during thin markets. +### B — reward routing -## 9. Compute prices are telemetry-only in ZAMP v0 +```text +ZCSI -> suggested compute reward share +inflation target -> unchanged +``` -The branch deliberately records `ComputeIndexQ9`, compute-price trend and reliability in the shadow monetary decision **without allowing them to change the inflation target yet**. +This is the preferred first candidate if devnet evidence eventually justifies activation. Scarce verified compute can receive a larger share of an already-defined issuance budget without changing total issuance. -This is intentional. +### C — reward routing + narrow monetary band -A rising ZPH price for compute can mean several different things: +```text +ZCSI -> suggested compute reward share +ZCSI -> small bounded shadow inflation correction +``` -- compute resources became scarce; -- demand for compute increased; -- ZPH purchasing power against compute fell; -- workload mix changed. +The total-inflation sensitivity is deliberately much smaller than the reward-routing sensitivity. -Without sufficient history it is unsafe to infer which cause dominates. +Mode C must demonstrate a material stability/capacity benefit over Mode B before it is considered for activation. -Activation requires simulation showing that a compute-price feedback term improves stability rather than creating a manipulable reflexive loop. +The current active economic boundary remains equivalent to Mode A: **no compute signal changes live supply**. ## 10. Native ZPH fee accounting -Current v2 transaction execution already requires native ZPH inputs to cover `outputs + Fee`. - -Before public economic activation this must evolve into an explicit fee-accounting engine rather than an implicit 100% fee disappearance. +V2 execution requires native ZPH inputs to cover outputs plus the signed fee. -The intended structure is: +The reference fee engine now supports: ```text -transaction resource charge - | - +-- base-fee component -> burn - | - +-- execution/priority component -> validator/reward pool +resource charge | - +-- optional protocol component -> protocol reserve + +-- burn + +-- validator/reward pool + +-- protocol reserve ``` -Percentages and fee parameters must be integer basis points and simulation-backed. +with integer basis-point splits and deterministic rounding that conserves every atomic unit. + +Until state-backed distribution is activated, the compatibility policy preserves the current effective 100% fee burn. ## 11. Smart-contract gas -Contract execution already reports deterministic `FuelUsed` and enforces `FuelLimit`. +Contract execution reports deterministic `FuelUsed` and enforces `FuelLimit`. -The economic fee engine should convert deterministic execution/resource consumption into ZPH cost, for example conceptually: +The reference resource fee model can include: ```text -contract fee = - base transaction resource charge - + FuelUsed * FuelPrice - + state read/write charges - + proof/data-availability charges +base transaction charge ++ signature work ++ witness bytes ++ state reads/writes ++ FuelUsed * FuelPrice ++ DA bytes ++ cross-shard receipts ``` -The wallet should sign a maximum acceptable resource/fee envelope. Validators must not be able to raise it after signing. +Final production prices and the active base-fee controller remain simulation/benchmark decisions. ## 12. Compute payment is not blockchain gas @@ -327,9 +347,7 @@ A 100 ZPH AI job does not imply 100 ZPH of gas. Validators settle commitments/pr ## 13. Native custom-token policy -Protocol-native custom assets retain independent supply policies. - -The desired explicit policies are: +Protocol-native custom assets now have explicit supply policies: ```text FIXED @@ -337,81 +355,98 @@ CAPPED MINTABLE ``` -Native mint/burn operations must update both coin objects and the authenticated `TokenDefinition.CurrentSupply` so Citizen Nodes can prove supply correctness. +The v2 executor implements: -Before public activation the executor must add explicit `MintToken` and `BurnToken` operations and enforce: +- custom-token creation; +- `MintToken` with mint-authority and cap enforcement; +- `BurnToken` with burn-permission enforcement; +- authenticated `TokenDefinition.CurrentSupply` updates; +- `Transferable` enforcement; +- read-only token-definition policy witnesses for parallel normal transfers. -- mint authority; -- cap where applicable; -- irreversible fixed-supply policy; -- burn permission; -- transferability policy. +ZPH itself is excluded from user-authority mint/burn paths. Future ZPH issuance can occur only through an explicitly activated protocol monetary transition. -ZPH itself must not have a human mint authority. ZPH issuance is controlled only by the protocol monetary state machine after activation. +Custom-token cross-shard transfer/mint is deliberately gated until a globally verifiable token-policy proof/registry is available. -## 14. Testing strategy +## 14. Economic epoch state -### Unit/conformance tests +Zephyr must not create one global monetary object touched by every transaction; that would serialize execution. -The branch includes deterministic tests for: +The current foundation therefore uses: -- normalized work-spec serialization; -- conflicting compute-registry definitions; -- deriving a compute observation only from a settled verified job; -- class medians and basket coverage; -- ZCPI reliability thresholds; -- compute price trend bounds; -- burn offset in the shadow monetary controller; -- bounded/rate-limited adaptive inflation target; -- compute telemetry having zero monetary influence in ZAMP v0. - -### Monetary replay simulator - -`cmd/zephyr-econ-sim` accepts a JSON epoch snapshot and prints the shadow decision. - -Example shape: - -```json -{ - "priorTargetBps": 200, - "metrics": { - "Supply": 1000000000, - "CirculatingSupply": 900000000, - "StakedSupply": 450000000, - "ProtocolReserve": 100000000, - "BurnedThisEpoch": 12000, - "FinalizedOperations": 1000000, - "ResourceUtilizationBps": 5000, - "AgeWeightedVelocityBps": 5000, - "ComputeIndexQ9": 7500000000, - "ComputePriceTrendBps": 250, - "ComputeIndexReliable": true - } -} +```text +finalized execution + -> per-shard epoch metrics + -> deterministic epoch aggregate + -> shadow MonetaryEpochState ``` -This enables historical replay, synthetic shocks and sensitivity analysis without changing consensus supply. +Per-shard metrics cover: + +- charged/burned/validator/reserve fees; +- finalized operations; +- chain resource used/capacity; +- shard circulating ZPH; +- age-weighted velocity; +- escrow-backed compute demand; +- verified compute supply; +- compute backlog/fulfillment. + +Exact fee conservation is validated. + +Global velocity is weighted by per-shard circulating ZPH rather than giving every shard equal influence. + +Chain resource utilization and compute utilization are calculated separately. + +The canonical aggregate hash is committed by a deterministic shadow `MonetaryEpochState` system object. Consecutive states bind `PreviousStateHash`. + +The object can enter the normal Sparse-Merkle state root through consume/recreate semantics, so a future epoch-boundary runtime transition does not require another special consensus root. + +The current transition builder returns a state delta for pre-QC simulation; it does not commit a store itself. + +## 15. Testing and replay + +The branch includes deterministic tests for: -## 15. Activation gates +- normalized work-spec serialization and registry conflicts; +- verified settlement observations; +- ZCPI medians, coverage, reliability and trend bounds; +- ZCSI demand/supply scarcity and reliability fail-closed behavior; +- A/B/C feedback separation; +- burn offset and rate-limited ZAMP target; +- fee split/resource quotation conservation; +- consensus override of wallet-provided coin creation height; +- old versus fresh age-weighted velocity; +- rapid self-cycling suppression; +- per-shard economic accounting and multi-shard aggregation; +- separation of chain and compute utilization; +- canonical shadow monetary state and previous-epoch binding; +- suggested Mode-C mint remaining shadow while `TotalSupply` is unchanged. + +`cmd/zephyr-econ-sim` supports base ZAMP replay and optional compute-market/ZCSI A/B/C inputs. See `docs/examples/zephyr-econ-sim-compute.json`. + +## 16. Activation gates ZAMP remains shadow-only until all of the following are true: -1. explicit ZPH supply/burn/mint accounting exists in authenticated protocol state; -2. fee distribution is explicit and conserves supply exactly; -3. velocity is demonstrably resistant to cheap self-cycling; -4. all monetary metrics are reproducible from finalized state; -5. long simulations cover low/high usage, partitions, validator churn, spam and compute-market shocks; -6. parameter sensitivity does not create oscillation or runaway mint/burn behavior; -7. governance can change parameters only through bounded, delayed transitions; -8. Citizen Nodes can independently verify monetary state and epoch decisions; -9. ZCPI has sufficient real-market coverage before any non-zero monetary weight is considered; -10. an emergency safety rule can freeze adaptive corrections while preserving deterministic base issuance if metrics become unavailable or invalid. +1. per-shard economic metrics are derived automatically from finalized runtime execution rather than caller-provided summaries; +2. fee distribution is state-backed and conserves supply exactly; +3. verified compute supply comes from authenticated benchmark/availability state; +4. velocity is stress-tested against long-horizon self-cycling and capital-lock attacks; +5. all monetary metrics are reproducible from finalized state; +6. long simulations cover usage shocks, partitions, validator churn, spam, compute booms/busts and oscillating adversarial inputs; +7. parameter sensitivity does not create runaway or oscillatory issuance; +8. governance can change parameters only through bounded delayed transitions; +9. Citizen Nodes can decode and independently verify monetary state/history; +10. ZCPI/ZCSI have sufficient real-market coverage before Mode B or C is considered; +11. an emergency deterministic fallback can zero adaptive corrections when required metrics are unavailable/invalid; +12. live mint/reward/fee distribution has an explicit protocol-version/activation-height transition. -## 16. Current policy boundary +## 17. Current policy boundary -The current branch implements **measurement and shadow decisions**, not live monetary issuance. +The branch implements **measurement, authenticated shadow state and shadow decisions**, not live monetary issuance. -The design principle is: +The design principle remains: ```text measure first @@ -419,4 +454,4 @@ simulate second activate last ``` -This lets Zephyr use an adaptive oracle-free economy without turning monetary policy into an untested consensus experiment. +This lets Zephyr develop an adaptive oracle-free economy without turning monetary policy into an untested consensus experiment. From 47d0ac878f288cec68532d15eec24efade0d76c7 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:02:51 +0200 Subject: [PATCH 198/274] Switch project license to Apache-2.0 --- LICENSE | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 201 insertions(+), 21 deletions(-) diff --git a/LICENSE b/LICENSE index 9b1ec2fa..261eeb9e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2025 the-code-learner - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 128f3bb11f315534f7337d5b4b514e730ff3706a Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:03:02 +0200 Subject: [PATCH 199/274] Add Apache-2.0 attribution notice --- NOTICE | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 NOTICE diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..04dfdd12 --- /dev/null +++ b/NOTICE @@ -0,0 +1,9 @@ +Zephyr Chain + +Copyright 2026 the-code-learner + +Original project by the-code-learner. + +This attribution notice must be preserved in redistributed +copies and derivative works in accordance with the +Apache License 2.0. From ac50372ac4d51f4d41aa918d8ca673dc21e2e889 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:06:08 +0200 Subject: [PATCH 200/274] Derive verified work from settled compute state --- internal/v2/compute/settled_work.go | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 internal/v2/compute/settled_work.go diff --git a/internal/v2/compute/settled_work.go b/internal/v2/compute/settled_work.go new file mode 100644 index 00000000..36583da3 --- /dev/null +++ b/internal/v2/compute/settled_work.go @@ -0,0 +1,87 @@ +package compute + +import ( + "math" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +// ObserveSettledRecord reconstructs an index-eligible VerifiedWork observation +// from finalized compute state alone. No provider offer price or external RPC +// settlement summary is trusted. For replicated jobs, only providers on the +// strict-majority result root are counted as paid, matching the on-chain +// majority settlement rule. +func ObserveSettledRecord(record OnChainJob, registry *WorkRegistry) (VerifiedWork, error) { + if record.Status != JobSettled || registry == nil || len(record.Assignments) == 0 || len(record.Results) == 0 { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + spec, ok := registry.Resolve(record.Job.WorkloadHash) + if !ok || spec.Validate() != nil { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + + resultByProvider := make(map[types.AccountID]types.Hash, len(record.Results)) + rootCounts := make(map[types.Hash]int, len(record.Results)) + for _, result := range record.Results { + if err := result.Validate(); err != nil || result.JobID != record.ID { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + if _, duplicate := resultByProvider[result.Provider]; duplicate { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + resultByProvider[result.Provider] = result.ResultRoot + rootCounts[result.ResultRoot]++ + } + + var acceptedRoot types.Hash + acceptedCount := 0 + for root, count := range rootCounts { + if count > acceptedCount { + acceptedRoot = root + acceptedCount = count + } + } + if types.IsZero32([32]byte(acceptedRoot)) { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + if record.Job.Verification == VerificationReplicated { + if acceptedCount*2 <= len(record.Results) { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + } else if acceptedCount != len(record.Results) { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + + var paid uint64 + seenAssignments := make(map[types.AccountID]struct{}, len(record.Assignments)) + for _, assignment := range record.Assignments { + if _, duplicate := seenAssignments[assignment.Provider]; duplicate || assignment.Price == 0 { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + seenAssignments[assignment.Provider] = struct{}{} + root, hasResult := resultByProvider[assignment.Provider] + if !hasResult { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + if record.Job.Verification == VerificationReplicated && root != acceptedRoot { + continue + } + if math.MaxUint64-paid < assignment.Price { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + paid += assignment.Price + } + if paid == 0 || paid > record.Escrow { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + + return VerifiedWork{ + JobID: record.ID, + Class: spec.Class, + Units: spec.Units, + Vector: spec.Vector, + PaidZPH: paid, + Verification: record.Job.Verification, + ResultRoot: acceptedRoot, + }, nil +} From 8d67582be270aaa8f8c25d3703b84473e869fc6e Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:06:33 +0200 Subject: [PATCH 201/274] Test finalized compute work reconstruction --- internal/v2/compute/settled_work_test.go | 81 ++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 internal/v2/compute/settled_work_test.go diff --git a/internal/v2/compute/settled_work_test.go b/internal/v2/compute/settled_work_test.go new file mode 100644 index 00000000..23c4e498 --- /dev/null +++ b/internal/v2/compute/settled_work_test.go @@ -0,0 +1,81 @@ +package compute + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestObserveSettledRecordReplicatedMajority(t *testing.T) { + workload := types.Hash{1} + registry, err := NewWorkRegistry([]WorkSpec{{ + Version: WorkSpecVersion, + Class: WorkAITraining, + Units: 100, + WorkloadHash: workload, + BenchmarkHash: types.Hash{2}, + Vector: WorkVector{TensorUnits: 100}, + }}) + if err != nil { + t.Fatal(err) + } + + jobID := types.JobID{3} + providers := []types.AccountID{{4}, {5}, {6}} + majorityRoot := types.Hash{7} + record := OnChainJob{ + ID: jobID, + Job: Job{ + Owner: types.AccountID{8}, + WorkloadHash: workload, + InputRoot: types.Hash{9}, + Resources: Resources{CPUCores: 1, MemoryMiB: 1}, + MaxPrice: 60, + CollateralRequired: 1, + Verification: VerificationReplicated, + DeadlineHeight: 100, + Replicas: 3, + }, + Escrow: 60, + Status: JobSettled, + Assignments: []Assignment{ + {OfferID: types.Hash{10}, Provider: providers[0], Price: 10}, + {OfferID: types.Hash{11}, Provider: providers[1], Price: 20}, + {OfferID: types.Hash{12}, Provider: providers[2], Price: 30}, + }, + Results: []Result{ + {JobID: jobID, Provider: providers[0], ResultRoot: majorityRoot, CompletedHeight: 10}, + {JobID: jobID, Provider: providers[1], ResultRoot: majorityRoot, CompletedHeight: 10}, + {JobID: jobID, Provider: providers[2], ResultRoot: types.Hash{13}, CompletedHeight: 10}, + }, + } + + observed, err := ObserveSettledRecord(record, registry) + if err != nil { + t.Fatal(err) + } + if observed.PaidZPH != 30 { + t.Fatalf("paid = %d, want 30", observed.PaidZPH) + } + if observed.ResultRoot != majorityRoot || observed.Units != 100 || observed.Class != WorkAITraining { + t.Fatalf("unexpected verified work: %+v", observed) + } +} + +func TestObserveSettledRecordRejectsUnsettled(t *testing.T) { + registry, err := NewWorkRegistry([]WorkSpec{{ + Version: WorkSpecVersion, + Class: WorkCPUGeneral, + Units: 1, + WorkloadHash: types.Hash{1}, + BenchmarkHash: types.Hash{2}, + Vector: WorkVector{CPUUnits: 1}, + }}) + if err != nil { + t.Fatal(err) + } + _, err = ObserveSettledRecord(OnChainJob{Status: JobPending}, registry) + if err == nil { + t.Fatal("expected unsettled record rejection") + } +} From a7d59062046b712b8b7f0cacdd4513f7b36fb157 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:08:58 +0200 Subject: [PATCH 202/274] Verify finalized compute settlement receipts --- internal/v2/compute/settlement_receipt.go | 129 ++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 internal/v2/compute/settlement_receipt.go diff --git a/internal/v2/compute/settlement_receipt.go b/internal/v2/compute/settlement_receipt.go new file mode 100644 index 00000000..f59e5d9b --- /dev/null +++ b/internal/v2/compute/settlement_receipt.go @@ -0,0 +1,129 @@ +package compute + +import ( + "bytes" + "math" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +// ParseSettlementReceipt decodes the canonical settlement evidence committed by +// compute finalization. The receipt is intentionally self-contained so light +// and economic replay code can verify settlement without trusting an RPC. +func ParseSettlementReceipt(data []byte) (SettlementReceipt, error) { + r := codec.NewReader(data) + jobRaw, err := r.Fixed(32) + if err != nil { + return SettlementReceipt{}, ErrMarketState + } + rootRaw, err := r.Fixed(32) + if err != nil { + return SettlementReceipt{}, ErrMarketState + } + payments, err := readAccountAmounts(r) + if err != nil { + return SettlementReceipt{}, err + } + refund, err := r.U64() + if err != nil { + return SettlementReceipt{}, ErrMarketState + } + slashed, err := readAccountAmounts(r) + if err != nil { + return SettlementReceipt{}, err + } + slashReward, err := r.U64() + if err != nil { + return SettlementReceipt{}, ErrMarketState + } + expired, err := r.Bool() + if err != nil || r.Done() != nil { + return SettlementReceipt{}, ErrMarketState + } + var jobID types.JobID + var resultRoot types.Hash + copy(jobID[:], jobRaw) + copy(resultRoot[:], rootRaw) + if types.IsZero32([32]byte(jobID)) { + return SettlementReceipt{}, ErrMarketState + } + out := SettlementReceipt{ + JobID: jobID, ResultRoot: resultRoot, Payments: payments, Refund: refund, + Slashed: slashed, SlashReward: slashReward, Expired: expired, + } + if expired { + if !types.IsZero32([32]byte(resultRoot)) || len(payments) != 0 || len(slashed) != 0 || slashReward != 0 { + return SettlementReceipt{}, ErrMarketState + } + } else if types.IsZero32([32]byte(resultRoot)) || len(payments) == 0 { + return SettlementReceipt{}, ErrMarketState + } + return out, nil +} + +func readAccountAmounts(r *codec.Reader) (map[types.AccountID]uint64, error) { + count, err := r.U32() + if err != nil || count > 1024 { + return nil, ErrMarketState + } + out := make(map[types.AccountID]uint64, int(count)) + for i := uint32(0); i < count; i++ { + raw, err := r.Fixed(32) + if err != nil { + return nil, ErrMarketState + } + amount, err := r.U64() + if err != nil || amount == 0 { + return nil, ErrMarketState + } + var account types.AccountID + copy(account[:], raw) + if types.IsZero32([32]byte(account)) { + return nil, ErrMarketState + } + if _, duplicate := out[account]; duplicate { + return nil, ErrMarketState + } + out[account] = amount + } + return out, nil +} + +// ObserveFinalizedSettlement verifies that a settlement receipt exactly matches +// the deterministic on-chain settlement for the supplied pre-finalization job +// record, then returns the ZCPI-eligible VerifiedWork observation. +func ObserveFinalizedSettlement(record OnChainJob, receipt SettlementReceipt, registry *WorkRegistry) (VerifiedWork, error) { + if registry == nil || receipt.Expired || receipt.JobID != record.ID || record.Status != JobAwaitingVerification { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + + updated, settlement, err := FinalizeOnChain(record, VerificationEvidence{}) + if err != nil { + updated, settlement, err = ResolveReplicatedMajority(record) + if err != nil { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + } + expected := SettlementReceipt{ + JobID: record.ID, ResultRoot: settlement.ResultRoot, Payments: settlement.Payments, + Refund: settlement.Refund, Slashed: settlement.SlashedCollateral, SlashReward: settlement.SlashReward, + } + if !bytes.Equal(receipt.MarshalBinary(), expected.MarshalBinary()) { + return VerifiedWork{}, ErrInvalidWorkSettlement + } + return ObserveVerifiedWork(updated, settlement, registry) +} + +// SettlementPaid returns the amount actually paid to compute providers. Refunds +// and collateral movements are deliberately excluded from ZCPI pricing. +func SettlementPaid(receipt SettlementReceipt) (uint64, error) { + var paid uint64 + for _, amount := range receipt.Payments { + if math.MaxUint64-paid < amount { + return 0, ErrMarketEscrow + } + paid += amount + } + return paid, nil +} From fbcdca96d227f488484fbc8e03d005fef94cb671 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:09:40 +0200 Subject: [PATCH 203/274] Test compute settlement receipt verification --- .../v2/compute/settlement_receipt_test.go | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 internal/v2/compute/settlement_receipt_test.go diff --git a/internal/v2/compute/settlement_receipt_test.go b/internal/v2/compute/settlement_receipt_test.go new file mode 100644 index 00000000..3b56fb5c --- /dev/null +++ b/internal/v2/compute/settlement_receipt_test.go @@ -0,0 +1,94 @@ +package compute + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestSettlementReceiptRoundTrip(t *testing.T) { + receipt := SettlementReceipt{ + JobID: types.JobID{1}, + ResultRoot: types.Hash{2}, + Payments: map[types.AccountID]uint64{{3}: 10, {4}: 20}, + Refund: 5, + Slashed: map[types.AccountID]uint64{{5}: 7}, + SlashReward: 7, + } + parsed, err := ParseSettlementReceipt(receipt.MarshalBinary()) + if err != nil { + t.Fatal(err) + } + if parsed.JobID != receipt.JobID || parsed.ResultRoot != receipt.ResultRoot || parsed.Refund != receipt.Refund || parsed.SlashReward != receipt.SlashReward { + t.Fatalf("round trip mismatch: %+v", parsed) + } + paid, err := SettlementPaid(parsed) + if err != nil || paid != 30 { + t.Fatalf("paid = %d err=%v, want 30", paid, err) + } +} + +func TestObserveFinalizedSettlementReplicatedMajority(t *testing.T) { + workload := types.Hash{11} + registry, err := NewWorkRegistry([]WorkSpec{{ + Version: WorkSpecVersion, + Class: WorkRendering, + Units: 50, + WorkloadHash: workload, + BenchmarkHash: types.Hash{12}, + Vector: WorkVector{GPUFP32Units: 50}, + }}) + if err != nil { + t.Fatal(err) + } + jobID := types.JobID{13} + providers := []types.AccountID{{14}, {15}, {16}} + root := types.Hash{17} + record := OnChainJob{ + ID: jobID, + Job: Job{ + Owner: types.AccountID{18}, + WorkloadHash: workload, + InputRoot: types.Hash{19}, + Resources: Resources{GPUCount: 1, MemoryMiB: 1}, + MaxPrice: 60, + CollateralRequired: 4, + Verification: VerificationReplicated, + DeadlineHeight: 100, + Replicas: 3, + }, + Escrow: 60, + Status: JobAwaitingVerification, + Assignments: []Assignment{ + {OfferID: types.Hash{20}, Provider: providers[0], Price: 10}, + {OfferID: types.Hash{21}, Provider: providers[1], Price: 20}, + {OfferID: types.Hash{22}, Provider: providers[2], Price: 30}, + }, + Results: []Result{ + {JobID: jobID, Provider: providers[0], ResultRoot: root, CompletedHeight: 10}, + {JobID: jobID, Provider: providers[1], ResultRoot: root, CompletedHeight: 10}, + {JobID: jobID, Provider: providers[2], ResultRoot: types.Hash{23}, CompletedHeight: 10}, + }, + } + receipt := SettlementReceipt{ + JobID: jobID, + ResultRoot: root, + Payments: map[types.AccountID]uint64{providers[0]: 10, providers[1]: 20}, + Refund: 30, + Slashed: map[types.AccountID]uint64{providers[2]: 4}, + SlashReward: 4, + } + observed, err := ObserveFinalizedSettlement(record, receipt, registry) + if err != nil { + t.Fatal(err) + } + if observed.PaidZPH != 30 || observed.Units != 50 || observed.ResultRoot != root { + t.Fatalf("unexpected observation: %+v", observed) + } + + tampered := receipt + tampered.Refund++ + if _, err := ObserveFinalizedSettlement(record, tampered, registry); err == nil { + t.Fatal("expected tampered receipt rejection") + } +} From 2ae11edddfdeaa71467a59bdd236509b39bc5cb8 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:11:43 +0200 Subject: [PATCH 204/274] Make compute epoch accounting carry backlog across epochs --- internal/v2/economics/epoch.go | 56 ++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/internal/v2/economics/epoch.go b/internal/v2/economics/epoch.go index 8fd3b6cd..6d791049 100644 --- a/internal/v2/economics/epoch.go +++ b/internal/v2/economics/epoch.go @@ -9,7 +9,7 @@ import ( "github.com/zephyr-chain/zephyr-chain/internal/v2/types" ) -const EpochMetricsVersion uint16 = 1 +const EpochMetricsVersion uint16 = 2 var ErrEpochMetrics = errors.New("invalid Zephyr economic epoch metrics") @@ -28,15 +28,22 @@ type ShardEpochMetrics struct { AgeWeightedVelocityBps uint32 EscrowBackedComputeDemand uint64 VerifiedComputeSupply uint64 - ComputeBacklog uint64 + OpeningComputeBacklog uint64 ComputeFulfilled uint64 + ComputeExpired uint64 + ComputeBacklog uint64 } func (m ShardEpochMetrics) Validate() error { if m.Version != EpochMetricsVersion || m.Epoch == 0 || m.ResourceCapacity == 0 || m.ResourceUsed > m.ResourceCapacity || m.AgeWeightedVelocityBps > 10*BasisPoints || - m.ComputeFulfilled > m.VerifiedComputeSupply || m.ComputeFulfilled > m.EscrowBackedComputeDemand || - m.ComputeBacklog > m.EscrowBackedComputeDemand-m.ComputeFulfilled { + m.ComputeFulfilled > m.VerifiedComputeSupply || !validComputeFlow( + m.OpeningComputeBacklog, + m.EscrowBackedComputeDemand, + m.ComputeFulfilled, + m.ComputeExpired, + m.ComputeBacklog, + ) { return ErrEpochMetrics } feeTotal := new(big.Int).SetUint64(m.BurnedFees) @@ -67,8 +74,10 @@ func (m ShardEpochMetrics) CanonicalBytes() ([]byte, error) { w.U32(m.AgeWeightedVelocityBps) w.U64(m.EscrowBackedComputeDemand) w.U64(m.VerifiedComputeSupply) - w.U64(m.ComputeBacklog) + w.U64(m.OpeningComputeBacklog) w.U64(m.ComputeFulfilled) + w.U64(m.ComputeExpired) + w.U64(m.ComputeBacklog) return w.BytesCopy(), nil } @@ -95,8 +104,10 @@ type EpochAggregate struct { AgeWeightedVelocityBps uint32 EscrowBackedComputeDemand uint64 VerifiedComputeSupply uint64 - ComputeBacklog uint64 + OpeningComputeBacklog uint64 ComputeFulfilled uint64 + ComputeExpired uint64 + ComputeBacklog uint64 ComputeUtilizationBps uint32 } @@ -108,7 +119,7 @@ func AggregateEpochMetrics(metrics []ShardEpochMetrics) (EpochAggregate, error) sort.Slice(ordered, func(i, j int) bool { return ordered[i].ShardID < ordered[j].ShardID }) epoch := ordered[0].Epoch var charged, burned, validators, reserve, operations, used, capacity, circulating big.Int - var demand, supply, backlog, fulfilled, weightedVelocity big.Int + var demand, supply, openingBacklog, fulfilled, expired, backlog, weightedVelocity big.Int for i, metric := range ordered { if err := metric.Validate(); err != nil || metric.Epoch != epoch || (i > 0 && ordered[i-1].ShardID == metric.ShardID) { return EpochAggregate{}, ErrEpochMetrics @@ -123,12 +134,17 @@ func AggregateEpochMetrics(metrics []ShardEpochMetrics) (EpochAggregate, error) addBig(&circulating, metric.CirculatingNativeSupply) addBig(&demand, metric.EscrowBackedComputeDemand) addBig(&supply, metric.VerifiedComputeSupply) - addBig(&backlog, metric.ComputeBacklog) + addBig(&openingBacklog, metric.OpeningComputeBacklog) addBig(&fulfilled, metric.ComputeFulfilled) + addBig(&expired, metric.ComputeExpired) + addBig(&backlog, metric.ComputeBacklog) term := new(big.Int).Mul(new(big.Int).SetUint64(metric.CirculatingNativeSupply), new(big.Int).SetUint64(uint64(metric.AgeWeightedVelocityBps))) weightedVelocity.Add(&weightedVelocity, term) } - values := []*big.Int{&charged, &burned, &validators, &reserve, &operations, &used, &capacity, &circulating, &demand, &supply, &backlog, &fulfilled} + values := []*big.Int{ + &charged, &burned, &validators, &reserve, &operations, &used, &capacity, &circulating, + &demand, &supply, &openingBacklog, &fulfilled, &expired, &backlog, + } for _, value := range values { if !value.IsUint64() { return EpochAggregate{}, ErrEpochMetrics @@ -147,10 +163,18 @@ func AggregateEpochMetrics(metrics []ShardEpochMetrics) (EpochAggregate, error) CirculatingNativeSupply: circulating.Uint64(), EscrowBackedComputeDemand: demand.Uint64(), VerifiedComputeSupply: supply.Uint64(), - ComputeBacklog: backlog.Uint64(), + OpeningComputeBacklog: openingBacklog.Uint64(), ComputeFulfilled: fulfilled.Uint64(), + ComputeExpired: expired.Uint64(), + ComputeBacklog: backlog.Uint64(), } - if out.ResourceCapacity == 0 { + if out.ResourceCapacity == 0 || !validComputeFlow( + out.OpeningComputeBacklog, + out.EscrowBackedComputeDemand, + out.ComputeFulfilled, + out.ComputeExpired, + out.ComputeBacklog, + ) { return EpochAggregate{}, ErrEpochMetrics } out.ResourceUtilizationBps = ratioBps(out.ResourceUsed, out.ResourceCapacity) @@ -188,7 +212,7 @@ func (a EpochAggregate) MonetaryMetrics(totalSupply, stakedSupply, protocolReser func (a EpochAggregate) ComputeMarketMetrics(computePriceTrendBps int32, computeIndexReliable bool) ComputeMarketMetrics { return ComputeMarketMetrics{ - EscrowBackedDemandUnits: a.EscrowBackedComputeDemand, + EscrowBackedDemandUnits: a.OpeningComputeBacklog + a.EscrowBackedComputeDemand, VerifiedSupplyUnits: a.VerifiedComputeSupply, BacklogUnits: a.ComputeBacklog, FulfilledUnits: a.ComputeFulfilled, @@ -198,6 +222,14 @@ func (a EpochAggregate) ComputeMarketMetrics(computePriceTrendBps int32, compute } } +func validComputeFlow(opening, demand, fulfilled, expired, closing uint64) bool { + available := new(big.Int).Add(new(big.Int).SetUint64(opening), new(big.Int).SetUint64(demand)) + resolved := new(big.Int).SetUint64(fulfilled) + resolved.Add(resolved, new(big.Int).SetUint64(expired)) + resolved.Add(resolved, new(big.Int).SetUint64(closing)) + return available.IsUint64() && resolved.IsUint64() && available.Cmp(resolved) == 0 +} + func addBig(target *big.Int, value uint64) { target.Add(target, new(big.Int).SetUint64(value)) } From 634703d00044e92932f9a31da67a2446bd36d8c1 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:11:57 +0200 Subject: [PATCH 205/274] Encode cross-epoch compute backlog accounting --- internal/v2/economics/epoch_wire.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/v2/economics/epoch_wire.go b/internal/v2/economics/epoch_wire.go index 0c62cca0..f747915a 100644 --- a/internal/v2/economics/epoch_wire.go +++ b/internal/v2/economics/epoch_wire.go @@ -8,8 +8,13 @@ import ( func (a EpochAggregate) CanonicalBytes() ([]byte, error) { if a.Epoch == 0 || a.ShardCount == 0 || a.ResourceCapacity == 0 || a.ResourceUsed > a.ResourceCapacity || a.ResourceUtilizationBps > BasisPoints || a.ComputeUtilizationBps > BasisPoints || a.AgeWeightedVelocityBps > 10*BasisPoints || - a.ComputeFulfilled > a.VerifiedComputeSupply || a.ComputeFulfilled > a.EscrowBackedComputeDemand || - a.ComputeBacklog > a.EscrowBackedComputeDemand-a.ComputeFulfilled { + a.ComputeFulfilled > a.VerifiedComputeSupply || !validComputeFlow( + a.OpeningComputeBacklog, + a.EscrowBackedComputeDemand, + a.ComputeFulfilled, + a.ComputeExpired, + a.ComputeBacklog, + ) { return nil, ErrEpochMetrics } if a.BurnedFees > a.ChargedFees || a.ValidatorFees > a.ChargedFees-a.BurnedFees || @@ -31,8 +36,10 @@ func (a EpochAggregate) CanonicalBytes() ([]byte, error) { w.U32(a.AgeWeightedVelocityBps) w.U64(a.EscrowBackedComputeDemand) w.U64(a.VerifiedComputeSupply) - w.U64(a.ComputeBacklog) + w.U64(a.OpeningComputeBacklog) w.U64(a.ComputeFulfilled) + w.U64(a.ComputeExpired) + w.U64(a.ComputeBacklog) w.U32(a.ComputeUtilizationBps) return w.BytesCopy(), nil } From 576f5effec9b6b525b3582886374bb77711c04e6 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:12:20 +0200 Subject: [PATCH 206/274] Test compute backlog flow across epochs --- internal/v2/economics/epoch_test.go | 30 ++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/internal/v2/economics/epoch_test.go b/internal/v2/economics/epoch_test.go index 526e41a1..21524d94 100644 --- a/internal/v2/economics/epoch_test.go +++ b/internal/v2/economics/epoch_test.go @@ -9,14 +9,16 @@ func TestAggregateEpochMetricsSeparatesChainAndComputeUtilization(t *testing.T) ChargedFees: 100, BurnedFees: 40, ValidatorFees: 50, ReserveFees: 10, FinalizedOperations: 1_000, ResourceUsed: 50, ResourceCapacity: 100, CirculatingNativeSupply: 800, AgeWeightedVelocityBps: 2_000, - EscrowBackedComputeDemand: 100, VerifiedComputeSupply: 80, ComputeBacklog: 20, ComputeFulfilled: 70, + EscrowBackedComputeDemand: 100, VerifiedComputeSupply: 80, + ComputeFulfilled: 70, ComputeExpired: 10, ComputeBacklog: 20, }, { Version: EpochMetricsVersion, Epoch: 7, ShardID: 1, ChargedFees: 50, BurnedFees: 20, ValidatorFees: 25, ReserveFees: 5, FinalizedOperations: 500, ResourceUsed: 10, ResourceCapacity: 100, CirculatingNativeSupply: 200, AgeWeightedVelocityBps: 8_000, - EscrowBackedComputeDemand: 50, VerifiedComputeSupply: 40, ComputeBacklog: 10, ComputeFulfilled: 30, + EscrowBackedComputeDemand: 50, VerifiedComputeSupply: 40, + ComputeFulfilled: 30, ComputeExpired: 10, ComputeBacklog: 10, }, } aggregate, err := AggregateEpochMetrics(metrics) @@ -35,12 +37,34 @@ func TestAggregateEpochMetricsSeparatesChainAndComputeUtilization(t *testing.T) if aggregate.AgeWeightedVelocityBps != 3_200 { t.Fatalf("velocity must be supply-weighted across shards, got %d", aggregate.AgeWeightedVelocityBps) } + if aggregate.ComputeExpired != 20 { + t.Fatalf("expired compute = %d, want 20", aggregate.ComputeExpired) + } market := aggregate.ComputeMarketMetrics(500, true) if market.UtilizationBps != aggregate.ComputeUtilizationBps || market.EscrowBackedDemandUnits != 150 || market.VerifiedSupplyUnits != 120 { t.Fatalf("unexpected compute market projection: %#v", market) } } +func TestShardEpochMetricsCarriesBacklogAcrossEpochs(t *testing.T) { + metrics := ShardEpochMetrics{ + Version: EpochMetricsVersion, Epoch: 2, ResourceCapacity: 100, + OpeningComputeBacklog: 100, EscrowBackedComputeDemand: 20, VerifiedComputeSupply: 100, + ComputeFulfilled: 80, ComputeExpired: 10, ComputeBacklog: 30, + } + if err := metrics.Validate(); err != nil { + t.Fatal(err) + } + market, err := AggregateEpochMetrics([]ShardEpochMetrics{metrics}) + if err != nil { + t.Fatal(err) + } + projected := market.ComputeMarketMetrics(0, false) + if projected.EscrowBackedDemandUnits != 120 || projected.BacklogUnits != 30 || projected.FulfilledUnits != 80 { + t.Fatalf("unexpected carried demand: %#v", projected) + } +} + func TestShardEpochMetricsRejectsInconsistentAccounting(t *testing.T) { base := ShardEpochMetrics{ Version: EpochMetricsVersion, Epoch: 1, ResourceCapacity: 100, @@ -58,7 +82,7 @@ func TestShardEpochMetricsRejectsInconsistentAccounting(t *testing.T) { badDemand := base badDemand.ComputeBacklog = 41 if err := badDemand.Validate(); err != ErrEpochMetrics { - t.Fatalf("overlapping backlog/fulfilled demand accepted: %v", err) + t.Fatalf("broken compute flow accepted: %v", err) } } From 46e744899a80337ac5277b358e86a4dcee20add7 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:13:02 +0200 Subject: [PATCH 207/274] Gate ZCSI on authenticated compute supply --- internal/v2/economics/compute_scarcity.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/v2/economics/compute_scarcity.go b/internal/v2/economics/compute_scarcity.go index 978fd7fc..970d40d0 100644 --- a/internal/v2/economics/compute_scarcity.go +++ b/internal/v2/economics/compute_scarcity.go @@ -22,6 +22,7 @@ type ComputeScarcityConfig struct { type ComputeMarketMetrics struct { EscrowBackedDemandUnits uint64 VerifiedSupplyUnits uint64 + VerifiedSupplyReliable bool BacklogUnits uint64 FulfilledUnits uint64 UtilizationBps uint32 @@ -57,7 +58,8 @@ func DefaultComputeScarcityConfig() ComputeScarcityConfig { // BuildComputeScarcity calculates the Zephyr Compute Scarcity Index (ZCSI). // Demand must represent standardized, escrow-backed work. Supply must represent // standardized, benchmarked and collateralized capacity. Advertised prices or -// self-reported peak FLOPS are not valid inputs. +// self-reported peak FLOPS are not valid inputs. An unauthenticated supply can +// still be observed as telemetry, but it can never make ZCSI reliable. func BuildComputeScarcity(epoch uint64, metrics ComputeMarketMetrics, cfg ComputeScarcityConfig) (ComputeScarcitySnapshot, error) { if epoch == 0 || cfg.UtilizationTargetBps > BasisPoints || cfg.MaxAbsScoreBps == 0 || cfg.MaxAbsScoreBps > BasisPoints || metrics.UtilizationBps > BasisPoints || metrics.BacklogUnits > metrics.EscrowBackedDemandUnits || @@ -109,7 +111,9 @@ func BuildComputeScarcity(epoch uint64, metrics ComputeMarketMetrics, cfg Comput return ComputeScarcitySnapshot{}, ErrComputeScarcity } out.ScoreBps = clampSigned(int64(weighted)/int64(effectiveWeight), int32(cfg.MaxAbsScoreBps)) - out.Reliable = metrics.EscrowBackedDemandUnits >= cfg.MinDemandUnits && metrics.VerifiedSupplyUnits >= cfg.MinSupplyUnits + out.Reliable = metrics.VerifiedSupplyReliable && + metrics.EscrowBackedDemandUnits >= cfg.MinDemandUnits && + metrics.VerifiedSupplyUnits >= cfg.MinSupplyUnits return out, nil } From e9a69bdd296d3e2d5aef3a6d57bec58e05abb48d Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:13:46 +0200 Subject: [PATCH 208/274] Track compute supply reliability in epoch metrics --- internal/v2/economics/epoch.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/v2/economics/epoch.go b/internal/v2/economics/epoch.go index 6d791049..5fca6378 100644 --- a/internal/v2/economics/epoch.go +++ b/internal/v2/economics/epoch.go @@ -28,6 +28,7 @@ type ShardEpochMetrics struct { AgeWeightedVelocityBps uint32 EscrowBackedComputeDemand uint64 VerifiedComputeSupply uint64 + ComputeSupplyReliable bool OpeningComputeBacklog uint64 ComputeFulfilled uint64 ComputeExpired uint64 @@ -37,7 +38,7 @@ type ShardEpochMetrics struct { func (m ShardEpochMetrics) Validate() error { if m.Version != EpochMetricsVersion || m.Epoch == 0 || m.ResourceCapacity == 0 || m.ResourceUsed > m.ResourceCapacity || m.AgeWeightedVelocityBps > 10*BasisPoints || - m.ComputeFulfilled > m.VerifiedComputeSupply || !validComputeFlow( + (m.ComputeSupplyReliable && m.ComputeFulfilled > m.VerifiedComputeSupply) || !validComputeFlow( m.OpeningComputeBacklog, m.EscrowBackedComputeDemand, m.ComputeFulfilled, @@ -74,6 +75,7 @@ func (m ShardEpochMetrics) CanonicalBytes() ([]byte, error) { w.U32(m.AgeWeightedVelocityBps) w.U64(m.EscrowBackedComputeDemand) w.U64(m.VerifiedComputeSupply) + w.Bool(m.ComputeSupplyReliable) w.U64(m.OpeningComputeBacklog) w.U64(m.ComputeFulfilled) w.U64(m.ComputeExpired) @@ -104,6 +106,7 @@ type EpochAggregate struct { AgeWeightedVelocityBps uint32 EscrowBackedComputeDemand uint64 VerifiedComputeSupply uint64 + ComputeSupplyReliable bool OpeningComputeBacklog uint64 ComputeFulfilled uint64 ComputeExpired uint64 @@ -120,6 +123,7 @@ func AggregateEpochMetrics(metrics []ShardEpochMetrics) (EpochAggregate, error) epoch := ordered[0].Epoch var charged, burned, validators, reserve, operations, used, capacity, circulating big.Int var demand, supply, openingBacklog, fulfilled, expired, backlog, weightedVelocity big.Int + supplyReliable := true for i, metric := range ordered { if err := metric.Validate(); err != nil || metric.Epoch != epoch || (i > 0 && ordered[i-1].ShardID == metric.ShardID) { return EpochAggregate{}, ErrEpochMetrics @@ -138,6 +142,7 @@ func AggregateEpochMetrics(metrics []ShardEpochMetrics) (EpochAggregate, error) addBig(&fulfilled, metric.ComputeFulfilled) addBig(&expired, metric.ComputeExpired) addBig(&backlog, metric.ComputeBacklog) + supplyReliable = supplyReliable && metric.ComputeSupplyReliable term := new(big.Int).Mul(new(big.Int).SetUint64(metric.CirculatingNativeSupply), new(big.Int).SetUint64(uint64(metric.AgeWeightedVelocityBps))) weightedVelocity.Add(&weightedVelocity, term) } @@ -163,6 +168,7 @@ func AggregateEpochMetrics(metrics []ShardEpochMetrics) (EpochAggregate, error) CirculatingNativeSupply: circulating.Uint64(), EscrowBackedComputeDemand: demand.Uint64(), VerifiedComputeSupply: supply.Uint64(), + ComputeSupplyReliable: supplyReliable, OpeningComputeBacklog: openingBacklog.Uint64(), ComputeFulfilled: fulfilled.Uint64(), ComputeExpired: expired.Uint64(), @@ -177,6 +183,9 @@ func AggregateEpochMetrics(metrics []ShardEpochMetrics) (EpochAggregate, error) ) { return EpochAggregate{}, ErrEpochMetrics } + if out.ComputeSupplyReliable && out.ComputeFulfilled > out.VerifiedComputeSupply { + return EpochAggregate{}, ErrEpochMetrics + } out.ResourceUtilizationBps = ratioBps(out.ResourceUsed, out.ResourceCapacity) if out.VerifiedComputeSupply != 0 { out.ComputeUtilizationBps = ratioBps(out.ComputeFulfilled, out.VerifiedComputeSupply) @@ -214,6 +223,7 @@ func (a EpochAggregate) ComputeMarketMetrics(computePriceTrendBps int32, compute return ComputeMarketMetrics{ EscrowBackedDemandUnits: a.OpeningComputeBacklog + a.EscrowBackedComputeDemand, VerifiedSupplyUnits: a.VerifiedComputeSupply, + VerifiedSupplyReliable: a.ComputeSupplyReliable, BacklogUnits: a.ComputeBacklog, FulfilledUnits: a.ComputeFulfilled, UtilizationBps: a.ComputeUtilizationBps, From 10fcff0b934e7b5b83e10805ff181bcddddf0fac Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:13:59 +0200 Subject: [PATCH 209/274] Commit compute supply reliability in epoch wire --- internal/v2/economics/epoch_wire.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/v2/economics/epoch_wire.go b/internal/v2/economics/epoch_wire.go index f747915a..cd468e63 100644 --- a/internal/v2/economics/epoch_wire.go +++ b/internal/v2/economics/epoch_wire.go @@ -8,7 +8,7 @@ import ( func (a EpochAggregate) CanonicalBytes() ([]byte, error) { if a.Epoch == 0 || a.ShardCount == 0 || a.ResourceCapacity == 0 || a.ResourceUsed > a.ResourceCapacity || a.ResourceUtilizationBps > BasisPoints || a.ComputeUtilizationBps > BasisPoints || a.AgeWeightedVelocityBps > 10*BasisPoints || - a.ComputeFulfilled > a.VerifiedComputeSupply || !validComputeFlow( + (a.ComputeSupplyReliable && a.ComputeFulfilled > a.VerifiedComputeSupply) || !validComputeFlow( a.OpeningComputeBacklog, a.EscrowBackedComputeDemand, a.ComputeFulfilled, @@ -36,6 +36,7 @@ func (a EpochAggregate) CanonicalBytes() ([]byte, error) { w.U32(a.AgeWeightedVelocityBps) w.U64(a.EscrowBackedComputeDemand) w.U64(a.VerifiedComputeSupply) + w.Bool(a.ComputeSupplyReliable) w.U64(a.OpeningComputeBacklog) w.U64(a.ComputeFulfilled) w.U64(a.ComputeExpired) From d18ba9caff3c864f77a29b32d406ccc5708f5cd5 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:14:21 +0200 Subject: [PATCH 210/274] Test compute supply reliability propagation --- internal/v2/economics/epoch_test.go | 37 +++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/internal/v2/economics/epoch_test.go b/internal/v2/economics/epoch_test.go index 21524d94..7080c14c 100644 --- a/internal/v2/economics/epoch_test.go +++ b/internal/v2/economics/epoch_test.go @@ -9,7 +9,7 @@ func TestAggregateEpochMetricsSeparatesChainAndComputeUtilization(t *testing.T) ChargedFees: 100, BurnedFees: 40, ValidatorFees: 50, ReserveFees: 10, FinalizedOperations: 1_000, ResourceUsed: 50, ResourceCapacity: 100, CirculatingNativeSupply: 800, AgeWeightedVelocityBps: 2_000, - EscrowBackedComputeDemand: 100, VerifiedComputeSupply: 80, + EscrowBackedComputeDemand: 100, VerifiedComputeSupply: 80, ComputeSupplyReliable: true, ComputeFulfilled: 70, ComputeExpired: 10, ComputeBacklog: 20, }, { @@ -17,7 +17,7 @@ func TestAggregateEpochMetricsSeparatesChainAndComputeUtilization(t *testing.T) ChargedFees: 50, BurnedFees: 20, ValidatorFees: 25, ReserveFees: 5, FinalizedOperations: 500, ResourceUsed: 10, ResourceCapacity: 100, CirculatingNativeSupply: 200, AgeWeightedVelocityBps: 8_000, - EscrowBackedComputeDemand: 50, VerifiedComputeSupply: 40, + EscrowBackedComputeDemand: 50, VerifiedComputeSupply: 40, ComputeSupplyReliable: true, ComputeFulfilled: 30, ComputeExpired: 10, ComputeBacklog: 10, }, } @@ -37,11 +37,11 @@ func TestAggregateEpochMetricsSeparatesChainAndComputeUtilization(t *testing.T) if aggregate.AgeWeightedVelocityBps != 3_200 { t.Fatalf("velocity must be supply-weighted across shards, got %d", aggregate.AgeWeightedVelocityBps) } - if aggregate.ComputeExpired != 20 { - t.Fatalf("expired compute = %d, want 20", aggregate.ComputeExpired) + if aggregate.ComputeExpired != 20 || !aggregate.ComputeSupplyReliable { + t.Fatalf("unexpected compute aggregate: %#v", aggregate) } market := aggregate.ComputeMarketMetrics(500, true) - if market.UtilizationBps != aggregate.ComputeUtilizationBps || market.EscrowBackedDemandUnits != 150 || market.VerifiedSupplyUnits != 120 { + if market.UtilizationBps != aggregate.ComputeUtilizationBps || market.EscrowBackedDemandUnits != 150 || market.VerifiedSupplyUnits != 120 || !market.VerifiedSupplyReliable { t.Fatalf("unexpected compute market projection: %#v", market) } } @@ -49,7 +49,8 @@ func TestAggregateEpochMetricsSeparatesChainAndComputeUtilization(t *testing.T) func TestShardEpochMetricsCarriesBacklogAcrossEpochs(t *testing.T) { metrics := ShardEpochMetrics{ Version: EpochMetricsVersion, Epoch: 2, ResourceCapacity: 100, - OpeningComputeBacklog: 100, EscrowBackedComputeDemand: 20, VerifiedComputeSupply: 100, + OpeningComputeBacklog: 100, EscrowBackedComputeDemand: 20, + VerifiedComputeSupply: 100, ComputeSupplyReliable: true, ComputeFulfilled: 80, ComputeExpired: 10, ComputeBacklog: 30, } if err := metrics.Validate(); err != nil { @@ -65,11 +66,28 @@ func TestShardEpochMetricsCarriesBacklogAcrossEpochs(t *testing.T) { } } +func TestUnauthenticatedComputeSupplyCannotBecomeReliable(t *testing.T) { + metrics := ShardEpochMetrics{ + Version: EpochMetricsVersion, Epoch: 1, ResourceCapacity: 100, + EscrowBackedComputeDemand: 100, VerifiedComputeSupply: 1_000, + ComputeFulfilled: 60, ComputeBacklog: 40, + } + aggregate, err := AggregateEpochMetrics([]ShardEpochMetrics{metrics}) + if err != nil { + t.Fatal(err) + } + market := aggregate.ComputeMarketMetrics(0, true) + if market.VerifiedSupplyReliable { + t.Fatal("unauthenticated compute supply became reliable") + } +} + func TestShardEpochMetricsRejectsInconsistentAccounting(t *testing.T) { base := ShardEpochMetrics{ Version: EpochMetricsVersion, Epoch: 1, ResourceCapacity: 100, ChargedFees: 10, BurnedFees: 4, ValidatorFees: 5, ReserveFees: 1, - EscrowBackedComputeDemand: 100, VerifiedComputeSupply: 100, ComputeFulfilled: 60, ComputeBacklog: 40, + EscrowBackedComputeDemand: 100, VerifiedComputeSupply: 100, ComputeSupplyReliable: true, + ComputeFulfilled: 60, ComputeBacklog: 40, } if err := base.Validate(); err != nil { t.Fatal(err) @@ -84,6 +102,11 @@ func TestShardEpochMetricsRejectsInconsistentAccounting(t *testing.T) { if err := badDemand.Validate(); err != ErrEpochMetrics { t.Fatalf("broken compute flow accepted: %v", err) } + badSupply := base + badSupply.VerifiedComputeSupply = 59 + if err := badSupply.Validate(); err != ErrEpochMetrics { + t.Fatalf("fulfilled work above authenticated supply accepted: %v", err) + } } func TestAggregateEpochMetricsRejectsDuplicateShard(t *testing.T) { From 033f8689145293dc8f5f77d7463097ea4e1c11a6 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:14:44 +0200 Subject: [PATCH 211/274] Test authenticated compute supply gate --- .../v2/economics/compute_scarcity_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/v2/economics/compute_scarcity_test.go b/internal/v2/economics/compute_scarcity_test.go index bb383624..7476328b 100644 --- a/internal/v2/economics/compute_scarcity_test.go +++ b/internal/v2/economics/compute_scarcity_test.go @@ -6,6 +6,7 @@ func TestComputeScarcityRisesWhenVerifiedDemandExceedsSupply(t *testing.T) { metrics := ComputeMarketMetrics{ EscrowBackedDemandUnits: 2_000, VerifiedSupplyUnits: 1_000, + VerifiedSupplyReliable: true, BacklogUnits: 500, FulfilledUnits: 1_500, UtilizationBps: 9_000, @@ -29,6 +30,7 @@ func TestComputeScarcityIgnoresUnreliablePriceSignal(t *testing.T) { metrics := ComputeMarketMetrics{ EscrowBackedDemandUnits: 2_000, VerifiedSupplyUnits: 2_000, + VerifiedSupplyReliable: true, FulfilledUnits: 2_000, UtilizationBps: cfg.UtilizationTargetBps, ComputePriceTrendBps: 10_000, @@ -55,6 +57,23 @@ func TestComputeScarcityRejectsImpossibleSettlementMetrics(t *testing.T) { } } +func TestUnauthenticatedSupplyCannotMakeZCSIReliable(t *testing.T) { + cfg := DefaultComputeScarcityConfig() + metrics := ComputeMarketMetrics{ + EscrowBackedDemandUnits: cfg.MinDemandUnits, + VerifiedSupplyUnits: cfg.MinSupplyUnits, + VerifiedSupplyReliable: false, + BacklogUnits: cfg.MinDemandUnits, + } + snapshot, err := BuildComputeScarcity(1, metrics, cfg) + if err != nil { + t.Fatal(err) + } + if snapshot.Reliable { + t.Fatal("unauthenticated compute supply made ZCSI reliable") + } +} + func TestComputeFeedbackModesKeepActivationShadowed(t *testing.T) { monetary := DefaultShadowPolicy() metrics := MonetaryMetrics{ From d4915199d9d85db6fbc38014fbb5d604df2c69b5 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:16:44 +0200 Subject: [PATCH 212/274] Derive economic epochs from finalized v2 execution --- internal/v2/economics/finalized_collector.go | 408 +++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 internal/v2/economics/finalized_collector.go diff --git a/internal/v2/economics/finalized_collector.go b/internal/v2/economics/finalized_collector.go new file mode 100644 index 00000000..ba0c4396 --- /dev/null +++ b/internal/v2/economics/finalized_collector.go @@ -0,0 +1,408 @@ +package economics + +import ( + "errors" + "math" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/execution" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrFinalizedEconomics = errors.New("invalid finalized Zephyr economic observation") + +// EpochCollectorConfig contains only deterministic protocol/shadow-policy +// inputs. Initial circulating supply and opening backlog are prior finalized +// state, not mutable operator telemetry. +type EpochCollectorConfig struct { + Epoch uint64 + ShardCount uint32 + NativeToken types.TokenID + InitialCirculatingSupply map[uint32]uint64 + OpeningComputeBacklog map[uint32]uint64 + ResourceCapacityPerBlock map[uint32]uint64 + VelocityPolicy VelocityPolicy + FeePolicy FeePolicy + WorkRegistry *compute.WorkRegistry +} + +type FinalizedShardObservation struct { + Transactions []tx.Transaction + Results []execution.Result + Imports []sharding.CrossShardReceipt + DataBytes uint64 + ComputeCapacityUnits uint64 + ComputeCapacityReliable bool +} + +type shardEpochAccumulator struct { + fees FeeAllocation + operations uint64 + resourceUsed uint64 + resourceCapacity uint64 + openingBacklog uint64 + newDemand uint64 + fulfilled uint64 + expired uint64 + closingBacklog uint64 + computeSupply uint64 + computeSupplyReliable bool + velocity *VelocityAccumulator +} + +type EpochCollector struct { + config EpochCollectorConfig + shards map[uint32]*shardEpochAccumulator + supply map[uint32]uint64 + verifiedWork []compute.VerifiedWork + lastHeight uint64 +} + +func NewEpochCollector(config EpochCollectorConfig) (*EpochCollector, error) { + if config.Epoch == 0 || config.ShardCount == 0 || types.IsZero32([32]byte(config.NativeToken)) || + config.FeePolicy != CompatibilityFeePolicy() { + return nil, ErrFinalizedEconomics + } + if _, err := NewVelocityAccumulator(config.VelocityPolicy); err != nil { + return nil, err + } + collector := &EpochCollector{ + config: config, + shards: make(map[uint32]*shardEpochAccumulator, config.ShardCount), + supply: make(map[uint32]uint64, config.ShardCount), + } + for shard := uint32(0); shard < config.ShardCount; shard++ { + capacity := config.ResourceCapacityPerBlock[shard] + if capacity == 0 { + return nil, ErrFinalizedEconomics + } + velocity, err := NewVelocityAccumulator(config.VelocityPolicy) + if err != nil { + return nil, err + } + opening := config.OpeningComputeBacklog[shard] + collector.shards[shard] = &shardEpochAccumulator{ + openingBacklog: opening, + closingBacklog: opening, + computeSupplyReliable: true, + velocity: velocity, + } + collector.supply[shard] = config.InitialCirculatingSupply[shard] + } + return collector, nil +} + +// ObserveFinalizedBlock mutates telemetry only after the caller has normal +// consensus finality. It deliberately does not mutate world state or mint ZPH. +func (c *EpochCollector) ObserveFinalizedBlock(height uint64, observations map[uint32]FinalizedShardObservation) error { + if c == nil || height == 0 || height <= c.lastHeight { + return ErrFinalizedEconomics + } + for shard := uint32(0); shard < c.config.ShardCount; shard++ { + observation := observations[shard] + if len(observation.Transactions) != len(observation.Results) { + return ErrFinalizedEconomics + } + acc := c.shards[shard] + if err := c.addResourceCapacity(acc, c.config.ResourceCapacityPerBlock[shard]); err != nil { + return err + } + if err := addTo(&acc.computeSupply, observation.ComputeCapacityUnits); err != nil { + return err + } + acc.computeSupplyReliable = acc.computeSupplyReliable && observation.ComputeCapacityReliable + + for i := range observation.Transactions { + transaction := observation.Transactions[i] + result := observation.Results[i] + if transaction.ShardID != shard || result.TxID != transaction.ID() { + return ErrFinalizedEconomics + } + if err := c.observeFinalizedTransaction(height, shard, transaction, result, acc); err != nil { + return err + } + } + for _, receipt := range observation.Imports { + if receipt.DestinationShard != shard || receipt.SourceShard >= c.config.ShardCount || receipt.SourceShard == receipt.DestinationShard { + return ErrFinalizedEconomics + } + if err := c.observeImportedReceipt(receipt); err != nil { + return err + } + if err := addTo(&acc.operations, 1); err != nil { + return err + } + if err := addTo(&acc.resourceUsed, 2); err != nil { + return err + } + } + if observation.DataBytes > 0 { + dataUnits := (observation.DataBytes + 1023) / 1024 + if observation.DataBytes > math.MaxUint64-1023 { + return ErrFinalizedEconomics + } + if err := addTo(&acc.resourceUsed, dataUnits); err != nil { + return err + } + } + if acc.resourceUsed > acc.resourceCapacity { + return ErrFinalizedEconomics + } + } + c.lastHeight = height + return nil +} + +func (c *EpochCollector) observeFinalizedTransaction(height uint64, shard uint32, transaction tx.Transaction, result execution.Result, acc *shardEpochAccumulator) error { + allocation, err := SplitFee(transaction.Fee, c.config.FeePolicy) + if err != nil || allocation.Validators != 0 || allocation.Reserve != 0 { + return ErrFinalizedEconomics + } + if err := addTo(&acc.fees.Total, allocation.Total); err != nil { + return err + } + if err := addTo(&acc.fees.Burn, allocation.Burn); err != nil { + return err + } + if c.supply[shard] < allocation.Burn { + return ErrFinalizedEconomics + } + c.supply[shard] -= allocation.Burn + if err := addTo(&acc.operations, uint64(len(transaction.Operations))); err != nil { + return err + } + resourceUnits, err := finalizedTransactionResourceUnits(transaction, result) + if err != nil { + return err + } + if err := addTo(&acc.resourceUsed, resourceUnits); err != nil { + return err + } + + consumed := make(map[types.ObjectID]struct{}, len(result.Consumed)) + for _, id := range result.Consumed { + consumed[id] = struct{}{} + } + for _, witness := range transaction.Witnesses { + if _, wasConsumed := consumed[witness.Object.ID]; !wasConsumed || witness.Object.Kind != object.KindCoin { + continue + } + coin, err := object.ParseCoin(witness.Object.Data) + if err != nil { + return err + } + if coin.Token == c.config.NativeToken { + if err := acc.velocity.ObserveCoin(coin, height); err != nil { + return err + } + } + } + return c.observeComputeLifecycle(transaction, result, acc) +} + +func finalizedTransactionResourceUnits(transaction tx.Transaction, result execution.Result) (uint64, error) { + intentBytes := uint64(len(transaction.IntentBytes())) + units := uint64(1) + (intentBytes+1023)/1024 + parts := []uint64{ + uint64(len(transaction.Inputs)), + uint64(len(result.Consumed)), + uint64(len(result.Created)), + uint64(len(result.Outbound)), + } + for _, value := range parts { + if math.MaxUint64-units < value { + return 0, ErrFinalizedEconomics + } + units += value + } + return units, nil +} + +func (c *EpochCollector) observeImportedReceipt(receipt sharding.CrossShardReceipt) error { + if receipt.Output.Kind != object.KindCoin { + return nil + } + coin, err := object.ParseCoin(receipt.Output.Data) + if err != nil { + return err + } + if coin.Token != c.config.NativeToken { + return nil + } + if c.supply[receipt.SourceShard] < coin.Amount || math.MaxUint64-c.supply[receipt.DestinationShard] < coin.Amount { + return ErrFinalizedEconomics + } + c.supply[receipt.SourceShard] -= coin.Amount + c.supply[receipt.DestinationShard] += coin.Amount + return nil +} + +func (c *EpochCollector) observeComputeLifecycle(transaction tx.Transaction, result execution.Result, acc *shardEpochAccumulator) error { + if c.config.WorkRegistry == nil || len(transaction.Operations) != 1 { + return nil + } + op := transaction.Operations[0] + switch op.Kind { + case tx.OpComputeJob: + job, err := compute.ParseJob(op.Payload) + if err != nil { + return err + } + spec, ok := c.config.WorkRegistry.Resolve(job.WorkloadHash) + if !ok { + return nil + } + if err := addTo(&acc.newDemand, spec.Units); err != nil { + return err + } + return addTo(&acc.closingBacklog, spec.Units) + case tx.OpComputeFinalize, tx.OpComputeResolveReplicated: + record, ok, err := computeJobWitness(transaction) + if err != nil || !ok { + return ErrFinalizedEconomics + } + spec, registered := c.config.WorkRegistry.Resolve(record.Job.WorkloadHash) + if !registered { + return nil + } + receipt, ok, err := settlementReceiptForJob(result.Created, record.ID) + if err != nil || !ok { + return ErrFinalizedEconomics + } + verified, err := compute.ObserveFinalizedSettlement(record, receipt, c.config.WorkRegistry) + if err != nil || verified.Units != spec.Units { + return ErrFinalizedEconomics + } + if acc.closingBacklog < spec.Units { + return ErrFinalizedEconomics + } + acc.closingBacklog -= spec.Units + if err := addTo(&acc.fulfilled, spec.Units); err != nil { + return err + } + c.verifiedWork = append(c.verifiedWork, verified) + case tx.OpComputeExpire: + record, ok, err := computeJobWitness(transaction) + if err != nil || !ok { + return ErrFinalizedEconomics + } + spec, registered := c.config.WorkRegistry.Resolve(record.Job.WorkloadHash) + if !registered { + return nil + } + if acc.closingBacklog < spec.Units { + return ErrFinalizedEconomics + } + acc.closingBacklog -= spec.Units + return addTo(&acc.expired, spec.Units) + } + return nil +} + +func computeJobWitness(transaction tx.Transaction) (compute.OnChainJob, bool, error) { + var record compute.OnChainJob + found := false + for _, witness := range transaction.Witnesses { + if witness.Object.Kind != object.KindComputeJob { + continue + } + if found { + return compute.OnChainJob{}, false, ErrFinalizedEconomics + } + parsed, err := compute.ParseOnChainJob(witness.Object.Data) + if err != nil { + return compute.OnChainJob{}, false, err + } + record, found = parsed, true + } + return record, found, nil +} + +func settlementReceiptForJob(created []object.Object, jobID types.JobID) (compute.SettlementReceipt, bool, error) { + for _, createdObject := range created { + if createdObject.Kind != object.KindSystem { + continue + } + receipt, err := compute.ParseSettlementReceipt(createdObject.Data) + if err != nil { + continue + } + if receipt.JobID == jobID { + return receipt, true, nil + } + } + return compute.SettlementReceipt{}, false, nil +} + +func (c *EpochCollector) FinalizeEpoch() ([]ShardEpochMetrics, []compute.VerifiedWork, error) { + if c == nil { + return nil, nil, ErrFinalizedEconomics + } + metrics := make([]ShardEpochMetrics, 0, c.config.ShardCount) + for shard := uint32(0); shard < c.config.ShardCount; shard++ { + acc := c.shards[shard] + velocity, err := acc.velocity.Finalize(c.supply[shard]) + if err != nil { + return nil, nil, err + } + metric := ShardEpochMetrics{ + Version: EpochMetricsVersion, Epoch: c.config.Epoch, ShardID: shard, + ChargedFees: acc.fees.Total, BurnedFees: acc.fees.Burn, + ValidatorFees: acc.fees.Validators, ReserveFees: acc.fees.Reserve, + FinalizedOperations: acc.operations, ResourceUsed: acc.resourceUsed, ResourceCapacity: acc.resourceCapacity, + CirculatingNativeSupply: c.supply[shard], AgeWeightedVelocityBps: velocity.AgeWeightedVelocityBps, + EscrowBackedComputeDemand: acc.newDemand, VerifiedComputeSupply: acc.computeSupply, + ComputeSupplyReliable: acc.computeSupplyReliable, OpeningComputeBacklog: acc.openingBacklog, + ComputeFulfilled: acc.fulfilled, ComputeExpired: acc.expired, ComputeBacklog: acc.closingBacklog, + } + if err := metric.Validate(); err != nil { + return nil, nil, err + } + metrics = append(metrics, metric) + } + return metrics, append([]compute.VerifiedWork(nil), c.verifiedWork...), nil +} + +func (c *EpochCollector) AdvanceEpoch(next uint64) error { + if c == nil || next != c.config.Epoch+1 { + return ErrFinalizedEconomics + } + c.config.Epoch = next + c.verifiedWork = nil + for shard := uint32(0); shard < c.config.ShardCount; shard++ { + prior := c.shards[shard] + velocity, err := NewVelocityAccumulator(c.config.VelocityPolicy) + if err != nil { + return err + } + c.shards[shard] = &shardEpochAccumulator{ + openingBacklog: prior.closingBacklog, + closingBacklog: prior.closingBacklog, + computeSupplyReliable: true, + velocity: velocity, + } + } + return nil +} + +func (c *EpochCollector) CirculatingSupply(shard uint32) (uint64, bool) { + if c == nil || shard >= c.config.ShardCount { + return 0, false + } + return c.supply[shard], true +} + +func (c *EpochCollector) addResourceCapacity(acc *shardEpochAccumulator, value uint64) error { + return addTo(&acc.resourceCapacity, value) +} + +func addTo(target *uint64, value uint64) error { + if target == nil || math.MaxUint64-*target < value { + return ErrFinalizedEconomics + } + *target += value + return nil +} From 39fce7b5c3d25ddfc237bb5aa3a1b98b6ad57cc5 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:18:05 +0200 Subject: [PATCH 213/274] Make finalized economics collection atomic --- internal/v2/economics/finalized_collector.go | 72 ++++++++++++++++---- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/internal/v2/economics/finalized_collector.go b/internal/v2/economics/finalized_collector.go index ba0c4396..8ae896f9 100644 --- a/internal/v2/economics/finalized_collector.go +++ b/internal/v2/economics/finalized_collector.go @@ -30,11 +30,11 @@ type EpochCollectorConfig struct { } type FinalizedShardObservation struct { - Transactions []tx.Transaction - Results []execution.Result - Imports []sharding.CrossShardReceipt - DataBytes uint64 - ComputeCapacityUnits uint64 + Transactions []tx.Transaction + Results []execution.Result + Imports []sharding.CrossShardReceipt + DataBytes uint64 + ComputeCapacityUnits uint64 ComputeCapacityReliable bool } @@ -95,10 +95,26 @@ func NewEpochCollector(config EpochCollectorConfig) (*EpochCollector, error) { return collector, nil } -// ObserveFinalizedBlock mutates telemetry only after the caller has normal -// consensus finality. It deliberately does not mutate world state or mint ZPH. +// ObserveFinalizedBlock applies a full global-block observation atomically. +// Telemetry changes only after normal consensus finality and never mutates +// world state or mints ZPH. func (c *EpochCollector) ObserveFinalizedBlock(height uint64, observations map[uint32]FinalizedShardObservation) error { - if c == nil || height == 0 || height <= c.lastHeight { + if c == nil { + return ErrFinalizedEconomics + } + preview := c.clone() + if preview == nil { + return ErrFinalizedEconomics + } + if err := preview.observeFinalizedBlock(height, observations); err != nil { + return err + } + *c = *preview + return nil +} + +func (c *EpochCollector) observeFinalizedBlock(height uint64, observations map[uint32]FinalizedShardObservation) error { + if height == 0 || height <= c.lastHeight { return ErrFinalizedEconomics } for shard := uint32(0); shard < c.config.ShardCount; shard++ { @@ -107,7 +123,7 @@ func (c *EpochCollector) ObserveFinalizedBlock(height uint64, observations map[u return ErrFinalizedEconomics } acc := c.shards[shard] - if err := c.addResourceCapacity(acc, c.config.ResourceCapacityPerBlock[shard]); err != nil { + if err := addTo(&acc.resourceCapacity, c.config.ResourceCapacityPerBlock[shard]); err != nil { return err } if err := addTo(&acc.computeSupply, observation.ComputeCapacityUnits); err != nil { @@ -140,10 +156,10 @@ func (c *EpochCollector) ObserveFinalizedBlock(height uint64, observations map[u } } if observation.DataBytes > 0 { - dataUnits := (observation.DataBytes + 1023) / 1024 if observation.DataBytes > math.MaxUint64-1023 { return ErrFinalizedEconomics } + dataUnits := (observation.DataBytes + 1023) / 1024 if err := addTo(&acc.resourceUsed, dataUnits); err != nil { return err } @@ -344,9 +360,13 @@ func (c *EpochCollector) FinalizeEpoch() ([]ShardEpochMetrics, []compute.Verifie metrics := make([]ShardEpochMetrics, 0, c.config.ShardCount) for shard := uint32(0); shard < c.config.ShardCount; shard++ { acc := c.shards[shard] - velocity, err := acc.velocity.Finalize(c.supply[shard]) - if err != nil { - return nil, nil, err + velocity := VelocitySnapshot{} + if c.supply[shard] > 0 { + var err error + velocity, err = acc.velocity.Finalize(c.supply[shard]) + if err != nil { + return nil, nil, err + } } metric := ShardEpochMetrics{ Version: EpochMetricsVersion, Epoch: c.config.Epoch, ShardID: shard, @@ -395,8 +415,30 @@ func (c *EpochCollector) CirculatingSupply(shard uint32) (uint64, bool) { return c.supply[shard], true } -func (c *EpochCollector) addResourceCapacity(acc *shardEpochAccumulator, value uint64) error { - return addTo(&acc.resourceCapacity, value) +func (c *EpochCollector) clone() *EpochCollector { + if c == nil { + return nil + } + out := &EpochCollector{ + config: c.config, + shards: make(map[uint32]*shardEpochAccumulator, len(c.shards)), + supply: make(map[uint32]uint64, len(c.supply)), + verifiedWork: append([]compute.VerifiedWork(nil), c.verifiedWork...), + lastHeight: c.lastHeight, + } + for shard, amount := range c.supply { + out.supply[shard] = amount + } + for shard, source := range c.shards { + copyAccumulator := *source + if source.velocity != nil { + velocityCopy := *source.velocity + velocityCopy.weightedValueBps.Set(&source.velocity.weightedValueBps) + copyAccumulator.velocity = &velocityCopy + } + out.shards[shard] = ©Accumulator + } + return out } func addTo(target *uint64, value uint64) error { From 025c794cdcaed9205f724543824364cf5d470469 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:18:51 +0200 Subject: [PATCH 214/274] Test finalized economic epoch collector --- .../v2/economics/finalized_collector_test.go | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 internal/v2/economics/finalized_collector_test.go diff --git a/internal/v2/economics/finalized_collector_test.go b/internal/v2/economics/finalized_collector_test.go new file mode 100644 index 00000000..8e221284 --- /dev/null +++ b/internal/v2/economics/finalized_collector_test.go @@ -0,0 +1,189 @@ +package economics + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/execution" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func testCollectorConfig(shards uint32, native types.TokenID) EpochCollectorConfig { + supply := make(map[uint32]uint64, shards) + capacity := make(map[uint32]uint64, shards) + for shard := uint32(0); shard < shards; shard++ { + capacity[shard] = 10_000 + } + return EpochCollectorConfig{ + Epoch: 1, ShardCount: shards, NativeToken: native, + InitialCirculatingSupply: supply, + OpeningComputeBacklog: make(map[uint32]uint64), + ResourceCapacityPerBlock: capacity, + VelocityPolicy: VelocityPolicy{ + MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000, + }, + FeePolicy: CompatibilityFeePolicy(), + } +} + +func TestEpochCollectorMovesCrossShardSupplyOnImport(t *testing.T) { + native := types.TokenID{1} + cfg := testCollectorConfig(2, native) + cfg.InitialCirculatingSupply[0] = 1_000 + collector, err := NewEpochCollector(cfg) + if err != nil { + t.Fatal(err) + } + output, err := object.NewCoinOutputAtHeight(types.AccountID{2}, native, 100, 1) + if err != nil { + t.Fatal(err) + } + receipt := sharding.CrossShardReceipt{SourceShard: 0, DestinationShard: 1, Output: output} + if err := collector.ObserveFinalizedBlock(1, map[uint32]FinalizedShardObservation{ + 1: {Imports: []sharding.CrossShardReceipt{receipt}}, + }); err != nil { + t.Fatal(err) + } + left, _ := collector.CirculatingSupply(0) + right, _ := collector.CirculatingSupply(1) + if left != 900 || right != 100 { + t.Fatalf("unexpected shard supply attribution: %d/%d", left, right) + } + metrics, _, err := collector.FinalizeEpoch() + if err != nil { + t.Fatal(err) + } + if metrics[0].CirculatingNativeSupply+metrics[1].CirculatingNativeSupply != 1_000 { + t.Fatal("cross-shard import changed global supply") + } +} + +func TestEpochCollectorRejectsBlockAtomically(t *testing.T) { + native := types.TokenID{1} + cfg := testCollectorConfig(2, native) + cfg.InitialCirculatingSupply[0] = 100 + collector, err := NewEpochCollector(cfg) + if err != nil { + t.Fatal(err) + } + output, err := object.NewCoinOutputAtHeight(types.AccountID{2}, native, 200, 1) + if err != nil { + t.Fatal(err) + } + err = collector.ObserveFinalizedBlock(1, map[uint32]FinalizedShardObservation{ + 1: {Imports: []sharding.CrossShardReceipt{{SourceShard: 0, DestinationShard: 1, Output: output}}}, + }) + if err == nil { + t.Fatal("expected impossible cross-shard supply rejection") + } + left, _ := collector.CirculatingSupply(0) + right, _ := collector.CirculatingSupply(1) + if left != 100 || right != 0 { + t.Fatalf("failed observation partially mutated collector: %d/%d", left, right) + } +} + +func TestEpochCollectorCarriesComputeBacklogAndBuildsVerifiedWork(t *testing.T) { + native := types.TokenID{1} + workload := types.Hash{3} + registry, err := compute.NewWorkRegistry([]compute.WorkSpec{{ + Version: compute.WorkSpecVersion, Class: compute.WorkAITraining, Units: 100, + WorkloadHash: workload, BenchmarkHash: types.Hash{4}, + Vector: compute.WorkVector{TensorUnits: 100}, + }}) + if err != nil { + t.Fatal(err) + } + cfg := testCollectorConfig(1, native) + cfg.InitialCirculatingSupply[0] = 1_000 + cfg.WorkRegistry = registry + collector, err := NewEpochCollector(cfg) + if err != nil { + t.Fatal(err) + } + + job := compute.Job{ + Owner: types.AccountID{5}, WorkloadHash: workload, InputRoot: types.Hash{6}, + Resources: compute.Resources{CPUCores: 1, MemoryMiB: 1}, MaxPrice: 30, + CollateralRequired: 2, Verification: compute.VerificationReplicated, + DeadlineHeight: 100, Replicas: 2, + } + jobRaw, err := job.MarshalBinary() + if err != nil { + t.Fatal(err) + } + post := tx.Transaction{ShardID: 0, Operations: []tx.Operation{{Kind: tx.OpComputeJob, Payload: jobRaw}}} + postResult := execution.Result{TxID: post.ID()} + if err := collector.ObserveFinalizedBlock(1, map[uint32]FinalizedShardObservation{ + 0: {Transactions: []tx.Transaction{post}, Results: []execution.Result{postResult}}, + }); err != nil { + t.Fatal(err) + } + first, _, err := collector.FinalizeEpoch() + if err != nil { + t.Fatal(err) + } + if first[0].EscrowBackedComputeDemand != 100 || first[0].ComputeBacklog != 100 { + t.Fatalf("unexpected first epoch compute flow: %#v", first[0]) + } + if err := collector.AdvanceEpoch(2); err != nil { + t.Fatal(err) + } + + jobID := types.JobID{7} + providers := []types.AccountID{{8}, {9}} + root := types.Hash{10} + record := compute.OnChainJob{ + ID: jobID, Job: job, Escrow: 30, Status: compute.JobAwaitingVerification, + Assignments: []compute.Assignment{ + {OfferID: types.Hash{11}, Provider: providers[0], Price: 10}, + {OfferID: types.Hash{12}, Provider: providers[1], Price: 20}, + }, + Results: []compute.Result{ + {JobID: jobID, Provider: providers[0], ResultRoot: root, CompletedHeight: 2}, + {JobID: jobID, Provider: providers[1], ResultRoot: root, CompletedHeight: 2}, + }, + } + recordRaw, err := record.MarshalBinary() + if err != nil { + t.Fatal(err) + } + receipt := compute.SettlementReceipt{ + JobID: jobID, ResultRoot: root, + Payments: map[types.AccountID]uint64{providers[0]: 10, providers[1]: 20}, + } + jobObject := object.Object{ID: types.ObjectID{13}, Version: 1, Owner: job.Owner, Kind: object.KindComputeJob, Data: recordRaw} + finalize := tx.Transaction{ + ShardID: 0, + Operations: []tx.Operation{{Kind: tx.OpComputeFinalize}}, + Witnesses: []tx.Witness{{Object: jobObject}}, + } + finalizeResult := execution.Result{ + TxID: finalize.ID(), + Created: []object.Object{{ID: types.ObjectID{14}, Version: 1, Kind: object.KindSystem, Data: receipt.MarshalBinary()}}, + } + if err := collector.ObserveFinalizedBlock(2, map[uint32]FinalizedShardObservation{ + 0: { + Transactions: []tx.Transaction{finalize}, Results: []execution.Result{finalizeResult}, + ComputeCapacityUnits: 100, ComputeCapacityReliable: true, + }, + }); err != nil { + t.Fatal(err) + } + second, verified, err := collector.FinalizeEpoch() + if err != nil { + t.Fatal(err) + } + if second[0].OpeningComputeBacklog != 100 || second[0].ComputeFulfilled != 100 || second[0].ComputeBacklog != 0 { + t.Fatalf("unexpected second epoch compute flow: %#v", second[0]) + } + if !second[0].ComputeSupplyReliable || second[0].VerifiedComputeSupply != 100 { + t.Fatalf("unexpected compute supply: %#v", second[0]) + } + if len(verified) != 1 || verified[0].PaidZPH != 30 || verified[0].Units != 100 { + t.Fatalf("unexpected verified work: %#v", verified) + } +} From 756be433e4f354bdb612167715125314ed270f68 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:22:43 +0200 Subject: [PATCH 215/274] Format compute settlement receipt tests --- .../v2/compute/settlement_receipt_test.go | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/v2/compute/settlement_receipt_test.go b/internal/v2/compute/settlement_receipt_test.go index 3b56fb5c..33080a95 100644 --- a/internal/v2/compute/settlement_receipt_test.go +++ b/internal/v2/compute/settlement_receipt_test.go @@ -8,11 +8,11 @@ import ( func TestSettlementReceiptRoundTrip(t *testing.T) { receipt := SettlementReceipt{ - JobID: types.JobID{1}, - ResultRoot: types.Hash{2}, - Payments: map[types.AccountID]uint64{{3}: 10, {4}: 20}, - Refund: 5, - Slashed: map[types.AccountID]uint64{{5}: 7}, + JobID: types.JobID{1}, + ResultRoot: types.Hash{2}, + Payments: map[types.AccountID]uint64{{3}: 10, {4}: 20}, + Refund: 5, + Slashed: map[types.AccountID]uint64{{5}: 7}, SlashReward: 7, } parsed, err := ParseSettlementReceipt(receipt.MarshalBinary()) @@ -71,11 +71,11 @@ func TestObserveFinalizedSettlementReplicatedMajority(t *testing.T) { }, } receipt := SettlementReceipt{ - JobID: jobID, - ResultRoot: root, - Payments: map[types.AccountID]uint64{providers[0]: 10, providers[1]: 20}, - Refund: 30, - Slashed: map[types.AccountID]uint64{providers[2]: 4}, + JobID: jobID, + ResultRoot: root, + Payments: map[types.AccountID]uint64{providers[0]: 10, providers[1]: 20}, + Refund: 30, + Slashed: map[types.AccountID]uint64{providers[2]: 4}, SlashReward: 4, } observed, err := ObserveFinalizedSettlement(record, receipt, registry) From e532035b59e608be54681d32a77122b8f92de226 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:23:39 +0200 Subject: [PATCH 216/274] Format economic epoch accounting --- internal/v2/economics/epoch.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/v2/economics/epoch.go b/internal/v2/economics/epoch.go index 5fca6378..164aa168 100644 --- a/internal/v2/economics/epoch.go +++ b/internal/v2/economics/epoch.go @@ -39,12 +39,12 @@ func (m ShardEpochMetrics) Validate() error { if m.Version != EpochMetricsVersion || m.Epoch == 0 || m.ResourceCapacity == 0 || m.ResourceUsed > m.ResourceCapacity || m.AgeWeightedVelocityBps > 10*BasisPoints || (m.ComputeSupplyReliable && m.ComputeFulfilled > m.VerifiedComputeSupply) || !validComputeFlow( - m.OpeningComputeBacklog, - m.EscrowBackedComputeDemand, - m.ComputeFulfilled, - m.ComputeExpired, - m.ComputeBacklog, - ) { + m.OpeningComputeBacklog, + m.EscrowBackedComputeDemand, + m.ComputeFulfilled, + m.ComputeExpired, + m.ComputeBacklog, + ) { return ErrEpochMetrics } feeTotal := new(big.Int).SetUint64(m.BurnedFees) From 9555a96f25df7e7bd521ec7710b471ca44c77494 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:24:04 +0200 Subject: [PATCH 217/274] Format economic epoch wire encoding --- internal/v2/economics/epoch_wire.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/v2/economics/epoch_wire.go b/internal/v2/economics/epoch_wire.go index cd468e63..8aa4905b 100644 --- a/internal/v2/economics/epoch_wire.go +++ b/internal/v2/economics/epoch_wire.go @@ -9,12 +9,12 @@ func (a EpochAggregate) CanonicalBytes() ([]byte, error) { if a.Epoch == 0 || a.ShardCount == 0 || a.ResourceCapacity == 0 || a.ResourceUsed > a.ResourceCapacity || a.ResourceUtilizationBps > BasisPoints || a.ComputeUtilizationBps > BasisPoints || a.AgeWeightedVelocityBps > 10*BasisPoints || (a.ComputeSupplyReliable && a.ComputeFulfilled > a.VerifiedComputeSupply) || !validComputeFlow( - a.OpeningComputeBacklog, - a.EscrowBackedComputeDemand, - a.ComputeFulfilled, - a.ComputeExpired, - a.ComputeBacklog, - ) { + a.OpeningComputeBacklog, + a.EscrowBackedComputeDemand, + a.ComputeFulfilled, + a.ComputeExpired, + a.ComputeBacklog, + ) { return nil, ErrEpochMetrics } if a.BurnedFees > a.ChargedFees || a.ValidatorFees > a.ChargedFees-a.BurnedFees || From 62622597a96b61b05c23ef8e85454521845cc8e8 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:24:57 +0200 Subject: [PATCH 218/274] Format finalized economics collector tests --- internal/v2/economics/finalized_collector_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/v2/economics/finalized_collector_test.go b/internal/v2/economics/finalized_collector_test.go index 8e221284..4987af3d 100644 --- a/internal/v2/economics/finalized_collector_test.go +++ b/internal/v2/economics/finalized_collector_test.go @@ -157,12 +157,12 @@ func TestEpochCollectorCarriesComputeBacklogAndBuildsVerifiedWork(t *testing.T) } jobObject := object.Object{ID: types.ObjectID{13}, Version: 1, Owner: job.Owner, Kind: object.KindComputeJob, Data: recordRaw} finalize := tx.Transaction{ - ShardID: 0, + ShardID: 0, Operations: []tx.Operation{{Kind: tx.OpComputeFinalize}}, - Witnesses: []tx.Witness{{Object: jobObject}}, + Witnesses: []tx.Witness{{Object: jobObject}}, } finalizeResult := execution.Result{ - TxID: finalize.ID(), + TxID: finalize.ID(), Created: []object.Object{{ID: types.ObjectID{14}, Version: 1, Kind: object.KindSystem, Data: receipt.MarshalBinary()}}, } if err := collector.ObserveFinalizedBlock(2, map[uint32]FinalizedShardObservation{ From 1fad49ddd9f01fe22e1372a1af1714b94c0ab123 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:27:24 +0200 Subject: [PATCH 219/274] Add finalized economics preview boundary --- .../economics/finalized_collector_preview.go | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 internal/v2/economics/finalized_collector_preview.go diff --git a/internal/v2/economics/finalized_collector_preview.go b/internal/v2/economics/finalized_collector_preview.go new file mode 100644 index 00000000..3f106d8b --- /dev/null +++ b/internal/v2/economics/finalized_collector_preview.go @@ -0,0 +1,34 @@ +package economics + +import "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + +// PreviewFinalizedBlock evaluates a finalized block observation without +// advancing the receiver. A node can prepare this preview before durable state +// Apply and promote it only after the block transition succeeds. +func (c *EpochCollector) PreviewFinalizedBlock(height uint64, observations map[uint32]FinalizedShardObservation) (*EpochCollector, error) { + if c == nil { + return nil, ErrFinalizedEconomics + } + preview := c.clone() + if preview == nil { + return nil, ErrFinalizedEconomics + } + if err := preview.observeFinalizedBlock(height, observations); err != nil { + return nil, err + } + return preview, nil +} + +func (c *EpochCollector) ShardCount() uint32 { + if c == nil { + return 0 + } + return c.config.ShardCount +} + +func (c *EpochCollector) NativeTokenID() types.TokenID { + if c == nil { + return types.TokenID{} + } + return c.config.NativeToken +} From d12df1881beac8713c0117c3625871408bf172a0 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:28:06 +0200 Subject: [PATCH 220/274] Add previewable shadow economic epoch engine --- internal/v2/economics/shadow_epoch_engine.go | 170 +++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 internal/v2/economics/shadow_epoch_engine.go diff --git a/internal/v2/economics/shadow_epoch_engine.go b/internal/v2/economics/shadow_epoch_engine.go new file mode 100644 index 00000000..a47a3538 --- /dev/null +++ b/internal/v2/economics/shadow_epoch_engine.go @@ -0,0 +1,170 @@ +package economics + +import ( + "errors" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrShadowEpochEngine = errors.New("invalid Zephyr shadow epoch transition") + +type ShadowEpochEngineConfig struct { + ComputeIndex ComputeIndexConfig + ComputeScarcity ComputeScarcityConfig + Monetary MonetaryPolicy + ComputeFeedback ComputeFeedbackPolicy +} + +type MonetaryBalanceSnapshot struct { + TotalSupply uint64 + StakedSupply uint64 + ProtocolReserve uint64 + BaseFee uint64 +} + +type ShadowEpochPreview struct { + Aggregate EpochAggregate + ComputeIndex ComputeIndexSnapshot + ComputePriceTrendBps int32 + ComputeScarcity ComputeScarcitySnapshot + MonetaryDecision MonetaryDecision + ComputeFeedback ComputeFeedbackDecision + State MonetaryEpochState + Consumed []types.ObjectID + Created []object.Object +} + +type ShadowEpochEngine struct { + Network types.NetworkID + config ShadowEpochEngineConfig + priorIndex ComputeIndexSnapshot + previous *MonetaryEpochState +} + +func NewShadowEpochEngine(network types.NetworkID, config ShadowEpochEngineConfig) (*ShadowEpochEngine, error) { + if types.IsZero32([32]byte(network)) { + return nil, ErrShadowEpochEngine + } + if config.Monetary.Validate() != nil || config.ComputeScarcity.MaxAbsScoreBps == 0 || config.ComputeFeedback.Validate() != nil { + return nil, ErrShadowEpochEngine + } + return &ShadowEpochEngine{Network: network, config: config}, nil +} + +// PreviewCloseEpoch deterministically composes finalized shard telemetry into +// ZCPI, ZCSI, ZAMP and a Merkle-authenticated shadow MonetaryEpochState. It does +// not advance controller history. The returned object delta must be included in +// a normal consensus candidate and finalized before Accept is called. +func (e *ShadowEpochEngine) PreviewCloseEpoch(metrics []ShardEpochMetrics, verifiedWork []compute.VerifiedWork, balances MonetaryBalanceSnapshot) (ShadowEpochPreview, error) { + if e == nil { + return ShadowEpochPreview{}, ErrShadowEpochEngine + } + aggregate, err := AggregateEpochMetrics(metrics) + if err != nil { + return ShadowEpochPreview{}, err + } + if e.previous == nil { + if aggregate.Epoch != 1 { + return ShadowEpochPreview{}, ErrShadowEpochEngine + } + } else if aggregate.Epoch != e.previous.Epoch+1 { + return ShadowEpochPreview{}, ErrShadowEpochEngine + } + + index, err := BuildComputeIndex(aggregate.Epoch, verifiedWork, e.priorIndex, e.config.ComputeIndex) + if err != nil { + return ShadowEpochPreview{}, err + } + trend := ComputePriceTrendBps(index.BasketPriceQ9, e.priorIndex.BasketPriceQ9) + scarcity, err := BuildComputeScarcity(aggregate.Epoch, aggregate.ComputeMarketMetrics(trend, index.Reliable), e.config.ComputeScarcity) + if err != nil { + return ShadowEpochPreview{}, err + } + state, monetary, feedback, err := BuildShadowMonetaryEpochState( + e.Network, + e.previous, + aggregate, + balances.TotalSupply, + balances.StakedSupply, + balances.ProtocolReserve, + index.BasketPriceQ9, + trend, + index.Reliable, + scarcity, + e.config.Monetary, + e.config.ComputeFeedback, + balances.BaseFee, + ) + if err != nil { + return ShadowEpochPreview{}, err + } + + var previousObject *object.Object + if e.previous != nil { + prior, err := e.previous.Object() + if err != nil { + return ShadowEpochPreview{}, err + } + previousObject = &prior + } + consumed, created, err := ShadowMonetaryTransition(previousObject, state) + if err != nil { + return ShadowEpochPreview{}, err + } + return ShadowEpochPreview{ + Aggregate: aggregate, ComputeIndex: index, ComputePriceTrendBps: trend, + ComputeScarcity: scarcity, MonetaryDecision: monetary, ComputeFeedback: feedback, + State: state, Consumed: consumed, Created: created, + }, nil +} + +// Accept advances the shadow controller only after the caller has finalized the +// exact monetary object returned by PreviewCloseEpoch. +func (e *ShadowEpochEngine) Accept(preview ShadowEpochPreview) error { + if e == nil || preview.State.Network != e.Network || preview.State.Epoch != preview.Aggregate.Epoch || + preview.ComputeIndex.Epoch != preview.State.Epoch || preview.ComputeScarcity.Epoch != preview.State.Epoch || + preview.State.Validate() != nil { + return ErrShadowEpochEngine + } + if e.previous == nil { + if preview.State.Epoch != 1 || !types.IsZero32([32]byte(preview.State.PreviousStateHash)) { + return ErrShadowEpochEngine + } + } else { + if preview.State.Epoch != e.previous.Epoch+1 { + return ErrShadowEpochEngine + } + priorHash, err := e.previous.Hash() + if err != nil || preview.State.PreviousStateHash != priorHash { + return ErrShadowEpochEngine + } + } + aggregateHash, err := preview.Aggregate.Hash() + if err != nil || aggregateHash != preview.State.AggregateHash { + return ErrShadowEpochEngine + } + if preview.State.ComputeIndexQ9 != preview.ComputeIndex.BasketPriceQ9 || preview.State.ComputeIndexReliable != preview.ComputeIndex.Reliable { + return ErrShadowEpochEngine + } + + stateCopy := preview.State + e.previous = &stateCopy + e.priorIndex = preview.ComputeIndex + return nil +} + +func (e *ShadowEpochEngine) PreviousState() (MonetaryEpochState, bool) { + if e == nil || e.previous == nil { + return MonetaryEpochState{}, false + } + return *e.previous, true +} + +func (e *ShadowEpochEngine) PriorComputeIndex() ComputeIndexSnapshot { + if e == nil { + return ComputeIndexSnapshot{} + } + return e.priorIndex +} From ea5d5b83a7f4c539d366cf133943aef2ec6b1f55 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:29:10 +0200 Subject: [PATCH 221/274] Validate shadow epoch engine through preview inputs --- internal/v2/economics/shadow_epoch_engine.go | 30 +++++++++----------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/internal/v2/economics/shadow_epoch_engine.go b/internal/v2/economics/shadow_epoch_engine.go index a47a3538..54ccb451 100644 --- a/internal/v2/economics/shadow_epoch_engine.go +++ b/internal/v2/economics/shadow_epoch_engine.go @@ -25,29 +25,27 @@ type MonetaryBalanceSnapshot struct { } type ShadowEpochPreview struct { - Aggregate EpochAggregate - ComputeIndex ComputeIndexSnapshot + Aggregate EpochAggregate + ComputeIndex ComputeIndexSnapshot ComputePriceTrendBps int32 - ComputeScarcity ComputeScarcitySnapshot - MonetaryDecision MonetaryDecision - ComputeFeedback ComputeFeedbackDecision - State MonetaryEpochState - Consumed []types.ObjectID - Created []object.Object + ComputeScarcity ComputeScarcitySnapshot + MonetaryDecision MonetaryDecision + ComputeFeedback ComputeFeedbackDecision + State MonetaryEpochState + Consumed []types.ObjectID + Created []object.Object } type ShadowEpochEngine struct { - Network types.NetworkID - config ShadowEpochEngineConfig - priorIndex ComputeIndexSnapshot - previous *MonetaryEpochState + Network types.NetworkID + config ShadowEpochEngineConfig + priorIndex ComputeIndexSnapshot + previous *MonetaryEpochState } func NewShadowEpochEngine(network types.NetworkID, config ShadowEpochEngineConfig) (*ShadowEpochEngine, error) { - if types.IsZero32([32]byte(network)) { - return nil, ErrShadowEpochEngine - } - if config.Monetary.Validate() != nil || config.ComputeScarcity.MaxAbsScoreBps == 0 || config.ComputeFeedback.Validate() != nil { + if types.IsZero32([32]byte(network)) || config.Monetary.EpochsPerYear == 0 || config.Monetary.OperationsTarget == 0 || + config.ComputeScarcity.MaxAbsScoreBps == 0 || config.ComputeFeedback.Mode > ComputeFeedbackMonetaryBand { return nil, ErrShadowEpochEngine } return &ShadowEpochEngine{Network: network, config: config}, nil From 8b6628dbcad7ce34d3fe64f4fa7dbc5bcd7eb478 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:29:41 +0200 Subject: [PATCH 222/274] Test shadow economic epoch preview and acceptance --- .../v2/economics/shadow_epoch_engine_test.go | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 internal/v2/economics/shadow_epoch_engine_test.go diff --git a/internal/v2/economics/shadow_epoch_engine_test.go b/internal/v2/economics/shadow_epoch_engine_test.go new file mode 100644 index 00000000..9d0c3adf --- /dev/null +++ b/internal/v2/economics/shadow_epoch_engine_test.go @@ -0,0 +1,126 @@ +package economics + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func testShadowEpochConfig() ShadowEpochEngineConfig { + index := ComputeIndexConfig{MinSamplesPerClass: 1, MinCoverageBps: 10_000, EWMABps: 10_000} + index.WeightsBps[compute.WorkCPUGeneral] = 10_000 + return ShadowEpochEngineConfig{ + ComputeIndex: index, + ComputeScarcity: DefaultComputeScarcityConfig(), + Monetary: DefaultShadowPolicy(), + ComputeFeedback: DefaultComputeFeedbackPolicy(ComputeFeedbackRewardRouting), + } +} + +func testEpochMetric(epoch uint64, opening, demand, fulfilled, closing uint64) ShardEpochMetrics { + return ShardEpochMetrics{ + Version: EpochMetricsVersion, Epoch: epoch, ShardID: 0, + ChargedFees: 10, BurnedFees: 10, + FinalizedOperations: 10_000, ResourceUsed: 50, ResourceCapacity: 100, + CirculatingNativeSupply: 900, AgeWeightedVelocityBps: 5_000, + EscrowBackedComputeDemand: demand, VerifiedComputeSupply: 1_000, + ComputeSupplyReliable: true, OpeningComputeBacklog: opening, + ComputeFulfilled: fulfilled, ComputeBacklog: closing, + } +} + +func testVerifiedCPUWork(jobByte byte, paid uint64) compute.VerifiedWork { + return compute.VerifiedWork{ + JobID: types.JobID{jobByte}, Class: compute.WorkCPUGeneral, Units: 100, + PaidZPH: paid, Verification: compute.VerificationReplicated, ResultRoot: types.Hash{jobByte + 1}, + } +} + +func TestShadowEpochEnginePreviewDoesNotAdvanceUntilAccepted(t *testing.T) { + engine, err := NewShadowEpochEngine(types.NetworkID{1}, testShadowEpochConfig()) + if err != nil { + t.Fatal(err) + } + balances := MonetaryBalanceSnapshot{TotalSupply: 1_000, StakedSupply: 400, ProtocolReserve: 100, BaseFee: 1} + preview, err := engine.PreviewCloseEpoch( + []ShardEpochMetrics{testEpochMetric(1, 0, 1_000, 500, 500)}, + []compute.VerifiedWork{testVerifiedCPUWork(1, 200)}, + balances, + ) + if err != nil { + t.Fatal(err) + } + if _, ok := engine.PreviousState(); ok { + t.Fatal("preview advanced controller history") + } + if !preview.State.Shadow || preview.State.Epoch != 1 || len(preview.Consumed) != 0 || len(preview.Created) != 1 { + t.Fatalf("unexpected first preview: %#v", preview) + } + if !preview.ComputeIndex.Reliable || !preview.ComputeScarcity.Reliable { + t.Fatalf("expected reliable test economics: index=%#v scarcity=%#v", preview.ComputeIndex, preview.ComputeScarcity) + } + if err := engine.Accept(preview); err != nil { + t.Fatal(err) + } + state, ok := engine.PreviousState() + if !ok || state.Epoch != 1 { + t.Fatalf("accepted state missing: %#v", state) + } +} + +func TestShadowEpochEngineChainsSecondEpochObject(t *testing.T) { + engine, err := NewShadowEpochEngine(types.NetworkID{1}, testShadowEpochConfig()) + if err != nil { + t.Fatal(err) + } + balances := MonetaryBalanceSnapshot{TotalSupply: 1_000, StakedSupply: 400, ProtocolReserve: 100, BaseFee: 1} + first, err := engine.PreviewCloseEpoch( + []ShardEpochMetrics{testEpochMetric(1, 0, 1_000, 500, 500)}, + []compute.VerifiedWork{testVerifiedCPUWork(1, 200)}, + balances, + ) + if err != nil { + t.Fatal(err) + } + if err := engine.Accept(first); err != nil { + t.Fatal(err) + } + second, err := engine.PreviewCloseEpoch( + []ShardEpochMetrics{testEpochMetric(2, 500, 0, 500, 0)}, + []compute.VerifiedWork{testVerifiedCPUWork(3, 300)}, + balances, + ) + if err != nil { + t.Fatal(err) + } + if len(second.Consumed) != 1 || len(second.Created) != 1 || second.State.PreviousStateHash == (types.Hash{}) { + t.Fatalf("second epoch did not replace and chain monetary object: %#v", second) + } + if err := engine.Accept(second); err != nil { + t.Fatal(err) + } + state, _ := engine.PreviousState() + if state.Epoch != 2 { + t.Fatalf("epoch = %d, want 2", state.Epoch) + } +} + +func TestShadowEpochEngineRejectsTamperedPreview(t *testing.T) { + engine, err := NewShadowEpochEngine(types.NetworkID{1}, testShadowEpochConfig()) + if err != nil { + t.Fatal(err) + } + preview, err := engine.PreviewCloseEpoch( + []ShardEpochMetrics{testEpochMetric(1, 0, 1_000, 500, 500)}, + []compute.VerifiedWork{testVerifiedCPUWork(1, 200)}, + MonetaryBalanceSnapshot{TotalSupply: 1_000, StakedSupply: 400, ProtocolReserve: 100, BaseFee: 1}, + ) + if err != nil { + t.Fatal(err) + } + preview.State.AggregateHash[0] ^= 0xff + if err := engine.Accept(preview); err == nil { + t.Fatal("tampered aggregate commitment accepted") + } +} From ee419c3a718bb8ab4c3fd41b9fced2cc9025c96c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:30:35 +0200 Subject: [PATCH 223/274] Document finalized economic runtime pipeline --- docs/economic-runtime-v2.md | 292 ++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 docs/economic-runtime-v2.md diff --git a/docs/economic-runtime-v2.md b/docs/economic-runtime-v2.md new file mode 100644 index 00000000..124d89c4 --- /dev/null +++ b/docs/economic-runtime-v2.md @@ -0,0 +1,292 @@ +# Zephyr v2 Finalized Economic Runtime + +Status: **shadow/experimental implementation**. + +This document describes the executable path that turns finalized Zephyr v2 activity into reproducible economic telemetry for ZCPI, ZCSI and ZAMP without activating live minting. + +Companion documents: + +- `docs/tokenomics-v2.md` +- `docs/compute-economics-v2.md` +- `docs/economic-state-v2.md` +- `docs/protocol-v2-implementation-status.md` + +The governing rule remains: + +```text +measure first -> simulate second -> activate last +``` + +## 1. Finality boundary + +Economic telemetry is not allowed to advance from mempool admission, proposal construction, unfinalized votes, provider advertisements or RPC summaries. + +The intended runtime path is: + +```text +proof-carrying transactions + -> deterministic execution + -> proposal / votes / QC + -> finalized global block + -> FinalizedShardObservation + -> EpochCollector + -> ShardEpochMetrics + -> EpochAggregate + -> ZCPI + -> ZCSI + -> ZAMP shadow decision + -> pending MonetaryEpochState + -> normal Merkle state transition in a later finalized candidate +``` + +`EpochCollector` has an explicit preview boundary so a node can evaluate telemetry before durable state apply and promote the preview only when the corresponding finalized transition succeeds. + +## 2. Native-supply attribution across shards + +A cross-shard ZPH transfer must not make global supply temporarily disappear while its receipt is in flight. + +The collector therefore treats per-shard native supply as an attribution ledger: + +```text +source finalizes outbound receipt + -> global circulating supply unchanged + -> source attribution remains until import + +destination finalizes receipt import + -> source attribution -= amount + -> destination attribution += amount + -> global circulating supply unchanged +``` + +Fee burn remains a real reduction in circulating ZPH under the current compatibility fee policy. + +This attribution rule is economic telemetry. Consensus double-spend protection remains the cross-shard receipt/anti-replay protocol. + +## 3. Age-weighted velocity from finalized coins + +Only consumed native coin witnesses that actually appear in the finalized execution result enter the velocity accumulator. + +`CreatedHeight` is consensus-stamped by execution. Wallet timestamps are not trusted. + +The existing minimum-age/full-weight policy therefore continues to make rapid fresh-coin cycling contribute little or zero weight. + +## 4. Compute demand is stock-flow, not same-epoch volume + +Long AI/scientific/rendering jobs may be posted in one epoch and settle in another. Zephyr therefore does not require `fulfilled <= new demand` inside a single epoch. + +The v2 economic wire now uses the exact conservation rule: + +```text +OpeningComputeBacklog ++ EscrowBackedComputeDemand += +ComputeFulfilled ++ ComputeExpired ++ ComputeBacklog +``` + +where `ComputeBacklog` is the closing backlog carried into the next epoch. + +This avoids declaring legitimate long-running jobs invalid merely because their creation and settlement occur in different economic epochs. + +## 5. Standardized compute only + +A compute job affects standardized ZCSI demand/backlog only when its `WorkloadHash` resolves to an active `WorkSpec` in the workload registry. + +Unregistered workloads may still execute in the compute market, but they do not silently acquire a made-up normalization factor and do not enter standardized monetary telemetry. + +The standardized job unit comes from the committed `WorkSpec`/`WorkVector`, not from provider-advertised peak FLOPS. + +## 6. ZCPI from finalized settlement evidence + +The compute execution path commits a canonical `SettlementReceipt` containing: + +```text +JobID +ResultRoot +provider payments +refund +slashed collateral +slash reward +expiry flag +``` + +The economic replay path parses that receipt and recomputes the deterministic settlement from the pre-finalization `OnChainJob` witness. + +A receipt is accepted for `VerifiedWork` only when the reconstructed settlement bytes match exactly. + +For replicated-majority jobs this includes: + +- strict-majority result selection; +- payment only to providers on the accepted result root; +- refund accounting; +- deterministic slashing of dissenting collateral; +- slash-reward accounting. + +ZCPI then uses the ZPH actually paid for successfully verified standardized work. Offer prices, refunds and collateral movement are excluded from the compute price observation. + +## 7. Authenticated compute supply is a separate reliability gate + +A numeric capacity value is not enough to make ZCSI trustworthy. + +Each shard metric carries: + +```text +VerifiedComputeSupply +ComputeSupplyReliable +``` + +The numeric value can still be recorded for experiments, but `ComputeSupplyReliable=false` prevents ZCSI from becoming reliable regardless of how large the claimed capacity is. + +The production path must eventually derive reliable supply from a consensus-reproducible benchmark/collateral/availability registry. + +Until that exists, the safe runtime behavior is: + +```text +capacity telemetry may exist +ZCSI reliability = false +ZCSI monetary influence = zero +``` + +## 8. ZCSI inputs + +For an epoch, ZCSI can combine: + +```text +active standardized demand +verified standardized supply +closing backlog +fulfilled work +compute utilization +reliable ZCPI trend +``` + +Active demand is: + +```text +OpeningComputeBacklog + EscrowBackedComputeDemand +``` + +The ZCPI price component is ignored automatically when ZCPI coverage/reliability is insufficient. + +The whole ZCSI signal is not marked reliable unless the compute-supply reliability gate and minimum demand/supply thresholds pass. + +## 9. Shadow epoch engine + +`ShadowEpochEngine` composes one closed economic epoch as: + +```text +ShardEpochMetrics + -> EpochAggregate + -> BuildComputeIndex (ZCPI) + -> ComputePriceTrendBps + -> BuildComputeScarcity (ZCSI) + -> BuildShadowMonetaryEpochState (ZAMP + compute feedback) + -> ShadowMonetaryTransition +``` + +It follows a preview/accept model. + +`PreviewCloseEpoch` produces: + +- canonical aggregate; +- ZCPI snapshot; +- bounded price trend; +- ZCSI snapshot; +- ZAMP decision; +- compute-feedback decision A/B/C; +- pending `MonetaryEpochState`; +- consumed/created object delta. + +It does **not** advance controller history. + +`Accept` advances the controller only after the caller has finalized the exact monetary object through normal consensus/state finality. + +## 10. Monetary state chaining + +Each accepted shadow monetary object commits the prior accepted state through: + +```text +PreviousStateHash +``` + +The engine rejects: + +- skipped epochs; +- another network; +- a mismatched aggregate hash; +- a mismatched ZCPI snapshot; +- a broken prior-state hash. + +This is designed so a Citizen Node can eventually verify the complete economic-controller history from Merkle proofs rather than trusting a dashboard or validator RPC. + +## 11. Feedback modes remain shadow + +The three compute-feedback modes remain: + +```text +A — observe only +B — change suggested compute reward routing only +C — B plus a narrow bounded suggested inflation correction +``` + +No mode mints live ZPH in the current implementation. + +A reliable ZCSI is mandatory before modes B/C can alter even the simulated reward routing/target. + +Mode B remains the preferred first activation candidate if long-run testing eventually supports it. + +## 12. Runtime resource utilization + +The finalized collector includes a deterministic shadow resource-unit counter so chain utilization can be replayed without wall-clock measurements. + +The current reference units account for bounded protocol work such as: + +- finalized base transaction; +- transaction intent bytes; +- inputs/consumed/created objects; +- cross-shard outputs/imports; +- data-availability bytes. + +This is **not** the final production gas schedule. It exists so simulations can test controller stability using a deterministic load signal while the final resource-pricing schedule remains an activation decision. + +## 13. What is implemented now + +Executable foundations now include: + +- finalized-block economic collector; +- atomic collector preview/apply semantics; +- native supply attribution across shard receipt imports; +- compatibility full-fee-burn accounting; +- age-weighted finalized-spend velocity; +- compute backlog carry-over across epochs; +- compute expiry accounting; +- verified settlement-receipt reconstruction; +- finalized `VerifiedWork` extraction; +- independent authenticated-supply reliability bit; +- canonical v2 shard/aggregate economic metrics; +- previewable ZCPI/ZCSI/ZAMP shadow epoch engine; +- pending Merkle monetary-object delta generation; +- tests for atomic rejection, cross-shard supply conservation, multi-epoch compute backlog, verified settlement pricing and shadow-state chaining. + +## 14. What is still required + +Before monetary activation, Zephyr still needs: + +1. node-runtime wiring from successful `Runtime.Commit` into the finalized collector; +2. automatic epoch-boundary scheduling; +3. insertion of the pending `MonetaryEpochState` into a normal candidate **before** its state root/QC is finalized; +4. authenticated total-supply, staking and protocol-reserve system objects rather than operator-supplied shadow inputs; +5. production benchmarked/collateralized compute-capacity registry; +6. governance-delayed workload-registry and economic-parameter changes; +7. final gas/resource pricing and active burn/validator/reserve distribution; +8. replay/oscillation/manipulation datasets over long devnet runs; +9. Citizen wallet economic-state decoder/history UI; +10. an explicit protocol version/height gate before any live issuance or reward redistribution. + +The current invariant is therefore: + +```text +finalized data -> reproducible shadow decision +shadow decision != permission to mint +``` From da2a6b07e056a191701e897de07a5a0bf9416ae5 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:31:57 +0200 Subject: [PATCH 224/274] Wire finalized economics preview into v2 runtime commits --- internal/v2/node/runtime.go | 65 ++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/internal/v2/node/runtime.go b/internal/v2/node/runtime.go index d868eba5..13a703c4 100644 --- a/internal/v2/node/runtime.go +++ b/internal/v2/node/runtime.go @@ -6,6 +6,7 @@ import ( "sync" v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" "github.com/zephyr-chain/zephyr-chain/internal/v2/execution" "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" "github.com/zephyr-chain/zephyr-chain/internal/v2/object" @@ -46,23 +47,25 @@ type shardDelta struct { } type Candidate struct { - Header sharding.GlobalHeader - Commitments []sharding.Commitment - Results map[uint32][]execution.Result - Receipts map[uint32][]sharding.CrossShardReceipt - deltas map[uint32]shardDelta + Header sharding.GlobalHeader + Commitments []sharding.Commitment + Results map[uint32][]execution.Result + Receipts map[uint32][]sharding.CrossShardReceipt + deltas map[uint32]shardDelta + economicObservations map[uint32]economics.FinalizedShardObservation } type Runtime struct { - mu sync.Mutex - Network types.NetworkID - NativeToken types.TokenID - ValidatorRoot types.Hash - ShardCount uint32 - States map[uint32]worldstate.Backend - Workers int - Height uint64 - ParentHash types.Hash + mu sync.Mutex + Network types.NetworkID + NativeToken types.TokenID + ValidatorRoot types.Hash + ShardCount uint32 + States map[uint32]worldstate.Backend + Workers int + Height uint64 + ParentHash types.Hash + economicCollector *economics.EpochCollector } func NewRuntime(network types.NetworkID, nativeToken types.TokenID, validatorRoot types.Hash, states map[uint32]worldstate.Backend, workers int) (*Runtime, error) { @@ -84,7 +87,12 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( if height != r.Height+1 || height == 0 { return Candidate{}, ErrCandidateHeight } - candidate := Candidate{Results: make(map[uint32][]execution.Result), Receipts: make(map[uint32][]sharding.CrossShardReceipt), deltas: make(map[uint32]shardDelta)} + candidate := Candidate{ + Results: make(map[uint32][]execution.Result), + Receipts: make(map[uint32][]sharding.CrossShardReceipt), + deltas: make(map[uint32]shardDelta), + economicObservations: make(map[uint32]economics.FinalizedShardObservation), + } commitments := make([]sharding.Commitment, 0, r.ShardCount) dataLeaves := make([]types.Hash, 0, r.ShardCount) for shard := uint32(0); shard < r.ShardCount; shard++ { @@ -161,6 +169,18 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( } commitments = append(commitments, sharding.Commitment{ShardID: shard, StateRoot: newRoot, ReceiptRoot: receiptRoot, DataRoot: dataRoot}) dataLeaves = append(dataLeaves, merkle.Leaf("shard-data-root", dataRoot[:])) + + if r.economicCollector != nil { + imports := make([]sharding.CrossShardReceipt, 0, len(batch.Imports)) + for _, receiptImport := range batch.Imports { + imports = append(imports, receiptImport.Receipt) + } + candidate.economicObservations[shard] = economics.FinalizedShardObservation{ + Transactions: append([]tx.Transaction(nil), batch.Transactions...), + Results: append([]execution.Result(nil), results...), + Imports: imports, + } + } } commitmentRoot, err := sharding.CommitmentRoot(commitments) if err != nil { @@ -207,6 +227,18 @@ func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificat if err := validators.VerifyCertificate(certificate); err != nil { return sharding.GlobalHeader{}, err } + + var economicPreview *economics.EpochCollector + if r.economicCollector != nil { + if len(candidate.economicObservations) != int(r.ShardCount) { + return sharding.GlobalHeader{}, ErrCandidateState + } + economicPreview, err = r.economicCollector.PreviewFinalizedBlock(candidate.Header.Height, candidate.economicObservations) + if err != nil { + return sharding.GlobalHeader{}, err + } + } + commitments := make(map[uint32]sharding.Commitment, len(candidate.Commitments)) for _, commitment := range candidate.Commitments { commitments[commitment.ShardID] = commitment @@ -227,6 +259,9 @@ func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificat return sharding.GlobalHeader{}, ErrCandidateState } } + if economicPreview != nil { + r.economicCollector = economicPreview + } finalized := candidate.Header finalized.CertificateHash = certificate.Hash() r.Height = finalized.Height From 2357c0aaa7bd93271bb53a171a7c45721cfc519e Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:32:16 +0200 Subject: [PATCH 225/274] Expose genesis-only shadow economics runtime configuration --- internal/v2/node/economics_runtime.go | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 internal/v2/node/economics_runtime.go diff --git a/internal/v2/node/economics_runtime.go b/internal/v2/node/economics_runtime.go new file mode 100644 index 00000000..37771963 --- /dev/null +++ b/internal/v2/node/economics_runtime.go @@ -0,0 +1,36 @@ +package node + +import ( + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" +) + +// EnableShadowEconomics attaches finalized economic telemetry to the v2 runtime. +// It is intentionally genesis-only for now: attaching a collector after blocks +// have finalized would require authenticated historical replay first. +func (r *Runtime) EnableShadowEconomics(collector *economics.EpochCollector) error { + if r == nil || collector == nil { + return ErrRuntimeConfig + } + r.mu.Lock() + defer r.mu.Unlock() + if r.Height != 0 || r.economicCollector != nil || collector.ShardCount() != r.ShardCount || collector.NativeTokenID() != r.NativeToken { + return ErrRuntimeConfig + } + r.economicCollector = collector + return nil +} + +// EconomicEpochSnapshot returns the current shadow epoch inputs derived only +// from successfully finalized Runtime.Commit calls. +func (r *Runtime) EconomicEpochSnapshot() ([]economics.ShardEpochMetrics, []compute.VerifiedWork, error) { + if r == nil { + return nil, nil, ErrRuntimeConfig + } + r.mu.Lock() + defer r.mu.Unlock() + if r.economicCollector == nil { + return nil, nil, ErrRuntimeConfig + } + return r.economicCollector.FinalizeEpoch() +} From 730935b43d532ae69ffd30237c1b19a2577c98b1 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:32:36 +0200 Subject: [PATCH 226/274] Test finalized runtime economics integration --- internal/v2/node/economics_runtime_test.go | 119 +++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 internal/v2/node/economics_runtime_test.go diff --git a/internal/v2/node/economics_runtime_test.go b/internal/v2/node/economics_runtime_test.go new file mode 100644 index 00000000..1f3ae255 --- /dev/null +++ b/internal/v2/node/economics_runtime_test.go @@ -0,0 +1,119 @@ +package node + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestRuntimeEconomicsAdvancesOnlyAfterValidQCCommit(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("economics-runtime"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + validatorKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + validatorPub := elliptic.Marshal(elliptic.P256(), validatorKey.PublicKey.X, validatorKey.PublicKey.Y) + validators := v2consensus.ValidatorSet{ + Network: network, + Validators: []v2consensus.Validator{{ + ID: types.ValidatorIDFromPublicKey(validatorPub), PublicKey: validatorPub, Power: 10, + }}, + } + validatorRoot, err := validators.Root() + if err != nil { + t.Fatal(err) + } + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: worldstate.NewMemory()}, 1) + if err != nil { + t.Fatal(err) + } + collector, err := economics.NewEpochCollector(economics.EpochCollectorConfig{ + Epoch: 1, ShardCount: 1, NativeToken: native, + InitialCirculatingSupply: map[uint32]uint64{0: 1_000}, + OpeningComputeBacklog: map[uint32]uint64{}, + ResourceCapacityPerBlock: map[uint32]uint64{0: 100}, + VelocityPolicy: economics.VelocityPolicy{ + MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000, + }, + FeePolicy: economics.CompatibilityFeePolicy(), + }) + if err != nil { + t.Fatal(err) + } + if err := runtime.EnableShadowEconomics(collector); err != nil { + t.Fatal(err) + } + + candidate, err := runtime.BuildCandidate(1, nil) + if err != nil { + t.Fatal(err) + } + proposal, err := v2consensus.SignProposal(validatorKey, candidate.Header, 0) + if err != nil { + t.Fatal(err) + } + vote, err := v2consensus.SignVote(validatorKey, network, 1, 0, v2consensus.HeaderConsensusHash(candidate.Header)) + if err != nil { + t.Fatal(err) + } + certificate, err := validators.BuildCertificate(proposal, []v2consensus.Vote{vote}) + if err != nil { + t.Fatal(err) + } + if _, err := runtime.Commit(candidate, certificate, validators); err != nil { + t.Fatal(err) + } + metrics, _, err := runtime.EconomicEpochSnapshot() + if err != nil { + t.Fatal(err) + } + if len(metrics) != 1 || metrics[0].ResourceCapacity != 100 || metrics[0].CirculatingNativeSupply != 1_000 { + t.Fatalf("unexpected finalized economics: %#v", metrics) + } + + candidate2, err := runtime.BuildCandidate(2, nil) + if err != nil { + t.Fatal(err) + } + if _, err := runtime.Commit(candidate2, v2consensus.Certificate{}, validators); err == nil { + t.Fatal("invalid certificate unexpectedly committed") + } + after, _, err := runtime.EconomicEpochSnapshot() + if err != nil { + t.Fatal(err) + } + if after[0].ResourceCapacity != metrics[0].ResourceCapacity { + t.Fatalf("failed commit advanced economics: before=%#v after=%#v", metrics[0], after[0]) + } +} + +func TestRuntimeRejectsMismatchedEconomicsCollector(t *testing.T) { + network := types.NetworkID{1} + native := types.TokenID{2} + runtime, err := NewRuntime(network, native, types.Hash{3}, map[uint32]worldstate.Backend{0: worldstate.NewMemory()}, 1) + if err != nil { + t.Fatal(err) + } + collector, err := economics.NewEpochCollector(economics.EpochCollectorConfig{ + Epoch: 1, ShardCount: 1, NativeToken: types.TokenID{9}, + InitialCirculatingSupply: map[uint32]uint64{0: 1}, + ResourceCapacityPerBlock: map[uint32]uint64{0: 1}, + VelocityPolicy: economics.VelocityPolicy{ + MinAgeBlocks: 1, FullWeightAgeBlocks: 1, MaxVelocityBps: 1, + }, + FeePolicy: economics.CompatibilityFeePolicy(), + }) + if err != nil { + t.Fatal(err) + } + if err := runtime.EnableShadowEconomics(collector); err != ErrRuntimeConfig { + t.Fatalf("mismatched collector accepted: %v", err) + } +} From d22b15bbb269e17ae449150468aaa2cacfa28bae Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:36:33 +0200 Subject: [PATCH 227/274] Update monetary state fixtures for compute stock-flow --- internal/v2/economics/monetary_state_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/v2/economics/monetary_state_test.go b/internal/v2/economics/monetary_state_test.go index 7c9c2eb7..4c35c3fc 100644 --- a/internal/v2/economics/monetary_state_test.go +++ b/internal/v2/economics/monetary_state_test.go @@ -14,7 +14,7 @@ func TestShadowMonetaryEpochStateRoundTripAndNoLiveMint(t *testing.T) { ResourceUsed: 50, ResourceCapacity: 100, ResourceUtilizationBps: 5_000, CirculatingNativeSupply: 900_000_000, AgeWeightedVelocityBps: 5_000, EscrowBackedComputeDemand: 2_000, VerifiedComputeSupply: 1_000, - ComputeBacklog: 500, ComputeFulfilled: 1_000, ComputeUtilizationBps: 10_000, + ComputeBacklog: 500, ComputeFulfilled: 1_000, ComputeExpired: 500, ComputeUtilizationBps: 10_000, } scarcity, err := BuildComputeScarcity(1, aggregate.ComputeMarketMetrics(1_000, true), DefaultComputeScarcityConfig()) if err != nil { @@ -65,7 +65,7 @@ func TestShadowMonetaryEpochStateChainsPreviousEpoch(t *testing.T) { FinalizedOperations: 100, ResourceUsed: 50, ResourceCapacity: 100, ResourceUtilizationBps: 5_000, CirculatingNativeSupply: 900_000_000, AgeWeightedVelocityBps: 5_000, EscrowBackedComputeDemand: 1_000, VerifiedComputeSupply: 1_000, - ComputeFulfilled: 700, ComputeUtilizationBps: 7_000, + ComputeFulfilled: 700, ComputeBacklog: 300, ComputeUtilizationBps: 7_000, } } firstAggregate := makeAggregate(1) From 60943e8cd7c6a96aab711db694e164fb2ef618e4 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:36:46 +0200 Subject: [PATCH 228/274] Update monetary transition fixture for compute stock-flow --- internal/v2/economics/monetary_transition_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/v2/economics/monetary_transition_test.go b/internal/v2/economics/monetary_transition_test.go index e9af8f7b..1ac68e12 100644 --- a/internal/v2/economics/monetary_transition_test.go +++ b/internal/v2/economics/monetary_transition_test.go @@ -16,7 +16,8 @@ func TestShadowMonetaryTransitionCanBeFinalizedThroughStateRoot(t *testing.T) { Epoch: epoch, ShardCount: 1, ChargedFees: 10, BurnedFees: 10, FinalizedOperations: 100, ResourceUsed: 50, ResourceCapacity: 100, ResourceUtilizationBps: 5_000, CirculatingNativeSupply: 900_000_000, AgeWeightedVelocityBps: 4_000, - EscrowBackedComputeDemand: 1_000, VerifiedComputeSupply: 1_000, ComputeFulfilled: 700, ComputeUtilizationBps: 7_000, + EscrowBackedComputeDemand: 1_000, VerifiedComputeSupply: 1_000, + ComputeFulfilled: 700, ComputeBacklog: 300, ComputeUtilizationBps: 7_000, } scarcity, err := BuildComputeScarcity(epoch, aggregate.ComputeMarketMetrics(0, false), DefaultComputeScarcityConfig()) if err != nil { From 21c46ea0176c63b73aa86b03a3daad6c9545941e Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:37:13 +0200 Subject: [PATCH 229/274] Expose deep clone for finalized economics collector --- internal/v2/economics/finalized_collector_clone.go | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 internal/v2/economics/finalized_collector_clone.go diff --git a/internal/v2/economics/finalized_collector_clone.go b/internal/v2/economics/finalized_collector_clone.go new file mode 100644 index 00000000..2fb39974 --- /dev/null +++ b/internal/v2/economics/finalized_collector_clone.go @@ -0,0 +1,8 @@ +package economics + +// Clone returns an independent deep copy of the finalized economics collector. +// Runtime owners use this to prevent external callers from mutating economic +// telemetry outside the node's synchronization boundary. +func (c *EpochCollector) Clone() *EpochCollector { + return c.clone() +} From cb8601cdbfa74cb7b76f911cae45531ac293b108 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:37:30 +0200 Subject: [PATCH 230/274] Own shadow economics collector inside runtime --- internal/v2/node/economics_runtime.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/v2/node/economics_runtime.go b/internal/v2/node/economics_runtime.go index 37771963..4e4079d4 100644 --- a/internal/v2/node/economics_runtime.go +++ b/internal/v2/node/economics_runtime.go @@ -7,7 +7,9 @@ import ( // EnableShadowEconomics attaches finalized economic telemetry to the v2 runtime. // It is intentionally genesis-only for now: attaching a collector after blocks -// have finalized would require authenticated historical replay first. +// have finalized would require authenticated historical replay first. The node +// owns an independent deep copy so callers cannot mutate telemetry outside the +// runtime synchronization boundary. func (r *Runtime) EnableShadowEconomics(collector *economics.EpochCollector) error { if r == nil || collector == nil { return ErrRuntimeConfig @@ -17,7 +19,11 @@ func (r *Runtime) EnableShadowEconomics(collector *economics.EpochCollector) err if r.Height != 0 || r.economicCollector != nil || collector.ShardCount() != r.ShardCount || collector.NativeTokenID() != r.NativeToken { return ErrRuntimeConfig } - r.economicCollector = collector + owned := collector.Clone() + if owned == nil { + return ErrRuntimeConfig + } + r.economicCollector = owned return nil } From b83922ba95d4d4d4b7d40048a0519a7c250cb9fb Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:37:46 +0200 Subject: [PATCH 231/274] Test runtime ownership of shadow economics state --- .../node/economics_runtime_ownership_test.go | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 internal/v2/node/economics_runtime_ownership_test.go diff --git a/internal/v2/node/economics_runtime_ownership_test.go b/internal/v2/node/economics_runtime_ownership_test.go new file mode 100644 index 00000000..1513fdb7 --- /dev/null +++ b/internal/v2/node/economics_runtime_ownership_test.go @@ -0,0 +1,43 @@ +package node + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestRuntimeOwnsIndependentEconomicsCollector(t *testing.T) { + network := types.NetworkID{1} + native := types.TokenID{2} + runtime, err := NewRuntime(network, native, types.Hash{3}, map[uint32]worldstate.Backend{0: worldstate.NewMemory()}, 1) + if err != nil { + t.Fatal(err) + } + collector, err := economics.NewEpochCollector(economics.EpochCollectorConfig{ + Epoch: 1, ShardCount: 1, NativeToken: native, + InitialCirculatingSupply: map[uint32]uint64{0: 1_000}, + ResourceCapacityPerBlock: map[uint32]uint64{0: 100}, + VelocityPolicy: economics.VelocityPolicy{ + MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000, + }, + FeePolicy: economics.CompatibilityFeePolicy(), + }) + if err != nil { + t.Fatal(err) + } + if err := runtime.EnableShadowEconomics(collector); err != nil { + t.Fatal(err) + } + if err := collector.AdvanceEpoch(2); err != nil { + t.Fatal(err) + } + metrics, _, err := runtime.EconomicEpochSnapshot() + if err != nil { + t.Fatal(err) + } + if len(metrics) != 1 || metrics[0].Epoch != 1 { + t.Fatalf("external collector mutation leaked into runtime: %#v", metrics) + } +} From f1e62e951fc6c6efc6d17353bd8fcc1748c798d0 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:39:05 +0200 Subject: [PATCH 232/274] Expose deep clone for shadow economic epoch engine --- .../v2/economics/shadow_epoch_engine_clone.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 internal/v2/economics/shadow_epoch_engine_clone.go diff --git a/internal/v2/economics/shadow_epoch_engine_clone.go b/internal/v2/economics/shadow_epoch_engine_clone.go new file mode 100644 index 00000000..6d1cdf06 --- /dev/null +++ b/internal/v2/economics/shadow_epoch_engine_clone.go @@ -0,0 +1,15 @@ +package economics + +// Clone returns an independent copy of the shadow epoch engine, including the +// accepted monetary-state history anchor and prior ZCPI snapshot. +func (e *ShadowEpochEngine) Clone() *ShadowEpochEngine { + if e == nil { + return nil + } + out := *e + if e.previous != nil { + previous := *e.previous + out.previous = &previous + } + return &out +} From 0b6988666f0e559bea0d9790b89cda4b447ddefa Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:40:18 +0200 Subject: [PATCH 233/274] Finalize shadow monetary state through v2 consensus candidates --- internal/v2/node/runtime.go | 130 +++++++++++++++++++++++++++++++----- 1 file changed, 114 insertions(+), 16 deletions(-) diff --git a/internal/v2/node/runtime.go b/internal/v2/node/runtime.go index 13a703c4..e1e285a4 100644 --- a/internal/v2/node/runtime.go +++ b/internal/v2/node/runtime.go @@ -47,25 +47,30 @@ type shardDelta struct { } type Candidate struct { - Header sharding.GlobalHeader - Commitments []sharding.Commitment - Results map[uint32][]execution.Result - Receipts map[uint32][]sharding.CrossShardReceipt - deltas map[uint32]shardDelta - economicObservations map[uint32]economics.FinalizedShardObservation + Header sharding.GlobalHeader + Commitments []sharding.Commitment + Results map[uint32][]execution.Result + Receipts map[uint32][]sharding.CrossShardReceipt + deltas map[uint32]shardDelta + economicObservations map[uint32]economics.FinalizedShardObservation + economicPendingStateHash types.Hash } type Runtime struct { - mu sync.Mutex - Network types.NetworkID - NativeToken types.TokenID - ValidatorRoot types.Hash - ShardCount uint32 - States map[uint32]worldstate.Backend - Workers int - Height uint64 - ParentHash types.Hash - economicCollector *economics.EpochCollector + mu sync.Mutex + Network types.NetworkID + NativeToken types.TokenID + ValidatorRoot types.Hash + ShardCount uint32 + States map[uint32]worldstate.Backend + Workers int + Height uint64 + ParentHash types.Hash + economicCollector *economics.EpochCollector + economicEngine *economics.ShadowEpochEngine + economicEpochLength uint64 + economicBalances economics.MonetaryBalanceSnapshot + pendingEconomic *economics.ShadowEpochPreview } func NewRuntime(network types.NetworkID, nativeToken types.TokenID, validatorRoot types.Hash, states map[uint32]worldstate.Backend, workers int) (*Runtime, error) { @@ -139,6 +144,18 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( } delta.Created = append(delta.Created, destinationObject, marker) } + + if shard == 0 && r.pendingEconomic != nil { + stateHash, err := r.pendingEconomic.State.Hash() + if err != nil { + return Candidate{}, err + } + if err := appendEconomicStateDelta(&delta, *r.pendingEconomic); err != nil { + return Candidate{}, err + } + candidate.economicPendingStateHash = stateHash + } + newRoot := currentRoot if len(delta.Consumed) > 0 || len(delta.Created) > 0 { simulator, ok := store.(worldstate.Simulator) @@ -191,6 +208,26 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( return candidate, nil } +func appendEconomicStateDelta(delta *shardDelta, preview economics.ShadowEpochPreview) error { + if delta == nil || len(preview.Created) != 1 || preview.Created[0].ID != economics.MonetaryStateObjectID(preview.State.Network) { + return ErrCandidateState + } + monetaryID := preview.Created[0].ID + for _, id := range delta.Consumed { + if id == monetaryID { + return ErrCandidateState + } + } + for _, created := range delta.Created { + if created.ID == monetaryID { + return ErrCandidateState + } + } + delta.Consumed = append(delta.Consumed, preview.Consumed...) + delta.Created = append(delta.Created, preview.Created...) + return nil +} + func (r *Runtime) validateReceiptImport(destinationShard uint32, receiptImport ReceiptImport) error { if receiptImport.Header.Network != r.Network || receiptImport.Validators.Network != r.Network || receiptImport.Certificate.Network != r.Network || receiptImport.Receipt.DestinationShard != destinationShard || receiptImport.Header.Height > r.Height || receiptImport.Header.Height != receiptImport.Receipt.SourceHeight { return ErrReceiptImport @@ -228,6 +265,16 @@ func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificat return sharding.GlobalHeader{}, err } + pendingApplied := r.pendingEconomic != nil + if pendingApplied { + expected, err := r.pendingEconomic.State.Hash() + if err != nil || candidate.economicPendingStateHash != expected { + return sharding.GlobalHeader{}, ErrCandidateState + } + } else if !types.IsZero32([32]byte(candidate.economicPendingStateHash)) { + return sharding.GlobalHeader{}, ErrCandidateState + } + var economicPreview *economics.EpochCollector if r.economicCollector != nil { if len(candidate.economicObservations) != int(r.ShardCount) { @@ -239,6 +286,46 @@ func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificat } } + var enginePreview *economics.ShadowEpochEngine + if r.economicEngine != nil { + enginePreview = r.economicEngine.Clone() + if enginePreview == nil { + return sharding.GlobalHeader{}, ErrRuntimeConfig + } + if pendingApplied { + if err := enginePreview.Accept(*r.pendingEconomic); err != nil { + return sharding.GlobalHeader{}, err + } + } + } else if pendingApplied { + return sharding.GlobalHeader{}, ErrRuntimeConfig + } + + var nextPending *economics.ShadowEpochPreview + nextBalances := r.economicBalances + if r.economicEngine != nil && r.economicEpochLength > 0 && candidate.Header.Height%r.economicEpochLength == 0 { + if economicPreview == nil { + return sharding.GlobalHeader{}, ErrRuntimeConfig + } + metrics, verifiedWork, err := economicPreview.FinalizeEpoch() + if err != nil { + return sharding.GlobalHeader{}, err + } + aggregate, err := economics.AggregateEpochMetrics(metrics) + if err != nil || nextBalances.TotalSupply < aggregate.BurnedFees { + return sharding.GlobalHeader{}, ErrRuntimeConfig + } + nextBalances.TotalSupply -= aggregate.BurnedFees + preview, err := enginePreview.PreviewCloseEpoch(metrics, verifiedWork, nextBalances) + if err != nil { + return sharding.GlobalHeader{}, err + } + if err := economicPreview.AdvanceEpoch(preview.State.Epoch + 1); err != nil { + return sharding.GlobalHeader{}, err + } + nextPending = &preview + } + commitments := make(map[uint32]sharding.Commitment, len(candidate.Commitments)) for _, commitment := range candidate.Commitments { commitments[commitment.ShardID] = commitment @@ -259,9 +346,20 @@ func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificat return sharding.GlobalHeader{}, ErrCandidateState } } + if economicPreview != nil { r.economicCollector = economicPreview } + if enginePreview != nil { + r.economicEngine = enginePreview + } + if nextPending != nil { + r.pendingEconomic = nextPending + r.economicBalances = nextBalances + } else if pendingApplied { + r.pendingEconomic = nil + } + finalized := candidate.Header finalized.CertificateHash = certificate.Hash() r.Height = finalized.Height From 4d735c7f3308fccf47ea6301ec455d42f78ffc07 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:40:34 +0200 Subject: [PATCH 234/274] Expose current finalized economics epoch --- internal/v2/economics/finalized_collector_clone.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/v2/economics/finalized_collector_clone.go b/internal/v2/economics/finalized_collector_clone.go index 2fb39974..bb9d07ed 100644 --- a/internal/v2/economics/finalized_collector_clone.go +++ b/internal/v2/economics/finalized_collector_clone.go @@ -6,3 +6,10 @@ package economics func (c *EpochCollector) Clone() *EpochCollector { return c.clone() } + +func (c *EpochCollector) Epoch() uint64 { + if c == nil { + return 0 + } + return c.config.Epoch +} From 05ceb4e80cf4829bc9fe0bd5ed0ea8927c60e217 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:40:55 +0200 Subject: [PATCH 235/274] Configure consensus-finalized shadow economic epochs --- internal/v2/node/economics_runtime.go | 57 +++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/internal/v2/node/economics_runtime.go b/internal/v2/node/economics_runtime.go index 4e4079d4..e1cda6ef 100644 --- a/internal/v2/node/economics_runtime.go +++ b/internal/v2/node/economics_runtime.go @@ -27,6 +27,35 @@ func (r *Runtime) EnableShadowEconomics(collector *economics.EpochCollector) err return nil } +// EnableShadowEconomicEpochs enables automatic shadow epoch closure. The +// closed epoch is evaluated only after its boundary block finalizes; the +// resulting MonetaryEpochState is then inserted into the first consensus +// candidate of the next epoch. No live minting occurs. +// +// Epoch length is deliberately at least two blocks so accepting the previous +// epoch object and closing the current epoch are separate consensus heights. +func (r *Runtime) EnableShadowEconomicEpochs(engine *economics.ShadowEpochEngine, epochLength uint64, balances economics.MonetaryBalanceSnapshot) error { + if r == nil || engine == nil { + return ErrRuntimeConfig + } + r.mu.Lock() + defer r.mu.Unlock() + if r.Height != 0 || r.economicCollector == nil || r.economicEngine != nil || r.pendingEconomic != nil || + r.economicCollector.Epoch() != 1 || engine.Network != r.Network || epochLength < 2 || + balances.TotalSupply == 0 || balances.StakedSupply > balances.TotalSupply || + balances.ProtocolReserve > balances.TotalSupply || balances.BaseFee == 0 { + return ErrRuntimeConfig + } + owned := engine.Clone() + if owned == nil { + return ErrRuntimeConfig + } + r.economicEngine = owned + r.economicEpochLength = epochLength + r.economicBalances = balances + return nil +} + // EconomicEpochSnapshot returns the current shadow epoch inputs derived only // from successfully finalized Runtime.Commit calls. func (r *Runtime) EconomicEpochSnapshot() ([]economics.ShardEpochMetrics, []compute.VerifiedWork, error) { @@ -40,3 +69,31 @@ func (r *Runtime) EconomicEpochSnapshot() ([]economics.ShardEpochMetrics, []comp } return r.economicCollector.FinalizeEpoch() } + +// PendingEconomicState returns the epoch state waiting to be committed through +// the next normal v2 candidate, if any. +func (r *Runtime) PendingEconomicState() (economics.MonetaryEpochState, bool) { + if r == nil { + return economics.MonetaryEpochState{}, false + } + r.mu.Lock() + defer r.mu.Unlock() + if r.pendingEconomic == nil { + return economics.MonetaryEpochState{}, false + } + return r.pendingEconomic.State, true +} + +// FinalizedEconomicState returns the last shadow monetary state accepted by a +// consensus-finalized candidate. +func (r *Runtime) FinalizedEconomicState() (economics.MonetaryEpochState, bool) { + if r == nil { + return economics.MonetaryEpochState{}, false + } + r.mu.Lock() + defer r.mu.Unlock() + if r.economicEngine == nil { + return economics.MonetaryEpochState{}, false + } + return r.economicEngine.PreviousState() +} From 19c98976333dd15a15154bb6364950ad98bb020c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:41:37 +0200 Subject: [PATCH 236/274] Test consensus-finalized shadow economic epoch scheduling --- .../v2/node/economic_epoch_scheduler_test.go | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 internal/v2/node/economic_epoch_scheduler_test.go diff --git a/internal/v2/node/economic_epoch_scheduler_test.go b/internal/v2/node/economic_epoch_scheduler_test.go new file mode 100644 index 00000000..52616f06 --- /dev/null +++ b/internal/v2/node/economic_epoch_scheduler_test.go @@ -0,0 +1,187 @@ +package node + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestShadowEconomicEpochStateEntersNextConsensusCandidate(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("economic-epoch-scheduler"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + validatorKey, validators, validatorRoot := schedulerValidatorSet(t, network) + store := worldstate.NewMemory() + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: store}, 1) + if err != nil { + t.Fatal(err) + } + + collector, err := economics.NewEpochCollector(economics.EpochCollectorConfig{ + Epoch: 1, ShardCount: 1, NativeToken: native, + InitialCirculatingSupply: map[uint32]uint64{0: 1_000_000}, + ResourceCapacityPerBlock: map[uint32]uint64{0: 100}, + VelocityPolicy: economics.VelocityPolicy{ + MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000, + }, + FeePolicy: economics.CompatibilityFeePolicy(), + }) + if err != nil { + t.Fatal(err) + } + if err := runtime.EnableShadowEconomics(collector); err != nil { + t.Fatal(err) + } + index := economics.ComputeIndexConfig{MinSamplesPerClass: 1, MinCoverageBps: 10_000, EWMABps: 10_000} + index.WeightsBps[compute.WorkCPUGeneral] = 10_000 + engine, err := economics.NewShadowEpochEngine(network, economics.ShadowEpochEngineConfig{ + ComputeIndex: index, + ComputeScarcity: economics.DefaultComputeScarcityConfig(), + Monetary: economics.DefaultShadowPolicy(), + ComputeFeedback: economics.DefaultComputeFeedbackPolicy(economics.ComputeFeedbackObserveOnly), + }) + if err != nil { + t.Fatal(err) + } + if err := runtime.EnableShadowEconomicEpochs(engine, 2, economics.MonetaryBalanceSnapshot{ + TotalSupply: 1_000_000, BaseFee: 1, + }); err != nil { + t.Fatal(err) + } + + commitEmptySchedulerBlock(t, runtime, validatorKey, validators, 1) + if _, pending := runtime.PendingEconomicState(); pending { + t.Fatal("epoch closed before configured boundary") + } + + commitEmptySchedulerBlock(t, runtime, validatorKey, validators, 2) + pending, ok := runtime.PendingEconomicState() + if !ok || pending.Epoch != 1 || !pending.Shadow || pending.TotalSupply != 1_000_000 { + t.Fatalf("unexpected pending epoch state: %#v", pending) + } + if _, exists := store.GetObject(economics.MonetaryStateObjectID(network)); exists { + t.Fatal("pending monetary state entered world state before a consensus candidate") + } + + rootBefore := store.Root() + candidate, err := runtime.BuildCandidate(3, nil) + if err != nil { + t.Fatal(err) + } + if candidate.Commitments[0].StateRoot == rootBefore { + t.Fatal("next candidate did not commit the pending monetary object") + } + commitSchedulerCandidate(t, runtime, validatorKey, validators, candidate) + + if _, pending := runtime.PendingEconomicState(); pending { + t.Fatal("finalized pending state was not cleared") + } + finalized, ok := runtime.FinalizedEconomicState() + if !ok || finalized.Epoch != 1 || !finalized.Shadow { + t.Fatalf("shadow monetary state was not finalized: %#v", finalized) + } + obj, exists := store.GetObject(economics.MonetaryStateObjectID(network)) + if !exists || obj.Version != 1 { + t.Fatalf("monetary system object missing after consensus finality: %#v", obj) + } + parsed, err := economics.ParseMonetaryEpochState(obj.Data) + if err != nil || parsed != finalized { + t.Fatalf("committed monetary object mismatch: %#v %v", parsed, err) + } + if finalized.ShadowGrossMintTarget == 0 { + t.Fatal("expected a shadow issuance suggestion for simulation") + } + if supply, _ := runtime.economicCollector.CirculatingSupply(0); supply != 1_000_000 { + t.Fatalf("shadow issuance mutated live circulating supply: %d", supply) + } +} + +func TestShadowEconomicEpochSchedulerRejectsOneBlockEpochs(t *testing.T) { + network := types.NetworkID{1} + native := types.TokenID{2} + runtime, err := NewRuntime(network, native, types.Hash{3}, map[uint32]worldstate.Backend{0: worldstate.NewMemory()}, 1) + if err != nil { + t.Fatal(err) + } + collector, err := economics.NewEpochCollector(economics.EpochCollectorConfig{ + Epoch: 1, ShardCount: 1, NativeToken: native, + InitialCirculatingSupply: map[uint32]uint64{0: 1_000}, + ResourceCapacityPerBlock: map[uint32]uint64{0: 100}, + VelocityPolicy: economics.VelocityPolicy{ + MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000, + }, + FeePolicy: economics.CompatibilityFeePolicy(), + }) + if err != nil { + t.Fatal(err) + } + if err := runtime.EnableShadowEconomics(collector); err != nil { + t.Fatal(err) + } + index := economics.ComputeIndexConfig{MinSamplesPerClass: 1, MinCoverageBps: 10_000, EWMABps: 10_000} + index.WeightsBps[compute.WorkCPUGeneral] = 10_000 + engine, err := economics.NewShadowEpochEngine(network, economics.ShadowEpochEngineConfig{ + ComputeIndex: index, ComputeScarcity: economics.DefaultComputeScarcityConfig(), + Monetary: economics.DefaultShadowPolicy(), ComputeFeedback: economics.DefaultComputeFeedbackPolicy(economics.ComputeFeedbackObserveOnly), + }) + if err != nil { + t.Fatal(err) + } + if err := runtime.EnableShadowEconomicEpochs(engine, 1, economics.MonetaryBalanceSnapshot{TotalSupply: 1_000, BaseFee: 1}); err != ErrRuntimeConfig { + t.Fatalf("one-block shadow epoch accepted: %v", err) + } +} + +func schedulerValidatorSet(t *testing.T, network types.NetworkID) (*ecdsa.PrivateKey, v2consensus.ValidatorSet, types.Hash) { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + publicKey := elliptic.Marshal(elliptic.P256(), key.PublicKey.X, key.PublicKey.Y) + validators := v2consensus.ValidatorSet{ + Network: network, + Validators: []v2consensus.Validator{{ + ID: types.ValidatorIDFromPublicKey(publicKey), PublicKey: publicKey, Power: 10, + }}, + } + root, err := validators.Root() + if err != nil { + t.Fatal(err) + } + return key, validators, root +} + +func commitEmptySchedulerBlock(t *testing.T, runtime *Runtime, key *ecdsa.PrivateKey, validators v2consensus.ValidatorSet, height uint64) { + t.Helper() + candidate, err := runtime.BuildCandidate(height, nil) + if err != nil { + t.Fatal(err) + } + commitSchedulerCandidate(t, runtime, key, validators, candidate) +} + +func commitSchedulerCandidate(t *testing.T, runtime *Runtime, key *ecdsa.PrivateKey, validators v2consensus.ValidatorSet, candidate Candidate) { + t.Helper() + proposal, err := v2consensus.SignProposal(key, candidate.Header, 0) + if err != nil { + t.Fatal(err) + } + vote, err := v2consensus.SignVote(key, runtime.Network, candidate.Header.Height, 0, v2consensus.HeaderConsensusHash(candidate.Header)) + if err != nil { + t.Fatal(err) + } + certificate, err := validators.BuildCertificate(proposal, []v2consensus.Vote{vote}) + if err != nil { + t.Fatal(err) + } + if _, err := runtime.Commit(candidate, certificate, validators); err != nil { + t.Fatal(err) + } +} From 2af00e08f282c3cf05308a941060479339b61da2 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:42:32 +0200 Subject: [PATCH 237/274] Document consensus-finalized shadow epoch scheduling --- docs/economic-runtime-v2.md | 70 +++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/docs/economic-runtime-v2.md b/docs/economic-runtime-v2.md index 124d89c4..219c4c95 100644 --- a/docs/economic-runtime-v2.md +++ b/docs/economic-runtime-v2.md @@ -21,7 +21,7 @@ measure first -> simulate second -> activate last Economic telemetry is not allowed to advance from mempool admission, proposal construction, unfinalized votes, provider advertisements or RPC summaries. -The intended runtime path is: +The runtime path is now executable as: ```text proof-carrying transactions @@ -36,11 +36,16 @@ proof-carrying transactions -> ZCSI -> ZAMP shadow decision -> pending MonetaryEpochState - -> normal Merkle state transition in a later finalized candidate + -> first candidate of next epoch + -> Merkle state root + -> proposal / votes / QC + -> finalized MonetaryEpochState ``` `EpochCollector` has an explicit preview boundary so a node can evaluate telemetry before durable state apply and promote the preview only when the corresponding finalized transition succeeds. +The node owns independent clones of the collector and shadow epoch engine. External configuration objects cannot mutate economic history behind the runtime lock. + ## 2. Native-supply attribution across shards A cross-shard ZPH transfer must not make global supply temporarily disappear while its receipt is in flight. @@ -74,7 +79,7 @@ The existing minimum-age/full-weight policy therefore continues to make rapid fr Long AI/scientific/rendering jobs may be posted in one epoch and settle in another. Zephyr therefore does not require `fulfilled <= new demand` inside a single epoch. -The v2 economic wire now uses the exact conservation rule: +The v2 economic wire uses the exact conservation rule: ```text OpeningComputeBacklog @@ -202,7 +207,27 @@ It does **not** advance controller history. `Accept` advances the controller only after the caller has finalized the exact monetary object through normal consensus/state finality. -## 10. Monetary state chaining +## 10. Automatic epoch scheduling and consensus inclusion + +When automatic shadow epochs are enabled, the runtime requires an epoch length of at least two blocks. + +At a configured epoch-boundary height: + +1. normal v2 consensus finalizes the boundary block; +2. the finalized collector preview closes the epoch; +3. ZCPI, ZCSI and ZAMP are evaluated; +4. the collector advances to the next economic epoch; +5. the resulting `MonetaryEpochState` becomes **pending**, not finalized. + +The next `BuildCandidate` automatically adds the pending monetary system-object delta to shard 0 before state-root simulation. + +Therefore the next proposal commits the exact monetary-state bytes through the shard `StateRoot` and global commitment root. The shadow epoch engine is advanced only if that candidate receives a valid QC and the state apply succeeds. + +If the certificate is rejected, neither the economic collector nor the monetary-history engine advances. + +The scheduler does not mint ZPH. `ShadowGrossMintTarget` and compute-incentive amounts remain recorded simulation outputs only. + +## 11. Monetary state chaining Each accepted shadow monetary object commits the prior accepted state through: @@ -220,7 +245,7 @@ The engine rejects: This is designed so a Citizen Node can eventually verify the complete economic-controller history from Merkle proofs rather than trusting a dashboard or validator RPC. -## 11. Feedback modes remain shadow +## 12. Feedback modes remain shadow The three compute-feedback modes remain: @@ -236,7 +261,7 @@ A reliable ZCSI is mandatory before modes B/C can alter even the simulated rewar Mode B remains the preferred first activation candidate if long-run testing eventually supports it. -## 12. Runtime resource utilization +## 13. Runtime resource utilization The finalized collector includes a deterministic shadow resource-unit counter so chain utilization can be replayed without wall-clock measurements. @@ -250,12 +275,15 @@ The current reference units account for bounded protocol work such as: This is **not** the final production gas schedule. It exists so simulations can test controller stability using a deterministic load signal while the final resource-pricing schedule remains an activation decision. -## 13. What is implemented now +## 14. What is implemented now Executable foundations now include: - finalized-block economic collector; - atomic collector preview/apply semantics; +- runtime-owned collector/engine clones; +- successful `Runtime.Commit` wiring into finalized economic telemetry; +- rejected-QC protection: failed commits cannot advance economic history; - native supply attribution across shard receipt imports; - compatibility full-fee-burn accounting; - age-weighted finalized-spend velocity; @@ -266,27 +294,31 @@ Executable foundations now include: - independent authenticated-supply reliability bit; - canonical v2 shard/aggregate economic metrics; - previewable ZCPI/ZCSI/ZAMP shadow epoch engine; +- automatic configured epoch-boundary closure; - pending Merkle monetary-object delta generation; -- tests for atomic rejection, cross-shard supply conservation, multi-epoch compute backlog, verified settlement pricing and shadow-state chaining. +- automatic pending-state insertion into the first candidate of the next epoch; +- QC/state-finality acceptance of the monetary object; +- explicit proof that shadow issuance suggestions do not mutate live supply; +- tests for atomic rejection, cross-shard supply conservation, multi-epoch compute backlog, verified settlement pricing, shadow-state chaining and three-block epoch scheduling. -## 14. What is still required +## 15. What is still required Before monetary activation, Zephyr still needs: -1. node-runtime wiring from successful `Runtime.Commit` into the finalized collector; -2. automatic epoch-boundary scheduling; -3. insertion of the pending `MonetaryEpochState` into a normal candidate **before** its state root/QC is finalized; -4. authenticated total-supply, staking and protocol-reserve system objects rather than operator-supplied shadow inputs; -5. production benchmarked/collateralized compute-capacity registry; -6. governance-delayed workload-registry and economic-parameter changes; -7. final gas/resource pricing and active burn/validator/reserve distribution; -8. replay/oscillation/manipulation datasets over long devnet runs; -9. Citizen wallet economic-state decoder/history UI; -10. an explicit protocol version/height gate before any live issuance or reward redistribution. +1. authenticated total-supply, staking and protocol-reserve system objects rather than genesis/operator-supplied shadow balance inputs; +2. production benchmarked/collateralized compute-capacity registry; +3. governance-delayed workload-registry and economic-parameter changes; +4. final gas/resource pricing and active burn/validator/reserve distribution; +5. replay/oscillation/manipulation datasets over long devnet runs; +6. Citizen wallet economic-state decoder/history UI; +7. durable recovery/replay of scheduler controller metadata across full node restarts; +8. transactional global multi-shard state-commit coordination so a backend error cannot leave an earlier shard applied while a later shard fails; +9. explicit protocol version/height gates before any live issuance or reward redistribution. The current invariant is therefore: ```text finalized data -> reproducible shadow decision +shadow decision -> consensus-finalized telemetry object shadow decision != permission to mint ``` From 080e765adfe133dc9f631eec830ef89e98c6d877 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:44:46 +0200 Subject: [PATCH 238/274] Rewrite README for Zephyr Protocol v2 and Apache-2.0 --- README.md | 543 ++++++++++++++++++++---------------------------------- 1 file changed, 198 insertions(+), 345 deletions(-) diff --git a/README.md b/README.md index 48122c8d..472b77b3 100644 --- a/README.md +++ b/README.md @@ -1,424 +1,277 @@ # Zephyr Chain -Zephyr Chain is an early-stage blockchain node and wallet stack focused on a production path toward validator-driven consensus, deterministic WASM execution, and a confidential compute marketplace. - -The long-term product vision lives in [Zaphyr-chain_manifesto.md](./Zaphyr-chain_manifesto.md). Application and use-case framing lives in [docs/applications.md](./docs/applications.md). Academic paper materials are maintained locally under `paper/` and kept private via `.gitignore`. This `README` stays practical: what works now, what changed in the latest iteration, and what comes next. - -## Current Status - -Implemented today: - -- Go HTTP node entrypoint in `cmd/node` -- DPoS election primitives and tests in `internal/dpos` -- transaction envelope validation in `internal/tx` -- durable accounts, mempool, committed blocks, atomic restart-safe state persistence, and snapshot restore in `internal/ledger` -- durable validator-set snapshots with versioning, proposer scheduling, and quorum summaries in `internal/ledger` -- durable consensus round state with restart-safe height, round, and round-start tracking in `internal/ledger` -- durable signed consensus proposals, validator votes, and quorum certificates in `internal/ledger` -- self-contained consensus proposals that carry the full candidate transaction body plus deterministic template fields -- automatic single-node block production plus manual dev block production -- optional proposer-schedule enforcement for block production when a validator set and local validator address are configured -- optional certificate-gated local block commit and remote block import when consensus enforcement is enabled -- certificate-gated local commit can replay a stored certified proposal body even when the local mempool no longer has that candidate -- optional consensus automation for scheduled self-proposal, validator auto-vote, timeout-driven round advance, proposer rotation, and certified proposer auto-commit on the current devnet path -- transport-backed peer replication in `internal/api` with the current implementation running over HTTP -- signed validator transport-identity proofs in status responses and peer verification views when a validator private key is configured -- optional strict peer-admission enforcement and peer-to-validator binding on the current HTTP transport -- peer status tracking, peer admission state, per-peer sync telemetry, block fetch by height, block import, snapshot-based catch-up, and consensus artifact replication for admitted peers -- consensus visibility endpoints for status, validator snapshots, active round inspection, proposer schedule inspection, latest consensus artifacts, and next-block template preview -- operator-facing observability endpoints for readiness, alerts, SLO summaries, alert-rule exports, recording-rule exports, dashboard bundles, Grafana dashboard export, JSON metrics, Prometheus metrics, and structured logs -- Vue wallet in `apps/wallet` -- wallet account generation, encrypted import/export, passphrase-protected local key storage, local signing, account inspection, faucet funding, and transaction broadcast - -Implemented in this iteration: - -- failed outgoing proposal, vote, and block dissemination now lands in durable `replication_blocked` peer incidents with artifact-specific `reason` labels and transport-oriented error-code rollups -- `GET /v1/alerts` now separates general peer-sync degradation from targeted `peer_import_blocked`, `peer_admission_blocked`, and `peer_replication_blocked` warnings built from durable peer incident rollups -- `GET /v1/alert-rules` and `GET /v1/alert-rules/prometheus` now export matching peer-import, peer-admission, and peer-replication diagnostic rules for scrape-based monitoring stacks -- `GET /metrics` now exports retained peer incident counts and latest observation timestamps per peer with the latest state, reason, and error-code labels attached for scrape-based drill-down -- `GET /v1/recording-rules` and `GET /v1/recording-rules/prometheus` now export a canonical per-peer incident-pressure rollup so downstream dashboards can reuse that peer view without rewriting PromQL -- `GET /v1/dashboards` and `GET /v1/dashboards/grafana` now expose peer incident reason panels plus a per-peer incident pressure panel built on that recording rule alongside state and error-code rollups so dissemination failures are visible in the peer-sync bundle -- `GET /v1/alerts` now also derives a targeted aggregate `peer_snapshot_restored` warning plus repair-path-specific `peer_snapshot_restore_divergence`, `peer_snapshot_restore_import_repair`, and `peer_snapshot_restore_fetch_fallback` warnings from retained `snapshot_restored` incidents so snapshot-based peer repair shows up as a first-class operator signal without hiding the exact repair path -- `GET /v1/alert-rules` and `GET /v1/alert-rules/prometheus` now additionally export `ZephyrPeerSnapshotRestore` so the snapshot-repair signal can be promoted into Prometheus-based alerting without custom rule authoring -- `GET /v1/recording-rules` and `GET /v1/recording-rules/prometheus` now additionally export the canonical peer-sync rollup `zephyr:peer_sync:snapshot_restore_pressure` so dashboards can track retained snapshot-repair pressure without re-deriving it from raw incident metrics -- `GET /v1/dashboards` and `GET /v1/dashboards/grafana` now add a `Peer snapshot restore pressure` stat to the peer-sync bundle so divergence, import-repair, and fetch-fallback recovery remain visible next to state, reason, error-code, and per-peer incident pressure -- `GET /v1/alert-rules` and `GET /v1/alert-rules/prometheus` now additionally export repair-path-specific `ZephyrPeerSnapshotRestoreDivergence`, `ZephyrPeerSnapshotRestoreImportRepair`, and `ZephyrPeerSnapshotRestoreFetchFallback` rules so downstream alerting can split divergence repair from import-repair and fetch-fallback paths without custom PromQL -- `GET /v1/recording-rules` and `GET /v1/recording-rules/prometheus` now additionally export the canonical peer-sync rollup `zephyr:peer_sync:snapshot_restore_pressure_by_reason` so dashboards and alert managers can reuse the filtered divergence, import-repair, and fetch-fallback series directly -- `GET /v1/dashboards` and `GET /v1/dashboards/grafana` now add a `Peer snapshot restore reasons` panel to the peer-sync bundle so repair-path pressure is graphed explicitly instead of only being inferred from the wider incident-reason panel -- `GET /v1/recording-rules`, `GET /v1/recording-rules/prometheus`, and `GET /v1/dashboards` now preserve the compatibility aggregate `peer_snapshot_restored` code alongside the split `peer_snapshot_restore_*` codes in related-alert metadata so downstream rollups and dashboard bundles can pivot incrementally without losing the older aggregate signal -- `GET /v1/metrics` now carries horizon-aware `peerSyncSummary.horizons` views for `5m`, `15m`, `1h`, `6h`, and `24h`, `GET /metrics` mirrors them through `zephyr_peer_sync_horizon_*` gauges, and `GET /v1/health` now includes recent peer incident occurrence and affected-peer horizon detail so operators can tell whether retained peer pressure is fresh or lingering -- `GET /v1/recording-rules` and `GET /v1/recording-rules/prometheus` now additionally export `zephyr:peer_sync:incident_pressure_by_horizon`, and `GET /v1/dashboards` plus `GET /v1/dashboards/grafana` now add a `Peer incident pressure horizons` panel so the peer-sync bundle can compare recent retained pressure across short and long windows without rebuilding PromQL -- `GET /metrics` now also exports per-peer snapshot-repair metadata through `zephyr_peer_snapshot_restore_last_height`, `zephyr_peer_snapshot_restore_last_observed_at_seconds`, and `zephyr_peer_snapshot_restore_age_seconds`, while `GET /v1/recording-rules` plus `GET /v1/recording-rules/prometheus` now add `zephyr:peer_sync:snapshot_restore_pressure_by_peer` and `zephyr:peer_sync:snapshot_restore_age_by_peer` for canonical per-peer repair pressure and repair age drill-down -- `GET /v1/dashboards` and `GET /v1/dashboards/grafana` now add `Peer snapshot restore pressure by peer`, `Peer snapshot restore heights`, and `Peer snapshot restore age` so operators can correlate repair pressure, retained repair reason, the latest restored height, and whether a restore is fresh before drilling into `/v1/peers` for block-hash and `recentIncidents` detail -- `GET /v1/metrics` now includes `chainThroughput` totals plus rolling `1m`, `5m`, and `15m` windows for committed blocks, committed transactions, average transactions per block, and recent TPS baselining, along with a `settlementThroughput` view carrying raw queue-drain lag, latest commit age, warn or fail thresholds, active alert metadata, normalized warn or fail utilization ratios, recent 1m, 5m, and 15m backlog-drain estimates, per-estimate warn utilization ratios, and an explicit `peakDrainEstimate` summary for the current worst-case backlog projection -- `GET /metrics` now also exports committed-block, committed-transaction, latest-block-interval, and rolling throughput gauges plus settlement queue-drain gauges such as `zephyr_settlement_queue_drain_lag_seconds`, `zephyr_settlement_queue_drain_threshold_seconds`, `zephyr_settlement_queue_drain_utilization_ratio`, `zephyr_settlement_estimated_queue_drain_warn_utilization_ratio`, `zephyr_settlement_estimated_queue_drain_warn_utilization_ratio_max`, `zephyr_settlement_estimated_queue_drain_seconds`, and `zephyr_settlement_estimated_queue_drain_seconds_max` so Prometheus-style monitoring can track both recent TPS and settlement pressure without re-deriving chain history -- `GET /v1/recording-rules` and `GET /v1/recording-rules/prometheus` now additionally export canonical `zephyr:chain:transactions_per_second_1m`, `zephyr:chain:transactions_per_second_5m`, and `zephyr:chain:transactions_per_second_15m` rollups for dashboard reuse -- `GET /v1/dashboards` and `GET /v1/dashboards/grafana` now add a `Recent transaction throughput` overview panel built on those rollups so operators can baseline recent TPS alongside readiness and peer health -- `GET /v1/health` now includes a `settlement_throughput` check that watches queued transaction drain against the configured automatic block interval when block production is enabled and carries the current worst-case drain forecast in its detail -- `GET /v1/alerts` and `GET /v1/slo` now derive `settlement_throughput_reduced`, `settlement_throughput_stalled`, and the `settlement_throughput` objective so slow or stalled queue drain becomes first-class operator evidence, with detail strings now carrying the current worst-case drain forecast when recent throughput baselines exist -- `GET /v1/alert-rules` and `GET /v1/alert-rules/prometheus` now export `ZephyrSettlementThroughputAtRisk` and `ZephyrSettlementThroughputStalled` so the same queue-drain signal can be promoted into Prometheus-based alerting -- `GET /v1/recording-rules` and `GET /v1/recording-rules/prometheus` now additionally export canonical `zephyr:settlement_throughput:at_risk` and `zephyr:settlement_throughput:breached` rollups plus normalized `zephyr:settlement_queue_drain:warn_utilization` and `zephyr:settlement_queue_drain:fail_utilization` series plus canonical `zephyr:settlement_queue_drain:estimate_seconds_1m`, `zephyr:settlement_queue_drain:estimate_seconds_5m`, `zephyr:settlement_queue_drain:estimate_seconds_15m`, and `zephyr:settlement_queue_drain:estimate_seconds_max` rollups and canonical `zephyr:settlement_queue_drain:estimate_warn_utilization_1m`, `zephyr:settlement_queue_drain:estimate_warn_utilization_5m`, `zephyr:settlement_queue_drain:estimate_warn_utilization_15m`, and `zephyr:settlement_queue_drain:estimate_warn_utilization_max` rollups for queue-drain dashboards and fleet summaries -- `GET /v1/dashboards` and `GET /v1/dashboards/grafana` now add a `Settlement throughput state` overview panel, a raw `Settlement queue-drain lag` panel, a normalized `Settlement queue-drain utilization` panel, an `Estimated queue-drain pressure` panel built on canonical warn-normalized drain-estimate recording rules, a `Worst-case estimated queue-drain pressure` stat built on the canonical max projected-pressure rollup, an `Estimated queue-drain time` panel built on canonical drain-estimate recording rules, and a `Worst-case estimated queue-drain time` stat built on the canonical max drain-estimate rollup so operators can see both absolute lag, threshold pressure, and recent backlog-drain expectations next to recent TPS baselines; on passive nodes these settlement-specific panels now stay visible in JSON with `disabledReason`, while Grafana export keeps only the enabled subset -- `GET /v1/peers` now backfills the latest import, snapshot-repair, and replication-failure telemetry from durable peer incidents so operator context survives restart before the next live sync pass -- successful local certified commits now record a durable `block_commit` consensus action so recovery history and action metrics cover the full proposer path from proposal and vote through commit -- focused tests now cover peer import, admission, replication, and snapshot-restore alerts, per-peer Prometheus incident metrics, restart-safe per-peer telemetry reconstruction, JSON and Prometheus throughput metrics including settlement alert metadata, normalized utilization ratios, raw settlement-lag gauges, recent backlog-drain estimates, the explicit peak drain-estimate summary, warn-normalized drain-estimate ratios, explicit max drain gauges, settlement health or alert or SLO detail enrichment with worst-case drain forecasts, the canonical max projected-pressure and max drain-estimate recording rules, throughput health or alert or SLO projections, alert-rule export, dashboard export, and durable `block_commit` history across the operator surfaces - -Planned but not implemented yet: - -- authenticated peer discovery and replay-safe transport over libp2p on top of the new HTTP admission and binding policy -- broader consensus recovery coverage plus richer dashboard packages, longer-horizon aggregation, and export adapters beyond the current local proposal, vote, block-commit, peer-import, snapshot-recovery, JSON metrics, Prometheus text export, alert-rule bundles, recording-rule bundles, dashboard bundles, Grafana dashboard export, derived readiness, alerts, SLO summaries, structured event logs, durable peer-sync history, and derived peer-sync summary surfaces -- on-chain staking and governance-driven validator updates instead of ad hoc election API writes -- deterministic WASM smart-contract runtime with native fee metering -- confidential compute marketplace for encrypted off-chain jobs paid in native tokens, with partitioned worker-lane scaling ahead of any full consensus-sharding step -- production observability, recovery tooling, and public testnet operations - -## Repository Layout - -- `cmd/node`: node process entrypoint and environment-based runtime configuration -- `internal/api`: HTTP handlers, peer replication, consensus surface, transport abstraction, sync loops, automation loop, and status endpoints -- `internal/consensus`: signed proposal and vote message primitives -- `internal/dpos`: candidate, vote, validator, and election service logic -- `internal/ledger`: persisted accounts, mempool entries, committed blocks, validator snapshots, round state, consensus artifacts, snapshots, and commit/import logic -- `internal/tx`: transaction envelope validation, address derivation, and signature verification -- `apps/wallet`: reference light wallet built with Vue 3, Vite, and Tailwind CSS -- `docs/`: architecture, API, usage, roadmap, and applications guides -- `paper/`: private local academic paper workspace, draft manuscript materials, and evaluation planning notes kept out of git via `.gitignore` -- `var/`: default local runtime state directory for the node, ignored by git - -## Prerequisites - -- Go 1.22 or newer -- Node.js -- npm - -PowerShell note: if your shell blocks `npm`, use `npm.cmd` instead. - -## Quick Start - -### 1. Run one node - -From the repository root, enable development-only endpoints only when you need the local faucet or manual block tools: - -```powershell -$env:ZEPHYR_ENABLE_DEV_ENDPOINTS="true" -go run ./cmd/node -``` +Zephyr Chain is an open-source blockchain project exploring a clean-break Protocol v2 designed around two equal engineering goals: -For a normal node process, leave `ZEPHYR_ENABLE_DEV_ENDPOINTS` unset or set it to `false`. The node process: - -- listens on `127.0.0.1:8080` by default; set `ZEPHYR_HTTP_ADDR` explicitly to bind another interface -- stores durable state in `var/node` -- produces blocks every `15s` when transactions are queued -- runs the consensus automation ticker every `1s`, but automation stays off until `ZEPHYR_ENABLE_CONSENSUS_AUTOMATION=true` -- uses a `5s` consensus round timeout once automation is enabled -- runs peer sync only if `ZEPHYR_PEERS` is configured -- exposes consensus status even before a validator set has been elected -- keeps `/v1/dev/*` unavailable unless `ZEPHYR_ENABLE_DEV_ENDPOINTS=true` is explicitly configured - -Useful probes: - -```powershell -Invoke-RestMethod http://localhost:8080/health -curl.exe -i http://localhost:8080/v1/health -Invoke-RestMethod http://localhost:8080/v1/alerts -Invoke-RestMethod http://localhost:8080/v1/slo -Invoke-RestMethod http://localhost:8080/v1/alert-rules -Invoke-RestMethod http://localhost:8080/v1/recording-rules -Invoke-RestMethod http://localhost:8080/v1/dashboards -Invoke-RestMethod http://localhost:8080/v1/status -curl.exe http://localhost:8080/metrics -curl.exe http://localhost:8080/v1/alert-rules/prometheus -curl.exe http://localhost:8080/v1/recording-rules/prometheus -curl.exe http://localhost:8080/v1/dashboards/grafana -``` +1. **scale up** toward extremely high transaction throughput when hardware is abundant; +2. **scale down** so verification, payments, relay, data-availability participation and useful network activity remain practical on commodity hardware and eventually inside the Zephyr smartphone wallet. -### 2. Run the wallet +Throughput may degrade when hardware is scarce. **Safety must not.** -In a second terminal: +The long-term mission is described in [Zaphyr-chain_manifesto.md](./Zaphyr-chain_manifesto.md). The executable v2 design is specified in [docs/protocol-v2.md](./docs/protocol-v2.md), and the exact implementation/non-claim boundary is tracked in [docs/protocol-v2-implementation-status.md](./docs/protocol-v2-implementation-status.md). -```powershell -cd apps/wallet -npm install -npm run dev -``` +> Zephyr's aspirational performance target is **1,000,000 transactions per second finalized through consensus**. It is a benchmark target, not a claim about current capacity. -If PowerShell execution policy blocks `npm`, run: +## Protocol v2 direction -```powershell -cd apps/wallet -npm.cmd install -npm.cmd run dev -``` +Zephyr v2 is being built as a clean break rather than preserving experimental wire/state compatibility. -Vite serves the wallet on `http://localhost:5173` by default and proxies `/health`, `/v1`, and `/metrics` to `http://127.0.0.1:8080`, so local development stays same-origin in the browser. Set `ZEPHYR_WALLET_DEV_NODE` to change the dev proxy target, or `VITE_ZEPHYR_API_BASE` when intentionally targeting a different API origin. +The architecture combines: -### 3. Run a two-node local devnet +- genesis-derived network identity and canonical bounded binary consensus encoding; +- separate account, node and validator identities; +- proof-oriented object state backed by an incremental Sparse Merkle Tree; +- proof-carrying transactions and deterministic parallel execution; +- weighted validator consensus, quorum certificates and global finalized headers; +- shard-ready state/execution with finalized asynchronous cross-shard receipts; +- Citizen/light verification intended for resource-constrained and mobile participants; +- deterministic smart-contract execution with a production WASM/Rust path; +- native custom-token creation, fixed/capped/mintable supply policies, mint/burn and transferability rules; +- a native distributed-compute market for workloads such as scientific computing, AI, rendering and other provider-executed jobs; +- authenticated data-availability foundations and libp2p/QUIC networking foundations; +- an oracle-free economic measurement layer for ZPH, compute demand/supply and adaptive monetary-policy experiments. -Node A, producer: +The implementation rule is: -```powershell -$env:ZEPHYR_NODE_ID="node-a" -$env:ZEPHYR_HTTP_ADDR=":8080" -$env:ZEPHYR_DATA_DIR="var/devnet-a" -$env:ZEPHYR_PEERS="http://localhost:8081" -$env:ZEPHYR_ENABLE_BLOCK_PRODUCTION="true" -$env:ZEPHYR_ENABLE_PEER_SYNC="true" -go run ./cmd/node +```text +measure first -> simulate second -> activate last ``` -Node B, replica: - -```powershell -$env:ZEPHYR_NODE_ID="node-b" -$env:ZEPHYR_HTTP_ADDR=":8081" -$env:ZEPHYR_DATA_DIR="var/devnet-b" -$env:ZEPHYR_PEERS="http://localhost:8080" -$env:ZEPHYR_ENABLE_BLOCK_PRODUCTION="false" -$env:ZEPHYR_ENABLE_PEER_SYNC="true" -go run ./cmd/node +## What is implemented on the v2 branch + +The current v2 development branch contains executable foundations and tests for: + +- canonical v2 codec and genesis/network identity; +- P-256 proof-carrying transactions with low-S canonical signatures; +- object/coin state and Sparse-Merkle inclusion/absence proofs; +- durable WAL/checkpoint state persistence and non-mutating state simulation; +- deterministic parallel batch execution; +- v2 proposal/vote/QC consensus and validator-root transitions; +- cross-shard ZPH receipts with proof verification and durable anti-replay state; +- Citizen/light proof verification and wallet-side quorum/state-proof verification; +- custom tokens with fixed, capped and mintable policies plus native mint/burn; +- transferability policy enforcement without serializing independent token transfers on one hot policy object; +- deterministic metered reference smart-contract execution and contract deploy/call paths; +- compute offers, escrow, collateral, assignment, provider results, replicated-majority settlement and objective slashing foundations; +- standardized compute `WorkVector`/`WorkSpec` representation; +- finalized compute settlement receipts that can be reconstructed and verified from chain state; +- ZCPI compute-price measurement from successfully verified standardized work; +- ZCSI compute-scarcity measurement with independent capacity-reliability gates; +- age-weighted native-money velocity and canonical per-shard economic epoch metrics; +- ZAMP shadow monetary-policy evaluation centered near a long-run ~2% net-supply-growth target; +- finalized-block economic collection, cross-epoch compute backlog accounting and chained Merkle-authenticated shadow monetary state; +- automatic **shadow** economic epoch closure and insertion of the pending `MonetaryEpochState` into the next normal consensus candidate; +- Reed-Solomon data-availability reconstruction foundations; +- libp2p/QUIC transport foundations; +- v1 and v2 consensus/performance CI labs. + +These are development/protocol foundations. They are not equivalent to a production public network. + +## Citizen Node / mobile goal + +A core Zephyr goal is to make the wallet a real network participant rather than a thin client that must trust a large RPC provider. + +The intended Citizen Node can progressively perform: + +```text +header + quorum verification + -> account/object proof verification + -> transaction relay + -> data-availability sampling/cache + -> opportunistic recent execution ``` -Use the wallet against Node A. Node B will follow through transaction, block, snapshot, and consensus-artifact sync from admitted peers. - -### 4. Enable certificate-gated commit/import +Participation should adapt to battery, connectivity and device resources. Smartphones are not assumed to be always-on validators. Consensus liveness must remain possible on inexpensive commodity hardware even when mobile operating systems suspend background work. -For production-style consensus enforcement on the current devnet flow, run a node with: +Two benchmark axes are therefore first-class: -```powershell -$env:ZEPHYR_NODE_ID="node-a" -$env:ZEPHYR_VALIDATOR_ADDRESS="zph_validator_a" -$env:ZEPHYR_VALIDATOR_PRIVATE_KEY="" -$env:ZEPHYR_ENFORCE_PROPOSER_SCHEDULE="true" -$env:ZEPHYR_REQUIRE_CONSENSUS_CERTIFICATES="true" -go run ./cmd/node +```text +How fast can Zephyr go? +How small can Zephyr run? ``` -Then use: +## Sharding and parallel execution -```powershell -Invoke-RestMethod http://localhost:8080/v1/status -Invoke-RestMethod http://localhost:8080/v1/dev/block-template -Invoke-RestMethod http://localhost:8080/v1/consensus -``` +Zephyr v2 is shard-aware but does not assume that more shards are automatically better. -`GET /v1/status` exposes the node's signed transport identity when `ZEPHYR_VALIDATOR_PRIVATE_KEY` is configured. The template response gives you the exact `height`, `previousHash`, `producedAt`, full `transactions`, ordered `transactionIds`, and `blockHash` that a signed proposal must certify. Once a matching quorum certificate exists, `POST /v1/dev/produce-block` can commit that exact block candidate from the stored proposal body. - -### 5. Enable autonomous certified consensus on a devnet - -Initial round-0 proposer: - -```powershell -$env:ZEPHYR_NODE_ID="node-a" -$env:ZEPHYR_HTTP_ADDR=":8080" -$env:ZEPHYR_DATA_DIR="var/devnet-a" -$env:ZEPHYR_PEERS="http://localhost:8081" -$env:ZEPHYR_VALIDATOR_PRIVATE_KEY="" -$env:ZEPHYR_ENABLE_BLOCK_PRODUCTION="true" -$env:ZEPHYR_ENABLE_PEER_SYNC="true" -$env:ZEPHYR_ENABLE_CONSENSUS_AUTOMATION="true" -$env:ZEPHYR_CONSENSUS_INTERVAL="250ms" -$env:ZEPHYR_CONSENSUS_ROUND_TIMEOUT="2s" -$env:ZEPHYR_ENFORCE_PROPOSER_SCHEDULE="true" -$env:ZEPHYR_REQUIRE_CONSENSUS_CERTIFICATES="true" -go run ./cmd/node -``` +The safe activation value remains a single shard until controlled 4/16-shard tests demonstrate safety, recovery and useful scaling. -Second validator: - -```powershell -$env:ZEPHYR_NODE_ID="node-b" -$env:ZEPHYR_HTTP_ADDR=":8081" -$env:ZEPHYR_DATA_DIR="var/devnet-b" -$env:ZEPHYR_PEERS="http://localhost:8080" -$env:ZEPHYR_VALIDATOR_PRIVATE_KEY="" -$env:ZEPHYR_ENABLE_BLOCK_PRODUCTION="true" -$env:ZEPHYR_ENABLE_PEER_SYNC="true" -$env:ZEPHYR_ENABLE_CONSENSUS_AUTOMATION="true" -$env:ZEPHYR_CONSENSUS_INTERVAL="250ms" -$env:ZEPHYR_CONSENSUS_ROUND_TIMEOUT="2s" -$env:ZEPHYR_REQUIRE_PEER_IDENTITY="true" -$env:ZEPHYR_PEER_VALIDATORS="http://localhost:8080=zph_validator_a" -$env:ZEPHYR_REQUIRE_CONSENSUS_CERTIFICATES="true" -go run ./cmd/node -``` +The intended model is: -With an active validator set and queued transactions, the scheduled proposer self-builds and signs the next proposal, active validators auto-vote, and the scheduled proposer auto-commits as soon as a matching quorum certificate exists. If the scheduled proposer stalls past the round timeout, the node advances the round, rotates the proposer, and the new proposer can reuse the latest stored candidate body for that same height. - -## Runtime Configuration - -### Node - -- `ZEPHYR_HTTP_ADDR`: HTTP bind address for the Go node -- `ZEPHYR_NODE_ID`: human-readable node identifier used in peer replication headers and status output -- `ZEPHYR_VALIDATOR_ADDRESS`: chain-level validator address used for proposer-schedule enforcement and status reporting -- `ZEPHYR_VALIDATOR_PRIVATE_KEY`: base64-encoded PKCS#8 P-256 private key used to derive and sign the node's validator transport identity plus automated proposal and vote messages -- `ZEPHYR_DATA_DIR`: local directory used for durable node state -- `ZEPHYR_PEERS`: comma-separated peer base URLs such as `http://localhost:8081,http://localhost:8082` -- `ZEPHYR_BLOCK_INTERVAL`: automatic block-production interval such as `15s` -- `ZEPHYR_CONSENSUS_INTERVAL`: automation ticker interval such as `250ms` or `1s` -- `ZEPHYR_CONSENSUS_ROUND_TIMEOUT`: timeout window for the active round before automation advances to the next round -- `ZEPHYR_SYNC_INTERVAL`: peer poll/sync interval such as `5s` -- `ZEPHYR_MAX_TXS_PER_BLOCK`: maximum committed transactions per produced block -- `ZEPHYR_ENABLE_BLOCK_PRODUCTION`: `true` or `false` -- `ZEPHYR_ENABLE_CONSENSUS_AUTOMATION`: `true` or `false`; when enabled, active validators automatically propose, vote, and advance rounds on the current devnet path -- `ZEPHYR_ENABLE_PEER_SYNC`: `true` or `false` -- `ZEPHYR_ENABLE_STRUCTURED_LOGS`: `true` or `false`; when enabled, the node emits newline-delimited JSON event logs for diagnostics, peer incidents, and snapshot recovery -- `ZEPHYR_REQUIRE_PEER_IDENTITY`: `true` or `false`; when enabled, replicated peer POST requests must include a valid signed transport identity -- `ZEPHYR_PEER_VALIDATORS`: comma-separated `=` bindings used to pin configured peers to expected validators -- `ZEPHYR_ENFORCE_PROPOSER_SCHEDULE`: `true` or `false`; when enabled and a validator set exists, only the scheduled proposer for the active round may produce the next block locally -- `ZEPHYR_REQUIRE_CONSENSUS_CERTIFICATES`: `true` or `false`; when enabled and a validator set exists, local block commit and remote block import require a matching proposal and quorum certificate - -Default values: - -- `ZEPHYR_HTTP_ADDR`: `:8080` -- `ZEPHYR_NODE_ID`: `node-local` -- `ZEPHYR_VALIDATOR_ADDRESS`: empty -- `ZEPHYR_VALIDATOR_PRIVATE_KEY`: empty -- `ZEPHYR_DATA_DIR`: `var/node` -- `ZEPHYR_PEERS`: empty -- `ZEPHYR_BLOCK_INTERVAL`: `15s` -- `ZEPHYR_CONSENSUS_INTERVAL`: `1s` -- `ZEPHYR_CONSENSUS_ROUND_TIMEOUT`: `5s` -- `ZEPHYR_SYNC_INTERVAL`: `5s` -- `ZEPHYR_MAX_TXS_PER_BLOCK`: `100` -- `ZEPHYR_ENABLE_BLOCK_PRODUCTION`: `true` -- `ZEPHYR_ENABLE_CONSENSUS_AUTOMATION`: `false` -- `ZEPHYR_ENABLE_PEER_SYNC`: `true` -- `ZEPHYR_ENABLE_STRUCTURED_LOGS`: `false` -- `ZEPHYR_REQUIRE_PEER_IDENTITY`: `false` -- `ZEPHYR_PEER_VALIDATORS`: empty -- `ZEPHYR_ENFORCE_PROPOSER_SCHEDULE`: `false` -- `ZEPHYR_REQUIRE_CONSENSUS_CERTIFICATES`: `false` - -### Wallet - -- `VITE_ZEPHYR_API_BASE`: base URL used by the wallet for node API calls -- default: `http://localhost:8080` - -Example `.env.local` inside `apps/wallet`: - -```env -VITE_ZEPHYR_API_BASE=http://localhost:8080 +```text +shard-local parallel execution + -> shard state/data/receipt commitments + -> global finalized commitment/QC ``` -## How The MVP Works - -1. The wallet generates an ECDSA P-256 keypair in the browser using Web Crypto. -2. It derives a Zephyr-style address from the SHA-256 hash of the exported public key. -3. The wallet stores the private key, public key, and address in browser `localStorage`. -4. The wallet can inspect node-side account state and use a dev faucet for local funding. -5. The wallet signs a canonical transaction payload locally and sends the signed envelope to the node. -6. The node validates the payload, address, signature, nonce, and available balance before persisting the transaction in the durable mempool. -7. A block-producing node can build a deterministic next-block template from the current mempool and latest chain tip. -8. DPoS elections persist a durable validator snapshot with versioning, voting-power totals, and next-proposer scheduling metadata. -9. Consensus state now persists the active height, round, and round start time separately from blocks and validator snapshots. -10. Operators can still submit signed proposals for the active round's concrete block template, including the exact `previousHash`, `producedAt`, full `transactions`, ordered `transactionIds`, and derived `blockHash`. -11. Validators can verify those proposal transactions directly from the proposal body instead of depending on local mempool convergence alone. -12. If consensus automation is enabled, the scheduled proposer for the active round can build that same template, sign a proposal, persist it, and disseminate it without an operator POST. -13. Active validators with automation enabled can sign and replicate a vote for the current known proposal. -14. If the active round times out, the node advances the round, rotates the scheduled proposer, and can accept higher-round messages from peers even if its own timer had not fired yet. -15. A new higher-round proposer can reuse the latest stored candidate body for that height instead of depending only on local mempool state. -16. Once vote power crosses quorum, the node stores a durable commit certificate artifact for that height and round. -17. If proposer-schedule enforcement is enabled, a node can refuse to produce a block unless its configured validator address matches the scheduled proposer for the active round. -18. If consensus-certificate enforcement is enabled, local block commit and remote block import both require a proposal and quorum certificate for the exact block template being committed. -19. A scheduled proposer with automation enabled and certificate enforcement turned on can auto-commit immediately from the stored certified proposal body. -20. A consensus-gated local commit can replay the stored certified proposal body even when the local mempool does not contain that candidate anymore. -21. If a validator private key is configured, the node derives a signed transport identity for its validator address and exposes that proof in runtime status. -22. Configured peer nodes receive transactions, self-contained consensus proposals, votes, and blocks over the current transport implementation, verify peer identity proofs when available, enforce admission and validator binding when configured, import blocks when possible, and fall back to snapshot restore when they need catch-up. - -## Current Limitations - -- the current multi-node layer is still HTTP-based under the new transport abstraction, not libp2p networking -- peer admission and validator pinning can now be enforced over the current HTTP transport, but peer discovery is still static configuration rather than libp2p -- the round engine now supports timeout-driven proposer rotation, latest-artifact rebroadcast after link recovery, richer `roundEvidence`, per-height `roundHistory`, `blockReadiness`, import-aware `recovery`, durable peer-sync incident history, bounded rejection diagnostics, machine-readable `GET /v1/metrics`, Prometheus-style `GET /metrics`, derived `GET /v1/health`, derived `GET /v1/alerts` including peer import, admission, replication, and snapshot-restore warnings, derived `GET /v1/slo`, recommended `GET /v1/alert-rules`, exported `GET /v1/alert-rules/prometheus`, recommended `GET /v1/recording-rules`, exported `GET /v1/recording-rules/prometheus`, recommended `GET /v1/dashboards`, exported `GET /v1/dashboards/grafana`, and local consensus-action history across restart for proposal, vote, round-advance, block-commit, import, and snapshot-repair events, but broader recovery tooling is still missing -- crash recovery now persists active round metadata plus a bounded local consensus-action WAL, and peer snapshot restore preserves local recovery, diagnostics, and peer-sync incident history, but replay coverage is still centered on local proposal, vote, certified block-commit, and import-repair paths rather than the full consensus lifecycle -- DPoS elections still happen through an API call, not an on-chain staking/governance flow -- snapshot restore is a state catch-up mechanism, not a trust-minimized proof-based sync protocol -- WASM smart-contract execution is planned, but not implemented yet -- confidential compute jobs, worker attestation, escrow, and settlement are planned, but not implemented yet -- wallet private keys are stored unencrypted in browser `localStorage` - -Because of these limitations, the current MVP should still be treated as a development prototype, not a production blockchain network. - -## Roadmap - -The production roadmap now lives in [docs/roadmap.md](./docs/roadmap.md). - -Short version: - -1. Move the new enforced HTTP peer-admission and validator-binding policy toward authenticated libp2p discovery plus replay-safe transport behavior. -2. Extend the new `blockReadiness`, `roundHistory`, `roundEvidence`, `recovery`, `diagnostics`, `peerSyncHistory`, `peerSyncSummary`, per-peer `recentIncidents`, `GET /v1/metrics`, `GET /metrics`, `GET /v1/health`, `GET /v1/alerts`, `GET /v1/slo`, `GET /v1/alert-rules`, `GET /v1/alert-rules/prometheus`, `GET /v1/recording-rules`, `GET /v1/recording-rules/prometheus`, `GET /v1/dashboards`, `GET /v1/dashboards/grafana`, and structured event logs into deeper recovery, longer-horizon incident retention, richer exported metrics, broader dashboard coverage, and production incident tooling. -3. Move validator lifecycle changes behind staking, delegation, slashing, and governance state transitions. -4. Add deterministic WASM execution, native fee metering, and the confidential compute lane. -5. Add production observability, recovery tooling, and public testnet operations. - -## Documentation - -- [docs/architecture.md](./docs/architecture.md) -- [docs/api.md](./docs/api.md) -- [docs/usage.md](./docs/usage.md) -- [docs/roadmap.md](./docs/roadmap.md) -- [docs/applications.md](./docs/applications.md) -- [paper/README.md](./paper/README.md) -- [Zaphyr-chain_manifesto.md](./Zaphyr-chain_manifesto.md) - -## License - -Zephyr Chain is licensed under the MIT License. See [LICENSE](./LICENSE). - - - - +Cross-shard native payments use finalized one-time receipts. General synchronous atomic cross-shard smart-contract semantics are deliberately not a v0 requirement. +## Smart contracts +The repository includes a bounded deterministic reference runtime and contract deploy/call execution path. The production direction is deterministic metered **WASM**, with Rust-first tooling/SDK work planned around a stable ABI and cross-machine conformance tests. +Validators must never depend on nondeterministic clock, network, filesystem or host randomness during consensus-critical execution. +## Native distributed compute +Heavy compute is intentionally separated from deterministic validator execution. +Examples include: +- scientific/HPC workloads; +- AI inference or training; +- video/3D rendering; +- simulation and optimization; +- other provider-executed workloads whose result can be verified through an approved evidence policy. +The chain manages economic and verification state such as: +```text +job -> escrow -> assignment -> provider result -> verification -> settlement +``` +Validators verify bounded settlement evidence rather than re-running arbitrarily expensive workloads. +Verification modes have explicit protocol boundaries for deterministic replication, replicated majority, challenge systems, ZK evidence, TEE evidence, client approval and future hybrids. Production ZK/TEE integrations are not yet claimed. +## Compute economics: ZCR, ZCPI and ZCSI +There is no honest single universal scalar that makes CPU, GPU tensor work, FP64 scientific work, memory, storage and network usage equivalent. +Zephyr therefore models compute as a normalized resource/work vector and only prices standardized workload classes whose definitions are committed by `WorkSpec`. +`ZCPI` measures the ZPH actually paid for finalized, verification-satisfied standardized work. It excludes provider-advertised offer prices and theoretical peak FLOPS. +`ZCSI` combines signals such as: +- escrow-backed standardized demand; +- verified standardized supply; +- opening/closing backlog; +- fulfilled work; +- compute utilization; +- reliable ZCPI trend. +Long jobs are accounted as stock-flow across epochs: +```text +opening backlog + new demand += +fulfilled + expired + closing backlog +``` +A numeric capacity value cannot make ZCSI trustworthy by itself. Compute supply has a separate reliability flag and remains monetarily inert until capacity can be derived from a consensus-reproducible benchmark/collateral/availability mechanism. +See [docs/compute-economics-v2.md](./docs/compute-economics-v2.md) and [docs/economic-runtime-v2.md](./docs/economic-runtime-v2.md). +## ZPH tokenomics — shadow mode +The v2 economic design does **not** assume a fixed maximum supply. +The research direction is an oracle-free burn/mint controller centered near approximately 2% long-run effective net supply growth, with bounded/rate-limited adjustments from on-chain signals such as: +- fee burn; +- staking/locked supply; +- protocol reserve; +- chain resource utilization; +- finalized operations; +- age-weighted money velocity; +- potentially reliable compute scarcity. +Compute feedback currently has three simulation modes: +```text +A — observe only +B — change suggested compute-reward routing only +C — B plus a narrow bounded shadow inflation correction +``` +Mode B is the preferred first activation candidate if long-run devnet evidence supports it. **No current mode mints live ZPH.** Suggested issuance is stored only as shadow economic state for replay and analysis. +See [docs/tokenomics-v2.md](./docs/tokenomics-v2.md) and [docs/economic-state-v2.md](./docs/economic-state-v2.md). +## Consensus & Performance Lab +Zephyr defines throughput as transactions **finalized through validator consensus**, not HTTP ingress or mempool acceptance. +CI includes multi-validator conformance, partition/recovery stress, finalized-throughput sampling, signature verification baselines and v2 batch-scaling tests. +Shared GitHub runner numbers are development signals only and must not be presented as production capacity claims. +See [docs/performance-lab.md](./docs/performance-lab.md). +## Current non-claims +The repository must not currently be presented as having production-ready: +- live adaptive ZPH issuance or final monetary parameters; +- active validator/compute/reserve reward distribution; +- production gas/resource pricing; +- authenticated governance-controlled economic parameters; +- production benchmarked/collateralized compute-capacity registry; +- production ZK/TEE confidential-compute integrations; +- audited deterministic WASM engine and stable Rust SDK; +- mobile OS lifecycle/background integration with measured device budgets; +- production peer discovery/NAT/mobile relay/shard-aware gossip; +- dynamic resharding or public 4/16-shard activation; +- public-mainnet operational/security maturity. +The authoritative detailed list is [docs/protocol-v2-implementation-status.md](./docs/protocol-v2-implementation-status.md). +## Repository layout +```text +apps/wallet/ Vue reference wallet / Citizen verification work +cmd/node/ legacy/current node entrypoint +cmd/compute-provider/ compute-provider tooling foundation +cmd/zephyr-econ-sim/ deterministic economic replay simulator +internal/v2/ clean-break Protocol v2 implementation +internal/v2/economics/ ZCPI, ZCSI, ZAMP, fee and epoch accounting +internal/v2/compute/ native distributed-compute market/state +internal/v2/node/ v2 candidate/finality runtime +internal/v2/worldstate/ proof-oriented in-memory/durable state +internal/v2/network/ v2 networking foundations +mobile/ Citizen/mobile policy foundations +docs/ protocol, economics, performance and roadmap docs +``` +The pre-v2 packages remain in the repository because the existing Consensus & Performance Lab and hardening work are still valuable regression gates while v2 is developed. +## Development checks +From the repository root: +```bash +gofmt -w ./internal/v2 ./mobile ./cmd/zephyr-econ-sim +go vet ./... +go test ./... +``` +Wallet: +```bash +cd apps/wallet +npm ci +npm run build +``` +The GitHub workflows are the authoritative shared gate for the v1 Lab, V2 Lab, wallet build and dependency lock. +## Documentation +Start with: +- [Protocol v2 architecture](./docs/protocol-v2.md) +- [Protocol v2 implementation status](./docs/protocol-v2-implementation-status.md) +- [Validator trust model](./docs/protocol-v2-validator-trust.md) +- [Tokenomics v2](./docs/tokenomics-v2.md) +- [Compute economics v2](./docs/compute-economics-v2.md) +- [Economic state v2](./docs/economic-state-v2.md) +- [Finalized economic runtime](./docs/economic-runtime-v2.md) +- [Consensus & Performance Lab](./docs/performance-lab.md) +- [Roadmap](./docs/roadmap.md) +- [Project manifesto](./Zaphyr-chain_manifesto.md) +## License +Zephyr Chain is licensed under the **Apache License, Version 2.0**. See [LICENSE](./LICENSE). +The redistribution attribution notice is provided in [NOTICE](./NOTICE) and must be preserved as required by the Apache License 2.0. From 353ac3230c544aa21ae78e7f88e7a0b325f6c701 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:46:32 +0200 Subject: [PATCH 239/274] Fix runtime economics ownership test precondition --- internal/v2/node/economics_runtime_ownership_test.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/v2/node/economics_runtime_ownership_test.go b/internal/v2/node/economics_runtime_ownership_test.go index 1513fdb7..dc8a0d33 100644 --- a/internal/v2/node/economics_runtime_ownership_test.go +++ b/internal/v2/node/economics_runtime_ownership_test.go @@ -33,11 +33,7 @@ func TestRuntimeOwnsIndependentEconomicsCollector(t *testing.T) { if err := collector.AdvanceEpoch(2); err != nil { t.Fatal(err) } - metrics, _, err := runtime.EconomicEpochSnapshot() - if err != nil { - t.Fatal(err) - } - if len(metrics) != 1 || metrics[0].Epoch != 1 { - t.Fatalf("external collector mutation leaked into runtime: %#v", metrics) + if runtime.economicCollector == nil || runtime.economicCollector.Epoch() != 1 { + t.Fatalf("external collector mutation leaked into runtime: runtime epoch=%d", runtime.economicCollector.Epoch()) } } From fadc9dd0c82e883b42a52d3f4006ce0e5d9d3c1c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:50:17 +0200 Subject: [PATCH 240/274] Commit normalized compute workload registry --- internal/v2/compute/work.go | 44 ++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/internal/v2/compute/work.go b/internal/v2/compute/work.go index 909cd090..b3a6c8e2 100644 --- a/internal/v2/compute/work.go +++ b/internal/v2/compute/work.go @@ -1,8 +1,10 @@ package compute import ( + "bytes" "errors" "math" + "sort" "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" @@ -167,7 +169,7 @@ func (r *WorkRegistry) Register(spec WorkSpec) error { if existing, ok := r.byWorkload[spec.WorkloadHash]; ok { a, _ := existing.MarshalBinary() b, _ := spec.MarshalBinary() - if string(a) != string(b) { + if !bytes.Equal(a, b) { return ErrInvalidWorkRegistry } return nil @@ -184,6 +186,46 @@ func (r *WorkRegistry) Resolve(workload types.Hash) (WorkSpec, bool) { return spec, ok } +// CanonicalSpecs returns an independent workload-hash-sorted registry view. +// Registry insertion order must never affect economic replay or checkpoint +// identity. +func (r *WorkRegistry) CanonicalSpecs() ([]WorkSpec, error) { + if r == nil { + return nil, ErrInvalidWorkRegistry + } + out := make([]WorkSpec, 0, len(r.byWorkload)) + for _, spec := range r.byWorkload { + if err := spec.Validate(); err != nil { + return nil, ErrInvalidWorkRegistry + } + out = append(out, spec) + } + sort.Slice(out, func(i, j int) bool { + return bytes.Compare(out[i].WorkloadHash[:], out[j].WorkloadHash[:]) < 0 + }) + return out, nil +} + +// Hash commits the exact normalized workload definitions used by ZCPI/ZCSI. +// Checkpoints bind this hash instead of serializing an implicitly trusted +// registry snapshot; restore must be supplied the same registry explicitly. +func (r *WorkRegistry) Hash() (types.Hash, error) { + specs, err := r.CanonicalSpecs() + if err != nil { + return types.Hash{}, err + } + var w codec.Writer + w.U32(uint32(len(specs))) + for _, spec := range specs { + raw, err := spec.MarshalBinary() + if err != nil { + return types.Hash{}, ErrInvalidWorkRegistry + } + w.Bytes(raw) + } + return types.Hash(codec.DomainHash("zephyr/work-registry/v2", w.BytesCopy())), nil +} + // VerifiedWork is an index-eligible observation. It can only be derived from a // finalized/verified on-chain settlement plus a workload spec already approved // by the protocol registry. Offer prices and provider self-reported capacity do From 1e3d604e48ae18271468f40f43b84f247d45d312 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:50:42 +0200 Subject: [PATCH 241/274] Test deterministic workload registry commitment --- internal/v2/compute/work_test.go | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/internal/v2/compute/work_test.go b/internal/v2/compute/work_test.go index d5cbab2a..447beb91 100644 --- a/internal/v2/compute/work_test.go +++ b/internal/v2/compute/work_test.go @@ -81,3 +81,49 @@ func TestWorkRegistryRejectsConflictingDefinition(t *testing.T) { t.Fatalf("expected conflicting registry definition rejection, got %v", err) } } + +func TestWorkRegistryHashIsInsertionOrderIndependent(t *testing.T) { + specA := WorkSpec{ + Version: WorkSpecVersion, Class: WorkCPUGeneral, Units: 10, + WorkloadHash: types.Hash{1}, BenchmarkHash: types.Hash{11}, + Vector: WorkVector{CPUUnits: 10}, + } + specB := WorkSpec{ + Version: WorkSpecVersion, Class: WorkRendering, Units: 20, + WorkloadHash: types.Hash{2}, BenchmarkHash: types.Hash{12}, + Vector: WorkVector{GPUFP32Units: 20}, + } + first, err := NewWorkRegistry([]WorkSpec{specA, specB}) + if err != nil { + t.Fatal(err) + } + second, err := NewWorkRegistry([]WorkSpec{specB, specA}) + if err != nil { + t.Fatal(err) + } + firstHash, err := first.Hash() + if err != nil { + t.Fatal(err) + } + secondHash, err := second.Hash() + if err != nil { + t.Fatal(err) + } + if firstHash != secondHash { + t.Fatalf("registry insertion order changed commitment: %x != %x", firstHash, secondHash) + } + + changed := specB + changed.Units++ + third, err := NewWorkRegistry([]WorkSpec{specA, changed}) + if err != nil { + t.Fatal(err) + } + thirdHash, err := third.Hash() + if err != nil { + t.Fatal(err) + } + if thirdHash == firstHash { + t.Fatal("changed workload definition did not change registry commitment") + } +} From 39788f56a8ec64a37874bfe84fb17b9f4c336070 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:51:28 +0200 Subject: [PATCH 242/274] Clone normalized workload registry for runtime ownership --- internal/v2/compute/work.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/v2/compute/work.go b/internal/v2/compute/work.go index b3a6c8e2..c2d90b20 100644 --- a/internal/v2/compute/work.go +++ b/internal/v2/compute/work.go @@ -206,6 +206,14 @@ func (r *WorkRegistry) CanonicalSpecs() ([]WorkSpec, error) { return out, nil } +func (r *WorkRegistry) Clone() (*WorkRegistry, error) { + specs, err := r.CanonicalSpecs() + if err != nil { + return nil, err + } + return NewWorkRegistry(specs) +} + // Hash commits the exact normalized workload definitions used by ZCPI/ZCSI. // Checkpoints bind this hash instead of serializing an implicitly trusted // registry snapshot; restore must be supplied the same registry explicitly. From d48554b516d3c05fa4328e3244bc59d96ff28383 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:52:49 +0200 Subject: [PATCH 243/274] Add canonical finalized economics collector checkpoint --- .../finalized_collector_checkpoint.go | 406 ++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 internal/v2/economics/finalized_collector_checkpoint.go diff --git a/internal/v2/economics/finalized_collector_checkpoint.go b/internal/v2/economics/finalized_collector_checkpoint.go new file mode 100644 index 00000000..1fd7b0e5 --- /dev/null +++ b/internal/v2/economics/finalized_collector_checkpoint.go @@ -0,0 +1,406 @@ +package economics + +import ( + "math/big" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + collectorCheckpointVersion uint16 = 1 + maxCheckpointVerifiedWork = 1_000_000 + maxVelocityAccumulatorBytes = 128 +) + +// CheckpointBytes serializes the exact mid-epoch collector state. The workload +// registry itself is not serialized; only its commitment is stored so restore +// must be supplied the intended registry explicitly. +func (c *EpochCollector) CheckpointBytes() ([]byte, error) { + if c == nil || c.config.Epoch == 0 || c.config.ShardCount == 0 || len(c.verifiedWork) > maxCheckpointVerifiedWork { + return nil, ErrFinalizedEconomics + } + registryHash, err := collectorRegistryHash(c.config.WorkRegistry) + if err != nil { + return nil, err + } + var w codec.Writer + w.U16(collectorCheckpointVersion) + w.U64(c.config.Epoch) + w.U32(c.config.ShardCount) + w.Fixed(c.config.NativeToken[:]) + w.Fixed(registryHash[:]) + w.U64(c.config.VelocityPolicy.MinAgeBlocks) + w.U64(c.config.VelocityPolicy.FullWeightAgeBlocks) + w.U32(c.config.VelocityPolicy.MaxVelocityBps) + w.U32(c.config.FeePolicy.BurnBps) + w.U32(c.config.FeePolicy.ValidatorBps) + w.U32(c.config.FeePolicy.ReserveBps) + w.U64(c.lastHeight) + + for shard := uint32(0); shard < c.config.ShardCount; shard++ { + acc := c.shards[shard] + if acc == nil || acc.velocity == nil || c.config.ResourceCapacityPerBlock[shard] == 0 { + return nil, ErrFinalizedEconomics + } + w.U64(c.config.ResourceCapacityPerBlock[shard]) + w.U64(c.supply[shard]) + w.U64(acc.fees.Total) + w.U64(acc.fees.Burn) + w.U64(acc.fees.Validators) + w.U64(acc.fees.Reserve) + w.U64(acc.operations) + w.U64(acc.resourceUsed) + w.U64(acc.resourceCapacity) + w.U64(acc.openingBacklog) + w.U64(acc.newDemand) + w.U64(acc.fulfilled) + w.U64(acc.expired) + w.U64(acc.closingBacklog) + w.U64(acc.computeSupply) + w.Bool(acc.computeSupplyReliable) + weighted := acc.velocity.weightedValueBps.Bytes() + if len(weighted) > maxVelocityAccumulatorBytes { + return nil, ErrFinalizedEconomics + } + w.Bytes(weighted) + w.U64(acc.velocity.observedSpends) + w.U64(acc.velocity.eligibleSpends) + w.U64(acc.velocity.unknownAgeSpends) + w.U64(acc.velocity.freshSpends) + } + + w.U32(uint32(len(c.verifiedWork))) + for _, observation := range c.verifiedWork { + if err := writeVerifiedWork(&w, observation); err != nil { + return nil, err + } + } + return w.BytesCopy(), nil +} + +func RestoreEpochCollector(data []byte, registry *compute.WorkRegistry) (*EpochCollector, error) { + r := codec.NewReader(data) + version, err := r.U16() + if err != nil || version != collectorCheckpointVersion { + return nil, ErrFinalizedEconomics + } + epoch, err := r.U64() + if err != nil || epoch == 0 { + return nil, ErrFinalizedEconomics + } + shardCount, err := r.U32() + if err != nil || shardCount == 0 || shardCount > 1_000_000 { + return nil, ErrFinalizedEconomics + } + nativeRaw, err := r.Fixed(32) + if err != nil { + return nil, ErrFinalizedEconomics + } + registryRaw, err := r.Fixed(32) + if err != nil { + return nil, ErrFinalizedEconomics + } + var native types.TokenID + var expectedRegistryHash types.Hash + copy(native[:], nativeRaw) + copy(expectedRegistryHash[:], registryRaw) + if types.IsZero32([32]byte(native)) { + return nil, ErrFinalizedEconomics + } + actualRegistryHash, err := collectorRegistryHash(registry) + if err != nil || actualRegistryHash != expectedRegistryHash { + return nil, ErrFinalizedEconomics + } + velocityPolicy := VelocityPolicy{} + velocityPolicy.MinAgeBlocks, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + velocityPolicy.FullWeightAgeBlocks, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + velocityPolicy.MaxVelocityBps, err = r.U32() + if err != nil { + return nil, ErrFinalizedEconomics + } + if _, err := NewVelocityAccumulator(velocityPolicy); err != nil { + return nil, err + } + feePolicy := FeePolicy{} + feePolicy.BurnBps, err = r.U32() + if err != nil { + return nil, ErrFinalizedEconomics + } + feePolicy.ValidatorBps, err = r.U32() + if err != nil { + return nil, ErrFinalizedEconomics + } + feePolicy.ReserveBps, err = r.U32() + if err != nil || feePolicy != CompatibilityFeePolicy() { + return nil, ErrFinalizedEconomics + } + lastHeight, err := r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + + capacity := make(map[uint32]uint64, shardCount) + supply := make(map[uint32]uint64, shardCount) + shards := make(map[uint32]*shardEpochAccumulator, shardCount) + for shard := uint32(0); shard < shardCount; shard++ { + perBlock, err := r.U64() + if err != nil || perBlock == 0 { + return nil, ErrFinalizedEconomics + } + capacity[shard] = perBlock + supply[shard], err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc := &shardEpochAccumulator{} + acc.fees.Total, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.fees.Burn, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.fees.Validators, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.fees.Reserve, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.operations, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.resourceUsed, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.resourceCapacity, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.openingBacklog, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.newDemand, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.fulfilled, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.expired, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.closingBacklog, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.computeSupply, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.computeSupplyReliable, err = r.Bool() + if err != nil { + return nil, ErrFinalizedEconomics + } + weighted, err := r.Bytes(maxVelocityAccumulatorBytes) + if err != nil { + return nil, ErrFinalizedEconomics + } + velocity, err := NewVelocityAccumulator(velocityPolicy) + if err != nil { + return nil, err + } + velocity.weightedValueBps.SetBytes(weighted) + velocity.observedSpends, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + velocity.eligibleSpends, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + velocity.unknownAgeSpends, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + velocity.freshSpends, err = r.U64() + if err != nil { + return nil, ErrFinalizedEconomics + } + acc.velocity = velocity + if err := validateRestoredAccumulator(acc); err != nil { + return nil, err + } + shards[shard] = acc + } + + verifiedCount, err := r.U32() + if err != nil || verifiedCount > maxCheckpointVerifiedWork { + return nil, ErrFinalizedEconomics + } + verified := make([]compute.VerifiedWork, int(verifiedCount)) + for i := range verified { + verified[i], err = readVerifiedWork(r) + if err != nil { + return nil, err + } + } + if r.Done() != nil { + return nil, ErrFinalizedEconomics + } + ownedRegistry, err := cloneCollectorRegistry(registry) + if err != nil { + return nil, err + } + return &EpochCollector{ + config: EpochCollectorConfig{ + Epoch: epoch, ShardCount: shardCount, NativeToken: native, + InitialCirculatingSupply: copyShardMap(supply), + OpeningComputeBacklog: make(map[uint32]uint64, shardCount), + ResourceCapacityPerBlock: capacity, + VelocityPolicy: velocityPolicy, + FeePolicy: feePolicy, + WorkRegistry: ownedRegistry, + }, + shards: shards, supply: supply, verifiedWork: verified, lastHeight: lastHeight, + }, nil +} + +func collectorRegistryHash(registry *compute.WorkRegistry) (types.Hash, error) { + if registry == nil { + return types.Hash{}, nil + } + return registry.Hash() +} + +func cloneCollectorRegistry(registry *compute.WorkRegistry) (*compute.WorkRegistry, error) { + if registry == nil { + return nil, nil + } + return registry.Clone() +} + +func validateRestoredAccumulator(acc *shardEpochAccumulator) error { + if acc == nil || acc.velocity == nil || acc.resourceUsed > acc.resourceCapacity || + acc.fees.Total != acc.fees.Burn+acc.fees.Validators+acc.fees.Reserve || + !validComputeFlow(acc.openingBacklog, acc.newDemand, acc.fulfilled, acc.expired, acc.closingBacklog) || + (acc.computeSupplyReliable && acc.fulfilled > acc.computeSupply) || + acc.velocity.eligibleSpends > acc.velocity.observedSpends || + acc.velocity.unknownAgeSpends > acc.velocity.observedSpends || + acc.velocity.freshSpends > acc.velocity.observedSpends { + return ErrFinalizedEconomics + } + return nil +} + +func writeVerifiedWork(w *codec.Writer, observation compute.VerifiedWork) error { + if w == nil || types.IsZero32([32]byte(observation.JobID)) || observation.Class <= compute.WorkUnknown || + observation.Class >= compute.WorkClassCount || observation.Units == 0 || observation.Vector.IsZero() || observation.PaidZPH == 0 || + observation.Verification <= compute.VerificationUnknown || observation.Verification > compute.VerificationHybrid || + types.IsZero32([32]byte(observation.ResultRoot)) { + return ErrFinalizedEconomics + } + w.Fixed(observation.JobID[:]) + w.U8(uint8(observation.Class)) + w.U64(observation.Units) + writeWorkVector(w, observation.Vector) + w.U64(observation.PaidZPH) + w.U8(uint8(observation.Verification)) + w.Fixed(observation.ResultRoot[:]) + return nil +} + +func readVerifiedWork(r *codec.Reader) (compute.VerifiedWork, error) { + jobRaw, err := r.Fixed(32) + if err != nil { + return compute.VerifiedWork{}, ErrFinalizedEconomics + } + class, err := r.U8() + if err != nil { + return compute.VerifiedWork{}, ErrFinalizedEconomics + } + units, err := r.U64() + if err != nil { + return compute.VerifiedWork{}, ErrFinalizedEconomics + } + vector, err := readWorkVector(r) + if err != nil { + return compute.VerifiedWork{}, err + } + paid, err := r.U64() + if err != nil { + return compute.VerifiedWork{}, ErrFinalizedEconomics + } + verification, err := r.U8() + if err != nil { + return compute.VerifiedWork{}, ErrFinalizedEconomics + } + rootRaw, err := r.Fixed(32) + if err != nil { + return compute.VerifiedWork{}, ErrFinalizedEconomics + } + var jobID types.JobID + var resultRoot types.Hash + copy(jobID[:], jobRaw) + copy(resultRoot[:], rootRaw) + out := compute.VerifiedWork{ + JobID: jobID, Class: compute.WorkClass(class), Units: units, Vector: vector, + PaidZPH: paid, Verification: compute.VerificationMode(verification), ResultRoot: resultRoot, + } + var sink codec.Writer + if err := writeVerifiedWork(&sink, out); err != nil { + return compute.VerifiedWork{}, err + } + return out, nil +} + +func writeWorkVector(w *codec.Writer, vector compute.WorkVector) { + w.U64(vector.CPUUnits) + w.U64(vector.GPUFP32Units) + w.U64(vector.GPUFP64Units) + w.U64(vector.TensorUnits) + w.U64(vector.MemoryByteSeconds) + w.U64(vector.VRAMByteSeconds) + w.U64(vector.StorageBytes) + w.U64(vector.NetworkBytes) +} + +func readWorkVector(r *codec.Reader) (compute.WorkVector, error) { + values := make([]uint64, 8) + var err error + for i := range values { + values[i], err = r.U64() + if err != nil { + return compute.WorkVector{}, ErrFinalizedEconomics + } + } + return compute.WorkVector{ + CPUUnits: values[0], GPUFP32Units: values[1], GPUFP64Units: values[2], TensorUnits: values[3], + MemoryByteSeconds: values[4], VRAMByteSeconds: values[5], StorageBytes: values[6], NetworkBytes: values[7], + }, nil +} + +func copyShardMap(source map[uint32]uint64) map[uint32]uint64 { + out := make(map[uint32]uint64, len(source)) + for shard, value := range source { + out[shard] = value + } + return out +} + +var _ = big.NewInt From 0df81bd82ae79c1622bf8f6fe920203f24402527 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:53:36 +0200 Subject: [PATCH 244/274] Deep-copy finalized economics runtime configuration --- .../v2/economics/finalized_collector_clone.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/internal/v2/economics/finalized_collector_clone.go b/internal/v2/economics/finalized_collector_clone.go index bb9d07ed..c9b679cd 100644 --- a/internal/v2/economics/finalized_collector_clone.go +++ b/internal/v2/economics/finalized_collector_clone.go @@ -2,9 +2,22 @@ package economics // Clone returns an independent deep copy of the finalized economics collector. // Runtime owners use this to prevent external callers from mutating economic -// telemetry outside the node's synchronization boundary. +// telemetry or its protocol configuration outside the node synchronization +// boundary. func (c *EpochCollector) Clone() *EpochCollector { - return c.clone() + out := c.clone() + if out == nil { + return nil + } + out.config.InitialCirculatingSupply = copyShardMap(c.config.InitialCirculatingSupply) + out.config.OpeningComputeBacklog = copyShardMap(c.config.OpeningComputeBacklog) + out.config.ResourceCapacityPerBlock = copyShardMap(c.config.ResourceCapacityPerBlock) + registry, err := cloneCollectorRegistry(c.config.WorkRegistry) + if err != nil { + return nil + } + out.config.WorkRegistry = registry + return out } func (c *EpochCollector) Epoch() uint64 { From 7c32ae1bc9358caf5affd8934fe353ac7daeb822 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:53:59 +0200 Subject: [PATCH 245/274] Test finalized economics collector checkpoint recovery --- .../finalized_collector_checkpoint_test.go | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 internal/v2/economics/finalized_collector_checkpoint_test.go diff --git a/internal/v2/economics/finalized_collector_checkpoint_test.go b/internal/v2/economics/finalized_collector_checkpoint_test.go new file mode 100644 index 00000000..c7eb07a1 --- /dev/null +++ b/internal/v2/economics/finalized_collector_checkpoint_test.go @@ -0,0 +1,152 @@ +package economics + +import ( + "bytes" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestEpochCollectorCheckpointRoundTrip(t *testing.T) { + registry, err := compute.NewWorkRegistry([]compute.WorkSpec{{ + Version: compute.WorkSpecVersion, Class: compute.WorkCPUGeneral, Units: 100, + WorkloadHash: types.Hash{1}, BenchmarkHash: types.Hash{2}, + Vector: compute.WorkVector{CPUUnits: 100}, + }}) + if err != nil { + t.Fatal(err) + } + collector, err := NewEpochCollector(EpochCollectorConfig{ + Epoch: 3, ShardCount: 2, NativeToken: types.TokenID{3}, + InitialCirculatingSupply: map[uint32]uint64{0: 900, 1: 100}, + OpeningComputeBacklog: map[uint32]uint64{0: 50}, + ResourceCapacityPerBlock: map[uint32]uint64{0: 1_000, 1: 2_000}, + VelocityPolicy: VelocityPolicy{ + MinAgeBlocks: 2, FullWeightAgeBlocks: 20, MaxVelocityBps: 10_000, + }, + FeePolicy: CompatibilityFeePolicy(), + WorkRegistry: registry, + }) + if err != nil { + t.Fatal(err) + } + collector.lastHeight = 22 + first := collector.shards[0] + first.fees = FeeAllocation{Total: 7, Burn: 7} + first.operations = 9 + first.resourceUsed = 50 + first.resourceCapacity = 2_000 + first.newDemand = 100 + first.fulfilled = 80 + first.expired = 10 + first.closingBacklog = 60 + first.computeSupply = 100 + first.computeSupplyReliable = true + first.velocity.weightedValueBps.SetUint64(123_456) + first.velocity.observedSpends = 4 + first.velocity.eligibleSpends = 2 + first.velocity.unknownAgeSpends = 1 + first.velocity.freshSpends = 1 + second := collector.shards[1] + second.resourceCapacity = 4_000 + collector.verifiedWork = []compute.VerifiedWork{{ + JobID: types.JobID{4}, Class: compute.WorkCPUGeneral, Units: 100, + Vector: compute.WorkVector{CPUUnits: 100}, PaidZPH: 25, + Verification: compute.VerificationReplicated, ResultRoot: types.Hash{5}, + }} + + raw, err := collector.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + restored, err := RestoreEpochCollector(raw, registry) + if err != nil { + t.Fatal(err) + } + rawAgain, err := restored.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(raw, rawAgain) { + t.Fatal("collector checkpoint is not canonical across restore") + } + if restored.Epoch() != 3 || restored.lastHeight != 22 || restored.supply[0] != 900 || restored.supply[1] != 100 { + t.Fatalf("restored collector lost identity/state: %#v", restored) + } + if restored.shards[0].velocity.weightedValueBps.Uint64() != 123_456 || len(restored.verifiedWork) != 1 { + t.Fatalf("restored collector lost accumulated telemetry: %#v", restored.shards[0]) + } +} + +func TestEpochCollectorCheckpointRejectsDifferentWorkRegistry(t *testing.T) { + original, err := compute.NewWorkRegistry([]compute.WorkSpec{{ + Version: compute.WorkSpecVersion, Class: compute.WorkCPUGeneral, Units: 100, + WorkloadHash: types.Hash{1}, BenchmarkHash: types.Hash{2}, Vector: compute.WorkVector{CPUUnits: 100}, + }}) + if err != nil { + t.Fatal(err) + } + collector, err := NewEpochCollector(EpochCollectorConfig{ + Epoch: 1, ShardCount: 1, NativeToken: types.TokenID{3}, + InitialCirculatingSupply: map[uint32]uint64{0: 1_000}, + ResourceCapacityPerBlock: map[uint32]uint64{0: 100}, + VelocityPolicy: VelocityPolicy{MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000}, + FeePolicy: CompatibilityFeePolicy(), WorkRegistry: original, + }) + if err != nil { + t.Fatal(err) + } + raw, err := collector.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + changed, err := compute.NewWorkRegistry([]compute.WorkSpec{{ + Version: compute.WorkSpecVersion, Class: compute.WorkCPUGeneral, Units: 101, + WorkloadHash: types.Hash{1}, BenchmarkHash: types.Hash{2}, Vector: compute.WorkVector{CPUUnits: 101}, + }}) + if err != nil { + t.Fatal(err) + } + if _, err := RestoreEpochCollector(raw, changed); err == nil { + t.Fatal("checkpoint restored with a different workload registry") + } +} + +func TestRuntimeCollectorCloneOwnsRegistryAndCapacityConfig(t *testing.T) { + registry, err := compute.NewWorkRegistry([]compute.WorkSpec{{ + Version: compute.WorkSpecVersion, Class: compute.WorkCPUGeneral, Units: 10, + WorkloadHash: types.Hash{1}, BenchmarkHash: types.Hash{2}, Vector: compute.WorkVector{CPUUnits: 10}, + }}) + if err != nil { + t.Fatal(err) + } + capacity := map[uint32]uint64{0: 100} + collector, err := NewEpochCollector(EpochCollectorConfig{ + Epoch: 1, ShardCount: 1, NativeToken: types.TokenID{3}, + InitialCirculatingSupply: map[uint32]uint64{0: 1_000}, + ResourceCapacityPerBlock: capacity, + VelocityPolicy: VelocityPolicy{MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000}, + FeePolicy: CompatibilityFeePolicy(), WorkRegistry: registry, + }) + if err != nil { + t.Fatal(err) + } + owned := collector.Clone() + if owned == nil { + t.Fatal("collector clone failed") + } + capacity[0] = 999 + if err := registry.Register(compute.WorkSpec{ + Version: compute.WorkSpecVersion, Class: compute.WorkRendering, Units: 20, + WorkloadHash: types.Hash{9}, BenchmarkHash: types.Hash{10}, Vector: compute.WorkVector{GPUFP32Units: 20}, + }); err != nil { + t.Fatal(err) + } + if owned.config.ResourceCapacityPerBlock[0] != 100 { + t.Fatal("external capacity map mutation leaked into runtime-owned collector") + } + if _, ok := owned.config.WorkRegistry.Resolve(types.Hash{9}); ok { + t.Fatal("external registry mutation leaked into runtime-owned collector") + } +} From b59341b7d3430c735845e99d9645628cb8958092 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:55:49 +0200 Subject: [PATCH 246/274] Temporarily format economics checkpoint files --- .github/workflows/v2-format-write.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/v2-format-write.yml b/.github/workflows/v2-format-write.yml index 26cc851a..3e8f53b3 100644 --- a/.github/workflows/v2-format-write.yml +++ b/.github/workflows/v2-format-write.yml @@ -9,6 +9,8 @@ on: - internal/v2/provider/service.go - internal/v2/provider/service_test.go - internal/v2/tx/transaction.go + - internal/v2/economics/finalized_collector_checkpoint.go + - internal/v2/economics/finalized_collector_checkpoint_test.go - .github/workflows/v2-format-write.yml permissions: @@ -30,7 +32,7 @@ jobs: go-version-file: go.mod cache: false - name: Format exact files - run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go + run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go internal/v2/economics/finalized_collector_checkpoint.go internal/v2/economics/finalized_collector_checkpoint_test.go - name: Commit formatting if needed run: | if git diff --quiet; then @@ -39,6 +41,6 @@ jobs: fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go - git commit -m 'gofmt Zephyr v2 compute and provider files' + git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go internal/v2/economics/finalized_collector_checkpoint.go internal/v2/economics/finalized_collector_checkpoint_test.go + git commit -m 'gofmt Zephyr v2 economics checkpoint files' git push origin HEAD:chatgpt/protocol-v2-foundation From a9dc7917ed3abf5bf18d508f640d79fb89093bd0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:56:09 +0000 Subject: [PATCH 247/274] gofmt Zephyr v2 economics checkpoint files --- internal/v2/economics/finalized_collector_checkpoint.go | 6 +++--- .../v2/economics/finalized_collector_checkpoint_test.go | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/v2/economics/finalized_collector_checkpoint.go b/internal/v2/economics/finalized_collector_checkpoint.go index 1fd7b0e5..d28ab30e 100644 --- a/internal/v2/economics/finalized_collector_checkpoint.go +++ b/internal/v2/economics/finalized_collector_checkpoint.go @@ -9,9 +9,9 @@ import ( ) const ( - collectorCheckpointVersion uint16 = 1 - maxCheckpointVerifiedWork = 1_000_000 - maxVelocityAccumulatorBytes = 128 + collectorCheckpointVersion uint16 = 1 + maxCheckpointVerifiedWork = 1_000_000 + maxVelocityAccumulatorBytes = 128 ) // CheckpointBytes serializes the exact mid-epoch collector state. The workload diff --git a/internal/v2/economics/finalized_collector_checkpoint_test.go b/internal/v2/economics/finalized_collector_checkpoint_test.go index c7eb07a1..64371b55 100644 --- a/internal/v2/economics/finalized_collector_checkpoint_test.go +++ b/internal/v2/economics/finalized_collector_checkpoint_test.go @@ -91,8 +91,8 @@ func TestEpochCollectorCheckpointRejectsDifferentWorkRegistry(t *testing.T) { Epoch: 1, ShardCount: 1, NativeToken: types.TokenID{3}, InitialCirculatingSupply: map[uint32]uint64{0: 1_000}, ResourceCapacityPerBlock: map[uint32]uint64{0: 100}, - VelocityPolicy: VelocityPolicy{MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000}, - FeePolicy: CompatibilityFeePolicy(), WorkRegistry: original, + VelocityPolicy: VelocityPolicy{MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000}, + FeePolicy: CompatibilityFeePolicy(), WorkRegistry: original, }) if err != nil { t.Fatal(err) @@ -126,8 +126,8 @@ func TestRuntimeCollectorCloneOwnsRegistryAndCapacityConfig(t *testing.T) { Epoch: 1, ShardCount: 1, NativeToken: types.TokenID{3}, InitialCirculatingSupply: map[uint32]uint64{0: 1_000}, ResourceCapacityPerBlock: capacity, - VelocityPolicy: VelocityPolicy{MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000}, - FeePolicy: CompatibilityFeePolicy(), WorkRegistry: registry, + VelocityPolicy: VelocityPolicy{MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000}, + FeePolicy: CompatibilityFeePolicy(), WorkRegistry: registry, }) if err != nil { t.Fatal(err) From 0d4587b2d5838771837b707e5121f1810a920aef Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:56:37 +0200 Subject: [PATCH 248/274] Restore scoped v2 format workflow --- .github/workflows/v2-format-write.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/v2-format-write.yml b/.github/workflows/v2-format-write.yml index 3e8f53b3..26cc851a 100644 --- a/.github/workflows/v2-format-write.yml +++ b/.github/workflows/v2-format-write.yml @@ -9,8 +9,6 @@ on: - internal/v2/provider/service.go - internal/v2/provider/service_test.go - internal/v2/tx/transaction.go - - internal/v2/economics/finalized_collector_checkpoint.go - - internal/v2/economics/finalized_collector_checkpoint_test.go - .github/workflows/v2-format-write.yml permissions: @@ -32,7 +30,7 @@ jobs: go-version-file: go.mod cache: false - name: Format exact files - run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go internal/v2/economics/finalized_collector_checkpoint.go internal/v2/economics/finalized_collector_checkpoint_test.go + run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go - name: Commit formatting if needed run: | if git diff --quiet; then @@ -41,6 +39,6 @@ jobs: fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go internal/v2/economics/finalized_collector_checkpoint.go internal/v2/economics/finalized_collector_checkpoint_test.go - git commit -m 'gofmt Zephyr v2 economics checkpoint files' + git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go + git commit -m 'gofmt Zephyr v2 compute and provider files' git push origin HEAD:chatgpt/protocol-v2-foundation From 46aa50255e4c7a04ec7ef0b64b8e30ea622d0844 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:57:42 +0200 Subject: [PATCH 249/274] Add policy-bound shadow epoch engine checkpoint --- .../shadow_epoch_engine_checkpoint.go | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 internal/v2/economics/shadow_epoch_engine_checkpoint.go diff --git a/internal/v2/economics/shadow_epoch_engine_checkpoint.go b/internal/v2/economics/shadow_epoch_engine_checkpoint.go new file mode 100644 index 00000000..5fb26b34 --- /dev/null +++ b/internal/v2/economics/shadow_epoch_engine_checkpoint.go @@ -0,0 +1,191 @@ +package economics + +import ( + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const shadowEpochEngineCheckpointVersion uint16 = 1 + +func ShadowEpochEngineConfigHash(config ShadowEpochEngineConfig) types.Hash { + var w codec.Writer + for class := compute.WorkClass(0); class < compute.WorkClassCount; class++ { + w.U32(config.ComputeIndex.WeightsBps[class]) + } + w.U32(config.ComputeIndex.MinSamplesPerClass) + w.U32(config.ComputeIndex.MinCoverageBps) + w.U32(config.ComputeIndex.EWMABps) + + w.U32(config.ComputeScarcity.DemandSupplyWeightBps) + w.U32(config.ComputeScarcity.PriceTrendWeightBps) + w.U32(config.ComputeScarcity.BacklogWeightBps) + w.U32(config.ComputeScarcity.UtilizationWeightBps) + w.U32(config.ComputeScarcity.FulfillmentWeightBps) + w.U32(config.ComputeScarcity.UtilizationTargetBps) + w.U64(config.ComputeScarcity.MinDemandUnits) + w.U64(config.ComputeScarcity.MinSupplyUnits) + w.U32(config.ComputeScarcity.MaxAbsScoreBps) + + w.U32(config.Monetary.TargetInflationBps) + w.U32(config.Monetary.MinInflationBps) + w.U32(config.Monetary.MaxInflationBps) + w.U32(config.Monetary.MaxEpochStepBps) + w.U64(config.Monetary.EpochsPerYear) + w.U32(config.Monetary.ReserveTargetBps) + w.U32(config.Monetary.StakeTargetBps) + w.U32(config.Monetary.UtilizationTargetBps) + w.U32(config.Monetary.VelocityTargetBps) + w.U64(config.Monetary.OperationsTarget) + w.U32(config.Monetary.ReserveWeightBps) + w.U32(config.Monetary.StakeWeightBps) + w.U32(config.Monetary.UtilizationWeightBps) + w.U32(config.Monetary.VelocityWeightBps) + w.U32(config.Monetary.OperationsWeightBps) + + w.U8(uint8(config.ComputeFeedback.Mode)) + w.U32(config.ComputeFeedback.BaseComputeRewardShareBps) + w.U32(config.ComputeFeedback.MinComputeRewardShareBps) + w.U32(config.ComputeFeedback.MaxComputeRewardShareBps) + w.U32(config.ComputeFeedback.RewardSensitivityBps) + w.U32(config.ComputeFeedback.MonetarySensitivityBps) + w.U32(config.ComputeFeedback.MaxInflationCorrectionBps) + return types.Hash(codec.DomainHash("zephyr/shadow-economics-config/v2", w.BytesCopy())) +} + +func (e *ShadowEpochEngine) CheckpointBytes() ([]byte, error) { + if e == nil || types.IsZero32([32]byte(e.Network)) { + return nil, ErrShadowEpochEngine + } + var w codec.Writer + w.U16(shadowEpochEngineCheckpointVersion) + w.Fixed(e.Network[:]) + configHash := ShadowEpochEngineConfigHash(e.config) + w.Fixed(configHash[:]) + writeComputeIndexSnapshot(&w, e.priorIndex) + w.Bool(e.previous != nil) + if e.previous != nil { + raw, err := e.previous.CanonicalBytes() + if err != nil { + return nil, err + } + w.Bytes(raw) + } + return w.BytesCopy(), nil +} + +func RestoreShadowEpochEngine(data []byte, expectedNetwork types.NetworkID, config ShadowEpochEngineConfig) (*ShadowEpochEngine, error) { + if types.IsZero32([32]byte(expectedNetwork)) { + return nil, ErrShadowEpochEngine + } + r := codec.NewReader(data) + version, err := r.U16() + if err != nil || version != shadowEpochEngineCheckpointVersion { + return nil, ErrShadowEpochEngine + } + networkRaw, err := r.Fixed(32) + if err != nil { + return nil, ErrShadowEpochEngine + } + var network types.NetworkID + copy(network[:], networkRaw) + if network != expectedNetwork { + return nil, ErrShadowEpochEngine + } + configRaw, err := r.Fixed(32) + if err != nil { + return nil, ErrShadowEpochEngine + } + var checkpointConfigHash types.Hash + copy(checkpointConfigHash[:], configRaw) + if checkpointConfigHash != ShadowEpochEngineConfigHash(config) { + return nil, ErrShadowEpochEngine + } + priorIndex, err := readComputeIndexSnapshot(r) + if err != nil { + return nil, err + } + hasPrevious, err := r.Bool() + if err != nil { + return nil, ErrShadowEpochEngine + } + var previous *MonetaryEpochState + if hasPrevious { + raw, err := r.Bytes(64 << 10) + if err != nil { + return nil, ErrShadowEpochEngine + } + parsed, err := ParseMonetaryEpochState(raw) + if err != nil || parsed.Network != expectedNetwork || parsed.Epoch != priorIndex.Epoch { + return nil, ErrShadowEpochEngine + } + previous = &parsed + } else if priorIndex != (ComputeIndexSnapshot{}) { + return nil, ErrShadowEpochEngine + } + if r.Done() != nil { + return nil, ErrShadowEpochEngine + } + engine, err := NewShadowEpochEngine(expectedNetwork, config) + if err != nil { + return nil, err + } + engine.priorIndex = priorIndex + engine.previous = previous + return engine, nil +} + +func writeComputeIndexSnapshot(w *codec.Writer, snapshot ComputeIndexSnapshot) { + w.U64(snapshot.Epoch) + for class := compute.WorkClass(0); class < compute.WorkClassCount; class++ { + w.U64(snapshot.ClassPriceQ9[class]) + } + for class := compute.WorkClass(0); class < compute.WorkClassCount; class++ { + w.U64(snapshot.ClassSamples[class]) + } + w.U64(snapshot.BasketPriceQ9) + w.U32(snapshot.CoverageBps) + w.Bool(snapshot.Reliable) + w.U64(snapshot.TotalSamples) +} + +func readComputeIndexSnapshot(r *codec.Reader) (ComputeIndexSnapshot, error) { + out := ComputeIndexSnapshot{} + var err error + out.Epoch, err = r.U64() + if err != nil { + return ComputeIndexSnapshot{}, ErrShadowEpochEngine + } + for class := compute.WorkClass(0); class < compute.WorkClassCount; class++ { + out.ClassPriceQ9[class], err = r.U64() + if err != nil { + return ComputeIndexSnapshot{}, ErrShadowEpochEngine + } + } + for class := compute.WorkClass(0); class < compute.WorkClassCount; class++ { + out.ClassSamples[class], err = r.U64() + if err != nil { + return ComputeIndexSnapshot{}, ErrShadowEpochEngine + } + } + out.BasketPriceQ9, err = r.U64() + if err != nil { + return ComputeIndexSnapshot{}, ErrShadowEpochEngine + } + out.CoverageBps, err = r.U32() + if err != nil || out.CoverageBps > BasisPoints { + return ComputeIndexSnapshot{}, ErrShadowEpochEngine + } + out.Reliable, err = r.Bool() + if err != nil { + return ComputeIndexSnapshot{}, ErrShadowEpochEngine + } + out.TotalSamples, err = r.U64() + if err != nil { + return ComputeIndexSnapshot{}, ErrShadowEpochEngine + } + if out.Epoch == 0 && out != (ComputeIndexSnapshot{}) { + return ComputeIndexSnapshot{}, ErrShadowEpochEngine + } + return out, nil +} From e6952d9936da6af0609f35b6872f772ad09942f1 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:57:56 +0200 Subject: [PATCH 250/274] Test shadow epoch engine checkpoint recovery --- .../shadow_epoch_engine_checkpoint_test.go | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 internal/v2/economics/shadow_epoch_engine_checkpoint_test.go diff --git a/internal/v2/economics/shadow_epoch_engine_checkpoint_test.go b/internal/v2/economics/shadow_epoch_engine_checkpoint_test.go new file mode 100644 index 00000000..dc81ce89 --- /dev/null +++ b/internal/v2/economics/shadow_epoch_engine_checkpoint_test.go @@ -0,0 +1,94 @@ +package economics + +import ( + "bytes" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestShadowEpochEngineCheckpointRoundTrip(t *testing.T) { + config := testShadowEpochConfig() + network := types.NetworkID{1} + engine, err := NewShadowEpochEngine(network, config) + if err != nil { + t.Fatal(err) + } + preview, err := engine.PreviewCloseEpoch( + []ShardEpochMetrics{testEpochMetric(1, 0, 1_000, 500, 500)}, + []compute.VerifiedWork{testVerifiedCPUWork(1, 200)}, + MonetaryBalanceSnapshot{TotalSupply: 1_000, StakedSupply: 400, ProtocolReserve: 100, BaseFee: 1}, + ) + if err != nil { + t.Fatal(err) + } + if err := engine.Accept(preview); err != nil { + t.Fatal(err) + } + raw, err := engine.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + restored, err := RestoreShadowEpochEngine(raw, network, config) + if err != nil { + t.Fatal(err) + } + rawAgain, err := restored.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(raw, rawAgain) { + t.Fatal("shadow epoch checkpoint is not canonical across restore") + } + state, ok := restored.PreviousState() + if !ok || state != preview.State { + t.Fatalf("restored engine lost prior monetary state: %#v", state) + } + if restored.PriorComputeIndex() != preview.ComputeIndex { + t.Fatal("restored engine lost prior compute index") + } +} + +func TestShadowEpochEngineCheckpointRejectsPolicyChange(t *testing.T) { + config := testShadowEpochConfig() + network := types.NetworkID{1} + engine, err := NewShadowEpochEngine(network, config) + if err != nil { + t.Fatal(err) + } + raw, err := engine.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + changed := config + changed.Monetary.TargetInflationBps++ + if _, err := RestoreShadowEpochEngine(raw, network, changed); err == nil { + t.Fatal("checkpoint restored under a different monetary policy") + } + changed = config + changed.ComputeScarcity.MinSupplyUnits++ + if _, err := RestoreShadowEpochEngine(raw, network, changed); err == nil { + t.Fatal("checkpoint restored under a different ZCSI policy") + } + changed = config + changed.ComputeFeedback.Mode = ComputeFeedbackMonetaryBand + if _, err := RestoreShadowEpochEngine(raw, network, changed); err == nil { + t.Fatal("checkpoint restored under a different compute-feedback mode") + } +} + +func TestShadowEpochEngineCheckpointRejectsWrongNetwork(t *testing.T) { + config := testShadowEpochConfig() + engine, err := NewShadowEpochEngine(types.NetworkID{1}, config) + if err != nil { + t.Fatal(err) + } + raw, err := engine.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + if _, err := RestoreShadowEpochEngine(raw, types.NetworkID{2}, config); err == nil { + t.Fatal("checkpoint restored on a different network") + } +} From 6ca5dce77e0d7df9ee414f5343aa229c65f7f2cd Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:58:50 +0200 Subject: [PATCH 251/274] Decode canonical economic epoch aggregates --- internal/v2/economics/epoch_parse.go | 103 +++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 internal/v2/economics/epoch_parse.go diff --git a/internal/v2/economics/epoch_parse.go b/internal/v2/economics/epoch_parse.go new file mode 100644 index 00000000..10ff0249 --- /dev/null +++ b/internal/v2/economics/epoch_parse.go @@ -0,0 +1,103 @@ +package economics + +import "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + +func ParseEpochAggregate(data []byte) (EpochAggregate, error) { + r := codec.NewReader(data) + out := EpochAggregate{} + var err error + out.Epoch, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ShardCount, err = r.U32() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ChargedFees, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.BurnedFees, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ValidatorFees, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ReserveFees, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.FinalizedOperations, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ResourceUsed, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ResourceCapacity, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ResourceUtilizationBps, err = r.U32() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.CirculatingNativeSupply, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.AgeWeightedVelocityBps, err = r.U32() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.EscrowBackedComputeDemand, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.VerifiedComputeSupply, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ComputeSupplyReliable, err = r.Bool() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.OpeningComputeBacklog, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ComputeFulfilled, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ComputeExpired, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ComputeBacklog, err = r.U64() + if err != nil { + return EpochAggregate{}, ErrEpochMetrics + } + out.ComputeUtilizationBps, err = r.U32() + if err != nil || r.Done() != nil { + return EpochAggregate{}, ErrEpochMetrics + } + if _, err := out.CanonicalBytes(); err != nil { + return EpochAggregate{}, err + } + if out.ResourceUtilizationBps != ratioBps(out.ResourceUsed, out.ResourceCapacity) { + return EpochAggregate{}, ErrEpochMetrics + } + expectedComputeUtilization := uint32(0) + if out.VerifiedComputeSupply != 0 { + expectedComputeUtilization = ratioBps(out.ComputeFulfilled, out.VerifiedComputeSupply) + } + if out.ComputeUtilizationBps != expectedComputeUtilization { + return EpochAggregate{}, ErrEpochMetrics + } + return out, nil +} From 7fd298ee0f97d1fbc5274c9824be4f81c94a5aa3 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:59:08 +0200 Subject: [PATCH 252/274] Add recoverable pending shadow epoch checkpoint --- .../shadow_epoch_pending_checkpoint.go | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 internal/v2/economics/shadow_epoch_pending_checkpoint.go diff --git a/internal/v2/economics/shadow_epoch_pending_checkpoint.go b/internal/v2/economics/shadow_epoch_pending_checkpoint.go new file mode 100644 index 00000000..439743ed --- /dev/null +++ b/internal/v2/economics/shadow_epoch_pending_checkpoint.go @@ -0,0 +1,90 @@ +package economics + +import ( + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const shadowEpochPendingCheckpointVersion uint16 = 1 + +func (p ShadowEpochPreview) PendingCheckpointBytes() ([]byte, error) { + aggregateRaw, err := p.Aggregate.CanonicalBytes() + if err != nil { + return nil, err + } + stateRaw, err := p.State.CanonicalBytes() + if err != nil { + return nil, err + } + if p.ComputeIndex.Epoch != p.State.Epoch || p.Aggregate.Epoch != p.State.Epoch { + return nil, ErrShadowEpochEngine + } + var w codec.Writer + w.U16(shadowEpochPendingCheckpointVersion) + w.Bytes(aggregateRaw) + writeComputeIndexSnapshot(&w, p.ComputeIndex) + w.Bytes(stateRaw) + return w.BytesCopy(), nil +} + +// RestoreShadowEpochPreview rebuilds the pending Merkle transition from the +// accepted engine history rather than trusting serialized consumed/created +// object lists. A cloned engine must be able to Accept the reconstructed +// preview before it is returned. +func RestoreShadowEpochPreview(data []byte, engine *ShadowEpochEngine) (ShadowEpochPreview, error) { + if engine == nil { + return ShadowEpochPreview{}, ErrShadowEpochEngine + } + r := codec.NewReader(data) + version, err := r.U16() + if err != nil || version != shadowEpochPendingCheckpointVersion { + return ShadowEpochPreview{}, ErrShadowEpochEngine + } + aggregateRaw, err := r.Bytes(64 << 10) + if err != nil { + return ShadowEpochPreview{}, ErrShadowEpochEngine + } + aggregate, err := ParseEpochAggregate(aggregateRaw) + if err != nil { + return ShadowEpochPreview{}, err + } + index, err := readComputeIndexSnapshot(r) + if err != nil { + return ShadowEpochPreview{}, err + } + stateRaw, err := r.Bytes(64 << 10) + if err != nil || r.Done() != nil { + return ShadowEpochPreview{}, ErrShadowEpochEngine + } + state, err := ParseMonetaryEpochState(stateRaw) + if err != nil || state.Network != engine.Network || state.Epoch != aggregate.Epoch || index.Epoch != state.Epoch { + return ShadowEpochPreview{}, ErrShadowEpochEngine + } + + var previousObject *object.Object + if previous, ok := engine.PreviousState(); ok { + obj, err := previous.Object() + if err != nil { + return ShadowEpochPreview{}, err + } + previousObject = &obj + } + consumed, created, err := ShadowMonetaryTransition(previousObject, state) + if err != nil { + return ShadowEpochPreview{}, err + } + preview := ShadowEpochPreview{ + Aggregate: aggregate, + ComputeIndex: index, + ComputeScarcity: ComputeScarcitySnapshot{Epoch: state.Epoch}, + State: state, + Consumed: append([]types.ObjectID(nil), consumed...), + Created: append([]object.Object(nil), created...), + } + validator := engine.Clone() + if validator == nil || validator.Accept(preview) != nil { + return ShadowEpochPreview{}, ErrShadowEpochEngine + } + return preview, nil +} From 1d67b6cb111f68d3bee8747bcda76aa7cb3e40b0 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:59:40 +0200 Subject: [PATCH 253/274] Test pending shadow epoch recovery --- .../shadow_epoch_pending_checkpoint_test.go | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 internal/v2/economics/shadow_epoch_pending_checkpoint_test.go diff --git a/internal/v2/economics/shadow_epoch_pending_checkpoint_test.go b/internal/v2/economics/shadow_epoch_pending_checkpoint_test.go new file mode 100644 index 00000000..ade1f5e7 --- /dev/null +++ b/internal/v2/economics/shadow_epoch_pending_checkpoint_test.go @@ -0,0 +1,85 @@ +package economics + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestPendingShadowEpochCheckpointRecovery(t *testing.T) { + config := testShadowEpochConfig() + network := types.NetworkID{1} + engine, err := NewShadowEpochEngine(network, config) + if err != nil { + t.Fatal(err) + } + preview, err := engine.PreviewCloseEpoch( + []ShardEpochMetrics{testEpochMetric(1, 0, 1_000, 500, 500)}, + []compute.VerifiedWork{testVerifiedCPUWork(1, 200)}, + MonetaryBalanceSnapshot{TotalSupply: 1_000, StakedSupply: 400, ProtocolReserve: 100, BaseFee: 1}, + ) + if err != nil { + t.Fatal(err) + } + raw, err := preview.PendingCheckpointBytes() + if err != nil { + t.Fatal(err) + } + restored, err := RestoreShadowEpochPreview(raw, engine) + if err != nil { + t.Fatal(err) + } + if restored.State != preview.State || restored.Aggregate != preview.Aggregate || restored.ComputeIndex != preview.ComputeIndex { + t.Fatal("pending checkpoint changed committed economics") + } + if len(restored.Consumed) != 0 || len(restored.Created) != 1 || restored.Created[0].ID != MonetaryStateObjectID(network) { + t.Fatal("pending checkpoint did not rebuild the first monetary object delta") + } +} + +func TestPendingShadowEpochCheckpointRequiresAcceptedHistory(t *testing.T) { + config := testShadowEpochConfig() + network := types.NetworkID{1} + engine, err := NewShadowEpochEngine(network, config) + if err != nil { + t.Fatal(err) + } + first, err := engine.PreviewCloseEpoch( + []ShardEpochMetrics{testEpochMetric(1, 0, 1_000, 500, 500)}, + []compute.VerifiedWork{testVerifiedCPUWork(1, 200)}, + MonetaryBalanceSnapshot{TotalSupply: 1_000, StakedSupply: 400, ProtocolReserve: 100, BaseFee: 1}, + ) + if err != nil { + t.Fatal(err) + } + if err := engine.Accept(first); err != nil { + t.Fatal(err) + } + second, err := engine.PreviewCloseEpoch( + []ShardEpochMetrics{testEpochMetric(2, 500, 0, 500, 0)}, + []compute.VerifiedWork{testVerifiedCPUWork(3, 300)}, + MonetaryBalanceSnapshot{TotalSupply: 1_000, StakedSupply: 400, ProtocolReserve: 100, BaseFee: 1}, + ) + if err != nil { + t.Fatal(err) + } + raw, err := second.PendingCheckpointBytes() + if err != nil { + t.Fatal(err) + } + fresh, err := NewShadowEpochEngine(network, config) + if err != nil { + t.Fatal(err) + } + if _, err := RestoreShadowEpochPreview(raw, fresh); err == nil { + t.Fatal("pending epoch restored without the accepted predecessor") + } + restored, err := RestoreShadowEpochPreview(raw, engine) + if err != nil { + t.Fatal(err) + } + if len(restored.Consumed) != 1 || len(restored.Created) != 1 { + t.Fatal("pending replacement delta was not rebuilt") + } +} From b904a75c6ced8a2af6d445a7fc670be3d7f7eacc Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:01:06 +0200 Subject: [PATCH 254/274] Expose finalized economics recovery anchor --- internal/v2/economics/finalized_collector_clone.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/v2/economics/finalized_collector_clone.go b/internal/v2/economics/finalized_collector_clone.go index c9b679cd..43702ea5 100644 --- a/internal/v2/economics/finalized_collector_clone.go +++ b/internal/v2/economics/finalized_collector_clone.go @@ -26,3 +26,10 @@ func (c *EpochCollector) Epoch() uint64 { } return c.config.Epoch } + +func (c *EpochCollector) LastHeight() uint64 { + if c == nil { + return 0 + } + return c.lastHeight +} From b7eb3ccf51b199d7348895dd873fc317191ffe86 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:01:50 +0200 Subject: [PATCH 255/274] Add atomic network-bound economic runtime checkpoint --- internal/v2/node/economic_checkpoint.go | 341 ++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 internal/v2/node/economic_checkpoint.go diff --git a/internal/v2/node/economic_checkpoint.go b/internal/v2/node/economic_checkpoint.go new file mode 100644 index 00000000..2d81671d --- /dev/null +++ b/internal/v2/node/economic_checkpoint.go @@ -0,0 +1,341 @@ +package node + +import ( + "bytes" + "encoding/binary" + "errors" + "os" + "path/filepath" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + economicCheckpointVersion uint16 = 1 + economicCheckpointMagic = "ZEC2" + maxEconomicCheckpointBytes = 64 << 20 +) + +var ErrEconomicCheckpoint = errors.New("invalid Zephyr v2 economic runtime checkpoint") + +func (r *Runtime) EconomicCheckpointBytes() ([]byte, error) { + if r == nil { + return nil, ErrEconomicCheckpoint + } + r.mu.Lock() + defer r.mu.Unlock() + return r.economicCheckpointBytesLocked() +} + +func (r *Runtime) economicCheckpointBytesLocked() ([]byte, error) { + if r.economicCollector == nil || r.economicCollector.LastHeight() != r.Height { + return nil, ErrEconomicCheckpoint + } + collectorRaw, err := r.economicCollector.CheckpointBytes() + if err != nil { + return nil, err + } + var payload codec.Writer + payload.U16(economicCheckpointVersion) + payload.Fixed(r.Network[:]) + payload.Fixed(r.NativeToken[:]) + payload.Fixed(r.ValidatorRoot[:]) + payload.U32(r.ShardCount) + payload.U64(r.Height) + payload.Fixed(r.ParentHash[:]) + payload.U64(r.economicEpochLength) + payload.U64(r.economicBalances.TotalSupply) + payload.U64(r.economicBalances.StakedSupply) + payload.U64(r.economicBalances.ProtocolReserve) + payload.U64(r.economicBalances.BaseFee) + payload.Bytes(collectorRaw) + payload.Bool(r.economicEngine != nil) + if r.economicEngine != nil { + engineRaw, err := r.economicEngine.CheckpointBytes() + if err != nil { + return nil, err + } + payload.Bytes(engineRaw) + } + payload.Bool(r.pendingEconomic != nil) + if r.pendingEconomic != nil { + if r.economicEngine == nil { + return nil, ErrEconomicCheckpoint + } + pendingRaw, err := r.pendingEconomic.PendingCheckpointBytes() + if err != nil { + return nil, err + } + payload.Bytes(pendingRaw) + } + rawPayload := payload.BytesCopy() + if len(rawPayload) == 0 || len(rawPayload) > maxEconomicCheckpointBytes { + return nil, ErrEconomicCheckpoint + } + digest := codec.DomainHash("zephyr/economic-runtime-checkpoint/v2", rawPayload) + var file bytes.Buffer + file.WriteString(economicCheckpointMagic) + var length [4]byte + binary.BigEndian.PutUint32(length[:], uint32(len(rawPayload))) + file.Write(length[:]) + file.Write(rawPayload) + file.Write(digest[:]) + return file.Bytes(), nil +} + +// RestoreEconomicCheckpointBytes restores only the economic subsystem. The +// caller must first recover the normal consensus/world-state runtime to the +// exact same height and ParentHash. A stale checkpoint therefore fails closed +// instead of moving consensus height or silently replaying economics against a +// different state root. +func (r *Runtime) RestoreEconomicCheckpointBytes(data []byte, registry *compute.WorkRegistry, engineConfig economics.ShadowEpochEngineConfig) error { + if r == nil { + return ErrEconomicCheckpoint + } + decoded, err := decodeEconomicCheckpoint(data, registry, engineConfig) + if err != nil { + return err + } + r.mu.Lock() + defer r.mu.Unlock() + if r.economicCollector != nil || r.economicEngine != nil || r.pendingEconomic != nil || + decoded.network != r.Network || decoded.nativeToken != r.NativeToken || decoded.validatorRoot != r.ValidatorRoot || + decoded.shardCount != r.ShardCount || decoded.height != r.Height || decoded.parentHash != r.ParentHash || + decoded.collector.LastHeight() != r.Height { + return ErrEconomicCheckpoint + } + if err := validateRecoveredEconomics(decoded.collector, decoded.engine, decoded.pending, decoded.epochLength, decoded.balances); err != nil { + return err + } + r.economicCollector = decoded.collector + r.economicEngine = decoded.engine + r.pendingEconomic = decoded.pending + r.economicEpochLength = decoded.epochLength + r.economicBalances = decoded.balances + return nil +} + +func (r *Runtime) SaveEconomicCheckpoint(path string) error { + raw, err := r.EconomicCheckpointBytes() + if err != nil { + return err + } + return writeAtomicEconomicCheckpoint(path, raw) +} + +func (r *Runtime) RestoreEconomicCheckpoint(path string, registry *compute.WorkRegistry, engineConfig economics.ShadowEpochEngineConfig) error { + raw, err := os.ReadFile(path) + if err != nil { + return err + } + if len(raw) > maxEconomicCheckpointBytes+8+32 { + return ErrEconomicCheckpoint + } + return r.RestoreEconomicCheckpointBytes(raw, registry, engineConfig) +} + +type decodedEconomicCheckpoint struct { + network types.NetworkID + nativeToken types.TokenID + validatorRoot types.Hash + shardCount uint32 + height uint64 + parentHash types.Hash + epochLength uint64 + balances economics.MonetaryBalanceSnapshot + collector *economics.EpochCollector + engine *economics.ShadowEpochEngine + pending *economics.ShadowEpochPreview +} + +func decodeEconomicCheckpoint(data []byte, registry *compute.WorkRegistry, engineConfig economics.ShadowEpochEngineConfig) (decodedEconomicCheckpoint, error) { + if len(data) < 8+32 || len(data) > maxEconomicCheckpointBytes+8+32 || string(data[:4]) != economicCheckpointMagic { + return decodedEconomicCheckpoint{}, ErrEconomicCheckpoint + } + length := binary.BigEndian.Uint32(data[4:8]) + if length == 0 || int(length) > maxEconomicCheckpointBytes || len(data) != 8+int(length)+32 { + return decodedEconomicCheckpoint{}, ErrEconomicCheckpoint + } + payload := data[8 : 8+int(length)] + digest := codec.DomainHash("zephyr/economic-runtime-checkpoint/v2", payload) + if !bytes.Equal(digest[:], data[8+int(length):]) { + return decodedEconomicCheckpoint{}, ErrEconomicCheckpoint + } + r := codec.NewReader(payload) + version, err := r.U16() + if err != nil || version != economicCheckpointVersion { + return decodedEconomicCheckpoint{}, ErrEconomicCheckpoint + } + out := decodedEconomicCheckpoint{} + networkRaw, err := r.Fixed(32) + if err != nil { + return out, ErrEconomicCheckpoint + } + copy(out.network[:], networkRaw) + nativeRaw, err := r.Fixed(32) + if err != nil { + return out, ErrEconomicCheckpoint + } + copy(out.nativeToken[:], nativeRaw) + validatorRaw, err := r.Fixed(32) + if err != nil { + return out, ErrEconomicCheckpoint + } + copy(out.validatorRoot[:], validatorRaw) + out.shardCount, err = r.U32() + if err != nil || out.shardCount == 0 { + return out, ErrEconomicCheckpoint + } + out.height, err = r.U64() + if err != nil { + return out, ErrEconomicCheckpoint + } + parentRaw, err := r.Fixed(32) + if err != nil { + return out, ErrEconomicCheckpoint + } + copy(out.parentHash[:], parentRaw) + out.epochLength, err = r.U64() + if err != nil { + return out, ErrEconomicCheckpoint + } + out.balances.TotalSupply, err = r.U64() + if err != nil { + return out, ErrEconomicCheckpoint + } + out.balances.StakedSupply, err = r.U64() + if err != nil { + return out, ErrEconomicCheckpoint + } + out.balances.ProtocolReserve, err = r.U64() + if err != nil { + return out, ErrEconomicCheckpoint + } + out.balances.BaseFee, err = r.U64() + if err != nil { + return out, ErrEconomicCheckpoint + } + collectorRaw, err := r.Bytes(maxEconomicCheckpointBytes) + if err != nil { + return out, ErrEconomicCheckpoint + } + out.collector, err = economics.RestoreEpochCollector(collectorRaw, registry) + if err != nil { + return out, err + } + hasEngine, err := r.Bool() + if err != nil { + return out, ErrEconomicCheckpoint + } + if hasEngine { + engineRaw, err := r.Bytes(maxEconomicCheckpointBytes) + if err != nil { + return out, ErrEconomicCheckpoint + } + out.engine, err = economics.RestoreShadowEpochEngine(engineRaw, out.network, engineConfig) + if err != nil { + return out, err + } + } + hasPending, err := r.Bool() + if err != nil { + return out, ErrEconomicCheckpoint + } + if hasPending { + if out.engine == nil { + return out, ErrEconomicCheckpoint + } + pendingRaw, err := r.Bytes(maxEconomicCheckpointBytes) + if err != nil { + return out, ErrEconomicCheckpoint + } + pending, err := economics.RestoreShadowEpochPreview(pendingRaw, out.engine) + if err != nil { + return out, err + } + out.pending = &pending + } + if r.Done() != nil { + return out, ErrEconomicCheckpoint + } + return out, nil +} + +func validateRecoveredEconomics(collector *economics.EpochCollector, engine *economics.ShadowEpochEngine, pending *economics.ShadowEpochPreview, epochLength uint64, balances economics.MonetaryBalanceSnapshot) error { + if collector == nil { + return ErrEconomicCheckpoint + } + if engine == nil { + if pending != nil || epochLength != 0 || balances != (economics.MonetaryBalanceSnapshot{}) { + return ErrEconomicCheckpoint + } + return nil + } + if epochLength < 2 || balances.TotalSupply == 0 || balances.StakedSupply > balances.TotalSupply || + balances.ProtocolReserve > balances.TotalSupply || balances.BaseFee == 0 { + return ErrEconomicCheckpoint + } + previous, hasPrevious := engine.PreviousState() + if pending != nil { + if pending.State.Epoch+1 != collector.Epoch() { + return ErrEconomicCheckpoint + } + if hasPrevious { + if previous.Epoch+1 != pending.State.Epoch { + return ErrEconomicCheckpoint + } + } else if pending.State.Epoch != 1 { + return ErrEconomicCheckpoint + } + return nil + } + if hasPrevious { + if previous.Epoch+1 != collector.Epoch() { + return ErrEconomicCheckpoint + } + } else if collector.Epoch() != 1 { + return ErrEconomicCheckpoint + } + return nil +} + +func writeAtomicEconomicCheckpoint(path string, raw []byte) error { + if path == "" || len(raw) == 0 || len(raw) > maxEconomicCheckpointBytes+8+32 { + return ErrEconomicCheckpoint + } + dir := filepath.Dir(path) + base := filepath.Base(path) + tmp, err := os.OpenFile(filepath.Join(dir, "."+base+".tmp"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) + if err != nil { + return err + } + tmpPath := tmp.Name() + if _, err = tmp.Write(raw); err == nil { + err = tmp.Sync() + } + closeErr := tmp.Close() + if err == nil { + err = closeErr + } + if err != nil { + _ = os.Remove(tmpPath) + return err + } + if err := os.Chmod(tmpPath, 0o600); err != nil { + _ = os.Remove(tmpPath) + return err + } + if err := os.Rename(tmpPath, path); err != nil { + _ = os.Remove(tmpPath) + return err + } + dirHandle, err := os.Open(dir) + if err != nil { + return err + } + defer dirHandle.Close() + return dirHandle.Sync() +} From 7614c61281c9349127bf1a55ec68595e086ca85f Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:02:25 +0200 Subject: [PATCH 256/274] Test economic runtime checkpoint restart and tamper rejection --- internal/v2/node/economic_checkpoint_test.go | 141 +++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 internal/v2/node/economic_checkpoint_test.go diff --git a/internal/v2/node/economic_checkpoint_test.go b/internal/v2/node/economic_checkpoint_test.go new file mode 100644 index 00000000..13afc13b --- /dev/null +++ b/internal/v2/node/economic_checkpoint_test.go @@ -0,0 +1,141 @@ +package node + +import ( + "os" + "path/filepath" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestEconomicCheckpointRestoresPendingEpochAndFinalizesIt(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("economic-checkpoint"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + key, validators, validatorRoot := schedulerValidatorSet(t, network) + store := worldstate.NewMemory() + runtime, engineConfig := newCheckpointEconomicsRuntime(t, network, native, validatorRoot, store) + + commitEmptySchedulerBlock(t, runtime, key, validators, 1) + commitEmptySchedulerBlock(t, runtime, key, validators, 2) + pendingBefore, ok := runtime.PendingEconomicState() + if !ok || pendingBefore.Epoch != 1 { + t.Fatalf("expected pending first epoch before checkpoint: %#v", pendingBefore) + } + + path := filepath.Join(t.TempDir(), "economics.checkpoint") + if err := runtime.SaveEconomicCheckpoint(path); err != nil { + t.Fatal(err) + } + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o600 { + t.Fatalf("checkpoint permissions = %o, want 600", info.Mode().Perm()) + } + + restored, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: store}, 1) + if err != nil { + t.Fatal(err) + } + // Normal consensus/world-state recovery owns these anchors. Economic restore + // is deliberately forbidden from inventing them. + restored.Height = runtime.Height + restored.ParentHash = runtime.ParentHash + if err := restored.RestoreEconomicCheckpoint(path, nil, engineConfig); err != nil { + t.Fatal(err) + } + pendingAfter, ok := restored.PendingEconomicState() + if !ok || pendingAfter != pendingBefore { + t.Fatalf("pending state changed across restart: %#v != %#v", pendingAfter, pendingBefore) + } + candidate, err := restored.BuildCandidate(3, nil) + if err != nil { + t.Fatal(err) + } + commitSchedulerCandidate(t, restored, key, validators, candidate) + finalized, ok := restored.FinalizedEconomicState() + if !ok || finalized != pendingBefore { + t.Fatalf("restored pending state did not finalize: %#v", finalized) + } + if _, exists := store.GetObject(economics.MonetaryStateObjectID(network)); !exists { + t.Fatal("restored pending monetary object was not committed to world state") + } +} + +func TestEconomicCheckpointRejectsWrongChainAnchorAndTamper(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("economic-checkpoint-reject"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + key, validators, validatorRoot := schedulerValidatorSet(t, network) + store := worldstate.NewMemory() + runtime, engineConfig := newCheckpointEconomicsRuntime(t, network, native, validatorRoot, store) + commitEmptySchedulerBlock(t, runtime, key, validators, 1) + + raw, err := runtime.EconomicCheckpointBytes() + if err != nil { + t.Fatal(err) + } + wrongAnchor, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: store}, 1) + if err != nil { + t.Fatal(err) + } + wrongAnchor.Height = runtime.Height + wrongAnchor.ParentHash = types.Hash{99} + if err := wrongAnchor.RestoreEconomicCheckpointBytes(raw, nil, engineConfig); err == nil { + t.Fatal("economic checkpoint restored against a different parent hash") + } + + tampered := append([]byte(nil), raw...) + tampered[len(tampered)/2] ^= 0xff + matching, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: store}, 1) + if err != nil { + t.Fatal(err) + } + matching.Height = runtime.Height + matching.ParentHash = runtime.ParentHash + if err := matching.RestoreEconomicCheckpointBytes(tampered, nil, engineConfig); err == nil { + t.Fatal("tampered economic checkpoint was accepted") + } +} + +func newCheckpointEconomicsRuntime(t *testing.T, network types.NetworkID, native types.TokenID, validatorRoot types.Hash, store worldstate.Backend) (*Runtime, economics.ShadowEpochEngineConfig) { + t.Helper() + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: store}, 1) + if err != nil { + t.Fatal(err) + } + collector, err := economics.NewEpochCollector(economics.EpochCollectorConfig{ + Epoch: 1, ShardCount: 1, NativeToken: native, + InitialCirculatingSupply: map[uint32]uint64{0: 1_000_000}, + ResourceCapacityPerBlock: map[uint32]uint64{0: 100}, + VelocityPolicy: economics.VelocityPolicy{MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000}, + FeePolicy: economics.CompatibilityFeePolicy(), + }) + if err != nil { + t.Fatal(err) + } + if err := runtime.EnableShadowEconomics(collector); err != nil { + t.Fatal(err) + } + index := economics.ComputeIndexConfig{MinSamplesPerClass: 1, MinCoverageBps: 10_000, EWMABps: 10_000} + index.WeightsBps[compute.WorkCPUGeneral] = 10_000 + engineConfig := economics.ShadowEpochEngineConfig{ + ComputeIndex: index, ComputeScarcity: economics.DefaultComputeScarcityConfig(), + Monetary: economics.DefaultShadowPolicy(), + ComputeFeedback: economics.DefaultComputeFeedbackPolicy(economics.ComputeFeedbackObserveOnly), + } + engine, err := economics.NewShadowEpochEngine(network, engineConfig) + if err != nil { + t.Fatal(err) + } + if err := runtime.EnableShadowEconomicEpochs(engine, 2, economics.MonetaryBalanceSnapshot{TotalSupply: 1_000_000, BaseFee: 1}); err != nil { + t.Fatal(err) + } + return runtime, engineConfig +} + +var _ v2consensus.ValidatorSet From 9dc80a54b1d0f80f67d41eba60261b7e88bf412a Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:03:48 +0200 Subject: [PATCH 257/274] Temporarily format shadow epoch checkpoint test --- .github/workflows/v2-format-write.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/v2-format-write.yml b/.github/workflows/v2-format-write.yml index 26cc851a..01a6feb1 100644 --- a/.github/workflows/v2-format-write.yml +++ b/.github/workflows/v2-format-write.yml @@ -9,6 +9,7 @@ on: - internal/v2/provider/service.go - internal/v2/provider/service_test.go - internal/v2/tx/transaction.go + - internal/v2/economics/shadow_epoch_engine_checkpoint_test.go - .github/workflows/v2-format-write.yml permissions: @@ -30,7 +31,7 @@ jobs: go-version-file: go.mod cache: false - name: Format exact files - run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go + run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go internal/v2/economics/shadow_epoch_engine_checkpoint_test.go - name: Commit formatting if needed run: | if git diff --quiet; then @@ -39,6 +40,6 @@ jobs: fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go - git commit -m 'gofmt Zephyr v2 compute and provider files' + git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go internal/v2/economics/shadow_epoch_engine_checkpoint_test.go + git commit -m 'gofmt Zephyr v2 shadow epoch checkpoint test' git push origin HEAD:chatgpt/protocol-v2-foundation From 10a7cbf2da6103da3a55b2e6622ccce2d0abb0a3 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:04:19 +0200 Subject: [PATCH 258/274] Restore scoped v2 format workflow --- .github/workflows/v2-format-write.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/v2-format-write.yml b/.github/workflows/v2-format-write.yml index 01a6feb1..26cc851a 100644 --- a/.github/workflows/v2-format-write.yml +++ b/.github/workflows/v2-format-write.yml @@ -9,7 +9,6 @@ on: - internal/v2/provider/service.go - internal/v2/provider/service_test.go - internal/v2/tx/transaction.go - - internal/v2/economics/shadow_epoch_engine_checkpoint_test.go - .github/workflows/v2-format-write.yml permissions: @@ -31,7 +30,7 @@ jobs: go-version-file: go.mod cache: false - name: Format exact files - run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go internal/v2/economics/shadow_epoch_engine_checkpoint_test.go + run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go - name: Commit formatting if needed run: | if git diff --quiet; then @@ -40,6 +39,6 @@ jobs: fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go internal/v2/economics/shadow_epoch_engine_checkpoint_test.go - git commit -m 'gofmt Zephyr v2 shadow epoch checkpoint test' + git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go + git commit -m 'gofmt Zephyr v2 compute and provider files' git push origin HEAD:chatgpt/protocol-v2-foundation From 1d845e73765557943b5663fa159cd9a9802fbb24 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:07:57 +0200 Subject: [PATCH 259/274] Preflight all shard commitments before state mutation --- internal/v2/node/commit_preflight.go | 79 ++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 internal/v2/node/commit_preflight.go diff --git a/internal/v2/node/commit_preflight.go b/internal/v2/node/commit_preflight.go new file mode 100644 index 00000000..3c781753 --- /dev/null +++ b/internal/v2/node/commit_preflight.go @@ -0,0 +1,79 @@ +package node + +import ( + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +// preflightCandidateState reconstructs every shard commitment against the +// currently committed state before Runtime.Commit mutates any backend. This +// catches stale/missing objects, duplicate/conflicting outputs and tampered +// commitment/receipt/data roots deterministically across all shards. +// +// It does not make independent durable backends transactionally atomic against +// a later storage I/O failure; that requires the global commit coordinator. +func (r *Runtime) preflightCandidateState(candidate Candidate) (map[uint32]sharding.Commitment, []int, error) { + if r == nil || len(candidate.Commitments) != int(r.ShardCount) { + return nil, nil, ErrCandidateState + } + commitmentRoot, err := sharding.CommitmentRoot(candidate.Commitments) + if err != nil || commitmentRoot != candidate.Header.ShardCommitmentRoot { + return nil, nil, ErrCandidateState + } + + commitments := make(map[uint32]sharding.Commitment, r.ShardCount) + for _, commitment := range candidate.Commitments { + if commitment.ShardID >= r.ShardCount { + return nil, nil, ErrCandidateState + } + if _, duplicate := commitments[commitment.ShardID]; duplicate { + return nil, nil, ErrCandidateState + } + commitments[commitment.ShardID] = commitment + } + + dataLeaves := make([]types.Hash, r.ShardCount) + for shard := uint32(0); shard < r.ShardCount; shard++ { + commitment, ok := commitments[shard] + if !ok { + return nil, nil, ErrCandidateState + } + stateRoot := r.States[shard].Root() + if delta, changed := candidate.deltas[shard]; changed { + simulator, ok := r.States[shard].(worldstate.Simulator) + if !ok { + return nil, nil, ErrStateSimulation + } + stateRoot, err = simulator.Simulate(delta.Consumed, delta.Created) + if err != nil { + return nil, nil, err + } + } + if stateRoot != commitment.StateRoot { + return nil, nil, ErrCandidateState + } + + receiptRoot, err := (sharding.ReceiptBatch{Receipts: candidate.Receipts[shard]}).Root() + if err != nil || receiptRoot != commitment.ReceiptRoot { + return nil, nil, ErrCandidateState + } + dataLeaves[shard] = merkle.Leaf("shard-data-root", commitment.DataRoot[:]) + } + if merkle.Root(dataLeaves) != candidate.Header.DataRoot { + return nil, nil, ErrCandidateState + } + + shards := make([]int, 0, len(candidate.deltas)) + for shard := range candidate.deltas { + if shard >= r.ShardCount { + return nil, nil, ErrCandidateState + } + shards = append(shards, int(shard)) + } + sort.Ints(shards) + return commitments, shards, nil +} From ba3125088ee456e306c13d5c0280aacb01c93581 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:08:36 +0200 Subject: [PATCH 260/274] Run all-shard commitment preflight before apply --- internal/v2/node/runtime.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/internal/v2/node/runtime.go b/internal/v2/node/runtime.go index e1e285a4..6c662085 100644 --- a/internal/v2/node/runtime.go +++ b/internal/v2/node/runtime.go @@ -2,7 +2,6 @@ package node import ( "errors" - "sort" "sync" v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" @@ -275,6 +274,11 @@ func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificat return sharding.GlobalHeader{}, ErrCandidateState } + commitments, shards, err := r.preflightCandidateState(candidate) + if err != nil { + return sharding.GlobalHeader{}, err + } + var economicPreview *economics.EpochCollector if r.economicCollector != nil { if len(candidate.economicObservations) != int(r.ShardCount) { @@ -326,15 +330,6 @@ func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificat nextPending = &preview } - commitments := make(map[uint32]sharding.Commitment, len(candidate.Commitments)) - for _, commitment := range candidate.Commitments { - commitments[commitment.ShardID] = commitment - } - shards := make([]int, 0, len(candidate.deltas)) - for shard := range candidate.deltas { - shards = append(shards, int(shard)) - } - sort.Ints(shards) for _, shardValue := range shards { shard := uint32(shardValue) delta := candidate.deltas[shard] From a50f8c53a471a8aa8abe13792b1f1406bf470bf5 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:09:17 +0200 Subject: [PATCH 261/274] Test all-shard commit preflight safety --- internal/v2/node/commit_preflight_test.go | 151 ++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 internal/v2/node/commit_preflight_test.go diff --git a/internal/v2/node/commit_preflight_test.go b/internal/v2/node/commit_preflight_test.go new file mode 100644 index 00000000..47a62444 --- /dev/null +++ b/internal/v2/node/commit_preflight_test.go @@ -0,0 +1,151 @@ +package node + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestCommitPreflightRejectsStaleLaterShardBeforeAnyApply(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("preflight-stale"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + key, validators, validatorRoot := schedulerValidatorSet(t, network) + state0 := worldstate.NewMemory() + state1 := worldstate.NewMemory() + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: state0, 1: state1}, 1) + if err != nil { + t.Fatal(err) + } + + old0 := systemObjectForShard(0, 1, "old-0") + old1 := systemObjectForShard(1, 1, "old-1") + if _, err := state0.Apply(nil, []object.Object{old0}); err != nil { + t.Fatal(err) + } + if _, err := state1.Apply(nil, []object.Object{old1}); err != nil { + t.Fatal(err) + } + new0 := systemObjectForShard(0, 2, "new-0") + new1 := systemObjectForShard(1, 2, "new-1") + deltas := map[uint32]shardDelta{ + 0: {Consumed: []types.ObjectID{old0.ID}, Created: []object.Object{new0}}, + 1: {Consumed: []types.ObjectID{old1.ID}, Created: []object.Object{new1}}, + } + candidate := manualCandidateForDeltas(t, runtime, deltas) + + root0Before := state0.Root() + interloper := systemObjectForShard(1, 3, "interloper") + if _, err := state1.Apply([]types.ObjectID{old1.ID}, []object.Object{interloper}); err != nil { + t.Fatal(err) + } + commitSchedulerCandidateExpectError(t, runtime, key, validators, candidate) + + if state0.Root() != root0Before { + t.Fatal("shard 0 mutated before stale shard 1 was rejected") + } + if _, ok := state0.GetObject(old0.ID); !ok { + t.Fatal("shard 0 consumed object despite failed all-shard preflight") + } + if _, ok := state0.GetObject(new0.ID); ok { + t.Fatal("shard 0 created object despite failed all-shard preflight") + } + if runtime.Height != 0 { + t.Fatalf("failed preflight advanced runtime height to %d", runtime.Height) + } +} + +func TestCommitPreflightRejectsTamperedUntouchedShardRoot(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("preflight-root"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + key, validators, validatorRoot := schedulerValidatorSet(t, network) + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: worldstate.NewMemory(), 1: worldstate.NewMemory()}, 1) + if err != nil { + t.Fatal(err) + } + candidate, err := runtime.BuildCandidate(1, nil) + if err != nil { + t.Fatal(err) + } + candidate.Commitments[1].StateRoot = types.Hash{99} + root, err := sharding.CommitmentRoot(candidate.Commitments) + if err != nil { + t.Fatal(err) + } + candidate.Header.ShardCommitmentRoot = root + commitSchedulerCandidateExpectError(t, runtime, key, validators, candidate) + if runtime.Height != 0 { + t.Fatal("tampered untouched shard commitment finalized") + } +} + +func TestCommitPreflightRejectsTamperedHeaderDataRoot(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("preflight-data"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + key, validators, validatorRoot := schedulerValidatorSet(t, network) + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: worldstate.NewMemory()}, 1) + if err != nil { + t.Fatal(err) + } + candidate, err := runtime.BuildCandidate(1, nil) + if err != nil { + t.Fatal(err) + } + candidate.Header.DataRoot = types.Hash{77} + commitSchedulerCandidateExpectError(t, runtime, key, validators, candidate) + if runtime.Height != 0 { + t.Fatal("tampered global data root finalized") + } +} + +func manualCandidateForDeltas(t *testing.T, runtime *Runtime, deltas map[uint32]shardDelta) Candidate { + t.Helper() + commitments := make([]sharding.Commitment, 0, runtime.ShardCount) + dataLeaves := make([]types.Hash, runtime.ShardCount) + receipts := make(map[uint32][]sharding.CrossShardReceipt, runtime.ShardCount) + for shard := uint32(0); shard < runtime.ShardCount; shard++ { + stateRoot := runtime.States[shard].Root() + if delta, ok := deltas[shard]; ok { + simulator := runtime.States[shard].(worldstate.Simulator) + var err error + stateRoot, err = simulator.Simulate(delta.Consumed, delta.Created) + if err != nil { + t.Fatal(err) + } + } + receiptRoot, err := (sharding.ReceiptBatch{}).Root() + if err != nil { + t.Fatal(err) + } + dataRoot := merkle.Root(nil) + commitments = append(commitments, sharding.Commitment{ShardID: shard, StateRoot: stateRoot, ReceiptRoot: receiptRoot, DataRoot: dataRoot}) + dataLeaves[shard] = merkle.Leaf("shard-data-root", dataRoot[:]) + } + commitmentRoot, err := sharding.CommitmentRoot(commitments) + if err != nil { + t.Fatal(err) + } + return Candidate{ + Header: sharding.GlobalHeader{ + Version: 2, Network: runtime.Network, Height: 1, ParentHash: runtime.ParentHash, + ShardCommitmentRoot: commitmentRoot, ValidatorRoot: runtime.ValidatorRoot, + NextValidatorRoot: runtime.ValidatorRoot, DataRoot: merkle.Root(dataLeaves), + }, + Commitments: commitments, + Results: make(map[uint32][]execution.Result), + Receipts: receipts, + deltas: deltas, + } +} + +func systemObjectForShard(shard, index uint32, label string) object.Object { + return object.Object{ + ID: types.ObjectIDForShard(types.HashBytes("preflight-object", []byte(label)), index, shard), + Version: 1, + Kind: object.KindSystem, + Data: []byte(label), + } +} From e31d60b9c8ba002d2299b0bd930512e1a2e2541c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:12:03 +0200 Subject: [PATCH 262/274] Expose v2 runtime fail-stop recovery state --- internal/v2/node/recovery_state.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 internal/v2/node/recovery_state.go diff --git a/internal/v2/node/recovery_state.go b/internal/v2/node/recovery_state.go new file mode 100644 index 00000000..23bd6fd2 --- /dev/null +++ b/internal/v2/node/recovery_state.go @@ -0,0 +1,14 @@ +package node + +// RecoveryRequired reports whether a state backend returned an uncertain +// result during a consensus-finalized apply. Once set, the runtime refuses to +// build or commit further candidates until the normal recovery path reconstructs +// a safe state anchor. +func (r *Runtime) RecoveryRequired() bool { + if r == nil { + return true + } + r.mu.Lock() + defer r.mu.Unlock() + return r.recoveryRequired +} From 5bceddcf67e29d4f04897c9020a9bbf56705db69 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:12:20 +0200 Subject: [PATCH 263/274] Test fail-stop after uncertain state apply --- internal/v2/node/recovery_state_test.go | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 internal/v2/node/recovery_state_test.go diff --git a/internal/v2/node/recovery_state_test.go b/internal/v2/node/recovery_state_test.go new file mode 100644 index 00000000..62468513 --- /dev/null +++ b/internal/v2/node/recovery_state_test.go @@ -0,0 +1,63 @@ +package node + +import ( + "errors" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +type uncertainBackend struct { + *worldstate.Memory + failAfterApply bool +} + +func (b *uncertainBackend) Apply(consumed []types.ObjectID, created []object.Object) (types.Hash, error) { + root, err := b.Memory.Apply(consumed, created) + if err != nil { + return root, err + } + if b.failAfterApply { + return root, errors.New("simulated durable write acknowledgement failure") + } + return root, nil +} + +func TestRuntimeFailStopsAfterUncertainStateApply(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("fail-stop"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + key, validators, validatorRoot := schedulerValidatorSet(t, network) + backend := &uncertainBackend{Memory: worldstate.NewMemory()} + oldObject := systemObjectForShard(0, 1, "fail-stop-old") + if _, err := backend.Memory.Apply(nil, []object.Object{oldObject}); err != nil { + t.Fatal(err) + } + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: backend}, 1) + if err != nil { + t.Fatal(err) + } + newObject := systemObjectForShard(0, 2, "fail-stop-new") + candidate := manualCandidateForDeltas(t, runtime, map[uint32]shardDelta{ + 0: {Consumed: []types.ObjectID{oldObject.ID}, Created: []object.Object{newObject}}, + }) + backend.failAfterApply = true + commitSchedulerCandidateExpectError(t, runtime, key, validators, candidate) + + if !runtime.RecoveryRequired() { + t.Fatal("runtime did not enter recovery-required state after uncertain apply") + } + if runtime.Height != 0 { + t.Fatalf("uncertain apply advanced consensus height to %d", runtime.Height) + } + if _, ok := backend.GetObject(newObject.ID); !ok { + t.Fatal("test backend did not simulate a post-mutation acknowledgement failure") + } + if _, err := runtime.BuildCandidate(1, nil); !errors.Is(err, ErrRuntimeRecoveryRequired) { + t.Fatalf("runtime continued building after uncertain state apply: %v", err) + } + if _, err := runtime.Commit(candidate, v2consensus.Certificate{}, validators); !errors.Is(err, ErrRuntimeRecoveryRequired) { + t.Fatalf("runtime continued committing after uncertain state apply: %v", err) + } +} From 5fe6168018d666384498fa59bde2f0c292a41a6d Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:15:48 +0200 Subject: [PATCH 264/274] Add canonical global shard commit journal format --- internal/v2/node/global_commit_journal.go | 430 ++++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 internal/v2/node/global_commit_journal.go diff --git a/internal/v2/node/global_commit_journal.go b/internal/v2/node/global_commit_journal.go new file mode 100644 index 00000000..c040fcc9 --- /dev/null +++ b/internal/v2/node/global_commit_journal.go @@ -0,0 +1,430 @@ +package node + +import ( + "bytes" + "encoding/binary" + "errors" + "os" + "path/filepath" + "sort" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + globalCommitJournalVersion uint16 = 1 + globalCommitJournalMagic = "ZGC2" + globalCommitPreparing uint8 = 1 + globalCommitCommitted uint8 = 2 + maxGlobalCommitJournalBytes = 256 << 20 + maxJournalObjects = 1_000_000 +) + +var ErrGlobalCommitJournal = errors.New("invalid Zephyr v2 global commit journal") + +type journalShardDelta struct { + ShardID uint32 + PreRoot types.Hash + PostRoot types.Hash + Consumed []types.ObjectID + Created []object.Object +} + +type globalCommitIntent struct { + Status uint8 + Network types.NetworkID + NativeToken types.TokenID + ValidatorRoot types.Hash + ShardCount uint32 + PreHeight uint64 + PreParentHash types.Hash + Header sharding.GlobalHeader + Certificate v2consensus.Certificate + Commitments []sharding.Commitment + Deltas []journalShardDelta + EconomicCheckpoint []byte +} + +func (r *Runtime) buildGlobalCommitIntent(candidate Candidate, certificate v2consensus.Certificate, commitments map[uint32]sharding.Commitment, economicCheckpoint []byte) (globalCommitIntent, error) { + if r == nil || candidate.Header.Height != r.Height+1 || candidate.Header.ParentHash != r.ParentHash || len(commitments) != int(r.ShardCount) { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + intent := globalCommitIntent{ + Status: globalCommitPreparing, Network: r.Network, NativeToken: r.NativeToken, + ValidatorRoot: r.ValidatorRoot, ShardCount: r.ShardCount, PreHeight: r.Height, + PreParentHash: r.ParentHash, Header: candidate.Header, Certificate: certificate, + Commitments: append([]sharding.Commitment(nil), candidate.Commitments...), + EconomicCheckpoint: append([]byte(nil), economicCheckpoint...), + } + shards := make([]int, 0, len(candidate.deltas)) + for shard := range candidate.deltas { + shards = append(shards, int(shard)) + } + sort.Ints(shards) + for _, shardValue := range shards { + shard := uint32(shardValue) + commitment, ok := commitments[shard] + if !ok { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + delta := candidate.deltas[shard] + intent.Deltas = append(intent.Deltas, journalShardDelta{ + ShardID: shard, PreRoot: r.States[shard].Root(), PostRoot: commitment.StateRoot, + Consumed: append([]types.ObjectID(nil), delta.Consumed...), + Created: cloneJournalObjects(delta.Created), + }) + } + if err := intent.Validate(); err != nil { + return globalCommitIntent{}, err + } + return intent, nil +} + +func (i globalCommitIntent) Validate() error { + if (i.Status != globalCommitPreparing && i.Status != globalCommitCommitted) || + types.IsZero32([32]byte(i.Network)) || types.IsZero32([32]byte(i.NativeToken)) || + types.IsZero32([32]byte(i.ValidatorRoot)) || i.ShardCount == 0 || + i.Header.Network != i.Network || i.Header.Height != i.PreHeight+1 || i.Header.ParentHash != i.PreParentHash || + i.Header.ValidatorRoot != i.ValidatorRoot || i.Certificate.Network != i.Network || + i.Certificate.Height != i.Header.Height || i.Certificate.HeaderHash != v2consensus.HeaderConsensusHash(i.Header) || + len(i.Commitments) != int(i.ShardCount) || len(i.Deltas) > int(i.ShardCount) || + len(i.EconomicCheckpoint) > maxEconomicCheckpointBytes+8+32 { + return ErrGlobalCommitJournal + } + root, err := sharding.CommitmentRoot(i.Commitments) + if err != nil || root != i.Header.ShardCommitmentRoot { + return ErrGlobalCommitJournal + } + commitments := make(map[uint32]sharding.Commitment, i.ShardCount) + for _, commitment := range i.Commitments { + if commitment.ShardID >= i.ShardCount { + return ErrGlobalCommitJournal + } + if _, duplicate := commitments[commitment.ShardID]; duplicate { + return ErrGlobalCommitJournal + } + commitments[commitment.ShardID] = commitment + } + seenDelta := make(map[uint32]struct{}, len(i.Deltas)) + for _, delta := range i.Deltas { + if delta.ShardID >= i.ShardCount || len(delta.Consumed) > maxJournalObjects || len(delta.Created) > maxJournalObjects || + types.IsZero32([32]byte(delta.PreRoot)) || types.IsZero32([32]byte(delta.PostRoot)) { + return ErrGlobalCommitJournal + } + if _, duplicate := seenDelta[delta.ShardID]; duplicate { + return ErrGlobalCommitJournal + } + seenDelta[delta.ShardID] = struct{}{} + commitment, ok := commitments[delta.ShardID] + if !ok || commitment.StateRoot != delta.PostRoot { + return ErrGlobalCommitJournal + } + for _, id := range delta.Consumed { + if types.IsZero32([32]byte(id)) { + return ErrGlobalCommitJournal + } + } + for _, created := range delta.Created { + if created.Validate() != nil { + return ErrGlobalCommitJournal + } + } + } + return nil +} + +func (i globalCommitIntent) MarshalBinary() ([]byte, error) { + if err := i.Validate(); err != nil { + return nil, err + } + var w codec.Writer + w.U16(globalCommitJournalVersion) + w.U8(i.Status) + w.Fixed(i.Network[:]) + w.Fixed(i.NativeToken[:]) + w.Fixed(i.ValidatorRoot[:]) + w.U32(i.ShardCount) + w.U64(i.PreHeight) + w.Fixed(i.PreParentHash[:]) + headerRaw, err := i.Header.MarshalBinary() + if err != nil { + return nil, err + } + w.Bytes(headerRaw) + certificateRaw, err := v2consensus.MarshalCertificate(i.Certificate) + if err != nil { + return nil, err + } + w.Bytes(certificateRaw) + + orderedCommitments := append([]sharding.Commitment(nil), i.Commitments...) + sort.Slice(orderedCommitments, func(a, b int) bool { return orderedCommitments[a].ShardID < orderedCommitments[b].ShardID }) + w.U32(uint32(len(orderedCommitments))) + for _, commitment := range orderedCommitments { + w.U32(commitment.ShardID) + w.Fixed(commitment.StateRoot[:]) + w.Fixed(commitment.ReceiptRoot[:]) + w.Fixed(commitment.DataRoot[:]) + } + + orderedDeltas := append([]journalShardDelta(nil), i.Deltas...) + sort.Slice(orderedDeltas, func(a, b int) bool { return orderedDeltas[a].ShardID < orderedDeltas[b].ShardID }) + w.U32(uint32(len(orderedDeltas))) + for _, delta := range orderedDeltas { + w.U32(delta.ShardID) + w.Fixed(delta.PreRoot[:]) + w.Fixed(delta.PostRoot[:]) + w.U32(uint32(len(delta.Consumed))) + for _, id := range delta.Consumed { + w.Fixed(id[:]) + } + w.U32(uint32(len(delta.Created))) + for _, created := range delta.Created { + w.Bytes(created.CanonicalBytes()) + } + } + w.Bytes(i.EconomicCheckpoint) + return w.BytesCopy(), nil +} + +func parseGlobalCommitIntent(data []byte) (globalCommitIntent, error) { + r := codec.NewReader(data) + version, err := r.U16() + if err != nil || version != globalCommitJournalVersion { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + intent := globalCommitIntent{} + intent.Status, err = r.U8() + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + networkRaw, err := r.Fixed(32) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + copy(intent.Network[:], networkRaw) + nativeRaw, err := r.Fixed(32) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + copy(intent.NativeToken[:], nativeRaw) + validatorRaw, err := r.Fixed(32) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + copy(intent.ValidatorRoot[:], validatorRaw) + intent.ShardCount, err = r.U32() + if err != nil || intent.ShardCount == 0 || intent.ShardCount > 1_000_000 { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + intent.PreHeight, err = r.U64() + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + parentRaw, err := r.Fixed(32) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + copy(intent.PreParentHash[:], parentRaw) + headerRaw, err := r.Bytes(1 << 20) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + intent.Header, err = sharding.ParseGlobalHeader(headerRaw) + if err != nil { + return globalCommitIntent{}, err + } + certificateRaw, err := r.Bytes(64 << 20) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + intent.Certificate, err = v2consensus.ParseCertificate(certificateRaw) + if err != nil { + return globalCommitIntent{}, err + } + + commitmentCount, err := r.U32() + if err != nil || commitmentCount != intent.ShardCount { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + intent.Commitments = make([]sharding.Commitment, int(commitmentCount)) + for index := range intent.Commitments { + intent.Commitments[index].ShardID, err = r.U32() + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + stateRaw, err := r.Fixed(32) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + copy(intent.Commitments[index].StateRoot[:], stateRaw) + receiptRaw, err := r.Fixed(32) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + copy(intent.Commitments[index].ReceiptRoot[:], receiptRaw) + dataRaw, err := r.Fixed(32) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + copy(intent.Commitments[index].DataRoot[:], dataRaw) + } + + deltaCount, err := r.U32() + if err != nil || deltaCount > intent.ShardCount { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + intent.Deltas = make([]journalShardDelta, int(deltaCount)) + for index := range intent.Deltas { + delta := &intent.Deltas[index] + delta.ShardID, err = r.U32() + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + preRaw, err := r.Fixed(32) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + copy(delta.PreRoot[:], preRaw) + postRaw, err := r.Fixed(32) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + copy(delta.PostRoot[:], postRaw) + consumedCount, err := r.U32() + if err != nil || consumedCount > maxJournalObjects { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + delta.Consumed = make([]types.ObjectID, int(consumedCount)) + for consumedIndex := range delta.Consumed { + raw, err := r.Fixed(32) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + copy(delta.Consumed[consumedIndex][:], raw) + } + createdCount, err := r.U32() + if err != nil || createdCount > maxJournalObjects { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + delta.Created = make([]object.Object, int(createdCount)) + for createdIndex := range delta.Created { + raw, err := r.Bytes(object.MaxObjectDataBytes + 128) + if err != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + created, err := object.ParseObject(raw) + if err != nil { + return globalCommitIntent{}, err + } + delta.Created[createdIndex] = created + } + } + intent.EconomicCheckpoint, err = r.Bytes(maxEconomicCheckpointBytes + 8 + 32) + if err != nil || r.Done() != nil { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + if err := intent.Validate(); err != nil { + return globalCommitIntent{}, err + } + return intent, nil +} + +func encodeGlobalCommitJournal(intent globalCommitIntent) ([]byte, error) { + payload, err := intent.MarshalBinary() + if err != nil || len(payload) == 0 || len(payload) > maxGlobalCommitJournalBytes { + return nil, ErrGlobalCommitJournal + } + digest := codec.DomainHash("zephyr/global-commit-journal/v2", payload) + var out bytes.Buffer + out.WriteString(globalCommitJournalMagic) + var length [4]byte + binary.BigEndian.PutUint32(length[:], uint32(len(payload))) + out.Write(length[:]) + out.Write(payload) + out.Write(digest[:]) + return out.Bytes(), nil +} + +func decodeGlobalCommitJournal(data []byte) (globalCommitIntent, error) { + if len(data) < 8+32 || len(data) > maxGlobalCommitJournalBytes+8+32 || string(data[:4]) != globalCommitJournalMagic { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + length := binary.BigEndian.Uint32(data[4:8]) + if length == 0 || int(length) > maxGlobalCommitJournalBytes || len(data) != 8+int(length)+32 { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + payload := data[8 : 8+int(length)] + digest := codec.DomainHash("zephyr/global-commit-journal/v2", payload) + if !bytes.Equal(digest[:], data[8+int(length):]) { + return globalCommitIntent{}, ErrGlobalCommitJournal + } + return parseGlobalCommitIntent(payload) +} + +func writeGlobalCommitJournal(path string, intent globalCommitIntent) error { + raw, err := encodeGlobalCommitJournal(intent) + if err != nil { + return err + } + return writeAtomicGlobalCommitFile(path, raw) +} + +func readGlobalCommitJournal(path string) (globalCommitIntent, error) { + raw, err := os.ReadFile(path) + if err != nil { + return globalCommitIntent{}, err + } + return decodeGlobalCommitJournal(raw) +} + +func writeAtomicGlobalCommitFile(path string, raw []byte) error { + if path == "" || len(raw) == 0 || len(raw) > maxGlobalCommitJournalBytes+8+32 { + return ErrGlobalCommitJournal + } + dir := filepath.Dir(path) + base := filepath.Base(path) + tmp, err := os.OpenFile(filepath.Join(dir, "."+base+".tmp"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) + if err != nil { + return err + } + tmpPath := tmp.Name() + if _, err = tmp.Write(raw); err == nil { + err = tmp.Sync() + } + closeErr := tmp.Close() + if err == nil { + err = closeErr + } + if err != nil { + _ = os.Remove(tmpPath) + return err + } + if err := os.Chmod(tmpPath, 0o600); err != nil { + _ = os.Remove(tmpPath) + return err + } + if err := os.Rename(tmpPath, path); err != nil { + _ = os.Remove(tmpPath) + return err + } + dirHandle, err := os.Open(dir) + if err != nil { + return err + } + defer dirHandle.Close() + return dirHandle.Sync() +} + +func cloneJournalObjects(source []object.Object) []object.Object { + out := make([]object.Object, len(source)) + for index, item := range source { + out[index] = item + out[index].Data = append([]byte(nil), item.Data...) + } + return out +} From f035cd8ab549b4dce4168ee9c6038f26e7861490 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:16:04 +0200 Subject: [PATCH 265/274] Test canonical global commit journal encoding --- .../v2/node/global_commit_journal_test.go | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 internal/v2/node/global_commit_journal_test.go diff --git a/internal/v2/node/global_commit_journal_test.go b/internal/v2/node/global_commit_journal_test.go new file mode 100644 index 00000000..2e69f1ac --- /dev/null +++ b/internal/v2/node/global_commit_journal_test.go @@ -0,0 +1,67 @@ +package node + +import ( + "bytes" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestGlobalCommitJournalRoundTripAndTamperRejection(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("global-journal"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + key, validators, validatorRoot := schedulerValidatorSet(t, network) + state0 := worldstate.NewMemory() + state1 := worldstate.NewMemory() + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: state0, 1: state1}, 1) + if err != nil { + t.Fatal(err) + } + old0 := systemObjectForShard(0, 1, "journal-old-0") + old1 := systemObjectForShard(1, 1, "journal-old-1") + if _, err := state0.Apply(nil, []object.Object{old0}); err != nil { + t.Fatal(err) + } + if _, err := state1.Apply(nil, []object.Object{old1}); err != nil { + t.Fatal(err) + } + candidate := manualCandidateForDeltas(t, runtime, map[uint32]shardDelta{ + 0: {Consumed: []types.ObjectID{old0.ID}, Created: []object.Object{systemObjectForShard(0, 2, "journal-new-0")}}, + 1: {Consumed: []types.ObjectID{old1.ID}, Created: []object.Object{systemObjectForShard(1, 2, "journal-new-1")}}, + }) + certificate := schedulerCertificate(t, runtime, key, validators, candidate) + commitments, _, err := runtime.preflightCandidateState(candidate) + if err != nil { + t.Fatal(err) + } + intent, err := runtime.buildGlobalCommitIntent(candidate, certificate, commitments, []byte("economic-checkpoint")) + if err != nil { + t.Fatal(err) + } + raw, err := encodeGlobalCommitJournal(intent) + if err != nil { + t.Fatal(err) + } + parsed, err := decodeGlobalCommitJournal(raw) + if err != nil { + t.Fatal(err) + } + rawAgain, err := encodeGlobalCommitJournal(parsed) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(raw, rawAgain) { + t.Fatal("global commit journal is not canonical across round trip") + } + if parsed.Header != intent.Header || parsed.Certificate.Hash() != intent.Certificate.Hash() || len(parsed.Deltas) != 2 { + t.Fatalf("global commit journal lost certified state: %#v", parsed) + } + + tampered := append([]byte(nil), raw...) + tampered[len(tampered)/2] ^= 0xff + if _, err := decodeGlobalCommitJournal(tampered); err == nil { + t.Fatal("tampered global commit journal was accepted") + } +} From 8459fcf3d2a5753f275a69d0859a9ced54cac974 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:17:35 +0200 Subject: [PATCH 266/274] Add global shard commit journal recovery --- internal/v2/node/global_commit_recovery.go | 171 +++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 internal/v2/node/global_commit_recovery.go diff --git a/internal/v2/node/global_commit_recovery.go b/internal/v2/node/global_commit_recovery.go new file mode 100644 index 00000000..17a0c1d3 --- /dev/null +++ b/internal/v2/node/global_commit_recovery.go @@ -0,0 +1,171 @@ +package node + +import ( + "errors" + "os" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +// EnableGlobalCommitJournal configures the crash-recovery journal used to make +// a QC-authorized multi-shard state transition recoverable across process or I/O +// failure. If a journal already exists, the runtime fails closed until +// RecoverGlobalCommitJournal validates it. +func (r *Runtime) EnableGlobalCommitJournal(path string) error { + if r == nil || path == "" { + return ErrRuntimeConfig + } + r.mu.Lock() + defer r.mu.Unlock() + if r.globalCommitJournalPath != "" { + return ErrRuntimeConfig + } + r.globalCommitJournalPath = path + if info, err := os.Stat(path); err == nil { + if info.Size() > 0 { + r.recoveryRequired = true + } + } else if !errors.Is(err, os.ErrNotExist) { + return err + } + return nil +} + +// RecoverGlobalCommitJournal verifies the certified commit intent and repairs a +// PREPARING journal by applying only shards still at their exact pre-root. A +// shard already at its post-root is never applied twice. A COMMITTED journal +// requires every shard to already be at its post-root. +func (r *Runtime) RecoverGlobalCommitJournal(validators v2consensus.ValidatorSet, registry *compute.WorkRegistry, engineConfig economics.ShadowEpochEngineConfig) error { + if r == nil { + return ErrGlobalCommitJournal + } + r.mu.Lock() + defer r.mu.Unlock() + if r.globalCommitJournalPath == "" { + return ErrGlobalCommitJournal + } + intent, err := readGlobalCommitJournal(r.globalCommitJournalPath) + if err != nil { + return err + } + if intent.Network != r.Network || intent.NativeToken != r.NativeToken || intent.ValidatorRoot != r.ValidatorRoot || intent.ShardCount != r.ShardCount { + return ErrGlobalCommitJournal + } + validatorRoot, err := validators.Root() + if err != nil || validatorRoot != r.ValidatorRoot || validators.Network != r.Network { + return ErrGlobalCommitJournal + } + if err := validators.VerifyCertificate(intent.Certificate); err != nil { + return err + } + if intent.Certificate.HeaderHash != v2consensus.HeaderConsensusHash(intent.Header) || intent.Certificate.Height != intent.Header.Height { + return ErrGlobalCommitJournal + } + postParent := v2consensus.HeaderConsensusHash(intent.Header) + atPreAnchor := r.Height == intent.PreHeight && r.ParentHash == intent.PreParentHash + atPostAnchor := r.Height == intent.Header.Height && r.ParentHash == postParent + if !atPreAnchor && !atPostAnchor { + return ErrGlobalCommitJournal + } + + commitments := make(map[uint32]types.Hash, intent.ShardCount) + for _, commitment := range intent.Commitments { + commitments[commitment.ShardID] = commitment.StateRoot + } + deltas := make(map[uint32]journalShardDelta, len(intent.Deltas)) + for _, delta := range intent.Deltas { + deltas[delta.ShardID] = delta + } + + for shard := uint32(0); shard < r.ShardCount; shard++ { + postRoot, ok := commitments[shard] + if !ok { + return ErrGlobalCommitJournal + } + current := r.States[shard].Root() + delta, changed := deltas[shard] + if !changed { + if current != postRoot { + return ErrGlobalCommitJournal + } + continue + } + if current == delta.PostRoot { + continue + } + if intent.Status == globalCommitCommitted || current != delta.PreRoot || postRoot != delta.PostRoot { + return ErrGlobalCommitJournal + } + simulator, ok := r.States[shard].(worldstate.Simulator) + if !ok { + return ErrStateSimulation + } + previewRoot, err := simulator.Simulate(delta.Consumed, delta.Created) + if err != nil || previewRoot != delta.PostRoot { + return ErrGlobalCommitJournal + } + appliedRoot, err := r.States[shard].Apply(delta.Consumed, delta.Created) + if err != nil || appliedRoot != delta.PostRoot { + r.recoveryRequired = true + if err != nil { + return errors.Join(ErrRuntimeRecoveryRequired, err) + } + return errors.Join(ErrRuntimeRecoveryRequired, ErrGlobalCommitJournal) + } + } + + if err := r.restoreJournalEconomics(intent.EconomicCheckpoint, registry, engineConfig, intent.Header.Height, postParent); err != nil { + return err + } + if intent.Status != globalCommitCommitted { + intent.Status = globalCommitCommitted + if err := writeGlobalCommitJournal(r.globalCommitJournalPath, intent); err != nil { + r.recoveryRequired = true + return errors.Join(ErrRuntimeRecoveryRequired, err) + } + } + r.Height = intent.Header.Height + r.ParentHash = postParent + r.recoveryRequired = false + return nil +} + +func (r *Runtime) restoreJournalEconomics(raw []byte, registry *compute.WorkRegistry, engineConfig economics.ShadowEpochEngineConfig, height uint64, parent types.Hash) error { + if len(raw) == 0 { + if r.economicCollector != nil || r.economicEngine != nil || r.pendingEconomic != nil { + return ErrGlobalCommitJournal + } + return nil + } + decoded, err := decodeEconomicCheckpoint(raw, registry, engineConfig) + if err != nil { + return err + } + if decoded.network != r.Network || decoded.nativeToken != r.NativeToken || decoded.validatorRoot != r.ValidatorRoot || + decoded.shardCount != r.ShardCount || decoded.height != height || decoded.parentHash != parent || decoded.collector.LastHeight() != height { + return ErrGlobalCommitJournal + } + r.economicCollector = decoded.collector + r.economicEngine = decoded.engine + r.pendingEconomic = decoded.pending + r.economicEpochLength = decoded.epochLength + r.economicBalances = decoded.balances + return nil +} + +func (r *Runtime) postCommitEconomicCheckpoint(collector *economics.EpochCollector, engine *economics.ShadowEpochEngine, pending *economics.ShadowEpochPreview, balances economics.MonetaryBalanceSnapshot, height uint64, parent types.Hash) ([]byte, error) { + if collector == nil { + return nil, nil + } + temporary := Runtime{ + Network: r.Network, NativeToken: r.NativeToken, ValidatorRoot: r.ValidatorRoot, + ShardCount: r.ShardCount, Height: height, ParentHash: parent, + economicCollector: collector, economicEngine: engine, + economicEpochLength: r.economicEpochLength, economicBalances: balances, pendingEconomic: pending, + } + return temporary.economicCheckpointBytesLocked() +} From 9c8bf054eba7c6647934770f6fbb76007550edf7 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:19:12 +0200 Subject: [PATCH 267/274] Test recovery of partial and committed global shard journals --- .../v2/node/global_commit_recovery_test.go | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 internal/v2/node/global_commit_recovery_test.go diff --git a/internal/v2/node/global_commit_recovery_test.go b/internal/v2/node/global_commit_recovery_test.go new file mode 100644 index 00000000..1f2b8cf6 --- /dev/null +++ b/internal/v2/node/global_commit_recovery_test.go @@ -0,0 +1,146 @@ +package node + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/object" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +type failBeforeApplyBackend struct { + *worldstate.Memory + fail bool +} + +func (b *failBeforeApplyBackend) Apply(consumed []types.ObjectID, created []object.Object) (types.Hash, error) { + if b.fail { + return b.Root(), errors.New("simulated shard storage failure before apply") + } + return b.Memory.Apply(consumed, created) +} + +func TestGlobalCommitJournalCompletesPartialMultiShardApply(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("global-recovery-partial"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + key, validators, validatorRoot := schedulerValidatorSet(t, network) + state0 := worldstate.NewMemory() + state1 := &failBeforeApplyBackend{Memory: worldstate.NewMemory()} + old0 := systemObjectForShard(0, 1, "recover-old-0") + old1 := systemObjectForShard(1, 1, "recover-old-1") + if _, err := state0.Apply(nil, []object.Object{old0}); err != nil { + t.Fatal(err) + } + if _, err := state1.Memory.Apply(nil, []object.Object{old1}); err != nil { + t.Fatal(err) + } + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: state0, 1: state1}, 1) + if err != nil { + t.Fatal(err) + } + journalPath := filepath.Join(t.TempDir(), "global-commit.journal") + if err := runtime.EnableGlobalCommitJournal(journalPath); err != nil { + t.Fatal(err) + } + new0 := systemObjectForShard(0, 2, "recover-new-0") + new1 := systemObjectForShard(1, 2, "recover-new-1") + candidate := manualCandidateForDeltas(t, runtime, map[uint32]shardDelta{ + 0: {Consumed: []types.ObjectID{old0.ID}, Created: []object.Object{new0}}, + 1: {Consumed: []types.ObjectID{old1.ID}, Created: []object.Object{new1}}, + }) + state1.fail = true + commitSchedulerCandidateExpectError(t, runtime, key, validators, candidate) + if !runtime.RecoveryRequired() || runtime.Height != 0 { + t.Fatal("partial global apply did not fail-stop at the pre-commit anchor") + } + intent, err := readGlobalCommitJournal(journalPath) + if err != nil { + t.Fatal(err) + } + if intent.Status != globalCommitPreparing { + t.Fatalf("partial apply journal status = %d, want PREPARING", intent.Status) + } + if _, ok := state0.GetObject(new0.ID); !ok { + t.Fatal("first shard was not applied before injected second-shard failure") + } + if _, ok := state1.GetObject(old1.ID); !ok { + t.Fatal("second shard unexpectedly mutated before injected failure") + } + + state1.fail = false + if err := runtime.RecoverGlobalCommitJournal(validators, nil, economics.ShadowEpochEngineConfig{}); err != nil { + t.Fatal(err) + } + if runtime.RecoveryRequired() || runtime.Height != 1 { + t.Fatalf("global recovery did not advance the certified commit: height=%d recovery=%v", runtime.Height, runtime.RecoveryRequired()) + } + if _, ok := state1.GetObject(new1.ID); !ok { + t.Fatal("recovery did not complete the missing shard transition") + } + intent, err = readGlobalCommitJournal(journalPath) + if err != nil { + t.Fatal(err) + } + if intent.Status != globalCommitCommitted { + t.Fatalf("recovered journal status = %d, want COMMITTED", intent.Status) + } + if _, err := runtime.BuildCandidate(2, nil); err != nil { + t.Fatalf("runtime remained blocked after successful recovery: %v", err) + } +} + +func TestCommittedGlobalJournalRestoresRuntimeAnchorAfterRestart(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("global-recovery-committed"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + key, validators, validatorRoot := schedulerValidatorSet(t, network) + state := worldstate.NewMemory() + oldObject := systemObjectForShard(0, 1, "restart-old") + if _, err := state.Apply(nil, []object.Object{oldObject}); err != nil { + t.Fatal(err) + } + runtime, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: state}, 1) + if err != nil { + t.Fatal(err) + } + journalPath := filepath.Join(t.TempDir(), "global-commit.journal") + if err := runtime.EnableGlobalCommitJournal(journalPath); err != nil { + t.Fatal(err) + } + candidate := manualCandidateForDeltas(t, runtime, map[uint32]shardDelta{ + 0: {Consumed: []types.ObjectID{oldObject.ID}, Created: []object.Object{systemObjectForShard(0, 2, "restart-new")}}, + }) + commitSchedulerCandidate(t, runtime, key, validators, candidate) + if runtime.Height != 1 { + t.Fatal("initial certified commit did not advance") + } + info, err := os.Stat(journalPath) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o600 { + t.Fatalf("global journal mode = %o, want 600", info.Mode().Perm()) + } + + restarted, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: state}, 1) + if err != nil { + t.Fatal(err) + } + if err := restarted.EnableGlobalCommitJournal(journalPath); err != nil { + t.Fatal(err) + } + if !restarted.RecoveryRequired() { + t.Fatal("existing committed journal did not force explicit restart recovery") + } + if err := restarted.RecoverGlobalCommitJournal(validators, nil, economics.ShadowEpochEngineConfig{}); err != nil { + t.Fatal(err) + } + if restarted.Height != 1 || restarted.ParentHash != runtime.ParentHash || restarted.RecoveryRequired() { + t.Fatalf("committed journal did not restore runtime anchor: height=%d parent=%x", restarted.Height, restarted.ParentHash) + } + if _, err := restarted.BuildCandidate(2, nil); err != nil { + t.Fatalf("restarted runtime cannot continue after journal recovery: %v", err) + } +} From 4ab5e8787c0145d08055841132fcb8341ddc132d Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:20:57 +0200 Subject: [PATCH 268/274] Temporarily format v2 recovery and journal tranche --- .github/workflows/v2-format-write.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/v2-format-write.yml b/.github/workflows/v2-format-write.yml index 26cc851a..4cc4aa55 100644 --- a/.github/workflows/v2-format-write.yml +++ b/.github/workflows/v2-format-write.yml @@ -9,6 +9,24 @@ on: - internal/v2/provider/service.go - internal/v2/provider/service_test.go - internal/v2/tx/transaction.go + - internal/v2/node/runtime.go + - internal/v2/node/commit_preflight.go + - internal/v2/node/commit_preflight_test.go + - internal/v2/node/recovery_state.go + - internal/v2/node/recovery_state_test.go + - internal/v2/node/global_commit_journal.go + - internal/v2/node/global_commit_journal_test.go + - internal/v2/node/global_commit_recovery.go + - internal/v2/node/global_commit_recovery_test.go + - internal/v2/economics/finalized_collector_checkpoint.go + - internal/v2/economics/finalized_collector_checkpoint_test.go + - internal/v2/economics/shadow_epoch_engine_checkpoint.go + - internal/v2/economics/shadow_epoch_engine_checkpoint_test.go + - internal/v2/economics/epoch_parse.go + - internal/v2/economics/shadow_epoch_pending_checkpoint.go + - internal/v2/economics/shadow_epoch_pending_checkpoint_test.go + - internal/v2/compute/work.go + - internal/v2/compute/work_test.go - .github/workflows/v2-format-write.yml permissions: @@ -30,7 +48,7 @@ jobs: go-version-file: go.mod cache: false - name: Format exact files - run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go + run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go internal/v2/node/runtime.go internal/v2/node/commit_preflight.go internal/v2/node/commit_preflight_test.go internal/v2/node/recovery_state.go internal/v2/node/recovery_state_test.go internal/v2/node/global_commit_journal.go internal/v2/node/global_commit_journal_test.go internal/v2/node/global_commit_recovery.go internal/v2/node/global_commit_recovery_test.go internal/v2/economics/finalized_collector_checkpoint.go internal/v2/economics/finalized_collector_checkpoint_test.go internal/v2/economics/shadow_epoch_engine_checkpoint.go internal/v2/economics/shadow_epoch_engine_checkpoint_test.go internal/v2/economics/epoch_parse.go internal/v2/economics/shadow_epoch_pending_checkpoint.go internal/v2/economics/shadow_epoch_pending_checkpoint_test.go internal/v2/compute/work.go internal/v2/compute/work_test.go - name: Commit formatting if needed run: | if git diff --quiet; then @@ -39,6 +57,6 @@ jobs: fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go - git commit -m 'gofmt Zephyr v2 compute and provider files' + git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go internal/v2/node/runtime.go internal/v2/node/commit_preflight.go internal/v2/node/commit_preflight_test.go internal/v2/node/recovery_state.go internal/v2/node/recovery_state_test.go internal/v2/node/global_commit_journal.go internal/v2/node/global_commit_journal_test.go internal/v2/node/global_commit_recovery.go internal/v2/node/global_commit_recovery_test.go internal/v2/economics/finalized_collector_checkpoint.go internal/v2/economics/finalized_collector_checkpoint_test.go internal/v2/economics/shadow_epoch_engine_checkpoint.go internal/v2/economics/shadow_epoch_engine_checkpoint_test.go internal/v2/economics/epoch_parse.go internal/v2/economics/shadow_epoch_pending_checkpoint.go internal/v2/economics/shadow_epoch_pending_checkpoint_test.go internal/v2/compute/work.go internal/v2/compute/work_test.go + git commit -m 'gofmt Zephyr v2 recovery and journal tranche' git push origin HEAD:chatgpt/protocol-v2-foundation From 257d8e0ee240a2bf2b57020964ca5bcc196c3d7d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:21:18 +0000 Subject: [PATCH 269/274] gofmt Zephyr v2 recovery and journal tranche --- .../economics/shadow_epoch_pending_checkpoint.go | 10 +++++----- internal/v2/node/commit_preflight_test.go | 6 +++--- internal/v2/node/global_commit_journal.go | 16 ++++++++-------- internal/v2/node/global_commit_recovery.go | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/internal/v2/economics/shadow_epoch_pending_checkpoint.go b/internal/v2/economics/shadow_epoch_pending_checkpoint.go index 439743ed..10ee4cd3 100644 --- a/internal/v2/economics/shadow_epoch_pending_checkpoint.go +++ b/internal/v2/economics/shadow_epoch_pending_checkpoint.go @@ -75,12 +75,12 @@ func RestoreShadowEpochPreview(data []byte, engine *ShadowEpochEngine) (ShadowEp return ShadowEpochPreview{}, err } preview := ShadowEpochPreview{ - Aggregate: aggregate, - ComputeIndex: index, + Aggregate: aggregate, + ComputeIndex: index, ComputeScarcity: ComputeScarcitySnapshot{Epoch: state.Epoch}, - State: state, - Consumed: append([]types.ObjectID(nil), consumed...), - Created: append([]object.Object(nil), created...), + State: state, + Consumed: append([]types.ObjectID(nil), consumed...), + Created: append([]object.Object(nil), created...), } validator := engine.Clone() if validator == nil || validator.Accept(preview) != nil { diff --git a/internal/v2/node/commit_preflight_test.go b/internal/v2/node/commit_preflight_test.go index 47a62444..194e37ae 100644 --- a/internal/v2/node/commit_preflight_test.go +++ b/internal/v2/node/commit_preflight_test.go @@ -143,9 +143,9 @@ func manualCandidateForDeltas(t *testing.T, runtime *Runtime, deltas map[uint32] func systemObjectForShard(shard, index uint32, label string) object.Object { return object.Object{ - ID: types.ObjectIDForShard(types.HashBytes("preflight-object", []byte(label)), index, shard), + ID: types.ObjectIDForShard(types.HashBytes("preflight-object", []byte(label)), index, shard), Version: 1, - Kind: object.KindSystem, - Data: []byte(label), + Kind: object.KindSystem, + Data: []byte(label), } } diff --git a/internal/v2/node/global_commit_journal.go b/internal/v2/node/global_commit_journal.go index c040fcc9..29cc349e 100644 --- a/internal/v2/node/global_commit_journal.go +++ b/internal/v2/node/global_commit_journal.go @@ -8,19 +8,19 @@ import ( "path/filepath" "sort" - v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" "github.com/zephyr-chain/zephyr-chain/internal/v2/object" "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" ) const ( - globalCommitJournalVersion uint16 = 1 - globalCommitJournalMagic = "ZGC2" - globalCommitPreparing uint8 = 1 - globalCommitCommitted uint8 = 2 - maxGlobalCommitJournalBytes = 256 << 20 + globalCommitJournalVersion uint16 = 1 + globalCommitJournalMagic = "ZGC2" + globalCommitPreparing uint8 = 1 + globalCommitCommitted uint8 = 2 + maxGlobalCommitJournalBytes = 256 << 20 maxJournalObjects = 1_000_000 ) @@ -57,7 +57,7 @@ func (r *Runtime) buildGlobalCommitIntent(candidate Candidate, certificate v2con Status: globalCommitPreparing, Network: r.Network, NativeToken: r.NativeToken, ValidatorRoot: r.ValidatorRoot, ShardCount: r.ShardCount, PreHeight: r.Height, PreParentHash: r.ParentHash, Header: candidate.Header, Certificate: certificate, - Commitments: append([]sharding.Commitment(nil), candidate.Commitments...), + Commitments: append([]sharding.Commitment(nil), candidate.Commitments...), EconomicCheckpoint: append([]byte(nil), economicCheckpoint...), } shards := make([]int, 0, len(candidate.deltas)) @@ -75,7 +75,7 @@ func (r *Runtime) buildGlobalCommitIntent(candidate Candidate, certificate v2con intent.Deltas = append(intent.Deltas, journalShardDelta{ ShardID: shard, PreRoot: r.States[shard].Root(), PostRoot: commitment.StateRoot, Consumed: append([]types.ObjectID(nil), delta.Consumed...), - Created: cloneJournalObjects(delta.Created), + Created: cloneJournalObjects(delta.Created), }) } if err := intent.Validate(); err != nil { diff --git a/internal/v2/node/global_commit_recovery.go b/internal/v2/node/global_commit_recovery.go index 17a0c1d3..4aae648b 100644 --- a/internal/v2/node/global_commit_recovery.go +++ b/internal/v2/node/global_commit_recovery.go @@ -4,8 +4,8 @@ import ( "errors" "os" - v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" From 140d6bbb4f0e2dc6cdd30fe5c1b8da1bd0bc16df Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:23:33 +0200 Subject: [PATCH 270/274] Test global commit journal with shadow economics recovery --- .../v2/node/global_commit_economics_test.go | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 internal/v2/node/global_commit_economics_test.go diff --git a/internal/v2/node/global_commit_economics_test.go b/internal/v2/node/global_commit_economics_test.go new file mode 100644 index 00000000..9a4a118e --- /dev/null +++ b/internal/v2/node/global_commit_economics_test.go @@ -0,0 +1,80 @@ +package node + +import ( + "path/filepath" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" + "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" +) + +func TestGlobalCommitJournalRestoresShadowEconomicsAcrossEpochBoundary(t *testing.T) { + network := types.NetworkID(types.HashBytes("network", []byte("global-journal-economics"))) + native := types.TokenID(types.HashBytes("token", []byte("ZPH"))) + key, validators, validatorRoot := schedulerValidatorSet(t, network) + store := worldstate.NewMemory() + journalPath := filepath.Join(t.TempDir(), "global-commit.journal") + + runtime, engineConfig := newCheckpointEconomicsRuntime(t, network, native, validatorRoot, store) + if err := runtime.EnableGlobalCommitJournal(journalPath); err != nil { + t.Fatal(err) + } + commitEmptySchedulerBlock(t, runtime, key, validators, 1) + + restarted1, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: store}, 1) + if err != nil { + t.Fatal(err) + } + if err := restarted1.EnableGlobalCommitJournal(journalPath); err != nil { + t.Fatal(err) + } + if err := restarted1.RecoverGlobalCommitJournal(validators, nil, engineConfig); err != nil { + t.Fatal(err) + } + metrics, _, err := restarted1.EconomicEpochSnapshot() + if err != nil { + t.Fatal(err) + } + if len(metrics) != 1 || metrics[0].Epoch != 1 || metrics[0].ResourceCapacity != 100 { + t.Fatalf("journal did not restore mid-epoch economics: %#v", metrics) + } + + commitEmptySchedulerBlock(t, restarted1, key, validators, 2) + pendingBefore, ok := restarted1.PendingEconomicState() + if !ok || pendingBefore.Epoch != 1 || !pendingBefore.Shadow { + t.Fatalf("epoch boundary did not produce pending shadow state: %#v", pendingBefore) + } + if _, exists := store.GetObject(economics.MonetaryStateObjectID(network)); exists { + t.Fatal("pending economic state entered world state before next consensus candidate") + } + + restarted2, err := NewRuntime(network, native, validatorRoot, map[uint32]worldstate.Backend{0: store}, 1) + if err != nil { + t.Fatal(err) + } + if err := restarted2.EnableGlobalCommitJournal(journalPath); err != nil { + t.Fatal(err) + } + if err := restarted2.RecoverGlobalCommitJournal(validators, nil, engineConfig); err != nil { + t.Fatal(err) + } + pendingAfter, ok := restarted2.PendingEconomicState() + if !ok || pendingAfter != pendingBefore { + t.Fatalf("pending shadow state changed across global journal restart: %#v != %#v", pendingAfter, pendingBefore) + } + + candidate, err := restarted2.BuildCandidate(3, nil) + if err != nil { + t.Fatal(err) + } + commitSchedulerCandidate(t, restarted2, key, validators, candidate) + finalized, ok := restarted2.FinalizedEconomicState() + if !ok || finalized != pendingBefore { + t.Fatalf("restored pending economics did not finalize: %#v", finalized) + } + objectState, exists := store.GetObject(economics.MonetaryStateObjectID(network)) + if !exists || objectState.Version != 1 { + t.Fatal("consensus-finalized monetary object missing after recovered epoch") + } +} From c6958f51553e4d0cdf6088077bfb3eb1c7e1838b Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:26:00 +0200 Subject: [PATCH 271/274] Add one-shot recovery tranche formatter cleanup --- .github/workflows/v2-format-restore-once.yml | 112 +++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 .github/workflows/v2-format-restore-once.yml diff --git a/.github/workflows/v2-format-restore-once.yml b/.github/workflows/v2-format-restore-once.yml new file mode 100644 index 00000000..312753c9 --- /dev/null +++ b/.github/workflows/v2-format-restore-once.yml @@ -0,0 +1,112 @@ +name: V2 Recovery Format Cleanup + +on: + push: + branches: + - chatgpt/protocol-v2-foundation + paths: + - .github/workflows/v2-format-restore-once.yml + +permissions: + contents: write + +jobs: + cleanup: + if: github.repository == 'the-code-learner/Zephyr-Chain' && github.ref == 'refs/heads/chatgpt/protocol-v2-foundation' + runs-on: ubuntu-latest + steps: + - name: Checkout v2 branch + uses: actions/checkout@v6 + with: + ref: chatgpt/protocol-v2-foundation + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: false + - name: Format recovery and journal tranche + run: | + gofmt -w \ + internal/v2/compute/messages.go \ + internal/v2/compute/work.go \ + internal/v2/compute/work_test.go \ + internal/v2/provider/service.go \ + internal/v2/provider/service_test.go \ + internal/v2/tx/transaction.go \ + internal/v2/node/runtime.go \ + internal/v2/node/commit_preflight.go \ + internal/v2/node/commit_preflight_test.go \ + internal/v2/node/recovery_state.go \ + internal/v2/node/recovery_state_test.go \ + internal/v2/node/global_commit_journal.go \ + internal/v2/node/global_commit_journal_test.go \ + internal/v2/node/global_commit_recovery.go \ + internal/v2/node/global_commit_recovery_test.go \ + internal/v2/node/global_commit_economics_test.go \ + internal/v2/economics/finalized_collector_checkpoint.go \ + internal/v2/economics/finalized_collector_checkpoint_test.go \ + internal/v2/economics/shadow_epoch_engine_checkpoint.go \ + internal/v2/economics/shadow_epoch_engine_checkpoint_test.go \ + internal/v2/economics/epoch_parse.go \ + internal/v2/economics/shadow_epoch_pending_checkpoint.go \ + internal/v2/economics/shadow_epoch_pending_checkpoint_test.go + - name: Restore permanent formatter and remove this workflow + run: | + cat > .github/workflows/v2-format-write.yml <<'EOF' + name: V2 Exact Format + + on: + push: + branches: + - chatgpt/protocol-v2-foundation + paths: + - internal/v2/compute/messages.go + - internal/v2/provider/service.go + - internal/v2/provider/service_test.go + - internal/v2/tx/transaction.go + - .github/workflows/v2-format-write.yml + + permissions: + contents: write + + jobs: + format: + if: github.repository == 'the-code-learner/Zephyr-Chain' && github.ref == 'refs/heads/chatgpt/protocol-v2-foundation' + runs-on: ubuntu-latest + steps: + - name: Checkout v2 branch + uses: actions/checkout@v6 + with: + ref: chatgpt/protocol-v2-foundation + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: false + - name: Format exact files + run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go + - name: Commit formatting if needed + run: | + if git diff --quiet; then + echo 'Formatting already exact.' + exit 0 + fi + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go + git commit -m 'gofmt Zephyr v2 compute and provider files' + git push origin HEAD:chatgpt/protocol-v2-foundation + EOF + git rm .github/workflows/v2-format-restore-once.yml + - name: Commit cleanup + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add -A + if git diff --cached --quiet; then + exit 0 + fi + git commit -m 'gofmt v2 recovery tranche and restore formatter' + git push origin HEAD:chatgpt/protocol-v2-foundation From 539a1346e59c0527cec3b018d0e9d0c23edb1b8f Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:20:09 +0200 Subject: [PATCH 272/274] Repair v2 global commit recovery path --- .github/workflows/v2-format-restore-once.yml | 112 ------------------ internal/v2/consensus/certificate_binary.go | 7 ++ internal/v2/node/economic_checkpoint.go | 6 +- internal/v2/node/economic_checkpoint_test.go | 6 +- .../v2/node/global_commit_recovery_test.go | 1 + internal/v2/node/recovery_state_test.go | 1 + internal/v2/node/runtime.go | 103 +++++++++++----- internal/v2/sharding/global_header_binary.go | 12 ++ 8 files changed, 98 insertions(+), 150 deletions(-) delete mode 100644 .github/workflows/v2-format-restore-once.yml create mode 100644 internal/v2/consensus/certificate_binary.go create mode 100644 internal/v2/sharding/global_header_binary.go diff --git a/.github/workflows/v2-format-restore-once.yml b/.github/workflows/v2-format-restore-once.yml deleted file mode 100644 index 312753c9..00000000 --- a/.github/workflows/v2-format-restore-once.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: V2 Recovery Format Cleanup - -on: - push: - branches: - - chatgpt/protocol-v2-foundation - paths: - - .github/workflows/v2-format-restore-once.yml - -permissions: - contents: write - -jobs: - cleanup: - if: github.repository == 'the-code-learner/Zephyr-Chain' && github.ref == 'refs/heads/chatgpt/protocol-v2-foundation' - runs-on: ubuntu-latest - steps: - - name: Checkout v2 branch - uses: actions/checkout@v6 - with: - ref: chatgpt/protocol-v2-foundation - fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v7 - with: - go-version-file: go.mod - cache: false - - name: Format recovery and journal tranche - run: | - gofmt -w \ - internal/v2/compute/messages.go \ - internal/v2/compute/work.go \ - internal/v2/compute/work_test.go \ - internal/v2/provider/service.go \ - internal/v2/provider/service_test.go \ - internal/v2/tx/transaction.go \ - internal/v2/node/runtime.go \ - internal/v2/node/commit_preflight.go \ - internal/v2/node/commit_preflight_test.go \ - internal/v2/node/recovery_state.go \ - internal/v2/node/recovery_state_test.go \ - internal/v2/node/global_commit_journal.go \ - internal/v2/node/global_commit_journal_test.go \ - internal/v2/node/global_commit_recovery.go \ - internal/v2/node/global_commit_recovery_test.go \ - internal/v2/node/global_commit_economics_test.go \ - internal/v2/economics/finalized_collector_checkpoint.go \ - internal/v2/economics/finalized_collector_checkpoint_test.go \ - internal/v2/economics/shadow_epoch_engine_checkpoint.go \ - internal/v2/economics/shadow_epoch_engine_checkpoint_test.go \ - internal/v2/economics/epoch_parse.go \ - internal/v2/economics/shadow_epoch_pending_checkpoint.go \ - internal/v2/economics/shadow_epoch_pending_checkpoint_test.go - - name: Restore permanent formatter and remove this workflow - run: | - cat > .github/workflows/v2-format-write.yml <<'EOF' - name: V2 Exact Format - - on: - push: - branches: - - chatgpt/protocol-v2-foundation - paths: - - internal/v2/compute/messages.go - - internal/v2/provider/service.go - - internal/v2/provider/service_test.go - - internal/v2/tx/transaction.go - - .github/workflows/v2-format-write.yml - - permissions: - contents: write - - jobs: - format: - if: github.repository == 'the-code-learner/Zephyr-Chain' && github.ref == 'refs/heads/chatgpt/protocol-v2-foundation' - runs-on: ubuntu-latest - steps: - - name: Checkout v2 branch - uses: actions/checkout@v6 - with: - ref: chatgpt/protocol-v2-foundation - fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v7 - with: - go-version-file: go.mod - cache: false - - name: Format exact files - run: gofmt -w internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go - - name: Commit formatting if needed - run: | - if git diff --quiet; then - echo 'Formatting already exact.' - exit 0 - fi - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add internal/v2/compute/messages.go internal/v2/provider/service.go internal/v2/provider/service_test.go internal/v2/tx/transaction.go - git commit -m 'gofmt Zephyr v2 compute and provider files' - git push origin HEAD:chatgpt/protocol-v2-foundation - EOF - git rm .github/workflows/v2-format-restore-once.yml - - name: Commit cleanup - run: | - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add -A - if git diff --cached --quiet; then - exit 0 - fi - git commit -m 'gofmt v2 recovery tranche and restore formatter' - git push origin HEAD:chatgpt/protocol-v2-foundation diff --git a/internal/v2/consensus/certificate_binary.go b/internal/v2/consensus/certificate_binary.go new file mode 100644 index 00000000..8f2294fd --- /dev/null +++ b/internal/v2/consensus/certificate_binary.go @@ -0,0 +1,7 @@ +package consensus + +// MarshalCertificate is the functional counterpart to ParseCertificate and +// preserves the existing Certificate.MarshalBinary canonical encoding. +func MarshalCertificate(c Certificate) ([]byte, error) { + return c.MarshalBinary() +} diff --git a/internal/v2/node/economic_checkpoint.go b/internal/v2/node/economic_checkpoint.go index 2d81671d..21b2d031 100644 --- a/internal/v2/node/economic_checkpoint.go +++ b/internal/v2/node/economic_checkpoint.go @@ -14,9 +14,9 @@ import ( ) const ( - economicCheckpointVersion uint16 = 1 - economicCheckpointMagic = "ZEC2" - maxEconomicCheckpointBytes = 64 << 20 + economicCheckpointVersion uint16 = 1 + economicCheckpointMagic = "ZEC2" + maxEconomicCheckpointBytes = 64 << 20 ) var ErrEconomicCheckpoint = errors.New("invalid Zephyr v2 economic runtime checkpoint") diff --git a/internal/v2/node/economic_checkpoint_test.go b/internal/v2/node/economic_checkpoint_test.go index 13afc13b..22179c58 100644 --- a/internal/v2/node/economic_checkpoint_test.go +++ b/internal/v2/node/economic_checkpoint_test.go @@ -112,8 +112,8 @@ func newCheckpointEconomicsRuntime(t *testing.T, network types.NetworkID, native Epoch: 1, ShardCount: 1, NativeToken: native, InitialCirculatingSupply: map[uint32]uint64{0: 1_000_000}, ResourceCapacityPerBlock: map[uint32]uint64{0: 100}, - VelocityPolicy: economics.VelocityPolicy{MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000}, - FeePolicy: economics.CompatibilityFeePolicy(), + VelocityPolicy: economics.VelocityPolicy{MinAgeBlocks: 1, FullWeightAgeBlocks: 10, MaxVelocityBps: 10_000}, + FeePolicy: economics.CompatibilityFeePolicy(), }) if err != nil { t.Fatal(err) @@ -125,7 +125,7 @@ func newCheckpointEconomicsRuntime(t *testing.T, network types.NetworkID, native index.WeightsBps[compute.WorkCPUGeneral] = 10_000 engineConfig := economics.ShadowEpochEngineConfig{ ComputeIndex: index, ComputeScarcity: economics.DefaultComputeScarcityConfig(), - Monetary: economics.DefaultShadowPolicy(), + Monetary: economics.DefaultShadowPolicy(), ComputeFeedback: economics.DefaultComputeFeedbackPolicy(economics.ComputeFeedbackObserveOnly), } engine, err := economics.NewShadowEpochEngine(network, engineConfig) diff --git a/internal/v2/node/global_commit_recovery_test.go b/internal/v2/node/global_commit_recovery_test.go index 1f2b8cf6..0a5f4242 100644 --- a/internal/v2/node/global_commit_recovery_test.go +++ b/internal/v2/node/global_commit_recovery_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "testing" + "github.com/zephyr-chain/zephyr-chain/internal/v2/economics" "github.com/zephyr-chain/zephyr-chain/internal/v2/object" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" diff --git a/internal/v2/node/recovery_state_test.go b/internal/v2/node/recovery_state_test.go index 62468513..728e9d8d 100644 --- a/internal/v2/node/recovery_state_test.go +++ b/internal/v2/node/recovery_state_test.go @@ -4,6 +4,7 @@ import ( "errors" "testing" + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" "github.com/zephyr-chain/zephyr-chain/internal/v2/object" "github.com/zephyr-chain/zephyr-chain/internal/v2/types" "github.com/zephyr-chain/zephyr-chain/internal/v2/worldstate" diff --git a/internal/v2/node/runtime.go b/internal/v2/node/runtime.go index 6c662085..12c81440 100644 --- a/internal/v2/node/runtime.go +++ b/internal/v2/node/runtime.go @@ -16,12 +16,13 @@ import ( ) var ( - ErrRuntimeConfig = errors.New("invalid v2 runtime configuration") - ErrCandidateHeight = errors.New("invalid v2 candidate height") - ErrCandidateState = errors.New("v2 candidate does not match committed state") - ErrCandidateCert = errors.New("v2 candidate certificate mismatch") - ErrStateSimulation = errors.New("v2 backend does not support state simulation") - ErrReceiptImport = errors.New("invalid v2 receipt import") + ErrRuntimeConfig = errors.New("invalid v2 runtime configuration") + ErrCandidateHeight = errors.New("invalid v2 candidate height") + ErrCandidateState = errors.New("v2 candidate does not match committed state") + ErrCandidateCert = errors.New("v2 candidate certificate mismatch") + ErrStateSimulation = errors.New("v2 backend does not support state simulation") + ErrReceiptImport = errors.New("invalid v2 receipt import") + ErrRuntimeRecoveryRequired = errors.New("v2 runtime recovery required") ) type ReceiptImport struct { @@ -56,20 +57,22 @@ type Candidate struct { } type Runtime struct { - mu sync.Mutex - Network types.NetworkID - NativeToken types.TokenID - ValidatorRoot types.Hash - ShardCount uint32 - States map[uint32]worldstate.Backend - Workers int - Height uint64 - ParentHash types.Hash - economicCollector *economics.EpochCollector - economicEngine *economics.ShadowEpochEngine - economicEpochLength uint64 - economicBalances economics.MonetaryBalanceSnapshot - pendingEconomic *economics.ShadowEpochPreview + mu sync.Mutex + Network types.NetworkID + NativeToken types.TokenID + ValidatorRoot types.Hash + ShardCount uint32 + States map[uint32]worldstate.Backend + Workers int + Height uint64 + ParentHash types.Hash + economicCollector *economics.EpochCollector + economicEngine *economics.ShadowEpochEngine + economicEpochLength uint64 + economicBalances economics.MonetaryBalanceSnapshot + pendingEconomic *economics.ShadowEpochPreview + globalCommitJournalPath string + recoveryRequired bool } func NewRuntime(network types.NetworkID, nativeToken types.TokenID, validatorRoot types.Hash, states map[uint32]worldstate.Backend, workers int) (*Runtime, error) { @@ -88,6 +91,9 @@ func NewRuntime(network types.NetworkID, nativeToken types.TokenID, validatorRoo func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) (Candidate, error) { r.mu.Lock() defer r.mu.Unlock() + if r.recoveryRequired { + return Candidate{}, ErrRuntimeRecoveryRequired + } if height != r.Height+1 || height == 0 { return Candidate{}, ErrCandidateHeight } @@ -250,6 +256,9 @@ func (r *Runtime) validateReceiptImport(destinationShard uint32, receiptImport R func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificate, validators v2consensus.ValidatorSet) (sharding.GlobalHeader, error) { r.mu.Lock() defer r.mu.Unlock() + if r.recoveryRequired { + return sharding.GlobalHeader{}, ErrRuntimeRecoveryRequired + } if candidate.Header.Height != r.Height+1 || candidate.Header.ParentHash != r.ParentHash || candidate.Header.Network != r.Network { return sharding.GlobalHeader{}, ErrCandidateState } @@ -330,15 +339,52 @@ func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificat nextPending = &preview } + postPending := r.pendingEconomic + if nextPending != nil { + postPending = nextPending + } else if pendingApplied { + postPending = nil + } + finalized := candidate.Header + finalized.CertificateHash = certificate.Hash() + postParent := v2consensus.HeaderConsensusHash(finalized) + + var journalIntent *globalCommitIntent + if r.globalCommitJournalPath != "" { + economicCheckpoint, err := r.postCommitEconomicCheckpoint(economicPreview, enginePreview, postPending, nextBalances, finalized.Height, postParent) + if err != nil { + return sharding.GlobalHeader{}, err + } + intent, err := r.buildGlobalCommitIntent(candidate, certificate, commitments, economicCheckpoint) + if err != nil { + return sharding.GlobalHeader{}, err + } + if err := writeGlobalCommitJournal(r.globalCommitJournalPath, intent); err != nil { + r.recoveryRequired = true + return sharding.GlobalHeader{}, errors.Join(ErrRuntimeRecoveryRequired, err) + } + journalIntent = &intent + } + for _, shardValue := range shards { shard := uint32(shardValue) delta := candidate.deltas[shard] root, err := r.States[shard].Apply(delta.Consumed, delta.Created) if err != nil { - return sharding.GlobalHeader{}, err + r.recoveryRequired = true + return sharding.GlobalHeader{}, errors.Join(ErrRuntimeRecoveryRequired, err) } if root != commitments[shard].StateRoot { - return sharding.GlobalHeader{}, ErrCandidateState + r.recoveryRequired = true + return sharding.GlobalHeader{}, errors.Join(ErrRuntimeRecoveryRequired, ErrCandidateState) + } + } + + if journalIntent != nil { + journalIntent.Status = globalCommitCommitted + if err := writeGlobalCommitJournal(r.globalCommitJournalPath, *journalIntent); err != nil { + r.recoveryRequired = true + return sharding.GlobalHeader{}, errors.Join(ErrRuntimeRecoveryRequired, err) } } @@ -348,16 +394,9 @@ func (r *Runtime) Commit(candidate Candidate, certificate v2consensus.Certificat if enginePreview != nil { r.economicEngine = enginePreview } - if nextPending != nil { - r.pendingEconomic = nextPending - r.economicBalances = nextBalances - } else if pendingApplied { - r.pendingEconomic = nil - } - - finalized := candidate.Header - finalized.CertificateHash = certificate.Hash() + r.pendingEconomic = postPending + r.economicBalances = nextBalances r.Height = finalized.Height - r.ParentHash = v2consensus.HeaderConsensusHash(finalized) + r.ParentHash = postParent return finalized, nil } diff --git a/internal/v2/sharding/global_header_binary.go b/internal/v2/sharding/global_header_binary.go new file mode 100644 index 00000000..05ea90ec --- /dev/null +++ b/internal/v2/sharding/global_header_binary.go @@ -0,0 +1,12 @@ +package sharding + +// MarshalBinary returns the canonical wire representation accepted by +// ParseGlobalHeader. Validation is performed before returning bytes so durable +// journal records cannot encode a header that the recovery path would reject. +func (h GlobalHeader) MarshalBinary() ([]byte, error) { + raw := h.CanonicalBytes() + if _, err := ParseGlobalHeader(raw); err != nil { + return nil, err + } + return raw, nil +} From 059470bbea80431467e9bce2bbc9d718c0221616 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:24:04 +0200 Subject: [PATCH 273/274] Restore v2 node test helpers --- internal/v2/node/commit_preflight_test.go | 1 + internal/v2/node/scheduler_helpers_test.go | 33 ++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 internal/v2/node/scheduler_helpers_test.go diff --git a/internal/v2/node/commit_preflight_test.go b/internal/v2/node/commit_preflight_test.go index 194e37ae..98d95969 100644 --- a/internal/v2/node/commit_preflight_test.go +++ b/internal/v2/node/commit_preflight_test.go @@ -3,6 +3,7 @@ package node import ( "testing" + "github.com/zephyr-chain/zephyr-chain/internal/v2/execution" "github.com/zephyr-chain/zephyr-chain/internal/v2/merkle" "github.com/zephyr-chain/zephyr-chain/internal/v2/object" "github.com/zephyr-chain/zephyr-chain/internal/v2/sharding" diff --git a/internal/v2/node/scheduler_helpers_test.go b/internal/v2/node/scheduler_helpers_test.go new file mode 100644 index 00000000..b04e8b23 --- /dev/null +++ b/internal/v2/node/scheduler_helpers_test.go @@ -0,0 +1,33 @@ +package node + +import ( + "crypto/ecdsa" + "testing" + + v2consensus "github.com/zephyr-chain/zephyr-chain/internal/v2/consensus" +) + +func schedulerCertificate(t *testing.T, runtime *Runtime, key *ecdsa.PrivateKey, validators v2consensus.ValidatorSet, candidate Candidate) v2consensus.Certificate { + t.Helper() + proposal, err := v2consensus.SignProposal(key, candidate.Header, 0) + if err != nil { + t.Fatal(err) + } + vote, err := v2consensus.SignVote(key, runtime.Network, candidate.Header.Height, 0, v2consensus.HeaderConsensusHash(candidate.Header)) + if err != nil { + t.Fatal(err) + } + certificate, err := validators.BuildCertificate(proposal, []v2consensus.Vote{vote}) + if err != nil { + t.Fatal(err) + } + return certificate +} + +func commitSchedulerCandidateExpectError(t *testing.T, runtime *Runtime, key *ecdsa.PrivateKey, validators v2consensus.ValidatorSet, candidate Candidate) { + t.Helper() + certificate := schedulerCertificate(t, runtime, key, validators, candidate) + if _, err := runtime.Commit(candidate, certificate, validators); err == nil { + t.Fatal("candidate commit unexpectedly succeeded") + } +} From 500aa50c9c9a3ac0093d88345f63545f01730902 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:26:38 +0200 Subject: [PATCH 274/274] Allow certified journal restart anchoring --- internal/v2/node/global_commit_recovery.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/v2/node/global_commit_recovery.go b/internal/v2/node/global_commit_recovery.go index 4aae648b..6b7e6b83 100644 --- a/internal/v2/node/global_commit_recovery.go +++ b/internal/v2/node/global_commit_recovery.go @@ -68,7 +68,13 @@ func (r *Runtime) RecoverGlobalCommitJournal(validators v2consensus.ValidatorSet postParent := v2consensus.HeaderConsensusHash(intent.Header) atPreAnchor := r.Height == intent.PreHeight && r.ParentHash == intent.PreParentHash atPostAnchor := r.Height == intent.Header.Height && r.ParentHash == postParent - if !atPreAnchor && !atPostAnchor { + unanchoredRestart := r.Height == 0 && types.IsZero32([32]byte(r.ParentHash)) + // A freshly constructed runtime has no durable consensus anchor of its own. + // In that case the certified journal is allowed to restore the anchor, but + // only after the shard loop below proves every state root is exactly at the + // journal's pre/post boundary. Any other non-matching runtime anchor fails + // closed so recovery cannot silently move an already anchored runtime. + if !atPreAnchor && !atPostAnchor && !unanchoredRestart { return ErrGlobalCommitJournal }