케노에서 프로비저블리 페어가 작동하는 방식
Cloudbet에서 케노를 플레이하는 방법과 모든 추첨이 프로비저블리 페어 방식임을 확인하는 방법을 소개합니다.
케노 플레이 방법
케노는 숫자를 추첨하는 게임입니다. 보드에서 원하는 숫자를 선택하면 게임이 자체적으로 숫자를 추첨합니다. 선택한 숫자가 추첨된 숫자와 많이 일치할수록 더 큰 당첨금을 받을 수 있으며, 당첨금의 규모는 처음에 몇 개의 숫자를 선택했는지에 따라 달라집니다. 숫자를 선택하고 베팅을 하면 즉시 추첨이 진행되고 적중 여부가 바로 공개됩니다.
Cloudbet Originals 케노
Cloudbet의 자체 Originals 케노는 40개의 숫자로 구성된 보드를 사용합니다. 1개에서 10개까지 숫자를 선택할 수 있으며, 게임은 10개의 숫자를 추첨합니다. RTP는 99%이며 최소 $0.01부터 베팅할 수 있습니다. 선택한 숫자가 적을수록 변동성이 높아지고 적중당 당첨금이 커지며, 숫자를 많이 선택할수록 당첨 빈도는 높아지지만 적중당 수익은 낮아집니다. Auto Bet 기능으로 여러 라운드를 자동으로 진행할 수 있습니다.
케노의 추첨은 다른 모든 Cloudbet Original과 동일한 방식으로 검증 가능하게 생성됩니다. Server Seed, Client Seed, Nonce가 결합되어 Round Signature를 생성하고, 이후 SHAKE256을 통해 난수 스트림이 만들어지는 과정은 프로비저블리 페어 개요에서 확인하세요. 이 페이지에서는 케노만의 특징, 즉 이 난수 시퀀스가 실제 추첨 숫자로 변환되는 과정을 다룹니다.
난수를 케노 추첨 숫자로 변환하는 과정
케노 보드는 1부터 40까지 번호가 매겨져 있습니다. 이 40개의 숫자 중 10개가 중복 없이 한 번에 하나씩 추첨되어야 합니다.

아직 추첨되지 않고 남아 있는 숫자의 개수를 P라고 부릅니다. P는 40에서 시작하며 숫자가 하나 추첨될 때마다 1씩 감소합니다.

10번의 추첨마다 SHAKE256 출력값에서 0부터 4,294,967,295 사이의 무작위 4바이트(32비트) 숫자가 하나씩 추출됩니다. 남아 있는 P개의 숫자 중에서 공정하게 하나를 선택하기 위해 리젝션 샘플링(rejection sampling)을 적용한 뒤(리젝션 샘플링 가이드 참고), 그 결과를 P로 나눈 나머지를 구해 다음 추첨 숫자의 위치를 정합니다. 추첨할 숫자가 더 남아 있다면 선택된 숫자는 후보군에서 제외되고 P가 1 감소하며, 이 과정이 반복됩니다.

이 과정은 10개의 숫자가 모두 추첨될 때까지 계속됩니다.


게임 진행 방식
10개의 추첨 숫자 전체는 숫자를 선택하기 전에 이미 서버에서 생성되어 숨겨져 있습니다. 이미 공유된 Commitment 덕분에 이후에 몰래 변경되거나 조작될 수 없습니다. 라운드가 종료되면 선택한 숫자를 이 사전 추첨 결과와 대조하기만 하면 되며, 적중한 숫자 개수에 따라 당첨금이 계산됩니다.
Cloudbet 키노 결과를 직접 검증하는 코드
Server Seed, Client Seed, Nonce를 입력하여 직접 결과를 검증해 보세요. 필요한 단 하나의 종속성을 설치한 뒤 직접 실행하여 게임 결과를 재현할 수 있습니다.
참고: Cloudbet에서 종료된 게임의 Server Seed를 확인하려면 이미 새 라운드를 시작한 상태여야 합니다(새 라운드를 시작하면 새로운 시드 쌍으로 전환됩니다).
종속성:
npm install js-sha3
코드:
// If you are running this yourself, then you can input your own data here
// You can then compare:
// 1) Cloudbet's provided Commitment to the Commitment outputted here. This proves that the Server Seed was not altered during gameplay.
// 2) Cloudbet's provided mine positions to the mine positions outputted here. This proves that your game's outcome was determined solely by the pre-game seeds and was not altered afterwards.
let myServerSeed = 'your_server_seed'; // ✍️YOUR_INPUT✍️: Enter your (unhashed) Server Seed here
let myClientSeed = 'your_client_seed'; // ✍️YOUR_INPUT✍️: Enter your Client Seed here
let myNonce = 1; // ✍️YOUR_INPUT✍️: Enter your Nonce here
const usingCustomData = false; // Change this to true if you are using your own data
// Load libraries
const crypto = require('crypto');
const { sha3_256, shake256 } = require('js-sha3');
// Step 1: Generate a Server Seed and Commitment
// And generate a Client Seed - either by player input or by the player's browser
function generateServerSeed() {
let seed = crypto.randomBytes(32).toString('hex');
if (usingCustomData) { seed = myServerSeed; }
const commitment = sha3_256(seed);
return { 'serverSeed': seed, 'commitment': commitment };
}
function getClientSeed() {
let seed = crypto.randomBytes(32).toString('hex');
if (usingCustomData) { seed = myClientSeed; }
return seed;
}
// Step 2: Create a Round Signature with SHA3 using a Server Seed, Client Seed and Nonce
function createRoundSignature(serverSeed, clientSeed, nonce) {
return sha3_256(`${serverSeed}:${clientSeed}:${nonce}`);
}
// Step 3: Create a SHAKE256 stream generator from a Round Signature.
// To simplify this example program, this function doesn't create a stream, but instead a sequence of custom length.
function* createRandomNumberSequence(numbersToGenerate, roundSignature) {
const numOfBytes = 4 * numbersToGenerate; // 4 bytes (32 bits) per random number we generate
const byteStream = shake256.create(8 * numOfBytes); // shake takes bits as input
byteStream.update(roundSignature); // Use the Round Signature to seed the generator
const buf = Buffer.from(byteStream.digest()); // The Buffer class lets us package every 4 bytes into one 32-bit integer
for (let i = 0; i < numbersToGenerate; i++) {
yield buf.readUInt32BE(i * 4); // Read buf in 4-byte steps
}
}
// Step 4: Pick a number of unique positions
function pickUniquePositions(numOfPicks, totalPositions, rng) {
const available = Array.from({ length: totalPositions }, (_, i) => i);
const picks = [];
for (let i = 0; i < numOfPicks; ++i) {
const index = sample(available.length, rng);
picks.push(available[index]);
available.splice(index, 1); // Remove chosen pump from list of non-faulty pumps
}
return picks;
}
// Rejection sampling for fair results
function sample(P, rng) {
const maxAcceptable = Math.floor(0x100000000 / P) * P;
// Rejection sampling
let rand;
do {
rand = rng.next().value;
} while (rand >= maxAcceptable);
// Modulo operation
const sampled_number = rand % P;
return sampled_number;
}
// Example usage
(function main() {
let nonce = 1;
if (usingCustomData) { nonce = myNonce; }
const numOfHits = 10;
const totalPositions = 40;
const { serverSeed, commitment } = generateServerSeed();
const clientSeed = getClientSeed();
const roundSignature = createRoundSignature(serverSeed, clientSeed, nonce);
const numbersToGenerate = totalPositions * 2; // We need at least one random number per position, but there is an improbable possibility we'll need two
const rng = createRandomNumberSequence(numbersToGenerate, roundSignature);
let hits = pickUniquePositions(numOfHits, totalPositions, rng);
hits = hits.map((value)=> value + 1 );
hits.sort((a, b)=>a-b);
console.log('📝 Commitment (shown before the game):', commitment);
console.log('🔒 Server Seed (revealed after the game):', serverSeed);
console.log('🎲 Client Seed:', clientSeed);
console.log('🔢 Nonce:', nonce);
console.log('🔁 Round Signature (can be verified after the game):', roundSignature);
console.log(`🎯 Hits:`, hits);
})();
