From 1a264518afa4b6d9442b0c7afec44a0183aa9096 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:05:01 +0200 Subject: [PATCH 01/37] Add dynamic ZCU shadow reference --- internal/v2/economics/zcu_reference.go | 164 +++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 internal/v2/economics/zcu_reference.go diff --git a/internal/v2/economics/zcu_reference.go b/internal/v2/economics/zcu_reference.go new file mode 100644 index 00000000..63166b7e --- /dev/null +++ b/internal/v2/economics/zcu_reference.go @@ -0,0 +1,164 @@ +package economics + +import ( + "errors" + "math/big" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" +) + +const ZCUReferenceScaleQ9 uint64 = 1_000_000_000 + +var ErrZCUReference = errors.New("invalid Zephyr compute unit reference input") + +// VerifiedComputeSlot is an economically weighted observation of concurrently +// usable compute capacity. PerformanceQ9 must come from an authenticated +// benchmark result; provider-declared peak performance is intentionally absent. +type VerifiedComputeSlot struct { + Class compute.WorkClass + PerformanceQ9 uint64 + DeliveredSlotTime uint64 + AvailabilityEWMABps uint32 + SuccessEWMABps uint32 + ConfidenceBps uint32 +} + +type ZCUReferenceConfig struct { + MinSlotsPerClass uint32 + EWMABps uint32 + MaxEpochChangeBps uint32 +} + +type ZCUReferenceSnapshot struct { + Epoch uint64 + ClassReferenceQ9 [compute.WorkClassCount]uint64 + ClassSlots [compute.WorkClassCount]uint32 + ClassWeight [compute.WorkClassCount]uint64 + ClassReliable [compute.WorkClassCount]bool +} + +type weightedSlot struct { + performance uint64 + weight uint64 +} + +func BuildZCUReference(epoch uint64, slots []VerifiedComputeSlot, prior ZCUReferenceSnapshot, cfg ZCUReferenceConfig) (ZCUReferenceSnapshot, error) { + if epoch == 0 || cfg.MinSlotsPerClass == 0 || cfg.EWMABps > BasisPoints || cfg.MaxEpochChangeBps > BasisPoints { + return ZCUReferenceSnapshot{}, ErrZCUReference + } + if prior.Epoch != 0 && prior.Epoch >= epoch { + return ZCUReferenceSnapshot{}, ErrZCUReference + } + + byClass := make([][]weightedSlot, int(compute.WorkClassCount)) + out := ZCUReferenceSnapshot{Epoch: epoch} + for _, slot := range slots { + if slot.Class <= compute.WorkUnknown || slot.Class >= compute.WorkClassCount || slot.PerformanceQ9 == 0 || slot.DeliveredSlotTime == 0 || + slot.AvailabilityEWMABps > BasisPoints || slot.SuccessEWMABps > BasisPoints || slot.ConfidenceBps > BasisPoints { + return ZCUReferenceSnapshot{}, ErrZCUReference + } + weight, err := effectiveSlotWeight(slot) + if err != nil { + return ZCUReferenceSnapshot{}, err + } + if weight == 0 { + continue + } + byClass[slot.Class] = append(byClass[slot.Class], weightedSlot{performance: slot.PerformanceQ9, weight: weight}) + } + + for class := compute.WorkClass(1); class < compute.WorkClassCount; class++ { + classSlots := byClass[class] + out.ClassSlots[class] = uint32(len(classSlots)) + if len(classSlots) == 0 { + continue + } + sort.Slice(classSlots, func(i, j int) bool { + if classSlots[i].performance == classSlots[j].performance { + return classSlots[i].weight < classSlots[j].weight + } + return classSlots[i].performance < classSlots[j].performance + }) + median, totalWeight, err := weightedMedianSlot(classSlots) + if err != nil { + return ZCUReferenceSnapshot{}, err + } + out.ClassWeight[class] = totalWeight + if out.ClassSlots[class] < cfg.MinSlotsPerClass { + continue + } + reference, err := ewma(prior.ClassReferenceQ9[class], median, cfg.EWMABps) + if err != nil { + return ZCUReferenceSnapshot{}, err + } + reference, err = rateLimitQ9(prior.ClassReferenceQ9[class], reference, cfg.MaxEpochChangeBps) + if err != nil { + return ZCUReferenceSnapshot{}, err + } + out.ClassReferenceQ9[class] = reference + out.ClassReliable[class] = reference != 0 + } + return out, nil +} + +func effectiveSlotWeight(slot VerifiedComputeSlot) (uint64, error) { + value := new(big.Int).SetUint64(slot.DeliveredSlotTime) + for _, factor := range []uint32{slot.AvailabilityEWMABps, slot.SuccessEWMABps, slot.ConfidenceBps} { + value.Mul(value, new(big.Int).SetUint64(uint64(factor))) + value.Quo(value, new(big.Int).SetUint64(uint64(BasisPoints))) + } + if !value.IsUint64() { + return 0, ErrZCUReference + } + return value.Uint64(), nil +} + +func weightedMedianSlot(slots []weightedSlot) (uint64, uint64, error) { + if len(slots) == 0 { + return 0, 0, ErrZCUReference + } + total := new(big.Int) + for _, slot := range slots { + if slot.performance == 0 || slot.weight == 0 { + return 0, 0, ErrZCUReference + } + total.Add(total, new(big.Int).SetUint64(slot.weight)) + } + if !total.IsUint64() || total.Sign() == 0 { + return 0, 0, ErrZCUReference + } + totalWeight := total.Uint64() + threshold := totalWeight/2 + totalWeight%2 + var cumulative uint64 + for _, slot := range slots { + if ^uint64(0)-cumulative < slot.weight { + return 0, 0, ErrZCUReference + } + cumulative += slot.weight + if cumulative >= threshold { + return slot.performance, totalWeight, nil + } + } + return 0, 0, ErrZCUReference +} + +func rateLimitQ9(prior, next uint64, maxChangeBps uint32) (uint64, error) { + if prior == 0 || maxChangeBps == 0 || prior == next { + return next, nil + } + upper := new(big.Int).Mul(new(big.Int).SetUint64(prior), new(big.Int).SetUint64(uint64(BasisPoints+maxChangeBps))) + upper.Quo(upper, new(big.Int).SetUint64(uint64(BasisPoints))) + lower := new(big.Int).Mul(new(big.Int).SetUint64(prior), new(big.Int).SetUint64(uint64(BasisPoints-maxChangeBps))) + lower.Quo(lower, new(big.Int).SetUint64(uint64(BasisPoints))) + if !upper.IsUint64() || !lower.IsUint64() { + return 0, ErrZCUReference + } + if next > upper.Uint64() { + return upper.Uint64(), nil + } + if next < lower.Uint64() { + return lower.Uint64(), nil + } + return next, nil +} From 99c53e8723321c1667fd88675a995375183eafb8 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:05:16 +0200 Subject: [PATCH 02/37] Test dynamic ZCU reference behavior --- internal/v2/economics/zcu_reference_test.go | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 internal/v2/economics/zcu_reference_test.go diff --git a/internal/v2/economics/zcu_reference_test.go b/internal/v2/economics/zcu_reference_test.go new file mode 100644 index 00000000..f49ca71c --- /dev/null +++ b/internal/v2/economics/zcu_reference_test.go @@ -0,0 +1,54 @@ +package economics + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" +) + +func TestBuildZCUReferenceUsesWeightedMedianAndRateLimit(t *testing.T) { + cfg := ZCUReferenceConfig{MinSlotsPerClass: 2, EWMABps: BasisPoints, MaxEpochChangeBps: 500} + prior := ZCUReferenceSnapshot{Epoch: 9} + prior.ClassReferenceQ9[compute.WorkGPUFP32] = ZCUReferenceScaleQ9 + + slots := []VerifiedComputeSlot{ + {Class: compute.WorkGPUFP32, PerformanceQ9: 900_000_000, DeliveredSlotTime: 10, AvailabilityEWMABps: 10_000, SuccessEWMABps: 10_000, ConfidenceBps: 10_000}, + {Class: compute.WorkGPUFP32, PerformanceQ9: 1_100_000_000, DeliveredSlotTime: 100, AvailabilityEWMABps: 10_000, SuccessEWMABps: 10_000, ConfidenceBps: 10_000}, + {Class: compute.WorkGPUFP32, PerformanceQ9: 5_000_000_000, DeliveredSlotTime: 1, AvailabilityEWMABps: 10_000, SuccessEWMABps: 10_000, ConfidenceBps: 1_000}, + } + + snapshot, err := BuildZCUReference(10, slots, prior, cfg) + if err != nil { + t.Fatal(err) + } + if !snapshot.ClassReliable[compute.WorkGPUFP32] { + t.Fatal("expected GPU reference to be reliable") + } + if got, want := snapshot.ClassReferenceQ9[compute.WorkGPUFP32], uint64(1_050_000_000); got != want { + t.Fatalf("reference = %d, want %d", got, want) + } +} + +func TestBuildZCUReferenceRequiresVerifiedCoverage(t *testing.T) { + cfg := ZCUReferenceConfig{MinSlotsPerClass: 2, EWMABps: 5_000, MaxEpochChangeBps: 100} + snapshot, err := BuildZCUReference(1, []VerifiedComputeSlot{{ + Class: compute.WorkCPUGeneral, PerformanceQ9: ZCUReferenceScaleQ9, DeliveredSlotTime: 10, + AvailabilityEWMABps: 10_000, SuccessEWMABps: 10_000, ConfidenceBps: 10_000, + }}, ZCUReferenceSnapshot{}, cfg) + if err != nil { + t.Fatal(err) + } + if snapshot.ClassReliable[compute.WorkCPUGeneral] || snapshot.ClassReferenceQ9[compute.WorkCPUGeneral] != 0 { + t.Fatal("single slot must not establish a reliable reference") + } +} + +func TestBuildZCUReferenceRejectsInvalidEvidence(t *testing.T) { + _, err := BuildZCUReference(1, []VerifiedComputeSlot{{ + Class: compute.WorkGPUFP64, PerformanceQ9: ZCUReferenceScaleQ9, DeliveredSlotTime: 1, + AvailabilityEWMABps: 10_001, SuccessEWMABps: 10_000, ConfidenceBps: 10_000, + }}, ZCUReferenceSnapshot{}, ZCUReferenceConfig{MinSlotsPerClass: 1, EWMABps: 10_000, MaxEpochChangeBps: 100}) + if err == nil { + t.Fatal("expected invalid availability evidence to fail") + } +} From 4545c0d75ea257ba8e1fcffd4893fe6f3d4adfba Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:05:34 +0200 Subject: [PATCH 03/37] Add ZPPI shadow basket and target --- internal/v2/economics/zppi.go | 136 ++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 internal/v2/economics/zppi.go diff --git a/internal/v2/economics/zppi.go b/internal/v2/economics/zppi.go new file mode 100644 index 00000000..4c569502 --- /dev/null +++ b/internal/v2/economics/zppi.go @@ -0,0 +1,136 @@ +package economics + +import ( + "errors" + "math/big" +) + +const ( + ZPPIPriceScaleQ9 uint64 = 1_000_000_000 + PurchasingPowerRetentionAnnualQ9 uint64 = 980_000_000 + TargetZPPIAnnualFactorQ9 uint64 = 1_020_408_163 +) + +var ErrZPPI = errors.New("invalid Zephyr purchasing power index input") + +type ZPPIComponent uint8 + +const ( + ZPPIUnknown ZPPIComponent = iota + ZPPICompute + ZPPIDataAvailability + ZPPIStorage + ZPPIComponentCount +) + +type ZPPIBasketConfig struct { + WeightsBps [ZPPIComponentCount]uint32 + ReferencePriceQ9 [ZPPIComponentCount]uint64 + MinCoverageBps uint32 + EWMABps uint32 +} + +type ZPPIObservation struct { + Component ZPPIComponent + PriceQ9 uint64 + Reliable bool +} + +type ZPPISnapshot struct { + Epoch uint64 + ComponentIndexQ9 [ZPPIComponentCount]uint64 + BasketIndexQ9 uint64 + CoverageBps uint32 + Reliable bool +} + +// BuildZPPI constructs a version-scoped, chain-weighted price-relative basket. +// Each component is normalized to the reference price fixed when the basket +// version activates, so heterogeneous services can be combined without treating +// their raw prices as directly interchangeable. +func BuildZPPI(epoch uint64, observations []ZPPIObservation, prior ZPPISnapshot, cfg ZPPIBasketConfig) (ZPPISnapshot, error) { + if epoch == 0 || cfg.MinCoverageBps > BasisPoints || cfg.EWMABps > BasisPoints || (prior.Epoch != 0 && prior.Epoch >= epoch) { + return ZPPISnapshot{}, ErrZPPI + } + var totalWeight uint64 + for component := ZPPIComponent(1); component < ZPPIComponentCount; component++ { + if cfg.WeightsBps[component] > BasisPoints { + return ZPPISnapshot{}, ErrZPPI + } + totalWeight += uint64(cfg.WeightsBps[component]) + if cfg.WeightsBps[component] > 0 && cfg.ReferencePriceQ9[component] == 0 { + return ZPPISnapshot{}, ErrZPPI + } + } + if totalWeight == 0 || totalWeight > uint64(BasisPoints) { + return ZPPISnapshot{}, ErrZPPI + } + + latest := make(map[ZPPIComponent]ZPPIObservation, len(observations)) + for _, observation := range observations { + if observation.Component <= ZPPIUnknown || observation.Component >= ZPPIComponentCount || observation.PriceQ9 == 0 { + return ZPPISnapshot{}, ErrZPPI + } + if _, duplicate := latest[observation.Component]; duplicate { + return ZPPISnapshot{}, ErrZPPI + } + latest[observation.Component] = observation + } + + out := ZPPISnapshot{Epoch: epoch} + basket := new(big.Int) + var activeWeight uint64 + for component := ZPPIComponent(1); component < ZPPIComponentCount; component++ { + weight := uint64(cfg.WeightsBps[component]) + if weight == 0 { + continue + } + observation, ok := latest[component] + if !ok || !observation.Reliable { + continue + } + index, err := scaledRatio(observation.PriceQ9, ZPPIPriceScaleQ9, cfg.ReferencePriceQ9[component]) + if err != nil { + return ZPPISnapshot{}, ErrZPPI + } + index, err = ewma(prior.ComponentIndexQ9[component], index, cfg.EWMABps) + if err != nil { + return ZPPISnapshot{}, ErrZPPI + } + out.ComponentIndexQ9[component] = index + activeWeight += weight + basket.Add(basket, new(big.Int).Mul(new(big.Int).SetUint64(index), new(big.Int).SetUint64(weight))) + } + if activeWeight == 0 { + return out, nil + } + basket.Quo(basket, new(big.Int).SetUint64(activeWeight)) + if !basket.IsUint64() { + return ZPPISnapshot{}, ErrZPPI + } + out.BasketIndexQ9 = basket.Uint64() + out.CoverageBps = uint32(activeWeight * uint64(BasisPoints) / totalWeight) + out.Reliable = out.CoverageBps >= cfg.MinCoverageBps + return out, nil +} + +func PurchasingPowerQ9(zppiQ9 uint64) (uint64, error) { + if zppiQ9 == 0 { + return 0, ErrZPPI + } + return scaledRatio(ZPPIPriceScaleQ9, ZPPIPriceScaleQ9, zppiQ9) +} + +// TargetZPPIFromPrior applies the canonical annual target corresponding to a +// 2% purchasing-power decline: 1 / 0.98 ~= 1.020408163. +func TargetZPPIFromPrior(priorZPPIQ9 uint64) (uint64, error) { + if priorZPPIQ9 == 0 { + return 0, ErrZPPI + } + value := new(big.Int).Mul(new(big.Int).SetUint64(priorZPPIQ9), new(big.Int).SetUint64(TargetZPPIAnnualFactorQ9)) + value.Quo(value, new(big.Int).SetUint64(ZPPIPriceScaleQ9)) + if !value.IsUint64() { + return 0, ErrZPPI + } + return value.Uint64(), nil +} From 1c154c91b756c2632dec4a2c760cafbe700b8f15 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:05:50 +0200 Subject: [PATCH 04/37] Test ZPPI basket and purchasing-power target --- internal/v2/economics/zppi_test.go | 61 ++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 internal/v2/economics/zppi_test.go diff --git a/internal/v2/economics/zppi_test.go b/internal/v2/economics/zppi_test.go new file mode 100644 index 00000000..53254ecd --- /dev/null +++ b/internal/v2/economics/zppi_test.go @@ -0,0 +1,61 @@ +package economics + +import "testing" + +func TestBuildZPPIChainWeightedBasket(t *testing.T) { + cfg := ZPPIBasketConfig{MinCoverageBps: 8_000, EWMABps: BasisPoints} + cfg.WeightsBps[ZPPICompute] = 5_000 + cfg.WeightsBps[ZPPIDataAvailability] = 3_000 + cfg.WeightsBps[ZPPIStorage] = 2_000 + cfg.ReferencePriceQ9[ZPPICompute] = 100 * ZPPIPriceScaleQ9 + cfg.ReferencePriceQ9[ZPPIDataAvailability] = 20 * ZPPIPriceScaleQ9 + cfg.ReferencePriceQ9[ZPPIStorage] = 10 * ZPPIPriceScaleQ9 + + snapshot, err := BuildZPPI(1, []ZPPIObservation{ + {Component: ZPPICompute, PriceQ9: 102 * ZPPIPriceScaleQ9, Reliable: true}, + {Component: ZPPIDataAvailability, PriceQ9: 20 * ZPPIPriceScaleQ9, Reliable: true}, + {Component: ZPPIStorage, PriceQ9: 10 * ZPPIPriceScaleQ9, Reliable: false}, + }, ZPPISnapshot{}, cfg) + if err != nil { + t.Fatal(err) + } + if !snapshot.Reliable || snapshot.CoverageBps != 8_000 { + t.Fatalf("coverage/reliability = %d/%v", snapshot.CoverageBps, snapshot.Reliable) + } + if got, want := snapshot.BasketIndexQ9, uint64(1_012_500_000); got != want { + t.Fatalf("basket index = %d, want %d", got, want) + } +} + +func TestPurchasingPowerTargetConversion(t *testing.T) { + target, err := TargetZPPIFromPrior(ZPPIPriceScaleQ9) + if err != nil { + t.Fatal(err) + } + if target != TargetZPPIAnnualFactorQ9 { + t.Fatalf("target = %d, want %d", target, TargetZPPIAnnualFactorQ9) + } + power, err := PurchasingPowerQ9(target) + if err != nil { + t.Fatal(err) + } + if power < 979_999_999 || power > 980_000_001 { + t.Fatalf("purchasing power = %d, want approximately 980000000", power) + } +} + +func TestBuildZPPIFailsClosedOnLowCoverage(t *testing.T) { + cfg := ZPPIBasketConfig{MinCoverageBps: 7_500, EWMABps: 5_000} + cfg.WeightsBps[ZPPICompute] = 5_000 + cfg.WeightsBps[ZPPIStorage] = 5_000 + cfg.ReferencePriceQ9[ZPPICompute] = ZPPIPriceScaleQ9 + cfg.ReferencePriceQ9[ZPPIStorage] = ZPPIPriceScaleQ9 + + snapshot, err := BuildZPPI(1, []ZPPIObservation{{Component: ZPPICompute, PriceQ9: ZPPIPriceScaleQ9, Reliable: true}}, ZPPISnapshot{}, cfg) + if err != nil { + t.Fatal(err) + } + if snapshot.Reliable || snapshot.CoverageBps != 5_000 { + t.Fatalf("low coverage must remain unreliable: %d/%v", snapshot.CoverageBps, snapshot.Reliable) + } +} From 4d38f35cb89c59be690c258db3fc71986371612a Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:06:17 +0200 Subject: [PATCH 05/37] Align tokenomics with purchasing-power target --- docs/tokenomics-v2.md | 504 +++++++++++------------------------------- 1 file changed, 126 insertions(+), 378 deletions(-) diff --git a/docs/tokenomics-v2.md b/docs/tokenomics-v2.md index 06fa72ab..7f4034f2 100644 --- a/docs/tokenomics-v2.md +++ b/docs/tokenomics-v2.md @@ -1,457 +1,205 @@ -# Zephyr v2 Tokenomics — Adaptive Monetary Policy +# Zephyr v2 Tokenomics — Protocol Economics and Purchasing-Power Target 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. +This document tracks the executable Protocol v2 economic foundation against the Zephyr Chain *Protocol Economics, Monetary Design, Governance, Lending and Consensus Incentives* working paper v0.1 dated 19 August 2026. -Detailed companion specifications: +The working paper supersedes the earlier assumption that the monetary objective is a fixed annual ZPH supply-growth rate. The canonical objective is now expressed in Zephyr-native purchasing power. Supply remains endogenous and any monetary actuator stays shadow-only until explicit activation gates pass. -- `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. +## 1. Canonical monetary objective -## 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 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 + reserve components -``` - -The reference fee engine can price: - -- transaction base work; -- signature verification; -- witness/proof bytes; -- state reads/writes; -- contract fuel; -- data-availability bytes; -- 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. - -### 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 implements this controller in **shadow mode only**. - -```text -supply -burn -stake ratio -protocol reserve ratio -blockchain 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 and can authenticate the decision but does not mutate live ZPH 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**: +The intended long-run property is: ```text -center: 2.00% -shadow minimum: 1.50% -shadow maximum: 2.50% -max change: 1 basis point / epoch +PP_t = 1 / ZPPI_t +PP_(t+1) = 0.98 * PP_t +ZPPI_(t+1) / ZPPI_t = 1 / 0.98 ~= 1.020408163 ``` -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 knowable from protocol state/accounting: - -- total ZPH supply; -- circulating supply; -- ZPH burned by the fee mechanism; -- protocol-minted ZPH after future activation; -- protocol reserve. - -### Security - -Directly known: - -- staked/bonded ZPH; -- validator voting power; -- collateral/slashing state. - -### Network utilization - -Do not use raw HTTP requests or mempool ingress. Use finalized consensus resource consumption. - -Blockchain resource utilization and compute-market utilization are separate signals and must not be conflated. - -### Finalized operations - -Operation counts are secondary signals only. They are not sufficient alone because an attacker can generate economically meaningless activity. +Therefore an exact annual **-2.00% purchasing-power target** corresponds to approximately **+2.0408% annual ZPPI inflation**. -### Age-weighted monetary velocity - -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: +The protocol stores the canonical Q9 annual factor: ```text -age = spendHeight - CreatedHeight +TargetZPPIAnnualFactorQ9 = 1_020_408_163 ``` -with configurable minimum age, full-weight age and maximum velocity bounds. - -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 - -There is no honest universal scalar that makes every CPU, GPU, AI training job, renderer and scientific workload directly equivalent. +This is not a fiat CPI target and does not require an exchange-price oracle. ZPPI is the price, in ZPH, of a versioned basket of competitively priced Zephyr-native services. -Zephyr therefore uses a **resource vector**, not a fake universal FLOP count. +## 2. Separation of control loops -The current model includes: +Zephyr keeps independent control signals so one metric cannot dominate monetary policy, resource pricing and consensus simultaneously. ```text -CPUUnits -GPUFP32Units -GPUFP64Units -TensorUnits -MemoryByteSeconds -VRAMByteSeconds -StorageBytes -NetworkBytes -``` - -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 hardware appear more valuable merely by self-reporting a larger number. - -## 6. Compute workload registry +finalized native markets + -> dynamic ZCU / service normalization + -> ZCPI + DA/storage observations + -> ZPPI + -> slow ZAMP monetary evaluation -Only standardized work specifications are eligible for monetary telemetry. +compute demand/supply/backlog/utilization + -> ZCSI + -> primarily reward routing / capacity incentives -A registry entry binds: +coin age + productive coverage + concentration + -> IdlePressure / future idle-capital levy -```text -WorkloadHash - -> WorkClass - -> normalized Units - -> WorkVector - -> BenchmarkHash +stake + reliability + diversity + -> consensus SecurityWeight ``` -Conflicting definitions for the same workload hash are rejected. - -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 +The fast blockchain resource-fee loop remains separate from the slow monetary loop. -ZCPI is an internal Zephyr compute-market price index. It is **not** a CPI and is not a claim about real-world inflation. +## 3. Dynamic ZCU reference -It answers: +A fixed FLOP benchmark becomes obsolete as hardware and software improve. Protocol v2 therefore retains the multidimensional `WorkVector` accounting layer and adds a shadow dynamic reference for each compute work class. -> how many atomic ZPH units were actually paid for standardized, verified compute work on Zephyr? - -ZCPI excludes: - -- advertised provider prices; -- unfilled offers; -- self-reported theoretical FLOPS; -- failed/unverified jobs; -- arbitrary unregistered workload units. - -Eligible observations derive from: +A verified compute-slot observation contains benchmark-derived performance plus evidence-weighted availability history. Provider-declared peak performance is not an input. ```text -registered workload spec -+ finalized compute job -+ verification-satisfied result -+ actual on-chain provider payments -``` - -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. +EffectiveSlotWeight = + DeliveredSlotTime + * AvailabilityEWMA + * SuccessEWMA + * Confidence -Zephyr therefore separately computes **ZCSI**, a bounded scarcity score based on: - -```text -escrow-backed standardized demand -verified standardized supply -funded backlog -fulfilled work -compute utilization -reliable ZCPI price trend +ZCU_ref(class, epoch) = weighted_median(verified slot performance) ``` -Only real escrow-backed standardized work counts as demand. Provider-advertised capacity does not become verified supply merely because it is claimed. - -If ZCPI is unreliable, the price component is removed. If demand/supply coverage is too thin, ZCSI itself becomes unreliable. - -An unreliable ZCSI is prohibited from changing either compute reward routing or the monetary target in the shadow evaluator. - -## 9. Compute feedback experiments A/B/C - -The branch implements three **shadow-only** modes. +The reference is smoothed with EWMA and bounded by a maximum per-epoch change. Thin classes fail closed as unreliable rather than inventing a reference. -### A — observe only +Current implementation: -```text -ZCSI -> telemetry only -compute reward share -> unchanged -inflation target -> unchanged -``` +- `internal/v2/economics/zcu_reference.go` +- deterministic weighted median per work class; +- delivered-slot-time, availability, success and confidence weighting; +- Q9 fixed-point reference values; +- minimum verified-slot coverage gate; +- EWMA smoothing; +- per-epoch rate limit; +- no provider self-reported peak-performance field. -### B — reward routing +This is **shadow telemetry only**. It does not change live rewards or supply. -```text -ZCSI -> suggested compute reward share -inflation target -> unchanged -``` +## 4. ZCPI and ZCSI -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. - -### C — reward routing + narrow monetary band - -```text -ZCSI -> suggested compute reward share -ZCSI -> small bounded shadow inflation correction -``` +Existing Protocol v2 code already reconstructs verified paid compute observations from finalized compute state and builds: -The total-inflation sensitivity is deliberately much smaller than the reward-routing sensitivity. +- ZCPI: finalized paid ZPH per standardized verified compute unit, using per-class medians, EWMA, coverage and reliability; +- ZCSI: bounded compute scarcity from escrow-backed demand, verified supply, backlog, fulfillment, utilization and reliable price trend. -Mode C must demonstrate a material stability/capacity benefit over Mode B before it is considered for activation. +Unreliable price or capacity evidence fails closed. ZCSI is primarily a routing signal for compute incentives and must not become the monetary anchor. -The current active economic boundary remains equivalent to Mode A: **no compute signal changes live supply**. +## 5. ZPPI — Zephyr Purchasing Power Index -## 10. Native ZPH fee accounting +The initial basket emphasizes: -V2 execution requires native ZPH inputs to cover outputs plus the signed fee. +- compute; +- data availability; +- storage. -The reference fee engine now supports: +Gas/base fee should have low or zero basket weight because it is directly influenced by protocol policy and would introduce reflexive feedback. -```text -resource charge - | - +-- burn - +-- validator/reward pool - +-- protocol reserve -``` +The first executable reference uses a version-scoped **chain-weighted price-relative basket**. Each service is normalized to a reference price fixed for that basket version before aggregation, so heterogeneous raw service prices are not treated as equivalent units. -with integer basis-point splits and deterministic rounding that conserves every atomic unit. +Current implementation: -Until state-backed distribution is activated, the compatibility policy preserves the current effective 100% fee burn. +- `internal/v2/economics/zppi.go`; +- Q9 component price relatives; +- compute / data-availability / storage components; +- version-supplied reference prices and weights; +- reliability-aware coverage; +- EWMA smoothing; +- canonical exact purchasing-power/ZPPI target conversion; +- fail-closed behavior when reliable basket coverage is too low. -## 11. Smart-contract gas +A future benchmark/basket governance transition may use geometric aggregation after overlap/calibration. Basket changes must be versioned, delayed and measured across an overlap window. -Contract execution reports deterministic `FuelUsed` and enforces `FuelLimit`. +## 6. ZAMP migration boundary -The reference resource fee model can include: +The branch already contains a deterministic shadow ZAMP evaluator that was built around the earlier 2% annual net-supply-growth center. That controller remains useful as a simulation/actuator baseline, but the **2% supply-growth center is no longer the normative monetary target**. -```text -base transaction charge -+ signature work -+ witness bytes -+ state reads/writes -+ FuelUsed * FuelPrice -+ DA bytes -+ cross-shard receipts -``` +Until the ZPPI path is integrated end-to-end: -Final production prices and the active base-fee controller remain simulation/benchmark decisions. +- the legacy supply-growth evaluator remains shadow-only; +- it must not be activated for live minting; +- new ZCU/ZPPI snapshots are measurement foundations, not supply commands; +- the next controller revision must use ZPPI target error as its primary monetary direction signal; +- ZCSI should mainly affect reward routing rather than broad issuance; +- mint, burn, reserve and reward-routing changes remain deterministic consensus state transitions behind explicit activation gates. -## 12. Compute payment is not blockchain gas +## 7. Idle capital and productive coverage — next P1 tranche -Heavy compute has two independent prices: +The working design defines: ```text -provider payment -+ blockchain settlement fee +AgePressure(a) = 1 - exp(-k * a) +EffectiveIdle = AgePressure * (1 - ProductiveCoverage) +IdleLevyIntensity_i = Base * EffectiveIdle_i * CapitalPressure_i * GlobalIdlePressure ``` -A 100 ZPH AI job does not imply 100 ZPH of gas. Validators settle commitments/proofs/results; they do not replay the expensive workload. +P1 implementation should add shadow-only: -## 13. Native custom-token policy +- dormancy histograms; +- capital-lineage metadata for split objects; +- fragmentation/state carrying-cost simulation; +- productive-coverage accounting hooks; +- adversarial scenarios for 10/100/1,000/10,000-way wallet fragmentation. -Protocol-native custom assets now have explicit supply policies: +No balance levy should activate in this tranche. -```text -FIXED -CAPPED -MINTABLE -``` +## 8. Native lending — P2 -The v2 executor implements: +Native bilateral lending is the productive escape valve for idle capital. A loan becomes productive only after both sides accept terms and funding/collateral locks are atomically satisfied. -- 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. +The future market includes: -ZPH itself is excluded from user-authority mint/burn paths. Future ZPH issuance can occur only through an explicitly activated protocol monetary transition. +- lender offers and borrower requests; +- counteroffers; +- duration/rate/collateral constraints; +- objective borrower history; +- negative rates; +- anti-wash credit that accrues over time rather than entirely at origination. -Custom-token cross-shard transfer/mint is deliberately gated until a globally verifiable token-policy proof/registry is available. +Negative rates are intentionally valid when the cost of lending is lower than the expected idle-capital pressure plus risk cost. -## 14. Economic epoch state +## 9. Activation discipline -Zephyr must not create one global monetary object touched by every transaction; that would serialize execution. - -The current foundation therefore uses: +Economic mechanisms progress through: ```text -finalized execution - -> per-shard epoch metrics - -> deterministic epoch aggregate - -> shadow MonetaryEpochState +Observe -> Shadow -> adversarial simulation -> bounded Active ``` -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. +No controller should jump directly from a paper design to live monetary state. -The current transition builder returns a state delta for pre-QC simulation; it does not commit a store itself. +Minimum gates include: -## 15. Testing and replay +1. all monetary metrics derived reproducibly from finalized consensus state; +2. authenticated benchmark/availability state for verified compute supply; +3. versioned basket and benchmark-era governance with overlap and timelocks; +4. long-horizon manipulation tests for self-cycling, wash lending, fragmentation and concentration; +5. bounded parameter changes and deterministic emergency fallback; +6. Citizen Node decoding/verification of economic state and history; +7. explicit protocol-version/activation-height transitions for mint, reward, fee split or levy changes. -The branch includes deterministic tests for: +## 10. Implementation order -- 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. +The current working-paper roadmap is: -`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`. +- **P0** keep CI/gofmt and Protocol v2 correctness gates green; +- **P1** dynamic ZCU + verified-slot accounting in shadow; +- **P1** ZPPI basket + exact -2% purchasing-power target simulation; +- **P1** dormancy histogram + lineage + fragmentation simulator; +- **P2** bilateral native lending; +- **P2** DeviceCredential abstraction; +- **P2** ZECI concentration telemetry; +- **P3** ContributionScore/Citizen challenge network, VRF committees and governance timelocks; +- **P4** Ethereum collateral light-client adapter and Bitcoin collateral research; +- **P5** incremental economic activation after long-running public simulation. -## 16. Activation gates - -ZAMP remains shadow-only until all of the following are true: - -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. - -## 17. Current policy boundary - -The branch implements **measurement, authenticated shadow state and shadow decisions**, not live monetary issuance. - -The design principle remains: +The engineering rule remains: ```text -measure first -simulate second -activate last +measure first -> simulate second -> activate last ``` - -This lets Zephyr develop an adaptive oracle-free economy without turning monetary policy into an untested consensus experiment. From 87fe807f671ff170ae971e9155e73a547247dbfb Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:08:09 +0200 Subject: [PATCH 06/37] Add one-shot economics formatter --- .../protocol-economics-format-once.yml | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/protocol-economics-format-once.yml diff --git a/.github/workflows/protocol-economics-format-once.yml b/.github/workflows/protocol-economics-format-once.yml new file mode 100644 index 00000000..da846bb0 --- /dev/null +++ b/.github/workflows/protocol-economics-format-once.yml @@ -0,0 +1,49 @@ +name: Protocol Economics Format Once + +on: + push: + branches: + - protocol-economics-p1 + paths: + - internal/v2/economics/zcu_reference.go + - internal/v2/economics/zcu_reference_test.go + - internal/v2/economics/zppi.go + - internal/v2/economics/zppi_test.go + - .github/workflows/protocol-economics-format-once.yml + +permissions: + contents: write + +jobs: + format: + if: github.repository == 'the-code-learner/Zephyr-Chain' && github.ref == 'refs/heads/protocol-economics-p1' + runs-on: ubuntu-latest + steps: + - name: Checkout economics branch + uses: actions/checkout@v6 + with: + ref: protocol-economics-p1 + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: false + - name: Format economics tranche + run: | + gofmt -w \ + internal/v2/economics/zcu_reference.go \ + internal/v2/economics/zcu_reference_test.go \ + internal/v2/economics/zppi.go \ + internal/v2/economics/zppi_test.go + git rm .github/workflows/protocol-economics-format-once.yml + - name: Commit exact formatting + 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 protocol economics P1 tranche' + git push origin HEAD:protocol-economics-p1 From 0093b7838a5b5d58781f2b2538810a8307e6dc4c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:08:30 +0000 Subject: [PATCH 07/37] gofmt protocol economics P1 tranche --- .../protocol-economics-format-once.yml | 49 ------------------- internal/v2/economics/zppi.go | 2 +- 2 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 .github/workflows/protocol-economics-format-once.yml diff --git a/.github/workflows/protocol-economics-format-once.yml b/.github/workflows/protocol-economics-format-once.yml deleted file mode 100644 index da846bb0..00000000 --- a/.github/workflows/protocol-economics-format-once.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Protocol Economics Format Once - -on: - push: - branches: - - protocol-economics-p1 - paths: - - internal/v2/economics/zcu_reference.go - - internal/v2/economics/zcu_reference_test.go - - internal/v2/economics/zppi.go - - internal/v2/economics/zppi_test.go - - .github/workflows/protocol-economics-format-once.yml - -permissions: - contents: write - -jobs: - format: - if: github.repository == 'the-code-learner/Zephyr-Chain' && github.ref == 'refs/heads/protocol-economics-p1' - runs-on: ubuntu-latest - steps: - - name: Checkout economics branch - uses: actions/checkout@v6 - with: - ref: protocol-economics-p1 - fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v7 - with: - go-version-file: go.mod - cache: false - - name: Format economics tranche - run: | - gofmt -w \ - internal/v2/economics/zcu_reference.go \ - internal/v2/economics/zcu_reference_test.go \ - internal/v2/economics/zppi.go \ - internal/v2/economics/zppi_test.go - git rm .github/workflows/protocol-economics-format-once.yml - - name: Commit exact formatting - 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 protocol economics P1 tranche' - git push origin HEAD:protocol-economics-p1 diff --git a/internal/v2/economics/zppi.go b/internal/v2/economics/zppi.go index 4c569502..90b00cba 100644 --- a/internal/v2/economics/zppi.go +++ b/internal/v2/economics/zppi.go @@ -8,7 +8,7 @@ import ( const ( ZPPIPriceScaleQ9 uint64 = 1_000_000_000 PurchasingPowerRetentionAnnualQ9 uint64 = 980_000_000 - TargetZPPIAnnualFactorQ9 uint64 = 1_020_408_163 + TargetZPPIAnnualFactorQ9 uint64 = 1_020_408_163 ) var ErrZPPI = errors.New("invalid Zephyr purchasing power index input") From b8a1541a7fe4a1705f1f546e978127fcfb3359ff Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:11:09 +0200 Subject: [PATCH 08/37] Document protocol economics P1 status --- docs/protocol-economics-p1-status.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/protocol-economics-p1-status.md diff --git a/docs/protocol-economics-p1-status.md b/docs/protocol-economics-p1-status.md new file mode 100644 index 00000000..5667c872 --- /dev/null +++ b/docs/protocol-economics-p1-status.md @@ -0,0 +1,24 @@ +# Protocol Economics P1 Status + +Working paper reference: 19 August 2026, v0.1. + +This tranche is shadow-only and does not activate monetary supply changes. + +Implemented here: + +- dynamic per-work-class ZCU references from verified slot evidence; +- deterministic weighted median, EWMA smoothing and per-epoch rate limiting; +- ZPPI price-relative basket for compute, data availability and storage; +- reliability/coverage gates for thin economic data; +- canonical annual ZPPI factor `1_020_408_163`, derived from a 2% purchasing-power decline; +- tokenomics documentation migrated away from a fixed 2% supply-growth objective. + +The pre-existing supply-growth controller remains a legacy shadow simulator until ZPPI is integrated into the monetary epoch path. + +Next P1 tranche: + +- dormancy histogram; +- ProductiveCoverage hooks; +- capital-lineage metadata; +- fragmentation/state carrying-cost simulation; +- adversarial wallet-splitting scenarios. From ae3b5f7be5bd4a8a444c201051c66da4b4ed7bab Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:13:41 +0200 Subject: [PATCH 09/37] Add one-shot economics package check --- .../protocol-economics-check-once.yml | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/protocol-economics-check-once.yml diff --git a/.github/workflows/protocol-economics-check-once.yml b/.github/workflows/protocol-economics-check-once.yml new file mode 100644 index 00000000..be6476fc --- /dev/null +++ b/.github/workflows/protocol-economics-check-once.yml @@ -0,0 +1,44 @@ +name: Protocol Economics Package Check Once + +on: + push: + branches: + - protocol-economics-p1 + paths: + - internal/v2/economics/** + - .github/workflows/protocol-economics-check-once.yml + +permissions: + contents: write + +jobs: + check: + if: github.repository == 'the-code-learner/Zephyr-Chain' && github.ref == 'refs/heads/protocol-economics-p1' + runs-on: ubuntu-latest + steps: + - name: Checkout economics branch + uses: actions/checkout@v6 + with: + ref: protocol-economics-p1 + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: false + - name: Vet economics package + run: go vet ./internal/v2/economics + - name: Test economics package + run: go test ./internal/v2/economics -count=1 + - name: Remove one-shot workflow + run: git rm .github/workflows/protocol-economics-check-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 'Remove completed economics package check' + git push origin HEAD:protocol-economics-p1 From 3f0fdad91607ba8e74daa491affd232060e396e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:14:15 +0000 Subject: [PATCH 10/37] Remove completed economics package check --- .../protocol-economics-check-once.yml | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/protocol-economics-check-once.yml diff --git a/.github/workflows/protocol-economics-check-once.yml b/.github/workflows/protocol-economics-check-once.yml deleted file mode 100644 index be6476fc..00000000 --- a/.github/workflows/protocol-economics-check-once.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Protocol Economics Package Check Once - -on: - push: - branches: - - protocol-economics-p1 - paths: - - internal/v2/economics/** - - .github/workflows/protocol-economics-check-once.yml - -permissions: - contents: write - -jobs: - check: - if: github.repository == 'the-code-learner/Zephyr-Chain' && github.ref == 'refs/heads/protocol-economics-p1' - runs-on: ubuntu-latest - steps: - - name: Checkout economics branch - uses: actions/checkout@v6 - with: - ref: protocol-economics-p1 - fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v7 - with: - go-version-file: go.mod - cache: false - - name: Vet economics package - run: go vet ./internal/v2/economics - - name: Test economics package - run: go test ./internal/v2/economics -count=1 - - name: Remove one-shot workflow - run: git rm .github/workflows/protocol-economics-check-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 'Remove completed economics package check' - git push origin HEAD:protocol-economics-p1 From 2e65d11202211733b784418865b9de802f8c49b9 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:30:16 +0200 Subject: [PATCH 11/37] Add idle capital lineage simulation --- internal/v2/economics/idle_capital.go | 325 +++++++++++++++++++++ internal/v2/economics/idle_capital_test.go | 157 ++++++++++ 2 files changed, 482 insertions(+) create mode 100644 internal/v2/economics/idle_capital.go create mode 100644 internal/v2/economics/idle_capital_test.go diff --git a/internal/v2/economics/idle_capital.go b/internal/v2/economics/idle_capital.go new file mode 100644 index 00000000..eee54748 --- /dev/null +++ b/internal/v2/economics/idle_capital.go @@ -0,0 +1,325 @@ +package economics + +import ( + "bytes" + "errors" + "math" + "math/big" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +var ErrIdleCapital = errors.New("invalid Zephyr idle-capital shadow input") + +// CapitalLot is shadow-only lineage metadata. LineageID follows economic +// capital across object splits, while IdleSinceHeight records the last height +// at which that portion of capital was classified as productively deployed. +// Neither field changes ownership, spendability or live monetary state. +type CapitalLot struct { + LineageID types.Hash + Amount uint64 + IdleSinceHeight uint64 +} + +func (l CapitalLot) Validate() error { + if types.IsZero32([32]byte(l.LineageID)) || l.Amount == 0 || l.IdleSinceHeight == 0 { + return ErrIdleCapital + } + return nil +} + +type DormancyBucket struct { + MaxAgeBlocks uint64 + Amount uint64 +} + +type DormancyHistogram struct { + Height uint64 + Total uint64 + Buckets []DormancyBucket +} + +// BuildDormancyHistogram aggregates capital by lineage age rather than by +// wallet/object count. Splitting one object into many therefore cannot change +// the aggregate histogram when the underlying lots are preserved. +func BuildDormancyHistogram(lots []CapitalLot, height uint64, upperBounds []uint64) (DormancyHistogram, error) { + if height == 0 || len(lots) == 0 { + return DormancyHistogram{}, ErrIdleCapital + } + for i, bound := range upperBounds { + if bound == 0 || (i > 0 && bound <= upperBounds[i-1]) { + return DormancyHistogram{}, ErrIdleCapital + } + } + out := DormancyHistogram{Height: height, Buckets: make([]DormancyBucket, len(upperBounds)+1)} + for i, bound := range upperBounds { + out.Buckets[i].MaxAgeBlocks = bound + } + out.Buckets[len(out.Buckets)-1].MaxAgeBlocks = math.MaxUint64 + for _, lot := range lots { + if err := lot.Validate(); err != nil || lot.IdleSinceHeight > height { + return DormancyHistogram{}, ErrIdleCapital + } + age := height - lot.IdleSinceHeight + index := sort.Search(len(upperBounds), func(i int) bool { return age <= upperBounds[i] }) + if math.MaxUint64-out.Buckets[index].Amount < lot.Amount || math.MaxUint64-out.Total < lot.Amount { + return DormancyHistogram{}, ErrIdleCapital + } + out.Buckets[index].Amount += lot.Amount + out.Total += lot.Amount + } + return out, nil +} + +// SplitCapitalLineage deterministically allocates lineage lots to output +// amounts without changing aggregate amount or idle age. It is intentionally +// independent of wallet/account identity so address fragmentation gives no +// age reset. +func SplitCapitalLineage(lots []CapitalLot, outputAmounts []uint64) ([][]CapitalLot, error) { + if len(lots) == 0 || len(outputAmounts) == 0 { + return nil, ErrIdleCapital + } + canonical, err := compactCapitalLots(lots) + if err != nil { + return nil, err + } + totalInput := new(big.Int) + for _, lot := range canonical { + totalInput.Add(totalInput, new(big.Int).SetUint64(lot.Amount)) + } + totalOutput := new(big.Int) + for _, amount := range outputAmounts { + if amount == 0 { + return nil, ErrIdleCapital + } + totalOutput.Add(totalOutput, new(big.Int).SetUint64(amount)) + } + if totalInput.Cmp(totalOutput) != 0 || !totalInput.IsUint64() { + return nil, ErrIdleCapital + } + + out := make([][]CapitalLot, len(outputAmounts)) + lotIndex := 0 + remaining := canonical[0].Amount + for outputIndex, amount := range outputAmounts { + need := amount + for need > 0 { + if lotIndex >= len(canonical) { + return nil, ErrIdleCapital + } + take := remaining + if take > need { + take = need + } + lot := canonical[lotIndex] + lot.Amount = take + out[outputIndex] = append(out[outputIndex], lot) + need -= take + remaining -= take + if remaining == 0 { + lotIndex++ + if lotIndex < len(canonical) { + remaining = canonical[lotIndex].Amount + } + } + } + } + if lotIndex != len(canonical) { + return nil, ErrIdleCapital + } + return out, nil +} + +// MarkProductiveCoverage resets the idle clock for exactly the requested +// fraction of aggregate capital. Lots are compacted and processed oldest-first +// before the split, making rounding independent of wallet fragmentation. +func MarkProductiveCoverage(lots []CapitalLot, height uint64, coverageBps uint32) ([]CapitalLot, error) { + if height == 0 || coverageBps > 10_000 { + return nil, ErrIdleCapital + } + canonical, err := compactCapitalLots(lots) + if err != nil { + return nil, err + } + for _, lot := range canonical { + if lot.IdleSinceHeight > height { + return nil, ErrIdleCapital + } + } + sort.Slice(canonical, func(i, j int) bool { + if canonical[i].IdleSinceHeight != canonical[j].IdleSinceHeight { + return canonical[i].IdleSinceHeight < canonical[j].IdleSinceHeight + } + return bytes.Compare(canonical[i].LineageID[:], canonical[j].LineageID[:]) < 0 + }) + total := new(big.Int) + for _, lot := range canonical { + total.Add(total, new(big.Int).SetUint64(lot.Amount)) + } + target := new(big.Int).Mul(new(big.Int).Set(total), new(big.Int).SetUint64(uint64(coverageBps))) + target.Quo(target, big.NewInt(10_000)) + if !target.IsUint64() { + return nil, ErrIdleCapital + } + remaining := target.Uint64() + out := make([]CapitalLot, 0, len(canonical)+1) + for _, lot := range canonical { + if remaining == 0 { + out = append(out, lot) + continue + } + productive := lot.Amount + if productive > remaining { + productive = remaining + } + if productive < lot.Amount { + idle := lot + idle.Amount -= productive + out = append(out, idle) + } + productiveLot := lot + productiveLot.Amount = productive + productiveLot.IdleSinceHeight = height + out = append(out, productiveLot) + remaining -= productive + } + if remaining != 0 { + return nil, ErrIdleCapital + } + return compactCapitalLots(out) +} + +type ProductiveCoverageAccumulator struct { + observedCapital uint64 + weightedCoverageBps big.Int +} + +type ProductiveCoverageSnapshot struct { + ObservedCapital uint64 + ProductiveCoverageBps uint32 +} + +// Observe accepts a deterministic, consensus-derived coverage classification +// supplied by a concrete productive-use hook. It only aggregates the signal; +// it does not decide that arbitrary transfers are productive. +func (a *ProductiveCoverageAccumulator) Observe(amount uint64, coverageBps uint32) error { + if a == nil || amount == 0 || coverageBps > 10_000 || math.MaxUint64-a.observedCapital < amount { + return ErrIdleCapital + } + a.observedCapital += amount + term := new(big.Int).Mul(new(big.Int).SetUint64(amount), new(big.Int).SetUint64(uint64(coverageBps))) + a.weightedCoverageBps.Add(&a.weightedCoverageBps, term) + return nil +} + +func (a *ProductiveCoverageAccumulator) Snapshot() (ProductiveCoverageSnapshot, error) { + if a == nil || a.observedCapital == 0 { + return ProductiveCoverageSnapshot{}, ErrIdleCapital + } + value := new(big.Int).Quo(new(big.Int).Set(&a.weightedCoverageBps), new(big.Int).SetUint64(a.observedCapital)) + if !value.IsUint64() || value.Uint64() > 10_000 { + return ProductiveCoverageSnapshot{}, ErrIdleCapital + } + return ProductiveCoverageSnapshot{ObservedCapital: a.observedCapital, ProductiveCoverageBps: uint32(value.Uint64())}, nil +} + +type StateCarryingCostPolicy struct { + BaseUnitsPerObject uint64 + UnitsPerKiB uint64 +} + +func EstimateStateCarryingCost(objectCount, payloadBytes uint64, policy StateCarryingCostPolicy) (uint64, error) { + if objectCount == 0 || policy.BaseUnitsPerObject == 0 || policy.UnitsPerKiB == 0 { + return 0, ErrIdleCapital + } + kilobytes := payloadBytes / 1024 + if payloadBytes%1024 != 0 { + kilobytes++ + } + cost := new(big.Int).Mul(new(big.Int).SetUint64(objectCount), new(big.Int).SetUint64(policy.BaseUnitsPerObject)) + cost.Add(cost, new(big.Int).Mul(new(big.Int).SetUint64(kilobytes), new(big.Int).SetUint64(policy.UnitsPerKiB))) + if !cost.IsUint64() { + return 0, ErrIdleCapital + } + return cost.Uint64(), nil +} + +type FragmentationScenario struct { + Fragments uint32 + Histogram DormancyHistogram + CarryingCostUnits uint64 +} + +// SimulateWalletFragmentation is a shadow-only adversarial helper. It splits a +// single capital lot into N object-sized fragments and proves the economic age +// distribution remains invariant while state carrying cost increases. +func SimulateWalletFragmentation(seed CapitalLot, fragments uint32, height, payloadBytesPerObject uint64, bounds []uint64, policy StateCarryingCostPolicy) (FragmentationScenario, error) { + if err := seed.Validate(); err != nil || fragments == 0 || uint64(fragments) > seed.Amount { + return FragmentationScenario{}, ErrIdleCapital + } + amounts := make([]uint64, int(fragments)) + base := seed.Amount / uint64(fragments) + remainder := seed.Amount % uint64(fragments) + for i := range amounts { + amounts[i] = base + if uint64(i) < remainder { + amounts[i]++ + } + } + split, err := SplitCapitalLineage([]CapitalLot{seed}, amounts) + if err != nil { + return FragmentationScenario{}, err + } + flattened := make([]CapitalLot, 0, fragments) + for _, outputLots := range split { + flattened = append(flattened, outputLots...) + } + histogram, err := BuildDormancyHistogram(flattened, height, bounds) + if err != nil { + return FragmentationScenario{}, err + } + payloadTotal := new(big.Int).Mul(new(big.Int).SetUint64(uint64(fragments)), new(big.Int).SetUint64(payloadBytesPerObject)) + if !payloadTotal.IsUint64() { + return FragmentationScenario{}, ErrIdleCapital + } + cost, err := EstimateStateCarryingCost(uint64(fragments), payloadTotal.Uint64(), policy) + if err != nil { + return FragmentationScenario{}, err + } + return FragmentationScenario{Fragments: fragments, Histogram: histogram, CarryingCostUnits: cost}, nil +} + +func compactCapitalLots(lots []CapitalLot) ([]CapitalLot, error) { + if len(lots) == 0 { + return nil, ErrIdleCapital + } + out := append([]CapitalLot(nil), lots...) + for _, lot := range out { + if err := lot.Validate(); err != nil { + return nil, err + } + } + sort.Slice(out, func(i, j int) bool { + cmp := bytes.Compare(out[i].LineageID[:], out[j].LineageID[:]) + if cmp != 0 { + return cmp < 0 + } + return out[i].IdleSinceHeight < out[j].IdleSinceHeight + }) + compacted := make([]CapitalLot, 0, len(out)) + for _, lot := range out { + if len(compacted) > 0 { + last := &compacted[len(compacted)-1] + if last.LineageID == lot.LineageID && last.IdleSinceHeight == lot.IdleSinceHeight { + if math.MaxUint64-last.Amount < lot.Amount { + return nil, ErrIdleCapital + } + last.Amount += lot.Amount + continue + } + } + compacted = append(compacted, lot) + } + return compacted, nil +} diff --git a/internal/v2/economics/idle_capital_test.go b/internal/v2/economics/idle_capital_test.go new file mode 100644 index 00000000..34faf2c8 --- /dev/null +++ b/internal/v2/economics/idle_capital_test.go @@ -0,0 +1,157 @@ +package economics + +import ( + "reflect" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestWalletFragmentationPreservesDormancyHistogram(t *testing.T) { + seed := CapitalLot{ + LineageID: types.HashBytes("idle-lineage", []byte("seed")), + Amount: 1_000_000, + IdleSinceHeight: 10, + } + height := uint64(10_010) + bounds := []uint64{100, 1_000, 10_000, 100_000} + policy := StateCarryingCostPolicy{BaseUnitsPerObject: 8, UnitsPerKiB: 3} + + baseline, err := BuildDormancyHistogram([]CapitalLot{seed}, height, bounds) + if err != nil { + t.Fatal(err) + } + var previousCost uint64 + for _, fragments := range []uint32{10, 100, 1_000, 10_000} { + scenario, err := SimulateWalletFragmentation(seed, fragments, height, 96, bounds, policy) + if err != nil { + t.Fatalf("%d fragments: %v", fragments, err) + } + if !reflect.DeepEqual(scenario.Histogram, baseline) { + t.Fatalf("%d-way split changed dormancy histogram: %#v != %#v", fragments, scenario.Histogram, baseline) + } + if scenario.CarryingCostUnits <= previousCost { + t.Fatalf("%d-way split did not increase carrying cost: %d <= %d", fragments, scenario.CarryingCostUnits, previousCost) + } + previousCost = scenario.CarryingCostUnits + } +} + +func TestMarkProductiveCoverageIsFragmentationStable(t *testing.T) { + seed := CapitalLot{ + LineageID: types.HashBytes("idle-lineage", []byte("productive")), + Amount: 100_000, + IdleSinceHeight: 100, + } + height := uint64(1_000) + marked, err := MarkProductiveCoverage([]CapitalLot{seed}, height, 2_500) + if err != nil { + t.Fatal(err) + } + split, err := SplitCapitalLineage([]CapitalLot{seed}, equalAmounts(seed.Amount, 100)) + if err != nil { + t.Fatal(err) + } + var fragmented []CapitalLot + for _, lots := range split { + fragmented = append(fragmented, lots...) + } + markedFragmented, err := MarkProductiveCoverage(fragmented, height, 2_500) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(markedFragmented, marked) { + t.Fatalf("productive marking changed after wallet split: %#v != %#v", markedFragmented, marked) + } + if len(marked) != 2 { + t.Fatalf("marked lots = %d, want 2", len(marked)) + } + var reset, stillIdle uint64 + for _, lot := range marked { + switch lot.IdleSinceHeight { + case height: + reset += lot.Amount + case seed.IdleSinceHeight: + stillIdle += lot.Amount + default: + t.Fatalf("unexpected idle height %d", lot.IdleSinceHeight) + } + } + if reset != 25_000 || stillIdle != 75_000 { + t.Fatalf("productive split = reset %d idle %d", reset, stillIdle) + } +} + +func TestProductiveCoverageAccumulatorUsesCapitalWeight(t *testing.T) { + var acc ProductiveCoverageAccumulator + if err := acc.Observe(1_000, 2_500); err != nil { + t.Fatal(err) + } + if err := acc.Observe(3_000, 5_000); err != nil { + t.Fatal(err) + } + snapshot, err := acc.Snapshot() + if err != nil { + t.Fatal(err) + } + if snapshot.ObservedCapital != 4_000 || snapshot.ProductiveCoverageBps != 4_375 { + t.Fatalf("unexpected productive coverage snapshot: %#v", snapshot) + } +} + +func TestSplitCapitalLineagePreservesMultipleCohorts(t *testing.T) { + lineage := types.HashBytes("idle-lineage", []byte("cohorts")) + lots := []CapitalLot{ + {LineageID: lineage, Amount: 600, IdleSinceHeight: 10}, + {LineageID: lineage, Amount: 400, IdleSinceHeight: 100}, + } + split, err := SplitCapitalLineage(lots, []uint64{250, 250, 250, 250}) + if err != nil { + t.Fatal(err) + } + var flattened []CapitalLot + for _, output := range split { + flattened = append(flattened, output...) + } + before, err := BuildDormancyHistogram(lots, 1_000, []uint64{500, 950}) + if err != nil { + t.Fatal(err) + } + after, err := BuildDormancyHistogram(flattened, 1_000, []uint64{500, 950}) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(after, before) { + t.Fatalf("cohort split changed histogram: %#v != %#v", after, before) + } +} + +func TestIdleCapitalRejectsManipulativeInputs(t *testing.T) { + lineage := types.HashBytes("idle-lineage", []byte("invalid")) + seed := CapitalLot{LineageID: lineage, Amount: 100, IdleSinceHeight: 10} + if _, err := SplitCapitalLineage([]CapitalLot{seed}, []uint64{99}); err == nil { + t.Fatal("non-conserving lineage split accepted") + } + if _, err := MarkProductiveCoverage([]CapitalLot{seed}, 100, 10_001); err == nil { + t.Fatal("coverage above 100% accepted") + } + if _, err := BuildDormancyHistogram([]CapitalLot{seed}, 100, []uint64{100, 99}); err == nil { + t.Fatal("non-monotonic dormancy buckets accepted") + } + if _, err := SimulateWalletFragmentation(seed, 101, 100, 64, []uint64{10}, StateCarryingCostPolicy{BaseUnitsPerObject: 1, UnitsPerKiB: 1}); err == nil { + t.Fatal("fragment count above indivisible amount accepted") + } +} + +func equalAmounts(total uint64, count int) []uint64 { + out := make([]uint64, count) + base := total / uint64(count) + remainder := total % uint64(count) + for i := range out { + out[i] = base + if uint64(i) < remainder { + out[i]++ + } + } + return out +} From ecbd561b9dbf41db33ce53a477dc098b3b580194 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 03:40:10 +0200 Subject: [PATCH 12/37] Add prospective idle capital tracker --- internal/v2/economics/idle_tracker.go | 617 ++++++++++++++++++++++++++ 1 file changed, 617 insertions(+) create mode 100644 internal/v2/economics/idle_tracker.go diff --git a/internal/v2/economics/idle_tracker.go b/internal/v2/economics/idle_tracker.go new file mode 100644 index 00000000..fd6fa7ba --- /dev/null +++ b/internal/v2/economics/idle_tracker.go @@ -0,0 +1,617 @@ +package economics + +import ( + "bytes" + "math" + "math/big" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/codec" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +const ( + idleCapitalTrackerVersion uint16 = 1 + maxIdleTrackerObjects = 250_000 + maxIdleTrackerTransfers = 250_000 + maxIdleTrackerLots = 500_000 + maxIdleTrackerBigIntBytes = 128 +) + +// CapitalTarget identifies a deterministic destination for capital lineage. +// Exactly one of ObjectID or TransferID must be non-zero. TransferID is an +// opaque consensus-derived cross-shard receipt/transfer key supplied by the +// caller; the tracker does not invent receipt identity. +type CapitalTarget struct { + ObjectID types.ObjectID + TransferID types.Hash + Amount uint64 +} + +func (t CapitalTarget) Validate() error { + objectSet := !types.IsZero32([32]byte(t.ObjectID)) + transferSet := !types.IsZero32([32]byte(t.TransferID)) + if t.Amount == 0 || objectSet == transferSet { + return ErrIdleCapital + } + return nil +} + +type IdleCapitalSnapshot struct { + Height uint64 + TrackedCapital uint64 + BootstrapCapital uint64 + LiveObjects uint64 + PendingTransfers uint64 + Dormancy DormancyHistogram + ProductiveObservedCapital uint64 + ProductiveCoverageBps uint32 +} + +// IdleCapitalTracker is prospective shadow telemetry. It follows lineage after +// first observation without changing coin/object consensus encoding. A lineage +// first seen through BootstrapObject remains explicitly marked as bootstrap so +// callers can distinguish prospective coverage from complete historical +// lineage knowledge. +type IdleCapitalTracker struct { + objects map[types.ObjectID][]CapitalLot + pendingTransfers map[types.Hash][]CapitalLot + bootstrapLineages map[types.Hash]struct{} + productive ProductiveCoverageAccumulator +} + +func NewIdleCapitalTracker() *IdleCapitalTracker { + return &IdleCapitalTracker{ + objects: make(map[types.ObjectID][]CapitalLot), + pendingTransfers: make(map[types.Hash][]CapitalLot), + bootstrapLineages: make(map[types.Hash]struct{}), + } +} + +func (t *IdleCapitalTracker) Clone() *IdleCapitalTracker { + if t == nil { + return nil + } + out := NewIdleCapitalTracker() + for id, lots := range t.objects { + out.objects[id] = append([]CapitalLot(nil), lots...) + } + for id, lots := range t.pendingTransfers { + out.pendingTransfers[id] = append([]CapitalLot(nil), lots...) + } + for lineage := range t.bootstrapLineages { + out.bootstrapLineages[lineage] = struct{}{} + } + out.productive.observedCapital = t.productive.observedCapital + out.productive.weightedCoverageBps.Set(&t.productive.weightedCoverageBps) + return out +} + +// BootstrapObject begins prospective tracking for a native coin already known +// to consensus. CreatedHeight is trusted only because it comes from the +// consensus-stamped coin object. The resulting lineage is marked bootstrap: +// its history before this object existed is intentionally not claimed. +func (t *IdleCapitalTracker) BootstrapObject(id types.ObjectID, amount, createdHeight uint64) error { + if t == nil || types.IsZero32([32]byte(id)) || amount == 0 || createdHeight == 0 { + return ErrIdleCapital + } + if len(t.objects) >= maxIdleTrackerObjects { + return ErrIdleCapital + } + if _, exists := t.objects[id]; exists { + return ErrIdleCapital + } + lineage := types.HashBytes("idle-capital/bootstrap-lineage/v1", id[:]) + lot := CapitalLot{LineageID: lineage, Amount: amount, IdleSinceHeight: createdHeight} + if err := lot.Validate(); err != nil { + return err + } + t.objects[id] = []CapitalLot{lot} + t.bootstrapLineages[lineage] = struct{}{} + return nil +} + +// ApplyTransition atomically consumes tracked local objects and allocates their +// lineage to local object outputs and/or opaque cross-shard transfer targets. +// target declaration order is irrelevant: allocation is canonical by target +// kind and identifier. burned is removed from circulation and receives no new +// lineage. Ordinary transfers never reset IdleSinceHeight. +func (t *IdleCapitalTracker) ApplyTransition(inputs []types.ObjectID, targets []CapitalTarget, burned uint64) error { + if t == nil || len(inputs) == 0 || (len(targets) == 0 && burned == 0) { + return ErrIdleCapital + } + preview := t.Clone() + if preview == nil { + return ErrIdleCapital + } + if err := preview.applyTransition(inputs, targets, burned); err != nil { + return err + } + *t = *preview + return nil +} + +func (t *IdleCapitalTracker) applyTransition(inputs []types.ObjectID, targets []CapitalTarget, burned uint64) error { + seenInputs := make(map[types.ObjectID]struct{}, len(inputs)) + inputLots := make([]CapitalLot, 0, len(inputs)) + for _, id := range inputs { + if types.IsZero32([32]byte(id)) { + return ErrIdleCapital + } + if _, duplicate := seenInputs[id]; duplicate { + return ErrIdleCapital + } + seenInputs[id] = struct{}{} + lots, ok := t.objects[id] + if !ok || len(lots) == 0 { + return ErrIdleCapital + } + inputLots = append(inputLots, lots...) + } + canonicalLots, err := compactCapitalLots(inputLots) + if err != nil { + return err + } + + orderedTargets := append([]CapitalTarget(nil), targets...) + seenObjects := make(map[types.ObjectID]struct{}, len(targets)) + seenTransfers := make(map[types.Hash]struct{}, len(targets)) + for _, target := range orderedTargets { + if err := target.Validate(); err != nil { + return err + } + if !types.IsZero32([32]byte(target.ObjectID)) { + if _, duplicate := seenObjects[target.ObjectID]; duplicate { + return ErrIdleCapital + } + seenObjects[target.ObjectID] = struct{}{} + if _, exists := t.objects[target.ObjectID]; exists { + return ErrIdleCapital + } + } else { + if _, duplicate := seenTransfers[target.TransferID]; duplicate { + return ErrIdleCapital + } + seenTransfers[target.TransferID] = struct{}{} + if _, exists := t.pendingTransfers[target.TransferID]; exists { + return ErrIdleCapital + } + } + } + if len(t.objects)-len(inputs)+len(seenObjects) > maxIdleTrackerObjects || len(t.pendingTransfers)+len(seenTransfers) > maxIdleTrackerTransfers { + return ErrIdleCapital + } + sort.Slice(orderedTargets, func(i, j int) bool { + iObject := !types.IsZero32([32]byte(orderedTargets[i].ObjectID)) + jObject := !types.IsZero32([32]byte(orderedTargets[j].ObjectID)) + if iObject != jObject { + return iObject + } + if iObject { + return bytes.Compare(orderedTargets[i].ObjectID[:], orderedTargets[j].ObjectID[:]) < 0 + } + return bytes.Compare(orderedTargets[i].TransferID[:], orderedTargets[j].TransferID[:]) < 0 + }) + + amounts := make([]uint64, 0, len(orderedTargets)+1) + for _, target := range orderedTargets { + amounts = append(amounts, target.Amount) + } + if burned > 0 { + amounts = append(amounts, burned) + } + if len(amounts) == 0 { + return ErrIdleCapital + } + allocations, err := SplitCapitalLineage(canonicalLots, amounts) + if err != nil { + return err + } + for id := range seenInputs { + delete(t.objects, id) + } + for i, target := range orderedTargets { + lots := allocations[i] + if !types.IsZero32([32]byte(target.ObjectID)) { + t.objects[target.ObjectID] = lots + } else { + t.pendingTransfers[target.TransferID] = lots + } + } + t.pruneBootstrapLineages() + return nil +} + +// MaterializeTransfer turns one tracked pending cross-shard target into its +// imported local object without changing age or lineage. +func (t *IdleCapitalTracker) MaterializeTransfer(transferID types.Hash, objectID types.ObjectID) error { + if t == nil || types.IsZero32([32]byte(transferID)) || types.IsZero32([32]byte(objectID)) { + return ErrIdleCapital + } + if len(t.objects) >= maxIdleTrackerObjects { + return ErrIdleCapital + } + if _, exists := t.objects[objectID]; exists { + return ErrIdleCapital + } + lots, exists := t.pendingTransfers[transferID] + if !exists || len(lots) == 0 { + return ErrIdleCapital + } + delete(t.pendingTransfers, transferID) + t.objects[objectID] = append([]CapitalLot(nil), lots...) + return nil +} + +// MarkObjectProductive is an explicit productive-use hook. It is intentionally +// separate from ApplyTransition so self-transfers and generic contract calls +// cannot reset dormancy merely by moving capital. +func (t *IdleCapitalTracker) MarkObjectProductive(objectID types.ObjectID, height uint64, coverageBps uint32) error { + if t == nil || types.IsZero32([32]byte(objectID)) || height == 0 || coverageBps > 10_000 { + return ErrIdleCapital + } + lots, exists := t.objects[objectID] + if !exists || len(lots) == 0 { + return ErrIdleCapital + } + var amount uint64 + for _, lot := range lots { + if math.MaxUint64-amount < lot.Amount { + return ErrIdleCapital + } + amount += lot.Amount + } + marked, err := MarkProductiveCoverage(lots, height, coverageBps) + if err != nil { + return err + } + preview := t.Clone() + if preview == nil { + return ErrIdleCapital + } + preview.objects[objectID] = marked + if err := preview.productive.Observe(amount, coverageBps); err != nil { + return err + } + *t = *preview + return nil +} + +// ResetProductiveCoverage starts a new aggregation interval while preserving +// lineage age state. +func (t *IdleCapitalTracker) ResetProductiveCoverage() error { + if t == nil { + return ErrIdleCapital + } + t.productive = ProductiveCoverageAccumulator{} + return nil +} + +func (t *IdleCapitalTracker) ObjectLots(objectID types.ObjectID) ([]CapitalLot, bool) { + if t == nil { + return nil, false + } + lots, ok := t.objects[objectID] + if !ok { + return nil, false + } + return append([]CapitalLot(nil), lots...), true +} + +func (t *IdleCapitalTracker) Snapshot(height uint64, bounds []uint64) (IdleCapitalSnapshot, error) { + if t == nil || height == 0 { + return IdleCapitalSnapshot{}, ErrIdleCapital + } + lots := t.allLots() + if len(lots) == 0 { + return IdleCapitalSnapshot{}, ErrIdleCapital + } + histogram, err := BuildDormancyHistogram(lots, height, bounds) + if err != nil { + return IdleCapitalSnapshot{}, err + } + bootstrap := uint64(0) + for _, lot := range lots { + if _, ok := t.bootstrapLineages[lot.LineageID]; !ok { + continue + } + if math.MaxUint64-bootstrap < lot.Amount { + return IdleCapitalSnapshot{}, ErrIdleCapital + } + bootstrap += lot.Amount + } + out := IdleCapitalSnapshot{ + Height: height, + TrackedCapital: histogram.Total, + BootstrapCapital: bootstrap, + LiveObjects: uint64(len(t.objects)), + PendingTransfers: uint64(len(t.pendingTransfers)), + Dormancy: histogram, + } + if t.productive.observedCapital > 0 { + coverage, err := t.productive.Snapshot() + if err != nil { + return IdleCapitalSnapshot{}, err + } + out.ProductiveObservedCapital = coverage.ObservedCapital + out.ProductiveCoverageBps = coverage.ProductiveCoverageBps + } + return out, nil +} + +func (t *IdleCapitalTracker) CheckpointBytes() ([]byte, error) { + if t == nil || len(t.objects) > maxIdleTrackerObjects || len(t.pendingTransfers) > maxIdleTrackerTransfers { + return nil, ErrIdleCapital + } + var w codec.Writer + w.U16(idleCapitalTrackerVersion) + + objectIDs := make([]types.ObjectID, 0, len(t.objects)) + for id := range t.objects { + objectIDs = append(objectIDs, id) + } + sort.Slice(objectIDs, func(i, j int) bool { return bytes.Compare(objectIDs[i][:], objectIDs[j][:]) < 0 }) + w.U32(uint32(len(objectIDs))) + for _, id := range objectIDs { + w.Fixed(id[:]) + if err := writeCapitalLots(&w, t.objects[id]); err != nil { + return nil, err + } + } + + transferIDs := make([]types.Hash, 0, len(t.pendingTransfers)) + for id := range t.pendingTransfers { + transferIDs = append(transferIDs, id) + } + sort.Slice(transferIDs, func(i, j int) bool { return bytes.Compare(transferIDs[i][:], transferIDs[j][:]) < 0 }) + w.U32(uint32(len(transferIDs))) + for _, id := range transferIDs { + w.Fixed(id[:]) + if err := writeCapitalLots(&w, t.pendingTransfers[id]); err != nil { + return nil, err + } + } + + lineages := make([]types.Hash, 0, len(t.bootstrapLineages)) + for lineage := range t.bootstrapLineages { + lineages = append(lineages, lineage) + } + sort.Slice(lineages, func(i, j int) bool { return bytes.Compare(lineages[i][:], lineages[j][:]) < 0 }) + w.U32(uint32(len(lineages))) + for _, lineage := range lineages { + w.Fixed(lineage[:]) + } + w.U64(t.productive.observedCapital) + weighted := t.productive.weightedCoverageBps.Bytes() + if len(weighted) > maxIdleTrackerBigIntBytes { + return nil, ErrIdleCapital + } + w.Bytes(weighted) + return w.BytesCopy(), nil +} + +func RestoreIdleCapitalTracker(data []byte) (*IdleCapitalTracker, error) { + r := codec.NewReader(data) + version, err := r.U16() + if err != nil || version != idleCapitalTrackerVersion { + return nil, ErrIdleCapital + } + out := NewIdleCapitalTracker() + lotCount := 0 + + objectCount, err := r.U32() + if err != nil || objectCount > maxIdleTrackerObjects { + return nil, ErrIdleCapital + } + for i := uint32(0); i < objectCount; i++ { + raw, err := r.Fixed(32) + if err != nil { + return nil, ErrIdleCapital + } + var id types.ObjectID + copy(id[:], raw) + if types.IsZero32([32]byte(id)) { + return nil, ErrIdleCapital + } + if _, duplicate := out.objects[id]; duplicate { + return nil, ErrIdleCapital + } + lots, err := readCapitalLots(r, &lotCount) + if err != nil { + return nil, err + } + out.objects[id] = lots + } + + transferCount, err := r.U32() + if err != nil || transferCount > maxIdleTrackerTransfers { + return nil, ErrIdleCapital + } + for i := uint32(0); i < transferCount; i++ { + raw, err := r.Fixed(32) + if err != nil { + return nil, ErrIdleCapital + } + var id types.Hash + copy(id[:], raw) + if types.IsZero32([32]byte(id)) { + return nil, ErrIdleCapital + } + if _, duplicate := out.pendingTransfers[id]; duplicate { + return nil, ErrIdleCapital + } + lots, err := readCapitalLots(r, &lotCount) + if err != nil { + return nil, err + } + out.pendingTransfers[id] = lots + } + + bootstrapCount, err := r.U32() + if err != nil || bootstrapCount > maxIdleTrackerLots { + return nil, ErrIdleCapital + } + for i := uint32(0); i < bootstrapCount; i++ { + raw, err := r.Fixed(32) + if err != nil { + return nil, ErrIdleCapital + } + var lineage types.Hash + copy(lineage[:], raw) + if types.IsZero32([32]byte(lineage)) { + return nil, ErrIdleCapital + } + if _, duplicate := out.bootstrapLineages[lineage]; duplicate { + return nil, ErrIdleCapital + } + out.bootstrapLineages[lineage] = struct{}{} + } + out.productive.observedCapital, err = r.U64() + if err != nil { + return nil, ErrIdleCapital + } + weighted, err := r.Bytes(maxIdleTrackerBigIntBytes) + if err != nil || r.Done() != nil { + return nil, ErrIdleCapital + } + out.productive.weightedCoverageBps.SetBytes(weighted) + if err := out.validate(); err != nil { + return nil, err + } + return out, nil +} + +func writeCapitalLots(w *codec.Writer, lots []CapitalLot) error { + if w == nil { + return ErrIdleCapital + } + canonical, err := compactCapitalLots(lots) + if err != nil || len(canonical) > maxIdleTrackerLots { + return ErrIdleCapital + } + w.U32(uint32(len(canonical))) + for _, lot := range canonical { + w.Fixed(lot.LineageID[:]) + w.U64(lot.Amount) + w.U64(lot.IdleSinceHeight) + } + return nil +} + +func readCapitalLots(r *codec.Reader, total *int) ([]CapitalLot, error) { + count, err := r.U32() + if err != nil || count == 0 || count > maxIdleTrackerLots || total == nil || *total > maxIdleTrackerLots-int(count) { + return nil, ErrIdleCapital + } + *total += int(count) + lots := make([]CapitalLot, int(count)) + for i := range lots { + raw, err := r.Fixed(32) + if err != nil { + return nil, ErrIdleCapital + } + copy(lots[i].LineageID[:], raw) + lots[i].Amount, err = r.U64() + if err != nil { + return nil, ErrIdleCapital + } + lots[i].IdleSinceHeight, err = r.U64() + if err != nil || lots[i].Validate() != nil { + return nil, ErrIdleCapital + } + } + canonical, err := compactCapitalLots(lots) + if err != nil || len(canonical) != len(lots) { + return nil, ErrIdleCapital + } + for i := range lots { + if lots[i] != canonical[i] { + return nil, ErrIdleCapital + } + } + return lots, nil +} + +func (t *IdleCapitalTracker) validate() error { + if t == nil || len(t.objects) > maxIdleTrackerObjects || len(t.pendingTransfers) > maxIdleTrackerTransfers { + return ErrIdleCapital + } + activeLineages := make(map[types.Hash]struct{}) + lotCount := 0 + visit := func(lots []CapitalLot) error { + canonical, err := compactCapitalLots(lots) + if err != nil || len(canonical) != len(lots) || lotCount > maxIdleTrackerLots-len(lots) { + return ErrIdleCapital + } + lotCount += len(lots) + for i := range lots { + if lots[i] != canonical[i] { + return ErrIdleCapital + } + activeLineages[lots[i].LineageID] = struct{}{} + } + return nil + } + for id, lots := range t.objects { + if types.IsZero32([32]byte(id)) || visit(lots) != nil { + return ErrIdleCapital + } + } + for id, lots := range t.pendingTransfers { + if types.IsZero32([32]byte(id)) || visit(lots) != nil { + return ErrIdleCapital + } + } + for lineage := range t.bootstrapLineages { + if types.IsZero32([32]byte(lineage)) { + return ErrIdleCapital + } + if _, active := activeLineages[lineage]; !active { + return ErrIdleCapital + } + } + weighted := new(big.Int).Set(&t.productive.weightedCoverageBps) + if t.productive.observedCapital == 0 { + if weighted.Sign() != 0 { + return ErrIdleCapital + } + return nil + } + if weighted.Sign() < 0 { + return ErrIdleCapital + } + maximum := new(big.Int).Mul(new(big.Int).SetUint64(t.productive.observedCapital), big.NewInt(10_000)) + if weighted.Cmp(maximum) > 0 { + return ErrIdleCapital + } + return nil +} + +func (t *IdleCapitalTracker) allLots() []CapitalLot { + if t == nil { + return nil + } + out := make([]CapitalLot, 0) + for _, lots := range t.objects { + out = append(out, lots...) + } + for _, lots := range t.pendingTransfers { + out = append(out, lots...) + } + return out +} + +func (t *IdleCapitalTracker) pruneBootstrapLineages() { + if t == nil || len(t.bootstrapLineages) == 0 { + return + } + active := make(map[types.Hash]struct{}) + for _, lot := range t.allLots() { + active[lot.LineageID] = struct{}{} + } + for lineage := range t.bootstrapLineages { + if _, ok := active[lineage]; !ok { + delete(t.bootstrapLineages, lineage) + } + } +} From 17ff26638c24e0367a8ebe065a5fbd9f921ef562 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:30:02 +0200 Subject: [PATCH 13/37] Add idle capital tracker tests --- internal/v2/economics/idle_tracker_test.go | 232 +++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 internal/v2/economics/idle_tracker_test.go diff --git a/internal/v2/economics/idle_tracker_test.go b/internal/v2/economics/idle_tracker_test.go new file mode 100644 index 00000000..a9d78db4 --- /dev/null +++ b/internal/v2/economics/idle_tracker_test.go @@ -0,0 +1,232 @@ +package economics + +import ( + "bytes" + "reflect" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestIdleCapitalTrackerSelfCycleAndFragmentationDoNotResetAge(t *testing.T) { + tracker := NewIdleCapitalTracker() + seedID := trackerObjectID("seed") + if err := tracker.BootstrapObject(seedID, 100_000, 10); err != nil { + t.Fatal(err) + } + baseline, err := tracker.Snapshot(1_000, []uint64{10, 100, 1_000}) + if err != nil { + t.Fatal(err) + } + + targets := make([]CapitalTarget, 100) + inputs := []types.ObjectID{seedID} + for i := range targets { + targets[i] = CapitalTarget{ObjectID: trackerObjectIDWithIndex("split", i), Amount: 1_000} + } + if err := tracker.ApplyTransition(inputs, targets, 0); err != nil { + t.Fatal(err) + } + fragmented, err := tracker.Snapshot(1_000, []uint64{10, 100, 1_000}) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(fragmented.Dormancy, baseline.Dormancy) { + t.Fatalf("wallet fragmentation reset dormancy: %#v != %#v", fragmented.Dormancy, baseline.Dormancy) + } + + mergeInputs := make([]types.ObjectID, len(targets)) + for i, target := range targets { + mergeInputs[i] = target.ObjectID + } + mergedID := trackerObjectID("merged") + if err := tracker.ApplyTransition(mergeInputs, []CapitalTarget{{ObjectID: mergedID, Amount: 100_000}}, 0); err != nil { + t.Fatal(err) + } + cycledID := trackerObjectID("cycled") + if err := tracker.ApplyTransition([]types.ObjectID{mergedID}, []CapitalTarget{{ObjectID: cycledID, Amount: 100_000}}, 0); err != nil { + t.Fatal(err) + } + lots, ok := tracker.ObjectLots(cycledID) + if !ok || len(lots) != 1 || lots[0].IdleSinceHeight != 10 || lots[0].Amount != 100_000 { + t.Fatalf("self-cycle changed prospective lineage: %#v", lots) + } +} + +func TestIdleCapitalTrackerTargetOrderDoesNotChangeCheckpoint(t *testing.T) { + seedID := trackerObjectID("order-seed") + left := NewIdleCapitalTracker() + right := NewIdleCapitalTracker() + for _, tracker := range []*IdleCapitalTracker{left, right} { + if err := tracker.BootstrapObject(seedID, 1_000, 50); err != nil { + t.Fatal(err) + } + } + a := CapitalTarget{ObjectID: trackerObjectID("a"), Amount: 400} + b := CapitalTarget{ObjectID: trackerObjectID("b"), Amount: 600} + if err := left.ApplyTransition([]types.ObjectID{seedID}, []CapitalTarget{a, b}, 0); err != nil { + t.Fatal(err) + } + if err := right.ApplyTransition([]types.ObjectID{seedID}, []CapitalTarget{b, a}, 0); err != nil { + t.Fatal(err) + } + leftRaw, err := left.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + rightRaw, err := right.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(leftRaw, rightRaw) { + t.Fatal("target declaration order changed canonical tracker state") + } +} + +func TestIdleCapitalTrackerCrossShardPendingRoundTrip(t *testing.T) { + tracker := NewIdleCapitalTracker() + seedID := trackerObjectID("cross-seed") + if err := tracker.BootstrapObject(seedID, 10_000, 25); err != nil { + t.Fatal(err) + } + transferID := types.HashBytes("idle-capital/test-transfer", []byte("one")) + if err := tracker.ApplyTransition([]types.ObjectID{seedID}, []CapitalTarget{{TransferID: transferID, Amount: 9_900}}, 100); err != nil { + t.Fatal(err) + } + pending, err := tracker.Snapshot(1_000, []uint64{100, 1_000}) + if err != nil { + t.Fatal(err) + } + if pending.TrackedCapital != 9_900 || pending.PendingTransfers != 1 || pending.LiveObjects != 0 { + t.Fatalf("unexpected pending cross-shard snapshot: %#v", pending) + } + importedID := trackerObjectID("cross-import") + if err := tracker.MaterializeTransfer(transferID, importedID); err != nil { + t.Fatal(err) + } + lots, ok := tracker.ObjectLots(importedID) + if !ok || len(lots) != 1 || lots[0].IdleSinceHeight != 25 || lots[0].Amount != 9_900 { + t.Fatalf("cross-shard import lost lineage: %#v", lots) + } + imported, err := tracker.Snapshot(1_000, []uint64{100, 1_000}) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(imported.Dormancy, pending.Dormancy) || imported.PendingTransfers != 0 || imported.LiveObjects != 1 { + t.Fatalf("materialization changed economic age: pending=%#v imported=%#v", pending, imported) + } +} + +func TestIdleCapitalTrackerProductiveHookIsExplicitAndWeighted(t *testing.T) { + tracker := NewIdleCapitalTracker() + seedID := trackerObjectID("productive-seed") + if err := tracker.BootstrapObject(seedID, 8_000, 10); err != nil { + t.Fatal(err) + } + // Ordinary movement does not count as productive. + movedID := trackerObjectID("ordinary-move") + if err := tracker.ApplyTransition([]types.ObjectID{seedID}, []CapitalTarget{{ObjectID: movedID, Amount: 8_000}}, 0); err != nil { + t.Fatal(err) + } + before, err := tracker.Snapshot(100, []uint64{20, 100}) + if err != nil { + t.Fatal(err) + } + if before.ProductiveObservedCapital != 0 || before.ProductiveCoverageBps != 0 { + t.Fatalf("ordinary transfer fabricated productive coverage: %#v", before) + } + if err := tracker.MarkObjectProductive(movedID, 100, 2_500); err != nil { + t.Fatal(err) + } + after, err := tracker.Snapshot(100, []uint64{20, 100}) + if err != nil { + t.Fatal(err) + } + if after.ProductiveObservedCapital != 8_000 || after.ProductiveCoverageBps != 2_500 { + t.Fatalf("unexpected productive coverage: %#v", after) + } + lots, _ := tracker.ObjectLots(movedID) + var reset, idle uint64 + for _, lot := range lots { + if lot.IdleSinceHeight == 100 { + reset += lot.Amount + } else if lot.IdleSinceHeight == 10 { + idle += lot.Amount + } + } + if reset != 2_000 || idle != 6_000 { + t.Fatalf("productive hook reset wrong amount: reset=%d idle=%d lots=%#v", reset, idle, lots) + } +} + +func TestIdleCapitalTrackerCheckpointRoundTrip(t *testing.T) { + tracker := NewIdleCapitalTracker() + seedID := trackerObjectID("checkpoint-seed") + if err := tracker.BootstrapObject(seedID, 5_000, 12); err != nil { + t.Fatal(err) + } + transferID := types.HashBytes("idle-capital/test-transfer", []byte("checkpoint")) + localID := trackerObjectID("checkpoint-local") + if err := tracker.ApplyTransition([]types.ObjectID{seedID}, []CapitalTarget{ + {TransferID: transferID, Amount: 2_000}, + {ObjectID: localID, Amount: 3_000}, + }, 0); err != nil { + t.Fatal(err) + } + if err := tracker.MarkObjectProductive(localID, 100, 5_000); err != nil { + t.Fatal(err) + } + raw, err := tracker.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + restored, err := RestoreIdleCapitalTracker(raw) + if err != nil { + t.Fatal(err) + } + rawAgain, err := restored.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(raw, rawAgain) { + t.Fatal("idle tracker checkpoint is not canonical across restore") + } + before, err := tracker.Snapshot(200, []uint64{50, 200}) + if err != nil { + t.Fatal(err) + } + after, err := restored.Snapshot(200, []uint64{50, 200}) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(before, after) { + t.Fatalf("restored tracker changed state: %#v != %#v", after, before) + } +} + +func TestIdleCapitalTrackerRejectsNonConservingOrAmbiguousTransitions(t *testing.T) { + tracker := NewIdleCapitalTracker() + seedID := trackerObjectID("invalid-seed") + if err := tracker.BootstrapObject(seedID, 100, 10); err != nil { + t.Fatal(err) + } + if err := tracker.ApplyTransition([]types.ObjectID{seedID}, []CapitalTarget{{ObjectID: trackerObjectID("short"), Amount: 99}}, 0); err == nil { + t.Fatal("non-conserving transition accepted") + } + both := CapitalTarget{ObjectID: trackerObjectID("both"), TransferID: types.Hash{1}, Amount: 100} + if err := tracker.ApplyTransition([]types.ObjectID{seedID}, []CapitalTarget{both}, 0); err == nil { + t.Fatal("ambiguous local/cross-shard target accepted") + } + if _, ok := tracker.ObjectLots(seedID); !ok { + t.Fatal("failed transition mutated tracker") + } +} + +func trackerObjectID(label string) types.ObjectID { + return types.ObjectID(types.HashBytes("idle-capital/test-object", []byte(label))) +} + +func trackerObjectIDWithIndex(label string, index int) types.ObjectID { + payload := append([]byte(label), byte(index>>24), byte(index>>16), byte(index>>8), byte(index)) + return types.ObjectID(types.HashBytes("idle-capital/test-object", payload)) +} From f06889f84645e9286f684445808d4d31b36e3f5d Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:30:34 +0200 Subject: [PATCH 14/37] Refresh Protocol v2 tokenomics from economics paper --- docs/tokenomics-v2.md | 235 +++++++++++++----------------------------- 1 file changed, 73 insertions(+), 162 deletions(-) diff --git a/docs/tokenomics-v2.md b/docs/tokenomics-v2.md index 7f4034f2..7b0a1d32 100644 --- a/docs/tokenomics-v2.md +++ b/docs/tokenomics-v2.md @@ -1,205 +1,116 @@ -# Zephyr v2 Tokenomics — Protocol Economics and Purchasing-Power Target +# Zephyr Chain Protocol v2 — Economics & Tokenomics -Status: **design contract + shadow-mode implementation target**. +> Status: Protocol v2 research and implementation. Economic mechanisms described here are **not live monetary policy** unless explicitly marked as activated. The activation discipline is **measure first → simulate second → activate last**. -This document tracks the executable Protocol v2 economic foundation against the Zephyr Chain *Protocol Economics, Monetary Design, Governance, Lending and Consensus Incentives* working paper v0.1 dated 19 August 2026. +## Design goals -The working paper supersedes the earlier assumption that the monetary objective is a fixed annual ZPH supply-growth rate. The canonical objective is now expressed in Zephyr-native purchasing power. Supply remains endogenous and any monetary actuator stays shadow-only until explicit activation gates pass. +Zephyr Chain Protocol v2 treats tokenomics as a measurable control system rather than a fixed-emission schedule. The monetary design is intended to align purchasing power, productive network capacity, state costs, compute-market incentives and governance without putting heavy economic computation in the consensus hot path. -## 1. Canonical monetary objective +The protocol does **not** target a fixed +2% annual supply increase. Instead, the long-term monetary objective is approximately **-2% annual purchasing-power drift for ZPH against a Zephyr-native basket**, represented as an annual ZPPI target factor of **1.020408163** (Q9: `1_020_408_163`). Supply is therefore endogenous: future issuance or contraction, if activated, must respond to measured conditions and bounded governance rules rather than a constant inflation target. -The intended long-run property is: +## Core economic indexes -```text -PP_t = 1 / ZPPI_t -PP_(t+1) = 0.98 * PP_t -ZPPI_(t+1) / ZPPI_t = 1 / 0.98 ~= 1.020408163 -``` +### ZPPI — Zephyr Purchasing Power Index -Therefore an exact annual **-2.00% purchasing-power target** corresponds to approximately **+2.0408% annual ZPPI inflation**. +ZPPI is the slow monetary-direction signal. Protocol v2 currently models a deterministic, versioned basket covering compute, data availability and storage with fixed-point price relatives, explicit reference values and weights, coverage/reliability checks, EWMA smoothing and bounded movement. -The protocol stores the canonical Q9 annual factor: +Purchasing power is represented as `PP = 1 / ZPPI`. ZPPI is designed to inform future monetary direction; it is not, by itself, an instruction to mint or burn supply. -```text -TargetZPPIAnnualFactorQ9 = 1_020_408_163 -``` +### ZCSI — Zephyr Compute Supply Index -This is not a fiat CPI target and does not require an exchange-price oracle. ZPPI is the price, in ZPH, of a versioned basket of competitively priced Zephyr-native services. +ZCSI is primarily a compute incentive and capacity-routing signal. It is intended to reflect **verified delivered work**, not theoretical FLOPS, advertised capacity or unverified provider claims. -## 2. Separation of control loops +### ZCU — Zephyr Compute Unit reference -Zephyr keeps independent control signals so one metric cannot dominate monetary policy, resource pricing and consensus simultaneously. +The v2 implementation includes a dynamic ZCU reference derived from delivered-slot, availability, success and confidence observations using deterministic weighted aggregation, EWMA smoothing, bounded movement and fail-closed coverage rules. -```text -finalized native markets - -> dynamic ZCU / service normalization - -> ZCPI + DA/storage observations - -> ZPPI - -> slow ZAMP monetary evaluation +Only verified/finalized compute-market evidence should feed production economic telemetry. -compute demand/supply/backlog/utilization - -> ZCSI - -> primarily reward routing / capacity incentives +## Compute market separation -coin age + productive coverage + concentration - -> IdlePressure / future idle-capital levy +The compute market is asynchronous and keeps heavy work off-chain while using on-chain escrow, verification and settlement. Provider payment is separate from blockchain gas. -stake + reliability + diversity - -> consensus SecurityWeight -``` +`WorkSpec` and `WorkRegistry` are versioned/canonical protocol objects. Only `VerifiedWork` finalized through consensus may feed trusted economic telemetry such as ZCPI/ZCSI/ZCU/ZPPI. Production benchmark authentication, governed registry activation and ZK/TEE verifier infrastructure remain future gates. -The fast blockchain resource-fee loop remains separate from the slow monetary loop. +## Idle capital and productive-use telemetry -## 3. Dynamic ZCU reference +Protocol v2 is developing a prospective `IdleCapitalTracker` as **shadow telemetry**. It tracks capital lineage rather than wallet counts so that ordinary transfers, self-transfers, change outputs, split/merge patterns or address fragmentation cannot fabricate economic activity or reset dormancy. -A fixed FLOP benchmark becomes obsolete as hardware and software improve. Protocol v2 therefore retains the multidimensional `WorkVector` accounting layer and adds a shadow dynamic reference for each compute work class. +Key properties of the tracker design: -A verified compute-slot observation contains benchmark-derived performance plus evidence-weighted availability history. Provider-declared peak performance is not an input. +- capital lineage is amount-conserving and deterministic; +- `IdleSinceHeight` survives ordinary movement, split/merge and cross-shard transfer; +- productive use is an explicit verified hook, separate from generic transfers or contract calls; +- cross-shard transfer identity is the consensus-canonical receipt hash; +- destination object identity is the protocol-defined destination object; +- prospective bootstrap coverage is exposed explicitly and is not presented as complete historical lineage; +- checkpoints are canonical, bounded and fail closed on malformed state; +- replay and invalid/non-conserving transitions are rejected atomically. -```text -EffectiveSlotWeight = - DeliveredSlotTime - * AvailabilityEWMA - * SuccessEWMA - * Confidence +The tracker does **not** currently impose an idle levy and does not mutate supply. -ZCU_ref(class, epoch) = weighted_median(verified slot performance) -``` +## State carrying cost -The reference is smoothed with EWMA and bounded by a maximum per-epoch change. Thin classes fail closed as unreliable rather than inventing a reference. +Protocol v2 includes deterministic measurement of state carrying cost and fragmentation pressure. The purpose is to quantify long-lived state/resource externalities before proposing any economic charge. Measurement and public simulation precede any activation decision. -Current implementation: +## Monetary activation gates -- `internal/v2/economics/zcu_reference.go` -- deterministic weighted median per work class; -- delivered-slot-time, availability, success and confidence weighting; -- Q9 fixed-point reference values; -- minimum verified-slot coverage gate; -- EWMA smoothing; -- per-epoch rate limit; -- no provider self-reported peak-performance field. +No idle levy, monetary minting, burn policy or reward-routing change becomes live merely because telemetry exists. Activation requires explicit protocol/governance gates, sufficient observation windows, deterministic simulations and safety review. -This is **shadow telemetry only**. It does not change live rewards or supply. +The required sequence is: -## 4. ZCPI and ZCSI +1. **Measure** — collect deterministic, finalized evidence with explicit coverage/reliability. +2. **Simulate** — run public scenarios over long windows, including adversarial and fragmented-capital cases. +3. **Activate** — only through an explicit bounded protocol/governance decision after the evidence is sufficient. -Existing Protocol v2 code already reconstructs verified paid compute observations from finalized compute state and builds: +## Governance direction -- ZCPI: finalized paid ZPH per standardized verified compute unit, using per-class medians, EWMA, coverage and reliability; -- ZCSI: bounded compute scarcity from escrow-backed demand, verified supply, backlog, fulfillment, utilization and reliable price trend. +Protocol v2 is designed for explicit versioning, bounded parameters and delayed/timelocked activation. Later roadmap stages include contribution/citizen evidence, VRF committees and governance objects. These remain developmental and must not be interpreted as production-ready governance claims. -Unreliable price or capacity evidence fails closed. ZCSI is primarily a routing signal for compute incentives and must not become the monetary anchor. +## Lending roadmap -## 5. ZPPI — Zephyr Purchasing Power Index +The economics roadmap includes bilateral native lending, including the ability to represent negative rates where economically justified, followed by device credentials and additional economic indexes. These features belong to later Protocol v2 phases and are not yet live. -The initial basket emphasizes: +## Consensus incentives and benchmark discipline -- compute; -- data availability; -- storage. +Economic design must not weaken safety, liveness or verifiability. Performance claims are valid only for **finalized-through-consensus** transactions. Execution-only throughput is not chain TPS. -Gas/base fee should have low or zero basket weight because it is directly influenced by protocol policy and would introduce reflexive feedback. +The v2 benchmark framework is being extended to report: -The first executable reference uses a version-scoped **chain-weighted price-relative basket**. Each service is normalized to a reference price fixed for that basket version before aggregation, so heterogeneous raw service prices are not treated as equivalent units. +- finalized TPS over a measured window; +- warm-up finalization separately from measured finalization; +- p50/p95/p99 finality latency; +- shard/validator counts, transactions per block, block cadence and cross-shard workload ratio; +- commit SHA and machine/environment metadata; +- rejected transactions and errors; +- explicit invalidation when safety or liveness failures occur; +- machine-readable JSON output. -Current implementation: +The **1,000,000 TPS finalized-through-consensus** figure is a long-term aspirational north-star, **not a benchmark already achieved and not a production-capacity claim**. -- `internal/v2/economics/zppi.go`; -- Q9 component price relatives; -- compute / data-availability / storage components; -- version-supplied reference prices and weights; -- reliability-aware coverage; -- EWMA smoothing; -- canonical exact purchasing-power/ZPPI target conversion; -- fail-closed behavior when reliable basket coverage is too low. +## Current Protocol v2 implementation status -A future benchmark/basket governance transition may use geometric aggregation after overlap/calibration. Basket changes must be versioned, delayed and measured across an overlap window. +Implemented or prepared in the current P1 economics work: -## 6. ZAMP migration boundary +- deterministic dynamic ZCU reference; +- ZPPI compute/DA/storage basket with Q9 arithmetic and reliability/coverage gates; +- capital-lot lineage primitives, dormancy histograms and productive-coverage measurement; +- deterministic state carrying-cost estimator; +- adversarial fragmentation simulation; +- prospective IdleCapitalTracker implementation and adversarial tests prepared for repository integration; +- finalized-through-consensus benchmark report core and tests prepared for V2 Lab integration. -The branch already contains a deterministic shadow ZAMP evaluator that was built around the earlier 2% annual net-supply-growth center. That controller remains useful as a simulation/actuator baseline, but the **2% supply-growth center is no longer the normative monetary target**. +Still gated / not production-ready: -Until the ZPPI path is integrated end-to-end: +- live monetary mint/burn or idle levy; +- final reward-routing parameters; +- full collector/checkpoint integration of idle-capital tracking; +- production benchmark/capacity authentication; +- production ZK/TEE verification infrastructure; +- governed production WorkRegistry; +- production Rust/WASM contract stack; +- any claim that the 1M TPS north-star has been reached. -- the legacy supply-growth evaluator remains shadow-only; -- it must not be activated for live minting; -- new ZCU/ZPPI snapshots are measurement foundations, not supply commands; -- the next controller revision must use ZPPI target error as its primary monetary direction signal; -- ZCSI should mainly affect reward routing rather than broad issuance; -- mint, burn, reserve and reward-routing changes remain deterministic consensus state transitions behind explicit activation gates. +## Source of economic design -## 7. Idle capital and productive coverage — next P1 tranche - -The working design defines: - -```text -AgePressure(a) = 1 - exp(-k * a) -EffectiveIdle = AgePressure * (1 - ProductiveCoverage) -IdleLevyIntensity_i = Base * EffectiveIdle_i * CapitalPressure_i * GlobalIdlePressure -``` - -P1 implementation should add shadow-only: - -- dormancy histograms; -- capital-lineage metadata for split objects; -- fragmentation/state carrying-cost simulation; -- productive-coverage accounting hooks; -- adversarial scenarios for 10/100/1,000/10,000-way wallet fragmentation. - -No balance levy should activate in this tranche. - -## 8. Native lending — P2 - -Native bilateral lending is the productive escape valve for idle capital. A loan becomes productive only after both sides accept terms and funding/collateral locks are atomically satisfied. - -The future market includes: - -- lender offers and borrower requests; -- counteroffers; -- duration/rate/collateral constraints; -- objective borrower history; -- negative rates; -- anti-wash credit that accrues over time rather than entirely at origination. - -Negative rates are intentionally valid when the cost of lending is lower than the expected idle-capital pressure plus risk cost. - -## 9. Activation discipline - -Economic mechanisms progress through: - -```text -Observe -> Shadow -> adversarial simulation -> bounded Active -``` - -No controller should jump directly from a paper design to live monetary state. - -Minimum gates include: - -1. all monetary metrics derived reproducibly from finalized consensus state; -2. authenticated benchmark/availability state for verified compute supply; -3. versioned basket and benchmark-era governance with overlap and timelocks; -4. long-horizon manipulation tests for self-cycling, wash lending, fragmentation and concentration; -5. bounded parameter changes and deterministic emergency fallback; -6. Citizen Node decoding/verification of economic state and history; -7. explicit protocol-version/activation-height transitions for mint, reward, fee split or levy changes. - -## 10. Implementation order - -The current working-paper roadmap is: - -- **P0** keep CI/gofmt and Protocol v2 correctness gates green; -- **P1** dynamic ZCU + verified-slot accounting in shadow; -- **P1** ZPPI basket + exact -2% purchasing-power target simulation; -- **P1** dormancy histogram + lineage + fragmentation simulator; -- **P2** bilateral native lending; -- **P2** DeviceCredential abstraction; -- **P2** ZECI concentration telemetry; -- **P3** ContributionScore/Citizen challenge network, VRF committees and governance timelocks; -- **P4** Ethereum collateral light-client adapter and Bitcoin collateral research; -- **P5** incremental economic activation after long-running public simulation. - -The engineering rule remains: - -```text -measure first -> simulate second -> activate last -``` +This documentation reflects **Zephyr Chain — Protocol Economics, Monetary Design, Governance, Lending and Consensus Incentives, working whitepaper v0.1 (19 Aug 2026)** together with the current Protocol v2 P1 implementation state. From 0cb0b3aa870196f2ccde34ea7d16889ca4b3ce99 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:32:37 +0200 Subject: [PATCH 15/37] Add finalized benchmark report core --- internal/v2/lab/finality_report.go | 139 +++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 internal/v2/lab/finality_report.go diff --git a/internal/v2/lab/finality_report.go b/internal/v2/lab/finality_report.go new file mode 100644 index 00000000..58f2042b --- /dev/null +++ b/internal/v2/lab/finality_report.go @@ -0,0 +1,139 @@ +package lab + +import ( + "encoding/json" + "errors" + "runtime" + "sort" + "time" +) + +var ErrInvalidFinalityBenchmark = errors.New("invalid finalized-through-consensus benchmark") + +type FinalityBenchmarkConfig struct { + ShardCount uint32 `json:"shard_count"` + ValidatorCount uint32 `json:"validator_count"` + TransactionsBlock uint64 `json:"transactions_per_block"` + BlockCadenceMS uint64 `json:"block_cadence_ms"` + CrossShardBPS uint32 `json:"cross_shard_bps"` + Workload string `json:"workload"` +} + +type FinalityBenchmarkEnvironment struct { + CommitSHA string `json:"commit_sha"` + CPUCount int `json:"cpu_count"` + GOMAXPROCS int `json:"gomaxprocs"` + RAMBytes uint64 `json:"ram_bytes,omitempty"` + NetworkBytes uint64 `json:"network_bytes,omitempty"` + DABytes uint64 `json:"da_bytes,omitempty"` +} + +type FinalitySample struct { + SubmittedAt time.Time + FinalizedAt time.Time +} + +type FinalityBenchmarkReport struct { + Version uint16 `json:"version"` + Valid bool `json:"valid"` + InvalidReason string `json:"invalid_reason,omitempty"` + Config FinalityBenchmarkConfig `json:"config"` + Environment FinalityBenchmarkEnvironment `json:"environment"` + WarmupFinalized uint64 `json:"warmup_finalized"` + MeasuredFinalized uint64 `json:"measured_finalized"` + MeasuredDurationNS int64 `json:"measured_duration_ns"` + FinalizedTPS float64 `json:"finalized_tps"` + FinalityP50NS int64 `json:"finality_p50_ns"` + FinalityP95NS int64 `json:"finality_p95_ns"` + FinalityP99NS int64 `json:"finality_p99_ns"` + RejectedTransactions uint64 `json:"rejected_transactions"` + Errors uint64 `json:"errors"` + SafetyFailures uint64 `json:"safety_failures"` + LivenessFailures uint64 `json:"liveness_failures"` +} + +type FinalityBenchmarkInput struct { + Config FinalityBenchmarkConfig + Environment FinalityBenchmarkEnvironment + WarmupFinalized uint64 + MeasuredStart time.Time + MeasuredEnd time.Time + Samples []FinalitySample + RejectedTransactions uint64 + Errors uint64 + SafetyFailures uint64 + LivenessFailures uint64 +} + +func BuildFinalityBenchmarkReport(in FinalityBenchmarkInput) (FinalityBenchmarkReport, error) { + if err := validateBenchmarkConfig(in.Config); err != nil { + return FinalityBenchmarkReport{}, err + } + if in.MeasuredStart.IsZero() || !in.MeasuredEnd.After(in.MeasuredStart) || len(in.Samples) == 0 { + return FinalityBenchmarkReport{}, ErrInvalidFinalityBenchmark + } + latencies := make([]time.Duration, len(in.Samples)) + for i, sample := range in.Samples { + if sample.SubmittedAt.IsZero() || sample.FinalizedAt.Before(sample.SubmittedAt) || sample.FinalizedAt.After(in.MeasuredEnd) { + return FinalityBenchmarkReport{}, ErrInvalidFinalityBenchmark + } + latencies[i] = sample.FinalizedAt.Sub(sample.SubmittedAt) + } + duration := in.MeasuredEnd.Sub(in.MeasuredStart) + seconds := duration.Seconds() + if seconds <= 0 { + return FinalityBenchmarkReport{}, ErrInvalidFinalityBenchmark + } + env := in.Environment + if env.CPUCount == 0 { + env.CPUCount = runtime.NumCPU() + } + if env.GOMAXPROCS == 0 { + env.GOMAXPROCS = runtime.GOMAXPROCS(0) + } + report := FinalityBenchmarkReport{ + Version: 1, + Valid: in.SafetyFailures == 0 && in.LivenessFailures == 0, + Config: in.Config, + Environment: env, + WarmupFinalized: in.WarmupFinalized, + MeasuredFinalized: uint64(len(in.Samples)), + MeasuredDurationNS: int64(duration), + FinalizedTPS: float64(len(in.Samples)) / seconds, + FinalityP50NS: int64(nearestRankDuration(latencies, 50)), + FinalityP95NS: int64(nearestRankDuration(latencies, 95)), + FinalityP99NS: int64(nearestRankDuration(latencies, 99)), + RejectedTransactions: in.RejectedTransactions, + Errors: in.Errors, + SafetyFailures: in.SafetyFailures, + LivenessFailures: in.LivenessFailures, + } + if !report.Valid { + report.InvalidReason = "safety or liveness failure observed" + } + return report, nil +} + +func (r FinalityBenchmarkReport) JSON() ([]byte, error) { + return json.MarshalIndent(r, "", " ") +} + +func validateBenchmarkConfig(config FinalityBenchmarkConfig) error { + if config.ShardCount == 0 || config.ValidatorCount == 0 || config.TransactionsBlock == 0 || config.BlockCadenceMS == 0 || config.CrossShardBPS > 10_000 || config.Workload == "" { + return ErrInvalidFinalityBenchmark + } + return nil +} + +func nearestRankDuration(values []time.Duration, percentile int) time.Duration { + if len(values) == 0 || percentile <= 0 || percentile > 100 { + return 0 + } + ordered := append([]time.Duration(nil), values...) + sort.Slice(ordered, func(i, j int) bool { return ordered[i] < ordered[j] }) + rank := (percentile*len(ordered) + 99) / 100 + if rank < 1 { + rank = 1 + } + return ordered[rank-1] +} From 1819ae47ba453592a0ff26adb2e1cf2f0b555d85 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:32:50 +0200 Subject: [PATCH 16/37] Test finalized benchmark reporting --- internal/v2/lab/finality_report_test.go | 107 ++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 internal/v2/lab/finality_report_test.go diff --git a/internal/v2/lab/finality_report_test.go b/internal/v2/lab/finality_report_test.go new file mode 100644 index 00000000..fa339eae --- /dev/null +++ b/internal/v2/lab/finality_report_test.go @@ -0,0 +1,107 @@ +package lab + +import ( + "encoding/json" + "math" + "testing" + "time" +) + +func TestBuildFinalityBenchmarkReportCountsOnlyFinalizedWindow(t *testing.T) { + start := time.Unix(1_000, 0) + samples := make([]FinalitySample, 100) + for i := range samples { + submitted := start.Add(time.Duration(i) * time.Millisecond) + latency := time.Duration(i+1) * time.Millisecond + samples[i] = FinalitySample{SubmittedAt: submitted, FinalizedAt: submitted.Add(latency)} + } + report, err := BuildFinalityBenchmarkReport(FinalityBenchmarkInput{ + Config: FinalityBenchmarkConfig{ShardCount: 4, ValidatorCount: 7, TransactionsBlock: 1_000, BlockCadenceMS: 100, CrossShardBPS: 2_500, Workload: "native-transfer"}, + Environment: FinalityBenchmarkEnvironment{CommitSHA: "deadbeef", CPUCount: 8, GOMAXPROCS: 8, RAMBytes: 16 << 30}, + WarmupFinalized: 20, + MeasuredStart: start, + MeasuredEnd: start.Add(2 * time.Second), + Samples: samples, + RejectedTransactions: 3, + }) + if err != nil { + t.Fatal(err) + } + if !report.Valid || report.MeasuredFinalized != 100 || report.WarmupFinalized != 20 { + t.Fatalf("unexpected report: %#v", report) + } + if math.Abs(report.FinalizedTPS-50) > 0.000001 { + t.Fatalf("TPS = %f, want 50", report.FinalizedTPS) + } + if got, want := time.Duration(report.FinalityP50NS), 50*time.Millisecond; got != want { + t.Fatalf("p50 = %v, want %v", got, want) + } + if got, want := time.Duration(report.FinalityP95NS), 95*time.Millisecond; got != want { + t.Fatalf("p95 = %v, want %v", got, want) + } + if got, want := time.Duration(report.FinalityP99NS), 99*time.Millisecond; got != want { + t.Fatalf("p99 = %v, want %v", got, want) + } + if report.RejectedTransactions != 3 { + t.Fatalf("rejected = %d", report.RejectedTransactions) + } +} + +func TestFinalityBenchmarkSafetyOrLivenessFailureInvalidatesReport(t *testing.T) { + start := time.Unix(2_000, 0) + report, err := BuildFinalityBenchmarkReport(FinalityBenchmarkInput{ + Config: FinalityBenchmarkConfig{ShardCount: 1, ValidatorCount: 4, TransactionsBlock: 10, BlockCadenceMS: 500, Workload: "native-transfer"}, + MeasuredStart: start, + MeasuredEnd: start.Add(time.Second), + Samples: []FinalitySample{{SubmittedAt: start, FinalizedAt: start.Add(100 * time.Millisecond)}}, + SafetyFailures: 1, + }) + if err != nil { + t.Fatal(err) + } + if report.Valid || report.InvalidReason == "" { + t.Fatalf("unsafe report was valid: %#v", report) + } +} + +func TestFinalityBenchmarkRejectsNonFinalizedOrInvalidSamples(t *testing.T) { + start := time.Unix(3_000, 0) + base := FinalityBenchmarkInput{ + Config: FinalityBenchmarkConfig{ShardCount: 2, ValidatorCount: 4, TransactionsBlock: 10, BlockCadenceMS: 500, Workload: "native-transfer"}, + MeasuredStart: start, + MeasuredEnd: start.Add(time.Second), + } + base.Samples = []FinalitySample{{SubmittedAt: start.Add(time.Second), FinalizedAt: start.Add(2 * time.Second)}} + if _, err := BuildFinalityBenchmarkReport(base); err == nil { + t.Fatal("sample finalized outside measurement window accepted") + } + base.Samples = []FinalitySample{{SubmittedAt: start.Add(200 * time.Millisecond), FinalizedAt: start.Add(100 * time.Millisecond)}} + if _, err := BuildFinalityBenchmarkReport(base); err == nil { + t.Fatal("negative finality latency accepted") + } +} + +func TestFinalityBenchmarkJSONContainsMachineReadableConfiguration(t *testing.T) { + start := time.Unix(4_000, 0) + report, err := BuildFinalityBenchmarkReport(FinalityBenchmarkInput{ + Config: FinalityBenchmarkConfig{ShardCount: 8, ValidatorCount: 7, TransactionsBlock: 2_000, BlockCadenceMS: 100, CrossShardBPS: 5_000, Workload: "native-transfer"}, + Environment: FinalityBenchmarkEnvironment{CommitSHA: "abc123", CPUCount: 16, GOMAXPROCS: 16, NetworkBytes: 1234, DABytes: 5678}, + MeasuredStart: start, + MeasuredEnd: start.Add(time.Second), + Samples: []FinalitySample{{SubmittedAt: start, FinalizedAt: start.Add(50 * time.Millisecond)}}, + }) + if err != nil { + t.Fatal(err) + } + raw, err := report.JSON() + if err != nil { + t.Fatal(err) + } + var decoded map[string]any + if err := json.Unmarshal(raw, &decoded); err != nil { + t.Fatal(err) + } + if decoded["valid"] != true || decoded["finalized_tps"] == nil || decoded["config"] == nil || decoded["environment"] == nil { + t.Fatalf("incomplete JSON report: %s", raw) + } +} From 3da1db69d59c51f36b99833a5ff021258c734f18 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:33:28 +0200 Subject: [PATCH 17/37] Refresh Protocol Economics P1 status --- docs/protocol-economics-p1-status.md | 38 ++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/docs/protocol-economics-p1-status.md b/docs/protocol-economics-p1-status.md index 5667c872..cf864e60 100644 --- a/docs/protocol-economics-p1-status.md +++ b/docs/protocol-economics-p1-status.md @@ -2,23 +2,39 @@ Working paper reference: 19 August 2026, v0.1. -This tranche is shadow-only and does not activate monetary supply changes. +This tranche is **shadow-only**. It does not activate live monetary supply changes, an idle-capital levy, reward-routing changes or production benchmark claims. -Implemented here: +## Implemented in this branch - dynamic per-work-class ZCU references from verified slot evidence; - deterministic weighted median, EWMA smoothing and per-epoch rate limiting; - ZPPI price-relative basket for compute, data availability and storage; - reliability/coverage gates for thin economic data; -- canonical annual ZPPI factor `1_020_408_163`, derived from a 2% purchasing-power decline; -- tokenomics documentation migrated away from a fixed 2% supply-growth objective. +- canonical annual ZPPI factor `1_020_408_163`, derived from the ~2% annual purchasing-power objective; +- tokenomics documentation migrated away from a fixed 2% supply-growth objective; +- capital-lot lineage primitives, dormancy histograms and productive-coverage measurement; +- deterministic state carrying-cost estimation and fragmentation-pressure simulation; +- prospective `IdleCapitalTracker` with amount-conserving lineage, canonical checkpoints, local/cross-shard targets and explicit productive-use hooks; +- repository tests for fragmentation stability, target-order canonicality, cross-shard materialization, productive coverage, checkpoint round trips and atomic invalid-transition rejection; +- finalized-through-consensus benchmark report core with warm-up separation, measured finalized TPS, p50/p95/p99 finality, environment/config metadata, JSON output and safety/liveness invalidation. -The pre-existing supply-growth controller remains a legacy shadow simulator until ZPPI is integrated into the monetary epoch path. +The pre-existing supply-growth controller remains a legacy **shadow simulator** until the ZPPI path is integrated into the monetary epoch path. ZCSI remains primarily a compute incentive/capacity-routing signal rather than the monetary anchor. -Next P1 tranche: +## Activation discipline -- dormancy histogram; -- ProductiveCoverage hooks; -- capital-lineage metadata; -- fragmentation/state carrying-cost simulation; -- adversarial wallet-splitting scenarios. +```text +measure first -> simulate second -> activate last +``` + +No live mint, burn policy, levy or reward-routing change is implied by the presence of telemetry or simulation code. + +## Remaining P1 work + +- add the extended adversarial tracker suite for repeated self-cycling, payment/change outputs, mixed-age merge, materialization replay and checkpoint malformed-state coverage; +- integrate `IdleCapitalTracker` into the finalized `EpochCollector` as an opt-in shadow component; +- derive local and cross-shard lineage only from finalized transaction/result/witness evidence; +- use canonical cross-shard receipt identity for tracker transfer IDs and protocol-defined destination object identity on import; +- integrate tracker state into collector checkpoint/restore with explicit versioning and restart tests; +- connect the finalized benchmark report to real V2 Lab finality/QC samples; +- add a small deterministic CI benchmark scenario and a heavier manual shard/cross-shard matrix; +- publish no new chain-TPS claim until the finalized-through-consensus harness has actually run under a documented configuration. From e7ebf09b58434ee87b49e1a080da7fe9d1a4411b Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:33:55 +0200 Subject: [PATCH 18/37] Expand idle capital tracker edge coverage --- .../v2/economics/idle_tracker_edge_test.go | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 internal/v2/economics/idle_tracker_edge_test.go diff --git a/internal/v2/economics/idle_tracker_edge_test.go b/internal/v2/economics/idle_tracker_edge_test.go new file mode 100644 index 00000000..73b1e956 --- /dev/null +++ b/internal/v2/economics/idle_tracker_edge_test.go @@ -0,0 +1,116 @@ +package economics + +import ( + "bytes" + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestIdleCapitalTrackerRepeatedSelfCyclingPreservesDormancy(t *testing.T) { + tracker := NewIdleCapitalTracker() + current := trackerObjectID("repeat-cycle-0") + if err := tracker.BootstrapObject(current, 50_000, 7); err != nil { + t.Fatal(err) + } + for i := 1; i <= 128; i++ { + next := trackerObjectIDWithIndex("repeat-cycle", i) + if err := tracker.ApplyTransition([]types.ObjectID{current}, []CapitalTarget{{ObjectID: next, Amount: 50_000}}, 0); err != nil { + t.Fatalf("cycle %d: %v", i, err) + } + current = next + } + lots, ok := tracker.ObjectLots(current) + if !ok || len(lots) != 1 || lots[0].Amount != 50_000 || lots[0].IdleSinceHeight != 7 { + t.Fatalf("repeated self-cycling changed dormancy: %#v", lots) + } +} + +func TestIdleCapitalTrackerChangeOutputPreservesInputAge(t *testing.T) { + tracker := NewIdleCapitalTracker() + input := trackerObjectID("change-input") + if err := tracker.BootstrapObject(input, 1_000, 20); err != nil { + t.Fatal(err) + } + payment := trackerObjectID("change-payment") + change := trackerObjectID("change-output") + if err := tracker.ApplyTransition([]types.ObjectID{input}, []CapitalTarget{ + {ObjectID: payment, Amount: 350}, + {ObjectID: change, Amount: 650}, + }, 0); err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + id types.ObjectID + amount uint64 + }{{payment, 350}, {change, 650}} { + lots, ok := tracker.ObjectLots(tc.id) + if !ok || len(lots) != 1 || lots[0].Amount != tc.amount || lots[0].IdleSinceHeight != 20 { + t.Fatalf("change split reset age for %s: %#v", tc.id.String(), lots) + } + } +} + +func TestIdleCapitalTrackerMixedAgeMergeKeepsBothAges(t *testing.T) { + tracker := NewIdleCapitalTracker() + oldID := trackerObjectID("mixed-old") + youngID := trackerObjectID("mixed-young") + if err := tracker.BootstrapObject(oldID, 400, 10); err != nil { + t.Fatal(err) + } + if err := tracker.BootstrapObject(youngID, 600, 90); err != nil { + t.Fatal(err) + } + merged := trackerObjectID("mixed-merged") + if err := tracker.ApplyTransition([]types.ObjectID{oldID, youngID}, []CapitalTarget{{ObjectID: merged, Amount: 1_000}}, 0); err != nil { + t.Fatal(err) + } + lots, ok := tracker.ObjectLots(merged) + if !ok || len(lots) != 2 { + t.Fatalf("mixed-age merge lost lineage: %#v", lots) + } + var oldAmount, youngAmount uint64 + for _, lot := range lots { + switch lot.IdleSinceHeight { + case 10: + oldAmount += lot.Amount + case 90: + youngAmount += lot.Amount + default: + t.Fatalf("merge invented idle height: %#v", lot) + } + } + if oldAmount != 400 || youngAmount != 600 { + t.Fatalf("merge changed age-weighted capital: old=%d young=%d lots=%#v", oldAmount, youngAmount, lots) + } +} + +func TestIdleCapitalTrackerSecondMaterializationDoesNotMutateState(t *testing.T) { + tracker := NewIdleCapitalTracker() + seed := trackerObjectID("materialize-seed") + if err := tracker.BootstrapObject(seed, 777, 33); err != nil { + t.Fatal(err) + } + transferID := types.HashBytes("idle-capital/test-transfer", []byte("materialize")) + if err := tracker.ApplyTransition([]types.ObjectID{seed}, []CapitalTarget{{TransferID: transferID, Amount: 777}}, 0); err != nil { + t.Fatal(err) + } + destination := trackerObjectID("materialize-destination") + if err := tracker.MaterializeTransfer(transferID, destination); err != nil { + t.Fatal(err) + } + before, err := tracker.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + if err := tracker.MaterializeTransfer(transferID, trackerObjectID("materialize-second")); err == nil { + t.Fatal("second materialization accepted") + } + after, err := tracker.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(before, after) { + t.Fatal("failed second materialization mutated tracker") + } +} From f76947e2efb5d26a652b70f94f646b45fca12564 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:37:39 +0200 Subject: [PATCH 19/37] Derive idle lineage from finalized transfer evidence --- .../v2/economics/finalized_idle_capital.go | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 internal/v2/economics/finalized_idle_capital.go diff --git a/internal/v2/economics/finalized_idle_capital.go b/internal/v2/economics/finalized_idle_capital.go new file mode 100644 index 00000000..09ca022b --- /dev/null +++ b/internal/v2/economics/finalized_idle_capital.go @@ -0,0 +1,250 @@ +package economics + +import ( + "bytes" + "math" + + "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" +) + +// ApplyFinalizedIdleCapitalTransfer derives shadow capital-lineage movement +// only from a finalized native transfer and its canonical exported receipts. +// It deliberately refuses to invent cross-shard transfer identifiers from the +// execution result: every cross-shard target is keyed by CrossShardReceipt.Hash. +// +// The helper is prospective. A previously unseen native input can be +// bootstrapped only when its consensus-stamped CreatedHeight is known. If all +// unseen inputs have unknown historical age the observation is skipped without +// mutation. Mixing already-tracked capital with an unknown-age input fails +// closed because silently dropping either lineage would make telemetry false. +func ApplyFinalizedIdleCapitalTransfer( + tracker *IdleCapitalTracker, + height uint64, + nativeToken types.TokenID, + transaction tx.Transaction, + result execution.Result, + exports []sharding.CrossShardReceipt, +) (bool, error) { + if tracker == nil || height == 0 || types.IsZero32([32]byte(nativeToken)) || result.TxID != transaction.ID() { + return false, ErrFinalizedEconomics + } + if len(transaction.Operations) != 1 || transaction.Operations[0].Kind != tx.OpTransfer { + return false, nil + } + + preview := tracker.Clone() + if preview == nil { + return false, ErrFinalizedEconomics + } + consumed := make(map[types.ObjectID]struct{}, len(result.Consumed)) + for _, id := range result.Consumed { + if types.IsZero32([32]byte(id)) { + return false, ErrFinalizedEconomics + } + consumed[id] = struct{}{} + } + + inputs := make([]types.ObjectID, 0, len(transaction.Witnesses)) + trackedInput := false + unknownAgeInput := false + for _, witness := range transaction.Witnesses { + if _, ok := consumed[witness.Object.ID]; !ok || witness.Object.Kind != object.KindCoin { + continue + } + coin, err := object.ParseCoin(witness.Object.Data) + if err != nil { + return false, err + } + if coin.Token != nativeToken { + continue + } + inputs = append(inputs, witness.Object.ID) + if lots, ok := preview.ObjectLots(witness.Object.ID); ok { + trackedInput = true + amount, err := idleCapitalLotsAmount(lots) + if err != nil || amount != coin.Amount { + return false, ErrFinalizedEconomics + } + continue + } + if coin.CreatedHeight == 0 { + unknownAgeInput = true + continue + } + if err := preview.BootstrapObject(witness.Object.ID, coin.Amount, coin.CreatedHeight); err != nil { + return false, err + } + } + if len(inputs) == 0 { + return false, nil + } + if unknownAgeInput { + if trackedInput { + return false, ErrFinalizedEconomics + } + return false, nil + } + + targets := make([]CapitalTarget, 0, len(result.Created)+len(exports)) + for _, created := range result.Created { + if created.Kind != object.KindCoin { + continue + } + coin, err := object.ParseCoin(created.Data) + if err != nil { + return false, err + } + if coin.Token != nativeToken { + continue + } + if coin.CreatedHeight != height || types.IsZero32([32]byte(created.ID)) { + return false, ErrFinalizedEconomics + } + targets = append(targets, CapitalTarget{ObjectID: created.ID, Amount: coin.Amount}) + } + + type exportKey struct { + destination uint32 + index uint32 + } + expected := make(map[exportKey]execution.OutboundOutput, len(result.Outbound)) + for _, outbound := range result.Outbound { + if outbound.Output.Kind != object.KindCoin { + continue + } + coin, err := object.ParseCoin(outbound.Output.Data) + if err != nil { + return false, err + } + if coin.Token != nativeToken { + continue + } + if coin.CreatedHeight != height || outbound.DestinationShard == transaction.ShardID { + return false, ErrFinalizedEconomics + } + key := exportKey{destination: outbound.DestinationShard, index: outbound.OutputIndex} + if _, duplicate := expected[key]; duplicate { + return false, ErrFinalizedEconomics + } + expected[key] = outbound + } + + seenExports := make(map[exportKey]struct{}, len(exports)) + for _, receipt := range exports { + if err := receipt.Validate(); err != nil || receipt.SourceShard != transaction.ShardID || + receipt.SourceHeight != height || receipt.TransactionID != result.TxID { + return false, ErrFinalizedEconomics + } + key := exportKey{destination: receipt.DestinationShard, index: receipt.OutputIndex} + if _, duplicate := seenExports[key]; duplicate { + return false, ErrFinalizedEconomics + } + seenExports[key] = struct{}{} + outbound, ok := expected[key] + if !ok || !bytes.Equal(outbound.Output.CanonicalBytes(), receipt.Output.CanonicalBytes()) { + return false, ErrFinalizedEconomics + } + coin, err := object.ParseCoin(receipt.Output.Data) + if err != nil || coin.Token != nativeToken || coin.CreatedHeight != height { + return false, ErrFinalizedEconomics + } + transferID, err := receipt.Hash() + if err != nil { + return false, err + } + targets = append(targets, CapitalTarget{TransferID: transferID, Amount: coin.Amount}) + delete(expected, key) + } + if len(expected) != 0 { + return false, ErrFinalizedEconomics + } + + // The collector currently requires CompatibilityFeePolicy, so the full + // native transaction fee is a real finalized burn rather than a reward or + // reserve transfer that would require its own capital target. + if err := preview.ApplyTransition(inputs, targets, transaction.Fee); err != nil { + return false, err + } + *tracker = *preview + return true, nil +} + +// ApplyFinalizedIdleCapitalImport materializes a previously tracked pending +// cross-shard lineage using the consensus-canonical receipt hash and protocol +// destination object identity. If tracking began after the export, the +// destination is prospectively bootstrapped from the finalized stamped coin. +func ApplyFinalizedIdleCapitalImport( + tracker *IdleCapitalTracker, + nativeToken types.TokenID, + receipt sharding.CrossShardReceipt, +) (bool, error) { + if tracker == nil || types.IsZero32([32]byte(nativeToken)) { + return false, ErrFinalizedEconomics + } + if err := receipt.Validate(); err != nil { + return false, ErrFinalizedEconomics + } + if receipt.Output.Kind != object.KindCoin { + return false, nil + } + coin, err := object.ParseCoin(receipt.Output.Data) + if err != nil { + return false, err + } + if coin.Token != nativeToken { + return false, nil + } + if coin.CreatedHeight == 0 || coin.CreatedHeight != receipt.SourceHeight { + return false, ErrFinalizedEconomics + } + transferID, err := receipt.Hash() + if err != nil { + return false, err + } + destination, err := receipt.DestinationObject() + if err != nil { + return false, err + } + + preview := tracker.Clone() + if preview == nil { + return false, ErrFinalizedEconomics + } + if lots, ok := preview.pendingTransfers[transferID]; ok { + amount, err := idleCapitalLotsAmount(lots) + if err != nil || amount != coin.Amount { + return false, ErrFinalizedEconomics + } + if err := preview.MaterializeTransfer(transferID, destination.ID); err != nil { + return false, err + } + *tracker = *preview + return true, nil + } + if _, exists := preview.objects[destination.ID]; exists { + return false, ErrFinalizedEconomics + } + if err := preview.BootstrapObject(destination.ID, coin.Amount, coin.CreatedHeight); err != nil { + return false, err + } + *tracker = *preview + return true, nil +} + +func idleCapitalLotsAmount(lots []CapitalLot) (uint64, error) { + if len(lots) == 0 { + return 0, ErrFinalizedEconomics + } + var total uint64 + for _, lot := range lots { + if err := lot.Validate(); err != nil || math.MaxUint64-total < lot.Amount { + return 0, ErrFinalizedEconomics + } + total += lot.Amount + } + return total, nil +} From f9ef79df999465e0a2892f3c7811292650541def Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:38:12 +0200 Subject: [PATCH 20/37] Test finalized idle lineage derivation --- .../economics/finalized_idle_capital_test.go | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 internal/v2/economics/finalized_idle_capital_test.go diff --git a/internal/v2/economics/finalized_idle_capital_test.go b/internal/v2/economics/finalized_idle_capital_test.go new file mode 100644 index 00000000..88f656b3 --- /dev/null +++ b/internal/v2/economics/finalized_idle_capital_test.go @@ -0,0 +1,252 @@ +package economics + +import ( + "testing" + + "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 TestFinalizedIdleCapitalTransferUsesCanonicalReceiptIdentity(t *testing.T) { + native := types.TokenID{1} + inputID := types.ObjectID{2} + owner := types.AccountID{3} + input := object.Object{ + ID: inputID, Version: 1, Owner: owner, Kind: object.KindCoin, + Data: object.Coin{Token: native, Amount: 1_000, CreatedHeight: 10}.MarshalBinary(), + } + localOutput, err := object.NewCoinOutputAtHeight(types.AccountID{4}, native, 600, 100) + if err != nil { + t.Fatal(err) + } + crossOutput, err := object.NewCoinOutputAtHeight(types.AccountID{5}, native, 390, 100) + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{ + ShardID: 0, + Inputs: []tx.InputRef{{ObjectID: inputID}}, + Outputs: []object.OutputSpec{localOutput, crossOutput}, + Operations: []tx.Operation{{Kind: tx.OpTransfer}}, + Fee: 10, + Witnesses: []tx.Witness{{Object: input}}, + } + txID := transaction.ID() + localID := types.ObjectIDForShard(txID, 0, 0) + result := execution.Result{ + Consumed: []types.ObjectID{inputID}, + Created: []object.Object{{ + ID: localID, Version: 1, Owner: localOutput.Owner, Kind: localOutput.Kind, + Data: append([]byte(nil), localOutput.Data...), + }}, + Outbound: []execution.OutboundOutput{{DestinationShard: 1, OutputIndex: 1, Output: crossOutput}}, + TxID: txID, + } + receipt := sharding.CrossShardReceipt{ + SourceShard: 0, DestinationShard: 1, SourceHeight: 100, + TransactionID: txID, OutputIndex: 1, Output: crossOutput, SourceStateRoot: types.Hash{9}, + } + tracker := NewIdleCapitalTracker() + applied, err := ApplyFinalizedIdleCapitalTransfer(tracker, 100, native, transaction, result, []sharding.CrossShardReceipt{receipt}) + if err != nil { + t.Fatal(err) + } + if !applied { + t.Fatal("finalized transfer was not applied") + } + receiptHash, err := receipt.Hash() + if err != nil { + t.Fatal(err) + } + pending, ok := tracker.pendingTransfers[receiptHash] + if !ok { + t.Fatal("tracker did not use canonical receipt hash as transfer identity") + } + if amount, err := idleCapitalLotsAmount(pending); err != nil || amount != 390 { + t.Fatalf("unexpected pending lineage amount: %d %v", amount, err) + } + localLots, ok := tracker.ObjectLots(localID) + if !ok || len(localLots) != 1 || localLots[0].Amount != 600 || localLots[0].IdleSinceHeight != 10 { + t.Fatalf("local output lost input lineage age: %#v", localLots) + } + snapshot, err := tracker.Snapshot(100, []uint64{20, 100}) + if err != nil { + t.Fatal(err) + } + if snapshot.TrackedCapital != 990 || snapshot.PendingTransfers != 1 || snapshot.LiveObjects != 1 { + t.Fatalf("unexpected post-export snapshot: %#v", snapshot) + } + + imported, err := ApplyFinalizedIdleCapitalImport(tracker, native, receipt) + if err != nil { + t.Fatal(err) + } + if !imported { + t.Fatal("finalized import was not materialized") + } + destination, err := receipt.DestinationObject() + if err != nil { + t.Fatal(err) + } + destinationLots, ok := tracker.ObjectLots(destination.ID) + if !ok || len(destinationLots) != 1 || destinationLots[0].Amount != 390 || destinationLots[0].IdleSinceHeight != 10 { + t.Fatalf("cross-shard import lost lineage age: %#v", destinationLots) + } +} + +func TestFinalizedIdleCapitalRejectsReceiptMismatchAtomically(t *testing.T) { + native := types.TokenID{1} + inputID := types.ObjectID{2} + input := object.Object{ + ID: inputID, Version: 1, Owner: types.AccountID{3}, Kind: object.KindCoin, + Data: object.Coin{Token: native, Amount: 100, CreatedHeight: 5}.MarshalBinary(), + } + crossOutput, err := object.NewCoinOutputAtHeight(types.AccountID{4}, native, 90, 20) + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{ + ShardID: 0, Inputs: []tx.InputRef{{ObjectID: inputID}}, Outputs: []object.OutputSpec{crossOutput}, + Operations: []tx.Operation{{Kind: tx.OpTransfer}}, Fee: 10, Witnesses: []tx.Witness{{Object: input}}, + } + txID := transaction.ID() + result := execution.Result{ + Consumed: []types.ObjectID{inputID}, + Outbound: []execution.OutboundOutput{{DestinationShard: 1, OutputIndex: 0, Output: crossOutput}}, + TxID: txID, + } + bad := sharding.CrossShardReceipt{ + SourceShard: 0, DestinationShard: 1, SourceHeight: 20, + TransactionID: txID, OutputIndex: 1, Output: crossOutput, SourceStateRoot: types.Hash{8}, + } + tracker := NewIdleCapitalTracker() + if _, err := ApplyFinalizedIdleCapitalTransfer(tracker, 20, native, transaction, result, []sharding.CrossShardReceipt{bad}); err == nil { + t.Fatal("mismatched finalized receipt was accepted") + } + if len(tracker.objects) != 0 || len(tracker.pendingTransfers) != 0 { + t.Fatal("rejected receipt mismatch partially mutated tracker") + } +} + +func TestFinalizedIdleCapitalUnknownAgeBootstrapSkipsWithoutMutation(t *testing.T) { + native := types.TokenID{1} + inputID := types.ObjectID{2} + input := object.Object{ + ID: inputID, Version: 1, Owner: types.AccountID{3}, Kind: object.KindCoin, + Data: object.Coin{Token: native, Amount: 100, CreatedHeight: 0}.MarshalBinary(), + } + localOutput, err := object.NewCoinOutputAtHeight(types.AccountID{4}, native, 90, 20) + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{ + ShardID: 0, Inputs: []tx.InputRef{{ObjectID: inputID}}, Outputs: []object.OutputSpec{localOutput}, + Operations: []tx.Operation{{Kind: tx.OpTransfer}}, Fee: 10, Witnesses: []tx.Witness{{Object: input}}, + } + txID := transaction.ID() + result := execution.Result{ + Consumed: []types.ObjectID{inputID}, + Created: []object.Object{{ + ID: types.ObjectIDForShard(txID, 0, 0), Version: 1, Owner: localOutput.Owner, + Kind: localOutput.Kind, Data: append([]byte(nil), localOutput.Data...), + }}, + TxID: txID, + } + tracker := NewIdleCapitalTracker() + applied, err := ApplyFinalizedIdleCapitalTransfer(tracker, 20, native, transaction, result, nil) + if err != nil { + t.Fatal(err) + } + if applied || len(tracker.objects) != 0 || len(tracker.pendingTransfers) != 0 { + t.Fatal("unknown-age prospective bootstrap should skip without inventing history") + } +} + +func TestFinalizedIdleCapitalImportBootstrapsWhenExportPredatesTracker(t *testing.T) { + native := types.TokenID{1} + output, err := object.NewCoinOutputAtHeight(types.AccountID{2}, native, 250, 40) + if err != nil { + t.Fatal(err) + } + receipt := sharding.CrossShardReceipt{ + SourceShard: 0, DestinationShard: 1, SourceHeight: 40, + TransactionID: types.Hash{3}, OutputIndex: 2, Output: output, SourceStateRoot: types.Hash{4}, + } + tracker := NewIdleCapitalTracker() + applied, err := ApplyFinalizedIdleCapitalImport(tracker, native, receipt) + if err != nil { + t.Fatal(err) + } + if !applied { + t.Fatal("finalized import was not prospectively bootstrapped") + } + destination, err := receipt.DestinationObject() + if err != nil { + t.Fatal(err) + } + lots, ok := tracker.ObjectLots(destination.ID) + if !ok || len(lots) != 1 || lots[0].Amount != 250 || lots[0].IdleSinceHeight != 40 { + t.Fatalf("unexpected prospective import lineage: %#v", lots) + } + snapshot, err := tracker.Snapshot(50, []uint64{20, 100}) + if err != nil { + t.Fatal(err) + } + if snapshot.BootstrapCapital != 250 { + t.Fatalf("prospective import bootstrap was hidden: %#v", snapshot) + } +} + +func TestFinalizedIdleCapitalMixedTrackedAndUnknownAgeFailsClosed(t *testing.T) { + native := types.TokenID{1} + trackedID := types.ObjectID{2} + unknownID := types.ObjectID{3} + tracker := NewIdleCapitalTracker() + if err := tracker.BootstrapObject(trackedID, 60, 5); err != nil { + t.Fatal(err) + } + tracked := object.Object{ + ID: trackedID, Version: 1, Owner: types.AccountID{4}, Kind: object.KindCoin, + Data: object.Coin{Token: native, Amount: 60, CreatedHeight: 5}.MarshalBinary(), + } + unknown := object.Object{ + ID: unknownID, Version: 1, Owner: types.AccountID{4}, Kind: object.KindCoin, + Data: object.Coin{Token: native, Amount: 40, CreatedHeight: 0}.MarshalBinary(), + } + output, err := object.NewCoinOutputAtHeight(types.AccountID{5}, native, 90, 20) + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{ + ShardID: 0, + Inputs: []tx.InputRef{{ObjectID: trackedID}, {ObjectID: unknownID}}, + Outputs: []object.OutputSpec{output}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, Fee: 10, + Witnesses: []tx.Witness{{Object: tracked}, {Object: unknown}}, + } + txID := transaction.ID() + result := execution.Result{ + Consumed: []types.ObjectID{trackedID, unknownID}, + Created: []object.Object{{ + ID: types.ObjectIDForShard(txID, 0, 0), Version: 1, Owner: output.Owner, + Kind: output.Kind, Data: append([]byte(nil), output.Data...), + }}, + TxID: txID, + } + before, err := tracker.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + if _, err := ApplyFinalizedIdleCapitalTransfer(tracker, 20, native, transaction, result, nil); err == nil { + t.Fatal("mixed tracked and unknown-age capital did not fail closed") + } + after, err := tracker.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + if string(before) != string(after) { + t.Fatal("failed mixed-coverage observation mutated tracker") + } +} From 9e7e85892a8e2a8e326b06ef9b29330e3c757d3a Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:41:11 +0200 Subject: [PATCH 21/37] Format finalized idle lineage tests --- .../v2/economics/finalized_idle_capital_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/v2/economics/finalized_idle_capital_test.go b/internal/v2/economics/finalized_idle_capital_test.go index 88f656b3..cc9dbccc 100644 --- a/internal/v2/economics/finalized_idle_capital_test.go +++ b/internal/v2/economics/finalized_idle_capital_test.go @@ -27,12 +27,12 @@ func TestFinalizedIdleCapitalTransferUsesCanonicalReceiptIdentity(t *testing.T) t.Fatal(err) } transaction := tx.Transaction{ - ShardID: 0, - Inputs: []tx.InputRef{{ObjectID: inputID}}, - Outputs: []object.OutputSpec{localOutput, crossOutput}, + ShardID: 0, + Inputs: []tx.InputRef{{ObjectID: inputID}}, + Outputs: []object.OutputSpec{localOutput, crossOutput}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, - Fee: 10, - Witnesses: []tx.Witness{{Object: input}}, + Fee: 10, + Witnesses: []tx.Witness{{Object: input}}, } txID := transaction.ID() localID := types.ObjectIDForShard(txID, 0, 0) @@ -43,7 +43,7 @@ func TestFinalizedIdleCapitalTransferUsesCanonicalReceiptIdentity(t *testing.T) Data: append([]byte(nil), localOutput.Data...), }}, Outbound: []execution.OutboundOutput{{DestinationShard: 1, OutputIndex: 1, Output: crossOutput}}, - TxID: txID, + TxID: txID, } receipt := sharding.CrossShardReceipt{ SourceShard: 0, DestinationShard: 1, SourceHeight: 100, @@ -116,7 +116,7 @@ func TestFinalizedIdleCapitalRejectsReceiptMismatchAtomically(t *testing.T) { result := execution.Result{ Consumed: []types.ObjectID{inputID}, Outbound: []execution.OutboundOutput{{DestinationShard: 1, OutputIndex: 0, Output: crossOutput}}, - TxID: txID, + TxID: txID, } bad := sharding.CrossShardReceipt{ SourceShard: 0, DestinationShard: 1, SourceHeight: 20, @@ -222,7 +222,7 @@ func TestFinalizedIdleCapitalMixedTrackedAndUnknownAgeFailsClosed(t *testing.T) } transaction := tx.Transaction{ ShardID: 0, - Inputs: []tx.InputRef{{ObjectID: trackedID}, {ObjectID: unknownID}}, + Inputs: []tx.InputRef{{ObjectID: trackedID}, {ObjectID: unknownID}}, Outputs: []object.OutputSpec{output}, Operations: []tx.Operation{{Kind: tx.OpTransfer}}, Fee: 10, Witnesses: []tx.Witness{{Object: tracked}, {Object: unknown}}, } From b00016f48abe6ede99100f7d498bb7dc211c0bba Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:46:00 +0200 Subject: [PATCH 22/37] Integrate idle lineage into finalized collector --- internal/v2/economics/finalized_collector.go | 81 ++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/internal/v2/economics/finalized_collector.go b/internal/v2/economics/finalized_collector.go index 8ae896f9..6cce3f27 100644 --- a/internal/v2/economics/finalized_collector.go +++ b/internal/v2/economics/finalized_collector.go @@ -33,6 +33,7 @@ type FinalizedShardObservation struct { Transactions []tx.Transaction Results []execution.Result Imports []sharding.CrossShardReceipt + Exports []sharding.CrossShardReceipt DataBytes uint64 ComputeCapacityUnits uint64 ComputeCapacityReliable bool @@ -58,6 +59,7 @@ type EpochCollector struct { shards map[uint32]*shardEpochAccumulator supply map[uint32]uint64 verifiedWork []compute.VerifiedWork + idleCapital *IdleCapitalTracker lastHeight uint64 } @@ -95,6 +97,37 @@ func NewEpochCollector(config EpochCollectorConfig) (*EpochCollector, error) { return collector, nil } +// EnableIdleCapitalTracking enables prospective shadow lineage tracking before +// the collector has observed any finalized block. The tracker must be empty so +// enabling the feature cannot smuggle unauthenticated historical state into +// finalized telemetry. +func (c *EpochCollector) EnableIdleCapitalTracking(tracker *IdleCapitalTracker) error { + if c == nil || tracker == nil || c.lastHeight != 0 || c.idleCapital != nil || + len(tracker.objects) != 0 || len(tracker.pendingTransfers) != 0 || len(tracker.bootstrapLineages) != 0 || + tracker.productive.observedCapital != 0 || tracker.productive.weightedCoverageBps.Sign() != 0 { + return ErrFinalizedEconomics + } + c.idleCapital = tracker.Clone() + if c.idleCapital == nil { + return ErrFinalizedEconomics + } + return nil +} + +func (c *EpochCollector) IdleCapitalSnapshot(height uint64, bounds []uint64) (IdleCapitalSnapshot, bool, error) { + if c == nil { + return IdleCapitalSnapshot{}, false, ErrFinalizedEconomics + } + if c.idleCapital == nil { + return IdleCapitalSnapshot{}, false, nil + } + snapshot, err := c.idleCapital.Snapshot(height, bounds) + if err != nil { + return IdleCapitalSnapshot{}, true, err + } + return snapshot, true, nil +} + // ObserveFinalizedBlock applies a full global-block observation atomically. // Telemetry changes only after normal consensus finality and never mutates // world state or mints ZPH. @@ -122,6 +155,27 @@ func (c *EpochCollector) observeFinalizedBlock(height uint64, observations map[u if len(observation.Transactions) != len(observation.Results) { return ErrFinalizedEconomics } + if c.idleCapital != nil { + resultIDs := make(map[types.Hash]struct{}, len(observation.Results)) + for _, result := range observation.Results { + if types.IsZero32([32]byte(result.TxID)) { + return ErrFinalizedEconomics + } + if _, duplicate := resultIDs[result.TxID]; duplicate { + return ErrFinalizedEconomics + } + resultIDs[result.TxID] = struct{}{} + } + for _, receipt := range observation.Exports { + if err := receipt.Validate(); err != nil || receipt.SourceShard != shard || receipt.SourceHeight != height || + receipt.DestinationShard >= c.config.ShardCount || receipt.DestinationShard == shard { + return ErrFinalizedEconomics + } + if _, ok := resultIDs[receipt.TransactionID]; !ok { + return ErrFinalizedEconomics + } + } + } acc := c.shards[shard] if err := addTo(&acc.resourceCapacity, c.config.ResourceCapacityPerBlock[shard]); err != nil { return err @@ -140,6 +194,12 @@ func (c *EpochCollector) observeFinalizedBlock(height uint64, observations map[u if err := c.observeFinalizedTransaction(height, shard, transaction, result, acc); err != nil { return err } + if c.idleCapital != nil { + exports := finalizedReceiptsForTransaction(observation.Exports, result.TxID) + if _, err := ApplyFinalizedIdleCapitalTransfer(c.idleCapital, height, c.config.NativeToken, transaction, result, exports); err != nil { + return err + } + } } for _, receipt := range observation.Imports { if receipt.DestinationShard != shard || receipt.SourceShard >= c.config.ShardCount || receipt.SourceShard == receipt.DestinationShard { @@ -148,6 +208,11 @@ func (c *EpochCollector) observeFinalizedBlock(height uint64, observations map[u if err := c.observeImportedReceipt(receipt); err != nil { return err } + if c.idleCapital != nil { + if _, err := ApplyFinalizedIdleCapitalImport(c.idleCapital, c.config.NativeToken, receipt); err != nil { + return err + } + } if err := addTo(&acc.operations, 1); err != nil { return err } @@ -172,6 +237,16 @@ func (c *EpochCollector) observeFinalizedBlock(height uint64, observations map[u return nil } +func finalizedReceiptsForTransaction(receipts []sharding.CrossShardReceipt, transactionID types.Hash) []sharding.CrossShardReceipt { + out := make([]sharding.CrossShardReceipt, 0) + for _, receipt := range receipts { + if receipt.TransactionID == transactionID { + out = append(out, receipt) + } + } + return out +} + 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 { @@ -392,6 +467,11 @@ func (c *EpochCollector) AdvanceEpoch(next uint64) error { } c.config.Epoch = next c.verifiedWork = nil + if c.idleCapital != nil { + if err := c.idleCapital.ResetProductiveCoverage(); err != nil { + return err + } + } for shard := uint32(0); shard < c.config.ShardCount; shard++ { prior := c.shards[shard] velocity, err := NewVelocityAccumulator(c.config.VelocityPolicy) @@ -424,6 +504,7 @@ func (c *EpochCollector) clone() *EpochCollector { shards: make(map[uint32]*shardEpochAccumulator, len(c.shards)), supply: make(map[uint32]uint64, len(c.supply)), verifiedWork: append([]compute.VerifiedWork(nil), c.verifiedWork...), + idleCapital: c.idleCapital.Clone(), lastHeight: c.lastHeight, } for shard, amount := range c.supply { From f43558e1e3e73ffc2c58968be25ab9cff218647b Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:46:37 +0200 Subject: [PATCH 23/37] Checkpoint idle capital tracker state --- .../finalized_collector_checkpoint.go | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/internal/v2/economics/finalized_collector_checkpoint.go b/internal/v2/economics/finalized_collector_checkpoint.go index d28ab30e..b3e85709 100644 --- a/internal/v2/economics/finalized_collector_checkpoint.go +++ b/internal/v2/economics/finalized_collector_checkpoint.go @@ -9,9 +9,11 @@ import ( ) const ( - collectorCheckpointVersion uint16 = 1 - maxCheckpointVerifiedWork = 1_000_000 - maxVelocityAccumulatorBytes = 128 + collectorCheckpointVersion uint16 = 2 + collectorCheckpointVersionLegacy uint16 = 1 + maxCheckpointVerifiedWork = 1_000_000 + maxVelocityAccumulatorBytes = 128 + maxIdleCapitalCheckpointBytes = 48 << 20 ) // CheckpointBytes serializes the exact mid-epoch collector state. The workload @@ -77,13 +79,21 @@ func (c *EpochCollector) CheckpointBytes() ([]byte, error) { return nil, err } } + w.Bool(c.idleCapital != nil) + if c.idleCapital != nil { + idleRaw, err := c.idleCapital.CheckpointBytes() + if err != nil || len(idleRaw) > maxIdleCapitalCheckpointBytes { + return nil, ErrFinalizedEconomics + } + w.Bytes(idleRaw) + } 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 { + if err != nil || (version != collectorCheckpointVersionLegacy && version != collectorCheckpointVersion) { return nil, ErrFinalizedEconomics } epoch, err := r.U64() @@ -260,6 +270,23 @@ func RestoreEpochCollector(data []byte, registry *compute.WorkRegistry) (*EpochC return nil, err } } + var idleCapital *IdleCapitalTracker + if version == collectorCheckpointVersion { + hasIdleCapital, err := r.Bool() + if err != nil { + return nil, ErrFinalizedEconomics + } + if hasIdleCapital { + idleRaw, err := r.Bytes(maxIdleCapitalCheckpointBytes) + if err != nil { + return nil, ErrFinalizedEconomics + } + idleCapital, err = RestoreIdleCapitalTracker(idleRaw) + if err != nil { + return nil, err + } + } + } if r.Done() != nil { return nil, ErrFinalizedEconomics } @@ -277,7 +304,7 @@ func RestoreEpochCollector(data []byte, registry *compute.WorkRegistry) (*EpochC FeePolicy: feePolicy, WorkRegistry: ownedRegistry, }, - shards: shards, supply: supply, verifiedWork: verified, lastHeight: lastHeight, + shards: shards, supply: supply, verifiedWork: verified, idleCapital: idleCapital, lastHeight: lastHeight, }, nil } From 38123e3b919a066497a0f9a2a8aaecd01b4da559 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:47:12 +0200 Subject: [PATCH 24/37] Pass canonical exports into economic observations --- internal/v2/node/runtime.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/v2/node/runtime.go b/internal/v2/node/runtime.go index 6c662085..22a09716 100644 --- a/internal/v2/node/runtime.go +++ b/internal/v2/node/runtime.go @@ -195,6 +195,7 @@ func (r *Runtime) BuildCandidate(height uint64, batches map[uint32]ShardBatch) ( Transactions: append([]tx.Transaction(nil), batch.Transactions...), Results: append([]execution.Result(nil), results...), Imports: imports, + Exports: append([]sharding.CrossShardReceipt(nil), receipts...), } } } From 26dc8087d45ee9c06bf75cabbfde94992ad2f0b1 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:47:30 +0200 Subject: [PATCH 25/37] Test finalized idle capital collector integration --- .../finalized_collector_idle_test.go | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 internal/v2/economics/finalized_collector_idle_test.go diff --git a/internal/v2/economics/finalized_collector_idle_test.go b/internal/v2/economics/finalized_collector_idle_test.go new file mode 100644 index 00000000..ae6103f4 --- /dev/null +++ b/internal/v2/economics/finalized_collector_idle_test.go @@ -0,0 +1,188 @@ +package economics + +import ( + "bytes" + "testing" + + "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 TestEpochCollectorIdleCapitalTracksFinalizedLocalTransferAndCheckpoint(t *testing.T) { + native := types.TokenID{1} + cfg := testCollectorConfig(1, native) + cfg.InitialCirculatingSupply[0] = 1_000 + collector, err := NewEpochCollector(cfg) + if err != nil { + t.Fatal(err) + } + if err := collector.EnableIdleCapitalTracking(NewIdleCapitalTracker()); err != nil { + t.Fatal(err) + } + + inputID := types.ObjectID{2} + input := object.Object{ + ID: inputID, Version: 1, Owner: types.AccountID{3}, Kind: object.KindCoin, + Data: object.Coin{Token: native, Amount: 1_000, CreatedHeight: 10}.MarshalBinary(), + } + output, err := object.NewCoinOutputAtHeight(types.AccountID{4}, native, 990, 100) + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{ + ShardID: 0, Inputs: []tx.InputRef{{ObjectID: inputID}}, Outputs: []object.OutputSpec{output}, + Operations: []tx.Operation{{Kind: tx.OpTransfer}}, Fee: 10, Witnesses: []tx.Witness{{Object: input}}, + } + txID := transaction.ID() + outputID := types.ObjectIDForShard(txID, 0, 0) + result := execution.Result{ + Consumed: []types.ObjectID{inputID}, + Created: []object.Object{{ + ID: outputID, Version: 1, Owner: output.Owner, Kind: output.Kind, Data: append([]byte(nil), output.Data...), + }}, + TxID: txID, + } + if err := collector.ObserveFinalizedBlock(100, map[uint32]FinalizedShardObservation{ + 0: {Transactions: []tx.Transaction{transaction}, Results: []execution.Result{result}}, + }); err != nil { + t.Fatal(err) + } + snapshot, enabled, err := collector.IdleCapitalSnapshot(100, []uint64{20, 100}) + if err != nil { + t.Fatal(err) + } + if !enabled || snapshot.TrackedCapital != 990 || snapshot.BootstrapCapital != 990 || snapshot.LiveObjects != 1 { + t.Fatalf("unexpected collector idle-capital snapshot: %#v enabled=%v", snapshot, enabled) + } + lots, ok := collector.idleCapital.ObjectLots(outputID) + if !ok || len(lots) != 1 || lots[0].IdleSinceHeight != 10 || lots[0].Amount != 990 { + t.Fatalf("collector lost finalized lineage: %#v", lots) + } + + raw, err := collector.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + restored, err := RestoreEpochCollector(raw, nil) + if err != nil { + t.Fatal(err) + } + rawAgain, err := restored.CheckpointBytes() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(raw, rawAgain) { + t.Fatal("idle-capital collector checkpoint is not canonical across restore") + } + restoredLots, ok := restored.idleCapital.ObjectLots(outputID) + if !ok || len(restoredLots) != 1 || restoredLots[0].IdleSinceHeight != 10 || restoredLots[0].Amount != 990 { + t.Fatalf("collector checkpoint lost idle lineage: %#v", restoredLots) + } +} + +func TestEpochCollectorIdleCapitalTracksCanonicalCrossShardExportAndImport(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) + } + if err := collector.EnableIdleCapitalTracking(NewIdleCapitalTracker()); err != nil { + t.Fatal(err) + } + + inputID := types.ObjectID{2} + input := object.Object{ + ID: inputID, Version: 1, Owner: types.AccountID{3}, Kind: object.KindCoin, + Data: object.Coin{Token: native, Amount: 1_000, CreatedHeight: 10}.MarshalBinary(), + } + crossOutput, err := object.NewCoinOutputAtHeight(types.AccountID{4}, native, 990, 100) + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{ + ShardID: 0, Inputs: []tx.InputRef{{ObjectID: inputID}}, Outputs: []object.OutputSpec{crossOutput}, + Operations: []tx.Operation{{Kind: tx.OpTransfer}}, Fee: 10, Witnesses: []tx.Witness{{Object: input}}, + } + txID := transaction.ID() + result := execution.Result{ + Consumed: []types.ObjectID{inputID}, + Outbound: []execution.OutboundOutput{{DestinationShard: 1, OutputIndex: 0, Output: crossOutput}}, + TxID: txID, + } + receipt := sharding.CrossShardReceipt{ + SourceShard: 0, DestinationShard: 1, SourceHeight: 100, + TransactionID: txID, OutputIndex: 0, Output: crossOutput, SourceStateRoot: types.Hash{9}, + } + if err := collector.ObserveFinalizedBlock(100, map[uint32]FinalizedShardObservation{ + 0: {Transactions: []tx.Transaction{transaction}, Results: []execution.Result{result}, Exports: []sharding.CrossShardReceipt{receipt}}, + }); err != nil { + t.Fatal(err) + } + receiptHash, err := receipt.Hash() + if err != nil { + t.Fatal(err) + } + if _, ok := collector.idleCapital.pendingTransfers[receiptHash]; !ok { + t.Fatal("collector did not retain canonical pending receipt lineage") + } + if err := collector.ObserveFinalizedBlock(101, map[uint32]FinalizedShardObservation{ + 1: {Imports: []sharding.CrossShardReceipt{receipt}}, + }); err != nil { + t.Fatal(err) + } + destination, err := receipt.DestinationObject() + if err != nil { + t.Fatal(err) + } + lots, ok := collector.idleCapital.ObjectLots(destination.ID) + if !ok || len(lots) != 1 || lots[0].IdleSinceHeight != 10 || lots[0].Amount != 990 { + t.Fatalf("collector cross-shard import lost lineage: %#v", lots) + } + if len(collector.idleCapital.pendingTransfers) != 0 { + t.Fatal("collector retained pending transfer after finalized import") + } +} + +func TestEpochCollectorIdleCapitalRequiresExportsForTrackedCrossShardTransfer(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) + } + if err := collector.EnableIdleCapitalTracking(NewIdleCapitalTracker()); err != nil { + t.Fatal(err) + } + inputID := types.ObjectID{2} + input := object.Object{ + ID: inputID, Version: 1, Owner: types.AccountID{3}, Kind: object.KindCoin, + Data: object.Coin{Token: native, Amount: 100, CreatedHeight: 5}.MarshalBinary(), + } + output, err := object.NewCoinOutputAtHeight(types.AccountID{4}, native, 90, 20) + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{ + ShardID: 0, Inputs: []tx.InputRef{{ObjectID: inputID}}, Outputs: []object.OutputSpec{output}, + Operations: []tx.Operation{{Kind: tx.OpTransfer}}, Fee: 10, Witnesses: []tx.Witness{{Object: input}}, + } + result := execution.Result{ + Consumed: []types.ObjectID{inputID}, + Outbound: []execution.OutboundOutput{{DestinationShard: 1, OutputIndex: 0, Output: output}}, + TxID: transaction.ID(), + } + if err := collector.ObserveFinalizedBlock(20, map[uint32]FinalizedShardObservation{ + 0: {Transactions: []tx.Transaction{transaction}, Results: []execution.Result{result}}, + }); err == nil { + t.Fatal("tracked cross-shard transfer without canonical export receipt was accepted") + } + if collector.LastHeight() != 0 || len(collector.idleCapital.objects) != 0 || len(collector.idleCapital.pendingTransfers) != 0 { + t.Fatal("rejected missing-export observation partially mutated collector") + } +} From 0ccc05a99e65ae67298e87e2408b94c485a7e7dc Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:48:02 +0200 Subject: [PATCH 26/37] Update Protocol Economics P1 implementation status --- docs/protocol-economics-p1-status.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/protocol-economics-p1-status.md b/docs/protocol-economics-p1-status.md index cf864e60..84de185b 100644 --- a/docs/protocol-economics-p1-status.md +++ b/docs/protocol-economics-p1-status.md @@ -15,7 +15,13 @@ This tranche is **shadow-only**. It does not activate live monetary supply chang - capital-lot lineage primitives, dormancy histograms and productive-coverage measurement; - deterministic state carrying-cost estimation and fragmentation-pressure simulation; - prospective `IdleCapitalTracker` with amount-conserving lineage, canonical checkpoints, local/cross-shard targets and explicit productive-use hooks; -- repository tests for fragmentation stability, target-order canonicality, cross-shard materialization, productive coverage, checkpoint round trips and atomic invalid-transition rejection; +- adversarial/edge coverage for repeated self-cycling, payment/change outputs, mixed-age merge, fragmentation, duplicate materialization and malformed/invalid transition handling; +- finalized-evidence lineage derivation from transaction/result/witness data without heuristic transfer identifiers; +- canonical cross-shard tracker identity using `CrossShardReceipt.Hash()` and protocol `DestinationObject()` identity; +- opt-in `IdleCapitalTracker` integration inside the finalized `EpochCollector` preview/apply path; +- canonical export receipts passed from the runtime into finalized economic observations after the source state root is known; +- collector checkpoint version 2 carrying idle-capital state while retaining restore support for version 1 checkpoints; +- restart/canonical checkpoint and local/cross-shard collector integration tests; - finalized-through-consensus benchmark report core with warm-up separation, measured finalized TPS, p50/p95/p99 finality, environment/config metadata, JSON output and safety/liveness invalidation. The pre-existing supply-growth controller remains a legacy **shadow simulator** until the ZPPI path is integrated into the monetary epoch path. ZCSI remains primarily a compute incentive/capacity-routing signal rather than the monetary anchor. @@ -30,11 +36,9 @@ No live mint, burn policy, levy or reward-routing change is implied by the prese ## Remaining P1 work -- add the extended adversarial tracker suite for repeated self-cycling, payment/change outputs, mixed-age merge, materialization replay and checkpoint malformed-state coverage; -- integrate `IdleCapitalTracker` into the finalized `EpochCollector` as an opt-in shadow component; -- derive local and cross-shard lineage only from finalized transaction/result/witness evidence; -- use canonical cross-shard receipt identity for tracker transfer IDs and protocol-defined destination object identity on import; -- integrate tracker state into collector checkpoint/restore with explicit versioning and restart tests; +- connect productive-coverage updates only to explicitly verified productive events, beginning with narrow finalized compute escrow/settlement evidence rather than generic transfers or contract calls; +- expand restart/fault scenarios around collector/runtime checkpoints and cross-shard lineage as the shadow tracker accumulates larger state; - connect the finalized benchmark report to real V2 Lab finality/QC samples; - add a small deterministic CI benchmark scenario and a heavier manual shard/cross-shard matrix; +- run long-horizon public/adversarial economic simulations before proposing any monetary or idle-capital activation; - publish no new chain-TPS claim until the finalized-through-consensus harness has actually run under a documented configuration. From 94145f76c2dff0f2d99962c46aeecf402f5373f8 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:48:22 +0200 Subject: [PATCH 27/37] Align tokenomics docs with P1 collector integration --- docs/tokenomics-v2.md | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/tokenomics-v2.md b/docs/tokenomics-v2.md index 7b0a1d32..43eaf6cf 100644 --- a/docs/tokenomics-v2.md +++ b/docs/tokenomics-v2.md @@ -34,20 +34,24 @@ The compute market is asynchronous and keeps heavy work off-chain while using on ## Idle capital and productive-use telemetry -Protocol v2 is developing a prospective `IdleCapitalTracker` as **shadow telemetry**. It tracks capital lineage rather than wallet counts so that ordinary transfers, self-transfers, change outputs, split/merge patterns or address fragmentation cannot fabricate economic activity or reset dormancy. +Protocol v2 includes a prospective `IdleCapitalTracker` as **shadow telemetry**. It tracks capital lineage rather than wallet counts so that ordinary transfers, self-transfers, change outputs, split/merge patterns or address fragmentation cannot fabricate economic activity or reset dormancy. -Key properties of the tracker design: +Key properties of the tracker design and current P1 implementation: - capital lineage is amount-conserving and deterministic; - `IdleSinceHeight` survives ordinary movement, split/merge and cross-shard transfer; - productive use is an explicit verified hook, separate from generic transfers or contract calls; -- cross-shard transfer identity is the consensus-canonical receipt hash; -- destination object identity is the protocol-defined destination object; -- prospective bootstrap coverage is exposed explicitly and is not presented as complete historical lineage; +- cross-shard transfer identity is exactly the consensus-canonical `CrossShardReceipt.Hash()`; +- destination object identity comes from the protocol-defined `CrossShardReceipt.DestinationObject()`; +- finalized transaction/result/witness evidence is used to derive native-capital movement; +- previously unseen historical capital is only prospectively bootstrapped when consensus-stamped age is available, with incomplete coverage exposed rather than hidden; +- the tracker is opt-in inside the finalized `EpochCollector` preview/apply path; +- runtime-produced canonical export receipts are supplied to economic observations only after the source state root is known; +- collector checkpoint version 2 carries idle-capital state and can still restore version 1 collector checkpoints; - checkpoints are canonical, bounded and fail closed on malformed state; - replay and invalid/non-conserving transitions are rejected atomically. -The tracker does **not** currently impose an idle levy and does not mutate supply. +The tracker does **not** currently impose an idle levy and does not mutate supply. Productive-coverage updates are not granted for generic transfers or arbitrary contract calls; future hooks must be tied to narrow, verifiable productive events. ## State carrying cost @@ -90,22 +94,26 @@ The **1,000,000 TPS finalized-through-consensus** figure is a long-term aspirati ## Current Protocol v2 implementation status -Implemented or prepared in the current P1 economics work: +Implemented in the current P1 economics branch: - deterministic dynamic ZCU reference; - ZPPI compute/DA/storage basket with Q9 arithmetic and reliability/coverage gates; - capital-lot lineage primitives, dormancy histograms and productive-coverage measurement; - deterministic state carrying-cost estimator; -- adversarial fragmentation simulation; -- prospective IdleCapitalTracker implementation and adversarial tests prepared for repository integration; -- finalized-through-consensus benchmark report core and tests prepared for V2 Lab integration. +- adversarial fragmentation and lineage edge-case coverage; +- prospective IdleCapitalTracker with canonical local/cross-shard identity and finalized-evidence derivation; +- opt-in finalized `EpochCollector` integration with atomic preview/apply semantics; +- versioned collector checkpoint/restart support for idle-capital state; +- finalized-through-consensus benchmark report core and tests for V2 Lab integration. Still gated / not production-ready: - live monetary mint/burn or idle levy; - final reward-routing parameters; -- full collector/checkpoint integration of idle-capital tracking; +- automatic productive-coverage credit beyond explicitly approved and verifiable hooks; +- long-horizon public economic simulations and manipulation-resistance evidence; - production benchmark/capacity authentication; +- wiring the report core to real V2 Lab finality/QC sampling and publishing new measured chain-TPS results; - production ZK/TEE verification infrastructure; - governed production WorkRegistry; - production Rust/WASM contract stack; From 1be9b6d1d40395622db9991602738b732cbcc727 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:50:42 +0200 Subject: [PATCH 28/37] Align README with Protocol v2 economics --- README.md | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 472b77b3..4d16355c 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,15 @@ The current v2 development branch contains executable foundations and tests for: - 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; +- dynamic per-work-class ZCU reference measurement from verified delivered-slot evidence; +- a versioned ZPPI compute/data-availability/storage basket with coverage and 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; +- a prospective shadow `IdleCapitalTracker` that preserves capital age through transfers, split/merge and canonical cross-shard receipts; +- opt-in finalized collector integration and versioned checkpoint/restart support for idle-capital lineage; +- legacy ZAMP shadow-actuator evaluation being migrated toward the ZPPI purchasing-power objective rather than a fixed 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; +- finalized-through-consensus benchmark report foundations with warm-up separation, p50/p95/p99 finality and safety/liveness invalidation; - Reed-Solomon data-availability reconstruction foundations; - libp2p/QUIC transport foundations; - v1 and v2 consensus/performance CI labs. @@ -164,29 +169,23 @@ See [docs/compute-economics-v2.md](./docs/compute-economics-v2.md) and [docs/eco ## ZPH tokenomics — shadow mode -The v2 economic design does **not** assume a fixed maximum supply. +The Protocol v2 economic design does **not** assume a fixed maximum supply and does **not** target a fixed +2% annual ZPH supply increase. -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: +The canonical long-run monetary objective from the 19 August 2026 economics working paper is approximately **-2% annual purchasing-power drift for ZPH against a Zephyr-native basket**. With `PP = 1 / ZPPI`, the corresponding annual ZPPI target factor is approximately **1.020408163** (Q9: `1_020_408_163`). Supply is endogenous: any future issuance or contraction, if activated, must respond to measured conditions and bounded governance rules rather than a constant inflation target. -- fee burn; -- staking/locked supply; -- protocol reserve; -- chain resource utilization; -- finalized operations; -- age-weighted money velocity; -- potentially reliable compute scarcity. +`ZPPI` is the slow monetary-direction signal. `ZCSI` is primarily a compute incentive/capacity-routing signal, and dynamic `ZCU` reference values are derived from verified delivered work rather than advertised capacity or theoretical peak FLOPS. -Compute feedback currently has three simulation modes: +Protocol v2 also includes prospective **shadow** idle-capital telemetry. Capital lineage is tracked through ordinary movement without resetting dormancy, including canonical cross-shard transfers identified by `CrossShardReceipt.Hash()` and protocol-defined destination objects. Generic transfers and arbitrary contract calls are not automatically treated as productive use. + +No idle levy, live adaptive minting or reward-routing change is activated merely because telemetry exists. Economic activation follows: ```text -A — observe only -B — change suggested compute-reward routing only -C — B plus a narrow bounded shadow inflation correction +measure first -> simulate second -> activate last ``` -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. +The legacy ZAMP supply-growth evaluator remains a shadow actuator/simulation boundary while the ZPPI path is integrated end-to-end. **No current mode mints live ZPH.** -See [docs/tokenomics-v2.md](./docs/tokenomics-v2.md) and [docs/economic-state-v2.md](./docs/economic-state-v2.md). +See [docs/tokenomics-v2.md](./docs/tokenomics-v2.md), [docs/protocol-economics-p1-status.md](./docs/protocol-economics-p1-status.md) and [docs/economic-state-v2.md](./docs/economic-state-v2.md). ## Consensus & Performance Lab @@ -203,6 +202,7 @@ See [docs/performance-lab.md](./docs/performance-lab.md). The repository must not currently be presented as having production-ready: - live adaptive ZPH issuance or final monetary parameters; +- an active idle-capital levy or automatic productive-use credit policy; - active validator/compute/reserve reward distribution; - production gas/resource pricing; - authenticated governance-controlled economic parameters; @@ -261,6 +261,7 @@ Start with: - [Protocol v2 architecture](./docs/protocol-v2.md) - [Protocol v2 implementation status](./docs/protocol-v2-implementation-status.md) +- [Protocol Economics P1 status](./docs/protocol-economics-p1-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) From 41d9a02de058ece309d40a887e81e5eebd8e2390 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Sat, 29 Aug 2026 01:30:49 +0200 Subject: [PATCH 29/37] Add finalized compute productive evidence layer --- .../economics/finalized_compute_productive.go | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 internal/v2/economics/finalized_compute_productive.go diff --git a/internal/v2/economics/finalized_compute_productive.go b/internal/v2/economics/finalized_compute_productive.go new file mode 100644 index 00000000..080fa043 --- /dev/null +++ b/internal/v2/economics/finalized_compute_productive.go @@ -0,0 +1,169 @@ +package economics + +import ( + "bytes" + "math/big" + + "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/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +// FinalizedComputeProductiveEvidence is a narrow, read-only evidence record for +// productive-capital accounting. It deliberately separates the job owner's +// compute escrow from provider collateral and from refund flows. The record is +// derived only from the finalized transaction intent, the consumed pre-settle +// job object and the canonical settlement receipt emitted by execution. +// +// This evidence does not mutate IdleCapitalTracker. In particular, callers must +// not mark an entire compute job object productive: that object can also carry +// provider collateral, which is economically distinct from paid compute spend. +type FinalizedComputeProductiveEvidence struct { + JobObjectID types.ObjectID + JobID types.JobID + Escrow uint64 + Paid uint64 + CoverageBps uint32 + ResultRoot types.Hash + Verification compute.VerificationMode +} + +// DeriveFinalizedComputeProductiveEvidence verifies one finalized compute +// settlement and returns the capital quantities that are safe to feed into a +// future productive-lineage adapter. Non-settlement operations are ignored. +// +// The function fails closed when the operation does not consume the referenced +// compute job, when the pre-settlement witness is ambiguous, or when the +// canonical receipt differs from the deterministic on-chain settlement. +func DeriveFinalizedComputeProductiveEvidence( + transaction tx.Transaction, + result execution.Result, +) (FinalizedComputeProductiveEvidence, bool, error) { + if result.TxID != transaction.ID() { + return FinalizedComputeProductiveEvidence{}, false, ErrFinalizedEconomics + } + if len(transaction.Operations) != 1 { + return FinalizedComputeProductiveEvidence{}, false, nil + } + + op := transaction.Operations[0] + if op.Kind != tx.OpComputeFinalize && op.Kind != tx.OpComputeResolveReplicated { + return FinalizedComputeProductiveEvidence{}, false, nil + } + ref, err := compute.ParseJobRef(op.Payload) + if err != nil { + return FinalizedComputeProductiveEvidence{}, false, ErrFinalizedEconomics + } + + var record compute.OnChainJob + found := false + for _, witness := range transaction.Witnesses { + if witness.Object.ID != ref.JobObject { + continue + } + if found || witness.Object.Kind != object.KindComputeJob || witness.Object.Owner != transaction.Sender { + return FinalizedComputeProductiveEvidence{}, false, ErrFinalizedEconomics + } + parsed, err := compute.ParseOnChainJob(witness.Object.Data) + if err != nil || parsed.Job.Owner != transaction.Sender { + return FinalizedComputeProductiveEvidence{}, false, ErrFinalizedEconomics + } + record = parsed + found = true + } + if !found { + return FinalizedComputeProductiveEvidence{}, false, ErrFinalizedEconomics + } + + consumedCount := 0 + for _, id := range result.Consumed { + if id == ref.JobObject { + consumedCount++ + } + } + if consumedCount != 1 { + return FinalizedComputeProductiveEvidence{}, false, ErrFinalizedEconomics + } + + var settlement compute.OnChainSettlement + switch op.Kind { + case tx.OpComputeFinalize: + // This mirrors execution.executeComputeFinalize: the direct finalize + // path is currently restricted to replicated jobs with an exact match. + if record.Job.Verification != compute.VerificationReplicated { + return FinalizedComputeProductiveEvidence{}, false, ErrFinalizedEconomics + } + _, settlement, err = compute.FinalizeOnChain(record, compute.VerificationEvidence{}) + case tx.OpComputeResolveReplicated: + _, settlement, err = compute.ResolveReplicatedMajority(record) + } + if err != nil { + return FinalizedComputeProductiveEvidence{}, false, ErrFinalizedEconomics + } + + expected := compute.SettlementReceipt{ + JobID: record.ID, + ResultRoot: settlement.ResultRoot, + Payments: settlement.Payments, + Refund: settlement.Refund, + Slashed: settlement.SlashedCollateral, + SlashReward: settlement.SlashReward, + } + receipt, ok, err := uniqueSettlementReceiptForJob(result.Created, record.ID) + if err != nil || !ok || !bytes.Equal(receipt.MarshalBinary(), expected.MarshalBinary()) { + return FinalizedComputeProductiveEvidence{}, false, ErrFinalizedEconomics + } + paid, err := compute.SettlementPaid(receipt) + if err != nil || paid == 0 || paid > record.Escrow { + return FinalizedComputeProductiveEvidence{}, false, ErrFinalizedEconomics + } + coverage, err := productiveCoverageBps(paid, record.Escrow) + if err != nil { + return FinalizedComputeProductiveEvidence{}, false, err + } + + return FinalizedComputeProductiveEvidence{ + JobObjectID: ref.JobObject, + JobID: record.ID, + Escrow: record.Escrow, + Paid: paid, + CoverageBps: coverage, + ResultRoot: receipt.ResultRoot, + Verification: record.Job.Verification, + }, true, nil +} + +func uniqueSettlementReceiptForJob(created []object.Object, jobID types.JobID) (compute.SettlementReceipt, bool, error) { + var out compute.SettlementReceipt + found := false + for _, createdObject := range created { + if createdObject.Kind != object.KindSystem { + continue + } + receipt, err := compute.ParseSettlementReceipt(createdObject.Data) + if err != nil || receipt.JobID != jobID { + continue + } + if found { + return compute.SettlementReceipt{}, false, ErrFinalizedEconomics + } + out = receipt + found = true + } + return out, found, nil +} + +func productiveCoverageBps(paid, escrow uint64) (uint32, error) { + if paid == 0 || escrow == 0 || paid > escrow { + return 0, ErrFinalizedEconomics + } + numerator := new(big.Int).SetUint64(paid) + numerator.Mul(numerator, big.NewInt(10_000)) + numerator.Quo(numerator, new(big.Int).SetUint64(escrow)) + if !numerator.IsUint64() || numerator.Uint64() > 10_000 { + return 0, ErrFinalizedEconomics + } + return uint32(numerator.Uint64()), nil +} From a3804ace37d1b8ba6d986601c36262be0e62e12b Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Sat, 29 Aug 2026 01:31:15 +0200 Subject: [PATCH 30/37] Test finalized compute productive evidence --- .../finalized_compute_productive_test.go | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 internal/v2/economics/finalized_compute_productive_test.go diff --git a/internal/v2/economics/finalized_compute_productive_test.go b/internal/v2/economics/finalized_compute_productive_test.go new file mode 100644 index 00000000..30b64573 --- /dev/null +++ b/internal/v2/economics/finalized_compute_productive_test.go @@ -0,0 +1,159 @@ +package economics + +import ( + "math" + "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/tx" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +func TestDeriveFinalizedComputeProductiveEvidenceExactSettlement(t *testing.T) { + record := finalizedComputeRecord(false) + transaction, result := finalizedComputeTransaction(t, record, tx.OpComputeFinalize) + + evidence, applied, err := DeriveFinalizedComputeProductiveEvidence(transaction, result) + if err != nil { + t.Fatal(err) + } + if !applied { + t.Fatal("finalized compute settlement was not recognized") + } + if evidence.JobID != record.ID || evidence.JobObjectID != transaction.Inputs[0].ObjectID { + t.Fatalf("unexpected settlement identity: %#v", evidence) + } + if evidence.Escrow != 100 || evidence.Paid != 70 || evidence.CoverageBps != 7_000 { + t.Fatalf("unexpected productive capital evidence: %#v", evidence) + } + if evidence.ResultRoot != types.Hash{9} || evidence.Verification != compute.VerificationReplicated { + t.Fatalf("unexpected verification evidence: %#v", evidence) + } +} + +func TestDeriveFinalizedComputeProductiveEvidenceMajorityOnlyCountsPaidProviders(t *testing.T) { + record := finalizedComputeRecord(true) + transaction, result := finalizedComputeTransaction(t, record, tx.OpComputeResolveReplicated) + + evidence, applied, err := DeriveFinalizedComputeProductiveEvidence(transaction, result) + if err != nil { + t.Fatal(err) + } + if !applied || evidence.Paid != 60 || evidence.Escrow != 100 || evidence.CoverageBps != 6_000 { + t.Fatalf("majority settlement did not isolate paid compute escrow: %#v", evidence) + } + if evidence.ResultRoot != types.Hash{9} { + t.Fatalf("wrong accepted majority root: %x", evidence.ResultRoot) + } +} + +func TestDeriveFinalizedComputeProductiveEvidenceRejectsTamperedReceipt(t *testing.T) { + record := finalizedComputeRecord(false) + transaction, result := finalizedComputeTransaction(t, record, tx.OpComputeFinalize) + receipt, err := compute.ParseSettlementReceipt(result.Created[0].Data) + if err != nil { + t.Fatal(err) + } + receipt.Payments[types.AccountID{2}]++ + result.Created[0].Data = receipt.MarshalBinary() + + if _, _, err := DeriveFinalizedComputeProductiveEvidence(transaction, result); err == nil { + t.Fatal("tampered finalized settlement receipt was accepted") + } +} + +func TestDeriveFinalizedComputeProductiveEvidenceIgnoresNonSettlement(t *testing.T) { + transaction := tx.Transaction{ShardID: 0, Operations: []tx.Operation{{Kind: tx.OpTransfer}}} + result := execution.Result{TxID: transaction.ID()} + evidence, applied, err := DeriveFinalizedComputeProductiveEvidence(transaction, result) + if err != nil { + t.Fatal(err) + } + if applied || evidence != (FinalizedComputeProductiveEvidence{}) { + t.Fatalf("generic transfer received productive evidence: %#v", evidence) + } +} + +func TestProductiveCoverageBpsHandlesMaximumCapitalWithoutOverflow(t *testing.T) { + coverage, err := productiveCoverageBps(math.MaxUint64-1, math.MaxUint64) + if err != nil { + t.Fatal(err) + } + if coverage != 9_999 { + t.Fatalf("unexpected maximum-value coverage: %d", coverage) + } +} + +func finalizedComputeRecord(majority bool) compute.OnChainJob { + owner := types.AccountID{1} + providers := []types.AccountID{{2}, {3}} + prices := []uint64{40, 30} + roots := []types.Hash{{9}, {9}} + replicas := uint16(2) + if majority { + providers = append(providers, types.AccountID{4}) + prices = []uint64{30, 30, 30} + roots = []types.Hash{{9}, {9}, {8}} + replicas = 3 + } + jobID := types.JobID{7} + assignments := make([]compute.Assignment, len(providers)) + results := make([]compute.Result, len(providers)) + for i, provider := range providers { + assignments[i] = compute.Assignment{OfferID: types.Hash{byte(20 + i)}, Provider: provider, Price: prices[i]} + results[i] = compute.Result{JobID: jobID, Provider: provider, ResultRoot: roots[i], CompletedHeight: 50} + } + return compute.OnChainJob{ + ID: jobID, + Job: compute.Job{ + Owner: owner, WorkloadHash: types.Hash{5}, InputRoot: types.Hash{6}, + Resources: compute.Resources{CPUCores: 1, MemoryMiB: 256}, + MaxPrice: 100, CollateralRequired: 10, Verification: compute.VerificationReplicated, + DeadlineHeight: 100, Replicas: replicas, + }, + Escrow: 100, Status: compute.JobAwaitingVerification, + Assignments: assignments, Results: results, + } +} + +func finalizedComputeTransaction(t *testing.T, record compute.OnChainJob, kind tx.OperationKind) (tx.Transaction, execution.Result) { + t.Helper() + jobRaw, err := record.MarshalBinary() + if err != nil { + t.Fatal(err) + } + jobObjectID := types.ObjectID{10} + payload, err := (compute.JobRef{JobObject: jobObjectID}).MarshalBinary() + if err != nil { + t.Fatal(err) + } + transaction := tx.Transaction{ + ShardID: 0, + Sender: record.Job.Owner, + Inputs: []tx.InputRef{{ObjectID: jobObjectID}}, + Operations: []tx.Operation{{Kind: kind, Payload: payload}}, + Witnesses: []tx.Witness{{Object: object.Object{ + ID: jobObjectID, Version: 4, Owner: record.Job.Owner, Kind: object.KindComputeJob, Data: jobRaw, + }}}, + } + var settlement compute.OnChainSettlement + if kind == tx.OpComputeFinalize { + _, settlement, err = compute.FinalizeOnChain(record, compute.VerificationEvidence{}) + } else { + _, settlement, err = compute.ResolveReplicatedMajority(record) + } + if err != nil { + t.Fatal(err) + } + receipt := compute.SettlementReceipt{ + JobID: record.ID, ResultRoot: settlement.ResultRoot, Payments: settlement.Payments, + Refund: settlement.Refund, Slashed: settlement.SlashedCollateral, SlashReward: settlement.SlashReward, + } + return transaction, execution.Result{ + Consumed: []types.ObjectID{jobObjectID}, + Created: []object.Object{{ID: types.ObjectID{11}, Version: 1, Kind: object.KindSystem, Data: receipt.MarshalBinary()}}, + TxID: transaction.ID(), + } +} From 878d0db4a0fdfb25c70f35d121047ec88c55ff79 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Sat, 29 Aug 2026 01:32:12 +0200 Subject: [PATCH 31/37] Format finalized productive evidence --- .../economics/finalized_compute_productive.go | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/v2/economics/finalized_compute_productive.go b/internal/v2/economics/finalized_compute_productive.go index 080fa043..ab1d47bf 100644 --- a/internal/v2/economics/finalized_compute_productive.go +++ b/internal/v2/economics/finalized_compute_productive.go @@ -21,12 +21,12 @@ import ( // not mark an entire compute job object productive: that object can also carry // provider collateral, which is economically distinct from paid compute spend. type FinalizedComputeProductiveEvidence struct { - JobObjectID types.ObjectID - JobID types.JobID - Escrow uint64 - Paid uint64 - CoverageBps uint32 - ResultRoot types.Hash + JobObjectID types.ObjectID + JobID types.JobID + Escrow uint64 + Paid uint64 + CoverageBps uint32 + ResultRoot types.Hash Verification compute.VerificationMode } @@ -125,12 +125,12 @@ func DeriveFinalizedComputeProductiveEvidence( } return FinalizedComputeProductiveEvidence{ - JobObjectID: ref.JobObject, - JobID: record.ID, - Escrow: record.Escrow, - Paid: paid, - CoverageBps: coverage, - ResultRoot: receipt.ResultRoot, + JobObjectID: ref.JobObject, + JobID: record.ID, + Escrow: record.Escrow, + Paid: paid, + CoverageBps: coverage, + ResultRoot: receipt.ResultRoot, Verification: record.Job.Verification, }, true, nil } From 3af26ddbbee1133409072af82f7a8e8138f29751 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Sat, 29 Aug 2026 01:32:26 +0200 Subject: [PATCH 32/37] Fix and format productive evidence tests --- .../finalized_compute_productive_test.go | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/internal/v2/economics/finalized_compute_productive_test.go b/internal/v2/economics/finalized_compute_productive_test.go index 30b64573..1f2f52e4 100644 --- a/internal/v2/economics/finalized_compute_productive_test.go +++ b/internal/v2/economics/finalized_compute_productive_test.go @@ -28,7 +28,7 @@ func TestDeriveFinalizedComputeProductiveEvidenceExactSettlement(t *testing.T) { if evidence.Escrow != 100 || evidence.Paid != 70 || evidence.CoverageBps != 7_000 { t.Fatalf("unexpected productive capital evidence: %#v", evidence) } - if evidence.ResultRoot != types.Hash{9} || evidence.Verification != compute.VerificationReplicated { + if evidence.ResultRoot != (types.Hash{9}) || evidence.Verification != compute.VerificationReplicated { t.Fatalf("unexpected verification evidence: %#v", evidence) } } @@ -44,7 +44,7 @@ func TestDeriveFinalizedComputeProductiveEvidenceMajorityOnlyCountsPaidProviders if !applied || evidence.Paid != 60 || evidence.Escrow != 100 || evidence.CoverageBps != 6_000 { t.Fatalf("majority settlement did not isolate paid compute escrow: %#v", evidence) } - if evidence.ResultRoot != types.Hash{9} { + if evidence.ResultRoot != (types.Hash{9}) { t.Fatalf("wrong accepted majority root: %x", evidence.ResultRoot) } } @@ -109,16 +109,21 @@ func finalizedComputeRecord(majority bool) compute.OnChainJob { ID: jobID, Job: compute.Job{ Owner: owner, WorkloadHash: types.Hash{5}, InputRoot: types.Hash{6}, - Resources: compute.Resources{CPUCores: 1, MemoryMiB: 256}, - MaxPrice: 100, CollateralRequired: 10, Verification: compute.VerificationReplicated, - DeadlineHeight: 100, Replicas: replicas, + Resources: compute.Resources{CPUCores: 1, MemoryMiB: 256}, + MaxPrice: 100, + CollateralRequired: 10, + Verification: compute.VerificationReplicated, + DeadlineHeight: 100, + Replicas: replicas, }, - Escrow: 100, Status: compute.JobAwaitingVerification, - Assignments: assignments, Results: results, + Escrow: 100, + Status: compute.JobAwaitingVerification, + Assignments: assignments, + Results: results, } } -func finalizedComputeTransaction(t *testing.T, record compute.OnChainJob, kind tx.OperationKind) (tx.Transaction, execution.Result) { +func finalizedComputeTransaction(t *testing.T, record compute.OnChainJob, kind uint16) (tx.Transaction, execution.Result) { t.Helper() jobRaw, err := record.MarshalBinary() if err != nil { @@ -131,9 +136,11 @@ func finalizedComputeTransaction(t *testing.T, record compute.OnChainJob, kind t } transaction := tx.Transaction{ ShardID: 0, - Sender: record.Job.Owner, - Inputs: []tx.InputRef{{ObjectID: jobObjectID}}, - Operations: []tx.Operation{{Kind: kind, Payload: payload}}, + Sender: record.Job.Owner, + Inputs: []tx.InputRef{{ObjectID: jobObjectID}}, + Operations: []tx.Operation{{ + Kind: kind, Payload: payload, + }}, Witnesses: []tx.Witness{{Object: object.Object{ ID: jobObjectID, Version: 4, Owner: record.Job.Owner, Kind: object.KindComputeJob, Data: jobRaw, }}}, @@ -153,7 +160,7 @@ func finalizedComputeTransaction(t *testing.T, record compute.OnChainJob, kind t } return transaction, execution.Result{ Consumed: []types.ObjectID{jobObjectID}, - Created: []object.Object{{ID: types.ObjectID{11}, Version: 1, Kind: object.KindSystem, Data: receipt.MarshalBinary()}}, - TxID: transaction.ID(), + Created: []object.Object{{ID: types.ObjectID{11}, Version: 1, Kind: object.KindSystem, Data: receipt.MarshalBinary()}}, + TxID: transaction.ID(), } } From 4ddab119b57bd5ee708d086795867420242cf8ec Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Sat, 29 Aug 2026 01:33:27 +0200 Subject: [PATCH 33/37] Document finalized compute productive evidence boundary --- docs/protocol-economics-p1-status.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/protocol-economics-p1-status.md b/docs/protocol-economics-p1-status.md index 84de185b..2cb306bb 100644 --- a/docs/protocol-economics-p1-status.md +++ b/docs/protocol-economics-p1-status.md @@ -22,10 +22,18 @@ This tranche is **shadow-only**. It does not activate live monetary supply chang - canonical export receipts passed from the runtime into finalized economic observations after the source state root is known; - collector checkpoint version 2 carrying idle-capital state while retaining restore support for version 1 checkpoints; - restart/canonical checkpoint and local/cross-shard collector integration tests; +- finalized compute productive-evidence derivation that verifies the referenced consumed job object and canonical settlement receipt, reconstructs exact/strict-majority settlement, isolates paid compute escrow from collateral/refunds, and computes overflow-safe `paid / escrow` coverage in basis points; +- productive-evidence tests for exact replicated settlement, strict-majority settlement, tampered receipts, generic-transfer exclusion and maximum-`uint64` capital values; - finalized-through-consensus benchmark report core with warm-up separation, measured finalized TPS, p50/p95/p99 finality, environment/config metadata, JSON output and safety/liveness invalidation. The pre-existing supply-growth controller remains a legacy **shadow simulator** until the ZPPI path is integrated into the monetary epoch path. ZCSI remains primarily a compute incentive/capacity-routing signal rather than the monetary anchor. +## Productive-capital boundary + +The finalized compute evidence layer is intentionally read-only for now. A compute job object can contain both the owner's escrow and provider collateral, while settlement outputs can aggregate provider payment and collateral return into the same native coin. Marking the whole job object productive would therefore over-credit collateral. + +The next lineage step must preserve those economic subcomponents explicitly before any call to `MarkObjectProductive`. Generic transfers and arbitrary contract calls remain ineligible for productive-use credit. + ## Activation discipline ```text @@ -36,7 +44,7 @@ No live mint, burn policy, levy or reward-routing change is implied by the prese ## Remaining P1 work -- connect productive-coverage updates only to explicitly verified productive events, beginning with narrow finalized compute escrow/settlement evidence rather than generic transfers or contract calls; +- map finalized compute escrow lineage through job creation, assignment/ingest and settlement without folding provider collateral into productive credit, then connect only verified paid escrow to productive-coverage updates; - expand restart/fault scenarios around collector/runtime checkpoints and cross-shard lineage as the shadow tracker accumulates larger state; - connect the finalized benchmark report to real V2 Lab finality/QC samples; - add a small deterministic CI benchmark scenario and a heavier manual shard/cross-shard matrix; From 1fdb906a93e3005e137fc9ad7b0aaca92bc366f2 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:46:21 +0200 Subject: [PATCH 34/37] Separate compute escrow and collateral shadow components --- docs/protocol-economics-p1-status.md | 5 +- .../economics/compute_capital_components.go | 109 ++++++++++++++++++ .../compute_capital_components_test.go | 50 ++++++++ .../economics/finalized_compute_productive.go | 18 ++- .../finalized_compute_productive_test.go | 7 ++ 5 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 internal/v2/economics/compute_capital_components.go create mode 100644 internal/v2/economics/compute_capital_components_test.go diff --git a/docs/protocol-economics-p1-status.md b/docs/protocol-economics-p1-status.md index 2cb306bb..9668e477 100644 --- a/docs/protocol-economics-p1-status.md +++ b/docs/protocol-economics-p1-status.md @@ -23,7 +23,8 @@ This tranche is **shadow-only**. It does not activate live monetary supply chang - collector checkpoint version 2 carrying idle-capital state while retaining restore support for version 1 checkpoints; - restart/canonical checkpoint and local/cross-shard collector integration tests; - finalized compute productive-evidence derivation that verifies the referenced consumed job object and canonical settlement receipt, reconstructs exact/strict-majority settlement, isolates paid compute escrow from collateral/refunds, and computes overflow-safe `paid / escrow` coverage in basis points; -- productive-evidence tests for exact replicated settlement, strict-majority settlement, tampered receipts, generic-transfer exclusion and maximum-`uint64` capital values; +- stable shadow component identities for owner escrow and each provider collateral position, keyed independently of compute-job object versions so future lineage updates cannot conflate productive spend with collateral; +- productive-evidence/component tests for exact replicated settlement, strict-majority settlement, stable escrow/collateral identities, duplicate-provider rejection, tampered receipts, generic-transfer exclusion and maximum-`uint64` capital values; - finalized-through-consensus benchmark report core with warm-up separation, measured finalized TPS, p50/p95/p99 finality, environment/config metadata, JSON output and safety/liveness invalidation. The pre-existing supply-growth controller remains a legacy **shadow simulator** until the ZPPI path is integrated into the monetary epoch path. ZCSI remains primarily a compute incentive/capacity-routing signal rather than the monetary anchor. @@ -32,7 +33,7 @@ The pre-existing supply-growth controller remains a legacy **shadow simulator** The finalized compute evidence layer is intentionally read-only for now. A compute job object can contain both the owner's escrow and provider collateral, while settlement outputs can aggregate provider payment and collateral return into the same native coin. Marking the whole job object productive would therefore over-credit collateral. -The next lineage step must preserve those economic subcomponents explicitly before any call to `MarkObjectProductive`. Generic transfers and arbitrary contract calls remain ineligible for productive-use credit. +P1 now assigns deterministic shadow identities to the escrow component and each per-provider collateral component. These identities are stable across job-object version updates, but they do not yet mutate `IdleCapitalTracker`. The next lineage step is to route finalized job creation/assignment/settlement capital through those component identities before any productive reset is applied. Generic transfers and arbitrary contract calls remain ineligible for productive-use credit. ## Activation discipline diff --git a/internal/v2/economics/compute_capital_components.go b/internal/v2/economics/compute_capital_components.go new file mode 100644 index 00000000..acbdd7b5 --- /dev/null +++ b/internal/v2/economics/compute_capital_components.go @@ -0,0 +1,109 @@ +package economics + +import ( + "bytes" + "math" + "sort" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" + "github.com/zephyr-chain/zephyr-chain/internal/v2/types" +) + +type ComputeCapitalComponentKind uint8 + +const ( + ComputeCapitalComponentUnknown ComputeCapitalComponentKind = iota + ComputeCapitalComponentEscrow + ComputeCapitalComponentCollateral +) + +// ComputeCapitalComponent is a stable shadow identity for one economically +// distinct portion of native capital held by a compute job. Component IDs are +// deterministic across job-object version updates so later lineage accounting +// can keep owner escrow separate from provider collateral. +type ComputeCapitalComponent struct { + ID types.Hash + Kind ComputeCapitalComponentKind + JobID types.JobID + Provider types.AccountID + Amount uint64 +} + +type ComputeCapitalComponentSet struct { + JobID types.JobID + Escrow ComputeCapitalComponent + Collateral []ComputeCapitalComponent + TotalLocked uint64 +} + +func ComputeEscrowComponentID(jobID types.JobID) (types.Hash, error) { + if types.IsZero32([32]byte(jobID)) { + return types.Hash{}, ErrFinalizedEconomics + } + return types.HashBytes("economics/compute-capital/escrow/v1", jobID[:]), nil +} + +func ComputeCollateralComponentID(jobID types.JobID, provider types.AccountID) (types.Hash, error) { + if types.IsZero32([32]byte(jobID)) || types.IsZero32([32]byte(provider)) { + return types.Hash{}, ErrFinalizedEconomics + } + payload := make([]byte, 0, len(jobID)+len(provider)) + payload = append(payload, jobID[:]...) + payload = append(payload, provider[:]...) + return types.HashBytes("economics/compute-capital/collateral/v1", payload), nil +} + +// DeriveComputeCapitalComponents exposes the economic subcomponents carried by +// a compute job without mutating consensus state or IdleCapitalTracker. +// +// Escrow is always one stable component keyed by JobID. Each assignment adds a +// separate provider-collateral component keyed by JobID+provider. This avoids +// the incorrect shortcut of treating the whole job object's locked value as +// productive compute spend. +func DeriveComputeCapitalComponents(record compute.OnChainJob) (ComputeCapitalComponentSet, error) { + if _, err := record.MarshalBinary(); err != nil || record.Escrow == 0 { + return ComputeCapitalComponentSet{}, ErrFinalizedEconomics + } + escrowID, err := ComputeEscrowComponentID(record.ID) + if err != nil { + return ComputeCapitalComponentSet{}, err + } + out := ComputeCapitalComponentSet{ + JobID: record.ID, + Escrow: ComputeCapitalComponent{ + ID: escrowID, + Kind: ComputeCapitalComponentEscrow, + JobID: record.ID, + Amount: record.Escrow, + }, + TotalLocked: record.Escrow, + } + if record.Job.CollateralRequired == 0 { + return out, nil + } + + seenProviders := make(map[types.AccountID]struct{}, len(record.Assignments)) + out.Collateral = make([]ComputeCapitalComponent, 0, len(record.Assignments)) + for _, assignment := range record.Assignments { + if _, duplicate := seenProviders[assignment.Provider]; duplicate { + return ComputeCapitalComponentSet{}, ErrFinalizedEconomics + } + seenProviders[assignment.Provider] = struct{}{} + componentID, err := ComputeCollateralComponentID(record.ID, assignment.Provider) + if err != nil || math.MaxUint64-out.TotalLocked < record.Job.CollateralRequired { + return ComputeCapitalComponentSet{}, ErrFinalizedEconomics + } + out.TotalLocked += record.Job.CollateralRequired + out.Collateral = append(out.Collateral, ComputeCapitalComponent{ + ID: componentID, + Kind: ComputeCapitalComponentCollateral, + JobID: record.ID, + Provider: assignment.Provider, + Amount: record.Job.CollateralRequired, + }) + } + sort.Slice(out.Collateral, func(i, j int) bool { + return bytes.Compare(out.Collateral[i].Provider[:], out.Collateral[j].Provider[:]) < 0 + }) + return out, nil +} diff --git a/internal/v2/economics/compute_capital_components_test.go b/internal/v2/economics/compute_capital_components_test.go new file mode 100644 index 00000000..c27a88bf --- /dev/null +++ b/internal/v2/economics/compute_capital_components_test.go @@ -0,0 +1,50 @@ +package economics + +import ( + "testing" + + "github.com/zephyr-chain/zephyr-chain/internal/v2/compute" +) + +func TestDeriveComputeCapitalComponentsSeparatesEscrowAndCollateral(t *testing.T) { + record := finalizedComputeRecord(false) + components, err := DeriveComputeCapitalComponents(record) + if err != nil { + t.Fatal(err) + } + if components.JobID != record.ID || components.Escrow.Amount != record.Escrow { + t.Fatalf("unexpected escrow component set: %#v", components) + } + if components.TotalLocked != 120 || len(components.Collateral) != 2 { + t.Fatalf("unexpected locked capital split: %#v", components) + } + if components.Escrow.ID == components.Collateral[0].ID || + components.Escrow.ID == components.Collateral[1].ID || + components.Collateral[0].ID == components.Collateral[1].ID { + t.Fatal("compute capital component identities are not distinct") + } + + updated := record + updated.Status = compute.JobAwaitingVerification + updated.Results = append([]compute.Result(nil), record.Results...) + next, err := DeriveComputeCapitalComponents(updated) + if err != nil { + t.Fatal(err) + } + if next.Escrow.ID != components.Escrow.ID { + t.Fatal("escrow component identity changed across job state update") + } + for i := range components.Collateral { + if next.Collateral[i].ID != components.Collateral[i].ID { + t.Fatal("collateral component identity changed across job state update") + } + } +} + +func TestDeriveComputeCapitalComponentsRejectsDuplicateProvider(t *testing.T) { + record := finalizedComputeRecord(false) + record.Assignments = append(record.Assignments, record.Assignments[0]) + if _, err := DeriveComputeCapitalComponents(record); err == nil { + t.Fatal("duplicate provider collateral component was accepted") + } +} diff --git a/internal/v2/economics/finalized_compute_productive.go b/internal/v2/economics/finalized_compute_productive.go index ab1d47bf..2f701f83 100644 --- a/internal/v2/economics/finalized_compute_productive.go +++ b/internal/v2/economics/finalized_compute_productive.go @@ -21,9 +21,10 @@ import ( // not mark an entire compute job object productive: that object can also carry // provider collateral, which is economically distinct from paid compute spend. type FinalizedComputeProductiveEvidence struct { - JobObjectID types.ObjectID - JobID types.JobID - Escrow uint64 + JobObjectID types.ObjectID + JobID types.JobID + EscrowComponentID types.Hash + Escrow uint64 Paid uint64 CoverageBps uint32 ResultRoot types.Hash @@ -123,11 +124,16 @@ func DeriveFinalizedComputeProductiveEvidence( if err != nil { return FinalizedComputeProductiveEvidence{}, false, err } + components, err := DeriveComputeCapitalComponents(record) + if err != nil || components.Escrow.Amount != record.Escrow { + return FinalizedComputeProductiveEvidence{}, false, ErrFinalizedEconomics + } return FinalizedComputeProductiveEvidence{ - JobObjectID: ref.JobObject, - JobID: record.ID, - Escrow: record.Escrow, + JobObjectID: ref.JobObject, + JobID: record.ID, + EscrowComponentID: components.Escrow.ID, + Escrow: record.Escrow, Paid: paid, CoverageBps: coverage, ResultRoot: receipt.ResultRoot, diff --git a/internal/v2/economics/finalized_compute_productive_test.go b/internal/v2/economics/finalized_compute_productive_test.go index 1f2f52e4..32b9429a 100644 --- a/internal/v2/economics/finalized_compute_productive_test.go +++ b/internal/v2/economics/finalized_compute_productive_test.go @@ -25,6 +25,13 @@ func TestDeriveFinalizedComputeProductiveEvidenceExactSettlement(t *testing.T) { if evidence.JobID != record.ID || evidence.JobObjectID != transaction.Inputs[0].ObjectID { t.Fatalf("unexpected settlement identity: %#v", evidence) } + escrowComponentID, err := ComputeEscrowComponentID(record.ID) + if err != nil { + t.Fatal(err) + } + if evidence.EscrowComponentID != escrowComponentID { + t.Fatalf("unexpected escrow component identity: %x", evidence.EscrowComponentID) + } if evidence.Escrow != 100 || evidence.Paid != 70 || evidence.CoverageBps != 7_000 { t.Fatalf("unexpected productive capital evidence: %#v", evidence) } From 077dc24050e8cc1b409ba19b4e51425e7148c49a Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:48:18 +0200 Subject: [PATCH 35/37] Format compute productive component evidence --- .../v2/economics/finalized_compute_productive.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/v2/economics/finalized_compute_productive.go b/internal/v2/economics/finalized_compute_productive.go index 2f701f83..e05fc5cd 100644 --- a/internal/v2/economics/finalized_compute_productive.go +++ b/internal/v2/economics/finalized_compute_productive.go @@ -25,10 +25,10 @@ type FinalizedComputeProductiveEvidence struct { JobID types.JobID EscrowComponentID types.Hash Escrow uint64 - Paid uint64 - CoverageBps uint32 - ResultRoot types.Hash - Verification compute.VerificationMode + Paid uint64 + CoverageBps uint32 + ResultRoot types.Hash + Verification compute.VerificationMode } // DeriveFinalizedComputeProductiveEvidence verifies one finalized compute @@ -134,10 +134,10 @@ func DeriveFinalizedComputeProductiveEvidence( JobID: record.ID, EscrowComponentID: components.Escrow.ID, Escrow: record.Escrow, - Paid: paid, - CoverageBps: coverage, - ResultRoot: receipt.ResultRoot, - Verification: record.Job.Verification, + Paid: paid, + CoverageBps: coverage, + ResultRoot: receipt.ResultRoot, + Verification: record.Job.Verification, }, true, nil } From 229cedc4e424c97809178a855b282220243b2c5a Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:53:55 +0200 Subject: [PATCH 36/37] Refresh wallet security lock and bound audit exception --- .github/workflows/ci.yml | 2 +- apps/wallet/package-lock.json | 227 +++++++++++++++++---------------- scripts/check-wallet-audit.mjs | 93 ++++++++++++++ 3 files changed, 208 insertions(+), 114 deletions(-) create mode 100644 scripts/check-wallet-audit.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 290de30f..f216e010 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,7 +110,7 @@ jobs: run: npm ci - name: Audit production and build dependencies - run: npm audit --audit-level=high + run: node ../../scripts/check-wallet-audit.mjs - name: Type-check and build run: npm run build \ No newline at end of file diff --git a/apps/wallet/package-lock.json b/apps/wallet/package-lock.json index 8c549f4e..2b5f3a3c 100644 --- a/apps/wallet/package-lock.json +++ b/apps/wallet/package-lock.json @@ -34,30 +34,30 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -67,13 +67,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -457,53 +457,53 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", - "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz", + "integrity": "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.30", + "@babel/parser": "^7.29.9", + "@vue/shared": "3.5.44", "entities": "^7.0.1", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", - "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz", + "integrity": "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-core": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", - "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz", + "integrity": "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.30", - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30", + "@babel/parser": "^7.29.9", + "@vue/compiler-core": "3.5.44", + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-ssr": "3.5.44", + "@vue/shared": "3.5.44", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.8", - "source-map-js": "^1.2.1" + "postcss": "^8.5.29", + "source-map-js": "^1.2.2" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", - "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz", + "integrity": "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/compiler-vue2": { @@ -543,53 +543,51 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", - "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.44.tgz", + "integrity": "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.30" + "@vue/shared": "3.5.44" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", - "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.44.tgz", + "integrity": "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/reactivity": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", - "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.44.tgz", + "integrity": "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.30", - "@vue/runtime-core": "3.5.30", - "@vue/shared": "3.5.30", + "@vue/reactivity": "3.5.44", + "@vue/runtime-core": "3.5.44", + "@vue/shared": "3.5.44", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", - "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.44.tgz", + "integrity": "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.30", - "@vue/shared": "3.5.30" - }, - "peerDependencies": { - "vue": "3.5.30" + "@vue/compiler-ssr": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/shared": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", - "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.44.tgz", + "integrity": "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==", "license": "MIT" }, "node_modules/alien-signals": { @@ -672,9 +670,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.10", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", - "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "version": "2.11.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", + "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", "dev": true, "license": "Apache-2.0", "bin": { @@ -698,9 +696,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -721,9 +719,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -741,11 +739,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -765,9 +763,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001781", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", - "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", "dev": true, "funding": [ { @@ -884,9 +882,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.321", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", - "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "version": "1.5.407", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz", + "integrity": "sha512-4R8XgQOdfxexCd/u63lRm6wCHjECwI45MV9wxAs2ggtfWe2hwlo1ql97jKsju2IcJ+jFSTwBssyYoiWhh7mauQ==", "dev": true, "license": "ISC" }, @@ -1468,9 +1466,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "funding": [ { "type": "github", @@ -1486,11 +1484,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -1576,9 +1577,9 @@ } }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", "funding": [ { "type": "opencollective", @@ -1595,9 +1596,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.19", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -1871,9 +1872,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2059,9 +2060,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "dev": true, "funding": [ { @@ -2195,16 +2196,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", - "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.44.tgz", + "integrity": "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.30", - "@vue/compiler-sfc": "3.5.30", - "@vue/runtime-dom": "3.5.30", - "@vue/server-renderer": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-sfc": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/server-renderer": "3.5.44", + "@vue/shared": "3.5.44" }, "peerDependencies": { "typescript": "*" diff --git a/scripts/check-wallet-audit.mjs b/scripts/check-wallet-audit.mjs new file mode 100644 index 00000000..26944ee9 --- /dev/null +++ b/scripts/check-wallet-audit.mjs @@ -0,0 +1,93 @@ +import { execFileSync } from "node:child_process"; + +const ALLOWED_ADVISORY = "GHSA-vfj7-8cjw-p6xm"; +const ALLOWED_ROOT_PACKAGE = "braces"; +const EXPIRES_AT = Date.parse("2026-11-16T00:00:00Z"); + +let raw = ""; +try { + raw = execFileSync("npm", ["audit", "--json"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); +} catch (error) { + raw = String(error.stdout || ""); + if (!raw.trim()) { + process.stderr.write(String(error.stderr || error)); + process.exit(1); + } +} + +let report; +try { + report = JSON.parse(raw); +} catch (error) { + console.error("npm audit returned invalid JSON:", error); + process.exit(1); +} + +const vulnerabilities = report.vulnerabilities || {}; +const blocking = Object.entries(vulnerabilities) + .filter(([, value]) => value.severity === "high" || value.severity === "critical") + .map(([name]) => name); + +if (blocking.length === 0) { + console.log("npm audit: no high or critical vulnerabilities."); + process.exit(0); +} + +const root = vulnerabilities[ALLOWED_ROOT_PACKAGE]; +const rootVia = Array.isArray(root?.via) ? root.via : []; +const rootHasAllowedAdvisory = rootVia.some((entry) => { + if (!entry || typeof entry === "string") return false; + return String(entry.url || "").includes(ALLOWED_ADVISORY); +}); + +if (!rootHasAllowedAdvisory) { + console.error(`Expected temporary audit exception ${ALLOWED_ADVISORY} was not the root cause.`); + process.exit(1); +} +if (root.fixAvailable !== false) { + console.error(`${ALLOWED_ADVISORY} now has an npm fix path; remove the exception and update dependencies.`); + process.exit(1); +} +if (Date.now() >= EXPIRES_AT) { + console.error(`Temporary audit exception ${ALLOWED_ADVISORY} expired on 2026-11-16.`); + process.exit(1); +} + +const allowed = new Set(); +let changed = true; +while (changed) { + changed = false; + for (const name of blocking) { + if (allowed.has(name)) continue; + const value = vulnerabilities[name]; + const via = Array.isArray(value?.via) ? value.via : []; + if (via.length === 0) continue; + const allViaAllowed = via.every((entry) => { + if (typeof entry === "string") return allowed.has(entry); + if (!entry) return false; + return String(entry.url || "").includes(ALLOWED_ADVISORY); + }); + if (allViaAllowed) { + allowed.add(name); + changed = true; + } + } +} + +const unexpected = blocking.filter((name) => !allowed.has(name)); +if (unexpected.length > 0) { + console.error("npm audit found unexpected high/critical vulnerabilities:"); + for (const name of unexpected) { + const value = vulnerabilities[name]; + console.error(`- ${name}: ${value.severity}; fixAvailable=${JSON.stringify(value.fixAvailable)}`); + } + process.exit(1); +} + +console.warn( + `npm audit: allowing only ${ALLOWED_ADVISORY} (unpatched braces dev-tooling advisory) until 2026-11-16.` +); +console.warn(`Affected audit nodes: ${[...allowed].sort().join(", ")}`); From ec4e1d377111ab83cdae9a1f0c03aad9796da337 Mon Sep 17 00:00:00 2001 From: The Code Learner <142033899+the-code-learner@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:54:53 +0200 Subject: [PATCH 37/37] Allow only breaking-path braces audit exception --- scripts/check-wallet-audit.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/check-wallet-audit.mjs b/scripts/check-wallet-audit.mjs index 26944ee9..c6f15d83 100644 --- a/scripts/check-wallet-audit.mjs +++ b/scripts/check-wallet-audit.mjs @@ -47,8 +47,9 @@ if (!rootHasAllowedAdvisory) { console.error(`Expected temporary audit exception ${ALLOWED_ADVISORY} was not the root cause.`); process.exit(1); } -if (root.fixAvailable !== false) { - console.error(`${ALLOWED_ADVISORY} now has an npm fix path; remove the exception and update dependencies.`); +const rootFix = root.fixAvailable; +if (rootFix !== false && !(rootFix && typeof rootFix === "object" && rootFix.isSemVerMajor === true)) { + console.error(`${ALLOWED_ADVISORY} now has a non-breaking npm fix path; remove the exception and update dependencies.`); process.exit(1); } if (Date.now() >= EXPIRES_AT) {