Documentation

X1 Randomness Protocol V4 — on-demand verifiable randomness on X1 Mainnet

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:

1

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).

2

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.

3

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.

4

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.

5

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

InstructionDescriptionWho calls
initializeCreate ProtocolConfig + EntropyPool PDAsAuthority (once)
advance_roundIncrement current_round, create WrapperRound PDAAnyone (permissionless)
create_fee_escrowCreate FeeEscrow PDA for a round (must precede first request)Anyone
init_ee_roundOpen 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_eeStake 0.01 XNT + commit hashed secret; eligibility derived on-chain from pool entropyValidator (entropy-selected)
reveal_via_eeReveal secret (after commit_deadline ~200 slots, before reveal_deadline ~600 slots), creates ValidatorReveal PDAValidator
finalize_via_eeFinalize the EE V4 round via CPIAnyone (permissionless)
aggregate_from_eeMix EE V4 entropy + SlotHash into EntropyPoolAnyone (permissionless)
request_randomnessRequest entropy output (0.01 XNT standard fee; premium dApps pay more)Any wallet / dApp
game_seedFast cheap seed from pool (0.001 XNT fee, warm pool only). Fee flows to validators.Any wallet
distribute_feesPay 5% to crank caller; 95% stays for validators to claimAnyone (permissionless, earns 5%)
claim_validator_rewardClaim per-validator share from FeeEscrowValidator
deliver_callbackCPI-call the dApp's callback program with entropy outputKeeper/crank (must sign)
register_dappRegister dApp PDA for callbacksAny wallet
unregister_dappClose dApp PDA, reclaim rentdApp authority
set_feeUpdate protocol-wide request feeAuthority
update_dapp_feeSet per-dApp fee override (0 = use protocol default)Protocol authority
refund_requestRefund fee if EE V4 round was cancelled (status = 3)Requester
close_requestClose fulfilled RequestState PDA and reclaim rentRequester
verify_entropyVerify a fulfilled RequestState's output matches receiptAnyone
migrate_entropy_poolOne-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:

ProtocolConfig113 bytes
8–39authorityPubkey
40–71insurance_fundPubkey — kept for layout compatibility; not used since V4.5
72–79current_roundu64
80–87current_round_start_slotu64
88–95ee_v4_round_idu64
96–103total_roundsu64
104–111request_feeu64 (lamports)
112bumpu8
EntropyPool75 bytes (67 before V4.3 migration)
8–39current_entropy[u8; 32] (hex)
40–47current_roundu64
48entropy_availablebool
49–56last_aggregated_slotu64
57–64total_requests_servedu64
65ee_v4_entropy_includedbool
66bumpu8
67–74total_game_seedsu64 — appended V4.3; check data.length >= 75 before reading
WrapperRound87 bytes
8–15roundu64
16–23ee_v4_round_idu64
24–31start_slotu64
32aggregatedbool
33–40aggregated_slotu64
41–72entropy_output[u8; 32]
73–76pending_requestsu32
77–84total_feesu64
85ee_v4_entropy_includedbool
86bumpu8
FeeEscrow42 bytes
8–15pending_feesu64 (lamports)
16–23roundu64
24–31original_feesu64 — total fees before crank cut (V4.5)
32–39ee_v4_round_idu64 — EE V4 round that services this protocol round
40fee_distributedbool
41bumpu8
DappRegistration145 bytes
8–39dapp_idPubkey (PDA seed)
40–71callback_programPubkey
72–79callback_instruction[u8; 8] discriminator
80–87min_round_intervalu64
88–95last_served_roundu64
96–103total_requestsu64
104–135authorityPubkey
136–143fee_overrideu64 (0 = protocol default)
144bumpu8
ValidatorReveal82 bytes
8–39contributorPubkey
40–71ee_roundPubkey
72–79protocol_roundu64
80claimedbool
81bumpu8
ValidatorRegistration139 bytes
8–39identityPubkey (validator identity key)
40–71vote_accountPubkey
72–103stake_accountPubkey
104–111verified_stakeu64 (lamports) — re-verified on each refresh
112–119registered_slotu64
120–127last_active_slotu64 — updated on successful commit
128–135last_round_participatedu64
136consecutive_missesu8 — 3+ triggers deactivation
137activebool
138bumpu8
RequestState202 bytes
8–39request_id[u8; 32]
40–71requesterPubkey
72–103seed[u8; 32]
104–135callback_programPubkey
136–143callback_instruction[u8; 8] discriminator
144–151roundu64
152fulfilledbool
153–184output[u8; 32]
185–192fee_paidu64
193–200created_slotu64
201bumpu8

PDA Seeds

AccountSeeds
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:

1

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.

2

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).

3

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.

4

Finalize + aggregate

Call finalize_via_ee (permissionless), then aggregate_from_ee. Pool is now warm — queued requests are fulfilled.

5

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.

Resources