diff --git a/.changeset/empty-boxes-repair.md b/.changeset/empty-boxes-repair.md new file mode 100644 index 00000000000..07e708d4030 --- /dev/null +++ b/.changeset/empty-boxes-repair.md @@ -0,0 +1,5 @@ +--- +"@eth-optimism/l2geth": patch +--- + +Add batch API to rollup client diff --git a/l2geth/rollup/client.go b/l2geth/rollup/client.go index d890d8e6782..0e55550e46b 100644 --- a/l2geth/rollup/client.go +++ b/l2geth/rollup/client.go @@ -13,12 +13,21 @@ import ( "github.com/go-resty/resty/v2" ) -/** - * GET /enqueue/index/{index} - * GET /transaction/index/{index} - * GET /eth/context/latest - */ +// Constants that are used to compare against values in the deserialized JSON +// fetched by the RollupClient +const ( + sequencer = "sequencer" + l1 = "l1" + EIP155 = "EIP155" + ETH_SIGN = "ETH_SIGN" +) + +// errElementNotFound represents the error case of the remote element not being +// found. It applies to transactions, queue elements and batches +var errElementNotFound = errors.New("element not found") +// Batch represents the data structure that is submitted with +// a series of transactions to layer one type Batch struct { Index uint64 `json:"index"` Root common.Hash `json:"root,omitempty"` @@ -30,22 +39,32 @@ type Batch struct { Submitter common.Address `json:"submitter"` } +// EthContext represents the L1 EVM context that is injected into +// the OVM at runtime. It is updated with each `enqueue` transaction +// and needs to be fetched from a remote server to be updated when +// too much time has passed between `enqueue` transactions. type EthContext struct { BlockNumber uint64 `json:"blockNumber"` BlockHash common.Hash `json:"blockHash"` Timestamp uint64 `json:"timestamp"` } +// SyncStatus represents the state of the remote server. The SyncService +// does not want to begin syncing until the remote server has fully synced. type SyncStatus struct { Syncing bool `json:"syncing"` HighestKnownTransactionIndex uint64 `json:"highestKnownTransactionIndex"` CurrentTransactionIndex uint64 `json:"currentTransactionIndex"` } +// L1GasPrice represents the gas price of L1. It is used as part of the gas +// estimatation logic. type L1GasPrice struct { GasPrice string `json:"gasPrice"` } +// transaction represents the return result of the remote server. +// It either came from a batch or was replicated from the sequencer. type transaction struct { Index uint64 `json:"index"` BatchIndex uint64 `json:"batchIndex"` @@ -61,6 +80,7 @@ type transaction struct { Decoded *decoded `json:"decoded"` } +// Enqueue represents an `enqueue` transaction or a L1 to L2 transaction. type Enqueue struct { Index *uint64 `json:"ctcIndex"` Target *common.Address `json:"target"` @@ -72,12 +92,16 @@ type Enqueue struct { QueueIndex *uint64 `json:"index"` } +// signature represents a secp256k1 ECDSA signature type signature struct { R hexutil.Bytes `json:"r"` S hexutil.Bytes `json:"s"` V uint `json:"v"` } +// decoded represents the decoded transaction from the batch. +// When this struct exists in other structs and is set to `nil`, +// it means that the decoding failed. type decoded struct { Signature signature `json:"sig"` GasLimit uint64 `json:"gasLimit"` @@ -87,31 +111,47 @@ type decoded struct { Data hexutil.Bytes `json:"data"` } +// RollupClient is able to query for information +// that is required by the SyncService type RollupClient interface { GetEnqueue(index uint64) (*types.Transaction, error) GetLatestEnqueue() (*types.Transaction, error) - GetTransaction(index uint64) (*types.Transaction, error) + GetTransaction(uint64) (*types.Transaction, error) GetLatestTransaction() (*types.Transaction, error) - GetEthContext(index uint64) (*EthContext, error) + GetEthContext(uint64) (*EthContext, error) GetLatestEthContext() (*EthContext, error) GetLastConfirmedEnqueue() (*types.Transaction, error) + GetLatestTransactionBatch() (*Batch, []*types.Transaction, error) + GetTransactionBatch(uint64) (*Batch, []*types.Transaction, error) SyncStatus() (*SyncStatus, error) GetL1GasPrice() (*big.Int, error) } +// Client is an HTTP based RollupClient type Client struct { client *resty.Client signer *types.OVMSigner } +// TransactionResponse represents the response from the remote server when +// querying transactions. type TransactionResponse struct { Transaction *transaction `json:"transaction"` Batch *Batch `json:"batch"` } +// TransactionBatchResponse represents the response from the remote server +// when querying batches. +type TransactionBatchResponse struct { + Batch *Batch `json:"batch"` + Transactions []*transaction `json:"transactions"` +} + +// NewClient create a new Client given a remote HTTP url and a chain id func NewClient(url string, chainID *big.Int) *Client { client := resty.New() client.SetHostURL(url) + client.SetHeader("User-Agent", "sequencer") signer := types.NewOVMSigner(chainID) return &Client{ @@ -120,7 +160,7 @@ func NewClient(url string, chainID *big.Int) *Client { } } -// This needs to return a transaction instead +// GetEnqueue fetches an `enqueue` transaction by queue index func (c *Client) GetEnqueue(index uint64) (*types.Transaction, error) { str := strconv.FormatUint(index, 10) response, err := c.client.R(). @@ -131,7 +171,7 @@ func (c *Client) GetEnqueue(index uint64) (*types.Transaction, error) { Get("/enqueue/index/{index}") if err != nil { - return nil, err + return nil, fmt.Errorf("cannot fetch enqueue: %w", err) } enqueue, ok := response.Result().(*Enqueue) if !ok { @@ -142,16 +182,21 @@ func (c *Client) GetEnqueue(index uint64) (*types.Transaction, error) { } tx, err := enqueueToTransaction(enqueue) if err != nil { - return nil, fmt.Errorf("Cannot parse enqueue tx :%w", err) + return nil, err } return tx, nil } +// enqueueToTransaction turns an Enqueue into a types.Transaction +// so that it can be consumed by the SyncService func enqueueToTransaction(enqueue *Enqueue) (*types.Transaction, error) { + if enqueue == nil { + return nil, errElementNotFound + } // When the queue index is nil, is means that the enqueue'd transaction // does not exist. if enqueue.QueueIndex == nil { - return nil, nil + return nil, errElementNotFound } // The queue index is the nonce nonce := *enqueue.QueueIndex @@ -203,13 +248,15 @@ func enqueueToTransaction(enqueue *Enqueue) (*types.Transaction, error) { return tx, nil } +// GetLatestEnqueue fetches the latest `enqueue`, meaning the `enqueue` +// transaction with the greatest queue index. func (c *Client) GetLatestEnqueue() (*types.Transaction, error) { response, err := c.client.R(). SetResult(&Enqueue{}). Get("/enqueue/latest") if err != nil { - return nil, err + return nil, fmt.Errorf("cannot fetch latest enqueue: %w", err) } enqueue, ok := response.Result().(*Enqueue) if !ok { @@ -217,50 +264,52 @@ func (c *Client) GetLatestEnqueue() (*types.Transaction, error) { } tx, err := enqueueToTransaction(enqueue) if err != nil { - return nil, fmt.Errorf("Cannot parse enqueue tx :%w", err) + return nil, fmt.Errorf("Cannot parse enqueue tx: %w", err) } return tx, nil } -func transactionResponseToTransaction(res *TransactionResponse, signer *types.OVMSigner) (*types.Transaction, error) { +// batchedTransactionToTransaction converts a transaction into a +// types.Transaction that can be consumed by the SyncService +func batchedTransactionToTransaction(res *transaction, signer *types.OVMSigner) (*types.Transaction, error) { // `nil` transactions are not found - if res.Transaction == nil { - return nil, nil + if res == nil { + return nil, errElementNotFound } // The queue origin must be either sequencer of l1, otherwise // it is considered an unknown queue origin and will not be processed var queueOrigin types.QueueOrigin - if res.Transaction.QueueOrigin == "sequencer" { + if res.QueueOrigin == sequencer { queueOrigin = types.QueueOriginSequencer - } else if res.Transaction.QueueOrigin == "l1" { + } else if res.QueueOrigin == l1 { queueOrigin = types.QueueOriginL1ToL2 } else { - return nil, fmt.Errorf("Unknown queue origin: %s", res.Transaction.QueueOrigin) + return nil, fmt.Errorf("Unknown queue origin: %s", res.QueueOrigin) } // The transaction type must be EIP155 or EthSign. Throughout this // codebase, it is referred to as "sighash type" but it could actually // be generalized to transaction type. Right now the only different // types use a different signature hashing scheme. var sighashType types.SignatureHashType - if res.Transaction.Type == "EIP155" { + if res.Type == EIP155 { sighashType = types.SighashEIP155 - } else if res.Transaction.Type == "ETH_SIGN" { + } else if res.Type == ETH_SIGN { sighashType = types.SighashEthSign } else { - return nil, fmt.Errorf("Unknown transaction type: %s", res.Transaction.Type) + return nil, fmt.Errorf("Unknown transaction type: %s", res.Type) } // Transactions that have been decoded are // Queue Origin Sequencer transactions - if res.Transaction.Decoded != nil { - nonce := res.Transaction.Decoded.Nonce - to := res.Transaction.Decoded.Target + if res.Decoded != nil { + nonce := res.Decoded.Nonce + to := res.Decoded.Target value := new(big.Int) // Note: there are two gas limits, one top level and // another on the raw transaction itself. Maybe maxGasLimit // for the top level? - gasLimit := res.Transaction.Decoded.GasLimit - gasPrice := new(big.Int).SetUint64(res.Transaction.Decoded.GasPrice) - data := res.Transaction.Decoded.Data + gasLimit := res.Decoded.GasLimit + gasPrice := new(big.Int).SetUint64(res.Decoded.GasPrice) + data := res.Decoded.Data var tx *types.Transaction if to == (common.Address{}) { @@ -270,22 +319,22 @@ func transactionResponseToTransaction(res *TransactionResponse, signer *types.OV } txMeta := types.NewTransactionMeta( - new(big.Int).SetUint64(res.Transaction.BlockNumber), - res.Transaction.Timestamp, - res.Transaction.Origin, + new(big.Int).SetUint64(res.BlockNumber), + res.Timestamp, + res.Origin, sighashType, queueOrigin, - &res.Transaction.Index, - res.Transaction.QueueIndex, - res.Transaction.Data, + &res.Index, + res.QueueIndex, + res.Data, ) tx.SetTransactionMeta(txMeta) - r, s := res.Transaction.Decoded.Signature.R, res.Transaction.Decoded.Signature.S + r, s := res.Decoded.Signature.R, res.Decoded.Signature.S sig := make([]byte, crypto.SignatureLength) copy(sig[32-len(r):32], r) copy(sig[64-len(s):64], s) - sig[64] = byte(res.Transaction.Decoded.Signature.V) + sig[64] = byte(res.Decoded.Signature.V) tx, err := tx.WithSignature(signer, sig[:]) if err != nil { @@ -298,31 +347,32 @@ func transactionResponseToTransaction(res *TransactionResponse, signer *types.OV // The transaction is either an L1 to L2 transaction or it does not have a // known deserialization nonce := uint64(0) - if res.Transaction.QueueOrigin == "l1" { - if res.Transaction.QueueIndex == nil { + if res.QueueOrigin == l1 { + if res.QueueIndex == nil { return nil, errors.New("Queue origin L1 to L2 without a queue index") } - nonce = *res.Transaction.QueueIndex + nonce = *res.QueueIndex } - target := res.Transaction.Target - gasLimit := res.Transaction.GasLimit - data := res.Transaction.Data - origin := res.Transaction.Origin + target := res.Target + gasLimit := res.GasLimit + data := res.Data + origin := res.Origin tx := types.NewTransaction(nonce, target, big.NewInt(0), gasLimit, big.NewInt(0), data) txMeta := types.NewTransactionMeta( - new(big.Int).SetUint64(res.Transaction.BlockNumber), - res.Transaction.Timestamp, + new(big.Int).SetUint64(res.BlockNumber), + res.Timestamp, origin, sighashType, queueOrigin, - &res.Transaction.Index, - res.Transaction.QueueIndex, - res.Transaction.Data, + &res.Index, + res.QueueIndex, + res.Data, ) tx.SetTransactionMeta(txMeta) return tx, nil } +// GetTransaction will get a transaction by Canonical Transaction Chain index func (c *Client) GetTransaction(index uint64) (*types.Transaction, error) { str := strconv.FormatUint(index, 10) response, err := c.client.R(). @@ -333,32 +383,34 @@ func (c *Client) GetTransaction(index uint64) (*types.Transaction, error) { Get("/transaction/index/{index}") if err != nil { - return nil, err + return nil, fmt.Errorf("cannot fetch transaction: %w", err) } res, ok := response.Result().(*TransactionResponse) if !ok { return nil, fmt.Errorf("could not get tx with index %d", index) } - - return transactionResponseToTransaction(res, c.signer) + return batchedTransactionToTransaction(res.Transaction, c.signer) } +// GetLatestTransaction will get the latest transaction, meaning the transaction +// with the greatest Canonical Transaction Chain index func (c *Client) GetLatestTransaction() (*types.Transaction, error) { response, err := c.client.R(). SetResult(&TransactionResponse{}). Get("/transaction/latest") if err != nil { - return nil, err + return nil, fmt.Errorf("cannot fetch latest transactions: %w", err) } res, ok := response.Result().(*TransactionResponse) if !ok { - return nil, errors.New("") + return nil, errors.New("Cannot get latest transaction") } - return transactionResponseToTransaction(res, c.signer) + return batchedTransactionToTransaction(res.Transaction, c.signer) } +// GetEthContext will return the EthContext by block number func (c *Client) GetEthContext(blockNumber uint64) (*EthContext, error) { str := strconv.FormatUint(blockNumber, 10) response, err := c.client.R(). @@ -376,10 +428,10 @@ func (c *Client) GetEthContext(blockNumber uint64) (*EthContext, error) { if !ok { return nil, errors.New("Cannot parse EthContext") } - return context, nil } +// GetLatestEthContext will return the latest EthContext func (c *Client) GetLatestEthContext() (*EthContext, error) { response, err := c.client.R(). SetResult(&EthContext{}). @@ -397,6 +449,8 @@ func (c *Client) GetLatestEthContext() (*EthContext, error) { return context, nil } +// GetLastConfirmedEnqueue will get the last `enqueue` transaction that has been +// batched up func (c *Client) GetLastConfirmedEnqueue() (*types.Transaction, error) { enqueue, err := c.GetLatestEnqueue() if err != nil { @@ -428,6 +482,7 @@ func (c *Client) GetLastConfirmedEnqueue() (*types.Transaction, error) { } } +// SyncStatus will query the remote server to determine if it is still syncing func (c *Client) SyncStatus() (*SyncStatus, error) { response, err := c.client.R(). SetResult(&SyncStatus{}). @@ -445,6 +500,61 @@ func (c *Client) SyncStatus() (*SyncStatus, error) { return status, nil } +// GetLatestTransactionBatch will return the latest transaction batch +func (c *Client) GetLatestTransactionBatch() (*Batch, []*types.Transaction, error) { + response, err := c.client.R(). + SetResult(&TransactionBatchResponse{}). + Get("/batch/transaction/latest") + + if err != nil { + return nil, nil, errors.New("Cannot get latest transaction batch") + } + txBatch, ok := response.Result().(*TransactionBatchResponse) + if !ok { + return nil, nil, fmt.Errorf("Cannot parse transaction batch response") + } + return parseTransactionBatchResponse(txBatch, c.signer) +} + +// GetTransactionBatch will return the transaction batch by batch index +func (c *Client) GetTransactionBatch(index uint64) (*Batch, []*types.Transaction, error) { + str := strconv.FormatUint(index, 10) + response, err := c.client.R(). + SetResult(&TransactionBatchResponse{}). + SetPathParams(map[string]string{ + "index": str, + }). + Get("/batch/transaction/index/{index}") + + if err != nil { + return nil, nil, fmt.Errorf("Cannot get transaction batch %d", index) + } + txBatch, ok := response.Result().(*TransactionBatchResponse) + if !ok { + return nil, nil, fmt.Errorf("Cannot parse transaction batch response") + } + return parseTransactionBatchResponse(txBatch, c.signer) +} + +// parseTransactionBatchResponse will turn a TransactionBatchResponse into a +// Batch and its corresponding types.Transactions +func parseTransactionBatchResponse(txBatch *TransactionBatchResponse, signer *types.OVMSigner) (*Batch, []*types.Transaction, error) { + if txBatch == nil { + return nil, nil, nil + } + batch := txBatch.Batch + txs := make([]*types.Transaction, len(txBatch.Transactions)) + for i, tx := range txBatch.Transactions { + transaction, err := batchedTransactionToTransaction(tx, signer) + if err != nil { + return nil, nil, fmt.Errorf("Cannot parse transaction batch: %w", err) + } + txs[i] = transaction + } + return batch, txs, nil +} + +// GetL1GasPrice will return the current gas price on L1 func (c *Client) GetL1GasPrice() (*big.Int, error) { response, err := c.client.R(). SetResult(&L1GasPrice{}). diff --git a/l2geth/rollup/sync_service.go b/l2geth/rollup/sync_service.go index 4a8864e5bc5..3601b75e737 100644 --- a/l2geth/rollup/sync_service.go +++ b/l2geth/rollup/sync_service.go @@ -249,13 +249,15 @@ func (s *SyncService) initializeLatestL1(ctcDeployHeight *big.Int) error { queueIndex := s.GetLatestEnqueueIndex() if queueIndex == nil { enqueue, err := s.client.GetLastConfirmedEnqueue() - if err != nil { - return fmt.Errorf("Cannot fetch last confirmed queue tx: %w", err) - } // There are no enqueues yet - if enqueue == nil { + if errors.Is(err, errElementNotFound) { return nil } + // Other unexpected error + if err != nil { + return fmt.Errorf("Cannot fetch last confirmed queue tx: %w", err) + } + // No error, the queue element was found queueIndex = enqueue.GetMeta().QueueIndex } s.SetLatestEnqueueIndex(queueIndex) @@ -307,14 +309,13 @@ func (s *SyncService) verify() error { // The verifier polls for ctc transactions. // the ctc transactions are extending the chain. latest, err := s.client.GetLatestTransaction() - if err != nil { - return err - } - - if latest == nil { + if errors.Is(err, errElementNotFound) { log.Debug("latest transaction not found") return nil } + if err != nil { + return err + } var start uint64 if s.GetLatestIndex() == nil { @@ -374,16 +375,13 @@ func (s *SyncService) sequence() error { // Place as many L1ToL2 transactions in the same context as possible // by executing them one after another. latest, err := s.client.GetLatestEnqueue() - if err != nil { - return err - } - - // This should never happen unless the backend is empty - if latest == nil { + if errors.Is(err, errElementNotFound) { log.Debug("No enqueue transactions found") return nil } - + if err != nil { + return fmt.Errorf("cannot fetch latest enqueue: %w", err) + } // Compare the remote latest queue index to the local latest // queue index. If the remote latest queue index is greater // than the local latest queue index, be sure to ingest more @@ -475,15 +473,15 @@ func (s *SyncService) syncTransactionsToTip() error { // This function must be sure to sync all the way to the tip. // First query the latest transaction latest, err := s.client.GetLatestTransaction() + if errors.Is(err, errElementNotFound) { + log.Info("No transactions to sync") + return nil + } if err != nil { log.Error("Cannot get latest transaction", "msg", err) time.Sleep(time.Second * 2) continue } - if latest == nil { - log.Info("No transactions to sync") - return nil - } tipHeight := latest.GetMeta().Index index := rawdb.ReadHeadIndex(s.db) start := uint64(0) diff --git a/l2geth/rollup/sync_service_test.go b/l2geth/rollup/sync_service_test.go index 2f3a981682b..2a3cfa99d9c 100644 --- a/l2geth/rollup/sync_service_test.go +++ b/l2geth/rollup/sync_service_test.go @@ -428,6 +428,14 @@ func (m *mockClient) GetLastConfirmedEnqueue() (*types.Transaction, error) { return nil, nil } +func (m *mockClient) GetLatestTransactionBatch() (*Batch, []*types.Transaction, error) { + return nil, nil, nil +} + +func (m *mockClient) GetTransactionBatch(index uint64) (*Batch, []*types.Transaction, error) { + return nil, nil, nil +} + func (m *mockClient) SyncStatus() (*SyncStatus, error) { return &SyncStatus{ Syncing: false,