From 56ba4bb003ccc15c65ff94f783dc5cbf5bd10f3d Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 10 Oct 2023 20:11:06 +0400 Subject: [PATCH] ir: Call `resolve` method of the NNS smart contract to check the record Previously, Inner Ring called `getAllRecords` method to lookup for the particular entry. In particular, this method was used during validation of verified nodes' domains. Implementation was pretty complex due to low-levelness. The `resolve` method is much simpler, but it returns all records on each call. Taking into account that each domain can have no more than 255 records, this drawback is considered insignificant. From now, Inner Ring calls `resolve` method to check domain record existence. This is done as simple as possible through RPC interface provided by NeoFS Contracts lib. Refs #2280. Signed-off-by: Leonard Lyubich --- pkg/innerring/nns.go | 145 ++------------------------------ pkg/innerring/nns_test.go | 170 ++++---------------------------------- 2 files changed, 23 insertions(+), 292 deletions(-) diff --git a/pkg/innerring/nns.go b/pkg/innerring/nns.go index 1cd205c934..c82fdb50b3 100644 --- a/pkg/innerring/nns.go +++ b/pkg/innerring/nns.go @@ -7,7 +7,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" nnsrpc "github.com/nspcc-dev/neofs-contract/rpc/nns" "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/privatedomains" ) @@ -29,82 +28,6 @@ func newNeoFSNNS(contractAddress util.Uint160, contractCaller nep11.Invoker) *ne var errDomainNotFound = errors.New("domain not found") -type errWrongStackItemType struct { - expected, got stackitem.Type -} - -func wrongStackItemTypeError(expected, got stackitem.Type) errWrongStackItemType { - return errWrongStackItemType{ - expected: expected, - got: got, - } -} - -func (x errWrongStackItemType) Error() string { - return fmt.Sprintf("wrong type: expected %s, got %s", x.expected, x.got) -} - -type errInvalidNumberOfStructFields struct { - expected, got int -} - -func invalidNumberOfStructFieldsError(expected, got int) errInvalidNumberOfStructFields { - return errInvalidNumberOfStructFields{ - expected: expected, - got: got, - } -} - -func (x errInvalidNumberOfStructFields) Error() string { - return fmt.Sprintf("invalid number of struct fields: expected %d, got %d", x.expected, x.got) -} - -type errInvalidStructField struct { - index uint - cause error -} - -func invalidStructFieldError(index uint, cause error) errInvalidStructField { - return errInvalidStructField{ - index: index, - cause: cause, - } -} - -func (x errInvalidStructField) Error() string { - return fmt.Sprintf("invalid struct field #%d: %v", x.index, x.cause) -} - -func (x errInvalidStructField) Unwrap() error { - return x.cause -} - -type errInvalidNNSDomainRecord struct { - domain string - record string - cause error -} - -func invalidNNSDomainRecordError(domain, record string, cause error) errInvalidNNSDomainRecord { - return errInvalidNNSDomainRecord{ - domain: domain, - record: record, - cause: cause, - } -} - -func (x errInvalidNNSDomainRecord) Error() string { - if x.record != "" { - return fmt.Sprintf("invalid record %q of the NNS domain %q: %v", x.record, x.domain, x.cause) - } - - return fmt.Sprintf("invalid record of the NNS domain %q: %v", x.domain, x.cause) -} - -func (x errInvalidNNSDomainRecord) Unwrap() error { - return x.cause -} - // CheckDomainRecord calls iterating 'getAllRecords' method of the parameterized // Neo smart contract passing the given domain name. If contract throws 'token // not found' exception, CheckDomainRecord returns errDomainNotFound. Each value @@ -114,77 +37,21 @@ func (x errInvalidNNSDomainRecord) Unwrap() error { // CheckDomainRecord returns nil. Otherwise, // [privatedomains.ErrMissingDomainRecord] is returned. func (x *neoFSNNS) CheckDomainRecord(domain string, record string) error { - sessionID, iter, err := x.contract.GetAllRecords(domain) + records, err := x.contract.Resolve(domain, nnsrpc.TXT) if err != nil { // Track https://github.com/nspcc-dev/neofs-node/issues/2583. if strings.Contains(err.Error(), "token not found") { return errDomainNotFound } - return fmt.Errorf("get iterator over all records of the NNS domain %q: %w", domain, err) + return fmt.Errorf("get all text records of the NNS domain %q: %w", domain, err) } - defer func() { - _ = x.invoker.TerminateSession(sessionID) - }() - - hasRecords := false - - for { - items, err := x.invoker.TraverseIterator(sessionID, &iter, 10) - if err != nil { - return fmt.Errorf("traverse iterator over all records of the NNS domain %q: %w", domain, err) - } - - if len(items) == 0 { - break - } - - hasRecords = true - - for i := range items { - fields, ok := items[i].Value().([]stackitem.Item) - if !ok { - return invalidNNSDomainRecordError(domain, "", - wrongStackItemTypeError(stackitem.StructT, items[i].Type())) - } - - if len(fields) < 3 { - return invalidNNSDomainRecordError(domain, "", - invalidNumberOfStructFieldsError(3, len(fields))) - } - - _, err = fields[0].TryBytes() - if err != nil { - return invalidNNSDomainRecordError(domain, "", - invalidStructFieldError(0, wrongStackItemTypeError(stackitem.ByteArrayT, fields[0].Type()))) - } - - typ, err := fields[1].TryInteger() - if err != nil { - return invalidNNSDomainRecordError(domain, "", - invalidStructFieldError(1, wrongStackItemTypeError(stackitem.IntegerT, fields[1].Type()))) - } - - if typ.Cmp(nnsrpc.TXT) != 0 { - continue - } - - data, err := fields[2].TryBytes() - if err != nil { - return invalidNNSDomainRecordError(domain, "", - invalidStructFieldError(2, wrongStackItemTypeError(stackitem.ByteArrayT, fields[2].Type()))) - } - - if string(data) == record { - return nil - } + for i := range records { + if records[i] == record { + return nil } } - if hasRecords { - return privatedomains.ErrMissingDomainRecord - } - - return nil + return privatedomains.ErrMissingDomainRecord } diff --git a/pkg/innerring/nns_test.go b/pkg/innerring/nns_test.go index e664c84a20..d5fa15533c 100644 --- a/pkg/innerring/nns_test.go +++ b/pkg/innerring/nns_test.go @@ -25,11 +25,6 @@ type testInvoker struct { callRes *result.Invoke callErr error - - traverseErr error - - items []stackitem.Item - readItems int } func newTestInvoker(tb testing.TB, contractAddr util.Uint160, domain string) *testInvoker { @@ -42,9 +37,10 @@ func newTestInvoker(tb testing.TB, contractAddr util.Uint160, domain string) *te func (x *testInvoker) Call(contract util.Uint160, method string, args ...any) (*result.Invoke, error) { require.Equal(x.tb, x.contractAddr, contract) - require.Equal(x.tb, "getAllRecords", method) - require.Len(x.tb, args, 1) + require.Equal(x.tb, "resolve", method) + require.Len(x.tb, args, 2) require.Equal(x.tb, x.domain, args[0]) + require.Equal(x.tb, nnsrpc.TXT, args[1]) if x.callErr != nil { return nil, x.callErr @@ -58,26 +54,11 @@ func (x *testInvoker) CallAndExpandIterator(contract util.Uint160, method string } func (x *testInvoker) TerminateSession(sessionID uuid.UUID) error { - require.Equal(x.tb, x.callRes.Session, sessionID) - return nil + panic("not expected to be called") } func (x *testInvoker) TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) { - require.Equal(x.tb, x.callRes.Session, sessionID) - - if x.traverseErr != nil { - return nil, x.traverseErr - } - - if num > len(x.items)-x.readItems { - num = len(x.items) - x.readItems - } - - defer func() { - x.readItems += num - }() - - return x.items[x.readItems : x.readItems+num], nil + panic("not expected to be called") } func TestNeoFSNNS_CheckDomainRecord(t *testing.T) { @@ -125,28 +106,26 @@ func TestNeoFSNNS_CheckDomainRecord(t *testing.T) { nnsService := newNeoFSNNS(contractAddr, inv) err := nnsService.CheckDomainRecord(domain, searchedRecord) - require.Error(t, err) + require.ErrorContains(t, err, "result stack is empty") inv.callRes.Stack = make([]stackitem.Item, 2) err = nnsService.CheckDomainRecord(domain, searchedRecord) require.Error(t, err) - nonIteratorItem := stackitem.NewBool(true) - inv.callRes.Stack = []stackitem.Item{nonIteratorItem} + nonArrayItem := stackitem.NewBool(true) + inv.callRes.Stack = []stackitem.Item{nonArrayItem} err = nnsService.CheckDomainRecord(domain, searchedRecord) - require.Error(t, err) + require.ErrorContains(t, err, "not an array") }) newWithRecords := func(items ...stackitem.Item) *neoFSNNS { - var iter result.Iterator inv := newTestInvoker(t, contractAddr, domain) inv.callRes = &result.Invoke{ State: vmstate.Halt.String(), - Stack: []stackitem.Item{stackitem.NewInterop(iter)}, + Stack: []stackitem.Item{stackitem.NewArray(items)}, } - inv.items = items return newNeoFSNNS(contractAddr, inv) } @@ -155,129 +134,22 @@ func TestNeoFSNNS_CheckDomainRecord(t *testing.T) { nnsService := newWithRecords() err := nnsService.CheckDomainRecord(domain, searchedRecord) - require.NoError(t, err) + require.ErrorIs(t, err, privatedomains.ErrMissingDomainRecord) }) - checkInvalidRecordError := func(record string, err error) { - var e errInvalidNNSDomainRecord - require.ErrorAs(t, err, &e) - require.Equal(t, domain, e.domain) - require.Equal(t, record, e.record) - } - - checkInvalidStructFieldError := func(index uint, err error) { - var e errInvalidStructField - require.ErrorAs(t, err, &e) - require.Equal(t, index, e.index) - } - t.Run("with invalid record", func(t *testing.T) { - t.Run("non-struct element", func(t *testing.T) { - nnsService := newWithRecords(stackitem.NewBool(true)) - - err := nnsService.CheckDomainRecord(domain, searchedRecord) - - checkInvalidRecordError("", err) - - var wrongTypeErr errWrongStackItemType - require.ErrorAs(t, err, &wrongTypeErr) - require.Equal(t, stackitem.StructT, wrongTypeErr.expected) - require.Equal(t, stackitem.BooleanT, wrongTypeErr.got) - }) - - t.Run("invalid number of fields", func(t *testing.T) { - for i := 0; i < 3; i++ { - nnsService := newWithRecords(stackitem.NewStruct(make([]stackitem.Item, i))) - - err := nnsService.CheckDomainRecord(domain, searchedRecord) - - checkInvalidRecordError("", err) - - var invalidFieldNumErr errInvalidNumberOfStructFields - require.ErrorAs(t, err, &invalidFieldNumErr) - require.Equal(t, 3, invalidFieldNumErr.expected, i) - require.Equal(t, i, invalidFieldNumErr.got, i) - } - }) - - t.Run("invalid 1st field", func(t *testing.T) { - nnsService := newWithRecords(stackitem.NewStruct([]stackitem.Item{ - stackitem.NewMap(), - stackitem.NewBigInteger(nnsrpc.TXT), - stackitem.NewByteArray([]byte("any")), - })) + t.Run("non-string element", func(t *testing.T) { + nnsService := newWithRecords(stackitem.NewMap()) err := nnsService.CheckDomainRecord(domain, searchedRecord) - - checkInvalidRecordError("", err) - checkInvalidStructFieldError(0, err) - - var wrongTypeErr errWrongStackItemType - require.ErrorAs(t, err, &wrongTypeErr) - require.Equal(t, stackitem.ByteArrayT, wrongTypeErr.expected) - require.Equal(t, stackitem.MapT, wrongTypeErr.got) - }) - - t.Run("invalid 2nd field", func(t *testing.T) { - t.Run("non-integer", func(t *testing.T) { - nnsService := newWithRecords(stackitem.NewStruct([]stackitem.Item{ - stackitem.NewByteArray([]byte("any")), - stackitem.NewMap(), - stackitem.NewByteArray([]byte("any")), - })) - - err := nnsService.CheckDomainRecord(domain, searchedRecord) - - checkInvalidRecordError("", err) - checkInvalidStructFieldError(1, err) - - var wrongTypeErr errWrongStackItemType - require.ErrorAs(t, err, &wrongTypeErr) - require.Equal(t, stackitem.IntegerT, wrongTypeErr.expected) - require.Equal(t, stackitem.MapT, wrongTypeErr.got) - }) - - t.Run("non-TXT record", func(t *testing.T) { - nnsService := newWithRecords(stackitem.NewStruct([]stackitem.Item{ - stackitem.NewByteArray([]byte("any")), - stackitem.NewBigInteger(nnsrpc.CNAME), - stackitem.NewByteArray([]byte("any")), - })) - - err := nnsService.CheckDomainRecord(domain, searchedRecord) - require.ErrorIs(t, err, privatedomains.ErrMissingDomainRecord) - }) - }) - - t.Run("invalid 3rd field", func(t *testing.T) { - t.Run("invalid type", func(t *testing.T) { - nnsService := newWithRecords(stackitem.NewStruct([]stackitem.Item{ - stackitem.NewByteArray([]byte("any")), - stackitem.NewBigInteger(nnsrpc.TXT), - stackitem.NewMap(), - })) - - err := nnsService.CheckDomainRecord(domain, searchedRecord) - - checkInvalidRecordError("", err) - checkInvalidStructFieldError(2, err) - - var wrongTypeErr errWrongStackItemType - require.ErrorAs(t, err, &wrongTypeErr) - require.Equal(t, stackitem.ByteArrayT, wrongTypeErr.expected) - require.Equal(t, stackitem.MapT, wrongTypeErr.got) - }) + require.ErrorIs(t, err, stackitem.ErrInvalidConversion) }) }) t.Run("without searched record", func(t *testing.T) { records := make([]stackitem.Item, 100) for i := range records { - records[i] = stackitem.NewStruct([]stackitem.Item{ - stackitem.NewByteArray([]byte("any")), - stackitem.NewBigInteger(nnsrpc.TXT), - stackitem.NewByteArray([]byte(searchedRecord + hex.EncodeToString([]byte{byte(i)}))), - }) + records[i] = stackitem.NewByteArray([]byte(searchedRecord + hex.EncodeToString([]byte{byte(i)}))) } nnsService := newWithRecords(records...) @@ -289,18 +161,10 @@ func TestNeoFSNNS_CheckDomainRecord(t *testing.T) { t.Run("with searched record", func(t *testing.T) { records := make([]stackitem.Item, 100) for i := range records { - records[i] = stackitem.NewStruct([]stackitem.Item{ - stackitem.NewByteArray([]byte("any")), - stackitem.NewBigInteger(nnsrpc.TXT), - stackitem.NewByteArray([]byte(searchedRecord + hex.EncodeToString([]byte{byte(i)}))), - }) + records[i] = stackitem.NewByteArray([]byte(searchedRecord + hex.EncodeToString([]byte{byte(i)}))) } - records = append(records, stackitem.NewStruct([]stackitem.Item{ - stackitem.NewByteArray([]byte("any")), - stackitem.NewBigInteger(nnsrpc.TXT), - stackitem.NewByteArray([]byte(searchedRecord)), - })) + records = append(records, stackitem.NewByteArray([]byte(searchedRecord))) nnsService := newWithRecords(records...)