Home 比特币-交易体
Post
Cancel

比特币-交易体

这次学习下比特币交易体,主要包括交易体的数据结构、签名的几种类型和分别是如何产生的。

本人知识有限,如有错误和疏漏,请务必指正,多谢。

前言

交易体位于区块体内,一个区块体内包含很多笔交易,每笔交易都是一个交易体。

为什么想要介绍下交易体的结构呢,有两个方面的原因:

  • 交易体内包含签名,而签名的流程是 TEE 钱包需要保护的内容,有的客户需要我们将整个交易体的打包都放到 TA 内部来做,所以这里需要了解整个流程。
  • 交易体的签名是有很多种类型的,类型不是指签名的算法,而是签名的范围(具体的后面会介绍),这个跟自己很感兴趣的比特币的合约密切相关,自己也打算近期写个相关的文章,所以这也是一个原因。

下面的介绍中,先从详细介绍比特币 transaciton wiki 的一张图开始,并会分析下 bitcoinj 的相关部分代码。

结构体

就是下面这张完整的 transation 的结构图:

结构体包含四个部分,版本、TxIns、TxOuts 和 锁定时间,TxIns 里可以包含多个 TxIn,TxOuts 也一样。下面就一部分一部分的分解开看。

1. 版本

开头的 Version 就是交易体的版本信息,4 个字节,目前是默认的 01,因为是小端顺序,所以是 01000000。

2. TxIns

  • 第一个 VI,TxIn 的总数量,这个是可变长度 Int (Var Int),1 - 9 个字节,主要是看数量的多少

    如果数量小于 0xFD,那么就只有一个字节长度,本身就是数量的值。

    如果数量大于0xFD,那么第一个字节就是一个标示符,代表的是后面跟着的字节是什么类型的 Int,比如 uint16、uint32、uint64。

    这个就是可变长度 Int,主要是为了缩减数据长度,VI 在整个交易体中出现很多次,如果每个 VI 不做可变长度,而是按照 uint64 的 8 字节固定长度,那么每个 VI 最多浪费了 7 个字节,一个交易体中,就算只有一个输入一个输出,也会浪费掉 3 * 7 = 21 字节,一个 block 算包含 1000 条交易体,那么一个 block 浪费掉的字节数为 21 * 1000 = 21K,现在由 50 多万个 block,大概一共会浪费掉 10 多 G。

    这样的设计确实重要,比特币中还有一些。

  • TxOutHash,32 字节,我们知道这次花费的币是来源于上一次交易得到的,所以这里指的是上一次交易的整个交易体的 hash。如果这个交易体对应的是挖矿奖励(区块记录的第一笔交易),那么因为币是凭空产生的,这里没有来源,所以都是 0 。

  • TxOutIndex,4 字节,每个交易体可能会包含很多个 TxOut,其中一个是这次花费的币的来源,所以这里指的是上一次交易体中这个 TxOut 的索引。和上一个参数一起,就可以唯一确定出来,币的来源。如果是挖矿奖励,这里是 0xffffffff。

  • ScriptLen,可变长度(VI),代表的是后面的 script 的总长度。

  • Script,这里有三种类型,Sig&PubKey、单独的 Signature 或 Arbitrary Data(抽象的数据)。 一般的交易(Standard TxIn),这里是 Sig&PubKey,也就是签名和公钥。 如果花费的是挖矿产生的从未交易过的币(Spend Coinbase),这里是 Signature,就是只提供签名即可,这个跟后面 TxOut 的对应。 如果是挖矿奖励,那么这里可以填入挖矿者自定义的一些数据。

    Script 是如何生成的,后面会介绍。

  • Sequence,一个序号,和后面会说的 locktime 配合使用。

    一般情况是 locktime 为 0, sequence 是 UINT_MAX,代表交易会被立即执行。

    如果 locktime 不为 0,sequence 是 UINT_MAX, 那么交易也会被立即执行。

    如果 locktime 不为 0, sequence 不是 UINT_MAX, 那么交易会等到 locktime 代表的时间或者块高度时,才会被执行,而且如果在还未执行的时候,出现 sequence 更大的相同交易,那么这次的交易会被取消掉。

3. TxOuts

  • VI,还是代表的是 TxOuts 的数量
  • Value,8 个字节,代表的是发送的比特币的数量,单位是 \(1e^{-8} BTC\),也就是一亿分之一比特币( 1 聪)
  • VI,PkScript 长度。
  • Script,有两种类型,Recipient Address 和 Recipient Public Key,分别对应于,Standard TxOut Script 和 Coinbase TxOut Script,代表,普通的交易和挖矿的奖励。 Script 就是赎回脚本了,里面包含脚本操作符和地址或者公钥信息。

4. LockTime

锁定时间,这个和之前的 sequence 配合使用。

LockTime 可以代表两个意思,一个是时间,一个是块数量

签名

签名就是指 TxIn 里的 Sig 或 Signature,签名需要两个部分,Key 和 待签的数据,Key 很好确定,就是用户的私钥,数据就有一些麻烦,到底应该签哪些内容呢?是不是整个交易体都需要签呢?交易体多个 TxIn 里的签名,要不要带到签名数据里呢?

这里就要引入比特币的另一个概念,签名标示,就直接引用原文了,后面再逐个解释下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
The SIGHASH flags have two parts, a mode and the ANYONECANPAY modifier:
=> 签名标示包含两个部分,模式和 ANYONECANPAY

SIGHASH_ALL: This is the default. It indicates that everything about the transaction is signed, except for the input scripts. Signing the input scripts as well would obviously make it impossible to construct a transaction, so they are always blanked out. Note, though, that other properties of the input, like the connected output and sequence numbers, are signed; it's only the scripts that are not. Intuitively, it means "I agree to put my money in, if everyone puts their money in and the outputs are this".
=> SIGHASH_ALL,这是钱包默认使用的模式,代表交易体内的所有 In 和 Out 我都关心,都不能随便改变,都签上。

SIGHASH_NONE: The outputs are not signed and can be anything. Use this to indicate "I agree to put my money in, as long as everyone puts their money in, but I don't care what's done with the output". This mode allows others to update the transaction by changing their inputs sequence numbers.
=> SIGHASH_NONE,代表交易体内,我只关心 In, 我只签 In,Out 我不关心,变化了我也不管。这里其他的 sequence 都被设为 0,代表不关心,别人可以更新 sequence。

SIGHASH_SINGLE: Like SIGHASH_NONE, the inputs are signed, but the sequence numbers are blanked, so others can create new versions of the transaction. However, the only output that is signed is the one at the same position as the input. Use this to indicate "I agree, as long as my output is what I want; I don't care about the others".
=> SIGHASH_SINGLE,代表交易体内,我关心 In,并且我还关心位置与我的 In 对应的 Out,这些我签名,其他的 Out 我不关心,另外像 SIGHASH_NONE 一样,别人可以更新 sequence。

The SIGHASH_ANYONECANPAY modifier can be combined with the above three modes. When set, only that input is signed and the other inputs can be anything.
=> SIGHASH_ANYONECANPAY,这个是一个附加值,可以附加在所有上述的模式上,额外表示,In 里我只关心自己的,其他的 In 我也不关心,别人都可以改。

最开始看这个的时候,搞不明白,这样做有什么意义,不就是签名么,怎么还分别人和我自己,不应该都是我自己的么,我自己都签上就好了。

后来看得多了,才知道,这个是为了比特币的合约准备的。在合约里,很多时候,交易体不是一个人完成的,一个人写完了自己的部分,并不直接发送到比特币网络,而是线下发给别人,别人完成自己的部分,然后传给下一个人,类似这样的流程,这就出现了上面说的,我只关心我自己的,或者我只关心 In,这种需求。大家都做好了之后,或者在某个合适的时候,这个交易体才会被真正发送到比特币网络,被矿工收录到区块内,这里后续有机会可以写一篇文章介绍下。

知道了签名标示,那么就可以开始介绍签名的具体流程了,这里引用 bitcoinj 的代码,一步步介绍下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    // 这个函数创建的是一个 TxIn,TxIn 内包含签名的创建。
    public TransactionInput addSignedInput(TransactionOutPoint prevOut, Script scriptPubKey, ECKey sigKey,
                                           SigHash sigHash, boolean anyoneCanPay) throws ScriptException {
        // Verify the API user didn't try to do operations out of order.
        checkState(!outputs.isEmpty(), "Attempting to sign tx without outputs.");
        TransactionInput input = new TransactionInput(params, this, new byte[]{}, prevOut);
        addInput(input);
        // hashForSignature,构建了待签名的数据,并求了 hash,主要看看这个函数
        Sha256Hash hash = hashForSignature(inputs.size() - 1, scriptPubKey, sigHash, anyoneCanPay);
        // 将上面求出来的 hash 签名
        ECKey.ECDSASignature ecSig = sigKey.sign(hash);
        TransactionSignature txSig = new TransactionSignature(ecSig, sigHash, anyoneCanPay);
       // 创建完整的脚本并加入到 TxIn 内
        if (scriptPubKey.isSentToRawPubKey())
            input.setScriptSig(ScriptBuilder.createInputScript(txSig));
        else if (scriptPubKey.isSentToAddress())
            input.setScriptSig(ScriptBuilder.createInputScript(txSig, sigKey));
        else
            throw new ScriptException("Don't know how to sign for this kind of scriptPubKey: " + scriptPubKey);
        return input;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
    // 这里构建了待签名的数据
    public Sha256Hash hashForSignature(int inputIndex, byte[] connectedScript, byte sigHashType) {
        try {
            // 复制了一份 transaction,方便后面的清理等操作
            Transaction tx = this.params.getDefaultSerializer().makeTransaction(this.bitcoinSerialize());
            for (int i = 0; i < tx.inputs.size(); i++) {
                // 清理掉所有 TxIn 内的脚本,注意哦, 签名数据是不带已经签好的脚本的
                tx.inputs.get(i).clearScriptBytes();
            }
           // 这里跟比特币的一个 bug 有关,需要清除掉 OP_CODESEPARATOR
            connectedScript = Script.removeAllInstancesOfOp(connectedScript, ScriptOpCodes.OP_CODESEPARATOR);
           // 获取当前的 TxIn
            TransactionInput input = tx.inputs.get(inputIndex);
            // 将 TxIn 中的脚本,替换为币的赎回脚本(connectedScript)
            input.setScriptBytes(connectedScript);

            if ((sigHashType & 0x1f) == SigHash.NONE.value) {
                // 如果是 NONE 模式,因为不关心 TxOut, 所以去掉所有的 Out
                tx.outputs = new ArrayList<TransactionOutput>(0);
                for (int i = 0; i < tx.inputs.size(); i++)
                    if (i != inputIndex)
                        // 将 sequence 设为0,因为不关心
                        tx.inputs.get(i).setSequenceNumber(0);
            } else if ((sigHashType & 0x1f) == SigHash.SINGLE.value) {
                if (inputIndex >= tx.outputs.size()) {
                    return Sha256Hash.wrap("0100000000000000000000000000000000000000000000000000000000000000");
                }
                // 在 SINGLE 模式下,下面这两行是将除了对应位置的 Out,其他的都清零
                tx.outputs = new ArrayList<TransactionOutput>(tx.outputs.subList(0, inputIndex + 1));
                for (int i = 0; i < inputIndex; i++)
                    tx.outputs.set(i, new TransactionOutput(tx.params, tx, Coin.NEGATIVE_SATOSHI, new byte[] {}));
                // 将 sequence 设为0,因为不关心
                for (int i = 0; i < tx.inputs.size(); i++)
                    if (i != inputIndex)
                        tx.inputs.get(i).setSequenceNumber(0);
            }

            // 如果有 ANYONECANPAY,那么除了当前 TxIn,其他 In 都清零
            if ((sigHashType & SigHash.ANYONECANPAY.value) == SigHash.ANYONECANPAY.value) {
                tx.inputs = new ArrayList<TransactionInput>();
                tx.inputs.add(input);
            }

            // 序列化,并求 hash
            ByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(tx.length == UNKNOWN_LENGTH ? 256 : tx.length + 4);
            tx.bitcoinSerialize(bos);
            uint32ToByteStreamLE(0x000000ff & sigHashType, bos);
            Sha256Hash hash = Sha256Hash.twiceOf(bos.toByteArray());
            bos.close();

            return hash;
        } catch (IOException e) {
            throw new RuntimeException(e);  // Cannot happen.
        }
    }

结束

比特币的设计确实考虑了很多东西,能稳定运行这么久,与设计者的知识面和用心程度分不开。希望自己以后也能创造出一套可以广为流传的系统。

这篇介绍了交易体的结构体和签名,希望对大家有所帮助。

This post is licensed under CC BY 4.0 by the author.
Contents