# VAL-003: Reject Duplicate Spends

> **Authoritative requirement:** [VAL-003](../NORMATIVE.md#VAL-003)
> **Verification:** [VERIFICATION.md](../VERIFICATION.md)
> **Tracking:** [TRACKING.yaml](../TRACKING.yaml)
> **Spec reference:** [SPEC.md](../../../../resources/SPEC.md) — Section 6.1

---

## Summary

A spend bundle may contain multiple `CoinSpend` entries, each specifying a coin to be spent along with its puzzle reveal and solution. A well-formed bundle MUST NOT contain two or more `CoinSpend` entries that reference the same coin (same `coin_id()`). If a duplicate is detected, the entire bundle MUST be rejected with `ValidationError::DoubleSpend`.

This check is one of the structural pre-validation steps that occurs before CLVM execution (step 1 of the validation flow in SPEC Section 6.1). Performing it early avoids wasting CLVM execution cost on a bundle that is fundamentally invalid. On Chia L1, this check is performed implicitly during mempool admission in [`add_spend_bundle()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool_manager.py#L480) where the mempool tracks which coins are currently being spent, and during block validation in [`validate_block_body()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py#L191) where removals are checked for uniqueness.

The duplicate check is important for security because allowing the same coin to appear twice in a bundle could lead to double-counting of the removal amount in the conservation check, potentially allowing value to be created from nothing.

---

## Specification

### Algorithm

Before calling `run_spendbundle()`, `validate_spend_bundle()` MUST:

1. Initialize an empty `HashSet<Bytes32>` for tracking seen coin IDs.
2. Iterate over `bundle.coin_spends`.
3. For each `CoinSpend`, compute `coin_id = coin_spend.coin.coin_id()`.
4. Attempt to insert `coin_id` into the set.
5. If the insertion fails (the ID was already present), immediately return `Err(ValidationError::DoubleSpend(coin_id))`.

```rust
fn check_no_duplicate_spends(bundle: &SpendBundle) -> Result<(), ValidationError> {
    let mut seen = HashSet::with_capacity(bundle.coin_spends.len());
    for cs in &bundle.coin_spends {
        let coin_id = cs.coin.coin_id();
        if !seen.insert(coin_id) {
            return Err(ValidationError::DoubleSpend(coin_id));
        }
    }
    Ok(())
}
```

### Error Details

```rust
#[error("Double spend in bundle: {0}")]
DoubleSpend(Bytes32),
```

The `Bytes32` payload is the coin ID that appears more than once, enabling the caller to identify which coin caused the rejection.

### Ordering

This check MUST execute before CLVM execution (step 2 in the validation flow). It is part of the structural checks in step 1, alongside coin existence (VAL-004) and already-spent checks (VAL-005).

---

## Acceptance Criteria

- [ ] A bundle containing two `CoinSpend` entries with the same `coin_id()` is rejected with `ValidationError::DoubleSpend`.
- [ ] The `DoubleSpend` error contains the coin ID of the duplicated coin.
- [ ] A bundle with all unique coin IDs passes this check (no false positives).
- [ ] The duplicate check occurs before CLVM execution (no CLVM cost is consumed on duplicate bundles).
- [ ] A bundle with three or more duplicates of the same coin is still rejected (the first duplicate triggers the error).
- [ ] A bundle with two different coins that each appear twice is rejected on the first duplicate encountered.

---

## Implementation Notes

- **Primary file:** `src/consensus/validate.rs`
- **Position in flow:** First structural check, before coin existence and CLVM execution.
- **Performance:** Using `HashSet::with_capacity(bundle.coin_spends.len())` avoids rehashing for typical bundle sizes.
- **coin_id() computation:** `Coin::coin_id()` is `sha256(parent_coin_info || puzzle_hash || amount)`. This comes from `chia-protocol`.
- **Dependencies:** No other VAL requirements are needed for this check. It depends only on the `SpendBundle` input.
- **Edge case:** A bundle with zero or one `CoinSpend` entries trivially passes this check.
- **Edge case:** Two different coins that happen to produce the same `coin_id()` would be caught, but this is cryptographically impossible (SHA-256 collision).

---

## Verification

### Test Plan

| Test | Type | Description | Expected Result |
|------|------|-------------|-----------------|
| Duplicate coin in bundle | Simulator test | Build a bundle with the same coin spent twice (identical `CoinSpend` entries) | `Err(ValidationError::DoubleSpend(coin_id))` |
| Same coin, different solutions | Simulator test | Build a bundle with the same coin but different solutions | `Err(ValidationError::DoubleSpend(coin_id))` (coin ID is the same regardless of solution) |
| No duplicates passes | Simulator test | Build a bundle with two distinct coins | Passes this check (may fail on other checks if coins are invalid) |
| Single-spend bundle passes | Unit test | Build a bundle with one `CoinSpend` | Passes this check |
| Empty bundle passes | Unit test | Build a bundle with zero `CoinSpend` entries | Passes this check |
| Error payload correct | Simulator test | Trigger `DoubleSpend`, inspect the contained `Bytes32` | Matches the `coin_id()` of the duplicated coin |

---

## Source Citations

- **SPEC.md Section 6.1** (line 640): Step 1 structural checks: "Check for duplicate spends in bundle."
- **SPEC.md Section 5.1** (line 541): `ValidationError::DoubleSpend(Bytes32)` definition.
- **L1 reference — `validate_block_body()`:** [`block_body_validation.py:191`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py#L191) — removals checked for uniqueness.
- **L1 reference — `add_spend_bundle()`:** [`mempool_manager.py:480`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool_manager.py#L480) — mempool tracks spent coins.

---

## References

- [VAL-001](VAL-001.md) — Entry point signature (orchestrates this check)
- [VAL-004](VAL-004.md) — Missing coin rejection (next structural check)
- [VAL-005](VAL-005.md) — Already-spent coin rejection (next structural check)
- [VAL-011](VAL-011.md) — Conservation check (duplicate coins could break conservation accounting)
