Skip to content

Bradbury rejects Governor deployment above a sub-16M gas ceiling #419

Description

@Snehal707

Summary

A current ~46KB Intelligent Contract (the Governor) cannot be deployed to Bradbury through either the standard SDK automatic-gas path or the manual transaction path tested. Bradbury rejects the transaction before acceptance with gas limit too high.

This appears to be a distinct hard ceiling finding, separate from the gas-underestimation behavior reported in #402.

Reproduction

Environment:

  • Network: Bradbury, chain ID 4221
  • Source: contracts/governor.py
  • Source size: 46,535 bytes
  • SHA-256: 87c21436acbd4079a30ee3b7bf9ad5c12ef8c69b180750983dd2aba8ebfa6c02
  • Governor source includes enrollment, review, claim, halt, and mandate-promotion logic
  • Upgrade/deploy payload: approximately 41KB

Manual path:

  • A gas limit based on an estimated requirement of approximately 32,489,986 was rejected with gas limit too high.
  • An explicit gas limit of 16,777,216 was rejected with gas limit too high.
  • An explicit gas limit of 16,000,000 was rejected with gas limit too high.

Standard SDK path:

The current source was then submitted once through the normal genlayer-js deployment path using automatic gas handling, with no manual gas override. It used a separately funded raw-key wallet:

  • Wallet nonce before submission: latest=0, pending=0
  • RPC returned identifier: 0xe4d645bf55acb1c661b1704428436a2330c1d56c6b06ce800185f610328b2703c
  • RPC error: gas limit too high
  • eth_getTransactionByHash: null
  • Receipt lookup: null
  • Wallet nonce afterward: latest=0, pending=0

No contract was created and no Intelligent Contract state changed.

Conclusion

Across the tested manual and automatic transaction-construction paths, this Governor-sized deployment is rejected at a gas limit below 16,000,000. The fresh wallet and clean nonce rule out the earlier stuck-nonce wallet as the cause, and the automatic path rules out a manually guessed gas limit as the sole cause.

This may affect other sizably-sized Intelligent Contract deployments or upgrades on Bradbury, not only this Governor. We are not claiming every contract has the same gas requirement; the exact Bradbury ceiling remains to be determined.

Question

What is Bradbury's actual per-transaction gas ceiling, and what is the supported route for deploying or upgrading Intelligent Contracts that require more gas? Is there a chunked deployment, protocol-specific upgrade route, different transaction type, or documented contract-size/gas guidance?

Activity

  1. Snehal707 commented on Sep 16, 2026

    @Snehal707
    Author

    Update on genlayer-cli#419: minified/minimal candidates tested, ceiling not resolved

    Minimal-patch candidate tested: governor.py from the exact baseline confirmed matching the live Bradbury deployment (a1899cb), plus one change only — get_vault() returns a zero-address default for never-enrolled agents instead of raising KeyError. Commit 4e479b7, 38,874 bytes — 59 bytes above the baseline already proven to deploy successfully.

    Bradbury result: rejected, gas limit too high, via standard genlayer deploy (confirmed uses eth_estimateGas → eth_sendTransaction, not the separately-known-broken estimateTransactionFeesForWrite() v2 estimator — ruled out as the cause). A contract just 59 bytes larger than one already live on this network was still rejected outright.

    Studio-dev attempted as a comparison point, inconclusive:

    • First attempt failed before reaching the network: the SDK's built-in studionet preset uses chain ID 61999; Studio-dev's actual RPC reports 61997. Separate bug, worth flagging on its own — this would silently break any Studio-dev deployment via the SDK's default preset.
    • Retried with chain ID corrected manually (61997). The transaction reached the network and returned a specific revert reason, FeesDistributionMissing; no contract was created.
    • This means Studio-dev's deployment path requires fee-distribution parameters (distribution/feeValue) that the tested SDK deployment API doesn't expose — not that the contract is too large for Studio-dev.

    Also ruled out: minification (stripping comments/docstrings/shortening locals) — a full-feature variant was reduced to 35,703 bytes, below the working baseline, and still rejected identically on Bradbury. This suggests raw source-text size isn't the primary driver of the rejection; something else (method count, storage complexity, or execution cost) is more likely.

    Net result: the Bradbury gas-limit rejection remains unresolved. We are not claiming a universal GenLayer protocol ceiling — only that it blocks deployment of a contract nearly identical in size to one already live on this same network, via every method tested (manual gas, automatic estimation, minified source). The get_vault() fix remains correct and tested in source, but not deployable to the live Governor by any method tried so far.

    Two new, separate issues surfaced during this investigation, reported for visibility even though unrelated to the core question:

    1. SDK's studionet preset chain ID (61999) doesn't match Studio-dev's actual chain ID (61997).
    2. Studio-dev's deployment path reverts with FeesDistributionMissing when fee-distribution parameters aren't supplied — worth documenting what format/API is expected.
  2. ygd58 commented on Sep 18, 2026

    @ygd58

    Ran a systematic binary search on this using our own funded Bradbury account (via a locally-patched CLI build with an explicit --gas flag, since the published CLI has no gas-override support yet). Found something that changes the framing of the question.

    Setup

    Used the small TransferTest contract (~500 bytes of calldata, not the 41KB Governor) and swept explicit gas limits on deploy:

    Gas limit Result
    16,000,000 ✅ deployed, 5/5 AGREE
    16,500,000 ✅ deployed, 5/5 AGREE
    16,777,216 (2^24) ✅ deployed, 5/5 AGREE
    16,800,000 ❌ gas limit too high
    17,000,000 ❌ gas limit too high
    20,000,000 ❌ gas limit too high
    24,000,000 ❌ gas limit too high
    32,000,000 ❌ gas limit too high

    So for a small-calldata deploy, the actual ceiling is somewhere in the narrow (16,777,216, 16,800,000) window — not below 16,000,000.

    This changes the diagnosis

    Your report found 16,777,216 (2^24) rejected for the 41KB Governor payload, and I initially assumed that was a fixed hard ceiling. It isn't — the exact same value succeeds here with small calldata. Since a fixed gas-limit ceiling would reject 16,777,216 regardless of payload size, and it clearly doesn't, the actual constraint has to be a function of both the gas limit and the calldata/payload size together — something like gas_limit + k * calldata_bytes <= some_fixed_resource_ceiling, not a flat per-transaction gas cap.

    That's consistent with your original finding too: the Governor's 41KB payload failed even at 16,000,000 gas (below where our ~500-byte payload starts failing at all), which fits a combined-cost model where a much larger payload eats into the same shared ceiling.

    What this means practically

    • "Bradbury's gas ceiling" isn't a single number you can document as a constant — it depends on payload size.
    • For anyone hitting this with a large contract: reducing calldata size (splitting a large deploy/upgrade into a smaller initial deploy + a following state-populating transaction, if the contract structure allows it) may unblock deployment even without raising any gas value, since the actual limiting resource appears to be the combined cost, not gas alone.
    • I don't have genlayer-node access to find the actual formula/constant, but if someone does, this narrowed range (~16.78M for ~500 bytes) plus your original data point (fails at 16M for ~41KB) gives two real (gas, size) pairs to reverse-engineer the exact relationship from.

    Happy to run more (gas, calldata-size) pairs if it'd help triangulate the formula further — e.g. testing a few contracts of graduated size to map out the tradeoff curve.

  3. Snehal707 commented on Sep 18, 2026

    @Snehal707
    Author

    The constructor is byte-for-byte identical to the working baseline and only registers the deploying address as an upgrader. There are no loops or pre-populated application collections to simplify.

    The gas sweep admitted this payload at 16,777,216 and rejected it at 16,800,000, matching the reported small-contract bracket. Every admitted attempt tested reverted. Minification and the minimal patch have not produced a successful deployment.

    We have not localized the failure. RPC returns no specific revert reason, and debug_traceTransaction is unavailable. We're pausing further deployment attempts pending guidance or trace access from maintainers.

  4. eudomar500 commented on Sep 21, 2026

    @eudomar500

    Traced this one down, since it also blocks redeploying a contract of ours that went out in May.

    The cap arrived with the sequencer upgrade at L2 block 21,205,822 (2026-09-09 12:09:27 UTC). The upgrade transaction itself, type 0x7e with a 72,000,000 gas limit, is the last transaction on the chain above 2^24:
    https://explorer-api.testnet-chain.genlayer.com/transactions/0x756696b4c9434f059369d35efed4e9f852f4a4f7343d319a9b576777b4f56fdb
    The last ordinary transaction above the cap landed three minutes earlier at block 21,205,619 (gas limit 24,974,206). Nothing above 16,777,216 has been accepted since, checked contiguously through block 21,235,399 and by sampling up to today. Block gasLimit is unchanged at 100,000,000 before and after (https://explorer-api.testnet-chain.genlayer.com/blocks/21205822), so this is a per-transaction cap, not a block parameter.

    The value is DEFAULT_MAX_TX_GAS_LIMIT = 1 << 24 in zksync-os, introduced by PR #683 "feat: add runtime chain config" (merged 2026-06-16, EIP-7825 alignment):
    https://github.com/matter-labs/zksync-os/blob/69bc430549e88f9264066d14f2001707572c5d33/zk_ee/src/system/metadata/chain_config.rs#L12-L15
    matter-labs/zksync-os#683
    web3_clientVersion on Bradbury reports zksync-os/v0.24.0. It is a runtime chain config parameter: validate() enforces 2^24 as the floor, and the chain admin can raise it without a fork. The check is skipped under simulation, which is why eth_estimateGas happily returns 28M and the rejection only shows up at eth_sendRawTransaction as "gas limit too high".

    Practical impact: at roughly 730 gas per byte of calldata, a Python intelligent contract above ~20 KB of source can no longer be deployed. Our Marketplace.py (35,650 bytes) deployed on 2026-05-21 with a 28,759,916 gas limit; the byte-identical payload now estimates 28,902,212 and is refused. Stripping does not help, since the source has no comments to remove.

    @ygd58 your bracket matches ours to the gas (we bisected to 16,777,216 accepted / 16,777,217 rejected). I think the combined-cost model is not needed though: the cap is flat, and calldata only enters through the gas a deploy actually needs, which on Bradbury runs at roughly 730 gas per byte of source. A 41 KB payload needs about 30M, so a transaction signed at 16.7M is admitted and then runs out of gas inside the consensus contract. That also explains @Snehal707's "admitted then reverted" attempts: those are not a second failure mode, they are the same ceiling seen from below. We traced one of ours with debug_traceTransaction on rpc.testnet-chain.genlayer.com and the revert is an out-of-gas five frames deep, with no reason string, so the RPC silence is expected.

    Two things I could not settle from public sources: whether the 2^24 default was chosen or inherited for Bradbury, and the exact pool-level wiring in v0.24.0, which is newer than any public zksync-os-server release. Full write-up with the block scans, the binary searches and the scripts to reproduce them:
    https://github.com/eudomar500/pointmarket/blob/main/experiments/wasm-deploy-probe/GAS_CAP_HISTORY.md

    If the intent is to keep the EIP-7825 default, it would be worth documenting the resulting contract size ceiling; if not, raising max_tx_gas_limit on Bradbury seems like the cheapest fix. Until then the only route for a contract above ~20 KB of source is splitting it into several contracts.

  5. Snehal707 commented on Sep 23, 2026

    @Snehal707
    Author

    This is an exceptional trace, thank you. It resolves both open questions from our last update at once.

    Confirms our bracket exactly (16,777,216 / 16,777,217), and the out-of-gas trace directly explains something we'd deliberately left unresolved: our "admitted then reverted" results across a full gas sweep on a minimal-patch Governor (38,875 bytes) weren't a separate failure mode from the outright rejections — they're the same fixed ceiling seen from below, exactly as you describe. Useful to have that confirmed rather than left as an open question.

    Given the ~730 gas/byte figure and the fixed 2^24 ceiling, our Governor (38–46KB depending on branch) is structurally above the deployable threshold on Bradbury as currently configured, consistent with every result we saw. Given this is a runtime chain config value rather than something baked into the fork, is there any indication whether GenLayer plans to raise max_tx_gas_limit on Bradbury, or is splitting into multiple contracts genuinely the only supported path forward for anything above ~20KB right now?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions