Skip to main content
The Work Only Circuit is a lightweight zkVM circuit that verifies Header Chain Circuit proofs and extracts their accumulated proof-of-work values. It produces compact proofs used by watchtowers to challenge operators in the Clementine bridge protocol.

Purpose

The circuit serves a specialized role in Clementine’s challenge mechanism:
  • Proof-of-Work Extraction: Isolates the total_work value from full header chain proofs
  • Work Compression: Converts 256-bit work values to efficient 128-bit representations
  • Watchtower Challenges: Enables watchtowers to submit compact work proofs when challenging operators
  • Groth16 Proof Generation: Produces small proofs suitable for Bitcoin Script verification

Core Logic

The circuit performs a focused verification and extraction process:

1. Method ID Validation

const HEADER_CHAIN_METHOD_ID: [u32; 8] = {
    match option_env!("BITCOIN_NETWORK") {
        Some(network) if matches!(network.as_bytes(), b"mainnet") => MAINNET_HEADER_CHAIN_METHOD_ID,
        Some(network) if matches!(network.as_bytes(), b"testnet4") => TESTNET4_HEADER_CHAIN_METHOD_ID,
        Some(network) if matches!(network.as_bytes(), b"signet") => SIGNET_HEADER_CHAIN_METHOD_ID,
        Some(network) if matches!(network.as_bytes(), b"regtest") => REGTEST_HEADER_CHAIN_METHOD_ID,
        None => MAINNET_HEADER_CHAIN_METHOD_ID,
        _ => panic!("Invalid network type"),
    }
};

assert_eq!(
    HEADER_CHAIN_METHOD_ID, input.header_chain_circuit_output.method_id,
    "Invalid method ID for header chain circuit"
);
  • Ensures the input proof comes from a compatible Header Chain Circuit
  • Prevents verification of proofs from wrong network types (mainnet vs testnet)
  • Compile-time constant matching the expected network configuration

2. Proof Verification

env::verify(
    input.header_chain_circuit_output.method_id,
    &borsh::to_vec(&input.header_chain_circuit_output).unwrap(),
)
.unwrap();
  • Cryptographically verifies the entire Header Chain Circuit output using RISC Zero’s env::verify()
  • This is a zero-knowledge check proving the integrity of the header chain proof
  • Ensures the proof was generated by a valid Header Chain Circuit execution

3. Work Extraction and Conversion

let total_work_u256: U256 =
    U256::from_be_bytes(input.header_chain_circuit_output.chain_state.total_work);
let words = work_conversion(total_work_u256);

fn work_conversion(work: U256) -> [u8; 16] {
    let (_, work): (U128, U128) = work.into();
    work.to_be_bytes()
}
  • Extracts the 256-bit total_work from the verified chain state
  • Converts to 128-bit representation by truncating upper 128 bits
  • Returns as big-endian byte array for efficient serialization
Bitcoin’s current total accumulated work is far below 2^128. The 128-bit representation provides:
  • Adequate Precision: Sufficient for comparing chain work for centuries at current hash rates
  • Storage Efficiency: Reduces proof size by 50% (16 bytes vs 32 bytes)
  • Script Compatibility: Easier to manipulate in Bitcoin Script constraints
The upper 128 bits are effectively zero for all realistic Bitcoin chains, making this truncation safe.

4. Output Commitment

guest.commit(&WorkOnlyCircuitOutput {
    work_u128: words,
    genesis_state_hash: input.header_chain_circuit_output.genesis_state_hash,
});
Commits the compact output:
  • work_u128: 128-bit accumulated proof-of-work (16 bytes)
  • genesis_state_hash: Original genesis state hash from header chain proof (32 bytes)
Total output: 48 bytes of committed data

Watchtower Challenge Flow

The Work Only Circuit enables watchtowers to participate in the challenge mechanism:
1

Header Chain Proof Generation

Watchtower generates a Header Chain Proof tracking their view of the canonical Bitcoin chain
2

Work Only Proof Creation

Watchtower executes this circuit with their Header Chain Proof as input, producing a compact Work Only Proof (WOP)
3

Groth16 Conversion

The Work Only Proof is converted to Groth16 format (128-byte compressed proof)
4

Challenge Transaction

Watchtower submits challenge transaction containing the Groth16 proof and work value in OP_RETURN outputs
5

Bridge Circuit Verification

Bridge Circuit verifies all watchtower Groth16 proofs and identifies the maximum valid work submitted
6

Work Comparison

If any watchtower’s work exceeds the Operator’s work, the Operator is proven to be following a non-canonical chain

Groth16 Proof Format

Watchtowers submit their Work Only Proofs as Groth16 proofs in Bitcoin transactions:

Transaction Output Formats

Watchtower challenge transactions use two valid output structures (as verified in Bridge Circuit): Single OP_RETURN Output
Output 0: OP_RETURN [144 bytes]
  - Bytes 0-127:   Compressed Groth16 proof
  - Bytes 128-143: Total work (16 bytes)
Three Output Format
Output 0: P2TR with 32 bytes of Groth16 proof in pubkey
Output 1: P2TR with 32 bytes of Groth16 proof in pubkey  
Output 2: OP_RETURN [80 bytes]
  - Bytes 0-63:  Remaining Groth16 proof bytes
  - Bytes 64-79: Total work (16 bytes)
The Bridge Circuit extracts and reconstructs the full 128-byte Groth16 proof from either format.
The Bridge Circuit verifies Work Only Groth16 proofs using:
fn convert_to_groth16_and_verify(
    compressed_proof: &[u8; 128],
    total_work: [u8; 16],
    image_id: &[u8; 32],
    genesis_state_hash: [u8; 32],
) -> bool {
    let seal = match CircuitGroth16Proof::from_compressed(compressed_proof) {
        Ok(seal) => seal,
        Err(_) => return false,
    };

    let groth16_proof = CircuitGroth16WithTotalWork::new(
        seal, total_work, genesis_state_hash
    );

    groth16_proof.verify(image_id)
}
The verifier reconstructs the Work Only Circuit output from total_work and genesis_state_hash, then verifies the Groth16 proof against the prepared verification key.

Circuit Input/Output

Input Structure

pub struct WorkOnlyCircuitInput {
    pub header_chain_circuit_output: BlockHeaderCircuitOutput,
}

// Where BlockHeaderCircuitOutput contains:
pub struct BlockHeaderCircuitOutput {
    pub method_id: [u32; 8],
    pub genesis_state_hash: [u8; 32],
    pub chain_state: ChainState,  // Contains total_work field
}

Output Structure

pub struct WorkOnlyCircuitOutput {
    pub work_u128: [u8; 16],
    pub genesis_state_hash: [u8; 32],
}
The output is committed to the zkVM’s public output and becomes the public inputs when converted to Groth16 format.

Key Implementation Files

RISC Zero Guest

risc0-circuits/work-only/guest/src/main.rs
use circuits_lib::common;
use circuits_lib::work_only::work_only_circuit;

fn main() {
    let zkvm_guest = common::zkvm::Risc0Guest::new();
    work_only_circuit(&zkvm_guest);
}
Minimal entry point that initializes the zkVM guest and invokes the core circuit logic.

Core Circuit Logic

circuits-lib/src/work_only/mod.rs The main circuit function at mod.rs:71-91:
  1. Reads WorkOnlyCircuitInput from host (line 72)
  2. Validates method ID matches expected header chain circuit (lines 73-77)
  3. Verifies the header chain proof cryptographically (lines 78-82)
  4. Extracts and converts work value (lines 83-85)
  5. Commits compact output (lines 87-90)
The work_conversion helper function at mod.rs:111-114 handles the 256-bit to 128-bit conversion.

Build Configuration

risc0-circuits/work-only/build.rs The build script:
  • Compiles work-only-guest into RISC Zero ELF binary
  • Computes unique method ID for the compiled program
  • Handles BITCOIN_NETWORK and REPR_GUEST_BUILD environment variables
  • Optionally uses Docker for reproducible guest builds
  • Copies generated ELF to elfs folder

Network-Specific Method IDs

The circuit uses compile-time constants to ensure network compatibility:
const HEADER_CHAIN_METHOD_ID: [u32; 8] = {
    match option_env!("BITCOIN_NETWORK") {
        Some(network) if matches!(network.as_bytes(), b"mainnet") => MAINNET_HEADER_CHAIN_METHOD_ID,
        // ... other networks
    }
};
These constants are defined in circuits-lib/src/common/constants.rs and must match between:
  • Header Chain Circuit (producer of proofs)
  • Work Only Circuit (consumer/verifier of header chain proofs)
  • Bridge Circuit (verifier of work only proofs)
Mismatched method IDs will cause verification to fail, preventing cross-network proof replay attacks.

Usage in Bridge Circuit

The Bridge Circuit uses Work Only Proofs to verify watchtower challenges:
// From bridge-circuit/guest/src/main.rs
pub static WORK_ONLY_IMAGE_ID: [u8; 32] = match option_env!("BITCOIN_NETWORK") {
    Some(network) if matches!(network.as_bytes(), b"mainnet") => MAINNET_WORK_ONLY_METHOD_ID,
    // ... other networks
};

fn main() {
    let zkvm_guest = common::zkvm::Risc0Guest::new();
    bridge_circuit(&zkvm_guest, WORK_ONLY_IMAGE_ID);
}
The WORK_ONLY_IMAGE_ID is passed to the Bridge Circuit for verifying watchtower Groth16 proofs. This ensures:
  1. Only Work Only Proofs from the correct network are accepted
  2. Watchtowers cannot submit proofs from different circuit versions
  3. Network-specific consensus rules are enforced throughout the verification chain
See Bridge Circuit - Watchtower Challenge Processing for detailed integration.

Security Considerations

Work Truncation: The conversion from 256-bit to 128-bit work discards the upper 128 bits. This is safe for current Bitcoin difficulty levels but should be monitored if hash rates increase exponentially.
Genesis State Hash: The genesis state hash is preserved to ensure all work proofs can be traced back to a specific chain starting point. This prevents mixing proofs from different chain histories.

Next Steps

Header Chain Circuit

Understand how header chain proofs are generated

Bridge Circuit

See how work proofs are verified in the bridge

Build docs developers (and LLMs) love