From 0dd13c419f44700865d5672dc8a62297db09c7da Mon Sep 17 00:00:00 2001 From: Artemii Gerasimovich Date: Tue, 26 May 2026 18:10:31 +0200 Subject: [PATCH 01/25] op-service: add ChainSigner interface and eth_sign helper Bring in op-service/crypto/espresso.go (ChainSigner interface unifying SignTransaction and arbitrary-data signing) and op-service/signer/espresso.go (SignerClient.Sign wrapper around eth_sign). Required by the Espresso batcher to sign batch-authentication payloads with either a remote signer or a local private key, in addition to the existing transaction-signing path. Co-authored-by: OpenCode --- op-service/crypto/espresso.go | 168 +++++++++++++++++++++++++++++ op-service/crypto/espresso_test.go | 134 +++++++++++++++++++++++ op-service/signer/espresso.go | 18 ++++ 3 files changed, 320 insertions(+) create mode 100644 op-service/crypto/espresso.go create mode 100644 op-service/crypto/espresso_test.go create mode 100644 op-service/signer/espresso.go diff --git a/op-service/crypto/espresso.go b/op-service/crypto/espresso.go new file mode 100644 index 00000000000..b61e4b6fff1 --- /dev/null +++ b/op-service/crypto/espresso.go @@ -0,0 +1,168 @@ +package crypto + +import ( + "bytes" + "context" + "crypto/ecdsa" + "errors" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + + hdwallet "github.com/ethereum-optimism/go-ethereum-hdwallet" + opsigner "github.com/ethereum-optimism/optimism/op-service/signer" +) + +// ChainSignerFactory creates a SignerFn that is bound to a specific ChainID +type ChainSignerFactory func(chainID *big.Int, from common.Address) ChainSigner + +// ChainSigner is a generic interface for signing transactions or arbitrary data. +type ChainSigner interface { + + // SignTransaction signs a transaction with the given address. + SignTransaction(ctx context.Context, addr common.Address, tx *types.Transaction) (*types.Transaction, error) + + // Sign signs the hash of arbitrary data. + Sign(ctx context.Context, hash []byte) ([]byte, error) +} + +// clientSigner is a ChainSigner that utilizes a remote signer to perform +// Sign and SignTransaction +type clientSigner struct { + signerClient *opsigner.SignerClient + fromAddress common.Address + chainID *big.Int +} + +// Sign implements Signer. +func (c *clientSigner) Sign(ctx context.Context, data []byte) ([]byte, error) { + return c.signerClient.Sign(ctx, c.fromAddress, data) +} + +// VerifySignature verifies that the signature was produced by the expected address. +// data is the original message (e.g., txdata.CallData()) and signature is the result +// from eth_sign. +func Verify(data []byte, signature []byte, expected common.Address) error { + + pubKey, err := crypto.SigToPub(data, signature) + if err != nil { + return fmt.Errorf("failed to recover public key: %w", err) + } + + // Convert the ecdsa.PublicKey to an Address + address := crypto.PubkeyToAddress(*pubKey) + + // Ensure that the derived address matches the expected address. + if !bytes.Equal(address.Bytes(), expected.Bytes()) { + return fmt.Errorf("address mismatch: got %s, expected %s", address.Hex(), expected.Hex()) + } + + return nil +} + +// SignTransaction implements Signer. +func (c *clientSigner) SignTransaction(ctx context.Context, address common.Address, tx *types.Transaction) (*types.Transaction, error) { + if !bytes.Equal(address[:], c.fromAddress[:]) { + return nil, fmt.Errorf("attempting to sign for %s, expected %s: ", address, c.fromAddress) + } + return c.signerClient.SignTransaction(ctx, c.chainID, address, tx) +} + +var _ ChainSigner = &clientSigner{} + +// privateKeySigner is a ChainSigner that delegates to the stored +// functions for performing Sign and SignTransaction. In general these stored +// functions are expected to have access to a private key that is not +// explicitly stored within the structure itself. +type privateKeySigner struct { + chainID *big.Int + st bind.SignerFn + fromAddress common.Address + s func(common.Address, []byte) ([]byte, error) +} + +// Sign implements Signer. +func (p *privateKeySigner) Sign(ctx context.Context, hash []byte) ([]byte, error) { + return p.s(p.fromAddress, hash) +} + +// SignTransaction implements Signer. +func (p *privateKeySigner) SignTransaction(ctx context.Context, addr common.Address, tx *types.Transaction) (*types.Transaction, error) { + return p.st(addr, tx) +} + +var _ ChainSigner = &privateKeySigner{} + +// ChainSignerFactoryFromConfig considers three ways that signers are created & then creates single factory from those config options. +// It can either take a remote signer (via opsigner.CLIConfig) or it can be provided either a mnemonic + derivation path or a private key. +// It prefers the remote signer, then the mnemonic or private key (only one of which can be provided). +func ChainSignerFactoryFromConfig(l log.Logger, privateKey, mnemonic, hdPath string, signerConfig opsigner.CLIConfig) (ChainSignerFactory, common.Address, error) { + var signer ChainSignerFactory + var fromAddress common.Address + if signerConfig.Enabled() { + signerClient, err := opsigner.NewSignerClientFromConfig(l, signerConfig) + if err != nil { + l.Error("Unable to create Signer Client", "error", err) + return nil, common.Address{}, fmt.Errorf("failed to create the signer client: %w", err) + } + fromAddress = common.HexToAddress(signerConfig.Address) + signer = func(chainID *big.Int, _ common.Address) ChainSigner { + return &clientSigner{ + signerClient: signerClient, + fromAddress: fromAddress, + chainID: chainID, + } + } + } else { + var privKey *ecdsa.PrivateKey + var err error + + if privateKey != "" && mnemonic != "" { + return nil, common.Address{}, errors.New("cannot specify both a private key and a mnemonic") + } + if privateKey == "" { + // Parse l2output wallet private key and L2OO contract address. + wallet, err := hdwallet.NewFromMnemonic(mnemonic) + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to parse mnemonic: %w", err) + } + + privKey, err = wallet.PrivateKey(accounts.Account{ + URL: accounts.URL{ + Path: hdPath, + }, + }) + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to create a wallet: %w", err) + } + } else { + privKey, err = crypto.HexToECDSA(strings.TrimPrefix(privateKey, "0x")) + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to parse the private key: %w", err) + } + } + // we force the curve to Geth's instance, because Geth does an equality check in the nocgo version: + // https://github.com/ethereum/go-ethereum/blob/723b1e36ad6a9e998f06f74cc8b11d51635c6402/crypto/signature_nocgo.go#L82 + privKey.PublicKey.Curve = crypto.S256() + fromAddress = crypto.PubkeyToAddress(privKey.PublicKey) + signer = func(chainID *big.Int, from common.Address) ChainSigner { + s := PrivateKeySignerFn(privKey, chainID) + return &privateKeySigner{ + chainID: chainID, + st: s, + s: func(addr common.Address, hash []byte) ([]byte, error) { + return crypto.Sign(hash, privKey) + }, + } + } + } + + return signer, fromAddress, nil +} diff --git a/op-service/crypto/espresso_test.go b/op-service/crypto/espresso_test.go new file mode 100644 index 00000000000..8d42df96f8b --- /dev/null +++ b/op-service/crypto/espresso_test.go @@ -0,0 +1,134 @@ +package crypto + +import ( + "testing" + + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/op-service/signer" + "github.com/ethereum-optimism/optimism/op-service/testlog" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" +) + +// should be run with CGO_ENABLED=0 + +func TestVerify(t *testing.T) { + // happy path + batcherSignature := []byte{ + 109, 206, 105, 108, 152, 110, 156, 111, 239, 153, 224, 182, 140, 49, 105, 120, + 153, 163, 162, 47, 119, 34, 68, 128, 118, 33, 143, 79, 101, 212, 75, 161, + 124, 77, 236, 159, 70, 167, 95, 51, 92, 127, 236, 253, 4, 211, 222, 117, + 54, 27, 214, 232, 135, 87, 33, 77, 16, 155, 164, 116, 220, 116, 31, 208, 1, + } + sequencerBatchesByte := []byte{ + 166, 136, 91, 55, 49, 112, 45, 166, + 46, 142, 74, 143, 88, 74, 196, 106, + 127, 104, 34, 244, 226, 186, 80, 251, + 169, 2, 246, 123, 21, 136, 210, 59, + } + + expected := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + err := Verify(sequencerBatchesByte, batcherSignature, expected) + require.NoError(t, err) + + // wrong length batcher signature + wrongLengthBatcherSignature := []byte{ + 1, + } + err = Verify(sequencerBatchesByte, wrongLengthBatcherSignature, expected) + // check it returns an correct error: address mismatch + require.Error(t, err) + require.Contains(t, err.Error(), "failed to recover public key: invalid signature length") + + // wrong batcher signature + wrongBatcherSignature := []byte{ + 1, 1, 1, 1, 152, 110, 156, 111, 239, 153, 224, 182, 140, 49, 105, 120, + 153, 163, 162, 47, 119, 34, 68, 128, 118, 33, 143, 79, 101, 212, 75, 161, + 124, 77, 236, 159, 70, 167, 95, 51, 92, 127, 236, 253, 4, 211, 222, 117, + 54, 27, 214, 232, 135, 87, 33, 77, 16, 155, 164, 116, 220, 116, 31, 208, 1, + } + err = Verify(sequencerBatchesByte, wrongBatcherSignature, expected) + // check it returns an correct error: address mismatch + require.Error(t, err) + require.Contains(t, err.Error(), "address mismatch") + + // wrong expected address + wrongExpected := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C9") + err = Verify(sequencerBatchesByte, batcherSignature, wrongExpected) + require.Error(t, err) + require.Contains(t, err.Error(), "address mismatch") + +} + +func TestChainSignerFactoryFromMnemonic(t *testing.T) { + mnemonic := "test test test test test test test test test test test junk" + hdPath := "m/44'/60'/0'/0/1" + testChainSignerSignTransaction(t, "", mnemonic, hdPath, signer.CLIConfig{}) + testChainSignerSign(t, "", mnemonic, hdPath, signer.CLIConfig{}) +} + +func TestChainSignerFactoryFromKey(t *testing.T) { + priv := "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + testChainSignerSignTransaction(t, priv, "", "", signer.CLIConfig{}) + testChainSignerSign(t, priv, "", "", signer.CLIConfig{}) +} + +func testChainSignerSignTransaction(t *testing.T, priv, mnemonic, hdPath string, cfg signer.CLIConfig) { + logger := testlog.Logger(t, log.LevelDebug) + + factoryFn, addr, err := ChainSignerFactoryFromConfig(logger, priv, mnemonic, hdPath, cfg) + require.NoError(t, err) + expectedAddr := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + require.Equal(t, expectedAddr, addr) + chainID := big.NewInt(10) + chainSigner := factoryFn(chainID, addr) // for chain ID 10 + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: 0, + GasTipCap: big.NewInt(1), + GasFeeCap: big.NewInt(1), + Gas: 21000, + To: nil, + Value: big.NewInt(0), + Data: []byte("test"), + }) + signedTx, err := chainSigner.SignTransaction(context.Background(), addr, tx) + require.NoError(t, err) + gethSigner := types.LatestSignerForChainID(chainID) + sender, err := gethSigner.Sender(signedTx) + require.NoError(t, err) + require.Equal(t, expectedAddr, sender) +} + +func testChainSignerSign(t *testing.T, priv, mnemonic, hdPath string, cfg signer.CLIConfig) { + logger := testlog.Logger(t, log.LevelDebug) + + factoryFn, addr, err := ChainSignerFactoryFromConfig(logger, priv, mnemonic, hdPath, cfg) + require.NoError(t, err) + expectedAddr := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + require.Equal(t, expectedAddr, addr) + chainID := big.NewInt(10) + chainSigner := factoryFn(chainID, addr) // for chain ID 10 + + payload := []byte{0x01, 0x02, 0x03, 0x04} + hash := crypto.Keccak256(payload) + signed, err := chainSigner.Sign(context.Background(), hash) + require.NoError(t, err) + + // Recover the public key from the signature and hash + pubKey, err := crypto.SigToPub(hash, signed) + require.NoError(t, err) + + // Convert the ecdsa.PublicKey to an Address + address := crypto.PubkeyToAddress(*pubKey) + + // Ensure that the derived address matches the expected address. + require.Equal(t, expectedAddr, address) +} diff --git a/op-service/signer/espresso.go b/op-service/signer/espresso.go new file mode 100644 index 00000000000..f5895de6473 --- /dev/null +++ b/op-service/signer/espresso.go @@ -0,0 +1,18 @@ +package signer + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// Sign represents the interface for signing things via eth_sign. +func (s *SignerClient) Sign(ctx context.Context, address common.Address, data []byte) ([]byte, error) { + var result hexutil.Bytes + if err := s.client.CallContext(ctx, &result, "eth_sign", address, data); err != nil { + return nil, fmt.Errorf("eth_sign failed: %w", err) + } + return result, nil +} From 760f9fa77cf2143511cbc22d32ba141bba7e5f29 Mon Sep 17 00:00:00 2001 From: Artemii Gerasimovich Date: Tue, 26 May 2026 18:12:52 +0200 Subject: [PATCH 02/25] op-node: add EspressoBatch type and converters Introduce op-node/rollup/derive/espresso_batch.go defining EspressoBatch (a SingularBatch with block number, L1 info deposit transaction, and signer address attached), along with BlockToEspressoBatch and the unmarshaler used by the streamer. Also pulls in the github.com/EspressoSystems/espresso-network/sdks/go dependency, which provides the Espresso transaction and namespace types. Consumed by the Espresso batcher (next commits) to convert L2 blocks into batches submitted to Espresso, and to round-trip those batches back through the streamer. Co-authored-by: OpenCode --- go.mod | 6 +- go.sum | 4 + op-node/rollup/derive/espresso_batch.go | 138 +++++++++++ op-node/rollup/derive/espresso_batch_test.go | 237 +++++++++++++++++++ 4 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 op-node/rollup/derive/espresso_batch.go create mode 100644 op-node/rollup/derive/espresso_batch_test.go diff --git a/go.mod b/go.mod index a51b969a148..a4e20093b5a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.24.13 require ( github.com/BurntSushi/toml v1.5.0 + github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 github.com/Masterminds/semver/v3 v3.3.1 github.com/andybalholm/brotli v1.1.0 github.com/base/go-bip39 v1.1.0 @@ -69,7 +70,10 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect +require ( + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f // indirect +) require ( github.com/benbjohnson/immutable v0.4.0 // indirect diff --git a/go.sum b/go.sum index e3f1c2994ab..23020ded907 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e h1:ZIWapoIRN1VqT8GR8jAwb1Ie9GyehWjVcGh32Y2MznE= github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 h1:4KqlmnDg8pPPj1hCtiLnN0Fh9ltp+PPKOft9I06zQOA= +github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513/go.mod h1:kaxR08mJb5Mijy7a2RhWCIWOevFI4PcXwDkzoEbsVTk= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -823,6 +825,8 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go. github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA= +github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= diff --git a/op-node/rollup/derive/espresso_batch.go b/op-node/rollup/derive/espresso_batch.go new file mode 100644 index 00000000000..fc22780bfd5 --- /dev/null +++ b/op-node/rollup/derive/espresso_batch.go @@ -0,0 +1,138 @@ +package derive + +import ( + "bytes" + "context" + "fmt" + + espressoCommon "github.com/EspressoSystems/espresso-network/sdks/go/types" + "github.com/ethereum-optimism/optimism/op-node/rollup" + opCrypto "github.com/ethereum-optimism/optimism/op-service/crypto" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" +) + +// A SingularBatch with block number attached to restore ordering +// when fetching from Espresso +type EspressoBatch struct { + BatchHeader *types.Header + Batch SingularBatch + L1InfoDeposit *types.Transaction + SignerAddress common.Address +} + +func (b EspressoBatch) Number() uint64 { + return b.BatchHeader.Number.Uint64() +} + +func (b EspressoBatch) L1Origin() eth.BlockID { + return b.Batch.Epoch() +} + +func (b EspressoBatch) Header() *types.Header { + return b.BatchHeader +} + +func (b EspressoBatch) Hash() common.Hash { + hash := crypto.Keccak256Hash(b.BatchHeader.Hash().Bytes(), b.L1InfoDeposit.Hash().Bytes()) + return hash +} + +func (b EspressoBatch) Signer() common.Address { + return b.SignerAddress +} + +func (b *EspressoBatch) ToEspressoTransaction(ctx context.Context, namespace uint64, signer opCrypto.ChainSigner) (*espressoCommon.Transaction, error) { + buf := new(bytes.Buffer) + err := rlp.Encode(buf, *b) + if err != nil { + return nil, fmt.Errorf("failed to encode batch: %w", err) + } + + batcherSignature, err := signer.Sign(ctx, crypto.Keccak256(buf.Bytes())) + + if err != nil { + return nil, fmt.Errorf("failed to create batcher signature: %w", err) + } + + payload := append(batcherSignature, buf.Bytes()...) + + return &espressoCommon.Transaction{Namespace: namespace, Payload: payload}, nil + +} + +func BlockToEspressoBatch(rollupCfg *rollup.Config, block *types.Block) (*EspressoBatch, error) { + if len(block.Transactions()) == 0 { + return nil, fmt.Errorf("Block doesn't contain any transactions") + } + + l1InfoDeposit := block.Transactions()[0] + if !l1InfoDeposit.IsDepositTx() { + return nil, fmt.Errorf("First transaction is not L1 info deposit") + } + + batch, _, err := BlockToSingularBatch(rollupCfg, block) + if err != nil { + return nil, err + } + + return &EspressoBatch{ + BatchHeader: block.Header(), + Batch: *batch, + L1InfoDeposit: l1InfoDeposit, + }, nil +} + +// CreateEspressoBatchUnmarshaler returns a function that can be used to +// unmarshal an Espresso transaction into an EspressoBatch. +// The signer address is recovered from the signature and stored on the batch +// for later verification in CheckBatch (two-phase verification). +func CreateEspressoBatchUnmarshaler() func(data []byte) (*EspressoBatch, error) { + return func(data []byte) (*EspressoBatch, error) { + return UnmarshalEspressoTransaction(data) + } +} + +func UnmarshalEspressoTransaction(data []byte) (*EspressoBatch, error) { + if len(data) < crypto.SignatureLength { + return nil, fmt.Errorf("transaction data too short: %d bytes, need at least %d", len(data), crypto.SignatureLength) + } + signatureData, batchData := data[:crypto.SignatureLength], data[crypto.SignatureLength:] + batchHash := crypto.Keccak256(batchData) + + signerKey, err := crypto.SigToPub(batchHash, signatureData) + if err != nil { + return nil, err + } + signer := crypto.PubkeyToAddress(*signerKey) + + var batch EspressoBatch + if err := rlp.DecodeBytes(batchData, &batch); err != nil { + return nil, err + } + batch.SignerAddress = signer + + return &batch, nil +} + +// NOTE: This function MUST guarantee no transient errors. It is allowed to fail only on +// invalid batches or in case of misconfiguration of the batcher, in which case it should fail +// for all batches. +func (b *EspressoBatch) ToBlock(rollupCfg *rollup.Config) (*types.Block, error) { + // Re-insert the deposit transaction + txs := []*types.Transaction{b.L1InfoDeposit} + for i, opaqueTx := range b.Batch.Transactions { + var tx types.Transaction + err := tx.UnmarshalBinary(opaqueTx) + if err != nil { + return nil, fmt.Errorf("could not decode tx %d: %w", i, err) + } + txs = append(txs, &tx) + } + return types.NewBlockWithHeader(b.BatchHeader).WithBody(types.Body{ + Transactions: txs, + }), nil +} diff --git a/op-node/rollup/derive/espresso_batch_test.go b/op-node/rollup/derive/espresso_batch_test.go new file mode 100644 index 00000000000..05ef9c2f9d3 --- /dev/null +++ b/op-node/rollup/derive/espresso_batch_test.go @@ -0,0 +1,237 @@ +package derive_test + +import ( + "bytes" + "math/big" + "math/rand" + "slices" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-node/rollup" + derive "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + dtest "github.com/ethereum-optimism/optimism/op-node/rollup/derive/test" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/common" + gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" +) + +var defaultTestRollUpConfig = &rollup.Config{ + Genesis: rollup.Genesis{L2: eth.BlockID{Number: 0}}, + L2ChainID: big.NewInt(1234), +} + +// compareHash is a helper function that compares two hashes. +func compareHash(a, b common.Hash) int { + if c := bytes.Compare(a[:], b[:]); c != 0 { + return c + } + return 0 +} + +// compareTransaction is a helper function that compares two transactions +// by only inspecting their hashes. +func compareTransaction(a, b *gethTypes.Transaction) int { + return compareHash(a.Hash(), b.Hash()) +} + +// compareHeader is a helper function that compares two headers +// by only inspecting their hashes. +func compareHeader(a, b *gethTypes.Header) int { + return compareHash(a.Hash(), b.Hash()) +} + +// compareWithdrawl is a helper function that compares two withdrawals +// by checking that their slice members compare equivalently. +func compareWithdrawl(a, b *gethTypes.Withdrawal) int { + if c := a.Index - b.Index; c != 0 { + return int(c) + } + + if c := a.Validator - b.Validator; c != 0 { + return int(c) + } + + if c := a.Address.Cmp(b.Address); c != 0 { + return c + } + + if c := a.Amount - b.Amount; c != 0 { + return int(c) + } + + return 0 +} + +// compareBody is a helper function that compares two bodies +// by checking that their slice members compare equivalently. +func compareBody(a, b *gethTypes.Body) int { + if c := slices.CompareFunc(a.Transactions, b.Transactions, compareTransaction); c != 0 { + return c + } + + if c := slices.CompareFunc(a.Uncles, b.Uncles, compareHeader); c != 0 { + return c + } + + if c := slices.CompareFunc(a.Withdrawals, b.Withdrawals, compareWithdrawl); c != 0 { + return c + } + + return 0 +} + +// TestUnmarshalEspressoTransactionTooShort verifies that UnmarshalEspressoTransaction +// returns an error (rather than panicking) when the input is shorter than a signature. +func TestUnmarshalEspressoTransactionTooShort(t *testing.T) { + cases := [][]byte{ + nil, + {}, + make([]byte, crypto.SignatureLength-1), + } + for _, data := range cases { + _, err := derive.UnmarshalEspressoTransaction(data) + require.Error(t, err, "expected error for %d-byte input", len(data)) + } +} + +// TestEspressoBatchConversion tests the conversion of a block to an Espresso +// Batch, and ensures that the recovery of the original Block is possible with +// the contents of the Espresso Batch. +func TestEspressoBatchConversion(t *testing.T) { + rng := rand.New(rand.NewSource(4982432)) + ti := time.Now() + + originalBlock := dtest.RandomL2BlockWithChainIdAndTime(rng, rng.Intn(32), defaultTestRollUpConfig.L2ChainID, ti) + + espressoBatch, err := derive.BlockToEspressoBatch(defaultTestRollUpConfig, originalBlock) + if have, want := err, error(nil); have != want { + t.Fatalf("failed to convert block to batch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + decodedBlock, err := espressoBatch.ToBlock(defaultTestRollUpConfig) + if have, want := err, error(nil); have != want { + t.Fatalf("failed to decode batch back to block:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + // Let's perform a sanity check on the decoded block to ensure that all of + // the fields match the original block. + + if have, want := decodedBlock.BaseFee(), originalBlock.BaseFee(); have.Cmp(want) != 0 { + t.Errorf("decoded block base fee mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.BeaconRoot(), originalBlock.BeaconRoot(); have != want { + t.Errorf("decoded block beacon root mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.BlobGasUsed(), originalBlock.BlobGasUsed(); have != want { + t.Errorf("decoded block blob gas used mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Bloom(), originalBlock.Bloom(); have != want { + t.Errorf("decoded block bloom mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Body(), originalBlock.Body(); compareBody(have, want) != 0 { + t.Errorf("decoded block body mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Coinbase(), originalBlock.Coinbase(); have != want { + t.Errorf("decoded block coinbase mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Difficulty(), originalBlock.Difficulty(); have.Cmp(want) != 0 { + t.Errorf("decoded block difficulty mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.ExcessBlobGas(), originalBlock.ExcessBlobGas(); have != want { + t.Errorf("decoded block excess blob gas mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.ExecutionWitness(), originalBlock.ExecutionWitness(); have != want { + t.Errorf("decoded block execution witness mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Extra(), originalBlock.Extra(); !bytes.Equal(have, want) { + t.Errorf("decoded block extra mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.GasLimit(), originalBlock.GasLimit(); have != want { + t.Errorf("decoded block gas limit mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.GasUsed(), originalBlock.GasUsed(); have != want { + t.Errorf("decoded block gas used mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Hash(), originalBlock.Hash(); have != want { + t.Errorf("decoded block hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Header(), originalBlock.Header(); compareHeader(have, want) != 0 { + t.Errorf("decoded block header mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.MixDigest(), originalBlock.MixDigest(); have != want { + t.Errorf("decoded block mix digest mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Nonce(), originalBlock.Nonce(); have != want { + t.Errorf("decoded block nonce mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Number(), originalBlock.Number(); have.Cmp(want) != 0 { + t.Errorf("decoded block number mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.NumberU64(), originalBlock.NumberU64(); have != want { + t.Errorf("decoded block number u64 mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.ParentHash(), originalBlock.ParentHash(); have != want { + t.Errorf("decoded block parent hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.ReceiptHash(), originalBlock.ReceiptHash(); have != want { + t.Errorf("decoded block receipt hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.RequestsHash(), originalBlock.RequestsHash(); have != want { + t.Errorf("decoded block requests hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Root(), originalBlock.Root(); have != want { + t.Errorf("decoded block root mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Size(), originalBlock.Size(); have != want { + t.Errorf("decoded block size mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Time(), originalBlock.Time(); have != want { + t.Errorf("decoded block time mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Transactions(), originalBlock.Transactions(); slices.CompareFunc(have, want, compareTransaction) != 0 { + t.Errorf("decoded block transactions mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.TxHash(), originalBlock.TxHash(); have != want { + t.Errorf("decoded block tx hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.UncleHash(), originalBlock.UncleHash(); have != want { + t.Errorf("decoded block uncle hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Withdrawals(), originalBlock.Withdrawals(); slices.CompareFunc(have, want, compareWithdrawl) != 0 { + t.Errorf("decoded block withdrawals mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.WithdrawalsRoot(), originalBlock.WithdrawalsRoot(); have != want { + t.Errorf("decoded block withdrawals root mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } +} From 39fc809631f425816deeee0608501cbf737b11d3 Mon Sep 17 00:00:00 2001 From: Artemii Gerasimovich Date: Wed, 27 May 2026 15:54:53 +0200 Subject: [PATCH 03/25] espresso: add TEE batcher CLI flags, streamer interface, and L1 adapter Bring in the parts of the espresso/ shared package that are TEE-only: - espresso/cli.go: full CLI flag set for --espresso.enabled, query service URLs, light-client/L1 endpoints, batch-authenticator address, receipt-verification tuning, namespace/origin-height parameters used to construct the espresso-streamer. (The --espresso.fallback-auth-lead-time flag lives in op-batcher/flags/flags.go and was added by the fallback PR.) - espresso/interface.go: EspressoStreamer[B] interface that wraps github.com/EspressoSystems/espresso-streamers/op.BatchStreamer. - espresso/ethclient.go: AdaptL1BlockRefClient adapter (used by cli.go to construct the streamer) and FetchEspressoBatcherAddress helper. Also adds the EspressoSystems/espresso-network/sdks/go and EspressoSystems/espresso-streamers Go module dependencies. The regenerated BatchAuthenticator bindings already live in the fallback PR's espresso/bindings/. Co-authored-by: OpenCode --- espresso/cli.go | 306 ++++++++++++++++++++++++++++++++++++++++++ espresso/ethclient.go | 60 +++++++++ espresso/interface.go | 77 +++++++++++ go.mod | 5 +- go.sum | 6 +- 5 files changed, 450 insertions(+), 4 deletions(-) create mode 100644 espresso/cli.go create mode 100644 espresso/ethclient.go create mode 100644 espresso/interface.go diff --git a/espresso/cli.go b/espresso/cli.go new file mode 100644 index 00000000000..9d1772ecf53 --- /dev/null +++ b/espresso/cli.go @@ -0,0 +1,306 @@ +package espresso + +import ( + "crypto/ecdsa" + "fmt" + "strings" + "time" + + op "github.com/EspressoSystems/espresso-streamers/op" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + + "github.com/urfave/cli/v2" + + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" +) + +// espressoFlags returns the flag names for espresso +func espressoFlags(v string) string { + return "espresso." + v +} + +func espressoEnvs(envprefix, v string) []string { + return []string{envprefix + "_ESPRESSO_" + v} +} + +// Default values for batch submission receipt verification tuning. +// Defined here so that both the CLI flag defaults and the batcher logic +// can reference a single source of truth. +// +// Note: DefaultBatchAuthLookbackWindow lives in constants.go (mips64-clean +// build target shared with the derivation pipeline). +const ( + DefaultVerifyReceiptMaxBlocks uint64 = 5 + DefaultVerifyReceiptSafetyTimeout time.Duration = 5 * time.Minute + DefaultVerifyReceiptRetryDelay time.Duration = 100 * time.Millisecond + DefaultMaxInFlightRequestsToEspresso = 128 +) + +var ( + EnabledFlagName = espressoFlags("enabled") + PollIntervalFlagName = espressoFlags("poll-interval") + QueryServiceUrlsFlagName = espressoFlags("urls") + LightClientAddrFlagName = espressoFlags("light-client-addr") + L1UrlFlagName = espressoFlags("l1-url") + TestingBatcherPrivateKeyFlagName = espressoFlags("testing-batcher-private-key") + CaffeinationHeightEspresso = espressoFlags("origin-height-espresso") + CaffeinationHeightL2 = espressoFlags("origin-height-l2") + NamespaceFlagName = espressoFlags("namespace") + RollupL1UrlFlagName = espressoFlags("rollup-l1-url") + AttestationServiceFlagName = espressoFlags("espresso-attestation-service") + BatchAuthenticatorAddrFlagName = espressoFlags("batch-authenticator-addr") + VerifyReceiptMaxBlocksFlagName = espressoFlags("verify-receipt-max-blocks") + VerifyReceiptSafetyTimeoutFlagName = espressoFlags("verify-receipt-safety-timeout") + VerifyReceiptRetryDelayFlagName = espressoFlags("verify-receipt-retry-delay") +) + +func CLIFlags(envPrefix string, category string) []cli.Flag { + return []cli.Flag{ + &cli.BoolFlag{ + Name: EnabledFlagName, + Usage: "Enable Espresso mode", + Value: false, + EnvVars: espressoEnvs(envPrefix, "ENABLED"), + Category: category, + }, + &cli.DurationFlag{ + Name: PollIntervalFlagName, + Usage: "Polling interval for Espresso queries", + Value: 250 * time.Millisecond, + EnvVars: espressoEnvs(envPrefix, "POLL_INTERVAL"), + Category: category, + }, + &cli.StringSliceFlag{ + Name: QueryServiceUrlsFlagName, + Usage: "Comma-separated list of Espresso query service URLs", + EnvVars: espressoEnvs(envPrefix, "URLS"), + Category: category, + }, + &cli.StringFlag{ + Name: LightClientAddrFlagName, + Usage: "Address of the Espresso light client", + EnvVars: espressoEnvs(envPrefix, "LIGHT_CLIENT_ADDR"), + Category: category, + }, + &cli.StringFlag{ + Name: L1UrlFlagName, + Usage: "L1 RPC URL Espresso contracts are deployed on", + EnvVars: espressoEnvs(envPrefix, "L1_URL"), + Category: category, + }, + &cli.StringFlag{ + Name: TestingBatcherPrivateKeyFlagName, + Usage: "Pre-approved batcher ephemeral key (testing only)", + EnvVars: espressoEnvs(envPrefix, "TESTING_BATCHER_PRIVATE_KEY"), + Category: category, + }, + &cli.Uint64Flag{ + Name: CaffeinationHeightEspresso, + Usage: "Espresso transactions below this height will not be considered", + EnvVars: espressoEnvs(envPrefix, "ORIGIN_HEIGHT_ESPRESSO"), + Category: category, + }, + &cli.Uint64Flag{ + Name: CaffeinationHeightL2, + Usage: "L2 batch position at which the Espresso streamer starts emitting batches. " + + "Operational parameter for restarting batchers mid-chain. " + + "When zero, the streamer falls back to its internal default. " + + "Independent of the EspressoTime hardfork, which gates derivation semantics.", + Value: 0, + EnvVars: espressoEnvs(envPrefix, "ORIGIN_HEIGHT_L2"), + Category: category, + }, + &cli.Uint64Flag{ + Name: NamespaceFlagName, + Usage: "Namespace of Espresso transactions", + EnvVars: espressoEnvs(envPrefix, "NAMESPACE"), + Category: category, + }, + &cli.StringFlag{ + Name: RollupL1UrlFlagName, + Usage: "RPC URL of L1 backing the Rollup we're streaming for", + EnvVars: espressoEnvs(envPrefix, "ROLLUP_L1_URL"), + Category: category, + }, + &cli.StringFlag{ + Name: AttestationServiceFlagName, + Usage: "URL of the Espresso attestation service", + EnvVars: espressoEnvs(envPrefix, "ESPRESSO_ATTESTATION_SERVICE"), + Category: category, + }, + &cli.StringFlag{ + Name: BatchAuthenticatorAddrFlagName, + Usage: "Address of the Batch Authenticator contract", + EnvVars: espressoEnvs(envPrefix, "BATCH_AUTHENTICATOR_ADDR"), + Category: category, + }, + &cli.Uint64Flag{ + Name: VerifyReceiptMaxBlocksFlagName, + Usage: "Number of HotShot blocks to wait for a submitted transaction to become queryable before re-submitting", + Value: DefaultVerifyReceiptMaxBlocks, + EnvVars: espressoEnvs(envPrefix, "VERIFY_RECEIPT_MAX_BLOCKS"), + Category: category, + }, + &cli.DurationFlag{ + Name: VerifyReceiptSafetyTimeoutFlagName, + Usage: "Wall-clock backstop for receipt verification; re-submits the transaction if this duration is exceeded", + Value: DefaultVerifyReceiptSafetyTimeout, + EnvVars: espressoEnvs(envPrefix, "VERIFY_RECEIPT_SAFETY_TIMEOUT"), + Category: category, + }, + &cli.DurationFlag{ + Name: VerifyReceiptRetryDelayFlagName, + Usage: "Delay between receipt verification retries", + Value: DefaultVerifyReceiptRetryDelay, + EnvVars: espressoEnvs(envPrefix, "VERIFY_RECEIPT_RETRY_DELAY"), + Category: category, + }, + // Note: --espresso.fallback-auth-lead-time is registered by the + // fallback batcher in op-batcher/flags/flags.go; it is read by both + // the fallback and the TEE batcher paths. + } +} + +type CLIConfig struct { + Enabled bool + PollInterval time.Duration + QueryServiceURLs []string + LightClientAddr common.Address + BatchAuthenticatorAddr common.Address + L1URL string + RollupL1URL string + TestingBatcherPrivateKey *ecdsa.PrivateKey + Namespace uint64 + CaffeinationHeightEspresso uint64 + CaffeinationHeightL2 uint64 + EspressoAttestationService string + + // Batch submission receipt verification tuning + VerifyReceiptMaxBlocks uint64 + VerifyReceiptSafetyTimeout time.Duration + VerifyReceiptRetryDelay time.Duration + + // Non directly configurable option + allowEmptyAttestationService bool `json:"-"` +} + +// AllowEmptyAttestationService allows the attestation service URL to be +// empty. This is set explicitly from a public method, and isn't derivable +// from serialization or any other form other than this method. This allows +// this setting to be configured via the code, but not externally. +func (c *CLIConfig) AllowEmptyAttestationService() { + c.allowEmptyAttestationService = true +} + +func (c CLIConfig) Check() error { + if c.Enabled { + // Check required fields when Espresso is enabled + if len(c.QueryServiceURLs) == 0 { + return fmt.Errorf("query service URLs are required when Espresso is enabled") + } + if c.LightClientAddr == (common.Address{}) { + return fmt.Errorf("light client address is required when Espresso is enabled") + } + if c.L1URL == "" { + return fmt.Errorf("L1 URL is required when Espresso is enabled") + } + if c.RollupL1URL == "" { + return fmt.Errorf("rollup L1 URL is required when Espresso is enabled") + } + if c.Namespace == 0 { + return fmt.Errorf("namespace is required when Espresso is enabled") + } + if !c.allowEmptyAttestationService && c.EspressoAttestationService == "" { + return fmt.Errorf("attestation service URL is required when Espresso is enabled") + } + if c.VerifyReceiptMaxBlocks == 0 { + return fmt.Errorf("verify-receipt-max-blocks must be > 0") + } + if c.VerifyReceiptSafetyTimeout <= 0 { + return fmt.Errorf("verify-receipt-safety-timeout must be > 0") + } + if c.VerifyReceiptRetryDelay <= 0 { + return fmt.Errorf("verify-receipt-retry-delay must be > 0") + } + } + return nil +} + +func ReadCLIConfig(c *cli.Context) CLIConfig { + config := CLIConfig{ + Enabled: c.Bool(EnabledFlagName), + PollInterval: c.Duration(PollIntervalFlagName), + L1URL: c.String(L1UrlFlagName), + RollupL1URL: c.String(RollupL1UrlFlagName), + Namespace: c.Uint64(NamespaceFlagName), + CaffeinationHeightEspresso: c.Uint64(CaffeinationHeightEspresso), + CaffeinationHeightL2: c.Uint64(CaffeinationHeightL2), + EspressoAttestationService: c.String(AttestationServiceFlagName), + VerifyReceiptMaxBlocks: c.Uint64(VerifyReceiptMaxBlocksFlagName), + VerifyReceiptSafetyTimeout: c.Duration(VerifyReceiptSafetyTimeoutFlagName), + VerifyReceiptRetryDelay: c.Duration(VerifyReceiptRetryDelayFlagName), + } + + config.QueryServiceURLs = c.StringSlice(QueryServiceUrlsFlagName) + + addrStr := c.String(LightClientAddrFlagName) + config.LightClientAddr = common.HexToAddress(addrStr) + + batchAuthenticatorAddrStr := c.String(BatchAuthenticatorAddrFlagName) + config.BatchAuthenticatorAddr = common.HexToAddress(batchAuthenticatorAddrStr) + + pkStr := c.String(TestingBatcherPrivateKeyFlagName) + pkStr = strings.TrimPrefix(pkStr, "0x") + pk, err := crypto.HexToECDSA(pkStr) + if err == nil { + config.TestingBatcherPrivateKey = pk + } + + return config +} + +func BatchStreamerFromCLIConfig[B op.Batch]( + cfg CLIConfig, + log log.Logger, + unmarshalBatch func([]byte) (*B, error), +) (*op.BatchStreamer[B], error) { + if !cfg.Enabled { + return nil, fmt.Errorf("espresso is not enabled") + } + + l1Client, err := ethclient.Dial(cfg.L1URL) + if err != nil { + return nil, fmt.Errorf("failed to dial L1 RPC at %s: %w", cfg.L1URL, err) + } + + RollupL1Client, err := ethclient.Dial(cfg.RollupL1URL) + if err != nil { + return nil, fmt.Errorf("failed to dial Rollup L1 RPC at %s: %w", cfg.RollupL1URL, err) + } + + urlZero := cfg.QueryServiceURLs[0] + espressoClient := espressoClient.NewClient(urlZero) + + espressoLightClient, err := espressoLightClient.NewLightclientCaller(cfg.LightClientAddr, l1Client) + if err != nil { + return nil, fmt.Errorf("failed to create Espresso light client") + } + + return op.NewEspressoStreamer( + cfg.Namespace, + NewAdaptL1BlockRefClient(l1Client), + NewAdaptL1BlockRefClient(RollupL1Client), + espressoClient, + espressoLightClient, + log, + unmarshalBatch, + cfg.CaffeinationHeightEspresso, + cfg.CaffeinationHeightL2, + cfg.BatchAuthenticatorAddr, + false, + ) +} diff --git a/espresso/ethclient.go b/espresso/ethclient.go new file mode 100644 index 00000000000..38328ce88f8 --- /dev/null +++ b/espresso/ethclient.go @@ -0,0 +1,60 @@ +package espresso + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/ethereum-optimism/optimism/espresso/bindings" +) + +// AdaptL1BlockRefClient is a wrapper around eth.L1BlockRef that implements the espresso.L1Client interface +type AdaptL1BlockRefClient struct { + L1Client *ethclient.Client +} + +// NewAdaptL1BlockRefClient creates a new L1BlockRefClient +func NewAdaptL1BlockRefClient(L1Client *ethclient.Client) *AdaptL1BlockRefClient { + return &AdaptL1BlockRefClient{ + L1Client: L1Client, + } +} + +// HeaderHashByNumber implements the espresso.L1Client interface +func (c *AdaptL1BlockRefClient) HeaderHashByNumber(ctx context.Context, number *big.Int) (common.Hash, error) { + expectedL1BlockRef, err := c.L1Client.HeaderByNumber(ctx, number) + if err != nil { + return common.Hash{}, err + } + + return expectedL1BlockRef.Hash(), nil +} + +func (c *AdaptL1BlockRefClient) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { + return c.L1Client.CodeAt(ctx, contract, blockNumber) +} + +func (c *AdaptL1BlockRefClient) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + return c.L1Client.CallContract(ctx, call, blockNumber) +} + +// FetchEspressoBatcherAddress reads the Espresso batcher address from the BatchAuthenticator +// contract on L1. This is used by the caff node to determine which address signed +// Espresso batches, since the Espresso batcher may use a different key than the +// SystemConfig batcher (fallback batcher). +func FetchEspressoBatcherAddress(ctx context.Context, l1Client *ethclient.Client, batchAuthenticatorAddr common.Address) (common.Address, error) { + caller, err := bindings.NewBatchAuthenticatorCaller(batchAuthenticatorAddr, l1Client) + if err != nil { + return common.Address{}, fmt.Errorf("failed to bind BatchAuthenticator at %s: %w", batchAuthenticatorAddr, err) + } + addr, err := caller.EspressoBatcher(&bind.CallOpts{Context: ctx}) + if err != nil { + return common.Address{}, fmt.Errorf("failed to call BatchAuthenticator.espressoBatcher(): %w", err) + } + return addr, nil +} diff --git a/espresso/interface.go b/espresso/interface.go new file mode 100644 index 00000000000..8445baf08c6 --- /dev/null +++ b/espresso/interface.go @@ -0,0 +1,77 @@ +package espresso + +import ( + "context" + + op "github.com/EspressoSystems/espresso-streamers/op" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/common" +) + +// EspressoStreamer defines the interface for the Espresso streamer. +type EspressoStreamer[B op.Batch] interface { + // Update will update the `EspressoStreamer“ by attempting to ensure that + // the next call to the `Next` method will return a `Batch`. + // + // It attempts to ensure the existence of a next batch, provided no errors + // occur when communicating with HotShot, by processing Blocks retrieved + // from `HotShot` in discreet batches. If each processing of a batch of + // blocks will not yield a new `Batch`, then it will continue to process + // the next batch of blocks from HotShot until it runs out of blocks to + // process. + // + // NOTE: this method is best effort. It is unable to guarantee that the + // next call to `Next` will return a batch. However, the only things + // that will prevent the next call to `Next` from returning a batch is if + // there are no more HotShot blocks to process currently, or if an error + // occurs when communicating with HotShot. + Update(ctx context.Context) error + + // Refresh updates the local references of the EspressoStreamer to the + // specified values. + // + // These values can be used to help determine whether the Streamer needs + // to be reset or not. + // + // NOTE: This will only automatically reset the Streamer if the + // `safeBatchNumber` moves backwards. + Refresh(ctx context.Context, finalizedL1 eth.L1BlockRef, safeBatchNumber uint64, safeL1Origin eth.BlockID) error + + // RefreshSafeL1Origin updates the safe L1 origin for the streamer. This is + // used to help the streamer determine if it needs to be reset or not based + // on the safe L1 origin moving backwards. + // + // NOTE: This will only automatically reset the Streamer if the + // `safeL1Origin` moves backwards. + RefreshSafeL1Origin(safeL1Origin eth.BlockID) + + // Reset will reset the Streamer to the last known good safe state. + // This generally means resetting to the last know good safe batch + // position, but in the case of consuming blocks from Espresso, it will + // also reset the starting Espresso block position to the last known + // good safe block position there as well. + Reset() + + // UnmarshalBatch is a convenience method that allows the caller to + // attempt to unmarshal a batch from the provided byte slice. + UnmarshalBatch(b []byte) (*B, error) + + // HasNext checks to see if there are any batches left to read in the + // streamer. + HasNext(ctx context.Context) bool + + // Next attempts to return the next batch from the streamer. If there + // are no batches left to read, at the moment of the call, it will return + // nil. + Next(ctx context.Context) *B + + // Peek attempts to return the next batch from the streamer without advancing the streamer's position. + // If there are no batches left to read, at the moment of the call, it will return nil. + Peek(ctx context.Context) *B + + // SetProperHead drains stale/wrong-fork entries from the buffer front, + // positioning it at the correct fork for the next Peek call. Should be called + // when Peek returns a batch whose parentHash doesn't match the current chain tip. + // No-ops if headBatch's block number doesn't match the expected next batch position. + SetProperHead(parentHash common.Hash) +} diff --git a/go.mod b/go.mod index a4e20093b5a..7cf03a16890 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.24.13 require ( github.com/BurntSushi/toml v1.5.0 github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 + github.com/EspressoSystems/espresso-streamers v1.1.0 github.com/Masterminds/semver/v3 v3.3.1 github.com/andybalholm/brotli v1.1.0 github.com/base/go-bip39 v1.1.0 @@ -22,7 +23,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20260115192958-fb86a23cd30e - github.com/ethereum/go-ethereum v1.17.0 + github.com/ethereum/go-ethereum v1.17.1 github.com/fsnotify/fsnotify v1.9.0 github.com/golang/snappy v1.0.0 github.com/google/go-cmp v0.7.0 @@ -117,7 +118,7 @@ require ( github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/dchest/siphash v1.2.3 // indirect diff --git a/go.sum b/go.sum index 23020ded907..254233ea5fc 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e h1:ZIWapoIRN1VqT8GR github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 h1:4KqlmnDg8pPPj1hCtiLnN0Fh9ltp+PPKOft9I06zQOA= github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513/go.mod h1:kaxR08mJb5Mijy7a2RhWCIWOevFI4PcXwDkzoEbsVTk= +github.com/EspressoSystems/espresso-streamers v1.1.0 h1:+35Y9I3BCMyVIyFX6DNO6tq8ZKeuyOAve4DrgIhaY2s= +github.com/EspressoSystems/espresso-streamers v1.1.0/go.mod h1:eDGUEvlyxNey9gdpQhKaswAOp5ZWHPVbhNtWfHkpJvU= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -173,8 +175,8 @@ github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf h1:GOPo6vn/vTN+3IwZBvXX github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= From 8f9a4ca90e36da49eea1e076a7110682d027a383 Mon Sep 17 00:00:00 2001 From: Artemii Gerasimovich Date: Tue, 26 May 2026 18:18:48 +0200 Subject: [PATCH 04/25] op-batcher: add Nitro NSM attestation helper Bring in op-batcher/enclave/attestation.go: a thin wrapper around the hf/nsm library that obtains an AWS Nitro NSM attestation document over a given public key. Used by the Espresso batcher (next commit) to attach a TEE attestation to its registration with the BatchAuthenticator. Adds the github.com/hf/nsm dependency. Builds on all platforms; NSM device access is only attempted at runtime when invoked from inside a Nitro enclave. Co-authored-by: OpenCode --- go.mod | 3 ++ go.sum | 8 ++++++ op-batcher/enclave/attestation.go | 47 +++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 op-batcher/enclave/attestation.go diff --git a/go.mod b/go.mod index 7cf03a16890..7729b48b7f1 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/hashicorp/raft v1.7.3 github.com/hashicorp/raft-boltdb/v2 v2.3.1 github.com/hashicorp/raft-wal v0.4.2 + github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 github.com/holiman/uint256 v1.3.2 github.com/ipfs/go-datastore v0.6.0 github.com/ipfs/go-ds-leveldb v0.5.0 @@ -82,10 +83,12 @@ require ( github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf // indirect github.com/fatih/color v1.18.0 // indirect + github.com/fxamacker/cbor/v2 v2.2.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/grafana/pyroscope-go v1.2.7 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/x448/float16 v0.8.4 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect ) diff --git a/go.sum b/go.sum index 254233ea5fc..c0e7d171b9d 100644 --- a/go.sum +++ b/go.sum @@ -260,6 +260,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ= +github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= @@ -423,6 +425,8 @@ github.com/hashicorp/raft-boltdb/v2 v2.3.1 h1:ackhdCNPKblmOhjEU9+4lHSJYFkJd6Jqyv github.com/hashicorp/raft-boltdb/v2 v2.3.1/go.mod h1:n4S+g43dXF1tqDT+yzcXHhXM6y7MrlUd3TTwGRcUvQE= github.com/hashicorp/raft-wal v0.4.2 h1:DV1jgqEumNfdNpOaZ9mL1Gu7Mz59epFtiE6CoqnHrlY= github.com/hashicorp/raft-wal v0.4.2/go.mod h1:S92ainH+6fRuWk6BtZKJ8EgcGgNTKx48Hk5dhOOY1DM= +github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 h1:pU32bJGmZwF4WXb9Yaz0T8vHDtIPVxqDOdmYdwTQPqw= +github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9/go.mod h1:MJsac5D0fKcNWfriUERtln6segcGfD6Nu0V5uGBbPf8= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= @@ -887,6 +891,8 @@ github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMI github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.4 h1:0de1OFQxnNqAu+x2FAKKCVIrnfGKQbs7FQz++tB0+Uw= github.com/wlynxg/anet v0.0.4/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= @@ -962,6 +968,7 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -1133,6 +1140,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105210202-9ed45478a130/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/op-batcher/enclave/attestation.go b/op-batcher/enclave/attestation.go new file mode 100644 index 00000000000..26c4adf7205 --- /dev/null +++ b/op-batcher/enclave/attestation.go @@ -0,0 +1,47 @@ +package enclave + +import ( + "crypto/ecdsa" + "errors" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/hf/nsm" + "github.com/hf/nsm/request" +) + +func attest(nonce, userData, publicKey []byte) ([]byte, error) { + sess, err := nsm.OpenDefaultSession() + if err != nil { + return nil, err + } + defer sess.Close() + + res, err := sess.Send(&request.Attestation{ + Nonce: nonce, + UserData: userData, + PublicKey: publicKey, + }) + if err != nil { + return nil, err + } + + if res.Error != "" { + return nil, errors.New(string(res.Error)) + } + + if res.Attestation == nil || res.Attestation.Document == nil { + return nil, errors.New("NSM device did not return an attestation") + } + + return res.Attestation.Document, nil +} + +func AttestationWithPublicKey(publicKey *ecdsa.PublicKey) ([]byte, error) { + // Use empty slices for nonce and publicKey when they're not needed + nonce := make([]byte, 0) + txData := make([]byte, 0) + publicKeyBytes := crypto.FromECDSAPub(publicKey) + + // Call the existing attest function with txData as userData + return attest(nonce, txData, publicKeyBytes) +} From 15d96f9c10369dab11128c2fb8ff515a65a1d8bd Mon Sep 17 00:00:00 2001 From: Artemii Gerasimovich Date: Wed, 27 May 2026 16:06:36 +0200 Subject: [PATCH 05/25] op-batcher: integrate TEE batcher and Espresso submission loop Add the Espresso TEE batcher write-path on top of the fallback batcher: - op-batcher/batcher/espresso.go: Espresso submission loop (peeks the channel manager, converts each L2 block to an EspressoBatch, submits it to Espresso, waits for inclusion, and then posts the batch txs to L1 with TEE-attested BatchAuthenticator.authenticateBatchInfo calls). - op-batcher/batcher/espresso_service.go: EspressoBatcherConfig, initEspresso (Espresso client / light-client construction, optional TEE attestation gathering), and the initChainSigner hook that wraps the txmgr into a opcrypto.ChainSigner. - op-batcher/batcher/espresso_helpers_test.go and espresso_transaction_submitter_test.go: unit tests for the helpers and the TEE transaction submitter. Extends the existing fallback wiring: - op-batcher/batcher/espresso_driver.go: adds EspressoDriverSetup fields (Client/LightClient/ChainSigner/SequencerAddress/Attestation), batcherL1Adapter, setupEspressoStreamer, startEspressoLoops, resetEspressoStreamer; extends dispatchAuthenticatedSendTx with the TEE branch (always authenticates when Espresso.Enabled). - op-batcher/batcher/espresso_active.go: adds isBatcherActive (queries BatchAuthenticator.activeIsEspresso to gate publishing against this batcher's role). - op-batcher/batcher/driver.go: extends DriverSetup with the Espresso EspressoDriverSetup field; adds espressoSubmitter / espressoStreamer / teeVerifierAddress / degradedLog fields on BatchSubmitter; calls setupEspressoStreamer in NewBatchSubmitter; branches StartBatchSubmitting on Espresso.Enabled to call startEspressoLoops; calls resetEspressoStreamer in clearState. - op-batcher/batcher/service.go: BatcherConfig.Espresso field; EspressoClient / EspressoLightClient / ChainSigner / Attestation runtime fields; initEspresso / initChainSigner / applyEspressoDriverSetup call-outs. - op-batcher/batcher/config.go: thread Espresso espresso.CLIConfig through CLIConfig. - op-batcher/flags/flags.go: register espresso.CLIFlags (TEE-only flags; the --espresso.fallback-auth-lead-time flag added by the fallback PR continues to live in op-batcher/flags/flags.go). Also adds op-service/log/repeat_state.go (RepeatStateLogger) and its test, used by the Espresso submission loop's tick-driven warnings. A safeTestRecorder helper is inlined into the test to avoid pulling in the unrelated debouncer. Adds the github.com/hf/nitrite dependency (transitively required by hf/nsm for attestation document parsing). Co-authored-by: OpenCode --- go.mod | 1 + go.sum | 2 + op-batcher/batcher/config.go | 6 + op-batcher/batcher/driver.go | 38 +- op-batcher/batcher/driver_test.go | 5 +- op-batcher/batcher/espresso.go | 1485 +++++++++++++++++ op-batcher/batcher/espresso_active.go | 61 + op-batcher/batcher/espresso_driver.go | 139 ++ op-batcher/batcher/espresso_fallback_gate.go | 27 +- .../batcher/espresso_fallback_gate_test.go | 5 + op-batcher/batcher/espresso_helpers_test.go | 263 +++ op-batcher/batcher/espresso_service.go | 191 +++ .../espresso_transaction_submitter_test.go | 177 ++ op-batcher/batcher/service.go | 23 + op-batcher/flags/flags.go | 2 + op-service/log/repeat_state.go | 91 + op-service/log/repeat_state_test.go | 234 +++ 17 files changed, 2736 insertions(+), 14 deletions(-) create mode 100644 op-batcher/batcher/espresso.go create mode 100644 op-batcher/batcher/espresso_active.go create mode 100644 op-batcher/batcher/espresso_driver.go create mode 100644 op-batcher/batcher/espresso_helpers_test.go create mode 100644 op-batcher/batcher/espresso_service.go create mode 100644 op-batcher/batcher/espresso_transaction_submitter_test.go create mode 100644 op-service/log/repeat_state.go create mode 100644 op-service/log/repeat_state_test.go diff --git a/go.mod b/go.mod index 7729b48b7f1..a98c2518a8d 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/hashicorp/raft v1.7.3 github.com/hashicorp/raft-boltdb/v2 v2.3.1 github.com/hashicorp/raft-wal v0.4.2 + github.com/hf/nitrite v0.0.0-20241225144000-c2d5d3c4f303 github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 github.com/holiman/uint256 v1.3.2 github.com/ipfs/go-datastore v0.6.0 diff --git a/go.sum b/go.sum index c0e7d171b9d..6e5dca219ba 100644 --- a/go.sum +++ b/go.sum @@ -425,6 +425,8 @@ github.com/hashicorp/raft-boltdb/v2 v2.3.1 h1:ackhdCNPKblmOhjEU9+4lHSJYFkJd6Jqyv github.com/hashicorp/raft-boltdb/v2 v2.3.1/go.mod h1:n4S+g43dXF1tqDT+yzcXHhXM6y7MrlUd3TTwGRcUvQE= github.com/hashicorp/raft-wal v0.4.2 h1:DV1jgqEumNfdNpOaZ9mL1Gu7Mz59epFtiE6CoqnHrlY= github.com/hashicorp/raft-wal v0.4.2/go.mod h1:S92ainH+6fRuWk6BtZKJ8EgcGgNTKx48Hk5dhOOY1DM= +github.com/hf/nitrite v0.0.0-20241225144000-c2d5d3c4f303 h1:XBSq4rXFUgD8ic6Mr7dBwJN/47yg87XpZQhiknfr4Cg= +github.com/hf/nitrite v0.0.0-20241225144000-c2d5d3c4f303/go.mod h1:ycRhVmo6wegyEl6WN+zXOHUTJvB0J2tiuH88q/McTK8= github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 h1:pU32bJGmZwF4WXb9Yaz0T8vHDtIPVxqDOdmYdwTQPqw= github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9/go.mod h1:MJsac5D0fKcNWfriUERtln6segcGfD6Nu0V5uGBbPf8= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= diff --git a/op-batcher/batcher/config.go b/op-batcher/batcher/config.go index c6530742453..a682a0586aa 100644 --- a/op-batcher/batcher/config.go +++ b/op-batcher/batcher/config.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/urfave/cli/v2" + "github.com/ethereum-optimism/optimism/espresso" altda "github.com/ethereum-optimism/optimism/op-alt-da" "github.com/ethereum-optimism/optimism/op-batcher/compressor" "github.com/ethereum-optimism/optimism/op-batcher/config" @@ -152,6 +153,10 @@ type CLIConfig struct { PprofConfig oppprof.CLIConfig RPC oprpc.CLIConfig AltDA altda.CLIConfig + + // Espresso groups all TEE-batcher-specific CLI flags. See + // espresso/cli.go for the flag definitions. + Espresso espresso.CLIConfig } func (c *CLIConfig) Check() error { @@ -248,6 +253,7 @@ func NewConfig(ctx *cli.Context) *CLIConfig { PprofConfig: oppprof.ReadCLIConfig(ctx), RPC: oprpc.ReadCLIConfig(ctx), AltDA: altda.ReadCLIConfig(ctx), + Espresso: espresso.ReadCLIConfig(ctx), ThrottleConfig: ThrottleConfig{ AdditionalEndpoints: ctx.StringSlice(flags.AdditionalThrottlingEndpointsFlag.Name), TxSizeLowerLimit: ctx.Uint64(flags.ThrottleTxSizeLowerLimitFlag.Name), diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 728e03a9b51..187fa9ca4bd 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -12,6 +12,7 @@ import ( "golang.org/x/sync/errgroup" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" @@ -20,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum-optimism/optimism/espresso" altda "github.com/ethereum-optimism/optimism/op-alt-da" "github.com/ethereum-optimism/optimism/op-batcher/batcher/throttler" config "github.com/ethereum-optimism/optimism/op-batcher/config" @@ -28,6 +30,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/eth" + oplog "github.com/ethereum-optimism/optimism/op-service/log" "github.com/ethereum-optimism/optimism/op-service/txmgr" ) @@ -85,6 +88,7 @@ func (r txRef) string(txIDStringer func(txID) string) string { type L1Client interface { HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) + bind.ContractBackend } type L2Client interface { @@ -112,6 +116,11 @@ type DriverSetup struct { ChannelConfig ChannelConfigProvider AltDA AltDAClient ChannelOutFactory ChannelOutFactory + + // Espresso groups all TEE-batcher-specific runtime state plumbed from + // BatcherService. Defined in espresso_driver.go to keep the upstream + // Optimism field block compact. + Espresso EspressoDriverSetup } // BatchSubmitter encapsulates a service responsible for submitting L2 tx @@ -137,7 +146,7 @@ type BatchSubmitter struct { publishSignal chan pubInfo - // authGroup tracks the fallback batcher's receipt-watcher goroutines (one + // authGroup tracks the fallback and TEE batchers' auth goroutines (one // per auth+batch pair) so the publishing loop can drain them via // waitForAuthGroup before closing receiptsCh. New watchers are back-pressured // (not hard-bounded) by the txmgr Queue: queue.Send blocks at @@ -145,6 +154,15 @@ type BatchSubmitter struct { // though a slow receipts loop can briefly leave more than that parked on their // final receiptsCh send. authGroup sync.WaitGroup + + espressoSubmitter *espressoTransactionSubmitter + espressoStreamer espresso.EspressoStreamer[derive.EspressoBatch] + + teeVerifierAddress common.Address + + // degradedLog throttles repeated warnings from tick-driven loops so the + // log debouncer doesn't see the same message every poll interval. + degradedLog *oplog.RepeatStateLogger } // NewBatchSubmitter initializes the BatchSubmitter driver from a preconfigured DriverSetup @@ -157,6 +175,7 @@ func NewBatchSubmitter(setup DriverSetup) *BatchSubmitter { batcher := &BatchSubmitter{ DriverSetup: setup, channelMgr: state, + degradedLog: oplog.NewRepeatStateLogger(), } err := batcher.SetThrottleController(setup.Config.ThrottleParams.ControllerType, setup.Config.ThrottleParams.PIDConfig) @@ -164,6 +183,8 @@ func NewBatchSubmitter(setup DriverSetup) *BatchSubmitter { panic(err) } + batcher.setupEspressoStreamer() + return batcher } @@ -211,10 +232,16 @@ func (l *BatchSubmitter) StartBatchSubmitting() error { l.Log.Warn("Throttling loop is DISABLED due to 0 throttle-threshold. This should not be disabled in prod.") } - l.wg.Add(3) - go l.receiptsLoop(l.wg, receiptsCh) // ranges over receiptsCh channel - go l.publishingLoop(l.killCtx, l.wg, receiptsCh, publishSignal) // ranges over publishSignal, spawns routines which send on receiptsCh. Closes receiptsCh when done. - go l.blockLoadingLoop(l.shutdownCtx, l.wg, unsafeBytesUpdated, publishSignal) // sends on unsafeBytesUpdated (if throttling enabled), and publishSignal. Closes them both when done + if l.Config.Espresso.Enabled { + if err := l.startEspressoLoops(receiptsCh, publishSignal); err != nil { + return err + } + } else { + l.wg.Add(3) + go l.receiptsLoop(l.wg, receiptsCh) // ranges over receiptsCh channel + go l.publishingLoop(l.killCtx, l.wg, receiptsCh, publishSignal) // ranges over publishSignal, spawns routines which send on receiptsCh. Closes receiptsCh when done. + go l.blockLoadingLoop(l.shutdownCtx, l.wg, unsafeBytesUpdated, publishSignal) // sends on unsafeBytesUpdated (if throttling enabled), and publishSignal. Closes them both when done + } l.Log.Info("Batch Submitter started") return nil @@ -856,6 +883,7 @@ func (l *BatchSubmitter) clearState(ctx context.Context) { l.channelMgrMutex.Lock() defer l.channelMgrMutex.Unlock() l.channelMgr.Clear(l1SafeOrigin) + l.resetEspressoStreamer() return true } } diff --git a/op-batcher/batcher/driver_test.go b/op-batcher/batcher/driver_test.go index a1fd5e2b535..90a9e2a8f2f 100644 --- a/op-batcher/batcher/driver_test.go +++ b/op-batcher/batcher/driver_test.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testutils" "github.com/ethereum-optimism/optimism/op-service/txmgr" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" @@ -485,7 +486,9 @@ func TestBatchSubmitter_CriticalError(t *testing.T) { // ======= ALTDA TESTS ======= // fakeL1Client is just a dummy struct. All fault injection is done via the fakeTxMgr (which doesn't interact with this fakeL1Client). -type fakeL1Client struct{} +type fakeL1Client struct { + bind.ContractBackend +} func (f *fakeL1Client) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { if number == nil { diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go new file mode 100644 index 00000000000..5825626bf11 --- /dev/null +++ b/op-batcher/batcher/espresso.go @@ -0,0 +1,1485 @@ +package batcher + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "math/big" + "net/http" + "strings" + "sync" + "sync/atomic" + "time" + + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + tagged_base64 "github.com/EspressoSystems/espresso-network/sdks/go/tagged-base64" + espressoCommon "github.com/EspressoSystems/espresso-network/sdks/go/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + + "github.com/ethereum-optimism/optimism/espresso" + "github.com/ethereum-optimism/optimism/espresso/bindings" + "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txmgr" +) + +// EspressoOnchainProof is the proof structure returned by the attestation service for onchain verification. +type EspressoOnchainProof struct { + Proof json.RawMessage `json:"proof,omitempty"` + Data json.RawMessage `json:"data,omitempty"` + RawProof struct { + Journal string `json:"journal"` + } `json:"raw_proof"` + OnchainProof string `json:"onchain_proof"` +} + +// espressoSubmitTransactionJob is a struct that holds the state required to +// submit a transaction to Espresso. +// It contains the transaction to be submitted itself, and a number to +// track the total number of attempts to submit this transaction to Espresso. +type espressoSubmitTransactionJob struct { + attempts int + transaction *espressoCommon.Transaction +} + +// espressoSubmitTransactionJobResponse is a struct that holds the +// response from the Espresso client after submitting a transaction. +// It contains the job that was submitted, the hash of the transaction +// that was submitted (if successful), and any error that occurred during the +// submission (if unsuccessful). +type espressoSubmitTransactionJobResponse struct { + job espressoSubmitTransactionJob + hash *espressoCommon.TaggedBase64 + err error +} + +// espressoTransactionJobAttempt is a struct that holds the job and +// response channel for a transaction submission job. +// +// This is the unit of work that is submitted to the worker to process +// for transaction submissions. +type espressoTransactionJobAttempt struct { + job espressoSubmitTransactionJob + resp chan espressoSubmitTransactionJobResponse +} + +// espressoVerifyReceiptJob is a struct that holds the state required to +// verify a receipt for a transaction that was submitted to Espresso. +// It contains the transaction that was submitted, the hash of the +// transaction, and the number of attempts to verify the receipt. +type espressoVerifyReceiptJob struct { + attempts int + startHeight uint64 // HotShot block height when verification began (set on first attempt) + startTime time.Time // wall-clock time when verification began (safety backstop) + transaction espressoSubmitTransactionJob + hash *espressoCommon.TaggedBase64 +} + +// espressoVerifyReceiptJobResponse is a struct that holds the +// response from the Espresso client after verifying a receipt. +// It contains the job that was submitted, and any error that occurred +// during the verification (if unsuccessful). +type espressoVerifyReceiptJobResponse struct { + job espressoVerifyReceiptJob + err error + currentHeight uint64 // latest known HotShot block height at time of verification attempt +} + +// espressoVerifyReceiptJobAttempt is a struct that holds the job and +// response channel for a receipt verification job. +// +// This is the unit of work that is submitted to the worker to process +// for receipt verifications. +type espressoVerifyReceiptJobAttempt struct { + job espressoVerifyReceiptJob + resp chan espressoVerifyReceiptJobResponse +} + +// espressoTransactionSubmitter is a struct that holds the state that governs +// the worker queue processing details for submitting transactions to Espresso +// without spawning arbitrarily many goroutines. +type espressoTransactionSubmitter struct { + ctx context.Context + wg *sync.WaitGroup + submitJobQueue chan espressoSubmitTransactionJob + submitRespQueue chan espressoSubmitTransactionJobResponse + submitWorkerQueue chan chan espressoTransactionJobAttempt + verifyReceiptJobQueue chan espressoVerifyReceiptJob + verifyReceiptRespQueue chan espressoVerifyReceiptJobResponse + verifyReceiptWorkerQueue chan chan espressoVerifyReceiptJobAttempt + espresso espressoClient.EspressoClient + latestBlockHeight atomic.Uint64 // shared HotShot block height, updated by trackBlockHeight + verifyReceiptMaxBlocks uint64 + verifyReceiptSafetyTimeout time.Duration + verifyReceiptRetryDelay time.Duration + numInFlightJobs atomic.Int64 + numMaxInFlightJobs int +} + +// EspressoTransactionSubmitterConfig is a configuration struct for the +// EspressoTransactionSubmitter. It contains the configurable details for +// creating the EspressoTransactionSubmitter. +type EspressoTransactionSubmitterConfig struct { + Ctx context.Context + EspressoClient espressoClient.EspressoClient + Wg *sync.WaitGroup + SubmitJobQueueCapacity int + SubmitResponseQueueCapacity int + VerifyReceiptJobQueueCapacity int + VerifyReceiptResponseQueueCapacity int + VerifyReceiptMaxBlocks uint64 + VerifyReceiptSafetyTimeout time.Duration + VerifyReceiptRetryDelay time.Duration + MaxInFlightJobs int +} + +// EspressoTransactionSubmitterOption is a function that can be used to +// configure the EspressoTransactionSubmitterConfig. +type EspressoTransactionSubmitterOption func(*EspressoTransactionSubmitterConfig) + +// WithContext is an option that can be used to set the Espresso client +// for the EspressoTransactionSubmitterConfig. +func WithContext(ctx context.Context) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.Ctx = ctx + } +} + +// WithEspressoClient is an option that can be used to set the Espresso client +// for the EspressoTransactionSubmitterConfig. +func WithEspressoClient(client espressoClient.EspressoClient) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.EspressoClient = client + } +} + +// WithWaitGroup is an option that can be used to set the wait group +// for the EspressoTransactionSubmitterConfig. +func WithWaitGroup(wg *sync.WaitGroup) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.Wg = wg + } +} + +// WithVerifyReceiptMaxBlocks sets the number of HotShot blocks to wait for a +// submitted transaction to become queryable before re-submitting. +func WithVerifyReceiptMaxBlocks(n uint64) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.VerifyReceiptMaxBlocks = n + } +} + +// WithVerifyReceiptSafetyTimeout sets the wall-clock backstop for receipt +// verification. If the block height tracker is stale or broken, re-submission +// is triggered after this duration. +func WithVerifyReceiptSafetyTimeout(d time.Duration) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.VerifyReceiptSafetyTimeout = d + } +} + +// WithVerifyReceiptRetryDelay sets the delay between receipt verification retries. +func WithVerifyReceiptRetryDelay(d time.Duration) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.VerifyReceiptRetryDelay = d + } +} + +// WithMaxInFlightJobs sets the maximum number of inflight requests to +// have at once. Once at capacity all new submission attempts will +// automatically fail. +func WithMaxInFlightJobs(n int) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.MaxInFlightJobs = n + } +} + +// NewEspressoTransactionSubmitter creates a new EspressoTransactionSubmitter +// throttle with the given context and espresso client. It will create a new +// transaction submitter with some default options, and apply those options to +// the configuration. +// +// The resulting instance should reflect the given configuration. +// After returning, the caller should call SpawnWorkers to start the workers, +// and Start to start the job scheduling and response handling portions of the +// transaction submitter. After that, the user should be able to submit +// transactions to the submitter via the SubmitTransaction method. +func NewEspressoTransactionSubmitter(options ...EspressoTransactionSubmitterOption) *espressoTransactionSubmitter { + config := EspressoTransactionSubmitterConfig{ + Ctx: context.Background(), + Wg: new(sync.WaitGroup), + SubmitJobQueueCapacity: espresso.DefaultMaxInFlightRequestsToEspresso, + SubmitResponseQueueCapacity: 10, + VerifyReceiptJobQueueCapacity: espresso.DefaultMaxInFlightRequestsToEspresso, + VerifyReceiptResponseQueueCapacity: 10, + VerifyReceiptMaxBlocks: espresso.DefaultVerifyReceiptMaxBlocks, + VerifyReceiptSafetyTimeout: espresso.DefaultVerifyReceiptSafetyTimeout, + VerifyReceiptRetryDelay: espresso.DefaultVerifyReceiptRetryDelay, + MaxInFlightJobs: espresso.DefaultMaxInFlightRequestsToEspresso, + } + + for _, option := range options { + option(&config) + } + + if config.EspressoClient == nil { + panic("Espresso client is required") + } + + return &espressoTransactionSubmitter{ + ctx: config.Ctx, + wg: config.Wg, + submitJobQueue: make(chan espressoSubmitTransactionJob, config.SubmitJobQueueCapacity), + submitRespQueue: make(chan espressoSubmitTransactionJobResponse, config.SubmitResponseQueueCapacity), + submitWorkerQueue: make(chan chan espressoTransactionJobAttempt), + verifyReceiptJobQueue: make(chan espressoVerifyReceiptJob, config.VerifyReceiptJobQueueCapacity), + verifyReceiptRespQueue: make(chan espressoVerifyReceiptJobResponse, config.VerifyReceiptResponseQueueCapacity), + verifyReceiptWorkerQueue: make(chan chan espressoVerifyReceiptJobAttempt), + espresso: config.EspressoClient, + verifyReceiptMaxBlocks: config.VerifyReceiptMaxBlocks, + verifyReceiptSafetyTimeout: config.VerifyReceiptSafetyTimeout, + verifyReceiptRetryDelay: config.VerifyReceiptRetryDelay, + numInFlightJobs: atomic.Int64{}, + numMaxInFlightJobs: config.MaxInFlightJobs, + } +} + +// ErrTooManyInFlightRequests is an error that is returned when there are +// too many requests in flight at once right now. +type ErrTooManyInFlightRequests struct { + NumInFlightRequests int + MaxInFlightRequests int +} + +// Error implements error +func (e ErrTooManyInFlightRequests) Error() string { + return fmt.Sprintf("too many requests in flight to espresso, in flight requests: %d, maximum allowed: %d", e.NumInFlightRequests, e.MaxInFlightRequests) +} + +// ErrSubmitToEspressoChannelFull is an error that is returned when the channel +// to submit a transaction to the espresso job channel is full. +// +// This ultimately means that the channel buffer size of the job channel is +// smaller than the max number of in flight requests allowed. +type ErrSubmitToEspressoChannelFull struct { + Capacity int + Len int +} + +// Error implements error +func (e ErrSubmitToEspressoChannelFull) Error() string { + return fmt.Sprintf("submit transaction to espresso job channel is full, len: %d, capacity: %d", e.Len, e.Capacity) +} + +// SubmitTransaction will submit a transaction to the Job queue. +// +// NOTE: There is a limit on the maximum number of inflight requests we allow +// at once. If we're over or at capacity, we'll return an error indicating so. +// If we have capacity available, we'll attempt to submit to the channel, and +// if we're unable to, we'll return an error. This will **NOT** block. +func (s *espressoTransactionSubmitter) SubmitTransaction(job *espressoCommon.Transaction) error { + // Check to see if we're over capacity, and if we are, then immediately + // return an error. + if numInFlightRequests, numMaxInFlightRequests := s.numInFlightJobs.Load(), int64(s.numMaxInFlightJobs); numInFlightRequests >= numMaxInFlightRequests { + return ErrTooManyInFlightRequests{ + NumInFlightRequests: int(numInFlightRequests), + MaxInFlightRequests: int(numMaxInFlightRequests), + } + } + + // Construct the job submission + jobSubmission := espressoSubmitTransactionJob{ + transaction: job, + } + + select { + default: + return ErrSubmitToEspressoChannelFull{ + Len: len(s.submitJobQueue), + Capacity: cap(s.submitJobQueue), + } + case s.submitJobQueue <- jobSubmission: + // increment in flight requests + s.numInFlightJobs.Add(1) + return nil + } +} + +// Evaluation result for a job. +type JobEvaluation int + +const ( + // Continue handling the current job. + Handle JobEvaluation = iota + // Retry the submission. + RetrySubmission + // Retry the verification. + RetryVerification + // Skip the current job and proceed to the next one. + Skip +) + +// Evaluate the submission job. +// +// # Returns +// +// * If there is no error: Handle. +// +// * If there is a permanent issue that won't be fixed by a retry: Skip. +// +// * Otherwise: RetrySubmission. +func evaluateSubmission(jobResp espressoSubmitTransactionJobResponse) JobEvaluation { + err := jobResp.err + + // If there's no error, continue handling the submission. + if err == nil { + return Handle + } + + if errors.Is(err, espressoClient.ErrPermanent) { + return Skip + } + + if !errors.Is(err, espressoClient.ErrEphemeral) { + // Log the warning for a potentially missed error handling, but still retry it. + log.Warn("error not explicitly marked as retryable or not", "err", err) + } + + // Otherwise, retry the submission. + return RetrySubmission +} + +// handleTransactionSubmitJobResponse is a function that is meant to be run in a +// goroutine. +// +// It handles the responses from the submit transaction jobs. It will +// determine if the transaction was successfully submitted to Espresso, and +// if not, it will retry the transaction. If the transaction was successfully +// submitted, it will then submit a job to the verify receipt job queue to +// verify the receipt of the transaction. +func (s *espressoTransactionSubmitter) handleTransactionSubmitJobResponse() { + ticker := time.NewTicker(10 * time.Minute) + defer ticker.Stop() + + for { + var jobResp espressoSubmitTransactionJobResponse + var ok bool + + select { + case <-s.ctx.Done(): + return + case <-ticker.C: + log.Debug("Espresso transaction submitter queue status", + "submitJobQueue", len(s.submitJobQueue), + "submitRespQueue", len(s.submitRespQueue), + "verifyReceiptJobQueue", len(s.verifyReceiptJobQueue), + "verifyReceiptRespQueue", len(s.verifyReceiptRespQueue)) + continue + case jobResp, ok = <-s.submitRespQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + switch evaluation := evaluateSubmission(jobResp); evaluation { + case Skip: + s.numInFlightJobs.Add(-1) + continue + case RetrySubmission: + s.submitJobQueue <- jobResp.job + continue + } + + verifyJob := espressoVerifyReceiptJob{ + startTime: time.Now(), + transaction: jobResp.job, + hash: jobResp.hash, + } + + select { + case <-s.ctx.Done(): + return + // Move to verifying the receipt + case s.verifyReceiptJobQueue <- verifyJob: + } + } +} + +// Default values for receipt verification tuning are defined as exported +// constants in the espresso package (espresso.DefaultVerifyReceipt*) so that +// the CLI flag defaults and this batcher logic share a single source of truth. + +// evaluateVerification evaluates the verification job response. +// +// # Returns +// +// * If there is no error: Handle. +// +// * If there is a permanent issue that won't be fixed by a retry: Skip. +// +// * If enough HotShot blocks have passed since verification started: RetrySubmission. +// +// * If the wall-clock safety timeout is exceeded: RetrySubmission. +// +// * Otherwise: RetryVerification. +func (s *espressoTransactionSubmitter) evaluateVerification(jobResp espressoVerifyReceiptJobResponse) JobEvaluation { + err := jobResp.err + + // If there's no error, continue handling the verification. + if err == nil { + return Handle + } + + if errors.Is(err, espressoClient.ErrPermanent) { + return Skip + } + + if !errors.Is(err, espressoClient.ErrEphemeral) { + // Log the warning for a potentially missed error handling, but still retry it. + log.Warn("error not explicitly marked as retryable or not", "err", err) + } + + // Block-count-based timeout: re-submit if enough HotShot blocks have + // passed since verification started. The startHeight guard handles the + // edge case where the height tracker hasn't fetched its first value yet. + if jobResp.job.startHeight > 0 && jobResp.currentHeight >= jobResp.job.startHeight+s.verifyReceiptMaxBlocks { + log.Info("Verification timed out by block count, re-submitting", + "startHeight", jobResp.job.startHeight, + "currentHeight", jobResp.currentHeight, + "maxBlocks", s.verifyReceiptMaxBlocks) + return RetrySubmission + } + + // Wall-clock safety backstop in case the block height tracker is stale + // or broken (e.g., query service returning old data). + if elapsed := time.Since(jobResp.job.startTime); elapsed > s.verifyReceiptSafetyTimeout { + log.Warn("Verification timed out by safety timeout, re-submitting", + "elapsed", elapsed, + "safetyTimeout", s.verifyReceiptSafetyTimeout) + return RetrySubmission + } + + // Otherwise, retry the verification. + return RetryVerification +} + +// handleVerifyReceiptJobResponse is a function that is meant to be run in a +// goroutine. +// +// This function handles responses from the verify receipt job queue. It will +// check the results for any errors, and if there are any errors that are +// applicable to retry, it will requeue the job for another attempt. +// If the the job is successful, no further processing is needed and it is +// considered complete. +// If the job has taken too long to verify, then it will re-submit the job +// back to the submit transaction queue for another attempt. +// +// NOTE: This function currently will loop forever if the transaction is +// never going to be available. +func (s *espressoTransactionSubmitter) handleVerifyReceiptJobResponse() { + for { + var jobResp espressoVerifyReceiptJobResponse + var ok bool + + select { + case <-s.ctx.Done(): + return + case jobResp, ok = <-s.verifyReceiptRespQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + switch evaluation := s.evaluateVerification(jobResp); evaluation { + case Skip: + // decrement in flight jobs on skip, since we're done with this job + s.numInFlightJobs.Add(-1) + continue + case RetrySubmission: + s.submitJobQueue <- jobResp.job.transaction + continue + case RetryVerification: + s.verifyReceiptJobQueue <- jobResp.job + continue + } + + s.numInFlightJobs.Add(-1) + // We're done with this job and transaction, we have successfully + // confirmed that the transaction was submitted to Espresso + commitment := jobResp.job.transaction.transaction.Commit() + hash, _ := tagged_base64.New("TX", commitment[:]) + log.Info("Transaction confirmed on Espresso", "hash", hash.String()) + } +} + +// scheduleSubmitTransactionJobs is a function that is meant to be run in a +// goroutine. +// +// It handles the scheduling of submit transaction jobs so that the submit +// transaction workers can process them. +func (s *espressoTransactionSubmitter) scheduleSubmitTransactionJobs() { + for { + var ok bool + + // Get a worker from the worker queue + var worker chan espressoTransactionJobAttempt + select { + case <-s.ctx.Done(): + return + + case worker, ok = <-s.submitWorkerQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // Get a job from the job queue + var job espressoSubmitTransactionJob + select { + case <-s.ctx.Done(): + return + case job, ok = <-s.submitJobQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // Submit the job to the worker + select { + case <-s.ctx.Done(): + return + + case worker <- espressoTransactionJobAttempt{job: job, resp: s.submitRespQueue}: + } + } +} + +// scheduleVerifyReceiptJobs is a function that is meant to be run in a +// goroutine. +// +// It handles the scheduling of verify receipt jobs so that the verify receipt +// workers can process them. +func (s *espressoTransactionSubmitter) scheduleVerifyReceiptsJobs() { + for { + var ok bool + + // Get a worker from the worker queue + var worker chan espressoVerifyReceiptJobAttempt + select { + case <-s.ctx.Done(): + return + + case worker, ok = <-s.verifyReceiptWorkerQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // Get a job from the job queue + var job espressoVerifyReceiptJob + select { + case <-s.ctx.Done(): + return + case job, ok = <-s.verifyReceiptJobQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // Submit the job to the worker + select { + case <-s.ctx.Done(): + return + + case worker <- espressoVerifyReceiptJobAttempt{job: job, resp: s.verifyReceiptRespQueue}: + } + } +} + +// espressoSubmitTransactionWorker is a function that is meant to be run as a +// goroutine. It will create a channel for it's job queue, and submit those to +// the worker queue in order to wait for work. It will then take that job and +// attempt to submit the transaction contained within to espresso using the +// given espresso client. It will submit the response back to the channel +// contained within the job attempt it received. +// +// It's lifetime is governed by the context passed to it, and it will stop +// processing when that context is cancelled. +// +// NOTE: If the context is cancelled after a job has been received, but before +// it is able to submit the transaction, or report about it's result, the job +// may be lost. +func espressoSubmitTransactionWorker( + ctx context.Context, + wg *sync.WaitGroup, + cli espressoClient.EspressoClient, + workerQueue chan<- chan espressoTransactionJobAttempt, +) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + defer wg.Done() + ch := make(chan espressoTransactionJobAttempt) + defer close(ch) + + for { + var ok bool + select { + case <-ctx.Done(): + return + + // Queue our job queue, asking for work + case workerQueue <- ch: + } + + // Wait for a job to run + var jobAttempt espressoTransactionJobAttempt + select { + case <-ctx.Done(): + return + case jobAttempt, ok = <-ch: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // Submit the transaction to Espresso + hash, err := cli.SubmitTransaction(ctx, *jobAttempt.job.transaction) + if err == nil { + log.Info("Submitted transaction to Espresso", "hash", hash) + } + + jobAttempt.job.attempts++ + resp := espressoSubmitTransactionJobResponse{ + job: jobAttempt.job, + hash: hash, + err: err, + } + + select { + case <-ctx.Done(): + return + + // Send the response back via the channel in the job attempt struct + case jobAttempt.resp <- resp: + } + } +} + +// espressoVerifyTransactionWorker is a function that is meant to be run as a +// goroutine. It will create a channel for it's job queue, and submit those to +// the worker queue in order to wait for work. It will then take that job and +// attempt to verify the transaction contained within to espresso using the +// given espresso client. It will submit the response back to the channel +// contained within the job attempt it received. +func espressoVerifyTransactionWorker( + ctx context.Context, + wg *sync.WaitGroup, + cli espressoClient.EspressoClient, + workerQueue chan<- chan espressoVerifyReceiptJobAttempt, + latestHeight *atomic.Uint64, + retryDelay time.Duration, +) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + defer wg.Done() + ch := make(chan espressoVerifyReceiptJobAttempt) + defer close(ch) + + for { + var ok bool + select { + case <-ctx.Done(): + return + + // Queue our job queue, asking for work + case workerQueue <- ch: + } + + // Wait for a job to run + var jobAttempt espressoVerifyReceiptJobAttempt + select { + case <-ctx.Done(): + return + case jobAttempt, ok = <-ch: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // On the first attempt, snapshot the current block height so we + // can measure how many blocks pass during verification. + if jobAttempt.job.attempts == 0 { + jobAttempt.job.startHeight = latestHeight.Load() + } + + if jobAttempt.job.attempts > 0 { + // We have already attempted this job, so we will wait a bit + // NOTE: this prevents this worker from being able to process + // other jobs while we wait for this delay. + time.Sleep(retryDelay) + } + + _, err := cli.FetchTransactionByHash(ctx, jobAttempt.job.hash) + + jobAttempt.job.attempts++ + resp := espressoVerifyReceiptJobResponse{ + job: jobAttempt.job, + err: err, + currentHeight: latestHeight.Load(), + } + + select { + case <-ctx.Done(): + return + + case jobAttempt.resp <- resp: + } + } +} + +// SpawnWorkers spawns the given number of workers to process the +// submit transaction jobs and verify receipt jobs. +func (s *espressoTransactionSubmitter) SpawnWorkers(numSubmitTransactionWorkers, numVerifyReceiptWorkers int) { + workersCtx := s.ctx + + for i := 0; i < numSubmitTransactionWorkers; i++ { + s.wg.Add(1) + go espressoSubmitTransactionWorker(workersCtx, s.wg, s.espresso, s.submitWorkerQueue) + } + + for i := 0; i < numVerifyReceiptWorkers; i++ { + s.wg.Add(1) + go espressoVerifyTransactionWorker(workersCtx, s.wg, s.espresso, s.verifyReceiptWorkerQueue, &s.latestBlockHeight, s.verifyReceiptRetryDelay) + } +} + +// trackBlockHeight periodically polls FetchLatestBlockHeight and stores +// the result in s.latestBlockHeight for verify jobs to compare against. +// This avoids redundant height queries from individual verify workers. +func (s *espressoTransactionSubmitter) trackBlockHeight() { + for { + height, err := s.espresso.FetchLatestBlockHeight(s.ctx) + if err == nil { + s.latestBlockHeight.Store(height) + } else if s.ctx.Err() == nil { + log.Debug("failed to fetch latest block height for verification tracking", "err", err) + } + + // Wait for the next interval or until context is done. + select { + case <-time.After(s.verifyReceiptRetryDelay): + case <-s.ctx.Done(): + return + } + } +} + +func (s *espressoTransactionSubmitter) Start() { + // Block height tracker for verify receipt timeout + go s.trackBlockHeight() + + // Submit Transaction Jobs + go s.scheduleSubmitTransactionJobs() + go s.handleTransactionSubmitJobResponse() + + // Verify Receipt Jobs + go s.scheduleVerifyReceiptsJobs() + go s.handleVerifyReceiptJobResponse() +} + +// Converts a block to an EspressoBatch and starts a goroutine that publishes it to Espresso +// Returns error only if batch conversion fails, otherwise it is infallible, as the goroutine +// will retry publishing until successful. +func (l *BatchSubmitter) queueBlockToEspresso(ctx context.Context, block *types.Block) error { + espressoBatch, err := derive.BlockToEspressoBatch(l.RollupConfig, block) + if err != nil { + l.Log.Warn("Failed to derive batch from block", "err", err) + return fmt.Errorf("failed to derive batch from block: %w", err) + } + + transaction, err := espressoBatch.ToEspressoTransaction(ctx, l.RollupConfig.L2ChainID.Uint64(), l.Espresso.ChainSigner) + if err != nil { + l.Log.Warn("Failed to create Espresso transaction from a batch", "err", err) + return fmt.Errorf("failed to create Espresso transaction from a batch: %w", err) + } + + commitment := transaction.Commit() + hash, _ := tagged_base64.New("TX", commitment[:]) + l.Log.Info("Created Espresso transaction from batch", "hash", hash, "batchNr", espressoBatch.BatchHeader.Number.Uint64()) + + if err := l.espressoSubmitter.SubmitTransaction(transaction); err != nil { + return fmt.Errorf("failed to submit job to espresso: %w", err) + } + + return nil +} + +func (l *BatchSubmitter) espressoSyncAndRefresh(ctx context.Context, newSyncStatus *eth.SyncStatus) { + err := l.EspressoStreamer().Refresh(ctx, newSyncStatus.FinalizedL1, newSyncStatus.SafeL2.Number, newSyncStatus.FinalizedL2.L1Origin) + if err != nil { + l.degradedLog.Warn(l.Log, "espressoStreamerRefreshErr", "Failed to refresh Espresso streamer", "err", err) + } else { + l.degradedLog.Clear(l.Log, "espressoStreamerRefreshErr", "Espresso streamer refresh recovered") + } + + l.channelMgrMutex.Lock() + defer l.channelMgrMutex.Unlock() + syncActions, outOfSync := computeSyncActions(*newSyncStatus, l.prevCurrentL1, l.channelMgr.blocks, l.channelMgr.channelQueue, l.Log) + if outOfSync { + l.degradedLog.Warn(l.Log, "sequencerOutOfSync", "Sequencer is out of sync, retrying next tick.") + return + } + l.degradedLog.Clear(l.Log, "sequencerOutOfSync", "Sequencer back in sync") + l.prevCurrentL1 = newSyncStatus.CurrentL1 + if syncActions.clearState != nil { + l.channelMgr.Clear(*syncActions.clearState) + l.EspressoStreamer().Reset() + } else { + l.channelMgr.PruneSafeBlocks(syncActions.blocksToPrune) + l.channelMgr.PruneChannels(syncActions.channelsToPrune) + } +} + +// peekNextBatch returns the next batch from the streamer, performing a fork check +// against an expected parent hash. +// +// The expected parent is tip when tip is set. When tip is zero (channel manager was +// just cleared), we fall back to safeL2.Hash if the batch is at exactly safeL2+1 — +// the one position where we can set tip to the known safe head. Otherwise we accept the batch as-is. +func (l *BatchSubmitter) peekNextBatch(ctx context.Context, syncStatus *eth.SyncStatus) *derive.EspressoBatch { + l.channelMgrMutex.Lock() + tip := l.channelMgr.tip + l.channelMgrMutex.Unlock() + + batch := l.EspressoStreamer().Peek(ctx) + if batch == nil { + return nil + } + + // Check if we can set the tip if not set + if tip == (common.Hash{}) && (*batch).Number() == syncStatus.SafeL2.Number+1 { + l.Log.Info( + "setting tip to safe l2 hash", + "batchNr", (*batch).Number(), + "batchParent", (*batch).Header().ParentHash.Hex(), + "tip", tip, + ) + tip = syncStatus.SafeL2.Hash + } + + if tip == (common.Hash{}) { + l.Log.Warn( + "tip is not set, taking available batch", + "blockParentHash", (*batch).Header().ParentHash.Hex(), + "blockHash", (*batch).Header().Hash().Hex(), + ) + return batch + } + + if (*batch).Header().ParentHash != tip { + l.Log.Warn( + "head batch fork mismatch, seeking to proper head", + "batchNr", (*batch).Number(), + "batchParent", (*batch).Header().ParentHash, + "tip", tip, + ) + l.EspressoStreamer().SetProperHead(tip) + return nil + } + + return batch +} + +// Periodically refreshes the sync status and polls Espresso streamer for new batches +func (l *BatchSubmitter) espressoBatchLoadingLoop(ctx context.Context, wg *sync.WaitGroup, publishSignal chan pubInfo) { + l.Log.Info("Starting EspressoBatchLoadingLoop", "polling interval", l.Config.Espresso.PollInterval) + + defer wg.Done() + ticker := time.NewTicker(l.Config.Espresso.PollInterval) + defer ticker.Stop() + defer close(publishSignal) + + for { + select { + case <-ticker.C: + newSyncStatus, err := l.getSyncStatus(ctx) + if err != nil { + l.degradedLog.Warn(l.Log, "syncStatusErr/espressoBatchLoading", "failed to refresh sync status", "err", err) + continue + } + l.degradedLog.Clear(l.Log, "syncStatusErr/espressoBatchLoading", "sync status fetch recovered") + + l.espressoSyncAndRefresh(ctx, newSyncStatus) + + err = l.EspressoStreamer().Update(ctx) + + var batch *derive.EspressoBatch + + for { + + batch = l.peekNextBatch(ctx, newSyncStatus) + + if batch == nil { + break + } + + // This should happen ONLY if the batch is malformed. ToBlock has to guarantee no + // transient errors. + block, err := batch.ToBlock(l.RollupConfig) + if err != nil { + l.Log.Error("failed to convert singular batch to block", "err", err) + l.EspressoStreamer().Next(ctx) + continue + } + + l.Log.Info( + "Received block from Espresso", + "blockNr", block.NumberU64(), + "blockHash", block.Hash(), + "parentHash", block.ParentHash(), + ) + + l.channelMgrMutex.Lock() + err = l.channelMgr.AddL2Block(block) + l.channelMgrMutex.Unlock() + + if err != nil { + l.Log.Error("failed to add L2 block to channel manager", "err", err) + l.clearState(ctx) + l.EspressoStreamer().Reset() + break + } + + l.EspressoStreamer().Next(ctx) + l.Log.Info("Added L2 block to channel manager", "blockNr", block.NumberU64()) + } + + l.tryPublishSignal(publishSignal, pubInfo{}) + + // A failure in the streamer Update can happen after the buffer has been partially filled + if err != nil { + l.degradedLog.Warn(l.Log, "espressoStreamerUpdateErr", "failed to update Espresso streamer", "err", err) + continue + } + l.degradedLog.Clear(l.Log, "espressoStreamerUpdateErr", "Espresso streamer update recovered") + + case <-ctx.Done(): + l.Log.Info("espressoBatchLoadingLoop returning") + return + } + } +} + +type BlockLoader struct { + queuedBlocks []eth.L2BlockRef + prevSyncStatus *eth.SyncStatus + batcher *BatchSubmitter +} + +func (l *BlockLoader) reset(ctx context.Context) { + l.prevSyncStatus = nil + l.queuedBlocks = nil + l.batcher.clearState(ctx) +} + +func (l *BlockLoader) EnqueueBlocks(ctx context.Context, blocksToQueue inclusiveBlockRange) { + l.batcher.Log.Debug("Loading and queueing blocks", "range", blocksToQueue) + for i := blocksToQueue.start; i <= blocksToQueue.end; i++ { + block, err := l.batcher.fetchBlock(ctx, i) + if err != nil { + l.batcher.degradedLog.Warn(l.batcher.Log, "fetchBlockErr", "Failed to fetch block", "err", err) + break + } + l.batcher.degradedLog.Clear(l.batcher.Log, "fetchBlockErr", "Block fetching recovered") + + for _, txn := range block.Transactions() { + l.batcher.Log.Debug("tx hash before submitting to Espresso", "hash", txn.Hash().String()) + } + + if len(l.queuedBlocks) > 0 && block.ParentHash() != l.queuedBlocks[len(l.queuedBlocks)-1].Hash { + l.batcher.Log.Warn("Found L2 reorg", "block_number", i) + l.reset(ctx) + break + } + + blockRef, err := derive.L2BlockToBlockRef(l.batcher.RollupConfig, block) + if err != nil { + // NOTE: if we fail to convert an L2Block to a BlockRef, it's + // unlikely that breaking here, and waiting for resubmission would + // actually ever result in it succeeding. + // For now, we add a log, but this may be a Fatal unrecoverable + // error if it ever occurs. + l.batcher.Log.Warn("failed to convert block to block reference", "err", err) + break + } + + err = l.batcher.queueBlockToEspresso(ctx, block) + if err != nil { + l.batcher.Log.Debug("queue block to espresso failed", "err", err) + break + } + + l.queuedBlocks = append(l.queuedBlocks, blockRef) + } +} + +type EnqueueBlockAction uint + +const ( + ActionEnqueue = iota + ActionRetry + ActionReset +) + +// This function is an analogue of `computeSyncActions` for Espresso batcher mode +// +// It computes the next block range to enqueue to Espresso based on new newSyncStatus and +// does a number of checks to ensure consistency of the chain. +// +// If reorg is detected, empty range and ActionReset is returned. +// If there isn't enough information or no blocks to load yet, empty range and ActionRetry is returned. +func (l *BlockLoader) nextBlockRange(newSyncStatus *eth.SyncStatus) (inclusiveBlockRange, EnqueueBlockAction) { + if newSyncStatus.HeadL1 == (eth.L1BlockRef{}) { + // empty sync status + return inclusiveBlockRange{}, ActionRetry + } + + if l.prevSyncStatus == nil { + l.prevSyncStatus = newSyncStatus + } + + if newSyncStatus.CurrentL1.Number < l.prevSyncStatus.CurrentL1.Number { + // sequencer restarted and hasn't caught up yet + l.batcher.degradedLog.Warn(l.batcher.Log, "sequencerCurrentL1Reversed", "sequencer currentL1 reversed", "new currentL1", newSyncStatus.CurrentL1.Number, "previous currentL1", l.prevSyncStatus.CurrentL1.Number) + return inclusiveBlockRange{}, ActionRetry + } + l.batcher.degradedLog.Clear(l.batcher.Log, "sequencerCurrentL1Reversed", "sequencer currentL1 caught up") + + safeL2 := newSyncStatus.SafeL2 + + // State empty, just enqueue all unsafe blocks + if len(l.queuedBlocks) == 0 { + return inclusiveBlockRange{safeL2.Number + 1, newSyncStatus.UnsafeL2.Number}, ActionEnqueue + } + + lastQueuedBlock := l.queuedBlocks[len(l.queuedBlocks)-1] + firstQueuedBlock := l.queuedBlocks[0] + nextSafeBlockNum := safeL2.Number + 1 + + if lastQueuedBlock.Number >= newSyncStatus.UnsafeL2.Number { + // nothing to enqueue, unsafe block number is not higher than safe + return inclusiveBlockRange{}, ActionRetry + } + + if lastQueuedBlock.Number < safeL2.Number { + // derivation pipeline is somehow ahead of us, reset + return inclusiveBlockRange{}, ActionReset + } + + if nextSafeBlockNum < firstQueuedBlock.Number { + l.batcher.Log.Warn("next safe block is below oldest block in state") + return inclusiveBlockRange{}, ActionReset + } + + numBlocksToEnqueue := nextSafeBlockNum - firstQueuedBlock.Number + + if numBlocksToEnqueue > uint64(len(l.queuedBlocks)) { + l.batcher.Log.Warn("safe head above newest block in state, resetting loader") + return inclusiveBlockRange{}, ActionReset + } + + if numBlocksToEnqueue > 0 && l.queuedBlocks[numBlocksToEnqueue-1].Hash != safeL2.Hash { + l.batcher.Log.Warn("safe chain reorg, resetting loader") + return inclusiveBlockRange{}, ActionReset + } + + if safeL2.Number > firstQueuedBlock.Number { + numFinalizedBlocksInQueue := safeL2.Number - firstQueuedBlock.Number + l.batcher.Log.Warn( + "Removing finalized blocks from queued", + "numFinalizedBlocksInQueue", numFinalizedBlocksInQueue, + "safeL2", safeL2, + "firstQueuedBlock", firstQueuedBlock) + l.queuedBlocks = l.queuedBlocks[numFinalizedBlocksInQueue:] + } + + return inclusiveBlockRange{lastQueuedBlock.Number + 1, newSyncStatus.UnsafeL2.Number}, ActionEnqueue +} + +// numBlocks is a convenience method for inclusiveBlockRange that can be +// utilized to quickly determine how many blocks are being referenced within +// the range. +func (i inclusiveBlockRange) numBlocks() uint64 { + return 1 + i.end - i.start +} + +// LARGE_BLOCK_GAP_THRESHOLD is a threshold for the number of blocks that we +// consider to be a "large gap" when queueing blocks to Espresso. We're +// interested in being alerted when we're falling behind. +const LARGE_BLOCK_GAP_THRESHOLD = 30 * 60 + +// blockLoadingLoop +// - polls the sequencer, +// - queues unsafe blocks from the sequencer to Espresso +func (l *BatchSubmitter) espressoBatchQueueingLoop(ctx context.Context, wg *sync.WaitGroup) { + ticker := time.NewTicker(l.Config.PollInterval) + defer ticker.Stop() + defer wg.Done() + + loader := BlockLoader{ + batcher: l, + } + + // * + // * BEFORE we start: + // * - scan batchInbox from batchInbox.lastBackfilled + // * - enqueue all batches from batchInbox that are _by fallback batcher_ to Espresso + // * - wait for espresso queue to clear + // * - set lastBackfilled to block height of the last of such batches + // * + + for { + select { + case <-ticker.C: + newSyncStatus, err := l.getSyncStatus(ctx) + if err != nil { + l.degradedLog.Warn(l.Log, "syncStatusErr/espressoBatchQueueing", "Couldn't get sync status", "error", err) + continue + } + l.degradedLog.Clear(l.Log, "syncStatusErr/espressoBatchQueueing", "sync status fetch recovered") + + blocksToQueue, action := loader.nextBlockRange(newSyncStatus) + + // We add a check here to add visibility to us that we've exceeded + // a threshold. + if numBlocks := blocksToQueue.numBlocks(); numBlocks >= LARGE_BLOCK_GAP_THRESHOLD { + l.Log.Warn("Large gap of blocks to enqueue to Espresso detected", "numBlocks", numBlocks, "blocksToQueue", blocksToQueue) + } + + if action == ActionEnqueue { + numEnqueuedBlocksBefore := len(loader.queuedBlocks) + loader.EnqueueBlocks(ctx, blocksToQueue) + numEnqueuedBlocksAfter := len(loader.queuedBlocks) + + // This is a check to help us determine whether we're able to + // push through all of the blocks we've attempted to or not. + if (numEnqueuedBlocksAfter - numEnqueuedBlocksBefore) < int(blocksToQueue.numBlocks()) { + // If we're in this conditional, it means we weren't able + // submit all of the blocks to Espresso that we were + // attempting to. + // + // TODO: We should probably throttle a bit. + } + } else if action == ActionReset { + loader.reset(ctx) + } + + case <-ctx.Done(): + l.Log.Info("blockLoadingLoop returning") + return + } + } +} + +func (l *BatchSubmitter) fetchBlock(ctx context.Context, blockNumber uint64) (*types.Block, error) { + l2Client, err := l.EndpointProvider.EthClient(ctx) + if err != nil { + return nil, fmt.Errorf("getting L2 client: %w", err) + } + + cCtx, cancel := context.WithTimeout(ctx, l.Config.NetworkTimeout) + defer cancel() + + block, err := l2Client.BlockByNumber(cCtx, new(big.Int).SetUint64(blockNumber)) + if err != nil { + return nil, fmt.Errorf("getting L2 block: %w", err) + } + + return block, nil +} + +// resolveTEEVerifierAddress queries the BatchAuthenticator contract to get the +// EspressoTEEVerifier address. +func (l *BatchSubmitter) resolveTEEVerifierAddress() error { + if l.RollupConfig.BatchAuthenticatorAddress == (common.Address{}) { + // If batcher authenticator address is nil, we will keep teeVerifierAddress to nil as well + return nil + } + auth, err := bindings.NewBatchAuthenticatorCaller(l.RollupConfig.BatchAuthenticatorAddress, l.L1Client) + if err != nil { + return fmt.Errorf("failed to create BatchAuthenticator caller: %w", err) + } + addr, err := auth.EspressoTEEVerifier(nil) + if err != nil { + return fmt.Errorf("failed to query EspressoTEEVerifier address: %w", err) + } + l.teeVerifierAddress = addr + l.Log.Info("Resolved TEE verifier address", "address", addr.Hex()) + return nil +} + +func (l *BatchSubmitter) registerBatcher(ctx context.Context) error { + if len(l.Espresso.Attestation) == 0 { + l.Log.Warn("Attestation is empty, skipping registration") + return nil + } + + if l.Config.Espresso.AttestationService == "" { + l.Log.Warn("EspressoAttestationServices is not set, skipping registration") + return nil + } + + l.Log.Info("Batch authenticator address", "value", l.RollupConfig.BatchAuthenticatorAddress) + code, err := l.L1Client.CodeAt(ctx, l.RollupConfig.BatchAuthenticatorAddress, nil) + if err != nil { + return fmt.Errorf("failed to check code at contract address: %w", err) + } + if len(code) == 0 { + return fmt.Errorf("no contract deployed at this address %w", err) + } + + abi, err := bindings.BatchAuthenticatorMetaData.GetAbi() + if err != nil { + return fmt.Errorf("failed to get Batch Authenticator ABI: %w", err) + } + + onchainProof, err := l.GenerateZKProof(ctx, l.Espresso.Attestation) + if err != nil { + l.Log.Error("failed to generate zk proof from nitro attestation", "err", err) + return fmt.Errorf("failed to generate zk proof from nitro attestation: %w", err) + } + + journalBytes, err := hex.DecodeString(stripHexPrefix(onchainProof.RawProof.Journal)) + if err != nil { + l.Log.Error("failed to decode journal hex string", "err", err) + return fmt.Errorf("failed to decode journal hex string: %w", err) + } + onchainProofBytes, err := hex.DecodeString(stripHexPrefix(onchainProof.OnchainProof)) + if err != nil { + l.Log.Error("failed to decode onchain proof hex string", "err", err) + return fmt.Errorf("failed to decode onchain proof hex string: %w", err) + } + log.Info("successfully generated zk proof from nitro attestation") + + txData, err := abi.Pack("registerSigner", journalBytes, onchainProofBytes) + if err != nil { + return fmt.Errorf("failed to create registerSigner transaction: %w", err) + } + + candidate := txmgr.TxCandidate{ + TxData: txData, + To: &l.RollupConfig.BatchAuthenticatorAddress, + } + + l.Log.Info("Registering batcher with the BatchAuthenticator contract") + _, err = l.Txmgr.Send(ctx, candidate) + if err != nil { + return fmt.Errorf("failed to send registerBatcher transaction: %w", err) + } + + l.Log.Info("Registered batcher with the BatchAuthenticator contract") + + return nil +} + +func (l *BatchSubmitter) GenerateZKProof(ctx context.Context, attestationBytes []byte) (*EspressoOnchainProof, error) { + attestationServiceURL := strings.TrimSuffix(l.Config.Espresso.AttestationService, "/") + url := attestationServiceURL + "/generate_proof" + request, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(attestationBytes)) + if err != nil { + return nil, err + } + + request.Header.Set("Content-Type", "application/octet-stream") + client := http.Client{ + Timeout: 5 * time.Minute, + } + res, err := client.Do(request) + if err != nil { + return nil, err + } + defer (func() { + _ = res.Body.Close() + })() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("received non-200 response: %d", res.StatusCode) + } + + responseData, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + var zkProof EspressoOnchainProof + err = json.Unmarshal(responseData, &zkProof) + if err != nil { + return nil, err + } + + return &zkProof, nil +} + +// sendTxWithEspresso uses the txmgr queue to send the given transaction candidate after setting +// its gaslimit. It will block if the txmgr queue has reached its MaxPendingTransactions limit. +func (l *BatchSubmitter) sendTxWithEspresso(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { + transactionReference := txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob} + l.Log.Debug("Sending Espresso-enabled L1 transaction", "txRef", transactionReference) + + commitment, err := computeCommitment(candidate) + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: err, + } + return + } + l.Log.Debug("Computed batch commitment", "txRef", transactionReference, "commitment", hexutil.Encode(commitment[:])) + + signature, err := l.signEIP712Commitment(commitment) + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to sign transaction: %w", err), + } + return + } + + l.Log.Debug("Signed transaction", "txRef", transactionReference, "commitment", hexutil.Encode(commitment[:]), "sig", hexutil.Encode(signature)) + + batchAuthenticatorAbi, err := bindings.BatchAuthenticatorMetaData.GetAbi() + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to get batch authenticator ABI: %w", err), + } + return + } + + authenticateBatchCalldata, err := batchAuthenticatorAbi.Pack("authenticateBatchInfo", commitment, signature) + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to pack authenticateBatch calldata: %w", err), + } + return + } + + verifyCandidate := txmgr.TxCandidate{ + TxData: authenticateBatchCalldata, + To: &l.RollupConfig.BatchAuthenticatorAddress, + } + + l.Log.Debug( + "Sending authenticateBatch transaction", + "txRef", transactionReference, + "commitment", hexutil.Encode(commitment[:]), + "sig", hexutil.Encode(signature), + "address", l.RollupConfig.BatchAuthenticatorAddress.String(), + ) + verificationReceipt, err := l.Txmgr.Send(l.killCtx, verifyCandidate) + if err != nil { + l.Log.Error("Failed to send authenticateBatch transaction", "txRef", transactionReference, "err", err) + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to send authenticateBatch transaction: %w", err), + } + return + } + + receipt, err := l.Txmgr.Send(l.killCtx, *candidate) + if err != nil { + l.Log.Error("Failed to send batch inbox transaction", "txRef", transactionReference, "err", err) + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to send batch inbox transaction: %w", err), + } + return + } + + distance := new(big.Int).Sub(receipt.BlockNumber, verificationReceipt.BlockNumber) + lookbackWindow := new(big.Int).SetUint64(derive.BatchAuthLookbackWindow) + if distance.Sign() < 0 || distance.Cmp(lookbackWindow) >= 0 { + l.Log.Error("authenticateBatch transaction too far from batch inbox transaction", "txRef", transactionReference, "distance", distance) + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("authenticateBatch transaction too far from batch inbox transaction: %s", distance), + } + return + } + + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Receipt: receipt, + Err: nil, + } +} + +// signEIP712Commitment creates an EIP-712 signature for the given commitment using the batcher's private key. +func (l *BatchSubmitter) signEIP712Commitment(commitment [32]byte) ([]byte, error) { + typedData := apitypes.TypedData{ + Types: apitypes.Types{ + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, + "EspressoTEEVerifier": []apitypes.Type{ + {Name: "commitment", Type: "bytes32"}, + }, + }, + PrimaryType: "EspressoTEEVerifier", + Domain: apitypes.TypedDataDomain{ + Name: "EspressoTEEVerifier", + Version: "1", + ChainId: (*math.HexOrDecimal256)(l.RollupConfig.L1ChainID), + VerifyingContract: l.teeVerifierAddress.String(), + }, + Message: map[string]interface{}{ + "commitment": commitment, + }, + } + // Calculate the hash using go-ethereum's EIP-712 implementation + hash, _, err := apitypes.TypedDataAndHash(typedData) + if err != nil { + return nil, fmt.Errorf("failed to calculate EIP-712 hash: %w", err) + } + + signature, err := crypto.Sign(hash, l.Config.Espresso.BatcherPrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to sign EIP-712 hash: %w", err) + } + + // Normalize the recovery ID (v) from 0/1 to 27/28 for Solidity's ECDSA.recover + // See: https://github.com/ethereum/go-ethereum/issues/19751#issuecomment-504900739 + if signature[64] < 27 { + signature[64] += 27 + } + return signature, nil +} + +func stripHexPrefix(hexStr string) string { + if len(hexStr) >= 2 && hexStr[:2] == "0x" { + return hexStr[2:] + } + return hexStr +} diff --git a/op-batcher/batcher/espresso_active.go b/op-batcher/batcher/espresso_active.go new file mode 100644 index 00000000000..e9c83a7b228 --- /dev/null +++ b/op-batcher/batcher/espresso_active.go @@ -0,0 +1,61 @@ +package batcher + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + + "github.com/ethereum-optimism/optimism/espresso/bindings" +) + +// isBatcherActive checks if the current batcher is the active one by querying +// the BatchAuthenticator contract. Returns true if this batcher instance should +// be publishing batches, false if it should stay idle. +// +// The active batcher is determined by the contract's activeIsEspresso flag: +// - If activeIsEspresso is true, the Espresso batcher address is active +// - If activeIsEspresso is false, the fallback batcher address is active +// +// This method compares the batcher's own role (Config.Espresso.Enabled) +// against the contract's activeIsEspresso flag. +func (l *BatchSubmitter) isBatcherActive(ctx context.Context) (bool, error) { + // Check if contract code exists at the address + code, err := l.L1Client.CodeAt(ctx, l.RollupConfig.BatchAuthenticatorAddress, nil) + if err != nil { + return false, fmt.Errorf("failed to check code at BatchAuthenticator address: %w", err) + } + if len(code) == 0 { + return false, fmt.Errorf("no contract code at BatchAuthenticator address %s", l.RollupConfig.BatchAuthenticatorAddress.Hex()) + } + + batchAuthenticator, err := bindings.NewBatchAuthenticator(l.RollupConfig.BatchAuthenticatorAddress, l.L1Client) + if err != nil { + return false, fmt.Errorf("failed to create BatchAuthenticator binding: %w", err) + } + + cCtx, cancel := context.WithTimeout(ctx, l.Config.NetworkTimeout) + defer cancel() + + callOpts := &bind.CallOpts{Context: cCtx} + + activeIsEspresso, err := batchAuthenticator.ActiveIsEspresso(callOpts) + if err != nil { + return false, fmt.Errorf("failed to check activeIsEspresso: %w", err) + } + + batcherAddr := l.Txmgr.From() + + isActive := (activeIsEspresso && l.Config.Espresso.Enabled) || + (!activeIsEspresso && !l.Config.Espresso.Enabled) + + if !isActive { + l.Log.Info("Batcher is not the active batcher, skipping publish", + "batcherAddr", batcherAddr, + "activeIsEspresso", activeIsEspresso, + "EspressoEnabled", l.Config.Espresso.Enabled, + ) + } + + return isActive, nil +} diff --git a/op-batcher/batcher/espresso_driver.go b/op-batcher/batcher/espresso_driver.go new file mode 100644 index 00000000000..c1571e4d0aa --- /dev/null +++ b/op-batcher/batcher/espresso_driver.go @@ -0,0 +1,139 @@ +package batcher + +import ( + "context" + "fmt" + "math/big" + + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" + op "github.com/EspressoSystems/espresso-streamers/op" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum-optimism/optimism/espresso" + "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" + "github.com/ethereum-optimism/optimism/op-service/txmgr" +) + +// EspressoDriverSetup groups all TEE-batcher-specific runtime state plumbed +// from BatcherService into DriverSetup. Defined here to keep the upstream +// Optimism DriverSetup field block compact (see driver.go). +// +// All fields are nil/zero when --espresso.enabled is false except for the +// fallback batcher's ChainSigner/SequencerAddress, which are always populated +// by applyEspressoDriverSetup. +type EspressoDriverSetup struct { + Client *espressoClient.MultipleNodesClient + LightClient *espressoLightClient.LightclientCaller + ChainSigner opcrypto.ChainSigner + SequencerAddress common.Address + Attestation []byte +} + +// batcherL1Adapter wraps the batcher's L1Client to implement espresso.L1Client +// (HeaderHashByNumber + bind.ContractCaller). +type batcherL1Adapter struct { + L1Client L1Client +} + +func (a *batcherL1Adapter) HeaderHashByNumber(ctx context.Context, number *big.Int) (common.Hash, error) { + h, err := a.L1Client.HeaderByNumber(ctx, number) + if err != nil { + return common.Hash{}, err + } + return h.Hash(), nil +} + +func (a *batcherL1Adapter) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { + return a.L1Client.CodeAt(ctx, contract, blockNumber) +} + +func (a *batcherL1Adapter) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + return a.L1Client.CallContract(ctx, call, blockNumber) +} + +// EspressoStreamer returns the Espresso batch streamer for use by the service and tests. +func (l *BatchSubmitter) EspressoStreamer() espresso.EspressoStreamer[derive.EspressoBatch] { + return l.espressoStreamer +} + +// setupEspressoStreamer constructs the Espresso streamer (and its buffered +// wrapper) for a freshly-built BatchSubmitter. Called from NewBatchSubmitter +// only when --espresso.enabled is set; no-op otherwise. Panics on streamer +// construction failure to mirror the existing NewBatchSubmitter behavior. +func (l *BatchSubmitter) setupEspressoStreamer() { + if !l.Config.Espresso.Enabled { + return + } + l1Adapter := &batcherL1Adapter{L1Client: l.L1Client} + // Convert typed nil pointer to untyped nil interface to avoid typed-nil interface panic + // in confirmEspressoBlockHeight when EspressoLightClient is not configured. + var lightClientIface op.LightClientCallerInterface + if l.Espresso.LightClient != nil { + lightClientIface = l.Espresso.LightClient + } + unbufferedStreamer, err := op.NewEspressoStreamer( + l.RollupConfig.L2ChainID.Uint64(), + l1Adapter, + l1Adapter, + l.Espresso.Client, + lightClientIface, + l.Log, + derive.CreateEspressoBatchUnmarshaler(), + l.Config.Espresso.CaffeinationHeightEspresso, + l.Config.Espresso.CaffeinationHeightL2, + l.RollupConfig.BatchAuthenticatorAddress, + false, + ) + if err != nil { + panic(fmt.Sprintf("failed to create Espresso streamer: %v", err)) + } + l.espressoStreamer = op.NewBufferedEspressoStreamer(unbufferedStreamer) + l.Log.Info("Streamer started", "streamer", l.espressoStreamer) +} + +// startEspressoLoops registers the batcher with the BatchAuthenticator +// contract, resolves the TEE verifier address, spawns the Espresso transaction +// submitter, and starts the four Espresso-specific batcher goroutines (in +// addition to the upstream receiptsLoop and publishingLoop). Replaces the +// upstream three-goroutine pattern when --espresso.enabled is set. +func (l *BatchSubmitter) startEspressoLoops(receiptsCh chan txmgr.TxReceipt[txRef], publishSignal chan pubInfo) error { + if err := l.registerBatcher(l.killCtx); err != nil { + return fmt.Errorf("could not register with BatchAuthenticator contract: %w", err) + } + + // Resolve the TEE verifier address from the BatchAuthenticator contract. + if err := l.resolveTEEVerifierAddress(); err != nil { + return fmt.Errorf("could not resolve TEE verifier address: %w", err) + } + + l.espressoSubmitter = NewEspressoTransactionSubmitter( + WithContext(l.shutdownCtx), + WithWaitGroup(l.wg), + WithEspressoClient(l.Espresso.Client), + WithVerifyReceiptMaxBlocks(l.Config.Espresso.VerifyReceiptMaxBlocks), + WithVerifyReceiptSafetyTimeout(l.Config.Espresso.VerifyReceiptSafetyTimeout), + WithVerifyReceiptRetryDelay(l.Config.Espresso.VerifyReceiptRetryDelay), + ) + l.espressoSubmitter.SpawnWorkers(4, 4) + l.espressoSubmitter.Start() + + l.wg.Add(4) + go l.receiptsLoop(l.wg, receiptsCh) // ranges over receiptsCh channel + go l.espressoBatchQueueingLoop(l.shutdownCtx, l.wg) + go l.espressoBatchLoadingLoop(l.shutdownCtx, l.wg, publishSignal) + go l.publishingLoop(l.killCtx, l.wg, receiptsCh, publishSignal) // ranges over publishSignal, spawns routines which send on receiptsCh. Closes receiptsCh when done. + return nil +} + +// resetEspressoStreamer resets the Espresso streamer when --espresso.enabled +// is set; no-op otherwise. Called from clearState alongside the upstream +// channel-manager reset so the streamer's view of "next batch" matches the +// freshly-cleared channel state. +func (l *BatchSubmitter) resetEspressoStreamer() { + if l.Config.Espresso.Enabled { + l.EspressoStreamer().Reset() + } +} diff --git a/op-batcher/batcher/espresso_fallback_gate.go b/op-batcher/batcher/espresso_fallback_gate.go index 1157950bca5..d4e7b7cad85 100644 --- a/op-batcher/batcher/espresso_fallback_gate.go +++ b/op-batcher/batcher/espresso_fallback_gate.go @@ -9,19 +9,30 @@ import ( "github.com/ethereum-optimism/optimism/op-service/txmgr" ) -// dispatchAuthenticatedSendTx routes sendTx through the fallback-batcher -// post-fork auth path, returning true when the tx has been handed off to -// authGroup. Returns false to mean "fall through to the upstream queue.Send -// path" — pre-fork operation and any cancel tx. +// dispatchAuthenticatedSendTx routes sendTx through the Espresso (TEE) auth +// path or the fallback-batcher post-fork auth path, returning true when the tx +// has been handed off to authGroup. Returns false to mean "fall through to +// the upstream queue.Send path" — pre-fork fallback batcher and any cancel tx. // -// The fallback batcher consults isFallbackAuthRequired to gate authentication +// The TEE batcher (Config.Espresso.Enabled == true) always authenticates. The +// fallback batcher consults isFallbackAuthRequired to gate authentication // behind the EspressoTime hardfork: pre-fork the verifier accepts plain // sender-authenticated batches, and the BatchAuthenticator contract is -// irrelevant. +// irrelevant; calling authenticateBatchInfo pre-fork would also revert against +// the default activeIsEspresso=true contract state. func (l *BatchSubmitter) dispatchAuthenticatedSendTx(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) bool { if isCancel { return false } + // Espresso batcher: authenticate via BatchAuthenticator. + if l.Config.Espresso.Enabled { + l.authGroup.Add(1) + go func() { + defer l.authGroup.Done() + l.sendTxWithEspresso(txdata, isCancel, candidate, queue, receiptsCh) + }() + return true + } fallbackAuthRequired, err := l.isFallbackAuthRequired(l.killCtx) if err != nil { receiptsCh <- txmgr.TxReceipt[txRef]{ @@ -58,8 +69,8 @@ func (l *BatchSubmitter) isFallbackAuthRequired(ctx context.Context) (bool, erro return l.RollupConfig.IsEspresso(tip.Time), nil } -// waitForAuthGroup blocks until all in-flight fallback-auth watcher goroutines -// have finished. publishingLoop calls it before closing receiptsCh: each watcher +// waitForAuthGroup blocks until all in-flight auth goroutines (fallback watchers +// or TEE submissions) have finished. publishingLoop calls it before closing receiptsCh: each watcher // is a sender on receiptsCh, so the receipts loop must still be draining // receiptsCh at this point or a watcher's final send would block forever. Each // watcher always terminates because the txmgr Queue emits exactly one receipt per diff --git a/op-batcher/batcher/espresso_fallback_gate_test.go b/op-batcher/batcher/espresso_fallback_gate_test.go index fc9453e42bd..e7234507eec 100644 --- a/op-batcher/batcher/espresso_fallback_gate_test.go +++ b/op-batcher/batcher/espresso_fallback_gate_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" @@ -16,7 +17,11 @@ import ( "github.com/ethereum-optimism/optimism/op-service/testlog" ) +// mockFixedTimeL1Client is an L1Client whose tip header carries a fixed +// timestamp. The embedded bind.ContractBackend satisfies the rest of the +// L1Client interface; only HeaderByNumber is exercised by the gate. type mockFixedTimeL1Client struct { + bind.ContractBackend time uint64 } diff --git a/op-batcher/batcher/espresso_helpers_test.go b/op-batcher/batcher/espresso_helpers_test.go new file mode 100644 index 00000000000..31118a3f80f --- /dev/null +++ b/op-batcher/batcher/espresso_helpers_test.go @@ -0,0 +1,263 @@ +package batcher_test + +import ( + "context" + "encoding/binary" + "encoding/json" + "errors" + "sync" + + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + tagged_base64 "github.com/EspressoSystems/espresso-network/sdks/go/tagged-base64" + "github.com/EspressoSystems/espresso-network/sdks/go/types" + common "github.com/EspressoSystems/espresso-network/sdks/go/types/common" +) + +// ErrNotImplemented is a sentinel error used to indicate that a method +// was not implemented. +var ErrNotImplemented = errors.New("not implemented") + +// AlwaysFailingEspressoClient is a mock implementation of the EspressoClient +// interface that always returns an error for every method call. This is +// useful for testing error handling in the batcher without relying on a +// real Espresso client. +type AlwaysFailingEspressoClient struct{} + +// Compile time check to ensure adherence to EspressoClient interface +var _ espressoClient.EspressoClient = (*AlwaysFailingEspressoClient)(nil) + +func (*AlwaysFailingEspressoClient) FetchLatestBlockHeight(ctx context.Context) (uint64, error) { + return 0, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchHeaderByHeight(ctx context.Context, height uint64) (types.HeaderImpl, error) { + return types.HeaderImpl{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchRawHeaderByHeight(ctx context.Context, height uint64) (json.RawMessage, error) { + return json.RawMessage{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchHeadersByRange(ctx context.Context, from uint64, until uint64) ([]types.HeaderImpl, error) { + return []types.HeaderImpl{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchTransactionsInBlock(ctx context.Context, blockHeight uint64, namespace uint64) (espressoClient.TransactionsInBlock, error) { + return espressoClient.TransactionsInBlock{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchTransactionByHash(ctx context.Context, hash *types.TaggedBase64) (types.TransactionQueryData, error) { + return types.TransactionQueryData{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchVidCommonByHeight(ctx context.Context, blockHeight uint64) (types.VidCommon, error) { + return types.VidCommon{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchExplorerTransactionByHash(ctx context.Context, hash *types.TaggedBase64) (types.ExplorerTransactionQueryData, error) { + return types.ExplorerTransactionQueryData{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) StreamPayloads(ctx context.Context, height uint64) (espressoClient.Stream[types.PayloadQueryData], error) { + return nil, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) StreamTransactions(ctx context.Context, height uint64) (espressoClient.Stream[types.TransactionQueryData], error) { + return nil, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) StreamTransactionsInNamespace(ctx context.Context, height uint64, namespace uint64) (espressoClient.Stream[types.TransactionQueryData], error) { + return nil, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchNamespaceTransactionsInRange(ctx context.Context, fromHeight uint64, toHeight uint64, namespace uint64) ([]types.NamespaceTransactionsRangeData, error) { + return []types.NamespaceTransactionsRangeData{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) SubmitTransaction(ctx context.Context, tx common.Transaction) (*common.TaggedBase64, error) { + return nil, ErrNotImplemented +} + +// EspressoClientSwappableImplementation is as implementation of EspressoClient +// that is just a proxy. +// +// This allows it to be created and swapped easily as needed for testing. +type EspressoClientSwappableImplementation struct { + sync.RWMutex + espClient espressoClient.EspressoClient +} + +// Compile time check to ensure adherence to EspressoClient interface +var _ espressoClient.EspressoClient = (*EspressoClientSwappableImplementation)(nil) + +func (c *EspressoClientSwappableImplementation) SetEspressoClient(client espressoClient.EspressoClient) { + c.Lock() + defer c.Unlock() + + c.espClient = client +} + +func (c *EspressoClientSwappableImplementation) FetchLatestBlockHeight(ctx context.Context) (uint64, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchLatestBlockHeight(ctx) +} + +func (c *EspressoClientSwappableImplementation) FetchHeaderByHeight(ctx context.Context, height uint64) (types.HeaderImpl, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchHeaderByHeight(ctx, height) +} + +func (c *EspressoClientSwappableImplementation) FetchRawHeaderByHeight(ctx context.Context, height uint64) (json.RawMessage, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchRawHeaderByHeight(ctx, height) +} + +func (c *EspressoClientSwappableImplementation) FetchHeadersByRange(ctx context.Context, from uint64, until uint64) ([]types.HeaderImpl, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchHeadersByRange(ctx, from, until) +} + +func (c *EspressoClientSwappableImplementation) FetchTransactionsInBlock(ctx context.Context, blockHeight uint64, namespace uint64) (espressoClient.TransactionsInBlock, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchTransactionsInBlock(ctx, blockHeight, namespace) +} + +func (c *EspressoClientSwappableImplementation) FetchTransactionByHash(ctx context.Context, hash *types.TaggedBase64) (types.TransactionQueryData, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchTransactionByHash(ctx, hash) +} + +func (c *EspressoClientSwappableImplementation) FetchVidCommonByHeight(ctx context.Context, blockHeight uint64) (types.VidCommon, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchVidCommonByHeight(ctx, blockHeight) +} + +func (c *EspressoClientSwappableImplementation) FetchExplorerTransactionByHash(ctx context.Context, hash *types.TaggedBase64) (types.ExplorerTransactionQueryData, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchExplorerTransactionByHash(ctx, hash) +} + +func (c *EspressoClientSwappableImplementation) StreamPayloads(ctx context.Context, height uint64) (espressoClient.Stream[types.PayloadQueryData], error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.StreamPayloads(ctx, height) +} + +func (c *EspressoClientSwappableImplementation) StreamTransactions(ctx context.Context, height uint64) (espressoClient.Stream[types.TransactionQueryData], error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.StreamTransactions(ctx, height) +} + +func (c *EspressoClientSwappableImplementation) StreamTransactionsInNamespace(ctx context.Context, height uint64, namespace uint64) (espressoClient.Stream[types.TransactionQueryData], error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.StreamTransactionsInNamespace(ctx, height, namespace) +} + +func (c *EspressoClientSwappableImplementation) FetchNamespaceTransactionsInRange(ctx context.Context, fromHeight uint64, toHeight uint64, namespace uint64) ([]types.NamespaceTransactionsRangeData, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchNamespaceTransactionsInRange(ctx, fromHeight, toHeight, namespace) +} + +func (c *EspressoClientSwappableImplementation) SubmitTransaction(ctx context.Context, tx common.Transaction) (*common.TaggedBase64, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.SubmitTransaction(ctx, tx) +} + +// FakeSubmissionSucceedingEspressoClient is a mock implementation of the +// EspressoClient for the explicit purposes of submitting transactions, and +// seeing their response. +type FakeSubmissionSucceedingEspressoClient struct { + sync.RWMutex + espressoClient.EspressoClient + txns map[string]common.Transaction +} + +// Compile time check to ensure adherence to EspressoClient interface +var _ espressoClient.EspressoClient = (*FakeSubmissionSucceedingEspressoClient)(nil) + +var ( + ErrNotInitialized = errors.New("not initialized") + ErrHashCannotBeNil = errors.New("hash cannot be nil") + ErrTransactionNotFound = errors.New("transaction not found") +) + +func (c *FakeSubmissionSucceedingEspressoClient) Init() { + c.Lock() + defer c.Unlock() + c.txns = make(map[string]common.Transaction) +} + +// FetchTransactionByHash simulates fetching a transaction by its hash. it +// looks up a transaction for the given hash, and returns the transaction +// if it is found. +func (c *FakeSubmissionSucceedingEspressoClient) FetchTransactionByHash(ctx context.Context, hash *types.TaggedBase64) (types.TransactionQueryData, error) { + c.RLock() + defer c.RUnlock() + if c.txns == nil { + return types.TransactionQueryData{}, ErrNotInitialized + } + + if hash == nil { + return types.TransactionQueryData{}, ErrHashCannotBeNil + } + + txn, found := c.txns[hash.String()] + if !found { + return types.TransactionQueryData{}, ErrTransactionNotFound + } + + // Just to simulate some processing on the transaction + height := binary.LittleEndian.Uint64(txn.Payload) + + return types.TransactionQueryData{ + Transaction: txn, + Hash: hash, + Index: 0, + Proof: json.RawMessage{}, + BlockHash: nil, + BlockHeight: height, + }, nil +} + +// SubmitTransaction simulates a successful transaction submission and stores +// it for future retrieval +func (c *FakeSubmissionSucceedingEspressoClient) SubmitTransaction(ctx context.Context, tx common.Transaction) (*common.TaggedBase64, error) { + c.Lock() + defer c.Unlock() + if c.txns == nil { + return nil, ErrNotInitialized + } + + hash, err := tagged_base64.New("TX", tx.Payload) + if err != nil { + return nil, err + } + + c.txns[hash.String()] = tx + return hash, nil +} diff --git a/op-batcher/batcher/espresso_service.go b/op-batcher/batcher/espresso_service.go new file mode 100644 index 00000000000..4217abd8af9 --- /dev/null +++ b/op-batcher/batcher/espresso_service.go @@ -0,0 +1,191 @@ +package batcher + +import ( + "crypto/ecdsa" + "fmt" + "time" + + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/hf/nitrite" + + "github.com/ethereum-optimism/optimism/espresso" + "github.com/ethereum-optimism/optimism/op-batcher/enclave" + "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" +) + +// EspressoBatcherConfig groups all Espresso-specific configuration the +// batcher consumes at runtime. It is embedded as a single field on +// BatcherConfig (see service.go) to keep the upstream Optimism +// BatcherConfig field block compact and minimize cherry-pick churn. +// +// Fields are populated from CLIConfig.Espresso (and a few RollupConfig +// fallbacks) by initEspresso below; ephemeral key material is generated +// by initKeyPair (TEE) or copied from CLIConfig.Espresso.TestingBatcherPrivateKey +// (devnet/test). +type EspressoBatcherConfig struct { + Enabled bool + PollInterval time.Duration + AttestationService string + CaffeinationHeightEspresso uint64 + // CaffeinationHeightL2 is the L2 batch position at which the Espresso + // streamer should start emitting batches. Operational parameter for + // restarting batchers mid-chain (e.g. after a fallback batcher event). + // When zero, the driver falls back to + // RollupConfig.EspressoOriginBatchPos(). + CaffeinationHeightL2 uint64 + + // Receipt verification tuning for the Espresso transaction submitter. + VerifyReceiptMaxBlocks uint64 + VerifyReceiptSafetyTimeout time.Duration + VerifyReceiptRetryDelay time.Duration + + // BatcherPublicKey/BatcherPrivateKey is the batcher's identity for the + // Espresso authentication path. In TEE deployments the private key is + // generated inside the enclave (initKeyPair) and the public key is + // attested to via Nitro Enclave PCR0; outside TEE (devnet/test), the + // configured TestingBatcherPrivateKey overrides them in initEspresso. + BatcherPublicKey *ecdsa.PublicKey + BatcherPrivateKey *ecdsa.PrivateKey +} + +// EspressoStreamer returns the Espresso batch streamer driven by this batcher. +func (bs *BatcherService) EspressoStreamer() espresso.EspressoStreamer[derive.EspressoBatch] { + return bs.driver.espressoStreamer +} + +// initChainSigner asserts that the configured TxManager implements the +// ChainSigner interface and stores the embedded ChainSigner on the service. +// Espresso uses ChainSigner to sign batch authentication payloads sent to the +// BatchAuthenticator contract; the cast is required by every Espresso path. +func (bs *BatcherService) initChainSigner() error { + cast, castOk := bs.TxManager.(opcrypto.ChainSigner) + if !castOk { + return fmt.Errorf("tx manager does not implement ChainSigner") + } + bs.ChainSigner = cast + return nil +} + +// applyEspressoDriverSetup writes the Espresso-specific fields onto a +// DriverSetup populated with upstream-Optimism fields. Kept separate from the +// main initDriver struct literal so that the upstream block stays in upstream +// shape — minimizing cherry-pick churn when upstream renames or reorders +// fields. +func (bs *BatcherService) applyEspressoDriverSetup(ds *DriverSetup) { + ds.Espresso.SequencerAddress = bs.TxManager.From() + ds.Espresso.ChainSigner = bs.ChainSigner + ds.Espresso.Client = bs.EspressoClient + ds.Espresso.LightClient = bs.EspressoLightClient + ds.Espresso.Attestation = bs.Attestation +} + +// initKeyPair generates an ephemeral ECDSA key pair for the batcher's +// Espresso authentication path. In TEE deployments this key is attested +// to via Nitro Enclave PCR0; outside TEE (devnet/test), the configured +// TestingBatcherPrivateKey overrides this key in initEspresso. +func (bs *BatcherService) initKeyPair() error { + key, err := crypto.GenerateKey() + if err != nil { + return fmt.Errorf("failed to generate key pair for batcher: %w", err) + } + bs.Espresso.BatcherPrivateKey = key + bs.Espresso.BatcherPublicKey = &key.PublicKey + return nil +} + +// initEspresso configures the Espresso TEE-batcher integration on the +// BatcherService. When --espresso.enabled is false this is a no-op (the +// fallback batcher gets its own FallbackAuthLeadTime knob from +// BatcherConfig). When enabled, it wires up the Espresso query-service +// client, light client, ephemeral key pair, and Nitro Enclave attestation +// (if running in TEE). +func (bs *BatcherService) initEspresso(cfg *CLIConfig) error { + if !cfg.Espresso.Enabled { + return nil + } + + if cfg.Espresso.RollupL1URL == "" { + cfg.Espresso.RollupL1URL = cfg.L1EthRpc + } + + if cfg.Espresso.RollupL1URL != cfg.L1EthRpc { + log.Warn("Espresso Rollup L1 URL differs from batcher's L1EthRpc") + } + + if cfg.Espresso.L1URL == "" { + log.Warn("Espresso L1 URL not provided, using batcher's L1EthRpc") + cfg.Espresso.L1URL = cfg.L1EthRpc + } + if cfg.Espresso.Namespace == 0 { + log.Info("Using L2 chain ID as namespace by default") + cfg.Espresso.Namespace = bs.RollupConfig.L2ChainID.Uint64() + } + if cfg.Espresso.BatchAuthenticatorAddr == (common.Address{}) { + cfg.Espresso.BatchAuthenticatorAddr = bs.RollupConfig.BatchAuthenticatorAddress + } + + if err := cfg.Espresso.Check(); err != nil { + return fmt.Errorf("invalid Espresso config: %w", err) + } + + bs.Espresso.Enabled = true + bs.Espresso.PollInterval = cfg.Espresso.PollInterval + bs.Espresso.AttestationService = cfg.Espresso.EspressoAttestationService + bs.Espresso.CaffeinationHeightEspresso = cfg.Espresso.CaffeinationHeightEspresso + bs.Espresso.CaffeinationHeightL2 = cfg.Espresso.CaffeinationHeightL2 + bs.Espresso.VerifyReceiptMaxBlocks = cfg.Espresso.VerifyReceiptMaxBlocks + bs.Espresso.VerifyReceiptSafetyTimeout = cfg.Espresso.VerifyReceiptSafetyTimeout + bs.Espresso.VerifyReceiptRetryDelay = cfg.Espresso.VerifyReceiptRetryDelay + + client, err := espressoClient.NewMultipleNodesClient(cfg.Espresso.QueryServiceURLs) + if err != nil { + return fmt.Errorf("failed to create Espresso client: %w", err) + } + bs.EspressoClient = client + + lightClient, err := espressoLightClient.NewLightclientCaller(cfg.Espresso.LightClientAddr, bs.L1Client) + if err != nil { + return fmt.Errorf("failed to create Espresso light client: %w", err) + } + bs.EspressoLightClient = lightClient + + if err := bs.initKeyPair(); err != nil { + return fmt.Errorf("failed to create key pair for batcher: %w", err) + } + + // try to generate attestationBytes on public key when start batcher + attestationBytes, err := enclave.AttestationWithPublicKey(bs.Espresso.BatcherPublicKey) + if err != nil { + bs.Log.Info("Not running in enclave, skipping attestation", "info", err) + + // Replace ephemeral keys with configured keys, as in devnet they'll be pre-approved for batching + privateKey := cfg.Espresso.TestingBatcherPrivateKey + if privateKey == nil { + return fmt.Errorf("when not running in enclave, testing batcher private key should be set") + } + + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return fmt.Errorf("error casting public key to ECDSA") + } + + bs.Espresso.BatcherPrivateKey = privateKey + bs.Espresso.BatcherPublicKey = publicKeyECDSA + } else { + // output length of attestation + bs.Log.Info("Successfully got attestation. Attestation length", "length", len(attestationBytes)) + _, err := nitrite.Verify(attestationBytes, nitrite.VerifyOptions{}) + if err != nil { + return fmt.Errorf("Couldn't verify attestation: %w", err) + } + bs.Attestation = attestationBytes + } + + return nil +} diff --git a/op-batcher/batcher/espresso_transaction_submitter_test.go b/op-batcher/batcher/espresso_transaction_submitter_test.go new file mode 100644 index 00000000000..6c736bdbe3b --- /dev/null +++ b/op-batcher/batcher/espresso_transaction_submitter_test.go @@ -0,0 +1,177 @@ +package batcher_test + +import ( + "context" + "encoding/binary" + "testing" + "time" + + espressoCommon "github.com/EspressoSystems/espresso-network/sdks/go/types" + "github.com/ethereum-optimism/optimism/op-batcher/batcher" + "github.com/stretchr/testify/require" +) + +const NUMBER_OF_TRANSACTIONS_TO_SUBMIT = 1024 * 4 + +// TestEspressoTransactionSubmitterDeadlock is a test that is meant to test +// the underlying conditions in the espressoTransactionSubmitter that may +// result in a deadlock. +// +// The basic idea is to intentionally trigger the circumstances that can +// trigger the deadlock to occur in the espressoTransactionSubmitter. This +// test with this description **SHOULD** remain as a regression test. +// +// The specific suspected criteria for triggering a deadlock in the +// implementation of the espressoTransactionSubmitter as of 2026-04-23 is +// as follows: +// +// - Assume that the Espresso Service is not ever returning successfully +// - Assume we are receiving a consistent stream of of new blocks coming in +// +// If we have both of these criteria, it should be possible to fill up the +// underlying channels of `espressoTransactionSubmitter` for both Submission +// jobs, and verification jobs. +// +// The way we *should* be able to detect the deadlock is if a call to +// SubmitTransaction blocks. +// +// NOTE: After this has been fixed, it is unlikely that this will fail in the +// future. This will primarily be due to `SubmitTransaction` being modified +// to longer explicitly block. +func TestEspressoTransactionSubmitterDeadlock(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + submitter := batcher.NewEspressoTransactionSubmitter( + batcher.WithEspressoClient(new(AlwaysFailingEspressoClient)), + batcher.WithContext(ctx), + ) + + submitter.SpawnWorkers(4, 4) + submitter.Start() + + for i := 0; i < NUMBER_OF_TRANSACTIONS_TO_SUBMIT; i++ { + txn := espressoCommon.Transaction{ + Namespace: 1, + Payload: make([]byte, 8), + } + binary.LittleEndian.PutUint64(txn.Payload, uint64(i)) + + submitCtx, submitCancel := context.WithCancel(context.Background()) + go (func(cancel context.CancelFunc, txn espressoCommon.Transaction) { + submitter.SubmitTransaction(&txn) + cancel() + })(submitCancel, txn) + + timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + + select { + case <-submitCtx.Done(): + // Things progressed without issue. + timeoutCancel() + submitCancel() + continue + case <-timeoutCtx.Done(): + // The call to SubmitTransaction did not return within the expected + // time frame. + + timeoutCancel() + submitCancel() + t.Fatalf("SubmitTransaction has blocked for longer than 200 milliseconds, on transaction %d, for error: %s\n", i, timeoutCtx.Err()) + } + } + + // If we've gotten here, we have passed without deadlocking. +} + +// TestEspressoTransactionSubmitterProgress is a test that is meant to test +// and verify the modified behavior of the espressoTransactionSubmitter after +// the behavior change to explicitly address the potential for the previous +// deadlock condition. +// +// The resolution is to target and limit the number of active inflight requests +// pending to be submitted and verified on Espresso at a given time. We target +// this behavior explicitly by being able to configure a maximum number of +// pending or "inflight" requests that are being waited on. By setting this +// limit, and being able to track things going through the pipeline, we can +// ensure that we continue to make progress, and put back pressure on the +// submitter themselves, so that they don't keep trying to submit new +// transactions if we are unable to effectively handle them at our current +// capacity. +// +// This test ensures that this workflow can be processed by first starting with +// a failing EspressoClient, and once we hit the threshold of having too +// many in flight requests, we swap the Client over to a succeeding client, +// and ensure that we're able to get through our pending backlog, and all new +// transactions to submit without stalling. +func TestEspressoTransactionSubmitterProgress(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + failingClient := new(AlwaysFailingEspressoClient) + succeedingClient := new(FakeSubmissionSucceedingEspressoClient) + succeedingClient.Init() + succeedingClient.EspressoClient = failingClient + espClient := new(EspressoClientSwappableImplementation) + espClient.SetEspressoClient(failingClient) + submitter := batcher.NewEspressoTransactionSubmitter( + batcher.WithEspressoClient(espClient), + batcher.WithContext(ctx), + ) + + submitter.SpawnWorkers(4, 4) + submitter.Start() + + i := 0 + for ; i < NUMBER_OF_TRANSACTIONS_TO_SUBMIT; i++ { + txn := espressoCommon.Transaction{ + Namespace: 1, + Payload: make([]byte, 8), + } + binary.LittleEndian.PutUint64(txn.Payload, uint64(i)) + err := submitter.SubmitTransaction(&txn) + + if err == nil { + continue + } + + // We've triggered the initial condition, and we're no longer + if _, ok := err.(batcher.ErrTooManyInFlightRequests); ok { + // able to submit new transactions to the queue, as we've filled + // the in flight capacity. + // NOTE: we decrement `i` here, as we didn't successfully submit it. + i-- + break + } + + require.NoError(t, err, "unexpected error encountered while submit transaction has been called") + } + + // Now we trigger the EspressoClient to start working again. + // This will effectively simulate that the external network has + // no recovered. + + espClient.SetEspressoClient(succeedingClient) + + // We need to wait a little bit to give the submitter time to process + // some of its backlog. + + time.Sleep(10 * time.Millisecond) + + for ; i < NUMBER_OF_TRANSACTIONS_TO_SUBMIT; i++ { + txn := espressoCommon.Transaction{ + Namespace: 1, + Payload: make([]byte, 8), + } + binary.LittleEndian.PutUint64(txn.Payload, uint64(i)) + err := submitter.SubmitTransaction(&txn) + + if _, ok := err.(batcher.ErrTooManyInFlightRequests); ok { + // Slow down a bit, and decrement `i` so we try it again. + i-- + time.Sleep(10 * time.Millisecond) + continue + } + + // No further errors should occur + require.NoError(t, err, "unexpected error encountered while submit transaction has been called") + } +} diff --git a/op-batcher/batcher/service.go b/op-batcher/batcher/service.go index 4c8892c6c74..bef9eef56d6 100644 --- a/op-batcher/batcher/service.go +++ b/op-batcher/batcher/service.go @@ -8,6 +8,8 @@ import ( "sync/atomic" "time" + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" @@ -22,6 +24,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-service/cliapp" + opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" "github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/httputil" @@ -52,6 +55,11 @@ type BatcherConfig struct { // For throttling DA. See CLIConfig in config.go for details on these parameters. ThrottleParams config.ThrottleParams + + // Espresso groups all TEE-batcher-specific configuration. Defined in + // espresso_service.go to keep the upstream Optimism field block compact. + // Zero-valued when --espresso.enabled=false. + Espresso EspressoBatcherConfig } // BatcherService represents a full batch-submitter instance and its resources, @@ -82,6 +90,14 @@ type BatcherService struct { stopped atomic.Bool NotSubmittingOnStart bool + + // Espresso runtime state. Defined in espresso_service.go to keep the + // upstream Optimism field block compact. EspressoClient and + // EspressoLightClient are nil when --espresso.enabled=false. + EspressoClient *espressoClient.MultipleNodesClient + EspressoLightClient *espressoLightClient.LightclientCaller + opcrypto.ChainSigner + Attestation []byte } type DriverSetupOption func(setup *DriverSetup) @@ -191,6 +207,9 @@ func (bs *BatcherService) initFromCLIConfig(ctx context.Context, closeApp contex if err := bs.initPProf(cfg); err != nil { return fmt.Errorf("failed to init profiling: %w", err) } + if err := bs.initEspresso(cfg); err != nil { + return fmt.Errorf("failed to init Espresso: %w", err) + } bs.initDriver(opts...) if err := bs.initRPCServer(cfg); err != nil { return fmt.Errorf("failed to start RPC server: %w", err) @@ -394,6 +413,9 @@ func (bs *BatcherService) initTxManager(_ context.Context, cfg *CLIConfig) error return err } bs.TxManager = txManager + if err := bs.initChainSigner(); err != nil { + return err + } return nil } @@ -446,6 +468,7 @@ func (bs *BatcherService) initDriver(opts ...DriverSetupOption) { ChannelConfig: bs.ChannelConfig, AltDA: bs.AltDA, } + bs.applyEspressoDriverSetup(&ds) for _, opt := range opts { opt(&ds) } diff --git a/op-batcher/flags/flags.go b/op-batcher/flags/flags.go index f17faf14c52..15e75126a04 100644 --- a/op-batcher/flags/flags.go +++ b/op-batcher/flags/flags.go @@ -8,6 +8,7 @@ import ( "github.com/urfave/cli/v2" + "github.com/ethereum-optimism/optimism/espresso" altda "github.com/ethereum-optimism/optimism/op-alt-da" "github.com/ethereum-optimism/optimism/op-batcher/compressor" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" @@ -198,6 +199,7 @@ func init() { optionalFlags = append(optionalFlags, oppprof.CLIFlags(EnvVarPrefix)...) optionalFlags = append(optionalFlags, txmgr.CLIFlagsWithBTO(EnvVarPrefix)...) optionalFlags = append(optionalFlags, altda.CLIFlags(EnvVarPrefix, "")...) + optionalFlags = append(optionalFlags, espresso.CLIFlags(EnvVarPrefix, "")...) Flags = append(requiredFlags, optionalFlags...) } diff --git a/op-service/log/repeat_state.go b/op-service/log/repeat_state.go new file mode 100644 index 00000000000..2147a05fec6 --- /dev/null +++ b/op-service/log/repeat_state.go @@ -0,0 +1,91 @@ +package log + +import ( + "sync" + "time" + + "github.com/ethereum/go-ethereum/log" +) + +// RepeatStateReminderInterval is how often a long-running degraded state +// re-emits a Warn so operators don't lose visibility while the state persists. +const RepeatStateReminderInterval = 5 * time.Minute + +// RepeatStateLogger collapses warnings tied to a "degraded state" into a single +// log on entry, periodic reminders while the state persists, and a recovery +// log on exit. This avoids flooding the log debouncer when a tick-driven loop +// fires the same warning every poll interval. +// +// State is keyed by a free-form string supplied by the caller; entries with +// different keys are independent. Safe for concurrent use. +// +// Unlike DebouncingHandler (which operates at the slog.Handler level on a +// short time window), RepeatStateLogger is caller-driven: the caller signals +// state recovery explicitly via Clear, which lets it emit a recovery log a +// handler-level facility can't produce. +type RepeatStateLogger struct { + mu sync.Mutex + states map[string]*repeatStateEntry + clock func() time.Time +} + +type repeatStateEntry struct { + firstSeen time.Time + lastLogged time.Time + occurrences int +} + +func NewRepeatStateLogger() *RepeatStateLogger { + return &RepeatStateLogger{ + states: make(map[string]*repeatStateEntry), + clock: time.Now, + } +} + +// Warn reports an observation of a degraded state. The first observation since +// the most recent Clear (or first ever for the key) emits at warn level. +// Subsequent observations within RepeatStateReminderInterval are silently +// counted; once the interval has elapsed a single reminder warn is emitted +// with the cumulative occurrence count and the total duration since the state +// became active. +func (r *RepeatStateLogger) Warn(l log.Logger, key, msg string, ctx ...any) { + now := r.clock() + r.mu.Lock() + e, active := r.states[key] + if !active { + r.states[key] = &repeatStateEntry{firstSeen: now, lastLogged: now, occurrences: 1} + r.mu.Unlock() + l.Warn(msg, ctx...) + return + } + e.occurrences++ + if now.Sub(e.lastLogged) < RepeatStateReminderInterval { + r.mu.Unlock() + return + } + occurrences := e.occurrences + duration := now.Sub(e.firstSeen).Round(time.Second) + e.lastLogged = now + r.mu.Unlock() + + l.Warn(msg, append([]any{"occurrences", occurrences, "duration", duration}, ctx...)...) +} + +// Clear marks the named state as resolved. If the state was active a single +// info-level recovery log is emitted summarising the duration and total +// occurrences. Calling Clear when the state is not active is a no-op, so it is +// safe to call on every successful tick of the loop. +func (r *RepeatStateLogger) Clear(l log.Logger, key, recoveryMsg string, ctx ...any) { + r.mu.Lock() + e, active := r.states[key] + delete(r.states, key) + r.mu.Unlock() + if !active { + return + } + args := append([]any{ + "duration", r.clock().Sub(e.firstSeen).Round(time.Second), + "occurrences", e.occurrences, + }, ctx...) + l.Info(recoveryMsg, args...) +} diff --git a/op-service/log/repeat_state_test.go b/op-service/log/repeat_state_test.go new file mode 100644 index 00000000000..1d74dc2a7be --- /dev/null +++ b/op-service/log/repeat_state_test.go @@ -0,0 +1,234 @@ +package log + +import ( + "context" + "log/slog" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/log" +) + +// safeTestRecorder is a thread-safe slog.Handler that captures records for +// assertions in concurrent tests. +type safeTestRecorder struct { + mu sync.Mutex + records []slog.Record +} + +func (r *safeTestRecorder) Enabled(context.Context, slog.Level) bool { return true } + +func (r *safeTestRecorder) Handle(_ context.Context, rec slog.Record) error { + r.mu.Lock() + defer r.mu.Unlock() + r.records = append(r.records, rec) + return nil +} + +func (r *safeTestRecorder) WithAttrs([]slog.Attr) slog.Handler { return r } +func (r *safeTestRecorder) WithGroup(string) slog.Handler { return r } + +func (r *safeTestRecorder) GetRecords() []slog.Record { + r.mu.Lock() + defer r.mu.Unlock() + result := make([]slog.Record, len(r.records)) + copy(result, r.records) + return result +} + +func (r *safeTestRecorder) Len() int { + r.mu.Lock() + defer r.mu.Unlock() + return len(r.records) +} + +// fakeClock returns the time stored at *t, allowing the test to advance time +// deterministically without sleeping. +func fakeClock(now *time.Time) func() time.Time { + return func() time.Time { return *now } +} + +// matchingRecords returns records whose level and message match the given +// level and msg. Pass an empty msg to match any message. +func matchingRecords(records []slog.Record, level slog.Level, msg string) []slog.Record { + var out []slog.Record + for _, r := range records { + if r.Level != level { + continue + } + if msg != "" && r.Message != msg { + continue + } + out = append(out, r) + } + return out +} + +// attrValue extracts the value of the named attribute, or nil if absent. +func attrValue(r slog.Record, key string) any { + var v any + r.Attrs(func(a slog.Attr) bool { + if a.Key == key { + v = a.Value.Any() + return false + } + return true + }) + return v +} + +func newCapturingLogger() (log.Logger, *safeTestRecorder) { + rec := new(safeTestRecorder) + return log.NewLogger(rec), rec +} + +func TestRepeatStateLogger_FirstWarnEmitsThenSuppresses(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + r.Warn(lgr, "k1", "degraded", "err", "boom") + now = now.Add(1 * time.Second) + r.Warn(lgr, "k1", "degraded", "err", "boom") + now = now.Add(1 * time.Second) + r.Warn(lgr, "k1", "degraded", "err", "boom") + + warns := matchingRecords(rec.GetRecords(), slog.LevelWarn, "degraded") + require.Len(t, warns, 1, "only the first observation should emit a log") + + require.Equal(t, "boom", attrValue(warns[0], "err")) + require.Nil(t, attrValue(warns[0], "occurrences"), "first emission should not carry an occurrences count") +} + +func TestRepeatStateLogger_ReminderAfterInterval(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + // Initial emission. + r.Warn(lgr, "k1", "degraded") + + // 9 silent observations, every 30s. Cumulative 4m30s, still under the 5m threshold. + for i := 0; i < 9; i++ { + now = now.Add(30 * time.Second) + r.Warn(lgr, "k1", "degraded") + } + warns := matchingRecords(rec.GetRecords(), slog.LevelWarn, "degraded") + require.Len(t, warns, 1, "no reminder before the interval has elapsed") + + // Cross the threshold. + now = now.Add(31 * time.Second) + r.Warn(lgr, "k1", "degraded") + + warns = matchingRecords(rec.GetRecords(), slog.LevelWarn, "degraded") + require.Len(t, warns, 2, "reminder should fire once the interval has elapsed") + + // Reminder reports cumulative occurrences: 1 initial + 9 silent + 1 reminder = 11. + require.EqualValues(t, int64(11), attrValue(warns[1], "occurrences")) + // Duration since firstSeen is 9*30s + 31s = 5m1s, rounded to nearest second. + require.Equal(t, 5*time.Minute+1*time.Second, attrValue(warns[1], "duration")) +} + +func TestRepeatStateLogger_KeysAreIndependent(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + r.Warn(lgr, "k1", "first state") + r.Warn(lgr, "k2", "second state") + // Both keys are now active. Repeats for either should be suppressed. + r.Warn(lgr, "k1", "first state") + r.Warn(lgr, "k2", "second state") + + require.Len(t, matchingRecords(rec.GetRecords(), slog.LevelWarn, "first state"), 1) + require.Len(t, matchingRecords(rec.GetRecords(), slog.LevelWarn, "second state"), 1) + + // Clearing one key must not affect the other. + r.Clear(lgr, "k1", "first recovered") + r.Warn(lgr, "k2", "second state") // still suppressed + require.Len(t, matchingRecords(rec.GetRecords(), slog.LevelWarn, "second state"), 1) +} + +func TestRepeatStateLogger_ClearEmitsRecoveryWhenActive(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + r.Warn(lgr, "k1", "degraded") + now = now.Add(2 * time.Second) + r.Warn(lgr, "k1", "degraded") // suppressed, but increments totalOccurrences + now = now.Add(3 * time.Second) + r.Clear(lgr, "k1", "recovered", "extra", "ctx") + + infos := matchingRecords(rec.GetRecords(), slog.LevelInfo, "recovered") + require.Len(t, infos, 1) + require.Equal(t, 5*time.Second, attrValue(infos[0], "duration")) + require.EqualValues(t, int64(2), attrValue(infos[0], "occurrences")) + require.Equal(t, "ctx", attrValue(infos[0], "extra")) +} + +func TestRepeatStateLogger_ClearWhenInactiveIsNoop(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + r.Clear(lgr, "k1", "recovered") + + require.Empty(t, matchingRecords(rec.GetRecords(), slog.LevelInfo, "recovered")) +} + +func TestRepeatStateLogger_FreshAfterClear(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + r.Warn(lgr, "k1", "degraded") + r.Warn(lgr, "k1", "degraded") // suppressed + r.Clear(lgr, "k1", "recovered") + + // After Clear, the next Warn should emit again as a fresh first observation. + r.Warn(lgr, "k1", "degraded") + + warns := matchingRecords(rec.GetRecords(), slog.LevelWarn, "degraded") + require.Len(t, warns, 2, "first Warn after Clear should emit") + require.Nil(t, attrValue(warns[1], "occurrences"), "fresh emission should not carry an occurrences count") +} + +func TestRepeatStateLogger_ConcurrentCallersDoNotRace(t *testing.T) { + lgr, _ := newCapturingLogger() + r := NewRepeatStateLogger() + + const goroutines = 32 + const callsPerG = 200 + + var wg sync.WaitGroup + wg.Add(goroutines) + var clears atomic.Int64 + for g := 0; g < goroutines; g++ { + go func(id int) { + defer wg.Done() + key := "k" + string(rune('a'+(id%4))) + for i := 0; i < callsPerG; i++ { + r.Warn(lgr, key, "degraded", "g", id) + if i%50 == 0 { + r.Clear(lgr, key, "recovered") + clears.Add(1) + } + } + }(g) + } + wg.Wait() + // No assertion on counts — this test exists to flush out races under -race + // and to assert the logger does not panic under concurrent Warn/Clear. + require.Greater(t, clears.Load(), int64(0)) +} From 137adc7e239d541f4476fafa7f3b385e4c6e783b Mon Sep 17 00:00:00 2001 From: Artemii Gerasimovich Date: Thu, 18 Jun 2026 16:23:33 +0200 Subject: [PATCH 06/25] op-batcher: route TEE auth through ordered tx queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TEE batcher's Espresso submission path called Txmgr.Send directly for both the authenticateBatchInfo tx and the batch inbox tx, and ran inside authGroup, so it bypassed MaxPendingTransactions, assigned nonces nondeterministically (violating Holocene's in-order L1 inclusion requirement), and never checked whether the auth tx reverted — a reverted authenticateBatchInfo emits no event, so the verifier would silently drop the batch. Submit both txs through the ordered queue.Send path on the publishing-loop goroutine (auth first, batch second) so the auth tx takes the lower nonce and is mined first, and both stay under MaxPendingTransactions. A watcher goroutine (tracked by authGroup) collects both receipts, fails the pair if the auth tx reverted, runs the lookback-window check, and emits a single synthetic receipt. This is the same fix already applied to the fallback path; extract the shared submission + receipt-watching flow into submitAuthenticatedBatch / watchAuthReceipts so both paths reuse it and differ only in how the authenticateBatchInfo calldata is built (TEE-attested signature vs empty signature). Rename fallback_auth.go to espresso_auth.go to reflect the shared scope, and restructure the tests: one suite drives the shared flow directly, plus per-path tests asserting the distinguishing auth calldata (empty sig vs a recoverable EIP-712 signature). Co-authored-by: OpenCode --- op-batcher/batcher/espresso.go | 54 +-- .../{fallback_auth.go => espresso_auth.go} | 76 ++-- ...ack_auth_test.go => espresso_auth_test.go} | 366 ++++++++++++------ op-batcher/batcher/espresso_fallback_gate.go | 10 +- 4 files changed, 303 insertions(+), 203 deletions(-) rename op-batcher/batcher/{fallback_auth.go => espresso_auth.go} (69%) rename op-batcher/batcher/{fallback_auth_test.go => espresso_auth_test.go} (50%) diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go index 5825626bf11..00d19f15526 100644 --- a/op-batcher/batcher/espresso.go +++ b/op-batcher/batcher/espresso.go @@ -1338,10 +1338,14 @@ func (l *BatchSubmitter) GenerateZKProof(ctx context.Context, attestationBytes [ return &zkProof, nil } -// sendTxWithEspresso uses the txmgr queue to send the given transaction candidate after setting -// its gaslimit. It will block if the txmgr queue has reached its MaxPendingTransactions limit. +// sendTxWithEspresso authenticates a batch transaction via the BatchAuthenticator contract using +// a TEE-attested EIP-712 signature, then sends the batch data to the BatchInbox address. Both txs +// are submitted through the ordered txmgr queue (auth first, batch second) so they are mined in +// submission order, as required under Holocene, and both stay under MaxPendingTransactions +// (queue.Send blocks when the queue is full). A watcher goroutine collects both receipts and +// emits a single synthetic receipt for the batch txData. func (l *BatchSubmitter) sendTxWithEspresso(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { - transactionReference := txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob} + transactionReference := txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()} l.Log.Debug("Sending Espresso-enabled L1 transaction", "txRef", transactionReference) commitment, err := computeCommitment(candidate) @@ -1388,49 +1392,7 @@ func (l *BatchSubmitter) sendTxWithEspresso(txdata txData, isCancel bool, candid To: &l.RollupConfig.BatchAuthenticatorAddress, } - l.Log.Debug( - "Sending authenticateBatch transaction", - "txRef", transactionReference, - "commitment", hexutil.Encode(commitment[:]), - "sig", hexutil.Encode(signature), - "address", l.RollupConfig.BatchAuthenticatorAddress.String(), - ) - verificationReceipt, err := l.Txmgr.Send(l.killCtx, verifyCandidate) - if err != nil { - l.Log.Error("Failed to send authenticateBatch transaction", "txRef", transactionReference, "err", err) - receiptsCh <- txmgr.TxReceipt[txRef]{ - ID: transactionReference, - Err: fmt.Errorf("failed to send authenticateBatch transaction: %w", err), - } - return - } - - receipt, err := l.Txmgr.Send(l.killCtx, *candidate) - if err != nil { - l.Log.Error("Failed to send batch inbox transaction", "txRef", transactionReference, "err", err) - receiptsCh <- txmgr.TxReceipt[txRef]{ - ID: transactionReference, - Err: fmt.Errorf("failed to send batch inbox transaction: %w", err), - } - return - } - - distance := new(big.Int).Sub(receipt.BlockNumber, verificationReceipt.BlockNumber) - lookbackWindow := new(big.Int).SetUint64(derive.BatchAuthLookbackWindow) - if distance.Sign() < 0 || distance.Cmp(lookbackWindow) >= 0 { - l.Log.Error("authenticateBatch transaction too far from batch inbox transaction", "txRef", transactionReference, "distance", distance) - receiptsCh <- txmgr.TxReceipt[txRef]{ - ID: transactionReference, - Err: fmt.Errorf("authenticateBatch transaction too far from batch inbox transaction: %s", distance), - } - return - } - - receiptsCh <- txmgr.TxReceipt[txRef]{ - ID: transactionReference, - Receipt: receipt, - Err: nil, - } + l.submitAuthenticatedBatch(transactionReference, verifyCandidate, candidate, queue, receiptsCh) } // signEIP712Commitment creates an EIP-712 signature for the given commitment using the batcher's private key. diff --git a/op-batcher/batcher/fallback_auth.go b/op-batcher/batcher/espresso_auth.go similarity index 69% rename from op-batcher/batcher/fallback_auth.go rename to op-batcher/batcher/espresso_auth.go index 419fb68d36e..d3a24e1439a 100644 --- a/op-batcher/batcher/fallback_auth.go +++ b/op-batcher/batcher/espresso_auth.go @@ -43,13 +43,6 @@ func computeCommitment(candidate *txmgr.TxCandidate) ([32]byte, error) { // separate signature is needed — the L1 transaction is already signed by the TxManager's key. func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { transactionReference := newTxRef(txdata, isCancel) - // The auth tx shares the batch txdata's identity (so a failure requeues the right frames) - // but is always a calldata tx. Auth failures must carry its real type: an ErrAlreadyReserved - // receipt labeled with the batch's blob type would make cancelBlockingTx cancel the wrong - // pool, leaving the reserving tx stuck. - authReference := transactionReference - authReference.isBlob = false - authReference.daType = DaTypeCalldata l.Log.Debug("Sending fallback-authenticated L1 transaction", "txRef", transactionReference) commitment, err := computeCommitment(candidate) @@ -86,16 +79,41 @@ func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, ca To: &l.RollupConfig.BatchAuthenticatorAddress, } + l.submitAuthenticatedBatch(transactionReference, verifyCandidate, candidate, queue, receiptsCh) +} + +// submitAuthenticatedBatch submits an authenticateBatchInfo tx (verifyCandidate) followed by the +// batch inbox tx (candidate), then spawns a watcher (tracked by authGroup so the publishing loop +// drains it before closing receiptsCh) that validates both receipts and emits a single synthetic +// receipt for the batch txData. +// +// Both the TEE and fallback auth paths share this submission flow; they differ only in how the +// authenticateBatchInfo calldata in verifyCandidate is built (TEE-attested signature vs empty +// signature relying on the contract's msg.sender check). +// +// The auth leg must land at or before the batch leg (the verifier scans a lookback window ending +// at the batch tx's L1 block), so the auth tx must take the lower, earlier-mined nonce. Calldata +// pairs are pipelined via queue.SendPair (auth leg first); blob batches fall back to a serialized +// submission because geth's cross-pool account reservation cannot hold a calldata auth tx and a +// blob batch tx at once. +func (l *BatchSubmitter) submitAuthenticatedBatch(transactionReference txRef, verifyCandidate txmgr.TxCandidate, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { + // The auth tx shares the batch txdata's identity (so a failure requeues the right frames) + // but is always a calldata tx. Auth failures must carry its real type: an ErrAlreadyReserved + // receipt labeled with the batch's blob type would make cancelBlockingTx cancel the wrong + // pool, leaving the reserving tx stuck. + authReference := transactionReference + authReference.isBlob = false + authReference.daType = DaTypeCalldata + l.Log.Debug( - "Sending fallback authenticateBatchInfo transaction", + "Sending authenticateBatchInfo transaction", "txRef", transactionReference, - "commitment", hexutil.Encode(commitment[:]), "address", l.RollupConfig.BatchAuthenticatorAddress.String(), ) if len(candidate.Blobs) > 0 { // SendPair doesn't support blobs. - l.sendFallbackAuthSerialized(transactionReference, authReference, verifyCandidate, candidate, queue, receiptsCh) + l.sendAuthSerialized(transactionReference, authReference, verifyCandidate, candidate, queue, receiptsCh) return } @@ -112,19 +130,19 @@ func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, ca l.authGroup.Add(1) go func() { defer l.authGroup.Done() - l.watchFallbackAuthReceipts(transactionReference, authReference, authReceiptCh, batchReceiptCh, receiptsCh) + l.watchAuthReceipts(transactionReference, authReference, authReceiptCh, batchReceiptCh, receiptsCh) }() } -// sendFallbackAuthSerialized submits an auth+batch pair serially: the auth tx is sent +// sendAuthSerialized submits an auth+batch pair serially: the auth tx is sent // and confirmed first, then the batch tx. It runs on the publishing-loop goroutine and // blocks it for the pair's full confirmation cycle, so consecutive pairs never overlap. -func (l *BatchSubmitter) sendFallbackAuthSerialized(transactionReference, authReference txRef, verifyCandidate txmgr.TxCandidate, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { +func (l *BatchSubmitter) sendAuthSerialized(transactionReference, authReference txRef, verifyCandidate txmgr.TxCandidate, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { authReceiptCh := make(chan txmgr.TxReceipt[txRef], 1) queue.Send(authReference, verifyCandidate, authReceiptCh) authResult := <-authReceiptCh - if err := fallbackAuthResultError(authResult); err != nil { - l.Log.Error("Fallback authenticateBatchInfo transaction failed", "txRef", transactionReference, "err", err) + if err := authResultError(authResult); err != nil { + l.Log.Error("authenticateBatchInfo transaction failed", "txRef", transactionReference, "err", err) receiptsCh <- txmgr.TxReceipt[txRef]{ ID: authReference, Err: err, @@ -137,42 +155,42 @@ func (l *BatchSubmitter) sendFallbackAuthSerialized(transactionReference, authRe batchReceiptCh := make(chan txmgr.TxReceipt[txRef], 1) queue.Send(transactionReference, *candidate, batchReceiptCh) batchResult := <-batchReceiptCh - l.resolveFallbackAuthPair(transactionReference, authReference, authResult, batchResult, receiptsCh) + l.resolveAuthPair(transactionReference, authReference, authResult, batchResult, receiptsCh) } -// watchFallbackAuthReceipts collects the auth and batch receipts of a pipelined fallback-auth -// pair and resolves them into a single synthetic receipt (see resolveFallbackAuthPair). -func (l *BatchSubmitter) watchFallbackAuthReceipts(transactionReference, authReference txRef, authReceiptCh, batchReceiptCh chan txmgr.TxReceipt[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { +// watchAuthReceipts collects the auth and batch receipts of a pipelined auth+batch +// pair and resolves them into a single synthetic receipt (see resolveAuthPair). +func (l *BatchSubmitter) watchAuthReceipts(transactionReference, authReference txRef, authReceiptCh, batchReceiptCh chan txmgr.TxReceipt[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { authResult := <-authReceiptCh batchResult := <-batchReceiptCh - l.resolveFallbackAuthPair(transactionReference, authReference, authResult, batchResult, receiptsCh) + l.resolveAuthPair(transactionReference, authReference, authResult, batchResult, receiptsCh) } -// fallbackAuthResultError interprets the auth leg's receipt: a send failure or a +// authResultError interprets the auth leg's receipt: a send failure or a // mined-but-reverted auth tx both fail the pair, since a reverted authenticateBatchInfo call // emits no BatchInfoAuthenticated event and the verifier would drop the batch (txmgr returns a // receipt as soon as the tx is mined, regardless of execution status). The batch inbox tx // needs no such status check: derivation reads its data by L1 inclusion, not by execution // status. -func fallbackAuthResultError(authResult txmgr.TxReceipt[txRef]) error { +func authResultError(authResult txmgr.TxReceipt[txRef]) error { if authResult.Err != nil { - return fmt.Errorf("failed to send fallback authenticateBatchInfo transaction: %w", authResult.Err) + return fmt.Errorf("failed to send authenticateBatchInfo transaction: %w", authResult.Err) } if authResult.Receipt.Status != types.ReceiptStatusSuccessful { - return fmt.Errorf("fallback authenticateBatchInfo transaction reverted: %s", authResult.Receipt.TxHash) + return fmt.Errorf("authenticateBatchInfo transaction reverted: %s", authResult.Receipt.TxHash) } return nil } -// resolveFallbackAuthPair validates the receipts of an auth+batch pair — auth success, batch +// resolveAuthPair validates the receipts of an auth+batch pair — auth success, batch // success, and that the batch tx landed within the lookback window of the auth tx — and // forwards a single synthetic receipt keyed to the batch txData onto receiptsCh. Any failure // produces an error receipt so the channel manager rewinds and resubmits the frame set. Auth -// failures are reported under authReference (see its construction in sendTxWithFallbackAuth +// failures are reported under authReference (see its construction in submitAuthenticatedBatch // for why the calldata typing matters). -func (l *BatchSubmitter) resolveFallbackAuthPair(transactionReference, authReference txRef, authResult, batchResult txmgr.TxReceipt[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { - if err := fallbackAuthResultError(authResult); err != nil { - l.Log.Error("Fallback authenticateBatchInfo transaction failed", "txRef", transactionReference, "err", err) +func (l *BatchSubmitter) resolveAuthPair(transactionReference, authReference txRef, authResult, batchResult txmgr.TxReceipt[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { + if err := authResultError(authResult); err != nil { + l.Log.Error("authenticateBatchInfo transaction failed", "txRef", transactionReference, "err", err) receiptsCh <- txmgr.TxReceipt[txRef]{ ID: authReference, Err: err, diff --git a/op-batcher/batcher/fallback_auth_test.go b/op-batcher/batcher/espresso_auth_test.go similarity index 50% rename from op-batcher/batcher/fallback_auth_test.go rename to op-batcher/batcher/espresso_auth_test.go index 980c910bf0b..e3985bcc182 100644 --- a/op-batcher/batcher/fallback_auth_test.go +++ b/op-batcher/batcher/espresso_auth_test.go @@ -6,10 +6,14 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/signer/core/apitypes" "github.com/stretchr/testify/require" + "github.com/ethereum-optimism/optimism/espresso/bindings" "github.com/ethereum-optimism/optimism/op-batcher/metrics" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" @@ -29,10 +33,9 @@ type recordedSend struct { } // fakeTxSender records queued transactions in order and immediately delivers a -// canned response (by index) on the receipt channel, mimicking the txmgr -// Queue, which forwards exactly one receipt per queued transaction. SendPair -// records its two legs as two consecutive sends consuming two consecutive -// canned responses. +// canned response (by index) on the receipt channel, mimicking the txmgr Queue, +// which forwards exactly one receipt per queued transaction. SendPair records +// its two legs as two consecutive sends consuming two consecutive responses. type fakeTxSender struct { sends []recordedSend pairCalls int @@ -57,6 +60,8 @@ func (f *fakeTxSender) SendPair(firstID txRef, first txmgr.TxCandidate, firstCh f.deliver(secondID, second, secondCh, true) } +// fallbackAuthMetricsSpy counts the auth failure metrics so tests can assert the +// right signal fired for each failure mode. type fallbackAuthMetricsSpy struct { metrics.Metricer windowExceeded int @@ -64,7 +69,7 @@ type fallbackAuthMetricsSpy struct { func (s *fallbackAuthMetricsSpy) RecordFallbackAuthWindowExceeded() { s.windowExceeded++ } -func newFallbackAuthSubmitter(t *testing.T) *BatchSubmitter { +func newAuthSubmitter(t *testing.T) *BatchSubmitter { l := &BatchSubmitter{} l.Log = testlog.Logger(t, log.LevelDebug) l.Metr = metrics.NoopMetrics @@ -74,12 +79,12 @@ func newFallbackAuthSubmitter(t *testing.T) *BatchSubmitter { return l } -func testFallbackTxData() txData { +func testAuthTxData() txData { return singleFrameTxData(frameData{data: []byte("frame-data")}) } -// testBlobCandidate returns a tx candidate carrying one (zero) blob, which -// routes sendTxWithFallbackAuth onto the serialized blob path. +// testBlobCandidate returns a tx candidate carrying one (zero) blob, which routes +// submitAuthenticatedBatch onto the serialized blob path. func testBlobCandidate() *txmgr.TxCandidate { return &txmgr.TxCandidate{Blobs: []*eth.Blob{{}}} } @@ -92,14 +97,24 @@ func revertedReceiptWithBlock(num int64) *types.Receipt { return &types.Receipt{BlockNumber: big.NewInt(num), Status: types.ReceiptStatusFailed} } -// TestFallbackAuth_OrderingAndSuccess verifies a calldata batch is submitted -// as a single pipelined pair — the auth tx first (so it takes the lower nonce -// and lands first, as Espresso requires), the batch tx second — and that a -// single success receipt for the batch txData is emitted when both txs land -// within the lookback window. -func TestFallbackAuth_OrderingAndSuccess(t *testing.T) { - l := newFallbackAuthSubmitter(t) - txdata := testFallbackTxData() +// authTxRef builds the txRef the auth paths key their receipts under. +func authTxRef(txdata txData) txRef { + return txRef{id: txdata.ID(), isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()} +} + +// The following suite drives submitAuthenticatedBatch / watchAuthReceipts directly. This is the +// flow shared by both the fallback and TEE auth paths (they differ only in how the auth calldata +// is built; see TestFallbackAuth_Calldata / TestEspressoAuth_Calldata for that distinction). + +// TestSubmitAuthenticatedBatch_OrderingAndSuccess verifies a calldata batch is submitted as a +// single pipelined pair — the auth tx first (so it takes the lower nonce and lands first, as +// required under Holocene), the batch tx second — and that a single success receipt for the batch +// txData is emitted when both txs land within the lookback window. +func TestSubmitAuthenticatedBatch_OrderingAndSuccess(t *testing.T) { + l := newAuthSubmitter(t) + txdata := testAuthTxData() + ref := authTxRef(txdata) + verifyCandidate := txmgr.TxCandidate{TxData: []byte("auth-calldata"), To: &l.RollupConfig.BatchAuthenticatorAddress} candidate := &txmgr.TxCandidate{TxData: []byte("batch-calldata")} queue := &fakeTxSender{ @@ -110,17 +125,16 @@ func TestFallbackAuth_OrderingAndSuccess(t *testing.T) { } receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) - l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + l.submitAuthenticatedBatch(ref, verifyCandidate, candidate, queue, receiptsCh) + l.authGroup.Wait() require.Len(t, queue.sends, 2) require.Equal(t, 1, queue.pairCalls, "calldata pair must be submitted via SendPair") require.True(t, queue.sends[0].viaPair) require.True(t, queue.sends[1].viaPair) - // First leg must target the BatchAuthenticator (the auth tx), giving it the - // lower, earlier-mined nonce. - require.NotNil(t, queue.sends[0].candidate.To) - require.Equal(t, l.RollupConfig.BatchAuthenticatorAddress, *queue.sends[0].candidate.To) - // Second leg is the batch tx itself. + // First send is the auth tx, giving it the lower, earlier-mined nonce. + require.Equal(t, verifyCandidate.TxData, queue.sends[0].candidate.TxData) + // Second send is the batch tx itself. require.Equal(t, candidate.TxData, queue.sends[1].candidate.TxData) got := <-receiptsCh @@ -129,42 +143,52 @@ func TestFallbackAuth_OrderingAndSuccess(t *testing.T) { require.Equal(t, txdata.ID().String(), got.ID.id.String()) } -// TestFallbackAuth_AuthFailureRetried verifies that an auth-leg failure of a -// pipelined pair produces an error receipt keyed to the batch txData so the -// frames are re-queued. Both legs enter the queue back-to-back (pipelined); -// the tx manager cancels the batch leg when the auth leg fails, surfacing as -// an ErrPairLegCancelled response on the batch channel. -func TestFallbackAuth_AuthFailureRetried(t *testing.T) { - l := newFallbackAuthSubmitter(t) - txdata := testFallbackTxData() - candidate := &txmgr.TxCandidate{TxData: []byte("batch-calldata")} +func TestSubmitAuthenticatedBatch_AuthSendFailureRetried(t *testing.T) { + l := newAuthSubmitter(t) + txdata := testAuthTxData() queue := &fakeTxSender{ responses: []txmgr.TxReceipt[txRef]{ - {Err: errSendFailed}, // auth leg fails + {Err: errSendFailed}, // auth fails to send {Err: txmgr.ErrPairLegCancelled}, // batch leg cancelled by the pair }, } receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) - l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + l.submitAuthenticatedBatch(authTxRef(txdata), txmgr.TxCandidate{}, &txmgr.TxCandidate{}, queue, receiptsCh) + l.authGroup.Wait() got := <-receiptsCh require.Error(t, got.Err) require.Equal(t, txdata.ID().String(), got.ID.id.String()) - require.Len(t, queue.sends, 2) - require.Equal(t, 1, queue.pairCalls) } -// TestFallbackAuth_AuthRevertedRetried verifies that an authenticateBatchInfo tx -// that mines but reverts (no event emitted for the verifier) produces an error -// receipt so the frames are re-queued, rather than being confirmed as success. -// The tx manager treats a reverted auth leg as a pair failure and cancels the -// batch leg. -func TestFallbackAuth_AuthRevertedRetried(t *testing.T) { - l := newFallbackAuthSubmitter(t) - txdata := testFallbackTxData() - candidate := &txmgr.TxCandidate{TxData: []byte("batch-calldata")} +func TestSubmitAuthenticatedBatch_BatchSendFailureRetried(t *testing.T) { + l := newAuthSubmitter(t) + txdata := testAuthTxData() + + queue := &fakeTxSender{ + responses: []txmgr.TxReceipt[txRef]{ + {Receipt: receiptWithBlock(100)}, // auth lands + {Err: errSendFailed}, // batch fails to send + }, + } + receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) + + l.submitAuthenticatedBatch(authTxRef(txdata), txmgr.TxCandidate{}, &txmgr.TxCandidate{}, queue, receiptsCh) + l.authGroup.Wait() + + got := <-receiptsCh + require.Error(t, got.Err) + require.Equal(t, txdata.ID().String(), got.ID.id.String()) +} + +// TestSubmitAuthenticatedBatch_AuthRevertedRetried verifies that an authenticateBatchInfo tx that +// mines but reverts (no event emitted for the verifier) produces an error receipt so the frames +// are re-queued, rather than being confirmed as success. +func TestSubmitAuthenticatedBatch_AuthRevertedRetried(t *testing.T) { + l := newAuthSubmitter(t) + txdata := testAuthTxData() queue := &fakeTxSender{ responses: []txmgr.TxReceipt[txRef]{ @@ -174,45 +198,74 @@ func TestFallbackAuth_AuthRevertedRetried(t *testing.T) { } receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) - l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + l.submitAuthenticatedBatch(authTxRef(txdata), txmgr.TxCandidate{}, &txmgr.TxCandidate{}, queue, receiptsCh) + l.authGroup.Wait() got := <-receiptsCh require.Error(t, got.Err) require.Equal(t, txdata.ID().String(), got.ID.id.String()) - require.Len(t, queue.sends, 2) - require.Equal(t, 1, queue.pairCalls) } -// TestFallbackAuth_BatchFailureRetried verifies a batch-leg failure produces an -// error receipt keyed to the batch txData. -func TestFallbackAuth_BatchFailureRetried(t *testing.T) { - l := newFallbackAuthSubmitter(t) - txdata := testFallbackTxData() - candidate := &txmgr.TxCandidate{TxData: []byte("batch-calldata")} +// TestSubmitAuthenticatedBatch_WindowViolationRetried verifies that a batch tx landing outside the +// lookback window of the auth tx produces an error receipt (so the channel manager rewinds and +// resubmits), rather than being confirmed, and records the window-exceeded metric. +func TestSubmitAuthenticatedBatch_WindowViolationRetried(t *testing.T) { + l := newAuthSubmitter(t) + metr := &fallbackAuthMetricsSpy{Metricer: metrics.NoopMetrics} + l.Metr = metr + txdata := testAuthTxData() + tooFar := int64(100 + derive.BatchAuthLookbackWindow + 1) queue := &fakeTxSender{ responses: []txmgr.TxReceipt[txRef]{ - {Receipt: receiptWithBlock(100)}, // auth lands - {Err: errSendFailed}, // batch fails + {Receipt: receiptWithBlock(100)}, // auth + {Receipt: receiptWithBlock(tooFar)}, // batch too far away }, } receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) - l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + l.submitAuthenticatedBatch(authTxRef(txdata), txmgr.TxCandidate{}, &txmgr.TxCandidate{}, queue, receiptsCh) + l.authGroup.Wait() got := <-receiptsCh require.Error(t, got.Err) require.Equal(t, txdata.ID().String(), got.ID.id.String()) - require.Equal(t, 1, queue.pairCalls) + require.Equal(t, 1, metr.windowExceeded, "window violation should record the window-exceeded metric") } -// TestFallbackAuth_BlobSerialized verifies a blob batch takes the serialized -// path: the auth tx is sent alone (not as a pair) and the blob batch tx is -// only sent after the auth tx confirms, because geth's cross-pool account -// reservation cannot hold a calldata auth tx and a blob batch tx at once. -func TestFallbackAuth_BlobSerialized(t *testing.T) { - l := newFallbackAuthSubmitter(t) - txdata := testFallbackTxData() +// TestSubmitAuthenticatedBatch_WindowBoundaryAccepted pins the inclusive bound of the +// lookback window: a batch landing exactly BatchAuthLookbackWindow blocks after the auth +// tx is still accepted by the verifier (CollectAuthenticatedBatches scans +// [batchBlock - BatchAuthLookbackWindow, batchBlock]), so the batcher must not +// re-queue it. +func TestSubmitAuthenticatedBatch_WindowBoundaryAccepted(t *testing.T) { + l := newAuthSubmitter(t) + txdata := testAuthTxData() + + boundary := int64(100 + derive.BatchAuthLookbackWindow) + queue := &fakeTxSender{ + responses: []txmgr.TxReceipt[txRef]{ + {Receipt: receiptWithBlock(100)}, // auth + {Receipt: receiptWithBlock(boundary)}, // batch at the exact edge of the window + }, + } + receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) + + l.submitAuthenticatedBatch(authTxRef(txdata), txmgr.TxCandidate{}, &txmgr.TxCandidate{}, queue, receiptsCh) + l.authGroup.Wait() + + got := <-receiptsCh + require.NoError(t, got.Err) + require.Equal(t, receiptWithBlock(boundary).BlockNumber, got.Receipt.BlockNumber) + require.Equal(t, txdata.ID().String(), got.ID.id.String()) +} + +// TestAuth_BlobSerialized verifies a blob batch takes the serialized path: the auth tx is sent +// alone (not as a pair) and the blob batch tx is only sent after the auth tx confirms, because +// geth's cross-pool account reservation cannot hold a calldata auth tx and a blob batch tx at once. +func TestAuth_BlobSerialized(t *testing.T) { + l := newAuthSubmitter(t) + txdata := testAuthTxData() txdata.daType = DaTypeBlob candidate := testBlobCandidate() @@ -225,6 +278,7 @@ func TestFallbackAuth_BlobSerialized(t *testing.T) { receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + l.authGroup.Wait() require.Len(t, queue.sends, 2) require.Equal(t, 0, queue.pairCalls, "blob pair must not be pipelined") @@ -237,13 +291,12 @@ func TestFallbackAuth_BlobSerialized(t *testing.T) { require.Equal(t, txdata.ID().String(), got.ID.id.String()) } -// TestFallbackAuth_BlobAuthFailureGatesBatch verifies the serialized blob path -// never sends the batch tx when the auth tx fails: the batch would be crafted -// at the next nonce while the failed auth send resets the txmgr nonce, leaving -// a nonce gap (and an unauthenticated blob batch) behind. -func TestFallbackAuth_BlobAuthFailureGatesBatch(t *testing.T) { - l := newFallbackAuthSubmitter(t) - txdata := testFallbackTxData() +// TestAuth_BlobAuthFailureGatesBatch verifies the serialized blob path never sends the batch tx +// when the auth tx fails: the batch would be crafted at the next nonce while the failed auth send +// resets the txmgr nonce, leaving a nonce gap (and an unauthenticated blob batch) behind. +func TestAuth_BlobAuthFailureGatesBatch(t *testing.T) { + l := newAuthSubmitter(t) + txdata := testAuthTxData() txdata.daType = DaTypeBlob candidate := testBlobCandidate() @@ -255,6 +308,7 @@ func TestFallbackAuth_BlobAuthFailureGatesBatch(t *testing.T) { receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + l.authGroup.Wait() got := <-receiptsCh require.Error(t, got.Err) @@ -262,12 +316,12 @@ func TestFallbackAuth_BlobAuthFailureGatesBatch(t *testing.T) { require.Len(t, queue.sends, 1) } -// TestFallbackAuth_BlobAuthRevertGatesBatch is the revert variant: a mined but -// reverted auth emits no BatchInfoAuthenticated event, so the blob batch would -// be unverifiable and must not be submitted at all. -func TestFallbackAuth_BlobAuthRevertGatesBatch(t *testing.T) { - l := newFallbackAuthSubmitter(t) - txdata := testFallbackTxData() +// TestAuth_BlobAuthRevertGatesBatch is the revert variant: a mined but reverted auth emits no +// BatchInfoAuthenticated event, so the blob batch would be unverifiable and must not be submitted +// at all. +func TestAuth_BlobAuthRevertGatesBatch(t *testing.T) { + l := newAuthSubmitter(t) + txdata := testAuthTxData() txdata.daType = DaTypeBlob candidate := testBlobCandidate() @@ -279,6 +333,7 @@ func TestFallbackAuth_BlobAuthRevertGatesBatch(t *testing.T) { receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + l.authGroup.Wait() got := <-receiptsCh require.Error(t, got.Err) @@ -286,15 +341,14 @@ func TestFallbackAuth_BlobAuthRevertGatesBatch(t *testing.T) { require.Len(t, queue.sends, 1) } -// TestFallbackAuth_AuthFailureTxRefType verifies that an auth-tx failure is -// reported under a calldata-typed txRef even when the batch txdata is blob. -// The auth tx is always calldata; if its ErrAlreadyReserved failure were -// labeled with the batch's blob type, cancelBlockingTx would send a calldata -// cancel against a blobpool reservation, which is rejected the same way, -// looping forever without ever displacing the stuck blob tx. -func TestFallbackAuth_AuthFailureTxRefType(t *testing.T) { - l := newFallbackAuthSubmitter(t) - txdata := testFallbackTxData() +// TestAuth_AuthFailureTxRefType verifies that an auth-tx failure is reported under a +// calldata-typed txRef even when the batch txdata is blob. The auth tx is always calldata; if its +// ErrAlreadyReserved failure were labeled with the batch's blob type, cancelBlockingTx would send +// a calldata cancel against a blobpool reservation, which is rejected the same way, looping +// forever without ever displacing the stuck blob tx. +func TestAuth_AuthFailureTxRefType(t *testing.T) { + l := newAuthSubmitter(t) + txdata := testAuthTxData() txdata.daType = DaTypeBlob candidate := testBlobCandidate() @@ -306,6 +360,7 @@ func TestFallbackAuth_AuthFailureTxRefType(t *testing.T) { receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + l.authGroup.Wait() got := <-receiptsCh require.Error(t, got.Err) @@ -315,11 +370,11 @@ func TestFallbackAuth_AuthFailureTxRefType(t *testing.T) { require.Equal(t, DaTypeCalldata, got.ID.daType) } -// TestFallbackAuth_BatchFailureTxRefType verifies the converse: a batch-tx -// failure keeps the batch txdata's own type on the forwarded receipt. -func TestFallbackAuth_BatchFailureTxRefType(t *testing.T) { - l := newFallbackAuthSubmitter(t) - txdata := testFallbackTxData() +// TestAuth_BatchFailureTxRefType verifies the converse: a batch-tx failure keeps the batch +// txdata's own type on the forwarded receipt. +func TestAuth_BatchFailureTxRefType(t *testing.T) { + l := newAuthSubmitter(t) + txdata := testAuthTxData() txdata.daType = DaTypeBlob candidate := testBlobCandidate() @@ -332,6 +387,7 @@ func TestFallbackAuth_BatchFailureTxRefType(t *testing.T) { receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + l.authGroup.Wait() got := <-receiptsCh require.Error(t, got.Err) @@ -341,58 +397,124 @@ func TestFallbackAuth_BatchFailureTxRefType(t *testing.T) { require.Equal(t, DaTypeBlob, got.ID.daType) } -// TestFallbackAuth_WindowViolationRetried verifies that a batch tx landing -// outside the lookback window of the auth tx produces an error receipt (so the -// channel manager rewinds and resubmits), rather than being confirmed. -func TestFallbackAuth_WindowViolationRetried(t *testing.T) { - l := newFallbackAuthSubmitter(t) - metr := &fallbackAuthMetricsSpy{Metricer: metrics.NoopMetrics} - l.Metr = metr - txdata := testFallbackTxData() +// unpackAuthenticateBatchInfo decodes an authenticateBatchInfo(bytes32,bytes) calldata blob into +// its commitment and signature arguments. +func unpackAuthenticateBatchInfo(t *testing.T, calldata []byte) (commitment [32]byte, signature []byte) { + abi, err := bindings.BatchAuthenticatorMetaData.GetAbi() + require.NoError(t, err) + method, ok := abi.Methods["authenticateBatchInfo"] + require.True(t, ok) + require.Equal(t, method.ID, calldata[:4], "calldata is not an authenticateBatchInfo call") + + args, err := method.Inputs.Unpack(calldata[4:]) + require.NoError(t, err) + require.Len(t, args, 2) + return args[0].([32]byte), args[1].([]byte) +} + +// TestFallbackAuth_Calldata verifies the distinguishing behavior of the fallback path: the auth tx +// it submits is authenticateBatchInfo(commitment, emptySig), relying on the contract's msg.sender +// check rather than a signature. +func TestFallbackAuth_Calldata(t *testing.T) { + l := newAuthSubmitter(t) + txdata := testAuthTxData() candidate := &txmgr.TxCandidate{TxData: []byte("batch-calldata")} - tooFar := int64(100 + derive.BatchAuthLookbackWindow + 1) queue := &fakeTxSender{ responses: []txmgr.TxReceipt[txRef]{ - {Receipt: receiptWithBlock(100)}, // auth - {Receipt: receiptWithBlock(tooFar)}, // batch too far away + {Receipt: receiptWithBlock(100)}, + {Receipt: receiptWithBlock(101)}, }, } receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + l.authGroup.Wait() - got := <-receiptsCh - require.Error(t, got.Err) - require.Equal(t, txdata.ID().String(), got.ID.id.String()) - require.Equal(t, 1, metr.windowExceeded, "window violation should record the fallback_auth_window_exceeded metric") + require.Len(t, queue.sends, 2) + require.NotNil(t, queue.sends[0].candidate.To) + require.Equal(t, l.RollupConfig.BatchAuthenticatorAddress, *queue.sends[0].candidate.To) + + expectedCommitment, err := computeCommitment(candidate) + require.NoError(t, err) + commitment, signature := unpackAuthenticateBatchInfo(t, queue.sends[0].candidate.TxData) + require.Equal(t, expectedCommitment, commitment) + require.Empty(t, signature, "fallback path must send an empty signature") } -// TestFallbackAuth_WindowBoundaryAccepted pins the inclusive bound of the lookback -// window: a batch landing exactly BatchAuthLookbackWindow blocks after the auth tx is -// still accepted by the verifier (CollectAuthenticatedBatches scans -// [batchBlock - BatchAuthLookbackWindow, batchBlock]), so the batcher must not -// re-queue it. -func TestFallbackAuth_WindowBoundaryAccepted(t *testing.T) { - l := newFallbackAuthSubmitter(t) - txdata := testFallbackTxData() +// TestEspressoAuth_Calldata verifies the distinguishing behavior of the TEE path: the auth tx it +// submits is authenticateBatchInfo(commitment, sig) where sig is a 65-byte EIP-712 signature over +// the commitment that recovers to the batcher's key. +func TestEspressoAuth_Calldata(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + + l := newAuthSubmitter(t) + l.RollupConfig.L1ChainID = big.NewInt(1) + l.teeVerifierAddress = common.HexToAddress("0x00000000000000000000000000000000000000bb") + l.Config.Espresso.BatcherPrivateKey = key + + txdata := testAuthTxData() candidate := &txmgr.TxCandidate{TxData: []byte("batch-calldata")} - boundary := int64(100 + derive.BatchAuthLookbackWindow) queue := &fakeTxSender{ responses: []txmgr.TxReceipt[txRef]{ - {Receipt: receiptWithBlock(100)}, // auth - {Receipt: receiptWithBlock(boundary)}, // batch at the exact edge of the window + {Receipt: receiptWithBlock(100)}, + {Receipt: receiptWithBlock(101)}, }, } receiptsCh := make(chan txmgr.TxReceipt[txRef], 1) - l.sendTxWithFallbackAuth(txdata, false, candidate, queue, receiptsCh) + l.sendTxWithEspresso(txdata, false, candidate, queue, receiptsCh) + l.authGroup.Wait() - got := <-receiptsCh - require.NoError(t, got.Err) - require.Equal(t, receiptWithBlock(boundary).BlockNumber, got.Receipt.BlockNumber) - require.Equal(t, txdata.ID().String(), got.ID.id.String()) + require.Len(t, queue.sends, 2) + require.NotNil(t, queue.sends[0].candidate.To) + require.Equal(t, l.RollupConfig.BatchAuthenticatorAddress, *queue.sends[0].candidate.To) + + expectedCommitment, err := computeCommitment(candidate) + require.NoError(t, err) + commitment, signature := unpackAuthenticateBatchInfo(t, queue.sends[0].candidate.TxData) + require.Equal(t, expectedCommitment, commitment) + require.Len(t, signature, 65, "TEE path must send a 65-byte EIP-712 signature") + + // Reconstruct the EIP-712 digest the batcher signed and confirm the signature recovers to the + // batcher's key. + typedData := apitypes.TypedData{ + Types: apitypes.Types{ + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, + "EspressoTEEVerifier": []apitypes.Type{ + {Name: "commitment", Type: "bytes32"}, + }, + }, + PrimaryType: "EspressoTEEVerifier", + Domain: apitypes.TypedDataDomain{ + Name: "EspressoTEEVerifier", + Version: "1", + ChainId: (*math.HexOrDecimal256)(l.RollupConfig.L1ChainID), + VerifyingContract: l.teeVerifierAddress.String(), + }, + Message: map[string]interface{}{ + "commitment": commitment, + }, + } + digest, _, err := apitypes.TypedDataAndHash(typedData) + require.NoError(t, err) + + // Denormalize v (27/28 -> 0/1) before recovering, undoing the Solidity-compat normalization. + recoverSig := make([]byte, 65) + copy(recoverSig, signature) + require.Contains(t, []byte{27, 28}, recoverSig[64]) + recoverSig[64] -= 27 + + recovered, err := crypto.SigToPub(digest, recoverSig) + require.NoError(t, err) + require.Equal(t, crypto.PubkeyToAddress(key.PublicKey), crypto.PubkeyToAddress(*recovered)) } // TestComputeCommitment_Parity locks the batcher's batch-commitment computation to diff --git a/op-batcher/batcher/espresso_fallback_gate.go b/op-batcher/batcher/espresso_fallback_gate.go index d4e7b7cad85..88e23e9e3cf 100644 --- a/op-batcher/batcher/espresso_fallback_gate.go +++ b/op-batcher/batcher/espresso_fallback_gate.go @@ -24,13 +24,11 @@ func (l *BatchSubmitter) dispatchAuthenticatedSendTx(txdata txData, isCancel boo if isCancel { return false } - // Espresso batcher: authenticate via BatchAuthenticator. + // Espresso batcher: authenticate via BatchAuthenticator. sendTxWithEspresso runs on the + // caller's goroutine and submits through the ordered queue (submitAuthenticatedBatch spawns + // its own authGroup-tracked watcher), so it must not be wrapped in a separate goroutine. if l.Config.Espresso.Enabled { - l.authGroup.Add(1) - go func() { - defer l.authGroup.Done() - l.sendTxWithEspresso(txdata, isCancel, candidate, queue, receiptsCh) - }() + l.sendTxWithEspresso(txdata, isCancel, candidate, queue, receiptsCh) return true } fallbackAuthRequired, err := l.isFallbackAuthRequired(l.killCtx) From ae48b73e4bdb17431a2fe951a39f13f2867fda3c Mon Sep 17 00:00:00 2001 From: Artemii Gerasimovich Date: Thu, 18 Jun 2026 17:00:03 +0200 Subject: [PATCH 07/25] op-node: add EspressoBatch roundtrip test, fix derive_test vet failure Port TestBatchRoundtrip from the integration branch and fold it into espresso_batch_test.go. It is the only test covering ToEspressoTransaction and the batcher->derivation serialization path; it asserts the decoded batch matches the original and that the recovered signer is the batcher. Also drop the decodedBlock.ExecutionWitness() comparison in TestEspressoBatchConversion: that method does not exist on the op-geth types.Block pinned in the rebase-18 base, so go vet of the derive_test package failed to build. EspressoBatch/ToBlock carries no execution witness. Co-authored-by: OpenCode --- op-node/rollup/derive/espresso_batch_test.go | 64 ++++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/op-node/rollup/derive/espresso_batch_test.go b/op-node/rollup/derive/espresso_batch_test.go index 05ef9c2f9d3..a97fcc417f1 100644 --- a/op-node/rollup/derive/espresso_batch_test.go +++ b/op-node/rollup/derive/espresso_batch_test.go @@ -2,6 +2,7 @@ package derive_test import ( "bytes" + "context" "math/big" "math/rand" "slices" @@ -11,13 +12,22 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup" derive "github.com/ethereum-optimism/optimism/op-node/rollup/derive" dtest "github.com/ethereum-optimism/optimism/op-node/rollup/derive/test" + "github.com/ethereum-optimism/optimism/op-service/crypto" "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/signer" + "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum/go-ethereum/common" gethTypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" + gethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" "github.com/stretchr/testify/require" ) +const ( + testMnemonic = "test test test test test test test test test test test junk" + testHDPath = "m/44'/60'/0'/0/1" +) + var defaultTestRollUpConfig = &rollup.Config{ Genesis: rollup.Genesis{L2: eth.BlockID{Number: 0}}, L2ChainID: big.NewInt(1234), @@ -89,7 +99,7 @@ func TestUnmarshalEspressoTransactionTooShort(t *testing.T) { cases := [][]byte{ nil, {}, - make([]byte, crypto.SignatureLength-1), + make([]byte, gethCrypto.SignatureLength-1), } for _, data := range cases { _, err := derive.UnmarshalEspressoTransaction(data) @@ -151,10 +161,6 @@ func TestEspressoBatchConversion(t *testing.T) { t.Errorf("decoded block excess blob gas mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) } - if have, want := decodedBlock.ExecutionWitness(), originalBlock.ExecutionWitness(); have != want { - t.Errorf("decoded block execution witness mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - if have, want := decodedBlock.Extra(), originalBlock.Extra(); !bytes.Equal(have, want) { t.Errorf("decoded block extra mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) } @@ -235,3 +241,49 @@ func TestEspressoBatchConversion(t *testing.T) { t.Errorf("decoded block withdrawals root mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) } } + +// TestBatchRoundtrip exercises the batcher serialization path +// (BlockToEspressoBatch -> ToEspressoTransaction) against the derivation +// deserialization path (UnmarshalEspressoTransaction): a block packed and signed +// by the batcher must decode back to an equivalent batch, and the signer address +// recovered from the payload signature must match the batcher's address. +func TestBatchRoundtrip(t *testing.T) { + rng := rand.New(rand.NewSource(1)) + + originalBlock := dtest.RandomL2BlockWithChainIdAndTime(rng, 10, defaultTestRollUpConfig.L2ChainID, time.Now()) + + batch, err := derive.BlockToEspressoBatch(defaultTestRollUpConfig, originalBlock) + require.NoError(t, err, "failed to convert block to batch") + + signerFactory, batcherAddress, err := crypto.ChainSignerFactoryFromConfig( + testlog.Logger(t, log.LevelDebug), + "", + testMnemonic, + testHDPath, + signer.NewCLIConfig(), + ) + require.NoError(t, err, "failed to build chain signer factory") + chainSigner := signerFactory(defaultTestRollUpConfig.L2ChainID, batcherAddress) + + transaction, err := batch.ToEspressoTransaction( + context.Background(), + defaultTestRollUpConfig.L2ChainID.Uint64(), + chainSigner, + ) + require.NoError(t, err, "failed to serialize batch to Espresso transaction") + + decodedBatch, err := derive.UnmarshalEspressoTransaction(transaction.Payload) + require.NoError(t, err, "failed to deserialize Espresso transaction back to batch") + + // The signer recovered from the payload signature must be the batcher. + require.Equal(t, batcherAddress, decodedBatch.SignerAddress, "recovered signer address mismatch") + + // The decoded batch must be equivalent to the original (the recovered + // SignerAddress is populated only on decode, so compare the encoded fields). + require.Equal(t, 0, compareHeader(decodedBatch.BatchHeader, batch.BatchHeader), "decoded batch header mismatch") + require.Equal(t, 0, compareTransaction(decodedBatch.L1InfoDeposit, batch.L1InfoDeposit), "decoded batch L1 info deposit mismatch") + require.Equal(t, batch.Batch.EpochNum, decodedBatch.Batch.EpochNum, "decoded batch epoch num mismatch") + require.Equal(t, batch.Batch.EpochHash, decodedBatch.Batch.EpochHash, "decoded batch epoch hash mismatch") + require.Equal(t, batch.Batch.Timestamp, decodedBatch.Batch.Timestamp, "decoded batch timestamp mismatch") + require.Equal(t, batch.Batch.Transactions, decodedBatch.Batch.Transactions, "decoded batch transactions mismatch") +} From 364a38bb1ade95be8fa4e0cced2930267371cd50 Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Mon, 6 Jul 2026 15:45:31 -0400 Subject: [PATCH 08/25] verify batches while converting to a block --- op-node/rollup/derive/espresso_batch.go | 24 +++++++ op-node/rollup/derive/espresso_batch_test.go | 67 ++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/op-node/rollup/derive/espresso_batch.go b/op-node/rollup/derive/espresso_batch.go index fc22780bfd5..9f7b6cb3475 100644 --- a/op-node/rollup/derive/espresso_batch.go +++ b/op-node/rollup/derive/espresso_batch.go @@ -122,6 +122,30 @@ func UnmarshalEspressoTransaction(data []byte) (*EspressoBatch, error) { // invalid batches or in case of misconfiguration of the batcher, in which case it should fail // for all batches. func (b *EspressoBatch) ToBlock(rollupCfg *rollup.Config) (*types.Block, error) { + // The produced block must round-trip through BlockToSingularBatch when the channel + // manager encodes it, so enforce that function's requirements up front, plus + // consistency between the header and the batch body it claims to describe. + if b.BatchHeader == nil { + return nil, fmt.Errorf("batch has no header") + } + if b.L1InfoDeposit == nil || !b.L1InfoDeposit.IsDepositTx() { + return nil, fmt.Errorf("first transaction is not an L1 info deposit") + } + l1Info, err := L1BlockInfoFromBytes(rollupCfg, b.BatchHeader.Time, b.L1InfoDeposit.Data()) + if err != nil { + return nil, fmt.Errorf("could not parse the L1 info deposit: %w", err) + } + if b.Batch.ParentHash != b.BatchHeader.ParentHash { + return nil, fmt.Errorf("batch parent hash %s does not match header parent hash %s", b.Batch.ParentHash, b.BatchHeader.ParentHash) + } + if b.Batch.Timestamp != b.BatchHeader.Time { + return nil, fmt.Errorf("batch timestamp %d does not match header timestamp %d", b.Batch.Timestamp, b.BatchHeader.Time) + } + if uint64(b.Batch.EpochNum) != l1Info.Number || b.Batch.EpochHash != l1Info.BlockHash { + return nil, fmt.Errorf("batch epoch %d (%s) does not match L1 info deposit epoch %d (%s)", + b.Batch.EpochNum, b.Batch.EpochHash, l1Info.Number, l1Info.BlockHash) + } + // Re-insert the deposit transaction txs := []*types.Transaction{b.L1InfoDeposit} for i, opaqueTx := range b.Batch.Transactions { diff --git a/op-node/rollup/derive/espresso_batch_test.go b/op-node/rollup/derive/espresso_batch_test.go index a97fcc417f1..f960fc72770 100644 --- a/op-node/rollup/derive/espresso_batch_test.go +++ b/op-node/rollup/derive/espresso_batch_test.go @@ -287,3 +287,70 @@ func TestBatchRoundtrip(t *testing.T) { require.Equal(t, batch.Batch.Timestamp, decodedBatch.Batch.Timestamp, "decoded batch timestamp mismatch") require.Equal(t, batch.Batch.Transactions, decodedBatch.Batch.Transactions, "decoded batch transactions mismatch") } + +// TestToBlockRejectsMalformedBatch verifies that ToBlock rejects validly-structured but +// semantically malformed batches. +func TestToBlockRejectsMalformedBatch(t *testing.T) { + validBatch := func(t *testing.T) *derive.EspressoBatch { + block := dtest.RandomL2BlockWithChainIdAndTime( + rand.New(rand.NewSource(time.Now().Unix())), + 3, + defaultTestRollUpConfig.L2ChainID, + time.Now(), + ) + b, err := derive.BlockToEspressoBatch(defaultTestRollUpConfig, block) + require.NoError(t, err) + return b + } + + t.Run("valid batch converts", func(t *testing.T) { + b := validBatch(t) + _, err := b.ToBlock(defaultTestRollUpConfig) + require.NoError(t, err) + }) + + t.Run("nil L1 info deposit", func(t *testing.T) { + b := validBatch(t) + b.L1InfoDeposit = nil + _, err := b.ToBlock(defaultTestRollUpConfig) + require.ErrorContains(t, err, "not an L1 info deposit") + }) + + t.Run("first tx not a deposit", func(t *testing.T) { + b := validBatch(t) + // Replace the L1 info deposit with a decoded non-deposit tx from the batch body. + var nonDeposit gethTypes.Transaction + require.NoError(t, nonDeposit.UnmarshalBinary(b.Batch.Transactions[0])) + b.L1InfoDeposit = &nonDeposit + _, err := b.ToBlock(defaultTestRollUpConfig) + require.ErrorContains(t, err, "not an L1 info deposit") + }) + + t.Run("malformed L1 info data", func(t *testing.T) { + b := validBatch(t) + b.L1InfoDeposit = gethTypes.NewTx(&gethTypes.DepositTx{Data: []byte{0xde, 0xad, 0xbe, 0xef}}) + _, err := b.ToBlock(defaultTestRollUpConfig) + require.ErrorContains(t, err, "could not parse the L1 info deposit") + }) + + t.Run("parent hash mismatch", func(t *testing.T) { + b := validBatch(t) + b.Batch.ParentHash[0] ^= 0xff + _, err := b.ToBlock(defaultTestRollUpConfig) + require.ErrorContains(t, err, "parent hash") + }) + + t.Run("timestamp mismatch", func(t *testing.T) { + b := validBatch(t) + b.Batch.Timestamp++ + _, err := b.ToBlock(defaultTestRollUpConfig) + require.ErrorContains(t, err, "timestamp") + }) + + t.Run("epoch mismatch", func(t *testing.T) { + b := validBatch(t) + b.Batch.EpochNum++ + _, err := b.ToBlock(defaultTestRollUpConfig) + require.ErrorContains(t, err, "epoch") + }) +} From affb987bd3d9e45b49e3db840f33214c56e13967 Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Mon, 6 Jul 2026 15:49:09 -0400 Subject: [PATCH 09/25] start throttling loop after espresso/non espresso loops --- op-batcher/batcher/driver.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 187fa9ca4bd..5e149cf8fc5 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -224,14 +224,6 @@ func (l *BatchSubmitter) StartBatchSubmitting() error { publishSignal := make(chan pubInfo, 1) l.publishSignal = publishSignal - // DA throttling loop should always be started except for testing (indicated by ThrottleThreshold == 0) - if l.Config.ThrottleParams.LowerThreshold > 0 { - l.wg.Add(1) - go l.throttlingLoop(l.wg, unsafeBytesUpdated) // ranges over unsafeBytesUpdated channel - } else { - l.Log.Warn("Throttling loop is DISABLED due to 0 throttle-threshold. This should not be disabled in prod.") - } - if l.Config.Espresso.Enabled { if err := l.startEspressoLoops(receiptsCh, publishSignal); err != nil { return err @@ -243,6 +235,14 @@ func (l *BatchSubmitter) StartBatchSubmitting() error { go l.blockLoadingLoop(l.shutdownCtx, l.wg, unsafeBytesUpdated, publishSignal) // sends on unsafeBytesUpdated (if throttling enabled), and publishSignal. Closes them both when done } + // DA throttling loop should always be started except for testing (indicated by ThrottleThreshold == 0) + if l.Config.ThrottleParams.LowerThreshold > 0 { + l.wg.Add(1) + go l.throttlingLoop(l.wg, unsafeBytesUpdated) // ranges over unsafeBytesUpdated channel + } else { + l.Log.Warn("Throttling loop is DISABLED due to 0 throttle-threshold. This should not be disabled in prod.") + } + l.Log.Info("Batch Submitter started") return nil } From a72e9c15f35c4690ecd588129f325c527edf0a02 Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Mon, 6 Jul 2026 16:01:53 -0400 Subject: [PATCH 10/25] make use of unsafeBytesUpdated --- op-batcher/batcher/driver.go | 2 +- op-batcher/batcher/espresso.go | 9 +++++++-- op-batcher/batcher/espresso_driver.go | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 5e149cf8fc5..ee37593a3dc 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -225,7 +225,7 @@ func (l *BatchSubmitter) StartBatchSubmitting() error { l.publishSignal = publishSignal if l.Config.Espresso.Enabled { - if err := l.startEspressoLoops(receiptsCh, publishSignal); err != nil { + if err := l.startEspressoLoops(receiptsCh, publishSignal, unsafeBytesUpdated); err != nil { return err } } else { diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go index 00d19f15526..17ba19751a8 100644 --- a/op-batcher/batcher/espresso.go +++ b/op-batcher/batcher/espresso.go @@ -908,14 +908,17 @@ func (l *BatchSubmitter) peekNextBatch(ctx context.Context, syncStatus *eth.Sync return batch } -// Periodically refreshes the sync status and polls Espresso streamer for new batches -func (l *BatchSubmitter) espressoBatchLoadingLoop(ctx context.Context, wg *sync.WaitGroup, publishSignal chan pubInfo) { +// Periodically refreshes the sync status and polls Espresso streamer for new batches. +// Owns publishSignal and unsafeBytesUpdated: it is their only closer, so the loops +// ranging over them (publishingLoop, throttlingLoop) terminate when this loop exits. +func (l *BatchSubmitter) espressoBatchLoadingLoop(ctx context.Context, wg *sync.WaitGroup, publishSignal chan pubInfo, unsafeBytesUpdated chan int64) { l.Log.Info("Starting EspressoBatchLoadingLoop", "polling interval", l.Config.Espresso.PollInterval) defer wg.Done() ticker := time.NewTicker(l.Config.Espresso.PollInterval) defer ticker.Stop() defer close(publishSignal) + defer close(unsafeBytesUpdated) for { select { @@ -970,6 +973,8 @@ func (l *BatchSubmitter) espressoBatchLoadingLoop(ctx context.Context, wg *sync. l.EspressoStreamer().Next(ctx) l.Log.Info("Added L2 block to channel manager", "blockNr", block.NumberU64()) + // We have increased the unsafe data. Signal the throttling loop to + l.sendToThrottlingLoop(unsafeBytesUpdated) } l.tryPublishSignal(publishSignal, pubInfo{}) diff --git a/op-batcher/batcher/espresso_driver.go b/op-batcher/batcher/espresso_driver.go index c1571e4d0aa..491a544aa15 100644 --- a/op-batcher/batcher/espresso_driver.go +++ b/op-batcher/batcher/espresso_driver.go @@ -99,7 +99,7 @@ func (l *BatchSubmitter) setupEspressoStreamer() { // submitter, and starts the four Espresso-specific batcher goroutines (in // addition to the upstream receiptsLoop and publishingLoop). Replaces the // upstream three-goroutine pattern when --espresso.enabled is set. -func (l *BatchSubmitter) startEspressoLoops(receiptsCh chan txmgr.TxReceipt[txRef], publishSignal chan pubInfo) error { +func (l *BatchSubmitter) startEspressoLoops(receiptsCh chan txmgr.TxReceipt[txRef], publishSignal chan pubInfo, unsafeBytesUpdated chan int64) error { if err := l.registerBatcher(l.killCtx); err != nil { return fmt.Errorf("could not register with BatchAuthenticator contract: %w", err) } @@ -123,8 +123,8 @@ func (l *BatchSubmitter) startEspressoLoops(receiptsCh chan txmgr.TxReceipt[txRe l.wg.Add(4) go l.receiptsLoop(l.wg, receiptsCh) // ranges over receiptsCh channel go l.espressoBatchQueueingLoop(l.shutdownCtx, l.wg) - go l.espressoBatchLoadingLoop(l.shutdownCtx, l.wg, publishSignal) - go l.publishingLoop(l.killCtx, l.wg, receiptsCh, publishSignal) // ranges over publishSignal, spawns routines which send on receiptsCh. Closes receiptsCh when done. + go l.espressoBatchLoadingLoop(l.shutdownCtx, l.wg, publishSignal, unsafeBytesUpdated) // sends on unsafeBytesUpdated (if throttling enabled) and publishSignal. Closes them both when done + go l.publishingLoop(l.killCtx, l.wg, receiptsCh, publishSignal) // ranges over publishSignal, spawns routines which send on receiptsCh. Closes receiptsCh when done. return nil } From e94cd76baf0eed4d9c32712078978bfd56a84047 Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Mon, 6 Jul 2026 19:15:12 -0400 Subject: [PATCH 11/25] initChainSigner fallback for non espresso mode --- op-batcher/batcher/espresso_service.go | 5 ++++- op-batcher/batcher/service.go | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/op-batcher/batcher/espresso_service.go b/op-batcher/batcher/espresso_service.go index 4217abd8af9..a6f86b151e9 100644 --- a/op-batcher/batcher/espresso_service.go +++ b/op-batcher/batcher/espresso_service.go @@ -62,7 +62,10 @@ func (bs *BatcherService) EspressoStreamer() espresso.EspressoStreamer[derive.Es // ChainSigner interface and stores the embedded ChainSigner on the service. // Espresso uses ChainSigner to sign batch authentication payloads sent to the // BatchAuthenticator contract; the cast is required by every Espresso path. -func (bs *BatcherService) initChainSigner() error { +func (bs *BatcherService) initChainSigner(cfg *CLIConfig) error { + if !cfg.Espresso.Enabled { + return nil + } cast, castOk := bs.TxManager.(opcrypto.ChainSigner) if !castOk { return fmt.Errorf("tx manager does not implement ChainSigner") diff --git a/op-batcher/batcher/service.go b/op-batcher/batcher/service.go index bef9eef56d6..01d39f7d3a4 100644 --- a/op-batcher/batcher/service.go +++ b/op-batcher/batcher/service.go @@ -413,7 +413,7 @@ func (bs *BatcherService) initTxManager(_ context.Context, cfg *CLIConfig) error return err } bs.TxManager = txManager - if err := bs.initChainSigner(); err != nil { + if err := bs.initChainSigner(cfg); err != nil { return err } return nil From 66f01dc6598df1c8cd3151edcb3de9d955203539 Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Mon, 6 Jul 2026 20:02:15 -0400 Subject: [PATCH 12/25] lint and make use of isBatcherActive --- op-batcher/batcher/driver.go | 4 +++ op-batcher/batcher/espresso.go | 13 +++---- op-batcher/batcher/espresso_driver.go | 35 ++++++++++++++++++- op-batcher/batcher/espresso_service.go | 3 +- .../espresso_transaction_submitter_test.go | 4 ++- op-node/rollup/derive/espresso_batch.go | 3 +- op-node/rollup/derive/espresso_batch_test.go | 3 +- 7 files changed, 54 insertions(+), 11 deletions(-) diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index ee37593a3dc..44de47e3047 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -858,6 +858,10 @@ func (l *BatchSubmitter) publishStateToL1(ctx context.Context, queue *txmgr.Queu return } + if l.shouldSkipPublishForActiveSeq(ctx) { + return + } + err := l.publishTxToL1(ctx, queue, receiptsCh, daGroup, pi) if err != nil { if err != io.EOF { diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go index 17ba19751a8..7052bb0d202 100644 --- a/op-batcher/batcher/espresso.go +++ b/op-batcher/batcher/espresso.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum-optimism/optimism/espresso" "github.com/ethereum-optimism/optimism/espresso/bindings" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + "github.com/ethereum-optimism/optimism/op-service/bigs" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/txmgr" ) @@ -815,7 +816,7 @@ func (l *BatchSubmitter) queueBlockToEspresso(ctx context.Context, block *types. return fmt.Errorf("failed to derive batch from block: %w", err) } - transaction, err := espressoBatch.ToEspressoTransaction(ctx, l.RollupConfig.L2ChainID.Uint64(), l.Espresso.ChainSigner) + transaction, err := espressoBatch.ToEspressoTransaction(ctx, bigs.Uint64Strict(l.RollupConfig.L2ChainID), l.Espresso.ChainSigner) if err != nil { l.Log.Warn("Failed to create Espresso transaction from a batch", "err", err) return fmt.Errorf("failed to create Espresso transaction from a batch: %w", err) @@ -823,7 +824,7 @@ func (l *BatchSubmitter) queueBlockToEspresso(ctx context.Context, block *types. commitment := transaction.Commit() hash, _ := tagged_base64.New("TX", commitment[:]) - l.Log.Info("Created Espresso transaction from batch", "hash", hash, "batchNr", espressoBatch.BatchHeader.Number.Uint64()) + l.Log.Info("Created Espresso transaction from batch", "hash", hash, "batchNr", bigs.Uint64Strict(espressoBatch.BatchHeader.Number)) if err := l.espressoSubmitter.SubmitTransaction(transaction); err != nil { return fmt.Errorf("failed to submit job to espresso: %w", err) @@ -1186,12 +1187,12 @@ func (l *BatchSubmitter) espressoBatchQueueingLoop(ctx context.Context, wg *sync // This is a check to help us determine whether we're able to // push through all of the blocks we've attempted to or not. - if (numEnqueuedBlocksAfter - numEnqueuedBlocksBefore) < int(blocksToQueue.numBlocks()) { - // If we're in this conditional, it means we weren't able - // submit all of the blocks to Espresso that we were - // attempting to. + if enqueued := numEnqueuedBlocksAfter - numEnqueuedBlocksBefore; enqueued < int(blocksToQueue.numBlocks()) { + // We weren't able to submit all of the blocks to Espresso + // that we were attempting to. // // TODO: We should probably throttle a bit. + l.Log.Debug("Could not enqueue all blocks to Espresso", "enqueued", enqueued, "attempted", blocksToQueue.numBlocks()) } } else if action == ActionReset { loader.reset(ctx) diff --git a/op-batcher/batcher/espresso_driver.go b/op-batcher/batcher/espresso_driver.go index 491a544aa15..da6f22e629f 100644 --- a/op-batcher/batcher/espresso_driver.go +++ b/op-batcher/batcher/espresso_driver.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum-optimism/optimism/espresso" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + "github.com/ethereum-optimism/optimism/op-service/bigs" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" "github.com/ethereum-optimism/optimism/op-service/txmgr" ) @@ -75,7 +76,7 @@ func (l *BatchSubmitter) setupEspressoStreamer() { lightClientIface = l.Espresso.LightClient } unbufferedStreamer, err := op.NewEspressoStreamer( - l.RollupConfig.L2ChainID.Uint64(), + bigs.Uint64Strict(l.RollupConfig.L2ChainID), l1Adapter, l1Adapter, l.Espresso.Client, @@ -128,6 +129,38 @@ func (l *BatchSubmitter) startEspressoLoops(receiptsCh chan txmgr.TxReceipt[txRe return nil } +// shouldSkipPublishForActiveSeq returns true if publishStateToL1 should skip +// publishing because this batcher is not the on-chain "active" batcher +// (BatchAuthenticator.activeIsEspresso). The Espresso TEE batcher always +// honors the flag (it is fundamentally a post-fork actor); the fallback +// batcher honors it only once fallback auth is required (pre-fork it must run +// as a vanilla upstream Optimism batcher with no BatchAuthenticator coupling). +// Fails closed: if either gate cannot be evaluated, publishing is skipped for +// this tick and retried on the next. +func (l *BatchSubmitter) shouldSkipPublishForActiveSeq(ctx context.Context) bool { + if l.RollupConfig.BatchAuthenticatorAddress == (common.Address{}) { + return false + } + consultActiveFlag := l.Config.Espresso.Enabled + if !consultActiveFlag { + fallbackAuthRequired, err := l.isFallbackAuthRequired(ctx) + if err != nil { + l.Log.Warn("Failed to evaluate fallback-auth gate, skipping publish", "err", err) + return true + } + consultActiveFlag = fallbackAuthRequired + } + if !consultActiveFlag { + return false + } + isActive, err := l.isBatcherActive(ctx) + if err != nil { + l.Log.Warn("Failed to check if batcher is active, skipping publish", "err", err) + return true + } + return !isActive +} + // resetEspressoStreamer resets the Espresso streamer when --espresso.enabled // is set; no-op otherwise. Called from clearState alongside the upstream // channel-manager reset so the streamer's view of "next batch" matches the diff --git a/op-batcher/batcher/espresso_service.go b/op-batcher/batcher/espresso_service.go index a6f86b151e9..bc016244c2d 100644 --- a/op-batcher/batcher/espresso_service.go +++ b/op-batcher/batcher/espresso_service.go @@ -15,6 +15,7 @@ import ( "github.com/ethereum-optimism/optimism/espresso" "github.com/ethereum-optimism/optimism/op-batcher/enclave" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + "github.com/ethereum-optimism/optimism/op-service/bigs" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" ) @@ -126,7 +127,7 @@ func (bs *BatcherService) initEspresso(cfg *CLIConfig) error { } if cfg.Espresso.Namespace == 0 { log.Info("Using L2 chain ID as namespace by default") - cfg.Espresso.Namespace = bs.RollupConfig.L2ChainID.Uint64() + cfg.Espresso.Namespace = bigs.Uint64Strict(bs.RollupConfig.L2ChainID) } if cfg.Espresso.BatchAuthenticatorAddr == (common.Address{}) { cfg.Espresso.BatchAuthenticatorAddr = bs.RollupConfig.BatchAuthenticatorAddress diff --git a/op-batcher/batcher/espresso_transaction_submitter_test.go b/op-batcher/batcher/espresso_transaction_submitter_test.go index 6c736bdbe3b..1e65887ee74 100644 --- a/op-batcher/batcher/espresso_transaction_submitter_test.go +++ b/op-batcher/batcher/espresso_transaction_submitter_test.go @@ -58,7 +58,9 @@ func TestEspressoTransactionSubmitterDeadlock(t *testing.T) { submitCtx, submitCancel := context.WithCancel(context.Background()) go (func(cancel context.CancelFunc, txn espressoCommon.Transaction) { - submitter.SubmitTransaction(&txn) + // The error is irrelevant here: the test only observes whether the + // call returns (via cancel) to detect a deadlock. + _ = submitter.SubmitTransaction(&txn) cancel() })(submitCancel, txn) diff --git a/op-node/rollup/derive/espresso_batch.go b/op-node/rollup/derive/espresso_batch.go index 9f7b6cb3475..74d5b447310 100644 --- a/op-node/rollup/derive/espresso_batch.go +++ b/op-node/rollup/derive/espresso_batch.go @@ -7,6 +7,7 @@ import ( espressoCommon "github.com/EspressoSystems/espresso-network/sdks/go/types" "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/bigs" opCrypto "github.com/ethereum-optimism/optimism/op-service/crypto" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" @@ -25,7 +26,7 @@ type EspressoBatch struct { } func (b EspressoBatch) Number() uint64 { - return b.BatchHeader.Number.Uint64() + return bigs.Uint64Strict(b.BatchHeader.Number) } func (b EspressoBatch) L1Origin() eth.BlockID { diff --git a/op-node/rollup/derive/espresso_batch_test.go b/op-node/rollup/derive/espresso_batch_test.go index f960fc72770..bed6e4856a5 100644 --- a/op-node/rollup/derive/espresso_batch_test.go +++ b/op-node/rollup/derive/espresso_batch_test.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup" derive "github.com/ethereum-optimism/optimism/op-node/rollup/derive" dtest "github.com/ethereum-optimism/optimism/op-node/rollup/derive/test" + "github.com/ethereum-optimism/optimism/op-service/bigs" "github.com/ethereum-optimism/optimism/op-service/crypto" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/signer" @@ -267,7 +268,7 @@ func TestBatchRoundtrip(t *testing.T) { transaction, err := batch.ToEspressoTransaction( context.Background(), - defaultTestRollUpConfig.L2ChainID.Uint64(), + bigs.Uint64Strict(defaultTestRollUpConfig.L2ChainID), chainSigner, ) require.NoError(t, err, "failed to serialize batch to Espresso transaction") From 4ece90988cec932b0f93da27c5ee6f4a8db4b8ec Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Mon, 6 Jul 2026 20:30:33 -0400 Subject: [PATCH 13/25] request to clear state from BlockLoader --- op-batcher/batcher/driver.go | 4 ++++ op-batcher/batcher/espresso.go | 32 ++++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 44de47e3047..4c5ea771d18 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -8,6 +8,7 @@ import ( "math/big" _ "net/http/pprof" "sync" + "sync/atomic" "time" "golang.org/x/sync/errgroup" @@ -158,6 +159,9 @@ type BatchSubmitter struct { espressoSubmitter *espressoTransactionSubmitter espressoStreamer espresso.EspressoStreamer[derive.EspressoBatch] + // clearStateRequested asks the espresso batch loading loop to run clearState + clearStateRequested atomic.Bool + teeVerifierAddress common.Address // degradedLog throttles repeated warnings from tick-driven loops so the diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go index 7052bb0d202..3c021b81085 100644 --- a/op-batcher/batcher/espresso.go +++ b/op-batcher/batcher/espresso.go @@ -909,6 +909,22 @@ func (l *BatchSubmitter) peekNextBatch(ctx context.Context, syncStatus *eth.Sync return batch } +// requestClearState asks the batch loading loop to perform `l.clearState`. +func (l *BatchSubmitter) requestClearState() { + l.clearStateRequested.Store(true) +} + +// performClearState runs clearState if it was requested via requestClearState, +// reporting whether a clear was performed. +func (l *BatchSubmitter) performClearState(ctx context.Context) bool { + if !l.clearStateRequested.CompareAndSwap(true, false) { + return false + } + l.Log.Info("Clearing state as requested by the block queueing loop") + l.clearState(ctx) + return true +} + // Periodically refreshes the sync status and polls Espresso streamer for new batches. // Owns publishSignal and unsafeBytesUpdated: it is their only closer, so the loops // ranging over them (publishingLoop, throttlingLoop) terminate when this loop exits. @@ -924,6 +940,9 @@ func (l *BatchSubmitter) espressoBatchLoadingLoop(ctx context.Context, wg *sync. for { select { case <-ticker.C: + // Check if block loader requested to clear state + l.performClearState(ctx) + newSyncStatus, err := l.getSyncStatus(ctx) if err != nil { l.degradedLog.Warn(l.Log, "syncStatusErr/espressoBatchLoading", "failed to refresh sync status", "err", err) @@ -938,6 +957,10 @@ func (l *BatchSubmitter) espressoBatchLoadingLoop(ctx context.Context, wg *sync. var batch *derive.EspressoBatch for { + // Check if block loader requested to clear state + if l.performClearState(ctx) { + break + } batch = l.peekNextBatch(ctx, newSyncStatus) @@ -975,6 +998,7 @@ func (l *BatchSubmitter) espressoBatchLoadingLoop(ctx context.Context, wg *sync. l.EspressoStreamer().Next(ctx) l.Log.Info("Added L2 block to channel manager", "blockNr", block.NumberU64()) // We have increased the unsafe data. Signal the throttling loop to + // check if it should throttle. l.sendToThrottlingLoop(unsafeBytesUpdated) } @@ -1000,10 +1024,10 @@ type BlockLoader struct { batcher *BatchSubmitter } -func (l *BlockLoader) reset(ctx context.Context) { +func (l *BlockLoader) reset() { l.prevSyncStatus = nil l.queuedBlocks = nil - l.batcher.clearState(ctx) + l.batcher.requestClearState() } func (l *BlockLoader) EnqueueBlocks(ctx context.Context, blocksToQueue inclusiveBlockRange) { @@ -1022,7 +1046,7 @@ func (l *BlockLoader) EnqueueBlocks(ctx context.Context, blocksToQueue inclusive if len(l.queuedBlocks) > 0 && block.ParentHash() != l.queuedBlocks[len(l.queuedBlocks)-1].Hash { l.batcher.Log.Warn("Found L2 reorg", "block_number", i) - l.reset(ctx) + l.reset() break } @@ -1195,7 +1219,7 @@ func (l *BatchSubmitter) espressoBatchQueueingLoop(ctx context.Context, wg *sync l.Log.Debug("Could not enqueue all blocks to Espresso", "enqueued", enqueued, "attempted", blocksToQueue.numBlocks()) } } else if action == ActionReset { - loader.reset(ctx) + loader.reset() } case <-ctx.Done(): From 293332992d19fba0b962ff49b398dad3ac07ab9b Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Thu, 16 Jul 2026 14:06:41 -0400 Subject: [PATCH 14/25] safety check for namespace and nil l1 deposit --- op-node/rollup/derive/espresso_batch.go | 10 ++++++++ op-node/rollup/derive/espresso_batch_test.go | 25 ++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/op-node/rollup/derive/espresso_batch.go b/op-node/rollup/derive/espresso_batch.go index 74d5b447310..5b9dc439f85 100644 --- a/op-node/rollup/derive/espresso_batch.go +++ b/op-node/rollup/derive/espresso_batch.go @@ -116,6 +116,16 @@ func UnmarshalEspressoTransaction(data []byte) (*EspressoBatch, error) { } batch.SignerAddress = signer + if batch.BatchHeader == nil || batch.BatchHeader.Number == nil { + return nil, fmt.Errorf("batch header is missing a block number") + } + if !batch.BatchHeader.Number.IsUint64() { + return nil, fmt.Errorf("batch header number %s does not fit in uint64", batch.BatchHeader.Number) + } + if batch.L1InfoDeposit == nil { + return nil, fmt.Errorf("batch is missing the L1 info deposit transaction") + } + return &batch, nil } diff --git a/op-node/rollup/derive/espresso_batch_test.go b/op-node/rollup/derive/espresso_batch_test.go index bed6e4856a5..9f7443a7444 100644 --- a/op-node/rollup/derive/espresso_batch_test.go +++ b/op-node/rollup/derive/espresso_batch_test.go @@ -21,6 +21,7 @@ import ( gethTypes "github.com/ethereum/go-ethereum/core/types" gethCrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/require" ) @@ -108,6 +109,30 @@ func TestUnmarshalEspressoTransactionTooShort(t *testing.T) { } } +// TestUnmarshalEspressoTransactionRejectsOversizedHeaderNumber verifies that a +// well-signed payload whose header number does not fit in uint64 is rejected at +// decode time. Posting to a namespace is permissionless and consumers call +// Number() before the signer is validated, so if such a batch survived +// unmarshaling it would panic in bigs.Uint64Strict. +func TestUnmarshalEspressoTransactionRejectsOversizedHeaderNumber(t *testing.T) { + rng := rand.New(rand.NewSource(2)) + block := dtest.RandomL2BlockWithChainIdAndTime(rng, 3, defaultTestRollUpConfig.L2ChainID, time.Now()) + batch, err := derive.BlockToEspressoBatch(defaultTestRollUpConfig, block) + require.NoError(t, err) + + batch.BatchHeader.Number = new(big.Int).Lsh(big.NewInt(1), 64) + + buf := new(bytes.Buffer) + require.NoError(t, rlp.Encode(buf, *batch)) + key, err := gethCrypto.GenerateKey() + require.NoError(t, err) + sig, err := gethCrypto.Sign(gethCrypto.Keccak256(buf.Bytes()), key) + require.NoError(t, err) + + _, err = derive.UnmarshalEspressoTransaction(append(sig, buf.Bytes()...)) + require.ErrorContains(t, err, "does not fit in uint64") +} + // TestEspressoBatchConversion tests the conversion of a block to an Espresso // Batch, and ensures that the recovery of the original Block is possible with // the contents of the Espresso Batch. From d9927014528db36721f78923902da50228111d43 Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Thu, 16 Jul 2026 15:22:07 -0400 Subject: [PATCH 15/25] remove duplicate files between espresso streamer and upstream --- espresso/interface.go | 77 ---- go.mod | 2 +- go.sum | 4 +- op-batcher/batcher/driver.go | 5 +- op-batcher/batcher/espresso.go | 7 +- op-batcher/batcher/espresso_driver.go | 7 +- op-batcher/batcher/espresso_service.go | 6 +- op-node/rollup/derive/espresso_batch.go | 173 --------- op-node/rollup/derive/espresso_batch_test.go | 382 ------------------- 9 files changed, 16 insertions(+), 647 deletions(-) delete mode 100644 espresso/interface.go delete mode 100644 op-node/rollup/derive/espresso_batch.go delete mode 100644 op-node/rollup/derive/espresso_batch_test.go diff --git a/espresso/interface.go b/espresso/interface.go deleted file mode 100644 index 8445baf08c6..00000000000 --- a/espresso/interface.go +++ /dev/null @@ -1,77 +0,0 @@ -package espresso - -import ( - "context" - - op "github.com/EspressoSystems/espresso-streamers/op" - "github.com/ethereum-optimism/optimism/op-service/eth" - "github.com/ethereum/go-ethereum/common" -) - -// EspressoStreamer defines the interface for the Espresso streamer. -type EspressoStreamer[B op.Batch] interface { - // Update will update the `EspressoStreamer“ by attempting to ensure that - // the next call to the `Next` method will return a `Batch`. - // - // It attempts to ensure the existence of a next batch, provided no errors - // occur when communicating with HotShot, by processing Blocks retrieved - // from `HotShot` in discreet batches. If each processing of a batch of - // blocks will not yield a new `Batch`, then it will continue to process - // the next batch of blocks from HotShot until it runs out of blocks to - // process. - // - // NOTE: this method is best effort. It is unable to guarantee that the - // next call to `Next` will return a batch. However, the only things - // that will prevent the next call to `Next` from returning a batch is if - // there are no more HotShot blocks to process currently, or if an error - // occurs when communicating with HotShot. - Update(ctx context.Context) error - - // Refresh updates the local references of the EspressoStreamer to the - // specified values. - // - // These values can be used to help determine whether the Streamer needs - // to be reset or not. - // - // NOTE: This will only automatically reset the Streamer if the - // `safeBatchNumber` moves backwards. - Refresh(ctx context.Context, finalizedL1 eth.L1BlockRef, safeBatchNumber uint64, safeL1Origin eth.BlockID) error - - // RefreshSafeL1Origin updates the safe L1 origin for the streamer. This is - // used to help the streamer determine if it needs to be reset or not based - // on the safe L1 origin moving backwards. - // - // NOTE: This will only automatically reset the Streamer if the - // `safeL1Origin` moves backwards. - RefreshSafeL1Origin(safeL1Origin eth.BlockID) - - // Reset will reset the Streamer to the last known good safe state. - // This generally means resetting to the last know good safe batch - // position, but in the case of consuming blocks from Espresso, it will - // also reset the starting Espresso block position to the last known - // good safe block position there as well. - Reset() - - // UnmarshalBatch is a convenience method that allows the caller to - // attempt to unmarshal a batch from the provided byte slice. - UnmarshalBatch(b []byte) (*B, error) - - // HasNext checks to see if there are any batches left to read in the - // streamer. - HasNext(ctx context.Context) bool - - // Next attempts to return the next batch from the streamer. If there - // are no batches left to read, at the moment of the call, it will return - // nil. - Next(ctx context.Context) *B - - // Peek attempts to return the next batch from the streamer without advancing the streamer's position. - // If there are no batches left to read, at the moment of the call, it will return nil. - Peek(ctx context.Context) *B - - // SetProperHead drains stale/wrong-fork entries from the buffer front, - // positioning it at the correct fork for the next Peek call. Should be called - // when Peek returns a batch whose parentHash doesn't match the current chain tip. - // No-ops if headBatch's block number doesn't match the expected next batch position. - SetProperHead(parentHash common.Hash) -} diff --git a/go.mod b/go.mod index a98c2518a8d..dfefbe22b90 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.24.13 require ( github.com/BurntSushi/toml v1.5.0 github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 - github.com/EspressoSystems/espresso-streamers v1.1.0 + github.com/EspressoSystems/espresso-streamers v1.2.1-0.20260716191442-1884a718fbf7 github.com/Masterminds/semver/v3 v3.3.1 github.com/andybalholm/brotli v1.1.0 github.com/base/go-bip39 v1.1.0 diff --git a/go.sum b/go.sum index 6e5dca219ba..a0e46cb0056 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e h1:ZIWapoIRN1VqT8GR github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 h1:4KqlmnDg8pPPj1hCtiLnN0Fh9ltp+PPKOft9I06zQOA= github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513/go.mod h1:kaxR08mJb5Mijy7a2RhWCIWOevFI4PcXwDkzoEbsVTk= -github.com/EspressoSystems/espresso-streamers v1.1.0 h1:+35Y9I3BCMyVIyFX6DNO6tq8ZKeuyOAve4DrgIhaY2s= -github.com/EspressoSystems/espresso-streamers v1.1.0/go.mod h1:eDGUEvlyxNey9gdpQhKaswAOp5ZWHPVbhNtWfHkpJvU= +github.com/EspressoSystems/espresso-streamers v1.2.1-0.20260716191442-1884a718fbf7 h1:PQVHO5oq8/WUjMwVkWB7MM0fqOmfCMQ72ak5fSl6L3s= +github.com/EspressoSystems/espresso-streamers v1.2.1-0.20260716191442-1884a718fbf7/go.mod h1:Op3SNwQnZ3bqwrUXMAORnL2/pNiFzpfOED4ltYs5o/U= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 4c5ea771d18..34cb2831a84 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -13,6 +13,8 @@ import ( "golang.org/x/sync/errgroup" + op "github.com/EspressoSystems/espresso-streamers/op" + "github.com/EspressoSystems/espresso-streamers/op/derivation" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -22,7 +24,6 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum-optimism/optimism/espresso" altda "github.com/ethereum-optimism/optimism/op-alt-da" "github.com/ethereum-optimism/optimism/op-batcher/batcher/throttler" config "github.com/ethereum-optimism/optimism/op-batcher/config" @@ -157,7 +158,7 @@ type BatchSubmitter struct { authGroup sync.WaitGroup espressoSubmitter *espressoTransactionSubmitter - espressoStreamer espresso.EspressoStreamer[derive.EspressoBatch] + espressoStreamer op.EspressoStreamer[derivation.EspressoBatch] // clearStateRequested asks the espresso batch loading loop to run clearState clearStateRequested atomic.Bool diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go index 3c021b81085..bcce9417e6d 100644 --- a/op-batcher/batcher/espresso.go +++ b/op-batcher/batcher/espresso.go @@ -18,6 +18,7 @@ import ( espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" tagged_base64 "github.com/EspressoSystems/espresso-network/sdks/go/tagged-base64" espressoCommon "github.com/EspressoSystems/espresso-network/sdks/go/types" + "github.com/EspressoSystems/espresso-streamers/op/derivation" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" @@ -810,7 +811,7 @@ func (s *espressoTransactionSubmitter) Start() { // Returns error only if batch conversion fails, otherwise it is infallible, as the goroutine // will retry publishing until successful. func (l *BatchSubmitter) queueBlockToEspresso(ctx context.Context, block *types.Block) error { - espressoBatch, err := derive.BlockToEspressoBatch(l.RollupConfig, block) + espressoBatch, err := derivation.BlockToEspressoBatch(l.RollupConfig, block) if err != nil { l.Log.Warn("Failed to derive batch from block", "err", err) return fmt.Errorf("failed to derive batch from block: %w", err) @@ -865,7 +866,7 @@ func (l *BatchSubmitter) espressoSyncAndRefresh(ctx context.Context, newSyncStat // The expected parent is tip when tip is set. When tip is zero (channel manager was // just cleared), we fall back to safeL2.Hash if the batch is at exactly safeL2+1 — // the one position where we can set tip to the known safe head. Otherwise we accept the batch as-is. -func (l *BatchSubmitter) peekNextBatch(ctx context.Context, syncStatus *eth.SyncStatus) *derive.EspressoBatch { +func (l *BatchSubmitter) peekNextBatch(ctx context.Context, syncStatus *eth.SyncStatus) *derivation.EspressoBatch { l.channelMgrMutex.Lock() tip := l.channelMgr.tip l.channelMgrMutex.Unlock() @@ -954,7 +955,7 @@ func (l *BatchSubmitter) espressoBatchLoadingLoop(ctx context.Context, wg *sync. err = l.EspressoStreamer().Update(ctx) - var batch *derive.EspressoBatch + var batch *derivation.EspressoBatch for { // Check if block loader requested to clear state diff --git a/op-batcher/batcher/espresso_driver.go b/op-batcher/batcher/espresso_driver.go index da6f22e629f..df82070a34a 100644 --- a/op-batcher/batcher/espresso_driver.go +++ b/op-batcher/batcher/espresso_driver.go @@ -8,11 +8,10 @@ import ( espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" op "github.com/EspressoSystems/espresso-streamers/op" + "github.com/EspressoSystems/espresso-streamers/op/derivation" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum-optimism/optimism/espresso" - "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-service/bigs" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" "github.com/ethereum-optimism/optimism/op-service/txmgr" @@ -56,7 +55,7 @@ func (a *batcherL1Adapter) CallContract(ctx context.Context, call ethereum.CallM } // EspressoStreamer returns the Espresso batch streamer for use by the service and tests. -func (l *BatchSubmitter) EspressoStreamer() espresso.EspressoStreamer[derive.EspressoBatch] { +func (l *BatchSubmitter) EspressoStreamer() op.EspressoStreamer[derivation.EspressoBatch] { return l.espressoStreamer } @@ -82,7 +81,7 @@ func (l *BatchSubmitter) setupEspressoStreamer() { l.Espresso.Client, lightClientIface, l.Log, - derive.CreateEspressoBatchUnmarshaler(), + derivation.CreateEspressoBatchUnmarshaler(), l.Config.Espresso.CaffeinationHeightEspresso, l.Config.Espresso.CaffeinationHeightL2, l.RollupConfig.BatchAuthenticatorAddress, diff --git a/op-batcher/batcher/espresso_service.go b/op-batcher/batcher/espresso_service.go index bc016244c2d..25102caa63f 100644 --- a/op-batcher/batcher/espresso_service.go +++ b/op-batcher/batcher/espresso_service.go @@ -7,14 +7,14 @@ import ( espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" + op "github.com/EspressoSystems/espresso-streamers/op" + "github.com/EspressoSystems/espresso-streamers/op/derivation" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/hf/nitrite" - "github.com/ethereum-optimism/optimism/espresso" "github.com/ethereum-optimism/optimism/op-batcher/enclave" - "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-service/bigs" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" ) @@ -55,7 +55,7 @@ type EspressoBatcherConfig struct { } // EspressoStreamer returns the Espresso batch streamer driven by this batcher. -func (bs *BatcherService) EspressoStreamer() espresso.EspressoStreamer[derive.EspressoBatch] { +func (bs *BatcherService) EspressoStreamer() op.EspressoStreamer[derivation.EspressoBatch] { return bs.driver.espressoStreamer } diff --git a/op-node/rollup/derive/espresso_batch.go b/op-node/rollup/derive/espresso_batch.go deleted file mode 100644 index 5b9dc439f85..00000000000 --- a/op-node/rollup/derive/espresso_batch.go +++ /dev/null @@ -1,173 +0,0 @@ -package derive - -import ( - "bytes" - "context" - "fmt" - - espressoCommon "github.com/EspressoSystems/espresso-network/sdks/go/types" - "github.com/ethereum-optimism/optimism/op-node/rollup" - "github.com/ethereum-optimism/optimism/op-service/bigs" - opCrypto "github.com/ethereum-optimism/optimism/op-service/crypto" - "github.com/ethereum-optimism/optimism/op-service/eth" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/rlp" -) - -// A SingularBatch with block number attached to restore ordering -// when fetching from Espresso -type EspressoBatch struct { - BatchHeader *types.Header - Batch SingularBatch - L1InfoDeposit *types.Transaction - SignerAddress common.Address -} - -func (b EspressoBatch) Number() uint64 { - return bigs.Uint64Strict(b.BatchHeader.Number) -} - -func (b EspressoBatch) L1Origin() eth.BlockID { - return b.Batch.Epoch() -} - -func (b EspressoBatch) Header() *types.Header { - return b.BatchHeader -} - -func (b EspressoBatch) Hash() common.Hash { - hash := crypto.Keccak256Hash(b.BatchHeader.Hash().Bytes(), b.L1InfoDeposit.Hash().Bytes()) - return hash -} - -func (b EspressoBatch) Signer() common.Address { - return b.SignerAddress -} - -func (b *EspressoBatch) ToEspressoTransaction(ctx context.Context, namespace uint64, signer opCrypto.ChainSigner) (*espressoCommon.Transaction, error) { - buf := new(bytes.Buffer) - err := rlp.Encode(buf, *b) - if err != nil { - return nil, fmt.Errorf("failed to encode batch: %w", err) - } - - batcherSignature, err := signer.Sign(ctx, crypto.Keccak256(buf.Bytes())) - - if err != nil { - return nil, fmt.Errorf("failed to create batcher signature: %w", err) - } - - payload := append(batcherSignature, buf.Bytes()...) - - return &espressoCommon.Transaction{Namespace: namespace, Payload: payload}, nil - -} - -func BlockToEspressoBatch(rollupCfg *rollup.Config, block *types.Block) (*EspressoBatch, error) { - if len(block.Transactions()) == 0 { - return nil, fmt.Errorf("Block doesn't contain any transactions") - } - - l1InfoDeposit := block.Transactions()[0] - if !l1InfoDeposit.IsDepositTx() { - return nil, fmt.Errorf("First transaction is not L1 info deposit") - } - - batch, _, err := BlockToSingularBatch(rollupCfg, block) - if err != nil { - return nil, err - } - - return &EspressoBatch{ - BatchHeader: block.Header(), - Batch: *batch, - L1InfoDeposit: l1InfoDeposit, - }, nil -} - -// CreateEspressoBatchUnmarshaler returns a function that can be used to -// unmarshal an Espresso transaction into an EspressoBatch. -// The signer address is recovered from the signature and stored on the batch -// for later verification in CheckBatch (two-phase verification). -func CreateEspressoBatchUnmarshaler() func(data []byte) (*EspressoBatch, error) { - return func(data []byte) (*EspressoBatch, error) { - return UnmarshalEspressoTransaction(data) - } -} - -func UnmarshalEspressoTransaction(data []byte) (*EspressoBatch, error) { - if len(data) < crypto.SignatureLength { - return nil, fmt.Errorf("transaction data too short: %d bytes, need at least %d", len(data), crypto.SignatureLength) - } - signatureData, batchData := data[:crypto.SignatureLength], data[crypto.SignatureLength:] - batchHash := crypto.Keccak256(batchData) - - signerKey, err := crypto.SigToPub(batchHash, signatureData) - if err != nil { - return nil, err - } - signer := crypto.PubkeyToAddress(*signerKey) - - var batch EspressoBatch - if err := rlp.DecodeBytes(batchData, &batch); err != nil { - return nil, err - } - batch.SignerAddress = signer - - if batch.BatchHeader == nil || batch.BatchHeader.Number == nil { - return nil, fmt.Errorf("batch header is missing a block number") - } - if !batch.BatchHeader.Number.IsUint64() { - return nil, fmt.Errorf("batch header number %s does not fit in uint64", batch.BatchHeader.Number) - } - if batch.L1InfoDeposit == nil { - return nil, fmt.Errorf("batch is missing the L1 info deposit transaction") - } - - return &batch, nil -} - -// NOTE: This function MUST guarantee no transient errors. It is allowed to fail only on -// invalid batches or in case of misconfiguration of the batcher, in which case it should fail -// for all batches. -func (b *EspressoBatch) ToBlock(rollupCfg *rollup.Config) (*types.Block, error) { - // The produced block must round-trip through BlockToSingularBatch when the channel - // manager encodes it, so enforce that function's requirements up front, plus - // consistency between the header and the batch body it claims to describe. - if b.BatchHeader == nil { - return nil, fmt.Errorf("batch has no header") - } - if b.L1InfoDeposit == nil || !b.L1InfoDeposit.IsDepositTx() { - return nil, fmt.Errorf("first transaction is not an L1 info deposit") - } - l1Info, err := L1BlockInfoFromBytes(rollupCfg, b.BatchHeader.Time, b.L1InfoDeposit.Data()) - if err != nil { - return nil, fmt.Errorf("could not parse the L1 info deposit: %w", err) - } - if b.Batch.ParentHash != b.BatchHeader.ParentHash { - return nil, fmt.Errorf("batch parent hash %s does not match header parent hash %s", b.Batch.ParentHash, b.BatchHeader.ParentHash) - } - if b.Batch.Timestamp != b.BatchHeader.Time { - return nil, fmt.Errorf("batch timestamp %d does not match header timestamp %d", b.Batch.Timestamp, b.BatchHeader.Time) - } - if uint64(b.Batch.EpochNum) != l1Info.Number || b.Batch.EpochHash != l1Info.BlockHash { - return nil, fmt.Errorf("batch epoch %d (%s) does not match L1 info deposit epoch %d (%s)", - b.Batch.EpochNum, b.Batch.EpochHash, l1Info.Number, l1Info.BlockHash) - } - - // Re-insert the deposit transaction - txs := []*types.Transaction{b.L1InfoDeposit} - for i, opaqueTx := range b.Batch.Transactions { - var tx types.Transaction - err := tx.UnmarshalBinary(opaqueTx) - if err != nil { - return nil, fmt.Errorf("could not decode tx %d: %w", i, err) - } - txs = append(txs, &tx) - } - return types.NewBlockWithHeader(b.BatchHeader).WithBody(types.Body{ - Transactions: txs, - }), nil -} diff --git a/op-node/rollup/derive/espresso_batch_test.go b/op-node/rollup/derive/espresso_batch_test.go deleted file mode 100644 index 9f7443a7444..00000000000 --- a/op-node/rollup/derive/espresso_batch_test.go +++ /dev/null @@ -1,382 +0,0 @@ -package derive_test - -import ( - "bytes" - "context" - "math/big" - "math/rand" - "slices" - "testing" - "time" - - "github.com/ethereum-optimism/optimism/op-node/rollup" - derive "github.com/ethereum-optimism/optimism/op-node/rollup/derive" - dtest "github.com/ethereum-optimism/optimism/op-node/rollup/derive/test" - "github.com/ethereum-optimism/optimism/op-service/bigs" - "github.com/ethereum-optimism/optimism/op-service/crypto" - "github.com/ethereum-optimism/optimism/op-service/eth" - "github.com/ethereum-optimism/optimism/op-service/signer" - "github.com/ethereum-optimism/optimism/op-service/testlog" - "github.com/ethereum/go-ethereum/common" - gethTypes "github.com/ethereum/go-ethereum/core/types" - gethCrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rlp" - "github.com/stretchr/testify/require" -) - -const ( - testMnemonic = "test test test test test test test test test test test junk" - testHDPath = "m/44'/60'/0'/0/1" -) - -var defaultTestRollUpConfig = &rollup.Config{ - Genesis: rollup.Genesis{L2: eth.BlockID{Number: 0}}, - L2ChainID: big.NewInt(1234), -} - -// compareHash is a helper function that compares two hashes. -func compareHash(a, b common.Hash) int { - if c := bytes.Compare(a[:], b[:]); c != 0 { - return c - } - return 0 -} - -// compareTransaction is a helper function that compares two transactions -// by only inspecting their hashes. -func compareTransaction(a, b *gethTypes.Transaction) int { - return compareHash(a.Hash(), b.Hash()) -} - -// compareHeader is a helper function that compares two headers -// by only inspecting their hashes. -func compareHeader(a, b *gethTypes.Header) int { - return compareHash(a.Hash(), b.Hash()) -} - -// compareWithdrawl is a helper function that compares two withdrawals -// by checking that their slice members compare equivalently. -func compareWithdrawl(a, b *gethTypes.Withdrawal) int { - if c := a.Index - b.Index; c != 0 { - return int(c) - } - - if c := a.Validator - b.Validator; c != 0 { - return int(c) - } - - if c := a.Address.Cmp(b.Address); c != 0 { - return c - } - - if c := a.Amount - b.Amount; c != 0 { - return int(c) - } - - return 0 -} - -// compareBody is a helper function that compares two bodies -// by checking that their slice members compare equivalently. -func compareBody(a, b *gethTypes.Body) int { - if c := slices.CompareFunc(a.Transactions, b.Transactions, compareTransaction); c != 0 { - return c - } - - if c := slices.CompareFunc(a.Uncles, b.Uncles, compareHeader); c != 0 { - return c - } - - if c := slices.CompareFunc(a.Withdrawals, b.Withdrawals, compareWithdrawl); c != 0 { - return c - } - - return 0 -} - -// TestUnmarshalEspressoTransactionTooShort verifies that UnmarshalEspressoTransaction -// returns an error (rather than panicking) when the input is shorter than a signature. -func TestUnmarshalEspressoTransactionTooShort(t *testing.T) { - cases := [][]byte{ - nil, - {}, - make([]byte, gethCrypto.SignatureLength-1), - } - for _, data := range cases { - _, err := derive.UnmarshalEspressoTransaction(data) - require.Error(t, err, "expected error for %d-byte input", len(data)) - } -} - -// TestUnmarshalEspressoTransactionRejectsOversizedHeaderNumber verifies that a -// well-signed payload whose header number does not fit in uint64 is rejected at -// decode time. Posting to a namespace is permissionless and consumers call -// Number() before the signer is validated, so if such a batch survived -// unmarshaling it would panic in bigs.Uint64Strict. -func TestUnmarshalEspressoTransactionRejectsOversizedHeaderNumber(t *testing.T) { - rng := rand.New(rand.NewSource(2)) - block := dtest.RandomL2BlockWithChainIdAndTime(rng, 3, defaultTestRollUpConfig.L2ChainID, time.Now()) - batch, err := derive.BlockToEspressoBatch(defaultTestRollUpConfig, block) - require.NoError(t, err) - - batch.BatchHeader.Number = new(big.Int).Lsh(big.NewInt(1), 64) - - buf := new(bytes.Buffer) - require.NoError(t, rlp.Encode(buf, *batch)) - key, err := gethCrypto.GenerateKey() - require.NoError(t, err) - sig, err := gethCrypto.Sign(gethCrypto.Keccak256(buf.Bytes()), key) - require.NoError(t, err) - - _, err = derive.UnmarshalEspressoTransaction(append(sig, buf.Bytes()...)) - require.ErrorContains(t, err, "does not fit in uint64") -} - -// TestEspressoBatchConversion tests the conversion of a block to an Espresso -// Batch, and ensures that the recovery of the original Block is possible with -// the contents of the Espresso Batch. -func TestEspressoBatchConversion(t *testing.T) { - rng := rand.New(rand.NewSource(4982432)) - ti := time.Now() - - originalBlock := dtest.RandomL2BlockWithChainIdAndTime(rng, rng.Intn(32), defaultTestRollUpConfig.L2ChainID, ti) - - espressoBatch, err := derive.BlockToEspressoBatch(defaultTestRollUpConfig, originalBlock) - if have, want := err, error(nil); have != want { - t.Fatalf("failed to convert block to batch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - decodedBlock, err := espressoBatch.ToBlock(defaultTestRollUpConfig) - if have, want := err, error(nil); have != want { - t.Fatalf("failed to decode batch back to block:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - // Let's perform a sanity check on the decoded block to ensure that all of - // the fields match the original block. - - if have, want := decodedBlock.BaseFee(), originalBlock.BaseFee(); have.Cmp(want) != 0 { - t.Errorf("decoded block base fee mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.BeaconRoot(), originalBlock.BeaconRoot(); have != want { - t.Errorf("decoded block beacon root mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.BlobGasUsed(), originalBlock.BlobGasUsed(); have != want { - t.Errorf("decoded block blob gas used mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Bloom(), originalBlock.Bloom(); have != want { - t.Errorf("decoded block bloom mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Body(), originalBlock.Body(); compareBody(have, want) != 0 { - t.Errorf("decoded block body mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Coinbase(), originalBlock.Coinbase(); have != want { - t.Errorf("decoded block coinbase mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Difficulty(), originalBlock.Difficulty(); have.Cmp(want) != 0 { - t.Errorf("decoded block difficulty mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.ExcessBlobGas(), originalBlock.ExcessBlobGas(); have != want { - t.Errorf("decoded block excess blob gas mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Extra(), originalBlock.Extra(); !bytes.Equal(have, want) { - t.Errorf("decoded block extra mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.GasLimit(), originalBlock.GasLimit(); have != want { - t.Errorf("decoded block gas limit mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.GasUsed(), originalBlock.GasUsed(); have != want { - t.Errorf("decoded block gas used mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Hash(), originalBlock.Hash(); have != want { - t.Errorf("decoded block hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Header(), originalBlock.Header(); compareHeader(have, want) != 0 { - t.Errorf("decoded block header mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.MixDigest(), originalBlock.MixDigest(); have != want { - t.Errorf("decoded block mix digest mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Nonce(), originalBlock.Nonce(); have != want { - t.Errorf("decoded block nonce mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Number(), originalBlock.Number(); have.Cmp(want) != 0 { - t.Errorf("decoded block number mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.NumberU64(), originalBlock.NumberU64(); have != want { - t.Errorf("decoded block number u64 mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.ParentHash(), originalBlock.ParentHash(); have != want { - t.Errorf("decoded block parent hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.ReceiptHash(), originalBlock.ReceiptHash(); have != want { - t.Errorf("decoded block receipt hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.RequestsHash(), originalBlock.RequestsHash(); have != want { - t.Errorf("decoded block requests hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Root(), originalBlock.Root(); have != want { - t.Errorf("decoded block root mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Size(), originalBlock.Size(); have != want { - t.Errorf("decoded block size mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Time(), originalBlock.Time(); have != want { - t.Errorf("decoded block time mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Transactions(), originalBlock.Transactions(); slices.CompareFunc(have, want, compareTransaction) != 0 { - t.Errorf("decoded block transactions mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.TxHash(), originalBlock.TxHash(); have != want { - t.Errorf("decoded block tx hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.UncleHash(), originalBlock.UncleHash(); have != want { - t.Errorf("decoded block uncle hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.Withdrawals(), originalBlock.Withdrawals(); slices.CompareFunc(have, want, compareWithdrawl) != 0 { - t.Errorf("decoded block withdrawals mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } - - if have, want := decodedBlock.WithdrawalsRoot(), originalBlock.WithdrawalsRoot(); have != want { - t.Errorf("decoded block withdrawals root mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) - } -} - -// TestBatchRoundtrip exercises the batcher serialization path -// (BlockToEspressoBatch -> ToEspressoTransaction) against the derivation -// deserialization path (UnmarshalEspressoTransaction): a block packed and signed -// by the batcher must decode back to an equivalent batch, and the signer address -// recovered from the payload signature must match the batcher's address. -func TestBatchRoundtrip(t *testing.T) { - rng := rand.New(rand.NewSource(1)) - - originalBlock := dtest.RandomL2BlockWithChainIdAndTime(rng, 10, defaultTestRollUpConfig.L2ChainID, time.Now()) - - batch, err := derive.BlockToEspressoBatch(defaultTestRollUpConfig, originalBlock) - require.NoError(t, err, "failed to convert block to batch") - - signerFactory, batcherAddress, err := crypto.ChainSignerFactoryFromConfig( - testlog.Logger(t, log.LevelDebug), - "", - testMnemonic, - testHDPath, - signer.NewCLIConfig(), - ) - require.NoError(t, err, "failed to build chain signer factory") - chainSigner := signerFactory(defaultTestRollUpConfig.L2ChainID, batcherAddress) - - transaction, err := batch.ToEspressoTransaction( - context.Background(), - bigs.Uint64Strict(defaultTestRollUpConfig.L2ChainID), - chainSigner, - ) - require.NoError(t, err, "failed to serialize batch to Espresso transaction") - - decodedBatch, err := derive.UnmarshalEspressoTransaction(transaction.Payload) - require.NoError(t, err, "failed to deserialize Espresso transaction back to batch") - - // The signer recovered from the payload signature must be the batcher. - require.Equal(t, batcherAddress, decodedBatch.SignerAddress, "recovered signer address mismatch") - - // The decoded batch must be equivalent to the original (the recovered - // SignerAddress is populated only on decode, so compare the encoded fields). - require.Equal(t, 0, compareHeader(decodedBatch.BatchHeader, batch.BatchHeader), "decoded batch header mismatch") - require.Equal(t, 0, compareTransaction(decodedBatch.L1InfoDeposit, batch.L1InfoDeposit), "decoded batch L1 info deposit mismatch") - require.Equal(t, batch.Batch.EpochNum, decodedBatch.Batch.EpochNum, "decoded batch epoch num mismatch") - require.Equal(t, batch.Batch.EpochHash, decodedBatch.Batch.EpochHash, "decoded batch epoch hash mismatch") - require.Equal(t, batch.Batch.Timestamp, decodedBatch.Batch.Timestamp, "decoded batch timestamp mismatch") - require.Equal(t, batch.Batch.Transactions, decodedBatch.Batch.Transactions, "decoded batch transactions mismatch") -} - -// TestToBlockRejectsMalformedBatch verifies that ToBlock rejects validly-structured but -// semantically malformed batches. -func TestToBlockRejectsMalformedBatch(t *testing.T) { - validBatch := func(t *testing.T) *derive.EspressoBatch { - block := dtest.RandomL2BlockWithChainIdAndTime( - rand.New(rand.NewSource(time.Now().Unix())), - 3, - defaultTestRollUpConfig.L2ChainID, - time.Now(), - ) - b, err := derive.BlockToEspressoBatch(defaultTestRollUpConfig, block) - require.NoError(t, err) - return b - } - - t.Run("valid batch converts", func(t *testing.T) { - b := validBatch(t) - _, err := b.ToBlock(defaultTestRollUpConfig) - require.NoError(t, err) - }) - - t.Run("nil L1 info deposit", func(t *testing.T) { - b := validBatch(t) - b.L1InfoDeposit = nil - _, err := b.ToBlock(defaultTestRollUpConfig) - require.ErrorContains(t, err, "not an L1 info deposit") - }) - - t.Run("first tx not a deposit", func(t *testing.T) { - b := validBatch(t) - // Replace the L1 info deposit with a decoded non-deposit tx from the batch body. - var nonDeposit gethTypes.Transaction - require.NoError(t, nonDeposit.UnmarshalBinary(b.Batch.Transactions[0])) - b.L1InfoDeposit = &nonDeposit - _, err := b.ToBlock(defaultTestRollUpConfig) - require.ErrorContains(t, err, "not an L1 info deposit") - }) - - t.Run("malformed L1 info data", func(t *testing.T) { - b := validBatch(t) - b.L1InfoDeposit = gethTypes.NewTx(&gethTypes.DepositTx{Data: []byte{0xde, 0xad, 0xbe, 0xef}}) - _, err := b.ToBlock(defaultTestRollUpConfig) - require.ErrorContains(t, err, "could not parse the L1 info deposit") - }) - - t.Run("parent hash mismatch", func(t *testing.T) { - b := validBatch(t) - b.Batch.ParentHash[0] ^= 0xff - _, err := b.ToBlock(defaultTestRollUpConfig) - require.ErrorContains(t, err, "parent hash") - }) - - t.Run("timestamp mismatch", func(t *testing.T) { - b := validBatch(t) - b.Batch.Timestamp++ - _, err := b.ToBlock(defaultTestRollUpConfig) - require.ErrorContains(t, err, "timestamp") - }) - - t.Run("epoch mismatch", func(t *testing.T) { - b := validBatch(t) - b.Batch.EpochNum++ - _, err := b.ToBlock(defaultTestRollUpConfig) - require.ErrorContains(t, err, "epoch") - }) -} From b1ca88741b9e9ccd3b54ddeb7f6e956200234786 Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Thu, 16 Jul 2026 15:34:40 -0400 Subject: [PATCH 16/25] address comments --- espresso/cli.go | 8 ++++---- op-batcher/batcher/driver.go | 4 ++-- op-batcher/batcher/espresso.go | 22 ++++++++-------------- op-batcher/batcher/espresso_driver.go | 10 +++++----- op-batcher/batcher/espresso_service.go | 4 ++-- 5 files changed, 21 insertions(+), 27 deletions(-) diff --git a/espresso/cli.go b/espresso/cli.go index 9d1772ecf53..b16fc7f01c4 100644 --- a/espresso/cli.go +++ b/espresso/cli.go @@ -6,7 +6,7 @@ import ( "strings" "time" - op "github.com/EspressoSystems/espresso-streamers/op" + espressoStreamers "github.com/EspressoSystems/espresso-streamers/op" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" @@ -263,11 +263,11 @@ func ReadCLIConfig(c *cli.Context) CLIConfig { return config } -func BatchStreamerFromCLIConfig[B op.Batch]( +func BatchStreamerFromCLIConfig[B espressoStreamers.Batch]( cfg CLIConfig, log log.Logger, unmarshalBatch func([]byte) (*B, error), -) (*op.BatchStreamer[B], error) { +) (*espressoStreamers.BatchStreamer[B], error) { if !cfg.Enabled { return nil, fmt.Errorf("espresso is not enabled") } @@ -290,7 +290,7 @@ func BatchStreamerFromCLIConfig[B op.Batch]( return nil, fmt.Errorf("failed to create Espresso light client") } - return op.NewEspressoStreamer( + return espressoStreamers.NewEspressoStreamer( cfg.Namespace, NewAdaptL1BlockRefClient(l1Client), NewAdaptL1BlockRefClient(RollupL1Client), diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 34cb2831a84..29d0d16e322 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -13,7 +13,7 @@ import ( "golang.org/x/sync/errgroup" - op "github.com/EspressoSystems/espresso-streamers/op" + espressoStreamers "github.com/EspressoSystems/espresso-streamers/op" "github.com/EspressoSystems/espresso-streamers/op/derivation" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -158,7 +158,7 @@ type BatchSubmitter struct { authGroup sync.WaitGroup espressoSubmitter *espressoTransactionSubmitter - espressoStreamer op.EspressoStreamer[derivation.EspressoBatch] + espressoStreamer espressoStreamers.EspressoStreamer[derivation.EspressoBatch] // clearStateRequested asks the espresso batch loading loop to run clearState clearStateRequested atomic.Bool diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go index bcce9417e6d..6fa34232339 100644 --- a/op-batcher/batcher/espresso.go +++ b/op-batcher/batcher/espresso.go @@ -636,11 +636,12 @@ func espressoSubmitTransactionWorker( ctx, cancel := context.WithCancel(ctx) defer cancel() defer wg.Done() + // The scheduler sends on ch, so the worker must not close it: a deferred + // close here races the scheduler's send on shutdown. Both sides exit on + // ctx.Done() instead and the channel is simply garbage collected. ch := make(chan espressoTransactionJobAttempt) - defer close(ch) for { - var ok bool select { case <-ctx.Done(): return @@ -654,11 +655,7 @@ func espressoSubmitTransactionWorker( select { case <-ctx.Done(): return - case jobAttempt, ok = <-ch: - if !ok { - // Our channel is closed, and we are done - return - } + case jobAttempt = <-ch: } // Submit the transaction to Espresso @@ -701,11 +698,12 @@ func espressoVerifyTransactionWorker( ctx, cancel := context.WithCancel(ctx) defer cancel() defer wg.Done() + // The scheduler sends on ch, so the worker must not close it: a deferred + // close here races the scheduler's send on shutdown. Both sides exit on + // ctx.Done() instead and the channel is simply garbage collected. ch := make(chan espressoVerifyReceiptJobAttempt) - defer close(ch) for { - var ok bool select { case <-ctx.Done(): return @@ -719,11 +717,7 @@ func espressoVerifyTransactionWorker( select { case <-ctx.Done(): return - case jobAttempt, ok = <-ch: - if !ok { - // Our channel is closed, and we are done - return - } + case jobAttempt = <-ch: } // On the first attempt, snapshot the current block height so we diff --git a/op-batcher/batcher/espresso_driver.go b/op-batcher/batcher/espresso_driver.go index df82070a34a..03ac3a52bb2 100644 --- a/op-batcher/batcher/espresso_driver.go +++ b/op-batcher/batcher/espresso_driver.go @@ -7,7 +7,7 @@ import ( espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" - op "github.com/EspressoSystems/espresso-streamers/op" + espressoStreamers "github.com/EspressoSystems/espresso-streamers/op" "github.com/EspressoSystems/espresso-streamers/op/derivation" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" @@ -55,7 +55,7 @@ func (a *batcherL1Adapter) CallContract(ctx context.Context, call ethereum.CallM } // EspressoStreamer returns the Espresso batch streamer for use by the service and tests. -func (l *BatchSubmitter) EspressoStreamer() op.EspressoStreamer[derivation.EspressoBatch] { +func (l *BatchSubmitter) EspressoStreamer() espressoStreamers.EspressoStreamer[derivation.EspressoBatch] { return l.espressoStreamer } @@ -70,11 +70,11 @@ func (l *BatchSubmitter) setupEspressoStreamer() { l1Adapter := &batcherL1Adapter{L1Client: l.L1Client} // Convert typed nil pointer to untyped nil interface to avoid typed-nil interface panic // in confirmEspressoBlockHeight when EspressoLightClient is not configured. - var lightClientIface op.LightClientCallerInterface + var lightClientIface espressoStreamers.LightClientCallerInterface if l.Espresso.LightClient != nil { lightClientIface = l.Espresso.LightClient } - unbufferedStreamer, err := op.NewEspressoStreamer( + unbufferedStreamer, err := espressoStreamers.NewEspressoStreamer( bigs.Uint64Strict(l.RollupConfig.L2ChainID), l1Adapter, l1Adapter, @@ -90,7 +90,7 @@ func (l *BatchSubmitter) setupEspressoStreamer() { if err != nil { panic(fmt.Sprintf("failed to create Espresso streamer: %v", err)) } - l.espressoStreamer = op.NewBufferedEspressoStreamer(unbufferedStreamer) + l.espressoStreamer = espressoStreamers.NewBufferedEspressoStreamer(unbufferedStreamer) l.Log.Info("Streamer started", "streamer", l.espressoStreamer) } diff --git a/op-batcher/batcher/espresso_service.go b/op-batcher/batcher/espresso_service.go index 25102caa63f..baf99c89e1e 100644 --- a/op-batcher/batcher/espresso_service.go +++ b/op-batcher/batcher/espresso_service.go @@ -7,7 +7,7 @@ import ( espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" - op "github.com/EspressoSystems/espresso-streamers/op" + espressoStreamers "github.com/EspressoSystems/espresso-streamers/op" "github.com/EspressoSystems/espresso-streamers/op/derivation" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -55,7 +55,7 @@ type EspressoBatcherConfig struct { } // EspressoStreamer returns the Espresso batch streamer driven by this batcher. -func (bs *BatcherService) EspressoStreamer() op.EspressoStreamer[derivation.EspressoBatch] { +func (bs *BatcherService) EspressoStreamer() espressoStreamers.EspressoStreamer[derivation.EspressoBatch] { return bs.driver.espressoStreamer } From 922c6bc1591f4dee0928b8909c3677fb9f11dcdf Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Thu, 16 Jul 2026 15:37:30 -0400 Subject: [PATCH 17/25] remove uneeded comments --- op-batcher/batcher/espresso.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go index 6fa34232339..23058faede4 100644 --- a/op-batcher/batcher/espresso.go +++ b/op-batcher/batcher/espresso.go @@ -636,9 +636,6 @@ func espressoSubmitTransactionWorker( ctx, cancel := context.WithCancel(ctx) defer cancel() defer wg.Done() - // The scheduler sends on ch, so the worker must not close it: a deferred - // close here races the scheduler's send on shutdown. Both sides exit on - // ctx.Done() instead and the channel is simply garbage collected. ch := make(chan espressoTransactionJobAttempt) for { @@ -698,9 +695,6 @@ func espressoVerifyTransactionWorker( ctx, cancel := context.WithCancel(ctx) defer cancel() defer wg.Done() - // The scheduler sends on ch, so the worker must not close it: a deferred - // close here races the scheduler's send on shutdown. Both sides exit on - // ctx.Done() instead and the channel is simply garbage collected. ch := make(chan espressoVerifyReceiptJobAttempt) for { From 1168c18581b13b830d662d13810f29b05fa2110a Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Thu, 16 Jul 2026 18:42:47 -0400 Subject: [PATCH 18/25] update unsafe bytes every 100 blocks. matching non espresso path --- op-batcher/batcher/espresso.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go index 23058faede4..f89819fd453 100644 --- a/op-batcher/batcher/espresso.go +++ b/op-batcher/batcher/espresso.go @@ -944,6 +944,7 @@ func (l *BatchSubmitter) espressoBatchLoadingLoop(ctx context.Context, wg *sync. err = l.EspressoStreamer().Update(ctx) var batch *derivation.EspressoBatch + blocksAdded := 0 for { // Check if block loader requested to clear state @@ -986,11 +987,16 @@ func (l *BatchSubmitter) espressoBatchLoadingLoop(ctx context.Context, wg *sync. l.EspressoStreamer().Next(ctx) l.Log.Info("Added L2 block to channel manager", "blockNr", block.NumberU64()) - // We have increased the unsafe data. Signal the throttling loop to - // check if it should throttle. - l.sendToThrottlingLoop(unsafeBytesUpdated) + + // During a large drain, signal periodically so throttling can engage + // before the whole backlog is consumed (mirrors loadBlocksIntoState). + blocksAdded++ + if blocksAdded%100 == 0 { + l.sendToThrottlingLoop(unsafeBytesUpdated) + } } + l.sendToThrottlingLoop(unsafeBytesUpdated) l.tryPublishSignal(publishSignal, pubInfo{}) // A failure in the streamer Update can happen after the buffer has been partially filled From ad7431a72fc15d88bbf4379cf44cc127445ed54e Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Thu, 16 Jul 2026 22:20:07 -0400 Subject: [PATCH 19/25] always set the prev sync status --- op-batcher/batcher/espresso.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go index f89819fd453..1bdda9d6766 100644 --- a/op-batcher/batcher/espresso.go +++ b/op-batcher/batcher/espresso.go @@ -1087,17 +1087,15 @@ func (l *BlockLoader) nextBlockRange(newSyncStatus *eth.SyncStatus) (inclusiveBl return inclusiveBlockRange{}, ActionRetry } - if l.prevSyncStatus == nil { - l.prevSyncStatus = newSyncStatus - } - - if newSyncStatus.CurrentL1.Number < l.prevSyncStatus.CurrentL1.Number { - // sequencer restarted and hasn't caught up yet + if l.prevSyncStatus != nil && newSyncStatus.CurrentL1.Number < l.prevSyncStatus.CurrentL1.Number { + // Sequencer restarted and hasn't caught up yet l.batcher.degradedLog.Warn(l.batcher.Log, "sequencerCurrentL1Reversed", "sequencer currentL1 reversed", "new currentL1", newSyncStatus.CurrentL1.Number, "previous currentL1", l.prevSyncStatus.CurrentL1.Number) return inclusiveBlockRange{}, ActionRetry } l.batcher.degradedLog.Clear(l.batcher.Log, "sequencerCurrentL1Reversed", "sequencer currentL1 caught up") + l.prevSyncStatus = newSyncStatus + safeL2 := newSyncStatus.SafeL2 // State empty, just enqueue all unsafe blocks From ff4bcaaf187d56fab99f28cd87fdf04e9cacf192 Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Mon, 20 Jul 2026 14:33:59 -0400 Subject: [PATCH 20/25] log at warn level if batcher isnt active --- op-batcher/batcher/espresso_active.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-batcher/batcher/espresso_active.go b/op-batcher/batcher/espresso_active.go index e9c83a7b228..964e2b1b4ea 100644 --- a/op-batcher/batcher/espresso_active.go +++ b/op-batcher/batcher/espresso_active.go @@ -50,7 +50,7 @@ func (l *BatchSubmitter) isBatcherActive(ctx context.Context) (bool, error) { (!activeIsEspresso && !l.Config.Espresso.Enabled) if !isActive { - l.Log.Info("Batcher is not the active batcher, skipping publish", + l.Log.Warn("Batcher is not the active batcher, skipping publish", "batcherAddr", batcherAddr, "activeIsEspresso", activeIsEspresso, "EspressoEnabled", l.Config.Espresso.Enabled, From a243125f1a4f1f847b243704beb8377bf468cbbd Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Tue, 21 Jul 2026 09:51:59 -0400 Subject: [PATCH 21/25] check the batcher address before publishing to avoid reverts --- espresso/bindings/system_config.go | 2640 +++++++++++++++++++++++++ op-batcher/batcher/espresso_active.go | 65 +- 2 files changed, 2695 insertions(+), 10 deletions(-) create mode 100644 espresso/bindings/system_config.go diff --git a/espresso/bindings/system_config.go b/espresso/bindings/system_config.go new file mode 100644 index 00000000000..0498555e156 --- /dev/null +++ b/espresso/bindings/system_config.go @@ -0,0 +1,2640 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// IResourceMeteringResourceConfig is an auto generated low-level Go binding around an user-defined struct. +type IResourceMeteringResourceConfig struct { + MaxResourceLimit uint32 + ElasticityMultiplier uint8 + BaseFeeMaxChangeDenominator uint8 + MinimumBaseFee uint32 + SystemTxMaxGas uint32 + MaximumBaseFee *big.Int +} + +// SystemConfigAddresses is an auto generated low-level Go binding around an user-defined struct. +type SystemConfigAddresses struct { + L1CrossDomainMessenger common.Address + L1ERC721Bridge common.Address + L1StandardBridge common.Address + OptimismPortal common.Address + OptimismMintableERC20Factory common.Address + DelayedWETH common.Address + Opcm common.Address +} + +// SystemConfigMetaData contains all meta data concerning the SystemConfig contract. +var SystemConfigMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"BATCH_INBOX_SLOT\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DELAYED_WETH_SLOT\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"L1_CROSS_DOMAIN_MESSENGER_SLOT\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"L1_ERC_721_BRIDGE_SLOT\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"L1_STANDARD_BRIDGE_SLOT\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"OPCM_SLOT\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"OPTIMISM_PORTAL_SLOT\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"START_BLOCK_SLOT\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"UNSAFE_BLOCK_SIGNER_SLOT\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"VERSION\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"basefeeScalar\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"batchInbox\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr_\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"batcherHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"blobbasefeeScalar\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"daFootprintGasScalar\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"delayedWETH\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr_\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"disputeGameFactory\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr_\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"eip1559Denominator\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"eip1559Elasticity\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"gasLimit\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getAddresses\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"l1CrossDomainMessenger\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1ERC721Bridge\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1StandardBridge\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismPortal\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismMintableERC20Factory\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"delayedWETH\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"opcm\",\"type\":\"address\"}],\"internalType\":\"structSystemConfig.Addresses\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"guardian\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"initVersion\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_owner\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"_basefeeScalar\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"_blobbasefeeScalar\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"_batcherHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint64\",\"name\":\"_gasLimit\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"_unsafeBlockSigner\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"maxResourceLimit\",\"type\":\"uint32\"},{\"internalType\":\"uint8\",\"name\":\"elasticityMultiplier\",\"type\":\"uint8\"},{\"internalType\":\"uint8\",\"name\":\"baseFeeMaxChangeDenominator\",\"type\":\"uint8\"},{\"internalType\":\"uint32\",\"name\":\"minimumBaseFee\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"systemTxMaxGas\",\"type\":\"uint32\"},{\"internalType\":\"uint128\",\"name\":\"maximumBaseFee\",\"type\":\"uint128\"}],\"internalType\":\"structIResourceMetering.ResourceConfig\",\"name\":\"_config\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"_batchInbox\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"l1CrossDomainMessenger\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1ERC721Bridge\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1StandardBridge\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismPortal\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismMintableERC20Factory\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"delayedWETH\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"opcm\",\"type\":\"address\"}],\"internalType\":\"structSystemConfig.Addresses\",\"name\":\"_addresses\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"_l2ChainId\",\"type\":\"uint256\"},{\"internalType\":\"contractISuperchainConfig\",\"name\":\"_superchainConfig\",\"type\":\"address\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"isCustomGasToken\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"isFeatureEnabled\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"l1CrossDomainMessenger\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr_\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"l1ERC721Bridge\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr_\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"l1StandardBridge\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr_\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"l2ChainId\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"lastUsedOPCM\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr_\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"lastUsedOPCMVersion\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"version_\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"maximumGasLimit\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"minBaseFee\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"minimumGasLimit\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"operatorFeeConstant\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"operatorFeeScalar\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"optimismMintableERC20Factory\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr_\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"optimismPortal\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr_\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"overhead\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"paused\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"proxyAdmin\",\"outputs\":[{\"internalType\":\"contractIProxyAdmin\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"proxyAdminOwner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"resourceConfig\",\"outputs\":[{\"components\":[{\"internalType\":\"uint32\",\"name\":\"maxResourceLimit\",\"type\":\"uint32\"},{\"internalType\":\"uint8\",\"name\":\"elasticityMultiplier\",\"type\":\"uint8\"},{\"internalType\":\"uint8\",\"name\":\"baseFeeMaxChangeDenominator\",\"type\":\"uint8\"},{\"internalType\":\"uint32\",\"name\":\"minimumBaseFee\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"systemTxMaxGas\",\"type\":\"uint32\"},{\"internalType\":\"uint128\",\"name\":\"maximumBaseFee\",\"type\":\"uint128\"}],\"internalType\":\"structIResourceMetering.ResourceConfig\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"scalar\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_batcher\",\"type\":\"address\"}],\"name\":\"setBatcherHash\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"_batcherHash\",\"type\":\"bytes32\"}],\"name\":\"setBatcherHash\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint16\",\"name\":\"_daFootprintGasScalar\",\"type\":\"uint16\"}],\"name\":\"setDAFootprintGasScalar\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"_denominator\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"_elasticity\",\"type\":\"uint32\"}],\"name\":\"setEIP1559Params\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"_feature\",\"type\":\"bytes32\"},{\"internalType\":\"bool\",\"name\":\"_enabled\",\"type\":\"bool\"}],\"name\":\"setFeature\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_overhead\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_scalar\",\"type\":\"uint256\"}],\"name\":\"setGasConfig\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"_basefeeScalar\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"_blobbasefeeScalar\",\"type\":\"uint32\"}],\"name\":\"setGasConfigEcotone\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"_gasLimit\",\"type\":\"uint64\"}],\"name\":\"setGasLimit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"_minBaseFee\",\"type\":\"uint64\"}],\"name\":\"setMinBaseFee\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"_operatorFeeScalar\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"_operatorFeeConstant\",\"type\":\"uint64\"}],\"name\":\"setOperatorFeeScalars\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_unsafeBlockSigner\",\"type\":\"address\"}],\"name\":\"setUnsafeBlockSigner\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"startBlock\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"startBlock_\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"superchainConfig\",\"outputs\":[{\"internalType\":\"contractISuperchainConfig\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"unsafeBlockSigner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr_\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"version\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"enumSystemConfig.UpdateType\",\"name\":\"updateType\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"ConfigUpdate\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"feature\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bool\",\"name\":\"enabled\",\"type\":\"bool\"}],\"name\":\"FeatureSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"ProxyAdminOwnedBase_NotProxyAdmin\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ProxyAdminOwnedBase_NotProxyAdminOwner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ProxyAdminOwnedBase_NotResolvedDelegateProxy\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ProxyAdminOwnedBase_NotSharedProxyAdminOwner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ProxyAdminOwnedBase_ProxyAdminNotFound\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ReinitializableBase_ZeroInitVersion\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SystemConfig_InvalidFeatureState\",\"type\":\"error\"}]", +} + +// SystemConfigABI is the input ABI used to generate the binding from. +// Deprecated: Use SystemConfigMetaData.ABI instead. +var SystemConfigABI = SystemConfigMetaData.ABI + +// SystemConfig is an auto generated Go binding around an Ethereum contract. +type SystemConfig struct { + SystemConfigCaller // Read-only binding to the contract + SystemConfigTransactor // Write-only binding to the contract + SystemConfigFilterer // Log filterer for contract events +} + +// SystemConfigCaller is an auto generated read-only Go binding around an Ethereum contract. +type SystemConfigCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SystemConfigTransactor is an auto generated write-only Go binding around an Ethereum contract. +type SystemConfigTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SystemConfigFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type SystemConfigFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SystemConfigSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type SystemConfigSession struct { + Contract *SystemConfig // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// SystemConfigCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type SystemConfigCallerSession struct { + Contract *SystemConfigCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// SystemConfigTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type SystemConfigTransactorSession struct { + Contract *SystemConfigTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// SystemConfigRaw is an auto generated low-level Go binding around an Ethereum contract. +type SystemConfigRaw struct { + Contract *SystemConfig // Generic contract binding to access the raw methods on +} + +// SystemConfigCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type SystemConfigCallerRaw struct { + Contract *SystemConfigCaller // Generic read-only contract binding to access the raw methods on +} + +// SystemConfigTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type SystemConfigTransactorRaw struct { + Contract *SystemConfigTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewSystemConfig creates a new instance of SystemConfig, bound to a specific deployed contract. +func NewSystemConfig(address common.Address, backend bind.ContractBackend) (*SystemConfig, error) { + contract, err := bindSystemConfig(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &SystemConfig{SystemConfigCaller: SystemConfigCaller{contract: contract}, SystemConfigTransactor: SystemConfigTransactor{contract: contract}, SystemConfigFilterer: SystemConfigFilterer{contract: contract}}, nil +} + +// NewSystemConfigCaller creates a new read-only instance of SystemConfig, bound to a specific deployed contract. +func NewSystemConfigCaller(address common.Address, caller bind.ContractCaller) (*SystemConfigCaller, error) { + contract, err := bindSystemConfig(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &SystemConfigCaller{contract: contract}, nil +} + +// NewSystemConfigTransactor creates a new write-only instance of SystemConfig, bound to a specific deployed contract. +func NewSystemConfigTransactor(address common.Address, transactor bind.ContractTransactor) (*SystemConfigTransactor, error) { + contract, err := bindSystemConfig(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &SystemConfigTransactor{contract: contract}, nil +} + +// NewSystemConfigFilterer creates a new log filterer instance of SystemConfig, bound to a specific deployed contract. +func NewSystemConfigFilterer(address common.Address, filterer bind.ContractFilterer) (*SystemConfigFilterer, error) { + contract, err := bindSystemConfig(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &SystemConfigFilterer{contract: contract}, nil +} + +// bindSystemConfig binds a generic wrapper to an already deployed contract. +func bindSystemConfig(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := SystemConfigMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_SystemConfig *SystemConfigRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _SystemConfig.Contract.SystemConfigCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_SystemConfig *SystemConfigRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _SystemConfig.Contract.SystemConfigTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_SystemConfig *SystemConfigRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _SystemConfig.Contract.SystemConfigTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_SystemConfig *SystemConfigCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _SystemConfig.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_SystemConfig *SystemConfigTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _SystemConfig.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_SystemConfig *SystemConfigTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _SystemConfig.Contract.contract.Transact(opts, method, params...) +} + +// BATCHINBOXSLOT is a free data retrieval call binding the contract method 0xbc49ce5f. +// +// Solidity: function BATCH_INBOX_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCaller) BATCHINBOXSLOT(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "BATCH_INBOX_SLOT") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// BATCHINBOXSLOT is a free data retrieval call binding the contract method 0xbc49ce5f. +// +// Solidity: function BATCH_INBOX_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigSession) BATCHINBOXSLOT() ([32]byte, error) { + return _SystemConfig.Contract.BATCHINBOXSLOT(&_SystemConfig.CallOpts) +} + +// BATCHINBOXSLOT is a free data retrieval call binding the contract method 0xbc49ce5f. +// +// Solidity: function BATCH_INBOX_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCallerSession) BATCHINBOXSLOT() ([32]byte, error) { + return _SystemConfig.Contract.BATCHINBOXSLOT(&_SystemConfig.CallOpts) +} + +// DELAYEDWETHSLOT is a free data retrieval call binding the contract method 0x7e54b8ad. +// +// Solidity: function DELAYED_WETH_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCaller) DELAYEDWETHSLOT(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "DELAYED_WETH_SLOT") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// DELAYEDWETHSLOT is a free data retrieval call binding the contract method 0x7e54b8ad. +// +// Solidity: function DELAYED_WETH_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigSession) DELAYEDWETHSLOT() ([32]byte, error) { + return _SystemConfig.Contract.DELAYEDWETHSLOT(&_SystemConfig.CallOpts) +} + +// DELAYEDWETHSLOT is a free data retrieval call binding the contract method 0x7e54b8ad. +// +// Solidity: function DELAYED_WETH_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCallerSession) DELAYEDWETHSLOT() ([32]byte, error) { + return _SystemConfig.Contract.DELAYEDWETHSLOT(&_SystemConfig.CallOpts) +} + +// L1CROSSDOMAINMESSENGERSLOT is a free data retrieval call binding the contract method 0x5d73369c. +// +// Solidity: function L1_CROSS_DOMAIN_MESSENGER_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCaller) L1CROSSDOMAINMESSENGERSLOT(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "L1_CROSS_DOMAIN_MESSENGER_SLOT") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// L1CROSSDOMAINMESSENGERSLOT is a free data retrieval call binding the contract method 0x5d73369c. +// +// Solidity: function L1_CROSS_DOMAIN_MESSENGER_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigSession) L1CROSSDOMAINMESSENGERSLOT() ([32]byte, error) { + return _SystemConfig.Contract.L1CROSSDOMAINMESSENGERSLOT(&_SystemConfig.CallOpts) +} + +// L1CROSSDOMAINMESSENGERSLOT is a free data retrieval call binding the contract method 0x5d73369c. +// +// Solidity: function L1_CROSS_DOMAIN_MESSENGER_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCallerSession) L1CROSSDOMAINMESSENGERSLOT() ([32]byte, error) { + return _SystemConfig.Contract.L1CROSSDOMAINMESSENGERSLOT(&_SystemConfig.CallOpts) +} + +// L1ERC721BRIDGESLOT is a free data retrieval call binding the contract method 0x19f5cea8. +// +// Solidity: function L1_ERC_721_BRIDGE_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCaller) L1ERC721BRIDGESLOT(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "L1_ERC_721_BRIDGE_SLOT") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// L1ERC721BRIDGESLOT is a free data retrieval call binding the contract method 0x19f5cea8. +// +// Solidity: function L1_ERC_721_BRIDGE_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigSession) L1ERC721BRIDGESLOT() ([32]byte, error) { + return _SystemConfig.Contract.L1ERC721BRIDGESLOT(&_SystemConfig.CallOpts) +} + +// L1ERC721BRIDGESLOT is a free data retrieval call binding the contract method 0x19f5cea8. +// +// Solidity: function L1_ERC_721_BRIDGE_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCallerSession) L1ERC721BRIDGESLOT() ([32]byte, error) { + return _SystemConfig.Contract.L1ERC721BRIDGESLOT(&_SystemConfig.CallOpts) +} + +// L1STANDARDBRIDGESLOT is a free data retrieval call binding the contract method 0xf8c68de0. +// +// Solidity: function L1_STANDARD_BRIDGE_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCaller) L1STANDARDBRIDGESLOT(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "L1_STANDARD_BRIDGE_SLOT") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// L1STANDARDBRIDGESLOT is a free data retrieval call binding the contract method 0xf8c68de0. +// +// Solidity: function L1_STANDARD_BRIDGE_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigSession) L1STANDARDBRIDGESLOT() ([32]byte, error) { + return _SystemConfig.Contract.L1STANDARDBRIDGESLOT(&_SystemConfig.CallOpts) +} + +// L1STANDARDBRIDGESLOT is a free data retrieval call binding the contract method 0xf8c68de0. +// +// Solidity: function L1_STANDARD_BRIDGE_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCallerSession) L1STANDARDBRIDGESLOT() ([32]byte, error) { + return _SystemConfig.Contract.L1STANDARDBRIDGESLOT(&_SystemConfig.CallOpts) +} + +// OPCMSLOT is a free data retrieval call binding the contract method 0x2efe2c63. +// +// Solidity: function OPCM_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCaller) OPCMSLOT(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "OPCM_SLOT") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// OPCMSLOT is a free data retrieval call binding the contract method 0x2efe2c63. +// +// Solidity: function OPCM_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigSession) OPCMSLOT() ([32]byte, error) { + return _SystemConfig.Contract.OPCMSLOT(&_SystemConfig.CallOpts) +} + +// OPCMSLOT is a free data retrieval call binding the contract method 0x2efe2c63. +// +// Solidity: function OPCM_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCallerSession) OPCMSLOT() ([32]byte, error) { + return _SystemConfig.Contract.OPCMSLOT(&_SystemConfig.CallOpts) +} + +// OPTIMISMMINTABLEERC20FACTORYSLOT is a free data retrieval call binding the contract method 0x06c92657. +// +// Solidity: function OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCaller) OPTIMISMMINTABLEERC20FACTORYSLOT(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// OPTIMISMMINTABLEERC20FACTORYSLOT is a free data retrieval call binding the contract method 0x06c92657. +// +// Solidity: function OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigSession) OPTIMISMMINTABLEERC20FACTORYSLOT() ([32]byte, error) { + return _SystemConfig.Contract.OPTIMISMMINTABLEERC20FACTORYSLOT(&_SystemConfig.CallOpts) +} + +// OPTIMISMMINTABLEERC20FACTORYSLOT is a free data retrieval call binding the contract method 0x06c92657. +// +// Solidity: function OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCallerSession) OPTIMISMMINTABLEERC20FACTORYSLOT() ([32]byte, error) { + return _SystemConfig.Contract.OPTIMISMMINTABLEERC20FACTORYSLOT(&_SystemConfig.CallOpts) +} + +// OPTIMISMPORTALSLOT is a free data retrieval call binding the contract method 0xfd32aa0f. +// +// Solidity: function OPTIMISM_PORTAL_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCaller) OPTIMISMPORTALSLOT(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "OPTIMISM_PORTAL_SLOT") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// OPTIMISMPORTALSLOT is a free data retrieval call binding the contract method 0xfd32aa0f. +// +// Solidity: function OPTIMISM_PORTAL_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigSession) OPTIMISMPORTALSLOT() ([32]byte, error) { + return _SystemConfig.Contract.OPTIMISMPORTALSLOT(&_SystemConfig.CallOpts) +} + +// OPTIMISMPORTALSLOT is a free data retrieval call binding the contract method 0xfd32aa0f. +// +// Solidity: function OPTIMISM_PORTAL_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCallerSession) OPTIMISMPORTALSLOT() ([32]byte, error) { + return _SystemConfig.Contract.OPTIMISMPORTALSLOT(&_SystemConfig.CallOpts) +} + +// STARTBLOCKSLOT is a free data retrieval call binding the contract method 0xe0e2016d. +// +// Solidity: function START_BLOCK_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCaller) STARTBLOCKSLOT(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "START_BLOCK_SLOT") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// STARTBLOCKSLOT is a free data retrieval call binding the contract method 0xe0e2016d. +// +// Solidity: function START_BLOCK_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigSession) STARTBLOCKSLOT() ([32]byte, error) { + return _SystemConfig.Contract.STARTBLOCKSLOT(&_SystemConfig.CallOpts) +} + +// STARTBLOCKSLOT is a free data retrieval call binding the contract method 0xe0e2016d. +// +// Solidity: function START_BLOCK_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCallerSession) STARTBLOCKSLOT() ([32]byte, error) { + return _SystemConfig.Contract.STARTBLOCKSLOT(&_SystemConfig.CallOpts) +} + +// UNSAFEBLOCKSIGNERSLOT is a free data retrieval call binding the contract method 0x4f16540b. +// +// Solidity: function UNSAFE_BLOCK_SIGNER_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCaller) UNSAFEBLOCKSIGNERSLOT(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "UNSAFE_BLOCK_SIGNER_SLOT") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// UNSAFEBLOCKSIGNERSLOT is a free data retrieval call binding the contract method 0x4f16540b. +// +// Solidity: function UNSAFE_BLOCK_SIGNER_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigSession) UNSAFEBLOCKSIGNERSLOT() ([32]byte, error) { + return _SystemConfig.Contract.UNSAFEBLOCKSIGNERSLOT(&_SystemConfig.CallOpts) +} + +// UNSAFEBLOCKSIGNERSLOT is a free data retrieval call binding the contract method 0x4f16540b. +// +// Solidity: function UNSAFE_BLOCK_SIGNER_SLOT() view returns(bytes32) +func (_SystemConfig *SystemConfigCallerSession) UNSAFEBLOCKSIGNERSLOT() ([32]byte, error) { + return _SystemConfig.Contract.UNSAFEBLOCKSIGNERSLOT(&_SystemConfig.CallOpts) +} + +// VERSION is a free data retrieval call binding the contract method 0xffa1ad74. +// +// Solidity: function VERSION() view returns(uint256) +func (_SystemConfig *SystemConfigCaller) VERSION(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "VERSION") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// VERSION is a free data retrieval call binding the contract method 0xffa1ad74. +// +// Solidity: function VERSION() view returns(uint256) +func (_SystemConfig *SystemConfigSession) VERSION() (*big.Int, error) { + return _SystemConfig.Contract.VERSION(&_SystemConfig.CallOpts) +} + +// VERSION is a free data retrieval call binding the contract method 0xffa1ad74. +// +// Solidity: function VERSION() view returns(uint256) +func (_SystemConfig *SystemConfigCallerSession) VERSION() (*big.Int, error) { + return _SystemConfig.Contract.VERSION(&_SystemConfig.CallOpts) +} + +// BasefeeScalar is a free data retrieval call binding the contract method 0xbfb14fb7. +// +// Solidity: function basefeeScalar() view returns(uint32) +func (_SystemConfig *SystemConfigCaller) BasefeeScalar(opts *bind.CallOpts) (uint32, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "basefeeScalar") + + if err != nil { + return *new(uint32), err + } + + out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) + + return out0, err + +} + +// BasefeeScalar is a free data retrieval call binding the contract method 0xbfb14fb7. +// +// Solidity: function basefeeScalar() view returns(uint32) +func (_SystemConfig *SystemConfigSession) BasefeeScalar() (uint32, error) { + return _SystemConfig.Contract.BasefeeScalar(&_SystemConfig.CallOpts) +} + +// BasefeeScalar is a free data retrieval call binding the contract method 0xbfb14fb7. +// +// Solidity: function basefeeScalar() view returns(uint32) +func (_SystemConfig *SystemConfigCallerSession) BasefeeScalar() (uint32, error) { + return _SystemConfig.Contract.BasefeeScalar(&_SystemConfig.CallOpts) +} + +// BatchInbox is a free data retrieval call binding the contract method 0xdac6e63a. +// +// Solidity: function batchInbox() view returns(address addr_) +func (_SystemConfig *SystemConfigCaller) BatchInbox(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "batchInbox") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// BatchInbox is a free data retrieval call binding the contract method 0xdac6e63a. +// +// Solidity: function batchInbox() view returns(address addr_) +func (_SystemConfig *SystemConfigSession) BatchInbox() (common.Address, error) { + return _SystemConfig.Contract.BatchInbox(&_SystemConfig.CallOpts) +} + +// BatchInbox is a free data retrieval call binding the contract method 0xdac6e63a. +// +// Solidity: function batchInbox() view returns(address addr_) +func (_SystemConfig *SystemConfigCallerSession) BatchInbox() (common.Address, error) { + return _SystemConfig.Contract.BatchInbox(&_SystemConfig.CallOpts) +} + +// BatcherHash is a free data retrieval call binding the contract method 0xe81b2c6d. +// +// Solidity: function batcherHash() view returns(bytes32) +func (_SystemConfig *SystemConfigCaller) BatcherHash(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "batcherHash") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// BatcherHash is a free data retrieval call binding the contract method 0xe81b2c6d. +// +// Solidity: function batcherHash() view returns(bytes32) +func (_SystemConfig *SystemConfigSession) BatcherHash() ([32]byte, error) { + return _SystemConfig.Contract.BatcherHash(&_SystemConfig.CallOpts) +} + +// BatcherHash is a free data retrieval call binding the contract method 0xe81b2c6d. +// +// Solidity: function batcherHash() view returns(bytes32) +func (_SystemConfig *SystemConfigCallerSession) BatcherHash() ([32]byte, error) { + return _SystemConfig.Contract.BatcherHash(&_SystemConfig.CallOpts) +} + +// BlobbasefeeScalar is a free data retrieval call binding the contract method 0xec707517. +// +// Solidity: function blobbasefeeScalar() view returns(uint32) +func (_SystemConfig *SystemConfigCaller) BlobbasefeeScalar(opts *bind.CallOpts) (uint32, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "blobbasefeeScalar") + + if err != nil { + return *new(uint32), err + } + + out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) + + return out0, err + +} + +// BlobbasefeeScalar is a free data retrieval call binding the contract method 0xec707517. +// +// Solidity: function blobbasefeeScalar() view returns(uint32) +func (_SystemConfig *SystemConfigSession) BlobbasefeeScalar() (uint32, error) { + return _SystemConfig.Contract.BlobbasefeeScalar(&_SystemConfig.CallOpts) +} + +// BlobbasefeeScalar is a free data retrieval call binding the contract method 0xec707517. +// +// Solidity: function blobbasefeeScalar() view returns(uint32) +func (_SystemConfig *SystemConfigCallerSession) BlobbasefeeScalar() (uint32, error) { + return _SystemConfig.Contract.BlobbasefeeScalar(&_SystemConfig.CallOpts) +} + +// DaFootprintGasScalar is a free data retrieval call binding the contract method 0xfe3d5710. +// +// Solidity: function daFootprintGasScalar() view returns(uint16) +func (_SystemConfig *SystemConfigCaller) DaFootprintGasScalar(opts *bind.CallOpts) (uint16, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "daFootprintGasScalar") + + if err != nil { + return *new(uint16), err + } + + out0 := *abi.ConvertType(out[0], new(uint16)).(*uint16) + + return out0, err + +} + +// DaFootprintGasScalar is a free data retrieval call binding the contract method 0xfe3d5710. +// +// Solidity: function daFootprintGasScalar() view returns(uint16) +func (_SystemConfig *SystemConfigSession) DaFootprintGasScalar() (uint16, error) { + return _SystemConfig.Contract.DaFootprintGasScalar(&_SystemConfig.CallOpts) +} + +// DaFootprintGasScalar is a free data retrieval call binding the contract method 0xfe3d5710. +// +// Solidity: function daFootprintGasScalar() view returns(uint16) +func (_SystemConfig *SystemConfigCallerSession) DaFootprintGasScalar() (uint16, error) { + return _SystemConfig.Contract.DaFootprintGasScalar(&_SystemConfig.CallOpts) +} + +// DelayedWETH is a free data retrieval call binding the contract method 0x215b7a1c. +// +// Solidity: function delayedWETH() view returns(address addr_) +func (_SystemConfig *SystemConfigCaller) DelayedWETH(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "delayedWETH") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// DelayedWETH is a free data retrieval call binding the contract method 0x215b7a1c. +// +// Solidity: function delayedWETH() view returns(address addr_) +func (_SystemConfig *SystemConfigSession) DelayedWETH() (common.Address, error) { + return _SystemConfig.Contract.DelayedWETH(&_SystemConfig.CallOpts) +} + +// DelayedWETH is a free data retrieval call binding the contract method 0x215b7a1c. +// +// Solidity: function delayedWETH() view returns(address addr_) +func (_SystemConfig *SystemConfigCallerSession) DelayedWETH() (common.Address, error) { + return _SystemConfig.Contract.DelayedWETH(&_SystemConfig.CallOpts) +} + +// DisputeGameFactory is a free data retrieval call binding the contract method 0xf2b4e617. +// +// Solidity: function disputeGameFactory() view returns(address addr_) +func (_SystemConfig *SystemConfigCaller) DisputeGameFactory(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "disputeGameFactory") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// DisputeGameFactory is a free data retrieval call binding the contract method 0xf2b4e617. +// +// Solidity: function disputeGameFactory() view returns(address addr_) +func (_SystemConfig *SystemConfigSession) DisputeGameFactory() (common.Address, error) { + return _SystemConfig.Contract.DisputeGameFactory(&_SystemConfig.CallOpts) +} + +// DisputeGameFactory is a free data retrieval call binding the contract method 0xf2b4e617. +// +// Solidity: function disputeGameFactory() view returns(address addr_) +func (_SystemConfig *SystemConfigCallerSession) DisputeGameFactory() (common.Address, error) { + return _SystemConfig.Contract.DisputeGameFactory(&_SystemConfig.CallOpts) +} + +// Eip1559Denominator is a free data retrieval call binding the contract method 0xd220a9e0. +// +// Solidity: function eip1559Denominator() view returns(uint32) +func (_SystemConfig *SystemConfigCaller) Eip1559Denominator(opts *bind.CallOpts) (uint32, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "eip1559Denominator") + + if err != nil { + return *new(uint32), err + } + + out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) + + return out0, err + +} + +// Eip1559Denominator is a free data retrieval call binding the contract method 0xd220a9e0. +// +// Solidity: function eip1559Denominator() view returns(uint32) +func (_SystemConfig *SystemConfigSession) Eip1559Denominator() (uint32, error) { + return _SystemConfig.Contract.Eip1559Denominator(&_SystemConfig.CallOpts) +} + +// Eip1559Denominator is a free data retrieval call binding the contract method 0xd220a9e0. +// +// Solidity: function eip1559Denominator() view returns(uint32) +func (_SystemConfig *SystemConfigCallerSession) Eip1559Denominator() (uint32, error) { + return _SystemConfig.Contract.Eip1559Denominator(&_SystemConfig.CallOpts) +} + +// Eip1559Elasticity is a free data retrieval call binding the contract method 0xc9ff2d16. +// +// Solidity: function eip1559Elasticity() view returns(uint32) +func (_SystemConfig *SystemConfigCaller) Eip1559Elasticity(opts *bind.CallOpts) (uint32, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "eip1559Elasticity") + + if err != nil { + return *new(uint32), err + } + + out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) + + return out0, err + +} + +// Eip1559Elasticity is a free data retrieval call binding the contract method 0xc9ff2d16. +// +// Solidity: function eip1559Elasticity() view returns(uint32) +func (_SystemConfig *SystemConfigSession) Eip1559Elasticity() (uint32, error) { + return _SystemConfig.Contract.Eip1559Elasticity(&_SystemConfig.CallOpts) +} + +// Eip1559Elasticity is a free data retrieval call binding the contract method 0xc9ff2d16. +// +// Solidity: function eip1559Elasticity() view returns(uint32) +func (_SystemConfig *SystemConfigCallerSession) Eip1559Elasticity() (uint32, error) { + return _SystemConfig.Contract.Eip1559Elasticity(&_SystemConfig.CallOpts) +} + +// GasLimit is a free data retrieval call binding the contract method 0xf68016b7. +// +// Solidity: function gasLimit() view returns(uint64) +func (_SystemConfig *SystemConfigCaller) GasLimit(opts *bind.CallOpts) (uint64, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "gasLimit") + + if err != nil { + return *new(uint64), err + } + + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + + return out0, err + +} + +// GasLimit is a free data retrieval call binding the contract method 0xf68016b7. +// +// Solidity: function gasLimit() view returns(uint64) +func (_SystemConfig *SystemConfigSession) GasLimit() (uint64, error) { + return _SystemConfig.Contract.GasLimit(&_SystemConfig.CallOpts) +} + +// GasLimit is a free data retrieval call binding the contract method 0xf68016b7. +// +// Solidity: function gasLimit() view returns(uint64) +func (_SystemConfig *SystemConfigCallerSession) GasLimit() (uint64, error) { + return _SystemConfig.Contract.GasLimit(&_SystemConfig.CallOpts) +} + +// GetAddresses is a free data retrieval call binding the contract method 0xa39fac12. +// +// Solidity: function getAddresses() view returns((address,address,address,address,address,address,address)) +func (_SystemConfig *SystemConfigCaller) GetAddresses(opts *bind.CallOpts) (SystemConfigAddresses, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "getAddresses") + + if err != nil { + return *new(SystemConfigAddresses), err + } + + out0 := *abi.ConvertType(out[0], new(SystemConfigAddresses)).(*SystemConfigAddresses) + + return out0, err + +} + +// GetAddresses is a free data retrieval call binding the contract method 0xa39fac12. +// +// Solidity: function getAddresses() view returns((address,address,address,address,address,address,address)) +func (_SystemConfig *SystemConfigSession) GetAddresses() (SystemConfigAddresses, error) { + return _SystemConfig.Contract.GetAddresses(&_SystemConfig.CallOpts) +} + +// GetAddresses is a free data retrieval call binding the contract method 0xa39fac12. +// +// Solidity: function getAddresses() view returns((address,address,address,address,address,address,address)) +func (_SystemConfig *SystemConfigCallerSession) GetAddresses() (SystemConfigAddresses, error) { + return _SystemConfig.Contract.GetAddresses(&_SystemConfig.CallOpts) +} + +// Guardian is a free data retrieval call binding the contract method 0x452a9320. +// +// Solidity: function guardian() view returns(address) +func (_SystemConfig *SystemConfigCaller) Guardian(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "guardian") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Guardian is a free data retrieval call binding the contract method 0x452a9320. +// +// Solidity: function guardian() view returns(address) +func (_SystemConfig *SystemConfigSession) Guardian() (common.Address, error) { + return _SystemConfig.Contract.Guardian(&_SystemConfig.CallOpts) +} + +// Guardian is a free data retrieval call binding the contract method 0x452a9320. +// +// Solidity: function guardian() view returns(address) +func (_SystemConfig *SystemConfigCallerSession) Guardian() (common.Address, error) { + return _SystemConfig.Contract.Guardian(&_SystemConfig.CallOpts) +} + +// InitVersion is a free data retrieval call binding the contract method 0x38d38c97. +// +// Solidity: function initVersion() view returns(uint8) +func (_SystemConfig *SystemConfigCaller) InitVersion(opts *bind.CallOpts) (uint8, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "initVersion") + + if err != nil { + return *new(uint8), err + } + + out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + + return out0, err + +} + +// InitVersion is a free data retrieval call binding the contract method 0x38d38c97. +// +// Solidity: function initVersion() view returns(uint8) +func (_SystemConfig *SystemConfigSession) InitVersion() (uint8, error) { + return _SystemConfig.Contract.InitVersion(&_SystemConfig.CallOpts) +} + +// InitVersion is a free data retrieval call binding the contract method 0x38d38c97. +// +// Solidity: function initVersion() view returns(uint8) +func (_SystemConfig *SystemConfigCallerSession) InitVersion() (uint8, error) { + return _SystemConfig.Contract.InitVersion(&_SystemConfig.CallOpts) +} + +// IsCustomGasToken is a free data retrieval call binding the contract method 0x21326849. +// +// Solidity: function isCustomGasToken() view returns(bool) +func (_SystemConfig *SystemConfigCaller) IsCustomGasToken(opts *bind.CallOpts) (bool, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "isCustomGasToken") + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsCustomGasToken is a free data retrieval call binding the contract method 0x21326849. +// +// Solidity: function isCustomGasToken() view returns(bool) +func (_SystemConfig *SystemConfigSession) IsCustomGasToken() (bool, error) { + return _SystemConfig.Contract.IsCustomGasToken(&_SystemConfig.CallOpts) +} + +// IsCustomGasToken is a free data retrieval call binding the contract method 0x21326849. +// +// Solidity: function isCustomGasToken() view returns(bool) +func (_SystemConfig *SystemConfigCallerSession) IsCustomGasToken() (bool, error) { + return _SystemConfig.Contract.IsCustomGasToken(&_SystemConfig.CallOpts) +} + +// IsFeatureEnabled is a free data retrieval call binding the contract method 0x47af267b. +// +// Solidity: function isFeatureEnabled(bytes32 ) view returns(bool) +func (_SystemConfig *SystemConfigCaller) IsFeatureEnabled(opts *bind.CallOpts, arg0 [32]byte) (bool, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "isFeatureEnabled", arg0) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsFeatureEnabled is a free data retrieval call binding the contract method 0x47af267b. +// +// Solidity: function isFeatureEnabled(bytes32 ) view returns(bool) +func (_SystemConfig *SystemConfigSession) IsFeatureEnabled(arg0 [32]byte) (bool, error) { + return _SystemConfig.Contract.IsFeatureEnabled(&_SystemConfig.CallOpts, arg0) +} + +// IsFeatureEnabled is a free data retrieval call binding the contract method 0x47af267b. +// +// Solidity: function isFeatureEnabled(bytes32 ) view returns(bool) +func (_SystemConfig *SystemConfigCallerSession) IsFeatureEnabled(arg0 [32]byte) (bool, error) { + return _SystemConfig.Contract.IsFeatureEnabled(&_SystemConfig.CallOpts, arg0) +} + +// L1CrossDomainMessenger is a free data retrieval call binding the contract method 0xa7119869. +// +// Solidity: function l1CrossDomainMessenger() view returns(address addr_) +func (_SystemConfig *SystemConfigCaller) L1CrossDomainMessenger(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "l1CrossDomainMessenger") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// L1CrossDomainMessenger is a free data retrieval call binding the contract method 0xa7119869. +// +// Solidity: function l1CrossDomainMessenger() view returns(address addr_) +func (_SystemConfig *SystemConfigSession) L1CrossDomainMessenger() (common.Address, error) { + return _SystemConfig.Contract.L1CrossDomainMessenger(&_SystemConfig.CallOpts) +} + +// L1CrossDomainMessenger is a free data retrieval call binding the contract method 0xa7119869. +// +// Solidity: function l1CrossDomainMessenger() view returns(address addr_) +func (_SystemConfig *SystemConfigCallerSession) L1CrossDomainMessenger() (common.Address, error) { + return _SystemConfig.Contract.L1CrossDomainMessenger(&_SystemConfig.CallOpts) +} + +// L1ERC721Bridge is a free data retrieval call binding the contract method 0xc4e8ddfa. +// +// Solidity: function l1ERC721Bridge() view returns(address addr_) +func (_SystemConfig *SystemConfigCaller) L1ERC721Bridge(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "l1ERC721Bridge") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// L1ERC721Bridge is a free data retrieval call binding the contract method 0xc4e8ddfa. +// +// Solidity: function l1ERC721Bridge() view returns(address addr_) +func (_SystemConfig *SystemConfigSession) L1ERC721Bridge() (common.Address, error) { + return _SystemConfig.Contract.L1ERC721Bridge(&_SystemConfig.CallOpts) +} + +// L1ERC721Bridge is a free data retrieval call binding the contract method 0xc4e8ddfa. +// +// Solidity: function l1ERC721Bridge() view returns(address addr_) +func (_SystemConfig *SystemConfigCallerSession) L1ERC721Bridge() (common.Address, error) { + return _SystemConfig.Contract.L1ERC721Bridge(&_SystemConfig.CallOpts) +} + +// L1StandardBridge is a free data retrieval call binding the contract method 0x078f29cf. +// +// Solidity: function l1StandardBridge() view returns(address addr_) +func (_SystemConfig *SystemConfigCaller) L1StandardBridge(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "l1StandardBridge") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// L1StandardBridge is a free data retrieval call binding the contract method 0x078f29cf. +// +// Solidity: function l1StandardBridge() view returns(address addr_) +func (_SystemConfig *SystemConfigSession) L1StandardBridge() (common.Address, error) { + return _SystemConfig.Contract.L1StandardBridge(&_SystemConfig.CallOpts) +} + +// L1StandardBridge is a free data retrieval call binding the contract method 0x078f29cf. +// +// Solidity: function l1StandardBridge() view returns(address addr_) +func (_SystemConfig *SystemConfigCallerSession) L1StandardBridge() (common.Address, error) { + return _SystemConfig.Contract.L1StandardBridge(&_SystemConfig.CallOpts) +} + +// L2ChainId is a free data retrieval call binding the contract method 0xd6ae3cd5. +// +// Solidity: function l2ChainId() view returns(uint256) +func (_SystemConfig *SystemConfigCaller) L2ChainId(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "l2ChainId") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// L2ChainId is a free data retrieval call binding the contract method 0xd6ae3cd5. +// +// Solidity: function l2ChainId() view returns(uint256) +func (_SystemConfig *SystemConfigSession) L2ChainId() (*big.Int, error) { + return _SystemConfig.Contract.L2ChainId(&_SystemConfig.CallOpts) +} + +// L2ChainId is a free data retrieval call binding the contract method 0xd6ae3cd5. +// +// Solidity: function l2ChainId() view returns(uint256) +func (_SystemConfig *SystemConfigCallerSession) L2ChainId() (*big.Int, error) { + return _SystemConfig.Contract.L2ChainId(&_SystemConfig.CallOpts) +} + +// LastUsedOPCM is a free data retrieval call binding the contract method 0x8ed95502. +// +// Solidity: function lastUsedOPCM() view returns(address addr_) +func (_SystemConfig *SystemConfigCaller) LastUsedOPCM(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "lastUsedOPCM") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// LastUsedOPCM is a free data retrieval call binding the contract method 0x8ed95502. +// +// Solidity: function lastUsedOPCM() view returns(address addr_) +func (_SystemConfig *SystemConfigSession) LastUsedOPCM() (common.Address, error) { + return _SystemConfig.Contract.LastUsedOPCM(&_SystemConfig.CallOpts) +} + +// LastUsedOPCM is a free data retrieval call binding the contract method 0x8ed95502. +// +// Solidity: function lastUsedOPCM() view returns(address addr_) +func (_SystemConfig *SystemConfigCallerSession) LastUsedOPCM() (common.Address, error) { + return _SystemConfig.Contract.LastUsedOPCM(&_SystemConfig.CallOpts) +} + +// LastUsedOPCMVersion is a free data retrieval call binding the contract method 0x9fabcc84. +// +// Solidity: function lastUsedOPCMVersion() view returns(string version_) +func (_SystemConfig *SystemConfigCaller) LastUsedOPCMVersion(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "lastUsedOPCMVersion") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// LastUsedOPCMVersion is a free data retrieval call binding the contract method 0x9fabcc84. +// +// Solidity: function lastUsedOPCMVersion() view returns(string version_) +func (_SystemConfig *SystemConfigSession) LastUsedOPCMVersion() (string, error) { + return _SystemConfig.Contract.LastUsedOPCMVersion(&_SystemConfig.CallOpts) +} + +// LastUsedOPCMVersion is a free data retrieval call binding the contract method 0x9fabcc84. +// +// Solidity: function lastUsedOPCMVersion() view returns(string version_) +func (_SystemConfig *SystemConfigCallerSession) LastUsedOPCMVersion() (string, error) { + return _SystemConfig.Contract.LastUsedOPCMVersion(&_SystemConfig.CallOpts) +} + +// MaximumGasLimit is a free data retrieval call binding the contract method 0x0ae14b1b. +// +// Solidity: function maximumGasLimit() pure returns(uint64) +func (_SystemConfig *SystemConfigCaller) MaximumGasLimit(opts *bind.CallOpts) (uint64, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "maximumGasLimit") + + if err != nil { + return *new(uint64), err + } + + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + + return out0, err + +} + +// MaximumGasLimit is a free data retrieval call binding the contract method 0x0ae14b1b. +// +// Solidity: function maximumGasLimit() pure returns(uint64) +func (_SystemConfig *SystemConfigSession) MaximumGasLimit() (uint64, error) { + return _SystemConfig.Contract.MaximumGasLimit(&_SystemConfig.CallOpts) +} + +// MaximumGasLimit is a free data retrieval call binding the contract method 0x0ae14b1b. +// +// Solidity: function maximumGasLimit() pure returns(uint64) +func (_SystemConfig *SystemConfigCallerSession) MaximumGasLimit() (uint64, error) { + return _SystemConfig.Contract.MaximumGasLimit(&_SystemConfig.CallOpts) +} + +// MinBaseFee is a free data retrieval call binding the contract method 0xa62611a2. +// +// Solidity: function minBaseFee() view returns(uint64) +func (_SystemConfig *SystemConfigCaller) MinBaseFee(opts *bind.CallOpts) (uint64, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "minBaseFee") + + if err != nil { + return *new(uint64), err + } + + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + + return out0, err + +} + +// MinBaseFee is a free data retrieval call binding the contract method 0xa62611a2. +// +// Solidity: function minBaseFee() view returns(uint64) +func (_SystemConfig *SystemConfigSession) MinBaseFee() (uint64, error) { + return _SystemConfig.Contract.MinBaseFee(&_SystemConfig.CallOpts) +} + +// MinBaseFee is a free data retrieval call binding the contract method 0xa62611a2. +// +// Solidity: function minBaseFee() view returns(uint64) +func (_SystemConfig *SystemConfigCallerSession) MinBaseFee() (uint64, error) { + return _SystemConfig.Contract.MinBaseFee(&_SystemConfig.CallOpts) +} + +// MinimumGasLimit is a free data retrieval call binding the contract method 0x4add321d. +// +// Solidity: function minimumGasLimit() view returns(uint64) +func (_SystemConfig *SystemConfigCaller) MinimumGasLimit(opts *bind.CallOpts) (uint64, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "minimumGasLimit") + + if err != nil { + return *new(uint64), err + } + + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + + return out0, err + +} + +// MinimumGasLimit is a free data retrieval call binding the contract method 0x4add321d. +// +// Solidity: function minimumGasLimit() view returns(uint64) +func (_SystemConfig *SystemConfigSession) MinimumGasLimit() (uint64, error) { + return _SystemConfig.Contract.MinimumGasLimit(&_SystemConfig.CallOpts) +} + +// MinimumGasLimit is a free data retrieval call binding the contract method 0x4add321d. +// +// Solidity: function minimumGasLimit() view returns(uint64) +func (_SystemConfig *SystemConfigCallerSession) MinimumGasLimit() (uint64, error) { + return _SystemConfig.Contract.MinimumGasLimit(&_SystemConfig.CallOpts) +} + +// OperatorFeeConstant is a free data retrieval call binding the contract method 0x16d3bc7f. +// +// Solidity: function operatorFeeConstant() view returns(uint64) +func (_SystemConfig *SystemConfigCaller) OperatorFeeConstant(opts *bind.CallOpts) (uint64, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "operatorFeeConstant") + + if err != nil { + return *new(uint64), err + } + + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + + return out0, err + +} + +// OperatorFeeConstant is a free data retrieval call binding the contract method 0x16d3bc7f. +// +// Solidity: function operatorFeeConstant() view returns(uint64) +func (_SystemConfig *SystemConfigSession) OperatorFeeConstant() (uint64, error) { + return _SystemConfig.Contract.OperatorFeeConstant(&_SystemConfig.CallOpts) +} + +// OperatorFeeConstant is a free data retrieval call binding the contract method 0x16d3bc7f. +// +// Solidity: function operatorFeeConstant() view returns(uint64) +func (_SystemConfig *SystemConfigCallerSession) OperatorFeeConstant() (uint64, error) { + return _SystemConfig.Contract.OperatorFeeConstant(&_SystemConfig.CallOpts) +} + +// OperatorFeeScalar is a free data retrieval call binding the contract method 0x4d5d9a2a. +// +// Solidity: function operatorFeeScalar() view returns(uint32) +func (_SystemConfig *SystemConfigCaller) OperatorFeeScalar(opts *bind.CallOpts) (uint32, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "operatorFeeScalar") + + if err != nil { + return *new(uint32), err + } + + out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) + + return out0, err + +} + +// OperatorFeeScalar is a free data retrieval call binding the contract method 0x4d5d9a2a. +// +// Solidity: function operatorFeeScalar() view returns(uint32) +func (_SystemConfig *SystemConfigSession) OperatorFeeScalar() (uint32, error) { + return _SystemConfig.Contract.OperatorFeeScalar(&_SystemConfig.CallOpts) +} + +// OperatorFeeScalar is a free data retrieval call binding the contract method 0x4d5d9a2a. +// +// Solidity: function operatorFeeScalar() view returns(uint32) +func (_SystemConfig *SystemConfigCallerSession) OperatorFeeScalar() (uint32, error) { + return _SystemConfig.Contract.OperatorFeeScalar(&_SystemConfig.CallOpts) +} + +// OptimismMintableERC20Factory is a free data retrieval call binding the contract method 0x9b7d7f0a. +// +// Solidity: function optimismMintableERC20Factory() view returns(address addr_) +func (_SystemConfig *SystemConfigCaller) OptimismMintableERC20Factory(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "optimismMintableERC20Factory") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// OptimismMintableERC20Factory is a free data retrieval call binding the contract method 0x9b7d7f0a. +// +// Solidity: function optimismMintableERC20Factory() view returns(address addr_) +func (_SystemConfig *SystemConfigSession) OptimismMintableERC20Factory() (common.Address, error) { + return _SystemConfig.Contract.OptimismMintableERC20Factory(&_SystemConfig.CallOpts) +} + +// OptimismMintableERC20Factory is a free data retrieval call binding the contract method 0x9b7d7f0a. +// +// Solidity: function optimismMintableERC20Factory() view returns(address addr_) +func (_SystemConfig *SystemConfigCallerSession) OptimismMintableERC20Factory() (common.Address, error) { + return _SystemConfig.Contract.OptimismMintableERC20Factory(&_SystemConfig.CallOpts) +} + +// OptimismPortal is a free data retrieval call binding the contract method 0x0a49cb03. +// +// Solidity: function optimismPortal() view returns(address addr_) +func (_SystemConfig *SystemConfigCaller) OptimismPortal(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "optimismPortal") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// OptimismPortal is a free data retrieval call binding the contract method 0x0a49cb03. +// +// Solidity: function optimismPortal() view returns(address addr_) +func (_SystemConfig *SystemConfigSession) OptimismPortal() (common.Address, error) { + return _SystemConfig.Contract.OptimismPortal(&_SystemConfig.CallOpts) +} + +// OptimismPortal is a free data retrieval call binding the contract method 0x0a49cb03. +// +// Solidity: function optimismPortal() view returns(address addr_) +func (_SystemConfig *SystemConfigCallerSession) OptimismPortal() (common.Address, error) { + return _SystemConfig.Contract.OptimismPortal(&_SystemConfig.CallOpts) +} + +// Overhead is a free data retrieval call binding the contract method 0x0c18c162. +// +// Solidity: function overhead() view returns(uint256) +func (_SystemConfig *SystemConfigCaller) Overhead(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "overhead") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Overhead is a free data retrieval call binding the contract method 0x0c18c162. +// +// Solidity: function overhead() view returns(uint256) +func (_SystemConfig *SystemConfigSession) Overhead() (*big.Int, error) { + return _SystemConfig.Contract.Overhead(&_SystemConfig.CallOpts) +} + +// Overhead is a free data retrieval call binding the contract method 0x0c18c162. +// +// Solidity: function overhead() view returns(uint256) +func (_SystemConfig *SystemConfigCallerSession) Overhead() (*big.Int, error) { + return _SystemConfig.Contract.Overhead(&_SystemConfig.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_SystemConfig *SystemConfigCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_SystemConfig *SystemConfigSession) Owner() (common.Address, error) { + return _SystemConfig.Contract.Owner(&_SystemConfig.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_SystemConfig *SystemConfigCallerSession) Owner() (common.Address, error) { + return _SystemConfig.Contract.Owner(&_SystemConfig.CallOpts) +} + +// Paused is a free data retrieval call binding the contract method 0x5c975abb. +// +// Solidity: function paused() view returns(bool) +func (_SystemConfig *SystemConfigCaller) Paused(opts *bind.CallOpts) (bool, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "paused") + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// Paused is a free data retrieval call binding the contract method 0x5c975abb. +// +// Solidity: function paused() view returns(bool) +func (_SystemConfig *SystemConfigSession) Paused() (bool, error) { + return _SystemConfig.Contract.Paused(&_SystemConfig.CallOpts) +} + +// Paused is a free data retrieval call binding the contract method 0x5c975abb. +// +// Solidity: function paused() view returns(bool) +func (_SystemConfig *SystemConfigCallerSession) Paused() (bool, error) { + return _SystemConfig.Contract.Paused(&_SystemConfig.CallOpts) +} + +// ProxyAdmin is a free data retrieval call binding the contract method 0x3e47158c. +// +// Solidity: function proxyAdmin() view returns(address) +func (_SystemConfig *SystemConfigCaller) ProxyAdmin(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "proxyAdmin") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// ProxyAdmin is a free data retrieval call binding the contract method 0x3e47158c. +// +// Solidity: function proxyAdmin() view returns(address) +func (_SystemConfig *SystemConfigSession) ProxyAdmin() (common.Address, error) { + return _SystemConfig.Contract.ProxyAdmin(&_SystemConfig.CallOpts) +} + +// ProxyAdmin is a free data retrieval call binding the contract method 0x3e47158c. +// +// Solidity: function proxyAdmin() view returns(address) +func (_SystemConfig *SystemConfigCallerSession) ProxyAdmin() (common.Address, error) { + return _SystemConfig.Contract.ProxyAdmin(&_SystemConfig.CallOpts) +} + +// ProxyAdminOwner is a free data retrieval call binding the contract method 0xdad544e0. +// +// Solidity: function proxyAdminOwner() view returns(address) +func (_SystemConfig *SystemConfigCaller) ProxyAdminOwner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "proxyAdminOwner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// ProxyAdminOwner is a free data retrieval call binding the contract method 0xdad544e0. +// +// Solidity: function proxyAdminOwner() view returns(address) +func (_SystemConfig *SystemConfigSession) ProxyAdminOwner() (common.Address, error) { + return _SystemConfig.Contract.ProxyAdminOwner(&_SystemConfig.CallOpts) +} + +// ProxyAdminOwner is a free data retrieval call binding the contract method 0xdad544e0. +// +// Solidity: function proxyAdminOwner() view returns(address) +func (_SystemConfig *SystemConfigCallerSession) ProxyAdminOwner() (common.Address, error) { + return _SystemConfig.Contract.ProxyAdminOwner(&_SystemConfig.CallOpts) +} + +// ResourceConfig is a free data retrieval call binding the contract method 0xcc731b02. +// +// Solidity: function resourceConfig() view returns((uint32,uint8,uint8,uint32,uint32,uint128)) +func (_SystemConfig *SystemConfigCaller) ResourceConfig(opts *bind.CallOpts) (IResourceMeteringResourceConfig, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "resourceConfig") + + if err != nil { + return *new(IResourceMeteringResourceConfig), err + } + + out0 := *abi.ConvertType(out[0], new(IResourceMeteringResourceConfig)).(*IResourceMeteringResourceConfig) + + return out0, err + +} + +// ResourceConfig is a free data retrieval call binding the contract method 0xcc731b02. +// +// Solidity: function resourceConfig() view returns((uint32,uint8,uint8,uint32,uint32,uint128)) +func (_SystemConfig *SystemConfigSession) ResourceConfig() (IResourceMeteringResourceConfig, error) { + return _SystemConfig.Contract.ResourceConfig(&_SystemConfig.CallOpts) +} + +// ResourceConfig is a free data retrieval call binding the contract method 0xcc731b02. +// +// Solidity: function resourceConfig() view returns((uint32,uint8,uint8,uint32,uint32,uint128)) +func (_SystemConfig *SystemConfigCallerSession) ResourceConfig() (IResourceMeteringResourceConfig, error) { + return _SystemConfig.Contract.ResourceConfig(&_SystemConfig.CallOpts) +} + +// Scalar is a free data retrieval call binding the contract method 0xf45e65d8. +// +// Solidity: function scalar() view returns(uint256) +func (_SystemConfig *SystemConfigCaller) Scalar(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "scalar") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Scalar is a free data retrieval call binding the contract method 0xf45e65d8. +// +// Solidity: function scalar() view returns(uint256) +func (_SystemConfig *SystemConfigSession) Scalar() (*big.Int, error) { + return _SystemConfig.Contract.Scalar(&_SystemConfig.CallOpts) +} + +// Scalar is a free data retrieval call binding the contract method 0xf45e65d8. +// +// Solidity: function scalar() view returns(uint256) +func (_SystemConfig *SystemConfigCallerSession) Scalar() (*big.Int, error) { + return _SystemConfig.Contract.Scalar(&_SystemConfig.CallOpts) +} + +// StartBlock is a free data retrieval call binding the contract method 0x48cd4cb1. +// +// Solidity: function startBlock() view returns(uint256 startBlock_) +func (_SystemConfig *SystemConfigCaller) StartBlock(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "startBlock") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// StartBlock is a free data retrieval call binding the contract method 0x48cd4cb1. +// +// Solidity: function startBlock() view returns(uint256 startBlock_) +func (_SystemConfig *SystemConfigSession) StartBlock() (*big.Int, error) { + return _SystemConfig.Contract.StartBlock(&_SystemConfig.CallOpts) +} + +// StartBlock is a free data retrieval call binding the contract method 0x48cd4cb1. +// +// Solidity: function startBlock() view returns(uint256 startBlock_) +func (_SystemConfig *SystemConfigCallerSession) StartBlock() (*big.Int, error) { + return _SystemConfig.Contract.StartBlock(&_SystemConfig.CallOpts) +} + +// SuperchainConfig is a free data retrieval call binding the contract method 0x35e80ab3. +// +// Solidity: function superchainConfig() view returns(address) +func (_SystemConfig *SystemConfigCaller) SuperchainConfig(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "superchainConfig") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// SuperchainConfig is a free data retrieval call binding the contract method 0x35e80ab3. +// +// Solidity: function superchainConfig() view returns(address) +func (_SystemConfig *SystemConfigSession) SuperchainConfig() (common.Address, error) { + return _SystemConfig.Contract.SuperchainConfig(&_SystemConfig.CallOpts) +} + +// SuperchainConfig is a free data retrieval call binding the contract method 0x35e80ab3. +// +// Solidity: function superchainConfig() view returns(address) +func (_SystemConfig *SystemConfigCallerSession) SuperchainConfig() (common.Address, error) { + return _SystemConfig.Contract.SuperchainConfig(&_SystemConfig.CallOpts) +} + +// UnsafeBlockSigner is a free data retrieval call binding the contract method 0x1fd19ee1. +// +// Solidity: function unsafeBlockSigner() view returns(address addr_) +func (_SystemConfig *SystemConfigCaller) UnsafeBlockSigner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "unsafeBlockSigner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// UnsafeBlockSigner is a free data retrieval call binding the contract method 0x1fd19ee1. +// +// Solidity: function unsafeBlockSigner() view returns(address addr_) +func (_SystemConfig *SystemConfigSession) UnsafeBlockSigner() (common.Address, error) { + return _SystemConfig.Contract.UnsafeBlockSigner(&_SystemConfig.CallOpts) +} + +// UnsafeBlockSigner is a free data retrieval call binding the contract method 0x1fd19ee1. +// +// Solidity: function unsafeBlockSigner() view returns(address addr_) +func (_SystemConfig *SystemConfigCallerSession) UnsafeBlockSigner() (common.Address, error) { + return _SystemConfig.Contract.UnsafeBlockSigner(&_SystemConfig.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() pure returns(string) +func (_SystemConfig *SystemConfigCaller) Version(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _SystemConfig.contract.Call(opts, &out, "version") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() pure returns(string) +func (_SystemConfig *SystemConfigSession) Version() (string, error) { + return _SystemConfig.Contract.Version(&_SystemConfig.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() pure returns(string) +func (_SystemConfig *SystemConfigCallerSession) Version() (string, error) { + return _SystemConfig.Contract.Version(&_SystemConfig.CallOpts) +} + +// Initialize is a paid mutator transaction binding the contract method 0x2b598b8a. +// +// Solidity: function initialize(address _owner, uint32 _basefeeScalar, uint32 _blobbasefeeScalar, bytes32 _batcherHash, uint64 _gasLimit, address _unsafeBlockSigner, (uint32,uint8,uint8,uint32,uint32,uint128) _config, address _batchInbox, (address,address,address,address,address,address,address) _addresses, uint256 _l2ChainId, address _superchainConfig) returns() +func (_SystemConfig *SystemConfigTransactor) Initialize(opts *bind.TransactOpts, _owner common.Address, _basefeeScalar uint32, _blobbasefeeScalar uint32, _batcherHash [32]byte, _gasLimit uint64, _unsafeBlockSigner common.Address, _config IResourceMeteringResourceConfig, _batchInbox common.Address, _addresses SystemConfigAddresses, _l2ChainId *big.Int, _superchainConfig common.Address) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "initialize", _owner, _basefeeScalar, _blobbasefeeScalar, _batcherHash, _gasLimit, _unsafeBlockSigner, _config, _batchInbox, _addresses, _l2ChainId, _superchainConfig) +} + +// Initialize is a paid mutator transaction binding the contract method 0x2b598b8a. +// +// Solidity: function initialize(address _owner, uint32 _basefeeScalar, uint32 _blobbasefeeScalar, bytes32 _batcherHash, uint64 _gasLimit, address _unsafeBlockSigner, (uint32,uint8,uint8,uint32,uint32,uint128) _config, address _batchInbox, (address,address,address,address,address,address,address) _addresses, uint256 _l2ChainId, address _superchainConfig) returns() +func (_SystemConfig *SystemConfigSession) Initialize(_owner common.Address, _basefeeScalar uint32, _blobbasefeeScalar uint32, _batcherHash [32]byte, _gasLimit uint64, _unsafeBlockSigner common.Address, _config IResourceMeteringResourceConfig, _batchInbox common.Address, _addresses SystemConfigAddresses, _l2ChainId *big.Int, _superchainConfig common.Address) (*types.Transaction, error) { + return _SystemConfig.Contract.Initialize(&_SystemConfig.TransactOpts, _owner, _basefeeScalar, _blobbasefeeScalar, _batcherHash, _gasLimit, _unsafeBlockSigner, _config, _batchInbox, _addresses, _l2ChainId, _superchainConfig) +} + +// Initialize is a paid mutator transaction binding the contract method 0x2b598b8a. +// +// Solidity: function initialize(address _owner, uint32 _basefeeScalar, uint32 _blobbasefeeScalar, bytes32 _batcherHash, uint64 _gasLimit, address _unsafeBlockSigner, (uint32,uint8,uint8,uint32,uint32,uint128) _config, address _batchInbox, (address,address,address,address,address,address,address) _addresses, uint256 _l2ChainId, address _superchainConfig) returns() +func (_SystemConfig *SystemConfigTransactorSession) Initialize(_owner common.Address, _basefeeScalar uint32, _blobbasefeeScalar uint32, _batcherHash [32]byte, _gasLimit uint64, _unsafeBlockSigner common.Address, _config IResourceMeteringResourceConfig, _batchInbox common.Address, _addresses SystemConfigAddresses, _l2ChainId *big.Int, _superchainConfig common.Address) (*types.Transaction, error) { + return _SystemConfig.Contract.Initialize(&_SystemConfig.TransactOpts, _owner, _basefeeScalar, _blobbasefeeScalar, _batcherHash, _gasLimit, _unsafeBlockSigner, _config, _batchInbox, _addresses, _l2ChainId, _superchainConfig) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_SystemConfig *SystemConfigTransactor) RenounceOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "renounceOwnership") +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_SystemConfig *SystemConfigSession) RenounceOwnership() (*types.Transaction, error) { + return _SystemConfig.Contract.RenounceOwnership(&_SystemConfig.TransactOpts) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_SystemConfig *SystemConfigTransactorSession) RenounceOwnership() (*types.Transaction, error) { + return _SystemConfig.Contract.RenounceOwnership(&_SystemConfig.TransactOpts) +} + +// SetBatcherHash is a paid mutator transaction binding the contract method 0x0a2ca2a9. +// +// Solidity: function setBatcherHash(address _batcher) returns() +func (_SystemConfig *SystemConfigTransactor) SetBatcherHash(opts *bind.TransactOpts, _batcher common.Address) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "setBatcherHash", _batcher) +} + +// SetBatcherHash is a paid mutator transaction binding the contract method 0x0a2ca2a9. +// +// Solidity: function setBatcherHash(address _batcher) returns() +func (_SystemConfig *SystemConfigSession) SetBatcherHash(_batcher common.Address) (*types.Transaction, error) { + return _SystemConfig.Contract.SetBatcherHash(&_SystemConfig.TransactOpts, _batcher) +} + +// SetBatcherHash is a paid mutator transaction binding the contract method 0x0a2ca2a9. +// +// Solidity: function setBatcherHash(address _batcher) returns() +func (_SystemConfig *SystemConfigTransactorSession) SetBatcherHash(_batcher common.Address) (*types.Transaction, error) { + return _SystemConfig.Contract.SetBatcherHash(&_SystemConfig.TransactOpts, _batcher) +} + +// SetBatcherHash0 is a paid mutator transaction binding the contract method 0xc9b26f61. +// +// Solidity: function setBatcherHash(bytes32 _batcherHash) returns() +func (_SystemConfig *SystemConfigTransactor) SetBatcherHash0(opts *bind.TransactOpts, _batcherHash [32]byte) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "setBatcherHash0", _batcherHash) +} + +// SetBatcherHash0 is a paid mutator transaction binding the contract method 0xc9b26f61. +// +// Solidity: function setBatcherHash(bytes32 _batcherHash) returns() +func (_SystemConfig *SystemConfigSession) SetBatcherHash0(_batcherHash [32]byte) (*types.Transaction, error) { + return _SystemConfig.Contract.SetBatcherHash0(&_SystemConfig.TransactOpts, _batcherHash) +} + +// SetBatcherHash0 is a paid mutator transaction binding the contract method 0xc9b26f61. +// +// Solidity: function setBatcherHash(bytes32 _batcherHash) returns() +func (_SystemConfig *SystemConfigTransactorSession) SetBatcherHash0(_batcherHash [32]byte) (*types.Transaction, error) { + return _SystemConfig.Contract.SetBatcherHash0(&_SystemConfig.TransactOpts, _batcherHash) +} + +// SetDAFootprintGasScalar is a paid mutator transaction binding the contract method 0x20f06fdc. +// +// Solidity: function setDAFootprintGasScalar(uint16 _daFootprintGasScalar) returns() +func (_SystemConfig *SystemConfigTransactor) SetDAFootprintGasScalar(opts *bind.TransactOpts, _daFootprintGasScalar uint16) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "setDAFootprintGasScalar", _daFootprintGasScalar) +} + +// SetDAFootprintGasScalar is a paid mutator transaction binding the contract method 0x20f06fdc. +// +// Solidity: function setDAFootprintGasScalar(uint16 _daFootprintGasScalar) returns() +func (_SystemConfig *SystemConfigSession) SetDAFootprintGasScalar(_daFootprintGasScalar uint16) (*types.Transaction, error) { + return _SystemConfig.Contract.SetDAFootprintGasScalar(&_SystemConfig.TransactOpts, _daFootprintGasScalar) +} + +// SetDAFootprintGasScalar is a paid mutator transaction binding the contract method 0x20f06fdc. +// +// Solidity: function setDAFootprintGasScalar(uint16 _daFootprintGasScalar) returns() +func (_SystemConfig *SystemConfigTransactorSession) SetDAFootprintGasScalar(_daFootprintGasScalar uint16) (*types.Transaction, error) { + return _SystemConfig.Contract.SetDAFootprintGasScalar(&_SystemConfig.TransactOpts, _daFootprintGasScalar) +} + +// SetEIP1559Params is a paid mutator transaction binding the contract method 0xc0fd4b41. +// +// Solidity: function setEIP1559Params(uint32 _denominator, uint32 _elasticity) returns() +func (_SystemConfig *SystemConfigTransactor) SetEIP1559Params(opts *bind.TransactOpts, _denominator uint32, _elasticity uint32) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "setEIP1559Params", _denominator, _elasticity) +} + +// SetEIP1559Params is a paid mutator transaction binding the contract method 0xc0fd4b41. +// +// Solidity: function setEIP1559Params(uint32 _denominator, uint32 _elasticity) returns() +func (_SystemConfig *SystemConfigSession) SetEIP1559Params(_denominator uint32, _elasticity uint32) (*types.Transaction, error) { + return _SystemConfig.Contract.SetEIP1559Params(&_SystemConfig.TransactOpts, _denominator, _elasticity) +} + +// SetEIP1559Params is a paid mutator transaction binding the contract method 0xc0fd4b41. +// +// Solidity: function setEIP1559Params(uint32 _denominator, uint32 _elasticity) returns() +func (_SystemConfig *SystemConfigTransactorSession) SetEIP1559Params(_denominator uint32, _elasticity uint32) (*types.Transaction, error) { + return _SystemConfig.Contract.SetEIP1559Params(&_SystemConfig.TransactOpts, _denominator, _elasticity) +} + +// SetFeature is a paid mutator transaction binding the contract method 0xf2c4bc9e. +// +// Solidity: function setFeature(bytes32 _feature, bool _enabled) returns() +func (_SystemConfig *SystemConfigTransactor) SetFeature(opts *bind.TransactOpts, _feature [32]byte, _enabled bool) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "setFeature", _feature, _enabled) +} + +// SetFeature is a paid mutator transaction binding the contract method 0xf2c4bc9e. +// +// Solidity: function setFeature(bytes32 _feature, bool _enabled) returns() +func (_SystemConfig *SystemConfigSession) SetFeature(_feature [32]byte, _enabled bool) (*types.Transaction, error) { + return _SystemConfig.Contract.SetFeature(&_SystemConfig.TransactOpts, _feature, _enabled) +} + +// SetFeature is a paid mutator transaction binding the contract method 0xf2c4bc9e. +// +// Solidity: function setFeature(bytes32 _feature, bool _enabled) returns() +func (_SystemConfig *SystemConfigTransactorSession) SetFeature(_feature [32]byte, _enabled bool) (*types.Transaction, error) { + return _SystemConfig.Contract.SetFeature(&_SystemConfig.TransactOpts, _feature, _enabled) +} + +// SetGasConfig is a paid mutator transaction binding the contract method 0x935f029e. +// +// Solidity: function setGasConfig(uint256 _overhead, uint256 _scalar) returns() +func (_SystemConfig *SystemConfigTransactor) SetGasConfig(opts *bind.TransactOpts, _overhead *big.Int, _scalar *big.Int) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "setGasConfig", _overhead, _scalar) +} + +// SetGasConfig is a paid mutator transaction binding the contract method 0x935f029e. +// +// Solidity: function setGasConfig(uint256 _overhead, uint256 _scalar) returns() +func (_SystemConfig *SystemConfigSession) SetGasConfig(_overhead *big.Int, _scalar *big.Int) (*types.Transaction, error) { + return _SystemConfig.Contract.SetGasConfig(&_SystemConfig.TransactOpts, _overhead, _scalar) +} + +// SetGasConfig is a paid mutator transaction binding the contract method 0x935f029e. +// +// Solidity: function setGasConfig(uint256 _overhead, uint256 _scalar) returns() +func (_SystemConfig *SystemConfigTransactorSession) SetGasConfig(_overhead *big.Int, _scalar *big.Int) (*types.Transaction, error) { + return _SystemConfig.Contract.SetGasConfig(&_SystemConfig.TransactOpts, _overhead, _scalar) +} + +// SetGasConfigEcotone is a paid mutator transaction binding the contract method 0x21d7fde5. +// +// Solidity: function setGasConfigEcotone(uint32 _basefeeScalar, uint32 _blobbasefeeScalar) returns() +func (_SystemConfig *SystemConfigTransactor) SetGasConfigEcotone(opts *bind.TransactOpts, _basefeeScalar uint32, _blobbasefeeScalar uint32) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "setGasConfigEcotone", _basefeeScalar, _blobbasefeeScalar) +} + +// SetGasConfigEcotone is a paid mutator transaction binding the contract method 0x21d7fde5. +// +// Solidity: function setGasConfigEcotone(uint32 _basefeeScalar, uint32 _blobbasefeeScalar) returns() +func (_SystemConfig *SystemConfigSession) SetGasConfigEcotone(_basefeeScalar uint32, _blobbasefeeScalar uint32) (*types.Transaction, error) { + return _SystemConfig.Contract.SetGasConfigEcotone(&_SystemConfig.TransactOpts, _basefeeScalar, _blobbasefeeScalar) +} + +// SetGasConfigEcotone is a paid mutator transaction binding the contract method 0x21d7fde5. +// +// Solidity: function setGasConfigEcotone(uint32 _basefeeScalar, uint32 _blobbasefeeScalar) returns() +func (_SystemConfig *SystemConfigTransactorSession) SetGasConfigEcotone(_basefeeScalar uint32, _blobbasefeeScalar uint32) (*types.Transaction, error) { + return _SystemConfig.Contract.SetGasConfigEcotone(&_SystemConfig.TransactOpts, _basefeeScalar, _blobbasefeeScalar) +} + +// SetGasLimit is a paid mutator transaction binding the contract method 0xb40a817c. +// +// Solidity: function setGasLimit(uint64 _gasLimit) returns() +func (_SystemConfig *SystemConfigTransactor) SetGasLimit(opts *bind.TransactOpts, _gasLimit uint64) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "setGasLimit", _gasLimit) +} + +// SetGasLimit is a paid mutator transaction binding the contract method 0xb40a817c. +// +// Solidity: function setGasLimit(uint64 _gasLimit) returns() +func (_SystemConfig *SystemConfigSession) SetGasLimit(_gasLimit uint64) (*types.Transaction, error) { + return _SystemConfig.Contract.SetGasLimit(&_SystemConfig.TransactOpts, _gasLimit) +} + +// SetGasLimit is a paid mutator transaction binding the contract method 0xb40a817c. +// +// Solidity: function setGasLimit(uint64 _gasLimit) returns() +func (_SystemConfig *SystemConfigTransactorSession) SetGasLimit(_gasLimit uint64) (*types.Transaction, error) { + return _SystemConfig.Contract.SetGasLimit(&_SystemConfig.TransactOpts, _gasLimit) +} + +// SetMinBaseFee is a paid mutator transaction binding the contract method 0x7616f0e8. +// +// Solidity: function setMinBaseFee(uint64 _minBaseFee) returns() +func (_SystemConfig *SystemConfigTransactor) SetMinBaseFee(opts *bind.TransactOpts, _minBaseFee uint64) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "setMinBaseFee", _minBaseFee) +} + +// SetMinBaseFee is a paid mutator transaction binding the contract method 0x7616f0e8. +// +// Solidity: function setMinBaseFee(uint64 _minBaseFee) returns() +func (_SystemConfig *SystemConfigSession) SetMinBaseFee(_minBaseFee uint64) (*types.Transaction, error) { + return _SystemConfig.Contract.SetMinBaseFee(&_SystemConfig.TransactOpts, _minBaseFee) +} + +// SetMinBaseFee is a paid mutator transaction binding the contract method 0x7616f0e8. +// +// Solidity: function setMinBaseFee(uint64 _minBaseFee) returns() +func (_SystemConfig *SystemConfigTransactorSession) SetMinBaseFee(_minBaseFee uint64) (*types.Transaction, error) { + return _SystemConfig.Contract.SetMinBaseFee(&_SystemConfig.TransactOpts, _minBaseFee) +} + +// SetOperatorFeeScalars is a paid mutator transaction binding the contract method 0x155b6c6f. +// +// Solidity: function setOperatorFeeScalars(uint32 _operatorFeeScalar, uint64 _operatorFeeConstant) returns() +func (_SystemConfig *SystemConfigTransactor) SetOperatorFeeScalars(opts *bind.TransactOpts, _operatorFeeScalar uint32, _operatorFeeConstant uint64) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "setOperatorFeeScalars", _operatorFeeScalar, _operatorFeeConstant) +} + +// SetOperatorFeeScalars is a paid mutator transaction binding the contract method 0x155b6c6f. +// +// Solidity: function setOperatorFeeScalars(uint32 _operatorFeeScalar, uint64 _operatorFeeConstant) returns() +func (_SystemConfig *SystemConfigSession) SetOperatorFeeScalars(_operatorFeeScalar uint32, _operatorFeeConstant uint64) (*types.Transaction, error) { + return _SystemConfig.Contract.SetOperatorFeeScalars(&_SystemConfig.TransactOpts, _operatorFeeScalar, _operatorFeeConstant) +} + +// SetOperatorFeeScalars is a paid mutator transaction binding the contract method 0x155b6c6f. +// +// Solidity: function setOperatorFeeScalars(uint32 _operatorFeeScalar, uint64 _operatorFeeConstant) returns() +func (_SystemConfig *SystemConfigTransactorSession) SetOperatorFeeScalars(_operatorFeeScalar uint32, _operatorFeeConstant uint64) (*types.Transaction, error) { + return _SystemConfig.Contract.SetOperatorFeeScalars(&_SystemConfig.TransactOpts, _operatorFeeScalar, _operatorFeeConstant) +} + +// SetUnsafeBlockSigner is a paid mutator transaction binding the contract method 0x18d13918. +// +// Solidity: function setUnsafeBlockSigner(address _unsafeBlockSigner) returns() +func (_SystemConfig *SystemConfigTransactor) SetUnsafeBlockSigner(opts *bind.TransactOpts, _unsafeBlockSigner common.Address) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "setUnsafeBlockSigner", _unsafeBlockSigner) +} + +// SetUnsafeBlockSigner is a paid mutator transaction binding the contract method 0x18d13918. +// +// Solidity: function setUnsafeBlockSigner(address _unsafeBlockSigner) returns() +func (_SystemConfig *SystemConfigSession) SetUnsafeBlockSigner(_unsafeBlockSigner common.Address) (*types.Transaction, error) { + return _SystemConfig.Contract.SetUnsafeBlockSigner(&_SystemConfig.TransactOpts, _unsafeBlockSigner) +} + +// SetUnsafeBlockSigner is a paid mutator transaction binding the contract method 0x18d13918. +// +// Solidity: function setUnsafeBlockSigner(address _unsafeBlockSigner) returns() +func (_SystemConfig *SystemConfigTransactorSession) SetUnsafeBlockSigner(_unsafeBlockSigner common.Address) (*types.Transaction, error) { + return _SystemConfig.Contract.SetUnsafeBlockSigner(&_SystemConfig.TransactOpts, _unsafeBlockSigner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_SystemConfig *SystemConfigTransactor) TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*types.Transaction, error) { + return _SystemConfig.contract.Transact(opts, "transferOwnership", newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_SystemConfig *SystemConfigSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _SystemConfig.Contract.TransferOwnership(&_SystemConfig.TransactOpts, newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_SystemConfig *SystemConfigTransactorSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _SystemConfig.Contract.TransferOwnership(&_SystemConfig.TransactOpts, newOwner) +} + +// SystemConfigConfigUpdateIterator is returned from FilterConfigUpdate and is used to iterate over the raw logs and unpacked data for ConfigUpdate events raised by the SystemConfig contract. +type SystemConfigConfigUpdateIterator struct { + Event *SystemConfigConfigUpdate // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *SystemConfigConfigUpdateIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(SystemConfigConfigUpdate) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(SystemConfigConfigUpdate) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *SystemConfigConfigUpdateIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *SystemConfigConfigUpdateIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// SystemConfigConfigUpdate represents a ConfigUpdate event raised by the SystemConfig contract. +type SystemConfigConfigUpdate struct { + Version *big.Int + UpdateType uint8 + Data []byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterConfigUpdate is a free log retrieval operation binding the contract event 0x1d2b0bda21d56b8bd12d4f94ebacffdfb35f5e226f84b461103bb8beab6353be. +// +// Solidity: event ConfigUpdate(uint256 indexed version, uint8 indexed updateType, bytes data) +func (_SystemConfig *SystemConfigFilterer) FilterConfigUpdate(opts *bind.FilterOpts, version []*big.Int, updateType []uint8) (*SystemConfigConfigUpdateIterator, error) { + + var versionRule []interface{} + for _, versionItem := range version { + versionRule = append(versionRule, versionItem) + } + var updateTypeRule []interface{} + for _, updateTypeItem := range updateType { + updateTypeRule = append(updateTypeRule, updateTypeItem) + } + + logs, sub, err := _SystemConfig.contract.FilterLogs(opts, "ConfigUpdate", versionRule, updateTypeRule) + if err != nil { + return nil, err + } + return &SystemConfigConfigUpdateIterator{contract: _SystemConfig.contract, event: "ConfigUpdate", logs: logs, sub: sub}, nil +} + +// WatchConfigUpdate is a free log subscription operation binding the contract event 0x1d2b0bda21d56b8bd12d4f94ebacffdfb35f5e226f84b461103bb8beab6353be. +// +// Solidity: event ConfigUpdate(uint256 indexed version, uint8 indexed updateType, bytes data) +func (_SystemConfig *SystemConfigFilterer) WatchConfigUpdate(opts *bind.WatchOpts, sink chan<- *SystemConfigConfigUpdate, version []*big.Int, updateType []uint8) (event.Subscription, error) { + + var versionRule []interface{} + for _, versionItem := range version { + versionRule = append(versionRule, versionItem) + } + var updateTypeRule []interface{} + for _, updateTypeItem := range updateType { + updateTypeRule = append(updateTypeRule, updateTypeItem) + } + + logs, sub, err := _SystemConfig.contract.WatchLogs(opts, "ConfigUpdate", versionRule, updateTypeRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(SystemConfigConfigUpdate) + if err := _SystemConfig.contract.UnpackLog(event, "ConfigUpdate", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseConfigUpdate is a log parse operation binding the contract event 0x1d2b0bda21d56b8bd12d4f94ebacffdfb35f5e226f84b461103bb8beab6353be. +// +// Solidity: event ConfigUpdate(uint256 indexed version, uint8 indexed updateType, bytes data) +func (_SystemConfig *SystemConfigFilterer) ParseConfigUpdate(log types.Log) (*SystemConfigConfigUpdate, error) { + event := new(SystemConfigConfigUpdate) + if err := _SystemConfig.contract.UnpackLog(event, "ConfigUpdate", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// SystemConfigFeatureSetIterator is returned from FilterFeatureSet and is used to iterate over the raw logs and unpacked data for FeatureSet events raised by the SystemConfig contract. +type SystemConfigFeatureSetIterator struct { + Event *SystemConfigFeatureSet // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *SystemConfigFeatureSetIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(SystemConfigFeatureSet) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(SystemConfigFeatureSet) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *SystemConfigFeatureSetIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *SystemConfigFeatureSetIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// SystemConfigFeatureSet represents a FeatureSet event raised by the SystemConfig contract. +type SystemConfigFeatureSet struct { + Feature [32]byte + Enabled bool + Raw types.Log // Blockchain specific contextual infos +} + +// FilterFeatureSet is a free log retrieval operation binding the contract event 0xb876f6594132c89891d2fd198e925e999be741ec809abb58bfe9b966876cc06c. +// +// Solidity: event FeatureSet(bytes32 indexed feature, bool indexed enabled) +func (_SystemConfig *SystemConfigFilterer) FilterFeatureSet(opts *bind.FilterOpts, feature [][32]byte, enabled []bool) (*SystemConfigFeatureSetIterator, error) { + + var featureRule []interface{} + for _, featureItem := range feature { + featureRule = append(featureRule, featureItem) + } + var enabledRule []interface{} + for _, enabledItem := range enabled { + enabledRule = append(enabledRule, enabledItem) + } + + logs, sub, err := _SystemConfig.contract.FilterLogs(opts, "FeatureSet", featureRule, enabledRule) + if err != nil { + return nil, err + } + return &SystemConfigFeatureSetIterator{contract: _SystemConfig.contract, event: "FeatureSet", logs: logs, sub: sub}, nil +} + +// WatchFeatureSet is a free log subscription operation binding the contract event 0xb876f6594132c89891d2fd198e925e999be741ec809abb58bfe9b966876cc06c. +// +// Solidity: event FeatureSet(bytes32 indexed feature, bool indexed enabled) +func (_SystemConfig *SystemConfigFilterer) WatchFeatureSet(opts *bind.WatchOpts, sink chan<- *SystemConfigFeatureSet, feature [][32]byte, enabled []bool) (event.Subscription, error) { + + var featureRule []interface{} + for _, featureItem := range feature { + featureRule = append(featureRule, featureItem) + } + var enabledRule []interface{} + for _, enabledItem := range enabled { + enabledRule = append(enabledRule, enabledItem) + } + + logs, sub, err := _SystemConfig.contract.WatchLogs(opts, "FeatureSet", featureRule, enabledRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(SystemConfigFeatureSet) + if err := _SystemConfig.contract.UnpackLog(event, "FeatureSet", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseFeatureSet is a log parse operation binding the contract event 0xb876f6594132c89891d2fd198e925e999be741ec809abb58bfe9b966876cc06c. +// +// Solidity: event FeatureSet(bytes32 indexed feature, bool indexed enabled) +func (_SystemConfig *SystemConfigFilterer) ParseFeatureSet(log types.Log) (*SystemConfigFeatureSet, error) { + event := new(SystemConfigFeatureSet) + if err := _SystemConfig.contract.UnpackLog(event, "FeatureSet", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// SystemConfigInitializedIterator is returned from FilterInitialized and is used to iterate over the raw logs and unpacked data for Initialized events raised by the SystemConfig contract. +type SystemConfigInitializedIterator struct { + Event *SystemConfigInitialized // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *SystemConfigInitializedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(SystemConfigInitialized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(SystemConfigInitialized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *SystemConfigInitializedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *SystemConfigInitializedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// SystemConfigInitialized represents a Initialized event raised by the SystemConfig contract. +type SystemConfigInitialized struct { + Version uint8 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInitialized is a free log retrieval operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_SystemConfig *SystemConfigFilterer) FilterInitialized(opts *bind.FilterOpts) (*SystemConfigInitializedIterator, error) { + + logs, sub, err := _SystemConfig.contract.FilterLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return &SystemConfigInitializedIterator{contract: _SystemConfig.contract, event: "Initialized", logs: logs, sub: sub}, nil +} + +// WatchInitialized is a free log subscription operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_SystemConfig *SystemConfigFilterer) WatchInitialized(opts *bind.WatchOpts, sink chan<- *SystemConfigInitialized) (event.Subscription, error) { + + logs, sub, err := _SystemConfig.contract.WatchLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(SystemConfigInitialized) + if err := _SystemConfig.contract.UnpackLog(event, "Initialized", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseInitialized is a log parse operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_SystemConfig *SystemConfigFilterer) ParseInitialized(log types.Log) (*SystemConfigInitialized, error) { + event := new(SystemConfigInitialized) + if err := _SystemConfig.contract.UnpackLog(event, "Initialized", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// SystemConfigOwnershipTransferredIterator is returned from FilterOwnershipTransferred and is used to iterate over the raw logs and unpacked data for OwnershipTransferred events raised by the SystemConfig contract. +type SystemConfigOwnershipTransferredIterator struct { + Event *SystemConfigOwnershipTransferred // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *SystemConfigOwnershipTransferredIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(SystemConfigOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(SystemConfigOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *SystemConfigOwnershipTransferredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *SystemConfigOwnershipTransferredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// SystemConfigOwnershipTransferred represents a OwnershipTransferred event raised by the SystemConfig contract. +type SystemConfigOwnershipTransferred struct { + PreviousOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferred is a free log retrieval operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_SystemConfig *SystemConfigFilterer) FilterOwnershipTransferred(opts *bind.FilterOpts, previousOwner []common.Address, newOwner []common.Address) (*SystemConfigOwnershipTransferredIterator, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _SystemConfig.contract.FilterLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &SystemConfigOwnershipTransferredIterator{contract: _SystemConfig.contract, event: "OwnershipTransferred", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferred is a free log subscription operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_SystemConfig *SystemConfigFilterer) WatchOwnershipTransferred(opts *bind.WatchOpts, sink chan<- *SystemConfigOwnershipTransferred, previousOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _SystemConfig.contract.WatchLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(SystemConfigOwnershipTransferred) + if err := _SystemConfig.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferred is a log parse operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_SystemConfig *SystemConfigFilterer) ParseOwnershipTransferred(log types.Log) (*SystemConfigOwnershipTransferred, error) { + event := new(SystemConfigOwnershipTransferred) + if err := _SystemConfig.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/op-batcher/batcher/espresso_active.go b/op-batcher/batcher/espresso_active.go index 964e2b1b4ea..a66c6c38a75 100644 --- a/op-batcher/batcher/espresso_active.go +++ b/op-batcher/batcher/espresso_active.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum-optimism/optimism/espresso/bindings" ) @@ -13,12 +14,13 @@ import ( // the BatchAuthenticator contract. Returns true if this batcher instance should // be publishing batches, false if it should stay idle. // -// The active batcher is determined by the contract's activeIsEspresso flag: -// - If activeIsEspresso is true, the Espresso batcher address is active -// - If activeIsEspresso is false, the fallback batcher address is active -// -// This method compares the batcher's own role (Config.Espresso.Enabled) -// against the contract's activeIsEspresso flag. +// It applies two gates: +// 1. Mode: the contract's activeIsEspresso flag must match this node's role +// (Config.Espresso.Enabled). activeIsEspresso==true means the Espresso batcher +// is active; false means the fallback batcher is active. +// 2. Identity: once the mode matches, the configured sender key (Txmgr.From) must +// be the authorized batcher for that mode, otherwise every authenticateBatchInfo +// call reverts (Unauthorized{Espresso,Fallback}Batcher) and the batcher loops. func (l *BatchSubmitter) isBatcherActive(ctx context.Context) (bool, error) { // Check if contract code exists at the address code, err := l.L1Client.CodeAt(ctx, l.RollupConfig.BatchAuthenticatorAddress, nil) @@ -46,16 +48,59 @@ func (l *BatchSubmitter) isBatcherActive(ctx context.Context) (bool, error) { batcherAddr := l.Txmgr.From() - isActive := (activeIsEspresso && l.Config.Espresso.Enabled) || + modeActive := (activeIsEspresso && l.Config.Espresso.Enabled) || (!activeIsEspresso && !l.Config.Espresso.Enabled) - - if !isActive { + if !modeActive { l.Log.Warn("Batcher is not the active batcher, skipping publish", "batcherAddr", batcherAddr, "activeIsEspresso", activeIsEspresso, "EspressoEnabled", l.Config.Espresso.Enabled, ) + return false, nil + } + + // Our mode is active; make sure our sender key is the authorized batcher for it, + // otherwise every publish reverts (Unauthorized*Batcher) in a loop. + var expected common.Address + if activeIsEspresso { + expected, err = batchAuthenticator.EspressoBatcher(callOpts) + if err != nil { + return false, fmt.Errorf("failed to read espressoBatcher: %w", err) + } + } else { + expected, err = l.fallbackBatcherAddr(batchAuthenticator, callOpts) + if err != nil { + return false, err + } + } + + if batcherAddr != expected { + l.Log.Warn("Configured batcher key is not the authorized batcher for the active mode, skipping publish", + "batcherAddr", batcherAddr, + "expected", expected, + "activeIsEspresso", activeIsEspresso, + ) + return false, nil } - return isActive, nil + return true, nil +} + +// fallbackBatcherAddr returns the address the BatchAuthenticator's fallback batcher +func (l *BatchSubmitter) fallbackBatcherAddr(batchAuthenticator *bindings.BatchAuthenticator, opts *bind.CallOpts) (common.Address, error) { + systemConfigAddr, err := batchAuthenticator.SystemConfig(opts) + if err != nil { + return common.Address{}, fmt.Errorf("failed to read systemConfig address: %w", err) + } + systemConfig, err := bindings.NewSystemConfigCaller(systemConfigAddr, l.L1Client) + if err != nil { + return common.Address{}, fmt.Errorf("failed to create SystemConfig binding: %w", err) + } + batcherHash, err := systemConfig.BatcherHash(opts) + if err != nil { + return common.Address{}, fmt.Errorf("failed to read batcherHash: %w", err) + } + // batcherHash stores the batcher address in its low 20 bytes, + // which is why we use bytes to address + return common.BytesToAddress(batcherHash[:]), nil } From 7e5d5913cf85d320e78d5820557f5b4b3a9077e7 Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Tue, 28 Jul 2026 16:56:07 -0400 Subject: [PATCH 22/25] copy espresso.go over from our fork --- op-service/txmgr/cli.go | 10 ++++++++-- op-service/txmgr/espresso.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 op-service/txmgr/espresso.go diff --git a/op-service/txmgr/cli.go b/op-service/txmgr/cli.go index 054c8f5ba0d..6fa048fa72c 100644 --- a/op-service/txmgr/cli.go +++ b/op-service/txmgr/cli.go @@ -491,7 +491,7 @@ func NewConfig(cfg CLIConfig, l log.Logger) (*Config, error) { hdPath = cfg.L2OutputHDPath } - signerFactory, from, err := opcrypto.SignerFactoryFromConfig(l, cfg.PrivateKey, cfg.Mnemonic, hdPath, cfg.SignerCLIConfig) + chainSignerFactory, from, err := opcrypto.ChainSignerFactoryFromConfig(l, cfg.PrivateKey, cfg.Mnemonic, hdPath, cfg.SignerCLIConfig) if err != nil { return nil, fmt.Errorf("could not init signer: %w", err) } @@ -527,13 +527,16 @@ func NewConfig(cfg CLIConfig, l log.Logger) (*Config, error) { } cellProofTime := fallbackToOsakaCellProofTimeIfKnown(chainID, cfg.CellProofTime) + chainSigner := chainSignerFactory(chainID, from) res := Config{ Backend: l1, ChainID: chainID, - Signer: signerFactory(chainID), + Signer: chainSigner.SignTransaction, From: from, + ChainSigner: chainSigner, + TxSendTimeout: cfg.TxSendTimeout, TxNotInMempoolTimeout: cfg.TxNotInMempoolTimeout, NetworkTimeout: cfg.NetworkTimeout, @@ -664,6 +667,9 @@ type Config struct { Signer opcrypto.SignerFn From common.Address + // ChainSigner is used to allow for easy signing of transactions and arbitrary data. + ChainSigner opcrypto.ChainSigner + // GasPriceEstimatorFn is used to estimate the gas price for a transaction. // If nil, DefaultGasPriceEstimatorFn is used. GasPriceEstimatorFn GasPriceEstimatorFn diff --git a/op-service/txmgr/espresso.go b/op-service/txmgr/espresso.go new file mode 100644 index 00000000000..1784f9fad14 --- /dev/null +++ b/op-service/txmgr/espresso.go @@ -0,0 +1,35 @@ +package txmgr + +import ( + "context" + + opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// Sign is a function that provides the ability to sign a transaction +func (c *Config) SignTransaction(ctx context.Context, address common.Address, tx *types.Transaction) (*types.Transaction, error) { + return c.ChainSigner.SignTransaction(ctx, address, tx) +} + +// Sign is a function that provides the ability to sign a hash +func (c *Config) Sign(ctx context.Context, hash []byte) ([]byte, error) { + return c.ChainSigner.Sign(ctx, hash) +} + +// Ensure adherence to the interface +var _ opcrypto.ChainSigner = &Config{} + +// SignTransaction is a function that provides the ability to sign a transaction +func (m *SimpleTxManager) SignTransaction(ctx context.Context, address common.Address, tx *types.Transaction) (*types.Transaction, error) { + return m.cfg.SignTransaction(ctx, address, tx) +} + +// Sign is a function that provides the ability to sign a hash +func (m *SimpleTxManager) Sign(ctx context.Context, hash []byte) ([]byte, error) { + return m.cfg.Sign(ctx, hash) +} + +// Ensure adherence to the interface +var _ opcrypto.ChainSigner = &SimpleTxManager{} From d79d8f01a6a8f05cd5f606c02fb74ace6d488f3b Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Tue, 28 Jul 2026 16:58:57 -0400 Subject: [PATCH 23/25] proper version of streamer --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index dfefbe22b90..ec4b66da367 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.24.13 require ( github.com/BurntSushi/toml v1.5.0 github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 - github.com/EspressoSystems/espresso-streamers v1.2.1-0.20260716191442-1884a718fbf7 + github.com/EspressoSystems/espresso-streamers v1.3.0 github.com/Masterminds/semver/v3 v3.3.1 github.com/andybalholm/brotli v1.1.0 github.com/base/go-bip39 v1.1.0 diff --git a/go.sum b/go.sum index a0e46cb0056..de973ef8869 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e h1:ZIWapoIRN1VqT8GR github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 h1:4KqlmnDg8pPPj1hCtiLnN0Fh9ltp+PPKOft9I06zQOA= github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513/go.mod h1:kaxR08mJb5Mijy7a2RhWCIWOevFI4PcXwDkzoEbsVTk= -github.com/EspressoSystems/espresso-streamers v1.2.1-0.20260716191442-1884a718fbf7 h1:PQVHO5oq8/WUjMwVkWB7MM0fqOmfCMQ72ak5fSl6L3s= -github.com/EspressoSystems/espresso-streamers v1.2.1-0.20260716191442-1884a718fbf7/go.mod h1:Op3SNwQnZ3bqwrUXMAORnL2/pNiFzpfOED4ltYs5o/U= +github.com/EspressoSystems/espresso-streamers v1.3.0 h1:LapR/wdCgbsiGzg13fsxitSDDzwauOBretOGJsj06p0= +github.com/EspressoSystems/espresso-streamers v1.3.0/go.mod h1:Op3SNwQnZ3bqwrUXMAORnL2/pNiFzpfOED4ltYs5o/U= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= From 57f013d25874843cb1560773aeebb9bfdfe50f91 Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Tue, 28 Jul 2026 17:16:22 -0400 Subject: [PATCH 24/25] address comment, remove uneeded methods and compile time assertion --- op-service/txmgr/espresso.go | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/op-service/txmgr/espresso.go b/op-service/txmgr/espresso.go index 1784f9fad14..37bf98190db 100644 --- a/op-service/txmgr/espresso.go +++ b/op-service/txmgr/espresso.go @@ -8,27 +8,14 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) -// Sign is a function that provides the ability to sign a transaction -func (c *Config) SignTransaction(ctx context.Context, address common.Address, tx *types.Transaction) (*types.Transaction, error) { - return c.ChainSigner.SignTransaction(ctx, address, tx) -} - -// Sign is a function that provides the ability to sign a hash -func (c *Config) Sign(ctx context.Context, hash []byte) ([]byte, error) { - return c.ChainSigner.Sign(ctx, hash) -} - -// Ensure adherence to the interface -var _ opcrypto.ChainSigner = &Config{} - // SignTransaction is a function that provides the ability to sign a transaction func (m *SimpleTxManager) SignTransaction(ctx context.Context, address common.Address, tx *types.Transaction) (*types.Transaction, error) { - return m.cfg.SignTransaction(ctx, address, tx) + return m.cfg.ChainSigner.SignTransaction(ctx, address, tx) } // Sign is a function that provides the ability to sign a hash func (m *SimpleTxManager) Sign(ctx context.Context, hash []byte) ([]byte, error) { - return m.cfg.Sign(ctx, hash) + return m.cfg.ChainSigner.Sign(ctx, hash) } // Ensure adherence to the interface From 8a27289f9d24bf17d0dccc3d3b51efedb99d6321 Mon Sep 17 00:00:00 2001 From: Luke Iannucci Date: Fri, 31 Jul 2026 08:27:34 -0400 Subject: [PATCH 25/25] use sync status interface for streamer --- espresso/cli.go | 4 +++- go.mod | 2 +- go.sum | 4 ++-- op-batcher/batcher/driver.go | 3 +++ op-batcher/batcher/espresso.go | 6 +++++- op-batcher/batcher/espresso_driver.go | 23 ++++++++++++++++++++++- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/espresso/cli.go b/espresso/cli.go index b16fc7f01c4..e6213171377 100644 --- a/espresso/cli.go +++ b/espresso/cli.go @@ -266,7 +266,8 @@ func ReadCLIConfig(c *cli.Context) CLIConfig { func BatchStreamerFromCLIConfig[B espressoStreamers.Batch]( cfg CLIConfig, log log.Logger, - unmarshalBatch func([]byte) (*B, error), + unmarshalBatch func(data []byte, l1Finalized uint64) (*B, error), + syncStatusProvider espressoStreamers.SyncStatusProvider, ) (*espressoStreamers.BatchStreamer[B], error) { if !cfg.Enabled { return nil, fmt.Errorf("espresso is not enabled") @@ -296,6 +297,7 @@ func BatchStreamerFromCLIConfig[B espressoStreamers.Batch]( NewAdaptL1BlockRefClient(RollupL1Client), espressoClient, espressoLightClient, + syncStatusProvider, log, unmarshalBatch, cfg.CaffeinationHeightEspresso, diff --git a/go.mod b/go.mod index ec4b66da367..1a7fe6d6934 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.24.13 require ( github.com/BurntSushi/toml v1.5.0 github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 - github.com/EspressoSystems/espresso-streamers v1.3.0 + github.com/EspressoSystems/espresso-streamers v1.3.1-0.20260731122451-83eab3d058ed github.com/Masterminds/semver/v3 v3.3.1 github.com/andybalholm/brotli v1.1.0 github.com/base/go-bip39 v1.1.0 diff --git a/go.sum b/go.sum index de973ef8869..c49842d2647 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e h1:ZIWapoIRN1VqT8GR github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 h1:4KqlmnDg8pPPj1hCtiLnN0Fh9ltp+PPKOft9I06zQOA= github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513/go.mod h1:kaxR08mJb5Mijy7a2RhWCIWOevFI4PcXwDkzoEbsVTk= -github.com/EspressoSystems/espresso-streamers v1.3.0 h1:LapR/wdCgbsiGzg13fsxitSDDzwauOBretOGJsj06p0= -github.com/EspressoSystems/espresso-streamers v1.3.0/go.mod h1:Op3SNwQnZ3bqwrUXMAORnL2/pNiFzpfOED4ltYs5o/U= +github.com/EspressoSystems/espresso-streamers v1.3.1-0.20260731122451-83eab3d058ed h1:Xm8wCCXQMvvXLPH/Ns9Rl3rhrQXSk9SBVwYN9JUxsqY= +github.com/EspressoSystems/espresso-streamers v1.3.1-0.20260731122451-83eab3d058ed/go.mod h1:Op3SNwQnZ3bqwrUXMAORnL2/pNiFzpfOED4ltYs5o/U= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 29d0d16e322..070b26e4cb8 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -159,6 +159,9 @@ type BatchSubmitter struct { espressoSubmitter *espressoTransactionSubmitter espressoStreamer espressoStreamers.EspressoStreamer[derivation.EspressoBatch] + // espressoSyncStatus feeds the streamer the sync status this batcher has already + // polled, so Refresh does not poll op-node again. + espressoSyncStatus *cachedSyncStatus // clearStateRequested asks the espresso batch loading loop to run clearState clearStateRequested atomic.Bool diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go index 1bdda9d6766..9038f4d8c7a 100644 --- a/op-batcher/batcher/espresso.go +++ b/op-batcher/batcher/espresso.go @@ -823,7 +823,11 @@ func (l *BatchSubmitter) queueBlockToEspresso(ctx context.Context, block *types. } func (l *BatchSubmitter) espressoSyncAndRefresh(ctx context.Context, newSyncStatus *eth.SyncStatus) { - err := l.EspressoStreamer().Refresh(ctx, newSyncStatus.FinalizedL1, newSyncStatus.SafeL2.Number, newSyncStatus.FinalizedL2.L1Origin) + // Hand the streamer the status we just polled: Refresh reads its positions from the + // provider, and taking them from one status keeps them related to each other. + l.espressoSyncStatus.status = newSyncStatus + + err := l.EspressoStreamer().Refresh(ctx) if err != nil { l.degradedLog.Warn(l.Log, "espressoStreamerRefreshErr", "Failed to refresh Espresso streamer", "err", err) } else { diff --git a/op-batcher/batcher/espresso_driver.go b/op-batcher/batcher/espresso_driver.go index 03ac3a52bb2..f964c6cd258 100644 --- a/op-batcher/batcher/espresso_driver.go +++ b/op-batcher/batcher/espresso_driver.go @@ -2,6 +2,7 @@ package batcher import ( "context" + "errors" "fmt" "math/big" @@ -14,6 +15,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/bigs" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/txmgr" ) @@ -59,6 +61,23 @@ func (l *BatchSubmitter) EspressoStreamer() espressoStreamers.EspressoStreamer[d return l.espressoStreamer } +// cachedSyncStatus is the streamer's SyncStatusProvider, serving the status the batch +// loading loop polled on its current tick rather than a second call to op-node. +// +// Unsynchronised on purpose: the streamer holds no locks of its own, so every call into it +// already has to come from espressoBatchLoadingLoop, and so does every write here. +type cachedSyncStatus struct { + status *eth.SyncStatus +} + +// FetchSyncStatus implements espressoStreamers.SyncStatusProvider. +func (c *cachedSyncStatus) FetchSyncStatus(context.Context) (*eth.SyncStatus, error) { + if c.status == nil { + return nil, errors.New("no sync status polled yet") + } + return c.status, nil +} + // setupEspressoStreamer constructs the Espresso streamer (and its buffered // wrapper) for a freshly-built BatchSubmitter. Called from NewBatchSubmitter // only when --espresso.enabled is set; no-op otherwise. Panics on streamer @@ -74,12 +93,14 @@ func (l *BatchSubmitter) setupEspressoStreamer() { if l.Espresso.LightClient != nil { lightClientIface = l.Espresso.LightClient } + l.espressoSyncStatus = &cachedSyncStatus{} unbufferedStreamer, err := espressoStreamers.NewEspressoStreamer( bigs.Uint64Strict(l.RollupConfig.L2ChainID), l1Adapter, l1Adapter, l.Espresso.Client, lightClientIface, + l.espressoSyncStatus, l.Log, derivation.CreateEspressoBatchUnmarshaler(), l.Config.Espresso.CaffeinationHeightEspresso, @@ -90,7 +111,7 @@ func (l *BatchSubmitter) setupEspressoStreamer() { if err != nil { panic(fmt.Sprintf("failed to create Espresso streamer: %v", err)) } - l.espressoStreamer = espressoStreamers.NewBufferedEspressoStreamer(unbufferedStreamer) + l.espressoStreamer = espressoStreamers.NewBufferedEspressoStreamer(unbufferedStreamer, l.espressoSyncStatus) l.Log.Info("Streamer started", "streamer", l.espressoStreamer) }