Commit 7e9ca1eb authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

l2geth: rollup client batch api (#516)

* l2geth: add batch querying to rollup client

* l2geth: add patch changeset

* l2geth: complete mock client interface

* l2geth: add comments to rollup client

* l2geth: more idiomatic error handling
parent 76c4ceb0
---
"@eth-optimism/l2geth": patch
---
Add batch API to rollup client
......@@ -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{}).
......
......@@ -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)
......@@ -310,14 +312,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 {
......@@ -372,16 +373,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
......@@ -486,15 +484,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)
......
......@@ -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,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment