diff --git a/op-challenger/cmd/main_test.go b/op-challenger/cmd/main_test.go index 012074846e729..ae0a710064686 100644 --- a/op-challenger/cmd/main_test.go +++ b/op-challenger/cmd/main_test.go @@ -8,6 +8,7 @@ import ( "time" gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum/go-ethereum/superchain" "github.com/stretchr/testify/require" @@ -83,6 +84,17 @@ func TestL1ETHRPCAddress(t *testing.T) { }) } +func TestL1ETHRPCKind(t *testing.T) { + t.Run("Valid", func(t *testing.T) { + const kind = sources.RPCKindAlchemy + cfg := configForArgs(t, addRequiredArgs(gameTypes.AlphabetGameType, "--l1-rpc-kind", kind.String())) + require.Equal(t, kind, cfg.L1RPCKind) + }) + t.Run("Invalid", func(t *testing.T) { + verifyArgsInvalid(t, "invalid value \"bob\" for flag -l1-rpc-kind", addRequiredArgs(gameTypes.AlphabetGameType, "--l1-rpc-kind", "bob")) + }) +} + func TestL1Beacon(t *testing.T) { t.Run("Required", func(t *testing.T) { verifyArgsInvalid(t, "flag l1-beacon is required", addRequiredArgsExcept(gameTypes.AlphabetGameType, "--l1-beacon")) diff --git a/op-challenger/config/config.go b/op-challenger/config/config.go index d1fc28a78cb8e..78c2e0b041bd7 100644 --- a/op-challenger/config/config.go +++ b/op-challenger/config/config.go @@ -12,6 +12,7 @@ import ( gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" "github.com/ethereum-optimism/optimism/op-service/oppprof" + "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum/go-ethereum/common" ) @@ -23,6 +24,7 @@ var ( ErrMissingL2Rpc = errors.New("missing L2 rpc url") ErrMissingCannonAbsolutePreState = errors.New("missing cannon absolute pre-state") ErrMissingL1EthRPC = errors.New("missing l1 eth rpc url") + ErrMissingL1RPCKind = errors.New("missing l1 eth rpc kind") ErrMissingL1Beacon = errors.New("missing l1 beacon url") ErrMissingGameFactoryAddress = errors.New("missing game factory address") ErrMissingCannonSnapshotFreq = errors.New("missing cannon snapshot freq") @@ -55,16 +57,17 @@ const ( // This also contains config options for auxiliary services. // It is used to initialize the challenger. type Config struct { - L1EthRpc string // L1 RPC Url - L1Beacon string // L1 Beacon API Url - GameFactoryAddress common.Address // Address of the dispute game factory - GameAllowlist []common.Address // Allowlist of fault game addresses - GameWindow time.Duration // Maximum time duration to look for games to progress - Datadir string // Data Directory - MaxConcurrency uint // Maximum number of threads to use when progressing games - PollInterval time.Duration // Polling interval for latest-block subscription when using an HTTP RPC provider - AllowInvalidPrestate bool // Whether to allow responding to games where the prestate does not match - MinUpdateInterval time.Duration // Minimum duration the L1 head block time must advance before scheduling a new update cycle + L1EthRpc string // L1 RPC Url + L1RPCKind sources.RPCProviderKind // L1 RPC kind + L1Beacon string // L1 Beacon API Url + GameFactoryAddress common.Address // Address of the dispute game factory + GameAllowlist []common.Address // Allowlist of fault game addresses + GameWindow time.Duration // Maximum time duration to look for games to progress + Datadir string // Data Directory + MaxConcurrency uint // Maximum number of threads to use when progressing games + PollInterval time.Duration // Polling interval for latest-block subscription when using an HTTP RPC provider + AllowInvalidPrestate bool // Whether to allow responding to games where the prestate does not match + MinUpdateInterval time.Duration // Minimum duration the L1 head block time must advance before scheduling a new update cycle AdditionalBondClaimants []common.Address // List of addresses to claim bonds for in addition to the tx manager sender @@ -116,6 +119,7 @@ func NewInteropConfig( ) Config { return Config{ L1EthRpc: l1EthRpc, + L1RPCKind: sources.RPCKindStandard, L1Beacon: l1BeaconApi, SuperRPC: superRpc, L2Rpcs: l2Rpcs, @@ -168,6 +172,7 @@ func NewConfig( ) Config { return Config{ L1EthRpc: l1EthRpc, + L1RPCKind: sources.RPCKindStandard, L1Beacon: l1BeaconApi, RollupRpc: l2RollupRpc, L2Rpcs: []string{l2EthRpc}, @@ -217,6 +222,9 @@ func (c Config) Check() error { if c.L1EthRpc == "" { return ErrMissingL1EthRPC } + if c.L1RPCKind == "" { + return ErrMissingL1RPCKind + } if c.L1Beacon == "" { return ErrMissingL1Beacon } diff --git a/op-challenger/config/config_test.go b/op-challenger/config/config_test.go index 25d8b9c7b45a8..70485d1f1265d 100644 --- a/op-challenger/config/config_test.go +++ b/op-challenger/config/config_test.go @@ -171,6 +171,12 @@ func TestL1EthRpcRequired(t *testing.T) { require.ErrorIs(t, config.Check(), ErrMissingL1EthRPC) } +func TestL1EthRpcKindRequired(t *testing.T) { + config := validConfig(t, gameTypes.CannonGameType) + config.L1RPCKind = "" + require.ErrorIs(t, config.Check(), ErrMissingL1RPCKind) +} + func TestL1BeaconRequired(t *testing.T) { config := validConfig(t, gameTypes.CannonGameType) config.L1Beacon = "" diff --git a/op-challenger/flags/flags.go b/op-challenger/flags/flags.go index 31136e1338ea5..bf758ca9141d1 100644 --- a/op-challenger/flags/flags.go +++ b/op-challenger/flags/flags.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/vm" gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-service/flags" + "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/superchain" @@ -49,6 +50,16 @@ var ( Usage: "HTTP provider URL for L1.", EnvVars: prefixEnvVars("L1_ETH_RPC"), } + L1RPCProviderKind = &cli.GenericFlag{ + Name: "l1-rpc-kind", + Usage: "The kind of RPC provider, used to inform optimal transactions receipts fetching, and thus reduce costs. Valid options: " + + openum.EnumString(sources.RPCProviderKinds), + EnvVars: prefixEnvVars("L1_RPC_KIND"), + Value: func() *sources.RPCProviderKind { + out := sources.RPCKindStandard + return &out + }(), + } L1BeaconFlag = &cli.StringFlag{ Name: "l1-beacon", Usage: "Address of L1 Beacon API endpoint to use", @@ -262,6 +273,7 @@ var requiredFlags = []cli.Flag{ // optionalFlags is a list of unchecked cli flags var optionalFlags = []cli.Flag{ + L1RPCProviderKind, RollupRpcFlag, NetworkFlag, FactoryAddressFlag, @@ -589,6 +601,7 @@ func NewConfigFromCLI(ctx *cli.Context, logger log.Logger) (*config.Config, erro return &config.Config{ // Required Flags L1EthRpc: l1EthRpc, + L1RPCKind: sources.RPCProviderKind(strings.ToLower(ctx.String(L1RPCProviderKind.Name))), L1Beacon: l1Beacon, GameTypes: enabledGameTypes, GameFactoryAddress: gameFactoryAddress, diff --git a/op-challenger/game/client/provider.go b/op-challenger/game/client/provider.go index bc08626592e45..f4ba49adc3776 100644 --- a/op-challenger/game/client/provider.go +++ b/op-challenger/game/client/provider.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum-optimism/optimism/op-challenger/config" "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum-optimism/optimism/op-service/sources/batching" @@ -17,7 +18,7 @@ type Provider struct { ctx context.Context logger log.Logger cfg *config.Config - l1Client *ethclient.Client + l1Client *sources.L1Client caller *batching.MultiCaller l2EL *ethclient.Client @@ -29,13 +30,13 @@ type Provider struct { toClose []func() } -func NewProvider(ctx context.Context, logger log.Logger, cfg *config.Config, l1Client *ethclient.Client) *Provider { +func NewProvider(ctx context.Context, logger log.Logger, cfg *config.Config, l1Client *sources.L1Client, rpcClient client.RPC) *Provider { return &Provider{ ctx: ctx, logger: logger, cfg: cfg, l1Client: l1Client, - caller: batching.NewMultiCaller(l1Client.Client(), batching.DefaultBatchSize), + caller: batching.NewMultiCaller(rpcClient, batching.DefaultBatchSize), } } @@ -45,7 +46,7 @@ func (c *Provider) Close() { } } -func (c *Provider) L1Client() *ethclient.Client { +func (c *Provider) L1Client() *sources.L1Client { return c.l1Client } diff --git a/op-challenger/game/fault/trace/outputs/provider.go b/op-challenger/game/fault/trace/outputs/provider.go index 410b7b15d02d0..2bacad8bcaadb 100644 --- a/op-challenger/game/fault/trace/outputs/provider.go +++ b/op-challenger/game/fault/trace/outputs/provider.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "math/big" + "time" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" @@ -114,7 +115,9 @@ func (o *OutputTraceProvider) GetL2BlockNumberChallenge(ctx context.Context) (*t if err != nil { return nil, err } - header, err := o.l2Client.HeaderByNumber(ctx, new(big.Int).SetUint64(outputBlock)) + tCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + header, err := o.l2Client.HeaderByNumber(tCtx, new(big.Int).SetUint64(outputBlock)) if err != nil { return nil, fmt.Errorf("failed to retrieve L2 block header %v: %w", outputBlock, err) } diff --git a/op-challenger/game/fault/trace/utils/local.go b/op-challenger/game/fault/trace/utils/local.go index 85f64e4021121..86dc3ac4b4646 100644 --- a/op-challenger/game/fault/trace/utils/local.go +++ b/op-challenger/game/fault/trace/utils/local.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/big" + "time" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -50,7 +51,9 @@ func FetchLocalInputs(ctx context.Context, caller GameInputsSource, l2Client L2H } func FetchLocalInputsFromProposals(ctx context.Context, l1Head common.Hash, l2Client L2HeaderSource, agreedOutput Proposal, claimedOutput Proposal) (LocalGameInputs, error) { - agreedHeader, err := l2Client.HeaderByNumber(ctx, agreedOutput.L2BlockNumber) + tCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + agreedHeader, err := l2Client.HeaderByNumber(tCtx, agreedOutput.L2BlockNumber) if err != nil { return LocalGameInputs{}, fmt.Errorf("fetch L2 block header %v: %w", agreedOutput.L2BlockNumber, err) } diff --git a/op-challenger/game/generic/player.go b/op-challenger/game/generic/player.go index 55850dfd0ebae..99643de23c7bb 100644 --- a/op-challenger/game/generic/player.go +++ b/op-challenger/game/generic/player.go @@ -8,7 +8,6 @@ import ( gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" "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/log" ) @@ -27,7 +26,7 @@ type GenericGameLoader interface { } type L1HeaderSource interface { - HeaderByHash(context.Context, common.Hash) (*gethTypes.Header, error) + BlockRefByHash(ctx context.Context, hash common.Hash) (eth.BlockRef, error) } type ActorCreator func(ctx context.Context, logger log.Logger, l1Head eth.BlockID) (Actor, error) @@ -79,11 +78,11 @@ func NewGenericGamePlayer( if err != nil { return nil, fmt.Errorf("failed to load game L1 head: %w", err) } - l1Header, err := l1HeaderSource.HeaderByHash(ctx, l1HeadHash) + l1Header, err := l1HeaderSource.BlockRefByHash(ctx, l1HeadHash) if err != nil { return nil, fmt.Errorf("failed to load L1 header %v: %w", l1HeadHash, err) } - l1Head := eth.HeaderBlockID(l1Header) + l1Head := l1Header.ID() actor, err := createActor(ctx, logger, l1Head) if err != nil { diff --git a/op-challenger/game/keccak/fetcher/fetcher.go b/op-challenger/game/keccak/fetcher/fetcher.go index 589bca09d9f1e..fa1443acd2996 100644 --- a/op-challenger/game/keccak/fetcher/fetcher.go +++ b/op-challenger/game/keccak/fetcher/fetcher.go @@ -8,6 +8,8 @@ import ( "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types" + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -19,9 +21,8 @@ var ( ) type L1Source interface { - BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) - TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) - ChainID(ctx context.Context) (*big.Int, error) + BlockRefByNumber(ctx context.Context, num uint64) (eth.BlockRef, error) + apis.ReceiptsFetcher } type Oracle interface { @@ -43,12 +44,16 @@ func (f *InputFetcher) FetchInputs(ctx context.Context, blockHash common.Hash, o var inputs []keccakTypes.InputData for _, blockNum := range blockNums { foundRelevantTx := false - block, err := f.source.BlockByNumber(ctx, new(big.Int).SetUint64(blockNum)) + blockRef, err := f.source.BlockRefByNumber(ctx, blockNum) if err != nil { - return nil, fmt.Errorf("failed getting tx for block %v: %w", blockNum, err) + return nil, fmt.Errorf("failed getting info for block %v: %w", blockNum, err) } - for _, tx := range block.Transactions() { - inputData, err := f.extractRelevantLeavesFromTx(ctx, oracle, tx, ident) + _, receipts, err := f.source.FetchReceipts(ctx, blockRef.Hash) + if err != nil { + return nil, fmt.Errorf("failed to retrieve receipts for block %v: %w", blockNum, err) + } + for _, rcpt := range receipts { + inputData, err := f.extractRelevantLeavesFromReceipt(rcpt, oracle, ident) if err != nil { return nil, err } @@ -67,13 +72,9 @@ func (f *InputFetcher) FetchInputs(ctx context.Context, blockHash common.Hash, o return inputs, nil } -func (f *InputFetcher) extractRelevantLeavesFromTx(ctx context.Context, oracle Oracle, tx *types.Transaction, ident keccakTypes.LargePreimageIdent) ([]keccakTypes.InputData, error) { - rcpt, err := f.source.TransactionReceipt(ctx, tx.Hash()) - if err != nil { - return nil, fmt.Errorf("failed to retrieve receipt for tx %v: %w", tx.Hash(), err) - } +func (f *InputFetcher) extractRelevantLeavesFromReceipt(rcpt *types.Receipt, oracle Oracle, ident keccakTypes.LargePreimageIdent) ([]keccakTypes.InputData, error) { if rcpt.Status != types.ReceiptStatusSuccessful { - f.log.Trace("Skipping transaction with failed receipt status", "tx", tx.Hash(), "status", rcpt.Status) + f.log.Trace("Skipping transaction with failed receipt status", "tx", rcpt.TxHash, "status", rcpt.Status) return nil, nil } @@ -81,29 +82,29 @@ func (f *InputFetcher) extractRelevantLeavesFromTx(ctx context.Context, oracle O var inputs []keccakTypes.InputData for i, txLog := range rcpt.Logs { if txLog.Address != oracle.Addr() { - f.log.Trace("Skip tx log not emitted by the oracle contract", "tx", tx.Hash(), "logIndex", i, "targetContract", oracle.Addr(), "actualContract", txLog.Address) + f.log.Trace("Skip tx log not emitted by the oracle contract", "tx", rcpt.TxHash, "logIndex", i, "targetContract", oracle.Addr(), "actualContract", txLog.Address) continue } if len(txLog.Data) < 20 { - f.log.Trace("Skip tx log with insufficient data (less than 20 bytes)", "tx", tx.Hash(), "logIndex", i, "dataLength", len(txLog.Data)) + f.log.Trace("Skip tx log with insufficient data (less than 20 bytes)", "tx", rcpt.TxHash, "logIndex", i, "dataLength", len(txLog.Data)) continue } caller := common.Address(txLog.Data[0:20]) callData := txLog.Data[20:] if caller != ident.Claimant { - f.log.Trace("Skip tx log from irrelevant claimant", "tx", tx.Hash(), "logIndex", i, "targetClaimant", ident.Claimant, "actualClaimant", caller) + f.log.Trace("Skip tx log from irrelevant claimant", "tx", rcpt.TxHash, "logIndex", i, "targetClaimant", ident.Claimant, "actualClaimant", caller) continue } uuid, inputData, err := oracle.DecodeInputData(callData) if errors.Is(err, contracts.ErrInvalidAddLeavesCall) { - f.log.Trace("Skip tx log with call data not targeting expected method", "tx", tx.Hash(), "logIndex", i, "err", err) + f.log.Trace("Skip tx log with call data not targeting expected method", "tx", rcpt.TxHash, "logIndex", i, "err", err) continue } else if err != nil { return nil, err } if uuid.Cmp(ident.UUID) != 0 { - f.log.Trace("Skip tx log with irrelevant UUID", "tx", tx.Hash(), "logIndex", i, "targetUUID", ident.UUID, "actualUUID", uuid) + f.log.Trace("Skip tx log with irrelevant UUID", "tx", rcpt.TxHash, "logIndex", i, "targetUUID", ident.UUID, "actualUUID", uuid) continue } inputs = append(inputs, inputData) diff --git a/op-challenger/game/keccak/fetcher/fetcher_test.go b/op-challenger/game/keccak/fetcher/fetcher_test.go index 2a976b3e6df02..4083935a305bc 100644 --- a/op-challenger/game/keccak/fetcher/fetcher_test.go +++ b/op-challenger/game/keccak/fetcher/fetcher_test.go @@ -2,15 +2,15 @@ package fetcher import ( "context" - "crypto/ecdsa" + "encoding/binary" "errors" "fmt" - "math" "math/big" "testing" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum/go-ethereum/common" @@ -20,21 +20,14 @@ import ( "github.com/stretchr/testify/require" ) -const ( - // Signal to indicate a receipt should be considered missing - MissingReceiptStatus = math.MaxUint64 -) - var ( oracleAddr = common.Address{0x99, 0x98} otherAddr = common.Address{0x12, 0x34} claimantKey, _ = crypto.GenerateKey() - otherKey, _ = crypto.GenerateKey() ident = keccakTypes.LargePreimageIdent{ Claimant: crypto.PubkeyToAddress(claimantKey.PublicKey), UUID: big.NewInt(888), } - chainID = big.NewInt(123) blockHash = common.Hash{0xdd} input1 = keccakTypes.InputData{ Input: []byte{0xbb, 0x11}, @@ -80,36 +73,21 @@ func TestFetchLeaves_ErrorOnUnavailableL1Block(t *testing.T) { // No txs means stubL1Source will return an error when we try to fetch the block leaves, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) - require.ErrorContains(t, err, fmt.Sprintf("failed getting tx for block %v", blockNum)) + require.ErrorContains(t, err, fmt.Sprintf("failed getting info for block %v", blockNum)) require.Empty(t, leaves) } func TestFetchLeaves_SingleTxSingleLog(t *testing.T) { - cases := []struct { - name string - txSender *ecdsa.PrivateKey - txModifier TxModifier - }{ - {"from EOA claimant address", claimantKey, ValidTx}, - {"from contract call", otherKey, WithToAddr(otherAddr)}, - {"from contract creation", otherKey, WithoutToAddr()}, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - fetcher, oracle, l1Source := setupFetcherTest(t) - blockNum := uint64(7) - oracle.leafBlocks = []uint64{blockNum} + fetcher, oracle, l1Source := setupFetcherTest(t) + blockNum := uint64(7) + oracle.leafBlocks = []uint64{blockNum} - proposal := oracle.createProposal(input1) - tx := l1Source.createTx(blockNum, tc.txSender, tc.txModifier) - l1Source.createLog(tx, proposal) + proposal := oracle.createProposal(input1) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal) - inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) - require.NoError(t, err) - require.Equal(t, []keccakTypes.InputData{input1}, inputs) - }) - } + inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) + require.NoError(t, err) + require.Equal(t, []keccakTypes.InputData{input1}, inputs) } func TestFetchLeaves_SingleTxMultipleLogs(t *testing.T) { @@ -119,9 +97,7 @@ func TestFetchLeaves_SingleTxMultipleLogs(t *testing.T) { proposal1 := oracle.createProposal(input1) proposal2 := oracle.createProposal(input2) - tx := l1Source.createTx(blockNum, otherKey, WithToAddr(otherAddr)) - l1Source.createLog(tx, proposal1) - l1Source.createLog(tx, proposal2) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal1, proposal2) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) require.NoError(t, err) @@ -138,14 +114,10 @@ func TestFetchLeaves_MultipleBlocksAndLeaves(t *testing.T) { proposal2 := oracle.createProposal(input2) proposal3 := oracle.createProposal(input3) proposal4 := oracle.createProposal(input4) - block1Tx := l1Source.createTx(block1, claimantKey, ValidTx) - block2TxA := l1Source.createTx(block2, claimantKey, ValidTx) - l1Source.createTx(block2, claimantKey, ValidTx) // Add tx with no logs - block2TxB := l1Source.createTx(block2, otherKey, WithoutToAddr()) - l1Source.createLog(block1Tx, proposal1) - l1Source.createLog(block2TxA, proposal2) - l1Source.createLog(block2TxB, proposal3) - l1Source.createLog(block2TxB, proposal4) + l1Source.createReceipt(block1, types.ReceiptStatusSuccessful, proposal1) + l1Source.createReceipt(block1, types.ReceiptStatusSuccessful, proposal2) + l1Source.createReceipt(block2, types.ReceiptStatusSuccessful) // Add tx with no logs + l1Source.createReceipt(block2, types.ReceiptStatusSuccessful, proposal3, proposal4) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) require.NoError(t, err) @@ -159,13 +131,12 @@ func TestFetchLeaves_SkipLogFromWrongContract(t *testing.T) { // Emit log from an irrelevant contract address proposal1 := oracle.createProposal(input2) - tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx) - log1 := l1Source.createLog(tx1, proposal1) - log1.Address = otherAddr + rcpt := l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal1) + rcpt.Logs[0].Address = otherAddr + // Valid tx proposal2 := oracle.createProposal(input1) - tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx2, proposal2) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal2) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) require.NoError(t, err) @@ -180,12 +151,11 @@ func TestFetchLeaves_SkipProposalWithWrongUUID(t *testing.T) { // Valid tx but with a different UUID proposal1 := oracle.createProposal(input2) proposal1.uuid = big.NewInt(874927294) - tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx1, proposal1) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal1) + // Valid tx proposal2 := oracle.createProposal(input1) - tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx2, proposal2) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal2) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) require.NoError(t, err) @@ -200,12 +170,10 @@ func TestFetchLeaves_SkipProposalWithWrongClaimant(t *testing.T) { // Valid tx but with a different claimant proposal1 := oracle.createProposal(input2) proposal1.claimantAddr = otherAddr - tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx1, proposal1) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal1) // Valid tx proposal2 := oracle.createProposal(input1) - tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx2, proposal2) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal2) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) require.NoError(t, err) @@ -220,12 +188,10 @@ func TestFetchLeaves_SkipInvalidProposal(t *testing.T) { // Set up proposal decoding to fail proposal1 := oracle.createProposal(input2) proposal1.valid = false - tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx1, proposal1) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal1) // Valid tx proposal2 := oracle.createProposal(input1) - tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx2, proposal2) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal2) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) require.NoError(t, err) @@ -240,13 +206,11 @@ func TestFetchLeaves_SkipProposalWithInsufficientData(t *testing.T) { // Log contains insufficient data // It should hold a 20 byte address followed by the proposal payload proposal1 := oracle.createProposal(input2) - tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx) - log1 := l1Source.createLog(tx1, proposal1) - log1.Data = proposal1.claimantAddr[:19] + rcpt1 := l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal1) + rcpt1.Logs[0].Data = proposal1.claimantAddr[:19] // Valid tx proposal2 := oracle.createProposal(input1) - tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx2, proposal2) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal2) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) require.NoError(t, err) @@ -260,13 +224,11 @@ func TestFetchLeaves_SkipProposalMissingCallData(t *testing.T) { // Truncate call data from log so that is only contains an address proposal1 := oracle.createProposal(input2) - tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx) - log1 := l1Source.createLog(tx1, proposal1) - log1.Data = log1.Data[0:20] + rcpt1 := l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal1) + rcpt1.Logs[0].Data = rcpt1.Logs[0].Data[0:20] // Valid tx proposal2 := oracle.createProposal(input1) - tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx2, proposal2) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal2) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) require.NoError(t, err) @@ -280,36 +242,26 @@ func TestFetchLeaves_SkipTxWithReceiptStatusFail(t *testing.T) { // Valid proposal, but tx reverted proposal1 := oracle.createProposal(input2) - tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx1, proposal1) - l1Source.rcptStatus[tx1.Hash()] = types.ReceiptStatusFailed + l1Source.createReceipt(blockNum, types.ReceiptStatusFailed, proposal1) // Valid tx proposal2 := oracle.createProposal(input1) - tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx2, proposal2) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful, proposal2) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) require.NoError(t, err) require.Equal(t, []keccakTypes.InputData{input1}, inputs) } -func TestFetchLeaves_ErrorsOnMissingReceipt(t *testing.T) { +func TestFetchLeaves_ErrorsOnMissingReceipts(t *testing.T) { fetcher, oracle, l1Source := setupFetcherTest(t) blockNum := uint64(7) oracle.leafBlocks = []uint64{blockNum} - // Valid tx - proposal1 := oracle.createProposal(input1) - tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx1, proposal1) - // Valid proposal, but tx receipt is missing - proposal2 := oracle.createProposal(input2) - tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx2, proposal2) - l1Source.rcptStatus[tx2.Hash()] = MissingReceiptStatus + // Block exists but receipts return not found + l1Source.blocks[blockNum] = uint64ToHash(blockNum) input, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) - require.ErrorContains(t, err, fmt.Sprintf("failed to retrieve receipt for tx %v", tx2.Hash())) + require.ErrorContains(t, err, fmt.Sprintf("failed to retrieve receipts for block %v", blockNum)) require.Nil(t, input) } @@ -320,11 +272,9 @@ func TestFetchLeaves_ErrorsWhenNoValidLeavesInBlock(t *testing.T) { // Irrelevant tx - reverted proposal1 := oracle.createProposal(input2) - tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx) - l1Source.createLog(tx1, proposal1) - l1Source.rcptStatus[tx1.Hash()] = types.ReceiptStatusFailed + l1Source.createReceipt(blockNum, types.ReceiptStatusFailed, proposal1) // Irrelevant tx - no logs are emitted - l1Source.createTx(blockNum, claimantKey, ValidTx) + l1Source.createReceipt(blockNum, types.ReceiptStatusSuccessful) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) require.ErrorIs(t, err, ErrNoLeavesFound) @@ -336,6 +286,8 @@ func setupFetcherTest(t *testing.T) (*InputFetcher, *stubOracle, *stubL1Source) proposals: make(map[byte]*proposalConfig), } l1Source := &stubL1Source{ + blocks: make(map[uint64]common.Hash), + rcpts: make(map[common.Hash]types.Receipts), txs: make(map[uint64]types.Transactions), rcptStatus: make(map[common.Hash]uint64), logs: make(map[common.Hash][]*types.Log), @@ -384,24 +336,6 @@ func (o *stubOracle) DecodeInputData(data []byte) (*big.Int, keccakTypes.InputDa return proposal.uuid, proposal.inputData, nil } -type TxModifier func(tx *types.DynamicFeeTx) - -var ValidTx TxModifier = func(_ *types.DynamicFeeTx) { - // no-op -} - -func WithToAddr(addr common.Address) TxModifier { - return func(tx *types.DynamicFeeTx) { - tx.To = &addr - } -} - -func WithoutToAddr() TxModifier { - return func(tx *types.DynamicFeeTx) { - tx.To = nil - } -} - func (o *stubOracle) createProposal(input keccakTypes.InputData) *proposalConfig { id := o.nextProposalId o.nextProposalId++ @@ -420,6 +354,11 @@ func (o *stubOracle) createProposal(input keccakTypes.InputData) *proposalConfig type stubL1Source struct { nextTxId uint64 + + // Map block number to block hash + blocks map[uint64]common.Hash + // Map block hash to receipts + rcpts map[common.Hash]types.Receipts // Map block number to tx txs map[uint64]types.Transactions // Map txHash to receipt @@ -428,78 +367,62 @@ type stubL1Source struct { logs map[common.Hash][]*types.Log } -func (s *stubL1Source) ChainID(_ context.Context) (*big.Int, error) { - return chainID, nil -} - -func (s *stubL1Source) BlockByNumber(_ context.Context, number *big.Int) (*types.Block, error) { - txs, ok := s.txs[number.Uint64()] +func (s *stubL1Source) BlockRefByNumber(_ context.Context, num uint64) (eth.BlockRef, error) { + hash, ok := s.blocks[num] if !ok { - return nil, errors.New("not found") + return eth.BlockRef{}, errors.New("not found") } - return (&types.Block{}).WithBody(types.Body{Transactions: txs}), nil + return eth.BlockRef{ + Number: num, + Hash: hash, + }, nil } -func (s *stubL1Source) TransactionReceipt(_ context.Context, txHash common.Hash) (*types.Receipt, error) { - rcptStatus, ok := s.rcptStatus[txHash] +func (s *stubL1Source) FetchReceipts(_ context.Context, blockHash common.Hash) (eth.BlockInfo, types.Receipts, error) { + rcpts, ok := s.rcpts[blockHash] if !ok { - rcptStatus = types.ReceiptStatusSuccessful - } else if rcptStatus == MissingReceiptStatus { - return nil, errors.New("not found") + return nil, nil, errors.New("not found") } + return nil, rcpts, nil +} - logs := s.logs[txHash] - return &types.Receipt{Status: rcptStatus, Logs: logs}, nil +func uint64ToHash(num uint64) common.Hash { + data := make([]byte, 8) + binary.BigEndian.PutUint64(data, num) + return crypto.Keccak256Hash(data) } -func (s *stubL1Source) createTx(blockNum uint64, key *ecdsa.PrivateKey, txMod TxModifier) *types.Transaction { +func (s *stubL1Source) createReceipt(blockNum uint64, status uint64, proposals ...*proposalConfig) *types.Receipt { + // Make the block exist + s.blocks[blockNum] = uint64ToHash(blockNum) + txId := s.nextTxId s.nextTxId++ - inner := &types.DynamicFeeTx{ - ChainID: chainID, - Nonce: txId, - To: &oracleAddr, - Value: big.NewInt(0), - GasTipCap: big.NewInt(1), - GasFeeCap: big.NewInt(2), - Gas: 3, - Data: []byte{}, + logs := make([]*types.Log, len(proposals)) + for i, proposal := range proposals { + // Concat the claimant address and the proposal id + // These will be split back into address and id in fetcher.extractRelevantLeavesFromTx + data := append(proposal.claimantAddr[:], proposal.id) + + txLog := &types.Log{ + Address: oracleAddr, + Data: data, + Topics: []common.Hash{}, + + // ignored (zeroed): + BlockNumber: 0, + TxHash: common.Hash{}, + TxIndex: 0, + BlockHash: common.Hash{}, + Index: 0, + Removed: false, + } + logs[i] = txLog } - txMod(inner) - tx := types.MustSignNewTx(key, types.LatestSignerForChainID(inner.ChainID), inner) - - // Track tx internally - txSet := s.txs[blockNum] - txSet = append(txSet, tx) - s.txs[blockNum] = txSet - - return tx -} - -func (s *stubL1Source) createLog(tx *types.Transaction, proposal *proposalConfig) *types.Log { - // Concat the claimant address and the proposal id - // These will be split back into address and id in fetcher.extractRelevantLeavesFromTx - data := append(proposal.claimantAddr[:], proposal.id) - - txLog := &types.Log{ - Address: oracleAddr, - Data: data, - Topics: []common.Hash{}, - - // ignored (zeroed): - BlockNumber: 0, - TxHash: common.Hash{}, - TxIndex: 0, - BlockHash: common.Hash{}, - Index: 0, - Removed: false, - } - - // Track tx log - logSet := s.logs[tx.Hash()] - logSet = append(logSet, txLog) - s.logs[tx.Hash()] = logSet - - return txLog + rcpt := &types.Receipt{TxHash: uint64ToHash(txId), Status: status, Logs: logs} + blockHash := s.blocks[blockNum] + rcpts := s.rcpts[blockHash] + s.rcpts[blockHash] = append(rcpts, rcpt) + return rcpt } diff --git a/op-challenger/game/service.go b/op-challenger/game/service.go index 8188667f5ed47..32635d087b9d0 100644 --- a/op-challenger/game/service.go +++ b/op-challenger/game/service.go @@ -6,12 +6,14 @@ import ( "fmt" "io" "sync/atomic" + "time" challengerClient "github.com/ethereum-optimism/optimism/op-challenger/game/client" "github.com/ethereum-optimism/optimism/op-challenger/game/keccak" "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher" "github.com/ethereum-optimism/optimism/op-challenger/game/zk" "github.com/ethereum-optimism/optimism/op-challenger/sender" + "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" @@ -57,8 +59,9 @@ type Service struct { registry *registry.GameTypeRegistry oracles *registry.OracleRegistry - l1Client *ethclient.Client - pollClient client.RPC + l1RPC client.RPC + l1Client *sources.L1Client + l1EthClient *ethclient.Client pprofService *oppprof.Service metricsSrv *httputil.HTTPServer @@ -90,12 +93,9 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error return fmt.Errorf("failed to init tx manager: %w", err) } s.initClaimants(cfg) - if err := s.initL1Client(ctx, cfg); err != nil { + if err := s.initL1Clients(ctx, cfg); err != nil { return fmt.Errorf("failed to init l1 client: %w", err) } - if err := s.initPollClient(ctx, cfg); err != nil { - return fmt.Errorf("failed to init poll client: %w", err) - } if err := s.initPProf(&cfg.PprofConfig); err != nil { return fmt.Errorf("failed to init profiling: %w", err) } @@ -140,21 +140,28 @@ func (s *Service) initTxManager(ctx context.Context, cfg *config.Config) error { return nil } -func (s *Service) initL1Client(ctx context.Context, cfg *config.Config) error { - l1Client, err := dial.DialEthClientWithTimeout(ctx, dial.DefaultDialTimeout, s.logger, cfg.L1EthRpc) +func (s *Service) initL1Clients(ctx context.Context, cfg *config.Config) error { + l1EthClient, err := dial.DialEthClientWithTimeout(ctx, dial.DefaultDialTimeout, s.logger, cfg.L1EthRpc) if err != nil { return fmt.Errorf("failed to dial L1: %w", err) } - s.l1Client = l1Client - return nil -} -func (s *Service) initPollClient(ctx context.Context, cfg *config.Config) error { - pollClient, err := client.NewRPCWithClient(ctx, s.logger, cfg.L1EthRpc, client.NewBaseRPCClient(s.l1Client.Client()), cfg.PollInterval) + l1RPC := client.NewBaseRPCClient(l1EthClient.Client(), client.WithCallTimeout(30*time.Second), client.WithBatchCallTimeout(60*time.Second)) + pollClient, err := client.NewRPCWithClient(ctx, s.logger, cfg.L1EthRpc, l1RPC, cfg.PollInterval) if err != nil { return fmt.Errorf("failed to create RPC client: %w", err) } - s.pollClient = pollClient + s.l1RPC = pollClient + + l1Client, err := sources.NewL1Client(s.l1RPC, s.logger, s.metrics, sources.L1ClientSimpleConfig(true, cfg.L1RPCKind, 100)) + if err != nil { + return fmt.Errorf("failed to dial L1: %w", err) + } + + s.l1Client = l1Client + s.l1RPC = l1Client.RPC() + + s.l1EthClient = l1EthClient return nil } @@ -190,13 +197,13 @@ func (s *Service) initMetricsServer(cfg *opmetrics.CLIConfig) error { } s.logger.Info("started metrics server", "addr", metricsSrv.Addr()) s.metricsSrv = metricsSrv - s.balanceMetricer = s.metrics.StartBalanceMetrics(s.logger, s.l1Client, s.txSender.From()) + s.balanceMetricer = s.metrics.StartBalanceMetrics(s.logger, s.l1EthClient, s.txSender.From()) return nil } func (s *Service) initFactoryContract(ctx context.Context, cfg *config.Config) error { factoryContract, err := contracts.NewDisputeGameFactoryContract(ctx, s.metrics, cfg.GameFactoryAddress, - batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize)) + batching.NewMultiCaller(s.l1RPC, batching.DefaultBatchSize)) if err != nil { return fmt.Errorf("failed to create factory contract: %w", err) } @@ -213,7 +220,7 @@ func (s *Service) initBondClaims() error { func (s *Service) registerGameTypes(ctx context.Context, cfg *config.Config) error { gameTypeRegistry := registry.NewGameTypeRegistry() oracles := registry.NewOracleRegistry() - s.clientProvider = challengerClient.NewProvider(ctx, s.logger, cfg, s.l1Client) + s.clientProvider = challengerClient.NewProvider(ctx, s.logger, cfg, s.l1Client, s.l1RPC) err := fault.RegisterGameTypes(ctx, s.systemClock, s.l1Clock, s.logger, s.metrics, cfg, gameTypeRegistry, oracles, s.txSender, s.factoryContract, s.clientProvider, cfg.SelectiveClaimResolution, s.claimants) if err != nil { return err @@ -242,7 +249,7 @@ func (s *Service) initLargePreimages() error { } func (s *Service) initMonitor(cfg *config.Config) { - s.monitor = newGameMonitor(s.logger, s.l1Clock, s.factoryContract, s.sched, s.preimages, cfg.GameWindow, s.claimer, cfg.GameAllowlist, s.pollClient, cfg.MinUpdateInterval) + s.monitor = newGameMonitor(s.logger, s.l1Clock, s.factoryContract, s.sched, s.preimages, cfg.GameWindow, s.claimer, cfg.GameAllowlist, s.l1RPC, cfg.MinUpdateInterval) } func (s *Service) Start(ctx context.Context) error { @@ -295,8 +302,8 @@ func (s *Service) Stop(ctx context.Context) error { s.txMgr.Close() } - if s.pollClient != nil { - s.pollClient.Close() + if s.l1RPC != nil { + s.l1RPC.Close() } if s.l1Client != nil { s.l1Client.Close()