On-demand verifiable randomness on X1 Mainnet. A fully permissionless, decentralised wrapper around EntropyEngine V4 — no keeper authority, no committee manager. Validators self-select based on on-chain entropy-derived eligibility. dApps get unique, unpredictable outputs with fee collection, game seed support, per-validator rewards, and on-chain verification receipts.
| Component | Address |
|---|---|
| Randomness Wrapper V4 | BSKTJpgAGHRaSMLA88chYPKuSuD9qbesEcHYmUrBWU7R |
| Entropy Engine V4 | FDyWtM9UBNfXNuc5oZJ1V86d3dz635WnqMfX8x5Uifbm |
┌──────────────────────────────────────────────────────────┐
│ dApp / User │
│ request_randomness(seed, callback) — 0.01 XNT │
│ game_seed(game_id) — 0.001 XNT │
│ │
│ Fast path: sub-second (entropy pool warm) │
│ Queue path: waits for next round aggregation │
└──────────────────┬───────────────────────────────────────┘
│ fee → FeeEscrow[current_round]
▼
┌──────────────────────────────────────────────────────────┐
│ Randomness Wrapper V4 (BSKTJp...) │
│ │
│ ┌────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ EntropyPool │ │ WrapperRound │ │ FeeEscrow │ │
│ │ (hot cache) │ │ (EE V4 map) │ │ per round │ │
│ └────────────┘ └──────────────┘ └──────────────┘ │
│ ┌────────────────┐ ┌───────────────────────┐ │
│ │ DappRegistration│ │ ValidatorRegistration │ │
│ └────────────────┘ └───────────────────────┘ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ ValidatorReveal │ │ RequestState │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ Request output = SHA256( │
│ pool_entropy, │
│ request_id, │
│ slot_hash, ← unknown at submission time │
│ ) │
└──────────────────┬───────────────────────────────────────┘
│ CPI (commit/reveal/finalize)
▼
┌──────────────────────────────────────────────────────────┐
│ Entropy Engine V4 (FDyWt...) │
│ Immutable · RANDAO + VDF · Perpetual round cycle │
│ n=2 contributors per round (grows with validator set) │
│ Commit stake 0.01 XNT (returned on valid reveal) │
└──────────────────────────────────────────────────────────┘
The protocol has no keeper authority, no committee manager, and no validator selection committee. Every instruction is either a permissionless crank or self-signed by the actor:
| Instruction | Who calls |
|---|---|
advance_round |
Anyone |
create_fee_escrow |
Anyone |
init_ee_round |
Any registered active validator (first wins; n/m/binding_slot are protocol constants, not caller args) |
commit_via_ee |
Any validator whose eligibility hash passes the on-chain threshold |
reveal_via_ee |
Validators who committed |
finalize_via_ee |
Anyone |
aggregate_from_ee |
Anyone |
distribute_fees |
Anyone |
claim_validator_reward |
Each validator independently |
Eligibility for commit_via_ee is derived deterministically from pool entropy — no external actor can control who participates:
round_seed = SHA256(entropy_pool.current_entropy ‖ ee_v4_round_id)
val_hash = SHA256(round_seed ‖ contributor.pubkey)
selector = val_hash[0..8] as u64
eligible = selector < COMMIT_SELECTION_THRESHOLD // currently u64::MAX (all eligible)
Lowering COMMIT_SELECTION_THRESHOLD as the validator set grows caps expected committee size probabilistically.
| Process | Keys held | Purpose |
|---|---|---|
run-round.js (crank) |
Crank key only | Calls permissionless on-chain cranks. Zero protocol authority. |
validator-daemon.js |
Own identity key only | Each validator runs independently. Monitors chain, commits, reveals, claims rewards. |
The crank has no special power — any node can replace it. Stopping the crank delays round advancement but cannot corrupt randomness.
advance_round()
→ increments protocol_config.current_round
→ creates WrapperRound PDA [b"wrapper-round", new_round]
create_fee_escrow(round)
→ creates FeeEscrow PDA [b"fee-escrow", round]
→ must be called before any request_randomness in this round
init_ee_round(ee_round_id)
→ n=MIN_EE_M_THRESHOLD(2), m=MIN_EE_M_THRESHOLD(2),
binding_slot=current_slot+EE_V4_MIN_BINDING_SLOTS(675) — all derived on-chain
→ CPIs initialize_round into EE V4
→ creates WrapperRound PDA [b"wrapper-round", ee_round_id]
→ sets protocol_config.ee_v4_round_id = ee_round_id
commit_via_ee(commitment)
→ on-chain eligibility check: SHA256(pool_entropy ‖ round_id) → SHA256(round_seed ‖ pubkey) < threshold
→ CPIs commit into EE V4
→ transfers 0.01 XNT stake from contributor to EE round account
→ commitment = SHA256(secret ‖ nonce ‖ contributor_pubkey)
reveal_via_ee(secret, nonce)
→ CPIs reveal into EE V4
→ verifies commitment, accumulates entropy via SHA256-chain
→ returns 0.01 XNT stake to contributor
→ creates ValidatorReveal PDA [b"validator-reveal", ee_round, contributor]
→ must be called after commit_deadline (~200 slots) and before reveal_deadline (~600 slots)
finalize_via_ee()
→ CPIs finalize into EE V4 (binds slot hash, produces entropy_output)
→ requires current_slot >= binding_slot (~init_slot + 675)
→ NOTE: if current_slot > binding_slot + 512, the slot hash is pruned from SlotHashes
and finalization is permanently impossible — cancel the EE round and open a new one
→ mixes: SHA256(ee_entropy ‖ slot_hash) into EntropyPool
→ marks the EE WrapperRound as aggregated
aggregate_from_ee(protocol_wrapper_round, ee_round)
→ reads finalized EE V4 round (no CPI, just account data)
→ verifies status == Finalized and round_id matches protocol_config.ee_v4_round_id
→ marks the advance_round WrapperRound as aggregated
→ updates EntropyPool — requests can now be fulfilled
distribute_fees()
→ requires protocol WrapperRound.aggregated == true
→ records original_fees = pending_fees on FeeEscrow
→ sends 5% of pending_fees to crank caller immediately
→ sets fee_distributed = true (idempotent — rejects on re-entry)
→ 95% stays in FeeEscrow for validators to claim
claim_validator_reward()
→ requires ValidatorReveal PDA created at reveal time
→ requires fee_escrow.fee_distributed == true
→ reads reveal_count from EE V4 round data at offset 75
→ pays: original_fees × 95% ÷ reveal_count to contributor
→ marks ValidatorReveal.claimed = true (rejects on re-entry)
request_randomness(seed, callback_program, callback_instruction)
→ transfers fee from requester to FeeEscrow[current_round]
→ Fast path (pool warm): output = SHA256(pool_entropy ‖ request_id ‖ slot_hash) — same tx
→ Queue path (pool cold): stores RequestState PDA, fulfilled after next aggregation
| Instruction | Description | Who |
|---|---|---|
initialize |
Bootstrap ProtocolConfig + EntropyPool | Authority (once) |
register_dapp |
Register dApp with callback and frequency config | Any wallet |
unregister_dapp |
Remove dApp registration, reclaim rent | dApp authority |
create_fee_escrow(round) |
Create FeeEscrow PDA for a round | Anyone |
request_randomness(seed, callback) |
Request entropy — instant if pool warm, queued if cold | Any wallet / dApp |
game_seed(game_id) |
Fast seed from pool entropy — warm pool only | Any wallet |
advance_round |
Move to next protocol round | Anyone (permissionless) |
distribute_fees |
Pay 5% to crank; record original_fees; enable validator 95% claims | Anyone (permissionless, earns 5%) |
claim_validator_reward |
Per-validator fee claim — original_fees × 95% ÷ reveal_count | Validator |
set_fee(new_fee) |
Update protocol-wide request fee | Authority |
update_dapp_fee(fee_override) |
Set per-dApp fee override, 0 = protocol default | Protocol authority |
refund_request |
Refund fee if EE V4 round was cancelled (status byte 140 == 3) | Requester |
deliver_callback |
Push entropy via CPI to registered dApp callback | Crank (must sign) |
verify_entropy(request_id) |
Create on-chain EntropyReceipt from fulfilled RequestState | Anyone |
close_request |
Close fulfilled RequestState, reclaim rent | Requester |
register_validator |
Register identity + vote + stake accounts | Validator |
deregister_validator |
Remove validator registration | Validator |
refresh_validator_status |
Re-verify stake and liveness; reactivate if back online | Validator |
| Instruction | Description |
|---|---|
init_ee_round(ee_round_id) |
Initialize EE V4 round; n/m/binding_slot are protocol constants |
commit_via_ee(commitment) |
Validator commits — eligibility derived on-chain; transfers 0.01 XNT stake |
reveal_via_ee(secret, nonce) |
Validator reveals — returns stake, accumulates entropy, creates ValidatorReveal PDA |
finalize_via_ee |
Finalize EE V4 round + mix entropy (using SlotHashes sysvar) into EntropyPool |
aggregate_from_ee |
Read pre-finalized EE V4 round into protocol WrapperRound; requires SlotHashes sysvar |
| Account | Seeds | Size | Description |
|---|---|---|---|
ProtocolConfig |
["protocol-config"] |
113 B | Global config, authority, current round, EE V4 round tracking |
EntropyPool |
["entropy-pool"] |
67 B | Hot entropy cache — fast path served from here |
WrapperRound (protocol) |
["wrapper-round", round] |
87 B | Created by advance_round. Tracks pending requests and fees. |
WrapperRound (EE) |
["wrapper-round", ee_round_id] |
87 B | Created by init_ee_round. Tracks EE V4 mapping and aggregation status. |
FeeEscrow |
["fee-escrow", round] |
42 B | Fee accumulation per protocol round |
DappRegistration |
["dapp", dapp_id] |
145 B | Per-dApp callback, frequency config, fee override |
ValidatorRegistration |
["val-reg", identity] |
139 B | Validator identity, vote/stake accounts, liveness tracking |
ValidatorReveal |
["validator-reveal", ee_round, contributor] |
82 B | Created at reveal time; used to claim per-validator reward |
RequestState |
["request", requester, seed] |
202 B | Individual randomness request (queue path) |
EntropyReceipt |
["receipt", request_id] |
— | Trustless provenance verification receipt |
| Item | Formula |
|---|---|
| Request ID | SHA256(callback_program ‖ callback_instruction ‖ seed ‖ requester) |
| Fast/queue path output | SHA256(pool_entropy ‖ request_id ‖ slot_hash) |
| Aggregated entropy | SHA256(ee_v4_entropy ‖ slot_hash) |
| EE V4 commitment | SHA256(secret ‖ nonce ‖ contributor_pubkey) |
| Game seed output | SHA256(pool_entropy ‖ game_id) |
| Validator eligibility | SHA256(SHA256(pool_entropy ‖ ee_round_id) ‖ contributor_pubkey)[0..8] < COMMIT_SELECTION_THRESHOLD |
| Per-validator reward | original_fees × 95% ÷ reveal_count |
| Item | Amount |
|---|---|
| Standard request fee | 0.01 XNT (default for all dApps) |
| Premium request fee | 0.05 XNT (set by protocol authority via update_dapp_fee) |
| Game seed fee | 0.001 XNT — flows to validators via FeeEscrow, same as request fees |
| EE V4 stake (per commit) | 0.01 XNT — returned in full on valid reveal; forfeited on miss |
| Crank reward | 5% of round fees to distribute_fees caller (V4.5) |
| Validator share | 95% ÷ reveal_count via claim_validator_reward (V4.5) |
All fees — both request_randomness and game_seed — accumulate in the round's FeeEscrow PDA and are distributed to validators after each round via distribute_fees + claim_validator_reward.
| Offset | Size | Field |
|---|---|---|
| 0 | 8 | Anchor discriminator |
| 8 | 8 | round (u64) |
| 16 | 8 | ee_v4_round_id (u64) |
| 24 | 8 | start_slot (u64) |
| 32 | 1 | aggregated (bool) |
| 33 | 8 | aggregated_slot (u64) |
| 41 | 32 | entropy_output ([u8; 32]) |
| 73 | 4 | pending_requests (u32) |
| 77 | 8 | total_fees (u64) |
| 85 | 1 | ee_v4_entropy_included (bool) |
| 86 | 1 | bump (u8) |
| Offset | Size | Field |
|---|---|---|
| 0 | 8 | Anchor discriminator |
| 8 | 8 | pending_fees (u64) |
| 16 | 8 | round (u64) |
| 24 | 8 | original_fees (u64) — total before crank cut (V4.5); used for per-validator 95% calc |
| 32 | 8 | ee_v4_round_id (u64) — EE V4 round that services this protocol round |
| 40 | 1 | fee_distributed (bool) — set by distribute_fees; required before claim_validator_reward |
| 41 | 1 | bump (u8) |
| Offset | Size | Field |
|---|---|---|
| 0 | 8 | Anchor discriminator |
| 8 | 32 | identity (Pubkey) |
| 40 | 32 | vote_account (Pubkey) |
| 72 | 32 | stake_account (Pubkey) |
| 104 | 8 | verified_stake (u64) — re-verified on each refresh |
| 112 | 8 | registered_slot (u64) |
| 120 | 8 | last_active_slot (u64) — updated on successful commit |
| 128 | 8 | last_round_participated (u64) |
| 136 | 1 | consecutive_misses (u8) — 3+ triggers deactivation |
| 137 | 1 | active (bool) |
| 138 | 1 | bump (u8) |
| Offset | Size | Field |
|---|---|---|
| 0 | 8 | Anchor discriminator |
| 8 | 32 | contributor (Pubkey) |
| 40 | 32 | ee_round (Pubkey) |
| 72 | 8 | protocol_round (u64) |
| 80 | 1 | claimed (bool) |
| 81 | 1 | bump (u8) |
| Offset | Size | Field |
|---|---|---|
| 0 | 8 | Anchor discriminator |
| 8 | 32 | dapp_id (Pubkey) |
| 40 | 32 | callback_program (Pubkey) |
| 72 | 8 | callback_instruction ([u8; 8]) |
| 80 | 8 | min_round_interval (u64) |
| 88 | 8 | last_served_round (u64) |
| 96 | 8 | total_requests (u64) |
| 104 | 32 | authority (Pubkey) |
| 136 | 8 | fee_override (u64) — 0 = protocol default |
| 144 | 1 | bump (u8) |
| Offset | Size | Field |
|---|---|---|
| 0 | 8 | Anchor discriminator |
| 8 | 32 | request_id ([u8; 32]) |
| 40 | 32 | requester (Pubkey) |
| 72 | 32 | seed ([u8; 32]) |
| 104 | 32 | callback_program (Pubkey) |
| 136 | 8 | callback_instruction ([u8; 8]) |
| 144 | 8 | round (u64) |
| 152 | 1 | fulfilled (bool) |
| 153 | 32 | output ([u8; 32]) |
| 185 | 8 | fee_paid (u64) |
| 193 | 8 | created_slot (u64) |
| 201 | 1 | bump (u8) |
| Offset | Size | Field |
|---|---|---|
| 0 | 8 | Anchor discriminator |
| 8 | 32 | coordinator (Pubkey) |
| 40 | 8 | round_id (u64) |
| 48 | 1 | n_contributors (u8) |
| 49 | 1 | m_threshold (u8) |
| 50 | 8 | commit_deadline (u64) |
| 58 | 8 | reveal_deadline (u64) |
| 66 | 8 | binding_slot (u64) |
| 74 | 1 | commit_count (u8) |
| 75 | 1 | reveal_count (u8) ← read by claim_validator_reward |
| 76 | 32 | entropy_accumulator ([u8; 32]) |
| 108 | 32 | entropy_output ([u8; 32]) ← extracted by wrapper |
| 140 | 1 | status (Finalized=2, Cancelled=3) ← verified by wrapper |
| 141 | 8 | slash_pool (u64) |
| 149 | 8 | finalized_slot (u64) |
| 157 | 1 | bump (u8) |
| 158 | 680 | contributors[10] |
Requires Solana platform-tools v1.52.
anchor build -- --tools-version v1.52IDL generation emits a version mismatch warning (anchor-lang 0.30.1 vs anchor-cli 0.31.0). The .so compiles correctly regardless.
If Cargo.lock is regenerated, re-pin these crates immediately:
cargo update -p "proc-macro-crate" --precise 3.2.0
cargo update -p blake3 --precise 1.7.0solana program deploy \
target/deploy/randomness_wrapper.so \
--keypair ~/.config/solana/x1randomness-key.json \
--program-id target/deploy/randomness_wrapper-keypair.json \
--url https://rpc.mainnet.x1.xyzCheck balance before deploying (~1.25–1.5 XNT per upgrade):
solana balance ~/.config/solana/x1randomness-key.json --url https://rpc.mainnet.x1.xyzEnd-to-end mainnet test suite (runs against live program, requires funded payer):
npm install # first time only
node tests/mainnet-e2e.jsTests 21 instructions in sequence. The EE V4 commit/reveal/finalize cycle waits ~4–5 minutes for the binding slot (automatic, with progress output).
cd keeper && npm install
# Register your validator (one-time)
VALIDATOR_KEYPAIR=/path/to/identity.json node validator-daemon.js --register
# Run your personal validator daemon (holds only your identity key)
VALIDATOR_KEYPAIR=/path/to/identity.json node validator-daemon.js --loop
# Optionally run the permissionless crank (anyone can run this)
node run-round.js --loopRequirements: ≥1,000 XNT delegated stake, active vote account voting within 500 slots.
Sometimes an EE V4 round gets stuck and can never complete. This happens in two situations:
Situation A — Not enough commits before the commit deadline.
The round opened, but fewer than 2 validators committed before the commit window closed (~200 slots / ~75 seconds after the round opened). The round is permanently stuck in CommitPhase and can never transition to RevealPhase.
Situation B — The slot hash expired.
The round completed commits and reveals, but nobody called finalize_via_ee for over 512 slots (~3.2 minutes) after the binding slot. The binding slot's hash has been pruned from the SlotHashes sysvar and finalization is permanently impossible.
In both cases the round must be cancelled directly on the EE V4 program by the round coordinator — the validator whose daemon called init_ee_round (they are listed as coordinator in the EE round account).
Run this to inspect the stuck round. Replace STUCK_EE_ID with the round number shown in your daemon logs:
node -e "
const { Connection, PublicKey } = require('@solana/web3.js');
const bs58 = require('bs58');
const conn = new Connection('https://rpc.mainnet.x1.xyz', 'confirmed');
const EE_V4 = new PublicKey('FDyWtM9UBNfXNuc5oZJ1V86d3dz635WnqMfX8x5Uifbm');
const STUCK_EE_ID = 394782; // ← change this to the stuck round number
async function main() {
function u64le(n) { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(n)); return b; }
const accts = await conn.getProgramAccounts(EE_V4, {
filters: [{ dataSize: 838 }, { memcmp: { offset: 40, bytes: bs58.encode(u64le(STUCK_EE_ID)) } }]
});
if (!accts.length) { console.log('Round not found — already cancelled or wrong ID'); return; }
const d = accts[0].account.data;
const coordinator = new PublicKey(d.slice(8, 40)).toBase58();
const status = d[140]; // 0=CommitPhase, 1=RevealPhase, 2=Finalized, 3=Cancelled
const commits = d[74];
const reveals = d[75];
const bindingSlot = Number(d.readBigUInt64LE(66));
const slot = await conn.getSlot();
const statuses = ['CommitPhase','RevealPhase','Finalized','Cancelled'];
console.log('EE round account :', accts[0].pubkey.toBase58());
console.log('Coordinator :', coordinator);
console.log('Status :', statuses[status] || status);
console.log('Commits / reveals:', commits, '/', reveals);
console.log('Binding slot :', bindingSlot, ' Current slot:', slot);
console.log('Slot hash expired:', slot > bindingSlot + 512 ? 'YES — cancel needed' : 'No');
}
main().catch(console.error);
"If the Coordinator line matches your validator identity pubkey, you are the one who must cancel. If it is a different validator, ask them to run the cancel script instead.
Open keeper/cancel-ee-round.js in a text editor and change the TARGET_EE_ID near the top to the stuck round number:
const TARGET_EE_ID = 394782n; // ← change this numberSave the file, then run:
cd ~/x1-randomness-protocol/keeper
VALIDATOR_KEYPAIR=~/.config/solana/identity.json node cancel-ee-round.jsThe script will:
- Find the EE round account on-chain and confirm the status is
CommitPhase(0). - Verify that your keypair matches the coordinator address — it will refuse to run if you are not the coordinator.
- Collect the pubkeys of any validators who committed (so the EE program can refund their 0.01 XNT stake).
- Send the
cancel_roundinstruction directly to the EE V4 program. - Print a confirmation transaction signature.
Expected output:
Looking for EE round 394782…
EE round: <pubkey>
Coordinator: <your pubkey>
Status: 0 (0=CommitPhase, 2=Finalized, 3=Cancelled)
Commits: 1
Contributors to refund: [<pubkey>]
Sending cancel_round…
✓ cancel_round: <signature>
After cancellation your daemon will see status == 3 (Cancelled) and automatically open the next EE round:
# The daemon handles this automatically in --loop mode.
# If you stopped it, restart it:
VALIDATOR_KEYPAIR=~/.config/solana/identity.json node validator-daemon.js --loopcancel_round only works on rounds still in CommitPhase (status = 0). A round that reached RevealPhase (status = 1) cannot be cancelled — it will either:
- Complete normally if reveals arrive before
reveal_deadline(~600 slots / ~3.75 minutes after round open) - Expire if reveals do not arrive in time. In this case the EE program marks it
Cancelledautomatically and your daemon opens the next round.
If a reveal-phase round is about to expire and you want to speed things up, call finalize_via_ee directly via the crank (node run-round.js without --loop will run one full cycle and finalize whatever is ready).
If a randomness request was made during a round that gets cancelled, the requester can recover their fee by calling refund_request on the wrapper program. Their request is not lost — they simply re-submit it in the next round.
All four CPI instructions enforce ee_v4_program.key() == ENTROPY_ENGINE_V4. Passing a stub program is rejected.
finalize_via_ee and aggregate_from_ee require ee_round.owner == &ENTROPY_ENGINE_V4. A crafted account at an arbitrary address cannot inject entropy.
init_ee_round requires ee_round_id == protocol_config.ee_v4_round_id + 1. ID jumping and round hijacking are rejected.
n_contributors, m_threshold, and binding_slot in init_ee_round are derived from protocol constants — no caller can override committee size, threshold, or timing.
commit_via_ee enforces entropy-derived eligibility — no external actor decides who participates. Selection is deterministic from pool entropy.
Both finalize_via_ee and aggregate_from_ee read the current slot hash from the SlotHashes sysvar. Slot hashes are not knowable before the slot completes, resisting output pre-computation.
request_randomness output = SHA256(pool_entropy ‖ request_id ‖ slot_hash). The slot hash at inclusion is unknown at submission, so outputs cannot be pre-computed even with known pool entropy.
distribute_fees sets fee_distributed = true and rejects re-entry. claim_validator_reward requires fee_distributed == true. original_fees is recorded at distribution time so per-validator shares are correct even as pending_fees decreases.
claim_validator_reward sets ValidatorReveal.claimed = true and rejects if already set.
refund_request requires fee_escrow.ee_v4_round_id != 0 (escrow must be linked to an EE round by aggregate_from_ee before refunds are allowed) and verifies the passed EE round's stored ID matches that field. Requesters cannot drain escrow using a pre-linked or different round's cancelled EE account.
claim_validator_reward reads the EE round ID at offset 40 from the passed EE round account and requires it equals fee_escrow.ee_v4_round_id. Prevents claiming from an escrow using an unrelated EE round to inflate or deflate reveal_count.
finalize_via_ee explicitly checks ee_data[140] == 2 (Finalized) after the CPI completes, in addition to CPI success. Guarantees the entropy output is from a legitimately finalized round.
If an EE V4 round is cancelled (status byte 140 == 3), refund_request lets requesters recover their fee. Validators who miss reveals forfeit their 0.01 XNT stake to the EE V4 slash pool.
request_randomness routes to the queue path (rather than failing) when pool entropy is older than STALENESS_HARD_LIMIT_SLOTS (21,600 slots ≈ 2.25 hours). The keepers' idle gate matches this threshold — they hold off opening a new EE round until the pool is stale OR a pending request appears, keeping costs low during quiet periods.
distribute_fees no longer sends any share to an insurance fund. 100% of fees go to protocol participants: 5% crank, 95% validators. claim_validator_fees (dust sweep) sends residual lamports to the protocol_config.authority wallet. The insurance_fund field in ProtocolConfig is retained for layout compatibility but is no longer used.
Program (deployed 2026-05-20, tx 2Fev2T9Y8EHN9eXkuHULB1nNwb84dFjYjMHvjrrsbxc8CGW2kPwMVfXj7pZaVzX8J2DoC9NVftzcdLR1CyndfVnG):
- Remove insurance fund —
insurance_fundaccount removed fromDistributeFeesstruct.distribute_feesnow pays 5% to crank, 95% stays in FeeEscrow for validators (was 5% crank + 5% insurance + 90% validators). claim_validator_rewardupdated — validator share formula changed fromoriginal_fees × 90%→original_fees × 95%.claim_validator_feesdust sweep — recipient changed frominsurance_fund→protocol_config.authority.
Keeper (run-round.js, validator-daemon.js):
ixDistributeFeesdrops from 6 to 5 accounts (no moreinsurance_fund).ixClaimRewardremoves unusedinsuranceFundparameter.
Frontend:
FEE_VALIDATORS_PCT = 95,FEE_INSURANCE_PCT = 0inconstants.ts.- Fee split updated to "95% validators / 5% crank" across all pages.
/requestpage: new "Request History by Address" panel — enter any wallet address to see total requests and fulfilled count. Connected wallet stats shown automatically.protocol.ts: newgetRequestsByRequester(requester)method usinggetProgramAccountswith memcmp on offset 40 (requester pubkey).
Security audit (V4.5): Full review found no critical vulnerabilities. Arithmetic is safe (checked_* throughout). Fee distribution is one-shot (fee_distributed flag). Validator eligibility is entropy-derived and manipulation-resistant. Slot-hash mixing prevents pre-computation. No reentrancy. Pre-existing functional note: queue-path RequestState requests have no on-chain fulfill instruction (pool is kept warm by keepers to avoid this path).
Program (deployed 2026-05-19, tx 2HHE1kpjbCaAGLyuKzf6maNt9MucCyrenjQU8efkinsekTdR2MJhp5geLKrBe5PqB2rrSfuuV7RVCN3nwELs5H4x):
- Crank reward —
distribute_feesnow pays 5% of round fees immediately to the caller (crank: Signeraccount). Insurance fund share reduced from 10% → 5%; validator share unchanged at 90%. (Superseded by V4.5: insurance removed entirely, validators now get 95%.) - dApp request counters —
request_randomnessnow incrementsDappRegistration.total_requestsand updateslast_served_roundwhen the dApp account is passed as writable. Previously these fields were never written. Pass the dApp registration asisWritable: trueto enable tracking.
Frontend:
- Fee split updated to 90% / 5% / 5% across all pages (home, request, dapps, rounds, docs, validators).
FEE_CRANK_PCT = 5added toconstants.ts.
Crank JS (run-round.js):
ixDistributeFeesnow includespayerasisSigner: true, isWritable: trueto receive the 5% crank reward.
Program (deployed 2026-05-19, tx CQy7shPspPrnUj5N1bdp1F1b3JGmY2Zo7MN5SJov8pA9YZ37hp8bHxUW1rm4E2VhdUsYWCashzpZgGUpTGgx8CX):
total_game_seedscounter —EntropyPoolgains au64counter at offset 67 (INIT_SPACE 67→75). Incremented on everygame_seedcall.migrate_entropy_poolinstruction — permissionless, idempotent one-shot migration that expands existing 67-byteEntropyPoolaccounts to 75 bytes. Required after the struct change because Anchor deserializes before running thereallocconstraint, causingAccountDidNotDeserializeon allgame_seedcalls until migration runs. Migration executed on mainnet:3JGVn5q9gKyPQt4P7gapANrvwYgUesmBNGfwsjFg6RY1jxNE3AtAwAqyt4fqps3FE4r5JUGmzySC9yqCnBxa1Cv6
Frontend:
- Dashboard shows
total_game_seedsalongsidetotal_requests_served. STALENESS_HARD_LIMIT_SLOTSconstant corrected to 21,600 in all three pages that used it (was hardcoded 1,500 inrequest/page.tsxandvalidators/page.tsx, causing false "Pool Stale" and "Offline" displays).- Validator status now uses
v.active(on-chain field) notlastActiveSlot > 500as the online/offline signal. - Game seed preimage docs corrected:
SHA256(pool_entropy ‖ game_id ‖ payer ‖ slot_hash).
Program (deployed 2026-05-18, tx ppA3RBfKsLuv3oscEnM5sjRap1QBXWKxTzgiV8JjUDEGyvvgyt85qMKKbb3Agjqw2ryp5jYcRPyyhjnoXyZrNz3):
- C-1: Explicit finalization status check —
finalize_via_eenow explicitly checksee_data[140] == 2after CPI, removing reliance on all-zero entropy as a finalization proxy. - C-2: Refund pre-link guard —
refund_requestnow requiresfee_escrow.ee_v4_round_id != 0; refunds are blocked untilaggregate_from_eehas linked the escrow to its EE round. - H-2: Reward EE-round binding —
claim_validator_rewardnow reads the EE round's stored ID and requires it equalsfee_escrow.ee_v4_round_id, preventing claims using an unrelated EE round account.
Validator daemon:
- H-4: Commit idempotency — removed
alreadyCommittedflag. Daemon always re-attempts the commit transaction (on-chain idempotent); previously a dropped network tx left secrets on disk but the commit never landed, silently causing the reveal to fail later. - M-4: Spurious account removed —
ixClaimRewardno longer passes a redundantSystemProgram.programId; account list now matchesClaimValidatorRewardexactly. - M-9: Log corrected — log after
init_ee_roundnow saysn=2(wasn=10, an old constant).
Frontend:
- C-2:
getProgramAccountsfilter encoding — all threememcmp.bytesdiscriminator filters inprotocol.tsused base64 encoding; Solana RPC requires base58. Validators and dApps pages were returning empty or unfiltered results. - M-2: Connection churn — dashboard
ProtocolClientmoved intouseState; previously re-instantiated (new WebSocket) on every 3-second poll render. - M-8: Fee escrow explorer link — broken placeholder link in
rounds/page.tsxreplaced with a working link to the actual escrow PDA on X1 Explorer. - Docs SDK example —
wrapperRoundPdaindocs/page.tsxcorrected toisWritable: false.
Earlier V4.3 changes (2026-05-17) — idle gate + account list fixes:
- Idle gate — crank and daemon check before opening any new EE round: if the entropy pool is warm (< 21,600 slots stale) and no unfulfilled
RequestStateaccounts exist, they idle. Rounds resume automatically when pool goes stale or a request appears. Reduces crank cost from ~43 → ~3 XNT/month. STALENESS_HARD_LIMIT_SLOTSraised — 1,500 → 21,600 slots (~2.25 hours) to match the idle gate.request_randomnessaccount list corrected —slot_hashessysvar and optionaldapp_registrationadded at positions 6 and 7.game_seedaccount list corrected —slot_hashessysvar added at position 4.
- C-1: Fee bypass closed —
request_randomnessnow verifies thedapp_infoaccount is owned by this program and passes theDappRegistrationdiscriminator before reading thefee_overridefield. A hand-crafted 1-lamport account could previously bypass the fee entirely. - C-2: Historical-round deactivation attack closed —
mark_validator_missednow checks that the EE round'sbinding_slot − EE_V4_MIN_BINDING_SLOTSis later than the validator'sregistered_slot. Previously, 3 old finalized/cancelled rounds could instantly deactivate any newly registered validator. - H-1: Borrowed-credentials attack closed —
register_validatornow readsnode_pubkeyat offset 4 of the VoteState account and requires it equals the signing identity. Previously a validator could register using someone else's high-stake vote + stake accounts to pass liveness and stake checks. - H-2: Premature round advance closed —
advance_roundnow requires the current protocol round'sWrapperRound.aggregated == truebefore creating the next round. Previously any permissionless caller could advance mid-EE-round, stranding the in-flight EE round with no protocol WrapperRound to aggregate into. - M-1: Same-round callback spam closed —
deliver_callbacknow enforcesmin_round_interval.max(1), so even subscriptions registered withinterval=0cannot fire twice in the same round. - M-3: Cross-round refund protection —
aggregate_from_eenow stampsfee_escrow.ee_v4_round_idwith the resolved EE round ID.refund_requestalready validated this field; it now always matches the round that actually serviced the escrow. - M-4: Node-pubkey identity check at commit time — both
init_ee_roundandcommit_via_eenow verify the coordinator/contributor owns the supplied vote account (samenode_pubkeycheck asregister_validator). Prevents impersonating another validator's identity at EE round time. - M-2: Secrets file permissions —
validator-daemon.jssaveSecrets()now writes withmode: 0o600; previously the ephemeral commit secret was world-readable. - L-5: Transaction retry —
validator-daemon.jssend()now retries up to 3 times with a fresh blockhash on each attempt. - Account list updates —
advance_roundnow requirescurrent_wrapper_round(position 2, read-only);aggregate_from_eenow requiresfee_escrow(position 3, writable).run-round.js,validator-daemon.js, andtests/mainnet-e2e.jsupdated accordingly.
n_contributors = 2(was 10) —init_ee_roundnow passesn=MIN_EE_M_THRESHOLD(2)instead ofMAX_COMMITTEE_SIZE(10). With only 2 validators, n=10 meantcommit_countnever reachedn_contributorsand rounds were permanently stuck in CommitPhase.- Commit/reveal phase gating fixed — validator daemon now uses
commit_deadline(EE round offset 50, ~init+200 slots) to split commit and reveal phases. Previously usedbinding_slot(offset 66, ~init+675 slots) which is forfinalize_via_ee, not reveals — reveals were always sent after the reveal window closed, causing WrongPhase errors. - Reveal deadline — reveals must arrive before
reveal_deadline(EE round offset 58, ~init+600 slots / ~3.75 min).binding_slot(offset 66, ~init+675 slots) only gatesfinalize_via_ee. - Expired slot hash detection — if
current_slot > binding_slot + 512, the EE round's binding slot hash has been pruned from SlotHashes and finalization is permanently impossible. Daemon detects this, logs the stuck round, and opens the next EE round. - Crank EE round ID alignment — crank now finalizes
eeV4RoundId(the round validators just opened) rather thaneeV4RoundId+1, fixing a one-round-ahead offset that caused ConstraintSeeds onfinalize_via_ee. - next-round gating — daemon only calls
init_ee_round(nextId)after the current round is finalized or cancelled (status == 2 || 3), preventing config advancement before the crank can finalize.
- Permissionless
init_ee_round— any registered active validator can start the next EE V4 round. n/m/binding_slot are now protocol constants, not caller args. Protected by sequential ID requirement. - On-chain validator selection —
commit_via_eenow enforces entropy-derived eligibility viaCOMMIT_SELECTION_THRESHOLD. No keeper can control who commits. - Keeper rewritten as pure crank —
run-round.jsholds only its own key, has zero protocol authority. Removed--key2/--key3flags entirely. - Validator daemon — new
validator-daemon.jsfor each validator to run independently with their own identity key. - Premium fee tier — 0.05 XNT/request set by protocol authority via
update_dapp_fee; standard 0.01 XNT remains default. claim_validator_feesdust sweep — recipient changed fromauthoritytoinsurance_fund.
- Per-validator fee rewards via
ValidatorRevealPDAs andclaim_validator_reward. - Game seed fee (0.001 XNT) now flows to validators via FeeEscrow.
- Per-dApp fee override via
update_dapp_fee. set_feeinstruction for protocol authority.- Liveness protection:
refund_requestfor cancelled rounds. - FeeEscrow grew from 26 → 34 bytes (
original_feesadded). Later extended to 42 bytes in V4 (ee_v4_round_idadded). - Validator registration system:
register_validator,deregister_validator,refresh_validator_status.
- [CRITICAL]
ee_v4_programaddress not validated — fixed. - [CRITICAL]
ee_roundownership unchecked infinalize_via_ee— fixed. - [CRITICAL]
distribute_feesre-enterable — fixed withfee_distributedflag. - [HIGH] Slot mixing used
hash(slot_number)— replaced with SlotHashes sysvar. - [HIGH]
verify_entropycreated receipts for arbitrary request IDs — fixed. - [MEDIUM] Stale entropy served indefinitely —
STALENESS_HARD_LIMIT_SLOTSadded.
- Fixed
create_fee_escrow— FeeEscrow PDA was never initialized. - Fixed
RequestState::INIT_SPACE— was 8 bytes too small. - Fixed
init_ee_round—WrapperRound.roundnow storesee_round_id. - Fixed
aggregate_from_ee— now accepts protocol WrapperRound.
- Delegated commit/reveal/finalize to EE V4 via CPI.
- Added
WrapperRound,aggregate_from_ee,claim_validator_fees.
- Original single-program architecture with inline committee management.
MIT