-
Notifications
You must be signed in to change notification settings - Fork 8
/
producer.go
515 lines (471 loc) · 14.6 KB
/
producer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
package fio
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/fioprotocol/fio-go/eos"
"github.com/fioprotocol/fio-go/eos/ecc"
"io/ioutil"
"net"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
)
// VoteProducer votes for a producer
type VoteProducer struct {
Producers []string `json:"producers"`
FioAddress string `json:"fio_address,omitempty"`
Actor eos.AccountName
MaxFee uint64 `json:"max_fee"`
}
// NewVoteProducer creates a VoteProducer action: note - fioAddress is optional as of FIP-009
func NewVoteProducer(producers []string, actor eos.AccountName, fioAddress string) *Action {
sort.Strings(producers)
return NewAction(
eos.AccountName("eosio"), "voteproducer", actor,
VoteProducer{
Producers: producers,
FioAddress: fioAddress,
Actor: actor,
MaxFee: Tokens(GetMaxFee(FeeVoteProducer)),
},
)
}
// BpClaim requests payout for a block producer
type BpClaim struct {
FioAddress string `json:"fio_address"`
Actor eos.AccountName `json:"actor"`
}
func NewBpClaim(fioAddress string, actor eos.AccountName) *Action {
return NewAction(
eos.AccountName("fio.treasury"), "bpclaim", actor,
BpClaim{
FioAddress: fioAddress,
Actor: actor,
},
)
}
// ProducerLocation valid values are 10-80 in increments of 10
type ProducerLocation uint16
const (
LocationEastAsia ProducerLocation = 10
LocationAustralia ProducerLocation = 20
LocationWestAsia ProducerLocation = 30
LocationAfrica ProducerLocation = 40
LocationEurope ProducerLocation = 50
LocationEastNorthAmerica ProducerLocation = 60
LocationSouthAmerica ProducerLocation = 70
LocationWestNorthAmerica ProducerLocation = 80
)
type RegProducer struct {
FioAddress string `json:"fio_address"`
FioPubKey string `json:"fio_pub_key"`
Url string `json:"url"`
Location uint16 `json:"location"`
Actor eos.AccountName `json:"actor"`
MaxFee uint64 `json:"max_fee"`
}
func NewRegProducer(fioAddress string, fioPubKey string, url string, location ProducerLocation, actor eos.AccountName) (*Action, error) {
if !strings.HasPrefix(url, "http") {
return nil, errors.New("url must begin with http:// or https://")
}
if !strings.Contains("10 20 30 40 50 60 70 80", strconv.Itoa(int(location))) {
return nil, errors.New("location must be one of: 10 20 30 40 50 60 70 80")
}
return NewAction("eosio", "regproducer", actor,
RegProducer{
FioAddress: fioAddress,
FioPubKey: fioPubKey,
Url: url,
Location: uint16(location),
Actor: actor,
MaxFee: Tokens(GetMaxFee(FeeRegisterProducer)),
}), nil
}
func MustNewRegProducer(fioAddress string, fioPubKey string, url string, location ProducerLocation, actor eos.AccountName) *Action {
p, err := NewRegProducer(fioAddress, fioPubKey, url, location, actor)
if err != nil {
fmt.Println("MustNewRegProducer failed")
panic(err)
}
return p
}
type UnRegProducer struct {
FioAddress string `json:"fio_address"`
Actor eos.AccountName `json:"actor"`
MaxFee uint64 `json:"max_fee"`
}
func NewUnRegProducer(fioAddress string, actor eos.AccountName) *Action {
return NewAction("eosio", "unregprod", actor, UnRegProducer{
FioAddress: fioAddress,
Actor: actor,
MaxFee: Tokens(GetMaxFee(FeeUnregisterProducer)),
})
}
type VoteProxy struct {
Proxy string `json:"proxy"`
FioAddress string `json:"fio_address,omitempty"`
Actor eos.AccountName `json:"actor"`
MaxFee uint64 `json:"max_fee"`
}
// NewVoteProxy creates a VoteProxy action: note - fioAddress is optional as of FIP-009
func NewVoteProxy(proxy string, fioAddress string, actor eos.AccountName) *Action {
return NewAction("eosio", "voteproxy", actor,
VoteProxy{
Proxy: proxy,
FioAddress: fioAddress,
Actor: actor,
MaxFee: Tokens(GetMaxFee(FeeProxyVote)),
},
)
}
type RegProxy struct {
FioAddress string `json:"fio_address"`
Actor eos.AccountName `json:"actor"`
MaxFee uint64 `json:"max_fee"`
}
func NewRegProxy(fioAddress string, actor eos.AccountName) *Action {
return NewAction("eosio", "regproxy", actor,
RegProxy{
FioAddress: fioAddress,
Actor: actor,
MaxFee: Tokens(GetMaxFee(FeeRegisterProxy)),
},
)
}
type ProducerKey struct {
AccountName eos.AccountName `json:"producer_name"`
BlockSigningKey ecc.PublicKey `json:"block_signing_key"`
}
type Schedule struct {
Version uint32 `json:"version"`
Producers []ProducerKey `json:"producers"`
}
type ProducerSchedule struct {
Active Schedule `json:"active"`
Pending Schedule `json:"pending"`
Proposed Schedule `json:"proposed"`
}
func (api *API) GetProducerSchedule() (*ProducerSchedule, error) {
res, err := api.HttpClient.Post(api.BaseURL+"/v1/chain/get_producer_schedule", "application/json", bytes.NewReader(nil))
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
sched := &ProducerSchedule{}
err = json.Unmarshal(body, sched)
if err != nil {
return nil, err
}
return sched, nil
}
// Producers is a modification of the corresponding eos-go structure
type Producers struct {
Producers []Producer `json:"producers"`
TotalProducerVoteWeight string `json:"total_producer_vote_weight"`
More string `json:"more"`
}
// Producer is a modification of the corresponding eos-go structure
type Producer struct {
Owner eos.AccountName `json:"owner"`
FioAddress Address `json:"fio_address"`
TotalVotes string `json:"total_votes"`
ProducerPublicKey string `json:"producer_public_key"`
IsActive uint8 `json:"is_active"`
Url string `json:"url"`
UnpaidBlocks uint64 `json:"unpaid_blocks"`
LastClaimTime string `json:"last_claim_time"`
Location uint8 `json:"location"`
}
// GetFioProducers retrieves the producer table.
// The producers table is a little different on FIO, use this instead of the GetProducers call from eos-go
// TODO: it defaults to a limit of 1,000 ... may want to rethink this as a default
func (api *API) GetFioProducers() (fioProducers *Producers, err error) {
req, err := http.NewRequest("POST", api.BaseURL+`/v1/chain/get_producers`, bytes.NewReader([]byte(`{"limit": 1000}`)))
if err != nil {
return nil, err
}
req.Header.Add("content-type", "application/json")
res, err := api.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(body, &fioProducers)
if err != nil {
return nil, err
}
return
}
type BpJsonOrg struct {
CandidateName string `json:"candidate_name"`
Website string `json:"website"`
CodeOfConduct string `json:"code_of_conduct"`
OwnershipDisclosure string `json:"ownership_disclosure"`
Email string `json:"email"`
Branding struct {
Logo256 string `json:"logo_256"`
Logo1024 string `json:"logo_1024"`
LogoSvg string `json:"logo_svg"`
} `json:"branding"`
Location BpJsonLocation `json:"location"`
Social BpJsonSocial `json:"social"`
}
type BpJsonSocial struct {
Steemit string `json:"steemit"`
Twitter string `json:"twitter"`
Youtube string `json:"youtube"`
Facebook string `json:"facebook"`
Github string `json:"github"`
Reddit string `json:"reddit"`
Keybase string `json:"keybase"`
Telegram string `json:"telegram"`
Wechat string `json:"wechat"`
}
type BpJsonLocation struct {
Name string `json:"name"`
Country string `json:"country"`
Latitude float32 `json:"latitude"`
Longitude float32 `json:"longitude"`
}
type BpJsonNode struct {
Location BpJsonLocation `json:"location"`
NodeType interface{} `json:"node_type,omitempty"`
P2pEndpoint string `json:"p2p_endpoint,omitempty"`
BnetEndpoint string `json:"bnet_endpoint,omitempty"`
ApiEndpoint string `json:"api_endpoint,omitempty"`
SslEndpoint string `json:"ssl_endpoint,omitempty"`
}
type BpJson struct {
ProducerAccountName string `json:"producer_account_name"`
Org BpJsonOrg `json:"org"`
Nodes []BpJsonNode `json:"nodes"`
BpJsonUrl string `json:"bp_json_url"`
}
type ChainsJson struct {
Chains map[string]string `json:"chains"`
}
// GetBpJson attempts to retrieve the bp.json file for a producer based on the URL in the eosio.producers table.
// It intentionally rejects URLs that are an IP address, or resolve to a private IP address to reduce the risk of
// SSRF attacks, note however this check is not comprehensive, and is not risk free.
func (api *API) GetBpJson(producer eos.AccountName) (*BpJson, error) {
return api.getBpJson(producer, false)
}
// allows override of private ip check for tests
func (api *API) getBpJson(producer eos.AccountName, allowIp bool) (*BpJson, error) {
gtr, err := api.GetTableRows(eos.GetTableRowsRequest{
Code: "eosio",
Scope: "eosio",
Table: "producers",
LowerBound: string(producer),
UpperBound: string(producer),
KeyType: "name",
Index: "4",
JSON: true,
})
if err != nil {
return nil, err
}
producerRows := make([]Producer, 0)
err = json.Unmarshal(gtr.Rows, &producerRows)
if len(producerRows) != 1 {
return nil, errors.New("account not found in producers table")
}
if !strings.HasPrefix(producerRows[0].Url, "http") {
producerRows[0].Url = "https://" + producerRows[0].Url
}
u, err := url.Parse(producerRows[0].Url)
if err != nil {
return nil, err
}
// ensure this is 1) a hostname, and 2) does not resolve to a private IP range:
if !allowIp {
ip := net.ParseIP(u.Host)
if ip != nil {
return nil, errors.New("URL is an IP address, refusing to fetch")
}
addrs, err := net.LookupHost(u.Host)
if err != nil {
return nil, err
}
if len(addrs) == 0 {
return nil, errors.New("could not resolve DNS for url")
}
for _, ip := range addrs {
if isPrivate(net.ParseIP(ip)) {
return nil, errors.New("url points to a private IP address, refusing to continue")
}
}
}
var regJson, chainsJson, thisChainJson string
info, err := api.GetInfo()
if err != nil {
return nil, err
}
server := strings.TrimRight(u.String(), "/")
thisChainJson = server + "/bp." + info.ChainID.String() + ".json"
regJson = server + "/bp.json"
chainsJson = server + "/chains.json"
// try to grab the chains.json file first, if that works override the thisChainJson value, don't complain on error
func() {
resp, err := api.HttpClient.Get(chainsJson)
if err == nil && resp != nil && resp.StatusCode == http.StatusOK {
chains := &ChainsJson{}
chains.Chains = make(map[string]string)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
_ = resp.Body.Close()
err = json.Unmarshal(body, chains)
if err != nil {
return
}
if chains.Chains != nil && chains.Chains[info.ChainID.String()] != "" {
thisChainJson = server + strings.ReplaceAll("/"+chains.Chains[info.ChainID.String()], "//", "/")
}
}
}()
// try chainId, ignore error fallback to bp.json
resp, err := api.HttpClient.Get(thisChainJson)
if err == nil && resp != nil {
if resp.StatusCode == http.StatusOK {
body, err := ioutil.ReadAll(resp.Body)
if err == nil {
_ = resp.Body.Close()
}
if len(body) != 0 {
bpj := &BpJson{}
err = json.Unmarshal(body, bpj)
if err == nil && bpj.ProducerAccountName != "" {
bpj.BpJsonUrl = thisChainJson
return bpj, nil
}
}
}
}
resp, err = api.HttpClient.Get(regJson)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
bpj := &BpJson{}
err = json.Unmarshal(body, bpj)
if err != nil {
return nil, err
}
if bpj.ProducerAccountName == "" {
return nil, errors.New("did not get valid bp.json")
}
bpj.BpJsonUrl = regJson
return bpj, nil
}
// adapted from https://github.com/emitter-io/address/blob/master/ipaddr.go
// Copyright (c) 2018 Roman Atachiants
var privateBlocks = [...]*net.IPNet{
parseCIDR("10.0.0.0/8"), // RFC 1918 IPv4 private network address
parseCIDR("100.64.0.0/10"), // RFC 6598 IPv4 shared address space
parseCIDR("127.0.0.0/8"), // RFC 1122 IPv4 loopback address
parseCIDR("169.254.0.0/16"), // RFC 3927 IPv4 link local address
parseCIDR("172.16.0.0/12"), // RFC 1918 IPv4 private network address
parseCIDR("192.0.0.0/24"), // RFC 6890 IPv4 IANA address
parseCIDR("192.0.2.0/24"), // RFC 5737 IPv4 documentation address
parseCIDR("192.168.0.0/16"), // RFC 1918 IPv4 private network address
parseCIDR("::1/128"), // RFC 1884 IPv6 loopback address
parseCIDR("fe80::/10"), // RFC 4291 IPv6 link local addresses
parseCIDR("fc00::/7"), // RFC 4193 IPv6 unique local addresses
parseCIDR("fec0::/10"), // RFC 1884 IPv6 site-local addresses
parseCIDR("2001:db8::/32"), // RFC 3849 IPv6 documentation address
}
func parseCIDR(s string) *net.IPNet {
_, block, err := net.ParseCIDR(s)
if err != nil {
panic(fmt.Sprintf("Bad CIDR %s: %s", s, err))
}
return block
}
func isPrivate(ip net.IP) bool {
if ip == nil {
return true // presumes a true result gets rejected
}
for _, priv := range privateBlocks {
if priv.Contains(ip) {
return true
}
}
return false
}
type existVotes struct {
Producers []string `json:"producers"`
}
type prodRow struct {
FioAddress string `json:"fio_address"`
}
// GetVotes returns a slice of an account's current votes
func (api *API) GetVotes(account string) (votedFor []string, err error) {
getVote, err := api.GetTableRows(eos.GetTableRowsRequest{
Code: "eosio",
Scope: "eosio",
Table: "voters",
Index: "3",
LowerBound: account,
UpperBound: account,
Limit: 1,
KeyType: "name",
JSON: true,
})
if err != nil {
return
}
v := make([]*existVotes, 0)
err = json.Unmarshal(getVote.Rows, &v)
if err != nil {
return
}
if len(v) == 0 {
return
}
votedFor = make([]string, 0)
for _, row := range v[0].Producers {
if row == "" {
continue
}
gtr, err := api.GetTableRows(eos.GetTableRowsRequest{
Code: "eosio",
Scope: "eosio",
Table: "producers",
LowerBound: row,
UpperBound: row,
KeyType: "name",
Index: "4",
JSON: true,
})
if err != nil {
continue
}
p := make([]*prodRow, 0)
err = json.Unmarshal(gtr.Rows, &p)
if err != nil {
continue
}
if len(p) == 1 && p[0].FioAddress != "" {
votedFor = append(votedFor, p[0].FioAddress)
}
}
return
}