cpp 区块链模拟示例(六) 交易

交易(transaction)是比特币的核心所在,而区块链的惟一目的,也正是为了可以安全可靠地存储交易。在区块链中,交易一旦被建立,就没有任何人可以再去修改或是删除它。在今天的文章中,咱们会实现交易的通用机制。web

若是之前开发过 web 应用,在支付的实现环节,你可能会在数据库中建立这样两张表:算法

  • accounts
  • transactions

account(帐户)会存储用户信息,里面包括了我的信息和余额。transaction(交易)会存储资金转移信息,也就是资金从一个帐户转移到另外一个帐户这样的内容。在比特币中,支付是另一种彻底不一样的方式:数据库

  1. 没有帐户(account)
  2. 没有余额(balance)
  3. 没有住址(address)
  4. 没有货币(coin)
  5. 没有发送人和接收人(sender,receiver)(这里所说的发送人和接收人是基于目前现实生活场景,交易双方与人是一一对应的。而在比特币中,“交易双方”是地址,地址背后才是人,人与地址并非一一对应的关系,一我的可能有不少个地址。)

鉴于区块链是一个公开开放的数据库,因此咱们并不想要存储钱包全部者的敏感信息(因此具备必定的匿名性)。资金不是经过帐户来收集,交易也不是从一个地址将钱转移到另外一个地址,也没有一个字段或者属性来保存帐户余额。交易就是区块链要表达的全部内容。那么,交易里面到底有什么内容呢?安全

比特币交易

一笔交易由一些输入(input)和输出(output)组合而来:函数

typedef struct txoutput {
    int        value;
    string    scriptPubKey;
}TXOutput;

typedef struct txinput {
    string    txid;
    int        vout;
    string    scriptSig;
}TXInput;

typedef struct transaction {
    string        id;
    vector<TXInput>        vin;
    vector<TXOutput>    vout;
}Transaction;

对于每一笔新的交易,它的输入会引用(reference)以前一笔交易的输出(这里有个例外,也就是咱们待会儿要谈到的 coinbase 交易)。所谓引用以前的一个输出,也就是将以前的一个输出包含在另外一笔交易的输入当中。交易的输出,也就是币实际存储的地方。下面的图示阐释了交易之间的互相关联:post

 

 

注意:区块链

  1. 有一些输出并无被关联到某个输入上
  2. 一笔交易的输入能够引用以前多笔交易的输出
  3. 一个输入必须引用一个输出

贯穿本文,咱们将会使用像“钱(money)”,“币(coin)”,“花费(spend)”,“发送(send)”,“帐户(account)” 等等这样的词。可是在比特币中,实际并不存在这样的概念。交易仅仅是经过一个脚本(script)来锁定(lock)一些价值(value),而这些价值只能够被锁定它们的人解锁(unlock)。ui

交易输出

让咱们先从输出(output)开始:spa

typedef struct txoutput {
    int        value;
    string    scriptPubKey;
}TXOutput;

实际上,正是输出里面存储了“币”(注意,也就是上面的 Value 字段)。而这里的存储,指的是用一个数学难题对输出进行锁定,这个难题被存储在 ScriptPubKey 里面。在内部,比特币使用了一个叫作 Script 的脚本语言,用它来定义锁定和解锁输出的逻辑。虽然这个语言至关的原始(这是为了不潜在的黑客攻击和滥用而有意为之),并不复杂,可是咱们并不会在这里讨论它的细节。你能够在这里 找到详细解释。.net

在比特币中,value 字段存储的是 satoshi 的数量,而不是>有 BTC 的数量。一个 satoshi 等于一百万分之一的 >BTC(0.00000001 BTC),这也是比特币里面最小的货币单位>(就像是 1 分的硬币)。

因为尚未实现地址(address),因此目前咱们会避免涉及逻辑相关的完整脚本。ScriptPubKey 将会存储一个任意的字符串(用户定义的钱包地址)。

顺便说一下,有了一个这样的脚本语言,也意味着比特币其实也能够做为一个智能合约平台。

关于输出,很是重要的一点是:它们是不可再分的(invisible),这也就是说,你没法仅引用它的其中某一部分。要么不用,若是要用,必须一次性用完。当一个新的交易中引用了某个输出,那么这个输出必须被所有花费。若是它的值比须要的值大,那么就会产生一个找零,找零会返还给发送方。这跟现实世界的场景十分相似,当你想要支付的时候,若是一个东西值 1 美圆,而你给了一个 5 美圆的纸币,那么你会获得一个 4 美圆的找零。

交易输入

这里是输入:

 

typedef struct txinput {
    string    txid;
    int        vout;
    string    scriptSig;
}TXInput;

正如以前所提到的,一个输入引用了以前一笔交易的一个输出:Txid 存储的是这笔交易的 ID,Vout 存储的是该输出在这笔交易中全部输出的索引(由于一笔交易可能有多个输出,须要有信息指明是具体的哪个)。ScriptSig 是一个脚本,提供了可做用于一个输出的 ScriptPubKey 的数据。若是 ScriptSig 提供的数据是正确的,那么输出就会被解锁,而后被解锁的值就能够被用于产生新的输出;若是数据不正确,输出就没法被引用在输入中,或者说,也就是没法使用这个输出。这种机制,保证了用户没法花费属于其余人的币。

再次强调,因为咱们尚未实现地址,因此 ScriptSig 将仅仅存储一个任意用户定义的钱包地址。咱们会在下一篇文章中实现公钥(public key)和签名(signature)。

来简要总结一下。输出,就是 “币” 存储的地方。每一个输出都会带有一个解锁脚本,这个脚本定义了解锁该输出的逻辑。每笔新的交易,必须至少有一个输入和输出。一个输入引用了以前一笔交易的输出,并提供了数据(也就是 ScriptSig 字段),该数据会被用在输出的解锁脚本中解锁输出,解锁完成后便可使用它的值去产生新的输出。

也就是说,每一笔输入都是以前一笔交易的输出,那么从一笔交易开始不断往前追溯,它涉及的输入和输出究竟是谁先存在呢?换个说法,这是个鸡和蛋谁先谁后的问题,是先有蛋仍是先有鸡呢?

先有蛋

在比特币中,是先有蛋,而后才有鸡。输入引用输出的逻辑,是经典的“蛋仍是鸡”问题:输入先产生输出,而后输出使得输入成为可能。在比特币中,最早有输出,而后才有输入。换而言之,第一笔交易只有输出,没有输入。

当矿工挖出一个新的块时,它会向新的块中添加一个 coinbase 交易。coinbase 交易是一种特殊的交易,它不须要引用以前一笔交易的输出。它“凭空”产生了币(也就是产生了新币),这也是矿工得到挖出新块的奖励,能够理解为“发行新币”。

在区块链的最初,也就是第一个块,叫作创世块。正是这个创世块,产生了区块链最开始的输出。对于创世块,不须要引用以前交易的输出。由于在创世块以前根本不存在交易,也就没有不存在有交易输出。

来建立一个 coinbase 交易:

Transaction* NewCoinbaseTX(string to, string data) {
    if (data == "")
        data = "Reward to " + to;
    TXInput txin = TXInput{ "",-1,data };
    TXOutput txout = TXOutput{ subsidy ,to };
    Transaction* tx = new Transaction();
    tx->id = "";
    tx->vin.push_back(txin);
    tx->vout.push_back(txout);
    SetID(tx);

    return tx;
}

coinbase 交易只有一个输出,没有输入。在咱们的实现中,它的 Txid 为空,Vout 等于 -1。而且,在目前的视线中,coinbase 交易也没有在 ScriptSig 中存储一个脚本,而只是存储了一个任意的字符串。

在比特币中,第一笔 coinbase 交易包含了以下信息:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。可点击这里查看.

subsidy 是奖励的数额。在比特币中,实际并无存储这个数字,而是基于区块总数进行计算而得:区块总数除以 210000 就是 subsidy。挖出创世块的奖励是 50 BTC,每挖出 210000 个块后,奖励减半。在咱们的实现中,这个奖励值将会是一个常量(至少目前是)。

将交易保存到区块链

从如今开始,每一个块必须存储至少一笔交易。若是没有交易,也就不可能挖出新的块。这意味着咱们应该移除 Block 的 Data 字段,取而代之的是存储交易:

struct block {
    time_t    timeStamp;
    //string    data;
    vector<struct transaction*>    transactions;
    string    prevBlockHash;
    string  hash;
    int64_t    nonce;
};

NewBlock 和 NewGenesisBlock 也必须作出相应改变:

struct block* NewBlock(vector<struct transaction*> transactions, string prevBlockHash) {
    struct block* block = new Block{ time(NULL),transactions,prevBlockHash,"",0 };
    ProofOfWork* pow =  NewProofOfWork(block);
    string hash = "";
    int64_t nonce = Run(hash, pow);
    if (pow != NULL) {
        delete pow;
        pow = NULL;
    }

    block->hash = hash;
    block->nonce = nonce;

    return block;
}

struct block* NewGenesisBlock(struct transaction* coinbase) {
    vector<struct transaction*> vec;
    vec.push_back(coinbase);
    return NewBlock(vec,"");
}

接下来修改建立新链的函数:

struct blockchain* CreateBlockchain(string address) {
    string tip;
    const string genesisCoinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks";
    struct transaction* cbtx = NewCoinbaseTX(address, genesisCoinbaseData);
    struct block* genesis = NewGenesisBlock(cbtx);

    g_db[genesis->hash] = genesis;
    g_db["l"] = genesis;
    tip = genesis->hash;

    struct blockchain* bc = new Blockchain{ tip, &g_db };

    return bc;
}

如今,这个函数会接受一个地址做为参数,这个地址会用来接收挖出创世块的奖励。

工做量证实

工做量证实算法必需要将存储在区块里面的交易考虑进去,以此保证区块链交易存储的一致性和可靠性。因此,咱们必须修改 ProofOfWork.prepareData 方法:

string  prepareData(int64_t nonce, ProofOfWork* pow) {
    stringstream ss;
    
    ss << pow->block->prevBlockHash << HashTransactions(pow->block) << pow->block->timeStamp << nonce;

    return ss.str();
}

不像以前使用 pow.block.Data,如今咱们使用 pow.block.HashTransactions() :

string HashTransactions(struct block* b) {
    vector<string> txHashes;

    for (int i = 0; i < b->transactions.size(); i++) {
        txHashes.push_back(b->transactions[i]->id);
    }

    stringstream ss;
    for (int i = 0; i < txHashes.size(); i++) {
        ss << txHashes[i];
    }
    string hash = sha256(ss.str());
    return hash;
}

咱们使用哈希提供数据的惟一表示,这个以前也遇到过。咱们想要经过仅仅一个哈希,就能够识别一个块里面的全部交易。为此,咱们得到每笔交易的哈希,将它们关联起来,而后得到一个链接后的组合哈希。

比特币使用了一个更加复杂的技术:它将一个块里面包含的全部交易表示为一个  Merkle tree ,而后在工做量证实系统中使用树的根哈希(root hash)。这个方法可以让咱们快速检索一个块里面是否包含了某笔交易,即只需 root hash 而无需下载全部交易便可完成判断。

来检查一下到目前为止是否正确:

很好!咱们已经得到了第一笔挖矿奖励,可是,咱们要如何查看余额呢?

未花费的交易输出

咱们须要找到全部的未花费交易输出(unspent transactions outputs, UTXO)。未花费(unspent) 指的是这个输出尚未被包含在任何交易的输入中,或者说没有被任何输入引用。在上面的图示中,未花费的输出是:

  1. tx0, output 1;
  2. tx1, output 0;
  3. tx3, output 0;
  4. tx4, output 0.

固然了,当咱们检查余额时,咱们并不须要知道整个区块链上全部的 UTXO,只须要关注那些咱们可以解锁的那些 UTXO(目前咱们尚未实现密钥,因此咱们将会使用用户定义的地址来代替)。首先,让咱们定义在输入和输出上的锁定和解锁方法:

bool CanUnlockOutputWith(string unlockingData, TXInput* in) {
    return in->scriptSig == unlockingData;
}

bool CanBeUnlockedWith(string unlockingData, TXOutput* out) {
    return out->scriptPubKey == unlockingData;
}

在这里,咱们只是将 script 字段与 unlockingData 进行了比较。在后续文章咱们基于私钥实现了地址之后,会对这部分进行改进。

下一步,找到包含未花费输出的交易,这一步至关困难:

vector<struct transaction> FindUnspentTransactions(string address, struct blockchain* bc) {
    vector<struct transaction>            unspentTXs;
    map<string, vector<int>>    spentTXOs;
    struct blockchainiterator* bci = Iterator(bc);

    while (1) {
        struct block* block = Next(bci);

        for (int i = 0; i < block->transactions.size(); i++) {
            string txID = block->transactions[i]->id;

            for (int j = 0; j < block->transactions[i]->vout.size(); j++) {
        
                if (spentTXOs[txID].size() != 0) {
                    for (int k = 0; k < spentTXOs[txID].size(); k++) {
                        if (spentTXOs[txID][k] == j) {
                            goto CONFLAG;
                        }
                    }//for (int k = 0; k < spentTXOs[txID].size(); k++) {
                }//if (spentTXOs[txID].size() != 0) {
                if ( CanBeUnlockedWith(address, &(block->transactions[i]->vout[j])) ) {
                    unspentTXs.push_back(*(block->transactions[i]));
                }
            CONFLAG:
                int tmp = 0;
            }//for (int j = 0; j < block->transactions[i]->vout.size(); i++) {

            if (false == IsCoinbase(*(block->transactions[i])) ) {
                for (int k = 0; k < block->transactions[i]->vin.size(); k++) {
                    if (CanUnlockOutputWith(address, &(block->transactions[i]->vin[k]))) {
                        string inTxID = block->transactions[i]->vin[k].txid;
                        spentTXOs[inTxID].push_back(block->transactions[i]->vin[k].vout);
                    }
                }
            }

        }//for (int i = 0; i < block->transactions.size(); i++) {
        if (block->prevBlockHash == "")
            break;
    }

    return unspentTXs;
}

因为交易被存储在区块里,因此咱们不得不检查区块链里的每一笔交易。从输出开始:

if ( CanBeUnlockedWith(address, &(block->transactions[i]->vout[j])) ) {
                    unspentTXs.push_back(*(block->transactions[i]));
                }

若是一个输出被一个地址锁定,而且这个地址刚好是咱们要找的未花费交易输出的地址,那么这个输出就是咱们想要的。不过在获取它以前,咱们须要检查该输出是否已经被包含在一个输入中,也就是检查它是否已经被花费了:

if (spentTXOs[txID].size() != 0) {
                    for (int k = 0; k < spentTXOs[txID].size(); k++) {
                        if (spentTXOs[txID][k] == j) {
                            goto CONFLAG;
                        }
                    }//for (int k = 0; k < spentTXOs[txID].size(); k++) {
                }//if (spentTXOs[txID].size() != 0) {

咱们跳过那些已经被包含在其余输入中的输出(被包含在输入中,也就是说明这个输出已经被花费,没法再用了)。检查完输出之后,咱们将全部可以解锁给定地址锁定的输出的输入汇集起来(这并不适用于 coinbase 交易,由于它们不解锁输出):

if (false == IsCoinbase(*(block->transactions[i])) ) {
                for (int k = 0; k < block->transactions[i]->vin.size(); k++) {
                    if (CanUnlockOutputWith(address, &(block->transactions[i]->vin[k]))) {
                        string inTxID = block->transactions[i]->vin[k].txid;
                        spentTXOs[inTxID].push_back(block->transactions[i]->vin[k].vout);
                    }
                }
            }

这个函数返回了一个交易列表,里面包含了未花费输出。为了计算余额,咱们还须要一个函数将这些交易做为输入,而后仅返回一个输出:

vector<TXOutput> FindUTXO(string address, struct blockchain* bc) {
    vector<TXOutput> UTXOs;
    vector<struct transaction> unspentTransactions  = FindUnspentTransactions(address, bc);

    for (auto& tx : unspentTransactions) {
        for (auto& out : tx.vout) {
            if (CanBeUnlockedWith(address, &out)) {
                UTXOs.push_back(out);
            }
        }
    }
    return UTXOs;
}

就是这么多了!如今咱们来实现 getbalance 命令:

void getBalance(string address) {
    Blockchain* bc = NewBlockchain(address);
    int balance = 0;
    vector<txoutput> UTXOs = FindUTXO(address, bc);
    for (auto& out : UTXOs) {
        balance += out.value;
    }

    std::cout << "Balance of " << address << " : " << balance << std::endl;
}

帐户余额就是由帐户地址锁定的全部未花费交易输出的总和。

在挖出创世块之后,来检查一下咱们的余额:

这就是咱们的第一笔钱!

发送币

如今,咱们想要给其余人发送一些币。为此,咱们须要建立一笔新的交易,将它放到一个块里,而后挖出这个块。以前咱们只实现了 coinbase 交易(这是一种特殊的交易),如今咱们须要一种通用的交易:

Transaction* NewUTXOTransaction(string from, string to, int amount, Blockchain* bc) {
    vector<TXInput> inputs;
    vector<TXOutput> outputs;
    map<string, vector<int>> validOutputs;
    int acc = FindSpendableOutputs(from,amount,bc, validOutputs);

    if (acc < amount){
        std::cerr << "ERROR: Not enough funds" << std::endl;
        return NULL;
    }

    map<string, vector<int>>::iterator it = validOutputs.begin();
    for (; it != validOutputs.end(); it++) {
        for (auto& out : it->second) {
            TXInput input = TXInput{ it->first,out,from };
            inputs.push_back(input);
        }
    }

    TXOutput output = TXOutput{ amount, to };
    outputs.push_back(output);
    if (acc > amount){
        TXOutput newout = TXOutput{ acc - amount, from }; 
        outputs.push_back(newout);
    }
    Transaction* tx = new Transaction{ "", inputs, outputs };
    SetID(tx);

    return tx;
}

在建立新的输出前,咱们首先必须找到全部的未花费输出,而且确保它们存储了足够的值(value),这就是 FindSpendableOutputs 方法作的事情。随后,对于每一个找到的输出,会建立一个引用该输出的输入。接下来,咱们建立两个输出:

  1. 一个由接收者地址锁定。这是给实际给其余地址转移的币。

  2. 一个由发送者地址锁定。这是一个找零。只有当未花费输出超过新交易所需时产生。记住:输出是不可再分的。

FindSpendableOutputs 方法基于以前定义的 FindUnspentTransactions 方法:

int FindSpendableOutputs(string address, int amount, struct blockchain* bc, map<string, vector<int>>& unspentOutputs) {
    unspentOutputs.clear();
    vector<struct transaction> unspentTXs  =  FindUnspentTransactions(address, bc);
    int accumulated = 0;

    for (auto& tx : unspentTXs) {
        string txID = tx.id;
        for (int outIdx = 0; outIdx < tx.vout.size(); outIdx++) {
            if (CanBeUnlockedWith(address, &tx.vout[outIdx]) && accumulated < amount) {
                accumulated += tx.vout[outIdx].value;
                unspentOutputs[txID].push_back(outIdx);

                if (accumulated >= amount){
                    break;
                }
            }
        }
    }

    return accumulated;
}

这个方法对全部的未花费交易进行迭代,并对它的值进行累加。当累加值大于或等于咱们想要传送的值时,它就会中止并返回累加值,同时返回的还有经过交易 ID 进行分组的输出索引。咱们并不想要取出超出须要花费的钱。

如今,咱们能够修改 Blockchain.MineBlock 方法:

void MineBlock(vector<struct transaction*> transactions, struct blockchain* bc) {
    string lastHash;

    struct block* p = g_db["l"];
    if (p == NULL)
        return;
    lastHash = p->hash;

    struct block* newBlock = NewBlock(transactions, lastHash);
    (*(bc->db))[newBlock->hash] = newBlock;
    (*(bc->db))["l"] = newBlock;

    bc->tip = newBlock->hash;
}

最后,让咱们来实现 send 方法:

void send(string from,string to,int amount) {
    Blockchain* bc = NewBlockchain(from);
    Transaction* tx = NewUTXOTransaction(from, to, amount, bc);
    vector<Transaction*> tmp;
    tmp.push_back(tx);
    MineBlock(tmp, bc);
    printChain(bc);
    getBalance(from);
    getBalance(to);
    std::cout << "sucess!!" << std::endl;
}

发送币意味着建立新的交易,并经过挖出新块的方式将交易打包到区块链中。不过,比特币并非一连串马上完成这些事情(不过咱们的实现是这么作的)。相反,它会将全部新的交易放到一个内存池中(mempool),而后当一个矿工准备挖出一个新块时,它就从内存池中取出全部的交易,建立一个候选块。只有当包含这些交易的块被挖出来,并添加到区块链之后,里面的交易才开始确认。

让咱们来检查一下发送币是否能工做(正确与错误状况):

 

 

总结

虽然不容易,可是如今终于实现交易了!不过,咱们依然缺乏了一些像比特币那样的一些关键特性:

  1. 地址(address)。咱们尚未基于私钥(private key)的真实地址。

  2. 奖励(reward)。如今挖矿是确定没法盈利的!

  3. UTXO 集。获取余额须要扫描整个区块链,而当区块很是多的时候,这么作就会花费很长时间。而且,若是咱们想要验证后续交易,也须要花费很长时间。而 UTXO 集就是为了解决这些问题,加快交易相关的操做。

  4. 内存池(mempool)。在交易被打包到块以前,这些交易被存储在内存池里面。在咱们目前的实现中,一个块仅仅包含一笔交易,这是至关低效的。

 

 

 

 工程代码见群下载  文件名为part4.zip

 

参考博文:

https://blog.csdn.net/simple_the_best/article/details/78236282

https://jeiwan.cc/posts/building-blockchain-in-go-part-4/

相关文章
相关标签/搜索