diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 0ad9fbb207..33443eec5f 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -130,6 +130,7 @@ var ( utils.MinerExtraDataFlag, utils.MinerRecommitIntervalFlag, utils.MinerNoVerifyFlag, + utils.MinerStoreSkippedTxTracesFlag, utils.NATFlag, utils.NoDiscoverFlag, utils.DiscoveryV5Flag, diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 38970ca686..9a1b2f38df 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -192,6 +192,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.MinerExtraDataFlag, utils.MinerRecommitIntervalFlag, utils.MinerNoVerifyFlag, + utils.MinerStoreSkippedTxTracesFlag, }, }, { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index ee28b04f36..3555667b46 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -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!") } diff --git a/core/block_validator.go b/core/block_validator.go index ab0be766f0..4cf511ee20 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -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 diff --git a/core/rawdb/accessors_skipped_txs.go b/core/rawdb/accessors_skipped_txs.go index 4d26031b30..9baccf99b7 100644 --- a/core/rawdb/accessors_skipped_txs.go +++ b/core/rawdb/accessors_skipped_txs.go @@ -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) diff --git a/core/rawdb/accessors_skipped_txs_test.go b/core/rawdb/accessors_skipped_txs_test.go index 926d0d9c96..1a7052f51f 100644 --- a/core/rawdb/accessors_skipped_txs_test.go +++ b/core/rawdb/accessors_skipped_txs_test.go @@ -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() diff --git a/eth/api.go b/eth/api.go index c22425f9bd..5ab334fe47 100644 --- a/eth/api.go +++ b/eth/api.go @@ -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 } diff --git a/miner/miner.go b/miner/miner.go index b46fbdb850..1f3f9d0d77 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -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. diff --git a/miner/worker.go b/miner/worker.go index fc455efc49..ed2324369a 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -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() diff --git a/params/version.go b/params/version.go index 3aec424bca..0ce86146aa 100644 --- a/params/version.go +++ b/params/version.go @@ -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 )