frostfs-node/pkg/morph/client/nns.go
Evgenii Stratonikov a4da1da767
All checks were successful
Vulncheck / Vulncheck (push) Successful in 1m19s
Pre-commit hooks / Pre-commit (push) Successful in 1m41s
Build / Build Components (push) Successful in 1m43s
Tests and linters / Run gofumpt (push) Successful in 3m38s
Tests and linters / gopls check (push) Successful in 3m41s
Tests and linters / Lint (push) Successful in 3m50s
Tests and linters / Staticcheck (push) Successful in 4m6s
Tests and linters / Tests with -race (push) Successful in 4m22s
Tests and linters / Tests (push) Successful in 4m32s
OCI image / Build container images (push) Successful in 5m0s
[#905] morph/client: Fetch NNS hash once on init
NNS contract hash is taken from the contract with ID=1.
Because morph client is expected to work with the same chain,
and because contract hash doesn't change on update, there is no need to
fetch it from each new endpoint.

Change-Id: Ic6dc18283789da076d6a0b3701139b97037714cc
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2025-03-21 15:13:54 +00:00

239 lines
6.3 KiB
Go

package client
import (
"errors"
"fmt"
"math/big"
"strconv"
"time"
"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
// 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"
// NNSGroupKeyName is a name for the FrostFS group key record in NNS.
NNSGroupKeyName = "group.frostfs"
// NNSPolicyContractName is a name of the policy contract in NNS.
NNSPolicyContractName = "policy.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 := c.NNSHash()
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 {
return c.nnsHash
}
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("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
}
// Don't change c before everything is OK.
cfg := c.cfg
cfg.signer = &transaction.Signer{
Scopes: transaction.CustomGroups | transaction.CalledByEntry,
AllowedGroups: []*keys.PublicKey{pub},
}
rpcActor, err := newActor(c.client, c.acc, cfg)
if err != nil {
return err
}
c.cfg = cfg
c.setActor(rpcActor)
return nil
}
// contractGroupKey returns public key designating FrostFS contract group.
func (c *Client) contractGroupKey() (*keys.PublicKey, error) {
success := false
startedAt := time.Now()
defer func() {
c.cache.metrics.AddMethodDuration("GroupKey", success, time.Since(startedAt))
}()
if gKey := c.cache.groupKey(); gKey != nil {
success = true
return gKey, nil
}
nnsHash := c.NNSHash()
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)
success = true
return pub, nil
}