Skip to content

Instantly share code, notes, and snippets.

@blockpane
Last active June 3, 2020 03:51
Show Gist options
  • Save blockpane/3a3922e0d4b2a70d755060490fc2a323 to your computer and use it in GitHub Desktop.
Save blockpane/3a3922e0d4b2a70d755060490fc2a323 to your computer and use it in GitHub Desktop.
chaos-voter ... votes for random FIO block producers, un-votes for those who are missing blocks.
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"github.com/fioprotocol/fio-go"
"github.com/fioprotocol/fio-go/imports/eos-go"
"io/ioutil"
"log"
"math/rand"
"os"
"sort"
"strings"
"sync"
"time"
)
var (
url string
perm string
actor string
key string
wl string
address string
frequency int
numVotes int
dry bool
v bool
vmux sync.Mutex
)
// tracks missed blocks
var (
missedAfter time.Time
missed = make(map[string]time.Time) // holds those who missed blocks, expires at 3*frequency
missedBlk = 720 // how many blocks since last producing that gets you kicked ....
)
func main() {
flag.StringVar(&url, "u", "", "url for connect")
flag.StringVar(&perm, "p", "", "permission")
flag.StringVar(&actor, "a", "", "actor")
flag.StringVar(&address, "address", "", "fio address")
flag.StringVar(&key, "k", "", "wif key")
flag.StringVar(&wl, "whitelist", "whitelist.txt", "allowed producers")
flag.IntVar(&frequency, "h", 24, "how often (hours) to run")
flag.IntVar(&numVotes, "n", 30, "how many producers to vote for")
flag.BoolVar(&dry, "dry-run", false, "don't push transactions, only print what would have been done.")
flag.BoolVar(&v, "v", false, "verbose logging")
flag.Parse()
switch "" {
case url, actor, key, wl, address:
fmt.Println("invalid options, use '-h' for help.")
os.Exit(1)
}
if perm == "" {
perm = actor + "@" + "active"
}
stat, err := os.Stat(wl)
if err != nil {
panic(err)
}
if stat.Size() == 0 {
fmt.Println("empty producer list")
os.Exit(1)
}
log.Println("chaos-voter starting")
// best effort to save and reload status
func() {
if vs, err := os.Open(".voter-missed"); err == nil && vs != nil {
defer vs.Close()
j, err := ioutil.ReadAll(vs)
if err != nil {
return
}
_ = json.Unmarshal(j, &missed)
if v && len(missed) > 0 {
log.Println("restored missed blocks map")
}
}
}()
_, api, _, err := fio.NewWifConnect(key, url)
if err != nil {
panic(err)
}
missedAfter = time.Now()
go findMisses(api)
tick := time.NewTicker(time.Duration(frequency) * time.Hour)
missedTick := time.NewTicker(time.Minute)
for {
select {
case <-tick.C:
if v {
log.Println("starting scheduled vote run")
}
err = vote(api)
if err != nil {
log.Println(err)
// wait, retry once ...
time.Sleep(time.Duration((frequency*60)/10) * time.Minute)
err = vote(api)
if err != nil {
log.Println(err)
}
}
case <-missedTick.C:
if v {
log.Println("searching for missed blocks")
}
err = findMisses(api)
if err != nil {
log.Println(err)
}
}
}
}
func findMisses(api *fio.API) error {
gi, err := api.GetInfo()
if err != nil {
return err
}
if missedAfter.After(time.Now()) {
if v {
log.Println("skipping missed block check, not been long enough")
}
return nil
}
gbh, err := api.GetBlockHeaderState(gi.HeadBlockNum)
if err != nil {
return err
}
// don't vote anyone out if there is a pending schedule:
if gbh.PendingSchedule != nil && gbh.PendingSchedule.Schedule != nil &&
gbh.PendingSchedule.Schedule.Producers != nil && len(gbh.PendingSchedule.Schedule.Producers) > 0 {
if v {
log.Println("there is a pending schedule update")
}
missedAfter = time.Now().Add(6 * time.Minute)
return nil
}
ok, ptl := gbh.ProducerToLast(fio.ProducerToLastProduced)
if !ok {
return errors.New("could not get last produced")
}
var slacker bool
for _, last := range ptl {
if v {
fmt.Printf("%s last produced %d blocks ago\n", last.Producer, gi.HeadBlockNum-last.BlockNum)
}
if last.BlockNum < gi.HeadBlockNum-uint32(missedBlk) {
slacker = true
log.Println(last.Producer, " is missing blocks. Refreshing votes.")
badAddr, err := addrForProdAccount(last.Producer, api)
if err != nil {
log.Println(err)
continue
}
missed[badAddr] = time.Now().Add(time.Duration(3*frequency) * time.Hour)
}
}
if slacker {
func() {
if j, err := json.Marshal(missed); err == nil {
f, err := os.OpenFile(".voter-missed", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
if err != nil {
if v {
log.Println(err)
}
return
}
defer f.Close()
n, err := f.Write(j)
if err != nil && v {
log.Println(err)
return
}
if v {
log.Printf("%+v\n", missed)
log.Println("wrote ", n, "bytes to .voter-missed")
}
}
}()
return vote(api)
}
return nil
}
func vote(api *fio.API) error {
vmux.Lock()
defer vmux.Unlock()
gp, err := api.GetFioProducers()
if err != nil {
return err
}
registered := make(map[string]bool)
for _, p := range gp.Producers {
if p.IsActive == 0 {
continue
}
registered[string(p.FioAddress)] = true
}
if v {
log.Println(len(registered), " producers are marked as active")
}
f, err := os.Open(wl)
if err != nil {
return err
}
defer f.Close()
fb, err := ioutil.ReadAll(f)
if err != nil {
return err
}
eligible := make([]string, 0)
prods := strings.Split(string(fb), "\n")
rand.Seed(time.Now().UnixNano())
// randomize order
sort.Slice(prods, func(int, int) bool {
return rand.Intn(10)%2 == 0
})
for _, prospect := range prods {
if len(eligible) >= numVotes {
if v {
log.Println("hit desired num of allowed votes")
}
break
}
prospect = strings.TrimSpace(prospect)
switch false {
case !fio.Address(prospect).Valid() || !strings.HasPrefix(prospect, "#"):
log.Println(prospect + " is not a valid fio address")
case registered[prospect]:
// nop, inactive in producers table
default:
if time.Now().Before(missed[prospect]) {
if v {
log.Println(prospect, " not considered, they are missing blocks")
}
break
}
eligible = append(eligible, prospect)
}
}
if len(eligible) == 0 {
return errors.New("no eligible producers")
}
// little bit more work when overriding the permission ... but this is best done via a linkauth
action := fio.NewActionWithPermission("eosio", "voteproducer",
eos.AccountName(strings.Split(perm, "@")[0]),
strings.Split(perm, "@")[1],
fio.VoteProducer{
Producers: eligible,
FioAddress: address,
Actor: eos.AccountName(actor),
MaxFee: fio.Tokens(fio.GetMaxFee(fio.FeeVoteProducer)),
},
)
if dry {
j, err := json.MarshalIndent(action, "", " ")
if err != nil {
return err
}
fmt.Println("would have voted for:")
fmt.Println(string(j))
return nil
}
resp, err := api.SignPushActions(action)
if v {
j, _ := json.MarshalIndent(resp, "", " ")
log.Println(string(j))
}
if resp != nil && err == nil {
missedAfter = time.Now().Add(12 * time.Minute)
}
return err
}
// only what we need.
type producerCompact struct {
FioAddress string `json:"fio_address"`
}
func addrForProdAccount(acc eos.AccountName, api *fio.API) (string, error) {
gtr, err := api.GetTableRows(eos.GetTableRowsRequest{
Code: "eosio",
Scope: "eosio",
Table: "producers",
LowerBound: string(acc),
UpperBound: string(acc),
KeyType: "name",
Index: "4",
JSON: true,
})
if err != nil {
return "", err
}
results := make([]*producerCompact, 0)
err = json.Unmarshal(gtr.Rows, &results)
if err != nil {
return "", err
}
if len(results) != 1 || results[0] == nil {
return "", errors.New("invalid query result")
}
if !fio.Address(results[0].FioAddress).Valid() {
return "", errors.New("invalid address")
}
return results[0].FioAddress, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment