diff --git a/CHANGELOG.md b/CHANGELOG.md index c5205e24b0..9d6213b4b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Changelog for NeoFS Node - IR `experimental.allow_ec` config option (#3570) - SN `pprof.enable_block` and `pprof.enable_mutex` options (#3655) - `neofs-adm fschain load-report` command (#3649) +- SN now supports new `getInfo` and `createV2` methods of the Container contract (#3670) +- IR now supports container creation requests submitted via new `createV2` contract method (#3670) +- IR structures containers in the contract iteratively (#3670) ### Fixed - Write cache using too much CPU (#3642) @@ -28,6 +31,7 @@ Changelog for NeoFS Node - Pre-0.46.0 write cache format migration (#3647) ### Updated +- `github.com/nspcc-dev/neofs-contract` dependency to `XXX` (#3670) ### Updating from v0.49.1 Erasure coding is available in experimental mode. To enable it, set diff --git a/cmd/neofs-node/container.go b/cmd/neofs-node/container.go index 3dde9bbb53..c875b03238 100644 --- a/cmd/neofs-node/container.go +++ b/cmd/neofs-node/container.go @@ -385,26 +385,15 @@ func (x *containersInChain) List(id user.ID) ([]cid.ID, error) { } func (x *containersInChain) Put(cnr containerSDK.Container, pub, sig []byte, st *session.Container) (cid.ID, error) { - data := cnr.Marshal() - d := cnr.ReadDomain() - var prm cntClient.PutPrm - prm.SetContainer(data) - prm.SetName(d.Name()) - prm.SetZone(d.Zone()) + prm.SetContainer(cnr) prm.SetKey(pub) prm.SetSignature(sig) if st != nil { prm.SetToken(st.Marshal()) } - if v := cnr.Attribute("__NEOFS__METAINFO_CONSISTENCY"); v == "optimistic" || v == "strict" { - prm.EnableMeta() - } - if err := x.cCli.Put(prm); err != nil { - return cid.ID{}, err - } - return cid.NewFromMarshalledContainer(data), nil + return x.cCli.Put(prm) } func (x *containersInChain) Delete(id cid.ID, pub, sig []byte, st *session.Container) error { diff --git a/go.mod b/go.mod index dc541bbd1d..a0ed2aaeaf 100644 --- a/go.mod +++ b/go.mod @@ -19,9 +19,9 @@ require ( github.com/nspcc-dev/bbolt v0.0.0-20250911202005-807225ebb0c8 github.com/nspcc-dev/hrw/v2 v2.0.4 github.com/nspcc-dev/locode-db v0.8.1 - github.com/nspcc-dev/neo-go v0.113.0 + github.com/nspcc-dev/neo-go v0.114.0 github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea - github.com/nspcc-dev/neofs-contract v0.24.0 + github.com/nspcc-dev/neofs-contract v0.25.1-0.20251118090121-b2011710f11d github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.15.0.20251015122943-b38583ddd311 github.com/nspcc-dev/tzhash v1.8.3 github.com/panjf2000/ants/v2 v2.11.3 @@ -46,7 +46,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/consensys/gnark-crypto v0.19.0 // indirect + github.com/consensys/gnark-crypto v0.19.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/crypto/ripemd160 v1.0.2 // indirect @@ -70,7 +70,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nspcc-dev/dbft v0.4.0 // indirect github.com/nspcc-dev/go-ordered-json v0.0.0-20250911084817-6fb4472993d1 // indirect - github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20250923153235-ffb84619d02f // indirect + github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251112080609-3c8e29c66609 // indirect github.com/nspcc-dev/rfc6979 v0.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect diff --git a/go.sum b/go.sum index 736a591435..5ad2179106 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/consensys/gnark-crypto v0.19.0 h1:zXCqeY2txSaMl6G5wFpZzMWJU9HPNh8qxPnYJ1BL9vA= -github.com/consensys/gnark-crypto v0.19.0/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= +github.com/consensys/gnark-crypto v0.19.1 h1:FWO1JDs7A2OajswzwMG7f8l2Zrxc/yOkxSTByKTc3O0= +github.com/consensys/gnark-crypto v0.19.1/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -191,14 +191,14 @@ github.com/nspcc-dev/hrw/v2 v2.0.4 h1:o3Zh/2aF+IgGpvt414f46Ya20WG9u9vWxVd16ErFI8 github.com/nspcc-dev/hrw/v2 v2.0.4/go.mod h1:dUjOx27zTTvoPmT5EG25vSSWL2tKS7ndAa2TPTiZwFo= github.com/nspcc-dev/locode-db v0.8.1 h1:BadCnDPfIbQXbi+X/m7DRhbiau81QelM/bD4QiiCGsI= github.com/nspcc-dev/locode-db v0.8.1/go.mod h1:PtAASXSG4D4Oz0js9elzTyTr8GLpOJO20qFL881Nims= -github.com/nspcc-dev/neo-go v0.113.0 h1:Xedd5hlN+sCQuaCFBm+0PPDWgHuw7c6/zNlhgqwBVfA= -github.com/nspcc-dev/neo-go v0.113.0/go.mod h1:wcWOspKlqzay5XGkCUV1x0AVB/ZAmiKu3RhcB+O7DVM= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20250923153235-ffb84619d02f h1:EOV4T/lbNIdUwAYBay3XbV3fL2Tq4TUvwg+GnwDX5aw= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20250923153235-ffb84619d02f/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= +github.com/nspcc-dev/neo-go v0.114.0 h1:JxyLGlQGtzrfWvhdrUa35BGzBaadwPtLdNL5ehfOF2k= +github.com/nspcc-dev/neo-go v0.114.0/go.mod h1:visra3tXvGBgBfhMizRGEB+bUI5a/zoeqr5WQRKXFGQ= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251112080609-3c8e29c66609 h1:9jH0IXFw8rjBgBVWSJbWeEHf7XDjANBnmEas49rdAH8= +github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251112080609-3c8e29c66609/go.mod h1:X2spkE8hK/l08CYulOF19fpK5n3p2xO0L1GnJFIywQg= github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea h1:mK0EMGLvunXcFyq7fBURS/CsN4MH+4nlYiqn6pTwWAU= github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea/go.mod h1:YzhD4EZmC9Z/PNyd7ysC7WXgIgURc9uCG1UWDeV027Y= -github.com/nspcc-dev/neofs-contract v0.24.0 h1:lQHtfRc00WEhW9qcnVNbM2sMa4oCBQ5v7vcunJKk9rA= -github.com/nspcc-dev/neofs-contract v0.24.0/go.mod h1:PPxjwRiK6hhXPXduvyojEqLMHNpgPaF+rULPhdFlzDg= +github.com/nspcc-dev/neofs-contract v0.25.1-0.20251118090121-b2011710f11d h1:RVatfw2zdmLOekUwZlF3U+WSRRcnkLrRVi5IacPvpTU= +github.com/nspcc-dev/neofs-contract v0.25.1-0.20251118090121-b2011710f11d/go.mod h1:uqTaIPQCIouOyflW4aRQAyl4w88GhxYosJ74wvS4AEQ= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.15.0.20251015122943-b38583ddd311 h1:iHjokyLIiOW7zvaNZZPmch0c1tghwkfOniL+JOt+F7M= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.15.0.20251015122943-b38583ddd311/go.mod h1:Vukuf6qDOQESOWAx5yOjYtVC5wdsQp3hiZrxbJIa2fs= github.com/nspcc-dev/rfc6979 v0.2.4 h1:NBgsdCjhLpEPJZqmC9rciMZDcSY297po2smeaRjw57k= diff --git a/pkg/innerring/innerring.go b/pkg/innerring/innerring.go index 07d80c8cff..66a101004e 100644 --- a/pkg/innerring/innerring.go +++ b/pkg/innerring/innerring.go @@ -88,7 +88,8 @@ type ( withoutMainNet bool // runtime processors - netmapProcessor *netmap.Processor + netmapProcessor *netmap.Processor + containerProcessor *container.Processor workers []func(context.Context) @@ -229,6 +230,12 @@ func (s *Server) Start(ctx context.Context, intError chan<- error) (err error) { go s.fsChainListener.ListenWithError(ctx, fsChainErr) // listen for neo:fs events go s.mainnetListener.ListenWithError(ctx, mainnnetErr) // listen for neo:mainnet events + go func() { + if err := s.containerProcessor.AddContainerStructs(ctx); err != nil { + fsChainErr <- fmt.Errorf("structurize containers in the contract: %w", err) + } + }() + s.startWorkers(ctx) return nil @@ -820,7 +827,7 @@ func New(ctx context.Context, log *zap.Logger, cfg *config.Config, errChan chan< } // container processor - containerProcessor, err := container.New(&container.Params{ + server.containerProcessor, err = container.New(&container.Params{ Log: log, PoolSize: cfg.Workers.Container, AlphabetState: server, @@ -833,7 +840,7 @@ func New(ctx context.Context, log *zap.Logger, cfg *config.Config, errChan chan< return nil, err } - err = bindFSChainProcessor(containerProcessor, server) + err = bindFSChainProcessor(server.containerProcessor, server) if err != nil { return nil, err } diff --git a/pkg/innerring/processors/container/handlers.go b/pkg/innerring/processors/container/handlers.go index 3f90954a05..0fa285d852 100644 --- a/pkg/innerring/processors/container/handlers.go +++ b/pkg/innerring/processors/container/handlers.go @@ -10,6 +10,15 @@ import ( "go.uber.org/zap" ) +func (cp *Processor) handleCreationRequest(ev event.Event) { + err := cp.pool.Submit(func() { cp.processCreateContainerRequest(ev.(containerEvent.CreateContainerV2Request)) }) + if err != nil { + // there system can be moved into controlled degradation stage + cp.log.Warn("container processor worker pool drained", + zap.Int("capacity", cp.pool.Cap())) + } +} + func (cp *Processor) handlePut(ev event.Event) { req, ok := ev.(containerEvent.CreateContainerRequest) if !ok { @@ -35,7 +44,7 @@ func (cp *Processor) handlePut(ev event.Event) { // send an event to the worker pool - err := cp.pool.Submit(func() { cp.processContainerPut(req) }) + err := cp.pool.Submit(func() { cp.processContainerPut(req, id) }) if err != nil { // there system can be moved into controlled degradation stage cp.log.Warn("container processor worker pool drained", diff --git a/pkg/innerring/processors/container/process_container.go b/pkg/innerring/processors/container/process_container.go index f2ed24e383..13673c33e8 100644 --- a/pkg/innerring/processors/container/process_container.go +++ b/pkg/innerring/processors/container/process_container.go @@ -5,7 +5,9 @@ import ( "fmt" "strings" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/network/payload" + fschaincontracts "github.com/nspcc-dev/neofs-node/pkg/morph/contracts" "github.com/nspcc-dev/neofs-node/pkg/morph/event" containerEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/container" containerSDK "github.com/nspcc-dev/neofs-sdk-go/container" @@ -14,6 +16,31 @@ import ( "go.uber.org/zap" ) +func (cp *Processor) processCreateContainerRequest(req containerEvent.CreateContainerV2Request) { + if !cp.alphabetState.IsAlphabet() { + cp.log.Info("non alphabet mode, ignore container creation request") + return + } + + cnr, err := fschaincontracts.ContainerFromStruct(req.Container) + if err != nil { + cp.log.Error("invalid container struct in creation request", zap.Error(err)) + return + } + + cnrBytes := cnr.Marshal() + id := cid.NewFromMarshalledContainer(cnrBytes) + + err = cp.checkPutContainer(cnr, cnrBytes, req.SessionToken, req.InvocationScript, req.VerificationScript, "", "") + if err != nil { + cp.log.Error("container creation request failed check", + zap.Stringer("container", id), zap.Error(err)) + return + } + + cp.approvePutContainer(req.MainTransaction, cnr, id) +} + // putEvent is a common interface of Put and PutNamed event. type putEvent interface { event.Event @@ -24,28 +51,23 @@ type putEvent interface { NotaryRequest() *payload.P2PNotaryRequest } -type putContainerContext struct { - e containerEvent.CreateContainerRequest - - // must be filled when verifying raw data from e - cID cid.ID - cnr containerSDK.Container - d containerSDK.Domain -} - // Process a new container from the user by checking the container sanity // and sending approve tx back to the FS chain. -func (cp *Processor) processContainerPut(req containerEvent.CreateContainerRequest) { +func (cp *Processor) processContainerPut(req containerEvent.CreateContainerRequest, id cid.ID) { if !cp.alphabetState.IsAlphabet() { cp.log.Info("non alphabet mode, ignore container put") return } - ctx := &putContainerContext{ - e: req, + var cnr containerSDK.Container + if err := cnr.Unmarshal(req.Container); err != nil { + cp.log.Error("put container check failed", + zap.Error(fmt.Errorf("invalid binary container: %w", err)), + ) + return } - err := cp.checkPutContainer(ctx) + err := cp.checkPutContainer(cnr, req.Container, req.SessionToken, req.InvocationScript, req.VerificationScript, req.DomainName, req.DomainZone) if err != nil { cp.log.Error("put container check failed", zap.Error(err), @@ -54,7 +76,7 @@ func (cp *Processor) processContainerPut(req containerEvent.CreateContainerReque return } - cp.approvePutContainer(ctx) + cp.approvePutContainer(req.MainTransaction, cnr, id) } const ( @@ -69,16 +91,8 @@ var allowedSystemAttributes = map[string]struct{}{ sysAttrChainMeta: {}, } -func (cp *Processor) checkPutContainer(ctx *putContainerContext) error { - binCnr := ctx.e.Container - ctx.cID = cid.NewFromMarshalledContainer(binCnr) - - err := ctx.cnr.Unmarshal(binCnr) - if err != nil { - return fmt.Errorf("invalid binary container: %w", err) - } - - for k := range ctx.cnr.Attributes() { +func (cp *Processor) checkPutContainer(cnr containerSDK.Container, cnrBytes, sessionToken, invocScript, verifScript []byte, domainName, domainZone string) error { + for k := range cnr.Attributes() { if strings.HasPrefix(k, sysAttrPrefix) { if _, ok := allowedSystemAttributes[k]; !ok { return fmt.Errorf("system attribute %s is not allowed", k) @@ -90,54 +104,53 @@ func (cp *Processor) checkPutContainer(ctx *putContainerContext) error { } } - ecRules := ctx.cnr.PlacementPolicy().ECRules() + ecRules := cnr.PlacementPolicy().ECRules() if !cp.allowEC && len(ecRules) > 0 { return errors.New("EC rules are not supported yet") } - if len(ecRules) > 0 && ctx.cnr.PlacementPolicy().NumberOfReplicas() > 0 { + if len(ecRules) > 0 && cnr.PlacementPolicy().NumberOfReplicas() > 0 { return errors.New("REP+EC rules are not supported yet") } - err = cp.verifySignature(signatureVerificationData{ - ownerContainer: ctx.cnr.Owner(), + err := cp.verifySignature(signatureVerificationData{ + ownerContainer: cnr.Owner(), verb: session.VerbContainerPut, - binTokenSession: ctx.e.SessionToken, - verifScript: ctx.e.VerificationScript, - invocScript: ctx.e.InvocationScript, - signedData: binCnr, + binTokenSession: sessionToken, + verifScript: verifScript, + invocScript: invocScript, + signedData: cnrBytes, }) if err != nil { return fmt.Errorf("auth container creation: %w", err) } - if err = ctx.cnr.PlacementPolicy().Verify(); err != nil { + if err = cnr.PlacementPolicy().Verify(); err != nil { return fmt.Errorf("invalid storage policy: %w", err) } // check homomorphic hashing setting - err = checkHomomorphicHashing(cp.netState, ctx.cnr) + err = checkHomomorphicHashing(cp.netState, cnr) if err != nil { return fmt.Errorf("incorrect homomorphic hashing setting: %w", err) } - // check native name and zone - err = checkNNS(ctx, ctx.cnr) - if err != nil { - return fmt.Errorf("NNS: %w", err) + if domainZone != "" { // if PutNamed event => check if values in-container domain name and zone correspond to args + err = checkNNS(cnr, domainName, domainZone) + if err != nil { + return fmt.Errorf("NNS: %w", err) + } } return nil } -func (cp *Processor) approvePutContainer(ctx *putContainerContext) { - l := cp.log.With(zap.Stringer("cID", ctx.cID)) +func (cp *Processor) approvePutContainer(mainTx transaction.Transaction, cnr containerSDK.Container, id cid.ID) { + l := cp.log.With(zap.Stringer("cID", id)) l.Debug("approving new container...") - e := ctx.e - var err error - err = cp.cnrClient.Morph().NotarySignAndInvokeTX(&e.MainTransaction, true) + err = cp.cnrClient.Morph().NotarySignAndInvokeTX(&mainTx, true) if err != nil { l.Error("could not approve put container", @@ -152,8 +165,8 @@ func (cp *Processor) approvePutContainer(ctx *putContainerContext) { return } - policy := ctx.cnr.PlacementPolicy() - vectors, err := nm.ContainerNodes(policy, ctx.cID) + policy := cnr.PlacementPolicy() + vectors, err := nm.ContainerNodes(policy, id) if err != nil { l.Error("could not build placement for Container contract update", zap.Error(err)) return @@ -168,7 +181,7 @@ func (cp *Processor) approvePutContainer(ctx *putContainerContext) { replicas[i] = 1 // each EC part is stored in a single copy } - err = cp.cnrClient.UpdateContainerPlacement(ctx.cID, vectors, replicas) + err = cp.cnrClient.UpdateContainerPlacement(id, vectors, replicas) if err != nil { l.Error("could not update Container contract", zap.Error(err)) return @@ -238,19 +251,16 @@ func (cp *Processor) approveDeleteContainer(e containerEvent.RemoveContainerRequ } } -func checkNNS(ctx *putContainerContext, cnr containerSDK.Container) error { +func checkNNS(cnr containerSDK.Container, name, zone string) error { // fetch domain info - ctx.d = cnr.ReadDomain() + d := cnr.ReadDomain() - // if PutNamed event => check if values in container correspond to args - if ctx.e.DomainName != "" { - if ctx.e.DomainName != ctx.d.Name() { - return fmt.Errorf("names differ %s/%s", ctx.e.DomainName, ctx.d.Name()) - } + if name != d.Name() { + return fmt.Errorf("names differ %s/%s", name, d.Name()) + } - if ctx.e.DomainZone != ctx.d.Zone() { - return fmt.Errorf("zones differ %s/%s", ctx.e.DomainZone, ctx.d.Zone()) - } + if zone != d.Zone() { + return fmt.Errorf("zones differ %s/%s", zone, d.Zone()) } return nil diff --git a/pkg/innerring/processors/container/processor.go b/pkg/innerring/processors/container/processor.go index 3beff00702..0ea0d533aa 100644 --- a/pkg/innerring/processors/container/processor.go +++ b/pkg/innerring/processors/container/processor.go @@ -1,9 +1,13 @@ package container import ( + "context" "errors" "fmt" + "time" + "github.com/nspcc-dev/neo-go/pkg/neorpc" + "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" "github.com/nspcc-dev/neofs-node/pkg/morph/client/container" fschaincontracts "github.com/nspcc-dev/neofs-node/pkg/morph/contracts" "github.com/nspcc-dev/neofs-node/pkg/morph/event" @@ -137,6 +141,10 @@ func (cp *Processor) ListenerNotaryParsers() []event.NotaryParserInfo { p.SetParser(containerEvent.RestoreCreateContainerRequest) pp = append(pp, p) + p.SetRequestType(fschaincontracts.CreateContainerV2Method) + p.SetParser(containerEvent.RestoreCreateContainerV2Request) + pp = append(pp, p) + // container delete p.SetRequestType(containerEvent.DeleteNotaryEvent) p.SetParser(containerEvent.ParseDeleteNotary) @@ -167,6 +175,10 @@ func (cp *Processor) ListenerNotaryParsers() []event.NotaryParserInfo { p.SetParser(containerEvent.ParseObjectPut) pp = append(pp, p) + // migrate protobuf->struct + p.SetRequestType(fschaincontracts.AddContainerStructsMethod) + p.SetParser(containerEvent.RestoreAddStructsRequest) + return pp } @@ -194,6 +206,10 @@ func (cp *Processor) ListenerNotaryHandlers() []event.NotaryHandlerInfo { h.SetHandler(cp.handlePut) hh = append(hh, h) + h.SetRequestType(fschaincontracts.CreateContainerV2Method) + h.SetHandler(cp.handleCreationRequest) + hh = append(hh, h) + // container delete h.SetRequestType(containerEvent.DeleteNotaryEvent) h.SetHandler(cp.handleDelete) @@ -224,6 +240,21 @@ func (cp *Processor) ListenerNotaryHandlers() []event.NotaryHandlerInfo { h.SetHandler(cp.handleObjectPut) hh = append(hh, h) + // migrate protobuf->struct + h.SetRequestType(fschaincontracts.AddContainerStructsMethod) + h.SetHandler(func(ev event.Event) { + cp.log.Info("received notary tx migrating containers' protobuf->struct, signing...") + + req := ev.(containerEvent.AddStructsRequest) + err := cp.cnrClient.Morph().NotarySignAndInvokeTX(&req.MainTransaction, false) + if err != nil { + cp.log.Error("failed to sign notary tx migrating containers' protobuf->struct", zap.Error(err)) + return + } + + cp.log.Info("notary tx migrating containers' protobuf->struct signed successfully") + }) + return hh } @@ -231,3 +262,47 @@ func (cp *Processor) ListenerNotaryHandlers() []event.NotaryHandlerInfo { func (cp *Processor) TimersHandlers() []event.NotificationHandlerInfo { return nil } + +// AddContainerStructs iteratively calls the contract to add structured storage +// items for containers. +func (cp *Processor) AddContainerStructs(ctx context.Context) error { + cp.log.Info("structuring containers in the contract...") + + cnrContract := cp.cnrClient.ContractAddress() + fsChain := cp.cnrClient.Morph() + for ; ; time.Sleep(5 * time.Second) { + txRes, err := fsChain.CallNotary(ctx, cnrContract, fschaincontracts.AddContainerStructsMethod) + if err != nil { + if !errors.Is(err, neorpc.ErrInsufficientFunds) { + return fmt.Errorf("notary call %s contract method: %w", fschaincontracts.AddContainerStructsMethod, err) + } + + cp.log.Warn("not enough GAS for notary call, will try again later", + zap.String("method", fschaincontracts.AddContainerStructsMethod), zap.Error(err)) + continue + } + + if !txRes.VMState.HasFlag(vmstate.Halt) { + cp.log.Warn("non-HALT VM state, will try again later", + zap.String("method", fschaincontracts.AddContainerStructsMethod), zap.Stringer("state", txRes.VMState), + zap.String("expection", txRes.FaultException), zap.String("tx", txRes.Container.StringLE())) + continue + } + + if len(txRes.Stack) == 0 { + return fmt.Errorf("empty stack in %s call result, tx %s", fschaincontracts.AddContainerStructsMethod, txRes.Container) + } + + b, err := txRes.Stack[0].TryBool() + if err != nil { + return fmt.Errorf("convert stack item in %s call result to bool (tx %s); %w", fschaincontracts.AddContainerStructsMethod, txRes.Container, err) + } + + if !b { + cp.log.Warn("all containers have benn successfully structured in the contract, interrupt") + return nil + } + + cp.log.Info("more containers have been successfully structured in the contract, continue") + } +} diff --git a/pkg/morph/client/container/client.go b/pkg/morph/client/container/client.go index a9bebf9099..aa95daa905 100644 --- a/pkg/morph/client/container/client.go +++ b/pkg/morph/client/container/client.go @@ -27,6 +27,7 @@ const ( deleteMethod = "delete" getMethod = "get" getDataMethod = "getContainerData" + getInfoMethod = "getInfo" listMethod = "containersOf" eaclMethod = "eACL" eaclDataMethod = "getEACLData" diff --git a/pkg/morph/client/container/get.go b/pkg/morph/client/container/get.go index 5056d73e34..a87d92cba7 100644 --- a/pkg/morph/client/container/get.go +++ b/pkg/morph/client/container/get.go @@ -4,9 +4,11 @@ import ( "fmt" "strings" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" containerrpc "github.com/nspcc-dev/neofs-contract/rpc/container" containercore "github.com/nspcc-dev/neofs-node/pkg/core/container" "github.com/nspcc-dev/neofs-node/pkg/morph/client" + fschaincontracts "github.com/nspcc-dev/neofs-node/pkg/morph/contracts" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" "github.com/nspcc-dev/neofs-sdk-go/container" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -37,16 +39,20 @@ func Get(c *Client, cnr cid.ID) (container.Container, error) { func (c *Client) Get(cid []byte) (container.Container, error) { var cnr container.Container prm := client.TestInvokePrm{} - method := getDataMethod + method := getInfoMethod prm.SetMethod(method) prm.SetArgs(cid) arr, err := c.client.TestInvoke(prm) - old := err != nil && isMethodNotFoundError(err, method) - if old { - method = getMethod + if err != nil && isMethodNotFoundError(err, method) { + method = getDataMethod prm.SetMethod(method) arr, err = c.client.TestInvoke(prm) + if err != nil && isMethodNotFoundError(err, method) { + method = getMethod + prm.SetMethod(method) + arr, err = c.client.TestInvoke(prm) + } } if err != nil { if strings.Contains(err.Error(), containerrpc.NotFoundError) { @@ -59,7 +65,23 @@ func (c *Client) Get(cid []byte) (container.Container, error) { return cnr, fmt.Errorf("unexpected stack item count (%s): %d", method, ln) } - if old { + if method != getInfoMethod { + return decodeOldGetResponse(arr, method) + } + + cnr, err = fschaincontracts.ContainerFromStackItem(arr[0]) + if err != nil { + return cnr, fmt.Errorf("invalid %q method result: invalid stack item: %w", method, err) + } + + return cnr, nil +} + +func decodeOldGetResponse(arr []stackitem.Item, method string) (container.Container, error) { + var cnr container.Container + + if method == getMethod { + var err error arr, err = client.ArrayFromStackItem(arr[0]) if err != nil { return cnr, fmt.Errorf("could not get item array of container (%s): %w", getMethod, err) diff --git a/pkg/morph/client/container/put.go b/pkg/morph/client/container/put.go index b293759272..9bf6342b96 100644 --- a/pkg/morph/client/container/put.go +++ b/pkg/morph/client/container/put.go @@ -5,23 +5,22 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/morph/client" fschaincontracts "github.com/nspcc-dev/neofs-node/pkg/morph/contracts" + "github.com/nspcc-dev/neofs-sdk-go/container" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" ) // PutPrm groups parameters of Put operation. type PutPrm struct { - cnr []byte - key []byte - sig []byte - token []byte - name string - zone string - enableMetaOnChain bool + cnr container.Container + key []byte + sig []byte + token []byte client.InvokePrmOptional } -// SetContainer sets container data. -func (p *PutPrm) SetContainer(cnr []byte) { +// SetContainer sets container. +func (p *PutPrm) SetContainer(cnr container.Container) { p.cnr = cnr } @@ -40,35 +39,20 @@ func (p *PutPrm) SetToken(token []byte) { p.token = token } -// SetName sets native name. -func (p *PutPrm) SetName(name string) { - p.name = name -} - -// SetZone sets zone. -func (p *PutPrm) SetZone(zone string) { - p.zone = zone -} - -// EnableMeta enables meta-on-chain. -func (p *PutPrm) EnableMeta() { - p.enableMetaOnChain = true -} - -// Put saves binary container with its session token, key and signature +// Put saves container with its session token, key and signature // in NeoFS system through Container contract call. // // Returns calculated container identifier and any error // encountered that caused the saving to interrupt. -func (c *Client) Put(p PutPrm) error { +func (c *Client) Put(p PutPrm) (cid.ID, error) { if len(p.sig) == 0 || len(p.key) == 0 { - return errNilArgument + return cid.ID{}, errNilArgument } var prm client.InvokePrm - prm.SetMethod(fschaincontracts.CreateContainerMethod) + prm.SetMethod(fschaincontracts.CreateContainerV2Method) prm.InvokePrmOptional = p.InvokePrmOptional - prm.SetArgs(p.cnr, p.sig, p.key, p.token, p.name, p.zone, p.enableMetaOnChain) + prm.SetArgs(fschaincontracts.ContainerToStackItem(p.cnr), p.sig, p.key, p.token) // no magic bugs with notary requests anymore, this operation should // _always_ be notary signed so make it one more time even if it is @@ -76,15 +60,31 @@ func (c *Client) Put(p PutPrm) error { prm.RequireAlphabetSignature() err := c.client.Invoke(prm) + if err == nil { + return cid.NewFromMarshalledContainer(p.cnr.Marshal()), nil + } + if !isMethodNotFoundError(err, fschaincontracts.CreateContainerV2Method) { + return cid.ID{}, fmt.Errorf("could not invoke method (%s): %w", fschaincontracts.CreateContainerV2Method, err) + } + + prm.SetMethod(fschaincontracts.CreateContainerMethod) + + domain := p.cnr.ReadDomain() + metaAttr := p.cnr.Attribute("__NEOFS__METAINFO_CONSISTENCY") + metaEnabled := metaAttr == "optimistic" || metaAttr == "strict" + cnrBytes := p.cnr.Marshal() + prm.SetArgs(cnrBytes, p.sig, p.key, p.token, domain.Name(), domain.Zone(), metaEnabled) + + err = c.client.Invoke(prm) if err != nil { if isMethodNotFoundError(err, fschaincontracts.CreateContainerMethod) { prm.SetMethod(putMethod) if err = c.client.Invoke(prm); err != nil { - return fmt.Errorf("could not invoke method (%s): %w", putMethod, err) + return cid.ID{}, fmt.Errorf("could not invoke method (%s): %w", putMethod, err) } - return nil + return cid.NewFromMarshalledContainer(cnrBytes), nil } - return fmt.Errorf("could not invoke method (%s): %w", fschaincontracts.CreateContainerMethod, err) + return cid.ID{}, fmt.Errorf("could not invoke method (%s): %w", fschaincontracts.CreateContainerMethod, err) } - return nil + return cid.NewFromMarshalledContainer(cnrBytes), nil } diff --git a/pkg/morph/client/notary.go b/pkg/morph/client/notary.go index 6c6549fe88..4d185aba7b 100644 --- a/pkg/morph/client/notary.go +++ b/pkg/morph/client/notary.go @@ -13,6 +13,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -323,6 +324,13 @@ func (c *Client) NotaryInvoke(ctx context.Context, contract util.Uint160, await return c.notaryInvoke(false, true, contract, await, nonce, vub, method, args...) } +// CallNotary calls contract method requiring Alphabet witness using Notary +// service and returns transaction result. +func (c *Client) CallNotary(_ context.Context, contract util.Uint160, method string, args ...any) (*state.AppExecResult, error) { + _, res, err := c._notaryInvoke(false, true, contract, true, 0, nil, method, args...) + return res, err +} + // NotaryInvokeNotAlpha does the same as NotaryInvoke but does not use client's // private key in Invocation script. It means that main TX of notary request is // not expected to be signed by the current node. @@ -475,15 +483,20 @@ func (c *Client) notaryInvokeAsCommittee(method string, nonce, vub uint32, args } func (c *Client) notaryInvoke(committee, invokedByAlpha bool, contract util.Uint160, await bool, nonce uint32, vub *uint32, method string, args ...any) (util.Uint256, error) { + txHash, _, err := c._notaryInvoke(committee, invokedByAlpha, contract, await, nonce, vub, method, args) + return txHash, err +} + +func (c *Client) _notaryInvoke(committee, invokedByAlpha bool, contract util.Uint160, await bool, nonce uint32, vub *uint32, method string, args ...any) (util.Uint256, *state.AppExecResult, error) { var conn = c.conn.Load() if conn == nil { - return util.Uint256{}, ErrConnectionLost + return util.Uint256{}, nil, ErrConnectionLost } alphabetList, err := c.notary.alphabetSource() // prepare arguments for test invocation if err != nil { - return util.Uint256{}, err + return util.Uint256{}, nil, err } var until uint32 @@ -493,18 +506,18 @@ func (c *Client) notaryInvoke(committee, invokedByAlpha bool, contract util.Uint } else { until, err = c.notaryTxValidationLimit(conn) if err != nil { - return util.Uint256{}, err + return util.Uint256{}, nil, err } } cosigners, err := c.notaryCosigners(invokedByAlpha, alphabetList, committee) if err != nil { - return util.Uint256{}, err + return util.Uint256{}, nil, err } nAct, err := notary.NewActor(conn.client, cosigners, c.acc) if err != nil { - return util.Uint256{}, err + return util.Uint256{}, nil, err } mainH, fbH, untilActual, err := nAct.Notarize(nAct.MakeTunedCall(contract, method, nil, func(r *result.Invoke, t *transaction.Transaction) error { @@ -521,12 +534,14 @@ func (c *Client) notaryInvoke(committee, invokedByAlpha bool, contract util.Uint return nil }, args...)) + + var res *state.AppExecResult if await { - _, err = nAct.WaitSuccess(mainH, fbH, untilActual, err) + res, err = nAct.WaitSuccess(mainH, fbH, untilActual, err) } if err != nil && !alreadyOnChainError(err) { - return util.Uint256{}, err + return util.Uint256{}, nil, err } c.logger.Debug("notary request invoked", @@ -535,7 +550,7 @@ func (c *Client) notaryInvoke(committee, invokedByAlpha bool, contract util.Uint zap.String("tx_hash", mainH.StringLE()), zap.String("fallback_hash", fbH.StringLE())) - return mainH, nil + return mainH, res, nil } func (c *Client) runAlphabetNotaryScript(script []byte, nonce uint32) error { diff --git a/pkg/morph/contracts/methods.go b/pkg/morph/contracts/methods.go index 7575d33ce5..e39d06d545 100644 --- a/pkg/morph/contracts/methods.go +++ b/pkg/morph/contracts/methods.go @@ -3,6 +3,7 @@ package fschaincontracts // Various methods of FS chain Container contract. const ( CreateContainerMethod = "create" + CreateContainerV2Method = "createV2" RemoveContainerMethod = "remove" PutContainerEACLMethod = "putEACL" PutContainerReportMethod = "putReport" @@ -11,6 +12,7 @@ const ( GetTakenSpaceByUserMethod = "getTakenSpaceByUser" GetContainerQuotaMethod = "containerQuota" GetUserQuotaMethod = "userQuota" + AddContainerStructsMethod = "addStructs" ) // CreateContainerParams are parameters of [CreateContainerMethod]. diff --git a/pkg/morph/contracts/models.go b/pkg/morph/contracts/models.go new file mode 100644 index 0000000000..2d2836975a --- /dev/null +++ b/pkg/morph/contracts/models.go @@ -0,0 +1,117 @@ +package fschaincontracts + +import ( + "errors" + "fmt" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + containerrpc "github.com/nspcc-dev/neofs-contract/rpc/container" + "github.com/nspcc-dev/neofs-sdk-go/container" + protocontainer "github.com/nspcc-dev/neofs-sdk-go/proto/container" + protonetmap "github.com/nspcc-dev/neofs-sdk-go/proto/netmap" + "github.com/nspcc-dev/neofs-sdk-go/proto/refs" + "github.com/nspcc-dev/neofs-sdk-go/user" + "google.golang.org/protobuf/proto" +) + +// ContainerFromStackItem decodes container from stack item. +func ContainerFromStackItem(item stackitem.Item) (container.Container, error) { + var contractStruct containerrpc.ContainerInfo + if err := contractStruct.FromStackItem(item); err != nil { + return container.Container{}, err + } + + return ContainerFromStruct(contractStruct) +} + +// ContainerFromStruct decodes container from contract structure. +func ContainerFromStruct(contractStruct containerrpc.ContainerInfo) (container.Container, error) { + mjr, mnr, err := containerVersionFromStruct(contractStruct.Version) + if err != nil { + return container.Container{}, err + } + + if len(contractStruct.Nonce) != 16 { + return container.Container{}, fmt.Errorf("invalid nonce: invalid len: expected 16, got %d", len(contractStruct.Nonce)) + } + + basicACL, err := toUint32(contractStruct.BasicACL) + if err != nil { + return container.Container{}, fmt.Errorf("invalid basic ACL: %w", err) + } + + attrs := make([]*protocontainer.Container_Attribute, len(contractStruct.Attributes)) + for i := range contractStruct.Attributes { + if contractStruct.Attributes[i] != nil { + attrs[i] = &protocontainer.Container_Attribute{ + Key: contractStruct.Attributes[i].Key, + Value: contractStruct.Attributes[i].Value, + } + } + } + + owner := user.NewFromScriptHash(contractStruct.Owner) + + // TODO: add version and nonce setter to get rid of proto instance + m := protocontainer.Container{ + Version: &refs.Version{Major: mjr, Minor: mnr}, + OwnerId: &refs.OwnerID{Value: owner[:]}, + Nonce: contractStruct.Nonce, + BasicAcl: basicACL, + Attributes: attrs, + PlacementPolicy: new(protonetmap.PlacementPolicy), + } + + if err := proto.Unmarshal(contractStruct.StoragePolicy, m.PlacementPolicy); err != nil { + return container.Container{}, fmt.Errorf("invalid storage policy binary: %w", err) + } + + var cnr container.Container + if err := cnr.FromProtoMessage(&m); err != nil { + return container.Container{}, fmt.Errorf("invalid container: %w", err) + } + + return cnr, nil +} + +func containerVersionFromStruct(src *containerrpc.ContainerAPIVersion) (uint32, uint32, error) { + if src == nil { + return 0, 0, errors.New("missing version") + } + + mjr, err := toUint32(src.Major) + if err != nil { + return 0, 0, fmt.Errorf("invalid major: %w", err) + } + + mnr, err := toUint32(src.Minor) + if err != nil { + return 0, 0, fmt.Errorf("invalid minor: %w", err) + } + + return mjr, mnr, nil +} + +// ContainerToStackItem encodes container to instance convertible to stack item. +func ContainerToStackItem(cnr container.Container) stackitem.Convertible { + ver := cnr.Version() + owner := cnr.Owner() + + var attrs []*containerrpc.ContainerAttribute + for k, v := range cnr.Attributes() { + attrs = append(attrs, &containerrpc.ContainerAttribute{Key: k, Value: v}) + } + + return &containerrpc.ContainerInfo{ + Version: &containerrpc.ContainerAPIVersion{ + Major: big.NewInt(int64(ver.Major())), + Minor: big.NewInt(int64(ver.Minor())), + }, + Owner: owner.ScriptHash(), + Nonce: cnr.ProtoMessage().Nonce, // TODO: provide and use nonce getter + BasicACL: big.NewInt(int64(cnr.BasicACL().Bits())), + Attributes: attrs, + StoragePolicy: cnr.PlacementPolicy().Marshal(), + } +} diff --git a/pkg/morph/contracts/util.go b/pkg/morph/contracts/util.go new file mode 100644 index 0000000000..03847b9a7b --- /dev/null +++ b/pkg/morph/contracts/util.go @@ -0,0 +1,20 @@ +package fschaincontracts + +import ( + "fmt" + "math" + "math/big" +) + +func toUint32(n *big.Int) (uint32, error) { + if !n.IsUint64() { + return 0, fmt.Errorf("%s is not a valid uint32", n) + } + + u64 := n.Uint64() + if u64 > math.MaxUint32 { + return 0, fmt.Errorf("%s is not a valid uint32", n) + } + + return uint32(u64), nil +} diff --git a/pkg/morph/event/container/notary_requests.go b/pkg/morph/event/container/notary_requests.go index a04b2cb9f7..0a54bc197a 100644 --- a/pkg/morph/event/container/notary_requests.go +++ b/pkg/morph/event/container/notary_requests.go @@ -4,7 +4,9 @@ import ( "fmt" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + containerrpc "github.com/nspcc-dev/neofs-contract/rpc/container" fschaincontracts "github.com/nspcc-dev/neofs-node/pkg/morph/contracts" "github.com/nspcc-dev/neofs-node/pkg/morph/event" ) @@ -17,14 +19,66 @@ func getArgsFromEvent(ne event.NotaryEvent, expectedNum int) ([]event.Op, error) return args, nil } +func wrapInvalidArgError(i int, typ stackitem.Type, desc string, err error) error { + return fmt.Errorf("arg#%d (%s, %s): %w", i, typ, desc, err) +} + func getValueFromArg[T any](args []event.Op, i int, desc string, typ stackitem.Type, f func(event.Op) (T, error)) (v T, err error) { v, err = f(args[i]) if err != nil { - return v, fmt.Errorf("arg#%d (%s, %s): %w", i, typ, desc, err) + return v, wrapInvalidArgError(i, typ, desc, err) } return v, nil } +// CreateContainerRequest wraps container creation request to provide +// app-internal event. +type CreateContainerV2Request struct { + event.Event + MainTransaction transaction.Transaction + + Container containerrpc.ContainerInfo + InvocationScript []byte + VerificationScript []byte + SessionToken []byte +} + +// RestoreCreateContainerV2Request restores [CreateContainerV2Request] from the +// notary one. +func RestoreCreateContainerV2Request(notaryReq event.NotaryEvent) (event.Event, error) { + testVM := vm.New() + testVM.LoadScript(notaryReq.ArgumentScript()) + + if err := testVM.Run(); err != nil { + return nil, fmt.Errorf("exec script on test VM: %w", err) + } + + stack := testVM.Estack() + const argNum = 4 + if got := stack.Len(); got != argNum { + return nil, fmt.Errorf("wrong/unsupported arg num %d instead of %d", got, argNum) // TODO: share error + } + + var res CreateContainerV2Request + var err error + + if err = res.Container.FromStackItem(stack.Pop().Item()); err != nil { + return nil, wrapInvalidArgError(argNum-1, stackitem.StructT, "container", err) + } + if res.InvocationScript, err = stack.Pop().Item().TryBytes(); err != nil { + return nil, wrapInvalidArgError(argNum-2, stackitem.ByteArrayT, "invocation script", err) + } + if res.VerificationScript, err = stack.Pop().Item().TryBytes(); err != nil { + return nil, wrapInvalidArgError(argNum-3, stackitem.ByteArrayT, "verification script", err) + } + if res.SessionToken, err = stack.Pop().Item().TryBytes(); err != nil { + return nil, wrapInvalidArgError(argNum-4, stackitem.ByteArrayT, "session token", err) + } + res.MainTransaction = *notaryReq.Raw().MainTransaction + + return res, nil +} + // CreateContainerRequest wraps container creation request to provide // app-internal event. type CreateContainerRequest struct { @@ -141,3 +195,23 @@ func RestorePutContainerEACLRequest(notaryReq event.NotaryEvent) (event.Event, e return res, nil } + +// AddStructsRequest wraps container protobuf->struct migration request to +// provide app-internal event. +type AddStructsRequest struct { + event.Event + MainTransaction transaction.Transaction +} + +// RestoreAddStructsRequest restores [AddStructsRequest] from the +// notary one. +func RestoreAddStructsRequest(notaryReq event.NotaryEvent) (event.Event, error) { + _, err := getArgsFromEvent(notaryReq, 0) + if err != nil { + return nil, err + } + + return AddStructsRequest{ + MainTransaction: *notaryReq.Raw().MainTransaction, + }, nil +}