feat: L1 msg tx (#610)

* update consensus/errors.go

* update core/types/receipt.go

* update core/genesis.go

* update core/blockchain.go

* update core/types/block.go

* update eth/handler.go

* update internal/ethapi/api.go

* update core/block_validator.go

* update internal/jsre/deps/web3.js

* add core/rawdb/accessors_skipped_txs.go & core/rawdb/accessors_skipped_txs_test.go

* update core/rawdb/schema.go

* update core/state_transition.go part-1

* update accounts/abi/bind/backends/simulated.go & interfaces.go

* update core/types/transaction.go

* update core/types/transaction_marshalling.go part-1

* update core/types/transaction_marshalling.go part-2

* update core/state_transition.go part-2

* update core/types/transaction_signing.go
This commit is contained in:
HAOYUatHZ 2023-12-28 16:09:51 +08:00 committed by GitHub
parent 35b8da75a0
commit adfec9a0a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 790 additions and 5 deletions

View file

@ -695,6 +695,7 @@ func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallM
Data: call.Data, Data: call.Data,
AccessList: call.AccessList, AccessList: call.AccessList,
SkipAccountChecks: true, SkipAccountChecks: true,
IsL1MessageTx: false,
} }
// Create a new environment which holds all relevant information // Create a new environment which holds all relevant information

View file

@ -41,4 +41,18 @@ var (
// ErrInvalidTxCount is returned if a block contains too many transactions. // ErrInvalidTxCount is returned if a block contains too many transactions.
ErrInvalidTxCount = errors.New("invalid transaction count") 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")
) )

View file

@ -21,8 +21,10 @@ import (
"fmt" "fmt"
"github.com/ethereum/go-ethereum/consensus" "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/state"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
) )
@ -124,6 +126,97 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
} }
return consensus.ErrPrunedAncestor 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 return nil
} }

View file

@ -319,6 +319,9 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis
return nil, ErrNoGenesis return nil, ErrNoGenesis
} }
// initialize L1 message index for genesis block
rawdb.WriteFirstQueueIndexNotInL2Block(db, bc.genesisBlock.Hash(), 0)
bc.currentBlock.Store(nil) bc.currentBlock.Store(nil)
bc.currentSnapBlock.Store(nil) bc.currentSnapBlock.Store(nil)
bc.currentFinalBlock.Store(nil) bc.currentFinalBlock.Store(nil)
@ -866,6 +869,7 @@ func (bc *BlockChain) ResetWithGenesisBlock(genesis *types.Block) error {
batch := bc.db.NewBatch() batch := bc.db.NewBatch()
rawdb.WriteTd(batch, genesis.Hash(), genesis.NumberU64(), genesis.Difficulty()) rawdb.WriteTd(batch, genesis.Hash(), genesis.NumberU64(), genesis.Difficulty())
rawdb.WriteBlock(batch, genesis) rawdb.WriteBlock(batch, genesis)
rawdb.WriteFirstQueueIndexNotInL2Block(batch, genesis.Hash(), 0)
if err := batch.Write(); err != nil { if err := batch.Write(); err != nil {
log.Crit("Failed to write genesis block", "err", err) 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() batch := bc.db.NewBatch()
rawdb.WriteTd(batch, block.Hash(), block.NumberU64(), td) rawdb.WriteTd(batch, block.Hash(), block.NumberU64(), td)
rawdb.WriteBlock(batch, block) 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 { if err := batch.Write(); err != nil {
log.Crit("Failed to write block into disk", "err", err) 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.WriteBlock(blockBatch, block)
rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts) rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts)
rawdb.WritePreimages(blockBatch, state.Preimages()) 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 { if err := blockBatch.Write(); err != nil {
log.Crit("Failed to write block into disk", "err", err) 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 return it.index, err
} }
// First block is future, shove it (and all children) to the future queue (unknown ancestor) // 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)) { for block != nil && (it.index == 0 || errors.Is(err, consensus.ErrUnknownAncestor)) {
log.Debug("Future block, postponing import", "number", block.Number(), "hash", block.Hash()) log.Debug("Future block, postponing import", "number", block.Number(), "hash", block.Hash())
if err := bc.addFutureBlock(block); err != nil { if err := bc.addFutureBlock(block); err != nil {

View file

@ -499,6 +499,7 @@ func (g *Genesis) Commit(db ethdb.Database, triedb *trie.Database) (*types.Block
rawdb.WriteHeadFastBlockHash(db, block.Hash()) rawdb.WriteHeadFastBlockHash(db, block.Hash())
rawdb.WriteHeadHeaderHash(db, block.Hash()) rawdb.WriteHeadHeaderHash(db, block.Hash())
rawdb.WriteChainConfig(db, block.Hash(), config) rawdb.WriteChainConfig(db, block.Hash(), config)
rawdb.WriteFirstQueueIndexNotInL2Block(db, block.Hash(), 0)
return block, nil return block, nil
} }

View file

@ -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()
}

View file

@ -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")
}
}

View file

@ -151,6 +151,11 @@ var (
batchChunkRangesPrefix = []byte("R-bcr") batchChunkRangesPrefix = []byte("R-bcr")
batchMetaPrefix = []byte("R-bm") batchMetaPrefix = []byte("R-bm")
finalizedL2BlockNumberKey = []byte("R-finalized") 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 // Use the updated "L1" prefix on all new networks
@ -379,6 +384,16 @@ func FirstQueueIndexNotInL2BlockKey(l2BlockHash common.Hash) []byte {
return append(firstQueueIndexNotInL2BlockPrefix, l2BlockHash.Bytes()...) 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) // batchChunkRangesKey = batchChunkRangesPrefix + batch index (uint64 big endian)
func batchChunkRangesKey(batchIndex uint64) []byte { func batchChunkRangesKey(batchIndex uint64) []byte {
return append(batchChunkRangesPrefix, encodeBigEndian(batchIndex)...) return append(batchChunkRangesPrefix, encodeBigEndian(batchIndex)...)

View file

@ -32,6 +32,7 @@ import (
// ExecutionResult includes all output after executing given evm // ExecutionResult includes all output after executing given evm
// message no matter the execution itself is successful or not. // message no matter the execution itself is successful or not.
type ExecutionResult struct { type ExecutionResult struct {
L1DataFee *big.Int
UsedGas uint64 // Total used gas but include the refunded gas UsedGas uint64 // Total used gas but include the refunded gas
Err error // Any error encountered during the execution(listed in core/vm/errors.go) 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) 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. // 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. // This field will be set to true for operations like RPC eth_call.
SkipAccountChecks bool SkipAccountChecks bool
// scroll-related fields
IsL1MessageTx bool
} }
// TransactionToMessage converts a transaction into a Message. // TransactionToMessage converts a transaction into a Message.
@ -160,6 +164,7 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In
SkipAccountChecks: false, SkipAccountChecks: false,
BlobHashes: tx.BlobHashes(), BlobHashes: tx.BlobHashes(),
BlobGasFeeCap: tx.BlobGasFeeCap(), BlobGasFeeCap: tx.BlobGasFeeCap(),
IsL1MessageTx: tx.IsL1MessageTx(),
} }
// If baseFee provided, set gasPrice to effectiveGasPrice. // If baseFee provided, set gasPrice to effectiveGasPrice.
if baseFee != nil { if baseFee != nil {
@ -265,6 +270,14 @@ func (st *StateTransition) buyGas() error {
} }
func (st *StateTransition) preCheck() 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 // Only check transactions that are not fake
msg := st.msg msg := st.msg
if !msg.SkipAccountChecks { 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) 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 { if !rules.IsLondon {
// Before EIP-3529: refunds were capped to gasUsed / 2 // Before EIP-3529: refunds were capped to gasUsed / 2
st.refundGas(params.RefundQuotient) st.refundGas(params.RefundQuotient)

View file

@ -199,6 +199,7 @@ type Block struct {
// caches // caches
hash atomic.Value hash atomic.Value
size atomic.Value size atomic.Value
l1MsgCount atomic.Value
// These fields are used by package eth to track // These fields are used by package eth to track
// inter-peer block relay. // inter-peer block relay.
@ -505,6 +506,55 @@ func (b *Block) Hash() common.Hash {
return v 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 type Blocks []*Block
// HeaderParentHashFromRLP returns the parentHash of an RLP-encoded // HeaderParentHashFromRLP returns the parentHash of an RLP-encoded

View file

@ -312,7 +312,7 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) {
} }
w.WriteByte(r.Type) w.WriteByte(r.Type)
switch r.Type { switch r.Type {
case AccessListTxType, DynamicFeeTxType, BlobTxType: case AccessListTxType, DynamicFeeTxType, BlobTxType, L1MessageTxType:
rlp.Encode(w, data) rlp.Encode(w, data)
default: default:
// For unsupported types, write nothing. Since this is for // For unsupported types, write nothing. Since this is for

View file

@ -19,14 +19,17 @@ package types
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"io" "io"
"math/big" "math/big"
"sort"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
) )
@ -205,7 +208,7 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) {
case BlobTxType: case BlobTxType:
inner = new(BlobTx) inner = new(BlobTx)
case L1MessageTxType: case L1MessageTxType:
inner = new(BlobTx) inner = new(L1MessageTx)
default: default:
return nil, ErrTxTypeNotSupported 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 // Note: if the effective gasTipCap is negative, this method returns both error
// the actual negative value, _and_ ErrGasFeeCapTooLow // the actual negative value, _and_ ErrGasFeeCapTooLow
func (tx *Transaction) EffectiveGasTip(baseFee *big.Int) (*big.Int, error) { func (tx *Transaction) EffectiveGasTip(baseFee *big.Int) (*big.Int, error) {
if tx.IsL1MessageTx() {
return new(big.Int), nil
}
if baseFee == nil { if baseFee == nil {
return tx.GasTipCap(), nil return tx.GasTipCap(), nil
} }
@ -607,3 +613,45 @@ func copyAddressPtr(a *common.Address) *common.Address {
cpy := *a cpy := *a
return &cpy 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()
}

View file

@ -49,6 +49,10 @@ type txJSON struct {
// Only used for encoding: // Only used for encoding:
Hash common.Hash `json:"hash"` 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, // 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()) enc.S = (*hexutil.Big)(itx.S.ToBig())
yparity := itx.V.Uint64() yparity := itx.V.Uint64()
enc.YParity = (*hexutil.Uint64)(&yparity) 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) 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: default:
return ErrTxTypeNotSupported return ErrTxTypeNotSupported
} }

View file

@ -188,6 +188,9 @@ func NewCancunSigner(chainId *big.Int) Signer {
} }
func (s cancunSigner) Sender(tx *Transaction) (common.Address, error) { func (s cancunSigner) Sender(tx *Transaction) (common.Address, error) {
if tx.IsL1MessageTx() {
return tx.AsL1MessageTx().Sender, nil
}
if tx.Type() != BlobTxType { if tx.Type() != BlobTxType {
return s.londonSigner.Sender(tx) 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) { 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) txdata, ok := tx.inner.(*BlobTx)
if !ok { if !ok {
return s.londonSigner.SignatureValues(tx, sig) 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. // Hash returns the hash to be signed by the sender.
// It does not uniquely identify the transaction. // It does not uniquely identify the transaction.
func (s cancunSigner) Hash(tx *Transaction) common.Hash { 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 { if tx.Type() != BlobTxType {
return s.londonSigner.Hash(tx) return s.londonSigner.Hash(tx)
} }
@ -256,6 +265,9 @@ func NewLondonSigner(chainId *big.Int) Signer {
} }
func (s londonSigner) Sender(tx *Transaction) (common.Address, error) { func (s londonSigner) Sender(tx *Transaction) (common.Address, error) {
if tx.IsL1MessageTx() {
return tx.AsL1MessageTx().Sender, nil
}
if tx.Type() != DynamicFeeTxType { if tx.Type() != DynamicFeeTxType {
return s.eip2930Signer.Sender(tx) 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) { 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) txdata, ok := tx.inner.(*DynamicFeeTx)
if !ok { if !ok {
return s.eip2930Signer.SignatureValues(tx, sig) 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. // Hash returns the hash to be signed by the sender.
// It does not uniquely identify the transaction. // It does not uniquely identify the transaction.
func (s londonSigner) Hash(tx *Transaction) common.Hash { 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 { if tx.Type() != DynamicFeeTxType {
return s.eip2930Signer.Hash(tx) 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. // id, add 27 to become equivalent to unprotected Homestead signatures.
V = new(big.Int).Add(V, big.NewInt(27)) V = new(big.Int).Add(V, big.NewInt(27))
default: default:
// L1MessageTx not supported
return common.Address{}, ErrTxTypeNotSupported return common.Address{}, ErrTxTypeNotSupported
} }
if tx.ChainId().Cmp(s.chainId) != 0 { 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) R, S, _ = decodeSignature(sig)
V = big.NewInt(int64(sig[64])) V = big.NewInt(int64(sig[64]))
default: default:
// L1MessageTx not supported
return nil, nil, nil, ErrTxTypeNotSupported return nil, nil, nil, ErrTxTypeNotSupported
} }
return R, S, V, nil return R, S, V, nil

View file

@ -611,6 +611,11 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) {
) )
// Broadcast transactions to a batch of peers not knowing about it // Broadcast transactions to a batch of peers not knowing about it
for _, tx := range txs { for _, tx := range txs {
// L1 messages are not broadcast to peers
if tx.IsL1MessageTx() {
continue
}
peers := h.peers.peersWithoutTransaction(tx.Hash()) peers := h.peers.peersWithoutTransaction(tx.Hash())
var numDirect int var numDirect int

View file

@ -142,6 +142,9 @@ type CallMsg struct {
Data []byte // input data, usually an ABI-encoded contract method invocation Data []byte // input data, usually an ABI-encoded contract method invocation
AccessList types.AccessList // EIP-2930 access list. 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 // A ContractCaller provides contract calls, essentially transactions that are executed by

View file

@ -1443,6 +1443,10 @@ type RPCTransaction struct {
R *hexutil.Big `json:"r"` R *hexutil.Big `json:"r"`
S *hexutil.Big `json:"s"` S *hexutil.Big `json:"s"`
YParity *hexutil.Uint64 `json:"yParity,omitempty"` 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 // 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.MaxFeePerBlobGas = (*hexutil.Big)(tx.BlobGasFeeCap())
result.BlobVersionedHashes = tx.BlobHashes() result.BlobVersionedHashes = tx.BlobHashes()
case types.L1MessageTxType:
msg := tx.AsL1MessageTx()
result.Sender = msg.Sender
result.QueueIndex = (*hexutil.Uint64)(&msg.QueueIndex)
} }
return result return result
} }

View file

@ -3790,6 +3790,9 @@ var outputTransactionFormatter = function (tx){
tx.maxPriorityFeePerGas = utils.toBigNumber(tx.maxPriorityFeePerGas); tx.maxPriorityFeePerGas = utils.toBigNumber(tx.maxPriorityFeePerGas);
} }
tx.value = utils.toBigNumber(tx.value); tx.value = utils.toBigNumber(tx.value);
if(tx.queueIndex !== undefined) {
tx.queueIndex = utils.toBigNumber(tx.queueIndex);
}
return tx; return tx;
}; };