CryptocurrencyTechGolang进阶Bitcoin

btcd是如何计算字节占用的

2018-02-13  本文已影响21人  qishuai

下面以交易的字节占用为例,解析btcd是如何处理golang的字节占用问题的。

交易结构

一个交易的大小分成了隔离见证占用和非隔离见证占用,非隔离见证部分包括:交易版本号、交易输入的数量、交易输入本身、交易输出数量、交易输出本身、锁定时间。

交易的结构:

type MsgTx struct {
    Version  int32
    TxIn     []*TxIn
    TxOut    []*TxOut
    LockTime uint32
}

一个交易的字节占用可被拆解为:

描述 长度(byte)
版本 4
交易输入数量 1+
交易输入 41+
交易输出数量 1+
交易输出 9+
锁定时间 4
```
func (msg *MsgTx) SerializeSize() int {
    n := msg.baseSize()

    if msg.HasWitness() {
        // The marker, and flag fields take up two additional bytes.
        n += 2

        // Additionally, factor in the serialized size of each of the
        // witnesses for each txin.
        for _, txin := range msg.TxIn {
            n += txin.Witness.SerializeSize()
        }
    }

    return n
}
```

隔离见证数据在交易结构中位于交易输入结构中,但btcd单独计算了其数据占用情况,而没有和TxIn一起计算。

交易的字节占用

一个交易包括4字节版本号、4字节锁定时间、txin长度的变长整数占用、txout长度的变长整数占用,再加上txin和txout占用。

```
func (msg *MsgTx) baseSize() int {
    // Version 4 bytes + LockTime 4 bytes + Serialized varint size for the
    // number of transaction inputs and outputs.
    n := 8 + VarIntSerializeSize(uint64(len(msg.TxIn))) + VarIntSerializeSize(uint64(len(msg.TxOut)))

    for _, txIn := range msg.TxIn {
        n += txIn.SerializeSize()
    }

    for _, txOut := range msg.TxOut {
        n += txOut.SerializeSize()
    }

    return n
}
```

VarIntSerializeSize()函数对不同大小的整数规范了不同的字节占用,也是为了优化存储,以及网络传输成本。具体内容可以参考: Variable length integer

```
func VarIntSerializeSize(val uint64) int {
    // The value is small enough to be represented by itself, so it's
    // just 1 byte.
    if val < 0xfd {
        return 1
    }

    // Discriminant 1 byte plus 2 bytes for the uint16.
    if val <= math.MaxUint16 {
        return 3
    }

    // Discriminant 1 byte plus 4 bytes for the uint32.
    if val <= math.MaxUint32 {
        return 5
    }

    // Discriminant 1 byte plus 8 bytes for the uint64.
    return 9
}
```

代码理解:

  1. 传递一个uint64类型的整数参数
  2. 如果比0xfd小,那么就返回1,说明数据在一个字节内存储而不会溢出
  3. 如果比0xfd大,在分别和math.MaxUint16、math.MaxUint32比较,也就是2字节和4字节能容纳的最大正整数,如果在相应的范围,则返回对应的字节数量。需要注意的是2字节、4字节判断前需要分别有0xFD、0xFE标记,故返回的数据需要加1
  4. 如果前面的分支都没有进入,由于变长整数的最大值被规范在uin64范围内,因此该数据占用为8个字节,加上开始的1字节0xFF标记,故返回9

txin的字节占用了:包括32字节的outpoint hash、4字节的outpoint索引、4字节的序列号,总共40字节。再加上sigScript长度的变长整数占用,以及sigScript占用字节数。

```
func (t *TxIn) SerializeSize() int {
    // Outpoint Hash 32 bytes + Outpoint Index 4 bytes + Sequence 4 bytes +
    // serialized varint size for the length of SignatureScript +
    // SignatureScript bytes.
    return 40 + VarIntSerializeSize(uint64(len(t.SignatureScript))) +
        len(t.SignatureScript)
}
```

txout的字节占用:8字节转账金额、pkScript长度变长整数、pkScript占用字节数。

```
func (t *TxOut) SerializeSize() int {
    // Value 8 bytes + serialized varint size for the length of PkScript +
    // PkScript bytes.
    return 8 + VarIntSerializeSize(uint64(len(t.PkScript))) + len(t.PkScript)
}
```

btcd在计算时加入了很多变长整数的字节占用,但是在其交易结构里面并没有对txin、txout的长度统计的字段,在使用该长度信息时在进行统计。比如在交易的序列化的过程中,对相应数据的长度进行了统计和写入。

```
func (msg *MsgTx) BtcEncode(w io.Writer, pver uint32, enc MessageEncoding) error {
    ...
    
    // 计算txin的数量,并写入到io.Writer
    count := uint64(len(msg.TxIn))
    err = WriteVarInt(w, pver, count)
    if err != nil {
        return err
    }

    for _, ti := range msg.TxIn {
        err = writeTxIn(w, pver, msg.Version, ti)
        if err != nil {
            return err
        }
    }

    // 计算txout的数量,并写入io.Writer
    count = uint64(len(msg.TxOut))
    err = WriteVarInt(w, pver, count)
    if err != nil {
        return err
    }

    for _, to := range msg.TxOut {
        err = WriteTxOut(w, pver, msg.Version, to)
        if err != nil {
            return err
        }
    }
    
    ...
```

下面通过一张图来说明整个流程


计算流程图

总结:btcd的这种统计内存占用的方式,其实和golang的内存分配和布局已经没有什么关联了,只是对bitcoin protocol的原始实现。虽然并不能体现其真实的底层内存占用情况,但是在一定程度反映了内存占用的变化情况。另外,在统计[]byte是并不是使用unsafe.SizeOf()函数简单实现,而是通过统计[]byte的长度,更贴近于真实情况,虽然golang真实占用比这个值要大。

上一篇下一篇

猜你喜欢

热点阅读