함정
해시는 32바이트입니다. 게임은 0과 1 사이의 숫자가 필요합니다. 명백한 방법은 일부 바이트를 정수로 읽고 가능한 최대값으로 나누는 것이며, provably fair 설명자를 포함한 모든 것이 그렇게 설명합니다. 명백한 방법에는 두 개의 다른 프로그램이 이를 수행할 때만 나타나는 문제가 있습니다.
서버와 브라우저 모두 일반적인 숫자를 IEEE 754 배정밀도로 저장합니다: 64비트이고 그 중 53비트가 정밀도를 가집니다. 해시의 7바이트는 56비트, 8바이트는 64비트이고 둘 다 맞지 않습니다. 따라서 변환은 비트를 손실해야 하며, 문제는 반올림 여부가 아니라 몇 번 반올림하는지입니다. 한 번에 한 바이트씩 누적하고 실행 합계에 256을 곱한 후 다음 바이트를 더하는 루프는 합계가 2⁵³를 통과하면 매 단계마다 반올림됩니다. Kotlin의 64비트 정수에서 배정밀도로의 변환은 정확히 한 번만 반올림됩니다. 동일한 값의 두 번의 반올림이 항상 한 번의 반올림과 동일한 배정밀도로 끝나지는 않습니다.
대부분의 경우 아무도 알아차리지 못합니다. 왜냐하면 차이가 16개의 유효 숫자를 가진 숫자의 마지막 비트에 있기 때문입니다. 그러면 게임은 숫자에 37, 52, 또는 1,000,000을 곱하고 정수로 반올림하면, 마지막 비트는 정확히 36.9999999999999가 36이 되는지 37이 되는지를 결정하는 비트입니다. 명백한 방식으로 빌드된 검증자는 완벽하게 정직한 라운드의 일부를 거부할 것이고, 거부를 본 플레이어는 정직한 반올림 오류와 부정직한 서버를 구별할 수 있는 방법이 없을 것입니다.
엔진이 이를 피하는 방법
해결책은 브라우저가 서버와 동일한 횟수로 반올림하도록 하는 것입니다. 즉 한 번입니다. 엔진은 처음 4바이트를 정수로 읽으므로 배정밀도에 정확히 맞고, 다음 3~4바이트를 다른 정수로 읽으므로 역시 정확히 맞습니다. 각각을 2의 거듭제곱으로 나누는데, 이는 배정밀도에서 정확한 연산입니다. 그런 다음 둘을 더합니다. 이 단 하나의 덧셈이 유일한 정밀도 손실 지점이며, Kotlin의 단일 변환과 동일한 방식으로 손실합니다.
ENGINE-VERIFIED_shared/rng.ts u56: "처음 7바이트 → uniform [0,1), 서버의 Long→Double 반올림을 비트 단위로 일치: hi/2^32와 lo/2^56 모두 정확한 dyadic 배정밀도이므로, 한 번의 IEEE 덧셈은 56비트 값을 정확히 한 번만 반올림합니다 — Kotlin의 toDouble()이 수행하는 동일한 단일 반올림입니다. (배정밀도의 순진한 56비트 누적은 2^53을 지나서 반복적으로 반올림할 수 있으며 floor 끝에서 다를 수 있습니다.)" u64는 8바이트에 대해 동일한 구조를 사용하며, "서버의 ULong→Double 변환을 LimboService.outcome100에서 일치"시키고, 모든 seed-pair 원본 임포트가 사용하는 함수입니다."
| 함수 | 읽은 바이트 | 구성 | 일치 |
|---|---|---|---|
| u56 | 처음 7 | hi ÷ 2³² + lo ÷ 2⁵⁶, 한 번의 덧셈 | 서버의 Long → Double 변환 |
| u64 | 처음 8 | hi ÷ 2³² + lo ÷ 2⁶⁴, 한 번의 덧셈 | 서버의 ULong → Double 변환 (Limbo) |
| h52 / bust100 | 처음 6.5 (13진 16진수 자리) | 정확한 BigInt 정수 연산, double 없음 | 서버의 CrashDerivation.crashPoint100 |
모든 seed-pair 원본 (bingo, blackjack, hold'em, koban, omikuji, sic bo, video poker, hanabi, fukubukuro, roulette)은 rng.ts에서 u64를 가져오며; 생성기와 검증기는 동일한 import입니다.
Crash는 인수를 결론까지 가져갑니다. 이 crash point는 bustabit 공식을 따릅니다: floor((100 × 2⁵² − h) ÷ (2⁵² − h)). 여기서 h는 해시의 처음 52비트이고, 그 나눗셈은 double이 1만큼 벗어날 수 있는 경계에 정확히 위치합니다. 따라서 엔진은 double을 사용하지 않습니다. 서버가 하는 방식대로, 브라우저에서 자의적 정밀도 정수로 공식을 계산하며, 소스의 주석이 한 줄로 이유를 설명합니다: float 근사는 floor 경계에서 정수 나눗셈과 다를 수 있고 검증기가 좋은 라운드를 거부하게 됩니다.
ENGINE-VERIFIED_shared/rng.ts bust100: h = h52(digest), 처음 13진 16진수 문자를 BigInt로; p = (100·2⁵² − h) ÷ (2⁵² − h) BigInt 나눗셈으로, [100, 1,000,000]에 고정; 주석은 "서버와 정확히 일치하는 BigInt 수학 (NewWhiteBack CrashDerivation.crashPoint100) 비트 단위 — float 근사는 floor 경계에서 정수 나눗셈과 다를 수 있고 검증기가 좋은 라운드를 거부하게 합니다." BigInt 값은 BigInt() 호출로 빌드되며, 프로젝트의 es5 대상에서 파일이 컴파일되도록 리터럴이 아닙니다."
하나의 구현, 두 가지 작업
같은 파일에 두 번째의, 더 조용한 결정이 있습니다. 해시를 숫자로 바꾸는 함수들은 게임 한 번, 체커 한 번 두 번 작성되지 않습니다. 헤더가 생성기와 검증기에 의해 공유되는 것으로 설명하는 모듈에 한 번 작성되며, 브라우저에서 로그인하지 않은 라운드를 재생하는 데모 엔진은 공정성 패널이 그것들을 확인하기 위해 호출하는 동일한 함수를 호출하여 결과를 생성합니다.
ENGINE-VERIFIED_shared/rng.ts 헤더: "증명 가능하게 공정한 GENERATOR (demoLocal.ts, 게임별 derive 모듈)와 VERIFIER (fairness.tsx)에 의해 공유되는 암호 원시. 순환 import 없이 게임의 파생을 양쪽에서 가져올 수 있도록 fairness.tsx에서 추출되었습니다." hmacSha256Utf8은 key와 message를 "서버의 RandomUtils.generateHash가 정확히 소비하는 방식으로" 소비하는 것으로 설명됩니다... "하나의 구현이므로 데모가 검증기가 수락하는 것과 절대 벗어날 수 없습니다."
그 설계는 쉽게 말할 수 있고 말할 가치가 있는 결과를 가져옵니다. 검증기가 라운드가 확인되었다고 말할 때, 그것은 자신의 서버 근사가 서버와 어떤 허용 범위 내에서 일치한다고 말하는 것이 아닙니다. 동일한 함수가 동일한 입력으로 동일한 출력을 생성했다고 말하는 것이며, 용인할 것이 없으므로 허용 범위가 없습니다. 라운드가 확인되지 않았다고 말할 때, 그것은 노이즈가 아닙니다. 입력이 다르다는 의미이며, 이것이 검증기가 존재하는 유일한 것입니다.
서버와 다르게 반올림하는 검증기는 때때로 거짓 경보를 우는 검증기입니다. 첫 번째 거짓 경보 후 아무도 실제 경보를 듣지 않습니다.반올림이 중요한 이유
무엇을 얻을 것인가
- "증명 가능하게 공정"은 산술에 대한 주장이며, 산술에는 경계가 있습니다. 약속 체계가 헤드라인이고; 비트 단위 파생이 헤드라인을 집행 가능하게 만드는 것입니다.
- 불일치는 드물어야 경보가 됩니다. 이 엔진에서는 정직한 라운드가 반올림 때문에 검증에 실패할 수 없으므로, 실패는 아티팩트가 아닌 정보입니다.
- 함수를 읽을 수 있습니다. 구성은 몇 줄이며, 검증 연습은 각각이 라이브 라운드에서 어디에 사용되는지 보여줍니다.
이 문서는 클라이언트 파생 모듈을 설명하고 미러하는 서버에 대한 주석을 인용합니다; 서버 자체는 공개 저장소에 없으며, 하우스 서비스가 지불 권한입니다. 이것은 인증이 아닌 엔지니어링 노트입니다.
FAQ
검증기와 서버가 동일한 해시를 사용하는데 불일치하는 이유는 무엇입니까?
해시를 0에서 1 사이의 숫자로 바꾸려면 53비트 정밀도로 반올림해야 하기 때문이고, 한 번 반올림하는 것이 여러 번 반올림하는 것과 같은 결과를 주지 않을 수도 있습니다. 루프에서 바이트를 누적하는 검증기는 한 번에 변환하는 서버와 마지막 비트에서 다를 수 있습니다.
한 비트가 정말 중요할까?
숫자에 37이나 52 또는 100만을 곱하고 내림할 때, 마지막 비트가 결과 정수를 결정할 수 있습니다. 그것은 다른 포켓, 카드 또는 아이템입니다.
엔진은 어떻게 이를 피할까요?
정확히 나타낼 수 있는 두 부분에서 숫자를 빌드하고, 첫 4바이트는 2³²에, 다음 바이트는 2⁵⁶ 또는 2⁶⁴에 나누어 한 번에 더하며, 서버의 정수-더블 변환의 단일 반올림과 일치합니다. Crash는 정확한 정수 연산을 사용하며 더블을 사용하지 않습니다.
검증자는 게임과 별개의 프로그램인가요?
아닙니다. 해시 및 숫자 함수는 데모 엔진과 공정성 패널이 모두 가져오는 하나의 공유 모듈에 있으므로 생성기는 검증자에서 벗어날 수 없습니다.
여기서 검증 실패는 무엇을 의미합니까?
입력이 서버가 사용한 것과 다르다는 뜻입니다. 왜냐하면 반올림은 이 엔진에서 거짓 실패를 일으킬 수 없기 때문입니다. 그것이 검증자가 주도록 존재하는 신호입니다.
소스 & 참고자료
- Betkyo 엔진 소스: _shared/rng.ts (u56, u64, h52, bust100, hmacSha256Utf8 및 주석)
- IEEE 754-2019 — 부동소수점 산술 표준 (binary64: 53비트 significand 정밀도)
- Bustabit provably fair crash-point 공식, bust100 구성이 반영
이 글의 게임
Limbo — 규칙 & 무료 플레이 →Crash — 규칙 & 무료 플레이 →Roulette — 규칙 & 무료 플레이 →