交易是比特币最重要的部分,比特币系统的其余部分是用于交易的。在前面的章节中,我们学习了各种共识算法和比特币POW共识的实现。本文将分析比特币交易相关的源代码。
1了解比特币交易
比特币区块链上任何交易的详细信息都可以通过比特币核心客户端的getrawtransaction和decoderwtransaction命令获取。下面是运行这两个命令后获得的事务的详细信息。这个例子来自《掌握比特币》一书:
{
quot;版本quot;:1,
quot;锁定时间quot;:0,
quot;vinquot;:[
{
quot;txidquot;:quot;7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18quot;,
quot;voutquot;:0,
quot;scriptSigquot;:quot;304502211008844D86652A3F47BA4746EC719BBFBD040A570B1CBB6498C75C4AE24CB02204B9F039FF08DF09CBE9F6ADDAC96029298CAD530A863EA8F533982C09DB8F8F6E3813[全部]0484ECC0D46F1918B30928FA0E4D46F44F30928FA0E4D60F8F0F0F0F735E7ADE7ADE8416AB9F423CC5412336363637678789D17278787D172787EC3457EEE4444F49938E44938D948DDE5CC17B4A10FA336A8D752ADFquot;,
quot;序列quot;:4294967295
}
],
quot;voutquot;:[
{
quot;值quot;:0.01500000,
quot;scriptPubKeyquot;:quot;OP_udup OP_uhash160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_ualverify OP_uchecksigquot;
}
{
quot;值quot;:0.08450000,
quot;scriptPubKeyquot;:quot;OP_udup OP_uhash160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_ualverify OP_uchecksigquot;,
}
]
}
让我们仔细看看上面的输出,看看比特币交易包含什么。
第一个是VIN字段,它是一个ON数组。数组中的每个元素表示事务的输入。在这个例子中,事务只有一个输入;
接下来是Vout字段,它也是一个ON数组。数组中的每个元素表示一个未使用的输出(utxo)。在本例中,事务生成两个新的utxo。
好吧,我们已经看到比特币交易包括两部分:输入和输出。输入表示要花费的比特币来自何处,而输出则表示输入所指向的比特币。换句话说,比特币交易实际上意味着价值的转移。以示例中的事务为例。交易所花费的比特币来自utxo,指数0为7957a35fe64f80d234d76d83a2a8f10d8149a41d81de548f0a65a8a999f18。utxo的目的地在Vout中指定。0.015比特币流向公钥ab68025513c3dbd2f7b92a94e0581f5d50f654e7对应的钱包,而0.0845比特币流向b68d60c536c2fd8aea53a8f3cc025a8对应的钱包公钥7f9b1a7f。
1.1贸易产出
事务的输出通常被称为utxo,也就是说,事务的输出不被消耗。从这个例子中,我们可以看到一个事务可能会产生多个utxo,这些utxo将用于后续事务。
事务输出包括以下内容:
Value:utxo的比特币数量;
Scriptpubkey:通常称为锁脚本,它决定谁可以使用utxo。只有提供了正确的解锁脚本,才能解锁和使用utxo;
1.2交易输入
事务的输入可以理解为指向utxo的指针,指示utxo用于事务的位置。事务输出包括以下内容:
Txid:utxo所在事务的哈希值,用于该事务;
Voit:索引。一个事务可以产生多个存储在数组中的utxo,索引是数组中utxo的下标。事务中的utxo可以通过(txid,VOUT)获取;
Scriptsig:Unlock脚本,用于解锁(txid,VOUT)所指向的utxo。如上所述,事务生成的每个utxo都将使用一个锁脚本设置,即scriptpubkey。解锁脚本scriptsig用于解锁。如果将utxo比作一个装有比特币的宝箱,那么scriptpubkey是宝箱上的锁,而scriptsig是钥匙。只有提供准确的钥匙,宝箱里的比特币才能解锁和消费。
1.3交易链
比特币的交易实际上是连锁的。事务通过事务输入与其前体事务相连接。假设张三钱包里有一个2比特币的utxo,然后张三把0.5个比特币转给他的朋友李思,产生一个类似于以下的交易:
事务T1的输入指向事务T0的utxo,它被分成两部分,形成两个新的utxo:0.5btc属于李四,剩下的1.5btc作为零钱返还给张三的钱包。假设李思在咖啡厅收到的0.5亿桶中消费了0.1万亿桶,交易链如下:
应该注意的是,每个新生成的事务的输入必须指向另一个事务的输出。比特币的交易在这条链条上连接在一起。通过输入事务,我们可以找到它所依赖的另一个事务。
2与事务相关的数据结构
现在我们已经直观地知道比特币交易是什么样子了,在本节中,我们将研究如何在代码中表示该交易。
2.1交易输入的数据结构
事务的输入由以下数据结构表示:
/**交易的输入。它包含上一个交易的位置
*事务和它声明的输出,以及与
*输出和公开密钥。
nbsp*/
CTxIn类
{
公众:
//此输入引用的utxo
COutPoint预测;
//解锁脚本,用于解锁输入指向的utxo
CScript脚本;
//相对时间锁定
uint32序列;
//证人证言
CScriptWitness脚本见证;//!仅通过CTransaction序列化
/*将事务中的每个输入的nSequence设置为该值
*禁用nLockTime。*/
静态常数uint32t SEQUENCE_u最终=0xffffffff;
/*以下标志适用于BIP 68的上下文*/
/*如果设置了此标志,则CTxIn::nSequence不会解释为
*相对锁定时间。*/
static const uint32t SEQUENCE_uktime_udisable_u标志=(1lt;lt;31);
/*如果CTxIn::nSequence编码相对锁定时间和此标志
*已设置,相对锁定时间的单位为512秒,
*否则它指定粒度为1的块。*/
static const uint32t SEQUENCE_uktime_uType_Ug标志=(1lt;lt;22);
/*如果CTxIn::nSequence编码相对锁定时间,则此掩码为
*应用于从序列字段提取锁定时间。*/
静态常数uint32t SEQUENCE_uLocktime_uMask=0x0000ffff;
/*为了使用相同的比特数对
*相同的挂钟持续时间,因为积木是自然形成的
*平均每600秒发生一次,最小粒度
*对于基于时间的相对锁定时间固定为512秒。
*从CTxIn::nSequence到seconds的转换由
*乘以512=2^9,或等效上移
*9位。*/
static const int SEQUENCE_LOCKTIME_ugranularity=9;
CTxIn()
{
nSequence=序列最终;
}
显式CTxIn(COutPoint prevoutIn,CScript scriptSigIn=CScript(),uint32_Tnsequencein=SEQUENCE_Ufinal);
CTxIn(uint256 hashPrevTx,uint32_utnout,CScript scriptSigIn=CScript(),uint32_Tnsequencein=SEQUENCE FINAL);
添加序列化方法;
模板
内联void SerializationOp(流和操作服务器操作){
读写(prevout);
读写(scriptSig);
读写(nSequence);
}
friend bool operator==(const CTxIna,const CTxInb)
{
返回(a.prevout==b.prevoutamp;
a、 scriptSig==b.scriptSigamp;
a、 n序列==b.n序列);
}
朋友,布尔接线员!=(const CTxIna,const CTxInb)
{
回来!(a==b);
}
std::string ToString()const;
}
代码中的coutpoint是输入指向的utxo。通过coutpoint,您可以找到输入指向的utxo
/**输出点-事务哈希和索引n的组合*/
类COutPoint
{
公众:
//utxo的事务哈希
uint256哈希;
//utxo索引
uint32;
COutPoint():n((uint32_ut)-1{}
COutPoint(const uint256amp;hashIn,uint32-nIn):哈希(hashIn),n(nIn){}
添加序列化方法;
模板
内联void SerializationOp(流和操作服务器操作){
读写(hash);
读写(n);
}
void SetNull(){哈希.SetNull();n=(uint32_Ut)-1;}
bool IsNull()const{return(哈希.IsNull()amp;n==(uint32_ut)-1);}
friend bool operatorlt;(const COutPointa,const COutPointb)
{
内部cmp=a。哈希。比较(b.杂凑;
返回cmplt;0 | |(cmp==0a.nlt;b.n);
}
friend bool operator==(常数COutPointa,const COutPointb)
{
返回(a.hash==b.hasha.n==b.n);
}
朋友,布尔接线员!=(常数COutPointa,const COutPointb)
{
回来!(a==b);
}
std::string ToString()const;
}
2.2事务输出的数据结构
事务输出的数据结构如下:
/**一个事务的输出。它包含下一个输入的公钥
*必须能够使用签名才能申领。
nbsp*/
CTxOut类
{
公众:
计算值;
CScript脚本pubkey;
CTxOut()
{
SetNull();
}
CTxOut(const CAmountnValueIn,CScript scriptPubKeyIn);
添加序列化方法;
模板
内联void SerializationOp(流和操作服务器操作){
读写(nValue);
读写(scriptPubKey);
}
无效SetNull()
{
n值=-1;
脚本pubkey.clear();
}
bool IsNull()常量
{
返回(nValue==-1);
}
friend bool operator==(const CTxOuta,const CTxOutb)
{
返回(a.n值==b.n值amp;
a、 scriptPubKey==b.scriptPubKey);
}
朋友,布尔接线员!=(const CTxOuta,const CTxOutb)
{
回来!(a==b);
}
std::string ToString()const;
}
正如你所看到的,这个定义非常简单。只有两个字段:camount表示utxo的比特币数量,scriptpubkey表示utxo的锁定脚本。
2.3 UTXO
utxo的概念在比特币中非常重要,比特币是由类币封装的
* *
*UTXO条目。
nbsp*
*序列化格式:
*-变量((CoinBase?1:0)|(高度lt;lt;1)
*-未用过的CTxOut(通过CTxOutCompressor)
nbsp*/
等级硬币
{
公众:
//! 未用事务输出
//utxo对应的紧急事务输出
CTX输出;
//! 包含事务是否为CoinBase
//utxo是CoinBase交易吗
无符号整数fCoinBase:1;
//! 在哪个高度上,该包含事务被包含在活动区块链中
//区块链上包含utxo的交易的高度
uint32;高度:31;
//! 根据CTxOut和高度/币库信息构造硬币。
硬币(CTxOutoutIn,int nHeightIn,bool fCoinBaseIn):输出(std::move(outIn)),fCoinBase(fCoinBaseIn),nHeight(nHeightIn){}
硬币(const CTxOutoutIn,int nHeightIn,bool fCoinBaseIn):输出(outIn),fCoinBase(fCoinBaseIn),nHeight(nHeightIn){}
无效清除(){
out.SetNull();
fCoinBase=假;
n高度=0;
}
//! 空构造函数
Coin():fCoinBase(false),nHeight(0){}
bool IsCoinBase()常量{
返回fCoinBase;
}
模板
void Serialize(Streams)常量{
断言(!IsSpent());
uint32代码=nHeight*2+fCoinBase;
●序列化(s,变量(代码));
●序列化(s,CTxOutCompressor(REF(out));
}
模板
作废未序列化(流和s){
uint32代码=0;
●未序列化(s,变量(代码));
nHeight=代码>1;
fCoinBase=代码1;
●取消序列化(s,CTxOutCompressor(out));
}
bool IsSpent()常量{
返回输出.IsNull();
}
sizet DynamicMemoryUsage()常量{
返回memusage::DynamicUsage(out.scriptPubKey);
}
}
比特币钱包实际上是一个由硬币组成的数据库。Bitpoint将从数据库加载硬币,并在启动时将其存储在内存中。
2.4交易脚本
事务输入解锁脚本scriptsig和事务输出锁定脚本scriptpubkey都是cscript类,cscript用于表示事务脚本。交易脚本是比特币中非常重要的内容。比特币提供的脚本语言可以完成非常复杂的功能。本文稍后将对其进行更详细的介绍。
/**序列化脚本,在事务输入和输出内部使用*/
类CScript:public CScriptBase
{
受保护的:
CScript和推送int64(int64)
{
如果(n==-1 | |(ngt;=1amp;nlt;=16))
{
向后推(n+(OP_1-1));
}
否则如果(n==0)
{
向后推(操作0);
}
其他的
{
*此lt;lt;CScriptNum::serialize(n);
}
return*这个;
}
公众:
CScript(){}
CScript(const_uIteratorpBegin,const_uIteratorpend):CScriptBase(pbegin,pend){}
CScript(std::vector::const_2;iterator pbegin,std::vector::const_uiterator pend):CScriptBase(pbegin,pend){}
CScript(const unsigned char*pbegin,const unsigned char*pend):CScriptBase(pbegin,pend){}
添加序列化方法;
模板
内联void SerializationOp(流和操作服务器操作){
读写(CScriptBase,*this);
}
CScriptoperator+=(const CScriptb)
{
保留(size()+b.size());
插入(end()、b.begin()、b.end());
return*这个;
}
friend CScript operator+(const CScripta,const CScriptb)
{
CScript ret=a;
ret+=b;
返回ret;
}
CScript(int64_2;t b){运算符lt;lt;(b);}
显式CScript(操作码类b){operatorlt;lt;(b);}
显式CScript(const CScriptNumb){operatorlt;lt;(b);}
显式CScript(const std::vectorb){operatorlt;lt;(b);}
CScriptoperatorlt;lt;(int64_2;t b){return push_u64(b);}
CScriptoperatorlt;lt;(操作码类操作码)
{
如果(操作码lt;0 | |操作码gt;0xff)
throw std::运行时错误(quot;script::operatorlt;():无效的操作码quot;);
insert(end(),(unsigned char)操作码);
return*这个;
}
CScriptoperatorlt;(const CScriptNumb)
{
*此lt;lt;b.getvch();
return*这个;
}
CScriptoperatorlt;lt;(const std::vectorb)
{
if(b.size()lt;OP_upushdata1)
{
insert(end(),(unsigned char)b.size());
}
else if(b.size()lt;=0xff)
{
插入(end(),操作PUSHDATA1);
insert(end(),(unsigned char)b.size());
}
else if(b.size()lt;=0xffff)
{
插入(end(),OP_upushdata2);
uint8_Ut数据[2];
WriteLE16(data,b.size());
插入(end(),data,data+sizeof(data));
}
其他的
{
插入(end(),操作PUSHDATA4);
uint8_Ut数据[4];
writel32(data,b.size());
插入(end(),data,data+sizeof(data));
}
插入(end()、b.begin()、b.end());
return*这个;
}
CScriptoperatorlt;(const CScriptb)
{
//我不确定这是应该推送脚本还是连接脚本。
//如果有将脚本推送到脚本上的用法,请删除此成员fn
断言(!quot;警告:使用lt;lt;将CScript推到CScript上可能不是有意的,请使用+连接!quot;);
return*这个;
}
bool GetOp(const_uiteratorpc,opcodetypeopcodeRet,std::vectorvchRet)常量
{
return GetScriptOp(pc,end(),opcodeRet,vchRet);
}
bool GetOp(const_uiteratorpc,opcodetypeopcodeRet)常量
{
返回GetScriptOp(pc,end(),opcodeRet,nullptr);
}
/**对小整数进行编码/解码:*/
静态int DecodeOP_N(操作码类操作码)
{
如果(操作码==操作码0)
返回0;
断言(操作码>=操作码1amp;操作码lt;=操作码16);
返回(int)操作码-(int)(OP_1-1);
}
静态操作码类编码器
{
断言(ngt;=0amp;nlt;=16);
如果(n==0)
返回OP_0;
返回(opcodetype)(OP_1+n-1);
}
* *
*0.6之前的版本,比特币始终计数支票多西格
*为20个信号。使用pay-to-script哈希,情况发生了变化:
*在scriptsig中序列化的CHECKMULTISIGs是
*计算得更准确,假设它们是
*nbsp。。。检查多行扫描。。。
nbsp*/
无符号int GetSigOpCount(bool fAccurate)const;
* *
*准确计数信号,包括
*按脚本付费哈希事务:
nbsp*/
无符号int GetSigOpCount(const CScriptscriptSig)const;
bool IsPayToScriptHash()常量;
bool IsPayToWitnessScriptHash()常量;
bool IsWitnessProgram(intversion,std::vectorprogram)常量;
/**由IsStandardTx和P2SH/BIP62 VerifyScript调用(这使得它具有一致性关键性)。*/
bool IsPushOn(const_uiterator pc)常量;
bool IsPushOn()常量;
/**检查脚本是否包含有效的操作代码*/
bool HasValidOps()常量;
* *
*返回脚本执行时是否保证失败,
*不考虑初始堆栈。这允许对输出进行删减
*在输入UTXO集合时立即。
nbsp*/
bool IsUnspendable()常量
{
return(size()gt;0amp;*begin()==OP_Ureturn)|(size()gt;MAX_uscript_Usize);
}
无效清除()
{
//默认的prevector::clear()不会释放内存
CScriptBase::clear();
将_U收缩到_fit();
}
}
Cscript从scriptbase继承:
* *
*我们为脚本使用prevector以减少相当大的内存开销
通常包含少量小元素的向量。
*2015年10月的测试表明,使用这种方法,dbcache内存使用率降低了23%
*,使初始同步速度加快13%。
nbsp*/
typeDeFi prevectorlt;28,无符号=quot;quot; char=quot;quot;gt;CScriptBase;
Cscriptbase实际上是一个自定义向量。Cscript重写了lt;lt;运算符,使向向量添加数据变得更加容易。
2.5交易
正如我们所见,比特币交易由一组输入和一组输出组成:
/**在网络上广播并包含在
*块。一个事务可以包含多个输入和输出。
nbsp*/
类转换
{
公众:
//默认事务版本。
static const int32当前版本=2;
//更改默认事务版本需要两个步骤:第一步
//调整中继策略,通过转发**标准版本,然后再更新
//在当前版本和
//**标准版本相同。
static const int32_xt MAX_u标准_uversion=2;
//局部变量被设置为常量,以防止意外修改
//不更新缓存的哈希值。然而,CTransaction并非如此
//实际上是不可变的;实现了反序列化和赋值,
//绕过康斯特斯。这是安全的,因为他们更新了整个
//结构,包括哈希。
//事务的所有输入
常数标准::向量机;
//事务的总输出
常数标准::向量;
//事务处理版本
32转换常数;
//事务锁定时间用于控制在该时间之后可以花费事务的输出
const uint32uut nLockTime;
私人电话:
/**仅限内存。*/
const uint256哈希;
uint256 ComputeHash()const;
公众:
/**一个限定为null()*/
CTransaction();
/**将CMutableTransaction转换为CTransaction。*/
交易(const CMutableTransactiontx);
交易(CMutableTransactiontx);
模板
内联void Serialize(Streams)const{
序列化事务(*this,s);
}
/**提供此反序列化构造函数,而不是非序列化方法。
*不可能取消序列化,因为它需要覆盖常量字段。*/
模板
CTransaction(反序列化类,流s):CTransaction(CMutableTransaction(反序列化,s)){}
bool IsNull()常量{
返回车辆识别号为空()amp;空的();
}
const uint256amp;GetHash()const{
返回哈希值;
}
//计算包含事务和见证数据的哈希
uint256 GetWitnessHash()const;
//返回txouts的总和。
CAmount GetValueOut()常量;
//GetValueIn()是cconsViewCache上的一个方法,因为
//输入必须已知才能在中计算值。
* *
*获取总事务大小(字节),包括见证数据。
*quot;总尺寸quot;在BIP141和BIP144中定义。
*@return总事务大小(字节)
nbsp*/
无符号int GetTotalSize()const;
bool IsCoinBase()常量
{
返回(车辆识别号()==1和vin[0]。prevout.IsNull());
}
friend bool operator==(const CTransactiona,const CTransactionb)
{
返回a.hash==b.hash;
}
朋友,布尔接线员!=(const CtrTransactiona,const CtrTransactionb)
{
返回a.hash!=b.散列;
}
std::string ToString()const;
bool HasWitness()常量
{
对于(尺寸i=0;ilt;车辆识别号();i++){
如果(!车辆识别号[i]。scriptWitness.IsNull()) {
返回true;
}
}
返回false;
}
}
除了事务输入和输出,还有事务版本和事务时间锁nlocktime。事务时间锁用于控制事务的输出。只有经过一段时间后,它才能被使用。比特币2对此领域进行了详细描述。
此外,需要注意的是,ctransaction中的所有字段都用const修饰符修饰,这表示一旦创建了ctransaction对象,就不能更改其内容。因此,ctransaction是一个不可变的对象。相应地,事务有一个可变版本
/**CTransaction的可变版本。*/
结构CMutableTransaction
{
标准::向量机;
标准::向量;
int32转换;
uint32次非锁定时间;
CMutableTransaction();
CMutableTransaction(const CtrTransactiontx);
模板
内联void Serialize(Streams)const{
序列化事务(*this,s);
}
模板
内联void取消序列化(Streams){
非证券化交易(*this,s);
}
模板
CMutableTransaction(反序列化类、流和s){
取消序列化;
}
/**计算此CMutableTransaction的哈希。这是根据
*f,与CTransaction中的GetHash()相反,后者使用缓存的结果。
nbsp*/
uint256 GetHash()const;
friend bool operator==(const CMutableTransactiona,const CMutableTransactionb)
{
返回a.GetHash()==b.GetHash();
}
bool HasWitness()常量
{
对于(尺寸i=0;ilt;车辆识别号();i++){
如果(!车辆识别号[i]。scriptWitness.IsNull()) {
返回true;
}
}
返回false;
}
}
cmutabletransaction和ctransaction的字段完全相同。不同的是字段前面缺少const修饰符。因此,在生成cmutabletransaction对象后,可以重新分配其字段。
3交易的创建
在理解了与交易相关的数据结构之后,在本节中,我们将分析比特币交易是如何创建的。
交易可以通过比特币的onap命令create transaction创建。此命令需要以以下形式传递ON参数:
quot;1。/quot;inputs/quot;(array,必选)on对象的on数组/nquot;
quot;[/nquot;
quot;{/nquot;
quot;/quot;txid/quot;:/quot;id/quot;,(字符串,必需)事务id/nquot;
quot;/quot;vout/quot;:n,(数字,必需)输出编号/nquot;
quot;/quot;序列/quot;:n(数字,可选)序列号/nquot;
quot;}/nquot;
quot;,…/nquot;
quot;]/nquot;
quot;2。/quot;输出/quot;;(数组,必需)具有输出的on数组(键值对)/nquot;
quot;[/nquot;
quot;{/nquot;
quot;/quot;address/quot;:x.xxx,(obj,可选)键值对。键(字符串)是比特币地址,值(浮点或字符串)是quot;+货币单位+quot;/nquot;的金额
quot;},/nquot;
quot;{/nquot;
quot;/quot;data/quot;:/quot;hex/quot;(obj,可选)键值对。密钥必须是/quot;data/quot;,值为十六进制编码数据/nquot;
quot;}/nquot;
quot;,…以上表单的更多键值对。出于兼容性的原因,直接保存键值对的字典也是/nquot;
quot;被接受为第二个参数。/nquot;
quot;]/nquot;
quot;3。锁定时间;(数字,可选,默认值=0)原始锁定时间。非0值也锁定时间激活输入/nquot;
quot;4。可替换;(布尔值,可选,默认值=false)将此事务标记为BIP125可替换。/nquot;
quot;允许用更高费用的交易取代此交易。如果提供,如果显式序列号不兼容,则为错误。/nquot;
您需要在参数中指定每个输入和输出。在实际使用比特币钱包时,这些肮脏的工作都是由钱包替我们完成的。
让我们看看create transaction如何创建比特币交易。此命令的实现位于rawtransaction.cpp文件中国人:
静态单值createrawtransaction(const ONRPCRequestrequest)
{
//如果输入参数是非法的,将抛出一个异常来提示参数格式
如果(请求帮助世界环境学会请求参数大小()lt;2 | |请求参数大小()>4){
抛出std::运行时错误(
//关闭“叮当”格式
quot;createrawtransaction[{/quot;txid/quot;:/quot;id/quot;,/quot;vout/quot;:n},…][{/quot;address/quot;:amount},{/quot;data/quot;:/quot;hex/quot;},…](锁定时间)(可替换)/nquot;
quot;/n创建一个使用给定输入并创建新输出的事务。/nquot;
quot;输出可以是地址或数据。/nquot;
quot;返回十六进制编码的原始事务。/nquot;
quot;请注意事务的输入没有签名,并且/nquot;
quot;它不会存储在钱包中或传输到网络。/nquot;
quot;/nArguments:/nquot;
quot;1。/quot;inputs/quot;(array,必选)on对象的on数组/nquot;
quot;[/nquot;
quot;{/nquot;
quot;/quot;txid/quot;:/quot;id/quot;,(字符串,必需)事务id/nquot;
quot;/quot;vout/quot;:n,(数字,必需)输出编号/nquot;
quot;/quot;序列/quot;:n(数字,可选)序列号/nquot;
quot;}/nquot;
quot;,…/nquot;
quot;]/nquot;
quot;2。/quot;输出/quot;;(数组,必需)具有输出的on数组(键值对)/nquot;
quot;[/nquot;
quot;{/nquot;
quot;/quot;address/quot;:x.xxx,(obj,可选)键值对。键(字符串)是比特币地址,值(浮点或字符串)是quot;+货币单位+quot;/nquot;的金额
quot;},/nquot;
quot;{/nquot;
quot;/quot;data/quot;:/quot;hex/quot;(obj,可选)键值对。密钥必须是/quot;data/quot;,值为十六进制编码数据/nquot;
quot;}/nquot;
quot;,…以上表单的更多键值对。出于兼容性的原因,直接保存键值对的字典也是/nquot;
quot;被接受为第二个参数。/nquot;
quot;]/nquot;
quot;3。锁定时间;(数字,可选,默认值=0)原始锁定时间。非0值也锁定时间激活输入/nquot;
quot;4。可替换;(布尔值,可选,默认值=false)将此事务标记为BIP125可替换。/nquot;
quot;允许用更高费用的交易取代此交易。如果提供,如果显式序列号不兼容,则为错误。/nquot;
quot;/n结果:/nquot;
quot;/quot;事务/quot;;(字符串)事务的十六进制字符串/nquot;
quot;/示例:/nquot;
+HelpExampleCli(quot;CreateRawTransaction6035,quot;/quot;[{///quot;txid///quot;:///quot;,///quot;vout///quot;:0}]/quot;/quot;[{///quot;地址///quot;:0.01}]/quot;quot;)
+HelpExampleCli(quot;CreateRawTransaction6035,quot;/quot;[{///quot;txid///quot;:///quot;,///quot;vout///quot;:0}]/quot;/quot;[{///quot;数据///quot;:///quot;:///quot;:///quot;}]/quot;quot;)
+帮助示例RPC(quot;CreateRawTransaction6035,quot;/quot;[{///quot;txid///quot;:///quot;myid///quot;,///quot;vout///quot;:0}]/quot;,/quot;[{///quot;地址///quot;:0.01}]/quot;quot;)
+HelpExampleRpc(quot;CreateRawTransaction6035,quot;/quot;[{///quot;txid///quot;:///quot;myid///quot;,///quot;vout///quot;:0}]/quot;,/quot;[{///quot;data///quot;:///quot;00010203///quot;}]/quot;quot;)
//打开“叮当”格式
);
}
//检查参数
RPCTypeCheck(请求参数{
单值::VARR,
UniValueType(),//ARR或OBJ,稍后检查
单值::VNUM,
唯一值::VBOOL
},真的
);
如果(请求.params[0].isNull()||请求.params[1] .isNull())
throw ONRPCError(RPC_Uinvalid_参数,quot;无效参数,参数1和2必须是非nullquot;);
单值输入=请求.params[0].get_uarray();
常量布尔输出值请求.params[1] .isObject();
单值输出=输出?
请求.params[1] .getobj():
请求.params[1] .get_uarray();
//生成交易对象
CMutableTransaction rawTx;
//从参数中提取事务的锁定时间(如果提供)
如果(!请求.params[2] .isNull()){
int64_UtnLocktime=请求.params[2] .get_int64();
if(nLockTimelt;0 | | nLockTimegt;std::numeric_ulimits::max())
抛出ONRPCError(RPC_0无效的参数,quot;无效的参数,锁定时间超出范围quot;);
rawTx.n时钟时间=n时钟时间;
}
布尔RBF选项=请求.params[3] .isTrue();
//解析参数并生成事务的输入
for(unsigned int idx=0;idxlt;输入.大小();idx++){
const UniValueinput=输入[idx];
常量UniValueo=输入.获取对象();
//此输入指向的事务
uint256 txid=ParseHashO(o,quot;txidquot;);
//事务中输入指向的utxo的索引
const UniValuevout_v=查找值(o,quot;voutquot;);
如果(!voutv.isNum())
抛出ONRPCError(RPC_u无效的参数,quot;无效的参数,缺少vout键quot;);
int nOutput=vout_v.get_uint();
如果(输出lt;0)
抛出ONRPCError(RPC_0无效的参数,quot;无效的参数,vout必须是正的quot;);
uint32序列;
if(RBF选项){
nSequence=MAX_BIP125_uRBF_u序列;
}否则如果(rawTx.n时钟时间{
nSequence=std::numeric_ulimits::max()-1;
}其他{
nSequence=std::numeric_ulimits::max();
}
//如果传入parameters对象,则设置序列号
const UniValuesequenceObj=查找值(o,quot;sequencequot;);
如果(序列对象.isNum()) {
int64序列号64=sequenceObj.getint64();
if(seqNr64lt;0 | | seqNr64gt;标准::数值极限::max()){
抛出ONRPCError(RPC_0无效的参数,quot;无效的参数,序列号超出范围quot;);
}其他{
nSequence=(uint32_uT)seqNr64;
}
}
CTxIn in(COutPoint(txid,nOutput),CScript(),nSequence);
rawTx.vin.推送后退(in);
}
std::设置目的地;
如果(!输出_uis_j){
//将键值对数组转换为dict
UniValue outputs UUDICT=UniValue(UniValue::VOBJ);
对于(尺寸i=0;ilt;输出.大小();++i){
const UniValueoutput=输出[i];
如果(!输出.isObject()) {
无效的RPC参数,参数为无效的RPC参数;
}
如果(输出.大小() !=1){
throw ONRPCError(RPC_Uinvalid_参数,quot;无效参数,键值对必须正好包含一个keyquot;);
}
输出udict.pushKVs公司(输出);
}
输出=std::移动(outputs_uDict);
}
//根据参数生成事务的输出
for(const std::stringname输出.getKeys()) {
if(名称_==quot;dataquot;){
std::vectordata=ParseHexV(输出[name].getValStr(),quot;Dataquot;);
CTxOut输出(0,CScript()lt;lt;OPrawTx.vout.push返回(out)返回lt;lt;数据);
}其他{
//解析目标地址(比特币最终流向何处)
CTxDestination destination=解码目的地(名称);
如果(!IsValidDestination(目的地)){
抛出ONRPCError(RPC_U无效的地址或密钥,std::string(quot;无效比特币地址:quot;)+名称;
}
如果(!目的地.插入(第二个目的地){
抛出ONRPCError(RPC_Uinvalid_参数,std::string(quot;无效参数,重复地址:quot;)+名称;
}
//基于地址为事务输出生成锁脚本
CScript scriptPubKey=GetScriptForDestination(目的地);
CAmount nAmount=AmountFromValue(输出[名称]);
CTxOut out(nAmount,scriptPubKey);
rawTx.vout.push后退(退出);
}
}
如果(!请求.params[3] .isNull()rbfOptIn!=信号输入RBF(rawTx){
抛出ONRPCError(RPC_0无效参数,quot;无效参数组合:序列号与可替换选项冲突6035);
}
//对事务进行编码并返回
返回EncodeHexTx(rawTx);
}
整个过程并不复杂:每个输入和输出都从参数中进行解析,并填充到cmutabletransaction对象中。**,对对象进行编码并返回。但有两个问题需要注意:
(1) 代码在事务输入中看不到解锁脚本scriptsig;
(2) 如何生成事务输出的锁脚本需要理解;
对于第一个问题(在分析事务签名时将得到回答),让我们首先来看第二个问题:如何为事务输出生成锁脚本。生成锁脚本的代码如下:
CScript scriptPubKey=GetScriptForDestination(目的地);
让我们来看看这个函数的实现
CScript GetScriptForDestination(const CTxDestination和dest)
{
CScript脚本;
boost::应用访问者(CScriptVisitorscript),dest;
返回脚本;
}
首先,该方法接受ctxdestination类的参数,其定义如下:
* *
*具有特定目标的txout脚本模板。它可以是:
**CNoDestination:未设置目的地
**CKeyID:TX_UkeyHash目的地(P2PKH)
**CScriptID:TX_uScriptHash目标(P2SH)
**WitnessV0ScriptHash:TX_uv0_uscripthash目的地(P2WSH)
**WitnessV0KeyHash:TX_Uv0_Uv0KeyHash目的地(P2WPKH)
**见证未知:TX?见证?未知目的地(P2W??)
*CTxDestination是比特币地址中编码的内部数据类
nbsp*/
typeDeFi boost::variantCTxDestination;
Ctxdestination是boost::variant类,它表示特定的比特币地址。variant可以理解为增强的联合类。从这种类的定义中,我们还可以看到比特币支持以下类的地址:
Ckeyid:公钥,适用于P2P KH标准交易,锁定脚本中指定的比特币接收方的公钥;
Cscriptid:适用于p2sh标准事务的地址;
见证v0scripthash:p2wsh事务的地址;
见证v0keyhash:p2wpkh事务的地址;
可以看出,不同类的交易有不同类的地址。因此,在为事务输出生成锁定脚本时,具体处理应基于事务类。为了避免许多if-else分支,比特币使用boost提供的访问者设计模式进行处理,并提供cscriptviewer为不同类的地址生成相应的锁定脚本
类CScriptVisitor:public boost::static_uvisitor
{
私人电话:
CScript*脚本;
公众:
显式CScriptVisitor(CScript*scriptin){script=scriptin;}
bool operator()(const CNoDestinationdest)常量{
脚本->清除();
返回false;
}
//P2P KH标准交易
bool operator()(const CKeyIDkeyID)常量{
脚本->清除();
*scriptlt;lt;OP_uduplt;lt;OP_uHash160lt;lt;ToByteVector(keyID)lt;lt;OP_UuVerifylt;lt;OP_UcheckSig;
返回true;
}
//P2sh标准交易
bool operator()(const CScriptIDscriptID)常量{
脚本->清除();
*脚本lt;lt;OP_160lt;lt;ToByteVector(scriptID)lt;lt;OP_uEqual;
返回true;
}
//P2wsh事务
bool operator()(const WitnessV0KeyHashid)常量
{
脚本->清除();
*编写脚本lt;lt;OP;0lt;lt;ToByteVector(id);
返回true;
}
//P2wkh交易
bool operator()(const WitnessV0ScriptHashid)常量
{
脚本->清除();
*编写脚本lt;lt;OP;0lt;lt;ToByteVector(id);
返回true;
}
bool operator()(const WitnessUnknownid)常量
{
脚本->清除();
*脚本lt;lt;CScript::EncodeOPN(id.版本)lt;lt;标准::矢量(id.程序, id.程序+ 内径长度);
返回true;
}
}
我们现在不关心脚本的生成,因为我们现在不关心脚本的生成。本文稍后将详细解释事务脚本的操作原理。
4交易签名
本节回答了上一节中提到的第一个问题:如何生成用于事务输入的解锁脚本script SIG。让我们先弄清楚一个问题:为什么我们需要签署一项交易,签名的原则是什么?
4.1为什么交易需要签名
在比特币中签署交易的主要功能是证明某人拥有某个utxo。假设张三将1btc转移给李四,交易中会产生一个1btc utxo。为了确保这个utxo只能由Li-Si使用,必须对事务进行数字签名。
4.2交易签字原则
事务签名实际上是事务的数字签名。数字签名在加密算法中已有阐述。让我们再回顾一下:假设张三在一个不可靠的通信信道上给李思发了一条消息msg。李四怎么能确认发信人是张三而不是其他人?
(1) 张三使用散列生成MSG的抽象D
(2) 张三使用一些签名算法f和他的私钥为摘要D生成签名s
(3) 张三给李四发签名和短信;
(4) 李思使用张三的公钥公钥公钥从接收到的签名s中提取消息摘要d
(5) Li Si对接收到的消息msg进行哈希处理以获得摘要D1。那么,D比较是否相同可以证明这条信息真的来自张三;
比特币交易的签名相同,其中MSG为交易,F为比特币采用的ECDSA椭圆曲线签名算法。我们以最常见的P2P-KH事务为例进行说明。
假设张三把1btc转给李四,张三的钱包产生了一笔交易。在事务T中,有一个utxo指向Li-Si,值1btc。为了保证这个utxo将来只能由李四使用,张三在锁脚本scriptpubkey中设置两个条件:
(C1)消费者必须提供自己的公钥,公钥的散列值需要等于Li Si公钥的哈希值。假设Li Si的公钥为p,消费者提供的公钥为pubkey,则必须满足以下要求:
张三将李四的公钥hash(P)写入scriptpubkey脚本;
(C2)消费者提供的签名必须正确。
然后,李四的钱包生成交易T,如果他想花这笔utxo,李四需要提供两件事:李四的公钥公钥公钥和他在交易T上的签名。
(1) Li-Si使用hash为事务t生成摘要D
(2) 李思使用ECDSA签名算法,用他的私钥为摘要d生成数字签名s
(3) 李思将自己的公钥公钥公钥和签名s写入事务T的解锁脚本scriptsig中,然后将事务T广播到网络上;
(4) 网络中的节点接收事务T,验证事务,并确认Li-Si确实可以花费utxo。首先对接收到的事务t的锁脚本中的公钥pubkey进行散列,看是否与utxo的锁脚本中的公钥散列相同(条件C1满足);然后检查签名:首先,节点对接收到的事务进行哈希,生成事务摘要D-ා:
然后,使用公钥pubkey从签名s中提取事务摘要d
如果能证明这个交易t确实是李四产生的,他有权花费utxo。
4.3交易签名的生成
我们再次回顾了比特币交易签名的原理。接下来,让我们看看事务输入的解锁脚本(公钥+签名)是如何生成的。第3节介绍了通过创建事务来创建事务的过程。但是,create事务生成的事务的输入缺少脚本scriptsig。解锁脚本需要另一个onapi:signrawtransaction。此命令所需的参数如下:
quot;1。/quot;hexstring/quot;;(字符串,必需)事务十六进制字符串/nquot;
quot;2。/quot;prevtxs/quot;;(字符串,可选)以前依赖事务输出的on数组/nquot;
quot;[(on对象的on数组,或#39;null#39;如果未提供)/nquot;
quot;{/nquot;
quot;/quot;txid/quot;:/quot;id/quot;,(字符串,必需)事务id/nquot;
quot;/quot;vout/quot;:n,(数字,必需)输出编号/nquot;
quot;;/quot;scriptPubKey/quot;:/quot;hex/quot;,(字符串,必需)脚本键/nquot;
quot;;/quot;赎回脚本/quot;:/quot;hex/quot;,(字符串,P2SH或P2WSH必需)兑换脚本/nquot;
quot;/quot; amount/quot;:值(数字,必需)花费的金额/nquot;
quot;}/nquot;
quot;,…/nquot;
quot;]/nquot;
quot;3。/quot;privkeys/quot;;(string,可选)用于签名的base58编码私钥的on数组/nquot;
quot;[(on字符串数组,或#39;null#39;如果未提供)/nquot;
quot;;/quot;privatekey/quot;;(字符串)base58编码的私钥/nquot;
quot;,…/nquot;
quot;]/nquot;
quot;4。/quot;sigashtype/quot;;(字符串,可选,默认值=ALL)签名哈希类。必须是/nquot;之一
quot;;quot;全部/quot;/nquot;
quot;;quot;无/quot;/nquot;
quot;/quot;单个/quot;/nquot;
quot;;/quot;所有人都可以支付/quot;/nquot;
quot;;/quot;无|任何人都可以支付/quot;/nquot;
quot;;/quot;单一|任何人都可以支付/quot;/nquot;
Prevtxs提供事务输入指向的utxo,privkeys是解锁这些utxo所需的私钥,signhashtype指定是只对事务的一部分签名,还是对所有事务输入进行签名。
签约流程如下:
单值signrawtransaction(const ONRPCRequestrequest){
#ifDeFi启用钱包
CWallet*const pwallet=GetWalletForONRPCRequest(请求);
#结束语
//检查参数格式。如果格式不正确,则抛出异常以提示正确的用法
如果(请求帮助世界环境学会请求参数大小第1241页请求参数大小()>4)
抛出std::运行时错误(
quot;signrawtransaction/quot;hexstring/quot;([{/quot;txid/quot;:/quot;id/quot;,/quot;vout/quot;:n,/quot;scriptPubKey/quot;:/quot;hex/quot;,/quot;RequireScript/quot;:/quot;hex/quot;},…/quot;privatekey1/quot;,…]sigashType/nquot;
quot;/未预制。对原始事务的输入进行签名(序列化,十六进制编码)。/nquot;
quot;第二个可选参数(可能为null)是以前事务输出的数组,该事务输出为/nquot;
quot;此交易依赖于区块链,但可能尚未在区块链中。/nquot;
quot;第三个可选参数(可以为null)是base58编码的private/nquot;数组
quot;密钥,如果给定,将是用于签署事务的唯一密钥。/nquot;
#ifDeFi启用钱包
+帮助要求密码(pwallet)+quot;/nquot;
#结束语
quot;/nArguments:/nquot;
quot;1。/quot;hexstring/quot;;(字符串,必需)事务十六进制字符串/nquot;
quot;2。/quot;prevtxs/quot;;(字符串,可选)以前依赖事务输出的on数组/nquot;
quot;[(on对象的on数组,或#39;null#39;如果未提供)/nquot;
quot;{/nquot;
quot;/quot;txid/quot;:/quot;id/quot;,(字符串,必需)事务id/nquot;
quot;/quot;vout/quot;:n,(数字,必需)输出编号/nquot;
quot;;/quot;scriptPubKey/quot;:/quot;hex/quot;,(字符串,必需)脚本键/nquot;
quot;;/quot;赎回脚本/quot;:/quot;hex/quot;,(字符串,P2SH或P2WSH必需)兑换脚本/nquot;
quot;/quot; amount/quot;:值(数字,必需)花费的金额/nquot;
quot;}/nquot;
quot;,…/nquot;
quot;]/nquot;
quot;3。/quot;privkeys/quot;;(string,可选)用于签名的base58编码私钥的on数组/nquot;
quot;[(on字符串数组,或#39;null#39;如果未提供)/nquot;
quot;;/quot;privatekey/quot;;(字符串)base58编码的私钥/nquot;
quot;,…/nquot;
quot;]/nquot;
quot;4。/quot;sigashtype/quot;;(字符串,可选,默认值=ALL)签名哈希类。必须是/nquot;之一
quot;;quot;全部/quot;/nquot;
quot;;quot;无/quot;/nquot;
quot;/quot;单个/quot;/nquot;
quot;;/quot;所有人都可以支付/quot;/nquot;
quot;;/quot;无|任何人都可以支付/quot;/nquot;
quot;;/quot;单一|任何人都可以支付/quot;/nquot;
quot;/n结果:/nquot;
quot;{/nquot;
quot;;/quot;hex/quot;:/quot;value/quot;,(字符串)带签名的十六进制编码原始事务/nquot;
quot;/quot; complete/quot;:true | false,(布尔值)如果交易具有完整的签名集/nquot;
quot;/quot;错误/quot;:[(对象的on数组)脚本验证错误(如果有)/nquot;
quot;{/nquot;
quot;;/quot;txid/quot;:/quot;hash/quot;,(string)引用的上一个事务的哈希/nquot;
quot;/quot;vout/quot;:n,(数字)要花费并用作输入的输出的索引/nquot;
quot;;/quot;scriptSig/quot;:/quot;hex/quot;,(字符串)十六进制编码的签名脚本/nquot;
quot;/quot;序列/quot;:n,(数字)脚本序列号/nquot;
quot;;/quot;错误/quot;:/quot;text/quot;;(字符串)与输入相关的验证或签名错误/nquot;
quot;}/nquot;
quot;,…/nquot;
quot;]/nquot;
quot;}/nquot;
quot;/示例:/nquot;
+帮助示例CLI(quot;SignRawTransaction6035,quot;/quot;myhex/quot;quot;)
+帮助示例RPC(quot;SignRawTransaction6035,quot;/quot;myhex/quot;quot;)
);
如果(!IsDeprecatedRPCEnabled(quot;SignRawTransaction6035)){
throw ONRPCError(RPC方法已弃用,
quot;signrawtransaction已弃用,将在v0.18中完全删除。quot;
quot;要在v0.17中使用signrawtransaction,请使用-deprecatedrpc=signrawtransaction重新启动比特币。/nquot;
quot;项目应在升级到v0.18quot;之前过渡到使用signrawtransactionwithkey和signrawtransactionwithwallet);
}
//检查参数
RPCTypeCheck(请求.params,
正确);
//执行ONRPCRequest以传递到正确的signrawtransaction*命令
ONRPCRequest新请求;
新建请求.id= 请求.id;
新建请求.params.setArray();
//用私钥签名
如果(!请求.params[2] .isNull()){
//提供的私钥
新建请求参数推送背面(请求.params[0]);
//注意:对于signrawtransactionwithkey,prevtxs和privkeys是相反的
新建请求参数推送背面(请求.params[2] );
新建请求参数推送背面(请求.params[1] );
新建请求参数推送背面(请求.params[3] );
return signrawtransactionwithkey(新请求);
}其他{
#ifDeFi启用钱包
//否则,请使用不接受privkeys参数的钱包进行签名
//用钱包签名
新建请求参数推送背面(请求.params[0]);
新建请求参数推送背面(请求.params[1] );
新建请求参数推送背面(请求.params[3] );
return signrawtransactionwithwallet(新请求);
#其他
//如果我们已经做到了这一点,那么钱包是禁用的,没有私钥,所以失败在这里。
抛出ONRPCError(RPC_u无效的参数,quot;没有可用的私钥。quot;);
#结束语
}
我们只分析通过这里提供的私钥进行签名的过程。这是通过signraw事务和key方法实现的
静态单值signrawtransactionwithkey(const ONRPCRequestrequest)
{
//参数检查
如果(请求帮助世界环境学会请求参数大小()lt;2 | |请求参数大小()>4)
抛出std::运行时错误(
quot;signrawtransactionwithkey/quot;hexstring/quot;[/quot;privatekey1/quot;,…]([{/quot;txid/quot;:/quot;id/quot;,/quot;vout/quot;:n,/quot;scriptPubKey/quot;:/quot;hex/quot;,/quot;rewritescript/quot;:/quot;hex/quot;},…]sigashtype)/nquot;
quot;/nSign原始事务的输入(序列化,十六进制编码)。/nquot;
quot;第二个参数是base58编码的private/nquot;数组
quot;将是唯一用于签署交易的密钥。/nquot;
quot;第三个可选参数(可能为null)是以前事务输出的数组,该事务输出为/nquot;
quot;此交易依赖于区块链,但可能尚未在区块链中。/nquot;
quot;/nArguments:/nquot;
quot;1。/quot;hexstring/quot;;(字符串,必需)事务十六进制字符串/nquot;
quot;2。/quot;privkeys/quot;(string,必选)用于签名的base58编码私钥的on数组/nquot;
quot;[(on字符串数组)/nquot;
quot;/quot;privatekey/quot;(字符串)base58编码的私钥/nquot;
quot;,…/nquot;
quot;]/nquot;
quot;3。/quot;prevtxs/quot;(字符串,可选)以前依赖事务输出的on数组/nquot;
quot;[(on对象的on数组,如果未提供,则为#39;null#39;)/nquot;
quot;{/nquot;
quot;;/quot;txid/quot;:/quot;id/quot;,(字符串,必需)事务id/nquot;
quot;/quot;vout/quot;:n,(数字,必需)输出编号/nquot;
quot;/quot;scriptPubKey/quot;:/quot;hex/quot;,(字符串,必需)脚本键/nquot;
quot;/quot;redemescript/quot;:/quot;hex/quot;,(字符串,P2SH或P2WSH必需)兑换脚本/nquot;
quot;/quot; amount/quot;:值(数字,必需)花费的金额/nquot;
quot;}/nquot;
quot;,…/nquot;
quot;]/nquot;
quot;4。/quot;sigashtype/quot;(字符串,可选,默认值=ALL)签名哈希类。必须是/nquot;之一
quot;;quot;全部/quot;/nquot;
quot;;quot;无/quot;/nquot;
quot;/quot;单个/quot;/nquot;
quot;;/quot;所有人都可以支付/quot;/nquot;
quot;;/quot;无|任何人都可以支付/quot;/nquot;
quot;;/quot;单一|任何人都可以支付/quot;/nquot;
quot;/n结果:/nquot;
quot;{/nquot;
quot;/quot;hex/quot;:/quot;value/quot;,(字符串)带签名的十六进制编码原始事务/nquot;
quot;/quot; complete/quot;:true | false,(布尔值)如果事务具有完整的签名集/nquot;
quot;/quot;错误/quot;:[(对象的on数组)脚本验证错误(如果有)/nquot;
quot;{/nquot;
quot;/quot;txid/quot;:/quot;hash/quot;,(string)引用的上一个事务的哈希/nquot;
quot;/quot;vout/quot;:n,(数字)要花费并用作输入的输出的索引/nquot;
quot;/quot;scriptSig/quot;:/quot;hex/quot;,(字符串)十六进制编码的签名脚本/nquot;
quot;;/quot;序列/quot;:n,(数字)脚本序列号/nquot;
quot;/quot;错误/quot;:/quot;text/quot;;(字符串)与输入相关的验证或签名错误/nquot;
quot;}/nquot;
quot;,…/nquot;
quot;]/nquot;
quot;}/nquot;
quot;/示例:/nquot;
+帮助示例CLI(quot;signrawtransactionwithkeyquot;、quot;/quot;myhex/quot;quot;)
+帮助示例RPC(quot;signrawtransactionwithkeyquot;、quot;/quot;myhex/quot;quot;)
);
RPCTypeCheck(请求.params,真);
//解码原始事务
CMutableTransaction mtx;
如果(!解码HEXTX(mtx,请求.params[0].getstr(),true)){
抛出ONRPCError(RPC反序列化错误,quot;TX解码失败AD6035);
}
//从提供的私钥中获取相应的公钥,私钥-公钥对保存在密钥库中
CBasicKeyStore密钥库;
常量唯一值和键=请求.params[1] .get_uarray();
for(unsigned int idx=0;idxlt;键.大小();++idx){
单值k=键[idx];
CKey key=解码密码(k.get_ustr());
如果(!密钥有效()) {
抛出ONRPCError(RPC_u无效的地址或密钥,quot;无效的私钥quot;);
}
密钥库.AddKey(钥匙);
}
//对事务输入进行签名并生成解锁脚本scriptsig
返回SignTransaction(mtx,请求.params[2] ,keystore,true,请求.params[3] );
}
在该函数中,从提供的私钥中获取相应的公钥,然后将私钥-公钥对存储在密钥库中以备后续检索。真正的事务签名在signtransaction中:
单值SignTransaction(CMutableTransactionmtx,const UniValueprevTxsUnival,CBasicKeyStore*密钥库,bool is_uTemp_ukeystore,const UniValuehashType)
{
//获取以前的事务(输入):
CCoinsView视图假人;
cconsViewCache视图(viewDummy);
{
锁2(cs主,内存池.cs);
cconsViewCacheviewChain=*pcoinsTip;
cconsViewMempool viewMempool(viewChain,mempool);
视图.SetBackend(viewMempool);//暂时切换缓存后端到db+mempool视图
对于(const CTxIntxin:mtx.vin{
查看.AccessCoin( txin.prevout公司);//将条目从viewChain加载到view中;可能会失败。
}
视图.SetBackend(viewDummy);//返回以避免锁定mempool的时间过长
}
//添加RPC调用中给定的先前txout:
//找到事务输入指向的utxo并将其添加到内存中
如果(!PrevTxUnival.isNull()) {
单值prevTxs=prevTxsUnival.get数组();
for(unsigned int idx=0;idxlt;上一个TXS.size();++idx){
const UniValuep=prevTxs[idx];
如果(!p、 isObject()){
抛出ONRPCError(RPC反序列化错误,quot;预期对象为{/quot;txid39;/quot;,/quot;vout/quot;,/quot;scriptPubKey/quot;}quot;);
}
UniValue prevOut=p.get_uobj();
//参数检查
RPCTypeCheckObj(预览,
{
{quot;txidquot;,UniValueType(UniValue::VSTR)},
{quot;voutquot;,UniValueType(UniValue::VNUM)},
{quot;scriptPubKeyquot;,UniValueType(UniValue::VSTR)},
};
//从参数中获取txid和Vout,并使用指向事务输入引用的coutpoint生成utxo
uint256 txid=ParseHashO(prevOut,quot;txidquot;);
int nOut=查找_U值(prevOut,quot;voutquot;);
如果(nOutlt;0){
抛出ONRPCError(RPC反序列化错误,quot;vout必须为正Equot;);
}
输出(txid,nOut);
//解析参数以获取事务输入所指向的utxo的锁脚本
std::vectorpkData(ParseHexO(prevOut,quot;scriptPubKeyquot;));
CScript scriptPubKey(pkData.begin(), pkData.end文件());
//找到事务输入指向的utxo
{
const Coin和Coin=查看.AccessCoin(出局);
如果(!硬币()amp;coin.out.scriptPubKey!=脚本pubkey){
std::string err(quot;以前的输出scriptPubKey不匹配:/nquot;);
err=err+ScriptToAStr(coin.out.scriptPubKey)+quot;/内华达州:/nquot;+
ScriptToAStr(scriptPubKey);
抛出ONRPCError(RPC反序列化错误,err);
}
投币纽币;
newcoin.out.scriptPubKey=scriptPubKey;
newcoin.out.n值=0;
如果(prevOut.存在(quot;金额6035){
newcoin.out.n值=AmountFromValue(查找值(prevOut,quot;amount6035));
}
纽币。恩=1;
查看.AddCoin(out,std::move(newcoin),真);
}
//如果提供了REQUESTScript且未使用本地钱包(私钥
//给定),请向密钥库中添加redempscript,以便对其进行签名:
//如果是p2sh或p2wsh事务,如果指定了赎回脚本,则需要将其添加到密钥库中
如果(是临时密钥库amp;)scriptPubKey.IsPayToScriptHash() || scriptPubKey.IsPayToWitnessScriptHash())) {
RPCTypeCheckObj(预览,
{
{quot;RequireScriptquot;,UniValueType(UniValue::VSTR)},
};
UniValue v=查找值(prevOut,quot;赎回脚本quot;);
如果(!v、 isNull()){
std::vectorsdata(ParseHexV(v,quot;赎回脚本quot;));
CScript赎回脚本(rsData.begin(), rsData.end.结束());
密钥库-gt;AddCScript(reducescript);
//还自动添加P2WSH包装的脚本版本(处理P2SH-P2WSH)。
密钥库-gt;AddCScript(GetScriptForWitness(RemeveScript));
}
}
}
}
//hashtype分析
int nHashType=SIGHASHuall;所有;
如果(!hashType.isNull()) {
静态标准::mapmapSigHashValues={
,
,
,
,
,
,
}
std::字符串strHashType=hashType.getstr();
如果(MapsigashValues.count(strHashType){
nHashType=MapsigashValues[strHashType];
}其他{
抛出ONRPCError(RPC_0无效的参数,quot;无效的sighash paramquot;);
}
}
bool fHashSingle=((nHashTypeamp;~SIGHASHuanyonecanpay)==SIGHASH单曲;
//脚本验证错误
UniValue vErrors(UniValue::VARR);
//对的常量部分使用CTransaction
//避免重新洗牌的交易。
施工转换txConst(mtx);
//尽我们所能签字:
//在事务输入上生成签名
for(unsigned int i=0;ilt;mtx.vin.尺寸();i++){
//找到事务输入指向的utxo
Ctx和txin=mtx.vin[i] ;
const Coin和Coin=查看.AccessCoin( txin.prevout公司);
如果(硬币()) {
TxInErrorToON(txin、vErrors、quot;输入未找到或已spentquot;);
继续;
}
//获取输入指向的utxo锁脚本
const CScriptprevPubKey=coin.out.scriptPubKey;
固定金额和金额=硬币价值;
签名数据sigdata;
//如果有相应的输出,则只需标记SIGHASH_35;SINGLE:
//事务的解锁脚本生成并存储在sigdata中
如果(!(ilt;mtx.vout.size公司())) {
ProduceSignature(*keystore,MutableTransactionSignatureCreator(mtx,i,amount,nHashType),prevPubKey,sigdata);
}
sigdata=组合签名(prevPubKey,TransactionSignatureChecker(txConst,i,amount),sigdata,DataFromTransaction(mtx,i));
//将生成的解锁脚本填充到事务输入中
更新事务(mtx,i,sigdata);
ScriptError serror=脚本错误确定;
//脚本验证
如果(!验证脚本(txin.scriptSig,prevPubKey,amp;txin.见证人,标准脚本验证标志,事务签名checker(txConst,i,amount),serror){
if(serror==脚本错误无效堆栈操作){
//无法对输入进行签名,验证失败(可能尝试部分签名)。
TxInErrorToON(txin,vErrors,quot;无法对输入进行签名,堆栈大小无效(可能缺少键)quot;;
}其他{
TxInErrorToON(txin,vErrors,ScriptErrorString(serror));
}
}
}
布尔fComplete=错误。空();
单值结果(UniValue::VOBJ);
结果.pushKV(quot;hexquot;,编码HEXTX(mtx));
结果.pushKV(quot;完成quot;,F完成);
如果(!错误。空()) {
结果.pushKV(quot;错误Squot;,错误);
}
返回结果;
}
撇开细节不谈,这个函数做两件事:为事务输入生成解锁脚本并验证脚本。
4.3.1为事务输入生成解锁脚本
为事务输入生成解锁脚本是在producesignature方法中执行的
bool ProduceSignature(const SigningProviderprovider,const BaseSignatureCreatorcreator,const CScriptfromPubKey,SignatureDatasigdata)
{
标准::矢量结果;
txnouttype whichype(血);
//签名
bool SOLved=SignStep(provider,creator,fromPubKey,result,whichype,SigVersion::BASE);
bool P2SH=假;
CScript下标;
sigdata.scriptWitness.stack.clear();
//对于p2sh事务,需要对子脚本进行签名
if(已解决whichType==TXSCRIPTHASH)
{
//Solver返回需要求值的下标;
//**一个脚本是从中得到的签名
//然后是序列化的下标:
下标=CScript(result[0].begin(),result[0].end());
SOLved=SOLvedSignStep(provider,creator,subscript,result,whichype,SigVersion::BASE)whichType!=TX_SCRIPTHASH;
P2SH=真;
}
//P2wkh事务需要见证脚本的签名
if(已解决whichType==TX_u-WITNESS_V0_ukeyhash)
{
CScript见证脚本;
witnessscriptlt;lt;OP_Uduplt;lt;OP_uHash160lt;lt;ToByteVector(结果[0])lt;lt;OP_UuVerifylt;lt;OP_UcheckSig;
txnouttype亚;
SOLved=已解决SignStep(提供者、创建者、见证脚本、结果、子类、SigVersion::WITNESS_V0);
sigdata.scriptWitness.stack=结果;
结果。清除();
}
//P2wsh事务
else if(已解决whichType==TX_u-WITNESS_V0_u-SCRIPTHASH)
{
CScript见证脚本(result[0].begin(),result[0].end());
txnouttype亚;
SOLved=SOLvedSignStep(提供者、创建者、见证脚本、结果、子类、SigVersion::WITNESS_V0)suype!=TXSCRIPTHASHsuype!=TX_u-WITNESS_V0_u-SCRIPTHASHsuype!=TX_U见证_V0_ukeyhash;
结果推送后退(std::vector(见证脚本.begin(), 见证脚本.end()));
sigdata.scriptWitness.stack=结果;
结果。清除();
}
如果(P2SH){
结果推送后退(std::vector(下标.开始(), 下标结束()));
}
//将生成的解锁脚本写入sigdata
sigdata.scriptSig=PushAll(结果);
//验证脚本
返回已解决和验证脚本(sigdata.scriptSig,从pubkey,amp;sigdata.scriptWitness,标准脚本验证标志,创建者.检查器());
}
让我们看一下函数的参数:provider:keystore,它存储公钥-私钥对,如前面的代码所述;
创建者:BaseSignitureCreator类的实例,用于最终为事务生成签名;
Frompubkey:cscript类,事务输入引用的utxo的锁定脚本;
Sigdata:signaturedata类,输出参数,用于存储生成的解锁脚本;
在像洋葱一样逐层之后,signstep方法用于对事务输入进行签名
* *
*使用creator生成的签名签名scriptPubKey。
*在scriptSigRet中返回签名(如果scriptPubKey不能签名,则返回false),
*除非whichTypeRet是TX-SCRIPTHASH,在这种情况下scriptSigRet是赎回脚本。
*如果无法完全满足scriptPubKey,则返回false。
nbsp*/
静态bool SignStep(const SigningProviderprovider,const BaseSignatureCreatorcreator,const CScriptscriptPubKey,
std::vectorret,txnouttypewhichTypeRet,SigVersion SigVersion)
{
CScript脚本;
uint160 h160;
返回清除();
std::矢量解决方案;
//解析事务输入引用的utxo的锁脚本。锁脚本类存在于输出参数typeret中,锁脚本的数据存储在vector vSOLutions中
如果(!解算器(scriptPubKey,whichTypeRet,vSolutions))
返回false;
密钥ID;
//签名根据锁脚本的类执行
开关(whichTypeRet)
{
案例发送-非标准:
案例发送空数据:
案件发送证人未知:
返回false;
case TX_Ukey://锁脚本的类为P2P K
keyID=CPubKey(vSolutions[0]).GetID();
return Sign1(provider,keyID,creator,scriptPubKey,ret,sigversion);
case TX_UkeyHash://lock script的类是p2pkh
keyID=CKeyID(uint160(vSolutions[0]);
如果(!Sign1(provider,keyID,creator,scriptPubKey,ret,sigversion))
返回false;
其他的
{
CPubKey vch;
提供程序.GetPubKey(密钥ID,vch);
返回推送后退(ToByteVector(vch));
}
返回true;
case TXüScripthash://lock script的类是p2sh
如果(提供者.GetCScript(uint160(vSolutions[0]),scriptRet){
返回推送std(返回矢量(scriptRet.begin(), 脚本返回结束()));
返回true;
}
返回false;
case TX_umultisig//锁定脚本为Multisig(multisignature)
返回推送back(valtype());//解决CHECKMULTISIG bug的方法
return(sign(provider,vSolutions,creator,scriptPubKey,ret,sigversion));
case TX_uwitness_V0_ukeyhash://lock script的类是p2wkh
返回推送后退(vSolutions[0]);
返回true;
case TX_uwitness_V0_uscripthash//锁脚本的类是p2wsh
cripmd160().Write(vSolutions[0][0],vSolutions[0].size()).Finalize(h160.begin());
如果(提供者.GetCScript(h160,脚本){
返回推送后退(std::vector(scriptRet.begin(), 脚本返回结束()));
返回true;
}
返回false;
违约:
返回false;
}
}
第一步是解析事务输入引用的utxo的锁脚本,然后根据不同的锁脚本类进行签名。这里我们以最常见的P2P-KH事务为例。其他事务类的原理类似。
(1) 解析锁脚本
首先,让我们看看源代码,看看锁脚本是如何解析的
布尔解算器(const CScriptscriptPubKey,txnouttypetypeRet,std::vectorlt;std::vectorgt;vSolutionsRet)
{
//模板
//P2P K/P2P KH/multisig事务的锁脚本模板
静态std::multimapmTemplates;
如果(mTemplates.empty())
{
//标准发送,发送方提供公钥,接收方添加签名
mTemplates.insert(std::make_Upair(TX_Ukey,CScript()lt;lt;OP_bkeylt;lt;OP_uCheckSig));
//比特币地址tx,发送方提供公钥哈希,接收方提供签名和公钥
mTemplates.insert(std::make_Upair(TX_UkeyHash,CScript()lt;lt;OP_u-DUPlt;lt;OP_uHash160lt;lt;OP_uUkeyHashlt;lt;OP_uCheckSig));
//发送方提供N个公钥,接收方提供M个签名
mTemplates.insert(std::make_Upair(TX_Umultisig,CScript()lt;lt;OP_uUallIntegerlt;lt;OP_uUkeyslt;lt;OP_uUkeyslt;lt;OP_uCheckMultiSig));
}
vSolutionsRet.clear();
//pay to script hash的快捷方式,它比其他类更受约束:
//它总是OP_uHash16020[20 byte hash]OP_uequal
//锁脚本的类是p2sh,这种类的锁脚本的格式是Op_hash16020[20 byte hash]Op_uequal,脚本中的2-22是20字节的数据,放入vSOLutionsret中
如果(scriptPubKey.IsPayToScriptHash())
{
typeRet=TX_4;SCRIPTHASH;
std::vectorhashBytes(scriptPubKey.begin()+2,scriptPubKey.begin()+22);
vSolutionsRet.pushback(哈希字节);
返回true;
}
//p2wkh/p2wsh的治疗
int见证版本;
std::矢量见证程序;
如果(scriptPubKey.iswitness程序(见证版本,见证程序){
如果(见证版本==0amp;见证程序.大小()==见证人?V0?KEYHASH?大小){
typeRet=TX_u-WITNESS_V0_ukeyhash;
vSolutionsRet.push返回(见证程序);
返回true;
}
如果(见证版本==0amp;见证程序.大小()==见证文件uv0uScriptHashuSize){
typeRet=TX_u-WITNESS_V0_u-SCRIPTHASH;
vSolutionsRet.push返回(见证程序);
返回true;
}
如果(见证版!=0){
typeRet=未知的TX_u证人;
vSolutionsRet.pushback(std::vector{(unsigned char)见证版本});
vSolutionsRet.push后退(std::move(witnessprogram));
返回true;
}
返回false;
}
//可证明可剪枝的数据携带输出
/ /
//只要脚本通过了isUnpentable()测试,除了第一个
//字节通过了IsPushOn()测试,我们不关心
//脚本。
如果(scriptPubKey.size()gt;=1amp;scriptPubKey[0]==OPRETURNamp;scriptPubKey.IsPushOn( scriptPubKey.begin()+1){
typeRet=发送空数据;
返回true;
}
//扫描模板
//对于P2P KH/P2P K/multisig类,扫描模板进行解析
const CScriptscript1=scriptPubKey;
for(const std::pairtplate:m模板)
{
const CScriptscript2=t板.秒;
vSolutionsRet.clear();
操作码类opcode1,opcode2;
标准::矢量vch1,vch2;
//比较
//PC1指向utxo锁脚本,PC2指向脚本模板
CScript::const_uiterator pc1=script1.begin();
CScript::const_uiterator pc2=script2.begin();
while(真)
{
//如果两个指针同时到达末尾,则找到并返回与模板匹配的事务类
if(pc1==script1.end()amp;pc2==script2.end())
{
//找到匹配项
类RET=t板.first;
if(typeRet==TX_umultisig)
{
//TX_umultisig的附加检查:
无符号字符m=vSolutionsRet.前端()[0];
无符号字符n=vSolutionsRet.back()[0];
如果(mlt;1 | | nlt;1 | | mgt;n |vSolutionsRet.size()-2!=n)
返回false;
}
返回true;
}
//获取运算符和操作数
如果(!脚本1.GetOp(pc1,opcode1,vch1))
休息;
如果(!脚本2.GetOp(pc2,opcode2,vch2))
休息;
//模板匹配操作码:
//几个操作员的处理
if(opcode2==操作PUBKEYS)
{
while(CPubKey::ValidSize(vch1))
{
vSolutionsRet.push背面(vch1);
如果(!脚本1.GetOp(pc1,opcode1,vch1))
休息;
}
如果(!脚本2.GetOp(pc2,opcode2,vch2))
休息;
//正常的情况是失败
//其他if/else语句
}
if(opcode2==OP_u-PUBKEY)
{
如果(!CPubKey::有效大小(vch1))
休息;
vSolutionsRet.push背面(vch1);
}
else if(opcode2==OP_uPubKeyHash)
{
如果(vch1.size()!=尺寸(uint160))
休息;
vSolutionsRet.push背面(vch1);
}
else if(opcode2==OP_uallinteger)
{/vSolutions上的单字节小整数
如果(操作码1==操作0||
(操作码1gt;=操作码1amp;操作码1lt;=操作码16))
{
char n=(char)CScript::DecodeOP_n(opcode1);
vSolutionsRet.push背部(valtype(1,n));
}
其他的
休息;
}
否则如果(操作码1!=操作码2 | | vch1!=vch2)
{
//其他的必须完全匹配
休息;
}
}
}
vSolutionsRet.clear();
typeRet=TX_u非标准;
返回false;
}
以上代码对不同类有不同的处理。对于P2P-KH/P2P-K/multisig,采用模板进行匹配处理。看代码可能无法理解这里的逻辑,画一个图来说明它会很清楚。以P2P-KH为例
首先,锁定脚本的模板,如下所示:
然后看看utxo锁脚本是什么样子的。在创建事务的代码(cscriptvvisitor)中检查为事务输出生成锁脚本的代码。对于P2P KH类:
bool operator()(const CKeyIDkeyID)常量{
脚本->清除();
*scriptlt;lt;OP_uduplt;lt;OP_uHash160lt;lt;ToByteVector(keyID)lt;lt;OP_UuVerifylt;lt;OP_UcheckSig;
返回true;
}
这里,我们要注意cscript重载的实现,以vector为参数,其中keyID是一个160位无符号整数,占20个字节。根据cscript overloadedlt;lt;operator的实现,生成的锁脚本的长度如下所示:
20表示脚本中的下一个元素是一个20字节的公钥。
根据代码,从模板脚本和锁脚本开始,对每个操作符逐一进行解析。首先,Op-DUP和Op-Hash什么都不做,只需将指针向前移动。当它达到以下状态时,需要小心:
模板的指针易于处理并直接向前移动,但锁定脚本的处理方式不同。看看用于解析运算符和参数的代码。代码已根据示例进行了注释:
bool GetScriptOp(CScriptBase::const_uiteratorpc,CScriptBase::const_uiterator end,opcodetypeopcodeRet,std::vector*pvchRet)
{
opcodeRet=操作无效代码;
如果(pvchRet)
pvchRet->清除();
if(pcgt;=结束)
返回false;
//阅读说明
if(结束-pclt;1)
返回false;
//在我们的示例中,操作码为20,然后指针向前移动到下一个元素(20字节的公钥)
无符号int操作码=*pc++;
//立即数操作数
//pushdata4的值是0x4e,在这个例子中,操作码是20,所以它进入分支
if(操作码lt;=操作码PUSHDATA4)
{
无符号int nSize=0;
//操作码是20,操作码Pushdata1是0x4c,输入这个分支
如果(操作码lt;操作码PUSHDATA1)
{
nSize=操作码;
}
else if(操作码==操作码PUSHDATA1)
{
if(结束-pclt;1)
返回false;
nSize=*pc++;
}
else if(操作码==操作码PUSHDATA2)
{
if(结束-pclt;2)
返回false;
nSize=ReadLE16(pc[0]);
pc+=2;
}
else if(操作码==操作码PUSHDATA4)
{
if(结束-pclt;4)
返回false;
nSize=ReadLE32(pc[0]);
pc+=4;
}
if(end-pclt;0 | |(unsigned int)(end-pc)lt;nSize)
返回false;
//接下来的20个字节的公钥填充在输出参数中
如果(pvchRet)
pvchRet-gt;分配(pc,pc+nSize);
//指针移动20个字节
pc+=nSize;
}
opcodeRet=静态播放(操作码);
返回true;
}
**,提取20个字节的公钥值并再次保存。操作员解析后,状态如下:
(2)签名
解析utxo锁脚本之后,下一步是开始签名。或者用上面的例子来分析。signstep函数调用SOLver解析锁脚本,获取锁脚本的类及其参数,然后根据不同的锁脚本类进行处理。对于P2P KH类,处理如下:
案例TX_UkeyHash:
keyID=CKeyID(uint160(vSolutions[0]);
如果(!Sign1(provider,keyID,creator,scriptPubKey,ret,sigversion))
返回false;
其他的
{
CPubKey vch;
提供程序.GetPubKey(密钥ID,vch);
返回推送后退(ToByteVector(vch));
}
返回true;
首先,获取Solver函数解析的20字节公钥,然后调用Sign1函数对其进行签名。
静态bool Sign1(const SigningProviderprovider,const CKeyIDaddress,const BaseSignatureCreatorcreator,const CScriptscriptCode,std::vectorret,SigVersion SigVersion)
{
std::矢量vchsig;
如果(!创建者.CreateSig(提供程序、vcshig、地址、脚本代码、sigversion)
返回false;
返回推送背面(vchSig);
返回true;
}
通过传入的参数basedesignaturecreator::createsig进行签名非常简单,并将生成的签名保存在输出参数ret中。这里,传入参数的basedesignaturecreator是mutabletransactionsignaturecreator类。回忆一下以前的producesignature代码:
如果(!(ilt;mtx.vout.size公司())) {
ProduceSignature(*keystore,MutableTransactionSignatureCreator(mtx,i,amount,nHashType),prevPubKey,sigdata);
}
如您所见,在创建mutabletransactionsignaturecreator时,会传入要签名的事务输入的索引、事务输入引用的utxo比特币数量和hashtype来查看签名过程
bool TransactionSignatureCreator::CreateSig(const SigningProviderprovider,std::vectorvchSig,const CKeyIDaddress,const CScriptscriptCode,SigVersion SigVersion)const
{
//从密钥库中获取与公钥对应的私钥
钥匙;
如果(!提供程序.GetKey(地址、钥匙)
返回false;
//在见证脚本中,禁止使用未压缩密钥进行签名
如果(sigversion==sigversion::WITNESS_V0amp!key.IsCompressed键())
返回false;
//为事务生成哈希摘要
uint256 hash=SignatureHash(脚本代码,*txTo,nIn,nHashType,amount,sigversion);
//事务的哈希摘要使用私钥进行数字签名,签名存储在输出参数vcshig中
如果(!钥匙。签字(散列,vcshig)
返回false;
vchSig.push后退((无符号字符)nHashType);
返回true;
}
到目前为止,交易的签名已经完成。
4.3.2验证脚本
事务签名完成后,事务输入将有一个解锁脚本。接下来,我们需要验证生成的unlock脚本是否能够匹配事务输入所指向的utxo锁脚本(一个密钥、一个锁、一个radish、一个pit)。无论是本地节点还是网络中的其他节点,对于新的事务,必须先验证脚本,然后才能将事务添加到内存事务池中,等待矿工挖矿。
4.3.2.1比特币脚本语言
比特币脚本语言是一种基于堆栈的非图灵完全脚本语言。我们知道区块链2.0的标志之一就是智能合约,以太坊就是代表。以太坊提供了一种坚实的语言,可以用来编写非常复杂的去中心化DAPP。比特币的脚本语言类似于SOLidness,它还可以编写逻辑复杂的执行脚本。我们前面介绍的P2P KH是用脚本语言编写的简单脚本的一个例子。
比特币的脚本语言不支持类似for的循环,这样可以防止恶意节点编写带有无限循环的恶意脚本来触发DoS攻击。但逻辑上像“如果其他”是支持的。
比特币脚本语言支持的运算符在枚举操作码中定义,此处列出。读者可以理解:
/**脚本操作码*/
枚举操作码类
{
//推送值
操作0=0x00,
OP_ofalse=操作0,
操作PUSHDATA1=0x4c,
操作PUSHDATA2=0x4d,
操作PUSHDATA4=0x4e,
u4f否,
操作保留=0x50,
操作1=0x51,
OP_utrue=操作1,
操作2=0x52,
操作3=0x53,
操作4=0x54,
OP_5=0x55,
运算6=0x56,
操作7=0x57,
操作8=0x58,
操作9=0x59,
OP_10=0x5a,
OP_11=0x5b,
OP_12=0x5c,
操作13=0x5d,
OP_14=0x5e,
OP_15=0x5f,
操作16=0x60,
//控制
操作否=0x61,
运算速度=0x62,
如果=0x63,
操作通知=0x64,
操作验证=0x65,
操作VERNOTIF=0x66,
运算结果=0x67,
运算结束=0x68,
操作验证=0x69,
运算返回=0x6a,
//堆栈操作
OP_u-TOALTSTACK=0x6b,
操作从AltStack=0x6c,
OP_2DROP=0x6d,
OP_2DUP=0x6e,
OP_3DUP=0x6f,
OP_2OVER=0x70,
OP_2ROT=0x71,
OP_2SWAP=0x72,
操作IFDUP=0x73,
工作深度=0x74,
操作下降=0x75,
运算重复=0x76,
操作压力=0x77,
操作结束=0x78,
操作选择=0x79,
操作辊=0x7a,
运行=0x7b,
操作交换=0x7c,
操作塔克=0x7d,
//拼接操作
操作猫=0x7e,
运算子串=0x7f,
左运算=0x80,
右图=0x81,
操作尺寸=0x82,
//位逻辑
操作反转=0x83,
运算且=0x84,
运算或=0x85,
运算异或=0x86,
运算等于0x87,
运算平均值=0x88,
操作预留1=0x89,
操作预留2=0x8a,
//数字
OP_1ADD=0x8b,
OP_1SUB=0x8c,
OP_2MUL=0x8d,
OP_2DIV=0x8e,
运算取反=0x8f,
0×90**值,
操作不=0x91,
OP_0NOTEQUAL=0x92,
运算加法=0x93,
运算值=0x94,
运算乘法=0x95,
运算除法=0x96,
操作模式=0x97,
工作位移=0x98,
操作位移=0x99,
操作布尔兰=0x9a,
操作布尔=0x9b,
操作数=0x9c,
操作数=0x9d,
操作数=0x9e,
操作数=0x9f,
操作大于等于0xa0,
操作最小值=0xa1,
运算量=0xa2,
操作最小值=0xa3,
操作**值=0xa4,
操作范围=0xa5,
//密码
操作参数160=0xa6,
OP_usha1=0xa7,
OP_usha256=0xa8,
操作哈希160=0xa9,
操作哈希256=0xaa,
操作码分隔符=0xab,
操作检查信号=0xac,
OP_uchecksigverify=0xad,
操作检查multisig=0xae,
操作检查multisigverify=0xaf,
//膨胀
操作NOP1=0xb0,
操作检查锁定时间验证=0xb1,
OP_onop2=OP_uCheckLockTimeVerify,
操作检查序列验证=0xb2,
OP_onop3=操作检查序列验证,
操作NOP4=0xb3,
OP_onop5=0xb4,
操作NOP6=0xb5,
OP_onop7=0xb6,
OP_onop8=0xb7,
OP_onop9=0xb8,
OP_onop10=0xb9,
//模板匹配参数
OP_uallinteger=0xfa,
操作键=0xfb,
oppubkeyhash=0xfd,
oppubkey=0xfe,
操作无效代码=0xff,
}
这些运算符分为几种类,如Op_N(N=0,1,…,16)用于推值,if,else等流控制运算符,加减乘除等数学运算运算符,以及用于操作堆栈的运算符。
4.3.2.2标准比特币交易
比特币目前支持的标准交易是用脚本语言编写的可执行脚本的例子,可以理解为在比特币上用脚本语言编写的智能合约。在本节中,我们将简要介绍这些标准事务。
(1) P2P KH事务
最常见的比特币标准交易是pay to public key hash的缩写。您可以从名称猜出事务是基于公钥的。这种事务将指定新utxo的接收方的公钥。使用utxo的人必须提供他的公钥和事务签名。只有满足以下两个条件,才能消耗Utxo:
(C1)使用者提供的公钥哈希值必须与utxo上指定的公钥哈希值相同;
(C2)消费者的签名必须正确(事务生成摘要D,然后使用给定公钥从签名中获得的事务摘要D’必须与D相同)
如果你能满足以上两个条件,你就可以证明某人拥有utxo。满足以上两个条件的脚本如下:
签名|公钥
上述脚本由事务的解锁脚本和锁定脚本组合而成,最终由脚本引擎进行解释和执行。
(2) P2P交易
pay to public key的缩写。这个交易更简单。与P2P-KH不同的是,它不需要验证utxo上指定的公钥和用户提供的公钥之间的哈希,而只检查签名。对应脚本如下:
签名|公钥| OP|CHECKSIG
(3) 多重交易
多重签名交易,这种交易是在对交易使用要求比较严格的情况下进行的。简而言之,它需要支付一笔钱,并且需要得到n个人中至少M个人的签名批准(Mlt;=n)。这种事务将在utxo上指定n个公钥(n个主管的公钥)。当需要提取资金时,必须至少提供m个签名,并且只有所有签名才能满足要求。例如,当三个人管理一个基金时,提取时必须得到其中两个人的签名。符合条件的脚本如下:
0|Sig1|Sig2|
注意multisig脚本必须以0开头,它是从checkmultisig操作符执行过程中的一个bug派生的。
(4) p2sh交易
指出了多sig多重签名交易中存在的一些问题。假设a必须向B支付一笔钱,而B要求基金需要multisig,也就是说,要花这笔钱,需要B和它的几个合伙人同时签字。这样,当a的钱包生成一个向B转账的交易时,交易的锁脚本会包含大量长度较长的公钥,这会导致交易所占用的交易量相当大,从而导致a支付大量的交易费用。P2sh交易可以解决这个问题。如果事务的锁脚本非常复杂,可以考虑使用p2sh。
仍然以2-3多重签名为例,如果锁脚本是s,则:
S=2 |公钥1 |公钥2 |公钥3 | 3 | OP|CHECKMULTISIG
因为公钥的长度很长,所以上面的锁脚本的最终体积将非常大。如果您使用p2sh事务,则只需散列上面的长锁脚本就可以生成一个20字节的脚本哈希。这样,锁定脚本将变成以下内容:
ophash160[20字节脚本哈希]|OP|EQUAL
然后,为了花钱,消费者必须提供一个赎回脚本。赎回脚本与S相同,执行脚本验证时,首先对消费者提供的赎回脚本进行哈希处理,然后比较哈希值是否与锁脚本中的哈希值相同
兑换|OP|HASH16020字节脚本哈希|OP|等于
如果上一步成功,请执行解锁脚本:
Sig1
与multisignature相比,p2sh和multisignature**的区别在于锁脚本较短,而解锁脚本较长,因此额外的交易费用由utxo用户支付而不是由支付方支付。
这些标准事务脚本最终由脚本引擎解释和执行,我们将在下一节中详细描述。
4.3.2.3交易脚本的操作原理
比特币的脚本引擎基于堆栈。脚本解释器将逐个解析脚本运算符,并根据运算符执行堆栈和弹出操作。以P2P-KH交易为例,比特币将utxo的锁脚本和解锁脚本合并在一起,然后在栈上进行操作。我们将用图表展示整个过程
(1) 签名堆栈
(2) 公钥堆栈
(3) **堆栈的顶层元素
(4) 栈顶元素弹出栈,hash后的值放入栈中
(5) 公钥哈希堆栈
(6)OP_uequiverify弹出堆栈中的前两个操作数并比较它们。如果它们是相同的,它们将继续。如果它们不相等,就会发生错误。
(7) 将弹出堆栈中的两个操作数,并执行签名验证。如果栈顶为真,否则栈顶为假
**,如果stack top元素为true,则脚本运行的结果为true,脚本验证通过。
上面描述的事务脚本位于翻译.cppevalscript函数的原如下:
bool EvalScript(std::vectorlt;std::vectorgt;stack,const CScriptscript,unsigned int flags,const BaseSignatureCheckERChecker,SigVersion SigVersion,ScriptError*serror)
此函数的参数如下:
stack:输出参数,上图栈;
脚本:要解释的脚本;
标志:检查相关标识;
checker:用于验证交易签名;
serror:存储错误信息;
执行时,evalscript从头开始解析输入脚本,并根据不同的运算符更改堆栈。整个过程如上图所示。读者可以阅读源代码中的代码,这里没有列出。
producesignature函数为事务输入生成签名后,**一步是验证脚本。如果脚本验证通过,则表明用户有权使用事务输入所引用的utxo。然后可以将事务添加到内存池并广播到网络
如果(!验证脚本(txin.scriptSig,prevPubKey,amp;txin.见证人,标准脚本验证标志,事务签名checker(txConst,i,amount),serror){
if(serror==脚本错误无效堆栈操作){
//无法对输入进行签名,验证失败(可能尝试部分签名)。
TxInErrorToON(txin,vErrors,quot;无法对输入进行签名,堆栈大小无效(可能缺少键)quot;;
}其他{
TxInErrorToON(txin,vErrors,ScriptErrorString(serror));
}
}
可以看到,调用verifyscript来验证脚本,传入了事务输入的解锁脚本和输入引用的utxo的锁脚本。此外,还有一个transactionsignaturechecker对象来验证事务签名。下面是verifyscript的源代码,供读者参考
bool VerifyScript(const CScriptscriptSig,const CScriptscriptPubKey,const CScriptWitness*见证,unsigned int flags,const BaseSignatureCheckERChecker,ScriptError*serror)
{
静态常数;脆性;空度;
if(见证==nullptr){
见证=空度;
}
bool hadWitness=错误;
设置错误(serror、脚本错误、未知错误);
if((标志和脚本验证SIGPUSHON)!=0amp!脚本sig.IsPushOn()) {
return set_uError(serror,SCRIPT_ERR_uSig_uPushOn);
}
//堆栈
std::vectorlt;std::vectorgt;堆栈,堆栈**;
//解析并执行解锁脚本。如果解析过程中出现错误,它将返回。以P2P-KH事务为例。执行后,公钥和签名将保存在堆栈中
如果(!EvalScript(堆栈,scriptSig,flags,checker,SigVersion::BASE,serror))
//已设置serror
返回false;
if(标记和脚本验证)
stackCopy=堆栈;
//继续分析执行锁定脚本
如果(!EvalScript(堆栈,scriptPubKey,flags,checker,SigVersion::BASE,serror))
//已设置serror
返回false;
//如果最终堆栈为空,则存在错误
如果(堆栈.空())
return set_uError(serror,SCRIPT_ERR_Ual_Ufalse);
//从堆栈中取出脚本运行的结果
如果(卡斯托布尔(堆栈.后退())==假)
return set_uError(serror,SCRIPT_ERR_Ual_Ufalse);
//其他交易类的处理
//裸证人程序
int见证版本;
std::矢量见证程序;
if(标记和脚本验证见证){
如果(scriptPubKey.iswitness程序(见证版本,见证程序){
hadWitness=真;
如果(脚本sig.size() !=0){
//scriptSig必须是uCScript(),否则我们将重新引入延展性。
返回set_uerror(serror,脚本_ERR_uwitness_umalelated);
}
如果(!验证见证程序(*见证,见证版本,见证程序,标志,检查程序,错误){
返回false;
}
//在末尾绕过cleanstack检查。实际的堆栈显然不干净
//对于证人程序。
堆栈.调整大小(1) ;
}
}
//用于编写哈希事务脚本的其他验证:
if((flagsSCRIPT_uverify_2sh)amp;scriptPubKey.IsPayToScriptHash())
{
//scriptSig只能是文本,否则验证失败
如果(!脚本sig.IsPushOn())
return set_uError(serror,SCRIPT_ERR_uSig_uPushOn);
//还原堆栈。
交换(堆栈、堆栈**);
//堆栈不能为空,因为如果
//P2SH HASHlt;EQUALscriptPubKey将使用
//空堆栈和上面的EvalScript将返回false。
断言(!堆栈.空());
常量值类和pubKeySerialized=堆栈.后退();
CScript pubKey2(pubKeySerialized.begin(), pubKeySerialized.end());
popstack(栈);
如果(!EvalScript(堆栈,pubKey2,flags,checker,SigVersion::BASE,serror))
//已设置serror
返回false;
如果(堆栈.空())
return set_uError(serror,SCRIPT_ERR_Ual_Ufalse);
如果(!卡斯托布尔(堆栈.后退()))
return set_uError(serror,SCRIPT_ERR_Ual_Ufalse);
//P2SH见证程序
if(标记和脚本验证见证){
if(pubKey2.IsWitnessProgram(witnessversion,witnessprogram)){
hadWitness=真;
如果(scriptSig!=CScript()lt;lt;std::vector(pubKey2.begin(),pubKey2.end()){
//scriptSig必须是一次push-reproveScript。否则我们
//重新引入延展性。
返回set_uerror(serror,脚本_ERR_uwwitness_u2sh);
}
如果(!验证见证程序(*见证,见证版本,见证程序,标志,检查程序,错误){
返回false;
}
//在末尾绕过cleanstack检查。实际的堆栈显然不干净
//对于证人程序。
堆栈.调整大小(1) ;
}
}
}
//仅在潜在的P2SH评估之后才执行CLEANSTACK检查,
//因为P2SH脚本的非P2SH求值显然不会导致
//一个干净的堆栈(保留P2SH输入)。证人评估也是如此。
if((标志和脚本验证CLEANSTACK)!=0){
//不允许没有P2SH的CLEANSTACK,否则开关CLEANSTACK-gt;P2SH+CLEANSTACK
//这是可能的,这不是一个softfork(P2SH应该是一个)。
断言((标志和脚本验证)!=0);
断言((标记和脚本验证见证)!=0);
如果(堆栈大小() !=1){
返回set_uerror(serror,SCRIPT_ERR_uCleanStack);
}
}
if(标记和脚本验证见证){
//如果P2SH关闭,则无法检查是否有正确的意外见证数据,因此需要
//该证人暗示P2SH。否则,从WITNESS-gt;P2SH+WITNESS将
//有可能,这不是一个软叉。
断言((标志和脚本验证)!=0);
如果(!见证人amp!见证->IsNull()){
返回set_uerror(serror,SCRIPT_ERR_Uunexpected);
}
}
返回集合成功(serror);
}
可以看到,源代码主要调用上面提到的evalscript函数来解析脚本和更新堆栈,这是比较容易理解的。
5交易广播与接待
在上一节的处理之后,事务脚本和脚本验证被传递,事务被正式创建成功。接下来,需要将事务添加到节点的内存事务池中进行挖矿。同时,该节点还将事务广播到网络,其他节点验证该事务,然后将其加入到自己的事务池中。
5.1广播事务
创建的事务可以通过onapi sendrawtransaction命令广播到网络。让我们直接看一下这个命令的实现
静态单值sendrawtransaction(const ONRPCRequestrequest)
{
//参数检查
如果(请求帮助世界环境学会请求参数大小第1241页请求参数大小()gt;2)
抛出std::运行时错误(
quot;sendrawtransaction/quot;hexstring/quot;(allowighfees)/nquot;
quot;/n向本地节点和网络提交原始事务(序列化、十六进制编码)。/nquot;
quot;/nAlso请参阅createrawtransaction和signrawtransaction调用。/nquot;
quot;/nArguments:/nquot;
quot;1。/quot;hexstring/quot;(string,必选)原始事务的十六进制字符串)/nquot;
quot;2。allowwhighfees(布尔值,可选,默认值=false)允许高费用/nquot;
quot;/n结果:/nquot;
quot;/quot;hex/quot;;(string)十六进制/nquot;中的事务哈希
quot;/示例:/nquot;
quot;/n创建事务/nquot;
+HelpExampleCli(quot;CreateRawTransaction6035,quot;/quot;[{///quot;txid///quot;:///quot;,///quot;vout///quot;:0}]/quot;/quot;{///quot;myaddress///quot;:0.01}/quot;quot;)+
quot;签署事务,并取回hex/nquot;
+帮助示例CLI(quot;SignRawTransaction6035,quot;/quot;myhex/quot;quot;)+
quot;/n发送事务(带符号的十六进制)/nquot;
+帮助示例CLI(quot;SendRawTransaction6035,quot;/quot;signedhex/quot;quot;)+
quot;/nAs a on-rpc调用/nquot;
+帮助示例RPC(quot;SendRawTransaction6035,quot;/quot;signedhex/quot;quot;)
);
承诺承诺;
RPCTypeCheck(请求.params, );
//从参数解析十六进制字符串
//破译交易
CMutableTransaction mtx;
如果(!解码HEXTX(mtx,请求.params[0].获取str())
抛出ONRPCError(RPC反序列化错误,quot;TX解码失败AD6035);
CTransactionRef tx(MakeTransactionRef(std::move(mtx)));
const uint256amp;hashTx=tx-gt;GetHash();
CAmount nMaxRawTxFee=maxTxFee;
如果(!请求.params[1] .isNull()amp;请求.params[1] .getbool())
nMaxRawTxFee=0;
{//cs主作用域
锁(cs主);
cconsViewCacheview=*pcoinsTip;
//确定事务是否已在内存池中,以及该事务是否有已花费的输出
bool fHaveChain=假;
对于(尺寸0=0!fHaveChainolt;tx-gt;体积大小();o++){
const Coin和existingCoin=查看.AccessCoin(COutPoint(hashTx,o));
fHaveChain=!现有硬币.IsSpent();
}
布尔fHaveMempool=内存池存在(hashTx);
//如果该事务不存在于内存池中,并且该事务的所有输出都没有花费,则该事务将添加到内存事务池中
如果(!fHaveMempoolamp!fHaveChain公司){
//推到本地节点并与钱包同步
校准状态状态;
布尔函数输入;
//尝试将交易添加到交易池中
如果(!AcceptToMemoryPool(内存池,状态,标准::移动(tx),fMissingInputs,
nullptr/*plTxnReplaced*/,false/*旁路限制*/,nMaxRawTxFee){
如果(state.IsInvalid()) {
throw ONRPCError(RPC_u事务被拒绝,FormatStateMessage(state));
}其他{
如果(fMissingInputs){
抛出ONRPCError(RPC_utransaction_uerror,quot;缺少输入squot;);
}
抛出ONRPCError(RPC_utransaction_错误,FormatStateMessage(state));
}
}其他{
//如果启用了电子钱包,请确保钱包已被识别
//返回前的新事务。这会阻止比赛
//其中用户可以使用事务调用sendrawtransaction
//到/从他们的钱包,立即呼叫一些钱包RPC,并得到
//一个过时的结果,因为尚未处理回调。
调用函数无效接口队列([amp;promise]{
承诺.set值();
};
}
}else if(fHaveChain){
抛出ONRPCError(RPC_utransaction_ualready_uin_uchain,quot; TRANSACTION ALREADY IN block chainquot;);
}其他{
//如果要发送邮件,请确保不会永远阻止
//已在mempool中的事务。
承诺.set值();
}
}//cs主
答应你。得到未来().wait();
如果(!葛康曼)
抛出ONRPCError(RPC_uclient_2;P2P_1禁用,quot;错误:对等功能缺失或禁用dquot;);
//发送inv消息以将事务广播到网络
CInv inv(消息发送、哈希发送);
G_uconnman-gt;ForEachNode([amp;inv](CNode*pnode)
nbsp{
pnode->推式库存(inv);
});
返回hashTx.GetHex();
}
只有在交易池中不存在交易且交易的每一个输出都没有花费时,才能将交易添加到交易池中。
**,将生成一条inv消息以加入集合并等待广播到网络。
5.2接收交易
让我们看看网络中的节点是如何处理新事务的。在procesessage中处理inv消息:
else if(strCommand==NetMsgType::INV)
{
//读取数据
标准::vectorvInv;
vRecvgt;>vInv;
如果(葡萄酒尺寸()gt;**投资额
{
锁(cs主);
行为不端(pfrom-gt;GetId(),20,strprintf(quot;消息inv size()=%uquot;,葡萄酒尺寸()));
返回false;
}
bool fBlocksOn=!氟利昂;
//如果whitelistrelay为true,则允许白名单对等方以仅块模式发送块以外的数据
如果(pfrom->fWhitelistedamp;加格斯。盖特布尔格(quot;-whitelistrelayquot;,默认值为“白名单中继”)
fBlocksOn=错误;
锁(cs主);
uint32_utnfetchflags=GetFetchFlags(pfrom);
//处理收到的每个inv消息
针对(CInvinv:vInv)
{
if(中断MsgProc)
返回true;
//判断交易是否已经存在于区块链上
bool fAlreadyHave=AlreadyHave(发票);
日志打印(BCLog::NET,quot;got inv:%s%s对等方=%d/nquot;,inv.ToString公司(),虚报?quot;havequot;:quot;newquot;,pfrom-gt;GetId());
如果(发票类==消息发送){
发票类|=nFetchFlags;
}
//如果是一个街区
如果(发票类==消息块){
UpdateBlockAvailability(pfrom-gt;GetId(),发票哈希);
如果(!fAlreadyHaveamp!F导入amp!fReindexamp!mapBlocksInFlight.count( 发票哈希)) {
//我们曾经在这里请求完整的块,但是由于标题通知现在是
//主要方法是在网络上发布消息,并且自,在一个节点的情况下
//回到投资部,我们可能有一个重组,我们应该首先得到标题,
//我们现在只在这里提供gETHeaders响应。当我们收到邮件头时,我们会
//那就要我们需要的积木。
connman->推送消息(pfrom,先生。制造(NetMsgType::GETHEADERS,chainActive.GetLocator(pindexBestHeader),发票哈希);
LogPrint(BCLog::NET,quot;ETHeaders(%d)%s到对等端=%d/nquot;,pindexBestHeader-gt;nHeight,inv.hash.ToString(),pfrom-gt;GetId());
}
}
//如果你收到一笔交易
其他的
{
pfrom-gt;AddInventoryKnown(inv);
如果(仅限fBlocksOn){
LogPrint(BCLog::NET,quot;事务(%s)inv发送违反协议peer=%d/nquot;,inv.hash.ToString(),pfrom-gt;GetId());
}否则如果(!fAlreadyHaveamp!F导入amp!fReindexamp!IsInitialBlockDownload()){
//如果事务不存在,则将其添加到请求中心化
pfrom-gt;AskFor(inv);
}
}
//跟踪我们的物品请求
GetMainSignals().Inventory(发票哈希);
}
}
如果节点没有接收到的事务哈希,它将调用askfor来请求事务数据,askfor将把请求添加到队列(mapaskfor)。之后,将遍历队列生成GetData消息,并成批提取本地缺失的事务(或块)数据(参见SendMessage函数)
//遍历队列
同时(!pto-gt;mapaskforempty()amp;(*pto-gt;mapAskFor.begin()).firstlt;=nNow)
{
const CInvinv=(*pto-gt;mapAskFor.begin())第二;
如果(!AlreadyHave(投资部))
{
//如果事务(或块)数据不存在,则将其插入vgetdata中心化。当集合中的数据达到1000+时,发送GetData消息批量获取数据
LogPrint(BCLog::NET,quot;请求%s对等方=%d/nquot;,inv.ToString公司(),pto-gt;GetId());
vGetData.push背面(inv);
如果(vGetData.size()gt;=1000)
{
康曼->按下信息(pto,先生。制造(NetMsgType::GETDATA,vGetData));
vGetData.clear();
}
}其他{
//如果我们不打算问,就别指望会有人回答。
//已经存在。从集合中删除
pto-gt;setaskforerase( 发票哈希);
}
//从队列中删除
pto-gt;mapAskFor.erase(pto-gt;mapAskFor.begin());
}
//如果集合不是空的
如果(!vGetData.empty())
康曼->按下信息(pto,先生。制造(NetMsgType::GETDATA,vGetData));
长话短说,当队列中收集到1000个或更多数据时,将发送GetData消息以成批提取数据。
让我们看看节点在接收到GetData消息后是如何处理这些消息的
else if(strCommand==NetMsgType::GETDATA)
{
//从流中读取数据
标准::vectorvInv;
vRecvgt;>vInv;
如果(葡萄酒尺寸()gt;**投资额
{
锁(cs主);
行为不端(pfrom-gt;GetId(),20,strprintf(quot;message getdata size()=%uquot;,葡萄酒尺寸()));
返回false;
}
LogPrint(BCLog::NET,quot;接收到getdata(%u invsz)对等端=%d/nquot;,葡萄酒尺寸(),pfrom-gt;GetId());
如果(葡萄酒尺寸()gt;0){
LogPrint(BCLog::NET,quot;接收到的getdata为:%s peer=%d/nquot;,vInv[0].ToString(),pfrom-gt;GetId());
}
//将所有GetData请求添加到集合中
pfrom-gt;vRecvGetData.insert(pfrom-gt;vRecvGetData.end(), 葡萄酒开始(), 葡萄酒结束());
//处理请求
处理数据(pfrom,链参数获取一致性(),康曼,中断msgproc);
}
很简单。它主要调用processgetdata来处理它,然后继续查找
无效静态处理数据(CNode*pfrom,const consummations::Paramsconsunsesparams,CConnman*connman,const std::atomicinterruptMsgProc)
{
资产锁定无场(cs_umain);
std::deque::iterator it=pfrom-gt;vRecvGetData.begin();
std::向量未找到;
const CNetMsgMaker msgMaker(pfrom-gt;GetSendVersion());
{
锁(cs主);
//遍历集
同时(它!=pfrom-gt;vRecvGetData.end()amp;(it-gt;type==MSG_utx | | it-gt;type==MSG_uuTx){
if(中断MsgProc)
返回;
//如果发送缓冲区已满而无法响应,则不必担心
如果(pfrom-gt;fPauseSend)
休息;
const CInvinv=*it;
it++;
//从中继存储器发送流
//检查maprelay或内存事务池中是否存在事务。如果发送了TX消息,则事务数据被发送到请求者
bool push=假;
自动mi=地图中继.find( 发票哈希);
int nSendFlags=(发票类==消息发送?序列化交易无见证:0);
如果(米!= 地图中继.end()) {
connman->推送消息(pfrom,先生。制造(nSendFlags,NetMsgType::TX,*mi-gt;秒));
推=真;
}else if(pfrom->timeLastMempoolReq){
自动txinfo=内存池.info( 发票哈希);
//为了保护隐私,请不要在以下情况下使用mempool回答getdata
//TX不可能在响应MEMPOOL请求时被激活。
如果(txinfo.tx公司ampamp;时间lt;=pfrom-=quot;quot;gt;timeLastMempoolReq){
connman->推送消息(pfrom,先生。制造(nSendFlags,NetMsgType::TX,*txinfo.tx公司);
推=真;
}
}
如果(!推动){
vNotFound.push背面(inv);
}
//跟踪对我们物品的需求。
GetMainSignals().Inventory(发票哈希);
}
}//释放cain
如果(它!=pfrom-gt;vRecvGetData.end()amp!pfrom-gt;fPauseSend){
const CInvinv=*it;
如果(发票类==消息块| |发票类==消息过滤块发票类==消息块发票类==消息见证块){
it++;
处理GetBlockData(pfrom、consunsparams、inv、connman、interruptMsgProc);
}
}
//处理后从集合中删除
pfrom-gt;vRecvGetData.erase(pfrom-gt;开始.vGetVrData(),它);
如果(!vNotFound.empty()) {
//让同行知道我们没有找到它要求的东西,所以它没有
//必须永远等着。目前只有SPV客户真正关心
//关于这个消息:当他们递归地遍历
//相关未确认交易的相关性。SPV客户希望
//这样做是因为他们想知道
//风险分析)与之相关的交易的依赖性,没有
//必须下载整个内存池。
connman->推送消息(pfrom,先生。制造(NetMsgType::NOTFOUND,vNotFound));
}
}
节点将从relaymap和内存中的事务池中查找请求的事务。如果找到,它将向对等方发送一条发送消息,并将事务数据发送给对等方。
**,对等方接收TX消息,获取事务,然后检查事务。核实后,交易被添加到自己的交易池中。TX报文的处理比较复杂,涉及到独立事务的处理(有两种情况:一种是事务所依赖的事务没有被接收到,此时接收到的事务成为一个独立的事务;另一种情况是,独立事务池依赖于事务当接收到事务时,需要将隔离事务从鼓励事务池中删除并添加到内存事务池中)。我们只截取一部分代码,读者可以阅读源代码。净处理.cpp的procesessage方法。
else if(strCommand==NetMsgType::TX)
{
//如果出现以下情况,请提前停止处理事务:
//我们处于仅块模式,对等端不是白名单就是白名单中继关闭
如果(!fRelayTxesamp;(!pfrom-gt;fWhitelisted | |!加格斯。盖特布尔格(quot;-whitelistrelayquot;,默认为“白名单中继”))
{
LogPrint(BCLog::NET,quot;违反协议发送事务peer=%d/nquot;,pfrom-gt;GetId());
返回true;
}
std::dequevWorkQueue;
std::矢量平均序列;
//从流中读取事务
CTransactionRef ptx公司;
vRecvgt;>ptx;
const CTransactiontx=*ptx;
CInv inv(信息发送,tx.GetHash());
pfrom-gt;AddInventoryKnown(inv);
锁2(cs_umain,g_xcs_u孤儿);
bool fMissingInputs=假;
校准状态状态;
//事务已从相关集合中获取并删除
pfrom-gt;setaskforerase( 发票哈希);
mapAlreadyAskedFor.erase( 发票哈希);
标准::列表删除txn;
//如果事务不存在,则调用accepttomemorypool将事务添加到内存中的事务池中
如果(!AlreadyHave(投资)amp;
AcceptToMemoryPool(mempool、state、ptx和fMissingInputs和lRemovedTxn,false/*绕过限制*/,0/*nAbsurdFee*/){
内存池.check( pcoinsTip.get.获取());
中继传输(tx,康曼);
**,还调用accept to memory pool来将事务添加到事务池中。accepttomemorypool函数的处理逻辑非常复杂。它可以检查交易,包括交易的格式,交易是否存在双花问题,以及交易签名的验证。这个功能很重要,我们建议读者可以仔细阅读这个函数的源代码,加深对交易的理解。accepttomemorypool函数调用checkinputs检查事务的每个输入,包括事务签名的验证
* *
*检查此事务的所有输入是否有效(无重复支出、脚本和签名、金额)
*这不会修改UTXO集合。
nbsp*
*如果pvChecks不是nullptr,则将脚本检查推送到它上,而不是内联执行。任何
*不需要的脚本检查(例如由于脚本执行缓存命中)显然是,
*未推到pvChecks/run上。
nbsp*
*将cacheSigStore/cacheFullScriptStore设置为false将从相应的缓存中删除元素
*匹配的。这对于检查可能永远不需要缓存的块非常有用
*再次输入。
nbsp*
*src/test/txvalidationcache测试.cpp
nbsp*/
bool CheckInputs(const CTransactiontx,CValidationStatestate,const CCoinsViewCacheinputs,bool fscript checks,unsigned int flags,bool cacheSigStore,bool cacheFullScriptStore,PrecomputedTransactionDatatxdata,std::vector*pvChecks)
{
如果(!tx.IsCoinBase公司())
{
如果(pvChecks)
pvChecks->保留(tx.vin.尺寸());
//上面的第一个循环执行所有便宜的检查。
//只有当所有输入都通过时,我们才会执行昂贵的ECDSA签名检查。
//有助于防止CPU耗尽攻击。
//连接下的块时跳过脚本验证
//assumevalid块。假设assumevalid块有效,则
//是安全的,因为块merkle哈希仍然是计算和检查的,
//当然,如果假定的有效块由于错误的scriptSigs而无效
//此优化将允许接受无效链。
if(fscript检查){
//首先检查脚本执行是否已使用相同的缓存
//旗帜。注意,这假设提供的输入是
//正确(即在tx#39;s中的事务哈希
//在的inputs视图中正确地提交到scriptPubKey
//交易)。
uint256哈希缓存项;
//我们只使用前19个nonce字节来避免第二个SHA
//取整-给我们19+32+4=55字节(+8+1=64)
静态断言(55-sizeof(flags)-32gt;=128/8,quot;需要至少128位的nonce用于脚本执行缓存6035);
CSHA256().Write(纸条tExecutionCacheNonce.begin(),55-sizeof(标志)-32。写入(tx.getWitness哈希().begin(),32).Write((unsigned char*)flags,sizeof(flags))。完成(hashCacheEntry.begin());
assertLockHold(cs_umain);//TODO:通过使buckoocache不需要外部锁来删除此要求
如果(scriptExecutionCache.containshashCacheEntry,(哈希缓存!cacheFullScriptStore){
返回true;
}
//检查事务的每个输入
for(unsigned int i=0;ilt;tx.vin.尺寸();i++){
//找到输入指向的utxo并确保它没有被使用
常数COutPointprevout=发送车辆识别号[i] .prevout;前一页;
const Coin和Coin=输入.AccessCoin(预告);
断言(!硬币());
//我们非常小心地只把东西交给CScriptCheck
//很明显是由tx和证人hash承诺的。这提供了
//一个健全的检查,我们的缓存没有引入共识
//通过额外的数据失败,例如,硬币
//作为CScriptCheck的一部分被检查过。
//验证签名
//验证交易签名
CScriptCheck检查(投币、tx、i、标志、cacheSigStore和txdata);
如果(pvChecks){
pvChecks—gt;回推(CScriptCheck());
支票交换(pvChecks-gt;back());
}否则如果(!检查()){
if(标志和标准标志非强制性标志验证标志){
//检查故障是否由
//非强制性脚本验证检查,例如
//非标准DER编码或非空伪码
//参数;如果是,则不要触发DoS保护
//避免在升级和
//未升级的节点。
CScriptCheck检查2(投币德克萨斯州,我,
flagsamp;~STANDARD_unot_u强制u VERIFY_uflags、cacheSigStore和txdata);
如果(check2())
返回状态。无效(false,拒绝非标准,strprintf(quot;非强制脚本验证标志(%s)quot;,ScriptErrorString(check.GetScriptError())));
}
//其他标志失败表示事务
//在新块中无效,例如P2SH无效。我们禁止
//这些节点不遵循协议。那个
//说升级的时候要慎重考虑
//至于正确的行为-我们可能想继续
//即使在软分叉之后也可以与未升级的节点进行对等
//出现了超多数信号。
返回状态.DoS(100,false,拒绝无效,strprintf(quot;强制脚本验证标志失败(%s)quot;,ScriptErrorString(check.GetScriptError())));
}
}
如果(cacheFullScriptStoreamp!私人支票){
//我们执行了所有提供的脚本,并被告知
//缓存结果。现在就这么做。
scriptExecutionCache.insert(hashCacheEntry);
}
}
}
返回true;
}
在代码中,事务的签名由函数cscriptcheck验证。以事务、事务输入的索引和事务输入指向的utxo作为参数构造cscriptcheck对象,并执行验证过程
bool CScriptCheck::operator()(){
const CScriptscriptSig=ptxTo-gt;vin[nIn].scriptSig;
const CScriptWitness*witness=ptxTo-gt;vin[nIn].scriptWitness;
返回VerifyScript(scriptSig,m_xout.scriptPubKey见证人、nFlags、CACHINGTransactions Signaturechecker(ptxTo,nIn,m_xout.n值,cacheStore,*txdata),amp;error);
}
**,调用第4节中描述的verifyscript函数来执行脚本的验证。
5.3时间安排
代码被一个接一个地粘贴,很少有读者能忍受阅读。这里我们用一个简单的图表来说明整个过程
在读取源代码的过程中,只要遵循图中的主线,就可以将事务从生成、签名、广播、接收到加入事务池的事务的整个过程进行梳理。
6小结
交易是比特币最重要的部分。本文从源代码的角度分析了比特币交易的产生、交易的签名、交易脚本的操作、交易的广播和接收。读者可以按照本文的主线来阅读代码,一旦你理解了这部分代码,就可以对比特币交易有更深入的了解。
文章标题:比特币源代码分析
文章链接:https://www.btchangqing.cn/87323.html
更新时间:2020年08月18日
本站大部分内容均收集于网络,若内容若侵犯到您的权益,请联系我们,我们将第一时间处理。