package helper import ( "encoding/hex" "encoding/json" "errors" "fmt" io2 "io" "os" "path/filepath" "git.frostfs.info/TrueCloudLab/frostfs-contract/nns" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client" "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/io" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/management" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/context" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/spf13/cobra" "github.com/spf13/viper" ) var ( errNegativeDuration = errors.New("epoch duration must be positive") errNegativeSize = errors.New("max object size must be positive") ) type ContractState struct { NEF *nef.File RawNEF []byte Manifest *manifest.Manifest RawManifest []byte Hash util.Uint160 } type Cache struct { NNSCs *state.Contract GroupKey *keys.PublicKey } type InitializeContext struct { ClientContext Cache // CommitteeAcc is used for retrieving the committee address and the verification script. CommitteeAcc *wallet.Account // ConsensusAcc is used for retrieving the committee address and the verification script. ConsensusAcc *wallet.Account Wallets []*wallet.Wallet // ContractWallet is a wallet for providing the contract group signature. ContractWallet *wallet.Wallet // Accounts contains simple signature accounts in the same order as in Wallets. Accounts []*wallet.Account Contracts map[string]*ContractState Command *cobra.Command ContractPath string ContractURL string } func (cs *ContractState) Parse() error { nf, err := nef.FileFromBytes(cs.RawNEF) if err != nil { return fmt.Errorf("can't parse NEF file: %w", err) } m := new(manifest.Manifest) if err := json.Unmarshal(cs.RawManifest, m); err != nil { return fmt.Errorf("can't parse manifest file: %w", err) } cs.NEF = &nf cs.Manifest = m return nil } func NewInitializeContext(cmd *cobra.Command, v *viper.Viper) (*InitializeContext, error) { walletDir := config.ResolveHomePath(viper.GetString(commonflags.AlphabetWalletsFlag)) wallets, err := GetAlphabetWallets(v, walletDir) if err != nil { return nil, err } needContracts := cmd.Name() == "update-contracts" || cmd.Name() == "init" var w *wallet.Wallet w, err = getWallet(cmd, v, needContracts, walletDir) if err != nil { return nil, err } c, err := createClient(cmd, v, wallets) if err != nil { return nil, err } committeeAcc, err := GetWalletAccount(wallets[0], constants.CommitteeAccountName) if err != nil { return nil, fmt.Errorf("can't find committee account: %w", err) } consensusAcc, err := GetWalletAccount(wallets[0], constants.ConsensusAccountName) if err != nil { return nil, fmt.Errorf("can't find consensus account: %w", err) } if err := validateInit(cmd); err != nil { return nil, err } ctrPath, err := getContractsPath(cmd, needContracts) if err != nil { return nil, err } var ctrURL string if needContracts { ctrURL, _ = cmd.Flags().GetString(commonflags.ContractsURLFlag) } if err := CheckNotaryEnabled(c); err != nil { return nil, err } accounts, err := createWalletAccounts(wallets) if err != nil { return nil, err } cliCtx, err := DefaultClientContext(c, committeeAcc) if err != nil { return nil, fmt.Errorf("client context: %w", err) } initCtx := &InitializeContext{ ClientContext: *cliCtx, ConsensusAcc: consensusAcc, CommitteeAcc: committeeAcc, ContractWallet: w, Wallets: wallets, Accounts: accounts, Command: cmd, Contracts: make(map[string]*ContractState), ContractPath: ctrPath, ContractURL: ctrURL, } if needContracts { err := readContracts(initCtx, constants.FullContractList) if err != nil { return nil, err } } return initCtx, nil } func validateInit(cmd *cobra.Command) error { if cmd.Name() != "init" { return nil } if viper.GetInt64(commonflags.EpochDurationInitFlag) <= 0 { return errNegativeDuration } if viper.GetInt64(commonflags.MaxObjectSizeInitFlag) <= 0 { return errNegativeSize } return nil } func createClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet) (Client, error) { var c Client var err error if ldf := cmd.Flags().Lookup(commonflags.LocalDumpFlag); ldf != nil && ldf.Changed { if cmd.Flags().Changed(commonflags.EndpointFlag) { return nil, fmt.Errorf("`%s` and `%s` flags are mutually exclusive", commonflags.EndpointFlag, commonflags.LocalDumpFlag) } c, err = NewLocalClient(cmd, v, wallets, ldf.Value.String()) } else { c, err = GetN3Client(v) } if err != nil { return nil, fmt.Errorf("can't create N3 client: %w", err) } return c, nil } func getContractsPath(cmd *cobra.Command, needContracts bool) (string, error) { if !needContracts { return "", nil } ctrPath, err := cmd.Flags().GetString(commonflags.ContractsInitFlag) if err != nil { return "", fmt.Errorf("invalid contracts path: %w", err) } return ctrPath, nil } func createWalletAccounts(wallets []*wallet.Wallet) ([]*wallet.Account, error) { accounts := make([]*wallet.Account, len(wallets)) for i, w := range wallets { acc, err := GetWalletAccount(w, constants.SingleAccountName) if err != nil { return nil, fmt.Errorf("wallet %s is invalid (no single account): %w", w.Path(), err) } accounts[i] = acc } return accounts, nil } func readContracts(c *InitializeContext, names []string) error { var ( fi os.FileInfo err error ) if c.ContractPath != "" { fi, err = os.Stat(c.ContractPath) if err != nil { return fmt.Errorf("invalid contracts path: %w", err) } } if c.ContractPath != "" && fi.IsDir() { for _, ctrName := range names { cs, err := ReadContract(filepath.Join(c.ContractPath, ctrName), ctrName) if err != nil { return err } c.Contracts[ctrName] = cs } } else { var r io2.ReadCloser if c.ContractPath != "" { r, err = os.Open(c.ContractPath) } else if c.ContractURL != "" { r, err = downloadContracts(c.Command, c.ContractURL) } else { r, err = downloadContractsFromRepository(c.Command) } if err != nil { return fmt.Errorf("can't open contracts archive: %w", err) } defer r.Close() m, err := readContractsFromArchive(r, names) if err != nil { return err } for _, name := range names { if err := m[name].Parse(); err != nil { return err } c.Contracts[name] = m[name] } } for _, ctrName := range names { if ctrName != constants.AlphabetContract { cs := c.Contracts[ctrName] cs.Hash = state.CreateContractHash(c.CommitteeAcc.Contract.ScriptHash(), cs.NEF.Checksum, cs.Manifest.Name) } } return nil } func (c *InitializeContext) Close() { if local, ok := c.Client.(*LocalClient); ok { err := local.Dump() if err != nil { c.Command.PrintErrf("Can't write dump: %v\n", err) os.Exit(1) } } } func (c *InitializeContext) AwaitTx() error { return c.ClientContext.AwaitTx(c.Command) } func (c *InitializeContext) NNSContractState() (*state.Contract, error) { if c.NNSCs != nil { return c.NNSCs, nil } r := management.NewReader(c.ReadOnlyInvoker) cs, err := r.GetContractByID(1) if err != nil { return nil, err } c.NNSCs = cs return cs, nil } func (c *InitializeContext) GetSigner(tryGroup bool, acc *wallet.Account) transaction.Signer { if tryGroup && c.GroupKey != nil { return transaction.Signer{ Account: acc.Contract.ScriptHash(), Scopes: transaction.CustomGroups, AllowedGroups: keys.PublicKeys{c.GroupKey}, } } signer := transaction.Signer{ Account: acc.Contract.ScriptHash(), Scopes: transaction.Global, // Scope is important, as we have nested call to container contract. } if !tryGroup { return signer } nnsCs, err := c.NNSContractState() if err != nil { return signer } groupKey, err := NNSResolveKey(c.ReadOnlyInvoker, nnsCs.Hash, client.NNSGroupKeyName) if err == nil { c.GroupKey = groupKey signer.Scopes = transaction.CustomGroups signer.AllowedGroups = keys.PublicKeys{groupKey} } return signer } // SendCommitteeTx creates transaction from script, signs it by committee nodes and sends it to RPC. // If tryGroup is false, global scope is used for the signer (useful when // working with native contracts). func (c *InitializeContext) SendCommitteeTx(script []byte, tryGroup bool) error { return c.sendMultiTx(script, tryGroup, false) } // SendConsensusTx creates transaction from script, signs it by alphabet nodes and sends it to RPC. // Not that because this is used only after the contracts were initialized and deployed, // we always try to have a group scope. func (c *InitializeContext) SendConsensusTx(script []byte) error { return c.sendMultiTx(script, true, true) } func (c *InitializeContext) sendMultiTx(script []byte, tryGroup bool, withConsensus bool) error { var act *actor.Actor var err error withConsensus = withConsensus && !c.ConsensusAcc.Contract.ScriptHash().Equals(c.CommitteeAcc.ScriptHash()) if tryGroup { // Even for consensus signatures we need the committee to pay. signers := make([]actor.SignerAccount, 1, 2) signers[0] = actor.SignerAccount{ Signer: c.GetSigner(tryGroup, c.CommitteeAcc), Account: c.CommitteeAcc, } if withConsensus { signers = append(signers, actor.SignerAccount{ Signer: c.GetSigner(tryGroup, c.ConsensusAcc), Account: c.ConsensusAcc, }) } act, err = actor.New(c.Client, signers) } else { if withConsensus { panic("BUG: should never happen") } act, err = c.CommitteeAct, nil } if err != nil { return fmt.Errorf("could not create actor: %w", err) } tx, err := act.MakeUnsignedRun(script, []transaction.Attribute{{Type: transaction.HighPriority}}) if err != nil { return fmt.Errorf("could not perform test invocation: %w", err) } if err := c.MultiSign(tx, constants.CommitteeAccountName); err != nil { return err } if withConsensus { if err := c.MultiSign(tx, constants.ConsensusAccountName); err != nil { return err } } return c.SendTx(tx, c.Command, false) } func (c *InitializeContext) MultiSignAndSend(tx *transaction.Transaction, accType string) error { if err := c.MultiSign(tx, accType); err != nil { return err } return c.SendTx(tx, c.Command, false) } func (c *InitializeContext) MultiSign(tx *transaction.Transaction, accType string) error { version, err := c.Client.GetVersion() if err != nil { // error appears only if client // has not been initialized panic(err) } network := version.Protocol.Network // Use parameter context to avoid dealing with signature order. pc := context.NewParameterContext("", network, tx) h := c.CommitteeAcc.Contract.ScriptHash() if accType == constants.ConsensusAccountName { h = c.ConsensusAcc.Contract.ScriptHash() } for _, w := range c.Wallets { acc, err := GetWalletAccount(w, accType) if err != nil { return fmt.Errorf("can't find %s wallet account: %w", accType, err) } priv := acc.PrivateKey() sign := priv.SignHashable(uint32(network), tx) if err := pc.AddSignature(h, acc.Contract, priv.PublicKey(), sign); err != nil { return fmt.Errorf("can't add signature: %w", err) } if len(pc.Items[h].Signatures) == len(acc.Contract.Parameters) { break } } w, err := pc.GetWitness(h) if err != nil { return fmt.Errorf("incomplete signature: %w", err) } for i := range tx.Signers { if tx.Signers[i].Account == h { if i < len(tx.Scripts) { tx.Scripts[i] = *w } else if i == len(tx.Scripts) { tx.Scripts = append(tx.Scripts, *w) } else { panic("BUG: invalid signing order") } return nil } } return fmt.Errorf("%s account was not found among transaction signers", accType) } // EmitUpdateNNSGroupScript emits script for updating group key stored in NNS. // First return value is true iff the key is already there and nothing should be done. // Second return value is true iff a domain registration code was emitted. func (c *InitializeContext) EmitUpdateNNSGroupScript(bw *io.BufBinWriter, nnsHash util.Uint160, pub *keys.PublicKey) (bool, bool, error) { isAvail, err := NNSIsAvailable(c.Client, nnsHash, client.NNSGroupKeyName) if err != nil { return false, false, err } if !isAvail { currentPub, err := NNSResolveKey(c.ReadOnlyInvoker, nnsHash, client.NNSGroupKeyName) if err != nil { return false, false, err } if pub.Equal(currentPub) { return true, false, nil } } if isAvail { emit.AppCall(bw.BinWriter, nnsHash, "register", callflag.All, client.NNSGroupKeyName, c.CommitteeAcc.Contract.ScriptHash(), constants.FrostfsOpsEmail, constants.NNSRefreshDefVal, constants.NNSRetryDefVal, int64(constants.DefaultExpirationTime), constants.NNSTtlDefVal) emit.Opcodes(bw.BinWriter, opcode.ASSERT) } emit.AppCall(bw.BinWriter, nnsHash, "deleteRecords", callflag.All, "group.frostfs", int64(nns.TXT)) emit.AppCall(bw.BinWriter, nnsHash, "addRecord", callflag.All, "group.frostfs", int64(nns.TXT), hex.EncodeToString(pub.Bytes())) return false, isAvail, nil } func (c *InitializeContext) NNSRegisterDomainScript(nnsHash, expectedHash util.Uint160, domain string) ([]byte, bool, error) { ok, err := NNSIsAvailable(c.Client, nnsHash, domain) if err != nil { return nil, false, err } if ok { bw := io.NewBufBinWriter() emit.AppCall(bw.BinWriter, nnsHash, "register", callflag.All, domain, c.CommitteeAcc.Contract.ScriptHash(), constants.FrostfsOpsEmail, constants.NNSRefreshDefVal, constants.NNSRetryDefVal, int64(constants.DefaultExpirationTime), constants.NNSTtlDefVal) emit.Opcodes(bw.BinWriter, opcode.ASSERT) if bw.Err != nil { panic(bw.Err) } return bw.Bytes(), false, nil } s, err := NNSResolveHash(c.ReadOnlyInvoker, nnsHash, domain) if err != nil { return nil, false, err } return nil, s == expectedHash, nil } func (c *InitializeContext) NNSRootRegistered(nnsHash util.Uint160, zone string) (bool, error) { res, err := c.CommitteeAct.Call(nnsHash, "isAvailable", "name."+zone) if err != nil { return false, err } return res.State == vmstate.Halt.String(), nil } func (c *InitializeContext) IsUpdated(ctrHash util.Uint160, cs *ContractState) bool { r := management.NewReader(c.ReadOnlyInvoker) realCs, err := r.GetContract(ctrHash) return err == nil && realCs != nil && realCs.NEF.Checksum == cs.NEF.Checksum } func (c *InitializeContext) GetContract(ctrName string) *ContractState { return c.Contracts[ctrName] } func (c *InitializeContext) GetAlphabetDeployItems(i, n int) []any { items := make([]any, 5) items[0] = c.Contracts[constants.NetmapContract].Hash items[1] = c.Contracts[constants.ProxyContract].Hash items[2] = innerring.GlagoliticLetter(i).String() items[3] = int64(i) items[4] = int64(n) return items }