Skip to content

Commit

Permalink
adm: Add commands to work with private node attributes
Browse files Browse the repository at this point in the history
Add command to get and set list of public keys for the storage nodes
allowed to use private attribute.

Refs #2280.

Signed-off-by: Leonard Lyubich <[email protected]>
  • Loading branch information
cthulhu-rider committed Sep 25, 2023
1 parent 50d91ed commit e9c205e
Show file tree
Hide file tree
Showing 5 changed files with 421 additions and 1 deletion.
2 changes: 1 addition & 1 deletion cmd/neofs-adm/internal/modules/morph/n3client.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type clientContext struct {
SentTxs []hashVUBPair
}

func getN3Client(v *viper.Viper) (Client, error) {
func getN3Client(v *viper.Viper) (*rpcclient.Client, error) {
// number of opened connections
// by neo-go client per one host
const (
Expand Down
121 changes: 121 additions & 0 deletions cmd/neofs-adm/internal/modules/morph/nns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package morph

import (
"errors"
"fmt"
"strings"

"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"
)

// Various NeoFS NNS errors.
var (
errMissingDomain = errors.New("missing domain")
errMissingDomainRecords = errors.New("missing domain records")
)

type errInvalidNNSDomainRecord struct {
domain string
cause error
}

func invalidNNSDomainRecordError(domain string, cause error) errInvalidNNSDomainRecord {
return errInvalidNNSDomainRecord{
domain: domain,
cause: cause,
}
}

func (x errInvalidNNSDomainRecord) Error() string {
return fmt.Sprintf("invalid record of the NNS domain %q: %s", x.domain, x.cause)
}

func (x errInvalidNNSDomainRecord) Unwrap() error {
return x.cause
}

var errBreakIterator = errors.New("break iterator")

// iterates over text records of the specified NeoFS NNS domain and passes them
// into f. Breaks on any f's error and returns it (if f returns
// errBreakIterator, iterateNNSDomainTextRecords returns no error). Returns
// errMissingDomain if domain is missing in the NNS. Returns
// errMissingDomainRecords if domain exists but has no records.
func iterateNNSDomainTextRecords(inv nnsrpc.Invoker, nnsContractAddr util.Uint160, domain string, f func(string) error) error {
nnsContract := nnsrpc.NewReader(inv, nnsContractAddr)

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

return fmt.Errorf("init iterator over all records of the NNS domain %q: %w", domain, err)
}

defer func() {
_ = inv.TerminateSession(sessionID)
}()

hasRecords := false

for {
items, err := inv.TraverseIterator(sessionID, &iter, 10)
if err != nil {
return fmt.Errorf("NNS domain %q records' iterator break: %w", domain, err)
}

if len(items) == 0 {
if hasRecords {
return nil
}

return errMissingDomainRecords
}

hasRecords = true

for i := range items {
fields, ok := items[i].Value().([]stackitem.Item)
if !ok {
return invalidNNSDomainRecordError(domain,
fmt.Errorf("unexpected type %s instead of %s", stackitem.StructT, items[i].Type()))
}

if len(fields) < 3 {
return invalidNNSDomainRecordError(domain,
fmt.Errorf("unsupported number of struct fields: expected at least 3, got %d", len(fields)))
}

_, err = fields[0].TryBytes()
if err != nil {
return invalidNNSDomainRecordError(domain, fmt.Errorf("1st field is not a byte array: got %v", fields[0].Type()))
}

typ, err := fields[1].TryInteger()
if err != nil {
return invalidNNSDomainRecordError(domain, fmt.Errorf("2nd field is not an integer: got %v", fields[1].Type()))
}

if typ.Cmp(nnsrpc.TXT) != 0 {
return invalidNNSDomainRecordError(domain, fmt.Errorf("non-TXT record of type %v", typ))
}

data, err := fields[2].TryBytes()
if err != nil {
return invalidNNSDomainRecordError(domain, fmt.Errorf("3rd field is not a byte array: got %v", fields[2].Type()))
}

if err = f(string(data)); err != nil {
if errors.Is(err, errBreakIterator) {
return nil
}

return invalidNNSDomainRecordError(domain, err)
}
}
}
}
208 changes: 208 additions & 0 deletions cmd/neofs-adm/internal/modules/morph/private_node_attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package morph

import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"

"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
nnsrpc "github.com/nspcc-dev/neofs-contract/rpc/nns"
"github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func attributeNNSDomain(s string) string {
cs := md5.Sum([]byte(s))
return hex.EncodeToString(cs[:])
}

const privateAttributesRootDomain = "private-node-attributes.neofs"

func privateAttributeDomain(key, value string) string {
return attributeNNSDomain(value) + "." + attributeNNSDomain(key) + "." + privateAttributesRootDomain
}

func privateAttributeAccessList(cmd *cobra.Command, _ []string) error {
vpr := viper.GetViper()

attrKey := vpr.GetString(attributeKeyFlag)
if attrKey == "" {
return errors.New("empty attribute key is not allowed")
}

attrVal := vpr.GetString(attributeValueFlag)
if attrVal == "" {
return errors.New("empty attribute value is not allowed")
}

n3Client, err := getN3Client(vpr)
if err != nil {
return fmt.Errorf("open connection: %w", err)
}

nnsState, err := n3Client.GetContractStateByID(1)
if err != nil {
return fmt.Errorf("get NeoFS NNS contract state: %w", err)
}

domain := privateAttributeDomain(attrKey, attrVal)

err = iterateNNSDomainTextRecords(invoker.New(n3Client, nil), nnsState.Hash, domain, func(rec string) error {
_, err := hex.DecodeString(rec)
if err != nil {
return fmt.Errorf("not a HEX string %q", rec)
}

cmd.Println(rec)

return nil
})
if err != nil {
if errors.Is(err, errMissingDomain) {
cmd.Printf("Attribute is not private (missing NNS domain %q).\n", domain)
return nil
}

if errors.Is(err, errMissingDomainRecords) {
cmd.Printf("Attribute is not private (NNS domain %q has no records).\n", domain)
return nil
}
}

return err
}

func privateAttributeSetAccessList(cmd *cobra.Command, _ []string) error {
vpr := viper.GetViper()

attrKey := vpr.GetString(attributeKeyFlag)
if attrKey == "" {
return errors.New("empty attribute key is not allowed")
}

attrVal := vpr.GetString(attributeValueFlag)
if attrVal == "" {
return errors.New("empty attribute value is not allowed")
}

strKeys := vpr.GetStringSlice(publicKeysFlag)
if len(strKeys) == 0 {
return errors.New("empty public key list is not allowed")
}

for i := range strKeys {
_, err := hex.DecodeString(strKeys[i])
if err != nil {
return fmt.Errorf("key #%d is not a valid HEX string", i)
}

for j := i + 1; j < len(strKeys); j++ {
if strKeys[i] == strKeys[j] {
return fmt.Errorf("duplicated public key %s", strKeys[i])
}
}
}

walletDir := config.ResolveHomePath(vpr.GetString(alphabetWalletsFlag))

wallets, err := openAlphabetWallets(vpr, walletDir)
if err != nil {
return err
}

committeeAcc, err := getWalletAccount(wallets[0], committeeAccountName)
if err != nil {
return fmt.Errorf("get committee account: %w", err)
}

n3Client, err := getN3Client(vpr)
if err != nil {
return fmt.Errorf("open connection: %w", err)
}

nnsState, err := n3Client.GetContractStateByID(1)
if err != nil {
return fmt.Errorf("get NeoFS NNS contract state: %w", err)
}

actr, err := actor.NewSimple(n3Client, committeeAcc)
if err != nil {
return fmt.Errorf("init committee actor: %w", err)
}

attrKeyDomain := attributeNNSDomain(attrKey)
attrValDomain := attributeNNSDomain(attrVal)

fullDomain := attrValDomain + "." + attrKeyDomain + "." + privateAttributesRootDomain

scriptBuilder := smartcontract.NewBuilder()

hasOtherKey := false
mAlreadySetIndices := make(map[int]struct{}, len(strKeys))

err = iterateNNSDomainTextRecords(actr, nnsState.Hash, fullDomain, func(rec string) error {
for i := range strKeys {
if strKeys[i] == rec {
mAlreadySetIndices[i] = struct{}{}
return nil
}
}

hasOtherKey = true

return errBreakIterator
})
if err != nil {
switch {
default:
return err
case errors.Is(err, errMissingDomain):
scriptBuilder.InvokeMethod(nnsState.Hash, "register",
privateAttributesRootDomain, committeeAcc.ScriptHash(), "[email protected]", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
case errors.Is(err, errMissingDomainRecords):
}
}

if !hasOtherKey && len(mAlreadySetIndices) == len(strKeys) {
cmd.Println("Key list is already the same, skip.")
return nil
}

if hasOtherKey {
// there is no way to delete particular key, so clean all first
scriptBuilder.InvokeMethod(nnsState.Hash, "deleteRecords",
fullDomain, nnsrpc.TXT.Int64())
}

for i := range strKeys {
if !hasOtherKey {
if _, ok := mAlreadySetIndices[i]; ok {
continue
}
}

scriptBuilder.InvokeMethod(nnsState.Hash, "addRecord",
fullDomain, nnsrpc.TXT.Int64(), strKeys[i])
}

txScript, err := scriptBuilder.Script()
if err != nil {
return fmt.Errorf("build transaction script: %w", err)
}

txID, vub, err := actr.SendRun(txScript)
if err != nil {
if err != nil {
return fmt.Errorf("send transction with built script: %w", err)
}
}

return awaitTx(cmd, n3Client, []hashVUBPair{{
hash: txID,
vub: vub,
}})
}
Loading

0 comments on commit e9c205e

Please sign in to comment.