From 425c1db5c0a31608f85c3a10266cbec3a569b68c Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 19 Jul 2021 14:04:19 +0300 Subject: [PATCH] [#687] neofs-adm: transfer funds to consensus wallets This is the first stage requiring running blockchain. Signed-off-by: Evgenii Stratonikov --- .../internal/modules/morph/initialize.go | 148 ++++++++++++++++++ .../modules/morph/initialize_transfer.go | 124 +++++++++++++++ .../internal/modules/morph/n3client.go | 21 +++ 3 files changed, 293 insertions(+) create mode 100644 cmd/neofs-adm/internal/modules/morph/initialize_transfer.go create mode 100644 cmd/neofs-adm/internal/modules/morph/n3client.go diff --git a/cmd/neofs-adm/internal/modules/morph/initialize.go b/cmd/neofs-adm/internal/modules/morph/initialize.go index 18a0da04f..3431ecee0 100644 --- a/cmd/neofs-adm/internal/modules/morph/initialize.go +++ b/cmd/neofs-adm/internal/modules/morph/initialize.go @@ -1,10 +1,35 @@ package morph import ( + "errors" + "fmt" + "io/ioutil" + "path" + "time" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/config" + "github.com/nspcc-dev/neofs-node/pkg/innerring" "github.com/spf13/cobra" "github.com/spf13/viper" ) +type initializeContext struct { + Client *client.Client + // CommitteeAcc is used for retrieving committee address and verification script. + CommitteeAcc *wallet.Account + // ConsensusAcc is used for retrieving committee address and verification script. + ConsensusAcc *wallet.Account + Wallets []*wallet.Wallet + Hashes []util.Uint256 + WaitDuration time.Duration + PollInterval time.Duration +} + func initializeSideChainCmd(cmd *cobra.Command, args []string) error { // contract path is not part of the config contractsPath, err := cmd.Flags().GetString(contractsInitFlag) @@ -12,6 +37,23 @@ func initializeSideChainCmd(cmd *cobra.Command, args []string) error { return err } + initCtx, err := newInitializeContext(viper.GetViper()) + if err != nil { + return fmt.Errorf("initialization error: %w", err) + } + + // 1. Transfer funds to committee accounts. + cmd.Println("Stage 1: transfer GAS to alphabet nodes.") + if err := initCtx.transferFunds(); err != nil { + return err + } + + // TODO 2. Setup notary and alphabet nodes in designate contract. + // TODO 3. Deploy NNS contract with ID=0. + // TODO 4. Deploy NeoFS contracts. + // TODO 5. Setup NeoFS contracts addresses in NNS. + // TODO 6. Register candidates and call alphabet.Vote. + cmd.Println("endpoint:", viper.GetString(endpointFlag)) cmd.Println("alphabet-wallets:", viper.GetString(alphabetWalletsFlag)) cmd.Println("contracts:", contractsPath) @@ -20,3 +62,109 @@ func initializeSideChainCmd(cmd *cobra.Command, args []string) error { return nil } + +func newInitializeContext(v *viper.Viper) (*initializeContext, error) { + walletDir := v.GetString(alphabetWalletsFlag) + wallets, err := openAlphabetWallets(walletDir) + if err != nil { + return nil, err + } + + c, err := getN3Client(v) + if err != nil { + return nil, fmt.Errorf("can't create N3 client: %w", err) + } + + committeeAcc, err := getWalletAccount(wallets[0], committeeAccountName) + if err != nil { + return nil, fmt.Errorf("can't find committee account: %w", err) + } + + consensusAcc, err := getWalletAccount(wallets[0], consensusAccountName) + if err != nil { + return nil, fmt.Errorf("can't find consensus account: %w", err) + } + + initCtx := &initializeContext{ + Client: c, + ConsensusAcc: consensusAcc, + CommitteeAcc: committeeAcc, + Wallets: wallets, + WaitDuration: time.Second * 30, + PollInterval: time.Second, + } + + return initCtx, nil +} + +func openAlphabetWallets(walletDir string) ([]*wallet.Wallet, error) { + walletFiles, err := ioutil.ReadDir(walletDir) + if err != nil { + return nil, fmt.Errorf("can't read alphabet wallets dir: %w", err) + } + + size := len(walletFiles) + if size == 0 { + return nil, errors.New("alphabet wallets dir is empty (run `generate-alphabet` command first)") + } + + wallets := make([]*wallet.Wallet, size) + for i := 0; i < size; i++ { + p := path.Join(walletDir, innerring.GlagoliticLetter(i).String()+".json") + w, err := wallet.NewWalletFromFile(p) + if err != nil { + return nil, fmt.Errorf("can't open wallet: %w", err) + } + + password, err := config.AlphabetPassword(viper.GetViper(), i) + if err != nil { + return nil, fmt.Errorf("can't fetch password: %w", err) + } + + for i := range w.Accounts { + if err := w.Accounts[i].Decrypt(password, keys.NEP2ScryptParams()); err != nil { + return nil, fmt.Errorf("can't unlock wallet: %w", err) + } + } + + wallets[i] = w + } + + return wallets, nil +} + +func (c *initializeContext) awaitTx() error { + tick := time.NewTicker(c.PollInterval) + defer tick.Stop() + + timer := time.NewTimer(c.WaitDuration) + defer timer.Stop() + + at := trigger.Application + +loop: + for i := range c.Hashes { + for { + select { + case <-tick.C: + _, err := c.Client.GetApplicationLog(c.Hashes[i], &at) + if err == nil { + continue loop + } + case <-timer.C: + return errors.New("timeout while waiting for transaction to persist") + } + } + } + + return nil +} + +func getWalletAccount(w *wallet.Wallet, typ string) (*wallet.Account, error) { + for i := range w.Accounts { + if w.Accounts[i].Label == typ { + return w.Accounts[i], nil + } + } + return nil, fmt.Errorf("account for '%s' not found", typ) +} diff --git a/cmd/neofs-adm/internal/modules/morph/initialize_transfer.go b/cmd/neofs-adm/internal/modules/morph/initialize_transfer.go new file mode 100644 index 000000000..563abd2f8 --- /dev/null +++ b/cmd/neofs-adm/internal/modules/morph/initialize_transfer.go @@ -0,0 +1,124 @@ +package morph + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" + scContext "github.com/nspcc-dev/neo-go/pkg/smartcontract/context" +) + +const ( + gasInitialTotalSupply = 30000000 * native.GASFactor + // initialAlphabetGASAmount represents amount of GAS given to each alphabet node. + initialAlphabetGASAmount = 10_000 * native.GASFactor +) + +func (c *initializeContext) transferFunds() error { + gasHash, err := c.Client.GetNativeContractHash(nativenames.Gas) + if err != nil { + return fmt.Errorf("can't fetch %s hash: %w", nativenames.Gas, err) + } + neoHash, err := c.Client.GetNativeContractHash(nativenames.Neo) + if err != nil { + return fmt.Errorf("can't fetch %s hash: %w", nativenames.Neo, err) + } + + var transfers []client.TransferTarget + for _, w := range c.Wallets { + acc, err := getWalletAccount(w, singleAccountName) + if err != nil { + return err + } + + to := acc.Contract.ScriptHash() + transfers = append(transfers, + client.TransferTarget{ + Token: gasHash, + Address: to, + Amount: initialAlphabetGASAmount, + }, + ) + } + + // It is convenient to have all funds at the committee account. + transfers = append(transfers, + client.TransferTarget{ + Token: gasHash, + Address: c.CommitteeAcc.Contract.ScriptHash(), + Amount: gasInitialTotalSupply - initialAlphabetGASAmount*int64(len(c.Wallets)), + }, + client.TransferTarget{ + Token: neoHash, + Address: c.CommitteeAcc.Contract.ScriptHash(), + Amount: native.NEOTotalSupply, + }, + ) + + tx, err := c.Client.CreateNEP17MultiTransferTx(c.ConsensusAcc, 0, transfers, []client.SignerAccount{{ + Signer: transaction.Signer{ + Account: c.ConsensusAcc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: c.ConsensusAcc, + }}) + if err != nil { + return fmt.Errorf("can't create transfer transaction: %w", err) + } + + if err := c.multiSignAndSend(tx, consensusAccountName); err != nil { + return fmt.Errorf("can't send transfer transaction: %w", err) + } + + return c.awaitTx() +} + +func (c *initializeContext) multiSignAndSend(tx *transaction.Transaction, accType string) error { + if err := c.multiSign(tx, accType); err != nil { + return err + } + + h, err := c.Client.SendRawTransaction(tx) + if err != nil { + return err + } + + c.Hashes = append(c.Hashes, h) + return nil +} + +func (c *initializeContext) multiSign(tx *transaction.Transaction, accType string) error { + network := c.Client.GetNetwork() + + // Use parameter context to avoid dealing with signature order. + pc := scContext.NewParameterContext("", network, tx) + h := c.CommitteeAcc.Contract.ScriptHash() + if accType == 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(tx.Signers[0].Account) + if err != nil { + return fmt.Errorf("incomplete signature: %w", err) + } + tx.Scripts = append(tx.Scripts, *w) + + return nil +} diff --git a/cmd/neofs-adm/internal/modules/morph/n3client.go b/cmd/neofs-adm/internal/modules/morph/n3client.go new file mode 100644 index 000000000..f3e4c1541 --- /dev/null +++ b/cmd/neofs-adm/internal/modules/morph/n3client.go @@ -0,0 +1,21 @@ +package morph + +import ( + "context" + + "github.com/nspcc-dev/neo-go/pkg/rpc/client" + "github.com/spf13/viper" +) + +func getN3Client(v *viper.Viper) (*client.Client, error) { + ctx := context.Background() // FIXME(@fyrchik): timeout context + endpoint := v.GetString(endpointFlag) + c, err := client.New(ctx, endpoint, client.Options{}) + if err != nil { + return nil, err + } + if err := c.Init(); err != nil { + return nil, err + } + return c, nil +}