From a0d36cb8c6002aa447a4c34f7ef3580b8637ad87 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Mon, 25 Nov 2024 13:38:42 +0100 Subject: [PATCH 1/5] rebase commit --- .../packages/iota_identity/sources/asset.move | 2 +- .../iota_identity/sources/controller.move | 144 ++++++++++++++ .../iota_identity/sources/identity.move | 176 ++++++++++++------ .../iota_identity/sources/migration.move | 10 +- .../sources/multicontroller.move | 110 ++++++++--- .../iota_identity/sources/permissions.move | 20 ++ .../sources/proposals/borrow.move | 5 +- .../sources/proposals/config.move | 7 +- .../sources/proposals/transfer.move | 5 +- .../sources/proposals/value.move | 9 +- .../src/rebased/migration/identity.rs | 6 +- .../sui/move_calls/identity/borrow_asset.rs | 10 +- .../rebased/sui/move_calls/identity/config.rs | 20 +- .../rebased/sui/move_calls/identity/create.rs | 10 +- .../sui/move_calls/identity/deactivate.rs | 10 +- .../sui/move_calls/identity/proposal.rs | 22 +-- .../sui/move_calls/identity/send_asset.rs | 12 +- .../rebased/sui/move_calls/identity/update.rs | 10 +- .../src/rebased/sui/move_calls/utils.rs | 30 +++ identity_iota_core/tests/e2e/identity.rs | 2 +- 20 files changed, 479 insertions(+), 141 deletions(-) create mode 100644 identity_iota_core/packages/iota_identity/sources/controller.move create mode 100644 identity_iota_core/packages/iota_identity/sources/permissions.move diff --git a/identity_iota_core/packages/iota_identity/sources/asset.move b/identity_iota_core/packages/iota_identity/sources/asset.move index 5747bf21a..40ed3703b 100644 --- a/identity_iota_core/packages/iota_identity/sources/asset.move +++ b/identity_iota_core/packages/iota_identity/sources/asset.move @@ -124,7 +124,7 @@ module iota_identity::asset { transfer::share_object(proposal); } - /// Strucure that encodes the logic required to transfer an `AuthenticatedAsset` + /// Structure that encodes the logic required to transfer an `AuthenticatedAsset` /// from one address to another. The transfer can be refused by the recipient. public struct TransferProposal has key { id: UID, diff --git a/identity_iota_core/packages/iota_identity/sources/controller.move b/identity_iota_core/packages/iota_identity/sources/controller.move new file mode 100644 index 000000000..ae39130d5 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/controller.move @@ -0,0 +1,144 @@ +module iota_identity::controller { + use iota::transfer::Receiving; + use iota::borrow::{Self, Referent, Borrow}; + use iota_identity::permissions; + + public use fun delete_controller_cap as ControllerCap.delete; + public use fun delete_delegation_token as DelegationToken.delete; + + /// This `ControllerCap` cannot delegate access. + const ECannotDelegate: u64 = 0; + // The permission of the provided `DelegationToken` are not + // valid to perform this operation. + const EInvalidPermissions: u64 = 1; + + /// Event that is created when a new `DelegationToken` is minted. + public struct NewDelegationTokenEvent has copy, drop { + controller: ID, + token: ID, + permissions: u32, + } + + /// Capability that allows to access mutative APIs of a `Multicontroller`. + public struct ControllerCap has key { + id: UID, + can_delegate: bool, + access_token: Referent, + } + + public fun id(self: &ControllerCap): &UID { + &self.id + } + + /// Borrows this `ControllerCap`'s access token. + public fun borrow(self: &mut ControllerCap): (DelegationToken, Borrow) { + self.access_token.borrow() + } + + /// Returns the borrowed access token together with the hot potato. + public fun put_back(self: &mut ControllerCap, token: DelegationToken, borrow: Borrow) { + self.access_token.put_back(token, borrow); + } + + /// Creates a delegation token for this controller. The created `DelegationToken` + /// will have full permissions. Use `delegate_with_permissions` to set or unset + /// specific permissions. + public fun delegate(self: &ControllerCap, ctx: &mut TxContext): DelegationToken { + assert!(self.can_delegate, ECannotDelegate); + new_delegation_token(self.id.to_inner(), permissions::all(), ctx) + } + + /// Creates a delegation token for this controller, specifying the delegate's permissions. + public fun delegate_with_permissions(self: &ControllerCap, permissions: u32, ctx: &mut TxContext): DelegationToken { + assert!(self.can_delegate, ECannotDelegate); + new_delegation_token(self.id.to_inner(), permissions, ctx) + } + + /// A token that allows an entity to act in a Controller's stead. + public struct DelegationToken has key, store { + id: UID, + permissions: u32, + controller: ID, + } + + /// Returns the controller's ID of this `DelegationToken`. + public fun controller(self: &DelegationToken): ID { + self.controller + } + + /// Returns the permissions of this `DelegationToken`. + public fun permissions(self: &DelegationToken): u32 { + self.permissions + } + + /// Returns true if this `DelegationToken` has permission `permission`. + public fun has_permission(self: &DelegationToken, permission: u32): bool { + self.permissions & permission != 0 + } + + /// Aborts if this `DelegationToken` doesn't have permission `permission`. + public fun assert_has_permission(self: &DelegationToken, permission: u32) { + assert!(self.has_permission(permission), EInvalidPermissions) + } + + /// Creates a new `ControllerCap`. + public(package) fun new(can_delegate: bool, ctx: &mut TxContext): ControllerCap { + let id = object::new(ctx); + let access_token = borrow::new(new_delegation_token(id.to_inner(), permissions::all(), ctx), ctx); + + ControllerCap { + id, + access_token, + can_delegate, + } + } + + /// Transfer a `ControllerCap`. + public(package) fun transfer(cap: ControllerCap, recipient: address) { + transfer::transfer(cap, recipient) + } + + /// Receives a `ControllerCap`. + public(package) fun receive(owner: &mut UID, cap: Receiving): ControllerCap { + transfer::receive(owner, cap) + } + + public(package) fun new_delegation_token( + controller: ID, + permissions: u32, + ctx: &mut TxContext + ): DelegationToken { + let id = object::new(ctx); + + iota::event::emit(NewDelegationTokenEvent { + controller, + token: id.to_inner(), + permissions, + }); + + DelegationToken { + id, + controller, + permissions, + } + } + + public(package) fun delete_controller_cap(cap: ControllerCap) { + let ControllerCap { + access_token, + id, + .. + } = cap; + + delete_delegation_token(access_token.destroy()); + object::delete(id); + } + + public(package) fun delete_delegation_token(token: DelegationToken) { + let DelegationToken { + id, + .. + } = token; + object::delete(id); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/identity.move b/identity_iota_core/packages/iota_identity/sources/identity.move index 0e68188df..592c624ea 100644 --- a/identity_iota_core/packages/iota_identity/sources/identity.move +++ b/identity_iota_core/packages/iota_identity/sources/identity.move @@ -8,7 +8,8 @@ module iota_identity::identity { clock::Clock, }; use iota_identity::{ - multicontroller::{Self, ControllerCap, Multicontroller, Action}, + multicontroller::{Self, Multicontroller, Action}, + controller::{DelegationToken, ControllerCap}, update_value_proposal, config_proposal, transfer_proposal::{Self, Send}, @@ -41,7 +42,7 @@ module iota_identity::identity { clock: &Clock, ctx: &mut TxContext ): Identity { - new_with_controller(doc, ctx.sender(), clock, ctx) + new_with_controller(doc, ctx.sender(), false, clock, ctx) } /// Creates an identity specifying its `created` timestamp. @@ -52,7 +53,7 @@ module iota_identity::identity { clock: &Clock, ctx: &mut TxContext ): Identity { - let mut identity = new_with_controller(doc, ctx.sender(), clock, ctx); + let mut identity = new_with_controller(doc, ctx.sender(), false, clock, ctx); assert!(identity.updated >= creation_timestamp, EInvalidTimestamp); identity.created = creation_timestamp; @@ -64,13 +65,14 @@ module iota_identity::identity { public fun new_with_controller( doc: vector, controller: address, + can_delegate: bool, clock: &Clock, ctx: &mut TxContext, ): Identity { let now = clock.timestamp_ms(); Identity { id: object::new(ctx), - did_doc: multicontroller::new_with_controller(doc, controller, ctx), + did_doc: multicontroller::new_with_controller(doc, controller, can_delegate, ctx), created: now, updated: now, } @@ -82,6 +84,7 @@ module iota_identity::identity { public fun new_with_controllers( doc: vector, controllers: VecMap, + controllers_that_can_delegate: VecMap, threshold: u64, clock: &Clock, ctx: &mut TxContext, @@ -93,7 +96,7 @@ module iota_identity::identity { let now = clock.timestamp_ms(); Identity { id: object::new(ctx), - did_doc: multicontroller::new_with_controllers(doc, controllers, threshold, ctx), + did_doc: multicontroller::new_with_controllers(doc, controllers, controllers_that_can_delegate, threshold, ctx), created: now, updated: now, } @@ -124,18 +127,16 @@ module iota_identity::identity { /// Approve an `Identity`'s `Proposal`. public fun approve_proposal( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ) { self.did_doc.approve_proposal, T>(cap, proposal_id); } - /// Proposes the deativates the DID Document contained in this `Identity`. - /// This function can deactivate the DID Document right away if `cap` has - /// enough voting power. + /// Proposes the deativation of the DID Document contained in this `Identity`. public fun propose_deactivation( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, clock: &Clock, ctx: &mut TxContext, @@ -160,7 +161,7 @@ module iota_identity::identity { /// Executes a proposal to deactivate this `Identity`'s DID document. public fun execute_deactivation( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, clock: &Clock, ctx: &mut TxContext, @@ -179,7 +180,7 @@ module iota_identity::identity { /// enough voting power. public fun propose_update( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, updated_doc: vector, expiration: Option, clock: &Clock, @@ -208,7 +209,7 @@ module iota_identity::identity { /// Executes a proposal to update the DID Document contained in this `Identity`. public fun execute_update( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, clock: &Clock, ctx: &mut TxContext, @@ -228,7 +229,7 @@ module iota_identity::identity { /// has enough voting power. public fun propose_config_change( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, threshold: Option, controllers_to_add: VecMap, @@ -261,7 +262,7 @@ module iota_identity::identity { /// Execute a proposal to change this `Identity`'s AC. public fun execute_config_change( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext ) { @@ -276,7 +277,7 @@ module iota_identity::identity { /// Proposes the transfer of a set of objects owned by this `Identity`. public fun propose_send( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, objects: vector, recipients: vector
, @@ -305,7 +306,7 @@ module iota_identity::identity { /// in order to use them in a transaction. Borrowed assets must be returned. public fun propose_borrow( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, objects: vector, ctx: &mut TxContext, @@ -334,7 +335,7 @@ module iota_identity::identity { /// to add a new controller. public fun propose_new_controller( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, new_controller_addr: address, voting_power: u64, @@ -349,14 +350,35 @@ module iota_identity::identity { /// Executes an `Identity`'s proposal. public fun execute_proposal( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext, ): Action { self.did_doc.execute_proposal(cap, proposal_id, ctx) } - /// Checks if `data` is a state matadata representing a DID. + /// revoke the `DelegationToken` with `ID` `deny_id`. Only controllers can perform this operation. + public fun revoke_token(self: &mut Identity, cap: &ControllerCap, deny_id: ID) { + self.did_doc.revoke_token(cap, deny_id); + } + + /// Un-revoke a `DelegationToken`. + public fun unrevoke_token(self: &mut Identity, cap: &ControllerCap, token_id: ID) { + self.did_doc.unrevoke_token(cap, token_id); + } + + /// Destroys a `ControllerCap`. Can only be used after a controller has been removed from + /// the controller committee. + public fun destroy_controller_cap(self: &Identity, cap: ControllerCap) { + self.did_doc.destroy_controller_cap(cap); + } + + /// Destroys a `DelegationToken`. + public fun destroy_delegation_token(self: &mut Identity, token: DelegationToken) { + self.did_doc.destroy_delegation_token(token); + } + + /// Checks if `data` is a state metadata representing a DID. /// i.e. starts with the bytes b"DID". public(package) fun is_did_output(data: &vector): bool { data[0] == 0x44 && // b'D' @@ -380,7 +402,8 @@ module iota_identity::identity_tests { use iota::test_scenario; use iota_identity::identity::{new, ENotADidDocument, Identity, new_with_controllers}; use iota_identity::config_proposal::Modify; - use iota_identity::multicontroller::{ControllerCap, EExpiredProposal, EThresholdNotReached}; + use iota_identity::multicontroller::{EExpiredProposal, EThresholdNotReached}; + use iota_identity::controller::ControllerCap; use iota::vec_map; use iota::clock; @@ -401,16 +424,19 @@ module iota_identity::identity_tests { // Create a request to add a second controller. let mut identity = scenario.take_shared(); - let controller1_cap = scenario.take_from_address(controller1); + let mut controller1_cap = scenario.take_from_address(controller1); + let (token, borrow) = controller1_cap.borrow(); // This is carried out immediately. - identity.propose_new_controller(&controller1_cap, option::none(), controller2, 1, scenario.ctx()); + identity.propose_new_controller(&token, option::none(), controller2, 1, scenario.ctx()); + controller1_cap.put_back(token, borrow); scenario.next_tx(controller2); - let controller2_cap = scenario.take_from_address(controller2); - - identity.did_doc().assert_is_member(&controller2_cap); + let mut controller2_cap = scenario.take_from_address(controller2); + let (token, borrow) = controller2_cap.borrow(); + identity.did_doc().assert_is_member(&token); + controller2_cap.put_back(token, borrow); // Cleanup test_scenario::return_to_address(controller1, controller1_cap); test_scenario::return_to_address(controller2, controller2_cap); @@ -437,6 +463,7 @@ module iota_identity::identity_tests { let identity = new_with_controllers( b"DID", controllers, + vec_map::empty(), 2, &clock, scenario.ctx(), @@ -447,11 +474,12 @@ module iota_identity::identity_tests { // `controller1` creates a request to remove `controller3`. let mut identity = scenario.take_shared(); - let controller1_cap = scenario.take_from_address(controller1); + let mut controller1_cap = scenario.take_from_address(controller1); let controller3_cap = scenario.take_from_address(controller3); + let (token, borrow) = controller1_cap.borrow(); let proposal_id = identity.propose_config_change( - &controller1_cap, + &token, option::none(), option::none(), vec_map::empty(), @@ -459,17 +487,22 @@ module iota_identity::identity_tests { vec_map::empty(), scenario.ctx() ).destroy_some(); + controller1_cap.put_back(token, borrow); scenario.next_tx(controller2); // `controller2` also approves the removal of `controller3`. - let controller2_cap = scenario.take_from_address(controller2); - identity.approve_proposal(&controller2_cap, proposal_id); + let mut controller2_cap = scenario.take_from_address(controller2); + let (token, borrow) = controller2_cap.borrow(); + identity.approve_proposal(&token, proposal_id); + controller2_cap.put_back(token, borrow); scenario.next_tx(controller2); // `controller3` is removed. - identity.execute_config_change(&controller2_cap, proposal_id, scenario.ctx()); + let (token, borrow) = controller2_cap.borrow(); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + controller2_cap.put_back(token, borrow); assert!(!identity.did_doc().controllers().contains(&controller3_cap.id().to_inner()), 0); // cleanup. @@ -505,6 +538,7 @@ module iota_identity::identity_tests { let identity = new_with_controllers( b"DID", controllers, + vec_map::empty(), 10, &clock, scenario.ctx(), @@ -514,16 +548,20 @@ module iota_identity::identity_tests { // Controller A alone should be able to do anything. let mut identity = scenario.take_shared(); - let controller_a_cap = scenario.take_from_address(controller_a); + let mut controller_a_cap = scenario.take_from_address(controller_a); + let (token, borrow) = controller_a_cap.borrow(); // Create a request to add a new controller. This is carried out immediately as controller_a has enough voting power - identity.propose_new_controller(&controller_a_cap, option::none(), controller_d, 1, scenario.ctx()); + identity.propose_new_controller(&token, option::none(), controller_d, 1, scenario.ctx()); + controller_a_cap.put_back(token, borrow); scenario.next_tx(controller_d); - let controller_d_cap = scenario.take_from_address(controller_d); + let mut controller_d_cap = scenario.take_from_address(controller_d); + let (token, borrow) = controller_d_cap.borrow(); - identity.did_doc().assert_is_member(&controller_d_cap); + identity.did_doc().assert_is_member(&token); + controller_d_cap.put_back(token, borrow); test_scenario::return_shared(identity); test_scenario::return_to_address(controller_a, controller_a_cap); @@ -536,6 +574,7 @@ module iota_identity::identity_tests { let identity = new_with_controllers( b"DID", controllers, + vec_map::empty(), 10, &clock, scenario.ctx(), @@ -544,12 +583,14 @@ module iota_identity::identity_tests { scenario.next_tx(controller_a); let mut identity = scenario.take_shared(); - let controller_b_cap = scenario.take_from_address(controller_b); + let mut controller_b_cap = scenario.take_from_address(controller_b); + let (token, borrow) = controller_b_cap.borrow(); - let proposal_id = identity.propose_new_controller(&controller_b_cap, option::none(), controller_d, 1, scenario.ctx()).destroy_some(); + let proposal_id = identity.propose_new_controller(&token, option::none(), controller_d, 1, scenario.ctx()).destroy_some(); scenario.next_tx(controller_b); - identity.execute_config_change(&controller_b_cap, proposal_id, scenario.ctx()); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + controller_b_cap.put_back(token, borrow); scenario.next_tx(controller_d); let controller_d_cap = scenario.take_from_address(controller_d); @@ -585,6 +626,7 @@ module iota_identity::identity_tests { let identity = new_with_controllers( b"DID", controllers, + vec_map::empty(), 10, &clock, scenario.ctx(), @@ -593,23 +635,28 @@ module iota_identity::identity_tests { scenario.next_tx(controller_b); let mut identity = scenario.take_shared(); - let controller_b_cap = scenario.take_from_address(controller_b); + let mut controller_b_cap = scenario.take_from_address(controller_b); + let (token, borrow) = controller_b_cap.borrow(); // Create a request to add a new controller. - let proposal_id = identity.propose_new_controller(&controller_b_cap, option::none(), controller_d, 10, scenario.ctx()).destroy_some(); + let proposal_id = identity.propose_new_controller(&token, option::none(), controller_d, 10, scenario.ctx()).destroy_some(); + controller_b_cap.put_back(token, borrow); scenario.next_tx(controller_b); - let controller_c_cap = scenario.take_from_address(controller_c); - identity.approve_proposal(&controller_c_cap, proposal_id); + let mut controller_c_cap = scenario.take_from_address(controller_c); + let (token, borrow) = controller_c_cap.borrow(); + identity.approve_proposal(&token, proposal_id); scenario.next_tx(controller_a); - identity.execute_config_change(&controller_c_cap, proposal_id, scenario.ctx()); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + controller_c_cap.put_back(token, borrow); scenario.next_tx(controller_d); - let controller_d_cap = scenario.take_from_address(controller_d); - - identity.did_doc().assert_is_member(&controller_d_cap); + let mut controller_d_cap = scenario.take_from_address(controller_d); + let (token, borrow) = controller_d_cap.borrow(); + identity.did_doc().assert_is_member(&token); + controller_d_cap.put_back(token, borrow); test_scenario::return_shared(identity); test_scenario::return_to_address(controller_b, controller_b_cap); @@ -639,6 +686,7 @@ module iota_identity::identity_tests { let second_identity = new_with_controllers( b"DID", controllers, + vec_map::empty(), 10, &clock, scenario.ctx(), @@ -647,18 +695,22 @@ module iota_identity::identity_tests { transfer::public_share_object(second_identity); scenario.next_tx(first_identity.to_address()); - let first_identity_cap = scenario.take_from_address(first_identity.to_address()); + let mut first_identity_cap = scenario.take_from_address(first_identity.to_address()); + let (token, borrow) = first_identity_cap.borrow(); let mut second_identity = scenario.take_shared(); assert!(second_identity.did_doc().controllers().contains(&first_identity_cap.id().to_inner()), 0); - second_identity.propose_new_controller(&first_identity_cap, option::none(), controller_a, 10, scenario.ctx()).destroy_none(); + second_identity.propose_new_controller(&token, option::none(), controller_a, 10, scenario.ctx()).destroy_none(); + first_identity_cap.put_back(token, borrow); scenario.next_tx(controller_a); - let controller_a_cap = scenario.take_from_address(controller_a); + let mut controller_a_cap = scenario.take_from_address(controller_a); + let (token, borrow) = controller_a_cap.borrow(); - second_identity.did_doc().assert_is_member(&controller_a_cap); + second_identity.did_doc().assert_is_member(&token); + controller_a_cap.put_back(token, borrow); test_scenario::return_shared(second_identity); test_scenario::return_to_address(controller_a, controller_a_cap); @@ -682,9 +734,11 @@ module iota_identity::identity_tests { // Propose a change for updating the did document let mut identity = scenario.take_shared(); - let cap = scenario.take_from_address(controller); + let mut cap = scenario.take_from_address(controller); + let (token, borrow) = cap.borrow(); - let _proposal_id = identity.propose_update(&cap, b"NOT DID", option::none(), &clock, scenario.ctx()); + let _proposal_id = identity.propose_update(&token, b"NOT DID", option::none(), &clock, scenario.ctx()); + cap.put_back(token, borrow); test_scenario::return_to_address(controller, cap); test_scenario::return_shared(identity); @@ -706,22 +760,28 @@ module iota_identity::identity_tests { controllers.insert(controller_a, 1); controllers.insert(controller_b, 1); - let identity = new_with_controllers(b"DID", controllers, 2, &clock, scenario.ctx()); + let identity = new_with_controllers(b"DID", controllers, vec_map::empty(), 2, &clock, scenario.ctx()); transfer::public_share_object(identity); scenario.next_tx(controller_a); let mut identity = scenario.take_shared(); - let cap = scenario.take_from_address(controller_a); - let proposal_id = identity.propose_new_controller(&cap, option::some(expiration_epoch), new_controller, 1, scenario.ctx()).destroy_some(); + let mut cap = scenario.take_from_address(controller_a); + let (token, borrow) = cap.borrow(); + let proposal_id = identity.propose_new_controller(&token, option::some(expiration_epoch), new_controller, 1, scenario.ctx()).destroy_some(); + cap.put_back(token, borrow); scenario.next_tx(controller_b); - let cap_b = scenario.take_from_address(controller_b); - identity.approve_proposal(&cap_b, proposal_id); + let mut cap_b = scenario.take_from_address(controller_b); + let (token, borrow) = cap_b.borrow(); + identity.approve_proposal(&token, proposal_id); + cap_b.put_back(token, borrow); scenario.later_epoch(100, controller_a); // this should fail! - identity.execute_config_change(&cap, proposal_id, scenario.ctx()); + let (token, borrow) = cap.borrow(); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + cap.put_back(token, borrow); test_scenario::return_to_address(controller_a, cap); test_scenario::return_to_address(controller_b, cap_b); diff --git a/identity_iota_core/packages/iota_identity/sources/migration.move b/identity_iota_core/packages/iota_identity/sources/migration.move index 566f41406..01b839ed7 100644 --- a/identity_iota_core/packages/iota_identity/sources/migration.move +++ b/identity_iota_core/packages/iota_identity/sources/migration.move @@ -73,7 +73,7 @@ module iota_identity::migration_tests { use iota_identity::migration::migrate_alias_output; use stardust::alias::{Self, Alias}; use iota_identity::migration_registry::{MigrationRegistry, init_testing}; - use iota_identity::multicontroller::ControllerCap; + use iota_identity::controller::ControllerCap; fun create_did_alias(ctx: &mut TxContext): Alias { let sender = ctx.sender(); @@ -115,13 +115,15 @@ module iota_identity::migration_tests { scenario.next_tx(controller_a); let identity = scenario.take_shared(); - let controller_a_cap = scenario.take_from_address(controller_a); + let mut controller_a_cap = scenario.take_from_address(controller_a); + let (token, borrow) = controller_a_cap.borrow(); - // Assert correct binding in migration regitry + // Assert correct binding in migration registry assert!(registry.lookup(alias_id) == identity.id().to_inner(), 0); // Assert the sender is controller - identity.did_doc().assert_is_member(&controller_a_cap); + identity.did_doc().assert_is_member(&token); + controller_a_cap.put_back(token, borrow); // assert the metadata is b"DID" let did = identity.did_doc().value(); diff --git a/identity_iota_core/packages/iota_identity/sources/multicontroller.move b/identity_iota_core/packages/iota_identity/sources/multicontroller.move index b8ae07f7e..3a49b5687 100644 --- a/identity_iota_core/packages/iota_identity/sources/multicontroller.move +++ b/identity_iota_core/packages/iota_identity/sources/multicontroller.move @@ -3,6 +3,8 @@ module iota_identity::multicontroller { use iota::{object_bag::{Self, ObjectBag}, vec_map::{Self, VecMap}, vec_set::{Self, VecSet}}; + use iota_identity::controller::{Self, DelegationToken, ControllerCap}; + use iota_identity::permissions; const EInvalidController: u64 = 0; const EControllerAlreadyVoted: u64 = 1; @@ -12,15 +14,6 @@ module iota_identity::multicontroller { const ENotVotedYet: u64 = 5; const EProposalNotFound: u64 = 6; - /// Capability that allows to access mutative APIs of a `Multicontroller`. - public struct ControllerCap has key { - id: UID, - } - - public fun id(self: &ControllerCap): &UID { - &self.id - } - /// Shares control of a value `V` with multiple entities called controllers. public struct Multicontroller has store { threshold: u64, @@ -28,33 +21,42 @@ module iota_identity::multicontroller { controlled_value: V, active_proposals: vector, proposals: ObjectBag, + revoked_tokens: VecSet, } /// Wraps a `V` in `Multicontroller`, making the tx's sender a controller with /// voting power 1. - public fun new(controlled_value: V, ctx: &mut TxContext): Multicontroller { - new_with_controller(controlled_value, ctx.sender(), ctx) + public fun new(controlled_value: V, can_delegate: bool, ctx: &mut TxContext): Multicontroller { + new_with_controller(controlled_value, ctx.sender(), can_delegate, ctx) } /// Wraps a `V` in `Multicontroller` and sends `controller` a `ControllerCap`. public fun new_with_controller( controlled_value: V, controller: address, + can_delegate: bool, ctx: &mut TxContext ): Multicontroller { let mut controllers = vec_map::empty(); controllers.insert(controller, 1); - new_with_controllers(controlled_value, controllers, 1, ctx) + if (can_delegate) { + new_with_controllers(controlled_value, vec_map::empty(), controllers, 1, ctx) + } else { + new_with_controllers(controlled_value, controllers, vec_map::empty(), 1, ctx) + } } /// Wraps a `V` in `Multicontroller`, settings `threshold` as the threshold, /// and using `controllers` to set controllers: i.e. each `(recipient, voting power)` /// in `controllers` results in `recipient` obtaining a `ControllerCap` with the /// specified voting power. + /// Controllers that are able to delegate their access, should be passed through + /// `controllers_that_can_delegate` parameter. public fun new_with_controllers( controlled_value: V, controllers: VecMap, + controllers_that_can_delegate: VecMap, threshold: u64, ctx: &mut TxContext, ): Multicontroller { @@ -64,10 +66,21 @@ module iota_identity::multicontroller { let addr = addrs.pop_back(); let vp = vps.pop_back(); - let cap = ControllerCap { id: object::new(ctx) }; - controllers.insert(cap.id.to_inner(), vp); + let cap = controller::new(false, ctx); + controllers.insert(cap.id().to_inner(), vp); + + cap.transfer(addr) + }; + + let (mut addrs, mut vps) = controllers_that_can_delegate.into_keys_values(); + while(!addrs.is_empty()) { + let addr = addrs.pop_back(); + let vp = vps.pop_back(); + + let cap = controller::new(true, ctx); + controllers.insert(cap.id().to_inner(), vp); - transfer::transfer(cap, addr); + cap.transfer(addr) }; let mut multi = Multicontroller { @@ -76,6 +89,7 @@ module iota_identity::multicontroller { threshold, active_proposals: vector[], proposals: object_bag::new(ctx), + revoked_tokens: vec_set::empty(), }; multi.set_threshold(threshold); @@ -102,7 +116,7 @@ module iota_identity::multicontroller { } } - /// Strucure that encapsulate the kind of change that will be performed + /// Structure that encapsulate the kind of change that will be performed /// when a proposal is carried out. public struct Action { inner: T, @@ -124,26 +138,28 @@ module iota_identity::multicontroller { &mut action.inner } - public(package) fun assert_is_member(multi: &Multicontroller, cap: &ControllerCap) { - assert!(multi.controllers.contains(&cap.id.to_inner()), EInvalidController); + public(package) fun assert_is_member(multi: &Multicontroller, cap: &DelegationToken) { + assert!(multi.controllers.contains(&cap.controller()), EInvalidController); } /// Creates a new proposal for `Multicontroller` `multi`. public fun create_proposal( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, action: T, expiration_epoch: Option, ctx: &mut TxContext, ): ID { multi.assert_is_member(cap); - let cap_id = cap.id.to_inner(); + cap.assert_has_permission(permissions::can_create_proposal()); + + let cap_id = cap.controller(); let voting_power = multi.voting_power(cap_id); let proposal = Proposal { id: object::new(ctx), votes: voting_power, - voters: vec_set::singleton(cap.id.to_inner()), + voters: vec_set::singleton(cap_id), expiration_epoch, action, }; @@ -157,11 +173,13 @@ module iota_identity::multicontroller { /// Approves an active `Proposal` in `multi`. public fun approve_proposal( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ) { multi.assert_is_member(cap); - let cap_id = cap.id.to_inner(); + cap.assert_has_permission(permissions::can_approve_proposal()); + + let cap_id = cap.controller(); let voting_power = multi.voting_power(cap_id); let proposal = multi.proposals.borrow_mut>(proposal_id); @@ -176,11 +194,12 @@ module iota_identity::multicontroller { /// This call fails if `multi`'s threshold has not been reached. public fun execute_proposal( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext, ): Action { multi.assert_is_member(cap); + cap.assert_has_permission(permissions::can_execute_proposal()); let proposal = multi.proposals.remove>(proposal_id); assert!(proposal.votes >= multi.threshold, EThresholdNotReached); @@ -208,10 +227,12 @@ module iota_identity::multicontroller { /// `proposal_id`. public fun remove_approval( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ) { - let cap_id = cap.id.to_inner(); + cap.assert_has_permission(permissions::can_remove_approval()); + + let cap_id = cap.controller(); let vp = multi.voting_power(cap_id); let proposal = multi.proposals.borrow_mut>(proposal_id); @@ -257,6 +278,37 @@ module iota_identity::multicontroller { sum } + /// Revoke the `DelegationToken` with `ID` `deny_id`. Only controllers can perform this operation. + public fun revoke_token(self: &mut Multicontroller, cap: &ControllerCap, deny_id: ID) { + assert!(self.controllers.contains(object::borrow_id(cap)), EInvalidController); + self.revoked_tokens.insert(deny_id); + } + + /// Un-revoke a `DelegationToken`. + public fun unrevoke_token(self: &mut Multicontroller, cap: &ControllerCap, token_id: ID) { + assert!(self.controllers.contains(object::borrow_id(cap)), EInvalidController); + self.revoked_tokens.remove(&token_id); + } + + /// Destroys a `ControllerCap`. Can only be used after a controller has been removed from + /// the controller committee. + public fun destroy_controller_cap(self: &Multicontroller, cap: ControllerCap) { + assert!(self.controllers.contains(&cap.id().to_inner()), EInvalidController); + + cap.delete(); + } + + /// Destroys a `DelegationToken`. + public fun destroy_delegation_token(self: &mut Multicontroller, token: DelegationToken) { + let token_id = object::id(&token); + let is_revoked = self.revoked_tokens.contains(&token_id); + if (is_revoked) { + self.revoked_tokens.remove(&token_id); + }; + + token.delete(); + } + public(package) fun unpack_action(action: Action): T { let Action { inner } = action; inner @@ -271,9 +323,9 @@ module iota_identity::multicontroller { let mut i = 0; while (i < to_add.size()) { let (addr, vp) = to_add.get_entry_by_idx(i); - let new_cap = ControllerCap { id: object::new(ctx) }; - multi.controllers.insert(new_cap.id.to_inner(), *vp); - transfer::transfer(new_cap, *addr); + let new_cap = controller::new(false, ctx); + multi.controllers.insert(new_cap.id().to_inner(), *vp); + new_cap.transfer(*addr); i = i + 1; } } diff --git a/identity_iota_core/packages/iota_identity/sources/permissions.move b/identity_iota_core/packages/iota_identity/sources/permissions.move new file mode 100644 index 000000000..152e7782a --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/permissions.move @@ -0,0 +1,20 @@ +module iota_identity::permissions { + /// Permission that enables a controller's delegate to create proposals. + const CAN_CREATE_PROPOSAL: u32 = 0x1; + /// Permission that enables a controller's delegate to approve proposals. + const CAN_APPROVE_PROPOSAL: u32 = 0x1 << 1; + /// Permission that enables a controller's delegate to execute proposals. + const CAN_EXECUTE_PROPOSAL: u32 = 0x1 << 2; + /// Permission that enables a controller's delegate to delete proposals. + const CAN_DELETE_PROPOSAL: u32 = 0x1 << 3; + /// Permission that enables a controller's delegate to remove a proposal's approval. + const CAN_REMOVE_APPROVAL: u32 = 0x1 << 4; + const ALL_PERMISSIONS: u32 = 0xFFFFFFFF; + + public fun can_create_proposal(): u32 { CAN_CREATE_PROPOSAL } + public fun can_approve_proposal(): u32 { CAN_APPROVE_PROPOSAL } + public fun can_execute_proposal(): u32 { CAN_EXECUTE_PROPOSAL } + public fun can_delete_proposal(): u32 { CAN_DELETE_PROPOSAL } + public fun can_remove_approval(): u32 { CAN_REMOVE_APPROVAL } + public fun all(): u32 { ALL_PERMISSIONS } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move index 4195e3461..dca212e8c 100644 --- a/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move +++ b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module iota_identity::borrow_proposal { - use iota_identity::{multicontroller::{Multicontroller, Action, ControllerCap}}; + use iota_identity::multicontroller::{Multicontroller, Action}; + use iota_identity::controller::DelegationToken; use iota::transfer::Receiving; const EInvalidObject: u64 = 0; @@ -19,7 +20,7 @@ module iota_identity::borrow_proposal { /// Propose the borrowing of a set of assets owned by this multicontroller. public fun propose_borrow( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, objects: vector, owner: address, diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/config.move b/identity_iota_core/packages/iota_identity/sources/proposals/config.move index 80755b2af..2758373a8 100644 --- a/identity_iota_core/packages/iota_identity/sources/proposals/config.move +++ b/identity_iota_core/packages/iota_identity/sources/proposals/config.move @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module iota_identity::config_proposal { - use iota_identity::multicontroller::{ControllerCap, Multicontroller}; + use iota_identity::multicontroller::Multicontroller; + use iota_identity::controller::DelegationToken; use iota::vec_map::VecMap; const ENotMember: u64 = 0; @@ -17,7 +18,7 @@ module iota_identity::config_proposal { public fun propose_modify( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, mut threshold: Option, controllers_to_add: VecMap, @@ -87,7 +88,7 @@ module iota_identity::config_proposal { public fun execute_modify( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext, ) { diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move b/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move index d16f8c36c..cd0a82440 100644 --- a/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move +++ b/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module iota_identity::transfer_proposal { - use iota_identity::{multicontroller::{Multicontroller, Action, ControllerCap}}; + use iota_identity::multicontroller::{Multicontroller, Action}; + use iota_identity::controller::DelegationToken; use iota::transfer::Receiving; const EDifferentLength: u64 = 0; @@ -16,7 +17,7 @@ module iota_identity::transfer_proposal { public fun propose_send( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, objects: vector, recipients: vector
, diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/value.move b/identity_iota_core/packages/iota_identity/sources/proposals/value.move index 468971af6..1788ed55d 100644 --- a/identity_iota_core/packages/iota_identity/sources/proposals/value.move +++ b/identity_iota_core/packages/iota_identity/sources/proposals/value.move @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module iota_identity::update_value_proposal { - use iota_identity::multicontroller::{Multicontroller, ControllerCap}; + use iota_identity::multicontroller::Multicontroller; + use iota_identity::controller::DelegationToken; public struct UpdateValue has store { new_value: V, @@ -10,7 +11,7 @@ module iota_identity::update_value_proposal { public fun propose_update( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, new_value: V, expiration: Option, ctx: &mut TxContext, @@ -21,10 +22,12 @@ module iota_identity::update_value_proposal { public fun execute_update( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext, ) { + + let action = multi.execute_proposal(cap, proposal_id, ctx); let UpdateValue { new_value } = action.unpack_action(); diff --git a/identity_iota_core/src/rebased/migration/identity.rs b/identity_iota_core/src/rebased/migration/identity.rs index 4b7fa47f8..938c03ded 100644 --- a/identity_iota_core/src/rebased/migration/identity.rs +++ b/identity_iota_core/src/rebased/migration/identity.rs @@ -144,7 +144,7 @@ impl OnChainIdentity { } pub(crate) async fn get_controller_cap(&self, client: &IdentityClient) -> Result { - let controller_cap_tag = StructTag::from_str(&format!("{}::multicontroller::ControllerCap", client.package_id())) + let controller_cap_tag = StructTag::from_str(&format!("{}::controller::ControllerCap", client.package_id())) .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; client .find_owned_ref(controller_cap_tag, |obj_data| { @@ -170,7 +170,7 @@ impl OnChainIdentity { } /// Sends assets owned by this [`OnChainIdentity`] to other addresses. - pub fn send_assets(&mut self) -> ProposalBuilder { + pub fn send_assets(&mut self) -> ProposalBuilder<'_, SendAction> { ProposalBuilder::new(self, SendAction::default()) } @@ -178,7 +178,7 @@ impl OnChainIdentity { /// # Notes /// Make sure to call [`super::Proposal::with_intent`] before executing the proposal. /// Failing to do so will make [`crate::proposals::ProposalT::execute`] return an error. - pub fn borrow_assets(&mut self) -> ProposalBuilder { + pub fn borrow_assets(&mut self) -> ProposalBuilder<'_, BorrowAction> { ProposalBuilder::new(self, BorrowAction::default()) } diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/borrow_asset.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/borrow_asset.rs index 2e5268e5e..95837f3ec 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/borrow_asset.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/borrow_asset.rs @@ -27,6 +27,7 @@ pub(crate) fn propose_borrow( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; let objects_arg = ptb.pure(objects)?; @@ -36,9 +37,11 @@ pub(crate) fn propose_borrow( ident_str!("identity").into(), ident_str!("propose_borrow").into(), vec![], - vec![identity_arg, cap_arg, exp_arg, objects_arg], + vec![identity_arg, delegation_token, exp_arg, objects_arg], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } @@ -56,6 +59,7 @@ where let mut ptb = ProgrammableTransactionBuilder::new(); let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow)= utils::get_controller_delegation(&mut ptb, controller_cap, package); let proposal_id = ptb.pure(proposal_id)?; // Get the proposal's action as argument. @@ -64,9 +68,11 @@ where ident_str!("identity").into(), ident_str!("execute_proposal").into(), vec![BorrowAction::move_type(package)], - vec![identity, controller_cap, proposal_id], + vec![identity, delegation_token, proposal_id], ); + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + // Borrow all the objects specified in the action. let obj_arg_map = objects .into_iter() diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/config.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/config.rs index 26a4d8d94..60fb207c5 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/config.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/config.rs @@ -8,7 +8,6 @@ use iota_sdk::rpc_types::OwnedObjectRef; use iota_sdk::types::base_types::IotaAddress; use iota_sdk::types::base_types::ObjectID; use iota_sdk::types::base_types::ObjectRef; -use iota_sdk::types::object::Owner; use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; use iota_sdk::types::transaction::ObjectArg; use iota_sdk::types::transaction::ProgrammableTransaction; @@ -62,6 +61,7 @@ where }; let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(controller_cap))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); let expiration = utils::option_to_move(expiration, &mut ptb, package)?; let threshold = utils::option_to_move(threshold, &mut ptb, package)?; let controllers_to_remove = ptb.pure(controllers_to_remove)?; @@ -73,7 +73,7 @@ where vec![], vec![ identity, - controller_cap, + delegation_token, expiration, threshold, controllers_to_add, @@ -82,6 +82,8 @@ where ], ); + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + Ok(ptb.finish()) } @@ -93,23 +95,19 @@ pub(crate) fn execute_config_change( ) -> anyhow::Result { let mut ptb = ProgrammableTransactionBuilder::new(); - let Owner::Shared { initial_shared_version } = identity.owner else { - anyhow::bail!("identity \"{}\" is a not shared object", identity.reference.object_id); - }; - let identity = ptb.obj(ObjectArg::SharedObject { - id: identity.reference.object_id, - initial_shared_version, - mutable: true, - })?; + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(controller_cap))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); let proposal_id = ptb.pure(proposal_id)?; ptb.programmable_move_call( package, ident_str!("identity").into(), ident_str!("execute_config_change").into(), vec![], - vec![identity, controller_cap, proposal_id], + vec![identity, delegation_token, proposal_id], ); + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + Ok(ptb.finish()) } diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/create.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/create.rs index 32be404e0..f36ce0b83 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/create.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/create.rs @@ -67,6 +67,14 @@ where vec![ids, vps], ) }; + + let controllers_that_can_delegate = ptb.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("vec_map").into(), + ident_str!("empty").into(), + vec![TypeTag::Address, TypeTag::U64], + vec![], + ); let doc_arg = ptb.pure(did_doc).map_err(|e| Error::InvalidArgument(e.to_string()))?; let threshold_arg = ptb.pure(threshold).map_err(|e| Error::InvalidArgument(e.to_string()))?; let clock = utils::get_clock_ref(&mut ptb); @@ -77,7 +85,7 @@ where module: ident_str!("identity").into(), function: ident_str!("new_with_controllers").into(), type_arguments: vec![], - arguments: vec![doc_arg, controllers, threshold_arg, clock], + arguments: vec![doc_arg, controllers, controllers_that_can_delegate, threshold_arg, clock], }))); // Share the resulting identity. diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/deactivate.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/deactivate.rs index bb1c7767f..b1cb785e3 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/deactivate.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/deactivate.rs @@ -19,6 +19,7 @@ pub(crate) fn propose_deactivation( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; let clock = utils::get_clock_ref(&mut ptb); @@ -28,9 +29,11 @@ pub(crate) fn propose_deactivation( ident_str!("identity").into(), ident_str!("propose_deactivation").into(), vec![], - vec![identity_arg, cap_arg, exp_arg, clock], + vec![identity_arg, delegation_token, exp_arg, clock], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } @@ -42,6 +45,7 @@ pub(crate) fn execute_deactivation( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let proposal_id = ptb.pure(proposal_id)?; let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let clock = utils::get_clock_ref(&mut ptb); @@ -51,8 +55,10 @@ pub(crate) fn execute_deactivation( ident_str!("identity").into(), ident_str!("execute_deactivation").into(), vec![], - vec![identity_arg, cap_arg, proposal_id, clock], + vec![identity_arg, delegation_token, proposal_id, clock], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/proposal.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/proposal.rs index 4335bc194..bb04d73a1 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/proposal.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/proposal.rs @@ -1,12 +1,12 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use crate::rebased::sui::move_calls::utils; use crate::rebased::utils::MoveType; use crate::rebased::Error; use iota_sdk::rpc_types::OwnedObjectRef; use iota_sdk::types::base_types::ObjectID; use iota_sdk::types::base_types::ObjectRef; -use iota_sdk::types::object::Owner; use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; use iota_sdk::types::transaction::ObjectArg; use iota_sdk::types::transaction::ProgrammableTransaction; @@ -19,22 +19,12 @@ pub(crate) fn approve( package: ObjectID, ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); - let Owner::Shared { initial_shared_version } = identity.owner else { - return Err(Error::TransactionBuildingFailed(format!( - "Identity \"{}\" is not a shared object", - identity.object_id() - ))); - }; - let identity = ptb - .obj(ObjectArg::SharedObject { - id: identity.object_id(), - initial_shared_version, - mutable: true, - }) - .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; let controller_cap = ptb .obj(ObjectArg::ImmOrOwnedObject(controller_cap)) .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); let proposal_id = ptb .pure(proposal_id) .map_err(|e| Error::InvalidArgument(e.to_string()))?; @@ -44,8 +34,10 @@ pub(crate) fn approve( ident_str!("identity").into(), ident_str!("approve_proposal").into(), vec![T::move_type(package)], - vec![identity, controller_cap, proposal_id], + vec![identity, delegation_token, proposal_id], ); + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + Ok(ptb.finish()) } diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/send_asset.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/send_asset.rs index 19c1a9f75..83348a867 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/send_asset.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/send_asset.rs @@ -15,6 +15,8 @@ use crate::rebased::proposals::SendAction; use crate::rebased::sui::move_calls; use crate::rebased::utils::MoveType; +use self::move_calls::utils; + pub(crate) fn propose_send( identity: OwnedObjectRef, capability: ObjectRef, @@ -24,6 +26,7 @@ pub(crate) fn propose_send( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = move_calls::utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let identity_arg = move_calls::utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let exp_arg = move_calls::utils::option_to_move(expiration, &mut ptb, package_id)?; let (objects, recipients) = { @@ -39,9 +42,11 @@ pub(crate) fn propose_send( ident_str!("identity").into(), ident_str!("propose_send").into(), vec![], - vec![identity_arg, cap_arg, exp_arg, objects, recipients], + vec![identity_arg, delegation_token, exp_arg, objects, recipients], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } @@ -55,6 +60,7 @@ pub(crate) fn execute_send( let mut ptb = ProgrammableTransactionBuilder::new(); let identity = move_calls::utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = move_calls::utils::get_controller_delegation(&mut ptb, controller_cap, package); let proposal_id = ptb.pure(proposal_id)?; // Get the proposal's action as argument. @@ -63,9 +69,11 @@ pub(crate) fn execute_send( ident_str!("identity").into(), ident_str!("execute_proposal").into(), vec![SendAction::move_type(package)], - vec![identity, controller_cap, proposal_id], + vec![identity, delegation_token, proposal_id], ); + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + // Send each object in this send action. // Traversing the map in reverse reduces the number of operations on the move side. for (obj, obj_type) in objects.into_iter().rev() { diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/update.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/update.rs index 93603b1da..9ec751425 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/update.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/update.rs @@ -20,6 +20,7 @@ pub(crate) fn propose_update( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token , borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; let doc_arg = ptb.pure(did_doc.as_ref())?; @@ -30,9 +31,11 @@ pub(crate) fn propose_update( ident_str!("identity").into(), ident_str!("propose_update").into(), vec![], - vec![identity_arg, cap_arg, doc_arg, exp_arg, clock], + vec![identity_arg, delegation_token, doc_arg, exp_arg, clock], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } @@ -44,6 +47,7 @@ pub(crate) fn execute_update( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow)= utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let proposal_id = ptb.pure(proposal_id)?; let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let clock = utils::get_clock_ref(&mut ptb); @@ -53,8 +57,10 @@ pub(crate) fn execute_update( ident_str!("identity").into(), ident_str!("execute_update").into(), vec![], - vec![identity_arg, cap_arg, proposal_id, clock], + vec![identity_arg, delegation_token, proposal_id, clock], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } diff --git a/identity_iota_core/src/rebased/sui/move_calls/utils.rs b/identity_iota_core/src/rebased/sui/move_calls/utils.rs index c35ee6552..bb8b66525 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/utils.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/utils.rs @@ -27,6 +27,36 @@ pub(crate) fn get_clock_ref(ptb: &mut Ptb) -> Argument { .expect("network has a singleton clock instantiated") } +pub(crate) fn get_controller_delegation(ptb: &mut Ptb, controller_cap: Argument, package: ObjectID) -> (Argument, Argument) { + let Argument::Result(idx) = ptb.programmable_move_call( + package, + ident_str!("controller").into(), + ident_str!("borrow").into(), + vec![], + vec![controller_cap], + ) else { + unreachable!("making move calls always return a result variant"); + }; + + (Argument::NestedResult(idx, 0), Argument::NestedResult(idx, 1)) +} + +pub(crate) fn put_back_delegation_token( + ptb: &mut Ptb, + controller_cap: Argument, + delegation_token: Argument, + borrow: Argument, + package: ObjectID, +) { + ptb.programmable_move_call( + package, + ident_str!("controller").into(), + ident_str!("put_back").into(), + vec![], + vec![controller_cap, delegation_token, borrow] + ); +} + pub(crate) fn owned_ref_to_shared_object_arg( owned_ref: OwnedObjectRef, ptb: &mut Ptb, diff --git a/identity_iota_core/tests/e2e/identity.rs b/identity_iota_core/tests/e2e/identity.rs index 7075128d2..2bd6236ad 100644 --- a/identity_iota_core/tests/e2e/identity.rs +++ b/identity_iota_core/tests/e2e/identity.rs @@ -158,7 +158,7 @@ async fn adding_controller_works() -> anyhow::Result<()> { let cap = bob_client .find_owned_ref( - StructTag::from_str(&format!("{}::multicontroller::ControllerCap", test_client.package_id())).unwrap(), + StructTag::from_str(&format!("{}::controller::ControllerCap", test_client.package_id())).unwrap(), |_| true, ) .await?; From 3171d71c877201a0d43fa7f81c19b4096ab3042a Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 27 Nov 2024 09:27:31 +0100 Subject: [PATCH 2/5] borrow controller cap proposal --- .../iota_identity/sources/identity.move | 36 ++- .../sources/proposals/borrow.move | 4 +- .../sources/proposals/controller.move | 56 +++++ .../sources/proposals/transfer.move | 4 +- .../src/rebased/proposals/borrow.rs | 48 ++-- .../src/rebased/proposals/controller.rs | 219 ++++++++++++++++++ .../src/rebased/proposals/mod.rs | 9 + .../identity/controller_execution.rs | 95 ++++++++ .../rebased/sui/move_calls/identity/mod.rs | 2 + 9 files changed, 432 insertions(+), 41 deletions(-) create mode 100644 identity_iota_core/packages/iota_identity/sources/proposals/controller.move create mode 100644 identity_iota_core/src/rebased/proposals/controller.rs create mode 100644 identity_iota_core/src/rebased/sui/move_calls/identity/controller_execution.rs diff --git a/identity_iota_core/packages/iota_identity/sources/identity.move b/identity_iota_core/packages/iota_identity/sources/identity.move index 592c624ea..69a38f72a 100644 --- a/identity_iota_core/packages/iota_identity/sources/identity.move +++ b/identity_iota_core/packages/iota_identity/sources/identity.move @@ -15,6 +15,7 @@ module iota_identity::identity { transfer_proposal::{Self, Send}, borrow_proposal::{Self, Borrow}, did_deactivation_proposal::{Self, DidDeactivation}, + controller_proposal::{Self, ControllerExecution}, }; const ENotADidDocument: u64 = 0; @@ -175,6 +176,33 @@ module iota_identity::identity { self.updated = clock.timestamp_ms(); } + /// Creates a new `ControllerExecution` proposal. + public fun propose_controller_execution( + self: &mut Identity, + cap: &DelegationToken, + controller_cap_id: ID, + expiration: Option, + ctx: &mut TxContext, + ): ID { + let identity_address = self.id().to_address(); + self.did_doc.create_proposal( + cap, + controller_proposal::new(controller_cap_id, identity_address), + expiration, + ctx, + ) + } + + /// Borrow the identity-owned controller cap specified in `ControllerExecution`. + /// The borrowed cap must be put back by calling `controller_proposal::put_back`. + public fun borrow_controller_cap( + self: &mut Identity, + action: &mut Action, + receiving: Receiving, + ): ControllerCap { + controller_proposal::receive(action, &mut self.id, receiving) + } + /// Proposes an update to the DID Document contained in this `Identity`. /// This function can update the DID Document right away if `cap` has /// enough voting power. @@ -282,7 +310,7 @@ module iota_identity::identity { objects: vector, recipients: vector
, ctx: &mut TxContext, - ) { + ): ID { transfer_proposal::propose_send( &mut self.did_doc, cap, @@ -290,7 +318,7 @@ module iota_identity::identity { objects, recipients, ctx - ); + ) } /// Sends one object among the one specified in a `Send` proposal. @@ -310,7 +338,7 @@ module iota_identity::identity { expiration: Option, objects: vector, ctx: &mut TxContext, - ) { + ): ID { let identity_address = self.id().to_address(); borrow_proposal::propose_borrow( &mut self.did_doc, @@ -319,7 +347,7 @@ module iota_identity::identity { objects, identity_address, ctx, - ); + ) } /// Takes one of the borrowed assets. diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move index dca212e8c..ac82cc9a7 100644 --- a/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move +++ b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move @@ -25,10 +25,10 @@ module iota_identity::borrow_proposal { objects: vector, owner: address, ctx: &mut TxContext, - ) { + ): ID { let action = Borrow { objects, objects_to_return: vector::empty(), owner }; - multi.create_proposal(cap, action,expiration, ctx); + multi.create_proposal(cap, action, expiration, ctx) } /// Borrows an asset from this action. This function will fail if: diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/controller.move b/identity_iota_core/packages/iota_identity/sources/proposals/controller.move new file mode 100644 index 000000000..f83b52d3f --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/controller.move @@ -0,0 +1,56 @@ +module iota_identity::controller_proposal { + use iota::transfer::Receiving; + use iota_identity::controller::{Self, ControllerCap}; + use iota_identity::multicontroller::Action; + + /// The received `ControllerCap` does not match the one + /// specified in the `ControllerExecution` action. + const EControllerCapMismatch: u64 = 0; + /// The provided `UID` is not the `UID` of the `Identity` + /// specified in the action. + const EInvalidIdentityUID: u64 = 1; + + /// Borrow a given `ControllerCap` from an `Identity` for + /// a single transaction. + public struct ControllerExecution has store { + /// ID of the `ControllerCap` to borrow. + controller_cap: ID, + /// The address of the `Identity` that owns + /// the `ControllerCap` we are borrowing. + identity: address, + } + + /// Returns a new `ControllerExecution` that - in a Proposal - allows whoever + /// executes it to receive `identity`'s `ControllerCap` (the one that has ID `controller_cap`) + /// for the duration of a single transaction. + public fun new(controller_cap: ID, identity: address): ControllerExecution { + ControllerExecution { + controller_cap, + identity, + } + } + + /// Returns the `ControllerCap` specified in this action. + public fun receive( + self: &mut Action, + identity: &mut UID, + cap: Receiving + ): ControllerCap { + assert!(identity.to_address() == self.borrow().identity, EInvalidIdentityUID); + assert!(cap.receiving_object_id() == self.borrow().controller_cap, EControllerCapMismatch); + + controller::receive(identity, cap) + } + + /// Consumes a `ControllerExecution` action by returning the borrowed `ControllerCap` + /// to the corresponding `Identity`. + public fun put_back( + action: Action, + cap: ControllerCap, + ) { + let ControllerExecution { identity, controller_cap } = action.unwrap(); + assert!(object::id(&cap) == controller_cap, EControllerCapMismatch); + + cap.transfer(identity); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move b/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move index cd0a82440..b87b14eca 100644 --- a/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move +++ b/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move @@ -22,11 +22,11 @@ module iota_identity::transfer_proposal { objects: vector, recipients: vector
, ctx: &mut TxContext, - ) { + ): ID { assert!(objects.length() == recipients.length(), EDifferentLength); let action = Send { objects, recipients }; - multi.create_proposal(cap, action,expiration, ctx); + multi.create_proposal(cap, action,expiration, ctx) } public fun send( diff --git a/identity_iota_core/src/rebased/proposals/borrow.rs b/identity_iota_core/src/rebased/proposals/borrow.rs index 60d309e7b..471e65a39 100644 --- a/identity_iota_core/src/rebased/proposals/borrow.rs +++ b/identity_iota_core/src/rebased/proposals/borrow.rs @@ -29,6 +29,7 @@ use super::ExecuteProposalTx; use super::OnChainIdentity; use super::ProposalBuilder; use super::ProposalT; +use super::UserDrivenTx; pub(crate) type IntentFn = Box) + Send>; @@ -36,8 +37,6 @@ pub(crate) type IntentFn = Box, - #[serde(skip)] - intent: Option, } /// A [`BorrowAction`] coupled with a user-provided function to describe how @@ -89,17 +88,6 @@ impl<'i> ProposalBuilder<'i, BorrowAction> { } } -impl Proposal { - /// Defines how the borrowed assets should be used. - pub fn with_intent(mut self, intent_fn: F) -> Self - where - F: FnOnce(&mut Ptb, &HashMap) + Send + 'static, - { - self.action.intent = Some(Box::new(intent_fn)); - self - } -} - #[async_trait] impl ProposalT for Proposal { type Action = BorrowAction; @@ -141,17 +129,17 @@ impl ProposalT for Proposal { self, identity: &'i mut OnChainIdentity, _: &IdentityClient, - ) -> Result, Error> + ) -> Result, Error> where S: Signer + Sync, { let proposal_id = self.id(); let borrow_action = self.into_action(); - Ok(ExecuteBorrowTx { + Ok(UserDrivenTx { identity, proposal_id, - borrow_action, + action: borrow_action, }) } @@ -160,37 +148,31 @@ impl ProposalT for Proposal { } } -pub struct ExecuteBorrowTx<'i, B> { - identity: &'i mut OnChainIdentity, - borrow_action: B, - proposal_id: ObjectID, -} - -impl<'i> ExecuteBorrowTx<'i, BorrowAction> { +impl<'i> UserDrivenTx<'i, BorrowAction> { /// Defines how the borrowed assets should be used. - pub fn with_intent(self, intent_fn: F) -> ExecuteBorrowTx<'i, BorrowActionWithIntent> + pub fn with_intent(self, intent_fn: F) -> UserDrivenTx<'i, BorrowActionWithIntent> where F: FnOnce(&mut Ptb, &HashMap), { - let ExecuteBorrowTx { + let UserDrivenTx { identity, - borrow_action, + action, proposal_id, } = self; - ExecuteBorrowTx { + UserDrivenTx { identity, proposal_id, - borrow_action: BorrowActionWithIntent { - action: borrow_action, + action: BorrowActionWithIntent { + action, intent_fn, }, } } } -impl<'i> ProtoTransaction for ExecuteBorrowTx<'i, BorrowAction> { +impl<'i> ProtoTransaction for UserDrivenTx<'i, BorrowAction> { type Input = IntentFn; - type Tx = ExecuteBorrowTx<'i, BorrowActionWithIntent>; + type Tx = UserDrivenTx<'i, BorrowActionWithIntent>; fn with(self, input: Self::Input) -> Self::Tx { self.with_intent(input) @@ -198,7 +180,7 @@ impl<'i> ProtoTransaction for ExecuteBorrowTx<'i, BorrowAction> { } #[async_trait] -impl<'i, F> Transaction for ExecuteBorrowTx<'i, BorrowActionWithIntent> +impl<'i, F> Transaction for UserDrivenTx<'i, BorrowActionWithIntent> where F: FnOnce(&mut Ptb, &HashMap) + Send, { @@ -213,7 +195,7 @@ where { let Self { identity, - borrow_action, + action: borrow_action, proposal_id, } = self; let identity_ref = client diff --git a/identity_iota_core/src/rebased/proposals/controller.rs b/identity_iota_core/src/rebased/proposals/controller.rs new file mode 100644 index 000000000..ec71fc0ec --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/controller.rs @@ -0,0 +1,219 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::migration::Proposal; +use crate::rebased::sui::move_calls; +use crate::rebased::transaction::ProtoTransaction; +use crate::rebased::transaction::Transaction; +use crate::rebased::transaction::TransactionOutput; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; +use async_trait::async_trait; +use iota_sdk::rpc_types::IotaObjectRef; +use iota_sdk::rpc_types::IotaTransactionBlockResponse; +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; +use iota_sdk::types::transaction::Argument; +use iota_sdk::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::OnChainIdentity; +use super::ProposalT; +use super::UserDrivenTx; + +pub(crate) type IntentFn = Box; + +/// Borrow an [`OnChainIdentity`]'s controller capability to exert control on +/// a sub-owned identity. +#[derive(Debug, Deserialize, Serialize)] +pub struct ControllerExecution { + controller_cap: ObjectID, + identity: IotaAddress, +} + +/// A [`ControllerExecution`] action coupled with a user-provided function to describe how +/// the borrowed identity's controller capability will be used. +pub struct ControllerExecutionWithIntent +where + F: FnOnce(&mut Ptb, &Argument), +{ + action: ControllerExecution, + intent_fn: F, +} + +impl ControllerExecution { + /// Creates a new [`ControllerExecution`] action, allowing a controller of `identity` to + /// borrow `identity`'s controller cap for a transaction. + pub fn new(controller_cap: ObjectID, identity: &OnChainIdentity) -> Self { + Self { + controller_cap, + identity: identity.id().into(), + } + } +} + +impl MoveType for ControllerExecution { + fn move_type(package: ObjectID) -> TypeTag { + use std::str::FromStr; + + TypeTag::from_str(&format!("{package}::controller_proposal::ControllerExecution")).expect("valid move type") + } +} + +#[async_trait] +impl ProposalT for Proposal { + type Action = ControllerExecution; + type Output = (); + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + let tx = move_calls::identity::propose_controller_execution( + identity_ref, + controller_cap_ref, + action.controller_cap, + expiration, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + // Borrow proposals cannot be chain-executed as they have to be driven. + chained_execution: false, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + _: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let proposal_id = self.id(); + let controller_execution_action = self.into_action(); + + Ok(UserDrivenTx { + identity, + proposal_id, + action: controller_execution_action, + }) + } + + fn parse_tx_effects(_tx_response: &IotaTransactionBlockResponse) -> Result { + Ok(()) + } +} + +impl<'i> UserDrivenTx<'i, ControllerExecution> { + /// Defines how the borrowed assets should be used. + pub fn with_intent(self, intent_fn: F) -> UserDrivenTx<'i, ControllerExecutionWithIntent> + where + F: FnOnce(&mut Ptb, &Argument), + { + let UserDrivenTx { + identity, + action, + proposal_id, + } = self; + UserDrivenTx { + identity, + proposal_id, + action: ControllerExecutionWithIntent { action, intent_fn }, + } + } +} + +impl<'i> ProtoTransaction for UserDrivenTx<'i, ControllerExecution> { + type Input = IntentFn; + type Tx = UserDrivenTx<'i, ControllerExecutionWithIntent>; + + fn with(self, input: Self::Input) -> Self::Tx { + self.with_intent(input) + } +} + +#[async_trait] +impl<'i, F> Transaction for UserDrivenTx<'i, ControllerExecutionWithIntent> +where + F: FnOnce(&mut Ptb, &Argument) + Send, +{ + type Output = (); + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let Self { + identity, + action, + proposal_id, + } = self; + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + let borrowing_cap_id = action.action.controller_cap; + let borrowing_controller_cap_ref = client + .get_object_ref_by_id(borrowing_cap_id) + .await? + .map(|OwnedObjectRef { reference, .. }| { + let IotaObjectRef { + object_id, + version, + digest, + } = reference; + (object_id, version, digest) + }) + .ok_or_else(|| Error::ObjectLookup(format!("object {borrowing_cap_id} doesn't exist")))?; + + let tx = move_calls::identity::execute_controller_execution( + identity_ref, + controller_cap_ref, + proposal_id, + borrowing_controller_cap_ref, + action.intent_fn, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + ExecuteProposalTx { + identity, + tx, + _action: PhantomData::, + } + .execute_with_opt_gas(gas_budget, client) + .await + } +} diff --git a/identity_iota_core/src/rebased/proposals/mod.rs b/identity_iota_core/src/rebased/proposals/mod.rs index 1869b76c1..5cbafa4e5 100644 --- a/identity_iota_core/src/rebased/proposals/mod.rs +++ b/identity_iota_core/src/rebased/proposals/mod.rs @@ -6,6 +6,7 @@ mod config_change; mod deactive_did; mod send; mod update_did_doc; +mod controller; use std::marker::PhantomData; use std::ops::Deref; @@ -20,6 +21,7 @@ use async_trait::async_trait; pub use borrow::*; pub use config_change::*; pub use deactive_did::*; +pub use controller::*; use iota_sdk::rpc_types::IotaExecutionStatus; use iota_sdk::rpc_types::IotaObjectData; use iota_sdk::rpc_types::IotaObjectDataOptions; @@ -331,3 +333,10 @@ async fn obj_ref_and_type_for_id( Ok((obj_ref, obj_type)) } + +/// A transaction that requires user input in order to be executed. +pub struct UserDrivenTx<'i, A> { + identity: &'i mut OnChainIdentity, + action: A, + proposal_id: ObjectID, +} \ No newline at end of file diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/controller_execution.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/controller_execution.rs new file mode 100644 index 000000000..86ee5e3d5 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/controller_execution.rs @@ -0,0 +1,95 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::Argument; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use move_core_types::ident_str; + +use crate::rebased::proposals::ControllerExecution; +use crate::rebased::sui::move_calls::utils; +use crate::rebased::utils::MoveType; + +pub(crate) fn propose_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + controller_cap_id: ObjectID, + expiration: Option, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap_id = ptb.pure(controller_cap_id)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + + let _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_controller_execution").into(), + vec![], + vec![identity_arg, delegation_token, controller_cap_id, exp_arg], + ); + + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package: ObjectID, +) -> Result +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &Argument), +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb.pure(proposal_id)?; + + // Get the proposal's action as argument. + let controller_execution_action = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_proposal").into(), + vec![ControllerExecution::move_type(package)], + vec![identity, delegation_token, proposal_id], + ); + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + // Borrow the controller cap into this transaction. + let receiving = ptb.obj(ObjectArg::Receiving(borrowing_controller_cap_ref))?; + let borrowed_controller_cap = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("borrow_controller_cap").into(), + vec![], + vec![identity, controller_execution_action, receiving], + ); + + // Apply the user-defined operation. + intent_fn(&mut ptb, &borrowed_controller_cap); + + // Put back the borrowed controller cap. + ptb.programmable_move_call( + package, + ident_str!("controller_proposal").into(), + ident_str!("put_back").into(), + vec![], + vec![controller_execution_action, borrowed_controller_cap], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/mod.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/mod.rs index 6c55b44b3..0862e4c82 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/mod.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/mod.rs @@ -8,6 +8,7 @@ mod deactivate; pub(crate) mod proposal; mod send_asset; mod update; +mod controller_execution; pub(crate) use borrow_asset::*; pub(crate) use config::*; @@ -15,3 +16,4 @@ pub(crate) use create::*; pub(crate) use deactivate::*; pub(crate) use send_asset::*; pub(crate) use update::*; +pub(crate) use controller_execution::*; From c44ef6d6f69e6e84de704150e2c05c949a38eabe Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 27 Nov 2024 10:40:35 +0100 Subject: [PATCH 3/5] e2e test for controller execution --- .../iota_identity/sources/identity.move | 2 +- .../sources/multicontroller.move | 2 +- .../src/rebased/client/read_only.rs | 4 +- .../src/rebased/migration/identity.rs | 12 ++- identity_iota_core/tests/e2e/identity.rs | 82 +++++++++++++++++++ 5 files changed, 95 insertions(+), 7 deletions(-) diff --git a/identity_iota_core/packages/iota_identity/sources/identity.move b/identity_iota_core/packages/iota_identity/sources/identity.move index 69a38f72a..f25e9db6e 100644 --- a/identity_iota_core/packages/iota_identity/sources/identity.move +++ b/identity_iota_core/packages/iota_identity/sources/identity.move @@ -397,7 +397,7 @@ module iota_identity::identity { /// Destroys a `ControllerCap`. Can only be used after a controller has been removed from /// the controller committee. - public fun destroy_controller_cap(self: &Identity, cap: ControllerCap) { + public(package) fun destroy_controller_cap(self: &Identity, cap: ControllerCap) { self.did_doc.destroy_controller_cap(cap); } diff --git a/identity_iota_core/packages/iota_identity/sources/multicontroller.move b/identity_iota_core/packages/iota_identity/sources/multicontroller.move index 3a49b5687..da88fd270 100644 --- a/identity_iota_core/packages/iota_identity/sources/multicontroller.move +++ b/identity_iota_core/packages/iota_identity/sources/multicontroller.move @@ -293,7 +293,7 @@ module iota_identity::multicontroller { /// Destroys a `ControllerCap`. Can only be used after a controller has been removed from /// the controller committee. public fun destroy_controller_cap(self: &Multicontroller, cap: ControllerCap) { - assert!(self.controllers.contains(&cap.id().to_inner()), EInvalidController); + assert!(!self.controllers.contains(&cap.id().to_inner()), EInvalidController); cap.delete(); } diff --git a/identity_iota_core/src/rebased/client/read_only.rs b/identity_iota_core/src/rebased/client/read_only.rs index 96cb6e800..cf155d7dd 100644 --- a/identity_iota_core/src/rebased/client/read_only.rs +++ b/identity_iota_core/src/rebased/client/read_only.rs @@ -118,8 +118,8 @@ impl IdentityClientReadOnly { .map_err(|e| Error::ObjectLookup(e.to_string())) } - #[allow(dead_code)] - pub(crate) async fn get_object_ref_by_id(&self, obj: ObjectID) -> Result, Error> { + /// Returns an object's [`OwnedObjectRef`], if any. + pub async fn get_object_ref_by_id(&self, obj: ObjectID) -> Result, Error> { self .read_api() .get_object_with_options(obj, IotaObjectDataOptions::default().with_owner()) diff --git a/identity_iota_core/src/rebased/migration/identity.rs b/identity_iota_core/src/rebased/migration/identity.rs index 938c03ded..7e494a653 100644 --- a/identity_iota_core/src/rebased/migration/identity.rs +++ b/identity_iota_core/src/rebased/migration/identity.rs @@ -6,6 +6,7 @@ use std::collections::HashSet; use std::ops::Deref; use std::str::FromStr; +use crate::rebased::proposals::ControllerExecution; use crate::rebased::sui::types::Number; use crate::IotaDID; use crate::IotaDocument; @@ -175,13 +176,18 @@ impl OnChainIdentity { } /// Borrows assets owned by this [`OnChainIdentity`] to use them in a custom transaction. - /// # Notes - /// Make sure to call [`super::Proposal::with_intent`] before executing the proposal. - /// Failing to do so will make [`crate::proposals::ProposalT::execute`] return an error. pub fn borrow_assets(&mut self) -> ProposalBuilder<'_, BorrowAction> { ProposalBuilder::new(self, BorrowAction::default()) } + /// Borrows a `ControllerCap` with ID `controller_cap` owned by this identity in a transaction. + /// This proposal is used to perform operation on a sub-identity controlled + /// by this one. + pub fn controller_execution(&mut self, controller_cap: ObjectID) -> ProposalBuilder<'_, ControllerExecution> { + let action = ControllerExecution::new(controller_cap, self); + ProposalBuilder::new(self, action) + } + /// Returns historical data for this [`OnChainIdentity`]. pub async fn get_history( &self, diff --git a/identity_iota_core/tests/e2e/identity.rs b/identity_iota_core/tests/e2e/identity.rs index 2bd6236ad..ab0e66ebb 100644 --- a/identity_iota_core/tests/e2e/identity.rs +++ b/identity_iota_core/tests/e2e/identity.rs @@ -20,7 +20,10 @@ use identity_iota_core::IotaDocument; use identity_verification::MethodScope; use identity_verification::VerificationMethod; use iota_sdk::rpc_types::IotaObjectData; +use iota_sdk::types::base_types::ObjectID; use iota_sdk::types::base_types::SequenceNumber; +use iota_sdk::types::object::Owner; +use iota_sdk::types::transaction::ObjectArg; use iota_sdk::types::TypeTag; use iota_sdk::types::IOTA_FRAMEWORK_PACKAGE_ID; use move_core_types::ident_str; @@ -355,3 +358,82 @@ async fn borrow_proposal_works() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn controller_execution_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut identity = identity_client + .create_identity(TEST_DOC) + .finish() + .execute(&identity_client) + .await? + .output; + let identity_address = identity.id().into(); + + // Create a second identity owned by the first. + let identity2 = identity_client + .create_identity(TEST_DOC) + .controller(identity_address, 1) + .threshold(1) + .finish() + .execute(&identity_client) + .await? + .output; + + // Let's find identity's controller cap for identity2. + let controller_cap = identity_client + .find_owned_ref_for_address( + identity_address, + format!("{}::controller::ControllerCap", identity_client.package_id()).parse()?, + |_| true, + ) + .await? + .expect("identity is a controller of identity2"); + + // Perform an action on `identity2` as a controller of `identity`. + let ProposalResult::Pending(controller_execution) = identity + .controller_execution(controller_cap.0) + .finish(&identity_client) + .await? + .execute(&identity_client) + .await? + .output + else { + panic!("controller execution proposals cannot be executed without being driven by the user") + }; + let identity2_ref = identity_client.get_object_ref_by_id(identity2.id()).await?.unwrap(); + let Owner::Shared { initial_shared_version } = identity2_ref.owner else { + panic!("identity2 is shared") + }; + let tx_output = controller_execution + .into_tx(&mut identity, &identity_client) + .await? + // specify the operation to perform with the borrowed identity's controller_cap + .with_intent(|ptb, controller_cap| { + let identity2 = ptb + .obj(ObjectArg::SharedObject { + id: identity2_ref.object_id(), + initial_shared_version, + mutable: true, + }) + .unwrap(); + + let token_to_revoke = ptb.pure(ObjectID::ZERO).unwrap(); + + ptb.programmable_move_call( + identity_client.package_id(), + ident_str!("identity").into(), + ident_str!("revoke_token").into(), + vec![], + vec![identity2, *controller_cap, token_to_revoke], + ); + }) + .execute(&identity_client) + .await?; + + assert!(tx_output.response.status_ok().unwrap()); + + Ok(()) +} From 79c136d1e546c0138507acaa18aea75610bba06c Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 27 Nov 2024 12:15:24 +0100 Subject: [PATCH 4/5] fix merge issues --- identity_iota_core/Cargo.toml | 2 +- .../src/rebased/migration/identity.rs | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index abc09cf2a..b06a4df5d 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -41,7 +41,7 @@ iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk itertools = { version = "0.13.0", optional = true } move-core-types = { git = "https://github.com/iotaledger/iota.git", package = "move-core-types", rev = "39c83ddcf07894cdee2abd146381d8704205e6e9", optional = true } rand = { version = "0.8.5", optional = true } -secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", branch = "main", optional = true } +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", tag = "v0.1.0", optional = true } serde-aux = { version = "4.5.0", optional = true } shared-crypto = { git = "https://github.com/iotaledger/iota.git", package = "shared-crypto", rev = "39c83ddcf07894cdee2abd146381d8704205e6e9", optional = true } tokio = { version = "1.29.0", default-features = false, optional = true, features = ["macros", "sync", "rt", "process"] } diff --git a/identity_iota_core/src/rebased/migration/identity.rs b/identity_iota_core/src/rebased/migration/identity.rs index 68a71cf51..f24901e38 100644 --- a/identity_iota_core/src/rebased/migration/identity.rs +++ b/identity_iota_core/src/rebased/migration/identity.rs @@ -426,6 +426,59 @@ fn is_identity(value: &IotaParsedMoveObject) -> bool { value.type_.module.as_ident_str().as_str() == MODULE && value.type_.name.as_ident_str().as_str() == NAME } +/// Unpack identity data from given `IotaObjectData` +/// +/// # Errors: +/// * in case given data for DID is not an object +/// * parsing identity data from object fails +pub(crate) fn unpack_identity_data( + did: &IotaDID, + data: &IotaObjectData, +) -> Result>, Timestamp, Timestamp)>, Error> { + let content = data + .clone() + .content + .ok_or_else(|| Error::ObjectLookup(format!("no content in retrieved object in object id {did}")))?; + let IotaParsedData::MoveObject(value) = content else { + return Err(Error::ObjectLookup(format!( + "given data for DID {did} is not an object" + ))); + }; + if !is_identity(&value) { + return Ok(None); + } + + #[derive(Deserialize)] + struct TempOnChainIdentity { + id: UID, + did_doc: Multicontroller>, + created: Number, + updated: Number, + } + + let TempOnChainIdentity { + id, + did_doc: multi_controller, + created, + updated, + } = serde_json::from_value::(value.fields.to_json_value()) + .map_err(|err| Error::ObjectLookup(format!("could not parse identity document with DID {did}; {err}")))?; + + // Parse DID document timestamps + let created = { + let timestamp_ms: u64 = created.try_into().expect("Move string-encoded u64 are valid u64"); + // `Timestamp` requires a timestamp expressed in seconds. + Timestamp::from_unix(timestamp_ms as i64 / 1000).expect("On-chain clock produces valid timestamps") + }; + let updated = { + let timestamp_ms: u64 = updated.try_into().expect("Move string-encoded u64 are valid u64"); + // `Timestamp` requires a timestamp expressed in seconds. + Timestamp::from_unix(timestamp_ms as i64 / 1000).expect("On-chain clock produces valid timestamps") + }; + + Ok(Some((id, multi_controller, created, updated))) +} + /// Builder-style struct to create a new [`OnChainIdentity`]. #[derive(Debug)] pub struct IdentityBuilder<'a> { From 93c8125a9fb754f2f7baea3afb03a252c9cf858d Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 27 Nov 2024 15:13:34 +0100 Subject: [PATCH 5/5] fix merge --- .../packages/iota_identity/sources/identity.move | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/identity_iota_core/packages/iota_identity/sources/identity.move b/identity_iota_core/packages/iota_identity/sources/identity.move index 265839420..e57dd213b 100644 --- a/identity_iota_core/packages/iota_identity/sources/identity.move +++ b/identity_iota_core/packages/iota_identity/sources/identity.move @@ -248,7 +248,7 @@ module iota_identity::identity { /// Proposes to upgrade this `Identity` to this package's version. public fun propose_upgrade( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, ctx: &mut TxContext, ): Option { @@ -266,7 +266,7 @@ module iota_identity::identity { self.execute_upgrade(cap, proposal_id, ctx); option::none() } else { - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, false); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); option::some(proposal_id) } } @@ -275,13 +275,13 @@ module iota_identity::identity { /// package's version. public fun execute_upgrade( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext, ) { self.execute_proposal(cap, proposal_id, ctx).unwrap(); self.migrate(); - emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, true); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, true); } /// Migrates this `Identity` to this package's version.