Skip to content

v0.9.0

Latest

Choose a tag to compare

@marcello33 marcello33 released this 17 Jun 17:19
1deada1

Heimdall v0.9.0 — Mainnet (Zurich hardfork)

Summary

Heimdall v0.9.0 is a mandatory release for all mainnet node operators. It activates the Zurich hardfork at Heimdall block 47,880,000 (estimated Thu June 25th, ~2PM UTC), and ships a large set of features, consensus hardening, and fixes.

All mainnet nodes must be running v0.9.0 before block 47,880,000. Nodes on older versions will fall out of consensus at the activation height.

Zurich hardfork (block 47,880,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 (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