Files
grin-wallet/libwallet/src/contract/utils.rs
T
YeastplumeandGitHub febffd4c68 [Contracts] Add self spend transaction state (#697)
* Add self spend transaction state

* subtle errors with output states and tx lookups - fixes
2023-10-19 11:04:44 +01:00

399 lines
14 KiB
Rust

// 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.
//! Contract building utility functions
use crate::contract::selection::verify_selection_consistency;
use crate::contract::types::ContractSetupArgsAPI;
use crate::grin_core::libtx::tx_fee;
use crate::grin_keychain::{Identifier, Keychain};
use crate::grin_util::secp::key::SecretKey;
use crate::slate::Slate;
use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType, WalletBackend};
use crate::util::OnionV3Address;
use crate::{address, Error, OutputData, OutputStatus, TxLogEntry};
use grin_core::core::FeeFields;
use uuid::Uuid;
/// Creates an initial TxLogEntry without input/output or kernel information
pub fn create_tx_log_entry(
slate: &Slate,
net_change: i64,
parent_key_id: Identifier,
log_id: u32,
) -> Result<TxLogEntry, Error> {
let log_type = if slate.num_participants == 1 {
TxLogEntryType::TxSelfSpend
} else {
if net_change > 0 {
TxLogEntryType::TxReceived
} else {
TxLogEntryType::TxSent
}
};
let mut t = TxLogEntry::new(parent_key_id.clone(), log_type, log_id);
// TODO: TxLogEntry has stored_tx field. Check what this needs to be set to and check other fields as well
t.tx_slate_id = Some(slate.id);
if net_change > 0 {
t.amount_credited = net_change as u64;
} else {
t.amount_debited = -net_change as u64;
}
t.ttl_cutoff_height = match slate.ttl_cutoff_height {
0 => None,
n => Some(n),
};
Ok(t)
}
/// Updates TxLogEntry for a contract with information available in the 'sign' step
pub fn update_tx_log_entry<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
context: &Context,
tx_log_entry: &mut TxLogEntry,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// This is expected to be called when we are signing the contract and have already contributed inputs & outputs
let keychain = wallet.keychain(keychain_mask)?;
let parent_key_id = context.parent_key_id.clone();
let current_height = wallet.w2n_client().get_chain_tip()?.0;
// We have already contributed inputs and outputs so we know how much of each we contribute
tx_log_entry.num_outputs = context.output_ids.len();
tx_log_entry.num_inputs = context.input_ids.len();
tx_log_entry.fee = context.fee;
// Set kernel information
match slate.calc_excess(keychain.secp()) {
Ok(e) => tx_log_entry.kernel_excess = Some(e),
Err(_) => panic!("We can't update tx log entry. Excess could not be computed."),
};
tx_log_entry.kernel_lookup_min_height = Some(current_height);
// If we're sending and there's payment proof info in the slate added by recipient, store as well
if let Some(ref p) = slate.payment_proof {
if tx_log_entry.amount_debited > 0 {
// note we only use a single path for now
let sender_address_path = 0u32;
let sender_key = address::address_from_derivation_path(
&keychain,
&parent_key_id,
sender_address_path,
)?;
let sender_address = OnionV3Address::from_private(&sender_key.0)?;
// We're looking for the OTHER party here, the recipient
let sender_index = slate.find_index_matching_context(&keychain, context)?;
let recipient_index = sender_index ^ 1;
tx_log_entry.payment_proof = Some(StoredProofInfo {
receiver_address: p.receiver_address,
receiver_signature: p.promise_signature,
sender_address: sender_address.to_ed25519()?,
sender_address_path,
sender_signature: None,
/// TODO: Will fill these as separate steps for now, check whether this
/// can be merged in a general case (which means knowing which nonces here belong to
/// the recipient)
proof_type: Some(1u8),
receiver_public_nonce: Some(slate.participant_data[recipient_index].public_nonce),
receiver_public_excess: Some(
slate.participant_data[recipient_index].public_blind_excess,
),
timestamp: Some(p.timestamp),
memo: p.memo.clone(),
promise_signature: p.promise_signature,
sender_part_sig: slate.participant_data[sender_index].part_sig,
});
}
}
Ok(())
}
/// Get net_change value. This is obtained either from the Context.net_change or the setup_args.net_change
pub fn get_net_change<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
// TODO: make this receive only slate.id instead of passing the whole slate
slate: &Slate,
setup_args_net_change: Option<i64>,
) -> Result<i64, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut expected_net_change: Option<i64> = setup_args_net_change;
match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
Ok(ctx) => {
debug!("contract::sign => context found");
// We have a context so we must have agreed on a certain net_change value in Context.net_change.
// If we have both Context.net_change and setup_args.net_change, then they must be equal.
match expected_net_change {
Some(args_net_change) => {
if ctx.get_net_change() != args_net_change {
panic!(
"Expected net change mismatch! Context.net_change: {}, setup_args.net_change: {}",
ctx.get_net_change(), args_net_change
);
}
}
None => (),
}
expected_net_change = Some(ctx.get_net_change());
}
Err(_) => debug!("contract::utils::get_net_change => context not found"),
};
// Fail if net_change was not passed to setup_args and was also not present in the context.
// This means it has not been explicitly agreed on and we require the user to pass it.
if expected_net_change.is_none() {
return Err(Error::GenericError(
"You did not agree on the expected net difference.".into(),
)
.into());
}
debug!(
"contract::utils::get_net_change => expected_net_change: {}",
expected_net_change.unwrap()
);
Ok(expected_net_change.unwrap())
}
/// Atomically locks the inputs and saves the changes of Context, TxLogEntry and OutputData.
/// Additionally, the transaction is saved in a file in case we signed it.
pub fn save_step<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
context: &mut Context,
step_added_outputs: bool,
is_signed: bool,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
debug!(
"contract::utils::save_step => performing atomic update for slate_id: {}",
slate.id
);
// Phase 1 - precompute the data needed for atomic update
let parent_key_id = &context.parent_key_id;
let current_height = w.w2n_client().get_chain_tip()?.0;
// We are at step2 if we don't have context.log_id and we have signed the slate
let is_step2 = !context.log_id.is_some() && is_signed;
let mut tx_log_entry = {
if !context.log_id.is_some() {
// We create a new entry with log_id=0 and but replace it with the real id before committing
create_tx_log_entry(slate, context.get_net_change(), parent_key_id.clone(), 0)?
} else {
w.get_tx_log_entry(parent_key_id.clone(), context.log_id.unwrap())?
.unwrap()
}
};
// Update TxLogEntry if we have signed the contract (we have data about the kernel)
if is_signed {
update_tx_log_entry(w, keychain_mask, &slate, &context, &mut tx_log_entry)?;
// TODO: It's possible to store the transaction in a file while and the atomic commit below fails
// In this case, we should revert to the previous stored tx to avoid having discrepancy
w.store_tx(&format!("{}", slate.id), slate.tx_or_err()?)?;
}
// If we added outputs in this step, we have to create OutputData here because 'batch'
// takes the mutable ref and we can no longer call calc_commit_for_cache for output
let added_outputs = if !step_added_outputs {
vec![]
} else {
let mut output_data_xs: Vec<OutputData> = vec![];
// Create an OutputData entry for every created output
for (key_id, _, amount) in context.get_outputs() {
let commit = w.calc_commit_for_cache(keychain_mask, amount, &key_id)?;
let output_data = OutputData {
root_key_id: parent_key_id.clone(),
key_id: key_id.clone(),
mmr_index: None,
n_child: key_id.to_path().last_path_index(),
commit: commit,
value: amount,
status: OutputStatus::Unconfirmed,
height: current_height,
lock_height: 0,
is_coinbase: false,
tx_log_entry: None,
};
output_data_xs.push(output_data);
}
output_data_xs
};
// Phase 2 - atomically update Context, OutputData and TxLogEntry
let mut batch = w.batch(keychain_mask)?;
// Update TxLogEntry
if !context.log_id.is_some() {
// If we just created the TxLogEntry, we have to assign it an id
let log_id = batch.next_tx_log_id(&parent_key_id)?;
tx_log_entry.id = log_id;
context.log_id = Some(log_id);
}
batch.save_tx_log_entry(tx_log_entry.clone(), &parent_key_id)?;
// Create OutputData entries and lock inputs if we added outputs at this step
if step_added_outputs {
// Create an OutputData entry for every created output
for mut output_data in added_outputs {
output_data.tx_log_entry = context.log_id;
batch.save(output_data)?;
}
// Lock inputs
for id in context.get_inputs() {
let mut coin = batch.get(&id.0, &id.1).unwrap();
// At this point we already have context.log_id set
coin.tx_log_entry = context.log_id;
batch.lock_output(&mut coin)?;
}
}
// Update context
if is_signed && !is_step2 {
// NOTE: We MUST forget the context when we sign. Ideally, these two would be atomic or perhaps
// when we call slate::sigadd_partial_signaturen we could swap the secret key with a temporary one just to be safe.
// The reason we don't delete if we are at step2 is because in case we want to do safe cancel,
// we need to know which inputs are in the context to know which input we have to double-spend.
batch.delete_private_context(slate.id.as_bytes())?;
} else {
batch.save_private_context(slate.id.as_bytes(), &context)?;
}
batch.commit()?;
// TODO: Assert we don't have the context to avoid potentially leaking it! Also write tests around this.
debug!("contract::utils::save_step => Atomic updated done");
Ok(())
}
/// Computes fees contribution for a participant
pub fn my_fee_contribution(
n_inputs: usize,
n_outputs: usize,
n_kernels: usize,
num_participants: u8,
) -> Result<FeeFields, Error> {
// Add our fee costs for our inputs and a single output
let mut fee = tx_fee(n_inputs, n_outputs, 0);
// Add out fee costs for kernel. We pay 1/num_participants of a kernel cost
let kernel_cost = tx_fee(0, 0, n_kernels);
// TODO: we slightly overpay. Make sure to cover all the cases
let my_kernel_cost = (kernel_cost as f64 / (num_participants as f64)).ceil();
fee += my_kernel_cost as u64;
// Add my fee contribution to the slate total fee.
// TODO: Does this break compatibility with existing slates?
let my_fee_fields = FeeFields::new(0, fee)?;
Ok(my_fee_fields)
}
/// 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<'a, T: ?Sized, C, K>(w: &mut T, slate_id: Uuid) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// If we have a transaction log entry for that slatepack that has a kernel value, then
// we have already signed this slate.
let tx = w
.tx_log_iter()
.find(|t| t.tx_slate_id.is_some() && t.tx_slate_id.unwrap() == slate_id);
let already_signed = tx.is_some() && tx.unwrap().kernel_excess.is_some();
if already_signed {
debug!("contract::utils::verify_not_signed => The slate has already been signed.");
return Err(Error::GenericError(
format!("Slate with id:{} has already been signed.", slate_id).into(),
)
.into());
}
Ok(())
}
/// Compares the setup args provided at call with those in the Context and checks whether they conflict.
/// This is relevant to see if there's any conflict in the arguments provided at step1 with step3.
pub fn verify_setup_args_consistency(
ctx_setup_args: &ContractSetupArgsAPI,
cur_setup_args: &ContractSetupArgsAPI,
) -> Result<(), Error> {
// Compare net_change
if ctx_setup_args.net_change.unwrap() != cur_setup_args.net_change.unwrap() {
panic!(
"Inconsistent net change. Ctx net_change:{}, Current net_change: {}",
ctx_setup_args.net_change.unwrap(),
cur_setup_args.net_change.unwrap()
);
}
// Compare num_participants
if ctx_setup_args.num_participants != cur_setup_args.num_participants {
panic!(
"Inconsistent num_participants. Ctx num_participants:{}, Current num_participants: {}",
ctx_setup_args.num_participants, cur_setup_args.num_participants
);
}
// TODO: Should we verify add_outputs?
// TODO: verify that the parent_key_id is consistent, perhaps even with the active_account set?
// Compare OutputSelectionArgs
verify_selection_consistency(
&ctx_setup_args.selection_args,
&cur_setup_args.selection_args,
)?;
Ok(())
}
/// Get the parent_key_id for a given wallet instance and src_acct_name
pub fn parent_key_for<'a, T: ?Sized, C, K>(w: &mut T, src_acct_name: Option<&String>) -> Identifier
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// TODO: does it matter what api.set_active_account is set? also check LMDB set_parent_key_id etc. methods
// - Does it matter what api.set_active_account is set? I think w.parent_key_id() already takes the active one
// but the verify_consistency may need to verify this or perhaps give a warning that active is different than
// the one that was set at the first setup phase.
let parent_key_id = match src_acct_name {
Some(d) => {
let pm = w.get_acct_path(d.clone()).unwrap();
match pm {
Some(p) => p.path,
// TODO: should we error if the path is not found?
None => w.parent_key_id(),
}
}
None => w.parent_key_id(),
};
parent_key_id
}