比特币交易的延展性问题(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):
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的计算。