比特币交易源代码分析

关于交易部分可以先阅读《精通比特币》第五章
本文内容参考自https://blog.csdn.net/g2com/article/details/64386251
对于初次分析比特币源代码,建议先阅读最原始版本的比特币源代码original-bitcoin。此版本源代码比较简单,可以帮助快速理解比特币各个阶段的工作流程及原理。

1.SendMoney()

当比特币客户端向某个地址发送比特币时,便会调用该函数。函数位于'src/main.cpp'第2625行。

bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew)
{
    CRITICAL_BLOCK(cs_main)
    {
        int64 nFeeRequired;
        if (!CreateTransaction(scriptPubKey, nValue, wtxNew, nFeeRequired))
        {
            strig strError;
            if (nValue + nFeeRequired > GetBalance())
                strError = strprintf("Error: This is an oversized transaction that requires a transaction fee of %s ", FormatMoney(nFeeRequired).c_str());
            else
                strError = "Error: Transaction creation failed ";
            wxMessageBox(strError, "Sending...");
            return error("SendMoney() : %s\n", strError.c_str());
        }
        if (!CommitTransactionSpent(wtxNew))
        {
            wxMessageBox("Error finalizing transaction", "Sending...");
            return error("SendMoney() : Error finalizing transaction");
        }

        printf("SendMoney: %s\n", wtxNew.GetHash().ToString().substr(0,6).c_str());

        // Broadcast
        if (!wtxNew.AcceptTransaction())
        {
            // This must not fail. The transaction has already been signed and recorded.
            throw runtime_error("SendMoney() : wtxNew.AcceptTransaction() failed\n");
            wxMessageBox("Error: Transaction not valid", "Sending...");
            return error("SendMoney() : Error: Transaction not valid");
        }
        wtxNew.RelayWalletTransaction();
    }
    MainFrameRepaint();
    return true;
}

该方法包含三个参数:

  • scriptPubKey为收款人公钥锁定脚本,关于锁定脚本和解锁脚本将会在下章做分析。
  • nValue表示将要转账的金额。该金额并未包含交易费nTrasactionFee。
  • wtxNew是一个CWalletTx类的本地变量。该变量目前的值为空,之后会包含若干CMerkleTX类对象。该类由CTransaction衍生而来,并且添加了若干方法。我们暂时先不管具体细节,仅将其看作CTransaction类。

SendMoney()首先调用了CreateTransaction()函数,这个函数作用便是构造一笔新的交易,也是本文重点分析的函数。该函数源代码如下:

bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, int64& nFeeRequiredRet)
{
    nFeeRequiredRet = 0;
    CRITICAL_BLOCK(cs_main)
    {
        // txdb must be opened before the mapWallet lock
        CTxDB txdb("r");
        CRITICAL_BLOCK(cs_mapWallet)
        {
            int64 nFee = nTransactionFee;
            loop
            {
                wtxNew.vin.clear();
                wtxNew.vout.clear();
                if (nValue < 0)
                    return false;
                int64 nValueOut = nValue;
                nValue += nFee;

                // Choose coins to use
                set setCoins;
                if (!SelectCoins(nValue, setCoins))
                    return false;
                int64 nValueIn = 0;
                foreach(CWalletTx* pcoin, setCoins)
                    nValueIn += pcoin->GetCredit();

                // Fill vout[0] to the payee
                wtxNew.vout.push_back(CTxOut(nValueOut, scriptPubKey));

                // Fill vout[1] back to self with any change
                if (nValueIn > nValue)
                {
                    // Use the same key as one of the coins
                    vector vchPubKey;
                    CTransaction& txFirst = *(*setCoins.begin());
                    foreach(const CTxOut& txout, txFirst.vout)
                        if (txout.IsMine())
                            if (ExtractPubKey(txout.scriptPubKey, true, vchPubKey))
                                break;
                    if (vchPubKey.empty())
                        return false;

                    // Fill vout[1] to ourself
                    CScript scriptPubKey;
                    scriptPubKey << vchPubKey << OP_CHECKSIG;
                    wtxNew.vout.push_back(CTxOut(nValueIn - nValue, scriptPubKey));
                }

                // Fill vin
                foreach(CWalletTx* pcoin, setCoins)
                    for (int nOut = 0; nOut < pcoin->vout.size(); nOut++)
                        if (pcoin->vout[nOut].IsMine())
                            wtxNew.vin.push_back(CTxIn(pcoin->GetHash(), nOut));

                // Sign
                int nIn = 0;
                foreach(CWalletTx* pcoin, setCoins)
                    for (int nOut = 0; nOut < pcoin->vout.size(); nOut++)
                        if (pcoin->vout[nOut].IsMine())
                            SignSignature(*pcoin, wtxNew, nIn++);

                // Check that enough fee is included
                if (nFee < wtxNew.GetMinFee(true))
                {
                    nFee = nFeeRequiredRet = wtxNew.GetMinFee(true);
                    continue;
                }

                // Fill vtxPrev by copying from previous transactions vtxPrev
                wtxNew.AddSupportingTransactions(txdb);
                wtxNew.fTimeReceivedIsTxTime = true;

                break;
            }
        }
    }
    return true;
}

调用该方法时,它所需要的四个参数如下:

  • scriptPubKey即脚本代码
  • nValue是将要转账的数额,交易费nTransactionFee并未包括在内。
  • wtxNew是一个新的Tx实例。
  • nFeeRequiredRet是一笔用来支付交易费的输出交易,在该方法执行完成之后获得。

函数首先对实例wtxNew初始化,随后计算总共费用nValue=转账金额+交易费,调用 SelectCoin()寻找合适的交易输入。

实际上,并不存在储存比特币地址或账户余额的地点,只有被所有者锁住的、分散的UTXO。“一个用户的比特币余额”,这个概念是一个通过比特币钱包应用创建的派生之物。比特币钱包通过扫描区块链并聚合所有属于该用户的UTXO来计算该用户的余额。

bool SelectCoins(int64 nTargetValue, set& setCoinsRet)
{
    setCoinsRet.clear();

    // List of values less than target
    int64 nLowestLarger = _I64_MAX;
    CWalletTx* pcoinLowestLarger = NULL;
    vector > vValue;
    int64 nTotalLower = 0;
    ...
}

我们知道比特币是基于UTXO模型的,所以SelectCoin便负责从所有属于该用户的UTXO中找到一组符合转账金额的输入。具体的寻找算法此处便不具体分析。
在得到一组输入之后会计算所有输入的总金额nValueIn,一般输入总金额是大于转账金额的,所以后面会构造一笔转给自己地址的输出,用于找零。
随后调用wtxNew.vout.push_back(CTxOut(nValueOut, scriptPubKey))构造第一笔输出,指向该笔交易的转账地址。关于CTxIn和CTxOut的数据结构可以参考https://blog.csdn.net/pure_lady/article/details/77771392

如果需要找零(nValueIn > nValue),添加另一笔输出交易至wtxNew并将零钱发回本人。该过程包含以下步骤:
从setCoin当中获取第一笔交易txFirst,依次检查txFirst.vout中的输出是否属于本人。如果是则从该笔输出交易当中提取出公钥ExtractPubKey,并放入本地变量vchPubKey
将vchPubKey放入脚本vchPubKey OP_CHECKSIG,并使用这段脚本代码为wtxNew添加一个支付给本人的输出交易。
因为setCoins包含支付给本人的交易,所以每笔交易一定包括至少一笔支付给本人的交易。从第一笔交易txFirst中即可找到。

至此,wtxNew的输出交易容器vout已准备就绪。现在,该设置输入交易容器vin。记住每一个输入交易列表vin均引用一笔来源交易,而且wtxNew的每笔来源交易均可在setCoins中被找到。对于每一笔setCoins中的交易pcoin,逐个遍历其输出交易pcoin->vout[nOut]。如果第nOut笔输出支付给本人(意味着wtxNew从该笔输出交易中获得币),则向wtxNew添加一笔新的输入交易(wtxNew.vin(wtxNew.vin.push_back(CTxIn(pcoin->GetHash(), nOut)),第51行)。该输入交易指向pcoin中的第nOut笔输出交易,由此将wtxNew.vin与pcoin的第nOut笔输出相连接。
对于setCoins当中的每笔交易pcoin,逐个遍历其所有输出交易pcoin->vout[nOut]。如果该笔交易属于本人,调用SignSignature(*pcoin,wtxNew, nIn++)为第nIn笔输入交易添加签名。注意nIn为wtxNew的输入交易位置。

对于交易签名函数SignSignature,以下为源代码:

bool SignSignature(const CTransaction& txFrom, CTransaction& txTo, unsigned int nIn, int nHashType, CScript scriptPrereq)
{
    assert(nIn < txTo.vin.size());
    CTxIn& txin = txTo.vin[nIn];
    assert(txin.prevout.n < txFrom.vout.size());
    const CTxOut& txout = txFrom.vout[txin.prevout.n];

    // Leave out the signature from the hash, since a signature can't sign itself.
    // The checksig op will also drop the signatures from its hash.
    uint256 hash = SignatureHash(scriptPrereq + txout.scriptPubKey, txTo, nIn, nHashType);

    if (!Solver(txout.scriptPubKey, hash, nHashType, txin.scriptSig))
        return false;

    txin.scriptSig = scriptPrereq + txin.scriptSig;

    // Test solution
    if (scriptPrereq.empty())
        if (!EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn))
            return false;

    return true;
}

首先需要注意的是,该函数有5个参数,而CreateTransaction()只有3个。这是因为在script.h文件里,后两个参数已默认给出。

SignSignature(*pcoin, wtxNew, nIn++)

  • txFrom是一个*pcoin对象。即我们前面找到的setCoins中的每一个。
  • txTo是CreateTransaction()里的wtxNew对象。它是将要花费来源交易txFrom的新交易。新交易需要被签署方可生效。
  • nIn是指向txTo中输入交易列表的索引位置。该输入交易列表包含一个对txFrom的输出交易列表的引用。更准确地讲,txin=txTo.vin[nIn](第4行)是txTo中的输入交易;txout=txFrom.vout[txin.prev.out.n](第6行)是txin所指向的txFrom中的输出交易。

以下是SignSignature()所做的工作:

  • 根据索引位置找到对应的输入输出交易。
  • 调用SignatureHash()方法生成txTo的哈希值。
  • 调用Solver()函数签署刚才生成的哈希。
  • 调用EvalScript()来运行一小段脚本并检查签名是否合法。

下面分别介绍这几个函数:

SignatureHash()

uint256 SignatureHash(CScript scriptCode, const CTransaction& txTo, unsigned int nIn, int nHashType)
{
    if (nIn >= txTo.vin.size())
    {
        printf("ERROR: SignatureHash() : nIn=%d out of range\n", nIn);
        return 1;
    }
    CTransaction txTmp(txTo);

    // In case concatenating two scripts ends up with two codeseparators,
    // or an extra one at the end, this prevents all those possible incompatibilities.
    scriptCode.FindAndDelete(CScript(OP_CODESEPARATOR));

    // Blank out other inputs' signatures
    for (int i = 0; i < txTmp.vin.size(); i++)
        txTmp.vin[i].scriptSig = CScript();
    txTmp.vin[nIn].scriptSig = scriptCode;

    // Blank out some of the outputs
    if ((nHashType & 0x1f) == SIGHASH_NONE)
    {
        // Wildcard payee
        txTmp.vout.clear();

        // Let the others update at will
        for (int i = 0; i < txTmp.vin.size(); i++)
            if (i != nIn)
                txTmp.vin[i].nSequence = 0;
    }
    else if ((nHashType & 0x1f) == SIGHASH_SINGLE)
    {
        // Only lockin the txout payee at same index as txin
        unsigned int nOut = nIn;
        if (nOut >= txTmp.vout.size())
        {
            printf("ERROR: SignatureHash() : nOut=%d out of range\n", nOut);
            return 1;
        }
        txTmp.vout.resize(nOut+1);
        for (int i = 0; i < nOut; i++)
            txTmp.vout[i].SetNull();

        // Let the others update at will
        for (int i = 0; i < txTmp.vin.size(); i++)
            if (i != nIn)
                txTmp.vin[i].nSequence = 0;
    }

    // Blank out other inputs completely, not recommended for open transactions
    if (nHashType & SIGHASH_ANYONECANPAY)
    {
        txTmp.vin[0] = txTmp.vin[nIn];
        txTmp.vin.resize(1);
    }

    // Serialize and hash
    CDataStream ss(SER_GETHASH);
    ss.reserve(10000);
    ss << txTmp << nHashType;
    return Hash(ss.begin(), ss.end());
}

SignatureHash(scriptPrereq + txout.scriptPubKey, txTo, nIn, nHashType);

以下是该函数所需要的参数:

  • txTo是将要被签署的交易。它同时也是CreateTransaction()中的wtxNew对象。它的输入交易列表中的第nIn项,txTo.vin[nIn],是该函数将要起作用的目标。
  • scriptCode是scriptPrereq + txout.scriptPubKey,其中txout是SignSignature()中定义的来源交易txFrom()的输出交易。由于此时scriptPrereq为空,scriptCode事实上是来源交易txFrom中的输出交易列表当中被txTo作为输入交易引用的那笔的脚本代码。txout.scriptPubKey有可能包含两类脚本:
    脚本A:OP_DUP OP_HASH160 <你地址的160位哈希> OP_EQUALVERIFY OP_CECKSIG。该脚本将来源交易txFrom中的币发送给你,其中<你地址的160位哈希>是你的比特币地址。
    脚本B:<你的公钥> OP_CHECKSIG。该脚本将剩余的币退还至来源交易txFrom的发起人。由于你创建的新交易txTo/wtxNew将会花费来自txFrom的币,你必须同时也是txFrom的创建者。换句话讲,当你在创建txFrom的时候,你其实是在花费之前别人发送给你的币。因此,<你的公钥>即是txFrom创建者的公钥,也是你自己的公钥。

在了解了输入交易之后,我们来一起了解SignatureHash()是怎样工作的。

SignatureHash()首先将txTO拷贝至txTmp,接着清空txTmp.vin中每一笔输入交易的scriptSig,除了txTmp.vin[nIn]之外,该输入交易的scriptSig被设为scriptCode(第14、15行)。

接着,该函数检验nHashType的值。根据不同的nHAshType选择不同的置空操作。

  • SIGHASH_ALL是默认选项,具体流程是把所有的TxOut都纳入临时Tx中用来生成被签署的交易,相当于针对这个TxIn,这个交易中的所有的TxOut都已经被这个TxIn承认,不可改
  • SIGHASH_NONE,具体流程是把所有的TxOut都置空,相当于针对这个TxIn,不关心这个交易的TxOut是什么情况,即使被替换了也是可以的
  • SIGHASH_SINGLE,具体流程是只保留和自已同样index的out,其他的out都置空,表示只关心和自己同样index的out,其他的out不关心。比如当前的txin是这个交易的第3个in(index=2),那么这个交易的第3个out保留,其他的out都置空。
  • SIGHASH_ANYONECANPAY比较特殊,他是独立的。可以和另3个标志取并集。它表示签署这个TxIn的时候我连其他的TxIn都不关心,可以和前面3个并存。

在最后4行代码中,txTmp和nHashType变成序列化后的类型CDataStream对象。该类型包括一个装有数据的字符容器类型。所返回的哈希值是Hash()方法在计算序列化后的数据所得到的。

到这里我们便生成了txOut的哈希值hash,接下来会调用Solver()函数签署刚才生成hash。

Solver()

Solver(txout.scriptPubKey, hash, nHashType, txin.scriptSig)
其源代码如下:

bool Solver(const CScript& scriptPubKey, uint256 hash, int nHashType, CScript& scriptSigRet)
{
    scriptSigRet.clear();

    vector > vSolution;
    if (!Solver(scriptPubKey, vSolution))
        return false;

    // Compile solution
    CRITICAL_BLOCK(cs_mapKeys)
    {
        foreach(PAIRTYPE(opcodetype, valtype)& item, vSolution)
        {
            if (item.first == OP_PUBKEY)
            {
                // Sign
                const valtype& vchPubKey = item.second;
                if (!mapKeys.count(vchPubKey))
                    return false;
                if (hash != 0)
                {
                    vector vchSig;
                    if (!CKey::Sign(mapKeys[vchPubKey], hash, vchSig))
                        return false;
                    vchSig.push_back((unsigned char)nHashType);
                    scriptSigRet << vchSig;
                }
            }
            else if (item.first == OP_PUBKEYHASH)
            {
                // Sign and give pubkey
                map::iterator mi = mapPubKeys.find(uint160(item.second));
                if (mi == mapPubKeys.end())
                    return false;
                const vector& vchPubKey = (*mi).second;
                if (!mapKeys.count(vchPubKey))
                    return false;
                if (hash != 0)
                {
                    vector vchSig;
                    if (!CKey::Sign(mapKeys[vchPubKey], hash, vchSig))
                        return false;
                    vchSig.push_back((unsigned char)nHashType);
                    scriptSigRet << vchSig << vchPubKey;
                }
            }
        }
    }

    return true;
}

以下是该方法所需要的4个参数:

  • 位于第10行的调用函数SignSignature()将txOut.scriptPubKey,来源交易txFrom的输出脚本,作为输入值传入第一个参数scriptPubKey。记住它可能包含脚本A或者脚本B。
  • 第二个参数hash是由SignatureHash()生成的哈希值。
  • 第三个参数nHashType的值默为SIGHASH_ALL。其余三种值见上一个函数的解释。
  • 第四个参数是该函数的返回值,即调用函数SignSIgnature()中位于第12行的txin.scriptSig。记住txin是新生成的交易wtxNew(在调用函数SignSignature()中作为txTo引用)位于第nIn的输入交易。因此,wtxNew第nIn笔输入交易的scriptSig将存放该函数返回的签名。

该函数首先将scriptSigRet清空,随后调用Solver(scriptPubKey, vSolution),此Solver函数有两个输入。其源代码为:

bool Solver(const CScript& scriptPubKey, vector >& vSolutionRet)
{
    // Templates
    static vector vTemplates;
    if (vTemplates.empty())
    {
        // Standard tx, sender provides pubkey, receiver adds signature
        vTemplates.push_back(CScript() << OP_PUBKEY << OP_CHECKSIG);

        // Short account number tx, sender provides hash of pubkey, receiver provides signature and pubkey
        vTemplates.push_back(CScript() << OP_DUP << OP_HASH160 << OP_PUBKEYHASH << OP_EQUALVERIFY << OP_CHECKSIG);
    }

    // Scan templates
    const CScript& script1 = scriptPubKey;
    foreach(const CScript& script2, vTemplates)
    {
        vSolutionRet.clear();
        opcodetype opcode1, opcode2;
        vector vch1, vch2;

        // Compare
        CScript::const_iterator pc1 = script1.begin();
        CScript::const_iterator pc2 = script2.begin();
        loop
        {
            bool f1 = script1.GetOp(pc1, opcode1, vch1);
            bool f2 = script2.GetOp(pc2, opcode2, vch2);
            if (!f1 && !f2)
            {
                // Success
                reverse(vSolutionRet.begin(), vSolutionRet.end());
                return true;
            }
            else if (f1 != f2)
            {
                break;
            }
            else if (opcode2 == OP_PUBKEY)
            {
                if (vch1.size() <= sizeof(uint256))
                    break;
                vSolutionRet.push_back(make_pair(opcode2, vch1));
            }
            else if (opcode2 == OP_PUBKEYHASH)
            {
                if (vch1.size() != sizeof(uint160))
                    break;
                vSolutionRet.push_back(make_pair(opcode2, vch1));
            }
            else if (opcode1 != opcode2)
            {
                break;
            }
        }
    }

    vSolutionRet.clear();
    return false;
}

该函数的作用是将scriptPubKey与两个模板相比较:

如果输入脚本为脚本A,则将模板A中的OP_PUBKEYHASH与脚本A中的<你的地址160位哈希>配对,并将该对放入vSolutionRet。
如果输入脚本为脚本B,则从模板B中提取运算符OP_PUBKEY,和从脚本B中提取运算元<你的公钥>,将二者配对并放入vSolutionRet。
如果输入脚本与两个模板均不匹配,则返回false。

回到有4个参数的Solver()并继续对该函数的分析。现在我们清楚了该函数的工作原理。它会在两个分支中选择一个执行,取决于从vSolutionRet得到的对来自脚本A还是脚本B。如果来自脚本A,item.first == OP_PUBKEYHASH;如果来自脚本B,item.first == OP_PUBKEY。

  • item.first == OP_PUBKEY(脚本B)。在该情形下,item.second包含<你的公钥>。全局变量mapKeys将你的全部公钥映射至与之对应的私钥。如果mapKeys当中没有该公钥,则报错(第16行)。否则,用从mapKeys中提取出的私钥签署新生成的交易wtxNew的哈希值,其中哈希值作为第2个被传入的参数(CKey::Sign(mapKeys[vchPubKey], hash, vchSig),第23行),再将结果放入vchSig,接着将其序列化成scriptSigRet(scriptSigRet << vchSig,第24行)并返回。
  • item.first == OP_PUBKEYHASH(脚本A)。在该情形下,item.second包含<你的地址160位哈希>。该比特币地址将被用于从位于第23行的全局映射mapPubKeys中找到其所对应的公钥。全局映射mapPubKeys将你的地址与生成它们的公钥建立一一对应关系(查看函数AddKey())。接着,通过该公钥从mapKeys中找到所对应的私钥,并用该私钥签署第二个参数hash。签名和公钥将一同被序列化至scriptSigRet并返回(scriptSig << vchSig << vchPubkey,第24行)

EvalScript()

最后将调用EvalScript()来运行一小段脚本并检查签名是否合法。
EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn)
其源代码如下:

bool EvalScript(const CScript& script, const CTransaction& txTo, unsigned int nIn, int nHashType,
                vector >* pvStackRet)
{
    CAutoBN_CTX pctx;
    CScript::const_iterator pc = script.begin();
    CScript::const_iterator pend = script.end();
    CScript::const_iterator pbegincodehash = script.begin();
    vector vfExec;
    vector stack;
    vector altstack;
    if (pvStackRet)
        pvStackRet->clear();


    while (pc < pend)
    {
        bool fExec = !count(vfExec.begin(), vfExec.end(), false);
    ...
    }
    if (pvStackRet)
        *pvStackRet = stack;
    return (stack.empty() ? false : CastToBool(stack.back()));
}

EvalScript()带有3个参数,分别为:

  • 第一个参数为txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey。它有可能是:
    验证情形A:<你的签名_vchSig> <你的公钥_vchPubKey> OP_CODESEPARATOR OP_DUP OP_HASH160 <你的地址160位哈希> OP_EQUALVERIFY OP_CHECKSIG,即签名A + OP_CODESEPARATOR + 脚本A。
    验证情形B:<你的签名_vchSig> OP_CODESEPARATOR <你的公钥_vchPubKey> OP_CHECKSIG,即签名B + OP_CODESEPARATOR + 脚本B。
  • 第二个参数为新创建的交易txTo,即CreateTransaction()中的wtxNew。
  • 第三个参数为nIn,即将被验证的交易在txTo输入交易列表中的位置。

该函数将根据你输入的脚本,依次取出脚本中的操作代码进行相应操作,并对最后的模拟执行结果做判断,返回执行结果。如果结果为true,则完成了SignSignature(),此时便生成了一笔新的交易。

回到SendMoney()

生成了一笔新的交易后,利用函数CommitTransactionSpent(wtxNet)尝试将这笔交易提交至数据库,之后判断交易是否提交成功,如果该笔交易提交成功wtxNew.AcceptTransaction()=true,将这笔交易广播至其他peer节点wtxNew.RelayWalletTransaction()

当矿工收到这笔交易的广播之后会对交易进行相应操作,之后的章节我们将对新区块的处理部分做详细分析。

总结

比特币生成一笔新的交易大致分为如下几个阶段:

  • 根据转账金额以及交易费用从UTXO中寻找一组满足条件的输入。
  • 对于这组输入以及转账地址构造一个新的交易:输入分别对应UTXO中的不同输出,第一个输出指向转账地址,如果有找零,则计算找零金额,将其放入第二个输出,同时指向自身的钱包地址。
  • 对构造好的输入输出分别进行签名。
  • 利用Solver函数对签名完的交易做模拟运行,如果运行通过则生成完毕。
  • 将交易提交至数据库并广播到其他节点。

你可能感兴趣的:(比特币交易源代码分析)