package morph import ( "archive/tar" "compress/gzip" "encoding/hex" "errors" "fmt" "io" "os" "path/filepath" "strings" "git.frostfs.info/TrueCloudLab/frostfs-contract/common" "git.frostfs.info/TrueCloudLab/frostfs-contract/nns" morphUtil "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/util" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring" morphClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/encoding/address" io2 "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/invoker" "github.com/nspcc-dev/neo-go/pkg/rpcclient/management" "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "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/stackitem" "github.com/spf13/viper" ) const frostfsIDAdminConfigKey = "frostfsid.admin" var ( contractList = []string{ morphUtil.BalanceContract, morphUtil.ContainerContract, morphUtil.FrostfsIDContract, morphUtil.NetmapContract, morphUtil.PolicyContract, morphUtil.ProxyContract, } fullContractList = append([]string{ morphUtil.FrostfsContract, morphUtil.ProcessingContract, morphUtil.NNSContract, morphUtil.AlphabetContract, }, contractList...) netmapConfigKeys = []string{ netmap.EpochDurationConfig, netmap.MaxObjectSizeConfig, netmap.ContainerFeeConfig, netmap.ContainerAliasFeeConfig, netmap.IrCandidateFeeConfig, netmap.WithdrawFeeConfig, netmap.HomomorphicHashingDisabledKey, netmap.MaintenanceModeAllowedConfig, } ) const ( updateMethodName = "update" deployMethodName = "deploy" ) func deployNNS(c *InitializeContext, method string) error { cs := c.GetContract(morphUtil.NNSContract) h := cs.Hash nnsCs, err := c.NNSContractState() if err != nil { return err } if nnsCs != nil { if nnsCs.NEF.Checksum == cs.NEF.Checksum { if method == deployMethodName { c.Command.Println("NNS contract is already deployed.") } else { c.Command.Println("NNS contract is already updated.") } return nil } h = nnsCs.Hash } err = addManifestGroup(c.ContractWallet, h, cs) if err != nil { return fmt.Errorf("can't sign manifest group: %v", err) } params := getContractDeployParameters(cs, nil) invokeHash := management.Hash if method == updateMethodName { invokeHash = nnsCs.Hash } tx, err := c.CommitteeAct.MakeCall(invokeHash, method, params...) if err != nil { return fmt.Errorf("failed to create deploy tx for %s: %w", morphUtil.NNSContract, err) } if err := c.MultiSignAndSend(tx, morphUtil.CommitteeAccountName); err != nil { return fmt.Errorf("can't send deploy transaction: %w", err) } return c.AwaitTx() } func updateContractsInternal(c *InitializeContext) error { alphaCs := c.GetContract(morphUtil.AlphabetContract) nnsCs, err := c.NNSContractState() if err != nil { return err } nnsHash := nnsCs.Hash w := io2.NewBufBinWriter() // Update script size for a single-node committee is close to the maximum allowed size of 65535. // Because of this we want to reuse alphabet contract NEF and manifest for different updates. // The generated script is as following. // 1. Initialize static slot for alphabet NEF. // 2. Store NEF into the static slot. // 3. Push parameters for each alphabet contract on stack. // 4. Add contract group to the manifest. // 5. For each alphabet contract, invoke `update` using parameters on stack and // NEF from step 2 and manifest from step 4. emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1}) emit.Bytes(w.BinWriter, alphaCs.RawNEF) emit.Opcodes(w.BinWriter, opcode.STSFLD0) keysParam, err := deployAlphabetAccounts(c, nnsHash, w, alphaCs) if err != nil { return err } w.Reset() if err = deployOrUpdateContracts(c, w, nnsHash, keysParam); err != nil { return err } groupKey := c.ContractWallet.Accounts[0].PrivateKey().PublicKey() _, _, err = c.EmitUpdateNNSGroupScript(w, nnsHash, groupKey) if err != nil { return err } c.Command.Printf("NNS: Set %s -> %s\n", morphClient.NNSGroupKeyName, hex.EncodeToString(groupKey.Bytes())) emit.Opcodes(w.BinWriter, opcode.LDSFLD0) emit.Int(w.BinWriter, 1) emit.Opcodes(w.BinWriter, opcode.PACK) emit.AppCallNoArgs(w.BinWriter, nnsHash, "setPrice", callflag.All) if err := c.SendCommitteeTx(w.Bytes(), false); err != nil { return err } return c.AwaitTx() } func deployOrUpdateContracts(c *InitializeContext, w *io2.BufBinWriter, nnsHash util.Uint160, keysParam []any) error { emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1}) emit.AppCall(w.BinWriter, nnsHash, "getPrice", callflag.All) emit.Opcodes(w.BinWriter, opcode.STSFLD0) emit.AppCall(w.BinWriter, nnsHash, "setPrice", callflag.All, 1) for _, ctrName := range contractList { cs := c.GetContract(ctrName) method := updateMethodName ctrHash, err := morphUtil.NNSResolveHash(c.ReadOnlyInvoker, nnsHash, morphUtil.DomainOf(ctrName)) if err != nil { if errors.Is(err, errMissingNNSRecord) { // if contract not found we deploy it instead of update method = deployMethodName } else { return fmt.Errorf("can't resolve hash for contract update: %w", err) } } err = addManifestGroup(c.ContractWallet, ctrHash, cs) if err != nil { return fmt.Errorf("can't sign manifest group: %v", err) } invokeHash := management.Hash if method == updateMethodName { invokeHash = ctrHash } args, err := getContractDeployData(c, ctrName, keysParam, updateMethodName) if err != nil { return fmt.Errorf("%s: getting update params: %v", ctrName, err) } params := getContractDeployParameters(cs, args) res, err := c.CommitteeAct.MakeCall(invokeHash, method, params...) if err != nil { if method != updateMethodName || !strings.Contains(err.Error(), common.ErrAlreadyUpdated) { return fmt.Errorf("deploy contract: %w", err) } c.Command.Printf("%s contract is already updated.\n", ctrName) continue } w.WriteBytes(res.Script) if method == deployMethodName { // same actions are done in InitializeContext.setNNS, can be unified domain := ctrName + ".frostfs" script, ok, err := c.NNSRegisterDomainScript(nnsHash, cs.Hash, domain) if err != nil { return err } if !ok { w.WriteBytes(script) emit.AppCall(w.BinWriter, nnsHash, "deleteRecords", callflag.All, domain, int64(nns.TXT)) emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All, domain, int64(nns.TXT), cs.Hash.StringLE()) emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All, domain, int64(nns.TXT), address.Uint160ToString(cs.Hash)) } c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE()) } } return nil } func deployAlphabetAccounts(c *InitializeContext, nnsHash util.Uint160, w *io2.BufBinWriter, alphaCs *morphUtil.ContractState) ([]any, error) { var keysParam []any baseGroups := alphaCs.Manifest.Groups // alphabet contracts should be deployed by individual nodes to get different hashes. for i, acc := range c.Accounts { ctrHash, err := morphUtil.NNSResolveHash(c.ReadOnlyInvoker, nnsHash, getAlphabetNNSDomain(i)) if err != nil { return nil, fmt.Errorf("can't resolve hash for contract update: %w", err) } keysParam = append(keysParam, acc.PrivateKey().PublicKey().Bytes()) params := c.GetAlphabetDeployItems(i, len(c.Wallets)) emit.Array(w.BinWriter, params...) alphaCs.Manifest.Groups = baseGroups err = addManifestGroup(c.ContractWallet, ctrHash, alphaCs) if err != nil { return nil, fmt.Errorf("can't sign manifest group: %v", err) } emit.Bytes(w.BinWriter, alphaCs.RawManifest) emit.Opcodes(w.BinWriter, opcode.LDSFLD0) emit.Int(w.BinWriter, 3) emit.Opcodes(w.BinWriter, opcode.PACK) emit.AppCallNoArgs(w.BinWriter, ctrHash, updateMethodName, callflag.All) } if err := c.SendCommitteeTx(w.Bytes(), false); err != nil { if !strings.Contains(err.Error(), common.ErrAlreadyUpdated) { return nil, err } c.Command.Println("Alphabet contracts are already updated.") } return keysParam, nil } func deployContracts(c *InitializeContext) error { alphaCs := c.GetContract(morphUtil.AlphabetContract) var keysParam []any baseGroups := alphaCs.Manifest.Groups // alphabet contracts should be deployed by individual nodes to get different hashes. for i, acc := range c.Accounts { ctrHash := state.CreateContractHash(acc.Contract.ScriptHash(), alphaCs.NEF.Checksum, alphaCs.Manifest.Name) if c.IsUpdated(ctrHash, alphaCs) { c.Command.Printf("Alphabet contract #%d is already deployed.\n", i) continue } alphaCs.Manifest.Groups = baseGroups err := addManifestGroup(c.ContractWallet, ctrHash, alphaCs) if err != nil { return fmt.Errorf("can't sign manifest group: %v", err) } keysParam = append(keysParam, acc.PrivateKey().PublicKey().Bytes()) params := getContractDeployParameters(alphaCs, c.GetAlphabetDeployItems(i, len(c.Wallets))) act, err := actor.NewSimple(c.Client, acc) if err != nil { return fmt.Errorf("could not create actor: %w", err) } txHash, vub, err := act.SendCall(management.Hash, deployMethodName, params...) if err != nil { return fmt.Errorf("can't deploy alphabet #%d contract: %w", i, err) } c.SentTxs = append(c.SentTxs, morphUtil.HashVUBPair{Hash: txHash, Vub: vub}) } for _, ctrName := range contractList { cs := c.GetContract(ctrName) ctrHash := cs.Hash if c.IsUpdated(ctrHash, cs) { c.Command.Printf("%s contract is already deployed.\n", ctrName) continue } err := addManifestGroup(c.ContractWallet, ctrHash, cs) if err != nil { return fmt.Errorf("can't sign manifest group: %v", err) } args, err := getContractDeployData(c, ctrName, keysParam, deployMethodName) if err != nil { return fmt.Errorf("%s: getting deploy params: %v", ctrName, err) } params := getContractDeployParameters(cs, args) res, err := c.CommitteeAct.MakeCall(management.Hash, deployMethodName, params...) if err != nil { return fmt.Errorf("can't deploy %s contract: %w", ctrName, err) } if err := c.SendCommitteeTx(res.Script, false); err != nil { return err } } return c.AwaitTx() } func (c *InitializeContext) IsUpdated(ctrHash util.Uint160, cs *morphUtil.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) *morphUtil.ContractState { return c.Contracts[ctrName] } func (c *InitializeContext) readContracts(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 io.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 != morphUtil.AlphabetContract { cs := c.Contracts[ctrName] cs.Hash = state.CreateContractHash(c.CommitteeAcc.Contract.ScriptHash(), cs.NEF.Checksum, cs.Manifest.Name) } } return nil } func readContract(ctrPath, ctrName string) (*morphUtil.ContractState, error) { rawNef, err := os.ReadFile(filepath.Join(ctrPath, ctrName+"_contract.nef")) if err != nil { return nil, fmt.Errorf("can't read NEF file for %s contract: %w", ctrName, err) } rawManif, err := os.ReadFile(filepath.Join(ctrPath, "config.json")) if err != nil { return nil, fmt.Errorf("can't read manifest file for %s contract: %w", ctrName, err) } cs := &morphUtil.ContractState{ RawNEF: rawNef, RawManifest: rawManif, } return cs, cs.Parse() } func readContractsFromArchive(file io.Reader, names []string) (map[string]*morphUtil.ContractState, error) { m := make(map[string]*morphUtil.ContractState, len(names)) for i := range names { m[names[i]] = new(morphUtil.ContractState) } gr, err := gzip.NewReader(file) if err != nil { return nil, fmt.Errorf("contracts file must be tar.gz archive: %w", err) } r := tar.NewReader(gr) for h, err := r.Next(); ; h, err = r.Next() { if err != nil { break } dir, _ := filepath.Split(h.Name) ctrName := filepath.Base(dir) cs, ok := m[ctrName] if !ok { continue } switch { case strings.HasSuffix(h.Name, filepath.Join(ctrName, ctrName+"_contract.nef")): cs.RawNEF, err = io.ReadAll(r) if err != nil { return nil, fmt.Errorf("can't read NEF file for %s contract: %w", ctrName, err) } case strings.HasSuffix(h.Name, "config.json"): cs.RawManifest, err = io.ReadAll(r) if err != nil { return nil, fmt.Errorf("can't read manifest file for %s contract: %w", ctrName, err) } } m[ctrName] = cs } for ctrName, cs := range m { if cs.RawNEF == nil { return nil, fmt.Errorf("NEF for %s contract wasn't found", ctrName) } if cs.RawManifest == nil { return nil, fmt.Errorf("manifest for %s contract wasn't found", ctrName) } } return m, nil } func getContractDeployParameters(cs *morphUtil.ContractState, deployData []any) []any { return []any{cs.RawNEF, cs.RawManifest, deployData} } func getContractDeployData(c *InitializeContext, ctrName string, keysParam []any, method string) ([]any, error) { items := make([]any, 0, 6) switch ctrName { case morphUtil.FrostfsContract: items = append(items, c.Contracts[morphUtil.ProcessingContract].Hash, keysParam, smartcontract.Parameter{}) case morphUtil.ProcessingContract: items = append(items, c.Contracts[morphUtil.FrostfsContract].Hash) return items[1:], nil // no notary info case morphUtil.BalanceContract: items = append(items, c.Contracts[morphUtil.NetmapContract].Hash, c.Contracts[morphUtil.ContainerContract].Hash) case morphUtil.ContainerContract: // In case if NNS is updated multiple times, we can't calculate // it's actual hash based on local data, thus query chain. r := management.NewReader(c.ReadOnlyInvoker) nnsCs, err := r.GetContractByID(1) if err != nil { return nil, fmt.Errorf("get nns contract: %w", err) } items = append(items, c.Contracts[morphUtil.NetmapContract].Hash, c.Contracts[morphUtil.BalanceContract].Hash, c.Contracts[morphUtil.FrostfsIDContract].Hash, nnsCs.Hash, "container") case morphUtil.FrostfsIDContract: var ( h util.Uint160 found bool err error ) if method == updateMethodName { h, found, err = getFrostfsIDAdminFromContract(c.ReadOnlyInvoker) } if method != updateMethodName || err == nil && !found { h, found, err = getFrostfsIDAdmin(viper.GetViper()) } if err != nil { return nil, err } if found { items = append(items, h) } else { items = append(items, c.Contracts[morphUtil.ProxyContract].Hash) } case morphUtil.NetmapContract: md := getDefaultNetmapContractConfigMap() if method == updateMethodName { if err := mergeNetmapConfig(c.ReadOnlyInvoker, md); err != nil { return nil, err } } var configParam []any for k, v := range md { configParam = append(configParam, k, v) } items = append(items, c.Contracts[morphUtil.BalanceContract].Hash, c.Contracts[morphUtil.ContainerContract].Hash, keysParam, configParam) case morphUtil.ProxyContract: items = nil case morphUtil.PolicyContract: items = append(items, c.Contracts[morphUtil.ProxyContract].Hash) default: panic(fmt.Sprintf("invalid contract name: %s", ctrName)) } return items, nil } func getFrostfsIDAdminFromContract(roInvoker *invoker.Invoker) (util.Uint160, bool, error) { r := management.NewReader(roInvoker) cs, err := r.GetContractByID(1) if err != nil { return util.Uint160{}, false, fmt.Errorf("get nns contract: %w", err) } fidHash, err := morphUtil.NNSResolveHash(roInvoker, cs.Hash, morphUtil.DomainOf(morphUtil.FrostfsIDContract)) if err != nil { return util.Uint160{}, false, fmt.Errorf("resolve frostfsid contract hash: %w", err) } item, err := unwrap.Item(roInvoker.Call(fidHash, "getAdmin")) if err != nil { return util.Uint160{}, false, fmt.Errorf("getAdmin: %w", err) } if _, ok := item.(stackitem.Null); ok { return util.Uint160{}, false, nil } bs, err := item.TryBytes() if err != nil { return util.Uint160{}, true, fmt.Errorf("getAdmin: decode result: %w", err) } h, err := util.Uint160DecodeBytesBE(bs) if err != nil { return util.Uint160{}, true, fmt.Errorf("getAdmin: decode result: %w", err) } return h, true, nil } func getNetConfigFromNetmapContract(roInvoker *invoker.Invoker) ([]stackitem.Item, error) { r := management.NewReader(roInvoker) cs, err := r.GetContractByID(1) if err != nil { return nil, fmt.Errorf("get nns contract: %w", err) } nmHash, err := morphUtil.NNSResolveHash(roInvoker, cs.Hash, morphUtil.DomainOf(morphUtil.NetmapContract)) if err != nil { return nil, fmt.Errorf("can't get netmap contract hash: %w", err) } arr, err := unwrap.Array(roInvoker.Call(nmHash, "listConfig")) if err != nil { return nil, fmt.Errorf("can't fetch list of network config keys from the netmap contract") } return arr, err } func mergeNetmapConfig(roInvoker *invoker.Invoker, md map[string]any) error { arr, err := getNetConfigFromNetmapContract(roInvoker) if err != nil { return err } m, err := parseConfigFromNetmapContract(arr) if err != nil { return err } for k, v := range m { for _, key := range netmapConfigKeys { if k == key { md[k] = v break } } } return nil } func (c *InitializeContext) GetAlphabetDeployItems(i, n int) []any { items := make([]any, 5) items[0] = c.Contracts[morphUtil.NetmapContract].Hash items[1] = c.Contracts[morphUtil.ProxyContract].Hash items[2] = innerring.GlagoliticLetter(i).String() items[3] = int64(i) items[4] = int64(n) return items }