From 32c7dc016056fd8bd3709e610ab7d33e457a94cc Mon Sep 17 00:00:00 2001 From: Pavel Karpy Date: Tue, 10 Mar 2026 14:13:49 +0300 Subject: [PATCH] sn/object: allow split of a split This is essentially means reverting of 60faa73a3829a410ecbae4c7e00260913f51ae4a, and verifying LINK objects considering complex parents as a normal situation. Split of a split is now have a use case in s3-gw's multipart code. Signed-off-by: Pavel Karpy --- CHANGELOG.md | 1 + pkg/core/object/fmt.go | 23 ++++++----- pkg/core/object/fmt_test.go | 60 +++++++++++++++++++++++++++++ pkg/services/object/split/verify.go | 1 - 4 files changed, 74 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6373afb5bd..a950cc8b01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Changelog for NeoFS Node - Shard evacuation replicate for EC parts (#3854) - Suboptimal shard selection for EC parts during data evacuation (#3870) - No redistribution of objects when adding a new `REP 3` node (#3873) +- Split of a split is allowed (#3867) ### Changed - SN returns unsigned responses to requests with API >= `v2.22` (#3785) diff --git a/pkg/core/object/fmt.go b/pkg/core/object/fmt.go index ecbbf506c9..ecd1c9f954 100644 --- a/pkg/core/object/fmt.go +++ b/pkg/core/object/fmt.go @@ -142,10 +142,12 @@ func NewFormatValidator(fsChain FSChain, netmapContract NetmapContract, containe // // Returns nil error if the object has valid structure. func (v *FormatValidator) Validate(obj *object.Object, unprepared bool) error { - return v.validate(obj, unprepared, false) + return v.validate(obj, unprepared, 0) } -func (v *FormatValidator) validate(obj *object.Object, unprepared, isParent bool) error { +const maxObjectNestingLevel = 2 + +func (v *FormatValidator) validate(obj *object.Object, unprepared bool, nestingLevel int) error { if obj == nil { return errNilObject } @@ -194,7 +196,7 @@ func (v *FormatValidator) validate(obj *object.Object, unprepared, isParent bool return fmt.Errorf("read container by ID=%s: %w", cnrID, err) } - isEC, err := checkEC(*obj, cnr.PlacementPolicy().ECRules(), unprepared, isParent) + isEC, err := checkEC(*obj, cnr.PlacementPolicy().ECRules(), unprepared, nestingLevel > 0) if err != nil { return err } @@ -208,10 +210,6 @@ func (v *FormatValidator) validate(obj *object.Object, unprepared, isParent bool par := obj.Parent() if !isEC && obj.HasParent() { - if par != nil && par.HasParent() { - return errors.New("parent object has a parent itself") - } - if splitID != nil { // V1 split if firstSet { @@ -257,9 +255,14 @@ func (v *FormatValidator) validate(obj *object.Object, unprepared, isParent bool } } - if par != nil && (firstSet || splitID != nil || isEC) { - // Parent object already exists. - return v.validate(par, false, true) + if par != nil { + if nestingLevel == maxObjectNestingLevel { + return fmt.Errorf("max object nesting level %d overflow", maxObjectNestingLevel) + } + + // it is possible to have a split of a split + prepared := firstSet || splitID != nil || isEC + return v.validate(par, !prepared, nestingLevel+1) } return nil diff --git a/pkg/core/object/fmt_test.go b/pkg/core/object/fmt_test.go index 1a99781e73..cda0881ce2 100644 --- a/pkg/core/object/fmt_test.go +++ b/pkg/core/object/fmt_test.go @@ -18,6 +18,7 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" + objecttest "github.com/nspcc-dev/neofs-sdk-go/object/test" "github.com/nspcc-dev/neofs-sdk-go/session" sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" "github.com/nspcc-dev/neofs-sdk-go/user" @@ -406,6 +407,27 @@ func TestFormatValidator_Validate(t *testing.T) { }) }) }) + + t.Run("split", func(t *testing.T) { + t.Run("nesting limit exceeded", func(t *testing.T) { + chain := getParentChain(t, maxObjectNestingLevel+2) // one exceeds and one is a child + child := chain[len(chain)-1] + + registerContainer(child.GetContainerID()) + + require.EqualError(t, v.Validate(&child, true), "max object nesting level 2 overflow") + }) + + t.Run("nesting is ok", func(t *testing.T) { + chain := getParentChain(t, maxObjectNestingLevel+1) // one exceeds and one is a child + child := chain[len(chain)-1] + + registerContainer(child.GetContainerID()) + + require.NoError(t, v.Validate(&child, true)) + }) + }) + for _, tc := range []struct { scheme neofscrypto.Scheme object object.Object @@ -420,6 +442,44 @@ func TestFormatValidator_Validate(t *testing.T) { } } +func getParentChain(t *testing.T, nesting int) []object.Object { + var ( + signer neofscrypto.Signer + parChain []object.Object + cID = cidtest.ID() + ) + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + signer = neofsecdsa.SignerRFC6979(pk.PrivateKey) + owner := user.NewFromECDSAPublicKey(pk.PrivateKey.PublicKey) + + for i := range nesting { + o := objecttest.Object() + + o.SetOwner(owner) + o.SetSessionToken(nil) + o.SetContainerID(cID) + o.ResetRelations() + o.SetType(object.TypeRegular) + if i != 0 { + o.SetParent(&parChain[i-1]) + } + err = o.SetIDWithSignature(signer) + require.NoError(t, err) + + err := o.CalculateAndSetID() + require.NoError(t, err) + + parChain = append(parChain, o) + } + + child := &parChain[len(parChain)-1] + child.SetFirstID(oidtest.ID()) + child.SetPreviousID(oidtest.ID()) + + return parChain +} + type testSplitVerifier struct { } diff --git a/pkg/services/object/split/verify.go b/pkg/services/object/split/verify.go index 47c0a4fd62..0c310835af 100644 --- a/pkg/services/object/split/verify.go +++ b/pkg/services/object/split/verify.go @@ -149,7 +149,6 @@ func (v *Verifier) verifySinglePart(ctx context.Context, cnr cid.ID, firstID *oi var prm getsvc.HeadPrm prm.SetHeaderWriter(&hw) prm.WithAddress(childAddr) - prm.WithRawFlag(true) err := v.get.Head(ctx, prm) if err != nil {