Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
433 changes: 355 additions & 78 deletions PLAN.md

Large diffs are not rendered by default.

148 changes: 135 additions & 13 deletions backends/mock/src/backend.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use clvm_zk_core::verify_ecdsa_signature_with_hasher;
use clvm_zk_core::{
compile_chialisp_to_bytecode, compute_coin_commitment, compute_nullifier,
compute_serial_commitment, create_veil_evaluator, enforce_ring_balance, is_clvm_nil,
parse_variable_length_amount, run_clvm_with_conditions, serialize_params_to_clvm,
compile_chialisp_to_bytecode, compute_coin_commitment, compute_genesis_nullifier,
compute_nullifier_v2, compute_serial_commitment, create_veil_evaluator, enforce_ring_balance,
is_clvm_nil, parse_variable_length_amount, run_clvm_with_conditions, serialize_params_to_clvm,
verify_merkle_proof, ClvmResult, ClvmZkError, CoinMode, Condition, ProgramParameter,
ProofOutput, ZKClvmResult, BLS_DST,
};
Expand Down Expand Up @@ -242,17 +242,20 @@ impl MockBackend {
// BALANCE ENFORCEMENT (critical security check)
// verify sum(inputs) == sum(outputs) and tail_hash consistency
// MUST run BEFORE CREATE_COIN transformation
enforce_ring_balance(&inputs, &conditions).map_err(|e| {
ClvmZkError::ProofGenerationFailed(format!("balance enforcement failed: {}", e))
})?;
// skipped for Mint: no input coin, balance enforced by TAIL program instead
if !matches!(inputs.coin_mode, CoinMode::Mint(_)) {
enforce_ring_balance(&inputs, &conditions).map_err(|e| {
ClvmZkError::ProofGenerationFailed(format!("balance enforcement failed: {}", e))
})?;
}

validate_signature_conditions(&conditions)?;
let tail_hash = inputs.tail_hash.unwrap_or([0u8; 32]);
let final_output =
transform_create_coin_conditions(&mut conditions, output_bytes, tail_hash)?;

let clvm_output = ClvmResult {
output: final_output,
output: final_output.clone(),
cost: 0,
};

Expand Down Expand Up @@ -343,18 +346,136 @@ impl MockBackend {
}
}

Some(compute_nullifier(
Some(compute_nullifier_v2(
hash_data,
&tail_hash,
&commitment_data.serial_number,
&program_hash,
commitment_data.amount,
))
}
CoinMode::Execute => None,
CoinMode::Mint(_) => {
return Err(ClvmZkError::ProofGenerationFailed(
"mint mode not yet implemented in mock backend".to_string(),
))
CoinMode::Mint(mint_data) => {
// Step 1: compile tail_source and verify hash matches inputs.tail_hash
let (tail_bytecode, tail_program_hash) =
compile_chialisp_to_bytecode(hash_data, &mint_data.tail_source).map_err(
|e| {
ClvmZkError::ProofGenerationFailed(format!(
"TAIL compilation failed: {:?}",
e
))
},
)?;

if let Some(expected_tail_hash) = inputs.tail_hash {
if tail_program_hash != expected_tail_hash {
return Err(ClvmZkError::ProofGenerationFailed(
"tail_hash mismatch: tail_source does not compile to the committed tail_hash".to_string()
));
}
}

// Step 2: execute TAIL with tail_params — must return truthy
let tail_args = serialize_params_to_clvm(&mint_data.tail_params);
let (tail_output, _) =
run_clvm_with_conditions(&evaluator, &tail_bytecode, &tail_args, max_cost)
.map_err(|e| {
ClvmZkError::ProofGenerationFailed(format!(
"TAIL execution failed: {:?}",
e
))
})?;
if is_clvm_nil(&tail_output) {
return Err(ClvmZkError::ProofGenerationFailed(
"TAIL authorization failed: TAIL returned nil — must return truthy to authorize mint".to_string(),
));
}

// Step 3: handle genesis coin (single-issuance enforcement)
let genesis_nullifier = if let Some(genesis) = &mint_data.genesis_coin {
let computed_serial =
compute_serial_commitment(hash_data, &genesis.serial_number, &genesis.serial_randomness);
if computed_serial != genesis.serial_commitment {
return Err(ClvmZkError::ProofGenerationFailed(
"genesis coin: serial commitment verification failed".to_string(),
));
}

let computed_coin = compute_coin_commitment(
hash_data,
genesis.tail_hash,
genesis.amount,
&genesis.puzzle_hash,
&computed_serial,
);
if computed_coin != genesis.coin_commitment {
return Err(ClvmZkError::ProofGenerationFailed(
"genesis coin: coin commitment verification failed".to_string(),
));
}

verify_merkle_proof(
hash_data,
computed_coin,
&genesis.merkle_path,
usize::try_from(genesis.leaf_index)
.expect("genesis leaf_index exceeds usize"),
genesis.merkle_root,
)
.map_err(|e| {
ClvmZkError::ProofGenerationFailed(format!(
"genesis coin merkle verification failed: {}",
e
))
})?;

Some(compute_genesis_nullifier(
hash_data,
&genesis.serial_number,
&genesis.tail_hash,
))
} else {
None
};

// Steps 4-6: compute output serial_commitment and coin_commitment
let output_serial_commitment = compute_serial_commitment(
hash_data,
&mint_data.output_serial,
&mint_data.output_rand,
);
let tail_hash = inputs.tail_hash.unwrap_or(tail_program_hash);
let output_coin_commitment = compute_coin_commitment(
hash_data,
tail_hash,
mint_data.output_amount,
&mint_data.output_puzzle_hash,
&output_serial_commitment,
);

// Step 7: emit proof — genesis_nullifier in nullifiers, coin_commitment in public_values
let nullifiers = genesis_nullifier.map(|n| vec![n]).unwrap_or_default();
let proof_output = ProofOutput {
program_hash,
nullifiers,
clvm_res: ClvmResult {
output: final_output,
cost: 0,
},
proof_type: 3, // Mint
public_values: vec![output_coin_commitment.to_vec()],
};

let proof_bytes = borsh::to_vec(&proof_output).map_err(|e| {
ClvmZkError::SerializationError(format!(
"failed to serialize mint proof: {e}"
))
})?;

return Ok(ZKClvmResult {
proof_output,
proof_bytes,
});
}
};

Expand Down Expand Up @@ -418,8 +539,9 @@ impl MockBackend {
}
}

nullifiers.push(compute_nullifier(
nullifiers.push(compute_nullifier_v2(
hash_data,
&coin.tail_hash,
&coin_data.serial_number,
&coin_program_hash,
coin_data.amount,
Expand Down
121 changes: 110 additions & 11 deletions backends/risc0/guest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ use risc0_zkvm::guest::env;
use risc0_zkvm::sha::{Impl, Sha256 as RiscSha256};

use clvm_zk_core::{
compile_chialisp_to_bytecode, compute_coin_commitment, compute_nullifier,
compute_serial_commitment, create_veil_evaluator, is_clvm_nil, parse_variable_length_amount,
run_clvm_with_conditions, serialize_params_to_clvm, verify_merkle_proof, ClvmResult, CoinMode,
Input, ProofOutput, BLS_DST,
compile_chialisp_to_bytecode, compute_coin_commitment, compute_genesis_nullifier,
compute_nullifier_v2, compute_serial_commitment, create_veil_evaluator, is_clvm_nil,
parse_variable_length_amount, run_clvm_with_conditions, serialize_params_to_clvm,
verify_merkle_proof, ClvmResult, CoinMode, Input, ProofOutput, BLS_DST,
};

use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve};
Expand Down Expand Up @@ -154,8 +154,11 @@ fn main() {
// ============================================================================
// verify sum(inputs) == sum(outputs) and tail_hash consistency
// MUST run BEFORE CREATE_COIN transformation (which replaces args)
clvm_zk_core::enforce_ring_balance(&private_inputs, &conditions)
.expect("balance enforcement failed");
// skipped for Mint: no input coin, balance enforced by TAIL program instead
if !matches!(private_inputs.coin_mode, CoinMode::Mint(_)) {
clvm_zk_core::enforce_ring_balance(&private_inputs, &conditions)
.expect("balance enforcement failed");
}

// Transform CREATE_COIN conditions for output privacy
let mut has_transformations = false;
Expand Down Expand Up @@ -282,17 +285,112 @@ fn main() {
);
}

Some(compute_nullifier(
Some(compute_nullifier_v2(
risc0_hasher,
&tail_hash,
&commitment_data.serial_number,
&program_hash,
commitment_data.amount,
))
}
CoinMode::Execute => None,
// host-side guard in risc0/src/lib.rs prevents this from being reached;
// this arm is a secondary defense — the guest cannot produce a valid proof for Mint yet.
CoinMode::Mint(_) => panic!("mint mode not yet implemented in this guest version"),
CoinMode::Mint(mint_data) => {
// Step 1: compile tail_source and verify hash matches private_inputs.tail_hash
let (tail_bytecode, tail_program_hash) =
compile_chialisp_to_bytecode(risc0_hasher, &mint_data.tail_source)
.expect("TAIL compilation failed");

if let Some(expected_tail_hash) = private_inputs.tail_hash {
assert_eq!(
tail_program_hash, expected_tail_hash,
"tail_hash mismatch: tail_source does not compile to the committed tail_hash"
);
}

// Step 2: execute TAIL — must return truthy to authorize mint
let tail_args = serialize_params_to_clvm(&mint_data.tail_params);
let (tail_output, _) =
run_clvm_with_conditions(&evaluator, &tail_bytecode, &tail_args, max_cost)
.expect("TAIL execution failed");
assert!(
!is_clvm_nil(&tail_output),
"TAIL authorization failed: TAIL returned nil — must return truthy to authorize mint"
);

// Step 3: handle genesis coin (single-issuance enforcement)
let genesis_nullifier = if let Some(genesis) = &mint_data.genesis_coin {
let computed_serial = compute_serial_commitment(
risc0_hasher,
&genesis.serial_number,
&genesis.serial_randomness,
);
assert_eq!(
computed_serial, genesis.serial_commitment,
"genesis coin: serial commitment verification failed"
);

let computed_coin = compute_coin_commitment(
risc0_hasher,
genesis.tail_hash,
genesis.amount,
&genesis.puzzle_hash,
&computed_serial,
);
assert_eq!(
computed_coin, genesis.coin_commitment,
"genesis coin: coin commitment verification failed"
);

verify_merkle_proof(
risc0_hasher,
computed_coin,
&genesis.merkle_path,
usize::try_from(genesis.leaf_index)
.expect("genesis leaf_index exceeds usize"),
genesis.merkle_root,
)
.expect("genesis coin merkle verification failed");

Some(compute_genesis_nullifier(
risc0_hasher,
&genesis.serial_number,
&genesis.tail_hash,
))
} else {
None
};

// Steps 4-6: compute output serial_commitment and coin_commitment
let output_serial_commitment = compute_serial_commitment(
risc0_hasher,
&mint_data.output_serial,
&mint_data.output_rand,
);
let tail_hash = private_inputs.tail_hash.unwrap_or(tail_program_hash);
let output_coin_commitment = compute_coin_commitment(
risc0_hasher,
tail_hash,
mint_data.output_amount,
&mint_data.output_puzzle_hash,
&output_serial_commitment,
);

// Step 7: emit — genesis_nullifier in nullifiers, coin_commitment in public_values
let nullifiers = genesis_nullifier.map(|n| vec![n]).unwrap_or_default();
let end_cycles = env::cycle_count();
let total_cycles = end_cycles.saturating_sub(start_cycles);
env::commit(&ProofOutput {
program_hash,
nullifiers,
clvm_res: ClvmResult {
output: final_output,
cost: total_cycles,
},
proof_type: 3, // Mint
public_values: vec![output_coin_commitment.to_vec()],
});
return;
}
};

// collect nullifiers: primary coin + additional coins for ring spends
Expand Down Expand Up @@ -378,8 +476,9 @@ fn main() {
);
}

nullifiers.push(compute_nullifier(
nullifiers.push(compute_nullifier_v2(
risc0_hasher,
&coin.tail_hash,
&coin_data.serial_number,
&coin_program_hash,
coin_data.amount,
Expand Down
5 changes: 3 additions & 2 deletions backends/risc0/guest_settlement/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extern crate alloc;
use alloc::vec::Vec;

use clvm_zk_core::{
compile_chialisp_to_bytecode, compute_coin_commitment, compute_nullifier,
compile_chialisp_to_bytecode, compute_coin_commitment, compute_nullifier_v2,
compute_serial_commitment, create_veil_evaluator, is_clvm_nil, run_clvm_with_conditions,
serialize_params_to_clvm, verify_merkle_proof, ProgramParameter,
};
Expand Down Expand Up @@ -191,8 +191,9 @@ fn main() {
// let commitments_cycles = env::cycle_count();

// compute taker's nullifier
let taker_nullifier = compute_nullifier(
let taker_nullifier = compute_nullifier_v2(
risc0_hasher,
&input.taker_tail_hash,
&input.taker_coin.serial_number,
&input.taker_coin.puzzle_hash,
input.taker_coin.amount,
Expand Down
8 changes: 0 additions & 8 deletions backends/risc0/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,6 @@ impl Risc0Backend {
}
}

// host-side guard: reject CoinMode::Mint before reaching the guest.
// the guest panics on Mint (not yet implemented); this surfaces a clean error instead.
if matches!(inputs.coin_mode, CoinMode::Mint(_)) {
return Err(ClvmZkError::ProofGenerationFailed(
"mint mode not yet supported in risc0 backend".to_string(),
));
}

// host-side guard: CAT spend without tail_source produces an opaque guest panic.
// surface a clean error here instead.
let is_cat = inputs.tail_hash.map_or(false, |h| h != [0u8; 32]);
Expand Down
Loading