# VAL-005: Reject Already-Spent Coins

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

---

## Summary

The UTXO (Unspent Transaction Output) model requires that each coin can only be spent once. Once a coin has been spent and its removal committed to blockchain state, any subsequent attempt to spend that coin must be rejected. The `CoinRecord` type (from `chia-sdk-coinset`) includes a `spent` boolean field that indicates whether the coin has already been consumed. `validate_spend_bundle()` MUST check this field for every coin being spent and reject bundles that attempt to spend already-spent coins.

On Chia L1, the coin store tracks whether each coin has been spent via `CoinRecord.spent` and `CoinRecord.spent_block_index`. The mempool manager at [`add_spend_bundle()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool_manager.py#L480) checks the coin store to ensure coins are unspent before admitting a transaction. Block validation at [`validate_block_body()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py#L191) performs the same check when applying a block.

This check is fundamental to preventing double-spending across blocks. While VAL-003 prevents double-spending within a single bundle, VAL-005 prevents spending a coin that was already consumed in a prior block or transaction. Without this check, an attacker could resubmit old transactions to create value from already-consumed coins.

---

## Specification

### Algorithm

For each `CoinSpend` in `bundle.coin_spends`:

1. Compute `coin_id = coin_spend.coin.coin_id()`.
2. Look up `coin_id` in `context.coin_records`.
3. If found and `coin_record.spent == true`, return `Err(ValidationError::AlreadySpent(coin_id))`.
4. If the coin is found in `ephemeral_coins` instead (not in `coin_records`), skip this check for that coin since ephemeral coins have no `CoinRecord` and cannot be "already spent."

```rust
fn check_not_already_spent(
    coin_id: Bytes32,
    context: &ValidationContext,
) -> Result<(), ValidationError> {
    if let Some(record) = context.coin_records.get(&coin_id) {
        if record.spent {
            return Err(ValidationError::AlreadySpent(coin_id));
        }
    }
    // Ephemeral coins (in ephemeral_coins but not coin_records) are not checked.
    // Coins not in coin_records at all are caught by VAL-004.
    Ok(())
}
```

### Error Details

```rust
#[error("Coin already spent: {0}")]
AlreadySpent(Bytes32),
```

The `Bytes32` payload is the coin ID of the coin that has already been spent.

### CoinRecord Structure (from chia-sdk-coinset)

```rust
pub struct CoinRecord {
    pub coin: Coin,
    pub coinbase: bool,
    pub confirmed_block_index: u32,
    pub spent: bool,               // <-- this field is checked
    pub spent_block_index: u32,
    pub timestamp: u64,
}
```

### Ordering

This check is part of the structural checks (step 1 in the validation flow), executed after the coin existence check (VAL-004) and before puzzle hash verification (VAL-006).

---

## Acceptance Criteria

- [ ] A bundle spending a coin where `coin_record.spent == true` is rejected with `ValidationError::AlreadySpent`.
- [ ] The `AlreadySpent` error contains the coin ID of the already-spent coin.
- [ ] A bundle spending a coin where `coin_record.spent == false` passes this check.
- [ ] Ephemeral coins (in `ephemeral_coins` but not `coin_records`) are not subjected to this check.
- [ ] A bundle with multiple already-spent coins fails on the first one encountered.
- [ ] The check occurs before CLVM execution (no cost consumed).

---

## Implementation Notes

- **Primary file:** `src/consensus/validate.rs`
- **Position in flow:** Third structural check (after VAL-003 and VAL-004).
- **CoinRecord source:** The `CoinRecord` type comes from `chia-sdk-coinset`. It is the same type used by Chia L1's coin store.
- **Caller responsibility:** The caller must populate `coin_records` with accurate `spent` status. If the caller provides a stale record where `spent` is incorrectly `false`, this check will not catch it. The crate trusts the caller's input.
- **Ephemeral coins:** Coins in `ephemeral_coins` bypass this check because they have no `CoinRecord`. They were just created in the current block and by definition have not been spent yet.
- **Edge case:** A coin that exists in both `coin_records` (with `spent == true`) and `ephemeral_coins` is an inconsistency in the caller's input. The implementation should check `coin_records` first, which would reject it as `AlreadySpent`. This is the conservative choice.
- **Edge case:** `spent_block_index` is available for informational purposes but is not used in this check. Only the boolean `spent` field matters.

---

## Verification

### Test Plan

| Test | Type | Description | Expected Result |
|------|------|-------------|-----------------|
| Already-spent coin rejected | Simulator test | Set `coin_record.spent = true` in context, attempt spend | `Err(ValidationError::AlreadySpent(coin_id))` |
| Unspent coin passes | Simulator test | Set `coin_record.spent = false`, attempt spend | Passes this check |
| Ephemeral coin not checked | Simulator test | Coin in `ephemeral_coins` only (no `CoinRecord`), attempt spend | Passes this check |
| Error payload correct | Simulator test | Trigger `AlreadySpent`, inspect the `Bytes32` | Matches coin ID |
| Multiple already-spent | Simulator test | Two coins both marked spent | First one triggers error |
| Spent at specific height | Simulator test | Coin with `spent_block_index = 100`, `spent = true` | `AlreadySpent` regardless of current height |

---

## Source Citations

- **SPEC.md Section 5.1** (line 538): `ValidationError::AlreadySpent(Bytes32)` definition.
- **SPEC.md Section 6.1** (line 642): Step 1 structural checks: "Verify no coin is already spent."
- **SPEC.md Section 2.11** (lines 199-207): `CoinRecord` type from `chia-sdk-coinset` with `spent` field.
- **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) — checks coin is unspent.
- **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) — checks coins are unspent during block application.
- **L1 reference — `check_time_locks()`:** [`check_time_locks.py:15`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/check_time_locks.py#L15) — uses `CoinRecord` fields.

---

## References

- [VAL-003](VAL-003.md) — Duplicate spend check (prevents double-spend within a bundle)
- [VAL-004](VAL-004.md) — Missing coin check (precedes this check)
- [VAL-006](VAL-006.md) — Puzzle hash mismatch (next structural check)
- [VAL-010](VAL-010.md) — Ephemeral coin support (ephemeral coins skip this check)
- [VAL-015](VAL-015.md) — coin_records is minimal (caller provides accurate records)
