Light Protocol is the ZK Compression Protocol for Solana, enabling developers to create rent-free tokens and PDAs without sacrificing performance, security, or composability. The protocol uses zero-knowledge proofs to compress account state into Merkle trees, reducing storage costs while maintaining full Solana compatibility.
Core Technologies: Rust, Solana, ZK circuits (Gnark), Poseidon hashing, batched Merkle trees
Architecture: On-chain programs + off-chain ZK provers + client SDKs + forester service, see light_paper_v0.1.0.pdf for details.
Detailed docs: See CLAUDE.md files in individual crates and **/*/docs/
light-protocol/
├── program-libs/ # Core Rust libraries (used in programs and sdk-libs)
│ ├── account-checks/ # Solana account validation (solana-program + pinocchio)
│ ├── aligned-sized/ # Macro to get the aligned size of rust structs
│ ├── array-map/ # Array-based map data structure
│ ├── batched-merkle-tree/ # Batched Merkle tree (Merkle tree updates with zk proofs)
│ ├── bloom-filter/ # Bloom filters
│ ├── compressed-account/ # Compressed account types and utilities
│ ├── compressible/ # Configuration for compressible token accounts
│ ├── concurrent-merkle-tree/ # Concurrent Merkle tree operations
│ ├── ctoken-interface/ # Compressed token types and interfaces
│ ├── hash-set/ # Hash set implementation for Solana programs
│ ├── hasher/ # Poseidon hash implementation
│ ├── heap/ # Heap data structure for Solana programs
│ ├── indexed-array/ # Indexed array utilities
│ ├── indexed-merkle-tree/ # Indexed Merkle tree with address management
│ ├── macros/ # Procedural macros for Light Protocol
│ ├── merkle-tree-metadata/ # Metadata types for Merkle trees
│ ├── verifier/ # ZKP verification logic in Solana programs
│ ├── zero-copy/ # Zero-copy serialization for efficient account access
│ └── zero-copy-derive/ # Derive macros for zero-copy serialization
├── programs/ # Light Protocol Solana programs
│ ├── account-compression/ # Core compression program (owns Merkle tree accounts)
│ ├── system/ # Light system program (compressed account validation)
│ ├── compressed-token/ # Compressed token implementation (similar to SPL Token)
│ └── registry/ # Protocol configuration and forester access control
├── sdk-libs/ # Rust libraries used in custom programs and clients
│ ├── client/ # RPC client for querying compressed accounts
│ ├── sdk/ # Core SDK for Rust/Anchor programs
│ ├── sdk-pinocchio/ # Pinocchio-specific SDK implementation
│ ├── ctoken-sdk/ # Compressed token client utilities
│ └── program-test/ # Fast local test environment (LiteSVM)
├── prover/ # ZK proof generation
│ ├── server/ # Go-based prover server and circuit implementation (Gnark)
│ └── client/ # Rust client for requesting proofs used in sdk/client and forester
├── forester/ # Server implementation to insert values from queue accounts into tree accounts.
├── cli/ # Light CLI tool (@lightprotocol/zk-compression-cli)
├── js/ # JavaScript/TypeScript libraries (@lightprotocol/stateless.js, @lightprotocol/compressed-token)
├── program-tests/ # Integration tests for programs
├── sdk-tests/ # Integration tests for sdk libraries in solana programs that integrate light protocol.
└── scripts/ # Build, test, and deployment scripts
- depend on other program-libs or external crates only
- unit test must not depend on light-test-utils, any test that requires light-test-utils must be in program-tests
- depend on program-libs and external crates only
- are used in program-tests, in sdk-libs only with devenv feature but should be avoided.
- unit test must not depend on light-test-utils, any test that requires light-test-utils must be in program-tests
- integration tests must be in program-tests
- light-test-utils contains assert functions to assert instruction success in integration tests.
- depend on program-libs, light-prover-client and external crates only
- must not depend on programs without devenv feature
- unit test must not depend on light-test-utils, any test that requires light-test-utils must be in sdk-tests
- integration tests must be in sdk-tests
# Build entire monorepo (uses Nx)
./scripts/build.shIMPORTANT: Do not run cargo test at the monorepo root. Always target specific packages with -p.
The repository has three main categories of tests:
Fast-running tests that don't require Solana runtime. Located in program-libs/ crates.
# Run with: cargo test -p <package-name>
cargo test -p light-batched-merkle-tree
cargo test -p light-account-checks
cargo test -p light-hasher --all-features
cargo test -p light-compressed-account --all-features
# ... see individual crate docs for specific testsEnvironment variables used in CI:
RUSTFLAGS="-D warnings"(fail on warnings)REDIS_URL=redis://localhost:6379
Long-running integration tests that require Solana runtime (SBF). Located in program-tests/.
Why tests live here:
- Most depend on
program-tests/utils(light-test-utils) batched-merkle-tree-testis here because it depends on program-tests/utilszero-copy-derive-testis here only to avoid cyclic dependencies (it's NOT a long-running integration test)
# Run with: cargo test-sbf -p <package-name>
cargo test-sbf -p account-compression-test
cargo test-sbf -p system-test
cargo test-sbf -p compressed-token-test
# ... see program-tests/CLAUDE.md for complete listFor detailed test commands, see: program-tests/CLAUDE.md
SDK integration tests for various SDK implementations (native, Anchor, Pinocchio, token).
# Run with: cargo test-sbf -p <package-name>
cargo test-sbf -p sdk-native-test
cargo test-sbf -p sdk-anchor-test
cargo test-sbf -p sdk-token-test
# ... see sdk-tests/CLAUDE.md for complete listFor detailed test commands, see: sdk-tests/CLAUDE.md
Version-specific tests (V1 and V2) for JS/TS packages.
# Build and test with just
just cli::build
just js::test-stateless
just js::test-compressed-token
just cli::test
# Or use root-level aggregates that include cli and js targets
just build
just testEnvironment variables:
LIGHT_PROTOCOL_VERSION=V1orV2REDIS_URL=redis://localhost:6379CI=true
For available test scripts, see: package.json files in js/ directory
Tests for the ZK proof generation server (Gnark circuits).
# Run from prover/server directory
cd prover/server
# Unit tests
go test ./prover/... -timeout 60m
# Redis integration tests
TEST_REDIS_URL=redis://localhost:6379/15 go test -v -run TestRedis -timeout 10m
# Integration tests
go test -run TestLightweight -timeout 15mFor detailed test commands, see: prover/server/ directory
End-to-end tests for the off-chain tree maintenance service.
TEST_MODE=local cargo test --package forester e2e_test -- --nocaptureEnvironment variables:
RUST_BACKTRACE=1TEST_MODE=localREDIS_URL=redis://localhost:6379
Format and clippy checks across the entire codebase.
./scripts/lint.shNote: This requires nightly Rust toolchain and clippy components.
program-libs/: Pure Rust libraries, unit tests withcargo testsdk-libs/: Pure Rust libraries, unit tests withcargo testprogram-tests/: Integration tests requiring Solana runtime, depend onlight-test-utilssdk-tests/: SDK-specific integration tests- Special case:
zero-copy-derive-testinprogram-tests/only to break cyclic dependencies
When testing account state, use borsh deserialization with a single assert_eq against an expected reference account:
use borsh::BorshDeserialize;
use light_ctoken_types::state::{
AccountState, CToken, ExtensionStruct, PausableAccountExtension,
PermanentDelegateAccountExtension,
};
// Deserialize the account
let ctoken = CToken::deserialize(&mut &account.data[..])
.expect("Failed to deserialize CToken account");
// Extract runtime-specific values from deserialized account
let compression_info = ctoken
.extensions
.as_ref()
.and_then(|exts| {
exts.iter().find_map(|e| match e {
ExtensionStruct::Compressible(info) => Some(info.clone()),
_ => None,
})
})
.expect("Should have Compressible extension");
// Build expected account for comparison
let expected_ctoken = CToken {
mint: mint_pubkey.to_bytes().into(),
owner: payer.pubkey().to_bytes().into(),
amount: 0,
delegate: None,
state: AccountState::Frozen,
is_native: None,
delegated_amount: 0,
close_authority: None,
extensions: Some(vec![
ExtensionStruct::Compressible(compression_info),
ExtensionStruct::PausableAccount(PausableAccountExtension),
ExtensionStruct::PermanentDelegateAccount(PermanentDelegateAccountExtension),
]),
};
// Single assert comparing full account state
assert_eq!(ctoken, expected_ctoken, "CToken account should match expected");Benefits:
- Type-safe assertions using actual struct fields instead of magic byte offsets
- Maintainable - if account layout changes, deserialization handles it
- Readable - clear field names vs
account.data[108] - Single assertion point for the entire account state
Anchor uses 8-byte discriminators derived from the instruction name. To get discriminators from an Anchor program:
#[cfg(test)]
mod discriminator_tests {
use super::*;
use anchor_lang::Discriminator;
#[test]
fn print_instruction_discriminators() {
// Each instruction in the #[program] module has a corresponding struct
// in the `instruction` module with the DISCRIMINATOR constant
println!("InstructionName: {:?}", instruction::InstructionName::DISCRIMINATOR);
}
}Run with: cargo test -p <program-crate> print_instruction_discriminators -- --nocapture
Example output:
Claim: [62, 198, 214, 193, 213, 159, 108, 210]
CompressAndClose: [96, 94, 135, 18, 121, 42, 213, 117]
When to use discriminators:
- Building instructions manually without Anchor's
InstructionDatatrait - Creating SDK functions that don't depend on Anchor crate
- Verifying instruction data in tests or validators