mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-10-17 15:21:39 +02:00
Contracts: deterministic self-spend id so an interrupted revoke resumes
revoke builds the self-spend via new(), which used a random slate id, so a crash between new() and sign() left an orphaned self-spend and a retry created another one. Derive the self-spend slate id deterministically from the revoked slate (blake2b, already a dependency) and let contract::new take an optional slate id; a retry then reuses the in-progress context via get_or_create instead of orphaning a fresh self-spend. The other new() caller passes None (random id). Adds a unit test that the derived id is deterministic and distinct.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<C, K>(
|
||||
w: &mut WalletBackend<C, K>,
|
||||
keychain_mask: Option<&SecretKey>,
|
||||
setup_args: &ContractSetupArgsAPI,
|
||||
slate_id: Option<Uuid>,
|
||||
) -> Result<Slate, Error>
|
||||
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<C, K>(
|
||||
w: &mut WalletBackend<C, K>,
|
||||
keychain_mask: Option<&SecretKey>,
|
||||
setup_args: &ContractSetupArgsAPI,
|
||||
slate_id: Option<Uuid>,
|
||||
) -> 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();
|
||||
|
||||
@@ -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<C, K>(
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user