1.GetCredit
我们来接上一章代码,如果这笔交易符合要求,即未花费,则调用下面语句进行查找,获取金额:
cpp
int64 n = pcoin->GetCredit();
if (n <= 0)
continue;
调用了这笔CWalletTx里的GetCredit(父类CMerkleTx)函数:
cpp
int64 GetCredit() const
{
// Must wait until coinbase is safely deep enough in the chain before valuing it
if (IsCoinBase() && GetBlocksToMaturity() > 0)
return 0;
return CTransaction::GetCredit();
}
在这里判断是你的引用来源是否为挖矿所得,如果是,则要100个区块之后,否则return 0;这笔交易无法被引用。
然后就是调用父类(CTransaction)的GetCredit来获取值,此函代码如下:
cpp
int64 GetCredit() const
{
int64 nCredit = 0;
foreach(const CTxOut& txout, vout)
nCredit += txout.GetCredit();
return nCredit;
}
我们可以看到,遍历了这笔tx的每一个vout,然后调用txout.GetCredit(),获取值,累加。
我们来看看这个CTxOut类里的函数代码:
cpp
class CTxOut
{
public:
int64 nValue;
CScript scriptPubKey;
int64 GetCredit() const
{
if (IsMine())
return nValue;
return 0;
}
2.IsMine
看到没有,调用了IsMine这个函数,判断这笔vout是否是转给我的,如果是则返回数额(nValue)
而这个IsMine是怎么判断的呢,它从scriptPubKey里解析出公钥,然后,从mapKeys(P2PK)和mapPubKeys(P2PKH Hash160)里查找,如果有对应的,即我们知道私钥,就判断是属于我的。
这两个变量之前我们学钱包那章的时候,知道就是存储关于我的钱包账号相关的公钥。
所以在这里就用上了。好了,具体代码就不细说了,有点复杂,通过构建签名成功的方式判断,不是直接判断的。就是用传入的scriptPubKey去构建签名,如果能签名成功,那肯定你是有这个账号相关的密钥。所以从侧面判断出这笔交易(vout)就是你的,你拥有对这笔交易的处理权(因为密钥你都有了)。从这里可以看出,如果你转给别人50btc,但你钱包下的单个地址不够,只有25btc,但如果你的钱包还有一个地址,有30btc,它会把这两个地址都引用的,凑够50btc,即一个tx下,不管是vin还是vout可以有不同的地址。那如何保证不会引起混乱呢,是签名,签名保证了,我引用多少,支出多少,不可改变,一变签名就错。
3.选币逻辑(挑选归类)
好了,跳回到之前的代码,我们来看一下后面的代码:
cpp
int64 n = pcoin->GetCredit();
if (n <= 0)
continue;
if (n < nTargetValue)
{
vValue.push_back(make_pair(n, pcoin));
nTotalLower += n;
}
else if (n == nTargetValue)
{
setCoinsRet.insert(pcoin);
return true;
}
else if (n < nLowestLarger)
{
nLowestLarger = n;
pcoinLowestLarger = pcoin;
}
调用GetCredit获取整笔的tx下的vout金额后,如果为0或小于0,那不用说了,失败了或者没有,肯定还得继续寻交易,所以调用了continue;
(从上面代码可以知道这个n,一旦获取,就是针对整笔tx的,这个和之前的fSpent标记对应上了)
然后是这个代码:
cpp
if (n < nTargetValue)
{
vValue.push_back(make_pair(n, pcoin));
nTotalLower += n;
}
如果这个n有值,但小于目标值,就是我要转账的金额,则将这笔值(n)和tx(pcoin)加入到vValue里,方便后续使用,并且累加值到nTotalLower里。
但是你在这里会看到,并没有nTotalLower>=nTargetValue的判断,从而跳出循环。而是会一直添加。
只有一个这样的跳出判断:
cpp
else if (n == nTargetValue)
{
setCoinsRet.insert(pcoin);
return true;
}
相等就跳出,但我们知道vout不是我们能规定的,所以恰好等于nTargetValue这种概率比较小。
所以这些代码是在干什么呢,包括后面的这个代码:
cpp
else if (n < nLowestLarger)
{
nLowestLarger = n;
pcoinLowestLarger = pcoin;
}
其实他是在挑选该怎么最好的构建这笔交易对应的金额vout,它不是简单的随便挑选,只要累加到符合金额的交易就构建交易了,它需要选择,所以它才会这样。
所以当找到一个tx正好跟你转账的金额相等,那么就直接用这个tx,这是最完美的,不用找零,所以直接退出。如果没找到,则选择拼凑交易,但这个拼凑也不是随便选的,也有逻辑,所以需要全部加到vValue里,然后再来挑选。
那么还有一个逻辑判断即:n<nLowestLarger
这里是判断n值大于目标金额,即需要找零的,处理逻辑。但为什么不是这样判断呢?
即n>nTargetValue呢,因为前面两个已经帮你判断了,n即不是小于nTargetValue,也不是等于nTargetValue,那么走到第三步,那肯定是大于nTargetValue咯。
所以这里的处理是记录这个大于的值,即存到nLowestLarger。然后继续,如果找到比这个大于的值更小的,就更新这个nLowestLarger,所以它也不是一找到大于的就用这个vout了。而是找到那个最小的大于n的vout。就像你花费8块钱,你手里有张10块的,20块的,100块的。你肯定一般是拿出10块的张支付然后找零。
根据上面代码的执行逻辑,我们可以推断出大概的选币逻辑,优先相等,然后拼凑,最后选找零的vout。其实跟我们自然人的支付是一样的逻辑,商家收我们10块,如果你口袋中,有一张10块,两张5块,一张20,你选择怎么给他?如果去掉那张10块的,你又会怎么选择?是一样的道理。
(当然这个拼凑也允许找零的,比如5元和6元,假设有6元面值,你还是会选择拼凑,更多的细节,我们可以看后续的代码逻辑)
我们来看接下来的代码:
cpp
if (nTotalLower < nTargetValue)
{
if (pcoinLowestLarger == NULL)
return false;
setCoinsRet.insert(pcoinLowestLarger);
return true;
}
这里的判断是当小额组合(拼凑)不够支付时,如果大额没为null,就是有大额,则选择大额。插入到setCoinsRet里,然后返回真,选币成功,注意这里的setCoinsRet,在之前如果有等额,也会被插入,并返回真,选币成功。这是两种选币成功的逻辑。从:
if (nTotalLower < nTargetValue)
这个判断可以看到,大额支付,是放在最后备选的。如果小额够,是优先选择小额支付,证明我们之前的推断没错。
4.小额组合选币算法
好,如果继续往下执行的话,那说明是进入小额组合选币的逻辑了,采取这种方法,因为在上面,已经判断了,如果小额不够,然后大额又没有,(到这里默认等额也没有)就会返回假(你的余额不够)。那么如果还能继续往下执行的话(说明没进入到上面判断的处理逻辑,也就是小额足够):
进入到了小额组合选币的处理逻辑,从小额集合中,挑选满足这笔支付金额的最优解组合,即从一堆零钱(UTXO)里挑出一组,尽量满足**nTargetValue,**但不要太超额。
详细代码:
cpp
// Solve subset sum by stochastic approximation
// 使用随机近似算法解决"子集和"问题(Subset Sum Problem)
sort(vValue.rbegin(), vValue.rend());
// 将所有小于目标金额的 UTXO 按金额从大到小排序
// 目的是让大额的小币优先被尝试,提高找到较优组合的概率
vector<char> vfIncluded; // 当前尝试的选中标记(临时使用)
vector<char> vfBest(vValue.size(), true); // 记录到目前为止最好的选中组合(默认全选)
int64 nBest = nTotalLower; // 记录当前最好的总金额(初始为所有小额总和)
// 进行最多 1000 次随机尝试,目标是找到总和最接近 nTargetValue 的组合
for (int nRep = 0; nRep < 1000 && nBest != nTargetValue; nRep++)
{
vfIncluded.assign(vValue.size(), false); // 重置本次尝试的选中状态
int64 nTotal = 0; // 本次尝试累加的金额
bool fReachedTarget = false; // 是否已经达到或超过目标金额
// 进行两轮尝试(nPass = 0 和 nPass = 1),增加找到更好组合的概率
for (int nPass = 0; nPass < 2 && !fReachedTarget; nPass++)
{
for (int i = 0; i < vValue.size(); i++)
{
// 第一轮:随机决定是否纳入该 UTXO
// 第二轮:只尝试之前未纳入的 UTXO(弥补第一轮遗漏)
if (nPass == 0 ? rand() % 2 : !vfIncluded[i])
{
nTotal += vValue[i].first; // 尝试加入当前 UTXO
vfIncluded[i] = true;
// 如果总金额已经达到或超过目标值
if (nTotal >= nTargetValue)
{
fReachedTarget = true;
// 如果本次组合比之前记录的最好结果"更接近"目标值,则更新最佳记录
if (nTotal < nBest)
{
nBest = nTotal; // 更新最好的总金额
vfBest = vfIncluded; // 保存当前最好的选中组合
}
// 回溯:撤销刚才加入的这个 UTXO,继续尝试其他组合
nTotal -= vValue[i].first;
vfIncluded[i] = false;
}
}
}
}
}
// ====================== 最终决策:小额组合 vs 单个最小大额 ======================
// 如果存在大额 UTXO,且使用它产生的找零金额 ≤ 当前小额组合产生的找零金额
// 则优先选择使用单个最小的大额 UTXO(因为找零更少,通常更好)
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); // 把选中的小额 UTXO 加入结果集
//// 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; // 选币成功,返回 true
5.sort
我们先来看第一句:
cpp
// Solve subset sum by stochastic approximation
sort(vValue.rbegin(), vValue.rend());
sort这个函数,将vValue数组里面的金额排序,即从大到小排序。然后随机尝试优先从大额里组合成支付金额,注意这里是随机尝试,为什么要这样,因为如果完整的遍历计算比较每个组合,找到最好的那个组合,几乎不可能完成,需要大量的计算,所以采用的是随机查找,大概有个差不多好的就行。当然也不是纯随机,那样效率也低,是一种特殊的随机查找算法。
具体看代码了解。
6.随机1000次
我们来看这个循环代码:
cpp
for (int nRep = 0; nRep < 1000 && nBest != nTargetValue; nRep++)
{
这里的意思,是进行随机1000次尝试,如果组合正好等于目标值,则停止查找,不用找零,这是最好的方案,如果没有,则找满1000次。然后保留那个最好的组合,即找零最少(即 nTotal 最接近 nTargetValue 且 ≥ nTargetValue)的那一个组合。
我们看具体的代码,实际代码有点复杂,因为它里面又细分了两次随机查找(正反),这里我把代码简化一下,理解了后,再把去掉的代码做说明,这样便于理解一点:
7.随机组合
我们来看进入循环后:
cpp
for (int i = 0; i < vValue.size(); i++)
开始遍历vValue中的每一个coin的值,然后挑选是通过随机的方式:
cpp
if (nPass == 0 ? rand() % 2 : !vfIncluded[i])
当然这里写这个三元表达试,是为了正反两轮随机的,这里假设没有,只有一轮。
那么你可以理解为这样:
cpp
if (rand() % 2)
那么if里的代码,理论上来说,有2分之一的概率会被执行,也就是随机选中这笔小额支付(vValue):
cpp
{
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;
}
}
将vValue[i].first的值进行累加到nTotal,然后累加到符合目标值后,即nTotal>=nTargetValuer后。
8.回退机制
则将此nTotal值记录在nBest里,但这一轮随机并不算完,它还会回退最后一个值,然后再继续找,直到遍历完整个vValue,这当中如果有符合要求,并小于上一轮的nTtotal,则用这个组合替代上一个,即这段代码逻辑:
cpp
if (nTotal < nBest)
{
nBest = nTotal;
...
}
注意,这里只回退一个值,并不是所有即:
cpp
nTotal -= vValue[i].first;
vfIncluded[i] = false;
好,这样遍历完vValue后,还不算完,还得将之前没选中的vValue,又按照上面的方法查找组合一下,如果能找到最优解,则进行代替。这样才算是完整的一次随机组合,找到一个最优的组合。
然后这样的随机,上面有个更大的for语句,还要进行1000次,从这1000次里找到的最优组合,就是最终的组合了。
9.组合记录
之前我们并没有看到怎么记录组合的,那它是怎么记录的呢?我们来看这两个变量:
cpp
vector<char> vfIncluded;
vector<char> vfBest(vValue.size(), true);
是一个数组,它们的大小是跟vValue一样的,并且是bool值,当元素设置为真时,即代表这个对应的coin值是组合中的一员。也就是,你可以遍历vfbest,看哪些元素为真,假设vfBest[2],vfBest[7],vfBest[8]为真,那么相应的vValue[2],vValue[7],vValue[8]就是一个最优的组合。
是这样记录的,我们可以在代码中看到:
cpp
if (nPass == 0 ? rand() % 2 : !vfIncluded[i])
{
nTotal += vValue[i].first;
vfIncluded[i] = true;
if (nTotal >= nTargetValue)
{
每当选中一个coin值,就是将其对应的vfIncluded[i] = true;设置为真(这个也可用于后面的正反选取,如果为假则表示这一轮没选中,那么下一轮就使用这些没选中的做组合)
然后当vfIncluded做为当前最优的组合时,就会被赋予给vfBest:
cpp
if (nTotal < nBest)
{
nBest = nTotal;
vfBest = vfIncluded;
}
所以vfBest记录了当前最优的组合。
10.正反选取
在正向选一轮之后,还会反向选一轮,就是上次随机(rand()%2),没选中的再进行一轮组合选取。
关键点在这个三元运算符:
cpp
if (nPass == 0 ? rand() % 2 : !vfIncluded[i])
{
当反向选取后,也就是第二轮,则nPass为1,那么nPass==0为假,则会执行后一个表达式,即!vfIncluded[i],也即,vfIncluded[i]为假,里面的代码才会被执行,就是没选中的那些,标记为假的vfIncluded[i]。当然由于回退机制,第一轮组合的最后一个并不在此列。
当然有的人可能会有疑问,如果只回退一个,那么目标值是100元,那么如果前面有900个1毛的,凑成90元,然后只盯着最后一个回退组合,那不是很粗糙?这个不用担心,别忘了前面的从大到小的排序。所以这个情况不可能发生。如果有最优解的话,但是这样的话,从大到小,会不会又发生了直接一张90元+50元,90元+80元,90元+20元,而有一种50+50元不会被选到。这个也不用担心,因为我们是随机的,忘记了那个rand()%2吗?所以50+50也有可能被抽中,而且这样的随机我们足足进行了1000次。就是为了尽可能的找到最优解。
所以这种是按照大小排序后再随机,不是完全的随机,目的是为了尽可能找到最合适的那个组合。
10.1余额等额
但是这里还有一个问题,如果我的余额为50btc,我正好要转账别人50btc,而我的这50btc分布在100个vout里,按照上面那种随机二分之一的概率选vout,那么必须是100次连续选中,才能凑出这个组合,这个概率为2的100次方之一,而随机1000次,根本不可能正好生成这个组合。
别急,这种漏洞是不会有的,因为我们漏掉了一些细节。
即,第一次选择(正向),如果没凑足目标值,这下面的变量是不会被清零的:
cpp
nTotal += vValue[i].first;
vfIncluded[i] = true;
nTotal会累加到第二次选择中,然后接着累加,直到凑够目标值。关键点在于nTotal并不会被重新赋值。
11.小额和大额
好,小额组合完成后,到了这里,也并不是最终的获胜者,如果你同样也是找零的,那么就是和大额的比较一下,谁找零的少就用谁,也就是,我手里一堆零钱,我随便试着凑钱,凑到差不多就停,试1000次,选一个最不浪费的方案。如果发现直接用一张大钞更划算,那就别折腾零钱了。"
所以你会看到以下判断:
cpp
// 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;
如果大额不为空(如有),判断大额的找零比组合的还少,则直接用大额的,即:
cpp
setCoinsRet.insert(pcoinLowestLarger);
否则 ,将小额的coin全插入setCoinsRet
cpp
for (int i = 0; i < vValue.size(); i++)
if (vfBest[i])
setCoinsRet.insert(vValue[i].second);
12.setCoinsRet
整个代码看完后,我们可以知道,如果选币成功,不管选择哪种支付方式,小额组合,等额,还是大额找零,都会将它们存到setCoinsRet里。
那么这个函数:
cpp
bool SelectCoins(int64 nTargetValue, set<CWalletTx*>& setCoinsRet)
在上一级调用中:
cpp
// Choose coins to use
set<CWalletTx*> setCoins;
if (!SelectCoins(nValue, setCoins))
return false;
关于setCoins获得了什么,以及怎么处理,现在我们就很清楚了,现在我们来看上一级后续的处理代码,就会简单一些。下一章中说明后续代码。