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/

相關文章
相關標籤/搜索