Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions .github/workflows/forester-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,48 @@ jobs:

- name: Test
run: cargo test --package forester e2e_test -- --nocapture

compressible-tests:
name: Forester compressible tests
runs-on: warp-ubuntu-latest-x64-4x
timeout-minutes: 60

services:
redis:
image: redis:8.0.1
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
RUST_LOG: forester=debug,light_client=debug
REDIS_URL: redis://localhost:6379

steps:
- uses: actions/checkout@v6

- name: Setup and build
uses: ./.github/actions/setup-and-build
with:
skip-components: "go"
cache-key: "rust"

- name: Build CLI
run: npx nx build @lightprotocol/zk-compression-cli

- name: Build test programs
run: |
cargo build-sbf --manifest-path sdk-tests/csdk-anchor-full-derived-test/Cargo.toml

- name: Test compressible PDA
run: cargo test --package forester --test test_compressible_pda -- --nocapture

- name: Test compressible Mint
run: cargo test --package forester --test test_compressible_mint -- --nocapture

- name: Test compressible ctoken
run: cargo test --package forester --test test_compressible_ctoken -- --nocapture
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ solana-zk-token-sdk = "2.3"
solana-logger = "2.3"
solana-bn254 = "2.2"
solana-sysvar = { version = "2.2" }
solana-rent = { version = "2.2" }
solana-program-error = { version = "2.2" }
solana-account-info = { version = "2.2" }
solana-transaction = { version = "2.2" }
Expand Down Expand Up @@ -240,6 +241,7 @@ light-indexed-array = { path = "program-libs/indexed-array", version = "0.3.0" }
light-array-map = { path = "program-libs/array-map", version = "0.1.1" }
light-program-profiler = { version = "0.1.0" }
create-address-program-test = { path = "program-tests/create-address-test-program", version = "1.0.0" }
sdk-compressible-test = { path = "sdk-tests/sdk-compressible-test", version = "0.1.0" }
groth16-solana = { version = "0.2.0" }
bytemuck = { version = "1.19.0" }
arrayvec = "0.7"
Expand Down
63 changes: 53 additions & 10 deletions cli/src/commands/test-validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class SetupCommand extends Command {
"$ light test-validator --geyser-config ./config.json",
'$ light test-validator --validator-args "--limit-ledger-size 50000000"',
"$ light test-validator --sbf-program <address> <path/program>",
"$ light test-validator --upgradeable-program <address> <path/program> <upgrade_authority>",
"$ light test-validator --devnet",
"$ light test-validator --mainnet",
];
Expand Down Expand Up @@ -111,6 +112,14 @@ class SetupCommand extends Command {
multiple: true,
summary: "Usage: --sbf-program <address> <path/program_name.so>",
}),
"upgradeable-program": Flags.string({
description:
"Add an upgradeable SBF program to the genesis configuration. Required for programs that need compressible config initialization. If the ledger already exists then this parameter is silently ignored.",
required: false,
multiple: true,
summary:
"Usage: --upgradeable-program <address> <path/program_name.so> <upgrade_authority>",
}),
devnet: Flags.boolean({
description:
"Clone Light Protocol programs and accounts from devnet instead of loading local binaries.",
Expand All @@ -134,10 +143,22 @@ class SetupCommand extends Command {
}),
};

validatePrograms(programs: { address: string; path: string }[]): void {
// Check for duplicate addresses among provided programs
validatePrograms(
programs: { address: string; path: string }[],
upgradeablePrograms: {
address: string;
path: string;
upgradeAuthority: string;
}[],
): void {
// Check for duplicate addresses among all provided programs
const addresses = new Set<string>();
for (const program of programs) {
const allPrograms = [
...programs.map((p) => ({ ...p, type: "sbf" })),
...upgradeablePrograms.map((p) => ({ ...p, type: "upgradeable" })),
];

for (const program of allPrograms) {
if (addresses.has(program.address)) {
this.error(`Duplicate program address detected: ${program.address}`);
}
Expand Down Expand Up @@ -192,24 +213,46 @@ class SetupCommand extends Command {
});
this.log("\nTest validator stopped successfully \x1b[32m✔\x1b[0m");
} else {
const rawValues = flags["sbf-program"] || [];

if (rawValues.length % 2 !== 0) {
// Parse --sbf-program flags (2 arguments each: address, path)
const rawSbfValues = flags["sbf-program"] || [];
if (rawSbfValues.length % 2 !== 0) {
this.error("Each --sbf-program flag must have exactly two arguments");
}

const programs: { address: string; path: string }[] = [];
for (let i = 0; i < rawValues.length; i += 2) {
for (let i = 0; i < rawSbfValues.length; i += 2) {
programs.push({
address: rawValues[i],
path: rawValues[i + 1],
address: rawSbfValues[i],
path: rawSbfValues[i + 1],
});
}

// Parse --upgradeable-program flags (3 arguments each: address, path, upgrade_authority)
const rawUpgradeableValues = flags["upgradeable-program"] || [];
if (rawUpgradeableValues.length % 3 !== 0) {
this.error(
"Each --upgradeable-program flag must have exactly three arguments: <address> <path> <upgrade_authority>",
);
}

const upgradeablePrograms: {
address: string;
path: string;
upgradeAuthority: string;
}[] = [];
for (let i = 0; i < rawUpgradeableValues.length; i += 3) {
upgradeablePrograms.push({
address: rawUpgradeableValues[i],
path: rawUpgradeableValues[i + 1],
upgradeAuthority: rawUpgradeableValues[i + 2],
});
}

this.validatePrograms(programs);
this.validatePrograms(programs, upgradeablePrograms);

await initTestEnv({
additionalPrograms: programs,
upgradeablePrograms: upgradeablePrograms,
checkPhotonVersion: !flags["relax-indexer-version-constraint"],
indexer: !flags["skip-indexer"],
limitLedgerSize: flags["limit-ledger-size"],
Expand Down
60 changes: 45 additions & 15 deletions cli/src/utils/initTestEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export async function stopTestEnv(options: {

export async function initTestEnv({
additionalPrograms,
upgradeablePrograms,
skipSystemAccounts,
indexer = true,
prover = true,
Expand All @@ -142,6 +143,11 @@ export async function initTestEnv({
skipReset,
}: {
additionalPrograms?: { address: string; path: string }[];
upgradeablePrograms?: {
address: string;
path: string;
upgradeAuthority: string;
}[];
skipSystemAccounts?: boolean;
indexer: boolean;
prover: boolean;
Expand All @@ -161,6 +167,7 @@ export async function initTestEnv({
// We cannot await this promise directly because it will hang the process
startTestValidator({
additionalPrograms,
upgradeablePrograms,
skipSystemAccounts,
limitLedgerSize,
rpcPort,
Expand All @@ -175,6 +182,18 @@ export async function initTestEnv({
await confirmServerStability(`http://127.0.0.1:${rpcPort}/health`);
await confirmRpcReadiness(`http://127.0.0.1:${rpcPort}`);

if (prover) {
const config = getConfig();
config.proverUrl = `http://127.0.0.1:${proverPort}`;
setConfig(config);
try {
await startProver(proverPort);
} catch (error) {
console.error("Failed to start prover:", error);
throw error;
}
}

if (indexer) {
const config = getConfig();
config.indexerUrl = `http://127.0.0.1:${indexerPort}`;
Expand All @@ -190,20 +209,6 @@ export async function initTestEnv({
proverUrlForIndexer,
);
}

if (prover) {
const config = getConfig();
config.proverUrl = `http://127.0.0.1:${proverPort}`;
setConfig(config);
try {
// TODO: check if using redisUrl is better here.
await startProver(proverPort);
} catch (error) {
console.error("Failed to start prover:", error);
// Prover logs will be automatically displayed by spawnBinary in process.ts
throw error;
}
}
}

export async function initTestEnvIfNeeded({
Expand Down Expand Up @@ -277,6 +282,7 @@ export function programFilePath(programName: string): string {

export async function getSolanaArgs({
additionalPrograms,
upgradeablePrograms,
skipSystemAccounts,
limitLedgerSize,
rpcPort,
Expand All @@ -287,6 +293,11 @@ export async function getSolanaArgs({
skipReset = false,
}: {
additionalPrograms?: { address: string; path: string }[];
upgradeablePrograms?: {
address: string;
path: string;
upgradeAuthority: string;
}[];
skipSystemAccounts?: boolean;
limitLedgerSize?: number;
rpcPort?: number;
Expand All @@ -301,7 +312,7 @@ export async function getSolanaArgs({
const solanaArgs = [
`--limit-ledger-size=${limitLedgerSize}`,
`--rpc-port=${rpcPort}`,
`--gossip-host=${gossipHost}`,
`--bind-address=${gossipHost}`,
"--quiet",
];

Expand Down Expand Up @@ -367,6 +378,18 @@ export async function getSolanaArgs({
}
}

// Add upgradeable programs (with upgrade authority)
if (upgradeablePrograms) {
for (const program of upgradeablePrograms) {
solanaArgs.push(
"--upgradeable-program",
program.address,
program.path,
program.upgradeAuthority,
);
}
}

// Load local system accounts only if not cloning from network
if (!skipSystemAccounts && !cloneNetwork) {
const accountsRelPath = "../../accounts";
Expand All @@ -379,6 +402,7 @@ export async function getSolanaArgs({

export async function startTestValidator({
additionalPrograms,
upgradeablePrograms,
skipSystemAccounts,
limitLedgerSize,
rpcPort,
Expand All @@ -390,6 +414,11 @@ export async function startTestValidator({
skipReset,
}: {
additionalPrograms?: { address: string; path: string }[];
upgradeablePrograms?: {
address: string;
path: string;
upgradeAuthority: string;
}[];
skipSystemAccounts?: boolean;
limitLedgerSize?: number;
rpcPort?: number;
Expand All @@ -403,6 +432,7 @@ export async function startTestValidator({
const command = "solana-test-validator";
const solanaArgs = await getSolanaArgs({
additionalPrograms,
upgradeablePrograms,
skipSystemAccounts,
limitLedgerSize,
rpcPort,
Expand Down
1 change: 1 addition & 0 deletions cli/src/utils/processProverServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export async function startProver(proverPort: number, redisUrl?: string) {

spawnBinary(getProverPathByArch(), args);
await waitForServers([{ port: proverPort, path: "/" }]);
await new Promise((r) => setTimeout(r, 5000));
console.log(`Prover started successfully!`);
}

Expand Down
2 changes: 2 additions & 0 deletions forester/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ logs
.env.devnet
*.json
!package.json
spawn.sh
spawn_devnet.sh
Comment on lines +8 to +9
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, explore the forester directory structure
ls -la forester/ | head -20

Repository: Lightprotocol/light-protocol

Length of output: 933


🏁 Script executed:

# Look for template/example versions of spawn scripts
find forester -type f \( -name "*spawn*" -o -name "*.example" -o -name "*.template" \) 2>/dev/null

Repository: Lightprotocol/light-protocol

Length of output: 93


🏁 Script executed:

# Check for README or documentation files
find forester -type f \( -name "README*" -o -name "*.md" \) | head -20

Repository: Lightprotocol/light-protocol

Length of output: 90


🏁 Script executed:

# Check for CLAUDE.md in root and forester directory
find . -maxdepth 2 -name "CLAUDE.md" -o -name "claude.md"

Repository: Lightprotocol/light-protocol

Length of output: 131


🏁 Script executed:

# Search for mentions of spawn.sh or spawn_devnet.sh in documentation
rg -l "spawn\.(sh|devnet)" forester/ --type md 2>/dev/null | head -10

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Check the gitignore file to see full context
cat -n forester/.gitignore

Repository: Lightprotocol/light-protocol

Length of output: 216


🏁 Script executed:

# Check README for spawn scripts
cat forester/README.md

Repository: Lightprotocol/light-protocol

Length of output: 6562


🏁 Script executed:

# Check root CLAUDE.md for guidance
cat CLAUDE.md

Repository: Lightprotocol/light-protocol

Length of output: 11430


🏁 Script executed:

# Also check if there are any comments or docs about these scripts elsewhere
rg -n "spawn" forester/ --type md --type sh --type toml -C2

Repository: Lightprotocol/light-protocol

Length of output: 54


Document spawn scripts or align ignore patterns with .env precedent.

The repository uses .env.example as a template for ignored developer config files, but spawn.sh and spawn_devnet.sh lack equivalent templates or documentation. Either add spawn.sh.example and spawn_devnet.sh.example to establish what developers need to configure locally, or document these scripts in the README to clarify their purpose and whether they're user-created (template needed) or auto-generated (no template needed). Currently they're absent from both the README and CLAUDE.md documentation.

🤖 Prompt for AI Agents
In `@forester/.gitignore` around lines 8 - 9, The repo ignores spawn.sh and
spawn_devnet.sh but lacks example templates or docs; either add spawn.sh.example
and spawn_devnet.sh.example (matching the .env.example pattern) with required
variables and usage comments, or update README and CLAUDE.md to document the
purpose, required configuration, and whether these scripts are user-created or
generated; ensure the .gitignore entry stays in sync with the chosen approach
and reference the exact filenames spawn.sh and spawn_devnet.sh in the
docs/templates.

4 changes: 3 additions & 1 deletion forester/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ light-sdk = { workspace = true, features = ["anchor"] }
light-program-test = { workspace = true }
light-compressible = { workspace = true, default-features = false, features = ["solana"] }
light-token-interface = { workspace = true }
light-token-client = { workspace = true }
light-token = { workspace = true }
solana-rpc-client-api = { workspace = true }
solana-transaction-status = { workspace = true }
Expand All @@ -42,6 +43,7 @@ futures = { workspace = true }
thiserror = { workspace = true }
borsh = { workspace = true }
bs58 = { workspace = true }
hex = "0.4"
env_logger = { workspace = true }
async-trait = { workspace = true }
tracing = { workspace = true }
Expand All @@ -64,7 +66,7 @@ serial_test = { workspace = true }
light-prover-client = { workspace = true, features = ["devenv"] }
light-test-utils = { workspace = true }
light-program-test = { workspace = true, features = ["devenv"] }
light-token-client = { workspace = true }
light-compressed-token = { workspace = true }
rand = { workspace = true }
create-address-test-program = { workspace = true }
csdk-anchor-full-derived-test = { path = "../sdk-tests/csdk-anchor-full-derived-test", features = ["no-entrypoint"] }
1 change: 1 addition & 0 deletions forester/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"build": "cargo build",
"test": "redis-start && TEST_MODE=local TEST_V1_STATE=true TEST_V2_STATE=true TEST_V1_ADDRESS=true TEST_V2_ADDRESS=true RUST_LOG=forester=debug,forester_utils=debug,light_prover_client=debug cargo test --package forester e2e_test -- --nocapture",
"test:compressible": "cargo build-sbf -- -p csdk-anchor-full-derived-test && RUST_LOG=forester=debug,light_client=debug cargo test --package forester --test test_compressible_pda --test test_compressible_mint --test test_compressible_ctoken -- --nocapture",
"docker:build": "docker build --tag forester -f Dockerfile .."
Comment thread
ananas-block marked this conversation as resolved.
},
"devDependencies": {
Expand Down
8 changes: 8 additions & 0 deletions forester/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,14 @@ pub struct StartArgs {
)]
pub enable_compressible: bool,

#[arg(
long = "compressible-pda-program",
env = "COMPRESSIBLE_PDA_PROGRAMS",
help = "Compressible PDA programs to track. Format: 'program_id:discriminator_base58'. Can be specified multiple times. Example: 'MyProg1111111111111111111111111111111111111:6kRvHBv2N3F'",
value_delimiter = ','
)]
pub compressible_pda_programs: Vec<String>,

#[arg(
long,
env = "LOOKUP_TABLE_ADDRESS",
Expand Down
Loading