Sitelet https://github.com/ethereum-optimism/optimism/commit/bfcf378660099b71cf94d197af13956bc758e8a5
Skip to content

Commit bfcf378

Browse files
Inphiclaude
andauthored
op-deployer: select the release SP1 verifier for ZK-enabled apply (#22367)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 17c8870 commit bfcf378

8 files changed

Lines changed: 273 additions & 45 deletions

File tree

‎docs/ai/devfeatures.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,17 @@ The bitmap has **two operator-facing input surfaces**, both in op-deployer:
3333
- `--dev-feature-bitmap` (env: `OP_DEPLOYER_DEV_FEATURE_BITMAP`), defined in `op-deployer/pkg/deployer/bootstrap/flags.go`
3434
- Raw 32-byte hex; default empty
3535
- Flows into `ImplementationsConfig.DevFeatureBitmap` and on into `DeployImplementationsInput` for L1 implementation deployment.
36-
- 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.
36+
- When the ZK bit is enabled, Ethereum mainnet and Sepolia default to Succinct's v6.1.0 PLONK verifier, resolved by `standard.SP1VerifierFor`. `--sp1-verifier-address` (env: `DEPLOYER_SP1_VERIFIER_ADDRESS`) overrides that release input and is required on other L1 networks.
3737

3838
2. **Intent file (`globalDeployOverrides.devFeatureBitmap`)**
3939
- Schema field on `Intent`, `op-deployer/pkg/deployer/state/intent.go`
4040
- Lives in the operator's intent TOML/JSON
4141
- Read by the L2 genesis pipeline.
42-
- A live ZK-enabled deployment must also set `globalDeployOverrides.sp1Verifier`.
43-
- 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.
42+
- All of the following applies only when `apply` deploys new implementations. With a predeployed OPCM (`opcmAddress` set), the implementations already exist and `ValidateInputs` rejects `sp1Verifier` outright, so those operators must not set the override.
43+
- A live ZK-enabled `apply` selects the same release-approved verifier as bootstrap on Ethereum mainnet and Sepolia; other L1 networks must set `globalDeployOverrides.sp1Verifier`, which always wins where it is set. Enabling ZK stays an explicit operator choice — the default only picks the verifier, never the feature.
44+
- Both surfaces read the mapping from `standard.SP1VerifierFor`, so bootstrap and apply can never drift.
45+
- The selected raw verifier is recorded in deployment state (`State.SP1Verifier`) and never written back into intent. A resumed deployment reuses the recorded address rather than re-resolving the default, so upgrading op-deployer mid-deployment cannot swap the verifier under a chain.
46+
- Genesis deployments never select the release verifier: it does not exist in a generated genesis. They must set `sp1Verifier` explicitly, or the op-devstack builder can opt into deploying a test raw verifier during genesis.
4447

4548
There is no other production operator-facing surface. `op-node`, `op-program`, `kona`, and rollup config do not take a bitmap at runtime.
4649

@@ -132,6 +135,7 @@ Interop and ZK use the bitmap as their per-chain provisioning switch and have no
132135
| Solidity constants & predicate | `packages/contracts-bedrock/src/libraries/DevFeatures.sol` |
133136
| CLI input | `op-deployer/pkg/deployer/bootstrap/flags.go` |
134137
| Intent input + composition | `op-deployer/pkg/deployer/pipeline/l2genesis.go` |
138+
| Release-approved SP1 verifier | `op-deployer/pkg/deployer/standard/standard.go` |
135139
| Genesis writer | `packages/contracts-bedrock/scripts/L2Genesis.s.sol` |
136140
| L2 storage | `packages/contracts-bedrock/src/L2/L2DevFeatureFlags.sol` |
137141
| L2 runtime reader | `packages/contracts-bedrock/src/L2/L2ContractsManager.sol` |

‎op-deployer/pkg/deployer/bootstrap/implementations.go‎

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster"
1717
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/forge"
1818
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
19+
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
1920
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/verify"
2021
"github.com/ethereum-optimism/optimism/op-deployer/pkg/env"
2122
"github.com/ethereum-optimism/optimism/op-service/bigs"
@@ -61,16 +62,6 @@ type ImplementationsConfig struct {
6162
privateKeyECDSA *ecdsa.PrivateKey
6263
}
6364

64-
const (
65-
// Source: succinctlabs/sp1-contracts@2ac5ecbbe473421a963d67e55f182e9a36576f7c,
66-
// contracts/deployments/1.json, V6_1_0_SP1_VERIFIER_PLONK.
67-
mainnetSP1VerifierV610 = "0xc3c6dDDAc8829b233Dc6536Ec024775a57b0AF2A"
68-
// Succinct deploys the same verifier bytecode and address deterministically on both networks.
69-
// Source: succinctlabs/sp1-contracts@2ac5ecbbe473421a963d67e55f182e9a36576f7c,
70-
// contracts/deployments/11155111.json, V6_1_0_SP1_VERIFIER_PLONK.
71-
sepoliaSP1VerifierV610 = "0xc3c6dDDAc8829b233Dc6536Ec024775a57b0AF2A"
72-
)
73-
7465
func (c *ImplementationsConfig) Check() error {
7566
if c.L1RPCUrl == "" {
7667
return errors.New("l1RPCUrl must be specified")
@@ -154,18 +145,15 @@ func (c *ImplementationsConfig) resolveSP1Verifier(chainID *big.Int) error {
154145
)
155146
}
156147
chainIDUint64 := bigs.Uint64Strict(chainID)
157-
switch chainIDUint64 {
158-
case 1:
159-
c.SP1Verifier = common.HexToAddress(mainnetSP1VerifierV610)
160-
case 11155111:
161-
c.SP1Verifier = common.HexToAddress(sepoliaSP1VerifierV610)
162-
default:
148+
verifier, err := standard.SP1VerifierFor(chainIDUint64)
149+
if err != nil {
163150
return fmt.Errorf(
164151
"no default SP1 verifier for L1 chain ID %d; specify --%s",
165152
chainIDUint64,
166153
SP1VerifierAddressFlagName,
167154
)
168155
}
156+
c.SP1Verifier = verifier
169157
return nil
170158
}
171159

‎op-deployer/pkg/deployer/integration_test/apply_test.go‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
"github.com/ethereum-optimism/optimism/op-chain-ops/addresses"
15+
"github.com/ethereum-optimism/optimism/op-core/devfeatures"
1516
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/bootstrap"
1617
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster"
1718
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/inspect"
@@ -212,6 +213,71 @@ func TestEndToEndBootstrapApplyWithUpgrade(t *testing.T) {
212213
runEndToEndBootstrapAndApplyUpgradeTest(t, afactsFS, cfg)
213214
}
214215

216+
// TestApplyDefaultsSP1VerifierOnSepolia pins that a ZK-enabled live apply with no sp1Verifier
217+
// override deploys an SP1PlonkAdapter wrapping the release-approved raw verifier.
218+
func TestApplyDefaultsSP1VerifierOnSepolia(t *testing.T) {
219+
op_e2e.InitParallel(t)
220+
221+
lgr := testlog.Logger(t, slog.LevelDebug)
222+
223+
forkedL1, stopL1, err := devnet.NewForkedSepolia(lgr)
224+
require.NoError(t, err)
225+
t.Cleanup(func() {
226+
require.NoError(t, stopL1())
227+
})
228+
l1RPC := forkedL1.RPCUrl()
229+
230+
loc, _ := testutil.LocalArtifacts(t)
231+
testCacheDir := testutils.IsolatedTestDirWithAutoCleanup(t)
232+
233+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
234+
defer cancel()
235+
236+
const sepoliaChainID = 11155111
237+
_, pk, dk := shared.DefaultPrivkey(t)
238+
intent, st := shared.NewIntent(t, big.NewInt(sepoliaChainID), dk, uint256.NewInt(12345), loc, loc, 30_000_000)
239+
intent.GlobalDeployOverrides = map[string]any{
240+
"devFeatureBitmap": devfeatures.EnableDevFeature(common.Hash{}, devfeatures.ZKDisputeGameFlag),
241+
}
242+
243+
require.NoError(t, deployer.ApplyPipeline(ctx, deployer.ApplyPipelineOpts{
244+
DeploymentTarget: deployer.DeploymentTargetLive,
245+
L1RPCUrl: l1RPC,
246+
DeployerPrivateKey: pk,
247+
Intent: intent,
248+
State: st,
249+
Logger: lgr,
250+
StateWriter: pipeline.NoopStateWriter(),
251+
CacheDir: testCacheDir,
252+
}))
253+
254+
expected, err := standard.SP1VerifierFor(sepoliaChainID)
255+
require.NoError(t, err)
256+
require.NotNil(t, st.SP1Verifier)
257+
require.Equal(t, expected, *st.SP1Verifier, "apply should persist the selected raw verifier")
258+
require.NotContains(t, intent.GlobalDeployOverrides, "sp1Verifier", "apply must not rewrite the intent")
259+
260+
client, err := ethclient.Dial(l1RPC)
261+
require.NoError(t, err)
262+
defer client.Close()
263+
264+
verifierCode, err := client.CodeAt(ctx, expected, nil)
265+
require.NoError(t, err)
266+
require.NotEmpty(t, verifierCode, "release verifier must already be deployed on Sepolia")
267+
268+
adapter := st.ImplementationsDeployment.SP1PlonkAdapterImpl
269+
require.NotEqual(t, common.Address{}, adapter, "ZK-enabled apply should deploy an SP1PlonkAdapter")
270+
271+
sp1VerifierFn := w3.MustNewFunc("sp1Verifier()", "address")
272+
calldata, err := sp1VerifierFn.EncodeArgs()
273+
require.NoError(t, err)
274+
ret, err := client.CallContract(ctx, ethereum.CallMsg{To: &adapter, Data: calldata}, nil)
275+
require.NoError(t, err)
276+
var wrapped common.Address
277+
require.NoError(t, sp1VerifierFn.DecodeReturns(ret, &wrapped))
278+
require.Equal(t, expected, wrapped, "adapter should wrap the release verifier")
279+
}
280+
215281
func TestEndToEndApply(t *testing.T) {
216282
op_e2e.InitParallel(t)
217283

‎op-deployer/pkg/deployer/pipeline/implementations.go‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,16 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro
6464
if !zkEnabled && hasSP1VerifierOverride {
6565
return fmt.Errorf("sp1Verifier must not be specified when ZK dispute games are disabled")
6666
}
67+
selectedSP1Verifier := requestedSP1Verifier
6768
if intent.OPCMAddress == nil && zkEnabled && !hasSP1VerifierOverride && !env.DeployMockSP1Verifier {
68-
return fmt.Errorf("sp1Verifier must be specified when ZK dispute games are enabled")
69+
// A generated genesis contains no release-approved verifier, so it must be selected explicitly.
70+
if env.IsGenesis {
71+
return fmt.Errorf("sp1Verifier must be specified when ZK dispute games are enabled")
72+
}
73+
selectedSP1Verifier, err = standard.SP1VerifierFor(intent.L1ChainID)
74+
if err != nil {
75+
return fmt.Errorf("sp1Verifier must be specified when ZK dispute games are enabled on L1 chain ID %d: %w", intent.L1ChainID, err)
76+
}
6977
}
7078

7179
if !shouldDeployImplementations(intent, st) {
@@ -110,14 +118,17 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro
110118
SuperchainProxyAdmin: st.SuperchainDeployment.SuperchainProxyAdminImpl,
111119
L1ProxyAdminOwner: st.SuperchainRoles.SuperchainProxyAdminOwner,
112120
Challenger: st.SuperchainRoles.Challenger,
113-
SP1Verifier: proofParams.SP1Verifier,
121+
SP1Verifier: selectedSP1Verifier,
114122
}
115123
if zkEnabled && input.SP1Verifier == (common.Address{}) {
116124
input.SP1Verifier, err = deployGenesisMockSP1Verifier(env)
117125
if err != nil {
118126
return err
119127
}
120128
}
129+
if zkEnabled {
130+
lgr.Info("using SP1 verifier", "address", input.SP1Verifier)
131+
}
121132

122133
if env.UseForge {
123134
lgr.Info("using Forge for DeployImplementations")

‎op-deployer/pkg/deployer/pipeline/implementations_test.go‎

Lines changed: 135 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/ethereum-optimism/optimism/op-chain-ops/script"
1212
"github.com/ethereum-optimism/optimism/op-core/devfeatures"
1313
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts"
14+
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
1415
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state"
1516
"github.com/ethereum-optimism/optimism/op-service/testlog"
1617
"github.com/ethereum/go-ethereum/common"
@@ -41,19 +42,21 @@ func TestParseSP1VerifierOverrideRequiresExactKey(t *testing.T) {
4142
require.False(t, found)
4243
}
4344

44-
func TestDeployImplementationsSP1VerifierValidation(t *testing.T) {
45-
newState := func() *state.State {
46-
return &state.State{
47-
SuperchainDeployment: &addresses.SuperchainContracts{
48-
SuperchainConfigProxy: common.Address{0x01},
49-
SuperchainProxyAdminImpl: common.Address{0x02},
50-
},
51-
SuperchainRoles: &addresses.SuperchainRoles{
52-
SuperchainProxyAdminOwner: common.Address{0x03},
53-
Challenger: common.Address{0x04},
54-
},
55-
}
45+
func newSP1VerifierTestState() *state.State {
46+
return &state.State{
47+
SuperchainDeployment: &addresses.SuperchainContracts{
48+
SuperchainConfigProxy: common.Address{0x01},
49+
SuperchainProxyAdminImpl: common.Address{0x02},
50+
},
51+
SuperchainRoles: &addresses.SuperchainRoles{
52+
SuperchainProxyAdminOwner: common.Address{0x03},
53+
Challenger: common.Address{0x04},
54+
},
5655
}
56+
}
57+
58+
func TestDeployImplementationsSP1VerifierValidation(t *testing.T) {
59+
newState := newSP1VerifierTestState
5760
env := &Env{Logger: testlog.Logger(t, slog.LevelDebug)}
5861

5962
t.Run("disabled rejects verifier", func(t *testing.T) {
@@ -180,3 +183,123 @@ func TestDeployImplementationsSP1VerifierValidation(t *testing.T) {
180183
require.ErrorContains(t, err, "does not match")
181184
})
182185
}
186+
187+
// capturingDeployImplementations records the input DeployImplementations would broadcast. The
188+
// embedded nil ForgeScript satisfies the rest of the interface; only Run is exercised.
189+
type capturingDeployImplementations struct {
190+
script.ForgeScript
191+
input opcm.DeployImplementationsInput
192+
ran bool
193+
}
194+
195+
func (c *capturingDeployImplementations) Run(input opcm.DeployImplementationsInput) (opcm.DeployImplementationsOutput, error) {
196+
c.input = input
197+
c.ran = true
198+
var out opcm.DeployImplementationsOutput
199+
return out, nil
200+
}
201+
202+
// TestDeployImplementationsSelectsStandardSP1Verifier pins which raw SP1 verifier a live
203+
// ZK-enabled apply hands to DeployImplementations.
204+
func TestDeployImplementationsSelectsStandardSP1Verifier(t *testing.T) {
205+
const standardVerifier = "0xc3c6dDDAc8829b233Dc6536Ec024775a57b0AF2A"
206+
207+
// Matches any line reporting a raw verifier, so a reintroduced premature log is caught too.
208+
verifierLogged := testlog.NewMessageContainsFilter("SP1 verifier")
209+
210+
deploy := func(t *testing.T, env Env, intent *state.Intent, st *state.State) (*capturingDeployImplementations, *testlog.CapturingHandler, error) {
211+
capture := new(capturingDeployImplementations)
212+
lgr, logs := testlog.CaptureLogger(t, slog.LevelDebug)
213+
env.Logger = lgr
214+
env.Scripts = &opcm.Scripts{DeployImplementations: capture}
215+
return capture, logs, DeployImplementations(&env, intent, st)
216+
}
217+
218+
zkIntent := func(l1ChainID uint64, overrides map[string]any) *state.Intent {
219+
merged := map[string]any{"devFeatureBitmap": devfeatures.ZKDisputeGameFlag}
220+
for k, v := range overrides {
221+
merged[k] = v
222+
}
223+
return &state.Intent{L1ChainID: l1ChainID, GlobalDeployOverrides: merged}
224+
}
225+
226+
for _, tt := range []struct {
227+
name string
228+
l1ChainID uint64
229+
}{
230+
{"mainnet", 1},
231+
{"sepolia", 11155111},
232+
} {
233+
t.Run(tt.name+" defaults to the release verifier", func(t *testing.T) {
234+
st := newSP1VerifierTestState()
235+
intent := zkIntent(tt.l1ChainID, nil)
236+
capture, logs, err := deploy(t, Env{}, intent, st)
237+
require.NoError(t, err)
238+
require.Equal(t, common.HexToAddress(standardVerifier), capture.input.SP1Verifier)
239+
require.NotNil(t, st.SP1Verifier)
240+
require.Equal(t, common.HexToAddress(standardVerifier), *st.SP1Verifier)
241+
require.NotContains(t, intent.GlobalDeployOverrides, "sp1Verifier")
242+
require.NotNil(t, logs.FindLog(
243+
verifierLogged,
244+
testlog.NewAttributesFilter("address", common.HexToAddress(standardVerifier).String()),
245+
), "should log the verifier it actually deploys against")
246+
})
247+
}
248+
249+
t.Run("explicit override wins", func(t *testing.T) {
250+
override := common.Address{0x05}
251+
capture, _, err := deploy(t, Env{}, zkIntent(11155111, map[string]any{"sp1Verifier": override}), newSP1VerifierTestState())
252+
require.NoError(t, err)
253+
require.Equal(t, override, capture.input.SP1Verifier)
254+
})
255+
256+
t.Run("unsupported network requires an override", func(t *testing.T) {
257+
capture, _, err := deploy(t, Env{}, zkIntent(900, nil), newSP1VerifierTestState())
258+
require.ErrorContains(t, err, "sp1Verifier must be specified")
259+
require.ErrorContains(t, err, "900")
260+
require.False(t, capture.ran)
261+
})
262+
263+
t.Run("disabled ZK selects nothing", func(t *testing.T) {
264+
st := newSP1VerifierTestState()
265+
capture, logs, err := deploy(t, Env{}, &state.Intent{L1ChainID: 11155111}, st)
266+
require.NoError(t, err)
267+
require.Equal(t, common.Address{}, capture.input.SP1Verifier)
268+
require.Nil(t, st.SP1Verifier)
269+
require.Nil(t, logs.FindLog(verifierLogged))
270+
})
271+
272+
t.Run("genesis does not select the release verifier", func(t *testing.T) {
273+
capture, _, err := deploy(t, Env{IsGenesis: true}, zkIntent(11155111, nil), newSP1VerifierTestState())
274+
require.ErrorContains(t, err, "sp1Verifier must be specified")
275+
require.False(t, capture.ran)
276+
})
277+
278+
t.Run("predeployed OPCM bypasses selection", func(t *testing.T) {
279+
opcmAddr := common.Address{0x07}
280+
st := newSP1VerifierTestState()
281+
st.ImplementationsDeployment = &addresses.ImplementationsContracts{OpcmV2Impl: opcmAddr}
282+
// An L1 with no release verifier proves the bypass: selection would otherwise error here.
283+
intent := zkIntent(900, nil)
284+
intent.OPCMAddress = &opcmAddr
285+
286+
capture, logs, err := deploy(t, Env{}, intent, st)
287+
require.NoError(t, err)
288+
require.False(t, capture.ran)
289+
require.Nil(t, st.SP1Verifier)
290+
require.Nil(t, logs.FindLog(verifierLogged))
291+
})
292+
293+
t.Run("resumed deployment keeps the verifier recorded in state", func(t *testing.T) {
294+
recorded := common.Address{0x06}
295+
st := newSP1VerifierTestState()
296+
st.ImplementationsDeployment = &addresses.ImplementationsContracts{SP1PlonkAdapterImpl: common.Address{0xad}}
297+
st.SP1Verifier = &recorded
298+
299+
capture, logs, err := deploy(t, Env{}, zkIntent(11155111, nil), st)
300+
require.NoError(t, err)
301+
require.False(t, capture.ran)
302+
require.Equal(t, recorded, *st.SP1Verifier)
303+
require.Nil(t, logs.FindLog(verifierLogged), "resume must not report a verifier it is not using")
304+
})
305+
}

‎op-deployer/pkg/deployer/standard/standard.go‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,31 @@ func ChallengerAddressFor(chainID uint64) (common.Address, error) {
102102
}
103103
}
104104

105+
const (
106+
// Source: succinctlabs/sp1-contracts@2ac5ecbbe473421a963d67e55f182e9a36576f7c,
107+
// contracts/deployments/1.json, V6_1_0_SP1_VERIFIER_PLONK.
108+
mainnetSP1VerifierV610 = "0xc3c6dDDAc8829b233Dc6536Ec024775a57b0AF2A"
109+
// Succinct deploys the same verifier bytecode and address deterministically on both networks.
110+
// Source: succinctlabs/sp1-contracts@2ac5ecbbe473421a963d67e55f182e9a36576f7c,
111+
// contracts/deployments/11155111.json, V6_1_0_SP1_VERIFIER_PLONK.
112+
sepoliaSP1VerifierV610 = "0xc3c6dDDAc8829b233Dc6536Ec024775a57b0AF2A"
113+
)
114+
115+
// SP1VerifierFor returns the raw SP1 verifier approved for the current OPCM release on the given L1
116+
// chain ID. Both `bootstrap implementations` and `apply` default to it when ZK dispute games are
117+
// enabled and the operator did not pin a verifier explicitly.
118+
// DO NOT MODIFY THIS METHOD WITHOUT CLEARING IT WITH THE EVM SAFETY TEAM.
119+
func SP1VerifierFor(chainID uint64) (common.Address, error) {
120+
switch chainID {
121+
case 1:
122+
return common.HexToAddress(mainnetSP1VerifierV610), nil
123+
case 11155111:
124+
return common.HexToAddress(sepoliaSP1VerifierV610), nil
125+
default:
126+
return common.Address{}, fmt.Errorf("unsupported chain ID: %d", chainID)
127+
}
128+
}
129+
105130
func SuperchainFor(chainID uint64) (superchain.Superchain, error) {
106131
switch chainID {
107132
case 1:

0 commit comments

Comments
 (0)