[#973] ir: Listen and process Put/Delete events of Subnet contract

Define notification events, implement parsers. Add morph client of
Subnet contract. Listen, verify and approve events in Inner Ring app.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2021-11-26 15:19:20 +03:00 committed by LeL
parent 214c2bd0cb
commit 41eaa1e246
22 changed files with 1278 additions and 12 deletions

View file

@ -80,6 +80,7 @@ func defaultConfiguration(cfg *viper.Viper) {
cfg.SetDefault("workers.container", "10") cfg.SetDefault("workers.container", "10")
cfg.SetDefault("workers.alphabet", "10") cfg.SetDefault("workers.alphabet", "10")
cfg.SetDefault("workers.reputation", "10") cfg.SetDefault("workers.reputation", "10")
cfg.SetDefault("workers.subnet", "10")
cfg.SetDefault("netmap_cleaner.enabled", true) cfg.SetDefault("netmap_cleaner.enabled", true)
cfg.SetDefault("netmap_cleaner.threshold", 3) cfg.SetDefault("netmap_cleaner.threshold", 3)

4
go.mod
View file

@ -11,9 +11,9 @@ require (
github.com/mr-tron/base58 v1.2.0 github.com/mr-tron/base58 v1.2.0
github.com/multiformats/go-multiaddr v0.4.0 github.com/multiformats/go-multiaddr v0.4.0
github.com/nspcc-dev/hrw v1.0.9 github.com/nspcc-dev/hrw v1.0.9
github.com/nspcc-dev/neo-go v0.97.4-pre.0.20211123163659-b25c3775e847 github.com/nspcc-dev/neo-go v0.97.4-pre.0.20211126130906-87f5113c031b
github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211124141318-d93828f46514 github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211124141318-d93828f46514
github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211126123811-1dde267424aa github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211126125208-279a5a1e0bfe
github.com/nspcc-dev/tzhash v1.4.0 github.com/nspcc-dev/tzhash v1.4.0
github.com/panjf2000/ants/v2 v2.4.0 github.com/panjf2000/ants/v2 v2.4.0
github.com/paulmach/orb v0.2.2 github.com/paulmach/orb v0.2.2

10
go.sum
View file

@ -360,8 +360,8 @@ github.com/nspcc-dev/go-ordered-json v0.0.0-20210915112629-e1b6cce73d02/go.mod h
github.com/nspcc-dev/hrw v1.0.9 h1:17VcAuTtrstmFppBjfRiia4K2wA/ukXZhLFS8Y8rz5Y= github.com/nspcc-dev/hrw v1.0.9 h1:17VcAuTtrstmFppBjfRiia4K2wA/ukXZhLFS8Y8rz5Y=
github.com/nspcc-dev/hrw v1.0.9/go.mod h1:l/W2vx83vMQo6aStyx2AuZrJ+07lGv2JQGlVkPG06MU= github.com/nspcc-dev/hrw v1.0.9/go.mod h1:l/W2vx83vMQo6aStyx2AuZrJ+07lGv2JQGlVkPG06MU=
github.com/nspcc-dev/neo-go v0.73.1-pre.0.20200303142215-f5a1b928ce09/go.mod h1:pPYwPZ2ks+uMnlRLUyXOpLieaDQSEaf4NM3zHVbRjmg= github.com/nspcc-dev/neo-go v0.73.1-pre.0.20200303142215-f5a1b928ce09/go.mod h1:pPYwPZ2ks+uMnlRLUyXOpLieaDQSEaf4NM3zHVbRjmg=
github.com/nspcc-dev/neo-go v0.97.4-pre.0.20211123163659-b25c3775e847 h1:9cRAXmYoMxWhCJnlh0c9twr6/1faDC57pdOv2ABxqLs= github.com/nspcc-dev/neo-go v0.97.4-pre.0.20211126130906-87f5113c031b h1:YEtImvVCnf7sTXJGd84e9SxzLG3LU6czJN/Q35XQGDY=
github.com/nspcc-dev/neo-go v0.97.4-pre.0.20211123163659-b25c3775e847/go.mod h1:ThDwtZ1KXjpz2n0UyKkS0sEth5weNqMkZ2cWSIKaxaE= github.com/nspcc-dev/neo-go v0.97.4-pre.0.20211126130906-87f5113c031b/go.mod h1:ThDwtZ1KXjpz2n0UyKkS0sEth5weNqMkZ2cWSIKaxaE=
github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211118144033-580f6c5554ff/go.mod h1:oS8dycEh8PPf2Jjp6+8dlwWyEv2Dy77h/XhhcdxYEFs= github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211118144033-580f6c5554ff/go.mod h1:oS8dycEh8PPf2Jjp6+8dlwWyEv2Dy77h/XhhcdxYEFs=
github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211124141318-d93828f46514 h1:ccZ+d8nJa8H2IWCLlr6YSanqwtTP6YiWa8RmCtT8aOs= github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211124141318-d93828f46514 h1:ccZ+d8nJa8H2IWCLlr6YSanqwtTP6YiWa8RmCtT8aOs=
github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211124141318-d93828f46514/go.mod h1:oS8dycEh8PPf2Jjp6+8dlwWyEv2Dy77h/XhhcdxYEFs= github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211124141318-d93828f46514/go.mod h1:oS8dycEh8PPf2Jjp6+8dlwWyEv2Dy77h/XhhcdxYEFs=
@ -370,8 +370,8 @@ github.com/nspcc-dev/neofs-crypto v0.2.3/go.mod h1:8w16GEJbH6791ktVqHN9YRNH3s9BE
github.com/nspcc-dev/neofs-crypto v0.3.0 h1:zlr3pgoxuzrmGCxc5W8dGVfA9Rro8diFvVnBg0L4ifM= github.com/nspcc-dev/neofs-crypto v0.3.0 h1:zlr3pgoxuzrmGCxc5W8dGVfA9Rro8diFvVnBg0L4ifM=
github.com/nspcc-dev/neofs-crypto v0.3.0/go.mod h1:8w16GEJbH6791ktVqHN9YRNH3s9BEEKYxGhlFnp0cDw= github.com/nspcc-dev/neofs-crypto v0.3.0/go.mod h1:8w16GEJbH6791ktVqHN9YRNH3s9BEEKYxGhlFnp0cDw=
github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211123100340-d9317cbea191/go.mod h1:zpeoUpflmlq9aDOTC72+E2JyJZDNzoqbZJFtqClBuu4= github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211123100340-d9317cbea191/go.mod h1:zpeoUpflmlq9aDOTC72+E2JyJZDNzoqbZJFtqClBuu4=
github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211126123811-1dde267424aa h1:twlGWZU0cwWnJvLrVVQxnNcJBj16HsNoOuZPJ/pP/vE= github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211126125208-279a5a1e0bfe h1:/c6mALl+YZh6A/5IZ+yTgkAroHGgId1bgvTAdrTtiKg=
github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211126123811-1dde267424aa/go.mod h1:kyRFPFVQ7z5oeqNxawVk05E4w+fkbkWbgBIRH/wlvRQ= github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211126125208-279a5a1e0bfe/go.mod h1:kyRFPFVQ7z5oeqNxawVk05E4w+fkbkWbgBIRH/wlvRQ=
github.com/nspcc-dev/rfc6979 v0.1.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso= github.com/nspcc-dev/rfc6979 v0.1.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso=
github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE= github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE=
github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso= github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso=
@ -525,7 +525,6 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
@ -548,7 +547,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

View file

@ -119,6 +119,8 @@ type (
// //
// TODO: unify with workers. // TODO: unify with workers.
runners []func(chan<- error) runners []func(chan<- error)
subnetHandler
} }
chainParams struct { chainParams struct {
@ -857,6 +859,10 @@ func New(ctx context.Context, log *zap.Logger, cfg *viper.Viper) (*Server, error
log.Info("no Control server endpoint specified, service is disabled") log.Info("no Control server endpoint specified, service is disabled")
} }
server.initSubnet(subnetConfig{
queueSize: cfg.GetUint32("workers.subnet"),
})
return server, nil return server, nil
} }

View file

@ -6,7 +6,7 @@ import (
timerEvent "github.com/nspcc-dev/neofs-node/pkg/innerring/timers" timerEvent "github.com/nspcc-dev/neofs-node/pkg/innerring/timers"
"github.com/nspcc-dev/neofs-node/pkg/morph/event" "github.com/nspcc-dev/neofs-node/pkg/morph/event"
netmapEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/netmap" netmapEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/netmap"
"github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet" subnetevents "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -102,7 +102,7 @@ func (np *Processor) handleCleanupTick(ev event.Event) {
} }
func (np *Processor) handleRemoveNode(ev event.Event) { func (np *Processor) handleRemoveNode(ev event.Event) {
removeNode := ev.(subnet.RemoveNode) removeNode := ev.(subnetevents.RemoveNode)
np.log.Info("notification", np.log.Info("notification",
zap.String("type", "remove node from subnet"), zap.String("type", "remove node from subnet"),

View file

@ -0,0 +1,22 @@
package subnetevents
import (
"fmt"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
)
// common interface of subnet notifications with subnet ID.
type eventWithID interface {
// ReadID reads identifier of the subnet.
ReadID(*subnetid.ID) error
}
// an error which is returned on zero subnet operation attempt.
type zeroSubnetOp struct {
op string
}
func (x zeroSubnetOp) Error() string {
return fmt.Sprintf("zero subnet %s", x.op)
}

View file

@ -0,0 +1,19 @@
package subnetevents
import subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
type idEvent struct {
id subnetid.ID
idErr error
}
func (x idEvent) ReadID(id *subnetid.ID) error {
if x.idErr != nil {
return x.idErr
}
*id = x.id
return nil
}

View file

@ -0,0 +1,44 @@
package subnetevents
import (
"fmt"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
)
// Delete represents notification about NeoFS subnet removal.
// Generated by a contract when intending to delete a subnet.
type Delete interface {
// Contains ID of the subnet to be removed.
eventWithID
}
// DeleteValidator asserts intent to remove a subnet.
type DeleteValidator struct{}
// Assert processes the attempt to remove a subnet. Approves the removal through nil return.
//
// All read errors of Delete are forwarded.
//
// Returns an error on:
// * zero subnet creation;
// * empty ID or different from the one wired into info;
// * empty owner ID or different from the one wired into info.
func (x DeleteValidator) Assert(event Delete) error {
var err error
// read ID
var id subnetid.ID
if err = event.ReadID(&id); err != nil {
return fmt.Errorf("read ID: %w", err)
}
// prevent zero subnet removal
if subnetid.IsZero(id) {
return zeroSubnetOp{
op: "removal",
}
}
return nil
}

View file

@ -0,0 +1,44 @@
package subnetevents
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
)
type delete struct {
idEvent
}
func TestDeleteValidator_Assert(t *testing.T) {
var (
v DeleteValidator
e delete
err error
)
// read ID error
e.idErr = errors.New("id err")
err = v.Assert(e)
require.ErrorIs(t, err, e.idErr)
e.idErr = nil
// zero subnet ID
subnetid.MakeZero(&e.id)
err = v.Assert(e)
require.ErrorAs(t, err, new(zeroSubnetOp))
const idNum = 13
e.id.SetNumber(idNum)
err = v.Assert(e)
require.NoError(t, err)
}

View file

@ -0,0 +1,82 @@
package subnetevents
import (
"errors"
"fmt"
"github.com/nspcc-dev/neofs-sdk-go/owner"
"github.com/nspcc-dev/neofs-sdk-go/subnet"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
)
// Put represents notification about NeoFS subnet creation.
// Generated by a contract when intending to create a subnet.
type Put interface {
// Contains ID of the subnet to be created.
eventWithID
// ReadCreator reads user ID of the subnet creator.
// Returns an error if ID is missing.
ReadCreator(id *owner.ID) error
// ReadInfo reads information about subnet to be created.
ReadInfo(info *subnet.Info) error
}
// PutValidator asserts intent to create a subnet.
type PutValidator struct{}
// errDiffOwner is returned when subnet owners differ.
var errDiffOwner = errors.New("diff subnet owners")
// errDiffID is returned when subnet IDs differ.
var errDiffID = errors.New("diff subnet IDs")
// Assert processes the attempt to create a subnet. Approves the creation through nil return.
//
// All read errors of Put are forwarded.
//
// Returns an error on:
// * zero subnet creation;
// * empty ID or different from the one wired into info;
// * empty owner ID or different from the one wired into info.
func (x PutValidator) Assert(event Put) error {
var err error
// read ID
var id subnetid.ID
if err = event.ReadID(&id); err != nil {
return fmt.Errorf("read ID: %w", err)
}
// prevent zero subnet creation
if subnetid.IsZero(id) {
return zeroSubnetOp{
op: "creation",
}
}
// read creator's user ID in NeoFS system
var creator owner.ID
if err = event.ReadCreator(&creator); err != nil {
return fmt.Errorf("read creator: %w", err)
}
// read information about the subnet
var info subnet.Info
if err = event.ReadInfo(&info); err != nil {
return fmt.Errorf("read info: %w", err)
}
// check if explicit ID equals to the one from info
if !subnet.IDEquals(info, id) {
return errDiffID
}
// check if explicit creator equals to the one from info
if !subnet.IsOwner(info, creator) {
return errDiffOwner
}
return nil
}

View file

@ -0,0 +1,115 @@
package subnetevents
import (
"errors"
"testing"
ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test"
"github.com/stretchr/testify/require"
"github.com/nspcc-dev/neofs-sdk-go/owner"
"github.com/nspcc-dev/neofs-sdk-go/subnet"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
)
type put struct {
idEvent
creator owner.ID
creatorErr error
info subnet.Info
infoErr error
}
func (x put) ReadCreator(id *owner.ID) error {
if x.creatorErr != nil {
return x.creatorErr
}
*id = x.creator
return nil
}
func (x put) ReadInfo(info *subnet.Info) error {
if x.infoErr != nil {
return x.infoErr
}
*info = x.info
return nil
}
func TestPutValidator_Assert(t *testing.T) {
var (
v PutValidator
e put
err error
)
// read ID error
e.idErr = errors.New("id err")
err = v.Assert(e)
require.ErrorIs(t, err, e.idErr)
e.idErr = nil
// zero subnet ID
subnetid.MakeZero(&e.id)
err = v.Assert(e)
require.ErrorAs(t, err, new(zeroSubnetOp))
const idNum = 13
e.id.SetNumber(idNum)
// read creator error
e.creatorErr = errors.New("creator err")
err = v.Assert(e)
require.ErrorIs(t, err, e.creatorErr)
e.creatorErr = nil
// read info error
e.infoErr = errors.New("info err")
err = v.Assert(e)
require.ErrorIs(t, err, e.infoErr)
e.infoErr = nil
// diff explicit ID and the one in info
var id2 subnetid.ID
id2.SetNumber(idNum + 1)
e.info.SetID(id2)
err = v.Assert(e)
require.ErrorIs(t, err, errDiffID)
e.info.SetID(e.id)
// diff explicit creator and the one in info
var creator2 owner.ID
creator2 = *ownertest.GenerateID()
e.info.SetOwner(creator2)
err = v.Assert(e)
require.ErrorIs(t, err, errDiffOwner)
e.info.SetOwner(e.creator)
err = v.Assert(e)
require.NoError(t, err)
}

332
pkg/innerring/subnet.go Normal file
View file

@ -0,0 +1,332 @@
package innerring
import (
"crypto/ecdsa"
"crypto/elliptic"
"errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/core/mempoolevent"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
irsubnet "github.com/nspcc-dev/neofs-node/pkg/innerring/processors/subnet"
morphsubnet "github.com/nspcc-dev/neofs-node/pkg/morph/client/subnet"
"github.com/nspcc-dev/neofs-node/pkg/morph/event"
subnetevents "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet"
"github.com/nspcc-dev/neofs-node/pkg/util"
"github.com/nspcc-dev/neofs-sdk-go/owner"
"github.com/nspcc-dev/neofs-sdk-go/subnet"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
"github.com/panjf2000/ants/v2"
"go.uber.org/zap"
)
// IR server's component to handle Subnet contract notifications.
type subnetHandler struct {
workerPool util.WorkerPool
morphClient morphsubnet.Client
putValidator irsubnet.PutValidator
delValidator irsubnet.DeleteValidator
}
// configuration of subnet component.
type subnetConfig struct {
queueSize uint32
}
// makes IR server to catch Subnet notifications from sidechain listener,
// and to release corresponding processing queue on stop.
func (s *Server) initSubnet(cfg subnetConfig) {
s.registerStarter(func() error {
var err error
// initialize queue for processing of the events from Subnet contract
s.subnetHandler.workerPool, err = ants.NewPool(int(cfg.queueSize), ants.WithNonblocking(true))
if err != nil {
return fmt.Errorf("subnet queue initialization: %w", err)
}
// initialize morph client of Subnet contract
clientMode := morphsubnet.NotaryAlphabet
if s.sideNotaryConfig.disabled {
clientMode = morphsubnet.NonNotary
}
var initPrm morphsubnet.InitPrm
initPrm.SetBaseClient(s.morphClient)
initPrm.SetContractAddress(s.contracts.subnet)
initPrm.SetMode(clientMode)
err = s.subnetHandler.morphClient.Init(initPrm)
if err != nil {
return fmt.Errorf("init morph subnet client: %w", err)
}
s.listenSubnet()
return nil
})
s.registerCloser(func() error {
s.stopSubnet()
return nil
})
}
// releases the Subnet contract notification processing queue.
func (s *Server) stopSubnet() {
s.workerPool.Release()
}
// names of listened notification events from Subnet contract.
const (
// subnet creation
subnetCreateEvName = "put"
// subnet removal
subnetRemoveEvName = "delete"
)
// makes IR server to listen notifications of Subnet contract.
// All required resources must be initialized before (initSubnet).
// Works in one of two modes (configured): notary and non-notary.
//
// All handlers are executed only if local node is an alphabet one.
//
// Events (notary):
// * put (parser: subnetevents.ParseNotaryPut, handler: catchSubnetCreation);
// * delete (parser: subnetevents.ParseNotaryDelete, handler: catchSubnetRemoval).
//
// Events (non-notary):
// * put (parser: subnetevents.ParsePut, handler: catchSubnetCreation);
// * delete (parser: subnetevents.ParseDelete, handler: catchSubnetCreation).
func (s *Server) listenSubnet() {
if s.sideNotaryConfig.disabled {
s.listenSubnetWithoutNotary()
return
}
var (
parserInfo event.NotaryParserInfo
handlerInfo event.NotaryHandlerInfo
)
parserInfo.SetScriptHash(s.contracts.subnet)
handlerInfo.SetScriptHash(s.contracts.subnet)
listenEvent := func(notifyName string, parser event.NotaryParser, handler event.Handler) {
notifyTyp := event.NotaryTypeFromString(notifyName)
parserInfo.SetMempoolType(mempoolevent.TransactionAdded)
handlerInfo.SetMempoolType(mempoolevent.TransactionAdded)
parserInfo.SetParser(parser)
handlerInfo.SetHandler(handler)
parserInfo.SetRequestType(notifyTyp)
handlerInfo.SetRequestType(notifyTyp)
s.morphListener.SetNotaryParser(parserInfo)
s.morphListener.RegisterNotaryHandler(handlerInfo)
}
// subnet creation
listenEvent(subnetCreateEvName, subnetevents.ParseNotaryPut, s.onlyAlphabetEventHandler(s.catchSubnetCreation))
// subnet removal
listenEvent(subnetRemoveEvName, subnetevents.ParseNotaryDelete, s.onlyAlphabetEventHandler(s.catchSubnetRemoval))
}
func (s *Server) listenSubnetWithoutNotary() {
var (
parserInfo event.NotificationParserInfo
handlerInfo event.NotificationHandlerInfo
)
parserInfo.SetScriptHash(s.contracts.subnet)
handlerInfo.SetScriptHash(s.contracts.subnet)
listenEvent := func(notifyName string, parser event.NotificationParser, handler event.Handler) {
notifyTyp := event.TypeFromString(notifyName)
parserInfo.SetType(notifyTyp)
handlerInfo.SetType(notifyTyp)
parserInfo.SetParser(parser)
handlerInfo.SetHandler(handler)
s.morphListener.SetNotificationParser(parserInfo)
s.morphListener.RegisterNotificationHandler(handlerInfo)
}
// subnet creation
listenEvent(subnetCreateEvName, subnetevents.ParsePut, s.onlyAlphabetEventHandler(s.catchSubnetCreation))
// subnet removal
listenEvent(subnetRemoveEvName, subnetevents.ParseDelete, s.onlyAlphabetEventHandler(s.catchSubnetRemoval))
}
// catchSubnetCreation catches event of subnet creation from listener and queues the processing.
func (s *Server) catchSubnetCreation(e event.Event) {
err := s.subnetHandler.workerPool.Submit(func() {
s.handleSubnetCreation(e)
})
if err != nil {
s.log.Error("subnet creation queue failure",
zap.String("error", err.Error()),
)
}
}
// implements irsubnet.Put event interface required by irsubnet.PutValidator.
type putSubnetEvent struct {
ev subnetevents.Put
}
// ReadID unmarshals subnet ID from a binary NeoFS API protocol's format.
func (x putSubnetEvent) ReadID(id *subnetid.ID) error {
return id.Unmarshal(x.ev.ID())
}
var errMissingSubnetOwner = errors.New("missing subnet owner")
// ReadCreator unmarshals subnet creator from a binary NeoFS API protocol's format.
// Returns an error if byte array is empty.
func (x putSubnetEvent) ReadCreator(id *owner.ID) error {
data := x.ev.Owner()
if len(data) == 0 {
return errMissingSubnetOwner
}
key, err := keys.NewPublicKeyFromBytes(data, elliptic.P256())
if err != nil {
return err
}
wal, err := owner.NEO3WalletFromPublicKey((*ecdsa.PublicKey)(key))
if err != nil {
return err
}
// it would be better if we could do it not like this
*id = *owner.NewIDFromNeo3Wallet(wal)
return nil
}
// ReadInfo unmarshal subnet info from a binary NeoFS API protocol's format.
func (x putSubnetEvent) ReadInfo(info *subnet.Info) error {
return info.Unmarshal(x.ev.Info())
}
// handleSubnetCreation handles event of subnet creation parsed via subnetevents.ParsePut.
//
// Validates the event using irsubnet.PutValidator. Logs message about (dis)agreement.
func (s *Server) handleSubnetCreation(e event.Event) {
putEv := e.(subnetevents.Put) // panic occurs only if we registered handler incorrectly
err := s.subnetHandler.putValidator.Assert(putSubnetEvent{
ev: putEv,
})
if err != nil {
s.log.Info("discard subnet creation",
zap.String("reason", err.Error()),
)
return
}
notaryMainTx := putEv.NotaryMainTx()
isNotary := notaryMainTx != nil
if isNotary {
// re-sign notary request
err = s.morphClient.NotarySignAndInvokeTX(notaryMainTx)
} else {
// send new transaction
var prm morphsubnet.PutPrm
prm.SetID(putEv.ID())
prm.SetOwner(putEv.Owner())
prm.SetInfo(putEv.Info())
prm.SetTxHash(putEv.TxHash())
_, err = s.subnetHandler.morphClient.Put(prm)
}
if err != nil {
s.log.Error("approve subnet creation",
zap.Bool("notary", isNotary),
zap.String("error", err.Error()),
)
return
}
}
// catchSubnetRemoval catches event of subnet removal from listener and queues the processing.
func (s *Server) catchSubnetRemoval(e event.Event) {
err := s.subnetHandler.workerPool.Submit(func() {
s.handleSubnetCreation(e)
})
if err != nil {
s.log.Error("subnet removal queue failure",
zap.String("error", err.Error()),
)
}
}
// implements irsubnet.Delete event interface required by irsubnet.DeleteValidator.
type deleteSubnetEvent struct {
ev subnetevents.Delete
}
// ReadID unmarshals subnet ID from a binary NeoFS API protocol's format.
func (x deleteSubnetEvent) ReadID(id *subnetid.ID) error {
return id.Unmarshal(x.ev.ID())
}
// handleSubnetRemoval handles event of subnet removal parsed via subnetevents.ParseDelete.
func (s *Server) handleSubnetRemoval(e event.Event) {
delEv := e.(subnetevents.Delete) // panic occurs only if we registered handler incorrectly
err := s.subnetHandler.delValidator.Assert(deleteSubnetEvent{
ev: delEv,
})
if err != nil {
s.log.Info("discard subnet removal",
zap.String("reason", err.Error()),
)
return
}
notaryMainTx := delEv.NotaryMainTx()
isNotary := notaryMainTx != nil
if isNotary {
// re-sign notary request
err = s.morphClient.NotarySignAndInvokeTX(notaryMainTx)
} else {
// send new transaction
var prm morphsubnet.DeletePrm
prm.SetID(delEv.ID())
prm.SetTxHash(delEv.TxHash())
_, err = s.subnetHandler.morphClient.Delete(prm)
}
if err != nil {
s.log.Error("approve subnet removal",
zap.Bool("notary", isNotary),
zap.String("error", err.Error()),
)
return
}
// TODO: handle removal of the subnet in netmap candidates
}

View file

@ -0,0 +1,91 @@
package morphsubnet
import (
"fmt"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neofs-node/pkg/morph/client"
)
// Client represents Subnet contract client.
//
// Client should be preliminary initialized (see Init method).
type Client struct {
client *client.StaticClient
}
// InitPrm groups parameters of Client's initialization.
type InitPrm struct {
base *client.Client
addr util.Uint160
modeSet bool
mode Mode
}
// SetBaseClient sets basic morph client.
func (x *InitPrm) SetBaseClient(base *client.Client) {
x.base = base
}
// SetContractAddress sets address of Subnet contract in NeoFS sidechain.
func (x *InitPrm) SetContractAddress(addr util.Uint160) {
x.addr = addr
}
// Mode regulates client work mode.
type Mode uint8
const (
_ Mode = iota
// NonNotary makes client to work in non-notary environment.
NonNotary
// NotaryAlphabet makes client to use its internal key for signing the notary requests.
NotaryAlphabet
// NotaryNonAlphabet makes client to not use its internal key for signing the notary requests.
NotaryNonAlphabet
lastMode
)
// SetMode makes client to work with non-notary sidechain.
// By default, NonNotary is used.
func (x *InitPrm) SetMode(mode Mode) {
x.modeSet = true
x.mode = mode
}
// Init initializes client with specified parameters.
//
// Base client must be set.
func (x *Client) Init(prm InitPrm) error {
if prm.base == nil {
panic("missing base morph client")
}
if !prm.modeSet {
prm.mode = NonNotary
}
var opts []client.StaticClientOption
switch prm.mode {
default:
panic(fmt.Sprintf("invalid work mode %d", prm.mode))
case NonNotary:
case NotaryNonAlphabet:
opts = []client.StaticClientOption{client.TryNotary()}
case NotaryAlphabet:
opts = []client.StaticClientOption{client.TryNotary(), client.AsAlphabet()}
}
var err error
x.client, err = client.NewStatic(prm.base, prm.addr, 0, opts...)
return err
}

View file

@ -0,0 +1,40 @@
package morphsubnet
import (
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neofs-node/pkg/morph/client"
)
// DeletePrm groups parameters of Delete method of Subnet contract.
type DeletePrm struct {
cliPrm client.InvokePrm
args [1]interface{}
}
// SetTxHash sets hash of the transaction which spawned the notification.
// Ignore this parameter for new requests.
func (x *DeletePrm) SetTxHash(hash util.Uint256) {
x.cliPrm.SetHash(hash)
}
// SetID sets identifier of the subnet to be removed in a binary NeoFS API protocol format.
func (x *DeletePrm) SetID(id []byte) {
x.args[0] = id
}
// DeleteRes groups resulting values of Delete method of Subnet contract.
type DeleteRes struct{}
// Delete removes subnet though the call of the corresponding method of the Subnet contract.
func (x Client) Delete(prm DeletePrm) (*DeleteRes, error) {
prm.cliPrm.SetMethod("delete")
prm.cliPrm.SetArgs(prm.args[:]...)
err := x.client.Invoke(prm.cliPrm)
if err != nil {
return nil, err
}
return new(DeleteRes), nil
}

View file

@ -0,0 +1,57 @@
package morphsubnet
import (
"errors"
"fmt"
"github.com/nspcc-dev/neofs-node/pkg/morph/client"
)
// GetPrm groups parameters of Get method of Subnet contract.
type GetPrm struct {
cliPrm client.TestInvokePrm
args [1]interface{}
}
// SetID sets identifier of the subnet to be read in a binary NeoFS API protocol format.
func (x *GetPrm) SetID(id []byte) {
x.args[0] = id
}
// GetRes groups resulting values of Get method of Subnet contract.
type GetRes struct {
info []byte
}
// Info returns information about the subnet in a binary format of NeoFS API protocol.
func (x GetRes) Info() []byte {
return x.info
}
var errEmptyResponse = errors.New("empty response")
// Get reads the subnet through the call of the corresponding method of the Subnet contract.
func (x *Client) Get(prm GetPrm) (*GetRes, error) {
prm.cliPrm.SetMethod("get")
prm.cliPrm.SetArgs(prm.args[:]...)
res, err := x.client.TestInvoke(prm.cliPrm)
if err != nil {
fmt.Println()
return nil, err
}
if len(res) == 0 {
return nil, errEmptyResponse
}
data, err := client.BytesFromStackItem(res[0])
if err != nil {
return nil, err
}
return &GetRes{
info: data,
}, nil
}

View file

@ -0,0 +1,50 @@
package morphsubnet
import (
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neofs-node/pkg/morph/client"
)
// PutPrm groups parameters of Put method of Subnet contract.
type PutPrm struct {
cliPrm client.InvokePrm
args [3]interface{}
}
// SetTxHash sets hash of the transaction which spawned the notification.
// Ignore this parameter for new requests.
func (x *PutPrm) SetTxHash(hash util.Uint256) {
x.cliPrm.SetHash(hash)
}
// SetID sets identifier of the created subnet in a binary NeoFS API protocol format.
func (x *PutPrm) SetID(id []byte) {
x.args[0] = id
}
// SetOwner sets identifier of the subnet owner in a binary NeoFS API protocol format.
func (x *PutPrm) SetOwner(id []byte) {
x.args[1] = id
}
// SetInfo sets information about the created subnet in a binary NeoFS API protocol format.
func (x *PutPrm) SetInfo(id []byte) {
x.args[2] = id
}
// PutRes groups resulting values of Put method of Subnet contract.
type PutRes struct{}
// Put creates subnet though the call of the corresponding method of the Subnet contract.
func (x Client) Put(prm PutPrm) (*PutRes, error) {
prm.cliPrm.SetMethod("put")
prm.cliPrm.SetArgs(prm.args[:]...)
err := x.client.Invoke(prm.cliPrm)
if err != nil {
return nil, err
}
return new(PutRes), nil
}

View file

@ -0,0 +1,109 @@
package subnetevents
import (
"fmt"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/network/payload"
"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"
)
// Delete structures information about the notification generated by Delete method of Subnet contract.
type Delete struct {
notaryRequest *payload.P2PNotaryRequest
txHash util.Uint256
id []byte
}
// MorphEvent implements Neo:Morph Event interface.
func (Delete) MorphEvent() {}
// ID returns identifier of the removed subnet in a binary format of NeoFS API protocol.
func (x Delete) ID() []byte {
return x.id
}
// TxHash returns hash of the transaction which thrown the notification event.
// Makes sense only in non-notary environments (see NotaryMainTx).
func (x Delete) TxHash() util.Uint256 {
return x.txHash
}
// NotaryMainTx returns main transaction of the request in the Notary service.
// Returns nil in non-notary environments.
func (x Delete) NotaryMainTx() *transaction.Transaction {
if x.notaryRequest != nil {
return x.notaryRequest.MainTransaction
}
return nil
}
const itemNumDelete = 1
// ParseDelete parses the notification about the removal of a subnet which has been thrown
// by the appropriate method of the Subnet contract.
//
// Resulting event is of Delete type.
func ParseDelete(e *subscriptions.NotificationEvent) (event.Event, error) {
var (
ev Delete
err error
)
items, err := event.ParseStackArray(e)
if err != nil {
return nil, fmt.Errorf("parse stack array: %w", err)
}
if ln := len(items); ln != itemNumDelete {
return nil, event.WrongNumberOfParameters(itemNumDelete, ln)
}
// parse ID
ev.id, err = client.BytesFromStackItem(items[0])
if err != nil {
return nil, fmt.Errorf("id item: %w", err)
}
ev.txHash = e.Container
return ev, nil
}
// ParseNotaryDelete parses the notary notification about the removal of a subnet which has been
// thrown by the appropriate method of the Subnet contract.
//
// Resulting event is of Delete type.
func ParseNotaryDelete(e event.NotaryEvent) (event.Event, error) {
var ev Delete
ev.notaryRequest = e.Raw()
if ev.notaryRequest == nil {
panic(fmt.Sprintf("nil %T in notary environment", ev.notaryRequest))
}
var (
err error
prms = e.Params()
)
if ln := len(prms); ln != itemNumDelete {
return nil, event.WrongNumberOfParameters(itemNumDelete, ln)
}
ev.id, err = event.BytesFromOpcode(prms[0])
if err != nil {
return nil, fmt.Errorf("id param: %w", err)
}
ev.notaryRequest = e.Raw()
return ev, nil
}

View file

@ -0,0 +1,42 @@
package subnetevents_test
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
subnetevents "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet"
"github.com/stretchr/testify/require"
)
func TestParseDelete(t *testing.T) {
id := []byte("id")
t.Run("wrong number of items", func(t *testing.T) {
prms := []stackitem.Item{
stackitem.NewByteArray(nil),
stackitem.NewByteArray(nil),
}
_, err := subnetevents.ParseDelete(createNotifyEventFromItems(prms))
require.Error(t, err)
})
t.Run("wrong id item", func(t *testing.T) {
_, err := subnetevents.ParseDelete(createNotifyEventFromItems([]stackitem.Item{
stackitem.NewMap(),
}))
require.Error(t, err)
})
t.Run("correct behavior", func(t *testing.T) {
ev, err := subnetevents.ParseDelete(createNotifyEventFromItems([]stackitem.Item{
stackitem.NewByteArray(id),
}))
require.NoError(t, err)
v := ev.(subnetevents.Delete)
require.Equal(t, id, v.ID())
})
}

View file

@ -0,0 +1,144 @@
package subnetevents
import (
"fmt"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/network/payload"
"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"
)
// Put structures information about the notification generated by Put method of Subnet contract.
type Put struct {
notaryRequest *payload.P2PNotaryRequest
txHash util.Uint256
id []byte
ownerID []byte
info []byte
}
// MorphEvent implements Neo:Morph Event interface.
func (Put) MorphEvent() {}
// ID returns identifier of the creating subnet in a binary format of NeoFS API protocol.
func (x Put) ID() []byte {
return x.id
}
// Owner returns subnet owner's ID in a binary format of NeoFS API protocol.
func (x Put) Owner() []byte {
return x.ownerID
}
// Info returns information about the subnet in a binary format of NeoFS API protocol.
func (x Put) Info() []byte {
return x.info
}
// TxHash returns hash of the transaction which thrown the notification event.
// Makes sense only in non-notary environments (see NotaryMainTx).
func (x Put) TxHash() util.Uint256 {
return x.txHash
}
// NotaryMainTx returns main transaction of the request in the Notary service.
// Returns nil in non-notary environments.
func (x Put) NotaryMainTx() *transaction.Transaction {
if x.notaryRequest != nil {
return x.notaryRequest.MainTransaction
}
return nil
}
// number of items in notification about subnet creation.
const itemNumPut = 3
// ParsePut parses the notification about the creation of a subnet which has been thrown
// by the appropriate method of the subnet contract.
//
// Resulting event is of Put type.
func ParsePut(e *subscriptions.NotificationEvent) (event.Event, error) {
var (
put Put
err error
)
items, err := event.ParseStackArray(e)
if err != nil {
return nil, fmt.Errorf("parse stack array: %w", err)
}
if ln := len(items); ln != itemNumPut {
return nil, event.WrongNumberOfParameters(itemNumPut, ln)
}
// parse ID
put.id, err = client.BytesFromStackItem(items[0])
if err != nil {
return nil, fmt.Errorf("id item: %w", err)
}
// parse owner
put.ownerID, err = client.BytesFromStackItem(items[1])
if err != nil {
return nil, fmt.Errorf("owner item: %w", err)
}
// parse public key
put.info, err = client.BytesFromStackItem(items[2])
if err != nil {
return nil, fmt.Errorf("info item: %w", err)
}
put.txHash = e.Container
return put, nil
}
// ParseNotaryPut parses the notary notification about the creation of a subnet which has been
// thrown by the appropriate method of the subnet contract.
//
// Resulting event is of Put type.
func ParseNotaryPut(e event.NotaryEvent) (event.Event, error) {
var put Put
put.notaryRequest = e.Raw()
if put.notaryRequest == nil {
panic(fmt.Sprintf("nil %T in notary environment", put.notaryRequest))
}
var (
err error
prms = e.Params()
)
if ln := len(prms); ln != itemNumPut {
return nil, event.WrongNumberOfParameters(itemNumPut, ln)
}
put.info, err = event.BytesFromOpcode(prms[0])
if err != nil {
return nil, fmt.Errorf("info param: %w", err)
}
put.ownerID, err = event.BytesFromOpcode(prms[1])
if err != nil {
return nil, fmt.Errorf("creator param: %w", err)
}
put.id, err = event.BytesFromOpcode(prms[2])
if err != nil {
return nil, fmt.Errorf("id param: %w", err)
}
return put, nil
}

View file

@ -0,0 +1,69 @@
package subnetevents_test
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
subnetevents "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet"
"github.com/stretchr/testify/require"
)
func TestParsePut(t *testing.T) {
var (
id = []byte("id")
owner = []byte("owner")
info = []byte("info")
)
t.Run("wrong number of items", func(t *testing.T) {
prms := []stackitem.Item{
stackitem.NewByteArray(nil),
stackitem.NewByteArray(nil),
}
_, err := subnetevents.ParsePut(createNotifyEventFromItems(prms))
require.Error(t, err)
})
t.Run("wrong id item", func(t *testing.T) {
_, err := subnetevents.ParsePut(createNotifyEventFromItems([]stackitem.Item{
stackitem.NewMap(),
}))
require.Error(t, err)
})
t.Run("wrong owner item", func(t *testing.T) {
_, err := subnetevents.ParsePut(createNotifyEventFromItems([]stackitem.Item{
stackitem.NewByteArray(id),
stackitem.NewMap(),
}))
require.Error(t, err)
})
t.Run("wrong info item", func(t *testing.T) {
_, err := subnetevents.ParsePut(createNotifyEventFromItems([]stackitem.Item{
stackitem.NewByteArray(id),
stackitem.NewByteArray(owner),
stackitem.NewMap(),
}))
require.Error(t, err)
})
t.Run("correct behavior", func(t *testing.T) {
ev, err := subnetevents.ParsePut(createNotifyEventFromItems([]stackitem.Item{
stackitem.NewByteArray(id),
stackitem.NewByteArray(owner),
stackitem.NewByteArray(info),
}))
require.NoError(t, err)
v := ev.(subnetevents.Put)
require.Equal(t, id, v.ID())
require.Equal(t, owner, v.Owner())
require.Equal(t, info, v.Info())
})
}

View file

@ -1,4 +1,4 @@
package subnet package subnetevents
import ( import (
"fmt" "fmt"

View file

@ -1,4 +1,4 @@
package subnet package subnetevents_test
import ( import (
"testing" "testing"
@ -7,6 +7,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result/subscriptions" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result/subscriptions"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
. "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id" subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )