Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support adding/removing services and keys to an ion DID #562

Merged
merged 16 commits into from
Aug 3, 2023

Conversation

andresuribe87
Copy link
Contributor

Overview

Support adding/removing services and keys to an ion DID. Must be custodied.

Description

This is the last part on #340. Most of the complexity comes from needing to ensure concurrent updates work.

Note that this PR uses the same abstractions from the SDK.

How Has This Been Tested?

Wrote a unit test.

@codecov-commenter
Copy link

codecov-commenter commented Jun 23, 2023

Codecov Report

Merging #562 (1b19f92) into main (5710ace) will decrease coverage by 1.21%.
The diff coverage is 7.31%.

@@            Coverage Diff             @@
##             main     #562      +/-   ##
==========================================
- Coverage   26.64%   25.44%   -1.21%     
==========================================
  Files          55       55              
  Lines        5855     6198     +343     
==========================================
+ Hits         1560     1577      +17     
- Misses       4023     4346     +323     
- Partials      272      275       +3     
Files Changed Coverage Δ
integration/common.go 2.20% <0.00%> (-0.03%) ⬇️
pkg/server/router/did.go 2.79% <0.00%> (-0.66%) ⬇️
pkg/service/did/service.go 0.00% <0.00%> (ø)
pkg/service/did/ion.go 30.39% <8.96%> (-31.93%) ⬇️
pkg/server/server.go 70.91% <100.00%> (+0.11%) ⬆️


type UpdateDIDByMethodRequest struct {
// Expected to be populated when `method == "ion"`. Describes the changes that are requested.
StateChange StateChange `json:"stateChange" validate:"required"`
Copy link
Member

Choose a reason for hiding this comment

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

this works until we support update for other methods, then we'll probably want to reconsider this API

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. Since we don't have other examples, I think this makes sense for now.

framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return
}
if *method != didsdk.IONMethod.String() {
Copy link
Member

Choose a reason for hiding this comment

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

I think this logic is better suited for the service layer, though it is OK to duplicate it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The service layer method is called UpdateIONDID, to which you pass in an UpdateIONDIDRequest object. By design, it's not possible to call that method without an ION did.

The external facing API is open for evolution. The internal is specific so it's more clear and it's easier to maintain.

@@ -167,6 +167,18 @@ func (s *Service) CreateDIDByMethod(ctx context.Context, request CreateDIDReques
return handler.CreateDID(ctx, request)
}

func (s *Service) UpdateIONDID(ctx context.Context, request UpdateDIDRequest) (*UpdateDIDResponse, error) {
Copy link
Member

Choose a reason for hiding this comment

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

this could work better (but also maybe premature optimization) to have this be a generic UpdateDID method, and then check if the method is ION. This helps us maintain a consistent abstraction between the router (just focused on http) and service (focused on all DID methods here)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do think it's premature. I propose we wait for when we have at least 2 methods for which we'll support updates.

Currently, it's only did:ion and I don't foresee others in the pipeline either.

@@ -69,7 +69,7 @@ func NewDIDStorage(db storage.ServiceStorage) (*Storage, error) {
return &Storage{db: db}, nil
}

func (ds *Storage) StoreDID(ctx context.Context, did StoredDID) error {
func StoreDID(ctx context.Context, did StoredDID, tx storage.Tx) error {
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
func StoreDID(ctx context.Context, did StoredDID, tx storage.Tx) error {
func StoreDID(ctx context.Context, tx storage.Tx, did StoredDID) error {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

didION := ion.ION(id)
if !didION.IsValid() {
return nil, errors.Errorf("invalid ion did %s", id)
}
Copy link
Member

Choose a reason for hiding this comment

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

could check to see whether any of the updates are set to make sure it's not a no-op

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added this validation to ionHandler.UpdateDID. Could validate in both places, but the ionHandler one seems more appropriate in case other endpoints end up calling that method.

Copy link
Member

Choose a reason for hiding this comment

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

sounds good

},
}

if state.status == "" {
Copy link
Member

Choose a reason for hiding this comment

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

could this be inverted to reduce nesting?

Copy link
Member

Choose a reason for hiding this comment

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

or consider using a switch and 3 helper methods for readability

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think it can be inverted. This works like a state machine. When all updates succeed, status will reflect all 3 states within the same call.

When there are failures in the middle, the last state is kept. This is done to ensure that our version of a DID is kept in sync with what was anchored.

return nil, errors.Wrap(err, "creating update request")
}

state.preAnchor = &preAnchor{
Copy link
Member

Choose a reason for hiding this comment

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

this should be blocked until 539 is in, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Depends on how much pushback there will be for #539 :)


updateKey := updatePrivateKey.ToPublicKeyJWK()

signer, err := ion.NewBTCSignerVerifier(*updatePrivateKey)
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't able to find a way in which I could create an instance of ion.DID with a custom update key. The only way to create an instance of that struct is by calling NewIONDID, which creates new update keys.

I agree that it would be desirable, but didn't want to block this PR on an SDK update.

Copy link
Member

Choose a reason for hiding this comment

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

ok fair


type UpdateDIDByMethodRequest struct {
// Expected to be populated when `method == "ion"`. Describes the changes that are requested.
StateChange StateChange `json:"stateChange" validate:"required"`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. Since we don't have other examples, I think this makes sense for now.

framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return
}
if *method != didsdk.IONMethod.String() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The service layer method is called UpdateIONDID, to which you pass in an UpdateIONDIDRequest object. By design, it's not possible to call that method without an ION did.

The external facing API is open for evolution. The internal is specific so it's more clear and it's easier to maintain.

didION := ion.ION(id)
if !didION.IsValid() {
return nil, errors.Errorf("invalid ion did %s", id)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added this validation to ionHandler.UpdateDID. Could validate in both places, but the ionHandler one seems more appropriate in case other endpoints end up calling that method.

},
}

if state.status == "" {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think it can be inverted. This works like a state machine. When all updates succeed, status will reflect all 3 states within the same call.

When there are failures in the middle, the last state is kept. This is done to ensure that our version of a DID is kept in sync with what was anchored.


updateKey := updatePrivateKey.ToPublicKeyJWK()

signer, err := ion.NewBTCSignerVerifier(*updatePrivateKey)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't able to find a way in which I could create an instance of ion.DID with a custom update key. The only way to create an instance of that struct is by calling NewIONDID, which creates new update keys.

I agree that it would be desirable, but didn't want to block this PR on an SDK update.

return nil, errors.Wrap(err, "creating update request")
}

state.preAnchor = &preAnchor{
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Depends on how much pushback there will be for #539 :)

@@ -167,6 +167,18 @@ func (s *Service) CreateDIDByMethod(ctx context.Context, request CreateDIDReques
return handler.CreateDID(ctx, request)
}

func (s *Service) UpdateIONDID(ctx context.Context, request UpdateDIDRequest) (*UpdateDIDResponse, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do think it's premature. I propose we wait for when we have at least 2 methods for which we'll support updates.

Currently, it's only did:ion and I don't foresee others in the pipeline either.

@@ -69,7 +69,7 @@ func NewDIDStorage(db storage.ServiceStorage) (*Storage, error) {
return &Storage{db: db}, nil
}

func (ds *Storage) StoreDID(ctx context.Context, did StoredDID) error {
func StoreDID(ctx context.Context, did StoredDID, tx storage.Tx) error {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

}

func isSignatureError(_ error) bool {
// TODO: figure out how to determine this error from the body of the response.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@decentralgabe could you help pointing me to the type of error that I would get if I send an operation to an ION node which has already been applied?

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure to be honest, maybe @thehenrytsai would know

}
updateIONDIDResponse, err := dr.service.UpdateIONDID(c, *updateDIDRequest)
if err != nil {
errMsg := fmt.Sprintf("could not create DID for method<%s>", *method)
Copy link
Contributor

Choose a reason for hiding this comment

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

is this the right error message?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@nitro-neal
Copy link
Contributor

nitro-neal commented Jun 26, 2023

if we remove pub keys / add other pub keys, and the private keys are not in the system, this will be like a "gimp" did if I'm understanding this correctly

It wont be able to do things like issue a vc and stuff, I think we already have good error messages that would explain that if you try to do something like that.

Copy link
Member

@decentralgabe decentralgabe left a comment

Choose a reason for hiding this comment

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

@andresuribe87 can we push this through?

@andresuribe87 andresuribe87 merged commit 028dbf9 into TBD54566975:main Aug 3, 2023
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants