Skip to content

Commit 0c585f4

Browse files
zsfelfoldienriquefynn
authored andcommitted
les: implement server priority API (ethereum#20070)
This PR implements the LES server RPC API. Methods for server capacity, client balance and client priority management are provided.
1 parent 3d7630a commit 0c585f4

File tree

6 files changed

+597
-107
lines changed

6 files changed

+597
-107
lines changed

internal/web3ext/web3ext.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,11 @@ web3._extend({
445445
params: 2,
446446
inputFormatter:[null, null],
447447
}),
448+
new web3._extend.Method({
449+
name: 'freezeClient',
450+
call: 'debug_freezeClient',
451+
params: 1,
452+
}),
448453
],
449454
properties: []
450455
});
@@ -798,6 +803,31 @@ web3._extend({
798803
call: 'les_getCheckpoint',
799804
params: 1
800805
}),
806+
new web3._extend.Method({
807+
name: 'clientInfo',
808+
call: 'les_clientInfo',
809+
params: 1
810+
}),
811+
new web3._extend.Method({
812+
name: 'priorityClientInfo',
813+
call: 'les_priorityClientInfo',
814+
params: 3
815+
}),
816+
new web3._extend.Method({
817+
name: 'setClientParams',
818+
call: 'les_setClientParams',
819+
params: 2
820+
}),
821+
new web3._extend.Method({
822+
name: 'setDefaultParams',
823+
call: 'les_setDefaultParams',
824+
params: 1
825+
}),
826+
new web3._extend.Method({
827+
name: 'updateBalance',
828+
call: 'les_updateBalance',
829+
params: 3
830+
}),
801831
],
802832
properties:
803833
[
@@ -809,6 +839,10 @@ web3._extend({
809839
name: 'checkpointContractAddress',
810840
getter: 'les_getCheckpointContractAddress'
811841
}),
842+
new web3._extend.Property({
843+
name: 'serverInfo',
844+
getter: 'les_serverInfo'
845+
}),
812846
]
813847
});
814848
`

les/api.go

Lines changed: 279 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,292 @@ package les
1818

1919
import (
2020
"errors"
21+
"fmt"
22+
"math"
23+
"time"
2124

2225
"github.com/ethereum/go-ethereum/common/hexutil"
26+
"github.com/ethereum/go-ethereum/common/mclock"
27+
"github.com/ethereum/go-ethereum/p2p/enode"
2328
)
2429

2530
var (
26-
errNoCheckpoint = errors.New("no local checkpoint provided")
27-
errNotActivated = errors.New("checkpoint registrar is not activated")
31+
errNoCheckpoint = errors.New("no local checkpoint provided")
32+
errNotActivated = errors.New("checkpoint registrar is not activated")
33+
errUnknownBenchmarkType = errors.New("unknown benchmark type")
34+
errBalanceOverflow = errors.New("balance overflow")
35+
errNoPriority = errors.New("priority too low to raise capacity")
2836
)
2937

38+
const maxBalance = math.MaxInt64
39+
40+
// PrivateLightServerAPI provides an API to access the LES light server.
41+
type PrivateLightServerAPI struct {
42+
server *LesServer
43+
defaultPosFactors, defaultNegFactors priceFactors
44+
}
45+
46+
// NewPrivateLightServerAPI creates a new LES light server API.
47+
func NewPrivateLightServerAPI(server *LesServer) *PrivateLightServerAPI {
48+
return &PrivateLightServerAPI{
49+
server: server,
50+
defaultPosFactors: server.clientPool.defaultPosFactors,
51+
defaultNegFactors: server.clientPool.defaultNegFactors,
52+
}
53+
}
54+
55+
// ServerInfo returns global server parameters
56+
func (api *PrivateLightServerAPI) ServerInfo() map[string]interface{} {
57+
res := make(map[string]interface{})
58+
res["minimumCapacity"] = api.server.minCapacity
59+
res["maximumCapacity"] = api.server.maxCapacity
60+
res["freeClientCapacity"] = api.server.freeCapacity
61+
res["totalCapacity"], res["totalConnectedCapacity"], res["priorityConnectedCapacity"] = api.server.clientPool.capacityInfo()
62+
return res
63+
}
64+
65+
// ClientInfo returns information about clients listed in the ids list or matching the given tags
66+
func (api *PrivateLightServerAPI) ClientInfo(ids []enode.ID) map[enode.ID]map[string]interface{} {
67+
res := make(map[enode.ID]map[string]interface{})
68+
api.server.clientPool.forClients(ids, func(client *clientInfo, id enode.ID) error {
69+
res[id] = api.clientInfo(client, id)
70+
return nil
71+
})
72+
return res
73+
}
74+
75+
// PriorityClientInfo returns information about clients with a positive balance
76+
// in the given ID range (stop excluded). If stop is null then the iterator stops
77+
// only at the end of the ID space. MaxCount limits the number of results returned.
78+
// If maxCount limit is applied but there are more potential results then the ID
79+
// of the next potential result is included in the map with an empty structure
80+
// assigned to it.
81+
func (api *PrivateLightServerAPI) PriorityClientInfo(start, stop enode.ID, maxCount int) map[enode.ID]map[string]interface{} {
82+
res := make(map[enode.ID]map[string]interface{})
83+
ids := api.server.clientPool.ndb.getPosBalanceIDs(start, stop, maxCount+1)
84+
if len(ids) > maxCount {
85+
res[ids[maxCount]] = make(map[string]interface{})
86+
ids = ids[:maxCount]
87+
}
88+
if len(ids) != 0 {
89+
api.server.clientPool.forClients(ids, func(client *clientInfo, id enode.ID) error {
90+
res[id] = api.clientInfo(client, id)
91+
return nil
92+
})
93+
}
94+
return res
95+
}
96+
97+
// clientInfo creates a client info data structure
98+
func (api *PrivateLightServerAPI) clientInfo(c *clientInfo, id enode.ID) map[string]interface{} {
99+
info := make(map[string]interface{})
100+
if c != nil {
101+
now := mclock.Now()
102+
info["isConnected"] = true
103+
info["connectionTime"] = float64(now-c.connectedAt) / float64(time.Second)
104+
info["capacity"] = c.capacity
105+
pb, nb := c.balanceTracker.getBalance(now)
106+
info["pricing/balance"], info["pricing/negBalance"] = pb, nb
107+
info["pricing/balanceMeta"] = c.balanceMetaInfo
108+
info["priority"] = pb != 0
109+
} else {
110+
info["isConnected"] = false
111+
pb := api.server.clientPool.getPosBalance(id)
112+
info["pricing/balance"], info["pricing/balanceMeta"] = pb.value, pb.meta
113+
info["priority"] = pb.value != 0
114+
}
115+
return info
116+
}
117+
118+
// setParams either sets the given parameters for a single connected client (if specified)
119+
// or the default parameters applicable to clients connected in the future
120+
func (api *PrivateLightServerAPI) setParams(params map[string]interface{}, client *clientInfo, posFactors, negFactors *priceFactors) (updateFactors bool, err error) {
121+
defParams := client == nil
122+
if !defParams {
123+
posFactors, negFactors = &client.posFactors, &client.negFactors
124+
}
125+
for name, value := range params {
126+
errValue := func() error {
127+
return fmt.Errorf("invalid value for parameter '%s'", name)
128+
}
129+
setFactor := func(v *float64) {
130+
if val, ok := value.(float64); ok && val >= 0 {
131+
*v = val / float64(time.Second)
132+
updateFactors = true
133+
} else {
134+
err = errValue()
135+
}
136+
}
137+
138+
switch {
139+
case name == "pricing/timeFactor":
140+
setFactor(&posFactors.timeFactor)
141+
case name == "pricing/capacityFactor":
142+
setFactor(&posFactors.capacityFactor)
143+
case name == "pricing/requestCostFactor":
144+
setFactor(&posFactors.requestFactor)
145+
case name == "pricing/negative/timeFactor":
146+
setFactor(&negFactors.timeFactor)
147+
case name == "pricing/negative/capacityFactor":
148+
setFactor(&negFactors.capacityFactor)
149+
case name == "pricing/negative/requestCostFactor":
150+
setFactor(&negFactors.requestFactor)
151+
case !defParams && name == "capacity":
152+
if capacity, ok := value.(float64); ok && uint64(capacity) >= api.server.minCapacity {
153+
err = api.server.clientPool.setCapacity(client, uint64(capacity))
154+
// Don't have to call factor update explicitly. It's already done
155+
// in setCapacity function.
156+
} else {
157+
err = errValue()
158+
}
159+
default:
160+
if defParams {
161+
err = fmt.Errorf("invalid default parameter '%s'", name)
162+
} else {
163+
err = fmt.Errorf("invalid client parameter '%s'", name)
164+
}
165+
}
166+
if err != nil {
167+
return
168+
}
169+
}
170+
return
171+
}
172+
173+
// UpdateBalance updates the balance of a client (either overwrites it or adds to it).
174+
// It also updates the balance meta info string.
175+
func (api *PrivateLightServerAPI) UpdateBalance(id enode.ID, value int64, meta string) (map[string]uint64, error) {
176+
oldBalance, newBalance, err := api.server.clientPool.updateBalance(id, value, meta)
177+
m := make(map[string]uint64)
178+
m["old"] = oldBalance
179+
m["new"] = newBalance
180+
return m, err
181+
}
182+
183+
// SetClientParams sets client parameters for all clients listed in the ids list
184+
// or all connected clients if the list is empty
185+
func (api *PrivateLightServerAPI) SetClientParams(ids []enode.ID, params map[string]interface{}) error {
186+
return api.server.clientPool.forClients(ids, func(client *clientInfo, id enode.ID) error {
187+
if client != nil {
188+
update, err := api.setParams(params, client, nil, nil)
189+
if update {
190+
client.updatePriceFactors()
191+
}
192+
return err
193+
} else {
194+
return fmt.Errorf("client %064x is not connected", id[:])
195+
}
196+
})
197+
}
198+
199+
// SetDefaultParams sets the default parameters applicable to clients connected in the future
200+
func (api *PrivateLightServerAPI) SetDefaultParams(params map[string]interface{}) error {
201+
update, err := api.setParams(params, nil, &api.defaultPosFactors, &api.defaultNegFactors)
202+
if update {
203+
api.server.clientPool.setDefaultFactors(api.defaultPosFactors, api.defaultNegFactors)
204+
}
205+
return err
206+
}
207+
208+
// Benchmark runs a request performance benchmark with a given set of measurement setups
209+
// in multiple passes specified by passCount. The measurement time for each setup in each
210+
// pass is specified in milliseconds by length.
211+
//
212+
// Note: measurement time is adjusted for each pass depending on the previous ones.
213+
// Therefore a controlled total measurement time is achievable in multiple passes.
214+
func (api *PrivateLightServerAPI) Benchmark(setups []map[string]interface{}, passCount, length int) ([]map[string]interface{}, error) {
215+
benchmarks := make([]requestBenchmark, len(setups))
216+
for i, setup := range setups {
217+
if t, ok := setup["type"].(string); ok {
218+
getInt := func(field string, def int) int {
219+
if value, ok := setup[field].(float64); ok {
220+
return int(value)
221+
}
222+
return def
223+
}
224+
getBool := func(field string, def bool) bool {
225+
if value, ok := setup[field].(bool); ok {
226+
return value
227+
}
228+
return def
229+
}
230+
switch t {
231+
case "header":
232+
benchmarks[i] = &benchmarkBlockHeaders{
233+
amount: getInt("amount", 1),
234+
skip: getInt("skip", 1),
235+
byHash: getBool("byHash", false),
236+
reverse: getBool("reverse", false),
237+
}
238+
case "body":
239+
benchmarks[i] = &benchmarkBodiesOrReceipts{receipts: false}
240+
case "receipts":
241+
benchmarks[i] = &benchmarkBodiesOrReceipts{receipts: true}
242+
case "proof":
243+
benchmarks[i] = &benchmarkProofsOrCode{code: false}
244+
case "code":
245+
benchmarks[i] = &benchmarkProofsOrCode{code: true}
246+
case "cht":
247+
benchmarks[i] = &benchmarkHelperTrie{
248+
bloom: false,
249+
reqCount: getInt("amount", 1),
250+
}
251+
case "bloom":
252+
benchmarks[i] = &benchmarkHelperTrie{
253+
bloom: true,
254+
reqCount: getInt("amount", 1),
255+
}
256+
case "txSend":
257+
benchmarks[i] = &benchmarkTxSend{}
258+
case "txStatus":
259+
benchmarks[i] = &benchmarkTxStatus{}
260+
default:
261+
return nil, errUnknownBenchmarkType
262+
}
263+
} else {
264+
return nil, errUnknownBenchmarkType
265+
}
266+
}
267+
rs := api.server.handler.runBenchmark(benchmarks, passCount, time.Millisecond*time.Duration(length))
268+
result := make([]map[string]interface{}, len(setups))
269+
for i, r := range rs {
270+
res := make(map[string]interface{})
271+
if r.err == nil {
272+
res["totalCount"] = r.totalCount
273+
res["avgTime"] = r.avgTime
274+
res["maxInSize"] = r.maxInSize
275+
res["maxOutSize"] = r.maxOutSize
276+
} else {
277+
res["error"] = r.err.Error()
278+
}
279+
result[i] = res
280+
}
281+
return result, nil
282+
}
283+
284+
// PrivateDebugAPI provides an API to debug LES light server functionality.
285+
type PrivateDebugAPI struct {
286+
server *LesServer
287+
}
288+
289+
// NewPrivateDebugAPI creates a new LES light server debug API.
290+
func NewPrivateDebugAPI(server *LesServer) *PrivateDebugAPI {
291+
return &PrivateDebugAPI{
292+
server: server,
293+
}
294+
}
295+
296+
// FreezeClient forces a temporary client freeze which normally happens when the server is overloaded
297+
func (api *PrivateDebugAPI) FreezeClient(id enode.ID) error {
298+
return api.server.clientPool.forClients([]enode.ID{id}, func(c *clientInfo, id enode.ID) error {
299+
if c == nil {
300+
return fmt.Errorf("client %064x is not connected", id[:])
301+
}
302+
c.peer.freezeClient()
303+
return nil
304+
})
305+
}
306+
30307
// PrivateLightAPI provides an API to access the LES light server or light client.
31308
type PrivateLightAPI struct {
32309
backend *lesCommons

les/balance.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ func (bt *balanceTracker) timeUntil(priority int64) (time.Duration, bool) {
160160
return time.Duration(dt), true
161161
}
162162

163+
// setCapacity updates the capacity value used for priority calculation
164+
func (bt *balanceTracker) setCapacity(capacity uint64) {
165+
bt.lock.Lock()
166+
defer bt.lock.Unlock()
167+
168+
bt.capacity = capacity
169+
}
170+
163171
// getPriority returns the actual priority based on the current balance
164172
func (bt *balanceTracker) getPriority(now mclock.AbsTime) int64 {
165173
bt.lock.Lock()

0 commit comments

Comments
 (0)