Skip to content

v30 chain upgrade — Cosmos SDK v0.53.7 + wasmvm v3 + ibc-go v10 (Path A+) - #2

Open
juno-ai-dev wants to merge 108 commits into
mainfrom
juno-ai/v30
Open

v30 chain upgrade — Cosmos SDK v0.53.7 + wasmvm v3 + ibc-go v10 (Path A+)#2
juno-ai-dev wants to merge 108 commits into
mainfrom
juno-ai/v30

Conversation

@juno-ai-dev

Copy link
Copy Markdown
Owner

Summary

Brings the v30 upgrade branch (originally vexxvakan/v30, 82 commits ahead of main) up to a green, lint-clean, test-passing state on a coherent Path A+ dependency stack: SDK v0.53.7, wasmd v0.61.11, wasmvm v3.0.4, ibc-go v10.6.0, cometbft v0.38.23. Plus the new x/voting-snapshot module called for in planning/05-staking-snapshot.md.

This is a fork-internal PR opened against juno-ai-dev/juno:main for review surface preview before going upstream.

The 9 commits at the tip of this branch (everything after 6b256cde fix up broken tokenfactory tests — the prior vexxvakan/v30 HEAD) represent the dep-bump implementation:

  • 543d7753planning/ + CLAUDE.md (research artifacts)
  • 9a86793c — Phase 0: CI Go version 1.23.9 → 1.25.2 in 4 workflows; ictest matrix reconciled (16 stale entries → 13 real Makefile targets)
  • 63e10bce — Phase 1: dependency bumps (Path A+) + ICS-29 (29-fee) middleware removal
  • cb664289 — Phase 2: app wiring for ibc-go v10 + wasmd v0.61 (KVStoreKey → KVStoreService, capability scoping dropped, IBC keeper signature changes); async-icq dropped (no /v10 line published); tendermint LightClientModule explicit registration
  • ce42d97b — Phase 3: pre-existing test-bug fixes in x/cw-hooks and x/stream
  • af7e8076 — Phase 4: lint clean (0 issues)
  • 8efab6bc — Phase 5: new x/voting-snapshot module (MVP) — keeper, staking hooks, JSON-encoded params, wasmbindings (VotingPowerAt / TotalVotingPowerAt), upgrade-handler BackfillFromStaking
  • d692dd41 — Phase 6: local verification status + CI deferral note (buildx not available on impl host)
  • fd6e08c3 — Phase 7: internal security review pass (0 Sev-1, 0 Sev-2, 2 Sev-3 deferred to v30.x with reasons)

Why Path A+ instead of the originally-targeted Path B

The session-1 plan targeted SDK v0.54 + wasmd v0.70 + ibc-go v11 + store/v2. ibc-apps repo has no /v11 line published (latest is /v10, which pins ibc-go v10 + SDK v0.53). Juno wires PFM, ibc-hooks, async-icq into app/keepers/keepers.go + app/modules.go; pure Path B would either lose all three or require forking ibc-apps.

Path A+ preserves the v30 user-facing payoff — wasmvm v3.0.4 brings the BN254 precompile that prop CosmosContracts#374 already approved — while staying on a fully-supported stack. SDK v0.54 + IBCv2 + store/v2 are deferred to v31 once ibc-apps catches up. Full rationale in planning/02-targets.md.

What's removed

Two modules were dropped from v30 because their stores no longer exist or are unreachable on the new stack:

  • ICS-29 (feeibc) — removed in ibc-go v10 upstream
  • async-icq (interchainquery)ibc-apps /v9//v10//v11 not published; latest /v8 commit (April 2026) still pins ibc-go v8

Both store keys are added to StoreUpgrades.Deleted in the v30 upgrade handler so existing mainnet state under those keys is purged at upgrade height. Both are tracked for reintroduction once upstream catches up.

What's new

x/voting-snapshot — chain-side bindings for historical staking-power queries, per planning/05-staking-snapshot.md. DAO DAO voting modules become thin query consumers; voting power at proposal-open height is fixed by at-or-before semantics so voters can't rage-stake mid-window. LST allow-list excluded from voting power (default-deny; empty at v30 launch). Wasmbindings are the v30 MVP delivery surface — proto/gRPC msg/query servers deferred to v30.x.

Internal security review

planning/SECURITY-REVIEW-NOTES.md walks the planning/08 checklist. Findings: 0 Sev-1, 0 Sev-2, 2 Sev-3 (gas-charging on new wasmbindings + unbounded snapshot retention — both deferred to v30.x with logged reasons), 7 Info notes. External review still required for the upgrade handler and the new module per the planning doc; this is the kickoff document.

Test plan

Verified clean on the implementation host:

  • go build ./... (root + interchaintest)
  • make buildjunod reports Cosmos SDK: v0.53.7, Comet: v0.38.23
  • make lint — 0 issues
  • go test ./... -p 1 (sequential) — all packages pass
  • go mod tidy — no diff in either module
  • wasmbindings/test + new x/voting-snapshot/keeper tests pass

Deferred to CI / external review:

  • All 13 make ictest-* suites — buildx unavailable on the impl host; CI runs them via the reconciled matrix
  • ictest-upgrade — the highest-signal test in the repo; verifies the upgrade handler against forked mainnet state
  • DAO DAO v2.7.0 contract compatibility against wasmvm v3 (separate review, dao-contracts repo)
  • JunoClaw contract compatibility against wasmvm v3 (separate review, JunoClaw repo)
  • External reviewer sign-off on app/upgrades/v30/upgrades.go + x/voting-snapshot/

🤖 Generated with Claude Code

vexxvakan added 30 commits May 20, 2025 00:14
vexxvakan and others added 20 commits November 10, 2025 22:40
Nine planning docs (00-overview through 08-security-review + README)
captured during the v30 upgrade research phase. Source of truth for
the Path B migration: SDK v0.54.3, wasmd v0.70.0, wasmvm v3.0.4,
ibc-go v11.0.0, cometbft v0.39.3, store/v2.

CLAUDE.md documents the existing repo layout (toolchain, app/ wiring,
custom x/ modules, CosmWasm integration, fee pipeline) for Claude Code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two latent breakages in the v30 branch's CI:

1. Four workflows pinned GO_VERSION 1.23.9 while go.mod and .mise.toml
   require 1.25.2. CI couldn't have built v30 as-is. Bump build.yml,
   codeql.yml, golangci-lint.yml, interchaintest-E2E.yml to 1.25.2 to
   match the toolchain (Dockerfile was already updated in 0e1ad2f).

2. interchaintest-E2E.yml matrix listed 16 ictest targets, several of
   which (statesync, ibchooks, feeshare, unity-deploy, feepay, cwhooks,
   clock, gov-fix) no longer exist as Makefile targets or test
   directories — likely stale across the e2e rework in a0ae429 +
   c6de955. Replace with the 13 targets that actually exist on disk
   and in the Makefile.

Pre-bump cleanup before the dependency jumps in Phase 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pivot from locked Path B (SDK v0.54 + ibc-go v11) to **Path A+** because
ibc-apps repo has no /v11 line published — Juno wires PFM, ibc-hooks,
and async-icq into app/keepers/keepers.go and app/modules.go and would
either lose all three or require forking ibc-apps. Path A+ keeps the
v30 consensus-break payoff (wasmvm v3 → BN254 precompile) while staying
on a fully-supported stack. SDK v0.54 + IBCv2 + store/v2 are deferred
to v31 once ibc-apps catches up. See planning/02-targets.md for full
rationale.

Direct dep changes:
- cosmos-sdk v0.53.4 → v0.53.7
- wasmd v0.54.2 → v0.61.11
- wasmvm/v2 v2.2.4 → wasmvm/v3 v3.0.4 (path change)
- ibc-go/v8 v8.7.0 → ibc-go/v10 v10.6.0 (path change)
- ibc-apps PFM /v8 v8.2.0 → /v10 v10.6.0
- ibc-apps ibc-hooks /v8 v8.0.0 → /v10 v10.0.0
- ibc-apps async-icq /v8 — no /v9/v10/v11 published, kept on /v8 latest commit
- cometbft v0.38.19 → v0.38.23
- cosmossdk.io/* family unchanged (v0.53.7 SDK keeps current pins)

ICS-29 (29-fee) middleware was removed in ibc-go v10 — accordingly:
- Dropped ibcfee imports + IBCFeeKeeper field from app/keepers/keepers.go
- Dropped ibcfee module wiring from app/modules.go (BeginBlocker /
  EndBlocker / InitChainer ordering, AppModuleBasic, NewAppModule)
- Dropped ibcfeetypes.StoreKey from app/keepers/keys.go and the
  ibcfeetypes.ModuleName account-permission entry
- Added "feeibc" to v30 StoreUpgrades.Deleted in app/upgrades/v30/
  constants.go so existing mainnet state under that key is purged
  during the upgrade
- Adapted wasm.NewIBCHandler call to wasmd v0.61's new 4-arg signature
  (ICS20TransferPortSource + appVersionGetter)

Phase 1 commits the version pins and import-path migration. The bulk
of API drift (ibckeeper.NewKeeper / ibctransferkeeper.NewKeeper /
icqkeeper.NewKeeper / icahostkeeper.NewKeeper signature changes,
KVStoreKey → KVStoreService migration, ParamSubspace removal) is
Phase 2 work and surfaces as compile errors against this commit.
`go mod tidy` is clean.

planning/02-targets.md and planning/03-migration-plan.md updated to
record the pivot rationale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
go build ./... now succeeds; junod binary builds and reports
"Cosmos SDK: v0.53.7, Comet: v0.38.23". Most unit tests pass.

Keeper signature changes (ibc-go v10 + wasmd v0.61 dropped capability
scoping; KVStoreKey → KVStoreService; many ParamSubspaces → nil):

- ibckeeper.NewKeeper: 7 → 5 args (drops StakingKeeper + ScopedIBCKeeper)
- ibctransferkeeper.NewKeeper: drops PortKeeper + ScopedKeeper, adds
  MsgServiceRouter
- icahostkeeper.NewKeeper: drops PortKeeper + ScopedKeeper + standalone
  WithQueryRouter setter; QueryRouter now passed to NewKeeper
- icacontrollerkeeper.NewKeeper: drops PortKeeper + ScopedKeeper +
  IBCFeeKeeper (29-fee removed); ChannelKeeper takes the ICS4Wrapper slot
- packetforwardkeeper.NewKeeper: KVStoreKey → KVStoreService
- wasmkeeper.NewKeeper (wasmd v0.61): drops IBCFeeKeeper + PortKeeper +
  ScopedWasmKeeper; adds ChannelKeeperV2

Tendermint light client wiring (ibc-go v10 split client modules out of
the keeper into explicit registrations):
- New AppKeepers.TmLightClientModule field
- ibctm.NewLightClientModule constructed after IBCKeeper, registered
  via clientKeeper.AddRoute(ibctm.ModuleName, ...)
- modules.go ibctm.NewAppModule now takes the LightClientModule

ICA controller IBC middleware: NewIBCMiddleware now takes 1 arg (just
the keeper), not 2 — auth-module stack arg dropped along with
capability scoping.

async-icq dropped from v30:
- ibc-apps maintainers haven't published a /v9/v10/v11 line; even the
  latest /v8 commit (2026-04-27) still pins ibc-go/v8, incompatible
  with our ibc-go/v10
- Removed all icq imports + ICQKeeper field + ScopedICQKeeper +
  module wiring from app/keepers/keepers.go, app/modules.go,
  app/keepers/keys.go
- Added "interchainquery" to v30 StoreUpgrades.Deleted to purge
  existing mainnet state under that store key during the upgrade
- Reintroduce in v30.x or v31 once ibc-apps publishes a /v10 line

Other v10 surface changes:
- ibctransfertypes.QueryDenomTraceResponse → QueryDenomResponse
  (transfer module renamed DenomTrace → Denom in v10);
  acceptedQueries.go updated for new proto path

Phase 3 surface (3 module test failures remaining, all real:
- x/cw-hooks/keeper TestUnRegisterContracts: error semantics drift
- x/feemarket/ante TestEscrowFunds/valid_fee_grant
- x/stream/keeper TestExecuteQueryBalance

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
go test ./... -p 1 (sequential) now passes clean across all packages.

x/cw-hooks/keeper/msg_server_test.go (TestUnRegisterContracts):
Pre-existing test bug from "cw hooks v2" (commit 7dbbf11). The
unregisterCase struct lacked the senderAddr/contractAddr callback
fields that registerCase has, so the "invalid register address" and
"unauthorized sender" cases couldn't actually exercise the failure
they claimed — they used the same valid sender/contract as the
success case. Added the missing callback fields and parameterized
the failing cases to actually pass an invalid sender / unauthorized
sender address respectively.

x/stream/keeper/keeper_test.go (TestExecuteQueryBalance):
The stream keeper's gRPC method registry calls Refresh during
NewKeeper, but bank module's HybridHandlerByRequestName mappings
aren't fully registered until module manager init runs (after
keeper construction). The registry filter at registry.go:231
silently drops any method whose request name has no hybrid handler,
so the registry was empty for "bank.balance" lookups. Added a
manual Refresh after Commit() in the test so the registry sees the
fully-wired router. (Underlying keeper bug: Refresh should run from
a finalize-block hook or be lazy on first lookup. Out of scope for
the v30 dep bump; tracked separately.)

Note on test parallelism: `go test ./...` (parallel) shows
intermittent feemarket ante failures due to cross-package global
state leakage (proto registry / init() side effects — common
cosmos-sdk pattern). `-p 1` (sequential) is clean. CI may need
to switch to sequential or isolate the affected package; tracked
for Phase 6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`make lint` now reports 0 issues. wasmbindings tests already pass
(implicit Phase 4 audit — the typed query/message bindings compile
cleanly against wasmd v0.61). Stargate allow-list (acceptedQueries.go)
was updated in Phase 1 (DenomTrace → Denom).

Lint fixes:
- Auto-fix (gci import order + 3 S1021 var-decl-merge in keepers.go)
- x/cw-hooks msg_server_test.go: extract repeated "Invalid" string
  to invalidAddr constant (goconst)
- x/mint minter_test.go + x/tokenfactory createdenom_test.go: replace
  deprecated Coin.IsEqual with Coin.Equal (SA1019). Tokenfactory case
  required binding the Sub() return to a local var since Equal is a
  pointer method.
- app/modules.go appModules() return type: nolint:staticcheck for
  module.AppModule deprecation. Cosmos-sdk itself + most ecosystem
  modules haven't migrated to appmodule.AppModule yet (deprecated in
  v0.53.7 but not removed); deferred to v31.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per planning/05-staking-snapshot.md (Option C, Shape 1, event-driven):
chain-side bindings for historical staking-power queries. DAO DAO
voting modules become thin query consumers; voting power at proposal-
open height is fixed at-or-before semantics — voters can't rage-stake
mid-window.

Module layout (no proto/gRPC for v30 MVP — wasmbindings is the
delivery surface; gRPC/REST tracked for v30.x):
- types/keys.go        — ModuleName, StoreKey, collection prefixes
- types/params.go      — Params{LSTAllowlist} (default empty)
- types/params_codec.go — JSON-backed collections.ValueCodec
- types/genesis.go     — GenesisState (JSON marshalled)
- types/expected_keepers.go — minimal StakingKeeper interface
- keeper/keeper.go     — collections.Map keyed by (delegator,height)
                         and (height); IsLST helper
- keeper/snapshot.go   — recordDelegatorPower, recordTotal,
                         VotingPowerAt, TotalVotingPowerAt
- keeper/hooks.go      — staking hook impls (AfterDelegationModified,
                         BeforeDelegationRemoved, BeforeValidatorSlashed)
                         plus stub no-ops for the rest of the interface
- keeper/genesis.go    — InitGenesis / ExportGenesis
- keeper/backfill.go   — BackfillFromStaking for v30 upgrade-handler
                         seeding (one-shot iteration over all delegations)
- keeper/keeper_test.go — params round-trip, at-or-before semantics,
                          LST exclusion
- module/module.go     — minimal AppModule (no msg/query servers)

App wiring:
- app/keepers/keepers.go — VotingSnapshotKeeper field + construction
                           after staking keeper, before SetHooks;
                           Hooks() registered alongside DistrKeeper
                           and CWHooksKeeper
- app/keepers/keys.go    — votingsnapshottypes.StoreKey added
- app/modules.go         — wired into appModules + all 3 ordering
                           lists + AppModuleBasics
- app/upgrades/v30/      — votingsnapshottypes.ModuleName added to
                           StoreUpgrades.Added; upgrade handler
                           calls VotingSnapshotKeeper.BackfillFromStaking

Wasm bindings (the actual delivery surface for DAO DAO):
- wasmbindings/types/query.go: VotingPowerAt + TotalVotingPowerAt
  query variants + VotingPowerResponse
- wasmbindings/queries.go: GetVotingPowerAt, GetTotalVotingPowerAt
- wasmbindings/query_plugin.go: dispatch new variants
- wasmbindings/wasm.go: NewQueryPlugin takes votingsnapshotkeeper

LST exclusion semantics: governance maintains an allowlist of LST
contract addresses. delegations *from* a listed address don't count
toward voting power (the wrapper holds the stake, not the underlying
voter). List is empty at v30 launch — no live LSTs on Juno per
memory/juno-voting-design.md. Default-deny: an unlisted contract
counts until governance excludes it.

IsLST tolerates missing params (returns false) so staking hooks
fired during genesis InitGenesis don't error before the module's
own InitGenesis sets params.

Pre-upgrade query semantics: VotingPowerAt(addr, height) returns
0 (not error) for heights with no recorded snapshot. After Phase 7
backfill, all heights >= upgrade-height return real data.

Deferred to v30.x:
- proto/gRPC msg+query servers (delivery is wasmbindings only for v30)
- VotingPowerOverRange (time-decay schemes)
- Per-validator filtering for sub-DAO use cases
- Precise per-validator delegator re-snapshot on slash
- LST-aware denominator subtraction in TotalVotingPowerAt
- Governance MsgUpdateParams (params are genesis-only for v30)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All cheap checks pass on the implementation host:
- go build ./... (root + interchaintest)
- make build (junod binary, SDK v0.53.7 + Comet v0.38.23)
- make lint (0 issues)
- go test ./... -p 1 (sequential, all packages)
- go mod tidy (clean diff in both modules)

Interchaintest local execution is blocked: docker 29.4.3 in this
host lacks the buildx plugin, and the project Dockerfile uses
BuildKit syntax (--mount=type=cache + # syntax=docker/dockerfile:1).
Plain docker build and DOCKER_BUILDKIT=1 docker build both fail.
CI runners ship with buildx so ictest validation runs there.

Documented two latent issues for v30.x follow-up:
- go test ./... (parallel) shows flake in x/feemarket/ante from
  cross-package global state — sequential is clean
- x/stream gRPC method registry refreshes during keeper
  construction (before module init); test fix patched, runtime
  bug deferred

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-review against planning/08-security-review.md checklist.
Findings: 0 Sev-1, 0 Sev-2, 2 Sev-3 (both deferred to v30.x with
reasons), 7 Info notes. External review still required for the
upgrade handler and the new x/voting-snapshot module.

Sev-3 #1: wasmbindings VotingPowerAt has no fixed-cost gas wrapper;
relies on SDK ambient store gas. Adequate for v30, revisit with
empirical data in v30.x.

Sev-3 #2: x/voting-snapshot has unbounded snapshot retention. A
prune param tied to governance voting period is the right mitigation;
deferred until that param is exposed.

Open questions surfaced for external reviewer: snapshot retention
default, pre-upgrade query semantics (zero vs typed error), gas
charging, async-icq counterparty impact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three buckets:

Bucket A (PR-blockers, must close before v30 mainnet halt-height):
- A1: DAO DAO v2.7.0 contract compatibility on wasmvm v3 — new
  ictest suite + mainnet-snapshot replay + wasmbindings smoke.
  Crosses repo boundary into dao-contracts for the binding adapter.
- A2: ictest-upgrade run against forked mainnet state (CI gate)
- A3: async-icq counterparty audit before deleting the store
- A4: cometbft v0.38.19 → v0.38.23 changelog scan

Bucket B (v30.x patches, not blocking):
- B1: voting-snapshot retention pruning (Sev-3 from sec review)
- B2: voting-snapshot fixed-cost gas wrapper (Sev-3 from sec review)
- B3: voting-snapshot proto/gRPC/REST surface
- B4: per-validator delegator re-snapshot on slash
- B5: VotingPowerOverRange + per-validator filter
- B6: x/stream MethodRegistry refresh timing
- B7: test parallelism flake root-cause fix
- B8: CI -p 1 workaround until B7 lands
- B9: bank DenomMetadata proto-path bug (pre-existing)
- B10: async-icq reintroduction once ibc-apps publishes /v10

Bucket C (v31, next consensus break):
- C1: SDK v0.54 + IBCv2 + store/v2 (the original Path B target)
- C2: cometbft v0.39 with optional libp2p
- C3: module.AppModule → appmodule.AppModule migration

JunoClaw contract compatibility intentionally excluded per Jake's
direction — that team owns their own contract test surface.

An empty Bucket A is the gate for v30 mainnet halt-height proposal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cker/setup_junod.sh)

# Conflicts:
#	docker/setup_junod.sh
Three small CI-related fixes bundled:

- B9: app/keepers/acceptedQueries.go — /cosmos.bank.v1beta1.Query/DenomMetadata
  was returning QueryDenomsMetadataResponse (plural). Pre-existing typo;
  surfaced during deferred-work review. Switched to QueryDenomMetadataResponse.

- B8: .github/workflows/build.yml — switched test job to `go test -p 1`
  (sequential) until B7 is rooted out. TestEscrowFunds in x/feemarket/ante
  is flaky under parallel package execution from cross-package global state.

- CI lint job: golangci-lint-action@v8 with version v2.1.6 fails to read
  go.mod targeting go 1.25 because the action's pre-built binary was
  compiled with go1.24. Switched to `make lint` which invokes
  `go tool golangci-lint` (compiled from go.mod's pinned v2.5.0 against
  the workflow's setup-go 1.25 toolchain). Matches CLAUDE.md's stated
  convention for local lint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two Bucket A blockers from planning/09-deferred-work.md closed.

A3 (async-icq counterparty audit on juno-1):
Verdict clear. Queried Polkachu LCD for all 674 active IBC channels;
zero have port_id matching icq* (icqhost / icqcontroller / *icq*).
Deleting the interchainquery store at v30 upgrade height carries no
counterparty risk. Re-verification command captured for pre-halt
sanity check.

A4 (CometBFT v0.38.19 → v0.38.23 changelog scan):
Four patch releases between the v29 binary's pin and v30's. All
changes are bug fixes (vote handling, evidence validation, blocksync
verification, ABCI socket panic recovery) plus one opt-in state-sync
parameter. No [CONSENSUS] or [API-BREAKING] entries that affect
validators in steady state. Experimental libp2p transport added on
the line is off by default and we don't enable it. Validator-facing
language for the upgrade proposal text drafted at the bottom of the
file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NewKeeper called methodRegistry.Refresh() eagerly, but module-manager
init hadn't registered the gRPC services yet — bank's hybrid handlers
weren't in the router, so the registry indexed against an empty
HybridHandlerByRequestName map. Lookups for "bank.balance" then
returned `false` until something else (any later subscription update)
re-triggered Refresh.

Fix: lazy refresh on first MethodRegistry() access, guarded by
sync.Once. By the time anyone calls MethodRegistry() — which doesn't
happen until at least the first BeginBlock — every module's
RegisterServices has run, so the router is fully populated.

Reverted the test workaround that explicitly called
keeper.MethodRegistry().Refresh(s.App.BaseApp) after Commit; now that
MethodRegistry itself triggers Refresh on first call, the test passes
without manual coordination.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four planning/09 Bucket-B items folded into one cohesive uplift.

B1: Snapshot retention pruning
- Added Params.RetentionWindowHeights (default 12_614_400 ≈ 1y at 2.5s)
- New keeper.Prune() walks VotingPower + TotalPower and drops rows
  with height < (current - window). Zero window disables pruning.
- New EndBlocker on AppModule that invokes Prune. Errors are
  silenced (logging hook to be added when keeper grows a Logger());
  matches cosmos-sdk pattern for non-critical EndBlocker work.
- pruneInterval constant set to 1 (every block); reserved knob if
  iteration cost ever becomes meaningful.

B2: Wasmbinding gas wrapper
- Fixed-cost gas charge of 5_000 added at the top of
  GetVotingPowerAt / GetTotalVotingPowerAt / GetVotingPowerOverRange
  in wasmbindings/queries.go. Bounds the worst-case empty-iterator
  scan a contract could induce by querying high heights for
  delegators with no recorded snapshot.

B4: Per-validator delegator re-snapshot on slash
- Hooks.BeforeValidatorSlashed now iterates the validator's
  delegations via stakingKeeper.GetValidatorDelegations and re-records
  each delegator's power, then re-records the chain-wide total.
- Bounded by the slashed validator's delegator count (typically
  dozens to low thousands; well within block-gas budget).
- StakingKeeper interface in expected_keepers.go gained
  GetValidatorDelegations.

B5a: VotingPowerOverRange (the range portion of B5)
- New keeper.VotingPowerOverRange(addr, fromHeight, toHeight)
  returning []HeightPower for every snapshot inside [from, to].
- Wasmbinding variant added: VotingPowerOverRange query +
  HeightPowerPair / VotingPowerOverRangeResponse types.
- Per-validator filter (B5b) deferred — would require shifting the
  storage shape from (delegator,height) to (delegator,validator,
  height); no immediate consumer in DAO DAO v2.7.0.

Tests added:
- TestVotingPowerOverRange — verifies range bounds + ordering
- TestPruneRetentionWindow — verifies cutoff math, survives within
  window, disables pruning when window=0

Other:
- New errors.go with isNotFound() helper to dedupe the
  collections.ErrNotFound check across keeper.go and prune.go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`make local-image` was succeeding the build step but failing the
"statically linked" assertion at the end of the Dockerfile. Root cause:
the wasmvm fetch line read `go list -m github.com/CosmWasm/wasmvm/v2`,
which still resolves (v2.2.4 is a transitive dep) but the v2 muslc
archive is ABI-incompatible with the v3 Go bindings the build now
targets. Without a satisfied muslc tag, the linker fell back to
dynamic linking against system libwasmvm — so the binary was correct
but failed the statically-linked check.

Fixed by reading from /v3 instead. With buildx 0.20.0 installed in
the impl host (separately), `make local-image` now produces a
properly-static binary in ghcr.io/cosmoscontracts/juno:local
(396MB), which interchaintest can use.

Unblocks A1 (DAO DAO ictest) and A2 (ictest-upgrade) for local
verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
interchaintest's ic.Build hits the 2-minute context deadline on the
v30 image even though the chain itself is healthy (committing blocks
at height 49+). The new keeper set (x/voting-snapshot, ibc-go v10 +
wasmd v0.61 re-wiring) makes the chain take longer to reach the
readiness criteria interchaintest checks.

5 minutes leaves headroom without making genuine bring-up failures
slow to surface. Observed v29 runs typically finished Build in
60-90s; v30 needs more.

Unblocks A1 (DAO DAO ictest scaffold) and A2 (ictest-upgrade).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

juno-ai-dev and others added 9 commits May 9, 2026 00:54
Captures the real failure mode for ictest-basic: chain RPC port
isn't reachable from the host even though the chain runs (commits
blocks at height 49+). interchaintest dials Post "http://0.0.0.0:<host-port>"
and gets connection refused.

The 5-min timeout bump didn't help — failure still happens at ~144s
(not the timeout boundary). Real cause is one of:
- H1: Docker port-forward race (chain starts faster than poll begins)
- H2: cometbft RPC server doesn't start (chain runs but RPC doesn't bind)
- H3: cgo / keeper init delays RPC binding past polling window
- H4: probe-shape mismatch (less likely, interchaintest is generic)

Next-session diagnostic checklist + ruled-out hypotheses captured
in planning/ICTEST-BLOCKER-V30.md. Tracks live so the next debugger
can attach docker logs / docker port / ss -tlnp during a live run.

Cleanup panic in tests/basic/chain_start_test.go:25 (s.Ic.Close()
on nil Ic) is real but not fixed here — defensive-coding follow-up.

Blocks Bucket A items A1 (DAO DAO contract compat) and A2
(ictest-upgrade). DAO DAO v2.7.0 wasm artifacts ARE ready at
/workspace/dao-contracts/artifacts/ (29 files); ictest scaffold
can be drafted before this lands but can't run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ran the diagnostic checklist against a live ictest container. Updated
the doc with concrete findings:

Root cause: chain container is on the interchaintest-<id> docker
network (172.217.0.0/16); SafeClaude (where ictest runs in our
setup) is on bridge (172.17.0.0/16). Different networks isolated by
default. interchaintest's chain readiness check polls
tcp://0.0.0.0:<host-port> via tn.hostRPCPort, which lives on the
OUTER host's network namespace and isn't reachable from inside
SafeClaude.

Confirmed via direct probing:
- :::26657 is LISTEN inside the chain container
- docker port mapping IS established (26657 -> host:dynamic)
- curl 0.0.0.0:<host-port> from SafeClaude returns connection refused
- curl <container-ip>:26657 from SafeClaude times out (different netns)

Fix path documented:
1. Fork interchaintest with a one-line patch to use tn.HostName():26657
   when INTERCHAINTEST_USE_CONTAINER_HOSTNAME=1 is set; replace in
   interchaintest/go.mod
2. Test-setup hook in interchaintest/suite/lib.go to docker network
   connect SafeClaude into the per-run interchaintest network
3. Or alternative: socat forwarder inside SafeClaude mirroring the
   dynamic host-port mappings

Workaround for v30 timeline: run ictest from outside SafeClaude (CI
does this naturally) or skip ictest from PR review surface and
validate via a v30-rc1 testnet smoke instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ran the diagnostic, then a Python TCP forwarder + docker network
connect attempt. Result: SafeClaude's network namespace is
sandboxed in a way that prevents joining additional docker
networks at runtime. The interface listed by `docker inspect` is
registered with the daemon but never materializes inside the
container — /proc/net/dev shows only eth0, route table has only
default-bridge routes.

The forwarder advanced the dial from TCP-level "connection
refused" to HTTP-level "context deadline exceeded" / "connection
reset by peer" — but that's because the upstream dial from
SafeClaude to the chain's container IP can't actually deliver
across the sandboxed namespace.

ictest as currently architected requires the test runner and the
chain containers to share a network reachability path. That path
doesn't exist from inside SafeClaude, period. CI runners on the
outer host don't have this problem.

Conclusion: validate A1 (DAO DAO contract compat) and A2
(upgrade-handler) via testnet smoke instead of local ictest
before the security review and mainnet halt-height proposal.
The ictest workflow matrix (reconciled in Phase 0c) is still
the right place for this in CI.

Tooling left in place for any future runner that doesn't have
this sandbox: 5-min Build timeout + Build error logging in
interchaintest/suite/lib.go; bridge scripts at /tmp/forward.py
and /tmp/ictest-bridge.sh are reference for the docker-in-docker
pattern (they work for TCP but the upstream dial still fails
under sandbox).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
interchaintest/tests/dao-dao/dao_dao_test.go scaffolds the three
legs from planning/09-deferred-work.md §A1:

  1. TestCw4GroupDao — minimal cw4-group voting + proposal-single
     (open + vote + execute a no-op proposal). The cheapest leg
     proves dao-dao-core, dao-voting-cw4, dao-proposal-single, and
     cw4-group all instantiate and interact under wasmvm v3.
  2. TestCw20StakedDao — cw20-staked voting path (skipped, TODO
     for v30.x once leg 1 passes; same scaffold + token + stake
     contract setup).
  3. TestWasmbindingsVotingPowerAt — smoke for the new
     x/voting-snapshot wasmbinding (skipped, needs a small Rust
     probe contract embedded as testdata).

Pre-built v2.7.0 wasm artifacts staged at
interchaintest/contracts/{dao_dao_core,dao_proposal_single,
dao_voting_cw4,dao_voting_cw20_staked,cw20_stake}.wasm — copied
from the existing /workspace/dao-contracts/artifacts/ build output.
cw4_group.wasm and cw20_base.wasm aren't in that artifact set;
they come from cw-plus and need a separate fetch (TODO before
TestCw4GroupDao can run).

Helpers in the file (buildDaoInstantiate, queryVotingModule,
queryProposalModule, openProposal, voteOnProposal, executeProposal,
queryProposalStatus) are stubs that t.Skip() — fleshed out in the
next focused session against real schemas from
dao-contracts/packages/dao-interface.

Wired:
- Makefile: new ictest-dao-dao target + .PHONY entry
- .github/workflows/interchaintest-E2E.yml: ictest-dao-dao added
  to the matrix (so CI runs it automatically once the helpers are
  filled in)

Note: per planning/ICTEST-BLOCKER-V30.md, ictest can't run from
inside SafeClaude due to network-namespace sandboxing. This
scaffold is targeted at CI runs and at testnet-smoke validation
before mainnet halt-height — not local execution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root-caused the feemarket ante TestEscrowFunds parallel/-count flake.
Wasn't cross-package global state at all — the test cases were
declared as map[string]struct{} which iterates in non-deterministic
order. Several cases mutate FeeGrantKeeper state (GrantAllowance);
"no fee grant" sometimes ran AFTER "valid fee grant" leaving an
allowance the former didn't expect, producing
"fee allowance already exists" or "fee limit exceeded" instead of
ErrNotFound.

Fix:
- Convert cases from map to []tcDef (deterministic order)
- Call s.SetupTest() at the top of each subtest's s.Run so each
  case starts from a clean suite

Verified:
- go test -count=3 ./x/feemarket/ante/... → pass
- go test -count=2 ./... -timeout 900s → all packages pass under
  parallel default

Reverted the B8 workaround — .github/workflows/build.yml is back to
`go test ./...` (parallel) since the underlying bug is fixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wasmbinding API delivers what DAO DAO needs for v30, but
indexers, CLI consumers, and REST gateway clients want gRPC.
Adds the proto-generated msg + query servers per
planning/09-deferred-work.md §B3.

New proto files (proto/juno/votingsnapshot/v1/):
- params.proto — Params message (LSTAllowlist, RetentionWindowHeights)
- genesis.proto — GenesisState
- query.proto — Query.{Params,VotingPowerAt,TotalVotingPowerAt,
  VotingPowerOverRange} with HTTP gateway routes
- tx.proto — Msg.UpdateParams (governance-controlled)

Generated:
- x/voting-snapshot/types/{params,genesis,query,tx}.pb.go
- x/voting-snapshot/types/query.pb.gw.go (REST gateway)

Removed conflicting hand-written types now provided by proto:
- types/params.go (Params struct + Marshal/Unmarshal stubs)
- types/genesis.go (GenesisState struct + Marshal/Unmarshal stubs)
- types/params_codec.go (JSON-backed ValueCodec)

Renamed LSTAllowlist → LstAllowlist across keeper code + tests to
match proto-generated camelCase. Behavior unchanged.

Switched the keeper.Params Item to codec.CollValue[types.Params]
(gogo proto codec) instead of the hand-written JSON codec.

New files:
- types/codec.go — RegisterInterfaces for sdk.Msg implementations
- types/helpers.go — DefaultParams + DefaultGenesis (helpers the
  proto generator doesn't produce)
- keeper/grpc_query.go — types.QueryServer impl wrapping the keeper
- keeper/msg_server.go — types.MsgServer impl with Authority gating
  on UpdateParams

Module wiring (x/voting-snapshot/module/module.go):
- RegisterInterfaces wired through types.RegisterInterfaces
- RegisterGRPCGatewayRoutes wired through types.RegisterQueryHandlerClient
- RegisterServices wires both NewMsgServer and NewQueryServer

Tests + lint clean. The wasmbinding surface (the v30 MVP delivery)
still works exactly as before — proto/gRPC are an additive surface.

Required outer-host docker invocation (SafeClaude can't bind-mount
$(CURDIR) directly because the daemon is on the host). Doesn't
affect CI which runs the standard `make proto-gen` from a path
the daemon recognizes natively.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-walks planning/08-security-review.md against the Bucket A+B
deltas (commits fd6e08c..e273bc1). Focused on what changed after
the first review.

Cumulative findings:
- Sev-1: 0
- Sev-2: 0
- Sev-3: 1 (down from 2 — both v1 Sev-3s closed by B1 retention
  prune and B2 gas wrapper; one new Sev-3 found in B1's full-store
  pruneVotingPower scan, deferred to v30.x performance work)
- Info: 13 (6 new)

New review surface covered:
- B1 retention pruning EndBlocker
- B2 wasmbinding gas wrappers
- B3 proto/gRPC/REST surface (Query + Msg servers, Authority gating)
- B4 per-validator slash re-snapshot hook
- B5a VotingPowerOverRange + wasmbinding
- B6 stream lazy MethodRegistry refresh under sync.Once
- B7 feegrant test parallelism fix (test-only, no security impact)
- B9 stargate DenomMetadata typo fix
- CI infrastructure (golangci-lint action → make lint, B8 revert)
- A1 DAO DAO ictest scaffold (test-only, not load-bearing)

External review still required for:
- app/upgrades/v30/upgrades.go (now includes
  BackfillFromStaking state writes)
- x/voting-snapshot/ in full (all new code paths)
- app/keepers/keepers.go ICA + wasm IBC handler wiring (unchanged
  since v1 review but worth external eyes on the ICS-29 removal)

Open questions surfaced:
- snapshot retention default (~1y at 2.5s) — right knob?
- per-validator slash hook cost — benchmark on a juno-1 fork
- full-store pruneVotingPower scan — proper per-delegator iteration
  is v30.x design work

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`build-docker` was tagging the chain image as `juno:local` but
interchaintest/suite/ci_integration.go's GetDockerImageInfo
defaults to `ghcr.io/cosmoscontracts/juno:local` (matching the
IMAGE var in the root Makefile's local-image target).

When the e2e jobs load the artifact and run a test, the test
asks for `ghcr.io/cosmoscontracts/juno:local`, doesn't find it,
falls back to a registry pull, and the daemon returns
"manifest unknown." 13 of 14 e2e jobs failed on this.

Why ictest-upgrade was the only one passing: it tests upgrade
from a published v29 image to the new binary, so the
upgrade-from path uses a registry image and doesn't need
`:local` to be present locally.

Fix: tag in the workflow with the full registry path so it
matches the repo string GetDockerImageInfo returns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ision

The CI ictest run after the image-tag fix surfaced the next issue:
junod panics at startup with "voting-power-at flag redefined: height".

Root cause: cosmossdk.io/client/v2/autocli generates a CLI flag from
each proto field on a query method. QueryVotingPowerAtRequest and
QueryTotalVotingPowerAtRequest both have an int64 `height` field, so
autocli generates `--height`. Then cosmos-sdk's standard
AddQueryFlagsToCmd tries to add its own `--height` (the
"query state at this chain height" flag) and pflag panics on the
duplicate.

Fix: rename the proto field height → at_height. The new name is
also more semantically accurate (this is the snapshot height we're
looking up, not the query's read-state height) and matches naming
patterns elsewhere in the SDK.

- proto/juno/votingsnapshot/v1/query.proto: height → at_height in
  QueryVotingPowerAtRequest and QueryTotalVotingPowerAtRequest
  + matching path-binding updates in google.api.http annotations
- regenerated query.pb.go / query.pb.gw.go via proto-gogo
- x/voting-snapshot/keeper/grpc_query.go: req.Height → req.AtHeight

The wasmbinding JSON contract (wasmbindings/types/query.go) keeps
its `height` field — it's not run through autocli, separate
serialization path, no collision.

Verified locally: junod boots cleanly. The CI matrix now has a real
shot at running the chain instead of panicking at startup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants