Skip to content
Closed
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: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ require (
github.com/getsentry/sentry-go v0.31.1
github.com/glebarez/go-sqlite v1.21.2
github.com/glebarez/sqlite v1.11.0
github.com/go-playground/validator/v10 v10.14.0
github.com/go-playground/validator/v10 v10.14.1
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/ipfs/go-cid v0.5.0
github.com/ipfs/go-datastore v0.8.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -702,8 +702,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
Expand Down
11 changes: 11 additions & 0 deletions pkg/pdp/api/middleware/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"net/http"

"github.com/labstack/echo/v4"

"github.com/storacha/piri/pkg/pdp/apiv2"
)

// ContextualError is a richer error interface that provides additional context
Expand Down Expand Up @@ -80,6 +82,15 @@ func NewError(operation string, message string, err error, code int) *PDPError {
}
}

func FromAPIError(operation string, err error) *PDPError {
code, msg := apiv2.GetAPIError(err)
return &PDPError{
Operation: operation,
Message: msg,
Code: code,
}
}

// WithContext adds context information to the error
func (e *PDPError) WithContext(key string, value interface{}) *PDPError {
e.Context[key] = value
Expand Down
241 changes: 241 additions & 0 deletions pkg/pdp/apiv2/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package apiv2

import (
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"

"github.com/ethereum/go-ethereum/common"
"github.com/google/uuid"
"github.com/ipfs/go-cid"

"github.com/storacha/piri/pkg/pdp/service"
"github.com/storacha/piri/pkg/pdp/service/types"
"github.com/storacha/piri/pkg/store"
)

var _ PDP = (*API)(nil)

// API implements core PDP operations (transport-agnostic)
type API struct {
service *service.PDPService
endpoint *url.URL
}

// endpoint is the URL the service backing this api is avaliable at
func New(endpoint *url.URL, s *service.PDPService) *API {
return &API{service: s, endpoint: endpoint}
}

func (h *API) CreateProofSet(ctx context.Context, req CreateProofSet) (StatusRef, error) {
if !common.IsHexAddress(req.RecordKeeper) {
return StatusRef{}, NewError(http.StatusBadRequest, "record keeper address is not a valid address")
}
recordKeeperAddr := common.HexToAddress(req.RecordKeeper)
resp, err := h.service.ProofSetCreate(ctx, recordKeeperAddr)
if err != nil {
return StatusRef{}, WrapError(err, http.StatusInternalServerError, "failed to create proof set")
}
return StatusRef{URL: path.Join("/pdp/proof-sets/created", resp.String())}, nil
}

func (h *API) ProofSetCreationStatus(ctx context.Context, ref StatusRef) (ProofSetStatus, error) {
// Clean txHash (ensure it starts with '0x' and is lowercase)
txHash := path.Base(ref.URL)
if !strings.HasPrefix(txHash, "0x") {
txHash = "0x" + txHash
}
txHash = strings.ToLower(txHash)

// Validate txHash is a valid hash
if len(txHash) != 66 { // '0x' + 64 hex chars
return ProofSetStatus{}, NewError(http.StatusBadRequest, "invalid tx hash length: %s", txHash)
}
if _, err := hex.DecodeString(txHash[2:]); err != nil {
return ProofSetStatus{}, WrapError(err, http.StatusBadRequest, "invalid tx hash: %s", txHash)
}
txh := common.HexToHash(txHash)
status, err := h.service.ProofSetStatus(ctx, txh)
if err != nil {
return ProofSetStatus{}, WrapError(err, http.StatusInternalServerError, "failed to set proof set status")
}
psID := uint64(status.ProofSetId)
return ProofSetStatus{
CreateMessageHash: status.CreateMessageHash,
ProofsetCreated: status.ProofsetCreated,
Service: status.Service,
TxStatus: status.TxStatus,
OK: &status.OK,
ProofSetId: &psID,
}, nil
}

func (h *API) GetProofSet(ctx context.Context, id uint64) (ProofSet, error) {
ps, err := h.service.ProofSet(ctx, int64(id))
if err != nil {
if errors.Is(err, service.ErrProofSetNotFound) {
return ProofSet{}, NewError(http.StatusNotFound, "proof set not found")
}
return ProofSet{}, WrapError(err, http.StatusInternalServerError, "failed to get proof set")
}

resp := ProofSet{
ID: uint64(ps.ID),
NextChallengeEpoch: &ps.NextChallengeEpoch,
}
for _, root := range ps.Roots {
resp.Roots = append(resp.Roots, RootEntry{
RootID: root.RootID,
RootCID: root.RootCID,
SubrootCID: root.SubrootCID,
SubrootOffset: root.SubrootOffset,
})
}
return resp, nil
}

func (h *API) DeleteProofSetRoot(ctx context.Context, proofSetID uint64, rootID uint64) error {
return h.service.RemoveRoot(ctx, proofSetID, rootID)
}

func (h *API) DeleteProofSet(ctx context.Context, id uint64) error {
return NewError(http.StatusNotImplemented, "delete proofSet not implemented")
}

func (h *API) AddRootsToProofSet(ctx context.Context, id uint64, roots []AddRootRequest) error {
serviceRequests := make([]service.AddRootRequest, 0, len(roots))
for _, r := range roots {
subroots := make([]string, 0, len(r.Subroots))
for _, s := range r.Subroots {
subroots = append(subroots, s.SubrootCID)
}
serviceRequests = append(serviceRequests, service.AddRootRequest{
RootCID: r.RootCID,
SubrootCIDs: subroots,
})
}

// TODO return the tx hash of the proof set create message
todoHash, err := h.service.ProofSetAddRoot(ctx, int64(id), serviceRequests)
_ = todoHash
return err
}

func (h *API) AddPiece(ctx context.Context, piece AddPiece) (*UploadRef, error) {
// Validate input
if piece.Check.Hash == "" {
return nil, NewError(http.StatusBadRequest, "piece hash is required")
}
if piece.Check.Name == "" {
return nil, NewError(http.StatusBadRequest, "piece name is required")
}

resp, err := h.service.PreparePiece(ctx, service.PiecePrepareRequest{
Check: types.PieceHash{
Name: piece.Check.Name,
Hash: piece.Check.Hash,
Size: piece.Check.Size,
},
Notify: piece.Notify,
})
if err != nil {
return nil, WrapError(err, http.StatusInternalServerError, "failed to add piece")
}
// piece already exists
// TODO do better, we should return a more complete response
if !resp.Created {
return nil, nil
}
return &UploadRef{URL: resp.Location}, nil
}

func (h *API) UploadPiece(ctx context.Context, ref UploadRef, data io.Reader) error {
pieceUUID := path.Base(ref.URL)
uploadID, err := uuid.Parse(pieceUUID)
if err != nil {
return WrapError(err, http.StatusBadRequest, "invalid upload uuid")
}
_, err = h.service.UploadPiece(ctx, uploadID, data)
if err != nil {
return WrapError(err, http.StatusInternalServerError, "failed to upload piece")
}
return nil
}

func (h *API) FindPiece(ctx context.Context, piece PieceHash) (FoundPiece, error) {
// Validate input
if piece.Hash == "" {
return FoundPiece{}, NewError(http.StatusBadRequest, "piece hash is required")
}
if piece.Name == "" {
return FoundPiece{}, NewError(http.StatusBadRequest, "piece name is required")
}

p, found, err := h.service.FindPiece(ctx, piece.Name, piece.Hash, piece.Size)
if err != nil {
return FoundPiece{}, WrapError(err, http.StatusInternalServerError, "failed to find piece")
}
if !found {
return FoundPiece{}, NewError(http.StatusNotFound, "piece not found")
}
return FoundPiece{PieceCID: p.String()}, nil
}

func (h *API) GetPiece(ctx context.Context, pieceCid string) (PieceReader, error) {
pCID, err := cid.Parse(pieceCid)
if err != nil {
return PieceReader{}, WrapError(err, http.StatusBadRequest, "invalid piece cid")
}
obj, err := h.service.Storage().Get(ctx, pCID.Hash())
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return PieceReader{}, WrapError(err, http.StatusNotFound, "piece not found")
}
return PieceReader{}, WrapError(err, http.StatusInternalServerError, "failed to read piece")
}
// TODO we should return an io.ReadCloser, which the object store now supports.
return PieceReader{
Data: &wrapper{obj: obj.Body()},
Size: obj.Size(),
}, nil
}

const piecePath = "/piece"

func (h *API) GetPieceURL(pieceCid string) url.URL {
return *h.endpoint.JoinPath(piecePath, "/", pieceCid)
}

func (h *API) Ping(_ context.Context) error {
return nil
}

type wrapper struct {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe io.NoopCloser will give you this functionality

obj io.Reader
}

func (w *wrapper) Read(p []byte) (n int, err error) {
return w.obj.Read(p)
}

func (w *wrapper) Close() error {
return fmt.Errorf("close not implemented")
}

// Helper to check if an error from the service layer indicates "not found"
func isNotFoundError(err error) bool {

Check failure on line 232 in pkg/pdp/apiv2/api.go

View workflow job for this annotation

GitHub Actions / go-check / All

func isNotFoundError is unused (U1000)
// This depends on how your service layer reports not found errors
// Examples:
// - return err.Error() == "not found"
// - return errors.Is(err, service.ErrNotFound)
// - return strings.Contains(err.Error(), "not found")

// For now, a simple check:
return err != nil && strings.Contains(strings.ToLower(err.Error()), "not found")
}
Loading
Loading