bitcoin地址是如何生成的


> ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169726/3bde7778-917f-d068-79e7-2d757ae8d396.jpeg)

手动生成 Bitcoin 地址看似有点儿事倍功半，如果你了解了这个过程，就会明白眼花缭乱的 Bitcoin 分叉币地址只是私钥的不同显示方式，对了解 Eth EOS 地址也很有帮助，也可以更清楚的了解 Bitcoin 是如何花费的，一通则百通，妙哉。

我们使用私钥 `0xccea9c5a20e2b78c2e0fbdd8ae2d2b67e6b1894ccb7a55fc1de08bd53994ea64` 生成了一个主网地址`14xfJr1DArtYR156XBs28FoYk6sQqirT2s `。

生成 Bitcoin 地址，先由私钥生成公钥，再有公钥生成 hash160，最后 hash160 再进行 base58 运算得到地址，如下图所示：
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169726/d22a3e65-9e17-d8e3-f877-5e37b5e4f187.png)


由私钥得到公钥，是由 ECDSA 实现的。ECDSA 是`Elliptic Curve Digital Signature Algorithm`的缩写，即**椭圆曲线数字签名算法**。

**椭圆曲线其实不是通常认为的椭圆**，而是类似下面的模样：
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169726/efd5637e-f5c4-6b0d-e2f2-83334766ec97.png)

比特币用到的椭圆曲线是 secp256k1： $y^2=x^3+7$

## 生成比特币地址

言归正传，正式进入主题，我将这个过程分成了 8 个步骤。

### Step1. 生成私钥

Bitcoin 要使用到 Secp256k1 这条特殊的椭圆曲线得到公私钥，
我们通过 OpenSSL 命令来生成私钥。

```shell
openssl ecparam -name secp256k1 -genkey > priv.pem

# 输出DER格式
openssl ec -in priv.pem -outform DER | tail -c +8 | head -c 32 | xxd -p -c 32

# 结果
ccea9c5a20e2b78c2e0fbdd8ae2d2b67e6b1894ccb7a55fc1de08bd53994ea64
```
这里的结果是个 16 进制数据
`0xccea9c5a20e2b78c2e0fbdd8ae2d2b67e6b1894ccb7a55fc1de08bd53994ea64`

更快捷的方式是 `openssl rand 32 -hex`

### Step2. 生成公钥

通过 priv.pem 生成 pub_key

```shell
openssl ec -in priv.pem -pubout -outform DER | tail -c 65 | xxd -p -c 65

# 输出未压缩公钥
04d061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f691757b28e31be71f09f24673eed52348e58d53bcfd26f4d96ec6bf1489eab429d
```

#### 得到压缩公钥
```shell
openssl ec -in priv.pem -pubout -conv_form compressed -outform DER | tail -c 33 | xxd -p -c 33

# 输出
03d061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f69
```

输出 DER 格式，字符长度是 130

pub_key = `04d061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f691757b28e31be71f09f24673eed52348e58d53bcfd26f4d96ec6bf1489eab429d`

这是未压缩公钥，压缩公钥是 `03d061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f69 `

#### 一步到位生成私钥、公钥

```
openssl ecparam -genkey -name secp256k1 -text -noout -outform DER | \
xxd -p -c 1000 | \
sed 's/41534e31204f49443a20736563703235366b310a30740201010420/PrivKey: /' | \
sed 's/a00706052b8104000aa144034200/\'$'\nPubKey: /'
```

详细内容请看[压缩公钥与非压缩公钥](https://qiita.com/shooter/items/9613a154f61dfe3a69fd)

### Step3. 第 2 步结果进行 hash160 运算
**hash160 运算** 就是先进行 SHA256，再进行 RMD160。

```ruby
bytes = [pub_key].pack("H*") # 转为 16 进制
hash160_val = Digest::RMD160.hexdigest(Digest::SHA256.digest(bytes) )
```

hash160_val = `2b6f3b9e337cedbb7c40839523fb1100709c12f7`

### Step4. 第 3 步结果加上前缀符

普通的主网地址的前缀符是`00`，Bitcoin 地址前缀符有很多种，具体看 https://en.bitcoin.it/wiki/List_of_address_prefixes

```ruby
'00'+ '2b6f3b9e337cedbb7c40839523fb1100709c12f7'
```

step_04 = `002b6f3b9e337cedbb7c40839523fb1100709c12f7`

### Step5. 第 4 步结果，执行 2 次 SHA256, 取前 8 位作为校验和

```ruby
hex_str = [step_04].pack("H*")
checksum = Digest::SHA256.hexdigest(Digest::SHA256.digest(hex_str) )[0...8]
```

得到 checksum = `86b2e90c`

### Step6. 第 4 步结果跟第 5 步结果合并

```ruby
'002b6f3b9e337cedbb7c40839523fb1100709c12f7' + '86b2e90c'
# step_04 + checksum
```

step_06 = `002b6f3b9e337cedbb7c40839523fb1100709c12f786b2e90c`

### Step7. Base58 编码

Base58 是一种独特的编码方式，是 Base64 的变形，主要用于 Bitcoin 的钱包地址。相比 Base64，Base58 去掉了数字`0`，大写字母`O`，大写字母`I`，小写字母`l`，`+`和`/`，避免引起视觉混淆。

来个 base58 算法

```ruby
def encode_base58(int_val, leading_zero_bytes=0)
  alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
  base58_val, base = '', alpha.size

  while int_val > 0
    int_val, remainder = int_val.divmod(base)
    base58_val = alpha[remainder] + base58_val
  end

  base58_val
end
```

### Step8. 第 6 步结果进行 base58 编码

```ruby
step_06 = "002b6f3b9e337cedbb7c40839523fb1100709c12f786b2e90c"
leading_zero_bytes = (step_06.match(/^([0]+)/) ? $1 : '').size / 2
# leading_zero_bytes 的作用是字母填充
address = ("1" * leading_zero_bytes) + encode_base58(step_06.to_i(16) )
```

得到 `14xfJr1DArtYR156XBs28FoYk6sQqirT2s`，这是一个标准的 bitcoin 地址，终于大功告成。

> 思考：通过 btc 地址能反向得到 hash160_val 么？

汇总代码： [gen_addr.rb](https://github.com/liushooter/learn-blockchain/blob/master/gen_addr.rb)

**可以使用 [bitcoin-ruby](https://github.com/lian/bitcoin-ruby) 生成地址**

```ruby
require 'bitcoin'

pri_key, pub_key = Bitcoin.generate_key # 私钥 公钥
# 通过 ffi 调用 OpenSSL 得到，很多类库都是这么做的
address = Bitcoin::pubkey_to_address(pub_key)
```

**生成测试网地址**

```ruby
# ruby code
require 'bitcoin'

Bitcoin::network = :testnet #使用测试网
pri_key, pub_key = Bitcoin.generate_key
address = Bitcoin::pubkey_to_address(pub_key)
```

在 Bitcoin 系统中，私钥能得公钥，公钥能得到钱包地址，
私钥=>公钥=>钱包地址，而反向是不可以的。
**牢记你的私钥**，而且**私钥不能修改**，**地址也不能修改**，谁掌握了私钥谁就拥有了这些币!!!

## python3 code

```python
## https://github.com/fortesp/bitcoinaddress

from bitcoinaddress import Wallet

wallet = Wallet()
priv_key = wallet.key.hex

print("priv_key: ", priv_key)
print(wallet)

testwallet = Wallet(hash_or_seed=priv_key, testnet=True)
print(testwallet)
```

---

参考：

https://bitcoin.stackexchange.com/a/111651
[How do these OpenSSL commands create a Bitcoin private/key from a ECDSA keypair](https://bitcoin.stackexchange.com/a/59646)
[How to convert an ECDSA key to PEM format ](https://stackoverflow.com/a/49213805)

https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses
https://en.bitcoin.it/wiki/Address
https://en.bitcoin.it/wiki/List_of_address_prefixes
https://github.com/liushooter/learn-blockchain/blob/master/gen_addr.rb