From ead918965159ba625852e1a259b80b7c0c9ee14d Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 22 Sep 2023 10:23:16 +0300 Subject: [PATCH 1/4] rpc/nns: add InferHash convenience function Refs. #348 Signed-off-by: Roman Khimov --- rpc/nns/hashes.go | 29 +++++++++++++++++++++++++++++ rpc/nns/hashes_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 rpc/nns/hashes.go create mode 100644 rpc/nns/hashes_test.go diff --git a/rpc/nns/hashes.go b/rpc/nns/hashes.go new file mode 100644 index 00000000..affcff2d --- /dev/null +++ b/rpc/nns/hashes.go @@ -0,0 +1,29 @@ +package nns + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/state" + "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 + +// 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 +} diff --git a/rpc/nns/hashes_test.go b/rpc/nns/hashes_test.go new file mode 100644 index 00000000..a7d87017 --- /dev/null +++ b/rpc/nns/hashes_test.go @@ -0,0 +1,37 @@ +package nns + +import ( + "errors" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/util" + "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) +} From 52a55f5ec6303e38d9877073c06bef33665068ee Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 22 Sep 2023 10:51:20 +0300 Subject: [PATCH 2/4] rpc/nns: add a method to resolve NeoFS contracts Fix #348. Signed-off-by: Roman Khimov --- rpc/nns/hashes.go | 33 ++++++++++++++++++ rpc/nns/hashes_test.go | 78 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/rpc/nns/hashes.go b/rpc/nns/hashes.go index affcff2d..41ac44c7 100644 --- a/rpc/nns/hashes.go +++ b/rpc/nns/hashes.go @@ -1,7 +1,10 @@ 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" ) @@ -10,6 +13,11 @@ import ( // 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 { @@ -27,3 +35,28 @@ func InferHash(sg ContractStateGetter) (util.Uint160, error) { 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") +} diff --git a/rpc/nns/hashes_test.go b/rpc/nns/hashes_test.go index a7d87017..cac26970 100644 --- a/rpc/nns/hashes_test.go +++ b/rpc/nns/hashes_test.go @@ -4,8 +4,12 @@ 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" ) @@ -35,3 +39,77 @@ func TestInferHash(t *testing.T) { 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) +} From 28102a76fec5473c3278ee5d8db0921b2d34c117 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 25 Sep 2023 19:50:20 +0300 Subject: [PATCH 3/4] rpc/nns: add standard contract names Signed-off-by: Roman Khimov --- rpc/nns/names.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 rpc/nns/names.go diff --git a/rpc/nns/names.go b/rpc/nns/names.go new file mode 100644 index 00000000..e88de9bf --- /dev/null +++ b/rpc/nns/names.go @@ -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" +) From 14a298455b1a429e4dd728a96e5c89fbb87e70ce Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 26 Sep 2023 16:07:36 +0300 Subject: [PATCH 4/4] nns: add ResolveFSContract example By @cthulhu-rider, thanks. Signed-off-by: Roman Khimov --- rpc/nns/example_test.go | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 rpc/nns/example_test.go diff --git a/rpc/nns/example_test.go b/rpc/nns/example_test.go new file mode 100644 index 00000000..a803666e --- /dev/null +++ b/rpc/nns/example_test.go @@ -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) + } +}