From 71e429da5f3cc09a1b744ef826018eeb2216cc22 Mon Sep 17 00:00:00 2001 From: CIAgent Date: Mon, 17 Aug 2026 19:56:21 +0000 Subject: [PATCH] feat(P01): OY Chain skeleton + Watcher/Mirror/Still modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ---ci--- project: oy phase: 1 milestone: v0.1 status: execute ---/ci--- Phase 1 (Component 1: OY Chain & Mirror) skeleton: - x/watcher: Watcher bond, attestation quorum (6-of-9), slashing types - x/mirror: NAV per Bread, reserve ratio publication types - x/still: Still/Stir (pause/resume) with auto-still on quorum lost - 12 unit tests passing (all locked constants verified: §7) - Cosmos SDK app.go deferred to P1 continuation (dependency resolution) - No regulated finance terminology used (lexicon compliant) --- .ciagent/CHECKPOINT.json | 6 +- go.mod | 3 + x/README.md | 55 ++++++++++++++++ x/mirror/types/types.go | 34 ++++++++++ x/mirror/types/types_test.go | 43 ++++++++++++ x/still/types/types.go | 63 ++++++++++++++++++ x/still/types/types_test.go | 30 +++++++++ x/watcher/types/types.go | 120 ++++++++++++++++++++++++++++++++++ x/watcher/types/types_test.go | 41 ++++++++++++ 9 files changed, 392 insertions(+), 3 deletions(-) create mode 100644 go.mod create mode 100644 x/README.md create mode 100644 x/mirror/types/types.go create mode 100644 x/mirror/types/types_test.go create mode 100644 x/still/types/types.go create mode 100644 x/still/types/types_test.go create mode 100644 x/watcher/types/types.go create mode 100644 x/watcher/types/types_test.go diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json index a4d059e..cb055f0 100644 --- a/.ciagent/CHECKPOINT.json +++ b/.ciagent/CHECKPOINT.json @@ -1,8 +1,8 @@ { - "phase": 0, - "stage": "complete", + "phase": 1, + "stage": "execute", "milestone": "v0.1", - "phase_role": "pre_execution", + "phase_role": "execution", "attempts": 0, "updated_at": "2026-08-17T00:00:00Z" } \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..36b5b3e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/oy/openyield + +go 1.22 diff --git a/x/README.md b/x/README.md new file mode 100644 index 0000000..f4cd3d1 --- /dev/null +++ b/x/README.md @@ -0,0 +1,55 @@ +# Component 1: OY Chain & Mirror — Phase 1 Skeleton + +## Status +v0.0.1 — skeleton with type definitions and locked-constant tests + +## Vision References +- §7: Architecture — Three Layers and the Mirror +- §3: Still/Stir (pause/resume) + +## Modules + +### x/watcher — Watcher Bond & Attestation Quorum +- `types.Watcher` — bonded Watcher (§7: 100,000 Bread minimum bond) +- `types.Attestation` — daily reserve snapshot signed by a Watcher +- `types.ReserveSnapshot` — off-chain reserve state (off-chain total, on-chain Bread, basket breakdown) +- `types.QuorumStatus` — QuorumMet / QuorumLost / StaleEpoch +- `types.SlashReason` — InsufficientBond / StaleAttestation / GeographicCollusion / Forgery + +**Locked constants (§7):** +- `MinBond = 100000` (100,000 Bread) +- `Quorum = 6` (6-of-9) +- `MaxWatchers = 9` +- `AttestationEpochHours = 24` (daily) + +### x/mirror — Reserve Ratio Publication +- `types.NavPerBread` — Net Asset Value per Bread (§4.2) +- `types.ReserveRatio` — real-time published ratio with quorum flag + +### x/still — Pause/Resume (Still/Stir) +- `types.StillState` — StirActive / StillPaused (§3) +- `types.StillReason` — CouncilVote / QuorumLost / SecurityAlert +- `types.StillRecord` — tracks pause events +- Auto-still on quorum lost (§4.2: Forge/Fold blocked until quorum restored) + +## Tests +12 tests, all passing: +- Watcher: MinBond, Quorum, MaxWatchers, AttestationEpoch, DefaultParams (5) +- Still: DefaultStillState, AutoStillOnQuorumLost, StillStates (3) +- Mirror: DefaultPublishInterval, ModuleName, NavPerBreadStruct, ReserveRatioQuorum (4) + +## What's NOT in this skeleton (deferred to P1 completion) +- Cosmos SDK app.go full integration (requires SDK v0.50+ dependency resolution) +- Keeper implementations (BondWatcher, SubmitAttestation, VerifyQuorum, SlashWatcher) +- CLI commands (oyd tx watcher bond, oyd query watcher quorum) +- Proto definitions (Buf-generated types) +- CometBFT consensus setup +- Genesis import/export + +## Next Steps (P1 continuation) +1. Resolve Cosmos SDK v0.50 dependency tree +2. Implement keeper logic with store operations +3. Add proto definitions and code generation +4. Wire modules into app.go +5. Build oyd daemon +6. Integration test: 9 Watchers bond, submit attestations, verify quorum \ No newline at end of file diff --git a/x/mirror/types/types.go b/x/mirror/types/types.go new file mode 100644 index 0000000..a6acc2f --- /dev/null +++ b/x/mirror/types/types.go @@ -0,0 +1,34 @@ +package types + +const ( + ModuleName = "mirror" + StoreKey = ModuleName + RouterKey = ModuleName + QuerierRoute = ModuleName +) + +// NavPerBread is the Net Asset Value per Bread (§4.2) +type NavPerBread struct { + Value int64 `json:"value" yaml:"value"` + Timestamp int64 `json:"timestamp" yaml:"timestamp"` + Attestations uint8 `json:"attestations" yaml:"attestations"` + AuditRef string `json:"audit_ref" yaml:"audit_ref"` +} + +// ReserveRatio is the real-time published reserve ratio (§7) +type ReserveRatio struct { + Ratio float64 `json:"ratio" yaml:"ratio"` + Timestamp int64 `json:"timestamp" yaml:"timestamp"` + QuorumMet bool `json:"quorum_met" yaml:"quorum_met"` +} + +// Params for the mirror module +type Params struct { + PublishIntervalBlocks uint64 `json:"publish_interval_blocks" yaml:"publish_interval_blocks"` +} + +func DefaultParams() Params { + return Params{ + PublishIntervalBlocks: 144, // ~daily at 10min blocks + } +} diff --git a/x/mirror/types/types_test.go b/x/mirror/types/types_test.go new file mode 100644 index 0000000..f24d51c --- /dev/null +++ b/x/mirror/types/types_test.go @@ -0,0 +1,43 @@ +package types_test + +import ( + "testing" + + "github.com/oy/openyield/x/mirror/types" +) + +func TestDefaultPublishInterval(t *testing.T) { + params := types.DefaultParams() + if params.PublishIntervalBlocks != 144 { + t.Errorf("PublishIntervalBlocks = %d, expected 144 (~daily at 10min blocks)", params.PublishIntervalBlocks) + } +} + +func TestModuleName(t *testing.T) { + if types.ModuleName != "mirror" { + t.Errorf("ModuleName = %s, expected 'mirror'", types.ModuleName) + } +} + +func TestNavPerBreadStruct(t *testing.T) { + nav := types.NavPerBread{ + Value: 10000, + Timestamp: 1700000000, + Attestations: 7, + AuditRef: "audit-001", + } + if nav.Attestations < 6 { + t.Error("NAV attestations should be ≥6 for quorum (§7)") + } +} + +func TestReserveRatioQuorum(t *testing.T) { + ratio := types.ReserveRatio{ + Ratio: 1.0, + Timestamp: 1700000000, + QuorumMet: true, + } + if !ratio.QuorumMet { + t.Error("ReserveRatio should have QuorumMet=true for valid publication (§7)") + } +} diff --git a/x/still/types/types.go b/x/still/types/types.go new file mode 100644 index 0000000..27e8eca --- /dev/null +++ b/x/still/types/types.go @@ -0,0 +1,63 @@ +package types + +import "encoding/json" + +const ( + ModuleName = "still" + StoreKey = ModuleName + RouterKey = ModuleName + QuerierRoute = ModuleName +) + +// StillState represents the pause/resume state (§3: Still/Stir) +type StillState string + +const ( + // StirActive — normal operation, Forge/Fold enabled + StirActive StillState = "StirActive" + // StillPaused — Forge/Fold blocked until Stir + StillPaused StillState = "StillPaused" +) + +// StillReason defines why the system was stilled +type StillReason string + +const ( + StillCouncilVote StillReason = "CouncilVote" + StillQuorumLost StillReason = "QuorumLost" + StillSecurityAlert StillReason = "SecurityAlert" +) + +// StillRecord tracks a pause event +type StillRecord struct { + State StillState `json:"state" yaml:"state"` + Reason StillReason `json:"reason" yaml:"reason"` + Timestamp int64 `json:"timestamp" yaml:"timestamp"` + TriggeredBy string `json:"triggered_by" yaml:"triggered_by"` +} + +// Params for the still module +type Params struct { + AutoStillOnQuorumLost bool `json:"auto_still_on_quorum_lost" yaml:"auto_still_on_quorum_lost"` +} + +func DefaultParams() Params { + return Params{ + AutoStillOnQuorumLost: true, // §4.2: Forge/Fold blocked until quorum restored + } +} + +// GenesisState defines the still module genesis state +type GenesisState struct { + Params Params `json:"params" yaml:"params"` + State *StillRecord `json:"state" yaml:"state"` +} + +func DefaultGenesisState() *GenesisState { + return &GenesisState{ + Params: DefaultParams(), + State: &StillRecord{State: StirActive}, + } +} + +func ValidateGenesis(bz json.RawMessage) error { return nil } diff --git a/x/still/types/types_test.go b/x/still/types/types_test.go new file mode 100644 index 0000000..9f12e7e --- /dev/null +++ b/x/still/types/types_test.go @@ -0,0 +1,30 @@ +package types_test + +import ( + "testing" + + "github.com/oy/openyield/x/still/types" +) + +func TestDefaultStillState(t *testing.T) { + state := types.DefaultGenesisState() + if state.State.State != types.StirActive { + t.Errorf("Default state = %s, expected StirActive", state.State.State) + } +} + +func TestAutoStillOnQuorumLost(t *testing.T) { + params := types.DefaultParams() + if !params.AutoStillOnQuorumLost { + t.Error("AutoStillOnQuorumLost should be true (§4.2: Forge/Fold blocked until quorum restored)") + } +} + +func TestStillStates(t *testing.T) { + if types.StirActive != "StirActive" { + t.Errorf("StirActive = %s, expected 'StirActive'", types.StirActive) + } + if types.StillPaused != "StillPaused" { + t.Errorf("StillPaused = %s, expected 'StillPaused'", types.StillPaused) + } +} diff --git a/x/watcher/types/types.go b/x/watcher/types/types.go new file mode 100644 index 0000000..4d8022a --- /dev/null +++ b/x/watcher/types/types.go @@ -0,0 +1,120 @@ +package types + +import "encoding/json" + +const ( + // ModuleName is the name of the watcher module + ModuleName = "watcher" + + // StoreKey is the string store key + StoreKey = ModuleName + + // RouterKey is the message route for the watcher module + RouterKey = ModuleName + + // QuerierRoute is the querier route for the watcher module + QuerierRoute = ModuleName + + // MinBond is the minimum bond for a Watcher: 100,000 Bread (§7) + // Bread atomic unit = Grain; 1 Bread = 10,000 Grain + MinBond = 100000 + + // Quorum is the minimum attestation count: 6-of-9 (§7) + Quorum = 6 + + // MaxWatchers is the maximum active Watcher count: 9 (§7) + MaxWatchers = 9 + + // AttestationEpochHours is the daily attestation window (§7) + AttestationEpochHours = 24 +) + +// Parameter store keys +var ( + KeyActiveWatchers = []byte("ActiveWatchers") + KeyBondAmount = []byte("BondAmount") +) + +// Watcher represents a bonded Watcher (§7) +type Watcher struct { + WatcherID string `json:"watcher_id" yaml:"watcher_id"` + BondAmount int64 `json:"bond_amount" yaml:"bond_amount"` + Jurisdiction string `json:"jurisdiction" yaml:"jurisdiction"` + Organization string `json:"organization" yaml:"organization"` + ActiveSince int64 `json:"active_since" yaml:"active_since"` + Slashes uint32 `json:"slashes" yaml:"slashes"` + LastAttestation int64 `json:"last_attestation" yaml:"last_attestation"` +} + +// Attestation is a daily reserve snapshot signed by a Watcher (§7) +type Attestation struct { + WatcherID string `json:"watcher_id" yaml:"watcher_id"` + Snapshot ReserveSnapshot `json:"snapshot" yaml:"snapshot"` + Signature []byte `json:"signature" yaml:"signature"` + Timestamp int64 `json:"timestamp" yaml:"timestamp"` +} + +// ReserveSnapshot is the off-chain reserve state attested by Watchers (§7) +type ReserveSnapshot struct { + OffChainTotal int64 `json:"off_chain_total" yaml:"off_chain_total"` + OnChainBread int64 `json:"on_chain_bread" yaml:"on_chain_bread"` + BasketBreakdown []BasketAsset `json:"basket_breakdown" yaml:"basket_breakdown"` + Timestamp int64 `json:"timestamp" yaml:"timestamp"` + AuditorRef string `json:"auditor_ref" yaml:"auditor_ref"` +} + +// BasketAsset represents one asset in the Root Basket (§6) +type BasketAsset struct { + AssetType string `json:"asset_type" yaml:"asset_type"` + Amount int64 `json:"amount" yaml:"amount"` +} + +// QuorumStatus represents the current quorum state +type QuorumStatus string + +const ( + QuorumMet QuorumStatus = "QuorumMet" // ≥6 valid sigs in last epoch + QuorumLost QuorumStatus = "QuorumLost" // <6 — Forge/Fold stilled + StaleEpoch QuorumStatus = "StaleEpoch" // no attestations in 24h +) + +// SlashReason defines why a Watcher was slashed +type SlashReason string + +const ( + SlashInsufficientBond SlashReason = "InsufficientBond" + SlashStaleAttestation SlashReason = "StaleAttestation" + SlashGeographicCollusion SlashReason = "GeographicCollusion" + SlashForgery SlashReason = "Forgery" +) + +// Params defines the parameters for the watcher module +type Params struct { + MinBondAmount int64 `json:"min_bond_amount" yaml:"min_bond_amount"` + QuorumCount uint32 `json:"quorum_count" yaml:"quorum_count"` + MaxWatchers uint32 `json:"max_watchers" yaml:"max_watchers"` +} + +// DefaultParams returns default watcher module parameters +func DefaultParams() Params { + return Params{ + MinBondAmount: int64(MinBond) * 10000, // 100,000 Bread in Grain + QuorumCount: Quorum, + MaxWatchers: MaxWatchers, + } +} + +// GenesisState defines the watcher module genesis state +type GenesisState struct { + Params Params `json:"params" yaml:"params"` + Watchers []Watcher `json:"watchers" yaml:"watchers"` +} + +func DefaultGenesisState() *GenesisState { + return &GenesisState{ + Params: DefaultParams(), + Watchers: []Watcher{}, + } +} + +func ValidateGenesis(bz json.RawMessage) error { return nil } diff --git a/x/watcher/types/types_test.go b/x/watcher/types/types_test.go new file mode 100644 index 0000000..9f75ae2 --- /dev/null +++ b/x/watcher/types/types_test.go @@ -0,0 +1,41 @@ +package types_test + +import ( + "testing" + + "github.com/oy/openyield/x/watcher/types" +) + +func TestMinBond(t *testing.T) { + if types.MinBond != 100000 { + t.Errorf("MinBond = %d, expected 100000 (§7: 100,000 Bread bond)", types.MinBond) + } +} + +func TestQuorum(t *testing.T) { + if types.Quorum != 6 { + t.Errorf("Quorum = %d, expected 6 (§7: 6-of-9 quorum)", types.Quorum) + } +} + +func TestMaxWatchers(t *testing.T) { + if types.MaxWatchers != 9 { + t.Errorf("MaxWatchers = %d, expected 9 (§7: 9 active Watchers)", types.MaxWatchers) + } +} + +func TestAttestationEpoch(t *testing.T) { + if types.AttestationEpochHours != 24 { + t.Errorf("AttestationEpochHours = %d, expected 24 (§7: daily attestations)", types.AttestationEpochHours) + } +} + +func TestDefaultParams(t *testing.T) { + params := types.DefaultParams() + if params.QuorumCount != 6 { + t.Errorf("DefaultParams QuorumCount = %d, expected 6", params.QuorumCount) + } + if params.MaxWatchers != 9 { + t.Errorf("DefaultParams MaxWatchers = %d, expected 9", params.MaxWatchers) + } +}