比特币全节点钱包发起交易流程分析

我们都知道,比特币钱包其实是私钥的钱包,这个比较好理解。那么钱包中除了私钥还保存了什么? 从钱包中发出一笔交易要经过哪些流程?今天就来讨论这些问题。

注1:本文的分析以中本聪的代码v0.1.15版本为蓝本,节点指代为全节点。

注2:如果对比特币交易的一些基本概念还不清楚,请先阅读MasteringBitcoin这本书的相应章节,此书中文版叫做《精通比特币》。

注3:文中提到的关键步骤会贴出相应源码,非关键部分请参考流程图自行去看源码

1. 钱包文件wallet.dat

比特币钱包的对应文件是wallet.dat, 里面除了保存公钥、私钥还保存和此节点有关的交易以及一些其他信息比如地址簿、默认手续费设定值等。这些信息都是通过Berkley DB数据库以键值对的形式存储的。存储形式大概像下面这样。

  • 交易 key:pair<"tx", hash值> value:交易引用
  • 公钥私钥对 key : pair<"key", 公钥> value : 私钥
  • 默认公钥 key: pair<"defaultkey"> value: 默认公钥

:很多研究过比特币的人都知道UTXO集,我们已经分析过几个存储比特币数据的文件,包括blkindex.dat、blkxxxx.dat(x代表十进制数字)和wallet.dat,这些文件并没有储存UTXO集。那么比特币的UTXO集到底保存在哪里? 实际上在0.8版本才使用了文件专门存储这些数据,这个文件是chainstate.dat

2. 重要数据结构

先介绍比较重要的类

CWalletTx是钱包中存储交易的类型,继承自CMerkleTx,CMerkleTx继承自CTransaction, 可以看到钱包交易类型包含了一些钱包使用者才关心的信息。

class CWalletTx : public CMerkleTx
{
public:
    vector<CMerkleTx> vtxPrev;     //前向交易集合
    map<string, string> mapValue;               
    vector<pair<string, string> > vOrderForm;
    unsigned int fTimeReceivedIsTxTime;
    unsigned int nTimeReceived;  // time received by this node
    char fFromMe;
    char fSpent;                   //是否已被花费
    //// probably need to sign the order info so know it came from payer

    // memory only
    mutable unsigned int nTimeDisplayed;
    /*成员函数略*/
}

class CMerkleTx : public CTransaction
{
public:
    uint256 hashBlock;                  //这个交易所在block的hash值
    vector<uint256> vMerkleBranch;      //这个交易在块中的merkle路径
    int nIndex;                         //这个交易在块中序列的索引

    // memory only
    mutable bool fMerkleVerified;
    /*成员函数略*/
}

对于钱包中的交易可以这样理解,钱包只保存发给自己的交易(自己主动发的交易也是发给自己的交易,因为找零),钱包中的交易一共有三个来源:

  • 验证单笔交易通过后,如果是给自己的,加入钱包
  • 验证新区块通过后,块中交易是给自己的部分,加入钱包
  • 自己主动发的交易,构造后加入钱包

下面介绍比较重要的全局变量

map<uint256, CWalletTx> mapWallet;                  //key:交易hash  //value: 交易对象
map<vector<unsigned char>, CPrivKey> mapKeys;       //key:公钥      //value: 私钥
CKey keyUser;                                       //控制公钥和私钥的handler

mapWallet保存钱包中的交易,mapKeys保存公钥私钥对,keyUser是公钥和私钥的管理者,比较简单,新的私钥和公钥是调用openssl库生成的。这三个全局变量的数据都是在通过LoadWallet函数加载进内存的。

3. 加载钱包文件

LoadWallet流程比价简单,就是将wallet.dat的数据加载到内存,然后设置一下默认使用的公钥的私钥,如果不存在就生成新的公钥私钥对。

bool LoadWallet()
{
    vector<unsigned char> vchDefaultKey;
    //从数据库文件wallet.dat将钱包内容加载内存, 默认公钥放在vchDefaultKey
    if (!CWalletDB("cr").LoadWallet(vchDefaultKey))
        return false;

    //默认公钥有对应私钥
    if (mapKeys.count(vchDefaultKey))
    {
        // Set keyUser
        keyUser.SetPubKey(vchDefaultKey);
        keyUser.SetPrivKey(mapKeys[vchDefaultKey]);
    }
    //map里找不着默认公钥对应的私钥,创造新key
    else
    {
        // Create new keyUser and set as default key
        RandAddSeed(true);
        keyUser.MakeNewKey();
        //新生成的公钥私钥对写数据库
        if (!AddKey(keyUser))
            return false;
        if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address"))
            return false;
        //写默认key
        CWalletDB().WriteDefaultKey(keyUser.GetPubKey());
    }
    return true;
}

4. 交易结构解析

想要使用钱包发起一笔交易,先要看一看交易的结构,以Alice向Bob购买咖啡的交易为例:

{
  "version": 1,
  "locktime": 0,
  "vin": [
    {
      "txid":"7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",
      "vout": 0,
      "scriptSig": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
      "sequence": 4294967295
    }
 ],
  "vout": [
    {
      "value": 0.01500000,
      "scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG"
    },
    {
      "value": 0.08450000,
      "scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG",
    }
  ]
}

这笔交易是由Alice的钱包构建生成然后广播出来的,矿工收到是序列化的交易,是一串数字:

咖啡交易交易的序列化十六进制表示(共258个字节)
0100000001186f9f998a5aa6f048e51dd8419a14d8a0f1a8a2836dd73 
4d2804fe65fa35779000000008b483045022100884d142d86652a3f47 
ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039 
ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813 
01410484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade84 
16ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc1 
7b4a10fa336a8d752adfffffffff0260e31600000000001976a914ab6 
8025513c3dbd2f7b92a94e0581f5d50f654e788acd0ef800000000000 
1976a9147f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a888ac00000000

下面根据序列化交易的结构解析下这串数字,熟悉一下每个字节,然后对重要字段作出说明:

01000000:          version字段(小端格式)
01:                vin的成员个数
186f....5779:      txid(小端格式)
00000000:          vin中的vout索引
8b:                解锁脚本的size(139个字节)
48:                签名部分的size(72个字节)
3845....3803:      签名数据
01:                签名类型(对整个交易签名)
41:                公钥的size(65个字节)
0484....2adf:      公钥
ffffffff:          Sequence(最大值)
02:                vout的成员个数
60e3160000000000:  vout[0]的value(小端格式,0.015btc)
19:                锁定脚本的size(25个字节)
76:                操作码OP_DUP
a9:                 操作码OP_HASH160
14:                公钥hash的size(20个字节)
ab68....54e7:      公钥hash
88:                操作码OP_EQUALVERIFY
ac:                 操作码OP_CHECKSIG
d0ef800000000000:  vout[1]的value(小端格式,0.0845btc)
19:                ....
76:                 ....
a9:                 ....
14:                 ....
7f9b....25a8:       公钥hash
88:                ....
ac:                 ....
00000000:           locktime
❖ scriptSig

解锁脚本,由签名(包括签名类型)和公钥组成

❖ scriptPubKey

锁定脚本,对于普通的交易,比如这笔,其格式为:OP_DUP OP_HASH160 公钥哈希 OP_EQUALVERIFY OP_CHECKSIG

❖ version

交易版本号,符合标准的是1和2。1代表普通交易,2代表支持操作码OP_CHECKSEQUENCEVERIFY的交易,其余数字预留。

❖ locktime

锁定区块高度,到了这个高度交易才可以被打包,默认是0

❖ sequence

交易可替换的标志,默认是UINT_MAX

中本聪在比特币的设计中,想要使用locktime和sequence完成交易的替换,设想是这样:

  1. 发送一笔交易时lockTime设置为非零某值,sequence为非最大值。
  2. 在LockTime到达前,可以用新版本的交易代替旧版本的交易,nSequence大被认为是新的

这个设想是很好的,可是在比特币的早期为了防止DOS攻击,实际上nSequence字段是没被启用的,一直被设置为最大值。直到BIP-125提出后,nSequence被主要用来做RBF(replace-by-fee)的信号,允许节点用更高交易费的交易代替原来的交易。

5. 发起交易流程

下面来介绍用钱包发起交易整体流程,先贴出流程图

下面对流程图的关键步骤作出解释

❖ 计算可用余额

因为mapWallet保存了所有发给自己钱的交易,所以需要遍历mapWallet,找出所有可用的交易,算下钱包一共有多少钱,那么什么样算是可用的交易?要满足两个条件:

  1. 未被花费
  2. final交易

未被花费好理解,但是请注意这和UTXO没有关系,UTXO集是从区块链账本里提炼出的集合。而我们这里是从mapWalle中查找未花费的交易,仅仅是看fSpent字段,这个字段在我们发出交易的时候会被置True。

那么final交易是什么? 就是不能再变的交易。我们用钱包花钱时是可以引用未被记录至区块链账本中的交易的,但是要保证交易不会被替换。贴出代码:

int64 GetBalance()
{
    int64 nStart, nEnd;
    QueryPerformanceCounter((LARGE_INTEGER*)&nStart);

    int64 nTotal = 0;
    CRITICAL_BLOCK(cs_mapWallet)
    {
        for (map<uint256, CWalletTx>::iterator it = mapWallet.begin(); it != mapWallet.end(); ++it)
        {
            CWalletTx* pcoin = &(*it).second;
            if (!pcoin->IsFinal() || pcoin->fSpent)
                continue;
            nTotal += pcoin->GetCredit();
        }
    }

    QueryPerformanceCounter((LARGE_INTEGER*)&nEnd);
    ///printf(" GetBalance() time = %16I64d\n", nEnd - nStart);
    return nTotal;
}
❖ CreateTransaction

CreateTransaction是创建交易的函数,流程稍微复杂,会在后面展开分析。

❖ CommintTransactionSpent

交易被成功的创建之后,接下来需要将交易的fSpent标志位置true,然后写入数据库。

❖ 检查和广播交易

到了这步,只要检查一下交易并广播出去就可以了

// 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();

6. 构造交易(CreateTransaction)

CreateTransaction函数就是用mapWallet中已有的数据按照模板把交易搭起来,先看流程图再解释。

❖ 选择交易

首先是从mapWallet中选择交易,排除掉非final和已经花费掉的交易后,在剩下的交易中凑出最接近于目标交易额(已加上默认手续费)的交易组合。

❖ 构造vout

中本聪这个版本的代码在构造vout时写的比较简单,vout[0]用于付款,vout[1]用于找零,从注释里看,他已经考虑到了这里的不妥,在这个版本中仅仅做了简单的实现。vout中除了金额就是锁定脚本。

// 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)
{
    /// todo: for privacy, should randomize the order of outputs,
    //        would also have to use a new key for the change.
    // Use the same key as one of the coins
    vector<unsigned char> 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));
}
❖ 构造vin

接下来,用选出的交易组合构造vin。 这个过程主要就是遍历vin数组,用私钥和一个临时构建的交易的hash值生成签名(调用openssl的椭圆曲线加密算法函数),然后将每个签名填入对应的vin位置。

 // 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++);

因为签名类型是01(对整个交易做签名),所以签名要由私钥和一个临时构建的交易通过函数运算生成,临时构建的交易是这样生成的:拷贝一个新交易的副本,将副本的vin清空,在vin对应的索引处本来填解锁脚本的位置填入父交易的锁定脚本,这样就生成了这个临时构建的交易,然后对其hash,将hash值和私钥当做参数传入底层的签名函数就生成了签名,贴出此处的代码

bool SignSignature(const CTransaction& txFrom, CTransaction& txTo, unsigned int nIn, int nHashType, CScript scriptPrereq)
{
    assert(nIn < txTo.vin.size());
    //本笔交易的vin的具体部分
    CTxIn& txin = txTo.vin[nIn];
    assert(txin.prevout.n < txFrom.vout.size());
    //上笔交易的vout的具体部分
    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.
    // 构建了一个txTemp,只留下txTo的vin[nIn]部分,解锁脚本处填的是vin的锁定脚本,hash之,返回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;
}

这里是调用底层openssl库的ECDSA_sign函数的地方,hash就是就是临时构建交易的hash,pkey是私钥,生成的签名存入vchSig中

bool Sign(uint256 hash, vector<unsigned char>& vchSig)
{
    vchSig.clear();
    unsigned char pchSig[10000];
    unsigned int nSize = 0;
    if (!ECDSA_sign(0, (unsigned char*)&hash, sizeof(hash), pchSig, &nSize, pkey))
        return false;
    vchSig.resize(nSize);
    memcpy(&vchSig[0], pchSig, nSize);
    return true;
}
❖ 比较手续费

比特币的最低手续费率是每1kb数据1000聪,接下来会检查钱包设定的手续费是否低于需要支付的最低手续费,如果是,那么要更新手续费,从SelectCoins函数开始重新走一遍流程。

❖ 更新前向交易

接下来,会以一定规则追溯新交易的前向交易,将找到的交易存在集合vtxPrev中,vtxPrev只在两个地方用到,一是加载钱包后,遍历这个集合,对不在区块链中的交易调用AcceptTransaction函数;第二是在RelayWalletTransaction函数中,也会遍历这个集合,查找里面不在交易池和区块链中的交易,找到后将这些交易和新创建的交易一起广播出去。
因此可以看出,vtxPrev的作用是存储支持新交易的那些交易,如过钱包发出的新交易想要尽快被打包,钱包需要“积极地”将这些支持他的交易也广播出去。

至此,使用具备钱包功能的全节点发送交易的流程就分析完了。

BTech原创,未经许可不得转载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容