feat: add scroll_api (#893)

* update eth/api.go

* update internal/web3ext/web3ext.go

* fix

* update eth/backend.go

* update ethclient/ethclient.go
This commit is contained in:
HAOYUatHZ 2024-07-10 13:25:51 +08:00 committed by GitHub
parent 65af7ee9e5
commit 1c9cc6d621
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 459 additions and 6 deletions

237
eth/api_scroll.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -354,6 +354,9 @@ func (s *Ethereum) APIs() []rpc.API {
}, {
Namespace: "net",
Service: s.netRPCService,
}, {
Namespace: "scroll",
Service: NewScrollAPI(s),
},
}...)
}

View file

@ -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 := &eth.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.

View file

@ -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 {

View file

@ -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 {

View file

@ -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',
}),
]
});
`