Documentation
X1 Randomness Protocol V4 — on-demand verifiable randomness on X1 Mainnet
Request Randomness
On-chain entropy + game seeds
dApp Registration
Register for callbacks & fee overrides
Round History
EE V4 commit/reveal lifecycle
Validators & Rewards
Earn fees by contributing entropy
Overview
The X1 Randomness Protocol V4 provides on-demand, verifiable randomness on X1 Mainnet. It wraps the EntropyEngine V4 commit/reveal scheme with a fully permissionless, decentralised architecture: no keeper authority, no committee manager — validators self-select based on on-chain entropy-derived eligibility.
Every randomness output is deterministic and auditable: SHA256(pool_entropy ‖ request_id ‖ slot_hash). The slot hash at transaction inclusion time is unknown at submission, making outputs unpredictable even if pool entropy is public.
Program ID
BSKTJpgAGHRaSMLA88chYPKuSuD9qbesEcHYmUrBWU7R
EE V4 Program
FDyWtM9UBNfXNuc5oZJ1V86d3dz635WnqMfX8x5Uifbm
Network
X1 Mainnet (RPC: rpc.mainnet.x1.xyz)
Architecture
The protocol has two layers:
Randomness Wrapper (this program)
Tracks protocol rounds (WrapperRound PDAs), collects request fees into FeeEscrow accounts, records validator reveals (ValidatorReveal PDAs), and manages fee distribution to validators.
EntropyEngine V4 (external program, CPI)
Runs the commit/reveal cycle. Validators (n=2 currently; grows with validator set) stake 0.01 XNT each, commit a hashed secret before commit_deadline (~200 slots), then reveal before reveal_deadline (~600 slots). After the binding slot (~675 slots / ~4.2 min), finalize_via_ee produces entropy_output.
The wrapper calls EE V4 via CPI for commit_via_ee, reveal_via_ee, and finalize_via_ee. After finalization, aggregate_from_ee mixes the EE V4 entropy output with the latest SlotHash into the protocol's EntropyPool.
Round Lifecycle
Each protocol round maps to one EE V4 commit/reveal cycle:
commit_via_ee
Validators stake 0.01 XNT and submit a hashed secret before commit_deadline (~200 slots after round init). Currently n=2 validators per round (grows with validator set; EE V4 max is 10).
reveal_via_ee
After commit_deadline (~200 slots) and before reveal_deadline (~600 slots), validators reveal their secret. This creates a ValidatorReveal PDA recording participation. The 0.01 XNT stake is returned on valid reveal.
finalize_via_ee + aggregate_from_ee
Any signer calls finalize_via_ee to mark the EE V4 round done, then aggregate_from_ee to mix entropy into the pool: SHA256(ee_output ‖ slot_hash). Both are permissionless cranks. EntropyPool is now warm.
distribute_fees
Permissionless crank. Pays 5% to the crank caller, records original_fees on FeeEscrow, marks fee_distributed = true. Validators can now claim their 95% share.
claim_validator_reward
Each validator calls this once per round to receive: original_fees × 95% ÷ reveal_count. Requires the ValidatorReveal PDA created at reveal time.
Program Instructions
| Instruction | Description | Who calls |
|---|---|---|
| initialize | Create ProtocolConfig + EntropyPool PDAs | Authority (once) |
| advance_round | Increment current_round, create WrapperRound PDA | Anyone (permissionless) |
| create_fee_escrow | Create FeeEscrow PDA for a round (must precede first request) | Anyone |
| init_ee_round | Open next EE V4 round (id must be current+1; n/m/binding_slot are protocol constants, not caller args) | Any registered active validator |
| commit_via_ee | Stake 0.01 XNT + commit hashed secret; eligibility derived on-chain from pool entropy | Validator (entropy-selected) |
| reveal_via_ee | Reveal secret (after commit_deadline ~200 slots, before reveal_deadline ~600 slots), creates ValidatorReveal PDA | Validator |
| finalize_via_ee | Finalize the EE V4 round via CPI | Anyone (permissionless) |
| aggregate_from_ee | Mix EE V4 entropy + SlotHash into EntropyPool | Anyone (permissionless) |
| request_randomness | Request entropy output (0.01 XNT standard fee; premium dApps pay more) | Any wallet / dApp |
| game_seed | Fast cheap seed from pool (0.001 XNT fee, warm pool only). Fee flows to validators. | Any wallet |
| distribute_fees | Pay 5% to crank caller; 95% stays for validators to claim | Anyone (permissionless, earns 5%) |
| claim_validator_reward | Claim per-validator share from FeeEscrow | Validator |
| deliver_callback | CPI-call the dApp's callback program with entropy output | Keeper/crank (must sign) |
| register_dapp | Register dApp PDA for callbacks | Any wallet |
| unregister_dapp | Close dApp PDA, reclaim rent | dApp authority |
| set_fee | Update protocol-wide request fee | Authority |
| update_dapp_fee | Set per-dApp fee override (0 = use protocol default) | Protocol authority |
| refund_request | Refund fee if EE V4 round was cancelled (status = 3) | Requester |
| close_request | Close fulfilled RequestState PDA and reclaim rent | Requester |
| verify_entropy | Verify a fulfilled RequestState's output matches receipt | Anyone |
| migrate_entropy_pool | One-time V4.3 migration: expands EntropyPool from 67→75 bytes to add total_game_seeds counter. Idempotent. | Anyone (permissionless) |
Account Types & Field Offsets
All accounts use 8-byte Anchor discriminators at offset 0. Raw deserialization offsets:
| 8–39 | authority | Pubkey |
| 40–71 | insurance_fund | Pubkey — kept for layout compatibility; not used since V4.5 |
| 72–79 | current_round | u64 |
| 80–87 | current_round_start_slot | u64 |
| 88–95 | ee_v4_round_id | u64 |
| 96–103 | total_rounds | u64 |
| 104–111 | request_fee | u64 (lamports) |
| 112 | bump | u8 |
| 8–39 | current_entropy | [u8; 32] (hex) |
| 40–47 | current_round | u64 |
| 48 | entropy_available | bool |
| 49–56 | last_aggregated_slot | u64 |
| 57–64 | total_requests_served | u64 |
| 65 | ee_v4_entropy_included | bool |
| 66 | bump | u8 |
| 67–74 | total_game_seeds | u64 — appended V4.3; check data.length >= 75 before reading |
| 8–15 | round | u64 |
| 16–23 | ee_v4_round_id | u64 |
| 24–31 | start_slot | u64 |
| 32 | aggregated | bool |
| 33–40 | aggregated_slot | u64 |
| 41–72 | entropy_output | [u8; 32] |
| 73–76 | pending_requests | u32 |
| 77–84 | total_fees | u64 |
| 85 | ee_v4_entropy_included | bool |
| 86 | bump | u8 |
| 8–15 | pending_fees | u64 (lamports) |
| 16–23 | round | u64 |
| 24–31 | original_fees | u64 — total fees before crank cut (V4.5) |
| 32–39 | ee_v4_round_id | u64 — EE V4 round that services this protocol round |
| 40 | fee_distributed | bool |
| 41 | bump | u8 |
| 8–39 | dapp_id | Pubkey (PDA seed) |
| 40–71 | callback_program | Pubkey |
| 72–79 | callback_instruction | [u8; 8] discriminator |
| 80–87 | min_round_interval | u64 |
| 88–95 | last_served_round | u64 |
| 96–103 | total_requests | u64 |
| 104–135 | authority | Pubkey |
| 136–143 | fee_override | u64 (0 = protocol default) |
| 144 | bump | u8 |
| 8–39 | contributor | Pubkey |
| 40–71 | ee_round | Pubkey |
| 72–79 | protocol_round | u64 |
| 80 | claimed | bool |
| 81 | bump | u8 |
| 8–39 | identity | Pubkey (validator identity key) |
| 40–71 | vote_account | Pubkey |
| 72–103 | stake_account | Pubkey |
| 104–111 | verified_stake | u64 (lamports) — re-verified on each refresh |
| 112–119 | registered_slot | u64 |
| 120–127 | last_active_slot | u64 — updated on successful commit |
| 128–135 | last_round_participated | u64 |
| 136 | consecutive_misses | u8 — 3+ triggers deactivation |
| 137 | active | bool |
| 138 | bump | u8 |
| 8–39 | request_id | [u8; 32] |
| 40–71 | requester | Pubkey |
| 72–103 | seed | [u8; 32] |
| 104–135 | callback_program | Pubkey |
| 136–143 | callback_instruction | [u8; 8] discriminator |
| 144–151 | round | u64 |
| 152 | fulfilled | bool |
| 153–184 | output | [u8; 32] |
| 185–192 | fee_paid | u64 |
| 193–200 | created_slot | u64 |
| 201 | bump | u8 |
PDA Seeds
| Account | Seeds |
|---|---|
| ProtocolConfig | ["protocol-config"] |
| EntropyPool | ["entropy-pool"] |
| WrapperRound | ["wrapper-round", round_as_u64_le] |
| FeeEscrow | ["fee-escrow", round_as_u64_le] |
| DappRegistration | ["dapp", dapp_id_pubkey] |
| ValidatorRegistration | ["val-reg", identity_pubkey] |
| ValidatorReveal | ["validator-reveal", ee_round_pubkey, contributor_pubkey] |
| RequestState | ["request", requester_pubkey, seed_bytes_32] |
Fee Economics
Standard request fee
0.01 XNT per request
Premium request fee
0.05 XNT per request (set by protocol authority via update_dapp_fee)
Game seed fee
0.001 XNT — flows to validators just like request fees
EE V4 commit stake
0.01 XNT (returned on valid reveal)
Validator share
95% of round fees ÷ reveal_count
Crank reward
5% to distribute_fees caller (V4.4)
All fees — both randomness requests and game seeds — flow into the FeeEscrow PDA for the current round. After distribute_fees runs, original_fees is recorded and fee_distributed = true. The crank caller receives 5% immediately; 95% stays in escrow for validators. Each validator who called reveal_via_ee in that round can then call claim_validator_reward to receive original_fees × 95% ÷ reveal_count.
SDK Integration
Use @solana/web3.js directly — no Anchor IDL required. All account data is raw-deserialized.
Read the entropy pool
import { Connection, PublicKey } from "@solana/web3.js";
const connection = new Connection("https://rpc.mainnet.x1.xyz", "confirmed");
const PROGRAM_ID = new PublicKey("BSKTJpgAGHRaSMLA88chYPKuSuD9qbesEcHYmUrBWU7R");
// EntropyPool PDA
const [poolPda] = PublicKey.findProgramAddressSync(
[Buffer.from("entropy-pool")],
PROGRAM_ID
);
const info = await connection.getAccountInfo(poolPda, "confirmed");
const d = Buffer.from(info.data);
const entropyHex = d.slice(8, 40).toString("hex"); // 32 bytes @ offset 8
const currentRound = Number(d.readBigUInt64LE(40));
const entropyAvailable = d[48] !== 0;
const lastAggregatedSlot = Number(d.readBigUInt64LE(49));Build a request_randomness transaction
import { Transaction, TransactionInstruction, SystemProgram } from "@solana/web3.js";
const DISC_REQUEST = Buffer.from([213, 5, 173, 166, 37, 236, 31, 18]); // sha256("global:request_randomness")[:8]
// Derive PDAs
function u64le(n) {
const b = Buffer.alloc(8);
b.writeBigUInt64LE(BigInt(n));
return b;
}
const [configPda] = PublicKey.findProgramAddressSync([Buffer.from("protocol-config")], PROGRAM_ID);
const [poolPda] = PublicKey.findProgramAddressSync([Buffer.from("entropy-pool")], PROGRAM_ID);
const [escrowPda] = PublicKey.findProgramAddressSync([Buffer.from("fee-escrow"), u64le(currentRound)], PROGRAM_ID);
const [wrapperRoundPda] = PublicKey.findProgramAddressSync([Buffer.from("wrapper-round"), u64le(currentRound)], PROGRAM_ID);
const [requestPda] = PublicKey.findProgramAddressSync(
[Buffer.from("request"), requesterPubkey.toBuffer(), seedBytes32],
PROGRAM_ID
);
const callbackProgram = new PublicKey("11111111111111111111111111111111"); // SystemProgram = no callback
const callbackInstruction = Buffer.alloc(8, 0);
const data = Buffer.concat([DISC_REQUEST, seedBytes32, callbackProgram.toBuffer(), callbackInstruction]);
const SLOT_HASHES = new PublicKey("SysvarS1otHashes111111111111111111111111111");
const ix = new TransactionInstruction({
programId: PROGRAM_ID,
keys: [
{ pubkey: requestPda, isSigner: false, isWritable: true },
{ pubkey: requesterPubkey, isSigner: true, isWritable: true },
{ pubkey: configPda, isSigner: false, isWritable: false },
{ pubkey: poolPda, isSigner: false, isWritable: true },
{ pubkey: escrowPda, isSigner: false, isWritable: true },
{ pubkey: wrapperRoundPda, isSigner: false, isWritable: false },
{ pubkey: SLOT_HASHES, isSigner: false, isWritable: false },
// dapp_registration: pass SystemProgram as opt-out (no fee override)
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
data,
});Poll for fulfillment (RequestState.fulfilled @ offset 152)
async function pollFulfillment(requestPda, connection, maxAttempts = 60) {
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 2000));
const info = await connection.getAccountInfo(requestPda, "confirmed");
if (!info || info.data.length < 202) continue;
const d = Buffer.from(info.data);
if (d[152]) { // fulfilled = true
const output = d.slice(153, 185).toString("hex");
return output;
}
}
return null;
}Running a Validator Node
As of V4, the full round lifecycle is permissionless and validator-driven. No protocol authority needs to act. Any X1 validator with a funded keypair can:
Start the next round
Call advance_round + create_fee_escrow (permissionless, any signer), then init_ee_round(ee_round_id = current+1). n, m, and binding_slot are protocol constants — not caller args. First validator to land init_ee_round opens the commit window.
Commit
Call commit_via_ee(SHA256(secret || nonce || pubkey)) before commit_deadline (~200 slots). Stake 0.01 XNT. Currently n=2 validators per round (EE V4 max is 10).
Reveal
After commit_deadline (~200 slots) and before reveal_deadline (~600 slots / ~3.75 min), call reveal_via_ee(secret, nonce). Stake is returned. Creates a ValidatorReveal PDA for fee claiming.
Finalize + aggregate
Call finalize_via_ee (permissionless), then aggregate_from_ee. Pool is now warm — queued requests are fulfilled.
Claim reward
Call distribute_fees (permissionless — crank runner earns 5%), then claim_validator_reward. Receive original_fees × 95% ÷ reveal_count.
The only coordination needed is off-chain: validators watch for new init_ee_round transactions (or check whether ProtocolConfig.ee_v4_round_id has a corresponding non-aggregated WrapperRound) and commit before the commit_deadline set in the EE V4 round.
Idle behaviour (V4.3)
Validators and the crank do not run EE rounds when nobody needs randomness. Before opening a new round, both check two conditions: (1) is the entropy pool stale (> 21,600 slots since last aggregation), and (2) are there any unfulfilled RequestState accounts on-chain? If the pool is warm and there are no queued requests, they idle and re-check on the next poll tick. A round starts automatically the moment either condition changes — no manual intervention required.
Validators per round
n=2 (wrapper config; EE V4 hardcoded max is 10)
Minimum reveal threshold
m=2 (enforced by wrapper, prevents solo runs)
Commit stake
0.01 XNT (returned on valid reveal)
Binding slot minimum
675 slots (~4.2 min after round init)
Slash on non-reveal
0.01 XNT forfeited to EE V4 slash pool
Reward per round
original_fees × 95% ÷ reveal_count
Security Properties
EE V4 program pinned
All four CPI instructions enforce address == FDyWtM9U... Cannot pass a counterfeit EE V4 program.
init_ee_round: sequential ID + constants-only params
ee_round_id must equal current+1 (prevents ID jumping). n_contributors, m_threshold, and binding_slot are hardcoded protocol constants — no caller can override committee size or threshold. Permissionless but fully constrained.
On-chain validator selection
commit_via_ee enforces entropy-derived eligibility: SHA256(pool_entropy ‖ ee_round_id) → SHA256(round_seed ‖ contributor_pubkey) → compare low 8 bytes against COMMIT_SELECTION_THRESHOLD. No external actor can decide who commits.
ee_round ownership enforced
finalize_via_ee checks ee_round.owner == EE V4. Prevents fake entropy injection via crafted accounts.
SlotHash mixing
Both finalize_via_ee and aggregate_from_ee XOR in the latest SlotHash — not a predictable slot number.
distribute_fees is idempotent
Sets fee_distributed = true and rejects if already set. claim_validator_reward requires fee_distributed == true.
Pool staleness hard limit
request_randomness routes to queue path (not fast path) for pool entropy older than 21,600 slots (~2.25 hr). Matches the idle gate threshold — validators idle when the pool is fresh and there are no queued requests.
Liveness protection
If an EE V4 round is cancelled (status byte 140 == 3), refund_request lets requesters recover their fee from the FeeEscrow.
Validator credential binding
register_validator, init_ee_round, and commit_via_ee all read the vote account's node_pubkey (offset 4) and require it matches the signing identity. A validator cannot borrow another's vote or stake accounts to inflate credentials or impersonate them during commits.
No premature round advance
advance_round requires the current protocol round's WrapperRound.aggregated == true before creating the next round. This prevents stranding an in-flight EE round without a protocol round to receive its entropy.
Cross-round refund protection
aggregate_from_ee stamps fee_escrow.ee_v4_round_id with the actual EE round that serviced the protocol round. refund_request validates this field, preventing requests from claiming refunds against a different round's escrow.
mark_validator_missed timing guard
mark_validator_missed only counts a miss if the validator was registered before the EE round opened (registered_slot < binding_slot − 675). This blocks using historical finalized/cancelled rounds to instantly deactivate newly registered validators.
SlotHashes sysvar required in request_randomness and game_seed
Both instructions include the SlotHashes sysvar (SysvarS1otHashes111111111111111111111111111) as a required account. This ensures the output mixes in a slot hash that is unknown at submission time, making outputs unpredictable even with known pool entropy.
Idle gate — no wasted validator resources
Validators and the crank skip opening new EE rounds when the pool is warm (< 21,600 slots stale) and there are no unfulfilled RequestState accounts. A round starts automatically when the pool goes stale or a queued request appears.
FAQ
How fast is a randomness request?
Instant if the pool is warm (fast path). Otherwise, your request queues and is fulfilled after the next EE V4 round completes — typically 4–10 minutes.
Can a validator bias the output?
No. They commit a hash first. The reveal is verified against the commit, so they cannot change their secret after seeing others'. The SlotHash mixes in additional unpredictability no validator controls.
What if fewer than the minimum validators reveal?
The EE V4 round is cancelled (status = 3). The pool does not update. Requesters can call refund_request to recover their fee.
How many validators participate per round?
The wrapper sets n_contributors=2 and m_threshold=2 when opening each EE V4 round, meaning exactly 2 validators commit and reveal per round. This matches the current validator set size. EntropyEngine V4 is hardcoded to accept at most 10 contributors per round total. As the validator set grows, n will be increased to match.
How do I earn rewards as a validator?
Run validator-daemon.js with your identity keypair. It monitors the chain, checks your on-chain entropy-based eligibility each round, commits and reveals autonomously, and calls claim_validator_reward once fees are distributed. Each round pays: original_fees × 95% ÷ reveal_count.
What does fee_override do for dApps?
The protocol authority (not the dApp) can set a custom per-dApp fee via update_dapp_fee after registration. When a request comes in from that dApp, the override fee is charged instead of the protocol default. 0 means use the protocol default. Higher fees mean larger validator rewards per round, incentivising liveness.
How does verify_entropy work?
It requires a fulfilled RequestState PDA. The receipt's derived_output is copied from request_state.output — the actual stored value — and confirmed on-chain. Anyone can verify any output permissionlessly.