증명 가능한 공정성(Provably Fair)은 코드에서 어떻게 구현되나요?
게임이 종료되어 해시되지 않은 서버 시드, 클라이언트 시드, 논스를 모두 확인할 수 있다고 가정할 때, 작동 방식은 다음과 같습니다.
게임 결과를 생성하려면 크게 세 단계를 거칩니다.
byteGenerator(무작위 바이트 생성)
generateFloats(바이트를 부동소수점 수(숫자)로 변환)
부동소수점 수를 게임 이벤트로 변환(부동소수점 수를 Shuffle Original 게임의 실제 이벤트로 변환)
ByteGenerator 함수: 무작위 바이트 생성기
byteGenerator 함수는 무작위 바이트 생성기 역할을 합니다.
이 함수는 clientSeed, serverSeed, nonce, cursor의 고유한 값을 입력받아, 암호화 함수인 HMAC_SHA256을 통해 무작위의 고유한 SHA-256 해시 값을 생성합니다.
생성된 SHA-256 값의 크기는 32바이트입니다. 게임 결과의 충분한 무작위성과 연산 부담 사이의 균형을 맞추기 위해, 32바이트를 4바이트* 단위의 8개 구간으로 나누어 각 게임 결과를 생성합니다.
8개를 초과하는 게임 결과가 필요한 일부 게임에서는 cursor를 사용합니다. 커서는 0에서 시작하며, 필요한 결과 수를 충족할 때까지 1, 2, 3, 4로 증가합니다.
필요한 무작위 결과가 8개 이하인 게임에서는 커서 값이 증가하지 않습니다.
*4바이트 데이터로는 2^32(4,294,967,296)가지 결과가 나올 수 있으며, 이는 무작위성을 확보하기에 충분히 큰 범위입니다.
function* byteGenerator({ serverSeed, clientSeed, nonce, cursor }: ByteGeneratorInterface) {
// Setup cursor variables let currentRound = Math.floor(cursor / 32);
let currentRoundCursor = cursor;
currentRoundCursor -= currentRound * 32;
// Generate outputs until cursor requirement fullfilled
while (true) {
// HMAC function used to output provided inputs into bytes
const hmac = crypto.createHmac('sha256', serverSeed);
hmac.update(`${clientSeed}:${nonce}:${currentRound}`);
const buffer = hmac.digest();
// Update curser for next iteration of loop
while (currentRoundCursor < 32) {
yield Number(buffer[currentRoundCursor]);
currentRoundCursor += 1;
}
currentRoundCursor = 0;
currentRound += 1;
}
}generateFloats 함수: 바이트를 부동소수점 수로 변환
이 함수는 SHA-256 16진수 값을 바이트에서 부동소수점 수로 변환하며, 변환된 값은 이후 게임 이벤트 계산에 사용됩니다.
아래는 SHA-256 16진수 값이 바이트에서 부동소수점 수로 변환되는 과정을 보여 줍니다. 최종 출력값인 numArr에는 게임에 필요한 모든 결과가 담깁니다. 결과가 1개만 필요한 경우 목록에는 값이 하나만 들어갑니다.
이 함수는 0~1 사이의 숫자로 이루어진 배열을 반환하며, count는 반환되는 배열의 요소 개수를 나타냅니다.
코드:
// Convert the hash output from the rng byteGenerator to floats
export function generateFloats({
serverSeed,
clientSeed,
nonce,
cursor,
count,
}: GenerateFloatsInterface) {
// Random number generator function
const rng = byteGenerator({ serverSeed, clientSeed, nonce, cursor });
// Declare bytes as empty array
const bytes = [];
// Populate bytes array with sets of 4 from RNG output
while (bytes.length < count * 4) {
bytes.push(rng.next().value!);
}
// Return bytes as floats using lodash reduce function
const numArr = chunk(bytes, 4).map(bytesChunk =>
bytesChunk.reduce((result, value, i) => {
const divider = 256 ** (i + 1);
const partialResult = value / divider;
return result + partialResult;
}, 0),
);
return numArr;
}
모든 Shuffle Original 게임은 ByteGenerator 및 generateFloats 함수를 모두 사용하여 0~1 사이의 무작위 부동소수점 수를 생성합니다. 다만 그 이후부터는 생성된 부동소수점 수로 게임 이벤트를 결정하는 절차가 게임마다 다릅니다.
게임별 고유 절차는 게임 이벤트에서 자세히 설명합니다.
예시로 살펴보기
다음은 입력값을 사용해 다이스 게임 이벤트를 생성하는 과정을 보여 주는 예시입니다.
Input Values: Given some random input values
serverSeed, clientSeed, nonce, cursor
Step 1: byteGenerator creates a SHA-256 byte
"a3f4e0ac7c7e8e9b5f16106c6b1d14e87c2c5a8d59b1d1c6a0b5f3e5a7d4c9a8”
Step 2: 256 bytes is split into 8 equal set of 32 bytes
"a3f4e0ac”, “7c7e8e9b”, “5f16106c”, “6b1d14e8”,
“7c2c5a8d”, “59b1d1c6”, “a0b5f3e5”, “a7d4c9a8”
Step 3: Each set is broken down into 2 bytes each
(only first 2 sets is shown)
set 1: a3-f4-e0-ac
set 2: 7c-7e-8e-9b
......
Step 4: Each of the 2 bytes represent a number from 0 to 255
set 1: [163, 244, 224, 172]
set 2: [124, 126, 142, 155]
......
Step 5: Using the formula in the code to generate numArr
Float 1 = (163 / (256^1)) + (244 / (256^2)) + (224 / (256^3))
+ (172 / (256^4)) = 0.64045528601
Float 2 = (124 / (256^1)) + (126 / (256^2)) + (142 / (256^3))
+ (155 / (256^4)) = 0.48630610737 Float 3 ......
Step 6: Output the floats in a list
numArr = [0.64045528601, 0.48630610737, ...... ]
*** Game Event Generation ***
Step 7: The numArr output will be used to generate game events,
depending on the game requirement
Example 7: Using dice as an example.
The game event is generated using the first numArr value.
const resultValue = floats.map(val => Math.floor(floats * 10001) / 100);
resultValue = Math.floor(0.64045528601 * 10001) / 100
= 64.05
참고: 실제로 게임이 진행 중일 때는 서버 시드가 해시된 상태이므로, 플레이어와 운영자 모두 게임 도중에는 이 과정의 개별 출력값을 확인할 수 없습니다.
게임 중과 게임 후에 동일한 알고리즘과 함수가 사용되므로, 사용자는 동일한 입력값으로 언제든지 결과를 직접 검증할 수 있습니다.
