우리를 믿지 마세요.
개인키를 다루는 사이트에서 “안전합니다”라는 말은 아무 가치가 없습니다. 그 말을 하는 사이트일수록 위험하고요. 그래서 이 페이지에는 우리 주장을 당신이 직접 깨보는 방법만 적었습니다.
“퍼즐 데이터를 우리가 지어내지 않았습니다”
퍼즐 표에 적힌 주소·범위·개인키는 외부에서 가져온 텍스트 파일입니다. 옮겨 적는 과정에서 틀릴 수도 있고, 원본이 틀렸을 수도 있습니다. 그래서 그 파일을 그대로 쓰지 않습니다. 빌드할 때마다 아래 검증을 전부 통과해야만 사이트가 만들어집니다.
- 160행이 다 있고 행 번호 = 퍼즐 번호인가
- 범위가 정확히 [2n-1, 2n−1]인가
- 해결된 퍼즐의 개인키가 그 범위 안에 있는가
- 그 개인키에서 secp256k1로 공개키를 유도하면 원본 공개키와 같은가
- 거기서 HASH160·Base58Check를 거치면 원본 주소가 나오는가
하나라도 어긋나면 process.exit(1)로 빌드가 실패합니다. 지금 보고 있는 페이지가 존재한다는 것 자체가 160개 전부 통과했다는 뜻입니다.
저장소를 받아서 같은 스크립트를 실행하면 같은 결과가 나와야 합니다.
node scripts/build-data.mjs # ✓ 검증 통과 # 전체 160 / 해결 83 / 미해결 77 # 미해결 중 공개키 노출: 140, 145, 150, 155, 160 # 남은 상금 합계: 903 BTC
검증 스크립트 전문 보기 (159줄)
/**
* 퍼즐 원본 데이터를 검증하고 data/puzzles.json 으로 만든다.
*
* 원본(data/raw/puzzles.txt)은 외부에서 가져온 것이라 그대로 믿지 않는다.
* 여기서 하는 검증:
* 1. 160행이 전부 있고 행 번호 = 퍼즐 번호인지
* 2. 범위가 정확히 [2^(n-1), 2^n - 1] 인지
* 3. 해결된 퍼즐: 개인키 -> 압축 공개키 -> 주소 가 원본 주소와 일치하는지
* 4. 개인키가 실제로 그 범위 안에 들어있는지
* 5. 공개키가 공개된 퍼즐: 공개키 -> 주소 가 원본 주소와 일치하는지
* 하나라도 틀리면 빌드를 실패시킨다.
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import * as secp from '@noble/secp256k1';
import { sha256 } from '@noble/hashes/sha2.js';
import { ripemd160 } from '@noble/hashes/legacy.js';
import { base58check } from '@scure/base';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const NOT_SOLVED = 'NOT SOLVED';
const b58c = base58check(sha256);
/** 압축 공개키(hex) -> P2PKH 주소 */
function pubkeyToAddress(pubHex) {
const h160 = ripemd160(sha256(hexToBytes(pubHex)));
const payload = new Uint8Array(21);
payload[0] = 0x00; // mainnet P2PKH
payload.set(h160, 1);
return b58c.encode(payload);
}
function hexToBytes(hex) {
const clean = hex.length % 2 ? '0' + hex : hex;
const out = new Uint8Array(clean.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
return out;
}
/** 개인키(hex, 64자) -> 압축 공개키(hex) */
function privToPub(privHex) {
return secp.etc.bytesToHex(secp.getPublicKey(hexToBytes(privHex.padStart(64, '0')), true));
}
// ---------------------------------------------------------------- 파싱
const raw = readFileSync(join(root, 'data/raw/puzzles.txt'), 'utf8');
const lines = raw.split('\n').filter((l) => l.trim());
const errors = [];
if (lines.length !== 160) errors.push(`행 수가 160이 아님: ${lines.length}`);
const puzzles = lines.map((line, idx) => {
const n = idx + 1;
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
if (cells.length !== 5) {
errors.push(`#${n}: 컬럼 수가 5가 아님 (${cells.length})`);
return null;
}
const [range, address, status, pub, priv] = cells;
const [loHex, hiHex] = range.split(':');
const lo = BigInt('0x' + loHex);
const hi = BigInt('0x' + hiHex);
// 2. 범위 검증
const expLo = n === 1 ? 1n : 1n << BigInt(n - 1);
const expHi = (1n << BigInt(n)) - 1n;
if (lo !== expLo || hi !== expHi) {
errors.push(`#${n}: 범위 불일치 — 원본 ${loHex}:${hiHex}, 기대 ${expLo.toString(16)}:${expHi.toString(16)}`);
}
const solved = status === 'SOLVED';
const hasPub = pub !== NOT_SOLVED;
const privateKey = priv !== NOT_SOLVED ? priv.toLowerCase() : null;
// 3 & 4. 해결된 퍼즐 검증
let publicKey = hasPub ? pub.toLowerCase() : null;
if (privateKey) {
const k = BigInt('0x' + privateKey);
if (k < lo || k > hi) {
errors.push(`#${n}: 개인키가 범위 밖 (${privateKey})`);
}
const derivedPub = privToPub(privateKey);
if (publicKey && derivedPub !== publicKey) {
errors.push(`#${n}: 개인키에서 유도한 공개키가 원본과 다름`);
}
publicKey = derivedPub;
const derivedAddr = pubkeyToAddress(derivedPub);
if (derivedAddr !== address) {
errors.push(`#${n}: 개인키 -> 주소 불일치 — 유도 ${derivedAddr}, 원본 ${address}`);
}
} else if (publicKey) {
// 5. 공개키만 있는 경우
const derivedAddr = pubkeyToAddress(publicKey);
if (derivedAddr !== address) {
errors.push(`#${n}: 공개키 -> 주소 불일치 — 유도 ${derivedAddr}, 원본 ${address}`);
}
}
// 2023-04-16 증액 이후 상금 규칙: 미해결 퍼즐은 n/10 BTC.
// 해결된 퍼즐은 이미 인출되어 0이므로, 참고용 "당초 상금"만 계산해 둔다.
const prize = n / 10;
return {
n,
address,
rangeStart: '0x' + lo.toString(16),
rangeEnd: '0x' + hi.toString(16),
bits: n,
solved,
// 공개키 노출 = Kangaroo/BSGS 로 O(2^(n/2)) 공격 가능
pubkeyExposed: hasPub,
publicKey,
privateKey,
prizeBtc: prize,
};
}).filter(Boolean);
if (errors.length) {
console.error('\n✗ 데이터 검증 실패\n');
for (const e of errors) console.error(' ' + e);
process.exit(1);
}
const solved = puzzles.filter((p) => p.solved);
const unsolved = puzzles.filter((p) => !p.solved);
const attackable = unsolved.filter((p) => p.pubkeyExposed);
const out = {
generatedAt: new Date().toISOString(),
source: 'https://github.com/roadhero/Bitcoin-Puzzle-Info (BTC-Solved-Unsolved.txt) — 전 항목 secp256k1 재유도 검증 완료',
totals: {
all: puzzles.length,
solved: solved.length,
unsolved: unsolved.length,
pubkeyExposedUnsolved: attackable.length,
remainingPrizeBtc: Number(unsolved.reduce((s, p) => s + p.prizeBtc, 0).toFixed(1)),
},
puzzles,
};
writeFileSync(join(root, 'data/puzzles.json'), JSON.stringify(out, null, 2) + '\n');
// /verify 페이지가 이 스크립트 전문을 보여준다. 런타임에 파일을 읽으면
// 파일시스템 없는 환경(Cloudflare Workers 등)에서 죽으므로 지금 박아둔다.
const selfPath = fileURLToPath(import.meta.url);
writeFileSync(
join(root, 'data/build-data-source.json'),
JSON.stringify({ path: 'scripts/build-data.mjs', source: readFileSync(selfPath, 'utf8') }, null, 2) + '\n',
);
console.log('✓ 검증 통과');
console.log(` 전체 ${out.totals.all} / 해결 ${out.totals.solved} / 미해결 ${out.totals.unsolved}`);
console.log(` 미해결 중 공개키 노출: ${attackable.map((p) => p.n).join(', ')}`);
console.log(` 남은 상금 합계: ${out.totals.remainingPrizeBtc} BTC`);
console.log(` 가장 쉬운 미해결: #${unsolved[0].n} (2^${unsolved[0].n - 1} ~ 2^${unsolved[0].n})`);
“계산 결과가 맞는지 직접 확인해 보세요”
아래 도구에 개인키를 넣으면 공개키와 주소를 유도해서 보여줍니다. 이 사이트의 어떤 퍼즐 페이지를 열어서 거기 적힌 개인키를 복사해 넣어보세요. 같은 주소가 나와야 합니다. 안 나오면 우리가 틀린 겁니다.
반대로도 됩니다. 다른 사이트나 포럼에서 본 “퍼즐 #NN의 키는 이거다”라는 주장을 여기 넣어보면 진위가 바로 갈립니다.
개인키를 받는 입력창은 그 자체로 위험합니다. 다른 사이트에서 이런 걸 보면 일단 의심하세요. 여기가 안전하다고 주장하는 대신, 확인하는 법을 알려드립니다 — F12 → 네트워크 탭을 열고 키를 입력해보세요. 요청이 하나도 안 생깁니다.
여기 넣은 값은 어디로도 가지 않습니다. 계산은 전부 이 탭에서 일어납니다. 그래도 실제로 쓰는 지갑의 키는 넣지 마세요 — 습관을 만들면 언젠가 다른 사이트에서 넣게 됩니다.
0000000000000000000000000000000000000000000000000000000000000001
KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn
0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm
“어떤 키도 서버로 보내지 않습니다”
말로는 증명이 안 되니, 확인 방법 세 가지를 적어둡니다.
가장 쉬운 방법 — 네트워크 탭
F12로 개발자 도구를 열고 네트워크 탭을 켠 뒤, 탐색기를 몇 분 돌려보세요. 수백만 개의 키가 생성되는 동안 새로운 요청이 0건이어야 합니다.
확실한 방법 — 인터넷 끊기
페이지를 완전히 불러온 뒤 Wi-Fi를 끄고 탐색기를 돌려보세요. 여전히 작동합니다. 서버가 필요했다면 여기서 멈춥니다.
가장 확실한 방법 — 소스 읽기
탐색 로직은 Web Worker 파일 하나에 다 들어 있습니다.postMessage로 메인 스레드에 넘기는 건 시도 횟수·경과 시간·현재 위치뿐이고, 적중했을 때만 그 키가 넘어옵니다. 그것도 화면에 띄우기 위해서고, 네트워크로 나가는 코드는 파일 전체에 없습니다.
self.postMessage({
type: "progress",
tried, // 시도 횟수
elapsedMs, // 경과 시간
cursor, // 현재 위치 (hex)
});
// 적중했을 때만 (메인 스레드로만, 네트워크 아님)
self.postMessage({
type: "found",
privateKeyHex, h160, compressed, tried, elapsedMs,
});- 계정도 로그인도 없습니다. 당신을 식별할 방법 자체가 없습니다.
- 애널리틱스·광고·추적 스크립트를 넣지 않았습니다.
- 퍼즐 페이지의 잔액만 서버에서 조회하고, 그건 공개된 퍼즐 주소지 당신 것이 아닙니다.
“몰래 쓸어가지 않습니다” — 허니팟
앞의 세 가지는 “우리가 보내지 않는다”는 증명입니다. 하지만 더 강한 질문이 남습니다 — 우리가 몰래 훔치고 있지 않다는 걸 어떻게 아는가?
여기에 대한 고전적인 답이 허니팟입니다. Arnaud Brousseau가 keys.lol을 검증할 때 쓴 방법이기도 합니다: 그 사이트에 노출되는 주소에 소액을 넣어두고, 몇 년간 없어지는지 지켜본 것. 3년 넘게 그대로였고, 그게 그 사이트에 대한 가장 설득력 있는 증거가 됐습니다.
같은 걸 우리 자신에게 겁니다. 규칙은 이렇습니다:
- 이 사이트가 만들어내는 주소 중 일부에 실제로 비트코인을 넣습니다.
- 그 주소와 잔액을 여기에 공개하고, 온체인에서 실시간으로 읽어옵니다.
- 우리가 몰래 키를 로깅해서 쓸어간다면, 이 숫자가 0이 됩니다.
- 당신이 직접 블록 익스플로러에서 확인할 수 있습니다.
이게 완벽한 증명은 아닙니다. 우리가 허니팟만 피해서 훔칠 수도 있으니까요. 하지만 “믿어주세요”보다는 훨씬 낫고, 시간이 쌓일수록 강해집니다.
아직 허니팟을 심지 않았습니다. 있는 척하지 않겠습니다 — 자금을 넣는 순간 이 자리에 주소와 잔액이 실시간으로 표시되고, 그때부터 시계가 돌기 시작합니다.
운영자용 메모: data/honeypots.json의 entries에 주소를 넣으면 이 블록이 자동으로 살아납니다.
이 방법은 우리한테만 쓰는 게 아닙니다. 어떤 키 생성 사이트든, 거기서 나온 주소에 소액을 넣고 며칠 지켜보면 됩니다. 없어지면 답이 나온 거고, 안 없어져도 최소한 즉시 쓸어가는 곳은 아닙니다.
여전히 의심스럽다면, 그게 맞습니다
암호화폐 근처에서 “개인키”라는 단어를 쓰는 사이트를 처음부터 신뢰하면 안 됩니다. 이 페이지의 목적은 우리를 믿게 만드는 게 아니라, 무엇을 어떻게 확인해야 하는지 알려드리는 것입니다. 그 기준을 익히고 나면 다른 사이트도 판별할 수 있게 됩니다 — 그게 더 중요합니다.
검증하다가 우리가 틀린 걸 찾으면 알려주세요. 고치고, 무엇이 틀렸었는지 여기에 적겠습니다.