Files
grin-wallet/libwallet/src/contract/context.rs
T
jwinterm 78aa41c77e Contracts: return errors for the remaining reachable panics
- 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.
2026-07-27 08:49:44 -04:00

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(())
}