简介
比特币系统是一种点对点的电子现金系统,在这个系统中,最重要的概念就是交易了(这里的交易就等同于转账),当我们现在说起交易的时候,脑海中可能浮现很多关于block chian关于交易的很多原理以及概念,但为了理解btc最基本的概念及源码呢,我们可以先抛开所有已学过的一些知识,一步步去学习。
从最简单的开始,在我们现实生活中,我们发起一笔银行转账,你需要打开你的电子账户,在你的银行应用上输入接收款项的银行账户以及你需要转账多少钱。然后发起这笔交易。等待若干时间后,银行会通过某种方式通知你是到账了还是被原路返回了,就标志着这次交易的成功或者失败,这里的银行就是一个第三方中介,所有的交易都需要经过它。这里的前提是我们大家都信任这个银行,但是现在我们拿掉这个可以被信任的第三方中介呢,我们该如何做这个事情呢?
在我们的btc系统中,做到了这一点。它基于密码学原理而不基于信用,使得任何达成一致的双方,能够直接进行支付,从而不需要第三方中介的参与。但这里也会有引入一个新的问题,你无法确定发起交易的人是不是恶意的,他把同一笔钱同时转向多个人,那这时候可能又会想到引入一个中介,那就又回到了老路了,这里btc又提出了一个解决方案,它构建了这样的一个网络,该网络通过随机散列(hashing)对全部交易加上时间戳(timestamps),将它们合并入一个不断延伸的基于随机散列的工作量证明(proof-of-work)的链条作为交易记录,除非重新完成全部的工作量证明,形成的交易记录将不可更改。最长的链条不仅将作为被观察到的事件序列(sequence)的证明,而且被看做是来自CPU计算能力最大的池(pool),只要大多数的CPU计算能力都没有打算合作起来对全网进行攻击,那么诚实的节点将会生成最长的、超过攻击者的链条。这个系统本身需要的基础设施非常少。信息尽最大努力在全网传播即可,节点(nodes)可以随时离开和重新加入网络,并将最长的工作量证明链条作为在该节点离线期间发生的交易的证明。这段话是抄自比特币白皮书的一段原话,下面我们会根据比特币的文档,比特币白皮书,还有精通比特币这三份资料对中本聪版本的比特币代码做一个阅读了解他的具体实现。
源码阅读
源代码含注释
我们看下交易的结构体,包含n个输入与n个输出
class CTransaction
{
public:
int nVersion;
vector<CTxIn> vin; //输入
vector<CTxOut> vout;//输出
int nLockTime;
...
}
输入的类
class CTxIn
{
public:
COutPoint prevout;// 包含了上一个交易的输出
CScript scriptSig; //解锁脚本
unsigned int nSequence;
......
}
class COutPoint
{
public:
uint256 hash; //引用的前一个交易的id
unsigned int n;//输出的索引 使用的是上面hash对应的交易的第几个输出
......
}
输出的结构
class CTxOut
{
public:
int64 nValue; //输出的金额
CScript scriptPubKey; //公钥脚本/锁定脚本
......
}
光看代码可能有点不理解我在网上找了个图解释下
从图来理解代码(先抛开脚本部分),我们可以看出一个交易的输入/输出可以是多个,交易的输入对应前一个交易的输出。
理解了交易大体的结构之后我们看下代码中是如何发起交易的??
我们观察一下的代码,这段代码是发起了一笔转账,我们目前只需要先关注第一个if语句,他创建了一笔tx,返回一个bool,参数分别是对应的公钥脚本,发送的金额 , CWalletTx类型的一个引用,他是一个带有附加信息的交易结构体,最后一个参数是手续费。
bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew)
{
CRITICAL_BLOCK(cs_main)
{
int64 nFeeRequired;
if (!CreateTransaction(scriptPubKey, nValue, wtxNew, nFeeRequired))
{
string strError;
if (nValue + nFeeRequired > GetBalance())
strError = strprintf("Error: This is an oversized transaction that requires a transaction fee of %s ", FormatMoney(nFeeRequired).c_str());
else
strError = "Error: Transaction creation failed ";
wxMessageBox(strError, "Sending...");
return error("SendMoney() : %s\n", strError.c_str());
}
if (!CommitTransactionSpent(wtxNew))
{
wxMessageBox("Error finalizing transaction", "Sending...");
return error("SendMoney() : Error finalizing transaction");
}
printf("SendMoney: %s\n", wtxNew.GetHash().ToString().substr(0,6).c_str());
// 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();
}
MainFrameRepaint();
return true;
}
让我们接下来观察下创建交易的函数方法,在这之前先观察以下集合函数。
通过调用 SelectCoins 函数选择一组足够支付目标金额的输入交易。这组输入交易会被加入到 setCoins 集合中。我们先来看下他的具体实现,通过for循环遍历自己未花费的金额,
map<uint256, CWalletTx> mapWallet; //utxo
bool SelectCoins(int64 nTargetValue, set<CWalletTx*>& setCoinsRet)
{
setCoinsRet.clear();
// List of values less than target
int64 nLowestLarger = _I64_MAX;
CWalletTx* pcoinLowestLarger = NULL;
vector<pair<int64, CWalletTx*> > vValue;
int64 nTotalLower = 0; //累积小于目标金额的所有 UTXO 的总和
CRITICAL_BLOCK(cs_mapWallet)
{
//遍历 UTXO
for (map<uint256, CWalletTx>::iterator it = mapWallet.begin(); it != mapWallet.end(); ++it)
{
CWalletTx* pcoin = &(*it).second;
if (!pcoin->IsFinal() || pcoin->fSpent)
continue;
//获取这笔未花费的金额
int64 n = pcoin->GetCredit();
if (n <= 0)
continue;
// 金额小于目标值的时候,先把它保存起来
if (n < nTargetValue)
{
vValue.push_back(make_pair(n, pcoin));
nTotalLower += n; //累计金额
}
//如果找到正好等于目标金额的utxo项,直接获取之后返回
else if (n == nTargetValue)
{
setCoinsRet.insert(pcoin);
return true;
}
//大于目标金额 小于pcoinLowestLarger,在找不到就使用这一项
else if (n < nLowestLarger)
{
nLowestLarger = n;
pcoinLowestLarger = pcoin;
}
}
}
//挑选一个大于目标金额的最小的一个金额 返回回去使用
if (nTotalLower < nTargetValue)
{
if (pcoinLowestLarger == NULL)
return false;
setCoinsRet.insert(pcoinLowestLarger);
return true;
}
//下面的逻辑是通过组合各种子集找到最接近目标金额的最优解
// Solve subset sum by stochastic approximation
sort(vValue.rbegin(), vValue.rend());
vector<char> vfIncluded;
vector<char> vfBest(vValue.size(), true);
int64 nBest = nTotalLower;
for (int nRep = 0; nRep < 1000 && nBest != nTargetValue; nRep++)
{
vfIncluded.assign(vValue.size(), false);
int64 nTotal = 0;
bool fReachedTarget = false;
for (int nPass = 0; nPass < 2 && !fReachedTarget; nPass++)
{
for (int i = 0; i < vValue.size(); i++)
{
if (nPass == 0 ? rand() % 2 : !vfIncluded[i])
{
nTotal += vValue[i].first;
vfIncluded[i] = true;
if (nTotal >= nTargetValue)
{
fReachedTarget = true;
if (nTotal < nBest)
{
nBest = nTotal;
vfBest = vfIncluded;
}
nTotal -= vValue[i].first;
vfIncluded[i] = false;
}
}
}
}
}
// If the next larger is still closer, return it
if (pcoinLowestLarger && nLowestLarger - nTargetValue <= nBest - nTargetValue)
setCoinsRet.insert(pcoinLowestLarger);
else
{
for (int i = 0; i < vValue.size(); i++)
if (vfBest[i])
setCoinsRet.insert(vValue[i].second);
debug print
printf("SelectCoins() best subset: ");
for (int i = 0; i < vValue.size(); i++)
if (vfBest[i])
printf("%s ", FormatMoney(vValue[i].first).c_str());
printf("total %s\n", FormatMoney(nBest).c_str());
}
return true;
}
//IsFinal() 函数是用来判断交易是否已经“最终确定”(finalized),也就是说,交易是否可以被包含在区块链中
bool IsFinal() const
{
//nLockTime 是交易的一个字段,表示交易在某个时间点或区块高度之前不能被包含在区块中。
if (nLockTime == 0 || nLockTime < nBestHeight)
return true;
// 遍历交易的每一个输入(vin 是交易输入的列表)
foreach(const CTxIn& txin, vin)
// 如果任何一个输入不是最终的,返回 false,表明交易不是最终的
if (!txin.IsFinal())
return false;
// 如果所有输入都是最终的,返回 true,表明交易是最终的
return true;
}
我们回到创建交易的地方,这里我有个地方一开始理解错了,在utxo中,保存到是跟用户钱包有关的未花费交易的完整信息,所以它包含了找零跟转账到另一个地址的输出,并不是我一开始想的utxo中就是单纯保存了只有我自己的关联的输出。所以每次使用的时候都需要去检查这笔输出是否属于自己。下面代码简单的注释了下,但是有个地方需要注意AddSupportingTransactions,他是用来加载需要前置的一些交易
map<uint256, CWalletTx> mapWallet; //utxo
...省略
bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, int64& nFeeRequiredRet)
{
nFeeRequiredRet = 0;
CRITICAL_BLOCK(cs_main)
{
// txdb must be opened before the mapWallet lock
//只读的方式打开交易数据库
CTxDB txdb("r");
CRITICAL_BLOCK(cs_mapWallet)
{
int64 nFee = nTransactionFee;
loop
{
wtxNew.vin.clear();
wtxNew.vout.clear();
if (nValue < 0)
return false;
int64 nValueOut = nValue;
nValue += nFee; //手续费+要发送的金额,得到总共需要多少资金
// Choose coins to use
set<CWalletTx*> setCoins;
if (!SelectCoins(nValue, setCoins))
return false;
int64 nValueIn = 0;
foreach(CWalletTx* pcoin, setCoins)
nValueIn += pcoin->GetCredit();
// 根据目标金额和手续费创建交易的输出 一个是目标收款方的输出
// 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)
{
// Use the same key as one of the coins
vector<unsigned char> 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));
}
// 填充输入
// Fill vin
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));
// 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++);
// Check that enough fee is included
//计算交易费
if (nFee < wtxNew.GetMinFee(true))
{
nFee = nFeeRequiredRet = wtxNew.GetMinFee(true);
continue;
}
// Fill vtxPrev by copying from previous transactions vtxPrev
//用于在创建新的交易时,确保交易的输入(即引用的 UTXO)所在的区块链分支已经被包括,并且能够追溯到最近的区块。
// 这对于节点在验证交易时很重要,特别是在分叉链或者孤立链的情况下。
wtxNew.AddSupportingTransactions(txdb);
wtxNew.fTimeReceivedIsTxTime = true;
break;
}
}
}
return true;
}
void CWalletTx::AddSupportingTransactions(CTxDB& txdb)
{
vtxPrev.clear();
const int COPY_DEPTH = 3;
//如果当前交易的 Merkle 分支深度小于 COPY_DEPTH,则需要从链上补充支持交易。
if (SetMerkleBranch() < COPY_DEPTH)
{
vector<uint256> vWorkQueue;
//遍历交易的所有输入,将每个输入的前置交易哈希加入工作队列 vWorkQueue。
foreach(const CTxIn& txin, vin)
vWorkQueue.push_back(txin.prevout.hash);
// This critsect is OK because txdb is already open
CRITICAL_BLOCK(cs_mapWallet)
{
map<uint256, const CMerkleTx*> mapWalletPrev; //跟踪处理过的交易。
set<uint256> setAlreadyDone; //用于记录已经处理的交易哈希,防止重复处理。
for (int i = 0; i < vWorkQueue.size(); i++)
{
uint256 hash = vWorkQueue[i];
if (setAlreadyDone.count(hash))
continue;
setAlreadyDone.insert(hash);
//查找 hash 对应的交易 tx
CMerkleTx tx;
if (mapWallet.count(hash))
{
//如果交易存在于 mapWallet 中,则直接使用该交易,并且将其前置交易加入 mapWalletPrev。
tx = mapWallet[hash];
foreach(const CMerkleTx& txWalletPrev, mapWallet[hash].vtxPrev)
mapWalletPrev[txWalletPrev.GetHash()] = &txWalletPrev;
}
//如果 mapWallet 中不存在,则检查 mapWalletPrev
else if (mapWalletPrev.count(hash))
{
tx = *mapWalletPrev[hash];
}
//也不存在则尝试从磁盘中读取
else if (!fClient && txdb.ReadDiskTx(hash, tx))
{
;
}
else
{
printf("ERROR: AddSupportingTransactions() : unsupported transaction\n");
continue;
}
//调用 SetMerkleBranch() 更新 tx 的 Merkle 分支深度。如果深度小于 COPY_DEPTH,则继续追溯前置交易,直到达到指定深度。
int nDepth = tx.SetMerkleBranch();
vtxPrev.push_back(tx);
if (nDepth < COPY_DEPTH)
foreach(const CTxIn& txin, tx.vin)
vWorkQueue.push_back(txin.prevout.hash);
}
}
}
//反转 vtxPrev 中的交易顺序,以确保这些支持交易按照正确的顺序(从最新到最旧)排列。
reverse(vtxPrev.begin(), vtxPrev.end());
}
总结
以上是比特币在发送到网络之前,创建一个交易的基本流程,有错误的希望指正。