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..d6cf762998e 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,6 +233,29 @@ func Deploy(ctx context.Context, prm Prm) error { prm.Logger.Info("committee group successfully initialized", zap.Stringer("public key", committeeGroupKey.PublicKey())) + 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) + } + // TODO: deploy contracts return nil diff --git a/pkg/morph/deploy/nns.go b/pkg/morph/deploy/nns.go index 1b1ebf2cdaf..13ce50fce49 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 + + // optional constructor of extra arguments to be passed into method updating + // the contract. If omitted, 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 the NNS contract state to be read from the chain: %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 +}