545 lines
15 KiB
Go
545 lines
15 KiB
Go
package util
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"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"
|
|
)
|
|
|
|
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 fmt.Errorf("epoch duration must be positive")
|
|
}
|
|
|
|
if viper.GetInt64(commonflags.MaxObjectSizeInitFlag) <= 0 {
|
|
return fmt.Errorf("max object size must be positive")
|
|
}
|
|
|
|
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, int64(3600), int64(600), int64(constants.DefaultExpirationTime), int64(3600))
|
|
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, int64(3600), int64(600), int64(constants.DefaultExpirationTime), int64(3600))
|
|
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
|
|
}
|