请选择 进入手机版 | 继续访问电脑版
开启辅助访问
QQ登录|微信登录|登录 |立即注册

盖茨网区块链技术社区

盖茨网区块链技术社区 首页 前沿 查看内容

比特币源码学习笔记

263 0
admin | 2018-3-22 15:17
摘要: 前言从事区块链的开发,不了解其底层核心技术是不够的。许多人在看了比特币白皮书之后仍然不清楚比特币是怎样实现的,因为比特币的源码设计精巧,有许多设计白皮书未曾提及,加上本身比特币的文档稀少,加大了新手理 ...

前言

从事区块链的开发,不了解其底层核心技术是不够的。许多人在看了比特币白皮书之后仍然不清楚比特币是怎样实现的,因为比特币的源码设计精巧,有许多设计白皮书未曾提及,加上本身比特币的文档稀少,加大了新手理解的困难程度。尽管现在已经有许多介绍区块链的书和文章,却很少是从源码着手分析的。我通过半年时间对于区块链的学习,开始撰写一份比特币源码的教程。本教程深入浅出,通过分析最经典的区块链——比特币的C++客户端源码,让开发者用最短的时间上手区块链技术。了解比特币源码可帮助开发者更好了解区块链的工作原理并在应用当中根据实际情况做出修改和调整。

本文所引用的源码均来自原始版比特币客户端,即由中本聪发布的第一版源码。该客户端包括大约16000行代码。尽管经过数年的发展,比特币客户端经过了几次较大更新,其数据结构和原理从诞生之日起一直延续至今。本文会尽可能保证文字的严谨准确,表达当中难免会产生疏漏,欢迎指正。

bitcoin-code

 

第一章

 

本章节讲述比特币客户端是怎样生成比特币地址,并创建新的交易。

我们来看一下GenerateNewKey()方法,该方法位于main.cpp。

 

bool AddKey(const CKey& key)
{
    CRITICAL_BLOCK(cs_mapKeys)
    {
        mapKeys[key.GetPubKey()] = key.GetPrivKey();
        mapPubKeys[Hash160(key.GetPubKey())] = key.GetPubKey();
    }
    return CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey());
}

vector GenerateNewKey()
{
    CKey key;
    key.MakeNewKey();
    if (!AddKey(key))
        throw runtime_error("GenerateNewKey() : AddKey failed\n");
    return key.GetPubKey();
}

该方法通过以下步骤生成一个新的公钥对:

 

  • 首先建立一个新的CKey类型对象(第13行)。
  • 调用addKey()方法将新建的key添加至1)全局映射mapKeys (第5行)2)全局map mapPubKeys(第6行)和钱包数据库wallet.dat(第8行)。

mapKeys建立公钥与私钥的一一对应关系。
mapPubKeys建立公钥的hash和公钥本身的对应关系。

  • 返回公钥(第16行)。

该公钥为未压缩的格式,属于OpenSSL标准格式之一。在得到公钥之后,比特币客户端会将该公钥传递至PubKeyToAddress()并调用Hash160ToAddress()方法生成地址。最后返回的Base58编码字符串值便是一个新生成的比特币地址。Base58由1-9和除i,l,0,o之外的英文字符组成。

CTransaction类

CTransaction的定义位于main.h。在比特币当中,所谓币的概念其实是一系列交易Tx的组合。这种方式虽然实现起来更为复杂,却提高了比特币的安全性。用户可以为每一笔交易创建一个新的地址,地址在使用一次之后可以立即作废。因此,CTransaction是比特币客户端最重要的类之一。

 

class CTransaction
{
public:
    int nVersion;
    vector vin;
    vector vout;
    int nLockTime;
    //......
}

 

CTransaction包含两个容器类型:输入交易vin和输出交易vout。每个vin由若干CTxIn对象组成,每个vout则由CTxOut组成。

每笔交易Tx的输入交易(CTxIn类)包含一个COutPoint对象prevout,该对象引用另外一笔交易Tx的输出交易作为来源交易。来源交易使当前交易Tx从另一笔交易当中得到可花费的比特币。一笔交易Tx可以拥有任意笔输入交易。

任何交易均由一个256位uint256哈希作为其唯一识别。若要引用某一笔来源交易TxSource当中某个特定的输出交易,我们需要两种信息:TxSource的哈希,和该输出交易在输出交易当中的位置n。这两种信息构成COutPoint类。一个COutPoint对象指向来源交易的某一笔输出交易TxSource.vout[n]。如果该笔输出交易被另外一笔交易Tx的位置i的输入交易所引用,例如Tx.vin[i].prevout,我们将其称为Tx的第i笔输入交易花费了TxSource中的第n笔输出交易。

uint256和uint160类

这两种类型的定义位于uint.h。一个uint256类包含有一个256位的哈希。它由一个长度为256/32=8的unsigned int数组构成。一个相似的数据结构是uint160,该结构的定义可在同一个文件当中找到。既然SHA-256的长度为256bit,读者不难推断出uint160的作用是存放RIPEMD-160哈希。uint256和uint160均由base_uint类继承而来。

 

class base_uint {
protected:
    enum { WIDTH = BITS / 32 };
    unsigned int pn[WIDTH];

public:
    bool operator!() const
    {
        for (int i = 0; i < WIDTH; i++)
            if (pn[i] != 0)
                return false;
        return true;
    }
    //......
    unsigned int GetSerializeSize(int nType = 0, int nVersion = VERSION) const
    {
        return sizeof(pn);
    }

    template 
    void Serialize(Stream& s, int nType = 0, int nVersion = VERSION) const
    {
        s.write((char*)pn, sizeof(pn));
    }

    template 
    void Unserialize(Stream& s, int nType = 0, int nVersion = VERSION)
    {
        s.read((char*)pn, sizeof(pn));
    }
}

该类重载了若干运算符。此外该类拥有3个序列化成员函数,GetSerializeSize()Serialize()Unserialize()。我们会在后面讲到这三种方法是如何工作的。

 

SendMoney()

该方法位于main.cpp。以下是该方法的源码:

 

bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew)
{
    CRITICAL_BLOCK(cs_main)
    {
        int64 nFeeRequired;
        if (!CreateTransaction(scriptPubKey, nValue, wtxNew, nFeeRequired))
        {
            string 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;
}

 

当用户发送比特币到某一个地址时,比特币客户端会调用SendMoney()方法。该方法包含三个参数:

  1. scriptPubKey包含脚本代码OP_DUP OP_HASH160 <收款人地址160位哈希> OP_EQUALVERIFY OP_CHECKSIG。
  2. nValue表示将要转账的金额。该金额并未包含交易费nTrasactionFee。
  3. wtxNew是一个CWalletTx类的本地变量。该变量目前的值为空,之后会包含若干CMerkleTX类对象。该类由CTransaction衍生而来,并且添加了若干方法。我们暂时先不管具体细节,仅将其看作CTransaction类。

该方法的流程显而易见:

  1. 首先建立一笔新的交易(CreateTransaction(scriptPubKey, nValue, wtxNet, nFeeRequired),第6行)。
  2. 尝试将这笔交易提交至数据库(CommitTransactionSpent(wtxNet),第16行)。
  3. 如果该笔交易提交成功(wtxNew.AcceptTransaction(),第23行),将其广播至其他peer节点(wtxNew.RelayWalletTransaction(),第30行)。

这四个方法都与wtxNew相关。我们在本章介绍了第一个,其余三个将会在后续文章中介绍。

CreateTransaction()

该方法位于main.cpp。以下是该方法的源码:

 

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;
}

 

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

  1. scriptPubKey包含脚本代码OP_DUP OP_HASH160 <收款人地址160位哈希> OP_EQUALVERIFY OP_CHECKSIG。
  2. nValue是将要转账的数额,交易费nTransactionFee并未包括在内。
  3. wtxNew是一个新的Tx实例。
  4. nFeeRequiredRet是一笔用来支付交易费的输出交易,在该方法执行完成之后获得。

该方法的流程如下:

  • 定义一个本地变量nValueOut = nValue来保存将转账的金额(第17行)。将nValue与交易费nFee相加得到新的包含转账费的nValue。
  • 执行位于第21行的SelectCoins(nValue, setCoins)得到一系列币,并放入setCoins。setCoins包含支付给你本人地址的交易,即你所拥有的币。这些交易将成为wtxNew的来源交易。
  • 执行位于第27行的wtxNew.vout.push_back(CTxOut (nValueOut,sciptPubKey))并添加一笔输出交易至wtxNew。该笔输出将支付给<收款人地址160位哈希>(包含在scriptPubKey里面)数量为的币。
  • 如果需要找零(nValueIn > nValue),添加另一笔输出交易至wtxNew并将零钱发回本人。该过程包含以下步骤:
    • 从setCoin当中获取第一笔交易txFirst,依次检查txFirst.vout中的交易是否属于本人。如果是则从该笔输出交易当中提取出公钥,并放入本地变量vchPubKey
    • 将vchPubKey放入脚本vchPubKey OP_CHECKSIG,并使用这段脚本代码为wtxNew添加一个支付给本人的输出交易(第45行)。
    • 因为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的输入交易位置。
  • 如果交易费nFee小于wtxNet.GetMinFee(true),将nFee设为后者,清空wtxNew中的所有数据并重新开始整个过程。在位于第11行的第一次迭代当中,nFee是全局变量nTransactionFee = 0的本地复制。
  • 如果你不明白为什么要如此费力地重新添满wtxNew,源码中的GetMinFee()提供了答案:交易的最低费用与交易的数据大小有关。wtxNew的大小只有在完整构建之后才可得知。如果wtxNew.GetMinFee(true)计算得到的最小交易费用大于之前创造wtxNew时假设的交易费nFee,则除了重新构建wtxNew之外别无他法。
  • 这里遇到了一个先有鸡还是先有蛋的局面:若想创建一笔新的交易,则必须知道交易费用是多少。而交易费只有在整个交易被创建以后才可得知。为了打破这个循环,本地变量nFee被用来放置预计的交易费用,并且新的交易构建在此基础上。在构建完成之后,得到真实的交易费并与预估的交易费作比较。如果预估的交易费小于真实的交易费,则替换成真实交易费并重新构造整个交易。

这里是GetMinFee()的源码:

 

int64 GetMinFee(bool fDiscount=false) const
    {
        unsigned int nBytes = ::GetSerializeSize(*this, SER_NETWORK);
        if (fDiscount && nBytes < 10000)
            return 0;
        return (1 + (int64)nBytes / 1000) * CENT;
    }

 

  • 如果计算得到的交易费比之前预计的交易费更高,则跳出第11行开始的循环并返回整个函数(第67行)。在此之前,需要进行以下两个步骤:
    • 执行wtxNew.AddSupportingTransactions(txdb)。这一部分以后会进行更详细介绍。
    • 设置wtxNet.fTimeReceivedIsTxTime=true(第66行)。

现在来看一下如何通过SignSignature()签署新生成的交易wtxNew。

SignSignature()

该方法位于script.cpp。以下是该方法的源码:

 

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文件里,后两个参数已默认给出。

以下是传递给CreateTransaction()中的3个参数:

  1. txFrom是一个*pcoin对象。它是CreateTransaction()里setCoins中的所有币中的某一个。它同时也是一笔来源交易。它的若干输出交易当中包含了新交易将要花费的币。
  2. txTo是CreateTransaction()里的wtxNew对象。它是将要花费来源交易txFrom的新交易。新交易需要被签署方可生效。
  3. nIn是指向txTo中输入交易列表的索引位置。该输入交易列表包含一个对txFrom的输出交易列表的引用。更准确地讲,txin=txTo.vin[nIn](第4行)是txTo中的输入交易;txout=txFrom.vout[txin.prev.out.n](第6行)是txin所指向的txFrom中的输出交易。

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

  1. 调用SignatureHash()方法生成txTo的哈希值。
  2. 调用Solver()函数签署刚才生成的哈希。
  3. 调用EvalScript()来运行一小段脚本并检查签名是否合法。

我们一起看一下这三个函数。

SignatureHash()

该方法位于script.cpp。以下是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());
}

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

 

  • 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创建者的公钥,也是你自己的公钥。

我们在此停留片刻,来思考一下脚本A和脚本B。你有可能会问,这些脚本是从哪来的。中本聪在创造比特币的时候为比特币添加了一套脚本语言系统,所以比特币中的交易都是由脚本代码完成的。该脚本系统其实也是后来智能合约的雏形。脚本A来自第29行,位于方法CSendDialog::OnButtonSend(),脚本B则来自第44行,位于方法CreateTransaction()。

  1. 当用户发起一笔交易时,比特币客户端会调用CSendDialog::OnButtonSend()方法并将脚本A添加至txFrom中的一笔输出交易中。由于该输出交易的收款方为你本人,从而脚本中的<收款人地址160位哈希>,就是<你的地址160位哈希>。
  2. 如果txFrom是你本人创建的,则脚本B会被添加至CreateTransaction()中txFrom的某一笔输出交易。在这里,第44行位于CreateTransaction()中的公钥vchPubKey是你本人的公钥。

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

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

接着,该函数检验nHashType的值。该函数的调用者将一个枚举值传递至该函数nHashType = SIGHASH_ALL。

 

enum
{
    SIGHASH_ALL = 1,
    SIGHASH_NONE = 2,
    SIGHASH_SINGLE = 3,
    SIGHASH_ANYONECANPAY = 0x80,
};

由于nHashType = SIGHASH_ALL,所有的if-else条件均不成立,该函数将直接执行最后4行代码。

 

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

一笔交易可以包含多笔输入交易。SignatureHash()取其中一笔作为目标。它通过以下步骤生成哈希:

  1. 清空除了目标交易之外的所有输入交易。
  2. 复制来源交易中被目标交易作为输入交易引用的那笔输出交易的脚本至目标交易的输入交易列表中。
  3. 为修改后的交易生成哈希值。

Hash()

该方法位于util.h。以下是生成哈希值的方法Hash()的源码:

 

template
inline uint256 Hash(const T1 pbegin, const T1 pend)
{
    uint256 hash1;
    SHA256((unsigned char*)&pbegin[0], (pend - pbegin) * sizeof(pbegin[0]), (unsigned char*)&hash1);
    uint256 hash2;
    SHA256((unsigned char*)&hash1, sizeof(hash1), (unsigned char*)&hash2);
    return hash2;
}

该函数对目标数据执行两次SHA256()方法并返回结果。SHA256()的声明可在openssl/sha.h中找到。

 

Solver()

该方法位于script.cpp。Solver()在SignSignature()中紧接着SignatureHash()被执行。它是真正用来为SignatureHash()返回的哈希值生成签名的函数。

 


鲜花

握手

雷人

路过

鸡蛋