Iawen's Blog

我喜欢这样自由的随手涂鸦,因为我喜欢风......

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