LPsLux Proposals
Precompiles
LP-510

STARK Verification Precompiles (Production Lane)

Draft

Native EVM precompiles for STARK proof verification and canonical receipt generation

Category
Core
Created
2026-01-01

Abstract

LP-0510 specifies native EVM precompiles for STARK proof verification in the address range 0x0510-0x051F. These precompiles enable the Z-Chain to verify transparent, post-quantum safe proofs and emit canonical receipts.

Key Properties:

  • Post-Quantum Safe: Hash-based security (no pairings)
  • Transparent: No trusted setup required
  • Production Lane: Proof System ID = 1, immutable verifier at 0x051F

Motivation

Why STARKs?

PropertySTARKGroth16PLONK
Trusted Setup❌ None✅ Required✅ Universal
PQ-Safe✅ Hash-based❌ Pairing-based❌ Pairing-based
Proof Size~50-100 KB~200 bytes~500 bytes
VerificationO(log n)O(1)O(1)
Prover TimeFastSlowMedium

Use Cases

  1. Universal Computation: Verify any program via STARK
  2. Receipt Generation: Canonical receipts for cross-chain interop
  3. Rollup Proofs: L2 state transitions
  4. zkVM Programs: RISC Zero, SP1, Cairo

Specification

Precompile Address Map

AddressNameDescription
0x0510STARK_FIELD_ARITHField arithmetic (add, mul, inv)
0x0511STARK_POLY_EVALPolynomial evaluation
0x0512STARK_MERKLE_PATHMerkle path verification
0x0513STARK_FRI_FOLDFRI folding step
0x0514STARK_CONSTRAINTConstraint evaluation
0x0515-0x051EReservedFuture STARK operations
0x051FSTARK_VERIFYFull proof verification

Proof System ID and Versioning

FieldValueDescription
proofSystemId1STARK (Production Lane)
version1Protocol version

Versioning Rule: Format changes require version bump. Old versions remain forever.

STARK_VERIFY (0x051F) - Primary Verifier

The main entry point for STARK verification.

Input Format

Input Layout:
┌─────────────────────────────────────────────────────────────────┐
 Offset  Size     Field                                       
├────────┼─────────┼─────────────────────────────────────────────┤
 0       32       programId: bytes32                          
 32      4        proofLength: uint32                         
 36      var      proof: bytes[proofLength]                   
 36+L    4        publicInputsCount: uint32                   
 40+L    32*N     publicInputs: bytes32[N]                    
└─────────────────────────────────────────────────────────────────┘

Output Format

Output Layout (success):
┌─────────────────────────────────────────────────────────────────┐
 Offset  Size     Field                                       
├────────┼─────────┼─────────────────────────────────────────────┤
 0       1        valid: bool (0x01 = valid)                  
 1       32       receiptHash: bytes32                        
 33      32       claimHash: bytes32                          
└─────────────────────────────────────────────────────────────────┘

Output Layout (failure):
┌─────────────────────────────────────────────────────────────────┐
 Offset  Size     Field                                       
├────────┼─────────┼─────────────────────────────────────────────┤
 0       1        valid: bool (0x00 = invalid)                
 1       32       errorCode: bytes32                          
└─────────────────────────────────────────────────────────────────┘

Receipt Hash Definition

The receipt hash binds the proof to its verification context:

// Hash function: Poseidon2 (from LP-3658)

claimHash = Poseidon2(DST_RECEIPT, publicInputs[0], publicInputs[1], ...)

receiptHash = Poseidon2(
    DST_RECEIPT,
    programId,
    claimHash,
    bytes32(proofSystemId),    // 1 for STARK
    bytes32(version),          // Protocol version
    bytes32(block.timestamp),  // verifiedAt
    parentReceipt,             // 0x0 if none
    aggregationRoot            // 0x0 if single proof
)

Input Limits

LimitValueRationale
Max proof size1 MBPractical STARK proof bound
Max public inputs256Gas predictability
Max queries (FRI)100Security parameter

Gas Schedule

OperationBase GasPer-Byte GasNotes
STARK_VERIFY500,00010Full verification
STARK_FIELD_ARITH50-Per operation
STARK_POLY_EVAL1,0005Per coefficient
STARK_MERKLE_PATH500300/levelPath verification
STARK_FRI_FOLD5,000100/layerFRI step
STARK_CONSTRAINT2,000-Per constraint

Gas Formula for STARK_VERIFY:

gas = 500,000 + 10 * proof_bytes + 1,000 * num_public_inputs

Failure Modes

ConditionBehaviorError Code
Invalid proofReturn (false, error)0x01
Malformed inputRevert-
Out of gasRevert-
Unknown programReturn (false, error)0x02
Size exceededRevert-

Determinism Requirements

  1. Fixed Transcript: Fiat-Shamir uses deterministic hash
  2. Canonical Encoding: Big-endian, fixed sizes
  3. No Randomness: All operations deterministic
  4. Cross-Client: Same result across all implementations

Solidity Interface

// SPDX-License-Identifier: BSD-3-Clause
pragma solidity ^0.8.24;

/* STARK Verification Precompile - Production Lane */

interface ISTARKVerifier {
    /// @notice Verify a STARK proof and generate receipt
    /// @param programId The program/circuit identifier
    /// @param proof The STARK proof data
    /// @param publicInputs Public inputs to the computation
    /// @return valid Whether the proof is valid
    /// @return receiptHash The canonical receipt hash
    function verify(
        bytes32 programId,
        bytes calldata proof,
        bytes32[] calldata publicInputs
    ) external view returns (bool valid, bytes32 receiptHash);
    
    /// @notice Get the claim hash from public inputs
    function computeClaimHash(bytes32[] calldata publicInputs)
        external pure returns (bytes32);
}

library STARKLib {
    address constant STARK_VERIFY = address(0x051F);
    address constant STARK_FIELD = address(0x0510);
    address constant STARK_FRI = address(0x0513);
    
    uint32 constant PROOF_SYSTEM_ID = 1;
    uint32 constant VERSION = 1;
    
    /// @notice Verify STARK proof
    function verify(
        bytes32 programId,
        bytes calldata proof,
        bytes32[] calldata publicInputs
    ) internal view returns (bool valid, bytes32 receiptHash) {
        bytes memory input = abi.encodePacked(
            programId,
            uint32(proof.length),
            proof,
            uint32(publicInputs.length)
        );
        for (uint i = 0; i < publicInputs.length; i++) {
            input = abi.encodePacked(input, publicInputs[i]);
        }
        
        (bool success, bytes memory output) = STARK_VERIFY.staticcall(input);
        require(success, "STARK: call failed");
        
        valid = output[0] == 0x01;
        if (valid) {
            receiptHash = bytes32(output[1:33]);
        }
    }
    
    /// @notice Field addition
    function fieldAdd(uint256 a, uint256 b) internal view returns (uint256) {
        (bool success, bytes memory output) = STARK_FIELD.staticcall(
            abi.encodePacked(uint8(0x01), a, b)
        );
        require(success, "STARK: field add failed");
        return abi.decode(output, (uint256));
    }
    
    /// @notice Field multiplication
    function fieldMul(uint256 a, uint256 b) internal view returns (uint256) {
        (bool success, bytes memory output) = STARK_FIELD.staticcall(
            abi.encodePacked(uint8(0x02), a, b)
        );
        require(success, "STARK: field mul failed");
        return abi.decode(output, (uint256));
    }
}

Receipt Structure

The canonical receipt format for STARK proofs:

struct Receipt {
    bytes32 programId;       // Hash of verified program
    bytes32 claimHash;       // Poseidon2(publicInputs)
    bytes32 receiptHash;     // Unique receipt identifier
    uint32 proofSystemId;    // 1 = STARK
    uint32 version;          // Protocol version
    uint64 verifiedAt;       // Block timestamp
    bytes32 parentReceipt;   // For proof chains
    bytes32 aggregationRoot; // For batched proofs
}

Go Implementation

package stark

import (
    "errors"
    "math/big"
    
    "github.com/luxfi/coreth/precompile/contract"
    "github.com/luxfi/coreth/precompile/poseidon2"
)

const (
    // Precompile addresses
    AddrFieldArith  = 0x0510
    AddrPolyEval    = 0x0511
    AddrMerklePath  = 0x0512
    AddrFRIFold     = 0x0513
    AddrConstraint  = 0x0514
    AddrVerify      = 0x051F
    
    // Proof system
    ProofSystemID   = 1
    Version         = 1
    
    // Gas
    GasVerifyBase   = 500_000
    GasPerByte      = 10
    GasPerInput     = 1_000
    
    // Limits
    MaxProofSize    = 1 << 20  // 1 MB
    MaxPublicInputs = 256
)

var (
    ErrInvalidProof    = errors.New("invalid proof")
    ErrMalformedInput  = errors.New("malformed input")
    ErrSizeExceeded    = errors.New("size exceeded")
    ErrUnknownProgram  = errors.New("unknown program")
)

type STARKVerifyPrecompile struct {
    registry ProgramRegistry
}

func (p *STARKVerifyPrecompile) RequiredGas(input []byte) uint64 {
    if len(input) < 36 {
        return 0
    }
    
    proofLen := binary.BigEndian.Uint32(input[32:36])
    if 36+int(proofLen)+4 > len(input) {
        return 0
    }
    
    inputsOffset := 36 + int(proofLen)
    inputsCount := binary.BigEndian.Uint32(input[inputsOffset:inputsOffset+4])
    
    return uint64(GasVerifyBase + GasPerByte*int(proofLen) + GasPerInput*int(inputsCount))
}

func (p *STARKVerifyPrecompile) Run(input []byte) ([]byte, error) {
    // Parse input
    if len(input) < 36 {
        return nil, ErrMalformedInput
    }
    
    programId := [32]byte(input[0:32])
    proofLen := binary.BigEndian.Uint32(input[32:36])
    
    if proofLen > MaxProofSize {
        return nil, ErrSizeExceeded
    }
    
    proofEnd := 36 + int(proofLen)
    if len(input) < proofEnd+4 {
        return nil, ErrMalformedInput
    }
    
    proof := input[36:proofEnd]
    inputsCount := binary.BigEndian.Uint32(input[proofEnd:proofEnd+4])
    
    if inputsCount > MaxPublicInputs {
        return nil, ErrSizeExceeded
    }
    
    inputsStart := proofEnd + 4
    if len(input) < inputsStart+int(inputsCount)*32 {
        return nil, ErrMalformedInput
    }
    
    publicInputs := make([][32]byte, inputsCount)
    for i := uint32(0); i < inputsCount; i++ {
        copy(publicInputs[i][:], input[inputsStart+int(i)*32:inputsStart+int(i+1)*32])
    }
    
    // Get program verification key
    program, err := p.registry.GetProgram(programId)
    if err != nil {
        return invalidResult(0x02), nil // Unknown program
    }
    
    // Verify STARK proof
    valid := verifySTARKProof(program.VK, proof, publicInputs)
    if !valid {
        return invalidResult(0x01), nil // Invalid proof
    }
    
    // Compute receipt hash
    claimHash := computeClaimHash(publicInputs)
    receiptHash := computeReceiptHash(
        programId,
        claimHash,
        ProofSystemID,
        Version,
        uint64(time.Now().Unix()),
        [32]byte{}, // parentReceipt
        [32]byte{}, // aggregationRoot
    )
    
    // Return success
    result := make([]byte, 65)
    result[0] = 0x01 // valid
    copy(result[1:33], receiptHash[:])
    copy(result[33:65], claimHash[:])
    
    return result, nil
}

func computeClaimHash(publicInputs [][32]byte) [32]byte {
    // Use Poseidon2 with DST_RECEIPT
    inputs := make([][]byte, len(publicInputs)+1)
    inputs[0] = []byte{poseidon2.DSTReceipt}
    for i, pi := range publicInputs {
        inputs[i+1] = pi[:]
    }
    return poseidon2.Hash(inputs...)
}

func computeReceiptHash(
    programId [32]byte,
    claimHash [32]byte,
    proofSystemId uint32,
    version uint32,
    verifiedAt uint64,
    parentReceipt [32]byte,
    aggregationRoot [32]byte,
) [32]byte {
    return poseidon2.Hash(
        []byte{poseidon2.DSTReceipt},
        programId[:],
        claimHash[:],
        uint32ToBytes(proofSystemId),
        uint32ToBytes(version),
        uint64ToBytes(verifiedAt),
        parentReceipt[:],
        aggregationRoot[:],
    )
}

func invalidResult(errorCode byte) []byte {
    result := make([]byte, 33)
    result[0] = 0x00 // invalid
    result[1] = errorCode
    return result
}

Test Vectors

Valid Proof Verification

{
  "test": "stark_verify_valid",
  "input": {
    "programId": "0xabcd...1234",
    "proof": "0x...(valid STARK proof)...",
    "publicInputs": [
      "0x0000000000000000000000000000000000000000000000000000000000000001",
      "0x0000000000000000000000000000000000000000000000000000000000000002"
    ]
  },
  "expected": {
    "valid": true,
    "receiptHash": "0x...",
    "claimHash": "0x..."
  }
}

Invalid Proof

{
  "test": "stark_verify_invalid",
  "input": {
    "programId": "0xabcd...1234",
    "proof": "0x...(corrupted proof)...",
    "publicInputs": ["0x01"]
  },
  "expected": {
    "valid": false,
    "errorCode": "0x01"
  }
}

Security Considerations

Hash Function Security

STARK security relies on:

  • Collision resistance of Poseidon2
  • Fiat-Shamir transcript security
  • FRI soundness (128-bit security)

Transcript Determinism

The Fiat-Shamir transcript must be:

  • Deterministic across all clients
  • Include all public data
  • Use canonical encoding

Program Registration

Programs should be:

  • Audited before production use
  • Registered with verification key commitment
  • Version-controlled for updates

Receipt Binding

Receipts are bound by:

  • Poseidon2 hash (PQ-safe)
  • Block timestamp (immutable)
  • Chain ID (cross-chain safety)

Backwards Compatibility

This LP introduces new precompiles at unused addresses. No breaking changes.

References


LP-0510 Draft - 2026-01-01