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

import (
	"encoding/json"
	"flag"
	"fmt"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/consensus/clique"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/crypto/sha3"
	"github.com/ethereum/go-ethereum/rlp"
	"github.com/ethereum/go-ethereum/rpc"
	"log"
	"os"
	"path"
	"runtime"
	"sort"
	"strconv"
	"strings"
)

const (
	DefaultURL = "http://localhost:8545"
)

type Node rpc.Client

func toHex(b []byte) string {
	return fmt.Sprintf("0x%02x", b)
}

func (node *Node) blockNumber() int64 {
	var bn rpc.BlockNumber
	node.Call(&bn, "eth_blockNumber")
	return bn.Int64()
}

func check(err error) {
	if err != nil {
		panic(err.Error())
	}
}

func (node *Node) Call(result interface{}, method string, args ...interface{}) {
	check((*rpc.Client)(node).Call(result, method, args...))
}

func (node *Node) getBlockByNumber(blockNumber int64) types.Header {
	var (
		number string
		resp   types.Header
	)
	if blockNumber < 0 {
		number = "latest"
	} else {
		number = fmt.Sprintf("0x%x", blockNumber)
	}
	node.Call(&resp, "eth_getBlockByNumber", number, false)
	return resp
}

func getSigner(header *types.Header) (signer string, err error) {
	signature := header.Extra[len(header.Extra)-65:]
	hash := sigHash(header).Bytes()
	pubkey, err := crypto.Ecrecover(hash, signature)
	address := make([]byte, 20)
	copy(address, crypto.Keccak256(pubkey[1:])[12:])
	signer = toHex(address)
	return
}

func (node *Node) getBlockSigner(blockNumber int64) (signer string) {
	header := node.getBlockByNumber(blockNumber)
	signer, err := getSigner(&header)
	check(err)
	return
}

type Snapshot struct {
	Number  uint64                          `json:"number"`  // Block number where the snapshot was created
	Hash    common.Hash                     `json:"hash"`    // Block hash where the snapshot was created
	Signers map[common.Address]struct{}     `json:"signers"` // Set of authorized signers at this moment
	Recents map[uint64]common.Address       `json:"recents"` // Set of recent signers for spam protections
	Votes   []*clique.Vote                  `json:"votes"`   // List of votes cast in chronological order
	Tally   map[common.Address]clique.Tally `json:"tally"`   // Current vote tally to avoid recalculating
}

type Tally struct {
	True  int `json:"true"`
	False int `json:"false"`
	Null  int `json:"null"`
}

type Proposals struct {
	BlockNumber int64                       `json:"number"`    // Block number where the snapshot was created
	Proposals   []string                    `json:"proposals"` // List of proposals being voted
	Signers     []string                    `json:"signers"`   // List of authorized signers at this moment
	Tally       map[string]*Tally           `json:"tally"`     // Count of positive, negative and empty votes for a proposal
	Votes       map[string]map[string]*bool `json:"votes"`     // List of votes for each proposal
}

func (node *Node) getSnapshot() (snapshot Snapshot) {
	node.Call(&snapshot, "clique_getSnapshot", nil)
	return
}

func (node *Node) getSnapshotAtHash(hash common.Hash) (snapshot Snapshot) {
	node.Call(&snapshot, "clique_getSnapshotAtHash", hash)
	return
}

func (node *Node) getSnapshotAtBlock(blockNumber int64) Snapshot {
	block := node.getBlockByNumber(blockNumber)
	return node.getSnapshotAtHash(block.Hash())
}

func (node *Node) getSigners() (signers []string) {
	var s []common.Address
	node.Call(&s, "clique_getSigners", nil)
	for _, signer := range s {
		signers = append(signers, toHex(signer.Bytes()))
	}
	return
}

func (node *Node) getSignersAtHash(hash common.Hash) (signers []string) {
	var s []common.Address
	node.Call(&signers, "clique_getSignersAtHash", hash)
	for _, signer := range s {
		signers = append(signers, toHex(signer.Bytes()))
	}
	return
}

func (node *Node) getSignersAtBlock(blockNumber int64) []string {
	block := node.getBlockByNumber(blockNumber)
	return node.getSignersAtHash(block.Hash())
}

func (node *Node) getVotes(blockNumber int64) (votes Proposals) {
	var (
		snapshot Snapshot
	)
	if blockNumber < 0 {
		snapshot = node.getSnapshot()
	} else {
		snapshot = node.getSnapshotAtBlock(blockNumber)
	}
	votes.BlockNumber = int64(snapshot.Number)
	for signer := range snapshot.Signers {
		votes.Signers = append(votes.Signers, toHex(signer[:]))
		sort.Strings(votes.Signers)
	}
	for proposal := range snapshot.Tally {
		votes.Proposals = append(votes.Proposals, toHex(proposal[:]))
	}
	votes.Votes = make(map[string]map[string]*bool)
	votes.Tally = make(map[string]*Tally)
	for _, v := range snapshot.Votes {
		proposal := toHex(v.Address[:])
		signer := toHex(v.Signer[:])
		if votes.Votes[proposal] == nil {
			votes.Votes[proposal] = make(map[string]*bool)
			for _, signer := range votes.Signers {
				votes.Votes[proposal][signer] = nil
			}
			votes.Tally[proposal] = &Tally{0, 0, len(votes.Signers)}
		}
		votes.Votes[proposal][signer] = &v.Authorize
		if v.Authorize {
			votes.Tally[proposal].True += 1
		} else {
			votes.Tally[proposal].False += 1
		}
		votes.Tally[proposal].Null -= 1
	}
	return
}

func (node *Node) sealersStatus() map[string]int64 {
	status := make(map[string]int64)
	for _, address := range node.getSigners() {
		status[address] = -1
	}
	notSeen := int64(len(status))
	block := node.getBlockByNumber(-1)
	blockNumber := block.Number.Int64()
	until := blockNumber - 5*notSeen
	for notSeen > 0 {
		signer, _ := getSigner(&block)
		if status[signer] == -1 {
			status[signer] = block.Number.Int64()
			notSeen--
		}
		if blockNumber == until {
			break
		}
		blockNumber--
		block = node.getBlockByNumber(blockNumber)
	}
	return status
}

func printDebug(msg string) {
	ptr, _, _, _ := runtime.Caller(1)
	fmt.Printf("%v: %v\n", msg, runtime.FuncForPC(ptr).Name())
}

func (node *Node) getSealerInception(address string) (since int64) {
	if signers := node.getSigners(); !contains(signers, address) {
		return -1
	}

	lo := int64(0)
	hi := node.blockNumber()
	for lo < hi {
		mid := lo + (hi-lo)/2
		signers := node.getSignersAtBlock(mid)
		if contains(signers, address) {
			hi = mid
		} else {
			lo = mid + 1
		}
	}
	return hi
}

func min(a, b int64) int64 {
	if a < b {
		return a
	}
	return b
}

func (node *Node) getSignerFirstBlock(signer string, since int64, until int64) (blockNumber int64) {
	if since < 0 {
	//snapshot := node.getSnapshotAtBlock(since)
	var count int64 = 20
	// first, we look close to the inception
	if blockNumber = node.searchSnapshotForward(since, signer, count); blockNumber > 0 {
		return
	}
	since += count
	n := min(10, (until-since)/count+1) // number of goroutines
	ch := make(chan int64)
	for i := int64(0); i < n; i++ {
		go func() {
			for blockNumber := <-ch; blockNumber > 0; blockNumber = <-ch {
				cnt := min(count, until-blockNumber+1)
				if found := node.searchSnapshotForward(blockNumber, signer, cnt); found > 0 {
					ch <- found
				}
			}
			ch <- (-1)
		}()
	}
	for {
		select {
		case ch <- since:
			switch {
			case since < 0:
				continue
			case since + count < until:
				since += count
			default:
				since = -1 //we have exhausted the search space
			}
		case found := <-ch:
			switch {
			case found < 0:
				n-- // a goroutine has ended
				if n == 0 { // all goroutines have ended
					return
				}
			case blockNumber < 0:
				blockNumber = found // first time we see this signer
				since = -1          // Notify everyone
			case found < blockNumber:
				blockNumber = found // found an earlier block
			}
		}
	}
}

func (node *Node) getSignerFirstBlock_(signer string, since int64, until int64) (blockNumber int64) {
	if since < 0 {
		return -1
	}
	last := since
	for i := since; ; {
		snapshot := node.getSnapshotAtBlock(i)
		n := int64(len(snapshot.Recents))
		if int64(snapshot.Number)-n > last {
			i--
			continue
		}
		for b, s := range snapshot.Recents {
			if toHex(s[:]) == signer {
				return int64(b)
			}
		}
		if i >= until {
			return -1
		}
		last = i
		i += n
	}
}

func (node *Node) searchSnapshot(blockNumber int64, signer string, count int64, forward bool) (found int64) {
	found = -1
	for count > 0 {
		snapshot := node.getSnapshotAtBlock(blockNumber + count - 1)
		for b, s := range snapshot.Recents {
			if toHex(s[:]) == signer {
				found = int64(b)
				if !forward {
					return
				}
			}
		count -= int64(len(snapshot.Recents))
	}
	return
}

func (node *Node) searchSnapshotForward(blockNumber int64, signer string, count int64) (found int64) {
	found = -1
	for count > 0 {
		snapshot := node.getSnapshotAtBlock(blockNumber + count - 1)
		for b, s := range snapshot.Recents {
			if toHex(s[:]) == signer {
				found = int64(b)
			}
		}
		count -= int64(len(snapshot.Recents))
	}
	return
}

func (node *Node) searchSnapshotBackward(blockNumber int64, signer string, count int64) (found int64) {
	found = -1
	for i := int64(0); i < count;  {
		snapshot := node.getSnapshotAtBlock(blockNumber - i)
		for b, s := range snapshot.Recents {
			if toHex(s[:]) == signer {
				return int64(b)
			}
		}
		i += int64(len(snapshot.Recents))
	}
	return
}

func (node *Node) getSignerLastBlock(signer string, since int64, until int64) (blockNumber int64) {
	if since < 0 {
		return -1
	}
	var count int64 = 20
	// first, we look close to the last block
	if blockNumber = node.searchSnapshotBackward(until, signer, count); blockNumber > 0 {
		return
	}
	until -= count
	n := min(10, (until-since)/count+1) // number of goroutines
	ch := make(chan int64)
	for i := int64(0); i < n; i++ {
		go func() {
			for blockNumber := <-ch; blockNumber > 0; blockNumber = <-ch {
				cnt := min(count, blockNumber - since + 1)
				if found := node.searchSnapshotBackward(blockNumber, signer, cnt); found > 0 {
					ch <- found
				}
			}
			ch <- (-1)
		}()
	}
	for {
		select {
		case ch <- since:
			switch {
			case until < 0:
				continue
			case until - count > since:
				until -= count
			default:
				until = -1 //we have exhausted the search space
			}
		case found := <-ch:
			switch {
			case found < 0:
				n-- // a goroutine has ended
				if n == 0 { // all goroutines have ended
					return
				}
			case blockNumber < 0:
				blockNumber = found // first time we see this signer
				until = -1          // Notify everyone
			case found > blockNumber:
				blockNumber = found // found a later block
			}
		}
	}
}

func (node *Node) getSignerLastBlock__(signer string, since int64, until int64) (blockNumber int64) {
	if since < 0 {
		return -1
	}
	for i := until; i >= since; {
		snapshot := node.getSnapshotAtBlock(i)
		for b, s := range snapshot.Recents {
			if toHex(s[:]) == signer {
				return int64(b)
			}
		}
		i -= int64(len(snapshot.Recents))
func (node *Node) getSignerLastBlock_(signer string, since int64, until int64) (blockNumber int64) {
	if since < 0 {
		return -1
	}
	for i := until; i >= since; i-- {
		if signer == node.getBlockSigner(i) {
			return i
		}
	}
	return -1
}

type SealerInfo struct {
	Address      string
	CurrentBlock int64
	Since        int64
	FirstBlock   int64
	LastBlock    int64
}

func (node *Node) sealerInfo(sealer string) (info SealerInfo, err error) {
	info.Address = sealer
	info.CurrentBlock = node.blockNumber()
	info.Since = node.getSealerInception(sealer)
	if info.Since == -1 {
		return info, fmt.Errorf("%q is not a sealer", sealer)
	}
	info.FirstBlock = node.getSignerFirstBlock(sealer, info.Since + 1, info.CurrentBlock)
	info.LastBlock = node.getSignerLastBlock(sealer, info.FirstBlock, info.CurrentBlock)
	return
}

func printVotes(node *Node, useJson bool) {
	votes := node.getVotes(-1)
	if useJson {
		v, err := json.MarshalIndent(votes, "", "  ")
		check(err)
		fmt.Println(string(v))
		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 printSealerInfo(node *Node, sealer string) {
	info, err := node.sealerInfo(sealer)
	check(err)
	v, err := json.MarshalIndent(info, "", "  ")
	check(err)
	fmt.Println(string(v))
}

func printSealers(node *Node, useJson bool) {
	sealers := node.sealersStatus()
	if useJson {
		v, err := json.MarshalIndent(sealers, "", "  ")
		check(err)
		fmt.Println(string(v))
		return
	}
	var list []string
	for sealer := range sealers {
		list = append(list, sealer)
	}
	sort.Slice(list, func(i, j int) bool { return sealers[list[i]] > sealers[list[j]] })
	length := len(strconv.FormatInt(sealers[list[0]], 10))
	for _, sealer := range list {
		fmt.Printf("%v: %*d\n", sealer, length, sealers[sealer])
	}
}

func sigHash(header *types.Header) (hash common.Hash) {
	hasher := sha3.NewKeccak256()

	rlp.Encode(hasher, []interface{}{
		header.ParentHash,
		header.UncleHash,
		header.Coinbase,
		header.Root,
		header.TxHash,
		header.ReceiptHash,
		header.Bloom,
		header.Difficulty,
		header.Number,
		header.GasLimit,
		header.GasUsed,
		header.Time,
		header.Extra[:len(header.Extra)-65], // Yes, this will panic if extra is too short
		header.MixDigest,
		header.Nonce,
	})
	hasher.Sum(hash[:0])
	return hash
}

func Dial(url string) *Node {
	client, err := rpc.Dial(url)
	check(err)
	return (*Node)(client)
}

func (node *Node) Close() {
	(*rpc.Client)(node).Close()
}

func contains(slice []string, s string) bool {
	for _, x := range slice {
		if x == s {
			return true
		}
	}
	return false
}

func main() {
	var (
		url      string
		useJson  bool
		help     bool
		flags    flag.FlagSet
		command  = ""
		commands = []string{"sealers", "proposals", "sealerInfo"}
		desc     = map[string]string{
			"proposals":  "Detalla el estado de una votación",
			"sealerInfo": "Brinda datos sobre un sealer",
			"sealers":    "Brinda información sobre la última actividad de los selladores",
		}
	)
	defer func() {
		if err := recover(); err != nil {
			log.Fatalf("Error: %s", err)
		}
	}()
	if len(os.Args) > 1 {
		command = os.Args[1]
	}
	if !contains(commands, command) {
		fmt.Fprintf(os.Stderr, "Uso: %v <%v> [opciones]\n", path.Base(os.Args[0]), strings.Join(commands, "|"))
		fmt.Fprintf(os.Stderr, "For help: %v <command> -h\n", path.Base(os.Args[0]))
		os.Exit(1)
	}
	flags.BoolVar(&useJson, "json", false, "Produce salida en formato json")
	flags.StringVar(&url, "url", DefaultURL, "URL para conexión con geth. Ejemplo: /home/bfa/bfa/network/node/geth.ipc")
	//flags.BoolVar(&help, "h", false, "")
	//flags.BoolVar(&help, "help", false, "Muestra esta ayuda")
	flags.Parse(os.Args[2:])
	if help {
		fmt.Fprintf(os.Stderr, "Uso: %v %v [opciones]\n%v\nOpciones: \n", path.Base(os.Args[0]), command, desc[command])
		flags.PrintDefaults()
		return
	}
	node := Dial(url)
	defer node.Close()
	switch command {
	case "sealers":
		printSealers(node, useJson)
	case "proposals":
		printVotes(node, useJson)
	case "sealerInfo":
		for i := 0; i < flags.NArg(); i++ {
			printSealerInfo(node, flags.Arg(i))