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
Risk Disclosure
Stake risk, permissionless use, no warranty
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. n=7 validators stake 0.01 XNT each, commit a hashed secret before commit_deadline (~200 slots), then reveal before reveal_deadline (~600 slots). m=5 reveals suffice to finalize. 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). n=7 validators are selected per round; all 7 must commit before reveals begin.
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) | dApp authority |
| migrate_validator_registration | V4.6 one-time migration: grow ValidatorRegistration 139→171 bytes and set x1_randomness_authority = identity. Permissionless. | Anyone |
| rotate_randomness_authority | Set a new hot key for commit/reveal/init_ee_round/claim — signed by identity (cold key). V4.6+ | Validator (identity) |
| revoke_randomness_authority | Reset x1_randomness_authority back to identity. V4.6+ | Validator (identity) |
| 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 cold 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 — 5+ triggers deactivation (V4.6; was 3) |
| 137 | active | bool |
| 138 | bump | u8 |
| 139–170 | x1_randomness_authority | Pubkey — hot key for commit/reveal/init_ee_round/claim; equals identity until rotate_randomness_authority is called. Appended V4.6; check data.length >= 171 before reading. |
| 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 dApp authority via update_dapp_fee — no protocol owner intervention needed)
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.5)
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=7, m=5, 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. n=7 validators per round; all 7 must commit before reveals begin. m=5 reveals suffice to finalize.
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=7 (all 7 must commit; grows probabilistically with validator set)
Minimum reveal threshold
m=5 (5 of 7 reveals suffice to finalize; prevents single-validator entropy)
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 ‖ validator_reg.identity) → compare low 8 bytes against COMMIT_SELECTION_THRESHOLD. Uses validator_reg.identity (cold key), not the signer — selection probability is stable across hot-key rotations. 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 checks that the vote account's node_pubkey matches the signing identity. init_ee_round and commit_via_ee verify node_pubkey matches validator_reg.identity (the registered cold key) — not the transaction signer. This means a hot-key-rotated validator cannot use a different validator's vote account; the check always goes back to the immutable identity field in the registration.
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.
Key separation (V4.6) — hot key for daily ops, cold key stays offline
Each ValidatorRegistration now holds an x1_randomness_authority hot key (offset 139). The hot key can sign commit/reveal/init_ee_round/claim; it cannot register, refresh, deregister, or rotate — those still require the identity cold key. Eligibility hash always uses identity (not the hot key) so selection probability is stable across rotations. Rotate via: VALIDATOR_KEYPAIR=identity.json node validator-daemon.js --rotate-authority <hotkey_pubkey>
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=7 and m_threshold=5 when opening each EE V4 round (V4.6). All 7 selected validators must commit before reveals begin; 5 valid reveals suffice to finalize. Eligibility is derived on-chain from pool entropy — no external actor controls who is in the committee.
How do I earn rewards as a validator?
Run validator-daemon.js on a dedicated randomness server. Recommended: generate a hot key on your validator server, rotate authority once (identity key signs once), then run the daemon on a separate server using VALIDATOR_IDENTITY_PUBKEY=<your_pubkey> X1_RANDOMNESS_KEYPAIR=<hotkey.json> — the identity key never touches the randomness server. The daemon monitors the chain, checks on-chain eligibility each round, commits and reveals autonomously, and calls claim_validator_reward once fees are distributed. Each round pays: original_fees × 95% ÷ reveal_count. See the Validators page for step-by-step setup.
What does fee_override do for dApps?
The dApp authority (the wallet that registered the dApp) can set a custom fee via update_dapp_fee. 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 for premium use cases.
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.
Risk Disclosure & Terms of Participation
This protocol is experimental software deployed on X1 Mainnet. By participating — as a validator, dApp integrator, or randomness consumer — you accept the risks described below in full. There is no warranty, no recourse, and no controlling entity that can reverse on-chain outcomes.
Validator commit stake — slash risk
Each validator who is selected to participate in a round must call commit_via_ee and stake 0.01 XNT. This stake is returned in full on a valid, on-time reveal. If the validator fails to call reveal_via_ee before reveal_deadline (~600 slots / ~3.75 min from round init), the 0.01 XNT is forfeited to the EE V4 slash pool. This forfeiture is enforced automatically and irreversibly by the on-chain program — no human intervention is needed or possible. Validators bear sole responsibility for maintaining the infrastructure required to reveal on time.
Permissionless protocol — no restrictions on use
The protocol is fully permissionless. Anyone can call request_randomness, game_seed, register as a dApp, or run a validator node. There are no terms of service, no whitelist, and no entity capable of restricting access. The randomness output can be used for any purpose. Participants accept that others may use the same infrastructure for applications they find objectionable — neither the protocol nor its operators have the ability to intervene.
dApp integrators — assume all risk
dApps that integrate this protocol for randomness-dependent applications (games, lotteries, NFT mints, financial products, etc.) do so entirely at their own risk. The protocol does not guarantee availability, latency, or any particular statistical property beyond what is mathematically enforced on-chain. dApp operators are solely responsible for ensuring their integration is correct, that they handle the queued path (pool stale or round cancelled) gracefully, and that their users understand the risks.
No warranty, no recourse
This protocol is provided as-is. There is no entity that controls it, no upgrade authority with special powers over user funds, and no mechanism to reverse, pause, or compensate for any on-chain outcome. Fees paid for requests that end up in a cancelled round can be recovered via refund_request, but this too is a permissionless on-chain call — no party is obligated to execute it on your behalf.
Experimental software
X1 Randomness Protocol V4 is experimental. It has been reviewed for known attack vectors and security properties are documented in the Security section above. Nevertheless, undiscovered vulnerabilities may exist. Do not use this protocol for applications where a loss of funds or biased randomness would be catastrophic or irreversible without independent security review.