diff --git a/share/namespace.go b/share/namespace.go index df4ad74058..2cea574bbc 100644 --- a/share/namespace.go +++ b/share/namespace.go @@ -2,7 +2,9 @@ package share import ( "bytes" + "encoding/binary" "encoding/hex" + "errors" "fmt" appns "github.com/celestiaorg/celestia-app/pkg/namespace" @@ -182,3 +184,49 @@ func (n Namespace) IsGreater(target Namespace) bool { func (n Namespace) IsGreaterOrEqualThan(target Namespace) bool { return bytes.Compare(n, target) > -1 } + +// AddInt adds arbitrary int value to namespace, treating namespace as big-endian +// implementation of int +func (n Namespace) AddInt(val int) (Namespace, error) { + if val == 0 { + return n, nil + } + // Convert the input integer to a byte slice and add it to result slice + result := make([]byte, len(n)) + if val > 0 { + binary.BigEndian.PutUint64(result[len(n)-8:], uint64(val)) + } else { + binary.BigEndian.PutUint64(result[len(n)-8:], uint64(-val)) + } + + // Perform addition byte by byte + var carry int + for i := len(n) - 1; i >= 0; i-- { + var sum int + if val > 0 { + sum = int(n[i]) + int(result[i]) + carry + } else { + sum = int(n[i]) - int(result[i]) + carry + } + + switch { + case sum > 255: + carry = 1 + sum -= 256 + case sum < 0: + carry = -1 + sum += 256 + default: + carry = 0 + } + + result[i] = uint8(sum) + } + + // Handle any remaining carry + if carry != 0 { + return nil, errors.New("namespace overflow") + } + + return result, nil +} diff --git a/share/shwap/namespaced_data.go b/share/shwap/namespaced_data.go index 558c3c03aa..3f230f2e44 100644 --- a/share/shwap/namespaced_data.go +++ b/share/shwap/namespaced_data.go @@ -33,22 +33,17 @@ type RowNamespaceData struct { // Verify checks the integrity of the NamespacedData against a provided root and namespace. func (ns NamespacedData) Verify(root *share.Root, namespace share.Namespace) error { - var originalRoots [][]byte - for _, rowRoot := range root.RowRoots { - if !namespace.IsOutsideRange(rowRoot, rowRoot) { - originalRoots = append(originalRoots, rowRoot) - } - } + rowRoots, _, _ := share.FilterRootByNamespace(root, namespace) - if len(originalRoots) != len(ns) { - return fmt.Errorf("expected %d rows, found %d rows", len(originalRoots), len(ns)) + if len(rowRoots) != len(ns) { + return fmt.Errorf("expected %d rows, found %d rows", len(rowRoots), len(ns)) } for i, row := range ns { if row.Proof == nil || len(row.Shares) == 0 { return fmt.Errorf("row %d is missing proofs or shares", i) } - if !row.VerifyInclusion(originalRoots[i], namespace) { + if !row.VerifyInclusion(rowRoots[i], namespace) { return fmt.Errorf("failed to verify row %d", i) } } @@ -156,6 +151,8 @@ func NamespacedRowFromShares(shares []share.Share, namespace share.Namespace, ro } } if count == 0 { + // FIXME: This should return Non-inclusion proofs instead. Need support in app wrapper to generate + // absence proofs. return RowNamespaceData{}, fmt.Errorf("no shares found in the namespace for row %d", rowIndex) } diff --git a/share/shwap/namespaced_data_test.go b/share/shwap/namespaced_data_test.go new file mode 100644 index 0000000000..b5765763d5 --- /dev/null +++ b/share/shwap/namespaced_data_test.go @@ -0,0 +1,104 @@ +package shwap + +import ( + "bytes" + "slices" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/edstest" + "github.com/celestiaorg/celestia-node/share/sharetest" +) + +func TestNamespacedRowFromShares(t *testing.T) { + const odsSize = 8 + + minNamespace, err := share.NewBlobNamespaceV0(slices.Concat(bytes.Repeat([]byte{0}, 8), []byte{1, 0})) + require.NoError(t, err) + err = minNamespace.ValidateForData() + require.NoError(t, err) + + for namespacedAmount := 1; namespacedAmount < odsSize; namespacedAmount++ { + shares := sharetest.RandSharesWithNamespace(t, minNamespace, namespacedAmount, odsSize) + parity, err := share.DefaultRSMT2DCodec().Encode(shares) + require.NoError(t, err) + extended := slices.Concat(shares, parity) + + nr, err := NamespacedRowFromShares(extended, minNamespace, 0) + require.NoError(t, err) + require.Equal(t, namespacedAmount, len(nr.Shares)) + } +} + +func TestNamespacedRowFromSharesNonIncluded(t *testing.T) { + // TODO: this will fail until absence proof support is added + t.Skip() + + const odsSize = 8 + // Test absent namespace + shares := sharetest.RandShares(t, odsSize) + absentNs, err := share.GetNamespace(shares[0]).AddInt(1) + require.NoError(t, err) + + parity, err := share.DefaultRSMT2DCodec().Encode(shares) + require.NoError(t, err) + extended := slices.Concat(shares, parity) + + nr, err := NamespacedRowFromShares(extended, absentNs, 0) + require.NoError(t, err) + require.Len(t, nr.Shares, 0) + require.True(t, nr.Proof.IsOfAbsence()) +} + +func TestNamespacedSharesFromEDS(t *testing.T) { + const odsSize = 8 + sharesAmount := odsSize * odsSize + namespace := sharetest.RandV0Namespace() + for amount := 1; amount < sharesAmount; amount++ { + eds, root := edstest.RandEDSWithNamespace(t, namespace, amount, odsSize) + nd, err := NewNamespacedSharesFromEDS(eds, namespace) + require.NoError(t, err) + require.True(t, len(nd) > 0) + require.Len(t, nd.Flatten(), amount) + + err = nd.Verify(root, namespace) + require.NoError(t, err) + } +} + +func TestValidateNamespacedRow(t *testing.T) { + const odsSize = 8 + sharesAmount := odsSize * odsSize + namespace := sharetest.RandV0Namespace() + for amount := 1; amount < sharesAmount; amount++ { + eds, root := edstest.RandEDSWithNamespace(t, namespace, amount, odsSize) + nd, err := NewNamespacedSharesFromEDS(eds, namespace) + require.NoError(t, err) + require.True(t, len(nd) > 0) + + _, from, to := share.FilterRootByNamespace(root, namespace) + require.Len(t, nd, to-from) + idx := from + for _, row := range nd { + err = row.Validate(root, namespace, idx) + require.NoError(t, err) + idx++ + } + } +} + +func TestNamespacedRowProtoEncoding(t *testing.T) { + const odsSize = 8 + namespace := sharetest.RandV0Namespace() + eds, _ := edstest.RandEDSWithNamespace(t, namespace, odsSize, odsSize) + nd, err := NewNamespacedSharesFromEDS(eds, namespace) + require.NoError(t, err) + require.True(t, len(nd) > 0) + + expected := nd[0] + pb := expected.ToProto() + ndOut := NamespacedRowFromProto(pb) + require.Equal(t, expected, ndOut) +} diff --git a/share/shwap/row.go b/share/shwap/row.go index 1570756740..423fb5bafe 100644 --- a/share/shwap/row.go +++ b/share/shwap/row.go @@ -110,9 +110,9 @@ func RowFromProto(r *pb.Row) Row { } } -// NewRowFromEDS constructs a new Row from an Extended Data Square based on the specified index and +// RowFromEDS constructs a new Row from an Extended Data Square based on the specified index and // side. -func NewRowFromEDS(square *rsmt2d.ExtendedDataSquare, idx int, side RowSide) Row { +func RowFromEDS(square *rsmt2d.ExtendedDataSquare, idx int, side RowSide) Row { sqrLn := int(square.Width()) shares := square.Row(uint(idx)) var halfShares []share.Share diff --git a/share/shwap/row_test.go b/share/shwap/row_test.go new file mode 100644 index 0000000000..007b191703 --- /dev/null +++ b/share/shwap/row_test.go @@ -0,0 +1,114 @@ +package shwap + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/edstest" +) + +func TestNewRowFromEDS(t *testing.T) { + const odsSize = 8 + eds := edstest.RandEDS(t, odsSize) + + for rowIdx := 0; rowIdx < odsSize*2; rowIdx++ { + for _, side := range []RowSide{Left, Right} { + row := RowFromEDS(eds, rowIdx, side) + + want := eds.Row(uint(rowIdx)) + shares, err := row.Shares() + require.NoError(t, err) + require.Equal(t, want, shares) + + var half []share.Share + if side == Right { + half = want[odsSize:] + } else { + half = want[:odsSize] + } + require.Equal(t, half, row.halfShares) + require.Equal(t, side, row.side) + } + } +} + +func TestRowValidate(t *testing.T) { + const odsSize = 8 + eds := edstest.RandEDS(t, odsSize) + root, err := share.NewRoot(eds) + require.NoError(t, err) + + for rowIdx := 0; rowIdx < odsSize*2; rowIdx++ { + for _, side := range []RowSide{Left, Right} { + row := RowFromEDS(eds, rowIdx, side) + + err := row.VerifyRoot(root, rowIdx) + require.NoError(t, err) + err = row.Validate(root, rowIdx) + require.NoError(t, err) + } + } +} + +func TestRowValidateNegativeCases(t *testing.T) { + eds := edstest.RandEDS(t, 8) // Generate a random Extended Data Square of size 8 + root, err := share.NewRoot(eds) + require.NoError(t, err) + row := RowFromEDS(eds, 0, Left) + + // Test with incorrect side specification + invalidSideRow := Row{halfShares: row.halfShares, side: RowSide(999)} + err = invalidSideRow.Validate(root, 0) + require.Error(t, err, "should error on invalid row side") + + // Test with invalid shares (more shares than expected) + incorrectShares := make([]share.Share, (eds.Width()/2)+1) // Adding an extra share + for i := range incorrectShares { + incorrectShares[i] = eds.GetCell(uint(i), 0) + } + invalidRow := Row{halfShares: incorrectShares, side: Left} + err = invalidRow.Validate(root, 0) + require.Error(t, err, "should error on incorrect number of shares") + + // Test with empty shares + emptyRow := Row{halfShares: []share.Share{}, side: Left} + err = emptyRow.Validate(root, 0) + require.Error(t, err, "should error on empty halfShares") + + // Doesn't match root. Corrupt root hash + root.RowRoots[0][len(root.RowRoots[0])-1] ^= 0xFF + err = row.Validate(root, 0) + require.Error(t, err, "should error on invalid root hash") +} + +func TestRowProtoEncoding(t *testing.T) { + const odsSize = 8 + eds := edstest.RandEDS(t, odsSize) + + for rowIdx := 0; rowIdx < odsSize*2; rowIdx++ { + for _, side := range []RowSide{Left, Right} { + row := RowFromEDS(eds, rowIdx, side) + + pb := row.ToProto() + rowOut := RowFromProto(pb) + require.Equal(t, row, rowOut) + } + } +} + +// BenchmarkRowValidate benchmarks the performance of row validation. +// BenchmarkRowValidate-10 9591 121802 ns/op +func BenchmarkRowValidate(b *testing.B) { + const odsSize = 32 + eds := edstest.RandEDS(b, odsSize) + root, err := share.NewRoot(eds) + require.NoError(b, err) + row := RowFromEDS(eds, 0, Left) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = row.Validate(root, 0) + } +} diff --git a/share/shwap/sample_test.go b/share/shwap/sample_test.go new file mode 100644 index 0000000000..39c0e72b41 --- /dev/null +++ b/share/shwap/sample_test.go @@ -0,0 +1,127 @@ +package shwap + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/celestiaorg/rsmt2d" + + "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/edstest" +) + +func TestNewSampleFromEDS(t *testing.T) { + const odsSize = 8 + eds := edstest.RandEDS(t, odsSize) + + for _, proofType := range []rsmt2d.Axis{rsmt2d.Row, rsmt2d.Col} { + for rowIdx := 0; rowIdx < odsSize*2; rowIdx++ { + for colIdx := 0; colIdx < odsSize*2; colIdx++ { + sample, err := SampleFromEDS(eds, proofType, rowIdx, colIdx) + require.NoError(t, err) + + want := eds.GetCell(uint(rowIdx), uint(colIdx)) + require.Equal(t, want, sample.Share) + require.Equal(t, proofType, sample.ProofType) + require.NotNil(t, sample.Proof) + require.Equal(t, sample.Proof.End()-sample.Proof.Start(), 1) + require.Len(t, sample.Proof.Nodes(), 4) + } + } + } +} + +func TestSampleValidate(t *testing.T) { + const odsSize = 8 + eds := edstest.RandEDS(t, odsSize) + root, err := share.NewRoot(eds) + require.NoError(t, err) + + for _, proofType := range []rsmt2d.Axis{rsmt2d.Row, rsmt2d.Col} { + for rowIdx := 0; rowIdx < odsSize*2; rowIdx++ { + for colIdx := 0; colIdx < odsSize*2; colIdx++ { + sample, err := SampleFromEDS(eds, proofType, rowIdx, colIdx) + require.NoError(t, err) + + require.True(t, sample.VerifyInclusion(root, rowIdx, colIdx)) + require.NoError(t, sample.Validate(root, rowIdx, colIdx)) + } + } + } +} + +// TestSampleNegativeVerifyInclusion checks +func TestSampleNegativeVerifyInclusion(t *testing.T) { + const odsSize = 8 + eds := edstest.RandEDS(t, odsSize) + root, err := share.NewRoot(eds) + require.NoError(t, err) + + sample, err := SampleFromEDS(eds, rsmt2d.Row, 0, 0) + require.NoError(t, err) + included := sample.VerifyInclusion(root, 0, 0) + require.True(t, included) + + // incorrect row index + included = sample.VerifyInclusion(root, 1, 0) + require.False(t, included) + + // incorrect col index is not used in the inclusion proof verification + included = sample.VerifyInclusion(root, 0, 1) + require.True(t, included) + + // Corrupt the share + sample.Share[0] ^= 0xFF + included = sample.VerifyInclusion(root, 0, 0) + require.False(t, included) + + // incorrect proofType + sample, err = SampleFromEDS(eds, rsmt2d.Row, 0, 0) + require.NoError(t, err) + sample.ProofType = rsmt2d.Col + included = sample.VerifyInclusion(root, 0, 0) + require.False(t, included) + + // Corrupt the last root hash byte + sample, err = SampleFromEDS(eds, rsmt2d.Row, 0, 0) + require.NoError(t, err) + root.RowRoots[0][len(root.RowRoots[0])-1] ^= 0xFF + included = sample.VerifyInclusion(root, 0, 0) + require.False(t, included) +} + +func TestSampleProtoEncoding(t *testing.T) { + const odsSize = 8 + eds := edstest.RandEDS(t, odsSize) + + for _, proofType := range []rsmt2d.Axis{rsmt2d.Row, rsmt2d.Col} { + for rowIdx := 0; rowIdx < odsSize*2; rowIdx++ { + for colIdx := 0; colIdx < odsSize*2; colIdx++ { + sample, err := SampleFromEDS(eds, proofType, rowIdx, colIdx) + require.NoError(t, err) + + pb := sample.ToProto() + sampleOut := SampleFromProto(pb) + require.NoError(t, err) + require.Equal(t, sample, sampleOut) + } + } + } +} + +// BenchmarkSampleValidate benchmarks the performance of sample validation. +// BenchmarkSampleValidate-10 284829 3935 ns/op +func BenchmarkSampleValidate(b *testing.B) { + const odsSize = 32 + eds := edstest.RandEDS(b, odsSize) + root, err := share.NewRoot(eds) + require.NoError(b, err) + sample, err := SampleFromEDS(eds, rsmt2d.Row, 0, 0) + require.NoError(b, err) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = sample.Validate(root, 0, 0) + } +}