[#179] resolver: Refactor and document the package
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
parent
c961aea144
commit
146fc4f07a
5 changed files with 276 additions and 102 deletions
|
@ -1,41 +1,35 @@
|
||||||
package resolver
|
package resolver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// DNS looks up NeoFS names using system DNS.
|
||||||
testnetDomain = ".containers.testnet.fs.neo.org"
|
//
|
||||||
mainnetDomain = ".containers.fs.neo.org"
|
// See also net package.
|
||||||
)
|
type DNS struct{}
|
||||||
|
|
||||||
// ResolveContainerTestnet request txt record name + '.containers.testnet.fs.neo.org' to default dns server.
|
// ResolveContainerName looks up for DNS TXT records for the given domain name
|
||||||
func ResolveContainerTestnet(name string) (*cid.ID, error) {
|
// and returns the first one which represents valid container ID in a string format.
|
||||||
return ResolveContainerDomainName(name + testnetDomain)
|
// Otherwise, returns an error.
|
||||||
}
|
//
|
||||||
|
// See also net.LookupTXT.
|
||||||
// ResolveContainerMainnet request txt record name + '.containers.fs.neo.org' to default dns server.
|
func (x *DNS) ResolveContainerName(name string) (*cid.ID, error) {
|
||||||
func ResolveContainerMainnet(name string) (*cid.ID, error) {
|
records, err := net.LookupTXT(name)
|
||||||
return ResolveContainerDomainName(name + mainnetDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveContainerDomainName trys to resolve container domain name to container ID using system dns server.
|
|
||||||
func ResolveContainerDomainName(domain string) (*cid.ID, error) {
|
|
||||||
results, err := net.LookupTXT(domain)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cnrID := cid.New()
|
var id cid.ID
|
||||||
for _, res := range results {
|
|
||||||
if err = cnrID.Parse(res); err != nil {
|
for i := range records {
|
||||||
continue
|
err = id.Parse(records[i])
|
||||||
|
if err == nil {
|
||||||
|
return &id, nil
|
||||||
}
|
}
|
||||||
return cnrID, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("not found")
|
return nil, errNotFound
|
||||||
}
|
}
|
||||||
|
|
22
resolver/doc.go
Normal file
22
resolver/doc.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
Package resolver provides functionality of NeoFS name system.
|
||||||
|
|
||||||
|
DNS type is designed to resolve NeoFS-related names using Domain Name System:
|
||||||
|
const containerName = "some-container"
|
||||||
|
|
||||||
|
var dns DNS
|
||||||
|
|
||||||
|
containerID, err := dns.ResolveContainerName(containerName)
|
||||||
|
// ...
|
||||||
|
|
||||||
|
NNS type is designed to resolve NeoFS-related names using Neo Name Service:
|
||||||
|
var nns NNS
|
||||||
|
|
||||||
|
err := nns.Dial(nnsServerAddress)
|
||||||
|
// ...
|
||||||
|
|
||||||
|
containerID, err := nns.ResolveContainerName(containerName)
|
||||||
|
// ...
|
||||||
|
|
||||||
|
*/
|
||||||
|
package resolver
|
154
resolver/nns.go
154
resolver/nns.go
|
@ -1,10 +1,12 @@
|
||||||
package resolver
|
package resolver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
|
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
|
neoclient "github.com/nspcc-dev/neo-go/pkg/rpc/client"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
|
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
@ -13,109 +15,103 @@ import (
|
||||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NNSResolver interface {
|
// NNS looks up NeoFS names using Neo Name Service.
|
||||||
// ResolveContainerName get record for domain name+'.container' from NNS contract.
|
//
|
||||||
ResolveContainerName(name string) (*cid.ID, error)
|
// Instances are created with a variable declaration. Before work, the connection
|
||||||
|
// to the NNS server MUST BE established using Dial method.
|
||||||
|
type NNS struct {
|
||||||
|
nnsContract util.Uint160
|
||||||
|
|
||||||
|
// neoclient.Client interface wrapper, needed for testing
|
||||||
|
neoClient interface {
|
||||||
|
invoke(contract util.Uint160, method string, prm []smartcontract.Parameter) (*result.Invoke, error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type nnsResolver struct {
|
// client is a core implementation of internal NNS.neoClient which is used by NNS.Dial.
|
||||||
rpc *client.Client
|
type client neoclient.Client
|
||||||
nnsHash util.Uint160
|
|
||||||
|
func (x *client) invoke(contract util.Uint160, method string, prm []smartcontract.Parameter) (*result.Invoke, error) {
|
||||||
|
return (*neoclient.Client)(x).InvokeFunction(contract, method, prm, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// Dial connects to the address of the NNS server. If fails, the instance
|
||||||
resolve = "resolve"
|
// SHOULD NOT be used.
|
||||||
containerDomain = ".container"
|
func (n *NNS) Dial(address string) error {
|
||||||
)
|
cli, err := neoclient.New(context.Background(), address, neoclient.Options{})
|
||||||
|
|
||||||
// NewNNSResolver creates resolver that can get records from NNS contract.
|
|
||||||
func NewNNSResolver(rpc *client.Client) (NNSResolver, error) {
|
|
||||||
nnsContract, err := rpc.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return fmt.Errorf("create neo client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &nnsResolver{
|
if err = cli.Init(); err != nil {
|
||||||
rpc: rpc,
|
return fmt.Errorf("initialize neo client: %w", err)
|
||||||
nnsHash: nnsContract.Hash,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *nnsResolver) ResolveContainerName(name string) (*cid.ID, error) {
|
nnsContract, err := cli.GetContractStateByID(1)
|
||||||
res, err := n.rpc.InvokeFunction(n.nnsHash, resolve, []smartcontract.Parameter{
|
if err != nil {
|
||||||
|
return fmt.Errorf("get NNS contract state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n.neoClient = (*client)(cli)
|
||||||
|
n.nnsContract = nnsContract.Hash
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveContainerName looks up for NNS TXT records for the given container name
|
||||||
|
// by calling `resolve` method of NNS contract. Returns the first record which represents
|
||||||
|
// valid container ID in a string format. Otherwise, returns an error.
|
||||||
|
//
|
||||||
|
// ResolveContainerName MUST NOT be called before successful Dial.
|
||||||
|
//
|
||||||
|
// See also https://docs.neo.org/docs/en-us/reference/nns.html.
|
||||||
|
func (n *NNS) ResolveContainerName(name string) (*cid.ID, error) {
|
||||||
|
res, err := n.neoClient.invoke(n.nnsContract, "resolve", []smartcontract.Parameter{
|
||||||
{
|
{
|
||||||
Type: smartcontract.StringType,
|
Type: smartcontract.StringType,
|
||||||
Value: name + containerDomain,
|
Value: name + ".container",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Type: smartcontract.IntegerType,
|
Type: smartcontract.IntegerType,
|
||||||
Value: int64(nns.TXT),
|
Value: int64(nns.TXT),
|
||||||
},
|
},
|
||||||
}, nil)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("invoke NNS contract: %w", err)
|
||||||
}
|
|
||||||
if err = getInvocationError(res); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
arr, err := getArrString(res.Stack)
|
if res.State != vm.HaltState.String() {
|
||||||
|
return nil, fmt.Errorf("NNS contract fault exception: %s", res.FaultException)
|
||||||
|
} else if len(res.Stack) == 0 {
|
||||||
|
return nil, errors.New("empty stack in invocation result")
|
||||||
|
}
|
||||||
|
|
||||||
|
itemArr, err := res.Stack[len(res.Stack)-1].Convert(stackitem.ArrayT) // top stack element is last in the array
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("convert stack item to %s", stackitem.ArrayT)
|
||||||
}
|
}
|
||||||
|
|
||||||
cnrID := cid.New()
|
if _, ok := itemArr.(stackitem.Null); !ok {
|
||||||
for _, rec := range arr {
|
arr, ok := itemArr.Value().([]stackitem.Item)
|
||||||
if err = cnrID.Parse(rec); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return cnrID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getArrString(st []stackitem.Item) ([]string, error) {
|
|
||||||
array, err := getArray(st)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res := make([]string, len(array))
|
|
||||||
for i, item := range array {
|
|
||||||
bs, err := item.TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res[i] = string(bs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getArray(st []stackitem.Item) ([]stackitem.Item, error) {
|
|
||||||
index := len(st) - 1 // top stack element is last in the array
|
|
||||||
arr, err := st[index].Convert(stackitem.ArrayT)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, ok := arr.(stackitem.Null); ok {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
iterator, ok := arr.Value().([]stackitem.Item)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("bad conversion")
|
// unexpected for types from stackitem package
|
||||||
}
|
return nil, errors.New("invalid cast to stack item slice")
|
||||||
return iterator, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getInvocationError(result *result.Invoke) error {
|
var id cid.ID
|
||||||
if result.State != vm.HaltState.String() {
|
|
||||||
return fmt.Errorf("invocation failed: %s", result.FaultException)
|
for i := range arr {
|
||||||
|
bs, err := arr[i].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("convert array item to byte slice: %w", err)
|
||||||
}
|
}
|
||||||
if len(result.Stack) == 0 {
|
|
||||||
return fmt.Errorf("result stack is empty")
|
err = id.Parse(string(bs))
|
||||||
|
if err == nil {
|
||||||
|
return &id, nil
|
||||||
}
|
}
|
||||||
return nil
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errNotFound
|
||||||
}
|
}
|
||||||
|
|
157
resolver/nns_test.go
Normal file
157
resolver/nns_test.go
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
package resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testNeoClient represents test Neo client which checks invocation arguments
|
||||||
|
// and returns predefined result.
|
||||||
|
type testNeoClient struct {
|
||||||
|
t *testing.T
|
||||||
|
|
||||||
|
expectedContract util.Uint160
|
||||||
|
|
||||||
|
res result.Invoke
|
||||||
|
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *testNeoClient) invoke(contract util.Uint160, method string, prm []smartcontract.Parameter) (*result.Invoke, error) {
|
||||||
|
require.Equal(x.t, x.expectedContract, contract)
|
||||||
|
require.Equal(x.t, "resolve", method)
|
||||||
|
require.Len(x.t, prm, 2)
|
||||||
|
require.Equal(x.t, smartcontract.StringType, prm[0].Type)
|
||||||
|
require.Equal(x.t, smartcontract.IntegerType, prm[1].Type)
|
||||||
|
require.EqualValues(x.t, nns.TXT, prm[1].Value)
|
||||||
|
|
||||||
|
val, ok := prm[0].Value.(string)
|
||||||
|
require.True(x.t, ok)
|
||||||
|
require.True(x.t, strings.HasSuffix(val, ".container"))
|
||||||
|
require.NotEmpty(x.t, strings.TrimSuffix(val, ".container"))
|
||||||
|
|
||||||
|
return &x.res, x.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements test stackitem.Item which is obviously incorrect:
|
||||||
|
// it returns itself on Convert(stackitem.ArrayT), but returns integer from Value.
|
||||||
|
type brokenArrayStackItem struct {
|
||||||
|
stackitem.Item
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x brokenArrayStackItem) Value() interface{} {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x brokenArrayStackItem) Convert(t stackitem.Type) (stackitem.Item, error) {
|
||||||
|
if t != stackitem.ArrayT {
|
||||||
|
panic(fmt.Sprintf("unexpected stack item type %s", t))
|
||||||
|
}
|
||||||
|
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNNS_ResolveContainerName(t *testing.T) {
|
||||||
|
const testContainerName = "some_container"
|
||||||
|
|
||||||
|
var nnsContract util.Uint160
|
||||||
|
|
||||||
|
rand.Read(nnsContract[:])
|
||||||
|
|
||||||
|
testC := &testNeoClient{
|
||||||
|
t: t,
|
||||||
|
expectedContract: nnsContract,
|
||||||
|
}
|
||||||
|
|
||||||
|
n := NNS{
|
||||||
|
nnsContract: nnsContract,
|
||||||
|
neoClient: testC,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("invocation failure", func(t *testing.T) {
|
||||||
|
testC.err = errors.New("invoke err")
|
||||||
|
|
||||||
|
_, err := n.ResolveContainerName(testContainerName)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
testC.err = nil
|
||||||
|
|
||||||
|
t.Run("fault exception", func(t *testing.T) {
|
||||||
|
_, err := n.ResolveContainerName(testContainerName)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
testC.res.State = vm.HaltState.String()
|
||||||
|
|
||||||
|
t.Run("empty stack", func(t *testing.T) {
|
||||||
|
_, err := n.ResolveContainerName(testContainerName)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
testC.res.Stack = make([]stackitem.Item, 1)
|
||||||
|
|
||||||
|
t.Run("non-array last stack item", func(t *testing.T) {
|
||||||
|
testC.res.Stack[0] = stackitem.NewBigInteger(big.NewInt(11))
|
||||||
|
|
||||||
|
_, err := n.ResolveContainerName(testContainerName)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("null array", func(t *testing.T) {
|
||||||
|
testC.res.Stack[0] = stackitem.Null{}
|
||||||
|
|
||||||
|
_, err := n.ResolveContainerName(testContainerName)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("array stack item with non-slice value", func(t *testing.T) {
|
||||||
|
testC.res.Stack[0] = brokenArrayStackItem{}
|
||||||
|
|
||||||
|
_, err := n.ResolveContainerName(testContainerName)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
arr := make([]stackitem.Item, 2)
|
||||||
|
testC.res.Stack[0] = stackitem.NewArray(arr)
|
||||||
|
|
||||||
|
t.Run("non-bytes array element", func(t *testing.T) {
|
||||||
|
arr[0] = stackitem.NewArray(nil)
|
||||||
|
|
||||||
|
_, err := n.ResolveContainerName(testContainerName)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
arr[0] = stackitem.NewByteArray([]byte("some byte array 1"))
|
||||||
|
|
||||||
|
t.Run("non-container array elements", func(t *testing.T) {
|
||||||
|
arr[1] = stackitem.NewByteArray([]byte("some byte array 2"))
|
||||||
|
|
||||||
|
_, err := n.ResolveContainerName(testContainerName)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with container array element", func(t *testing.T) {
|
||||||
|
id := cidtest.ID()
|
||||||
|
|
||||||
|
arr[1] = stackitem.NewByteArray([]byte(id.String()))
|
||||||
|
|
||||||
|
res, err := n.ResolveContainerName(testContainerName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, id, res)
|
||||||
|
})
|
||||||
|
}
|
5
resolver/util.go
Normal file
5
resolver/util.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package resolver
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var errNotFound = errors.New("record not found")
|
Loading…
Reference in a new issue