diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index a26ee12e0a..c0ad623f98 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -695,6 +695,7 @@ func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallM Data: call.Data, AccessList: call.AccessList, SkipAccountChecks: true, + IsL1MessageTx: false, } // Create a new environment which holds all relevant information diff --git a/consensus/errors.go b/consensus/errors.go index d72e713a00..096cf6b600 100644 --- a/consensus/errors.go +++ b/consensus/errors.go @@ -41,4 +41,18 @@ var ( // ErrInvalidTxCount is returned if a block contains too many transactions. ErrInvalidTxCount = errors.New("invalid transaction count") + + // ErrMissingL1MessageData is returned if a block contains L1 messages that the + // node has not synced yet. In this case we insert the block into the future + // queue and process it again later. + ErrMissingL1MessageData = errors.New("unknown L1 message data") + + // ErrInvalidL1MessageOrder is returned if a block contains L1 messages in the wrong + // order. Possible scenarios are: (1) L1 messages do not follow their QueueIndex order, + // (2) L1 messages are not included in a contiguous block at the front of the block. + ErrInvalidL1MessageOrder = errors.New("invalid L1 message order") + + // ErrUnknownL1Message is returned if a block contains an L1 message that does not + // match the corresponding message in the node's local database. + ErrUnknownL1Message = errors.New("unknown L1 message") ) diff --git a/core/block_validator.go b/core/block_validator.go index fc5890af7c..7b375c2ef8 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -21,8 +21,10 @@ import ( "fmt" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" ) @@ -124,6 +126,97 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { } return consensus.ErrPrunedAncestor } + + if err := v.ValidateL1Messages(block); err != nil { + return err + } + + return nil +} + +// ValidateL1Messages validates L1 messages contained in a block. +// We check the following conditions: +// - L1 messages are in a contiguous section at the front of the block. +// - The first L1 message's QueueIndex is right after the last L1 message included in the chain. +// - L1 messages follow the QueueIndex order. +// - The L1 messages included in the block match the node's view of the L1 ledger. +func (v *BlockValidator) ValidateL1Messages(block *types.Block) error { + // skip DB read if the block contains no L1 messages + if !block.ContainsL1Messages() { + return nil + } + + blockHash := block.Hash() + + if v.config.Scroll.L1Config == nil { + // TODO: should we allow follower nodes to skip L1 message verification? + panic("Running on L1Message-enabled network but no l1Config was provided") + } + + nextQueueIndex := rawdb.ReadFirstQueueIndexNotInL2Block(v.bc.db, block.ParentHash()) + if nextQueueIndex == nil { + // we'll reprocess this block at a later time + return consensus.ErrMissingL1MessageData + } + queueIndex := *nextQueueIndex + + L1SectionOver := false + it := rawdb.IterateL1MessagesFrom(v.bc.db, queueIndex) + + for _, tx := range block.Transactions() { + if !tx.IsL1MessageTx() { + L1SectionOver = true + continue // we do not verify L2 transactions here + } + + // check that L1 messages are before L2 transactions + if L1SectionOver { + return consensus.ErrInvalidL1MessageOrder + } + + // queue index cannot decrease + txQueueIndex := tx.AsL1MessageTx().QueueIndex + + if txQueueIndex < queueIndex { + return consensus.ErrInvalidL1MessageOrder + } + + // skipped messages + // TODO: consider verifying that skipped messages overflow + for index := queueIndex; index < txQueueIndex; index++ { + if exists := it.Next(); !exists { + // the message in this block is not available in our local db. + // we'll reprocess this block at a later time. + return consensus.ErrMissingL1MessageData + } + + l1msg := it.L1Message() + skippedTx := types.NewTx(&l1msg) + log.Debug("Skipped L1 message", "queueIndex", index, "tx", skippedTx.Hash().String(), "block", blockHash.String()) + rawdb.WriteSkippedTransaction(v.bc.db, skippedTx, nil, "unknown", block.NumberU64(), &blockHash) + } + + queueIndex = txQueueIndex + 1 + + if exists := it.Next(); !exists { + // the message in this block is not available in our local db. + // we'll reprocess this block at a later time. + return consensus.ErrMissingL1MessageData + } + + // check that the L1 message in the block is the same that we collected from L1 + msg := it.L1Message() + expectedHash := types.NewTx(&msg).Hash() + + if tx.Hash() != expectedHash { + return consensus.ErrUnknownL1Message + } + } + + // TODO: consider adding a rule to enforce L1Config.NumL1MessagesPerBlock. + // If there are L1 messages available, sequencer nodes should include them. + // However, this is hard to enforce as different nodes might have different views of L1. + return nil } diff --git a/core/blockchain.go b/core/blockchain.go index f458da8257..04677ca900 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -319,6 +319,9 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis return nil, ErrNoGenesis } + // initialize L1 message index for genesis block + rawdb.WriteFirstQueueIndexNotInL2Block(db, bc.genesisBlock.Hash(), 0) + bc.currentBlock.Store(nil) bc.currentSnapBlock.Store(nil) bc.currentFinalBlock.Store(nil) @@ -866,6 +869,7 @@ func (bc *BlockChain) ResetWithGenesisBlock(genesis *types.Block) error { batch := bc.db.NewBatch() rawdb.WriteTd(batch, genesis.Hash(), genesis.NumberU64(), genesis.Difficulty()) rawdb.WriteBlock(batch, genesis) + rawdb.WriteFirstQueueIndexNotInL2Block(batch, genesis.Hash(), 0) if err := batch.Write(); err != nil { log.Crit("Failed to write genesis block", "err", err) } @@ -1364,6 +1368,36 @@ func (bc *BlockChain) writeBlockWithoutState(block *types.Block, td *big.Int) (e batch := bc.db.NewBatch() rawdb.WriteTd(batch, block.Hash(), block.NumberU64(), td) rawdb.WriteBlock(batch, block) + + queueIndex := rawdb.ReadFirstQueueIndexNotInL2Block(bc.db, block.ParentHash()) + // note: we can insert blocks with header-only ancestors here, + // so queueIndex might not yet be available in DB. + if queueIndex != nil { + numProcessed := uint64(block.NumL1MessagesProcessed(*queueIndex)) + // do not overwrite the index written by the miner worker + if index := rawdb.ReadFirstQueueIndexNotInL2Block(bc.db, block.Hash()); index == nil { + newIndex := *queueIndex + numProcessed + log.Trace( + "Blockchain.writeBlockWithoutState WriteFirstQueueIndexNotInL2Block", + "number", block.Number(), + "hash", block.Hash().String(), + "queueIndex", *queueIndex, + "numProcessed", numProcessed, + "newIndex", newIndex, + ) + rawdb.WriteFirstQueueIndexNotInL2Block(batch, block.Hash(), newIndex) + } else { + log.Trace( + "Blockchain.writeBlockWithoutState WriteFirstQueueIndexNotInL2Block: not overwriting existing index", + "number", block.Number(), + "hash", block.Hash().String(), + "queueIndex", *queueIndex, + "numProcessed", numProcessed, + "index", *index, + ) + } + } + if err := batch.Write(); err != nil { log.Crit("Failed to write block into disk", "err", err) } @@ -1403,6 +1437,37 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. rawdb.WriteBlock(blockBatch, block) rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts) rawdb.WritePreimages(blockBatch, state.Preimages()) + + queueIndex := rawdb.ReadFirstQueueIndexNotInL2Block(bc.db, block.ParentHash()) + if queueIndex == nil { + // We expect that we only insert contiguous chain segments, + // so the parent will always be inserted first. + log.Crit("Queue index in DB is nil", "parent", block.ParentHash(), "hash", block.Hash()) + } + numProcessed := uint64(block.NumL1MessagesProcessed(*queueIndex)) + // do not overwrite the index written by the miner worker + if index := rawdb.ReadFirstQueueIndexNotInL2Block(bc.db, block.Hash()); index == nil { + newIndex := *queueIndex + numProcessed + log.Trace( + "Blockchain.writeBlockWithState WriteFirstQueueIndexNotInL2Block", + "number", block.Number(), + "hash", block.Hash().String(), + "queueIndex", *queueIndex, + "numProcessed", numProcessed, + "newIndex", newIndex, + ) + rawdb.WriteFirstQueueIndexNotInL2Block(blockBatch, block.Hash(), newIndex) + } else { + log.Trace( + "Blockchain.writeBlockWithState WriteFirstQueueIndexNotInL2Block: not overwriting existing index", + "number", block.Number(), + "hash", block.Hash().String(), + "queueIndex", *queueIndex, + "numProcessed", numProcessed, + "index", *index, + ) + } + if err := blockBatch.Write(); err != nil { log.Crit("Failed to write block into disk", "err", err) } @@ -1686,7 +1751,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) return it.index, err } // First block is future, shove it (and all children) to the future queue (unknown ancestor) - case errors.Is(err, consensus.ErrFutureBlock) || (errors.Is(err, consensus.ErrUnknownAncestor) && bc.futureBlocks.Contains(it.first().ParentHash())): + case errors.Is(err, consensus.ErrFutureBlock) || errors.Is(err, consensus.ErrMissingL1MessageData) || (errors.Is(err, consensus.ErrUnknownAncestor) && bc.futureBlocks.Contains(it.first().ParentHash())): for block != nil && (it.index == 0 || errors.Is(err, consensus.ErrUnknownAncestor)) { log.Debug("Future block, postponing import", "number", block.Number(), "hash", block.Hash()) if err := bc.addFutureBlock(block); err != nil { diff --git a/core/genesis.go b/core/genesis.go index 0f1e8baf43..61672142e0 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -499,6 +499,7 @@ func (g *Genesis) Commit(db ethdb.Database, triedb *trie.Database) (*types.Block rawdb.WriteHeadFastBlockHash(db, block.Hash()) rawdb.WriteHeadHeaderHash(db, block.Hash()) rawdb.WriteChainConfig(db, block.Hash(), config) + rawdb.WriteFirstQueueIndexNotInL2Block(db, block.Hash(), 0) return block, nil } diff --git a/core/rawdb/accessors_skipped_txs.go b/core/rawdb/accessors_skipped_txs.go new file mode 100644 index 0000000000..d732c66e8f --- /dev/null +++ b/core/rawdb/accessors_skipped_txs.go @@ -0,0 +1,255 @@ +package rawdb + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" +) + +// mutex used to avoid concurrent updates of NumSkippedTransactions +var mu sync.Mutex + +// writeNumSkippedTransactions writes the number of skipped transactions to the database. +func writeNumSkippedTransactions(db ethdb.KeyValueWriter, numSkipped uint64) { + value := big.NewInt(0).SetUint64(numSkipped).Bytes() + + if err := db.Put(numSkippedTransactionsKey, value); err != nil { + log.Crit("Failed to update the number of skipped transactions", "err", err) + } +} + +// ReadNumSkippedTransactions retrieves the number of skipped transactions. +func ReadNumSkippedTransactions(db ethdb.Reader) uint64 { + data, err := db.Get(numSkippedTransactionsKey) + if err != nil && isNotFoundErr(err) { + return 0 + } + if err != nil { + log.Crit("Failed to read number of skipped transactions from database", "err", err) + } + if len(data) == 0 { + return 0 + } + + number := new(big.Int).SetBytes(data) + if !number.IsUint64() { + log.Crit("Unexpected number of skipped transactions in database", "number", number) + } + return number.Uint64() +} + +// SkippedTransaction stores the transaction object, along with the skip reason and block context. +type SkippedTransaction struct { + // Tx is the skipped transaction. + // We store the tx itself because otherwise geth will discard it after skipping. + Tx *types.Transaction + + // Reason is the skip reason. + Reason string + + // BlockNumber is the number of the block in which this transaction was skipped. + BlockNumber uint64 + + // BlockHash is the hash of the block in which this transaction was skipped or nil. + 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 + + // BlockHash 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, traces *types.BlockTrace, reason string, blockNumber uint64, blockHash *common.Hash) { + var err error + // workaround: RLP decoding fails if this is nil + if blockHash == nil { + blockHash = &common.Hash{} + } + stx := SkippedTransactionV2{Tx: tx, Reason: reason, BlockNumber: blockNumber, BlockHash: blockHash} + if traces != nil { + if stx.TracesBytes, err = json.Marshal(traces); err != nil { + log.Crit("Failed to json marshal skipped transaction", "hash", tx.Hash().String(), "err", err) + } + } + 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{} + } + 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", "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) + } +} + +// readSkippedTransactionRLP retrieves a skipped transaction in its raw RLP database encoding. +func readSkippedTransactionRLP(db ethdb.Reader, txHash common.Hash) rlp.RawValue { + data, err := db.Get(SkippedTransactionKey(txHash)) + if err != nil && isNotFoundErr(err) { + return nil + } + if err != nil { + log.Crit("Failed to load skipped transaction", "hash", txHash.String(), "err", err) + } + return data +} + +// ReadSkippedTransaction retrieves a skipped transaction by its hash, along with its skipped reason. +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), &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 stxV2.BlockHash != nil && *stxV2.BlockHash == (common.Hash{}) { + stxV2.BlockHash = nil + } + return &stxV2 +} + +// writeSkippedTransactionHash writes the hash of a skipped transaction to the database. +func writeSkippedTransactionHash(db ethdb.KeyValueWriter, index uint64, txHash common.Hash) { + if err := db.Put(SkippedTransactionHashKey(index), txHash[:]); err != nil { + log.Crit("Failed to store skipped transaction hash", "index", index, "hash", txHash.String(), "err", err) + } +} + +// ReadSkippedTransactionHash retrieves the hash of a skipped transaction by its index. +func ReadSkippedTransactionHash(db ethdb.Reader, index uint64) *common.Hash { + data, err := db.Get(SkippedTransactionHashKey(index)) + if err != nil && isNotFoundErr(err) { + return nil + } + if err != nil { + log.Crit("Failed to load skipped transaction hash", "index", index, "err", err) + } + hash := common.BytesToHash(data) + return &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, 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() + + index := ReadNumSkippedTransactions(db) + + // update in a batch + batch := db.NewBatch() + writeSkippedTransaction(batch, tx, traces, reason, blockNumber, blockHash) + writeSkippedTransactionHash(batch, index, tx.Hash()) + writeNumSkippedTransactions(batch, index+1) + + // write to DB + if err := batch.Write(); err != nil { + log.Crit("Failed to store skipped transaction", "hash", tx.Hash().String(), "err", err) + } +} + +// SkippedTransactionIterator is a wrapper around ethdb.Iterator that +// allows us to iterate over skipped transaction hashes in the database. +// It implements an interface similar to ethdb.Iterator. +type SkippedTransactionIterator struct { + inner ethdb.Iterator + db ethdb.Reader + keyLength int +} + +// IterateSkippedTransactionsFrom creates a SkippedTransactionIterator that iterates +// over all skipped transaction hashes in the database starting at the provided index. +func IterateSkippedTransactionsFrom(db ethdb.Database, index uint64) SkippedTransactionIterator { + start := encodeBigEndian(index) + it := db.NewIterator(skippedTransactionHashPrefix, start) + keyLength := len(skippedTransactionHashPrefix) + 8 + + return SkippedTransactionIterator{ + inner: it, + db: db, + keyLength: keyLength, + } +} + +// Next moves the iterator to the next key/value pair. +// It returns false when the iterator is exhausted. +// TODO: Consider reading items in batches. +func (it *SkippedTransactionIterator) Next() bool { + for it.inner.Next() { + key := it.inner.Key() + if len(key) == it.keyLength { + return true + } + } + return false +} + +// Index returns the index of the current skipped transaction hash. +func (it *SkippedTransactionIterator) Index() uint64 { + key := it.inner.Key() + raw := key[len(skippedTransactionHashPrefix) : len(skippedTransactionHashPrefix)+8] + index := binary.BigEndian.Uint64(raw) + return index +} + +// TransactionHash returns the current skipped transaction hash. +func (it *SkippedTransactionIterator) TransactionHash() common.Hash { + data := it.inner.Value() + return common.BytesToHash(data) +} + +// Release releases the associated resources. +func (it *SkippedTransactionIterator) Release() { + it.inner.Release() +} diff --git a/core/rawdb/accessors_skipped_txs_test.go b/core/rawdb/accessors_skipped_txs_test.go new file mode 100644 index 0000000000..fb5e9c3a98 --- /dev/null +++ b/core/rawdb/accessors_skipped_txs_test.go @@ -0,0 +1,144 @@ +package rawdb + +import ( + "math/big" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +func TestReadWriteNumSkippedTransactions(t *testing.T) { + blockNumbers := []uint64{ + 1, + 1 << 2, + 1 << 8, + 1 << 16, + 1 << 32, + } + + db := NewMemoryDatabase() + for _, num := range blockNumbers { + writeNumSkippedTransactions(db, num) + got := ReadNumSkippedTransactions(db) + + if got != num { + t.Fatal("Num skipped transactions mismatch", "expected", num, "got", got) + } + } +} + +func newTestTransaction(queueIndex uint64) *types.Transaction { + l1msg := types.L1MessageTx{ + QueueIndex: queueIndex, + Gas: 0, + To: &common.Address{}, + Value: big.NewInt(0), + Data: nil, + Sender: common.Address{}, + } + return types.NewTx(&l1msg) +} + +func TestReadWriteSkippedTransactionNoIndex(t *testing.T) { + tx := newTestTransaction(123) + db := NewMemoryDatabase() + 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) + } +} + +func TestReadWriteSkippedTransaction(t *testing.T) { + tx := newTestTransaction(123) + db := NewMemoryDatabase() + 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) + } + count := ReadNumSkippedTransactions(db) + if count != 1 { + t.Fatal("Skipped transaction count mismatch", "expected", 1, "got", count) + } + hash := ReadSkippedTransactionHash(db, 0) + if hash == nil || *hash != tx.Hash() { + t.Fatal("Skipped L1 message hash mismatch", "expected", tx.Hash(), "got", hash) + } +} + +func TestSkippedTransactionConcurrentUpdate(t *testing.T) { + count := 20 + tx := newTestTransaction(123) + db := NewMemoryDatabase() + var wg sync.WaitGroup + for ii := 0; ii < count; ii++ { + wg.Add(1) + go func() { + defer wg.Done() + WriteSkippedTransaction(db, tx, nil, "random reason", 1, &common.Hash{1}) + }() + } + wg.Wait() + got := ReadNumSkippedTransactions(db) + if got != uint64(count) { + t.Fatal("Skipped transaction count mismatch", "expected", count, "got", got) + } +} + +func TestIterateSkippedTransactions(t *testing.T) { + db := NewMemoryDatabase() + + txs := []*types.Transaction{ + newTestTransaction(1), + newTestTransaction(2), + newTestTransaction(3), + newTestTransaction(4), + newTestTransaction(5), + } + + for _, tx := range txs { + 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, nil, "random reason", 1, &common.Hash{1}) + + it := IterateSkippedTransactionsFrom(db, 2) + defer it.Release() + + for ii := 2; ii < len(txs); ii++ { + finished := !it.Next() + if finished { + t.Fatal("Iterator terminated early", "ii", ii) + } + + index := it.Index() + if index != uint64(ii) { + t.Fatal("Invalid skipped transaction index", "expected", ii, "got", index) + } + + hash := it.TransactionHash() + if hash != txs[ii].Hash() { + t.Fatal("Invalid skipped transaction hash", "expected", txs[ii].Hash(), "got", hash) + } + } + + finished := !it.Next() + if !finished { + t.Fatal("Iterator did not terminate") + } +} diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 078d830006..654d338912 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -151,6 +151,11 @@ var ( batchChunkRangesPrefix = []byte("R-bcr") batchMetaPrefix = []byte("R-bm") finalizedL2BlockNumberKey = []byte("R-finalized") + + // Skipped transactions + numSkippedTransactionsKey = []byte("NumberOfSkippedTransactions") + skippedTransactionPrefix = []byte("skip") // skippedTransactionPrefix + tx hash -> skipped transaction + skippedTransactionHashPrefix = []byte("sh") // skippedTransactionHashPrefix + index -> tx hash ) // Use the updated "L1" prefix on all new networks @@ -379,6 +384,16 @@ func FirstQueueIndexNotInL2BlockKey(l2BlockHash common.Hash) []byte { return append(firstQueueIndexNotInL2BlockPrefix, l2BlockHash.Bytes()...) } +// SkippedTransactionKey = skippedTransactionPrefix + tx hash +func SkippedTransactionKey(txHash common.Hash) []byte { + return append(skippedTransactionPrefix, txHash.Bytes()...) +} + +// SkippedTransactionHashKey = skippedTransactionHashPrefix + index (uint64 big endian) +func SkippedTransactionHashKey(index uint64) []byte { + return append(skippedTransactionHashPrefix, encodeBigEndian(index)...) +} + // batchChunkRangesKey = batchChunkRangesPrefix + batch index (uint64 big endian) func batchChunkRangesKey(batchIndex uint64) []byte { return append(batchChunkRangesPrefix, encodeBigEndian(batchIndex)...) diff --git a/core/state_transition.go b/core/state_transition.go index da28b3802b..6b79c1c137 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -32,6 +32,7 @@ import ( // ExecutionResult includes all output after executing given evm // message no matter the execution itself is successful or not. type ExecutionResult struct { + L1DataFee *big.Int UsedGas uint64 // Total used gas but include the refunded gas Err error // Any error encountered during the execution(listed in core/vm/errors.go) ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode) @@ -143,6 +144,9 @@ type Message struct { // account nonce in state. It also disables checking that the sender is an EOA. // This field will be set to true for operations like RPC eth_call. SkipAccountChecks bool + + // scroll-related fields + IsL1MessageTx bool } // TransactionToMessage converts a transaction into a Message. @@ -160,6 +164,7 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In SkipAccountChecks: false, BlobHashes: tx.BlobHashes(), BlobGasFeeCap: tx.BlobGasFeeCap(), + IsL1MessageTx: tx.IsL1MessageTx(), } // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { @@ -265,6 +270,14 @@ func (st *StateTransition) buyGas() error { } func (st *StateTransition) preCheck() error { + if st.msg.IsL1MessageTx { + // No fee fields to check, no nonce to check, and no need to check if EOA (L1 already verified it for us) + // Gas is free, but no refunds! + st.gasRemaining += st.msg.GasLimit + st.initialGas = st.msg.GasLimit + return st.gp.SubGas(st.msg.GasLimit) // gas used by deposits may not be used by other txs + } + // Only check transactions that are not fake msg := st.msg if !msg.SkipAccountChecks { @@ -415,6 +428,16 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, msg.Value) } + // no refunds for l1 messages + if st.msg.IsL1MessageTx { + return &ExecutionResult{ + L1DataFee: big.NewInt(0), + UsedGas: st.gasUsed(), + Err: vmerr, + ReturnData: ret, + }, nil + } + if !rules.IsLondon { // Before EIP-3529: refunds were capped to gasUsed / 2 st.refundGas(params.RefundQuotient) diff --git a/core/types/block.go b/core/types/block.go index 641b5ac92c..55f904b3e2 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -197,8 +197,9 @@ type Block struct { withdrawals Withdrawals // caches - hash atomic.Value - size atomic.Value + hash atomic.Value + size atomic.Value + l1MsgCount atomic.Value // These fields are used by package eth to track // inter-peer block relay. @@ -505,6 +506,55 @@ func (b *Block) Hash() common.Hash { return v } +// ContainsL1Messages returns true if this block contains at least one L1 message. +func (b *Block) ContainsL1Messages() bool { + for _, tx := range b.transactions { + if tx.IsL1MessageTx() { + return true + } + } + return false +} + +// NumL1MessagesProcessed returns the number of L1 messages processed in this block. +// This count includes both skipped and included messages. +// `firstQueueIndex` is the first queue index available for this block to process. +func (b *Block) NumL1MessagesProcessed(firstQueueIndex uint64) int { + if l1MsgCount := b.l1MsgCount.Load(); l1MsgCount != nil { + return l1MsgCount.(int) + } + + // find first and last queue index in block + var lastQueueIndex *uint64 + + for ii, tx := range b.transactions { + if !tx.IsL1MessageTx() { + break + } + lastQueueIndex = &b.transactions[ii].AsL1MessageTx().QueueIndex + } + + // calculate and cache L1 message count + count := 0 + if lastQueueIndex != nil { + // lastQueueIndex is guaranteed to be non-nil in this case + count = int(*lastQueueIndex - firstQueueIndex + 1) + } + b.l1MsgCount.Store(count) + return count +} + +// CountL2Tx returns the number of L2 transactions in this block. +func (b *Block) CountL2Tx() int { + count := 0 + for _, tx := range b.transactions { + if !tx.IsL1MessageTx() { + count += 1 + } + } + return count +} + type Blocks []*Block // HeaderParentHashFromRLP returns the parentHash of an RLP-encoded diff --git a/core/types/receipt.go b/core/types/receipt.go index 4f96fde59c..8a6c831d34 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -312,7 +312,7 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) { } w.WriteByte(r.Type) switch r.Type { - case AccessListTxType, DynamicFeeTxType, BlobTxType: + case AccessListTxType, DynamicFeeTxType, BlobTxType, L1MessageTxType: rlp.Encode(w, data) default: // For unsupported types, write nothing. Since this is for diff --git a/core/types/transaction.go b/core/types/transaction.go index 25a81a56b9..621ae67c01 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -19,14 +19,17 @@ package types import ( "bytes" "errors" + "fmt" "io" "math/big" + "sort" "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" ) @@ -205,7 +208,7 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) { case BlobTxType: inner = new(BlobTx) case L1MessageTxType: - inner = new(BlobTx) + inner = new(L1MessageTx) default: return nil, ErrTxTypeNotSupported } @@ -349,6 +352,9 @@ func (tx *Transaction) GasTipCapIntCmp(other *big.Int) int { // Note: if the effective gasTipCap is negative, this method returns both error // the actual negative value, _and_ ErrGasFeeCapTooLow func (tx *Transaction) EffectiveGasTip(baseFee *big.Int) (*big.Int, error) { + if tx.IsL1MessageTx() { + return new(big.Int), nil + } if baseFee == nil { return tx.GasTipCap(), nil } @@ -607,3 +613,45 @@ func copyAddressPtr(a *common.Address) *common.Address { cpy := *a return &cpy } + +// L1MessagesByQueueIndex represents a set of L1 messages ordered by their queue indices. +type L1MessagesByQueueIndex struct { + msgs []L1MessageTx +} + +func NewL1MessagesByQueueIndex(msgs []L1MessageTx) (*L1MessagesByQueueIndex, error) { + // sort by queue index + sort.Slice(msgs, func(i, j int) bool { + return msgs[i].QueueIndex < msgs[j].QueueIndex + }) + + // check for duplicates/gaps + for ii := 0; ii < len(msgs)-1; ii++ { + current := msgs[ii].QueueIndex + next := msgs[ii+1].QueueIndex + if next != current+1 { + return nil, fmt.Errorf("invalid L1 message set, current index: %d, next index: %d", current, next) + } + } + + return &L1MessagesByQueueIndex{msgs: msgs}, nil +} + +func (t *L1MessagesByQueueIndex) Peek() *Transaction { + if len(t.msgs) == 0 { + return nil + } + return NewTx(&t.msgs[0]) +} + +func (t *L1MessagesByQueueIndex) Shift() { + t.msgs = t.msgs[1:] +} + +func (t *L1MessagesByQueueIndex) Pop() { + log.Error("Pop() is called on L1MessagesByQueueIndex") + + // this is a logic error, the intention should be "Shift()", + // so we will follow the same behavior in Pop + t.Shift() +} diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go index e5d71a85d6..39eb207628 100644 --- a/core/types/transaction_marshalling.go +++ b/core/types/transaction_marshalling.go @@ -49,6 +49,10 @@ type txJSON struct { // Only used for encoding: Hash common.Hash `json:"hash"` + + // L1 message transaction fields: + Sender common.Address `json:"sender,omitempty"` + QueueIndex *hexutil.Uint64 `json:"queueIndex,omitempty"` } // yParityValue returns the YParity value from JSON. For backwards-compatibility reasons, @@ -142,6 +146,14 @@ func (tx *Transaction) MarshalJSON() ([]byte, error) { enc.S = (*hexutil.Big)(itx.S.ToBig()) yparity := itx.V.Uint64() enc.YParity = (*hexutil.Uint64)(&yparity) + + case *L1MessageTx: + enc.QueueIndex = (*hexutil.Uint64)(&itx.QueueIndex) + enc.Gas = (*hexutil.Uint64)(&itx.Gas) + enc.To = tx.To() + enc.Value = (*hexutil.Big)(itx.Value) + enc.Input = (*hexutil.Bytes)(&itx.Data) + enc.Sender = itx.Sender } return json.Marshal(&enc) } @@ -404,6 +416,30 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error { } } + case L1MessageTxType: + var itx L1MessageTx + inner = &itx + if dec.QueueIndex == nil { + return errors.New("missing required field 'queueIndex' in transaction") + } + itx.QueueIndex = uint64(*dec.QueueIndex) + if dec.Gas == nil { + return errors.New("missing required field 'gas' in transaction") + } + itx.Gas = uint64(*dec.Gas) + if dec.To != nil { + itx.To = dec.To + } + if dec.Value == nil { + return errors.New("missing required field 'value' in transaction") + } + itx.Value = (*big.Int)(dec.Value) + if dec.Input == nil { + return errors.New("missing required field 'input' in transaction") + } + itx.Data = *dec.Input + itx.Sender = dec.Sender + default: return ErrTxTypeNotSupported } diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index 9e26642f75..86649d0c26 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -188,6 +188,9 @@ func NewCancunSigner(chainId *big.Int) Signer { } func (s cancunSigner) Sender(tx *Transaction) (common.Address, error) { + if tx.IsL1MessageTx() { + return tx.AsL1MessageTx().Sender, nil + } if tx.Type() != BlobTxType { return s.londonSigner.Sender(tx) } @@ -207,6 +210,9 @@ func (s cancunSigner) Equal(s2 Signer) bool { } func (s cancunSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) { + if tx.IsL1MessageTx() { + return nil, nil, nil, fmt.Errorf("l1 message tx do not have a signature") + } txdata, ok := tx.inner.(*BlobTx) if !ok { return s.londonSigner.SignatureValues(tx, sig) @@ -224,6 +230,9 @@ func (s cancunSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big // Hash returns the hash to be signed by the sender. // It does not uniquely identify the transaction. func (s cancunSigner) Hash(tx *Transaction) common.Hash { + if tx.IsL1MessageTx() { + panic("l1 message tx cannot be signed and do not have a signing hash") + } if tx.Type() != BlobTxType { return s.londonSigner.Hash(tx) } @@ -256,6 +265,9 @@ func NewLondonSigner(chainId *big.Int) Signer { } func (s londonSigner) Sender(tx *Transaction) (common.Address, error) { + if tx.IsL1MessageTx() { + return tx.AsL1MessageTx().Sender, nil + } if tx.Type() != DynamicFeeTxType { return s.eip2930Signer.Sender(tx) } @@ -275,6 +287,9 @@ func (s londonSigner) Equal(s2 Signer) bool { } func (s londonSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) { + if tx.IsL1MessageTx() { + return nil, nil, nil, fmt.Errorf("l1 message tx do not have a signature") + } txdata, ok := tx.inner.(*DynamicFeeTx) if !ok { return s.eip2930Signer.SignatureValues(tx, sig) @@ -292,6 +307,9 @@ func (s londonSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big // Hash returns the hash to be signed by the sender. // It does not uniquely identify the transaction. func (s londonSigner) Hash(tx *Transaction) common.Hash { + if tx.IsL1MessageTx() { + panic("l1 message tx cannot be signed and do not have a signing hash") + } if tx.Type() != DynamicFeeTxType { return s.eip2930Signer.Hash(tx) } @@ -337,6 +355,7 @@ func (s eip2930Signer) Sender(tx *Transaction) (common.Address, error) { // id, add 27 to become equivalent to unprotected Homestead signatures. V = new(big.Int).Add(V, big.NewInt(27)) default: + // L1MessageTx not supported return common.Address{}, ErrTxTypeNotSupported } if tx.ChainId().Cmp(s.chainId) != 0 { @@ -358,6 +377,7 @@ func (s eip2930Signer) SignatureValues(tx *Transaction, sig []byte) (R, S, V *bi R, S, _ = decodeSignature(sig) V = big.NewInt(int64(sig[64])) default: + // L1MessageTx not supported return nil, nil, nil, ErrTxTypeNotSupported } return R, S, V, nil diff --git a/eth/handler.go b/eth/handler.go index f0021e5644..8fa8072b98 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -611,6 +611,11 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) { ) // Broadcast transactions to a batch of peers not knowing about it for _, tx := range txs { + // L1 messages are not broadcast to peers + if tx.IsL1MessageTx() { + continue + } + peers := h.peers.peersWithoutTransaction(tx.Hash()) var numDirect int diff --git a/interfaces.go b/interfaces.go index eb9af60076..729b47f0de 100644 --- a/interfaces.go +++ b/interfaces.go @@ -142,6 +142,9 @@ type CallMsg struct { Data []byte // input data, usually an ABI-encoded contract method invocation AccessList types.AccessList // EIP-2930 access list. + + // scroll-related: + // not need to have a `IsL1MessageTx` field, should always be false } // A ContractCaller provides contract calls, essentially transactions that are executed by diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index f967e7b230..f1f9ce82f0 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1443,6 +1443,10 @@ type RPCTransaction struct { R *hexutil.Big `json:"r"` S *hexutil.Big `json:"s"` YParity *hexutil.Uint64 `json:"yParity,omitempty"` + + // L1 message transaction fields: + Sender common.Address `json:"sender,omitempty"` + QueueIndex *hexutil.Uint64 `json:"queueIndex,omitempty"` } // newRPCTransaction returns a transaction that will serialize to the RPC @@ -1517,6 +1521,11 @@ func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber } result.MaxFeePerBlobGas = (*hexutil.Big)(tx.BlobGasFeeCap()) result.BlobVersionedHashes = tx.BlobHashes() + + case types.L1MessageTxType: + msg := tx.AsL1MessageTx() + result.Sender = msg.Sender + result.QueueIndex = (*hexutil.Uint64)(&msg.QueueIndex) } return result } diff --git a/internal/jsre/deps/web3.js b/internal/jsre/deps/web3.js index 7a09fddab0..bd4077e8a4 100644 --- a/internal/jsre/deps/web3.js +++ b/internal/jsre/deps/web3.js @@ -3790,6 +3790,9 @@ var outputTransactionFormatter = function (tx){ tx.maxPriorityFeePerGas = utils.toBigNumber(tx.maxPriorityFeePerGas); } tx.value = utils.toBigNumber(tx.value); + if(tx.queueIndex !== undefined) { + tx.queueIndex = utils.toBigNumber(tx.queueIndex); + } return tx; };