neoneo-go/pkg/core/native/designate.go
Anna Shaleva 33c971b0e4 core: add InitializeCache method to Contract interface
Make the contracts cache initialization unified. The order of cache
iniitialization is not important and Nottary contract is added to the
bc.contracts.Contracts wrt P2PSigExtensions setting, thus no functional
changes, just refactoring for future applications.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2023-04-26 12:57:48 +03:00

404 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/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
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) *Designate {
s := &Designate{ContractMD: *interop.NewContractMD(nativenames.Designation, designateContractID)}
s.p2pSigExtensionsEnabled = p2pSigExtensionsEnabled
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)
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) {
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
}
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))
}