Skip to content
Snippets Groups Projects
node.go 11.4 KiB
Newer Older
	. "../clique"
	. "../util"
	"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/rpc"
	"sort"
)

type Node rpc.Client

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
}

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

type SealerStatus struct {
	LastBlock int64  `json:"lastBlockSigned"`
	Time      uint64 `json:"timestamp"`
}

const (
	Latest = "latest"
	Self   = "self"
)

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

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

func (node *Node) Coinbase() (coinbase string, err error) {
		if e := recover(); e != nil {
			switch s := e.(type) {
			case string:
				err = fmt.Errorf(s)
			case error:
				err = s
			default:
				err = fmt.Errorf("unknown error while getting coinbase: %v", e)
			}
		}
	}()
	var address common.Address
	node.Call(&address, "eth_coinbase")
	coinbase = BytesToHex(address[:])
	return
}

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 (node *Node) GetBlockSigner(blockNumber int64) (signer string) {
	header := node.GetBlockByNumber(blockNumber)
	signer, err := GetSigner(&header)
	Check(err)
	return
}

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 Snapshot) {
	if blockNumber < 0 {
		return node.GetSnapshot()
	}
	node.Call(&snapshot, "clique_getSnapshot", Int64ToHex(blockNumber))
	return
}

func (node *Node) GetSigners() (signers []string) {
	var s []common.Address
	node.Call(&s, "clique_getSigners", nil)
	for _, signer := range s {
		signers = append(signers, BytesToHex(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, BytesToHex(signer.Bytes()))
	}
	return
}

func (node *Node) GetSignersAtBlock(blockNumber int64) (signers []string) {
	var s []common.Address
	if blockNumber < 0 {
		return node.GetSigners()
	}
	node.Call(&s, "clique_getSigners", Int64ToHex(blockNumber))
	for _, signer := range s {
		signers = append(signers, BytesToHex(signer.Bytes()))
func (node *Node) IsSealer(address string) bool {
	if address == Self {
		var err error
		if address, err = node.Coinbase(); err != nil {
			return false
		}
	}
	return Contains(node.GetSigners(), address)
}

func (node *Node) Propose(address string, vote bool) {
	node.Call(nil, "clique_propose", address, vote)
	return
}

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, BytesToHex(signer[:]))
		sort.Strings(votes.Signers)
	}
	for proposal := range snapshot.Tally {
		votes.Proposals = append(votes.Proposals, BytesToHex(proposal[:]))
	}
	votes.Votes = make(map[string]map[string]*bool)
	votes.Tally = make(map[string]*Tally)
	for _, v := range snapshot.Votes {
		proposal := BytesToHex(v.Address[:])
		signer := BytesToHex(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(blockNumber int64) (status map[string]*SealerStatus) {
	if blockNumber == 0 { // Genesis block doesn't have signer
		return
	}
	status = make(map[string]*SealerStatus)
	for _, address := range node.GetSignersAtBlock(blockNumber) {
		status[address] = &SealerStatus{}
	}
	notSeen := int64(len(status))
	block := node.GetBlockByNumber(blockNumber)
	blockNumber = block.Number.Int64()
	until := Max(1, blockNumber-5*notSeen)
		signer, _ := GetSigner(&block)
		if status[signer].LastBlock == 0 {
			status[signer].LastBlock = block.Number.Int64()
			status[signer].Time = block.Time.Uint64()
			notSeen--
		}
		if blockNumber == until {
			break
		}
		blockNumber--
		block = node.GetBlockByNumber(blockNumber)
	}
	return status
}

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 (node *Node) getSignerFirstBlock(signer string, since int64, until int64) (blockNumber int64) {
	if since < 0 {
		return -1
	}
	//snapshot := node.GetSnapshotAtBlock(since)
	var (
		count int64 = 20
		found int64 = -1
	)
	// first, we look close to the inception
	if blockNumber, _ = node.searchSnapshotForward(since, signer, count); blockNumber > 0 {
		return
	}
	// next, we do a coarse search
	since += count
	for i := since + 10000; i < until && found == -1; i += 10000 {
		if found, _ = node.searchSnapshotBackward(i, signer, 0); found == -1 { // still not found
			since = i
		}
	}
	if found > 0 {
		until = found
	}
	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) getSignerLastBlock(signer string, since int64, until int64) (blockNumber int64) {
	if since < 0 {
		return -1
	}
	var (
		count   int64 = 20
		visited int64 = 0
		found   int64 = -1
	)
	// first, we look close to the last block
	if blockNumber, visited = node.searchSnapshotBackward(until, signer, 0); blockNumber > 0 {
		return
	}
	// next, we do a coarse search
	until -= visited
	for i := until - 10000; i > since && found == -1; i -= 10000 {
		if found, _ = node.searchSnapshotBackward(i, signer, 0); found == -1 { // still not found
			until = i
		}
	}
	if found > 0 {
		since = found
	}
	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 <- until:
			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) searchSnapshotForward(blockNumber int64, signer string, count int64) (found int64, visited int64) {
	found = -1
	if blockNumber+count < 1 {
		return
	}
	for count > 0 {
		snapshot := node.GetSnapshotAtBlock(blockNumber + count - 1)
		recents := int64(len(snapshot.Recents))
		visited += recents
		count -= recents
		for b, s := range snapshot.Recents {
			if BytesToHex(s[:]) == signer {
				found = int64(b)
			}
		}
	}
	return
}

func (node *Node) searchSnapshotBackward(blockNumber int64, signer string, count int64) (found int64, visited int64) {
	found = -1
	if blockNumber < 1 { // Genesis block has no signers
		return
	}
	count = Min(count, blockNumber) // Never search below block 1
	for {
		snapshot := node.GetSnapshotAtBlock(blockNumber - visited)
		visited += int64(len(snapshot.Recents))
		if count == 0 {
			count = Min(blockNumber, int64(2*len(snapshot.Signers)))
		}
		for b, s := range snapshot.Recents {
			if BytesToHex(s[:]) == signer {
				found = int64(b)
				return
			}
		}
		if visited >= count {
			return
		}
	}
}

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 Dial(url string) (*Node, error) {
	client, err := rpc.Dial(url)
	return (*Node)(client), err
}

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