0x00 读码即挖矿
前两天看到群里还有人在讨论ETH和EOS的DAPP开发, 看来区块链的落地还是一线希望, 大家可以继续给自己的信仰充值。充值方式众多, 比如加仓, Fomo, 或是写DAPP, 读代码。那我继续前两次的操作, 继续阅读BTC的代码, 版本0.8.2。上次粗读了一番交易的流程, 大概的来龙去脉也略知一二, 这次就按照关键的执行函数来扣一下具体的细节。
0x01 函数CreateTransaction
函数原型如下:
bool CWallet::CreateTransaction(const vector<pair<CScript, int64> >& vecSend,
CWalletTx& wtxNew, CReserveKey& reservekey, int64& nFeeRet, std::string& strFailReason)
我们先看一下该函数的具体引用, 出现在rpcwallet.cpp中, 具体的用途就是RPC调用, 进行转账交易。
Value sendmany(const Array& params, bool fHelp)
{
...
...
// Send
CReserveKey keyChange(pwalletMain);
int64 nFeeRequired = 0;
string strFailReason;
bool fCreated = pwalletMain->CreateTransaction(vecSend, wtx, keyChange, nFeeRequired, strFailReason);
if (!fCreated)
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strFailReason);
if (!pwalletMain->CommitTransaction(wtx, keyChange))
throw JSONRPCError(RPC_WALLET_ERROR, "Transaction commit failed");
return wtx.GetHash().GetHex();
}
现在来看CreateTransaction函数的返回值和参数:
返回值: 如果创建成功返回true, 否则false
参数:
1, const vector<pair<CScript, int64> >& vecSend: 目标地址和金额, 数组, 支持批量转账
2, CWalletTx& wtxNew: 生成的钱包关联交易详情
3, int64& nFeeRet: gas费用
4, std::string& strFailReason: 失败原因
整个代码的模块是一个loop, 通过找到符合条件的utxo再break出来, 看一看代码的内部实现:
1.1 判断发送目标地址和金额是否合法, 没啥好说的
int64 nValue = 0;
BOOST_FOREACH (const PAIRTYPE(CScript, int64)& s, vecSend)
{
if (nValue < 0)
{
strFailReason = _("Transaction amounts must be positive");
return false;
}
nValue += s.second;
}
if (vecSend.empty() || nValue < 0)
{
strFailReason = _("Transaction amounts must be positive");
return false;
}
1.2 构造交易输出, 并丢弃粉尘交易, 防止DDOS攻击, 这块下文会详细描述
// vouts to the payees
BOOST_FOREACH (const PAIRTYPE(CScript, int64)& s, vecSend)
{
CTxOut txout(s.second, s.first);
if (txout.IsDust())
{
strFailReason = _("Transaction amount too small");
return false;
}
wtxNew.vout.push_back(txout);
}
1.3 构造交易出入, 也就是根据规则找到发送方最优的utxo集合, SelectCoins下文详细描述
// Choose coins to use
set<pair<const CWalletTx*,unsigned int> > setCoins;
int64 nValueIn = 0;
if (!SelectCoins(nTotalValue, setCoins, nValueIn))
{
strFailReason = _("Insufficient funds");
return false;
}
1.4 先计算优先级, 再根据优先级推导出交易费用, 下文详细描述
BOOST_FOREACH(PAIRTYPE(const CWalletTx*, unsigned int) pcoin, setCoins)
{
int64 nCredit = pcoin.first->vout[pcoin.second].nValue;
//The priority after the next block (depth+1) is used instead of the current,
//reflecting an assumption the user would accept a bit more delay for
//a chance at a free transaction.
dPriority += (double)nCredit * (pcoin.first->GetDepthInMainChain()+1)
}
...
dPriority /= nBytes;
1.5 计算找零, 并创建新的找零钱包公钥, 把找零加入到vOut中
int64 nChange = nValueIn - nValue - nFeeRet;
// if sub-cent change is required, the fee must be raised to at least nMinTxFee
// or until nChange becomes zero
// NOTE: this depends on the exact behaviour of GetMinFee
if (nFeeRet < CTransaction::nMinTxFee && nChange > 0 && nChange < CENT)
{
int64 nMoveToFee = min(nChange, CTransaction::nMinTxFee - nFeeRet);
nChange -= nMoveToFee;
nFeeRet += nMoveToFee;
}
if (nChange > 0)
{
// Note: We use a new key here to keep it from being obvious which side is the change.
// The drawback is that by not reusing a previous key, the change may be lost if a
// backup is restored, if the backup doesn't have the new private key for the change.
// If we reused the old key, it would be possible to add code to look for and
// rediscover unknown transactions that were written with keys of ours to recover
// post-backup change.
// Reserve a new key pair from key pool
CPubKey vchPubKey;
assert(reservekey.GetReservedKey(vchPubKey)); // should never fail, as we just unlocked
// Fill a vout to ourself
// TODO: pass in scriptChange instead of reservekey so
// change transaction isn't always pay-to-bitcoin-address
// 构建找零的新的公钥
CScript scriptChange;
scriptChange.SetDestination(vchPubKey.GetID());
CTxOut newTxOut(nChange, scriptChange);
// Never create dust outputs; if we would, just
// add the dust to the fee.
if (newTxOut.IsDust())
{
nFeeRet += nChange;
reservekey.ReturnKey();
}
else
{
// Insert change txn at random position:
vector<CTxOut>::iterator position = wtxNew.vout.begin()+GetRandInt(wtxNew.vout.size()+1);
wtxNew.vout.insert(position, newTxOut);
}
}
else
reservekey.ReturnKey();
1.6 签名, 并计算交易费用, 如果费用不满足条件, 再重新进入循环, 重复上述流程
// Fill vin
BOOST_FOREACH(const PAIRTYPE(const CWalletTx*,unsigned int)& coin, setCoins)
wtxNew.vin.push_back(CTxIn(coin.first->GetHash(),coin.second));
// Sign
int nIn = 0;
BOOST_FOREACH(const PAIRTYPE(const CWalletTx*,unsigned int)& coin, setCoins)
if (!SignSignature(*this, *coin.first, wtxNew, nIn++))
{
strFailReason = _("Signing transaction failed");
return false;
}
// Limit size
unsigned int nBytes = ::GetSerializeSize(*(CTransaction*)&wtxNew, SER_NETWORK, PROTOCOL_VERSION);
if (nBytes >= MAX_STANDARD_TX_SIZE)
{
strFailReason = _("Transaction too large");
return false;
}
dPriority /= nBytes;
// Check that enough fee is included
int64 nPayFee = nTransactionFee * (1 + (int64)nBytes / 1000);
bool fAllowFree = CTransaction::AllowFree(dPriority);
int64 nMinFee = wtxNew.GetMinFee(1, fAllowFree, GMF_SEND);
if (nFeeRet < max(nPayFee, nMinFee))
{
nFeeRet = max(nPayFee, nMinFee);
continue;
}
1.7 填充好WalletTX的vIn和vOut后, 用vIn的prevout来填充WalletTX的vtxPrev
// Fill vtxPrev by copying from previous transactions vtxPrev
wtxNew.AddSupportingTransactions();
wtxNew.fTimeReceivedIsTxTime = true;
0x02 粉尘交易Dust
前文我们已经讲过, 比特币的交易并不是传统意义上的数字加减运算, 它实质是utxo的转移。所以A向B转移1个BTC这个动作, 产生的结果是未知的, 比如就可以是1个完整的BTC转移, 也有可能是1000个0.001个BTC的转移, 也有可能是1.05个BTC转移(0.05个找零), 当然用户不需要自己去手工配置, 代码已经帮我们完成(下文的SelectCoin), 但有时候还是无法避免有dust交易的产生。dust的存在会造成区块链网络上DDOS攻击, 攻击者通过发送许多这样小金额的交易, 来堵塞整个网络。在比特币的网络中, 如果交易费(Fee)用高于1/3的交易价值(Value), 则被视为粉尘交易。 常规的来说, 一个P2PKH交易, 由于其最小体积时为一个输入, 一个输出, 总共 ‘148 + 34 = 182’ 字节, 而交易手续费是由每个节点配置的minRelayTxFee(比特币中默认为'0.00001BTC/KB’)决定的, 故而手续费就是 ‘182 / 1000 minRelayTxFee’, 那么其的3倍即 ‘546 / 1000 minRelayTxFee’, 也即默认为 0.00000546 BTC, 在BCH网络上也是一样的。
钱包在准备支付金额的时候有一个既定的规则, 就是在众多输入(inputs)中筹备支付金额的时候尽量避免产生小于0.01BTC的金额变动(比如你要支付5.005BTC, 钱包尽可能的选择3+2.005或者1+1+3.005, 而不是5+0.005)。
Dust代码描述如下:
bool CTxOut::IsDust() const
{
// "Dust" is defined in terms of CTransaction::nMinRelayTxFee,
// which has units satoshis-per-kilobyte.
// If you'd pay more than 1/3 in fees
// to spend something, then we consider it dust.
// A typical txout is 33 bytes big, and will
// need a CTxIn of at least 148 bytes to spend,
// so dust is a txout less than 54 uBTC
// (5430 satoshis) with default nMinRelayTxFee
return ((nValue*1000)/(3*((int)GetSerializeSize(SER_DISK,0)+148)) < CTransaction::nMinRelayTxFee);
}
0x03 交易费用计算Fee
数额越大、币龄(age)越高优先级越高
如果你发送金额太小或者是你的比特币刚开采出来不久, 那么你的转账就不再免费之列。每一个交易都会分配一个优先级, 这个优先级通过币的新旧程度、交易的字节数和交易的数量。具体来说, 对于每一个输入(inputs)来讲, 客户端会先将比特币的数量乘以这些币在块中存在的时间(币龄, age), 然后将所有的乘积加起来除以此次交易的大小(以字节为单位), 计算公式: priority = sum(input_value_in_base_units * input_age)/size_in_bytes, 计算结果如果小于0.576, 那么该交易就必须支付手续费。如果你确实大量的小额输入, 又想免费转出, 这时候你可以加一个数额大的、币龄大的比特币金额(多余的找零返回), 就会将平均优先级提高, 从而可以免费转出比特币。
代码如下:
static bool AllowFree(double dPriority)
{
// Large (in bytes) low-priority (new, small-coin) transactions
// need a fee.
return dPriority > COIN * 144 / 250;
}
在转账的最后客户端会检测本次转账的大小(以字节为单位), 大小一般取决于输入和输出的数额大小。
如果该次转账的大小超过10000字节但是优先级符合免费的标准, 那么仍然可以享受免费转账, 否则需要支付手续费。没1000字节的费用默认是0.0001BTC, 但是你也可以在客户端里进行追加, 依次打开选项卡“设置>选项>主要”进行手续费的调整。
相关代码如下:
int64 CTransaction::GetMinFee(unsigned int nBlockSize, bool fAllowFree,
enum GetMinFee_mode mode) const
{
// Base fee is either nMinTxFee or nMinRelayTxFee
int64 nBaseFee = (mode == GMF_RELAY) ? nMinRelayTxFee : nMinTxFee;
unsigned int nBytes = ::GetSerializeSize(*this, SER_NETWORK, PROTOCOL_VERSION);
unsigned int nNewBlockSize = nBlockSize + nBytes;
int64 nMinFee = (1 + (int64)nBytes / 1000) * nBaseFee;
if (fAllowFree)
{
if (nBlockSize == 1)
{
// Transactions under 10K are free
// (about 4500 BTC if made of 50 BTC inputs)
if (nBytes < 10000)
nMinFee = 0;
}
else
{
// Free transaction area
if (nNewBlockSize < 27000)
nMinFee = 0;
}
}
// To limit dust spam, require base fee if any output is less than 0.01
if (nMinFee < nBaseFee)
{
BOOST_FOREACH(const CTxOut& txout, vout)
if (txout.nValue < CENT)
nMinFee = nBaseFee;
}
// Raise the price as the block approaches full
if (nBlockSize != 1 && nNewBlockSize >= MAX_BLOCK_SIZE_GEN/2)
{
if (nNewBlockSize >= MAX_BLOCK_SIZE_GEN)
return MAX_MONEY;
nMinFee *= MAX_BLOCK_SIZE_GEN / (MAX_BLOCK_SIZE_GEN - nNewBlockSize);
}
if (!MoneyRange(nMinFee))
nMinFee = MAX_MONEY;
return nMinFee;
}
0x04 寻找最佳utxo集合策略SelectCoins
这个函数比较有意思, 我们先看下函数实现
bool CWallet::SelectCoins(int64 nTargetValue, set<pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64& nValueRet) const
{
vector<COutput> vCoins;
AvailableCoins(vCoins);
return (SelectCoinsMinConf(nTargetValue, 1, 6, vCoins, setCoinsRet, nValueRet) ||
SelectCoinsMinConf(nTargetValue, 1, 1, vCoins, setCoinsRet, nValueRet) ||
SelectCoinsMinConf(nTargetValue, 0, 1, vCoins, setCoinsRet, nValueRet));
}
先说一说函数的返回值和参数
返回值: 成功找到TxIn的集合返回true, 否则返回false
nTargetValue:目标金额
setCoinsRet: 要返回的TxIn集合
nValueRet: 实际金额(有可能包含找零)
AvailabeCoins通过在当前钱包视图中预过滤筛选出COutput的集合, 筛选策略主要是判断当前的引用输入是否经过确认等。
下一步再来看看SelectCoinsMinConf这个函数, 看到(1, 6),(1, 1), (0, 1)这样的硬编码顿时就有一种紧张感, 还好参数的命名比较直观。这三次相同函数不同参数的函数调用, 其实反映了一种选择策略, 就是按照交易被区块链确认次数递减来扩大选择范围, 转账给自己优先要用通过1次确认的, 转给别人要优先用通过6次确认的。这就是为什么我们现在在交易所进行法币场外交易时, 交易所作为第三方担保, 一般会在交易经过6次甚至更多次确认后才会把法币打给转出方, 这样实施双花恶意攻击就会难上加难。
再来说说SelectCoinsMinConf的策略:
- 1, 如果存在某UTXO值正好等于发送金额nValue(已包含手续费nFee), 则将该UTXO加入目标交易集并返回成功
- 2, 找出账户中UTXO值小于发送金额nValue的UTXO集vValue, 并将vValue中所有UTXO值求和为nTotalLower, 并找出所有UTXO值大于nValue的最小值nLowestLarger, 再分两种情况
- 2.1: nTotalLower小于nValue, 如果nLowestLarger存在, 则将该值对应的pcoinLowestLarger交易加入目标交易集并返回成功, 如果nLowestLarger不存在, 则说明“余额”不足, 返回失败
- 2.2: nTotalLower大于nValue, 则使用随进逼近法(最多1000次)找出UTXO值的和nBest最接近nValue的集合vfBest, 看nBest和nLowestLarger(如果存在)谁更接近nValue, 则选择谁为相应的目标UTXO集, 并返回成功
代码如下:
bool CWallet::SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, vector<COutput> vCoins,
set<pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64& nValueRet) const
{
setCoinsRet.clear();
nValueRet = 0;
// List of values less than target
pair<int64, pair<const CWalletTx*,unsigned int> > coinLowestLarger;
coinLowestLarger.first = std::numeric_limits<int64>::max();
coinLowestLarger.second.first = NULL;
vector<pair<int64, pair<const CWalletTx*,unsigned int> > > vValue;
int64 nTotalLower = 0;
random_shuffle(vCoins.begin(), vCoins.end(), GetRandInt);
BOOST_FOREACH(COutput output, vCoins)
{
const CWalletTx *pcoin = output.tx;
if (output.nDepth < (pcoin->IsFromMe() ? nConfMine : nConfTheirs))
continue;
int i = output.i;
int64 n = pcoin->vout[i].nValue;
pair<int64,pair<const CWalletTx*,unsigned int> > coin = make_pair(n,make_pair(pcoin, i));
if (n == nTargetValue)
{
setCoinsRet.insert(coin.second);
nValueRet += coin.first;
return true;
}
else if (n < nTargetValue + CENT)
{
vValue.push_back(coin);
nTotalLower += n;
}
else if (n < coinLowestLarger.first)
{
coinLowestLarger = coin;
}
}
if (nTotalLower == nTargetValue)
{
for (unsigned int i = 0; i < vValue.size(); ++i)
{
setCoinsRet.insert(vValue[i].second);
nValueRet += vValue[i].first;
}
return true;
}
if (nTotalLower < nTargetValue)
{
if (coinLowestLarger.second.first == NULL)
return false;
setCoinsRet.insert(coinLowestLarger.second);
nValueRet += coinLowestLarger.first;
return true;
}
// Solve subset sum by stochastic approximation
sort(vValue.rbegin(), vValue.rend(), CompareValueOnly());
vector<char> vfBest;
int64 nBest;
ApproximateBestSubset(vValue, nTotalLower, nTargetValue, vfBest, nBest, 1000);
if (nBest != nTargetValue && nTotalLower >= nTargetValue + CENT)
ApproximateBestSubset(vValue, nTotalLower, nTargetValue + CENT, vfBest, nBest, 1000);
// If we have a bigger coin and (either the stochastic approximation didn't find a good solution,
// or the next bigger coin is closer), return the bigger coin
if (coinLowestLarger.second.first &&
((nBest != nTargetValue && nBest < nTargetValue + CENT) || coinLowestLarger.first <= nBest))
{
setCoinsRet.insert(coinLowestLarger.second);
nValueRet += coinLowestLarger.first;
}
else {
for (unsigned int i = 0; i < vValue.size(); i++)
if (vfBest[i])
{
setCoinsRet.insert(vValue[i].second);
nValueRet += vValue[i].first;
}
//// debug print
printf("SelectCoins() best subset: ");
for (unsigned 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;
}
0x05 总结
从上面分析来看, 交易的流程主要就是创建交易输出vOut, 以及寻找到交易输入vIn, 然后配置上gas, 广播到网络节点中去。比特币的交易模块是整个区块链技术体系中比较重要的部分, 也是比较晦涩难懂的部分, 值得花上一些时间细细品读。由于本人水平有限, 文中难免有理解不透的地方, 还请大家多多指教!
网友Black在我学习的过程中给出了大量的指点和帮助, 特此感谢!
参考:
原文引自: https://bbs.pediy.com/thread-247491.htm
作者: Hefe