Published: 2018-10-01

比特币交易的延展性问题(Transaction Malleability)

1 概述

正如黄金等金属具有延展性,比特币也是有的;)

比特币交易的延展性的本质在于: 一个交易的签名可以稍作修改但不影响其含义,但因为这个修改却产生了不同的交易哈希值(也就是交易ID)。 也就是说,可以将一笔交易的签名稍作修改,然后广播,它有可能在原始交易之前被包含进区块链,使得原始交易成为无效的交易。

2 细节

具体来讲,以btcd 的实现为例,先看看比特币交易的结构和交易的Id的产生方式

2.1 Tx的数据结构

// OutPoint defines a bitcoin data type that is used to track previous
// transaction outputs.
type OutPoint struct {
        Hash  chainhash.Hash  // 指向未花费TxOut所在的交易
        Index uint32          // 未花费的TxOut在该交易里的索引
}

// TxIn defines a bitcoin transaction input.
type TxIn struct {
        PreviousOutPoint OutPoint  // 指向要花费的交易,以及该交易的哪一个输出
        SignatureScript  []byte   // 签名脚本,每个Input都有一个签名脚本,用来解锁PreviousOutPoint指向的TxOut的PkScript
        Witness          TxWitness  // 稍后再看
        Sequence         uint32
}

// TxOut defines a bitcoin transaction output.
type TxOut struct {
        Value    int64
        PkScript []byte  // 将比特币锁定到特定脚本,使用者提供能解锁该脚本的脚本才能花费该比特币
}

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


2.2 TxId生成方法

// 1. Txid就是将Tx的各个部分序列化后doublHash(两次sha256)得到的

// TxHash generates the Hash for the transaction.
func (msg *MsgTx) TxHash() chainhash.Hash {
        // Encode the transaction and calculate double sha256 on the result.
        // Ignore the error returns since the only way the encode could fail
        // is being out of memory or due to nil pointers, both of which would
        // cause a run-time panic.
        buf := bytes.NewBuffer(make([]byte, 0, msg.SerializeSizeStripped()))
        _ = msg.SerializeNoWitness(buf)  // 注意,这是里segwit之后的不包含witness的TxId
        return chainhash.DoubleHashH(buf.Bytes())
}


// 2. 求TxId的中的序列化方法实现,调用MsgTx的BtcEncode方法

// SerializeNoWitness encodes the transaction to w in an identical manner to
// Serialize, however even if the source transaction has inputs with witness
// data, the old serialization format will still be used.
func (msg *MsgTx) SerializeNoWitness(w io.Writer) error {
        return msg.BtcEncode(w, 0, BaseEncoding)
}

// BtcEncode encodes the receiver to w using the bitcoin protocol encoding.
// This is part of the Message interface implementation.
// See Serialize for encoding transactions to be stored to disk, such as in a
// database, as opposed to encoding transactions for the wire.
func (msg *MsgTx) BtcEncode(w io.Writer, pver uint32, enc MessageEncoding) error {
        // 省略,核心功能是是将各部分按照协议序列化成bytes
}

2.3 签名脚本的产生

以一个Pay-To-Public-Key-Hash (P2PKH)为例, 假设Alice给Bob转账1BTC,那么Alice需要知道Bob的地址,这里的地址是Bob的公钥经过Hash得到的(具体地址计算的方式和种类可以参见这里)。

参见下面示意图(图片来自bitcoin.org):

Sorry, your browser does not support SVG.

Alice在她生成的Tx1里的 TxOut 结构里,Value是1e8,PkScript是类似 OP_DUP OP_HASH160 <Bob's PubkeyHash> OP_EQUALVERIFY OP_CHECKSIG 这样的脚本。假设Alice的Tx1被确认成功,这样Bob就有了1BTC。

当Bob想花费这1BTC赋给Carol的时候,同样的Bob需要获得Carol的地址包含到自己创建的Tx2的 Txout 里,而在 TxIn 里,他先通过 PreviousOutPoint 表明他想花费Tx1里的Alice给他1BTC的那个输出,然后填充 TxIn.SignatureScript 这个字段。

如果是花费一个P2PKH的输出,那么 TxIn.SignatureScript 分为两个部分:Bob's Pubkey和一个secp256k1签名(signature),类似: <Bob's Sig> <Bob's PubKey> 这种格式。

Bob的公钥就是提供hash值给Alice的那个公钥,签名(signature)是通过Bob的私钥和该交易Tx2的部分数据一起计算得到的。

Tx2包含的主要数据有:Tx2.TxIn[0].PreviousOutPoint, Tx1.TxOut[0].PkScript, Tx2.TxOut[0].Value, Tx2.TxOut[0].PkScript

本质上来说,Tx2除了TxIn的SignatureScript其它都包含在内(以及加上Tx1的PkScript)

所以Bob的secp256k1签名不仅证明了Bob拥有相应的私钥,还保证了该交易除了签名脚本的部分在传播时不会被修改(因为一旦被修改,那么签名就不对了)。

2.3.1 实现

具体生成Signature Script的实现可以参看btcd实现:

// 1. 签名一个Tx2的TxIn的主要的方法
func sign(/* 参数省略 */) {

        /* 省略 */

        // 根据Alice的PkScript的类型来确认如何生成签名
        switch class {

        /* 其它类型省略 */

        // 这是一个P2PKH
        case PubKeyHashTy:
                /* 获取私钥步骤省略 */

                // tx 是Alice的Tx1
                // idx Bob要签名的这个TxIn的索引
                // subScript是Tx1里Alice设置的 OP_DUP OP_HASH160 <Bob's PubkeyHash> OP_EQUALVERIFY OP_CHECKSIG
                // hashType 是用来配置不同的签名方式
                // key 是Bob的私钥
                // compressed 表示Bob的公钥是否压缩(这是另外一个话题了)
                script, err := SignatureScript(tx, idx, subScript, hashType,
                        key, compressed)
                if err != nil {
                        return nil, class, nil, 0, err
                }

                return script, class, addresses, nrequired, nil

        /* 其它类型省略 */

        }
}


// 2. sign调用的核心,生成P2PKH的signature script
// SignatureScript creates an input signature script for tx to spend BTC sent
// from a previous output to the owner of privKey. tx must include all
// transaction inputs and outputs, however txin scripts are allowed to be filled
// or empty. The returned script is calculated to be used as the idx'th txin
// sigscript for tx. subscript is the PkScript of the previous output being used
// as the idx'th input. privKey is serialized in either a compressed or
// uncompressed format based on compress. This format must match the same format
// used to generate the payment address, or the script validation will fail.
func SignatureScript(tx *wire.MsgTx, idx int, subscript []byte, hashType SigHashType, privKey *btcec.PrivateKey, compress bool) ([]byte, error) {

        // 先生成signature
        sig, err := RawTxInSignature(tx, idx, subscript, hashType, privKey)
        if err != nil {
                return nil, err
        }

        // 再根据是否压缩生成Bob的Pubkey
        pk := (*btcec.PublicKey)(&privKey.PublicKey)
        var pkData []byte
        if compress {
                pkData = pk.SerializeCompressed()
        } else {
                pkData = pk.SerializeUncompressed()
        }


        // 最后组合成 <Sig> <PubKey> 比特币脚本
        return NewScriptBuilder().AddData(sig).AddData(pkData).Script()
}


// 3. 生成signature的方法
// RawTxInSignature returns the serialized ECDSA signature for the input idx of
// the given transaction, with hashType appended to it.
func RawTxInSignature(tx *wire.MsgTx, idx int, subScript []byte,
        hashType SigHashType, key *btcec.PrivateKey) ([]byte, error) {

        // 先按一定方法计算各部分哈希值
        hash, err := CalcSignatureHash(subScript, hashType, tx, idx)
        if err != nil {
                return nil, err
        }

        // 再用私钥签名该哈希值
        signature, err := key.Sign(hash)
        if err != nil {
                return nil, fmt.Errorf("cannot sign tx input: %s", err)
        }

        // 最后将签名序列化和哈希类型一起返回
        return append(signature.Serialize(), byte(hashType)), nil
}


// 4. 看看CalcSignatureHash调用的calcSignatureHash方法
// calcSignatureHash will, given a script and hash type for the current script
// engine instance, calculate the signature hash to be used for signing and
// verification.
func calcSignatureHash(script []parsedOpcode, hashType SigHashType, tx *wire.MsgTx, idx int) []byte {
        /* 参数说明:
           script是Tx1里Alice设置的: OP_DUP OP_HASH160 <Bob's PubkeyHash> OP_EQUALVERIFY OP_CHECKSIG.
           hashType 是用来配置不同的签名方式
           tx 是Bob的Tx2
           idx 是 Bob要签名的这个txin的索引
        */

        /* 省略部分 */

        // 复制Tx2, 得到移除这个TxIn外其它TxIn的signature script
        // Make a shallow copy of the transaction, zeroing out the script for
        // all inputs that are not currently being processed.
        txCopy := shallowCopyTx(tx)
        for i := range txCopy.TxIn {
                if i == idx {
                        // UnparseScript cannot fail here because removeOpcode
                        // above only returns a valid script.
                        sigScript, _ := unparseScript(script)
                        // 注意,这里用Tx1的PkScript来填充这个相应index的Signature script
                        txCopy.TxIn[idx].SignatureScript = sigScript
                } else {
                        txCopy.TxIn[i].SignatureScript = nil
                }
        }

        // 根据不同的hash的方法邀请,变换操作txCopy达到要求
        switch hashType & sigHashMask {
        case SigHashNone:
                txCopy.TxOut = txCopy.TxOut[0:0] // Empty slice.
                for i := range txCopy.TxIn {
                        if i != idx {
                                txCopy.TxIn[i].Sequence = 0
                        }
                }

        case SigHashSingle:
                // Resize output array to up to and including requested index.
                txCopy.TxOut = txCopy.TxOut[:idx+1]

                // All but current output get zeroed out.
                for i := 0; i < idx; i++ {
                        txCopy.TxOut[i].Value = -1
                        txCopy.TxOut[i].PkScript = nil
                }

                // Sequence on all other inputs is 0, too.
                for i := range txCopy.TxIn {
                        if i != idx {
                                txCopy.TxIn[i].Sequence = 0
                        }
                }

        default:
                // Consensus treats undefined hashtypes like normal SigHashAll
                // for purposes of hash generation.
                fallthrough
        case SigHashOld:
                fallthrough
        case SigHashAll:
                // Nothing special here.
        }
        if hashType&SigHashAnyOneCanPay != 0 {
                txCopy.TxIn = txCopy.TxIn[idx : idx+1]
        }


        /* 最后 DoubleHash(
                 MsgTxCopy序列化(所有Tx2的的内容,除了第idx个Input的Signature Script用Tx1的PKscript代替,以及移除其它的TxIn的Signature Script)
                 + hash类别

        */
        // The final hash is the double sha256 of both the serialized modified
        // transaction and the hash type (encoded as a 4-byte little-endian
        // value) appended.
        wbuf := bytes.NewBuffer(make([]byte, 0, txCopy.SerializeSizeStripped()+4))
        txCopy.SerializeNoWitness(wbuf)
        binary.Write(wbuf, binary.LittleEndian, hashType)
        return chainhash.DoubleHashB(wbuf.Bytes())
}

3 延展性的影响

可以看到,交易延展性并不影响交易的输出输出等,它对于交易的核心功能是没有影响的,只有TxId受影响,并不会造成比特币的窃取或者阻碍交易的进行等。

但交易延展性却会造成一些麻烦成不小的麻烦:

一个是采用TxId为标识的业务逻辑,比如MtGox交易所声称,其由于用户声称没有收到bitcoin(因为交易所记录的TxId确实没有被包含进区块链)而发起退款等请求等, 造成用户收到比他应得要多的比特币,而使得交易所蒙受损失。

另外一个是对串联交易的影响。串联交易有很多形式,最常见的是交易的Input执行的上一个TxId已经被修改了,导致该交易失败。 另外一个问题是像闪电网络这样的上层协议,其基于的正式交易串联的方式。

4 解决方案

喂,醒醒,现在已经8102年了,segwit的采用已经解决了这个问题,因为它将问题的根源signature script移除了Txid的计算。

Author: Nisen

Email: imnisen@163.com