diff --git a/op-e2e/system_test.go b/op-e2e/system_test.go index 17736d8850d55..1210e2cd61c0d 100644 --- a/op-e2e/system_test.go +++ b/op-e2e/system_test.go @@ -12,7 +12,7 @@ import ( "github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/predeploys" - "github.com/ethereum-optimism/optimism/op-node/l2" + "github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/node" rollupNode "github.com/ethereum-optimism/optimism/op-node/node" "github.com/ethereum-optimism/optimism/op-node/rollup" @@ -20,7 +20,6 @@ import ( "github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/withdrawals" "github.com/ethereum-optimism/optimism/op-proposer/rollupclient" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -265,7 +264,7 @@ func TestSystemE2E(t *testing.T) { receipt, err := waitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.L1BlockTime)*time.Second) require.Nil(t, err, "Waiting for deposit tx on L1") - reconstructedDep, err := derive.UnmarshalLogEvent(receipt.Logs[0]) + reconstructedDep, err := derive.UnmarshalDepositLogEvent(receipt.Logs[0]) require.NoError(t, err, "Could not reconstruct L2 Deposit") tx = types.NewTx(reconstructedDep) receipt, err = waitForTransaction(tx.Hash(), l2Verif, 3*time.Duration(cfg.L1BlockTime)*time.Second) @@ -355,7 +354,7 @@ func TestMintOnRevertedDeposit(t *testing.T) { receipt, err := waitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.L1BlockTime)*time.Second) require.Nil(t, err, "Waiting for deposit tx on L1") - reconstructedDep, err := derive.UnmarshalLogEvent(receipt.Logs[0]) + reconstructedDep, err := derive.UnmarshalDepositLogEvent(receipt.Logs[0]) require.NoError(t, err, "Could not reconstruct L2 Deposit") tx = types.NewTx(reconstructedDep) receipt, err = waitForTransaction(tx.Hash(), l2Verif, 3*time.Duration(cfg.L1BlockTime)*time.Second) @@ -501,10 +500,10 @@ func TestSystemMockP2P(t *testing.T) { var published, received []common.Hash seqTracer, verifTracer := new(FnTracer), new(FnTracer) - seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *l2.ExecutionPayload) { + seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) { published = append(published, payload.BlockHash) } - verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *l2.ExecutionPayload) { + verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) { received = append(received, payload.BlockHash) } cfg.Nodes["sequencer"].Tracer = seqTracer @@ -715,7 +714,7 @@ func TestWithdrawals(t *testing.T) { require.Nil(t, err, "binding withdrawer on L2") // Wait for deposit to arrive - reconstructedDep, err := derive.UnmarshalLogEvent(receipt.Logs[0]) + reconstructedDep, err := derive.UnmarshalDepositLogEvent(receipt.Logs[0]) require.NoError(t, err, "Could not reconstruct L2 Deposit") tx = types.NewTx(reconstructedDep) receipt, err = waitForTransaction(tx.Hash(), l2Verif, 3*time.Duration(cfg.L1BlockTime)*time.Second) diff --git a/op-e2e/tracer.go b/op-e2e/tracer.go index 045e558486e72..ab95675886854 100644 --- a/op-e2e/tracer.go +++ b/op-e2e/tracer.go @@ -4,15 +4,14 @@ import ( "context" "github.com/ethereum-optimism/optimism/op-node/eth" - "github.com/ethereum-optimism/optimism/op-node/l2" "github.com/ethereum-optimism/optimism/op-node/node" "github.com/libp2p/go-libp2p-core/peer" ) type FnTracer struct { OnNewL1HeadFn func(ctx context.Context, sig eth.L1BlockRef) - OnUnsafeL2PayloadFn func(ctx context.Context, from peer.ID, payload *l2.ExecutionPayload) - OnPublishL2PayloadFn func(ctx context.Context, payload *l2.ExecutionPayload) + OnUnsafeL2PayloadFn func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) + OnPublishL2PayloadFn func(ctx context.Context, payload *eth.ExecutionPayload) } func (n *FnTracer) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) { @@ -21,13 +20,13 @@ func (n *FnTracer) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) { } } -func (n *FnTracer) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *l2.ExecutionPayload) { +func (n *FnTracer) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) { if n.OnUnsafeL2PayloadFn != nil { n.OnUnsafeL2PayloadFn(ctx, from, payload) } } -func (n *FnTracer) OnPublishL2Payload(ctx context.Context, payload *l2.ExecutionPayload) { +func (n *FnTracer) OnPublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) { if n.OnPublishL2PayloadFn != nil { n.OnPublishL2PayloadFn(ctx, payload) } diff --git a/op-node/l2/ssz.go b/op-node/eth/ssz.go similarity index 99% rename from op-node/l2/ssz.go rename to op-node/eth/ssz.go index 38a8e54bcea01..7d6eca93a8344 100644 --- a/op-node/l2/ssz.go +++ b/op-node/eth/ssz.go @@ -1,4 +1,4 @@ -package l2 +package eth import ( "encoding/binary" diff --git a/op-node/l2/ssz_test.go b/op-node/eth/ssz_test.go similarity index 99% rename from op-node/l2/ssz_test.go rename to op-node/eth/ssz_test.go index 94949fd5342a9..3682e4e8eb336 100644 --- a/op-node/l2/ssz_test.go +++ b/op-node/eth/ssz_test.go @@ -1,4 +1,4 @@ -package l2 +package eth import ( "bytes" diff --git a/op-node/l2/api.go b/op-node/eth/types.go similarity index 95% rename from op-node/l2/api.go rename to op-node/eth/types.go index 3f0911b0bdaed..0d8704036c80e 100644 --- a/op-node/l2/api.go +++ b/op-node/eth/types.go @@ -1,5 +1,4 @@ -// Package l2 connects to the L2 execution engine over the Engine API. -package l2 +package eth import ( "bytes" @@ -9,8 +8,6 @@ import ( "github.com/ethereum/go-ethereum/trie" - "github.com/ethereum-optimism/optimism/op-node/eth" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/beacon" @@ -111,16 +108,16 @@ type ExecutionPayload struct { Transactions []Data `json:"transactions"` } -func (payload *ExecutionPayload) ID() eth.BlockID { - return eth.BlockID{Hash: payload.BlockHash, Number: uint64(payload.BlockNumber)} +func (payload *ExecutionPayload) ID() BlockID { + return BlockID{Hash: payload.BlockHash, Number: uint64(payload.BlockNumber)} } -func (payload *ExecutionPayload) ParentID() eth.BlockID { +func (payload *ExecutionPayload) ParentID() BlockID { n := uint64(payload.BlockNumber) if n > 0 { n -= 1 } - return eth.BlockID{Hash: payload.ParentHash, Number: n} + return BlockID{Hash: payload.ParentHash, Number: n} } type rawTransactions []Data diff --git a/op-node/l2/source.go b/op-node/l2/source.go index 2fb9dc2a95ab3..e4ed4102428f0 100644 --- a/op-node/l2/source.go +++ b/op-node/l2/source.go @@ -38,26 +38,26 @@ func (s *Source) Close() { s.rpc.Close() } -func (s *Source) PayloadByHash(ctx context.Context, hash common.Hash) (*ExecutionPayload, error) { +func (s *Source) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) { // TODO: we really do not need to parse every single tx and block detail, keeping transactions encoded is faster. block, err := s.client.BlockByHash(ctx, hash) if err != nil { return nil, fmt.Errorf("failed to retrieve L2 block by hash: %v", err) } - payload, err := BlockAsPayload(block) + payload, err := eth.BlockAsPayload(block) if err != nil { return nil, fmt.Errorf("failed to read L2 block as payload: %w", err) } return payload, nil } -func (s *Source) PayloadByNumber(ctx context.Context, number *big.Int) (*ExecutionPayload, error) { +func (s *Source) PayloadByNumber(ctx context.Context, number *big.Int) (*eth.ExecutionPayload, error) { // TODO: we really do not need to parse every single tx and block detail, keeping transactions encoded is faster. block, err := s.client.BlockByNumber(ctx, number) if err != nil { return nil, fmt.Errorf("failed to retrieve L2 block by number: %v", err) } - payload, err := BlockAsPayload(block) + payload, err := eth.BlockAsPayload(block) if err != nil { return nil, fmt.Errorf("failed to read L2 block as payload: %w", err) } @@ -67,12 +67,12 @@ func (s *Source) PayloadByNumber(ctx context.Context, number *big.Int) (*Executi // ForkchoiceUpdate updates the forkchoice on the execution client. If attributes is not nil, the engine client will also begin building a block // based on attributes after the new head block and return the payload ID. // May return an error in ForkChoiceResult, but the error is marshalled into the error return -func (s *Source) ForkchoiceUpdate(ctx context.Context, fc *ForkchoiceState, attributes *PayloadAttributes) (*ForkchoiceUpdatedResult, error) { +func (s *Source) ForkchoiceUpdate(ctx context.Context, fc *eth.ForkchoiceState, attributes *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) { e := s.log.New("state", fc, "attr", attributes) e.Debug("Sharing forkchoice-updated signal") fcCtx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - var result ForkchoiceUpdatedResult + var result eth.ForkchoiceUpdatedResult err := s.rpc.CallContext(fcCtx, &result, "engine_forkchoiceUpdatedV1", fc, attributes) if err == nil { e.Debug("Shared forkchoice-updated signal") @@ -82,21 +82,21 @@ func (s *Source) ForkchoiceUpdate(ctx context.Context, fc *ForkchoiceState, attr } else { e = e.New("err", err) if rpcErr, ok := err.(rpc.Error); ok { - code := ErrorCode(rpcErr.ErrorCode()) + code := eth.ErrorCode(rpcErr.ErrorCode()) e.Warn("Unexpected error code in forkchoice-updated response", "code", code) } else { e.Error("Failed to share forkchoice-updated signal") } } switch result.PayloadStatus.Status { - case ExecutionSyncing: + case eth.ExecutionSyncing: return nil, fmt.Errorf("updated forkchoice, but node is syncing: %v", err) - case ExecutionAccepted, ExecutionInvalidTerminalBlock, ExecutionInvalidBlockHash: + case eth.ExecutionAccepted, eth.ExecutionInvalidTerminalBlock, eth.ExecutionInvalidBlockHash: // ACCEPTED, INVALID_TERMINAL_BLOCK, INVALID_BLOCK_HASH are only for execution return nil, fmt.Errorf("unexpected %s status, could not update forkchoice: %v", result.PayloadStatus.Status, err) - case ExecutionInvalid: + case eth.ExecutionInvalid: return nil, fmt.Errorf("cannot update forkchoice, block is invalid: %v", err) - case ExecutionValid: + case eth.ExecutionValid: return &result, nil default: return nil, fmt.Errorf("unknown forkchoice status on %s: %q, ", fc.SafeBlockHash, string(result.PayloadStatus.Status)) @@ -104,13 +104,13 @@ func (s *Source) ForkchoiceUpdate(ctx context.Context, fc *ForkchoiceState, attr } // ExecutePayload executes a built block on the execution engine and returns an error if it was not successful. -func (s *Source) NewPayload(ctx context.Context, payload *ExecutionPayload) error { +func (s *Source) NewPayload(ctx context.Context, payload *eth.ExecutionPayload) error { e := s.log.New("block_hash", payload.BlockHash) e.Debug("sending payload for execution") execCtx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - var result PayloadStatusV1 + var result eth.PayloadStatusV1 err := s.rpc.CallContext(execCtx, &result, "engine_newPayloadV1", payload) e.Debug("Received payload execution result", "status", result.Status, "latestValidHash", result.LatestValidHash, "message", result.ValidationError) if err != nil { @@ -119,17 +119,17 @@ func (s *Source) NewPayload(ctx context.Context, payload *ExecutionPayload) erro } switch result.Status { - case ExecutionValid: + case eth.ExecutionValid: return nil - case ExecutionSyncing: + case eth.ExecutionSyncing: return fmt.Errorf("failed to execute payload %s, node is syncing", payload.ID()) - case ExecutionInvalid: + case eth.ExecutionInvalid: return fmt.Errorf("execution payload %s was INVALID! Latest valid hash is %s, ignoring bad block: %q", payload.ID(), result.LatestValidHash, result.ValidationError) - case ExecutionInvalidBlockHash: + case eth.ExecutionInvalidBlockHash: return fmt.Errorf("execution payload %s has INVALID BLOCKHASH! %v", payload.BlockHash, result.ValidationError) - case ExecutionInvalidTerminalBlock: + case eth.ExecutionInvalidTerminalBlock: return fmt.Errorf("engine is misconfigured. Received invalid-terminal-block error while engine API should be active at genesis. err: %v", result.ValidationError) - case ExecutionAccepted: + case eth.ExecutionAccepted: return fmt.Errorf("execution payload cannot be validated yet, latest valid hash is %s", result.LatestValidHash) default: return fmt.Errorf("unknown execution status on %s: %q, ", payload.ID(), string(result.Status)) @@ -137,16 +137,16 @@ func (s *Source) NewPayload(ctx context.Context, payload *ExecutionPayload) erro } // GetPayload gets the execution payload associated with the PayloadId -func (s *Source) GetPayload(ctx context.Context, payloadId PayloadID) (*ExecutionPayload, error) { +func (s *Source) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) { e := s.log.New("payload_id", payloadId) e.Debug("getting payload") - var result ExecutionPayload + var result eth.ExecutionPayload err := s.rpc.CallContext(ctx, &result, "engine_getPayloadV1", payloadId) if err != nil { e = e.New("payload_id", payloadId, "err", err) if rpcErr, ok := err.(rpc.Error); ok { - code := ErrorCode(rpcErr.ErrorCode()) - if code != UnavailablePayload { + code := eth.ErrorCode(rpcErr.ErrorCode()) + if code != eth.UnavailablePayload { e.Warn("unexpected error code in get-payload response", "code", code) } else { e.Warn("unavailable payload in get-payload request") @@ -217,46 +217,6 @@ func blockToBlockRef(block *types.Block, genesis *rollup.Genesis) (eth.L2BlockRe }, nil } -// PayloadToBlockRef extracts the essential L2BlockRef information from an execution payload, -// falling back to genesis information if necessary. -func PayloadToBlockRef(payload *ExecutionPayload, genesis *rollup.Genesis) (eth.L2BlockRef, error) { - var l1Origin eth.BlockID - var sequenceNumber uint64 - if uint64(payload.BlockNumber) == genesis.L2.Number { - if payload.BlockHash != genesis.L2.Hash { - return eth.L2BlockRef{}, fmt.Errorf("expected L2 genesis hash to match L2 block at genesis block number %d: %s <> %s", genesis.L2.Number, payload.BlockHash, genesis.L2.Hash) - } - l1Origin = genesis.L1 - sequenceNumber = 0 - } else { - if len(payload.Transactions) == 0 { - return eth.L2BlockRef{}, fmt.Errorf("l2 block is missing L1 info deposit tx, block hash: %s", payload.BlockHash) - } - var tx types.Transaction - if err := tx.UnmarshalBinary(payload.Transactions[0]); err != nil { - return eth.L2BlockRef{}, fmt.Errorf("failed to decode first tx to read l1 info from: %v", err) - } - if tx.Type() != types.DepositTxType { - return eth.L2BlockRef{}, fmt.Errorf("first payload tx has unexpected tx type: %d", tx.Type()) - } - info, err := derive.L1InfoDepositTxData(tx.Data()) - if err != nil { - return eth.L2BlockRef{}, fmt.Errorf("failed to parse L1 info deposit tx from L2 block: %v", err) - } - l1Origin = eth.BlockID{Hash: info.BlockHash, Number: info.Number} - sequenceNumber = info.SequenceNumber - } - - return eth.L2BlockRef{ - Hash: payload.BlockHash, - Number: uint64(payload.BlockNumber), - ParentHash: payload.ParentHash, - Time: uint64(payload.Timestamp), - L1Origin: l1Origin, - SequenceNumber: sequenceNumber, - }, nil -} - type ReadOnlySource struct { rpc *rpc.Client // raw RPC client. Used for methods that do not already have bindings client *ethclient.Client // go-ethereum's wrapper around the rpc client for the eth namespace diff --git a/op-node/l2/util.go b/op-node/l2/util.go index 6109945efe9c6..a79c2c611887c 100644 --- a/op-node/l2/util.go +++ b/op-node/l2/util.go @@ -6,6 +6,8 @@ import ( "fmt" "math/big" + "github.com/ethereum-optimism/optimism/op-node/eth" + "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum/go-ethereum/core/types" @@ -20,13 +22,13 @@ import ( "github.com/ethereum/go-ethereum/trie" ) -func ComputeL2OutputRoot(l2OutputRootVersion Bytes32, blockHash common.Hash, blockRoot common.Hash, storageRoot common.Hash) Bytes32 { +func ComputeL2OutputRoot(l2OutputRootVersion eth.Bytes32, blockHash common.Hash, blockRoot common.Hash, storageRoot common.Hash) eth.Bytes32 { var buf bytes.Buffer buf.Write(l2OutputRootVersion[:]) buf.Write(blockRoot.Bytes()) buf.Write(storageRoot[:]) buf.Write(blockHash.Bytes()) - return Bytes32(crypto.Keccak256Hash(buf.Bytes())) + return eth.Bytes32(crypto.Keccak256Hash(buf.Bytes())) } type AccountResult struct { diff --git a/op-node/node/api.go b/op-node/node/api.go index 512238a8b17c1..21406347e0029 100644 --- a/op-node/node/api.go +++ b/op-node/node/api.go @@ -50,7 +50,7 @@ func newNodeAPI(config *rollup.Config, l2Client l2EthClient, log log.Logger) *no } } -func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([]l2.Bytes32, error) { +func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([]eth.Bytes32, error) { // TODO: rpc.BlockNumber doesn't support the "safe" tag. Need a new type head, err := n.client.GetBlockHeader(ctx, toBlockNumArg(number)) @@ -76,10 +76,10 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([] return nil, fmt.Errorf("invalid withdrawal root hash") } - var l2OutputRootVersion l2.Bytes32 // it's zero for now + var l2OutputRootVersion eth.Bytes32 // it's zero for now l2OutputRoot := l2.ComputeL2OutputRoot(l2OutputRootVersion, head.Hash(), head.Root, proof.StorageHash) - return []l2.Bytes32{l2OutputRootVersion, l2OutputRoot}, nil + return []eth.Bytes32{l2OutputRootVersion, l2OutputRoot}, nil } func (n *nodeAPI) Version(ctx context.Context) (string, error) { diff --git a/op-node/node/comms.go b/op-node/node/comms.go index fa2045aa024f9..b84ada6bd9976 100644 --- a/op-node/node/comms.go +++ b/op-node/node/comms.go @@ -4,24 +4,23 @@ import ( "context" "github.com/ethereum-optimism/optimism/op-node/eth" - "github.com/ethereum-optimism/optimism/op-node/l2" "github.com/libp2p/go-libp2p-core/peer" ) // Tracer configures the OpNode to share events type Tracer interface { OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) - OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *l2.ExecutionPayload) - OnPublishL2Payload(ctx context.Context, payload *l2.ExecutionPayload) + OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) + OnPublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) } type noOpTracer struct{} func (n noOpTracer) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) {} -func (n noOpTracer) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *l2.ExecutionPayload) { +func (n noOpTracer) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) { } -func (n noOpTracer) OnPublishL2Payload(ctx context.Context, payload *l2.ExecutionPayload) {} +func (n noOpTracer) OnPublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) {} var _ Tracer = (*noOpTracer)(nil) diff --git a/op-node/node/node.go b/op-node/node/node.go index ebb9ccc23d672..049ca5204f215 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -215,7 +215,7 @@ func (n *OpNode) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) { } -func (n *OpNode) PublishL2Payload(ctx context.Context, payload *l2.ExecutionPayload) error { +func (n *OpNode) PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error { n.tracer.OnPublishL2Payload(ctx, payload) // publish to p2p, if we are running p2p at all @@ -230,7 +230,7 @@ func (n *OpNode) PublishL2Payload(ctx context.Context, payload *l2.ExecutionPayl return nil } -func (n *OpNode) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *l2.ExecutionPayload) error { +func (n *OpNode) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) error { // ignore if it's from ourselves if n.p2pNode != nil && from == n.p2pNode.Host().ID() { return nil diff --git a/op-node/node/server_test.go b/op-node/node/server_test.go index 94dce3f0edadf..88a198f417837 100644 --- a/op-node/node/server_test.go +++ b/op-node/node/server_test.go @@ -94,7 +94,7 @@ func TestOutputAtBlock(t *testing.T) { client, err := dialRPCClientWithBackoff(context.Background(), log, "http://"+server.Addr().String(), nil) assert.NoError(t, err) - var out []l2.Bytes32 + var out []eth.Bytes32 err = client.CallContext(context.Background(), &out, "optimism_outputAtBlock", "latest") assert.NoError(t, err) assert.Len(t, out, 2) diff --git a/op-node/p2p/gossip.go b/op-node/p2p/gossip.go index 1390d0dfcb24d..777bd61de3366 100644 --- a/op-node/p2p/gossip.go +++ b/op-node/p2p/gossip.go @@ -9,10 +9,11 @@ import ( "sync" "time" + "github.com/ethereum-optimism/optimism/op-node/eth" + "github.com/ethereum/go-ethereum/common" lru "github.com/hashicorp/golang-lru" - "github.com/ethereum-optimism/optimism/op-node/l2" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" @@ -216,7 +217,7 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config) pubsub.ValidatorEx signatureBytes, payloadBytes := data[:65], data[65:] // [REJECT] if the block encoding is not valid - var payload l2.ExecutionPayload + var payload eth.ExecutionPayload if err := payload.UnmarshalSSZ(uint32(len(payloadBytes)), bytes.NewReader(payloadBytes)); err != nil { log.Warn("invalid payload", "err", err, "peer", id) return pubsub.ValidationReject @@ -286,7 +287,7 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config) pubsub.ValidatorEx } type GossipIn interface { - OnUnsafeL2Payload(ctx context.Context, from peer.ID, msg *l2.ExecutionPayload) error + OnUnsafeL2Payload(ctx context.Context, from peer.ID, msg *eth.ExecutionPayload) error } type GossipTopicInfo interface { @@ -295,7 +296,7 @@ type GossipTopicInfo interface { type GossipOut interface { GossipTopicInfo - PublishL2Payload(ctx context.Context, msg *l2.ExecutionPayload, signer Signer) error + PublishL2Payload(ctx context.Context, msg *eth.ExecutionPayload, signer Signer) error Close() error } @@ -311,7 +312,7 @@ func (p *publisher) BlocksTopicPeers() []peer.ID { return p.blocksTopic.ListPeers() } -func (p *publisher) PublishL2Payload(ctx context.Context, payload *l2.ExecutionPayload, signer Signer) error { +func (p *publisher) PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload, signer Signer) error { res := msgBufPool.Get().(*[]byte) buf := bytes.NewBuffer((*res)[:0]) defer func() { @@ -382,9 +383,9 @@ func JoinGossip(p2pCtx context.Context, self peer.ID, ps *pubsub.PubSub, log log type TopicSubscriber func(ctx context.Context, sub *pubsub.Subscription) type MessageHandler func(ctx context.Context, from peer.ID, msg interface{}) error -func BlocksHandler(onBlock func(ctx context.Context, from peer.ID, msg *l2.ExecutionPayload) error) MessageHandler { +func BlocksHandler(onBlock func(ctx context.Context, from peer.ID, msg *eth.ExecutionPayload) error) MessageHandler { return func(ctx context.Context, from peer.ID, msg interface{}) error { - payload, ok := msg.(*l2.ExecutionPayload) + payload, ok := msg.(*eth.ExecutionPayload) if !ok { return fmt.Errorf("expected topic validator to parse and validate data into execution payload, but got %T", msg) } diff --git a/op-node/p2p/host_test.go b/op-node/p2p/host_test.go index c1f4ddabc31c8..2fd7ed57c5422 100644 --- a/op-node/p2p/host_test.go +++ b/op-node/p2p/host_test.go @@ -9,7 +9,8 @@ import ( "testing" "time" - "github.com/ethereum-optimism/optimism/op-node/l2" + "github.com/ethereum-optimism/optimism/op-node/eth" + "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum/go-ethereum/log" @@ -76,10 +77,10 @@ func TestP2PSimple(t *testing.T) { } type mockGossipIn struct { - OnUnsafeL2PayloadFn func(ctx context.Context, from peer.ID, msg *l2.ExecutionPayload) error + OnUnsafeL2PayloadFn func(ctx context.Context, from peer.ID, msg *eth.ExecutionPayload) error } -func (m *mockGossipIn) OnUnsafeL2Payload(ctx context.Context, from peer.ID, msg *l2.ExecutionPayload) error { +func (m *mockGossipIn) OnUnsafeL2Payload(ctx context.Context, from peer.ID, msg *eth.ExecutionPayload) error { if m.OnUnsafeL2PayloadFn != nil { return m.OnUnsafeL2PayloadFn(ctx, from, msg) } diff --git a/op-node/rollup/derive/batches.go b/op-node/rollup/derive/batches.go new file mode 100644 index 0000000000000..ef1a24c5347a8 --- /dev/null +++ b/op-node/rollup/derive/batches.go @@ -0,0 +1,116 @@ +package derive + +import ( + "bytes" + "fmt" + + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum/go-ethereum/core/types" +) + +func BatchesFromEVMTransactions(config *rollup.Config, txLists []types.Transactions) ([]*BatchData, []error) { + var out []*BatchData + var errs []error + l1Signer := config.L1Signer() + for i, txs := range txLists { + for j, tx := range txs { + if to := tx.To(); to != nil && *to == config.BatchInboxAddress { + seqDataSubmitter, err := l1Signer.Sender(tx) // optimization: only derive sender if To is correct + if err != nil { + errs = append(errs, fmt.Errorf("invalid signature: tx list: %d, tx: %d, err: %w", i, j, err)) + continue // bad signature, ignore + } + // some random L1 user might have sent a transaction to our batch inbox, ignore them + if seqDataSubmitter != config.BatchSenderAddress { + errs = append(errs, fmt.Errorf("unauthorized batch submitter: tx list: %d, tx: %d", i, j)) + continue // not an authorized batch submitter, ignore + } + batches, err := DecodeBatches(config, bytes.NewReader(tx.Data())) + if err != nil { + errs = append(errs, fmt.Errorf("invalid batch: tx list: %d, tx: %d, err: %w", i, j, err)) + continue + } + out = append(out, batches...) + } + } + } + return out, errs +} + +func FilterBatches(config *rollup.Config, epoch rollup.Epoch, minL2Time uint64, maxL2Time uint64, batches []*BatchData) (out []*BatchData) { + uniqueTime := make(map[uint64]struct{}) + for _, batch := range batches { + if !ValidBatch(batch, config, epoch, minL2Time, maxL2Time) { + continue + } + // Check if we have already seen a batch for this L2 block + if _, ok := uniqueTime[batch.Timestamp]; ok { + // block already exists, batch is duplicate (first batch persists, others are ignored) + continue + } + uniqueTime[batch.Timestamp] = struct{}{} + out = append(out, batch) + } + return +} + +func ValidBatch(batch *BatchData, config *rollup.Config, epoch rollup.Epoch, minL2Time uint64, maxL2Time uint64) bool { + if batch.Epoch != epoch { + // Batch was tagged for past or future epoch, + // i.e. it was included too late or depends on the given L1 block to be processed first. + return false + } + if (batch.Timestamp-config.Genesis.L2Time)%config.BlockTime != 0 { + return false // bad timestamp, not a multiple of the block time + } + if batch.Timestamp < minL2Time { + return false // old batch + } + // limit timestamp upper bound to avoid huge amount of empty blocks + if batch.Timestamp >= maxL2Time { + return false // too far in future + } + for _, txBytes := range batch.Transactions { + if len(txBytes) == 0 { + return false // transaction data must not be empty + } + if txBytes[0] == types.DepositTxType { + return false // sequencers may not embed any deposits into batch data + } + } + return true +} + +// FillMissingBatches turns a collection of batches to the input batches for a series of blocks +func FillMissingBatches(batches []*BatchData, epoch, blockTime, minL2Time, nextL1Time uint64) []*BatchData { + m := make(map[uint64]*BatchData) + // The number of L2 blocks per sequencing window is variable, we do not immediately fill to maxL2Time: + // - ensure at least 1 block + // - fill up to the next L1 block timestamp, if higher, to keep up with L1 time + // - fill up to the last valid batch, to keep up with L2 time + newHeadL2Timestamp := minL2Time + if nextL1Time > newHeadL2Timestamp+blockTime { + newHeadL2Timestamp = nextL1Time - blockTime + } + for _, b := range batches { + m[b.BatchV1.Timestamp] = b + if b.Timestamp > newHeadL2Timestamp { + newHeadL2Timestamp = b.Timestamp + } + } + var out []*BatchData + for t := minL2Time; t <= newHeadL2Timestamp; t += blockTime { + b, ok := m[t] + if ok { + out = append(out, b) + } else { + out = append(out, &BatchData{ + BatchV1{ + Epoch: rollup.Epoch(epoch), + Timestamp: t, + }, + }) + } + } + return out +} diff --git a/op-node/rollup/derive/batches_test.go b/op-node/rollup/derive/batches_test.go new file mode 100644 index 0000000000000..8ee4dad183a9c --- /dev/null +++ b/op-node/rollup/derive/batches_test.go @@ -0,0 +1,134 @@ +package derive + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" +) + +type ValidBatchTestCase struct { + Name string + Epoch rollup.Epoch + MinL2Time uint64 + MaxL2Time uint64 + Batch BatchData + Valid bool +} + +func TestValidBatch(t *testing.T) { + testCases := []ValidBatchTestCase{ + { + Name: "valid epoch", + Epoch: 123, + MinL2Time: 43, + MaxL2Time: 52, + Batch: BatchData{BatchV1: BatchV1{ + Epoch: 123, + Timestamp: 43, + Transactions: []hexutil.Bytes{{0x01, 0x13, 0x37}, {0x02, 0x13, 0x37}}, + }}, + Valid: true, + }, + { + Name: "ignored epoch", + Epoch: 123, + MinL2Time: 43, + MaxL2Time: 52, + Batch: BatchData{BatchV1: BatchV1{ + Epoch: 122, + Timestamp: 43, + Transactions: nil, + }}, + Valid: false, + }, + { + Name: "too old", + Epoch: 123, + MinL2Time: 43, + MaxL2Time: 52, + Batch: BatchData{BatchV1: BatchV1{ + Epoch: 123, + Timestamp: 42, + Transactions: nil, + }}, + Valid: false, + }, + { + Name: "too new", + Epoch: 123, + MinL2Time: 43, + MaxL2Time: 52, + Batch: BatchData{BatchV1: BatchV1{ + Epoch: 123, + Timestamp: 52, + Transactions: nil, + }}, + Valid: false, + }, + { + Name: "wrong time alignment", + Epoch: 123, + MinL2Time: 43, + MaxL2Time: 52, + Batch: BatchData{BatchV1: BatchV1{ + Epoch: 123, + Timestamp: 46, + Transactions: nil, + }}, + Valid: false, + }, + { + Name: "good time alignment", + Epoch: 123, + MinL2Time: 43, + MaxL2Time: 52, + Batch: BatchData{BatchV1: BatchV1{ + Epoch: 123, + Timestamp: 51, // 31 + 2*10 + Transactions: nil, + }}, + Valid: true, + }, + { + Name: "empty tx", + Epoch: 123, + MinL2Time: 43, + MaxL2Time: 52, + Batch: BatchData{BatchV1: BatchV1{ + Epoch: 123, + Timestamp: 43, + Transactions: []hexutil.Bytes{{}}, + }}, + Valid: false, + }, + { + Name: "sneaky deposit", + Epoch: 123, + MinL2Time: 43, + MaxL2Time: 52, + Batch: BatchData{BatchV1: BatchV1{ + Epoch: 123, + Timestamp: 43, + Transactions: []hexutil.Bytes{{0x01}, {types.DepositTxType, 0x13, 0x37}, {0xc0, 0x13, 0x37}}, + }}, + Valid: false, + }, + } + conf := rollup.Config{ + Genesis: rollup.Genesis{ + L2Time: 31, // a genesis time that itself does not align to make it more interesting + }, + BlockTime: 2, + // other config fields are ignored and can be left empty. + } + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := ValidBatch(&testCase.Batch, &conf, testCase.Epoch, testCase.MinL2Time, testCase.MaxL2Time) + if got != testCase.Valid { + t.Fatalf("case %v was expected to return %v, but got %v", testCase, testCase.Valid, got) + } + }) + } +} diff --git a/op-node/rollup/derive/deposit_log.go b/op-node/rollup/derive/deposit_log.go new file mode 100644 index 0000000000000..40703fe83dd65 --- /dev/null +++ b/op-node/rollup/derive/deposit_log.go @@ -0,0 +1,162 @@ +package derive + +import ( + "encoding/binary" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" +) + +var ( + DepositEventABI = "TransactionDeposited(address,address,uint256,uint256,uint64,bool,bytes)" + DepositEventABIHash = crypto.Keccak256Hash([]byte(DepositEventABI)) +) + +// UnmarshalDepositLogEvent decodes an EVM log entry emitted by the deposit contract into typed deposit data. +// +// parse log data for: +// event TransactionDeposited( +// address indexed from, +// address indexed to, +// uint256 mint, +// uint256 value, +// uint64 gasLimit, +// bool isCreation, +// data data +// ); +// +// Additionally, the event log-index and +func UnmarshalDepositLogEvent(ev *types.Log) (*types.DepositTx, error) { + if len(ev.Topics) != 3 { + return nil, fmt.Errorf("expected 3 event topics (event identity, indexed from, indexed to)") + } + if ev.Topics[0] != DepositEventABIHash { + return nil, fmt.Errorf("invalid deposit event selector: %s, expected %s", ev.Topics[0], DepositEventABIHash) + } + if len(ev.Data) < 6*32 { + return nil, fmt.Errorf("deposit event data too small (%d bytes): %x", len(ev.Data), ev.Data) + } + + var dep types.DepositTx + + source := UserDepositSource{ + L1BlockHash: ev.BlockHash, + LogIndex: uint64(ev.Index), + } + dep.SourceHash = source.SourceHash() + + // indexed 0 + dep.From = common.BytesToAddress(ev.Topics[1][12:]) + // indexed 1 + to := common.BytesToAddress(ev.Topics[2][12:]) + + // unindexed data + offset := uint64(0) + + dep.Mint = new(big.Int).SetBytes(ev.Data[offset : offset+32]) + // 0 mint is represented as nil to skip minting code + if dep.Mint.Cmp(new(big.Int)) == 0 { + dep.Mint = nil + } + offset += 32 + + dep.Value = new(big.Int).SetBytes(ev.Data[offset : offset+32]) + offset += 32 + + gas := new(big.Int).SetBytes(ev.Data[offset : offset+32]) + if !gas.IsUint64() { + return nil, fmt.Errorf("bad gas value: %x", ev.Data[offset:offset+32]) + } + offset += 32 + dep.Gas = gas.Uint64() + // isCreation: If the boolean byte is 1 then dep.To will stay nil, + // and it will create a contract using L2 account nonce to determine the created address. + if ev.Data[offset+31] == 0 { + dep.To = &to + } + offset += 32 + // dynamic fields are encoded in three parts. The fixed size portion is the offset of the start of the + // data. The first 32 bytes of a `bytes` object is the length of the bytes. Then are the actual bytes + // padded out to 32 byte increments. + var dataOffset uint256.Int + dataOffset.SetBytes(ev.Data[offset : offset+32]) + offset += 32 + if !dataOffset.Eq(uint256.NewInt(offset)) { + return nil, fmt.Errorf("incorrect data offset: %v", dataOffset[0]) + } + + var dataLen uint256.Int + dataLen.SetBytes(ev.Data[offset : offset+32]) + offset += 32 + + if !dataLen.IsUint64() { + return nil, fmt.Errorf("data too large: %s", dataLen.String()) + } + // The data may be padded to a multiple of 32 bytes + maxExpectedLen := uint64(len(ev.Data)) - offset + dataLenU64 := dataLen.Uint64() + if dataLenU64 > maxExpectedLen { + return nil, fmt.Errorf("data length too long: %d, expected max %d", dataLenU64, maxExpectedLen) + } + + // remaining bytes fill the data + dep.Data = ev.Data[offset : offset+dataLenU64] + + return &dep, nil +} + +// MarshalDepositLogEvent returns an EVM log entry that encodes a TransactionDeposited event from the deposit contract. +// This is the reverse of the deposit transaction derivation. +func MarshalDepositLogEvent(depositContractAddr common.Address, deposit *types.DepositTx) *types.Log { + toBytes := common.Hash{} + if deposit.To != nil { + toBytes = deposit.To.Hash() + } + topics := []common.Hash{ + DepositEventABIHash, + deposit.From.Hash(), + toBytes, + } + + data := make([]byte, 6*32) + offset := 0 + if deposit.Mint != nil { + deposit.Mint.FillBytes(data[offset : offset+32]) + } + offset += 32 + + deposit.Value.FillBytes(data[offset : offset+32]) + offset += 32 + + binary.BigEndian.PutUint64(data[offset+24:offset+32], deposit.Gas) + offset += 32 + if deposit.To == nil { // isCreation + data[offset+31] = 1 + } + offset += 32 + binary.BigEndian.PutUint64(data[offset+24:offset+32], 5*32) + offset += 32 + binary.BigEndian.PutUint64(data[offset+24:offset+32], uint64(len(deposit.Data))) + data = append(data, deposit.Data...) + if len(data)%32 != 0 { // pad to multiple of 32 + data = append(data, make([]byte, 32-(len(data)%32))...) + } + + return &types.Log{ + Address: depositContractAddr, + Topics: topics, + Data: data, + Removed: false, + + // ignored (zeroed): + BlockNumber: 0, + TxHash: common.Hash{}, + TxIndex: 0, + BlockHash: common.Hash{}, + Index: 0, + } +} diff --git a/op-node/rollup/derive/deposit_log_test.go b/op-node/rollup/derive/deposit_log_test.go new file mode 100644 index 0000000000000..d5d2dfca00667 --- /dev/null +++ b/op-node/rollup/derive/deposit_log_test.go @@ -0,0 +1,113 @@ +package derive + +import ( + "fmt" + "math/rand" + "testing" + + "github.com/ethereum-optimism/optimism/op-node/testutils" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalLogEvent(t *testing.T) { + for i := int64(0); i < 100; i++ { + t.Run(fmt.Sprintf("random_deposit_%d", i), func(t *testing.T) { + rng := rand.New(rand.NewSource(1234 + i)) + source := UserDepositSource{ + L1BlockHash: testutils.RandomHash(rng), + LogIndex: uint64(rng.Intn(10000)), + } + depInput := testutils.GenerateDeposit(source.SourceHash(), rng) + log := MarshalDepositLogEvent(MockDepositContractAddr, depInput) + + log.TxIndex = uint(rng.Intn(10000)) + log.Index = uint(source.LogIndex) + log.BlockHash = source.L1BlockHash + depOutput, err := UnmarshalDepositLogEvent(log) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, depInput, depOutput) + }) + } +} + +// DeriveL1InfoDeposit is tested in reading_test.go, combined with the inverse ParseL1InfoDepositTxData + +// receiptData defines what a test receipt looks like +type receiptData struct { + // false = failed tx + goodReceipt bool + // false = not a deposit log + DepositLogs []bool +} + +type DeriveUserDepositsTestCase struct { + name string + // generate len(receipts) receipts + receipts []receiptData +} + +func TestDeriveUserDeposits(t *testing.T) { + testCases := []DeriveUserDepositsTestCase{ + {"no deposits", []receiptData{}}, + {"other log", []receiptData{{true, []bool{false}}}}, + {"success deposit", []receiptData{{true, []bool{true}}}}, + {"failed deposit", []receiptData{{false, []bool{true}}}}, + {"mixed deposits", []receiptData{{true, []bool{true}}, {false, []bool{true}}}}, + {"success multiple logs", []receiptData{{true, []bool{true, true}}}}, + {"failed multiple logs", []receiptData{{false, []bool{true, true}}}}, + {"not all deposit logs", []receiptData{{true, []bool{true, false, true}}}}, + {"random", []receiptData{{true, []bool{false, false, true}}, {false, []bool{}}, {true, []bool{true}}}}, + } + for i, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + rng := rand.New(rand.NewSource(1234 + int64(i))) + var receipts []*types.Receipt + var expectedDeposits []*types.DepositTx + logIndex := uint(0) + blockHash := testutils.RandomHash(rng) + for txIndex, rData := range testCase.receipts { + var logs []*types.Log + status := types.ReceiptStatusSuccessful + if !rData.goodReceipt { + status = types.ReceiptStatusFailed + } + for _, isDeposit := range rData.DepositLogs { + var ev *types.Log + if isDeposit { + source := UserDepositSource{L1BlockHash: blockHash, LogIndex: uint64(logIndex)} + dep := testutils.GenerateDeposit(source.SourceHash(), rng) + if status == types.ReceiptStatusSuccessful { + expectedDeposits = append(expectedDeposits, dep) + } + ev = MarshalDepositLogEvent(MockDepositContractAddr, dep) + } else { + ev = testutils.GenerateLog(testutils.RandomAddress(rng), nil, nil) + } + ev.TxIndex = uint(txIndex) + ev.Index = logIndex + ev.BlockHash = blockHash + logs = append(logs, ev) + logIndex++ + } + + receipts = append(receipts, &types.Receipt{ + Type: types.DynamicFeeTxType, + Status: status, + Logs: logs, + BlockHash: blockHash, + TransactionIndex: uint(txIndex), + }) + } + got, errs := UserDeposits(receipts, MockDepositContractAddr) + assert.Equal(t, len(errs), 0) + assert.Equal(t, len(got), len(expectedDeposits)) + for d, depTx := range got { + expected := expectedDeposits[d] + assert.Equal(t, expected, depTx) + } + }) + } +} diff --git a/op-node/rollup/derive/deposit_source.go b/op-node/rollup/derive/deposit_source.go new file mode 100644 index 0000000000000..cb16423184316 --- /dev/null +++ b/op-node/rollup/derive/deposit_source.go @@ -0,0 +1,46 @@ +package derive + +import ( + "encoding/binary" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +type UserDepositSource struct { + L1BlockHash common.Hash + LogIndex uint64 +} + +const ( + UserDepositSourceDomain = 0 + L1InfoDepositSourceDomain = 1 +) + +func (dep *UserDepositSource) SourceHash() common.Hash { + var input [32 * 2]byte + copy(input[:32], dep.L1BlockHash[:]) + binary.BigEndian.PutUint64(input[32*2-8:], dep.LogIndex) + depositIDHash := crypto.Keccak256Hash(input[:]) + var domainInput [32 * 2]byte + binary.BigEndian.PutUint64(domainInput[32-8:32], UserDepositSourceDomain) + copy(domainInput[32:], depositIDHash[:]) + return crypto.Keccak256Hash(domainInput[:]) +} + +type L1InfoDepositSource struct { + L1BlockHash common.Hash + SeqNumber uint64 +} + +func (dep *L1InfoDepositSource) SourceHash() common.Hash { + var input [32 * 2]byte + copy(input[:32], dep.L1BlockHash[:]) + binary.BigEndian.PutUint64(input[32*2-8:], dep.SeqNumber) + depositIDHash := crypto.Keccak256Hash(input[:]) + + var domainInput [32 * 2]byte + binary.BigEndian.PutUint64(domainInput[32-8:32], L1InfoDepositSourceDomain) + copy(domainInput[32:], depositIDHash[:]) + return crypto.Keccak256Hash(domainInput[:]) +} diff --git a/op-node/rollup/derive/deposits.go b/op-node/rollup/derive/deposits.go new file mode 100644 index 0000000000000..eb419dde2475f --- /dev/null +++ b/op-node/rollup/derive/deposits.go @@ -0,0 +1,46 @@ +package derive + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" +) + +// UserDeposits transforms the L2 block-height and L1 receipts into the transaction inputs for a full L2 block +func UserDeposits(receipts []*types.Receipt, depositContractAddr common.Address) ([]*types.DepositTx, []error) { + var out []*types.DepositTx + var errs []error + + for i, rec := range receipts { + if rec.Status != types.ReceiptStatusSuccessful { + continue + } + for j, log := range rec.Logs { + if log.Address == depositContractAddr && len(log.Topics) > 0 && log.Topics[0] == DepositEventABIHash { + dep, err := UnmarshalDepositLogEvent(log) + if err != nil { + errs = append(errs, fmt.Errorf("malformatted L1 deposit log in receipt %d, log %d: %w", i, j, err)) + } else { + out = append(out, dep) + } + } + } + } + return out, errs +} + +func DeriveDeposits(receipts []*types.Receipt, depositContractAddr common.Address) ([]hexutil.Bytes, []error) { + userDeposits, errs := UserDeposits(receipts, depositContractAddr) + encodedTxs := make([]hexutil.Bytes, 0, len(userDeposits)) + for i, tx := range userDeposits { + opaqueTx, err := types.NewTx(tx).MarshalBinary() + if err != nil { + errs = append(errs, fmt.Errorf("failed to encode user tx %d", i)) + } else { + encodedTxs = append(encodedTxs, opaqueTx) + } + } + return encodedTxs, errs +} diff --git a/op-node/rollup/derive/doc.go b/op-node/rollup/derive/doc.go index 4c03584371fe6..387508c77e0d9 100644 --- a/op-node/rollup/derive/doc.go +++ b/op-node/rollup/derive/doc.go @@ -3,14 +3,13 @@ // turned back into L1 data. // // The flow is data is as follows -// receipts, batches -> l2.PayloadAttributes with `payload_attributes.go` -// l2.PayloadAttributes -> l2.ExecutionPayload with `execution_payload.go` -// L2 block -> Corresponding L1 block info with `l1_block_info.go` +// receipts, batches -> eth.PayloadAttributes, by parsing the L1 data and deriving L2 inputs +// l2.PayloadAttributes -> l2.ExecutionPayload, by running the EVM (using an Execution Engine) +// L2 block -> Corresponding L1 block info, by parsing the first deposited transaction // // The Payload Attributes derivation stage is a pure function. -// The Execution Payload derivation stage relies on the L2 execution engine to perform the -// state update. +// The Execution Payload derivation stage relies on the L2 execution engine to perform the state update. // The inversion step is a pure function. // -// The steps should be keep separate to enable easier testing. +// The steps should be kept separate to enable easier testing. package derive diff --git a/op-node/rollup/derive/fuzz_parsers_test.go b/op-node/rollup/derive/fuzz_parsers_test.go index 8319519aa1b80..b3c075fa50d16 100644 --- a/op-node/rollup/derive/fuzz_parsers_test.go +++ b/op-node/rollup/derive/fuzz_parsers_test.go @@ -125,7 +125,7 @@ func FuzzL1InfoAgainstContract(f *testing.F) { } // FuzzUnmarshallLogEvent runs a deposit event through the EVM and checks that output of the abigen parsing matches -// what was inputted and what we parsed during the UnmarshalLogEvent function (which turns it into a deposit tx) +// what was inputted and what we parsed during the UnmarshalDepositLogEvent function (which turns it into a deposit tx) // The purpose is to check that we can never create a transaction that emits a log that we cannot parse as well // as ensuring that our custom marshalling matches abigen. func FuzzUnmarshallLogEvent(f *testing.F) { @@ -202,7 +202,7 @@ func FuzzUnmarshallLogEvent(f *testing.F) { depositEvent.Raw = types.Log{} // Clear out the log // Verify that is passes our custom unmarshalling logic - dep, err := UnmarshalLogEvent(logs[0]) + dep, err := UnmarshalDepositLogEvent(logs[0]) if err != nil { t.Fatalf("Could not unmarshal log that was emitted by the deposit contract: %v", err) } diff --git a/op-node/rollup/derive/l1_block_info.go b/op-node/rollup/derive/l1_block_info.go index ae0de6e03ff97..72e342027e8fe 100644 --- a/op-node/rollup/derive/l1_block_info.go +++ b/op-node/rollup/derive/l1_block_info.go @@ -6,9 +6,34 @@ import ( "fmt" "math/big" + "github.com/ethereum-optimism/optimism/op-bindings/predeploys" + "github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" ) +var ( + L1InfoFuncSignature = "setL1BlockValues(uint64,uint64,uint256,bytes32,uint64)" + L1InfoFuncBytes4 = crypto.Keccak256([]byte(L1InfoFuncSignature))[:4] + L1InfoDepositerAddress = common.HexToAddress("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001") + L1BlockAddress = common.HexToAddress(predeploys.L1Block) +) + +type L1Info interface { + Hash() common.Hash + ParentHash() common.Hash + Root() common.Hash // state-root + NumberU64() uint64 + Time() uint64 + // MixDigest field, reused for randomness after The Merge (Bellatrix hardfork) + MixDigest() common.Hash + BaseFee() *big.Int + ID() eth.BlockID + BlockRef() eth.L1BlockRef + ReceiptHash() common.Hash +} + // L1BlockInfo presents the information stored in a L1Block.setL1BlockValues call type L1BlockInfo struct { Number uint64 @@ -70,3 +95,50 @@ func L1InfoDepositTxData(data []byte) (L1BlockInfo, error) { err := info.UnmarshalBinary(data) return info, err } + +// L1InfoDeposit creates a L1 Info deposit transaction based on the L1 block, +// and the L2 block-height difference with the start of the epoch. +func L1InfoDeposit(seqNumber uint64, block L1Info) (*types.DepositTx, error) { + infoDat := L1BlockInfo{ + Number: block.NumberU64(), + Time: block.Time(), + BaseFee: block.BaseFee(), + BlockHash: block.Hash(), + SequenceNumber: seqNumber, + } + data, err := infoDat.MarshalBinary() + if err != nil { + return nil, err + } + + source := L1InfoDepositSource{ + L1BlockHash: block.Hash(), + SeqNumber: seqNumber, + } + // Uses ~30k normal case + // Uses ~70k on first transaction + // Round up to 75k to ensure that we always have enough gas. + return &types.DepositTx{ + SourceHash: source.SourceHash(), + From: L1InfoDepositerAddress, + To: &L1BlockAddress, + Mint: nil, + Value: big.NewInt(0), + Gas: 150_000, // TODO: temporary work around. Block 1 seems to require more gas than specced. + Data: data, + }, nil +} + +// L1InfoDepositBytes returns a serialized L1-info attributes transaction. +func L1InfoDepositBytes(seqNumber uint64, l1Info L1Info) ([]byte, error) { + dep, err := L1InfoDeposit(seqNumber, l1Info) + if err != nil { + return nil, fmt.Errorf("failed to create L1 info tx: %v", err) + } + l1Tx := types.NewTx(dep) + opaqueL1Tx, err := l1Tx.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("failed to encode L1 info tx: %v", err) + } + return opaqueL1Tx, nil +} diff --git a/op-node/rollup/derive/l1_block_info_test.go b/op-node/rollup/derive/l1_block_info_test.go index c27935cce5552..f6efabe910309 100644 --- a/op-node/rollup/derive/l1_block_info_test.go +++ b/op-node/rollup/derive/l1_block_info_test.go @@ -5,106 +5,17 @@ import ( "math/rand" "testing" - "github.com/stretchr/testify/require" - - "github.com/ethereum/go-ethereum/core/types" - - "github.com/ethereum-optimism/optimism/op-node/eth" - + "github.com/ethereum-optimism/optimism/op-node/testutils" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -type l1MockInfo struct { - hash common.Hash - parentHash common.Hash - root common.Hash - num uint64 - time uint64 - mixDigest [32]byte - baseFee *big.Int - receiptRoot common.Hash - sequenceNumber uint64 -} - -func (l *l1MockInfo) Hash() common.Hash { - return l.hash -} - -func (l *l1MockInfo) ParentHash() common.Hash { - return l.parentHash -} - -func (l *l1MockInfo) Root() common.Hash { - return l.root -} - -func (l *l1MockInfo) NumberU64() uint64 { - return l.num -} - -func (l *l1MockInfo) Time() uint64 { - return l.time -} - -func (l *l1MockInfo) MixDigest() common.Hash { - return l.mixDigest -} - -func (l *l1MockInfo) BaseFee() *big.Int { - return l.baseFee -} - -func (l *l1MockInfo) ReceiptHash() common.Hash { - return l.receiptRoot -} - -func (l *l1MockInfo) ID() eth.BlockID { - return eth.BlockID{Hash: l.hash, Number: l.num} -} - -func (l *l1MockInfo) BlockRef() eth.L1BlockRef { - return eth.L1BlockRef{ - Hash: l.hash, - Number: l.num, - ParentHash: l.parentHash, - Time: l.time, - } -} - -func randomHash(rng *rand.Rand) (out common.Hash) { - rng.Read(out[:]) - return -} - -func randomL1Info(rng *rand.Rand) *l1MockInfo { - return &l1MockInfo{ - parentHash: randomHash(rng), - num: rng.Uint64(), - time: rng.Uint64(), - hash: randomHash(rng), - baseFee: big.NewInt(rng.Int63n(1000_000 * 1e9)), // a million GWEI - receiptRoot: types.EmptyRootHash, - root: randomHash(rng), - sequenceNumber: rng.Uint64(), - } -} - -func makeInfo(fn func(l *l1MockInfo)) func(rng *rand.Rand) *l1MockInfo { - return func(rng *rand.Rand) *l1MockInfo { - l := randomL1Info(rng) - if fn != nil { - fn(l) - } - return l - } -} - -var _ L1Info = (*l1MockInfo)(nil) +var _ L1Info = (*testutils.MockL1Info)(nil) type infoTest struct { name string - mkInfo func(rng *rand.Rand) *l1MockInfo + mkInfo func(rng *rand.Rand) *testutils.MockL1Info } var MockDepositContractAddr = common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeef00000000") @@ -112,35 +23,35 @@ var MockDepositContractAddr = common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdea func TestParseL1InfoDepositTxData(t *testing.T) { // Go 1.18 will have native fuzzing for us to use, until then, we cover just the below cases cases := []infoTest{ - {"random", makeInfo(nil)}, - {"zero basefee", makeInfo(func(l *l1MockInfo) { - l.baseFee = new(big.Int) + {"random", testutils.MakeL1Info(nil)}, + {"zero basefee", testutils.MakeL1Info(func(l *testutils.MockL1Info) { + l.InfoBaseFee = new(big.Int) })}, - {"zero time", makeInfo(func(l *l1MockInfo) { - l.time = 0 + {"zero time", testutils.MakeL1Info(func(l *testutils.MockL1Info) { + l.InfoTime = 0 })}, - {"zero num", makeInfo(func(l *l1MockInfo) { - l.num = 0 + {"zero num", testutils.MakeL1Info(func(l *testutils.MockL1Info) { + l.InfoNum = 0 })}, - {"zero seq", makeInfo(func(l *l1MockInfo) { - l.sequenceNumber = 0 + {"zero seq", testutils.MakeL1Info(func(l *testutils.MockL1Info) { + l.InfoSequenceNumber = 0 })}, - {"all zero", func(rng *rand.Rand) *l1MockInfo { - return &l1MockInfo{baseFee: new(big.Int)} + {"all zero", func(rng *rand.Rand) *testutils.MockL1Info { + return &testutils.MockL1Info{InfoBaseFee: new(big.Int)} }}, } for i, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { info := testCase.mkInfo(rand.New(rand.NewSource(int64(1234 + i)))) - depTx, err := L1InfoDeposit(123, info) + depTx, err := L1InfoDeposit(info.SequenceNumber(), info) require.NoError(t, err) res, err := L1InfoDepositTxData(depTx.Data) require.NoError(t, err, "expected valid deposit info") - assert.Equal(t, res.Number, info.num) - assert.Equal(t, res.Time, info.time) + assert.Equal(t, res.Number, info.NumberU64()) + assert.Equal(t, res.Time, info.Time()) assert.True(t, res.BaseFee.Sign() >= 0) - assert.Equal(t, res.BaseFee.Bytes(), info.baseFee.Bytes()) - assert.Equal(t, res.BlockHash, info.hash) + assert.Equal(t, res.BaseFee.Bytes(), info.BaseFee().Bytes()) + assert.Equal(t, res.BlockHash, info.Hash()) }) } t.Run("no data", func(t *testing.T) { diff --git a/op-node/rollup/derive/payload_attributes.go b/op-node/rollup/derive/payload_attributes.go deleted file mode 100644 index af9a457ed07e9..0000000000000 --- a/op-node/rollup/derive/payload_attributes.go +++ /dev/null @@ -1,367 +0,0 @@ -package derive - -import ( - "bytes" - "encoding/binary" - "fmt" - "math/big" - - "github.com/ethereum-optimism/optimism/op-bindings/predeploys" - "github.com/ethereum-optimism/optimism/op-node/eth" - "github.com/ethereum-optimism/optimism/op-node/rollup" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/holiman/uint256" -) - -var ( - DepositEventABI = "TransactionDeposited(address,address,uint256,uint256,uint64,bool,bytes)" - DepositEventABIHash = crypto.Keccak256Hash([]byte(DepositEventABI)) - L1InfoFuncSignature = "setL1BlockValues(uint64,uint64,uint256,bytes32,uint64)" - L1InfoFuncBytes4 = crypto.Keccak256([]byte(L1InfoFuncSignature))[:4] - L1InfoDepositerAddress = common.HexToAddress("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001") - L1BlockAddress = common.HexToAddress(predeploys.L1Block) -) - -type UserDepositSource struct { - L1BlockHash common.Hash - LogIndex uint64 -} - -const ( - UserDepositSourceDomain = 0 - L1InfoDepositSourceDomain = 1 -) - -func (dep *UserDepositSource) SourceHash() common.Hash { - var input [32 * 2]byte - copy(input[:32], dep.L1BlockHash[:]) - binary.BigEndian.PutUint64(input[32*2-8:], dep.LogIndex) - depositIDHash := crypto.Keccak256Hash(input[:]) - var domainInput [32 * 2]byte - binary.BigEndian.PutUint64(domainInput[32-8:32], UserDepositSourceDomain) - copy(domainInput[32:], depositIDHash[:]) - return crypto.Keccak256Hash(domainInput[:]) -} - -type L1InfoDepositSource struct { - L1BlockHash common.Hash - SeqNumber uint64 -} - -func (dep *L1InfoDepositSource) SourceHash() common.Hash { - var input [32 * 2]byte - copy(input[:32], dep.L1BlockHash[:]) - binary.BigEndian.PutUint64(input[32*2-8:], dep.SeqNumber) - depositIDHash := crypto.Keccak256Hash(input[:]) - - var domainInput [32 * 2]byte - binary.BigEndian.PutUint64(domainInput[32-8:32], L1InfoDepositSourceDomain) - copy(domainInput[32:], depositIDHash[:]) - return crypto.Keccak256Hash(domainInput[:]) -} - -// UnmarshalLogEvent decodes an EVM log entry emitted by the deposit contract into typed deposit data. -// -// parse log data for: -// event TransactionDeposited( -// address indexed from, -// address indexed to, -// uint256 mint, -// uint256 value, -// uint64 gasLimit, -// bool isCreation, -// data data -// ); -// -// Additionally, the event log-index and -func UnmarshalLogEvent(ev *types.Log) (*types.DepositTx, error) { - if len(ev.Topics) != 3 { - return nil, fmt.Errorf("expected 3 event topics (event identity, indexed from, indexed to)") - } - if ev.Topics[0] != DepositEventABIHash { - return nil, fmt.Errorf("invalid deposit event selector: %s, expected %s", ev.Topics[0], DepositEventABIHash) - } - if len(ev.Data) < 6*32 { - return nil, fmt.Errorf("deposit event data too small (%d bytes): %x", len(ev.Data), ev.Data) - } - - var dep types.DepositTx - - source := UserDepositSource{ - L1BlockHash: ev.BlockHash, - LogIndex: uint64(ev.Index), - } - dep.SourceHash = source.SourceHash() - - // indexed 0 - dep.From = common.BytesToAddress(ev.Topics[1][12:]) - // indexed 1 - to := common.BytesToAddress(ev.Topics[2][12:]) - - // unindexed data - offset := uint64(0) - - dep.Mint = new(big.Int).SetBytes(ev.Data[offset : offset+32]) - // 0 mint is represented as nil to skip minting code - if dep.Mint.Cmp(new(big.Int)) == 0 { - dep.Mint = nil - } - offset += 32 - - dep.Value = new(big.Int).SetBytes(ev.Data[offset : offset+32]) - offset += 32 - - gas := new(big.Int).SetBytes(ev.Data[offset : offset+32]) - if !gas.IsUint64() { - return nil, fmt.Errorf("bad gas value: %x", ev.Data[offset:offset+32]) - } - offset += 32 - dep.Gas = gas.Uint64() - // isCreation: If the boolean byte is 1 then dep.To will stay nil, - // and it will create a contract using L2 account nonce to determine the created address. - if ev.Data[offset+31] == 0 { - dep.To = &to - } - offset += 32 - // dynamic fields are encoded in three parts. The fixed size portion is the offset of the start of the - // data. The first 32 bytes of a `bytes` object is the length of the bytes. Then are the actual bytes - // padded out to 32 byte increments. - var dataOffset uint256.Int - dataOffset.SetBytes(ev.Data[offset : offset+32]) - offset += 32 - if !dataOffset.Eq(uint256.NewInt(offset)) { - return nil, fmt.Errorf("incorrect data offset: %v", dataOffset[0]) - } - - var dataLen uint256.Int - dataLen.SetBytes(ev.Data[offset : offset+32]) - offset += 32 - - if !dataLen.IsUint64() { - return nil, fmt.Errorf("data too large: %s", dataLen.String()) - } - // The data may be padded to a multiple of 32 bytes - maxExpectedLen := uint64(len(ev.Data)) - offset - dataLenU64 := dataLen.Uint64() - if dataLenU64 > maxExpectedLen { - return nil, fmt.Errorf("data length too long: %d, expected max %d", dataLenU64, maxExpectedLen) - } - - // remaining bytes fill the data - dep.Data = ev.Data[offset : offset+dataLenU64] - - return &dep, nil -} - -type L1Info interface { - Hash() common.Hash - ParentHash() common.Hash - Root() common.Hash // state-root - NumberU64() uint64 - Time() uint64 - // MixDigest field, reused for randomness after The Merge (Bellatrix hardfork) - MixDigest() common.Hash - BaseFee() *big.Int - ID() eth.BlockID - BlockRef() eth.L1BlockRef - ReceiptHash() common.Hash -} - -// L1InfoDeposit creates a L1 Info deposit transaction based on the L1 block, -// and the L2 block-height difference with the start of the epoch. -func L1InfoDeposit(seqNumber uint64, block L1Info) (*types.DepositTx, error) { - infoDat := L1BlockInfo{ - Number: block.NumberU64(), - Time: block.Time(), - BaseFee: block.BaseFee(), - BlockHash: block.Hash(), - SequenceNumber: seqNumber, - } - data, err := infoDat.MarshalBinary() - if err != nil { - return nil, err - } - - source := L1InfoDepositSource{ - L1BlockHash: block.Hash(), - SeqNumber: seqNumber, - } - // Uses ~30k normal case - // Uses ~70k on first transaction - // Round up to 75k to ensure that we always have enough gas. - return &types.DepositTx{ - SourceHash: source.SourceHash(), - From: L1InfoDepositerAddress, - To: &L1BlockAddress, - Mint: nil, - Value: big.NewInt(0), - Gas: 150_000, // TODO: temporary work around. Block 1 seems to require more gas than specced. - Data: data, - }, nil -} - -// UserDeposits transforms the L2 block-height and L1 receipts into the transaction inputs for a full L2 block -func UserDeposits(receipts []*types.Receipt, depositContractAddr common.Address) ([]*types.DepositTx, []error) { - var out []*types.DepositTx - var errs []error - - for i, rec := range receipts { - if rec.Status != types.ReceiptStatusSuccessful { - continue - } - for j, log := range rec.Logs { - if log.Address == depositContractAddr && len(log.Topics) > 0 && log.Topics[0] == DepositEventABIHash { - dep, err := UnmarshalLogEvent(log) - if err != nil { - errs = append(errs, fmt.Errorf("malformatted L1 deposit log in receipt %d, log %d: %w", i, j, err)) - } else { - out = append(out, dep) - } - } - } - } - return out, errs -} - -func BatchesFromEVMTransactions(config *rollup.Config, txLists []types.Transactions) ([]*BatchData, []error) { - var out []*BatchData - var errs []error - l1Signer := config.L1Signer() - for i, txs := range txLists { - for j, tx := range txs { - if to := tx.To(); to != nil && *to == config.BatchInboxAddress { - seqDataSubmitter, err := l1Signer.Sender(tx) // optimization: only derive sender if To is correct - if err != nil { - errs = append(errs, fmt.Errorf("invalid signature: tx list: %d, tx: %d, err: %w", i, j, err)) - continue // bad signature, ignore - } - // some random L1 user might have sent a transaction to our batch inbox, ignore them - if seqDataSubmitter != config.BatchSenderAddress { - errs = append(errs, fmt.Errorf("unauthorized batch submitter: tx list: %d, tx: %d", i, j)) - continue // not an authorized batch submitter, ignore - } - batches, err := DecodeBatches(config, bytes.NewReader(tx.Data())) - if err != nil { - errs = append(errs, fmt.Errorf("invalid batch: tx list: %d, tx: %d, err: %w", i, j, err)) - continue - } - out = append(out, batches...) - } - } - } - return out, errs -} - -func FilterBatches(config *rollup.Config, epoch rollup.Epoch, minL2Time uint64, maxL2Time uint64, batches []*BatchData) (out []*BatchData) { - uniqueTime := make(map[uint64]struct{}) - for _, batch := range batches { - if !ValidBatch(batch, config, epoch, minL2Time, maxL2Time) { - continue - } - // Check if we have already seen a batch for this L2 block - if _, ok := uniqueTime[batch.Timestamp]; ok { - // block already exists, batch is duplicate (first batch persists, others are ignored) - continue - } - uniqueTime[batch.Timestamp] = struct{}{} - out = append(out, batch) - } - return -} - -func ValidBatch(batch *BatchData, config *rollup.Config, epoch rollup.Epoch, minL2Time uint64, maxL2Time uint64) bool { - if batch.Epoch != epoch { - // Batch was tagged for past or future epoch, - // i.e. it was included too late or depends on the given L1 block to be processed first. - return false - } - if (batch.Timestamp-config.Genesis.L2Time)%config.BlockTime != 0 { - return false // bad timestamp, not a multiple of the block time - } - if batch.Timestamp < minL2Time { - return false // old batch - } - // limit timestamp upper bound to avoid huge amount of empty blocks - if batch.Timestamp >= maxL2Time { - return false // too far in future - } - for _, txBytes := range batch.Transactions { - if len(txBytes) == 0 { - return false // transaction data must not be empty - } - if txBytes[0] == types.DepositTxType { - return false // sequencers may not embed any deposits into batch data - } - } - return true -} - -type L2Info interface { - Time() uint64 -} - -// FillMissingBatches turns a collection of batches to the input batches for a series of blocks -func FillMissingBatches(batches []*BatchData, epoch, blockTime, minL2Time, nextL1Time uint64) []*BatchData { - m := make(map[uint64]*BatchData) - // The number of L2 blocks per sequencing window is variable, we do not immediately fill to maxL2Time: - // - ensure at least 1 block - // - fill up to the next L1 block timestamp, if higher, to keep up with L1 time - // - fill up to the last valid batch, to keep up with L2 time - newHeadL2Timestamp := minL2Time - if nextL1Time > newHeadL2Timestamp+blockTime { - newHeadL2Timestamp = nextL1Time - blockTime - } - for _, b := range batches { - m[b.BatchV1.Timestamp] = b - if b.Timestamp > newHeadL2Timestamp { - newHeadL2Timestamp = b.Timestamp - } - } - var out []*BatchData - for t := minL2Time; t <= newHeadL2Timestamp; t += blockTime { - b, ok := m[t] - if ok { - out = append(out, b) - } else { - out = append(out, &BatchData{ - BatchV1{ - Epoch: rollup.Epoch(epoch), - Timestamp: t, - }, - }) - } - } - return out -} - -// L1InfoDepositBytes returns a serialized L1-info attributes transaction. -func L1InfoDepositBytes(seqNumber uint64, l1Info L1Info) (hexutil.Bytes, error) { - dep, err := L1InfoDeposit(seqNumber, l1Info) - if err != nil { - return nil, fmt.Errorf("failed to create L1 info tx: %v", err) - } - l1Tx := types.NewTx(dep) - opaqueL1Tx, err := l1Tx.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("failed to encode L1 info tx: %v", err) - } - return opaqueL1Tx, nil -} - -func DeriveDeposits(receipts []*types.Receipt, depositContractAddr common.Address) ([]hexutil.Bytes, []error) { - userDeposits, errs := UserDeposits(receipts, depositContractAddr) - encodedTxs := make([]hexutil.Bytes, 0, len(userDeposits)) - for i, tx := range userDeposits { - opaqueTx, err := types.NewTx(tx).MarshalBinary() - if err != nil { - errs = append(errs, fmt.Errorf("failed to encode user tx %d", i)) - } else { - encodedTxs = append(encodedTxs, opaqueTx) - } - } - return encodedTxs, errs -} diff --git a/op-node/rollup/derive/payload_attributes_test.go b/op-node/rollup/derive/payload_attributes_test.go deleted file mode 100644 index 8fa52cd4de8f2..0000000000000 --- a/op-node/rollup/derive/payload_attributes_test.go +++ /dev/null @@ -1,341 +0,0 @@ -package derive - -import ( - "encoding/binary" - "fmt" - "math/big" - "math/rand" - "testing" - - "github.com/ethereum-optimism/optimism/op-node/rollup" - "github.com/ethereum/go-ethereum/common/hexutil" - - "github.com/stretchr/testify/assert" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" -) - -func GenerateAddress(rng *rand.Rand) (out common.Address) { - rng.Read(out[:]) - return -} - -func RandETH(rng *rand.Rand, max int64) *big.Int { - x := big.NewInt(rng.Int63n(max)) - x = new(big.Int).Mul(x, big.NewInt(1e18)) - return x -} - -// Returns a DepositEvent customized on the basis of the id parameter. -func GenerateDeposit(source UserDepositSource, rng *rand.Rand) *types.DepositTx { - dataLen := rng.Int63n(10_000) - data := make([]byte, dataLen) - rng.Read(data) - - var to *common.Address - if rng.Intn(2) == 0 { - x := GenerateAddress(rng) - to = &x - } - var mint *big.Int - if rng.Intn(2) == 0 { - mint = RandETH(rng, 200) - } - - dep := &types.DepositTx{ - SourceHash: source.SourceHash(), - From: GenerateAddress(rng), - To: to, - Value: RandETH(rng, 200), - Gas: uint64(rng.Int63n(10 * 1e6)), // 10 M gas max - Data: data, - Mint: mint, - } - return dep -} - -// Generates an EVM log entry that encodes a TransactionDeposited event from the deposit contract. -// Calls GenerateDeposit with random number generator to generate the deposit. -func GenerateDepositLog(deposit *types.DepositTx) *types.Log { - - toBytes := common.Hash{} - if deposit.To != nil { - toBytes = deposit.To.Hash() - } - topics := []common.Hash{ - DepositEventABIHash, - deposit.From.Hash(), - toBytes, - } - - data := make([]byte, 6*32) - offset := 0 - if deposit.Mint != nil { - deposit.Mint.FillBytes(data[offset : offset+32]) - } - offset += 32 - - deposit.Value.FillBytes(data[offset : offset+32]) - offset += 32 - - binary.BigEndian.PutUint64(data[offset+24:offset+32], deposit.Gas) - offset += 32 - if deposit.To == nil { // isCreation - data[offset+31] = 1 - } - offset += 32 - binary.BigEndian.PutUint64(data[offset+24:offset+32], 5*32) - offset += 32 - binary.BigEndian.PutUint64(data[offset+24:offset+32], uint64(len(deposit.Data))) - data = append(data, deposit.Data...) - if len(data)%32 != 0 { // pad to multiple of 32 - data = append(data, make([]byte, 32-(len(data)%32))...) - } - - return GenerateLog(MockDepositContractAddr, topics, data) -} - -// Generates an EVM log entry with the given topics and data. -func GenerateLog(addr common.Address, topics []common.Hash, data []byte) *types.Log { - return &types.Log{ - Address: addr, - Topics: topics, - Data: data, - Removed: false, - - // ignored (zeroed): - BlockNumber: 0, - TxHash: common.Hash{}, - TxIndex: 0, - BlockHash: common.Hash{}, - Index: 0, - } -} - -func TestUnmarshalLogEvent(t *testing.T) { - for i := int64(0); i < 100; i++ { - t.Run(fmt.Sprintf("random_deposit_%d", i), func(t *testing.T) { - rng := rand.New(rand.NewSource(1234 + i)) - source := UserDepositSource{ - L1BlockHash: randomHash(rng), - LogIndex: uint64(rng.Intn(10000)), - } - depInput := GenerateDeposit(source, rng) - log := GenerateDepositLog(depInput) - - log.TxIndex = uint(rng.Intn(10000)) - log.Index = uint(source.LogIndex) - log.BlockHash = source.L1BlockHash - depOutput, err := UnmarshalLogEvent(log) - if err != nil { - t.Fatal(err) - } - assert.Equal(t, depInput, depOutput) - }) - } -} - -// DeriveL1InfoDeposit is tested in reading_test.go, combined with the inverse ParseL1InfoDepositTxData - -// receiptData defines what a test receipt looks like -type receiptData struct { - // false = failed tx - goodReceipt bool - // false = not a deposit log - DepositLogs []bool -} - -type DeriveUserDepositsTestCase struct { - name string - // generate len(receipts) receipts - receipts []receiptData -} - -func TestDeriveUserDeposits(t *testing.T) { - testCases := []DeriveUserDepositsTestCase{ - {"no deposits", []receiptData{}}, - {"other log", []receiptData{{true, []bool{false}}}}, - {"success deposit", []receiptData{{true, []bool{true}}}}, - {"failed deposit", []receiptData{{false, []bool{true}}}}, - {"mixed deposits", []receiptData{{true, []bool{true}}, {false, []bool{true}}}}, - {"success multiple logs", []receiptData{{true, []bool{true, true}}}}, - {"failed multiple logs", []receiptData{{false, []bool{true, true}}}}, - {"not all deposit logs", []receiptData{{true, []bool{true, false, true}}}}, - {"random", []receiptData{{true, []bool{false, false, true}}, {false, []bool{}}, {true, []bool{true}}}}, - } - for i, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - rng := rand.New(rand.NewSource(1234 + int64(i))) - var receipts []*types.Receipt - var expectedDeposits []*types.DepositTx - logIndex := uint(0) - blockHash := randomHash(rng) - for txIndex, rData := range testCase.receipts { - var logs []*types.Log - status := types.ReceiptStatusSuccessful - if !rData.goodReceipt { - status = types.ReceiptStatusFailed - } - for _, isDeposit := range rData.DepositLogs { - var ev *types.Log - if isDeposit { - source := UserDepositSource{L1BlockHash: blockHash, LogIndex: uint64(logIndex)} - dep := GenerateDeposit(source, rng) - if status == types.ReceiptStatusSuccessful { - expectedDeposits = append(expectedDeposits, dep) - } - ev = GenerateDepositLog(dep) - } else { - ev = GenerateLog(GenerateAddress(rng), nil, nil) - } - ev.TxIndex = uint(txIndex) - ev.Index = logIndex - ev.BlockHash = blockHash - logs = append(logs, ev) - logIndex++ - } - - receipts = append(receipts, &types.Receipt{ - Type: types.DynamicFeeTxType, - Status: status, - Logs: logs, - BlockHash: blockHash, - TransactionIndex: uint(txIndex), - }) - } - got, errs := UserDeposits(receipts, MockDepositContractAddr) - assert.Equal(t, len(errs), 0) - assert.Equal(t, len(got), len(expectedDeposits)) - for d, depTx := range got { - expected := expectedDeposits[d] - assert.Equal(t, expected, depTx) - } - }) - } -} - -type ValidBatchTestCase struct { - Name string - Epoch rollup.Epoch - MinL2Time uint64 - MaxL2Time uint64 - Batch BatchData - Valid bool -} - -func TestValidBatch(t *testing.T) { - testCases := []ValidBatchTestCase{ - { - Name: "valid epoch", - Epoch: 123, - MinL2Time: 43, - MaxL2Time: 52, - Batch: BatchData{BatchV1: BatchV1{ - Epoch: 123, - Timestamp: 43, - Transactions: []hexutil.Bytes{{0x01, 0x13, 0x37}, {0x02, 0x13, 0x37}}, - }}, - Valid: true, - }, - { - Name: "ignored epoch", - Epoch: 123, - MinL2Time: 43, - MaxL2Time: 52, - Batch: BatchData{BatchV1: BatchV1{ - Epoch: 122, - Timestamp: 43, - Transactions: nil, - }}, - Valid: false, - }, - { - Name: "too old", - Epoch: 123, - MinL2Time: 43, - MaxL2Time: 52, - Batch: BatchData{BatchV1: BatchV1{ - Epoch: 123, - Timestamp: 42, - Transactions: nil, - }}, - Valid: false, - }, - { - Name: "too new", - Epoch: 123, - MinL2Time: 43, - MaxL2Time: 52, - Batch: BatchData{BatchV1: BatchV1{ - Epoch: 123, - Timestamp: 52, - Transactions: nil, - }}, - Valid: false, - }, - { - Name: "wrong time alignment", - Epoch: 123, - MinL2Time: 43, - MaxL2Time: 52, - Batch: BatchData{BatchV1: BatchV1{ - Epoch: 123, - Timestamp: 46, - Transactions: nil, - }}, - Valid: false, - }, - { - Name: "good time alignment", - Epoch: 123, - MinL2Time: 43, - MaxL2Time: 52, - Batch: BatchData{BatchV1: BatchV1{ - Epoch: 123, - Timestamp: 51, // 31 + 2*10 - Transactions: nil, - }}, - Valid: true, - }, - { - Name: "empty tx", - Epoch: 123, - MinL2Time: 43, - MaxL2Time: 52, - Batch: BatchData{BatchV1: BatchV1{ - Epoch: 123, - Timestamp: 43, - Transactions: []hexutil.Bytes{{}}, - }}, - Valid: false, - }, - { - Name: "sneaky deposit", - Epoch: 123, - MinL2Time: 43, - MaxL2Time: 52, - Batch: BatchData{BatchV1: BatchV1{ - Epoch: 123, - Timestamp: 43, - Transactions: []hexutil.Bytes{{0x01}, {types.DepositTxType, 0x13, 0x37}, {0xc0, 0x13, 0x37}}, - }}, - Valid: false, - }, - } - conf := rollup.Config{ - Genesis: rollup.Genesis{ - L2Time: 31, // a genesis time that itself does not align to make it more interesting - }, - BlockTime: 2, - // other config fields are ignored and can be left empty. - } - for _, testCase := range testCases { - t.Run(testCase.Name, func(t *testing.T) { - got := ValidBatch(&testCase.Batch, &conf, testCase.Epoch, testCase.MinL2Time, testCase.MaxL2Time) - if got != testCase.Valid { - t.Fatalf("case %v was expected to return %v, but got %v", testCase, testCase.Valid, got) - } - }) - } -} diff --git a/op-node/rollup/derive/payload_util.go b/op-node/rollup/derive/payload_util.go new file mode 100644 index 0000000000000..fbe058313a2f7 --- /dev/null +++ b/op-node/rollup/derive/payload_util.go @@ -0,0 +1,49 @@ +package derive + +import ( + "fmt" + + "github.com/ethereum-optimism/optimism/op-node/eth" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum/go-ethereum/core/types" +) + +// PayloadToBlockRef extracts the essential L2BlockRef information from an execution payload, +// falling back to genesis information if necessary. +func PayloadToBlockRef(payload *eth.ExecutionPayload, genesis *rollup.Genesis) (eth.L2BlockRef, error) { + var l1Origin eth.BlockID + var sequenceNumber uint64 + if uint64(payload.BlockNumber) == genesis.L2.Number { + if payload.BlockHash != genesis.L2.Hash { + return eth.L2BlockRef{}, fmt.Errorf("expected L2 genesis hash to match L2 block at genesis block number %d: %s <> %s", genesis.L2.Number, payload.BlockHash, genesis.L2.Hash) + } + l1Origin = genesis.L1 + sequenceNumber = 0 + } else { + if len(payload.Transactions) == 0 { + return eth.L2BlockRef{}, fmt.Errorf("l2 block is missing L1 info deposit tx, block hash: %s", payload.BlockHash) + } + var tx types.Transaction + if err := tx.UnmarshalBinary(payload.Transactions[0]); err != nil { + return eth.L2BlockRef{}, fmt.Errorf("failed to decode first tx to read l1 info from: %v", err) + } + if tx.Type() != types.DepositTxType { + return eth.L2BlockRef{}, fmt.Errorf("first payload tx has unexpected tx type: %d", tx.Type()) + } + info, err := L1InfoDepositTxData(tx.Data()) + if err != nil { + return eth.L2BlockRef{}, fmt.Errorf("failed to parse L1 info deposit tx from L2 block: %v", err) + } + l1Origin = eth.BlockID{Hash: info.BlockHash, Number: info.Number} + sequenceNumber = info.SequenceNumber + } + + return eth.L2BlockRef{ + Hash: payload.BlockHash, + Number: uint64(payload.BlockNumber), + ParentHash: payload.ParentHash, + Time: uint64(payload.Timestamp), + L1Origin: l1Origin, + SequenceNumber: sequenceNumber, + }, nil +} diff --git a/op-node/rollup/driver/driver.go b/op-node/rollup/driver/driver.go index faa1a3afff523..982026c875e9e 100644 --- a/op-node/rollup/driver/driver.go +++ b/op-node/rollup/driver/driver.go @@ -29,11 +29,11 @@ type Downloader interface { } type Engine interface { - GetPayload(ctx context.Context, payloadId l2.PayloadID) (*l2.ExecutionPayload, error) - ForkchoiceUpdate(ctx context.Context, state *l2.ForkchoiceState, attr *l2.PayloadAttributes) (*l2.ForkchoiceUpdatedResult, error) - NewPayload(ctx context.Context, payload *l2.ExecutionPayload) error - PayloadByHash(context.Context, common.Hash) (*l2.ExecutionPayload, error) - PayloadByNumber(context.Context, *big.Int) (*l2.ExecutionPayload, error) + GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) + ForkchoiceUpdate(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) + NewPayload(ctx context.Context, payload *eth.ExecutionPayload) error + PayloadByHash(context.Context, common.Hash) (*eth.ExecutionPayload, error) + PayloadByNumber(context.Context, *big.Int) (*eth.ExecutionPayload, error) } type L1Chain interface { @@ -44,7 +44,7 @@ type L1Chain interface { } type L2Chain interface { - ForkchoiceUpdate(ctx context.Context, state *l2.ForkchoiceState, attr *l2.PayloadAttributes) (*l2.ForkchoiceUpdatedResult, error) + ForkchoiceUpdate(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error) L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) } @@ -55,15 +55,15 @@ type outputInterface interface { insertEpoch(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.L2BlockRef, l2Finalized eth.BlockID, l1Input []eth.BlockID) (eth.L2BlockRef, eth.L2BlockRef, bool, error) // createNewBlock builds a new block based on the L2 Head, L1 Origin, and the current mempool. - createNewBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, l1Origin eth.L1BlockRef) (eth.L2BlockRef, *l2.ExecutionPayload, error) + createNewBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, l1Origin eth.L1BlockRef) (eth.L2BlockRef, *eth.ExecutionPayload, error) // processBlock simply tries to add the block to the chain, reorging if necessary, and updates the forkchoice of the engine. - processBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, payload *l2.ExecutionPayload) error + processBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, payload *eth.ExecutionPayload) error } type Network interface { // PublishL2Payload is called by the driver whenever there is a new payload to publish, synchronously with the driver main loop. - PublishL2Payload(ctx context.Context, payload *l2.ExecutionPayload) error + PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error } func NewDriver(cfg rollup.Config, l2 *l2.Source, l1 *l1.Source, network Network, log log.Logger, snapshotLog log.Logger, sequencer bool) *Driver { @@ -82,7 +82,7 @@ func (d *Driver) OnL1Head(ctx context.Context, head eth.L1BlockRef) error { return d.s.OnL1Head(ctx, head) } -func (d *Driver) OnUnsafeL2Payload(ctx context.Context, payload *l2.ExecutionPayload) error { +func (d *Driver) OnUnsafeL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error { return d.s.OnUnsafeL2Payload(ctx, payload) } diff --git a/op-node/rollup/driver/state.go b/op-node/rollup/driver/state.go index 5c05210600b64..382707967a424 100644 --- a/op-node/rollup/driver/state.go +++ b/op-node/rollup/driver/state.go @@ -8,8 +8,8 @@ import ( "time" "github.com/ethereum-optimism/optimism/op-node/eth" - "github.com/ethereum-optimism/optimism/op-node/l2" "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/sync" "github.com/ethereum/go-ethereum/log" ) @@ -28,7 +28,7 @@ type state struct { // Connections (in/out) l1Heads chan eth.L1BlockRef - unsafeL2Payloads chan *l2.ExecutionPayload + unsafeL2Payloads chan *eth.ExecutionPayload l1 L1Chain l2 L2Chain output outputInterface @@ -55,7 +55,7 @@ func NewState(log log.Logger, snapshotLog log.Logger, config rollup.Config, l1Ch network: network, sequencer: sequencer, l1Heads: make(chan eth.L1BlockRef, 10), - unsafeL2Payloads: make(chan *l2.ExecutionPayload, 10), + unsafeL2Payloads: make(chan *eth.ExecutionPayload, 10), } } @@ -120,7 +120,7 @@ func (s *state) OnL1Head(ctx context.Context, head eth.L1BlockRef) error { } } -func (s *state) OnUnsafeL2Payload(ctx context.Context, payload *l2.ExecutionPayload) error { +func (s *state) OnUnsafeL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error { select { case <-ctx.Done(): return ctx.Err() @@ -167,7 +167,7 @@ func (s *state) handleNewL1Block(ctx context.Context, newL1Head eth.L1BlockRef) return err } // Update forkchoice - fc := l2.ForkchoiceState{ + fc := eth.ForkchoiceState{ HeadBlockHash: unsafeL2Head.Hash, SafeBlockHash: safeL2Head.Hash, FinalizedBlockHash: s.l2Finalized.Hash, @@ -316,7 +316,7 @@ func (s *state) handleEpoch(ctx context.Context) (bool, error) { } -func (s *state) handleUnsafeL2Payload(ctx context.Context, payload *l2.ExecutionPayload) error { +func (s *state) handleUnsafeL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error { if s.l2SafeHead.Number > uint64(payload.BlockNumber) { s.log.Info("ignoring unsafe L2 execution payload, already have safe payload", "id", payload.ID()) return nil @@ -326,7 +326,7 @@ func (s *state) handleUnsafeL2Payload(ctx context.Context, payload *l2.Execution // The engine should never reorg past the finalized block hash however. // The engine may attempt syncing via p2p if there is a larger gap in the L2 chain. - l2Ref, err := l2.PayloadToBlockRef(payload, &s.Config.Genesis) + l2Ref, err := derive.PayloadToBlockRef(payload, &s.Config.Genesis) if err != nil { return fmt.Errorf("failed to derive L2 block ref from payload: %v", err) } diff --git a/op-node/rollup/driver/state_test.go b/op-node/rollup/driver/state_test.go index d11ccd983e80a..7eb8f1bfbae1d 100644 --- a/op-node/rollup/driver/state_test.go +++ b/op-node/rollup/driver/state_test.go @@ -2,17 +2,13 @@ package driver import ( "context" - "strconv" - "strings" "testing" "time" "github.com/ethereum-optimism/optimism/op-node/eth" - "github.com/ethereum-optimism/optimism/op-node/l2" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testutils" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,31 +17,11 @@ import ( var _ L1Chain = (*testutils.FakeChainSource)(nil) var _ L2Chain = (*testutils.FakeChainSource)(nil) -type testID string - -func (id testID) ID() eth.BlockID { - parts := strings.Split(string(id), ":") - if len(parts) != 2 { - panic("bad id") - } - if len(parts[0]) > 32 { - panic("test ID hash too long") - } - var h common.Hash - copy(h[:], parts[0]) - v, err := strconv.ParseUint(parts[1], 0, 64) - if err != nil { - panic(err) - } - return eth.BlockID{ - Hash: h, - Number: v, - } -} +type TestID = testutils.TestID type outputHandlerFn func(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.L2BlockRef, l2Finalized eth.BlockID, l1Input []eth.BlockID) (eth.L2BlockRef, eth.L2BlockRef, bool, error) -func (fn outputHandlerFn) processBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, payload *l2.ExecutionPayload) error { +func (fn outputHandlerFn) processBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, payload *eth.ExecutionPayload) error { // TODO: maybe mock a failed block? return nil } @@ -54,7 +30,7 @@ func (fn outputHandlerFn) insertEpoch(ctx context.Context, l2Head eth.L2BlockRef return fn(ctx, l2Head, l2SafeHead, l2Finalized, l1Input) } -func (fn outputHandlerFn) createNewBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, l1Origin eth.L1BlockRef) (eth.L2BlockRef, *l2.ExecutionPayload, error) { +func (fn outputHandlerFn) createNewBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, l1Origin eth.L1BlockRef) (eth.L2BlockRef, *eth.ExecutionPayload, error) { panic("Unimplemented") } @@ -71,13 +47,13 @@ type outputReturnArgs struct { type stateTestCaseStep struct { // Expect l1head, l2head, and sequence window - l1head testID - l2head testID - window []testID + l1head TestID + l2head TestID + window []TestID // l1act and l2act are ran at each step l1act func(t *testing.T, s *state, src *testutils.FakeChainSource) - l2act func(t *testing.T, expectedWindow []testID, s *state, src *testutils.FakeChainSource, outputIn chan outputArgs, outputReturn chan outputReturnArgs) + l2act func(t *testing.T, expectedWindow []TestID, s *state, src *testutils.FakeChainSource, outputIn chan outputArgs, outputReturn chan outputReturnArgs) reorg bool } @@ -99,7 +75,7 @@ func stutterAdvance(t *testing.T, s *state, src *testutils.FakeChainSource) { stutterL1(t, s, src) } -func stutterL2(t *testing.T, expectedWindow []testID, s *state, src *testutils.FakeChainSource, outputIn chan outputArgs, outputReturn chan outputReturnArgs) { +func stutterL2(t *testing.T, expectedWindow []TestID, s *state, src *testutils.FakeChainSource, outputIn chan outputArgs, outputReturn chan outputReturnArgs) { select { case <-outputIn: t.Error("Got a step when no step should have occurred (l1 only advance)") @@ -107,7 +83,7 @@ func stutterL2(t *testing.T, expectedWindow []testID, s *state, src *testutils.F } } -func advanceL2(t *testing.T, expectedWindow []testID, s *state, src *testutils.FakeChainSource, outputIn chan outputArgs, outputReturn chan outputReturnArgs) { +func advanceL2(t *testing.T, expectedWindow []TestID, s *state, src *testutils.FakeChainSource, outputIn chan outputArgs, outputReturn chan outputReturnArgs) { args := <-outputIn assert.Equal(t, int(s.Config.SeqWindowSize), len(args.l1Window), "Invalid L1 window size") assert.Equal(t, len(expectedWindow), len(args.l1Window), "L1 Window size does not match expectedWindow") @@ -117,7 +93,7 @@ func advanceL2(t *testing.T, expectedWindow []testID, s *state, src *testutils.F outputReturn <- outputReturnArgs{l2Head: src.SetL2Head(int(args.l2Head.Number) + 1), err: nil} } -func reorg__L2(t *testing.T, expectedWindow []testID, s *state, src *testutils.FakeChainSource, outputIn chan outputArgs, outputReturn chan outputReturnArgs) { +func reorg__L2(t *testing.T, expectedWindow []TestID, s *state, src *testutils.FakeChainSource, outputIn chan outputArgs, outputReturn chan outputReturnArgs) { args := <-outputIn assert.Equal(t, int(s.Config.SeqWindowSize), len(args.l1Window), "Invalid L1 window size") assert.Equal(t, len(expectedWindow), len(args.l1Window), "L1 Window size does not match expectedWindow") @@ -183,12 +159,12 @@ func TestDriver(t *testing.T) { genesis: testutils.FakeGenesis('a', 'A', 0), steps: []stateTestCaseStep{ {l1act: stutterL1, l2act: stutterL2, l1head: "a:0", l2head: "A:0"}, - {l1act: advanceL1, l2act: stutterL2, l1head: "b:1", l2head: "A:0", window: []testID{"a:0", "b:1"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "c:2", l2head: "B:1", window: []testID{"b:1", "c:2"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "d:3", l2head: "C:2", window: []testID{"c:2", "d:3"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "e:4", l2head: "D:3", window: []testID{"d:3", "e:4"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "f:5", l2head: "E:4", window: []testID{"e:4", "f:5"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "g:6", l2head: "F:5", window: []testID{"f:5", "g:6"}}, + {l1act: advanceL1, l2act: stutterL2, l1head: "b:1", l2head: "A:0", window: []TestID{"a:0", "b:1"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "c:2", l2head: "B:1", window: []TestID{"b:1", "c:2"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "d:3", l2head: "C:2", window: []TestID{"c:2", "d:3"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "e:4", l2head: "D:3", window: []TestID{"d:3", "e:4"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "f:5", l2head: "E:4", window: []TestID{"e:4", "f:5"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "g:6", l2head: "F:5", window: []TestID{"f:5", "g:6"}}, }, }, { @@ -199,17 +175,17 @@ func TestDriver(t *testing.T) { genesis: testutils.FakeGenesis('a', 'A', 0), steps: []stateTestCaseStep{ {l1act: stutterL1, l2act: stutterL2, l1head: "a:0", l2head: "A:0"}, - {l1act: advanceL1, l2act: stutterL2, l1head: "b:1", l2head: "A:0", window: []testID{"a:0", "b:1"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "c:2", l2head: "B:1", window: []testID{"b:1", "c:2"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "d:3", l2head: "C:2", window: []testID{"c:2", "d:3"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "e:4", l2head: "D:3", window: []testID{"d:3", "e:4"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "f:5", l2head: "E:4", window: []testID{"e:4", "f:5"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "g:6", l2head: "F:5", window: []testID{"f:5", "g:6"}}, - {l1act: stutterL1, l2act: reorg__L2, l1head: "z:6", l2head: "C:2", window: []testID{"c:2", "w:3"}, reorg: true}, - {l1act: stutterL1, l2act: advanceL2, l1head: "z:6", l2head: "W:3", window: []testID{"w:3", "x:4"}}, - {l1act: stutterL1, l2act: advanceL2, l1head: "z:6", l2head: "X:4", window: []testID{"x:4", "y:5"}}, - {l1act: stutterL1, l2act: advanceL2, l1head: "z:6", l2head: "Y:5", window: []testID{"y:5", "z:6"}}, - {l1act: stutterL1, l2act: stutterL2, l1head: "z:6", l2head: "Y:5", window: []testID{}}, + {l1act: advanceL1, l2act: stutterL2, l1head: "b:1", l2head: "A:0", window: []TestID{"a:0", "b:1"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "c:2", l2head: "B:1", window: []TestID{"b:1", "c:2"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "d:3", l2head: "C:2", window: []TestID{"c:2", "d:3"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "e:4", l2head: "D:3", window: []TestID{"d:3", "e:4"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "f:5", l2head: "E:4", window: []TestID{"e:4", "f:5"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "g:6", l2head: "F:5", window: []TestID{"f:5", "g:6"}}, + {l1act: stutterL1, l2act: reorg__L2, l1head: "z:6", l2head: "C:2", window: []TestID{"c:2", "w:3"}, reorg: true}, + {l1act: stutterL1, l2act: advanceL2, l1head: "z:6", l2head: "W:3", window: []TestID{"w:3", "x:4"}}, + {l1act: stutterL1, l2act: advanceL2, l1head: "z:6", l2head: "X:4", window: []TestID{"x:4", "y:5"}}, + {l1act: stutterL1, l2act: advanceL2, l1head: "z:6", l2head: "Y:5", window: []TestID{"y:5", "z:6"}}, + {l1act: stutterL1, l2act: stutterL2, l1head: "z:6", l2head: "Y:5", window: []TestID{}}, }, }, { @@ -220,12 +196,12 @@ func TestDriver(t *testing.T) { genesis: testutils.FakeGenesis('a', 'A', 0), steps: []stateTestCaseStep{ {l1act: stutterL1, l2act: stutterL2, l1head: "a:0", l2head: "A:0"}, - {l1act: advanceL1, l2act: stutterL2, l1head: "b:1", l2head: "A:0", window: []testID{"a:0", "b:1"}}, - {l1act: stutterAdvance, l2act: advanceL2, l1head: "c:2", l2head: "B:1", window: []testID{"b:1", "c:2"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "d:3", l2head: "C:2", window: []testID{"c:2", "d:3"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "e:4", l2head: "D:3", window: []testID{"d:3", "e:4"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "f:5", l2head: "E:4", window: []testID{"e:4", "f:5"}}, - {l1act: advanceL1, l2act: advanceL2, l1head: "g:6", l2head: "F:5", window: []testID{"f:5", "g:6"}}, + {l1act: advanceL1, l2act: stutterL2, l1head: "b:1", l2head: "A:0", window: []TestID{"a:0", "b:1"}}, + {l1act: stutterAdvance, l2act: advanceL2, l1head: "c:2", l2head: "B:1", window: []TestID{"b:1", "c:2"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "d:3", l2head: "C:2", window: []TestID{"c:2", "d:3"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "e:4", l2head: "D:3", window: []TestID{"d:3", "e:4"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "f:5", l2head: "E:4", window: []TestID{"e:4", "f:5"}}, + {l1act: advanceL1, l2act: advanceL2, l1head: "g:6", l2head: "F:5", window: []TestID{"f:5", "g:6"}}, }, }, } diff --git a/op-node/rollup/driver/step.go b/op-node/rollup/driver/step.go index 4a1de9c746c00..11dc6addc6215 100644 --- a/op-node/rollup/driver/step.go +++ b/op-node/rollup/driver/step.go @@ -9,7 +9,6 @@ import ( "time" "github.com/ethereum-optimism/optimism/op-node/eth" - "github.com/ethereum-optimism/optimism/op-node/l2" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" @@ -28,7 +27,7 @@ type outputImpl struct { // isDepositTx checks an opaqueTx to determine if it is a Deposit Trransaction // It has to return an error in the case the transaction is empty -func isDepositTx(opaqueTx l2.Data) (bool, error) { +func isDepositTx(opaqueTx eth.Data) (bool, error) { if len(opaqueTx) == 0 { return false, errors.New("empty transaction") } @@ -38,7 +37,7 @@ func isDepositTx(opaqueTx l2.Data) (bool, error) { // lastDeposit finds the index of last deposit at the start of the transactions. // It walks the transactions from the start until it finds a non-deposit tx. // An error is returned if any looked at transaction cannot be decoded -func lastDeposit(txns []l2.Data) (int, error) { +func lastDeposit(txns []eth.Data) (int, error) { var lastDeposit int for i, tx := range txns { deposit, err := isDepositTx(tx) @@ -54,13 +53,13 @@ func lastDeposit(txns []l2.Data) (int, error) { return lastDeposit, nil } -func (d *outputImpl) processBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, payload *l2.ExecutionPayload) error { +func (d *outputImpl) processBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, payload *eth.ExecutionPayload) error { d.log.Info("processing new block", "parent", payload.ParentID(), "l2Head", l2Head, "id", payload.ID()) if err := d.l2.NewPayload(ctx, payload); err != nil { return fmt.Errorf("failed to insert new payload: %v", err) } // now try to persist a reorg to the new payload - fc := l2.ForkchoiceState{ + fc := eth.ForkchoiceState{ HeadBlockHash: payload.BlockHash, SafeBlockHash: l2SafeHead.Hash, FinalizedBlockHash: l2Finalized.Hash, @@ -69,13 +68,13 @@ func (d *outputImpl) processBlock(ctx context.Context, l2Head eth.L2BlockRef, l2 if err != nil { return fmt.Errorf("failed to update forkchoice to point to new payload: %v", err) } - if res.PayloadStatus.Status != l2.ExecutionValid { + if res.PayloadStatus.Status != eth.ExecutionValid { return fmt.Errorf("failed to persist forkchoice update: %v", err) } return nil } -func (d *outputImpl) createNewBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, l1Origin eth.L1BlockRef) (eth.L2BlockRef, *l2.ExecutionPayload, error) { +func (d *outputImpl) createNewBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, l1Origin eth.L1BlockRef) (eth.L2BlockRef, *eth.ExecutionPayload, error) { d.log.Info("creating new block", "parent", l2Head, "l1Origin", l1Origin) fetchCtx, cancel := context.WithTimeout(ctx, time.Second*20) @@ -101,7 +100,7 @@ func (d *outputImpl) createNewBlock(ctx context.Context, l2Head eth.L2BlockRef, } // Start building the list of transactions to include in the new block. - var txns []l2.Data + var txns []eth.Data // First transaction in every block is always the L1 info transaction. l1InfoTx, err := derive.L1InfoDepositBytes(seqNumber, l1Info) @@ -128,9 +127,9 @@ func (d *outputImpl) createNewBlock(ctx context.Context, l2Head eth.L2BlockRef, shouldProduceEmptyBlock := nextL2Time >= l1Origin.Time+d.Config.MaxSequencerDrift // Put together our payload attributes. - attrs := &l2.PayloadAttributes{ + attrs := ð.PayloadAttributes{ Timestamp: hexutil.Uint64(nextL2Time), - PrevRandao: l2.Bytes32(l1Info.MixDigest()), + PrevRandao: eth.Bytes32(l1Info.MixDigest()), SuggestedFeeRecipient: d.Config.FeeRecipientAddress, Transactions: txns, NoTxPool: shouldProduceEmptyBlock, @@ -138,7 +137,7 @@ func (d *outputImpl) createNewBlock(ctx context.Context, l2Head eth.L2BlockRef, // And construct our fork choice state. This is our current fork choice state and will be // updated as a result of executing the block based on the attributes described above. - fc := l2.ForkchoiceState{ + fc := eth.ForkchoiceState{ HeadBlockHash: l2Head.Hash, SafeBlockHash: l2SafeHead.Hash, FinalizedBlockHash: l2Finalized.Hash, @@ -151,7 +150,7 @@ func (d *outputImpl) createNewBlock(ctx context.Context, l2Head eth.L2BlockRef, } // Generate an L2 block ref from the payload. - ref, err := l2.PayloadToBlockRef(payload, &d.Config.Genesis) + ref, err := derive.PayloadToBlockRef(payload, &d.Config.Genesis) return ref, payload, err } @@ -215,7 +214,7 @@ func (d *outputImpl) insertEpoch(ctx context.Context, l2Head eth.L2BlockRef, l2S batches = derive.FilterBatches(&d.Config, epoch, minL2Time, maxL2Time, batches) batches = derive.FillMissingBatches(batches, uint64(epoch), d.Config.BlockTime, minL2Time, nextL1Block.Time()) - fc := l2.ForkchoiceState{ + fc := eth.ForkchoiceState{ HeadBlockHash: l2Head.Hash, SafeBlockHash: l2SafeHead.Hash, FinalizedBlockHash: l2Finalized.Hash, @@ -224,10 +223,10 @@ func (d *outputImpl) insertEpoch(ctx context.Context, l2Head eth.L2BlockRef, l2S lastHead := l2Head lastSafeHead := l2SafeHead didReorg := false - var payload *l2.ExecutionPayload + var payload *eth.ExecutionPayload var reorg bool for i, batch := range batches { - var txns []l2.Data + var txns []eth.Data l1InfoTx, err := derive.L1InfoDepositBytes(uint64(i), l1Info) if err != nil { return l2Head, l2SafeHead, false, fmt.Errorf("failed to create l1InfoTx: %w", err) @@ -237,9 +236,9 @@ func (d *outputImpl) insertEpoch(ctx context.Context, l2Head eth.L2BlockRef, l2S txns = append(txns, deposits...) } txns = append(txns, batch.Transactions...) - attrs := &l2.PayloadAttributes{ + attrs := ð.PayloadAttributes{ Timestamp: hexutil.Uint64(batch.Timestamp), - PrevRandao: l2.Bytes32(l1Info.MixDigest()), + PrevRandao: eth.Bytes32(l1Info.MixDigest()), SuggestedFeeRecipient: d.Config.FeeRecipientAddress, Transactions: txns, // we are verifying, not sequencing, we've got all transactions and do not pull from the tx-pool @@ -264,7 +263,7 @@ func (d *outputImpl) insertEpoch(ctx context.Context, l2Head eth.L2BlockRef, l2S return lastHead, lastSafeHead, didReorg, fmt.Errorf("failed to extend L2 chain at block %d/%d of epoch %d: %w", i, len(batches), epoch, err) } - newLast, err := l2.PayloadToBlockRef(payload, &d.Config.Genesis) + newLast, err := derive.PayloadToBlockRef(payload, &d.Config.Genesis) if err != nil { return lastHead, lastSafeHead, didReorg, fmt.Errorf("failed to derive block references: %w", err) } @@ -286,7 +285,7 @@ func (d *outputImpl) insertEpoch(ctx context.Context, l2Head eth.L2BlockRef, l2S // attributesMatchBlock checks if the L2 attributes pre-inputs match the output // nil if it is a match. If err is not nil, the error contains the reason for the mismatch -func attributesMatchBlock(attrs *l2.PayloadAttributes, parentHash common.Hash, block *l2.ExecutionPayload) error { +func attributesMatchBlock(attrs *eth.PayloadAttributes, parentHash common.Hash, block *eth.ExecutionPayload) error { if parentHash != block.ParentHash { return fmt.Errorf("parent hash field does not match. expected: %v. got: %v", parentHash, block.ParentHash) } @@ -309,12 +308,12 @@ func attributesMatchBlock(attrs *l2.PayloadAttributes, parentHash common.Hash, b // verifySafeBlock reconciles the supplied payload attributes against the actual L2 block. // If they do not match, it inserts the new block and sets the head and safe head to the new block in the FC. -func (d *outputImpl) verifySafeBlock(ctx context.Context, fc l2.ForkchoiceState, attrs *l2.PayloadAttributes, parent eth.BlockID) (*l2.ExecutionPayload, bool, error) { +func (d *outputImpl) verifySafeBlock(ctx context.Context, fc eth.ForkchoiceState, attrs *eth.PayloadAttributes, parent eth.BlockID) (*eth.ExecutionPayload, bool, error) { payload, err := d.l2.PayloadByNumber(ctx, new(big.Int).SetUint64(parent.Number+1)) if err != nil { return nil, false, fmt.Errorf("failed to get L2 block: %w", err) } - ref, err := l2.PayloadToBlockRef(payload, &d.Config.Genesis) + ref, err := derive.PayloadToBlockRef(payload, &d.Config.Genesis) if err != nil { return nil, false, fmt.Errorf("failed to parse block ref: %w", err) } @@ -343,12 +342,12 @@ func (d *outputImpl) verifySafeBlock(ctx context.Context, fc l2.ForkchoiceState, // sets the FC to the same safe and finalized hashes, but updates the head hash to the new block. // If updateSafe is true, the head block is considered to be the safe head as well as the head. // It returns the payload, the count of deposits, and an error. -func (d *outputImpl) insertHeadBlock(ctx context.Context, fc l2.ForkchoiceState, attrs *l2.PayloadAttributes, updateSafe bool) (*l2.ExecutionPayload, error) { +func (d *outputImpl) insertHeadBlock(ctx context.Context, fc eth.ForkchoiceState, attrs *eth.PayloadAttributes, updateSafe bool) (*eth.ExecutionPayload, error) { fcRes, err := d.l2.ForkchoiceUpdate(ctx, &fc, attrs) if err != nil { return nil, fmt.Errorf("failed to create new block via forkchoice: %w", err) } - if fcRes.PayloadStatus.Status != l2.ExecutionValid { + if fcRes.PayloadStatus.Status != eth.ExecutionValid { return nil, fmt.Errorf("engine not ready, forkchoice pre-state is not valid: %s", fcRes.PayloadStatus.Status) } id := fcRes.PayloadID @@ -403,7 +402,7 @@ func (d *outputImpl) insertHeadBlock(ctx context.Context, fc l2.ForkchoiceState, if err != nil { return nil, fmt.Errorf("failed to make the new L2 block canonical via forkchoice: %w", err) } - if fcRes.PayloadStatus.Status != l2.ExecutionValid { + if fcRes.PayloadStatus.Status != eth.ExecutionValid { return nil, fmt.Errorf("failed to persist forkchoice change: %s", fcRes.PayloadStatus.Status) } return payload, nil diff --git a/op-node/testutils/block_id.go b/op-node/testutils/block_id.go new file mode 100644 index 0000000000000..ad9b7b09fe353 --- /dev/null +++ b/op-node/testutils/block_id.go @@ -0,0 +1,37 @@ +package testutils + +import ( + "strconv" + "strings" + + "github.com/ethereum-optimism/optimism/op-node/eth" + "github.com/ethereum/go-ethereum/common" +) + +// TestID represents an eth.BlockID as string, and can be shortened for convenience in test definitions. +// +// Format: : where the are +// copied over (i.e. not hex) and is in decimal. +// +// Examples: "foobar:123", or "B:2" +type TestID string + +func (id TestID) ID() eth.BlockID { + parts := strings.Split(string(id), ":") + if len(parts) != 2 { + panic("bad id") + } + if len(parts[0]) > 32 { + panic("test ID hash too long") + } + var h common.Hash + copy(h[:], parts[0]) + v, err := strconv.ParseUint(parts[1], 0, 64) + if err != nil { + panic(err) + } + return eth.BlockID{ + Hash: h, + Number: v, + } +} diff --git a/op-node/testutils/deposits.go b/op-node/testutils/deposits.go new file mode 100644 index 0000000000000..ae24177cfce6d --- /dev/null +++ b/op-node/testutils/deposits.go @@ -0,0 +1,54 @@ +package testutils + +import ( + "math/big" + "math/rand" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// Returns a DepositEvent customized on the basis of the id parameter. +func GenerateDeposit(sourceHash common.Hash, rng *rand.Rand) *types.DepositTx { + dataLen := rng.Int63n(10_000) + data := make([]byte, dataLen) + rng.Read(data) + + var to *common.Address + if rng.Intn(2) == 0 { + x := RandomAddress(rng) + to = &x + } + var mint *big.Int + if rng.Intn(2) == 0 { + mint = RandomETH(rng, 200) + } + + dep := &types.DepositTx{ + SourceHash: sourceHash, + From: RandomAddress(rng), + To: to, + Value: RandomETH(rng, 200), + Gas: uint64(rng.Int63n(10 * 1e6)), // 10 M gas max + Data: data, + Mint: mint, + } + return dep +} + +// Generates an EVM log entry with the given topics and data. +func GenerateLog(addr common.Address, topics []common.Hash, data []byte) *types.Log { + return &types.Log{ + Address: addr, + Topics: topics, + Data: data, + Removed: false, + + // ignored (zeroed): + BlockNumber: 0, + TxHash: common.Hash{}, + TxIndex: 0, + BlockHash: common.Hash{}, + Index: 0, + } +} diff --git a/op-node/testutils/fake_chain.go b/op-node/testutils/fake_chain.go index ec1c601343fe1..6bb454c8ae252 100644 --- a/op-node/testutils/fake_chain.go +++ b/op-node/testutils/fake_chain.go @@ -11,7 +11,6 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum-optimism/optimism/op-node/eth" - "github.com/ethereum-optimism/optimism/op-node/l2" "github.com/ethereum-optimism/optimism/op-node/rollup" ) @@ -167,7 +166,7 @@ func (m *FakeChainSource) L2BlockRefByHash(ctx context.Context, l2Hash common.Ha return eth.L2BlockRef{}, ethereum.NotFound } -func (m *FakeChainSource) ForkchoiceUpdate(ctx context.Context, state *l2.ForkchoiceState, attr *l2.PayloadAttributes) (*l2.ForkchoiceUpdatedResult, error) { +func (m *FakeChainSource) ForkchoiceUpdate(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) { m.log.Trace("ForkchoiceUpdate", "newHead", state.HeadBlockHash, "l2Head", m.l2head, "reorg", m.l2reorg) m.l2reorg++ if m.l2reorg >= len(m.l2s) { diff --git a/op-node/testutils/l1info.go b/op-node/testutils/l1info.go new file mode 100644 index 0000000000000..e14d9d4ad4026 --- /dev/null +++ b/op-node/testutils/l1info.go @@ -0,0 +1,96 @@ +package testutils + +import ( + "math/big" + "math/rand" + + "github.com/ethereum-optimism/optimism/op-node/eth" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +type MockL1Info struct { + // Prefixed all fields with "Info" to avoid collisions with the interface method names. + + InfoHash common.Hash + InfoParentHash common.Hash + InfoRoot common.Hash + InfoNum uint64 + InfoTime uint64 + InfoMixDigest [32]byte + InfoBaseFee *big.Int + InfoReceiptRoot common.Hash + InfoSequenceNumber uint64 +} + +func (l *MockL1Info) Hash() common.Hash { + return l.InfoHash +} + +func (l *MockL1Info) ParentHash() common.Hash { + return l.InfoParentHash +} + +func (l *MockL1Info) Root() common.Hash { + return l.InfoRoot +} + +func (l *MockL1Info) NumberU64() uint64 { + return l.InfoNum +} + +func (l *MockL1Info) Time() uint64 { + return l.InfoTime +} + +func (l *MockL1Info) MixDigest() common.Hash { + return l.InfoMixDigest +} + +func (l *MockL1Info) BaseFee() *big.Int { + return l.InfoBaseFee +} + +func (l *MockL1Info) ReceiptHash() common.Hash { + return l.InfoReceiptRoot +} + +func (l *MockL1Info) ID() eth.BlockID { + return eth.BlockID{Hash: l.InfoHash, Number: l.InfoNum} +} + +func (l *MockL1Info) BlockRef() eth.L1BlockRef { + return eth.L1BlockRef{ + Hash: l.InfoHash, + Number: l.InfoNum, + ParentHash: l.InfoParentHash, + Time: l.InfoTime, + } +} + +func (l *MockL1Info) SequenceNumber() uint64 { + return l.InfoSequenceNumber +} + +func RandomL1Info(rng *rand.Rand) *MockL1Info { + return &MockL1Info{ + InfoParentHash: RandomHash(rng), + InfoNum: rng.Uint64(), + InfoTime: rng.Uint64(), + InfoHash: RandomHash(rng), + InfoBaseFee: big.NewInt(rng.Int63n(1000_000 * 1e9)), // a million GWEI + InfoReceiptRoot: types.EmptyRootHash, + InfoRoot: RandomHash(rng), + InfoSequenceNumber: rng.Uint64(), + } +} + +func MakeL1Info(fn func(l *MockL1Info)) func(rng *rand.Rand) *MockL1Info { + return func(rng *rand.Rand) *MockL1Info { + l := RandomL1Info(rng) + if fn != nil { + fn(l) + } + return l + } +} diff --git a/op-node/testutils/random.go b/op-node/testutils/random.go new file mode 100644 index 0000000000000..9f4bdd9a446ac --- /dev/null +++ b/op-node/testutils/random.go @@ -0,0 +1,24 @@ +package testutils + +import ( + "math/big" + "math/rand" + + "github.com/ethereum/go-ethereum/common" +) + +func RandomHash(rng *rand.Rand) (out common.Hash) { + rng.Read(out[:]) + return +} + +func RandomAddress(rng *rand.Rand) (out common.Address) { + rng.Read(out[:]) + return +} + +func RandomETH(rng *rand.Rand, max int64) *big.Int { + x := big.NewInt(rng.Int63n(max)) + x = new(big.Int).Mul(x, big.NewInt(1e18)) + return x +} diff --git a/op-proposer/drivers/l2output/driver.go b/op-proposer/drivers/l2output/driver.go index bb33b32206913..52aa81ee80186 100644 --- a/op-proposer/drivers/l2output/driver.go +++ b/op-proposer/drivers/l2output/driver.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/ethereum-optimism/optimism/op-bindings/bindings" - "github.com/ethereum-optimism/optimism/op-node/l2" + "github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-proposer/rollupclient" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -20,7 +20,7 @@ import ( ) var bigOne = big.NewInt(1) -var supportedL2OutputVersion = l2.Bytes32{} +var supportedL2OutputVersion = eth.Bytes32{} type Config struct { Log log.Logger @@ -261,16 +261,16 @@ func (d *Driver) SendTransaction( return d.cfg.L1Client.SendTransaction(ctx, tx) } -func (d *Driver) outputRootAtBlock(ctx context.Context, blockNum *big.Int) (l2.Bytes32, error) { +func (d *Driver) outputRootAtBlock(ctx context.Context, blockNum *big.Int) (eth.Bytes32, error) { output, err := d.cfg.RollupClient.OutputAtBlock(ctx, blockNum) if err != nil { - return l2.Bytes32{}, err + return eth.Bytes32{}, err } if len(output) != 2 { - return l2.Bytes32{}, fmt.Errorf("invalid outputAtBlock response") + return eth.Bytes32{}, fmt.Errorf("invalid outputAtBlock response") } if version := output[0]; version != supportedL2OutputVersion { - return l2.Bytes32{}, fmt.Errorf("unsupported l2 output version") + return eth.Bytes32{}, fmt.Errorf("unsupported l2 output version") } return output[1], nil } diff --git a/op-proposer/rollupclient/rollupclient.go b/op-proposer/rollupclient/rollupclient.go index 3607bf66e1819..1cc8a5419619c 100644 --- a/op-proposer/rollupclient/rollupclient.go +++ b/op-proposer/rollupclient/rollupclient.go @@ -4,7 +4,7 @@ import ( "context" "math/big" - "github.com/ethereum-optimism/optimism/op-node/l2" + "github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/node" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rpc" @@ -28,8 +28,8 @@ func (r *RollupClient) GetBatchBundle( return batchResponse, err } -func (r *RollupClient) OutputAtBlock(ctx context.Context, blockNum *big.Int) ([]l2.Bytes32, error) { - var output []l2.Bytes32 +func (r *RollupClient) OutputAtBlock(ctx context.Context, blockNum *big.Int) ([]eth.Bytes32, error) { + var output []eth.Bytes32 err := r.rpc.CallContext(ctx, &output, "optimism_outputAtBlock", hexutil.EncodeBig(blockNum)) return output, err }