diff --git a/deploy/alphabet.go b/deploy/alphabet.go new file mode 100644 index 00000000..f7a19d59 --- /dev/null +++ b/deploy/alphabet.go @@ -0,0 +1,294 @@ +package deploy + +import ( + "context" + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neorpc" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" + "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/wallet" + "go.uber.org/zap" +) + +// initAlphabetPrm groups parameters of Alphabet members initialization. +type initAlphabetPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + committee keys.PublicKeys + localAcc *wallet.Account +} + +// initAlphabet designates NeoFS Alphabet role to all committee members on the +// given Blockchain. +func initAlphabet(ctx context.Context, prm initAlphabetPrm) 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() + + 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) + } + + roleContract := rolemgmt.New(committeeActor) + txMonitor := newTransactionGroupMonitor(committeeActor) + + for ; ; err = prm.monitor.waitForNextBlock(ctx) { + if err != nil { + return fmt.Errorf("wait for NeoFS Alphabet role to be designated for the committee: %w", err) + } + + prm.logger.Info("checking NeoFS Alphabet role of the committee members...") + + accsWithAlphabetRole, err := roleContract.GetDesignatedByRole(noderoles.NeoFSAlphabet, prm.monitor.currentHeight()) + if err != nil { + prm.logger.Error("failed to check role of the committee, will try again later", zap.Error(err)) + continue + } + + someoneWithoutRole := len(accsWithAlphabetRole) < len(prm.committee) + if !someoneWithoutRole { + for i := range prm.committee { + if !accsWithAlphabetRole.Contains(prm.committee[i]) { + someoneWithoutRole = true + break + } + } + } + if !someoneWithoutRole { + prm.logger.Info("all committee members have a NeoFS Alphabet role") + return nil + } + + prm.logger.Info("not all members of the committee have a NeoFS Alphabet role, designation is needed") + + if txMonitor.isPending() { + prm.logger.Info("previously sent Notary request designating NeoFS Alphabet role to the committee is still pending, will wait for the outcome") + continue + } + + mainTxID, fallbackTxID, vub, err := committeeActor.Notarize( + roleContract.DesignateAsRoleTransaction(noderoles.NeoFSAlphabet, prm.committee)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to send new Notary request designating NeoFS Alphabet role to the committee, skip") + } else { + prm.logger.Error("failed to send new Notary request designating NeoFS Alphabet role to the committee, skip", zap.Error(err)) + } + continue + } + + prm.logger.Info("Notary request designating NeoFS Alphabet role to the committee has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + txMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } +} + +// groups parameters of initVoteForAlphabet. +type initVoteForAlphabetPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + committee keys.PublicKeys + localAcc *wallet.Account + + // pays for Notary transactions + proxyContract util.Uint160 +} + +// initializes vote for NeoFS Alphabet members for the role of validators. +func initVoteForAlphabet(ctx context.Context, prm initVoteForAlphabetPrm) error { + committeeActor, err := newProxyCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee, prm.proxyContract) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) + } + + roleContract := rolemgmt.NewReader(committeeActor) + + alphabet, err := roleContract.GetDesignatedByRole(noderoles.NeoFSAlphabet, prm.monitor.currentHeight()) + if err != nil { + return fmt.Errorf("request NeoFS Alphabet members: %w", err) + } + + if len(alphabet) == 0 { + return errors.New("no NeoFS Alphabet members are set") + } + + // 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() + + neoContract := neo.New(committeeActor) + txMonitor := newTransactionGroupMonitor(committeeActor) + mRegisteredAlphabetIndices := make(map[int]struct{}, len(alphabet)) + var originalPrice int64 + scriptBuilder := smartcontract.NewBuilder() + setRegisterPrice := func(price int64) { scriptBuilder.InvokeMethod(neo.Hash, "setRegisterPrice", price) } + +mainLoop: + for ; ; err = prm.monitor.waitForNextBlock(ctx) { + if err != nil { + return fmt.Errorf("wait for NeoFS Alphabet to be registered as candidates to validators: %w", err) + } + + prm.logger.Info("checking registered candidates to validators...") + + iterCandidates, err := neoContract.GetAllCandidates() + if err != nil { + prm.logger.Error("init iterator over registered candidates to validators, will try again later", zap.Error(err)) + continue + } + + for k := range mRegisteredAlphabetIndices { + delete(mRegisteredAlphabetIndices, k) + } + + for { + candidates, err := iterCandidates.Next(len(alphabet) - len(mRegisteredAlphabetIndices)) + if err != nil { + prm.logger.Error("get next list of registered candidates to validators, will try again later", zap.Error(err)) + continue mainLoop + } + + if len(candidates) == 0 { + break + } + + loop: + for i := range alphabet { + if _, ok := mRegisteredAlphabetIndices[i]; ok { + continue + } + + for j := range candidates { + if candidates[j].PublicKey.Equal(alphabet[i]) { + mRegisteredAlphabetIndices[i] = struct{}{} + if len(mRegisteredAlphabetIndices) == len(alphabet) { + break loop + } + continue loop + } + } + } + } + + err = iterCandidates.Terminate() + if err != nil { + prm.logger.Info("failed to terminate iterator over registered candidates to validators, ignore", zap.Error(err)) + } + + if len(mRegisteredAlphabetIndices) == len(alphabet) { + prm.logger.Info("all NeoFS Alphabet members are registered as candidates to validators") + return nil + } + + prm.logger.Info("not all members of the NeoFS Alphabet are candidates to validators, registration is needed") + + if txMonitor.isPending() { + prm.logger.Info("previously sent Notary request registering NeoFS Alphabet members as candidates to validators is still pending, will wait for the outcome") + continue + } + + originalPrice, err = neoContract.GetRegisterPrice() + if err != nil { + prm.logger.Info("failed to get original candidate registration price, will try again later", + zap.Error(err)) + continue + } + + scriptBuilder.Reset() + + const minPrice = 1 // 0 is forbidden + if originalPrice > minPrice { + setRegisterPrice(minPrice) + } + + for i := range alphabet { + if _, ok := mRegisteredAlphabetIndices[i]; ok { + continue + } + + prm.logger.Info("NeoFS Alphabet member is not yet a candidate to validators, going to register", + zap.Stringer("member", alphabet[i])) + + scriptBuilder.InvokeWithAssert(neo.Hash, "registerCandidate", alphabet[i].Bytes()) + } + + if originalPrice > minPrice { + setRegisterPrice(originalPrice) + } + + script, err := scriptBuilder.Script() + if err != nil { + prm.logger.Info("failed to build script registering NeoFS Alphabet members as validators, will try again later", + zap.Error(err)) + continue + } + + candidateSigners := make([]actor.SignerAccount, 0, len(alphabet)-len(mRegisteredAlphabetIndices)) + + for i := range alphabet { + if _, ok := mRegisteredAlphabetIndices[i]; ok { + continue + } + + var acc *wallet.Account + if alphabet[i].Equal(prm.localAcc.PublicKey()) { + acc = prm.localAcc + } else { + acc = notary.FakeSimpleAccount(alphabet[i]) + } + candidateSigners = append(candidateSigners, actor.SignerAccount{ + Signer: transaction.Signer{ + Account: alphabet[i].GetScriptHash(), + Scopes: transaction.CustomContracts, + AllowedContracts: []util.Uint160{neo.Hash}, + }, + Account: acc, + }) + } + + curActor, err := newProxyCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee, prm.proxyContract, candidateSigners...) + if err != nil { + prm.logger.Error("failed to make Notary actor with candidate signers, will try again later", + zap.Error(err)) + continue + } + + mainTxID, fallbackTxID, vub, err := curActor.Notarize(curActor.MakeRun(script)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to send new Notary request registering NeoFS Alphabet members as validators, skip") + } else { + prm.logger.Error("failed to send new Notary request registering NeoFS Alphabet members as validators, skip", zap.Error(err)) + } + continue + } + + prm.logger.Info("Notary request registering NeoFS Alphabet members as validators has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + txMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } +} diff --git a/deploy/contracts.go b/deploy/contracts.go new file mode 100644 index 00000000..486f6e0c --- /dev/null +++ b/deploy/contracts.go @@ -0,0 +1,395 @@ +package deploy + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neorpc" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/management" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "go.uber.org/zap" +) + +const ( + methodUpdate = "update" +) + +const ( + _ uint8 = iota + witnessValidators + witnessValidatorsAndCommittee +) + +// syncNeoFSContractPrm groups parameters of syncNeoFSContract. +type syncNeoFSContractPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + localAcc *wallet.Account + + // address of the NeoFS NNS contract deployed in the blockchain + nnsContract util.Uint160 + systemEmail string + + committee keys.PublicKeys + + // with localAcc signer only + simpleLocalActor *actor.Actor + // committee multi-sig signs, localAcc pays + committeeLocalActor *notary.Actor + + localNEF nef.File + localManifest manifest.Manifest + + // L2 domain name in domainContractAddresses TLD in the NNS + domainName string + + // if set, syncNeoFSContract attempts to deploy the contract when it's + // missing on the chain + tryDeploy bool + // 0: committee witness is not needed + // witnessValidators: committee 2/3n+1 with validatorsDeployAllowedContracts + // witnessValidatorsAndCommittee: witnessValidators + committee n/2+1 with allowed NNS contract calls + deployWitness uint8 + // contracts that are allowed to be called for the validators-witnessed deployment + validatorsDeployAllowedContracts []util.Uint160 + // additional option for unset tryDeploy to specify deployer of the contract + // designated globally. Has no effect if tryDeploy is set. + designatedDeployer *util.Uint160 + + // optional constructor of extra arguments to be passed into method deploying + // the contract. If returns both nil, no data is passed (noExtraDeployArgs can + // be used). + // + // Ignored if tryDeploy is unset. + buildExtraDeployArgs func() ([]any, error) + + // address of the Proxy contract deployed in the blockchain. The contract + // pays for update transactions. + proxyContract util.Uint160 + // set when syncNeoFSContractPrm relates to Proxy contract. In this case + // proxyContract field is unused because address is dynamically resolved within + // syncNeoFSContract. + isProxy bool +} + +// syncNeoFSContract behaves similar to updateNNSContract but also attempts to +// deploy the contract if it is missing on the chain and tryDeploy flag is set. +// If committeeDeployRequired is set, the contract is deployed on behalf of the +// committee with NNS custom contract scope. +// +// Returns address of the on-chain contract synchronized with the record of the +// NNS domain with parameterized name. +func syncNeoFSContract(ctx context.Context, prm syncNeoFSContractPrm) (util.Uint160, error) { + bLocalNEF, err := prm.localNEF.Bytes() + if err != nil { + // not really expected + return util.Uint160{}, fmt.Errorf("encode local NEF of the contract into binary: %w", err) + } + + jLocalManifest, err := json.Marshal(prm.localManifest) + if err != nil { + // not really expected + return util.Uint160{}, fmt.Errorf("encode local manifest of the contract into JSON: %w", err) + } + + var proxyCommitteeActor *notary.Actor + + initProxyCommitteeActor := func(proxyContract util.Uint160) error { + var err error + proxyCommitteeActor, err = newProxyCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee, proxyContract) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee and paid by Proxy contract: %w", err) + } + return nil + } + + if !prm.isProxy { + // otherwise, we dynamically receive Proxy contract address below and construct + // proxyCommitteeActor after + err = initProxyCommitteeActor(prm.proxyContract) + if err != nil { + return util.Uint160{}, 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 contractDeployer interface { + Sender() util.Uint160 + } + var managementContract *management.Contract + if prm.deployWitness > 0 { + if prm.deployWitness != witnessValidators && prm.deployWitness != witnessValidatorsAndCommittee { + panic(fmt.Sprintf("unexpected deploy witness mode value %v", prm.deployWitness)) + } + + validatorsMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) + err := validatorsMultiSigAcc.ConvertMultisig(smartcontract.GetDefaultHonestNodeCount(len(prm.committee)), prm.committee) + if err != nil { + return util.Uint160{}, fmt.Errorf("compose validators multi-signature account: %w", err) + } + + signers := make([]actor.SignerAccount, 2, 3) + // payer + signers[0].Account = prm.localAcc + signers[0].Signer.Account = prm.localAcc.ScriptHash() + signers[0].Signer.Scopes = transaction.None + // validators + signers[1].Account = validatorsMultiSigAcc + signers[1].Signer.Account = validatorsMultiSigAcc.ScriptHash() + signers[1].Signer.Scopes = transaction.CustomContracts + signers[1].Signer.AllowedContracts = prm.validatorsDeployAllowedContracts + + if prm.deployWitness == witnessValidatorsAndCommittee { + committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) + err := committeeMultiSigAcc.ConvertMultisig(smartcontract.GetMajorityHonestNodeCount(len(prm.committee)), prm.committee) + if err != nil { + return util.Uint160{}, fmt.Errorf("compose committee multi-signature account: %w", err) + } + + if acc := committeeMultiSigAcc.ScriptHash(); acc.Equals(signers[1].Signer.Account) { + signers[1].Account = committeeMultiSigAcc + signers[1].Signer.Account = acc + signers[1].Signer.Scopes = transaction.CustomContracts + signers[1].Signer.AllowedContracts = append(prm.validatorsDeployAllowedContracts, prm.nnsContract) + } else { + // prevent 'transaction signers should be unique' error + signers = append(signers, actor.SignerAccount{ + Signer: transaction.Signer{ + Account: committeeMultiSigAcc.ScriptHash(), + Scopes: transaction.CustomContracts, + AllowedContracts: []util.Uint160{prm.nnsContract}, + }, + Account: committeeMultiSigAcc, + }) + } + } + + deployCommitteeActor, err := notary.NewActor(prm.blockchain, signers, prm.localAcc) + if err != nil { + return util.Uint160{}, fmt.Errorf("create Notary service client sending deploy transactions to be signed by the committee: %w", err) + } + + managementContract = management.New(deployCommitteeActor) + contractDeployer = deployCommitteeActor + } else { + managementContract = management.New(prm.simpleLocalActor) + contractDeployer = prm.simpleLocalActor + } + + var alreadyUpdated bool + domainNameForAddress := prm.domainName + "." + domainContractAddresses + l := prm.logger.With(zap.String("contract", prm.localManifest.Name), zap.String("domain", domainNameForAddress)) + updateTxModifier := neoFSRuntimeTransactionModifier(prm.monitor.currentHeight) + deployTxMonitor := newTransactionGroupMonitor(prm.simpleLocalActor) + updateTxMonitor := newTransactionGroupMonitor(prm.simpleLocalActor) + setContractRecordPrm := setNeoFSContractDomainRecordPrm{ + logger: l, + setRecordTxMonitor: newTransactionGroupMonitor(prm.simpleLocalActor), + registerTLDTxMonitor: newTransactionGroupMonitor(prm.simpleLocalActor), + nnsContract: prm.nnsContract, + systemEmail: prm.systemEmail, + localActor: prm.simpleLocalActor, + committeeActor: prm.committeeLocalActor, + domain: domainNameForAddress, + record: "", // set in for loop + } + + for ; ; err = prm.monitor.waitForNextBlock(ctx) { + if err != nil { + return util.Uint160{}, fmt.Errorf("wait for the contract synchronization: %w", err) + } + + l.Info("reading on-chain state of the contract by NNS domain name...") + + var missingDomainRecord bool + + onChainState, err := readContractOnChainStateByDomainName(prm.blockchain, prm.nnsContract, domainNameForAddress) + if err != nil { + if errors.Is(err, neorpc.ErrUnknownContract) { + l.Error("contract is recorded in the NNS but not found on the chain, will wait for a background fix") + continue + } + + missingDomainRecord = errors.Is(err, errMissingDomain) || errors.Is(err, errMissingDomainRecord) + if !missingDomainRecord { + if errors.Is(err, errInvalidContractDomainRecord) { + l.Error("contract's domain record is invalid/unsupported, will wait for a background fix", zap.Error(err)) + } else { + l.Error("failed to read on-chain state of the contract record by NNS domain name, will try again later", zap.Error(err)) + } + continue + } + + var deployerAcc util.Uint160 + if prm.tryDeploy { + deployerAcc = contractDeployer.Sender() + } else { + if prm.designatedDeployer == nil { + // contract address is pre-calculated only when deployer is designated globally + // to prevent domain record corruption. + l.Info("domain record for the contract is missing, will try again later") + continue + } + deployerAcc = *prm.designatedDeployer + } + + l.Info("domain record for the contract is missing, trying by pre-calculated address...") + + preCalculatedAddr := state.CreateContractHash(deployerAcc, prm.localNEF.Checksum, prm.localManifest.Name) + + onChainState, err = prm.blockchain.GetContractStateByHash(preCalculatedAddr) + if err != nil { + if !errors.Is(err, neorpc.ErrUnknownContract) { + l.Error("failed to read on-chain state of the contract by pre-calculated address, will try again later", + zap.Stringer("address", preCalculatedAddr), zap.Stringer("deployer", deployerAcc), + zap.Error(err)) + continue + } + + onChainState = nil // for condition below, GetContractStateByHash may return empty + } + } + + if onChainState == nil { + // according to instructions above, we get here when contract is missing on the chain + if !prm.tryDeploy { + l.Info("contract is missing on the chain but attempts to deploy are disabled, will wait for background deployment") + continue + } + + l.Info("contract is missing on the chain, deployment needed") + + if deployTxMonitor.isPending() { + l.Info("previously sent transaction deploying the contract is still pending, will wait for the outcome") + continue + } + + extraDeployArgs, err := prm.buildExtraDeployArgs() + if err != nil { + l.Info("failed to prepare extra deployment arguments, will try again later", zap.Error(err)) + continue + } + + // just to definitely avoid mutation + nefCp := prm.localNEF + manifestCp := prm.localManifest + + if prm.deployWitness > 0 { + l.Info("contract requires committee witness for deployment, sending Notary request...") + + mainTxID, fallbackTxID, vub, err := prm.committeeLocalActor.Notarize(managementContract.DeployTransaction(&nefCp, &manifestCp, extraDeployArgs)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + l.Info("insufficient Notary balance to deploy the contract, will try again later") + } else { + l.Error("failed to send Notary request deploying the contract, will try again later", zap.Error(err)) + } + continue + } + + l.Info("Notary request deploying the contract has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + deployTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + + continue + } + + l.Info("contract does not require committee witness for deployment, sending simple transaction...") + + txID, vub, err := managementContract.Deploy(&nefCp, &manifestCp, extraDeployArgs) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + l.Info("not enough GAS to deploy the contract, will try again later") + } else { + l.Error("failed to send transaction deploying the contract, will try again later", zap.Error(err)) + } + continue + } + + l.Info("transaction deploying the contract has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub), + ) + + deployTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + + continue + } + + if alreadyUpdated { + if !missingDomainRecord { + return onChainState.Hash, nil + } + } else { + if prm.isProxy && proxyCommitteeActor == nil { + err = initProxyCommitteeActor(onChainState.Hash) + if err != nil { + return util.Uint160{}, err + } + } + + tx, err := proxyCommitteeActor.MakeTunedCall(onChainState.Hash, methodUpdate, nil, updateTxModifier, + bLocalNEF, jLocalManifest, nil) + if err != nil { + if isErrContractAlreadyUpdated(err) { + l.Info("the contract is unchanged or has already been updated") + if !missingDomainRecord { + return onChainState.Hash, nil + } + alreadyUpdated = true + } else { + l.Error("failed to make transaction updating the contract, will try again later", zap.Error(err)) + } + continue + } + + if updateTxMonitor.isPending() { + l.Info("previously sent Notary request updating the contract is still pending, will wait for the outcome") + continue + } + + l.Info("sending new Notary request updating the contract...") + + mainTxID, fallbackTxID, vub, err := proxyCommitteeActor.Notarize(tx, nil) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + l.Info("insufficient Notary balance to update the contract, will try again later") + } else { + l.Error("failed to send Notary request updating the contract, will try again later", zap.Error(err)) + } + continue + } + + l.Info("Notary request updating the contract has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + updateTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + + continue + } + + setContractRecordPrm.record = onChainState.Hash.StringLE() + + setNeoFSContractDomainRecord(ctx, setContractRecordPrm) + } +} diff --git a/deploy/deploy.go b/deploy/deploy.go new file mode 100644 index 00000000..7d0b5c8f --- /dev/null +++ b/deploy/deploy.go @@ -0,0 +1,647 @@ +package deploy + +import ( + "context" + "errors" + "fmt" + "math" + "math/big" + "sort" + "strconv" + + "github.com/nspcc-dev/neo-go/pkg/core/block" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "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/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "go.uber.org/zap" +) + +// 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 (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. + GetCommittee() (keys.PublicKeys, error) + + // GetContractStateByID returns network state of the smart contract by its ID. + // GetContractStateByID returns error with 'Unknown contract' substring if + // requested contract is missing. + GetContractStateByID(id int32) (*state.Contract, error) + + // GetContractStateByHash is similar to GetContractStateByID but accepts address. + // GetContractStateByHash may return non-nil state.Contract along with an error. + GetContractStateByHash(util.Uint160) (*state.Contract, error) + + // SubscribeToNewBlocks opens stream of the new blocks persisted in the + // blockchain and returns channel to read them. The channel is closed only when + // connection to the blockchain is lost and there will be no more events. Caller + // subscribes once, regularly reads events from the channel and is resistant to + // event replay. + SubscribeToNewBlocks() (<-chan *block.Block, error) + + // SubscribeToNotaryRequests opens stream of the notary request events from the + // blockchain and returns channel to read them. The channel is closed only when + // connection to the blockchain is lost and there will be no more events. Caller + // subscribes once, regularly reads events from the channel and is resistant to + // event replay. + SubscribeToNotaryRequests() (<-chan *result.NotaryRequestEvent, error) +} + +// CommonDeployPrm groups common deployment parameters of the smart contract. +type CommonDeployPrm struct { + NEF nef.File + Manifest manifest.Manifest +} + +// NNSPrm groups deployment parameters of the NeoFS NNS contract. +type NNSPrm struct { + Common CommonDeployPrm + SystemEmail string +} + +// AlphabetContractPrm groups deployment parameters of the NeoFS Alphabet contract. +type AlphabetContractPrm struct { + Common CommonDeployPrm +} + +// AuditContractPrm groups deployment parameters of the NeoFS Audit contract. +type AuditContractPrm struct { + Common CommonDeployPrm +} + +// BalanceContractPrm groups deployment parameters of the NeoFS Balance contract. +type BalanceContractPrm struct { + Common CommonDeployPrm +} + +// ContainerContractPrm groups deployment parameters of the Container contract. +type ContainerContractPrm struct { + Common CommonDeployPrm +} + +// NeoFSIDContractPrm groups deployment parameters of the NeoFS ID contract. +type NeoFSIDContractPrm struct { + Common CommonDeployPrm +} + +// NetmapContractPrm groups deployment parameters of the Netmap contract. +type NetmapContractPrm struct { + Common CommonDeployPrm + Config NetworkConfiguration +} + +// ProxyContractPrm groups deployment parameters of the NeoFS Proxy contract. +type ProxyContractPrm struct { + Common CommonDeployPrm +} + +// ReputationContractPrm groups deployment parameters of the NeoFS Reputation contract. +type ReputationContractPrm struct { + Common CommonDeployPrm +} + +// Glagolitsa is using for alphabet contract deploy routine. +type Glagolitsa interface { + Size() int + LetterByIndex(ind int) string +} + +// Prm groups all parameters of the NeoFS Sidechain deployment procedure. +type Prm struct { + // Writes progress into the log. + Logger *zap.Logger + + // Particular Neo blockchain instance to be used as NeoFS Sidechain. + Blockchain Blockchain + + // Local process account used for transaction signing (must be unlocked). + LocalAccount *wallet.Account + + // Validator multi-sig account to spread initial GAS to network + // participants (must be unlocked). + ValidatorMultiSigAccount *wallet.Account + + NNS NNSPrm + + AlphabetContract AlphabetContractPrm + AuditContract AuditContractPrm + BalanceContract BalanceContractPrm + ContainerContract ContainerContractPrm + NeoFSIDContract NeoFSIDContractPrm + NetmapContract NetmapContractPrm + ProxyContract ProxyContractPrm + ReputationContract ReputationContractPrm + + Glagolitsa Glagolitsa +} + +// Deploy initializes Neo network represented by given Prm.Blockchain as NeoFS +// Sidechain and makes it full-featured for NeoFS storage system operation. +// +// Deploy aborts only by context or when a fatal error occurs. Deployment +// progress is logged in detail. It is expected that some situations can be +// changed/fixed on the chain from the outside, so Deploy adapts flexibly and +// does not stop at the moment. +// +// Deployment process is detailed in NeoFS docs. Summary of stages: +// 1. NNS contract deployment +// 2. launch of a notary service for the committee +// 3. initial GAS distribution between committee members +// 4. Alphabet initialization (incl. registration as candidates to validators) +// 5. deployment/update of the NeoFS system contracts +// 6. distribution of all available NEO between the Alphabet contracts +// +// 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) + } + + sort.Sort(committee) + + // determine a leader + localPrivateKey := prm.LocalAccount.PrivateKey() + localPublicKey := localPrivateKey.PublicKey() + localAccCommitteeIndex := -1 + + for i := range committee { + if committee[i].Equal(localPublicKey) { + localAccCommitteeIndex = i + break + } + } + + if localAccCommitteeIndex < 0 { + return errors.New("local account does not belong to any Neo committee member") + } + + simpleLocalActor, err := actor.NewSimple(prm.Blockchain, prm.LocalAccount) + if err != nil { + return fmt.Errorf("init transaction sender from single local account: %w", err) + } + + committeeLocalActor, err := newCommitteeNotaryActor(prm.Blockchain, prm.LocalAccount, committee) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %w", err) + } + + chNewBlock := make(chan struct{}, 1) + + monitor, err := newBlockchainMonitor(prm.Logger, prm.Blockchain, chNewBlock) + if err != nil { + return fmt.Errorf("init blockchain monitor: %w", err) + } + + deployNNSPrm := deployNNSContractPrm{ + 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, + tryDeploy: localAccCommitteeIndex == 0, // see below + } + + // if local node is the first committee member (Az) => deploy NNS contract, + // otherwise just wait. This will avoid duplication of contracts. This also + // makes the procedure more centralized, however, in practice, at the start of + // the network, all members are expected to be healthy and active. + + prm.Logger.Info("initializing NNS contract on the chain...") + + nnsOnChainAddress, err := initNNSContract(ctx, deployNNSPrm) + if err != nil { + return fmt.Errorf("init NNS contract on the chain: %w", err) + } + + prm.Logger.Info("NNS contract successfully initialized on the chain", zap.Stringer("address", nnsOnChainAddress)) + + prm.Logger.Info("enable Notary service for the committee...") + + err = enableNotary(ctx, enableNotaryPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + nnsOnChainAddress: nnsOnChainAddress, + systemEmail: prm.NNS.SystemEmail, + committee: committee, + localAcc: prm.LocalAccount, + localAccCommitteeIndex: localAccCommitteeIndex, + }) + if err != nil { + return fmt.Errorf("enable Notary service for the committee: %w", err) + } + + prm.Logger.Info("Notary service successfully enabled for the committee") + + go autoReplenishNotaryBalance(ctx, prm.Logger, prm.Blockchain, prm.LocalAccount, chNewBlock) + + err = listenCommitteeNotaryRequests(ctx, listenCommitteeNotaryRequestsPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + localAcc: prm.LocalAccount, + committee: committee, + validatorMultiSigAcc: prm.ValidatorMultiSigAccount, + }) + if err != nil { + return fmt.Errorf("start listener of committee notary requests: %w", err) + } + + prm.Logger.Info("making initial transfer of funds to the committee...") + + err = makeInitialTransferToCommittee(ctx, makeInitialGASTransferToCommitteePrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + committee: committee, + localAcc: prm.LocalAccount, + validatorMultiSigAcc: prm.ValidatorMultiSigAccount, + tryTransfer: localAccCommitteeIndex == 0, + }) + if err != nil { + return fmt.Errorf("initial transfer funds to the committee: %w", err) + } + + prm.Logger.Info("initial transfer to the committee successfully done") + + prm.Logger.Info("initializing NeoFS Alphabet...") + + err = initAlphabet(ctx, initAlphabetPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + committee: committee, + localAcc: prm.LocalAccount, + }) + if err != nil { + return fmt.Errorf("init NeoFS Alphabet: %w", err) + } + + prm.Logger.Info("NeoFS Alphabet successfully initialized") + + syncPrm := syncNeoFSContractPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + localAcc: prm.LocalAccount, + nnsContract: nnsOnChainAddress, + systemEmail: prm.NNS.SystemEmail, + committee: committee, + simpleLocalActor: simpleLocalActor, + committeeLocalActor: committeeLocalActor, + } + + localAccLeads := localAccCommitteeIndex == 0 + + // we attempt to deploy contracts (except Alphabet ones) by single committee + // member (1st for simplicity) to reduce the likelihood of contract duplication + // in the chain and better predictability of the final address (the address is a + // function from the sender of the deploying transaction). While this approach + // is centralized, we still expect any node incl. 1st one to be "healthy". + // Updates are done concurrently. + syncPrm.tryDeploy = localAccLeads + + var notaryDisabledExtraUpdateArg bool + + // Deploy NeoFS contracts in strict order. Contracts dependent on others come + // after. + + // 1. Proxy + // + // It's required for Notary service to work, and also pays for subsequent + // contract updates. + syncPrm.localNEF = prm.ProxyContract.Common.NEF + syncPrm.localManifest = prm.ProxyContract.Common.Manifest + syncPrm.domainName = domainProxy + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + syncPrm.isProxy = true + + prm.Logger.Info("synchronizing Proxy contract with the chain...") + + proxyContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Proxy contract with the chain: %w", err) + } + + prm.Logger.Info("Proxy contract successfully synchronized", zap.Stringer("address", proxyContractAddress)) + + // use on-chain address of the Proxy contract to update all others + syncPrm.isProxy = false + syncPrm.proxyContract = proxyContractAddress + + prm.Logger.Info("replenishing the the Proxy contract's balance...") + + err = transferGASToProxy(ctx, transferGASToProxyPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + proxyContract: proxyContractAddress, + committee: committee, + localAcc: prm.LocalAccount, + tryTransfer: localAccLeads, + }) + if err != nil { + return fmt.Errorf("replenish balance of the Proxy contract: %w", err) + } + + prm.Logger.Info("Proxy balance successfully replenished") + + prm.Logger.Info("initializing vote for NeoFS Alphabet members to role of validators...") + + err = initVoteForAlphabet(ctx, initVoteForAlphabetPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + committee: committee, + localAcc: prm.LocalAccount, + proxyContract: proxyContractAddress, + }) + if err != nil { + return fmt.Errorf("init vote for NeoFS Alphabet members to role of validators: %w", err) + } + + prm.Logger.Info("vote for NeoFS Alphabet to role of validators successfully initialized") + + // NNS (update) + // + // Special contract which is always deployed first, but its update depends on + // Proxy contract. + 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, + proxyContract: proxyContractAddress, + }) + if err != nil { + return fmt.Errorf("update NNS contract on the chain: %w", err) + } + + prm.Logger.Info("on-chain NNS contract successfully updated") + + // 2. Audit + syncPrm.localNEF = prm.AuditContract.Common.NEF + syncPrm.localManifest = prm.AuditContract.Common.Manifest + syncPrm.domainName = domainAudit + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + + prm.Logger.Info("synchronizing Audit contract with the chain...") + + auditContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Audit contract with the chain: %w", err) + } + + prm.Logger.Info("Audit contract successfully synchronized", zap.Stringer("address", auditContractAddress)) + + // 3. Netmap + // + // Required for: + // - Balance + // - Container + netConfig := []any{ + []byte(MaxObjectSizeConfig), encodeUintConfig(prm.NetmapContract.Config.MaxObjectSize), + []byte(BasicIncomeRateConfig), encodeUintConfig(prm.NetmapContract.Config.StoragePrice), + []byte(AuditFeeConfig), encodeUintConfig(prm.NetmapContract.Config.AuditFee), + []byte(EpochDurationConfig), encodeUintConfig(prm.NetmapContract.Config.EpochDuration), + []byte(ContainerFeeConfig), encodeUintConfig(prm.NetmapContract.Config.ContainerFee), + []byte(ContainerAliasFeeConfig), encodeUintConfig(prm.NetmapContract.Config.ContainerAliasFee), + []byte(EigenTrustIterationsConfig), encodeUintConfig(prm.NetmapContract.Config.EigenTrustIterations), + []byte(EigenTrustAlphaConfig), encodeFloatConfig(prm.NetmapContract.Config.EigenTrustAlpha), + []byte(InnerRingCandidateFeeConfig), encodeUintConfig(prm.NetmapContract.Config.IRCandidateFee), + []byte(WithdrawFeeConfig), encodeUintConfig(prm.NetmapContract.Config.WithdrawalFee), + []byte(HomomorphicHashingDisabledKey), encodeBoolConfig(prm.NetmapContract.Config.HomomorphicHashingDisabled), + []byte(MaintenanceModeAllowedConfig), encodeBoolConfig(prm.NetmapContract.Config.MaintenanceModeAllowed), + } + + for i := range prm.NetmapContract.Config.Raw { + netConfig = append(netConfig, []byte(prm.NetmapContract.Config.Raw[i].Name), prm.NetmapContract.Config.Raw[i].Value) + } + + syncPrm.localNEF = prm.NetmapContract.Common.NEF + syncPrm.localManifest = prm.NetmapContract.Common.Manifest + syncPrm.domainName = domainNetmap + syncPrm.buildExtraDeployArgs = func() ([]any, error) { + return []any{ + notaryDisabledExtraUpdateArg, + util.Uint160{}, // Balance contract address legacy + util.Uint160{}, // Container contract address legacy + []any(nil), // keys, currently unused + netConfig, + }, nil + } + + prm.Logger.Info("synchronizing Netmap contract with the chain...") + + netmapContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Netmap contract with the chain: %w", err) + } + + prm.Logger.Info("Netmap contract successfully synchronized", zap.Stringer("address", netmapContractAddress)) + + // 4. Balance + syncPrm.localNEF = prm.BalanceContract.Common.NEF + syncPrm.localManifest = prm.BalanceContract.Common.Manifest + syncPrm.domainName = domainBalance + syncPrm.deployWitness = witnessValidators + syncPrm.validatorsDeployAllowedContracts = []util.Uint160{netmapContractAddress} + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + + prm.Logger.Info("synchronizing Balance contract with the chain...") + + balanceContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Balance contract with the chain: %w", err) + } + + prm.Logger.Info("Balance contract successfully synchronized", zap.Stringer("address", balanceContractAddress)) + + syncPrm.deployWitness = 0 + syncPrm.validatorsDeployAllowedContracts = nil + + // 5. Reputation + syncPrm.localNEF = prm.ReputationContract.Common.NEF + syncPrm.localManifest = prm.ReputationContract.Common.Manifest + syncPrm.domainName = domainReputation + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + + prm.Logger.Info("synchronizing Reputation contract with the chain...") + + reputationContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Reputation contract with the chain: %w", err) + } + + prm.Logger.Info("Reputation contract successfully synchronized", zap.Stringer("address", reputationContractAddress)) + + // 6. NeoFSID + syncPrm.localNEF = prm.NeoFSIDContract.Common.NEF + syncPrm.localManifest = prm.NeoFSIDContract.Common.Manifest + syncPrm.domainName = domainNeoFSID + syncPrm.buildExtraDeployArgs = noExtraDeployArgs + + prm.Logger.Info("synchronizing NeoFSID contract with the chain...") + + neoFSIDContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync NeoFSID contract with the chain: %w", err) + } + + prm.Logger.Info("NeoFSID contract successfully synchronized", zap.Stringer("address", neoFSIDContractAddress)) + + // 7. Container + syncPrm.localNEF = prm.ContainerContract.Common.NEF + syncPrm.localManifest = prm.ContainerContract.Common.Manifest + syncPrm.domainName = domainContainer + syncPrm.deployWitness = witnessValidatorsAndCommittee + syncPrm.validatorsDeployAllowedContracts = []util.Uint160{netmapContractAddress} + syncPrm.buildExtraDeployArgs = func() ([]any, error) { + return []any{ + notaryDisabledExtraUpdateArg, + netmapContractAddress, + balanceContractAddress, + neoFSIDContractAddress, + nnsOnChainAddress, + domainContainers, + }, nil + } + + prm.Logger.Info("synchronizing Container contract with the chain...") + + containerContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Container contract with the chain: %w", err) + } + + prm.Logger.Info("Container contract successfully synchronized", zap.Stringer("address", containerContractAddress)) + + syncPrm.deployWitness = 0 + syncPrm.validatorsDeployAllowedContracts = nil + + // 8. Alphabet + syncPrm.localNEF = prm.AlphabetContract.Common.NEF + syncPrm.localManifest = prm.AlphabetContract.Common.Manifest + + var alphabetContracts []util.Uint160 + + for ind := 0; ind < len(committee) && ind < prm.Glagolitsa.Size(); ind++ { + syncPrm.tryDeploy = ind == localAccCommitteeIndex // each member deploys its own Alphabet contract + if !syncPrm.tryDeploy { + temp := committee[ind].GetScriptHash() + syncPrm.designatedDeployer = &temp + } + syncPrm.domainName = calculateAlphabetContractAddressDomain(ind) + syncPrm.buildExtraDeployArgs = func() ([]any, error) { + return []any{ + notaryDisabledExtraUpdateArg, + netmapContractAddress, + proxyContractAddress, + prm.Glagolitsa.LetterByIndex(ind), + ind, + len(committee), + }, nil + } + + prm.Logger.Info("synchronizing Alphabet contract with the chain...", zap.Int("index", ind)) + + alphabetContractAddress, err := syncNeoFSContract(ctx, syncPrm) + if err != nil { + return fmt.Errorf("sync Alphabet contract #%d with the chain: %w", ind, err) + } + + prm.Logger.Info("Alphabet contract successfully synchronized", + zap.Int("index", ind), zap.Stringer("address", alphabetContractAddress)) + + alphabetContracts = append(alphabetContracts, alphabetContractAddress) + } + + // note: this instruction has no effect anymore, but saves future code added + // below from potential problems + syncPrm.designatedDeployer = nil + + prm.Logger.Info("distributing NEO to the Alphabet contracts...") + + err = distributeNEOToAlphabetContracts(ctx, distributeNEOToAlphabetContractsPrm{ + logger: prm.Logger, + blockchain: prm.Blockchain, + monitor: monitor, + proxyContract: proxyContractAddress, + committee: committee, + localAcc: prm.LocalAccount, + alphabetContracts: alphabetContracts, + }) + if err != nil { + return fmt.Errorf("distribute NEO to the Alphabet contracts: %w", err) + } + + prm.Logger.Info("NEO distribution to the Alphabet contracts successfully completed") + + return nil +} + +func noExtraDeployArgs() ([]any, error) { return nil, nil } + +func encodeUintConfig(v uint64) []byte { + return stackitem.NewBigInteger(new(big.Int).SetUint64(v)).Bytes() +} + +func encodeFloatConfig(v float64) []byte { + return []byte(strconv.FormatFloat(v, 'f', -1, 64)) +} + +func encodeBoolConfig(v bool) []byte { + return stackitem.NewBool(v).Bytes() +} + +// returns actor.TransactionCheckerModifier which checks that invocation +// finished with 'HALT' state and, if so, sets transaction's nonce and +// ValidUntilBlock to 100*N and 100*(N+1) correspondingly, where +// 100*N <= current height < 100*(N+1). +func neoFSRuntimeTransactionModifier(getBlockchainHeight func() uint32) actor.TransactionCheckerModifier { + return func(r *result.Invoke, tx *transaction.Transaction) error { + err := actor.DefaultCheckerModifier(r, tx) + if err != nil { + return err + } + + curHeight := getBlockchainHeight() + const span = 100 + n := curHeight / span + + tx.Nonce = n * span + + if math.MaxUint32-span > tx.Nonce { + tx.ValidUntilBlock = tx.Nonce + span + } else { + tx.ValidUntilBlock = math.MaxUint32 + } + + return nil + } +} diff --git a/deploy/funds.go b/deploy/funds.go new file mode 100644 index 00000000..e5ec2044 --- /dev/null +++ b/deploy/funds.go @@ -0,0 +1,478 @@ +package deploy + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "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/rpcclient/gas" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "go.uber.org/zap" +) + +const ( + initialAlphabetGASAmount = 300 + // lower threshold of GAS remaining on validator multi-sig account. It is needed + // to pay fees for transfer transaction(s). The value is big enough for + // transfer, and not very big to leave no tail on the account. + validatorLowerGASThreshold = 10 + // share of GAS on the committee multi-sig account to be transferred to the + // Proxy contract (in %). + initialProxyGASPercent = 90 +) + +// groups parameters of makeInitialGASTransferToCommittee. +type makeInitialGASTransferToCommitteePrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + committee keys.PublicKeys + + localAcc *wallet.Account + validatorMultiSigAcc *wallet.Account + + tryTransfer bool +} + +// makes initial transfer of funds to the committee for deployment procedure. In +// the initial state of the Blockchain, all funds are on the validator multisig +// account. Transfers: +// - 300GAS to each account of the Alphabet members +// - all other GAS to the committee multisig account +// - all NEO to the committee multisig account +func makeInitialTransferToCommittee(ctx context.Context, prm makeInitialGASTransferToCommitteePrm) error { + validatorMultiSigAccAddress := prm.validatorMultiSigAcc.ScriptHash() + + validatorMultiSigNotaryActor, err := notary.NewActor(prm.blockchain, []actor.SignerAccount{ + { + Signer: transaction.Signer{ + Account: prm.localAcc.ScriptHash(), + Scopes: transaction.None, + }, + Account: prm.localAcc, + }, + { + Signer: transaction.Signer{ + Account: validatorMultiSigAccAddress, + Scopes: transaction.CalledByEntry, + }, + Account: prm.validatorMultiSigAcc, + }, + }, prm.localAcc) + if err != nil { + return fmt.Errorf("init notary actor for validator multi-sig account: %w", err) + } + + committeeMajorityMultiSigScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(prm.committee) + if err != nil { + return fmt.Errorf("compose majority committee verification script: %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() + + lowerGASThreshold := big.NewInt(validatorLowerGASThreshold * native.GASFactor) + neoContract := neo.New(validatorMultiSigNotaryActor) + gasContract := gas.New(validatorMultiSigNotaryActor) + committeeMultiSigAccAddress := hash.Hash160(committeeMajorityMultiSigScript) + committeeDiffersValidator := !validatorMultiSigAccAddress.Equals(committeeMultiSigAccAddress) + transferTxMonitor := newTransactionGroupMonitor(validatorMultiSigNotaryActor) + +upperLoop: + for ; ; err = prm.monitor.waitForNextBlock(ctx) { + if err != nil { + return fmt.Errorf("wait for distribution of initial funds: %w", err) + } + + remGAS, err := gasContract.BalanceOf(validatorMultiSigAccAddress) + if err != nil { + prm.logger.Error("failed to get GAS balance on validator multi-sig account, will try again later", + zap.Error(err)) + continue + } + + remNEO, err := neoContract.BalanceOf(validatorMultiSigAccAddress) + if err != nil { + prm.logger.Error("failed to get NEO balance on validator multi-sig account, will try again later", + zap.Error(err)) + continue + } + + prm.logger.Info("got current balance of the validator multi-sig account, distributing between the committee...", + zap.Stringer("NEO", remNEO), zap.Stringer("GAS", remGAS)) + + if remGAS.Cmp(lowerGASThreshold) <= 0 { + prm.logger.Info("residual GAS on validator multi-sig account does not exceed the lower threshold, initial transfer has already succeeded, skip", + zap.Stringer("rem", remGAS)) + return nil + } + + // prevent transfer of all available GAS in order to pay fees + remGAS.Sub(remGAS, lowerGASThreshold) + gasTransfers := make([]nep17.TransferParameters, 0, len(prm.committee)+1) // + to committee multi-sig + + for i := range prm.committee { + memberBalance, err := gasContract.BalanceOf(prm.committee[i].GetScriptHash()) + if err != nil { + prm.logger.Info("failed to get GAS balance of the committee member, will try again later", + zap.Stringer("member", prm.committee[i]), zap.Error(err)) + continue upperLoop + } + + toTransfer := big.NewInt(initialAlphabetGASAmount * native.GASFactor) + needAtLeast := new(big.Int).Div(toTransfer, big.NewInt(2)) + + if memberBalance.Cmp(needAtLeast) >= 0 { + prm.logger.Info("enough GAS on the committee member's account, skip transfer", + zap.Stringer("member", prm.committee[i]), zap.Stringer("balance", memberBalance), + zap.Stringer("need at least", needAtLeast)) + continue + } + + prm.logger.Info("not enough GAS on the committee member's account, need replenishment", + zap.Stringer("member", prm.committee[i]), zap.Stringer("balance", memberBalance), + zap.Stringer("need at least", needAtLeast)) + + if remGAS.Cmp(toTransfer) <= 0 { + toTransfer.Set(remGAS) + } + + gasTransfers = append(gasTransfers, nep17.TransferParameters{ + From: validatorMultiSigAccAddress, + To: prm.committee[i].GetScriptHash(), + Amount: toTransfer, + }) + + remGAS.Sub(remGAS, toTransfer) + if remGAS.Sign() <= 0 { + break + } + } + + if committeeDiffersValidator && remGAS.Sign() > 0 { + gasTransfers = append(gasTransfers, nep17.TransferParameters{ + From: validatorMultiSigAccAddress, + To: committeeMultiSigAccAddress, + Amount: remGAS, + }) + + prm.logger.Info("going to transfer all remaining GAS from validator multi-sig account to the committee one", + zap.Stringer("from", validatorMultiSigAccAddress), zap.Stringer("to", committeeMultiSigAccAddress), + zap.Stringer("amount", remGAS)) + } + + var script []byte + + if len(gasTransfers) > 0 { + tx, err := gasContract.MultiTransferUnsigned(gasTransfers) + if err != nil { + prm.logger.Error("failed to make transaction transferring GAS from validator multi-sig account to the committee, will try again later", + zap.Error(err)) + continue + } + + script = tx.Script + } + + if committeeDiffersValidator && remNEO.Sign() > 0 { + tx, err := neoContract.TransferUnsigned(validatorMultiSigAccAddress, committeeMultiSigAccAddress, remNEO, nil) + if err != nil { + prm.logger.Error("failed to transaction transferring NEO from validator multi-sig account to the committee one, will try again later", + zap.Error(err)) + continue + } + + script = append(script, tx.Script...) + + prm.logger.Info("going to transfer all remaining NEO from validator multi-sig account to the committee one", + zap.Stringer("from", validatorMultiSigAccAddress), zap.Stringer("to", committeeMultiSigAccAddress), + zap.Stringer("amount", remNEO)) + } + + if len(script) == 0 { + prm.logger.Info("nothing to transfer, skip") + return nil + } + + if !prm.tryTransfer { + prm.logger.Info("need transfer from validator multi-sig account but attempts are disabled, will wait for a leader") + continue + } + + if transferTxMonitor.isPending() { + prm.logger.Info("previously sent transaction transferring funds from validator multi-sig account to the committee is still pending, will wait for the outcome") + continue + } + + prm.logger.Info("sending new Notary request transferring funds from validator multi-sig account to the committee...") + + mainTxID, fallbackTxID, vub, err := validatorMultiSigNotaryActor.Notarize(validatorMultiSigNotaryActor.MakeRun(script)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to transfer funds from validator multi-sig account to the committee, will try again later") + } else { + prm.logger.Error("failed to send Notary request transferring funds from validator multi-sig account to the committee, will try again later", zap.Error(err)) + } + continue + } + + prm.logger.Info("Notary request transferring funds from validator multi-sig account to the committee has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + transferTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } +} + +// groups parameters of transferGASToProxy. +type transferGASToProxyPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + proxyContract util.Uint160 + + committee keys.PublicKeys + + localAcc *wallet.Account + + tryTransfer bool +} + +// transfers 90% of GAS from committee multi-sig account to the Proxy contract. +// No-op if Proxy contract already has GAS. +func transferGASToProxy(ctx context.Context, prm transferGASToProxyPrm) error { + var committeeMultiSigAccAddress util.Uint160 + + committeeActor, err := newCommitteeNotaryActorWithCustomCommitteeSigner(prm.blockchain, prm.localAcc, prm.committee, func(s *transaction.Signer) { + committeeMultiSigAccAddress = s.Account + s.Scopes = transaction.CalledByEntry + }) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee: %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() + + gasContract := gas.New(committeeActor) + transferTxMonitor := newTransactionGroupMonitor(committeeActor) + + for ; ; err = prm.monitor.waitForNextBlock(ctx) { + if err != nil { + return fmt.Errorf("wait for distribution of initial funds: %w", err) + } + + proxyBalance, err := gasContract.BalanceOf(prm.proxyContract) + if err != nil { + prm.logger.Error("failed to get GAS balance of the Proxy contract, will try again later", + zap.Error(err)) + continue + } + + if proxyBalance.Sign() > 0 { + prm.logger.Info("Proxy contract already has GAS, skip transfer") + return nil + } + + if !prm.tryTransfer { + prm.logger.Info("GAS balance of the Proxy contract is empty but attempts to transfer are disabled, will wait for a leader") + continue + } + + committeeBalance, err := gasContract.BalanceOf(committeeMultiSigAccAddress) + if err != nil { + prm.logger.Error("failed to get GAS balance of the committee multi-sig account, will try again later", + zap.Error(err)) + continue + } + + amount := new(big.Int).Mul(committeeBalance, big.NewInt(initialProxyGASPercent)) + amount.Div(amount, big.NewInt(100)) + if amount.Sign() <= 0 { + prm.logger.Info("nothing to transfer from the committee multi-sig account, skip") + return nil + } + + if transferTxMonitor.isPending() { + prm.logger.Info("previously sent transaction transferring funds from committee multi-sig account to the Proxy contract is still pending, will wait for the outcome") + continue + } + + prm.logger.Info("sending new Notary request transferring funds from committee multi-sig account to the Proxy contract...") + + mainTxID, fallbackTxID, vub, err := committeeActor.Notarize( + gasContract.TransferTransaction(committeeMultiSigAccAddress, prm.proxyContract, amount, nil)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to transfer funds from committee multi-sig account to the Proxy contract, will try again later") + } else { + prm.logger.Error("failed to send Notary request transferring funds from committee multi-sig account to the Proxy contract, will try again later", zap.Error(err)) + } + continue + } + + prm.logger.Info("Notary request transferring funds from committee multi-sig account to the Proxy contract has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + transferTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } +} + +// groups parameters of distributeNEOToAlphabetContracts. +type distributeNEOToAlphabetContractsPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + proxyContract util.Uint160 + + committee keys.PublicKeys + + localAcc *wallet.Account + + alphabetContracts []util.Uint160 +} + +// distributes all available NEO between NeoFS Alphabet members evenly. +func distributeNEOToAlphabetContracts(ctx context.Context, prm distributeNEOToAlphabetContractsPrm) 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() + + committeeActor, err := notary.NewActor(prm.blockchain, []actor.SignerAccount{ + { + Signer: transaction.Signer{ + Account: prm.proxyContract, + Scopes: transaction.None, + }, + Account: notary.FakeContractAccount(prm.proxyContract), + }, + { + Signer: transaction.Signer{ + Account: committeeMultiSigAccID, + Scopes: transaction.CalledByEntry, + }, + Account: committeeMultiSigAcc, + }, + }, prm.localAcc) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee and paid by Proxy contract: %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() + + neoContract := neo.NewReader(committeeActor) + scriptBuilder := smartcontract.NewBuilder() + transfer := func(to util.Uint160, amount uint64) { + scriptBuilder.InvokeWithAssert(neo.Hash, "transfer", committeeMultiSigAccID, to, amount, nil) + } + txMonitor := newTransactionGroupMonitor(committeeActor) + + for ; ; err = prm.monitor.waitForNextBlock(ctx) { + if err != nil { + return fmt.Errorf("wait for distribution of NEO between Alphabet contracts: %w", err) + } + + bal, err := neoContract.BalanceOf(committeeMultiSigAccID) + if err != nil { + prm.logger.Error("failed to get NEO balance of the committee multi-sig account", zap.Error(err)) + continue + } + + if bal.Sign() <= 0 { + prm.logger.Error("no NEO on the committee multi-sig account, nothing to transfer, skip") + return nil + } + + if !bal.IsUint64() { + // should never happen since NEO is <=100KK according to https://docs.neo.org/docs/en-us/basic/concept/blockchain/token_model.html + // see also https://github.com/nspcc-dev/neo-go/issues/3268 + return fmt.Errorf("NEO balance exceeds uint64: %v", bal) + } + + prm.logger.Info("have available NEO on the committee multi-sig account, going to transfer to the Alphabet contracts", + zap.Stringer("balance", bal)) + + scriptBuilder.Reset() + + divideFundsEvenly(bal.Uint64(), len(prm.alphabetContracts), func(i int, amount uint64) { + prm.logger.Info("going to transfer NEO from the committee multi-sig account to the Alphabet contract", + zap.Stringer("contract", prm.alphabetContracts[i]), zap.Uint64("amount", amount)) + transfer(prm.alphabetContracts[i], amount) + }) + + script, err := scriptBuilder.Script() + if err != nil { + prm.logger.Info("failed to build script transferring Neo from committee multi-sig account to the Alphabet contracts, will try again later", + zap.Error(err)) + continue + } + + mainTxID, fallbackTxID, vub, err := committeeActor.Notarize(committeeActor.MakeRun(script)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to transfer Neo from committee multi-sig account to the Alphabet contracts, skip") + } else { + prm.logger.Error("failed to send new Notary request transferring Neo from committee multi-sig account to the Alphabet contracts, skip", zap.Error(err)) + } + continue + } + + prm.logger.Info("Notary request transferring Neo from committee multi-sig account to the Alphabet contracts has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + txMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } +} + +func divideFundsEvenly(fullAmount uint64, n int, f func(ind int, amount uint64)) { + quot := fullAmount / uint64(n) + rem := fullAmount % uint64(n) + + for i := 0; i < n; i++ { + amount := quot + if rem > 0 { + amount++ + rem-- + } else if amount == 0 { + return + } + + f(i, amount) + } +} diff --git a/deploy/netmap.go b/deploy/netmap.go new file mode 100644 index 00000000..10ce4c60 --- /dev/null +++ b/deploy/netmap.go @@ -0,0 +1,56 @@ +package deploy + +const ( + MaxObjectSizeConfig = "MaxObjectSize" + BasicIncomeRateConfig = "BasicIncomeRate" + AuditFeeConfig = "AuditFee" + EpochDurationConfig = "EpochDuration" + ContainerFeeConfig = "ContainerFee" + ContainerAliasFeeConfig = "ContainerAliasFee" + EigenTrustIterationsConfig = "EigenTrustIterations" + EigenTrustAlphaConfig = "EigenTrustAlpha" + InnerRingCandidateFeeConfig = "InnerRingCandidateFee" + WithdrawFeeConfig = "WithdrawFee" + HomomorphicHashingDisabledKey = "HomomorphicHashingDisabled" + MaintenanceModeAllowedConfig = "MaintenanceModeAllowed" +) + +// RawNetworkParameter is a NeoFS network parameter which is transmitted but +// not interpreted by the NeoFS API protocol. +type RawNetworkParameter struct { + // Name of the parameter. + Name string + + // Raw parameter value. + Value []byte +} + +// NetworkConfiguration represents NeoFS network configuration stored +// in the NeoFS Sidechain. +type NetworkConfiguration struct { + MaxObjectSize uint64 + + StoragePrice uint64 + + AuditFee uint64 + + EpochDuration uint64 + + ContainerFee uint64 + + ContainerAliasFee uint64 + + EigenTrustIterations uint64 + + EigenTrustAlpha float64 + + IRCandidateFee uint64 + + WithdrawalFee uint64 + + HomomorphicHashingDisabled bool + + MaintenanceModeAllowed bool + + Raw []RawNetworkParameter +} diff --git a/deploy/nns.go b/deploy/nns.go new file mode 100644 index 00000000..6e355c6d --- /dev/null +++ b/deploy/nns.go @@ -0,0 +1,409 @@ +package deploy + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "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/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/management" + "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/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/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "go.uber.org/zap" +) + +// various NNS domain names. +const ( + domainBootstrap = "bootstrap" + domainDesignateNotaryPrefix = "designate-committee-notary-" + domainDesignateNotaryTx = domainDesignateNotaryPrefix + "tx." + domainBootstrap + domainContractAddresses = "neofs" + domainContainers = "container" + + domainAlphabetFmt = "alphabet%d" + domainAudit = "audit" + domainBalance = "balance" + domainContainer = "container" + domainNeoFSID = "neofsid" + domainNetmap = "netmap" + domainProxy = "proxy" + domainReputation = "reputation" +) + +func calculateAlphabetContractAddressDomain(index int) string { + return fmt.Sprintf(domainAlphabetFmt, index) +} + +func designateNotarySignatureDomainForMember(memberIndex int) string { + return fmt.Sprintf("%s%d.%s", domainDesignateNotaryPrefix, memberIndex, domainBootstrap) +} + +// various methods of the NeoFS NNS contract. +const ( + methodNNSRegister = "register" + methodNNSRegisterTLD = "registerTLD" + methodNNSResolve = "resolve" + methodNNSAddRecord = "addRecord" + methodNNSSetRecord = "setRecord" +) + +// default NNS domain settings. See DNS specification and also +// https://www.ripe.net/publications/docs/ripe-203. +const ( + nnsRefresh = 3600 + nnsRetry = 600 + nnsExpire = int64(10 * 365 * 24 * time.Hour / time.Second) + nnsMinimum = 3600 +) + +// various NNS errors. +var ( + errMissingDomain = errors.New("missing domain") + errMissingDomainRecord = errors.New("missing domain record") +) + +// deployNNSContractPrm groups parameters of NeoFS NNS contract deployment. +type deployNNSContractPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + localAcc *wallet.Account + + localNEF nef.File + localManifest manifest.Manifest + systemEmail string + + tryDeploy bool +} + +// initNNSContract synchronizes NNS contract with the chain and returns the +// address. Success is the presence of NNS contract in the chain with ID=1. +// initNNSContract returns any error encountered due to which the contract +// cannot be synchronized in any way. For example, problems that can be fixed on +// the chain in the background (manually or automatically over time) do not stop +// the procedure. Breaking the context stops execution immediately (so hangup is +// not possible) and the function returns an error. In this case, +// initNNSContract can be re-called (expected after application restart): all +// previously succeeded actions will be skipped, and execution will be continued +// from the last failed stage. +// +// If contract is missing and deployNNSContractPrm.tryDeploy is set, +// initNNSContract attempts to deploy local contract. +func initNNSContract(ctx context.Context, prm deployNNSContractPrm) (res util.Uint160, err error) { + 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() + + txMonitor := newTransactionGroupMonitor(localActor) + managementContract := management.New(localActor) + + for ; ; err = prm.monitor.waitForNextBlock(ctx) { + if err != nil { + return res, fmt.Errorf("wait for NNS contract synchronization: %w", err) + } + + prm.logger.Info("reading on-chain state of the NNS contract by ID=1") + + stateOnChain, 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 + } + + if stateOnChain != nil { + // declared in https://github.com/nspcc-dev/neofs-contract sources + const nnsContractName = "NameService" + if stateOnChain.Manifest.Name != nnsContractName { + return res, fmt.Errorf("wrong name of the contract with ID=1: expected '%s', got '%s'", + nnsContractName, stateOnChain.Manifest.Name) + } + + return stateOnChain.Hash, nil + } + + if !prm.tryDeploy { + prm.logger.Info("NNS contract is missing on the chain but attempts to deploy are disabled, will wait for background deployment") + continue + } + + prm.logger.Info("NNS contract is missing on the chain, contract needs to be deployed") + + 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...") + + // just to definitely avoid mutation + nefCp := prm.localNEF + manifestCp := prm.localManifest + + txID, vub, err := managementContract.Deploy(&nefCp, &manifestCp, []any{ + []any{ + []any{domainBootstrap, prm.systemEmail}, + []any{domainContractAddresses, prm.systemEmail}, + }, + }) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("not enough GAS to deploy NNS contract, will try again later") + } else { + prm.logger.Error("failed to send transaction deploying NNS contract, will try again later", zap.Error(err)) + } + continue + } + + prm.logger.Info("transaction deploying NNS contract has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub), + ) + + txMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + } +} + +// lookupNNSDomainRecord looks up for the 1st record of the NNS domain with +// given name. Returns errMissingDomain if domain doesn't exist. Returns +// errMissingDomainRecord if domain has no records. +func lookupNNSDomainRecord(inv *invoker.Invoker, nnsContract util.Uint160, domainName string) (string, error) { + item, err := unwrap.Item(inv.Call(nnsContract, methodNNSResolve, domainName, int64(nns.TXT))) + if err != nil { + if strings.Contains(err.Error(), "token not found") { + return "", errMissingDomain + } + + return "", fmt.Errorf("call '%s' method of the NNS contract: %w", methodNNSResolve, err) + } + + arr, ok := item.Value().([]stackitem.Item) + if !ok { + if _, ok = item.(stackitem.Null); !ok { + return "", fmt.Errorf("malformed/unsupported response of the NNS '%s' method: expected array, got %s", + methodNNSResolve, item.Type()) + } + } else if len(arr) > 0 { + b, err := arr[0].TryBytes() + if err != nil { + return "", fmt.Errorf("malformed/unsupported 1st array item of the NNS '%s' method response (expected %v): %w", + methodNNSResolve, stackitem.ByteArrayT, err) + } + + return string(b), nil + } + + 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 + + // address of the Proxy contract deployed in the blockchain. The contract + // pays for update transactions. + proxyContract util.Uint160 +} + +// 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. +// +// 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 := newProxyCommitteeNotaryActor(prm.blockchain, prm.localAcc, prm.committee, prm.proxyContract) + if err != nil { + return fmt.Errorf("create Notary service client sending transactions to be signed by the committee and paid by Proxy contract: %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() + + updateTxModifier := neoFSRuntimeTransactionModifier(prm.monitor.currentHeight) + txMonitor := newTransactionGroupMonitor(committeeActor) + + for ; ; err = prm.monitor.waitForNextBlock(ctx) { + if err != nil { + return fmt.Errorf("wait for NNS contract synchronization: %w", err) + } + + 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") + } + + // 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.MakeTunedCall(nnsOnChainState.Hash, methodUpdate, nil, updateTxModifier, + bLocalNEF, jLocalManifest, nil) + if err != nil { + if isErrContractAlreadyUpdated(err) { + prm.logger.Info("NNS contract is unchanged or has already been updated, skip") + return nil + } + + prm.logger.Error("failed to make transaction updating NNS contract, will try again later", zap.Error(err)) + continue + } + + 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 { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to send new Notary request updating NNS contract, skip") + } else { + prm.logger.Error("failed to send new Notary request updating NNS contract, skip", zap.Error(err)) + } + continue + } + + 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)) + + txMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } +} + +// setNeoFSContractDomainRecord groups parameters of setNeoFSContractDomainRecord. +type setNeoFSContractDomainRecordPrm struct { + logger *zap.Logger + + setRecordTxMonitor *transactionGroupMonitor + registerTLDTxMonitor *transactionGroupMonitor + + nnsContract util.Uint160 + systemEmail string + + localActor *actor.Actor + + committeeActor *notary.Actor + + domain string + record string +} + +func setNeoFSContractDomainRecord(ctx context.Context, prm setNeoFSContractDomainRecordPrm) { + prm.logger.Info("NNS domain record is missing, registration is needed") + + if prm.setRecordTxMonitor.isPending() { + prm.logger.Info("previously sent transaction setting domain in the NNS is still pending, will wait for the outcome") + return + } + + prm.logger.Info("sending new transaction setting domain in the NNS...") + + resRegister, err := prm.localActor.Call(prm.nnsContract, methodNNSRegister, + prm.domain, prm.localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) + if err != nil { + prm.logger.Info("test invocation registering domain in the NNS failed, will try again later", zap.Error(err)) + return + } + + resAddRecord, err := prm.localActor.Call(prm.nnsContract, methodNNSAddRecord, + prm.domain, int64(nns.TXT), prm.record) + if err != nil { + prm.logger.Info("test invocation setting domain record in the NNS failed, will try again later", zap.Error(err)) + return + } + + txID, vub, err := prm.localActor.SendRun(append(resRegister.Script, resAddRecord.Script...)) + if err != nil { + switch { + default: + prm.logger.Error("failed to send transaction setting domain in the NNS, will try again later", zap.Error(err)) + case errors.Is(err, neorpc.ErrInsufficientFunds): + prm.logger.Info("not enough GAS to set domain record in the NNS, will try again later") + case isErrTLDNotFound(err): + prm.logger.Info("missing TLD, need registration") + + if prm.registerTLDTxMonitor.isPending() { + prm.logger.Info("previously sent Notary request registering TLD in the NNS is still pending, will wait for the outcome") + return + } + + prm.logger.Info("sending new Notary registering TLD in the NNS...") + + mainTxID, fallbackTxID, vub, err := prm.committeeActor.Notarize(prm.committeeActor.MakeCall(prm.nnsContract, methodNNSRegisterTLD, + domainContractAddresses, prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum)) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("insufficient Notary balance to register TLD in the NNS, will try again later") + } else { + prm.logger.Error("failed to send Notary request registering TLD in the NNS, will try again later", zap.Error(err)) + } + return + } + + prm.logger.Info("Notary request registering TLD in the NNS has been successfully sent, will wait for the outcome", + zap.Stringer("main tx", mainTxID), zap.Stringer("fallback tx", fallbackTxID), zap.Uint32("vub", vub)) + + prm.registerTLDTxMonitor.trackPendingTransactionsAsync(ctx, vub, mainTxID, fallbackTxID) + } + return + } + + prm.logger.Info("transaction settings domain record in the NNS has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub), + ) + + prm.setRecordTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) +} diff --git a/deploy/notary.go b/deploy/notary.go new file mode 100644 index 00000000..dadd664e --- /dev/null +++ b/deploy/notary.go @@ -0,0 +1,1259 @@ +package deploy + +import ( + "bytes" + "context" + "crypto/elliptic" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "math" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/core/mempoolevent" + "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/neorpc" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nns" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt" + "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" + "go.uber.org/zap" +) + +// enableNotaryPrm groups parameters of Notary service initialization parameters +// for the committee. +type enableNotaryPrm struct { + logger *zap.Logger + + blockchain Blockchain + + // based on blockchain + monitor *blockchainMonitor + + nnsOnChainAddress util.Uint160 + systemEmail string + + committee keys.PublicKeys + localAcc *wallet.Account + localAccCommitteeIndex int +} + +// 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(ctx, prm) + if err != nil { + return fmt.Errorf("construct action designating Notary role to the local account: %w", err) + } + } else { + prm.logger.Info("committee is multi-acc, multi-signature is needed for Notary role designation") + + if prm.localAccCommitteeIndex == 0 { + 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(ctx, prm) + if err != nil { + return fmt.Errorf("construct action designating Notary role to the multi-acc committee as signer: %w", err) + } + } + } + + roleContract := rolemgmt.NewReader(invoker.New(prm.blockchain, nil)) + + for ; ; err = prm.monitor.waitForNextBlock(ctx) { + if err != nil { + return fmt.Errorf("wait for Notary service to be enabled for the committee: %w", err) + } + + prm.logger.Info("checking Notary role of the committee members...") + + accsWithNotaryRole, err := roleContract.GetDesignatedByRole(noderoles.P2PNotary, prm.monitor.currentHeight()) + if err != nil { + prm.logger.Error("failed to check role of the committee, will try again later", zap.Error(err)) + continue + } + + someoneWithoutNotaryRole := len(accsWithNotaryRole) < len(prm.committee) + if !someoneWithoutNotaryRole { + for i := range prm.committee { + if !accsWithNotaryRole.Contains(prm.committee[i]) { + someoneWithoutNotaryRole = true + break + } + } + } + if !someoneWithoutNotaryRole { + prm.logger.Info("all committee members have a Notary role") + return nil + } + + prm.logger.Info("not all members of the committee have a Notary role, designation is needed") + + tick() + } +} + +// initDesignateNotaryRoleToLocalAccountTick returns a function that preserves +// context of the Notary role designation to the local account between calls. +func initDesignateNotaryRoleToLocalAccountTick(ctx context.Context, prm enableNotaryPrm) (func(), error) { + committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(prm.committee)) + committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) + + err := committeeMultiSigAcc.ConvertMultisig(committeeMultiSigM, prm.committee) + if err != nil { + return nil, fmt.Errorf("compose committee multi-signature account: %w", err) + } + + localActor, err := actor.NewSimple(prm.blockchain, committeeMultiSigAcc) + if err != nil { + return nil, fmt.Errorf("init transaction sender from local account: %w", err) + } + + roleContract := rolemgmt.New(localActor) + + // multi-tick context + txMonitor := newTransactionGroupMonitor(localActor) + + return func() { + 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 + } + + prm.logger.Info("sending new transaction designating Notary role to the local account...") + + var err error + + txID, vub, err := roleContract.DesignateAsRole(noderoles.P2PNotary, keys.PublicKeys{prm.localAcc.PublicKey()}) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("not enough GAS to designate Notary role to the local account, will try again later") + } else { + prm.logger.Error("failed to send transaction designating Notary role to the local account, will try again later", zap.Error(err)) + } + return + } + + 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)) + + txMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + }, nil +} + +// initDesignateNotaryRoleAsLeaderTick returns a function that preserves context +// 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(ctx context.Context, prm enableNotaryPrm) (func(), error) { + committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(prm.committee)) + committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) + + err := committeeMultiSigAcc.ConvertMultisig(committeeMultiSigM, prm.committee) + if err != nil { + return nil, fmt.Errorf("compose committee multi-signature account: %w", err) + } + + localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) + if err != nil { + return nil, fmt.Errorf("init transaction sender from local account: %w", err) + } + + committeeSigners := []actor.SignerAccount{ + { + Signer: transaction.Signer{ + Account: prm.localAcc.ScriptHash(), + Scopes: transaction.None, + }, + Account: prm.localAcc, + }, + { + Signer: transaction.Signer{ + Account: committeeMultiSigAcc.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: committeeMultiSigAcc, + }, + } + + committeeActor, err := actor.New(prm.blockchain, committeeSigners) + if err != nil { + return nil, fmt.Errorf("init transaction sender with committee signers: %w", err) + } + + invkr := invoker.New(prm.blockchain, nil) + roleContract := rolemgmt.New(committeeActor) + + // multi-tick context + var tx *transaction.Transaction + var mCommitteeIndexToSignature map[int][]byte + var txFullySigned bool + var triedDesignateRoleTx bool + registerDomainTxMonitor := newTransactionGroupMonitor(localActor) + setDomainRecordTxMonitor := newTransactionGroupMonitor(localActor) + designateRoleTxMonitor := newTransactionGroupMonitor(localActor) + + resetTx := func() { + tx = nil + for k := range mCommitteeIndexToSignature { + delete(mCommitteeIndexToSignature, k) + } + txFullySigned = false + setDomainRecordTxMonitor.reset() + designateRoleTxMonitor.reset() + } + + return func() { + l := prm.logger.With(zap.String("domain", domainDesignateNotaryTx)) + + l.Info("synchronizing shared data of the transaction designating Notary role to the committee with NNS domain record...") + + var sharedTxData sharedTransactionData + + generateAndShareTxData := func(recordExists bool) { + resetTx() + + prm.logger.Info("generating shared data for the transaction designating Notary role to the committee...") + + ver, err := prm.blockchain.GetVersion() + if err != nil { + prm.logger.Error("failed request Neo protocol configuration, will try again later", zap.Error(err)) + return + } + + // localActor.CalculateValidUntilBlock is not used because it is rather "idealized" + // in terms of the accessibility of committee member nodes. So, we need a more + // practically viable timeout to reduce the chance of transaction re-creation. + const defaultValidUntilBlockIncrement = 120 // ~30m for 15s block interval + var txValidUntilBlock uint32 + + if defaultValidUntilBlockIncrement <= ver.Protocol.MaxValidUntilBlockIncrement { + txValidUntilBlock = prm.monitor.currentHeight() + defaultValidUntilBlockIncrement + } else { + txValidUntilBlock = prm.monitor.currentHeight() + ver.Protocol.MaxValidUntilBlockIncrement + } + + strSharedTxData := sharedTransactionData{ + sender: localActor.Sender(), + validUntilBlock: txValidUntilBlock, + nonce: Uint32(), + }.encodeToString() + + l.Info("sending new transaction setting domain record in the NNS...") + + var txID util.Uint256 + var vub uint32 + if recordExists { + txID, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, + domainDesignateNotaryTx, int64(nns.TXT), 0, strSharedTxData) + } else { + txID, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, + domainDesignateNotaryTx, int64(nns.TXT), strSharedTxData) + } + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("not enough GAS to set NNS domain record, will try again later") + } else { + prm.logger.Error("failed to send transaction setting NNS domain record, will try again later", zap.Error(err)) + } + return + } + + l.Info("transaction setting NNS domain record has been successfully sent, will wait for the outcome", + zap.Stringer("tx", txID), zap.Uint32("vub", vub)) + + setDomainRecordTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + } + + strSharedTxData, err := lookupNNSDomainRecord(invkr, prm.nnsOnChainAddress, domainDesignateNotaryTx) + if err != nil { + if errors.Is(err, errMissingDomain) { + l.Info("NNS domain is missing, registration is needed") + + 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...") + + txID, vub, err := localActor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, + domainDesignateNotaryTx, localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("not enough GAS to register domain in the NNS, will try again later") + } else { + prm.logger.Error("failed to send transaction registering domain in the NNS, will try again later", zap.Error(err)) + } + return + } + + 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)) + return + } + + l.Info("missing record of the NNS domain, needed to be set") + + if setDomainRecordTxMonitor.isPending() { + prm.logger.Info("previously sent transaction setting NNS domain record is still pending, will wait for the outcome") + return + } + + generateAndShareTxData(false) + return + } + + err = sharedTxData.decodeString(strSharedTxData) + if err != nil { + l.Error("failed to decode shared data of the transaction got from the NNS domain record, will wait for a background fix", + zap.Error(err)) + return + } + + if cur := prm.monitor.currentHeight(); cur > sharedTxData.validUntilBlock { + l.Error("previously used shared data of the transaction expired, need a reset", + zap.Uint32("expires after height", sharedTxData.validUntilBlock), zap.Uint32("current height", cur)) + generateAndShareTxData(true) + return + } + + l.Info("shared data of the transaction designating Notary role to the committee synchronized successfully", + zap.Uint32("nonce", sharedTxData.nonce), zap.Uint32("expires after height", sharedTxData.validUntilBlock), + zap.Stringer("sender", sharedTxData.sender), + ) + + if tx == nil || !sharedTxDataMatches(tx, sharedTxData) { + prm.logger.Info("making new transaction designating Notary role to the committee...") + + tx, err = makeUnsignedDesignateCommitteeNotaryTx(roleContract, prm.committee, sharedTxData) + if err != nil { + prm.logger.Error("failed to make unsigned transaction designating Notary role to the committee, will try again later", + zap.Error(err)) + return + } + + prm.logger.Info("transaction designating Notary role to the committee initialized, signing...") + + netMagic := localActor.GetNetwork() + + err = prm.localAcc.SignTx(netMagic, tx) + if err != nil { + prm.logger.Error("failed to sign transaction designating Notary role to the committee by local node's account, will try again later", + zap.Error(err)) + return + } + + err = committeeMultiSigAcc.SignTx(netMagic, tx) + if err != nil { + prm.logger.Error("failed to sign transaction designating Notary role to the committee by committee multi-signature account, will try again later", + zap.Error(err)) + return + } + + prm.logger.Info("new transaction designating Notary role to the committee successfully made") + } else { + prm.logger.Info("previously made transaction designating Notary role to the committee is still relevant, continue with it") + } + + needRemoteSignatures := committeeMultiSigM - 1 // -1 local, we always have it + + if len(mCommitteeIndexToSignature) < needRemoteSignatures { + if mCommitteeIndexToSignature == nil { + mCommitteeIndexToSignature = make(map[int][]byte, needRemoteSignatures) + } + + prm.logger.Info("collecting signatures of the transaction designating notary role to the committee from other members using NNS...") + + var invalidSignatureCounter int + + for i := range prm.committee[1:] { + domain := designateNotarySignatureDomainForMember(i) + + rec, err := lookupNNSDomainRecord(invkr, prm.nnsOnChainAddress, domain) + if err != nil { + if errors.Is(err, errMissingDomain) || errors.Is(err, errMissingDomainRecord) { + prm.logger.Info("missing NNS domain record with committee member's signature of the transaction designating Notary role to the committee, will wait", + zap.Stringer("member", prm.committee[i]), + zap.String("domain", domain)) + } else { + prm.logger.Error("failed to read NNS domain record with committee member's signature of the transaction designating Notary role to the committee, will try again later", + zap.Stringer("member", prm.committee[i]), + zap.String("domain", domain), + zap.Error(err)) + } + continue + } + + bRec, err := base64.StdEncoding.DecodeString(rec) + if err != nil { + prm.logger.Info("failed to decode NNS domain record with committee member's signature of the transaction designating Notary role to the committee from base64, will wait for a background fix", + zap.Stringer("member", prm.committee[i]), + zap.String("domain", domain), + zap.Error(err)) + continue + } + + checksumMatches, bSignature := sharedTxData.shiftChecksum(bRec) + if !checksumMatches { + prm.logger.Info("checksum of shared data of the transaction designating Notary role to the committee submitted by committee member mismatches, skip signature", + zap.Stringer("member", prm.committee[i]), + zap.String("domain", domain)) + continue + } + + txCp := *tx // to safely call Hash method below + if !prm.committee[i].VerifyHashable(bSignature, uint32(localActor.GetNetwork()), &txCp) { + prm.logger.Info("invalid signature of the transaction designating Notary role to the committee submitted by committee member", + zap.Stringer("member", prm.committee[i]), + zap.String("domain", domain)) + + invalidSignatureCounter++ + + if invalidSignatureCounter+committeeMultiSigM > len(prm.committee) { + prm.logger.Info("number of invalid signatures of the transaction designating Notary role to the committee submitted by remote members exceeded the threshold, will recreate the transaction", + zap.Int("invalid", invalidSignatureCounter), zap.Int("need", committeeMultiSigM), + zap.Int("total members", len(prm.committee))) + generateAndShareTxData(true) + return + } + + continue + } + + prm.logger.Info("received valid signature of the transaction designating Notary role to the committee submitted by committee member", + zap.Stringer("member", prm.committee[i]), + zap.String("domain", domain)) + + mCommitteeIndexToSignature[i] = bSignature + if len(mCommitteeIndexToSignature) == needRemoteSignatures { + break + } + } + + if len(mCommitteeIndexToSignature) < needRemoteSignatures { + prm.logger.Info("there are still not enough signatures of the transaction designating Notary role to the committee in the NNS, will wait", + zap.Int("need", needRemoteSignatures), zap.Int("got", len(mCommitteeIndexToSignature))) + return + } + } + + prm.logger.Info("gathered enough signatures of the transaction designating Notary role to the committee") + + 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 + } + + if !txFullySigned { + prm.logger.Info("finalizing the transaction designating Notary role to the committee...") + + initialLen := len(tx.Scripts[1].InvocationScript) + var extraLen int + + for _, sig := range mCommitteeIndexToSignature { + extraLen += 1 + 1 + len(sig) // opcode + length + value + } + + tx.Scripts[1].InvocationScript = append(tx.Scripts[1].InvocationScript, + make([]byte, extraLen)...) + buf := tx.Scripts[1].InvocationScript[initialLen:] + + for _, sig := range mCommitteeIndexToSignature { + buf[0] = byte(opcode.PUSHDATA1) + buf[1] = byte(len(sig)) + buf = buf[2:] + buf = buf[copy(buf, sig):] + } + + txFullySigned = true + } + + prm.logger.Info("sending the transaction designating Notary role to the committee...") + + txID, vub, err := localActor.Send(tx) + if err != nil { + switch { + default: + prm.logger.Error("failed to send transaction designating Notary role to the committee, will try again later", + zap.Error(err)) + case errors.Is(err, neorpc.ErrInsufficientFunds): + prm.logger.Info("not enough GAS for transaction designating Notary role to the committee, will try again later") + case errors.Is(err, neorpc.ErrVerificationFailed): + prm.logger.Warn("composed transaction designating Notary role to the committee is invalid and can't be sent, will recreate", + zap.Error(err)) + generateAndShareTxData(true) + } + return + } + + 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)) + + triedDesignateRoleTx = true + designateRoleTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + }, nil +} + +// initDesignateNotaryRoleAsSignerTick returns a function that preserves context +// 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(ctx context.Context, prm enableNotaryPrm) (func(), error) { + committeeMultiSigM := smartcontract.GetMajorityHonestNodeCount(len(prm.committee)) + committeeMultiSigAcc := wallet.NewAccountFromPrivateKey(prm.localAcc.PrivateKey()) + + err := committeeMultiSigAcc.ConvertMultisig(committeeMultiSigM, prm.committee) + if err != nil { + return nil, fmt.Errorf("compose committee multi-signature account: %w", err) + } + + localActor, err := actor.NewSimple(prm.blockchain, prm.localAcc) + if err != nil { + return nil, fmt.Errorf("init transaction sender from local account: %w", err) + } + + committeeSigners := []actor.SignerAccount{ + { + Signer: transaction.Signer{ + Account: prm.localAcc.ScriptHash(), + Scopes: transaction.None, + }, + Account: prm.localAcc, + }, + { + Signer: transaction.Signer{ + Account: committeeMultiSigAcc.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: committeeMultiSigAcc, + }, + } + + committeeActor, err := actor.New(prm.blockchain, committeeSigners) + if err != nil { + return nil, fmt.Errorf("init transaction sender with committee signers: %w", err) + } + + invkr := invoker.New(prm.blockchain, nil) + roleContract := rolemgmt.New(committeeActor) + + // multi-tick context + var tx *transaction.Transaction + registerDomainTxMonitor := newTransactionGroupMonitor(localActor) + setDomainRecordTxMonitor := newTransactionGroupMonitor(localActor) + + resetTx := func() { + tx = nil + setDomainRecordTxMonitor.reset() + } + + return func() { + l := prm.logger.With(zap.String("domain", domainDesignateNotaryTx)) + + prm.logger.Info("synchronizing shared data of the transaction designating Notary role to the committee with NNS domain record...") + + strSharedTxData, err := lookupNNSDomainRecord(invkr, prm.nnsOnChainAddress, domainDesignateNotaryTx) + if err != nil { + switch { + default: + l.Error("failed to lookup NNS domain record, will try again later", zap.Error(err)) + case errors.Is(err, errMissingDomain): + l.Info("NNS domain is missing, will wait for a leader") + case errors.Is(err, errMissingDomainRecord): + l.Info("missing record in the NNS domain, will wait for a leader") + } + return + } + + var sharedTxData sharedTransactionData + + err = sharedTxData.decodeString(strSharedTxData) + if err != nil { + l.Error("failed to decode shared data of the transaction got from the NNS domain record, will wait for a background fix", + zap.Error(err)) + return + } + + if cur := prm.monitor.currentHeight(); cur > sharedTxData.validUntilBlock { + l.Error("previously used shared data of the transaction expired, will wait for update by leader", + zap.Uint32("expires after height", sharedTxData.validUntilBlock), zap.Uint32("current height", cur)) + resetTx() + return + } + + l.Info("shared data of the transaction designating Notary role to the committee synchronized successfully", + zap.Uint32("nonce", sharedTxData.nonce), zap.Uint32("expires after height", sharedTxData.validUntilBlock), + zap.Stringer("sender", sharedTxData.sender), + ) + + if tx == nil || !sharedTxDataMatches(tx, sharedTxData) { + prm.logger.Info("recreating the transaction designating Notary role to the committee...") + + tx, err = makeUnsignedDesignateCommitteeNotaryTx(roleContract, prm.committee, sharedTxData) + if err != nil { + prm.logger.Error("failed to make unsigned transaction designating Notary role to the committee, will try again later", + zap.Error(err)) + return + } + + prm.logger.Info("transaction designating Notary role to the committee successfully recreated") + } else { + prm.logger.Info("previously made transaction designating Notary role to the committee is still relevant, continue with it") + } + + domain := designateNotarySignatureDomainForMember(prm.localAccCommitteeIndex) + + l = prm.logger.With(zap.String("domain", domain)) + + var recordExists bool + var needReset bool + + rec, err := lookupNNSDomainRecord(invkr, prm.nnsOnChainAddress, domain) + if err != nil { + if errors.Is(err, errMissingDomain) { + l.Info("NNS domain is missing, registration is needed") + + 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...") + + txID, vub, err := localActor.SendCall(prm.nnsOnChainAddress, methodNNSRegister, + domain, localActor.Sender(), prm.systemEmail, nnsRefresh, nnsRetry, nnsExpire, nnsMinimum) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("not enough GAS to register domain in the NNS, will try again later") + } else { + prm.logger.Error("failed to send transaction registering domain in the NNS, will try again later", zap.Error(err)) + } + return + } + + 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)) + + 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)) + return + } + + l.Info("missing record of the NNS domain, needed to be set") + + if setDomainRecordTxMonitor.isPending() { + prm.logger.Info("previously sent transaction setting NNS domain record is still pending, will wait for the outcome") + return + } + + needReset = true + } else { + bRec, err := base64.StdEncoding.DecodeString(rec) + if err != nil { + l.Info("failed to decode NNS domain record with local account's signature of the transaction designating Notary role to the committee from base64, will wait for a background fix", + zap.String("domain", domain), zap.Error(err)) + return + } + + checksumMatches, bSignature := sharedTxData.shiftChecksum(bRec) + if !checksumMatches { + l.Info("checksum of shared data of the transaction designating Notary role to the committee submitted by committee member mismatches, need to be recalculated") + needReset = true + } else { + txCp := *tx // to safely call Hash method below + if !prm.localAcc.PublicKey().VerifyHashable(bSignature, uint32(localActor.GetNetwork()), &txCp) { + l.Info("invalid signature of the transaction designating Notary role to the committee submitted by local account, need to be recalculated") + needReset = true + } + } + + recordExists = true + } + + if needReset { + prm.logger.Info("calculating signature of the transaction designating Notary role to the committee using local account...") + + sig := prm.localAcc.SignHashable(localActor.GetNetwork(), tx) + sig = sharedTxData.unshiftChecksum(sig) + + rec = base64.StdEncoding.EncodeToString(sig) + + l.Info("sending new transaction setting domain record in the NNS...") + + var txID util.Uint256 + var vub uint32 + if recordExists { + txID, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSSetRecord, + domain, int64(nns.TXT), 0, rec) + } else { + txID, vub, err = localActor.SendCall(prm.nnsOnChainAddress, methodNNSAddRecord, + domain, int64(nns.TXT), rec) + } + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + prm.logger.Info("not enough GAS to set NNS domain record, will try again later") + } else { + prm.logger.Error("failed to send transaction setting NNS domain record, will try again later", zap.Error(err)) + } + return + } + + l.Info("transaction setting NNS domain record has been successfully sent, will wait for the outcome") + + setDomainRecordTxMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + + return + } + }, nil +} + +// sharedTransactionData groups transaction parameters that cannot be predicted +// in a decentralized way and need to be sent out. +type sharedTransactionData struct { + sender util.Uint160 + validUntilBlock uint32 + nonce uint32 +} + +// bytes serializes sharedTransactionData. +func (x sharedTransactionData) bytes() []byte { + b := make([]byte, sharedTransactionDataLen) + // fixed size is more convenient for potential format changes in the future + copy(b, x.sender.BytesBE()) + binary.BigEndian.PutUint32(b[util.Uint160Size:], x.validUntilBlock) + binary.BigEndian.PutUint32(b[util.Uint160Size+4:], x.nonce) + return b +} + +// encodeToString returns serialized sharedTransactionData in base64. +func (x sharedTransactionData) encodeToString() string { + return base64.StdEncoding.EncodeToString(x.bytes()) +} + +// decodeString decodes serialized sharedTransactionData from base64. +func (x *sharedTransactionData) decodeString(s string) (err error) { + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return fmt.Errorf("decode shared transaction data from base64: %w", err) + } + + if len(b) != sharedTransactionDataLen { + return fmt.Errorf("invalid/unsupported length of shared transaction data: expected %d, got %d", + sharedTransactionDataLen, len(b)) + } + + x.sender, err = util.Uint160DecodeBytesBE(b[:util.Uint160Size]) + if err != nil { + return fmt.Errorf("decode sender account binary: %w", err) + } + + x.validUntilBlock = binary.BigEndian.Uint32(b[util.Uint160Size:]) + x.nonce = binary.BigEndian.Uint32(b[util.Uint160Size+4:]) + + return nil +} + +const ( + sharedTransactionDataLen = util.Uint160Size + 4 + 4 + sharedTransactionDataChecksumLen = 4 +) + +// unshiftChecksum prepends given payload with first 4 bytes of the +// sharedTransactionData SHA-256 checksum. Inverse operation to shiftChecksum. +func (x sharedTransactionData) unshiftChecksum(data []byte) []byte { + h := sha256.Sum256(x.bytes()) + return append(h[:sharedTransactionDataChecksumLen], data...) +} + +// shiftChecksum matches checksum of the sharedTransactionData and returns +// payload. Inverse operation to unshiftChecksum. +func (x sharedTransactionData) shiftChecksum(data []byte) (bool, []byte) { + if len(data) < sharedTransactionDataChecksumLen { + return false, data + } + + h := sha256.Sum256(x.bytes()) + if !bytes.HasPrefix(data, h[:sharedTransactionDataChecksumLen]) { + return false, nil + } + + return true, data[sharedTransactionDataChecksumLen:] +} + +// sharedTxDataMatches checks if given transaction is constructed using provided +// shared parameters. +func sharedTxDataMatches(tx *transaction.Transaction, sharedTxData sharedTransactionData) bool { + return sharedTxData.nonce == tx.Nonce && + sharedTxData.validUntilBlock == tx.ValidUntilBlock && + len(tx.Signers) > 0 && tx.Signers[0].Account.Equals(sharedTxData.sender) +} + +// makeUnsignedDesignateCommitteeNotaryTx constructs unsigned transaction that +// designates Notary role to the specified committee members using shared +// parameters. +// +// Note: RoleManagement contract client must be initialized with two signers: +// 1. simple account with transaction.None witness scope +// 2. committee multi-signature account with transaction.CalledByEntry witness scope +func makeUnsignedDesignateCommitteeNotaryTx(roleContract *rolemgmt.Contract, committee keys.PublicKeys, sharedTxData sharedTransactionData) (*transaction.Transaction, error) { + tx, err := roleContract.DesignateAsRoleUnsigned(noderoles.P2PNotary, committee) + if err != nil { + return nil, err + } + + tx.ValidUntilBlock = sharedTxData.validUntilBlock + tx.Nonce = sharedTxData.nonce + tx.Signers[0].Account = sharedTxData.sender + + return tx, nil +} + +// newCommitteeNotaryActor calls newCommitteeNotaryActorWithScope with transaction.CalledByEntry +// witness scope appropriate for most transactions. +func newCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee keys.PublicKeys) (*notary.Actor, error) { + return newCommitteeNotaryActorWithCustomCommitteeSigner(b, localAcc, committee, func(s *transaction.Signer) { + s.Scopes = transaction.CalledByEntry + }) +} + +// calls newCommitteeNotaryActorWithCustomCommitteeSignerAndPayer with local account +// set as payer. +func newCommitteeNotaryActorWithCustomCommitteeSigner( + b Blockchain, + localAcc *wallet.Account, + committee keys.PublicKeys, + fCommitteeSigner func(*transaction.Signer), +) (*notary.Actor, error) { + return _newCustomCommitteeNotaryActor(b, localAcc, committee, localAcc, fCommitteeSigner) +} + +// returns notary.Actor that builds and sends Notary service requests witnessed +// by the specified committee members to the provided Blockchain. Local account +// should be one of the committee members. Given Proxy contract pays for main +// transactions. Allows to specify extra transaction signers. +func newProxyCommitteeNotaryActor(b Blockchain, localAcc *wallet.Account, committee keys.PublicKeys, proxyContract util.Uint160, extraSigners ...actor.SignerAccount) (*notary.Actor, error) { + return _newCustomCommitteeNotaryActor(b, localAcc, committee, notary.FakeContractAccount(proxyContract), func(s *transaction.Signer) { + s.Scopes = transaction.CalledByEntry + }, extraSigners...) +} + +// returns notary.Actor builds and sends Notary service requests witnessed by +// the specified committee members to the provided Blockchain. Local account +// should be one of the committee members. Specified account pays for +// main transactions. Allows to specify extra transaction signers. +// +// Transaction signer callback allows to specify committee signer (e.g. tune +// witness scope). Instance passed to it has Account set to multi-signature +// account for the parameterized committee. +// +// This function is presented to share common code and is expected to be called +// by helper constructors only. +func _newCustomCommitteeNotaryActor( + b Blockchain, + localAcc *wallet.Account, + committee keys.PublicKeys, + payerAcc *wallet.Account, + fCommitteeSigner func(*transaction.Signer), + extraSigners ...actor.SignerAccount, +) (*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) + } + + committeeSignerAcc := actor.SignerAccount{ + Signer: transaction.Signer{ + Account: committeeMultiSigAcc.ScriptHash(), + }, + Account: committeeMultiSigAcc, + } + + fCommitteeSigner(&committeeSignerAcc.Signer) + + signers := []actor.SignerAccount{ + { + Signer: transaction.Signer{ + Account: payerAcc.ScriptHash(), + Scopes: transaction.None, + }, + Account: payerAcc, + }, + committeeSignerAcc, + } + + return notary.NewActor(b, append(signers, extraSigners...), localAcc) +} + +// Amount of GAS for the single local account's GAS->Notary transfer. Relatively +// small value for fallback transactions' fees. +var singleNotaryDepositAmount = big.NewInt(1_0000_0000) // 1 GAS + +func autoReplenishNotaryBalance(ctx context.Context, l *zap.Logger, b Blockchain, localAcc *wallet.Account, chTrigger <-chan struct{}) { + l.Info("tracking Notary balance for auto-replenishment...") + + var err error + var localActor *actor.Actor + var notaryContract *notary.Contract + var gasContract *nep17.Token + var txMonitor *transactionGroupMonitor + localAccID := localAcc.ScriptHash() + + for { + select { + case <-ctx.Done(): + l.Info("Notary balance tracker stopped by context", zap.Error(ctx.Err())) + return + case _, ok := <-chTrigger: + if !ok { + l.Info("Notary balance tracker stopped by closed block channel") + return + } + } + + if localActor == nil { + localActor, err = actor.NewSimple(b, localAcc) + if err != nil { + l.Error("failed to init transaction sender from local account, will try again later", zap.Error(err)) + continue + } + + notaryContract = notary.New(localActor) + gasContract = gas.New(localActor) + txMonitor = newTransactionGroupMonitor(localActor) + } + + 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)) + continue + } + + // deposit when balance falls below 1/5 of the single deposit amount + const refillProportion = 5 + + if new(big.Int).Mul(notaryBalance, big.NewInt(refillProportion)).Cmp(singleNotaryDepositAmount) >= 0 { + l.Info("enough funds on the notary balance, deposit is not needed", zap.Stringer("balance", notaryBalance)) + continue + } + + // simple deposit scheme: transfer 1GAS (at most 2% of GAS token balance) for + // 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. + if txMonitor.isPending() { + l.Info("previously sent transaction transferring local account's GAS to the Notary contract is still pending, will wait for the outcome") + continue + } + + var transferData = new(notary.OnNEP17PaymentData) + transferData.Account = &localAccID + transferData.Till = math.MaxUint32 // deposit "forever" so we don't have to renew + + l.Info("sending new transaction transferring local account's GAS to the Notary contract...", + zap.Stringer("amount", singleNotaryDepositAmount), zap.Uint32("till", transferData.Till)) + + txID, vub, err := gasContract.Transfer(localAccID, notary.Hash, singleNotaryDepositAmount, transferData) + 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)) + continue + } + + l.Info("transaction transferring local account's GAS to the Notary contract has been successfully sent, will wait for the outcome") + + txMonitor.trackPendingTransactionsAsync(ctx, vub, txID) + } +} + +// listenCommitteeNotaryRequestsPrm groups parameters of listenCommitteeNotaryRequests. +type listenCommitteeNotaryRequestsPrm struct { + logger *zap.Logger + + blockchain Blockchain + + localAcc *wallet.Account + + committee keys.PublicKeys + + validatorMultiSigAcc *wallet.Account +} + +// 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) + } + + ver, err := prm.blockchain.GetVersion() + if err != nil { + return fmt.Errorf("read protocol configuration: %w", err) + } + + netMagic := ver.Protocol.Network + localAccID := prm.localAcc.ScriptHash() + validatorMultiSigAccID := prm.validatorMultiSigAcc.ScriptHash() + committeeMultiSigAccID := committeeMultiSigAcc.ScriptHash() + // 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{}) + + chNotaryRequests, err := prm.blockchain.SubscribeToNotaryRequests() + if err != nil { + return fmt.Errorf("subscribe to notary requests: %w", err) + } + + go func() { + prm.logger.Info("listening to committee notary requests...") + + upperLoop: + 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 + + 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() + l := prm.logger.With(zap.Stringer("tx", srcMainTxHash)) + _, processed := mProcessedMainTxs[srcMainTxHash] + if processed { + l.Info("main transaction of the notary request has already been processed, skip") + continue + } + + mProcessedMainTxs[srcMainTxHash] = struct{}{} + + // revise severity level of the messages + // https://github.com/nspcc-dev/neofs-node/issues/2419 + switch { + case notaryEvent.Type != mempoolevent.TransactionAdded: + l.Info("unsupported type of the notary request event, skip", + zap.Stringer("got", notaryEvent.Type), zap.Stringer("expect", mempoolevent.TransactionAdded)) + continue + case len(mainTx.Scripts) != len(mainTx.Signers): + l.Info("different number of signers and scripts of main transaction from the received notary request, skip") + continue + case len(mainTx.Signers) == 0 || !mainTx.Signers[len(mainTx.Signers)-1].Account.Equals(notary.Hash): + l.Info("Notary contract is not the last signer of main transaction from the received notary request, skip") + continue + } + + localAccSignerIndex := -1 + committeeMultiSigSignerIndex := -1 + validatorMultiSigSignerIndex := -1 + notaryContractSignerIndex := -1 + + for i := range mainTx.Signers { + switch mainTx.Signers[i].Account { + case notary.Hash: + notaryContractSignerIndex = i + case localAccID: + if len(mainTx.Scripts[i].InvocationScript) > 0 { + l.Info("main transaction from the received notary request already has local account's signature, skip") + continue upperLoop // correctness doesn't matter + } + + localAccSignerIndex = i + case committeeMultiSigAccID: + // simplified: we know binary format, so may match faster + if bytes.Contains(mainTx.Scripts[i].InvocationScript, committeeMultiSigAcc.SignHashable(netMagic, mainTx)) { + l.Info("main transaction from the received notary request already has local account's committee signature, skip") + continue upperLoop // correctness doesn't matter + } + + // we cannot differ missing signature from the incorrect one in this case + + committeeMultiSigSignerIndex = i + case validatorMultiSigAccID: + // simplified: we know binary format, so may match faster + if bytes.Contains(mainTx.Scripts[i].InvocationScript, prm.validatorMultiSigAcc.SignHashable(netMagic, mainTx)) { + l.Info("main transaction from the received notary request already has local account's committee signature, skip") + continue upperLoop // correctness doesn't matter + } + + // we cannot differ missing signature from the incorrect one in this case + + validatorMultiSigSignerIndex = i + } + } + + if notaryContractSignerIndex < 0 { + l.Info("Notary contract is not a signer of main transaction of the received notary request, skip") + continue + } + + if localAccSignerIndex < 0 && committeeMultiSigSignerIndex < 0 && validatorMultiSigSignerIndex < 0 { + l.Info("local account is not a signer of main transaction of the received notary request, skip") + continue + } + + signers := make([]actor.SignerAccount, 0, len(mainTx.Signers)-1) // Notary contract added by actor + + for i := range mainTx.Signers { + if i == notaryContractSignerIndex { + continue + } + + var acc *wallet.Account + switch i { + case localAccSignerIndex: + acc = prm.localAcc + case committeeMultiSigSignerIndex: + acc = committeeMultiSigAcc + case validatorMultiSigSignerIndex: + acc = prm.validatorMultiSigAcc + default: + if len(mainTx.Scripts[i].VerificationScript) > 0 { + if bSenderKey, ok := vm.ParseSignatureContract(mainTx.Scripts[i].VerificationScript); ok { + senderKey, err := keys.NewPublicKeyFromBytes(bSenderKey, elliptic.P256()) + if err != nil { + l.Info("failed to decode public key from simple signature contract verification script of main transaction from the received notary request, skip", + zap.Int("script#", i), zap.Error(err)) + continue + } + + acc = notary.FakeSimpleAccount(senderKey) + } else if m, bKeys, ok := vm.ParseMultiSigContract(mainTx.Scripts[i].VerificationScript); ok { + pKeys := make(keys.PublicKeys, len(bKeys)) + for j := range bKeys { + err := pKeys[j].DecodeBytes(bKeys[j]) + if err != nil { + l.Info("failed to decode public key from multi-sig contract verification script of main transaction from the received notary request, skip", + zap.Int("script#", i), zap.Int("key#", j), zap.Error(err)) + continue + } + } + + acc, err = notary.FakeMultisigAccount(m, pKeys) + if err != nil { + l.Info("failed to build fake multi-sig account from verification script of main transaction from the received notary request, skip", + zap.Int("script#", i), zap.Error(err)) + continue + } + } else { + l.Info("got invalid/unsupported verification script in main transaction from the received notary request, skip", + zap.Int("script#", i)) + continue upperLoop + } + } else { + acc = notary.FakeContractAccount(mainTx.Signers[i].Account) + } + } + + signers = append(signers, actor.SignerAccount{ + Signer: mainTx.Signers[i], + Account: acc, + }) + } + + // copy transaction to avoid pointer mutation + mainTxCp := *mainTx + mainTx = &mainTxCp // source one isn't needed anymore + + // it'd be safer to get into the transaction and analyze what it is trying to do. + // For simplicity, now we blindly sign it. Track https://github.com/nspcc-dev/neofs-node/issues/2430 + + l.Info("signing main transaction from the received notary request by the local account...") + + // reset all existing script because Notary actor adds itself + mainTx.Scripts = nil + + // 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, signers, prm.localAcc) + if err != nil { + // not really expected + l.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 { + l.Error("failed to sign main transaction from the received notary request by the local account, skip", zap.Error(err)) + continue + } + + l.Info("sending new notary request with the main transaction signed by the local account...") + + _, _, _, err = notaryActor.Notarize(mainTx, nil) + if err != nil { + if errors.Is(err, neorpc.ErrInsufficientFunds) { + l.Info("insufficient Notary balance to send new Notary request with the main transaction signed by the local account, skip") + } else { + l.Error("failed to send new Notary request with the main transaction signed by the local account, skip", zap.Error(err)) + } + continue + } + + l.Info("main transaction from the received notary request has been successfully signed and sent by the local account") + } + } + }() + + return nil +} diff --git a/deploy/rand.go b/deploy/rand.go new file mode 100644 index 00000000..d187737b --- /dev/null +++ b/deploy/rand.go @@ -0,0 +1,44 @@ +package deploy + +import ( + crand "crypto/rand" + "encoding/binary" + mrand "math/rand" +) + +var source = mrand.New(&cryptoSource{}) + +// Uint64 returns a random uint64 value. +func Uint64() uint64 { + return source.Uint64() +} + +// Uint32 returns a random uint32 value. +func Uint32() uint32 { + return source.Uint32() +} + +// Shuffle randomizes the order of elements. +// n is the number of elements. Shuffle panics if n < 0. +// swap swaps the elements with indexes i and j. +func Shuffle(n int, swap func(i, j int)) { + source.Shuffle(n, swap) +} + +// cryptoSource is math/rand.Source which takes entropy via crypto/rand. +type cryptoSource struct{} + +// Seed implements math/rand.Source. +func (s *cryptoSource) Seed(int64) {} + +// Int63 implements math/rand.Source. +func (s *cryptoSource) Int63() int64 { + return int64(s.Uint64() >> 1) +} + +// Uint64 implements math/rand.Source64. +func (s *cryptoSource) Uint64() uint64 { + var buf [8]byte + _, _ = crand.Read(buf[:]) // always returns nil + return binary.BigEndian.Uint64(buf[:]) +} diff --git a/deploy/util.go b/deploy/util.go new file mode 100644 index 00000000..1ad3a8fb --- /dev/null +++ b/deploy/util.go @@ -0,0 +1,205 @@ +package deploy + +import ( + "context" + "errors" + "fmt" + "strings" + "sync/atomic" + "time" + + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/neorpc" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neofs-contract/common" + "go.uber.org/zap" +) + +func isErrContractAlreadyUpdated(err error) bool { + return strings.Contains(err.Error(), common.ErrAlreadyUpdated) +} + +func isErrTLDNotFound(err error) bool { + return strings.Contains(err.Error(), "TLD not found") +} + +// blockchainMonitor is a thin utility around Blockchain providing state +// monitoring. +type blockchainMonitor struct { + logger *zap.Logger + + blockchain Blockchain + + blockInterval time.Duration + + height atomic.Uint32 + + chConnLost chan struct{} +} + +// newBlockchainMonitor constructs and runs monitor for the given Blockchain. +func newBlockchainMonitor(l *zap.Logger, b Blockchain, chNewBlock chan<- struct{}) (*blockchainMonitor, error) { + ver, err := b.GetVersion() + if err != nil { + return nil, fmt.Errorf("request Neo protocol configuration: %w", err) + } + + initialBlock, err := b.GetBlockCount() + if err != nil { + return nil, fmt.Errorf("get current blockchain height: %w", err) + } + + blockCh, err := b.SubscribeToNewBlocks() + if err != nil { + return nil, fmt.Errorf("subscribe to new blocks of the chain: %w", err) + } + + res := &blockchainMonitor{ + logger: l, + blockchain: b, + blockInterval: time.Duration(ver.Protocol.MillisecondsPerBlock) * time.Millisecond, + chConnLost: make(chan struct{}), + } + + res.height.Store(initialBlock) + + go func() { + l.Info("listening to new blocks...") + for { + b, ok := <-blockCh + if !ok { + close(chNewBlock) + close(res.chConnLost) + l.Info("new blocks channel is closed, listening stopped") + return + } + + res.height.Store(b.Index) + + select { + case chNewBlock <- struct{}{}: + default: + } + + l.Info("new block arrived", zap.Uint32("height", b.Index)) + } + }() + + return res, nil +} + +// currentHeight returns current blockchain height. +func (x *blockchainMonitor) currentHeight() uint32 { + return x.height.Load() +} + +// waitForNextBlock blocks until blockchainMonitor encounters new block on the +// chain, underlying connection with the [Blockchain] is lost or provided +// context is done (returns context error). +func (x *blockchainMonitor) waitForNextBlock(ctx context.Context) error { + initialBlock := x.currentHeight() + + ticker := time.NewTicker(x.blockInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-x.chConnLost: + return errors.New("connection to the blockchain is lost") + case <-ticker.C: + if x.height.Load() > initialBlock { + return nil + } + } + } +} + +// readNNSOnChainState reads state of the NeoFS NNS contract in the given +// Blockchain. Returns both nil if contract is missing. +func readNNSOnChainState(b Blockchain) (*state.Contract, error) { + // NNS must always have ID=1 in the NeoFS Sidechain + const nnsContractID = 1 + res, err := b.GetContractStateByID(nnsContractID) + if err != nil { + if errors.Is(err, neorpc.ErrUnknownContract) { + return nil, nil + } + return nil, fmt.Errorf("read contract state by ID=%d: %w", nnsContractID, err) + } + return res, nil +} + +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() + }() +} + +var errInvalidContractDomainRecord = errors.New("invalid contract domain record") + +// readContractOnChainStateByDomainName reads address state of contract deployed +// in the given Blockchain and recorded in the NNS with the specified domain +// name. Returns errMissingDomain if domain doesn't exist. Returns +// errMissingDomainRecord if domain has no records. Returns +// errInvalidContractDomainRecord if domain record has invalid/unsupported +// format. Returns [neorpc.ErrUnknownContract] if contract is recorded in the NNS but +// missing in the Blockchain. +func readContractOnChainStateByDomainName(b Blockchain, nnsContract util.Uint160, domainName string) (*state.Contract, error) { + rec, err := lookupNNSDomainRecord(invoker.New(b, nil), nnsContract, domainName) + if err != nil { + return nil, err + } + + // historically two formats may occur + addr, err := util.Uint160DecodeStringLE(rec) + if err != nil { + addr, err = address.StringToUint160(rec) + if err != nil { + return nil, fmt.Errorf("%w: domain record '%s' neither NEO address nor little-endian hex-encoded script hash", errInvalidContractDomainRecord, rec) + } + } + + res, err := b.GetContractStateByHash(addr) + if err != nil { + return nil, fmt.Errorf("get contract by address=%s: %w", addr, err) + } + + return res, nil +}