花了一周多的时间看比特币0.1.0的源码,到现在基本是都看完了,虽然不能保证所有地方都统统了解,但至少能够理论界和实际。从书本上看得理论,基本都能在源码中的到印证。
这篇文章是只针对比特币交易部分的解析。因为结合着源码讲述,读者观看请自备源码。源码内容也不是特别详细,基本只列出关键函数和关键步骤。后面马上要做别的项目了,比特币的其他部分解析不知道还有没有机会补上。文档是在word中写好的,直接粘过来。
公钥加密,私钥解密;私钥签名,公钥验签。
上面是比特币加密与认证的本质,然而本文不讲述公私钥原理,只讲述比特币原码具体是如何使用这种机制实现加密与认证的。欲了解请先查看相关资料。
首先比特币用户的公私钥信息存储在globle 变量keyUser中:
CKey keyUser; // 当前用户公私钥对信息
CKey类有以下几个函数:
MakeNewKey();//生成一对公私钥
GetPubKey();//返回公钥
GetPrivKey()//返回私钥
公钥和私钥都是256位的数字串。私钥是用某种安全的方式随机获得的,本质就是一个256位的随机数。公钥是将私钥通过一个椭圆曲线乘法(K = k * G ,其中k是私钥,G是被称为生成点的常数点,而K是所得公钥))的算法计算得来,这些都会在MakeNewKey()这个函数中实现,我们只需要知道,这种运算不可逆。
即由私钥可以推出公钥,反之不然。
一个私钥可以生成多个公钥。
比特币程序运行后,程序会先从wallet.dat数据文件中加载钱包数据信息,其中就包括密钥信息,将密钥信息放入两个map表中:
map, CPrivKey> mapKeys;
// 公钥和私钥对应的映射关系,其中key为公钥,value为私钥
map > mapPubKeys;
// 公钥的hash值和公钥的关系,其中key为公钥的hash值,value为公钥
比特币的钱包地址由公钥进行hash160加密生成,同样可以有多个,公钥得到钱包地址的过程如下
Inline string PubKeyToAddress
(constvector& vchPubKey)
{
return Hash160ToAddress(Hash160(vchPubKey));
}
Hash160加密“先SHA256加密后RIPEMD160加密”的简称,也就是进行可两次加密。之后得到一个160位的数字。
然而这还没完,为了更加简洁方便的表示长串数字,比特币又调用Hash160ToAddress对该数字进行base58编码,从而获得了我们常常看到的比特币地址,例如:
1JyShDpyqafQ88EaLvUQdhajKCzYG4zxd9
总结比特币地址的生成过程如下图所示
比特币的交易方式与传统的交易方式相比有着非常明显的不同。传统的交易方式是数据库式的。比如你从网上买了一件衣服,则从你的支付宝账户中减去这个衣服的金额。支付宝账户代表数据库。而比特币交易方式则不同。比特币的交易是记账式的。比特币值记录每笔交易历史,而不专门存储余额数据。
所以比特币的本质就是一个分布式账本,每个人的账本都相同,都记录着所有的交易历史。
然而只有交易的记录,比特币是如何将确定金钱的归属的呢?
下面就要讲述比特币交易账本的实现方式。如下如图
比特币的每笔交易包含输入和输出两部分,输入和输出都可以使多个。每笔交易的输入都来自其他交易的输出。输入表明了这笔交易金额的具体来源,而输出表明的了金额的去向。
特别的,创币交易,也叫币基交易,就是矿工打包块所得奖励的交易的输入为空,因为这笔交易的金额是“凭空产生”的。
除去币基交易,每笔交易的输入金额之和都应等于输出金额之和。
那么,比特币究竟是怎么保证属于我们的比特币只有我们可以使用呢?
一笔交易刚刚生成时,我们可以把它的输出比作存钱的保险箱。这些保险箱被主人的锁给锁住了。这个锁只有交易的输出目标能够解锁,也就代表只有拥有解开锁的钥匙的人可以打开这个箱子,相当于这个人拥有这笔财产
如果一笔交易的某个输出没有作为另一个交易的输入,也就是没有被打开过,则这笔交易就是UTXO(未花费交易输出)。
一个比特币用户的钱包中会存放若干个钥匙,这把钥匙能打开的锁的交易输出都属于该用户。
以上就是交易的基本原理,下面结合代码具体看比特币如何实现上述过程的。
比特的交易信息存储在CTransaction类中,其内容是:
int nVersion; // 交易的版本号,用于升级
vector vin; // 交易对应的输入
vector vout; // 交易对应的输出
int nLockTime; // 交易对应的锁定时间
重要的是vin和vout两个成员,分别记录了这笔交易对应的输入与输出信息。一笔交易的输入和输出都可能是多个,所以用vector定义。
CTxIn的内容:
COutPoint prevout; // 该输入的来源
CScript scriptSig; // 输入脚本对应的签名
unsigned int nSequence;// 主要是用于判断相同输入的交易哪一个更新,值越大越新
其中COutPoint的定义如下:
uint256 hash; // 交易对应的hash
unsigned int n; // 交易对应的第几个输出
也就说交易中包含了输入究竟来哪个交易的哪个输出的具体信息。
CTxOut的内容:
int64 nValue; // 交易输出对应的金额
CScript scriptPubKey; // 交易对应的公钥
上面是比特币钱包软件的界面。当点击第一张图中的Send Coin按钮就会弹出第二张图所示的窗口。填写目标地址和余额后即可发起交易。在源码中,对应发起交易的函数是:
bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew);
这个函数的第一个参数是有比特币钱包地址构成的脚本数据,后续会给出具体描述。第二个参数是这笔转账的金额,第三个变量是个返回值变量。
这个函数的核心就是运行下面这个函数
前三个参数继承自上面的函数,第四个函数也是个返回值变量,代表的是交易费。
CreateTransaction主要内容是搜索可用币,填写输出信息和输入信息。
搜索可用币:
SelectCoins(int64 nTargetValue, set& setCoinsRet)
根据参数一的金额,去寻找能够凑够该金额的相关交易
该函数的内容就会从头检索mapWallet表,这个表中记录着所有输出包括自己的交易。从这交易中挑选出可用的交易输出并且加起来大于目标金额。
填写交易输出:
wtxNew.vout.push_back(CTxOut(nValueOut, scriptPubKey));
这部分很简单,就是把输出的金额和接收方提供的锁作为参数填写输出。不过有时候我们搜集到的输入币值总和可能大于要发送的金额,这就需要找零。也就在输出中加一条目标是自己的输出:
wtxNew.vout.push_back(CTxOut(nValueIn - nValue, scriptPubKey));
填写交易输入:
foreach(CWalletTx* pcoin, setCoins)
for (int nOut = 0; nOut < pcoin->vout.size(); nOut++)
if (pcoin->vout[nOut].IsMine())
wtxNew.vin.push_back(CTxIn(pcoin->GetHash(), nOut));
遍历之前搜集到的交易的集合,将具体的哪个交易的哪个输出填写到vin里面去。
交易签名:
调用
SignSignature(*pcoin, wtxNew, nIn++);//对每笔输入交易进行签名
在这个函数中会调用
Solver(txout.scriptPubKey, hash, nHashType, txin.scriptSig)
//验证自己是否具有打开这个公钥的私钥
如果成功,就会获得这个输入的签名txin.scriptSig。
下面是整体流程:
首先我们要知道一个签名和验签的流程,A有一对密钥,公钥为m,私钥为n,私钥只有A持有,公钥则所有人拥有。A要给B发送密文x,那么A会用私钥m对密文进行签名,并向B发送签名和密文,B收到后用公钥对签名进行处理,若处理后与密文相同则代表签名者为A。
上节讲述了交易发起的整体流程,但是对细节,我们还不是很清楚,尤其是scriptSig和scriptPubKey这对脚本究竟是怎么产生和怎么运作的。
scriptPubKey << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG;
在发送比特币的代码中,scriptPubKey 被这样填充。其中hash160是解码后的比特币钱包地址。而OP_DUP这些代表的是操作。
可以理解为,这个scriptPubKey就是归属者给交易上的锁,scriptSig就是钥匙。用户想要使用一笔交易的某个输出,就需要提供能够解锁scriptPubKey的交易来供所有人验证。
那么scriptSig究竟是怎么得到呢?scriptSig实际上是比特币用户使用自己的私钥对交易进行的签名。
主要调用SignSignature这个函数:
在这个函数中首先执行:
uint256 hash =SignatureHash(scriptPrereq + txout.scriptPubKey, txTo, nIn, nHashType);
这个函数对只有某个输入的交易数据进行hash运算并返回hash值(感觉这里很奇怪)
之后执行:
Solver(txout.scriptPubKey, hash, nHashType, txin.scriptSig)
{
......
scriptSigRet << vchSig << vchPubKey;
// 除了 sig 外 还要把 pubkey 也添加进入scriptsig中 // 这里就是生成答案的地方
......
}
先验证自己是否拥有该对应该公钥的私钥,若有,则用私钥对上面得到的hash进行签名,并将该签名填入到txin成员里。
这就是scriptSig的获得方式。
对于签名的认证,主要是执行VerifySignature这个函数
而该函数主要执行的是
EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) +
txout.scriptPubKey, txTo, nIn, nHashType)
也就是解析脚本。
EvalScript函数的运行方式是这样的:
首先创建一个stack(栈),其实就是个vector。
stack.push_back(vchPushValue);
先将scriptSig部分压入栈中,然后首先执行scriptPubKey部分:
每读取scriptPubKey中的一个操作符,就执行一次。可以把其当作解释形语言的形式,读取一条执行一条,具体流程如下:
上面图片摘自链接https://zhuanlan.zhihu.com/p/27512347,多谢!
公钥验证可以初步验证身份
由上图我们就知道,当执行到OP_CHECKSIG操作符时,代表对签名进行验证。
查看EvalScript中的代码,其中主要是swith case结构,根据操作符对应操作。
EvalScript(...)
{
Switch(opcode)
{
...
Case OP_CHECKSIG:
case OP_CHECKSIGVERIFY:
{
...
bool fSuccess = CheckSig(vchSig, vchPubKey, scriptCode, txTo, nIn, nHashType);
...
}
...
}
}
CheckSig就是认证函数,它的参数分别是对交易hash的签名,私钥对应的公钥与公钥脚本...进入该函数我们看到:
key.Verify(SignatureHash(scriptCode, txTo, nIn, nHashType), vchSig)
这里我们再次看到了SignatureHash,这个函数,这个函数会返回交易对应的hash值,而Verify便会验证,验证过程是,用公钥对签名进行运算得到的hash值是否和这里用SignatureHash运算得到的hash相同,如果相同,则代表签名有效。
我们假定私钥签名为函数S,公钥验签函数为V,交易为tx,hash函数为H有:
V(S(H(tx))) = H(tx)
下图可直观看出整个交易的签名和验证过程
需要指出的是,这里面的tx只是代指,但并不是整个交易,一个交易有很多输入,tx只是有某个输入的被拆分后的交易。