diff --git a/CHANGELOG.md b/CHANGELOG.md index abb9eaae54..2d1f1bc669 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/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/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 85c9a45afe..43a5a2d80d 100755 Binary files a/contracts/00-nns.nef and b/contracts/00-nns.nef differ diff --git a/contracts/01-audit.manifest.json b/contracts/01-audit.manifest.json new file mode 100755 index 0000000000..c01f60f7e4 --- /dev/null +++ b/contracts/01-audit.manifest.json @@ -0,0 +1 @@ +{"name":"NeoFS Audit","abi":{"methods":[{"name":"_deploy","offset":0,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"get","offset":892,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"list","offset":917,"parameters":[],"returntype":"Array","safe":true},{"name":"listByCID","offset":983,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"listByEpoch","offset":951,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Array","safe":true},{"name":"listByNode","offset":1026,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"ByteArray"},{"name":"key","type":"PublicKey"}],"returntype":"Array","safe":true},{"name":"put","offset":745,"parameters":[{"name":"rawAuditResult","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"update","offset":616,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"version","offset":1104,"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/01-audit.nef b/contracts/01-audit.nef new file mode 100755 index 0000000000..1e90dfaba6 Binary files /dev/null and b/contracts/01-audit.nef differ diff --git a/contracts/02-balance.manifest.json b/contracts/02-balance.manifest.json new file mode 100755 index 0000000000..138b636b49 --- /dev/null +++ b/contracts/02-balance.manifest.json @@ -0,0 +1 @@ +{"name":"NeoFS Balance","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":93,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"balanceOf","offset":1104,"parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","safe":true},{"name":"burn","offset":1637,"parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"txDetails","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"decimals","offset":1082,"parameters":[],"returntype":"Integer","safe":true},{"name":"lock","offset":1250,"parameters":[{"name":"txDetails","type":"ByteArray"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"until","type":"Integer"}],"returntype":"Void","safe":false},{"name":"mint","offset":1516,"parameters":[{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"txDetails","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"newEpoch","offset":1387,"parameters":[{"name":"epochNum","type":"Integer"}],"returntype":"Void","safe":false},{"name":"symbol","offset":1078,"parameters":[],"returntype":"String","safe":true},{"name":"totalSupply","offset":1086,"parameters":[],"returntype":"Integer","safe":true},{"name":"transfer","offset":1123,"parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","safe":false},{"name":"transferX","offset":1147,"parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"details","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"update","offset":947,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"version","offset":1788,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"Lock","parameters":[{"name":"txID","type":"ByteArray"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"until","type":"Integer"}]},{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]},{"name":"TransferX","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"details","type":"ByteArray"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update"]}],"supportedstandards":["NEP-17"],"trusts":[],"extra":null} \ No newline at end of file diff --git a/contracts/02-balance.nef b/contracts/02-balance.nef new file mode 100755 index 0000000000..cacd9e26cc Binary files /dev/null and b/contracts/02-balance.nef differ 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 0000000000..a6a92f8bda Binary files /dev/null and b/contracts/03-proxy.nef differ 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 0000000000..c907e13d80 Binary files /dev/null and b/contracts/04-reputation.nef differ 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 0000000000..0952dce8be Binary files /dev/null and b/contracts/05-neofsid.nef differ diff --git a/contracts/06-container.manifest.json b/contracts/06-container.manifest.json new file mode 100755 index 0000000000..57bb7af2f8 --- /dev/null +++ b/contracts/06-container.manifest.json @@ -0,0 +1 @@ +{"name":"NeoFS Container","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":83,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"alias","offset":3058,"parameters":[{"name":"cid","type":"ByteArray"}],"returntype":"String","safe":true},{"name":"containersOf","offset":3198,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"InteropInterface","safe":true},{"name":"count","offset":3153,"parameters":[],"returntype":"Integer","safe":true},{"name":"delete","offset":2649,"parameters":[{"name":"containerID","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"eACL","offset":3610,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"get","offset":2945,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"getContainerSize","offset":3870,"parameters":[{"name":"id","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"iterateAllContainerSizes","offset":4243,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"InteropInterface","safe":true},{"name":"iterateContainerSizes","offset":4145,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"Hash256"}],"returntype":"InteropInterface","safe":true},{"name":"list","offset":3252,"parameters":[{"name":"owner","type":"ByteArray"}],"returntype":"Array","safe":true},{"name":"listContainerSizes","offset":3984,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Array","safe":true},{"name":"newEpoch","offset":4295,"parameters":[{"name":"epochNum","type":"Integer"}],"returntype":"Void","safe":false},{"name":"onNEP11Payment","offset":1164,"parameters":[{"name":"a","type":"Hash160"},{"name":"b","type":"Integer"},{"name":"c","type":"ByteArray"},{"name":"d","type":"Any"}],"returntype":"Void","safe":false},{"name":"owner","offset":3007,"parameters":[{"name":"containerID","type":"ByteArray"}],"returntype":"ByteArray","safe":true},{"name":"put","offset":1565,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"putContainerSize","offset":3668,"parameters":[{"name":"epoch","type":"Integer"},{"name":"cid","type":"ByteArray"},{"name":"usedSize","type":"Integer"},{"name":"pubKey","type":"PublicKey"}],"returntype":"Void","safe":false},{"name":"putNamed","offset":1581,"parameters":[{"name":"container","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"},{"name":"name","type":"String"},{"name":"zone","type":"String"}],"returntype":"Void","safe":false},{"name":"setEACL","offset":3348,"parameters":[{"name":"eACL","type":"ByteArray"},{"name":"signature","type":"Signature"},{"name":"publicKey","type":"PublicKey"},{"name":"token","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"startContainerEstimation","offset":4325,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"stopContainerEstimation","offset":4406,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Void","safe":false},{"name":"update","offset":1432,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"version","offset":4486,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"PutSuccess","parameters":[{"name":"containerID","type":"Hash256"},{"name":"publicKey","type":"PublicKey"}]},{"name":"DeleteSuccess","parameters":[{"name":"containerID","type":"ByteArray"}]},{"name":"SetEACLSuccess","parameters":[{"name":"containerID","type":"ByteArray"},{"name":"publicKey","type":"PublicKey"}]},{"name":"StartEstimation","parameters":[{"name":"epoch","type":"Integer"}]},{"name":"StopEstimation","parameters":[{"name":"epoch","type":"Integer"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update","addKey","transferX","register","registerTLD","addRecord","deleteRecords"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file diff --git a/contracts/06-container.nef b/contracts/06-container.nef new file mode 100755 index 0000000000..a42dfcf9d3 Binary files /dev/null and b/contracts/06-container.nef differ diff --git a/contracts/07-netmap.manifest.json b/contracts/07-netmap.manifest.json new file mode 100755 index 0000000000..ba6c0a3826 --- /dev/null +++ b/contracts/07-netmap.manifest.json @@ -0,0 +1 @@ +{"name":"NeoFS Netmap","abi":{"methods":[{"name":"_initialize","offset":0,"parameters":[],"returntype":"Void","safe":false},{"name":"_deploy","offset":93,"parameters":[{"name":"data","type":"Any"},{"name":"isUpdate","type":"Boolean"}],"returntype":"Void","safe":false},{"name":"addPeer","offset":1692,"parameters":[{"name":"nodeInfo","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"addPeerIR","offset":1647,"parameters":[{"name":"nodeInfo","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"config","offset":3153,"parameters":[{"name":"key","type":"ByteArray"}],"returntype":"Any","safe":true},{"name":"epoch","offset":2333,"parameters":[],"returntype":"Integer","safe":true},{"name":"innerRingList","offset":1602,"parameters":[],"returntype":"Array","safe":true},{"name":"lastEpochBlock","offset":2372,"parameters":[],"returntype":"Integer","safe":false},{"name":"listConfig","offset":3239,"parameters":[],"returntype":"Array","safe":true},{"name":"netmap","offset":2411,"parameters":[],"returntype":"Array","safe":true},{"name":"netmapCandidates","offset":2493,"parameters":[],"returntype":"Array","safe":true},{"name":"newEpoch","offset":2042,"parameters":[{"name":"epochNum","type":"Integer"}],"returntype":"Void","safe":false},{"name":"setConfig","offset":3171,"parameters":[{"name":"id","type":"ByteArray"},{"name":"key","type":"ByteArray"},{"name":"val","type":"ByteArray"}],"returntype":"Void","safe":false},{"name":"snapshot","offset":2509,"parameters":[{"name":"diff","type":"Integer"}],"returntype":"Array","safe":true},{"name":"snapshotByEpoch","offset":3105,"parameters":[{"name":"epoch","type":"Integer"}],"returntype":"Array","safe":true},{"name":"update","offset":1472,"parameters":[{"name":"script","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","safe":false},{"name":"updateSnapshotCount","offset":2663,"parameters":[{"name":"count","type":"Integer"}],"returntype":"Void","safe":false},{"name":"updateState","offset":1937,"parameters":[{"name":"state","type":"Integer"},{"name":"publicKey","type":"PublicKey"}],"returntype":"Void","safe":false},{"name":"updateStateIR","offset":2013,"parameters":[{"name":"state","type":"Integer"},{"name":"publicKey","type":"PublicKey"}],"returntype":"Void","safe":false},{"name":"version","offset":3321,"parameters":[],"returntype":"Integer","safe":true}],"events":[{"name":"AddPeerSuccess","parameters":[{"name":"publicKey","type":"PublicKey"}]},{"name":"UpdateStateSuccess","parameters":[{"name":"publicKey","type":"PublicKey"},{"name":"state","type":"Integer"}]},{"name":"NewEpoch","parameters":[{"name":"epoch","type":"Integer"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":["update","newEpoch"]}],"supportedstandards":[],"trusts":[],"extra":null} \ No newline at end of file diff --git a/contracts/07-netmap.nef b/contracts/07-netmap.nef new file mode 100755 index 0000000000..bd4c590a3e Binary files /dev/null and b/contracts/07-netmap.nef differ 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 0000000000..2e4bb372af Binary files /dev/null and b/contracts/08-alphabet.nef differ diff --git a/go.mod b/go.mod index 24448fd59a..96c0621ee6 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 1745d1fb4f..063a2a34ef 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= @@ -422,7 +421,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= @@ -596,7 +594,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/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/config.go b/pkg/innerring/config.go index 35c3132aab..474b2eb3eb 100644 --- a/pkg/innerring/config.go +++ b/pkg/innerring/config.go @@ -14,11 +14,18 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" "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/cast" "github.com/spf13/viper" "go.uber.org/zap" ) +// checks whether Inner Ring app is configured to initialize underlying NeoFS +// Sidechain or await for a background deployment. +func isAutoDeploymentMode(cfg *viper.Viper) bool { + return cfg.IsSet("network_settings") +} + // checks if Inner Ring app is configured to be launched in local consensus // mode. func isLocalConsensusMode(cfg *viper.Viper) bool { @@ -262,6 +269,157 @@ func parseBlockchainConfig(v *viper.Viper, _logger *zap.Logger) (c blockchain.Co return c, nil } +const networkSettingsConfigSection = "network_settings" + +func parseNetworkSettingsConfig(v *viper.Viper) (c netmap.NetworkConfiguration, err error) { + if !v.IsSet(networkSettingsConfigSection) { + return c, fmt.Errorf("missing root section '%s'", networkSettingsConfigSection) + } + + c.EpochDuration, err = parseConfigUint64Range(v, networkSettingsConfigSection+".epoch_duration", "epoch duration", 1, math.MaxUint32) + if err != nil { + return + } + + c.MaxObjectSize, err = parseConfigUint64Range(v, networkSettingsConfigSection+".max_object_size", "max object size", 1, math.MaxUint64) + if err != nil { + return + } + + requireHomoHash, err := parseConfigBool(v, networkSettingsConfigSection+".require_homomorphic_hashing", "is homomorphic hashing required") + if err != nil { + return + } + + c.HomomorphicHashingDisabled = !requireHomoHash + + c.MaintenanceModeAllowed, err = parseConfigBool(v, networkSettingsConfigSection+".allow_maintenance_mode", "is maintenance mode allowed") + if err != nil { + return + } + + const eigenTrustSection = networkSettingsConfigSection + ".eigen_trust" + if !v.IsSet(eigenTrustSection) { + return c, fmt.Errorf("missing EigenTrust section '%s'", eigenTrustSection) + } + + c.EigenTrustAlpha, err = parseConfigFloatRange(v, eigenTrustSection+".alpha", "EigenTrust alpha parameter", 0, 1) + if err != nil { + return + } + + c.EigenTrustIterations, err = parseConfigUint64Range(v, eigenTrustSection+".iterations_number", "number of EigenTrust iterations", 1, math.MaxUint64) + if err != nil { + return + } + + const priceSection = networkSettingsConfigSection + ".price" + if !v.IsSet(priceSection) { + return c, fmt.Errorf("missing price section '%s'", priceSection) + } + + c.StoragePrice, err = parseConfigUint64Max(v, priceSection+".storage", "storage price", math.MaxUint64) + if err != nil { + return + } + + const feeSection = priceSection + ".fee" + if !v.IsSet(feeSection) { + return c, fmt.Errorf("missing fee section '%s'", feeSection) + } + + c.IRCandidateFee, err = parseConfigUint64Max(v, feeSection+".ir_candidate", "Inner Ring candidate fee", math.MaxUint64) + if err != nil { + return + } + + c.WithdrawalFee, err = parseConfigUint64Max(v, feeSection+".withdraw", "withdrawal fee", math.MaxUint64) + if err != nil { + return + } + + c.AuditFee, err = parseConfigUint64Max(v, feeSection+".audit", "data audit fee", math.MaxUint64) + if err != nil { + return + } + + c.ContainerFee, err = parseConfigUint64Max(v, feeSection+".new_container", "container creation fee", math.MaxUint64) + if err != nil { + return + } + + c.ContainerAliasFee, err = parseConfigUint64Max(v, feeSection+".container_domain", "container domain fee", math.MaxUint64) + if err != nil { + return + } + + customSettingsKey := networkSettingsConfigSection + ".custom" + if v.IsSet(customSettingsKey) { + var sss []string + sss, err = parseConfigStrings(v, customSettingsKey, "custom settings") + if err != nil { + return + } + + if len(sss) == 0 { + return c, fmt.Errorf("missing custom settings '%s'", customSettingsKey) + } + + c.Raw = make([]netmap.RawNetworkParameter, len(sss)) + + for i := range sss { + const sep = "=" + ss := strings.Split(sss[i], sep) + if len(ss) != 2 { + return c, fmt.Errorf("invalid %s '%s' (%s-separated key-value): failed to parse element #%d", customSettingsKey, ss[i], sep, i) + } + + switch ss[0] { + default: + for j := 0; j < i; j++ { + if ss[0] == c.Raw[j].Name { + return c, fmt.Errorf("duplicated custom network setting '%s' in '%s'", ss[0], customSettingsKey) + } + } + case "AuditFee", + "BasicIncomeRate", + "ContainerAliasFee", + "ContainerFee", + "EigenTrustAlpha", + "EigenTrustIterations", + "EpochDuration", + "HomomorphicHashingDisabled", + "InnerRingCandidateFee", + "MaintenanceModeAllowed", + "MaxObjectSize", + "WithdrawFee": + return c, fmt.Errorf("invalid %s '%s' (%s-separated key-value): key to element #%d is forbidden", customSettingsKey, ss[i], sep, i) + } + + c.Raw[i].Name = ss[0] + c.Raw[i].Value = []byte(ss[1]) + } + } + + return +} + +type nnsConfig struct { + systemEmail string +} + +func parseNNSConfig(v *viper.Viper) (c nnsConfig, err error) { + const rootSection = "nns" + + if !v.IsSet(rootSection) { + return c, fmt.Errorf("missing root section '%s'", rootSection) + } + + c.systemEmail, err = parseConfigString(v, rootSection+".system_email", "system email for NNS") + + return +} + var errMissingConfig = errors.New("config value is missing") func parseConfigUint64Condition(v *viper.Viper, key, desc string, cond func(uint64) error) (uint64, error) { @@ -271,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) } @@ -404,3 +568,62 @@ func parseConfigMapUint32(v *viper.Viper, key, desc string, limit uint64) (map[s return err }) } + +func parseConfigBool(v *viper.Viper, key, desc string) (bool, error) { + var res bool + var err error + if !v.IsSet(key) { + err = errMissingConfig + } + if err == nil { + switch val := v.GetString(key); val { + default: + err = errors.New("neither true nor false") + case "false": + case "true": + res = true + } + } + if err != nil { + return res, fmt.Errorf("invalid %s '%s' (boolean): %w", desc, key, err) + } + return res, nil +} + +func parseConfigFloatRange(v *viper.Viper, key, desc string, min, max float64) (float64, error) { + var res float64 + var err error + if !v.IsSet(key) { + err = errMissingConfig + } + if err == nil { + res, err = cast.ToFloat64E(v.Get(key)) + if err == nil { + if res < min || res > 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..74d9f59def 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) @@ -250,6 +254,7 @@ func TestConfigParser(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"})}, @@ -301,7 +306,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 +322,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 +366,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 +435,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 d05b7d42e9..bd3288a06d 100644 --- a/pkg/innerring/contracts.go +++ b/pkg/innerring/contracts.go @@ -9,7 +9,10 @@ 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" "go.uber.org/zap" @@ -88,26 +91,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 +118,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 +135,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) { @@ -201,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..ab4caf270d --- /dev/null +++ b/pkg/innerring/deploy.go @@ -0,0 +1,155 @@ +package innerring + +import ( + "encoding/json" + "fmt" + "math" + "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) + } + + 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 +} + +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/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/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) } 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/morph/deploy/alphabet.go b/pkg/morph/deploy/alphabet.go new file mode 100644 index 0000000000..4ddd5fab88 --- /dev/null +++ b/pkg/morph/deploy/alphabet.go @@ -0,0 +1,298 @@ +package deploy + +import ( + "context" + "errors" + "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" +) + +// 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) + } +} + +// 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/contracts.go b/pkg/morph/deploy/contracts.go index dcce0d988e..80a29c4fa9 100644 --- a/pkg/morph/deploy/contracts.go +++ b/pkg/morph/deploy/contracts.go @@ -1,7 +1,345 @@ 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/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/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 + + neoFS NeoFS + + // 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 + + // with localAcc signer only + simpleLocalActor *actor.Actor + // committee multi-sig signs, localAcc pays + committeeLocalActor *notary.Actor + + 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 (noExtraUpdateArgs may be + // used). + buildExtraUpdateArgs func() ([]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 +// 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) + } + + 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) + 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(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(prm.simpleLocalActor) + updateTxMonitor := newTransactionGroupMonitor(prm.simpleLocalActor) + setContractRecordPrm := setNeoFSContractDomainRecordPrm{ + logger: l, + setRecordTxMonitor: newTransactionGroupMonitor(prm.simpleLocalActor), + registerTLDTxMonitor: newTransactionGroupMonitor(prm.simpleLocalActor), + nnsContract: prm.nnsContract, + systemEmail: prm.systemEmail, + localActor: prm.simpleLocalActor, + committeeActor: prm.committeeLocalActor, + domain: domainNameForAddress, + record: "", // set in for loop + } + + 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 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 + } + + 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...") + + 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 := 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") + } 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 !missingDomainRecord { + return onChainState.Hash, nil + } + } else { + extraUpdateArgs, err := prm.buildExtraUpdateArgs() + if err != nil { + l.Error("failed to prepare build extra arguments for the contract update, will try again later", + zap.Error(err)) + continue + } + + if prm.isProxy && proxyCommitteeActor == nil { + err = initProxyCommitteeActor(onChainState.Hash) + if err != nil { + return util.Uint160{}, err + } + } + + tx, err := proxyCommitteeActor.MakeTunedCall(onChainState.Hash, methodUpdate, nil, updateTxModifier, + bLocalNEF, jLocalManifest, extraUpdateArgs) + if err != nil { + if isErrContractAlreadyUpdated(err) { + l.Info("the contract is unchanged or has already been updated") + if !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 := 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") + } 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 + } + + setContractRecordPrm.record = onChainState.Hash.StringLE() + + setNeoFSContractDomainRecord(ctx, setContractRecordPrm) + } +} diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index 09328a5811..f2891c8df2 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -5,17 +5,26 @@ import ( "context" "errors" "fmt" + "math" + "math/big" "sort" + "strconv" "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" + "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 +44,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 @@ -60,6 +73,22 @@ 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 + // Duration of the single NeoFS epoch measured in Sidechain blocks. + EpochDuration 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 @@ -72,6 +101,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. @@ -83,10 +153,26 @@ 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 + // Running NeoFS network for which deployment procedure is performed. + NeoFS NeoFS + 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 @@ -100,9 +186,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. deployment/update of the NeoFS system contracts (currently only NNS) -// 5. deployment of custom contracts +// 3. initial GAS distribution between committee members +// 4. committee group initialization +// 5. Alphabet initialization (incl. registration as candidates to validators) +// 6. deployment/update of the NeoFS system contracts +// 7. distribution of all available NEO between the Alphabet contracts // // See project documentation for details. func Deploy(ctx context.Context, prm Prm) error { @@ -134,6 +222,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) @@ -208,15 +306,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{ @@ -236,19 +352,166 @@ 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{ + 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") + + 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, + simpleLocalActor: simpleLocalActor, + committeeLocalActor: committeeLocalActor, + } + + 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 + + // 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) + 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 + } + + // 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 + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + syncPrm.buildExtraUpdateArgs = 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("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") + + 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 + // Proxy contract. 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, + 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) @@ -256,9 +519,269 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("on-chain NNS contract successfully updated") - // TODO: deploy/update other contracts + // 2. Audit + syncPrm.localNEF = prm.AuditContract.Common.NEF + syncPrm.localManifest = prm.AuditContract.Common.Manifest + syncPrm.domainName = domainAudit + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { + return []interface{}{notaryDisabledExtraUpdateArg}, 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)) + + // 3. Balance + syncPrm.localNEF = prm.BalanceContract.Common.NEF + syncPrm.localManifest = prm.BalanceContract.Common.Manifest + syncPrm.domainName = domainBalance + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { + return []interface{}{notaryDisabledExtraUpdateArg}, 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)) + + // 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) + } + return []interface{}{ + notaryDisabledExtraUpdateArg, + netmapContractAddress, + }, nil + } + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, error) { + return []interface{}{notaryDisabledExtraUpdateArg}, 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)) + + // 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 { + return nil, fmt.Errorf("resolve address of the Netmap 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...") + + 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 + + // 7. 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) { + return []interface{}{ + notaryDisabledExtraUpdateArg, + balanceContractAddress, + containerContractAddress, + []interface{}(nil), // keys, currently unused + netConfig, + }, nil + } + syncPrm.buildExtraUpdateArgs = func() ([]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)) + + // 8. Alphabet + syncPrm.localNEF = prm.AlphabetContract.Common.NEF + syncPrm.localManifest = prm.AlphabetContract.Common.Manifest + syncPrm.buildExtraUpdateArgs = func() ([]interface{}, 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) + syncPrm.buildExtraDeployArgs = func() ([]interface{}, error) { + 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)) + + 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 } -func noExtraUpdateArgs(contractVersion) ([]interface{}, error) { return nil, nil } +func noExtraUpdateArgs() ([]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() +} + +// 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) + if math.MaxUint32-neoFSState.CurrentEpochBlock > neoFSState.EpochDuration { + tx.ValidUntilBlock = neoFSState.CurrentEpochBlock + neoFSState.EpochDuration + } else { + tx.ValidUntilBlock = math.MaxUint32 + } + + return nil + } +} diff --git a/pkg/morph/deploy/funds.go b/pkg/morph/deploy/funds.go new file mode 100644 index 0000000000..b6d62cc803 --- /dev/null +++ b/pkg/morph/deploy/funds.go @@ -0,0 +1,456 @@ +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/util" + "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 + // share of GAS on the committee multi-sig account to be transferred to the + // Proxy contract (in %). + initialProxyGASPercent = 90 +) + +// 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) + } +} + +// 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) + } +} + +// 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) + } +} diff --git a/pkg/morph/deploy/group.go b/pkg/morph/deploy/group.go index 699cce1805..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" @@ -176,7 +177,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 +188,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 +223,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", @@ -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 f8c95abbb5..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" @@ -29,8 +30,28 @@ 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" + + domainCommitteeGroup = "group" ) +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 +62,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 @@ -229,6 +251,8 @@ type updateNNSContractPrm struct { blockchain Blockchain + neoFS NeoFS + // based on blockchain monitor *blockchainMonitor @@ -244,7 +268,11 @@ 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. + proxyContract util.Uint160 } // updateNNSContract synchronizes on-chain NNS contract (its presence is a @@ -269,14 +297,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) - } - - localVersion, err := readContractLocalVersion(prm.blockchain, prm.localNEF, prm.localManifest) - if err != nil { - return fmt.Errorf("read version of the local NNS contract: %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 @@ -284,6 +307,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) { @@ -304,35 +328,10 @@ 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) + 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 } @@ -341,13 +340,11 @@ 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) { - // 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 } @@ -376,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) +} diff --git a/pkg/morph/deploy/notary.go b/pkg/morph/deploy/notary.go index 9e255d7c9b..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) } @@ -842,10 +850,54 @@ 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 + }) +} + +// 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. 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. 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 +// account for the parameterized committee. +// +// 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), + extraSigners ...actor.SignerAccount, +) (*notary.Actor, error) { committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(committee)) committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(localAcc.PrivateKey()) @@ -854,22 +906,27 @@ func newCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee k return nil, fmt.Errorf("compose committee multi-signature account: %w", err) } - return notary.NewActor(b, []actor.SignerAccount{ - { - Signer: transaction.Signer{ - Account: localAcc.ScriptHash(), - Scopes: transaction.None, - }, - Account: localAcc, + committeeSignerAcc := actor.SignerAccount{ + Signer: transaction.Signer{ + Account: committeeMultiSigAcc.ScriptHash(), }, + Account: committeeMultiSigAcc, + } + + fCommitteeSigner(&committeeSignerAcc.Signer) + + signers := []actor.SignerAccount{ { Signer: transaction.Signer{ - Account: committeeMultiSigAcc.ScriptHash(), - Scopes: transaction.CalledByEntry, + Account: payerAcc.ScriptHash(), + Scopes: transaction.None, }, - Account: committeeMultiSigAcc, + Account: payerAcc, }, - }, localAcc) + committeeSignerAcc, + } + + return notary.NewActor(b, append(signers, extraSigners...), localAcc) } // Amount of GAS for the single local account's GAS->Notary transfer. Relatively @@ -964,6 +1021,8 @@ type listenCommitteeNotaryRequestsPrm struct { localAcc *wallet.Account committee keys.PublicKeys + + validatorMultiSigAcc *wallet.Account } // listenCommitteeNotaryRequests starts background process listening to incoming @@ -980,6 +1039,14 @@ 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 // cache processed operations: when main transaction from received notary @@ -987,9 +1054,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) } @@ -1004,6 +1069,7 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar prm.logger.Info("listening to committee notary requests...") + upperLoop: for { select { case <-ctx.Done(): @@ -1018,102 +1084,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 + 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.HasSigner(committeeMultiSigAccID): - prm.logger.Info("committee is not a signer of main transaction from the received notary request, 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 - case len(mainTx.Scripts) == 0: - prm.logger.Info("missing scripts of main transaction from the received notary request, skip") + 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 } - 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)) + 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 + } + } + + if notaryContractSignerIndex < 0 { + l.Info("Notary contract is not a signer of main transaction of the received notary request, skip") 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)) + 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 } + signers := make([]actor.SignerAccount, 0, len(mainTx.Signers)-1) // Notary contract added by actor + + for i := range mainTx.Signers { + if i == notaryContractSignerIndex { + continue + } + + 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...") + + // 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: notary.FakeSimpleAccount(senderKey), - }, - { - Signer: mainTx.Signers[1], - Account: committeeMultiSigAcc, - }, - }, 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") } } }() diff --git a/pkg/morph/deploy/util.go b/pkg/morph/deploy/util.go index 473ff1bd63..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,21 +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/crypto/keys" - "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" "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/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" ) @@ -33,6 +25,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()) @@ -173,105 +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. -func readContractLocalVersion(rpc invoker.RPCInvoke, localNEF nef.File, localManifest manifest.Manifest) (contractVersion, error) { - 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) - } - - 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.Opcodes(script.BinWriter, opcode.PUSH2, opcode.PICKITEM) - emit.Syscall(script.BinWriter, interopnames.SystemContractCall) - - res, err := invoker.New(rpc, nil).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) } @@ -310,3 +207,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/morph/deploy/util_test.go b/pkg/morph/deploy/util_test.go deleted file mode 100644 index 2d2816e5cd..0000000000 --- a/pkg/morph/deploy/util_test.go +++ /dev/null @@ -1,154 +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/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"}) - - res, err := readContractLocalVersion(newTestRPCInvoker(t, e), *ctr.NEF, *ctr.Manifest) - require.NoError(t, err) - require.EqualValues(t, version, res.toUint64()) -} 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)) + } +} 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) +}