https://www.jianshu.com/p/7748d6e5fdac
我们都知道,比特币钱包其实是私钥的钱包,这个比较好理解。那么钱包中除了私钥还保存了什么? 从钱包中发出一笔交易要经过哪些流程?今天就来讨论这些问题。
注1:本文的分析以中本聪的代码v0.1.15版本为蓝本,节点指代为全节点。
注2:如果对比特币交易的一些基本概念还不清楚,请先阅读MasteringBitcoin这本书的相应章节,此书中文版叫做《精通比特币》。
注3:文中提到的关键步骤会贴出相应源码,非关键部分请参考流程图自行去看源码
比特币钱包的对应文件是wallet.dat, 里面除了保存公钥、私钥还保存和此节点有关的交易以及一些其他信息比如地址簿、默认手续费设定值等。这些信息都是通过Berkley DB数据库以键值对的形式存储的。存储形式大概像下面这样。
注:很多研究过比特币的人都知道UTXO集,我们已经分析过几个存储比特币数据的文件,包括blkindex.dat、blkxxxx.dat(x代表十进制数字)和wallet.dat,这些文件并没有储存UTXO集。那么比特币的UTXO集到底保存在哪里? 实际上在0.8版本才使用了文件专门存储这些数据,这个文件是chainstate.dat
先介绍比较重要的类
CWalletTx是钱包中存储交易的类型,继承自CMerkleTx,CMerkleTx继承自CTransaction, 可以看到钱包交易类型包含了一些钱包使用者才关心的信息。
class CWalletTx : public CMerkleTx
{
public:
vector vtxPrev; //前向交易集合
map mapValue;
vector > vOrderForm;
unsigned int fTimeReceivedIsTxTime;
unsigned int nTimeReceived; // time received by this node
char fFromMe;
char fSpent; //是否已被花费
//// probably need to sign the order info so know it came from payer
// memory only
mutable unsigned int nTimeDisplayed;
/*成员函数略*/
}
class CMerkleTx : public CTransaction
{
public:
uint256 hashBlock; //这个交易所在block的hash值
vector vMerkleBranch; //这个交易在块中的merkle路径
int nIndex; //这个交易在块中序列的索引
// memory only
mutable bool fMerkleVerified;
/*成员函数略*/
}
对于钱包中的交易可以这样理解,钱包只保存发给自己的交易(自己主动发的交易也是发给自己的交易,因为找零),钱包中的交易一共有三个来源:
下面介绍比较重要的全局变量
map mapWallet; //key:交易hash //value: 交易对象
map, CPrivKey> mapKeys; //key:公钥 //value: 私钥
CKey keyUser; //控制公钥和私钥的handler
mapWallet保存钱包中的交易,mapKeys保存公钥私钥对,keyUser是公钥和私钥的管理者,比较简单,新的私钥和公钥是调用openssl库生成的。这三个全局变量的数据都是在通过LoadWallet函数加载进内存的。
LoadWallet流程比价简单,就是将wallet.dat的数据加载到内存,然后设置一下默认使用的公钥的私钥,如果不存在就生成新的公钥私钥对。
bool LoadWallet()
{
vector vchDefaultKey;
//从数据库文件wallet.dat将钱包内容加载内存, 默认公钥放在vchDefaultKey
if (!CWalletDB("cr").LoadWallet(vchDefaultKey))
return false;
//默认公钥有对应私钥
if (mapKeys.count(vchDefaultKey))
{
// Set keyUser
keyUser.SetPubKey(vchDefaultKey);
keyUser.SetPrivKey(mapKeys[vchDefaultKey]);
}
//map里找不着默认公钥对应的私钥,创造新key
else
{
// Create new keyUser and set as default key
RandAddSeed(true);
keyUser.MakeNewKey();
//新生成的公钥私钥对写数据库
if (!AddKey(keyUser))
return false;
if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address"))
return false;
//写默认key
CWalletDB().WriteDefaultKey(keyUser.GetPubKey());
}
return true;
}
想要使用钱包发起一笔交易,先要看一看交易的结构,以Alice向Bob购买咖啡的交易为例:
{
"version": 1,
"locktime": 0,
"vin": [
{
"txid":"7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",
"vout": 0,
"scriptSig": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
"sequence": 4294967295
}
],
"vout": [
{
"value": 0.01500000,
"scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG"
},
{
"value": 0.08450000,
"scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG",
}
]
}
这笔交易是由Alice的钱包构建生成然后广播出来的,矿工收到是序列化的交易,是一串数字:
咖啡交易交易的序列化十六进制表示(共258个字节)
0100000001186f9f998a5aa6f048e51dd8419a14d8a0f1a8a2836dd73
4d2804fe65fa35779000000008b483045022100884d142d86652a3f47
ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039
ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813
01410484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade84
16ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc1
7b4a10fa336a8d752adfffffffff0260e31600000000001976a914ab6
8025513c3dbd2f7b92a94e0581f5d50f654e788acd0ef800000000000
1976a9147f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a888ac00000000
下面根据序列化交易的结构解析下这串数字,熟悉一下每个字节,然后对重要字段作出说明:
01000000: version字段(小端格式)
01: vin的成员个数
186f....5779: txid(小端格式)
00000000: vin中的vout索引
8b: 解锁脚本的size(139个字节)
48: 签名部分的size(72个字节)
3845....3803: 签名数据
01: 签名类型(对整个交易签名)
41: 公钥的size(65个字节)
0484....2adf: 公钥
ffffffff: Sequence(最大值)
02: vout的成员个数
60e3160000000000: vout[0]的value(小端格式,0.015btc)
19: 锁定脚本的size(25个字节)
76: 操作码OP_DUP
a9: 操作码OP_HASH160
14: 公钥hash的size(20个字节)
ab68....54e7: 公钥hash
88: 操作码OP_EQUALVERIFY
ac: 操作码OP_CHECKSIG
d0ef800000000000: vout[1]的value(小端格式,0.0845btc)
19: ....
76: ....
a9: ....
14: ....
7f9b....25a8: 公钥hash
88: ....
ac: ....
00000000: locktime
❖ scriptSig
解锁脚本,由签名(包括签名类型)和公钥组成
❖ scriptPubKey
锁定脚本,对于普通的交易,比如这笔,其格式为:OP_DUP OP_HASH160 公钥哈希 OP_EQUALVERIFY OP_CHECKSIG
❖ version
交易版本号,符合标准的是1和2。1代表普通交易,2代表支持操作码OP_CHECKSEQUENCEVERIFY的交易,其余数字预留。
❖ locktime
锁定区块高度,到了这个高度交易才可以被打包,默认是0
❖ sequence
交易可替换的标志,默认是UINT_MAX
中本聪在比特币的设计中,想要使用locktime和sequence完成交易的替换,设想是这样:
这个设想是很好的,可是在比特币的早期为了防止DOS攻击,实际上nSequence字段是没被启用的,一直被设置为最大值。直到BIP-125提出后,nSequence被主要用来做RBF(replace-by-fee)的信号,允许节点用更高交易费的交易代替原来的交易。
下面来介绍用钱包发起交易整体流程,先贴出流程图
下面对流程图的关键步骤作出解释
❖ 计算可用余额
因为mapWallet保存了所有发给自己钱的交易,所以需要遍历mapWallet,找出所有可用的交易,算下钱包一共有多少钱,那么什么样算是可用的交易?要满足两个条件:
未被花费好理解,但是请注意这和UTXO没有关系,UTXO集是从区块链账本里提炼出的集合。而我们这里是从mapWalle中查找未花费的交易,仅仅是看fSpent字段,这个字段在我们发出交易的时候会被置True。
那么final交易是什么? 就是不能再变的交易。我们用钱包花钱时是可以引用未被记录至区块链账本中的交易的,但是要保证交易不会被替换。贴出代码:
int64 GetBalance()
{
int64 nStart, nEnd;
QueryPerformanceCounter((LARGE_INTEGER*)&nStart);
int64 nTotal = 0;
CRITICAL_BLOCK(cs_mapWallet)
{
for (map::iterator it = mapWallet.begin(); it != mapWallet.end(); ++it)
{
CWalletTx* pcoin = &(*it).second;
if (!pcoin->IsFinal() || pcoin->fSpent)
continue;
nTotal += pcoin->GetCredit();
}
}
QueryPerformanceCounter((LARGE_INTEGER*)&nEnd);
///printf(" GetBalance() time = %16I64d\n", nEnd - nStart);
return nTotal;
}
❖ CreateTransaction
CreateTransaction是创建交易的函数,流程稍微复杂,会在后面展开分析。
❖ CommintTransactionSpent
交易被成功的创建之后,接下来需要将交易的fSpent标志位置true,然后写入数据库。
❖ 检查和广播交易
到了这步,只要检查一下交易并广播出去就可以了
// Broadcast
if (!wtxNew.AcceptTransaction())
{
// This must not fail. The transaction has already been signed and recorded.
throw runtime_error("SendMoney() : wtxNew.AcceptTransaction() failed\n");
wxMessageBox("Error: Transaction not valid ", "Sending...");
return error("SendMoney() : Error: Transaction not valid");
}
wtxNew.RelayWalletTransaction();
CreateTransaction函数就是用mapWallet中已有的数据按照模板把交易搭起来,先看流程图再解释。
❖ 选择交易
首先是从mapWallet中选择交易,排除掉非final和已经花费掉的交易后,在剩下的交易中凑出最接近于目标交易额(已加上默认手续费)的交易组合。
❖ 构造vout
中本聪这个版本的代码在构造vout时写的比较简单,vout[0]用于付款,vout[1]用于找零,从注释里看,他已经考虑到了这里的不妥,在这个版本中仅仅做了简单的实现。vout中除了金额就是锁定脚本。
// Fill vout[0] to the payee
wtxNew.vout.push_back(CTxOut(nValueOut, scriptPubKey));
// Fill vout[1] back to self with any change
if (nValueIn > nValue)
{
/// todo: for privacy, should randomize the order of outputs,
// would also have to use a new key for the change.
// Use the same key as one of the coins
vector vchPubKey;
CTransaction& txFirst = *(*setCoins.begin());
foreach(const CTxOut& txout, txFirst.vout)
if (txout.IsMine())
if (ExtractPubKey(txout.scriptPubKey, true, vchPubKey))
break;
if (vchPubKey.empty())
return false;
// Fill vout[1] to ourself
CScript scriptPubKey;
scriptPubKey << vchPubKey << OP_CHECKSIG;
wtxNew.vout.push_back(CTxOut(nValueIn - nValue, scriptPubKey));
}
❖ 构造vin
接下来,用选出的交易组合构造vin。 这个过程主要就是遍历vin数组,用私钥和一个临时构建的交易的hash值生成签名(调用openssl的椭圆曲线加密算法函数),然后将每个签名填入对应的vin位置。
// Sign
int nIn = 0;
//对于使用的每笔交易,签名
foreach(CWalletTx* pcoin, setCoins)
for (int nOut = 0; nOut < pcoin->vout.size(); nOut++)
if (pcoin->vout[nOut].IsMine())
SignSignature(*pcoin, wtxNew, nIn++);
因为签名类型是01(对整个交易做签名),所以签名要由私钥和一个临时构建的交易通过函数运算生成,临时构建的交易是这样生成的:拷贝一个新交易的副本,将副本的vin清空,在vin对应的索引处本来填解锁脚本的位置填入父交易的锁定脚本,这样就生成了这个临时构建的交易,然后对其hash,将hash值和私钥当做参数传入底层的签名函数就生成了签名,贴出此处的代码
bool SignSignature(const CTransaction& txFrom, CTransaction& txTo, unsigned int nIn, int nHashType, CScript scriptPrereq)
{
assert(nIn < txTo.vin.size());
//本笔交易的vin的具体部分
CTxIn& txin = txTo.vin[nIn];
assert(txin.prevout.n < txFrom.vout.size());
//上笔交易的vout的具体部分
const CTxOut& txout = txFrom.vout[txin.prevout.n];
// Leave out the signature from the hash, since a signature can't sign itself.
// The checksig op will also drop the signatures from its hash.
// 构建了一个txTemp,只留下txTo的vin[nIn]部分,解锁脚本处填的是vin的锁定脚本,hash之,返回hash值
uint256 hash = SignatureHash(scriptPrereq + txout.scriptPubKey, txTo, nIn, nHashType);
if (!Solver(txout.scriptPubKey, hash, nHashType, txin.scriptSig))
return false;
txin.scriptSig = scriptPrereq + txin.scriptSig;
// Test solution
if (scriptPrereq.empty())
if (!EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn))
return false;
return true;
}
这里是调用底层openssl库的ECDSA_sign函数的地方,hash就是就是临时构建交易的hash,pkey是私钥,生成的签名存入vchSig中
bool Sign(uint256 hash, vector& vchSig)
{
vchSig.clear();
unsigned char pchSig[10000];
unsigned int nSize = 0;
if (!ECDSA_sign(0, (unsigned char*)&hash, sizeof(hash), pchSig, &nSize, pkey))
return false;
vchSig.resize(nSize);
memcpy(&vchSig[0], pchSig, nSize);
return true;
}
❖ 比较手续费
比特币的最低手续费率是每1kb数据1000聪,接下来会检查钱包设定的手续费是否低于需要支付的最低手续费,如果是,那么要更新手续费,从SelectCoins函数开始重新走一遍流程。
❖ 更新前向交易
接下来,会以一定规则追溯新交易的前向交易,将找到的交易存在集合vtxPrev中,vtxPrev只在两个地方用到,一是加载钱包后,遍历这个集合,对不在区块链中的交易调用AcceptTransaction函数;第二是在RelayWalletTransaction函数中,也会遍历这个集合,查找里面不在交易池和区块链中的交易,找到后将这些交易和新创建的交易一起广播出去。
因此可以看出,vtxPrev的作用是存储支持新交易的那些交易,如过钱包发出的新交易想要尽快被打包,钱包需要“积极地”将这些支持他的交易也广播出去。
至此,使用具备钱包功能的全节点发送交易的流程就分析完了。