mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-10-17 15:21:39 +02:00
- Context::get_net_change unwrapped setup_args, which is None for the standard flows that share Context, so a non-contract context reaching a contract path terminated the wallet. It returns a Result now. - A recomputed fee that no longer matches the stored one was an assert_eq, which aborts; report both values instead. - Selection used abs(), which panics on i64::MIN, and unchecked addition for the amount equation. - build_output_amount_list subtracted the custom output sum from the input sum as u64, before the checked conversion that was meant to guard it. A receiver without a payjoin contributes no inputs while --make-outputs is still allowed, so that underflows. The balance is evaluated in checked i64 now, with a test for the no-payjoin case.
232 lines
7.9 KiB
Rust
232 lines
7.9 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 functions on the Context
|
|
|
|
use crate::backend::WalletBackend;
|
|
use crate::contract::selection::prepare_outputs;
|
|
use crate::contract::types::ContractSetupArgsAPI;
|
|
use crate::contract::utils as contract_utils;
|
|
use crate::grin_keychain::{Identifier, Keychain};
|
|
use crate::grin_util::secp::key::SecretKey;
|
|
use crate::internal::updater;
|
|
use crate::slate::Slate;
|
|
use crate::types::{Context, NodeClient};
|
|
use crate::{Error, OutputData};
|
|
use grin_core::core::FeeFields;
|
|
|
|
/// Get or create transaction Context for the given slate
|
|
pub fn get_or_create<C, K>(
|
|
w: &mut WalletBackend<C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
slate: &mut Slate,
|
|
setup_args: &ContractSetupArgsAPI,
|
|
) -> Result<Context, Error>
|
|
where
|
|
C: NodeClient,
|
|
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 height = w.w2n_client().get_chain_tip()?.0;
|
|
let parent_key_id =
|
|
contract_utils::parent_key_for(w, setup_args.src_acct_name.as_ref())?;
|
|
self::create(
|
|
w,
|
|
keychain_mask,
|
|
slate,
|
|
height,
|
|
// &args,
|
|
setup_args,
|
|
&parent_key_id,
|
|
false,
|
|
)?
|
|
}
|
|
Err(e) => return Err(e),
|
|
Ok(ctx) => ctx,
|
|
};
|
|
Ok(context)
|
|
}
|
|
|
|
/// Creates a context for a contract
|
|
fn create<C, K>(
|
|
w: &mut WalletBackend<C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
slate: &mut Slate,
|
|
current_height: u64,
|
|
setup_args: &ContractSetupArgsAPI,
|
|
parent_key_id: &Identifier,
|
|
use_test_rng: bool,
|
|
) -> Result<Context, Error>
|
|
where
|
|
C: NodeClient,
|
|
K: Keychain,
|
|
{
|
|
debug!("Creating a new contract context");
|
|
// Validate num_participants before any fee math (kernel cost is divided by it, so 0
|
|
// would be a divide-by-zero). This is the common setup path for every party (the
|
|
// signer's value comes from the slate), so it also guards against a tampered slate.
|
|
// Only 1 (self-spend) and 2 (standard) are implemented today; lift this when
|
|
// multi-party contracts are supported.
|
|
if setup_args.num_participants < 1 || setup_args.num_participants > 2 {
|
|
return Err(Error::GenericError(format!(
|
|
"Unsupported num_participants: {} (expected 1 or 2)",
|
|
setup_args.num_participants
|
|
)));
|
|
}
|
|
// sender should always refresh outputs
|
|
updater::refresh_outputs(w, keychain_mask, parent_key_id, false)?;
|
|
|
|
// Fee contribution estimation
|
|
let net_change = setup_args.net_change.ok_or_else(|| {
|
|
Error::GenericError("Contract requires a net change (--send or --receive)".to_string())
|
|
})?;
|
|
// select inputs to estimate fee cost
|
|
let (inputs, _, my_fee) =
|
|
prepare_outputs(w, &parent_key_id, current_height, &setup_args, None)?;
|
|
// The number of outputs we expect is the number of custom outputs plus one change output
|
|
debug!(
|
|
"My fee contribution estimation: {} for n_inputs: {}, n_outputs: {}, n_kernels: {}, num_participants: {}",
|
|
my_fee.fee(), inputs.len(), setup_args.selection_args.num_custom_outputs() + 1, 1, setup_args.num_participants
|
|
);
|
|
// Make sure `my_fee < net_change` holds for the receiver. This can't be true for a self-spend, because nobody
|
|
// has a net_change > 0 which makes a self-spend ok to be a net negative when fees are included.
|
|
if net_change > 0 && my_fee.fee() > net_change.abs() as u64 {
|
|
return Err(Error::GenericError(format!(
|
|
"My contribution as a receiver would be net negative. my_fee: {}, net_change: {}",
|
|
my_fee.fee(),
|
|
net_change
|
|
)));
|
|
}
|
|
// Add my share of fee contribution to the slate fees
|
|
slate.fee_fields = FeeFields::new(0, slate.fee_fields.fee() + my_fee.fee())?;
|
|
debug!("Slate.fee: {}", slate.fee_fields.fee());
|
|
|
|
// Create a Context for this slate
|
|
let keychain = w.keychain(keychain_mask)?;
|
|
// is_initiator only affects the deterministic test RNG; contracts always pass true.
|
|
let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, true);
|
|
// Context.fee will hold _our_ fee contribution and not the total slate fee
|
|
context.fee = my_fee.as_opt();
|
|
// Context.amount is not used in contracts, but we set it anyway.
|
|
context.amount = slate.amount;
|
|
// Store the full setup args on the context; later steps (add_outputs) read the
|
|
// SelectionArgs and ttl from here.
|
|
context.setup_args = Some(setup_args.clone());
|
|
debug!(
|
|
"Setting Context.net_change as: {}",
|
|
context.get_net_change()?
|
|
);
|
|
|
|
Ok(context)
|
|
}
|
|
|
|
/// Add outputs to a contract context (including spent outputs which get locked)
|
|
pub fn add_outputs<C, K>(
|
|
w: &mut WalletBackend<C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
context: &mut Context,
|
|
) -> Result<(), Error>
|
|
where
|
|
C: NodeClient,
|
|
K: Keychain,
|
|
{
|
|
debug!("contract::utils::add_outputs => called");
|
|
// Do nothing if we have already contributed our outputs. The assumption is that if this was done,
|
|
// our output contribution is complete.
|
|
if context.output_ids.len() > 0 || context.input_ids.len() > 0 {
|
|
debug!("contract::utils::add_outputs => outputs have already been added, returning.");
|
|
return Ok(());
|
|
}
|
|
let setup_args = context
|
|
.setup_args
|
|
.as_ref()
|
|
.ok_or_else(|| Error::GenericError("Context carries no contract setup args".to_string()))?;
|
|
debug!("contract::utils::add_outputs => adding outputs");
|
|
let current_height = w.w2n_client().get_chain_tip()?.0;
|
|
let parent_key_id = &context.parent_key_id;
|
|
|
|
// Select inputs for which `Σmy_inputs >= Σmy_outputs + my_fee_cost` holds. Uses committed fee if present.
|
|
let (inputs, my_output_amounts, my_fee) = prepare_outputs(
|
|
&mut *w,
|
|
parent_key_id,
|
|
current_height,
|
|
&setup_args,
|
|
context.fee,
|
|
)?;
|
|
// An old or damaged context, or a future change to the fee schedule, can reach this
|
|
// with a stored fee that no longer matches what we just recomputed. Report both
|
|
// values instead of terminating the wallet.
|
|
let ctx_fee = context
|
|
.fee
|
|
.ok_or_else(|| Error::GenericError("Context carries no fee".to_string()))?;
|
|
if my_fee.fee() != ctx_fee.fee() {
|
|
return Err(Error::GenericError(format!(
|
|
"Recomputed fee {} does not match the fee stored in the context {}",
|
|
my_fee.fee(),
|
|
ctx_fee.fee()
|
|
)));
|
|
}
|
|
// Add selected/created inputs/outputs to the context
|
|
add_inputs_to_ctx(context, &inputs)?;
|
|
add_outputs_to_ctx(w, keychain_mask, context, my_output_amounts)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Add inputs to Context
|
|
fn add_inputs_to_ctx(context: &mut Context, inputs: &Vec<OutputData>) -> Result<(), Error> {
|
|
debug!("contract::utils::add_inputs_to_ctx => adding inputs to context");
|
|
for input in inputs {
|
|
context.add_input(&input.key_id, &input.mmr_index, input.value);
|
|
debug!(
|
|
"contract::utils::add_inputs_to_ctx => input id: {}, value:{}",
|
|
&input.key_id, input.value
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Add outputs to Context
|
|
fn add_outputs_to_ctx<C, K>(
|
|
w: &mut WalletBackend<C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
context: &mut Context,
|
|
amounts: Vec<u64>,
|
|
) -> Result<(), Error>
|
|
where
|
|
C: NodeClient,
|
|
K: Keychain,
|
|
{
|
|
// Derive under the contract context's account, not the active account: a late-lock
|
|
// may have a different account active than the one set on the Context.
|
|
let parent_key_id = context.parent_key_id.clone();
|
|
for amount in amounts {
|
|
let key_id = w.next_child_for(&parent_key_id, keychain_mask)?;
|
|
context.add_output(&key_id, &None, amount);
|
|
debug!(
|
|
"contract::utils::add_output_to_ctx => added output to context. Output id: {}, amount: {}",
|
|
key_id.clone(),
|
|
amount
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|