From 1c9cc6d621cb858acc8599ed0a51bf867b2553a8 Mon Sep 17 00:00:00 2001 From: HAOYUatHZ <37070449+HAOYUatHZ@users.noreply.github.com> Date: Wed, 10 Jul 2024 13:25:51 +0800 Subject: [PATCH] feat: add scroll_api (#893) * update eth/api.go * update internal/web3ext/web3ext.go * fix * update eth/backend.go * update ethclient/ethclient.go --- eth/api_scroll.go | 237 ++++++++++++++++++++++++++++++++++++ eth/backend.go | 3 + ethclient/ethclient.go | 129 ++++++++++++++++++++ internal/ethapi/api.go | 10 +- internal/ethapi/api_test.go | 2 +- internal/web3ext/web3ext.go | 84 +++++++++++++ 6 files changed, 459 insertions(+), 6 deletions(-) create mode 100644 eth/api_scroll.go diff --git a/eth/api_scroll.go b/eth/api_scroll.go new file mode 100644 index 000000000000..5a931a90bca4 --- /dev/null +++ b/eth/api_scroll.go @@ -0,0 +1,237 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package eth + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/rpc" +) + +// ScrollAPI provides private RPC methods to query the L1 message database. +type ScrollAPI struct { + eth *Ethereum +} + +// l1MessageTxRPC is the RPC-layer representation of an L1 message. +type l1MessageTxRPC struct { + QueueIndex uint64 `json:"queueIndex"` + Gas uint64 `json:"gas"` + To *common.Address `json:"to"` + Value *hexutil.Big `json:"value"` + Data hexutil.Bytes `json:"data"` + Sender common.Address `json:"sender"` + Hash common.Hash `json:"hash"` +} + +// NewScrollAPI creates a new RPC service to query the L1 message database. +func NewScrollAPI(eth *Ethereum) *ScrollAPI { + return &ScrollAPI{eth: eth} +} + +// GetL1SyncHeight returns the latest synced L1 block height from the local database. +func (api *ScrollAPI) GetL1SyncHeight(ctx context.Context) (height *uint64, err error) { + return rawdb.ReadSyncedL1BlockNumber(api.eth.ChainDb()), nil +} + +// GetL1MessageByIndex queries an L1 message by its index in the local database. +func (api *ScrollAPI) GetL1MessageByIndex(ctx context.Context, queueIndex uint64) (height *l1MessageTxRPC, err error) { + msg := rawdb.ReadL1Message(api.eth.ChainDb(), queueIndex) + if msg == nil { + return nil, nil + } + rpcMsg := l1MessageTxRPC{ + QueueIndex: msg.QueueIndex, + Gas: msg.Gas, + To: msg.To, + Value: (*hexutil.Big)(msg.Value), + Data: msg.Data, + Sender: msg.Sender, + Hash: types.NewTx(msg).Hash(), + } + return &rpcMsg, nil +} + +// GetFirstQueueIndexNotInL2Block returns the first L1 message queue index that is +// not included in the chain up to and including the provided block. +func (api *ScrollAPI) GetFirstQueueIndexNotInL2Block(ctx context.Context, hash common.Hash) (queueIndex *uint64, err error) { + return rawdb.ReadFirstQueueIndexNotInL2Block(api.eth.ChainDb(), hash), nil +} + +// GetLatestRelayedQueueIndex returns the highest L1 message queue index included in the canonical chain. +func (api *ScrollAPI) GetLatestRelayedQueueIndex(ctx context.Context) (queueIndex *uint64, err error) { + block := api.eth.blockchain.CurrentBlock() + queueIndex, err = api.GetFirstQueueIndexNotInL2Block(ctx, block.Hash()) + if queueIndex == nil || err != nil { + return queueIndex, err + } + if *queueIndex == 0 { + return nil, nil + } + lastIncluded := *queueIndex - 1 + return &lastIncluded, nil +} + +// rpcMarshalBlock uses the generalized output filler, then adds the total difficulty field, which requires +// a `ScrollAPI`. +func (api *ScrollAPI) rpcMarshalBlock(ctx context.Context, b *types.Block, fullTx bool) (map[string]interface{}, error) { + fields := ethapi.RPCMarshalBlock(b, true, fullTx, api.eth.APIBackend.ChainConfig()) + fields["totalDifficulty"] = (*hexutil.Big)(api.eth.APIBackend.GetTd(ctx, b.Hash())) + rc := rawdb.ReadBlockRowConsumption(api.eth.ChainDb(), b.Hash()) + if rc != nil { + fields["rowConsumption"] = rc + } else { + fields["rowConsumption"] = nil + } + return fields, nil +} + +// GetBlockByHash returns the requested block. When fullTx is true all transactions in the block are returned in full +// detail, otherwise only the transaction hash is returned. +func (api *ScrollAPI) GetBlockByHash(ctx context.Context, hash common.Hash, fullTx bool) (map[string]interface{}, error) { + block, err := api.eth.APIBackend.BlockByHash(ctx, hash) + if block != nil { + return api.rpcMarshalBlock(ctx, block, fullTx) + } + return nil, err +} + +// GetBlockByNumber returns the requested block. When fullTx is true all transactions in the block are returned in full +// detail, otherwise only the transaction hash is returned. +func (api *ScrollAPI) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { + block, err := api.eth.APIBackend.BlockByNumber(ctx, number) + if block != nil { + return api.rpcMarshalBlock(ctx, block, fullTx) + } + return nil, err +} + +// GetNumSkippedTransactions returns the number of skipped transactions. +func (api *ScrollAPI) GetNumSkippedTransactions(ctx context.Context) (uint64, error) { + return rawdb.ReadNumSkippedTransactions(api.eth.ChainDb()), nil +} + +// SyncStatus includes L2 block sync height, L1 rollup sync height, +// L1 message sync height, and L2 finalized block height. +type SyncStatus struct { + L2BlockSyncHeight uint64 `json:"l2BlockSyncHeight,omitempty"` + L1RollupSyncHeight uint64 `json:"l1RollupSyncHeight,omitempty"` + L1MessageSyncHeight uint64 `json:"l1MessageSyncHeight,omitempty"` + L2FinalizedBlockHeight uint64 `json:"l2FinalizedBlockHeight,omitempty"` +} + +// SyncStatus returns the overall rollup status including L2 block sync height, L1 rollup sync height, +// L1 message sync height, and L2 finalized block height. +func (api *ScrollAPI) SyncStatus(_ context.Context) *SyncStatus { + status := &SyncStatus{} + + l2BlockHeader := api.eth.blockchain.CurrentHeader() + if l2BlockHeader != nil { + status.L2BlockSyncHeight = l2BlockHeader.Number.Uint64() + } + + l1RollupSyncHeightPtr := rawdb.ReadRollupEventSyncedL1BlockNumber(api.eth.ChainDb()) + if l1RollupSyncHeightPtr != nil { + status.L1RollupSyncHeight = *l1RollupSyncHeightPtr + } + + l1MessageSyncHeightPtr := rawdb.ReadSyncedL1BlockNumber(api.eth.ChainDb()) + if l1MessageSyncHeightPtr != nil { + status.L1MessageSyncHeight = *l1MessageSyncHeightPtr + } + + l2FinalizedBlockHeightPtr := rawdb.ReadFinalizedL2BlockNumber(api.eth.ChainDb()) + if l2FinalizedBlockHeightPtr != nil { + status.L2FinalizedBlockHeight = *l2FinalizedBlockHeightPtr + } + + return status +} + +// EstimateL1DataFee returns an estimate of the L1 data fee required to +// process the given transaction against the current pending block. +func (api *ScrollAPI) EstimateL1DataFee(ctx context.Context, args ethapi.TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash) (*hexutil.Uint64, error) { + bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) + if blockNrOrHash != nil { + bNrOrHash = *blockNrOrHash + } + + l1DataFee, err := ethapi.EstimateL1MsgFee(ctx, api.eth.APIBackend, args, bNrOrHash, nil, 0, api.eth.APIBackend.RPCGasCap(), api.eth.APIBackend.ChainConfig()) + if err != nil { + return nil, fmt.Errorf("failed to estimate L1 data fee: %w", err) + } + + result := hexutil.Uint64(l1DataFee.Uint64()) + return &result, nil +} + +// RPCTransaction is the standard RPC transaction return type with some additional skip-related fields. +type RPCTransaction struct { + ethapi.RPCTransaction + SkipReason string `json:"skipReason"` + SkipBlockNumber *hexutil.Big `json:"skipBlockNumber"` + SkipBlockHash *common.Hash `json:"skipBlockHash,omitempty"` + + // wrapped traces, currently only available for `scroll_getSkippedTransaction` API, when `MinerStoreSkippedTxTracesFlag` is set + Traces *types.BlockTrace `json:"traces,omitempty"` +} + +// GetSkippedTransaction returns a skipped transaction by its hash. +func (api *ScrollAPI) GetSkippedTransaction(ctx context.Context, hash common.Hash) (*RPCTransaction, error) { + stx := rawdb.ReadSkippedTransaction(api.eth.ChainDb(), hash) + if stx == nil { + return nil, nil + } + var rpcTx RPCTransaction + rpcTx.RPCTransaction = *ethapi.NewRPCTransaction(stx.Tx, common.Hash{}, 0, 0, 0, nil, api.eth.blockchain.Config()) + rpcTx.SkipReason = stx.Reason + rpcTx.SkipBlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(stx.BlockNumber)) + rpcTx.SkipBlockHash = stx.BlockHash + if len(stx.TracesBytes) != 0 { + traces := &types.BlockTrace{} + if err := json.Unmarshal(stx.TracesBytes, traces); err != nil { + return nil, fmt.Errorf("fail to Unmarshal traces for skipped tx, hash: %s, err: %w", hash.String(), err) + } + rpcTx.Traces = traces + } + return &rpcTx, nil +} + +// GetSkippedTransactionHashes returns a list of skipped transaction hashes between the two indices provided (inclusive). +func (api *ScrollAPI) GetSkippedTransactionHashes(ctx context.Context, from uint64, to uint64) ([]common.Hash, error) { + it := rawdb.IterateSkippedTransactionsFrom(api.eth.ChainDb(), from) + defer it.Release() + + var hashes []common.Hash + + for it.Next() { + if it.Index() > to { + break + } + hashes = append(hashes, it.TransactionHash()) + } + + return hashes, nil +} diff --git a/eth/backend.go b/eth/backend.go index d788f7dec6f7..928e1bf2a849 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -354,6 +354,9 @@ func (s *Ethereum) APIs() []rpc.API { }, { Namespace: "net", Service: s.netRPCService, + }, { + Namespace: "scroll", + Service: NewScrollAPI(s), }, }...) } diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 5faf3159d74f..3b91ea9c58d9 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -28,6 +28,7 @@ import ( "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/eth" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/rpc" ) @@ -66,6 +67,11 @@ func (ec *Client) Client() *rpc.Client { return ec.c } +// SetHeader expose the function, in able to set http header. +func (ec *Client) SetHeader(key, value string) { + ec.c.SetHeader(key, value) +} + // Blockchain Access // ChainID retrieves the current chain ID for transaction replay protection. @@ -366,6 +372,129 @@ func (ec *Client) GetTxBlockTraceOnTopOfBlock(ctx context.Context, tx *types.Tra return blockTrace, ec.c.CallContext(ctx, &blockTrace, "scroll_getTxBlockTraceOnTopOfBlock", tx, blockNumberOrHash, config) } +// GetNumSkippedTransactions returns the ... +func (ec *Client) GetNumSkippedTransactions(ctx context.Context) (uint64, error) { + var num uint64 + return num, ec.c.CallContext(ctx, &num, "scroll_getNumSkippedTransactions") +} + +// GetSkippedTransactionHashes returns the BlockTrace given the tx and block. +func (ec *Client) GetSkippedTransactionHashes(ctx context.Context, from uint64, to uint64) ([]common.Hash, error) { + hashes := []common.Hash{} + return hashes, ec.c.CallContext(ctx, &hashes, "scroll_getSkippedTransactionHashes", from, to) +} + +// GetSkippedTransaction returns the BlockTrace given the tx and block. +func (ec *Client) GetSkippedTransaction(ctx context.Context, txHash common.Hash) (*eth.RPCTransaction, error) { + tx := ð.RPCTransaction{} + return tx, ec.c.CallContext(ctx, &tx, "scroll_getSkippedTransaction", txHash) +} + +type rpcRowConsumption struct { + RowConsumption types.RowConsumption `json:"rowConsumption"` +} + +// UnmarshalJSON unmarshals from JSON. +func (r *rpcRowConsumption) UnmarshalJSON(input []byte) error { + type rpcRowConsumption struct { + RowConsumption types.RowConsumption `json:"rowConsumption"` + } + var dec rpcRowConsumption + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.RowConsumption == nil { + return errors.New("missing required field 'RowConsumption' for rpcRowConsumption") + } + r.RowConsumption = dec.RowConsumption + return nil +} + +// GetBlockByNumberOrHash returns the requested block +func (ec *Client) GetBlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.BlockWithRowConsumption, error) { + var raw json.RawMessage + var err error + if number, ok := blockNrOrHash.Number(); ok { + err = ec.c.CallContext(ctx, &raw, "scroll_getBlockByNumber", number, true) + } + if hash, ok := blockNrOrHash.Hash(); ok { + err = ec.c.CallContext(ctx, &raw, "scroll_getBlockByHash", hash, true) + } + if err != nil { + return nil, err + } else if len(raw) == 0 { + return nil, ethereum.NotFound + } + // Decode header and transactions. + var head *types.Header + var body rpcBlock + var rpcRc rpcRowConsumption + var rc *types.RowConsumption + if err := json.Unmarshal(raw, &head); err != nil { + return nil, err + } + if err := json.Unmarshal(raw, &body); err != nil { + return nil, err + } + if err := json.Unmarshal(raw, &rpcRc); err != nil { + // don't return error here if there is no RowConsumption data, because many l2geth nodes will not have this data + // instead of error l2_watcher and other services that require RowConsumption should check it + rc = nil + } else { + rc = &rpcRc.RowConsumption + } + // Quick-verify transaction and uncle lists. This mostly helps with debugging the server. + if head.UncleHash == types.EmptyUncleHash && len(body.UncleHashes) > 0 { + return nil, fmt.Errorf("server returned non-empty uncle list but block header indicates no uncles") + } + if head.UncleHash != types.EmptyUncleHash && len(body.UncleHashes) == 0 { + return nil, fmt.Errorf("server returned empty uncle list but block header indicates uncles") + } + if head.TxHash == types.EmptyRootHash && len(body.Transactions) > 0 { + return nil, fmt.Errorf("server returned non-empty transaction list but block header indicates no transactions") + } + if head.TxHash != types.EmptyRootHash && len(body.Transactions) == 0 { + return nil, fmt.Errorf("server returned empty transaction list but block header indicates transactions") + } + // Load uncles because they are not included in the block response. + var uncles []*types.Header + if len(body.UncleHashes) > 0 { + uncles = make([]*types.Header, len(body.UncleHashes)) + reqs := make([]rpc.BatchElem, len(body.UncleHashes)) + for i := range reqs { + reqs[i] = rpc.BatchElem{ + Method: "eth_getUncleByBlockHashAndIndex", + Args: []interface{}{body.Hash, hexutil.EncodeUint64(uint64(i))}, + Result: &uncles[i], + } + } + if err := ec.c.BatchCallContext(ctx, reqs); err != nil { + return nil, err + } + for i := range reqs { + if reqs[i].Error != nil { + return nil, reqs[i].Error + } + if uncles[i] == nil { + return nil, fmt.Errorf("got null header for uncle %d of block %x", i, body.Hash[:]) + } + } + } + // Fill the sender cache of transactions in the block. + txs := make([]*types.Transaction, len(body.Transactions)) + for i, tx := range body.Transactions { + if tx.From != nil { + setSenderFromServer(tx.tx, *tx.From, body.Hash) + } + txs[i] = tx.tx + } + block := types.NewBlockWithHeader(head).WithBody(txs, uncles) + return &types.BlockWithRowConsumption{ + Block: block, + RowConsumption: rc, + }, nil +} + // State Access // NetworkID returns the network ID for this client. diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index c56a744ae817..2630d2d39d5e 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1522,9 +1522,9 @@ type RPCTransaction struct { QueueIndex *hexutil.Uint64 `json:"queueIndex,omitempty"` } -// newRPCTransaction returns a transaction that will serialize to the RPC +// NewRPCTransaction returns a transaction that will serialize to the RPC // representation, with the given location metadata set (if available). -func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, blockTime uint64, index uint64, baseFee *big.Int, config *params.ChainConfig) *RPCTransaction { +func NewRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, blockTime uint64, index uint64, baseFee *big.Int, config *params.ChainConfig) *RPCTransaction { signer := types.MakeSigner(config, new(big.Int).SetUint64(blockNumber), blockTime) from, _ := types.Sender(signer, tx) v, r, s := tx.RawSignatureValues() @@ -1627,7 +1627,7 @@ func NewRPCPendingTransaction(tx *types.Transaction, current *types.Header, conf blockNumber = current.Number.Uint64() blockTime = current.Time } - return newRPCTransaction(tx, common.Hash{}, blockNumber, blockTime, 0, baseFee, config) + return NewRPCTransaction(tx, common.Hash{}, blockNumber, blockTime, 0, baseFee, config) } // newRPCTransactionFromBlockIndex returns a transaction that will serialize to the RPC representation. @@ -1636,7 +1636,7 @@ func newRPCTransactionFromBlockIndex(b *types.Block, index uint64, config *param if index >= uint64(len(txs)) { return nil } - return newRPCTransaction(txs[index], b.Hash(), b.NumberU64(), b.Time(), index, b.BaseFee(), config) + return NewRPCTransaction(txs[index], b.Hash(), b.NumberU64(), b.Time(), index, b.BaseFee(), config) } // newRPCRawTransactionFromBlockIndex returns the bytes of a transaction given a block and a transaction index. @@ -1840,7 +1840,7 @@ func (s *TransactionAPI) GetTransactionByHash(ctx context.Context, hash common.H if err != nil { return nil, err } - return newRPCTransaction(tx, blockHash, blockNumber, header.Time, index, header.BaseFee, s.b.ChainConfig()), nil + return NewRPCTransaction(tx, blockHash, blockNumber, header.Time, index, header.BaseFee, s.b.ChainConfig()), nil } // No finalized transaction, try to retrieve it from the pool if tx := s.b.GetPoolTransaction(hash); tx != nil { diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index ce3af293cf97..00596dd05512 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -76,7 +76,7 @@ func testTransactionMarshal(t *testing.T, tests []txData, config *params.ChainCo } // rpcTransaction - rpcTx := newRPCTransaction(tx, common.Hash{}, 0, 0, 0, nil, config) + rpcTx := NewRPCTransaction(tx, common.Hash{}, 0, 0, 0, nil, config) if data, err := json.Marshal(rpcTx); err != nil { t.Fatalf("test %d: marshalling failed; %v", i, err) } else if err = tx2.UnmarshalJSON(data); err != nil { diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index b86b5909d2cb..975b96a9b498 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -31,6 +31,7 @@ var Modules = map[string]string{ "les": LESJs, "vflux": VfluxJs, "dev": DevJs, + "scroll": ScrollJs, } const CliqueJs = ` @@ -911,3 +912,86 @@ web3._extend({ ], }); ` + +const ScrollJs = ` +web3._extend({ + property: 'scroll', + methods: [ + new web3._extend.Method({ + name: 'getBlockTraceByNumber', + call: 'scroll_getBlockTraceByNumberOrHash', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), + new web3._extend.Method({ + name: 'getBlockTraceByHash', + call: 'scroll_getBlockTraceByNumberOrHash', + params: 1 + }), + new web3._extend.Method({ + name: 'getTxBlockTraceOnTopOfBlock', + call: 'scroll_getTxBlockTraceOnTopOfBlock', + params: 3, + inputFormatter: [web3._extend.formatters.inputTransactionFormatter, null, null] + }), + new web3._extend.Method({ + name: 'getL1MessageByIndex', + call: 'scroll_getL1MessageByIndex', + params: 1 + }), + new web3._extend.Method({ + name: 'getFirstQueueIndexNotInL2Block', + call: 'scroll_getFirstQueueIndexNotInL2Block', + params: 1 + }), + new web3._extend.Method({ + name: 'getBlockByHash', + call: 'scroll_getBlockByHash', + params: 2, + inputFormatter: [null, function (val) { return !!val; }] + }), + new web3._extend.Method({ + name: 'getBlockByNumber', + call: 'scroll_getBlockByNumber', + params: 2, + inputFormatter: [null, function (val) { return !!val; }] + }), + new web3._extend.Method({ + name: 'getSkippedTransaction', + call: 'scroll_getSkippedTransaction', + params: 1 + }), + new web3._extend.Method({ + name: 'getSkippedTransactionHashes', + call: 'scroll_getSkippedTransactionHashes', + params: 2 + }), + new web3._extend.Method({ + name: 'estimateL1DataFee', + call: 'scroll_estimateL1DataFee', + params: 2, + inputFormatter: [web3._extend.formatters.inputCallFormatter, web3._extend.formatters.inputBlockNumberFormatter], + outputFormatter: web3._extend.utils.toDecimal + }), + ], + properties: + [ + new web3._extend.Property({ + name: 'l1SyncHeight', + getter: 'scroll_getL1SyncHeight' + }), + new web3._extend.Property({ + name: 'latestRelayedQueueIndex', + getter: 'scroll_getLatestRelayedQueueIndex' + }), + new web3._extend.Property({ + name: 'numSkippedTransactions', + getter: 'scroll_getNumSkippedTransactions' + }), + new web3._extend.Property({ + name: 'syncStatus', + getter: 'scroll_syncStatus', + }), + ] +}); +`