學習 btc 錢包私鑰、公鑰和地址的生成過程
一個 Bitcoin 錢包包含了一系列的金鑰對,每個金鑰對都是由一對公鑰(public key)和私鑰(private key)組成。私鑰(k)通常是隨機選出的一串數字串,之後我們就可以通過橢圓曲線密碼學(ECC)演算法來產生一個公鑰(K),然後再通過單向的 Hash 演算法來生成 Bitcoin 地址。
如下圖所示,描述了生成過程及主要的演算法,以及整個過程的每一步都是不可逆的。

如何生成私鑰(private key)
本質上私鑰就是一串隨機選出的 256 個 bit 的 01 數字(32 位元組 * 8 = 256 bits),但是這串數字卻控制著你的比特幣賬號的所有權,因此這串數字相當重要,要具有足夠的隨機性,一般採用密碼學安全的 偽隨機數生成器(CSPNG) ,並且需要有一個來自具有足夠熵值的源的種子(seed)。
為什麼會選擇 32 個位元組?因為 Bitcoin 使用的是 ECDSA 演算法,並且使用的是 secp256k1 曲線。
譬如:對於 Java 實現我們可以使用 java.security.SecureRandom 來生成隨機數,如下為 SecureRandom 的預設構造方法,沒有設定 seed,使用預設的(支援 RNG 演算法的)provider 來生成:
/** * Constructs a secure random number generator (RNG) implementing the * default random number algorithm. * * <p> This constructor traverses the list of registered security Providers, * starting with the most preferred Provider. * A new SecureRandom object encapsulating the * SecureRandomSpi implementation from the first * Provider that supports a SecureRandom (RNG) algorithm is returned. * If none of the Providers support a RNG algorithm, * then an implementation-specific default is returned. * * <p> Note that the list of registered providers may be retrieved via * the {@link Security#getProviders() Security.getProviders()} method. * * <p> See the SecureRandom section in the <a href= * "{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/StandardNames.html#SecureRandom"> * Java Cryptography Architecture Standard Algorithm Name Documentation</a> * for information about standard RNG algorithm names. * * <p> The returned SecureRandom object has not been seeded.To seed the * returned object, call the {@code setSeed} method. * If {@code setSeed} is not called, the first call to * {@code nextBytes} will force the SecureRandom object to seed itself. * This self-seeding will not occur if {@code setSeed} was * previously called. */ public SecureRandom() { /* * This call to our superclass constructor will result in a call * to our own {@code setSeed} method, which will return * immediately when it is passed zero. */ super(0); getDefaultPRNG(false, null); }
也可以指定 SecureRandomSpi 實現類(implementation)和 provider,以及演算法:
/** * Creates a SecureRandom object. * * @param secureRandomSpi the SecureRandom implementation. * @param provider the provider. */ protected SecureRandom(SecureRandomSpi secureRandomSpi, Provider provider) { this(secureRandomSpi, provider, null); } private SecureRandom(SecureRandomSpi secureRandomSpi, Provider provider, String algorithm) { super(0); this.secureRandomSpi = secureRandomSpi; this.provider = provider; this.algorithm = algorithm; // BEGIN Android-removed: this debugging mechanism is not supported in Android. /* if (!skipDebug && pdebug != null) { pdebug.println("SecureRandom." + algorithm + " algorithm from: " + this.provider.getName()); } */ // END Android-removed }
上面原始碼是 Android 中 Java 的原始碼實現,因此會出現 // BEGIN Android-removed 等移除 Java 原始實現程式碼的註釋。
下面是使用預設構造生成的隨機數示例:
public static String generateRandomSeed(int seedLen) { SecureRandom secureRandom = new SecureRandom(); byte[] seed = new byte[seedLen]; secureRandom.nextBytes(seed); String seedHexStr = HexUtils.encodeHexString(seed); Log.i(TAG, "seed is " + seedHexStr); return seedHexStr; }
以下是上面的函式生成的一個隨機的以 16 進位制串表示的私鑰:
f7387afb5ae15aac9acccc8d8823c8a1c2443b72a042b2274fc7433b4f0dd2f4
也可以通過 bitcoin-cli 來生成一個私鑰和地址,如下:
$ bitcoin-cli -testnet --datadir=/var/bitcoin/testnet getnewaddress 2NBqPd4bmXEXXt3SqjxdMEYv3q46j6KMszq $ bitcoin-cli -testnet --datadir=/var/bitcoin/testnet dumpprivkey 2NBqPd4bmXEXXt3SqjxdMEYv3q46j6KMszq cQmW1c27ZFYy6XQxvUD6yMgbTm4cDSJYaaCM8NPKZZA4oMMdGB5X $ bx base58check-decode cQmW1c27ZFYy6XQxvUD6yMgbTm4cDSJYaaCM8NPKZZA4oMMdGB5X wrapper { checksum 2258650717 payload 5f115a837d4c782cd0e5214fe67a0434895dd7264e9949c40fe1cc129f1d9d2701 version 239 }
其中:
- getnewaddress 命令來生成錢包地址(內部已生成和儲存了私鑰);
- dumpprivkey 命令輸出對應錢包的 Base58Check 的 WIF 錢包匯入格式的私鑰;
- 最後一行命令是將解碼 Base58Check 格式的私鑰至 16 進位制格式
注:其中的 -testnet --datadir 等選項引數是在測試網路裡使用的。使用主網的可以忽略這些選項引數。
如何生成公鑰
Bitcoin 的公鑰是通過 橢圓曲線密碼學演算法(K = k * G)來生成,其中公式中的:
- K :公鑰;
- k :私鑰,為上一段生成的 32 位元組的位元組陣列(16 進位制串表示);
- G :為一個生成點;
Bitcoin 使用了 secp256k1 標準定義的一種特殊的橢圓曲線和一系列的數學常量。如上公式,以私鑰 k 為起點,與預定的生成點 G 相乘來生成公鑰 K,並且因為所有 Bitcoin 使用者的生成點 G 都是相同的(常量),所以由一個確定的私鑰 k 生成一個確定的公鑰 K,並且是單向的。
ofollow,noindex">Bitcoin 中使用的 secp256k1 標準細節參考
詳細的橢圓曲線公式的數學解釋與參考下面為使用 spongycastle 庫中提供的 EC 演算法庫來生成公鑰,分別支援 16 進位制串和位元組陣列格式的私鑰:
public static String generatePublicKey(byte [] privateKey, boolean compressed) { ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256k1"); ECPoint pointQ = spec.getG().multiply(new BigInteger(1, privateKey)); byte [] publicKeyBytes = pointQ.getEncoded(compressed); String publicKeyHexStr = HexUtils.encodeHexString(publicKeyBytes); Log.i(TAG, "==> public key is 0x" + publicKeyHexStr); return publicKeyHexStr; } public static String generatePublicKey(String privateKeyHexStr, boolean compressed) throws HexDecodeException { byte [] privateKeyBytes = HexUtils.decodeHex(privateKeyHexStr); return generatePublicKey(privateKeyBytes, compressed); }
如下為一個生成示例(16 進位制串表示,且手工加上了 0x 字首):
private key: de97fdbdb823a197603e1f2cb8b1bded3824147e88ebd47367ba82d4b5600d73 public key:047c91259636a5a16538e0603636f06c532dd6f2bb42f8dd33fa0cdb39546cf449612f3eaf15db9443b7e0668ef22187de9059633eb23112643a38771c630db911
壓縮格式的公鑰
從上面的輸出示例中可以看到 public key 一共有 130 個 16 進位制的字元,共 520 個位元組,其中的字首為 04 ,這裡的 04 表示該公鑰為 非壓縮格式 ,即完整儲存了 x 和 y 座標(各 256 個 bits),但是從 secp256k1 的橢圓曲線方式可以看到,只要知道其中一個座標值,另外一個座標值都是可以通過解方程得出的,因為可以只儲存其中一個座標,這樣就可以節約 256 個 bits,從而引入了 壓縮格式 的公鑰。
上面的 04 字首表示 非壓縮格式 ,如果為壓縮格式,則字首為 02 或 03 ,有兩個字首主要是因為方程(y² = x³ + ax + b)的左側的 y 為平方根,可能為正或者為負。
在上面提供的 generatePublicKey 方法中支援是否輸出壓縮格式,如下為一個與上面示例對應的壓縮格式的公鑰值:
private key: de97fdbdb823a197603e1f2cb8b1bded3824147e88ebd47367ba82d4b5600d73 public key compressed: 037c91259636a5a16538e0603636f06c532dd6f2bb42f8dd33fa0cdb39546cf449
如何生成地址
Bitcoin 的地址由公鑰經過單向的加密雜湊演算法 SHA256 和 RIPEMD160 生成,公式如下:
A = RIPEMD160(SHA256(K))
其中:
- K 為公鑰
- A 為最終生成的地址;
下面為根據 公鑰 生成的 bitcoin 錢包的地址(16 進製表示)的程式碼:
// RIPEMD160 ( SHA256 (publicKey) ) public static String generateAddress(byte [] publicKey) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte [] pubKeySha256 = digest.digest(publicKey); Log.i(TAG, "==> sha256: 0x" + HexUtils.encodeHexString(pubKeySha256)); digest = MessageDigest.getInstance("RIPEMD160"); byte [] bytesAddr = digest.digest(pubKeySha256); Log.i(TAG, "==> sha256: 0x" + HexUtils.encodeHexString(bytesAddr)); return HexUtils.encodeHexString(bytesAddr); } public static String generateAddress(String publicKeyHex) throws HexDecodeException, NoSuchAlgorithmException { return generateAddress(HexUtils.decodeHex(publicKeyHex)); }
如下為生成的地址示例,地址的長度為 40 個 16 進位制串,即 160 個bits:
private key: de97fdbdb823a197603e1f2cb8b1bded3824147e88ebd47367ba82d4b5600d73 public key compressed: 037c91259636a5a16538e0603636f06c532dd6f2bb42f8dd33fa0cdb39546cf449 address: 52dab5e951ef4848a31b7ead8437df8184acbc54
Base58, Base58Check 以及壓縮格式
我們通常看到的 Bitcoin 地址都是經過 Base58Check 編碼後的地址,Base58Check 編碼也用於私鑰,加密的金鑰以及指令碼中,用來提高可讀性和錄入的正確性。下圖描述了通過 公鑰生成 Base58Check 編碼格式的地址的整個過程:

其中 Public Key Hash 我們在上面已經生成的地址,之後就是通過 Base58Check 編碼生成 Bitcoin 的地址格式。
(摘自 wiki)相比Base64,Base58不使用數字"0",字母大寫"O",字母大寫"I",和字母小寫"l",以及"+“和”/"符號。
設計Base58主要的目的是:
- 避免混淆。在某些字型下,數字0和字母大寫O,以及字母大寫I和字母小寫l會非常相似。
- 不使用"+“和”/"的原因是非字母或數字的字串作為帳號較難被接受。
- 沒有標點符號,通常不會被從中間分行。
- 大部分的軟體支援雙擊選擇整個字串。
如下為 Base58 符號對映表:

為了進一步增加安全性,Base58Check 格式又在 Base58 的基礎上新增了內建檢查錯誤的校驗和(checksum),該校驗和是新增到末尾的額外 4 個位元組,校驗和的生成演算法如下:
checksum = SHA256(SHA256(prefix+data))
- data 為原始的資料;
- prefix 字首是個版本欄位,是用來識別編碼的資料的型別,如:Bitcoin 地址(也是 public key hash)的字首為 0(即 0x00),當前支援的如下型別:

其中 私鑰 的字首為 128(即 0x80),對應的編碼後字首為 5,如下為我們的私鑰編碼後的:
$ bx base58check-encode --version 128 de97fdbdb823a197603e1f2cb8b1bded3824147e88ebd47367ba82d4b5600d73 5KWKSRnmzxCjUP1NKR4dNyyHhaZWSGRTbGzBnm1vwgwpoe2AVGQ
下圖為 Base58Check 的編碼過程:

在 bitcoin 中大多數需要向用戶展示的資料都是使用的 Base58Check 編碼格式。
如下為根據 public key 生成 address 的實現程式碼:
static byte[] generateBase58CheckSum(byte[] data) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte [] dataOneHash = digest.digest(data); byte [] dataDoubleHash = digest.digest(dataOneHash); byte [] checkSum = Arrays.copyOf(dataDoubleHash, 4); Log.i(TAG, "==> base58check sum: " + HexUtils.encodeHexString(checkSum)); return checkSum; } // Base58Check(RIPEMD160(SHA256(publicKey)) public static String generateAddressWithBase58Check(byte [] publicKey) throws NoSuchAlgorithmException, HexDecodeException { String addrHex = generateAddress(publicKey); byte [] addrBytes = HexUtils.decodeHex(addrHex); byte [] checksum = generateBase58CheckSum(addrBytes); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(0);// address version prefix baos.write(addrBytes, 0, addrBytes.length); baos.write(checksum, 0, checksum.length); String addressWithBase58Check = Base58.encode(baos.toByteArray()); Log.i(TAG, "==> address with base58check format: " + addressWithBase58Check); return addressWithBase58Check; }
下面為私鑰、公鑰以及生成的 Base58Check 格式的地址資訊:
private key hex: de97fdbdb823a197603e1f2cb8b1bded3824147e88ebd47367ba82d4b5600d73 private key base58check: 5KWKSRnmzxCjUP1NKR4dNyyHhaZWSGRTbGzBnm1vwgwpoe2AVGQ public key compressed: 037c91259636a5a16538e0603636f06c532dd6f2bb42f8dd33fa0cdb39546cf449 checksum: 4caf1695 base58check address: 18Z6R1VF7Do8RTHneeGzdVdbgjtXDVPmfS
完整示例程式碼
如下為上面示例中生成私鑰, 公鑰以及地址的完整程式碼:
public class BitcoinUtils { public static final String TAG = BitcoinUtils.class.getName(); public static final boolean IS_PRODUCTION = false; public static NetworkParameters getNetParams() { return (IS_PRODUCTION ? MainNetParams.get() : TestNet3Params.get()); } public static String generateRandomSeed(int seedLen) { SecureRandom secureRandom = new SecureRandom(); byte[] seed = new byte[seedLen]; secureRandom.nextBytes(seed); String seedHexStr = HexUtils.encodeHexString(seed); Log.i(TAG, "==> seed is 0x" + seedHexStr); return seedHexStr; } public static String generatePublicKey(byte [] privateKey, boolean compressed) { ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256k1"); ECPoint pointQ = spec.getG().multiply(new BigInteger(1, privateKey)); byte [] publicKeyBytes = pointQ.getEncoded(compressed); String publicKeyHexStr = HexUtils.encodeHexString(publicKeyBytes); Log.i(TAG, "==> public key is 0x" + publicKeyHexStr); return publicKeyHexStr; } public static String generatePublicKey(String privateKeyHexStr, boolean compressed) throws HexDecodeException { byte [] privateKeyBytes = HexUtils.decodeHex(privateKeyHexStr); return generatePublicKey(privateKeyBytes, compressed); } // RIPEMD160 ( SHA256 (publicKey) ) public static String generateAddress(byte [] publicKey) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte [] pubKeySha256 = digest.digest(publicKey); Log.i(TAG, "==> sha256: 0x" + HexUtils.encodeHexString(pubKeySha256)); RIPEMD160Digest d = new RIPEMD160Digest(); d.update(pubKeySha256, 0, pubKeySha256.length); byte [] bytesAddr = new byte[d.getDigestSize()]; d.doFinal(bytesAddr, 0); Log.i(TAG, "==> ripemd160: 0x" + HexUtils.encodeHexString(bytesAddr)); return HexUtils.encodeHexString(bytesAddr); } public static String generateAddress(String publicKeyHex) throws HexDecodeException, NoSuchAlgorithmException { return generateAddress(HexUtils.decodeHex(publicKeyHex)); } static byte[] generateBase58CheckSum(byte[] data) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte [] dataOneHash = digest.digest(data); byte [] dataDoubleHash = digest.digest(dataOneHash); byte [] checkSum = Arrays.copyOf(dataDoubleHash, 4); Log.i(TAG, "==> base58check sum: " + HexUtils.encodeHexString(checkSum)); return checkSum; } // Base58Check ( RIPEMD160 ( SHA256 (publicKey) ) public static String generateAddressWithBase58Check(byte [] publicKey) throws NoSuchAlgorithmException, HexDecodeException { String addrHex = generateAddress(publicKey); byte [] addrBytes = HexUtils.decodeHex(addrHex); byte [] checksum = generateBase58CheckSum(addrBytes); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(0);// address version prefix baos.write(addrBytes, 0, addrBytes.length); baos.write(checksum, 0, checksum.length); String addressWithBase58Check = Base58.encode(baos.toByteArray()); Log.i(TAG, "==> address with base58check format: " + addressWithBase58Check); return addressWithBase58Check; } public static String generateAddressWithBase58Check(String publicKeyHex) throws HexDecodeException, NoSuchAlgorithmException { return generateAddressWithBase58Check(HexUtils.decodeHex(publicKeyHex)); } }