From bad739258e36bc2b2d8e155b4265c0fd37bc6fea Mon Sep 17 00:00:00 2001
From: Pavel Karpy <carpawell@nspcc.ru>
Date: Wed, 10 Nov 2021 14:05:51 +0300
Subject: [PATCH] [#971] *: Add notification TX hash to neofs/netmap morph
 client calls

Add hash of the TX that generated notification
to neofs/netmap event structures. Adapt all
neofs/netmap wrapper calls to new structures.

Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
---
 cmd/neofs-node/config.go                      |  5 ++-
 cmd/neofs-node/netmap.go                      | 11 +++--
 pkg/innerring/innerring.go                    |  5 ++-
 .../processors/balance/process_assets.go      | 15 ++++---
 pkg/innerring/processors/governance/events.go | 23 ++++++++--
 .../processors/governance/handlers.go         | 11 ++++-
 .../processors/governance/process_update.go   | 43 ++++++++++++++++---
 .../processors/governance/processor.go        | 17 ++++++--
 .../processors/neofs/process_config.go        | 10 ++++-
 .../processors/netmap/internal_events.go      | 13 ++++++
 .../processors/netmap/process_cleanup.go      | 21 +++------
 .../processors/netmap/process_epoch.go        | 10 ++---
 .../processors/netmap/process_peers.go        | 14 +++++-
 pkg/innerring/state.go                        | 28 +++++++++---
 pkg/morph/event/balance/lock.go               | 13 +++++-
 pkg/morph/event/neofs/config.go               | 14 ++++++
 pkg/morph/event/netmap/epoch.go               | 15 ++++++-
 pkg/morph/event/netmap/update_peer.go         |  1 -
 pkg/morph/event/rolemanagement/designate.go   | 11 ++++-
 19 files changed, 220 insertions(+), 60 deletions(-)

diff --git a/cmd/neofs-node/config.go b/cmd/neofs-node/config.go
index d71ef9acee..4d2811f4c1 100644
--- a/cmd/neofs-node/config.go
+++ b/cmd/neofs-node/config.go
@@ -476,7 +476,10 @@ func (c *cfg) bootstrap() error {
 	ni := c.cfgNodeInfo.localInfo
 	ni.SetState(netmap.NodeStateOnline)
 
-	return c.cfgNetmap.wrapper.AddPeer(&ni)
+	prm := nmwrapper.AddPeerPrm{}
+	prm.SetNodeInfo(&ni)
+
+	return c.cfgNetmap.wrapper.AddPeer(prm)
 }
 
 // needBootstrap checks if local node should be registered in network on bootup.
diff --git a/cmd/neofs-node/netmap.go b/cmd/neofs-node/netmap.go
index db620b2165..81b2048d67 100644
--- a/cmd/neofs-node/netmap.go
+++ b/cmd/neofs-node/netmap.go
@@ -9,6 +9,7 @@ import (
 	netmapGRPC "github.com/nspcc-dev/neofs-api-go/v2/netmap/grpc"
 	"github.com/nspcc-dev/neofs-api-go/v2/refs"
 	"github.com/nspcc-dev/neofs-node/pkg/core/netmap"
+	"github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap/wrapper"
 	"github.com/nspcc-dev/neofs-node/pkg/morph/event"
 	netmapEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/netmap"
 	"github.com/nspcc-dev/neofs-node/pkg/network"
@@ -283,10 +284,12 @@ func (c *cfg) SetNetmapStatus(st control.NetmapStatus) error {
 
 	c.cfgNetmap.reBoostrapTurnedOff.Store(true)
 
-	return c.cfgNetmap.wrapper.UpdatePeerState(
-		c.key.PublicKey().Bytes(),
-		apiState,
-	)
+	prm := wrapper.UpdatePeerPrm{}
+
+	prm.SetKey(c.key.PublicKey().Bytes())
+	prm.SetState(apiState)
+
+	return c.cfgNetmap.wrapper.UpdatePeerState(prm)
 }
 
 type netInfo struct {
diff --git a/pkg/innerring/innerring.go b/pkg/innerring/innerring.go
index 1b7c1c02ae..37646cc510 100644
--- a/pkg/innerring/innerring.go
+++ b/pkg/innerring/innerring.go
@@ -191,8 +191,11 @@ func (s *Server) Start(ctx context.Context, intError chan<- error) (err error) {
 		}
 	}
 
+	prm := governance.VoteValidatorPrm{}
+	prm.Validators = s.predefinedValidators
+
 	// vote for sidechain validator if it is prepared in config
-	err = s.voteForSidechainValidator(s.predefinedValidators)
+	err = s.voteForSidechainValidator(prm)
 	if err != nil {
 		// we don't stop inner ring execution on this error
 		s.log.Warn("can't vote for prepared validators",
diff --git a/pkg/innerring/processors/balance/process_assets.go b/pkg/innerring/processors/balance/process_assets.go
index 5ee16cdf90..9dbc595630 100644
--- a/pkg/innerring/processors/balance/process_assets.go
+++ b/pkg/innerring/processors/balance/process_assets.go
@@ -1,6 +1,7 @@
 package balance
 
 import (
+	neofscontract "github.com/nspcc-dev/neofs-node/pkg/morph/client/neofs/wrapper"
 	balanceEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/balance"
 	"go.uber.org/zap"
 )
@@ -13,11 +14,15 @@ func (bp *Processor) processLock(lock *balanceEvent.Lock) {
 		return
 	}
 
-	err := bp.neofsClient.Cheque(
-		lock.ID(),
-		lock.User(),
-		bp.converter.ToFixed8(lock.Amount()),
-		lock.LockAccount())
+	prm := neofscontract.ChequePrm{}
+
+	prm.SetID(lock.ID())
+	prm.SetUser(lock.User())
+	prm.SetAmount(bp.converter.ToFixed8(lock.Amount()))
+	prm.SetLock(lock.LockAccount())
+	prm.SetHash(lock.TxHash())
+
+	err := bp.neofsClient.Cheque(prm)
 	if err != nil {
 		bp.log.Error("can't send lock asset tx", zap.Error(err))
 	}
diff --git a/pkg/innerring/processors/governance/events.go b/pkg/innerring/processors/governance/events.go
index a56fc47faa..76cfddc07e 100644
--- a/pkg/innerring/processors/governance/events.go
+++ b/pkg/innerring/processors/governance/events.go
@@ -1,11 +1,26 @@
 package governance
 
-// Sync is a event to start governance synchronization.
-type Sync struct{}
+import "github.com/nspcc-dev/neo-go/pkg/util"
+
+// Sync is an event to start governance synchronization.
+type Sync struct {
+	// txHash is used in notary environmental
+	// for calculating unique but same for
+	// all notification receivers values.
+	txHash util.Uint256
+}
+
+// TxHash returns hash of the TX that triggers
+// synchronization process.
+func (s Sync) TxHash() util.Uint256 {
+	return s.txHash
+}
 
 // MorphEvent implements Event interface.
 func (s Sync) MorphEvent() {}
 
-func NewSyncEvent() Sync {
-	return Sync{}
+// NewSyncEvent creates Sync event that was produced
+// in transaction with txHash hash.
+func NewSyncEvent(txHash util.Uint256) Sync {
+	return Sync{txHash: txHash}
 }
diff --git a/pkg/innerring/processors/governance/handlers.go b/pkg/innerring/processors/governance/handlers.go
index 9b6ceb8dea..24f460ab1e 100644
--- a/pkg/innerring/processors/governance/handlers.go
+++ b/pkg/innerring/processors/governance/handlers.go
@@ -3,22 +3,29 @@ package governance
 import (
 	"github.com/nspcc-dev/neo-go/pkg/core/native"
 	"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
+	"github.com/nspcc-dev/neo-go/pkg/util"
 	"github.com/nspcc-dev/neofs-node/pkg/morph/event"
 	"github.com/nspcc-dev/neofs-node/pkg/morph/event/rolemanagement"
 	"go.uber.org/zap"
 )
 
 func (gp *Processor) HandleAlphabetSync(e event.Event) {
-	var typ string
+	var (
+		typ  string
+		hash util.Uint256
+	)
 
 	switch et := e.(type) {
 	case Sync:
 		typ = "sync"
+		hash = et.TxHash()
 	case rolemanagement.Designate:
 		if et.Role != noderoles.NeoFSAlphabet {
 			return
 		}
+
 		typ = native.DesignationEventName
+		hash = et.TxHash
 	default:
 		return
 	}
@@ -27,7 +34,7 @@ func (gp *Processor) HandleAlphabetSync(e event.Event) {
 
 	// send event to the worker pool
 
-	err := gp.pool.Submit(func() { gp.processAlphabetSync() })
+	err := gp.pool.Submit(func() { gp.processAlphabetSync(hash) })
 	if err != nil {
 		// there system can be moved into controlled degradation stage
 		gp.log.Warn("governance worker pool drained",
diff --git a/pkg/innerring/processors/governance/process_update.go b/pkg/innerring/processors/governance/process_update.go
index a3de8ecb44..bd787c14e5 100644
--- a/pkg/innerring/processors/governance/process_update.go
+++ b/pkg/innerring/processors/governance/process_update.go
@@ -6,7 +6,12 @@ import (
 	"sort"
 	"strings"
 
+	"github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap/wrapper"
+
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+	"github.com/nspcc-dev/neo-go/pkg/util"
+	"github.com/nspcc-dev/neofs-node/pkg/morph/client"
+	neofscontract "github.com/nspcc-dev/neofs-node/pkg/morph/client/neofs/wrapper"
 	"go.uber.org/zap"
 )
 
@@ -14,7 +19,7 @@ const (
 	alphabetUpdateIDPrefix = "AlphabetUpdate"
 )
 
-func (gp *Processor) processAlphabetSync() {
+func (gp *Processor) processAlphabetSync(txHash util.Uint256) {
 	if !gp.alphabetState.IsAlphabet() {
 		gp.log.Info("non alphabet mode, ignore alphabet sync")
 		return
@@ -51,8 +56,13 @@ func (gp *Processor) processAlphabetSync() {
 		zap.String("new_alphabet", prettyKeys(newAlphabet)),
 	)
 
+	votePrm := VoteValidatorPrm{
+		Validators: newAlphabet,
+		Hash:       &txHash,
+	}
+
 	// 1. Vote to side chain committee via alphabet contracts.
-	err = gp.voter.VoteForSidechainValidator(newAlphabet)
+	err = gp.voter.VoteForSidechainValidator(votePrm)
 	if err != nil {
 		gp.log.Error("can't vote for side chain committee",
 			zap.String("error", err.Error()))
@@ -77,9 +87,19 @@ func (gp *Processor) processAlphabetSync() {
 			)
 
 			if gp.notaryDisabled {
-				err = gp.netmapClient.UpdateInnerRing(newInnerRing)
+				updPrm := wrapper.UpdateIRPrm{}
+
+				updPrm.SetKeys(newInnerRing)
+				updPrm.SetHash(txHash)
+
+				err = gp.netmapClient.UpdateInnerRing(updPrm)
 			} else {
-				err = gp.morphClient.UpdateNeoFSAlphabetList(newInnerRing)
+				updPrm := client.UpdateAlphabetListPrm{}
+
+				updPrm.SetList(newInnerRing)
+				updPrm.SetHash(txHash)
+
+				err = gp.morphClient.UpdateNeoFSAlphabetList(updPrm)
 			}
 
 			if err != nil {
@@ -91,7 +111,13 @@ func (gp *Processor) processAlphabetSync() {
 
 	if !gp.notaryDisabled {
 		// 3. Update notary role in side chain.
-		err = gp.morphClient.UpdateNotaryList(newAlphabet)
+
+		updPrm := client.UpdateNotaryListPrm{}
+
+		updPrm.SetList(newAlphabet)
+		updPrm.SetHash(txHash)
+
+		err = gp.morphClient.UpdateNotaryList(updPrm)
 		if err != nil {
 			gp.log.Error("can't update list of notary nodes in side chain",
 				zap.String("error", err.Error()))
@@ -106,7 +132,12 @@ func (gp *Processor) processAlphabetSync() {
 
 	id := append([]byte(alphabetUpdateIDPrefix), buf...)
 
-	err = gp.neofsClient.AlphabetUpdate(id, newAlphabet)
+	prm := neofscontract.AlphabetUpdatePrm{}
+
+	prm.SetID(id)
+	prm.SetPubs(newAlphabet)
+
+	err = gp.neofsClient.AlphabetUpdate(prm)
 	if err != nil {
 		gp.log.Error("can't update list of alphabet nodes in neofs contract",
 			zap.String("error", err.Error()))
diff --git a/pkg/innerring/processors/governance/processor.go b/pkg/innerring/processors/governance/processor.go
index e9c76817af..62cc00a69e 100644
--- a/pkg/innerring/processors/governance/processor.go
+++ b/pkg/innerring/processors/governance/processor.go
@@ -26,12 +26,21 @@ type (
 	AlphabetState interface {
 		IsAlphabet() bool
 	}
+)
 
-	// Voter is a callback interface for alphabet contract voting.
-	Voter interface {
-		VoteForSidechainValidator(keys keys.PublicKeys) error
-	}
+// VoteValidatorPrm groups parameters of the VoteForSidechainValidator
+// operation.
+type VoteValidatorPrm struct {
+	Validators keys.PublicKeys
+	Hash       *util.Uint256 // hash of the transaction that triggered voting
+}
 
+// Voter is a callback interface for alphabet contract voting.
+type Voter interface {
+	VoteForSidechainValidator(VoteValidatorPrm) error
+}
+
+type (
 	// EpochState is a callback interface for innerring global state.
 	EpochState interface {
 		EpochCounter() uint64
diff --git a/pkg/innerring/processors/neofs/process_config.go b/pkg/innerring/processors/neofs/process_config.go
index 3a494640db..ddfddd3cbd 100644
--- a/pkg/innerring/processors/neofs/process_config.go
+++ b/pkg/innerring/processors/neofs/process_config.go
@@ -1,6 +1,7 @@
 package neofs
 
 import (
+	"github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap/wrapper"
 	neofsEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/neofs"
 	"go.uber.org/zap"
 )
@@ -13,7 +14,14 @@ func (np *Processor) processConfig(config *neofsEvent.Config) {
 		return
 	}
 
-	err := np.netmapClient.SetConfig(config.ID(), config.Key(), config.Value())
+	prm := wrapper.SetConfigPrm{}
+
+	prm.SetID(config.ID())
+	prm.SetKey(config.Key())
+	prm.SetValue(config.Value())
+	prm.SetHash(config.TxHash())
+
+	err := np.netmapClient.SetConfig(prm)
 	if err != nil {
 		np.log.Error("can't relay set config event", zap.Error(err))
 	}
diff --git a/pkg/innerring/processors/netmap/internal_events.go b/pkg/innerring/processors/netmap/internal_events.go
index 6b3251fbf2..17db375dbd 100644
--- a/pkg/innerring/processors/netmap/internal_events.go
+++ b/pkg/innerring/processors/netmap/internal_events.go
@@ -1,8 +1,21 @@
 package netmap
 
+import "github.com/nspcc-dev/neo-go/pkg/util"
+
 // netmapCleanupTick is a event to remove offline nodes.
 type netmapCleanupTick struct {
 	epoch uint64
+
+	// txHash is used in notary environmental
+	// for calculating unique but same for
+	// all notification receivers values.
+	txHash util.Uint256
+}
+
+// TxHash returns hash of the TX that triggers
+// synchronization process.
+func (s netmapCleanupTick) TxHash() util.Uint256 {
+	return s.txHash
 }
 
 // MorphEvent implements Event interface.
diff --git a/pkg/innerring/processors/netmap/process_cleanup.go b/pkg/innerring/processors/netmap/process_cleanup.go
index 7363f21c28..1bd7cee06a 100644
--- a/pkg/innerring/processors/netmap/process_cleanup.go
+++ b/pkg/innerring/processors/netmap/process_cleanup.go
@@ -2,7 +2,7 @@ package netmap
 
 import (
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
-	netmapEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/netmap"
+	netmapclient "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap/wrapper"
 	"github.com/nspcc-dev/neofs-sdk-go/netmap"
 	"go.uber.org/zap"
 )
@@ -25,20 +25,13 @@ func (np *Processor) processNetmapCleanupTick(ev netmapCleanupTick) {
 
 		np.log.Info("vote to remove node from netmap", zap.String("key", s))
 
-		if np.notaryDisabled {
-			err = np.netmapClient.UpdatePeerState(key.Bytes(), netmap.NodeStateOffline)
-		} else {
-			// use epoch as TX nonce to prevent collisions
-			err = np.netmapClient.Morph().NotaryInvoke(
-				np.netmapClient.ContractAddress(),
-				0,
-				uint32(ev.epoch),
-				netmapEvent.UpdateStateNotaryEvent,
-				int64(netmap.NodeStateOffline.ToV2()),
-				key.Bytes(),
-			)
-		}
+		prm := netmapclient.UpdatePeerPrm{}
 
+		prm.SetKey(key.Bytes())
+		prm.SetState(netmap.NodeStateOffline)
+		prm.SetHash(ev.TxHash())
+
+		err = np.netmapClient.UpdatePeerState(prm)
 		if err != nil {
 			np.log.Error("can't invoke netmap.UpdateState", zap.Error(err))
 		}
diff --git a/pkg/innerring/processors/netmap/process_epoch.go b/pkg/innerring/processors/netmap/process_epoch.go
index 1ee4c463a0..cad1fd6f42 100644
--- a/pkg/innerring/processors/netmap/process_epoch.go
+++ b/pkg/innerring/processors/netmap/process_epoch.go
@@ -10,8 +10,8 @@ import (
 
 // Process new epoch notification by setting global epoch value and resetting
 // local epoch timer.
-func (np *Processor) processNewEpoch(event netmapEvent.NewEpoch) {
-	epoch := event.EpochNumber()
+func (np *Processor) processNewEpoch(ev netmapEvent.NewEpoch) {
+	epoch := ev.EpochNumber()
 
 	epochDuration, err := np.netmapClient.EpochDuration()
 	if err != nil {
@@ -47,11 +47,11 @@ func (np *Processor) processNewEpoch(event netmapEvent.NewEpoch) {
 	}
 
 	np.netmapSnapshot.update(networkMap, epoch)
-	np.handleCleanupTick(netmapCleanupTick{epoch: epoch})
+	np.handleCleanupTick(netmapCleanupTick{epoch: epoch, txHash: ev.TxHash()})
 	np.handleNewAudit(audit.NewAuditStartEvent(epoch))
 	np.handleAuditSettlements(settlement.NewAuditEvent(epoch))
-	np.handleAlphabetSync(governance.NewSyncEvent())
-	np.handleNotaryDeposit(event)
+	np.handleAlphabetSync(governance.NewSyncEvent(ev.TxHash()))
+	np.handleNotaryDeposit(ev)
 }
 
 // Process new epoch tick by invoking new epoch method in network map contract.
diff --git a/pkg/innerring/processors/netmap/process_peers.go b/pkg/innerring/processors/netmap/process_peers.go
index 1fc3a0e712..3026e64568 100644
--- a/pkg/innerring/processors/netmap/process_peers.go
+++ b/pkg/innerring/processors/netmap/process_peers.go
@@ -5,6 +5,7 @@ import (
 	"sort"
 	"strings"
 
+	netmapclient "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap/wrapper"
 	netmapEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/netmap"
 	"github.com/nspcc-dev/neofs-sdk-go/netmap"
 	"go.uber.org/zap"
@@ -81,18 +82,22 @@ func (np *Processor) processAddPeer(ev netmapEvent.AddPeer) {
 		np.log.Info("approving network map candidate",
 			zap.String("key", keyString))
 
+		prm := netmapclient.AddPeerPrm{}
+		prm.SetNodeInfo(nodeInfo)
+
 		if nr := ev.NotaryRequest(); nr != nil {
 			// create new notary request with the original nonce
 			err = np.netmapClient.Morph().NotaryInvoke(
 				np.netmapClient.ContractAddress(),
 				0,
 				nr.MainTransaction.Nonce,
+				nil,
 				netmapEvent.AddPeerNotaryEvent,
 				nodeInfoBinary,
 			)
 		} else {
 			// notification event case
-			err = np.netmapClient.AddPeer(nodeInfo)
+			err = np.netmapClient.AddPeer(prm)
 		}
 
 		if err != nil {
@@ -126,7 +131,12 @@ func (np *Processor) processUpdatePeer(ev netmapEvent.UpdatePeer) {
 	if nr := ev.NotaryRequest(); nr != nil {
 		err = np.netmapClient.Morph().NotarySignAndInvokeTX(nr.MainTransaction)
 	} else {
-		err = np.netmapClient.UpdatePeerState(ev.PublicKey().Bytes(), ev.Status())
+		prm := netmapclient.UpdatePeerPrm{}
+
+		prm.SetState(ev.Status())
+		prm.SetKey(ev.PublicKey().Bytes())
+
+		err = np.netmapClient.UpdatePeerState(prm)
 	}
 	if err != nil {
 		np.log.Error("can't invoke netmap.UpdatePeer", zap.Error(err))
diff --git a/pkg/innerring/state.go b/pkg/innerring/state.go
index c3c069f258..89b9be78ea 100644
--- a/pkg/innerring/state.go
+++ b/pkg/innerring/state.go
@@ -4,8 +4,8 @@ import (
 	"fmt"
 	"sort"
 
-	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
 	"github.com/nspcc-dev/neo-go/pkg/util"
+	"github.com/nspcc-dev/neofs-node/pkg/innerring/processors/governance"
 	"github.com/nspcc-dev/neofs-node/pkg/services/audit"
 	control "github.com/nspcc-dev/neofs-node/pkg/services/control/ir"
 	"github.com/nspcc-dev/neofs-node/pkg/util/state"
@@ -88,7 +88,9 @@ func (s *Server) AlphabetIndex() int {
 	return int(index)
 }
 
-func (s *Server) voteForSidechainValidator(validators keys.PublicKeys) error {
+func (s *Server) voteForSidechainValidator(prm governance.VoteValidatorPrm) error {
+	validators := prm.Validators
+
 	index := s.InnerRingIndex()
 	if s.contracts.alphabet.indexOutOfRange(index) {
 		s.log.Info("ignore validator vote: node not in alphabet range")
@@ -104,9 +106,21 @@ func (s *Server) voteForSidechainValidator(validators keys.PublicKeys) error {
 
 	epoch := s.EpochCounter()
 
+	var (
+		nonce uint32 = 1
+		vub   uint32
+		err   error
+	)
+
+	if prm.Hash != nil {
+		nonce, vub, err = s.morphClient.CalculateNonceAndVUB(*prm.Hash)
+		if err != nil {
+			return fmt.Errorf("could not calculate nonce and `validUntilBlock` values: %w", err)
+		}
+	}
+
 	s.contracts.alphabet.iterate(func(letter GlagoliticLetter, contract util.Uint160) {
-		// FIXME: do not use constant nonce for alphabet NR: #844
-		err := s.morphClient.NotaryInvoke(contract, s.feeConfig.SideChainFee(), 1, voteMethod, int64(epoch), validators)
+		err := s.morphClient.NotaryInvoke(contract, s.feeConfig.SideChainFee(), nonce, &vub, voteMethod, int64(epoch), validators)
 		if err != nil {
 			s.log.Warn("can't invoke vote method in alphabet contract",
 				zap.Int8("alphabet_index", int8(letter)),
@@ -120,9 +134,9 @@ func (s *Server) voteForSidechainValidator(validators keys.PublicKeys) error {
 
 // VoteForSidechainValidator calls vote method on alphabet contracts with
 // provided list of keys.
-func (s *Server) VoteForSidechainValidator(validators keys.PublicKeys) error {
-	sort.Sort(validators)
-	return s.voteForSidechainValidator(validators)
+func (s *Server) VoteForSidechainValidator(prm governance.VoteValidatorPrm) error {
+	sort.Sort(prm.Validators)
+	return s.voteForSidechainValidator(prm)
 }
 
 // WriteReport composes audit result structure from audit report
diff --git a/pkg/morph/event/balance/lock.go b/pkg/morph/event/balance/lock.go
index 1e465b4e62..0fd122e124 100644
--- a/pkg/morph/event/balance/lock.go
+++ b/pkg/morph/event/balance/lock.go
@@ -17,6 +17,11 @@ type Lock struct {
 	lock   util.Uint160
 	amount int64 // Fixed16
 	until  int64
+
+	// txHash is used in notary environmental
+	// for calculating unique but same for
+	// all notification receivers values.
+	txHash util.Uint256
 }
 
 // MorphEvent implements Neo:Morph Event interface.
@@ -34,9 +39,13 @@ func (l Lock) LockAccount() util.Uint160 { return l.lock }
 // Amount of the locked assets.
 func (l Lock) Amount() int64 { return l.amount }
 
-// Until is a epoch before locked account exists.
+// Until is an epoch before locked account exists.
 func (l Lock) Until() int64 { return l.until }
 
+// TxHash returns hash of the TX with lock
+// notification.
+func (l Lock) TxHash() util.Uint256 { return l.txHash }
+
 // ParseLock from notification into lock structure.
 func ParseLock(e *subscriptions.NotificationEvent) (event.Event, error) {
 	var (
@@ -93,5 +102,7 @@ func ParseLock(e *subscriptions.NotificationEvent) (event.Event, error) {
 		return nil, fmt.Errorf("could not get lock deadline: %w", err)
 	}
 
+	ev.txHash = e.Container
+
 	return ev, nil
 }
diff --git a/pkg/morph/event/neofs/config.go b/pkg/morph/event/neofs/config.go
index 01efc955ef..0853617f91 100644
--- a/pkg/morph/event/neofs/config.go
+++ b/pkg/morph/event/neofs/config.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/nspcc-dev/neo-go/pkg/rpc/response/result/subscriptions"
+	"github.com/nspcc-dev/neo-go/pkg/util"
 	"github.com/nspcc-dev/neofs-node/pkg/morph/client"
 	"github.com/nspcc-dev/neofs-node/pkg/morph/event"
 )
@@ -12,6 +13,17 @@ type Config struct {
 	key   []byte
 	value []byte
 	id    []byte
+
+	// txHash is used in notary environmental
+	// for calculating unique but same for
+	// all notification receivers values.
+	txHash util.Uint256
+}
+
+// TxHash returns hash of the TX with new epoch
+// notification.
+func (u Config) TxHash() util.Uint256 {
+	return u.txHash
 }
 
 // MorphEvent implements Neo:Morph Event interface.
@@ -56,5 +68,7 @@ func ParseConfig(e *subscriptions.NotificationEvent) (event.Event, error) {
 		return nil, fmt.Errorf("could not get config value: %w", err)
 	}
 
+	ev.txHash = e.Container
+
 	return ev, nil
 }
diff --git a/pkg/morph/event/netmap/epoch.go b/pkg/morph/event/netmap/epoch.go
index 371a47ec6d..19922392cd 100644
--- a/pkg/morph/event/netmap/epoch.go
+++ b/pkg/morph/event/netmap/epoch.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/nspcc-dev/neo-go/pkg/rpc/response/result/subscriptions"
+	"github.com/nspcc-dev/neo-go/pkg/util"
 	"github.com/nspcc-dev/neofs-node/pkg/morph/client"
 	"github.com/nspcc-dev/neofs-node/pkg/morph/event"
 )
@@ -11,6 +12,11 @@ import (
 // NewEpoch is a new epoch Neo:Morph event.
 type NewEpoch struct {
 	num uint64
+
+	// txHash is used in notary environmental
+	// for calculating unique but same for
+	// all notification receivers values.
+	txHash util.Uint256
 }
 
 // MorphEvent implements Neo:Morph Event interface.
@@ -21,6 +27,12 @@ func (s NewEpoch) EpochNumber() uint64 {
 	return s.num
 }
 
+// TxHash returns hash of the TX with new epoch
+// notification.
+func (s NewEpoch) TxHash() util.Uint256 {
+	return s.txHash
+}
+
 // ParseNewEpoch is a parser of new epoch notification event.
 //
 // Result is type of NewEpoch.
@@ -40,6 +52,7 @@ func ParseNewEpoch(e *subscriptions.NotificationEvent) (event.Event, error) {
 	}
 
 	return NewEpoch{
-		num: uint64(prmEpochNum),
+		num:    uint64(prmEpochNum),
+		txHash: e.Container,
 	}, nil
 }
diff --git a/pkg/morph/event/netmap/update_peer.go b/pkg/morph/event/netmap/update_peer.go
index 7d0897d78f..6e7fa7395e 100644
--- a/pkg/morph/event/netmap/update_peer.go
+++ b/pkg/morph/event/netmap/update_peer.go
@@ -8,7 +8,6 @@ import (
 
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
 	"github.com/nspcc-dev/neo-go/pkg/network/payload"
-	"github.com/nspcc-dev/neofs-api-go/pkg/netmap"
 	v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
 	"github.com/nspcc-dev/neofs-node/pkg/morph/client"
 	"github.com/nspcc-dev/neofs-node/pkg/morph/event"
diff --git a/pkg/morph/event/rolemanagement/designate.go b/pkg/morph/event/rolemanagement/designate.go
index 1a0a1e304c..1fc7bcc732 100644
--- a/pkg/morph/event/rolemanagement/designate.go
+++ b/pkg/morph/event/rolemanagement/designate.go
@@ -5,12 +5,18 @@ import (
 
 	"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
 	"github.com/nspcc-dev/neo-go/pkg/rpc/response/result/subscriptions"
+	"github.com/nspcc-dev/neo-go/pkg/util"
 	"github.com/nspcc-dev/neofs-node/pkg/morph/event"
 )
 
 // Designate represents designation event of the mainnet RoleManagement contract.
 type Designate struct {
 	Role noderoles.Role
+
+	// TxHash is used in notary environmental
+	// for calculating unique but same for
+	// all notification receivers values.
+	TxHash util.Uint256
 }
 
 // MorphEvent implements Neo:Morph Event interface.
@@ -32,5 +38,8 @@ func ParseDesignate(e *subscriptions.NotificationEvent) (event.Event, error) {
 		return nil, fmt.Errorf("invalid stackitem type: %w", err)
 	}
 
-	return Designate{Role: noderoles.Role(bi.Int64())}, nil
+	return Designate{
+		Role:   noderoles.Role(bi.Int64()),
+		TxHash: e.Container,
+	}, nil
 }