package morph import ( "encoding/hex" "errors" "fmt" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/internal" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/rand" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/subnet" subnetid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/subnet/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "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" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "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/spf13/cobra" "github.com/spf13/viper" ) func viperBindFlags(cmd *cobra.Command, flags ...string) { for i := range flags { _ = viper.BindPFlag(flags[i], cmd.Flags().Lookup(flags[i])) } } // subnet command section. var cmdSubnet = &cobra.Command{ Use: "subnet", Short: "FrostFS subnet management", PreRun: func(cmd *cobra.Command, _ []string) { viperBindFlags(cmd, endpointFlag, ) }, } // shared flags of cmdSubnet sub-commands. const ( flagSubnet = "subnet" // subnet identifier flagSubnetGroup = "group" // subnet client group ID flagSubnetWallet = "wallet" // filepath to wallet flagSubnetAddress = "address" // address in the wallet, optional ) // reads wallet from the filepath configured in flagSubnetWallet flag, // looks for address specified in flagSubnetAddress flag (uses default // address if flag is empty) and decrypts private key. func readSubnetKey(key *keys.PrivateKey) error { // read wallet from file walletPath := viper.GetString(flagSubnetWallet) if walletPath == "" { return errors.New("missing path to wallet") } w, err := wallet.NewWalletFromFile(walletPath) if err != nil { return fmt.Errorf("read wallet from file: %w", err) } // read account from the wallet var ( addr util.Uint160 addrStr = viper.GetString(flagSubnetAddress) ) if addrStr == "" { addr = w.GetChangeAddress() } else { addr, err = flags.ParseAddress(addrStr) if err != nil { return fmt.Errorf("read wallet address: %w", err) } } acc := w.GetAccount(addr) if acc == nil { return fmt.Errorf("address %s not found in %s", addrStr, walletPath) } // read password pass, err := input.ReadPassword("Enter password > ") if err != nil { return fmt.Errorf("read password: %w", err) } // decrypt with just read password err = acc.Decrypt(pass, keys.NEP2ScryptParams()) if err != nil { return fmt.Errorf("decrypt wallet: %w", err) } *key = *acc.PrivateKey() return nil } // create subnet command. var cmdSubnetCreate = &cobra.Command{ Use: "create", Short: "Create FrostFS subnet", PreRun: func(cmd *cobra.Command, _ []string) { viperBindFlags(cmd, flagSubnetWallet, flagSubnetAddress, ) }, RunE: func(cmd *cobra.Command, _ []string) error { // read private key var key keys.PrivateKey err := readSubnetKey(&key) if err != nil { return fmt.Errorf("read private key: %w", err) } // generate subnet ID and marshal it var ( id subnetid.ID num uint32 ) for { num = rand.Uint32() id.SetNumeric(num) if !subnetid.IsZero(id) { break } } // declare creator ID and encode it var creator user.ID user.IDFromKey(&creator, key.PrivateKey.PublicKey) // fill subnet info and encode it var info subnet.Info info.SetID(id) info.SetOwner(creator) err = invokeMethod(key, true, "put", id.Marshal(), key.PublicKey().Bytes(), info.Marshal()) if err != nil { return fmt.Errorf("morph invocation: %w", err) } cmd.Printf("Create subnet request sent successfully. ID: %s.\n", &id) return nil }, } // cmdSubnetRemove flags. const ( // subnet ID to be removed. flagSubnetRemoveID = flagSubnet ) // errZeroSubnet is returned on attempts to work with zero subnet which is virtual. var errZeroSubnet = errors.New("zero subnet") // remove subnet command. var cmdSubnetRemove = &cobra.Command{ Use: "remove", Short: "Remove FrostFS subnet", PreRun: func(cmd *cobra.Command, _ []string) { viperBindFlags(cmd, flagSubnetWallet, flagSubnetAddress, flagSubnetRemoveID, ) }, RunE: func(cmd *cobra.Command, _ []string) error { // read private key var key keys.PrivateKey err := readSubnetKey(&key) if err != nil { return fmt.Errorf("read private key: %w", err) } // read ID and encode it var id subnetid.ID err = id.DecodeString(viper.GetString(flagSubnetRemoveID)) if err != nil { return fmt.Errorf("decode ID text: %w", err) } if subnetid.IsZero(id) { return errZeroSubnet } err = invokeMethod(key, false, "delete", id.Marshal()) if err != nil { return fmt.Errorf("morph invocation: %w", err) } cmd.Println("Remove subnet request sent successfully") return nil }, } // cmdSubnetGet flags. const ( // subnet ID to be read. flagSubnetGetID = flagSubnet ) // get subnet command. var cmdSubnetGet = &cobra.Command{ Use: "get", Short: "Read information about the FrostFS subnet", PreRun: func(cmd *cobra.Command, _ []string) { viperBindFlags(cmd, flagSubnetGetID, ) }, RunE: func(cmd *cobra.Command, _ []string) error { // read ID and encode it var id subnetid.ID err := id.DecodeString(viper.GetString(flagSubnetGetID)) if err != nil { return fmt.Errorf("decode ID text: %w", err) } if subnetid.IsZero(id) { return errZeroSubnet } // 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 key, err := keys.NewPrivateKey() if err != nil { return fmt.Errorf("init subnet client: %w", err) } res, err := testInvokeMethod(*key, "get", id.Marshal()) if err != nil { return fmt.Errorf("morph invocation: %w", err) } if len(res) == 0 { return errors.New("subnet does not exist") } data, err := client.BytesFromStackItem(res[0]) if err != nil { return fmt.Errorf("decoding contract response: %w", err) } // decode info var info subnet.Info if err = info.Unmarshal(data); err != nil { return fmt.Errorf("decode subnet info: %w", err) } // print information cmd.Printf("Owner: %s\n", info.Owner()) return nil }, } // cmdSubnetAdmin subnet flags. const ( flagSubnetAdminSubnet = flagSubnet // subnet ID to be managed flagSubnetAdminID = "admin" // admin public key flagSubnetAdminClient = "client" // manage client admins instead of node ones ) // command to manage subnet admins. var cmdSubnetAdmin = &cobra.Command{ Use: "admin", Short: "Manage administrators of the FrostFS subnet", PreRun: func(cmd *cobra.Command, args []string) { viperBindFlags(cmd, flagSubnetWallet, flagSubnetAddress, flagSubnetAdminSubnet, flagSubnetAdminID, ) }, } // cmdSubnetAdminAdd flags. const ( flagSubnetAdminAddGroup = flagSubnetGroup // client group ID ) // common executor cmdSubnetAdminAdd and cmdSubnetAdminRemove commands. func manageSubnetAdmins(cmd *cobra.Command, rm bool) error { // read private key var key keys.PrivateKey err := readSubnetKey(&key) if err != nil { return fmt.Errorf("read private key: %w", err) } // read ID and encode it var id subnetid.ID err = id.DecodeString(viper.GetString(flagSubnetAdminSubnet)) if err != nil { return fmt.Errorf("decode ID text: %w", err) } if subnetid.IsZero(id) { return errZeroSubnet } // read admin key and decode it binAdminKey, err := hex.DecodeString(viper.GetString(flagSubnetAdminID)) if err != nil { return fmt.Errorf("decode admin key text: %w", err) } var pubkey keys.PublicKey if err = pubkey.DecodeBytes(binAdminKey); err != nil { return fmt.Errorf("admin key format: %w", err) } return invokeMethodWithParams(cmd, id, rm, binAdminKey, key) } func invokeMethodWithParams(cmd *cobra.Command, id subnetid.ID, rm bool, binAdminKey []byte, key keys.PrivateKey) error { prm := make([]any, 0, 3) prm = append(prm, id.Marshal()) var method string if viper.GetBool(flagSubnetAdminClient) { var groupID internal.SubnetClientGroupID err := groupID.UnmarshalText([]byte(viper.GetString(flagSubnetAdminAddGroup))) if err != nil { return fmt.Errorf("decode group ID text: %w", err) } binGroupID, err := groupID.Marshal() if err != nil { return fmt.Errorf("marshal group ID: %w", err) } if rm { method = "removeClientAdmin" } else { method = "addClientAdmin" } prm = append(prm, binGroupID) } else { if rm { method = "removeNodeAdmin" } else { method = "addNodeAdmin" } } prm = append(prm, binAdminKey) err := invokeMethod(key, false, method, prm...) if err != nil { return fmt.Errorf("morph invocation: %w", err) } var op string if rm { op = "Remove" } else { op = "Add" } cmd.Printf("%s admin request sent successfully.\n", op) return nil } // command to add subnet admin. var cmdSubnetAdminAdd = &cobra.Command{ Use: "add", Short: "Add admin to the FrostFS subnet", PreRun: func(cmd *cobra.Command, _ []string) { viperBindFlags(cmd, flagSubnetAdminAddGroup, flagSubnetAdminClient, ) }, RunE: func(cmd *cobra.Command, _ []string) error { return manageSubnetAdmins(cmd, false) }, } // command to remove subnet admin. var cmdSubnetAdminRemove = &cobra.Command{ Use: "remove", Short: "Remove admin of the FrostFS subnet", PreRun: func(cmd *cobra.Command, _ []string) { viperBindFlags(cmd, flagSubnetAdminClient, ) }, RunE: func(cmd *cobra.Command, _ []string) error { return manageSubnetAdmins(cmd, true) }, } // cmdSubnetClient flags. const ( flagSubnetClientSubnet = flagSubnet // ID of the subnet to be managed flagSubnetClientID = flagSubnetAdminClient // client's NeoFS ID flagSubnetClientGroup = flagSubnetGroup // ID of the subnet client group ) // command to manage subnet clients. var cmdSubnetClient = &cobra.Command{ Use: "client", Short: "Manage clients of the FrostFS subnet", PreRun: func(cmd *cobra.Command, _ []string) { viperBindFlags(cmd, flagSubnetWallet, flagSubnetAddress, flagSubnetClientSubnet, flagSubnetClientID, flagSubnetClientGroup, ) }, } // common executor cmdSubnetClientAdd and cmdSubnetClientRemove commands. func manageSubnetClients(cmd *cobra.Command, rm bool) error { // read private key var key keys.PrivateKey err := readSubnetKey(&key) if err != nil { return fmt.Errorf("read private key: %w", err) } // read ID and encode it var id subnetid.ID err = id.DecodeString(viper.GetString(flagSubnetClientSubnet)) if err != nil { return fmt.Errorf("decode ID text: %w", err) } if subnetid.IsZero(id) { return errZeroSubnet } // read client ID and encode it var clientID user.ID err = clientID.DecodeString(viper.GetString(flagSubnetClientID)) if err != nil { return fmt.Errorf("decode client ID text: %w", err) } // read group ID and encode it var groupID internal.SubnetClientGroupID err = groupID.UnmarshalText([]byte(viper.GetString(flagSubnetAdminAddGroup))) if err != nil { return fmt.Errorf("decode group ID text: %w", err) } binGroupID, err := groupID.Marshal() if err != nil { return fmt.Errorf("marshal group ID: %w", err) } var method string if rm { method = "removeUser" } else { method = "addUser" } err = invokeMethod(key, false, method, id.Marshal(), binGroupID, clientID.WalletBytes()) if err != nil { return fmt.Errorf("morph invocation: %w", err) } var op string if rm { op = "Remove" } else { op = "Add" } cmd.Printf("%s client request sent successfully.\n", op) return nil } // command to add subnet client. var cmdSubnetClientAdd = &cobra.Command{ Use: "add", Short: "Add client to the FrostFS subnet", RunE: func(cmd *cobra.Command, _ []string) error { return manageSubnetClients(cmd, false) }, } // command to remove subnet client. var cmdSubnetClientRemove = &cobra.Command{ Use: "remove", Short: "Remove client of the FrostFS subnet", RunE: func(cmd *cobra.Command, _ []string) error { return manageSubnetClients(cmd, true) }, } // cmdSubnetNode flags. const ( flagSubnetNode = "node" // node ID flagSubnetNodeSubnet = flagSubnet // ID of the subnet to be managed ) // common executor cmdSubnetNodeAdd and cmdSubnetNodeRemove commands. func manageSubnetNodes(cmd *cobra.Command, rm bool) error { // read private key var key keys.PrivateKey err := readSubnetKey(&key) if err != nil { return fmt.Errorf("read private key: %w", err) } // read ID and encode it var id subnetid.ID err = id.DecodeString(viper.GetString(flagSubnetNodeSubnet)) if err != nil { return fmt.Errorf("decode ID text: %w", err) } if subnetid.IsZero(id) { return errZeroSubnet } // read node ID and encode it binNodeID, err := hex.DecodeString(viper.GetString(flagSubnetNode)) if err != nil { return fmt.Errorf("decode node ID text: %w", err) } var pubkey keys.PublicKey if err = pubkey.DecodeBytes(binNodeID); err != nil { return fmt.Errorf("node ID format: %w", err) } var method string if rm { method = "removeNode" } else { method = "addNode" } err = invokeMethod(key, false, method, id.Marshal(), binNodeID) if err != nil { return fmt.Errorf("morph invocation: %w", err) } var op string if rm { op = "Remove" } else { op = "Add" } cmd.Printf("%s node request sent successfully.\n", op) return nil } // command to manage subnet nodes. var cmdSubnetNode = &cobra.Command{ Use: "node", Short: "Manage nodes of the FrostFS subnet", PreRun: func(cmd *cobra.Command, _ []string) { viperBindFlags(cmd, flagSubnetWallet, flagSubnetNode, flagSubnetNodeSubnet, ) }, } // command to add subnet node. var cmdSubnetNodeAdd = &cobra.Command{ Use: "add", Short: "Add node to the FrostFS subnet", RunE: func(cmd *cobra.Command, _ []string) error { return manageSubnetNodes(cmd, false) }, } // command to remove subnet node. var cmdSubnetNodeRemove = &cobra.Command{ Use: "remove", Short: "Remove node from the FrostFS subnet", RunE: func(cmd *cobra.Command, _ []string) error { return manageSubnetNodes(cmd, true) }, } // returns function which calls PreRun on parent if it exists. func inheritPreRun(preRun func(*cobra.Command, []string)) func(*cobra.Command, []string) { return func(cmd *cobra.Command, args []string) { par := cmd.Parent() if par != nil && par.PreRun != nil { par.PreRun(par, args) } if preRun != nil { preRun(cmd, args) } } } // inherits PreRun function of parent command in all sub-commands and // adds them to the parent. func addCommandInheritPreRun(par *cobra.Command, subs ...*cobra.Command) { for _, sub := range subs { sub.PreRun = inheritPreRun(sub.PreRun) } par.AddCommand(subs...) } // registers flags and binds sub-commands for subnet commands. func init() { initCreateSubnetFlags() initGetSubnetFlags() initRemoveSubnetFlags() initSubnetAdminFlags() initSubnetAdminAddFlags() initSubnetAdminRemoveFlags() initClientManagementFlags() // add all admin managing commands to corresponding command section addCommandInheritPreRun(cmdSubnetAdmin, cmdSubnetAdminAdd, cmdSubnetAdminRemove, ) // add all client managing commands to corresponding command section addCommandInheritPreRun(cmdSubnetClient, cmdSubnetClientAdd, cmdSubnetClientRemove, ) initSubnetNodeFlags() // add all node managing commands to corresponding command section addCommandInheritPreRun(cmdSubnetNode, cmdSubnetNodeAdd, cmdSubnetNodeRemove, ) initSubnetGlobalFlags() // add all subnet commands to corresponding command section addCommandInheritPreRun(cmdSubnet, cmdSubnetCreate, cmdSubnetRemove, cmdSubnetGet, cmdSubnetAdmin, cmdSubnetClient, cmdSubnetNode, ) } func initSubnetGlobalFlags() { cmdSubnetFlags := cmdSubnet.PersistentFlags() cmdSubnetFlags.StringP(endpointFlag, "r", "", "N3 RPC node endpoint") _ = cmdSubnet.MarkFlagRequired(endpointFlag) } func initSubnetNodeFlags() { nodeFlags := cmdSubnetNode.PersistentFlags() nodeFlags.StringP(flagSubnetWallet, "w", "", "Path to file with wallet") _ = cmdSubnetNode.MarkFlagRequired(flagSubnetWallet) nodeFlags.String(flagSubnetNode, "", "Hex-encoded public key of the node") _ = cmdSubnetNode.MarkFlagRequired(flagSubnetNode) nodeFlags.String(flagSubnetNodeSubnet, "", "ID of the subnet to manage nodes") _ = cmdSubnetNode.MarkFlagRequired(flagSubnetNodeSubnet) } func initClientManagementFlags() { clientFlags := cmdSubnetClient.PersistentFlags() clientFlags.String(flagSubnetClientSubnet, "", "ID of the subnet to be managed") _ = cmdSubnetClient.MarkFlagRequired(flagSubnetClientSubnet) clientFlags.String(flagSubnetClientGroup, "", "ID of the client group to work with") _ = cmdSubnetClient.MarkFlagRequired(flagSubnetClientGroup) clientFlags.String(flagSubnetClientID, "", "Client's user ID in FrostFS system in text format") _ = cmdSubnetClient.MarkFlagRequired(flagSubnetClientID) clientFlags.StringP(flagSubnetWallet, "w", "", "Path to file with wallet") _ = cmdSubnetClient.MarkFlagRequired(flagSubnetWallet) clientFlags.StringP(flagSubnetAddress, "a", "", "Address in the wallet, optional") } func initSubnetAdminRemoveFlags() { cmdSubnetAdminRemoveFlags := cmdSubnetAdminRemove.Flags() cmdSubnetAdminRemoveFlags.Bool(flagSubnetAdminClient, false, "Remove client admin instead of node one") } func initSubnetAdminAddFlags() { cmdSubnetAdminAddFlags := cmdSubnetAdminAdd.Flags() cmdSubnetAdminAddFlags.String(flagSubnetAdminAddGroup, "", fmt.Sprintf( "Client group ID in text format (needed with --%s only)", flagSubnetAdminClient)) cmdSubnetAdminAddFlags.Bool(flagSubnetAdminClient, false, "Add client admin instead of node one") } func initSubnetAdminFlags() { adminFlags := cmdSubnetAdmin.PersistentFlags() adminFlags.String(flagSubnetAdminSubnet, "", "ID of the subnet to manage administrators") _ = cmdSubnetAdmin.MarkFlagRequired(flagSubnetAdminSubnet) adminFlags.String(flagSubnetAdminID, "", "Hex-encoded public key of the admin") _ = cmdSubnetAdmin.MarkFlagRequired(flagSubnetAdminID) adminFlags.StringP(flagSubnetWallet, "w", "", "Path to file with wallet") _ = cmdSubnetAdmin.MarkFlagRequired(flagSubnetWallet) adminFlags.StringP(flagSubnetAddress, "a", "", "Address in the wallet, optional") } func initRemoveSubnetFlags() { cmdSubnetRemove.Flags().String(flagSubnetRemoveID, "", "ID of the subnet to remove") _ = cmdSubnetRemove.MarkFlagRequired(flagSubnetRemoveID) cmdSubnetRemove.Flags().StringP(flagSubnetWallet, "w", "", "Path to file with wallet") _ = cmdSubnetRemove.MarkFlagRequired(flagSubnetWallet) cmdSubnetRemove.Flags().StringP(flagSubnetAddress, "a", "", "Address in the wallet, optional") } func initGetSubnetFlags() { cmdSubnetGet.Flags().String(flagSubnetGetID, "", "ID of the subnet to read") _ = cmdSubnetAdminAdd.MarkFlagRequired(flagSubnetGetID) } func initCreateSubnetFlags() { cmdSubnetCreate.Flags().StringP(flagSubnetWallet, "w", "", "Path to file with wallet") _ = cmdSubnetCreate.MarkFlagRequired(flagSubnetWallet) cmdSubnetCreate.Flags().StringP(flagSubnetAddress, "a", "", "Address in the wallet, optional") } func testInvokeMethod(key keys.PrivateKey, method string, args ...any) ([]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) } cosigner := []transaction.Signer{ { Account: key.PublicKey().GetScriptHash(), Scopes: transaction.Global, }, } inv := invoker.New(c, cosigner) subnetHash, err := nnsResolveHash(inv, nnsCs.Hash, subnetContract+".frostfs") if err != nil { return nil, fmt.Errorf("subnet hash resolving: %w", err) } res, err := inv.Call(subnetHash, method, args...) 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 ...any) 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 ...any) error { nnsCs, err := c.GetContractStateByID(1) if err != nil { return fmt.Errorf("NNS contract resolving: %w", err) } acc := wallet.NewAccountFromPrivateKey(&key) cosigner := []transaction.Signer{ { Account: key.PublicKey().GetScriptHash(), Scopes: transaction.Global, }, } cosignerAcc := []rpcclient.SignerAccount{ { Signer: cosigner[0], Account: acc, }, } inv := invoker.New(c, cosigner) subnetHash, err := nnsResolveHash(inv, nnsCs.Hash, subnetContract+".frostfs") if err != nil { return fmt.Errorf("subnet hash resolving: %w", err) } test, err := inv.Call(subnetHash, method, args...) 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 ...any) error { nnsCs, err := c.GetContractStateByID(1) if err != nil { return fmt.Errorf("NNS contract 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) } inv := invoker.New(c, cosigners) subnetHash, err := nnsResolveHash(inv, nnsCs.Hash, subnetContract+".frostfs") if err != nil { return fmt.Errorf("subnet hash resolving: %w", err) } test, err := makeTestInvocation(inv, subnetHash, method, args) 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) } return createAndPushTransaction(alphabet, test, bc, cosigners, c, key, multisigAccount) } func makeTestInvocation(inv *invoker.Invoker, subnetHash util.Uint160, method string, args []any) (*result.Invoke, error) { test, err := inv.Call(subnetHash, method, args...) if err != nil { return nil, fmt.Errorf("test invocation: %w", err) } err = checkInvocationResults(test) if err != nil { return nil, err } return test, nil } func createAndPushTransaction(alphabet keys.PublicKeys, test *result.Invoke, blockCount uint32, cosigners []transaction.Signer, client Client, key keys.PrivateKey, multisigAccount *wallet.Account) error { // alphabet multisig + key signature signersNumber := uint8(smartcontract.GetDefaultHonestNodeCount(len(alphabet)) + 1) // 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: blockCount + notaryRequestValidity, Script: test.Script, Attributes: []transaction.Attribute{ { Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: signersNumber}, }, }, Signers: cosigners, } notaryFee, err := client.CalculateNotaryFee(signersNumber) if err != nil { return err } acc := wallet.NewAccountFromPrivateKey(&key) aa := notaryAccounts(multisigAccount, acc) err = client.AddNetworkFee(mainTx, notaryFee, aa...) if err != nil { return fmt.Errorf("notary network fee adding: %w", err) } mainTx.Scripts = notaryWitnesses(client, multisigAccount, acc, mainTx) _, err = client.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(invoker.New(c, nil), nnsCs.Hash, proxyContract+".frostfs") 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 }