From 814921d22d992c81c3913573317d60f1de63a9d0 Mon Sep 17 00:00:00 2001 From: Arkadiy Kukarkin Date: Fri, 1 May 2026 19:50:53 +0100 Subject: [PATCH] content-provider: accept v2 piece CID in /piece/ --- service/contentprovider/http.go | 32 +++++++++++++++++++------ service/contentprovider/http_test.go | 36 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/service/contentprovider/http.go b/service/contentprovider/http.go index ad4e8957..a7fc35c9 100644 --- a/service/contentprovider/http.go +++ b/service/contentprovider/http.go @@ -14,6 +14,7 @@ import ( "github.com/data-preservation-programs/singularity/storagesystem" "github.com/data-preservation-programs/singularity/store" "github.com/data-preservation-programs/singularity/util" + commcid "github.com/filecoin-project/go-fil-commcid" "github.com/fxamacker/cbor/v2" "github.com/ipfs/boxo/blockservice" "github.com/ipfs/boxo/exchange/offline" @@ -207,9 +208,9 @@ func getPieceMetadata(ctx context.Context, db *gorm.DB, car model.Car) (*PieceMe // - An error if there was a problem handling the request. func GetMetadataHandler(c echo.Context, db *gorm.DB) error { id := c.Param("id") - pieceCid, err := cid.Parse(id) + pieceCid, err := parsePieceCID(id) if err != nil { - return c.String(http.StatusBadRequest, "failed to parse piece CID: "+err.Error()) + return c.String(http.StatusBadRequest, err.Error()) } // filter to rows with an attachment -- orphaned cars (attachment_id NULL @@ -250,6 +251,26 @@ func (s *HTTPServer) getMetadataHandler(c echo.Context) error { return GetMetadataHandler(c, s.dbNoContext.WithContext(c.Request().Context())) } +// parsePieceCID accepts either a v1 (FilCommitmentUnsealed multicodec) or +// v2 (raw multicodec with PieceMh hash carrying CommP + payload size) CID +// and returns the v1 form. Curio and other FWSS-mediated callers use v2 +// CIDs in their /piece/ URLs; the singularity store still indexes by +// v1, so we normalize on input. +func parsePieceCID(id string) (cid.Cid, error) { + c, err := cid.Parse(id) + if err != nil { + return cid.Undef, fmt.Errorf("failed to parse piece CID: %w", err) + } + if c.Type() == cid.FilCommitmentUnsealed { + return c, nil + } + v1, _, err := commcid.PieceCidV1FromV2(c) + if err != nil { + return cid.Undef, fmt.Errorf("CID %s is neither v1 commp nor a v2 piece CID: %w", id, err) + } + return v1, nil +} + type PieceMetadata struct { Car model.Car `cbor:"1,keyasint,omitempty" json:"car"` Storage model.Storage `cbor:"2,keyasint,omitempty" json:"storage"` @@ -383,12 +404,9 @@ func SetCommonHeaders(c echo.Context, pieceCid string) { // - An error if there was a problem handling the request. func (s *HTTPServer) handleGetPiece(c echo.Context) error { id := c.Param("id") - pieceCid, err := cid.Parse(id) + pieceCid, err := parsePieceCID(id) if err != nil { - return c.String(http.StatusBadRequest, "failed to parse piece CID: "+err.Error()) - } - if pieceCid.Type() != cid.FilCommitmentUnsealed { - return c.String(http.StatusBadRequest, "CID is not a commp") + return c.String(http.StatusBadRequest, err.Error()) } reader, lastModified, err := s.findPiece(c.Request().Context(), pieceCid) diff --git a/service/contentprovider/http_test.go b/service/contentprovider/http_test.go index 239b3650..1133c805 100644 --- a/service/contentprovider/http_test.go +++ b/service/contentprovider/http_test.go @@ -13,6 +13,7 @@ import ( "github.com/data-preservation-programs/singularity/model" "github.com/data-preservation-programs/singularity/store" "github.com/data-preservation-programs/singularity/util/testutil" + commcid "github.com/filecoin-project/go-fil-commcid" "github.com/gotidy/ptr" "github.com/ipfs/boxo/blockservice" "github.com/ipfs/boxo/exchange/offline" @@ -450,3 +451,38 @@ func TestIPFSGateway_FileChanged(t *testing.T) { require.Equal(t, http.StatusConflict, rec.Code) }) } + +func TestParsePieceCID(t *testing.T) { + commD := make([]byte, 32) + for i := range commD { + commD[i] = byte(i) + } + v1, err := commcid.DataCommitmentV1ToCID(commD) + require.NoError(t, err) + require.Equal(t, uint64(cid.FilCommitmentUnsealed), v1.Type()) + + // v2 piece CID carrying the same commitment; Curio/FWSS callers use this form + v2, err := commcid.PieceCidV2FromV1(v1, 1016) + require.NoError(t, err) + require.NotEqual(t, v1, v2) + + t.Run("v1 passes through unchanged", func(t *testing.T) { + got, err := parsePieceCID(v1.String()) + require.NoError(t, err) + require.Equal(t, v1, got) + }) + t.Run("v2 normalizes to v1", func(t *testing.T) { + got, err := parsePieceCID(v2.String()) + require.NoError(t, err) + require.Equal(t, v1, got) + }) + t.Run("non-piece CID is rejected", func(t *testing.T) { + raw := cid.NewCidV1(cid.Raw, util.Hash([]byte("not a piece"))) + _, err := parsePieceCID(raw.String()) + require.Error(t, err) + }) + t.Run("garbage is rejected", func(t *testing.T) { + _, err := parsePieceCID("not-a-cid") + require.Error(t, err) + }) +}