From d51910fe3bd63146e2e22a4b0f783cf838e760ce Mon Sep 17 00:00:00 2001
From: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com>
Date: Thu, 29 Feb 2024 09:57:56 +0100
Subject: [PATCH] Allow setting additional controllers for `IotaDocument`.
(#1314)
---
bindings/wasm/docs/api-reference.md | 111 +++++++-------
bindings/wasm/src/iota/iota_document.rs | 20 +++
.../src/document/iota_document.rs | 135 ++++++++++++++++--
.../src/state_metadata/document.rs | 29 +++-
4 files changed, 235 insertions(+), 60 deletions(-)
diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md
index e9e3a65a85..e83bbc81b6 100644
--- a/bindings/wasm/docs/api-reference.md
+++ b/bindings/wasm/docs/api-reference.md
@@ -187,10 +187,6 @@ working with storage backed DID documents.
## Members
-- StateMetadataEncoding
-
-- MethodRelationship
-
- CredentialStatus
- SubjectHolderRelationship
@@ -207,6 +203,9 @@ This variant is the default.
- Any
The holder is not required to have any kind of relationship to any credential subject.
+- StatusPurpose
+Purpose of a StatusList2021.
+
- FailFast
Declares when validation should return if an error occurs.
@@ -216,9 +215,6 @@ This variant is the default.
- FirstError
Return after the first error occurs.
-- StatusPurpose
-Purpose of a StatusList2021.
-
- StatusCheck
Controls validation behaviour when checking whether or not a credential has been revoked by its
credentialStatus
.
@@ -236,16 +232,17 @@ This variant is the default.
- SkipAll
Skip all status checks.
+- StateMetadataEncoding
+
+- MethodRelationship
+
## Functions
-- encodeB64(data) ⇒
string
-Encode the given bytes in url-safe base64.
-
-- decodeB64(data) ⇒
Uint8Array
-Decode the given url-safe base64-encoded slice into its raw bytes.
+- start()
+Initializes the console error panic hook for better error messages
- verifyEd25519(alg, signingInput, decodedSignature, publicKey)
Verify a JWS signature secured with the EdDSA
algorithm and curve Ed25519
.
@@ -255,8 +252,11 @@ This variant is the default.
This function does not check whether alg = EdDSA
in the protected header. Callers are expected to assert this
prior to calling the function.
-- start()
-Initializes the console error panic hook for better error messages
+- encodeB64(data) ⇒
string
+Encode the given bytes in url-safe base64.
+
+- decodeB64(data) ⇒
Uint8Array
+Decode the given url-safe base64-encoded slice into its raw bytes.
@@ -1967,6 +1967,7 @@ if the object is being concurrently modified.
* _instance_
* [.id()](#IotaDocument+id) ⇒ [IotaDID
](#IotaDID)
* [.controller()](#IotaDocument+controller) ⇒ [Array.<IotaDID>
](#IotaDID)
+ * [.setController(controllers)](#IotaDocument+setController)
* [.alsoKnownAs()](#IotaDocument+alsoKnownAs) ⇒ Array.<string>
* [.setAlsoKnownAs(urls)](#IotaDocument+setAlsoKnownAs)
* [.properties()](#IotaDocument+properties) ⇒ Map.<string, any>
@@ -2039,6 +2040,20 @@ NOTE: controllers are determined by the `state_controller` unlock condition of t
during resolution and are omitted when publishing.
**Kind**: instance method of [IotaDocument
](#IotaDocument)
+
+
+### iotaDocument.setController(controllers)
+Sets the controllers of the document.
+
+Note: Duplicates will be ignored.
+Use `null` to remove all controllers.
+
+**Kind**: instance method of [IotaDocument
](#IotaDocument)
+
+| Param | Type |
+| --- | --- |
+| controllers | [CoreDID
](#CoreDID) \| [Array.<CoreDID>
](#CoreDID) \| null
|
+
### iotaDocument.alsoKnownAs() ⇒ Array.<string>
@@ -6104,14 +6119,6 @@ Deserializes an instance from a JSON object.
| --- | --- |
| json | any
|
-
-
-## StateMetadataEncoding
-**Kind**: global variable
-
-
-## MethodRelationship
-**Kind**: global variable
## CredentialStatus
@@ -6142,6 +6149,12 @@ The holder must match the subject only for credentials where the [`nonTransferab
## Any
The holder is not required to have any kind of relationship to any credential subject.
+**Kind**: global variable
+
+
+## StatusPurpose
+Purpose of a [StatusList2021](#StatusList2021).
+
**Kind**: global variable
@@ -6160,12 +6173,6 @@ Return all errors that occur during validation.
## FirstError
Return after the first error occurs.
-**Kind**: global variable
-
-
-## StatusPurpose
-Purpose of a [StatusList2021](#StatusList2021).
-
**Kind**: global variable
@@ -6198,28 +6205,20 @@ Validate the status if supported, skip any unsupported
Skip all status checks.
**Kind**: global variable
-
-
-## encodeB64(data) ⇒ string
-Encode the given bytes in url-safe base64.
-
-**Kind**: global function
+
-| Param | Type |
-| --- | --- |
-| data | Uint8Array
|
+## StateMetadataEncoding
+**Kind**: global variable
+
-
+## MethodRelationship
+**Kind**: global variable
+
-## decodeB64(data) ⇒ Uint8Array
-Decode the given url-safe base64-encoded slice into its raw bytes.
+## start()
+Initializes the console error panic hook for better error messages
**Kind**: global function
-
-| Param | Type |
-| --- | --- |
-| data | Uint8Array
|
-
## verifyEd25519(alg, signingInput, decodedSignature, publicKey)
@@ -6242,9 +6241,25 @@ prior to calling the function.
| decodedSignature | Uint8Array
|
| publicKey | [Jwk
](#Jwk) |
-
+
-## start()
-Initializes the console error panic hook for better error messages
+## encodeB64(data) ⇒ string
+Encode the given bytes in url-safe base64.
+
+**Kind**: global function
+
+| Param | Type |
+| --- | --- |
+| data | Uint8Array
|
+
+
+
+## decodeB64(data) ⇒ Uint8Array
+Decode the given url-safe base64-encoded slice into its raw bytes.
**Kind**: global function
+
+| Param | Type |
+| --- | --- |
+| data | Uint8Array
|
+
diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs
index 8f8cbe6823..8d004422ad 100644
--- a/bindings/wasm/src/iota/iota_document.rs
+++ b/bindings/wasm/src/iota/iota_document.rs
@@ -5,12 +5,14 @@ use std::rc::Rc;
use identity_iota::core::Object;
use identity_iota::core::OneOrMany;
+
use identity_iota::core::OrderedSet;
use identity_iota::core::Timestamp;
use identity_iota::core::Url;
use identity_iota::credential::Credential;
use identity_iota::credential::JwtPresentationOptions;
use identity_iota::credential::Presentation;
+
use identity_iota::did::DIDUrl;
use identity_iota::iota::block::output::dto::AliasOutputDto;
use identity_iota::iota::block::output::AliasOutput;
@@ -48,6 +50,7 @@ use crate::credential::WasmJws;
use crate::credential::WasmJwt;
use crate::credential::WasmPresentation;
use crate::did::CoreDocumentLock;
+
use crate::did::PromiseJws;
use crate::did::PromiseJwt;
use crate::did::WasmCoreDocument;
@@ -156,6 +159,20 @@ impl WasmIotaDocument {
)
}
+ /// Sets the controllers of the document.
+ ///
+ /// Note: Duplicates will be ignored.
+ /// Use `null` to remove all controllers.
+ #[wasm_bindgen(js_name = setController)]
+ pub fn set_controller(&mut self, controller: &OptionArrayIotaDID) -> Result<()> {
+ let controller: Option> = controller.into_serde().wasm_result()?;
+ match controller {
+ Some(controller) => self.0.try_write()?.set_controller(controller),
+ None => self.0.try_write()?.set_controller([]),
+ };
+ Ok(())
+ }
+
/// Returns a copy of the document's `alsoKnownAs` set.
#[wasm_bindgen(js_name = alsoKnownAs)]
pub fn also_known_as(&self) -> Result {
@@ -845,6 +862,9 @@ impl From for WasmIotaDocument {
#[wasm_bindgen]
extern "C" {
+ #[wasm_bindgen(typescript_type = "IotaDID[] | null")]
+ pub type OptionArrayIotaDID;
+
#[wasm_bindgen(typescript_type = "IotaDID[]")]
pub type ArrayIotaDID;
diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs
index 89abf06cf5..7ae60381d7 100644
--- a/identity_iota_core/src/document/iota_document.rs
+++ b/identity_iota_core/src/document/iota_document.rs
@@ -5,7 +5,6 @@ use core::fmt;
use core::fmt::Debug;
use core::fmt::Display;
use identity_credential::credential::Jws;
-#[cfg(feature = "client")]
use identity_did::CoreDID;
use identity_did::DIDUrl;
use identity_document::verifiable::JwsVerificationOptions;
@@ -15,7 +14,6 @@ use serde::Deserialize;
use serde::Serialize;
use identity_core::common::Object;
-#[cfg(feature = "client")]
use identity_core::common::OneOrSet;
use identity_core::common::OrderedSet;
use identity_core::common::Url;
@@ -123,9 +121,6 @@ impl IotaDocument {
}
/// Returns an iterator yielding the DID controllers.
- ///
- /// NOTE: controllers are determined by the `state_controller` unlock condition of the output
- /// during resolution and are omitted when publishing.
pub fn controller(&self) -> impl Iterator- + '_ {
let core_did_controller_iter = self
.document
@@ -134,11 +129,31 @@ impl IotaDocument {
.into_iter()
.flatten();
- // CORRECTNESS: These casts are OK because the public API does not expose methods
- // enabling unchecked mutation of the controllers.
+ // CORRECTNESS: These casts are OK because the public API only allows setting IotaDIDs.
core_did_controller_iter.map(IotaDID::from_inner_ref_unchecked)
}
+ /// Sets the value of the document controller.
+ ///
+ /// Note:
+ /// * Duplicates in `controller` will be ignored.
+ /// * Use an empty collection to clear all controllers.
+ pub fn set_controller(&mut self, controller: T)
+ where
+ T: IntoIterator
- ,
+ {
+ let controller_core_dids: Option> = {
+ let controller_set: OrderedSet = controller.into_iter().map(CoreDID::from).collect();
+ if controller_set.is_empty() {
+ None
+ } else {
+ Some(OneOrSet::new_set(controller_set).expect("controller is checked to be not empty"))
+ }
+ };
+
+ *self.document.controller_mut() = controller_core_dids;
+ }
+
/// Returns a reference to the `alsoKnownAs` set.
pub fn also_known_as(&self) -> &OrderedSet {
self.document.also_known_as()
@@ -442,7 +457,14 @@ mod client_document {
_ => None,
};
- *self.core_document_mut().controller_mut() = controller_did.map(CoreDID::from).map(OneOrSet::new_one);
+ if let Some(controller_did) = controller_did {
+ match self.core_document_mut().controller_mut() {
+ Some(controllers) => {
+ controllers.append(CoreDID::from(controller_did));
+ }
+ None => *self.core_document_mut().controller_mut() = Some(OneOrSet::new_one(CoreDID::from(controller_did))),
+ }
+ }
Ok(())
}
@@ -731,6 +753,98 @@ mod tests {
assert_eq!(doc1, doc2);
}
+ #[test]
+ fn test_unpack_no_external_controller() {
+ let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ .parse()
+ .unwrap();
+ let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
+ .parse()
+ .unwrap();
+
+ let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone());
+ original_doc.set_controller([]);
+
+ let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did))
+ .with_state_metadata(original_doc.pack().unwrap())
+ .add_unlock_condition(UnlockCondition::StateControllerAddress(
+ StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))),
+ ))
+ .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new(
+ Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))),
+ )))
+ .finish()
+ .unwrap();
+
+ let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap();
+ let controllers: Vec = document.controller().cloned().collect::>();
+ assert_eq!(controllers.first().unwrap(), &alias_controller);
+ assert_eq!(controllers.len(), 1);
+ }
+
+ #[test]
+ fn test_unpack_with_duplicate_controller() {
+ let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ .parse()
+ .unwrap();
+ let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
+ .parse()
+ .unwrap();
+
+ let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone());
+ original_doc.set_controller([alias_controller.clone()]);
+
+ let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did))
+ .with_state_metadata(original_doc.pack().unwrap())
+ .add_unlock_condition(UnlockCondition::StateControllerAddress(
+ StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))),
+ ))
+ .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new(
+ Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))),
+ )))
+ .finish()
+ .unwrap();
+
+ let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap();
+ let controllers: Vec = document.controller().cloned().collect::>();
+ assert_eq!(controllers.first().unwrap(), &alias_controller);
+ assert_eq!(controllers.len(), 1);
+ }
+
+ #[test]
+ fn test_unpack_with_external_controller() {
+ let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ .parse()
+ .unwrap();
+ let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
+ .parse()
+ .unwrap();
+ let external_controller_did: IotaDID =
+ "did:iota:0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
+ .parse()
+ .unwrap();
+
+ let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone());
+ original_doc.set_controller([external_controller_did.clone()]);
+
+ let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did))
+ .with_state_metadata(original_doc.pack().unwrap())
+ .add_unlock_condition(UnlockCondition::StateControllerAddress(
+ StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))),
+ ))
+ .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new(
+ Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))),
+ )))
+ .finish()
+ .unwrap();
+
+ let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap();
+ let controllers: Vec = document.controller().cloned().collect::>();
+ assert_eq!(controllers.first().unwrap(), &external_controller_did);
+ assert_eq!(controllers.get(1).unwrap(), &alias_controller);
+ assert_eq!(controllers.len(), 2);
+ }
+
#[test]
fn test_unpack_empty() {
let controller_did: IotaDID = valid_did();
@@ -765,7 +879,10 @@ mod tests {
let packed: Vec = document.pack_with_encoding(StateMetadataEncoding::Json).unwrap();
let state_metadata_document: StateMetadataDocument = StateMetadataDocument::unpack(&packed).unwrap();
let unpacked_document: IotaDocument = state_metadata_document.into_iota_document(&did).unwrap();
- assert!(unpacked_document.document.controller().is_none());
+ assert_eq!(
+ unpacked_document.document.controller().unwrap().get(0).unwrap().clone(),
+ CoreDID::from(controller_did)
+ );
assert!(unpacked_document.metadata.state_controller_address.is_none());
assert!(unpacked_document.metadata.governor_address.is_none());
}
diff --git a/identity_iota_core/src/state_metadata/document.rs b/identity_iota_core/src/state_metadata/document.rs
index d15f0d8d26..e14e381f5b 100644
--- a/identity_iota_core/src/state_metadata/document.rs
+++ b/identity_iota_core/src/state_metadata/document.rs
@@ -79,7 +79,6 @@ impl StateMetadataDocument {
// Unset Governor and State Controller Addresses to avoid bloating the payload
self.metadata.governor_address = None;
self.metadata.state_controller_address = None;
- *self.document.controller_mut() = None;
let encoded_message_data: Vec = match encoding {
StateMetadataEncoding::Json => self
@@ -410,8 +409,7 @@ mod tests {
let TestSetup { document, .. } = test_document();
let mut state_metadata_doc: StateMetadataDocument = StateMetadataDocument::from(document);
let packed: Vec = state_metadata_doc.clone().pack(StateMetadataEncoding::Json).unwrap();
- // Controller and State Controller are set to None when packing
- *state_metadata_doc.document.controller_mut() = None;
+ // Governor and State Controller are set to None when packing
state_metadata_doc.metadata.governor_address = None;
state_metadata_doc.metadata.state_controller_address = None;
let expected_payload: String = format!(
@@ -434,6 +432,31 @@ mod tests {
assert_eq!(&packed[7..], expected_payload.as_bytes());
}
+ #[test]
+ fn test_no_controller() {
+ let TestSetup {
+ mut document, did_self, ..
+ } = test_document();
+ *document.core_document_mut().controller_mut() = None;
+ let state_metadata_doc: StateMetadataDocument = StateMetadataDocument::from(document);
+ let packed: Vec = state_metadata_doc.clone().pack(StateMetadataEncoding::Json).unwrap();
+ let expected_payload: String = format!(
+ "{{\"doc\":{},\"meta\":{}}}",
+ state_metadata_doc.document, state_metadata_doc.metadata
+ );
+ assert_eq!(&packed[7..], expected_payload.as_bytes());
+ let unpacked = StateMetadataDocument::unpack(&packed).unwrap();
+ assert_eq!(
+ unpacked
+ .into_iota_document(&did_self)
+ .unwrap()
+ .controller()
+ .collect::>()
+ .len(),
+ 0
+ );
+ }
+
#[test]
fn test_unpack_length_prefix() {
// Changing the serialization is a breaking change!