Skip to content

Commit

Permalink
Simplify resolving NNS hashes via RPC binding (#361)
Browse files Browse the repository at this point in the history
  • Loading branch information
roman-khimov authored Sep 26, 2023
2 parents bc33c71 + 14a2984 commit 86765df
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 0 deletions.
51 changes: 51 additions & 0 deletions rpc/nns/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package nns_test

import (
"context"
"fmt"
"log"

"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neofs-contract/rpc/nns"
)

// Resolve addresses of NeoFS smart contracts deployed in a particular
// NeoFS sidechain by their NNS domain names.
func ExampleContractReader_ResolveFSContract() {
const sidechainRPCEndpoint = "https://rpc1.morph.fs.neo.org:40341"

c, err := rpcclient.New(context.Background(), sidechainRPCEndpoint, rpcclient.Options{})
if err != nil {
log.Fatal(err)
}

err = c.Init()
if err != nil {
log.Fatal(err)
}

nnsAddress, err := nns.InferHash(c)
if err != nil {
log.Fatal(err)
}

nnsContract := nns.NewReader(invoker.New(c, nil), nnsAddress)

for _, name := range []string{
nns.NameAudit,
nns.NameBalance,
nns.NameContainer,
nns.NameNeoFSID,
nns.NameNetmap,
nns.NameProxy,
nns.NameReputation,
} {
addr, err := nnsContract.ResolveFSContract(name)
if err != nil {
log.Fatal(err)
}

fmt.Printf("%s: %s\n", name, addr)
}
}
62 changes: 62 additions & 0 deletions rpc/nns/hashes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package nns

import (
"errors"

"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/util"
)

// ID is the default NNS contract ID in all NeoFS networks. NeoFS networks
// always deploy NNS first and can't work without it, therefore it always gets
// an ID of 1.
const ID = 1

// ContractTLD is the default TLD for NeoFS contracts. It's a convention that
// is not likely to be used by any non-NeoFS networks, but for NeoFS ones it
// allows to find contract hashes more easily.
const ContractTLD = "neofs"

// ContractStateGetter is the interface required for contract state resolution
// using a known contract ID.
type ContractStateGetter interface {
GetContractStateByID(int32) (*state.Contract, error)
}

// InferHash simplifies resolving NNS contract hash in existing NeoFS networks.
// It assumes that NNS follows [ID] assignment assumptions which likely won't
// be the case for any non-NeoFS network.
func InferHash(sg ContractStateGetter) (util.Uint160, error) {
c, err := sg.GetContractStateByID(ID)
if err != nil {
return util.Uint160{}, err
}

return c.Hash, nil
}

// ResolveFSContract is a convenience method that doesn't exist in the NNS
// contract itself (it doesn't care which data is stored there). It assumes
// that contracts follow the [ContractTLD] convention, gets simple contract
// names (like "container" or "netmap") and extracts the hash for the
// respective NNS record in any of the formats (of which historically there's
// been a few).
func (c *ContractReader) ResolveFSContract(name string) (util.Uint160, error) {
strs, err := c.Resolve(name+"."+ContractTLD, TXT)
if err != nil {
return util.Uint160{}, err
}
for i := range strs {
h, err := util.Uint160DecodeStringLE(strs[i])
if err == nil {
return h, nil
}

h, err = address.StringToUint160(strs[i])
if err == nil {
return h, nil
}
}
return util.Uint160{}, errors.New("no valid hashes are found")
}
115 changes: 115 additions & 0 deletions rpc/nns/hashes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package nns

import (
"errors"
"testing"

"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)

type stateGetter struct {
f func(int32) (*state.Contract, error)
}

func (s stateGetter) GetContractStateByID(id int32) (*state.Contract, error) {
return s.f(id)
}

func TestInferHash(t *testing.T) {
var sg stateGetter
sg.f = func(int32) (*state.Contract, error) {
return nil, errors.New("bad")
}
_, err := InferHash(sg)
require.Error(t, err)
sg.f = func(int32) (*state.Contract, error) {
return &state.Contract{
ContractBase: state.ContractBase{
Hash: util.Uint160{0x01, 0x02, 0x03},
},
}, nil
}
h, err := InferHash(sg)
require.NoError(t, err)
require.Equal(t, util.Uint160{0x01, 0x02, 0x03}, h)
}

type testInv struct {
err error
res *result.Invoke
}

func (t *testInv) Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) {
return t.res, t.err
}

func (t *testInv) CallAndExpandIterator(contract util.Uint160, operation string, i int, params ...any) (*result.Invoke, error) {
return t.res, t.err
}
func (t *testInv) TraverseIterator(uuid.UUID, *result.Iterator, int) ([]stackitem.Item, error) {
return nil, nil
}
func (t *testInv) TerminateSession(uuid.UUID) error {
return nil
}

func TestBaseErrors(t *testing.T) {
ti := new(testInv)
r := NewReader(ti, util.Uint160{1, 2, 3})

ti.err = errors.New("bad")
_, err := r.ResolveFSContract("blah")
require.Error(t, err)

ti.err = nil
ti.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{}),
},
}
_, err = r.ResolveFSContract("blah")
require.Error(t, err)

ti.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{
stackitem.Make(100500),
}),
},
}
_, err = r.ResolveFSContract("blah")
require.Error(t, err)

h := util.Uint160{1, 2, 3, 4, 5}
ti.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{
stackitem.Make(h.StringLE()),
}),
},
}
res, err := r.ResolveFSContract("blah")
require.NoError(t, err)
require.Equal(t, h, res)

ti.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{
stackitem.Make(address.Uint160ToString(h)),
}),
},
}
res, err = r.ResolveFSContract("blah")
require.NoError(t, err)
require.Equal(t, h, res)
}
16 changes: 16 additions & 0 deletions rpc/nns/names.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package nns

// A set of standard contract names deployed into NeoFS sidechain.
const (
// NameAlphabetPrefix differs from other names in this list, because
// in reality there will be multiple alphabets contract deployed to
// a network named alphabet0, alphabet1, alphabet2, etc.
NameAlphabetPrefix = "alphabet"
NameAudit = "audit"
NameBalance = "balance"
NameContainer = "container"
NameNeoFSID = "neofsid"
NameNetmap = "netmap"
NameProxy = "proxy"
NameReputation = "reputation"
)

0 comments on commit 86765df

Please sign in to comment.