공정성 확인: 결과 화면에서 브라우저가 SHA-256(씨앗)을 직접 계산해 시작 때 공개한 값과 비교

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-04 05:46:30 +09:00
parent 9519b09be8
commit 31aedbfcc9
2 changed files with 33 additions and 6 deletions

View File

@@ -612,17 +612,40 @@ function ResultPanel({ room, me, players, onLeave }: { room: RoomView; me: Publi
방 나가기 방 나가기
</button> </button>
{room.lastSeed && ( {room.lastSeed && (
<details className="small muted"> <FairnessCheck seedHash={room.seedHash} seed={room.lastSeed} />
<summary>공정성 확인</summary>
<p>시작할 때 공개한 값: {room.seedHash ?? '-'}</p>
<p>이번 판의 난수 씨앗: {room.lastSeed}</p>
<p>씨앗의 SHA-256이 위 값과 같으면 서버가 게임 도중 난수를 바꾸지 않았다는 뜻이에요.</p>
</details>
)} )}
</Sheet> </Sheet>
); );
} }
/**
* docs/04 §5: the seed's SHA-256 was published when the game started; the seed is revealed at the end.
* The browser recomputes the hash itself so players don't have to trust the server's word.
*/
function FairnessCheck({ seedHash, seed }: { seedHash: string | null; seed: string }) {
const [verdict, setVerdict] = useState<null | boolean>(null);
const check = async () => {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(seed));
const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
setVerdict(hex === seedHash);
};
return (
<details className="small muted">
<summary>공정성 확인</summary>
<p>시작할 때 공개한 값(SHA-256): {seedHash ?? '-'}</p>
<p>끝나고 공개한 난수 씨앗: {seed}</p>
<button type="button" className="btn btn-secondary btn-sm" onClick={check}>
내 브라우저에서 직접 확인하기
</button>
{verdict !== null && (
<p style={{ fontWeight: 700, color: verdict ? 'var(--success)' : 'var(--danger)' }}>
{verdict ? '✓ 일치해요. 게임 도중 카드 순서나 난수가 바뀌지 않았어요.' : '✗ 일치하지 않아요. 관리자에게 알려 주세요.'}
</p>
)}
</details>
);
}
// ---------------------------------------------------------------- chat // ---------------------------------------------------------------- chat
function ChatButton({ onOpen }: { onOpen: () => void }) { function ChatButton({ onOpen }: { onOpen: () => void }) {

View File

@@ -129,6 +129,10 @@ try {
await a.getByText('🎉 이겼어요!').waitFor(); await a.getByText('🎉 이겼어요!').waitFor();
await b.getByText('아쉽게 졌어요').waitFor(); await b.getByText('아쉽게 졌어요').waitFor();
await a.waitForTimeout(500); // let the sheet animation finish await a.waitForTimeout(500); // let the sheet animation finish
// Fairness check: the browser recomputes SHA-256(seed) and compares with the published hash.
await a.getByText('공정성 확인').click();
await a.getByRole('button', { name: '내 브라우저에서 직접 확인하기' }).click();
await a.getByText('✓ 일치해요').waitFor();
await a.screenshot({ path: join(SHOTS, '3-result-pc.png') }); await a.screenshot({ path: join(SHOTS, '3-result-pc.png') });
await b.screenshot({ path: join(SHOTS, '4-result-phone.png') }); await b.screenshot({ path: join(SHOTS, '4-result-phone.png') });
}); });