Reference for the DevFeatures bitmap system. The bitmap is transitional infrastructure that lets in-development features ride alongside production code without a full hardfork. It is expected to disappear once each feature it gates either ships as a real hardfork or is abandoned.
A 32-byte bitmap (bytes32 on chain, common.Hash in Go) where each feature occupies its own nibble — 0x...01, 0x...10, 0x...100, etc. — giving 64 concurrent feature slots. Defined identically on both sides:
- Go:
op-core/devfeatures/devfeatures.go— flag constants,IsDevFeatureEnabled(bitmap, flag),EnableDevFeature(bitmap, flag) - Solidity:
packages/contracts-bedrock/src/libraries/DevFeatures.sol— same constants, same predicate
The two sides are independent literals with no compile-time sync check — a mismatch silently diverges Go and Solidity behavior. The constant values are canonical in those two files; don't trust copies elsewhere.
Active flags:
| Flag | Gates |
|---|---|
OptimismPortalInterop |
Interop migration functions on OptimismPortal2 |
DeployV2DisputeGames |
Legacy, no longer used; constant kept for historical reasons |
ZKDisputeGame |
ZK dispute game system |
SuperRootGamesMigration |
Super-root games migration path in OPCM upgrade — enabled by default |
The predicate is a bitwise AND ((bitmap & flag) == flag && flag != 0) — except that IsDevFeatureEnabled short-circuits to true for SuperRootGamesMigration, which is enabled by default on both the Go and Solidity sides. The bitmap no longer acts as a circuit breaker for it; removal is tracked in #21662.
Adding a new dev feature: the full checklist lives in the DevFeatures.sol natspec — both constant files, the env-var reader in scripts/libraries/Config.sol, the test assembler in test/setup/FeatureFlags.sol, and the CI &features_matrix anchor in .circleci/continue/main.yml all need updating; there is no compile-time link between them.
The bitmap has two operator-facing input surfaces, both in op-deployer:
-
CLI flag on
op-deployer bootstrap implementations--dev-feature-bitmap(env:OP_DEPLOYER_DEV_FEATURE_BITMAP), defined inop-deployer/pkg/deployer/bootstrap/flags.go- Raw 32-byte hex; default empty
- Flows into
ImplementationsConfig.DevFeatureBitmapand on intoDeployImplementationsInputfor L1 implementation deployment. - When the ZK bit is enabled, Ethereum mainnet and Sepolia default to Succinct's v6.1.0 PLONK verifier.
--sp1-verifier-address(env:DEPLOYER_SP1_VERIFIER_ADDRESS) overrides that release input and is required on other L1 networks.
-
Intent file (
globalDeployOverrides.devFeatureBitmap)- Schema field on
Intent,op-deployer/pkg/deployer/state/intent.go - Lives in the operator's intent TOML/JSON
- Read by the L2 genesis pipeline.
- A live ZK-enabled deployment must also set
globalDeployOverrides.sp1Verifier. - The op-devstack builder instead explicitly opts into deploying a test raw verifier during genesis. The generated address is recorded in deployment state, not written back into intent.
- Schema field on
There is no other production operator-facing surface. op-node, op-program, kona, and rollup config do not take a bitmap at runtime.
A third input mechanism, distinct from the bitmap, lives in op-node/rollup/toggles.go. It is a source-code-edit-only developer toggle — not exposed via config, CLI, or env var. The file holds ephemeral Is<Feature>(time) methods on *Config that map an in-development feature to its eventual hardfork timestamp:
func (c *Config) IsL2CM(time uint64) bool {
return c.IsKarst(time)
}The file's comment instructs developers to "replace with return false to disable NUT bundle execution during development." This is a temporary scaffold for the active fork scope; entries are removed after the fork is locked.
What it gates is runtime activation timing in op-node (when NUT bundle deposit transactions are emitted at the activation block), which is orthogonal to what the bitmap gates (whether the supporting predeploys and contracts exist on a given chain at all).
A separate, test-only assembler exists for Foundry tests and fork scripts. It reads env vars for opt-in features and hardcodes default-on features. It is isolated from the production path — no src/ contract and no production deploy script reads these controls.
DEV_FEATURE__OPTIMISM_PORTAL_INTEROPDEV_FEATURE__ZK_DISPUTE_GAME
Interop and ZK are read via vm.envOr(..., false) in packages/contracts-bedrock/scripts/libraries/Config.sol; devFeatureSuperRootGamesMigration() returns true unconditionally. The only callers are under test/:
test/setup/FeatureFlags.sol—resolveFeaturesFromEnv()OR-s each enabled flag intodevFeatureBitmaptest/setup/CommonTest.sol,test/setup/ForkL1Live.s.sol,test/setup/ForkL2Live.s.sol— branch on individualConfig.devFeature*returnstest/L1/OPContractsManagerStandardValidator.t.sol—vm.skip()based on flag state
The env vars and default-on helper exist purely to set up local test fixtures and to skip or branch tests. They never reach a deployed chain. To exercise an opt-in feature in production you must set the bitmap via op-deployer.
All composition happens in op-deployer at deploy / genesis time. The central function is:
buildDevFeatureBitmap()inop-deployer/pkg/deployer/pipeline/l2genesis.go- Reads
intent.GlobalDeployOverrides["devFeatureBitmap"] - Cross-validates the interop bit against the separate
intent.UseInteropboolean — fails the deploy if they disagree - Returns the bitmap to the genesis builder.
- Reads
What this does not do: it does not OR together a list of named feature toggles. The operator hands in a raw bitmap; op-deployer just validates one bit (interop) against a parallel boolean, then passes the bitmap through verbatim. The CLI flag and the intent override are independent surfaces — they are not merged; whichever subcommand runs uses its own input.
The Foundry test-only assembler in FeatureFlags.sol is a separate composition layer entirely; it is never invoked by op-deployer.
From op-deployer the bitmap fans out three ways:
- Into L2 genesis state —
scripts/L2Genesis.s.sol(Foundry script) writes the bitmap into theL2DevFeatureFlagspredeploy at0x42...2DviasetDevFeatureBitmap(). OnlyDEPOSITOR_ACCOUNTcan write; effectively write-once at genesis. - Into L1 implementation deployment —
scripts/deploy/DeployImplementations.s.solconsults the bitmap to decide which implementation contracts to deploy / configure. - Into NUT bundle generation —
scripts/upgrade/GenerateNUTBundle.s.soluses it to decide which predeploy upgrades to include.
It does not flow to op-node, op-program, or kona at runtime. They learn about feature activation through other channels (hardfork timestamps in rollup config, primarily).
On L2 (Solidity), readers consult either the in-memory bitmap (during deploy scripts) or the L2DevFeatureFlags predeploy (at runtime):
Predeploys.isSupportedPredeploy(...)insrc/libraries/Predeploys.soltakes the bitmap as a parameter. Gates whether the interop predeploys (CrossL2Inbox,L2ToL2CrossDomainMessenger,SuperchainETHBridge,ETHLiquidity, all underOptimismPortalInterop) are considered "real" predeploys.scripts/L2Genesis.s.sol— gates which predeploys get installed at genesis.L2ContractsManager._isDevFeatureEnabled()insrc/L2/L2ContractsManager.sol— at runtime, callsIL2DevFeatureFlags.isDevFeatureEnabled(...)on the predeploy to decide whether to execute upgrade flows.
On Go, the bitmap is essentially deploy-time-only:
- The interop cross-check in
buildDevFeatureBitmap()noted above. - No runtime callers. (op-node / op-program use hardfork timestamps, not the bitmap.)
The L2DevFeatureFlags predeploy API (src/L2/L2DevFeatureFlags.sol):
setDevFeatureBitmap(bytes32)— DEPOSITOR_ACCOUNT only, set at genesisdevFeatureBitmap()— read full bitmapisDevFeatureEnabled(bytes32 feature)— per-flag query
Interop and ZK use the bitmap as their per-chain provisioning switch and have no parallel hardfork timestamp. Super-root migration also has no parallel hardfork timestamp, but it is default-on in the predicate, so its bitmap bit no longer acts as a circuit breaker. Network-wide activation timing is a separate mechanism: the hardfork timestamps mapped by the developer toggles in op-node/rollup/toggles.go (see section B), e.g. IsL2CM(time) decides when L2ContractsManager upgrade transactions execute across the network.
- SuperRootGamesMigration is default-on:
IsDevFeatureEnabled/isDevFeatureEnabledreturntruefor it regardless of the bitmap. - #21662 tracks removing the SuperRootGamesMigration flag and its remaining scaffolding.
| Concern | Path |
|---|---|
| Go constants & predicate | op-core/devfeatures/devfeatures.go |
| Solidity constants & predicate | packages/contracts-bedrock/src/libraries/DevFeatures.sol |
| CLI input | op-deployer/pkg/deployer/bootstrap/flags.go |
| Intent input + composition | op-deployer/pkg/deployer/pipeline/l2genesis.go |
| Genesis writer | packages/contracts-bedrock/scripts/L2Genesis.s.sol |
| L2 storage | packages/contracts-bedrock/src/L2/L2DevFeatureFlags.sol |
| L2 runtime reader | packages/contracts-bedrock/src/L2/L2ContractsManager.sol |
| Predeploy gating | packages/contracts-bedrock/src/libraries/Predeploys.sol |
| L2CM hardfork gate | op-node/rollup/toggles.go |
| Test-only feature controls | packages/contracts-bedrock/scripts/libraries/Config.sol |
| Test bitmap assembler | packages/contracts-bedrock/test/setup/FeatureFlags.sol |