|
17 | 17 | package eth |
18 | 18 |
|
19 | 19 | import ( |
| 20 | + "cmp" |
| 21 | + crand "crypto/rand" |
20 | 22 | "errors" |
21 | 23 | "maps" |
22 | 24 | "math" |
23 | | - "math/big" |
24 | 25 | "slices" |
25 | 26 | "sync" |
26 | 27 | "sync/atomic" |
27 | 28 | "time" |
28 | 29 |
|
| 30 | + "github.com/dchest/siphash" |
29 | 31 | "github.com/ethereum/go-ethereum/common" |
30 | 32 | "github.com/ethereum/go-ethereum/core" |
31 | 33 | "github.com/ethereum/go-ethereum/core/rawdb" |
32 | 34 | "github.com/ethereum/go-ethereum/core/txpool" |
33 | 35 | "github.com/ethereum/go-ethereum/core/types" |
34 | | - "github.com/ethereum/go-ethereum/crypto" |
35 | 36 | "github.com/ethereum/go-ethereum/eth/downloader" |
36 | 37 | "github.com/ethereum/go-ethereum/eth/ethconfig" |
37 | 38 | "github.com/ethereum/go-ethereum/eth/fetcher" |
@@ -119,9 +120,10 @@ type handler struct { |
119 | 120 | chain *core.BlockChain |
120 | 121 | maxPeers int |
121 | 122 |
|
122 | | - downloader *downloader.Downloader |
123 | | - txFetcher *fetcher.TxFetcher |
124 | | - peers *peerSet |
| 123 | + downloader *downloader.Downloader |
| 124 | + txFetcher *fetcher.TxFetcher |
| 125 | + peers *peerSet |
| 126 | + txBroadcastKey [16]byte |
125 | 127 |
|
126 | 128 | eventMux *event.TypeMux |
127 | 129 | txsCh chan core.NewTxsEvent |
@@ -153,6 +155,7 @@ func newHandler(config *handlerConfig) (*handler, error) { |
153 | 155 | txpool: config.TxPool, |
154 | 156 | chain: config.Chain, |
155 | 157 | peers: newPeerSet(), |
| 158 | + txBroadcastKey: newBroadcastChoiceKey(), |
156 | 159 | requiredBlocks: config.RequiredBlocks, |
157 | 160 | quitSync: make(chan struct{}), |
158 | 161 | handlerDoneCh: make(chan struct{}), |
@@ -480,58 +483,40 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) { |
480 | 483 |
|
481 | 484 | txset = make(map[*ethPeer][]common.Hash) // Set peer->hash to transfer directly |
482 | 485 | annos = make(map[*ethPeer][]common.Hash) // Set peer->hash to announce |
483 | | - ) |
484 | | - // Broadcast transactions to a batch of peers not knowing about it |
485 | | - direct := big.NewInt(int64(math.Sqrt(float64(h.peers.len())))) // Approximate number of peers to broadcast to |
486 | | - if direct.BitLen() == 0 { |
487 | | - direct = big.NewInt(1) |
488 | | - } |
489 | | - total := new(big.Int).Exp(direct, big.NewInt(2), nil) // Stabilise total peer count a bit based on sqrt peers |
490 | 486 |
|
491 | | - var ( |
492 | | - signer = types.LatestSigner(h.chain.Config()) // Don't care about chain status, we just need *a* sender |
493 | | - hasher = crypto.NewKeccakState() |
494 | | - hash = make([]byte, 32) |
| 487 | + signer = types.LatestSigner(h.chain.Config()) |
| 488 | + choice = newBroadcastChoice(h.nodeID, h.txBroadcastKey) |
| 489 | + peers = h.peers.all() |
495 | 490 | ) |
| 491 | + |
496 | 492 | for _, tx := range txs { |
497 | | - var maybeDirect bool |
| 493 | + var directSet map[*ethPeer]struct{} |
498 | 494 | switch { |
499 | 495 | case tx.Type() == types.BlobTxType: |
500 | 496 | blobTxs++ |
501 | 497 | case tx.Size() > txMaxBroadcastSize: |
502 | 498 | largeTxs++ |
503 | 499 | default: |
504 | | - maybeDirect = true |
| 500 | + // Get transaction sender address. Here we can ignore any error |
| 501 | + // since we're just interested in any value. |
| 502 | + txSender, _ := types.Sender(signer, tx) |
| 503 | + directSet = choice.choosePeers(peers, txSender) |
505 | 504 | } |
506 | | - // Send the transaction (if it's small enough) directly to a subset of |
507 | | - // the peers that have not received it yet, ensuring that the flow of |
508 | | - // transactions is grouped by account to (try and) avoid nonce gaps. |
509 | | - // |
510 | | - // To do this, we hash the local enode IW with together with a peer's |
511 | | - // enode ID together with the transaction sender and broadcast if |
512 | | - // `sha(self, peer, sender) mod peers < sqrt(peers)`. |
513 | | - for _, peer := range h.peers.peersWithoutTransaction(tx.Hash()) { |
514 | | - var broadcast bool |
515 | | - if maybeDirect { |
516 | | - hasher.Reset() |
517 | | - hasher.Write(h.nodeID.Bytes()) |
518 | | - hasher.Write(peer.Node().ID().Bytes()) |
519 | | - |
520 | | - from, _ := types.Sender(signer, tx) // Ignore error, we only use the addr as a propagation target splitter |
521 | | - hasher.Write(from.Bytes()) |
522 | | - |
523 | | - hasher.Read(hash) |
524 | | - if new(big.Int).Mod(new(big.Int).SetBytes(hash), total).Cmp(direct) < 0 { |
525 | | - broadcast = true |
526 | | - } |
| 505 | + |
| 506 | + for _, peer := range peers { |
| 507 | + if peer.KnownTransaction(tx.Hash()) { |
| 508 | + continue |
527 | 509 | } |
528 | | - if broadcast { |
| 510 | + if _, ok := directSet[peer]; ok { |
| 511 | + // Send direct. |
529 | 512 | txset[peer] = append(txset[peer], tx.Hash()) |
530 | 513 | } else { |
| 514 | + // Send announcement. |
531 | 515 | annos[peer] = append(annos[peer], tx.Hash()) |
532 | 516 | } |
533 | 517 | } |
534 | 518 | } |
| 519 | + |
535 | 520 | for peer, hashes := range txset { |
536 | 521 | directCount += len(hashes) |
537 | 522 | peer.AsyncSendTransactions(hashes) |
@@ -696,3 +681,62 @@ func (st *blockRangeState) stop() { |
696 | 681 | func (st *blockRangeState) currentRange() eth.BlockRangeUpdatePacket { |
697 | 682 | return *st.next.Load() |
698 | 683 | } |
| 684 | + |
| 685 | +// broadcastChoice implements a deterministic random choice of peers. This is designed |
| 686 | +// specifically for choosing which peer receives a direct broadcast of a transaction. |
| 687 | +// |
| 688 | +// The choice is made based on the involved p2p node IDs and the transaction sender, |
| 689 | +// ensuring that the flow of transactions is grouped by account to (try and) avoid nonce |
| 690 | +// gaps. |
| 691 | +type broadcastChoice struct { |
| 692 | + self enode.ID |
| 693 | + key [16]byte |
| 694 | + buffer map[*ethPeer]struct{} |
| 695 | + tmp []broadcastPeer |
| 696 | +} |
| 697 | + |
| 698 | +type broadcastPeer struct { |
| 699 | + p *ethPeer |
| 700 | + score uint64 |
| 701 | +} |
| 702 | + |
| 703 | +func newBroadcastChoiceKey() (k [16]byte) { |
| 704 | + crand.Read(k[:]) |
| 705 | + return k |
| 706 | +} |
| 707 | + |
| 708 | +func newBroadcastChoice(self enode.ID, key [16]byte) *broadcastChoice { |
| 709 | + return &broadcastChoice{ |
| 710 | + self: self, |
| 711 | + key: key, |
| 712 | + buffer: make(map[*ethPeer]struct{}), |
| 713 | + } |
| 714 | +} |
| 715 | + |
| 716 | +// choosePeers selects the peers that will receive a direct transaction broadcast message. |
| 717 | +// Note the return value will only stay valid until the next call to choosePeers. |
| 718 | +func (bc *broadcastChoice) choosePeers(peers []*ethPeer, txSender common.Address) map[*ethPeer]struct{} { |
| 719 | + // Compute randomized scores. |
| 720 | + bc.tmp = slices.Grow(bc.tmp[:0], len(peers))[:len(peers)] |
| 721 | + hash := siphash.New(bc.key[:]) |
| 722 | + for i, peer := range peers { |
| 723 | + hash.Reset() |
| 724 | + hash.Write(bc.self[:]) |
| 725 | + hash.Write(peer.Peer.Peer.ID().Bytes()) |
| 726 | + hash.Write(txSender[:]) |
| 727 | + bc.tmp[i] = broadcastPeer{peer, hash.Sum64()} |
| 728 | + } |
| 729 | + |
| 730 | + // Sort by score. |
| 731 | + slices.SortFunc(bc.tmp, func(a, b broadcastPeer) int { |
| 732 | + return cmp.Compare(a.score, b.score) |
| 733 | + }) |
| 734 | + |
| 735 | + // Take top n. |
| 736 | + clear(bc.buffer) |
| 737 | + n := int(math.Ceil(math.Sqrt(float64(len(bc.tmp))))) |
| 738 | + for i := range n { |
| 739 | + bc.buffer[bc.tmp[i].p] = struct{}{} |
| 740 | + } |
| 741 | + return bc.buffer |
| 742 | +} |
0 commit comments