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
Add Wasm Account
Storage
interface
#597
Add Wasm Account
Storage
interface
#597
Conversation
…ount # Conflicts: # identity/Cargo.toml
…Value, JsValue> to an AccountResult
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.
Thanks for the memory store implementation in TS!
I still have two primary concerns.
-
There are still a lot of types that seem unnecessarily exposed. On the flipside, I'm concerned we might be missing places where the types that appear in the storage trait do not have (de)serialization functions exposed. Ideally, we would verify that these types can all be (de)serialized in the memory_storage.ts implementation, by turning it into a file store, or something like that. In-memory by itself also seems fine, as long as all types go through serialization at some point.
-
The other bigger concern is around documentation of functions, mostly relating to stating the base58 requirement, and if we actually want to keep that, since it requires lots of encoding from implementors, while we could just as easily expose these things as bytes.
#[cfg_attr(not(feature = "wasm"), async_trait)] | ||
#[cfg_attr(feature = "wasm", async_trait(?Send))] |
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 suspect that we will need this Send
/?Send
feature for future bindings as well (Python comes to mind), and so we could introduce a separate feature for this, so it's not tied to wasm exclusively. Maybe non-send-storage
, or some better name 😅? Anyway, this might be overdoing it for now. I think we can change this in non-breaking ways in the future.
/** Deletes the keypair specified by `location`.*/ | ||
keyDel: (did: DID, keyLocation: KeyLocation) => Promise<void>; |
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.
Is keyDel
supposed to be idempotent? This came up when I looked over the memory_storage.ts implementation, which behaved slightly differently from the Rust MemStore
. Idempotency seems like a good idea to me here, and we should state that in the doc as well, then. That would also require changes to all implementations. Feel free to leave this for a later PR, in which case we should open an issue, so it's not forgotten about. I can open the issue if there is agreement for that.
/** Returns the last generation that has been published to the tangle for the given `did`.*/ | ||
publishedGeneration: (did: DID) => Promise<Generation>; | ||
|
||
/** Sets the last generation that has been published to the tangle for the given `did`.*/ | ||
setPublishedGeneration: (did: DID, generation: Generation) => Promise<void>; |
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 assume this will be removed in the napi or epic branch?
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.
Yes. Handled on #660
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 few minor notes on the latest commits, will do another pass in a bit.
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 looks good to me now. Thank you very much for addressing all the comments! This is a really solid Rust "trait export" to Wasm, imho.
I honestly may have missed a thing or two because of the split in epic branch and napi stronghold, but since this goes through more reviews in those PRs I believe that should be okay.
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 assume things like DIDLease
and publishedGeneration
have been removed in the other PR.
Otherwise it seems good overall, few general comments.
public async setPassword(_encryptionKey: Uint8Array): Promise<void> {} | ||
|
||
public async flushChanges(): Promise<void> {} |
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.
We might need a comment here explaining that these functions are not necessary here but are needed for other Storage
implementations. I.e. it's fine to have no-op functions?
Aside: still not sure if we're going to keep setPassword
, since I'm hoping that Stronghold
will take over password management. Basically authentication should be part of the implementation rather than the interface, ideally.
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.
Good point, it should be public async flushChanges(): Promise<void> {return Promise.resolve()}}
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.
Fully agree setPassword
should be removed, but perhaps in a follow-up PR? The changes required are minimal, I think, so it could be done here as well. I'm fine either way. Passwords should be passed to storage constructors and not be required post-creation anymore, which will, for example, be the case with the upcoming stronghold refactor. Some storages might not even need a password, so it shouldn't be part of the 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.
I'm fine with any changes being added to an issue or appended to #669, as long as there's something to keep track of them.
#[cfg(feature = "wasm")] | ||
#[error("JsValue serialization error: {0}")] | ||
SerializationError(String), | ||
#[cfg(feature = "wasm")] | ||
#[error("javascript function threw an exception: {0}")] | ||
JsError(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.
Regarding errors for the Storage
interface, I think it's a bit unwieldy to restrict implementers to the top-level AccountError
.
Maybe we can have a sub-error enum for the Storage
and map them to a single AccountError
internally? This would be similar to the Error
in the diff crate, where each variant maps to a function.
#[derive(Debug, thiserror::Error, strum::IntoStaticStr)]
pub enum Error {
#[error("Diff Error: {0}")]
DiffError(String),
#[error("Merge Error: {0}")]
MergeError(String),
#[error("Conversion Error: {0}")]
ConversionError(String),
}
We could use strings too or retain the error source
with e.g. Box<dyn Error>
to preserve reporting, as mentioned in #636.
This would basically eliminate the need for these feature-gated JsError
/SerializationError
variants since we could define errors in the Wasm bindings themselves that are bubbled up to the correct variant with no loss of context (hopefully).
This would benefit future bindings and Storage
implementers using the core Rust library too.
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.
There definitely needs to be a separate StorageError
, and I really like your suggestion with Box<dyn Error>
. Still, I think this needs a good amount of thought put into it to provide a reasonably abstract error that can accommodate not just stronghold and memory stores, which is why I would suggest doing this in a separate PR and leave the current approach as an acceptable temporary solution. I'd be happy to open an issue for it.
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.
Sure, it can be added to #669 or its own issue. I wouldn't merge this without having a tracking issue for what's left, however.
let signature: Uint8Array = Ed25519.sign(data, keyPair.private); | ||
return new Signature(keyPair.public, signature) |
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 discussion is continued in the related issue #669 (comment)
#[wasm_bindgen(js_name = "fromBase58String")] | ||
pub fn from_base58_string(private_key: &str) -> Result<WasmPrivateKey> { | ||
let private_key: PrivateKey = decode_b58(private_key).map_err(wasm_error)?.into(); | ||
let private_key_bytes: [u8; 32] = <[u8; 32]>::try_from(private_key.as_ref()) | ||
.map_err(|err| AccountError::InvalidPrivateKey(format!("expected a slice of 32 bytes - {}", err))) | ||
.wasm_result()?; | ||
Ok(WasmPrivateKey(ed25519::SecretKey::from_bytes(private_key_bytes))) | ||
} | ||
|
||
/// Returns a base58 encoded string that represents the PublicKey. | ||
#[wasm_bindgen(js_name = publicKey)] | ||
pub fn public_key(&self) -> String { | ||
let public: ed25519::PublicKey = self.0.public_key(); | ||
let public_key: PublicKey = public.to_bytes().to_vec().into(); | ||
encode_b58(&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.
Mentioned in #669 that we should probably move to UInt8Array
rather than Base58-BTC encoded strings but that's a larger refactor for the Wasm bindings. We should really use the same types everywhere though, so WasmPrivateKey
in WasmDocument
and WasmKeyPair
too?
Doesn't need to be addressed in this PR, maybe added to #669 as a discussion point?
/// Convenience struct to convert Result<JsValue, JsValue> to an AccountResult<_, AccountError> | ||
pub struct JsValueResult(pub(crate) Result<JsValue>); |
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.
See the previous comment on possibly refactoring Storage
errors, it might make JsValueResult
unnecessary since errors would be mapped to e.g. StorageError
variants internally?
#[derive(Clone, Debug, Deserialize)] | ||
pub struct Signature { | ||
pub(crate) pkey: PublicKey, | ||
pub(crate) data: Vec<u8>, |
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 I commented about this on the other PR but do we need Signature
to contain the PublicKey
as well? I don't think it's used. Is it needed for future signature schemes?
Not related to the changes in this PR but it is about the Storage
implementation.
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 we don't need it. In fact, this is unused in the account, too. The account Signature
type could need a refactoring to simply return either a Vec<u8>
or a newtype wrapper around it. The pkey
is simply discarded in RemoteSign
. Could be part of a number of storage refactors mentioned before (redesign errors, remove set_password
, refactor Signature
)?
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'm fine with any changes being added to an issue or appended to #669.
Description of change
Expose
Storage
trait in the bindings.Links to any relevant issues
Type of change
How the change has been tested
Change checklist