33c971b0e4
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>
404 lines
12 KiB
Go
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))
|
|
}
|