diff --git a/cmd/neofs-ir/defaults.go b/cmd/neofs-ir/defaults.go index 516192baa..1a466ab49 100644 --- a/cmd/neofs-ir/defaults.go +++ b/cmd/neofs-ir/defaults.go @@ -80,6 +80,7 @@ func defaultConfiguration(cfg *viper.Viper) { cfg.SetDefault("workers.container", "10") cfg.SetDefault("workers.alphabet", "10") cfg.SetDefault("workers.reputation", "10") + cfg.SetDefault("workers.subnet", "10") cfg.SetDefault("netmap_cleaner.enabled", true) cfg.SetDefault("netmap_cleaner.threshold", 3) diff --git a/go.mod b/go.mod index 0806974ab..9f262722e 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,9 @@ require ( github.com/mr-tron/base58 v1.2.0 github.com/multiformats/go-multiaddr v0.4.0 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-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/panjf2000/ants/v2 v2.4.0 github.com/paulmach/orb v0.2.2 diff --git a/go.sum b/go.sum index 014590ee1..d3e97d650 100644 --- a/go.sum +++ b/go.sum @@ -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/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.97.4-pre.0.20211123163659-b25c3775e847 h1:9cRAXmYoMxWhCJnlh0c9twr6/1faDC57pdOv2ABxqLs= -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 h1:YEtImvVCnf7sTXJGd84e9SxzLG3LU6czJN/Q35XQGDY= +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.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= @@ -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/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-20211126123811-1dde267424aa h1:twlGWZU0cwWnJvLrVVQxnNcJBj16HsNoOuZPJ/pP/vE= -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 h1:/c6mALl+YZh6A/5IZ+yTgkAroHGgId1bgvTAdrTtiKg= +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.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE= 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.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/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= 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= @@ -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-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-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/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= diff --git a/pkg/innerring/innerring.go b/pkg/innerring/innerring.go index 07914d57c..471769db4 100644 --- a/pkg/innerring/innerring.go +++ b/pkg/innerring/innerring.go @@ -119,6 +119,8 @@ type ( // // TODO: unify with workers. runners []func(chan<- error) + + subnetHandler } 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") } + server.initSubnet(subnetConfig{ + queueSize: cfg.GetUint32("workers.subnet"), + }) + return server, nil } diff --git a/pkg/innerring/processors/netmap/handlers.go b/pkg/innerring/processors/netmap/handlers.go index 4d91f882b..c2a1b50c4 100644 --- a/pkg/innerring/processors/netmap/handlers.go +++ b/pkg/innerring/processors/netmap/handlers.go @@ -6,7 +6,7 @@ import ( timerEvent "github.com/nspcc-dev/neofs-node/pkg/innerring/timers" "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/morph/event/subnet" + subnetevents "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet" "go.uber.org/zap" ) @@ -102,7 +102,7 @@ func (np *Processor) handleCleanupTick(ev event.Event) { } func (np *Processor) handleRemoveNode(ev event.Event) { - removeNode := ev.(subnet.RemoveNode) + removeNode := ev.(subnetevents.RemoveNode) np.log.Info("notification", zap.String("type", "remove node from subnet"), diff --git a/pkg/innerring/processors/subnet/common.go b/pkg/innerring/processors/subnet/common.go new file mode 100644 index 000000000..08f3df0f1 --- /dev/null +++ b/pkg/innerring/processors/subnet/common.go @@ -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) +} diff --git a/pkg/innerring/processors/subnet/common_test.go b/pkg/innerring/processors/subnet/common_test.go new file mode 100644 index 000000000..3834114f8 --- /dev/null +++ b/pkg/innerring/processors/subnet/common_test.go @@ -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 +} diff --git a/pkg/innerring/processors/subnet/delete.go b/pkg/innerring/processors/subnet/delete.go new file mode 100644 index 000000000..e27d68093 --- /dev/null +++ b/pkg/innerring/processors/subnet/delete.go @@ -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 +} diff --git a/pkg/innerring/processors/subnet/delete_test.go b/pkg/innerring/processors/subnet/delete_test.go new file mode 100644 index 000000000..04db0e34a --- /dev/null +++ b/pkg/innerring/processors/subnet/delete_test.go @@ -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) +} diff --git a/pkg/innerring/processors/subnet/put.go b/pkg/innerring/processors/subnet/put.go new file mode 100644 index 000000000..5676446dc --- /dev/null +++ b/pkg/innerring/processors/subnet/put.go @@ -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 +} diff --git a/pkg/innerring/processors/subnet/put_test.go b/pkg/innerring/processors/subnet/put_test.go new file mode 100644 index 000000000..50c5f31b1 --- /dev/null +++ b/pkg/innerring/processors/subnet/put_test.go @@ -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) +} diff --git a/pkg/innerring/subnet.go b/pkg/innerring/subnet.go new file mode 100644 index 000000000..d709ce28b --- /dev/null +++ b/pkg/innerring/subnet.go @@ -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 +} diff --git a/pkg/morph/client/subnet/client.go b/pkg/morph/client/subnet/client.go new file mode 100644 index 000000000..bbd10dd09 --- /dev/null +++ b/pkg/morph/client/subnet/client.go @@ -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 +} diff --git a/pkg/morph/client/subnet/delete.go b/pkg/morph/client/subnet/delete.go new file mode 100644 index 000000000..e52ed8828 --- /dev/null +++ b/pkg/morph/client/subnet/delete.go @@ -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 +} diff --git a/pkg/morph/client/subnet/get.go b/pkg/morph/client/subnet/get.go new file mode 100644 index 000000000..b5fab8dff --- /dev/null +++ b/pkg/morph/client/subnet/get.go @@ -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 +} diff --git a/pkg/morph/client/subnet/put.go b/pkg/morph/client/subnet/put.go new file mode 100644 index 000000000..fc3dcfa02 --- /dev/null +++ b/pkg/morph/client/subnet/put.go @@ -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 +} diff --git a/pkg/morph/event/subnet/delete.go b/pkg/morph/event/subnet/delete.go new file mode 100644 index 000000000..4c174abb0 --- /dev/null +++ b/pkg/morph/event/subnet/delete.go @@ -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 +} diff --git a/pkg/morph/event/subnet/delete_test.go b/pkg/morph/event/subnet/delete_test.go new file mode 100644 index 000000000..28c9c8471 --- /dev/null +++ b/pkg/morph/event/subnet/delete_test.go @@ -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()) + }) +} diff --git a/pkg/morph/event/subnet/put.go b/pkg/morph/event/subnet/put.go new file mode 100644 index 000000000..ed962d101 --- /dev/null +++ b/pkg/morph/event/subnet/put.go @@ -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 +} diff --git a/pkg/morph/event/subnet/put_test.go b/pkg/morph/event/subnet/put_test.go new file mode 100644 index 000000000..85e7fc5cc --- /dev/null +++ b/pkg/morph/event/subnet/put_test.go @@ -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()) + }) +} diff --git a/pkg/morph/event/subnet/remove_node.go b/pkg/morph/event/subnet/remove_node.go index 81b2b7acc..229df2477 100644 --- a/pkg/morph/event/subnet/remove_node.go +++ b/pkg/morph/event/subnet/remove_node.go @@ -1,4 +1,4 @@ -package subnet +package subnetevents import ( "fmt" diff --git a/pkg/morph/event/subnet/remove_node_test.go b/pkg/morph/event/subnet/remove_node_test.go index 40c3d76cb..17410a012 100644 --- a/pkg/morph/event/subnet/remove_node_test.go +++ b/pkg/morph/event/subnet/remove_node_test.go @@ -1,4 +1,4 @@ -package subnet +package subnetevents_test import ( "testing" @@ -7,6 +7,7 @@ import ( "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/vm/stackitem" + . "github.com/nspcc-dev/neofs-node/pkg/morph/event/subnet" subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id" "github.com/stretchr/testify/require" )