From a3c7130ab2399149d8682f1a843d4bd83c7cdb81 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 5 Nov 2020 10:43:43 +0300 Subject: [PATCH] native: cache committee together with votes --- pkg/core/native/gas_record.go | 13 ----- pkg/core/native/native_neo.go | 80 +++++++++++++++----------- pkg/core/native/neo_types.go | 104 ++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 45 deletions(-) delete mode 100644 pkg/core/native/gas_record.go create mode 100644 pkg/core/native/neo_types.go diff --git a/pkg/core/native/gas_record.go b/pkg/core/native/gas_record.go deleted file mode 100644 index 1c9121ac1..000000000 --- a/pkg/core/native/gas_record.go +++ /dev/null @@ -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 diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index b6698c8e8..46dda1cdf 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -37,8 +37,8 @@ type NEO struct { votesChanged atomic.Value nextValidators atomic.Value validators atomic.Value - // committee contains cached committee members and - // is updated once in a while depending on committee size + // committee contains cached committee members and their votes. + // It is updated once in a while depending on committee size // (every 28 blocks for mainnet). It's value // is always equal to value stored by `prefixCommittee`. committee atomic.Value @@ -46,14 +46,6 @@ type NEO struct { 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 ( neoName = "NEO" neoContractID = -1 @@ -107,7 +99,7 @@ func newNEO() *NEO { n.votesChanged.Store(true) n.nextValidators.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{}) onp := n.Methods["onPersist"] @@ -175,12 +167,13 @@ func (n *NEO) Initialize(ic *interop.Context) error { } committee := ic.Chain.GetStandByCommittee() - err := n.updateCache(committee, ic.Chain) + cvs := toKeysWithVotes(committee) + err := n.updateCache(cvs, ic.Chain) if err != nil { 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 { 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 // called only when deploying native contracts. func (n *NEO) InitializeCache(bc blockchainer.Blockchainer, d dao.DAO) error { - committee := keys.PublicKeys{} + var committee = keysWithVotes{} si := d.GetStorageItem(n.ContractID, prefixCommittee) if err := committee.DecodeBytes(si.Value); err != nil { return err @@ -231,8 +224,10 @@ func (n *NEO) InitializeCache(bc blockchainer.Blockchainer, d dao.DAO) error { return nil } -func (n *NEO) updateCache(committee keys.PublicKeys, bc blockchainer.Blockchainer) error { - n.committee.Store(committee) +func (n *NEO) updateCache(cvs keysWithVotes, bc blockchainer.Blockchainer) error { + n.committee.Store(cvs) + + var committee = n.GetCommitteeMembers() script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy()) if err != nil { return err @@ -249,20 +244,22 @@ func (n *NEO) updateCommittee(ic *interop.Context) error { votesChanged := n.votesChanged.Load().(bool) if !votesChanged { // 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()} 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 { 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 } n.votesChanged.Store(false) - si := &state.StorageItem{Value: committee.Bytes()} + si := &state.StorageItem{Value: cvs.Bytes()} 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 { c := new(candidate).FromBytes(si.Value) 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 }) @@ -668,7 +665,7 @@ func (n *NEO) ComputeNextBlockValidators(bc blockchainer.Blockchainer, d dao.DAO if vals := n.validators.Load().(keys.PublicKeys); vals != nil { return vals.Copy(), nil } - result, err := n.ComputeCommitteeMembers(bc, d) + result, _, err := n.computeCommitteeMembers(bc, d) if err != nil { 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. 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 (n *NEO) ComputeCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) { +func toKeysWithVotes(pubs keys.PublicKeys) keysWithVotes { + 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} si := d.GetStorageItem(n.ContractID, key) 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 / 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)) if voterTurnout.Sign() != 1 { pubs := bc.GetStandByCommittee() - return pubs, nil + return pubs, nil, nil } cs, err := n.getCandidates(d) if err != nil { - return nil, err + return nil, nil, err } sbVals := bc.GetStandByCommittee() count := len(sbVals) if len(cs) < count { - return sbVals, nil + return sbVals, nil, nil } sort.Slice(cs, func(i, j int) bool { // 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) for i := range pubs { - pubs[i], err = keys.NewPublicKeyFromBytes([]byte(cs[i].Key), elliptic.P256()) + pubs[i], err = cs[i].PublicKey() 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 { diff --git a/pkg/core/native/neo_types.go b/pkg/core/native/neo_types.go new file mode 100644 index 000000000..aeefda5b9 --- /dev/null +++ b/pkg/core/native/neo_types.go @@ -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) +}