Skip to content

Commit 8afbcf4

Browse files
authored
eth: enforce announcement metadatas and drop peers violating the protocol (#28261)
* eth: enforce announcement metadatas and drop peers violating the protocol * eth/fetcher: relax eth/68 validation a bit for flakey clients * tests/fuzzers/txfetcher: pull in suggestion from Marius * eth/fetcher: add tests for peer dropping * eth/fetcher: linter linter linter linter linter
1 parent 6505297 commit 8afbcf4

File tree

5 files changed

+531
-65
lines changed

5 files changed

+531
-65
lines changed

eth/fetcher/tx_fetcher.go

Lines changed: 96 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"bytes"
2121
"errors"
2222
"fmt"
23+
"math"
2324
mrand "math/rand"
2425
"sort"
2526
"time"
@@ -105,6 +106,14 @@ var (
105106
type txAnnounce struct {
106107
origin string // Identifier of the peer originating the notification
107108
hashes []common.Hash // Batch of transaction hashes being announced
109+
metas []*txMetadata // Batch of metadatas associated with the hashes (nil before eth/68)
110+
}
111+
112+
// txMetadata is a set of extra data transmitted along the announcement for better
113+
// fetch scheduling.
114+
type txMetadata struct {
115+
kind byte // Transaction consensus type
116+
size uint32 // Transaction size in bytes
108117
}
109118

110119
// txRequest represents an in-flight transaction retrieval request destined to
@@ -120,6 +129,7 @@ type txRequest struct {
120129
type txDelivery struct {
121130
origin string // Identifier of the peer originating the notification
122131
hashes []common.Hash // Batch of transaction hashes having been delivered
132+
metas []txMetadata // Batch of metadatas associated with the delivered hashes
123133
direct bool // Whether this is a direct reply or a broadcast
124134
}
125135

@@ -155,14 +165,14 @@ type TxFetcher struct {
155165

156166
// Stage 1: Waiting lists for newly discovered transactions that might be
157167
// broadcast without needing explicit request/reply round trips.
158-
waitlist map[common.Hash]map[string]struct{} // Transactions waiting for an potential broadcast
159-
waittime map[common.Hash]mclock.AbsTime // Timestamps when transactions were added to the waitlist
160-
waitslots map[string]map[common.Hash]struct{} // Waiting announcements grouped by peer (DoS protection)
168+
waitlist map[common.Hash]map[string]struct{} // Transactions waiting for an potential broadcast
169+
waittime map[common.Hash]mclock.AbsTime // Timestamps when transactions were added to the waitlist
170+
waitslots map[string]map[common.Hash]*txMetadata // Waiting announcements grouped by peer (DoS protection)
161171

162172
// Stage 2: Queue of transactions that waiting to be allocated to some peer
163173
// to be retrieved directly.
164-
announces map[string]map[common.Hash]struct{} // Set of announced transactions, grouped by origin peer
165-
announced map[common.Hash]map[string]struct{} // Set of download locations, grouped by transaction hash
174+
announces map[string]map[common.Hash]*txMetadata // Set of announced transactions, grouped by origin peer
175+
announced map[common.Hash]map[string]struct{} // Set of download locations, grouped by transaction hash
166176

167177
// Stage 3: Set of transactions currently being retrieved, some which may be
168178
// fulfilled and some rescheduled. Note, this step shares 'announces' from the
@@ -175,6 +185,7 @@ type TxFetcher struct {
175185
hasTx func(common.Hash) bool // Retrieves a tx from the local txpool
176186
addTxs func([]*types.Transaction) []error // Insert a batch of transactions into local txpool
177187
fetchTxs func(string, []common.Hash) error // Retrieves a set of txs from a remote peer
188+
dropPeer func(string) // Drops a peer in case of announcement violation
178189

179190
step chan struct{} // Notification channel when the fetcher loop iterates
180191
clock mclock.Clock // Time wrapper to simulate in tests
@@ -183,14 +194,14 @@ type TxFetcher struct {
183194

184195
// NewTxFetcher creates a transaction fetcher to retrieve transaction
185196
// based on hash announcements.
186-
func NewTxFetcher(hasTx func(common.Hash) bool, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error) *TxFetcher {
187-
return NewTxFetcherForTests(hasTx, addTxs, fetchTxs, mclock.System{}, nil)
197+
func NewTxFetcher(hasTx func(common.Hash) bool, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string)) *TxFetcher {
198+
return NewTxFetcherForTests(hasTx, addTxs, fetchTxs, dropPeer, mclock.System{}, nil)
188199
}
189200

190201
// NewTxFetcherForTests is a testing method to mock out the realtime clock with
191202
// a simulated version and the internal randomness with a deterministic one.
192203
func NewTxFetcherForTests(
193-
hasTx func(common.Hash) bool, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error,
204+
hasTx func(common.Hash) bool, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string),
194205
clock mclock.Clock, rand *mrand.Rand) *TxFetcher {
195206
return &TxFetcher{
196207
notify: make(chan *txAnnounce),
@@ -199,8 +210,8 @@ func NewTxFetcherForTests(
199210
quit: make(chan struct{}),
200211
waitlist: make(map[common.Hash]map[string]struct{}),
201212
waittime: make(map[common.Hash]mclock.AbsTime),
202-
waitslots: make(map[string]map[common.Hash]struct{}),
203-
announces: make(map[string]map[common.Hash]struct{}),
213+
waitslots: make(map[string]map[common.Hash]*txMetadata),
214+
announces: make(map[string]map[common.Hash]*txMetadata),
204215
announced: make(map[common.Hash]map[string]struct{}),
205216
fetching: make(map[common.Hash]string),
206217
requests: make(map[string]*txRequest),
@@ -209,14 +220,15 @@ func NewTxFetcherForTests(
209220
hasTx: hasTx,
210221
addTxs: addTxs,
211222
fetchTxs: fetchTxs,
223+
dropPeer: dropPeer,
212224
clock: clock,
213225
rand: rand,
214226
}
215227
}
216228

217229
// Notify announces the fetcher of the potential availability of a new batch of
218230
// transactions in the network.
219-
func (f *TxFetcher) Notify(peer string, hashes []common.Hash) error {
231+
func (f *TxFetcher) Notify(peer string, types []byte, sizes []uint32, hashes []common.Hash) error {
220232
// Keep track of all the announced transactions
221233
txAnnounceInMeter.Mark(int64(len(hashes)))
222234

@@ -226,28 +238,35 @@ func (f *TxFetcher) Notify(peer string, hashes []common.Hash) error {
226238
// still valuable to check here because it runs concurrent to the internal
227239
// loop, so anything caught here is time saved internally.
228240
var (
229-
unknowns = make([]common.Hash, 0, len(hashes))
241+
unknownHashes = make([]common.Hash, 0, len(hashes))
242+
unknownMetas = make([]*txMetadata, 0, len(hashes))
243+
230244
duplicate int64
231245
underpriced int64
232246
)
233-
for _, hash := range hashes {
247+
for i, hash := range hashes {
234248
switch {
235249
case f.hasTx(hash):
236250
duplicate++
237251
case f.isKnownUnderpriced(hash):
238252
underpriced++
239253
default:
240-
unknowns = append(unknowns, hash)
254+
unknownHashes = append(unknownHashes, hash)
255+
if types == nil {
256+
unknownMetas = append(unknownMetas, nil)
257+
} else {
258+
unknownMetas = append(unknownMetas, &txMetadata{kind: types[i], size: sizes[i]})
259+
}
241260
}
242261
}
243262
txAnnounceKnownMeter.Mark(duplicate)
244263
txAnnounceUnderpricedMeter.Mark(underpriced)
245264

246265
// If anything's left to announce, push it into the internal loop
247-
if len(unknowns) == 0 {
266+
if len(unknownHashes) == 0 {
248267
return nil
249268
}
250-
announce := &txAnnounce{origin: peer, hashes: unknowns}
269+
announce := &txAnnounce{origin: peer, hashes: unknownHashes, metas: unknownMetas}
251270
select {
252271
case f.notify <- announce:
253272
return nil
@@ -290,6 +309,7 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
290309
// re-requesting them and dropping the peer in case of malicious transfers.
291310
var (
292311
added = make([]common.Hash, 0, len(txs))
312+
metas = make([]txMetadata, 0, len(txs))
293313
)
294314
// proceed in batches
295315
for i := 0; i < len(txs); i += 128 {
@@ -325,6 +345,10 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
325345
otherreject++
326346
}
327347
added = append(added, batch[j].Hash())
348+
metas = append(metas, txMetadata{
349+
kind: batch[j].Type(),
350+
size: uint32(batch[j].Size()),
351+
})
328352
}
329353
knownMeter.Mark(duplicate)
330354
underpricedMeter.Mark(underpriced)
@@ -337,7 +361,7 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
337361
}
338362
}
339363
select {
340-
case f.cleanup <- &txDelivery{origin: peer, hashes: added, direct: direct}:
364+
case f.cleanup <- &txDelivery{origin: peer, hashes: added, metas: metas, direct: direct}:
341365
return nil
342366
case <-f.quit:
343367
return errTerminated
@@ -394,13 +418,15 @@ func (f *TxFetcher) loop() {
394418
want := used + len(ann.hashes)
395419
if want > maxTxAnnounces {
396420
txAnnounceDOSMeter.Mark(int64(want - maxTxAnnounces))
421+
397422
ann.hashes = ann.hashes[:want-maxTxAnnounces]
423+
ann.metas = ann.metas[:want-maxTxAnnounces]
398424
}
399425
// All is well, schedule the remainder of the transactions
400426
idleWait := len(f.waittime) == 0
401427
_, oldPeer := f.announces[ann.origin]
402428

403-
for _, hash := range ann.hashes {
429+
for i, hash := range ann.hashes {
404430
// If the transaction is already downloading, add it to the list
405431
// of possible alternates (in case the current retrieval fails) and
406432
// also account it for the peer.
@@ -409,9 +435,9 @@ func (f *TxFetcher) loop() {
409435

410436
// Stage 2 and 3 share the set of origins per tx
411437
if announces := f.announces[ann.origin]; announces != nil {
412-
announces[hash] = struct{}{}
438+
announces[hash] = ann.metas[i]
413439
} else {
414-
f.announces[ann.origin] = map[common.Hash]struct{}{hash: {}}
440+
f.announces[ann.origin] = map[common.Hash]*txMetadata{hash: ann.metas[i]}
415441
}
416442
continue
417443
}
@@ -422,22 +448,28 @@ func (f *TxFetcher) loop() {
422448

423449
// Stage 2 and 3 share the set of origins per tx
424450
if announces := f.announces[ann.origin]; announces != nil {
425-
announces[hash] = struct{}{}
451+
announces[hash] = ann.metas[i]
426452
} else {
427-
f.announces[ann.origin] = map[common.Hash]struct{}{hash: {}}
453+
f.announces[ann.origin] = map[common.Hash]*txMetadata{hash: ann.metas[i]}
428454
}
429455
continue
430456
}
431457
// If the transaction is already known to the fetcher, but not
432458
// yet downloading, add the peer as an alternate origin in the
433459
// waiting list.
434460
if f.waitlist[hash] != nil {
461+
// Ignore double announcements from the same peer. This is
462+
// especially important if metadata is also passed along to
463+
// prevent malicious peers flip-flopping good/bad values.
464+
if _, ok := f.waitlist[hash][ann.origin]; ok {
465+
continue
466+
}
435467
f.waitlist[hash][ann.origin] = struct{}{}
436468

437469
if waitslots := f.waitslots[ann.origin]; waitslots != nil {
438-
waitslots[hash] = struct{}{}
470+
waitslots[hash] = ann.metas[i]
439471
} else {
440-
f.waitslots[ann.origin] = map[common.Hash]struct{}{hash: {}}
472+
f.waitslots[ann.origin] = map[common.Hash]*txMetadata{hash: ann.metas[i]}
441473
}
442474
continue
443475
}
@@ -446,9 +478,9 @@ func (f *TxFetcher) loop() {
446478
f.waittime[hash] = f.clock.Now()
447479

448480
if waitslots := f.waitslots[ann.origin]; waitslots != nil {
449-
waitslots[hash] = struct{}{}
481+
waitslots[hash] = ann.metas[i]
450482
} else {
451-
f.waitslots[ann.origin] = map[common.Hash]struct{}{hash: {}}
483+
f.waitslots[ann.origin] = map[common.Hash]*txMetadata{hash: ann.metas[i]}
452484
}
453485
}
454486
// If a new item was added to the waitlist, schedule it into the fetcher
@@ -474,9 +506,9 @@ func (f *TxFetcher) loop() {
474506
f.announced[hash] = f.waitlist[hash]
475507
for peer := range f.waitlist[hash] {
476508
if announces := f.announces[peer]; announces != nil {
477-
announces[hash] = struct{}{}
509+
announces[hash] = f.waitslots[peer][hash]
478510
} else {
479-
f.announces[peer] = map[common.Hash]struct{}{hash: {}}
511+
f.announces[peer] = map[common.Hash]*txMetadata{hash: f.waitslots[peer][hash]}
480512
}
481513
delete(f.waitslots[peer], hash)
482514
if len(f.waitslots[peer]) == 0 {
@@ -545,10 +577,27 @@ func (f *TxFetcher) loop() {
545577

546578
case delivery := <-f.cleanup:
547579
// Independent if the delivery was direct or broadcast, remove all
548-
// traces of the hash from internal trackers
549-
for _, hash := range delivery.hashes {
580+
// traces of the hash from internal trackers. That said, compare any
581+
// advertised metadata with the real ones and drop bad peers.
582+
for i, hash := range delivery.hashes {
550583
if _, ok := f.waitlist[hash]; ok {
551584
for peer, txset := range f.waitslots {
585+
if meta := txset[hash]; meta != nil {
586+
if delivery.metas[i].kind != meta.kind {
587+
log.Warn("Announced transaction type mismatch", "peer", peer, "tx", hash, "type", delivery.metas[i].kind, "ann", meta.kind)
588+
f.dropPeer(peer)
589+
} else if delivery.metas[i].size != meta.size {
590+
log.Warn("Announced transaction size mismatch", "peer", peer, "tx", hash, "size", delivery.metas[i].size, "ann", meta.size)
591+
if math.Abs(float64(delivery.metas[i].size)-float64(meta.size)) > 8 {
592+
// Normally we should drop a peer considering this is a protocol violation.
593+
// However, due to the RLP vs consensus format messyness, allow a few bytes
594+
// wiggle-room where we only warn, but don't drop.
595+
//
596+
// TODO(karalabe): Get rid of this relaxation when clients are proven stable.
597+
f.dropPeer(peer)
598+
}
599+
}
600+
}
552601
delete(txset, hash)
553602
if len(txset) == 0 {
554603
delete(f.waitslots, peer)
@@ -558,6 +607,22 @@ func (f *TxFetcher) loop() {
558607
delete(f.waittime, hash)
559608
} else {
560609
for peer, txset := range f.announces {
610+
if meta := txset[hash]; meta != nil {
611+
if delivery.metas[i].kind != meta.kind {
612+
log.Warn("Announced transaction type mismatch", "peer", peer, "tx", hash, "type", delivery.metas[i].kind, "ann", meta.kind)
613+
f.dropPeer(peer)
614+
} else if delivery.metas[i].size != meta.size {
615+
log.Warn("Announced transaction size mismatch", "peer", peer, "tx", hash, "size", delivery.metas[i].size, "ann", meta.size)
616+
if math.Abs(float64(delivery.metas[i].size)-float64(meta.size)) > 8 {
617+
// Normally we should drop a peer considering this is a protocol violation.
618+
// However, due to the RLP vs consensus format messyness, allow a few bytes
619+
// wiggle-room where we only warn, but don't drop.
620+
//
621+
// TODO(karalabe): Get rid of this relaxation when clients are proven stable.
622+
f.dropPeer(peer)
623+
}
624+
}
625+
}
561626
delete(txset, hash)
562627
if len(txset) == 0 {
563628
delete(f.announces, peer)
@@ -859,7 +924,7 @@ func (f *TxFetcher) forEachPeer(peers map[string]struct{}, do func(peer string))
859924

860925
// forEachHash does a range loop over a map of hashes in production, but during
861926
// testing it does a deterministic sorted random to allow reproducing issues.
862-
func (f *TxFetcher) forEachHash(hashes map[common.Hash]struct{}, do func(hash common.Hash) bool) {
927+
func (f *TxFetcher) forEachHash(hashes map[common.Hash]*txMetadata, do func(hash common.Hash) bool) {
863928
// If we're running production, use whatever Go's map gives us
864929
if f.rand == nil {
865930
for hash := range hashes {

0 commit comments

Comments
 (0)