Skip to main content
Cryptographic inlines are custom instructions that replace computationally expensive RISC-V implementations of cryptographic primitives with efficient, constraint-native implementations. This optimization can provide 2-5x throughput improvements for cryptography-heavy programs.

Problem: Expensive RISC-V Execution

Proving RISC-V execution of cryptographic operations is inefficient:
// SHA-256 in pure RISC-V requires:
// - ~1000 instructions per block
// - Proving 1000 instruction lookups
// - Proving 1000 memory operations
// - Committing to ~1000 witness rows

fn sha256_block_naive(state: &mut [u32; 8], block: &[u8; 64]) {
    // Each of these operations becomes multiple RISC-V instructions:
    // - Bitwise rotations (RISC-V has no native rotate)
    // - Array indexing (load/store with bounds checking)
    // - 64 rounds of compression function
    // Total: ~1000 instructions → ~1000 witness rows
}

Solution: Constraint-Native Primitives

Inlines replace the RISC-V trace with a pre-computed sequence of constraint-native operations:
// SHA-256 inline:
// - Single "virtual instruction" in the trace
// - Pre-computed sequence of ~80 optimized operations
// - Each operation is already expressed in constraint-friendly form
// - Witness size: ~80 rows instead of ~1000

#[cfg(not(feature = "host"))]
pub unsafe fn sha256_compression(input: *const u32, state: *mut u32) {
    // Guest: Emit custom instruction opcode
    core::arch::asm!(
        ".insn i {OPCODE}, {FUNCT3}, x0, {input}, {FUNCT7}",
        input = in(reg) input as usize,
        // Custom instruction encoding
    );
}

#[cfg(feature = "host")]
pub unsafe fn sha256_compression(input: *const u32, state: *mut u32) {
    // Host: Execute native Rust implementation
    let result = exec::execute_sha256_compression(state_array, input_array);
}

Available Inlines

Jolt provides optimized inlines for common cryptographic primitives:

Hash Functions

SHA-256

  • Module: jolt-inlines/sha2
  • Operations: Block compression, initial compression
  • Speedup: ~10-15x vs. RISC-V

Keccak-256

  • Module: jolt-inlines/keccak256
  • Operations: Permutation function
  • Speedup: ~20-30x vs. RISC-V

BLAKE2b

  • Module: jolt-inlines/blake2
  • Operations: Compression function
  • Speedup: ~12-18x vs. RISC-V

BLAKE3

  • Module: jolt-inlines/blake3
  • Operations: Compression function
  • Speedup: ~12-18x vs. RISC-V

Elliptic Curve Operations

secp256k1

  • Module: jolt-inlines/secp256k1
  • Operations: Point multiplication, ECDSA
  • Speedup: ~50-100x vs. RISC-V

Grumpkin

  • Module: jolt-inlines/grumpkin
  • Operations: Curve arithmetic
  • Speedup: ~50-100x vs. RISC-V

Big Integer Arithmetic

BigInt Multiplication

  • Module: jolt-inlines/bigint
  • Operations: 256-bit multiplication
  • Speedup: ~5-10x vs. RISC-V

How Inlines Work

1. Custom Instruction Encoding

Inlines use RISC-V’s custom instruction extension space:
// Constants defining the custom instruction
pub const INLINE_OPCODE: u32 = 0x0B;  // Custom-0 opcode
pub const SHA256_FUNCT3: u32 = 0x00;   // Function identifier
pub const SHA256_FUNCT7: u32 = 0x00;   // Variant identifier

2. Sequence Builder

Each inline defines a sequence builder that generates the constraint-native operation sequence:
// Location: jolt-inlines/sha2/src/sequence_builder.rs
pub fn sha2_inline_sequence_builder(
    input: SequenceInputs,
    state: SequenceInputs,
) -> Vec<Instruction> {
    let mut builder = Sha256CompressionSequenceBuilder::new(false);
    
    // Generate optimized instruction sequence:
    // - Load input words
    // - Expand message schedule
    // - 64 compression rounds (optimized)
    // - Store result
    
    builder.build()
}

3. Tracer Integration

The tracer replaces custom instructions with the pre-computed sequence:
// During trace generation:
// 1. Encounter custom instruction
// 2. Look up registered sequence builder
// 3. Execute sequence builder with current state
// 4. Emit sequence of operations instead of RISC-V instructions

register_inline(
    INLINE_OPCODE,
    SHA256_FUNCT3,
    SHA256_FUNCT7,
    "SHA256_INLINE",
    Box::new(sha2_inline_sequence_builder),
    None,  // Optional validator
)?;

4. Witness Generation

The inline sequence generates compact witness data:
// Instead of proving 1000 RISC-V instructions:
// - 1000 instruction lookups
// - 1000 memory operations  
// - 1000 register operations

// Inline proves ~80 optimized operations:
// - Pre-computed instruction sequence (no lookup needed)
// - Minimal memory operations (optimized layout)
// - Direct constraint evaluation

Performance Impact

SHA-256 Example

MetricRISC-VInlineImprovement
Instructions/block~1000~8012.5x
Witness rows~1000~8012.5x
Prover time100ms~7ms14x
Memory usageHighLow~10x

secp256k1 Example

MetricRISC-VInlineImprovement
Instructions/op~50,000~500100x
Witness rows~50,000~500100x
Prover time5s~50ms100x

End-to-End Impact

For a program that performs:
  • 100 SHA-256 blocks
  • 10 secp256k1 signatures
Without inlines:
  • SHA-256: 100,000 instructions
  • secp256k1: 500,000 instructions
  • Total: ~600,000 instructions
  • Proving time: ~60s
With inlines:
  • SHA-256: 8,000 instructions (inline sequences)
  • secp256k1: 5,000 instructions (inline sequences)
  • Other logic: ~10,000 instructions
  • Total: ~23,000 instructions
  • Proving time: ~12s
Result: ~5x end-to-end speedup

Creating Custom Inlines

To create a new inline for your application:

1. Define the Inline Module

// my-inline/src/lib.rs
pub const INLINE_OPCODE: u32 = 0x0B;
pub const MY_OP_FUNCT3: u32 = 0x05;  // Choose unused funct3
pub const MY_OP_FUNCT7: u32 = 0x00;
pub const MY_OP_NAME: &str = "MY_CUSTOM_OP";

pub mod sdk;          // Guest API
pub mod exec;         // Host execution
pub mod sequence_builder;  // Constraint sequence

2. Implement Sequence Builder

// my-inline/src/sequence_builder.rs
pub fn my_op_sequence_builder(
    input: SequenceInputs,
) -> Vec<Instruction> {
    let mut instructions = Vec::new();
    
    // Generate optimized instruction sequence
    // Use constraint-friendly operations
    
    instructions
}

3. Register the Inline

// my-inline/src/lib.rs
#[cfg(feature = "host")]
pub fn init_inlines() -> Result<(), String> {
    register_inline(
        INLINE_OPCODE,
        MY_OP_FUNCT3,
        MY_OP_FUNCT7,
        MY_OP_NAME,
        Box::new(my_op_sequence_builder),
        None,
    )
}

#[cfg(not(target_arch = "wasm32"))]
#[ctor::ctor]
fn auto_register() {
    if let Err(e) = init_inlines() {
        tracing::error!("Failed to register inline: {e}");
    }
}

4. Provide Guest API

// my-inline/src/sdk.rs
#[cfg(not(feature = "host"))]
pub unsafe fn my_custom_op(input: *const u64, output: *mut u64) {
    core::arch::asm!(
        ".insn i {OPCODE}, {FUNCT3}, x0, {input}, {FUNCT7}",
        input = in(reg) input as usize,
        OPCODE = const INLINE_OPCODE,
        FUNCT3 = const MY_OP_FUNCT3,
        FUNCT7 = const MY_OP_FUNCT7,
    );
}

#[cfg(feature = "host")]
pub unsafe fn my_custom_op(input: *const u64, output: *mut u64) {
    let result = exec::execute_my_op(input_array);
    // Write result to output
}

Best Practices

When to Use Inlines

Good candidates:
  • Cryptographic primitives (hash, signatures, encryption)
  • Fixed computation patterns (no data-dependent branching)
  • High instruction count in RISC-V (>100 instructions)
  • Performance-critical operations
Poor candidates:
  • Simple operations (less than 10 instructions)
  • Data-dependent control flow
  • Operations that vary significantly based on input

Optimization Guidelines

  1. Minimize witness size: Each operation in the sequence adds to witness
  2. Use constraint-friendly operations: Prefer operations that map cleanly to R1CS constraints
  3. Avoid branches: Keep the sequence deterministic and fixed-length
  4. Test thoroughly: Verify correctness in both host and guest modes

References

Build docs developers (and LLMs) love