Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/neofs-node/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/google/uuid"
lru "github.com/hashicorp/golang-lru/v2"
iec "github.com/nspcc-dev/neofs-node/internal/ec"
coreclient "github.com/nspcc-dev/neofs-node/pkg/core/client"
containercore "github.com/nspcc-dev/neofs-node/pkg/core/container"
"github.com/nspcc-dev/neofs-node/pkg/core/netmap"
Expand Down Expand Up @@ -804,6 +805,7 @@ type containerNodesSorter struct {

func (x *containerNodesSorter) Unsorted() [][]netmapsdk.NodeInfo { return x.policy.nodeSets }
func (x *containerNodesSorter) PrimaryCounts() []uint { return x.policy.repCounts }
func (x *containerNodesSorter) ECRules() []iec.Rule { return nil }
func (x *containerNodesSorter) SortForObject(obj oid.ID) ([][]netmapsdk.NodeInfo, error) {
cacheKey := objectNodesCacheKey{epoch: x.curEpoch}
cacheKey.addr.SetContainer(x.cnrID)
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ require (
github.com/google/uuid v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/klauspost/compress v1.17.11
github.com/klauspost/reedsolomon v1.12.4
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.5.0
github.com/mr-tron/base58 v1.2.0
github.com/multiformats/go-multiaddr v0.12.2
github.com/mxschmitt/golang-combinations v1.2.0
github.com/nspcc-dev/hrw/v2 v2.0.3
github.com/nspcc-dev/locode-db v0.6.0
github.com/nspcc-dev/neo-go v0.111.0
Expand Down Expand Up @@ -62,7 +64,7 @@ require (
github.com/holiman/uint256 v1.3.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/go-cid v0.4.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
Expand Down
9 changes: 6 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,10 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/reedsolomon v1.12.4 h1:5aDr3ZGoJbgu/8+j45KtUJxzYm8k08JGtB9Wx1VQ4OA=
github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
Expand Down Expand Up @@ -185,6 +187,8 @@ github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/n
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxschmitt/golang-combinations v1.2.0 h1:V5E7MncIK8Yr1SL/SpdqMuSquFsfoIs5auI7Y3n8z14=
github.com/mxschmitt/golang-combinations v1.2.0/go.mod h1:RCm5eR03B+JrBOMRDLsKZWShluXdrHu+qwhPEJ0miBM=
github.com/nspcc-dev/bbolt v0.0.0-20250612101626-5df2544a4a22 h1:M5Nmg1iCnbZngzIBDIlMr9vW+okFfcSMBvBlXG8r+14=
github.com/nspcc-dev/bbolt v0.0.0-20250612101626-5df2544a4a22/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
github.com/nspcc-dev/dbft v0.4.0 h1:4/atD4GrrMEtrYBDiZPrPzdKZ6ws7PR/cg0M4DEdVeI=
Expand Down Expand Up @@ -358,7 +362,6 @@ golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
Expand Down
80 changes: 80 additions & 0 deletions internal/ec/ec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package ec

import (
"fmt"
"slices"
"strconv"

"github.com/klauspost/reedsolomon"
)

// Erasure coding attributes.
const (
AttributePrefix = "__NEOFS__EC_"
AttributeRuleIdx = AttributePrefix + "RULE_IDX"
AttributePartIdx = AttributePrefix + "PART_IDX"
)

// Rule represents erasure coding rule for object payload's encoding and placement.
type Rule struct {
DataPartNum uint8
ParityPartNum uint8
}

// String implements [fmt.Stringer].
func (x Rule) String() string {
return strconv.FormatUint(uint64(x.DataPartNum), 10) + "/" + strconv.FormatUint(uint64(x.ParityPartNum), 10)
}

// Encode encodes given data according to specified EC rule and returns coded
// parts. First [Rule.DataPartNum] elements are data parts, other
// [Rule.ParityPartNum] ones are parity blocks.
//
// All parts are the same length. If data len is not divisible by
// [Rule.DataPartNum], last data part is aligned with zeros.
//
// If data is empty, all parts are nil.
func Encode(rule Rule, data []byte) ([][]byte, error) {
if len(data) == 0 {
return make([][]byte, rule.DataPartNum+rule.ParityPartNum), nil
}

// TODO: Explore reedsolomon.Option for performance improvement. https://github.com/nspcc-dev/neofs-node/issues/3501
enc, err := reedsolomon.New(int(rule.DataPartNum), int(rule.ParityPartNum))
if err != nil { // should never happen with correct rule
return nil, fmt.Errorf("init Reed-Solomon encoder: %w", err)
}

parts, err := enc.Split(data)
if err != nil {
return nil, fmt.Errorf("split data: %w", err)
}

if err := enc.Encode(parts); err != nil {
return nil, fmt.Errorf("calculate Reed-Solomon parity: %w", err)
}

return parts, nil
}

// Decode decodes source data of known len from EC parts obtained by applying
// specified rule.
func Decode(rule Rule, dataLen uint64, parts [][]byte) ([]byte, error) {
// TODO: Explore reedsolomon.Option for performance improvement. https://github.com/nspcc-dev/neofs-node/issues/3501
dec, err := reedsolomon.New(int(rule.DataPartNum), int(rule.ParityPartNum))
if err != nil { // should never happen with correct rule
return nil, fmt.Errorf("init Reed-Solomon decoder: %w", err)
}

required := make([]bool, rule.DataPartNum+rule.ParityPartNum)
for i := range rule.DataPartNum {
required[i] = true
}

if err := dec.ReconstructSome(parts, required); err != nil {
return nil, fmt.Errorf("restore Reed-Solomon: %w", err)
}

// TODO: last part may be shorter, do not overallocate buffer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt it's a problem practically, just a tiny rounding error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thinkin about parts of several MB len with the tiny last one (1B in particular)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But aren't EC part all ~same size? The difference I expect here is on the order of NUM_OF_PARTS bytes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they all exactly the same size. So the last part may require alignment. And it may be quite large

return slices.Concat(parts[:rule.DataPartNum]...)[:dataLen], nil
}
76 changes: 76 additions & 0 deletions internal/ec/ec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package ec_test

import (
"testing"

"github.com/klauspost/reedsolomon"
iec "github.com/nspcc-dev/neofs-node/internal/ec"
islices "github.com/nspcc-dev/neofs-node/internal/slices"
"github.com/nspcc-dev/neofs-node/internal/testutil"
"github.com/stretchr/testify/require"
)

func TestRule_String(t *testing.T) {
r := iec.Rule{
DataPartNum: 12,
ParityPartNum: 23,
}
require.Equal(t, "12/23", r.String())
}

func testEncode(t *testing.T, rule iec.Rule, data []byte) {
ln := uint64(len(data))

parts, err := iec.Encode(rule, data)
require.NoError(t, err)

res, err := iec.Decode(rule, ln, parts)
require.NoError(t, err)
require.Equal(t, data, res)

for lostCount := 1; lostCount <= int(rule.ParityPartNum); lostCount++ {
for _, lostIdxs := range islices.IndexCombos(len(parts), lostCount) {
res, err := iec.Decode(rule, ln, islices.NilTwoDimSliceElements(parts, lostIdxs))
require.NoError(t, err)
require.Equal(t, data, res)
}
}

for _, lostIdxs := range islices.IndexCombos(len(parts), int(rule.ParityPartNum)+1) {
_, err := iec.Decode(rule, ln, islices.NilTwoDimSliceElements(parts, lostIdxs))
require.ErrorContains(t, err, "restore Reed-Solomon")
require.ErrorIs(t, err, reedsolomon.ErrTooFewShards)
}
}

func TestEncode(t *testing.T) {
rules := []iec.Rule{
{DataPartNum: 3, ParityPartNum: 1},
{DataPartNum: 12, ParityPartNum: 4},
}

data := testutil.RandByteSlice(4 << 10)

t.Run("empty", func(t *testing.T) {
for _, rule := range rules {
t.Run(rule.String(), func(t *testing.T) {
test := func(t *testing.T, data []byte) {
res, err := iec.Encode(rule, []byte{})
require.NoError(t, err)

total := int(rule.DataPartNum + rule.ParityPartNum)
require.Len(t, res, total)
require.EqualValues(t, total, islices.CountNilsInTwoDimSlice(res))
}
test(t, nil)
test(t, []byte{})
})
}
})

for _, rule := range rules {
t.Run(rule.String(), func(t *testing.T) {
testEncode(t, rule, data)
})
}
}
Loading
Loading