Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f403b82
feat: forester: pda & mint compression
sergeytimoshin Jan 7, 2026
9b6ae17
refactor rent exemption calculations
sergeytimoshin Jan 21, 2026
342640c
format
sergeytimoshin Jan 21, 2026
66deaec
feat: add support for compressed mint retrieval in the indexer
sergeytimoshin Jan 21, 2026
12b236c
get_compressed_mints_by_authority authority type filtering
sergeytimoshin Jan 21, 2026
01911fb
fix: update PHOTON_COMMIT version in versions.sh
sergeytimoshin Jan 21, 2026
841a8c2
docs: update CLI parameter descriptions for compressible PDA program
sergeytimoshin Jan 21, 2026
1dea574
feat: add hex dependency and update existing hex usage in Cargo.toml …
sergeytimoshin Jan 21, 2026
e748e41
fix: update authority_type field in GetCompressedMintsByAuthorityOpti…
sergeytimoshin Jan 21, 2026
93d61a7
fix: update mint_authority and mint fields in build_expected_mint fun…
sergeytimoshin Jan 21, 2026
9121a06
feat: refactor bootstrap logic to use run_bootstrap helper; enhance m…
sergeytimoshin Jan 21, 2026
0adcca5
fix: update build_expected_mint function to accept version parameter …
sergeytimoshin Jan 21, 2026
265dbaa
fix: adjust calculate_compressible_slot to correctly compute availabl…
sergeytimoshin Jan 21, 2026
10bae7c
bump photon version
sergeytimoshin Jan 21, 2026
005ddfb
wip
sergeytimoshin Jan 23, 2026
4659bb4
cleanup
sergeytimoshin Jan 23, 2026
8e23a82
cleanup
sergeytimoshin Jan 23, 2026
a60962e
cleanup
sergeytimoshin Jan 23, 2026
91f5d91
cleanup
sergeytimoshin Jan 23, 2026
32c6cba
new apis
sergeytimoshin Jan 27, 2026
fb11ddd
feat: add support for unified account interface with hot/cold context…
sergeytimoshin Jan 28, 2026
5f12024
cleanup
sergeytimoshin Jan 28, 2026
d2fbfa9
format
sergeytimoshin Jan 28, 2026
11215b3
refactor account interface
sergeytimoshin Jan 29, 2026
00f85ab
bump photon version
sergeytimoshin Jan 29, 2026
27a78d7
feat: implement batch lookup for multiple compressed accounts in RPC
sergeytimoshin Jan 29, 2026
f43212b
enhance account interface
sergeytimoshin Jan 29, 2026
f46c619
fix: update account types in get_accounts_to_update test
sergeytimoshin Jan 29, 2026
e52e51e
cleanup
sergeytimoshin Jan 29, 2026
5dcf777
bump photon
sergeytimoshin Jan 29, 2026
93ea0bb
cleanup
sergeytimoshin Jan 29, 2026
1e4d918
fix: update error assertion in test_create_ata_failing for invalid mi…
sergeytimoshin Jan 29, 2026
8216d7e
fix: update error assertion in test_create_ata_failing for invalid mi…
sergeytimoshin Jan 30, 2026
44202c2
Potential fix for code scanning alert no. 143: Workflow does not cont…
sergeytimoshin Jan 30, 2026
5ca6ab9
fix: update default version fallback to V2 in LIGHT_PROTOCOL_VERSION …
sergeytimoshin Jan 30, 2026
28b7db0
cleanup
sergeytimoshin Jan 31, 2026
1723ce4
feat: add forester to cli (particularly useful for compression tests)…
sergeytimoshin Feb 1, 2026
311564f
bump photon commit hash in versions.sh
sergeytimoshin Feb 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 18 additions & 54 deletions .github/workflows/programs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
- "program-tests/**"
- "program-libs/**"
- "prover/client/**"
- ".github/workflows/light-system-programs-tests.yml"
- ".github/workflows/programs.yml"
pull_request:
branches:
- "*"
Expand All @@ -16,22 +16,24 @@ on:
- "program-tests/**"
- "program-libs/**"
- "prover/client/**"
- ".github/workflows/light-system-programs-tests.yml"
- ".github/workflows/programs.yml"
types:
- opened
- synchronize
- reopened
- ready_for_review

name: programs
permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
system-programs:
name: programs
name: ${{ matrix.test-group }}
if: github.event.pull_request.draft == false
runs-on: warp-ubuntu-latest-x64-4x
timeout-minutes: 90
Expand All @@ -52,27 +54,16 @@ jobs:

strategy:
matrix:
include:
- program: account-compression-and-registry
sub-tests: '["cargo-test-sbf -p account-compression-test", "cargo-test-sbf -p registry-test"]'
- program: light-system-program-address
sub-tests: '["cargo-test-sbf -p system-test -- test_with_address", "cargo-test-sbf -p e2e-test", "cargo-test-sbf -p compressed-token-test --test light_token"]'
- program: light-system-program-compression
sub-tests: '["cargo-test-sbf -p system-test -- test_with_compression", "cargo-test-sbf -p system-test --test test_re_init_cpi_account"]'
- program: compressed-token-and-e2e
sub-tests: '["cargo test -p light-compressed-token", "cargo-test-sbf -p compressed-token-test --test v1", "cargo-test-sbf -p compressed-token-test --test mint"]'
- program: compressed-token-batched-tree
sub-tests: '["cargo-test-sbf -p compressed-token-test -- test_transfer_with_photon_and_batched_tree"]'
- program: system-cpi-test
sub-tests:
'["cargo-test-sbf -p system-cpi-test", "cargo test -p light-system-program-pinocchio",
"cargo-test-sbf -p system-cpi-v2-test -- --skip functional_ --skip event::parse", "cargo-test-sbf -p system-cpi-v2-test -- event::parse",
"cargo-test-sbf -p compressed-token-test --test transfer2"
]'
- program: system-cpi-test-v2-functional-read-only
sub-tests: '["cargo-test-sbf -p system-cpi-v2-test -- functional_read_only"]'
- program: system-cpi-test-v2-functional-account-infos
sub-tests: '["cargo-test-sbf -p system-cpi-v2-test -- functional_account_infos"]'
test-group:
- account-compression-and-registry
- system-address
- system-compression
- compressed-token-and-e2e
- compressed-token-batched-tree
- system-cpi
- system-cpi-v2-functional-read-only
- system-cpi-v2-functional-account-infos

steps:
- name: Checkout sources
uses: actions/checkout@v6
Expand All @@ -87,34 +78,7 @@ jobs:
run: |
just cli build

- name: ${{ matrix.program }}
- name: Run tests
working-directory: program-tests
run: |

IFS=',' read -r -a sub_tests <<< "${{ join(fromJSON(matrix['sub-tests']), ', ') }}"
for subtest in "${sub_tests[@]}"
do
echo "$subtest"

# Retry logic for flaky batched-tree test
if [[ "$subtest" == *"test_transfer_with_photon_and_batched_tree"* ]]; then
echo "Running flaky test with retry logic (max 3 attempts)..."
attempt=1
max_attempts=3
until RUSTFLAGS="-D warnings" eval "$subtest"; do
attempt=$((attempt + 1))
if [ $attempt -gt $max_attempts ]; then
echo "Test failed after $max_attempts attempts"
exit 1
fi
echo "Attempt $attempt/$max_attempts failed, retrying..."
sleep 5
done
echo "Test passed on attempt $attempt"
else
RUSTFLAGS="-D warnings" eval "$subtest"
if [ "$subtest" == "cargo-test-sbf -p e2e-test" ]; then
just programs build-compressed-token-small
RUSTFLAGS="-D warnings" eval "$subtest -- --test test_10_all"
fi
fi
done
just ci-${{ matrix.test-group }}
4 changes: 4 additions & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Disable mise's Go management for this project.
# We use our own Go installation via devenv.sh.
[settings]
disable_tools = ["go"]
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ pinocchio-pubkey = { version = "0.3.0" }
pinocchio-system = { version = "0.3.0" }
bs58 = "^0.5.1"
sha2 = "0.10"
hex = "0.4"
litesvm = "0.7"
# Anchor
anchor-lang = { version = "0.31.1" }
Expand Down
20 changes: 20 additions & 0 deletions cli/src/commands/test-validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ class SetupCommand extends Command {
"Runs a test validator without starting a new prover service.",
default: false,
}),
forester: Flags.boolean({
description:
"Start the forester service for auto-compression of compressible accounts.",
default: false,
}),
"forester-port": Flags.integer({
description: "Port for the forester API server.",
required: false,
default: 8080,
}),
"compressible-pda-program": Flags.string({
description:
"Compressible PDA programs to track. Format: 'program_id:discriminator_base58'. Can be specified multiple times.",
required: false,
multiple: true,
}),
"skip-system-accounts": Flags.boolean({
description:
"Runs a test validator without initialized light system accounts.",
Expand Down Expand Up @@ -210,6 +226,7 @@ class SetupCommand extends Command {
await stopTestEnv({
indexer: !flags["skip-indexer"],
prover: !flags["skip-prover"],
forester: flags.forester,
});
this.log("\nTest validator stopped successfully \x1b[32m✔\x1b[0m");
} else {
Expand Down Expand Up @@ -262,6 +279,9 @@ class SetupCommand extends Command {
indexerPort: flags["indexer-port"],
proverPort: flags["prover-port"],
prover: !flags["skip-prover"],
forester: flags.forester,
foresterPort: flags["forester-port"],
compressiblePdaPrograms: flags["compressible-pda-program"],
skipSystemAccounts: flags["skip-system-accounts"],
geyserConfig: flags["geyser-config"],
validatorArgs: flags["validator-args"],
Expand Down
1 change: 1 addition & 0 deletions cli/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const CARGO_GENERATE_TAG = "v0.18.4";
export const SOLANA_VALIDATOR_PROCESS_NAME = "solana-test-validator";
export const LIGHT_PROVER_PROCESS_NAME = "light-prover";
export const INDEXER_PROCESS_NAME = "photon";
export const FORESTER_PROCESS_NAME = "forester";

export const PHOTON_VERSION = "0.51.2";

Expand Down
34 changes: 34 additions & 0 deletions cli/src/utils/initTestEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import {
} from "./process";
import { killProver, startProver } from "./processProverServer";
import { killIndexer, startIndexer } from "./processPhotonIndexer";
import {
killForester,
startForester,
getPayerForForester,
} from "./processForester";
import { Connection, PublicKey } from "@solana/web3.js";

type Program = { id: string; name?: string; tag?: string; path?: string };
Expand Down Expand Up @@ -95,8 +100,10 @@ async function getProgramOwnedAccounts(
export async function stopTestEnv(options: {
indexer: boolean;
prover: boolean;
forester?: boolean;
}) {
const processesToKill = [
{ name: "forester", condition: options.forester ?? false, killFunction: killForester },
{ name: "photon", condition: options.indexer, killFunction: killIndexer },
{ name: "prover", condition: options.prover, killFunction: killProver },
{
Expand Down Expand Up @@ -129,9 +136,11 @@ export async function initTestEnv({
skipSystemAccounts,
indexer = true,
prover = true,
forester = false,
rpcPort = 8899,
indexerPort = 8784,
proverPort = 3001,
foresterPort = 8080,
gossipHost = "127.0.0.1",
checkPhotonVersion = true,
photonDatabaseUrl,
Expand All @@ -141,6 +150,7 @@ export async function initTestEnv({
cloneNetwork,
verbose,
skipReset,
compressiblePdaPrograms,
}: {
additionalPrograms?: { address: string; path: string }[];
upgradeablePrograms?: {
Expand All @@ -151,9 +161,11 @@ export async function initTestEnv({
skipSystemAccounts?: boolean;
indexer: boolean;
prover: boolean;
forester?: boolean;
rpcPort?: number;
indexerPort?: number;
proverPort?: number;
foresterPort?: number;
gossipHost?: string;
checkPhotonVersion?: boolean;
photonDatabaseUrl?: string;
Expand All @@ -163,6 +175,7 @@ export async function initTestEnv({
cloneNetwork?: "devnet" | "mainnet";
verbose?: boolean;
skipReset?: boolean;
compressiblePdaPrograms?: string[];
}) {
// We cannot await this promise directly because it will hang the process
startTestValidator({
Expand Down Expand Up @@ -209,6 +222,27 @@ export async function initTestEnv({
proverUrlForIndexer,
);
}

if (forester) {
if (!indexer || !prover) {
throw new Error("Forester requires both indexer and prover to be running");
}
try {
const payer = getPayerForForester();
await startForester({
rpcUrl: `http://127.0.0.1:${rpcPort}`,
wsRpcUrl: `ws://127.0.0.1:${rpcPort + 1}`,
indexerUrl: `http://127.0.0.1:${indexerPort}`,
proverUrl: `http://127.0.0.1:${proverPort}`,
payer,
foresterPort,
compressiblePdaPrograms,
});
} catch (error) {
console.error("Failed to start forester:", error);
throw error;
}
}
}

export async function initTestEnvIfNeeded({
Expand Down
107 changes: 107 additions & 0 deletions cli/src/utils/processForester.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import which from "which";
import { killProcess, spawnBinary, waitForServers } from "./process";
import { FORESTER_PROCESS_NAME } from "./constants";
import { exec } from "node:child_process";
import * as util from "node:util";
import { exit } from "node:process";
import * as fs from "fs";
import * as path from "path";

const execAsync = util.promisify(exec);

async function isForesterInstalled(): Promise<boolean> {
try {
const resolvedOrNull = which.sync("forester", { nothrow: true });
return resolvedOrNull !== null;
} catch (error) {
return false;
}
}

function getForesterInstallMessage(): string {
return `\nForester not found. Please install it by running: "cargo install --git https://github.com/Lightprotocol/light-protocol forester --locked --force"`;
}

export interface ForesterConfig {
rpcUrl: string;
wsRpcUrl: string;
indexerUrl: string;
proverUrl: string;
payer: string;
foresterPort: number;
compressiblePdaPrograms?: string[];
}

/**
* Starts the forester service for auto-compression of compressible accounts.
*
* @param config - Forester configuration
*/
export async function startForester(config: ForesterConfig) {
await killForester();

if (!(await isForesterInstalled())) {
console.log(getForesterInstallMessage());
return exit(1);
}

console.log("Starting forester...");

const args: string[] = [
"start",
"--rpc-url",
config.rpcUrl,
"--ws-rpc-url",
config.wsRpcUrl,
"--indexer-url",
config.indexerUrl,
"--prover-url",
config.proverUrl,
"--payer",
config.payer,
"--api-server-port",
config.foresterPort.toString(),
"--enable-compressible",
];

// Add compressible PDA programs if specified
if (config.compressiblePdaPrograms && config.compressiblePdaPrograms.length > 0) {
for (const program of config.compressiblePdaPrograms) {
args.push("--compressible-pda-program", program);
}
}

spawnBinary(FORESTER_PROCESS_NAME, args);
await waitForServers([{ port: config.foresterPort, path: "/health" }]);
console.log("Forester started successfully!");
}

export async function killForester() {
await killProcess(FORESTER_PROCESS_NAME);
}

/**
* Gets the payer keypair as a JSON array string for forester.
* Reads from ~/.config/solana/id.json or SOLANA_PAYER environment variable.
*
* @returns JSON array string of the keypair bytes
*/
export function getPayerForForester(): string {
// Check for SOLANA_PAYER environment variable first
if (process.env.SOLANA_PAYER) {
return process.env.SOLANA_PAYER;
}

// Default to standard Solana keypair location
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
const keypairPath = path.join(homeDir, ".config", "solana", "id.json");

if (fs.existsSync(keypairPath)) {
const keypairData = fs.readFileSync(keypairPath, "utf-8");
return keypairData.trim();
}

throw new Error(
"No payer keypair found. Set SOLANA_PAYER environment variable or create ~/.config/solana/id.json",
);
}
2 changes: 1 addition & 1 deletion forester/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ futures = { workspace = true }
thiserror = { workspace = true }
borsh = { workspace = true }
bs58 = { workspace = true }
hex = "0.4"
hex = { workspace = true }
env_logger = { workspace = true }
async-trait = { workspace = true }
tracing = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions forester/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ test-compressible-mint: build-compressible-test-deps
test-compressible-ctoken: build-compressible-test-deps
RUST_LOG=forester=debug,light_client=debug \
cargo test --package forester --test test_compressible_ctoken -- --nocapture

# Test for indexer interface scenarios (creates test data for photon)
test-indexer-interface: build-test-deps
cargo test --package forester --test test_indexer_interface -- --nocapture
Comment on lines +38 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Test recipe looks functional, consider adding RUST_LOG for consistency.

The recipe structure is correct and the dependency on build-test-deps makes sense if the indexer interface tests use the create-address-test-program.

Unlike the other compressible test recipes (lines 28-37) which set RUST_LOG=forester=debug,light_client=debug, this one doesn't. If debugging indexer interface issues becomes necessary, operators will need to manually add it.

♻️ Optional: Add RUST_LOG for debugging consistency
 # Test for indexer interface scenarios (creates test data for photon)
 test-indexer-interface: build-test-deps
+    RUST_LOG=forester=debug,light_client=debug \
     cargo test --package forester --test test_indexer_interface -- --nocapture
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Test for indexer interface scenarios (creates test data for photon)
test-indexer-interface: build-test-deps
cargo test --package forester --test test_indexer_interface -- --nocapture
# Test for indexer interface scenarios (creates test data for photon)
test-indexer-interface: build-test-deps
RUST_LOG=forester=debug,light_client=debug \
cargo test --package forester --test test_indexer_interface -- --nocapture
🤖 Prompt for AI Agents
In `@forester/justfile` around lines 38 - 41, The test-indexer-interface recipe is
missing the RUST_LOG environment setting used elsewhere; update the
test-indexer-interface recipe so it exports or prefixes the cargo command with
RUST_LOG=forester=debug,light_client=debug (same format as the other
compressible test recipes) before invoking cargo test, keeping the dependency on
build-test-deps and the existing cargo test invocation (refer to the
test-indexer-interface recipe and build-test-deps).

Loading
Loading