From e2216faf1f350ba7a02174aa4527d4e6bb2ed75a Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 29 Jun 2023 13:44:12 +0400 Subject: [PATCH 01/22] sidechain/netmap: Export configuration key constants It's worth to make these constants reusable. Signed-off-by: Leonard Lyubich --- pkg/morph/client/netmap/config.go | 74 +++++++++++++++---------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/pkg/morph/client/netmap/config.go b/pkg/morph/client/netmap/config.go index d7da04d323..f9a8e1159e 100644 --- a/pkg/morph/client/netmap/config.go +++ b/pkg/morph/client/netmap/config.go @@ -11,24 +11,24 @@ import ( ) const ( - maxObjectSizeConfig = "MaxObjectSize" - basicIncomeRateConfig = "BasicIncomeRate" - auditFeeConfig = "AuditFee" - epochDurationConfig = "EpochDuration" - containerFeeConfig = "ContainerFee" - containerAliasFeeConfig = "ContainerAliasFee" - etIterationsConfig = "EigenTrustIterations" - etAlphaConfig = "EigenTrustAlpha" - irCandidateFeeConfig = "InnerRingCandidateFee" - withdrawFeeConfig = "WithdrawFee" - homomorphicHashingDisabledKey = "HomomorphicHashingDisabled" - maintenanceModeAllowedConfig = "MaintenanceModeAllowed" + MaxObjectSizeConfig = "MaxObjectSize" + BasicIncomeRateConfig = "BasicIncomeRate" + AuditFeeConfig = "AuditFee" + EpochDurationConfig = "EpochDuration" + ContainerFeeConfig = "ContainerFee" + ContainerAliasFeeConfig = "ContainerAliasFee" + EigenTrustIterationsConfig = "EigenTrustIterations" + EigenTrustAlphaConfig = "EigenTrustAlpha" + InnerRingCandidateFeeConfig = "InnerRingCandidateFee" + WithdrawFeeConfig = "WithdrawFee" + HomomorphicHashingDisabledKey = "HomomorphicHashingDisabled" + MaintenanceModeAllowedConfig = "MaintenanceModeAllowed" ) // MaxObjectSize receives max object size configuration // value through the Netmap contract call. func (c *Client) MaxObjectSize() (uint64, error) { - objectSize, err := c.readUInt64Config(maxObjectSizeConfig) + objectSize, err := c.readUInt64Config(MaxObjectSizeConfig) if err != nil { return 0, fmt.Errorf("(%T) could not get epoch number: %w", c, err) } @@ -39,7 +39,7 @@ func (c *Client) MaxObjectSize() (uint64, error) { // BasicIncomeRate returns basic income rate configuration value from network // config in netmap contract. func (c *Client) BasicIncomeRate() (uint64, error) { - rate, err := c.readUInt64Config(basicIncomeRateConfig) + rate, err := c.readUInt64Config(BasicIncomeRateConfig) if err != nil { return 0, fmt.Errorf("(%T) could not get basic income rate: %w", c, err) } @@ -50,7 +50,7 @@ func (c *Client) BasicIncomeRate() (uint64, error) { // AuditFee returns audit fee configuration value from network // config in netmap contract. func (c *Client) AuditFee() (uint64, error) { - fee, err := c.readUInt64Config(auditFeeConfig) + fee, err := c.readUInt64Config(AuditFeeConfig) if err != nil { return 0, fmt.Errorf("(%T) could not get audit fee: %w", c, err) } @@ -60,7 +60,7 @@ func (c *Client) AuditFee() (uint64, error) { // EpochDuration returns number of sidechain blocks per one NeoFS epoch. func (c *Client) EpochDuration() (uint64, error) { - epochDuration, err := c.readUInt64Config(epochDurationConfig) + epochDuration, err := c.readUInt64Config(EpochDurationConfig) if err != nil { return 0, fmt.Errorf("(%T) could not get epoch duration: %w", c, err) } @@ -71,7 +71,7 @@ func (c *Client) EpochDuration() (uint64, error) { // ContainerFee returns fee paid by container owner to each alphabet node // for container registration. func (c *Client) ContainerFee() (uint64, error) { - fee, err := c.readUInt64Config(containerFeeConfig) + fee, err := c.readUInt64Config(ContainerFeeConfig) if err != nil { return 0, fmt.Errorf("(%T) could not get container fee: %w", c, err) } @@ -82,7 +82,7 @@ func (c *Client) ContainerFee() (uint64, error) { // ContainerAliasFee returns additional fee paid by container owner to each // alphabet node for container nice name registration. func (c *Client) ContainerAliasFee() (uint64, error) { - fee, err := c.readUInt64Config(containerAliasFeeConfig) + fee, err := c.readUInt64Config(ContainerAliasFeeConfig) if err != nil { return 0, fmt.Errorf("(%T) could not get container alias fee: %w", c, err) } @@ -93,7 +93,7 @@ func (c *Client) ContainerAliasFee() (uint64, error) { // EigenTrustIterations returns global configuration value of iteration cycles // for EigenTrust algorithm per epoch. func (c *Client) EigenTrustIterations() (uint64, error) { - iterations, err := c.readUInt64Config(etIterationsConfig) + iterations, err := c.readUInt64Config(EigenTrustIterationsConfig) if err != nil { return 0, fmt.Errorf("(%T) could not get eigen trust iterations: %w", c, err) } @@ -104,7 +104,7 @@ func (c *Client) EigenTrustIterations() (uint64, error) { // EigenTrustAlpha returns global configuration value of alpha parameter. // It receives the alpha as a string and tries to convert it to float. func (c *Client) EigenTrustAlpha() (float64, error) { - strAlpha, err := c.readStringConfig(etAlphaConfig) + strAlpha, err := c.readStringConfig(EigenTrustAlphaConfig) if err != nil { return 0, fmt.Errorf("(%T) could not get eigen trust alpha: %w", c, err) } @@ -117,13 +117,13 @@ func (c *Client) EigenTrustAlpha() (float64, error) { // // Returns (false, nil) if config key is not found in the contract. func (c *Client) HomomorphicHashDisabled() (bool, error) { - return c.readBoolConfig(homomorphicHashingDisabledKey) + return c.readBoolConfig(HomomorphicHashingDisabledKey) } // InnerRingCandidateFee returns global configuration value of fee paid by // node to be in inner ring candidates list. func (c *Client) InnerRingCandidateFee() (uint64, error) { - fee, err := c.readUInt64Config(irCandidateFeeConfig) + fee, err := c.readUInt64Config(InnerRingCandidateFeeConfig) if err != nil { return 0, fmt.Errorf("(%T) could not get inner ring candidate fee: %w", c, err) } @@ -134,7 +134,7 @@ func (c *Client) InnerRingCandidateFee() (uint64, error) { // WithdrawFee returns global configuration value of fee paid by user to // withdraw assets from NeoFS contract. func (c *Client) WithdrawFee() (uint64, error) { - fee, err := c.readUInt64Config(withdrawFeeConfig) + fee, err := c.readUInt64Config(WithdrawFeeConfig) if err != nil { return 0, fmt.Errorf("(%T) could not get withdraw fee: %w", c, err) } @@ -148,7 +148,7 @@ func (c *Client) WithdrawFee() (uint64, error) { // // By default, maintenance state is disallowed. func (c *Client) MaintenanceModeAllowed() (bool, error) { - return c.readBoolConfig(maintenanceModeAllowedConfig) + return c.readBoolConfig(MaintenanceModeAllowedConfig) } func (c *Client) readUInt64Config(key string) (uint64, error) { @@ -299,32 +299,32 @@ func (c *Client) ReadNetworkConfiguration() (NetworkConfiguration, error) { Name: name, Value: value, }) - case maxObjectSizeConfig: + case MaxObjectSizeConfig: res.MaxObjectSize = bytesToUint64(value) - case basicIncomeRateConfig: + case BasicIncomeRateConfig: res.StoragePrice = bytesToUint64(value) - case auditFeeConfig: + case AuditFeeConfig: res.AuditFee = bytesToUint64(value) - case epochDurationConfig: + case EpochDurationConfig: res.EpochDuration = bytesToUint64(value) - case containerFeeConfig: + case ContainerFeeConfig: res.ContainerFee = bytesToUint64(value) - case containerAliasFeeConfig: + case ContainerAliasFeeConfig: res.ContainerAliasFee = bytesToUint64(value) - case etIterationsConfig: + case EigenTrustIterationsConfig: res.EigenTrustIterations = bytesToUint64(value) - case etAlphaConfig: + case EigenTrustAlphaConfig: res.EigenTrustAlpha, err = strconv.ParseFloat(string(value), 64) if err != nil { - return fmt.Errorf("invalid prm %s: %v", etAlphaConfig, err) + return fmt.Errorf("invalid prm %s: %v", EigenTrustAlphaConfig, err) } - case irCandidateFeeConfig: + case InnerRingCandidateFeeConfig: res.IRCandidateFee = bytesToUint64(value) - case withdrawFeeConfig: + case WithdrawFeeConfig: res.WithdrawalFee = bytesToUint64(value) - case homomorphicHashingDisabledKey: + case HomomorphicHashingDisabledKey: res.HomomorphicHashingDisabled = bytesToBool(value) - case maintenanceModeAllowedConfig: + case MaintenanceModeAllowedConfig: res.MaintenanceModeAllowed = bytesToBool(value) } From e7e167ca43187ac7f08ffa1624b4eba544d48db1 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 18 Jul 2023 16:53:59 +0400 Subject: [PATCH 02/22] sidechain/deploy: Fix version reading of local contracts Previously, `readContractLocalVersion` didn't pass extra arguments to deploy method. This was normal for the NNS contract, but some other system contracts would fail without the args. The function accepts optional data from now. Another drawback: if contract already exists (deployed earlier), `deploy` method throws `contract already exists` exception. To avoid this, dummy (zero) signer is used as a sender which almost definitely doesn't collide with the real one. One more disadvantage: function didn't add committee signers to deploy invocation. For some contracts that require committee witness for deployment (e.g. Container) version reader could not execute. To cover this, the function now accepts committee members and, for simplicity, always attach them as deployent transaction's signers. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/util.go | 31 +++++++++++++++++++++++++++---- pkg/morph/deploy/util_test.go | 11 ++++++++++- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/pkg/morph/deploy/util.go b/pkg/morph/deploy/util.go index 473ff1bd63..85e31b9678 100644 --- a/pkg/morph/deploy/util.go +++ b/pkg/morph/deploy/util.go @@ -12,6 +12,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "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/io" "github.com/nspcc-dev/neo-go/pkg/neorpc" @@ -19,6 +21,7 @@ import ( "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/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" @@ -244,8 +247,14 @@ func readContractOnChainVersion(b Blockchain, onChainAddress util.Uint160) (cont } // readContractLocalVersion returns version of the local smart contract -// represented by its compiled artifacts. -func readContractLocalVersion(rpc invoker.RPCInvoke, localNEF nef.File, localManifest manifest.Manifest) (contractVersion, error) { +// represented by its compiled artifacts. Deployment is tested using provided +// invoker on behalf of the committee. +func readContractLocalVersion(rpc invoker.RPCInvoke, committee keys.PublicKeys, localNEF nef.File, localManifest manifest.Manifest, deployArgs ...interface{}) (contractVersion, error) { + multiSigScript, err := smartcontract.CreateMultiSigRedeemScript(smartcontract.GetMajorityHonestNodeCount(len(committee)), committee) + if err != nil { + return contractVersion{}, fmt.Errorf("create committee multi-signature verification script: %w", err) + } + jManifest, err := json.Marshal(localManifest) if err != nil { return contractVersion{}, fmt.Errorf("encode manifest into JSON: %w", err) @@ -256,15 +265,29 @@ func readContractLocalVersion(rpc invoker.RPCInvoke, localNEF nef.File, localMan return contractVersion{}, fmt.Errorf("encode NEF into binary: %w", err) } + var deployData interface{} + if len(deployArgs) > 0 { + deployData = deployArgs + } + script := io.NewBufBinWriter() emit.Opcodes(script.BinWriter, opcode.NEWARRAY0) emit.Int(script.BinWriter, int64(callflag.All)) emit.String(script.BinWriter, methodVersion) - emit.AppCall(script.BinWriter, management.Hash, "deploy", callflag.All, bNEF, jManifest) + emit.AppCall(script.BinWriter, management.Hash, "deploy", callflag.All, bNEF, jManifest, deployData) emit.Opcodes(script.BinWriter, opcode.PUSH2, opcode.PICKITEM) emit.Syscall(script.BinWriter, interopnames.SystemContractCall) - res, err := invoker.New(rpc, nil).Run(script.Bytes()) + res, err := invoker.New(rpc, []transaction.Signer{ + { + Account: util.Uint160{}, // zero hash to avoid 'contract already exists' case + Scopes: transaction.None, + }, + { + Account: hash.Hash160(multiSigScript), + Scopes: transaction.Global, + }, + }).Run(script.Bytes()) if err != nil { return contractVersion{}, fmt.Errorf("run test script deploying contract and calling its '%s' method: %w", methodVersion, err) } diff --git a/pkg/morph/deploy/util_test.go b/pkg/morph/deploy/util_test.go index 2d2816e5cd..ce9ab09e54 100644 --- a/pkg/morph/deploy/util_test.go +++ b/pkg/morph/deploy/util_test.go @@ -9,6 +9,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/compiler" "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/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" @@ -148,7 +149,15 @@ func TestReadContractLocalVersion(t *testing.T) { ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{Name: "Helper"}) - res, err := readContractLocalVersion(newTestRPCInvoker(t, e), *ctr.NEF, *ctr.Manifest) + var committeeSigner neotest.SingleSigner + + if single, ok := acc.(neotest.SingleSigner); ok { + committeeSigner = single + } else { + committeeSigner = acc.(neotest.MultiSigner).Single(0) + } + + res, err := readContractLocalVersion(newTestRPCInvoker(t, e), keys.PublicKeys{committeeSigner.Account().PublicKey()}, *ctr.NEF, *ctr.Manifest) require.NoError(t, err) require.EqualValues(t, version, res.toUint64()) } From 7070b8f93be6e8f87170b0d0f31f71f449546d08 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 10 Jul 2023 10:39:29 +0400 Subject: [PATCH 03/22] sidechain/deploy: Add deployment/update stage for system NeoFS contracts Extend Sidechain deployment procedure with Alphabet initialization and deployment/update of all system NeoFS contracts except NNS (implemented earlier). Glagolitic script is moved to shared package in order to reuse it between different applications. Refs #2195. Signed-off-by: Leonard Lyubich --- .../internal/modules/config/config.go | 7 +- .../internal/modules/config/config_test.go | 10 +- .../internal/modules/morph/generate.go | 7 +- .../internal/modules/morph/generate_test.go | 4 +- .../internal/modules/morph/initialize.go | 6 +- .../modules/morph/initialize_deploy.go | 4 +- .../internal/modules/morph/initialize_test.go | 6 +- pkg/innerring/alphabet.go | 161 +------- pkg/innerring/contracts.go | 22 +- pkg/innerring/state.go | 9 +- pkg/morph/deploy/alphabet.go | 97 +++++ pkg/morph/deploy/contracts.go | 384 ++++++++++++++++++ pkg/morph/deploy/deploy.go | 382 ++++++++++++++++- pkg/morph/deploy/nns.go | 29 +- pkg/morph/deploy/notary.go | 41 +- pkg/morph/deploy/util.go | 37 ++ pkg/util/glagolitsa/glagolitsa.go | 56 +++ pkg/util/glagolitsa/glagolitsa_test.go | 60 +++ 18 files changed, 1119 insertions(+), 203 deletions(-) create mode 100644 pkg/morph/deploy/alphabet.go create mode 100644 pkg/util/glagolitsa/glagolitsa.go create mode 100644 pkg/util/glagolitsa/glagolitsa_test.go diff --git a/cmd/neofs-adm/internal/modules/config/config.go b/cmd/neofs-adm/internal/modules/config/config.go index 6f3204fd39..e263e4fd03 100644 --- a/cmd/neofs-adm/internal/modules/config/config.go +++ b/cmd/neofs-adm/internal/modules/config/config.go @@ -8,7 +8,7 @@ import ( "text/template" "github.com/nspcc-dev/neo-go/cli/input" - "github.com/nspcc-dev/neofs-node/pkg/innerring" + "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -126,9 +126,8 @@ func generateConfigExample(appDir string, credSize int) (string, error) { } tmpl.AlphabetDir = filepath.Join(appDir, "alphabet-wallets") - var i innerring.GlagoliticLetter - for i = 0; i < innerring.GlagoliticLetter(credSize); i++ { - tmpl.Glagolitics = append(tmpl.Glagolitics, i.String()) + for i := 0; i < credSize; i++ { + tmpl.Glagolitics = append(tmpl.Glagolitics, glagolitsa.LetterByIndex(i)) } t, err := template.New("config.yml").Parse(configTxtTemplate) diff --git a/cmd/neofs-adm/internal/modules/config/config_test.go b/cmd/neofs-adm/internal/modules/config/config_test.go index 46d8230776..3ea134fcfa 100644 --- a/cmd/neofs-adm/internal/modules/config/config_test.go +++ b/cmd/neofs-adm/internal/modules/config/config_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/nspcc-dev/neofs-node/pkg/innerring" + "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) @@ -34,12 +34,12 @@ func TestGenerateConfigExample(t *testing.T) { require.Equal(t, 1000, v.GetInt("network.fee.container")) require.Equal(t, 100000000, v.GetInt("network.fee.withdraw")) - var i innerring.GlagoliticLetter - for i = 0; i < innerring.GlagoliticLetter(n); i++ { - key := "credentials." + i.String() + var i int + for i = 0; i < n; i++ { + key := "credentials." + glagolitsa.LetterByIndex(i) require.Equal(t, "password", v.GetString(key)) } - key := "credentials." + i.String() + key := "credentials." + glagolitsa.LetterByIndex(i) require.Equal(t, "", v.GetString(key)) } diff --git a/cmd/neofs-adm/internal/modules/morph/generate.go b/cmd/neofs-adm/internal/modules/morph/generate.go index 8eeb781276..f7afea577c 100644 --- a/cmd/neofs-adm/internal/modules/morph/generate.go +++ b/cmd/neofs-adm/internal/modules/morph/generate.go @@ -18,7 +18,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "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/nspcc-dev/neofs-node/pkg/util/glagolitsa" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -61,12 +61,13 @@ func initializeWallets(v *viper.Viper, walletDir string, size int) ([]string, er passwords := make([]string, size) for i := range wallets { - password, err := config.GetPassword(v, innerring.GlagoliticLetter(i).String()) + letter := glagolitsa.LetterByIndex(i) + password, err := config.GetPassword(v, letter) if err != nil { return nil, fmt.Errorf("can't fetch password: %w", err) } - p := filepath.Join(walletDir, innerring.GlagoliticLetter(i).String()+".json") + p := filepath.Join(walletDir, letter+".json") f, err := os.OpenFile(p, os.O_CREATE, 0644) if err != nil { return nil, fmt.Errorf("can't create wallet file: %w", err) diff --git a/cmd/neofs-adm/internal/modules/morph/generate_test.go b/cmd/neofs-adm/internal/modules/morph/generate_test.go index ad2fcae239..f5a63d37d0 100644 --- a/cmd/neofs-adm/internal/modules/morph/generate_test.go +++ b/cmd/neofs-adm/internal/modules/morph/generate_test.go @@ -13,7 +13,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/wallet" - "github.com/nspcc-dev/neofs-node/pkg/innerring" + "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" "github.com/spf13/viper" "github.com/stretchr/testify/require" "golang.org/x/term" @@ -60,7 +60,7 @@ func TestGenerateAlphabet(t *testing.T) { require.NoError(t, generateAlphabetCreds(generateAlphabetCmd, nil)) for i := uint64(0); i < size; i++ { - p := filepath.Join(walletDir, innerring.GlagoliticLetter(i).String()+".json") + p := filepath.Join(walletDir, glagolitsa.LetterByIndex(int(i))+".json") w, err := wallet.NewWalletFromFile(p) require.NoError(t, err, "wallet doesn't exist") require.Equal(t, 3, len(w.Accounts), "not all accounts were created") diff --git a/cmd/neofs-adm/internal/modules/morph/initialize.go b/cmd/neofs-adm/internal/modules/morph/initialize.go index c45acab7e9..9f4c1012a5 100644 --- a/cmd/neofs-adm/internal/modules/morph/initialize.go +++ b/cmd/neofs-adm/internal/modules/morph/initialize.go @@ -17,8 +17,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neofs-contract/rpc/nns" "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/config" - "github.com/nspcc-dev/neofs-node/pkg/innerring" morphClient "github.com/nspcc-dev/neofs-node/pkg/morph/client" + "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -198,7 +198,7 @@ func openAlphabetWallets(v *viper.Viper, walletDir string) ([]*wallet.Wallet, er var size int loop: for i := 0; i < len(walletFiles); i++ { - name := innerring.GlagoliticLetter(i).String() + ".json" + name := glagolitsa.LetterByIndex(i) + ".json" for j := range walletFiles { if walletFiles[j].Name() == name { size++ @@ -213,7 +213,7 @@ loop: wallets := make([]*wallet.Wallet, size) for i := 0; i < size; i++ { - letter := innerring.GlagoliticLetter(i).String() + letter := glagolitsa.LetterByIndex(i) p := filepath.Join(walletDir, letter+".json") w, err := wallet.NewWalletFromFile(p) if err != nil { diff --git a/cmd/neofs-adm/internal/modules/morph/initialize_deploy.go b/cmd/neofs-adm/internal/modules/morph/initialize_deploy.go index 889b0f6108..9e6e687426 100644 --- a/cmd/neofs-adm/internal/modules/morph/initialize_deploy.go +++ b/cmd/neofs-adm/internal/modules/morph/initialize_deploy.go @@ -27,7 +27,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neofs-contract/common" "github.com/nspcc-dev/neofs-contract/rpc/nns" - "github.com/nspcc-dev/neofs-node/pkg/innerring" + "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" "github.com/spf13/viper" ) @@ -549,7 +549,7 @@ func (c *initializeContext) getAlphabetDeployItems(i, n int) []interface{} { items[0] = false items[1] = c.Contracts[netmapContract].Hash items[2] = c.Contracts[proxyContract].Hash - items[3] = innerring.GlagoliticLetter(i).String() + items[3] = glagolitsa.LetterByIndex(i) items[4] = int64(i) items[5] = int64(n) return items diff --git a/cmd/neofs-adm/internal/modules/morph/initialize_test.go b/cmd/neofs-adm/internal/modules/morph/initialize_test.go index 6736b59681..4b4871d17b 100644 --- a/cmd/neofs-adm/internal/modules/morph/initialize_test.go +++ b/cmd/neofs-adm/internal/modules/morph/initialize_test.go @@ -12,7 +12,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/wallet" - "github.com/nspcc-dev/neofs-node/pkg/innerring" + "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" "github.com/spf13/viper" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" @@ -83,7 +83,7 @@ func generateTestData(t *testing.T, dir string, size int) { var pubs []string for i := 0; i < size; i++ { - p := filepath.Join(dir, innerring.GlagoliticLetter(i).String()+".json") + p := filepath.Join(dir, glagolitsa.LetterByIndex(i)+".json") w, err := wallet.NewWalletFromFile(p) require.NoError(t, err, "wallet doesn't exist") for _, acc := range w.Accounts { @@ -111,6 +111,6 @@ func generateTestData(t *testing.T, dir string, size int) { func setTestCredentials(v *viper.Viper, size int) { for i := 0; i < size; i++ { - v.Set("credentials."+innerring.GlagoliticLetter(i).String(), strconv.FormatUint(uint64(i), 10)) + v.Set("credentials."+glagolitsa.LetterByIndex(i), strconv.FormatUint(uint64(i), 10)) } } diff --git a/pkg/innerring/alphabet.go b/pkg/innerring/alphabet.go index be16f82328..a840b2fd0a 100644 --- a/pkg/innerring/alphabet.go +++ b/pkg/innerring/alphabet.go @@ -1,159 +1,22 @@ package innerring -import "github.com/nspcc-dev/neo-go/pkg/util" - -type GlagoliticLetter int8 - -const ( - _ GlagoliticLetter = iota - 1 - - az - buky - vedi - glagoli - dobro - yest - zhivete - dzelo - zemlja - izhe - izhei - gerv - kako - ljudi - mislete - nash - on - pokoj - rtsi - slovo - tverdo - uk - fert - kher - oht - shta - tsi - cherv - sha - yer - yeri - yerj - yat - jo - yu - smallYus - smallIotatedYus - bigYus - bigIotatedYus - fita - izhitsa - - lastLetterNum +import ( + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" ) -// String returns l in config-compatible format. -func (l GlagoliticLetter) String() string { - switch l { - default: - return "unknown" - case az: - return "az" - case buky: - return "buky" - case vedi: - return "vedi" - case glagoli: - return "glagoli" - case dobro: - return "dobro" - case yest: - return "yest" - case zhivete: - return "zhivete" - case dzelo: - return "dzelo" - case zemlja: - return "zemlja" - case izhe: - return "izhe" - case izhei: - return "izhei" - case gerv: - return "gerv" - case kako: - return "kako" - case ljudi: - return "ljudi" - case mislete: - return "mislete" - case nash: - return "nash" - case on: - return "on" - case pokoj: - return "pokoj" - case rtsi: - return "rtsi" - case slovo: - return "slovo" - case tverdo: - return "tverdo" - case uk: - return "uk" - case fert: - return "fert" - case kher: - return "kher" - case oht: - return "oht" - case shta: - return "shta" - case tsi: - return "tsi" - case cherv: - return "cherv" - case sha: - return "sha" - case yer: - return "yer" - case yeri: - return "yeri" - case yerj: - return "yerj" - case yat: - return "yat" - case jo: - return "jo" - case yu: - return "yu" - case smallYus: - return "small.yus" - case smallIotatedYus: - return "small.iotated.yus" - case bigYus: - return "big.yus" - case bigIotatedYus: - return "big.iotated.yus" - case fita: - return "fita" - case izhitsa: - return "izhitsa" - } -} - -type alphabetContracts map[GlagoliticLetter]util.Uint160 +type alphabetContracts map[int]util.Uint160 func newAlphabetContracts() alphabetContracts { - return make(map[GlagoliticLetter]util.Uint160, lastLetterNum) + return make(map[int]util.Uint160, glagolitsa.Size) } func (a alphabetContracts) GetByIndex(ind int) (util.Uint160, bool) { - if ind < 0 || ind >= int(lastLetterNum) { + if ind < 0 || ind >= glagolitsa.Size { return util.Uint160{}, false } - contract, ok := a[GlagoliticLetter(ind)] + contract, ok := a[ind] return contract, ok } @@ -162,12 +25,12 @@ func (a alphabetContracts) indexOutOfRange(ind int) bool { return ind < 0 && ind >= len(a) } -func (a alphabetContracts) iterate(f func(GlagoliticLetter, util.Uint160)) { - for letter, contract := range a { - f(letter, contract) +func (a alphabetContracts) iterate(f func(int, util.Uint160)) { + for ind, contract := range a { + f(ind, contract) } } -func (a *alphabetContracts) set(l GlagoliticLetter, h util.Uint160) { - (*a)[l] = h +func (a *alphabetContracts) set(ind int, h util.Uint160) { + (*a)[ind] = h } diff --git a/pkg/innerring/contracts.go b/pkg/innerring/contracts.go index d05b7d42e9..0a93571230 100644 --- a/pkg/innerring/contracts.go +++ b/pkg/innerring/contracts.go @@ -10,6 +10,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/neorpc" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neofs-node/pkg/morph/client" + "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" "github.com/spf13/cast" "github.com/spf13/viper" "go.uber.org/zap" @@ -88,26 +89,26 @@ func initContracts(ctx context.Context, _logger *zap.Logger, cfg *viper.Viper, m } func parseAlphabetContracts(ctx *nnsContext, _logger *zap.Logger, cfg *viper.Viper, morph *client.Client) (alphabetContracts, error) { - var num GlagoliticLetter + var num int const numConfigKey = "contracts.alphabet.amount" if cfg.IsSet(numConfigKey) { u, err := cast.ToUintE(cfg.Get(numConfigKey)) if err != nil { return nil, fmt.Errorf("invalid config '%s': %w", numConfigKey, err) } - num = GlagoliticLetter(u) + num = int(u) } else { committee, err := morph.Committee() if err != nil { return nil, fmt.Errorf("get Sidechain committee: %w", err) } - num = GlagoliticLetter(len(committee)) + num = len(committee) } alpha := newAlphabetContracts() - if num > lastLetterNum { - return nil, fmt.Errorf("amount of alphabet contracts overflows glagolitsa %d > %d", num, lastLetterNum) + if num > glagolitsa.Size { + return nil, fmt.Errorf("amount of alphabet contracts overflows glagolitsa %d > %d", num, glagolitsa.Size) } thresholdIsSet := num != 0 @@ -115,13 +116,14 @@ func parseAlphabetContracts(ctx *nnsContext, _logger *zap.Logger, cfg *viper.Vip if !thresholdIsSet { // try to read maximum alphabet contracts // if threshold has not been set manually - num = lastLetterNum + num = glagolitsa.Size } - for letter := az; letter < num; letter++ { + for ind := 0; ind < num; ind++ { + letter := glagolitsa.LetterByIndex(ind) contractHash, err := parseContract(ctx, _logger, cfg, morph, - "contracts.alphabet."+letter.String(), - client.NNSAlphabetContractName(int(letter)), + "contracts.alphabet."+letter, + client.NNSAlphabetContractName(ind), ) if err != nil { if errors.Is(err, client.ErrNNSRecordNotFound) { @@ -131,7 +133,7 @@ func parseAlphabetContracts(ctx *nnsContext, _logger *zap.Logger, cfg *viper.Vip return nil, fmt.Errorf("invalid alphabet %s contract: %w", letter, err) } - alpha.set(letter, contractHash) + alpha.set(ind, contractHash) } if thresholdIsSet && len(alpha) != int(num) { diff --git a/pkg/innerring/state.go b/pkg/innerring/state.go index a6df68fe5d..dc268a28d6 100644 --- a/pkg/innerring/state.go +++ b/pkg/innerring/state.go @@ -10,6 +10,7 @@ import ( auditClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/audit" "github.com/nspcc-dev/neofs-node/pkg/services/audit" control "github.com/nspcc-dev/neofs-node/pkg/services/control/ir" + "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" "github.com/nspcc-dev/neofs-node/pkg/util/state" "github.com/spf13/viper" "go.uber.org/zap" @@ -134,11 +135,11 @@ func (s *Server) voteForSidechainValidator(prm governance.VoteValidatorPrm) erro vubP = &vub } - s.contracts.alphabet.iterate(func(letter GlagoliticLetter, contract util.Uint160) { + s.contracts.alphabet.iterate(func(ind int, contract util.Uint160) { err := s.morphClient.NotaryInvoke(contract, 0, nonce, vubP, voteMethod, epoch, validators) if err != nil { s.log.Warn("can't invoke vote method in alphabet contract", - zap.Int8("alphabet_index", int8(letter)), + zap.Int("alphabet_index", ind), zap.Uint64("epoch", epoch), zap.String("error", err.Error())) } @@ -149,10 +150,10 @@ func (s *Server) voteForSidechainValidator(prm governance.VoteValidatorPrm) erro func (s *Server) alreadyVoted(validatorsToVote keys.PublicKeys) (bool, error) { currentValidators := make(map[keys.PublicKey]struct{}, len(s.contracts.alphabet)) - for letter, contract := range s.contracts.alphabet { + for ind, contract := range s.contracts.alphabet { validator, err := s.morphClient.AccountVote(contract) if err != nil { - return false, fmt.Errorf("receiving %s's vote: %w", letter, err) + return false, fmt.Errorf("receiving %s's vote: %w", glagolitsa.LetterByIndex(ind), err) } if validator == nil { diff --git a/pkg/morph/deploy/alphabet.go b/pkg/morph/deploy/alphabet.go new file mode 100644 index 0000000000..7a1e7004a3 --- /dev/null +++ b/pkg/morph/deploy/alphabet.go @@ -0,0 +1,97 @@ +package deploy + +import ( + "context" + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neorpc" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "go.uber.org/zap" +) + +// initAlphabetPrm groups parameters of Alphabet members initialization. +type initAlphabetPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + committee keys.PublicKeys + localAcc *wallet.Account +} + +// initAlphabet designates NeoFS Alphabet role to all committee members on the +// given Blockchain. +func initAlphabet(ctx context.Context, prm initAlphabetPrm) error { + // wrap the parent context into the context of the current function so that + // transaction wait routines do not leak + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + committeeActor, err := newCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) + } + + roleContract := rolemgmt.New(committeeActor) + txMonitor := newTransactionGroupMonitor(committeeActor) + + for ; ; prm.monitor.waitForNextBlock(ctx) { + select { + case <-ctx.Done(): + return fmt.Errorf("wait for NeoFS Alphabet role to be designated for the committee: %w", ctx.Err()) + default: + } + + prm.logger.Info("checking NeoFS Alphabet role of the committee members...") + + accsWithAlphabetRole, err := roleContract.GetDesignatedByRole(noderoles.NeoFSAlphabet, prm.monitor.currentHeight()) + if err != nil { + prm.logger.Error("failed to check role of the committee, will try again later", zap.Error(err)) + continue + } + + someoneWithoutRole := len(accsWithAlphabetRole) < len(prm.committee) + if !someoneWithoutRole { + for i := range prm.committee { + if !accsWithAlphabetRole.Contains(prm.committee[i]) { + someoneWithoutRole = true + break + } + } + } + if !someoneWithoutRole { + prm.logger.Info("all committee members have a NeoFS Alphabet role") + return nil + } + + prm.logger.Info("not all members of the committee have a NeoFS Alphabet role, designation is needed") + + if txMonitor.isPending() { + prm.logger.Info("previously sent Notary request designating NeoFS Alphabet role to the committee is still pending, will wait for the outcome") + continue + } + + mainTxID, fallbackTxID, vub, err := committeeActor.Notarize( + roleContract.DesignateAsRoleTransaction(noderoles.NeoFSAlphabet, prm.committee)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to send new Notary request designating NeoFS Alphabet role to the committee, skip") + } else { + prm.logger.Error("failed to send new Notary request designating NeoFS Alphabet role to the committee, skip", zap.Error(err)) + } + continue + } + + prm.logger.Info("Notary request designating NeoFS Alphabet role to the committee has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + txMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } +} diff --git a/pkg/morph/deploy/contracts.go b/pkg/morph/deploy/contracts.go index dcce0d988e..6e77bf81dc 100644 --- a/pkg/morph/deploy/contracts.go +++ b/pkg/morph/deploy/contracts.go @@ -1,7 +1,391 @@ package deploy +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "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/neorpc" + "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/rpcclient/nns" + "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/wallet" + "go.uber.org/zap" +) + // various common methods of the NeoFS contracts. const ( methodUpdate = "update" methodVersion = "version" ) + +// syncNeoFSContractPrm groups parameters of syncNeoFSContract. +type syncNeoFSContractPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + localAcc *wallet.Account + + // address of the NeoFS NNS contract deployed in the blockchain + nnsContract util.Uint160 + systemEmail string + + committee keys.PublicKeys + committeeGroupKey *keys.PrivateKey + + localNEF nef.File + localManifest manifest.Manifest + + // L2 domain name in domainContractAddresses TLD in the NNS + domainName string + + // if set, syncNeoFSContract attempts to deploy the contract when it's + // missing on the chain + tryDeploy bool + // is contract must be deployed by the committee + committeeDeployRequired bool + + // optional constructor of extra arguments to be passed into method deploying + // the contract. If returns both nil, no data is passed (noExtraDeployArgs can + // be used). + // + // Ignored if tryDeploy is unset. + buildExtraDeployArgs func() ([]interface{}, error) + + // constructor of extra arguments to be passed into method updating the + // contract. If returns both nil, no data is passed. + buildVersionedExtraUpdateArgs func(versionOnChain contractVersion) ([]interface{}, error) +} + +// syncNeoFSContract behaves similar to updateNNSContract but also attempts to +// deploy the contract if it is missing on the chain and tryDeploy flag is set. +// If committeeDeployRequired is set, the contract is deployed on behalf of the +// committee with NNS custom contract scope. +// +// Returns address of the on-chain contract synchronized with the record of the +// NNS domain with parameterized name. +func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint160, error) { + bLocalNEF, err := prm.localNEF.Bytes() + if err != nil { + // not really expected + return util.Uint160{}, fmt.Errorf("encode local NEF of the contract into binary: %w", err) + } + + jLocalManifest, err := json.Marshal(prm.localManifest) + if err != nil { + // not really expected + return util.Uint160{}, fmt.Errorf("encode local manifest of the contract into JSON: %w", err) + } + + localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) + if err != nil { + return util.Uint160{}, fmt.Errorf("init transaction sender from local account: %w", err) + } + + committeeActor, err := newCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee) + if err != nil { + return util.Uint160{}, fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) + } + + // wrap the parent context into the context of the current function so that + // transaction wait routines do not leak + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + setGroupInManifest(&prm.localManifest, prm.localNEF, prm.committeeGroupKey, prm.localAcc.ScriptHash()) + + var contractDeployer interface { + Sender() util.Uint160 + } + var managementContract *management.Contract + if prm.committeeDeployRequired { + deployCommitteeActor, err := newCommitteeNotaryActorWithCustomCommitteeSigner(prm.blockchain, prm.localAcc, prm.committee, func(s *transaction.Signer) { + s.Scopes = transaction.CustomContracts + s.AllowedContracts = []util.Uint160{prm.nnsContract} + }) + if err != nil { + return util.Uint160{}, fmt.Errorf("create Notary service client sending deploy transactions to be signed by the committee: %w", err) + } + + managementContract = management.New(deployCommitteeActor) + contractDeployer = deployCommitteeActor + } else { + managementContract = management.New(localActor) + contractDeployer = localActor + } + + var alreadyUpdated bool + domainNameForAddress := prm.domainName + "." + domainContractAddresses + l := prm.logger.With(zap.String("contract", prm.localManifest.Name), zap.String("domain", domainNameForAddress)) + deployTxMonitor := newTransactionGroupMonitor(localActor) + updateTxMonitor := newTransactionGroupMonitor(localActor) + registerDomainTxMonitor := newTransactionGroupMonitor(localActor) + registerTLDTxMonitor := newTransactionGroupMonitor(localActor) + setDomainRecordTxMonitor := newTransactionGroupMonitor(localActor) + + for ; ; prm.monitor.waitForNextBlock(ctx) { + select { + case <-ctx.Done(): + return util.Uint160{}, fmt.Errorf("wait for the contract synchronization: %w", ctx.Err()) + default: + } + + l.Info("reading on-chain state of the contract by NNS domain name...") + + var missingDomainName, missingDomainRecord bool + + onChainState, err := readContractOnChainStateByDomainName(prm.blockchain, prm.nnsContract, domainNameForAddress) + if err != nil { + if errors.Is(err, neorpc.ErrUnknownContract) { + l.Error("contract is recorded in the NNS but not found on the chain, will wait for a background fix") + continue + } + + missingDomainName = errors.Is(err, errMissingDomain) + if !missingDomainName { + missingDomainRecord = errors.Is(err, errMissingDomainRecord) + if !missingDomainRecord { + if errors.Is(err, errInvalidContractDomainRecord) { + l.Error("contract's domain record is invalid/unsupported, will wait for a background fix", zap.Error(err)) + } else { + l.Error("failed to read on-chain state of the contract record by NNS domain name, will try again later", zap.Error(err)) + } + continue + } + } + + l.Info("could not read on-chain state of the contract by NNS domain name, trying by pre-calculated address...") + + preCalculatedAddr := state.CreateContractHash(contractDeployer.Sender(), prm.localNEF.Checksum, prm.localManifest.Name) + + onChainState, err = prm.blockchain.GetContractStateByHash(preCalculatedAddr) + if err != nil { + if !errors.Is(err, neorpc.ErrUnknownContract) { + l.Error("failed to read on-chain state of the contract by pre-calculated address, will try again later", + zap.Stringer("address", preCalculatedAddr), zap.Error(err)) + continue + } + + onChainState = nil // for condition below, GetContractStateByHash may return empty + } + } + + if onChainState == nil { + // according to instructions above, we get here when contract is missing on the chain + if !prm.tryDeploy { + l.Info("contract is missing on the chain but attempts to deploy are disabled, will wait for background deployment") + continue + } + + l.Info("contract is missing on the chain, deployment needed") + + if deployTxMonitor.isPending() { + l.Info("previously sent transaction deploying the contract is still pending, will wait for the outcome") + continue + } + + extraDeployArgs, err := prm.buildExtraDeployArgs() + if err != nil { + l.Info("failed to prepare extra deployment arguments, will try again later", zap.Error(err)) + continue + } + + // just to definitely avoid mutation + nefCp := prm.localNEF + manifestCp := prm.localManifest + + if prm.committeeDeployRequired { + l.Info("contract requires committee witness for deployment, sending Notary request...") + + mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(managementContract.DeployTransaction(&nefCp, &manifestCp, extraDeployArgs)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + l.Info("insufficient Notary balance to deploy the contract, will try again later") + } else { + l.Error("failed to send Notary request deploying the contract, will try again later", zap.Error(err)) + } + continue + } + + l.Info("Notary request deploying the contract has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + deployTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + + continue + } + + l.Info("contract does not require committee witness for deployment, sending simple transaction...") + + txID, vub, err := managementContract.Deploy(&nefCp, &manifestCp, extraDeployArgs) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + l.Info("not enough GAS to deploy the contract, will try again later") + } else { + l.Error("failed to send transaction deploying the contract, will try again later", zap.Error(err)) + } + continue + } + + l.Info("transaction deploying the contract has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub), + ) + + deployTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + + continue + } + + if alreadyUpdated { + if !missingDomainName && !missingDomainRecord { + return onChainState.Hash, nil + } + } else { + versionOnChain, err := readContractOnChainVersion(prm.blockchain, onChainState.Hash) + if err != nil { + l.Error("failed to read on-chain version of the contract, will try again later", zap.Error(err)) + continue + } + + extraUpdateArgs, err := prm.buildVersionedExtraUpdateArgs(versionOnChain) + if err != nil { + l.Error("failed to prepare build extra arguments for the contract update, will try again later", + zap.Stringer("on-chain version", versionOnChain), zap.Error(err)) + continue + } + + tx, err := committeeActor.MakeCall(onChainState.Hash, methodUpdate, + bLocalNEF, jLocalManifest, extraUpdateArgs) + if err != nil { + if isErrContractAlreadyUpdated(err) { + l.Info("the contract is unchanged or has already been updated") + if !missingDomainName && !missingDomainRecord { + return onChainState.Hash, nil + } + alreadyUpdated = true + } else { + l.Error("failed to make transaction updating the contract, will try again later", zap.Error(err)) + } + continue + } + + if updateTxMonitor.isPending() { + l.Info("previously sent Notary request updating the contract is still pending, will wait for the outcome") + continue + } + + l.Info("sending new Notary request updating the contract...") + + mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(tx, nil) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + l.Info("insufficient Notary balance to update the contract, will try again later") + } else { + l.Error("failed to send Notary request updating the contract, will try again later", zap.Error(err)) + } + continue + } + + l.Info("Notary request updating the contract has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + updateTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + + continue + } + + if missingDomainName { + l.Info("NNS domain is missing, registration is needed") + + if registerDomainTxMonitor.isPending() { + l.Info("previously sent transaction registering domain in the NNS is still pending, will wait for the outcome") + continue + } + + l.Info("sending new transaction registering domain in the NNS...") + + txID, vub, err := localActor.SendCall(prm.nnsContract, methodNNSRegister, + domainNameForAddress, localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) + if err != nil { + switch { + default: + l.Error("failed to send transaction registering domain in the NNS, will try again later", zap.Error(err)) + case errors.Is(err, neorpc.ErrInsufficientFunds): + l.Info("not enough GAS to register domain in the NNS, will try again later") + case isErrTLDNotFound(err): + l.Info("missing TLD, need registration") + + if registerTLDTxMonitor.isPending() { + l.Info("previously sent Notary request registering TLD in the NNS is still pending, will wait for the outcome") + continue + } + + l.Info("sending new Notary registering TLD in the NNS...") + + mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(committeeActor.MakeCall(prm.nnsContract, methodNNSRegisterTLD, + domainContractAddresses, prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + l.Info("insufficient Notary balance to register TLD in the NNS, will try again later") + } else { + l.Error("failed to send Notary request registering TLD in the NNS, will try again later", zap.Error(err)) + } + continue + } + + l.Info("Notary request registering TLD in the NNS has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + registerTLDTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } + continue + } + + l.Info("transaction registering domain in the NNS has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub), + ) + + registerDomainTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + + continue + } + + // we come here only when missingDomainRecord is true + l.Info("missing domain record in the NNS, needed to be set") + + if setDomainRecordTxMonitor.isPending() { + l.Info("previously sent transaction setting domain record in the NNS is still pending, will wait for the outcome") + continue + } + + l.Info("sending new transaction setting domain record in the NNS...") + + txID, vub, err := localActor.SendCall(prm.nnsContract, methodNNSAddRecord, + domainNameForAddress, int64(nns.TXT), onChainState.Hash.StringLE()) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + l.Info("not enough GAS to set domain record in the NNS, will try again later") + } else { + l.Error("failed to send transaction setting domain record in the NNS, will try again later", zap.Error(err)) + } + continue + } + + l.Info("transaction setting domain record in the NNS has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub), + ) + + setDomainRecordTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + } +} diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index 09328a5811..ba1d5732ac 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -5,7 +5,9 @@ import ( "context" "errors" "fmt" + "math/big" "sort" + "strconv" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/state" @@ -15,7 +17,11 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" "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/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap" + "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" "go.uber.org/zap" ) @@ -35,6 +41,10 @@ type Blockchain interface { // requested contract is missing. GetContractStateByID(id int32) (*state.Contract, error) + // GetContractStateByHash is similar to GetContractStateByID but accepts address. + // GetContractStateByHash may return non-nil state.Contract along with an error. + GetContractStateByHash(util.Uint160) (*state.Contract, error) + // ReceiveBlocks starts background process that forwards new blocks of the // blockchain to the provided channel. The process handles all new blocks when // ReceiveBlocks is called with nil filter. Returns unique identifier to be used @@ -72,6 +82,47 @@ type NNSPrm struct { SystemEmail string } +// AlphabetContractPrm groups deployment parameters of the NeoFS Alphabet contract. +type AlphabetContractPrm struct { + Common CommonDeployPrm +} + +// AuditContractPrm groups deployment parameters of the NeoFS Audit contract. +type AuditContractPrm struct { + Common CommonDeployPrm +} + +// BalanceContractPrm groups deployment parameters of the NeoFS Balance contract. +type BalanceContractPrm struct { + Common CommonDeployPrm +} + +// ContainerContractPrm groups deployment parameters of the Container contract. +type ContainerContractPrm struct { + Common CommonDeployPrm +} + +// NeoFSIDContractPrm groups deployment parameters of the NeoFS ID contract. +type NeoFSIDContractPrm struct { + Common CommonDeployPrm +} + +// NetmapContractPrm groups deployment parameters of the Netmap contract. +type NetmapContractPrm struct { + Common CommonDeployPrm + Config netmap.NetworkConfiguration +} + +// ProxyContractPrm groups deployment parameters of the NeoFS Proxy contract. +type ProxyContractPrm struct { + Common CommonDeployPrm +} + +// ReputationContractPrm groups deployment parameters of the NeoFS Reputation contract. +type ReputationContractPrm struct { + Common CommonDeployPrm +} + // Prm groups all parameters of the NeoFS Sidechain deployment procedure. type Prm struct { // Writes progress into the log. @@ -87,6 +138,15 @@ type Prm struct { KeyStorage KeyStorage NNS NNSPrm + + AlphabetContract AlphabetContractPrm + AuditContract AuditContractPrm + BalanceContract BalanceContractPrm + ContainerContract ContainerContractPrm + NeoFSIDContract NeoFSIDContractPrm + NetmapContract NetmapContractPrm + ProxyContract ProxyContractPrm + ReputationContract ReputationContractPrm } // Deploy initializes Neo network represented by given Prm.Blockchain as NeoFS @@ -101,8 +161,9 @@ type Prm struct { // 1. NNS contract deployment // 2. launch of a notary service for the committee // 3. committee group initialization -// 4. deployment/update of the NeoFS system contracts (currently only NNS) -// 5. deployment of custom contracts +// 4. Alphabet initialization +// 5. deployment/update of the NeoFS system contracts +// 6. deployment of custom contracts (currently not supported) // // See project documentation for details. func Deploy(ctx context.Context, prm Prm) error { @@ -236,6 +297,21 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("committee group successfully initialized", zap.Stringer("public key", committeeGroupKey.PublicKey())) + prm.Logger.Info("initializing NeoFS Alphabet...") + + err = initAlphabet(ctx, initAlphabetPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + committee: committee, + localAcc: prm.LocalAccount, + }) + if err != nil { + return fmt.Errorf("init NeoFS Alphabet: %w", err) + } + + prm.Logger.Info("NeoFS Alphabet successfully initialized") + prm.Logger.Info("updating on-chain NNS contract...") err = updateNNSContract(ctx, updateNNSContractPrm{ @@ -256,9 +332,309 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("on-chain NNS contract successfully updated") - // TODO: deploy/update other contracts + syncPrm := syncNeoFSContractPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + localAcc: prm.LocalAccount, + nnsContract: nnsOnChainAddress, + systemEmail: prm.NNS.SystemEmail, + committee: committee, + committeeGroupKey: committeeGroupKey, + } + + localAccLeads := localAccCommitteeIndex == 0 + + var notaryDisabledExtraUpdateArg bool + + // then go dependent contracts. Things may become better/easier after + // https://github.com/nspcc-dev/neofs-contract/issues/325 + resolveContractAddressDynamically := func(commonPrm CommonDeployPrm, contractDomain string) (util.Uint160, error) { + domain := calculateContractAddressDomain(contractDomain) + onChainState, err := readContractOnChainStateByDomainName(prm.Blockchain, nnsOnChainAddress, domain) + if err != nil { + // contract may be deployed but not registered in the NNS yet + if localAccLeads && (errors.Is(err, errMissingDomain) || errors.Is(err, errMissingDomainRecord)) { + return state.CreateContractHash(prm.LocalAccount.ScriptHash(), commonPrm.NEF.Checksum, commonPrm.Manifest.Name), nil + } + return util.Uint160{}, fmt.Errorf("failed to read on-chain state of the contract by NNS domain '%s': %w", domain, err) + } + return onChainState.Hash, nil + } + + // Alphabet + syncPrm.localNEF = prm.AlphabetContract.Common.NEF + syncPrm.localManifest = prm.AlphabetContract.Common.Manifest + syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { + if versionOnChain.equals(0, 17, 0) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil + } + return nil, nil + } + + for ind := 0; ind < len(committee) && ind < glagolitsa.Size; ind++ { + syncPrm.tryDeploy = ind == localAccCommitteeIndex // each member deploys its own Alphabet contract + syncPrm.domainName = calculateAlphabetContractAddressDomain(ind) + syncPrm.buildExtraDeployArgs = func() ([]interface{}, error) { + netmapContractAddress, err := resolveContractAddressDynamically(prm.NetmapContract.Common, domainNetmap) + if err != nil { + return nil, fmt.Errorf("resolve address of the Netmap contract: %w", err) + } + proxyContractAddress, err := resolveContractAddressDynamically(prm.ProxyContract.Common, domainProxy) + if err != nil { + return nil, fmt.Errorf("resolve address of the Proxy contract: %w", err) + } + return []interface{}{ + notaryDisabledExtraUpdateArg, + netmapContractAddress, + proxyContractAddress, + glagolitsa.LetterByIndex(ind), + ind, + len(committee), + }, nil + } + + prm.Logger.Info("synchronizing Alphabet contract with the chain...", zap.Int("index", ind)) + + alphabetContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Alphabet contract #%d with the chain: %w", ind, err) + } + + prm.Logger.Info("Alphabet contract successfully synchronized", + zap.Int("index", ind), zap.Stringer("address", alphabetContractAddress)) + } + + // we attempt to deploy contracts other than Alphabet ones by single committee + // member (1st for simplicity) to reduce the likelihood of contract duplication + // in the chain and better predictability of the final address (the address is a + // function from the sender of the deploying transaction). While this approach + // is centralized, we still expect any node incl. 1st one to be "healthy". + // Updates are done concurrently. + syncPrm.tryDeploy = localAccLeads + + // Audit + syncPrm.localNEF = prm.AuditContract.Common.NEF + syncPrm.localManifest = prm.AuditContract.Common.Manifest + syncPrm.domainName = domainAudit + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { + if versionOnChain.equals(0, 17, 0) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil + } + return nil, nil + } + + prm.Logger.Info("synchronizing Audit contract with the chain...") + + auditContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Audit contract with the chain: %w", err) + } + + prm.Logger.Info("Audit contract successfully synchronized", zap.Stringer("address", auditContractAddress)) + + // Balance + syncPrm.localNEF = prm.BalanceContract.Common.NEF + syncPrm.localManifest = prm.BalanceContract.Common.Manifest + syncPrm.domainName = domainBalance + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { + if versionOnChain.equals(0, 17, 0) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil + } + return nil, nil + } + + prm.Logger.Info("synchronizing Balance contract with the chain...") + + balanceContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Balance contract with the chain: %w", err) + } + + prm.Logger.Info("Balance contract successfully synchronized", zap.Stringer("address", balanceContractAddress)) + + // Container + syncPrm.localNEF = prm.ContainerContract.Common.NEF + syncPrm.localManifest = prm.ContainerContract.Common.Manifest + syncPrm.domainName = domainContainer + syncPrm.committeeDeployRequired = true + syncPrm.buildExtraDeployArgs = func() ([]interface{}, error) { + netmapContractAddress, err := resolveContractAddressDynamically(prm.NetmapContract.Common, domainNetmap) + if err != nil { + return nil, fmt.Errorf("resolve address of the Netmap contract: %w", err) + } + balanceContractAddress, err := resolveContractAddressDynamically(prm.BalanceContract.Common, domainBalance) + if err != nil { + return nil, fmt.Errorf("resolve address of the Balance contract: %w", err) + } + neoFSIDContractAddress, err := resolveContractAddressDynamically(prm.NeoFSIDContract.Common, domainNeoFSID) + if err != nil { + return nil, fmt.Errorf("resolve address of the NeoFSID contract: %w", err) + } + return []interface{}{ + notaryDisabledExtraUpdateArg, + netmapContractAddress, + balanceContractAddress, + neoFSIDContractAddress, + nnsOnChainAddress, + domainContainers, + }, nil + } + syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { + if versionOnChain.equals(0, 17, 0) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil + } + return nil, nil + } + + prm.Logger.Info("synchronizing Container contract with the chain...") + + containerContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Container contract with the chain: %w", err) + } + + prm.Logger.Info("Container contract successfully synchronized", zap.Stringer("address", containerContractAddress)) + + syncPrm.committeeDeployRequired = false + + // NeoFSID + syncPrm.localNEF = prm.NeoFSIDContract.Common.NEF + syncPrm.localManifest = prm.NeoFSIDContract.Common.Manifest + syncPrm.domainName = domainNeoFSID + syncPrm.buildExtraDeployArgs = func() ([]interface{}, error) { + netmapContractAddress, err := resolveContractAddressDynamically(prm.NetmapContract.Common, domainNetmap) + if err != nil { + return nil, fmt.Errorf("resolve address of the Netmap contract: %w", err) + } + return []interface{}{ + notaryDisabledExtraUpdateArg, + netmapContractAddress, + }, nil + } + syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { + if versionOnChain.equals(0, 17, 0) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil + } + return nil, nil + } + + prm.Logger.Info("synchronizing NeoFSID contract with the chain...") + + neoFSIDContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync NeoFSID contract with the chain: %w", err) + } + + prm.Logger.Info("NeoFSID contract successfully synchronized", zap.Stringer("address", neoFSIDContractAddress)) + + // Netmap + netConfig := []interface{}{ + []byte(netmap.MaxObjectSizeConfig), encodeUintConfig(prm.NetmapContract.Config.MaxObjectSize), + []byte(netmap.BasicIncomeRateConfig), encodeUintConfig(prm.NetmapContract.Config.StoragePrice), + []byte(netmap.AuditFeeConfig), encodeUintConfig(prm.NetmapContract.Config.AuditFee), + []byte(netmap.EpochDurationConfig), encodeUintConfig(prm.NetmapContract.Config.EpochDuration), + []byte(netmap.ContainerFeeConfig), encodeUintConfig(prm.NetmapContract.Config.ContainerFee), + []byte(netmap.ContainerAliasFeeConfig), encodeUintConfig(prm.NetmapContract.Config.ContainerAliasFee), + []byte(netmap.EigenTrustIterationsConfig), encodeUintConfig(prm.NetmapContract.Config.EigenTrustIterations), + []byte(netmap.EigenTrustAlphaConfig), encodeFloatConfig(prm.NetmapContract.Config.EigenTrustAlpha), + []byte(netmap.InnerRingCandidateFeeConfig), encodeUintConfig(prm.NetmapContract.Config.IRCandidateFee), + []byte(netmap.WithdrawFeeConfig), encodeUintConfig(prm.NetmapContract.Config.WithdrawalFee), + []byte(netmap.HomomorphicHashingDisabledKey), encodeBoolConfig(prm.NetmapContract.Config.HomomorphicHashingDisabled), + []byte(netmap.MaintenanceModeAllowedConfig), encodeBoolConfig(prm.NetmapContract.Config.MaintenanceModeAllowed), + } + + for i := range prm.NetmapContract.Config.Raw { + netConfig = append(netConfig, []byte(prm.NetmapContract.Config.Raw[i].Name), prm.NetmapContract.Config.Raw[i].Value) + } + + syncPrm.localNEF = prm.NetmapContract.Common.NEF + syncPrm.localManifest = prm.NetmapContract.Common.Manifest + syncPrm.domainName = domainNetmap + syncPrm.buildExtraDeployArgs = func() ([]interface{}, error) { + balanceContractAddress, err := resolveContractAddressDynamically(prm.BalanceContract.Common, domainBalance) + if err != nil { + return nil, fmt.Errorf("resolve address of the Balance contract: %w", err) + } + containerContractAddress, err := resolveContractAddressDynamically(prm.ContainerContract.Common, domainContainer) + if err != nil { + return nil, fmt.Errorf("resolve address of the Container contract: %w", err) + } + return []interface{}{ + notaryDisabledExtraUpdateArg, + balanceContractAddress, + containerContractAddress, + []interface{}(nil), // keys, currently unused + netConfig, + }, nil + } + syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { + return []interface{}{notaryDisabledExtraUpdateArg, util.Uint160{}, util.Uint160{}, []interface{}(nil), []interface{}(nil)}, nil + } + + prm.Logger.Info("synchronizing Netmap contract with the chain...") + + netmapContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Netmap contract with the chain: %w", err) + } + + prm.Logger.Info("Netmap contract successfully synchronized", zap.Stringer("address", netmapContractAddress)) + + // Proxy + syncPrm.localNEF = prm.ProxyContract.Common.NEF + syncPrm.localManifest = prm.ProxyContract.Common.Manifest + syncPrm.domainName = domainProxy + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + syncPrm.buildVersionedExtraUpdateArgs = noExtraUpdateArgs + + prm.Logger.Info("synchronizing Proxy contract with the chain...") + + proxyContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Proxy contract with the chain: %w", err) + } + + prm.Logger.Info("Proxy contract successfully synchronized", zap.Stringer("address", proxyContractAddress)) + + // Reputation + syncPrm.localNEF = prm.ReputationContract.Common.NEF + syncPrm.localManifest = prm.ReputationContract.Common.Manifest + syncPrm.domainName = domainReputation + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { + if versionOnChain.equals(0, 17, 0) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil + } + return nil, nil + } + + prm.Logger.Info("synchronizing Reputation contract with the chain...") + + reputationContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Reputation contract with the chain: %w", err) + } + + prm.Logger.Info("Reputation contract successfully synchronized", zap.Stringer("address", reputationContractAddress)) return nil } func noExtraUpdateArgs(contractVersion) ([]interface{}, error) { return nil, nil } + +func noExtraDeployArgs() ([]interface{}, error) { return nil, nil } + +func encodeUintConfig(v uint64) []byte { + return stackitem.NewBigInteger(new(big.Int).SetUint64(v)).Bytes() +} + +func encodeFloatConfig(v float64) []byte { + return []byte(strconv.FormatFloat(v, 'f', -1, 64)) +} + +func encodeBoolConfig(v bool) []byte { + return stackitem.NewBool(v).Bytes() +} diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index f8c95abbb5..744cbc7e10 100644 --- a/pkg/morph/deploy/nns.go +++ b/pkg/morph/deploy/nns.go @@ -29,8 +29,26 @@ const ( domainDesignateNotaryPrefix = "designate-committee-notary-" domainDesignateNotaryTx = domainDesignateNotaryPrefix + "tx." + domainBootstrap domainContractAddresses = "neofs" + domainContainers = "container" + + domainAlphabetFmt = "alphabet%d" + domainAudit = "audit" + domainBalance = "balance" + domainContainer = "container" + domainNeoFSID = "neofsid" + domainNetmap = "netmap" + domainProxy = "proxy" + domainReputation = "reputation" ) +func calculateAlphabetContractAddressDomain(index int) string { + return fmt.Sprintf(domainAlphabetFmt, index) +} + +func calculateContractAddressDomain(contractDomain string) string { + return contractDomain + "." + domainContractAddresses +} + func designateNotarySignatureDomainForMember(memberIndex int) string { return fmt.Sprintf("%s%d.%s", domainDesignateNotaryPrefix, memberIndex, domainBootstrap) } @@ -41,10 +59,11 @@ func committeeGroupDomainForMember(memberIndex int) string { // various methods of the NeoFS NNS contract. const ( - methodNNSRegister = "register" - methodNNSResolve = "resolve" - methodNNSAddRecord = "addRecord" - methodNNSSetRecord = "setRecord" + methodNNSRegister = "register" + methodNNSRegisterTLD = "registerTLD" + methodNNSResolve = "resolve" + methodNNSAddRecord = "addRecord" + methodNNSSetRecord = "setRecord" ) // default NNS domain settings. See DNS specification and also @@ -274,7 +293,7 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) } - localVersion, err := readContractLocalVersion(prm.blockchain, prm.localNEF, prm.localManifest) + localVersion, err := readContractLocalVersion(prm.blockchain, prm.committee, prm.localNEF, prm.localManifest) if err != nil { return fmt.Errorf("read version of the local NNS contract: %w", err) } diff --git a/pkg/morph/deploy/notary.go b/pkg/morph/deploy/notary.go index 9e255d7c9b..ec61b4df87 100644 --- a/pkg/morph/deploy/notary.go +++ b/pkg/morph/deploy/notary.go @@ -842,10 +842,28 @@ func makeUnsignedDesignateCommitteeNotaryTx(roleContract *rolemgmt.Contract, com return tx, nil } -// newCommitteeNotaryActor returns notary.Actor that builds and sends Notary -// service requests witnessed by the specified committee members to the provided -// Blockchain. Given local account pays for transactions. +// newCommitteeNotaryActor calls newCommitteeNotaryActorWithScope with transaction.CalledByEntry +// witness scope appropriate for most transactions. func newCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee keys.PublicKeys) (*notary.Actor, error) { + return newCommitteeNotaryActorWithCustomCommitteeSigner(b, localAcc, committee, func(s *transaction.Signer) { + s.Scopes = transaction.CalledByEntry + }) +} + +// returns notary.Actor builds and sends Notary service requests witnessed by +// the specified committee members to the provided Blockchain. Composed main +// transactions will have specified witness scope. Given local account pays for +// transactions. +// +// Transaction signer callback allows to specify committee signer (e.g. tune +// witness scope). Instance passed to it has Account set to multi-signature +// account for the parameterized committee. +func newCommitteeNotaryActorWithCustomCommitteeSigner( + b Blockchain, + localAcc *wallet.Account, + committee keys.PublicKeys, + fCommitteeSigner func(*transaction.Signer), +) (*notary.Actor, error) { committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(committee)) committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(localAcc.PrivateKey()) @@ -854,6 +872,15 @@ func newCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee k return nil, fmt.Errorf("compose committee multi-signature account: %w", err) } + committeeSignerAcc := actor.SignerAccount{ + Signer: transaction.Signer{ + Account: committeeMultiSigAcc.ScriptHash(), + }, + Account: committeeMultiSigAcc, + } + + fCommitteeSigner(&committeeSignerAcc.Signer) + return notary.NewActor(b, []actor.SignerAccount{ { Signer: transaction.Signer{ @@ -862,13 +889,7 @@ func newCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee k }, Account: localAcc, }, - { - Signer: transaction.Signer{ - Account: committeeMultiSigAcc.ScriptHash(), - Scopes: transaction.CalledByEntry, - }, - Account: committeeMultiSigAcc, - }, + committeeSignerAcc, }, localAcc) } diff --git a/pkg/morph/deploy/util.go b/pkg/morph/deploy/util.go index 85e31b9678..027e9d5633 100644 --- a/pkg/morph/deploy/util.go +++ b/pkg/morph/deploy/util.go @@ -15,6 +15,7 @@ import ( "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/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neorpc" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" @@ -36,6 +37,10 @@ func isErrContractAlreadyUpdated(err error) bool { return strings.Contains(err.Error(), common.ErrAlreadyUpdated) } +func isErrTLDNotFound(err error) bool { + return strings.Contains(err.Error(), "TLD not found") +} + func setGroupInManifest(manif *manifest.Manifest, nefFile nef.File, groupPrivKey *keys.PrivateKey, deployerAcc util.Uint160) { contractAddress := state.CreateContractHash(deployerAcc, nefFile.Checksum, manif.Name) sig := groupPrivKey.Sign(contractAddress.BytesBE()) @@ -333,3 +338,35 @@ func (x *transactionGroupMonitor) trackPendingTransactionsAsync(ctx context.Cont cancel() }() } + +var errInvalidContractDomainRecord = errors.New("invalid contract domain record") + +// readContractOnChainStateByDomainName reads address state of contract deployed +// in the given Blockchain and recorded in the NNS with the specified domain +// name. Returns errMissingDomain if domain doesn't exist. Returns +// errMissingDomainRecord if domain has no records. Returns +// errInvalidContractDomainRecord if domain record has invalid/unsupported +// format. Returns [neorpc.ErrUnknownContract] if contract is recorded in the NNS but +// missing in the Blockchain. +func readContractOnChainStateByDomainName(b Blockchain, nnsContract util.Uint160, domainName string) (*state.Contract, error) { + rec, err := lookupNNSDomainRecord(invoker.New(b, nil), nnsContract, domainName) + if err != nil { + return nil, err + } + + // historically two formats may occur + addr, err := util.Uint160DecodeStringLE(rec) + if err != nil { + addr, err = address.StringToUint160(rec) + if err != nil { + return nil, fmt.Errorf("%w: domain record '%s' neither NEO address nor little-endian hex-encoded script hash", errInvalidContractDomainRecord, rec) + } + } + + res, err := b.GetContractStateByHash(addr) + if err != nil { + return nil, fmt.Errorf("get contract by address=%s: %w", addr, err) + } + + return res, nil +} diff --git a/pkg/util/glagolitsa/glagolitsa.go b/pkg/util/glagolitsa/glagolitsa.go new file mode 100644 index 0000000000..be04fb8856 --- /dev/null +++ b/pkg/util/glagolitsa/glagolitsa.go @@ -0,0 +1,56 @@ +// Package glagolitsa provides Glagolitic script for NeoFS Alphabet. +package glagolitsa + +var script = []string{ + "az", + "buky", + "vedi", + "glagoli", + "dobro", + "yest", + "zhivete", + "dzelo", + "zemlja", + "izhe", + "izhei", + "gerv", + "kako", + "ljudi", + "mislete", + "nash", + "on", + "pokoj", + "rtsi", + "slovo", + "tverdo", + "uk", + "fert", + "kher", + "oht", + "shta", + "tsi", + "cherv", + "sha", + "yer", + "yeri", + "yerj", + "yat", + "jo", + "yu", + "small.yus", + "small.iotated.yus", + "big.yus", + "big.iotated.yus", + "fita", + "izhitsa", +} + +const Size = 41 + +// LetterByIndex returns string representation of Glagolitic letter compatible +// with NeoFS Alphabet contract by index. Index must be in range [0, Size). +// +// Track https://github.com/nspcc-dev/neofs-node/issues/2431 +func LetterByIndex(ind int) string { + return script[ind] +} diff --git a/pkg/util/glagolitsa/glagolitsa_test.go b/pkg/util/glagolitsa/glagolitsa_test.go new file mode 100644 index 0000000000..3e650380b2 --- /dev/null +++ b/pkg/util/glagolitsa/glagolitsa_test.go @@ -0,0 +1,60 @@ +package glagolitsa_test + +import ( + "testing" + + "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" + "github.com/stretchr/testify/require" +) + +func TestLetterByIndex(t *testing.T) { + require.Panics(t, func() { glagolitsa.LetterByIndex(-1) }) + require.Panics(t, func() { glagolitsa.LetterByIndex(glagolitsa.Size) }) + require.Panics(t, func() { glagolitsa.LetterByIndex(glagolitsa.Size + 1) }) + + for i, letter := range []string{ + "az", + "buky", + "vedi", + "glagoli", + "dobro", + "yest", + "zhivete", + "dzelo", + "zemlja", + "izhe", + "izhei", + "gerv", + "kako", + "ljudi", + "mislete", + "nash", + "on", + "pokoj", + "rtsi", + "slovo", + "tverdo", + "uk", + "fert", + "kher", + "oht", + "shta", + "tsi", + "cherv", + "sha", + "yer", + "yeri", + "yerj", + "yat", + "jo", + "yu", + "small.yus", + "small.iotated.yus", + "big.yus", + "big.iotated.yus", + "fita", + "izhitsa", + } { + require.Equal(t, letter, glagolitsa.LetterByIndex(i)) + } +} From daa2af15ad9bb27bc1c9982593073562cb6f6831 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 18 Jul 2023 18:26:27 +0400 Subject: [PATCH 04/22] sidechain/deploy: Do not repeat already failed actions within one block Some actions of committee group distribution routine are similar for all domains, so it's redundant to repeat same failed ops for other domains. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/group.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/morph/deploy/group.go b/pkg/morph/deploy/group.go index 699cce1805..49bfd1582a 100644 --- a/pkg/morph/deploy/group.go +++ b/pkg/morph/deploy/group.go @@ -176,7 +176,7 @@ func initShareCommitteeGroupKeyAsLeaderTick(ctx context.Context, prm initCommitt } else { l.Error("failed to send transaction registering domain in the NNS, will try again later", zap.Error(err)) } - continue + return } l.Info("transaction registering domain in the NNS has been successfully sent, will wait for the outcome", @@ -187,7 +187,7 @@ func initShareCommitteeGroupKeyAsLeaderTick(ctx context.Context, prm initCommitt continue } else if !errors.Is(err, errMissingDomainRecord) { l.Error("failed to lookup NNS domain record, will try again later", zap.Error(err)) - continue + return } l.Info("missing record of the NNS domain, needed to be set") @@ -222,7 +222,7 @@ func initShareCommitteeGroupKeyAsLeaderTick(ctx context.Context, prm initCommitt } else { l.Error("failed to send transaction setting NNS domain record, will try again later", zap.Error(err)) } - continue + return } l.Info("transaction setting NNS domain record has been successfully sent, will wait for the outcome", From 6e29fda2975d09def226392ddf4565cc4e359418 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 17 Aug 2023 12:56:32 +0400 Subject: [PATCH 05/22] sidechain/deploy: Perform active update attempt for the NNS contract Drop additional pre-checks of contract checksums and versions, and perform active `update` method call which can throw `already updated` exception. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/nns.go | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index 744cbc7e10..66e6ad22f6 100644 --- a/pkg/morph/deploy/nns.go +++ b/pkg/morph/deploy/nns.go @@ -293,11 +293,6 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) } - localVersion, err := readContractLocalVersion(prm.blockchain, prm.committee, prm.localNEF, prm.localManifest) - if err != nil { - return fmt.Errorf("read version of the local NNS contract: %w", err) - } - // wrap the parent context into the context of the current function so that // transaction wait routines do not leak ctx, cancel := context.WithCancel(ctx) @@ -323,31 +318,12 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { return errors.New("missing required NNS contract on the chain") } - if nnsOnChainState.NEF.Checksum == prm.localNEF.Checksum { - // manifests may differ, but currently we should bump internal contract version - // (i.e. change NEF) to make such updates. Right now they are not supported due - // to dubious practical need - // Track https://github.com/nspcc-dev/neofs-contract/issues/340 - prm.logger.Info("same local and on-chain checksums of the NNS contract NEF, update is not needed") - return nil - } - - prm.logger.Info("NEF checksums of the on-chain and local NNS contracts differ, need an update") - versionOnChain, err := readContractOnChainVersion(prm.blockchain, nnsOnChainState.Hash) if err != nil { prm.logger.Error("failed to read on-chain version of the NNS contract, will try again later", zap.Error(err)) continue } - if v := localVersion.cmp(versionOnChain); v == -1 { - prm.logger.Info("local contract version is < than the on-chain one, update is not needed", - zap.Stringer("local", localVersion), zap.Stringer("on-chain", versionOnChain)) - return nil - } else if v == 0 { - return fmt.Errorf("local and on-chain contracts have different NEF checksums but same version '%s'", versionOnChain) - } - extraUpdateArgs, err := prm.buildVersionedExtraUpdateArgs(versionOnChain) if err != nil { prm.logger.Error("failed to prepare build extra arguments for NNS contract update, will try again later", @@ -364,9 +340,7 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { bLocalNEF, jLocalManifest, extraUpdateArgs) if err != nil { if isErrContractAlreadyUpdated(err) { - // note that we can come here only if local version is > than the on-chain one - // (compared above) - prm.logger.Info("NNS contract has already been updated, skip") + prm.logger.Info("NNS contract is unchanged or has already been updated, skip") return nil } From c53628bebadfebcdfd349c14cdb365d88b843af0 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 17 Aug 2023 19:09:30 +0400 Subject: [PATCH 06/22] sidechain/deploy: Make Proxy contract to pay for update transactions Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/contracts.go | 40 +++++++++++++++++- pkg/morph/deploy/deploy.go | 79 +++++++++++++++++++---------------- pkg/morph/deploy/nns.go | 8 +++- pkg/morph/deploy/notary.go | 58 ++++++++++++++++++------- 4 files changed, 130 insertions(+), 55 deletions(-) diff --git a/pkg/morph/deploy/contracts.go b/pkg/morph/deploy/contracts.go index 6e77bf81dc..267a86a3c1 100644 --- a/pkg/morph/deploy/contracts.go +++ b/pkg/morph/deploy/contracts.go @@ -13,6 +13,7 @@ import ( "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/rpcclient/nns" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" "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" @@ -66,6 +67,14 @@ type syncNeoFSContractPrm struct { // constructor of extra arguments to be passed into method updating the // contract. If returns both nil, no data is passed. buildVersionedExtraUpdateArgs func(versionOnChain contractVersion) ([]interface{}, error) + + // address of the Proxy contract deployed in the blockchain. The contract + // pays for update transactions. + proxyContract util.Uint160 + // set when syncNeoFSContractPrm relates to Proxy contract. In this case + // proxyContract field is unused because address is dynamically resolved within + // syncNeoFSContract. + isProxy bool } // syncNeoFSContract behaves similar to updateNNSContract but also attempts to @@ -98,6 +107,26 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint return util.Uint160{}, fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) } + var proxyCommitteeActor *notary.Actor + + initProxyCommitteeActor := func(proxyContract util.Uint160) error { + var err error + proxyCommitteeActor, err = newProxyCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee, proxyContract) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee and paid by Proxy contract: %w", err) + } + return nil + } + + if !prm.isProxy { + // otherwise, we dynamically receive Proxy contract address below and construct + // proxyCommitteeActor after + err = initProxyCommitteeActor(prm.proxyContract) + if err != nil { + return util.Uint160{}, err + } + } + // wrap the parent context into the context of the current function so that // transaction wait routines do not leak ctx, cancel := context.WithCancel(ctx) @@ -265,7 +294,14 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint continue } - tx, err := committeeActor.MakeCall(onChainState.Hash, methodUpdate, + if prm.isProxy && proxyCommitteeActor == nil { + err = initProxyCommitteeActor(onChainState.Hash) + if err != nil { + return util.Uint160{}, err + } + } + + tx, err := proxyCommitteeActor.MakeCall(onChainState.Hash, methodUpdate, bLocalNEF, jLocalManifest, extraUpdateArgs) if err != nil { if isErrContractAlreadyUpdated(err) { @@ -287,7 +323,7 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint l.Info("sending new Notary request updating the contract...") - mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(tx, nil) + mainTxID, fallbackTxID, vub, err := proxyCommitteeActor.Notarize(tx, nil) if err != nil { if errors.Is(err, neorpc.ErrInsufficientFunds) { l.Info("insufficient Notary balance to update the contract, will try again later") diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index ba1d5732ac..ccf019bca7 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -312,26 +312,6 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("NeoFS Alphabet successfully initialized") - prm.Logger.Info("updating on-chain NNS contract...") - - err = updateNNSContract(ctx, updateNNSContractPrm{ - logger: prm.Logger, - blockchain: prm.Blockchain, - monitor: monitor, - localAcc: prm.LocalAccount, - localNEF: prm.NNS.Common.NEF, - localManifest: prm.NNS.Common.Manifest, - systemEmail: prm.NNS.SystemEmail, - committee: committee, - committeeGroupKey: committeeGroupKey, - buildVersionedExtraUpdateArgs: noExtraUpdateArgs, - }) - if err != nil { - return fmt.Errorf("update NNS contract on the chain: %w", err) - } - - prm.Logger.Info("on-chain NNS contract successfully updated") - syncPrm := syncNeoFSContractPrm{ logger: prm.Logger, blockchain: prm.Blockchain, @@ -362,6 +342,49 @@ func Deploy(ctx context.Context, prm Prm) error { return onChainState.Hash, nil } + // Proxy goes first. It's required for Notary service to work, and also pays for + // subsequent contract updates. + syncPrm.localNEF = prm.ProxyContract.Common.NEF + syncPrm.localManifest = prm.ProxyContract.Common.Manifest + syncPrm.domainName = domainProxy + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + syncPrm.buildVersionedExtraUpdateArgs = noExtraUpdateArgs + syncPrm.isProxy = true + + prm.Logger.Info("synchronizing Proxy contract with the chain...") + + proxyContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Proxy contract with the chain: %w", err) + } + + prm.Logger.Info("Proxy contract successfully synchronized", zap.Stringer("address", proxyContractAddress)) + + // use on-chain address of the Proxy contract to update all others + syncPrm.isProxy = false + syncPrm.proxyContract = proxyContractAddress + + prm.Logger.Info("updating on-chain NNS contract...") + + err = updateNNSContract(ctx, updateNNSContractPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + localAcc: prm.LocalAccount, + localNEF: prm.NNS.Common.NEF, + localManifest: prm.NNS.Common.Manifest, + systemEmail: prm.NNS.SystemEmail, + committee: committee, + committeeGroupKey: committeeGroupKey, + buildVersionedExtraUpdateArgs: noExtraUpdateArgs, + proxyContract: proxyContractAddress, + }) + if err != nil { + return fmt.Errorf("update NNS contract on the chain: %w", err) + } + + prm.Logger.Info("on-chain NNS contract successfully updated") + // Alphabet syncPrm.localNEF = prm.AlphabetContract.Common.NEF syncPrm.localManifest = prm.AlphabetContract.Common.Manifest @@ -583,22 +606,6 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("Netmap contract successfully synchronized", zap.Stringer("address", netmapContractAddress)) - // Proxy - syncPrm.localNEF = prm.ProxyContract.Common.NEF - syncPrm.localManifest = prm.ProxyContract.Common.Manifest - syncPrm.domainName = domainProxy - syncPrm.buildExtraDeployArgs = noExtraDeployArgs - syncPrm.buildVersionedExtraUpdateArgs = noExtraUpdateArgs - - prm.Logger.Info("synchronizing Proxy contract with the chain...") - - proxyContractAddress, err := syncNeoFSContract(ctx, syncPrm) - if err != nil { - return fmt.Errorf("sync Proxy contract with the chain: %w", err) - } - - prm.Logger.Info("Proxy contract successfully synchronized", zap.Stringer("address", proxyContractAddress)) - // Reputation syncPrm.localNEF = prm.ReputationContract.Common.NEF syncPrm.localManifest = prm.ReputationContract.Common.Manifest diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index 66e6ad22f6..5b9d5f6edf 100644 --- a/pkg/morph/deploy/nns.go +++ b/pkg/morph/deploy/nns.go @@ -264,6 +264,10 @@ type updateNNSContractPrm struct { // contract. If returns both nil, no data is passed (noExtraUpdateArgs may be // used). buildVersionedExtraUpdateArgs func(versionOnChain contractVersion) ([]interface{}, error) + + // address of the Proxy contract deployed in the blockchain. The contract + // pays for update transactions. + proxyContract util.Uint160 } // updateNNSContract synchronizes on-chain NNS contract (its presence is a @@ -288,9 +292,9 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { return fmt.Errorf("encode local manifest of the NNS contract into JSON: %w", err) } - committeeActor, err := newCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee) + committeeActor, err := newProxyCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee, prm.proxyContract) if err != nil { - return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee and paid by Proxy contract: %w", err) } // wrap the parent context into the context of the current function so that diff --git a/pkg/morph/deploy/notary.go b/pkg/morph/deploy/notary.go index ec61b4df87..f6e16dfa96 100644 --- a/pkg/morph/deploy/notary.go +++ b/pkg/morph/deploy/notary.go @@ -850,18 +850,43 @@ func newCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee k }) } -// returns notary.Actor builds and sends Notary service requests witnessed by -// the specified committee members to the provided Blockchain. Composed main -// transactions will have specified witness scope. Given local account pays for +// calls newCommitteeNotaryActorWithCustomCommitteeSignerAndPayer with local account +// set as payer. +func newCommitteeNotaryActorWithCustomCommitteeSigner( + b Blockchain, + localAcc *wallet.Account, + committee keys.PublicKeys, + fCommitteeSigner func(*transaction.Signer), +) (*notary.Actor, error) { + return _newCustomCommitteeNotaryActor(b, localAcc, committee, localAcc, fCommitteeSigner) +} + +// returns notary.Actor that builds and sends Notary service requests witnessed +// by the specified committee members to the provided Blockchain. Local account +// should be one of the committee members. Given Proxy contract pays for main // transactions. +func newProxyCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee keys.PublicKeys, proxyContract util.Uint160) (*notary.Actor, error) { + return _newCustomCommitteeNotaryActor(b, localAcc, committee, notary.FakeContractAccount(proxyContract), func(s *transaction.Signer) { + s.Scopes = transaction.CalledByEntry + }) +} + +// returns notary.Actor builds and sends Notary service requests witnessed by +// the specified committee members to the provided Blockchain. Local account +// should be one of the committee members. Specified account pays for +// main transactions. // // Transaction signer callback allows to specify committee signer (e.g. tune // witness scope). Instance passed to it has Account set to multi-signature // account for the parameterized committee. -func newCommitteeNotaryActorWithCustomCommitteeSigner( +// +// This function is presented to share common code and is expected to be called +// by helper constructors only. +func _newCustomCommitteeNotaryActor( b Blockchain, localAcc *wallet.Account, committee keys.PublicKeys, + payerAcc *wallet.Account, fCommitteeSigner func(*transaction.Signer), ) (*notary.Actor, error) { committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(committee)) @@ -884,10 +909,10 @@ func newCommitteeNotaryActorWithCustomCommitteeSigner( return notary.NewActor(b, []actor.SignerAccount{ { Signer: transaction.Signer{ - Account: localAcc.ScriptHash(), + Account: payerAcc.ScriptHash(), Scopes: transaction.None, }, - Account: localAcc, + Account: payerAcc, }, committeeSignerAcc, }, localAcc) @@ -1074,16 +1099,19 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar continue } + var payerAcc *wallet.Account + bSenderKey, ok := vm.ParseSignatureContract(mainTx.Scripts[0].VerificationScript) - if !ok { - prm.logger.Info("first verification script in main transaction of the received notary request is not a signature one, skip", zap.Error(err)) - continue - } + if ok { + senderKey, err := keys.NewPublicKeyFromBytes(bSenderKey, elliptic.P256()) + if err != nil { + prm.logger.Info("failed to decode sender's public key from first script of main transaction from the received notary request, skip", zap.Error(err)) + continue + } - senderKey, err := keys.NewPublicKeyFromBytes(bSenderKey, elliptic.P256()) - if err != nil { - prm.logger.Info("failed to decode sender's public key from first script of main transaction from the received notary request, skip", zap.Error(err)) - continue + payerAcc = notary.FakeSimpleAccount(senderKey) + } else { + payerAcc = notary.FakeContractAccount(mainTx.Signers[0].Account) } // copy transaction to avoid pointer mutation @@ -1103,7 +1131,7 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar notaryActor, err := notary.NewActor(prm.blockchain, []actor.SignerAccount{ { Signer: mainTx.Signers[0], - Account: notary.FakeSimpleAccount(senderKey), + Account: payerAcc, }, { Signer: mainTx.Signers[1], From a97bc9a9a38f6db7da5b5d18843d4f4e0424c459 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 17 Aug 2023 21:11:39 +0400 Subject: [PATCH 07/22] sidechain/deploy: Calculate nonce and vub from NeoFS network state In order to prevent duplication of update transactions, there is a need to sync nonce and ValidUntilBlock values in a distributed manner. Since Sidechain is tied to the particular running NeoFS network, and update procedure is performed during its lifetime, it makes sense to base these variables to the network state. For all update transactions, set: * nonce to the current NeoFS epoch; * ValidUntilBlock to E+100, where E is a height of the Sidechain at which the current epoch began. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/contracts.go | 5 +++- pkg/morph/deploy/deploy.go | 43 +++++++++++++++++++++++++++++++++++ pkg/morph/deploy/nns.go | 5 +++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/pkg/morph/deploy/contracts.go b/pkg/morph/deploy/contracts.go index 267a86a3c1..62ced19bc6 100644 --- a/pkg/morph/deploy/contracts.go +++ b/pkg/morph/deploy/contracts.go @@ -33,6 +33,8 @@ type syncNeoFSContractPrm struct { blockchain Blockchain + neoFS NeoFS + // based on blockchain monitor *blockchainMonitor @@ -157,6 +159,7 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint var alreadyUpdated bool domainNameForAddress := prm.domainName + "." + domainContractAddresses l := prm.logger.With(zap.String("contract", prm.localManifest.Name), zap.String("domain", domainNameForAddress)) + updateTxModifier := neoFSRuntimeTransactionModifier(prm.neoFS) deployTxMonitor := newTransactionGroupMonitor(localActor) updateTxMonitor := newTransactionGroupMonitor(localActor) registerDomainTxMonitor := newTransactionGroupMonitor(localActor) @@ -301,7 +304,7 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint } } - tx, err := proxyCommitteeActor.MakeCall(onChainState.Hash, methodUpdate, + tx, err := proxyCommitteeActor.MakeTunedCall(onChainState.Hash, methodUpdate, nil, updateTxModifier, bLocalNEF, jLocalManifest, extraUpdateArgs) if err != nil { if isErrContractAlreadyUpdated(err) { diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index ccf019bca7..41ff0cacda 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -11,9 +11,11 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "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/neorpc" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" @@ -70,6 +72,20 @@ type KeyStorage interface { GetPersistedPrivateKey() (*keys.PrivateKey, error) } +// NeoFSState groups information about NeoFS network state processed by Deploy. +type NeoFSState struct { + // Current NeoFS epoch. + CurrentEpoch uint64 + // Height of the NeoFS Sidechain at which CurrentEpoch began. + CurrentEpochBlock uint32 +} + +// NeoFS provides access to the running NeoFS network. +type NeoFS interface { + // CurrentState returns current state of the NeoFS network. + CurrentState() (NeoFSState, error) +} + // CommonDeployPrm groups common deployment parameters of the smart contract. type CommonDeployPrm struct { NEF nef.File @@ -137,6 +153,9 @@ type Prm struct { // Storage for single committee group key. KeyStorage KeyStorage + // Running NeoFS network for which deployment procedure is performed. + NeoFS NeoFS + NNS NNSPrm AlphabetContract AlphabetContractPrm @@ -315,6 +334,7 @@ func Deploy(ctx context.Context, prm Prm) error { syncPrm := syncNeoFSContractPrm{ logger: prm.Logger, blockchain: prm.Blockchain, + neoFS: prm.NeoFS, monitor: monitor, localAcc: prm.LocalAccount, nnsContract: nnsOnChainAddress, @@ -369,6 +389,7 @@ func Deploy(ctx context.Context, prm Prm) error { err = updateNNSContract(ctx, updateNNSContractPrm{ logger: prm.Logger, blockchain: prm.Blockchain, + neoFS: prm.NeoFS, monitor: monitor, localAcc: prm.LocalAccount, localNEF: prm.NNS.Common.NEF, @@ -645,3 +666,25 @@ func encodeFloatConfig(v float64) []byte { func encodeBoolConfig(v bool) []byte { return stackitem.NewBool(v).Bytes() } + +// returns actor.TransactionCheckerModifier which sets current NeoFS epoch as +// nonce of the transaction and makes it valid 100 blocks after Sidechain block +// when the epoch began. +func neoFSRuntimeTransactionModifier(neoFS NeoFS) actor.TransactionCheckerModifier { + return func(r *result.Invoke, tx *transaction.Transaction) error { + err := actor.DefaultCheckerModifier(r, tx) + if err != nil { + return err + } + + neoFSState, err := neoFS.CurrentState() + if err != nil { + return fmt.Errorf("get current NeoFS network state: %w", err) + } + + tx.Nonce = uint32(neoFSState.CurrentEpoch) + tx.ValidUntilBlock = neoFSState.CurrentEpochBlock + 100 + + return nil + } +} diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index 5b9d5f6edf..addcd4091d 100644 --- a/pkg/morph/deploy/nns.go +++ b/pkg/morph/deploy/nns.go @@ -248,6 +248,8 @@ type updateNNSContractPrm struct { blockchain Blockchain + neoFS NeoFS + // based on blockchain monitor *blockchainMonitor @@ -302,6 +304,7 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { ctx, cancel := context.WithCancel(ctx) defer cancel() + updateTxModifier := neoFSRuntimeTransactionModifier(prm.neoFS) txMonitor := newTransactionGroupMonitor(committeeActor) for ; ; prm.monitor.waitForNextBlock(ctx) { @@ -340,7 +343,7 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { // we pre-check 'already updated' case via MakeCall in order to not potentially // wait for previously sent transaction to be expired (condition below) and // immediately succeed - tx, err := committeeActor.MakeCall(nnsOnChainState.Hash, methodUpdate, + tx, err := committeeActor.MakeTunedCall(nnsOnChainState.Hash, methodUpdate, nil, updateTxModifier, bLocalNEF, jLocalManifest, extraUpdateArgs) if err != nil { if isErrContractAlreadyUpdated(err) { From d2ee8cc72fd5f93cda14b541e05ce4172a735fc2 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 21 Aug 2023 14:53:38 +0400 Subject: [PATCH 08/22] sidechain/deploy: Build update args regardless of the on-chain version Previously, update procedure calculated extra update arguments according to the on-chain version for each contract. This didn't make any sense because update callback (`_deploy` function in contract sources) is known in advance and called around new version of the contract (in this context, on-chain version is previous). Drop `contractVersion` parameter from update data constructors. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/contracts.go | 15 +++----- pkg/morph/deploy/deploy.go | 72 +++++++++++++---------------------- pkg/morph/deploy/nns.go | 12 ++---- 3 files changed, 35 insertions(+), 64 deletions(-) diff --git a/pkg/morph/deploy/contracts.go b/pkg/morph/deploy/contracts.go index 62ced19bc6..7acfe85792 100644 --- a/pkg/morph/deploy/contracts.go +++ b/pkg/morph/deploy/contracts.go @@ -67,8 +67,9 @@ type syncNeoFSContractPrm struct { buildExtraDeployArgs func() ([]interface{}, error) // constructor of extra arguments to be passed into method updating the - // contract. If returns both nil, no data is passed. - buildVersionedExtraUpdateArgs func(versionOnChain contractVersion) ([]interface{}, error) + // contract. If returns both nil, no data is passed (noExtraUpdateArgs may be + // used). + buildExtraUpdateArgs func() ([]interface{}, error) // address of the Proxy contract deployed in the blockchain. The contract // pays for update transactions. @@ -284,16 +285,10 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint return onChainState.Hash, nil } } else { - versionOnChain, err := readContractOnChainVersion(prm.blockchain, onChainState.Hash) - if err != nil { - l.Error("failed to read on-chain version of the contract, will try again later", zap.Error(err)) - continue - } - - extraUpdateArgs, err := prm.buildVersionedExtraUpdateArgs(versionOnChain) + extraUpdateArgs, err := prm.buildExtraUpdateArgs() if err != nil { l.Error("failed to prepare build extra arguments for the contract update, will try again later", - zap.Stringer("on-chain version", versionOnChain), zap.Error(err)) + zap.Error(err)) continue } diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index 41ff0cacda..0b5ab1e5aa 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -368,7 +368,7 @@ func Deploy(ctx context.Context, prm Prm) error { syncPrm.localManifest = prm.ProxyContract.Common.Manifest syncPrm.domainName = domainProxy syncPrm.buildExtraDeployArgs = noExtraDeployArgs - syncPrm.buildVersionedExtraUpdateArgs = noExtraUpdateArgs + syncPrm.buildExtraUpdateArgs = noExtraUpdateArgs syncPrm.isProxy = true prm.Logger.Info("synchronizing Proxy contract with the chain...") @@ -387,18 +387,18 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("updating on-chain NNS contract...") err = updateNNSContract(ctx, updateNNSContractPrm{ - logger: prm.Logger, - blockchain: prm.Blockchain, - neoFS: prm.NeoFS, - monitor: monitor, - localAcc: prm.LocalAccount, - localNEF: prm.NNS.Common.NEF, - localManifest: prm.NNS.Common.Manifest, - systemEmail: prm.NNS.SystemEmail, - committee: committee, - committeeGroupKey: committeeGroupKey, - buildVersionedExtraUpdateArgs: noExtraUpdateArgs, - proxyContract: proxyContractAddress, + logger: prm.Logger, + blockchain: prm.Blockchain, + neoFS: prm.NeoFS, + monitor: monitor, + localAcc: prm.LocalAccount, + localNEF: prm.NNS.Common.NEF, + localManifest: prm.NNS.Common.Manifest, + systemEmail: prm.NNS.SystemEmail, + committee: committee, + committeeGroupKey: committeeGroupKey, + buildExtraUpdateArgs: noExtraUpdateArgs, + proxyContract: proxyContractAddress, }) if err != nil { return fmt.Errorf("update NNS contract on the chain: %w", err) @@ -409,11 +409,8 @@ func Deploy(ctx context.Context, prm Prm) error { // Alphabet syncPrm.localNEF = prm.AlphabetContract.Common.NEF syncPrm.localManifest = prm.AlphabetContract.Common.Manifest - syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { - if versionOnChain.equals(0, 17, 0) { - return []interface{}{notaryDisabledExtraUpdateArg}, nil - } - return nil, nil + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil } for ind := 0; ind < len(committee) && ind < glagolitsa.Size; ind++ { @@ -462,11 +459,8 @@ func Deploy(ctx context.Context, prm Prm) error { syncPrm.localManifest = prm.AuditContract.Common.Manifest syncPrm.domainName = domainAudit syncPrm.buildExtraDeployArgs = noExtraDeployArgs - syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { - if versionOnChain.equals(0, 17, 0) { - return []interface{}{notaryDisabledExtraUpdateArg}, nil - } - return nil, nil + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil } prm.Logger.Info("synchronizing Audit contract with the chain...") @@ -483,11 +477,8 @@ func Deploy(ctx context.Context, prm Prm) error { syncPrm.localManifest = prm.BalanceContract.Common.Manifest syncPrm.domainName = domainBalance syncPrm.buildExtraDeployArgs = noExtraDeployArgs - syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { - if versionOnChain.equals(0, 17, 0) { - return []interface{}{notaryDisabledExtraUpdateArg}, nil - } - return nil, nil + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil } prm.Logger.Info("synchronizing Balance contract with the chain...") @@ -526,11 +517,8 @@ func Deploy(ctx context.Context, prm Prm) error { domainContainers, }, nil } - syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { - if versionOnChain.equals(0, 17, 0) { - return []interface{}{notaryDisabledExtraUpdateArg}, nil - } - return nil, nil + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil } prm.Logger.Info("synchronizing Container contract with the chain...") @@ -558,11 +546,8 @@ func Deploy(ctx context.Context, prm Prm) error { netmapContractAddress, }, nil } - syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { - if versionOnChain.equals(0, 17, 0) { - return []interface{}{notaryDisabledExtraUpdateArg}, nil - } - return nil, nil + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil } prm.Logger.Info("synchronizing NeoFSID contract with the chain...") @@ -614,7 +599,7 @@ func Deploy(ctx context.Context, prm Prm) error { netConfig, }, nil } - syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { return []interface{}{notaryDisabledExtraUpdateArg, util.Uint160{}, util.Uint160{}, []interface{}(nil), []interface{}(nil)}, nil } @@ -632,11 +617,8 @@ func Deploy(ctx context.Context, prm Prm) error { syncPrm.localManifest = prm.ReputationContract.Common.Manifest syncPrm.domainName = domainReputation syncPrm.buildExtraDeployArgs = noExtraDeployArgs - syncPrm.buildVersionedExtraUpdateArgs = func(versionOnChain contractVersion) ([]interface{}, error) { - if versionOnChain.equals(0, 17, 0) { - return []interface{}{notaryDisabledExtraUpdateArg}, nil - } - return nil, nil + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil } prm.Logger.Info("synchronizing Reputation contract with the chain...") @@ -651,7 +633,7 @@ func Deploy(ctx context.Context, prm Prm) error { return nil } -func noExtraUpdateArgs(contractVersion) ([]interface{}, error) { return nil, nil } +func noExtraUpdateArgs() ([]interface{}, error) { return nil, nil } func noExtraDeployArgs() ([]interface{}, error) { return nil, nil } diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index addcd4091d..69e529395e 100644 --- a/pkg/morph/deploy/nns.go +++ b/pkg/morph/deploy/nns.go @@ -265,7 +265,7 @@ type updateNNSContractPrm struct { // constructor of extra arguments to be passed into method updating the // contract. If returns both nil, no data is passed (noExtraUpdateArgs may be // used). - buildVersionedExtraUpdateArgs func(versionOnChain contractVersion) ([]interface{}, error) + buildExtraUpdateArgs func() ([]interface{}, error) // address of the Proxy contract deployed in the blockchain. The contract // pays for update transactions. @@ -325,16 +325,10 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { return errors.New("missing required NNS contract on the chain") } - versionOnChain, err := readContractOnChainVersion(prm.blockchain, nnsOnChainState.Hash) - if err != nil { - prm.logger.Error("failed to read on-chain version of the NNS contract, will try again later", zap.Error(err)) - continue - } - - extraUpdateArgs, err := prm.buildVersionedExtraUpdateArgs(versionOnChain) + extraUpdateArgs, err := prm.buildExtraUpdateArgs() if err != nil { prm.logger.Error("failed to prepare build extra arguments for NNS contract update, will try again later", - zap.Stringer("on-chain version", versionOnChain), zap.Error(err)) + zap.Error(err)) continue } From 34f3e5eaec96868a09e5ee6f287b67f647259f51 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 21 Aug 2023 15:40:02 +0400 Subject: [PATCH 09/22] sidechain/deploy: Deploy contracts in a strict order Previously, NeoFS contracts deployed in free order. In order to avoid potential dependency or stability issues, it's worth to make deployment in a fixed and clearly defined sequence. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/deploy.go | 209 +++++++++++++++++++------------------ 1 file changed, 105 insertions(+), 104 deletions(-) diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index 0b5ab1e5aa..38f3043960 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -345,9 +345,24 @@ func Deploy(ctx context.Context, prm Prm) error { localAccLeads := localAccCommitteeIndex == 0 + // we attempt to deploy contracts (except Alphabet ones) by single committee + // member (1st for simplicity) to reduce the likelihood of contract duplication + // in the chain and better predictability of the final address (the address is a + // function from the sender of the deploying transaction). While this approach + // is centralized, we still expect any node incl. 1st one to be "healthy". + // Updates are done concurrently. + syncPrm.tryDeploy = localAccLeads + var notaryDisabledExtraUpdateArg bool - // then go dependent contracts. Things may become better/easier after + // function allowing to calculate addresses of cross-dependent contracts. For + // example, when A contract requires address of the B one, and B contract + // requires address of the A one, we cannot get on-chain addresses of them both + // because it's a cross dependency. Since fixed account performs initial + // deployment (see above why), we are able to pre-calculate addresses and + // resolve dependency problem. + // + // Things may become better/easier after // https://github.com/nspcc-dev/neofs-contract/issues/325 resolveContractAddressDynamically := func(commonPrm CommonDeployPrm, contractDomain string) (util.Uint160, error) { domain := calculateContractAddressDomain(contractDomain) @@ -362,8 +377,13 @@ func Deploy(ctx context.Context, prm Prm) error { return onChainState.Hash, nil } - // Proxy goes first. It's required for Notary service to work, and also pays for - // subsequent contract updates. + // Deploy NeoFS contracts in strict order. Contracts dependent on others come + // after. + + // 1. Proxy + // + // It's required for Notary service to work, and also pays for subsequent + // contract updates. syncPrm.localNEF = prm.ProxyContract.Common.NEF syncPrm.localManifest = prm.ProxyContract.Common.Manifest syncPrm.domainName = domainProxy @@ -384,6 +404,10 @@ func Deploy(ctx context.Context, prm Prm) error { syncPrm.isProxy = false syncPrm.proxyContract = proxyContractAddress + // NNS (update) + // + // Special contract which is always deployed first, but its update depends on + // Proxy contract. prm.Logger.Info("updating on-chain NNS contract...") err = updateNNSContract(ctx, updateNNSContractPrm{ @@ -406,55 +430,7 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("on-chain NNS contract successfully updated") - // Alphabet - syncPrm.localNEF = prm.AlphabetContract.Common.NEF - syncPrm.localManifest = prm.AlphabetContract.Common.Manifest - syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { - return []interface{}{notaryDisabledExtraUpdateArg}, nil - } - - for ind := 0; ind < len(committee) && ind < glagolitsa.Size; ind++ { - syncPrm.tryDeploy = ind == localAccCommitteeIndex // each member deploys its own Alphabet contract - syncPrm.domainName = calculateAlphabetContractAddressDomain(ind) - syncPrm.buildExtraDeployArgs = func() ([]interface{}, error) { - netmapContractAddress, err := resolveContractAddressDynamically(prm.NetmapContract.Common, domainNetmap) - if err != nil { - return nil, fmt.Errorf("resolve address of the Netmap contract: %w", err) - } - proxyContractAddress, err := resolveContractAddressDynamically(prm.ProxyContract.Common, domainProxy) - if err != nil { - return nil, fmt.Errorf("resolve address of the Proxy contract: %w", err) - } - return []interface{}{ - notaryDisabledExtraUpdateArg, - netmapContractAddress, - proxyContractAddress, - glagolitsa.LetterByIndex(ind), - ind, - len(committee), - }, nil - } - - prm.Logger.Info("synchronizing Alphabet contract with the chain...", zap.Int("index", ind)) - - alphabetContractAddress, err := syncNeoFSContract(ctx, syncPrm) - if err != nil { - return fmt.Errorf("sync Alphabet contract #%d with the chain: %w", ind, err) - } - - prm.Logger.Info("Alphabet contract successfully synchronized", - zap.Int("index", ind), zap.Stringer("address", alphabetContractAddress)) - } - - // we attempt to deploy contracts other than Alphabet ones by single committee - // member (1st for simplicity) to reduce the likelihood of contract duplication - // in the chain and better predictability of the final address (the address is a - // function from the sender of the deploying transaction). While this approach - // is centralized, we still expect any node incl. 1st one to be "healthy". - // Updates are done concurrently. - syncPrm.tryDeploy = localAccLeads - - // Audit + // 2. Audit syncPrm.localNEF = prm.AuditContract.Common.NEF syncPrm.localManifest = prm.AuditContract.Common.Manifest syncPrm.domainName = domainAudit @@ -472,7 +448,7 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("Audit contract successfully synchronized", zap.Stringer("address", auditContractAddress)) - // Balance + // 3. Balance syncPrm.localNEF = prm.BalanceContract.Common.NEF syncPrm.localManifest = prm.BalanceContract.Common.Manifest syncPrm.domainName = domainBalance @@ -490,52 +466,65 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("Balance contract successfully synchronized", zap.Stringer("address", balanceContractAddress)) - // Container - syncPrm.localNEF = prm.ContainerContract.Common.NEF - syncPrm.localManifest = prm.ContainerContract.Common.Manifest - syncPrm.domainName = domainContainer - syncPrm.committeeDeployRequired = true + // 4. Reputation + syncPrm.localNEF = prm.ReputationContract.Common.NEF + syncPrm.localManifest = prm.ReputationContract.Common.Manifest + syncPrm.domainName = domainReputation + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { + return []interface{}{notaryDisabledExtraUpdateArg}, nil + } + + prm.Logger.Info("synchronizing Reputation contract with the chain...") + + reputationContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Reputation contract with the chain: %w", err) + } + + prm.Logger.Info("Reputation contract successfully synchronized", zap.Stringer("address", reputationContractAddress)) + + // order of the following contracts is trickier: + // - Netmap depends on Container + // - NeoFS ID depends on Netmap + // - Container depends on Netmap and NeoFS ID + // (other dependencies doesn't matter in current context) + // + // according to this, we cannot select linear deployment order, so, taking + // into account we use workaround described above, the order is any + + // 5. NeoFSID + syncPrm.localNEF = prm.NeoFSIDContract.Common.NEF + syncPrm.localManifest = prm.NeoFSIDContract.Common.Manifest + syncPrm.domainName = domainNeoFSID syncPrm.buildExtraDeployArgs = func() ([]interface{}, error) { netmapContractAddress, err := resolveContractAddressDynamically(prm.NetmapContract.Common, domainNetmap) if err != nil { return nil, fmt.Errorf("resolve address of the Netmap contract: %w", err) } - balanceContractAddress, err := resolveContractAddressDynamically(prm.BalanceContract.Common, domainBalance) - if err != nil { - return nil, fmt.Errorf("resolve address of the Balance contract: %w", err) - } - neoFSIDContractAddress, err := resolveContractAddressDynamically(prm.NeoFSIDContract.Common, domainNeoFSID) - if err != nil { - return nil, fmt.Errorf("resolve address of the NeoFSID contract: %w", err) - } return []interface{}{ notaryDisabledExtraUpdateArg, netmapContractAddress, - balanceContractAddress, - neoFSIDContractAddress, - nnsOnChainAddress, - domainContainers, }, nil } syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { return []interface{}{notaryDisabledExtraUpdateArg}, nil } - prm.Logger.Info("synchronizing Container contract with the chain...") + prm.Logger.Info("synchronizing NeoFSID contract with the chain...") - containerContractAddress, err := syncNeoFSContract(ctx, syncPrm) + neoFSIDContractAddress, err := syncNeoFSContract(ctx, syncPrm) if err != nil { - return fmt.Errorf("sync Container contract with the chain: %w", err) + return fmt.Errorf("sync NeoFSID contract with the chain: %w", err) } - prm.Logger.Info("Container contract successfully synchronized", zap.Stringer("address", containerContractAddress)) - - syncPrm.committeeDeployRequired = false + prm.Logger.Info("NeoFSID contract successfully synchronized", zap.Stringer("address", neoFSIDContractAddress)) - // NeoFSID - syncPrm.localNEF = prm.NeoFSIDContract.Common.NEF - syncPrm.localManifest = prm.NeoFSIDContract.Common.Manifest - syncPrm.domainName = domainNeoFSID + // 6. Container + syncPrm.localNEF = prm.ContainerContract.Common.NEF + syncPrm.localManifest = prm.ContainerContract.Common.Manifest + syncPrm.domainName = domainContainer + syncPrm.committeeDeployRequired = true syncPrm.buildExtraDeployArgs = func() ([]interface{}, error) { netmapContractAddress, err := resolveContractAddressDynamically(prm.NetmapContract.Common, domainNetmap) if err != nil { @@ -544,22 +533,28 @@ func Deploy(ctx context.Context, prm Prm) error { return []interface{}{ notaryDisabledExtraUpdateArg, netmapContractAddress, + balanceContractAddress, + neoFSIDContractAddress, + nnsOnChainAddress, + domainContainers, }, nil } syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { return []interface{}{notaryDisabledExtraUpdateArg}, nil } - prm.Logger.Info("synchronizing NeoFSID contract with the chain...") + prm.Logger.Info("synchronizing Container contract with the chain...") - neoFSIDContractAddress, err := syncNeoFSContract(ctx, syncPrm) + containerContractAddress, err := syncNeoFSContract(ctx, syncPrm) if err != nil { - return fmt.Errorf("sync NeoFSID contract with the chain: %w", err) + return fmt.Errorf("sync Container contract with the chain: %w", err) } - prm.Logger.Info("NeoFSID contract successfully synchronized", zap.Stringer("address", neoFSIDContractAddress)) + prm.Logger.Info("Container contract successfully synchronized", zap.Stringer("address", containerContractAddress)) + + syncPrm.committeeDeployRequired = false - // Netmap + // 7. Netmap netConfig := []interface{}{ []byte(netmap.MaxObjectSizeConfig), encodeUintConfig(prm.NetmapContract.Config.MaxObjectSize), []byte(netmap.BasicIncomeRateConfig), encodeUintConfig(prm.NetmapContract.Config.StoragePrice), @@ -583,14 +578,6 @@ func Deploy(ctx context.Context, prm Prm) error { syncPrm.localManifest = prm.NetmapContract.Common.Manifest syncPrm.domainName = domainNetmap syncPrm.buildExtraDeployArgs = func() ([]interface{}, error) { - balanceContractAddress, err := resolveContractAddressDynamically(prm.BalanceContract.Common, domainBalance) - if err != nil { - return nil, fmt.Errorf("resolve address of the Balance contract: %w", err) - } - containerContractAddress, err := resolveContractAddressDynamically(prm.ContainerContract.Common, domainContainer) - if err != nil { - return nil, fmt.Errorf("resolve address of the Container contract: %w", err) - } return []interface{}{ notaryDisabledExtraUpdateArg, balanceContractAddress, @@ -612,23 +599,37 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("Netmap contract successfully synchronized", zap.Stringer("address", netmapContractAddress)) - // Reputation - syncPrm.localNEF = prm.ReputationContract.Common.NEF - syncPrm.localManifest = prm.ReputationContract.Common.Manifest - syncPrm.domainName = domainReputation - syncPrm.buildExtraDeployArgs = noExtraDeployArgs + // 8. Alphabet + syncPrm.localNEF = prm.AlphabetContract.Common.NEF + syncPrm.localManifest = prm.AlphabetContract.Common.Manifest syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { return []interface{}{notaryDisabledExtraUpdateArg}, nil } - prm.Logger.Info("synchronizing Reputation contract with the chain...") + for ind := 0; ind < len(committee) && ind < glagolitsa.Size; ind++ { + syncPrm.tryDeploy = ind == localAccCommitteeIndex // each member deploys its own Alphabet contract + syncPrm.domainName = calculateAlphabetContractAddressDomain(ind) + syncPrm.buildExtraDeployArgs = func() ([]interface{}, error) { + return []interface{}{ + notaryDisabledExtraUpdateArg, + netmapContractAddress, + proxyContractAddress, + glagolitsa.LetterByIndex(ind), + ind, + len(committee), + }, nil + } - reputationContractAddress, err := syncNeoFSContract(ctx, syncPrm) - if err != nil { - return fmt.Errorf("sync Reputation contract with the chain: %w", err) - } + prm.Logger.Info("synchronizing Alphabet contract with the chain...", zap.Int("index", ind)) - prm.Logger.Info("Reputation contract successfully synchronized", zap.Stringer("address", reputationContractAddress)) + alphabetContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Alphabet contract #%d with the chain: %w", ind, err) + } + + prm.Logger.Info("Alphabet contract successfully synchronized", + zap.Int("index", ind), zap.Stringer("address", alphabetContractAddress)) + } return nil } From e25778f8e01ac171f7b69a38e5981ec660c7bfa1 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 21 Aug 2023 17:10:56 +0400 Subject: [PATCH 10/22] sidechain/deploy: Register domain with record in single transaction Previously, Sidechain deployment procedure registered contract domain in the NNS and set its record in two separate transactions. According to `register` method that doesn't fail if requested domain already exists, nothing prevents us to stick both actions together into single transaction. Make `syncNeoFSContract` to concatenate scripts of `register` and `addRecord` calls and send single transaction with the result. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/contracts.go | 132 ++++++++++++++-------------------- 1 file changed, 55 insertions(+), 77 deletions(-) diff --git a/pkg/morph/deploy/contracts.go b/pkg/morph/deploy/contracts.go index 7acfe85792..c506bf978d 100644 --- a/pkg/morph/deploy/contracts.go +++ b/pkg/morph/deploy/contracts.go @@ -163,7 +163,6 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint updateTxModifier := neoFSRuntimeTransactionModifier(prm.neoFS) deployTxMonitor := newTransactionGroupMonitor(localActor) updateTxMonitor := newTransactionGroupMonitor(localActor) - registerDomainTxMonitor := newTransactionGroupMonitor(localActor) registerTLDTxMonitor := newTransactionGroupMonitor(localActor) setDomainRecordTxMonitor := newTransactionGroupMonitor(localActor) @@ -176,7 +175,7 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint l.Info("reading on-chain state of the contract by NNS domain name...") - var missingDomainName, missingDomainRecord bool + var missingDomainRecord bool onChainState, err := readContractOnChainStateByDomainName(prm.blockchain, prm.nnsContract, domainNameForAddress) if err != nil { @@ -185,17 +184,14 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint continue } - missingDomainName = errors.Is(err, errMissingDomain) - if !missingDomainName { - missingDomainRecord = errors.Is(err, errMissingDomainRecord) - if !missingDomainRecord { - if errors.Is(err, errInvalidContractDomainRecord) { - l.Error("contract's domain record is invalid/unsupported, will wait for a background fix", zap.Error(err)) - } else { - l.Error("failed to read on-chain state of the contract record by NNS domain name, will try again later", zap.Error(err)) - } - continue + missingDomainRecord = errors.Is(err, errMissingDomain) || errors.Is(err, errMissingDomainRecord) + if !missingDomainRecord { + if errors.Is(err, errInvalidContractDomainRecord) { + l.Error("contract's domain record is invalid/unsupported, will wait for a background fix", zap.Error(err)) + } else { + l.Error("failed to read on-chain state of the contract record by NNS domain name, will try again later", zap.Error(err)) } + continue } l.Info("could not read on-chain state of the contract by NNS domain name, trying by pre-calculated address...") @@ -281,7 +277,7 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint } if alreadyUpdated { - if !missingDomainName && !missingDomainRecord { + if !missingDomainRecord { return onChainState.Hash, nil } } else { @@ -304,7 +300,7 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint if err != nil { if isErrContractAlreadyUpdated(err) { l.Info("the contract is unchanged or has already been updated") - if !missingDomainName && !missingDomainRecord { + if !missingDomainRecord { return onChainState.Hash, nil } alreadyUpdated = true @@ -339,84 +335,66 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint continue } - if missingDomainName { - l.Info("NNS domain is missing, registration is needed") - - if registerDomainTxMonitor.isPending() { - l.Info("previously sent transaction registering domain in the NNS is still pending, will wait for the outcome") - continue - } - - l.Info("sending new transaction registering domain in the NNS...") - - txID, vub, err := localActor.SendCall(prm.nnsContract, methodNNSRegister, - domainNameForAddress, localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) - if err != nil { - switch { - default: - l.Error("failed to send transaction registering domain in the NNS, will try again later", zap.Error(err)) - case errors.Is(err, neorpc.ErrInsufficientFunds): - l.Info("not enough GAS to register domain in the NNS, will try again later") - case isErrTLDNotFound(err): - l.Info("missing TLD, need registration") - - if registerTLDTxMonitor.isPending() { - l.Info("previously sent Notary request registering TLD in the NNS is still pending, will wait for the outcome") - continue - } - - l.Info("sending new Notary registering TLD in the NNS...") - - mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(committeeActor.MakeCall(prm.nnsContract, methodNNSRegisterTLD, - domainContractAddresses, prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum)) - if err != nil { - if errors.Is(err, neorpc.ErrInsufficientFunds) { - l.Info("insufficient Notary balance to register TLD in the NNS, will try again later") - } else { - l.Error("failed to send Notary request registering TLD in the NNS, will try again later", zap.Error(err)) - } - continue - } - - l.Info("Notary request registering TLD in the NNS has been successfully sent, will wait for the outcome", - zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) - - registerTLDTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) - } - continue - } - - l.Info("transaction registering domain in the NNS has been successfully sent, will wait for the outcome", - zap.Stringer("tx", txID), zap.Uint32("vub", vub), - ) - - registerDomainTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + l.Info("NNS domain record is missing, registration is needed") + if setDomainRecordTxMonitor.isPending() { + l.Info("previously sent transaction setting domain in the NNS is still pending, will wait for the outcome") continue } - // we come here only when missingDomainRecord is true - l.Info("missing domain record in the NNS, needed to be set") + l.Info("sending new transaction setting domain in the NNS...") - if setDomainRecordTxMonitor.isPending() { - l.Info("previously sent transaction setting domain record in the NNS is still pending, will wait for the outcome") + resRegister, err := localActor.Call(prm.nnsContract, methodNNSRegister, + domainNameForAddress, localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) + if err != nil { + l.Info("test invocation registering domain in the NNS failed, will try again later") continue } - l.Info("sending new transaction setting domain record in the NNS...") - - txID, vub, err := localActor.SendCall(prm.nnsContract, methodNNSAddRecord, + resAddRecord, err := localActor.Call(prm.nnsContract, methodNNSAddRecord, domainNameForAddress, int64(nns.TXT), onChainState.Hash.StringLE()) if err != nil { - if errors.Is(err, neorpc.ErrInsufficientFunds) { + l.Info("test invocation setting domain record in the NNS failed, will try again later") + continue + } + + txID, vub, err := localActor.SendRun(append(resRegister.Script, resAddRecord.Script...)) + if err != nil { + switch { + default: + l.Error("failed to send transaction setting domain in the NNS, will try again later", zap.Error(err)) + case errors.Is(err, neorpc.ErrInsufficientFunds): l.Info("not enough GAS to set domain record in the NNS, will try again later") - } else { - l.Error("failed to send transaction setting domain record in the NNS, will try again later", zap.Error(err)) + case isErrTLDNotFound(err): + l.Info("missing TLD, need registration") + + if registerTLDTxMonitor.isPending() { + l.Info("previously sent Notary request registering TLD in the NNS is still pending, will wait for the outcome") + continue + } + + l.Info("sending new Notary registering TLD in the NNS...") + + mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(committeeActor.MakeCall(prm.nnsContract, methodNNSRegisterTLD, + domainContractAddresses, prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + l.Info("insufficient Notary balance to register TLD in the NNS, will try again later") + } else { + l.Error("failed to send Notary request registering TLD in the NNS, will try again later", zap.Error(err)) + } + continue + } + + l.Info("Notary request registering TLD in the NNS has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + registerTLDTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) } continue } - l.Info("transaction setting domain record in the NNS has been successfully sent, will wait for the outcome", + l.Info("transaction settings domain record in the NNS has been successfully sent, will wait for the outcome", zap.Stringer("tx", txID), zap.Uint32("vub", vub), ) From db31aaed3b70eb10af30c714e4402cf4db1ec307 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 21 Aug 2023 18:31:58 +0400 Subject: [PATCH 11/22] sidechain/deploy: Register committee group key in the NNS Make Sidechain deployment procedure to register 'group.neofs' domain with public key of the committee group. Register functionality is almost the same (different domain name and record format) as for contract addresses, so it's shared in new helper function. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/contracts.go | 79 +++++-------------------------- pkg/morph/deploy/deploy.go | 18 +++++++ pkg/morph/deploy/group.go | 89 +++++++++++++++++++++++++++++++++++ pkg/morph/deploy/nns.go | 88 ++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 66 deletions(-) diff --git a/pkg/morph/deploy/contracts.go b/pkg/morph/deploy/contracts.go index c506bf978d..6d6a55a00d 100644 --- a/pkg/morph/deploy/contracts.go +++ b/pkg/morph/deploy/contracts.go @@ -12,7 +12,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/neorpc" "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/rpcclient/nns" "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" @@ -163,8 +162,17 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint updateTxModifier := neoFSRuntimeTransactionModifier(prm.neoFS) deployTxMonitor := newTransactionGroupMonitor(localActor) updateTxMonitor := newTransactionGroupMonitor(localActor) - registerTLDTxMonitor := newTransactionGroupMonitor(localActor) - setDomainRecordTxMonitor := newTransactionGroupMonitor(localActor) + setContractRecordPrm := setNeoFSContractDomainRecordPrm{ + logger: l, + setRecordTxMonitor: newTransactionGroupMonitor(localActor), + registerTLDTxMonitor: newTransactionGroupMonitor(localActor), + nnsContract: prm.nnsContract, + systemEmail: prm.systemEmail, + localActor: localActor, + committeeActor: committeeActor, + domain: domainNameForAddress, + record: "", // set in for loop + } for ; ; prm.monitor.waitForNextBlock(ctx) { select { @@ -335,69 +343,8 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint continue } - l.Info("NNS domain record is missing, registration is needed") - - if setDomainRecordTxMonitor.isPending() { - l.Info("previously sent transaction setting domain in the NNS is still pending, will wait for the outcome") - continue - } - - l.Info("sending new transaction setting domain in the NNS...") - - resRegister, err := localActor.Call(prm.nnsContract, methodNNSRegister, - domainNameForAddress, localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) - if err != nil { - l.Info("test invocation registering domain in the NNS failed, will try again later") - continue - } - - resAddRecord, err := localActor.Call(prm.nnsContract, methodNNSAddRecord, - domainNameForAddress, int64(nns.TXT), onChainState.Hash.StringLE()) - if err != nil { - l.Info("test invocation setting domain record in the NNS failed, will try again later") - continue - } - - txID, vub, err := localActor.SendRun(append(resRegister.Script, resAddRecord.Script...)) - if err != nil { - switch { - default: - l.Error("failed to send transaction setting domain in the NNS, will try again later", zap.Error(err)) - case errors.Is(err, neorpc.ErrInsufficientFunds): - l.Info("not enough GAS to set domain record in the NNS, will try again later") - case isErrTLDNotFound(err): - l.Info("missing TLD, need registration") - - if registerTLDTxMonitor.isPending() { - l.Info("previously sent Notary request registering TLD in the NNS is still pending, will wait for the outcome") - continue - } - - l.Info("sending new Notary registering TLD in the NNS...") - - mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(committeeActor.MakeCall(prm.nnsContract, methodNNSRegisterTLD, - domainContractAddresses, prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum)) - if err != nil { - if errors.Is(err, neorpc.ErrInsufficientFunds) { - l.Info("insufficient Notary balance to register TLD in the NNS, will try again later") - } else { - l.Error("failed to send Notary request registering TLD in the NNS, will try again later", zap.Error(err)) - } - continue - } - - l.Info("Notary request registering TLD in the NNS has been successfully sent, will wait for the outcome", - zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) - - registerTLDTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) - } - continue - } - - l.Info("transaction settings domain record in the NNS has been successfully sent, will wait for the outcome", - zap.Stringer("tx", txID), zap.Uint32("vub", vub), - ) + setContractRecordPrm.record = onChainState.Hash.StringLE() - setDomainRecordTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + setNeoFSContractDomainRecord(ctx, setContractRecordPrm) } } diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index 38f3043960..9e5b3b372e 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -316,6 +316,24 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("committee group successfully initialized", zap.Stringer("public key", committeeGroupKey.PublicKey())) + prm.Logger.Info("registering committee group in the NNS...") + + err = registerCommitteeGroupInNNS(ctx, registerCommitteeGroupInNNSPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + nnsContract: nnsOnChainAddress, + systemEmail: prm.NNS.SystemEmail, + localAcc: prm.LocalAccount, + committee: committee, + committeeGroupKey: committeeGroupKey, + }) + if err != nil { + return fmt.Errorf("regsiter committee group in the NNS: %w", err) + } + + prm.Logger.Info("committee group successfully registered in the NNS") + prm.Logger.Info("initializing NeoFS Alphabet...") err = initAlphabet(ctx, initAlphabetPrm{ diff --git a/pkg/morph/deploy/group.go b/pkg/morph/deploy/group.go index 49bfd1582a..4df8522265 100644 --- a/pkg/morph/deploy/group.go +++ b/pkg/morph/deploy/group.go @@ -6,6 +6,7 @@ import ( "crypto/cipher" "crypto/rand" "encoding/base64" + "encoding/hex" "errors" "fmt" @@ -337,3 +338,91 @@ func calculateSharedSecret(localPrivKey *keys.PrivateKey, remotePubKey *keys.Pub x, _ := localPrivKey.ScalarMult(remotePubKey.X, remotePubKey.Y, localPrivKey.D.Bytes()) return x.Bytes(), nil } + +// registerCommitteeGroupInNNSPrm groups parameters of committee group's +// register in the NNS. +type registerCommitteeGroupInNNSPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + nnsContract util.Uint160 + systemEmail string + + localAcc *wallet.Account + + committee keys.PublicKeys + committeeGroupKey *keys.PrivateKey +} + +// registerCommitteeGroupInNNS registers committee group in the NNS. +func registerCommitteeGroupInNNS(ctx context.Context, prm registerCommitteeGroupInNNSPrm) error { + localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) + if err != nil { + return fmt.Errorf("init transaction sender from local account: %w", err) + } + + committeeActor, err := newCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) + } + + // wrap the parent context into the context of the current function so that + // transaction wait routines do not leak + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + inv := invoker.New(prm.blockchain, nil) + domain := domainCommitteeGroup + "." + domainContractAddresses + l := prm.logger.With(zap.String("domain", domain)) + committeeGroupPubKey := prm.committeeGroupKey.PublicKey() + setContractRecordPrm := setNeoFSContractDomainRecordPrm{ + logger: l, + setRecordTxMonitor: newTransactionGroupMonitor(localActor), + registerTLDTxMonitor: newTransactionGroupMonitor(localActor), + nnsContract: prm.nnsContract, + systemEmail: prm.systemEmail, + localActor: localActor, + committeeActor: committeeActor, + domain: domain, + record: hex.EncodeToString(committeeGroupPubKey.Bytes()), + } + + for ; ; prm.monitor.waitForNextBlock(ctx) { + select { + case <-ctx.Done(): + return fmt.Errorf("wait for committee group key to be registered in the NNS: %w", ctx.Err()) + default: + } + + rec, err := lookupNNSDomainRecord(inv, prm.nnsContract, domain) + if err != nil { + if !errors.Is(err, errMissingDomain) && !errors.Is(err, errMissingDomainRecord) { + l.Error("failed to lookup NNS domain record, will try again later") + continue + } + + setNeoFSContractDomainRecord(ctx, setContractRecordPrm) + + continue + } + + pubKeyInNNS, err := keys.NewPublicKeyFromString(rec) + if err != nil { + l.Error("failed to parse public key of the committee group, will wait for a background fix", + zap.Error(err)) + continue + } + + if !pubKeyInNNS.Equal(committeeGroupPubKey) { + l.Error("public key of the committee group from the NNS differs with the local one, will wait for a background fix", + zap.Stringer("nns", pubKeyInNNS), zap.Stringer("local", committeeGroupPubKey)) + continue + } + + return nil + } +} diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index 69e529395e..df852bb5f2 100644 --- a/pkg/morph/deploy/nns.go +++ b/pkg/morph/deploy/nns.go @@ -14,6 +14,7 @@ import ( "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/nns" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" @@ -39,6 +40,8 @@ const ( domainNetmap = "netmap" domainProxy = "proxy" domainReputation = "reputation" + + domainCommitteeGroup = "group" ) func calculateAlphabetContractAddressDomain(index int) string { @@ -370,3 +373,88 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { txMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) } } + +// setNeoFSContractDomainRecord groups parameters of setNeoFSContractDomainRecord. +type setNeoFSContractDomainRecordPrm struct { + logger *zap.Logger + + setRecordTxMonitor *transactionGroupMonitor + registerTLDTxMonitor *transactionGroupMonitor + + nnsContract util.Uint160 + systemEmail string + + localActor *actor.Actor + + committeeActor *notary.Actor + + domain string + record string +} + +func setNeoFSContractDomainRecord(ctx context.Context, prm setNeoFSContractDomainRecordPrm) { + prm.logger.Info("NNS domain record is missing, registration is needed") + + if prm.setRecordTxMonitor.isPending() { + prm.logger.Info("previously sent transaction setting domain in the NNS is still pending, will wait for the outcome") + return + } + + prm.logger.Info("sending new transaction setting domain in the NNS...") + + resRegister, err := prm.localActor.Call(prm.nnsContract, methodNNSRegister, + prm.domain, prm.localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) + if err != nil { + prm.logger.Info("test invocation registering domain in the NNS failed, will try again later", zap.Error(err)) + return + } + + resAddRecord, err := prm.localActor.Call(prm.nnsContract, methodNNSAddRecord, + prm.domain, int64(nns.TXT), prm.record) + if err != nil { + prm.logger.Info("test invocation setting domain record in the NNS failed, will try again later", zap.Error(err)) + return + } + + txID, vub, err := prm.localActor.SendRun(append(resRegister.Script, resAddRecord.Script...)) + if err != nil { + switch { + default: + prm.logger.Error("failed to send transaction setting domain in the NNS, will try again later", zap.Error(err)) + case errors.Is(err, neorpc.ErrInsufficientFunds): + prm.logger.Info("not enough GAS to set domain record in the NNS, will try again later") + case isErrTLDNotFound(err): + prm.logger.Info("missing TLD, need registration") + + if prm.registerTLDTxMonitor.isPending() { + prm.logger.Info("previously sent Notary request registering TLD in the NNS is still pending, will wait for the outcome") + return + } + + prm.logger.Info("sending new Notary registering TLD in the NNS...") + + mainTxID, fallbackTxID, vub, err := prm.committeeActor.Notarize(prm.committeeActor.MakeCall(prm.nnsContract, methodNNSRegisterTLD, + domainContractAddresses, prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to register TLD in the NNS, will try again later") + } else { + prm.logger.Error("failed to send Notary request registering TLD in the NNS, will try again later", zap.Error(err)) + } + return + } + + prm.logger.Info("Notary request registering TLD in the NNS has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + prm.registerTLDTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } + return + } + + prm.logger.Info("transaction settings domain record in the NNS has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub), + ) + + prm.setRecordTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) +} From e3a34b5351117a8ea791d4a7ec7ef1aa2e0b98bf Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 22 Aug 2023 20:47:16 +0400 Subject: [PATCH 12/22] sidechain/deploy: Transfer initial GAS to the committee Initially, all Sidechain GAS is owned by the validator multi-sig account. We need to distribute this GAS between the committee (multi-sig account and single ones) for initial deployment. It doesn't make sense to leave GAS on validator account. Add stage following Notary service initialization that: * preserves at least 150GAS (initially 300GAS) on each committee member's account * preserves (almost) all remaining GAS on the committee multi-sig account (keeps 10GAS on validator acc to pay transfer fees) * preserves all available NEO on the committee multi-sig acc Notary requests with transactions to be signed by the validator multi-sig account are also supported in the listening routine. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/deploy.go | 39 +++++-- pkg/morph/deploy/funds.go | 232 +++++++++++++++++++++++++++++++++++++ pkg/morph/deploy/notary.go | 28 +++-- 3 files changed, 282 insertions(+), 17 deletions(-) create mode 100644 pkg/morph/deploy/funds.go diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index 9e5b3b372e..f9c6841d10 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -150,6 +150,10 @@ type Prm struct { // Local process account used for transaction signing (must be unlocked). LocalAccount *wallet.Account + // Validator multi-sig account to spread initial GAS to network + // participants (must be unlocked). + ValidatorMultiSigAccount *wallet.Account + // Storage for single committee group key. KeyStorage KeyStorage @@ -179,10 +183,11 @@ type Prm struct { // Deployment process is detailed in NeoFS docs. Summary of stages: // 1. NNS contract deployment // 2. launch of a notary service for the committee -// 3. committee group initialization -// 4. Alphabet initialization -// 5. deployment/update of the NeoFS system contracts -// 6. deployment of custom contracts (currently not supported) +// 3. initial GAS distribution between committee members +// 4. committee group initialization +// 5. Alphabet initialization +// 6. deployment/update of the NeoFS system contracts +// 7. deployment of custom contracts (currently not supported) // // See project documentation for details. func Deploy(ctx context.Context, prm Prm) error { @@ -288,15 +293,33 @@ func Deploy(ctx context.Context, prm Prm) error { go autoReplenishNotaryBalance(ctx, prm.Logger, prm.Blockchain, prm.LocalAccount, chNewBlock) err = listenCommitteeNotaryRequests(ctx, listenCommitteeNotaryRequestsPrm{ - logger: prm.Logger, - blockchain: prm.Blockchain, - localAcc: prm.LocalAccount, - committee: committee, + logger: prm.Logger, + blockchain: prm.Blockchain, + localAcc: prm.LocalAccount, + committee: committee, + validatorMultiSigAcc: prm.ValidatorMultiSigAccount, }) if err != nil { return fmt.Errorf("start listener of committee notary requests: %w", err) } + prm.Logger.Info("making initial transfer of funds to the committee...") + + err = makeInitialTransferToCommittee(ctx, makeInitialGASTransferToCommitteePrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + committee: committee, + localAcc: prm.LocalAccount, + validatorMultiSigAcc: prm.ValidatorMultiSigAccount, + tryTransfer: localAccCommitteeIndex == 0, + }) + if err != nil { + return fmt.Errorf("initial transfer funds to the committee: %w", err) + } + + prm.Logger.Info("initial transfer to the committee successfully done") + prm.Logger.Info("initializing committee group for contract management...") committeeGroupKey, err := initCommitteeGroup(ctx, initCommitteeGroupPrm{ diff --git a/pkg/morph/deploy/funds.go b/pkg/morph/deploy/funds.go new file mode 100644 index 0000000000..f88e4c340d --- /dev/null +++ b/pkg/morph/deploy/funds.go @@ -0,0 +1,232 @@ +package deploy + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/core/native" + "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" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "go.uber.org/zap" +) + +const ( + initialAlphabetGASAmount = 300 + // lower threshold of GAS remaining on validator multi-sig account. It is needed + // to pay fees for transfer transaction(s). The value is big enough for + // transfer, and not very big to leave no tail on the account. + validatorLowerGASThreshold = 10 +) + +// groups parameters of makeInitialGASTransferToCommittee. +type makeInitialGASTransferToCommitteePrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + committee keys.PublicKeys + + localAcc *wallet.Account + validatorMultiSigAcc *wallet.Account + + tryTransfer bool +} + +// makes initial transfer of funds to the committee for deployment procedure. In +// the initial state of the Blockchain, all funds are on the validator multisig +// account. Transfers: +// - 300GAS to each account of the Alphabet members +// - all other GAS to the committee multisig account +// - all NEO to the committee multisig account +func makeInitialTransferToCommittee(ctx context.Context, prm makeInitialGASTransferToCommitteePrm) error { + validatorMultiSigAccAddress := prm.validatorMultiSigAcc.ScriptHash() + + validatorMultiSigNotaryActor, err := notary.NewActor(prm.blockchain, []actor.SignerAccount{ + { + Signer: transaction.Signer{ + Account: prm.localAcc.ScriptHash(), + Scopes: transaction.None, + }, + Account: prm.localAcc, + }, + { + Signer: transaction.Signer{ + Account: validatorMultiSigAccAddress, + Scopes: transaction.CalledByEntry, + }, + Account: prm.validatorMultiSigAcc, + }, + }, prm.localAcc) + if err != nil { + return fmt.Errorf("init notary actor for validator multi-sig account: %w", err) + } + + committeeMajorityMultiSigScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(prm.committee) + if err != nil { + return fmt.Errorf("compose majority committee verification script: %w", err) + } + + // wrap the parent context into the context of the current function so that + // transaction wait routines do not leak + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + lowerGASThreshold := big.NewInt(validatorLowerGASThreshold * native.GASFactor) + neoContract := neo.New(validatorMultiSigNotaryActor) + gasContract := gas.New(validatorMultiSigNotaryActor) + committeeMultiSigAccAddress := hash.Hash160(committeeMajorityMultiSigScript) + committeeDiffersValidator := !validatorMultiSigAccAddress.Equals(committeeMultiSigAccAddress) + transferTxMonitor := newTransactionGroupMonitor(validatorMultiSigNotaryActor) + +upperLoop: + for ; ; prm.monitor.waitForNextBlock(ctx) { + select { + case <-ctx.Done(): + return fmt.Errorf("wait for distribution of initial funds: %w", ctx.Err()) + default: + } + + remGAS, err := gasContract.BalanceOf(validatorMultiSigAccAddress) + if err != nil { + prm.logger.Error("failed to get GAS balance on validator multi-sig account, will try again later", + zap.Error(err)) + continue + } + + remNEO, err := neoContract.BalanceOf(validatorMultiSigAccAddress) + if err != nil { + prm.logger.Error("failed to get NEO balance on validator multi-sig account, will try again later", + zap.Error(err)) + continue + } + + prm.logger.Info("got current balance of the validator multi-sig account, distributing between the committee...", + zap.Stringer("NEO", remNEO), zap.Stringer("GAS", remGAS)) + + if remGAS.Cmp(lowerGASThreshold) <= 0 { + prm.logger.Info("residual GAS on validator multi-sig account does not exceed the lower threshold, initial transfer has already succeeded, skip", + zap.Stringer("rem", remGAS)) + return nil + } + + // prevent transfer of all available GAS in order to pay fees + remGAS.Sub(remGAS, lowerGASThreshold) + gasTransfers := make([]nep17.TransferParameters, 0, len(prm.committee)+1) // + to committee multi-sig + + for i := range prm.committee { + memberBalance, err := gasContract.BalanceOf(prm.committee[i].GetScriptHash()) + if err != nil { + prm.logger.Info("failed to get GAS balance of the committee member, will try again later", + zap.Stringer("member", prm.committee[i]), zap.Error(err)) + continue upperLoop + } + + toTransfer := big.NewInt(initialAlphabetGASAmount * native.GASFactor) + needAtLeast := new(big.Int).Div(toTransfer, big.NewInt(2)) + + if memberBalance.Cmp(needAtLeast) >= 0 { + prm.logger.Info("enough GAS on the committee member's account, skip transfer", + zap.Stringer("member", prm.committee[i]), zap.Stringer("balance", memberBalance), + zap.Stringer("need at least", needAtLeast)) + continue + } + + prm.logger.Info("not enough GAS on the committee member's account, need replenishment", + zap.Stringer("member", prm.committee[i]), zap.Stringer("balance", memberBalance), + zap.Stringer("need at least", needAtLeast)) + + if remGAS.Cmp(toTransfer) <= 0 { + toTransfer.Set(remGAS) + } + + gasTransfers = append(gasTransfers, nep17.TransferParameters{ + From: validatorMultiSigAccAddress, + To: prm.committee[i].GetScriptHash(), + Amount: toTransfer, + }) + + remGAS.Sub(remGAS, toTransfer) + if remGAS.Sign() <= 0 { + break + } + } + + if committeeDiffersValidator && remGAS.Sign() > 0 { + gasTransfers = append(gasTransfers, nep17.TransferParameters{ + From: validatorMultiSigAccAddress, + To: committeeMultiSigAccAddress, + Amount: remGAS, + }) + } + + var script []byte + + if len(gasTransfers) > 0 { + tx, err := gasContract.MultiTransferUnsigned(gasTransfers) + if err != nil { + prm.logger.Error("failed to make transaction transferring GAS from validator multi-sig account to the committee, will try again later", + zap.Error(err)) + continue + } + + script = tx.Script + } + + if committeeDiffersValidator && remNEO.Sign() > 0 { + tx, err := neoContract.TransferUnsigned(validatorMultiSigAccAddress, committeeMultiSigAccAddress, remNEO, nil) + if err != nil { + prm.logger.Error("failed to transaction transferring NEO from validator multi-sig account to the committee one, will try again later", + zap.Error(err)) + continue + } + + script = append(script, tx.Script...) + } + + if len(script) == 0 { + prm.logger.Info("nothing to transfer, skip") + return nil + } + + if !prm.tryTransfer { + prm.logger.Info("need transfer from validator multi-sig account but attempts are disabled, will wait for a leader") + continue + } + + if transferTxMonitor.isPending() { + prm.logger.Info("previously sent transaction transferring funds from validator multi-sig account to the committee is still pending, will wait for the outcome") + continue + } + + prm.logger.Info("sending new Notary request transferring funds from validator multi-sig account to the committee...") + + mainTxID, fallbackTxID, vub, err := validatorMultiSigNotaryActor.Notarize(validatorMultiSigNotaryActor.MakeRun(script)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to transfer funds from validator multi-sig account to the committee, will try again later") + } else { + prm.logger.Error("failed to send Notary request transferring funds from validator multi-sig account to the committee, will try again later", zap.Error(err)) + } + continue + } + + prm.logger.Info("Notary request transferring funds from validator multi-sig account to the committee has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + transferTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } +} diff --git a/pkg/morph/deploy/notary.go b/pkg/morph/deploy/notary.go index f6e16dfa96..0d3ca43e35 100644 --- a/pkg/morph/deploy/notary.go +++ b/pkg/morph/deploy/notary.go @@ -16,7 +16,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "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/neorpc" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" @@ -1010,6 +1009,8 @@ type listenCommitteeNotaryRequestsPrm struct { localAcc *wallet.Account committee keys.PublicKeys + + validatorMultiSigAcc *wallet.Account } // listenCommitteeNotaryRequests starts background process listening to incoming @@ -1026,6 +1027,7 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar return fmt.Errorf("compose committee multi-signature account: %w", err) } + validatorMultiSigAccID := prm.validatorMultiSigAcc.ScriptHash() committeeMultiSigAccID := committeeMultiSigAcc.ScriptHash() chNotaryRequests := make(chan *result.NotaryRequestEvent, 100) // secure from blocking // cache processed operations: when main transaction from received notary @@ -1033,9 +1035,7 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar // the channel again mProcessedMainTxs := make(map[util.Uint256]struct{}) - subID, err := prm.blockchain.ReceiveNotaryRequests(&neorpc.TxFilter{ - Signer: &committeeMultiSigAccID, - }, chNotaryRequests) + subID, err := prm.blockchain.ReceiveNotaryRequests(nil, chNotaryRequests) if err != nil { return fmt.Errorf("subscribe to notary requests from committee: %w", err) } @@ -1064,7 +1064,7 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar // for simplicity, requests are handled one-by one. We could process them in parallel // using worker pool, but actions seem to be relatively lightweight - const expectedSignersCount = 3 // sender + committee + Notary + const expectedSignersCount = 3 // sender + committee|validator + Notary mainTx := notaryEvent.NotaryRequest.MainTransaction // note: instruction above can throw NPE and it's ok to panic: we confidently // expect that only non-nil pointers will come from the channel (NeoGo @@ -1088,9 +1088,11 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar prm.logger.Info("unsupported number of signers of main transaction from the received notary request, skip", zap.Int("expected", expectedSignersCount), zap.Int("got", len(mainTx.Signers))) continue - case !mainTx.HasSigner(committeeMultiSigAccID): - prm.logger.Info("committee is not a signer of main transaction from the received notary request, skip") - continue + case !mainTx.Signers[1].Account.Equals(committeeMultiSigAccID): + if !mainTx.Signers[1].Account.Equals(validatorMultiSigAccID) { + prm.logger.Info("2nd signer of main transaction from the received notary request is neither committee nor validator multi-sig account, skip") + continue + } case mainTx.HasSigner(prm.localAcc.ScriptHash()): prm.logger.Info("main transaction from the received notary request is signed by a local account, skip") continue @@ -1125,6 +1127,14 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar prm.logger.Info("signing main transaction from the received notary request by the local account...") + var multiSigAcc *wallet.Account + if mainTx.Signers[1].Account.Equals(committeeMultiSigAccID) { + multiSigAcc = committeeMultiSigAcc + } else { + // note: switch-case above denies any other cases + multiSigAcc = prm.validatorMultiSigAcc + } + // create new actor for current signers. As a slight optimization, we could also // compare with signers of previously created actor and deduplicate. // See also https://github.com/nspcc-dev/neofs-node/issues/2314 @@ -1135,7 +1145,7 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar }, { Signer: mainTx.Signers[1], - Account: committeeMultiSigAcc, + Account: multiSigAcc, }, }, prm.localAcc) if err != nil { From ac68affa87a3a7b47b3b0d9ed71003d6d62c3d59 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 22 Aug 2023 21:34:54 +0400 Subject: [PATCH 13/22] sidechain/deploy: Transfer GAS to the Proxy contract when deployed Proxy contact pays for main transactions sent using Notary service, so it needs a lot of GAS to do so. Transfer 90% of GAS from the committee multi-sig account to the Proxy contract if it has no GAS. Later replenishments are done by NeoFS GAS emission cycles. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/deploy.go | 17 ++++++ pkg/morph/deploy/funds.go | 106 +++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index f9c6841d10..3e91d2bf99 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -445,6 +445,23 @@ func Deploy(ctx context.Context, prm Prm) error { syncPrm.isProxy = false syncPrm.proxyContract = proxyContractAddress + prm.Logger.Info("replenishing the the Proxy contract's balance...") + + err = transferGASToProxy(ctx, transferGASToProxyPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + proxyContract: proxyContractAddress, + committee: committee, + localAcc: prm.LocalAccount, + tryTransfer: localAccLeads, + }) + if err != nil { + return fmt.Errorf("replenish balance of the Proxy contract: %w", err) + } + + prm.Logger.Info("Proxy balance successfully replenished") + // NNS (update) // // Special contract which is always deployed first, but its update depends on diff --git a/pkg/morph/deploy/funds.go b/pkg/morph/deploy/funds.go index f88e4c340d..440c790720 100644 --- a/pkg/morph/deploy/funds.go +++ b/pkg/morph/deploy/funds.go @@ -17,6 +17,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" "go.uber.org/zap" ) @@ -27,6 +28,9 @@ const ( // to pay fees for transfer transaction(s). The value is big enough for // transfer, and not very big to leave no tail on the account. validatorLowerGASThreshold = 10 + // share of GAS on the committee multi-sig account to be transferred to the + // Proxy contract (in %). + initialProxyGASPercent = 90 ) // groups parameters of makeInitialGASTransferToCommittee. @@ -230,3 +234,105 @@ upperLoop: transferTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) } } + +// groups parameters of transferGASToProxy. +type transferGASToProxyPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + proxyContract util.Uint160 + + committee keys.PublicKeys + + localAcc *wallet.Account + + tryTransfer bool +} + +// transfers 90% of GAS from committee multi-sig account to the Proxy contract. +// No-op if Proxy contract already has GAS. +func transferGASToProxy(ctx context.Context, prm transferGASToProxyPrm) error { + var committeeMultiSigAccAddress util.Uint160 + + committeeActor, err := newCommitteeNotaryActorWithCustomCommitteeSigner(prm.blockchain, prm.localAcc, prm.committee, func(s *transaction.Signer) { + committeeMultiSigAccAddress = s.Account + s.Scopes = transaction.CalledByEntry + }) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) + } + + // wrap the parent context into the context of the current function so that + // transaction wait routines do not leak + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + gasContract := gas.New(committeeActor) + transferTxMonitor := newTransactionGroupMonitor(committeeActor) + + for ; ; prm.monitor.waitForNextBlock(ctx) { + select { + case <-ctx.Done(): + return fmt.Errorf("wait for distribution of initial funds: %w", ctx.Err()) + default: + } + + proxyBalance, err := gasContract.BalanceOf(prm.proxyContract) + if err != nil { + prm.logger.Error("failed to get GAS balance of the Proxy contract, will try again later", + zap.Error(err)) + continue + } + + if proxyBalance.Sign() > 0 { + prm.logger.Info("Proxy contract already has GAS, skip transfer") + return nil + } + + if !prm.tryTransfer { + prm.logger.Info("GAS balance of the Proxy contract is empty but attempts to transfer are disabled, will wait for a leader") + continue + } + + committeeBalance, err := gasContract.BalanceOf(committeeMultiSigAccAddress) + if err != nil { + prm.logger.Error("failed to get GAS balance of the committee multi-sig account, will try again later", + zap.Error(err)) + continue + } + + amount := new(big.Int).Mul(committeeBalance, big.NewInt(initialProxyGASPercent)) + amount.Div(amount, big.NewInt(100)) + if amount.Sign() <= 0 { + prm.logger.Info("nothing to transfer from the committee multi-sig account, skip") + return nil + } + + if transferTxMonitor.isPending() { + prm.logger.Info("previously sent transaction transferring funds from committee multi-sig account to the Proxy contract is still pending, will wait for the outcome") + continue + } + + prm.logger.Info("sending new Notary request transferring funds from committee multi-sig account to the Proxy contract...") + + mainTxID, fallbackTxID, vub, err := committeeActor.Notarize( + gasContract.TransferTransaction(committeeMultiSigAccAddress, prm.proxyContract, amount, nil)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to transfer funds from committee multi-sig account to the Proxy contract, will try again later") + } else { + prm.logger.Error("failed to send Notary request transferring funds from committee multi-sig account to the Proxy contract, will try again later", zap.Error(err)) + } + continue + } + + prm.logger.Info("Notary request transferring funds from committee multi-sig account to the Proxy contract has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + transferTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } +} From 9c3e328d111f07eb692a14f187051b30ca86c5bf Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Wed, 23 Aug 2023 23:16:48 +0400 Subject: [PATCH 14/22] sidechain/deploy: Fix and improve Notary request re-signing routine Prevent double processing. Support different number and order of signers. Skip transactions that have already been signed by the local account. Log main transaction hash. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/notary.go | 183 ++++++++++++++++++++++++++----------- 1 file changed, 131 insertions(+), 52 deletions(-) diff --git a/pkg/morph/deploy/notary.go b/pkg/morph/deploy/notary.go index 0d3ca43e35..26acd62bd0 100644 --- a/pkg/morph/deploy/notary.go +++ b/pkg/morph/deploy/notary.go @@ -16,6 +16,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "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/neorpc" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" @@ -1027,6 +1028,13 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar return fmt.Errorf("compose committee multi-signature account: %w", err) } + ver, err := prm.blockchain.GetVersion() + if err != nil { + return fmt.Errorf("read protocol configuration: %w", err) + } + + netMagic := ver.Protocol.Network + localAccID := prm.localAcc.ScriptHash() validatorMultiSigAccID := prm.validatorMultiSigAcc.ScriptHash() committeeMultiSigAccID := committeeMultiSigAcc.ScriptHash() chNotaryRequests := make(chan *result.NotaryRequestEvent, 100) // secure from blocking @@ -1050,6 +1058,7 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar prm.logger.Info("listening to committee notary requests...") + upperLoop: for { select { case <-ctx.Done(): @@ -1064,115 +1073,185 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar // for simplicity, requests are handled one-by one. We could process them in parallel // using worker pool, but actions seem to be relatively lightweight - const expectedSignersCount = 3 // sender + committee|validator + Notary mainTx := notaryEvent.NotaryRequest.MainTransaction // note: instruction above can throw NPE and it's ok to panic: we confidently // expect that only non-nil pointers will come from the channel (NeoGo // guarantees) srcMainTxHash := mainTx.Hash() + l := prm.logger.With(zap.Stringer("tx", srcMainTxHash)) _, processed := mProcessedMainTxs[srcMainTxHash] + if processed { + l.Info("main transaction of the notary request has already been processed, skip") + continue + } + + mProcessedMainTxs[srcMainTxHash] = struct{}{} // revise severity level of the messages // https://github.com/nspcc-dev/neofs-node/issues/2419 switch { - case processed: - prm.logger.Info("main transaction of the notary request has already been processed, skip", - zap.Stringer("ID", srcMainTxHash)) - continue case notaryEvent.Type != mempoolevent.TransactionAdded: - prm.logger.Info("unsupported type of the notary request event, skip", + l.Info("unsupported type of the notary request event, skip", zap.Stringer("got", notaryEvent.Type), zap.Stringer("expect", mempoolevent.TransactionAdded)) continue - case len(mainTx.Signers) != expectedSignersCount: - prm.logger.Info("unsupported number of signers of main transaction from the received notary request, skip", - zap.Int("expected", expectedSignersCount), zap.Int("got", len(mainTx.Signers))) + case len(mainTx.Scripts) != len(mainTx.Signers): + l.Info("different number of signers and scripts of main transaction from the received notary request, skip") continue - case !mainTx.Signers[1].Account.Equals(committeeMultiSigAccID): - if !mainTx.Signers[1].Account.Equals(validatorMultiSigAccID) { - prm.logger.Info("2nd signer of main transaction from the received notary request is neither committee nor validator multi-sig account, skip") - continue + case len(mainTx.Signers) == 0 || !mainTx.Signers[len(mainTx.Signers)-1].Account.Equals(notary.Hash): + l.Info("Notary contract is not the last signer of main transaction from the received notary request, skip") + continue + } + + localAccSignerIndex := -1 + committeeMultiSigSignerIndex := -1 + validatorMultiSigSignerIndex := -1 + notaryContractSignerIndex := -1 + + for i := range mainTx.Signers { + switch mainTx.Signers[i].Account { + case notary.Hash: + notaryContractSignerIndex = i + case localAccID: + if len(mainTx.Scripts[i].InvocationScript) > 0 { + l.Info("main transaction from the received notary request already has local account's signature, skip") + continue upperLoop // correctness doesn't matter + } + + localAccSignerIndex = i + case committeeMultiSigAccID: + // simplified: we know binary format, so may match faster + if bytes.Contains(mainTx.Scripts[i].InvocationScript, committeeMultiSigAcc.SignHashable(netMagic, mainTx)) { + l.Info("main transaction from the received notary request already has local account's committee signature, skip") + continue upperLoop // correctness doesn't matter + } + + // we cannot differ missing signature from the incorrect one in this case + + committeeMultiSigSignerIndex = i + case validatorMultiSigAccID: + // simplified: we know binary format, so may match faster + if bytes.Contains(mainTx.Scripts[i].InvocationScript, prm.validatorMultiSigAcc.SignHashable(netMagic, mainTx)) { + l.Info("main transaction from the received notary request already has local account's committee signature, skip") + continue upperLoop // correctness doesn't matter + } + + // we cannot differ missing signature from the incorrect one in this case + + validatorMultiSigSignerIndex = i } - case mainTx.HasSigner(prm.localAcc.ScriptHash()): - prm.logger.Info("main transaction from the received notary request is signed by a local account, skip") + } + + if notaryContractSignerIndex < 0 { + l.Info("Notary contract is not a signer of main transaction of the received notary request, skip") continue - case len(mainTx.Scripts) == 0: - prm.logger.Info("missing scripts of main transaction from the received notary request, skip") + } + + if localAccSignerIndex < 0 && committeeMultiSigSignerIndex < 0 && validatorMultiSigSignerIndex < 0 { + l.Info("local account is not a signer of main transaction of the received notary request, skip") continue } - var payerAcc *wallet.Account + signers := make([]actor.SignerAccount, 0, len(mainTx.Signers)-1) // Notary contract added by actor - bSenderKey, ok := vm.ParseSignatureContract(mainTx.Scripts[0].VerificationScript) - if ok { - senderKey, err := keys.NewPublicKeyFromBytes(bSenderKey, elliptic.P256()) - if err != nil { - prm.logger.Info("failed to decode sender's public key from first script of main transaction from the received notary request, skip", zap.Error(err)) + for i := range mainTx.Signers { + if i == notaryContractSignerIndex { continue } - payerAcc = notary.FakeSimpleAccount(senderKey) - } else { - payerAcc = notary.FakeContractAccount(mainTx.Signers[0].Account) + var acc *wallet.Account + switch i { + case localAccSignerIndex: + acc = prm.localAcc + case committeeMultiSigSignerIndex: + acc = committeeMultiSigAcc + case validatorMultiSigSignerIndex: + acc = prm.validatorMultiSigAcc + default: + if len(mainTx.Scripts[i].VerificationScript) > 0 { + if bSenderKey, ok := vm.ParseSignatureContract(mainTx.Scripts[i].VerificationScript); ok { + senderKey, err := keys.NewPublicKeyFromBytes(bSenderKey, elliptic.P256()) + if err != nil { + l.Info("failed to decode public key from simple signature contract verification script of main transaction from the received notary request, skip", + zap.Int("script#", i), zap.Error(err)) + continue + } + + acc = notary.FakeSimpleAccount(senderKey) + } else if m, bKeys, ok := vm.ParseMultiSigContract(mainTx.Scripts[i].VerificationScript); ok { + pKeys := make(keys.PublicKeys, len(bKeys)) + for j := range bKeys { + err := pKeys[j].DecodeBytes(bKeys[j]) + if err != nil { + l.Info("failed to decode public key from multi-sig contract verification script of main transaction from the received notary request, skip", + zap.Int("script#", i), zap.Int("key#", j), zap.Error(err)) + continue + } + } + + acc, err = notary.FakeMultisigAccount(m, pKeys) + if err != nil { + l.Info("failed to build fake multi-sig account from verification script of main transaction from the received notary request, skip", + zap.Int("script#", i), zap.Error(err)) + continue + } + } else { + l.Info("got invalid/unsupported verification script in main transaction from the received notary request, skip", + zap.Int("script#", i)) + continue upperLoop + } + } else { + acc = notary.FakeContractAccount(mainTx.Signers[i].Account) + } + } + + signers = append(signers, actor.SignerAccount{ + Signer: mainTx.Signers[i], + Account: acc, + }) } // copy transaction to avoid pointer mutation mainTxCp := *mainTx - mainTxCp.Scripts = nil - mainTx = &mainTxCp // source one isn't needed anymore // it'd be safer to get into the transaction and analyze what it is trying to do. // For simplicity, now we blindly sign it. Track https://github.com/nspcc-dev/neofs-node/issues/2430 - prm.logger.Info("signing main transaction from the received notary request by the local account...") + l.Info("signing main transaction from the received notary request by the local account...") - var multiSigAcc *wallet.Account - if mainTx.Signers[1].Account.Equals(committeeMultiSigAccID) { - multiSigAcc = committeeMultiSigAcc - } else { - // note: switch-case above denies any other cases - multiSigAcc = prm.validatorMultiSigAcc - } + // reset all existing script because Notary actor adds itself + mainTx.Scripts = nil // create new actor for current signers. As a slight optimization, we could also // compare with signers of previously created actor and deduplicate. // See also https://github.com/nspcc-dev/neofs-node/issues/2314 - notaryActor, err := notary.NewActor(prm.blockchain, []actor.SignerAccount{ - { - Signer: mainTx.Signers[0], - Account: payerAcc, - }, - { - Signer: mainTx.Signers[1], - Account: multiSigAcc, - }, - }, prm.localAcc) + notaryActor, err := notary.NewActor(prm.blockchain, signers, prm.localAcc) if err != nil { // not really expected - prm.logger.Error("failed to init Notary request sender with signers from the main transaction of the received notary request", zap.Error(err)) + l.Error("failed to init Notary request sender with signers from the main transaction of the received notary request", zap.Error(err)) continue } err = notaryActor.Sign(mainTx) if err != nil { - prm.logger.Error("failed to sign main transaction from the received notary request by the local account, skip", zap.Error(err)) + l.Error("failed to sign main transaction from the received notary request by the local account, skip", zap.Error(err)) continue } - prm.logger.Info("sending new notary request with the main transaction signed by the local account...") + l.Info("sending new notary request with the main transaction signed by the local account...") _, _, _, err = notaryActor.Notarize(mainTx, nil) if err != nil { if errors.Is(err, neorpc.ErrInsufficientFunds) { - prm.logger.Info("insufficient Notary balance to send new Notary request with the main transaction signed by the local account, skip") + l.Info("insufficient Notary balance to send new Notary request with the main transaction signed by the local account, skip") } else { - prm.logger.Error("failed to send new Notary request with the main transaction signed by the local account, skip", zap.Error(err)) + l.Error("failed to send new Notary request with the main transaction signed by the local account, skip", zap.Error(err)) } continue } - prm.logger.Info("main transaction from the received notary request has been successfully signed and sent by the local account") + l.Info("main transaction from the received notary request has been successfully signed and sent by the local account") } } }() From cd53fb01f927851151b6b44dfb19422a8a07ed81 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Wed, 23 Aug 2023 23:20:11 +0400 Subject: [PATCH 15/22] sidechain/deploy: Register Alphabet members as candidates to validators All NeoFS Alphabet (i.e. committee) members must be registered as candidates to Sidechain validators. This is vital for NeoFS network processing. Extended Sidechain deployment procedure with registration stage. It sends one transaction witnessed by committee multi-sig (to temporary minimize registration price) and each individual Alphabet member (required by Neo contract). Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/alphabet.go | 201 +++++++++++++++++++++++++++++++++++ pkg/morph/deploy/deploy.go | 18 +++- pkg/morph/deploy/notary.go | 15 +-- 3 files changed, 227 insertions(+), 7 deletions(-) diff --git a/pkg/morph/deploy/alphabet.go b/pkg/morph/deploy/alphabet.go index 7a1e7004a3..4ddd5fab88 100644 --- a/pkg/morph/deploy/alphabet.go +++ b/pkg/morph/deploy/alphabet.go @@ -6,9 +6,15 @@ import ( "fmt" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "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/neorpc" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" "github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" "go.uber.org/zap" ) @@ -95,3 +101,198 @@ func initAlphabet(ctx context.Context, prm initAlphabetPrm) error { txMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) } } + +// groups parameters of initVoteForAlphabet. +type initVoteForAlphabetPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + committee keys.PublicKeys + localAcc *wallet.Account + + // pays for Notary transactions + proxyContract util.Uint160 +} + +// initializes vote for NeoFS Alphabet members for the role of validators. +func initVoteForAlphabet(ctx context.Context, prm initVoteForAlphabetPrm) error { + committeeActor, err := newProxyCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee, prm.proxyContract) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) + } + + roleContract := rolemgmt.NewReader(committeeActor) + + alphabet, err := roleContract.GetDesignatedByRole(noderoles.NeoFSAlphabet, prm.monitor.currentHeight()) + if err != nil { + return fmt.Errorf("request NeoFS Alphabet members: %w", err) + } + + if len(alphabet) == 0 { + return errors.New("no NeoFS Alphabet members are set") + } + + // wrap the parent context into the context of the current function so that + // transaction wait routines do not leak + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + neoContract := neo.New(committeeActor) + txMonitor := newTransactionGroupMonitor(committeeActor) + mRegisteredAlphabetIndices := make(map[int]struct{}, len(alphabet)) + var originalPrice int64 + scriptBuilder := smartcontract.NewBuilder() + setRegisterPrice := func(price int64) { scriptBuilder.InvokeMethod(neo.Hash, "setRegisterPrice", price) } + +mainLoop: + for ; ; prm.monitor.waitForNextBlock(ctx) { + select { + case <-ctx.Done(): + return fmt.Errorf("wait for NeoFS Alphabet to be registered as candidates to validators: %w", ctx.Err()) + default: + } + + prm.logger.Info("checking registered candidates to validators...") + + iterCandidates, err := neoContract.GetAllCandidates() + if err != nil { + prm.logger.Error("init iterator over registered candidates to validators, will try again later", zap.Error(err)) + continue + } + + for k := range mRegisteredAlphabetIndices { + delete(mRegisteredAlphabetIndices, k) + } + + for { + candidates, err := iterCandidates.Next(len(alphabet) - len(mRegisteredAlphabetIndices)) + if err != nil { + prm.logger.Error("get next list of registered candidates to validators, will try again later", zap.Error(err)) + continue mainLoop + } + + if len(candidates) == 0 { + break + } + + loop: + for i := range alphabet { + if _, ok := mRegisteredAlphabetIndices[i]; ok { + continue + } + + for j := range candidates { + if candidates[j].PublicKey.Equal(alphabet[i]) { + mRegisteredAlphabetIndices[i] = struct{}{} + if len(mRegisteredAlphabetIndices) == len(alphabet) { + break loop + } + continue loop + } + } + } + } + + err = iterCandidates.Terminate() + if err != nil { + prm.logger.Info("failed to terminate iterator over registered candidates to validators, ignore", zap.Error(err)) + } + + if len(mRegisteredAlphabetIndices) == len(alphabet) { + prm.logger.Info("all NeoFS Alphabet members are registered as candidates to validators") + return nil + } + + prm.logger.Info("not all members of the NeoFS Alphabet are candidates to validators, registration is needed") + + if txMonitor.isPending() { + prm.logger.Info("previously sent Notary request registering NeoFS Alphabet members as candidates to validators is still pending, will wait for the outcome") + continue + } + + originalPrice, err = neoContract.GetRegisterPrice() + if err != nil { + prm.logger.Info("failed to get original candidate registration price, will try again later", + zap.Error(err)) + continue + } + + scriptBuilder.Reset() + + const minPrice = 1 // 0 is forbidden + if originalPrice > minPrice { + setRegisterPrice(minPrice) + } + + for i := range alphabet { + if _, ok := mRegisteredAlphabetIndices[i]; ok { + continue + } + + prm.logger.Info("NeoFS Alphabet member is not yet a candidate to validators, going to register", + zap.Stringer("member", alphabet[i])) + + scriptBuilder.InvokeWithAssert(neo.Hash, "registerCandidate", alphabet[i].Bytes()) + } + + if originalPrice > minPrice { + setRegisterPrice(originalPrice) + } + + script, err := scriptBuilder.Script() + if err != nil { + prm.logger.Info("failed to build script registering NeoFS Alphabet members as validators, will try again later", + zap.Error(err)) + continue + } + + candidateSigners := make([]actor.SignerAccount, 0, len(alphabet)-len(mRegisteredAlphabetIndices)) + + for i := range alphabet { + if _, ok := mRegisteredAlphabetIndices[i]; ok { + continue + } + + var acc *wallet.Account + if alphabet[i].Equal(prm.localAcc.PublicKey()) { + acc = prm.localAcc + } else { + acc = notary.FakeSimpleAccount(alphabet[i]) + } + candidateSigners = append(candidateSigners, actor.SignerAccount{ + Signer: transaction.Signer{ + Account: alphabet[i].GetScriptHash(), + Scopes: transaction.CustomContracts, + AllowedContracts: []util.Uint160{neo.Hash}, + }, + Account: acc, + }) + } + + curActor, err := newProxyCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee, prm.proxyContract, candidateSigners...) + if err != nil { + prm.logger.Error("failed to make Notary actor with candidate signers, will try again later", + zap.Error(err)) + continue + } + + mainTxID, fallbackTxID, vub, err := curActor.Notarize(curActor.MakeRun(script)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to send new Notary request registering NeoFS Alphabet members as validators, skip") + } else { + prm.logger.Error("failed to send new Notary request registering NeoFS Alphabet members as validators, skip", zap.Error(err)) + } + continue + } + + prm.logger.Info("Notary request registering NeoFS Alphabet members as validators has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + txMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } +} diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index 3e91d2bf99..b6cb2f2d09 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -185,7 +185,7 @@ type Prm struct { // 2. launch of a notary service for the committee // 3. initial GAS distribution between committee members // 4. committee group initialization -// 5. Alphabet initialization +// 5. Alphabet initialization (incl. registration as candidates to validators) // 6. deployment/update of the NeoFS system contracts // 7. deployment of custom contracts (currently not supported) // @@ -462,6 +462,22 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("Proxy balance successfully replenished") + prm.Logger.Info("initializing vote for NeoFS Alphabet members to role of validators...") + + err = initVoteForAlphabet(ctx, initVoteForAlphabetPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + committee: committee, + localAcc: prm.LocalAccount, + proxyContract: proxyContractAddress, + }) + if err != nil { + return fmt.Errorf("init vote for NeoFS Alphabet members to role of validators: %w", err) + } + + prm.Logger.Info("vote for NeoFS Alphabet to role of validators successfully initialized") + // NNS (update) // // Special contract which is always deployed first, but its update depends on diff --git a/pkg/morph/deploy/notary.go b/pkg/morph/deploy/notary.go index 26acd62bd0..d969989775 100644 --- a/pkg/morph/deploy/notary.go +++ b/pkg/morph/deploy/notary.go @@ -864,17 +864,17 @@ func newCommitteeNotaryActorWithCustomCommitteeSigner( // returns notary.Actor that builds and sends Notary service requests witnessed // by the specified committee members to the provided Blockchain. Local account // should be one of the committee members. Given Proxy contract pays for main -// transactions. -func newProxyCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee keys.PublicKeys, proxyContract util.Uint160) (*notary.Actor, error) { +// transactions. Allows to specify extra transaction signers. +func newProxyCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee keys.PublicKeys, proxyContract util.Uint160, extraSigners ...actor.SignerAccount) (*notary.Actor, error) { return _newCustomCommitteeNotaryActor(b, localAcc, committee, notary.FakeContractAccount(proxyContract), func(s *transaction.Signer) { s.Scopes = transaction.CalledByEntry - }) + }, extraSigners...) } // returns notary.Actor builds and sends Notary service requests witnessed by // the specified committee members to the provided Blockchain. Local account // should be one of the committee members. Specified account pays for -// main transactions. +// main transactions. Allows to specify extra transaction signers. // // Transaction signer callback allows to specify committee signer (e.g. tune // witness scope). Instance passed to it has Account set to multi-signature @@ -888,6 +888,7 @@ func _newCustomCommitteeNotaryActor( committee keys.PublicKeys, payerAcc *wallet.Account, fCommitteeSigner func(*transaction.Signer), + extraSigners ...actor.SignerAccount, ) (*notary.Actor, error) { committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(committee)) committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(localAcc.PrivateKey()) @@ -906,7 +907,7 @@ func _newCustomCommitteeNotaryActor( fCommitteeSigner(&committeeSignerAcc.Signer) - return notary.NewActor(b, []actor.SignerAccount{ + signers := []actor.SignerAccount{ { Signer: transaction.Signer{ Account: payerAcc.ScriptHash(), @@ -915,7 +916,9 @@ func _newCustomCommitteeNotaryActor( Account: payerAcc, }, committeeSignerAcc, - }, localAcc) + } + + return notary.NewActor(b, append(signers, extraSigners...), localAcc) } // Amount of GAS for the single local account's GAS->Notary transfer. Relatively From b5790929f3b9c3ca61b7b05ad1d181d044377fd1 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 24 Aug 2023 13:29:56 +0400 Subject: [PATCH 16/22] sidechain/deploy: Distribute all available NEO between Alphabet contracts Make Sidechain deployment procedure to evenly distribute all available NEO tokens between deployed NeoFS Alphabet contracts. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/deploy.go | 23 +++++++- pkg/morph/deploy/funds.go | 118 +++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index b6cb2f2d09..b440cf10b6 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -187,7 +187,7 @@ type Prm struct { // 4. committee group initialization // 5. Alphabet initialization (incl. registration as candidates to validators) // 6. deployment/update of the NeoFS system contracts -// 7. deployment of custom contracts (currently not supported) +// 7. distribution of all available NEO between the Alphabet contracts // // See project documentation for details. func Deploy(ctx context.Context, prm Prm) error { @@ -680,6 +680,8 @@ func Deploy(ctx context.Context, prm Prm) error { return []interface{}{notaryDisabledExtraUpdateArg}, nil } + var alphabetContracts []util.Uint160 + for ind := 0; ind < len(committee) && ind < glagolitsa.Size; ind++ { syncPrm.tryDeploy = ind == localAccCommitteeIndex // each member deploys its own Alphabet contract syncPrm.domainName = calculateAlphabetContractAddressDomain(ind) @@ -703,8 +705,27 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("Alphabet contract successfully synchronized", zap.Int("index", ind), zap.Stringer("address", alphabetContractAddress)) + + alphabetContracts = append(alphabetContracts, alphabetContractAddress) } + prm.Logger.Info("distributing NEO to the Alphabet contracts...") + + err = distributeNEOToAlphabetContracts(ctx, distributeNEOToAlphabetContractsPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + proxyContract: proxyContractAddress, + committee: committee, + localAcc: prm.LocalAccount, + alphabetContracts: alphabetContracts, + }) + if err != nil { + return fmt.Errorf("distribute NEO to the Alphabet contracts: %w", err) + } + + prm.Logger.Info("NEO distribution to the Alphabet contracts successfully completed") + return nil } diff --git a/pkg/morph/deploy/funds.go b/pkg/morph/deploy/funds.go index 440c790720..b6d62cc803 100644 --- a/pkg/morph/deploy/funds.go +++ b/pkg/morph/deploy/funds.go @@ -336,3 +336,121 @@ func transferGASToProxy(ctx context.Context, prm transferGASToProxyPrm) error { transferTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) } } + +// groups parameters of distributeNEOToAlphabetContracts. +type distributeNEOToAlphabetContractsPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + proxyContract util.Uint160 + + committee keys.PublicKeys + + localAcc *wallet.Account + + alphabetContracts []util.Uint160 +} + +// distributes all available NEO between NeoFS Alphabet members evenly. +func distributeNEOToAlphabetContracts(ctx context.Context, prm distributeNEOToAlphabetContractsPrm) error { + committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(prm.committee)) + committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) + + err := committeeMultiSigAcc.ConvertMultisig(committeeMultiSigM, prm.committee) + if err != nil { + return fmt.Errorf("compose committee multi-signature account: %w", err) + } + + committeeMultiSigAccID := committeeMultiSigAcc.ScriptHash() + + committeeActor, err := notary.NewActor(prm.blockchain, []actor.SignerAccount{ + { + Signer: transaction.Signer{ + Account: prm.proxyContract, + Scopes: transaction.None, + }, + Account: notary.FakeContractAccount(prm.proxyContract), + }, + { + Signer: transaction.Signer{ + Account: committeeMultiSigAccID, + Scopes: transaction.CalledByEntry, + }, + Account: committeeMultiSigAcc, + }, + }, prm.localAcc) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee and paid by Proxy contract: %w", err) + } + + // wrap the parent context into the context of the current function so that + // transaction wait routines do not leak + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + neoContract := neo.NewReader(committeeActor) + scriptBuilder := smartcontract.NewBuilder() + transfer := func(to util.Uint160, amount *big.Int) { + scriptBuilder.InvokeWithAssert(neo.Hash, "transfer", committeeMultiSigAccID, to, amount, nil) + } + txMonitor := newTransactionGroupMonitor(committeeActor) + + for ; ; prm.monitor.waitForNextBlock(ctx) { + select { + case <-ctx.Done(): + return fmt.Errorf("wait for distribution of NEO between Alphabet contracts: %w", ctx.Err()) + default: + } + + bal, err := neoContract.BalanceOf(committeeMultiSigAccID) + if err != nil { + prm.logger.Error("failed to get NEO balance of the committee multi-sig account", zap.Error(err)) + continue + } + + if bal.Sign() <= 0 { + prm.logger.Error("no NEO on the committee multi-sig account, nothing to transfer, skip") + return nil + } + + prm.logger.Info("have available NEO on the committee multi-sig account, going to transfer to the Alphabet contracts", + zap.Stringer("balance", bal)) + + singleAmount := new(big.Int).Div(bal, big.NewInt(int64(len(prm.alphabetContracts)))) + + scriptBuilder.Reset() + + for i := range prm.alphabetContracts { + prm.logger.Info("going to transfer NEO from the committee multi-sig account to the Alphabet contract", + zap.Stringer("contract", prm.alphabetContracts[i]), zap.Stringer("", singleAmount)) + transfer(prm.alphabetContracts[i], singleAmount) + bal.Sub(bal, singleAmount) + } + + script, err := scriptBuilder.Script() + if err != nil { + prm.logger.Info("failed to build script transferring Neo from committee multi-sig account to the Alphabet contracts, will try again later", + zap.Error(err)) + continue + } + + mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(committeeActor.MakeRun(script)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to transfer Neo from committee multi-sig account to the Alphabet contracts, skip") + } else { + prm.logger.Error("failed to send new Notary request transferring Neo from committee multi-sig account to the Alphabet contracts, skip", zap.Error(err)) + } + continue + } + + prm.logger.Info("Notary request transferring Neo from committee multi-sig account to the Alphabet contracts has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + txMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } +} From 1707a7c8c2d0a99f5e01aeb4dd252a43419acd65 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Fri, 25 Aug 2023 16:26:03 +0400 Subject: [PATCH 17/22] ir: Run Sidechain auto-deployment procedure in local consensus launch Make NeoFS Inner Ring application to perform Sidechain auto-deployment (incl. contracts' update) on each run in local consensus mode. Embedded contracts are built from v0.18.0 revision of NeoFS Contracts with updated NNS behavior (TLDs owned by the committee). Refs #2195. Signed-off-by: Leonard Lyubich --- CHANGELOG.md | 1 + config/example/ir.yaml | 42 +++ contracts/00-nns.manifest.json | 2 +- contracts/00-nns.nef | Bin 6114 -> 7072 bytes contracts/01-audit.manifest.json | 1 + contracts/01-audit.nef | Bin 0 -> 1469 bytes contracts/02-balance.manifest.json | 1 + contracts/02-balance.nef | Bin 0 -> 2512 bytes contracts/03-proxy.manifest.json | 1 + contracts/03-proxy.nef | Bin 0 -> 794 bytes contracts/04-reputation.manifest.json | 1 + contracts/04-reputation.nef | Bin 0 -> 1404 bytes contracts/05-neofsid.manifest.json | 1 + contracts/05-neofsid.nef | Bin 0 -> 1791 bytes contracts/06-container.manifest.json | 1 + contracts/06-container.nef | Bin 0 -> 5852 bytes contracts/07-netmap.manifest.json | 1 + contracts/07-netmap.nef | Bin 0 -> 4124 bytes contracts/08-alphabet.manifest.json | 1 + contracts/08-alphabet.nef | Bin 0 -> 4088 bytes pkg/innerring/config.go | 217 +++++++++++ pkg/innerring/config_test.go | 336 ++++++++++++++++-- pkg/innerring/contracts.go | 42 +++ pkg/innerring/deploy.go | 143 ++++++++ pkg/innerring/deploy_test.go | 66 ++++ pkg/innerring/innerring.go | 100 +++++- .../internal/blockchain/blockchain.go | 16 - pkg/morph/client/nns.go | 14 + pkg/util/state/storage.go | 69 +++- pkg/util/state/storage_test.go | 34 +- 30 files changed, 1025 insertions(+), 65 deletions(-) create mode 100755 contracts/01-audit.manifest.json create mode 100755 contracts/01-audit.nef create mode 100755 contracts/02-balance.manifest.json create mode 100755 contracts/02-balance.nef create mode 100755 contracts/03-proxy.manifest.json create mode 100755 contracts/03-proxy.nef create mode 100755 contracts/04-reputation.manifest.json create mode 100755 contracts/04-reputation.nef create mode 100755 contracts/05-neofsid.manifest.json create mode 100755 contracts/05-neofsid.nef create mode 100755 contracts/06-container.manifest.json create mode 100755 contracts/06-container.nef create mode 100755 contracts/07-netmap.manifest.json create mode 100755 contracts/07-netmap.nef create mode 100755 contracts/08-alphabet.manifest.json create mode 100755 contracts/08-alphabet.nef create mode 100644 pkg/innerring/deploy.go create mode 100644 pkg/innerring/deploy_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f940138814..976f40f132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ simple Peapod. - `neofs-lens storage inspect` CLI command (#1336) - `neofs-lens` payload-only flag (#2543) - `neofs-lens meta put` CLI command (#1816) +- Sidechain auto-deployment to the Inner Ring app (#2195) ### Fixed - `neo-go` RPC connection loss handling (#1337) diff --git a/config/example/ir.yaml b/config/example/ir.yaml index 2e6efdb795..19a61d7600 100644 --- a/config/example/ir.yaml +++ b/config/example/ir.yaml @@ -92,6 +92,48 @@ morph: CryptoLib: [0] StdLib: [0] +network_settings: # NeoFS network settings managed in the Netmap contract + epoch_duration: 240 # Time interval (approximate) between two adjacent NeoFS epochs measured in Sidechain blocks. + # Must be an integer in range [1, 18446744073709551615] + max_object_size: 67108864 # [bytes] Maximum size of physically stored NeoFS objects. Note that this applies + # only to objects located on storage nodes: user objects have no restrictions and, if necessary, are sliced. + # Must be an integer in range [1, 18446744073709551615] + require_homomorphic_hashing: true # Toggles the requirement for homomorphic hashing of object payloads. + # Must be 'true' or 'false' + allow_maintenance_mode: true # Toggles permission to transition storage nodes to maintenance state. + # Must be 'true' or 'false' + eigen_trust: + alpha: 0.1 # Alpha parameter of EigenTrust algorithm used in the Reputation system. + # Must be a floating point number in range [0, 1]. + iterations_number: 4 # Number of EigenTrust algorithm iterations to pass in the Reputation system. + # Must be an integer in range [1, 18446744073709551615] + price: # Price settings. NEOFS means NeoFS Balance contract tokens (usually GASe-12). + storage: 100000000 # [NEOFS] Price for 1GB of data paid every epoch by data owner to storage nodes. + # Must be an integer in range [0, 18446744073709551615] + fee: + ir_candidate: 100 # [GASe-8] Contribution from the new candidate to the Inner Ring. Must be non-negative integer + # Must be an integer in range [0, 18446744073709551615] + withdraw: 100000000 # [GASe-8] Fee paid by the user account to; + # - NeoFS Processing contract (if Notary service is enabled in the NeoFS Mainchain) + # - each Alphabet member (otherwise) + # Must be an integer in range [0, 18446744073709551615] + audit: 10000 # [NEOFS] Fee for data audit paid by storage group owner to the auditor (Inner Ring member). + # Must be an integer in range [0, 18446744073709551615] + new_container: 1000 # [NEOFS] Fee for new container paid by creator to each Alphabet member. + # Must be an integer in range [0, 18446744073709551615] + container_domain: 500 # [NEOFS] Fee for container's NNS domain paid by container creator to each Alphabet member. + # Must be a non-negative integer + custom: # Optional list of custom key-value pairs to be set in the network configuration. Forbidden keys: + # [AuditFee, BasicIncomeRate, ContainerAliasFee, ContainerFee, EigenTrustAlpha, EigenTrustIterations, EpochDuration, + # HomomorphicHashingDisabled, InnerRingCandidateFee, MaintenanceModeAllowed, MaxObjectSize, WithdrawFee] + # Note that this list can be extended in the future, so, to avoid potential collision, it is recommended + # to use the most specific keys. + - my_custom_key1=val1 + - my_custom_key2=val2 + +nns: + system_email: usr@domain.io + mainnet: dial_timeout: 5s # Timeout for RPC client connection to mainchain; ignore if mainchain is disabled reconnections_number: 5 # number of reconnection attempts diff --git a/contracts/00-nns.manifest.json b/contracts/00-nns.manifest.json index b858bab519..72b678fe17 100755 --- a/contracts/00-nns.manifest.json +++ b/contracts/00-nns.manifest.json @@ -1 +1 @@ -{"name":"NameService","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":32,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addRecord","offset":2567,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"},{"name":"data","type":"String"}],"returntype":"Void","safe":false},{"name":"balanceOf","offset":568,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"Integer","safe":true},{"name":"decimals","offset":479,"parameters":[],"returntype":"Integer","safe":true},{"name":"deleteRecords","offset":2702,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Void","safe":false},{"name":"getAllRecords","offset":2858,"parameters":[{"name":"name","type":"String"}],"returntype":"InteropInterface","safe":false},{"name":"getPrice","offset":972,"parameters":[],"returntype":"Integer","safe":true},{"name":"getRecords","offset":2659,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Array","safe":true},{"name":"isAvailable","offset":1006,"parameters":[{"name":"name","type":"String"}],"returntype":"Boolean","safe":true},{"name":"ownerOf","offset":501,"parameters":[{"name":"tokenID","type":"ByteArray"}],"returntype":"Hash160","safe":true},{"name":"properties","offset":523,"parameters":[{"name":"tokenID","type":"ByteArray"}],"returntype":"Map","safe":true},{"name":"register","offset":1267,"parameters":[{"name":"name","type":"String"},{"name":"owner","type":"Hash160"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"renew","offset":2026,"parameters":[{"name":"name","type":"String"}],"returntype":"Integer","safe":false},{"name":"resolve","offset":2836,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Array","safe":true},{"name":"roots","offset":866,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"setAdmin","offset":2237,"parameters":[{"name":"name","type":"String"},{"name":"admin","type":"Hash160"}],"returntype":"Void","safe":false},{"name":"setPrice","offset":894,"parameters":[{"name":"price","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setRecord","offset":2371,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"},{"name":"id","type":"Integer"},{"name":"data","type":"String"}],"returntype":"Void","safe":false},{"name":"symbol","offset":473,"parameters":[],"returntype":"String","safe":true},{"name":"tokens","offset":644,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"tokensOf","offset":673,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"InteropInterface","safe":true},{"name":"totalSupply","offset":485,"parameters":[],"returntype":"Integer","safe":true},{"name":"transfer","offset":735,"parameters":[{"name":"to","type":"Hash160"},{"name":"tokenID","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Boolean","safe":false},{"name":"update","offset":386,"parameters":[{"name":"nef","type":"ByteArray"},{"name":"manifest","type":"String"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"updateSOA","offset":2147,"parameters":[{"name":"name","type":"String"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Void","safe":false},{"name":"version","offset":481,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"tokenId","type":"ByteArray"}]}]},"features":{},"groups":[],"permissions":[{"contract":"0xfffdc93764dbaddd97c48f252a53ea4643faa3fd","methods":["update"]},{"contract":"*","methods":["onNEP11Payment"]}],"supportedstandards":["NEP-11"],"trusts":[],"extra":null} \ No newline at end of file +{"name":"NameService","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":32,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addRecord","offset":3224,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"},{"name":"data","type":"String"}],"returntype":"Void","safe":false},{"name":"balanceOf","offset":976,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"Integer","safe":true},{"name":"decimals","offset":791,"parameters":[],"returntype":"Integer","safe":true},{"name":"deleteRecords","offset":3448,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Void","safe":false},{"name":"getAllRecords","offset":3692,"parameters":[{"name":"name","type":"String"}],"returntype":"InteropInterface","safe":true},{"name":"getPrice","offset":1427,"parameters":[],"returntype":"Integer","safe":true},{"name":"getRecords","offset":3364,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Array","safe":true},{"name":"isAvailable","offset":1461,"parameters":[{"name":"name","type":"String"}],"returntype":"Boolean","safe":true},{"name":"ownerOf","offset":813,"parameters":[{"name":"tokenID","type":"ByteArray"}],"returntype":"Hash160","safe":true},{"name":"properties","offset":883,"parameters":[{"name":"tokenID","type":"ByteArray"}],"returntype":"Map","safe":true},{"name":"register","offset":1722,"parameters":[{"name":"name","type":"String"},{"name":"owner","type":"Hash160"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"registerTLD","offset":2397,"parameters":[{"name":"name","type":"String"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Void","safe":false},{"name":"renew","offset":2598,"parameters":[{"name":"name","type":"String"}],"returntype":"Integer","safe":false},{"name":"resolve","offset":3630,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"}],"returntype":"Array","safe":true},{"name":"roots","offset":1321,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"setAdmin","offset":2809,"parameters":[{"name":"name","type":"String"},{"name":"admin","type":"Hash160"}],"returntype":"Void","safe":false},{"name":"setPrice","offset":1349,"parameters":[{"name":"price","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setRecord","offset":2984,"parameters":[{"name":"name","type":"String"},{"name":"typ","type":"Integer"},{"name":"id","type":"Integer"},{"name":"data","type":"String"}],"returntype":"Void","safe":false},{"name":"symbol","offset":785,"parameters":[],"returntype":"String","safe":true},{"name":"tokens","offset":1052,"parameters":[],"returntype":"InteropInterface","safe":true},{"name":"tokensOf","offset":1081,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"InteropInterface","safe":true},{"name":"totalSupply","offset":797,"parameters":[],"returntype":"Integer","safe":true},{"name":"transfer","offset":1143,"parameters":[{"name":"to","type":"Hash160"},{"name":"tokenID","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Boolean","safe":false},{"name":"update","offset":698,"parameters":[{"name":"nef","type":"ByteArray"},{"name":"manifest","type":"String"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"updateSOA","offset":2719,"parameters":[{"name":"name","type":"String"},{"name":"email","type":"String"},{"name":"refresh","type":"Integer"},{"name":"retry","type":"Integer"},{"name":"expire","type":"Integer"},{"name":"ttl","type":"Integer"}],"returntype":"Void","safe":false},{"name":"version","offset":793,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"tokenId","type":"ByteArray"}]}]},"features":{},"groups":[],"permissions":[{"contract":"0xfffdc93764dbaddd97c48f252a53ea4643faa3fd","methods":["update"]},{"contract":"*","methods":["onNEP11Payment"]}],"supportedstandards":["NEP-11"],"trusts":[],"extra":null} \ No newline at end of file diff --git a/contracts/00-nns.nef b/contracts/00-nns.nef index 85c9a45afe66df2182c698a4d0608e424811bee6..43a5a2d80dfb912c9f114a0653f48dbbcbebc353 100755 GIT binary patch delta 2751 zcmb7GeQXTAoIB^EX$hU<1*gl_d^CM$2wH1Pbtc>{T<2t2nSh1m7T@{f zV#kT^nzf!_+J;ggl2a?qWAE_b$nd^Z>255pKHaj)wG4&GhLX+*&YX;>K_c15D=SLe!k zUBtcdw}$)J-w$^uqHhns^xXwP5|PlcCHS`r!eT1A!M(T9p8+4K==+mfpN_v3g~icA zS}x0FXXjE@#Dk8lgb|Y0_pm{tZ(jm-Oe)LkljP%8gamk%j=llUFh+}dS}JD}(LWvR ze&Y7EYPDJqt>`g4B1Vo|ss7*a&|apqWLEl?Oec~eXK+$E{+e|7@V>9ai(bQ1dpEpg zc+?HKj3pm_kYSMPf*;HsQ>2qaNbJe0bJ{{d&8xG4M~qBHD-={UFl%IHv}~a1WF>m> zoqJDj1fPw?Vv%WtO}s(xVMaIxt&EvaO1i`h-=U1}Qo^+__KHe~gCFv*A`oK)suhV> zLTv0@yteux6ZOX{eeTzSY)p~{I8u@kiqKZqm^T8JX&oo`QN@)gDdUl8jG|Y6mbyHj zBpDG_O}j97+yfr>K*5!;3(Y5+zT|-pv8`aF)NCMY6atSLi`m&RdwWhZlL=yhhrBM8 zZ_>+w|KVoTcH=LLylT#C`I&-dWOcbLve}u8>VG9N&0v$r(i%I*{J3q6-A$ijca_{O zljv;-oeg!0l9pYXNo%u#y!wc$Evfl2>(+9z2rnR%bMUJvRkNj#Wxe=Sy--H=^7S);jNjbJV~AhD-Fe;BzN)j zHg_NMb03YlbIg%8`f^Jjz2yD@GvTGBmLAN~jh3C9-(zwLxz`7OhYo4-K%2-<9yt`4 zRkNB3Ww*JN)a37%$*Lcs(^E=x?aS|uR6>5SwYKnji*y;9m47*WLFa&AegdrH-CjQPCJT?S0IsK1nm0bB}wOVX7TBGN`bn2IlSV*@Ap`uSHwtX^X!{_8&lnklTchHbN zk$O>%zIgs=mm&MhgV1$DmdfC%;oNAeHM#UtH$p*Pmlg7@7U;KmeEG2xC#~)&@*ZQ>vik}s0S7y#_Pc;e9?PqcQ-G-LOx$yuxT}Hk zS%Yry{t|f~;GZ)!2L^pRM1KtiGwHlKGiwD@6NG4C%+ws66Y}*_PU_wEIfyfXC3g47 z?;s?>PWuFY*Y(U9tK{j}a*RwE*Z3ah zZ*IDqAMd%wLUbB=>ceU^*~pI!L2F<;eVpIhZ{D2X(C7HO#zAkKSOM7&f#;g+kRM(J zMCiaoO}B$p$RFWXWSV77au7tt*)ne}DmqU;DcgT;~+|tP|TZv<0foG*aFU zs2inyq^|8ISqDfuWy{%0pVu`g`7#_5(>2qYB*&l_k!cS-Duzl&uCE5)H;j>6$lznR zK|pyQ|G-8{g8T%+F2Ja-i_Bui7M9Sij3X-cuv*fyO0+ce;IH2cUbfp{H=enPRBxDK zf@%4oV22?OfDQitg=*9(0bPy<>-5=Ps8PrzrkM)agn&S!X;VSzh1~Hb5~TtEe~hlLoAU7l8MsRC_s#S+^%=-^;R-Yy<~d_EFHPhg z`ljy=j5q(~djMlhr=&>j1|j0W%5wcOQ8ORAZl1Vqo~)Z)R>#&tDvap`N$F@Fw~&JA zj00RE%+X+HKe448VyQyWAF8CeO)`& z_ZVz^&`8q7IPV7&K%6A_VbM<`!5AG1F(jas7!yh~QHdfZ5>d$nB-Ycnc9Zy}J@?#u z&-tBmf4}qQd^gtfyMjA6CWn;nVWqpbr?0nfmfwW!v#)KK{qgwhm4?})p4sypox%s0 zb#?S6ZZR^9=53|bOmkEJ27lnh(r|uE;xGzb?`>nh>E$DVo7);*mgw({i%uCfTM)_( zs94-i7Y)6g;`YX{ujKK^5LOFb3v39V_|?#LKV0^Q3SKsNAyhhD&j#oy>$5I75K^^6 z#=uQv4G$c(7 zr3W-}z=}|++yF65&cHIA;PV+daIgK>nRzd3^~>s;^WODpg5Q1Bo~YS3&C!(YddL(J85G5Q~GE^HS$1I3)=2qqbNBhiU>@2{b->&JPdGiTo-b`gn zQ%9-g0q9y_LQFdIy{yGAzm@UKS^cqEvuX*G84loy1uwnBHPbIF_RSo2mmdlGYz%Tp zl!UkQWmhh(SCWwv;lLaFZuN~z!s9)p*^bZ%owGM|%8ByJw#pcM1+SB-|wA?<~{yIVk>_4OD*Oi-)wJUbMQcSi=sZwmZqpi?p zyzF6vyd~;mP?l3Te#0(>Q*5Ei#wo>4J8V7&T+a=(CX2rfnli_gw{RDnVQMuf7}_AD=RxaCp!N_B3z=&Ya!g|=;q@c-JKfpZs+85E~8-vcyJ zoO~W4Re~#gi5Axk`w{ZkWFbt>u?X4q8%OXh!`vxVt398sk(Fwtrc9?XfFrAiCJ!H( zs^3GthxV^T(|}kr2Iv>BRT9{v1VKm(wR3%Vfo|p2S~tTwogh{BX`E|f=2<$;?QFUN z*Ty6=cdu9!ftC;*g8u@kKr-|PuG^>;+S)Wa#kNfA>`9503}Nz=ewWXapZLl5`>j>RCRj@ z>H_!@i14Tm4l!y-SnzT|jcfu*`8~0aYr)G0HIT*UX1dYQ!Bku6K8MF-0F6Iyqpvu+ zVyhXtmNTGA2G<^v1<0lNA3`1|o>2({L!|;;A3$TJ{W~CzU)ytZGS53e>>UTC(Qsh>GoRh|+$iO< z>Uc0y9)WgMa{^ksJ{S!!)wbw~-MV~It1vrDFhv-cCri&RCFr~Sd)PoDg4oWMo`%u- z@1|5S-2cRw7P_t{=5SBgXGCdLNJQXG>3t#IvZ7_EqU&z4n1JcbXhKDXBuXcpo!GQ+ z#<>?`6Mb0YFAaZ~L%GT2??A~sylNg19KXw(ot~0QWR}gcXQfNaq&4l5g$1T}+frS+&CnDgs2^_0vp!Fn++FUa zyDRFaA!vu9qC+iRN54h=q!dB1-I$7nr3ij#9kMxfy0J2tY>H6h|B_2P#ZTUcyL*2B z-~aXbJ}y`xHRw68xp&=+O?$K(I}makvfz4Q3l%sXFQJrw$#nZw_4c~jG{ zY#fve*kTx#z4F$Nua6vh=#`bHyf+SCs9V`1O9#WF*f@Z7Ix0&BF6^_1l@j&<_jdle z@LA8^!w>k+zdGyx=+=xb+~k0}Un!TVVPNb8T)4TXcKN4kJH9+OQ?!3LaR0JW`@lyH zMS=*-*4yU${LAD-Sh_cUrst&#m*+}aKqt~bd^ZbbhFSa6m-BkW_wJ0=R6C+t=@p^N zcTuQNnqKbmFLwG;LH=|ty|Nw9?OHLu2tG7et5zckZopIvP%ttSBr9^StQeAJqq0)K zi0S|tNHwq%9Nk!op*%ml>J~pW&yW3H~n3L5P(Ri@UCEa(rWF zRU-b;edDB%V_1uwO7_LLh^pans+c+&$C^$R8I`GCmW(_}B5bKR@6`X;ULbGDB)rUI z(qc1%(LLNF8)fTADrs0M*ho2s41!S!=As*|fJp=%u5%;ta-JG~t7rOu@R zTmk}gwbBCv@3GzQDIw=&tzb>HLbZgCsn8(PK6Lt|M$#)$;6=gRpW+ycuepE4$w_FJ zwYWfYS2E%o&;8o3lI2J@*YRI` zJ>0hSh`O22V_iRHmP)px07=`3nPa{27yqi)t58FyA_F1c(lN?QGIG__ij}nNiTJm` z&xRLN?!mR$Klcw_&+gpu+WAx8e|WKw_THp_p+e5XxCV}KF+M9^ihlBLg~m-dgpMMb468!uKPg%&YNY5!)5l`nK=n4M6SftehXilJd~`DAav&U>H4l5AYKc>At5cB?WRce8b4Xfx3*rFT2#Vr;u8=cOc_RQ|=%yecJ zX13sy2@iNlj7n6(CHP_^iHT@@Fj9$^$irfy4@G0V#Gnuf2}V@3uK!%N7X?l1OXvU3 z|DW&suV=UR^{-X1x@tsS)!ntadren2^*{c?Ka1zNcRJQ@xgq@i(?`Q^TsmxU7tshd zH>#zQWSSUL6irWDTvz@2yK~DwJbAcae{=WslWO&~sDX7!kfjQyDVn;PMMyGLApnQ( zUJ9+|ZtL9jUgpJ*&Tek{g?bBrj>NLAW5v8z$z$tU7>#*62Zf@hK8-xkfM_~>rn^1# zMfYQf4)~t}qJ#eD5TZl=r-+savPOg}D9oX;aFhN$KfnAHOm zg}K3CfPeY&NL90wnw37o0SGrLN$JV<@To+oFBU&kO`ll?+GToyT}o4w6swMpCwtMF zvX&Q2%$O>psLCouH3i*fN@^Z6k^w4{G0K{z>L$)Jf?}6c9j6L3+0Yds>9<21zlxEo^%b&e#!4updMej&YCc}M*vi3RBgL9;ZW=S+{X%G7{ z@$(EWG{q{lT1_MTuPc6h(z(K-52U(!P*T(JPDw2r%ucKulBzHz$tVe?NKy>8G%Pwf zxpO@jFUs@=sC_Am&h&S$LpRSP!e9hh$3os_)M3UXn2{mZhFQNb#ej$J>;g|FBcO>- zaP=Aj-B91jG$>DVgCW}LS+Dg5snM8TAk%4kGdZ6Ws)kf-sve?RZV5%X!T~+;wT0$E zWn|SfGW@MOKDZEpQ0dvqXLCQS*2rXgYdsmgAOm_M0h((h8JN6)+I%>xB^oh4E*nepkw}D{Sh|S}of= zp`n(kqgOWepU-wKf9C!DUmp7+-}~`Y4K;W^ZDfES+Vk-JeQ3cy#>cbpQ*J zv?+i2c=1~;M>6bN&fq~c>l!P|PI#)2w*_(ps6-oY+|Iu3>Vv+X-StPUHY%~pfuw5_ z83UW=z(y~)d55T0m<)`uVhm%Q5ex&HMhdmf$x{7TcXonn;g5Mk*n?+d9?ZLtJBzJ= zpxe7+%_j^}DV|JkJN;Rf?FD6y*p;zJ0T?s^15+MPwxXEFKvtbqS61=>-Xg*XIkV;} z1yNMXis>RkkYl@Bx3^A|QD`%GlrsGDv?_3e4YW^W@9%EBED#|Gy4&5p@htyU_uDV& z1^z(f!hvMA!2dLGVu^of6sHG^YUv0i){`#RiR&XJKg$FP+Y$c|rvY2c@>xRKxP0-pl`fUQOk_$b`@U%)-v0JW?jMu+>H-*gjFNGOIY9E5UMiCjv)z!6g%C2u9l( yX|jEx1uI`lpXq>K10C?rxc~RVX(AYLyqs4yVA(Tun)^QR;Kp-qw48lx-_&1&Uw(Z6 literal 0 HcmV?d00001 diff --git a/contracts/03-proxy.manifest.json b/contracts/03-proxy.manifest.json new file mode 100755 index 0000000000..cb27a21ea1 --- /dev/null +++ b/contracts/03-proxy.manifest.json @@ -0,0 +1 @@ +{"name":"NeoFS Notary Proxy","abi":{"methods":[{"name":"_deploy","offset":0,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"onNEP17Payment","offset":314,"parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"update","offset":472,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"verify","offset":601,"parameters":[],"returntype":"Boolean","safe":true},{"name":"version","offset":644,"parameters":[],"returntype":"Integer","safe":true}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file diff --git a/contracts/03-proxy.nef b/contracts/03-proxy.nef new file mode 100755 index 0000000000000000000000000000000000000000..a6a92f8bdaa9965aa0cdbcd5405f186fd00345c6 GIT binary patch literal 794 zcmeZsbu-RO&DTxO*EP^HG%(ULU?2l9eNBGlut&qzmyh*G-(uFypB5B5yyrx$01;`kaZ3P*QKh)p!fW;Z-Sm~MYs1+2YmSyIb7Ausc78Pga=PBf7 z7Uw3GBxhJDq*fH9CYPk9DA?Ho6-xz}Gct5rRWJs)8CddYBOF|;keE}HnwV0lke{Yd zl98&AlUR~kTmm!DN&%}VOBXH$?- zn0Z4@nn&b(*`w|YY&u@t6h7vQUidW_h6=xjsDq5LdH#LBvtbttt zRB}4d93^IsRcabM@@UbJn4FwiP*SYm?ij3)pO;f37%OPBB0E z_dgFCM2~z%h+|T1bdW2L7@A8U^036f1W62>0jA&n{{LT?369TIYC^!|Vfq9i;=thm L3{Hob8~yhH)?XE< literal 0 HcmV?d00001 diff --git a/contracts/04-reputation.manifest.json b/contracts/04-reputation.manifest.json new file mode 100755 index 0000000000..7d79a5661f --- /dev/null +++ b/contracts/04-reputation.manifest.json @@ -0,0 +1 @@ +{"name":"NeoFS Reputation","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":35,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"get","offset":974,"parameters":[{"name":"epoch","type":"Integer"},{"name":"peerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"getByID","offset":990,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"listByEpoch","offset":1080,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Array","safe":true},{"name":"put","offset":864,"parameters":[{"name":"epoch","type":"Integer"},{"name":"peerID","type":"ByteArray"},{"name":"value","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"update","offset":730,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"version","offset":1158,"parameters":[],"returntype":"Integer","safe":false}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file diff --git a/contracts/04-reputation.nef b/contracts/04-reputation.nef new file mode 100755 index 0000000000000000000000000000000000000000..c907e13d8038f1a14c6c0c24ef64fffa4e311375 GIT binary patch literal 1404 zcmbW0Pi)&%9LJv%r%P3XlCDE&Q`NVu%gUf7-D09eD%gw^kpVJvB9o>`xb|yp$FUvz zxpOLULtHw6!~q10Xs2>0~||MBQQq#N9qBNHz@9J}!L$=IpeOAdF7nMHWN zS*fTPBLo0Nt=!tzxbn;OonN0{(rQq+?y22J&j}xVbM3|c zKf!7A10%V%jST#PAtUd<6#8W3AX^o6bskYf2!Qny^lKC|DC(9XP9qGDs@On|10@AX zWjG_MI+9=KO3?^^{O)YSs--QjxW*0Bm$|B{7MsJdiz&L0NG>*tYukx+yREU`5m`0S zSYA%&m_4o~ix@#{Lc=sh4HJuY4OUDULDeB93?0|9OdBH^ibkzs+9d?H@A&ip^Y_54_tNn19WQ%%=_bI5l5S}Lkj z5wWD?A@VFF`3*chL7cY=ISMS~JxZ9&>|-9^DA|FcZX;2yL30LT1wow*#=#xIJR}ie zaFDo}juT6Ah3gEw*i6%f^F?A@^OgDjFx*s&c40RJYt|Sp)XIx#c`9O*GMv4*@Wi$7D^<7NLG>C)LxiKh- z1`OfsXyv`yJo^^$pW3`)F$3!-|K2}&V`}%#cP_mDZI1n9KkxM-Rcmyk?6?1 HJDdIi<#O;$ literal 0 HcmV?d00001 diff --git a/contracts/05-neofsid.manifest.json b/contracts/05-neofsid.manifest.json new file mode 100755 index 0000000000..bfdb0b1b40 --- /dev/null +++ b/contracts/05-neofsid.manifest.json @@ -0,0 +1 @@ +{"name":"NeoFS ID","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":35,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addKey","offset":1030,"parameters":[{"name":"owner","type":"ByteArray"},{"name":"keys","type":"Array"}],"returntype":"Void","safe":false},{"name":"key","offset":1442,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"removeKey","offset":1235,"parameters":[{"name":"owner","type":"ByteArray"},{"name":"keys","type":"Array"}],"returntype":"Void","safe":false},{"name":"update","offset":899,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"version","offset":1513,"parameters":[],"returntype":"Integer","safe":true}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file diff --git a/contracts/05-neofsid.nef b/contracts/05-neofsid.nef new file mode 100755 index 0000000000000000000000000000000000000000..0952dce8be87a299f9f560f9bbef340cfd620e7a GIT binary patch literal 1791 zcmcIkU2NM_6t>f}wAuxdHk-C-Wp`QUg+WW&B~*)4uqi2!0gC8EDXm8bc;zv9n)=mSoJ){y(?IyH=rojf&q#*`ElyL31Nu?Epkno#7 zo%@~ho%5aZYlN)h5Xo?p6oq;~nrwF6(CStz*QK7RKqshtXAO5_m7bD!S|4~h>Tnp@7l{ngc{ zyMN_AK^K5hu`FcJrwkQ2f5)&*MK-dqtmET|XG9#g-q-Ow4|-+Y)Z{5d;T)z0vTdkn zNGZZ;8S6-WNt_6Wq<3%6)J#_}o!pw(7rrQ#G0v^_wV#iNvyteDT5fG0L-$#Q^d#fO zk=o*-Fbsw&rYcheDS-wt`VB&5%Y`MPB8Y7UQD|38lUNj~P&V8Wu~4SKYmW_dYU#lc z$B8VP#CRpU7^w;UAn~tRiqTrLJT8`CPqCp8)*^*M9T_uJgJc>i#GsbUAX~ApNueg& zT1EpA1JROfj{D$~3}bBL($zoZ9T;Xrj3@W(#rQwyeUrb+%U(PT30?5 zY5sKPQ*G=gb@P!C7Q8z@%yaV@htH2CkAMf7YFY1P-9obJ zdQG4jf;yX$O*aTLki7^V`-xYDPKHF6ym|Q3&#emK?3o+`SC~6^CpBUYhq!XYDrilC z9}7FQLe0j-?%D}1_F%Y6EL5%dWe40fCF?q&c3ygD@{1dtOeen?eB;F*22E|XZ@ZRT zOS0i7hugRytr%JI>zVYs$%P!hH;Y2p%`{j#8Wzh-0f}%w=-o=TAI7#I7+E}_F~9D2 z(&>7=z9{Sl+f8Js*qDK{gxOa`)bDD>0nG$n=?>`e$YpFGt7VpkERQe&_BNB-6-8m2 zHC@ql*K@vXwbD=UZRyybUgm=!P7D^HY#alq$Oa6yXoD-`xX0547C+vzWP;fG$=^rE zu8$u&@cP-ezWLyyI(%-u4muid_G-tZsh3}PE(>J$aA31t-Xdq|0+NR~&m)Hejtyr@%rDFvak(;T;zs_c}{^cmAfBd6fXV+nrl?`|j zOqSWGJqtKz-VVrx94!W*tJmf$xCD4U2ySd(HGYASf*mXYoFWxN^^S{YVZX>=1OIO^ z_xWN1^CiH3Y|Iixrrs`2TcqS2JBSU|G-!DyGT6b({DbN}cc^Z7s2ab*w2BjLeCtGP zJsauvN5${VEmD*Blw9TAAH8g{9r3h)PlZAhEeFb&Yga1cTwWuO&=2Adcg8!cm_nCCui zcUd~-e{8?+`+Pso?|I($uJJ>=MHcUv#XCkvc8=~E86`fzCwMXYO6tj>eRpo?c;-uw zbo}s*vr6hUG|Q?7upscN%CZFE@hrb~YyHBXU%ugIPoK@}e>~W;itFDCDXh#hMShO; z5FTQ!6MkOBj2AYadc(gnwRgDslgvXuf9Xi%U&LeV?@+cZ%dDv0A?8@^y<~Lf*fuHG zT`Ye$`~~j5UZ#tFc<6J#yzp32?o>Et7rpnrlt9yr!qOia7qd8LYPuGWkjzW0klVR; z)L=spm-X1k{K!L2oF5(ZzcBhK)Xx+pj+tgvs>-V(t0+{KW3vTnhT)5BjzQ72mZy=Q zO&>U7Gs&i=I=$&{UYe~-`t}$|(I(EPfP1RM^AoFs9nZ!6<79ZTK5_nfSiN4()4%W% z1W(qNmJ(xVS6Rw2Dod%D60x{L#401}lmIqSyaG!qrIaNJ%PO0r7*Q9n%qBTR*s{sV z^oyfE{)SRWF9cs**#1Q@v5Z}V(c!odY5N}$dRzYw0c#$z0kSWvQbksrRXGaJP+2UhG6PFW7P3L5I7Z>X zg`~);0wYbDiywtGkY<_Aq3|?QWW+4nvKetVBYY0hn^*Og-ClRMM--Koji|d3fQ`E` zR)v_oBt+}*J3}u*HlVGBmJBbla#LSkbLo}8eB|q?F>vOThk!4M3uX3@?w??0 zj}QKMH8<9gv%L?8&q#VA+_Gk@a9Spv-~Dn3j{D}uJcOj_oMm=uituPH$D@mf7wpXH=?F;ELDWUP*jIuk^%~w*wl435rmcS zvXNkB!GWIs@uw$XaV0h7_h@_eNn<{uM9O*2bx52f-8a{PjZ z0ub{eRu!u{f?%qrb@3~o{OeAMTOEvXcJ7b)p$JW}DRLR~0q36|f|?uB?eEj3k{{3P zLFSK7m@$H5rstLtemx26eoIcLRri^+Ehcdz>R-z-<}pBKJ0apj9Dv9+jqZh*EG7C- zOk%|xFV0dG449H*RnzZD4Y>l$(g&g;G~fzQ%GoSH-%Pn!)D6gCi^B>n5R!8Z{cq08W;JiXg+Sua_Q;%E!cM-%&S5J&6P7>=b^9{Rkux)F!)Mv&Wk zJi1)nWLO9-L{w&2`Q!lJWTz93E>xrO$GJRTe0t*8%5RR-w}c47uy^|t(1?WuhXd&s z{`(zuy_(o`V$*WRkmJzqBlykzw#vB%)T~IhsOX2`Q z=Eph-sDx^th(b&*cQ*@tFgJkvY*kS=bRTGPrtjOEcfMA=j>FgCfYk-&T~Cw4Tlu;0 zawf7|-BLiE!1XYz58y2h%U)OkOc&W|)8S^H%Fh`z{irKbRwpfJ!Tp)^*F2apMkRe+ z5J~uhDLA13_TnnELrAHDCDDden4h}--DueV9!wm~n?82`-;R<^;J{Wpy`r%SW#}rv z-SsG#Vfizu6C@~K)=(FW%9a#wUYuNCk4Eo^rasa{$z|I^zMM+rJLv*P*p^-y9aAh!uajjP3_^8Fi7-lS&wHzG|adfr=Cc^F#jPedMm)a(E|Tvkr8ObFvKb5JYf%YGdx?&Q2;!~1&G>zl{nUR~(c897N$2$&EYmw#+3K%nu+^0VS)>PGUTPT$ z+z;m8SH1wf@~ol;aR zGqWtrWpE{&k+A@Xt5&gGpy2ibQ_%b*{b@tjx%tRBIS&)P!tn+BA_Kdj*a`YBkb(vj zrUD$BAyHt+qhP+~VM>R2-MV5-IknQvG-BK6phhBJR+QcBfRNdEVXi-z*n&1yWekI7 zlOY8+f*D>*HaV3vo}du9<|vULAJ>&Rc1Zw!*~E z!!TuLfpI&grdd{m=;2&B>)bQ|cInSq3~zE6ZkSZD^a1(79SyJ$o?@;5uDO~7cQq3M z^_rw{x|PXITy-}gnda?M-wTQv_lK);v-+pD1~+@h!4kb@EkN)+-BJrdujT#{Z^3#$ z-wLa;p}GO9`4uVtq5<~aCPOgB>KiZ?491+SHgA#U>qyFd)LG|8^c>jfZPCYdhsFEd zH`M=|x1QMpuLNx!+25Ul1@nmkUI^+^gafn(dpP1+1H8dWK24e%3lM9kuXAIrxit;h z?-T-0_5llcS@8M5(Yjp{@A9u{#ufgZi-0#e!5i;-%5fUB2?|m;ybO1?pdvDo!eKRL zUpd`ZItvR1C*V<_PS(|8gxF@_hAw32;ny3DL|}Y;JeXRD{~o?!%nNkPgQ*ho0o1jR z(sQ+v)4sV{saw{d#Ec8QeM%{;-Y{>x&^4b9V8%0nh%tbud}=8%p$m(>yAHM@p&eqt zFw>k`MYLDunm;+qixo|@@;2UM_}559VMShuFM{UIg1!QJ0r-0UZWK+2w^bC>lnt@) zz+=29CJu9L37)r3JFl9+(|+FsTsP5HDX4nUz923}cXeqnEEzLv{0AxD%8w@IND&Rx90v%-4-<&(1 ztNeX`_Y$rBIGB|*NhM-|OiLcd!#w|vZ)g1eZN{%s-+$?~heFqwpGz;JOj%VWSwARe zC6i%1o-0rPgmozddi@KEETX}>QBrnMRD|0pe z%lyl&{T+Oq<4SuaA;UhW~;SR{f^>=$d=R+IC zf`Y|yN#_ls%aW$?87yV;{DepfQua%zceP^?b!4bKK_-(I2m5@N2FFn#Bg$Elg*e=! zOnkK0oA}{-lQqRmUQB>gqC|RY$)&#b^AX<&tL8!{*#32h8yB;@W2~%-I-zom)$+N} zv-8mi$mc`HYr6Bv&DP4eP)rLayBQ{5)gqTFTfYE-khL{oV;g+3wdgR~RaPu7c%AYx zm3PQg7u5=1q*;k48XW1oR#p_MLc)1bt`w;%#lV8+>kr=N1!Gv)9blL#6wqW*(JqujaV!;61T3(rI7kzZJOzY%-iv8ywUUG{%&-e-;cr&W!c`+2`4dKKnyxrA+PoEdu^`GB zDQ$cHX>c4;&~Pbi1tt<%Qd=H}+{frnS<;K5vK&2;iS9r|HnI56Pkk>w>S2aGjKZ*( zn@tUS8T+7(QE(tN#5RBUQ$ua=!)rDNC3HOeEH|rEqFl?Fb%M=QBDedE0Eo9u5BnI! ztYF7k%i7S-6r6Z}>zhx+M;VXlsm2C|P*hQ+DMHH{KP9Oej2d4gT2a(9IL1q+0(FF; zcyJF0uTKrPGcz&MlR7Z?0J^tXQy3w!ph{x4VwGCQ5?_E})AWYH7!Mk*vVHMV)DI`z zLL5NA?tQn>;cziN>hqXGdlX1Z3Ge_nL!OVW!?~J9@}b%s)4Sali07(mu?~$ znPIRNy$5YvEq+@J7{D!Wq#vI;72RSA+hIIUMc1RXiUf!#Px4a~;3zBUc6wtbYF=Q) zV4?t#p{G=kbfU7%^rKD9E46Ya1F$?%E)*&jeu?VxrBHcc?SHNNwxS4?K_Jth2p#~L zcd@WxD`r-vg*?co!wU-Py?*k8eFv_lx841n7oYj-PcCJLe|No(Ivkx%_-JDMt7DIh zpgUG}wjNZB$dc9s(*o6{y0Bv1TC2p0Wirm@7QZ;fFfQ-aPy&i`VgmzF7O5lkZ zVzwNbd!huBW`@qXXf)XY=rh{p3O7WEa1a!P^B>mhYR;}&G<<{FpsfbOkPMxUe!{Gz zgva0n^{Y~mPD#9`Q&pUlAcw#O6Dlp*{R`0r(p5P@{)zuF)T>!45J}V zQK{9Xyet{cBRdZ(4Vn%tmyF>h1swh@+a3(d89PnzDRgfX8rAkB+~b@uOekL$<~Et%|K2QjyYQO|P!WXZ$? zF!61#S99hZnI$K*TX>OgdZFV&O86t}T{fJq;P(}=cYB2Jzo7@e21z&BfjiN@mq) zxtN>fW~WSU&gi6_@);l;+F6{VooV4Yd%gRS(HVI?$IWW+U!2uRVE{Jg$T8RfStF1$ z2YQG(XZYcv5s36b&ftsx=KI0E(~>k`ZG+}~v=`GZ>Yt@u>7hfkt7>#bu9^^g-U!lQ zVLp2QEz(Yd&Ab~3Q{+$u4OWd{?G}V+_NK7{+N+zI?MWL!ma*o?JVt@1u zK(Xe8Zvehn4C?kEjMgO>Q?StHVm^1xf+^w4?^`$o!1@<>jP3$%g7*k_^|zKq=m)96 zTn&1DAooA86ZyJ6&ESE1r_xqve}dbYo7zvxh6b#Thy)GS$qyVO{>*LO5J^=M3$nx zXpI8athW)^zBJ(d7+GG^)#)7f@P!;WqnSA0sDia8?XoC##hSFiT9Y8;P=9wL!2Dv# zT9#f0wr%sU3yL|m!Ff{vhOiU1k+ddEg{{ke^T;hKk;~X34Ioat4Q{tdSv2Lm`YDiW z`%MVXluY7iT18*|`E)ZPuW7ydtys4kuwlPcDiscP3gib$v0--;aaR%(t+tEbpsRwe QmTS??ySnigHoegEKW6UFIRF3v literal 0 HcmV?d00001 diff --git a/contracts/08-alphabet.manifest.json b/contracts/08-alphabet.manifest.json new file mode 100755 index 0000000000..ec361e6622 --- /dev/null +++ b/contracts/08-alphabet.manifest.json @@ -0,0 +1 @@ +{"name":"NeoFS Alphabet","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":35,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"emit","offset":2900,"parameters":[],"returntype":"Void","safe":false},{"name":"gas","offset":2721,"parameters":[],"returntype":"Integer","safe":true},{"name":"name","offset":3531,"parameters":[],"returntype":"String","safe":true},{"name":"neo","offset":2735,"parameters":[],"returntype":"Integer","safe":true},{"name":"onNEP17Payment","offset":914,"parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"update","offset":2589,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"version","offset":3547,"parameters":[],"returntype":"Integer","safe":true},{"name":"vote","offset":3359,"parameters":[{"name":"epoch","type":"Integer"},{"name":"candidates","type":"Array"}],"returntype":"Void","safe":false}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update","transfer","vote"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file diff --git a/contracts/08-alphabet.nef b/contracts/08-alphabet.nef new file mode 100755 index 0000000000000000000000000000000000000000..2e4bb372af7828f33d4d0acd2e2928058f33ef2f GIT binary patch literal 4088 zcmbVPZ)_W989zHt<0O~l{4Y&f{#>2R$+9+0(|B$@%Ir327q?AKQ^+KQ#KreIcjxoH zm%DS$uC#ARNOT&a0>)U>H0wT05eTHweLx6he*kp?r`i|Nm5H@Og_fd8g_UWdnCHE- zFAjBCMZP%id(S=3^FHtI`8~gznmD{yL3r03-Ze73XJmAEg!(`JI(~V_vnTQRkokK0 zh2Pvb=Km-48hV$`6f_Mf#-zfdB1O4e*T46{cTOGK`mHM`nrE|vthz91s1J&qZJCAgviJUB+MlP_qcj1#mhi^>}J@W0ho`3Jh zSNYhbdzCILiehM7#yD7-nwS^G z)OvsO+hNZH<6A1HuIzx_9hwvkyD3U!$}2087(H50dAMrUz^sCmT?#fht;FV`LaeC6 zmci-;RmB>hf#sA^9&0ErxrBqmZGxDN20Rp%fOtvpoYZqnIkJl$d}u#8U)f2J3J&>m z{`&PGxDu5N2GC{L-ME>*+DQr#oJ^T?xcd6Ww{>t>O(M=t1Wv>JkjbOFnJE>ztFb2_w^VO5p`%;-z zO-Q}X96dVPozkV$dc-sFMoP$)nev*p7``xjNf!8Bm*zv6rTv<4-76LZcNFj@32UMu;Jflg1bJmNIZb2&R zB0y+~3hwl@CZt6z{C<&cfqs^;p{J9(pZ?X~yl_re{pQ)P{bi3TtoMh6)Rhqc@kq== zRl_EjRV$yTt-@07qE(xfh^G`!%rrG6_fIQdP=$GMJ+hq+HX@4ka8ql4N?h+h^nmC6 zLt^U6AVeK(Y*2mpJopJWc~=HrhEX7Bk`=HKt=Iu2D`3Ot{X>!yu#v%k1&r*ahaBE< zE|kk;kZS1ck>oTm0?$rOoM5pcm!hxUt5gy*+ufxAaE)~=wMcKV86}-0l_tg7EzqB< z>zxgBScWhNYE9I46UaqeSsAznza^js`+_uRj4G)yqenLuRB&(~y_e&8=vKW3wKVDb zP0;|pIVDj+szF`+d`C`qKo?DOf(!rX^)YP=|YMgGjIAj=xb5^IAw12e# zSfg7Y8kQGOC~vp2Ay?6}plqZ$^aTAlFY*?fW`SQ=js;G`8vmd)6{}Ameq3SdC~u`QI@f?H}8>B|E!h^1X2vlX|-S}4f;u9S2J>~97ic)`pPxM_Dvf;*gl&zU~VVBfOo$9@PFzyUY)-KaU* zYW=|4j{{3)5O~i`z0*aVYxl33A;#>+-E~eb4!j9Kz#X`|oLqb1n{IO}?!;Shu$pw7 zjy6bIN^L_#nS2M55ayqu&UC*Mv9x>V!!iw!ET@ly7&GmV4Luc-m!X06`-Stl&G!RVC9B^ zd!IPl)Vd&i<^{SLFp?2AG?ArNtqDw9BCYm{HCgO^TddU{R9|RvCGY+$cU=|6-kaYL zJ|fWsMX{_&Rf*G1*=Et7+^tmR#j#5(ks#ewV@UYLIi$s{gJ@E$jnXA42aVJY-4BIs z0JLmD;q{#?Hw$7JS|=_+EsXoMXDwSxZ4_kx@`Az)1?W!To;gk@yn*10DlX-fYTNFi z2Q#=J+vCIGnkvUwL&-skbsMs>WY8l8LzG3M1pabJAsU&TMeq;tO(YwHY4B?FsgHmk zTjZ_hdzN9m)aL9m870i4KOl|RVhhx?|M<}|88mhywn?}RdmkoYtRTWRlP9h2vl3nm z{}iMxe2IK&%9eU&wHh($RrGnQ-#$Y0uOLC@h3IeLM~lAuk4i<)MGwIJ3enTBWfbK> zKhfaU+-EJk#Y max { + err = fmt.Errorf("out of allowable range [%.2f:%.2f]", min, max) + } + } + } + if err != nil { + return res, fmt.Errorf("invalid %s '%s' (boolean): %w", desc, key, err) + } + return res, nil +} + +func parseConfigString(v *viper.Viper, key, desc string) (string, error) { + var res string + var err error + if !v.IsSet(key) { + err = errMissingConfig + } + if err == nil { + res, err = cast.ToStringE(v.Get(key)) + if err == nil && res == "" { + err = errMissingConfig + } + } + if err != nil { + return res, fmt.Errorf("invalid %s '%s' (string): %w", desc, key, err) + } + return res, nil +} diff --git a/pkg/innerring/config_test.go b/pkg/innerring/config_test.go index df5076ea7a..7ffeae9a33 100644 --- a/pkg/innerring/config_test.go +++ b/pkg/innerring/config_test.go @@ -11,13 +11,14 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/blockchain" + "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap" "github.com/spf13/viper" "github.com/stretchr/testify/require" "go.uber.org/zap" ) -// Path of YAML configuration of the IR consensus with all required fields. -const validConfigMinimal = ` +// YAML configuration of the IR consensus with all required fields. +const validBlockchainConfigMinimal = ` morph: consensus: magic: 15405 @@ -29,8 +30,8 @@ morph: path: chain.db ` -// Path of YAML configuration of the IR consensus with all optional fields. -const validConfigOptions = ` +// YAML sub-configuration of the IR consensus with all optional fields. +const validBlockchainConfigOptions = ` time_per_block: 1s max_traceable_blocks: 200 seed_nodes: @@ -78,22 +79,25 @@ const validConfigOptions = ` timeout: 55s ` -// returns viper.Viper initialized from valid configuration above. -func newValidConfig(tb testing.TB, full bool) *viper.Viper { +func _newConfigFromYAML(tb testing.TB, yaml1, yaml2 string) *viper.Viper { v := viper.New() v.SetConfigType("yaml") - src := validConfigMinimal - if full { - src += validConfigOptions - } - - err := v.ReadConfig(strings.NewReader(src)) + err := v.ReadConfig(strings.NewReader(yaml1 + yaml2)) require.NoError(tb, err) return v } +// returns viper.Viper initialized from valid blockchain configuration above. +func newValidBlockchainConfig(tb testing.TB, full bool) *viper.Viper { + if full { + return _newConfigFromYAML(tb, validBlockchainConfigMinimal, validBlockchainConfigOptions) + } + + return _newConfigFromYAML(tb, validBlockchainConfigMinimal, "") +} + // resets value by key. Currently, viper doesn't provide unset method. Here is a // workaround suggested in https://github.com/spf13/viper/issues/632. func resetConfig(tb testing.TB, v *viper.Viper, key string) { @@ -126,7 +130,7 @@ func resetConfig(tb testing.TB, v *viper.Viper, key string) { } } -func TestConfigParser(t *testing.T) { +func TestParseBlockchainConfig(t *testing.T) { fullConfig := true _logger := zap.NewNop() @@ -137,7 +141,7 @@ func TestConfigParser(t *testing.T) { require.NoError(t, err) t.Run("minimal", func(t *testing.T) { - v := newValidConfig(t, !fullConfig) + v := newValidBlockchainConfig(t, !fullConfig) c, err := parseBlockchainConfig(v, _logger) require.NoError(t, err) @@ -150,7 +154,7 @@ func TestConfigParser(t *testing.T) { }) t.Run("full", func(t *testing.T) { - v := newValidConfig(t, fullConfig) + v := newValidBlockchainConfig(t, fullConfig) c, err := parseBlockchainConfig(v, _logger) require.NoError(t, err) @@ -224,7 +228,7 @@ func TestConfigParser(t *testing.T) { "storage", "storage.type", } { - v := newValidConfig(t, !fullConfig) + v := newValidBlockchainConfig(t, !fullConfig) resetConfig(t, v, "morph.consensus."+requiredKey) _, err := parseBlockchainConfig(v, _logger) require.Error(t, err, requiredKey) @@ -232,7 +236,7 @@ func TestConfigParser(t *testing.T) { }) t.Run("invalid", func(t *testing.T) { - v := newValidConfig(t, fullConfig) + v := newValidBlockchainConfig(t, fullConfig) resetConfig(t, v, "morph.consensus") _, err := parseBlockchainConfig(v, _logger) require.Error(t, err) @@ -301,7 +305,7 @@ func TestConfigParser(t *testing.T) { } { var reportMsg []string - v := newValidConfig(t, fullConfig) + v := newValidBlockchainConfig(t, fullConfig) for _, kvPair := range testCase { key := kvPair.key val := kvPair.val @@ -317,7 +321,7 @@ func TestConfigParser(t *testing.T) { t.Run("enums", func(t *testing.T) { t.Run("storage", func(t *testing.T) { - v := newValidConfig(t, fullConfig) + v := newValidBlockchainConfig(t, fullConfig) const path = "path/to/db" v.Set("morph.consensus.storage.path", path) @@ -361,7 +365,7 @@ func TestConfigParser(t *testing.T) { nativenames.StdLib, } - v := newValidConfig(t, fullConfig) + v := newValidBlockchainConfig(t, fullConfig) setI := func(name string, i int) { v.Set("morph.consensus.native_activations."+strings.ToLower(name), []interface{}{i}) @@ -430,3 +434,295 @@ morph: require.True(t, isLocalConsensusMode(v)) }) } + +// YAML configuration of the NeoFS network settings with all required fields. +const validNetworkSettingsConfigMinimal = ` +network_settings: + epoch_duration: 1 + max_object_size: 2 + require_homomorphic_hashing: true + allow_maintenance_mode: false + eigen_trust: + alpha: 0.1 + iterations_number: 3 + price: + storage: 4 + fee: + ir_candidate: 5 + withdraw: 6 + audit: 7 + new_container: 8 + container_domain: 9 +` + +// YAML configuration the NeoFS network settings with all optional fields. +const validNetworkSettingsConfigOptions = ` + custom: + - my_custom_key1=val1 + - my_custom_key2=val2 +` + +// returns viper.Viper initialized from valid network configuration above. +func newValidNetworkSettingsConfig(tb testing.TB, full bool) *viper.Viper { + if full { + return _newConfigFromYAML(tb, validNetworkSettingsConfigMinimal, validNetworkSettingsConfigOptions) + } + + return _newConfigFromYAML(tb, validNetworkSettingsConfigMinimal, "") +} + +func TestParseNetworkSettingsConfig(t *testing.T) { + fullConfig := true + + t.Run("minimal", func(t *testing.T) { + v := newValidNetworkSettingsConfig(t, !fullConfig) + c, err := parseNetworkSettingsConfig(v) + require.NoError(t, err) + + require.Equal(t, netmap.NetworkConfiguration{ + MaxObjectSize: 2, + StoragePrice: 4, + AuditFee: 7, + EpochDuration: 1, + ContainerFee: 8, + ContainerAliasFee: 9, + EigenTrustIterations: 3, + EigenTrustAlpha: 0.1, + IRCandidateFee: 5, + WithdrawalFee: 6, + HomomorphicHashingDisabled: false, + MaintenanceModeAllowed: false, + }, c) + }) + + t.Run("full", func(t *testing.T) { + v := newValidNetworkSettingsConfig(t, fullConfig) + c, err := parseNetworkSettingsConfig(v) + require.NoError(t, err) + + require.Equal(t, netmap.NetworkConfiguration{ + MaxObjectSize: 2, + StoragePrice: 4, + AuditFee: 7, + EpochDuration: 1, + ContainerFee: 8, + ContainerAliasFee: 9, + EigenTrustIterations: 3, + EigenTrustAlpha: 0.1, + IRCandidateFee: 5, + WithdrawalFee: 6, + HomomorphicHashingDisabled: false, + MaintenanceModeAllowed: false, + Raw: []netmap.RawNetworkParameter{ + {Name: "my_custom_key1", Value: []byte("val1")}, + {Name: "my_custom_key2", Value: []byte("val2")}, + }, + }, c) + }) + + t.Run("incomplete", func(t *testing.T) { + for _, requiredKey := range []string{ + "epoch_duration", + "max_object_size", + "require_homomorphic_hashing", + "allow_maintenance_mode", + "eigen_trust", + "eigen_trust.alpha", + "eigen_trust.iterations_number", + "price.storage", + "price.fee", + "price.fee.ir_candidate", + "price.fee.withdraw", + "price.fee.audit", + "price.fee.new_container", + "price.fee.container_domain", + } { + v := newValidNetworkSettingsConfig(t, !fullConfig) + resetConfig(t, v, "network_settings."+requiredKey) + _, err := parseNetworkSettingsConfig(v) + require.Error(t, err, requiredKey) + } + }) + + t.Run("invalid", func(t *testing.T) { + type kv struct { + key string + val interface{} + } + + kvF := func(k string, v interface{}) kv { + return kv{k, v} + } + + for _, testCase := range [][]kv{ + {kvF("epoch_duration", "not an integer")}, + {kvF("epoch_duration", -1)}, + {kvF("epoch_duration", 0)}, + {kvF("epoch_duration", 0.1)}, + {kvF("max_object_size", "not an integer")}, + {kvF("max_object_size", -1)}, + {kvF("max_object_size", 0)}, + {kvF("max_object_size", 0.1)}, + {kvF("require_homomorphic_hashing", "not a boolean")}, + {kvF("require_homomorphic_hashing", 1)}, + {kvF("require_homomorphic_hashing", "True")}, + {kvF("require_homomorphic_hashing", "False")}, + {kvF("allow_maintenance_mode", "not a boolean")}, + {kvF("allow_maintenance_mode", 1)}, + {kvF("allow_maintenance_mode", "True")}, + {kvF("allow_maintenance_mode", "False")}, + {kvF("eigen_trust.alpha", "not a float")}, + {kvF("eigen_trust.alpha", -0.1)}, + {kvF("eigen_trust.alpha", 1.1)}, + {kvF("eigen_trust.iterations_number", "not an integer")}, + {kvF("eigen_trust.iterations_number", -1)}, + {kvF("eigen_trust.iterations_number", 0)}, + {kvF("eigen_trust.iterations_number", 0.1)}, + {kvF("price.storage", "not an integer")}, + {kvF("price.storage", -1)}, + {kvF("price.storage", 0.1)}, + {kvF("price.fee.ir_candidate", "not an integer")}, + {kvF("price.fee.ir_candidate", -1)}, + {kvF("price.fee.ir_candidate", 0.1)}, + {kvF("price.fee.withdraw", "not an integer")}, + {kvF("price.fee.withdraw", -1)}, + {kvF("price.fee.withdraw", 0.1)}, + {kvF("price.fee.audit", "not an integer")}, + {kvF("price.fee.audit", -1)}, + {kvF("price.fee.audit", 0.1)}, + {kvF("price.fee.new_container", "not an integer")}, + {kvF("price.fee.new_container", -1)}, + {kvF("price.fee.new_container", 0.1)}, + {kvF("price.fee.container_domain", "not an integer")}, + {kvF("price.fee.container_domain", -1)}, + {kvF("price.fee.container_domain", 0.1)}, + {kvF("custom", []string{})}, + {kvF("custom", []string{"without_separator"})}, + {kvF("custom", []string{"with=several=separators"})}, + {kvF("custom", []string{"dup=1", "dup=2"})}, + {kvF("custom", []string{"AuditFee=any"})}, + {kvF("custom", []string{"BasicIncomeRate=any"})}, + {kvF("custom", []string{"ContainerAliasFee=any"})}, + {kvF("custom", []string{"EigenTrustIterations=any"})}, + {kvF("custom", []string{"EpochDuration=any"})}, + {kvF("custom", []string{"HomomorphicHashingDisabled=any"})}, + {kvF("custom", []string{"MaintenanceModeAllowed=any"})}, + {kvF("custom", []string{"MaxObjectSize=any"})}, + {kvF("custom", []string{"WithdrawFee=any"})}, + } { + var reportMsg []string + + v := newValidNetworkSettingsConfig(t, fullConfig) + for _, kvPair := range testCase { + key := kvPair.key + val := kvPair.val + + v.Set("network_settings."+key, val) + reportMsg = append(reportMsg, fmt.Sprintf("%s=%v", key, val)) + } + + _, err := parseNetworkSettingsConfig(v) + require.Error(t, err, strings.Join(reportMsg, ", ")) + } + }) +} + +// YAML configuration of the NNS with all required fields. +const validNNSConfig = ` +nns: + system_email: usr@domain.io +` + +// returns viper.Viper initialized from valid NNS configuration above. +func newValidNNSConfig(tb testing.TB) *viper.Viper { + return _newConfigFromYAML(tb, validNNSConfig, "") +} + +func TestParseNNSConfig(t *testing.T) { + t.Run("minimal", func(t *testing.T) { + v := newValidNNSConfig(t) + c, err := parseNNSConfig(v) + require.NoError(t, err) + + require.Equal(t, nnsConfig{ + systemEmail: "usr@domain.io", + }, c) + }) + + t.Run("incomplete", func(t *testing.T) { + for _, requiredKey := range []string{ + "system_email", + } { + v := newValidNNSConfig(t) + resetConfig(t, v, "nns."+requiredKey) + _, err := parseNNSConfig(v) + require.Error(t, err, requiredKey) + } + }) + + t.Run("invalid", func(t *testing.T) { + type kv struct { + key string + val interface{} + } + + kvF := func(k string, v interface{}) kv { + return kv{k, v} + } + + for _, testCase := range [][]kv{ + {kvF("system_email", "")}, + } { + var reportMsg []string + + v := newValidNNSConfig(t) + for _, kvPair := range testCase { + key := kvPair.key + val := kvPair.val + + v.Set("nns."+key, val) + reportMsg = append(reportMsg, fmt.Sprintf("%s=%v", key, val)) + } + + _, err := parseNNSConfig(v) + require.Error(t, err, strings.Join(reportMsg, ", ")) + } + }) +} + +func TestIsAutoDeploymentMode(t *testing.T) { + t.Run("ENV", func(t *testing.T) { + v := viper.New() + v.AutomaticEnv() + v.SetEnvPrefix("neofs_ir") + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + const envKey = "NEOFS_IR_NETWORK_SETTINGS" + + err := os.Unsetenv(envKey) + require.NoError(t, err) + + require.False(t, isAutoDeploymentMode(v)) + + err = os.Setenv(envKey, "any string") + require.NoError(t, err) + + require.True(t, isAutoDeploymentMode(v)) + }) + + t.Run("YAML", func(t *testing.T) { + v := viper.New() + v.SetConfigType("yaml") + err := v.ReadConfig(strings.NewReader(` +network_settings: + any_key: any_val +`)) + require.NoError(t, err) + + require.True(t, isAutoDeploymentMode(v)) + + resetConfig(t, v, "network_settings") + + require.False(t, isAutoDeploymentMode(v)) + }) +} diff --git a/pkg/innerring/contracts.go b/pkg/innerring/contracts.go index 0a93571230..bd3288a06d 100644 --- a/pkg/innerring/contracts.go +++ b/pkg/innerring/contracts.go @@ -9,7 +9,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/neorpc" "github.com/nspcc-dev/neo-go/pkg/util" + embeddedcontracts "github.com/nspcc-dev/neofs-node/contracts" "github.com/nspcc-dev/neofs-node/pkg/morph/client" + "github.com/nspcc-dev/neofs-node/pkg/morph/deploy" "github.com/nspcc-dev/neofs-node/pkg/util/glagolitsa" "github.com/spf13/cast" "github.com/spf13/viper" @@ -203,3 +205,43 @@ func parseContract(ctx *nnsContext, _logger *zap.Logger, cfg *viper.Viper, morph time.Sleep(pollInterval) } } + +func readEmbeddedContracts(deployPrm *deploy.Prm) error { + cs, err := embeddedcontracts.Read() + if err != nil { + return fmt.Errorf("read embedded contracts: %w", err) + } + + mRequired := map[string]*deploy.CommonDeployPrm{ + "NameService": &deployPrm.NNS.Common, + "NeoFS Alphabet": &deployPrm.AlphabetContract.Common, + "NeoFS Audit": &deployPrm.AuditContract.Common, + "NeoFS Balance": &deployPrm.BalanceContract.Common, + "NeoFS Container": &deployPrm.ContainerContract.Common, + "NeoFS ID": &deployPrm.NeoFSIDContract.Common, + "NeoFS Netmap": &deployPrm.NetmapContract.Common, + "NeoFS Notary Proxy": &deployPrm.ProxyContract.Common, + "NeoFS Reputation": &deployPrm.ReputationContract.Common, + } + + for i := range cs { + p, ok := mRequired[cs[i].Manifest.Name] + if ok { + p.Manifest = cs[i].Manifest + p.NEF = cs[i].NEF + + delete(mRequired, cs[i].Manifest.Name) + } + } + + if len(mRequired) > 0 { + missing := make([]string, 0, len(mRequired)) + for name := range mRequired { + missing = append(missing, name) + } + + return fmt.Errorf("some contracts are required but not embedded: %v", missing) + } + + return nil +} diff --git a/pkg/innerring/deploy.go b/pkg/innerring/deploy.go new file mode 100644 index 0000000000..3af20bcba4 --- /dev/null +++ b/pkg/innerring/deploy.go @@ -0,0 +1,143 @@ +package innerring + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/nspcc-dev/neofs-node/pkg/morph/client" + "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap" + "github.com/nspcc-dev/neofs-node/pkg/morph/deploy" + "github.com/nspcc-dev/neofs-node/pkg/util/state" +) + +type neoFSSidechain struct { + client *client.Client + + netmapContractMtx sync.RWMutex + netmapContract *netmap.Client +} + +func newNeoFSSidechain(sidechainClient *client.Client) *neoFSSidechain { + return &neoFSSidechain{ + client: sidechainClient, + } +} + +func (x *neoFSSidechain) CurrentState() (deploy.NeoFSState, error) { + var res deploy.NeoFSState + var err error + + x.netmapContractMtx.RLock() + netmapContract := x.netmapContract + x.netmapContractMtx.RUnlock() + + if netmapContract == nil { + x.netmapContractMtx.Lock() + + if x.netmapContract == nil { + netmapContractAddress, err := x.client.NNSContractAddress(client.NNSNetmapContractName) + if err != nil { + x.netmapContractMtx.Unlock() + return res, fmt.Errorf("resolve address of the '%s' contract in NNS: %w", client.NNSNetmapContractName, err) + } + + x.netmapContract, err = netmap.NewFromMorph(x.client, netmapContractAddress, 0) + if err != nil { + x.netmapContractMtx.Unlock() + return res, fmt.Errorf("create Netmap contract client: %w", err) + } + } + + netmapContract = x.netmapContract + + x.netmapContractMtx.Unlock() + } + + res.CurrentEpoch, err = netmapContract.Epoch() + if err != nil { + return res, fmt.Errorf("get current epoch from Netmap contract: %w", err) + } + + res.CurrentEpochBlock, err = netmapContract.LastEpochBlock() + if err != nil { + return res, fmt.Errorf("get last epoch block from Netmap contract: %w", err) + } + + return res, nil +} + +type sidechainKeyStorage struct { + persistentStorage *state.PersistentStorage +} + +func newSidechainKeyStorage(persistentStorage *state.PersistentStorage) *sidechainKeyStorage { + return &sidechainKeyStorage{ + persistentStorage: persistentStorage, + } +} + +var committeeGroupKey = []byte("committeeGroupKey") + +// GetPersistedPrivateKey reads persisted private key from the underlying +// storage. If key is missing, it's randomized and saved first. +func (x *sidechainKeyStorage) GetPersistedPrivateKey() (*keys.PrivateKey, error) { + b, err := x.persistentStorage.Bytes(committeeGroupKey) + if err != nil { + return nil, fmt.Errorf("read persistent storage: %w", err) + } + + const password = "" + + if b != nil { + var wlt wallet.Wallet + + err = json.Unmarshal(b, &wlt) + if err != nil { + return nil, fmt.Errorf("decode persisted NEO wallet from JSON: %w", err) + } + + if len(wlt.Accounts) != 1 { + return nil, fmt.Errorf("unexpected number of accounts in the persisted NEO wallet: %d instead of 1", len(wlt.Accounts)) + } + + err = wlt.Accounts[0].Decrypt(password, keys.NEP2ScryptParams()) + if err != nil { + return nil, fmt.Errorf("unlock 1st NEO account of the persisted NEO wallet: %w", err) + } + + return wlt.Accounts[0].PrivateKey(), nil + } + + acc, err := wallet.NewAccount() + if err != nil { + return nil, fmt.Errorf("generate random NEO account: %w", err) + } + + scryptPrm := keys.NEP2ScryptParams() + + err = acc.Encrypt(password, scryptPrm) + if err != nil { + return nil, fmt.Errorf("protect NEO account with password: %w", err) + } + + wlt := wallet.Wallet{ + Version: "1.0", // copy-paste from wallet package + Accounts: []*wallet.Account{acc}, + Scrypt: scryptPrm, + } + + jWallet, err := json.Marshal(wlt) + if err != nil { + return nil, fmt.Errorf("encode NEO wallet with randomized NEO account into JSON: %w", err) + } + + err = x.persistentStorage.SetBytes(committeeGroupKey, jWallet) + if err != nil { + return nil, fmt.Errorf("save generated key in the persistent storage: %w", err) + } + + return acc.PrivateKey(), nil +} diff --git a/pkg/innerring/deploy_test.go b/pkg/innerring/deploy_test.go new file mode 100644 index 0000000000..a47e3f439e --- /dev/null +++ b/pkg/innerring/deploy_test.go @@ -0,0 +1,66 @@ +package innerring + +import ( + "encoding/json" + "path/filepath" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/nspcc-dev/neofs-node/pkg/util/state" + "github.com/stretchr/testify/require" +) + +func newTestPersistentStorage(tb testing.TB) *state.PersistentStorage { + ps, err := state.NewPersistentStorage(filepath.Join(tb.TempDir(), "storage")) + require.NoError(tb, err) + + tb.Cleanup(func() { + _ = ps.Close() + }) + + return ps +} + +func TestSidechainKeyStorage_GetPersistedPrivateKey(t *testing.T) { + testPersistedKey := func(tb testing.TB, ks *sidechainKeyStorage, persistedKey *keys.PrivateKey) { + key, err := ks.GetPersistedPrivateKey() + require.NoError(tb, err) + require.Equal(tb, persistedKey, key) + } + + t.Run("fresh", func(t *testing.T) { + ks := newSidechainKeyStorage(newTestPersistentStorage(t)) + + initKey, err := ks.GetPersistedPrivateKey() + require.NoError(t, err) + require.NotNil(t, initKey) + + testPersistedKey(t, ks, initKey) + }) + + t.Run("preset", func(t *testing.T) { + ps := newTestPersistentStorage(t) + + acc, err := wallet.NewAccount() + require.NoError(t, err) + + err = acc.Encrypt("", keys.NEP2ScryptParams()) + require.NoError(t, err) + + jWallet, err := json.Marshal(wallet.Wallet{ + Version: "1.0", + Accounts: []*wallet.Account{acc}, + Scrypt: keys.ScryptParams{}, + Extra: wallet.Extra{}, + }) + require.NoError(t, err) + + err = ps.SetBytes(committeeGroupKey, jWallet) + require.NoError(t, err) + + ks := newSidechainKeyStorage(ps) + + testPersistedKey(t, ks, acc.PrivateKey()) + }) +} diff --git a/pkg/innerring/innerring.go b/pkg/innerring/innerring.go index 06a19e9978..0ecc0381b9 100644 --- a/pkg/innerring/innerring.go +++ b/pkg/innerring/innerring.go @@ -13,6 +13,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" + "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neofs-node/misc" "github.com/nspcc-dev/neofs-node/pkg/innerring/config" "github.com/nspcc-dev/neofs-node/pkg/innerring/internal/blockchain" @@ -41,6 +42,7 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/morph/client/neofsid" nmClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap" repClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/reputation" + "github.com/nspcc-dev/neofs-node/pkg/morph/deploy" "github.com/nspcc-dev/neofs-node/pkg/morph/event" "github.com/nspcc-dev/neofs-node/pkg/morph/timer" "github.com/nspcc-dev/neofs-node/pkg/network/cache" @@ -373,6 +375,42 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper, errChan chan<- return nil, fmt.Errorf("invalid blockchain configuration: %w", err) } + wlt, err := wallet.NewWalletFromFile(walletPath) + if err != nil { + return nil, fmt.Errorf("read wallet from file '%s': %w", walletPath, err) + } + + const singleAccLabel = "single" + const consensusAccLabel = "consensus" + var singleAcc *wallet.Account + var consensusAcc *wallet.Account + + for i := range wlt.Accounts { + err = wlt.Accounts[i].Decrypt(walletPass, keys.NEP2ScryptParams()) + switch wlt.Accounts[i].Label { + case singleAccLabel: + if err != nil { + return nil, fmt.Errorf("failed to decrypt account with label '%s' in wallet '%s': %w", singleAccLabel, walletPass, err) + } + + singleAcc = wlt.Accounts[i] + case consensusAccLabel: + if err != nil { + return nil, fmt.Errorf("failed to decrypt account with label '%s' in wallet '%s': %w", consensusAccLabel, walletPass, err) + } + + consensusAcc = wlt.Accounts[i] + } + } + + if singleAcc == nil { + return nil, fmt.Errorf("missing account with label '%s' in wallet '%s'", singleAccLabel, walletPass) + } + + if consensusAcc == nil { + return nil, fmt.Errorf("missing account with label '%s' in wallet '%s'", consensusAccLabel, walletPass) + } + if len(server.predefinedValidators) == 0 { server.predefinedValidators = cfgBlockchain.Committee } @@ -409,19 +447,65 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper, errChan chan<- return nil, fmt.Errorf("build WS client on internal blockchain: %w", err) } - server.key = server.bc.LocalKey() + server.key = singleAcc.PrivateKey() morphChain.key = server.key + sidechainOpts := make([]client.Option, 3, 4) + sidechainOpts[0] = client.WithContext(ctx) + sidechainOpts[1] = client.WithLogger(log) + sidechainOpts[2] = client.WithSingleClient(wsClient) - server.morphClient, err = client.New( - server.key, - client.WithContext(ctx), - client.WithLogger(log), - client.WithSingleClient(wsClient), - client.WithAutoSidechainScope(), - ) + isAutoDeploy := isAutoDeploymentMode(cfg) + + if !isAutoDeploy { + sidechainOpts = append(sidechainOpts, client.WithAutoSidechainScope()) + } + + server.morphClient, err = client.New(server.key, sidechainOpts...) if err != nil { return nil, fmt.Errorf("init internal morph client: %w", err) } + + if isAutoDeploy { + log.Info("auto-deployment configured, initializing Sidechain...") + + sidechain := newNeoFSSidechain(server.morphClient) + sidechainKeyStorage := newSidechainKeyStorage(server.persistate) + + var deployPrm deploy.Prm + deployPrm.Logger = server.log + deployPrm.Blockchain = wsClient + deployPrm.LocalAccount = singleAcc + deployPrm.ValidatorMultiSigAccount = consensusAcc + deployPrm.KeyStorage = sidechainKeyStorage + deployPrm.NeoFS = sidechain + + nnsCfg, err := parseNNSConfig(cfg) + if err != nil { + return nil, fmt.Errorf("invalid NNS configuration: %w", err) + } + + deployPrm.NNS.SystemEmail = nnsCfg.systemEmail + + err = readEmbeddedContracts(&deployPrm) + if err != nil { + return nil, err + } + + deployPrm.NetmapContract.Config, err = parseNetworkSettingsConfig(cfg) + if err != nil { + return nil, fmt.Errorf("invalid configuration of network settings: %w", err) + } + + err = deploy.Deploy(ctx, deployPrm) + if err != nil { + return nil, fmt.Errorf("deploy Sidechain: %w", err) + } + + err = server.morphClient.InitSidechainScope() + if err != nil { + return nil, fmt.Errorf("init Sidechain witness scope: %w", err) + } + } } else { if len(server.predefinedValidators) == 0 { return nil, fmt.Errorf("empty '%s' list in config", validatorsConfigKey) diff --git a/pkg/innerring/internal/blockchain/blockchain.go b/pkg/innerring/internal/blockchain/blockchain.go index c9d8b5c872..27a843d21f 100644 --- a/pkg/innerring/internal/blockchain/blockchain.go +++ b/pkg/innerring/internal/blockchain/blockchain.go @@ -22,8 +22,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/services/notary" "github.com/nspcc-dev/neo-go/pkg/services/rpcsrv" - "github.com/nspcc-dev/neo-go/pkg/wallet" - utilConfig "github.com/nspcc-dev/neofs-node/pkg/util/config" "go.uber.org/zap" ) @@ -44,8 +42,6 @@ type Blockchain struct { netServer *network.Server rpcServer *rpcsrv.Server - nodeAcc *wallet.Account - chErr chan error } @@ -234,7 +230,6 @@ type Config struct { Storage StorageConfig // NEO wallet of the node. The wallet is used by Consensus and Notary services. - // Corresponding private key be accessed via Blockchain.LocalKey. // // Required. Wallet config.Wallet @@ -329,11 +324,6 @@ func New(cfg Config) (res *Blockchain, err error) { cfg.P2P.Ping.Timeout = time.Minute } - nodeAcc, err := utilConfig.LoadAccount(cfg.Wallet.Path, "", cfg.Wallet.Password) - if err != nil { - return nil, fmt.Errorf("read node account: %w", err) - } - standByCommittee := make([]string, len(cfg.Committee)) for i := range cfg.Committee { standByCommittee[i] = hex.EncodeToString(cfg.Committee[i].Bytes()) @@ -489,7 +479,6 @@ func New(cfg Config) (res *Blockchain, err error) { netServer.AddService(&rpcServer) return &Blockchain{ - nodeAcc: nodeAcc, logger: cfg.Logger, storage: bcStorage, core: bc, @@ -550,11 +539,6 @@ func (x *Blockchain) Stop() { close(x.chErr) } -// LocalKey returns keys.PrivateKey corresponding to the configured wallet. -func (x *Blockchain) LocalKey() *keys.PrivateKey { - return x.nodeAcc.PrivateKey() -} - // BuildWSClient initializes rpcclient.WSClient with direct access to the // underlying blockchain. func (x *Blockchain) BuildWSClient(ctx context.Context) (*rpcclient.WSClient, error) { diff --git a/pkg/morph/client/nns.go b/pkg/morph/client/nns.go index e2dfabdc55..7893bd9aae 100644 --- a/pkg/morph/client/nns.go +++ b/pkg/morph/client/nns.go @@ -84,6 +84,20 @@ func (c *Client) NNSHash() (util.Uint160, error) { return *nnsHash, nil } +// InitSidechainScope allows to replace [WithAutoSidechainScope] option and +// postpone Sidechain scope initialization when NNS contract is not yet ready +// while Client is already needed. +func (c *Client) InitSidechainScope() error { + c.switchLock.RLock() + defer c.switchLock.RUnlock() + + if c.inactive { + return ErrConnectionLost + } + + return autoSidechainScope(c.client, &c.cfg) +} + func autoSidechainScope(ws *rpcclient.WSClient, conf *cfg) error { nnsHash, err := nns.InferHash(ws) if err != nil { diff --git a/pkg/util/state/storage.go b/pkg/util/state/storage.go index 0485b14813..84a56db425 100644 --- a/pkg/util/state/storage.go +++ b/pkg/util/state/storage.go @@ -2,9 +2,9 @@ package state import ( "encoding/binary" - "encoding/hex" "fmt" + "github.com/nspcc-dev/neo-go/pkg/util/slice" "go.etcd.io/bbolt" ) @@ -27,38 +27,56 @@ func NewPersistentStorage(path string) (*PersistentStorage, error) { return &PersistentStorage{db: db}, nil } -// SetUInt32 sets a uint32 value in the storage. -func (p PersistentStorage) SetUInt32(key []byte, value uint32) error { +// saves given KV in the storage. +func (p PersistentStorage) put(k, v []byte) error { return p.db.Update(func(tx *bbolt.Tx) error { b, err := tx.CreateBucketIfNotExists(stateBucket) if err != nil { return fmt.Errorf("can't create state bucket in state persistent storage: %w", err) } - buf := make([]byte, 8) - binary.LittleEndian.PutUint64(buf, uint64(value)) + return b.Put(k, v) + }) +} - return b.Put(key, buf) +// looks up for value in the storage by specified key and passes the value into +// provided handler. Nil corresponds to missing value. Handler's error is +// forwarded. +// +// Handler MUST NOT retain passed []byte, make a copy if needed. +func (p PersistentStorage) lookup(k []byte, f func(v []byte) error) error { + return p.db.View(func(tx *bbolt.Tx) error { + var v []byte + + b := tx.Bucket(stateBucket) + if b != nil { + v = b.Get(k) + } + + return f(v) }) } +// SetUInt32 sets a uint32 value in the storage. +func (p PersistentStorage) SetUInt32(key []byte, value uint32) error { + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, uint64(value)) + + return p.put(key, buf) +} + // UInt32 returns a uint32 value from persistent storage. If the value does not exist, // returns 0. func (p PersistentStorage) UInt32(key []byte) (n uint32, err error) { - err = p.db.View(func(tx *bbolt.Tx) error { - b := tx.Bucket(stateBucket) - if b == nil { - return nil // if bucket not exists yet, return default n = 0 - } + err = p.lookup(key, func(v []byte) error { + if v != nil { + if len(v) != 8 { + return fmt.Errorf("unexpected byte len: %d instead of %d", len(v), 8) + } - buf := b.Get(key) - if len(buf) != 8 { - return fmt.Errorf("persistent storage does not store uint data in %s", hex.EncodeToString(key)) + n = uint32(binary.LittleEndian.Uint64(v)) } - u64 := binary.LittleEndian.Uint64(buf) - n = uint32(u64) - return nil }) @@ -69,3 +87,20 @@ func (p PersistentStorage) UInt32(key []byte) (n uint32, err error) { func (p PersistentStorage) Close() error { return p.db.Close() } + +// SetBytes saves binary value in the storage by specified key. +func (p PersistentStorage) SetBytes(key []byte, value []byte) error { + return p.put(key, value) +} + +// Bytes reads binary value by specified key. Returns nil if value is missing. +func (p PersistentStorage) Bytes(key []byte) (res []byte, err error) { + err = p.lookup(key, func(v []byte) error { + if v != nil { + res = slice.Copy(v) + } + return nil + }) + + return +} diff --git a/pkg/util/state/storage_test.go b/pkg/util/state/storage_test.go index 0e00aed30b..e1f1e965c0 100644 --- a/pkg/util/state/storage_test.go +++ b/pkg/util/state/storage_test.go @@ -8,10 +8,19 @@ import ( "github.com/stretchr/testify/require" ) +func newStorage(tb testing.TB) *state.PersistentStorage { + storage, err := state.NewPersistentStorage(filepath.Join(tb.TempDir(), ".storage")) + require.NoError(tb, err) + + tb.Cleanup(func() { + _ = storage.Close() + }) + + return storage +} + func TestPersistentStorage_UInt32(t *testing.T) { - storage, err := state.NewPersistentStorage(filepath.Join(t.TempDir(), ".storage")) - require.NoError(t, err) - defer storage.Close() + storage := newStorage(t) n, err := storage.UInt32([]byte("unset-value")) require.NoError(t, err) @@ -24,3 +33,22 @@ func TestPersistentStorage_UInt32(t *testing.T) { require.NoError(t, err) require.EqualValues(t, 10, n) } + +func TestPersistentStorage_Bytes(t *testing.T) { + storage := newStorage(t) + + bKey := []byte("bytes") + + bRes, err := storage.Bytes(bKey) + require.NoError(t, err) + require.Nil(t, bRes) + + bVal := []byte("Hello, world!") + + err = storage.SetBytes(bKey, bVal) + require.NoError(t, err) + + bRes, err = storage.Bytes(bKey) + require.NoError(t, err) + require.Equal(t, bVal, bRes) +} From c5670bacfbd76d3456246aa0387032913004323b Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 28 Aug 2023 19:34:18 +0400 Subject: [PATCH 18/22] ir/config: Fix parser of integer values Previously, floating point numbers were successfully parsed into integers: mantissa were silently dropped. This behavior could lead to false-positive acceptance of the incorrect configuration. Check floating point separately as a workaround for missing check in `viper` library. Signed-off-by: Leonard Lyubich --- pkg/innerring/config.go | 8 +++++++- pkg/innerring/config_test.go | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/innerring/config.go b/pkg/innerring/config.go index a3fea5ad25..474b2eb3eb 100644 --- a/pkg/innerring/config.go +++ b/pkg/innerring/config.go @@ -429,7 +429,13 @@ func parseConfigUint64Condition(v *viper.Viper, key, desc string, cond func(uint err = errMissingConfig } if err == nil { - res, err = cast.ToUint64E(v.Get(key)) + switch val := v.Get(key).(type) { + case float32, float64: + // cast.ToUint64E just drops mantissa + return 0, fmt.Errorf("unable to cast %#v of type %T to uint64", val, val) + default: + res, err = cast.ToUint64E(val) + } if err == nil && cond != nil { err = cond(res) } diff --git a/pkg/innerring/config_test.go b/pkg/innerring/config_test.go index 7ffeae9a33..74d9f59def 100644 --- a/pkg/innerring/config_test.go +++ b/pkg/innerring/config_test.go @@ -254,6 +254,7 @@ func TestParseBlockchainConfig(t *testing.T) { {kvF("magic", "not an integer")}, {kvF("magic", -1)}, {kvF("magic", 0)}, + {kvF("magic", 0.1)}, {kvF("magic", math.MaxUint32+1)}, {kvF("committee", []string{})}, {kvF("committee", []string{"not a key"})}, From b18bf35896b3d6b493bfb93db3b686e5300a85fb Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 28 Aug 2023 20:57:57 +0400 Subject: [PATCH 19/22] sidechain/deploy: Fix Notary role designation for single node Despite the fact that the committee consists of one member, the transaction must be witnessed by a multi-sig 1:1 account. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/notary.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/morph/deploy/notary.go b/pkg/morph/deploy/notary.go index d969989775..accea076a4 100644 --- a/pkg/morph/deploy/notary.go +++ b/pkg/morph/deploy/notary.go @@ -125,7 +125,15 @@ func enableNotary(ctx context.Context, prm enableNotaryPrm) error { // initDesignateNotaryRoleToLocalAccountTick returns a function that preserves // context of the Notary role designation to the local account between calls. func initDesignateNotaryRoleToLocalAccountTick(ctx context.Context, prm enableNotaryPrm) (func(), error) { - localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) + committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(prm.committee)) + committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) + + err := committeeMultiSigAcc.ConvertMultisig(committeeMultiSigM, prm.committee) + if err != nil { + return nil, fmt.Errorf("compose committee multi-signature account: %w", err) + } + + localActor, err := actor.NewSimple(prm.blockchain, committeeMultiSigAcc) if err != nil { return nil, fmt.Errorf("init transaction sender from local account: %w", err) } From 54f7d664ec527fd035826c0609d5e78be2c75eb4 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 29 Aug 2023 16:26:17 +0400 Subject: [PATCH 20/22] sidechain/deploy: Do not init reused actors multiple times Simple local and committee multi-sig notary actors are used in all `syncNeoFSContract` calls, so it's worth to create them once and share. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/contracts.go | 33 ++++++++++++++------------------- pkg/morph/deploy/deploy.go | 30 +++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/pkg/morph/deploy/contracts.go b/pkg/morph/deploy/contracts.go index 6d6a55a00d..80a29c4fa9 100644 --- a/pkg/morph/deploy/contracts.go +++ b/pkg/morph/deploy/contracts.go @@ -46,6 +46,11 @@ type syncNeoFSContractPrm struct { committee keys.PublicKeys committeeGroupKey *keys.PrivateKey + // with localAcc signer only + simpleLocalActor *actor.Actor + // committee multi-sig signs, localAcc pays + committeeLocalActor *notary.Actor + localNEF nef.File localManifest manifest.Manifest @@ -99,16 +104,6 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint return util.Uint160{}, fmt.Errorf("encode local manifest of the contract into JSON: %w", err) } - localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) - if err != nil { - return util.Uint160{}, fmt.Errorf("init transaction sender from local account: %w", err) - } - - committeeActor, err := newCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee) - if err != nil { - return util.Uint160{}, fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) - } - var proxyCommitteeActor *notary.Actor initProxyCommitteeActor := func(proxyContract util.Uint160) error { @@ -152,24 +147,24 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint managementContract = management.New(deployCommitteeActor) contractDeployer = deployCommitteeActor } else { - managementContract = management.New(localActor) - contractDeployer = localActor + managementContract = management.New(prm.simpleLocalActor) + contractDeployer = prm.simpleLocalActor } var alreadyUpdated bool domainNameForAddress := prm.domainName + "." + domainContractAddresses l := prm.logger.With(zap.String("contract", prm.localManifest.Name), zap.String("domain", domainNameForAddress)) updateTxModifier := neoFSRuntimeTransactionModifier(prm.neoFS) - deployTxMonitor := newTransactionGroupMonitor(localActor) - updateTxMonitor := newTransactionGroupMonitor(localActor) + deployTxMonitor := newTransactionGroupMonitor(prm.simpleLocalActor) + updateTxMonitor := newTransactionGroupMonitor(prm.simpleLocalActor) setContractRecordPrm := setNeoFSContractDomainRecordPrm{ logger: l, - setRecordTxMonitor: newTransactionGroupMonitor(localActor), - registerTLDTxMonitor: newTransactionGroupMonitor(localActor), + setRecordTxMonitor: newTransactionGroupMonitor(prm.simpleLocalActor), + registerTLDTxMonitor: newTransactionGroupMonitor(prm.simpleLocalActor), nnsContract: prm.nnsContract, systemEmail: prm.systemEmail, - localActor: localActor, - committeeActor: committeeActor, + localActor: prm.simpleLocalActor, + committeeActor: prm.committeeLocalActor, domain: domainNameForAddress, record: "", // set in for loop } @@ -245,7 +240,7 @@ func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint if prm.committeeDeployRequired { l.Info("contract requires committee witness for deployment, sending Notary request...") - mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(managementContract.DeployTransaction(&nefCp, &manifestCp, extraDeployArgs)) + mainTxID, fallbackTxID, vub, err := prm.committeeLocalActor.Notarize(managementContract.DeployTransaction(&nefCp, &manifestCp, extraDeployArgs)) if err != nil { if errors.Is(err, neorpc.ErrInsufficientFunds) { l.Info("insufficient Notary balance to deploy the contract, will try again later") diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index b440cf10b6..d2ddd408e5 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -219,6 +219,16 @@ func Deploy(ctx context.Context, prm Prm) error { return errors.New("local account does not belong to any Neo committee member") } + simpleLocalActor, err := actor.NewSimple(prm.Blockchain, prm.LocalAccount) + if err != nil { + return fmt.Errorf("init transaction sender from single local account: %w", err) + } + + committeeLocalActor, err := newCommitteeNotaryActor(prm.Blockchain, prm.LocalAccount, committee) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) + } + chNewBlock := make(chan struct{}, 1) monitor, err := newBlockchainMonitor(prm.Logger, prm.Blockchain, chNewBlock) @@ -373,15 +383,17 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("NeoFS Alphabet successfully initialized") syncPrm := syncNeoFSContractPrm{ - logger: prm.Logger, - blockchain: prm.Blockchain, - neoFS: prm.NeoFS, - monitor: monitor, - localAcc: prm.LocalAccount, - nnsContract: nnsOnChainAddress, - systemEmail: prm.NNS.SystemEmail, - committee: committee, - committeeGroupKey: committeeGroupKey, + logger: prm.Logger, + blockchain: prm.Blockchain, + neoFS: prm.NeoFS, + monitor: monitor, + localAcc: prm.LocalAccount, + nnsContract: nnsOnChainAddress, + systemEmail: prm.NNS.SystemEmail, + committee: committee, + committeeGroupKey: committeeGroupKey, + simpleLocalActor: simpleLocalActor, + committeeLocalActor: committeeLocalActor, } localAccLeads := localAccCommitteeIndex == 0 From 073754c66f664ce5744ac07caf00c0f2ce1a3059 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 29 Aug 2023 18:08:17 +0400 Subject: [PATCH 21/22] sidechain/deploy: Drop unused utilities working with contract versions Signed-off-by: Leonard Lyubich --- go.mod | 2 - go.sum | 4 - pkg/morph/deploy/util.go | 131 --------------------------- pkg/morph/deploy/util_test.go | 163 ---------------------------------- 4 files changed, 300 deletions(-) delete mode 100644 pkg/morph/deploy/util_test.go diff --git a/go.mod b/go.mod index 8832982e77..4cf076350f 100644 --- a/go.mod +++ b/go.mod @@ -98,11 +98,9 @@ require ( go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/text v0.13.0 // indirect - golang.org/x/tools v0.13.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect gopkg.in/ini.v1 v1.67.0 // indirect lukechampine.com/blake3 v1.1.7 // indirect diff --git a/go.sum b/go.sum index a542684354..d648ca538b 100644 --- a/go.sum +++ b/go.sum @@ -186,7 +186,6 @@ github.com/ipfs/go-cid v0.3.2 h1:OGgOd+JCFM+y1DjWPmVH+2/4POtpDzwcr7VgnB7mZXc= github.com/ipfs/go-cid v0.3.2/go.mod h1:gQ8pKqT/sUxGY+tIwy1RPpAojYu7jAyCp5Tz1svoupw= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= @@ -368,7 +367,6 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -421,7 +419,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -595,7 +592,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/morph/deploy/util.go b/pkg/morph/deploy/util.go index 027e9d5633..bd01dab17e 100644 --- a/pkg/morph/deploy/util.go +++ b/pkg/morph/deploy/util.go @@ -2,7 +2,6 @@ package deploy import ( "context" - "encoding/json" "errors" "fmt" "strings" @@ -10,25 +9,14 @@ import ( "time" "github.com/nspcc-dev/neo-go/pkg/core/block" - "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "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/encoding/address" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neorpc" - "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "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/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/neofs-contract/common" "go.uber.org/zap" ) @@ -181,125 +169,6 @@ func readNNSOnChainState(b Blockchain) (*state.Contract, error) { return res, nil } -// contractVersion describes versioning of NeoFS smart contracts. -type contractVersion struct{ major, minor, patch uint64 } - -// space sizes for major and minor versions of the NeoFS contracts. -const majorSpace, minorSpace = 1e6, 1e3 - -// equals checks if contractVersion equals to the specified SemVer version. -// -//nolint:unused -func (x contractVersion) equals(major, minor, patch uint64) bool { - return x.major == major && x.minor == minor && x.patch == patch -} - -// returns contractVersion as single integer. -func (x contractVersion) toUint64() uint64 { - return x.major*majorSpace + x.minor*minorSpace + x.patch -} - -// cmp compares x and y and returns: -// -// -1 if x < y -// 0 if x == y -// +1 if x > y -func (x contractVersion) cmp(y contractVersion) int { - xN := x.toUint64() - yN := y.toUint64() - if xN < yN { - return -1 - } else if xN == yN { - return 0 - } - return 1 -} - -func (x contractVersion) String() string { - const sep = "." - return fmt.Sprintf("%d%s%d%s%d", x.major, sep, x.minor, sep, x.patch) -} - -// parses contractVersion from the invocation result of methodVersion method. -func parseContractVersionFromInvocationResult(res *result.Invoke) (contractVersion, error) { - bigVersionOnChain, err := unwrap.BigInt(res, nil) - if err != nil { - return contractVersion{}, fmt.Errorf("unwrap big integer from '%s' method return: %w", methodVersion, err) - } else if !bigVersionOnChain.IsUint64() { - return contractVersion{}, fmt.Errorf("invalid/unsupported format of the '%s' method return: expected uint64, got %v", methodVersion, bigVersionOnChain) - } - - n := bigVersionOnChain.Uint64() - - mjr := n / majorSpace - - return contractVersion{ - major: mjr, - minor: (n - mjr*majorSpace) / minorSpace, - patch: n % minorSpace, - }, nil -} - -// readContractOnChainVersion returns current version of the smart contract -// presented in given Blockchain with specified address. -func readContractOnChainVersion(b Blockchain, onChainAddress util.Uint160) (contractVersion, error) { - res, err := invoker.New(b, nil).Call(onChainAddress, methodVersion) - if err != nil { - return contractVersion{}, fmt.Errorf("call '%s' contract method: %w", methodVersion, err) - } - - return parseContractVersionFromInvocationResult(res) -} - -// readContractLocalVersion returns version of the local smart contract -// represented by its compiled artifacts. Deployment is tested using provided -// invoker on behalf of the committee. -func readContractLocalVersion(rpc invoker.RPCInvoke, committee keys.PublicKeys, localNEF nef.File, localManifest manifest.Manifest, deployArgs ...interface{}) (contractVersion, error) { - multiSigScript, err := smartcontract.CreateMultiSigRedeemScript(smartcontract.GetMajorityHonestNodeCount(len(committee)), committee) - if err != nil { - return contractVersion{}, fmt.Errorf("create committee multi-signature verification script: %w", err) - } - - jManifest, err := json.Marshal(localManifest) - if err != nil { - return contractVersion{}, fmt.Errorf("encode manifest into JSON: %w", err) - } - - bNEF, err := localNEF.Bytes() - if err != nil { - return contractVersion{}, fmt.Errorf("encode NEF into binary: %w", err) - } - - var deployData interface{} - if len(deployArgs) > 0 { - deployData = deployArgs - } - - script := io.NewBufBinWriter() - emit.Opcodes(script.BinWriter, opcode.NEWARRAY0) - emit.Int(script.BinWriter, int64(callflag.All)) - emit.String(script.BinWriter, methodVersion) - emit.AppCall(script.BinWriter, management.Hash, "deploy", callflag.All, bNEF, jManifest, deployData) - emit.Opcodes(script.BinWriter, opcode.PUSH2, opcode.PICKITEM) - emit.Syscall(script.BinWriter, interopnames.SystemContractCall) - - res, err := invoker.New(rpc, []transaction.Signer{ - { - Account: util.Uint160{}, // zero hash to avoid 'contract already exists' case - Scopes: transaction.None, - }, - { - Account: hash.Hash160(multiSigScript), - Scopes: transaction.Global, - }, - }).Run(script.Bytes()) - if err != nil { - return contractVersion{}, fmt.Errorf("run test script deploying contract and calling its '%s' method: %w", methodVersion, err) - } - - return parseContractVersionFromInvocationResult(res) -} - type transactionGroupWaiter interface { WaitAny(ctx context.Context, vub uint32, hashes ...util.Uint256) (*state.AppExecResult, error) } diff --git a/pkg/morph/deploy/util_test.go b/pkg/morph/deploy/util_test.go deleted file mode 100644 index ce9ab09e54..0000000000 --- a/pkg/morph/deploy/util_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package deploy - -import ( - "math" - "math/big" - "strconv" - "strings" - "testing" - - "github.com/nspcc-dev/neo-go/pkg/compiler" - "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/neorpc/result" - "github.com/nspcc-dev/neo-go/pkg/neotest" - "github.com/nspcc-dev/neo-go/pkg/neotest/chain" - "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" - "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" - "github.com/stretchr/testify/require" -) - -func TestVersionCmp(t *testing.T) { - for _, tc := range []struct { - xmjr, xmnr, xpatch uint64 - ymjr, ymnr, ypatch uint64 - expected int - }{ - { - 1, 2, 3, - 1, 2, 3, - 0, - }, - { - 0, 1, 1, - 0, 2, 0, - -1, - }, - { - 1, 2, 2, - 1, 2, 3, - -1, - }, - { - 1, 2, 4, - 1, 2, 3, - 1, - }, - { - 0, 10, 0, - 1, 2, 3, - -1, - }, - { - 2, 0, 0, - 1, 2, 3, - 1, - }, - } { - x := contractVersion{tc.xmjr, tc.xmnr, tc.xpatch} - y := contractVersion{tc.ymjr, tc.ymnr, tc.ypatch} - require.Equal(t, tc.expected, x.cmp(y), tc) - } -} - -func TestParseContractVersionFromInvocationResult(t *testing.T) { - var err error - var res result.Invoke - - // non-HALT state - _, err = parseContractVersionFromInvocationResult(&res) - require.Error(t, err) - - res.State = vmstate.Halt.String() - - // empty stack - _, err = parseContractVersionFromInvocationResult(&res) - require.Error(t, err) - - // invalid item - res.Stack = []stackitem.Item{stackitem.Null{}} - - _, err = parseContractVersionFromInvocationResult(&res) - require.Error(t, err) - - // correct - ver := contractVersion{1, 2, 3} - i := new(big.Int) - - res.Stack = []stackitem.Item{stackitem.NewBigInteger(i.SetUint64(ver.toUint64()))} - - // overflow uint64 - i.SetUint64(math.MaxUint64).Add(i, big.NewInt(1)) - - _, err = parseContractVersionFromInvocationResult(&res) - require.Error(t, err) -} - -type testRPCInvoker struct { - invoker.RPCInvoke - tb testing.TB - exec *neotest.Executor -} - -func newTestRPCInvoker(tb testing.TB, exec *neotest.Executor) *testRPCInvoker { - return &testRPCInvoker{ - tb: tb, - exec: exec, - } -} - -func (x *testRPCInvoker) InvokeScript(script []byte, _ []transaction.Signer) (*result.Invoke, error) { - tx := transaction.New(script, 0) - tx.Nonce = neotest.Nonce() - tx.ValidUntilBlock = x.exec.Chain.BlockHeight() + 1 - tx.Signers = []transaction.Signer{{Account: x.exec.Committee.ScriptHash()}} - - b := x.exec.NewUnsignedBlock(x.tb, tx) - ic, err := x.exec.Chain.GetTestVM(trigger.Application, tx, b) - if err != nil { - return nil, err - } - x.tb.Cleanup(ic.Finalize) - - ic.VM.LoadWithFlags(tx.Script, callflag.All) - err = ic.VM.Run() - if err != nil { - return nil, err - } - - return &result.Invoke{ - State: vmstate.Halt.String(), - Stack: ic.VM.Estack().ToArray(), - }, nil -} - -func TestReadContractLocalVersion(t *testing.T) { - const version = 1_002_003 - - bc, acc := chain.NewSingle(t) - e := neotest.NewExecutor(t, bc, acc, acc) - - src := `package foo - const version = ` + strconv.Itoa(version) + ` - func Version() int { - return version - }` - - ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{Name: "Helper"}) - - var committeeSigner neotest.SingleSigner - - if single, ok := acc.(neotest.SingleSigner); ok { - committeeSigner = single - } else { - committeeSigner = acc.(neotest.MultiSigner).Single(0) - } - - res, err := readContractLocalVersion(newTestRPCInvoker(t, e), keys.PublicKeys{committeeSigner.Account().PublicKey()}, *ctr.NEF, *ctr.Manifest) - require.NoError(t, err) - require.EqualValues(t, version, res.toUint64()) -} From 3dad944a2e4a34ae89aae7d44a0c23e5118a6f80 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 23 Oct 2023 12:37:48 +0400 Subject: [PATCH 22/22] sidechain/deploy: Make contracts' update transactions valid for an epoch Previously, updating transactions of the NeoFS smart contracts (calling `update` method) were sent with ValidUntilBlock set to last epoch block + 100 by the sidechain auto-deployment procedure. This could cause the update idleness when epoch duration was more than 100 Sidechain blocks. To prevent this, the transaction should be valid at least one epoch time. Refs #2195. Signed-off-by: Leonard Lyubich --- pkg/innerring/deploy.go | 12 ++++++++++++ pkg/morph/deploy/deploy.go | 9 ++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pkg/innerring/deploy.go b/pkg/innerring/deploy.go index 3af20bcba4..ab4caf270d 100644 --- a/pkg/innerring/deploy.go +++ b/pkg/innerring/deploy.go @@ -3,6 +3,7 @@ package innerring import ( "encoding/json" "fmt" + "math" "sync" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -66,6 +67,17 @@ func (x *neoFSSidechain) CurrentState() (deploy.NeoFSState, error) { return res, fmt.Errorf("get last epoch block from Netmap contract: %w", err) } + epochDur, err := netmapContract.EpochDuration() + if err != nil { + return res, fmt.Errorf("get epoch duration from Netmap contract: %w", err) + } + + if epochDur > math.MaxUint32 { + return res, fmt.Errorf("epoch duration from Netmap contract overflows uint32: %d", epochDur) + } + + res.EpochDuration = uint32(epochDur) + return res, nil } diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index d2ddd408e5..f2891c8df2 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "math" "math/big" "sort" "strconv" @@ -78,6 +79,8 @@ type NeoFSState struct { CurrentEpoch uint64 // Height of the NeoFS Sidechain at which CurrentEpoch began. CurrentEpochBlock uint32 + // Duration of the single NeoFS epoch measured in Sidechain blocks. + EpochDuration uint32 } // NeoFS provides access to the running NeoFS network. @@ -773,7 +776,11 @@ func neoFSRuntimeTransactionModifier(neoFS NeoFS) actor.TransactionCheckerModifi } tx.Nonce = uint32(neoFSState.CurrentEpoch) - tx.ValidUntilBlock = neoFSState.CurrentEpochBlock + 100 + if math.MaxUint32-neoFSState.CurrentEpochBlock > neoFSState.EpochDuration { + tx.ValidUntilBlock = neoFSState.CurrentEpochBlock + neoFSState.EpochDuration + } else { + tx.ValidUntilBlock = math.MaxUint32 + } return nil }