Skip to content

Commit

Permalink
Merge pull request ethereum#237 from ethereum-optimism/feat/depositNo…
Browse files Browse the repository at this point in the history
…nceCorrection

Snap Sync: DepositNonce Data Correction
  • Loading branch information
axelKingsley committed Feb 7, 2024
2 parents 70103aa + fdf0da8 commit 57bfac5
Show file tree
Hide file tree
Showing 12 changed files with 272 additions and 4 deletions.
8 changes: 6 additions & 2 deletions eth/downloader/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ type Downloader struct {
syncStartBlock uint64 // Head snap block when Geth was started
syncStartTime time.Time // Time instance when chain sync started
syncLogTime time.Time // Time instance when status was last reported

// Chain ID for downloaders to reference
chainID uint64
}

// LightChain encapsulates functions required to synchronise a light chain.
Expand Down Expand Up @@ -216,7 +219,7 @@ type BlockChain interface {
}

// New creates a new downloader to fetch hashes and blocks from remote peers.
func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchain LightChain, dropPeer peerDropFn, success func()) *Downloader {
func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchain LightChain, dropPeer peerDropFn, success func(), chainID uint64) *Downloader {
if lightchain == nil {
lightchain = chain
}
Expand All @@ -233,6 +236,7 @@ func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchai
SnapSyncer: snap.NewSyncer(stateDb, chain.TrieDB().Scheme()),
stateSyncStart: make(chan *stateSync),
syncStartBlock: chain.CurrentSnapBlock().Number.Uint64(),
chainID: chainID,
}
// Create the post-merge skeleton syncer and start the process
dl.skeleton = newSkeleton(stateDb, dl.peers, dropPeer, newBeaconBackfiller(dl, success))
Expand Down Expand Up @@ -1718,7 +1722,7 @@ func (d *Downloader) commitSnapSyncData(results []*fetchResult, stateSync *state
receipts := make([]types.Receipts, len(results))
for i, result := range results {
blocks[i] = types.NewBlockWithHeader(result.Header).WithBody(result.Transactions, result.Uncles).WithWithdrawals(result.Withdrawals)
receipts[i] = result.Receipts
receipts[i] = correctReceipts(result.Receipts, result.Transactions, blocks[i].NumberU64(), d.chainID)
}
if index, err := d.blockchain.InsertReceiptChain(blocks, receipts, d.ancientLimit); err != nil {
log.Debug("Downloaded item processing failed", "number", results[index].Header.Number, "hash", results[index].Header.Hash(), "err", err)
Expand Down
2 changes: 1 addition & 1 deletion eth/downloader/downloader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func newTesterWithNotification(t *testing.T, success func()) *downloadTester {
chain: chain,
peers: make(map[string]*downloadTesterPeer),
}
tester.downloader = New(db, new(event.TypeMux), tester.chain, nil, tester.dropPeer, success)
tester.downloader = New(db, new(event.TypeMux), tester.chain, nil, tester.dropPeer, success, 0)
return tester
}

Expand Down
144 changes: 144 additions & 0 deletions eth/downloader/receiptreference.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package downloader

import (
"bytes"
"embed"
"encoding/gob"
"fmt"
"path"
"strings"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)

// userDepositNonces is a struct to hold the reference data for user deposits
// The reference data is used to correct the deposit nonce in the receipts
type userDepositNonces struct {
ChainID uint64
First uint64
Last uint64 // non inclusive
Results map[uint64][]uint64
}

var (
systemAddress = common.HexToAddress("0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001")
receiptReferencePath = "userDepositData"
//go:embed userDepositData/*.gob
receiptReference embed.FS
userDepositNoncesAlreadySearched = map[uint64]bool{}
userDepositNoncesReference = map[uint64]userDepositNonces{}
)

// lazy load the reference data for the requested chain
// if this chain data was already requested, returns early
func initReceiptReferences(chainID uint64) {
// if already loaded, return
if userDepositNoncesAlreadySearched[chainID] {
return
}
// look for a file prefixed by the chainID
fPrefix := fmt.Sprintf("%d.", chainID)
files, err := receiptReference.ReadDir(receiptReferencePath)
if err != nil {
log.Warn("Receipt Correction: Failed to read reference directory", "err", err)
return
}
// mark as loaded so we don't try again, even if no files match
userDepositNoncesAlreadySearched[chainID] = true
for _, file := range files {
// skip files which don't match the prefix
if !strings.HasPrefix(file.Name(), fPrefix) {
continue
}
fpath := path.Join(receiptReferencePath, file.Name())
bs, err := receiptReference.ReadFile(fpath)
if err != nil {
log.Warn("Receipt Correction: Failed to read reference data", "err", err)
continue
}
udns := userDepositNonces{}
err = gob.NewDecoder(bytes.NewReader(bs)).Decode(&udns)
if err != nil {
log.Warn("Receipt Correction: Failed to decode reference data", "err", err)
continue
}
userDepositNoncesReference[udns.ChainID] = udns
return
}
}

// correctReceipts corrects the deposit nonce in the receipts using the reference data
// prior to Canyon Hard Fork, DepositNonces were not cryptographically verifiable.
// As a consequence, the deposit nonces found during Snap Sync may be incorrect.
// This function inspects transaction data for user deposits, and if it is found to be incorrect, it is corrected.
// The data used to correct the deposit nonce is stored in the userDepositData directory,
// and was generated with the receipt reference tool in the optimism monorepo.
func correctReceipts(receipts types.Receipts, transactions types.Transactions, blockNumber uint64, chainID uint64) types.Receipts {
initReceiptReferences(chainID)
// if there is no data even after initialization, return the receipts as is
depositNoncesForChain, ok := userDepositNoncesReference[chainID]
if !ok {
log.Trace("Receipt Correction: No data source for chain", "chainID", chainID)
return receipts
}

// check that the block number being examined is within the range of the reference data
if blockNumber < depositNoncesForChain.First || blockNumber > depositNoncesForChain.Last {
log.Trace("Receipt Correction: Block is out of range for receipt reference",
"blockNumber", blockNumber,
"start", depositNoncesForChain.First,
"end", depositNoncesForChain.Last)
return receipts
}

// get the block nonces
blockNonces, ok := depositNoncesForChain.Results[blockNumber]
if !ok {
log.Trace("Receipt Correction: Block does not contain user deposits", "blockNumber", blockNumber)
return receipts
}

// iterate through the receipts and transactions to correct the deposit nonce
// user deposits should always be at the front of the block, but we will check all transactions to be sure
udCount := 0
for i := 0; i < len(receipts); i++ {
r := receipts[i]
tx := transactions[i]
from, err := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx)
if err != nil {
log.Warn("Receipt Correction: Failed to determine sender", "err", err)
continue
}
// break as soon as a non deposit is found
if r.Type != types.DepositTxType {
break
}
// ignore any transactions from the system address
if from != systemAddress {
// prevent index out of range (indicates a problem with the reference data or the block data)
if udCount >= len(blockNonces) {
log.Warn("Receipt Correction: More user deposits in block than included in reference data", "in_reference", len(blockNonces))
break
}
nonce := blockNonces[udCount]
udCount++
log.Trace("Receipt Correction: User Deposit detected", "address", from, "nonce", nonce)
if nonce != *r.DepositNonce {
// correct the deposit nonce
// warn because this should not happen unless the data was modified by corruption or a malicious peer
// by correcting the nonce, the entire block is still valid for use
log.Warn("Receipt Correction: Corrected deposit nonce", "nonce", *r.DepositNonce, "corrected", nonce)
r.DepositNonce = &nonce
}
}
}
// check for unused reference data (indicates a problem with the reference data or the block data)
if udCount < len(blockNonces) {
log.Warn("Receipt Correction: More user deposits in reference data than found in block", "in_reference", len(blockNonces), "in_block", udCount)
}

log.Trace("Receipt Correction: Completed", "blockNumber", blockNumber, "userDeposits", udCount, "receipts", len(receipts), "transactions", len(transactions))
return receipts
}
108 changes: 108 additions & 0 deletions eth/downloader/receiptreference_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package downloader

import (
"testing"

"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/assert"
)

// makeCorrection is a helper function to create a slice of receipts and a slice of corrected receipts
func makeCorrection(bn uint64, cid uint64, ns []uint64, ty []uint8) (types.Receipts, types.Receipts) {
receipts := make(types.Receipts, len(ns))
correctedReceipts := make(types.Receipts, len(ns))
transactions := make(types.Transactions, len(ns))
for i := range ns {
receipts[i] = &types.Receipt{Type: ty[i], DepositNonce: &ns[i]}
correctedReceipts[i] = &types.Receipt{Type: ty[i], DepositNonce: &ns[i]}
transactions[i] = types.NewTx(&types.DepositTx{})
}

correctedReceipts = correctReceipts(correctedReceipts, transactions, bn, cid)

return receipts, correctedReceipts
}

func TestCorrectReceipts(t *testing.T) {
type testcase struct {
blockNum uint64
chainID uint64
nonces []uint64
txTypes []uint8
validate func(types.Receipts, types.Receipts)
}

// Tests use the real reference data, so block numbers and chainIDs are selected for different test cases
testcases := []testcase{
// Test case 1: No receipts
{
blockNum: 6825767,
chainID: 420,
nonces: []uint64{},
txTypes: []uint8{},
validate: func(receipts types.Receipts, correctedReceipts types.Receipts) {
assert.Empty(t, correctedReceipts)
},
},
// Test case 2: No deposits
{
blockNum: 6825767,
chainID: 420,
nonces: []uint64{1, 2, 3},
txTypes: []uint8{1, 1, 1},
validate: func(receipts types.Receipts, correctedReceipts types.Receipts) {
assert.Equal(t, receipts, correctedReceipts)
},
},
// Test case 3: all deposits with no correction
{
blockNum: 8835769,
chainID: 420,
nonces: []uint64{78756, 78757, 78758, 78759, 78760, 78761, 78762, 78763, 78764},
txTypes: []uint8{126, 126, 126, 126, 126, 126, 126, 126, 126},
validate: func(receipts types.Receipts, correctedReceipts types.Receipts) {
assert.Equal(t, receipts, correctedReceipts)
},
},
// Test case 4: all deposits with a correction
{
blockNum: 8835769,
chainID: 420,
nonces: []uint64{78756, 78757, 78758, 12345, 78760, 78761, 78762, 78763, 78764},
txTypes: []uint8{126, 126, 126, 126, 126, 126, 126, 126, 126},
validate: func(receipts types.Receipts, correctedReceipts types.Receipts) {
assert.NotEqual(t, receipts[3], correctedReceipts[3])
for i := range receipts {
if i != 3 {
assert.Equal(t, receipts[i], correctedReceipts[i])
}
}
},
},
// Test case 5: deposits with several corrections and non-deposits
{
blockNum: 8835769,
chainID: 420,
nonces: []uint64{0, 1, 2, 78759, 78760, 78761, 6, 78763, 78764, 9, 10, 11},
txTypes: []uint8{126, 126, 126, 126, 126, 126, 126, 126, 126, 1, 1, 1},
validate: func(receipts types.Receipts, correctedReceipts types.Receipts) {
// indexes 0, 1, 2, 6 were modified
// indexes 9, 10, 11 were added too, but they are not user deposits
assert.NotEqual(t, receipts[0], correctedReceipts[0])
assert.NotEqual(t, receipts[1], correctedReceipts[1])
assert.NotEqual(t, receipts[2], correctedReceipts[2])
assert.NotEqual(t, receipts[6], correctedReceipts[6])
for i := range receipts {
if i != 0 && i != 1 && i != 2 && i != 6 {
assert.Equal(t, receipts[i], correctedReceipts[i])
}
}
},
},
}

for _, tc := range testcases {
receipts, correctedReceipts := makeCorrection(tc.blockNum, tc.chainID, tc.nonces, tc.txTypes)
tc.validate(receipts, correctedReceipts)
}
}
Binary file not shown.
Binary file added eth/downloader/userDepositData/291.0-4192087.gob
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
8 changes: 7 additions & 1 deletion eth/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,14 @@ func newHandler(config *handlerConfig) (*handler, error) {
if h.snapSync.Load() && config.Chain.Snapshots() == nil {
return nil, errors.New("snap sync not supported with snapshots disabled")
}
// if the chainID is set, pass it to the downloader for use in sync
// this might not be set in tests
var chainID uint64
if cid := h.chain.Config().ChainID; cid != nil {
chainID = cid.Uint64()
}
// Construct the downloader (long sync)
h.downloader = downloader.New(config.Database, h.eventMux, h.chain, nil, h.removePeer, h.enableSyncedFeatures)
h.downloader = downloader.New(config.Database, h.eventMux, h.chain, nil, h.removePeer, h.enableSyncedFeatures, chainID)
if ttd := h.chain.Config().TerminalTotalDifficulty; ttd != nil {
if h.chain.Config().TerminalTotalDifficultyPassed {
log.Info("Chain post-merge, sync via beacon client")
Expand Down
6 changes: 6 additions & 0 deletions fork.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ def:
globs:
- "core/blockchain_reader.go"
- "eth/protocols/snap/handler.go"
- title: Historical data for Snap-sync
description: Snap-sync has access to trusted Deposit Transaction Nonce Data.
globs:
- "eth/handler.go"
- "eth/downloader/downloader.go"
- "eth/downloader/receiptreference.go"
- title: Discv5 node discovery
description: Fix discv5 option to allow discv5 to be an active source for node-discovery.
globs:
Expand Down

0 comments on commit 57bfac5

Please sign in to comment.