From 52c6ef006e1899a12d3025afe1f9abcd16dabcab Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 27 Jun 2023 21:20:38 +0400 Subject: [PATCH 1/4] sidechain/deploy: Use single blockchain monitoring utility Previously, Sidechain deployment procedure initialized and stopped multiple times. It's more efficient to run monitor once (it's almost always needed) and stop at the end of the procedure. This also prevents duplicated log messages about new block arrival. Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/deploy.go | 10 ++++++++ pkg/morph/deploy/group.go | 21 ++++++++--------- pkg/morph/deploy/nns.go | 13 ++++------- pkg/morph/deploy/notary.go | 48 ++++++++++++++++++-------------------- 4 files changed, 48 insertions(+), 44 deletions(-) diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index 20a55a35a9..bb22e93ec6 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -121,9 +121,17 @@ func Deploy(ctx context.Context, prm Prm) error { return errors.New("local account does not belong to any Neo committee member") } + monitor, err := newBlockchainMonitor(prm.Logger, prm.Blockchain) + 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 +177,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, @@ -186,6 +195,7 @@ func Deploy(ctx context.Context, prm Prm) error { committeeGroupKey, err := initCommitteeGroup(ctx, initCommitteeGroupPrm{ logger: prm.Logger, blockchain: prm.Blockchain, + monitor: monitor, nnsOnChainAddress: nnsOnChainAddress, systemEmail: prm.NNS.SystemEmail, committee: committee, diff --git a/pkg/morph/deploy/group.go b/pkg/morph/deploy/group.go index f26e57b8ec..69a6ca7412 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,19 +39,13 @@ 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() - inv := invoker.New(prm.blockchain, nil) const leaderCommitteeIndex = 0 var committeeGroupKey *keys.PrivateKey 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 +97,8 @@ upperLoop: continue } + var err error + if committeeGroupKey == nil { committeeGroupKey, err = prm.keyStorage.GetPersistedPrivateKey() if err != nil { @@ -109,7 +108,7 @@ upperLoop: } if leaderTick == nil { - leaderTick, err = initShareCommitteeGroupKeyAsLeaderTick(prm, monitor, committeeGroupKey) + leaderTick, err = initShareCommitteeGroupKeyAsLeaderTick(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,7 +123,7 @@ 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) { +func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, committeeGroupKey *keys.PrivateKey) (func(), error) { _actor, err := actor.NewSimple(prm.blockchain, prm.localAcc) if err != nil { return nil, fmt.Errorf("init transaction sender from local account: %w", err) @@ -153,7 +152,7 @@ func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, monitor * if ok && vubs[0] > 0 { l.Info("transaction registering NNS domain was sent earlier, checking relevance...") - if cur := monitor.currentHeight(); cur <= vubs[0] { + if cur := prm.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 @@ -194,7 +193,7 @@ func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, monitor * if ok && vubs[1] > 0 { l.Info("transaction setting NNS domain record was sent earlier, checking relevance...") - if cur := monitor.currentHeight(); cur <= vubs[1] { + if cur := prm.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 diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index 109a0242ce..1b1ebf2cda 100644 --- a/pkg/morph/deploy/nns.go +++ b/pkg/morph/deploy/nns.go @@ -66,6 +66,9 @@ type deployNNSContractPrm struct { blockchain Blockchain + // based on blockchain + monitor *blockchainMonitor + localAcc *wallet.Account localNEF nef.File @@ -91,17 +94,11 @@ 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) - if err != nil { - return res, fmt.Errorf("init blockchain monitor: %w", err) - } - defer monitor.stop() - var managementContract *management.Contract var sentTxValidUntilBlock uint32 var committeeGroupKey *keys.PrivateKey - 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()) @@ -149,7 +146,7 @@ func initNNSContract(ctx context.Context, prm deployNNSContractPrm) (res util.Ui if sentTxValidUntilBlock > 0 { prm.logger.Info("transaction deploying NNS contract was sent earlier, checking relevance...") - if cur := monitor.currentHeight(); cur <= sentTxValidUntilBlock { + if cur := prm.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 diff --git a/pkg/morph/deploy/notary.go b/pkg/morph/deploy/notary.go index 799a705061..470a79e4c6 100644 --- a/pkg/morph/deploy/notary.go +++ b/pkg/morph/deploy/notary.go @@ -31,6 +31,9 @@ type enableNotaryPrm struct { blockchain Blockchain + // based on blockchain + monitor *blockchainMonitor + nnsOnChainAddress util.Uint160 systemEmail string @@ -41,18 +44,13 @@ 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() - 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(prm) if err != nil { return fmt.Errorf("construct action designating Notary role to the local account: %w", err) } @@ -60,12 +58,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(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(prm) if err != nil { return fmt.Errorf("construct action designating Notary role to the multi-acc committee as signer: %w", err) } @@ -74,7 +72,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 +81,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,7 +109,7 @@ 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) { +func initDesignateNotaryRoleToLocalAccountTick(prm enableNotaryPrm) (func(), error) { _actor, err := actor.NewSimple(prm.blockchain, prm.localAcc) if err != nil { return nil, fmt.Errorf("init transaction sender from local account: %w", err) @@ -123,7 +121,7 @@ func initDesignateNotaryRoleToLocalAccountTick(prm enableNotaryPrm, monitor *blo var sentTxValidUntilBlock uint32 return func() { - if sentTxValidUntilBlock > 0 && sentTxValidUntilBlock <= monitor.currentHeight() { + if sentTxValidUntilBlock > 0 && sentTxValidUntilBlock <= prm.monitor.currentHeight() { prm.logger.Info("previously sent transaction designating Notary role to the local account may still be relevant, will wait for the outcome") return } @@ -131,7 +129,7 @@ func initDesignateNotaryRoleToLocalAccountTick(prm enableNotaryPrm, monitor *blo 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 { + if cur := prm.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 @@ -165,7 +163,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(prm enableNotaryPrm) (func(), error) { committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(prm.committee)) committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) @@ -247,9 +245,9 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai 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{ @@ -291,7 +289,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai if registerDomainTxValidUntilBlock > 0 { l.Info("transaction registering NNS domain was sent earlier, checking relevance...") - if cur := monitor.currentHeight(); cur <= registerDomainTxValidUntilBlock { + if cur := prm.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 @@ -329,7 +327,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai if setDomainRecordTxValidUntilBlock > 0 { l.Info("transaction setting NNS domain record was sent earlier, checking relevance...") - if cur := monitor.currentHeight(); cur <= setDomainRecordTxValidUntilBlock { + if cur := prm.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 @@ -349,7 +347,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) @@ -481,7 +479,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm, monitor *blockchai if designateRoleTxValidUntilBlock > 0 { prm.logger.Info("transaction designating Notary role to the committee was sent earlier, checking relevance...") - if cur := monitor.currentHeight(); cur <= designateRoleTxValidUntilBlock { + if cur := prm.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 @@ -545,7 +543,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(prm enableNotaryPrm) (func(), error) { committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(prm.committee)) committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) @@ -621,7 +619,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() @@ -663,7 +661,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai if registerDomainTxValidUntilBlock > 0 { l.Info("transaction registering NNS domain was sent earlier, checking relevance...") - if cur := monitor.currentHeight(); cur <= registerDomainTxValidUntilBlock { + if cur := prm.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 @@ -701,7 +699,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm, monitor *blockchai if setDomainRecordTxValidUntilBlock > 0 { l.Info("transaction setting NNS domain record was sent earlier, checking relevance...") - if cur := monitor.currentHeight(); cur <= setDomainRecordTxValidUntilBlock { + if cur := prm.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 From 7384e8cd4d2fc729998a0772f137f1e6bb3ccb0d Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 27 Jun 2023 22:42:44 +0400 Subject: [PATCH 2/4] sidechain/deploy: Auto-update on-chain NeoFS NNS smart contract There is a need to automatically update on-chain NNS contract to new code embedded into application importing `contracts` package. Sidechain deployment procedure fits well for this purpose. It already initializes Notary service for the committee members, and the update procedure (`update` method of each system contract) just requires a committee witness. Add new stage (currently last one) to Sidechain deployment procedure that updates on-chain NNS contract. Since 'update' methods require committee witness, also run background routine which signs incoming Notary requests on behalf of the local account. The routine will be useful for deployment/update of other NeoFS contracts (to be implemented in the future). Signed-off-by: Leonard Lyubich --- go.mod | 2 + go.sum | 3 + pkg/morph/deploy/contracts.go | 7 + pkg/morph/deploy/deploy.go | 61 ++++- pkg/morph/deploy/group.go | 12 +- pkg/morph/deploy/nns.go | 176 ++++++++++++++- pkg/morph/deploy/notary.go | 404 +++++++++++++++++++++++++++++++--- pkg/morph/deploy/util.go | 132 ++++++++++- pkg/morph/deploy/util_test.go | 154 +++++++++++++ 9 files changed, 904 insertions(+), 47 deletions(-) create mode 100644 pkg/morph/deploy/contracts.go create mode 100644 pkg/morph/deploy/util_test.go diff --git a/go.mod b/go.mod index 3f26cad57c..99dd1211a1 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 f1b4b142c4..1b52dc55fd 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 bb22e93ec6..fe3b7c257a 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,7 +101,7 @@ 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. @@ -190,6 +198,22 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("Notary service successfully enabled for the committee") + onNotaryDepositDeficiency, err := initNotaryDepositDeficiencyHandler(prm.Logger, prm.Blockchain, monitor, prm.LocalAccount) + if err != nil { + return fmt.Errorf("construct action depositing funds to the local account's Notary balance: %w", err) + } + + err = listenCommitteeNotaryRequests(ctx, listenCommitteeNotaryRequestsPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + localAcc: prm.LocalAccount, + committee: committee, + onNotaryDepositDeficiency: onNotaryDepositDeficiency, + }) + 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{ @@ -209,7 +233,30 @@ 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, + onNotaryDepositDeficiency: onNotaryDepositDeficiency, + }) + 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 69a6ca7412..d94e51676c 100644 --- a/pkg/morph/deploy/group.go +++ b/pkg/morph/deploy/group.go @@ -124,12 +124,12 @@ upperLoop: // context of the committee group key distribution by leading committee member // between calls. func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, committeeGroupKey *keys.PrivateKey) (func(), error) { - _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) } - _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 @@ -143,7 +143,7 @@ func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, committee 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") @@ -163,8 +163,8 @@ func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, committee 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) + _, 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 @@ -213,7 +213,7 @@ func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, committee l.Info("sending new transaction setting domain record in the NNS...") - _, vub, err := _actor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, + _, vub, err := localActor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, domain, int64(nns.TXT), keyCipher) if err != nil { vubs[1] = 0 diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index 1b1ebf2cda..ec0972c906 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" @@ -158,13 +159,13 @@ func initNNSContract(ctx context.Context, prm deployNNSContractPrm) (res util.Ui prm.logger.Info("sending new transaction deploying NNS contract...") if managementContract == nil { - _actor, err := actor.NewSimple(prm.blockchain, prm.localAcc) + localActor, 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) + managementContract = management.New(localActor) setGroupInManifest(&prm.localManifest, prm.localNEF, committeeGroupKey, prm.localAcc.ScriptHash()) } @@ -226,3 +227,174 @@ 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) + + onNotaryDepositDeficiency notaryDepositDeficiencyHandler +} + +// 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) + } + + var updateTxValidUntilBlock uint32 + + 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()) + + var vub uint32 + + // 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 + } + } else { + if updateTxValidUntilBlock > 0 { + prm.logger.Info("transaction updating NNS contract was sent earlier, checking relevance...") + + if cur := prm.monitor.currentHeight(); cur <= updateTxValidUntilBlock { + prm.logger.Info("previously sent transaction updating NNS contract may still be relevant, will wait for the outcome", + zap.Uint32("current height", cur), zap.Uint32("retry after height", updateTxValidUntilBlock)) + continue + } + + prm.logger.Info("previously sent transaction updating NNS contract expired without side-effect") + } + + prm.logger.Info("sending new transaction updating NNS contract...") + + _, _, vub, err = committeeActor.Notarize(tx, nil) + } + if err != nil { + lackOfGAS := isErrNotEnoughGAS(err) + // here lackOfGAS=true always means lack of Notary balance and not related to + // the main transaction itself + if !lackOfGAS { + if !isErrNotaryDepositExpires(err) { + prm.logger.Error("failed to send transaction deploying NNS contract, will try again later", zap.Error(err)) + continue + } + } + + // same approach with in-place deposit is going to be used in other functions. + // Consider replacement with background process (e.g. blockchainMonitor + // internal) which periodically checks Notary balance and updates it when, for + // example, balance goes lower than 20% of desired amount or expires soon. With + // this approach functions like current will not try to make a deposit, but + // simply wait until it becomes enough. + prm.onNotaryDepositDeficiency(lackOfGAS) + + continue + } + + updateTxValidUntilBlock = vub + + prm.logger.Info("transaction updating NNS contract has been successfully sent, will wait for the outcome") + } +} diff --git a/pkg/morph/deploy/notary.go b/pkg/morph/deploy/notary.go index 470a79e4c6..8e28dafb6c 100644 --- a/pkg/morph/deploy/notary.go +++ b/pkg/morph/deploy/notary.go @@ -3,21 +3,30 @@ 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/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" @@ -110,12 +119,12 @@ 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) (func(), error) { - _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) } - roleContract := rolemgmt.New(_actor) + roleContract := rolemgmt.New(localActor) // multi-tick context var sentTxValidUntilBlock uint32 @@ -172,7 +181,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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) } @@ -199,7 +208,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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 @@ -238,7 +247,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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 @@ -251,7 +260,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { } strSharedTxData := sharedTransactionData{ - sender: _actor.Sender(), + sender: localActor.Sender(), validUntilBlock: txValidUntilBlock, nonce: randutil.Uint32(), }.encodeToString() @@ -260,10 +269,10 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { var vub uint32 if recordExists { - _, vub, err = _actor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, + _, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, domainDesignateNotaryTx, int64(nns.TXT), 0, strSharedTxData) } else { - _, vub, err = _actor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, + _, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, domainDesignateNotaryTx, int64(nns.TXT), strSharedTxData) } if err != nil { @@ -281,7 +290,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { l.Info("transaction setting NNS domain record has been successfully sent, will wait for the outcome") } - 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") @@ -300,8 +309,8 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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) + _, vub, err := localActor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, + domainDesignateNotaryTx, localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) if err != nil { registerDomainTxValidUntilBlock = 0 if isErrNotEnoughGAS(err) { @@ -371,7 +380,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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 { @@ -406,7 +415,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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", @@ -439,7 +448,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { } 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)) @@ -516,7 +525,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { prm.logger.Info("sending the transaction designating Notary role to the committee...") - _, vub, err := _actor.Send(tx) + _, vub, err := localActor.Send(tx) if err != nil { designateRoleTxValidUntilBlock = 0 switch { @@ -552,7 +561,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { 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) } @@ -579,7 +588,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { 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 @@ -597,7 +606,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { 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: @@ -653,7 +662,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { 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") @@ -672,8 +681,8 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { 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) + _, vub, err := localActor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, + domain, localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) if err != nil { registerDomainTxValidUntilBlock = 0 if isErrNotEnoughGAS(err) { @@ -723,7 +732,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { 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 } @@ -735,7 +744,7 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { 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) @@ -744,10 +753,10 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { var vub uint32 if recordExists { - _, vub, err = _actor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, + _, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, domain, int64(nns.TXT), 0, rec) } else { - _, vub, err = _actor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, + _, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, domain, int64(nns.TXT), rec) } if err != nil { @@ -869,3 +878,348 @@ 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) +} + +// notaryDepositDeficiencyHandler is a function returned by initNotaryDepositDeficiencyHandler. +// True argument is passed when there is not enough GAS on local account's +// balance in the Notary contract, false - when local account's Notary deposit +// expires before particular fallback transaction. +// +// The function is intended to be called multiple times on each deposit problem +// encounter. On each call, It attempts to fix Notary deposit problem without +// waiting for success. Caller should by default wait for the problem to be +// fixed, and if not, retry. +// +// notaryDepositDeficiencyHandler must not be called from multiple routines in +// parallel. +type notaryDepositDeficiencyHandler = func(lackOfGAS bool) + +// 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 + +// constructs notaryDepositDeficiencyHandler working with the specified +// Blockchain and GAS/Notary balance of the given account. +func initNotaryDepositDeficiencyHandler(l *zap.Logger, b Blockchain, monitor *blockchainMonitor, localAcc *wallet.Account) (notaryDepositDeficiencyHandler, error) { + localActor, err := actor.NewSimple(b, localAcc) + if err != nil { + return nil, fmt.Errorf("init transaction sender from local account: %w", err) + } + + notaryContract := notary.New(localActor) + gasContract := gas.New(localActor) + localAccID := localAcc.ScriptHash() + + // multi-tick context + var transferTxValidUntilBlock uint32 + var expirationTxValidUntilBlock uint32 + + return func(lackOfGAS bool) { + 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)) + return + } + + gasBalance, err := gasContract.BalanceOf(localAccID) + if err != nil { + l.Error("failed to read GAS token balance of the local account, will try again later", zap.Error(err)) + return + } + + // 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. + + const ( + // GAS:Notary proportion, see scheme above + gasProportion = 50 + // even there is no lack of GAS at the moment, when the balance falls below 1/5 + // of the supported value - replenish + refillProportion = 5 + // for simplicity, we just make Notary deposit "infinite" not to prolong + till = math.MaxUint32 + ) + + if !lackOfGAS { // deposit expired + if new(big.Int).Mul(notaryBalance, big.NewInt(refillProportion)).Cmp(singleNotaryDepositAmount) >= 0 { + if expirationTxValidUntilBlock > 0 { + l.Info("transaction increasing expiration time of the Notary deposit was sent earlier, checking relevance...") + + if cur := monitor.currentHeight(); cur <= expirationTxValidUntilBlock { + l.Info("previously sent transaction increasing expiration time of the Notary deposit may still be relevant, will wait for the outcome", + zap.Uint32("current height", cur), zap.Uint32("retry after height", expirationTxValidUntilBlock)) + return + } + + l.Info("previously sent transaction increasing expiration time of the Notary deposit expired without side-effect ") + } + + l.Info("sending new transaction increasing expiration time of the Notary deposit...", zap.Uint32("till", till)) + + _, vub, err := notaryContract.LockDepositUntil(localAccID, till) + if err != nil { + l.Error("failed to send transaction increasing expiration time of the Notary deposit, will try again later", zap.Error(err)) + return + } + + expirationTxValidUntilBlock = vub + + l.Info("transaction increasing expiration time of the Notary deposit has been successfully sent, will wait for the outcome") + + return + } + } + + if transferTxValidUntilBlock > 0 { + l.Info("transaction transferring local account's GAS to the Notary contract was sent earlier, checking relevance...") + + // for simplicity, we track ValidUntilBlock. In this particular case, it'd be + // more efficient to monitor a transaction by ID, because side effect is + // inconsistent (funds can be spent in background). + + if cur := monitor.currentHeight(); cur <= transferTxValidUntilBlock { + l.Info("previously sent transaction transferring local account's GAS to the Notary contract may still be relevant, will wait for the outcome", + zap.Uint32("current height", cur), zap.Uint32("retry after height", transferTxValidUntilBlock)) + return + } + + l.Info("previously sent transaction transferring local account's GAS to the Notary contract expired without side-effect") + } + + needAtLeast := new(big.Int).Mul(singleNotaryDepositAmount, big.NewInt(gasProportion)) + if gasBalance.Cmp(needAtLeast) < 0 { + l.Info("minimum threshold for GAS transfer from local account to the Notary contract not reached, will wait for replenishment", + zap.Stringer("need at least", needAtLeast), zap.Stringer("have", gasBalance)) + return + } + + var transferData notary.OnNEP17PaymentData + transferData.Account = &localAccID + transferData.Till = till + + 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 + _, 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)) + return + } + + transferTxValidUntilBlock = vub + + l.Info("transaction transferring local account's GAS to the Notary contract has been successfully sent, will wait for the outcome") + }, nil +} + +// listenCommitteeNotaryRequestsPrm groups parameters of listenCommitteeNotaryRequests. +type listenCommitteeNotaryRequestsPrm struct { + logger *zap.Logger + + blockchain Blockchain + + localAcc *wallet.Account + + committee keys.PublicKeys + + onNotaryDepositDeficiency notaryDepositDeficiencyHandler +} + +// 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 { + lackOfGAS := isErrNotEnoughGAS(err) + // here lackOfGAS=true always means lack of Notary balance and not related to + // the main transaction itself + if !lackOfGAS { + if !isErrNotaryDepositExpires(err) { + prm.logger.Error("failed to send transaction deploying NNS contract, will try again later", zap.Error(err)) + continue + } + } + + prm.onNotaryDepositDeficiency(lackOfGAS) + + 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..fc164a4458 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,41 @@ 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 isErrNotaryDepositExpires(err error) bool { + return strings.Contains(err.Error(), "fallback transaction is valid after deposit is unlocked") +} + +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, }) @@ -166,3 +185,102 @@ 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) +} 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()) +} From 1c67f9ac412e4fe1ccfd42560abaaad877201ad2 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Sun, 9 Jul 2023 14:14:28 +0400 Subject: [PATCH 3/4] sidechain/deploy: Use Waiter to track pending transactions Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/deploy.go | 7 +- pkg/morph/deploy/group.go | 76 +++++++------ pkg/morph/deploy/nns.go | 86 +++++++-------- pkg/morph/deploy/notary.go | 219 ++++++++++++++----------------------- pkg/morph/deploy/util.go | 39 +++++++ 5 files changed, 203 insertions(+), 224 deletions(-) diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index fe3b7c257a..ab17b9dcff 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -106,6 +106,11 @@ type Prm struct { // // 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) @@ -198,7 +203,7 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("Notary service successfully enabled for the committee") - onNotaryDepositDeficiency, err := initNotaryDepositDeficiencyHandler(prm.Logger, prm.Blockchain, monitor, prm.LocalAccount) + onNotaryDepositDeficiency, err := initNotaryDepositDeficiencyHandler(ctx, prm.Logger, prm.Blockchain, prm.LocalAccount) if err != nil { return fmt.Errorf("construct action depositing funds to the local account's Notary balance: %w", err) } diff --git a/pkg/morph/deploy/group.go b/pkg/morph/deploy/group.go index d94e51676c..ed1739628b 100644 --- a/pkg/morph/deploy/group.go +++ b/pkg/morph/deploy/group.go @@ -39,6 +39,11 @@ type initCommitteeGroupPrm struct { // initCommitteeGroup initializes committee group and returns corresponding private key. func initCommitteeGroup(ctx context.Context, prm initCommitteeGroupPrm) (*keys.PrivateKey, 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() + inv := invoker.New(prm.blockchain, nil) const leaderCommitteeIndex = 0 var committeeGroupKey *keys.PrivateKey @@ -108,7 +113,7 @@ upperLoop: } if leaderTick == nil { - leaderTick, err = initShareCommitteeGroupKeyAsLeaderTick(prm, 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)) @@ -123,7 +128,7 @@ upperLoop: // initShareCommitteeGroupKeyAsLeaderTick returns a function that preserves // context of the committee group key distribution by leading committee member // between calls. -func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, committeeGroupKey *keys.PrivateKey) (func(), error) { +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) @@ -132,7 +137,8 @@ func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, committee 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...") @@ -148,58 +154,52 @@ func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, committee 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 := prm.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 := localActor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, + 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 := prm.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...") @@ -208,28 +208,26 @@ func initShareCommitteeGroupKeyAsLeaderTick(prm initCommitteeGroupPrm, committee 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 := localActor.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 ec0972c906..d5825c41eb 100644 --- a/pkg/morph/deploy/nns.go +++ b/pkg/morph/deploy/nns.go @@ -95,9 +95,19 @@ 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) { - var managementContract *management.Contract - var sentTxValidUntilBlock uint32 + localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) + if err != nil { + return res, fmt.Errorf("init transaction sender from local account: %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() + var committeeGroupKey *keys.PrivateKey + txMonitor := newTransactionGroupMonitor(localActor) + managementContract := management.New(localActor) for ; ; prm.monitor.waitForNextBlock(ctx) { select { @@ -141,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 := prm.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 { - localActor, 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(localActor) - - 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 { @@ -190,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) } } @@ -286,7 +280,12 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { return fmt.Errorf("read version of the local NNS contract: %w", err) } - var updateTxValidUntilBlock 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() + + txMonitor := newTransactionGroupMonitor(committeeActor) for ; ; prm.monitor.waitForNextBlock(ctx) { select { @@ -340,8 +339,6 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { setGroupInManifest(&prm.localManifest, prm.localNEF, prm.committeeGroupKey, prm.localAcc.ScriptHash()) - var vub uint32 - // 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 @@ -354,23 +351,17 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { prm.logger.Info("NNS contract has already been updated, skip") return nil } - } else { - if updateTxValidUntilBlock > 0 { - prm.logger.Info("transaction updating NNS contract was sent earlier, checking relevance...") - if cur := prm.monitor.currentHeight(); cur <= updateTxValidUntilBlock { - prm.logger.Info("previously sent transaction updating NNS contract may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", updateTxValidUntilBlock)) - continue - } - - prm.logger.Info("previously sent transaction updating NNS contract expired without side-effect") - } - - prm.logger.Info("sending new transaction updating NNS contract...") + prm.logger.Error("failed to make transaction updating NNS contract, will try again later", zap.Error(err)) + continue + } - _, _, vub, err = committeeActor.Notarize(tx, nil) + 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 { lackOfGAS := isErrNotEnoughGAS(err) // here lackOfGAS=true always means lack of Notary balance and not related to @@ -393,8 +384,9 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { continue } - updateTxValidUntilBlock = vub + 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)) - prm.logger.Info("transaction updating NNS contract has been successfully sent, will wait for the outcome") + txMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) } } diff --git a/pkg/morph/deploy/notary.go b/pkg/morph/deploy/notary.go index 8e28dafb6c..960942d1ac 100644 --- a/pkg/morph/deploy/notary.go +++ b/pkg/morph/deploy/notary.go @@ -53,13 +53,18 @@ type enableNotaryPrm struct { // enableNotary makes Notary service ready-to-go for the committee members. func enableNotary(ctx context.Context, prm enableNotaryPrm) 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() + 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) + tick, err = initDesignateNotaryRoleToLocalAccountTick(ctx, prm) if err != nil { return fmt.Errorf("construct action designating Notary role to the local account: %w", err) } @@ -67,12 +72,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) + 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) + 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) } @@ -118,7 +123,7 @@ 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) (func(), error) { +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) @@ -127,33 +132,20 @@ func initDesignateNotaryRoleToLocalAccountTick(prm enableNotaryPrm) (func(), err roleContract := rolemgmt.New(localActor) // multi-tick context - var sentTxValidUntilBlock uint32 + txMonitor := newTransactionGroupMonitor(localActor) return func() { - if sentTxValidUntilBlock > 0 && sentTxValidUntilBlock <= prm.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 := prm.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 { @@ -162,9 +154,10 @@ func initDesignateNotaryRoleToLocalAccountTick(prm enableNotaryPrm) (func(), err 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 } @@ -172,7 +165,7 @@ func initDesignateNotaryRoleToLocalAccountTick(prm enableNotaryPrm) (func(), err // 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) (func(), error) { +func initDesignateNotaryRoleAsLeaderTick(ctx context.Context, prm enableNotaryPrm) (func(), error) { committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(prm.committee)) committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) @@ -212,21 +205,22 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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() { @@ -267,16 +261,16 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { l.Info("sending new transaction setting domain record in the NNS...") + var txID util.Uint256 var vub uint32 if recordExists { - _, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, + txID, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, domainDesignateNotaryTx, int64(nns.TXT), 0, strSharedTxData) } else { - _, vub, err = localActor.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 { @@ -285,9 +279,10 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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(invkr, prm.nnsOnChainAddress, domainDesignateNotaryTx) @@ -295,24 +290,16 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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 := prm.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 := localActor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, + 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 { @@ -321,10 +308,10 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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)) @@ -333,16 +320,9 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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 := prm.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) @@ -485,15 +465,10 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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 := prm.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 @@ -525,9 +500,8 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { prm.logger.Info("sending the transaction designating Notary role to the committee...") - _, vub, err := localActor.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", @@ -542,9 +516,11 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { 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 } @@ -552,7 +528,7 @@ func initDesignateNotaryRoleAsLeaderTick(prm enableNotaryPrm) (func(), error) { // 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) (func(), error) { +func initDesignateNotaryRoleAsSignerTick(ctx context.Context, prm enableNotaryPrm) (func(), error) { committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(prm.committee)) committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) @@ -593,12 +569,12 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { // 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() { @@ -667,24 +643,16 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { 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 := prm.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 := localActor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, + 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 { @@ -693,9 +661,10 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { 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) { @@ -705,16 +674,9 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { 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 := prm.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 @@ -751,16 +713,16 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { l.Info("sending new transaction setting domain record in the NNS...") + var txID util.Uint256 var vub uint32 if recordExists { - _, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, + txID, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, domain, int64(nns.TXT), 0, rec) } else { - _, vub, err = localActor.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 { @@ -769,10 +731,10 @@ func initDesignateNotaryRoleAsSignerTick(prm enableNotaryPrm) (func(), error) { 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 @@ -929,7 +891,7 @@ var singleNotaryDepositAmount = big.NewInt(1_0000_0000) // 1 GAS // constructs notaryDepositDeficiencyHandler working with the specified // Blockchain and GAS/Notary balance of the given account. -func initNotaryDepositDeficiencyHandler(l *zap.Logger, b Blockchain, monitor *blockchainMonitor, localAcc *wallet.Account) (notaryDepositDeficiencyHandler, error) { +func initNotaryDepositDeficiencyHandler(ctx context.Context, l *zap.Logger, b Blockchain, localAcc *wallet.Account) (notaryDepositDeficiencyHandler, error) { localActor, err := actor.NewSimple(b, localAcc) if err != nil { return nil, fmt.Errorf("init transaction sender from local account: %w", err) @@ -940,8 +902,8 @@ func initNotaryDepositDeficiencyHandler(l *zap.Logger, b Blockchain, monitor *bl localAccID := localAcc.ScriptHash() // multi-tick context - var transferTxValidUntilBlock uint32 - var expirationTxValidUntilBlock uint32 + transferTxMonitor := newTransactionGroupMonitor(localActor) + expirationTxMonitor := newTransactionGroupMonitor(localActor) return func(lackOfGAS bool) { notaryBalance, err := notaryContract.BalanceOf(localAccID) @@ -976,48 +938,31 @@ func initNotaryDepositDeficiencyHandler(l *zap.Logger, b Blockchain, monitor *bl if !lackOfGAS { // deposit expired if new(big.Int).Mul(notaryBalance, big.NewInt(refillProportion)).Cmp(singleNotaryDepositAmount) >= 0 { - if expirationTxValidUntilBlock > 0 { - l.Info("transaction increasing expiration time of the Notary deposit was sent earlier, checking relevance...") - - if cur := monitor.currentHeight(); cur <= expirationTxValidUntilBlock { - l.Info("previously sent transaction increasing expiration time of the Notary deposit may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", expirationTxValidUntilBlock)) - return - } - - l.Info("previously sent transaction increasing expiration time of the Notary deposit expired without side-effect ") + if expirationTxMonitor.isPending() { + l.Info("previously sent transaction increasing expiration time of the Notary deposit is still pending, will wait for the outcome") + return } l.Info("sending new transaction increasing expiration time of the Notary deposit...", zap.Uint32("till", till)) - _, vub, err := notaryContract.LockDepositUntil(localAccID, till) + txID, vub, err := notaryContract.LockDepositUntil(localAccID, till) if err != nil { l.Error("failed to send transaction increasing expiration time of the Notary deposit, will try again later", zap.Error(err)) return } - expirationTxValidUntilBlock = vub + l.Info("transaction increasing expiration time of the Notary deposit has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub)) - l.Info("transaction increasing expiration time of the Notary deposit has been successfully sent, will wait for the outcome") + expirationTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) return } } - if transferTxValidUntilBlock > 0 { - l.Info("transaction transferring local account's GAS to the Notary contract was sent earlier, checking relevance...") - - // for simplicity, we track ValidUntilBlock. In this particular case, it'd be - // more efficient to monitor a transaction by ID, because side effect is - // inconsistent (funds can be spent in background). - - if cur := monitor.currentHeight(); cur <= transferTxValidUntilBlock { - l.Info("previously sent transaction transferring local account's GAS to the Notary contract may still be relevant, will wait for the outcome", - zap.Uint32("current height", cur), zap.Uint32("retry after height", transferTxValidUntilBlock)) - return - } - - l.Info("previously sent transaction transferring local account's GAS to the Notary contract expired without side-effect") + if transferTxMonitor.isPending() { + l.Info("previously sent transaction local account's GAS to the Notary contract is still pending, will wait for the outcome") + return } needAtLeast := new(big.Int).Mul(singleNotaryDepositAmount, big.NewInt(gasProportion)) @@ -1037,15 +982,15 @@ func initNotaryDepositDeficiencyHandler(l *zap.Logger, b Blockchain, monitor *bl // nep17.TokenWriter.Transfer doesn't support notary.OnNEP17PaymentData // directly, so split the args // Track https://github.com/nspcc-dev/neofs-node/issues/2429 - _, vub, err := gasContract.Transfer(localAccID, notary.Hash, singleNotaryDepositAmount, []interface{}{transferData.Account, transferData.Till}) + 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)) return } - transferTxValidUntilBlock = vub - l.Info("transaction transferring local account's GAS to the Notary contract has been successfully sent, will wait for the outcome") + + transferTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) }, nil } diff --git a/pkg/morph/deploy/util.go b/pkg/morph/deploy/util.go index fc164a4458..903d91ad2c 100644 --- a/pkg/morph/deploy/util.go +++ b/pkg/morph/deploy/util.go @@ -284,3 +284,42 @@ func readContractLocalVersion(rpc invoker.RPCInvoke, localNEF nef.File, localMan 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() + }() +} From 29877e96c59051ea739816a501bd57e0b01892fa Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Sun, 9 Jul 2023 21:08:15 +0400 Subject: [PATCH 4/4] sidechain/deploy: Use separate autonomous routine for Notary deposit Signed-off-by: Leonard Lyubich --- pkg/morph/deploy/deploy.go | 19 +++-- pkg/morph/deploy/nns.go | 23 ++---- pkg/morph/deploy/notary.go | 143 +++++++++++++------------------------ pkg/morph/deploy/util.go | 12 ++-- 4 files changed, 68 insertions(+), 129 deletions(-) diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index ab17b9dcff..09328a5811 100644 --- a/pkg/morph/deploy/deploy.go +++ b/pkg/morph/deploy/deploy.go @@ -134,7 +134,9 @@ func Deploy(ctx context.Context, prm Prm) error { return errors.New("local account does not belong to any Neo committee member") } - monitor, err := newBlockchainMonitor(prm.Logger, prm.Blockchain) + chNewBlock := make(chan struct{}, 1) + + monitor, err := newBlockchainMonitor(prm.Logger, prm.Blockchain, chNewBlock) if err != nil { return fmt.Errorf("init blockchain monitor: %w", err) } @@ -203,17 +205,13 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("Notary service successfully enabled for the committee") - onNotaryDepositDeficiency, err := initNotaryDepositDeficiencyHandler(ctx, prm.Logger, prm.Blockchain, prm.LocalAccount) - if err != nil { - return fmt.Errorf("construct action depositing funds to the local account's Notary balance: %w", err) - } + 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, - onNotaryDepositDeficiency: onNotaryDepositDeficiency, + 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) @@ -251,7 +249,6 @@ func Deploy(ctx context.Context, prm Prm) error { committee: committee, committeeGroupKey: committeeGroupKey, buildVersionedExtraUpdateArgs: noExtraUpdateArgs, - onNotaryDepositDeficiency: onNotaryDepositDeficiency, }) if err != nil { return fmt.Errorf("update NNS contract on the chain: %w", err) diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index d5825c41eb..66f9bec002 100644 --- a/pkg/morph/deploy/nns.go +++ b/pkg/morph/deploy/nns.go @@ -244,8 +244,6 @@ type updateNNSContractPrm struct { // contract. If returns both nil, no data is passed (noExtraUpdateArgs may be // used). buildVersionedExtraUpdateArgs func(versionOnChain contractVersion) ([]interface{}, error) - - onNotaryDepositDeficiency notaryDepositDeficiencyHandler } // updateNNSContract synchronizes on-chain NNS contract (its presence is a @@ -363,24 +361,11 @@ func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error { mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(tx, nil) if err != nil { - lackOfGAS := isErrNotEnoughGAS(err) - // here lackOfGAS=true always means lack of Notary balance and not related to - // the main transaction itself - if !lackOfGAS { - if !isErrNotaryDepositExpires(err) { - prm.logger.Error("failed to send transaction deploying NNS contract, will try again later", zap.Error(err)) - continue - } + 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)) } - - // same approach with in-place deposit is going to be used in other functions. - // Consider replacement with background process (e.g. blockchainMonitor - // internal) which periodically checks Notary balance and updates it when, for - // example, balance goes lower than 20% of desired amount or expires soon. With - // this approach functions like current will not try to make a deposit, but - // simply wait until it becomes enough. - prm.onNotaryDepositDeficiency(lackOfGAS) - continue } diff --git a/pkg/morph/deploy/notary.go b/pkg/morph/deploy/notary.go index 960942d1ac..64c4e7cb76 100644 --- a/pkg/morph/deploy/notary.go +++ b/pkg/morph/deploy/notary.go @@ -21,6 +21,7 @@ import ( "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" @@ -871,51 +872,56 @@ func newCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee k }, localAcc) } -// notaryDepositDeficiencyHandler is a function returned by initNotaryDepositDeficiencyHandler. -// True argument is passed when there is not enough GAS on local account's -// balance in the Notary contract, false - when local account's Notary deposit -// expires before particular fallback transaction. -// -// The function is intended to be called multiple times on each deposit problem -// encounter. On each call, It attempts to fix Notary deposit problem without -// waiting for success. Caller should by default wait for the problem to be -// fixed, and if not, retry. -// -// notaryDepositDeficiencyHandler must not be called from multiple routines in -// parallel. -type notaryDepositDeficiencyHandler = func(lackOfGAS bool) - // 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 -// constructs notaryDepositDeficiencyHandler working with the specified -// Blockchain and GAS/Notary balance of the given account. -func initNotaryDepositDeficiencyHandler(ctx context.Context, l *zap.Logger, b Blockchain, localAcc *wallet.Account) (notaryDepositDeficiencyHandler, error) { - localActor, err := actor.NewSimple(b, localAcc) - if err != nil { - return nil, fmt.Errorf("init transaction sender from local account: %w", err) - } +func autoReplenishNotaryBalance(ctx context.Context, l *zap.Logger, b Blockchain, localAcc *wallet.Account, chTrigger <-chan struct{}) { + l.Info("tracking Notary balance for auto-replenishment...") - notaryContract := notary.New(localActor) - gasContract := gas.New(localActor) + var err error + var localActor *actor.Actor + var notaryContract *notary.Contract + var gasContract *nep17.Token + var txMonitor *transactionGroupMonitor localAccID := localAcc.ScriptHash() - // multi-tick context - transferTxMonitor := newTransactionGroupMonitor(localActor) - expirationTxMonitor := newTransactionGroupMonitor(localActor) + 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) + } - return func(lackOfGAS bool) { 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)) - return + continue } - gasBalance, err := gasContract.BalanceOf(localAccID) - if err != nil { - l.Error("failed to read GAS token balance of the local account, will try again later", zap.Error(err)) - return + // 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 @@ -925,56 +931,14 @@ func initNotaryDepositDeficiencyHandler(ctx context.Context, l *zap.Logger, b Bl // 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. - - const ( - // GAS:Notary proportion, see scheme above - gasProportion = 50 - // even there is no lack of GAS at the moment, when the balance falls below 1/5 - // of the supported value - replenish - refillProportion = 5 - // for simplicity, we just make Notary deposit "infinite" not to prolong - till = math.MaxUint32 - ) - - if !lackOfGAS { // deposit expired - if new(big.Int).Mul(notaryBalance, big.NewInt(refillProportion)).Cmp(singleNotaryDepositAmount) >= 0 { - if expirationTxMonitor.isPending() { - l.Info("previously sent transaction increasing expiration time of the Notary deposit is still pending, will wait for the outcome") - return - } - - l.Info("sending new transaction increasing expiration time of the Notary deposit...", zap.Uint32("till", till)) - - txID, vub, err := notaryContract.LockDepositUntil(localAccID, till) - if err != nil { - l.Error("failed to send transaction increasing expiration time of the Notary deposit, will try again later", zap.Error(err)) - return - } - - l.Info("transaction increasing expiration time of the Notary deposit has been successfully sent, will wait for the outcome", - zap.Stringer("tx", txID), zap.Uint32("vub", vub)) - - expirationTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) - - return - } - } - - if transferTxMonitor.isPending() { - l.Info("previously sent transaction local account's GAS to the Notary contract is still pending, will wait for the outcome") - return - } - - needAtLeast := new(big.Int).Mul(singleNotaryDepositAmount, big.NewInt(gasProportion)) - if gasBalance.Cmp(needAtLeast) < 0 { - l.Info("minimum threshold for GAS transfer from local account to the Notary contract not reached, will wait for replenishment", - zap.Stringer("need at least", needAtLeast), zap.Stringer("have", gasBalance)) - return + 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 = till + 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)) @@ -985,13 +949,13 @@ func initNotaryDepositDeficiencyHandler(ctx context.Context, l *zap.Logger, b Bl 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)) - return + continue } l.Info("transaction transferring local account's GAS to the Notary contract has been successfully sent, will wait for the outcome") - transferTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) - }, nil + txMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + } } // listenCommitteeNotaryRequestsPrm groups parameters of listenCommitteeNotaryRequests. @@ -1003,8 +967,6 @@ type listenCommitteeNotaryRequestsPrm struct { localAcc *wallet.Account committee keys.PublicKeys - - onNotaryDepositDeficiency notaryDepositDeficiencyHandler } // listenCommitteeNotaryRequests starts background process listening to incoming @@ -1146,18 +1108,11 @@ func listenCommitteeNotaryRequests(ctx context.Context, prm listenCommitteeNotar _, _, _, err = notaryActor.Notarize(mainTx, nil) if err != nil { - lackOfGAS := isErrNotEnoughGAS(err) - // here lackOfGAS=true always means lack of Notary balance and not related to - // the main transaction itself - if !lackOfGAS { - if !isErrNotaryDepositExpires(err) { - prm.logger.Error("failed to send transaction deploying NNS contract, will try again later", zap.Error(err)) - continue - } + 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)) } - - prm.onNotaryDepositDeficiency(lackOfGAS) - continue } diff --git a/pkg/morph/deploy/util.go b/pkg/morph/deploy/util.go index 903d91ad2c..30d37d4e86 100644 --- a/pkg/morph/deploy/util.go +++ b/pkg/morph/deploy/util.go @@ -44,10 +44,6 @@ func isErrInvalidTransaction(err error) bool { return errors.Is(err, neorpc.ErrValidationFailed) } -func isErrNotaryDepositExpires(err error) bool { - return strings.Contains(err.Error(), "fallback transaction is valid after deposit is unlocked") -} - func isErrContractAlreadyUpdated(err error) bool { return strings.Contains(err.Error(), common.ErrAlreadyUpdated) } @@ -92,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) @@ -124,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)) } }()