From 1c67f9ac412e4fe1ccfd42560abaaad877201ad2 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Sun, 9 Jul 2023 14:14:28 +0400 Subject: [PATCH] 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() + }() +}