mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-10-17 15:21:39 +02:00
contracts between local accounts
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<Context, Error> {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<u8> {
|
||||
let mut key = slate_id.as_bytes().to_vec();
|
||||
key.extend_from_slice(&parent.to_bytes());
|
||||
key
|
||||
}
|
||||
|
||||
pub(super) fn stored<C: NodeClient, K: Keychain>(
|
||||
w: &WalletBackend<C, K>,
|
||||
mask: Option<&SecretKey>,
|
||||
slate_id: uuid::Uuid,
|
||||
) -> Result<Vec<(Vec<u8>, 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<C: NodeClient, K: Keychain>(
|
||||
w: &WalletBackend<C, K>,
|
||||
mask: Option<&SecretKey>,
|
||||
slate_id: uuid::Uuid,
|
||||
parent: &Identifier,
|
||||
) -> Result<Vec<u8>, 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<C: NodeClient, K: Keychain>(
|
||||
w: &WalletBackend<C, K>,
|
||||
mask: Option<&SecretKey>,
|
||||
slate: &Slate,
|
||||
parent: &Identifier,
|
||||
requested_change: Option<i64>,
|
||||
) -> Result<Option<Context>, 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<C, K>(
|
||||
w: &mut WalletBackend<C, K>,
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<C, K>(w: &mut WalletBackend<C, K>, slate_id: Uuid) -> Result<(), Error>
|
||||
pub fn verify_not_signed<C, K>(
|
||||
w: &mut WalletBackend<C, K>,
|
||||
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.",
|
||||
|
||||
Reference in New Issue
Block a user