From c716c971193eb0eb679b2a921e98b4cf6f1cdf76 Mon Sep 17 00:00:00 2001 From: Vlad <13818348+walldiss@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:31:07 +0200 Subject: [PATCH] feat(shwap): Add axis roots to Accessor (#3586) --- share/eds/edstest/testing.go | 19 +++++ share/new_eds/accessor.go | 2 + share/new_eds/close_once.go | 7 ++ share/new_eds/close_once_test.go | 6 +- share/new_eds/nd.go | 8 ++- share/new_eds/nd_test.go | 2 +- share/new_eds/proofs_cache.go | 50 ++++++++++--- share/new_eds/rsmt2d.go | 9 +++ share/new_eds/testing.go | 42 +++++++++++ share/root.go | 9 ++- share/shwap/row_namespace_data_test.go | 6 +- store/cache/accessor_cache_test.go | 4 ++ store/cache/noop.go | 7 +- store/file/ods.go | 98 ++++++++++++++++++++------ store/file/ods_test.go | 29 +++++--- store/file/q1q4_file.go | 13 ++-- store/file/q1q4_file_test.go | 11 +-- store/store.go | 45 +++++++----- store/store_test.go | 37 +++++----- 19 files changed, 306 insertions(+), 98 deletions(-) diff --git a/share/eds/edstest/testing.go b/share/eds/edstest/testing.go index 8a015032e6..a0126da2a4 100644 --- a/share/eds/edstest/testing.go +++ b/share/eds/edstest/testing.go @@ -1,6 +1,7 @@ package edstest import ( + "crypto/rand" "testing" "github.com/stretchr/testify/require" @@ -47,3 +48,21 @@ func RandEDSWithNamespace( require.NoError(t, err) return eds, roots } + +// RandomAxisRoots generates random share.AxisRoots for the given eds size. +func RandomAxisRoots(t testing.TB, edsSize int) *share.AxisRoots { + roots := make([][]byte, edsSize*2) + for i := range roots { + root := make([]byte, edsSize) + _, err := rand.Read(root) + require.NoError(t, err) + roots[i] = root + } + + rows := roots[:edsSize] + cols := roots[edsSize:] + return &share.AxisRoots{ + RowRoots: rows, + ColumnRoots: cols, + } +} diff --git a/share/new_eds/accessor.go b/share/new_eds/accessor.go index 66296ad95b..447001e24a 100644 --- a/share/new_eds/accessor.go +++ b/share/new_eds/accessor.go @@ -16,6 +16,8 @@ type Accessor interface { Size(ctx context.Context) int // DataHash returns data hash of the Accessor. DataHash(ctx context.Context) (share.DataHash, error) + // AxisRoots returns share.AxisRoots (DataAvailabilityHeader) of the Accessor. + AxisRoots(ctx context.Context) (*share.AxisRoots, error) // Sample returns share and corresponding proof for row and column indices. Implementation can // choose which axis to use for proof. Chosen axis for proof should be indicated in the returned // Sample. diff --git a/share/new_eds/close_once.go b/share/new_eds/close_once.go index 6297a32e52..2150ff7232 100644 --- a/share/new_eds/close_once.go +++ b/share/new_eds/close_once.go @@ -49,6 +49,13 @@ func (c *closeOnce) DataHash(ctx context.Context) (share.DataHash, error) { return c.f.DataHash(ctx) } +func (c *closeOnce) AxisRoots(ctx context.Context) (*share.AxisRoots, error) { + if c.closed.Load() { + return nil, errAccessorClosed + } + return c.f.AxisRoots(ctx) +} + func (c *closeOnce) Sample(ctx context.Context, rowIdx, colIdx int) (shwap.Sample, error) { if c.closed.Load() { return shwap.Sample{}, errAccessorClosed diff --git a/share/new_eds/close_once_test.go b/share/new_eds/close_once_test.go index 26b9a9b24a..c31d9ba099 100644 --- a/share/new_eds/close_once_test.go +++ b/share/new_eds/close_once_test.go @@ -51,7 +51,11 @@ func (s *stubEdsAccessorCloser) Size(context.Context) int { } func (s *stubEdsAccessorCloser) DataHash(context.Context) (share.DataHash, error) { - return nil, nil + return share.DataHash{}, nil +} + +func (s *stubEdsAccessorCloser) AxisRoots(context.Context) (*share.AxisRoots, error) { + return &share.AxisRoots{}, nil } func (s *stubEdsAccessorCloser) Sample(context.Context, int, int) (shwap.Sample, error) { diff --git a/share/new_eds/nd.go b/share/new_eds/nd.go index 58cf26cae5..45b700f482 100644 --- a/share/new_eds/nd.go +++ b/share/new_eds/nd.go @@ -13,13 +13,15 @@ import ( // avoiding the need to recalculate the row roots for each row. func NamespacedData( ctx context.Context, - root *share.AxisRoots, eds Accessor, namespace share.Namespace, ) (shwap.NamespacedData, error) { - rowIdxs := share.RowsWithNamespace(root, namespace) + roots, err := eds.AxisRoots(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get AxisRoots: %w", err) + } + rowIdxs := share.RowsWithNamespace(roots, namespace) rows := make(shwap.NamespacedData, len(rowIdxs)) - var err error for i, idx := range rowIdxs { rows[i], err = eds.RowNamespaceData(ctx, namespace, idx) if err != nil { diff --git a/share/new_eds/nd_test.go b/share/new_eds/nd_test.go index 60fd9888c3..5d3765e03f 100644 --- a/share/new_eds/nd_test.go +++ b/share/new_eds/nd_test.go @@ -21,7 +21,7 @@ func TestNamespacedData(t *testing.T) { for amount := 1; amount < sharesAmount; amount++ { eds, root := edstest.RandEDSWithNamespace(t, namespace, amount, odsSize) rsmt2d := &Rsmt2D{ExtendedDataSquare: eds} - nd, err := NamespacedData(ctx, root, rsmt2d, namespace) + nd, err := NamespacedData(ctx, rsmt2d, namespace) require.NoError(t, err) require.True(t, len(nd) > 0) require.Len(t, nd.Flatten(), amount) diff --git a/share/new_eds/proofs_cache.go b/share/new_eds/proofs_cache.go index 24f068caed..a98e6b8fc4 100644 --- a/share/new_eds/proofs_cache.go +++ b/share/new_eds/proofs_cache.go @@ -25,17 +25,23 @@ var _ AccessorStreamer = (*proofsCache)(nil) // proofsCache is eds accessor that caches proofs for rows and columns. It also caches extended // axis Shares. It is used to speed up the process of building proofs for rows and columns, -// reducing the number of reads from the underlying accessor. +// reducing the number of reads from the underlying accessor. Cache does not synchronize access +// to the underlying accessor. type proofsCache struct { inner AccessorStreamer - // lock protects axisCache - lock sync.RWMutex + // size caches the size of the data square + size atomic.Int32 + // dataHash caches the data hash + dataHash atomic.Pointer[share.DataHash] + // rootsCache caches the axis roots + rootsCache atomic.Pointer[share.AxisRoots] + // axisCacheLock protects proofCache + axisCacheLock sync.RWMutex // axisCache caches the axis Shares and proofs. Index in the slice corresponds to the axis type. // The map key is the index of the axis. axisCache []map[int]axisWithProofs - // size caches the size of the data square - size atomic.Int32 + // disableCache disables caching of rows for testing purposes disableCache bool } @@ -78,7 +84,31 @@ func (c *proofsCache) Size(ctx context.Context) int { } func (c *proofsCache) DataHash(ctx context.Context) (share.DataHash, error) { - return c.inner.DataHash(ctx) + dataHash := c.dataHash.Load() + if dataHash != nil { + return *dataHash, nil + } + loaded, err := c.inner.DataHash(ctx) + if err != nil { + return nil, err + } + c.dataHash.Store(&loaded) + return loaded, nil +} + +func (c *proofsCache) AxisRoots(ctx context.Context) (*share.AxisRoots, error) { + roots := c.rootsCache.Load() + if roots != nil { + return roots, nil + } + + // if roots are not in cache, read them from the inner accessor + roots, err := c.inner.AxisRoots(ctx) + if err != nil { + return nil, err + } + c.rootsCache.Store(roots) + return roots, nil } func (c *proofsCache) Sample(ctx context.Context, rowIdx, colIdx int) (shwap.Sample, error) { @@ -250,14 +280,14 @@ func (c *proofsCache) axisShares(ctx context.Context, axisType rsmt2d.Axis, axis } func (c *proofsCache) storeAxisInCache(axisType rsmt2d.Axis, axisIdx int, axis axisWithProofs) { - c.lock.Lock() - defer c.lock.Unlock() + c.axisCacheLock.Lock() + defer c.axisCacheLock.Unlock() c.axisCache[axisType][axisIdx] = axis } func (c *proofsCache) getAxisFromCache(axisType rsmt2d.Axis, axisIdx int) (axisWithProofs, bool) { - c.lock.RLock() - defer c.lock.RUnlock() + c.axisCacheLock.RLock() + defer c.axisCacheLock.RUnlock() ax, ok := c.axisCache[axisType][axisIdx] return ax, ok } diff --git a/share/new_eds/rsmt2d.go b/share/new_eds/rsmt2d.go index b18700f6c4..032a7f2993 100644 --- a/share/new_eds/rsmt2d.go +++ b/share/new_eds/rsmt2d.go @@ -33,6 +33,15 @@ func (eds *Rsmt2D) DataHash(context.Context) (share.DataHash, error) { return roots.Hash(), nil } +// AxisRoots returns AxisRoots of the Accessor. +func (eds *Rsmt2D) AxisRoots(context.Context) (*share.AxisRoots, error) { + roots, err := share.NewAxisRoots(eds.ExtendedDataSquare) + if err != nil { + return nil, fmt.Errorf("while creating axis roots: %w", err) + } + return roots, nil +} + // Sample returns share and corresponding proof for row and column indices. func (eds *Rsmt2D) Sample( _ context.Context, diff --git a/share/new_eds/testing.go b/share/new_eds/testing.go index cb9a6e2ec8..48a2123db5 100644 --- a/share/new_eds/testing.go +++ b/share/new_eds/testing.go @@ -35,6 +35,14 @@ func TestSuiteAccessor( t.Errorf("minSize must be power of 2: %v", maxSize) } for size := minSize; size <= maxSize; size *= 2 { + t.Run(fmt.Sprintf("DataHash:%d", size), func(t *testing.T) { + testAccessorDataHash(ctx, t, createAccessor, size) + }) + + t.Run(fmt.Sprintf("AxisRoots:%d", size), func(t *testing.T) { + testAccessorAxisRoots(ctx, t, createAccessor, size) + }) + t.Run(fmt.Sprintf("Sample:%d", size), func(t *testing.T) { testAccessorSample(ctx, t, createAccessor, size) }) @@ -64,6 +72,40 @@ func TestStreamer( }) } +func testAccessorDataHash( + ctx context.Context, + t *testing.T, + createAccessor createAccessor, + odsSize int, +) { + eds := edstest.RandEDS(t, odsSize) + fl := createAccessor(t, eds) + + roots, err := share.NewAxisRoots(eds) + require.NoError(t, err) + + datahash, err := fl.DataHash(ctx) + require.NoError(t, err) + require.Equal(t, share.DataHash(roots.Hash()), datahash) +} + +func testAccessorAxisRoots( + ctx context.Context, + t *testing.T, + createAccessor createAccessor, + odsSize int, +) { + eds := edstest.RandEDS(t, odsSize) + fl := createAccessor(t, eds) + + roots, err := share.NewAxisRoots(eds) + require.NoError(t, err) + + axisRoots, err := fl.AxisRoots(ctx) + require.NoError(t, err) + require.True(t, roots.Equals(axisRoots)) +} + func testAccessorSample( ctx context.Context, t *testing.T, diff --git a/share/root.go b/share/root.go index 997cb7e979..bad14b472f 100644 --- a/share/root.go +++ b/share/root.go @@ -11,6 +11,13 @@ import ( "github.com/celestiaorg/rsmt2d" ) +const ( + // DataHashSize is the size of the DataHash. + DataHashSize = 32 + // AxisRootSize is the size of the single root in AxisRoots. + AxisRootSize = 90 +) + // AxisRoots represents root commitment to multiple Shares. // In practice, it is a commitment to all the Data in a square. type AxisRoots = da.DataAvailabilityHeader @@ -19,7 +26,7 @@ type AxisRoots = da.DataAvailabilityHeader type DataHash []byte func (dh DataHash) Validate() error { - if len(dh) != 32 { + if len(dh) != DataHashSize { return fmt.Errorf("invalid hash size, expected 32, got %d", len(dh)) } return nil diff --git a/share/shwap/row_namespace_data_test.go b/share/shwap/row_namespace_data_test.go index 19f15ef7a6..87cf26ecaf 100644 --- a/share/shwap/row_namespace_data_test.go +++ b/share/shwap/row_namespace_data_test.go @@ -66,7 +66,7 @@ func TestValidateNamespacedRow(t *testing.T) { for amount := 1; amount < sharesAmount; amount++ { randEDS, root := edstest.RandEDSWithNamespace(t, namespace, amount, odsSize) rsmt2d := &eds.Rsmt2D{ExtendedDataSquare: randEDS} - nd, err := eds.NamespacedData(ctx, root, rsmt2d, namespace) + nd, err := eds.NamespacedData(ctx, rsmt2d, namespace) require.NoError(t, err) require.True(t, len(nd) > 0) @@ -86,9 +86,9 @@ func TestNamespacedRowProtoEncoding(t *testing.T) { const odsSize = 8 namespace := sharetest.RandV0Namespace() - randEDS, root := edstest.RandEDSWithNamespace(t, namespace, odsSize, odsSize) + randEDS, _ := edstest.RandEDSWithNamespace(t, namespace, odsSize, odsSize) rsmt2d := &eds.Rsmt2D{ExtendedDataSquare: randEDS} - nd, err := eds.NamespacedData(ctx, root, rsmt2d, namespace) + nd, err := eds.NamespacedData(ctx, rsmt2d, namespace) require.NoError(t, err) require.True(t, len(nd) > 0) diff --git a/store/cache/accessor_cache_test.go b/store/cache/accessor_cache_test.go index b37d852a80..f721fce78a 100644 --- a/store/cache/accessor_cache_test.go +++ b/store/cache/accessor_cache_test.go @@ -304,6 +304,10 @@ func (m *mockAccessor) DataHash(context.Context) (share.DataHash, error) { panic("implement me") } +func (m *mockAccessor) AxisRoots(context.Context) (*share.AxisRoots, error) { + panic("implement me") +} + func (m *mockAccessor) Sample(context.Context, int, int) (shwap.Sample, error) { panic("implement me") } diff --git a/store/cache/noop.go b/store/cache/noop.go index f327ec5c69..c4a6800d05 100644 --- a/store/cache/noop.go +++ b/store/cache/noop.go @@ -46,9 +46,12 @@ func (n NoopFile) Size(context.Context) int { return 0 } -// DataHash returns root hash of Accessor's underlying EDS. func (n NoopFile) DataHash(context.Context) (share.DataHash, error) { - return nil, nil + return share.DataHash{}, nil +} + +func (n NoopFile) AxisRoots(context.Context) (*share.AxisRoots, error) { + return &share.AxisRoots{}, nil } func (n NoopFile) Sample(context.Context, int, int) (shwap.Sample, error) { diff --git a/store/file/ods.go b/store/file/ods.go index ff39a77684..0ea4b71890 100644 --- a/store/file/ods.go +++ b/store/file/ods.go @@ -63,7 +63,7 @@ func OpenODSFile(path string) (*ODSFile, error) { // CreateODSFile creates a new file. File has to be closed after usage. func CreateODSFile( path string, - datahash share.DataHash, + roots *share.AxisRoots, eds *rsmt2d.ExtendedDataSquare, ) (*ODSFile, error) { mod := os.O_RDWR | os.O_CREATE | os.O_EXCL // ensure we fail if already exist @@ -72,15 +72,7 @@ func CreateODSFile( return nil, fmt.Errorf("file create: %w", err) } - h := &headerV0{ - fileVersion: fileV0, - fileType: ods, - shareSize: share.Size, - squareSize: uint16(eds.Width()), - datahash: datahash, - } - - err = writeODSFile(f, h, eds) + h, err := writeODSFile(f, eds, roots) if err != nil { return nil, fmt.Errorf("writing ODS file: %w", err) } @@ -97,15 +89,39 @@ func CreateODSFile( }, nil } -func writeODSFile(w io.Writer, h *headerV0, eds *rsmt2d.ExtendedDataSquare) error { +func writeODSFile(w io.Writer, eds *rsmt2d.ExtendedDataSquare, axisRoots *share.AxisRoots) (*headerV0, error) { + // write header + h := &headerV0{ + fileVersion: fileV0, + fileType: ods, + shareSize: share.Size, + squareSize: uint16(eds.Width()), + datahash: axisRoots.Hash(), + } err := writeHeader(w, h) if err != nil { - return err + return nil, fmt.Errorf("writing header: %w", err) + } + + err = writeAxisRoots(w, axisRoots) + if err != nil { + return nil, fmt.Errorf("writing axis roots: %w", err) } for _, shr := range eds.FlattenedODS() { if _, err := w.Write(shr); err != nil { - return err + return nil, fmt.Errorf("writing shares: %w", err) + } + } + return h, nil +} + +func writeAxisRoots(w io.Writer, roots *share.AxisRoots) error { + for _, roots := range [][][]byte{roots.RowRoots, roots.ColumnRoots} { + for _, root := range roots { + if _, err := w.Write(root); err != nil { + return fmt.Errorf("writing axis root: %w", err) + } } } return nil @@ -125,6 +141,30 @@ func (f *ODSFile) DataHash(context.Context) (share.DataHash, error) { return f.hdr.datahash, nil } +// AxisRoots reads AxisRoots stored in the file. AxisRoots are stored after the header and before the +// ODS data. +func (f *ODSFile) AxisRoots(context.Context) (*share.AxisRoots, error) { + roots := make([]byte, f.axisRootsSize()) + n, err := f.fl.ReadAt(roots, int64(f.hdr.Size())) + if err != nil { + return nil, fmt.Errorf("reading axis roots: %w", err) + } + if n != len(roots) { + return nil, fmt.Errorf("reading axis roots: expected %d bytes, got %d", len(roots), n) + } + rowRoots := make([][]byte, f.size()) + colRoots := make([][]byte, f.size()) + for i := 0; i < f.size(); i++ { + rowRoots[i] = roots[i*share.AxisRootSize : (i+1)*share.AxisRootSize] + colRoots[i] = roots[(f.size()+i)*share.AxisRootSize : (f.size()+i+1)*share.AxisRootSize] + } + axisRoots := &share.AxisRoots{ + RowRoots: rowRoots, + ColumnRoots: colRoots, + } + return axisRoots, nil +} + // Close closes the file. func (f *ODSFile) Close() error { return f.fl.Close() @@ -212,9 +252,9 @@ func (f *ODSFile) Reader() (io.Reader, error) { return ods.reader() } - offset := int64(f.hdr.Size()) + offset := f.sharesOffset() total := int64(f.hdr.shareSize) * int64(f.size()*f.size()/4) - reader := io.NewSectionReader(f.fl, offset, total) + reader := io.NewSectionReader(f.fl, int64(offset), total) return reader, nil } @@ -226,15 +266,16 @@ func (f *ODSFile) readAxisHalf(axisType rsmt2d.Axis, axisIdx int) (eds.AxisHalf, return f.ods.axisHalf(axisType, axisIdx) } + offset := f.sharesOffset() switch axisType { case rsmt2d.Col: - col, err := readCol(f.fl, f.hdr, axisIdx, 0) + col, err := readCol(f.fl, f.hdr, offset, 0, axisIdx) return eds.AxisHalf{ Shares: col, IsParity: false, }, err case rsmt2d.Row: - row, err := readRow(f.fl, f.hdr, axisIdx, 0) + row, err := readRow(f.fl, f.hdr, offset, 0, axisIdx) return eds.AxisHalf{ Shares: row, IsParity: false, @@ -243,6 +284,16 @@ func (f *ODSFile) readAxisHalf(axisType rsmt2d.Axis, axisIdx int) (eds.AxisHalf, return eds.AxisHalf{}, fmt.Errorf("unknown axis") } +func (f *ODSFile) sharesOffset() int { + return f.hdr.Size() + f.axisRootsSize() +} + +func (f *ODSFile) axisRootsSize() int { + // axis roots are stored in two parts: row roots and column roots, each part has size equal to + // the square size. Thus, the total amount of roots is equal to the square size * 2. + return share.AxisRootSize * 2 * f.size() +} + func (f *ODSFile) readODS() (square, error) { f.lock.RLock() ods := f.ods @@ -252,7 +303,8 @@ func (f *ODSFile) readODS() (square, error) { } // reset file pointer to the beginning of the file shares data - _, err := f.fl.Seek(int64(f.hdr.Size()), io.SeekStart) + offset := f.hdr.Size() + f.axisRootsSize() + _, err := f.fl.Seek(int64(offset), io.SeekStart) if err != nil { return nil, fmt.Errorf("discarding header: %w", err) } @@ -270,15 +322,15 @@ func (f *ODSFile) readODS() (square, error) { return square, nil } -func readRow(fl io.ReaderAt, hdr *headerV0, rowIdx, quadrantIdx int) ([]share.Share, error) { +func readRow(fl io.ReaderAt, hdr *headerV0, sharesOffset, quadrantIdx, rowIdx int) ([]share.Share, error) { shrLn := int(hdr.shareSize) odsLn := int(hdr.squareSize / 2) quadrantOffset := quadrantIdx * odsLn * odsLn * shrLn shares := make([]share.Share, odsLn) - pos := rowIdx * odsLn - offset := hdr.Size() + quadrantOffset + pos*shrLn + rowOffset := rowIdx * odsLn * shrLn + offset := sharesOffset + quadrantOffset + rowOffset axsData := make([]byte, odsLn*shrLn) if _, err := fl.ReadAt(axsData, int64(offset)); err != nil { @@ -291,7 +343,7 @@ func readRow(fl io.ReaderAt, hdr *headerV0, rowIdx, quadrantIdx int) ([]share.Sh return shares, nil } -func readCol(fl io.ReaderAt, hdr *headerV0, colIdx, quadrantIdx int) ([]share.Share, error) { +func readCol(fl io.ReaderAt, hdr *headerV0, sharesOffset, quadrantIdx, colIdx int) ([]share.Share, error) { shrLn := int(hdr.shareSize) odsLn := int(hdr.squareSize / 2) quadrantOffset := quadrantIdx * odsLn * odsLn * shrLn @@ -299,7 +351,7 @@ func readCol(fl io.ReaderAt, hdr *headerV0, colIdx, quadrantIdx int) ([]share.Sh shares := make([]share.Share, odsLn) for i := range shares { pos := colIdx + i*odsLn - offset := hdr.Size() + quadrantOffset + pos*shrLn + offset := sharesOffset + quadrantOffset + pos*shrLn shr := make(share.Share, shrLn) if _, err := fl.ReadAt(shr, int64(offset)); err != nil { diff --git a/store/file/ods_test.go b/store/file/ods_test.go index 307689092c..c26517d883 100644 --- a/store/file/ods_test.go +++ b/store/file/ods_test.go @@ -21,16 +21,20 @@ func TestCreateODSFile(t *testing.T) { t.Cleanup(cancel) edsIn := edstest.RandEDS(t, 8) - datahash := share.DataHash(rand.Bytes(32)) - path := t.TempDir() + "/" + datahash.String() - f, err := CreateODSFile(path, datahash, edsIn) + roots, err := share.NewAxisRoots(edsIn) + require.NoError(t, err) + path := t.TempDir() + "/" + roots.String() + f, err := CreateODSFile(path, roots, edsIn) require.NoError(t, err) shares, err := f.Shares(ctx) require.NoError(t, err) expected := edsIn.FlattenedODS() require.Equal(t, expected, shares) - require.Equal(t, datahash, f.hdr.datahash) + require.Equal(t, share.DataHash(roots.Hash()), f.hdr.datahash) + readRoots, err := share.NewAxisRoots(edsIn) + require.NoError(t, err) + require.True(t, roots.Equals(readRoots)) require.NoError(t, f.Close()) f, err = OpenODSFile(path) @@ -38,14 +42,19 @@ func TestCreateODSFile(t *testing.T) { shares, err = f.Shares(ctx) require.NoError(t, err) require.Equal(t, expected, shares) - require.Equal(t, datahash, f.hdr.datahash) + require.Equal(t, share.DataHash(roots.Hash()), f.hdr.datahash) + readRoots, err = share.NewAxisRoots(edsIn) + require.NoError(t, err) + require.True(t, roots.Equals(readRoots)) require.NoError(t, f.Close()) } func TestReadODSFromFile(t *testing.T) { eds := edstest.RandEDS(t, 8) + roots, err := share.NewAxisRoots(eds) + require.NoError(t, err) path := t.TempDir() + "/testfile" - f, err := CreateODSFile(path, []byte{}, eds) + f, err := CreateODSFile(path, roots, eds) require.NoError(t, err) ods, err := f.readODS() @@ -180,14 +189,18 @@ func createCachedStreamer(t testing.TB, eds *rsmt2d.ExtendedDataSquare) eds.Acce func createODSFile(t testing.TB, eds *rsmt2d.ExtendedDataSquare) *ODSFile { path := t.TempDir() + "/" + strconv.Itoa(rand.Intn(1000)) - fl, err := CreateODSFile(path, []byte{}, eds) + roots, err := share.NewAxisRoots(eds) + require.NoError(t, err) + fl, err := CreateODSFile(path, roots, eds) require.NoError(t, err) return fl } func createODSFileDisabledCache(t testing.TB, eds *rsmt2d.ExtendedDataSquare) eds.Accessor { path := t.TempDir() + "/" + strconv.Itoa(rand.Intn(1000)) - fl, err := CreateODSFile(path, []byte{}, eds) + roots, err := share.NewAxisRoots(eds) + require.NoError(t, err) + fl, err := CreateODSFile(path, roots, eds) require.NoError(t, err) fl.disableCache = true return fl diff --git a/store/file/q1q4_file.go b/store/file/q1q4_file.go index 21e72e2072..ad861a382c 100644 --- a/store/file/q1q4_file.go +++ b/store/file/q1q4_file.go @@ -33,8 +33,8 @@ func OpenQ1Q4File(path string) (*Q1Q4File, error) { }, nil } -func CreateQ1Q4File(path string, datahash share.DataHash, eds *rsmt2d.ExtendedDataSquare) (*Q1Q4File, error) { - ods, err := CreateODSFile(path, datahash, eds) +func CreateQ1Q4File(path string, roots *share.AxisRoots, eds *rsmt2d.ExtendedDataSquare) (*Q1Q4File, error) { + ods, err := CreateODSFile(path, roots, eds) if err != nil { return nil, err } @@ -57,6 +57,10 @@ func (f *Q1Q4File) DataHash(ctx context.Context) (share.DataHash, error) { return f.ods.DataHash(ctx) } +func (f *Q1Q4File) AxisRoots(ctx context.Context) (*share.AxisRoots, error) { + return f.ods.AxisRoots(ctx) +} + func (f *Q1Q4File) Sample(ctx context.Context, rowIdx, colIdx int) (shwap.Sample, error) { // use native AxisHalf implementation, to read axis from Q4 quandrant when possible half, err := f.AxisHalf(ctx, rsmt2d.Row, rowIdx) @@ -127,9 +131,10 @@ func (f *Q1Q4File) readAxisHalfFromQ4(axisType rsmt2d.Axis, axisIdx int) (eds.Ax if q4idx < 0 { return eds.AxisHalf{}, fmt.Errorf("invalid axis index for Q4: %d", axisIdx) } + offset := f.ods.sharesOffset() switch axisType { case rsmt2d.Col: - shares, err := readCol(f.ods.fl, f.ods.hdr, q4idx, 1) + shares, err := readCol(f.ods.fl, f.ods.hdr, offset, 1, q4idx) if err != nil { return eds.AxisHalf{}, err } @@ -138,7 +143,7 @@ func (f *Q1Q4File) readAxisHalfFromQ4(axisType rsmt2d.Axis, axisIdx int) (eds.Ax IsParity: true, }, nil case rsmt2d.Row: - shares, err := readRow(f.ods.fl, f.ods.hdr, q4idx, 1) + shares, err := readRow(f.ods.fl, f.ods.hdr, offset, 1, q4idx) if err != nil { return eds.AxisHalf{}, err } diff --git a/store/file/q1q4_file_test.go b/store/file/q1q4_file_test.go index d739ad40ba..7ab235b82a 100644 --- a/store/file/q1q4_file_test.go +++ b/store/file/q1q4_file_test.go @@ -21,9 +21,10 @@ func TestCreateQ1Q4File(t *testing.T) { t.Cleanup(cancel) edsIn := edstest.RandEDS(t, 8) - datahash := share.DataHash(rand.Bytes(32)) - path := t.TempDir() + "/" + datahash.String() - f, err := CreateQ1Q4File(path, datahash, edsIn) + roots, err := share.NewAxisRoots(edsIn) + require.NoError(t, err) + path := t.TempDir() + "/" + roots.String() + f, err := CreateQ1Q4File(path, roots, edsIn) require.NoError(t, err) shares, err := f.Shares(ctx) @@ -98,7 +99,9 @@ func BenchmarkSampleFromQ1Q4File(b *testing.B) { func createQ1Q4File(t testing.TB, eds *rsmt2d.ExtendedDataSquare) eds.Accessor { path := t.TempDir() + "/" + strconv.Itoa(rand.Intn(1000)) - fl, err := CreateQ1Q4File(path, []byte{}, eds) + roots, err := share.NewAxisRoots(eds) + require.NoError(t, err) + fl, err := CreateQ1Q4File(path, roots, eds) require.NoError(t, err) return fl } diff --git a/store/store.go b/store/store.go index 8fa04849bc..5a7aa3e6d8 100644 --- a/store/store.go +++ b/store/store.go @@ -97,22 +97,22 @@ func (s *Store) Close() error { func (s *Store) Put( ctx context.Context, - datahash share.DataHash, + roots *share.AxisRoots, height uint64, square *rsmt2d.ExtendedDataSquare, ) error { tNow := time.Now() + datahash := share.DataHash(roots.Hash()) lock := s.stripLock.byDatahashAndHeight(datahash, height) lock.lock() defer lock.unlock() - path := filepath.Join(s.basepath, blocksPath, datahash.String()) if datahash.IsEmptyEDS() { - err := s.ensureHeightLink(path, height) + err := s.ensureHeightLink(roots.Hash(), height) return err } - exists, err := s.createFile(path, datahash, height, square) + exists, err := s.createFile(square, roots, height) if exists { s.metrics.observePutExist(ctx) return nil @@ -134,12 +134,12 @@ func (s *Store) Put( } func (s *Store) createFile( - path string, - datahash share.DataHash, - height uint64, square *rsmt2d.ExtendedDataSquare, + roots *share.AxisRoots, + height uint64, ) (exists bool, err error) { - f, err := file.CreateQ1Q4File(path, datahash, square) + path := s.hashToPath(roots.Hash()) + f, err := file.CreateQ1Q4File(path, roots, square) if errors.Is(err, os.ErrExist) { return true, nil } @@ -154,18 +154,19 @@ func (s *Store) createFile( } // create hard link with height as name - err = s.ensureHeightLink(path, height) + err = s.ensureHeightLink(roots.Hash(), height) if err != nil { // remove the file if we failed to create a hard link - removeErr := s.removeFile(datahash) + removeErr := s.removeFile(roots.Hash()) return false, fmt.Errorf("creating hard link: %w", errors.Join(err, removeErr)) } return false, nil } -func (s *Store) ensureHeightLink(path string, height uint64) error { +func (s *Store) ensureHeightLink(datahash share.DataHash, height uint64) error { + path := s.hashToPath(datahash) // create hard link with height as name - linkPath := filepath.Join(s.basepath, heightsPath, strconv.Itoa(int(height))) + linkPath := s.heightToPath(height) err := os.Link(path, linkPath) if err != nil && !errors.Is(err, os.ErrExist) { return fmt.Errorf("creating hard link: %w", err) @@ -188,7 +189,7 @@ func (s *Store) GetByDataRoot(ctx context.Context, datahash share.DataHash) (eds } func (s *Store) getByDataRoot(datahash share.DataHash) (eds.AccessorStreamer, error) { - path := filepath.Join(s.basepath, blocksPath, datahash.String()) + path := s.hashToPath(datahash) return s.openFile(path) } @@ -208,7 +209,7 @@ func (s *Store) getByHeight(height uint64) (eds.AccessorStreamer, error) { if err == nil { return f, nil } - path := filepath.Join(s.basepath, heightsPath, strconv.Itoa(int(height))) + path := s.heightToPath(height) return s.openFile(path) } @@ -241,7 +242,7 @@ func (s *Store) HasByHash(ctx context.Context, datahash share.DataHash) (bool, e } func (s *Store) hasByHash(datahash share.DataHash) (bool, error) { - path := filepath.Join(s.basepath, blocksPath, datahash.String()) + path := s.hashToPath(datahash) return pathExists(path) } @@ -262,7 +263,7 @@ func (s *Store) hasByHeight(height uint64) (bool, error) { return true, nil } - path := filepath.Join(s.basepath, heightsPath, strconv.Itoa(int(height))) + path := s.heightToPath(height) return pathExists(path) } @@ -296,7 +297,7 @@ func (s *Store) removeLink(height uint64) error { } // remove hard link by height - heightPath := filepath.Join(s.basepath, heightsPath, strconv.Itoa(int(height))) + heightPath := s.heightToPath(height) err := os.Remove(heightPath) if err != nil && !errors.Is(err, os.ErrNotExist) { return err @@ -310,7 +311,7 @@ func (s *Store) removeFile(hash share.DataHash) error { return nil } - hashPath := filepath.Join(s.basepath, blocksPath, hash.String()) + hashPath := s.hashToPath(hash) err := os.Remove(hashPath) if err != nil && !errors.Is(err, os.ErrNotExist) { return err @@ -318,6 +319,14 @@ func (s *Store) removeFile(hash share.DataHash) error { return nil } +func (s *Store) hashToPath(datahash share.DataHash) string { + return filepath.Join(s.basepath, blocksPath, datahash.String()) +} + +func (s *Store) heightToPath(height uint64) string { + return filepath.Join(s.basepath, heightsPath, strconv.Itoa(int(height))) +} + func accessorLoader(accessor eds.AccessorStreamer) cache.OpenAccessorFn { return func(context.Context) (eds.AccessorStreamer, error) { return wrapAccessor(accessor), nil diff --git a/store/store_test.go b/store/store_test.go index c60fb8c4a2..b58562e1a8 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -2,7 +2,6 @@ package store import ( "context" - "crypto/rand" "sync/atomic" "testing" "time" @@ -33,7 +32,7 @@ func TestEDSStore(t *testing.T) { eds, roots := randomEDS(t) height := height.Add(1) - err := edsStore.Put(ctx, roots.Hash(), height, eds) + err := edsStore.Put(ctx, roots, height, eds) require.NoError(t, err) // file should become available by hash @@ -54,7 +53,7 @@ func TestEDSStore(t *testing.T) { eds, roots := randomEDS(t) height := height.Add(1) - err = edsStore.Put(ctx, roots.Hash(), height, eds) + err = edsStore.Put(ctx, roots, height, eds) require.NoError(t, err) // file should be cached after put @@ -74,10 +73,10 @@ func TestEDSStore(t *testing.T) { eds, roots := randomEDS(t) height := height.Add(1) - err := edsStore.Put(ctx, roots.Hash(), height, eds) + err := edsStore.Put(ctx, roots, height, eds) require.NoError(t, err) - err = edsStore.Put(ctx, roots.Hash(), height, eds) + err = edsStore.Put(ctx, roots, height, eds) require.NoError(t, err) // TODO: check amount of files in the store after the second Put // after store supports listing @@ -87,7 +86,7 @@ func TestEDSStore(t *testing.T) { eds, roots := randomEDS(t) height := height.Add(1) - err = edsStore.Put(ctx, roots.Hash(), height, eds) + err = edsStore.Put(ctx, roots, height, eds) require.NoError(t, err) f, err := edsStore.GetByHeight(ctx, height) @@ -105,7 +104,7 @@ func TestEDSStore(t *testing.T) { eds, roots := randomEDS(t) height := height.Add(1) - err := edsStore.Put(ctx, roots.Hash(), height, eds) + err := edsStore.Put(ctx, roots, height, eds) require.NoError(t, err) f, err := edsStore.GetByDataRoot(ctx, roots.Hash()) @@ -146,7 +145,7 @@ func TestEDSStore(t *testing.T) { eds, roots := randomEDS(t) height := height.Add(1) - err = edsStore.Put(ctx, roots.Hash(), height, eds) + err = edsStore.Put(ctx, roots, height, eds) require.NoError(t, err) err = edsStore.Remove(ctx, height, roots.Hash()) @@ -200,7 +199,7 @@ func TestEDSStore(t *testing.T) { require.NoError(t, err) require.False(t, has) - err = edsStore.Put(ctx, roots.Hash(), height, eds) + err = edsStore.Put(ctx, roots, height, eds) require.NoError(t, err) // assert that the empty file can be accessed by height @@ -224,7 +223,7 @@ func TestEDSStore(t *testing.T) { // store empty EDSs for i := from; i <= to; i++ { - err := edsStore.Put(ctx, roots.Hash(), uint64(i), eds) + err := edsStore.Put(ctx, roots, uint64(i), eds) require.NoError(t, err) } @@ -255,19 +254,17 @@ func BenchmarkStore(b *testing.B) { eds := edstest.RandEDS(b, 128) require.NoError(b, err) - // BenchmarkStore/bench_put_128-10 27 43968818 ns/op (~43ms) + // BenchmarkStore/bench_put_128-10 27 79025268 ns/op (~79ms) b.Run("put 128", func(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - bytes := make([]byte, 5) - rand.Read(bytes) //nolint:errcheck - h := share.DataHash(bytes) - _ = edsStore.Put(ctx, h, uint64(i), eds) + roots := edstest.RandomAxisRoots(b, 1) + _ = edsStore.Put(ctx, roots, uint64(i), eds) } }) // read 128 EDSs does not read full EDS, but only the header - // BenchmarkStore/bench_read_128-10 82766 14678 ns/op (~14ms) + // BenchmarkStore/bench_read_128-10 82766 14678 ns/op (~14mcs) b.Run("open by height, 128", func(b *testing.B) { edsStore, err := NewStore(DefaultParameters(), b.TempDir()) require.NoError(b, err) @@ -279,7 +276,7 @@ func BenchmarkStore(b *testing.B) { require.NoError(b, err) height := uint64(1984) - err = edsStore.Put(ctx, roots.Hash(), height, eds) + err = edsStore.Put(ctx, roots, height, eds) require.NoError(b, err) b.ResetTimer() @@ -290,7 +287,7 @@ func BenchmarkStore(b *testing.B) { } }) - // BenchmarkStore/open_by_hash,_128-10 72921 16799 ns/op (~16ms) + // BenchmarkStore/open_by_hash,_128-10 72921 16799 ns/op (~16mcs) b.Run("open by hash, 128", func(b *testing.B) { edsStore, err := NewStore(DefaultParameters(), b.TempDir()) require.NoError(b, err) @@ -302,7 +299,7 @@ func BenchmarkStore(b *testing.B) { require.NoError(b, err) height := uint64(1984) - err = edsStore.Put(ctx, roots.Hash(), height, eds) + err = edsStore.Put(ctx, roots, height, eds) require.NoError(b, err) b.ResetTimer() @@ -314,7 +311,7 @@ func BenchmarkStore(b *testing.B) { }) } -func randomEDS(t *testing.T) (*rsmt2d.ExtendedDataSquare, *share.AxisRoots) { +func randomEDS(t testing.TB) (*rsmt2d.ExtendedDataSquare, *share.AxisRoots) { eds := edstest.RandEDS(t, 4) roots, err := share.NewAxisRoots(eds) require.NoError(t, err)