最近在和同事交流我们中对UTXO和签名的处理,有些心得,写下此博文。对比特币有点基本概念的都知道,比特币是通过ECDSA数字签名来解锁UTXO中的未花费余额。
关于UTXO我不需要做太多介绍,毕竟介绍这个概念的文章已经很多了。我主要是谈谈已经有UTXO了,该怎么花掉。
交易的结构
我们先来看看在比特币中,一个交易的结构是什么样的?
Version int32
TxIn []</span>*<span style="color: #000000;">TxIn
TxOut []</span>*<span style="color: #000000;">TxOut
LockTime uint32
}
type TxOut
<span style="color: #0000ff;">struct<span style="color: #000000;"> {Value int64
PkScript []</span><span style="color: #0000ff;">byte</span><span style="color: #000000;">
}
type TxIn <span style="color: #0000ff;">struct<span style="color: #000000;"> {
PreviousOutPoint OutPoint
SignatureScript []</span><span style="color: #0000ff;">byte</span><span style="color: #000000;">
Sequence uint32
}
type OutPoint <span style="color: #0000ff;">struct<span style="color: #000000;"> {
Hash chainhash.Hash
Index uint32
}
我们可以看到,一个交易(MsgTx)是由多个Input和多个Output组成的,而在Input中是由指向UTXO的OutPoint,解锁脚本SignatureScript和序列Sequence组成。
UTXO我们可以认为是一个KeyValue的大表,在该表中,交易的Hash和该交易中Output所在的位置索引Index就构成了UTXO的Key,而Value就是比特币Amount、锁定脚本等信息,所以在UTXO数据库中,我们通过OutPoint能够很快的找到对应的Amount和锁定脚本。
在比特币中,要做一笔交易分为三个步骤:
- 构建原始交易RawTransaction,该交易包含了输入指向的OutPoint,也包含了完整的Output,但是没有签名,也就是没有设置SignatureScript的内容。
- 用私钥对签名构建的RawTransaction进行签名,并将签名构建成完整的解锁脚本,填入对应的Input的SignatureScript字段中。
- 将签名后的Transaction发送到P2P网络中。
构建原始交易RawTransaction
现在假设我有一个地址mx3KrUjRzzqYTcsyyvWBiHBncLrrTPXnkV(这是一个测试网地址),该地址收到了两笔转账,一笔0.4BTC(),另一笔1.1BTC(),这两笔收入都是在其交易Output的第二条,也就是Index=1(Index从0开始算)。现在我们想要做一笔1.2BTC的转账,然后给一定的手续费后,找零到原地址,所以我们会构建一笔交易,该交易有2Input和2Output。
以下是我用Go基于btcd写的示例代码,这里我们就构建好了一个RawTransaction。
<span style="color: #000000;">
tx :=<span style="color: #000000;"> wire.NewMsgTx(wire.TxVersion)
<span style="color: #008000;">//<span style="color: #008000; text-decoration: underline;">https://testnet.blockchain.info/tx-index/239152566/1<span style="color: #008000;"> 0.4BTC
<span style="color: #000000;">
utxoHash,_ := chainhash.NewHashFromStr(<span style="color: #800000;">"<span style="color: #800000;">1dda832890f85288fec616ef1f4113c0c86b7bf36b560ea244fd8a6ed12ada52<span style="color: #800000;">"<span style="color: #000000;">)
point := wire.OutPoint{Hash: *utxoHash,Index: <span style="color: #800080;">1<span style="color: #000000;">}
<span style="color: #008000;">//<span style="color: #008000;">构建第一个Input,指向一个0.4BTC的UTXO,第二个参数是解锁脚本,现在是nil
<span style="color: #000000;">
tx.AddTxIn(wire.NewTxIn(&<span style="color: #000000;">point,nil,nil))
<span style="color: #008000;">//<span style="color: #008000; text-decoration: underline;">https://testnet.blockchain.info/tx-index/239157459/1<span style="color: #008000;"> 1.1BTC
<span style="color: #000000;">
utxoHash2,_ := chainhash.NewHashFromStr(<span style="color: #800000;">"<span style="color: #800000;">24f284aed2b9dbc19f0d435b1fe1ee3b3ddc763f28ca28bad798d22b6bea0c66<span style="color: #800000;">"<span style="color: #000000;">)
point2 := wire.OutPoint{Hash: *utxoHash2,Index: <span style="color: #800080;">1<span style="color: #000000;">}
<span style="color: #008000;">//<span style="color: #008000;">构建第二个Input,指向一个1.1BTC的UTXO,第二个参数是解锁脚本,现在是nil
<span style="color: #000000;">
tx.AddTxIn(wire.NewTxIn(&<span style="color: #000000;">point2,nil))
<span style="color: #008000;">//<span style="color: #008000;">找零的地址(这里是16进制形式,变成Base58格式就是mx3KrUjRzzqYTcsyyvWBiHBncLrrTPXnkV)
<span style="color: #000000;">
pubKeyHash,_ := hex.DecodeString(<span style="color: #800000;">"<span style="color: #800000;">b5407cec767317d41442aab35bad2712626e17ca<span style="color: #800000;">"<span style="color: #000000;">)
<span style="color: #0000ff;">lock,_ :=<span style="color: #000000;"> txscript.NewScriptBuilder().AddOp(txscript.OP_DUP).AddOp(txscript.OP_HASH160).
AddData(pubKeyHash).AddOp(txscript.OP_EQUALVERIFY).AddOp(txscript.OP_CHECKSIG).
Script()
<span style="color: #008000;">//<span style="color: #008000;">构建第一个Output,是找零0.2991024 BTC
<span style="color: #000000;">
tx.AddTxOut(wire.NewTxOut(<span style="color: #800080;">29910240,<span style="color: #0000ff;">lock<span style="color: #000000;">))
<span style="color: #008000;">//<span style="color: #008000;">支付给了某个地址,仍然是16进制形式,Base58形式是:mxqnGTekzKqnMqNFHKYi8FhV99WcvQGhfH。
<span style="color: #000000;">
pubKeyHash2,_ := hex.DecodeString(<span style="color: #800000;">"<span style="color: #800000;">be09abcbfda1f2c26899f062979ab0708731235a<span style="color: #800000;">"<span style="color: #000000;">)
lock2,_ :=<span style="color: #000000;"> txscript.NewScriptBuilder().AddOp(txscript.OP_DUP).AddOp(txscript.OP_HASH160).
AddData(pubKeyHash2).AddOp(txscript.OP_EQUALVERIFY).AddOp(txscript.OP_CHECKSIG).
Script()
<span style="color: #008000;">//<span style="color: #008000;">构建第二个Output,支付1.2 BTC出去
<span style="color: #000000;">
tx.AddTxOut(wire.NewTxOut(<span style="color: #800080;">120000000<span style="color: #000000;">,lock2))
<span style="color: #0000ff;">return<span style="color: #000000;"> tx
}
交易的签名过程
现在我们知道私钥,需要对该交易进行签名,因为有2个Input,所以我们要签名2次,每个签名的原理是一样的,我就以第一个Input为例来说明吧。
在比特币中,对一笔交易的签名流程是这样的:
1.查找该笔交易对应的UTXO
2.获得该UTXO对应的锁定脚本
3.复制该交易对象,并在复制副本中将该Input的解锁脚本字段的值设置为对应的锁定脚本
4.清除其他Input的解锁脚本字段
5.对这个改造后的交易对象计算Hash
6.使用私钥对Hash进行签名。
用表格的形式可以更容易表达:
这是原始未签名的交易RawTransaction,主要是第二列和第三列:
所以签完名后,我们的交易变成:
我们把这个签名和公钥再放回原始交易中,就变成我们需要的完整签名的交易:
总结
实际上在比特币的源码中比我上面说的还要复杂一些,还涉及到这个hash是对整个交易进行SigHashAll还是SigHashSingle或者SigHashNone,这些都是很特殊的情况,一般的比特币钱包也不支持,具体可以参加精通比特币书中的介绍:6.5.3签名哈希类型( SIGHASH)
普通来说,我们要对一笔交易进行签名或者验签,就是把当前Input中的解锁脚本替换成锁定脚本,而其他Input的解锁脚本情况,然后计算Hash和签名!
其实我还是有点不明白,为什么比特币中不直接对没有任何解锁脚本的RawTransaction进行签名呢?而是非要加上锁定脚本来签名?不知道这里面有什么更深的考虑。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。