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/packages/iota_identity/sources/identity.move b/identity_iota_core/packages/iota_identity/sources/identity.move index 1003d1e29..0cbacf0e2 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}, + upgrade_proposal::{Self, Upgrade}, }; const ENotADidDocument: u64 = 0; @@ -24,6 +25,10 @@ module iota_identity::identity { const EInvalidThreshold: u64 = 2; /// The controller list must contain at least 1 element. const EInvalidControllersList: u64 = 3; + /// There's no upgrade available for this identity. + const ENoUpgrade: u64 = 4; + + const PACKAGE_VERSION: u64 = 0; // ===== Events ====== /// Event emitted when an `identity`'s `Proposal` with `ID` `proposal` is created or executed by `controller`. @@ -53,6 +58,8 @@ module iota_identity::identity { created: u64, /// Timestamp of this Identity's last update. updated: u64, + /// Package version used by this object. + version: u64, } /// Creates a new DID Document with a single controller. @@ -94,6 +101,7 @@ module iota_identity::identity { did_doc: multicontroller::new_with_controller(doc, controller, can_delegate, ctx), created: now, updated: now, + version: PACKAGE_VERSION, } } @@ -118,6 +126,7 @@ module iota_identity::identity { did_doc: multicontroller::new_with_controllers(doc, controllers, controllers_that_can_delegate, threshold, ctx), created: now, updated: now, + version: PACKAGE_VERSION, } } @@ -205,6 +214,51 @@ module iota_identity::identity { emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, true); } + /// Proposes to upgrade this `Identity` to this package's version. + public fun propose_upgrade( + self: &mut Identity, + cap: &ControllerCap, + expiration: Option, + ctx: &mut TxContext, + ): Option { + assert!(self.version < PACKAGE_VERSION, ENoUpgrade); + let proposal_id = self.did_doc.create_proposal( + cap, + upgrade_proposal::new(), + expiration, + ctx + ); + let is_approved = self + .did_doc + .is_proposal_approved<_, Upgrade>(proposal_id); + if (is_approved) { + self.execute_upgrade(cap, proposal_id, ctx); + option::none() + } else { + emit_proposal_event(self.id().to_inner(), cap.id().to_inner(), proposal_id, false); + option::some(proposal_id) + } + } + + /// Consumes a `Proposal` that migrates `Identity` to this + /// package's version. + public fun execute_upgrade( + self: &mut Identity, + cap: &ControllerCap, + 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); + } + + /// Migrates this `Identity` to this package's version. + fun migrate(self: &mut Identity) { + // ADD migration logic when needed! + self.version = PACKAGE_VERSION; + } + /// 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. diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/upgrade.move b/identity_iota_core/packages/iota_identity/sources/proposals/upgrade.move new file mode 100644 index 000000000..0f7984a56 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/upgrade.move @@ -0,0 +1,12 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::upgrade_proposal { + /// Proposal's action used to upgrade an `Identity` to the package's current version. + public struct Upgrade has store, copy, drop {} + + /// Creates a new `Upgrade` action. + public fun new(): Upgrade { + Upgrade {} + } +} \ No newline at end of file diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs index bee1db7b3..8ba02c4ce 100644 --- a/identity_iota_core/src/document/iota_document.rs +++ b/identity_iota_core/src/document/iota_document.rs @@ -427,7 +427,7 @@ pub mod client_document { None, )) })?; - let (_, multi_controller, created, updated) = match unpacked { + let (_, multi_controller, created, updated, _) = match unpacked { Some(data) => data, None => { return Err(Error::InvalidDoc(identity_document::Error::InvalidDocument( diff --git a/identity_iota_core/src/rebased/migration/identity.rs b/identity_iota_core/src/rebased/migration/identity.rs index b1a09d03a..3b7069e61 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::Upgrade; use crate::rebased::sui::types::Number; use crate::IotaDID; use crate::IotaDocument; @@ -99,6 +100,7 @@ pub struct OnChainIdentity { id: UID, multi_controller: Multicontroller>, did_doc: IotaDocument, + version: u64, } impl Deref for OnChainIdentity { @@ -169,6 +171,11 @@ impl OnChainIdentity { ProposalBuilder::new(self, DeactivateDid::new()) } + /// Upgrades this [`OnChainIdentity`]'s version to match the package's. + pub fn upgrade_version(&mut self) -> ProposalBuilder<'_, Upgrade> { + ProposalBuilder::new(self, Upgrade::default()) + } + /// Sends assets owned by this [`OnChainIdentity`] to other addresses. pub fn send_assets(&mut self) -> ProposalBuilder<'_, SendAction> { ProposalBuilder::new(self, SendAction::default()) @@ -339,16 +346,13 @@ pub async fn get_identity( // no issues with call but let Some(data) = response.data else { - // call was successful but not data for alias id + // call was successful but no data for alias id return Ok(None); }; let did = IotaDID::from_alias_id(&object_id.to_string(), client.network()); - let (id, multi_controller, created, updated) = match unpack_identity_data(&did, &data)? { - Some(data) => data, - None => { - return Ok(None); - } + let Some((id, multi_controller, created, updated, version)) = unpack_identity_data(&did, &data)? else { + return Ok(None); }; let did_doc = @@ -359,6 +363,7 @@ pub async fn get_identity( id, multi_controller, did_doc, + version, })) } @@ -376,7 +381,7 @@ fn is_identity(value: &IotaParsedMoveObject) -> bool { pub(crate) fn unpack_identity_data( did: &IotaDID, data: &IotaObjectData, -) -> Result>, Timestamp, Timestamp)>, Error> { +) -> Result>, Timestamp, Timestamp, u64)>, Error> { let content = data .clone() .content @@ -396,6 +401,7 @@ pub(crate) fn unpack_identity_data( did_doc: Multicontroller>, created: Number, updated: Number, + version: Number, } let TempOnChainIdentity { @@ -403,6 +409,7 @@ pub(crate) fn unpack_identity_data( did_doc: multi_controller, created, updated, + version } = serde_json::from_value::(value.fields.to_json_value()) .map_err(|err| Error::ObjectLookup(format!("could not parse identity document with DID {did}; {err}")))?; @@ -417,8 +424,9 @@ pub(crate) fn unpack_identity_data( // `Timestamp` requires a timestamp expressed in seconds. Timestamp::from_unix(timestamp_ms as i64 / 1000).expect("On-chain clock produces valid timestamps") }; + let version = version.try_into().expect("Move string-encoded u64 are valid u64"); - Ok(Some((id, multi_controller, created, updated))) + Ok(Some((id, multi_controller, created, updated, version))) } /// Builder-style struct to create a new [`OnChainIdentity`]. diff --git a/identity_iota_core/src/rebased/proposals/mod.rs b/identity_iota_core/src/rebased/proposals/mod.rs index 766d575b8..52058ac0f 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 deactivate_did; mod send; mod update_did_doc; +mod upgrade; use std::marker::PhantomData; use std::ops::Deref; @@ -19,6 +20,7 @@ use crate::rebased::transaction::ProtoTransaction; use async_trait::async_trait; pub use borrow::*; pub use config_change::*; +pub use upgrade::*; pub use deactivate_did::*; use iota_sdk::rpc_types::IotaExecutionStatus; use iota_sdk::rpc_types::IotaObjectData; diff --git a/identity_iota_core/src/rebased/proposals/upgrade.rs b/identity_iota_core/src/rebased/proposals/upgrade.rs new file mode 100644 index 000000000..5b0099d10 --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/upgrade.rs @@ -0,0 +1,109 @@ +// 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::sui::move_calls; +use async_trait::async_trait; +use iota_sdk::rpc_types::IotaTransactionBlockResponse; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::migration::Proposal; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::ProposalT; + +/// Action for upgrading the version of an on-chain identity to the package's version. +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +pub struct Upgrade; + +impl Upgrade { + /// Creates a new [`Upgrade`] action. + pub const fn new() -> Self { + Self + } +} + +impl MoveType for Upgrade { + fn move_type(package: ObjectID) -> TypeTag { + format!("{package}::upgrade_proposal::Upgrade") + .parse() + .expect("valid utf8") + } +} + +#[async_trait] +impl ProposalT for Proposal { + type Action = Upgrade; + 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 sender_vp = identity + .controller_voting_power(controller_cap_ref.0) + .expect("controller exists"); + let chained_execution = sender_vp >= identity.threshold(); + let tx = + move_calls::identity::propose_upgrade(identity_ref, controller_cap_ref, expiration, client.package_id()) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + chained_execution, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let proposal_id = self.id(); + 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::execute_upgrade(identity_ref, controller_cap_ref, proposal_id, client.package_id()) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(ExecuteProposalTx { + identity, + tx, + _action: PhantomData, + }) + } + + fn parse_tx_effects(_tx_response: &IotaTransactionBlockResponse) -> Result { + Ok(()) + } +} 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..cb57e14a4 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 upgrade; 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 upgrade::*; diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/upgrade.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/upgrade.rs new file mode 100644 index 000000000..376acae21 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/upgrade.rs @@ -0,0 +1,56 @@ +// 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::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use move_core_types::ident_str; + +use crate::rebased::sui::move_calls::utils; + +pub(crate) fn propose_upgrade( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + 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 _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_upgrade").into(), + vec![], + vec![identity_arg, cap_arg, exp_arg], + ); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_upgrade( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let proposal_id = ptb.pure(proposal_id)?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + + let _ = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("execute_upgrade").into(), + vec![], + vec![identity_arg, cap_arg, proposal_id], + ); + + Ok(ptb.finish()) +}