feat(api/rawdb): allow storing traces for skipped txs (#500)

This commit is contained in:
HAOYUatHZ 2023-09-05 11:03:15 +08:00 committed by GitHub
parent 2d9d48d463
commit 0b6c3be40d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 132 additions and 29 deletions

View file

@ -130,6 +130,7 @@ var (
utils.MinerExtraDataFlag,
utils.MinerRecommitIntervalFlag,
utils.MinerNoVerifyFlag,
utils.MinerStoreSkippedTxTracesFlag,
utils.NATFlag,
utils.NoDiscoverFlag,
utils.DiscoveryV5Flag,

View file

@ -192,6 +192,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{
utils.MinerExtraDataFlag,
utils.MinerRecommitIntervalFlag,
utils.MinerNoVerifyFlag,
utils.MinerStoreSkippedTxTracesFlag,
},
},
{

View file

@ -491,6 +491,10 @@ var (
Name: "miner.noverify",
Usage: "Disable remote sealing verification",
}
MinerStoreSkippedTxTracesFlag = cli.BoolFlag{
Name: "miner.storeskippedtxtraces",
Usage: "Store the wrapped traces when storing a skipped tx",
}
// Account settings
UnlockedAccountFlag = cli.StringFlag{
Name: "unlock",
@ -1488,6 +1492,9 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) {
if ctx.GlobalIsSet(MinerNoVerifyFlag.Name) {
cfg.Noverify = ctx.GlobalBool(MinerNoVerifyFlag.Name)
}
if ctx.GlobalIsSet(MinerStoreSkippedTxTracesFlag.Name) {
cfg.StoreSkippedTxTraces = ctx.GlobalBool(MinerStoreSkippedTxTracesFlag.Name)
}
if ctx.GlobalIsSet(LegacyMinerGasTargetFlag.Name) {
log.Warn("The generic --miner.gastarget flag is deprecated and will be removed in the future!")
}

View file

@ -179,7 +179,7 @@ func (v *BlockValidator) ValidateL1Messages(block *types.Block) error {
l1msg := it.L1Message()
skippedTx := types.NewTx(&l1msg)
log.Debug("Skipped L1 message", "queueIndex", index, "tx", skippedTx.Hash().String(), "block", blockHash.String())
rawdb.WriteSkippedTransaction(v.db, skippedTx, "unknown", block.NumberU64(), &blockHash)
rawdb.WriteSkippedTransaction(v.db, skippedTx, nil, "unknown", block.NumberU64(), &blockHash)
}
queueIndex = txQueueIndex + 1

View file

@ -3,6 +3,7 @@ package rawdb
import (
"bytes"
"encoding/binary"
"encoding/json"
"math/big"
"sync"
@ -61,8 +62,50 @@ type SkippedTransaction struct {
BlockHash *common.Hash
}
// SkippedTransactionV2 stores the SkippedTransaction object along with serialized traces.
type SkippedTransactionV2 struct {
// Tx is the skipped transaction.
// We store the tx itself otherwise geth will discard it after skipping.
Tx *types.Transaction
// Traces is the serialized wrapped traces of the skipped transaction.
// We only store it when `MinerStoreSkippedTxTracesFlag` is enabled, so it might be empty.
// Note that we do not directly utilize `*types.BlockTrace` due to the fact that
// types.BlockTrace.StorageTrace.Proofs is of type `map[string][]hexutil.Bytes`, which is not RLP-serializable.
TracesBytes []byte
// Reason is the skip reason.
Reason string
// BlockNumber is the number of the block in which this transaction was skipped.
BlockNumber uint64
// BlockNumber is the hash of the block in which this transaction was skipped or nil.
BlockHash *common.Hash
}
// writeSkippedTransaction writes a skipped transaction to the database.
func writeSkippedTransaction(db ethdb.KeyValueWriter, tx *types.Transaction, reason string, blockNumber uint64, blockHash *common.Hash) {
func writeSkippedTransaction(db ethdb.KeyValueWriter, tx *types.Transaction, traces *types.BlockTrace, reason string, blockNumber uint64, blockHash *common.Hash) {
// workaround: RLP decoding fails if this is nil
if blockHash == nil {
blockHash = &common.Hash{}
}
b, err := json.Marshal(traces)
if err != nil {
log.Crit("Failed to json marshal skipped transaction", "hash", tx.Hash().String(), "err", err)
}
stx := SkippedTransactionV2{Tx: tx, TracesBytes: b, Reason: reason, BlockNumber: blockNumber, BlockHash: blockHash}
bytes, err := rlp.EncodeToBytes(stx)
if err != nil {
log.Crit("Failed to RLP encode skipped transaction", "hash", tx.Hash().String(), "err", err)
}
if err := db.Put(SkippedTransactionKey(tx.Hash()), bytes); err != nil {
log.Crit("Failed to store skipped transaction", "hash", tx.Hash().String(), "err", err)
}
}
// writeSkippedTransactionV1 is the old version of writeSkippedTransaction, we keep it for testing compatibility purpose.
func writeSkippedTransactionV1(db ethdb.KeyValueWriter, tx *types.Transaction, reason string, blockNumber uint64, blockHash *common.Hash) {
// workaround: RLP decoding fails if this is nil
if blockHash == nil {
blockHash = &common.Hash{}
@ -70,7 +113,7 @@ func writeSkippedTransaction(db ethdb.KeyValueWriter, tx *types.Transaction, rea
stx := SkippedTransaction{Tx: tx, Reason: reason, BlockNumber: blockNumber, BlockHash: blockHash}
bytes, err := rlp.EncodeToBytes(stx)
if err != nil {
log.Crit("Failed to RLP encode skipped transaction", "err", err)
log.Crit("Failed to RLP encode skipped transaction", "hash", tx.Hash().String(), "err", err)
}
if err := db.Put(SkippedTransactionKey(tx.Hash()), bytes); err != nil {
log.Crit("Failed to store skipped transaction", "hash", tx.Hash().String(), "err", err)
@ -90,19 +133,27 @@ func readSkippedTransactionRLP(db ethdb.Reader, txHash common.Hash) rlp.RawValue
}
// ReadSkippedTransaction retrieves a skipped transaction by its hash, along with its skipped reason.
func ReadSkippedTransaction(db ethdb.Reader, txHash common.Hash) *SkippedTransaction {
func ReadSkippedTransaction(db ethdb.Reader, txHash common.Hash) *SkippedTransactionV2 {
data := readSkippedTransactionRLP(db, txHash)
if len(data) == 0 {
return nil
}
var stxV2 SkippedTransactionV2
var stx SkippedTransaction
if err := rlp.Decode(bytes.NewReader(data), &stx); err != nil {
log.Crit("Invalid skipped transaction RLP", "hash", txHash.String(), "data", data, "err", err)
if err := rlp.Decode(bytes.NewReader(data), &stxV2); err != nil {
if err := rlp.Decode(bytes.NewReader(data), &stx); err != nil {
log.Crit("Invalid skipped transaction RLP", "hash", txHash.String(), "data", data, "err", err)
}
stxV2.Tx = stx.Tx
stxV2.Reason = stx.Reason
stxV2.BlockNumber = stx.BlockNumber
stxV2.BlockHash = stx.BlockHash
}
if stx.BlockHash != nil && *stx.BlockHash == (common.Hash{}) {
stx.BlockHash = nil
if stxV2.BlockHash != nil && *stxV2.BlockHash == (common.Hash{}) {
stxV2.BlockHash = nil
}
return &stx
return &stxV2
}
// writeSkippedTransactionHash writes the hash of a skipped transaction to the database.
@ -127,7 +178,7 @@ func ReadSkippedTransactionHash(db ethdb.Reader, index uint64) *common.Hash {
// WriteSkippedTransaction writes a skipped transaction to the database and also updates the count and lookup index.
// Note: The lookup index and count will include duplicates if there are chain reorgs.
func WriteSkippedTransaction(db ethdb.Database, tx *types.Transaction, reason string, blockNumber uint64, blockHash *common.Hash) {
func WriteSkippedTransaction(db ethdb.Database, tx *types.Transaction, traces *types.BlockTrace, reason string, blockNumber uint64, blockHash *common.Hash) {
// this method is not accessed concurrently, but just to be sure...
mu.Lock()
defer mu.Unlock()
@ -136,7 +187,7 @@ func WriteSkippedTransaction(db ethdb.Database, tx *types.Transaction, reason st
// update in a batch
batch := db.NewBatch()
writeSkippedTransaction(db, tx, reason, blockNumber, blockHash)
writeSkippedTransaction(db, tx, traces, reason, blockNumber, blockHash)
writeSkippedTransactionHash(db, index, tx.Hash())
writeNumSkippedTransactions(db, index+1)

View file

@ -44,7 +44,17 @@ func newTestTransaction(queueIndex uint64) *types.Transaction {
func TestReadWriteSkippedTransactionNoIndex(t *testing.T) {
tx := newTestTransaction(123)
db := NewMemoryDatabase()
writeSkippedTransaction(db, tx, "random reason", 1, &common.Hash{1})
writeSkippedTransaction(db, tx, nil, "random reason", 1, &common.Hash{1})
got := ReadSkippedTransaction(db, tx.Hash())
if got == nil || got.Tx.Hash() != tx.Hash() || got.Reason != "random reason" || got.BlockNumber != 1 || got.BlockHash == nil || *got.BlockHash != (common.Hash{1}) {
t.Fatal("Skipped transaction mismatch", "got", got)
}
}
func TestReadSkippedTransactionV1AsV2(t *testing.T) {
tx := newTestTransaction(123)
db := NewMemoryDatabase()
writeSkippedTransactionV1(db, tx, "random reason", 1, &common.Hash{1})
got := ReadSkippedTransaction(db, tx.Hash())
if got == nil || got.Tx.Hash() != tx.Hash() || got.Reason != "random reason" || got.BlockNumber != 1 || got.BlockHash == nil || *got.BlockHash != (common.Hash{1}) {
t.Fatal("Skipped transaction mismatch", "got", got)
@ -54,7 +64,7 @@ func TestReadWriteSkippedTransactionNoIndex(t *testing.T) {
func TestReadWriteSkippedTransaction(t *testing.T) {
tx := newTestTransaction(123)
db := NewMemoryDatabase()
WriteSkippedTransaction(db, tx, "random reason", 1, &common.Hash{1})
WriteSkippedTransaction(db, tx, nil, "random reason", 1, &common.Hash{1})
got := ReadSkippedTransaction(db, tx.Hash())
if got == nil || got.Tx.Hash() != tx.Hash() || got.Reason != "random reason" || got.BlockNumber != 1 || got.BlockHash == nil || *got.BlockHash != (common.Hash{1}) {
t.Fatal("Skipped transaction mismatch", "got", got)
@ -78,7 +88,7 @@ func TestSkippedTransactionConcurrentUpdate(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
WriteSkippedTransaction(db, tx, "random reason", 1, &common.Hash{1})
WriteSkippedTransaction(db, tx, nil, "random reason", 1, &common.Hash{1})
}()
}
wg.Wait()
@ -100,12 +110,12 @@ func TestIterateSkippedTransactions(t *testing.T) {
}
for _, tx := range txs {
WriteSkippedTransaction(db, tx, "random reason", 1, &common.Hash{1})
WriteSkippedTransaction(db, tx, nil, "random reason", 1, &common.Hash{1})
}
// simulate skipped L2 tx that's not included in the index
l2tx := newTestTransaction(6)
writeSkippedTransaction(db, l2tx, "random reason", 1, &common.Hash{1})
writeSkippedTransaction(db, l2tx, nil, "random reason", 1, &common.Hash{1})
it := IterateSkippedTransactionsFrom(db, 2)
defer it.Release()

View file

@ -19,6 +19,7 @@ package eth
import (
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
@ -720,6 +721,9 @@ type RPCTransaction struct {
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.
@ -733,6 +737,11 @@ func (api *ScrollAPI) GetSkippedTransaction(ctx context.Context, hash common.Has
rpcTx.SkipReason = stx.Reason
rpcTx.SkipBlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(stx.BlockNumber))
rpcTx.SkipBlockHash = stx.BlockHash
if len(stx.TracesBytes) != 0 {
if err := json.Unmarshal(stx.TracesBytes, rpcTx.Traces); err != nil {
return nil, fmt.Errorf("fail to Unmarshal traces for skipped tx, hash: %s, err: %w", hash.String(), err)
}
}
return &rpcTx, nil
}

View file

@ -56,6 +56,8 @@ type Config struct {
GasPrice *big.Int // Minimum gas price for mining a transaction
Recommit time.Duration // The time interval for miner to re-create mining work.
Noverify bool // Disable remote mining solution verification(only useful in ethash).
StoreSkippedTxTraces bool // Whether store the wrapped traces when storing a skipped tx
}
// Miner creates blocks and searches for proof-of-work values.

View file

@ -884,8 +884,10 @@ func (w *worker) updateSnapshot() {
w.snapshotState = w.current.state.Copy()
}
func (w *worker) commitTransaction(tx *types.Transaction, coinbase common.Address) ([]*types.Log, error) {
func (w *worker) commitTransaction(tx *types.Transaction, coinbase common.Address) ([]*types.Log, *types.BlockTrace, error) {
var accRows *types.RowConsumption
var traces *types.BlockTrace
var err error
// do not do CCC checks on follower nodes
if w.isRunning() {
@ -904,18 +906,18 @@ func (w *worker) commitTransaction(tx *types.Transaction, coinbase common.Addres
// 2.1 when starting handling the first tx, `state.refund` is 0 by default,
// 2.2 after tracing, the state is either committed in `core.ApplyTransaction`, or reverted, so the `state.refund` can be cleared,
// 2.3 when starting handling the following txs, `state.refund` comes as 0
traces, err := w.current.traceEnv.GetBlockTrace(
traces, err = w.current.traceEnv.GetBlockTrace(
types.NewBlockWithHeader(w.current.header).WithBody([]*types.Transaction{tx}, nil),
)
// `w.current.traceEnv.State` & `w.current.state` share a same pointer to the state, so only need to revert `w.current.state`
// revert to snapshot for calling `core.ApplyMessage` again, (both `traceEnv.GetBlockTrace` & `core.ApplyTransaction` will call `core.ApplyMessage`)
w.current.state.RevertToSnapshot(snap)
if err != nil {
return nil, err
return nil, nil, err
}
accRows, err = w.circuitCapacityChecker.ApplyTransaction(traces)
if err != nil {
return nil, err
return nil, traces, err
}
log.Trace(
"Worker apply ccc for tx result",
@ -931,14 +933,14 @@ func (w *worker) commitTransaction(tx *types.Transaction, coinbase common.Addres
receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &coinbase, w.current.gasPool, w.current.state, w.current.header, tx, &w.current.header.GasUsed, *w.chain.GetVMConfig())
if err != nil {
w.current.state.RevertToSnapshot(snap)
return nil, err
return nil, traces, err
}
w.current.txs = append(w.current.txs, tx)
w.current.receipts = append(w.current.receipts, receipt)
w.current.accRows = accRows
return receipt.Logs, nil
return receipt.Logs, traces, nil
}
func (w *worker) commitTransactions(txs *types.TransactionsByPriceAndNonce, coinbase common.Address, interrupt *int32) (bool, bool) {
@ -1023,7 +1025,7 @@ loop:
// Start executing the transaction
w.current.state.Prepare(tx.Hash(), w.current.tcount)
logs, err := w.commitTransaction(tx, coinbase)
logs, traces, err := w.commitTransaction(tx, coinbase)
switch {
case errors.Is(err, core.ErrGasLimitReached) && tx.IsL1MessageTx():
// If this block already contains some L1 messages,
@ -1036,7 +1038,11 @@ loop:
log.Info("Skipping L1 message", "queueIndex", queueIndex, "tx", tx.Hash().String(), "block", w.current.header.Number, "reason", "gas limit exceeded")
w.current.nextL1MsgIndex = queueIndex + 1
txs.Shift()
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, "gas limit exceeded", w.current.header.Number.Uint64(), nil)
if w.config.StoreSkippedTxTraces {
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, traces, "gas limit exceeded", w.current.header.Number.Uint64(), nil)
} else {
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, nil, "gas limit exceeded", w.current.header.Number.Uint64(), nil)
}
l1TxGasLimitExceededCounter.Inc(1)
case errors.Is(err, core.ErrGasLimitReached):
@ -1113,7 +1119,11 @@ loop:
circuitCapacityReached = false
// Store skipped transaction in local db
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, "row consumption overflow", w.current.header.Number.Uint64(), nil)
if w.config.StoreSkippedTxTraces {
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, traces, "row consumption overflow", w.current.header.Number.Uint64(), nil)
} else {
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, nil, "row consumption overflow", w.current.header.Number.Uint64(), nil)
}
}
case (errors.Is(err, circuitcapacitychecker.ErrUnknown) && tx.IsL1MessageTx()):
@ -1124,7 +1134,11 @@ loop:
log.Info("Skipping L1 message", "queueIndex", queueIndex, "tx", tx.Hash().String(), "block", w.current.header.Number, "reason", "unknown row consumption error")
w.current.nextL1MsgIndex = queueIndex + 1
// TODO: propagate more info about the error from CCC
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, "unknown circuit capacity checker error", w.current.header.Number.Uint64(), nil)
if w.config.StoreSkippedTxTraces {
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, traces, "unknown circuit capacity checker error", w.current.header.Number.Uint64(), nil)
} else {
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, nil, "unknown circuit capacity checker error", w.current.header.Number.Uint64(), nil)
}
l1TxCccUnknownErrCounter.Inc(1)
// Normally we would do `txs.Shift()` here.
@ -1138,7 +1152,11 @@ loop:
log.Trace("Unknown circuit capacity checker error for L2MessageTx", "tx", tx.Hash().String())
log.Info("Skipping L2 message", "tx", tx.Hash().String(), "block", w.current.header.Number, "reason", "unknown row consumption error")
// TODO: propagate more info about the error from CCC
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, "unknown circuit capacity checker error", w.current.header.Number.Uint64(), nil)
if w.config.StoreSkippedTxTraces {
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, traces, "unknown circuit capacity checker error", w.current.header.Number.Uint64(), nil)
} else {
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, nil, "unknown circuit capacity checker error", w.current.header.Number.Uint64(), nil)
}
l2TxCccUnknownErrCounter.Inc(1)
// Normally we would do `txs.Pop()` here.
@ -1156,7 +1174,11 @@ loop:
queueIndex := tx.AsL1MessageTx().QueueIndex
log.Info("Skipping L1 message", "queueIndex", queueIndex, "tx", tx.Hash().String(), "block", w.current.header.Number, "reason", "strange error", "err", err)
w.current.nextL1MsgIndex = queueIndex + 1
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, fmt.Sprintf("strange error: %v", err), w.current.header.Number.Uint64(), nil)
if w.config.StoreSkippedTxTraces {
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, traces, fmt.Sprintf("strange error: %v", err), w.current.header.Number.Uint64(), nil)
} else {
rawdb.WriteSkippedTransaction(w.eth.ChainDb(), tx, nil, fmt.Sprintf("strange error: %v", err), w.current.header.Number.Uint64(), nil)
}
l1TxStrangeErrCounter.Inc(1)
}
txs.Shift()

View file

@ -24,7 +24,7 @@ import (
const (
VersionMajor = 4 // Major version component of the current release
VersionMinor = 3 // Minor version component of the current release
VersionPatch = 62 // Patch version component of the current release
VersionPatch = 63 // Patch version component of the current release
VersionMeta = "sepolia" // Version metadata to append to the version string
)