-
Notifications
You must be signed in to change notification settings - Fork 10
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
DID ION Update, Recover, and Deactivate Operations #77
Conversation
Codecov Report
@@ Coverage Diff @@
## main #77 +/- ##
==========================================
+ Coverage 63.17% 69.58% +6.40%
==========================================
Files 21 21
Lines 964 1200 +236
Branches 123 136 +13
==========================================
+ Hits 609 835 +226
Misses 294 294
- Partials 61 71 +10
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, just make sure the sign is working the same way as #63
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the latest changes from the base branch now
public class UpdateDidIonOptions( | ||
public val didString: String, | ||
public val updateKeyAlias: String, | ||
public val servicesToAdd: Iterable<Service>? = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious as to why the adds are more generic than the remove (Iterable
vs Set
). Is order important for add operations?
JsonSubTypes.Type(ReplaceAction::class, name = "replace"), | ||
JsonSubTypes.Type(RemoveServicesAction::class, name = "remove-services"), | ||
JsonSubTypes.Type(AddPublicKeysAction::class, name = "add-public-keys"), | ||
JsonSubTypes.Type(RemovePublicKeysAction::class, name = "remove-public-keys"), | ||
) | ||
public sealed class PatchAction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious why the sealed class route vs a marker interface?
@@ -151,6 +169,11 @@ public data class OperationSuffixDataObject( | |||
*/ | |||
public typealias Commitment = String | |||
|
|||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: IMHO these comments are super obvious. I don't think they add value here and make the code less readable.
public val servicesToAdd: Iterable<Service>? = null, | ||
public val idsOfServicesToRemove: Set<String>? = null, | ||
public val publicKeysToAdd: Iterable<PublicKey>? = null, | ||
public val idsOfPublicKeysToRemove: Set<String>? = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be preferable to just create empty collections by default rather than cater for the possibility of a null collection
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed
* @param publicKeysToAdd PublicKeys to add to the DID document. | ||
* @param idsOfPublicKeysToRemove Keys to remove from the DID document. | ||
*/ | ||
public class UpdateDidIonOptions( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A possible alternative to this impl might be
public data class UpdateDidIonOptions(
val didString: String,
val updateKeyAlias: String,
val servicesToAdd: List<Service> = emptyList(),
val idsOfServicesToRemove: List<String> = emptyList(),
val publicKeysToAdd: List<PublicKey> = emptyList(),
val idsOfPublicKeysToRemove: List<String> = emptyList()
) {
internal fun toPatches(): List<PatchAction> {
fun <T> MutableList<PatchAction>.addIfNotEmpty(list: List<T>, action: (List<T>) -> PatchAction) {
list.takeIf { it.isNotEmpty() }?.let { this.add(action(it)) }
}
return buildList {
addIfNotEmpty(servicesToAdd, ::AddServicesAction)
addIfNotEmpty(idsOfServicesToRemove, ::RemoveServicesAction)
addIfNotEmpty(publicKeysToAdd, ::AddPublicKeysAction)
addIfNotEmpty(idsOfPublicKeysToRemove, ::RemovePublicKeysAction)
}
}
}
For a rough and untested stab at it. I think this covers the comments below.
updateKeyAlias = newUpdateKeyAlias, | ||
) | ||
} | ||
throw Exception("received error response '$opBody'") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RuntimeException
or even better a custom runtime exception type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is an interop issue with Java. The declared package doesn't match the file location
@bradleydwyer you made comments on a very old commit. Can you take another stab at it, please? |
public class UpdateDidIonOptions( | ||
public val didString: String, | ||
public val updateKeyAlias: String, | ||
public val servicesToAdd: Iterable<Service>? = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be preferable to just create empty collections by default rather than cater for the possibility of a null collection (#77 (comment))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
* @param publicKeysToAdd PublicKeys to add to the DID document. | ||
* @param idsOfPublicKeysToRemove Keys to remove from the DID document. | ||
*/ | ||
public class UpdateDidIonOptions( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A possible alternative to this impl might be
public data class UpdateDidIonOptions(
val didString: String,
val updateKeyAlias: String,
val servicesToAdd: List<Service> = emptyList(),
val idsOfServicesToRemove: List<String> = emptyList(),
val publicKeysToAdd: List<PublicKey> = emptyList(),
val idsOfPublicKeysToRemove: List<String> = emptyList()
) {
internal fun toPatches(): List<PatchAction> {
fun <T> MutableList<PatchAction>.addIfNotEmpty(list: List<T>, action: (List<T>) -> PatchAction) {
list.takeIf { it.isNotEmpty() }?.let { this.add(action(it)) }
}
return buildList {
addIfNotEmpty(servicesToAdd, ::AddServicesAction)
addIfNotEmpty(idsOfServicesToRemove, ::RemoveServicesAction)
addIfNotEmpty(publicKeysToAdd, ::AddPublicKeysAction)
addIfNotEmpty(idsOfPublicKeysToRemove, ::RemovePublicKeysAction)
}
}
}
For a rough and untested stab at it. I think this covers the comments below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this, thanks! Adopted! Just made a tiny change to accept iterables everywhere.
public class UpdateDidIonOptions( | ||
public val didString: String, | ||
public val updateKeyAlias: String, | ||
public val servicesToAdd: Iterable<Service>? = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious as to why the adds are more generic than the remove (Iterable vs Set). Is order important for add operations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I looked at the spec it didn't seem like the order was important (or that each type of patch was required to be present)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And in addition to https://github.com/TBD54566975/web5-kt/pull/77/files#r1359995250 was confused at marshalling everything into a List at the end - why not just be a List from the start?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The spec says, for the add operation:
In the case a service entry already exists for the given id specified within an add-services Patch Action, the implementation MUST overwrite the existing entry entirely with the incoming patch. To construct an add-services patch, compose an object as follows:
So my understanding is that order is important. Am I reading this correctly?
Re:
was confused at marshalling everything into a List at the end - why not just be a List from the start?
Great question. UpdateIonOptions
is part of the public API. As such, my intention was to adopt Postel's law: "Be liberal in what you accept, and conservative in what you send.". By having Iterable
, clients can have any sort of collection before the pass in the data.
I went ahead and updated the classes inside Models
to follow this pattern as well.
/** | ||
* Type alias for reveal value. | ||
*/ | ||
public typealias Reveal = String |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: IMHO these comments are super obvious. I don't think they add value here and make the code less readable. This is also a general comment on things like @param kdoc that adds no detail beyond what is already obvious from the associate type and param name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. That said, with the changes to this class, the comments have become more useful.
JsonSubTypes.Type(ReplaceAction::class, name = "replace"), | ||
JsonSubTypes.Type(RemoveServicesAction::class, name = "remove-services"), | ||
JsonSubTypes.Type(AddPublicKeysAction::class, name = "add-public-keys"), | ||
JsonSubTypes.Type(RemovePublicKeysAction::class, name = "remove-public-keys"), | ||
) | ||
public sealed class PatchAction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious why the sealed class route vs a marker interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason - changed in the base PR in 5392a4e
@andresuribe87 They were made 5 days ago, but held off hitting submit on the review to avoid distracting from deadline. Have duplicated the comment on the current HEAD with pointers back. |
@@ -151,6 +169,11 @@ public data class OperationSuffixDataObject( | |||
*/ | |||
public typealias Commitment = String |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Declared but not used in places like
public class MetadataMethod(
public val published: Boolean,
public val recoveryCommitment: String,
public val updateCommitment: String,
)
which I presume it should be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely! I massaged this a bit and now all instances are using the Commitment
and Reveal
classes.
public data class SidetreeUpdateOperation( | ||
public val type: String, | ||
public val didSuffix: String, | ||
public val revealValue: String, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use the Reveal
typealias?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
require(jwk.isPrivate) { | ||
"Importing a non-private key is not permitted" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the use-case driving the ability to import a public key?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to use the same test vectors from the ion-sdk
repository. The update request tests uses the public key of the next update, but not the private key.
See https://github.com/TBD54566975/web5-kt/blob/ad6a91e1a8cac29c752156116fdf253395c5bf7c/dids/src/test/kotlin/web5/sdk/dids/DidIonTest.kt#L292 for where it's used.
And see https://github.com/decentralized-identity/ion-sdk/blob/main/tests/IonRequest.spec.ts#L48 for the original test.
Took some time to use the current ION implementation last night beyond the default scenario. There's quite a bit of optionality that ends up creating a strange usage pattern where the caller effectively ends up having to use the keyManger outside ion to create keys for or associated to an ion did. I think it'd make more sense for the optionality to look something like this: package web5.sdk.dids
import web5.sdk.crypto.InMemoryKeyManager
public fun main() {
val km = InMemoryKeyManager()
val opts = CreateDidIonOptions(
services = listOf(
Service(...)
),
verificationMethods = listOf(
IonVerificationMethod(
alg: JWSAlgorithm.ES256K,
relationships = listOf(VerificationRelationships.Authentication, VerificationRelationships.Assertion)
)
)
)
val did = DidIon.create(km)
} other thoughts:
I think we can merge this PR into main and then address the above as a whole in a separate PR. thoughts? |
credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt
Outdated
Show resolved
Hide resolved
didResolutionResult.didDocument.assertionMethodVerificationMethodsDereferenced | ||
|
||
require(!assertionMethods.isNullOrEmpty()) { | ||
throw SignatureException("no assertion methods found in did document") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
throw SignatureException("no assertion methods found in did document") | |
throw SignatureException("No assertion methods found in DID document") |
Co-authored-by: Tom Daffurn <[email protected]>
thanks for the feedback @mistermoe
I agree that there is too much optionality. That said, I'm not a fan of digressing from the VerificationMethod data model described in the spec. Let's chat more about it in the followup PR.
Curious what made you think there wasn't any state within the manager?
My take on I originally thought to use
I actually found
No preference whatsoever. I did an example PR in #86 showcasing what it would look like to support any http framework, which I discourage as it seems like overkill.
|
Overview
Fixes #1 by implementing did ion updates.
Includes the updates of PR #88, which fixes #3 and #2.
Description
After this PR, all ion type of updates will be supported.
How Has This Been Tested?
Unit test that mimics https://github.com/decentralized-identity/ion-sdk/blob/main/tests/IonRequest.spec.ts#L38 as much as possible.