-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
adm: Add commands to work with private node attributes
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
1 parent
50d91ed
commit e9c205e
Showing
5 changed files
with
421 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
208
cmd/neofs-adm/internal/modules/morph/private_node_attributes.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}}) | ||
} |
Oops, something went wrong.