Type System

This document describes Lof’s type system research and design, focusing on how dependent information flow types enable fine-grained security policies for ZK circuits.

Overview

Lof combines three type system features to provide compile-time security guarantees for zero-knowledge circuits:

  1. Strong Static Typing: Every value has a precise type known at compile time
  2. Information Flow Tracking: Track public/private data flow through computations
  3. Dependent Types: Types that depend on values to express invariants

This combination allows Lof to prevent privacy leaks, ensure correctness, and verify security properties before proof generation.

The Problem: Privacy in ZK Circuits

Zero-knowledge circuits have a fundamental security requirement: private witnesses must never leak to public outputs.

Traditional type systems are insufficient because:

  • They don’t distinguish public vs private data
  • Information flow isn’t tracked through computations
  • No way to express “this computation preserves privacy”

Example Vulnerability

proof Vulnerable {
    witness secret: Field;
    output leaked: Field;

    leaked === secret  // BUG: private data exposed publicly!
}

A naive type system sees both as Field and allows this. Lof’s information flow types catch this at compile time.

Information Flow Types

Visibility Levels

Lof tracks visibility as part of the type:

Public    // Data visible to everyone
Private   // Witness data (secrets)

These form a security lattice:

Public ⊑ Private

Meaning: public data can flow into private contexts, but not vice versa.

Tagged Types

Types are tagged with visibility:

Field@Public     // Public field element
Field@Private    // Private field element
Bool@Public      // Public boolean

Flow Rules

Information flow follows these rules:

Rule 1: Computation propagates privacy

witness x: Field@Private
input y: Field@Public

let z = x + y  // Type: Field@Private (private taints the result)

Rule 2: Cannot assign private to public

witness secret: Field@Private
output result: Field@Public

result === secret  // TYPE ERROR: cannot flow Private to Public

Rule 3: Same-level operations allowed

input a: Field@Public
input b: Field@Public

output sum: Field@Public
sum === a + b  // OK: Public + Public = Public

Dependent Types

Types can depend on values to express invariants and policies.

Basic Refinement Types

type PositiveField = refined { Field, x > 0 }

witness amount: PositiveField  // Statically guaranteed > 0

Dependent Security Policies

Combine visibility with refinements:

type SecureRange(level: Visibility, min: Nat, max: Nat) =
    refined { Field@level, x >= min && x < max }

// Private age between 0-150
witness age: SecureRange(Private, 0, 150)

// Public proof that age is in valid range (without revealing exact value)
output ageValid: SecureRange(Public, 18, 200)

Value-Dependent Visibility

Types can express how visibility changes based on values:

// Declassification: convert private to public with proof
type Declassified(x: Field@Private, proof: Proof) =
    { y: Field@Public | verifies(proof, x, y) }

Declassification

Converting private data to public requires explicit proof obligations.

Controlled Declassification

// Explicit declassification function
let declassify<T> (x: T@Private) (proof: SafetyProof): T@Public =
    // Compiler ensures proof demonstrates safety
    ...

Range Proof Example

proof AgeVerification {
    witness age: Field@Private;
    output valid: Bool@Public;

    // Prove age >= 18 without revealing exact age
    let isAdult: Bool@Private = age >= 18;

    // Declassify only the boolean result
    valid === declassify(isAdult)(rangeProof)
}

Hash Commitment Pattern

proof CommitmentVerify {
    witness secret: Field@Private;
    input commitment: Field@Public;
    output verified: Bool@Public;

    // Hash stays private
    let hash: Field@Private = poseidon(secret);

    // But we can prove equality
    assert hash === commitment  // TYPE ERROR: mismatched visibility!
}

Wait - this reveals a challenge. How do we compare private and public values?

Cross-Level Constraints

We need special constraint operators that work across visibility levels:

// Constraint equality works across levels (generates R1CS)
witness w: Field@Private
input x: Field@Public

assert w === x  // OK: creates constraint, doesn't leak w

The key insight: constraint generation doesn’t leak information, it just enforces relationships.

Dependent Information Flow Types

Combining dependent types with information flow:

Security-Indexed Types

// Type indexed by security level
type Secure<α: Visibility>(T: Type) = T@α

// Function preserving security level
let add<α> (x: Secure<α>(Field)) (y: Secure<α>(Field)): Secure<α>(Field) =
    x + y

Context-Dependent Types

// Type changes based on computation context
type Computed<α, β>(x: T@α, y: T@β): T@(α ⊔ β)  // ⊔ is lattice join (max)

witness w: Field@Private
input i: Field@Public

let result = compute(w, i)  // Type: Field@Private (Private ⊔ Public = Private)

Proof-Indexed Declassification

type SafelyDeclassified(x: Field@Private, policy: Policy) =
    { y: Field@Public |
        ∃ proof. verify(proof, policy, x, y)
    }

// Example policy: "only reveal range, not exact value"
let rangePolicy = RangePolicy(18, 150)

witness age: Field@Private
let ageProof: SafelyDeclassified(age, rangePolicy) = ...

Type Checking Algorithm

Flow Sensitivity

Track visibility through control flow:

match condition with
| true => {
    witness secret: Field@Private;
    // secret is Private here
}
| false => {
    input public: Field@Public;
    // public is Public here
}

Constraint Context

Special context for constraint generation:

// In constraint context, cross-level operations allowed
assert (witness: Field@Private) === (input: Field@Public)  // OK

// In computation context, not allowed
let x = (witness: Field@Private) + (input: Field@Public)  // Type: Field@Private
output y: Field@Public
y === x  // ERROR: cannot assign Private to Public

Refinement Checking

Verify dependent type predicates:

type PositiveField = refined { Field, x > 0 }

let addPositive (a: PositiveField) (b: PositiveField): PositiveField =
    // Compiler proves: (a > 0 ∧ b > 0) ⟹ (a + b > 0)
    a + b

Advanced Features

Linear Information Flow

Prevent witness reuse to avoid side channels:

witness secret: Linear<Field@Private>

let hash1 = hash(secret)  // OK: first use
let hash2 = hash(secret)  // ERROR: secret already consumed

// This prevents timing attacks from multiple uses

Effect System Integration

Track constraint generation as effects:

effect Constrained(s: Signal)

assert x === y
// Effects: Constrained(x), Constrained(y)

// Proof must constrain all witnesses
proof P {
    witness w: Field@Private;
    // ERROR: witness w not constrained (no effect Constrained(w))
}

Information Erasure

Computations that provably erase information:

// One-way function: Private -> Public is safe
type OneWay(f: Field@Private -> Field@Public) = {
    f | ∀ y: Field@Public. |{ x | f(x) = y }| > 1
}

let hash: OneWay = poseidon  // OK: hash has collisions

Practical Applications

Anonymous Credentials

proof AnonCredential {
    witness age: Field@Private;
    witness credential: Field@Private;
    input credRoot: Field@Public;
    output valid: Bool@Public;

    // Prove credential membership without revealing it
    let proof = merkleProof(credential, credRoot);
    assert verifyMerkle(proof);  // Private credential, public root

    // Prove age requirement
    assert age >= 18;

    valid === true
}

Private Voting

proof Vote {
    witness vote: Field@Private;  // 0 or 1
    witness voterKey: Field@Private;
    input eligibleVoters: Field@Public;
    output voteCommitment: Field@Public;

    // Prove voter is eligible
    assert memberOf(voterKey, eligibleVoters);

    // Prove vote is valid
    assert vote * (vote - 1) === 0;  // vote ∈ {0, 1}

    // Commit to vote without revealing it
    voteCommitment === hash(vote, voterKey)
}

Confidential Transactions

proof ConfidentialTransfer {
    witness balance: Field@Private;
    witness amount: Field@Private;
    input commitment: Field@Public;
    output newCommitment: Field@Public;

    // Prove balance matches commitment
    assert hash(balance) === commitment;

    // Prove sufficient funds
    assert balance >= amount;

    // Create new commitment
    newCommitment === hash(balance - amount)
}

Research Directions

Open Questions

  1. Type Inference: Can we infer visibility levels automatically, or must they be explicit?

  2. Decidability: Is type checking decidable for the full dependent information flow system?

  3. Granularity: Should we support more fine-grained levels than Public/Private?

  4. Composition: How do types compose across proof boundaries and proof composition?

  5. Optimization: Can type information guide constraint optimization?

Future Extensions

Multi-Level Security

// Lattice with multiple security levels
type Classified = Public | Internal | Secret | TopSecret

// Automatic level propagation
let combine (x: Field@Secret) (y: Field@Internal): Field@Secret = x + y

Probabilistic Privacy

// Differential privacy via types
type DP<ε>(T) = DifferentiallyPrivate<ε, T>

let noisySum (data: Array<Field@Private, N>): Field@DP<1.0> = ...

Proof Composition

// Compose proofs while tracking information flow
proof P1 { ... }
proof P2 { ... }

let composed = P1 ∘ P2  // Type system ensures safe composition

Formal Foundations

Type System Specification

Γ ⊢ e : T@α

Where:
- Γ is the typing context
- e is an expression
- T is the base type
- α is the visibility level

Flow Rules (Formal)

Γ ⊢ e₁ : T@α    Γ ⊢ e₂ : T@β
─────────────────────────────────  (T-OP)
Γ ⊢ e₁ ⊕ e₂ : T@(α ⊔ β)


Γ ⊢ e : T@Private    x : T@Public ∈ Γ
─────────────────────────────────────  (T-LEAK-ERROR)
Γ ⊬ x := e

Soundness Theorem

Theorem (Noninterference): If Γ ⊢ proof : Valid and all witnesses are typed @Private, then public outputs are independent of witness values (modulo constraints).

Proof sketch: By induction on typing derivations, show that information only flows upward in the security lattice.

Implementation Notes

Current Status in Lof

The current Lof implementation has:

  • Basic visibility tracking (input, witness, output)
  • Type checking for field operations
  • Pattern matching with type safety

Planned Enhancements

  • Full dependent information flow types
  • Automatic visibility inference
  • Refinement type checking
  • Linear witness tracking
  • Effect system for constraints

Further Reading

  • Philosophy - Design motivation
  • Syntax - Language syntax
  • Data Types - Practical type usage
  • Information Flow Control (research paper - coming soon)
  • Dependent Types for ZK Circuits (research paper - coming soon)

References

  • Myers, A.C. “JFlow: Practical Mostly-Static Information Flow Control” (1999)
  • Zdancewic, S. “Programming Languages for Information Security” (2002)
  • Xi, H. “Dependent Types in Practical Programming” (1998)
  • Norell, U. “Dependently Typed Programming in Agda” (2009)
  • Swamy, N. et al. “Secure Distributed Programming with Value-Dependent Types” (2011)