Skip to content

Commit

Permalink
sidechain/deploy: Auto-update on-chain NeoFS NNS smart contract
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
cthulhu-rider committed Jun 29, 2023
1 parent 52c6ef0 commit 8fa29f0
Show file tree
Hide file tree
Showing 5 changed files with 642 additions and 8 deletions.
7 changes: 7 additions & 0 deletions pkg/morph/deploy/contracts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package deploy

// various common methods of the NeoFS contracts.
const (
methodUpdate = "update"
methodVersion = "version"
)
61 changes: 54 additions & 7 deletions pkg/morph/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand All @@ -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
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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{
Expand All @@ -209,7 +233,30 @@ func Deploy(ctx context.Context, prm Prm) error {

prm.Logger.Info("committee group successfully initialized", zap.Stringer("public key", committeeGroupKey.PublicKey()))

// TODO: deploy contracts
prm.Logger.Info("updating on-chain NNS contract...")

err = updateNNSContract(ctx, updateNNSContractPrm{
logger: prm.Logger,
blockchain: prm.Blockchain,
monitor: monitor,
localAcc: prm.LocalAccount,
localNEF: prm.NNS.Common.NEF,
localManifest: prm.NNS.Common.Manifest,
systemEmail: prm.NNS.SystemEmail,
committee: committee,
committeeGroupKey: committeeGroupKey,
buildVersionedExtraUpdateArgs: noExtraUpdateArgs,
onNotaryDepositDeficiency: onNotaryDepositDeficiency,
})
if err != nil {
return fmt.Errorf("update NNS contract on the chain: %w", err)
}

prm.Logger.Info("on-chain NNS contract successfully updated")

// TODO: deploy/update other contracts

return nil
}

func noExtraUpdateArgs(contractVersion) ([]interface{}, error) { return nil, nil }
172 changes: 172 additions & 0 deletions pkg/morph/deploy/nns.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package deploy

import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
Expand Down Expand Up @@ -226,3 +227,174 @@ func lookupNNSDomainRecord(inv *invoker.Invoker, nnsContract util.Uint160, domai

return "", errMissingDomainRecord
}

// updateNNSContractPrm groups parameters of NeoFS NNS contract update.
type updateNNSContractPrm struct {
logger *zap.Logger

blockchain Blockchain

// based on blockchain
monitor *blockchainMonitor

localAcc *wallet.Account

localNEF nef.File
localManifest manifest.Manifest
systemEmail string

committee keys.PublicKeys
committeeGroupKey *keys.PrivateKey

// constructor of extra arguments to be passed into method updating the
// contract. If returns both nil, no data is passed (noExtraUpdateArgs may be
// used).
buildVersionedExtraUpdateArgs func(versionOnChain contractVersion) ([]interface{}, error)

onNotaryDepositDeficiency notaryDepositDeficiencyHandler
}

// updateNNSContract synchronizes on-chain NNS contract (its presence is a
// precondition) with the local one represented by compiled executables. If
// on-chain version is greater or equal to the local one, nothing happens.
// Otherwise, transaction calling 'update' method is sent.
//
// Local manifest is extended with committee group represented by the
// parameterized private key.
//
// Function behaves similar to initNNSContract in terms of context.
func updateNNSContract(ctx context.Context, prm updateNNSContractPrm) error {
bLocalNEF, err := prm.localNEF.Bytes()
if err != nil {
// not really expected
return fmt.Errorf("encode local NEF of the NNS contract into binary: %w", err)
}

jLocalManifest, err := json.Marshal(prm.localManifest)
if err != nil {
// not really expected
return fmt.Errorf("encode local manifest of the NNS contract into JSON: %w", err)
}

committeeActor, err := newCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee)
if err != nil {
return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err)
}

var updateTxValidUntilBlock uint32

for ; ; prm.monitor.waitForNextBlock(ctx) {
select {
case <-ctx.Done():
return fmt.Errorf("wait for NNS contract synchronization: %w", ctx.Err())
default:
}

prm.logger.Info("reading on-chain state of the NNS contract...")

nnsOnChainState, err := readNNSOnChainState(prm.blockchain)
if err != nil {
prm.logger.Error("failed to read on-chain state of the NNS contract, will try again later", zap.Error(err))
continue
} else if nnsOnChainState == nil {
// NNS contract must be deployed at this stage
return errors.New("missing required NNS contract on the chain")
}

if nnsOnChainState.NEF.Checksum == prm.localNEF.Checksum {
// manifests may differ, but currently we should bump internal contract version
// (i.e. change NEF) to make such updates. Right now they are not supported due
// to dubious practical need
// Track https://github.com/nspcc-dev/neofs-contract/issues/340
prm.logger.Info("same local and on-chain checksums of the NNS contract NEF, update is not needed")
return nil
}

prm.logger.Info("NEF checksums of the on-chain and local NNS contracts differ, need an update")

versionOnChain, err := readContractOnChainVersion(prm.blockchain, nnsOnChainState.Hash)
if err != nil {
prm.logger.Error("failed to read on-chain version of the NNS contract, will try again later", zap.Error(err))
continue
}

// 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")
}
}
Loading

0 comments on commit 8fa29f0

Please sign in to comment.