# PAR-004: BLS Domain Separation for All AGG_SIG Variants

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

---

## Summary

BLS signature domain separation in `dig-clvm` must match Chia L1's [`make_aggsig_final_message()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L74) at `condition_tools.py:74` for all 8 `AGG_SIG_*` condition variants. Each variant appends a different coin-specific addendum to the message before signing, preventing signature replay attacks across different coins and contexts. The final signed message is always `msg + addendum + additional_data[opcode]`, where `addendum` is determined by a lookup table at [`condition_tools.py:87-97`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L87) and `additional_data` is a per-network constant.

Domain separation is a critical security property. Without it, a signature valid for `AGG_SIG_ME` on one coin could be replayed as `AGG_SIG_PUZZLE` on another coin with the same puzzle hash, or `AGG_SIG_UNSAFE` could be exploited to forge arbitrary messages. The L1 lookup table ensures each opcode produces a unique final message by binding the signature to specific coin fields. The `additional_data` suffix (typically the genesis challenge hash) provides network-level domain separation, preventing signatures from one Chia-compatible network from being replayed on another.

This requirement is satisfied by using `chia-consensus` for condition processing (per [PAR-002](PAR-002.md)), which internally implements `make_aggsig_final_message()`. The `dig-clvm` crate must not implement its own domain separation logic. The `additional_data` values come from `ConsensusConstants`, which is populated from `dig-constants` for the DIG network. Any custom implementation would risk getting even one byte wrong in the addendum computation, which would cause valid signatures to be rejected or invalid signatures to be accepted.

---

## Specification

### Domain Separation Lookup Table

The following table defines the addendum for each `AGG_SIG_*` opcode, per [`condition_tools.py:87-97`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L87):

| Opcode | Value | Addendum | Description |
|--------|-------|----------|-------------|
| `AGG_SIG_PARENT` | 43 | `coin.parent_coin_info` | Binds to parent coin ID (32 bytes) |
| `AGG_SIG_PUZZLE` | 47 | `coin.puzzle_hash` | Binds to puzzle hash (32 bytes) |
| `AGG_SIG_AMOUNT` | 46 | `int_to_bytes(coin.amount)` | Binds to coin amount (variable length) |
| `AGG_SIG_PUZZLE_AMOUNT` | 45 | `coin.puzzle_hash + int_to_bytes(coin.amount)` | Binds to puzzle + amount |
| `AGG_SIG_PARENT_AMOUNT` | 44 | `coin.parent_coin_info + int_to_bytes(coin.amount)` | Binds to parent + amount |
| `AGG_SIG_PARENT_PUZZLE` | 43 | `coin.parent_coin_info + coin.puzzle_hash` | Binds to parent + puzzle |
| `AGG_SIG_ME` | 50 | `coin.name()` | Binds to full coin ID (sha256 of parent+puzzle+amount) |
| `AGG_SIG_UNSAFE` | 49 | (none) | No domain separation -- use with extreme caution |

### Final Message Construction

```python
# From condition_tools.py:74
def make_aggsig_final_message(
    opcode: int,
    msg: bytes,           # raw message from condition
    coin: Coin,           # the coin being spent
    additional_data: dict # per-opcode additional_data from ConsensusConstants
) -> bytes:
    if opcode == AGG_SIG_UNSAFE:
        return msg
    addendum = COIN_TO_ADDENDUM_F_LOOKUP[opcode](coin)
    return msg + addendum + additional_data[opcode]
```

In Rust (via `chia-consensus`):

```rust
// chia-consensus handles this internally during run_spendbundle().
// The extracted conditions include fully-constructed (pk, final_message) pairs:
let conditions: OwnedSpendBundleConditions = run_spendbundle(
    &mut allocator, bundle, max_cost, height, constants, flags
)?;

// conditions.agg_sig_unsafe contains raw messages (no domain separation)
// conditions.spends[i].agg_sig_me contains domain-separated messages
// conditions.spends[i].agg_sig_parent, etc.
```

### Additional Data

The `additional_data` is a per-opcode, per-network constant stored in `ConsensusConstants`:

```rust
// Each AGG_SIG_* opcode (except UNSAFE) has its own additional_data field:
constants.agg_sig_me_additional_data       // for AGG_SIG_ME
constants.agg_sig_parent_additional_data   // for AGG_SIG_PARENT
// ... etc.

// For Chia mainnet, these are derived from the genesis challenge.
// For DIG, they come from dig-constants::NetworkConstants.
```

### Security Properties

1. **Cross-opcode isolation**: A signature valid for `AGG_SIG_ME` cannot be replayed as `AGG_SIG_PUZZLE` because the addendum differs.
2. **Cross-coin isolation**: A signature for `AGG_SIG_ME` on coin A cannot be replayed on coin B because `coin.name()` differs.
3. **Cross-network isolation**: A signature valid on Chia mainnet cannot be replayed on DIG because `additional_data` differs.
4. **AGG_SIG_UNSAFE exception**: Has no domain separation by design. Signatures using `AGG_SIG_UNSAFE` can be replayed across coins -- this is intentional for use cases like oracle attestations.

---

## Acceptance Criteria

- [ ] All 8 `AGG_SIG_*` variants produce correct final messages matching L1's `make_aggsig_final_message()`
- [ ] No custom `make_aggsig_final_message()` implementation exists in `dig-clvm` (delegated to `chia-consensus`)
- [ ] `AGG_SIG_UNSAFE` messages have no addendum or additional_data suffix
- [ ] `AGG_SIG_ME` messages include `coin.name() + additional_data[AGG_SIG_ME]`
- [ ] The `additional_data` values come from `ConsensusConstants` (populated from `dig-constants`)
- [ ] A cross-validation test confirms message construction matches L1 reference values

---

## Implementation Notes

- The domain separation logic lives entirely within `chia-consensus`. The `dig-clvm` crate does not need to implement or call `make_aggsig_final_message()` directly.
- The `pkm_pairs()` function at [`condition_tools.py:100`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L100) collects all `(public_key, final_message)` pairs from conditions. The Rust equivalent in `chia-consensus` does the same.
- The `int_to_bytes()` function for amounts uses big-endian encoding with minimal byte length (no leading zeros, except for the value 0 which is encoded as empty bytes).
- The 6 scoped `AGG_SIG_*` variants (PARENT, PUZZLE, AMOUNT, PUZZLE_AMOUNT, PARENT_AMOUNT, PARENT_PUZZLE) were added in the Chia hard fork. DIG L2 has `hard_fork_height = 0`, so all variants are available from block 0.

---

## Verification

### Test Plan

1. **AGG_SIG_ME message construction**:
   - Create a known coin with `parent=0xaa..aa`, `puzzle_hash=0xbb..bb`, `amount=1000`.
   - Build a spend emitting `AGG_SIG_ME(pk, b"test_msg")`.
   - Extract the final message from `chia-consensus` conditions.
   - Assert it equals `b"test_msg" + coin.name() + additional_data[AGG_SIG_ME]`.

2. **AGG_SIG_UNSAFE message construction**:
   - Build a spend emitting `AGG_SIG_UNSAFE(pk, b"raw_msg")`.
   - Extract the final message.
   - Assert it equals `b"raw_msg"` (no addendum, no additional_data).

3. **All 8 variants**:
   - For each `AGG_SIG_*` opcode, build a spend, extract the final message, and verify the addendum matches the lookup table.
   - Use a known coin so addendum bytes can be precomputed.

4. **Cross-opcode replay prevention**:
   - Sign a message with `AGG_SIG_ME` domain separation.
   - Attempt to verify it as an `AGG_SIG_PUZZLE` signature.
   - Assert verification fails.

5. **Cross-network replay prevention**:
   - Compute `AGG_SIG_ME` final message with Chia mainnet `additional_data`.
   - Compute the same with DIG `additional_data`.
   - Assert the final messages differ.

6. **Reference value cross-validation**:
   - Compute final messages for test vectors derived from L1 test cases.
   - Assert exact byte-level match.

### Expected Test Files

- `tests/signature_tests.rs` -- PAR-004 domain separation tests for all 8 variants

---

## Source Citations

- Chia L1 domain separation: [`condition_tools.py:74-97`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L74)
- Chia L1 `pkm_pairs()`: [`condition_tools.py:100`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L100)
- Scoped AGG_SIG opcodes: [`condition_opcodes.py:22-27`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/condition_opcodes.py#L22)
- dig-clvm BLS architecture: [SPEC.md Section 2.6](../../../../resources/SPEC.md)

---

## References

- [PAR-002](PAR-002.md) -- Condition parsing (extracts AGG_SIG conditions)
- [PAR-005](PAR-005.md) -- `chia-bls::aggregate_verify()` (verifies the domain-separated signatures)
- [BLS-001](../../bls_cache/specs/BLS-001.md) -- BlsCache parameter (caches verified pairings)
