在解析前,需要准备比特币区块数据。可在安装比特币QT客户端后,到指定目录下获取。注意不需要全部同步完成,只需要开启同步后,有第一个区块数据即可。文件路径,分别是:
- Mac: $HOME/Library/Application Support/Bitcoin/blocks/blkXXXX.dat
- Windows: %APPDATA%/Bitcoin/blocks/blkXXXX.dat
通过Go代码的方式,一步一步讲解比特币区块的解析逻辑。 下图是比特币区块数据结构,主要由区块头和交易清单组成。
项 | name | 字节 | 用途 |
---|---|---|---|
神奇数 | magic | 4 | 固定为0xD9B4BEF9,作为区块间的分隔符 |
区块大小 | block_size | 4 | 记录当前区块的大小 |
区块头信息 | header | 80 | 记录当前区块的头部信息 |
交易总数 | tx_count | 1-9 | 当前区块所含记录数 |
交易清单 | tx_list | * | 记录当前区块交易细节 |
其中,神奇数固定为0xD9B4BEF9
,作为区块的分隔标识符。而比特币的交易数据是以128Mb为上限记录在dat文件中的。利用此神奇数和区块大小,可将区块数据读取。
先打开文件blk00001.bat
f, err := os.Open(name)
if err != nil {
log.Fatal(err)
}
defer f.Close()
再按分隔符读取区块数据:
for {
mg := ReadUint32(f)
if mg != MARGIC { // MARGIC uint32 = 0xd9b4bef9
Failt(fmt.Errorf("expect margic %x,not %x", margic, mg))
}
//get block size
blockSize := ReadUint32(f)
//get block bytes
blockData := ReadBytes(f, uint64(blockSize))
}
区块头信息
区块头是重要的信息,保障了区块内容的不可篡改。因此一些轻钱包,可不下载完整的区块,而只需获取区块头,便可验证交易。
项 | 字节 | 用途 |
---|---|---|
版本号 | 4 | 区块的版本号 |
上一区块头Hash | 32 | 上一区块的 SHA256 Hash值,以确保上一区块无法修改,继续加强稳定性 |
Merkle树根值 | 32 | 交易Merkle树根节点Hash |
时间戳 | 4 | 区块生成时间UNIX格式 |
目标值 | 4 | 本区块头Hash必须>=本值 |
随机数 | 4 | 一个使得本区块头Hash>=目标值的任意数。 |
区块头信息中记录上一个区块头HASH值,是对历史交易的再次确认。比特币中只有在6次确认后才交易稳定。
那么此Hash是对头部所有内容经过两次SHA256加密获得,而在内容中含有交易记录的Merkle数根值,可以保障交易内容摘要也在此HASH范围内。注意每个区块的HASH并不存储在当前区块中,其他区块要引用此区块时需对此区块头进行HASH计算。
// head
head := decodeHeader(r)
head.Hash = DoubleHashH(blockData[:80]) //头部80个字节
func decodeHeader(r io.Reader) (h BlockHeader) {
h.Version = ReadUint32(r)
h.PrevBlock = ReadHash(r)
h.MerkleRoot = ReadHash(r)
h.Timestamp = time.Unix(int64(ReadUint32(r)), 0)
h.Bits = ReadUint32(r)
h.Nonce = ReadUint32(r)
return
}
此处的Merkle树便是一个Hash二叉树。本区块中所有交易,按时间顺序将单笔交易的HASH值(TXID)两两组成一个二叉树的叶子,根便是最终的Hash值。 通过对每笔交易进行HASH签名,构成唯一的根HASH存放在区块头中,以确保每笔交易不可伪造,不可重复。
交易记录
交易记录便是整个账簿的一页内容,详细记录当下所有比特币交易信息。每个比特币账户下的收支被永久的记录在区块中。
在解析交易记录前,需要获取总记录数,这个总记录数是一个不固定长度数值。
// transaction
txCount := ReadVarInt(r)
txs := make([]*MsgTx, txCount)
for i := uint64(0); i < txCount; i++ {
txs[i] = decoderTX(r)
}
项 | name | 字节 | 用途 |
---|---|---|---|
版本 | version | 4 | 比特币协议版本 |
输入数 | tx_in_count | 1+ | 区块中所有输入记录数 |
输入明细 | tx_in | 40+ | 区块中所有支出明细 |
输出数 | tx_out_count | 1+ | 区块中所有输出记录数 |
输出明细 | tx_out | 40+ | 区块中所有输出明细 |
交易时间戳 | local_time | 4 | 交易被网络确认的UNIX时间 |
其中,支出和接收明细是衔接在一块,拥有相同的数据结构。每一单笔收支明细都有自己的编号以供查询。
msg := &MsgTx{}
msg.Version = ReadUint32(r)
txInCount := ReadVarInt(r)
msg.TxIn = make([]*TxIn, txInCount)
for i := uint64(0); i < txInCount; i++ {
//input
}
txOutCount := ReadVarInt(r)
msg.TxOut = make([]*TxOut, txOutCount)
for i := uint64(0); i < txOutCount; i++ {
//output
}
msg.LockTime = ReadUint32(r)
输出和输入明细稍有不同,输入:
项 | name | 字节 | 用途 |
---|---|---|---|
输出记录Hash | hash | 32 | 本输入资金来源的输出记录HASH |
脚本长度 | length | 1+ | 交易脚本可变长度值 |
脚本内容 | script | + | 本输入的脚本内容,包含签名信息 |
Sequence | 4 | 当前无用途的一个固定数值:0xFFFFFFFF |
tx := TxIn{}
// prev
tx.PreviousOutPoint.Hasx = ReadHash(r)
tx.PreviousOutPoint.Index = ReadUint32(r)
scriptLen := ReadVarInt(r)
tx.SignatureScript = ReadBytes(r, scriptLen)
tx.Sequence = ReadUint32(r)
输出信息仅仅包含两项信息,
项 | name | 字节 | 用途 |
---|---|---|---|
金额 | value | 8 | 接收方获得的金额,单位:聪 |
公钥脚本长度 | length | 1+ | 交易脚本可变长度值 |
公钥脚本 | length | 接收方信息脚本 |
tx := TxOut{}
tx.Value = ReadUint64(r)
scriptLen := ReadVarInt(r)
tx.PkScript = ReadBytes(r, scriptLen)
区块完整数据结构
比特币区块数据以节约空间和简洁为前提,布局为一个区块头和交易明细结构。下图是完整的区块结构。
协助
实际在解析比特币区块时,为简化代码,提取了些公共方法。以方便错误处理和获取不同类型的数据,具体如下:
type Hash [HashSize]byte
const (
HashSize = 32
)
// String returns the Hash as the hexadecimal string of the byte-reversed
// hash.
func (hash Hash) String() string {
for i := 0; i < HashSize/2; i++ {
hash[i], hash[HashSize-1-i] = hash[HashSize-1-i], hash[i]
}
return hex.EncodeToString(hash[:])
}
func DoubleHashH(b []byte) Hash {
first := sha256.Sum256(b)
return Hash(sha256.Sum256(first[:]))
}
func Failt(err error) {
log.Output(2, err.Error())
os.Exit(1)
}
常见的读取单项值方法:
func ReadHash(r io.Reader) Hash {
var h Hash
_, err := io.ReadFull(r, h[:])
if err != nil {
Failt(err)
}
return h
}
func ReadBytes(r io.Reader, size uint64) []byte {
b := make([]byte, size)
_, err := io.ReadFull(r, b)
if err != nil {
Failt(err)
}
return b
}
func ReadUint32(r io.Reader) uint32 {
var v uint32
err := binary.Read(r, binary.LittleEndian, &v)
if err != nil {
Failt(err)
}
return v
}
func ReadUint64(r io.Reader) uint64 {
var v uint64
err := binary.Read(r, binary.LittleEndian, &v)
if err != nil {
Failt(err)
}
return v
}
// ReadVarInt reads a variable length integer from r and returns it as a uint64
func ReadVarInt(r io.Reader) uint64 {
var discriminant uint8
err := binary.Read(r, binary.LittleEndian, &discriminant)
if err != nil {
Failt(err)
}
var rv uint64
switch discriminant {
case 0xff:
err = binary.Read(r, binary.LittleEndian, &rv)
if err != nil {
Failt(err)
}
// The encoding is not canonical if the value could have been
// encoded using fewer bytes.
min := uint64(0x100000000)
if rv < min {
Failt(fmt.Errorf("var int %x < min value %x", rv, min))
}
case 0xfe:
var v uint32
err = binary.Read(r, binary.LittleEndian, &v)
if err != nil {
Failt(err)
}
rv = uint64(v)
// The encoding is not canonical if the value could have been
// encoded using fewer bytes.
min := uint64(0x10000)
if rv < min {
Failt(fmt.Errorf("var int %x < min value %x", rv, min))
}
case 0xfd:
var v uint16
err = binary.Read(r, binary.LittleEndian, &v)
if err != nil {
Failt(err)
}
rv = uint64(v)
// The encoding is not canonical if the value could have been
// encoded using fewer bytes.
min := uint64(0xfd)
if rv < min {
Failt(fmt.Errorf("var int %x < min value %x", rv, min))
}
default:
rv = uint64(discriminant)
}
return rv
}
结尾
上文中还有许多内容未详细说明,如脚本是什么?Merkle树如何构建的?交易费如何计算?交易中的脚本的用途等。待后续一一写文。
本文完整源代码见:github gist