Skip to content

Commit 60a9a3f

Browse files
Add seed to discovery service protocol (#3479)
1 parent 5d334e5 commit 60a9a3f

18 files changed

+282
-119
lines changed

discovery/api/server/api.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,12 @@ func (w *Wrapper) GetPresentations(ctx context.Context, request GetPresentations
7171
timestamp = *request.Params.Timestamp
7272
}
7373

74-
presentations, newTimestamp, err := w.Server.Get(contextWithForwardedHost(ctx), request.ServiceID, timestamp)
74+
presentations, seed, newTimestamp, err := w.Server.Get(contextWithForwardedHost(ctx), request.ServiceID, timestamp)
7575
if err != nil {
7676
return nil, err
7777
}
7878
return GetPresentations200JSONResponse{
79+
Seed: seed,
7980
Entries: presentations,
8081
Timestamp: newTimestamp,
8182
}, nil

discovery/api/server/api_test.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,24 @@ const serviceID = "wonderland"
3535
func TestWrapper_GetPresentations(t *testing.T) {
3636
lastTimestamp := 1
3737
presentations := map[string]vc.VerifiablePresentation{}
38+
seed := "seed"
3839
ctx := context.Background()
3940
t.Run("no timestamp", func(t *testing.T) {
4041
test := newMockContext(t)
41-
test.server.EXPECT().Get(gomock.Any(), serviceID, 0).Return(presentations, lastTimestamp, nil)
42+
test.server.EXPECT().Get(gomock.Any(), serviceID, 0).Return(presentations, seed, lastTimestamp, nil)
4243

4344
response, err := test.wrapper.GetPresentations(ctx, GetPresentationsRequestObject{ServiceID: serviceID})
4445

4546
require.NoError(t, err)
4647
require.IsType(t, GetPresentations200JSONResponse{}, response)
4748
assert.Equal(t, lastTimestamp, response.(GetPresentations200JSONResponse).Timestamp)
4849
assert.Equal(t, presentations, response.(GetPresentations200JSONResponse).Entries)
50+
assert.Equal(t, seed, response.(GetPresentations200JSONResponse).Seed)
4951
})
5052
t.Run("with timestamp", func(t *testing.T) {
5153
givenTimestamp := 1
5254
test := newMockContext(t)
53-
test.server.EXPECT().Get(gomock.Any(), serviceID, 1).Return(presentations, lastTimestamp, nil)
55+
test.server.EXPECT().Get(gomock.Any(), serviceID, 1).Return(presentations, seed, lastTimestamp, nil)
5456

5557
response, err := test.wrapper.GetPresentations(ctx, GetPresentationsRequestObject{
5658
ServiceID: serviceID,
@@ -66,7 +68,7 @@ func TestWrapper_GetPresentations(t *testing.T) {
6668
})
6769
t.Run("error", func(t *testing.T) {
6870
test := newMockContext(t)
69-
test.server.EXPECT().Get(gomock.Any(), serviceID, 0).Return(nil, 0, errors.New("foo"))
71+
test.server.EXPECT().Get(gomock.Any(), serviceID, 0).Return(nil, "", 0, errors.New("foo"))
7072

7173
_, err := test.wrapper.GetPresentations(ctx, GetPresentationsRequestObject{ServiceID: serviceID})
7274

discovery/api/server/client/http.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -71,31 +71,31 @@ func (h DefaultHTTPClient) Register(ctx context.Context, serviceEndpointURL stri
7171
return nil
7272
}
7373

74-
func (h DefaultHTTPClient) Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, int, error) {
74+
func (h DefaultHTTPClient) Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, string, int, error) {
7575
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, serviceEndpointURL, nil)
7676
httpRequest.URL.RawQuery = url.Values{"timestamp": []string{fmt.Sprintf("%d", timestamp)}}.Encode()
7777
if err != nil {
78-
return nil, 0, err
78+
return nil, "", 0, err
7979
}
8080
httpRequest.Header.Set("X-Forwarded-Host", httpRequest.Host) // prevent cycles
8181
httpResponse, err := h.client.Do(httpRequest)
8282
if err != nil {
83-
return nil, 0, fmt.Errorf("failed to invoke remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
83+
return nil, "", 0, fmt.Errorf("failed to invoke remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
8484
}
8585
defer httpResponse.Body.Close()
8686
if err := core.TestResponseCode(200, httpResponse); err != nil {
8787
httpErr := err.(core.HttpError) // TestResponseCodeWithLog always returns an HttpError
88-
return nil, 0, fmt.Errorf("non-OK response from remote Discovery Service (url=%s): %s", serviceEndpointURL, problemResponseToError(httpErr))
88+
return nil, "", 0, fmt.Errorf("non-OK response from remote Discovery Service (url=%s): %s", serviceEndpointURL, problemResponseToError(httpErr))
8989
}
9090
responseData, err := io.ReadAll(httpResponse.Body)
9191
if err != nil {
92-
return nil, 0, fmt.Errorf("failed to read response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
92+
return nil, "", 0, fmt.Errorf("failed to read response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
9393
}
9494
var result PresentationsResponse
9595
if err := json.Unmarshal(responseData, &result); err != nil {
96-
return nil, 0, fmt.Errorf("failed to unmarshal response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
96+
return nil, "", 0, fmt.Errorf("failed to unmarshal response from remote Discovery Service (url=%s): %w", serviceEndpointURL, err)
9797
}
98-
return result.Entries, result.Timestamp, nil
98+
return result.Entries, result.Seed, result.Timestamp, nil
9999
}
100100

101101
// problemResponseToError converts a Problem Details response to an error.

discovery/api/server/client/http_test.go

+8-5
Original file line numberDiff line numberDiff line change
@@ -79,29 +79,32 @@ func TestHTTPInvoker_Get(t *testing.T) {
7979
t.Run("no timestamp from client", func(t *testing.T) {
8080
handler := &testHTTP.Handler{StatusCode: http.StatusOK}
8181
handler.ResponseData = map[string]interface{}{
82+
"seed": "seed",
8283
"entries": map[string]interface{}{"1": vp},
8384
"timestamp": 1,
8485
}
8586
server := httptest.NewServer(handler)
8687
client := New(false, time.Minute, server.TLS)
8788

88-
presentations, timestamp, err := client.Get(context.Background(), server.URL, 0)
89+
presentations, seed, timestamp, err := client.Get(context.Background(), server.URL, 0)
8990

9091
assert.NoError(t, err)
9192
assert.Len(t, presentations, 1)
9293
assert.Equal(t, "0", handler.RequestQuery.Get("timestamp"))
9394
assert.Equal(t, 1, timestamp)
95+
assert.Equal(t, "seed", seed)
9496
})
9597
t.Run("timestamp provided by client", func(t *testing.T) {
9698
handler := &testHTTP.Handler{StatusCode: http.StatusOK}
9799
handler.ResponseData = map[string]interface{}{
100+
"seed": "seed",
98101
"entries": map[string]interface{}{"1": vp},
99102
"timestamp": 1,
100103
}
101104
server := httptest.NewServer(handler)
102105
client := New(false, time.Minute, server.TLS)
103106

104-
presentations, timestamp, err := client.Get(context.Background(), server.URL, 1)
107+
presentations, _, timestamp, err := client.Get(context.Background(), server.URL, 1)
105108

106109
assert.NoError(t, err)
107110
assert.Len(t, presentations, 1)
@@ -119,7 +122,7 @@ func TestHTTPInvoker_Get(t *testing.T) {
119122
server := httptest.NewServer(http.HandlerFunc(handler))
120123
client := New(false, time.Minute, server.TLS)
121124

122-
_, _, err := client.Get(context.Background(), server.URL, 0)
125+
_, _, _, err := client.Get(context.Background(), server.URL, 0)
123126

124127
require.NoError(t, err)
125128
assert.True(t, strings.HasPrefix(capturedRequest.Header.Get("X-Forwarded-Host"), "127.0.0.1"))
@@ -129,7 +132,7 @@ func TestHTTPInvoker_Get(t *testing.T) {
129132
server := httptest.NewServer(handler)
130133
client := New(false, time.Minute, server.TLS)
131134

132-
_, _, err := client.Get(context.Background(), server.URL, 0)
135+
_, _, _, err := client.Get(context.Background(), server.URL, 0)
133136

134137
assert.ErrorContains(t, err, "non-OK response from remote Discovery Service")
135138
assert.ErrorContains(t, err, "server returned HTTP status code 500")
@@ -141,7 +144,7 @@ func TestHTTPInvoker_Get(t *testing.T) {
141144
server := httptest.NewServer(handler)
142145
client := New(false, time.Minute, server.TLS)
143146

144-
_, _, err := client.Get(context.Background(), server.URL, 0)
147+
_, _, _, err := client.Get(context.Background(), server.URL, 0)
145148

146149
assert.ErrorContains(t, err, "failed to unmarshal response from remote Discovery Service")
147150
})

discovery/api/server/client/interface.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@ type HTTPClient interface {
3131
// Get retrieves Verifiable Presentations from the remote Discovery Service, that were added since the given timestamp.
3232
// If the call succeeds it returns the Verifiable Presentations and the timestamp that was returned by the server.
3333
// If the given timestamp is 0, all Verifiable Presentations are retrieved.
34-
Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, int, error)
34+
Get(ctx context.Context, serviceEndpointURL string, timestamp int) (map[string]vc.VerifiablePresentation, string, int, error)
3535
}

discovery/api/server/client/mock.go

+5-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

discovery/api/server/client/types.go

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import "github.com/nuts-foundation/go-did/vc"
2424
type PresentationsResponse struct {
2525
// Entries contains mappings from timestamp (as string) to a VerifiablePresentation.
2626
Entries map[string]vc.VerifiablePresentation `json:"entries"`
27+
// Seed is a unique value for the combination of serviceID and a server instance.
28+
Seed string `json:"seed"`
2729
// Timestamp is the timestamp of the latest entry. It's not a unix timestamp but a Lamport Clock.
2830
Timestamp int `json:"timestamp"`
2931
}

discovery/client.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -367,16 +367,21 @@ func (u *clientUpdater) updateService(ctx context.Context, service ServiceDefini
367367
log.Logger().
368368
WithField("discoveryService", service.ID).
369369
Tracef("Checking for new Verifiable Presentations from Discovery Service (timestamp: %d)", currentTimestamp)
370-
presentations, serverTimestamp, err := u.client.Get(ctx, service.Endpoint, currentTimestamp)
370+
presentations, seed, serverTimestamp, err := u.client.Get(ctx, service.Endpoint, currentTimestamp)
371371
if err != nil {
372372
return fmt.Errorf("failed to get presentations from discovery service (id=%s): %w", service.ID, err)
373373
}
374+
// check testSeed in store, wipe if it's different. Done by the store for transaction safety.
375+
err = u.store.wipeOnSeedChange(service.ID, seed)
376+
if err != nil {
377+
return fmt.Errorf("failed to wipe on testSeed change (service=%s, testSeed=%s): %w", service.ID, seed, err)
378+
}
374379
for _, presentation := range presentations {
375380
if err := u.verifier(service, presentation); err != nil {
376381
log.Logger().WithError(err).Warnf("Presentation verification failed, not adding it (service=%s, id=%s)", service.ID, presentation.ID)
377382
continue
378383
}
379-
if err := u.store.add(service.ID, presentation, serverTimestamp); err != nil {
384+
if err := u.store.add(service.ID, presentation, seed, serverTimestamp); err != nil {
380385
return fmt.Errorf("failed to store presentation (service=%s, id=%s): %w", service.ID, presentation.ID, err)
381386
}
382387
log.Logger().

discovery/client_test.go

+34-13
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) {
221221
ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any())
222222
ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil)
223223
ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil)
224-
require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1))
224+
require.NoError(t, ctx.store.add(testServiceID, vpAlice, testSeed, 1))
225225

226226
err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject)
227227

@@ -236,7 +236,7 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) {
236236
claims["retract_jti"] = vpAlice.ID.String()
237237
vp.Type = append(vp.Type, retractionPresentationType)
238238
}, vcAlice)
239-
require.NoError(t, ctx.store.add(testServiceID, vpAliceDeactivated, 1))
239+
require.NoError(t, ctx.store.add(testServiceID, vpAliceDeactivated, testSeed, 1))
240240

241241
err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject)
242242

@@ -255,7 +255,7 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) {
255255
ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error"))
256256
ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil)
257257
ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil)
258-
require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1))
258+
require.NoError(t, ctx.store.add(testServiceID, vpAlice, testSeed, 1))
259259

260260
err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject)
261261

@@ -266,7 +266,7 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) {
266266
ctx := newTestContext(t)
267267
ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(nil, assert.AnError)
268268
ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil)
269-
require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1))
269+
require.NoError(t, ctx.store.add(testServiceID, vpAlice, testSeed, 1))
270270

271271
err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject)
272272

@@ -394,7 +394,7 @@ func Test_clientUpdater_updateService(t *testing.T) {
394394
httpClient := client.NewMockHTTPClient(ctrl)
395395
updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient)
396396

397-
httpClient.EXPECT().Get(ctx, testDefinitions()[testServiceID].Endpoint, 0).Return(map[string]vc.VerifiablePresentation{}, 0, nil)
397+
httpClient.EXPECT().Get(ctx, testDefinitions()[testServiceID].Endpoint, 0).Return(map[string]vc.VerifiablePresentation{}, testSeed, 0, nil)
398398

399399
err := updater.updateService(ctx, testDefinitions()[testServiceID])
400400

@@ -406,7 +406,7 @@ func Test_clientUpdater_updateService(t *testing.T) {
406406
httpClient := client.NewMockHTTPClient(ctrl)
407407
updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient)
408408

409-
httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 0).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, 1, nil)
409+
httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 0).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, testSeed, 1, nil)
410410

411411
err := updater.updateService(ctx, testDefinitions()[testServiceID])
412412

@@ -423,7 +423,7 @@ func Test_clientUpdater_updateService(t *testing.T) {
423423
return nil
424424
}, httpClient)
425425

426-
httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 0).Return(map[string]vc.VerifiablePresentation{"1": vpAlice, "2": vpBob}, 2, nil)
426+
httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 0).Return(map[string]vc.VerifiablePresentation{"1": vpAlice, "2": vpBob}, testSeed, 2, nil)
427427

428428
err := updater.updateService(ctx, testDefinitions()[testServiceID])
429429

@@ -440,28 +440,49 @@ func Test_clientUpdater_updateService(t *testing.T) {
440440
resetStore(t, storageEngine.GetSQLDatabase())
441441
ctrl := gomock.NewController(t)
442442
httpClient := client.NewMockHTTPClient(ctrl)
443-
err := store.setTimestamp(store.db, testServiceID, 1)
443+
err := store.setTimestamp(store.db, testServiceID, testSeed, 1)
444444
require.NoError(t, err)
445445
updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient)
446446

447-
httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 1).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, 1, nil)
447+
httpClient.EXPECT().Get(ctx, serviceDefinition.Endpoint, 1).Return(map[string]vc.VerifiablePresentation{"1": vpAlice}, testSeed, 1, nil)
448448

449449
err = updater.updateService(ctx, testDefinitions()[testServiceID])
450450

451451
require.NoError(t, err)
452452
})
453+
t.Run("seed change wipes entries", func(t *testing.T) {
454+
resetStore(t, storageEngine.GetSQLDatabase())
455+
ctrl := gomock.NewController(t)
456+
httpClient := client.NewMockHTTPClient(ctrl)
457+
updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient)
458+
store.add(testServiceID, vpAlice, testSeed, 0)
459+
460+
exists, err := store.exists(testServiceID, aliceDID.String(), vpAlice.ID.String())
461+
require.NoError(t, err)
462+
require.True(t, exists)
463+
464+
httpClient.EXPECT().Get(ctx, testDefinitions()[testServiceID].Endpoint, 1).Return(map[string]vc.VerifiablePresentation{}, "other", 0, nil)
465+
466+
err = updater.updateService(ctx, testDefinitions()[testServiceID])
467+
468+
require.NoError(t, err)
469+
exists, err = store.exists(testServiceID, aliceDID.String(), vpAlice.ID.String())
470+
require.NoError(t, err)
471+
require.False(t, exists)
472+
})
453473
}
454474

455475
func Test_clientUpdater_update(t *testing.T) {
476+
seed := "seed"
456477
t.Run("proceeds when service update fails", func(t *testing.T) {
457478
storageEngine := storage.NewTestStorageEngine(t)
458479
require.NoError(t, storageEngine.Start())
459480
store := setupStore(t, storageEngine.GetSQLDatabase())
460481
ctrl := gomock.NewController(t)
461482
httpClient := client.NewMockHTTPClient(ctrl)
462-
httpClient.EXPECT().Get(gomock.Any(), "http://example.com/usecase", gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, 0, nil)
463-
httpClient.EXPECT().Get(gomock.Any(), "http://example.com/other", gomock.Any()).Return(nil, 0, errors.New("test"))
464-
httpClient.EXPECT().Get(gomock.Any(), "http://example.com/unsupported", gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, 0, nil)
483+
httpClient.EXPECT().Get(gomock.Any(), "http://example.com/usecase", gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, seed, 0, nil)
484+
httpClient.EXPECT().Get(gomock.Any(), "http://example.com/other", gomock.Any()).Return(nil, "", 0, errors.New("test"))
485+
httpClient.EXPECT().Get(gomock.Any(), "http://example.com/unsupported", gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, seed, 0, nil)
465486
updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient)
466487

467488
err := updater.update(context.Background())
@@ -474,7 +495,7 @@ func Test_clientUpdater_update(t *testing.T) {
474495
store := setupStore(t, storageEngine.GetSQLDatabase())
475496
ctrl := gomock.NewController(t)
476497
httpClient := client.NewMockHTTPClient(ctrl)
477-
httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, 0, nil).MinTimes(2)
498+
httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(map[string]vc.VerifiablePresentation{}, seed, 0, nil).MinTimes(2)
478499
updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient)
479500

480501
err := updater.update(context.Background())

discovery/interface.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type Server interface {
4848
Register(context context.Context, serviceID string, presentation vc.VerifiablePresentation) error
4949
// Get retrieves the presentations for the given service, starting from the given timestamp.
5050
// If the node is not configured as server for the given serviceID, the call will be forwarded to the configured server.
51-
Get(context context.Context, serviceID string, startAfter int) (map[string]vc.VerifiablePresentation, int, error)
51+
Get(context context.Context, serviceID string, startAfter int) (map[string]vc.VerifiablePresentation, string, int, error)
5252
}
5353

5454
// Client defines the API for Discovery Clients.

discovery/mock.go

+5-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)