Skip to content

Commit

Permalink
ir: Support private attributes of the storage nodes
Browse files Browse the repository at this point in the history
Previously, storage nodes could declare (almost) any key-value
attributes while entering the network map. Sometimes there is a need
to restrict access to a specific attribute value. To do this, the
concept of a private node attribute is introduced. Access lists with
public key are stored in the NeoFS NNS: for each private attribute there
is a domain, and only nodes recorded in this domain are able to use
this attribute.

From now, the Inner Ring checks any incoming node for permission to use
private attributes (if any).

Closes #2280.

Signed-off-by: Leonard Lyubich <leonard@morphbits.io>
  • Loading branch information
cthulhu-rider committed Sep 25, 2023
1 parent 9207437 commit 93a5475
Show file tree
Hide file tree
Showing 13 changed files with 791 additions and 158 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ Changelog for NeoFS Node

## [Unreleased]

### Added
- Support of private attributes of the storage nodes (#2280)

### Fixed
- `neofs-cli netmap netinfo` documentation (#2555)
- `GETRANGEHASH` to a node without an object produced `GETRANGE` or `GET` requests (#2541)
Expand Down
4 changes: 4 additions & 0 deletions config/example/node.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ node:
- /dns4/s02.neofs.devenv/tcp/8081
- grpc://127.0.0.1:8082
- grpcs://localhost:8083
# List of colon-separated key-value attributes.
#
# Note that if the attribute is private, the node must have public key from the corresponding access list.
# See docs for more detailed information.
attribute_0: "Price:11"
attribute_1: UN-LOCODE:RU MSK
relay: true # start Storage node in relay mode without bootstrapping into the Network map
Expand Down
25 changes: 25 additions & 0 deletions docs/private-node-attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Private attributes of the NeoFS storage nodes

Storage nodes declare key-value string attributes when applying to enter the
network map. In general, any attributes can be declared, however, some of them
may be subject to restrictions. In particular, to specify so-called private
attributes, the node must be in the corresponding access list.

## Access lists

These lists are stored in the NeoFS NNS. For each private attribute value
`KEY=VALUE`, there is a domain with name `MD5(VALUE).MD5(KEY).private-node-attributes.neofs`
and records of `TXT` type with HEX-encoded public keys. If the domain exists
and has non-empty list of public key records, only storage nodes with keys from
this list will be able to set this attribute value. The Inner Ring will deny
everyone else access to the network map.

Note that if domain for the attribute exists but has no records, attribute is
non-private.

### Domain record format

For each public key, a record is created - a structure with at least 3 fields:
1. `ByteString` with name of the corresponding domain
2. `Integer` that should be `16` (TXT records)
3. `ByteString` with HEX-encoded public key
2 changes: 1 addition & 1 deletion docs/storage-node-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ node:
| `key` | `string` | | Path to the binary-encoded private key. |
| `wallet` | [Wallet config](#wallet-subsection) | | Wallet configuration. Has no effect if `key` is provided. |
| `addresses` | `[]string` | | Addresses advertised in the netmap. |
| `attribute` | `[]string` | | Node attributes as a list of key-value pairs in `<key>:<value>` format. |
| `attribute` | `[]string` | | Node attributes as a list of key-value pairs in `<key>:<value>` format. See also docs about private attributes.|
| `relay` | `bool` | | Enable relay mode. |
| `persistent_sessions` | [Persistent sessions config](#persistent_sessions-subsection) | | Persistent session token store configuration. |
| `persistent_state` | [Persistent state config](#persistent_state-subsection) | | Persistent state configuration. |
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ require (
github.com/nspcc-dev/hrw v1.0.9
github.com/nspcc-dev/neo-go v0.101.1
github.com/nspcc-dev/neofs-api-go/v2 v2.14.0
github.com/nspcc-dev/neofs-contract v0.16.0
github.com/nspcc-dev/neofs-contract v0.17.1-0.20230922122459-8170ce150d61
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.11
github.com/nspcc-dev/tzhash v1.7.0
github.com/olekukonko/tablewriter v0.0.5
Expand Down Expand Up @@ -72,7 +72,7 @@ require (
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nspcc-dev/dbft v0.0.0-20230315155759-60347b1563e7 // indirect
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20221202075445-cb5c18dc73eb // indirect
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20230208100456-1d6e48ee78e5 // indirect
github.com/nspcc-dev/neofs-crypto v0.4.0 // indirect
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
Expand Down
145 changes: 4 additions & 141 deletions go.sum

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions pkg/innerring/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,23 @@ import (
"time"

"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neofs-node/pkg/morph/client"
"github.com/spf13/cast"
"github.com/spf13/viper"
"go.uber.org/zap"
)

// provides functionality to work with Neo smart contract. The type allows to replace
// complex production components with the test ones.
type neoSmartContracts interface {
// CallIterating calls specified method of the referenced Neo smart contract
// that returns an iterator with provided arguments. If call succeeds,
// CallIterating iterates over all items and passes them into f. If f returns
// false, CallIterating breaks and returns nil.
CallIterating(contract util.Uint160, method string, args []any, f func(item stackitem.Item) bool) error
}

type contracts struct {
neofs util.Uint160 // in mainnet
netmap util.Uint160 // in morph
Expand Down
9 changes: 9 additions & 0 deletions pkg/innerring/innerring.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/nspcc-dev/neofs-node/pkg/innerring/processors/neofs"
"github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap"
nodevalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation"
attributevalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/attributes"
availabilityvalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/availability"
statevalidation "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/state"
addrvalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/structure"
Expand Down Expand Up @@ -730,6 +731,13 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper, errChan chan<-
var netMapCandidateStateValidator statevalidation.NetMapCandidateValidator
netMapCandidateStateValidator.SetNetworkSettings(netSettings)

nnsContractAddr, err := server.morphClient.NNSHash()
if err != nil {
return nil, fmt.Errorf("get NeoFS NNS contract address: %w", err)
}

nnsService := newNeoFSNNS(nnsContractAddr, server.morphClient)

// create netmap processor
server.netmapProcessor, err = netmap.New(&netmap.Params{
Log: log,
Expand All @@ -755,6 +763,7 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper, errChan chan<-
&netMapCandidateStateValidator,
addrvalidator.New(),
availabilityvalidator.New(),
attributevalidator.New(nnsService),
locodeValidator,
),
NodeStateSettings: netSettings,
Expand Down
114 changes: 114 additions & 0 deletions pkg/innerring/nns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package innerring

import (
"fmt"
"strings"

"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"
attributevalidator "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/netmap/nodevalidation/attributes"
)

// provides services of the NeoFS Name Service consumed by the Inner Ring node.
type neoFSNNS struct {
invoker nep11.Invoker
contract *nnsrpc.ContractReader
}

// creates NeoFS Name Service provider working with the Neo smart contract
// deployed in the Neo network accessed through the specified [nep11.Invoker].
func newNeoFSNNS(contractAddress util.Uint160, contractCaller nep11.Invoker) *neoFSNNS {
return &neoFSNNS{
invoker: contractCaller,
contract: nnsrpc.NewReader(contractCaller, contractAddress),
}
}

// 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
// [attributevalidator.ErrMissingDomain]. Each value in the resulting iterator
// is expected to be structure with at least 3 fields. If any value has the 2nd
// field is a number equal to 16 (TXT record type in the NNS) and the 3rd one is
// a string equal to the specified record, CheckDomainRecord returns nil.
// Otherwise, [attributevalidator.ErrMissingDomainRecord] is returned.
func (x *neoFSNNS) CheckDomainRecord(domainPath []string, record string) error {
domain := strings.Join(domainPath, ".")

sessionID, iter, err := x.contract.GetAllRecords(domain)
if err != nil {
// Track https://github.com/nspcc-dev/neofs-node/issues/2583.
if strings.Contains(err.Error(), "token not found") {
return attributevalidator.ErrMissingDomain
}

return fmt.Errorf("get iterator over all 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 fmt.Errorf("invalid element returned by iterator over all record of the NNS domain %q: unexpected type %s instead of %s",
domain, stackitem.StructT, items[i].Type())
}

if len(fields) < 3 {
return fmt.Errorf("invalid element returned by iterator over all record of the NNS domain %q: unsupported number of struct fields: expected at least 3, got %d",
domain, len(fields))
}

_, err = fields[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid element returned by iterator over all record of the NNS domain %q: convert 1st field to byte array: %w",
domain, err)
}

typ, err := fields[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid element returned by iterator over all record of the NNS domain %q: convert 2nd field to integer: %w",
domain, err)
}

if typ.Cmp(nnsrpc.TXT) != 0 {
return fmt.Errorf("invalid element returned by iterator over all record of the NNS domain %q: expected TXT (%v) record, got %v",
domain, nnsrpc.TXT, typ)
}

data, err := fields[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid element returned by iterator over all record of the NNS domain %q: convert 3rd field to byte array: %w",
domain, err)
}

if string(data) == record {
return nil
}
}
}

if hasRecords {
return attributevalidator.ErrMissingDomainRecord
}

return nil
}
Loading

0 comments on commit 93a5475

Please sign in to comment.