diff --git a/go.mod b/go.mod index cdf7e4f7d6..783aeb0f34 100644 --- a/go.mod +++ b/go.mod @@ -94,11 +94,13 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.4.0 // indirect golang.org/x/exp v0.0.0-20221227203929-1b447090c38c // indirect + golang.org/x/mod v0.6.0 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.1.0 // indirect + golang.org/x/tools v0.2.0 // indirect google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 938fe6c268..0e6ae1e750 100644 --- a/go.sum +++ b/go.sum @@ -256,6 +256,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 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= @@ -572,6 +573,7 @@ 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.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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= @@ -780,6 +782,7 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/morph/deploy/contracts.go b/pkg/morph/deploy/contracts.go new file mode 100644 index 0000000000..dcce0d988e --- /dev/null +++ b/pkg/morph/deploy/contracts.go @@ -0,0 +1,7 @@ +package deploy + +// various common methods of the NeoFS contracts. +const ( + methodUpdate = "update" + methodVersion = "version" +) diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index 20a55a35a9..09328a5811 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -11,7 +11,8 @@ import ( "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/neorpc" - "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "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/wallet" @@ -21,9 +22,9 @@ import ( // Blockchain groups services provided by particular Neo blockchain network // representing NeoFS Sidechain that are required for its deployment. type Blockchain interface { - // RPCActor groups functions needed to compose and send transactions to the - // blockchain. - actor.RPCActor + // RPCActor groups functions needed to compose and send transactions (incl. + // Notary service requests) to the blockchain. + notary.RPCActor // GetCommittee returns list of public keys owned by Neo blockchain committee // members. Resulting list is non-empty, unique and unsorted. @@ -40,7 +41,14 @@ type Blockchain interface { // to stop the process via Unsubscribe. ReceiveBlocks(*neorpc.BlockFilter, chan<- *block.Block) (id string, err error) - // Unsubscribe stops background process started by ReceiveBlocks by ID. + // ReceiveNotaryRequests starts background process that forwards new notary + // requests of the blockchain to the provided channel. The process skips + // requests that don't match specified filter. Returns unique identifier to be + // used to stop the process via Unsubscribe. + ReceiveNotaryRequests(*neorpc.TxFilter, chan<- *result.NotaryRequestEvent) (string, error) + + // Unsubscribe stops background process started by ReceiveBlocks or + // ReceiveNotaryRequests by ID. Unsubscribe(id string) error } @@ -93,11 +101,16 @@ type Prm struct { // 1. NNS contract deployment // 2. launch of a notary service for the committee // 3. committee group initialization -// 4. deployment of the NeoFS system contracts (currently not done) +// 4. deployment/update of the NeoFS system contracts (currently only NNS) // 5. deployment of custom contracts // // See project documentation for details. func Deploy(ctx context.Context, prm Prm) 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() + committee, err := prm.Blockchain.GetCommittee() if err != nil { return fmt.Errorf("get Neo committee of the network: %w", err) @@ -121,9 +134,19 @@ func Deploy(ctx context.Context, prm Prm) error { return errors.New("local account does not belong to any Neo committee member") } + chNewBlock := make(chan struct{}, 1) + + monitor, err := newBlockchainMonitor(prm.Logger, prm.Blockchain, chNewBlock) + if err != nil { + return fmt.Errorf("init blockchain monitor: %w", err) + } + + defer monitor.stop() + deployNNSPrm := deployNNSContractPrm{ logger: prm.Logger, blockchain: prm.Blockchain, + monitor: monitor, localAcc: prm.LocalAccount, localNEF: prm.NNS.Common.NEF, localManifest: prm.NNS.Common.Manifest, @@ -169,6 +192,7 @@ func Deploy(ctx context.Context, prm Prm) error { err = enableNotary(ctx, enableNotaryPrm{ logger: prm.Logger, blockchain: prm.Blockchain, + monitor: monitor, nnsOnChainAddress: nnsOnChainAddress, systemEmail: prm.NNS.SystemEmail, committee: committee, @@ -181,11 +205,24 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("Notary service successfully enabled for the committee") + 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, + }) + if err != nil { + return fmt.Errorf("start listener of committee notary requests: %w", err) + } + prm.Logger.Info("initializing committee group for contract management...") committeeGroupKey, err := initCommitteeGroup(ctx, initCommitteeGroupPrm{ logger: prm.Logger, blockchain: prm.Blockchain, + monitor: monitor, nnsOnChainAddress: nnsOnChainAddress, systemEmail: prm.NNS.SystemEmail, committee: committee, @@ -199,7 +236,29 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("committee group successfully initialized", zap.Stringer("public key", committeeGroupKey.PublicKey())) - // TODO: deploy contracts + prm.Logger.Info("updating on-chain NNS contract...") + + err = updateNNSContract(ctx, updateNNSContractPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + localAcc: prm.LocalAccount, + localNEF: prm.NNS.Common.NEF, + localManifest: prm.NNS.Common.Manifest, + systemEmail: prm.NNS.SystemEmail, + committee: committee, + committeeGroupKey: committeeGroupKey, + buildVersionedExtraUpdateArgs: noExtraUpdateArgs, + }) + if err != nil { + return fmt.Errorf("update NNS contract on the chain: %w", err) + } + + prm.Logger.Info("on-chain NNS contract successfully updated") + + // TODO: deploy/update other contracts return nil } + +func noExtraUpdateArgs(contractVersion) ([]interface{}, error) { return nil, nil } diff --git a/pkg/morph/deploy/group.go b/pkg/morph/deploy/group.go index f26e57b8ec..ed1739628b 100644 --- a/pkg/morph/deploy/group.go +++ b/pkg/morph/deploy/group.go @@ -24,6 +24,9 @@ type initCommitteeGroupPrm struct { blockchain Blockchain + // based on blockchain + monitor *blockchainMonitor + nnsOnChainAddress util.Uint160 systemEmail string @@ -36,11 +39,10 @@ type initCommitteeGroupPrm struct { // initCommitteeGroup initializes committee group and returns corresponding private key. func initCommitteeGroup(ctx context.Context, prm initCommitteeGroupPrm) (*keys.PrivateKey, error) { - monitor, err := newBlockchainMonitor(prm.logger, prm.blockchain) - if err != nil { - return nil, fmt.Errorf("init blockchain monitor: %w", err) - } - defer monitor.stop() + // 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) const leaderCommitteeIndex = 0 @@ -48,7 +50,7 @@ func initCommitteeGroup(ctx context.Context, prm initCommitteeGroupPrm) (*keys.P var leaderTick func() upperLoop: - for ; ; monitor.waitForNextBlock(ctx) { + for ; ; prm.monitor.waitForNextBlock(ctx) { select { case <-ctx.Done(): return nil, fmt.Errorf("wait for committee group key to be distributed: %w", ctx.Err()) @@ -100,6 +102,8 @@ upperLoop: continue } + var err error + if committeeGroupKey == nil { committeeGroupKey, err = prm.keyStorage.GetPersistedPrivateKey() if err != nil { @@ -109,7 +113,7 @@ upperLoop: } if leaderTick == nil { - leaderTick, err = initShareCommitteeGroupKeyAsLeaderTick(prm, monitor, committeeGroupKey) + leaderTick, err = initShareCommitteeGroupKeyAsLeaderTick(ctx, prm, committeeGroupKey) if err != nil { prm.logger.Error("failed to construct action sharing committee group key between committee members as leader, will try again later", zap.Error(err)) @@ -124,16 +128,17 @@ upperLoop: // initShareCommitteeGroupKeyAsLeaderTick returns a function that preserves // context of the committee group key distribution by leading committee member // between calls. -func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, monitor *blockchainMonitor, committeeGroupKey *keys.PrivateKey) (func(), error) { - _actor, err := actor.NewSimple(prm.blockchain, prm.localAcc) +func initShareCommitteeGroupKeyAsLeaderTick(ctx context.Context, prm initCommitteeGroupPrm, committeeGroupKey *keys.PrivateKey) (func(), error) { + localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) if err != nil { return nil, fmt.Errorf("init transaction sender from local account: %w", err) } - _invoker := invoker.New(prm.blockchain, nil) + invkr := invoker.New(prm.blockchain, nil) // multi-tick context - mDomainsToVubs := make(map[string][2]uint32) // 1st - register, 2nd - addRecord + mDomainsToRegisterTxs := make(map[string]*transactionGroupMonitor, len(prm.committee)) + mDomainsToSetRecordTxs := make(map[string]*transactionGroupMonitor, len(prm.committee)) return func() { prm.logger.Info("distributing committee group key between committee members using NNS...") @@ -144,63 +149,57 @@ func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, monitor * l.Info("synchronizing committee group key with NNS domain record...") - _, err := lookupNNSDomainRecord(_invoker, prm.nnsOnChainAddress, domain) + _, err := lookupNNSDomainRecord(invkr, prm.nnsOnChainAddress, domain) if err != nil { if errors.Is(err, errMissingDomain) { l.Info("NNS domain is missing, registration is needed") - vubs, ok := mDomainsToVubs[domain] - if ok && vubs[0] > 0 { - l.Info("transaction registering NNS domain was sent earlier, checking relevance...") - - if cur := monitor.currentHeight(); cur <= vubs[0] { - l.Info("previously sent transaction registering NNS domain may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", vubs[0])) - return + txMonitor, ok := mDomainsToRegisterTxs[domain] + if ok { + if txMonitor.isPending() { + l.Info("previously sent transaction registering NNS domain is still pending, will wait for the outcome") + continue } - - l.Info("previously sent transaction registering NNS domain expired without side-effect") + } else { + txMonitor = newTransactionGroupMonitor(localActor) + mDomainsToRegisterTxs[domain] = txMonitor } l.Info("sending new transaction registering domain in the NNS...") - _, vub, err := _actor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, - domain, _actor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) + txID, vub, err := localActor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, + domain, localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) if err != nil { - vubs[0] = 0 - mDomainsToVubs[domain] = vubs if isErrNotEnoughGAS(err) { l.Info("not enough GAS to register domain in the NNS, will try again later") } else { l.Error("failed to send transaction registering domain in the NNS, will try again later", zap.Error(err)) } - return + continue } - vubs[0] = vub - mDomainsToVubs[domain] = vubs + l.Info("transaction registering domain in the NNS has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub)) - l.Info("transaction registering domain in the NNS has been successfully sent, will wait for the outcome") + txMonitor.trackPendingTransactionsAsync(ctx, vub, txID) continue } else if !errors.Is(err, errMissingDomainRecord) { l.Error("failed to lookup NNS domain record, will try again later", zap.Error(err)) - return + continue } l.Info("missing record of the NNS domain, needed to be set") - vubs, ok := mDomainsToVubs[domain] - if ok && vubs[1] > 0 { - l.Info("transaction setting NNS domain record was sent earlier, checking relevance...") - - if cur := monitor.currentHeight(); cur <= vubs[1] { - l.Info("previously sent transaction setting NNS domain record may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", vubs[1])) - return + txMonitor, ok := mDomainsToSetRecordTxs[domain] + if ok { + if txMonitor.isPending() { + l.Info("previously sent transaction setting NNS domain record is still pending, will wait for the outcome") + continue } - - l.Info("previously sent transaction setting NNS domain record expired without side-effect") + } else { + txMonitor = newTransactionGroupMonitor(localActor) + mDomainsToSetRecordTxs[domain] = txMonitor } l.Info("sharing encrypted committee group key with the committee member...") @@ -209,28 +208,26 @@ func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, monitor * if err != nil { l.Error("failed to encrypt committee group key to share with the committee member, will try again later", zap.Error(err)) - return + continue } l.Info("sending new transaction setting domain record in the NNS...") - _, vub, err := _actor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, + txID, vub, err := localActor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, domain, int64(nns.TXT), keyCipher) if err != nil { - vubs[1] = 0 - mDomainsToVubs[domain] = vubs if isErrNotEnoughGAS(err) { l.Info("not enough GAS to set NNS domain record, will try again later") } else { l.Error("failed to send transaction setting NNS domain record, will try again later", zap.Error(err)) } - return + continue } - vubs[1] = vub - mDomainsToVubs[domain] = vubs + l.Info("transaction setting NNS domain record has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub)) - l.Info("transaction setting NNS domain record has been successfully sent, will wait for the outcome") + txMonitor.trackPendingTransactionsAsync(ctx, vub, txID) continue } diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index 109a0242ce..66f9bec002 100644 --- a/pkg/morph/deploy/nns.go +++ b/pkg/morph/deploy/nns.go @@ -2,6 +2,7 @@ package deploy import ( "context" + "encoding/json" "errors" "fmt" "strings" @@ -66,6 +67,9 @@ type deployNNSContractPrm struct { blockchain Blockchain + // based on blockchain + monitor *blockchainMonitor + localAcc *wallet.Account localNEF nef.File @@ -91,17 +95,21 @@ type deployNNSContractPrm struct { // If contract is missing and deployNNSContractPrm.initCommitteeGroupKey is provided, // initNNSContract attempts to deploy local contract. func initNNSContract(ctx context.Context, prm deployNNSContractPrm) (res util.Uint160, err error) { - monitor, err := newBlockchainMonitor(prm.logger, prm.blockchain) + localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) if err != nil { - return res, fmt.Errorf("init blockchain monitor: %w", err) + return res, fmt.Errorf("init transaction sender from local account: %w", err) } - defer monitor.stop() - var managementContract *management.Contract - var sentTxValidUntilBlock uint32 + // 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() + var committeeGroupKey *keys.PrivateKey + txMonitor := newTransactionGroupMonitor(localActor) + managementContract := management.New(localActor) - for ; ; monitor.waitForNextBlock(ctx) { + for ; ; prm.monitor.waitForNextBlock(ctx) { select { case <-ctx.Done(): return res, fmt.Errorf("wait for NNS contract synchronization: %w", ctx.Err()) @@ -143,47 +151,29 @@ func initNNSContract(ctx context.Context, prm deployNNSContractPrm) (res util.Ui continue } + setGroupInManifest(&prm.localManifest, prm.localNEF, committeeGroupKey, prm.localAcc.ScriptHash()) + prm.logger.Info("private key of the committee group has been initialized", zap.Stringer("public key", committeeGroupKey.PublicKey())) } - if sentTxValidUntilBlock > 0 { - prm.logger.Info("transaction deploying NNS contract was sent earlier, checking relevance...") - - if cur := monitor.currentHeight(); cur <= sentTxValidUntilBlock { - prm.logger.Info("previously sent transaction deploying NNS contract may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", sentTxValidUntilBlock)) - continue - } - - prm.logger.Info("previously sent transaction deploying NNS contract expired without side-effect") + if txMonitor.isPending() { + prm.logger.Info("previously sent transaction updating NNS contract is still pending, will wait for the outcome") + continue } prm.logger.Info("sending new transaction deploying NNS contract...") - if managementContract == nil { - _actor, err := actor.NewSimple(prm.blockchain, prm.localAcc) - if err != nil { - prm.logger.Warn("NNS contract is missing on the chain but attempts to deploy are disabled, will try again later") - continue - } - - managementContract = management.New(_actor) - - setGroupInManifest(&prm.localManifest, prm.localNEF, committeeGroupKey, prm.localAcc.ScriptHash()) - } - // just to definitely avoid mutation nefCp := prm.localNEF manifestCp := prm.localManifest - _, vub, err := managementContract.Deploy(&nefCp, &manifestCp, []interface{}{ + txID, vub, err := managementContract.Deploy(&nefCp, &manifestCp, []interface{}{ []interface{}{ []interface{}{domainBootstrap, prm.systemEmail}, []interface{}{domainContractAddresses, prm.systemEmail}, }, }) if err != nil { - sentTxValidUntilBlock = 0 if isErrNotEnoughGAS(err) { prm.logger.Info("not enough GAS to deploy NNS contract, will try again later") } else { @@ -192,9 +182,11 @@ func initNNSContract(ctx context.Context, prm deployNNSContractPrm) (res util.Ui continue } - sentTxValidUntilBlock = vub + prm.logger.Info("transaction deploying NNS contract has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub), + ) - prm.logger.Info("transaction deploying NNS contract has been successfully sent, will wait for the outcome") + txMonitor.trackPendingTransactionsAsync(ctx, vub, txID) } } @@ -229,3 +221,157 @@ func lookupNNSDomainRecord(inv *invoker.Invoker, nnsContract util.Uint160, domai return "", errMissingDomainRecord } + +// updateNNSContractPrm groups parameters of NeoFS NNS contract update. +type updateNNSContractPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + localAcc *wallet.Account + + localNEF nef.File + localManifest manifest.Manifest + systemEmail string + + committee keys.PublicKeys + committeeGroupKey *keys.PrivateKey + + // 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) +} + +// updateNNSContract synchronizes on-chain NNS contract (its presence is a +// precondition) with the local one represented by compiled executables. If +// on-chain version is greater or equal to the local one, nothing happens. +// Otherwise, transaction calling 'update' method is sent. +// +// Local manifest is extended with committee group represented by the +// parameterized private key. +// +// Function behaves similar to initNNSContract in terms of context. +func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { + bLocalNEF, err := prm.localNEF.Bytes() + if err != nil { + // not really expected + return fmt.Errorf("encode local NEF of the NNS contract into binary: %w", err) + } + + jLocalManifest, err := json.Marshal(prm.localManifest) + if err != nil { + // not really expected + return fmt.Errorf("encode local manifest of the NNS contract into JSON: %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) + } + + localVersion, err := readContractLocalVersion(prm.blockchain, prm.localNEF, prm.localManifest) + if err != nil { + return fmt.Errorf("read version of the local NNS contract: %w", err) + } + + // wrap the parent context into the context of the current function so that + // transaction wait routines do not leak + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + txMonitor := newTransactionGroupMonitor(committeeActor) + + for ; ; prm.monitor.waitForNextBlock(ctx) { + select { + case <-ctx.Done(): + return fmt.Errorf("wait for NNS contract synchronization: %w", ctx.Err()) + default: + } + + prm.logger.Info("reading on-chain state of the NNS contract...") + + nnsOnChainState, err := readNNSOnChainState(prm.blockchain) + if err != nil { + prm.logger.Error("failed to read on-chain state of the NNS contract, will try again later", zap.Error(err)) + continue + } else if nnsOnChainState == nil { + // NNS contract must be deployed at this stage + return errors.New("missing required NNS contract on the chain") + } + + if nnsOnChainState.NEF.Checksum == prm.localNEF.Checksum { + // manifests may differ, but currently we should bump internal contract version + // (i.e. change NEF) to make such updates. Right now they are not supported due + // to dubious practical need + // Track https://github.com/nspcc-dev/neofs-contract/issues/340 + prm.logger.Info("same local and on-chain checksums of the NNS contract NEF, update is not needed") + return nil + } + + prm.logger.Info("NEF checksums of the on-chain and local NNS contracts differ, need an update") + + versionOnChain, err := readContractOnChainVersion(prm.blockchain, nnsOnChainState.Hash) + if err != nil { + prm.logger.Error("failed to read on-chain version of the NNS contract, will try again later", zap.Error(err)) + continue + } + + if v := localVersion.cmp(versionOnChain); v == -1 { + prm.logger.Info("local contract version is < than the on-chain one, update is not needed", + zap.Stringer("local", localVersion), zap.Stringer("on-chain", versionOnChain)) + return nil + } else if v == 0 { + return fmt.Errorf("local and on-chain contracts have different NEF checksums but same version '%s'", versionOnChain) + } + + extraUpdateArgs, err := prm.buildVersionedExtraUpdateArgs(versionOnChain) + if err != nil { + prm.logger.Error("failed to prepare build extra arguments for NNS contract update, will try again later", + zap.Stringer("on-chain version", versionOnChain), zap.Error(err)) + continue + } + + setGroupInManifest(&prm.localManifest, prm.localNEF, prm.committeeGroupKey, prm.localAcc.ScriptHash()) + + // 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, + 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") + return nil + } + + prm.logger.Error("failed to make transaction updating NNS contract, will try again later", zap.Error(err)) + continue + } + + if txMonitor.isPending() { + prm.logger.Info("previously sent notary request updating NNS contract is still pending, will wait for the outcome") + continue + } + + mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(tx, nil) + if err != nil { + if isErrNotEnoughGAS(err) { + prm.logger.Info("insufficient Notary balance to send new Notary request updating NNS contract, skip") + } else { + prm.logger.Error("failed to send new Notary request updating NNS contract, skip", zap.Error(err)) + } + continue + } + + prm.logger.Info("notary request updating NNS contract 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/notary.go b/pkg/morph/deploy/notary.go index 799a705061..64c4e7cb76 100644 --- a/pkg/morph/deploy/notary.go +++ b/pkg/morph/deploy/notary.go @@ -3,21 +3,31 @@ package deploy import ( "bytes" "context" + "crypto/elliptic" "crypto/sha256" "encoding/base64" "encoding/binary" "errors" "fmt" + "math" + "math/big" + "github.com/nspcc-dev/neo-go/pkg/core/mempoolevent" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neorpc" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" "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/rolemgmt" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/wallet" randutil "github.com/nspcc-dev/neofs-node/pkg/util/rand" @@ -31,6 +41,9 @@ type enableNotaryPrm struct { blockchain Blockchain + // based on blockchain + monitor *blockchainMonitor + nnsOnChainAddress util.Uint160 systemEmail string @@ -41,18 +54,18 @@ type enableNotaryPrm struct { // enableNotary makes Notary service ready-to-go for the committee members. func enableNotary(ctx context.Context, prm enableNotaryPrm) error { - monitor, err := newBlockchainMonitor(prm.logger, prm.blockchain) - if err != nil { - return fmt.Errorf("init blockchain monitor: %w", err) - } - defer monitor.stop() + // 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() var tick func() + var err error if len(prm.committee) == 1 { prm.logger.Info("committee is single-acc, no multi-signature needed for Notary role designation") - tick, err = initDesignateNotaryRoleToLocalAccountTick(prm, monitor) + tick, err = initDesignateNotaryRoleToLocalAccountTick(ctx, prm) if err != nil { return fmt.Errorf("construct action designating Notary role to the local account: %w", err) } @@ -60,12 +73,12 @@ func enableNotary(ctx context.Context, prm enableNotaryPrm) error { prm.logger.Info("committee is multi-acc, multi-signature is needed for Notary role designation") if prm.localAccCommitteeIndex == 0 { - tick, err = initDesignateNotaryRoleAsLeaderTick(prm, monitor) + tick, err = initDesignateNotaryRoleAsLeaderTick(ctx, prm) if err != nil { return fmt.Errorf("construct action designating Notary role to the multi-acc committee as leader: %w", err) } } else { - tick, err = initDesignateNotaryRoleAsSignerTick(prm, monitor) + tick, err = initDesignateNotaryRoleAsSignerTick(ctx, prm) if err != nil { return fmt.Errorf("construct action designating Notary role to the multi-acc committee as signer: %w", err) } @@ -74,7 +87,7 @@ func enableNotary(ctx context.Context, prm enableNotaryPrm) error { roleContract := rolemgmt.NewReader(invoker.New(prm.blockchain, nil)) - for ; ; monitor.waitForNextBlock(ctx) { + for ; ; prm.monitor.waitForNextBlock(ctx) { select { case <-ctx.Done(): return fmt.Errorf("wait for Notary service to be enabled for the committee: %w", ctx.Err()) @@ -83,7 +96,7 @@ func enableNotary(ctx context.Context, prm enableNotaryPrm) error { prm.logger.Info("checking Notary role of the committee members...") - accsWithNotaryRole, err := roleContract.GetDesignatedByRole(noderoles.P2PNotary, monitor.currentHeight()) + accsWithNotaryRole, err := roleContract.GetDesignatedByRole(noderoles.P2PNotary, prm.monitor.currentHeight()) if err != nil { prm.logger.Error("failed to check role of the committee, will try again later", zap.Error(err)) continue @@ -111,42 +124,29 @@ 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(prm enableNotaryPrm, monitor *blockchainMonitor) (func(), error) { - _actor, err := actor.NewSimple(prm.blockchain, prm.localAcc) +func initDesignateNotaryRoleToLocalAccountTick(ctx context.Context, prm enableNotaryPrm) (func(), error) { + localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) if err != nil { return nil, fmt.Errorf("init transaction sender from local account: %w", err) } - roleContract := rolemgmt.New(_actor) + roleContract := rolemgmt.New(localActor) // multi-tick context - var sentTxValidUntilBlock uint32 + txMonitor := newTransactionGroupMonitor(localActor) return func() { - if sentTxValidUntilBlock > 0 && sentTxValidUntilBlock <= monitor.currentHeight() { - prm.logger.Info("previously sent transaction designating Notary role to the local account may still be relevant, will wait for the outcome") + if txMonitor.isPending() { + prm.logger.Info("previously sent transaction designating Notary role to the local account is still pending, will wait for the outcome") return } - if sentTxValidUntilBlock > 0 { - prm.logger.Info("transaction designating Notary role to the local account was sent earlier, checking relevance...") - - if cur := monitor.currentHeight(); cur <= sentTxValidUntilBlock { - prm.logger.Info("previously sent transaction designating Notary role to the local account may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", sentTxValidUntilBlock)) - return - } - - prm.logger.Info("previously sent transaction designating Notary role to the local account expired without side-effect") - } - prm.logger.Info("sending new transaction designating Notary role to the local account...") var err error - _, vub, err := roleContract.DesignateAsRole(noderoles.P2PNotary, keys.PublicKeys{prm.localAcc.PublicKey()}) + txID, vub, err := roleContract.DesignateAsRole(noderoles.P2PNotary, keys.PublicKeys{prm.localAcc.PublicKey()}) if err != nil { - sentTxValidUntilBlock = 0 if isErrNotEnoughGAS(err) { prm.logger.Info("not enough GAS to designate Notary role to the local account, will try again later") } else { @@ -155,9 +155,10 @@ func initDesignateNotaryRoleToLocalAccountTick(prm enableNotaryPrm, monitor *blo return } - sentTxValidUntilBlock = vub + prm.logger.Info("transaction designating Notary role to the local account has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub)) - prm.logger.Info("transaction designating Notary role to the local account has been successfully sent, will wait for the outcome") + txMonitor.trackPendingTransactionsAsync(ctx, vub, txID) }, nil } @@ -165,7 +166,7 @@ func initDesignateNotaryRoleToLocalAccountTick(prm enableNotaryPrm, monitor *blo // of the Notary role designation to the multi-acc committee between calls. The // operation is performed by the leading committee member which is assigned to // collect signatures for the corresponding transaction. -func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchainMonitor) (func(), error) { +func initDesignateNotaryRoleAsLeaderTick(ctx context.Context, prm enableNotaryPrm) (func(), error) { committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(prm.committee)) committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) @@ -174,7 +175,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai return nil, fmt.Errorf("compose committee multi-signature account: %w", err) } - _actor, err := actor.NewSimple(prm.blockchain, prm.localAcc) + localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) if err != nil { return nil, fmt.Errorf("init transaction sender from local account: %w", err) } @@ -201,25 +202,26 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai return nil, fmt.Errorf("init transaction sender with committee signers: %w", err) } - _invoker := invoker.New(prm.blockchain, nil) + invkr := invoker.New(prm.blockchain, nil) roleContract := rolemgmt.New(committeeActor) // multi-tick context - var registerDomainTxValidUntilBlock uint32 - var setDomainRecordTxValidUntilBlock uint32 var tx *transaction.Transaction var mCommitteeIndexToSignature map[int][]byte - var designateRoleTxValidUntilBlock uint32 var txFullySigned bool + var triedDesignateRoleTx bool + registerDomainTxMonitor := newTransactionGroupMonitor(localActor) + setDomainRecordTxMonitor := newTransactionGroupMonitor(localActor) + designateRoleTxMonitor := newTransactionGroupMonitor(localActor) resetTx := func() { tx = nil - setDomainRecordTxValidUntilBlock = 0 for k := range mCommitteeIndexToSignature { delete(mCommitteeIndexToSignature, k) } - designateRoleTxValidUntilBlock = 0 txFullySigned = false + setDomainRecordTxMonitor.reset() + designateRoleTxMonitor.reset() } return func() { @@ -240,36 +242,36 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai return } - // _actor.CalculateValidUntilBlock is not used because it is rather "idealized" + // localActor.CalculateValidUntilBlock is not used because it is rather "idealized" // in terms of the accessibility of committee member nodes. So, we need a more // practically viable timeout to reduce the chance of transaction re-creation. const defaultValidUntilBlockIncrement = 120 // ~30m for 15s block interval var txValidUntilBlock uint32 if defaultValidUntilBlockIncrement <= ver.Protocol.MaxValidUntilBlockIncrement { - txValidUntilBlock = monitor.currentHeight() + defaultValidUntilBlockIncrement + txValidUntilBlock = prm.monitor.currentHeight() + defaultValidUntilBlockIncrement } else { - txValidUntilBlock = monitor.currentHeight() + ver.Protocol.MaxValidUntilBlockIncrement + txValidUntilBlock = prm.monitor.currentHeight() + ver.Protocol.MaxValidUntilBlockIncrement } strSharedTxData := sharedTransactionData{ - sender: _actor.Sender(), + sender: localActor.Sender(), validUntilBlock: txValidUntilBlock, nonce: randutil.Uint32(), }.encodeToString() l.Info("sending new transaction setting domain record in the NNS...") + var txID util.Uint256 var vub uint32 if recordExists { - _, vub, err = _actor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, + txID, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, domainDesignateNotaryTx, int64(nns.TXT), 0, strSharedTxData) } else { - _, vub, err = _actor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, + txID, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, domainDesignateNotaryTx, int64(nns.TXT), strSharedTxData) } if err != nil { - setDomainRecordTxValidUntilBlock = 0 if isErrNotEnoughGAS(err) { prm.logger.Info("not enough GAS to set NNS domain record, will try again later") } else { @@ -278,34 +280,27 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai return } - setDomainRecordTxValidUntilBlock = vub + l.Info("transaction setting NNS domain record has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub)) - l.Info("transaction setting NNS domain record has been successfully sent, will wait for the outcome") + setDomainRecordTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) } - strSharedTxData, err := lookupNNSDomainRecord(_invoker, prm.nnsOnChainAddress, domainDesignateNotaryTx) + strSharedTxData, err := lookupNNSDomainRecord(invkr, prm.nnsOnChainAddress, domainDesignateNotaryTx) if err != nil { if errors.Is(err, errMissingDomain) { l.Info("NNS domain is missing, registration is needed") - if registerDomainTxValidUntilBlock > 0 { - l.Info("transaction registering NNS domain was sent earlier, checking relevance...") - - if cur := monitor.currentHeight(); cur <= registerDomainTxValidUntilBlock { - l.Info("previously sent transaction registering NNS domain may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", registerDomainTxValidUntilBlock)) - return - } - - l.Info("previously sent transaction registering NNS domain expired without side-effect") + if registerDomainTxMonitor.isPending() { + prm.logger.Info("previously sent transaction registering NNS domain is still pending, will wait for the outcome") + return } l.Info("sending new transaction registering domain in the NNS...") - _, vub, err := _actor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, - domainDesignateNotaryTx, _actor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) + txID, vub, err := localActor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, + domainDesignateNotaryTx, localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) if err != nil { - registerDomainTxValidUntilBlock = 0 if isErrNotEnoughGAS(err) { prm.logger.Info("not enough GAS to register domain in the NNS, will try again later") } else { @@ -314,10 +309,10 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai return } - registerDomainTxValidUntilBlock = vub - l.Info("transaction registering domain in the NNS has been successfully sent, will wait for the outcome") + registerDomainTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + return } else if !errors.Is(err, errMissingDomainRecord) { l.Error("failed to lookup NNS domain record, will try again later", zap.Error(err)) @@ -326,16 +321,9 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai l.Info("missing record of the NNS domain, needed to be set") - if setDomainRecordTxValidUntilBlock > 0 { - l.Info("transaction setting NNS domain record was sent earlier, checking relevance...") - - if cur := monitor.currentHeight(); cur <= setDomainRecordTxValidUntilBlock { - l.Info("previously sent transaction setting NNS domain record may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", setDomainRecordTxValidUntilBlock)) - return - } - - l.Info("previously sent transaction setting NNS domain record expired without side-effect") + if setDomainRecordTxMonitor.isPending() { + prm.logger.Info("previously sent transaction setting NNS domain record is still pending, will wait for the outcome") + return } generateAndShareTxData(false) @@ -349,7 +337,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai return } - if cur := monitor.currentHeight(); cur > sharedTxData.validUntilBlock { + if cur := prm.monitor.currentHeight(); cur > sharedTxData.validUntilBlock { l.Error("previously used shared data of the transaction expired, need a reset", zap.Uint32("expires after height", sharedTxData.validUntilBlock), zap.Uint32("current height", cur)) generateAndShareTxData(true) @@ -373,7 +361,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai prm.logger.Info("transaction designating Notary role to the committee initialized, signing...") - netMagic := _actor.GetNetwork() + netMagic := localActor.GetNetwork() err = prm.localAcc.SignTx(netMagic, tx) if err != nil { @@ -408,7 +396,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai for i := range prm.committee[1:] { domain := designateNotarySignatureDomainForMember(i) - rec, err := lookupNNSDomainRecord(_invoker, prm.nnsOnChainAddress, domain) + rec, err := lookupNNSDomainRecord(invkr, prm.nnsOnChainAddress, domain) if err != nil { if errors.Is(err, errMissingDomain) || errors.Is(err, errMissingDomainRecord) { prm.logger.Info("missing NNS domain record with committee member's signature of the transaction designating Notary role to the committee, will wait", @@ -441,7 +429,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai } txCp := *tx // to safely call Hash method below - if !prm.committee[i].VerifyHashable(bSignature, uint32(_actor.GetNetwork()), &txCp) { + if !prm.committee[i].VerifyHashable(bSignature, uint32(localActor.GetNetwork()), &txCp) { prm.logger.Info("invalid signature of the transaction designating Notary role to the committee submitted by committee member", zap.Stringer("member", prm.committee[i]), zap.String("domain", domain)) @@ -478,15 +466,10 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai prm.logger.Info("gathered enough signatures of the transaction designating Notary role to the committee") - if designateRoleTxValidUntilBlock > 0 { - prm.logger.Info("transaction designating Notary role to the committee was sent earlier, checking relevance...") - - if cur := monitor.currentHeight(); cur <= designateRoleTxValidUntilBlock { - prm.logger.Info("previously sent transaction designating Notary role to the committee may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", designateRoleTxValidUntilBlock)) - return - } - + if registerDomainTxMonitor.isPending() { + prm.logger.Info("previously sent transaction designating Notary role to the committee is still pending, will wait for the outcome") + return + } else if triedDesignateRoleTx { prm.logger.Info("previously sent transaction designating Notary role to the committee expired without side-effect, will recreate") generateAndShareTxData(true) return @@ -518,9 +501,8 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai prm.logger.Info("sending the transaction designating Notary role to the committee...") - _, vub, err := _actor.Send(tx) + txID, vub, err := localActor.Send(tx) if err != nil { - designateRoleTxValidUntilBlock = 0 switch { default: prm.logger.Error("failed to send transaction designating Notary role to the committee, will try again later", @@ -535,9 +517,11 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai return } - designateRoleTxValidUntilBlock = vub + prm.logger.Info("transaction designating Notary role to the committee has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub)) - prm.logger.Info("transaction designating Notary role to the committee has been successfully sent, will wait for the outcome") + triedDesignateRoleTx = true + designateRoleTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) }, nil } @@ -545,7 +529,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai // of the Notary role designation to the multi-acc committee between calls. The // operation is performed by the non-leading committee member which is assigned to // sign transaction submitted by the leader. -func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchainMonitor) (func(), error) { +func initDesignateNotaryRoleAsSignerTick(ctx context.Context, prm enableNotaryPrm) (func(), error) { committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(prm.committee)) committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) @@ -554,7 +538,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai return nil, fmt.Errorf("compose committee multi-signature account: %w", err) } - _actor, err := actor.NewSimple(prm.blockchain, prm.localAcc) + localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) if err != nil { return nil, fmt.Errorf("init transaction sender from local account: %w", err) } @@ -581,17 +565,17 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai return nil, fmt.Errorf("init transaction sender with committee signers: %w", err) } - _invoker := invoker.New(prm.blockchain, nil) + invkr := invoker.New(prm.blockchain, nil) roleContract := rolemgmt.New(committeeActor) // multi-tick context var tx *transaction.Transaction - var registerDomainTxValidUntilBlock uint32 - var setDomainRecordTxValidUntilBlock uint32 + registerDomainTxMonitor := newTransactionGroupMonitor(localActor) + setDomainRecordTxMonitor := newTransactionGroupMonitor(localActor) resetTx := func() { tx = nil - setDomainRecordTxValidUntilBlock = 0 + setDomainRecordTxMonitor.reset() } return func() { @@ -599,7 +583,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai prm.logger.Info("synchronizing shared data of the transaction designating Notary role to the committee with NNS domain record...") - strSharedTxData, err := lookupNNSDomainRecord(_invoker, prm.nnsOnChainAddress, domainDesignateNotaryTx) + strSharedTxData, err := lookupNNSDomainRecord(invkr, prm.nnsOnChainAddress, domainDesignateNotaryTx) if err != nil { switch { default: @@ -621,7 +605,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai return } - if cur := monitor.currentHeight(); cur > sharedTxData.validUntilBlock { + if cur := prm.monitor.currentHeight(); cur > sharedTxData.validUntilBlock { l.Error("previously used shared data of the transaction expired, will wait for update by leader", zap.Uint32("expires after height", sharedTxData.validUntilBlock), zap.Uint32("current height", cur)) resetTx() @@ -655,29 +639,21 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai var recordExists bool var needReset bool - rec, err := lookupNNSDomainRecord(_invoker, prm.nnsOnChainAddress, domain) + rec, err := lookupNNSDomainRecord(invkr, prm.nnsOnChainAddress, domain) if err != nil { if errors.Is(err, errMissingDomain) { l.Info("NNS domain is missing, registration is needed") - if registerDomainTxValidUntilBlock > 0 { - l.Info("transaction registering NNS domain was sent earlier, checking relevance...") - - if cur := monitor.currentHeight(); cur <= registerDomainTxValidUntilBlock { - l.Info("previously sent transaction registering NNS domain may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", registerDomainTxValidUntilBlock)) - return - } - - l.Info("previously sent transaction registering NNS domain expired without side-effect") + if registerDomainTxMonitor.isPending() { + prm.logger.Info("previously sent transaction registering NNS domain is still pending, will wait for the outcome") + return } l.Info("sending new transaction registering domain in the NNS...") - _, vub, err := _actor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, - domain, _actor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) + txID, vub, err := localActor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, + domain, localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) if err != nil { - registerDomainTxValidUntilBlock = 0 if isErrNotEnoughGAS(err) { prm.logger.Info("not enough GAS to register domain in the NNS, will try again later") } else { @@ -686,9 +662,10 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai return } - registerDomainTxValidUntilBlock = vub + l.Info("transaction registering domain in the NNS has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub)) - l.Info("transaction registering domain in the NNS has been successfully sent, will wait for the outcome") + registerDomainTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) return } else if !errors.Is(err, errMissingDomainRecord) { @@ -698,16 +675,9 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai l.Info("missing record of the NNS domain, needed to be set") - if setDomainRecordTxValidUntilBlock > 0 { - l.Info("transaction setting NNS domain record was sent earlier, checking relevance...") - - if cur := monitor.currentHeight(); cur <= setDomainRecordTxValidUntilBlock { - l.Info("previously sent transaction setting NNS domain record may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", setDomainRecordTxValidUntilBlock)) - return - } - - l.Info("previously sent transaction setting NNS domain record expired without side-effect") + if setDomainRecordTxMonitor.isPending() { + prm.logger.Info("previously sent transaction setting NNS domain record is still pending, will wait for the outcome") + return } needReset = true @@ -725,7 +695,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai needReset = true } else { txCp := *tx // to safely call Hash method below - if !prm.localAcc.PublicKey().VerifyHashable(bSignature, uint32(_actor.GetNetwork()), &txCp) { + if !prm.localAcc.PublicKey().VerifyHashable(bSignature, uint32(localActor.GetNetwork()), &txCp) { l.Info("invalid signature of the transaction designating Notary role to the committee submitted by local account, need to be recalculated") needReset = true } @@ -737,23 +707,23 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai if needReset { prm.logger.Info("calculating signature of the transaction designating Notary role to the committee using local account...") - sig := prm.localAcc.SignHashable(_actor.GetNetwork(), tx) + sig := prm.localAcc.SignHashable(localActor.GetNetwork(), tx) sig = sharedTxData.unshiftChecksum(sig) rec = base64.StdEncoding.EncodeToString(sig) l.Info("sending new transaction setting domain record in the NNS...") + var txID util.Uint256 var vub uint32 if recordExists { - _, vub, err = _actor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, + txID, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, domain, int64(nns.TXT), 0, rec) } else { - _, vub, err = _actor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, + txID, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, domain, int64(nns.TXT), rec) } if err != nil { - setDomainRecordTxValidUntilBlock = 0 if isErrNotEnoughGAS(err) { prm.logger.Info("not enough GAS to set NNS domain record, will try again later") } else { @@ -762,10 +732,10 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai return } - setDomainRecordTxValidUntilBlock = vub - l.Info("transaction setting NNS domain record has been successfully sent, will wait for the outcome") + setDomainRecordTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + return } }, nil @@ -871,3 +841,285 @@ 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. +func newCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee keys.PublicKeys) (*notary.Actor, error) { + committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(committee)) + committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(localAcc.PrivateKey()) + + err := committeeMultiSigAcc.ConvertMultisig(committeeMultiSigM, committee) + if err != nil { + 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, + }, + { + Signer: transaction.Signer{ + Account: committeeMultiSigAcc.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: committeeMultiSigAcc, + }, + }, localAcc) +} + +// Amount of GAS for the single local account's GAS->Notary transfer. Relatively +// small value for fallback transactions' fees. +var singleNotaryDepositAmount = big.NewInt(1_0000_0000) // 1 GAS + +func autoReplenishNotaryBalance(ctx context.Context, l *zap.Logger, b Blockchain, localAcc *wallet.Account, chTrigger <-chan struct{}) { + l.Info("tracking Notary balance for auto-replenishment...") + + var err error + var localActor *actor.Actor + var notaryContract *notary.Contract + var gasContract *nep17.Token + var txMonitor *transactionGroupMonitor + localAccID := localAcc.ScriptHash() + + for { + select { + case <-ctx.Done(): + l.Info("Notary balance tracker stopped by context", zap.Error(ctx.Err())) + return + case _, ok := <-chTrigger: + if !ok { + l.Info("Notary balance tracker stopped by closed block channel") + return + } + } + + if localActor == nil { + localActor, err = actor.NewSimple(b, localAcc) + if err != nil { + l.Error("failed to init transaction sender from local account, will try again later", zap.Error(err)) + continue + } + + notaryContract = notary.New(localActor) + gasContract = gas.New(localActor) + txMonitor = newTransactionGroupMonitor(localActor) + } + + notaryBalance, err := notaryContract.BalanceOf(localAccID) + if err != nil { + l.Error("failed to read Notary balance of the local account, will try again later", zap.Error(err)) + continue + } + + // deposit when balance falls below 1/5 of the single deposit amount + const refillProportion = 5 + + if new(big.Int).Mul(notaryBalance, big.NewInt(refillProportion)).Cmp(singleNotaryDepositAmount) >= 0 { + l.Info("enough funds on the notary balance, deposit is not needed", zap.Stringer("balance", notaryBalance)) + continue + } + + // simple deposit scheme: transfer 1GAS (at most 2% of GAS token balance) for + // 100 blocks after the latest deposit's expiration height (if first, then from + // current height). + // + // If we encounter deposit expiration and current Notary balance >=20% of single + // transfer, we just increase the expiration time of the deposit, otherwise, we + // make transfer. + if txMonitor.isPending() { + l.Info("previously sent transaction transferring local account's GAS to the Notary contract is still pending, will wait for the outcome") + continue + } + + var transferData notary.OnNEP17PaymentData + transferData.Account = &localAccID + transferData.Till = math.MaxUint32 // deposit "forever" so we don't have to renew + + l.Info("sending new transaction transferring local account's GAS to the Notary contract...", + zap.Stringer("amount", singleNotaryDepositAmount), zap.Uint32("till", transferData.Till)) + + // nep17.TokenWriter.Transfer doesn't support notary.OnNEP17PaymentData + // directly, so split the args + // Track https://github.com/nspcc-dev/neofs-node/issues/2429 + txID, vub, err := gasContract.Transfer(localAccID, notary.Hash, singleNotaryDepositAmount, []interface{}{transferData.Account, transferData.Till}) + if err != nil { + l.Error("failed to send transaction transferring local account's GAS to the Notary contract, will try again later", zap.Error(err)) + continue + } + + l.Info("transaction transferring local account's GAS to the Notary contract has been successfully sent, will wait for the outcome") + + txMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + } +} + +// listenCommitteeNotaryRequestsPrm groups parameters of listenCommitteeNotaryRequests. +type listenCommitteeNotaryRequestsPrm struct { + logger *zap.Logger + + blockchain Blockchain + + localAcc *wallet.Account + + committee keys.PublicKeys +} + +// listenCommitteeNotaryRequests starts background process listening to incoming +// Notary service requests. The process filters transactions witnessed by the +// committee and signs them on behalf of the local account (representing +// committee member). Routine handles only requests sent by the remote accounts. +// The process is stopped by context or internal Blockchain signal. +func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotaryRequestsPrm) 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() + chNotaryRequests := make(chan *result.NotaryRequestEvent, 100) // secure from blocking + // cache processed operations: when main transaction from received notary + // request is signed and sent by the local account, we receive the request from + // the channel again + mProcessedMainTxs := make(map[util.Uint256]struct{}) + + subID, err := prm.blockchain.ReceiveNotaryRequests(&neorpc.TxFilter{ + Signer: &committeeMultiSigAccID, + }, chNotaryRequests) + if err != nil { + return fmt.Errorf("subscribe to notary requests from committee: %w", err) + } + + go func() { + defer func() { + err := prm.blockchain.Unsubscribe(subID) + if err != nil { + prm.logger.Warn("failed to cancel subscription to notary requests", zap.Error(err)) + } + }() + + prm.logger.Info("listening to committee notary requests...") + + for { + select { + case <-ctx.Done(): + prm.logger.Info("stop listening to committee notary requests (context is done)", zap.Error(ctx.Err())) + return + case notaryEvent, ok := <-chNotaryRequests: + if !ok { + prm.logger.Info("stop listening to committee notary requests (subscription channel closed)") + return + } + + // 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() + _, processed := mProcessedMainTxs[srcMainTxHash] + + // 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", + 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))) + 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") + 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)) + continue + } + + senderKey, err := keys.NewPublicKeyFromBytes(bSenderKey, elliptic.P256()) + if err != nil { + prm.logger.Info("failed to decode sender's public key from first script of main transaction from the received notary request, skip", zap.Error(err)) + continue + } + + // 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...") + + // 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) + 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)) + 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)) + continue + } + + prm.logger.Info("sending new notary request with the main transaction signed by the local account...") + + _, _, _, err = notaryActor.Notarize(mainTx, nil) + if err != nil { + if isErrNotEnoughGAS(err) { + prm.logger.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)) + } + continue + } + + prm.logger.Info("main transaction from the received notary request has been successfully signed and sent by the local account") + } + } + }() + + return nil +} diff --git a/pkg/morph/deploy/util.go b/pkg/morph/deploy/util.go index e2e7d18eda..30d37d4e86 100644 --- a/pkg/morph/deploy/util.go +++ b/pkg/morph/deploy/util.go @@ -2,18 +2,29 @@ package deploy import ( "context" + "encoding/json" "errors" "fmt" "strings" "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/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/atomic" "go.uber.org/zap" ) @@ -26,33 +37,37 @@ func isErrContractNotFound(err error) bool { } func isErrNotEnoughGAS(err error) bool { - return errors.Is(err, neorpc.ErrValidationFailed) && strings.Contains(err.Error(), "insufficient funds") + return isErrInvalidTransaction(err) && strings.Contains(err.Error(), "insufficient funds") } func isErrInvalidTransaction(err error) bool { return errors.Is(err, neorpc.ErrValidationFailed) } -func setGroupInManifest(_manifest *manifest.Manifest, _nef nef.File, groupPrivKey *keys.PrivateKey, deployerAcc util.Uint160) { - contractAddress := state.CreateContractHash(deployerAcc, _nef.Checksum, _manifest.Name) +func isErrContractAlreadyUpdated(err error) bool { + return strings.Contains(err.Error(), common.ErrAlreadyUpdated) +} + +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()) groupPubKey := groupPrivKey.PublicKey() ind := -1 - for i := range _manifest.Groups { - if _manifest.Groups[i].PublicKey.Equal(groupPubKey) { + for i := range manif.Groups { + if manif.Groups[i].PublicKey.Equal(groupPubKey) { ind = i break } } if ind >= 0 { - _manifest.Groups[ind].Signature = sig + manif.Groups[ind].Signature = sig return } - _manifest.Groups = append(_manifest.Groups, manifest.Group{ + manif.Groups = append(manif.Groups, manifest.Group{ PublicKey: groupPubKey, Signature: sig, }) @@ -73,7 +88,7 @@ type blockchainMonitor struct { // newBlockchainMonitor constructs and runs monitor for the given Blockchain. // Resulting blockchainMonitor must be stopped when no longer needed. -func newBlockchainMonitor(l *zap.Logger, b Blockchain) (*blockchainMonitor, error) { +func newBlockchainMonitor(l *zap.Logger, b Blockchain, chNewBlock chan<- struct{}) (*blockchainMonitor, error) { ver, err := b.GetVersion() if err != nil { return nil, fmt.Errorf("request Neo protocol configuration: %w", err) @@ -105,12 +120,18 @@ func newBlockchainMonitor(l *zap.Logger, b Blockchain) (*blockchainMonitor, erro for { b, ok := <-blockCh if !ok { + close(chNewBlock) l.Info("listening to new blocks stopped") return } res.height.Store(b.Index) + select { + case chNewBlock <- struct{}{}: + default: + } + l.Info("new block arrived", zap.Uint32("height", b.Index)) } }() @@ -166,3 +187,141 @@ 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) +} + +type transactionGroupMonitor struct { + waiter transactionGroupWaiter + pending atomic.Bool +} + +func newTransactionGroupMonitor(w transactionGroupWaiter) *transactionGroupMonitor { + return &transactionGroupMonitor{ + waiter: w, + } +} + +func (x *transactionGroupMonitor) reset() { + x.pending.Store(false) +} + +func (x *transactionGroupMonitor) isPending() bool { + return x.pending.Load() +} + +func (x *transactionGroupMonitor) trackPendingTransactionsAsync(ctx context.Context, vub uint32, txs ...util.Uint256) { + if len(txs) == 0 { + panic("missing transactions") + } + + x.pending.Store(true) + + waitCtx, cancel := context.WithCancel(ctx) + + go func() { + _, _ = x.waiter.WaitAny(waitCtx, vub, txs...) + x.reset() + cancel() + }() +} diff --git a/pkg/morph/deploy/util_test.go b/pkg/morph/deploy/util_test.go new file mode 100644 index 0000000000..2d2816e5cd --- /dev/null +++ b/pkg/morph/deploy/util_test.go @@ -0,0 +1,154 @@ +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()) +}