Release candidate on top of v0.9.0, introducing the Ithaca hardfork.
Full diff: v0.9.0...v0.10.0-beta-candidate
Highlights
This is a hardfork release. The headline work is the new Ithaca hardfork (autonomous recovery from a producer stall under a pending milestone), gated at a per-network Heimdall block height; until the height is reached, runtime behavior is unchanged. The rest is a mempool DoS guard, a transaction-fee nil-guard, operator-facing per-module logging, and dependency/CI housekeeping.
Ithaca activation heights
- Amoy at block
40,776,000(2026-07-20 ~14:00 UTC) - Mainnet at block
50,185,000(2026-07-29 ~14:00 UTC)
Heimdall block heights (not Bor). All validators must upgrade before their network's height.
Ithaca hardfork — span rotation on bor pending-head stall
Post-Rio, a pending milestone (1/3 <= PM < 2/3) unconditionally suppressed span rotation. If the block producer stalled while a milestone sat in that band, the chain wedged with no autonomous recovery (the ~15-min stress-test halt). Ithaca adds a hardfork-gated rule: even under a pending milestoneen the >1/3-agreed bor head stops advancing for a stall threshold, starting the new span atN+1 (no reorg of pending blocks). The >1/3 agreement is the adversarial guard against a fabricated head.
Key pieces:
helper— Ithaca fork gate (per-network activation heights) + configurable bor-stall thresher.x/milestone— carries the validator's actual latest bor head in the milestone vote extension (latest_block_number/latest_block_hash, emitted post-fork only); tallies a >1/3 agreed head selected by greatest summed voting power (GetMajorityActualHead), so a byzantine minority reporting a fabricated the honest tip. Deterministic canonical-parent selection inGetMajorityMilestoneProposition.x/bor— honors an explicit excluded-producer set in both active and fallback selection; applies exclusions before the empty check so an exclusion that empties the active set re-triggers the fallback instead of wedging.app— gatedPreBlockerdispatch that rotates from the agreed actual head +1, bounds the agreed head to the span that owns the stall region, debounces both rotation clocks, and guards span-exhaustion / overflow (MaxUint64) boundaries.
Extensive adversarial hardening and regression coverage across the milestone tally, VE valid and boundary cases. The new vote-extension fields and pending-headtracking keys are only written/emitted post-fork, so there's no migration of existing state.
VeBLOP producer fallback when candidate set empties (#618)
SelectNextSpanProducer could hand an empty candidate slice to SelectProducer when the elected producer set collapsed to the current producer and it was filtered out of the active set; in the future-span path that error propagated out of PreBlocker. Adds a deterministic, Ithaca-gated fallback that draws ave/supporting set, then the validator set (sorted, excluding current and excluded producers), so future-span creation always has a candidate. Scoped to the future-span path only (checkAndAddFutureSpan) via an explicit opt-in flag; non-fatal rotation paths keep their skip-and-retry behavior.
CheckTx mempool DoS guard — maxMsgsPerTx (#621)
Caps a single tx at 16 messages in CheckTx. An oversized multi-message tx would otherwise trigger the per-message VeBLOP validation loop and full ante processing on every gossip hop. Enforced in CheckTx only — block validity and consensus are unaffected.
Transaction fee nil-guard (cosmos-sdk fork v0.2.12-polygon, #620)
The tx wrapper's fee accessors (GetGas, GetFee, FeePayer, FeeGranter) now return zero/nil when a decoded tx carries no AuthInfo.Fee instead of dereferencing it. A fee-less tx can be decoded during mempool admission and gossiped between nodes; without the guard the first fee-reading ante decorator dereferences the nil fee and panics on the processing node. The guard makes such a tx fail validation cleanly. Node-local hardening — no consensus change, no fee-behavior change for well-formed txs. Ships via the cosmos-sdk fork bump v0.2.11-polygon to v0.2.12-polygon.
Operator-facing: granular per-module logging (#619)
log_level now accepts a per-module form in addition to a single global level:
- Single level applied to every module, e.g.
"info"or"debug". - Comma-separated
module:levelpairs with an optional*:leveldefault, e.g."*:info,mempool:debug"— scope debug to one subsystem without the volume of a global debug level..
Levels: trace, debug, info, warn, error, fatal, panic, disabled. The module key is the module field of a log line (CometBFT: consensus, p2p, mempool, blocksync, statesync, …; Heimdall: server, x/bor, x/checkpoint, x/milestone, bridge/processor, …). Documented in the packaged mainnet/amoy config.toml.
Dependencies & CI
go.mod— cosmos-sdk fork v0.2.11-polygon → v0.2.12-polygon (the fee nil-guard above) (#620).- chore — remove SonarQube/SonarCloud integration and sonar-project.properties (#622).
- CI — pin kurtosis-pos to v1.3.4 for e2e (#613).
Rollout notes
- Consensus-affecting hardfork. Ithaca activates at the per-network Heimdall heights above, wired in helper/config.go (Amoy 40,776,000 ≈ 2026-07-20 14:00 UTC; mainnet 50,185,000 ≈ 2026-07-29 14:00 UTC). All validators must upgrade before their network's height; the fork is inert until then. Heightst cadence and may be nudged before the tag if cadence drifts (mainnet is furthest out).
- No store migration / ConsensusVersion bump in this release (Ithaca adds only post-fork vote-extension fields and tracking keys, no migration of existing state).
- maxMsgsPerTx, the fee nil-guard, and per-module logging take effect immediately on upgrade (require no coordination).
Merged PRs
- #611 — app, bor, milestone, helper: rotate span when bor pending head stalls
- #618 — bor: fall back to eligible producers when veblop candidate set is empty
- #621 — app: add maxMsgsPerTx
- #619 — cmd, helper, x/bor: granular per-module logs
- #620 — go.mod: bump cosmos dep
- #622 — chore: remove SonarQube/SonarCloud integration
- #613 — fix: pin kurtosis-pos to v1.3.4