diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index d4a72c4..abfd899 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -1529,7 +1529,7 @@ where C: NodeClient, K: Keychain, { - contract::new(&mut *w, keychain_mask, &args.setup_args) + contract::new(&mut *w, keychain_mask, &args.setup_args, None) } /// Sign transaction contract diff --git a/libwallet/src/contract/actions/new.rs b/libwallet/src/contract/actions/new.rs index 18ad453..4682509 100644 --- a/libwallet/src/contract/actions/new.rs +++ b/libwallet/src/contract/actions/new.rs @@ -23,19 +23,23 @@ use crate::grin_util::secp::key::SecretKey; use crate::slate::Slate; use crate::types::{Context, NodeClient}; use crate::backend::WalletBackend; +use uuid::Uuid; -/// Create a new contract with initial setup done by the initiator +/// Create a new contract with initial setup done by the initiator. `slate_id`, when +/// provided, fixes the slate id (rather than a random one) so the caller can make a +/// retried creation idempotent: get_or_create reuses the existing context for that id. pub fn new( w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, setup_args: &ContractSetupArgsAPI, + slate_id: Option, ) -> Result where C: NodeClient, K: Keychain, { // Compute state for 'new' - let (slate, mut context) = compute(w, keychain_mask, setup_args)?; + let (slate, mut context) = compute(w, keychain_mask, setup_args, slate_id)?; // Atomically commit state contract::utils::save_step( @@ -55,6 +59,7 @@ pub fn compute( w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, setup_args: &ContractSetupArgsAPI, + slate_id: Option, ) -> Result<(Slate, Context), Error> where C: NodeClient, @@ -68,6 +73,10 @@ where // Initialize a new contract (if net_change is positive, I'm the receiver meaning this is invoice flow) let num_participants = setup_args.num_participants; let mut slate = Slate::blank(num_participants, net_change > 0); + // Use a caller-supplied id when given, so a retried creation reuses the same context. + if let Some(id) = slate_id { + slate.id = id; + } // We set slate.amount to contain the _positive_ net_change for the other party so they can derive expectations. // unsigned_abs avoids the i64::MIN overflow panic of abs(). slate.amount = net_change.unsigned_abs(); diff --git a/libwallet/src/contract/actions/revoke.rs b/libwallet/src/contract/actions/revoke.rs index 77ea1aa..bdea330 100644 --- a/libwallet/src/contract/actions/revoke.rs +++ b/libwallet/src/contract/actions/revoke.rs @@ -22,6 +22,19 @@ use crate::internal::tx; use crate::slate::Slate; use crate::types::{NodeClient, OutputData, OutputStatus, TxLogEntryType}; use crate::backend::WalletBackend; +use crate::blake2::blake2b::blake2b; +use uuid::Uuid; + +/// Deterministic slate id for the self-spend that revokes a given contract slate. +/// Derived from the revoked slate id so a revoke interrupted between creating and +/// signing the self-spend resumes by reusing the same context (get_or_create), rather +/// than orphaning a fresh self-spend on each retry. +fn self_spend_slate_id(revoked_slate_id: Uuid) -> Uuid { + let hash = blake2b(16, b"grin-contract-revoke", revoked_slate_id.as_bytes()); + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(hash.as_bytes()); + Uuid::from_bytes(bytes) +} /// Contract revocation is done by double-spending the input pub fn revoke( @@ -67,6 +80,7 @@ where // it is already a *Cancelled type (and the inputs are Unspent), so we skip straight to // re-spending them. let revoked = w.get_tx_log_entry(parent_key_id.clone(), tx_id)?; + let revoked_slate_id = revoked.as_ref().and_then(|e| e.tx_slate_id); let needs_cancel = match revoked.as_ref() { Some(e) => matches!( e.tx_type, @@ -82,7 +96,7 @@ where tx::cancel_tx(&mut *w, keychain_mask, &parent_key_id, Some(tx_id), None)?; // Drop the canceled slate's private context if one still exists (signing already // deletes it). - if let Some(slate_id) = revoked.and_then(|e| e.tx_slate_id) { + if let Some(slate_id) = revoked_slate_id { if w .get_private_context(keychain_mask, slate_id.as_bytes()) .is_ok() @@ -107,6 +121,10 @@ where .acct_path_iter()? .find(|m| m.path == parent_key_id) .map(|m| m.label); + // Deterministic self-spend slate id (when we know the revoked slate) so a crash + // between new() and sign() is resumed by reusing the same context rather than + // orphaning a fresh self-spend on the retry. + let self_spend_id = revoked_slate_id.map(self_spend_slate_id); // 2. Create a 1-1 self-spend transaction using this input let ct_slate = new( w, @@ -122,6 +140,7 @@ where }, proof_args: Default::default(), }, + self_spend_id, )?; let finished_slate = sign( w, @@ -142,3 +161,21 @@ where Ok(Some(finished_slate)) } + +#[cfg(test)] +mod tests { + use super::self_spend_slate_id; + use uuid::Uuid; + + #[test] + fn self_spend_id_is_deterministic_and_distinct() { + let revoked = Uuid::parse_str("936da01f-9abd-4d9d-80c7-02af85c822a8").unwrap(); + // Same revoked slate -> same self-spend id, so a retried revoke reuses the context. + assert_eq!(self_spend_slate_id(revoked), self_spend_slate_id(revoked)); + // Distinct from the revoked id (the self-spend is a different tx). + assert_ne!(self_spend_slate_id(revoked), revoked); + // Different revoked slates -> different self-spend ids. + let other = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); + assert_ne!(self_spend_slate_id(revoked), self_spend_slate_id(other)); + } +}