diff --git a/controller/tests/contract_local_accounts.rs b/controller/tests/contract_local_accounts.rs new file mode 100644 index 0000000..5f7abd7 --- /dev/null +++ b/controller/tests/contract_local_accounts.rs @@ -0,0 +1,278 @@ +// Copyright 2022 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Contracts between accounts in one wallet +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_core::core::{Input, OutputFeatures}; +use grin_wallet_libwallet as libwallet; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractRevokeArgsAPI}; +use libwallet::{Error, SlateState}; +use std::path::PathBuf; +use std::sync::atomic::Ordering; + +#[macro_use] +mod common; + +fn local_transfer( + test_dir: &'static str, + invoice: bool, + early_lock: bool, + revoke: bool, +) -> Result<(), Error> { + common::setup(test_dir); + let (wallets, _chain, stopper, _) = common::create_wallets( + vec![vec![("default", 4), ("receiver", 4), ("unrelated", 4)]], + test_dir, + ) + .unwrap(); + let wallet = wallets[0].0.clone(); + let mask = wallets[0].1.as_ref(); + let result = + common::owner_single_use(wallet.clone(), mask, PathBuf::from(test_dir), |api, m| { + let amount = 5_000_000_000; + let (initiator, counterparty) = if invoice { + ("receiver", "default") + } else { + ("default", "receiver") + }; + let change = if invoice { amount } else { -amount }; + api.set_active_account(m, "unrelated")?; + let (_, unrelated) = api.retrieve_outputs(m, false, true, None)?; + let unrelated = unrelated.first().unwrap().clone(); + api.set_active_account(m, initiator)?; + let mut args = common::contract_setup_args(Some(change)); + args.add_outputs = early_lock; + let first = api.contract_new( + m, + &ContractNewArgsAPI { + setup_args: args, + ttl_blocks: Some(20), + }, + )?; + // Preserve the legacy context key + let initiator_parent = { + wallet_inst!(wallet, w); + let ctx = w.get_private_context(m, first.id.as_bytes())?; + ctx.parent_key_id + }; + + // Setup retries stay with the original account + api.set_active_account(m, "unrelated")?; + { + wallet_inst!(wallet, w); + let retried = libwallet::contract::setup( + w, + m, + &first, + &common::contract_setup_args(Some(change)), + )?; + assert_eq!(retried.participant_data.len(), 1); + assert_eq!(w.private_context_iter()?.count(), 1); + assert_eq!( + w.get_private_context(m, first.id.as_bytes())?.parent_key_id, + initiator_parent + ); + } + let mut sign_args = common::contract_setup_args(Some(-change)); + api.set_active_account(m, if invoice { "unrelated" } else { counterparty })?; + if invoice { + sign_args.src_acct_name = Some(counterparty.into()); + } + let second = common::roundtrip_slate(&api.contract_sign(m, &first, &sign_args)?, 4)?; + { + wallet_inst!(wallet, w); + assert_eq!( + w.get_private_context(m, first.id.as_bytes())?.parent_key_id, + initiator_parent + ); + assert_eq!(w.private_context_iter()?.count(), 2); + } + api.set_active_account(m, counterparty)?; + let counterparty_tx = common::tx_log_for_slate(api, m, &second)?; + let progress = common::wallet_progress(api, m)?; + // Reject replay of both messages + for slate in [&first, &second] { + let err = api.contract_sign(m, slate, &sign_args).unwrap_err(); + assert!( + matches!(err, Error::GenericError(ref msg) if msg.contains("already been signed")) + ); + } + assert_eq!(common::wallet_progress(api, m)?, progress); + assert_eq!( + api.contract_view(m, &second)?.agreed_net_change, + Some(-change) + ); + + api.set_active_account(m, "unrelated")?; + assert!(api.contract_view(m, &second).is_err()); + assert!(api + .contract_sign(m, &second, &common::contract_setup_args(None)) + .is_err()); + api.set_active_account(m, initiator)?; + assert_eq!( + api.contract_view(m, &second)?.agreed_net_change, + Some(change) + ); + let progress = common::wallet_progress(api, m)?; + // Reject inputs from unrelated accounts + let mut tampered = second.clone(); + tampered.tx = Some( + tampered + .tx_or_err()? + .clone() + .with_input(Input::new(OutputFeatures::Coinbase, unrelated.commit)), + ); + let err = api + .contract_sign(m, &tampered, &common::contract_setup_args(None)) + .unwrap_err(); + assert!( + matches!(err, Error::GenericError(ref msg) if msg.contains("unexpected input commitment")) + ); + assert_eq!(common::wallet_progress(api, m)?, progress); + + // Require the counterparty's signature + let mut unsigned = second.clone(); + for participant in &mut unsigned.participant_data { + participant.part_sig = None; + } + let err = api + .contract_sign(m, &unsigned, &common::contract_setup_args(None)) + .unwrap_err(); + assert!( + matches!(err, Error::GenericError(ref msg) if msg.contains("unexpected input")) + ); + assert_eq!(common::wallet_progress(api, m)?, progress); + + if revoke { + let initiator_tx = common::tx_log_for_slate(api, m, &second)?; + // Revoking one party preserves the other context + let replacement = api + .contract_revoke( + m, + &ContractRevokeArgsAPI { + tx_id: counterparty_tx.id, + src_acct_name: Some(counterparty.into()), + }, + )? + .unwrap(); + { + wallet_inst!(wallet, w); + assert_eq!( + w.get_private_context(m, first.id.as_bytes())?.parent_key_id, + initiator_parent + ); + assert_eq!(w.private_context_iter()?.count(), 1); + } + assert!(api + .contract_revoke( + m, + &ContractRevokeArgsAPI { + tx_id: counterparty_tx.id, + src_acct_name: Some(counterparty.into()), + } + )? + .is_none()); + // Revoked contributions are no longer authorized + assert!(api + .contract_sign(m, &second, &common::contract_setup_args(None)) + .is_err()); + let other_replacement = api + .contract_revoke( + m, + &ContractRevokeArgsAPI { + tx_id: initiator_tx.id, + src_acct_name: Some(initiator.into()), + }, + )? + .unwrap(); + assert_ne!(replacement.id, other_replacement.id); + wallet_inst!(wallet, w); + assert_eq!(w.private_context_iter()?.count(), 0); + return Ok(()); + } + + let final_slate = api.contract_sign(m, &second, &common::contract_setup_args(None))?; + assert_eq!( + final_slate.state, + if invoice { + SlateState::Invoice3 + } else { + SlateState::Standard3 + } + ); + { + wallet_inst!(wallet, w); + assert!(matches!( + w.get_private_context(m, first.id.as_bytes()), + Err(Error::NotFoundErr(_)) + )); + assert_eq!(w.private_context_iter()?.count(), 1); + } + // Reject signing again even after context deletion + for (account, net) in [(initiator, change), (counterparty, -change)] { + api.set_active_account(m, account)?; + for slate in [&first, &second] { + assert!(api + .contract_sign(m, slate, &common::contract_setup_args(Some(net))) + .is_err()); + } + let view = api.contract_view(m, &final_slate)?; + assert_eq!(view.agreed_net_change, Some(net)); + assert_eq!( + view.own_commitment_status, + libwallet::contract::types::OwnCommitmentStatus::Unknown + ); + } + // Mine rewards into a third account to isolate transfer balances + api.set_active_account(m, "unrelated")?; + assert!(api.contract_view(m, &final_slate).is_err()); + api.post_tx(m, &final_slate, false)?; + let fee = my_fee_contribution(1, 1, 1, 2)?.fee(); + for (account, net) in [("default", -amount), ("receiver", amount)] { + api.set_active_account(m, account)?; + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert_eq!( + info.total, + (4 * grin_core::consensus::REWARD as i64 + net) as u64 - fee + ); + assert!(common::tx_log_for_slate(api, m, &final_slate)?.confirmed); + } + Ok(()) + }); + stopper.store(false, Ordering::Relaxed); + result?; + common::clean_output_dir(test_dir); + Ok(()) +} + +#[test] +fn contracts_between_local_accounts() -> Result<(), Error> { + for (dir, invoice, early) in [ + ("test_output/contract_local_srs", false, false), + ("test_output/contract_local_srs_early", false, true), + ("test_output/contract_local_rsr", true, false), + ("test_output/contract_local_rsr_early", true, true), + ] { + local_transfer(dir, invoice, early, false)?; + } + Ok(()) +} + +#[test] +fn revoke_local_counterparty() -> Result<(), Error> { + local_transfer("test_output/contract_local_revoke", false, true, true) +} diff --git a/doc/contracts.md b/doc/contracts.md index 706b333..8a41eaf 100644 --- a/doc/contracts.md +++ b/doc/contracts.md @@ -53,6 +53,8 @@ does not, revoke the contract and create a new one. Each side's account is fixed when it first joins the contract. Changing the active account later does not affect setup or signing; they use the account stored in the context. Encrypted Slatepacks still need their account selected with `--account`; the error names it. +For contracts between accounts in the same wallet, select the party with `--account` +for each step. ## View and revoke diff --git a/libwallet/src/backend.rs b/libwallet/src/backend.rs index 876719d..c7e2492 100644 --- a/libwallet/src/backend.rs +++ b/libwallet/src/backend.rs @@ -463,7 +463,7 @@ where /// Retrieve the private context associated with a given slate id. pub fn get_private_context( - &mut self, + &self, keychain_mask: Option<&SecretKey>, slate_id: &[u8], ) -> Result { diff --git a/libwallet/src/contract/actions/new.rs b/libwallet/src/contract/actions/new.rs index 2a1ed77..db59244 100644 --- a/libwallet/src/contract/actions/new.rs +++ b/libwallet/src/contract/actions/new.rs @@ -79,13 +79,14 @@ where let mut existing_context = None; if let Some(id) = slate_id { slate.id = id; - match w.get_private_context(keychain_mask, slate.id.as_bytes()) { - Ok(context) => { - slate.ttl_cutoff_height = context.contract_ttl_cutoff_height.unwrap_or(0); - existing_context = Some(context); - } - Err(Error::NotFoundErr(_)) => {} - Err(e) => return Err(e), + let parent = contract::utils::parent_key_for(w, setup_args.src_acct_name.as_deref())?; + contract::utils::verify_not_signed(w, slate.id, &parent)?; + existing_context = contract::context::stored(w, keychain_mask, slate.id)? + .into_iter() + .find(|(_, ctx)| ctx.parent_key_id == parent) + .map(|(_, ctx)| ctx); + if let Some(context) = &existing_context { + slate.ttl_cutoff_height = context.contract_ttl_cutoff_height.unwrap_or(0); } } if existing_context.is_none() { diff --git a/libwallet/src/contract/actions/revoke.rs b/libwallet/src/contract/actions/revoke.rs index cddc941..4911e47 100644 --- a/libwallet/src/contract/actions/revoke.rs +++ b/libwallet/src/contract/actions/revoke.rs @@ -93,14 +93,13 @@ where // Drop the canceled slate's private context if one still exists (signing already // deletes it). if let Some(slate_id) = revoked_slate_id { - match w.get_private_context(keychain_mask, slate_id.as_bytes()) { - Ok(_) => { - let mut batch = w.batch(keychain_mask)?; - batch.delete_private_context(slate_id.as_bytes())?; - batch.commit()?; - } - Err(Error::NotFoundErr(_)) => {} - Err(e) => return Err(e), + if let Some((key, _)) = crate::contract::context::stored(w, keychain_mask, slate_id)? + .into_iter() + .find(|(_, ctx)| ctx.parent_key_id == parent_key_id) + { + let mut batch = w.batch(keychain_mask)?; + batch.delete_private_context(&key)?; + batch.commit()?; } } } diff --git a/libwallet/src/contract/actions/setup.rs b/libwallet/src/contract/actions/setup.rs index 1c9d6d0..59db912 100644 --- a/libwallet/src/contract/actions/setup.rs +++ b/libwallet/src/contract/actions/setup.rs @@ -56,9 +56,16 @@ where slate.num_participants, setup_args.num_participants ))); } - contract::utils::verify_not_signed(w, slate.id)?; + let parent = contract::utils::parent_key_for(w, setup_args.src_acct_name.as_deref())?; + let context = + contract::context::resolve(w, keychain_mask, slate, &parent, setup_args.net_change)?; + let parent = context + .as_ref() + .map(|ctx| &ctx.parent_key_id) + .unwrap_or(&parent); + contract::utils::verify_not_signed(w, slate.id, parent)?; // Compute state for 'setup' - let (slate, context) = compute(w, keychain_mask, slate, setup_args, None)?; + let (slate, context) = compute(w, keychain_mask, slate, setup_args, context)?; // Atomically commit state contract::utils::save_step(w, keychain_mask, &slate, context, setup_args.add_outputs)?; diff --git a/libwallet/src/contract/actions/sign.rs b/libwallet/src/contract/actions/sign.rs index 3accbfb..ba28994 100644 --- a/libwallet/src/contract/actions/sign.rs +++ b/libwallet/src/contract/actions/sign.rs @@ -62,11 +62,9 @@ where contract::utils::verify_fee_rate(setup_args.fee_rate)?; // Compute if we will add outputs at this step. Only a missing context means we // still need to add them; propagate any other error. - let existing_context = match w.get_private_context(keychain_mask, slate.id.as_bytes()) { - Ok(context) => Some(context), - Err(Error::NotFoundErr(_)) => None, - Err(e) => return Err(e), - }; + let parent = contract::utils::parent_key_for(w, setup_args.src_acct_name.as_deref())?; + let existing_context = + contract::context::resolve(w, keychain_mask, slate, &parent, setup_args.net_change)?; let will_add_outputs = existing_context .as_ref() .map(|context| !context.has_inputs_or_outputs()) @@ -92,7 +90,11 @@ where C: NodeClient, K: Keychain, { - contract::utils::verify_not_signed(w, slate.id)?; + let parent = match &existing_context { + Some(context) => context.parent_key_id.clone(), + None => contract::utils::parent_key_for(w, setup_args.src_acct_name.as_deref())?, + }; + contract::utils::verify_not_signed(w, slate.id, &parent)?; // Reject terminal and unknown states before setup reserves keys. contract::slate::next_state(&slate.state)?; diff --git a/libwallet/src/contract/actions/view.rs b/libwallet/src/contract/actions/view.rs index 5fa11ff..7de4e5d 100644 --- a/libwallet/src/contract/actions/view.rs +++ b/libwallet/src/contract/actions/view.rs @@ -76,16 +76,32 @@ where )); } contract::utils::verify_num_participants(slate.num_participants)?; - let context = match w.get_private_context(keychain_mask, slate.id.as_bytes()) { - Ok(context) => Some(context), - Err(Error::NotFoundErr(_)) => None, - Err(e) => return Err(e), - }; + let parent = w.parent_key_id(); + let context = contract::context::resolve(w, keychain_mask, slate, &parent, None)?; let suggested_net_change = initial_net_change(&slate.state, slate.amount)?; // A contract is executed once the transaction it produced has confirmed. That is // recorded on our own tx log entry for this slate, so no chain lookup is needed; a // slate we have never signed simply has no entry and is not executed. - let txs = updater::retrieve_txs(w, None, Some(slate.id), None, None, false)?; + let mut txs = updater::retrieve_txs(w, None, Some(slate.id), None, None, false)?; + let contexts = contract::context::stored(w, keychain_mask, slate.id)?; + let missing_signed_context = txs.iter().any(|tx| { + contract::utils::is_signed_tx(tx, slate.id) + && !contexts + .iter() + .any(|(_, ctx)| ctx.parent_key_id == tx.parent_key_id) + }); + let parent = context + .as_ref() + .map(|ctx| ctx.parent_key_id.clone()) + .unwrap_or(parent); + // Show only the selected party when both accounts are local + if txs.iter().any(|tx| tx.parent_key_id == parent) + || txs + .windows(2) + .any(|pair| pair[0].parent_key_id != pair[1].parent_key_id) + { + txs.retain(|tx| tx.parent_key_id == parent); + } let is_executed = txs.iter().any(|tx| tx.confirmed); let signed_tx = txs .iter() @@ -101,16 +117,17 @@ where } // Once we have signed, the context identifies the commitments we added. If it has // already been removed, the slate no longer carries enough information to do that. - let own_commitment_status = if slate.tx.is_none() || (context.is_none() && !txs.is_empty()) { - OwnCommitmentStatus::Unknown - } else { - contract::slate::own_commitment_status( - w, - keychain_mask, - slate, - if has_signed { context.as_ref() } else { None }, - )? - }; + let own_commitment_status = + if slate.tx.is_none() || missing_signed_context || (context.is_none() && !txs.is_empty()) { + OwnCommitmentStatus::Unknown + } else { + contract::slate::own_commitment_status( + w, + keychain_mask, + slate, + if has_signed { context.as_ref() } else { None }, + )? + }; // Count signatures present (a participant is "complete" once it has a partial sig). let num_sigs = slate .participant_data diff --git a/libwallet/src/contract/context.rs b/libwallet/src/contract/context.rs index 85dcf31..8b37ee8 100644 --- a/libwallet/src/contract/context.rs +++ b/libwallet/src/contract/context.rs @@ -26,6 +26,108 @@ use grin_core::core::FeeFields; use grin_keychain::{Identifier, Keychain}; use grin_util::secp::key::SecretKey; +// Keep the legacy key for the first party and qualify the second by account +fn account_key(slate_id: uuid::Uuid, parent: &Identifier) -> Vec { + let mut key = slate_id.as_bytes().to_vec(); + key.extend_from_slice(&parent.to_bytes()); + key +} + +pub(super) fn stored( + w: &WalletBackend, + mask: Option<&SecretKey>, + slate_id: uuid::Uuid, +) -> Result, Context)>, Error> { + let mut keys = vec![slate_id.as_bytes().to_vec()]; + keys.extend( + w.acct_path_iter()? + .map(|account| account_key(slate_id, &account.path)), + ); + let mut contexts = Vec::new(); + for key in keys { + match w.get_private_context(mask, &key) { + Ok(context) => contexts.push((key, context)), + Err(Error::NotFoundErr(_)) => {} + Err(e) => return Err(e), + } + } + Ok(contexts) +} + +pub(super) fn storage_key( + w: &WalletBackend, + mask: Option<&SecretKey>, + slate_id: uuid::Uuid, + parent: &Identifier, +) -> Result, Error> { + let contexts = stored(w, mask, slate_id)?; + if let Some((key, _)) = contexts + .iter() + .find(|(_, ctx)| &ctx.parent_key_id == parent) + { + return Ok(key.clone()); + } + if contexts.iter().any(|(key, _)| key == slate_id.as_bytes()) { + Ok(account_key(slate_id, parent)) + } else { + Ok(slate_id.as_bytes().to_vec()) + } +} + +/// Resolve the selected party, preserving retries after account switches +pub(super) fn resolve( + w: &WalletBackend, + mask: Option<&SecretKey>, + slate: &Slate, + parent: &Identifier, + requested_change: Option, +) -> Result, Error> { + let mut contexts = stored(w, mask, slate.id)?; + if let Some(index) = contexts + .iter() + .position(|(_, ctx)| &ctx.parent_key_id == parent) + { + return Ok(Some(contexts.swap_remove(index).1)); + } + // Never fall back to another party after context deletion + let mut parents = Vec::new(); + for tx in w.tx_log_iter()? { + let tx = tx?; + if tx.tx_slate_id == Some(slate.id) { + if &tx.parent_key_id == parent { + return Ok(None); + } + if !parents.contains(&tx.parent_key_id) { + parents.push(tx.parent_key_id); + } + } + } + if parents.len() > 1 { + return Err(Error::GenericError( + "Select an account participating in this contract".into(), + )); + } + match contexts.len() { + 0 => Ok(None), + 1 => { + let (_, context) = contexts.pop().unwrap(); + if slate.num_participants == 2 { + if let Some(change) = super::initial_net_change(&slate.state, slate.amount)? { + if change.signum() != context.get_net_change()?.signum() + && requested_change.is_none_or(|net| net.signum() == change.signum()) + { + return Ok(None); + } + } + } + Ok(Some(context)) + } + _ => Err(Error::GenericError( + "Select an account participating in this contract".into(), + )), + } +} + /// Get or create transaction Context for the given slate pub fn get_or_create( w: &mut WalletBackend, @@ -38,28 +140,27 @@ where K: Keychain, { debug!("contract::context::get_or_create => called"); - let maybe_context = w.get_private_context(keychain_mask, slate.id.as_bytes()); - - let context = match maybe_context { - // Only create a new context when there genuinely isn't one; propagate other errors. - Err(Error::NotFoundErr(_)) => { - // Get data required for creating a context + let parent_key_id = contract_utils::parent_key_for(w, setup_args.src_acct_name.as_deref())?; + let context = match resolve( + w, + keychain_mask, + slate, + &parent_key_id, + setup_args.net_change, + )? { + Some(context) => context, + None => { let height = w.w2n_client().get_chain_tip()?.0; - let parent_key_id = - contract_utils::parent_key_for(w, setup_args.src_acct_name.as_deref())?; self::create( w, keychain_mask, slate, height, - // &args, setup_args, &parent_key_id, false, )? } - Err(e) => return Err(e), - Ok(ctx) => ctx, }; Ok(context) } diff --git a/libwallet/src/contract/slate.rs b/libwallet/src/contract/slate.rs index 30619be..46b494b 100644 --- a/libwallet/src/contract/slate.rs +++ b/libwallet/src/contract/slate.rs @@ -247,7 +247,7 @@ where Inputs::CommitOnly(_) => return Ok(OwnCommitmentStatus::Unknown), Inputs::FeaturesAndCommit(inputs) => inputs, }; - let (expected_inputs, expected_outputs) = match signed_context { + let (mut expected_inputs, mut expected_outputs) = match signed_context { Some(context) => { let (inputs, outputs) = expected_identifiers(w, &keychain, context)?; ( @@ -263,6 +263,37 @@ where } None => (BTreeSet::new(), BTreeSet::new()), }; + // Allow only stored local contributions backed by a valid partial signature + for (_, context) in super::context::stored(w, keychain_mask, slate.id)? { + let Some(log_id) = context.log_id else { + continue; + }; + let Some(tx_log) = w.get_tx_log_entry_by_id(&context.parent_key_id, log_id)? else { + continue; + }; + if !super::utils::is_signed_tx(&tx_log, slate.id) + || !matches!( + tx_log.tx_type, + crate::TxLogEntryType::TxSent + | crate::TxLogEntryType::TxReceived + | crate::TxLogEntryType::TxSelfSpend + ) { + continue; + } + let index = match slate.find_index_matching_context(&keychain, &context) { + Ok(index) => index, + Err(Error::ContextToIndex) => continue, + Err(e) => return Err(e), + }; + if !slate.participant_data[index].is_complete() { + continue; + } + slate.verify_part_sigs(keychain.secp())?; + verify_own_commitments(w, keychain_mask, slate, &context)?; + let (inputs, outputs) = expected_identifiers(w, &keychain, &context)?; + expected_inputs.extend(inputs.into_iter().map(|input| input.commitment())); + expected_outputs.extend(outputs.into_iter().map(|output| output.commitment())); + } let incoming_inputs = inputs .into_iter() .map(|input| input.commitment()) diff --git a/libwallet/src/contract/utils.rs b/libwallet/src/contract/utils.rs index e46b37d..db7295a 100644 --- a/libwallet/src/contract/utils.rs +++ b/libwallet/src/contract/utils.rs @@ -271,6 +271,7 @@ where }; // Phase 2 - atomically update Context, OutputData and TxLogEntry + let context_key = super::context::storage_key(w, keychain_mask, slate.id, parent_key_id)?; let mut batch = w.batch(keychain_mask)?; // Update TxLogEntry @@ -300,17 +301,17 @@ where // Update context if is_signed && !is_step2 { // Forget the signing context in the same batch as the transaction log update - batch.delete_private_context(slate.id.as_bytes())?; + batch.delete_private_context(&context_key)?; } else { // Keep unsigned contexts for retries and the step2 context for contract view - batch.save_private_context(slate.id.as_bytes(), &context)?; + batch.save_private_context(&context_key, &context)?; } batch.commit()?; // Confirm sec_key/sec_nonce are gone after signing, except for step2 if is_signed && !is_step2 { - match w.get_private_context(keychain_mask, slate.id.as_bytes()) { + match w.get_private_context(keychain_mask, &context_key) { Err(Error::NotFoundErr(_)) => {} Err(e) => return Err(e), Ok(_) => { @@ -424,7 +425,11 @@ pub(super) fn is_signed_tx(tx: &TxLogEntry, slate_id: Uuid) -> bool { /// Returns an error if the slate has already been signed (in our local database). Even if the /// result is Ok, it's still possible it was signed but we don't have the data about it locally. -pub fn verify_not_signed(w: &mut WalletBackend, slate_id: Uuid) -> Result<(), Error> +pub fn verify_not_signed( + w: &mut WalletBackend, + slate_id: Uuid, + parent: &Identifier, +) -> Result<(), Error> where C: NodeClient, K: Keychain, @@ -433,7 +438,7 @@ where // we have already signed this slate. for tx in w.tx_log_iter()? { let tx = tx?; - if is_signed_tx(&tx, slate_id) { + if &tx.parent_key_id == parent && is_signed_tx(&tx, slate_id) { debug!("contract::utils::verify_not_signed => The slate has already been signed."); return Err(Error::GenericError(format!( "Slate with id:{} has already been signed.",