Cointime

扫码下载App
iOS & Android

比特币交易基础知识(二):手动建立比特币交易

个人专家

简介

好吧,还不是全部;但是这里描述的过程对于我的目的来说已经足够手动的了。我们的目标是足够详细地了解交易的构成,以便让我们理解交易的可延展性问题,以及对这个 "错误 "的修正建议。我想在这篇文章中完成的是充分深入地了解交易是如何工作的,以及它们是如何组合在一起的,同时避免使用像比特币库和参考客户端这样的工具,无论它们在哪里抽象出某些相关的细节。

让我们回到我们简单的单输入、单输出交易。

图1:我们将在本文中建立的P2PKH交易。

图1,也就是你在第一部分看到的那个图,说明了一个典型的P2PKH交易的结构。事实上,这个图代表了我们努力手工构建交易的最终结果。在这第二部分中,我们将经历所有的步骤,最终导致我们获得图1中描述的原始交易数据结构。

第1步:建立交易,减去scriptSig

输入输出点和其他标准字段

输入段有两个功能。首先,它确定了我们打算在当前交易中使用的前一个交易的输出。第二,它持有将解锁该输出的scriptPubKey的scriptSig。我们将在后面处理 scriptSig。

第一个字段指定前一个交易的交易哈希(交易ID),其中包含我们现在想要花费的输出。第二个字段指定了该输出的索引。这两个字段一起被称为outpoint。我们想要花费的输出的前一个交易可以被称为资金交易。

在我们的例子中,我使用以下交易作为我们的资金交易:

https://blockchain.info/tx/7e3ab0ea65b60f7d1ff4b231016fc958bc0766a46770410caa0a1855459b6e41

如果你点击这个链接,你会看到这个特定的交易(从一个交易所发出)有许多输出。但巧合的是,我们想要花费的输出是第一个输出。因此,该输出的索引,即我们的输入UTXO,是0或0x00的十六进制。

图2:显示区块浏览器页面顶部的资金交易。

图3:交易模板填写完毕,去掉了scriptSig和输出段

在输入前一笔交易的交易哈希值(或交易ID)时,注意我们需要与区块探索器(和比特币客户端RPC)中显示的字节顺序相反。将我们输入到输入段的哈希值与上面图2中显示的哈希值进行比较。这种古怪的原因尚不完全清楚。(如果你有兴趣,可以在这里找到一个详细的说明)。

前一个输出的索引必须是一个little endian格式的4字节条目。(如果我们花的是输出01,那么我们的条目将是01000000)。

在一个典型的交易中,序列字段总是如图所示输入(0xffffffff)。Number of Inputs 和 Number of Outputs 均为 01,而 Version 和 Locktime 字段的输入如图所示。这两个字段都被填充为四个字节,并且是little endian格式。Locktime指定了该交易的有效时间。如果设置为0,通常情况下,该交易可以立即被广播和开采。

填写交易模板的输出部分

接下来,我们将着手建立输出段。显然,我们正在创建的输出的价值必须小于我们在输入段中花费的前一个输出的价值。这个差额将成为交易费用。要知道前一个输出的价值,我们需要检查图2中的资金交易,它显示为0.0004 BTC。

我选择了一个0.0002 BTC(或20,000 Satoshi)的值给输出,因此使交易费用也是0.0002 BTC。值字段必须显示输出值,以十六进制为单位,填充为八个字节,并以little endian顺序输入。

让我们看看我们是如何得出最终结果的:20000的十六进制是4e20。填充到八个字节等于00000000004e20。最后,当反转到little endian顺序时,我们得到了: 204e000000000000.

图4:填入的输出段

正如我们在第一部分中所学到的,输出段本质上是创建一个UTXO。我们还向你介绍了一个典型的P2PKH交易的scriptPubKey。

在上面的图4中,我以十六进制格式输入了我们正在建立的P2PKH交易的scriptPubKey。我们究竟是如何得到这串字节的呢?

如果你记得第一部分的内容,scriptPubKey的一般格式是这样的:

Bitcoin Wiki你可以得到这个锁定脚本中四个操作码的十六进制代码(OP_DUP是0x76,OP_HASH160是0xa9,OP_EQUALVERIFY是0x88,OP_CHECKSIG是0xac)。

蓝色的字节代表比特币地址,一旦创建,将与这个UTXO相关。也就是说,这笔交易是将20,000个聪币的所有权分配给控制蓝色字节所代表的地址的实体。但这串蓝色字节看起来一点也不像比特币地址?那是因为这串字节实际上是该地址的公钥哈希表示。从比特币地址衍生出PUB-KEY-HASH,在这种情况下,1NAK3za9MkbAkkSBMLcvmhTD6etgB4Vhpr,可以通过使用任何比特币库很容易做到。例如,使用Vitalik Buterin的Pybitcointools,下面的命令可以将地址解码为公钥哈希值:

>> b58check_to_hex('1NAK3za9MkbAkkSBMLcvmhTD6etgB4Vhpr')

'e81d742e2c3c7acd4c29de090fc2c4d4120b2bf8'

>>> 

你还可以使用这个网站,然后按照本文档中的说明去除前缀和后缀。

Hashmal,第一部分介绍的比特币脚本IDE,它允许你将base58check编码的地址解码成公钥哈希,只需粘贴和点击即可。

我们已经基本完成了scriptPubKey的输入,只剩下红色的字节需要处理。0x14是一个PUSHDATA操作码,表示接下来的20个字节(或0x14字节的十六进制)将被推入堆栈。

脚本长度字段规定了组成scriptPubKey的字节数,即25个字节或十六进制的0x19。

图5:填写的交易模板减去了scriptSig

第2步:创建scriptSig(解锁脚本)。

我们又回到了输入段的工作上。现在我们需要创建重要的scriptSig或解锁脚本,这是手工创建交易最棘手(脚注1)的1部分。

使用比特币的ECDSA(椭圆曲线数字签名算法)的实现来制作数字签名,需要拥有签名的私钥和要签名的信息的哈希值。我们要签署的信息是交易本身,但实际上它是最终交易的一个修改版本。接下来,我们将通过以下步骤来构建将被签署的信息,这些步骤主要来自Stack Exchange的答案,而这个答案又主要是对之前答案的重述。

构建将被签名的交易信息

如图5所示,我们将继续为我们已经建立的部分交易构建消息。注意scriptSig(解锁脚本)字段。当然,我们还没有最终的scriptSig,而是输入我们要花费的前一个输出的scriptPubKey来代替。也就是说,我们检查我们的资金交易并检索该特定输出的scriptPubKey(锁定脚本)。在图2中,你看到的是资金交易的顶部。在下面的图6中,我们看到了同样的交易,但是显示了区块资源管理器页面的下部,我们可以看到索引为0的输出(第一个输出)的scriptPubKey。

如果你点击这个链接并滚动到页面的下部,你应该看到以下内容:

图6:资金交易显示上一个输出的scriptPubKey

scriptPubKey在区块资源管理器中以人类可读的形式呈现,当转换为十六进制时,它看起来像这样:

76a91499b1ebcfc11a13df5161aba8160460fe1601d54188ac

上面在填写输出段时已经介绍了如何导出的细节。

Script Length字段是Ox19(25字节),是上面scriptPubKey的字节长度。

图7:签名信息模板,用之前输出的scriptPubKey代替scriptSig的位置

请注意,图7变成了签名信息模板,而不是交易模板,因为如前所述,你签署的东西与最终的交易略有不同。主要的区别是用scriptPubKey代替最终的scriptSig,以及在底部增加了SigHash代码,指定签名哈希类型。各种签名哈希类型表明交易的哪些部分包括在签名的消息中。

对于正常交易,SigHash代码将是1,表示SIGHASH_ALL。这种签名散列类型意味着签名包括所有的输入和输出,减去scriptSig。

SigHash代码被填充为四个字节,并以little endian格式输入,因此是你在图7中看到的条目。

在多输入交易的情况下,每个输入必须单独签名。由于一个Input正在被签名,在构建的签名信息中,所有其他的输入将在 scriptSig 应该出现的地方为空。

完成的交易信息,作为十六进制数据,将被双重散列,然后签名:

0100000001416e9b4555180aaa0c417067a46607bc58c96f0131b2f41f7d0fb665eab03a7e000000001976a91499b1ebcfc11a13df5161aba8160460fe1601d54188acffffffff01204e0000000000001976a914e81d742e2c3c7acd4c29de090fc2c4d4120b2bf888ac0000000001000000

在签名前对交易信息进行双SHA256哈希运算

产生交易信息的双SHA256哈希值可以通过几种方式完成。最简单的方法可能是在Python中执行以下程序:

>>>导入hashlib

>> mhex = 'transaction_message_hex'。

>> hashlib.sha256(hashlib.sha256(mhex.decode('hex')).digest()) .hexdigest()

这将为你提供信息输出的双SHA256哈希值的十六进制:

'456f9e1b6184d770f1a240da9a3c4458e55b6b4ba2244dd21404db30b3131b94'

请注意,当我们在下一节实际进行签名时,我们将需要以字节为单位的输出,而不是十六进制。要以字节为单位输出双字节哈希值,请执行以下操作:

>>> hashlib.sha256(hashlib.sha256(mhex.decode('hex')).digest()) 。

如果你有一个Linux终端,你可以使用这个方法

klmoney@aspire: ~$ mhex=0100000001416e9b4555180aaa0c417067a46607bc58c96f0131b2f41f7d0fb665eab03a7e000000001976a91499b1ebcfc11a13df5161aba8160460fe1601d54188acffffffff01204e0000000000001976a914e81d742e2c3c7acd4c29de090fc2c4d4120b2bf888ac0000000001000000

klmoney@aspire:~$ echo -n $mhex | xxd -r -p | openssl dgst -sha256 -binary | openssl dgst -sha256

(stdin)= 456f9e1b6184d770f1a240da9a3c4458e55b6b4ba2244dd21404db30b3131b94

你也可以使用任何比特币库,例如Pybitcointools2(脚注2)。

产生scriptSig的签名(ECDSA)

我们需要在这里退一步,重新回忆一下这个签名--每个输入都有一个单独的签名--应该是为了实现什么。比特币使用的椭圆曲线数字签名算法(ECDSA)产生的签名可以完成两件事: 首先,它证明签名者拥有与我们想在当前交易中使用的前一个输出的scriptPubKey(锁定脚本)中的地址相关的私钥。拥有这个私钥,签名人就有权使用该产出。因此,签名证明了支出者的真实性。其次,因为交易,尽管是一个修改过的形式(交易信息),是被签署的,它将允许比特币客户端验证软件确认,自签署以来,交易的重要部分没有被改变。在执行OP_CHECKSIG操作码时,任何对交易有意义部分的改变都会被发现,并使签名无效。因此,该签名证明了交易的完整性。

我所描述的对信息进行签名的过程大体上遵循这里所概述的方法,其中使用了Python ECDSA库。大多数密码库会包括一个函数,以更直接的方式得出签名,但我认为这种方法更有教学意义。

在交互式模式下使用Python,这些步骤最终产生了签名:

>> 导入 hashlib, ecdsa, binascii

>> 从 ecdsa 导入 SigningKey, SECP256k1

a) 将交易信息的十六进制字符串分配给一个变量:

>> unsigned = ‘0100000001416e9b4555180aaa0c417067a46607bc58c96f0131b2f41f7d0fb665eab03a7e000000001976a91499b1ebcfc11a13df5161aba8160460fe1601d54188acffffffff01204e0000000000001976a914e81d742e2c3c7acd4c29de090fc2c4d4120b2bf888ac0000000001000000’

b) 生成该信息的双SHA256哈希值,并以字节为单位输出结果:

>> txhash = hashlib.sha256(hashlib.sha256(unsigned.decode('hex')).digest()) 。

c) 与前一个Output的地址相关的私钥,以十六进制格式:

>>> privkey = '3cd0560f5b27591916c643a0b7aa69d03839380a738d2e912990dcc573715d2c'

注意:假设你从一个WIF压缩格式的私钥开始,比如那些在bitaddress.org产生的私钥,那么你将有一个以'K'或'L'开头的私钥。与我正在使用的前一个Output相关的私钥是:

KyFvbums8LKuiVFHRzq2iEUsg7wvXC6fMkJC3PFLjGVQaaN9F1Ln

(重要提示:通常情况下,你不应该泄露私钥)。

WIF(钱包导入格式)压缩的私钥是私钥的十六进制版本的base58check编码表示。由于我们使用的是十六进制版本,我们需要对WIF压缩的密钥进行解码。Bitaddress.org将为你做这件事。或者,你可以使用一个比特币库,如Libbitcoin或Pybitcointools3(脚注3)。如果你使用这些库中的任何一个,你将需要删除0x01后缀,这个后缀是在编码成WIF压缩格式之前添加到私钥中的。这一点Andreas Antonoupolous在这里有解释。

d) 接下来执行下面的代码来获取签名:

>> signingkey = ecdsa.SigningKey.from_string(privkey.decode('hex'), curve=ecdsa.SECP256k1)

>> SIG = signingkey.sign_digest(txhash, sigencode=ecdsa.util.sigencode_der_canonize)

>> binascii.hexlify(SIG)

'304402201c3be71e1794621cbe3a7adec1af25f818f238f5796d47152137eba710f2174a02204f8fe667b696e30012ef4e56ac96afb830bddffee3b15d2e474066ab3aa39bad'

注意:签名的生成涉及到一个随机数的输入。因此,每次你重新执行上述代码时,你都会得到一个不同的签名。

将scriptSig放在一起

scriptSig包括签名和从签名的私钥中提取的公钥。我们已经有了签名,所以现在我们需要公钥,在这种情况下,它将是一个压缩的公钥。Bitaddress.org将从WIF压缩的私钥中生成压缩的公钥。但你也可以使用一个比特币库(脚注4)。最终的结果将是:

’03bf350d2821375158a608b51e3e898e507fe47f2d2e8c774de4a9a7edecf74eda’

以上是由该私钥导出的压缩公钥:

KyFvbums8LKuiVFHRzq2iEUsg7wvXC6fMkJC3PFLjGVQaaN9F1Ln

如果你跳回到表5,也就是我们的交易模板,你会发现唯一缺少的部分其实是scriptSig。下表是Peter Wuille在Stack Exchange answer中对scriptSig各部分的描述。

图8:组成scriptSig的各种元素。

关于scriptSig的构成,有几个要点需要注意:

  • PUSHDATA操作码0x47(或十进制71)是将被推入堆栈的字节数。这包括一个字节的sigHash代码。
  • DER编码的签名的R和S部分可以是32或33字节的长度(如上面链接的Stack Exchange答案中所解释的)。因此,顶部的PUSHDATA操作码可能是0x47、0x48或0x49。
  • 最近的交易使用了压缩的公钥,如本例中。压缩的公钥是32个字节,前缀为02或03的一个字节。未压缩的公钥是64字节,加上一个04的前缀。

最后的scriptSig为十六进制:

47304402201c3be71e1794621cbe3a7adec1af25f818f238f5796d47152137eba710f2174a02204f8fe667b696e30012ef4e56ac96afb830bddffee3b15d2e474066ab3aa39bad012103bf350d2821375158a608b51e3e898e507fe47f2d2e8c774de4a9a7edecf74eda

现在可以将scriptSig输入到我们之前未完成的交易模板中(图5),完成我们的P2PKH交易构建:

图9:完成的交易(如图1)。

将广播到比特币网络中的最终序列化原始交易(脚注5):

0100000001416e9b4555180aaa0c417067a46607bc58c96f0131b2f41f7d0fb665eab03a7e000000006a47304402201c3be71e1794621cbe3a7adec1af25f818f238f5796d47152137eba710f2174a02204f8fe667b696e30012ef4e56ac96afb830bddffee3b15d2e474066ab3aa39bad012103bf350d2821375158a608b51e3e898e507fe47f2d2e8c774de4a9a7edecf74edaffffffff01204e0000000000001976a914e81d742e2c3c7acd4c29de090fc2c4d4120b2bf888ac00000000

在广播前评估原始交易

如果你已经安装了第一部分中介绍的Bitcoin Script IDE Hashmal,那你做接下来的几件事会更容易

  • 确保你的原始交易是正确组合的。(稍后你会看到如何在没有Hashmal的情况下将你的原始交易反序列化)。
  • 检查签名是否真的有效,交易是否被验证。
  • 检查解锁脚本和锁定脚本的执行情况,看看比特币脚本堆栈操作的视觉表现。

图10:Hashmal中原始交易的反序列化视图

上面你看到的是我们的原始交易在Hashmal的交易分析器中的人可读视图。

图11:在Hashmal的交易分析器中,选择了验证标签。

在这个屏幕上,我们可以验证我们在交易中试图花费的输入(或输入)。输入段的解锁脚本(scriptSig)必须与输入UTXO(以前的输出)的锁定脚本(scriptPubKey)合作,才能对该特定的输入进行验证。这个屏幕提供了一个方便的方法来检查你生成的ECDSA签名是否正常。

在第一部分中,我们使用了一个解锁和锁定脚本的示意图,并看到了比特币的脚本语言是如何执行的。下面的两个屏幕共同说明了比特币的脚本如何在一个真实世界的交易中工作。我们甚至可以看到堆栈操作一步步的可视化。

在我们能一步步看到堆栈操作之前,选择Stack Evaluator的交易标签,并粘贴原始交易,如下图所示。

图12:在Stack Evaluator的事务选项卡中,粘贴原始事务。

一旦你完成了上述工作,继续选择Stack Evaluator中的堆栈标签。接下来,你可以在格式文本框中建立输入UTXO的(先前输出的)scriptPubKey,如图13所示,或者你可以简单地将scriptPubKey的十六进制形式直接粘贴到脚本文本框中。当你构建要签名的交易信息时,你应该已经有了这个hex格式的scriptPubKey。

图13:输入输入UTXO的scriptPubKey(之前的输出)并选择Evaluate。

当你选择Evaluate按钮时,Hashmal将在一个共享堆栈中一个接一个地分两步执行scriptSig和scriptPubKey,就像我们在第一部分中对原理图脚本所做的那样。

这似乎是它的工作方式: scriptSig(签名+公钥)是从我们粘贴到图12中的原始事务中拉过来的。然后用你在图13中输入的scriptPubKey进行堆栈执行。如果一切顺利,包括最重要的OP_CHECKSIG操作,你应该获得以绿色突出显示的“通过”和“已验证”结果作为奖励。

让我们逐个字节地完成脚本的执行,至少是部分地完成。如图13所示,Hashmal对堆栈进行了一步步的可视化处理,使这个过程变得超级清晰。

从原始交易中提取的scriptSig看起来是这样的:

47304402201c3be71e1794621cbe3a7adec1af25f818f238f5796d47152137eba710f2174a02204f8fe667b696e30012ef4e56ac96afb830bddffee3b15d2e474066ab3aa39bad012103bf350d2821375158a608b51e3e898e507fe47f2d2e8c774de4a9a7edecf74eda

而scriptPubKey是这样的:

76a91499b1ebcfc11a13df5161aba8160460fe1601d54188ac

1. 第0步:以PUSHDATA操作码476开始执行,这意味着接下来的0x47(脚注6)字节(71字节)要推到堆栈中。

在这个操作中,签名加上sigHash代码(01)被推入。

2. 第1步:另一个PUSHDATA操作码21将包含公钥的下一个0x21(33)字节推入堆栈。

3. 第2步:我们现在进入脚本执行的第二阶段,OP_DUP操作码(0x76)被执行。

4. 第3步到第6步: 这些在第一部分已经描述过了,从图13中应该是很明显的。

将原始交易广播到比特币网络

除了使用参考客户端外,我们有几个选择来广播原始交易。其中包括:

1. https://insight.bitpay.com/tx/send

2. https://live.blockcypher.com/btc-testnet/pushtx/

3. https://blockchain.info/pushtx

我们将使用Blockchain.info服务。在这里,我们也可以在广播前对我们的原始交易进行解码。

图14:解码后的原始交易。

最后一步是让我们的手工制作的原始交易进入比特币网络,最终被挖掘并记录在区块链上。

图15:广播原始交易到比特币网络。

脚注:

[1] 签名在比特币交易中的实现方式引起了很多争议,至今仍在持续。关于这个讨论的一些历史,你可能想看看传说中的发帖人DeathAndTaxes在Bitcointalk的帖子。在2014年5月的一篇文章中,他写道:"老实说,我不知道中本聪想用过于复杂的混乱的比特币Tx签名来完成什么,但考虑到其他一些有问题的决定(使用未压缩的pubkeys,非经典签名,在输入中包括pubkey,而它们可以从签名中重建,等等),我相信像中本聪那样聪明的ECDSA并不是他的强项。 他使用它,但他并不是这方面的专家。"

[2] 用Pybitcointools来加倍SHA256:

>>> dbl_sha256(binascii.unhexlify('message_hex'))

[3] 在 Pybitcointools 中:

>> b58check_to_hex('KyFvbums8LKuiVFHRzq2iEUsg7wvXC6fMkJC3PFLjGVQaaN9F1Ln')

'3cd0560f5b27591916c643a0b7aa69d03839380a738d2e912990dcc573715d2c01'

[4] 使用Pybitcointools,使用这个:

>>> privtopub('3cd0560f5b27591916c643a0b7aa69d03839380a738d2e912990dcc573715d2c01')

'03bf350d2821375158a608b51e3e898e507fe47f2d2e8c774de4a9a7edecf74eda'

注意,0x01的后缀被添加到私钥的十六进制中,以得到一个压缩的公钥。

[5] 获得该交易的交易哈希或交易ID是一个简单的问题,即对十六进制字符串进行双SHA256哈希。你得到的哈希值将与区块资源管理器中显示的相反,这意味着显示的哈希值是 "错误的方向"。

[6] 如果一个操作码位于0x01和0x4b(包括)之间,它是一个推送数据的操作,操作码本身就是要推送的以下数据的字节长度

评论

所有评论

推荐阅读