neoneo-go/pkg/core/native/designate.go
Anna Shaleva 3b11f98cd0 core: review usages of (*intero.Context).BlockHeight method
This method returns persisted block height and doesn't take into account
persisting block height. Some of the callers of this method relay on
the wrong assumption that BlockHeight() returns persisting block index.

Fix improper usages of this method and adjust tests. Ref.
61a066583e/src/Neo/SmartContract/ApplicationEngine.cs (L634).

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2023-11-21 13:46:13 +03:00

426 lines
12 KiB
Go

package native
import (
"encoding/binary"
"errors"
"fmt"
"math"
"math/big"
"sort"
"sync/atomic"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
"github.com/nspcc-dev/neo-go/pkg/core/stateroot"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// Designate represents a designation contract.
type Designate struct {
interop.ContractMD
NEO *NEO
// p2pSigExtensionsEnabled defines whether the P2P signature extensions logic is relevant.
p2pSigExtensionsEnabled bool
// initialNodeRoles defines a set of node roles that should be defined at the contract
// deployment (initialization).
initialNodeRoles map[noderoles.Role]keys.PublicKeys
OracleService atomic.Value
// NotaryService represents a Notary node module.
NotaryService atomic.Value
// StateRootService represents a StateRoot node module.
StateRootService *stateroot.Module
}
type roleData struct {
nodes keys.PublicKeys
addr util.Uint160
height uint32
}
type DesignationCache struct {
// rolesChangedFlag shows whether any of designated nodes were changed within the current block.
// It is used to notify dependant services about updated node roles during PostPersist.
rolesChangedFlag bool
oracles roleData
stateVals roleData
neofsAlphabet roleData
notaries roleData
}
const (
designateContractID = -8
// maxNodeCount is the maximum number of nodes to set the role for.
maxNodeCount = 32
// DesignationEventName is the name of the designation event.
DesignationEventName = "Designation"
)
// Various errors.
var (
ErrAlreadyDesignated = errors.New("already designated given role at current block")
ErrEmptyNodeList = errors.New("node list is empty")
ErrInvalidIndex = errors.New("invalid index")
ErrInvalidRole = errors.New("invalid role")
ErrLargeNodeList = errors.New("node list is too large")
ErrNoBlock = errors.New("no persisting block in the context")
)
var (
_ interop.Contract = (*Designate)(nil)
_ dao.NativeContractCache = (*DesignationCache)(nil)
)
// Copy implements NativeContractCache interface.
func (c *DesignationCache) Copy() dao.NativeContractCache {
cp := &DesignationCache{}
copyDesignationCache(c, cp)
return cp
}
func copyDesignationCache(src, dst *DesignationCache) {
*dst = *src
}
func (s *Designate) isValidRole(r noderoles.Role) bool {
return r == noderoles.Oracle || r == noderoles.StateValidator ||
r == noderoles.NeoFSAlphabet || (s.p2pSigExtensionsEnabled && r == noderoles.P2PNotary)
}
func newDesignate(p2pSigExtensionsEnabled bool, initialNodeRoles map[noderoles.Role]keys.PublicKeys) *Designate {
s := &Designate{ContractMD: *interop.NewContractMD(nativenames.Designation, designateContractID)}
s.p2pSigExtensionsEnabled = p2pSigExtensionsEnabled
s.initialNodeRoles = initialNodeRoles
defer s.UpdateHash()
desc := newDescriptor("getDesignatedByRole", smartcontract.ArrayType,
manifest.NewParameter("role", smartcontract.IntegerType),
manifest.NewParameter("index", smartcontract.IntegerType))
md := newMethodAndPrice(s.getDesignatedByRole, 1<<15, callflag.ReadStates)
s.AddMethod(md, desc)
desc = newDescriptor("designateAsRole", smartcontract.VoidType,
manifest.NewParameter("role", smartcontract.IntegerType),
manifest.NewParameter("nodes", smartcontract.ArrayType))
md = newMethodAndPrice(s.designateAsRole, 1<<15, callflag.States|callflag.AllowNotify)
s.AddMethod(md, desc)
s.AddEvent(DesignationEventName,
manifest.NewParameter("Role", smartcontract.IntegerType),
manifest.NewParameter("BlockIndex", smartcontract.IntegerType))
return s
}
// Initialize initializes Designation contract. It is called once at native Management's OnPersist
// at the genesis block, and we can't properly fill the cache at this point, as there are no roles
// data in the storage.
func (s *Designate) Initialize(ic *interop.Context) error {
cache := &DesignationCache{}
ic.DAO.SetCache(s.ID, cache)
if len(s.initialNodeRoles) != 0 {
for _, r := range noderoles.Roles {
pubs, ok := s.initialNodeRoles[r]
if !ok {
continue
}
err := s.DesignateAsRole(ic, r, pubs)
if err != nil {
return fmt.Errorf("failed to initialize Designation role data for role %s: %w", r, err)
}
}
}
return nil
}
// InitializeCache fills native Designate cache from DAO. It is called at non-zero height, thus
// we can fetch the roles data right from the storage.
func (s *Designate) InitializeCache(blockHeight uint32, d *dao.Simple) error {
cache := &DesignationCache{}
roles := []noderoles.Role{noderoles.Oracle, noderoles.NeoFSAlphabet, noderoles.StateValidator}
if s.p2pSigExtensionsEnabled {
roles = append(roles, noderoles.P2PNotary)
}
for _, r := range roles {
err := s.updateCachedRoleData(cache, d, r)
if err != nil {
return fmt.Errorf("failed to get nodes from storage for %d role: %w", r, err)
}
}
d.SetCache(s.ID, cache)
return nil
}
// OnPersist implements the Contract interface.
func (s *Designate) OnPersist(ic *interop.Context) error {
return nil
}
// PostPersist implements the Contract interface.
func (s *Designate) PostPersist(ic *interop.Context) error {
cache := ic.DAO.GetRWCache(s.ID).(*DesignationCache)
if !cache.rolesChangedFlag {
return nil
}
s.notifyRoleChanged(&cache.oracles, noderoles.Oracle)
s.notifyRoleChanged(&cache.stateVals, noderoles.StateValidator)
s.notifyRoleChanged(&cache.neofsAlphabet, noderoles.NeoFSAlphabet)
if s.p2pSigExtensionsEnabled {
s.notifyRoleChanged(&cache.notaries, noderoles.P2PNotary)
}
cache.rolesChangedFlag = false
return nil
}
// Metadata returns contract metadata.
func (s *Designate) Metadata() *interop.ContractMD {
return &s.ContractMD
}
func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.Item) stackitem.Item {
r, ok := s.getRole(args[0])
if !ok {
panic(ErrInvalidRole)
}
ind, err := args[1].TryInteger()
if err != nil || !ind.IsUint64() {
panic(ErrInvalidIndex)
}
index := ind.Uint64()
if index > uint64(ic.BlockHeight()+1) { // persisting block should be taken into account.
panic(ErrInvalidIndex)
}
pubs, _, err := s.GetDesignatedByRole(ic.DAO, r, uint32(index))
if err != nil {
panic(err)
}
return pubsToArray(pubs)
}
func (s *Designate) hashFromNodes(r noderoles.Role, nodes keys.PublicKeys) util.Uint160 {
if len(nodes) == 0 {
return util.Uint160{}
}
var script []byte
switch r {
case noderoles.P2PNotary:
script, _ = smartcontract.CreateMultiSigRedeemScript(1, nodes.Copy())
default:
script, _ = smartcontract.CreateDefaultMultiSigRedeemScript(nodes.Copy())
}
return hash.Hash160(script)
}
// updateCachedRoleData fetches the most recent role data from the storage and
// updates the given cache.
func (s *Designate) updateCachedRoleData(cache *DesignationCache, d *dao.Simple, r noderoles.Role) error {
var v *roleData
switch r {
case noderoles.Oracle:
v = &cache.oracles
case noderoles.StateValidator:
v = &cache.stateVals
case noderoles.NeoFSAlphabet:
v = &cache.neofsAlphabet
case noderoles.P2PNotary:
v = &cache.notaries
}
nodeKeys, height, err := s.getDesignatedByRoleFromStorage(d, r, math.MaxUint32)
if err != nil {
return err
}
v.nodes = nodeKeys
v.addr = s.hashFromNodes(r, nodeKeys)
v.height = height
cache.rolesChangedFlag = true
return nil
}
func (s *Designate) notifyRoleChanged(v *roleData, r noderoles.Role) {
switch r {
case noderoles.Oracle:
if orc, _ := s.OracleService.Load().(*OracleService); orc != nil && *orc != nil {
(*orc).UpdateOracleNodes(v.nodes.Copy())
}
case noderoles.P2PNotary:
if ntr, _ := s.NotaryService.Load().(*NotaryService); ntr != nil && *ntr != nil {
(*ntr).UpdateNotaryNodes(v.nodes.Copy())
}
case noderoles.StateValidator:
if s.StateRootService != nil {
s.StateRootService.UpdateStateValidators(v.height, v.nodes.Copy())
}
}
}
func getCachedRoleData(cache *DesignationCache, r noderoles.Role) *roleData {
switch r {
case noderoles.Oracle:
return &cache.oracles
case noderoles.StateValidator:
return &cache.stateVals
case noderoles.NeoFSAlphabet:
return &cache.neofsAlphabet
case noderoles.P2PNotary:
return &cache.notaries
}
return nil
}
// GetLastDesignatedHash returns the last designated hash of the given role.
func (s *Designate) GetLastDesignatedHash(d *dao.Simple, r noderoles.Role) (util.Uint160, error) {
if !s.isValidRole(r) {
return util.Uint160{}, ErrInvalidRole
}
cache := d.GetROCache(s.ID).(*DesignationCache)
if val := getCachedRoleData(cache, r); val != nil {
return val.addr, nil
}
return util.Uint160{}, nil
}
// GetDesignatedByRole returns nodes for role r.
func (s *Designate) GetDesignatedByRole(d *dao.Simple, r noderoles.Role, index uint32) (keys.PublicKeys, uint32, error) {
if !s.isValidRole(r) {
return nil, 0, ErrInvalidRole
}
cache := d.GetROCache(s.ID).(*DesignationCache)
if val := getCachedRoleData(cache, r); val != nil {
if val.height <= index {
return val.nodes.Copy(), val.height, nil
}
} else {
// Cache is always valid, thus if there's no cache then there's no designated nodes for this role.
return nil, 0, nil
}
// Cache stores only latest designated nodes, so if the old info is requested, then we still need
// to search in the storage.
return s.getDesignatedByRoleFromStorage(d, r, index)
}
// getDesignatedByRoleFromStorage returns nodes for role r from the storage.
func (s *Designate) getDesignatedByRoleFromStorage(d *dao.Simple, r noderoles.Role, index uint32) (keys.PublicKeys, uint32, error) {
var (
ns NodeList
bestIndex uint32
resVal []byte
start = make([]byte, 4)
)
binary.BigEndian.PutUint32(start, index)
d.Seek(s.ID, storage.SeekRange{
Prefix: []byte{byte(r)},
Start: start,
Backwards: true,
}, func(k, v []byte) bool {
bestIndex = binary.BigEndian.Uint32(k) // If len(k) < 4 the DB is broken and it deserves a panic.
resVal = v
// Take just the latest item, it's the one we need.
return false
})
if resVal != nil {
err := stackitem.DeserializeConvertible(resVal, &ns)
if err != nil {
return nil, 0, err
}
}
return keys.PublicKeys(ns), bestIndex, nil
}
func (s *Designate) designateAsRole(ic *interop.Context, args []stackitem.Item) stackitem.Item {
r, ok := s.getRole(args[0])
if !ok {
panic(ErrInvalidRole)
}
var ns NodeList
if err := ns.FromStackItem(args[1]); err != nil {
panic(err)
}
err := s.DesignateAsRole(ic, r, keys.PublicKeys(ns))
if err != nil {
panic(err)
}
return stackitem.Null{}
}
// DesignateAsRole sets nodes for role r.
func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs keys.PublicKeys) error {
length := len(pubs)
if length == 0 {
return ErrEmptyNodeList
}
if length > maxNodeCount {
return ErrLargeNodeList
}
if !s.isValidRole(r) {
return ErrInvalidRole
}
if ic.Trigger != trigger.OnPersist {
h := s.NEO.GetCommitteeAddress(ic.DAO)
if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok {
return ErrInvalidWitness
}
}
if ic.Block == nil {
return ErrNoBlock
}
var key = make([]byte, 5)
key[0] = byte(r)
binary.BigEndian.PutUint32(key[1:], ic.Block.Index+1)
si := ic.DAO.GetStorageItem(s.ID, key)
if si != nil {
return ErrAlreadyDesignated
}
sort.Sort(pubs)
nl := NodeList(pubs)
err := putConvertibleToDAO(s.ID, ic.DAO, key, &nl)
if err != nil {
return err
}
cache := ic.DAO.GetRWCache(s.ID).(*DesignationCache)
err = s.updateCachedRoleData(cache, ic.DAO, r)
if err != nil {
return fmt.Errorf("failed to update Designation role data cache: %w", err)
}
ic.AddNotification(s.Hash, DesignationEventName, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(r))),
stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))),
}))
return nil
}
func (s *Designate) getRole(item stackitem.Item) (noderoles.Role, bool) {
bi, err := item.TryInteger()
if err != nil {
return 0, false
}
if !bi.IsUint64() {
return 0, false
}
u := bi.Uint64()
return noderoles.Role(u), u <= math.MaxUint8 && s.isValidRole(noderoles.Role(u))
}