Skip to content

Commit ef458fc

Browse files
committed
fix(storage): restore identity, resume, and context validation
1 parent b6d61d4 commit ef458fc

9 files changed

Lines changed: 202 additions & 9 deletions

File tree

docs/GETTING_STARTED.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ target. Its zero value is invalid; construct it explicitly and use accessors to
275275
read IDs.
276276

277277
```go
278-
ref, err := storage.NewDataSetRef(providerID, dataSetID, clientDataSetID)
278+
ref, err := storage.NewDataSetRef(providerID, dataSetID, dataSetCtx.ClientDataSetID())
279279
if err != nil {
280280
return err
281281
}

storage/context.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,11 +673,17 @@ func (c *DataSetContext) PieceURL(pieceCID cid.Cid) string {
673673

674674
// ProviderID returns the provider's numeric ID.
675675
func (c *ProviderContext) ProviderID() types.BigInt {
676+
if c == nil || c.core == nil {
677+
return types.BigInt{}
678+
}
676679
return copyBigInt(c.core.provider.ID)
677680
}
678681

679682
// ProviderID returns the provider's numeric ID.
680683
func (c *DataSetContext) ProviderID() types.BigInt {
684+
if c == nil || c.core == nil {
685+
return types.BigInt{}
686+
}
681687
return copyBigInt(c.core.provider.ID)
682688
}
683689

storage/context_dataset.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ func (c *ProviderContext) waitForDataSetCreated(ctx context.Context, op string,
144144

145145
func validateCreateDataSetSubmission(op string, providerID types.BigInt, submission CreateDataSetSubmission) (CreateDataSetSubmission, error) {
146146
submission = copyCreateDataSetSubmission(submission)
147+
if submission.ProviderID.IsZero() {
148+
submission.ProviderID = copyBigInt(providerID)
149+
}
147150
if submission.ProviderID.IsZero() {
148151
return CreateDataSetSubmission{}, fmt.Errorf("%s: %w: zero providerID", op, ErrInvalidArgument)
149152
}

storage/context_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/strahe/synapse-go/piece"
2222
"github.com/strahe/synapse-go/signer"
2323
"github.com/strahe/synapse-go/types"
24+
"github.com/strahe/synapse-go/warmstorage"
2425
)
2526

2627
func mustPieceInfo(t *testing.T) piece.PieceInfo {
@@ -576,6 +577,148 @@ func TestProviderContextWaitForDataSetCreatedRejectsWrongProvider(t *testing.T)
576577
}
577578
}
578579

580+
func TestProviderContextWaitForDataSetCreatedAcceptsZeroProviderID(t *testing.T) {
581+
txHash := common.HexToHash("0x1234")
582+
dataSetID := types.NewBigInt(77)
583+
clientID := types.NewBigInt(7)
584+
waitCalls := 0
585+
client := &fakePDPProviderClient{
586+
waitForCreatedFn: func(context.Context, string, time.Duration) (*pdp.CreateDataSetStatus, error) {
587+
waitCalls++
588+
id := copyBigInt(dataSetID)
589+
return &pdp.CreateDataSetStatus{CreateMessageHash: txHash, DataSetID: &id}, nil
590+
},
591+
}
592+
c := mustWritableProviderContext(t, client)
593+
result, err := c.WaitForDataSetCreated(context.Background(), CreateDataSetSubmission{
594+
TransactionID: txHash.Hex(),
595+
StatusURL: "https://sp.example.com/status",
596+
ClientDataSetID: &clientID,
597+
})
598+
if err != nil {
599+
t.Fatalf("WaitForDataSetCreated: %v", err)
600+
}
601+
if waitCalls != 1 {
602+
t.Fatalf("waitCalls=%d want 1", waitCalls)
603+
}
604+
if !result.DataSet.ProviderID().Equal(testProvider().ID) ||
605+
!result.DataSet.DataSetID().Equal(dataSetID) ||
606+
!result.DataSet.ClientDataSetID().Equal(clientID) {
607+
t.Fatalf("result=%+v", result.DataSet)
608+
}
609+
}
610+
611+
func TestProviderContextWaitForDataSetCreatedRejectsInvalidSubmission(t *testing.T) {
612+
clientID := types.NewBigInt(7)
613+
valid := CreateDataSetSubmission{
614+
TransactionID: common.HexToHash("0x1234").Hex(),
615+
StatusURL: "https://sp.example.com/status",
616+
ClientDataSetID: &clientID,
617+
}
618+
tests := map[string]CreateDataSetSubmission{
619+
"empty transaction": {TransactionID: "", StatusURL: valid.StatusURL, ClientDataSetID: valid.ClientDataSetID},
620+
"short transaction": {TransactionID: "0xbeef", StatusURL: valid.StatusURL, ClientDataSetID: valid.ClientDataSetID},
621+
"zero transaction": {TransactionID: common.Hash{}.Hex(), StatusURL: valid.StatusURL, ClientDataSetID: valid.ClientDataSetID},
622+
"empty status URL": {TransactionID: valid.TransactionID, StatusURL: "", ClientDataSetID: valid.ClientDataSetID},
623+
"missing client ID": {TransactionID: valid.TransactionID, StatusURL: valid.StatusURL},
624+
}
625+
for name, submission := range tests {
626+
t.Run(name, func(t *testing.T) {
627+
waitCalls := 0
628+
client := &fakePDPProviderClient{
629+
waitForCreatedFn: func(context.Context, string, time.Duration) (*pdp.CreateDataSetStatus, error) {
630+
waitCalls++
631+
return nil, errors.New("wait should not be called")
632+
},
633+
}
634+
c := mustWritableProviderContext(t, client)
635+
_, err := c.WaitForDataSetCreated(context.Background(), submission)
636+
if !errors.Is(err, ErrInvalidArgument) {
637+
t.Fatalf("WaitForDataSetCreated error=%v want ErrInvalidArgument", err)
638+
}
639+
if waitCalls != 0 {
640+
t.Fatalf("waitCalls=%d want 0", waitCalls)
641+
}
642+
})
643+
}
644+
}
645+
646+
func TestDataSetContextCommitRejectsEndedAndValidatorFailuresBeforeAddPieces(t *testing.T) {
647+
info := mustPieceInfo(t)
648+
dataSetID := types.NewBigInt(42)
649+
want := errors.New("not live")
650+
651+
t.Run("ended rail", func(t *testing.T) {
652+
addCalls := 0
653+
reader := &fakeFWSSDataSetReader{
654+
info: &warmstorage.DataSetInfo{DataSetID: dataSetID, PDPEndEpoch: 3778900},
655+
}
656+
client := &fakePDPProviderClient{
657+
addPiecesFn: func(context.Context, types.BigInt, []pdp.AddPieceInput, []byte) (*pdp.AddPiecesResult, error) {
658+
addCalls++
659+
t.Fatal("AddPieces must not run for an ended data set")
660+
return nil, nil
661+
},
662+
}
663+
c := mustWritableDataSetContext(t, client, testDataSetRef(dataSetID, types.NewBigInt(7)), WithFWSSDataSetReader(reader))
664+
_, err := c.Commit(context.Background(), CommitRequest{Pieces: []PieceInput{{PieceCID: info.CIDv2}}})
665+
requireDataSetPDPPaymentTerminated(t, err, dataSetID, 3778900)
666+
if addCalls != 0 {
667+
t.Fatalf("addCalls=%d want 0", addCalls)
668+
}
669+
if reader.calls != 1 || !reader.gotID.Equal(dataSetID) {
670+
t.Fatalf("reader calls=%d gotID=%s", reader.calls, reader.gotID.String())
671+
}
672+
})
673+
674+
t.Run("validator failure", func(t *testing.T) {
675+
addCalls := 0
676+
validator := &fakeDataSetValidator{err: want}
677+
client := &fakePDPProviderClient{
678+
addPiecesFn: func(context.Context, types.BigInt, []pdp.AddPieceInput, []byte) (*pdp.AddPiecesResult, error) {
679+
addCalls++
680+
t.Fatal("AddPieces must not run after validator failure")
681+
return nil, nil
682+
},
683+
}
684+
c := mustWritableDataSetContext(t, client, testDataSetRef(dataSetID, types.NewBigInt(7)), WithDataSetValidator(validator))
685+
_, err := c.Commit(context.Background(), CommitRequest{Pieces: []PieceInput{{PieceCID: info.CIDv2}}})
686+
if !errors.Is(err, want) {
687+
t.Fatalf("Commit error=%v want %v", err, want)
688+
}
689+
if addCalls != 0 {
690+
t.Fatalf("addCalls=%d want 0", addCalls)
691+
}
692+
if len(validator.calls) != 1 || !validator.calls[0].Equal(dataSetID) {
693+
t.Fatalf("validator calls=%v", validator.calls)
694+
}
695+
})
696+
}
697+
698+
func TestDataSetContextUploadRejectsEndedExistingDataSetBeforeStore(t *testing.T) {
699+
dataSetID := types.NewBigInt(13269)
700+
storeCalled := false
701+
reader := &fakeFWSSDataSetReader{
702+
info: &warmstorage.DataSetInfo{DataSetID: dataSetID, PDPEndEpoch: 3778900},
703+
}
704+
client := &fakePDPProviderClient{
705+
uploadStreamingFn: func(context.Context, io.Reader, pdp.UploadPieceStreamingOptions) (*pdp.UploadStreamingResult, error) {
706+
storeCalled = true
707+
t.Fatal("Store must not run when the existing data set cannot accept uploads")
708+
return nil, nil
709+
},
710+
}
711+
c := mustWritableDataSetContext(t, client, testDataSetRef(dataSetID, types.NewBigInt(99)), WithFWSSDataSetReader(reader))
712+
_, err := c.Upload(context.Background(), bytes.NewReader([]byte("payload")), nil)
713+
requireDataSetPDPPaymentTerminated(t, err, dataSetID, 3778900)
714+
if storeCalled {
715+
t.Fatal("Store was called")
716+
}
717+
if reader.calls != 1 || !reader.gotID.Equal(dataSetID) {
718+
t.Fatalf("reader calls=%d gotID=%s", reader.calls, reader.gotID.String())
719+
}
720+
}
721+
579722
func TestContextPullRoutesByConcreteType(t *testing.T) {
580723
info := mustPieceInfo(t)
581724
dataSetID := types.NewBigInt(42)

storage/create.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,10 +275,11 @@ func (s *Service) validateContextIdentity(op string, storageCtx StorageContext)
275275
if isNilStorageContext(storageCtx) {
276276
return fmt.Errorf("%s: %w: nil storage context", op, ErrInvalidArgument)
277277
}
278+
if s.signerAddr == (common.Address{}) || !s.chainID.IsValid() || s.recordKeeper == (common.Address{}) {
279+
return fmt.Errorf("%s: %w: service identity is incomplete (payer, chain, and record keeper)", op, ErrInvalidArgument)
280+
}
278281
identity := storageCtx.ContextIdentity()
279-
if identity.Payer == s.signerAddr && identity.Payer != (common.Address{}) &&
280-
identity.ChainID == s.chainID && identity.ChainID.IsValid() &&
281-
identity.RecordKeeper == s.recordKeeper && identity.RecordKeeper != (common.Address{}) {
282+
if identity.Payer == s.signerAddr && identity.ChainID == s.chainID && identity.RecordKeeper == s.recordKeeper {
282283
return nil
283284
}
284285
return fmt.Errorf("%s: %w: context identity does not match service payer, chain, and record keeper", op, ErrInvalidArgument)
@@ -342,6 +343,12 @@ func isNilStorageContext(storageCtx StorageContext) bool {
342343
if storageCtx == nil {
343344
return true
344345
}
346+
switch c := storageCtx.(type) {
347+
case *ProviderContext:
348+
return c == nil || c.core == nil
349+
case *DataSetContext:
350+
return c == nil || c.core == nil
351+
}
345352
value := reflect.ValueOf(storageCtx)
346353
switch value.Kind() {
347354
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
@@ -352,7 +359,7 @@ func isNilStorageContext(storageCtx StorageContext) bool {
352359
}
353360

354361
func storageCtxID(storageCtx *ProviderContext) types.BigInt {
355-
if storageCtx == nil {
362+
if storageCtx == nil || storageCtx.core == nil {
356363
return types.BigInt{}
357364
}
358365
return storageCtx.ProviderID()

storage/create_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package storage
33
import (
44
"context"
55
"errors"
6+
"strings"
67
"testing"
78

89
"github.com/ethereum/go-ethereum/common"
@@ -231,12 +232,39 @@ func TestServiceSelectUploadContextsDiscardsHardErrorResult(t *testing.T) {
231232
}
232233
}
233234

235+
func TestServiceSelectProviderContextRejectsNilCore(t *testing.T) {
236+
svc := newTestService()
237+
svc.contextSelector = &fakeContextSelector{providerFn: func(context.Context, SelectProviderContextOptions) (*ProviderContext, error) {
238+
return &ProviderContext{}, nil
239+
}}
240+
result, err := svc.SelectProviderContext(context.Background(), SelectProviderContextOptions{})
241+
if result != nil || !errors.Is(err, ErrInvalidArgument) {
242+
t.Fatalf("result=%v error=%v want ErrInvalidArgument", result, err)
243+
}
244+
}
245+
246+
func TestServiceUploadToContextsIncompleteServiceIdentity(t *testing.T) {
247+
svc, err := New(Options{SignerAddress: testPayer()})
248+
if err != nil {
249+
t.Fatalf("New: %v", err)
250+
}
251+
reader := &readCountingReader{}
252+
result, err := svc.UploadToContexts(context.Background(), reader, []StorageContext{&fakeUploadContext{id: types.NewBigInt(1)}}, nil)
253+
if result != nil || !errors.Is(err, ErrInvalidArgument) || !strings.Contains(err.Error(), "service identity") || strings.Contains(err.Error(), "does not match") {
254+
t.Fatalf("result=%v error=%v want ErrInvalidArgument service identity", result, err)
255+
}
256+
if reader.reads != 0 {
257+
t.Fatalf("reader reads=%d want 0", reader.reads)
258+
}
259+
}
260+
234261
func TestServiceSelectUploadContextsRejectsTypedNilAndIdentityMismatch(t *testing.T) {
235262
for _, tt := range []struct {
236263
name string
237264
ctx StorageContext
238265
}{
239266
{name: "typed nil", ctx: (*ProviderContext)(nil)},
267+
{name: "nil core", ctx: &ProviderContext{}},
240268
{name: "wrong payer", ctx: testProviderContextWithID(t, types.NewBigInt(1), ContextIdentity{
241269
Payer: common.HexToAddress("0x9999"), ChainID: types.ChainID(314159), RecordKeeper: testRecordKeeper(),
242270
})},

storage/service.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -767,16 +767,16 @@ func validateExplicitUploadOptions(opts *UploadOptions) error {
767767
return nil
768768
}
769769
if opts.Copies != 0 {
770-
return fmt.Errorf("%w: Copies is not supported by UploadToContexts", ErrInvalidArgument)
770+
return fmt.Errorf("%w: Copies is not supported for explicit-context uploads", ErrInvalidArgument)
771771
}
772772
if len(opts.ExcludeProviderIDs) != 0 {
773-
return fmt.Errorf("%w: ExcludeProviderIDs is not supported by UploadToContexts", ErrInvalidArgument)
773+
return fmt.Errorf("%w: ExcludeProviderIDs is not supported for explicit-context uploads; pass it to SelectUploadContexts instead", ErrInvalidArgument)
774774
}
775775
if len(opts.DataSetMetadata) != 0 {
776-
return fmt.Errorf("%w: DataSetMetadata is not supported by UploadToContexts", ErrInvalidArgument)
776+
return fmt.Errorf("%w: DataSetMetadata is not supported for explicit-context uploads; pass it to SelectUploadContexts or the context constructor instead", ErrInvalidArgument)
777777
}
778778
if opts.WithCDN != nil {
779-
return fmt.Errorf("%w: WithCDN is not supported by UploadToContexts", ErrInvalidArgument)
779+
return fmt.Errorf("%w: WithCDN is not supported for explicit-context uploads; pass it to SelectUploadContexts or the context constructor instead", ErrInvalidArgument)
780780
}
781781
return nil
782782
}

storage/service_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,8 @@ func TestServiceUploadToContextsValidatesBeforeReading(t *testing.T) {
942942
tests := map[string][]StorageContext{
943943
"empty": nil,
944944
"typed nil": {(*fakeUploadContext)(nil)},
945+
"nil-core provider": {&ProviderContext{}},
946+
"nil-core data set": {&DataSetContext{}},
945947
"duplicate provider": {valid, valid},
946948
"identity mismatch": {wrong},
947949
}
@@ -976,6 +978,9 @@ func TestServiceUploadToContextsRejectsSelectionOptionsBeforeReading(t *testing.
976978
if result != nil || !errors.Is(err, ErrInvalidArgument) {
977979
t.Fatalf("result=%v error=%v want ErrInvalidArgument", result, err)
978980
}
981+
if !strings.Contains(err.Error(), "explicit-context uploads") {
982+
t.Fatalf("error=%q want explicit-context uploads", err)
983+
}
979984
if reader.reads != 0 {
980985
t.Fatalf("reader reads=%d want 0", reader.reads)
981986
}

storage/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ type CreateDataSetOptions struct {
139139

140140
// CreateDataSetSubmission identifies a submitted create-dataset transaction.
141141
// Persist and restore all fields together; incomplete submissions are rejected.
142+
// A zero ProviderID is filled from the ProviderContext used to wait.
142143
type CreateDataSetSubmission struct {
143144
ProviderID types.BigInt
144145
TransactionID string

0 commit comments

Comments
 (0)