우리를 믿지 마세요.
개인키를 다루는 사이트에서 “안전합니다”라는 말은 아무 가치가 없습니다. 그 말을 하는 사이트일수록 위험하고요. 그래서 이 페이지에는 우리 주장을 당신이 직접 깨보는 방법만 적었습니다.
“퍼즐 데이터를 우리가 지어내지 않았습니다”
퍼즐 표에 적힌 주소·범위·개인키는 외부에서 가져온 텍스트 파일입니다. 옮겨 적는 과정에서 틀릴 수도 있고, 원본이 틀렸을 수도 있습니다. 그래서 그 파일을 그대로 쓰지 않습니다. 빌드할 때마다 아래 검증을 전부 통과해야만 사이트가 만들어집니다.
- 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
검증 스크립트 전문 보기 (179줄)
/**
* 퍼즐 원본 데이터를 검증하고 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 body = {
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,
};
/* generatedAt 은 "데이터가 바뀐 시각" 이지 "빌드를 돌린 시각" 이 아니다.
* 빌드마다 새로 찍으면 커밋되는 data/puzzles.json 이 매번 더러워지고,
* sitemap 의 lastmod 도 아무것도 안 바뀐 날 전부 갱신된 것처럼 보인다
* (app/sitemap.ts 가 피하려던 바로 그 잡음이다).
* 그래서 나머지 내용이 이전과 같으면 이전 시각을 그대로 쓴다. */
const outPath = join(root, 'data/puzzles.json');
const sameAsBefore = (prev) =>
prev && typeof prev.generatedAt === 'string' &&
JSON.stringify({ ...prev, generatedAt: undefined }) ===
JSON.stringify({ ...body, generatedAt: undefined });
let generatedAt = new Date().toISOString();
try {
const prev = JSON.parse(readFileSync(outPath, 'utf8'));
if (sameAsBefore(prev)) generatedAt = prev.generatedAt;
} catch {
// 파일이 없거나 깨졌으면 지금 시각으로 새로 찍는다
}
const out = { generatedAt, ...body };
writeFileSync(outPath, 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})`);
그런데 그 검증은 라이브러리를 믿고 있습니다
위 스크립트는 @noble/secp256k1 로 개인키에서 공개키를 다시 만들어 대조합니다. 좋은 라이브러리지만, 여기에 구멍이 하나 있습니다 — 그 라이브러리가 틀렸다면 검증도 같이 틀립니다. “우리를 믿지 마세요” 라고 해 놓고 남을 믿으라고 하는 셈입니다.
그래서 타원곡선 산술을 BigInt 로 처음부터 다시 짰습니다. 유한체 나눗셈도, 점 덧셈도, 배가도, 스칼라 곱도 직접 구현했고 라이브러리를 한 줄도 쓰지 않았습니다. 그리고 개인키가 공개된 퍼즐 83개 전부를 두 구현으로 각각 유도해 대조합니다.
두 구현은 서로를 참조하지 않습니다. 그러니 둘 다 틀리려면 서로 다른 코드가 같은 방식으로 틀려야 합니다. 곡선 산술이 스스로 일관적인지도 함께 확인합니다 — (a+b)G = aG + bG 인지, n·G 가 무한원점인지 같은 것들입니다.
이 방식은 Arnaud Brousseau 의 keys.deconstructed 에서 가져왔습니다. 그가 keys.lol 을 의존성 0으로 재구현한 것과 같은 동기입니다. 다만 코드는 옮기지 않고 새로 썼습니다 — 그쪽 점 덧셈에는 대칭점 분기가 죽어 있는 버그가 있습니다.
빌드할 때마다 자동으로 돌고, 하나라도 어긋나면 빌드가 실패합니다.
node scripts/crosscheck.mjs # 곡선 산술이 스스로 일관적인가 # ✓ G + (-G) = 무한원점 # ✓ (a+b)G = aG + bG # ✓ n·G = 무한원점 (군의 위수) # # 퍼즐 83개를 두 구현으로 각각 유도해 대조 # ✓ 83개 전부 일치 (직접 구현 = 노블 = 저장된 값) # # ✓ 교차검증 통과
교차검증 스크립트 전문 보기 (195줄)
/**
* 노블 라이브러리를 믿지 않기 위한 두 번째 구현.
*
* 이 사이트는 @noble/secp256k1 로 개인키가 공개된 퍼즐의 개인키 -> 주소를 재유도해서
* 데이터가 맞는지 확인한다(scripts/build-data.mjs). 그런데 그 검증은
* "노블이 맞다" 를 전제로 한다. 노블이 틀렸다면 검증도 같이 틀린다.
*
* 그래서 여기서는 타원곡선 산술을 BigInt 로 처음부터 다시 짠다.
* 라이브러리를 한 줄도 쓰지 않는다 — 유한체 나눗셈도, 점 덧셈도, 배가도.
* 그리고 개인키가 있는 83개 전부에서 노블과 같은 공개키가 나오는지 대조한다.
* (나머지 77개는 개인키가 없어서 유도할 대상 자체가 없다.)
*
* 두 구현이 독립이므로, 둘 다 틀리려면 서로 다른 코드가 같은 방식으로
* 틀려야 한다. 그건 훨씬 있을 법하지 않은 일이다.
*
* 참고: Arnaud Brousseau 의 keys.deconstructed 가 같은 동기로 쓰였다.
* 다만 그 구현의 점 덧셈에는 대칭점 분기가 죽어 있는 버그가 있어
* (this.y.eq(this.y) 는 언제나 참) 코드를 가져오지 않고 새로 썼다.
*/
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';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
/* ────────────────────────── 유한체 ──────────────────────────
* 비트코인 곡선이 사는 소수체. p = 2^256 - 2^32 - 977
* (부동소수점을 거치지 않고 BigInt 로만 쓴다) */
const P = 2n ** 256n - 2n ** 32n - 977n;
const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
const mod = (a) => ((a % P) + P) % P;
/** a^e mod P — 제곱-곱셈법. BigInt 의 ** 는 지수가 크면 터진다. */
function powmod(a, e) {
let r = 1n, b = mod(a);
while (e > 0n) {
if (e & 1n) r = (r * b) % P;
b = (b * b) % P;
e >>= 1n;
}
return r;
}
/** 1/a mod P — 페르마의 소정리: p 가 소수면 a^(p-2) ≡ 1/a */
const inv = (a) => powmod(a, P - 2n);
/* ──────────────────── 타원곡선 y² = x³ + 7 ────────────────────
* 점은 {x, y} 이고 무한원점(항등원)은 null 로 둔다. */
/** 이 점이 정말 곡선 위에 있는가 */
function onCurve(pt) {
if (pt === null) return true;
return mod(pt.y * pt.y) === mod(pt.x * pt.x * pt.x + 7n);
}
function double(pt) {
if (pt === null) return null;
if (pt.y === 0n) return null; // 접선이 수직 -> 무한원점
const s = mod(3n * pt.x * pt.x * inv(2n * pt.y));
const x = mod(s * s - 2n * pt.x);
return { x, y: mod(s * (pt.x - x) - pt.y) };
}
function add(a, b) {
if (a === null) return b; // 0 + B = B
if (b === null) return a; // A + 0 = A
if (a.x === b.x) {
// 여기가 브루소 구현이 틀린 자리다. y 가 다르면 서로 대칭점이므로 합은 0.
if (mod(a.y + b.y) === 0n) return null;
return double(a); // 같은 점이면 배가
}
const s = mod((b.y - a.y) * inv(b.x - a.x));
const x = mod(s * s - a.x - b.x);
return { x, y: mod(s * (a.x - x) - a.y) };
}
/** k·G — 아래에서 위로 훑는 배가-덧셈 */
function multiply(k, pt) {
let r = null, cur = pt, n = k;
while (n > 0n) {
if (n & 1n) r = add(r, cur);
cur = double(cur);
n >>= 1n;
}
return r;
}
const G = {
x: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
y: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
};
const hex64 = (n) => n.toString(16).padStart(64, '0');
/** 개인키 -> 압축 공개키 hex. 라이브러리 없이. */
function derivePubkey(k) {
const pt = multiply(k, G);
if (pt === null) throw new Error('무한원점이 나왔다');
if (!onCurve(pt)) throw new Error('곡선 위의 점이 아니다');
return (pt.y % 2n === 0n ? '02' : '03') + hex64(pt.x);
}
/* ──────────────────────────── 검사 ──────────────────────────── */
let fail = 0;
const ok = (name, cond, extra = '') => {
if (cond) console.log(` ✓ ${name}${extra ? ' ' + extra : ''}`);
else { console.log(` ✗ ${name}${extra ? ' ' + extra : ''}`); fail++; }
};
console.log('\n곡선 산술이 스스로 일관적인가');
ok('G 가 곡선 위에 있다', onCurve(G));
ok('2G 가 곡선 위에 있다', onCurve(double(G)));
ok('G + G = 2G', (() => { const a = add(G, G), b = double(G); return a.x === b.x && a.y === b.y; })());
ok('G + (-G) = 무한원점', add(G, { x: G.x, y: mod(-G.y) }) === null);
ok('3G = 2G + G = G + 2G', (() => {
const a = add(double(G), G), b = add(G, double(G));
return a.x === b.x && a.y === b.y;
})());
ok('(a+b)G = aG + bG', (() => {
const a = 1234567n, b = 7654321n;
const l = multiply(a + b, G), r = add(multiply(a, G), multiply(b, G));
return l.x === r.x && l.y === r.y;
})());
ok('n·G = 무한원점 (군의 위수)', multiply(N, G) === null);
// 공개된 표준 벡터. https://en.bitcoin.it/wiki/Secp256k1
console.log('\n표준 시험값과 대조');
ok('1·G 의 x 좌표', hex64(multiply(1n, G).x) === '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798');
ok('2·G 의 x 좌표', hex64(multiply(2n, G).x) === 'c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5');
ok('3·G 의 x 좌표', hex64(multiply(3n, G).x) === 'f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9');
// SHA-256 과 RIPEMD-160 은 노블 것을 쓰므로, 공개된 시험값으로 따로 확인한다
const h = (b) => Buffer.from(b).toString('hex');
ok('SHA-256("abc")', h(sha256(Buffer.from('abc'))) === 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad');
ok('RIPEMD-160("abc")', h(ripemd160(Buffer.from('abc'))) === '8eb208f7e05d987a9b044a8e98c6b087f15a0bfc');
/* ── 본론: 160개 퍼즐을 두 구현으로 각각 유도해 대조 ── */
const data = JSON.parse(readFileSync(join(root, 'data/puzzles.json'), 'utf8'));
const withKey = data.puzzles.filter((p) => p.privateKey);
console.log(`\n퍼즐 ${withKey.length}개를 두 구현으로 각각 유도해 대조`);
const t0 = process.hrtime.bigint();
let mismatch = [];
for (const p of withKey) {
const k = BigInt('0x' + p.privateKey);
const mine = derivePubkey(k);
const noble = secp.etc.bytesToHex(secp.getPublicKey(hexToBytes(hex64(k)), true));
if (mine !== noble || mine !== p.publicKey) {
mismatch.push({ n: p.n, mine, noble, stored: p.publicKey });
}
}
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
ok(`${withKey.length}개 전부 일치 (직접 구현 = 노블 = 저장된 값)`, mismatch.length === 0,
`${ms.toFixed(0)}ms`);
for (const m of mismatch) {
console.log(` #${m.n}\n 직접 ${m.mine}\n 노블 ${m.noble}\n 저장 ${m.stored}`);
}
// 공개키만 공개된 미해결 퍼즐: 저장된 공개키가 곡선 위의 점인지라도 확인한다
const pubOnly = data.puzzles.filter((p) => p.publicKey && !p.privateKey);
let bad = 0;
for (const p of pubOnly) {
const x = BigInt('0x' + p.publicKey.slice(2));
const y2 = mod(x * x * x + 7n);
// 제곱근이 존재하는가 — 오일러 판정법
if (powmod(y2, (P - 1n) / 2n) !== 1n) bad++;
}
ok(`미해결 중 공개키 공개된 ${pubOnly.length}개가 전부 곡선 위의 점`, bad === 0);
function hexToBytes(s) {
const o = new Uint8Array(s.length / 2);
for (let i = 0; i < o.length; i++) o[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
return o;
}
// /verify 페이지가 이 스크립트 전문을 보여준다. 런타임에 파일을 읽으면
// 파일시스템 없는 환경에서 죽으므로 지금 박아둔다.
// 커밋되는 파일이므로 실행마다 달라지는 값(측정 시간 같은)은 넣지 않는다.
// 걸린 시간은 위 콘솔 출력에만 남긴다.
writeFileSync(
join(root, 'data/crosscheck-source.json'),
JSON.stringify({
path: 'scripts/crosscheck.mjs',
source: readFileSync(fileURLToPath(import.meta.url), 'utf8'),
checked: withKey.length,
}, null, 2) + '\n',
);
console.log(fail === 0 ? '\n✓ 교차검증 통과 — 독립된 두 구현이 같은 답을 냅니다\n' : `\n✗ ${fail}개 실패\n`);
process.exit(fail === 0 ? 0 : 1);
“계산 결과가 맞는지 직접 확인해 보세요”
아래 도구에 개인키를 넣으면 공개키와 주소를 유도해서 보여줍니다. 이 사이트의 어떤 퍼즐 페이지를 열어서 거기 적힌 개인키를 복사해 넣어보세요. 같은 주소가 나와야 합니다. 안 나오면 우리가 틀린 겁니다.
반대로도 됩니다. 다른 사이트나 포럼에서 본 “퍼즐 #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에 주소를 넣으면 이 블록이 자동으로 살아납니다.
Arnaud Brousseau 는 2019년에 keys.lol 을 검증하려고 이 방법을 먼저 썼습니다. 그 사이트가 표시하는 키 중 하나에 0.001 BTC 를 넣고, 사이트가 몰래 쓸어가는지 지켜본 것입니다.
1A2zNEFyMAfmZdy4yeXUoaPdt5Kns6vu3s6.8년이 지나도록 움직이지 않았습니다. 이건 우리 사이트에 대한 증거가 아니라 이 방법이 실제로 작동한다는 증거입니다 — 그리고 시간이 쌓일수록 강해진다는 것도요. 우리 것은 이제 시작입니다.
Arnaud Brousseau 의 원문 →이 방법은 우리한테만 쓰는 게 아닙니다. 어떤 키 생성 사이트든, 거기서 나온 주소에 소액을 넣고 며칠 지켜보면 됩니다. 없어지면 답이 나온 거고, 안 없어져도 최소한 즉시 쓸어가는 곳은 아닙니다.
여전히 의심스럽다면, 그게 맞습니다
암호화폐 근처에서 “개인키”라는 단어를 쓰는 사이트를 처음부터 신뢰하면 안 됩니다. 이 페이지의 목적은 우리를 믿게 만드는 게 아니라, 무엇을 어떻게 확인해야 하는지 알려드리는 것입니다. 그 기준을 익히고 나면 다른 사이트도 판별할 수 있게 됩니다 — 그게 더 중요합니다.
검증하다가 우리가 틀린 걸 찾으면 알려주세요. 고치고, 무엇이 틀렸었는지 여기에 적겠습니다.
이 사이트는 광고를 받지 않습니다
그래서 수익이 없습니다. 쓰시다가 도움이 됐다면 아래 둘 중 하나로 보태 주시면 됩니다. 둘 다 링크와 글자뿐입니다 — 누르기 전에는 아무 일도 일어나지 않고, 누구를 추적하지도 않습니다.