跳到主要内容

比特币相关算法

区块链常用哈希算法

常用的哈希算法以及它们的输出长度如下:

哈希算法输出长度(bit)输出长度(字节)
MD5128 bit16 bytes
RipeMD160160 bits20 bytes
SHA-1160 bits20 bytes
SHA-256256 bits32 bytes
SHA-512512 bits64 bytes

比特币使用的哈希算法有两种:SHA-256 和 RipeMD160

SHA-256 的理论碰撞概率是:尝试 2 的 130 次方的随机输入,有 99.8% 的概率碰撞。

注意 21302^{130} 是一个非常大的数字,大约是 1361万亿亿亿亿。以现有的计算机的计算能力,是不可能在短期内破解的。

比特币使用两种哈希算法:

  • 一种是对数据进行两次 SHA-256 计算,这种算法在比特币协议中通常被称为 hash256 或者 dhash。
  • 另一种算法是先计算 SHA-256,再计算 RipeMD160,这种算法在比特币协议中通常被称为 hash160。
package main

import (
"crypto/sha256"
"fmt"
"hash"

"golang.org/x/crypto/ripemd160"
)

func main() {
s := "bitcoin is awesome"
// the %x is print in lower-case hexadecimal.
fmt.Printf("hash160 = %x\n", Hash160([]byte(s)))
fmt.Printf("hash256 = %x\n", Hash256([]byte(s)))
}

// Calculate the hash of hasher over buf.
func calcHash(buf []byte, hasher hash.Hash) []byte {
hasher.Write(buf)
return hasher.Sum(nil)
}

// Hash160 calculates the hash ripemd160(sha256(b)).
func Hash160(buf []byte) []byte {
return calcHash(calcHash(buf, sha256.New()), ripemd160.New())
}

// Hash256 calculates the hash sha256(sha256(b)).
func Hash256(buf []byte) []byte {
return calcHash(calcHash(buf, sha256.New()), sha256.New())
}

output

hash160 = fe56649aa4f8fdb1edf6b88d2d41f3c1f72cf431
hash256 = 1c78f53758ac96f43b99ed080f36327d2a823c4df4fa094e59b006d945bbb84d

比特币私钥

在比特币中,私钥本质上就是一个 256 位的随机整数。通过椭圆曲线算法生成私钥

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"log"
)

func main() {
curve := elliptic.P256()
private, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
log.Panic(err)
}

// 打印私钥:
fmt.Printf("private key = %d\n", private.D)
// 以十六进制打印:
fmt.Printf("hex = %x\n", private.D)
}

output

private key = 42460593681250629878673149152767221862207279422129710803353970316369332023366
hex = 5ddfd828717eeca20b7d886b34988ef0bda2f7fdcec2a5d6751cb9b4e08c3046

或者直接使用工具包生成

import (
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"fmt"
)

func GenerateBTC() (string, string, error) {
privKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
return "", "", err
}

privKeyWif, err := btcutil.NewWIF(privKey, &chaincfg.MainNetParams, false)
if err != nil {
return "", "", err
}
pubKeySerial := privKey.PubKey().SerializeUncompressed()

pubKeyAddress, err := btcutil.NewAddressPubKey(pubKeySerial, &chaincfg.MainNetParams)
if err != nil {
return "", "", err
}

return privKeyWif.String(), pubKeyAddress.EncodeAddress(), nil
}

WIF 钱包导入格式

每次运行上述程序,都会生成一个随机的 ECPair,即每次生成的私钥都是不同的。256 位的整数通常以十六进制表示,想要记住一个 256 位的整数是非常困难的,并且,如果记错了其中某些位,这个记错的整数仍然是一个有效的私钥,因此,比特币有一种对私钥进行编码的方式,这种编码方式就是带校验的 Base58 编码。

对私钥进行 Base58 编码有两种方式,一种是非压缩的私钥格式,一种是压缩的私钥格式,它们分别对应非压缩的公钥格式和压缩的公钥格式。

具体地来说,非压缩的私钥格式是指在32字节的私钥前添加一个 0x80 字节前缀,得到 33 字节的数据,对其计算 4 字节的校验码,附加到最后,一共得到 37 字节的数据:

0x80           256bit             check
┌─┬──────────────────────────────┬─────┐
│1│ 32 │ 4 │
└─┴──────────────────────────────┴─────┘

计算校验码非常简单,对其进行两次 SHA256,取开头 4 字节作为校验码。对这 37 字节的数据进行 Base58 编码,得到总是以 5 开头的字符串编码,这个字符串就是我们需要非常小心地保存的私钥地址,又称为钱包导入格式:WIF(Wallet Import Format)

20220706153208

另一种压缩格式的私钥编码方式,与非压缩格式不同的是,压缩的私钥格式会在32字节的私钥前后各添加一个0x80字节前缀和0x01字节后缀,共34字节的数据,对其计算4字节的校验码,附加到最后,一共得到38字节的数据:

0x80           256bit           0x01 check
┌─┬──────────────────────────────┬─┬─────┐
│1│ 32 │1│ 4 │
└─┴──────────────────────────────┴─┴─────┘
package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"log"

"github.com/btcsuite/btcutil/base58"
)

func main() {
curve := elliptic.P256()
private, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
log.Panic(err)
}
keyStr := fmt.Sprintf("%x", private.D)
// 原始的私钥
fmt.Printf("%s\n", keyStr)

//得到 wif 格式的私钥
wifPrivateKey := base58.Encode(private.D.Bytes())
fmt.Printf("%s\n", wifPrivateKey)

// 解码 wif 格式的私钥
de := base58.Decode(wifPrivateKey)
fmt.Printf("%x\n", de)
}

output

c6c9a3741105e7bb88a3cf8374770b00b04caf515dbd521da3700104b056501c
ENz49XdryFuzaCgoXNAKiV9cdByK61X39VopdGCNXR5u
c6c9a3741105e7bb88a3cf8374770b00b04caf515dbd521da3700104b056501c
  • 使用非压缩格式的 WIF 是以 5 开头的字符串。
  • 使用压缩格式的 WIF 是以 K 或 L 开头的字符串。

目前,非压缩的格式几乎已经不使用了,所以上面使用的那个工具包生成的 wif 默认是压缩的。

比特币公钥

比特币的公钥是根据私钥计算出来的。

非压缩的公钥

私钥本质上是一个 256 位整数,记作 k。根据比特币采用的 ECDSA 算法,可以推导出两个 256 位整数,记作 (x, y),这两个 256 位整数即为非压缩格式的公钥。

package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"log"
)

func main() {
curve := elliptic.P256()
private, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
log.Panic(err)
}

pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)
fmt.Printf("%x\n", pubKey)
}

output

65e447801e747cca43cf907e2380d4e3873eba27456397510ef45f96d5a60415338700eb2b81ccfec90b7781e968c75238a1428ab66691a0e001e446460b836f

压缩的公钥

由于 ECC 曲线的特点,根据非压缩格式的公钥 (x, y)x 实际上也可推算出 y,但需要知道 y 的奇偶性,因此,可以根据 (x, y) 推算出 x',作为压缩格式的公钥。

压缩格式的公钥实际上只保存 x 这一个 256 位整数,但需要根据 y 的奇偶性在 x 前面添加 0203 前缀,y 为偶数时添加 02,否则添加 03,这样,得到一个 1+32=33 字节的压缩格式的公钥数据,记作 x'

注意压缩格式的公钥和非压缩格式的公钥是可以互相转换的,但均不可反向推导出私钥。

比特币的地址

要特别注意,比特币的地址并不是公钥,而是公钥的哈希,即从公钥能推导出地址,但从地址不能反推公钥,因为哈希函数是单向函数。

提示

如果想知道为啥还需要把公钥 Hash 一次生成地址(钱包地址),则看这篇回答 为什么比特币地址不直接使用公钥,而需要通过哈希生成?为什么每次付款都应该设置找零地址?

以压缩格式的公钥为例,从公钥计算地址的方法是,首先对 1+32=33 字节的公钥数据进行 Hash160(即先计算 SHA256,再计算 RipeMD160),得到 20 字节的哈希。然后,添加 0x00 前缀,得到 1+20=21 字节数据,再计算 4 字节校验码,拼在一起,总计得到 1+20+4=25 字节数据:

0x00      hash160         check
┌─┬──────────────────────┬─────┐
│1│ 20 │ 4 │
└─┴──────────────────────┴─────┘

对上述 25 字节数据进行 Base58 编码,得到总是以 1 开头的字符串,该字符串即为比特币地址,整个过程如下:

20220706163112

package main

import (
"fmt"

"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
)

func main() {
pv, pu, _ := GenerateBTC()
fmt.Println(pv)
fmt.Println(pu)
}

func GenerateBTC() (string, string, error) {
privKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
return "", "", err
}

privKeyWif, err := btcutil.NewWIF(privKey, &chaincfg.MainNetParams, false)
if err != nil {
return "", "", err
}
pubKeySerial := privKey.PubKey().SerializeUncompressed()

pubKeyAddress, err := btcutil.NewAddressPubKey(pubKeySerial, &chaincfg.MainNetParams)
if err != nil {
return "", "", err
}

return privKeyWif.String(), pubKeyAddress.EncodeAddress(), nil
}

output

5J6ynJ4dCVNxxzqxNZ9pTgMABsqJE1kstxsLCG9Xn7b5dTmT6F2
12mSqonxjhDqpmu8MHdnJqLQrkpxyQqFKF

私钥、公钥以及地址的推导关系如下:

┌───────────┐      ┌───────────┐
│Private Key│─────▶│Public Key │
└───────────┘ └───────────┘
▲ │
│ │
▼ ▼
┌───────────┐ ┌───────────┐
│ WIF │ │ Address │
└───────────┘ └───────────┘

签名算法

签名算法是使用私钥签名,公钥验证的方法,对一个消息的真伪进行确认。如果一个人持有私钥,他就可以使用私钥对任意的消息进行签名,即通过私钥 sk 对消息 message 进行签名,得到 signature:

signature = sign(message, sk);

签名的目的是为了证明,该消息确实是由持有私钥 sk 的人发出的,任何其他人都可以对签名进行验证。验证方法是,由私钥持有人公开对应的公钥pk,其他人用公钥 pk 对消息 message 和签名 signature 进行验证:

isValid = verify(message, signature, pk);

如下所示:

package main

import (
"encoding/hex"
"fmt"

"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
)

func main() {
// Decode a hex-encoded private key.
pkBytes, err := hex.DecodeString("22a47fa09a223f2aa079edf85a7c2d4f87" +
"20ee63e502ee2869afab7de234b80c")
if err != nil {
fmt.Println(err)
return
}

privKey, pubKey := btcec.PrivKeyFromBytes(btcec.S256(), pkBytes)

// Sign a message using the private key.
message := "test message"
messageHash := chainhash.DoubleHashB([]byte(message))
signature, err := privKey.Sign(messageHash)
if err != nil {
fmt.Println(err)
return
}

// Serialize and display the signature.
fmt.Printf("Serialized Signature: %x\n", signature.Serialize())

// Verify the signature for the message using the public key.
verified := signature.Verify(messageHash, pubKey)
fmt.Printf("Signature Verified? %v\n", verified)

}

References