Skip to content
Snippets Groups Projects
bfa_client.go 17 KiB
Newer Older
package main

import (
	"../bfa"
	"../util"
	"flag"
	"fmt"
	"github.com/ethereum/go-ethereum/common"
	"os"
	"path"
	"sort"
	"strconv"
	"strings"
	latest            = -1
	minSigners        = 5 // We don't want to have less sealers than this number
	voteThreshold int = 3 // Number of manual votes needed to enable autovote
type Node = bfa.Node
var (
	url         string
	json        bool
	help        bool
	flags       = flag.NewFlagSet("", flag.ExitOnError)
	description string
	otherArgs   string
	wei         = new(big.Float).SetFloat64(1)
	kilowei     = new(big.Float).SetFloat64(1e3)
	megawei     = new(big.Float).SetFloat64(1e6)
	gigawei     = new(big.Float).SetFloat64(1e9)
	microether  = new(big.Float).SetFloat64(1e12)
	milliether  = new(big.Float).SetFloat64(1e15)
	ether       = new(big.Float).SetFloat64(1e18)

	units = map[string]*big.Float{
		"wei":        wei,
		"kwei":       kilowei,
		"kilowei":    kilowei,
		"babbage":    kilowei,
		"mwei":       megawei,
		"megawei":    megawei,
		"lovelace":   megawei,
		"gwei":       gigawei,
		"gigawei":    gigawei,
		"shannon":    gigawei,
		"microether": microether,
		"szabo":      microether,
		"milliether": milliether,
		"finney":     milliether,
		"ether":      ether,
	}
)

func setFlags() {
	flags.BoolVar(&json, "json", false, "Produce salida en formato json")
	flags.StringVar(&url, "url", "", "URL para conexión con geth. Ejemplo: '/home/bfa/bfa/network/node/geth.ipc' o 'http://localhost:8545'")
	flags.BoolVar(&help, "h", false, "")
	flags.BoolVar(&help, "help", false, "Muestra esta ayuda")
}

func updateURL(url string) (updated string) {
	const (
		defaultIPC  = "/home/bfa/bfa/network/node/geth.ipc"
		defaultHTTP = "http://localhost:8545"
	)
	if url != "" { // We accept the user selected URL
	}
	// First, we try IPC
	updated = defaultIPC
	if bfaNetworkDir := os.Getenv("BFANETWORKDIR"); bfaNetworkDir != "" {
		updated = bfaNetworkDir + "/node/get.ipc"
	}
	if fileInfo, err := os.Stat(updated); err == nil && (fileInfo.Mode()&os.ModeSocket) != 0 {
		return
	}
	updated = defaultHTTP
	return
}

func usage(errorCode int) {
	util.Require(len(os.Args) > 1, "not enough arguments")
	_, _ = fmt.Fprintf(os.Stderr, "Uso: %v %v [opciones] %v\n%v\n", os.Args[0], os.Args[1], otherArgs, description)
	flags.PrintDefaults()
	os.Exit(errorCode)
}

func parseFlags(allowAdditionalArgs bool) {
	err := flags.Parse(os.Args[2:])
	util.Check(err)
	if help {
		usage(0)
	}
	if flags.NArg() > 0 && !allowAdditionalArgs {
		_, _ = fmt.Fprintln(os.Stderr, "Demasiados argumentos")
		usage(1)
	}
}

func proposals() {
	var blockNumber int64
	description = "Detalla el estado de las votaciones en curso"
	setFlags()
	flags.Int64Var(&blockNumber, "block-number", latest, "Número del bloque en el cual se quiere conocer el estado de la propuesta (-1 para el último)")
	parseFlags(false)
	url = updateURL(url)
	node, err := bfa.Dial(url)
	util.Check(err)
	defer node.Close()
	blockNumber = node.BlockNumberInRange(blockNumber)
	votes := node.Votes(blockNumber)
	if json {
		util.PrintJson(votes)
		return
	}
	fmt.Printf("Bloque: %d\nPropuestas en curso: %d\n", votes.BlockNumber, len(votes.Proposals))
	for _, proposal := range votes.Proposals {
		fmt.Printf("Propuesta: %v\n", proposal)
		for _, signer := range votes.Signers {
			b := votes.Votes[proposal][signer]
			var v string
			if b == nil {
				v = ""
			} else {
				v = strconv.FormatBool(*b)
			}
			fmt.Printf("\t%v: %v\n", signer, v)
		}
		tally := votes.Tally[proposal]
		fmt.Printf("A favor: %v, en contra: %v, no votaron: %v\n", tally.True, tally.False, tally.Null)
	}
}

func parseFormatString(s string) (format string) {
	replacements := []struct {
		old string
		new string
	}{
		{"YYYY", "2006"},
		{"YYY", "006"},
		{"YY", "06"},
		{"MM", "01"},
		{"DD", "02"},
		{"hh", "15"},
		{"mm", "04"},
		{"ss", "05"},
	}
	switch s {
	case "long":
		return "2006-01-02 15:04:05"
	case "short":
		return "15:04:05"
	case "unix":
		return time.UnixDate
	case "rfc3339":
		return time.RFC3339
	}
	format = s
	for _, repl := range replacements {
		format = strings.Replace(format, repl.old, repl.new, 1)
	}
	return
}

func printSealers(sealers []string) {
	sort.Slice(sealers, func(i, j int) bool { return sealers[i] < sealers[j] })
	if json {
		util.PrintJson(sealers)
	} else {
		for _, sealer := range sealers {
			fmt.Println(sealer)
		}
	}
}

func weiTo(wei *big.Float, unit string) (value *big.Float, ok bool) {
	divisor, ok := units[unit]
	if ok {
		value = wei.Quo(wei, divisor)
	}
	return
}

func sealers() {
	var (
		blockNumber  int64
		lastBlock    bool
		timestamp    bool
		difficulty   bool
		balance      bool
		header       bool
		format       string
		formatStr    string
		unit         string
		list         []string
		lastBlockLen int64
		timestampLen int64
		balanceLen   int64
	description = "Presenta la lista de selladores. Opcionalmente presenta información sobre los selladores."
	setFlags()
	flags.Int64Var(&blockNumber, "block-number", latest, "Número del bloque en el cual se quiere conocer la lista de selladores (-1 para el último)")
	flags.BoolVar(&lastBlock, "last-block", false, "Muestra el último bloque sellado por cada sellador, o 0 si un nodo no ha sellado en las últimas 5 rondas.")
	flags.BoolVar(&timestamp, "timestamp", false, "Muestra el timestamp del sellado.")
	flags.BoolVar(&difficulty, "difficulty", false, "Muestra la dificultad del sellado (1: fuera de turno, 2: en turno).")
	flags.BoolVar(&balance, "balance", false, "Muestra el saldo del sellador (en Wei).")
	flags.BoolVar(&header, "header", false, "Muestra un encabezado en cada columna.")
	flags.StringVar(&format, "format", "", "Formato del timestamp. Ignorado en formato json. Opciones: 'unix', 'rfc3339', 'long' ('YYYY-MM-DD hh:mm:ss'), 'short' ('hh:mm:ss') o un formato específico. Ejemplo 'DD/MM/YY hh.mm.ss'. También se admite el formato del paquete 'time' de go.")
	flags.StringVar(&formatStr, "output-format", "", "Formato de la salida. Ignorado en formato json. Es una string en la que se reemplazan las ocurrencias de {sealer}, {last-block}, {timestamp}, {difficulty} y {balance} por sus respectivos valores.")
	flags.StringVar(&unit, "unit", "wei", "Unidades en la que se expresa el balance. Posibles valores: wei, Kwei, kilowei, Mwei, megawei, Gwei, gigawei, microether, milliether, ether, babbage, lovelace, shannon, szabo, finney.")
	parseFlags(false)
	if blockNumber == 0 {
		util.Error("El bloque génesis no tiene firmantes\n")
	}
	unit = strings.ToLower(unit)
	if _, ok := units[unit]; !ok {
		util.Error("Unidad '%v' desconocida\n", unit)
	}
	url = updateURL(url)
	node, err := bfa.Dial(url)
	defer node.Close()
	blockNumber = node.BlockNumberInRange(blockNumber)
	extended := lastBlock || timestamp || difficulty || balance || len(formatStr) > 0

	if !extended {
		printSealers(node.SealersAtBlock(blockNumber))
	sealers := node.SealersStatus(blockNumber)
	if json {
		util.PrintJson(sealers)
		return
	}

	if len(format) > 0 {
		format = parseFormatString(format)
		timestampLen = int64(len(format))
	} else {
		timestampLen = 10
	}

	for sealer := range sealers {
		list = append(list, sealer)
	}
	if formatStr != "" {
		sort.Strings(list)
		formatStr = strings.Replace(formatStr, "%", "%%", -1)
		formatStr = strings.Replace(formatStr, "{sealer}", "%[1]v", -1)
		formatStr = strings.Replace(formatStr, "{last-block}", "%[3]v", -1)
		formatStr = strings.Replace(formatStr, "{timestamp}", "%[5]v", -1)
		formatStr = strings.Replace(formatStr, "{difficulty}", "%[6]v", -1)
		formatStr = strings.Replace(formatStr, "{balance}", "%[8]v", -1)
		switch {
		case lastBlock:
			sort.Slice(list, func(i, j int) bool { return sealers[list[i]].LastBlock > sealers[list[j]].LastBlock })
		case timestamp:
			sort.Slice(list, func(i, j int) bool { return sealers[list[i]].Time > sealers[list[j]].Time })
		case difficulty:
			sort.Slice(list, func(i, j int) bool { return sealers[list[i]].Difficulty > sealers[list[j]].Difficulty })
		case balance:
			sort.Slice(list, func(i, j int) bool { return sealers[list[i]].Balance.Cmp(sealers[list[j]].Balance) > 0 })
		}
		if lastBlock {
			formatStr += " %[2]*[3]v"
			lastBlockLen = util.Max(2, int64(len(strconv.FormatInt(sealers[list[0]].LastBlock, 10))))
		}
		if timestamp {
			formatStr += " %[4]*[5]v"
		if difficulty {
			formatStr += " %2[6]v"
		}
		if balance {
			formatStr += " %[7]*[8]v"
			balanceLen = 0
			for _, s := range sealers {
				f := new(big.Float).SetInt(s.Balance)
				b, _ := weiTo(f, unit)
				balanceLen = util.Max(balanceLen, int64(len(fmt.Sprintf("%v", b))))
			}
		}
		if header {
			fmt.Printf(formatStr,
				fmt.Sprintf("%-24v", "Sealer"),
				lastBlockLen, fmt.Sprintf("%*v", -lastBlockLen/2-2, "Last"),
				timestampLen, fmt.Sprintf("%*v", -timestampLen/2-2, "Time"),
				"Dif",
				balanceLen, fmt.Sprintf("%*v", -balanceLen/2-4, "Balance"))
		}
	}
	formatStr += "\n"
	for _, sealer := range list {
		var formatedTimestamp interface{}
		s := sealers[sealer]
		t := int64(s.Time)
		formatedTimestamp = t
		if len(format) > 0 && t > 0 {
			formatedTimestamp = time.Unix(t, 0).Format(format)
		b := new(big.Float).SetInt(s.Balance)
		convertedBalance, _ := weiTo(b, unit)
		fmt.Printf(formatStr, sealer, lastBlockLen, s.LastBlock, timestampLen, formatedTimestamp, s.Difficulty, balanceLen, convertedBalance)
func autovote() {
	var (
		removedSealers = 0
		threshold      = voteThreshold
		voted          = make(map[string]bool)
	)
	description = fmt.Sprintf(`Vota automáticamente en todas las propuestas activas, en el sentido que corresponda, con ciertas restricciones.
  - No deja votar si el voto puede reducir la cantidad de selladores a menos de %v.
  - No permite eliminar a los selladores establecidos en el bloque génesis.
  - Requiere que la propuesta tenga al menos %v votos.
  - No permite votar para eliminarse a uno mismo de la lista de selladores.`, minSigners, threshold)
	flags.IntVar(&threshold, "threshold", voteThreshold, "Cantidad mínima de votos en una propuesta para habilitar el voto automático.")
	setFlags()
	parseFlags(false)
	util.Ensure(threshold >= voteThreshold, "No se puede especificar una cantidad de votos inferior a %v.", voteThreshold)
	url = updateURL(url)
	node, err := bfa.Dial(url)
	util.Check(err)
	defer node.Close()
	util.Ensure(node.IsSealer(bfa.Self), "Solo los selladores pueden votar")
	votes := node.Votes(latest)
	genesisSigners := node.SealersAtBlock(0)
	self, err := node.Coinbase()
	util.Check(err)
	for _, tally := range votes.Tally {
		if tally.False >= threshold { // We are trying to remove a sealer
			removedSealers += 1
		}
	}
	util.Ensure(len(votes.Signers)-removedSealers >= minSigners, "No se puede emitir un voto automático que reduzca la cantidad de selladores por debajo de %v.", minSigners)
	for _, proposal := range votes.Proposals {
		isSealer := util.Contains(votes.Signers, proposal)
		switch {
		case votes.Votes[proposal][self] != nil:
			continue // Already voted
		case isSealer && votes.Tally[proposal].False < threshold:
			continue // There aren't enough votes to enable autovote
		case !isSealer && votes.Tally[proposal].True < threshold:
			continue // There aren't enough votes to enable autovote
		case util.Contains(genesisSigners, proposal) && isSealer:
			log.Printf("No se puede quitar en forma automática a un sellador del bloque génesis: %v.\n", proposal)
			continue
		case proposal == self:
			log.Println("No se puede votar para eliminarse uno mismo de la lista de selladores.")
			continue
		}
		node.Propose(proposal, !isSealer)
		if json {
			voted[proposal] = !isSealer
		} else {
			fmt.Printf("Voto por %v: %v\n", proposal, !isSealer)
		}
	}
	if json {
		util.PrintJson(voted)
	}
}

func propose() {
	var (
		authorize bool
		voted     = make(map[string]bool)
	)
	description = "Vota por una propuesta."
	otherArgs = "[propuesta...]"
	setFlags()
	flags.BoolVar(&authorize, "authorize", true, "Sentido del voto (true: a favor, false: en contra).")
	parseFlags(true)
	url = updateURL(url)
	node, err := bfa.Dial(url)
	util.Check(err)
	defer node.Close()
	util.Ensure(node.IsSealer(bfa.Self), "Solo los selladores pueden votar")
	votes := node.Votes(latest)
	util.Ensure(flags.NArg() > 0, "No se especificaron candidatos por los cuales votar\n")
	for i := 0; i < flags.NArg(); i++ {
		address := flags.Arg(i)
		util.Ensure(util.IsAddress(address), "'%v' no es una dirección válida", address)
		if _, ok := votes.Tally[address]; !ok {
			isSealer := util.Contains(votes.Signers, address)
			switch { // address is not in a proposal, we only allow removing signers or adding non signers
			case isSealer && authorize:
				util.Error("'%v' ya es un sellador\n", address)
				util.Error("'%v' no es un sellador\n", address)
		node.Propose(address, authorize)
		if json {
			voted[address] = authorize
		} else {
			fmt.Printf("Voto por %v: %v\n", address, authorize)
	if json {
		util.PrintJson(voted)
func status() {
	nodeStatus := struct {
		Enode                   string              `json:"enode"`
		Network                 uint64              `json:"network"`
		Genesis                 string              `json:"genesis"`
		BFANetwork              bool                `json:"bfaNetwork"`
		BFAGenesis              bool                `json:"bfaGenesis"`
		Accounts                map[string]*big.Int `json:"accounts"`
		Coinbase                string              `json:"coinbase"`
		IsSealer                bool                `json:"isSealer"`
		IsMining                bool                `json:"isMining"`
		BlockNumber             int64               `json:"blockNumber"`
		Time                    int64               `json:"timestamp"`
		DateTime                string              `json:"datetime"`
		LastBlockSigned         int64               `json:"lastBlockSigned,omitempty"`
		LastTimeSigned          int64               `json:"lastTimeSigned,omitempty"`
		DateTimeLastBlockSigned string              `json:"datetimeLastSigned,omitempty"`
		PeerCount               uint64              `json:"peerCount"`
	}{
		Accounts: make(map[string]*big.Int),
	}
	description = "Muestra el estado del nodo."
	setFlags()
	parseFlags(false)
	url = updateURL(url)
	node, err := bfa.Dial(url)
	util.Check(err)
	defer node.Close()
	for _, account := range node.Accounts() {
		nodeStatus.Accounts[account] = node.Balance(common.HexToAddress(account))
	}
	nodeStatus.Coinbase, _ = node.Coinbase()
	nodeStatus.BlockNumber = node.BlockNumber()
	header := node.HeaderByNumber(nodeStatus.BlockNumber)
	nodeStatus.Time = header.Time.Int64()
	nodeStatus.DateTime = time.Unix(nodeStatus.Time, 0).Format("2006-01-02 15:04:05")
	nodeStatus.IsSealer = node.IsSealer(bfa.Self)
	nodeStatus.IsMining = node.IsMining()
	if nodeStatus.IsSealer {
		nodeStatus.LastBlockSigned = node.LastBlockSignedBy(nodeStatus.Coinbase, nodeStatus.BlockNumber)
		if nodeStatus.LastBlockSigned != 0 {
			header := node.HeaderByNumber(nodeStatus.LastBlockSigned)
			nodeStatus.LastTimeSigned = header.Time.Int64()
			nodeStatus.DateTimeLastBlockSigned = time.Unix(nodeStatus.LastTimeSigned, 0).Format("2006-01-02 15:04:05")
		}
	}
	nodeInfo := node.NodeInfo()
	nodeStatus.Enode = nodeInfo.Enode
	ethInfo := nodeInfo.Protocols["eth"].(map[string]interface{})
	nodeStatus.Genesis = ethInfo["genesis"].(string)
	nodeStatus.Network = uint64(ethInfo["network"].(float64))
	nodeStatus.BFAGenesis = nodeStatus.Genesis == bfa.Genesis
	nodeStatus.BFANetwork = nodeStatus.Network == bfa.Network
Miguel Montes's avatar
Miguel Montes committed
	nodeStatus.PeerCount = node.PeerCount()
func block() {
	var (
		blockNumber int64
	)
	description = "Presenta la lista de selladores. Opcionalmente indica el último bloque sellado por cada uno."
	setFlags()
	flags.Int64Var(&blockNumber, "block-number", latest, "Número del bloque en el cual se quiere conocer la lista de selladores (-1 para el último)")
	parseFlags(false)
	url = updateURL(url)
	node, err := bfa.Dial(url)
	util.Check(err)
	defer node.Close()
	blockNumber = node.BlockNumberInRange(blockNumber)
	block := node.BlockByNumber(blockNumber)
	util.PrintJson(block)
func snapshot() {
	var blockNumber int64
	description = "Muestra el snapshot en un bloque"
	setFlags()
	flags.Int64Var(&blockNumber, "block-number", latest, "Número del bloque en el cual se quiere conocer la lista de selladores (-1 para el último)")
	parseFlags(false)
	url = updateURL(url)
	node, err := bfa.Dial(url)
	util.Check(err)
	defer node.Close()
	blockNumber = node.BlockNumberInRange(blockNumber)
	snapshot := node.SnapshotAtBlock(blockNumber)
	util.PrintJson(snapshot)
}

func main() {
	var (
		commands = map[string]func(){
			"proposals": proposals,
			"sealers":   sealers,
			"vote":      propose,
			"autovote":  autovote,
		validCommands []string
		command       func()
	for cmd := range commands {
		validCommands = append(validCommands, cmd)
	if len(os.Args) > 1 {
		command = commands[os.Args[1]]
	util.Ensure(command != nil, "Uso: %v <%v> [opciones]\nPara ayuda: %v <command> -h\n", path.Base(os.Args[0]), strings.Join(validCommands, "|"), path.Base(os.Args[0]))
	command()