Skip to content

Commit d968704

Browse files
authored
les: UDP pre-negotiation of available server capacity (#22183)
This PR implements the first one of the "lespay" UDP queries which is already useful in itself: the capacity query. The server pool is making use of this query by doing a cheap UDP query to determine whether it is worth starting the more expensive TCP connection process.
1 parent 498458b commit d968704

18 files changed

+915
-89
lines changed

common/prque/lazyqueue.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type LazyQueue struct {
4848
}
4949

5050
type (
51-
PriorityCallback func(data interface{}, now mclock.AbsTime) int64 // actual priority callback
51+
PriorityCallback func(data interface{}) int64 // actual priority callback
5252
MaxPriorityCallback func(data interface{}, until mclock.AbsTime) int64 // estimated maximum priority callback
5353
)
5454

@@ -139,11 +139,10 @@ func (q *LazyQueue) peekIndex() int {
139139
// Pop multiple times. Popped items are passed to the callback. MultiPop returns
140140
// when the callback returns false or there are no more items to pop.
141141
func (q *LazyQueue) MultiPop(callback func(data interface{}, priority int64) bool) {
142-
now := q.clock.Now()
143142
nextIndex := q.peekIndex()
144143
for nextIndex != -1 {
145144
data := heap.Pop(q.queue[nextIndex]).(*item).value
146-
heap.Push(q.popQueue, &item{data, q.priority(data, now)})
145+
heap.Push(q.popQueue, &item{data, q.priority(data)})
147146
nextIndex = q.peekIndex()
148147
for q.popQueue.Len() != 0 && (nextIndex == -1 || q.queue[nextIndex].blocks[0][0].priority < q.popQueue.blocks[0][0].priority) {
149148
i := heap.Pop(q.popQueue).(*item)

common/prque/lazyqueue_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ type lazyItem struct {
4040
index int
4141
}
4242

43-
func testPriority(a interface{}, now mclock.AbsTime) int64 {
43+
func testPriority(a interface{}) int64 {
4444
return a.(*lazyItem).p
4545
}
4646

les/client.go

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,33 @@ import (
3636
"github.com/ethereum/go-ethereum/eth/gasprice"
3737
"github.com/ethereum/go-ethereum/event"
3838
"github.com/ethereum/go-ethereum/internal/ethapi"
39+
"github.com/ethereum/go-ethereum/les/vflux"
3940
vfc "github.com/ethereum/go-ethereum/les/vflux/client"
4041
"github.com/ethereum/go-ethereum/light"
4142
"github.com/ethereum/go-ethereum/log"
4243
"github.com/ethereum/go-ethereum/node"
4344
"github.com/ethereum/go-ethereum/p2p"
4445
"github.com/ethereum/go-ethereum/p2p/enode"
46+
"github.com/ethereum/go-ethereum/p2p/enr"
4547
"github.com/ethereum/go-ethereum/params"
48+
"github.com/ethereum/go-ethereum/rlp"
4649
"github.com/ethereum/go-ethereum/rpc"
4750
)
4851

4952
type LightEthereum struct {
5053
lesCommons
5154

52-
peers *serverPeerSet
53-
reqDist *requestDistributor
54-
retriever *retrieveManager
55-
odr *LesOdr
56-
relay *lesTxRelay
57-
handler *clientHandler
58-
txPool *light.TxPool
59-
blockchain *light.LightChain
60-
serverPool *vfc.ServerPool
61-
dialCandidates enode.Iterator
62-
pruner *pruner
55+
peers *serverPeerSet
56+
reqDist *requestDistributor
57+
retriever *retrieveManager
58+
odr *LesOdr
59+
relay *lesTxRelay
60+
handler *clientHandler
61+
txPool *light.TxPool
62+
blockchain *light.LightChain
63+
serverPool *vfc.ServerPool
64+
serverPoolIterator enode.Iterator
65+
pruner *pruner
6366

6467
bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests
6568
bloomIndexer *core.ChainIndexer // Bloom indexer operating during block imports
@@ -112,7 +115,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*LightEthereum, error) {
112115
p2pConfig: &stack.Config().P2P,
113116
}
114117

115-
leth.serverPool, leth.dialCandidates = vfc.NewServerPool(lesDb, []byte("serverpool:"), time.Second, nil, &mclock.System{}, config.UltraLightServers, requestList)
118+
leth.serverPool, leth.serverPoolIterator = vfc.NewServerPool(lesDb, []byte("serverpool:"), time.Second, leth.prenegQuery, &mclock.System{}, config.UltraLightServers, requestList)
116119
leth.serverPool.AddMetrics(suggestedTimeoutGauge, totalValueGauge, serverSelectableGauge, serverConnectedGauge, sessionValueMeter, serverDialedMeter)
117120

118121
leth.retriever = newRetrieveManager(peers, leth.reqDist, leth.serverPool.GetTimeout)
@@ -189,6 +192,62 @@ func New(stack *node.Node, config *ethconfig.Config) (*LightEthereum, error) {
189192
return leth, nil
190193
}
191194

195+
// VfluxRequest sends a batch of requests to the given node through discv5 UDP TalkRequest and returns the responses
196+
func (s *LightEthereum) VfluxRequest(n *enode.Node, reqs vflux.Requests) vflux.Replies {
197+
reqsEnc, _ := rlp.EncodeToBytes(&reqs)
198+
repliesEnc, _ := s.p2pServer.DiscV5.TalkRequest(s.serverPool.DialNode(n), "vfx", reqsEnc)
199+
var replies vflux.Replies
200+
if len(repliesEnc) == 0 || rlp.DecodeBytes(repliesEnc, &replies) != nil {
201+
return nil
202+
}
203+
return replies
204+
}
205+
206+
// vfxVersion returns the version number of the "les" service subdomain of the vflux UDP
207+
// service, as advertised in the ENR record
208+
func (s *LightEthereum) vfxVersion(n *enode.Node) uint {
209+
if n.Seq() == 0 {
210+
var err error
211+
if n, err = s.p2pServer.DiscV5.RequestENR(n); n != nil && err == nil && n.Seq() != 0 {
212+
s.serverPool.Persist(n)
213+
} else {
214+
return 0
215+
}
216+
}
217+
218+
var les []rlp.RawValue
219+
if err := n.Load(enr.WithEntry("les", &les)); err != nil || len(les) < 1 {
220+
return 0
221+
}
222+
var version uint
223+
rlp.DecodeBytes(les[0], &version) // Ignore additional fields (for forward compatibility).
224+
return version
225+
}
226+
227+
// prenegQuery sends a capacity query to the given server node to determine whether
228+
// a connection slot is immediately available
229+
func (s *LightEthereum) prenegQuery(n *enode.Node) int {
230+
if s.vfxVersion(n) < 1 {
231+
// UDP query not supported, always try TCP connection
232+
return 1
233+
}
234+
235+
var requests vflux.Requests
236+
requests.Add("les", vflux.CapacityQueryName, vflux.CapacityQueryReq{
237+
Bias: 180,
238+
AddTokens: []vflux.IntOrInf{{}},
239+
})
240+
replies := s.VfluxRequest(n, requests)
241+
var cqr vflux.CapacityQueryReply
242+
if replies.Get(0, &cqr) != nil || len(cqr) != 1 { // Note: Get returns an error if replies is nil
243+
return -1
244+
}
245+
if cqr[0] > 0 {
246+
return 1
247+
}
248+
return 0
249+
}
250+
192251
type LightDummyAPI struct{}
193252

194253
// Etherbase is the address that mining rewards will be send to
@@ -269,7 +328,7 @@ func (s *LightEthereum) Protocols() []p2p.Protocol {
269328
return p.Info()
270329
}
271330
return nil
272-
}, s.dialCandidates)
331+
}, s.serverPoolIterator)
273332
}
274333

275334
// Start implements node.Lifecycle, starting all internal goroutines needed by the

les/clientpool.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ import (
2424
"github.com/ethereum/go-ethereum/common/mclock"
2525
"github.com/ethereum/go-ethereum/ethdb"
2626
"github.com/ethereum/go-ethereum/les/utils"
27+
"github.com/ethereum/go-ethereum/les/vflux"
2728
vfs "github.com/ethereum/go-ethereum/les/vflux/server"
2829
"github.com/ethereum/go-ethereum/log"
2930
"github.com/ethereum/go-ethereum/p2p/enode"
3031
"github.com/ethereum/go-ethereum/p2p/enr"
3132
"github.com/ethereum/go-ethereum/p2p/nodestate"
33+
"github.com/ethereum/go-ethereum/rlp"
3234
)
3335

3436
const (
@@ -382,3 +384,56 @@ func (f *clientPool) forClients(ids []enode.ID, cb func(client *clientInfo)) {
382384
}
383385
}
384386
}
387+
388+
// serveCapQuery serves a vflux capacity query. It receives multiple token amount values
389+
// and a bias time value. For each given token amount it calculates the maximum achievable
390+
// capacity in case the amount is added to the balance.
391+
func (f *clientPool) serveCapQuery(id enode.ID, freeID string, data []byte) []byte {
392+
var req vflux.CapacityQueryReq
393+
if rlp.DecodeBytes(data, &req) != nil {
394+
return nil
395+
}
396+
if l := len(req.AddTokens); l == 0 || l > vflux.CapacityQueryMaxLen {
397+
return nil
398+
}
399+
node := f.ns.GetNode(id)
400+
if node == nil {
401+
node = enode.SignNull(&enr.Record{}, id)
402+
}
403+
c, _ := f.ns.GetField(node, clientInfoField).(*clientInfo)
404+
if c == nil {
405+
c = &clientInfo{node: node}
406+
f.ns.SetField(node, clientInfoField, c)
407+
f.ns.SetField(node, connAddressField, freeID)
408+
defer func() {
409+
f.ns.SetField(node, connAddressField, nil)
410+
f.ns.SetField(node, clientInfoField, nil)
411+
}()
412+
if c.balance, _ = f.ns.GetField(node, f.BalanceField).(*vfs.NodeBalance); c.balance == nil {
413+
log.Error("BalanceField is missing", "node", node.ID())
414+
return nil
415+
}
416+
}
417+
// use vfs.CapacityCurve to answer request for multiple newly bought token amounts
418+
curve := f.pp.GetCapacityCurve().Exclude(id)
419+
result := make(vflux.CapacityQueryReply, len(req.AddTokens))
420+
bias := time.Second * time.Duration(req.Bias)
421+
if f.connectedBias > bias {
422+
bias = f.connectedBias
423+
}
424+
pb, _ := c.balance.GetBalance()
425+
for i, addTokens := range req.AddTokens {
426+
add := addTokens.Int64()
427+
result[i] = curve.MaxCapacity(func(capacity uint64) int64 {
428+
return c.balance.EstimatePriority(capacity, add, 0, bias, false) / int64(capacity)
429+
})
430+
if add <= 0 && uint64(-add) >= pb && result[i] > f.minCap {
431+
result[i] = f.minCap
432+
}
433+
if result[i] < f.minCap {
434+
result[i] = 0
435+
}
436+
}
437+
reply, _ := rlp.EncodeToBytes(&result)
438+
return reply
439+
}

les/clientpool_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,10 @@ func TestNegativeBalanceCalculation(t *testing.T) {
508508
for i := 0; i < 10; i++ {
509509
pool.disconnect(newPoolTestPeer(i, nil))
510510
_, nb := getBalance(pool, newPoolTestPeer(i, nil))
511-
if checkDiff(nb, uint64(time.Minute)/1000) {
512-
t.Fatalf("Negative balance mismatch, want %v, got %v", uint64(time.Minute)/1000, nb)
511+
exp := uint64(time.Minute) / 1000
512+
exp -= exp / 120 // correct for negative balance expiration
513+
if checkDiff(nb, exp) {
514+
t.Fatalf("Negative balance mismatch, want %v, got %v", exp, nb)
513515
}
514516
}
515517
}

les/enr_entry.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import (
2727
// lesEntry is the "les" ENR entry. This is set for LES servers only.
2828
type lesEntry struct {
2929
// Ignore additional fields (for forward compatibility).
30-
_ []rlp.RawValue `rlp:"tail"`
30+
VfxVersion uint
31+
Rest []rlp.RawValue `rlp:"tail"`
3132
}
3233

3334
func (lesEntry) ENRKey() string { return "les" }

les/server.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/ethereum/go-ethereum/eth/ethconfig"
2727
"github.com/ethereum/go-ethereum/ethdb"
2828
"github.com/ethereum/go-ethereum/les/flowcontrol"
29+
"github.com/ethereum/go-ethereum/les/vflux"
2930
vfs "github.com/ethereum/go-ethereum/les/vflux/server"
3031
"github.com/ethereum/go-ethereum/light"
3132
"github.com/ethereum/go-ethereum/log"
@@ -68,6 +69,7 @@ type LesServer struct {
6869
archiveMode bool // Flag whether the ethereum node runs in archive mode.
6970
handler *serverHandler
7071
broadcaster *broadcaster
72+
vfluxServer *vfs.Server
7173
privateKey *ecdsa.PrivateKey
7274

7375
// Flow control and capacity management
@@ -112,12 +114,14 @@ func NewLesServer(node *node.Node, e ethBackend, config *ethconfig.Config) (*Les
112114
ns: ns,
113115
archiveMode: e.ArchiveMode(),
114116
broadcaster: newBroadcaster(ns),
117+
vfluxServer: vfs.NewServer(time.Millisecond * 10),
115118
fcManager: flowcontrol.NewClientManager(nil, &mclock.System{}),
116119
servingQueue: newServingQueue(int64(time.Millisecond*10), float64(config.LightServ)/100),
117120
threadsBusy: config.LightServ/100 + 1,
118121
threadsIdle: threads,
119122
p2pSrv: node.Server(),
120123
}
124+
srv.vfluxServer.Register(srv)
121125
issync := e.Synced
122126
if config.LightNoSyncServe {
123127
issync = func() bool { return true }
@@ -201,7 +205,9 @@ func (s *LesServer) Protocols() []p2p.Protocol {
201205
}, nil)
202206
// Add "les" ENR entries.
203207
for i := range ps {
204-
ps[i].Attributes = []enr.Entry{&lesEntry{}}
208+
ps[i].Attributes = []enr.Entry{&lesEntry{
209+
VfxVersion: 1,
210+
}}
205211
}
206212
return ps
207213
}
@@ -211,10 +217,11 @@ func (s *LesServer) Start() error {
211217
s.privateKey = s.p2pSrv.PrivateKey
212218
s.broadcaster.setSignerKey(s.privateKey)
213219
s.handler.start()
214-
215220
s.wg.Add(1)
216221
go s.capacityManagement()
217-
222+
if s.p2pSrv.DiscV5 != nil {
223+
s.p2pSrv.DiscV5.RegisterTalkHandler("vfx", s.vfluxServer.ServeEncoded)
224+
}
218225
return nil
219226
}
220227

@@ -228,6 +235,7 @@ func (s *LesServer) Stop() error {
228235
s.costTracker.stop()
229236
s.handler.stop()
230237
s.servingQueue.stop()
238+
s.vfluxServer.Stop()
231239

232240
// Note, bloom trie indexer is closed by parent bloombits indexer.
233241
s.chtIndexer.Close()
@@ -311,3 +319,18 @@ func (s *LesServer) dropClient(id enode.ID) {
311319
p.Peer.Disconnect(p2p.DiscRequested)
312320
}
313321
}
322+
323+
// ServiceInfo implements vfs.Service
324+
func (s *LesServer) ServiceInfo() (string, string) {
325+
return "les", "Ethereum light client service"
326+
}
327+
328+
// Handle implements vfs.Service
329+
func (s *LesServer) Handle(id enode.ID, address string, name string, data []byte) []byte {
330+
switch name {
331+
case vflux.CapacityQueryName:
332+
return s.clientPool.serveCapQuery(id, address, data)
333+
default:
334+
return nil
335+
}
336+
}

0 commit comments

Comments
 (0)