eth: implement new broadcast choice

This commit is contained in:
Felix Lange 2025-08-26 10:47:57 +02:00
parent fcc53c7a0d
commit 256bf9d8b0
2 changed files with 126 additions and 46 deletions

View file

@ -17,9 +17,12 @@
package eth package eth
import ( import (
"bytes"
"crypto/sha256"
"errors" "errors"
"hash"
"maps" "maps"
"math" gmath "math"
"math/big" "math/big"
"slices" "slices"
"sync" "sync"
@ -27,11 +30,11 @@ import (
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/fetcher" "github.com/ethereum/go-ethereum/eth/fetcher"
@ -484,29 +487,11 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) {
txset = make(map[*ethPeer][]common.Hash) // Set peer->hash to transfer directly txset = make(map[*ethPeer][]common.Hash) // Set peer->hash to transfer directly
annos = make(map[*ethPeer][]common.Hash) // Set peer->hash to announce annos = make(map[*ethPeer][]common.Hash) // Set peer->hash to announce
signer = types.LatestSigner(h.chain.Config())
choice = newBroadcastChoice(h.peers.len())
) )
// Broadcast transactions to a batch of peers not knowing about it
sqrtPeers := math.Sqrt(float64(h.peers.len())) // Approximate number of peers to broadcast to
// Use some hysteresis to avoid oscillating between two values, stabilising the modulus in the peer selection
// If the number of peers is small, use a minimum of 1 peer
var directInt int64
lastDirect := h.lastDirect.Load()
if int64(sqrtPeers) >= lastDirect {
directInt = max(int64(sqrtPeers), 1)
} else {
directInt = max(min(int64(sqrtPeers+directPeersHysteresis), lastDirect), 1)
}
h.lastDirect.Store(directInt)
direct := big.NewInt(directInt) // Number of peers to send directly to
total := big.NewInt(directInt * directInt) // Stabilise total peer count a bit based on sqrt peers
var (
signer = types.LatestSigner(h.chain.Config()) // Don't care about chain status, we just need *a* sender
hasher = crypto.NewKeccakState()
hash = make([]byte, 32)
)
for _, tx := range txs { for _, tx := range txs {
var maybeDirect bool var maybeDirect bool
switch { switch {
@ -517,33 +502,21 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) {
default: default:
maybeDirect = true maybeDirect = true
} }
// Send the transaction (if it's small enough) directly to a subset of
// the peers that have not received it yet, ensuring that the flow of
// transactions is grouped by account to (try and) avoid nonce gaps.
//
// To do this, we hash the local enode IW with together with a peer's
// enode ID together with the transaction sender and broadcast if
// `sha(self, peer, sender) mod peers < sqrt(peers)`.
for _, peer := range h.peers.peersWithoutTransaction(tx.Hash()) { for _, peer := range h.peers.peersWithoutTransaction(tx.Hash()) {
var broadcast bool
if maybeDirect { if maybeDirect {
hasher.Reset() // Get transaction sender address. Here we can ignore any error
hasher.Write(h.nodeID.Bytes()) // since we're just interested in any value.
hasher.Write(peer.Node().ID().Bytes()) txSender, _ := types.Sender(signer, tx)
if choice.shouldBroadcastTx(h.nodeID, peer.Peer.Peer.ID(), txSender) {
from, _ := types.Sender(signer, tx) // Ignore error, we only use the addr as a propagation target splitter // Send directly to peer.
hasher.Write(from.Bytes()) txset[peer] = append(txset[peer], tx.Hash())
continue
hasher.Read(hash)
if new(big.Int).Mod(new(big.Int).SetBytes(hash), total).Cmp(direct) < 0 {
broadcast = true
} }
} }
if broadcast {
txset[peer] = append(txset[peer], tx.Hash()) // Send announcement to peer.
} else { annos[peer] = append(annos[peer], tx.Hash())
annos[peer] = append(annos[peer], tx.Hash())
}
} }
} }
for peer, hashes := range txset { for peer, hashes := range txset {
@ -710,3 +683,44 @@ func (st *blockRangeState) stop() {
func (st *blockRangeState) currentRange() eth.BlockRangeUpdatePacket { func (st *blockRangeState) currentRange() eth.BlockRangeUpdatePacket {
return *st.next.Load() return *st.next.Load()
} }
// Send the transaction (if it's small enough) directly to a subset of
// the peers that have not received it yet, ensuring that the flow of
// transactions is grouped by account to (try and) avoid nonce gaps.
//
// To do this, we hash the local node ID together with a peer's
// node ID together with the transaction sender and broadcast if
// `sha(self, peer, sender) mod peers < sqrt(peers)`.
type broadcastChoice struct {
hash hash.Hash
threshold []byte
hashBytes []byte
}
func newBroadcastChoice(npeers int) *broadcastChoice {
var bc broadcastChoice
bc.hash = sha256.New()
bc.hashBytes = make([]byte, 32)
// compute the hash comparison threshold for one peer
unit := math.BigPow(2, 256)
unit.Sub(unit, common.Big1)
unit.Div(unit, big.NewInt(int64(max(1, npeers))))
// compute the threshold for n peers
sqrtp := max(gmath.Sqrt(float64(npeers)), 1)
thr := big.NewInt(int64(gmath.Ceil(sqrtp)))
thr.Mul(thr, unit)
bc.threshold = thr.FillBytes(make([]byte, 32))
return &bc
}
func (bc *broadcastChoice) shouldBroadcastTx(self, peer enode.ID, txSender common.Address) bool {
bc.hash.Reset()
bc.hash.Write(self[:])
bc.hash.Write(peer[:])
bc.hash.Write(txSender[:])
bc.hash.Sum(bc.hashBytes[:0])
return bytes.Compare(bc.hashBytes, bc.threshold) < 0
}

View file

@ -18,8 +18,10 @@ package eth
import ( import (
"math/big" "math/big"
"math/rand"
"sort" "sort"
"sync" "sync"
"testing"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/consensus/ethash"
@ -31,6 +33,7 @@ import (
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/holiman/uint256" "github.com/holiman/uint256"
@ -212,3 +215,66 @@ func (b *testHandler) close() {
b.handler.Stop() b.handler.Stop()
b.chain.Stop() b.chain.Stop()
} }
func TestBroadcastChoice(t *testing.T) {
choice49 := newBroadcastChoice(49)
choice50 := newBroadcastChoice(50)
var (
self = enode.HexID("1111111111111111111111111111111111111111111111111111111111111111")
peers = make([]enode.ID, 50)
txsenders = make([]common.Address, 400)
rand = rand.New(rand.NewSource(33))
)
for i := range peers {
rand.Read(peers[i][:])
}
for i := range txsenders {
rand.Read(txsenders[i][:])
}
// Evaluate choice49 first.
var chosen49 = make([][]bool, len(txsenders))
for i, txSender := range txsenders {
chosen49[i] = make([]bool, len(peers))
for peerIndex, peer := range peers {
chosen49[i][peerIndex] = choice49.shouldBroadcastTx(self, peer, txSender)
}
}
// Sanity check choices.
for i := range chosen49 {
c := count(chosen49[i], true)
if c == 0 {
t.Errorf("for tx %d, choice49 chose zero peers", i)
}
}
// Evaluate choice50 for the same peers and transactions. It should always yield more
// peers than choice49, and the chosen set should be a superset of choice49's.
for i, txSender := range txsenders {
var chosen50 int
for peerIndex, peer := range peers {
send := choice50.shouldBroadcastTx(self, peer, txSender)
if chosen49[i][peerIndex] && !send {
t.Errorf("for tx %d, choice50 did not choose peer %d, but choice49 did", i, peerIndex)
}
if send {
chosen50++
}
}
if chosen50 < count(chosen49[i], true) {
t.Errorf("for tx %d, choice50 has less peers than choice49", i)
}
}
}
func count[T comparable](s []T, v T) int {
var c int
for _, elem := range s {
if elem == v {
c++
}
}
return c
}