Skip to content

Commit dc00126

Browse files
authored
Merge pull request OffchainLabs#3309 from OffchainLabs/raul/mel-replay-txs-walking
[MEL] - Fetch All Txs for Block Using Preimage Reads
2 parents 14aa343 + 9e856bf commit dc00126

File tree

4 files changed

+370
-0
lines changed

4 files changed

+370
-0
lines changed

cmd/mel-replay/db.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/ethereum/go-ethereum/ethdb"
9+
10+
"github.com/offchainlabs/nitro/arbutil"
11+
)
12+
13+
// Ensures that the DB implementation satisfied the ethdb.Database interface.
14+
var _ ethdb.Database = (*DB)(nil)
15+
16+
type DB struct {
17+
resolver preimageResolver
18+
}
19+
20+
func (d *DB) Get(key []byte) ([]byte, error) {
21+
if len(key) != 32 {
22+
return nil, fmt.Errorf("expected 32 byte key query, but got %d bytes: %x", len(key), key)
23+
}
24+
preimage, err := d.resolver.ResolveTypedPreimage(arbutil.Keccak256PreimageType, common.BytesToHash(key))
25+
if err != nil {
26+
return nil, fmt.Errorf("error resolving preimage for %#x: %w", key, err)
27+
}
28+
return preimage, nil
29+
}
30+
31+
func (d *DB) Has(key []byte) (bool, error) {
32+
return false, errors.New("unimplemented")
33+
}
34+
35+
func (d *DB) Put(key []byte, value []byte) error {
36+
return errors.New("unimplemented")
37+
}
38+
39+
func (p DB) Delete(key []byte) error {
40+
return errors.New("unimplemented")
41+
}
42+
43+
func (d *DB) DeleteRange(start, end []byte) error {
44+
return errors.New("unimplemented")
45+
}
46+
47+
func (p DB) Stat() (string, error) {
48+
return "", errors.New("unimplemented")
49+
}
50+
51+
func (p DB) NewBatch() ethdb.Batch {
52+
panic("unimplemented")
53+
}
54+
55+
func (p DB) NewBatchWithSize(size int) ethdb.Batch {
56+
panic("unimplemented")
57+
}
58+
59+
func (p DB) NewIterator(prefix []byte, start []byte) ethdb.Iterator {
60+
panic("unimplemented")
61+
}
62+
63+
func (p DB) Compact(start []byte, limit []byte) error {
64+
return nil // no-op
65+
}
66+
67+
func (p DB) Close() error {
68+
return nil
69+
}
70+
71+
func (d *DB) HasAncient(kind string, number uint64) (bool, error) {
72+
return false, errors.New("unimplemented")
73+
}
74+
75+
func (d *DB) Ancient(kind string, number uint64) ([]byte, error) {
76+
return nil, errors.New("unimplemented")
77+
}
78+
79+
func (d *DB) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) {
80+
return nil, errors.New("unimplemented")
81+
}
82+
83+
func (d *DB) Ancients() (uint64, error) {
84+
return 0, errors.New("unimplemented")
85+
}
86+
87+
func (d *DB) Tail() (uint64, error) {
88+
return 0, errors.New("unimplemented")
89+
}
90+
91+
func (d *DB) AncientSize(kind string) (uint64, error) {
92+
return 0, errors.New("unimplemented")
93+
}
94+
95+
func (d *DB) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) {
96+
panic("unimplemented")
97+
}
98+
99+
func (d *DB) ModifyAncients(f func(ethdb.AncientWriteOp) error) (int64, error) {
100+
return 0, errors.New("unimplemented")
101+
}
102+
103+
func (d *DB) TruncateHead(n uint64) (uint64, error) {
104+
return 0, errors.New("unimplemented")
105+
}
106+
107+
func (d *DB) TruncateTail(n uint64) (uint64, error) {
108+
return 0, errors.New("unimplemented")
109+
}
110+
111+
func (d *DB) Sync() error {
112+
return errors.New("unimplemented")
113+
}
114+
115+
func (d *DB) MigrateTable(s string, f func([]byte) ([]byte, error)) error {
116+
return errors.New("unimplemented")
117+
}
118+
119+
func (d *DB) AncientDatadir() (string, error) {
120+
return "", errors.New("unimplemented")
121+
}
122+
123+
func (d *DB) WasmDataBase() (ethdb.KeyValueStore, uint32) {
124+
panic("unimplemented")
125+
}
126+
127+
func (d *DB) WasmTargets() []ethdb.WasmTarget {
128+
panic("unimplemented")
129+
}

cmd/mel-replay/main.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2025-2026, Offchain Labs, Inc.
2+
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md
3+
4+
package main
5+
6+
import (
7+
"github.com/ethereum/go-ethereum/common"
8+
9+
"github.com/offchainlabs/nitro/arbutil"
10+
)
11+
12+
type preimageResolver interface {
13+
ResolveTypedPreimage(preimageType arbutil.PreimageType, hash common.Hash) ([]byte, error)
14+
}
15+
16+
func main() {
17+
}

cmd/mel-replay/txs_fetcher.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/ethereum/go-ethereum/common/hexutil"
9+
"github.com/ethereum/go-ethereum/core/types"
10+
"github.com/ethereum/go-ethereum/rlp"
11+
"github.com/ethereum/go-ethereum/trie"
12+
"github.com/ethereum/go-ethereum/triedb"
13+
)
14+
15+
type txsFetcherForBlock struct {
16+
header *types.Header
17+
preimageResolver preimageResolver
18+
}
19+
20+
func (tf *txsFetcherForBlock) TransactionsByHeader(
21+
ctx context.Context,
22+
parentChainHeaderHash common.Hash,
23+
) (types.Transactions, error) {
24+
preimageDB := &DB{
25+
resolver: tf.preimageResolver,
26+
}
27+
tdb := triedb.NewDatabase(preimageDB, nil)
28+
tr, err := trie.New(trie.TrieID(tf.header.TxHash), tdb)
29+
if err != nil {
30+
panic(err)
31+
}
32+
entries, indices := tf.collectTrieEntries(tr)
33+
rawTxs := tf.reconstructOrderedData(entries, indices)
34+
return tf.decodeTransactionData(rawTxs)
35+
}
36+
37+
func (btr *txsFetcherForBlock) collectTrieEntries(txTrie *trie.Trie) ([][]byte, []uint64) {
38+
nodeIterator, iterErr := txTrie.NodeIterator(nil)
39+
if iterErr != nil {
40+
panic(iterErr)
41+
}
42+
43+
var rawValues [][]byte
44+
var indexKeys []uint64
45+
46+
for nodeIterator.Next(true) {
47+
if !nodeIterator.Leaf() {
48+
continue
49+
}
50+
51+
leafKey := nodeIterator.LeafKey()
52+
var decodedIndex uint64
53+
54+
decodeErr := rlp.DecodeBytes(leafKey, &decodedIndex)
55+
if decodeErr != nil {
56+
panic(fmt.Errorf("key decoding error: %w", decodeErr))
57+
}
58+
59+
indexKeys = append(indexKeys, decodedIndex)
60+
rawValues = append(rawValues, nodeIterator.LeafBlob())
61+
}
62+
63+
return rawValues, indexKeys
64+
}
65+
66+
func (btr *txsFetcherForBlock) reconstructOrderedData(rawValues [][]byte, indices []uint64) []hexutil.Bytes {
67+
orderedData := make([]hexutil.Bytes, len(rawValues))
68+
for position, index := range indices {
69+
if index >= uint64(len(rawValues)) {
70+
panic(fmt.Sprintf("index out of bounds: %d", index))
71+
}
72+
if orderedData[index] != nil {
73+
panic(fmt.Sprintf("index collision detected: %d", index))
74+
}
75+
orderedData[index] = rawValues[position]
76+
}
77+
return orderedData
78+
}
79+
80+
func (btr *txsFetcherForBlock) decodeTransactionData(encodedData []hexutil.Bytes) (types.Transactions, error) {
81+
transactionList := make(types.Transactions, 0, len(encodedData))
82+
for _, encodedTx := range encodedData {
83+
decodedTx := new(types.Transaction)
84+
if decodeErr := decodedTx.UnmarshalBinary(encodedTx); decodeErr != nil {
85+
return nil, fmt.Errorf("transaction decoding failed: %w", decodeErr)
86+
}
87+
transactionList = append(transactionList, decodedTx)
88+
}
89+
return transactionList, nil
90+
}

cmd/mel-replay/txs_fetcher_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"math/big"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/ethereum/go-ethereum/common"
12+
"github.com/ethereum/go-ethereum/core/types"
13+
"github.com/ethereum/go-ethereum/crypto"
14+
"github.com/ethereum/go-ethereum/trie"
15+
16+
"github.com/offchainlabs/nitro/arbutil"
17+
)
18+
19+
func TestFetchTransactionsForBlockHeader_DynamicFeeTxs(t *testing.T) {
20+
ctx := context.Background()
21+
total := uint64(42)
22+
txes := make([]*types.Transaction, total)
23+
for i := uint64(0); i < total; i++ {
24+
txData := types.DynamicFeeTx{
25+
Nonce: i,
26+
To: nil,
27+
Gas: 21000,
28+
GasTipCap: big.NewInt(1),
29+
GasFeeCap: big.NewInt(1),
30+
}
31+
txes[i] = types.NewTx(&txData)
32+
}
33+
hasher := newRecordingHasher()
34+
txsRoot := types.DeriveSha(types.Transactions(txes), hasher)
35+
header := &types.Header{
36+
TxHash: txsRoot,
37+
}
38+
preimages := hasher.GetPreimages()
39+
mockPreimageResolver := &mockPreimageResolver{
40+
preimages: preimages,
41+
}
42+
txsFetcher := &txsFetcherForBlock{
43+
header: header,
44+
preimageResolver: mockPreimageResolver,
45+
}
46+
fetched, err := txsFetcher.TransactionsByHeader(ctx, header.Hash())
47+
require.NoError(t, err)
48+
require.True(t, uint64(len(fetched)) == total) // #nosec G115
49+
for i, tx := range fetched {
50+
require.Equal(t, txes[i].Hash(), tx.Hash())
51+
require.Equal(t, uint64(i), tx.Nonce()) // #nosec G115
52+
}
53+
}
54+
55+
func TestFetchTransactionsForBlockHeader_LegacyTxs(t *testing.T) {
56+
ctx := context.Background()
57+
total := uint64(42)
58+
txes := make([]*types.Transaction, total)
59+
for i := uint64(0); i < total; i++ {
60+
txes[i] = types.NewTransaction(i, common.Address{}, big.NewInt(0), 21000, big.NewInt(1), nil)
61+
}
62+
hasher := newRecordingHasher()
63+
txsRoot := types.DeriveSha(types.Transactions(txes), hasher)
64+
header := &types.Header{
65+
TxHash: txsRoot,
66+
}
67+
preimages := hasher.GetPreimages()
68+
mockPreimageResolver := &mockPreimageResolver{
69+
preimages: preimages,
70+
}
71+
txsFetcher := &txsFetcherForBlock{
72+
header: header,
73+
preimageResolver: mockPreimageResolver,
74+
}
75+
fetched, err := txsFetcher.TransactionsByHeader(ctx, header.Hash())
76+
require.NoError(t, err)
77+
require.True(t, uint64(len(fetched)) == total) // #nosec G115
78+
for i, tx := range fetched {
79+
require.Equal(t, txes[i].Hash(), tx.Hash())
80+
require.Equal(t, uint64(i), tx.Nonce()) // #nosec G115
81+
}
82+
}
83+
84+
type mockPreimageResolver struct {
85+
preimages map[common.Hash][]byte
86+
}
87+
88+
func (m *mockPreimageResolver) ResolveTypedPreimage(preimageType arbutil.PreimageType, hash common.Hash) ([]byte, error) {
89+
if preimage, exists := m.preimages[hash]; exists {
90+
return preimage, nil
91+
}
92+
return nil, fmt.Errorf("preimage not found for hash: %s", hash.Hex())
93+
}
94+
95+
// Implements a hasher that captures preimages of hashes as it computes them.
96+
type preimageRecordingHasher struct {
97+
trie *trie.StackTrie
98+
preimages map[common.Hash][]byte
99+
}
100+
101+
func newRecordingHasher() *preimageRecordingHasher {
102+
h := &preimageRecordingHasher{
103+
preimages: make(map[common.Hash][]byte),
104+
}
105+
// OnTrieNode callback captures all trie nodes.
106+
onTrieNode := func(path []byte, hash common.Hash, blob []byte) {
107+
// Deep copy the blob since the callback warns contents may change, so this is required.
108+
h.preimages[hash] = common.CopyBytes(blob)
109+
}
110+
111+
h.trie = trie.NewStackTrie(onTrieNode)
112+
return h
113+
}
114+
115+
func (h *preimageRecordingHasher) Reset() {
116+
onTrieNode := func(path []byte, hash common.Hash, blob []byte) {
117+
h.preimages[hash] = common.CopyBytes(blob)
118+
}
119+
h.trie = trie.NewStackTrie(onTrieNode)
120+
}
121+
122+
func (h *preimageRecordingHasher) Update(key, value []byte) error {
123+
valueHash := crypto.Keccak256Hash(value)
124+
h.preimages[valueHash] = common.CopyBytes(value)
125+
return h.trie.Update(key, value)
126+
}
127+
128+
func (h *preimageRecordingHasher) Hash() common.Hash {
129+
return h.trie.Hash()
130+
}
131+
132+
func (h *preimageRecordingHasher) GetPreimages() map[common.Hash][]byte {
133+
return h.preimages
134+
}

0 commit comments

Comments
 (0)