dig-clvm 0.1.1

DIG L2 CLVM consensus engine — validates spend bundles, computes coin additions and removals
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
# dig-clvm: Modular CLVM Consensus Crate

## 1. Purpose

`dig-clvm` is a standalone Rust crate used by DIG validators to **validate spend bundles and compute the resulting coin additions and removals**. It is the module that runs CLVM programs when coins are spent, producing the set of state changes (new coins created, old coins destroyed) that the caller then commits to blockchain state.

It is built as a **thin orchestration layer on top of the Chia crate ecosystem**, not a reimplementation. The Chia crates already provide the CLVM runtime, condition types, tree hashing, currying, BLS signatures, puzzle drivers, and an in-memory simulator. `dig-clvm` composes these into a consensus-grade API with L2-specific validation rules and configurable cost limits.

### Core Contract

```
Input:  SpendBundle (coin spends + aggregated BLS signature)
Output: SpendResult { additions: Vec<Coin>, removals: Vec<Coin>, fee: u64 }
        or ValidationError
```

Blockchain state management (UTXO set persistence, Merkle roots, block assembly) is **out of scope**. This crate validates and computes; the caller commits to state.

This mirrors how Chia L1 separates execution from state: [`tx_removals_and_additions()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/generator_tools.py#L54) extracts additions and removals from `SpendBundleConditions`, then the caller ([`validate_block_body()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py#L191)) commits them to the coin store.

### Goals

1. **Maximize reuse** - Use `chia-consensus`, `chia-sdk-types`, `chia-sdk-driver`, `clvmr`, `clvm-utils`, `clvm-traits`, `chia-bls`, and `chia-sdk-test` directly. Never reimplement what they provide.
2. **1:1 Chia L1 parity** - Identical behavior to Chia full nodes because we use the same libraries they use. Parity is achieved by construction, not by reimplementation.
3. **Self-contained testing** - The crate ships with its own test suite using `chia-sdk-test::Simulator` for full spend-bundle-level tests, plus targeted unit tests for L2-specific logic.
4. **Consensus-grade API** - A well-defined public surface for DIG validators, mempools, and block builders. No UI, networking, or storage concerns leak in.

### Non-Goals

- Compiling Chialisp source to CLVM bytecode (use `clvm_tools_rs` externally).
- Blockchain state management (UTXO set persistence, Merkle roots, block storage, state commitment).
- Reimplementing anything the Chia crates already provide.

---

## 2. Chia Crate Ecosystem Inventory

This section documents what each upstream crate provides and how `dig-clvm` uses it. This is the foundation: everything listed here is **used directly, not reimplemented**.

### 2.1 `clvmr` (v0.14) - CLVM Runtime

The bytecode interpreter. Everything starts here. Chia L1 delegates all CLVM execution to this Rust crate via `chia_rs` — see the import at [`multiprocess_validation.py:20-21`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/multiprocess_validation.py#L20) where `run_block_generator` and `run_block_generator2` are pulled from `chia_rs`.

| API | Purpose |
|---|---|
| `run_program(allocator, dialect, program, env, max_cost)` | Execute CLVM bytecode with cost tracking |
| `Allocator` | Memory manager for CLVM S-expressions |
| `NodePtr`, `SExp` | Typed pointers and atom/pair discrimination |
| `ChiaDialect::new(flags)` | Chia-specific CLVM dialect with soft-fork flags |
| `Cost` | `u64` type alias for cost tracking |
| `serde::{node_from_bytes, node_to_bytes}` | Binary serialization of CLVM trees |
| `MEMPOOL_MODE`, `NO_UNKNOWN_OPS`, `LIMIT_HEAP` | Execution flag constants — Chia L1 imports these at [`mempool.py:13-14`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool.py#L13) |

**dig-clvm usage**: Direct dependency. The `run_program` function is the core execution primitive.

### 2.2 `clvm-traits` (v0.26) - Serialization Traits

| API | Purpose |
|---|---|
| `ToClvm<E>` / `FromClvm<D>` | Encode/decode Rust types to/from CLVM |
| `#[derive(ToClvm, FromClvm)]` | Derive macros with `#[clvm(list)]`, `#[clvm(curry)]` attributes |

**dig-clvm usage**: Re-exported. Callers use these to define custom puzzle arguments.

### 2.3 `clvm-utils` (v0.26) - Tree Hash & Currying

Chia L1 tests tree hash correctness in [`test_curry_and_treehash.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/_tests/clvm/test_curry_and_treehash.py).

| API | Purpose |
|---|---|
| `tree_hash(allocator, node)` | Canonical CLVM tree hash (atom: `sha256(0x01 \|\| data)`, pair: `sha256(0x02 \|\| left \|\| right)`) |
| `tree_hash_atom(bytes)` / `tree_hash_pair(left, right)` | Standalone hash primitives |
| `TreeHash` | 32-byte hash newtype with conversions |
| `CurriedProgram<P, A>` | `(a (q . program) args)` representation with CLVM serialization |
| `curry_tree_hash(program_hash, arg_hashes)` | Compute curried puzzle hash without an allocator |
| `ToTreeHash` | Trait for types that can compute their own tree hash |

**dig-clvm usage**: Re-exported. Used for puzzle hash computation and currying throughout.

### 2.4 `chia-protocol` (v0.26) - Core Protocol Types

These are the canonical wire types used by Chia full nodes. `SpendBundle` is imported from `chia_rs` at [`mempool_manager.py:6`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/mempool_item.py#L6), and `CoinSpend` at [`coin_spend.py:6`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/coin_spend.py#L6).

| API | Purpose |
|---|---|
| `Coin` | `(parent_coin_info, puzzle_hash, amount)` with `coin_id()` method |
| `CoinSpend` | `(coin, puzzle_reveal, solution)` |
| `SpendBundle` | `(coin_spends, aggregated_signature)` with `aggregate()`, `additions()`, `name()` |
| `Program` | Serialized CLVM bytecode wrapper |
| `CoinState` | Coin with creation/spend height tracking |
| `Bytes`, `Bytes32` | Byte array types |

**dig-clvm usage**: Re-exported. These are the canonical wire types.

### 2.5 `chia-consensus` (v0.26) - Consensus Validation Engine

This is the critical crate. It provides the **actual consensus logic** used by Chia full nodes. On L1, the mempool calls [`validate_clvm_and_signature()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool_manager.py#L445) at `mempool_manager.py:445`, and block validation calls [`_run_block()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/multiprocess_validation.py#L62) at `multiprocess_validation.py:62` which selects between `run_block_generator` (pre-hard-fork) and `run_block_generator2` (post-hard-fork) at [lines 69-71](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/multiprocess_validation.py#L69).

| API | Purpose |
|---|---|
| `run_spendbundle(allocator, bundle, max_cost, height, constants, flags)` | Execute all spends in a bundle, extract and validate conditions |
| `validate_clvm_and_signature(allocator, bundle, max_cost, constants, height)` | Full validation: CLVM execution + BLS signature verification |
| `get_conditions_from_spendbundle(allocator, bundle, max_cost, constants, height, flags)` | Conditions without signature validation |
| `SpendBundleConditions` / `OwnedSpendBundleConditions` | Aggregated conditions across all spends — imported at [`multiprocess_validation.py:17`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/multiprocess_validation.py#L17) |
| `SpendConditions` / `OwnedSpendConditions` | Per-spend conditions (coin_id, puzzle_hash, create_coin, agg_sig_*, etc.) |
| `SpendVisitor` trait | Hook into condition processing (e.g., `MempoolVisitor`) |
| `ConsensusConstants` | All blockchain parameters (costs, fork heights, additional_data) — instantiated at [`default_constants.py:13`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/default_constants.py#L13) |
| `get_flags_for_height_and_constants(height, constants)` | Compute execution flags for a given block height — imported at [`multiprocess_validation.py:19`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/multiprocess_validation.py#L19) |
| `make_allocator(flags)` | Factory for properly-configured allocators |
| Opcode constants | `AGG_SIG_ME`, `CREATE_COIN`, etc. — defined at [`condition_opcodes.py:7-73`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/condition_opcodes.py#L7) |
| Cost constants | `AGG_SIG_COST=1_200_000` ([`condition_costs.py:8`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_costs.py#L8)), `CREATE_COIN_COST=1_800_000` ([`:9`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_costs.py#L9)), `GENERIC_CONDITION_COST=500` ([`:13`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_costs.py#L13)) |

**dig-clvm usage**: The validation backbone. `run_spendbundle()` replaces the hand-rolled `run_puzzle()` + `parse_conditions()` loop. `OwnedSpendBundleConditions` replaces the custom `ClvmResult` + `Condition` enum for consensus paths.

### 2.6 `chia-bls` (v0.26) - BLS12-381 Signatures

Chia L1 constructs signature messages via [`make_aggsig_final_message()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L74) at `condition_tools.py:74`, and collects (pubkey, message) pairs via [`pkm_pairs()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L100) at `:100`. The domain separation lookup table is at [`:87-97`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L87):

```python
COIN_TO_ADDENDUM_F_LOOKUP = {
    AGG_SIG_PARENT:         lambda coin: coin.parent_coin_info,
    AGG_SIG_PUZZLE:         lambda coin: coin.puzzle_hash,
    AGG_SIG_AMOUNT:         lambda coin: int_to_bytes(coin.amount),
    AGG_SIG_PUZZLE_AMOUNT:  lambda coin: coin.puzzle_hash + int_to_bytes(coin.amount),
    AGG_SIG_PARENT_AMOUNT:  lambda coin: coin.parent_coin_info + int_to_bytes(coin.amount),
    AGG_SIG_PARENT_PUZZLE:  lambda coin: coin.parent_coin_info + coin.puzzle_hash,
    AGG_SIG_ME:             lambda coin: coin.name(),
}
final_message = msg + addendum + additional_data[opcode]
```

| API | Purpose |
|---|---|
| `PublicKey`, `SecretKey`, `Signature` | Key and signature types |
| `sign(sk, msg)` | Sign a message |
| `verify(pk, msg, sig)` | Verify a single signature |
| `aggregate_verify(pks, msgs, sig)` | Verify aggregated BLS signature |
| `aggregate(signatures)` | Aggregate multiple signatures |
| `BlsCache` | Cache for verified signature pairings |

**dig-clvm usage**: Re-exported. Used by the validation layer for signature verification.

### 2.7 `chia-sdk-types` (v0.30) - SDK Type Definitions

| API | Purpose |
|---|---|
| `Condition<T>` enum | All condition opcodes with typed fields, generic over node representation. Covers all opcodes from [`condition_opcodes.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/condition_opcodes.py#L7) including [`SEND_MESSAGE(66)` / `RECEIVE_MESSAGE(67)`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/condition_opcodes.py#L37) (Chia 2.3+) |
| `Conditions<T>` | Builder for condition lists with `.with()` chaining |
| `run_puzzle(allocator, puzzle, solution)` | Convenience wrapper: `ChiaDialect(0)` + 11B cost limit (mirrors L1's [`MAX_BLOCK_COST_CLVM=11_000_000_000`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/default_constants.py#L68)) |
| `announcement_id(source, message)` | Compute announcement hash |
| `Mod` trait | `mod_reveal()`, `mod_hash()`, `curry_tree_hash()` for puzzle modules |
| `MAINNET_CONSTANTS`, `TESTNET11_CONSTANTS` | Network-specific constants |
| `MerkleTree`, `MerkleProof` | Merkle proof construction and verification |

**dig-clvm usage**: `Condition<T>` and `Conditions<T>` are the primary condition types for spend construction and inspection. `run_puzzle()` is the high-level execution function. `Mod` is the trait for defining puzzle modules.

### 2.8 `chia-sdk-driver` (v0.30) - Puzzle Drivers & Spend Building

| API | Purpose |
|---|---|
| `SpendContext` | Allocator wrapper with puzzle caching, currying, CLVM execution, and spend collection |
| `Layer` trait | Composable puzzle abstraction: `parse_puzzle`, `construct_puzzle`, `construct_solution`, `construct_spend` |
| `Spend { puzzle, solution }` | Puzzle + solution pair as NodePtrs |
| `SpendWithConditions` trait | Build spends from condition lists |
| `Puzzle` enum | `Curried(CurriedPuzzle)` or `Raw(RawPuzzle)` parsed puzzles |
| `DriverError` | Comprehensive error type for CLVM/driver operations |
| Primitives: `Launcher`, `Cat`, `Did`, `Nft`, `Singleton`, `Vault`, etc. | High-level asset type drivers — corresponding to puzzles tested at [`test_puzzle_drivers.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/_tests/clvm/test_puzzle_drivers.py) and [`test_singletons.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/_tests/clvm/test_singletons.py) |
| `StandardLayer` | Standard XCH transaction puzzle (p2_delegated_puzzle_or_hidden_puzzle) |
| Action system | CHIP-0050 action-based spending pattern |

**dig-clvm usage**: `SpendContext` is the primary interface for test fixture construction and any puzzle-building operations. `Layer` implementations for standard puzzles are used in tests and by callers building transactions.

### 2.9 `chia-sdk-test` (v0.30) - Testing Infrastructure

The Simulator uses `chia_consensus::run_spendbundle()` internally — the same code path that Chia L1 uses at [`_run_block()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/multiprocess_validation.py#L62) for block validation and [`pre_validate_spendbundle()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool_manager.py#L428) for mempool admission.

| API | Purpose |
|---|---|
| `Simulator` | In-memory blockchain with coin state tracking |
| `Simulator::new_transaction(bundle)` | Validate and apply a spend bundle |
| `Simulator::spend_coins(spends, keys)` | Convenience for spending with auto-signing |
| `BlsPair` / `BlsPairWithCoin` | Test key pairs with pre-funded coins |
| `to_program()`, `to_puzzle()` | Convert ToClvm values to Program/puzzle_hash |
| `expect_spend(result, should_pass)` | Assert spend success/failure |

**dig-clvm usage**: Primary testing tool. Gives us L1 parity in tests by construction.

### 2.10 `chia-puzzles` (v0.20) - Puzzle Bytecode

150+ hardcoded Chialisp puzzle constants including:
- `P2_DELEGATED_PUZZLE_OR_HIDDEN_PUZZLE` (standard transaction)
- `SINGLETON_TOP_LAYER_V1_1`, `SINGLETON_LAUNCHER`
- `CAT_PUZZLE`, `DID_INNERPUZ`, `NFT_STATE_LAYER`
- `SETTLEMENT_PAYMENT`, `VAULT`, `CLAWBACK`
- All with pre-computed `_HASH` variants

**dig-clvm usage**: Re-exported for callers that need puzzle bytecodes. Used in parity tests.

### 2.11 `chia-sdk-coinset` (v0.30) - Coin State

Mirrors the L1 `CoinRecord` used throughout [`check_time_locks()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/check_time_locks.py#L15) and [`add_spend_bundle()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool_manager.py#L480).

| API | Purpose |
|---|---|
| `CoinRecord` | `{ coin, coinbase, confirmed_block_index, spent, spent_block_index, timestamp }` |

**dig-clvm usage**: Re-exported. Used in `ValidationContext` for tracking coin state.

---

## 3. What dig-clvm Adds (L2 Consensus Orchestration)

The Chia crates provide all the primitives. `dig-clvm` adds the **L2-specific orchestration** that composes them into a consensus API.

On Chia L1, this orchestration role is split between Python code in [`mempool_manager.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool_manager.py#L428) (mempool path) and [`block_body_validation.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py#L191) (block path), with the heavy lifting delegated to Rust via `chia_rs`. `dig-clvm` does the same thing as a single Rust crate.

### 3.1 What dig-clvm Owns

| Component | Why it can't come from Chia crates |
|---|---|
| `ValidationContext` | L2 chain state: height, timestamp, genesis challenge, unspent coin set, ephemeral tracking |
| `ValidationConfig` | L2-specific parameters: configurable cost limits (L2 block cost differs from L1's [`MAX_BLOCK_COST_CLVM=11B`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/default_constants.py#L68)), `MEMPOOL_MODE` flag support for SpendVisitor |
| `validate_spend_bundle()` | Orchestrates `chia-consensus::run_spendbundle()` with L2 rules, produces additions + removals |
| `build_block_generator()` | Assembles spend bundles into a compressed block generator via `solution_generator_backrefs()` |
| `validate_block()` | Orchestrates `chia-consensus::run_block_generator2()` for block-level validation |
| `SpendResult` | The output: `additions` (new coins), `removals` (spent coins), `fee`, and full conditions for inspection |
| `ValidationError` | L2-specific error enum wrapping `chia-consensus` errors with additional context |
| BLS cache management | `validate_spend_bundle()` and `validate_block()` accept `Option<&mut BlsCache>` to avoid re-verifying signatures across mempool → block |
| L2 cost constants | `MAX_COST_PER_BLOCK` for L2 (50x L1 per-spend limit) |

### 3.2 What dig-clvm Re-exports

Everything else comes from upstream:

```rust
// Core execution
pub use clvmr::{Allocator, NodePtr, Cost};
pub use chia_sdk_types::run_puzzle;

// Conditions
pub use chia_sdk_types::{Condition, Conditions};

// Consensus engine
pub use chia_consensus::spendbundle_conditions::run_spendbundle;
pub use chia_consensus::spendbundle_validation::validate_clvm_and_signature;
pub use chia_consensus::conditions::{
    SpendBundleConditions, OwnedSpendBundleConditions,
    SpendConditions, OwnedSpendConditions,
};
pub use chia_consensus::consensus_constants::ConsensusConstants;
pub use chia_consensus::opcodes::*;

// Types
pub use chia_protocol::{Coin, CoinSpend, SpendBundle, Program, Bytes32, CoinState};
pub use chia_bls::{PublicKey, SecretKey, Signature, aggregate_verify, BlsCache};
pub use chia_sdk_coinset::CoinRecord;

// Hashing & currying
pub use clvm_utils::{tree_hash, curry_tree_hash, CurriedProgram, TreeHash, ToTreeHash};

// Serialization
pub use clvm_traits::{ToClvm, FromClvm};

// Spend construction
pub use chia_sdk_driver::{SpendContext, Layer, Spend, SpendWithConditions, DriverError, Puzzle};

// Puzzle modules
pub use chia_sdk_types::Mod;
pub use chia_puzzles;

// Constants
pub use chia_sdk_types::{MAINNET_CONSTANTS, TESTNET11_CONSTANTS};
```

---

## 4. Crate Architecture

### 4.1 Module Layout

```
dig-clvm/
  Cargo.toml
  src/
    lib.rs                  -- Re-exports from Chia crates + dig-clvm's own API
    consensus/
      mod.rs                -- Module root
      validate.rs           -- validate_spend_bundle() orchestration
      block.rs              -- Block generator construction + validate_block()
      context.rs            -- ValidationContext
      config.rs             -- ValidationConfig, L2 cost constants
      result.rs             -- SpendResult
      cache.rs              -- BLS signature cache management
      error.rs              -- ValidationError
  tests/
    validation_tests.rs     -- Full bundle validation via Simulator
    block_tests.rs          -- Block generator construction + validation
    parity_tests.rs         -- Golden tests against known Chia L1 behavior
    condition_tests.rs      -- Condition round-trip via SpendContext + Simulator
    signature_tests.rs      -- BLS domain separation for all AGG_SIG_* variants
    cost_tests.rs           -- L2 cost limit enforcement
    cache_tests.rs          -- BLS cache hit/miss across mempool → block
    simulator_tests.rs      -- Simulator-based multi-spend scenarios
```

The crate is intentionally small. Most of the complexity lives in upstream Chia crates where it is already tested and maintained.

### 4.2 Dependencies

```toml
[dependencies]
# CLVM runtime
clvmr = "0.14"
clvm-traits = "0.26"
clvm-utils = "0.26"

# Chia protocol & consensus
chia-protocol = "0.26"
chia-consensus = "0.26"
chia-bls = "0.26"
chia-traits = "0.26"

# Chia SDK (individual crates, not the umbrella)
chia-sdk-types = "0.30"
chia-sdk-driver = { version = "0.30", features = ["action-layer"] }
chia-sdk-coinset = "0.30"
chia-puzzles = "0.20"

# DIG ecosystem
dig-constants = { path = "../dig-constants" }

# Minimal own dependencies
thiserror = "2"
hex = "0.4"

[dev-dependencies]
chia-sdk-test = "0.30"
hex-literal = "0.4"
rand = "0.8"
```

No `tokio`, `serde_json`, `rocksdb`, `reqwest`, or async/IO/storage dependencies. Pure computation. Individual SDK crates are used instead of the `chia-wallet-sdk` umbrella to avoid pulling in RPC clients and wallet utilities.

### 4.3 `dig-constants` (Sibling Crate)

`dig-constants` is a separate crate at `../dig-constants` that defines DIG's network parameters. It is imported by `dig-clvm` and any other DIG crate that needs network constants. It already exists and compiles.

```rust
/// DIG network constants. Wraps chia-consensus ConsensusConstants
/// with DIG-specific values (genesis challenge, AGG_SIG additional data, etc.)
pub struct NetworkConstants { /* wraps ConsensusConstants */ }

impl NetworkConstants {
    pub fn consensus(&self) -> &ConsensusConstants;
    pub fn genesis_challenge(&self) -> Bytes32;
    pub fn agg_sig_me_additional_data(&self) -> Bytes32;
    pub fn max_block_cost_clvm(&self) -> u64;
}

pub const DIG_MAINNET: NetworkConstants = /* ... */;
pub const DIG_TESTNET: NetworkConstants = /* ... */;
```

Key design choices:
- `hard_fork_height` and `hard_fork2_height` are set to `0` — DIG L2 starts with all consensus features enabled from block 0.
- Proof-of-space and VDF fields are set to neutral values since DIG does not use Chia's PoS consensus.
- Genesis challenge and all `agg_sig_*_additional_data` fields are placeholder zeros until mainnet launch.
- Dependencies: `chia-consensus`, `chia-protocol`, `chia-bls`, `hex-literal` only.

---

## 5. Public API

### 5.1 Validation (dig-clvm's own code)

```rust
/// Validate a spend bundle against L2 consensus rules.
///
/// Internally calls chia_consensus::run_spendbundle() for CLVM execution
/// and condition extraction, then applies L2-specific validation rules.
///
/// This mirrors the L1 flow where pre_validate_spendbundle() calls
/// validate_clvm_and_signature() (mempool_manager.py:445) and
/// validate_block_body() (block_body_validation.py:191) checks the results.
///
/// `bls_cache`: When provided, verified signature pairings are cached and
/// reused. Pass the same cache across mempool → block validation to avoid
/// re-verifying signatures. Pass None to verify from scratch.
pub fn validate_spend_bundle(
    bundle: &SpendBundle,
    context: &ValidationContext,
    config: &ValidationConfig,
    bls_cache: Option<&mut BlsCache>,
) -> Result<SpendResult, ValidationError>;

/// Build a block generator from a set of spend bundles.
///
/// Bundles are added in order until `max_cost` is reached. Bundles that
/// would exceed the limit are skipped. The caller should pre-sort bundles
/// by fee/cost ratio (highest first) to maximize fee revenue — matching
/// L1's create_block_generator() (mempool.py:505).
///
/// The output includes the compressed block program (using CLVM
/// back-references via solution_generator_backrefs() at mempool.py:529),
/// the aggregated signature, additions, removals, and total cost —
/// matching L1's NewBlockGenerator (generator_types.py:28).
pub fn build_block_generator(
    bundles: &[SpendBundle],
    context: &ValidationContext,
    max_cost: Cost,
) -> Result<BlockGeneratorResult, ValidationError>;

/// Output of build_block_generator(). Mirrors L1's NewBlockGenerator
/// (generator_types.py:28).
pub struct BlockGeneratorResult {
    /// The compressed block-level CLVM program
    pub generator: BlockGenerator,
    /// Block heights of referenced previous generators (for cross-block
    /// back-references). Mirrors L1's NewBlockGenerator.block_refs.
    pub block_refs: Vec<u32>,
    /// Aggregated BLS signature across all included bundles
    pub aggregated_signature: Signature,
    /// Coins created by all included spends
    pub additions: Vec<Coin>,
    /// Coins spent by all included spends
    pub removals: Vec<Coin>,
    /// Total CLVM cost of all included spends
    pub cost: Cost,
    /// Number of bundles included (may be less than input if cost limit hit)
    pub bundles_included: usize,
}

/// Validate a block generator and return the combined additions + removals.
///
/// Executes the block-level CLVM program (which produces all spends),
/// validates all conditions, and returns the aggregate SpendResult.
/// Mirrors L1's _run_block() (multiprocess_validation.py:62) followed
/// by validate_block_body() (block_body_validation.py:191).
pub fn validate_block(
    generator: &BlockGenerator,
    generator_refs: &[Vec<u8>],
    context: &ValidationContext,
    config: &ValidationConfig,
    bls_cache: Option<&mut BlsCache>,
) -> Result<SpendResult, ValidationError>;

/// Re-exported from chia-consensus.
pub use chia_consensus::run_block_generator::BlockGenerator;

/// L2 chain state for validation.
///
/// Analogous to the coin_records and blockchain state that Chia L1 passes
/// through check_time_locks() (check_time_locks.py:15) and
/// add_spend_bundle() (mempool_manager.py:480).
///
/// `coin_records` should contain only the coins being spent in this bundle,
/// not the full UTXO set. The caller loads these from their database and
/// passes them in. dig-clvm never touches storage directly.
pub struct ValidationContext {
    /// Current L2 block height
    pub height: u32,
    /// Current block timestamp (seconds since epoch)
    pub timestamp: u64,
    /// DIG network constants (from dig-constants crate).
    /// Contains genesis_challenge, AGG_SIG additional data, cost parameters,
    /// and fork heights. Wraps chia-consensus ConsensusConstants with
    /// DIG-specific values.
    pub constants: dig_constants::NetworkConstants,
    /// Coins being spent in this bundle (coin_id -> CoinRecord).
    /// Only the coins relevant to this validation — NOT the full UTXO set.
    pub coin_records: HashMap<Bytes32, CoinRecord>,
    /// Coins created by earlier bundles in the same block (ephemeral).
    /// Allows later bundles to spend coins that don't yet exist in the
    /// persistent UTXO set, matching Chia L1's ASSERT_EPHEMERAL behavior.
    pub ephemeral_coins: HashSet<Bytes32>,
}

/// L2-specific validation parameters.
///
/// On Chia L1, these are derived from ConsensusConstants and block height
/// via get_flags_for_height_and_constants() (multiprocess_validation.py:19).
/// L2 makes them explicitly configurable.
pub struct ValidationConfig {
    /// Maximum CLVM cost per individual spend
    pub max_cost_per_spend: Cost,
    /// Maximum total CLVM cost per block
    pub max_cost_per_block: Cost,
    /// Execution flags (from chia-consensus).
    ///
    /// Common flags:
    /// - `0` — block validation (default, most permissive)
    /// - `MEMPOOL_MODE` — stricter mempool validation (rejects unknown
    ///   opcodes, stricter cost accounting). Mirrors L1 at mempool.py:13.
    /// - `DONT_VALIDATE_SIGNATURE` — skip BLS signature verification
    ///   (for mempool pre-validation when sigs are checked separately).
    ///
    /// Flags can be combined: `MEMPOOL_MODE | DONT_VALIDATE_SIGNATURE`.
    pub flags: u32,
}

impl Default for ValidationConfig {
    fn default() -> Self {
        Self {
            max_cost_per_spend: L1_MAX_COST_PER_SPEND,
            max_cost_per_block: L2_MAX_COST_PER_BLOCK,
            flags: 0,
        }
    }
}

/// Result of validating a spend bundle.
///
/// This is the primary output of the crate: the set of coin state changes
/// that the caller commits to blockchain state. Mirrors what L1 extracts
/// via tx_removals_and_additions() (generator_tools.py:54).
pub struct SpendResult {
    /// Coins to add to the UTXO set (created by CREATE_COIN conditions)
    pub additions: Vec<Coin>,
    /// Coins to remove from the UTXO set (the spent coins)
    pub removals: Vec<Coin>,
    /// Total fee (sum of removals - sum of additions)
    pub fee: u64,
    /// The full parsed conditions from chia-consensus (for callers that
    /// need to inspect announcements, signatures, time locks, etc.)
    pub conditions: OwnedSpendBundleConditions,
}

/// Validation errors.
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
    #[error("CLVM execution failed: {0}")]
    Clvm(chia_consensus::validation_error::ValidationErr),

    #[error("Coin not found: {0}")]
    CoinNotFound(Bytes32),

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

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

    #[error("Puzzle hash mismatch: {0}")]
    PuzzleHashMismatch(Bytes32),

    #[error("Signature verification failed")]
    SignatureFailed,

    #[error("Conservation violation: input={input}, output={output}")]
    ConservationViolation { input: u64, output: u64 },

    #[error("Cost exceeded: limit={limit}, consumed={consumed}")]
    CostExceeded { limit: Cost, consumed: Cost },

    #[error("Driver error: {0}")]
    Driver(#[from] DriverError),
}

/// L2 cost constants.
/// L1 reference: MAX_BLOCK_COST_CLVM=11_000_000_000 (default_constants.py:68)
pub const L1_MAX_COST_PER_SPEND: Cost = 11_000_000_000;
pub const L2_MAX_COST_PER_BLOCK: Cost = 550_000_000_000; // 50 * L1 per-spend
```

### 5.2 Re-exports (everything else)

```rust
// ── CLVM Runtime ──
pub use clvmr::{self, Allocator, NodePtr, Cost};
pub use clvm_traits::{self, ToClvm, FromClvm};
pub use clvm_utils::{self, tree_hash, curry_tree_hash, CurriedProgram, TreeHash, ToTreeHash};

// ── Chia Protocol Types ──
pub use chia_protocol::{self, Bytes, Bytes32, Coin, CoinSpend, CoinState, Program, SpendBundle};

// ── Consensus Engine ──
pub use chia_consensus::{self, consensus_constants::ConsensusConstants, opcodes};
pub use chia_consensus::spendbundle_conditions::run_spendbundle;
pub use chia_consensus::spendbundle_validation::validate_clvm_and_signature;
pub use chia_consensus::conditions::{
    SpendBundleConditions, OwnedSpendBundleConditions,
    SpendConditions, OwnedSpendConditions,
};

// ── BLS Signatures ──
pub use chia_bls::{self, PublicKey, SecretKey, Signature, aggregate_verify, BlsCache};

// ── SDK Types & Conditions ──
pub use chia_sdk_types::{self, Condition, Conditions, Mod};

// ── DIG Network Constants ──
pub use dig_constants::{self, DIG_MAINNET, DIG_TESTNET, NetworkConstants};

// ── Spend Construction ──
pub use chia_sdk_driver::{self, SpendContext, Layer, Spend, SpendWithConditions, Puzzle, DriverError};

// ── Coin State ──
pub use chia_sdk_coinset::{self, CoinRecord};

// ── Puzzles ──
pub use chia_puzzles;

// ── Block Generator ──
pub use chia_consensus::run_block_generator::BlockGenerator;
```

---

## 6. Validation Flow

### 6.1 Internal Implementation

`validate_spend_bundle()` orchestrates the upstream crates. This mirrors the L1 flow traced through the codebase:

```
L1 transaction flow:
  full_node.py:2755     add_transaction()
  mempool_manager.py:428  pre_validate_spendbundle()
  mempool_manager.py:445  validate_clvm_and_signature()  ← Rust
  mempool_manager.py:480  add_spend_bundle()

L1 block validation flow:
  blockchain.py:286            add_block()
  multiprocess_validation.py:62  _run_block()
  multiprocess_validation.py:69  run_block_generator2()  ← Rust
  block_body_validation.py:191   validate_block_body()
  check_time_locks.py:15         check_time_locks()
  generator_tools.py:54          tx_removals_and_additions()
```

dig-clvm provides two entry points, mirroring L1's mempool and block paths:

**Path A: Per-bundle validation (mempool admission)**

```
validate_spend_bundle(bundle, context, config, bls_cache)
  |
  v
[1. Structural checks]                     ← dig-clvm's own code
  |- Check for duplicate spends in bundle
  |- Verify all spent coins exist in context.coin_records
  |- Verify no coin is already spent
  |
  v
[2. CLVM execution + condition extraction]  ← chia-consensus::run_spendbundle()
  |- Creates allocator via make_allocator(config.flags)
  |- If config.flags includes MEMPOOL_MODE, uses MempoolVisitor
  |  for stricter validation (reject unknown opcodes, etc.)
  |- Runs each puzzle+solution through clvmr
  |- Parses all conditions from CLVM output
  |- Tracks cost per spend and total
  |- Returns SpendBundleConditions
  |
  v
[3. Cost enforcement]                       ← dig-clvm checks L2 limits
  |- conditions.cost <= config.max_cost_per_block
  |
  v
[4. Condition validation]                   ← chia-consensus handles internally
  |- Announcement matching (create vs assert)
  |- Concurrent spend/puzzle assertions
  |- Identity assertions (MY_COIN_ID, etc.)
  |- Time/height locks (relative + absolute)
  |- Ephemeral coin assertions
  |
  v
[5. BLS signature verification]             ← chia-consensus + chia-bls
  |- Unless config.flags includes DONT_VALIDATE_SIGNATURE:
  |    Collect (pubkey, message) pairs with domain separation
  |    Check bls_cache for already-verified pairings
  |    aggregate_verify remaining against bundle.aggregated_signature
  |    Store verified pairings in bls_cache
  |
  v
[6. Conservation check]                     ← dig-clvm's own code
  |- sum(removals) >= sum(additions) + fee
  |
  v
SpendResult { additions, removals, fee, conditions }
```

**Path B: Block-level validation (block building + block validation)**

```
build_block_generator(bundles, context, max_cost)
  |
  v
[1. Assemble spends]                        ← dig-clvm
  |- Iterate bundles in order (caller pre-sorts by fee/cost ratio)
  |- For each bundle: run CLVM to compute cost, skip if exceeds remaining
  |- Collect (coin, puzzle_reveal, solution) from included bundles
  |- Aggregate BLS signatures across included bundles
  |- Call solution_generator_backrefs() to create compressed
  |  block program with CLVM back-references
  |- Return BlockGeneratorResult { generator, sig, additions, removals, cost }
  |
  v
validate_block(generator, refs, context, config, bls_cache)
  |
  v
[2. Execute generator]                      ← chia-consensus::run_block_generator2()
  |- Runs the block-level CLVM program
  |- Produces all spends + conditions in one pass
  |
  v
[3-6. Same as Path A]                       ← cost, conditions, BLS, conservation
  |
  v
SpendResult { additions, removals, fee, conditions }
```

### 6.2 Consumer: Single Spend (using SDK types)

```rust
use dig_clvm::{SpendContext, Condition, Conditions, Coin, Bytes32};

let mut ctx = SpendContext::new();

// Run a puzzle+solution (uses chia-sdk-types::run_puzzle internally)
let puzzle_ptr = ctx.puzzle(puzzle_hash, &puzzle_bytes)?;
let solution_ptr = ctx.alloc(&solution_value)?;
let output = ctx.run(puzzle_ptr, solution_ptr)?;

// Extract conditions using SDK types
let conditions: Vec<Condition<NodePtr>> = ctx.extract(output)?;
```

### 6.3 Consumer: Full Bundle Validation

```rust
use dig_clvm::{
    validate_spend_bundle, ValidationContext, ValidationConfig,
    CoinRecord, BlsCache,
};
use dig_constants::DIG_MAINNET;

let mut ctx = ValidationContext {
    height: current_height,
    timestamp: current_timestamp,
    constants: DIG_MAINNET, // from dig-constants crate
    coin_records: coins_being_spent, // only the coins in this bundle
    ephemeral_coins: HashSet::new(),
};

// BLS cache persists across calls — mempool-verified sigs are reused in block validation
let mut bls_cache = BlsCache::default();

let config = ValidationConfig::default(); // L2 cost limits
let result = validate_spend_bundle(&bundle, &ctx, &config, Some(&mut bls_cache))?;

// Caller commits the additions and removals to blockchain state
for coin in &result.additions {
    state.add_coin(coin);       // new coins enter the UTXO set
}
for coin in &result.removals {
    state.remove_coin(&coin);   // spent coins leave the UTXO set
}
// result.fee is available for block reward accounting
```

### 6.4 Consumer: Block Generator Construction + Validation

```rust
use dig_clvm::{
    build_block_generator, validate_block, ValidationContext,
    ValidationConfig, BlsCache, L2_MAX_COST_PER_BLOCK,
};

// Sort mempool bundles by fee/cost ratio (highest first), then build.
// build_block_generator adds bundles until the cost limit is reached.
mempool_bundles.sort_by(|a, b| fee_rate(b).cmp(&fee_rate(a)));
let block = build_block_generator(
    &mempool_bundles,
    &ctx,
    L2_MAX_COST_PER_BLOCK,
)?;
// block.bundles_included tells you how many fit

// Validate the full block (reuses BLS cache from mempool validation)
let result = validate_block(
    &block.generator,
    &[], // generator_refs from previous blocks (if any)
    &ctx,
    &ValidationConfig::default(),
    Some(&mut bls_cache),
)?;

// result.additions and result.removals cover all spends in the block
// block.aggregated_signature goes into the block header
```

### 6.5 Consumer: Spend Construction (using SDK driver)

```rust
use dig_clvm::{
    SpendContext, StandardLayer, Conditions, Condition, Spend,
    SpendBundle, Coin, Bytes32, SecretKey,
};

let mut ctx = SpendContext::new();

// Use the standard puzzle layer
let layer = StandardLayer::new(synthetic_key);
let conditions = Conditions::new()
    .with(Condition::CreateCoin(CreateCoin {
        puzzle_hash: recipient_ph,
        amount: 1000,
        memos: vec![],
    }));

let spend = layer.spend_with_conditions(&mut ctx, conditions)?;
ctx.spend(coin, spend)?;

let coin_spends = ctx.take();
let bundle = SpendBundle::new(coin_spends, aggregated_sig);
```

---

## 7. Chia L1 Parity

Parity is achieved **by construction**: we use the same Rust crates that Chia full nodes use.

| Concern | How parity is achieved | L1 reference |
|---|---|---|
| CLVM execution | Same `clvmr` runtime, same `ChiaDialect` | [`multiprocess_validation.py:62`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/multiprocess_validation.py#L62) — `_run_block()` |
| Condition parsing | Same `chia-consensus` condition processing with `SpendVisitor` | [`condition_opcodes.py:7-73`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/condition_opcodes.py#L7) — all opcodes |
| Tree hash | Same `clvm_utils::tree_hash()` | [`test_curry_and_treehash.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/_tests/clvm/test_curry_and_treehash.py) — parity tests |
| BLS signatures | Same `chia-bls` with identical domain separation | [`condition_tools.py:74-97`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L74) — `make_aggsig_final_message()` |
| Cost model | Same opcode costs from `chia-consensus::opcodes` | [`condition_costs.py:8-13`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_costs.py#L8) — AGG_SIG, CREATE_COIN, GENERIC |
| Cost limits | Same per-spend limit, configurable per-block | [`default_constants.py:68`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/default_constants.py#L68) — `MAX_BLOCK_COST_CLVM=11B` |
| Puzzle bytecodes | Same `chia-puzzles` constants | [`test_puzzle_drivers.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/_tests/clvm/test_puzzle_drivers.py) |
| Condition types | Same `chia-sdk-types::Condition<T>` enum | Covers all opcodes including [`SEND_MESSAGE/RECEIVE_MESSAGE`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/condition_opcodes.py#L37) |
| Spend construction | Same `chia-sdk-driver::SpendContext` and `Layer` trait | Tested via [`test_singletons.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/_tests/clvm/test_singletons.py) |
| Mempool flags | Same `MEMPOOL_MODE`, `DONT_VALIDATE_SIGNATURE` | [`mempool.py:13-14`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool.py#L13) |
| Block generators | Same `solution_generator_backrefs()` + `run_block_generator2()` | [`mempool.py:529`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool.py#L529), [`multiprocess_validation.py:69`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/multiprocess_validation.py#L69) |
| BLS cache | Same `BlsCache` for mempool→block sig reuse | `chia-bls::BlsCache` |

The only L2-specific divergence is block-level cost limits (`L2_MAX_COST_PER_BLOCK`), which is explicitly configurable via `ValidationConfig`.

---

## 8. Testing Strategy

### 8.1 Simulator-Based Tests (Primary)

Use `chia-sdk-test::Simulator` for end-to-end validation. The Simulator internally uses `chia_consensus::run_spendbundle()` — the same code path as Chia full nodes at [`_run_block()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/multiprocess_validation.py#L62) and [`pre_validate_spendbundle()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool_manager.py#L428).

```rust
use chia_sdk_test::{Simulator, BlsPair};
use dig_clvm::{validate_spend_bundle, ValidationContext, ValidationConfig};

#[test]
fn test_standard_spend() {
    let mut sim = Simulator::new();
    let alice = sim.bls(1_000_000);

    // Build and validate a spend using the Simulator
    let spend_bundle = /* build bundle using SpendContext */;
    let result = sim.new_transaction(spend_bundle.clone());
    assert!(result.is_ok());

    // Also validate through dig-clvm's API for L2 rules
    let ctx = ValidationContext::from_simulator(&sim);
    let result = validate_spend_bundle(&spend_bundle, &ctx, &ValidationConfig::default());
    assert!(result.is_ok());
}
```

### 8.2 Test Scenarios

**Validation tests** (`tests/validation_tests.rs`) — mirrors scenarios from [`test_conditions.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/_tests/core/full_node/test_conditions.py):
- Happy path: standard spend with CREATE_COIN + AGG_SIG_ME
- Double spend within a bundle
- Puzzle hash mismatch
- Conservation violation (outputs > inputs)
- Announcement graph (create + assert across coins)
- Missing announcement assertion
- Height/time lock enforcement (absolute and relative) — as validated by [`check_time_locks()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/check_time_locks.py#L15)
- Before-height/before-time lock enforcement
- Ephemeral coins (created and spent in same block)
- Concurrent spend assertions
- Multi-spend bundles with fee

**Parity tests** (`tests/parity_tests.rs`):
- Known standard transaction puzzle hashes from `chia-puzzles`
- Round-trip: build spend via `SpendContext` -> validate via both Simulator and `validate_spend_bundle()`
- All 8 `AGG_SIG_*` variants produce correct domain-separated messages — verified against the lookup table at [`condition_tools.py:87-97`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/condition_tools.py#L87)
- `tree_hash` of known puzzles matches published Chia puzzle hashes — see [`test_curry_and_treehash.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/_tests/clvm/test_curry_and_treehash.py)

**Cost tests** (`tests/cost_tests.rs`):
- L2 block cost limit enforcement
- Per-spend cost limit (same as L1's [`MAX_BLOCK_COST_CLVM=11B`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/default_constants.py#L68))
- Cost boundary: program at exactly max_cost succeeds
- Cost boundary: program at max_cost + 1 fails

**Block generator tests** (`tests/block_tests.rs`) — mirrors [`create_block_generator()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool.py#L505) and [`simple_solution_generator_backrefs()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/bundle_tools.py#L15):
- Build generator from single bundle, validate, compare additions/removals to per-bundle validation
- Build generator from multiple bundles, verify all spends included
- Back-reference deduplication: bundles sharing the same puzzle produce a smaller generator than naive serialization
- Generator with `generator_refs` from previous blocks
- Round-trip: `build_block_generator()` output validates identically via `validate_block()`

**BLS cache tests** (`tests/cache_tests.rs`):
- Validate bundle with empty cache, verify cache is populated
- Re-validate same bundle, verify cache hit (no re-verification)
- Validate bundle in mempool (with cache), then validate same bundle in block (cache reused)
- Cache miss: modified signature invalidates cache entry

**Condition tests** (`tests/condition_tests.rs`) — covers all opcodes from [`condition_opcodes.py:7-73`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/condition_opcodes.py#L7):
- Build conditions via `Conditions<T>` builder
- Execute via SpendContext
- Verify parsed output matches for each opcode
- Unknown opcode passthrough
- `SEND_MESSAGE(66)` / `RECEIVE_MESSAGE(67)` — tested on L1 at [`test_message_conditions.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/_tests/clvm/test_message_conditions.py)
- `MEMPOOL_MODE` flag rejects unknown opcodes that block validation accepts

### 8.3 Parity by Construction

Because tests use `chia-sdk-test::Simulator` which internally runs `chia_consensus::run_spendbundle()`, any test that passes in the Simulator is **by definition** Chia L1 compatible. We don't need separate "parity" testing infrastructure — it's built into the test framework.

---

## 9. Design Decisions

### 9.1 Why Maximize Chia Crate Reuse

The previous approach (in `l2_driver_state_channel`) reimplemented condition parsing, tree hashing, currying helpers, and validation logic. This created:
- Maintenance burden: upstream changes required manual porting
- Parity risk: subtle differences in condition parsing or cost accounting
- Test duplication: testing behavior already tested upstream

By building on the Chia crates directly, we get:
- Parity by construction (same code = same behavior)
- Upstream bug fixes for free
- Access to the full SDK (SpendContext, Layer trait, puzzle drivers, Simulator)
- Smaller codebase to maintain (~200 lines of L2-specific code vs ~3000 lines of reimplemented CLVM logic)

### 9.2 Why `chia-consensus` for Validation Instead of Hand-Rolled

`chia-consensus::run_spendbundle()` handles the entire CLVM execution + condition extraction + announcement matching + time lock validation pipeline. This is the same engine Chia L1 uses at [`validate_clvm_and_signature()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool_manager.py#L445) (`mempool_manager.py:445`). Using it directly:
- Eliminates the custom condition parser (640+ lines in the old `clvm.rs`)
- Eliminates the custom validation logic (500+ lines in the old `validation.rs`)
- Automatically supports [`SEND_MESSAGE(66)` / `RECEIVE_MESSAGE(67)`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/condition_opcodes.py#L37) and future opcodes
- Uses the same `SpendVisitor` pattern Chia uses for mempool vs block validation

### 9.3 Why `chia-sdk-types::Condition<T>` Instead of Custom Enum

The SDK's `Condition<T>` is generic over the node representation, supports all opcodes (including NFT/CAT-specific ones), and is directly serializable to/from CLVM via `ToClvm`/`FromClvm`. The previous custom enum:
- Missed `SEND_MESSAGE` and `RECEIVE_MESSAGE`
- Required manual synchronization with upstream opcode changes
- Couldn't be used for spend construction (only parsing)

The SDK type works for both construction (via `Conditions<T>` builder) and parsing (via `FromClvm`), eliminating the need for separate types.

### 9.4 Why `SpendContext` Instead of Raw Allocator Wrappers

The spec previously proposed custom `allocator.rs`, `node_codec.rs`, and `clvm_codec.rs` wrappers. `SpendContext` already provides all of this plus puzzle caching, currying, and spend collection. It's the standard way to interact with CLVM in the Chia Rust ecosystem.

### 9.5 Why `ValidationConfig` with Configurable Cost Limits

The only L2-specific parameter that differs from Chia L1 is the block-level cost limit. L1 uses [`MAX_BLOCK_COST_CLVM=11_000_000_000`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/default_constants.py#L68). Rather than forking the consensus code, we wrap it with configurable limits. The per-spend limit matches L1 exactly.

### 9.6 Soft Fork Flags

`chia-consensus::get_flags_for_height_and_constants()` (imported at [`multiprocess_validation.py:19`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/multiprocess_validation.py#L19)) computes the correct execution flags for any block height, handling hard fork transitions automatically. `dig-clvm` exposes this via `ValidationConfig::flags` so L2 can track upstream fork activations.

---

## 10. Resolved Decisions

1. **Block-level generator: Yes.** `dig-clvm` supports block-level generator construction and execution, matching Chia L1's [`run_block_generator()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/multiprocess_validation.py#L71) and [`solution_generator_backrefs()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool.py#L529) (`mempool.py:529`). This enables compact block encoding where repeated puzzle bytecodes are deduplicated via CLVM back-references. The crate provides both per-bundle validation (for mempool admission) and block-generator-level validation (for block building and block validation). See [`simple_solution_generator_backrefs()`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/bundle_tools.py#L15) (`bundle_tools.py:15`) for L1's approach.

2. **BLS cache: dig-clvm owns it.** `validate_spend_bundle()` and `validate_block()` accept an optional `&mut BlsCache` parameter. When provided, verified signature pairings are cached and reused across calls — avoiding re-verification when a mempool-validated bundle later appears in a block. This matches Chia L1's pattern where `BLSCache` is passed through the validation pipeline. When `None`, signatures are verified from scratch every time.

3. **SpendVisitor customization: Yes.** `dig-clvm` exposes the `SpendVisitor` hook from `chia-consensus`, enabling different validation strictness for mempool admission vs block validation. The `ValidationConfig::flags` field accepts `MEMPOOL_MODE` ([`mempool.py:13`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/mempool.py#L13)) for stricter mempool rules (reject unknown opcodes, stricter cost accounting). This allows L2-specific mempool policies (e.g., minimum fee thresholds, puzzle blacklists) to be enforced at the CLVM validation layer rather than requiring external filtering.

4. **Individual crates.** `dig-clvm` depends on `chia-sdk-types`, `chia-sdk-driver`, `chia-sdk-coinset`, etc. individually rather than the `chia-wallet-sdk` umbrella. This avoids pulling in RPC client code, wallet utilities, and other modules that a consensus crate doesn't need. Each sub-crate is pinned to the same version for consistency.