DIG L2 CLVM consensus engine. Validates spend bundles and computes coin additions/removals for DIG validators.
Built as a thin orchestration layer on top of the Chia crate ecosystem. Never reimplements what chia-consensus, chia-sdk-types, chia-sdk-driver, clvmr, clvm-utils, or chia-bls already provide.
Input: SpendBundle (coin spends + aggregated BLS signature)
Output: SpendResult { additions: Vec<Coin>, removals: Vec<Coin>, fee: u64 }
or ValidationError
Blockchain state management (UTXO persistence, Merkle roots, block storage) is out of scope. This crate validates and computes; the caller commits to state.
pub fn validate_spend_bundle(
bundle: &SpendBundle,
context: &ValidationContext,
config: &ValidationConfig,
bls_cache: Option<&mut BlsCache>,
) -> Result<SpendResult, ValidationError>;Validates a spend bundle for mempool admission or block inclusion. Internally delegates CLVM execution, condition parsing, and BLS signature verification to chia-consensus.
Validation pipeline:
- Structural checks (duplicate spends, coin existence, already-spent)
- CLVM execution + condition extraction via
chia-consensus::run_spendbundle() - Cost enforcement against
config.max_cost_per_block - BLS signature verification (skipped if
DONT_VALIDATE_SIGNATUREflag set) - Conservation check (
sum(removals) >= sum(additions) + fee)
pub fn build_block_generator(
bundles: &[SpendBundle],
context: &ValidationContext,
max_cost: Cost,
) -> Result<BlockGeneratorResult, ValidationError>;Iterates bundles in order, adds each until max_cost is reached. Caller pre-sorts by fee/cost ratio. Produces a compressed CLVM block program via solution_generator_backrefs().
pub fn validate_block(
generator: &[u8],
generator_refs: &[Vec<u8>],
context: &ValidationContext,
config: &ValidationConfig,
bls_cache: Option<&mut BlsCache>,
aggregated_signature: &Signature,
) -> Result<SpendResult, ValidationError>;Executes a block-level CLVM program via chia-consensus::run_block_generator2(), validates conditions, and returns additions/removals.
pub struct ValidationContext {
pub height: u32, // Current L2 block height
pub timestamp: u64, // Seconds since epoch
pub constants: NetworkConstants, // From dig-constants crate
pub coin_records: HashMap<Bytes32, CoinRecord>, // Only coins being spent (NOT full UTXO set)
pub ephemeral_coins: HashSet<Bytes32>, // Coins from earlier bundles in same block
}coin_records contains only the coins relevant to this validation. The caller loads them from their database. This crate never touches storage.
ephemeral_coins tracks coins created by earlier bundles in the same block, enabling same-block create+spend (Chia L1's ASSERT_EPHEMERAL behavior).
pub struct ValidationConfig {
pub max_cost_per_spend: Cost, // Default: 11_000_000_000 (matches Chia L1)
pub max_cost_per_block: Cost, // Default: 550_000_000_000 (50x L1)
pub flags: u32, // 0 = block validation, MEMPOOL_MODE = stricter,
// DONT_VALIDATE_SIGNATURE = skip BLS
}Flags can be combined: MEMPOOL_MODE | DONT_VALIDATE_SIGNATURE.
pub struct SpendResult {
pub additions: Vec<Coin>, // Coins to add to UTXO set
pub removals: Vec<Coin>, // Coins to remove from UTXO set
pub fee: u64, // sum(removals) - sum(additions)
pub conditions: OwnedSpendBundleConditions, // Full conditions from chia-consensus
}additions and removals are what the caller commits to blockchain state. conditions is the raw output from chia-consensus for callers that need to inspect announcements, signatures, or time locks.
pub struct BlockGeneratorResult {
pub generator: Vec<u8>, // Compressed CLVM block program
pub block_refs: Vec<u32>, // Referenced previous block heights
pub aggregated_signature: Signature, // Combined BLS signature
pub additions: Vec<Coin>, // All coins created
pub removals: Vec<Coin>, // All coins spent
pub cost: Cost, // Total CLVM cost
pub bundles_included: usize, // How many bundles fit in the block
}pub enum ValidationError {
Clvm(String), // CLVM execution failed
CoinNotFound(Bytes32), // Coin not in coin_records or ephemeral_coins
AlreadySpent(Bytes32), // Coin marked spent in coin_records
DoubleSpend(Bytes32), // Same coin spent twice in one bundle
PuzzleHashMismatch(Bytes32), // tree_hash(puzzle_reveal) != coin.puzzle_hash
SignatureFailed, // BLS aggregate signature invalid
ConservationViolation { input: u64, output: u64 }, // Outputs exceed inputs
CostExceeded { limit: Cost, consumed: Cost }, // CLVM cost over budget
Driver(DriverError), // Spend driver error
}pub const L1_MAX_COST_PER_SPEND: Cost = 11_000_000_000; // Matches Chia L1
pub const L2_MAX_COST_PER_BLOCK: Cost = 550_000_000_000; // 50x L1 per-spendAll Chia ecosystem types are re-exported so callers need only depend on dig-clvm:
| Source Crate | Re-exported Types |
|---|---|
clvmr |
Allocator, NodePtr, Cost |
clvm-traits |
ToClvm, FromClvm |
clvm-utils |
tree_hash, CurriedProgram, TreeHash, ToTreeHash |
chia-protocol |
Coin, CoinSpend, SpendBundle, Program, Bytes32, CoinState |
chia-bls |
PublicKey, SecretKey, Signature, BlsCache, aggregate_verify |
chia-consensus |
ConsensusConstants, opcodes |
chia-sdk-types |
Condition, Conditions, Mod |
chia-sdk-driver |
SpendContext, Layer, Spend, SpendWithConditions, Puzzle, DriverError |
chia-sdk-coinset |
CoinRecord |
chia-puzzles |
All puzzle bytecodes |
dig-constants |
NetworkConstants, DIG_MAINNET, DIG_TESTNET |
use std::collections::{HashMap, HashSet};
use dig_clvm::{
validate_spend_bundle, ValidationContext, ValidationConfig,
BlsCache, DIG_MAINNET, CoinRecord,
};
let ctx = ValidationContext {
height: current_height,
timestamp: current_timestamp,
constants: DIG_MAINNET.clone(),
coin_records: coins_being_spent, // HashMap<Bytes32, CoinRecord>
ephemeral_coins: HashSet::new(),
};
let mut cache = BlsCache::default();
let config = ValidationConfig::default();
let result = validate_spend_bundle(&bundle, &ctx, &config, Some(&mut cache))?;
// Commit to state
for coin in &result.additions { state.add_coin(coin); }
for coin in &result.removals { state.remove_coin(coin); }use dig_clvm::{
build_block_generator, validate_block, ValidationConfig,
BlsCache, L2_MAX_COST_PER_BLOCK,
};
// Sort bundles by fee/cost ratio, then build
let block = build_block_generator(&bundles, &ctx, L2_MAX_COST_PER_BLOCK)?;
// Validate the block
let result = validate_block(
&block.generator,
&[],
&ctx,
&ValidationConfig::default(),
Some(&mut cache),
&block.aggregated_signature,
)?;use chia_consensus::flags::DONT_VALIDATE_SIGNATURE;
let config = ValidationConfig {
flags: DONT_VALIDATE_SIGNATURE,
..ValidationConfig::default()
};
let result = validate_spend_bundle(&bundle, &ctx, &config, None)?;src/
lib.rs -- Re-exports only
consensus/
mod.rs -- Module root
validate.rs -- validate_spend_bundle()
block.rs -- build_block_generator(), validate_block()
context.rs -- ValidationContext
config.rs -- ValidationConfig, cost constants
result.rs -- SpendResult, BlockGeneratorResult
cache.rs -- BLS cache management
error.rs -- ValidationError
No async, no IO, no storage. Pure computation. All CLVM execution, condition parsing, tree hashing, BLS verification, and cost accounting delegated to the Chia crate ecosystem.
[dependencies]
clvmr = "0.14"
clvm-traits = "0.26"
clvm-utils = "0.26"
chia-protocol = "0.26"
chia-consensus = "0.26"
chia-bls = "0.26"
chia-traits = "0.26"
chia-sdk-types = "0.30"
chia-sdk-driver = { version = "0.30", features = ["action-layer"] }
chia-sdk-coinset = "0.30"
chia-puzzles = "0.20"
dig-constants = { path = "../dig-constants" }
thiserror = "2"
hex = "0.4"
[dev-dependencies]
chia-sdk-test = "0.30"55 dedicated test files, 154 tests. One file per requirement (tests/vv_req_{prefix}_{nnn}.rs).
cargo test # Run all 154 tests
cargo test vv_req_val # Run all validation tests
cargo test vv_req_blk # Run all block generator tests
cargo test vv_req_con # Run all constants tests| Document | Path |
|---|---|
| Specification | docs/resources/SPEC.md |
| Requirements (55) | docs/requirements/ |
| Workflow | docs/prompt/start.md |