The jury program seals a panel of evaluators with a Switchboard On-Demand randomness result. Same seed always produces the same panel, so anyone can replay the draw and prove the gateway did not stuff the panel.
npm i @telaro/sacp @solana/web3.js
# Switchboard SDK is only needed for the production path.
npm i @switchboard-xyz/on-demand @coral-xyz/anchorimport { Connection, Transaction } from "@solana/web3.js";
import { jury } from "@telaro/sacp";
import { sha256 } from "@noble/hashes/sha256";
import { randomBytes } from "node:crypto";
const conn = new Connection("https://api.devnet.solana.com");
const jobIdHash = sha256(new TextEncoder().encode("acme-translate-#42"));
// 1. Open the jury PDA.
const openIx = jury.buildOpenJuryIx({
caller: wallet.publicKey,
jobIdHash,
panelSize: 3,
});
// 2. Seal it with a random seed. Same seed always picks the same panel.
const sealIx = jury.buildSealPanelIx({
cranker: wallet.publicKey,
jury: jury.deriveJuryPda(jobIdHash)[0],
candidates: [
{ authority: validatorA, stakeAtoms: 5_000_000n },
{ authority: validatorB, stakeAtoms: 3_000_000n },
{ authority: validatorC, stakeAtoms: 1_000_000n },
],
mockSeed: new Uint8Array(randomBytes(32)),
});
const tx = new Transaction().add(openIx, sealIx);
tx.feePayer = wallet.publicKey;
tx.recentBlockhash = (await conn.getLatestBlockhash()).blockhash;
await wallet.signAndSend(tx);
// 3. Read the sealed panel.
const fetched = await jury.fetchJury(conn, jobIdHash);
console.log("panel:", fetched?.panel.map(p => p.toBase58()));Switchboard reveal writes the random bytes into the same transaction that runs your settle step. Bundle reveal and seal together. A standalone seal in a later transaction reads pre-reveal state and fails with RandomnessNotResolved.
import { Keypair, Transaction } from "@solana/web3.js";
import { AnchorProvider, Wallet } from "@coral-xyz/anchor";
import * as sb from "@switchboard-xyz/on-demand";
import { jury } from "@telaro/sacp";
const provider = new AnchorProvider(conn, new Wallet(payer), {});
const sbProgram = await sb.AnchorUtils.loadProgramFromConnection(
conn,
provider.wallet,
);
const queue = await sb.Queue.loadDefault(sbProgram);
// 1. Randomness create.
const rngKp = Keypair.generate();
const [randomness, createIx] = await sb.Randomness.create(
sbProgram,
rngKp,
queue.pubkey,
);
await sendTx(conn, [createIx], payer, [rngKp]);
// 2. Open the jury PDA.
const openIx = jury.buildOpenJuryIx({
caller: payer.publicKey,
jobIdHash,
panelSize: 3,
});
await sendTx(conn, [openIx], payer);
// 3. Commit. Wait a few slots.
await sendTx(conn, [await randomness.commitIx(queue.pubkey)], payer);
await new Promise(r => setTimeout(r, 4000));
// 4. Reveal + seal in the same tx.
const sealIx = jury.buildSealPanelFromRandomnessIx({
cranker: payer.publicKey,
jury: jury.deriveJuryPda(jobIdHash)[0],
candidates: [...],
randomness: rngKp.publicKey,
});
await sendTx(conn, [await randomness.revealIx(), sealIx], payer);The evaluator dashboard does this on every load to make sure the gateway's panel matches the on-chain panel. If they don't, the gateway might be lying.
const fetched = await jury.fetchJury(conn, jobIdHash);
if (fetched?.sealed) {
const onChainPanel = fetched.panel.map(p => p.toBase58());
const gatewayPanel = dispute.panel;
const ok = onChainPanel.length === gatewayPanel.length &&
onChainPanel.every(p => gatewayPanel.includes(p));
if (!ok) {
// refuse to vote
}
}| Helper | Use |
|---|---|
| deriveJuryPda(jobIdHash) | Find the on-chain account for a given jobId. |
| buildOpenJuryIx(...) | Create the PDA. Panel is still empty until a seal IX runs. |
| buildSealPanelIx(...) | Dev path. Caller supplies a 32-byte mock seed. |
| buildSealPanelFromRandomnessIx(...) | Production path. Pair with a revealed Switchboard Randomness PDA. |
| fetchJury(conn, jobIdHash) | Read + parse the PDA. Returns null if not yet opened. |
See also: Agreement (PoA) for the spec that validators rule against, and the end-to-end devnet smoke for a working reference.