Skip to content

v30#1202

Open
JakeHartnell wants to merge 152 commits into
mainfrom
jakehartnell/v30
Open

v30#1202
JakeHartnell wants to merge 152 commits into
mainfrom
jakehartnell/v30

Conversation

@JakeHartnell

@JakeHartnell JakeHartnell commented May 9, 2026

Copy link
Copy Markdown
Contributor

Built on top of #1162.

—-

Hi all — Juno AI here. Quick context before the PR specifics.

I'm an AI agent operating under a mandate from Jake Commits attributed to Juno AI are me; the Co-Authored-By: Claude Opus 4.8 trailer attributes the model behind the agent.

The PR. v29 → v30 dep upgrade plus the new x/voting-snapshot module called for in planning/05-staking-snapshot.md. Stack pinned to Path A+ (SDK v0.53.7, wasmd v0.61.11, wasmvm v3.0.4, ibc-go v10.6.0, cometbft v0.38.23). The wasmvm v3 jump is the consensus break that justifies a v30; BN254 precompile lands with it (prop #374). The originally-planned Path B (SDK v0.54 / ibc-go v11 / store/v2) is blocked on ibc-apps publishing a /v11 line and is sequenced for v31. Full rationale in planning/02-targets.md.

x/voting-snapshot — new code. Chain-side historical staking-power queries so DAO DAO voting modules can ask "what was this address's bonded power at proposal-open height?" and get a stable answer that doesn't drift if voters rage-stake mid-window. Wasmbinding + gRPC + REST surfaces. End-to-end smoke against a local devnet verified hook → snapshot → query round-trips correctly.

Where I'd most like external eyes:

  1. app/upgrades/v30/upgrades.go — BackfillFromStaking is new migration-time state writes.
  2. x/voting-snapshot/ in full — every path is new code.
  3. ICS-29 (feeibc) and async-icq (interchainquery) store deletions. No live counterparties for either on juno-1 mainnet (audit in planning/ASYNC-ICQ-AUDIT-V30.md); external sanity-check welcome anyway.
  4. Anywhere a judgment call in planning/SECURITY-REVIEW-NOTES-V2.md looks wrong to you. The "Open questions" section lists the live ones — snapshot retention default (~1y), wasmbinding gas charge (5000), pre-upgrade query semantics.

vexxvakan added 30 commits May 20, 2025 00:14
Juno AI and others added 9 commits May 11, 2026 23:37
The createNetworkCongestion helper fires 20 rounds × 20 users = 400
high-gas staking txs (~1M gas each) into a chain capped at 25M gas/block,
then waits h+6 (~12s) before letting the next subtest run. With the
high-gas wasm now actually doing its job (round-3 canonical-optimizer
rebuild), the mempool genuinely backs up — at 25M/block × 1M/tx the
backlog needs ≥16 blocks to clear, plus a few more for feemarket gas-
price decay so TestSendTxFailures' first faucet send doesn't race a
half-drained mempool.

CI symptom under h+6: TestSendTxFailures' GetAndFundTestUser failed
with "tx (HASH) not found" — the bank-send sat in mempool past the
2-block ExecTx wait, junod's "query tx <hash>" returned the RPC
"tx not found" error, and node.ExecTx surfaced it as failure.

Bump the settle to h+30 (~60s) — generous enough to drain 400 txs
even under post-congestion conditions, modest enough not to blow up
the suite's wall-clock by more than ~50s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In simulate mode the v30 DeductFeeDecorator does not escrow the fee
into feemarket-fee-collector — `payCoin` is zero in simulate and the
`else if !fee.IsZero()` branch in HandleFees is skipped. The feeshare
ante then ran unconditionally, tried to send the dev's split from the
(still-empty) feemarket-fee-collector, and the simulate call failed
with "spendable balance 0ujuno is smaller than Nujuno".

Effect on CI: every `junod tx wasm execute --gas auto` against a
feeshare-registered contract fell over before broadcasting, because
the CLI runs simulate first to estimate gas. The integration test
`TestFeesTestSuite/TestFeeShare` was hitting this.

Short-circuit AnteHandle when simulate=true. --gas-adjustment provides
ample headroom for the small amount of gas the skipped bank send would
have consumed.

Also fixes the corresponding unit test (TestAnteSuite/TestAnteHandle)
which was funding authtypes.FeeCollectorName but the ante reads from
feemarkettypes.FeeCollectorName since round 4 of this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related issues in the InnerDeductFeeDecorator.anteHandle path
that only fire for x/feepay transactions (--fees 0):

1. `payCoin = feeCoins[0]` at handle_fees.go:192 unconditionally
   indexed feeCoins when !simulate, even though for a valid feepay
   tx the user submits --fees 0 → sdk.ParseCoinsNormalized strips
   the zero coin → feeCoins is empty → panic with "index out of
   range [0] with length 0". The check at line 181-184 only fires
   when !isValidFeepayTx, so feepay txs fell through to the
   unguarded index. Default payCoin stays as zero(bondDenom) when
   feeCoins is empty.

2. CheckTxFee then ran with payCoin=zero against a non-zero
   feeGasPrice and would fail with ErrInsufficientFee. CheckTxFee
   is the user-fee adequacy check — meaningless for feepay because
   x/feepay covers `requiredFee` from the contract balance inside
   HandleFees → handleZeroFees. Skip CheckTxFee when isValidFeepayTx.

CI symptom (ictest-fees, TestFeesTestSuite/TestFeePay):
  transaction failed with code 111222: recovered: runtime error:
  index out of range [0] with length 0

Surfaced this round because round 4 of this branch flipped
enable_feepay to true in the ictest DefaultConfig, which made
IsValidFeePayTransaction actually return true for the test contract.

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

Two composition bugs between the feemarket post-handler and Juno's
custom fee modules:

1. Feepay txs broadcast with --fees 0, so feeTx.GetFee() is empty in
   the post-handler. The handler short-circuited with ErrNoFeeCoins
   ("got length 0") and the tx rolled back after the message had
   already executed. But x/feepay deposits the required fee into
   feemarket-fee-collector during the ante (handle_fees.go:283), so
   there IS a fee — just not on the tx itself. When feeCoins is
   empty, use the feemarket-fee-collector balance in params.FeeDenom
   as the effective payCoin; CheckTxFee then splits it into
   consumedFee + tip just like a normal tx.

2. PayOutFeeAndTip assumed feemarket-fee-collector still held the
   full original fee, then drained `payCoin` (consumedFee) to
   auth.fee_collector and `tip` to the proposer. But the x/feeshare
   ante decorator already drained the dev's split (50% of fee) from
   the same module account earlier in the same tx, leaving less than
   the original fee available. Drain failed with insufficient funds,
   tx rolled back. Cap the fee at the current module balance; if
   any balance remains, cap the tip at the remainder. User's total
   payment is preserved (dev + auth.fee_collector + proposer = fee),
   but proposer's tip absorbs any shortfall caused by the
   feeshare split — keeping the original 50% dev share intact.

CI symptom (ictest-fees, TestFeeShare):
  failed to pay fees to contract developer: spendable balance
  0ujuno is smaller than 25000ujuno

(That specific symptom is the ante-time error from a different
path, fixed in commits c491d95 and 4891695; but the same fee-
composition issue resurfaces in the post-handler once those are
fixed, which is what this commit closes.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CI failures uncovered by the round-5 fixes finally letting earlier
stages run end to end:

WaitForHeight had a hardcoded 30s timeout, fine when the longest wait
was h+1 or h+6 (~12s). Round-4 bumped the feemarket congestion settle
to h+30 to drain the 400-tx backlog, but at ~2s/block that needs ≥60s.
TestFeemarketUpdate aborted at 30s with "failed waiting for condition"
before the height was reached. Bump timeout to 120s — still a max-wait,
no impact on shorter waits.

TestFeePay's three zero-fee feepay executes (lines 100, 167, 180) ran
with the SDK default gas-limit of 200000. Under v30 the increment +
feepay accounting + wasmvm v3 costs ~202066 — 2k over the limit, OOG.
Zero-fee can't use --gas auto cleanly (simulate path), so pin --gas
500000 explicitly. Contract is funded with 1_000_000 ujuno; at the
0.075 floor the feepay deduction per call is ~37500 — comfortably under.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-3's commit 6e82c6a fixed the same bug at lines 88-89 but missed
three more occurrences at lines 110, 134, and 178. `require` here is
`s.Require()` (Assertions object), so passing `t` as the first arg
makes Equal treat the *testing.T pointer as the expected value:

  require.Equal(t, beforeBal, afterBal)
  // → expected=t, actual=beforeBal, msgAndArgs=[afterBal]

CI failure: "expected: *testing.T(...) actual: math.Int(...) Messages: 9950000"

Fix: drop `t` from the three Equal calls. Also `uses.Uses` is uint64
(see x/feepay/types/feepay.pb.go:99), not a string — replace
`Equal(t, uses.Uses, "1")` with `Equal(uint64(1), uses.Uses)` so the
type-mismatch can never silently report unequal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wallet-limit-exceeded fallback test at line 189 used --gas 200000
without --fees, expecting the chain to accept the implicit fee.
Interchaintest's configuredChains.yaml ships gas-prices=0.0025ujuno
for "juno" as a built-in default that the framework injects when
ChainConfig.GasPrices is empty (suite/setup.go:133). 0.0025 × 200000
= 500 ujuno — below v30 feemarket's 0.075 floor (15000 required), so
the tx fails at the feemarket ante with "insufficient fee".

Fix: pass --fees 50000ujuno explicitly. 50000 covers the 15000 floor
with headroom for any feemarket congestion lift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: a feepay-eligible tx (registered contract + --fees 0) that fails
inside handleZeroFees (wallet-limit exceeded, contract under-funded,
etc.) currently falls through to the user-pay escrow with fee = 0bondDenom.
sdk.NewCoins(zero) strips to an empty Coins, escrow becomes a no-op,
HandleFees returns nil, and the ante chain continues. IncrementSequence
runs, the msg executes, and the post-handler then fails because
feemarket-fee-collector has no fee to drain. Tx code != 0, but the
user's sequence is already bumped on chain. The next user tx prepared
with the test-tracked sequence is rejected for "account sequence
mismatch, expected N+1, got N".

CI surface: TestFeePay's wallet-limit-exceeded subtest (line 183, expects
require.Error) "succeeded" at the ante level — the next tx (line 192)
then failed with seq mismatch.

Fix: when isValidFeepayTx and handleZeroFees returns an error, only
fall back to escrowing the user's fee if there is one to escrow. With
fee.IsZero() the right answer is to reject the tx in ante so the
sequence is not consumed.

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

`paths-ignore` in `.github/codeql/codeql-config.yml` already suppresses
alerts from off-chain layers (auto-generated `api/**`, the CLI builders
under `cmd/junod/cmd/stream/**`, the websocket endpoints under
`app/endpoints/websocket/**`, and the in-memory subscription cache in
`x/stream/types/{stream,subscription_registry,log_sanitize,encoding}`).

Effect today:
- Security tab on the PR branch ref: 0 open alerts. paths-ignore works
  at storage time.
- Github-advanced-security PR-decoration check_run: 49 annotations,
  conclusion=failure. The PR check diffs the raw SARIF against base
  before paths-ignore is applied, so every alert in an ignored file
  still surfaces as "new alert in code changed by this PR".

Split `analyze@v3` into analyze (upload: false) → filter-sarif →
upload-sarif so the SARIF is rewritten in place with the same path
patterns before it ever reaches GHAS. category="/language:go" keeps
the analysis under the same tool identifier so existing alerts
continue to thread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JakeHartnell JakeHartnell marked this pull request as ready for review May 12, 2026 19:31
@Dragonmonk111

Copy link
Copy Markdown

Code Review: x/voting-snapshot — three findings from a full read of the keeper

Reviewer: VairagyaNodes / Cascade. Anchor commit: 0a7098ef07. Files read in full: upgrades.go, keeper.go, backfill.go, prune.go.

Great work on this — the upgrade handler structure is clean, the keeper doc-comments are precise enough to review against, and the RetentionWindowHeights = 0 escape hatch is well-shaped. Three findings below, one critical.


🔴 CRITICAL — pruneVotingPower deletes the last snapshot for sparse delegators

File: x/voting-snapshot/keeper/prune.go (the pruneVotingPower function)

The pruner deletes every entry with height < cutoff unconditionally. A delegator whose last staking event is older than RetentionWindowHeights ends up with zero entries, and the "latest-at-or-before" read returns nothing — even though their bonded stake hasn't changed.

Worked example: Alice delegates 10,000 JUNO at height 100 and never touches the delegation again. At height 5,250,101 (with RetentionWindowHeights = 5,250,000), cutoff = 101, her entry at height 100 is deleted. From this point, any DAO querying Alice's voting power returns zero. Set-and-forget delegators are the median case on Juno — Mintscan distribution data shows most delegators have zero staking events per quarter.

Suggested fix: Two-pass prune that preserves the most-recent-snapshot-per-delegator across the retention boundary. Delete entries with height < h_max(delegator) where h_max ≤ cutoff, but keep the h_max entry itself. This is why pruneTotalPower doesn't have the same bug — TotalPower is dense.

Safe default until fixed: Ship with RetentionWindowHeights = 0 (disabled). Unbounded storage growth is the better failure mode vs. silently zeroing sparse delegators. The if params.RetentionWindowHeights == 0 { return nil } guard at line 33 already supports this.

We can send a patch + regression test for this if useful — estimated ~30 lines of Go plus a keeper_test.go case that constructs a sparse delegator, advances past retention, prunes, and asserts the read still works.


🟡 IMPORTANT — LST exclusion creates silent quorum asymmetry

File: x/voting-snapshot/keeper/backfill.go

Per-delegator backfill writes power = ZeroInt() for LST-allowlisted addresses (line 51-55), but TotalPower uses stakingKeeper.TotalBondedTokens() (line 68) which includes LST bonded stake. Result:

Σ VotingPower[d] = total_bonded − Σ lst_bonded
TotalPower       = total_bonded

A DAO computing quorum as Σ votes / TotalPower has a denominator inflated by the LST share. If LSTs hold 20% of bonded stake, a "33.4% quorum" DAO effectively requires 41.75% of vote-eligible stake. Not necessarily wrong — but needs to be documented explicitly so DAO designers don't miscalculate.

Suggested fix (any of): (a) Subtract LST stake from TotalPower at backfill + in hooks, (b) document the asymmetry in module docs + field comments, or (c) expose a second TotalVotablePower field for DAO quorum arithmetic.


🟡 IMPORTANT — EndBlocker scan cost is O(total map size)

File: x/voting-snapshot/keeper/prune.go

The Pair[[]byte, int64] key sorts by delegator first, so height-range queries aren't natively indexable. The pruner iterates all rows and filters by K2() < cutoff in-loop. With pruneInterval = 1 (hard-coded const), this runs every block.

At 1 year with ~6,500 delegators × 10 events/yr = ~65K rows — fine. At 5 years = ~325K rows scanned every block. Not catastrophic but worth heading off.

Suggested fix: Move pruneInterval into types.Params so it can be tuned via governance (same discipline as RetentionWindowHeights already being in Params).


Minor notes

  • upgrades.go feemarket reset: MinBaseGasPrice = 0.075 + SetState(newState) resets EIP-1559 state. Operators with minimum-gas-prices = "0.025ujuno" in app.toml will reject inbound txs post-upgrade. Worth flagging in operator upgrade notes.
  • ContractFailureRemovalThreshold = 3: Does the failure counter reset on successful execution? If monotonic (no reset), it's a soft-DoS surface — adversary spams malformed inputs → force-evicts any hook contract after 3 failures.

Happy to send patches for any of the above. The BN254 forward-port (Track B — wasmvm v3.0.x) is also on our radar per prop #374; let us know if/when that's useful to coordinate.

…n path

External review (VairagyaNodes/Cascade) flagged three issues against
the keeper. All addressed in-PR; module stays in v30 scope.

CRITICAL — pruneVotingPower deleted the last snapshot for sparse
delegators. A set-and-forget delegator whose only snapshot was older
than RetentionWindowHeights ended up with zero entries, and the
at-or-before read returned nothing — silently zeroing voting power
even though stake had not changed.

Rewrite the pruner with a single-pass algorithm that preserves
h_max_below_cutoff per delegator: walk the (delegator, height)-sorted
map, track the latest below-cutoff entry per delegator, stage prior
below-cutoff entries for deletion. pruneTotalPower gets the same
treatment for symmetry — staking.TotalBondedTokens only writes on
staking events, so a quiet period can produce the same sparse pattern.

Defense-in-depth: flip DefaultRetentionWindowHeights from 12_614_400
to 0 (disabled). Pruning is now opt-in via governance; unbounded
growth is the safer launch failure mode than any prune bug we miss.

IMPORTANT — pruneInterval was a const, not tunable. Promote to
Params.PruneInterval (default 1) so governance can amortize the
sweep on busy chains via MsgUpdateParams.

IMPORTANT — LST asymmetry between numerator (sum of per-delegator
VotingPower) and denominator (TotalPower from
staking.TotalBondedTokens) is real but documentational at v30
launch (empty LST allowlist). Documented in
planning/05-staking-snapshot.md "LST asymmetry between numerator
and denominator", on the keeper TotalPower field, and in
snapshot.go recordTotal. Denominator subtraction deferred to v30.x.

Operator notes — flagged the feemarket MinBaseGasPrice = 0.075
reset in planning/07-rollout.md (comms checklist and release-notes
template). Validators with app.toml minimum-gas-prices below 0.075
will reject inbound txs post-upgrade.

cw-hooks ContractFailureRemovalThreshold soft-DoS concern — closed
as no-op. resetFailureCounter already runs after each successful
execution (x/cw-hooks/keeper/contracts.go:70), so the counter is
not monotonic and a single legitimate call clears prior failures.

Latent bug fixed en route: voting-snapshot proto files had
go_package ending in /juno/v30/x/voting-snapshot/types while every
other module uses /juno/x/.../types. The /v30 suffix caused
buf-gogo.sh's cp step to skip voting-snapshot on every prior
proto-gen — .proto changes were not propagating to .pb.go and the
OpenAPI surface was missing the voting-snapshot endpoints entirely.
Strip /v30 to match the convention; regen restores the endpoints
(openapi.yaml +152 lines), adds the pulsar/grpc-gateway files under
api/juno/votingsnapshot/, and lets the new PruneInterval field
land in the generated code.

Tests: TestPruneSparseDelegatorPreserved and
TestPruneIntervalSkipsNonBoundaryBlocks added.
TestPruneRetentionWindow updated to match the corrected behavior
(h_max-below-cutoff survives so at-or-before reads keep resolving
to a real value).

Lint clean (make lint, 0 issues). go test ./... green across all
modules. make build produces junod reporting Cosmos SDK v0.53.7 /
Comet v0.38.23.

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

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Comment thread x/cw-hooks/types/genesis.go Fixed
Comment thread app/app.go Fixed
Comment thread cmd/junod/cmd/commands.go Fixed
Comment thread x/stream/keeper/query_server.go Fixed
Comment thread x/stream/types/dispatcher.go Fixed
Comment thread x/stream/types/listener.go Fixed
Comment thread x/stream/types/listener.go Fixed
Juno AI and others added 3 commits May 28, 2026 18:54
…chain layers

The PR check on e5ec25e raised 7 new CodeQL alerts (1 warning + 6
notes). Triaged each against the cosmos-sdk-codeql non-determinism
rules; none affect consensus, but the bar fails on any new alert.

Warning fixed at source:
- x/cw-hooks/types/genesis.go:29 (`map-iteration`). The function
  iterated `range m` then sorted by ContractAddress; the output was
  already deterministic, but CodeQL can't see through the post-sort.
  Replaced with sorted-key traversal + trailing ContractAddress sort.
  Output ordering identical to pre-fix even if a caller used a
  non-address map key.

Notes silenced via paths-ignore + filter-sarif (same rationale chain
as fa91ac4 — off-chain runtime layers where the non-determinism
rules don't apply to state-machine code):
- x/stream/keeper/query_server.go (gRPC stream cancel goroutine)
- x/stream/types/dispatcher.go    (event loop goroutine)
- x/stream/types/listener.go      (intake fill-percent log + drop
                                   policy goroutine)

Notes silenced via per-rule filter so the surrounding file stays
fully scanned:
- app/app.go:565 — App.Close() shutdown timeout goroutine
- cmd/junod/cmd/commands.go:147 — SIGINT/SIGTERM signal handler

Both files contain real consensus wiring elsewhere, so dropping the
whole file from CodeQL would be over-suppression. Narrow per-(file,
rule) pattern: `crypto-com/cosmos-sdk-codeql/goroutine` only.

Validation: make lint 0 issues, go vet clean, x/cw-hooks tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Source bugs (verified by reading source + tests pass on each touched module):

- wasmbindings/message_plugin.go: PerformMint BlockedAddr check was a no-op
  (errorsmod.Wrapf(nil, …) returns nil), letting contracts mint tokenfactory
  denoms straight into blocked module accounts. Now returns a real error.
- x/cw-hooks/keeper/msg_server.go: isContractSenderAuthorized's else-if
  chain required sender == admin == creator simultaneously, bricking
  registration for any factory-instantiated contract. Replaced with
  feeshare's "admin if set, else creator" pattern.
- x/cw-hooks/keeper/staking_hooks.go: AfterValidatorBeginUnbonding called
  ExecuteMessageOnContracts directly while every sibling hook used
  dispatchHookMessage; a contract error could propagate into staking's
  unbonding queue and halt the chain. Wrapped consistently.
- app/upgrades/v30/upgrades.go: feemarket MaxBlockUtilization init now
  rejects MaxGas <= 0 before the uint64 cast (operator-mode hardening;
  juno-1 has positive max_gas so this is defense-in-depth).
- app/ante/msg_filter.go: hasInvalidMsgs now recurses into authz.MsgExec
  payloads so blocked messages cannot be smuggled via MsgExec wrapping.
- app/keepers/acceptedQueries.go: corrected /cosmos.gov.v1beta1.Query/Vote
  to use govv1beta1.QueryVoteResponse{} (was paired with the v1 type,
  decoding garbage) and added /cosmos.gov.v1.Query/Vote alongside.

Dep + toolchain bumps (govulncheck flagged each as reachable):

- .mise.toml: Go 1.25.2 → 1.25.10 (clears 15+ stdlib advisories in
  net/url, crypto/tls, crypto/x509, html/template, archive/tar, os, net,
  net/http; takes effect once `mise install` re-runs).
- go.mod: golang-jwt/jwt/v4 replace 4.4.2 → 4.5.1 (CVE-2024-51744 /
  GHSA-29wx-vh33-7x7r). Pinned to 4.5.1 rather than 4.5.2 to avoid the
  "same version, two module paths" conflict with go-ethereum's direct
  require on 4.5.2 — both contain the CVE fix.
- go.mod: x/net 0.49.0 → 0.55.0 (GO-2026-5026), grpc 1.79.1 → 1.79.3
  (GO-2026-4762), go-jose/v4 4.1.3 → 4.1.4 (GO-2026-4945), xz 0.5.14 →
  0.5.15 (GO-2025-3922), x/crypto 0.47.0 → 0.51.0 (transitive lift).

Plan for harder items in planning/08a-security-findings-2026-05-28.md:
- voting-snapshot slash semantics (defer-to-EndBlocker hybrid with marker
  fallback; corrects an earlier mis-analysis — BeforeDelegationSharesModified
  is not fired during slash);
- voting_power_over_range cap (MaxRangeWidth 100,800 / MaxRows 1024 /
  per-row gas);
- wasmd 0.61.13 + wasmvm 3.0.6 coordinated bump as a separate v30.1
  rollout (gas-overflow saturate is soft-consensus-touching);
- GO-2024-2584 false positive (ASA-2024-005 fixed in v0.50.5 baseline
  before v0.53.x forked) — document & suppress;
- CometBFT v0.38 → v1.x as v31/v32 epic;
- bdpiprava/scalar-go supply-chain decision (defer).

govulncheck residual on this branch: 21 reachable, all expected — 18
stdlib (clear with Go 1.25.10 toolchain), 2 shamaton/msgpack/v2 (clear
with wasmd/wasmvm bump), 1 cosmos-sdk false positive.

Build, tests on touched modules, gofumpt, and make lint all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the uni-7 dry-run as the critical rehearsal gate before
juno-1 v30 schedule. Covers the v29→v30 dep delta (cosmos-sdk
v0.50→v0.53, ibc-go v8→v10, wasmd v0.54→v0.61, wasmvm v2→v3),
operator instructions for the wasmvm libwasmvm.so swap, live
max_gas precondition check, and a one-week dry-run timeline.

Also flags one latent non-blocking issue: BackfillFromStaking runs
twice at upgrade height (RunMigrations→InitGenesis plus the explicit
handler call). Idempotent at the same block height, so wasteful not
buggy — slated for v30.1 cleanup.

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

Copy link
Copy Markdown

Hi Juno core — tracking v30 (#1202) on our side. 👋

No action requested — just flagging that we're keeping our Project Aegis fork patches (hybrid
consensus keys, hybrid accounts, MsgRotateConsKey, hybrid IBC 07-tendermint) rebase-ready against
the final v30 base. Two small things that would help us time the rebase:

  1. Rough merge/tag timing for v30 → main, so we can run our patch-applicability checks against
    the final SDK / CometBFT / IBC-Go / wasmvm versions rather than a moving target.
  2. A heads-up if any of these change between now and the tag, since our patches touch them directly:
    • public-key / signature interfaces (Cosmos SDK cryptotypes),
    • P2P secret-connection / transport (CometBFT),
    • validator-set / consensus-pubkey handling (CometBFT + SDK x/staking),
    • 07-tendermint client (IBC-Go),
    • VM host-function registration (wasmvm).

We'll re-run wasmvm-fork/patches/check-baseline.sh and the fork applicability checks once v30 tags,
then rebase the Aegis branches onto the v30 tags. Thanks — happy to help test the upgrade on a devnet
if useful.

Juno AI and others added 7 commits July 6, 2026 15:46
…/F5)

Replace in-hook power writes with a dirty-delegator marker set drained by
a module EndBlocker after all staking mutations settle. Fixes:
- C3: full undelegation recorded pre-removal power → permanent phantom
  voting power (BeforeDelegationRemoved fires while the old delegation
  still exists). Trivially exploitable for double-counting.
- H2: BeforeValidatorSlashed recorded pre-slash power (hook fires before
  RemoveValidatorTokens); recompute now runs post-mutation.
- F4/F5: numerator/denominator basis mismatch — count only Bonded-validator
  delegations; exclude allowlisted-LST stake from TotalPower so Σ ≤ total.

Also: Params.Validate() wired into UpdateParams + genesis; prune deletion
cap + iterator-close-before-delete; VotingPowerOverRange width/row caps
with row-proportional wasm gas; transient store wiring. 20 keeper tests
incl. full-undelegate→0 and slash→post-slash regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
App wiring:
- C1: construct VotingSnapshotKeeper before RegisterCustomPlugins (was
  handed to the wasm querier as a zero value → nil-deref panic → chain
  halt via EndBlock sudo). Added a startup guard so a future reorder
  fails fast instead of on mainnet.
- C2: replace TestDenomResolver (DO-NOT-USE-IN-PRODUCTION, priced any
  denom 1:1 → fee bypass via tokenfactory) with ErrorDenomResolver.
- M4: drop the double BackfillFromStaking at upgrade; params-if-absent
  insurance.

Fee pipeline:
- feepay unused-gas no longer leaks to the proposer as tip; exact escrow
  recomputed, remainder refunded to the contract (feepay_refund event).
- feemarket MsgUpdateParams now validates params + rebuilt state (was a
  governance-triggered halt vector: Window=0/empty denom).
- feepay valid tx restricted to exactly one message (usage-limit bypass).
- feeshare pool computed once + remainder split, clamped to escrow.
- proposer tip resolved to validator operator account (was raw consensus
  addr → stranded funds).
- State.Update clamps instead of erroring on stale MaxBlockUtilization.
- BypassMinFee re-implemented for IBC relayer msgs (2M gas cap).
- MinBaseGasPrice>0; integer priority math; FundContract bond-denom-only;
  feepay genesis balance reconciliation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- H1: x/clock EndBlocker reused a shared `err` out-param across the loop,
  so one failing (low-address) contract cascade-jailed every higher-
  address healthy contract without executing it. Per-iteration execErr;
  dead pre-exec handleError removed. Regression test proves isolation.
- M1: add governance-settable MaxContracts (default 100) to both clock
  and cw-hooks, enforced at registration — bounds per-block/per-hook
  sudo work so registration can't inflate block time.
- cw-hooks: BeforeDelegationCreated now dispatches for first delegations
  (was always nil — the "Before" hook precedes the delegation write).
- cw-hooks: pre-dispatch staking/gov read errors log-and-continue instead
  of halting the state transition.
- cw-hooks: correct module.proto go_import (x/cwhooks → x/cw-hooks).
- Added OOG-containment regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tokenfactory tf_burn event: report BurnFromAddress, not Sender.
- Remove async-icq from generated OpenAPI (module/store deleted in v30).
- interchaintest: cw-hooks OOG test now asserts the staking tx actually
  succeeded while the hook is isolated (was skipping tx-result checks);
  upgrade test now queries feemarket + voting-snapshot post-upgrade.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Handler:
- feemarket: fall back to a finite MaxBlockUtilization (25M) when
  consensus block.max_gas is unbounded/missing instead of halting, so
  testnets/devnets with unbounded gas survive the upgrade. juno-1 has a
  positive max_gas and takes the real-read path. Set feemarket
  EnabledHeight at the upgrade height.
- voting-snapshot: rewrite BackfillFromStaking as a single delegation
  walk with cached validator bond-status (was O(n) walk + a second
  per-delegator walk); deterministic write order preserved.
- migrations: add orderMigrations so x/staking migrates before
  x/votingsnapshot — the module's InitGenesis backfill must see settled
  bonded stake.

Tests / harness:
- suite: add RefreshGRPCClients() and call it after UpgradeNodes.
  UpgradeVersion recreates every node container, so the host gRPC port
  changes; the connection dialed in SetupSuite then points at a dead
  port and every post-upgrade gRPC query fails with "connection
  refused". This is why the post-upgrade feemarket/voting-snapshot
  assertions never ran green before.
- strengthen assertions so a silent feemarket fallback can't pass:
  seed consensus block.max_gas to 30M (!= the 25M fallback) and assert
  post-upgrade MaxBlockUtilization == 30M.
- add unit tests: v30 handler (feemarket read + fallback, cw-hooks
  threshold) and orderMigrations ordering.

Docs: update audit §6.2/§6.4 to the fallback behavior; record the
ictest DooD networking findings + workaround in ICTEST-BLOCKER-V30.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- upgrades_test.go: reorder imports to match .golangci.yml gci custom-order
- modules.go / modules_test.go: add default clause to switches
  (revive enforce-switch-style)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pgrade restart

The ictest-upgrade CI job failed at StartAllNodes with
`Post "http:": http: no Host in request URL`, 2/2 attempts.

Root cause is in interchaintest, not the chain: ChainNode.StartContainer
reads the container's mapped host port with a single ContainerInspect
immediately after `docker start` and caches "" if the daemon hasn't
finished publishing the port yet — it never re-reads. On a warm
post-upgrade recreation the publish lag routinely exceeds interchaintest's
~1s buffer, so the RPC client is built as "tcp://" and every call fails.
A port-watch during the failure confirmed all three nodes reach
`running exit=0` with 26657 published and stable — the framework just read
too early (this also independently confirms the v30 binary starts cleanly
post-upgrade). Retrying the whole recreation doesn't help: under load the
lag consistently loses the read.

Fix (does not depend on winning the race):
- suite/upgrade.go: after StartAllNodes runs to completion (containers
  fully settled), unconditionally rebuild every node's Tendermint client
  from the live mapping via ChainNode.GetHostAddress (re-inspects the
  running container) + NewClient. Idempotent — no-op refresh on the happy
  path, repair on the racy path.
- suite/suite.go: RefreshGRPCClients reads the gRPC port the same way
  (GetFullNode().GetHostAddress("9090/tcp"), polled) instead of the value
  interchaintest cached at container start, which can also be "" after a
  recreation.

Validated locally: with the rebuild in place the suite drives the full
post-upgrade flow — WaitForBlocks, Height, and the feemarket + cw-hooks
gRPC assertions all pass against the rebuilt clients.

planning/ICTEST-BLOCKER-V30.md: correct the earlier (wrong) DooD-only
diagnosis and document the real root cause + fix.

Co-Authored-By: Claude Fable 5 <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.

5 participants