# BLK-009: Round-Trip Consistency

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

---

## Summary

Round-trip consistency is the fundamental correctness invariant for the block generator system: the additions, removals, and fees produced by `validate_block` on the output of `build_block_generator` MUST exactly equal the union of additions, removals, and fees from individually validating each spend bundle that was used to construct the generator. If this invariant is violated, the block construction and validation paths have diverged, which would mean that transactions could be silently altered or lost during block assembly.

This invariant connects the two sides of the block pipeline. On the construction side, `build_block_generator` takes a set of validated spend bundles and serializes them into a single CLVM generator program (with optional back-references for compression). On the validation side, `validate_block` executes that generator program and extracts the coin spends for validation. The round-trip property guarantees that no information is lost, added, or corrupted in this serialization/deserialization cycle.

The test methodology is explicit: validate N spend bundles individually, collecting their additions, removals, and fees. Then call `build_block_generator` with those same N bundles, run `validate_block` on the output, and compare. The sets must be identical (allowing for reordering within a bundle but preserving cross-bundle ordering). This test should be run with diverse bundle compositions including single-spend bundles, multi-spend bundles, bundles with ephemeral coins, and bundles near cost limits.

---

## Specification

### Invariant Definition

```
Let bundles = [B1, B2, ..., BN]  (input spend bundles)
Let G = build_block_generator(bundles)  (serialized generator)
Let R_block = validate_block(G, context, config, cache)  (block-level result)

For each Bi:
    Let R_i = validate_spend_bundle(Bi, context_i, config, cache)

Then:
    R_block.additions == union(R_1.additions, R_2.additions, ..., R_N.additions)
    R_block.removals  == union(R_1.removals, R_2.removals, ..., R_N.removals)
    R_block.fee       == sum(R_1.fee, R_2.fee, ..., R_N.fee)
```

### Properties That Must Be Preserved

1. **Coin identity:** Every coin in additions/removals from per-bundle validation appears in the block result, and vice versa. Coin identity is determined by `coin_id()` (sha256 of parent_coin_info + puzzle_hash + amount).

2. **Amount integrity:** The `amount` field of every coin is identical in both paths. No rounding, truncation, or modification occurs during serialization.

3. **Fee additivity:** The total fee from `validate_block` equals the sum of individual bundle fees. No fee is created or destroyed during block assembly.

4. **Condition preservation:** All conditions emitted by CLVM execution are preserved through the round trip. Timelocks, assertions, AGG_SIG pairs, and other conditions are identical.

### Test Implementation

```rust
#[test]
fn round_trip_consistency() {
    // Setup: Create N diverse spend bundles
    let bundles = vec![
        create_simple_spend_bundle(),      // Single coin spend
        create_multi_spend_bundle(),       // Multiple coin spends
        create_ephemeral_bundle(),         // Creates coins for later bundle
    ];

    // Path A: Validate each bundle individually
    let mut expected_additions = Vec::new();
    let mut expected_removals = Vec::new();
    let mut expected_fee = 0u64;
    let mut ephemeral = HashSet::new();

    for bundle in &bundles {
        let context = make_context(&bundle, &ephemeral);
        let result = validate_spend_bundle(
            bundle, &context, &config, Some(&mut cache)
        ).expect("individual validation should pass");

        for coin in &result.additions {
            ephemeral.insert(coin.coin_id());
        }

        expected_additions.extend(result.additions);
        expected_removals.extend(result.removals);
        expected_fee += result.fee;
    }

    // Path B: Build generator, then validate as block
    let generator = build_block_generator(&bundles)
        .expect("generator construction should succeed");

    let block_context = make_block_context(&bundles);
    let block_result = validate_block(
        &generator, &block_context, &config, Some(&mut cache)
    ).expect("block validation should pass");

    // Compare: Sets must be identical
    let expected_add_ids: HashSet<Bytes32> = expected_additions
        .iter().map(|c| c.coin_id()).collect();
    let actual_add_ids: HashSet<Bytes32> = block_result.additions
        .iter().map(|c| c.coin_id()).collect();
    assert_eq!(expected_add_ids, actual_add_ids, "additions mismatch");

    let expected_rem_ids: HashSet<Bytes32> = expected_removals
        .iter().map(|c| c.coin_id()).collect();
    let actual_rem_ids: HashSet<Bytes32> = block_result.removals
        .iter().map(|c| c.coin_id()).collect();
    assert_eq!(expected_rem_ids, actual_rem_ids, "removals mismatch");

    assert_eq!(expected_fee, block_result.fee, "fee mismatch");
}
```

### Edge Cases for Round-Trip Testing

```rust
// Edge case 1: Empty block (no bundles)
let generator = build_block_generator(&[]).unwrap();
let result = validate_block(&generator, &ctx, &cfg, None).unwrap();
assert!(result.additions.is_empty());
assert!(result.removals.is_empty());
assert_eq!(result.fee, 0);

// Edge case 2: Single bundle
let generator = build_block_generator(&[single_bundle]).unwrap();
// Result must exactly match validate_spend_bundle output

// Edge case 3: Bundles with ephemeral coins
// Bundle A creates coin X, Bundle B spends coin X
// The round-trip must preserve both the creation and spending

// Edge case 4: Maximum cost block
// Bundles that together approach max_block_cost_clvm
// Serialization overhead must not push over the limit

// Edge case 5: Back-references
// Bundles with shared puzzle reveals (common in DID/NFT batches)
// Back-reference compression must not alter semantics
```

### What Round-Trip Does NOT Guarantee

- **Byte-level identity of the generator:** The serialized generator bytes may differ between builds (due to compression choices, back-reference strategies). Only the semantic output must match.
- **Ordering within a bundle:** Additions from a single bundle may appear in a different order. The comparison uses set equality on coin IDs.
- **Cross-bundle ordering:** The order of bundles within the block is preserved. Bundle 1's additions come before Bundle 2's additions in the block result.

---

## Acceptance Criteria

- [ ] Validating individual bundles and then building a block generator from them produces identical additions (by coin_id set equality).
- [ ] Validating individual bundles and then building a block generator from them produces identical removals (by coin_id set equality).
- [ ] The total fee from `validate_block` equals the sum of individual bundle fees.
- [ ] The round-trip holds for bundles with ephemeral coins (intra-block spending).
- [ ] The round-trip holds when back-reference compression is used in the generator.
- [ ] At least one integration test explicitly verifies the round-trip invariant.

---

## Implementation Notes

- **Primary file:** `tests/round_trip.rs` or `tests/integration/block_round_trip.rs` (test), `src/block/generator.rs` and `src/consensus/validate_block.rs` (implementation).
- **Not a runtime check:** The round-trip invariant is verified by tests, not enforced at runtime. The validator does not re-validate individual bundles and compare — that would be prohibitively expensive.
- **Generator determinism:** For a given set of bundles in a given order, `build_block_generator` should produce a deterministic output. Non-determinism (e.g., from HashMap iteration order) would make the round-trip test flaky.
- **Back-reference transparency:** `solution_generator_backrefs()` compresses repeated puzzle reveals using CLVM back-references. This is a serialization optimization that must be transparent to the validation output. The round-trip test implicitly verifies this.
- **Cost accounting:** The total CLVM cost from `validate_block` may differ slightly from the sum of individual bundle costs due to generator overhead (the cost of running the generator program itself). This is expected and acceptable — the invariant is on additions/removals/fee, not cost.
- **Fuzz testing:** This invariant is an excellent candidate for property-based testing (e.g., with `proptest` or `arbitrary`). Generate random valid bundles, build a generator, validate the block, and assert the invariant.

---

## Verification

### Test Plan

| Test | Type | Description | Expected Result |
|------|------|-------------|-----------------|
| Simple round-trip | Integration test | 3 standard bundles: individual vs block | Additions, removals, fee match |
| Single bundle round-trip | Integration test | 1 bundle: validate_spend_bundle vs validate_block | Identical results |
| Ephemeral round-trip | Integration test | Bundle A creates coin, Bundle B spends it | Both paths handle ephemeral correctly |
| Back-reference round-trip | Integration test | Bundles with shared puzzles, backrefs enabled | Results match despite compression |
| Empty block round-trip | Unit test | No bundles, empty generator | Empty additions/removals, zero fee |
| Large block round-trip | Integration test | 50+ bundles approaching cost limit | Results match, no overflow |
| Deterministic generator | Unit test | Build generator twice from same input | Identical bytes |

---

## Source Citations

- **SPEC.md Section 6.1**: "additions/removals from validate_block on build_block_generator output MUST equal union of per-bundle validation."
- **SPEC.md Section 6.1**: Test methodology — "validate N bundles individually, build generator from same N, validate_block, compare."
- **L1 reference — bundle_tools.py:15:** `solution_generator_backrefs()` — generator construction with compression.
- **L1 reference — multiprocess_validation.py:69:** `run_block_generator2()` — generator execution during block validation.

---

## References

- [BLK-001](BLK-001.md) — Block generator construction (build_block_generator)
- [BLK-008](BLK-008.md) — Same checks as validate_spend_bundle (validation parity)
- [VAL-011](../../validation/specs/VAL-011.md) — Conservation check (fee computation)
- [VAL-014](../../validation/specs/VAL-014.md) — SpendResult output structure (additions, removals, fee)
- [PAR-010](../../parity/specs/PAR-010.md) — Block generator parity with L1
