一.概述
在上一课中,我们使用了节点软件的getnewaddress调用来创建一个新的比特币地址。地址对应的私钥和交易的签名都由节点钱包模块管理,应用无法控制。在某些应用场景下,这可能会限制应用的功能实现。
如果我们想要最大的灵活性,我们需要放弃节点软件,使用C#代码离线生成地址。这些离线生成的地址自然不属于节点钱包管理,所以也会带来一些额外的问题,比如:
我们需要明白,比特币的内部机制,如密钥、地址、脚本等,都需要我们去构造和签署裸交易,而不是简单地调用sendtoaddress,需要我们去跟踪与这些地址相关的UTXO,而不是简单地调用listunspent,需要我们自己去汇总比特币余额。没有可用的getbalance。所有这些麻烦都是我们试图自己管理地址造成的。在某种程度上,一旦我们决定自己管理地址,我们基本上需要实现一个钱包模块:
在接下来的课程中,我们仍然使用NBitcoin来完成这些任务。如前所述,NBitcoin是上最完整的比特币协议实现。NET平台,它包含了超过RPC的封装。
其次,创建私钥和公钥
正如我们之前所知,公钥可以从私钥中导出,地址可以从公钥中导出。地址只是公钥的简明表达:
私钥本质上是一个随机数。从私钥出发,通过椭圆曲线乘法可以推导出公钥,从公钥出发,通过哈希算法可以得到比特币地址。这两种操作都是单向的、不可逆的,所以不可能从地址推导出公钥,也不可能从公钥推导出私钥。
地址来源于密钥,所以让我们首先使用NBitcoin的密钥类来创建公钥和私钥:
例如,下面的代码创建一个密钥对,并显示私钥和公钥的十六进制字符串:
Key Key=new Key();控制台。WriteLine(\’is compressed={0} \’),键。is compressed);//压缩公钥?字符串prv=编码器。十六进制编码数据(密钥。ToBytes());//16十进制字符串console.writeline (\’private={0} \’,PRV);控制台。WriteLine(\’private wif={0} \’),key。GetWif(网络。RegTest));//WIF格式私钥PubKey pubKey=key。PubKey//返回公钥对象console.writeline (\’public={0} \’,pubkey . to hex());//16十六进制字符串压缩后的公钥几乎比未压缩的公钥短一半,但用法没有区别。因此,key默认生成的所有公钥都使用压缩公钥。您可以使用IsCompressed属性来验证这一点。
公钥的hash属性可以得到公钥的Hash值,这是构造比特币地址的核心数据。我们先来看看:
KeyId hash=pubKey。哈希;控制台。WriteLine(\’hash={0} \’,hash。ToString());//使用NBitcoin的16进制字符串;使用NBitcoin。数据编码器;使用系统;命名空间new Key { class Program { static void Main(string[]args){ Key Key=new Key();控制台。WriteLine(\’compressed={0} \’),键。is compressed);控制台。WriteLine(\’prv key={0} \’),编码器。十六进制编码数据(密钥。ToBytes()));控制台。WriteLine(\’prv key wif={0} \’,key。GetWif(网络。RegTest));PubKey pubKey=key。PubKey控制台。WriteLine(\’pub key={0} \’,pub key。to hex());控制台。WriteLine(\’pub key hash={0} \’,pub key。哈希);控制台。ReadLine();} }}
三。创建P2PKH地址
在比特币网络中,地址的作用是接收以太币,以UTXO的形式留在交易中,等待消费。因此,地址最初与密钥相关:因为密钥对应于某个用户/身份。在比特币的演变过程中,先后出现了几种地址形式,但核心始终是一样的:识别目标用户/身份。
先说最简单的P2PKH地址。
P2PKH(Pay To Public Key Hash)地址是第一个定义的比特币地址,基于公钥的哈希生成:
P2H地址由三部分组成:8位网络前缀、160位公钥哈希和32位校验码后缀。这三部分拼接在一起,用base58编码得到P2PKH地址。
地址前缀
由于比特币的P2P协议目前应用于多个区块链,如比特币主链、测试链、莱特币、dash coin等。并且比特币有多个地址,前缀用于区分不同的区块链或地址格式。例如,对于比特币骨干网的P2PKH地址,其前缀为00;对于测试链的P2PKH地址,前缀是6F。不同的前缀经过base58编码后,就形成了不同的前导码,这就便于我们区分地址的类型:
更多关于地址前缀的信息,请参考官网的描述。
NBitcoin为不同的网络提供了相应的封装类,比如在这些网络封装类中标记不同的前缀方案。因此,当我们生成地址时,我们需要指定一个网络参数对象,以便正确应用地址前缀:
例如,以下代码在私有链模式下获取网络参数对象:
网络网络=网络。RegTest用NBitcoin中的BitcoinPubKeyAddress类来表征一个P2PKH的比特币地址,基于上面P2PKH地址的构成,很好理解。实例化P2PKH地址需要传入公钥散列和网络参数。例如,下面的代码创建一个新密钥,并以私有链模式返回其地址:
Key Key=new Key();BitcoinAddress addr=new BitcoinPubKeyAddress(key。公钥散列网络。RegTest);控制台。WriteLine(\’address={0} \’,addr);由于密钥和P2PKH地址是一一对应的,也可以通过私钥/公钥的方式直接返回P2PKH地址,例如:
BitcoinAddress addr=key。PubKey.GetAddress(网络。RegTest);使用NBitcoin使用系统;命名空间new p2pkh { class Program { static void Main(string[]args){ Key Key=new Key();PubKey pubKey=key。PubKey控制台。WriteLine(\’pub key={0} \’,pubKey);KeyId pubKeyHash=pubKey。哈希;控制台。WriteLine(\’pub key hash={0} \’,pubkey hash);控制台。WriteLine(\’script pubkey={0} \’,pubkey。script pubkey);BitcoinAddress addr=new bitcoinpubkey address(pubkey hash,Network。RegTest);控制台。WriteLine(\’ p2pkh address @ regtest={ 0 } \’,addr);控制台。WriteLine(\’ p2pkh address @ regtest={ 0 } \’,pubKey。GetAddress(网络。RegTest));控制台。WriteLine(\’ p2pkh address @ testnet={ 0 } \’,pubKey。GetAddress(网络。TestNet));控制台。WriteLine(\’ p2pkh address @ main={ 0 } \’,pubKey。GetAddress(网络。main));控制台。ReadLine();} }}
四。认证逻辑
BitcoinAddress类除了网络属性之外,还有一个属性ScriptPubKey值得研究:
ScriptPubKey属性用于获取该地址对应的公钥脚本,它将返回以下结果
公钥脚本的作用是什么?
我们先考虑一个相关的问题:如果一个UTXO标有接收地址,那么接收地址的持有者如何向节点证明这个UTXO是属于他的?
P2PKH地址来源于公钥,我们知道公钥可以验证私钥的签名。所以,只要对UTXO的交易进行报价,并提供交易的签名和公钥,节点就可以使用公钥来验证提交交易者是否是地址的持有者:
在上图中,事务2222的提交者需要为事务输入中引用的每个UTXO补充自己的公钥和事务签名,然后提交给节点。节点将根据以下逻辑验证提交者是否是地址X的真正持有者:
验证公钥:使用公钥计算地址,检查是否与地址X一致,不一致则拒绝交易。验证私钥:用交易和公钥验证提交的签名是否匹配,如果不一致,拒绝交易接受,广播交易。所以当我们向目标地址发送比特币的时候,实际上相当于给这个传出的UTXO加了一个由目标地址提供的锁,只有目标地址对应的私钥才能解锁这个锁。回到上一个问题,getScriptPubKey()方法返回的公钥脚本对应的是——中提供给发送方的这个锁发送给我的UTXO。请先用我提供的锁锁上。
五、P2PKH脚本执行原则
在上一节中,我们了解了节点如何验证事务提交者对UTXO的所有权。那么就很容易理解ScriptPub属性获取的脚本是什么了。
简单来说,比特币实际上将UTXO所有权的验证逻辑从节点分离到交易:在UTXO中定义一个脚本(公钥脚本),引用UTXO时定义另一个脚本(签名脚本)。当节点验证UTXO所有权时,它只需要拼接这两个脚本,并确定运行结果为真,这意味着事务提交者确实持有UTXO:
比特币使用的脚本采用简单的自定义语法,不支持循环,所以不是图灵的完整语言,但也降低了安全风险。脚本是使用预定义的指令编写的,从左到右执行。
例如,对于P2PKH地址,其对应的由助记符表示的两部分脚本如下:
script PubKey:op _ dupop _ hash 160 pubkey hash op _ equal verify op _ checksigscriptSig:SIG pubkey当最后一个节点合并脚本时,脚本pubkey会一直放在后面,而scriptSig会一直放在前面:
比特币脚本指令的操作需要堆栈。上图中列出了整个脚本中七条指令执行过程中,每条指令执行后的堆栈。接下来,让我们一步跟踪指令。
签名和公钥堆叠
指令1和2首先将签名和公钥推送到堆栈上。当执行第一条指令时,签名sig将被推到栈顶,当执行第二条指令时,公钥pubkey将被推到栈顶。
公钥验证
下一条指令3/4/5/6将验证公钥是否与scriptPubKey中保留的未锁定公钥哈希相匹配。
首先,使用指令OP_DUP复制栈顶成员,然后将其压入栈中。所以指令执行后,栈顶会有两个公钥pubkey。
接下来,我们使用指令OP_HASH160提取堆栈的顶部成员,并执行双重哈希计算(SHA-256-里姆佩德-160)。我们知道这是公钥哈希的算法。此指令的结果将被推送到堆栈,以便与scriptPubKey中保留的公钥哈希进行比较。
然后,脚本会将scriptPubKey中的预留解锁公钥哈希推到栈顶,这样栈顶就有两个公钥哈希:预留解锁公钥哈希和根据解锁脚本提供的公钥重新生成的公钥哈希。
指令OP_EQUALVERIFY比较提取的堆栈顶部的两个公钥哈希。如果它们不相等,它直接将事务标记为无效,并退出脚本执行。如果成功,栈顶只有两个成员:公钥和事务签名。
签名验证
指令OP_CHECKSIG负责提取栈顶的两个成员进行签名验证。如果验证成功,01将被推入堆栈。堆栈顶部的非零值意味着验证成功。否则,00被推入堆栈,这意味着验证失败。
不及物动词创建P2SH地址
基于上一节的学习,我们知道比特币的UTXO所有权的认证完全基于嵌入在交易中的两部分脚本。这种独立于节点的脚本验证机制为比特币的支付提供了极大的灵活性。
P2SH(Pay To Script Hash)地址是充分利用比特币的脚本能力的改进。很容易理解,这个地址是基于脚本的哈希构造的。——该脚本称为“赎回脚本”:
P2SH地址的公钥脚本只是验证UTXO消费者提交的序列化赎回脚本serializedRedeemScript是否与保留的scriptHash脚本Hash匹配:
如果上述验证通过,节点将扩展序列化的兑换脚本,并将其与签名再次拼接。例如,下图显示了一个简单的赎回脚本展开并与签名拼接后的完整脚本:
类似地,P2SH地址前缀因网络而异:
脚本
它实现了完整的NBitcoin脚本编写和执行功能,并使用ScriptBuilder类提供的方便函数来构造脚本对象:
基于P2SH地址的构造原理,我们可以创建任意脚本作为赎回脚本来创建P2SH地址。例如,在下面的代码中,第一个教师编写了前面描述的简单的赎回脚本,然后创建脚本的P2SH地址:
Key Key=new Key();Script rede Script=new Script(Op . GetPushOp(key .PubKey。ToBytes()),OpcodeType .OP _ check SIG);BitcoinAddress addr=new BitcoinScriptAddress(rede script .哈希,网络RegTest);控制台WriteLine(\’p2sh address={0} \’,addr);使用NBitcoin使用系统;命名空间new p2sh { class Program { static void Main(string[]args){ Key Key=new Key();Script rede Script=new Script(Op . GetPushOp(key .PubKey。ToBytes()),OpcodeType .OP _ check SIG);BitcoinAddress addr=new BitcoinScriptAddress(rede script .哈希,网络RegTest);控制台WriteLine(\’ p2sh addr @ regtest={ 0 } \’,addr);控制台WriteLine(\’ p2sh addr @ regtest={ 0 } \’,redeemScript .哈希。获取地址(网络RegTest));控制台. ReadLine();} }}
七、多重签名赎回脚本
P2SH地址应用最多的领域就是进行多重签名交易:一个UTXO的消费交易必须从n个参与者中至少获得m个签名才能被确认,这被称为n的m签名。
例如,一个三选二的多重签名的赎回脚本如下:
多重签名的赎回脚本主要使用指令OP_CHECKMULTISIG完成,它执行时需要栈顶的成员如下
我们可以使用脚本类从头创建多重签名赎回脚本,但更简单的是使用PayToMultiSigTemplate类直接返回赎回脚本:
当获得赎回脚本后,使用赎回脚本的混杂属性值,结合对应的网络实例,就可以获得这个赎回脚本对应的P2SH地址了。例如,下面的代码构造一个二选二签名脚本,并创建其对应的P2SH地址:
Key Key tommy=new Key();Key Key Jerry=new Key();var generator=PayToMultiSigTemplate .实例;脚本赎回脚本=生成器GenerateScriptPubKey(2,keyTommy .公共钥匙,钥匙杰瑞PubKey);BitcoinAddress addr=new BitcoinScriptAddress(rede script .哈希,网络RegTest);控制台WriteLine(\’ p2sh address @ regtest={ 0 } \’,addr);使用NBitcoin使用系统;使用系统100 . Linq命名空间newp 2 shmsig { class Program { static void Main(string[]args){ Key[]keys=new[]{ new Key(),new Key(),new Key()};PubKey[] pubKeys=keys .选择(key=key .PubKey).to array();for(var I=0;我是pubKeys .count();i ) {控制台WriteLine(\’pubkey#{0}={1} \’,I,pubKeys[I]);} Script redempt=PayToMultiSigTemplate .实例。GenerateScriptPubKey(2,pubKeys);控制台WriteLine(\’msig script={0} \’,redempt);BitcoinAddress addr=new BitcoinScriptAddress(redempt .哈希,网络RegTest);控制台. WriteLine(\’ msig p2sh address @ regtest={ 0 } \’,addr);控制台. ReadLine();} }}
作者:社会主义接班人出处:http://www.cnblogs.com/5ishare/
本文版权归作者和博客园共有,未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如文中有误,欢迎指出。以免更多的人被误导。