-
-
[原创]比特币源码研读--交易细节
-
发表于: 2018-10-26 15:30 8811
-
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,判断发送目标地址和金额是否合法,没啥好说的
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; }
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); }
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; }
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;
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();
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; }
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在我学习的过程中给出了大量的指点和帮助,特此感谢!
参考:
1,http://www.btc38.com/btc/btc_learning/220.html
2,https://zhuanlan.zhihu.com/p/43203734
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
赞赏
- [原创]比特币源码研读--交易细节 8812
- [原创]比特币源码研读---交易 5174
- [原创]记一次linux(被)入侵,服务器变矿机~ 17151