From 01dfe5b68e489d4eb2525e904e6ed5b4ecf35e68 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 27 Jun 2023 22:42:44 +0400 Subject: [PATCH] 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 --- pkg/morph/deploy/contracts.go | 7 + pkg/morph/deploy/deploy.go | 63 +++++- pkg/morph/deploy/nns.go | 170 ++++++++++++++++ pkg/morph/deploy/notary.go | 363 ++++++++++++++++++++++++++++++++++ pkg/morph/deploy/util.go | 51 ++++- 5 files changed, 646 insertions(+), 8 deletions(-) create mode 100644 pkg/morph/deploy/contracts.go diff --git a/pkg/morph/deploy/contracts.go b/pkg/morph/deploy/contracts.go new file mode 100644 index 00000000000..76987082417 --- /dev/null +++ b/pkg/morph/deploy/contracts.go @@ -0,0 +1,7 @@ +package deploy + +// various common methods of the NNS contracts. +const ( + methodUpdate = "update" + methodVersion = "version" +) diff --git a/pkg/morph/deploy/deploy.go b/pkg/morph/deploy/deploy.go index bb22e93ec61..1cd7cc4042b 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,32 @@ 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: func(versionOnChain contractVersion) ([]interface{}, error) { + // no extra update arguments for now: they are unlikely to appear, but the + // groundwork is laid + return nil, nil + }, + 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 } diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index 1b1ebf2cdaf..90254f5381e 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" @@ -226,3 +227,172 @@ 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. + 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) + } + + 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)) + } 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 + } + + // we could also try to compare on-chain version with the local one and tune + // update strategy: don't try to update with earlier version and catch updated + // contracts with unchanged version (blunder, we should react to it). For + // simplicity, we naively rely on the version change when the contract changes + + 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) { + // FIXME: we get here when on-chain version is >= than the local one. If equals, + // this case must be considered as failure since version must be changed if code + // changes (NEF checksum differs here according to condition above). If greater, + // we should try to update at all. For simplicity, such condition is considered + // as success by current procedure now. To catch it, we need to pre-read version + // of the local contract using local NEF. + 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 can become true even if error corresponds to insufficient + // funds for the main transaction, not Notary balance problem. Right now we + // cannot distinguish between these two cases. So, we naively believe that the + // problem is in the Notary balance, because according to used procedure we make + // Notary deposit when at least 50GAS is available (most likely enough for all + // transactions). + 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 470a79e4c6a..fcb97d4e254 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/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/encoding/fixedn" + "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" @@ -869,3 +878,357 @@ func makeUnsignedDesignateCommitteeNotaryTx(roleContract *rolemgmt.Contract, com return tx, nil } + +// newCommitteeNotaryActor returns notary.Actor 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.CalledByEntry, + }, + 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(int64(fixedn.Fixed8FromInt64(1))) // 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) { + _actor, err := actor.NewSimple(b, localAcc) + if err != nil { + return nil, fmt.Errorf("init transaction sender from local account: %w", err) + } + + notaryContract := notary.New(_actor) + gasContract := gas.New(_actor) + + // multi-tick context + var transferTxValidUntilBlock uint32 + var expirationTxValidUntilBlock uint32 + + return func(lackOfGAS bool) { + currentDepositExpirationHeight, err := notaryContract.ExpirationOf(localAcc.ScriptHash()) + if err != nil { + l.Error("failed to read blockchain height when local account's GAS deposit expires, will try again later", zap.Error(err)) + return + } + + if currentDepositExpirationHeight == 0 { // no deposit yet + currentDepositExpirationHeight, err = b.GetBlockCount() + if err != nil { + l.Error("failed to read current blockchain height, will try again later", zap.Error(err)) + return + } + } + + localAccID := localAcc.ScriptHash() + + 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. + + till := currentDepositExpirationHeight + 100 + + if !lackOfGAS { // deposit expired + if new(big.Int).Mul(notaryBalance, big.NewInt(5)).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(50)) + 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 release of https://github.com/nspcc-dev/neo-go/issues/2987 + _, 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 expectedSigners = 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] + + 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) != expectedSigners: + prm.logger.Info("unsupported number of signers of main transaction from the received notary request, skip", + zap.Int("expected", expectedSigners), zap.Int("got", len(mainTx.Signers))) + continue + case !mainTx.HasSigner(committeeMultiSigAccID): + // in theory, there can be any notary requests besides those sent by the current + // auto-deploy procedure. However, it's better to log with 'info' severity since + // this isn't really expected in practice. + 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. For simplicity, make a shallow + // copy: we change only Scripts field below. Overall, this approach is very + // risky, and it'd better to make a deep copy + 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 + + 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) + // see same place in updateNNS + 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 e2e7d18edac..a9d6a1c2739 100644 --- a/pkg/morph/deploy/util.go +++ b/pkg/morph/deploy/util.go @@ -11,9 +11,12 @@ 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/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neofs-contract/common" "go.uber.org/atomic" "go.uber.org/zap" ) @@ -26,13 +29,21 @@ 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 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(_manifest *manifest.Manifest, _nef nef.File, groupPrivKey *keys.PrivateKey, deployerAcc util.Uint160) { contractAddress := state.CreateContractHash(deployerAcc, _nef.Checksum, _manifest.Name) sig := groupPrivKey.Sign(contractAddress.BytesBE()) @@ -166,3 +177,41 @@ func readNNSOnChainState(b Blockchain) (*state.Contract, error) { } return res, nil } + +// contractVersion describes versioning of NeoFS smart contracts. +type contractVersion struct{ major, minor, patch uint64 } + +// 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 +} + +func (x contractVersion) String() string { + const sep = "." + return fmt.Sprintf("%d%s%d%s%d", x.major, sep, x.minor, sep, x.patch) +} + +// readContractOnChainVersion returns current version of the smart contract +// presented in given Blockchain with specified address. +func readContractOnChainVersion(b Blockchain, onChainAddress util.Uint160) (contractVersion, error) { + bigVersionOnChain, err := unwrap.BigInt(invoker.New(b, nil).Call(onChainAddress, methodVersion)) + if err != nil { + return contractVersion{}, fmt.Errorf("call '%s' method: %w", methodVersion, err) + } else if !bigVersionOnChain.IsUint64() { + return contractVersion{}, fmt.Errorf("invalid/unsupported response of '%s' method: expected uint64, got %v", + methodVersion, bigVersionOnChain) + } + + const majorSpace, minorSpace = 1e6, 1e3 + n := bigVersionOnChain.Uint64() + + mjr := n / majorSpace + + return contractVersion{ + major: mjr, + minor: (n - mjr*majorSpace) / minorSpace, + patch: n % minorSpace, + }, nil +}