1

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

 2 years ago
source link: https://imnisen.github.io/bitcoin-malleability.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

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

具体来讲,以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):

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的计算。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK