validate contract participant count

This commit is contained in:
wiesche
2026-09-04 21:35:36 +02:00
parent 7bd69374af
commit 93881dfb35
9 changed files with 118 additions and 25 deletions
-1
View File
@@ -39,7 +39,6 @@ Separating side effects until the 'save_step' part would make these functions mu
- Reserve funds across open late-locked contracts, or make the possible signing failure
clearer to callers.
- Keep contracts at two participants until the known multi-party attack is addressed.
- For a payjoin, `--use-inputs any` picks an input for us; naming the commitment would be
clearer, for the API as much as the CLI.
- `new` has no `--no-setup`.
+1
View File
@@ -38,6 +38,7 @@ where
C: NodeClient,
K: Keychain,
{
contract::utils::verify_num_participants(setup_args.num_participants)?;
// Compute state for 'new'
let (slate, context) = compute(w, keychain_mask, setup_args, slate_id)?;
+7
View File
@@ -35,6 +35,13 @@ where
C: NodeClient,
K: Keychain,
{
contract::utils::verify_num_participants(slate.num_participants)?;
if slate.num_participants != setup_args.num_participants {
return Err(Error::GenericError(format!(
"Inconsistent num_participants. Slate num_participants:{}, Setup num_participants: {}",
slate.num_participants, setup_args.num_participants
)));
}
contract::utils::verify_not_signed(w, slate.id)?;
// Compute state for 'setup'
let (slate, context) = compute(w, keychain_mask, slate, setup_args)?;
+1
View File
@@ -58,6 +58,7 @@ where
C: NodeClient,
K: Keychain,
{
contract::utils::verify_num_participants(slate.num_participants)?;
// 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()) {
+1 -7
View File
@@ -75,13 +75,7 @@ where
"Cannot view a slate with an Unknown state".to_string(),
));
}
// Mirror the contract setup bound so a tampered slate can't surface a bogus count.
if slate.num_participants < 1 || slate.num_participants > 2 {
return Err(Error::GenericError(format!(
"Unsupported num_participants: {} (expected 1 or 2)",
slate.num_participants
)));
}
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,
+1 -11
View File
@@ -79,17 +79,7 @@ where
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
)));
}
contract_utils::verify_num_participants(setup_args.num_participants)?;
// sender should always refresh outputs
updater::refresh_outputs(w, keychain_mask, parent_key_id, false)?;
+31
View File
@@ -346,6 +346,7 @@ pub fn my_fee_contribution(
n_kernels: usize,
num_participants: u8,
) -> Result<FeeFields, Error> {
verify_num_participants(num_participants)?;
// 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
@@ -362,6 +363,18 @@ pub fn my_fee_contribution(
Ok(my_fee_fields)
}
/// Contracts need at least one participant for fee splitting
/// More than two remain disabled because of the known multi-party attack
pub(super) fn verify_num_participants(num_participants: u8) -> Result<(), Error> {
if !(1..=2).contains(&num_participants) {
return Err(Error::GenericError(format!(
"Unsupported num_participants: {} (expected 1 or 2)",
num_participants
)));
}
Ok(())
}
/// Whether a transaction log entry shows that this wallet signed the slate.
pub(super) fn is_signed_tx(tx: &TxLogEntry, slate_id: Uuid) -> bool {
tx.tx_slate_id == Some(slate_id) && tx.kernel_excess.is_some()
@@ -469,4 +482,22 @@ mod tests {
// ... and applying the same split, the participants together cover the kernel cost.
assert!(2 * my_share >= kernel_cost);
}
#[test]
fn participant_count() {
assert!(verify_num_participants(1).is_ok());
assert!(verify_num_participants(2).is_ok());
for count in [0, 3] {
let err = verify_num_participants(count).unwrap_err();
assert!(matches!(
err,
Error::GenericError(ref msg)
if msg == &format!(
"Unsupported num_participants: {} (expected 1 or 2)",
count
)
));
assert!(my_fee_contribution(1, 1, 1, count).is_err());
}
}
}