diff --git a/cmd/neofs-adm/internal/modules/morph/local_client.go b/cmd/neofs-adm/internal/modules/morph/local_client.go index 7b862d838..746079c7a 100644 --- a/cmd/neofs-adm/internal/modules/morph/local_client.go +++ b/cmd/neofs-adm/internal/modules/morph/local_client.go @@ -19,7 +19,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" @@ -143,7 +145,7 @@ func (l *localClient) CreateTxFromScript(script []byte, acc *wallet.Account, sys tx.Signers = signers tx.ValidUntilBlock = l.bc.BlockHeight() + 2 - err = l.addNetworkFee(tx, netFee, accounts...) + err = l.AddNetworkFee(tx, netFee, accounts...) if err != nil { return nil, fmt.Errorf("failed to add network fee: %w", err) } @@ -151,11 +153,36 @@ func (l *localClient) CreateTxFromScript(script []byte, acc *wallet.Account, sys return tx, nil } -// addNetworkFee adds network fee for each witness script and optional extra +func (l *localClient) GetCommittee() (keys.PublicKeys, error) { + // not used by `morph init` command + panic("unexpected call") +} + +func (l *localClient) InvokeFunction(_ util.Uint160, _ string, _ []smartcontract.Parameter, _ []transaction.Signer) (*result.Invoke, error) { + // not used by `morph init` command + panic("unexpected call") +} + +func (l *localClient) CalculateNotaryFee(_ uint8) (int64, error) { + // not used by `morph init` command + panic("unexpected call") +} + +func (l *localClient) SignAndPushP2PNotaryRequest(_ *transaction.Transaction, _ []byte, _ int64, _ int64, _ uint32, _ *wallet.Account) (*payload.P2PNotaryRequest, error) { + // not used by `morph init` command + panic("unexpected call") +} + +func (l *localClient) SignAndPushInvocationTx(_ []byte, _ *wallet.Account, _ int64, _ fixedn.Fixed8, _ []client.SignerAccount) (util.Uint256, error) { + // not used by `morph init` command + panic("unexpected call") +} + +// AddNetworkFee adds network fee for each witness script and optional extra // network fee to transaction. `accs` is an array signer's accounts. // Copied from neo-go with minor corrections (no need to support contract signers): // https://github.com/nspcc-dev/neo-go/blob/6ff11baa1b9e4c71ef0d1de43b92a8c541ca732c/pkg/rpc/client/rpc.go#L960 -func (l *localClient) addNetworkFee(tx *transaction.Transaction, extraFee int64, accs ...*wallet.Account) error { +func (l *localClient) AddNetworkFee(tx *transaction.Transaction, extraFee int64, accs ...*wallet.Account) error { if len(tx.Signers) != len(accs) { return errors.New("number of signers must match number of scripts") } diff --git a/cmd/neofs-adm/internal/modules/morph/n3client.go b/cmd/neofs-adm/internal/modules/morph/n3client.go index 82f1ef61f..fdf1b6330 100644 --- a/cmd/neofs-adm/internal/modules/morph/n3client.go +++ b/cmd/neofs-adm/internal/modules/morph/n3client.go @@ -9,6 +9,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" + "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" @@ -31,6 +34,11 @@ type Client interface { NEP17BalanceOf(util.Uint160, util.Uint160) (int64, error) InvokeScript([]byte, []transaction.Signer) (*result.Invoke, error) SendRawTransaction(*transaction.Transaction) (util.Uint256, error) + GetCommittee() (keys.PublicKeys, error) + CalculateNotaryFee(uint8) (int64, error) + AddNetworkFee(*transaction.Transaction, int64, ...*wallet.Account) error + SignAndPushInvocationTx([]byte, *wallet.Account, int64, fixedn.Fixed8, []client.SignerAccount) (util.Uint256, error) + SignAndPushP2PNotaryRequest(*transaction.Transaction, []byte, int64, int64, uint32, *wallet.Account) (*payload.P2PNotaryRequest, error) } type clientContext struct { diff --git a/cmd/neofs-adm/internal/modules/morph/subnet.go b/cmd/neofs-adm/internal/modules/morph/subnet.go index 036494519..8f8f88663 100644 --- a/cmd/neofs-adm/internal/modules/morph/subnet.go +++ b/cmd/neofs-adm/internal/modules/morph/subnet.go @@ -7,12 +7,20 @@ import ( "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/input" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + neogocli "github.com/nspcc-dev/neo-go/pkg/rpc/client" + "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/morph/internal" "github.com/nspcc-dev/neofs-node/pkg/morph/client" - morphsubnet "github.com/nspcc-dev/neofs-node/pkg/morph/client/subnet" "github.com/nspcc-dev/neofs-node/pkg/util/rand" "github.com/nspcc-dev/neofs-sdk-go/subnet" subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id" @@ -21,12 +29,6 @@ import ( "github.com/spf13/viper" ) -// cmdSubnet flags. -const ( - // Neo RPC endpoint - flagSubnetEndpoint = endpointFlag -) - func viperBindFlags(cmd *cobra.Command, flags ...string) { for i := range flags { _ = viper.BindPFlag(flags[i], cmd.Flags().Lookup(flags[i])) @@ -39,7 +41,7 @@ var cmdSubnet = &cobra.Command{ Short: "NeoFS subnet management.", PreRun: func(cmd *cobra.Command, _ []string) { viperBindFlags(cmd, - flagSubnetEndpoint, + endpointFlag, ) }, } @@ -110,71 +112,11 @@ func readSubnetKey(key *keys.PrivateKey) error { return nil } -// calls initSubnetClientCheckNotary with unset checkNotary. -func initSubnetClient(c *morphsubnet.Client, key *keys.PrivateKey) error { - return initSubnetClientCheckNotary(c, key, false) -} - const ( // enable notary flagSubnetNotary = "notary" ) -// initializes morph subnet client with the specified private key. -// -// Parameters are read from: -// * contract address: flagSubnetContract; -// * endpoint: flagSubnetEndpoint. -// -// If checkNotary is set, notary mode is read from flagSubnetNotary. -func initSubnetClientCheckNotary(c *morphsubnet.Client, key *keys.PrivateKey, checkNotary bool) error { - // read endpoint - endpoint := viper.GetString(flagSubnetEndpoint) - if endpoint == "" { - return errors.New("missing endpoint") - } - - // create base morph client - cMorph, err := client.New(key, endpoint) - if err != nil { - return err - } - - // Error means group was not set in NNS, continue with Global scope in this case. - _ = cMorph.SetGroupSignerScope() - - // read contract address - contractAddr, err := cMorph.NNSContractAddress(client.NNSSubnetworkContractName) - if err != nil { - return fmt.Errorf("read subnet contract address in NNS: %w", err) - } - - // calc client mode - cMode := morphsubnet.NonNotary - - if checkNotary && viper.GetBool(flagSubnetNotary) { - err = cMorph.EnableNotarySupport() - if err != nil { - return fmt.Errorf("enable notary support: %w", err) - } - - cMode = morphsubnet.NotaryNonAlphabet - } - - // init subnet morph client - var prmInit morphsubnet.InitPrm - - prmInit.SetBaseClient(cMorph) - prmInit.SetContractAddress(contractAddr) - prmInit.SetMode(cMode) - - if err := c.Init(prmInit); err != nil { - return fmt.Errorf("init call: %w", err) - } - - return nil -} - // create subnet command. var cmdSubnetCreate = &cobra.Command{ Use: "create", @@ -231,24 +173,9 @@ var cmdSubnetCreate = &cobra.Command{ return fmt.Errorf("marshal subnet info: %w", err) } - // initialize morph subnet client - var cSubnet morphsubnet.Client - - err = initSubnetClientCheckNotary(&cSubnet, &key, true) + err = invokeMethod(key, true, "put", binID, key.PublicKey().Bytes(), binInfo) if err != nil { - return fmt.Errorf("init subnet client: %w", err) - } - - // prepare call parameters and create subnet - var prm morphsubnet.PutPrm - - prm.SetID(binID) - prm.SetOwner(key.PublicKey().Bytes()) - prm.SetInfo(binInfo) - - _, err = cSubnet.Put(prm) - if err != nil { - return fmt.Errorf("morph call: %w", err) + return fmt.Errorf("morph invocation: %w", err) } cmd.Printf("Create subnet request sent successfully. ID: %s.\n", &id) @@ -303,22 +230,9 @@ var cmdSubnetRemove = &cobra.Command{ return fmt.Errorf("marshal subnet ID: %w", err) } - // initialize morph subnet client - var cSubnet morphsubnet.Client - - err = initSubnetClient(&cSubnet, &key) + err = invokeMethod(key, false, "delete", binID) if err != nil { - return fmt.Errorf("init subnet client: %w", err) - } - - // prepare call parameters and remove subnet - var prm morphsubnet.DeletePrm - - prm.SetID(binID) - - _, err = cSubnet.Delete(prm) - if err != nil { - return fmt.Errorf("morph call: %w", err) + return fmt.Errorf("morph invocation: %w", err) } cmd.Println("Remove subnet request sent successfully.") @@ -360,9 +274,6 @@ var cmdSubnetGet = &cobra.Command{ return fmt.Errorf("marshal subnet ID: %w", err) } - // initialize morph subnet client - var cSubnet morphsubnet.Client - // use random key to fetch the data // we could use raw neo-go client to perform testInvoke // without keys, as it is done in other commands @@ -371,25 +282,23 @@ var cmdSubnetGet = &cobra.Command{ return fmt.Errorf("init subnet client: %w", err) } - err = initSubnetClient(&cSubnet, key) + res, err := testInvokeMethod(*key, "get", binID) if err != nil { - return fmt.Errorf("init subnet client: %w", err) + return fmt.Errorf("morph invocation: %w", err) } - // prepare call parameters and read subnet - var prm morphsubnet.GetPrm + if len(res) == 0 { + return errors.New("subnet does not exist") + } - prm.SetID(binID) - - res, err := cSubnet.Get(prm) + data, err := client.BytesFromStackItem(res[0]) if err != nil { - return fmt.Errorf("morph call: %w", err) + return fmt.Errorf("decoding contract response: %w", err) } // decode info var info subnet.Info - - if err = info.Unmarshal(res.Info()); err != nil { + if err = info.Unmarshal(data); err != nil { return fmt.Errorf("decode subnet info: %w", err) } @@ -473,7 +382,10 @@ func manageSubnetAdmins(cmd *cobra.Command, rm bool) error { } // prepare call parameters - var prm morphsubnet.ManageAdminsPrm + prm := make([]interface{}, 0, 3) + prm = append(prm, binID) + + var method string if viper.GetBool(flagSubnetAdminClient) { // read group ID and encode it @@ -489,28 +401,26 @@ func manageSubnetAdmins(cmd *cobra.Command, rm bool) error { return fmt.Errorf("marshal group ID: %w", err) } - prm.SetClient() - prm.SetGroup(binGroupID) + if rm { + method = "removeClientAdmin" + } else { + method = "addClientAdmin" + } + + prm = append(prm, binGroupID) + } else { + if rm { + method = "removeNodeAdmin" + } else { + method = "addNodeAdmin" + } } - prm.SetSubnet(binID) - prm.SetAdmin(binAdminKey) + prm = append(prm, binAdminKey) - if rm { - prm.SetRemove() - } - - // initialize morph subnet client - var cSubnet morphsubnet.Client - - err = initSubnetClient(&cSubnet, &key) + err = invokeMethod(key, false, method, prm...) if err != nil { - return fmt.Errorf("init subnet client: %w", err) - } - - _, err = cSubnet.ManageAdmins(prm) - if err != nil { - return fmt.Errorf("morph call: %w", err) + return fmt.Errorf("morph invocation: %w", err) } var op string @@ -628,27 +538,16 @@ func manageSubnetClients(cmd *cobra.Command, rm bool) error { return fmt.Errorf("marshal group ID: %w", err) } - var prm morphsubnet.ManageClientsPrm - - prm.SetGroup(binGroupID) - prm.SetSubnet(binID) - prm.SetClient(clientID.WalletBytes()) - + var method string if rm { - prm.SetRemove() + method = "removeUser" + } else { + method = "addUser" } - // initialize morph subnet client - var cSubnet morphsubnet.Client - - err = initSubnetClient(&cSubnet, &key) + err = invokeMethod(key, false, method, binID, binGroupID, clientID.WalletBytes()) if err != nil { - return fmt.Errorf("init subnet client: %w", err) - } - - _, err = cSubnet.ManageClients(prm) - if err != nil { - return fmt.Errorf("morph call: %w", err) + return fmt.Errorf("morph invocation: %w", err) } var op string @@ -728,26 +627,16 @@ func manageSubnetNodes(cmd *cobra.Command, rm bool) error { return fmt.Errorf("node ID format: %w", err) } - var prm morphsubnet.ManageNodesPrm - - prm.SetSubnet(binID) - prm.SetNode(binNodeID) - + var method string if rm { - prm.SetRemove() + method = "removeNode" + } else { + method = "addNode" } - // initialize morph subnet client - var cSubnet morphsubnet.Client - - err = initSubnetClient(&cSubnet, &key) + err = invokeMethod(key, false, method, binID, binNodeID) if err != nil { - return fmt.Errorf("init subnet client: %w", err) - } - - _, err = cSubnet.ManageNodes(prm) - if err != nil { - return fmt.Errorf("morph call: %w", err) + return fmt.Errorf("morph invocation: %w", err) } var op string @@ -897,8 +786,8 @@ func init() { // subnet global flags cmdSubnetFlags := cmdSubnet.PersistentFlags() - cmdSubnetFlags.StringP(flagSubnetEndpoint, "r", "", "N3 RPC node endpoint") - _ = cmdSubnet.MarkFlagRequired(flagSubnetEndpoint) + cmdSubnetFlags.StringP(endpointFlag, "r", "", "N3 RPC node endpoint") + _ = cmdSubnet.MarkFlagRequired(endpointFlag) // add all subnet commands to corresponding command section addCommandInheritPreRun(cmdSubnet, @@ -910,3 +799,318 @@ func init() { cmdSubnetNode, ) } + +func testInvokeMethod(key keys.PrivateKey, method string, args ...interface{}) ([]stackitem.Item, error) { + c, err := getN3Client(viper.GetViper()) + if err != nil { + return nil, fmt.Errorf("morph client creation: %w", err) + } + + nnsCs, err := c.GetContractStateByID(1) + if err != nil { + return nil, fmt.Errorf("NNS contract resolving: %w", err) + } + + subnetHash, err := nnsResolveHash(c, nnsCs.Hash, subnetContract+".neofs") + if err != nil { + return nil, fmt.Errorf("subnet hash resolving: %w", err) + } + + cosigner := []transaction.Signer{ + { + Account: key.PublicKey().GetScriptHash(), + Scopes: transaction.Global, + }, + } + + res, err := invokeFunction(c, subnetHash, method, args, cosigner) + if err != nil { + return nil, fmt.Errorf("invocation parameters prepararion: %w", err) + } + + err = checkInvocationResults(res) + if err != nil { + return nil, err + } + + return res.Stack, nil +} + +func invokeMethod(key keys.PrivateKey, tryNotary bool, method string, args ...interface{}) error { + c, err := getN3Client(viper.GetViper()) + if err != nil { + return fmt.Errorf("morph client creation: %w", err) + } + + if tryNotary { + cc, err := c.GetNativeContracts() + if err != nil { + return fmt.Errorf("native hashes: %w", err) + } + + var notary bool + var notaryHash util.Uint160 + for _, c := range cc { + if c.Manifest.Name == nativenames.Notary { + notary = len(c.UpdateHistory) > 0 + notaryHash = c.Hash + + break + } + } + + if notary { + err = invokeNotary(c, key, method, notaryHash, args...) + if err != nil { + return fmt.Errorf("notary invocation: %w", err) + } + + return nil + } + } + + err = invokeNonNotary(c, key, method, args...) + if err != nil { + return fmt.Errorf("non-notary invocation: %w", err) + } + + return nil +} + +func invokeNonNotary(c Client, key keys.PrivateKey, method string, args ...interface{}) error { + nnsCs, err := c.GetContractStateByID(1) + if err != nil { + return fmt.Errorf("NNS contract resolving: %w", err) + } + + subnetHash, err := nnsResolveHash(c, nnsCs.Hash, subnetContract+".neofs") + if err != nil { + return fmt.Errorf("subnet hash resolving: %w", err) + } + + acc := wallet.NewAccountFromPrivateKey(&key) + + cosigner := []transaction.Signer{ + { + Account: key.PublicKey().GetScriptHash(), + Scopes: transaction.Global, + }, + } + + cosignerAcc := []neogocli.SignerAccount{ + { + Signer: cosigner[0], + Account: acc, + }, + } + + test, err := invokeFunction(c, subnetHash, method, args, cosigner) + if err != nil { + return fmt.Errorf("test invocation: %w", err) + } + + err = checkInvocationResults(test) + if err != nil { + return err + } + + _, err = c.SignAndPushInvocationTx(test.Script, acc, test.GasConsumed, 0, cosignerAcc) + if err != nil { + return fmt.Errorf("sending transaction: %w", err) + } + + return nil +} + +func invokeNotary(c Client, key keys.PrivateKey, method string, notaryHash util.Uint160, args ...interface{}) error { + nnsCs, err := c.GetContractStateByID(1) + if err != nil { + return fmt.Errorf("NNS contract resolving: %w", err) + } + + subnetHash, err := nnsResolveHash(c, nnsCs.Hash, subnetContract+".neofs") + if err != nil { + return fmt.Errorf("subnet hash resolving: %w", err) + } + + alphabet, err := c.GetCommittee() + if err != nil { + return fmt.Errorf("alphabet list: %w", err) + } + + multisigScript, err := smartcontract.CreateDefaultMultiSigRedeemScript(alphabet) + if err != nil { + return fmt.Errorf("alphabet multi-signature script: %w", err) + } + + cosigners, err := notaryCosigners(c, notaryHash, nnsCs, key, hash.Hash160(multisigScript)) + if err != nil { + return fmt.Errorf("cosigners collecting: %w", err) + } + + // make test invocation of the method + test, err := invokeFunction(c, subnetHash, method, args, cosigners) + if err != nil { + return fmt.Errorf("test invocation: %w", err) + } + + err = checkInvocationResults(test) + if err != nil { + return err + } + + multisigAccount := &wallet.Account{ + Contract: &wallet.Contract{ + Script: multisigScript, + }, + } + + bc, err := c.GetBlockCount() + if err != nil { + return fmt.Errorf("blockchain height: %w", err) + } + + signersNumber := uint8(smartcontract.GetDefaultHonestNodeCount(len(alphabet)) + 1) // alphabet multisig + key signature + + // notaryRequestValidity is number of blocks during + // witch notary request is considered valid + const notaryRequestValidity = 100 + + mainTx := &transaction.Transaction{ + Nonce: rand.Uint32(), + SystemFee: test.GasConsumed, + ValidUntilBlock: bc + notaryRequestValidity, + Script: test.Script, + Attributes: []transaction.Attribute{ + { + Type: transaction.NotaryAssistedT, + Value: &transaction.NotaryAssisted{NKeys: signersNumber}, + }, + }, + Signers: cosigners, + } + + notaryFee, err := c.CalculateNotaryFee(signersNumber) + if err != nil { + return err + } + + acc := wallet.NewAccountFromPrivateKey(&key) + aa := notaryAccounts(multisigAccount, acc) + + err = c.AddNetworkFee(mainTx, notaryFee, aa...) + if err != nil { + return fmt.Errorf("notary network fee adding: %w", err) + } + + mainTx.Scripts = notaryWitnesses(c, multisigAccount, acc, mainTx) + + _, err = c.SignAndPushP2PNotaryRequest(mainTx, + []byte{byte(opcode.RET)}, + -1, + 0, + 40, + acc) + if err != nil { + return fmt.Errorf("sending notary request: %w", err) + } + + return nil +} + +func notaryCosigners(c Client, notaryHash util.Uint160, nnsCs *state.Contract, + key keys.PrivateKey, alphabetAccount util.Uint160) ([]transaction.Signer, error) { + proxyHash, err := nnsResolveHash(c, nnsCs.Hash, proxyContract+".neofs") + if err != nil { + return nil, fmt.Errorf("proxy hash resolving: %w", err) + } + + return []transaction.Signer{ + { + Account: proxyHash, + Scopes: transaction.None, + }, + { + Account: alphabetAccount, + Scopes: transaction.Global, + }, + { + Account: hash.Hash160(key.PublicKey().GetVerificationScript()), + Scopes: transaction.Global, + }, + { + Account: notaryHash, + Scopes: transaction.None, + }, + }, nil +} + +func notaryAccounts(alphabet, acc *wallet.Account) []*wallet.Account { + return []*wallet.Account{ + // proxy + { + Contract: &wallet.Contract{ + Deployed: true, + }, + }, + alphabet, + // caller's account + acc, + // last one is a placeholder for notary contract account + { + Contract: &wallet.Contract{}, + }, + } +} + +func notaryWitnesses(c Client, alphabet, acc *wallet.Account, tx *transaction.Transaction) []transaction.Witness { + ww := make([]transaction.Witness, 0, 4) + + // empty proxy contract witness + ww = append(ww, transaction.Witness{ + InvocationScript: []byte{}, + VerificationScript: []byte{}, + }) + + // alphabet multi-address witness + ww = append(ww, transaction.Witness{ + InvocationScript: append( + []byte{byte(opcode.PUSHDATA1), 64}, + make([]byte, 64)..., + ), + VerificationScript: alphabet.GetVerificationScript(), + }) + + magicNumber, _ := c.GetNetwork() + + // caller's witness + ww = append(ww, transaction.Witness{ + InvocationScript: append( + []byte{byte(opcode.PUSHDATA1), 64}, + acc.PrivateKey().SignHashable(uint32(magicNumber), tx)...), + VerificationScript: acc.GetVerificationScript(), + }) + + // notary contract witness + ww = append(ww, transaction.Witness{ + InvocationScript: append( + []byte{byte(opcode.PUSHDATA1), 64}, + make([]byte, 64)..., + ), + VerificationScript: []byte{}, + }) + + return ww +} + +func checkInvocationResults(res *result.Invoke) error { + if res.State != "HALT" { + return fmt.Errorf("test invocation state: %s, exception %s: ", res.State, res.FaultException) + } + + if len(res.Script) == 0 { + return errors.New("empty invocation script") + } + + return nil +}