github 0xPolygon/heimdall-v2 v0.9.0-beta

pre-release8 hours ago

Heimdall v0.9.0-beta — Amoy (Zurich hardfork)

Summary

Heimdall v0.9.0-beta is a mandatory release for all Amoy node operators. It activates the Zurich hardfork at Heimdall block 37,750,000 (estimated Wednesday, June 17th 2026, ~14:00 UTC), and ships a large set of features, consensus hardening, and fixes.

All Amoy nodes must be running v0.9.0-beta before block 37,750,000. Nodes on older versions will fall out of consensus at the activation height.

Zurich hardfork (block 37,750,000)

Consensus-affecting changes, all gated by the Zurich activation height:

  • Deterministic state-sync processing (x/clerk) — event visibility moves from wall-clock-based to block-height-based. Events recorded in block H become visible at H+1, with visibility heights assigned deterministically in PreBlocker. Out-of-order events at the upgrade boundary are handled explicitly, and any error on the visibility path now aborts block processing instead of being logged and skipped. New gRPC/REST queries expose record lists by height and by time.
  • Symmetric side-transaction capsPrepareProposal stops including side transactions beyond the per-block cap (50), and ProcessProposal deterministically rejects proposals that exceed it.
  • Wall-clock budgets for proposal and vote-extension constructionPrepareProposal operates under a 500 ms budget and ExtendVote under an 800 ms budget, returning partial results instead of overrunning CometBFT timeouts (milestone proposition is skipped when the budget is exhausted).
  • Commit-only checkpoint signatures — checkpoint signature aggregation includes only votes flagged as Commit.
  • Deterministic milestone proposition — the majority parent-hash evaluation now binds explicitly to the last end-block hash instead of iterating candidate parent hashes, guaranteeing identical results across validators.
  • Producer votes restricted to the active validator setMsgVoteProducers requires the voter to be in the active set, and voting-power resolution uses active-set power consistently on both sides of the majority threshold.
  • Bank-transfer output cap — a new ante decorator caps the aggregate number of bank-transfer outputs per transaction at 16 (MsgMultiSend counts len(outputs), MsgSend counts 1), keeping per-transaction work proportional under the flat-fee model (via cosmos-sdk fork v0.2.11-polygon).
  • MsgCheckpoint account root hash validation — an ante decorator rejects checkpoints whose AccountRootHash is not exactly 32 bytes.
  • Bor availability toleranceProcessProposal and VerifyVoteExtension treat "block not found" responses from Bor as transient (same handling as query failures), avoiding spurious proposal rejections while a local Bor is catching up.

Features

  • Full Heimdall↔Bor gRPC transport (opt-in) — every Bor-facing call now has a gRPC path, selectable per node via bor_grpc_flag in app.toml (with bor_grpc_url and bor_grpc_token). Includes a startup hash-parity check across both transports and a single-round-trip batch call for milestone proposition (≈4.4× faster than the HTTP path on devnet benchmarks). HTTP remains the default; no behavior change unless enabled. Requires Bor v2.8.3 or newer (the full Bor gRPC server, #2194) on the connected Bor node when enabled.
  • Bor endpoint failovereth_rpc_url and bor_grpc_url accept comma-separated endpoints with automatic failover, health probing, and metrics (bor_healthcheck_*). The primary endpoint is authoritative and reclaims after recovery. Failover is refused at startup on block-producing validators (safety guard).
  • EIP-1559 L1 transactions — bridge transactions to Ethereum use dynamic fees, configurable via main_chain_gas_fee_cap (default 500 gwei) and main_chain_gas_tip_cap (default 10 gwei) in app.toml.
  • Bridge self-heal extensions — new recovery methods reconstruct checkpoint and state-sync data from authoritative L1/Bor sources when local records are incomplete.
  • tx_index-free bridge + pruning defaults — the bridge checkpoint flow no longer depends on CometBFT's tx indexer, so it works on pruned nodes; default pruning settings updated accordingly.
  • Targeted producer replacementMsgSetProducerDowntime accepts an optional target_producer_id to designate a specific replacement producer during planned downtime (default remains round-robin).
  • Millisecond-precision log timestamps — Heimdall logs now match Bor's ISO-8601/ms format for cross-service correlation.

Fixes

  • Producer-downtime span off-by-one and a divide-by-zero panic on empty producer sets (x/bor).
  • Checkpoint transaction lookup off-by-one in the bridge after the tx_index removal.
  • Bridge memory-safety fixes, context-aware retry sleeps, and clean shutdown of bridge goroutines.
  • HeimdallListener no longer gets stuck on nodes restored from pruned snapshots.
  • Data race on the shared contracts-caller instance.
  • GetBorTxReceipt and milestone governance-parameter loading fixes.
  • Better gRPC client logging and a startup warning on Bor client misconfiguration; bor_rpc_timeout is clamped to 3 s to fit ABCI budgets.
  • Node home directory is no longer wiped when /tmp is not writable.
  • Packaging: postrm script cleanup, Dockerfile and docker-compose fixes, refreshed seeds and persistent peers.

Dependencies

  • 0xPolygon/cometbftv0.3.8-polygon
  • 0xPolygon/cosmos-sdkv0.2.11-polygon
  • 0xPolygon/polyprotov0.0.8
  • Bor dependency rebased onto a go-ethereum v1.17 base
  • golang.org/x/{crypto,net,sys} and quic-go bumped to clear all govulncheck advisories

Required change

In app.toml (default location ~/.heimdalld/config/app.toml, /var/lib/heimdall/config/app.toml on packaged installs), replace:

#### gas limits ####
main_chain_gas_limit = "5000000"

#### gas price ####
main_chain_max_gas_price = "400000000000"

with:

#### gas price configs (EIP-1559) ####
main_chain_gas_fee_cap = "500000000000"   # max fee per gas, wei (default 500 gwei)
main_chain_gas_tip_cap = "10000000000"    # max priority fee per gas, wei (default 10 gwei)

Apply this before restarting on the new version.

Configuration changes (app.toml)

Field Default Purpose
bor_grpc_flag false Enable gRPC transport to Bor
bor_grpc_url localhost:3131 Bor gRPC endpoint(s), comma-separated for failover
bor_grpc_token empty Bearer token for authenticated Bor gRPC
main_chain_gas_fee_cap 500000000000 Max fee per gas for L1 txs (wei)
main_chain_gas_tip_cap 10000000000 Max priority fee for L1 txs (wei)
eth_rpc_url unchanged Now accepts comma-separated endpoints (failover)

Enabling gRPC to Bor (optional)

gRPC is opt-in; HTTP JSON-RPC stays the default, so existing operators see no change. Enable it Bor first, then Heimdall — Bor stays HTTP-compatible so bringing the server up first is safe, and Heimdall's startup parity check needs Bor already serving gRPC. Requires Bor v2.8.3 or newer (the full gRPC server #2194, plus its [grpc] loopback-default prerequisite #2078) paired with this Heimdall release (the client side).

Step 1 — Bor (config.toml): opt into the gRPC server on an address Heimdall can reach.

[grpc]
addr  = "127.0.0.1:3131"   # same-host validator pair; loopback is the access control, no TLS needed
# addr = "0.0.0.0:3131"    # cross-host; must pair with TLS / a firewall
token = ""                  # bearer token; leave empty on loopback, set it for any non-loopback bind

For authenticated (non-loopback) deployments, prefer the env var over the flag/file (the flag leaks into ps/shell history):

export BOR_GRPC_TOKEN="$(openssl rand -hex 32)"

Bor's gRPC exposes only read-only public chain data, so on a same-host loopback bind a token isn't required; it matters cross-host, on shared multi-tenant hosts, and as defense-in-depth if the bind is later widened. (Equivalent flags: --grpc.addr, --grpc.token.)

Step 2 — Heimdall (app.toml): point the client at Bor and match the token.

bor_grpc_flag  = "true"
bor_grpc_url   = "http://127.0.0.1:3131"          # same-host
# bor_grpc_url = "https://bor.example.net:3131"   # cross-host (TLS)
bor_grpc_token = "<match Bor; empty if Bor's token is empty>"

Restart Heimdall; at startup it runs one HeaderByNumber over both transports and logs a warning/fatal if the hashes diverge (guards against a stale Bor that doesn't populate the full proto header). Put credentials in bor_grpc_token, never in the URL; bor_grpc_url also accepts a comma-separated list for failover.

Layouts.

  • Same host / same container / same pod — loopback (http://127.0.0.1:3131), no token. Simplest; matches the defaults.
  • Cross-host, or separate Docker/compose containers (different network namespaces, so not loopback) — Bor on 0.0.0.0:3131 + a token; Heimdall on https://<host-or-service>:3131 with the matching token. A remote endpoint needs an explicit http:///https:// scheme, and Heimdall refuses to send a token over plaintext to a non-loopback peer — so a cross-host token means TLS (terminate it in front of Bor's plaintext gRPC, or keep the link on a private network/firewall).

Downgrade: set bor_grpc_flag = "false" in Heimdall and restart — Bor doesn't need changing (it keeps serving both transports).

No store migration or resync is required; the upgrade is a binary swap + restart. Pre-activation behavior is unchanged, so mixed-version operation is safe only until the activation height.

Upgrade instructions

  1. Stop heimdalld.
  2. Install v0.9.0-beta (packages attached, or build from source).
  3. Review the new app.toml fields above (all optional; defaults preserve current behavior).
  4. Restart and confirm the node resumes signing/syncing.

Deadline: before block 37,750,000 (estimated June 17th 2026, ~14:00–15:00 UTC — track the live height as the date approaches).

What's Changed

New Contributors

Full Changelog: v0.8.2...v0.9.0-beta

Don't miss a new heimdall-v2 release

NewReleases is sending notifications on new releases.