美文网首页
比特币源码研读--交易细节

比特币源码研读--交易细节

作者: Hefe | 来源:发表于2018-10-26 15:12 被阅读0次

    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在我学习的过程中给出了大量的指点和帮助,特此感谢!

    本文由区块链研习社源码研读班@Hefe原创,转载请注明出处!

    参考:

    1,http://www.btc38.com/btc/btc_learning/220.html
    2,https://zhuanlan.zhihu.com/p/43203734

    相关文章

      网友评论

          本文标题:比特币源码研读--交易细节

          本文链接:https://www.haomeiwen.com/subject/ijzktqtx.html