-
Notifications
You must be signed in to change notification settings - Fork 61
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 (Sidetree) resolver and client #379
Conversation
7a2f694
to
0cc6689
Compare
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.
Half of the code is checking whether fields exist and/or are of the right type. It breaks my heart a little bit 😅
Don't take my comments as requests for changes. They're mostly questions, and sometimes complaints at how spaghetti dish-like the specs appear to me.
/// A transaction for a DID method | ||
#[derive(Debug, Serialize, Deserialize, Builder, Clone, PartialEq)] | ||
#[serde(rename_all = "camelCase")] | ||
pub struct DIDMethodTransaction { |
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 don't fully understand everything behind DID transactions, but I don't know if it's a good idea to generalise them? We give up the benefits of the type system for a slight improvement in the time it takes to make a change?
Would one really use DID methods interchangeably so much that they would expect to be able to be able to make transactions generically?
Apart from DIDKit, sort of, but I think that's its role, to provide a fairly unified interface, but keep the "type purity" in
ssi
.
My initial thoughts for alternatives would be:
- a separate trait for transactions, probably with this struct's
value
type being that is can be serialised; or - don't have a trait, let methods implement whatever they need (maybe some of them wouldn't have an
update
transaction, or instead more kinds of transactions).
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.
Ok I see this is from the did-registrar. I still think it would be better to have a separate trait.
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.
More info about the proposed transaction concept, in didkit-cli's readme: https://github.com/spruceid/didkit/blob/3a220b0fddfa6565a2214284430a079ac7057a10/cli/README.md#did-method-transaction
DID Method Transaction is like a Sidetree Operation Request generalized to arbitrary DID methods. The contents are DID-method-specific, except for the didMethod
property which would be used by DIDKit and/or ssi's DIDMethods struct to route the operation to a DID method implementation for submission/processing.
In terms of DID Registration, the transaction represents a DID operation that is ready to be written to the DID method's verifiable data registry. I don't see this data structure as being exposed in DID Registration API; it's otherwise internal to the
implementation. It's not using DID Registration's didDocument/didDocumentOperation fields - although it might be convertible to/from them - but its signature is over a method-specific data structure. For other DID methods, the DID method transaction might contain a signed blockchain transaction (e.g. for did:ethr
, did:tz
, and did:btcr
). (For Sidetree, the blockchain transaction is a separate step, in this implementation assumed to be done on the other side of the Sidetree REST API.)
Sidetree in https://identity.foundation/sidetree/spec/v1.0.0/#sidetree-operations requires clients to "keep the operation payload once it is submitted to a Sidetree node until it is generally available and observed" and to resubmit the same operation payload "if the submitted operation is not anchored and propagated, for whatever reason". Without the transaction concept, the operation functions here would need to block until the operation is observed, and/or maintain the operation in some external state. By returning the Sidetree Operation request as a DID method transaction to be separately submitted/published using DIDMethod::submit_transaction
(didkit did-submit-tx
), the operation functions stay more side-effect-free and do not risk being interrupted and losing the operation. Keeping the signed operations also allows the caller to prove their DID state with the chain of signed operations.
Note that currently this implementation does not wait for and detect that an operation has been successfully applied. So the caller must do that, resubmitting the operation if needed. This limitation could be addressed by making the submit tx function poll the resolver to detect the corresponding update - or by adding a separate function to do that.
I might need help to make the transaction into a trait. I thought of making the transaction type into an associated type on DIDMethod, but this does not work, because DIDResolver is used as a trait object, so DIDMethod cannot have generic type parameters, e.g.:
error[E0038]: the trait `DIDResolver` cannot be made into an object
--> src/ldp.rs:2085:20
|
2085 | resolver: &dyn DIDResolver,
| ^^^^^^^^^^^^^^^ `DIDResolver` cannot be made into an object
|
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visi
t <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
--> src/did_resolve.rs:469:8
|
410 | pub trait DIDResolver: Sync {
| ----------- this trait cannot be made into an object...
...
469 | fn to_did_method<T>(&self) -> Option<&dyn DIDMethod<Transaction = T>> {
| ^^^^^^^^^^^^^ ...because method `to_did_method` has generic type parameters
= help: consider moving `to_did_method` to another trait
Also I'm not sure how this would be handled in DIDMethods where there is a map of strings to DIDMethod trait objects which each might use a different transaction type, but I'm not sure how to express that:
pub struct DIDMethods<'a> {
/// Collection of DID methods by method id.
pub methods: HashMap<&'a str, &'a dyn DIDMethod<Transaction = ...>>,
}
If the functions using the Transaction type are moved out of DIDMethod into a new trait, e.g. DIDRegistrar or DIDOperator (like how the resolve functions are in DIDResolver), I would still assume to use a conversion function like fn to_registrar<T>(&self) -> &dyn DIDRegistrar<Transaction = T>
(like DIDMethod::to_resolver
) but this still doesn't work with DIDMethod being object-safe, so again I'm not sure what to do.
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.
TBH I think to_resolver
is an anti-pattern. If you need both DIDMethod
and DIDResolver
they should be both specified or if you can't have one without the other they should be combined.
I like the idea of having multiple implementations of a DIDRegistar
trait for each method for each kind of transaction. It puts the burden on the user of ssi
to route the types of messages, but it probably make sense? Some DIDs can be created generated locally and/or created in a registrar -- there are so many variants that users are probably best placed to chose what to do?
did-ion/src/sidetree.rs
Outdated
verification_key, | ||
options, | ||
} = create; | ||
ensure!(is_empty(&options), "Create options not supported"); |
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.
Just to double check, in this kind of scenario, should it be a hard failure or just a warning?
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.
Right now the options are method-specific, and there aren't other options to use, so I don't know exactly.
Erroring on unknown option seems to me like erring on the side of caution.
But in DID Registrar driver implementations that I looked at, there is no such requirement that all passed options are recognized. So it might be more consistent to drop this ensure
- or indeed make it a warning (changing the function's return type and plumbing the warnings through to DIDKit).
It could be nice if there was some convention to indicate whether an option may be safely ignored or is required to be understood - like using the "crit" JWS header parameter.
Unlike with DID resolution input metadata / options, there isn't yet a registry of DID operation options. DID resolution options have a similar issue I think, of how to know whether an option must be understood or can be ignored. I think the tendency is to treat options as optional to implement by default. Then the caller might have to infer from the output whether the option was actually understood and processed. (vs. inferring from an error message that a given option was unsupported, which would happen with this current implementation). The current Sidetree/ION DID resolver HTTP(S) endpoint though actually fail if any query string parameters are passed, making any resolution input-metadata/options result in an error... Some of those options, like service
, could be handled client-side (in ssi's dereference function).
For DID method operations besides read/resolve, one approach could be that a critical option (option that must be understood) should just have a new operation defined for it specifically. This PR doesn't add a generic mechanism for additional DID method operations, although of course DID method implementations could add their own method-specific operation implementations.
did-ion/src/lib.rs
Outdated
@@ -0,0 +1,26 @@ | |||
use anyhow::{ensure, Context, Error, Result}; |
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 don't know how it will be in the future, but maybe it would be good to use thiserror
at the top level, and just have one variant to catch all anyhow
errors?
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.
In this file anyhow
is being used for the return types of functions in the implementation of the Sidetree
trait defined in sidetree.rs
. Should there be an associated type on the Sidetree trait for these errors? In this file it's just generate_key
and validate_key
implemented, but the other functions in the Sidetree trait are currently using anyhow::Result
also; presumably those should be changed too, to use various thiserror
Error structs, or a single Error struct for the base Sidetree implementation? I might need help with this.
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.
To me, it seems like the Error type can be shared between all implementations? Either it's an ssi
error, or a request error, or something else that the user can't really do anything about. You can also use different error types for different methods.
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.
Opened #392 about this.
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 in 909790b and d96ee38 (Edit: and bc52ab0, 1b30a4f); there shouldn't be any more anyhow
exposed in the public APIs now - except in the error variant SidetreeError::Other.
.post(url) | ||
.json(&op) | ||
.header("Accept", "application/json") | ||
.header("User-Agent", ssi::USER_AGENT) |
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.
Could maybe share the client in the struct, and maybe have a helper in ssi
to make sure we always have this header?
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.
Sharing the client would be good to allow connection reuse.
I think the User-Agent header should include the did-ion crate version also. (similar for the other DID method crates)
// Update operation may return empty body with 200 OK. | ||
return Ok(Value::Null); | ||
} | ||
let bytes = resp.bytes().await.context("Unable to read HTTP response")?; |
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.
Why not use .json()
?
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.
.json()
could be used. I think the current way is leftover from when I was debugging why certain results failed JSON parsing. (It was because the body was empty, now caught above). Edit: yes, the next line shows falling back to return the unparsed output in the error if parsing failed. Now that I know that the Sidetree reference implementation returns JSON on Create and an empty body for all other operations, maybe this could be safely changed. But I'm still wary that there might possibly be some useful non-JSON output returned...
fn deactivate(&self, _deactivate: DIDDeactivate) -> AResult<DIDMethodTransaction> { | ||
bail!("Deactivate operation not implemented for DID Method"); | ||
} | ||
|
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.
Similar to https://github.com/spruceid/ssi/pull/379/files#r806777763 about the Sidetree
trait..
I since learned that anyhow
is not recommended for APIs; maybe these should be changed back to using ssi::error::Error
or something? Error associated type seems problematic for object-safety (also discussed in https://github.com/spruceid/ssi/pull/379/files#r807305656 about DID method transactions).
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.
Opened #392
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.
Wouldn't it be preferable to do the switch now to avoid breaking the API in the future? Not that it's a lot of work to adapt code for consumers.
The code could simply be
#[derive(Error, Debug)]
pub enum MyError {
#[error(transparent)]
Other(#[from] anyhow::Error), // source and Display delegate to anyhow::Error
}
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.
Trying that... It seems that bail!
and ensure!
cannot be used then.
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 in a7879ce
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 have a few comment/questions for future improvements but I guess it can be merged as is.
- Clippy has a few errors/warnings.
- Maybe
DIDMethodError::NotImplemented
should actually be a panic, withunimplemented!
ortodo!
. - If
DIDMethodError::OptionNotSupported
means that not all methods support all operations, maybe that means the trait should be split, to get compile time guarantees. cargo build
indid-ion
doesn't work for me. We probably should try to have every crate in the workspace compile separately with default features.
- Use reqwest. - Keep hyper as dev-dependency for testing server. - Remove hyper-tls as unused.
- Don't use anyhow in did module - Use Map for DID method options, for simpler checking
- Add SidetreeError and JWSDecodeVerifyError - Remove anyhow from sidetree API
- Add PublicKeyJwkFromJWKError - Add JWKFromPublicKeyJwkError - Remove anyhow::Error from JWSDecodeVerifyError
Rebased and squashed some commits (1b30a4f → cd6a760). @sbihel |
[ ] Handle response from Sidetree node- Not sure what to do with the responses. Just passing back Value to in thesubmit_transaction
function, for now.Docs preview: https://demo.didkit.dev/2022/02/25/ssi-rustdoc/did_ion/sidetree/