package state
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
. "github.com/tendermint/tendermint/account"
|
|
. "github.com/tendermint/tendermint/binary"
|
|
. "github.com/tendermint/tendermint/block"
|
|
. "github.com/tendermint/tendermint/common"
|
|
db_ "github.com/tendermint/tendermint/db"
|
|
"github.com/tendermint/tendermint/merkle"
|
|
)
|
|
|
|
var (
|
|
stateKey = []byte("stateKey")
|
|
minBondAmount = uint64(1) // TODO adjust
|
|
defaultAccountsCacheCapacity = 1000 // TODO adjust
|
|
unbondingPeriodBlocks = uint(60 * 24 * 365) // TODO probably better to make it time based.
|
|
validatorTimeoutBlocks = uint(10) // TODO adjust
|
|
)
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
type InvalidTxError struct {
|
|
Tx Tx
|
|
Reason error
|
|
}
|
|
|
|
func (txErr InvalidTxError) Error() string {
|
|
return fmt.Sprintf("Invalid tx: [%v] reason: [%v]", txErr.Tx, txErr.Reason)
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// NOTE: not goroutine-safe.
|
|
type State struct {
|
|
DB db_.DB
|
|
LastBlockHeight uint
|
|
LastBlockHash []byte
|
|
LastBlockParts PartSetHeader
|
|
LastBlockTime time.Time
|
|
BondedValidators *ValidatorSet
|
|
UnbondingValidators *ValidatorSet
|
|
accounts merkle.Tree // Shouldn't be accessed directly.
|
|
validatorInfos merkle.Tree // Shouldn't be accessed directly.
|
|
}
|
|
|
|
func LoadState(db db_.DB) *State {
|
|
s := &State{DB: db}
|
|
buf := db.Get(stateKey)
|
|
if len(buf) == 0 {
|
|
return nil
|
|
} else {
|
|
r, n, err := bytes.NewReader(buf), new(int64), new(error)
|
|
s.LastBlockHeight = ReadUvarint(r, n, err)
|
|
s.LastBlockHash = ReadByteSlice(r, n, err)
|
|
s.LastBlockParts = ReadBinary(PartSetHeader{}, r, n, err).(PartSetHeader)
|
|
s.LastBlockTime = ReadTime(r, n, err)
|
|
s.BondedValidators = ReadBinary(&ValidatorSet{}, r, n, err).(*ValidatorSet)
|
|
s.UnbondingValidators = ReadBinary(&ValidatorSet{}, r, n, err).(*ValidatorSet)
|
|
accountsHash := ReadByteSlice(r, n, err)
|
|
s.accounts = merkle.NewIAVLTree(BasicCodec, AccountCodec, defaultAccountsCacheCapacity, db)
|
|
s.accounts.Load(accountsHash)
|
|
validatorInfosHash := ReadByteSlice(r, n, err)
|
|
s.validatorInfos = merkle.NewIAVLTree(BasicCodec, ValidatorInfoCodec, 0, db)
|
|
s.validatorInfos.Load(validatorInfosHash)
|
|
if *err != nil {
|
|
panic(*err)
|
|
}
|
|
// TODO: ensure that buf is completely read.
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Save this state into the db.
|
|
func (s *State) Save() {
|
|
s.accounts.Save()
|
|
s.validatorInfos.Save()
|
|
buf, n, err := new(bytes.Buffer), new(int64), new(error)
|
|
WriteUvarint(s.LastBlockHeight, buf, n, err)
|
|
WriteByteSlice(s.LastBlockHash, buf, n, err)
|
|
WriteBinary(s.LastBlockParts, buf, n, err)
|
|
WriteTime(s.LastBlockTime, buf, n, err)
|
|
WriteBinary(s.BondedValidators, buf, n, err)
|
|
WriteBinary(s.UnbondingValidators, buf, n, err)
|
|
WriteByteSlice(s.accounts.Hash(), buf, n, err)
|
|
WriteByteSlice(s.validatorInfos.Hash(), buf, n, err)
|
|
if *err != nil {
|
|
panic(*err)
|
|
}
|
|
s.DB.Set(stateKey, buf.Bytes())
|
|
}
|
|
|
|
func (s *State) Copy() *State {
|
|
return &State{
|
|
DB: s.DB,
|
|
LastBlockHeight: s.LastBlockHeight,
|
|
LastBlockHash: s.LastBlockHash,
|
|
LastBlockParts: s.LastBlockParts,
|
|
LastBlockTime: s.LastBlockTime,
|
|
BondedValidators: s.BondedValidators.Copy(),
|
|
UnbondingValidators: s.UnbondingValidators.Copy(),
|
|
accounts: s.accounts.Copy(),
|
|
validatorInfos: s.validatorInfos.Copy(),
|
|
}
|
|
}
|
|
|
|
// The accounts from the TxInputs must either already have
|
|
// account.PubKey.(type) != PubKeyNil, (it must be known),
|
|
// or it must be specified in the TxInput. But not both.
|
|
func (s *State) GetOrMakeAccounts(ins []*TxInput, outs []*TxOutput) (map[string]*Account, error) {
|
|
accounts := map[string]*Account{}
|
|
for _, in := range ins {
|
|
// Account shouldn't be duplicated
|
|
if _, ok := accounts[string(in.Address)]; ok {
|
|
return nil, ErrTxDuplicateAddress
|
|
}
|
|
account := s.GetAccount(in.Address)
|
|
if account == nil {
|
|
return nil, ErrTxInvalidAddress
|
|
}
|
|
// PubKey should be present in either "account" or "in"
|
|
if _, isNil := account.PubKey.(PubKeyNil); isNil {
|
|
if _, isNil := in.PubKey.(PubKeyNil); isNil {
|
|
return nil, ErrTxUnknownPubKey
|
|
}
|
|
if !bytes.Equal(in.PubKey.Address(), account.Address) {
|
|
return nil, ErrTxInvalidPubKey
|
|
}
|
|
account.PubKey = in.PubKey
|
|
} else {
|
|
if _, isNil := in.PubKey.(PubKeyNil); !isNil {
|
|
return nil, ErrTxRedeclaredPubKey
|
|
}
|
|
}
|
|
accounts[string(in.Address)] = account
|
|
}
|
|
for _, out := range outs {
|
|
// Account shouldn't be duplicated
|
|
if _, ok := accounts[string(out.Address)]; ok {
|
|
return nil, ErrTxDuplicateAddress
|
|
}
|
|
account := s.GetAccount(out.Address)
|
|
// output account may be nil (new)
|
|
if account == nil {
|
|
account = &Account{
|
|
Address: out.Address,
|
|
PubKey: PubKeyNil{},
|
|
Sequence: 0,
|
|
Balance: 0,
|
|
}
|
|
}
|
|
accounts[string(out.Address)] = account
|
|
}
|
|
return accounts, nil
|
|
}
|
|
|
|
func (s *State) ValidateInputs(accounts map[string]*Account, signBytes []byte, ins []*TxInput) (total uint64, err error) {
|
|
for _, in := range ins {
|
|
account := accounts[string(in.Address)]
|
|
if account == nil {
|
|
panic("ValidateInputs() expects account in accounts")
|
|
}
|
|
// Check TxInput basic
|
|
if err := in.ValidateBasic(); err != nil {
|
|
return 0, err
|
|
}
|
|
// Check amount
|
|
if account.Balance < in.Amount {
|
|
return 0, ErrTxInsufficientFunds
|
|
}
|
|
// Check signatures
|
|
if !account.PubKey.VerifyBytes(signBytes, in.Signature) {
|
|
return 0, ErrTxInvalidSignature
|
|
}
|
|
// Check sequences
|
|
if account.Sequence+1 != in.Sequence {
|
|
return 0, ErrTxInvalidSequence
|
|
}
|
|
// Good. Add amount to total
|
|
total += in.Amount
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
func (s *State) ValidateOutputs(outs []*TxOutput) (total uint64, err error) {
|
|
for _, out := range outs {
|
|
// Check TxOutput basic
|
|
if err := out.ValidateBasic(); err != nil {
|
|
return 0, err
|
|
}
|
|
// Good. Add amount to total
|
|
total += out.Amount
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
func (s *State) AdjustByInputs(accounts map[string]*Account, ins []*TxInput) {
|
|
for _, in := range ins {
|
|
account := accounts[string(in.Address)]
|
|
if account == nil {
|
|
panic("AdjustByInputs() expects account in accounts")
|
|
}
|
|
if account.Balance < in.Amount {
|
|
panic("AdjustByInputs() expects sufficient funds")
|
|
}
|
|
account.Balance -= in.Amount
|
|
account.Sequence += 1
|
|
}
|
|
}
|
|
|
|
func (s *State) AdjustByOutputs(accounts map[string]*Account, outs []*TxOutput) {
|
|
for _, out := range outs {
|
|
account := accounts[string(out.Address)]
|
|
if account == nil {
|
|
panic("AdjustByOutputs() expects account in accounts")
|
|
}
|
|
account.Balance += out.Amount
|
|
}
|
|
}
|
|
|
|
// If the tx is invalid, an error will be returned.
|
|
// Unlike AppendBlock(), state will not be altered.
|
|
func (s *State) ExecTx(tx_ Tx) error {
|
|
|
|
// TODO: do something with fees
|
|
fees := uint64(0)
|
|
|
|
// Exec tx
|
|
switch tx_.(type) {
|
|
case *SendTx:
|
|
tx := tx_.(*SendTx)
|
|
accounts, err := s.GetOrMakeAccounts(tx.Inputs, tx.Outputs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
signBytes := SignBytes(tx)
|
|
inTotal, err := s.ValidateInputs(accounts, signBytes, tx.Inputs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
outTotal, err := s.ValidateOutputs(tx.Outputs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if outTotal > inTotal {
|
|
return ErrTxInsufficientFunds
|
|
}
|
|
fee := inTotal - outTotal
|
|
fees += fee
|
|
|
|
// Good! Adjust accounts
|
|
s.AdjustByInputs(accounts, tx.Inputs)
|
|
s.AdjustByOutputs(accounts, tx.Outputs)
|
|
s.SetAccounts(accounts)
|
|
return nil
|
|
|
|
case *BondTx:
|
|
tx := tx_.(*BondTx)
|
|
valInfo := s.GetValidatorInfo(tx.PubKey.Address())
|
|
if valInfo != nil {
|
|
// TODO: In the future, check that the validator wasn't destroyed,
|
|
// add funds, merge UnbondTo outputs, and unbond validator.
|
|
return errors.New("Adding coins to existing validators not yet supported")
|
|
}
|
|
accounts, err := s.GetOrMakeAccounts(tx.Inputs, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
signBytes := SignBytes(tx)
|
|
inTotal, err := s.ValidateInputs(accounts, signBytes, tx.Inputs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := tx.PubKey.ValidateBasic(); err != nil {
|
|
return err
|
|
}
|
|
outTotal, err := s.ValidateOutputs(tx.UnbondTo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if outTotal > inTotal {
|
|
return ErrTxInsufficientFunds
|
|
}
|
|
fee := inTotal - outTotal
|
|
fees += fee
|
|
|
|
// Good! Adjust accounts
|
|
s.AdjustByInputs(accounts, tx.Inputs)
|
|
s.SetAccounts(accounts)
|
|
// Add ValidatorInfo
|
|
s.SetValidatorInfo(&ValidatorInfo{
|
|
Address: tx.PubKey.Address(),
|
|
PubKey: tx.PubKey,
|
|
UnbondTo: tx.UnbondTo,
|
|
FirstBondHeight: s.LastBlockHeight + 1,
|
|
FirstBondAmount: outTotal,
|
|
})
|
|
// Add Validator
|
|
added := s.BondedValidators.Add(&Validator{
|
|
Address: tx.PubKey.Address(),
|
|
PubKey: tx.PubKey,
|
|
BondHeight: s.LastBlockHeight + 1,
|
|
VotingPower: outTotal,
|
|
Accum: 0,
|
|
})
|
|
if !added {
|
|
panic("Failed to add validator")
|
|
}
|
|
return nil
|
|
|
|
case *UnbondTx:
|
|
tx := tx_.(*UnbondTx)
|
|
|
|
// The validator must be active
|
|
_, val := s.BondedValidators.GetByAddress(tx.Address)
|
|
if val == nil {
|
|
return ErrTxInvalidAddress
|
|
}
|
|
|
|
// Verify the signature
|
|
signBytes := SignBytes(tx)
|
|
if !val.PubKey.VerifyBytes(signBytes, tx.Signature) {
|
|
return ErrTxInvalidSignature
|
|
}
|
|
|
|
// tx.Height must be greater than val.LastCommitHeight
|
|
if tx.Height <= val.LastCommitHeight {
|
|
return errors.New("Invalid unbond height")
|
|
}
|
|
|
|
// Good!
|
|
s.unbondValidator(val)
|
|
return nil
|
|
|
|
case *RebondTx:
|
|
tx := tx_.(*RebondTx)
|
|
|
|
// The validator must be inactive
|
|
_, val := s.UnbondingValidators.GetByAddress(tx.Address)
|
|
if val == nil {
|
|
return ErrTxInvalidAddress
|
|
}
|
|
|
|
// Verify the signature
|
|
signBytes := SignBytes(tx)
|
|
if !val.PubKey.VerifyBytes(signBytes, tx.Signature) {
|
|
return ErrTxInvalidSignature
|
|
}
|
|
|
|
// tx.Height must be equal to the next height
|
|
if tx.Height != s.LastBlockHeight+1 {
|
|
return errors.New("Invalid rebond height")
|
|
}
|
|
|
|
// tx.Height must be
|
|
|
|
// Good!
|
|
s.rebondValidator(val)
|
|
return nil
|
|
|
|
case *DupeoutTx:
|
|
tx := tx_.(*DupeoutTx)
|
|
|
|
// Verify the signatures
|
|
_, accused := s.BondedValidators.GetByAddress(tx.Address)
|
|
voteASignBytes := SignBytes(&tx.VoteA)
|
|
voteBSignBytes := SignBytes(&tx.VoteB)
|
|
if !accused.PubKey.VerifyBytes(voteASignBytes, tx.VoteA.Signature) ||
|
|
!accused.PubKey.VerifyBytes(voteBSignBytes, tx.VoteB.Signature) {
|
|
return ErrTxInvalidSignature
|
|
}
|
|
|
|
// Verify equivocation
|
|
// TODO: in the future, just require one vote from a previous height that
|
|
// doesn't exist on this chain.
|
|
if tx.VoteA.Height != tx.VoteB.Height {
|
|
return errors.New("DupeoutTx heights don't match")
|
|
}
|
|
if tx.VoteA.Type == VoteTypeCommit && tx.VoteA.Round < tx.VoteB.Round {
|
|
// Check special case.
|
|
// Validators should not sign another vote after committing.
|
|
} else {
|
|
if tx.VoteA.Round != tx.VoteB.Round {
|
|
return errors.New("DupeoutTx rounds don't match")
|
|
}
|
|
if tx.VoteA.Type != tx.VoteB.Type {
|
|
return errors.New("DupeoutTx types don't match")
|
|
}
|
|
if bytes.Equal(tx.VoteA.BlockHash, tx.VoteB.BlockHash) {
|
|
return errors.New("DupeoutTx blockhashes shouldn't match")
|
|
}
|
|
}
|
|
|
|
// Good! (Bad validator!)
|
|
s.destroyValidator(accused)
|
|
return nil
|
|
|
|
default:
|
|
panic("Unknown Tx type")
|
|
}
|
|
}
|
|
|
|
func (s *State) unbondValidator(val *Validator) {
|
|
// Move validator to UnbondingValidators
|
|
val, removed := s.BondedValidators.Remove(val.Address)
|
|
if !removed {
|
|
panic("Couldn't remove validator for unbonding")
|
|
}
|
|
val.UnbondHeight = s.LastBlockHeight + 1
|
|
added := s.UnbondingValidators.Add(val)
|
|
if !added {
|
|
panic("Couldn't add validator for unbonding")
|
|
}
|
|
}
|
|
|
|
func (s *State) rebondValidator(val *Validator) {
|
|
// Move validator to BondingValidators
|
|
val, removed := s.UnbondingValidators.Remove(val.Address)
|
|
if !removed {
|
|
panic("Couldn't remove validator for rebonding")
|
|
}
|
|
val.BondHeight = s.LastBlockHeight + 1
|
|
added := s.BondedValidators.Add(val)
|
|
if !added {
|
|
panic("Couldn't add validator for rebonding")
|
|
}
|
|
}
|
|
|
|
func (s *State) releaseValidator(val *Validator) {
|
|
// Update validatorInfo
|
|
valInfo := s.GetValidatorInfo(val.Address)
|
|
if valInfo == nil {
|
|
panic("Couldn't find validatorInfo for release")
|
|
}
|
|
valInfo.ReleasedHeight = s.LastBlockHeight + 1
|
|
s.SetValidatorInfo(valInfo)
|
|
|
|
// Send coins back to UnbondTo outputs
|
|
accounts, err := s.GetOrMakeAccounts(nil, valInfo.UnbondTo)
|
|
if err != nil {
|
|
panic("Couldn't get or make unbondTo accounts")
|
|
}
|
|
s.AdjustByOutputs(accounts, valInfo.UnbondTo)
|
|
s.SetAccounts(accounts)
|
|
|
|
// Remove validator from UnbondingValidators
|
|
_, removed := s.UnbondingValidators.Remove(val.Address)
|
|
if !removed {
|
|
panic("Couldn't remove validator for release")
|
|
}
|
|
}
|
|
|
|
func (s *State) destroyValidator(val *Validator) {
|
|
// Update validatorInfo
|
|
valInfo := s.GetValidatorInfo(val.Address)
|
|
if valInfo == nil {
|
|
panic("Couldn't find validatorInfo for release")
|
|
}
|
|
valInfo.DestroyedHeight = s.LastBlockHeight + 1
|
|
valInfo.DestroyedAmount = val.VotingPower
|
|
s.SetValidatorInfo(valInfo)
|
|
|
|
// Remove validator
|
|
_, removed := s.BondedValidators.Remove(val.Address)
|
|
if !removed {
|
|
_, removed := s.UnbondingValidators.Remove(val.Address)
|
|
if !removed {
|
|
panic("Couldn't remove validator for destruction")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// "checkStateHash": If false, instead of checking the resulting
|
|
// state.Hash() against block.StateHash, it *sets* the block.StateHash.
|
|
// (used for constructing a new proposal)
|
|
// NOTE: If an error occurs during block execution, state will be left
|
|
// at an invalid state. Copy the state before calling AppendBlock!
|
|
func (s *State) AppendBlock(block *Block, blockPartsHeader PartSetHeader, checkStateHash bool) error {
|
|
// Basic block validation.
|
|
err := block.ValidateBasic(s.LastBlockHeight, s.LastBlockHash, s.LastBlockParts, s.LastBlockTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate block Validation.
|
|
if block.Height == 1 {
|
|
if len(block.Validation.Commits) != 0 {
|
|
return errors.New("Block at height 1 (first block) should have no Validation commits")
|
|
}
|
|
} else {
|
|
if uint(len(block.Validation.Commits)) != s.BondedValidators.Size() {
|
|
return errors.New("Invalid block validation size")
|
|
}
|
|
var sumVotingPower uint64
|
|
s.BondedValidators.Iterate(func(index uint, val *Validator) bool {
|
|
commit := block.Validation.Commits[index]
|
|
if commit.IsZero() {
|
|
return false
|
|
} else {
|
|
vote := &Vote{
|
|
Height: block.Height - 1,
|
|
Round: commit.Round,
|
|
Type: VoteTypeCommit,
|
|
BlockHash: block.LastBlockHash,
|
|
BlockParts: block.LastBlockParts,
|
|
}
|
|
if val.PubKey.VerifyBytes(SignBytes(vote), commit.Signature) {
|
|
sumVotingPower += val.VotingPower
|
|
return false
|
|
} else {
|
|
log.Warning("Invalid validation signature.\nval: %v\nvote: %v", val, vote)
|
|
err = errors.New("Invalid validation signature")
|
|
return true
|
|
}
|
|
}
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sumVotingPower <= s.BondedValidators.TotalVotingPower()*2/3 {
|
|
return errors.New("Insufficient validation voting power")
|
|
}
|
|
}
|
|
|
|
// Commit each tx
|
|
for _, tx := range block.Data.Txs {
|
|
err := s.ExecTx(tx)
|
|
if err != nil {
|
|
return InvalidTxError{tx, err}
|
|
}
|
|
}
|
|
|
|
// Update Validator.LastCommitHeight as necessary.
|
|
for i, commit := range block.Validation.Commits {
|
|
if commit.IsZero() {
|
|
continue
|
|
}
|
|
_, val := s.BondedValidators.GetByIndex(uint(i))
|
|
if val == nil {
|
|
Panicf("Failed to fetch validator at index %v", i)
|
|
}
|
|
val.LastCommitHeight = block.Height - 1
|
|
updated := s.BondedValidators.Update(val)
|
|
if !updated {
|
|
panic("Failed to update validator LastCommitHeight")
|
|
}
|
|
}
|
|
|
|
// If any unbonding periods are over,
|
|
// reward account with bonded coins.
|
|
toRelease := []*Validator{}
|
|
s.UnbondingValidators.Iterate(func(index uint, val *Validator) bool {
|
|
if val.UnbondHeight+unbondingPeriodBlocks < block.Height {
|
|
toRelease = append(toRelease, val)
|
|
}
|
|
return false
|
|
})
|
|
for _, val := range toRelease {
|
|
s.releaseValidator(val)
|
|
}
|
|
|
|
// If any validators haven't signed in a while,
|
|
// unbond them, they have timed out.
|
|
toTimeout := []*Validator{}
|
|
s.BondedValidators.Iterate(func(index uint, val *Validator) bool {
|
|
if val.LastCommitHeight+validatorTimeoutBlocks < block.Height {
|
|
toTimeout = append(toTimeout, val)
|
|
}
|
|
return false
|
|
})
|
|
for _, val := range toTimeout {
|
|
s.unbondValidator(val)
|
|
}
|
|
|
|
// Increment validator AccumPowers
|
|
s.BondedValidators.IncrementAccum(1)
|
|
|
|
// Check or set block.StateHash
|
|
stateHash := s.Hash()
|
|
if checkStateHash {
|
|
// State hash should match
|
|
if !bytes.Equal(stateHash, block.StateHash) {
|
|
return Errorf("Invalid state hash. Got %X, block says %X",
|
|
stateHash, block.StateHash)
|
|
}
|
|
} else {
|
|
// Set the state hash.
|
|
if block.StateHash != nil {
|
|
panic("Cannot overwrite block.StateHash")
|
|
}
|
|
block.StateHash = stateHash
|
|
}
|
|
|
|
s.LastBlockHeight = block.Height
|
|
s.LastBlockHash = block.Hash()
|
|
s.LastBlockParts = blockPartsHeader
|
|
s.LastBlockTime = block.Time
|
|
return nil
|
|
}
|
|
|
|
// The returned Account is a copy, so mutating it
|
|
// has no side effects.
|
|
func (s *State) GetAccount(address []byte) *Account {
|
|
_, account := s.accounts.Get(address)
|
|
if account == nil {
|
|
return nil
|
|
}
|
|
return account.(*Account).Copy()
|
|
}
|
|
|
|
// The account is copied before setting, so mutating it
|
|
// afterwards has no side effects.
|
|
func (s *State) SetAccount(account *Account) {
|
|
s.accounts.Set(account.Address, account.Copy())
|
|
}
|
|
|
|
// The accounts are copied before setting, so mutating it
|
|
// afterwards has no side effects.
|
|
func (s *State) SetAccounts(accounts map[string]*Account) {
|
|
for _, account := range accounts {
|
|
s.accounts.Set(account.Address, account.Copy())
|
|
}
|
|
}
|
|
|
|
// The returned ValidatorInfo is a copy, so mutating it
|
|
// has no side effects.
|
|
func (s *State) GetValidatorInfo(address []byte) *ValidatorInfo {
|
|
_, valInfo := s.validatorInfos.Get(address)
|
|
if valInfo == nil {
|
|
return nil
|
|
}
|
|
return valInfo.(*ValidatorInfo).Copy()
|
|
}
|
|
|
|
// Returns false if new, true if updated.
|
|
// The valInfo is copied before setting, so mutating it
|
|
// afterwards has no side effects.
|
|
func (s *State) SetValidatorInfo(valInfo *ValidatorInfo) (updated bool) {
|
|
return s.validatorInfos.Set(valInfo.Address, valInfo.Copy())
|
|
}
|
|
|
|
// Returns a hash that represents the state data,
|
|
// excluding LastBlock*
|
|
func (s *State) Hash() []byte {
|
|
hashables := []merkle.Hashable{
|
|
s.BondedValidators,
|
|
s.UnbondingValidators,
|
|
s.accounts,
|
|
s.validatorInfos,
|
|
}
|
|
return merkle.HashFromHashables(hashables)
|
|
}
|