比特币中对交易进行签名的详细过程

最近在和同事交流我们中对UTXO和签名的处理,有些心得,写下此博文。对比特币有点基本概念的都知道,比特币是通过ECDSA数字签名来解锁UTXO中的未花费余额。

关于UTXO我不需要做太多介绍,毕竟介绍这个概念的文章已经很多了。我主要是谈谈已经有UTXO了,该怎么花掉。

交易的结构

我们先来看看在比特币中,一个交易的结构是什么样的?

type MsgTx Version int32 TxIn []</span>*<span style="color: #000000;"&gt;TxIn TxOut []</span>*<span style="color: #000000;"&gt;TxOut LockTime uint32

}

type TxOut <span style="color: #0000ff;">struct<span style="color: #000000;"> {

Value int64

PkScript []</span><span style="color: #0000ff;"&gt;byte</span><span style="color: #000000;"&gt;

}

type TxIn <span style="color: #0000ff;">struct<span style="color: #000000;"> {

PreviousOutPoint OutPoint

SignatureScript []</span><span style="color: #0000ff;"&gt;byte</span><span style="color: #000000;"&gt;

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和锁定脚本。

在比特币中,要做一笔交易分为三个步骤:

  1. 构建原始交易RawTransaction,该交易包含了输入指向的OutPoint,也包含了完整的Output,但是没有签名,也就是没有设置SignatureScript的内容。
  2. 用私钥对签名构建的RawTransaction进行签名,并将签名构建成完整的解锁脚本,填入对应的Input的SignatureScript字段中。
  3. 将签名后的Transaction发送到P2P网络中。

构建原始交易RawTransaction

现在假设我有一个地址mx3KrUjRzzqYTcsyyvWBiHBncLrrTPXnkV(这是一个测试网地址),该地址收到了两笔转账,一笔0.4BTC(),另一笔1.1BTC(),这两笔收入都是在其交易Output的第二条,也就是Index=1(Index从0开始算)。现在我们想要做一笔1.2BTC的转账,然后给一定的手续费后,找零到原地址,所以我们会构建一笔交易,该交易有2Input和2Output。

以下是我用Go基于btcd写的示例代码,这里我们就构建好了一个RawTransaction。

func buildRawTx() *<span style="color: #008000;">//<span style="color: #008000; text-decoration: underline;">https://testnet.blockchain.info/tx/f0d9d482eb122535e32a3ae92809dd87839e63410d5fd52816fc9fc6215018cc?show_adv=true
<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,主要是第二列和第三列:

TxHash:,

OutIndex:1,

Amount:0.4BTC,PkScript:

OP_DUP OP_HASH160 PUSHDATA(20)b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG

TxHash:
,

OutIndex:1}

SignatureScript =NULL,Sequence =0xFFFFFFFF

Value=29910240

PkScript=

OP_DUP OP_HASH160 PUSHDATA(20)b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG

TxHash:
,

Amount:1.1BTC,PkScript:

OP_DUP OP_HASH160 PUSHDATA(20)b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG

TxHash:
,Sequence =0xFFFFFFFF

Value=120000000

PkScript=

OP_DUP OP_HASH160 PUSHDATA(20)be09abcbfda1f2c26899f062979ab0708731235a OP_EQUALVERIFY OP_CHECKSIG

接下来我们要对第一个Input签名,于是我们需要将交易复制一个副本,并改为:

TxHash:,

OutIndex:1}

SignatureScript =

OP_DUP OP_HASH160 PUSHDATA(20) b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG

,Sequence =0xFFFFFFFF

Value=29910240

PkScript=

OP_DUP OP_HASH160 PUSHDATA(20)b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG

TxHash:
,Sequence =0xFFFFFFFF

Value=120000000

PkScript=

OP_DUP OP_HASH160 PUSHDATA(20)be09abcbfda1f2c26899f062979ab0708731235a OP_EQUALVERIFY OP_CHECKSIG

接下来对这个交易计算Hash,然后进行签名。得到签名结果:3045022100c435eb458b295381d6e1f489b8683d1b10ecad0a7691949a4ae7ffee74bd22ae022031e47b9ebed5b90f6d51cd05e6f53bdc59f5d6d754aff14a88a6e8659b5fdad501 而我们知道这个地址的公钥是:038cc8c907b29a58b00f8c2590303bfc93c69d773b9da204337678865ee0cafadb

所以签完名后,我们的交易变成:

TxHash:,

OutIndex:1}

SignatureScript =

PUSHDATA(72)[3045022100c435eb458b295381d6e1f489b8683d1b10ecad0a7691949a4ae7ffee74bd2

2ae022031e47b9ebed5b90f6d51cd05e6f53bdc59f5d6d754aff14a88a6e8659b5fdad501] PUSHDATA(33)[038cc8c907b29a58b00f8c2590303bfc93c69d773b9da204337678865ee0cafadb]

,Sequence =0xFFFFFFFF

Value=120000000

PkScript=

OP_DUP OP_HASH160 PUSHDATA(20)be09abcbfda1f2c26899f062979ab0708731235a OP_EQUALVERIFY OP_CHECKSIG

这才只是完成了第一个Input的签名,接下来我们再对第二个Input进行签名,同样的道理,我们需要制造一个交易的副本,然后把第一个Input的SignatureScript清空,然后给第二个Input的SignatureScript赋值:

TxHash:,

OutIndex:1}

SignatureScript =OP_DUP OP_HASH160 PUSHDATA(20) b5407cec767317d41442aab35bad2712626e17ca OP_EQUALVERIFY OP_CHECKSIG

,Sequence =0xFFFFFFFF

Value=120000000

PkScript=

OP_DUP OP_HASH160 PUSHDATA(20)be09abcbfda1f2c26899f062979ab0708731235a OP_EQUALVERIFY OP_CHECKSIG

显然这个副本与第一个签名时的数据是不一样的,所以签名结果也不一样,最终签名结果为:30440220196bce75f0a25ac8afa7218aefc86cba3924845450f3d311c89e9c2a3438a99c0220230bed598a610be971ca49690f4b42ac2acfa80c09d4cbabd278b03c824af14501,当然我们因为是同一个地址,所以公钥是一样的:038cc8c907b29a58b00f8c2590303bfc93c69d773b9da204337678865ee0cafadb

我们把这个签名和公钥再放回原始交易中,就变成我们需要的完整签名的交易:

TxHash:,

OutIndex:1}

SignatureScript =PUSHDATA(72)[3045022100c435eb458b295381d6e1f489b8683d1b10ecad0a7691949a4ae7ffee74bd22ae022031e47b9ebed5b90f6d51cd05e6f53bdc59f5d6d754aff14a88a6e8659b5fdad501] PUSHDATA(33)[038cc8c907b29a58b00f8c2590303bfc93c69d773b9da204337678865ee0cafadb]

,

OutIndex:1}

SignatureScript =PUSHDATA(71)[30440220196bce75f0a25ac8afa7218aefc86cba3924845450f3d311c89e9c2a3438a99c0220230bed598a610be971ca49690f4b42ac2acfa80c09d4cbabd278b03c824af14501] PUSHDATA(33)[038cc8c907b29a58b00f8c2590303bfc93c69d773b9da204337678865ee0cafadb]

,Sequence =0xFFFFFFFF

Value=120000000

PkScript=

OP_DUP OP_HASH160 PUSHDATA(20)be09abcbfda1f2c26899f062979ab0708731235a OP_EQUALVERIFY OP_CHECKSIG

这就是一个真实完整的交易了,接下来就可以通过P2P网络发送该交易,并最终被矿工打包确认。

总结

实际上在比特币的源码中比我上面说的还要复杂一些,还涉及到这个hash是对整个交易进行SigHashAll还是SigHashSingle或者SigHashNone,这些都是很特殊的情况,一般的比特币钱包也不支持,具体可以参加精通比特币书中的介绍:6.5.3签名哈希类型( SIGHASH)

普通来说,我们要对一笔交易进行签名或者验签,就是把当前Input中的解锁脚本替换成锁定脚本,而其他Input的解锁脚本情况,然后计算Hash和签名!

其实我还是有点不明白,为什么比特币中不直接对没有任何解锁脚本的RawTransaction进行签名呢?而是非要加上锁定脚本来签名?不知道这里面有什么更深的考虑。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


类型转换 1、int转string 2、string转int 3、string转float 4、用户结构类型转换
package main import s &quot;strings&quot; import &quot;fmt&quot; var p = fmt.Println func main() { p(&quot;Contains: &quot;, s.Contains(&quot;test&quo
类使用:实现一个people中有一个sayhi的方法调用功能,代码如下: 接口使用:实现上面功能,代码如下:
html代码: beego代码:
1、读取文件信息: 2、读取文件夹下的所有文件: 3、写入文件信息 4、删除文件,成功返回true,失败返回false
配置环境:Windows7+推荐IDE:LiteIDEGO下载地址:http://www.golangtc.com/downloadBeego开发文档地址:http://beego.me/docs/intro/ 安装步骤: 一、GO环境安装 二、配置系统变量 三、Beego安装 一、GO环境安装 根
golang获取程序运行路径:
Golang的文档和社区资源:为什么它可以帮助开发人员快速上手?
Golang:AI 开发者的实用工具
Golang的标准库:为什么它可以大幅度提高开发效率?
Golang的部署和运维:如何将应用程序部署到生产环境中?
高性能AI开发:Golang的优势所在
本篇文章和大家了解一下go语言开发优雅得关闭协程的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。1.简介本文将介绍首先为什么需要主...
这篇文章主要介绍了Go关闭goroutine协程的方法,具有一定借鉴价值,需要的朋友可以参考下。下面就和我一起来看看吧。1.简介本文将介绍首先为什么需要主动关闭gor...
本篇文章和大家了解一下go关闭GracefulShutdown服务的几种方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。目录Shutdown方法Regi...
这篇文章主要介绍了Go语言如何实现LRU算法的核心思想和实现过程,具有一定借鉴价值,需要的朋友可以参考下。下面就和我一起来看看吧。GO实现Redis的LRU例子常
今天小编给大家分享的是Go简单实现多租户数据库隔离的方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会...
这篇“Linux系统中怎么安装NSQ的Go语言客户端”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希
本文小编为大家详细介绍“怎么在Go语言中实现锁机制”,内容详细,步骤清晰,细节处理妥当,希望这篇“怎么在Go语言中实现锁机制”文章能帮助大家解决疑惑,下面...
今天小编给大家分享一下Go语言中interface类型怎么使用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考