Skip to content

Commit d878723

Browse files
committed
eth, les, light: enforce CHT checkpoints on fast-sync too
1 parent cdae1c5 commit d878723

File tree

11 files changed

+212
-115
lines changed

11 files changed

+212
-115
lines changed

cmd/geth/chaincmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ func copyDb(ctx *cli.Context) error {
372372
chain, chainDb := utils.MakeChain(ctx, stack)
373373

374374
syncmode := *utils.GlobalTextMarshaler(ctx, utils.SyncModeFlag.Name).(*downloader.SyncMode)
375-
dl := downloader.New(syncmode, chainDb, new(event.TypeMux), chain, nil, nil)
375+
dl := downloader.New(syncmode, 0, chainDb, new(event.TypeMux), chain, nil, nil)
376376

377377
// Create a source peer to satisfy downloader requests from
378378
db, err := ethdb.NewLDBDatabase(ctx.Args().First(), ctx.GlobalInt(utils.CacheFlag.Name), 256)

eth/downloader/downloader.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ var (
7575
errUnknownPeer = errors.New("peer is unknown or unhealthy")
7676
errBadPeer = errors.New("action from bad peer ignored")
7777
errStallingPeer = errors.New("peer is stalling")
78+
errUnsyncedPeer = errors.New("unsynced peer")
7879
errNoPeers = errors.New("no peers to keep download active")
7980
errTimeout = errors.New("timeout")
8081
errEmptyHeaderSet = errors.New("empty header set by peer")
@@ -99,10 +100,11 @@ type Downloader struct {
99100
mode SyncMode // Synchronisation mode defining the strategy used (per sync cycle)
100101
mux *event.TypeMux // Event multiplexer to announce sync operation events
101102

102-
genesis uint64 // Genesis block number to limit sync to (e.g. light client CHT)
103-
queue *queue // Scheduler for selecting the hashes to download
104-
peers *peerSet // Set of active peers from which download can proceed
105-
stateDB ethdb.Database
103+
checkpoint uint64 // Checkpoint block number to enforce head against (e.g. fast sync)
104+
genesis uint64 // Genesis block number to limit sync to (e.g. light client CHT)
105+
queue *queue // Scheduler for selecting the hashes to download
106+
peers *peerSet // Set of active peers from which download can proceed
107+
stateDB ethdb.Database
106108

107109
rttEstimate uint64 // Round trip time to target for download requests
108110
rttConfidence uint64 // Confidence in the estimated RTT (unit: millionths to allow atomic ops)
@@ -205,15 +207,15 @@ type BlockChain interface {
205207
}
206208

207209
// New creates a new downloader to fetch hashes and blocks from remote peers.
208-
func New(mode SyncMode, stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchain LightChain, dropPeer peerDropFn) *Downloader {
210+
func New(mode SyncMode, checkpoint uint64, stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchain LightChain, dropPeer peerDropFn) *Downloader {
209211
if lightchain == nil {
210212
lightchain = chain
211213
}
212-
213214
dl := &Downloader{
214215
mode: mode,
215216
stateDB: stateDb,
216217
mux: mux,
218+
checkpoint: checkpoint,
217219
queue: newQueue(),
218220
peers: newPeerSet(),
219221
rttEstimate: uint64(rttMaxEstimate),
@@ -326,7 +328,7 @@ func (d *Downloader) Synchronise(id string, head common.Hash, td *big.Int, mode
326328
case nil:
327329
case errBusy:
328330

329-
case errTimeout, errBadPeer, errStallingPeer,
331+
case errTimeout, errBadPeer, errStallingPeer, errUnsyncedPeer,
330332
errEmptyHeaderSet, errPeersUnavailable, errTooOld,
331333
errInvalidAncestor, errInvalidChain:
332334
log.Warn("Synchronisation failed, dropping peer", "peer", id, "err", err)
@@ -577,6 +579,10 @@ func (d *Downloader) fetchHeight(p *peerConnection) (*types.Header, error) {
577579
return nil, errBadPeer
578580
}
579581
head := headers[0]
582+
if d.mode == FastSync && head.Number.Uint64() < d.checkpoint {
583+
p.log.Warn("Remote head below checkpoint", "number", head.Number, "hash", head.Hash())
584+
return nil, errUnsyncedPeer
585+
}
580586
p.log.Debug("Remote head header identified", "number", head.Number, "hash", head.Hash())
581587
return head, nil
582588

eth/downloader/downloader_test.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
"testing"
2727
"time"
2828

29-
ethereum "github.com/ethereum/go-ethereum"
29+
"github.com/ethereum/go-ethereum"
3030
"github.com/ethereum/go-ethereum/common"
3131
"github.com/ethereum/go-ethereum/core/types"
3232
"github.com/ethereum/go-ethereum/ethdb"
@@ -73,7 +73,8 @@ func newTester() *downloadTester {
7373
}
7474
tester.stateDb = ethdb.NewMemDatabase()
7575
tester.stateDb.Put(testGenesis.Root().Bytes(), []byte{0x00})
76-
tester.downloader = New(FullSync, tester.stateDb, new(event.TypeMux), tester, nil, tester.dropPeer)
76+
77+
tester.downloader = New(FullSync, 0, tester.stateDb, new(event.TypeMux), tester, nil, tester.dropPeer)
7778
return tester
7879
}
7980

@@ -1049,6 +1050,7 @@ func testBlockHeaderAttackerDropping(t *testing.T, protocol int) {
10491050
{errUnknownPeer, false}, // Peer is unknown, was already dropped, don't double drop
10501051
{errBadPeer, true}, // Peer was deemed bad for some reason, drop it
10511052
{errStallingPeer, true}, // Peer was detected to be stalling, drop it
1053+
{errUnsyncedPeer, true}, // Peer was detected to be unsynced, drop it
10521054
{errNoPeers, false}, // No peers to download from, soft race, no issue
10531055
{errTimeout, true}, // No hashes received in due time, drop the peer
10541056
{errEmptyHeaderSet, true}, // No headers were returned as a response, drop as it's a dead end
@@ -1567,3 +1569,39 @@ func TestRemoteHeaderRequestSpan(t *testing.T) {
15671569
}
15681570
}
15691571
}
1572+
1573+
// Tests that peers below a pre-configured checkpoint block are prevented from
1574+
// being fast-synced from, avoiding potential cheap eclipse attacks.
1575+
func TestCheckpointEnforcement62(t *testing.T) { testCheckpointEnforcement(t, 62, FullSync) }
1576+
func TestCheckpointEnforcement63Full(t *testing.T) { testCheckpointEnforcement(t, 63, FullSync) }
1577+
func TestCheckpointEnforcement63Fast(t *testing.T) { testCheckpointEnforcement(t, 63, FastSync) }
1578+
func TestCheckpointEnforcement64Full(t *testing.T) { testCheckpointEnforcement(t, 64, FullSync) }
1579+
func TestCheckpointEnforcement64Fast(t *testing.T) { testCheckpointEnforcement(t, 64, FastSync) }
1580+
func TestCheckpointEnforcement64Light(t *testing.T) { testCheckpointEnforcement(t, 64, LightSync) }
1581+
1582+
func testCheckpointEnforcement(t *testing.T, protocol int, mode SyncMode) {
1583+
t.Parallel()
1584+
1585+
// Create a new tester with a particular hard coded checkpoint block
1586+
tester := newTester()
1587+
defer tester.terminate()
1588+
1589+
tester.downloader.checkpoint = uint64(fsMinFullBlocks) + 256
1590+
chain := testChainBase.shorten(int(tester.downloader.checkpoint) - 1)
1591+
1592+
// Attempt to sync with the peer and validate the result
1593+
tester.newPeer("peer", protocol, chain)
1594+
1595+
var expect error
1596+
if mode == FastSync {
1597+
expect = errUnsyncedPeer
1598+
}
1599+
if err := tester.sync("peer", nil, mode); err != expect {
1600+
t.Fatalf("block sync error mismatch: have %v, want %v", err, expect)
1601+
}
1602+
if mode == FastSync {
1603+
assertOwnChain(t, tester, 1)
1604+
} else {
1605+
assertOwnChain(t, tester, chain.len())
1606+
}
1607+
}

eth/handler.go

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828

2929
"github.com/ethereum/go-ethereum/common"
3030
"github.com/ethereum/go-ethereum/consensus"
31-
"github.com/ethereum/go-ethereum/consensus/misc"
3231
"github.com/ethereum/go-ethereum/core"
3332
"github.com/ethereum/go-ethereum/core/types"
3433
"github.com/ethereum/go-ethereum/eth/downloader"
@@ -55,7 +54,7 @@ const (
5554
)
5655

5756
var (
58-
daoChallengeTimeout = 15 * time.Second // Time allowance for a node to reply to the DAO handshake challenge
57+
syncChallengeTimeout = 15 * time.Second // Time allowance for a node to reply to the sync progress challenge
5958
)
6059

6160
// errIncompatibleConfig is returned if the requested protocols and configs are
@@ -72,6 +71,9 @@ type ProtocolManager struct {
7271
fastSync uint32 // Flag whether fast sync is enabled (gets disabled if we already have blocks)
7372
acceptTxs uint32 // Flag whether we're considered synchronised (enables transaction processing)
7473

74+
checkpointNumber uint64 // Block number for the sync progress validator to cross reference
75+
checkpointHash common.Hash // Block hash for the sync progress validator to cross reference
76+
7577
txpool txPool
7678
blockchain *core.BlockChain
7779
chainconfig *params.ChainConfig
@@ -126,6 +128,11 @@ func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, ne
126128
if mode == downloader.FastSync {
127129
manager.fastSync = uint32(1)
128130
}
131+
// If we have trusted checkpoints, enforce them on the chain
132+
if checkpoint, ok := params.TrustedCheckpoints[blockchain.Genesis().Hash()]; ok {
133+
manager.checkpointNumber = (checkpoint.SectionIndex+1)*params.CHTFrequencyClient - 1
134+
manager.checkpointHash = checkpoint.SectionHead
135+
}
129136
// Initiate a sub-protocol for every implemented version we can handle
130137
manager.SubProtocols = make([]p2p.Protocol, 0, len(ProtocolVersions))
131138
for i, version := range ProtocolVersions {
@@ -165,7 +172,7 @@ func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, ne
165172
return nil, errIncompatibleConfig
166173
}
167174
// Construct the different synchronisation mechanisms
168-
manager.downloader = downloader.New(mode, chaindb, manager.eventMux, blockchain, nil, manager.removePeer)
175+
manager.downloader = downloader.New(mode, manager.checkpointNumber, chaindb, manager.eventMux, blockchain, nil, manager.removePeer)
169176

170177
validator := func(header *types.Header) error {
171178
return engine.VerifyHeader(blockchain, header, true)
@@ -291,22 +298,22 @@ func (pm *ProtocolManager) handle(p *peer) error {
291298
// after this will be sent via broadcasts.
292299
pm.syncTransactions(p)
293300

294-
// If we're DAO hard-fork aware, validate any remote peer with regard to the hard-fork
295-
if daoBlock := pm.chainconfig.DAOForkBlock; daoBlock != nil {
296-
// Request the peer's DAO fork header for extra-data validation
297-
if err := p.RequestHeadersByNumber(daoBlock.Uint64(), 1, 0, false); err != nil {
301+
// If we have a trusted CHT, reject all peers below that (avoid fast sync eclipse)
302+
if pm.checkpointHash != (common.Hash{}) {
303+
// Request the peer's checkpoint header for chain height/weight validation
304+
if err := p.RequestHeadersByNumber(pm.checkpointNumber, 1, 0, false); err != nil {
298305
return err
299306
}
300307
// Start a timer to disconnect if the peer doesn't reply in time
301-
p.forkDrop = time.AfterFunc(daoChallengeTimeout, func() {
302-
p.Log().Debug("Timed out DAO fork-check, dropping")
308+
p.syncDrop = time.AfterFunc(syncChallengeTimeout, func() {
309+
p.Log().Warn("Checkpoint challenge timed out, dropping", "addr", p.RemoteAddr(), "type", p.Name())
303310
pm.removePeer(p.id)
304311
})
305312
// Make sure it's cleaned up if the peer dies off
306313
defer func() {
307-
if p.forkDrop != nil {
308-
p.forkDrop.Stop()
309-
p.forkDrop = nil
314+
if p.syncDrop != nil {
315+
p.syncDrop.Stop()
316+
p.syncDrop = nil
310317
}
311318
}()
312319
}
@@ -438,41 +445,33 @@ func (pm *ProtocolManager) handleMsg(p *peer) error {
438445
if err := msg.Decode(&headers); err != nil {
439446
return errResp(ErrDecode, "msg %v: %v", msg, err)
440447
}
441-
// If no headers were received, but we're expending a DAO fork check, maybe it's that
442-
if len(headers) == 0 && p.forkDrop != nil {
443-
// Possibly an empty reply to the fork header checks, sanity check TDs
444-
verifyDAO := true
445-
446-
// If we already have a DAO header, we can check the peer's TD against it. If
447-
// the peer's ahead of this, it too must have a reply to the DAO check
448-
if daoHeader := pm.blockchain.GetHeaderByNumber(pm.chainconfig.DAOForkBlock.Uint64()); daoHeader != nil {
449-
if _, td := p.Head(); td.Cmp(pm.blockchain.GetTd(daoHeader.Hash(), daoHeader.Number.Uint64())) >= 0 {
450-
verifyDAO = false
451-
}
452-
}
453-
// If we're seemingly on the same chain, disable the drop timer
454-
if verifyDAO {
455-
p.Log().Debug("Seems to be on the same side of the DAO fork")
456-
p.forkDrop.Stop()
457-
p.forkDrop = nil
458-
return nil
448+
// If no headers were received, but we're expencting a checkpoint header, consider it that
449+
if len(headers) == 0 && p.syncDrop != nil {
450+
// Stop the timer either way, decide later to drop or not
451+
p.syncDrop.Stop()
452+
p.syncDrop = nil
453+
454+
// If we're doing a fast sync, we must enforce the checkpoint block to avoid
455+
// eclipse attacks. Unsynced nodes are welcome to connect after we're done
456+
// joining the network
457+
if atomic.LoadUint32(&pm.fastSync) == 1 {
458+
p.Log().Warn("Dropping unsynced node during fast sync", "addr", p.RemoteAddr(), "type", p.Name())
459+
return errors.New("unsynced node cannot serve fast sync")
459460
}
460461
}
461462
// Filter out any explicitly requested headers, deliver the rest to the downloader
462463
filter := len(headers) == 1
463464
if filter {
464-
// If it's a potential DAO fork check, validate against the rules
465-
if p.forkDrop != nil && pm.chainconfig.DAOForkBlock.Cmp(headers[0].Number) == 0 {
466-
// Disable the fork drop timer
467-
p.forkDrop.Stop()
468-
p.forkDrop = nil
465+
// If it's a potential sync progress check, validate the content and advertised chain weight
466+
if p.syncDrop != nil && headers[0].Number.Uint64() == pm.checkpointNumber {
467+
// Disable the sync drop timer
468+
p.syncDrop.Stop()
469+
p.syncDrop = nil
469470

470471
// Validate the header and either drop the peer or continue
471-
if err := misc.VerifyDAOHeaderExtraData(pm.chainconfig, headers[0]); err != nil {
472-
p.Log().Debug("Verified to be on the other side of the DAO fork, dropping")
473-
return err
472+
if headers[0].Hash() != pm.checkpointHash {
473+
return errors.New("checkpoint hash mismatch")
474474
}
475-
p.Log().Debug("Verified to be on the same side of the DAO fork")
476475
return nil
477476
}
478477
// Otherwise if it's a whitelisted block, validate against the set

0 commit comments

Comments
 (0)