ビットコインの「秘密鍵」と「アドレス」はどう作る?(Pythonで生成して理解する)
この記事では、ビットコインの秘密鍵(private key)から公開鍵(public key)を導出し、アドレス(P2WPKH / P2PKH)を作る までの流れを、Pythonコードとあわせて丁寧に説明します。
重要:ここで扱う秘密鍵は、資産の「所有権そのもの」です。 記事のサンプルは学習目的であり、実運用の資産用の鍵を安易に出力・共有しないでください。 ターミナル履歴やログに残る点も含め、取り扱いには十分注意してください。
※本記事は一般的な技術解説であり、投資助言・法的助言ではありません。
全体像:秘密鍵 → 公開鍵 → アドレス
ビットコインの鍵生成は、ざっくり言うと次のパイプラインです。
「秘密鍵を持つ」=「そのアドレスの資産を動かせる」。
だからこそ、生成・保存・表示には細心の注意が必要です。
Step 1:秘密鍵(secp256k1)を生成する
秘密鍵は、暗号学的に安全な乱数で生成するのが基本です。 このサンプルでは Python の secrets を使い、 32バイトの乱数を作って整数に変換し、secp256k1 の位数 n に対して 1 ≤ k < n を満たすまで繰り返します。
- 32バイト:ビットコインの秘密鍵は通常 256bit(32 bytes)
- 範囲チェック:楕円曲線の有効範囲(1〜n-1)に入れる
- secrets:暗号用途向けの安全な乱数生成
while True:
k = secure_random_32bytes()
if 1 <= int(k) < secp256k1_n:
return k
Step 2:秘密鍵から公開鍵を導出する(圧縮形式)
公開鍵は、楕円曲線 secp256k1 上で k × G(Gは生成点)を計算して得られます。 実装では ecdsa ライブラリを使い、(x,y) を取り出します。
非圧縮公開鍵(65 bytes)
- 0x04 + x(32) + y(32)
- 昔からある表現
圧縮公開鍵(33 bytes)
- 先頭は 0x02(y偶数)or 0x03(y奇数)
- 続けて x(32)
- 現在の標準的な形式(推奨)
本記事のPythonコードでも、P2WPKH作成時に「圧縮公開鍵でなければエラー」にしています。
Step 3:アドレスを作る(P2WPKH / P2PKH)
アドレスは「公開鍵そのもの」ではなく、公開鍵をハッシュ化・エンコードした表現です。 ここでは代表的な2種類を作ります。
| 種類 | 見た目 | 作り方(要点) |
|---|---|---|
| P2WPKH(SegWit v0) | bc1... | HASH160(圧縮公開鍵) を witness program として Bech32(BIP-0173) でエンコード |
| P2PKH(Legacy) | 1... | HASH160(公開鍵) に version byte(mainnet: 0x00)を付け、 Base58Check でエンコード |
HASH160 とは?
HASH160(x) = RIPEMD160(SHA256(x))。 ビットコインでは公開鍵・スクリプト等を短く表すのに広く使われます。
Step 4:秘密鍵をWIF形式で表現する
WIF(Wallet Import Format)は秘密鍵を人間が扱いやすい文字列にした形式です。 このコードでは Base58Check を使い、mainnet/testnet と圧縮鍵かどうかで payload を変えています。
payload の構造(mainnet例)
- version: 0x80(testnetは 0xEF)
- 秘密鍵 32 bytes
- 圧縮鍵なら末尾に 0x01 を追加
- 最後に checksum(ダブルSHA256の先頭4バイト)
その瞬間に「資産の操作権」を渡すのと同じです。学習目的なら、必ずテスト環境やダミー前提で扱いましょう。
実行方法:コードをダウンロードして動かす
ここまでの生成手順を、ひとつのPythonスクリプトとしてまとめたものを用意しておくと便利です。 このブログでは、次の2つのやり方を提示します。
選択肢A:このページの内容を元に自分で作る
手順を追いながら、コードを分割して理解しやすい形で実装できます。 「Base58Check」「Bech32」「HASH160」の役割が掴みやすいです。
ただし、実装ミスがあるとアドレスが不正になることがあるので、検証は慎重に。
すぐ動かして確認したい場合は、スクリプトをそのまま利用できます(依存:ecdsa と requests)。
※リンク先は例です。実運用では、このHTMLと同じディレクトリに btc_keypair_generator.py を置く想定です。
動かし方(最小手順)
- Python 3 を用意
- 依存ライブラリをインストール
- スクリプトを実行
# 依存のインストール
pip install ecdsa requests
# 実行
python3 btc_keypair_generator.py
補足:スクリプトは TESTNET と COMPRESSED を切り替えられます。 学習目的なら、まずは TESTNET=True にして試すと安心です。
※ただし、秘密鍵の表示・保存の扱いはテストネットでも慎重に。
コード全文(ダウンロード版と同じ)
下のコードは「秘密鍵生成 → 公開鍵導出 → WIF → アドレス(P2WPKH / P2PKH) → 残高確認」の一式です。 私用に改変する場合も、まずはそのまま動かして出力を確認するのが近道です。
#!/usr/bin/env python3
import secrets
import hashlib
# ---- secp256k1 ----
SECP256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
# ---- Base58Check ----
BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
def base58_encode(b: bytes) -> str:
"""Encode bytes as Base58 (Bitcoin alphabet), preserving leading zeroes as '1'."""
if not isinstance(b, (bytes, bytearray)):
raise TypeError("base58_encode expects bytes")
num = int.from_bytes(b, "big")
out = []
while num > 0:
num, rem = divmod(num, 58)
out.append(BASE58_ALPHABET[rem])
out_str = "".join(reversed(out)) # may be ""
# Leading 0x00 bytes become '1' in Base58
pad = 0
for byte in b:
if byte == 0:
pad += 1
else:
break
if out_str == "":
return "1" * pad if pad else "1"
return "1" * pad + out_str
def base58check_encode(payload: bytes) -> str:
"""Base58Check = Base58(payload || checksum4), checksum4 = SHA256(SHA256(payload))[:4]."""
if not isinstance(payload, (bytes, bytearray)):
raise TypeError("base58check_encode expects bytes")
checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
return base58_encode(payload + checksum)
# ---- Hash helpers ----
def sha256(b: bytes) -> bytes:
return hashlib.sha256(b).digest()
def ripemd160(b: bytes) -> bytes:
h = hashlib.new("ripemd160")
h.update(b)
return h.digest()
def hash160(b: bytes) -> bytes:
return ripemd160(sha256(b))
# ---- Bech32 (BIP-0173) for SegWit v0 ----
BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def bech32_polymod(values):
GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
chk = 1
for v in values:
b = (chk >> 25) & 0xFF
chk = ((chk & 0x1FFFFFF) << 5) ^ v
for i in range(5):
chk ^= GEN[i] if ((b >> i) & 1) else 0
return chk
def bech32_hrp_expand(hrp: str):
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
def bech32_create_checksum(hrp: str, data):
values = bech32_hrp_expand(hrp) + data
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 # BIP-0173 constant for v0
return [(polymod >> (5 * (5 - i))) & 31 for i in range(6)]
def bech32_encode(hrp: str, data) -> str:
combined = data + bech32_create_checksum(hrp, data)
return hrp + "1" + "".join(BECH32_CHARSET[d] for d in combined)
def convertbits(data: bytes, frombits: int, tobits: int, pad: bool = True):
"""General power-of-2 base conversion (BIP-0173 style). Returns list of ints or None on error."""
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
for b in data:
if b < 0 or b >> frombits:
return None
acc = (acc << frombits) | b
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad:
if bits:
ret.append((acc << (tobits - bits)) & maxv)
else:
if bits >= frombits:
return None
if (acc << (tobits - bits)) & maxv:
return None
return ret
# ---- Key generation / formats ----
def generate_private_key() -> bytes:
"""Generate a uniformly random 32-byte secp256k1 private key in [1, n-1]."""
while True:
k = secrets.token_bytes(32)
ki = int.from_bytes(k, "big")
if 1 <= ki < SECP256K1_N:
return k
def to_wif(privkey32: bytes, compressed: bool = True, testnet: bool = False) -> str:
"""Encode a 32-byte private key as WIF (Base58Check)."""
if not isinstance(privkey32, (bytes, bytearray)) or len(privkey32) != 32:
raise ValueError("privkey32 must be 32 bytes")
version = b"\xEF" if testnet else b"\x80"
payload = version + privkey32 + (b"\x01" if compressed else b"")
return base58check_encode(payload)
def pubkey_from_privkey(privkey32: bytes, compressed: bool = True) -> bytes:
"""Derive a secp256k1 public key from a 32-byte private key."""
if not isinstance(privkey32, (bytes, bytearray)) or len(privkey32) != 32:
raise ValueError("privkey32 must be 32 bytes")
# dependency: pip install ecdsa
from ecdsa import SigningKey, SECP256k1
sk = SigningKey.from_string(privkey32, curve=SECP256k1)
vk = sk.get_verifying_key()
xy = vk.to_string() # 64 bytes: x(32) || y(32)
x = xy[:32]
y = xy[32:]
if not compressed:
return b"\x04" + x + y
# Compressed form: 0x02 if y even, 0x03 if y odd, followed by x
prefix = b"\x03" if (y[-1] & 1) else b"\x02"
return prefix + x
def address_p2pkh(pubkey: bytes, testnet: bool = False) -> str:
"""Legacy P2PKH address (Base58Check), starts with '1' (mainnet) or 'm/n' (testnet)."""
if not isinstance(pubkey, (bytes, bytearray)) or len(pubkey) not in (33, 65):
raise ValueError("pubkey must be 33 (compressed) or 65 (uncompressed) bytes")
h160 = hash160(pubkey)
version = b"\x6F" if testnet else b"\x00"
return base58check_encode(version + h160)
def address_p2wpkh(pubkey: bytes, testnet: bool = False) -> str:
"""
Native SegWit v0 P2WPKH (Bech32).
IMPORTANT: Standard P2WPKH uses HASH160(compressed_pubkey) only.
"""
if not isinstance(pubkey, (bytes, bytearray)):
raise TypeError("pubkey must be bytes")
# Enforce compressed pubkey (33 bytes starting with 0x02 or 0x03)
if not (len(pubkey) == 33 and pubkey[0] in (2, 3)):
raise ValueError("P2WPKH requires a compressed public key (33 bytes, prefix 0x02/0x03).")
h160 = hash160(pubkey) # 20 bytes
hrp = "tb" if testnet else "bc"
prog5 = convertbits(h160, 8, 5, pad=True)
if prog5 is None:
raise ValueError("convertbits failed")
data = [0] + prog5 # witness version 0 + program
return bech32_encode(hrp, data)
import requests
SATOSHIS_PER_BTC = 100_000_000
def get_address_balance(address: str, testnet: bool = False, timeout: float = 10.0):
"""
Fetch balance info for a Bitcoin address using Blockstream's API.
Returns a dict with:
- confirmed_sats / confirmed_btc
- mempool_sats / mempool_btc (net in mempool)
- total_sats / total_btc (confirmed + mempool)
- also raw stats for reference
"""
if not isinstance(address, str) or not address:
raise ValueError("address must be a non-empty string")
base = "https://blockstream.info"
api = f"{base}/testnet/api" if testnet else f"{base}/api"
url = f"{api}/address/{address}"
r = requests.get(url, timeout=timeout)
r.raise_for_status()
data = r.json()
cs = data.get("chain_stats", {})
ms = data.get("mempool_stats", {})
confirmed = int(cs.get("funded_txo_sum", 0)) - int(cs.get("spent_txo_sum", 0))
mempool = int(ms.get("funded_txo_sum", 0)) - int(ms.get("spent_txo_sum", 0))
total = confirmed + mempool
return {
"address": address,
"network": "testnet" if testnet else "mainnet",
"confirmed_sats": confirmed,
"confirmed_btc": confirmed / SATOSHIS_PER_BTC,
"mempool_sats": mempool,
"mempool_btc": mempool / SATOSHIS_PER_BTC,
"total_sats": total,
"total_btc": total / SATOSHIS_PER_BTC,
"chain_stats": cs,
"mempool_stats": ms,
}
if __name__ == "__main__":
TESTNET = False # True -> testnet (tb1..., m/n..., WIF usually starts with 'c')
COMPRESSED = True # Recommended: True (modern standard)
priv = generate_private_key()
pub = pubkey_from_privkey(priv, compressed=COMPRESSED)
# WARNING: Printing private keys exposes them in terminal history/logs.
print("Private key (hex):", priv.hex())
print("Public key (hex): ", pub.hex())
print("WIF: ", to_wif(priv, compressed=COMPRESSED, testnet=TESTNET))
print("Address (P2WPKH): ", address_p2wpkh(pub, testnet=TESTNET)) # bc1... / tb1...
print("Address (P2PKH): ", address_p2pkh(pub, testnet=TESTNET)) # 1... / m/n...
addr_wpkh = address_p2wpkh(pub, testnet=TESTNET)
addr_p2pkh = address_p2pkh(pub, testnet=TESTNET)
print("Address (P2WPKH): ", addr_wpkh)
print("Address (P2PKH): ", addr_p2pkh)
# Check balances
try:
b1 = get_address_balance(addr_wpkh, testnet=TESTNET)
b2 = get_address_balance(addr_p2pkh, testnet=TESTNET)
print("\\nBalance (P2WPKH):", b1["total_btc"], "BTC",
f"(confirmed={b1['confirmed_btc']} BTC, mempool={b1['mempool_btc']} BTC)")
print("Balance (P2PKH): ", b2["total_btc"], "BTC",
f"(confirmed={b2['confirmed_btc']} BTC, mempool={b2['mempool_btc']} BTC)")
except Exception as e:
print("Balance check failed:", e)
よくある注意点
1) 秘密鍵を表示・保存しない工夫
- ターミナル履歴・CIログ・スクショに残りやすい
- 学習目的でも、出力は最小限に
- 本格運用はハードウェアウォレット等も検討
2) テストネットを活用する
- TESTNET=True で試す
- アドレスは tb1... / m/n... になる
- 学習の安全性が上がる
実装の中身(HASH160 / Base58Check / Bech32)を追うことで、「なぜその形式になるのか」が腹落ちします。