From f77720083284d01a6c0f2245bde609d5227878fc Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 25 Nov 2025 11:40:04 +0300 Subject: [PATCH 01/11] go.mod: Upgrade `github.com/nspcc-dev/neofs-contract` to the latest Brings https://github.com/nspcc-dev/neofs-contract/pull/534. Signed-off-by: Leonard Lyubich --- CHANGELOG.md | 1 + go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95f6b6b324..28f9d7c354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Changelog for NeoFS Node ### Removed ### Updated +- `github.com/nspcc-dev/neofs-contract` module to `v0.25.2-0.20251124180339-40ec608b4893` (#3670) ### Updating from v0.50.1 diff --git a/go.mod b/go.mod index b564c50da6..f2cacd7d56 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/nspcc-dev/locode-db v0.8.1 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.25.2-0.20251124164423-4fffaa262a8a + github.com/nspcc-dev/neofs-contract v0.25.2-0.20251124180339-40ec608b4893 github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251118154818-9480da80f9ad github.com/nspcc-dev/tzhash v1.8.3 github.com/panjf2000/ants/v2 v2.11.3 diff --git a/go.sum b/go.sum index c50f621f75..58b63ed4ee 100644 --- a/go.sum +++ b/go.sum @@ -197,8 +197,8 @@ github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20251112080609-3c8e29c66609 h1:9j 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.25.2-0.20251124164423-4fffaa262a8a h1:BNuNJsp7vLDourjsPio/aEfuEaKzrpjiPaaq0iHsFgY= -github.com/nspcc-dev/neofs-contract v0.25.2-0.20251124164423-4fffaa262a8a/go.mod h1:uqTaIPQCIouOyflW4aRQAyl4w88GhxYosJ74wvS4AEQ= +github.com/nspcc-dev/neofs-contract v0.25.2-0.20251124180339-40ec608b4893 h1:sh25Y5GMLL9ixlcJdbktHkFUY3nyK9DeLY5EBaEQDwQ= +github.com/nspcc-dev/neofs-contract v0.25.2-0.20251124180339-40ec608b4893/go.mod h1:CYX51uP2pNBCK7Q0ygD1LNsoFSHbB2F5luaBrluFkUo= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251118154818-9480da80f9ad h1:hWJBAWUqGHqvtInPljzfGP2TIDdMA+jYMX5/DCCyJjA= github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.16.0.20251118154818-9480da80f9ad/go.mod h1:MbgvTuiYY3uG+iMqIcvojmNHfZcxGNz4U+EKNBKPHzE= github.com/nspcc-dev/rfc6979 v0.2.4 h1:NBgsdCjhLpEPJZqmC9rciMZDcSY297po2smeaRjw57k= From 29ae0a661076d1491afc319e30fe8c9786d39ead Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 25 Nov 2025 11:44:05 +0300 Subject: [PATCH 02/11] sn/container: Try new getInfo contract method for reading If new method is missing, SN falls back to the old ones. Signed-off-by: Leonard Lyubich --- CHANGELOG.md | 1 + pkg/morph/client/container/client.go | 1 + pkg/morph/client/container/get.go | 32 ++++++++-- pkg/morph/contracts/models.go | 88 ++++++++++++++++++++++++++++ pkg/morph/contracts/util.go | 20 +++++++ 5 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 pkg/morph/contracts/models.go create mode 100644 pkg/morph/contracts/util.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f9d7c354..6bf9fa2f17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changelog for NeoFS Node ## [Unreleased] ### Added +- SN now supports new `getInfo` method of the Container contract (#3670) ### Fixed 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/contracts/models.go b/pkg/morph/contracts/models.go new file mode 100644 index 0000000000..00da9fe717 --- /dev/null +++ b/pkg/morph/contracts/models.go @@ -0,0 +1,88 @@ +package fschaincontracts + +import ( + "errors" + "fmt" + + "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 + } + + 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 +} 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 +} From 8cf96ed5ad15d9ed59b4e65a1e0e1305187a4612 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 6 Nov 2025 18:16:12 +0300 Subject: [PATCH 03/11] sn/container: Do not calculate ID on tx submission errors It's not needed in this case. Signed-off-by: Leonard Lyubich --- cmd/neofs-node/container.go | 8 ++------ pkg/morph/client/container/put.go | 13 +++++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/cmd/neofs-node/container.go b/cmd/neofs-node/container.go index 4e3e27838d..c964d9c12f 100644 --- a/cmd/neofs-node/container.go +++ b/cmd/neofs-node/container.go @@ -471,11 +471,10 @@ 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.SetContainer(cnr.Marshal()) prm.SetName(d.Name()) prm.SetZone(d.Zone()) prm.SetKey(pub) @@ -486,11 +485,8 @@ func (x *containersInChain) Put(cnr containerSDK.Container, pub, sig []byte, st 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/pkg/morph/client/container/put.go b/pkg/morph/client/container/put.go index b293759272..292216f396 100644 --- a/pkg/morph/client/container/put.go +++ b/pkg/morph/client/container/put.go @@ -5,6 +5,7 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/morph/client" fschaincontracts "github.com/nspcc-dev/neofs-node/pkg/morph/contracts" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" ) // PutPrm groups parameters of Put operation. @@ -60,9 +61,9 @@ func (p *PutPrm) EnableMeta() { // // 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 @@ -80,11 +81,11 @@ func (c *Client) Put(p PutPrm) error { 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(p.cnr), 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(p.cnr), nil } From 4f9ef5dbf714a01609f53cea201fc9db3d233a97 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 6 Nov 2025 18:22:07 +0300 Subject: [PATCH 04/11] sn/container: Refactor `Client.Put()` method input This will make it easier to support https://github.com/nspcc-dev/neofs-contract/pull/534. Signed-off-by: Leonard Lyubich --- cmd/neofs-node/container.go | 9 +------ pkg/morph/client/container/put.go | 44 +++++++++++-------------------- 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/cmd/neofs-node/container.go b/cmd/neofs-node/container.go index c964d9c12f..972da047db 100644 --- a/cmd/neofs-node/container.go +++ b/cmd/neofs-node/container.go @@ -471,20 +471,13 @@ 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) { - d := cnr.ReadDomain() - var prm cntClient.PutPrm - prm.SetContainer(cnr.Marshal()) - 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() - } return x.cCli.Put(prm) } diff --git a/pkg/morph/client/container/put.go b/pkg/morph/client/container/put.go index 292216f396..1bf3f37ada 100644 --- a/pkg/morph/client/container/put.go +++ b/pkg/morph/client/container/put.go @@ -5,24 +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 } @@ -41,22 +39,7 @@ 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 @@ -69,7 +52,12 @@ func (c *Client) Put(p PutPrm) (cid.ID, error) { var prm client.InvokePrm prm.SetMethod(fschaincontracts.CreateContainerMethod) prm.InvokePrmOptional = p.InvokePrmOptional - prm.SetArgs(p.cnr, p.sig, p.key, p.token, p.name, p.zone, p.enableMetaOnChain) + + 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) // no magic bugs with notary requests anymore, this operation should // _always_ be notary signed so make it one more time even if it is @@ -83,9 +71,9 @@ func (c *Client) Put(p PutPrm) (cid.ID, error) { if err = c.client.Invoke(prm); err != nil { return cid.ID{}, fmt.Errorf("could not invoke method (%s): %w", putMethod, err) } - return cid.NewFromMarshalledContainer(p.cnr), nil + return cid.NewFromMarshalledContainer(cnrBytes), nil } return cid.ID{}, fmt.Errorf("could not invoke method (%s): %w", fschaincontracts.CreateContainerMethod, err) } - return cid.NewFromMarshalledContainer(p.cnr), nil + return cid.NewFromMarshalledContainer(cnrBytes), nil } From c953714d78feb8c1031a049d5d27c9daf10a12f5 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 6 Nov 2025 19:56:46 +0300 Subject: [PATCH 05/11] sn/container: Try new `createV2` method for contract creation Will become available with https://github.com/nspcc-dev/neofs-contract/pull/534 upgrade. If new method is missing, SN falls back to old methods. Signed-off-by: Leonard Lyubich --- CHANGELOG.md | 2 +- pkg/morph/client/container/put.go | 25 ++++++++++++++++++------- pkg/morph/contracts/methods.go | 1 + pkg/morph/contracts/models.go | 24 ++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf9fa2f17..a9773057ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changelog for NeoFS Node ## [Unreleased] ### Added -- SN now supports new `getInfo` method of the Container contract (#3670) +- SN now supports new `getInfo` and `createV2` methods of the Container contract (#3670) ### Fixed diff --git a/pkg/morph/client/container/put.go b/pkg/morph/client/container/put.go index 1bf3f37ada..9bf6342b96 100644 --- a/pkg/morph/client/container/put.go +++ b/pkg/morph/client/container/put.go @@ -50,14 +50,9 @@ func (c *Client) Put(p PutPrm) (cid.ID, error) { } var prm client.InvokePrm - prm.SetMethod(fschaincontracts.CreateContainerMethod) + prm.SetMethod(fschaincontracts.CreateContainerV2Method) prm.InvokePrmOptional = p.InvokePrmOptional - - 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) + 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 @@ -65,6 +60,22 @@ func (c *Client) Put(p PutPrm) (cid.ID, 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) diff --git a/pkg/morph/contracts/methods.go b/pkg/morph/contracts/methods.go index 30c6a8f610..be1eb4ec12 100644 --- a/pkg/morph/contracts/methods.go +++ b/pkg/morph/contracts/methods.go @@ -5,6 +5,7 @@ const ( PayBalanceMethod = "settleContainerPayment" UnpaidBalanceMethod = "getUnpaidContainerEpoch" CreateContainerMethod = "create" + CreateContainerV2Method = "createV2" RemoveContainerMethod = "remove" PutContainerEACLMethod = "putEACL" PutContainerReportMethod = "putReport" diff --git a/pkg/morph/contracts/models.go b/pkg/morph/contracts/models.go index 00da9fe717..69ab122f55 100644 --- a/pkg/morph/contracts/models.go +++ b/pkg/morph/contracts/models.go @@ -3,6 +3,7 @@ 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" @@ -86,3 +87,26 @@ func containerVersionFromStruct(src *containerrpc.ContainerAPIVersion) (uint32, 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(), + } +} From 0e92f7af1d73f5e5232bcfbbe281b182016e6552 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 6 Nov 2025 20:17:39 +0300 Subject: [PATCH 06/11] ir/container: Refactor `checkNNS()` It makes no sense to keep domain as struct field. Signed-off-by: Leonard Lyubich --- .../processors/container/process_container.go | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/pkg/innerring/processors/container/process_container.go b/pkg/innerring/processors/container/process_container.go index f2ed24e383..246a5b2871 100644 --- a/pkg/innerring/processors/container/process_container.go +++ b/pkg/innerring/processors/container/process_container.go @@ -30,7 +30,6 @@ type putContainerContext struct { // 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 @@ -120,10 +119,11 @@ func (cp *Processor) checkPutContainer(ctx *putContainerContext) error { 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 ctx.e.DomainName != "" { // if PutNamed event => check if values in-container domain name and zone correspond to args + err = checkNNS(ctx.cnr, ctx.e.DomainName, ctx.e.DomainZone) + if err != nil { + return fmt.Errorf("NNS: %w", err) + } } return nil @@ -238,19 +238,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 From c0eb8faeff874ec546bc54f1853bcecf2f0c7520 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 6 Nov 2025 20:28:23 +0300 Subject: [PATCH 07/11] ir/container: Reuse already calculated ID in `processContainerPut()` Signed-off-by: Leonard Lyubich --- pkg/innerring/processors/container/handlers.go | 2 +- .../processors/container/process_container.go | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pkg/innerring/processors/container/handlers.go b/pkg/innerring/processors/container/handlers.go index 3f1591098c..f74e931694 100644 --- a/pkg/innerring/processors/container/handlers.go +++ b/pkg/innerring/processors/container/handlers.go @@ -36,7 +36,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 246a5b2871..ed351a7159 100644 --- a/pkg/innerring/processors/container/process_container.go +++ b/pkg/innerring/processors/container/process_container.go @@ -28,13 +28,12 @@ type putContainerContext struct { e containerEvent.CreateContainerRequest // must be filled when verifying raw data from e - cID cid.ID cnr containerSDK.Container } // 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 @@ -53,7 +52,7 @@ func (cp *Processor) processContainerPut(req containerEvent.CreateContainerReque return } - cp.approvePutContainer(ctx) + cp.approvePutContainer(ctx, id) } const ( @@ -70,7 +69,6 @@ var allowedSystemAttributes = map[string]struct{}{ func (cp *Processor) checkPutContainer(ctx *putContainerContext) error { binCnr := ctx.e.Container - ctx.cID = cid.NewFromMarshalledContainer(binCnr) err := ctx.cnr.Unmarshal(binCnr) if err != nil { @@ -129,8 +127,8 @@ func (cp *Processor) checkPutContainer(ctx *putContainerContext) error { return nil } -func (cp *Processor) approvePutContainer(ctx *putContainerContext) { - l := cp.log.With(zap.Stringer("cID", ctx.cID)) +func (cp *Processor) approvePutContainer(ctx *putContainerContext, id cid.ID) { + l := cp.log.With(zap.Stringer("cID", id)) l.Debug("approving new container...") e := ctx.e @@ -153,7 +151,7 @@ func (cp *Processor) approvePutContainer(ctx *putContainerContext) { } policy := ctx.cnr.PlacementPolicy() - vectors, err := nm.ContainerNodes(policy, ctx.cID) + 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 +166,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 From a0b12ce1bac4e80cd3c22ec243e9b0a464b71401 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 6 Nov 2025 20:35:26 +0300 Subject: [PATCH 08/11] ir/container: De-struct method parameters Passing explicit parameters is easier to understand. Signed-off-by: Leonard Lyubich --- .../processors/container/process_container.go | 63 ++++++++----------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/pkg/innerring/processors/container/process_container.go b/pkg/innerring/processors/container/process_container.go index ed351a7159..35f8266fc4 100644 --- a/pkg/innerring/processors/container/process_container.go +++ b/pkg/innerring/processors/container/process_container.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neofs-node/pkg/morph/event" containerEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/container" @@ -24,13 +25,6 @@ type putEvent interface { NotaryRequest() *payload.P2PNotaryRequest } -type putContainerContext struct { - e containerEvent.CreateContainerRequest - - // must be filled when verifying raw data from e - cnr containerSDK.Container -} - // 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, id cid.ID) { @@ -39,11 +33,15 @@ func (cp *Processor) processContainerPut(req containerEvent.CreateContainerReque 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(req, cnr) if err != nil { cp.log.Error("put container check failed", zap.Error(err), @@ -52,7 +50,7 @@ func (cp *Processor) processContainerPut(req containerEvent.CreateContainerReque return } - cp.approvePutContainer(ctx, id) + cp.approvePutContainer(req.MainTransaction, cnr, id) } const ( @@ -67,15 +65,8 @@ var allowedSystemAttributes = map[string]struct{}{ sysAttrChainMeta: {}, } -func (cp *Processor) checkPutContainer(ctx *putContainerContext) error { - binCnr := ctx.e.Container - - 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(req containerEvent.CreateContainerRequest, cnr containerSDK.Container) 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) @@ -87,38 +78,38 @@ 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: req.SessionToken, + verifScript: req.VerificationScript, + invocScript: req.InvocationScript, + signedData: req.Container, }) 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) } - if ctx.e.DomainName != "" { // if PutNamed event => check if values in-container domain name and zone correspond to args - err = checkNNS(ctx.cnr, ctx.e.DomainName, ctx.e.DomainZone) + if req.DomainName != "" { // if PutNamed event => check if values in-container domain name and zone correspond to args + err = checkNNS(cnr, req.DomainName, req.DomainZone) if err != nil { return fmt.Errorf("NNS: %w", err) } @@ -127,15 +118,13 @@ func (cp *Processor) checkPutContainer(ctx *putContainerContext) error { return nil } -func (cp *Processor) approvePutContainer(ctx *putContainerContext, id cid.ID) { +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", @@ -150,7 +139,7 @@ func (cp *Processor) approvePutContainer(ctx *putContainerContext, id cid.ID) { return } - policy := ctx.cnr.PlacementPolicy() + 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)) From 0b04a9bed31ac90fba02237a7e7a7b54be1f08e1 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 6 Nov 2025 20:52:04 +0300 Subject: [PATCH 09/11] ir/container: De-struct `checkPutContainer()` method parameters Signed-off-by: Leonard Lyubich --- .../processors/container/process_container.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/innerring/processors/container/process_container.go b/pkg/innerring/processors/container/process_container.go index 35f8266fc4..7c46c3f371 100644 --- a/pkg/innerring/processors/container/process_container.go +++ b/pkg/innerring/processors/container/process_container.go @@ -41,7 +41,7 @@ func (cp *Processor) processContainerPut(req containerEvent.CreateContainerReque return } - err := cp.checkPutContainer(req, cnr) + 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), @@ -65,7 +65,7 @@ var allowedSystemAttributes = map[string]struct{}{ sysAttrChainMeta: {}, } -func (cp *Processor) checkPutContainer(req containerEvent.CreateContainerRequest, cnr containerSDK.Container) error { +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 { @@ -89,10 +89,10 @@ func (cp *Processor) checkPutContainer(req containerEvent.CreateContainerRequest err := cp.verifySignature(signatureVerificationData{ ownerContainer: cnr.Owner(), verb: session.VerbContainerPut, - binTokenSession: req.SessionToken, - verifScript: req.VerificationScript, - invocScript: req.InvocationScript, - signedData: req.Container, + binTokenSession: sessionToken, + verifScript: verifScript, + invocScript: invocScript, + signedData: cnrBytes, }) if err != nil { return fmt.Errorf("auth container creation: %w", err) @@ -108,8 +108,8 @@ func (cp *Processor) checkPutContainer(req containerEvent.CreateContainerRequest return fmt.Errorf("incorrect homomorphic hashing setting: %w", err) } - if req.DomainName != "" { // if PutNamed event => check if values in-container domain name and zone correspond to args - err = checkNNS(cnr, req.DomainName, req.DomainZone) + 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) } From 7fd2c0e990abe00a87540d779881c986c5aa7239 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 6 Nov 2025 21:24:32 +0300 Subject: [PATCH 10/11] ir/container: Handle creation request calling new `CreateV2` method Will become available with https://github.com/nspcc-dev/neofs-contract/pull/534 upgrade. Signed-off-by: Leonard Lyubich --- CHANGELOG.md | 1 + .../processors/container/handlers.go | 9 +++ .../processors/container/process_container.go | 26 ++++++++ .../processors/container/processor.go | 8 +++ pkg/morph/contracts/models.go | 5 ++ pkg/morph/event/container/notary_requests.go | 62 ++++++++++++++++++- 6 files changed, 109 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9773057ce..d8d005acb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Changelog for NeoFS Node ### Added - 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) ### Fixed diff --git a/pkg/innerring/processors/container/handlers.go b/pkg/innerring/processors/container/handlers.go index f74e931694..5bca0ecab1 100644 --- a/pkg/innerring/processors/container/handlers.go +++ b/pkg/innerring/processors/container/handlers.go @@ -11,6 +11,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 { diff --git a/pkg/innerring/processors/container/process_container.go b/pkg/innerring/processors/container/process_container.go index 7c46c3f371..13673c33e8 100644 --- a/pkg/innerring/processors/container/process_container.go +++ b/pkg/innerring/processors/container/process_container.go @@ -7,6 +7,7 @@ import ( "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" @@ -15,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 diff --git a/pkg/innerring/processors/container/processor.go b/pkg/innerring/processors/container/processor.go index 3beff00702..3c414fcd0d 100644 --- a/pkg/innerring/processors/container/processor.go +++ b/pkg/innerring/processors/container/processor.go @@ -137,6 +137,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) @@ -194,6 +198,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) diff --git a/pkg/morph/contracts/models.go b/pkg/morph/contracts/models.go index 69ab122f55..2d2836975a 100644 --- a/pkg/morph/contracts/models.go +++ b/pkg/morph/contracts/models.go @@ -22,6 +22,11 @@ func ContainerFromStackItem(item stackitem.Item) (container.Container, error) { 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 diff --git a/pkg/morph/event/container/notary_requests.go b/pkg/morph/event/container/notary_requests.go index a04b2cb9f7..e931ab22e6 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" ) @@ -12,7 +14,7 @@ import ( func getArgsFromEvent(ne event.NotaryEvent, expectedNum int) ([]event.Op, error) { args := ne.Params() if len(args) != expectedNum { - return nil, fmt.Errorf("wrong/unsupported arg num %d instead of %d", len(args), expectedNum) + return nil, newWrongArgNumError(expectedNum, len(args)) } return args, nil } @@ -20,11 +22,67 @@ func getArgsFromEvent(ne event.NotaryEvent, expectedNum int) ([]event.Op, error) 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 } +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 newWrongArgNumError(expected, actual int) error { + return fmt.Errorf("wrong/unsupported arg num %d instead of %d", actual, expected) +} + +// 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, newWrongArgNumError(argNum, got) + } + + 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 { From fb20039ba6dfcac74fb0768e2cfc647c2efad91a Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 18 Nov 2025 13:23:53 +0300 Subject: [PATCH 11/11] ir/container: Implement iterative background structuring in contract Within https://github.com/nspcc-dev/neofs-contract/issues/449, there is a need to store containers as VM structs. Since https://github.com/nspcc-dev/neofs-contract/pull/534, struct items are stored for new containers. This also needs to be done for containers created before the upgrade. Since doing this in a contract update transaction turned out to be too GAS-intensive, this implements structuring through the IR background process. Specialized method is called iteratively doing the same thing, but for several containers at a time. Signed-off-by: Leonard Lyubich --- CHANGELOG.md | 1 + pkg/innerring/innerring.go | 13 +++- .../processors/container/processor.go | 69 +++++++++++++++++++ pkg/morph/client/notary.go | 33 ++++++--- pkg/morph/contracts/methods.go | 1 + pkg/morph/event/container/notary_requests.go | 20 ++++++ 6 files changed, 125 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d005acb6..42eee817d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Changelog for NeoFS Node ### Added - 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 diff --git a/pkg/innerring/innerring.go b/pkg/innerring/innerring.go index 5ba2d84ad8..65c808c6f3 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/processor.go b/pkg/innerring/processors/container/processor.go index 3c414fcd0d..3bc46d3553 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" @@ -171,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 } @@ -232,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 } @@ -239,3 +262,49 @@ 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 + } + + txs := txRes.Container.StringLE() + + 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("exception", txRes.FaultException), zap.String("tx", txs)) + continue + } + + if len(txRes.Stack) == 0 { + return fmt.Errorf("empty stack in %s call result, tx %s", fschaincontracts.AddContainerStructsMethod, txs) + } + + 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, txs, err) + } + + if !b { + cp.log.Warn("all containers have been successfully structured in the contract, interrupt", zap.String("tx", txs)) + return nil + } + + cp.log.Info("more containers have been successfully structured in the contract, continue", zap.String("tx", txs)) + } +} diff --git a/pkg/morph/client/notary.go b/pkg/morph/client/notary.go index 1c40fdb81d..7cf282d4d7 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. @@ -484,15 +492,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 @@ -502,18 +515,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 { @@ -530,8 +543,10 @@ 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 { @@ -544,10 +559,10 @@ func (c *Client) notaryInvoke(committee, invokedByAlpha bool, contract util.Uint zap.String("fallback_hash", fbH.StringLE()), ) - return mainH, nil + return mainH, res, nil } - return util.Uint256{}, err + return util.Uint256{}, nil, err } c.logger.Debug("notary request invoked", @@ -556,7 +571,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 be1eb4ec12..143f99c342 100644 --- a/pkg/morph/contracts/methods.go +++ b/pkg/morph/contracts/methods.go @@ -14,6 +14,7 @@ const ( GetTakenSpaceByUserMethod = "getTakenSpaceByUser" GetContainerQuotaMethod = "containerQuota" GetUserQuotaMethod = "userQuota" + AddContainerStructsMethod = "addStructs" ) // CreateContainerParams are parameters of [CreateContainerMethod]. diff --git a/pkg/morph/event/container/notary_requests.go b/pkg/morph/event/container/notary_requests.go index e931ab22e6..7f3badb781 100644 --- a/pkg/morph/event/container/notary_requests.go +++ b/pkg/morph/event/container/notary_requests.go @@ -199,3 +199,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 +}