native: cache committee together with votes

This commit is contained in:
Evgenii Stratonikov 2020-11-05 10:43:43 +03:00
parent ede2b05f29
commit a3c7130ab2
3 changed files with 152 additions and 45 deletions

View file

@ -1,13 +0,0 @@
package native
import "math/big"
// gasIndexPair contains block index together with generated gas per block.
// It is used to cache NEO GASRecords.
type gasIndexPair struct {
Index uint32
GASPerBlock big.Int
}
// gasRecord contains history of gas per block changes. It is used only by NEO cache.
type gasRecord []gasIndexPair

View file

@ -37,8 +37,8 @@ type NEO struct {
votesChanged atomic.Value votesChanged atomic.Value
nextValidators atomic.Value nextValidators atomic.Value
validators atomic.Value validators atomic.Value
// committee contains cached committee members and // committee contains cached committee members and their votes.
// is updated once in a while depending on committee size // It is updated once in a while depending on committee size
// (every 28 blocks for mainnet). It's value // (every 28 blocks for mainnet). It's value
// is always equal to value stored by `prefixCommittee`. // is always equal to value stored by `prefixCommittee`.
committee atomic.Value committee atomic.Value
@ -46,14 +46,6 @@ type NEO struct {
committeeHash atomic.Value committeeHash atomic.Value
} }
// keyWithVotes is a serialized key with votes balance. It's not deserialized
// because some uses of it imply serialized-only usage and converting to
// PublicKey is quite expensive.
type keyWithVotes struct {
Key string
Votes *big.Int
}
const ( const (
neoName = "NEO" neoName = "NEO"
neoContractID = -1 neoContractID = -1
@ -107,7 +99,7 @@ func newNEO() *NEO {
n.votesChanged.Store(true) n.votesChanged.Store(true)
n.nextValidators.Store(keys.PublicKeys(nil)) n.nextValidators.Store(keys.PublicKeys(nil))
n.validators.Store(keys.PublicKeys(nil)) n.validators.Store(keys.PublicKeys(nil))
n.committee.Store(keys.PublicKeys(nil)) n.committee.Store(keysWithVotes(nil))
n.committeeHash.Store(util.Uint160{}) n.committeeHash.Store(util.Uint160{})
onp := n.Methods["onPersist"] onp := n.Methods["onPersist"]
@ -175,12 +167,13 @@ func (n *NEO) Initialize(ic *interop.Context) error {
} }
committee := ic.Chain.GetStandByCommittee() committee := ic.Chain.GetStandByCommittee()
err := n.updateCache(committee, ic.Chain) cvs := toKeysWithVotes(committee)
err := n.updateCache(cvs, ic.Chain)
if err != nil { if err != nil {
return err return err
} }
err = ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, &state.StorageItem{Value: committee.Bytes()}) err = ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, &state.StorageItem{Value: cvs.Bytes()})
if err != nil { if err != nil {
return err return err
} }
@ -212,7 +205,7 @@ func (n *NEO) Initialize(ic *interop.Context) error {
// Cache initialisation should be done apart from Initialize because Initialize is // Cache initialisation should be done apart from Initialize because Initialize is
// called only when deploying native contracts. // called only when deploying native contracts.
func (n *NEO) InitializeCache(bc blockchainer.Blockchainer, d dao.DAO) error { func (n *NEO) InitializeCache(bc blockchainer.Blockchainer, d dao.DAO) error {
committee := keys.PublicKeys{} var committee = keysWithVotes{}
si := d.GetStorageItem(n.ContractID, prefixCommittee) si := d.GetStorageItem(n.ContractID, prefixCommittee)
if err := committee.DecodeBytes(si.Value); err != nil { if err := committee.DecodeBytes(si.Value); err != nil {
return err return err
@ -231,8 +224,10 @@ func (n *NEO) InitializeCache(bc blockchainer.Blockchainer, d dao.DAO) error {
return nil return nil
} }
func (n *NEO) updateCache(committee keys.PublicKeys, bc blockchainer.Blockchainer) error { func (n *NEO) updateCache(cvs keysWithVotes, bc blockchainer.Blockchainer) error {
n.committee.Store(committee) n.committee.Store(cvs)
var committee = n.GetCommitteeMembers()
script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy()) script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy())
if err != nil { if err != nil {
return err return err
@ -249,20 +244,22 @@ func (n *NEO) updateCommittee(ic *interop.Context) error {
votesChanged := n.votesChanged.Load().(bool) votesChanged := n.votesChanged.Load().(bool)
if !votesChanged { if !votesChanged {
// We need to put in storage anyway, as it affects dumps // We need to put in storage anyway, as it affects dumps
committee := n.committee.Load().(keys.PublicKeys) committee := n.committee.Load().(keysWithVotes)
si := &state.StorageItem{Value: committee.Bytes()} si := &state.StorageItem{Value: committee.Bytes()}
return ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, si) return ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, si)
} }
committee, err := n.ComputeCommitteeMembers(ic.Chain, ic.DAO) committee, cvs, err := n.computeCommitteeMembers(ic.Chain, ic.DAO)
if err != nil { if err != nil {
return err return err
} else if cvs == nil {
cvs = toKeysWithVotes(committee)
} }
if err := n.updateCache(committee, ic.Chain); err != nil { if err := n.updateCache(cvs, ic.Chain); err != nil {
return err return err
} }
n.votesChanged.Store(false) n.votesChanged.Store(false)
si := &state.StorageItem{Value: committee.Bytes()} si := &state.StorageItem{Value: cvs.Bytes()}
return ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, si) return ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, si)
} }
@ -623,7 +620,7 @@ func (n *NEO) getCandidates(d dao.DAO) ([]keyWithVotes, error) {
for key, si := range siMap { for key, si := range siMap {
c := new(candidate).FromBytes(si.Value) c := new(candidate).FromBytes(si.Value)
if c.Registered { if c.Registered {
arr = append(arr, keyWithVotes{key, &c.Votes}) arr = append(arr, keyWithVotes{Key: key, Votes: &c.Votes})
} }
} }
sort.Slice(arr, func(i, j int) bool { return strings.Compare(arr[i].Key, arr[j].Key) == -1 }) sort.Slice(arr, func(i, j int) bool { return strings.Compare(arr[i].Key, arr[j].Key) == -1 })
@ -668,7 +665,7 @@ func (n *NEO) ComputeNextBlockValidators(bc blockchainer.Blockchainer, d dao.DAO
if vals := n.validators.Load().(keys.PublicKeys); vals != nil { if vals := n.validators.Load().(keys.PublicKeys); vals != nil {
return vals.Copy(), nil return vals.Copy(), nil
} }
result, err := n.ComputeCommitteeMembers(bc, d) result, _, err := n.computeCommitteeMembers(bc, d)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -698,15 +695,34 @@ func (n *NEO) modifyVoterTurnout(d dao.DAO, amount *big.Int) error {
// GetCommitteeMembers returns public keys of nodes in committee using cached value. // GetCommitteeMembers returns public keys of nodes in committee using cached value.
func (n *NEO) GetCommitteeMembers() keys.PublicKeys { func (n *NEO) GetCommitteeMembers() keys.PublicKeys {
return n.committee.Load().(keys.PublicKeys).Copy() var cvs = n.committee.Load().(keysWithVotes)
var committee = make(keys.PublicKeys, len(cvs))
var err error
for i := range committee {
committee[i], err = cvs[i].PublicKey()
if err != nil {
panic(err)
}
}
return committee
} }
// ComputeCommitteeMembers returns public keys of nodes in committee. func toKeysWithVotes(pubs keys.PublicKeys) keysWithVotes {
func (n *NEO) ComputeCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) { ks := make(keysWithVotes, len(pubs))
for i := range pubs {
ks[i].UnmarshaledKey = pubs[i]
ks[i].Key = string(pubs[i].Bytes())
ks[i].Votes = big.NewInt(0)
}
return ks
}
// computeCommitteeMembers returns public keys of nodes in committee.
func (n *NEO) computeCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, keysWithVotes, error) {
key := []byte{prefixVotersCount} key := []byte{prefixVotersCount}
si := d.GetStorageItem(n.ContractID, key) si := d.GetStorageItem(n.ContractID, key)
if si == nil { if si == nil {
return nil, errors.New("voters count not found") return nil, nil, errors.New("voters count not found")
} }
votersCount := bigint.FromBytes(si.Value) votersCount := bigint.FromBytes(si.Value)
// votersCount / totalSupply must be >= 0.2 // votersCount / totalSupply must be >= 0.2
@ -714,16 +730,16 @@ func (n *NEO) ComputeCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (
voterTurnout := votersCount.Div(votersCount, n.getTotalSupply(d)) voterTurnout := votersCount.Div(votersCount, n.getTotalSupply(d))
if voterTurnout.Sign() != 1 { if voterTurnout.Sign() != 1 {
pubs := bc.GetStandByCommittee() pubs := bc.GetStandByCommittee()
return pubs, nil return pubs, nil, nil
} }
cs, err := n.getCandidates(d) cs, err := n.getCandidates(d)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
sbVals := bc.GetStandByCommittee() sbVals := bc.GetStandByCommittee()
count := len(sbVals) count := len(sbVals)
if len(cs) < count { if len(cs) < count {
return sbVals, nil return sbVals, nil, nil
} }
sort.Slice(cs, func(i, j int) bool { sort.Slice(cs, func(i, j int) bool {
// The most-voted validators should end up in the front of the list. // The most-voted validators should end up in the front of the list.
@ -736,12 +752,12 @@ func (n *NEO) ComputeCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (
}) })
pubs := make(keys.PublicKeys, count) pubs := make(keys.PublicKeys, count)
for i := range pubs { for i := range pubs {
pubs[i], err = keys.NewPublicKeyFromBytes([]byte(cs[i].Key), elliptic.P256()) pubs[i], err = cs[i].PublicKey()
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
} }
return pubs, nil return pubs, cs[:count], nil
} }
func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []stackitem.Item) stackitem.Item { func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []stackitem.Item) stackitem.Item {

View file

@ -0,0 +1,104 @@
package native
import (
"crypto/elliptic"
"errors"
"math/big"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// gasIndexPair contains block index together with generated gas per block.
// It is used to cache NEO GASRecords.
type gasIndexPair struct {
Index uint32
GASPerBlock big.Int
}
// gasRecord contains history of gas per block changes. It is used only by NEO cache.
type gasRecord []gasIndexPair
type (
// keyWithVotes is a serialized key with votes balance. It's not deserialized
// because some uses of it imply serialized-only usage and converting to
// PublicKey is quite expensive.
keyWithVotes struct {
Key string
Votes *big.Int
// UnmarshaledKey contains public key if it was unmarshaled.
UnmarshaledKey *keys.PublicKey
}
keysWithVotes []keyWithVotes
)
// PublicKey unmarshals and returns public key of k.
func (k keyWithVotes) PublicKey() (*keys.PublicKey, error) {
if k.UnmarshaledKey != nil {
return k.UnmarshaledKey, nil
}
return keys.NewPublicKeyFromBytes([]byte(k.Key), elliptic.P256())
}
func (k keysWithVotes) toStackItem() stackitem.Item {
arr := make([]stackitem.Item, len(k))
for i := range k {
arr[i] = stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray([]byte(k[i].Key)),
stackitem.NewBigInteger(k[i].Votes),
})
}
return stackitem.NewArray(arr)
}
func (k *keysWithVotes) fromStackItem(item stackitem.Item) error {
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not an array")
}
var kvs = make(keysWithVotes, len(arr))
for i := range arr {
s, ok := arr[i].Value().([]stackitem.Item)
if !ok {
return errors.New("element is not a struct")
} else if len(s) < 2 {
return errors.New("invalid length")
}
pub, err := s[0].TryBytes()
if err != nil {
return err
}
vs, err := s[1].TryInteger()
if err != nil {
return err
}
kvs[i].Key = string(pub)
kvs[i].Votes = vs
}
*k = kvs
return nil
}
// Bytes serializes keys with votes slice.
func (k keysWithVotes) Bytes() []byte {
var it = k.toStackItem()
var w = io.NewBufBinWriter()
stackitem.EncodeBinaryStackItem(it, w.BinWriter)
if w.Err != nil {
panic(w.Err)
}
return w.Bytes()
}
// DecodeBytes deserializes keys and votes slice.
func (k *keysWithVotes) DecodeBytes(data []byte) error {
var r = io.NewBinReaderFromBuf(data)
var it = stackitem.DecodeBinaryStackItem(r)
if r.Err != nil {
return r.Err
}
return k.fromStackItem(it)
}