Don’t trust us.
On a site that handles private keys, the words “we’re safe” are worth nothing — and the sites that say it loudest are the worst. So this page contains only the ways for you to break our claims yourself.
“We did not invent the puzzle data”
The addresses, ranges and private keys in the puzzle table come from a text file obtained elsewhere. It could have been mistranscribed, or wrong at the source. So we do not use that file as given. Every build has to pass all of the checks below before the site is produced at all.
- Are all 160 rows present, with row number = puzzle number?
- Is each range exactly [2n-1, 2n−1]?
- Does each solved puzzle’s private key fall inside that range?
- Does deriving the public key with secp256k1 reproduce the published one?
- Does HASH160 and Base58Check on that give back the published address?
A single mismatch and process.exit(1) fails the build. The fact that this page exists at all means all 160 passed.
The scripts below are printed exactly as they sit in the repository, Korean comments and all. Translating them here would mean showing you something other than the file that actually runs, which would defeat the point.
Clone the repository, run the same script, and you should get the same output.
node scripts/build-data.mjs # ✓ verification passed # total 160 / solved 83 / unsolved 77 # unsolved with exposed public key: 140, 145, 150, 155, 160 # prize remaining: 903 BTC
Read the whole verification script (159 lines) — comments in Korean
/**
* 퍼즐 원본 데이터를 검증하고 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})`);
But that check is trusting a library
The script above uses @noble/secp256k1 to rebuild the public key from the private key and compare. It is a good library, but there is a hole here — if the library is wrong, the verification is wrong with it. Telling you not to trust us while asking you to trust somebody else.
So the elliptic curve arithmetic was written again from scratch in BigInt. Field inversion, point addition, doubling, scalar multiplication — all implemented directly, without a single line from a library. Then all 83 puzzles whose private keys are public are derived through both implementations and compared.
Neither implementation refers to the other. For both to be wrong, two different pieces of code would have to be wrong in the same way. The curve arithmetic is also checked for internal consistency — whether (a+b)G = aG + bG, whether n·G is the point at infinity, and so on.
The approach comes from Arnaud Brousseau’s keys.deconstructed , the same motivation behind his zero-dependency reimplementation of keys.lol. The code was written fresh rather than copied, though — his point addition has a bug where the inverse-point branch can never fire.
It runs automatically on every build, and a single mismatch fails the build.
node scripts/crosscheck.mjs # is the curve arithmetic self-consistent # ✓ G + (-G) = point at infinity # ✓ (a+b)G = aG + bG # ✓ n·G = point at infinity (group order) # # deriving 83 puzzles through both implementations # ✓ all 83 agree (our own = noble = stored value) # # ✓ cross-check passed
Read the whole cross-check script (194 lines) — comments in Korean
/**
* 노블 라이브러리를 믿지 않기 위한 두 번째 구현.
*
* 이 사이트는 @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,
ms: Math.round(ms),
}, null, 2) + '\n',
);
console.log(fail === 0 ? '\n✓ 교차검증 통과 — 독립된 두 구현이 같은 답을 냅니다\n' : `\n✗ ${fail}개 실패\n`);
process.exit(fail === 0 ? 0 : 1);
“Check our arithmetic for yourself”
Put a private key into the tool below and it derives the public key and address. Open any puzzle page on this site, copy the private key printed there, and paste it in. The same address has to come out. If it does not, we are wrong.
It works the other way too. Paste in any “the key to puzzle #NN is this” claim you find on another site or a forum, and it settles the question immediately.
A box that accepts private keys is dangerous by its nature. When you see one on another site, start from suspicion. Rather than claiming this one is safe, here is how to check: open F12 → Network and type a key in. Not one request appears.
Nothing you type here goes anywhere. Every calculation happens in this tab. Even so, do not paste a key from a wallet you actually use — build the habit and one day you will paste it into a different site.
0000000000000000000000000000000000000000000000000000000000000001
KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn
0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm
“No key is ever sent to a server”
Saying so proves nothing, so here are three ways to check.
The easiest — the network tab
Open the devtools with F12, switch to Network, and run the search tool for a few minutes. While millions of keys are generated there should be zero new requests.
The convincing one — disconnect
Let the page load fully, turn off Wi-Fi, and run the search. It keeps working. If it needed a server, this is where it would stop.
The definitive one — read the source
The search logic lives in a single Web Worker file. What postMessage hands to the main thread is the attempt count, the elapsed time and the current position — and, only on a hit, the key itself, so it can be displayed. There is no networking code anywhere in the file.
self.postMessage({
type: "progress",
tried, // attempts
elapsedMs, // elapsed time
cursor, // current position (hex)
});
// only on a hit (to the main thread only, never the network)
self.postMessage({
type: "found",
privateKeyHex, h160, compressed, tried, elapsedMs,
});- There are no accounts and no login. There is no way to identify you at all.
- No analytics, no ads, no tracking scripts.
- Only the puzzle balances are fetched, and those are published puzzle addresses, not yours.
“We are not quietly sweeping” — honeypots
The three above prove that we do not transmit anything. But a harder question remains — how do you know we are not quietly stealing?
The classical answer is a honeypot. It is what Arnaud Brousseau used to test keys.lol: put a small amount into an address that site exposes, then watch for years to see whether it disappears. It sat untouched for over three years, and that became the most convincing evidence anyone had about that site.
We are pointing the same thing at ourselves. The rules:
- Put real bitcoin into some of the addresses this site produces.
- Publish those addresses and balances here, read live from the chain.
- If we were secretly logging keys and sweeping them, these numbers would go to zero.
- You can confirm all of it on a block explorer yourself.
It is not a perfect proof — we could steal while avoiding the honeypots. But it beats “please trust us”, and it gets stronger the longer it holds.
We have not planted the honeypots yet, and we will not pretend otherwise. The moment funds go in, the addresses and balances appear here live, and the clock starts running.
Operator note: add addresses to entries in data/honeypots.json and this block comes alive on its own.
Arnaud Brousseau used this method on keys.lol back in 2019. He put 0.001 BTC into one of the keys that site displays and watched to see whether the site would sweep it.
1A2zNEFyMAfmZdy4yeXUoaPdt5Kns6vu3s6.7 years and it has not moved. That is not evidence about this site — it is evidence that the method works, and that it strengthens with time. Ours is only beginning.
Arnaud Brousseau’s write-up →This is not only for us. Take any key-generating site, put a small amount into an address it produced, and watch for a few days. If it vanishes you have your answer; if it does not, at least it is not a site that sweeps immediately.
If you are still suspicious, you should be
Anywhere near cryptocurrency, a site that uses the words “private key” should not be trusted from the outset. The purpose of this page is not to make you trust us but to show you what to check and how. Once you have that standard you can apply it to other sites — which matters more.
If you find something we got wrong, tell us. We will fix it and write down what was wrong, here.