frostfs-node/pkg/morph/client/nns.go
Roman Khimov 96b38f7e86 [#239] morph/client: Add CalledByEntry into the "grouped" scope
Fixes #2230, fixes #2263. CustomGroups are nice while we're only calling NeoFS
contracts, but it doesn't work at all for standard ones like GAS or Notary.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-13 13:15:30 +00:00

249 lines
6.6 KiB
Go

package client
import (
"errors"
"fmt"
"math/big"
"strconv"
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
)
const (
nnsContractID = 1 // NNS contract must be deployed first in the sidechain
// NNSAuditContractName is a name of the audit contract in NNS.
NNSAuditContractName = "audit.frostfs"
// NNSBalanceContractName is a name of the balance contract in NNS.
NNSBalanceContractName = "balance.frostfs"
// NNSContainerContractName is a name of the container contract in NNS.
NNSContainerContractName = "container.frostfs"
// NNSFrostFSIDContractName is a name of the frostfsid contract in NNS.
NNSFrostFSIDContractName = "frostfsid.frostfs"
// NNSNetmapContractName is a name of the netmap contract in NNS.
NNSNetmapContractName = "netmap.frostfs"
// NNSProxyContractName is a name of the proxy contract in NNS.
NNSProxyContractName = "proxy.frostfs"
// NNSReputationContractName is a name of the reputation contract in NNS.
NNSReputationContractName = "reputation.frostfs"
// NNSSubnetworkContractName is a name of the subnet contract in NNS.
NNSSubnetworkContractName = "subnet.frostfs"
// NNSGroupKeyName is a name for the FrostFS group key record in NNS.
NNSGroupKeyName = "group.frostfs"
)
var (
// ErrNNSRecordNotFound means that there is no such record in NNS contract.
ErrNNSRecordNotFound = errors.New("record has not been found in NNS contract")
errEmptyResultStack = errors.New("returned result stack is empty")
)
// NNSAlphabetContractName returns contract name of the alphabet contract in NNS
// based on alphabet index.
func NNSAlphabetContractName(index int) string {
return "alphabet" + strconv.Itoa(index) + ".frostfs"
}
// NNSContractAddress returns contract address script hash based on its name
// in NNS contract.
// If script hash has not been found, returns ErrNNSRecordNotFound.
func (c *Client) NNSContractAddress(name string) (sh util.Uint160, err error) {
c.switchLock.RLock()
defer c.switchLock.RUnlock()
if c.inactive {
return util.Uint160{}, ErrConnectionLost
}
nnsHash, err := c.NNSHash()
if err != nil {
return util.Uint160{}, err
}
sh, err = nnsResolve(c.client, nnsHash, name)
if err != nil {
return sh, fmt.Errorf("NNS.resolve: %w", err)
}
return sh, nil
}
// NNSHash returns NNS contract hash.
func (c *Client) NNSHash() (util.Uint160, error) {
c.switchLock.RLock()
defer c.switchLock.RUnlock()
if c.inactive {
return util.Uint160{}, ErrConnectionLost
}
nnsHash := c.cache.nns()
if nnsHash == nil {
cs, err := c.client.GetContractStateByID(nnsContractID)
if err != nil {
return util.Uint160{}, fmt.Errorf("NNS contract state: %w", err)
}
c.cache.setNNSHash(cs.Hash)
nnsHash = &cs.Hash
}
return *nnsHash, nil
}
func nnsResolveItem(c *rpcclient.WSClient, nnsHash util.Uint160, domain string) (stackitem.Item, error) {
found, err := exists(c, nnsHash, domain)
if err != nil {
return nil, fmt.Errorf("could not check presence in NNS contract for %s: %w", domain, err)
}
if !found {
return nil, ErrNNSRecordNotFound
}
result, err := c.InvokeFunction(nnsHash, "resolve", []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: domain,
},
{
Type: smartcontract.IntegerType,
Value: big.NewInt(int64(nns.TXT)),
},
}, nil)
if err != nil {
return nil, err
}
if result.State != vmstate.Halt.String() {
return nil, fmt.Errorf("invocation failed: %s", result.FaultException)
}
if len(result.Stack) == 0 {
return nil, errEmptyResultStack
}
return result.Stack[0], nil
}
func nnsResolve(c *rpcclient.WSClient, nnsHash util.Uint160, domain string) (util.Uint160, error) {
res, err := nnsResolveItem(c, nnsHash, domain)
if err != nil {
return util.Uint160{}, err
}
// Parse the result of resolving NNS record.
// It works with multiple formats (corresponding to multiple NNS versions).
// If array of hashes is provided, it returns only the first one.
if arr, ok := res.Value().([]stackitem.Item); ok {
if len(arr) == 0 {
return util.Uint160{}, errors.New("NNS record is missing")
}
res = arr[0]
}
bs, err := res.TryBytes()
if err != nil {
return util.Uint160{}, fmt.Errorf("malformed response: %w", err)
}
// We support several formats for hash encoding, this logic should be maintained in sync
// with parseNNSResolveResult from cmd/frostfs-adm/internal/modules/morph/initialize_nns.go
h, err := util.Uint160DecodeStringLE(string(bs))
if err == nil {
return h, nil
}
h, err = address.StringToUint160(string(bs))
if err == nil {
return h, nil
}
return util.Uint160{}, errors.New("no valid hashes are found")
}
func exists(c *rpcclient.WSClient, nnsHash util.Uint160, domain string) (bool, error) {
result, err := c.InvokeFunction(nnsHash, "isAvailable", []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: domain,
},
}, nil)
if err != nil {
return false, err
}
if len(result.Stack) == 0 {
return false, errEmptyResultStack
}
res := result.Stack[0]
available, err := res.TryBool()
if err != nil {
return false, fmt.Errorf("malformed response: %w", err)
}
// not available means that it is taken
// and, therefore, exists
return !available, nil
}
// SetGroupSignerScope makes the default signer scope include all FrostFS contracts.
// Should be called for side-chain client only.
func (c *Client) SetGroupSignerScope() error {
c.switchLock.RLock()
defer c.switchLock.RUnlock()
if c.inactive {
return ErrConnectionLost
}
pub, err := c.contractGroupKey()
if err != nil {
return err
}
c.signer.Scopes = transaction.CustomGroups | transaction.CalledByEntry
c.signer.AllowedGroups = []*keys.PublicKey{pub}
return nil
}
// contractGroupKey returns public key designating FrostFS contract group.
func (c *Client) contractGroupKey() (*keys.PublicKey, error) {
if gKey := c.cache.groupKey(); gKey != nil {
return gKey, nil
}
nnsHash, err := c.NNSHash()
if err != nil {
return nil, err
}
item, err := nnsResolveItem(c.client, nnsHash, NNSGroupKeyName)
if err != nil {
return nil, err
}
arr, ok := item.Value().([]stackitem.Item)
if !ok || len(arr) == 0 {
return nil, errors.New("NNS record is missing")
}
bs, err := arr[0].TryBytes()
if err != nil {
return nil, err
}
pub, err := keys.NewPublicKeyFromString(string(bs))
if err != nil {
return nil, err
}
c.cache.setGroupKey(pub)
return pub, nil
}