Skip to content
Snippets Groups Projects
bfa_client.go 13.9 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
)

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 sealers() {
	var (
		blockNumber int64
		difficulty  bool
		balance     bool
		header      bool
	)
	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)")
	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", true, "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.")
	parseFlags(false)
	if blockNumber == 0 {
		util.Error("El bloque génesis no tiene firmantes")
	}
	url = updateURL(url)
	node, err := bfa.Dial(url)
	defer node.Close()
	blockNumber = node.BlockNumberInRange(blockNumber)
	extended := lastBlock || timestamp || difficulty || balance
	if extended {
		sealers := node.SealersStatus(blockNumber)
		if json {
			util.PrintJson(sealers)
			return
		}
		var (
			list         []string
			lastBlockLen int64
			timestampLen int64
			balanceLen   int64
		)
		for sealer := range sealers {
			list = append(list, sealer)
		}
		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 })
		}
		formatStr := "%42v"
		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 len(format) > 0 {
				format = parseFormatString(format)
				timestampLen = int64(len(format))
			} else {
				timestampLen = 10
		if difficulty {
			formatStr += " %2[6]v"
		}
		if balance {
			formatStr += " %[7]*[8]v"
			balanceLen = 0
			for _, s := range sealers {
				balanceLen = util.Max(balanceLen, int64(len(s.Balance.String())))
			}
		}
		formatStr += "\n"
		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"))
		}
		for _, sealer := range list {
			var formatedTimestamp interface{}
			s := sealers[sealer]
				if len(format) > 0 && t > 0 {
					formatedTimestamp = time.Unix(t, 0).Format(format)
			fmt.Printf(formatStr, sealer, lastBlockLen, s.LastBlock, timestampLen, formatedTimestamp, s.Difficulty, balanceLen, s.Balance)
		sealers := node.SealersAtBlock(blockNumber)
		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 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", address)
			case !isSealer && !authorize:
				util.Error("'%v' no es un sellador", 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"`
		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()
	nodeStatus.IsSealer = node.IsSealer(bfa.Self)
	nodeStatus.IsMining = node.IsMining()
	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()