第四章 本身動手寫比特幣之錢包

概覽

錢包的目的是爲了給用戶建立更高層的抽象接口來對交易進行管理。node

咱們最終的目的是讓用戶能夠方便的:git

  • 建立一個新錢包
  • 查看錢包的餘額
  • 在錢包之間進行交易

以上這些生效後,用戶就不須要知道上一章節中描述的inputs和outpus這些交易的細節,就能對交易進行管理了。就比如在比特幣網絡中,你只須要把比特幣打入對應地址就能給別人打入比特幣, 同時,你只須要將你本身的地址發佈出去,別人就能給你打比特幣了。github

本章節完整的代碼請看這裏算法

生成錢包

本教程中咱們將會用最簡單的方法對錢包進行初始化和存儲:將未加密的私鑰保存在node/wallet/private_key這個文件中。typescript

const privateKeyLocation = 'node/wallet/private_key';

const generatePrivatekey = (): string => {
    const keyPair = EC.genKeyPair();
    const privateKey = keyPair.getPrivate();
    return privateKey.toString(16);
};

const initWallet = () => {
    //let's not override existing private keys
    if (existsSync(privateKeyLocation)) {
        return;
    }
    const newPrivateKey = generatePrivatekey();

    writeFileSync(privateKeyLocation, newPrivateKey);
    console.log('new wallet with private key created');
};

如以前所言,咱們的公鑰(錢包地址)是經過私鑰演繹出來的。shell

const getPublicFromWallet = (): string => {
    const privateKey = getPrivateFromWallet();
    const key = EC.keyFromPrivate(privateKey, 'hex');
    return key.getPublic().encode('hex');
};

須要提一提的是,把私鑰明文保存在文件中是一個很是不安全的作法。咱們這樣子作只是爲了演示的簡單起見而已。同時,一個錢包當前只支持一個私鑰,因此,若是你須要一個新的公鑰來做爲地址的話,必需要建立一個新的錢包。npm

錢包餘額

複習下上一章節提到的一個說法:你在區塊鏈中擁有的加密貨幣, 指的其實就是在「未消費交易outputs」中,接收者地址爲本身的公鑰的一系列outputs。json

這意味着,咱們若是要查看錢包餘額的話,事情就變得很是簡單了:你只須要將該地址下的全部「未消費交易output」記錄的貨幣數加起來就完了。api

const getBalance = (address: string, unspentTxOuts: UnspentTxOut[]): number => {
    return _(unspentTxOuts)
        .filter((uTxO: UnspentTxOut) => uTxO.address === address)
        .map((uTxO: UnspentTxOut) => uTxO.amount)
        .sum();
};

爲了讓演示更簡單,咱們在查詢一個錢包地址的餘額時並不須要提供任何私鑰信息。也就是說,任何人均可以查看別人的帳戶餘額。安全

生成交易

進行加密貨幣交易時,用戶不該該須要關心交易中的inputs和outputs這些細枝末節。可是,當用戶A的帳戶有50個幣時,若是他要給用戶B發送10個幣時,交易後面究竟發生了什麼事情呢?

這種狀況下,系統會將10個幣發送到B的公鑰地址,同時會將剩餘的40個幣還給用戶A。也就是說,來源的50個幣必須消費完,因此在未來源的幣賦給交易的outputs時必須進行拆分。交易完後必須未來源50幣的output在「未消費交易outputs」中刪除,將新產生的兩個outputs加上去。 也就是說「未消費交易outputs」上的貨幣總量不會變,只是有些幣被交易了,地址屬於不一樣的用戶了而已。

下圖演示了上面所說起交易:

image

下面咱們來看一個更復雜點的場景:

  • 用戶C最開始擁有0個幣
  • 以後的三個交易讓C分別得到了10,20,30個幣
  • C想要給D轉發55個幣。

這種狀況下,用戶C的全部3個outputs(在「未消費交易outputs」中address爲C公鑰的那些outputs)都爲被用上才能湊夠55個幣給D,剩餘的5個幣則會還給C。

image

那麼如何將以上的描述用代碼邏輯來表示呢? 首先,咱們會爲交易建立相應的inputs。怎麼建立呢?咱們會遍歷「未消費交易outputs」中地址爲發送者公鑰的項,直到找到可以湊夠足夠的幣數(outputs的幣數加起來大於等於目標幣數)的outputs。

const findTxOutsForAmount = (amount: number, myUnspentTxOuts: UnspentTxOut[]) => {
    let currentAmount = 0;
    const includedUnspentTxOuts = [];
    for (const myUnspentTxOut of myUnspentTxOuts) {
        includedUnspentTxOuts.push(myUnspentTxOut);
        currentAmount = currentAmount + myUnspentTxOut.amount;
        if (currentAmount >= amount) {
            const leftOverAmount = currentAmount - amount;
            return {includedUnspentTxOuts, leftOverAmount}
        }
    }
    throw Error('not enough coins to send transaction');
};

如代碼所示,咱們除了找到知足條件的那些未消費的交易outputs,還會記錄下交易後剩餘的須要還給發送者的幣數leftOverAmount。

找到這些未消費的交易outputs以後,咱們就能夠爲當前交易建立對應的inputs了:

const toUnsignedTxIn = (unspentTxOut: UnspentTxOut) => {
    const txIn: TxIn = new TxIn();
    txIn.txOutId = unspentTxOut.txOutId;
    txIn.txOutIndex = unspentTxOut.txOutIndex;
    return txIn;
};
const {includedUnspentTxOuts, leftOverAmount} = findTxOutsForAmount(amount, myUnspentTxouts);
const unsignedTxIns: TxIn[] = includedUnspentTxOuts.map(toUnsignedTxIn);

作法很簡單,主要就是將每一個input中的txOutId指向上面找到的對應的未消費交易output項目。

緊跟着就須要建立示例中的2個outputs了:一個output是給接收者的,另一個output是發送者本身的,由於須要把剩餘的幣數還回來。固然,若是inputs中指向的幣數總和恰好等於交易量,即leftOverAmount爲0, 咱們就只須要建立一個發送給目標用戶的output就夠了。

const createTxOuts = (receiverAddress:string, myAddress:string, amount, leftOverAmount: number) => {
    const txOut1: TxOut = new TxOut(receiverAddress, amount);
    if (leftOverAmount === 0) {
        return [txOut1]
    } else {
        const leftOverTx = new TxOut(myAddress, leftOverAmount);
        return [txOut1, leftOverTx];
    }
};

最後,咱們會進行交易id計算(對交易內容作哈希),並對交易進行簽名,最終將交易簽名賦予給每一個input:

const tx: Transaction = new Transaction();
    tx.txIns = unsignedTxIns;
    tx.txOuts = createTxOuts(receiverAddress, myAddress, amount, leftOverAmount);
    tx.id = getTransactionId(tx);

    tx.txIns = tx.txIns.map((txIn: TxIn, index: number) => {
        txIn.signature = signTxIn(tx, index, privateKey, unspentTxOuts);
        return txIn;
    });

使用錢包

咱們會提供'/mineTransaction‘這個api來讓用戶方便的使用錢包這個功能:

app.post('/mineTransaction', (req, res) => {
        const address = req.body.address;
        const amount = req.body.amount;
        const resp = generatenextBlockWithTransaction(address, amount);
        res.send(resp);
    });

如代碼所示,使用者只須要提供接收方的地址和交易數量就能經過該api來實現交易。該api調用後爲首先進行一次挖礦,得到一筆50個幣的原始交易,而後根據接收方地址和交易數量完成指定交易,最終將這些交易記錄到新增長的區塊中,同時更新咱們的「未消費交易outputs」。

測試體驗

  • 啓動

爲了方便測試,本人對package.json中的啓動腳本作了些修改, 讓咱們能夠快速的啓動兩個節點進行測試,而不須要每次啓動都輸入一堆的參數。

npm run node1
npm run node 2

同時, 在啓動第二個節點時,加入PEER參數,讓第二個節點自動和節點1創建P2P鏈接。

"scripts": {
    "prestart": "npm run compile",
    "node1": "HTTP_PORT=3001 P2P_PORT=6001 WALLET=1 npm start ",
    "node2": "HTTP_PORT=3002 P2P_PORT=6002 WALLET=2 PEER=ws://localhost:6001 npm start ",
    "start": "node src/main.js",
    "compile": "tsc"
  },

最後,經過新加的WALLET參數的支持,咱們會爲每一個節點自動初始化一個錢包,這樣咱們就更容易觀察錢包和交易的行爲。

  • 提供額外的查看「未消費交易outputs」接口

由於「未消費交易outputs」這個清單是很是重要的,這時咱們進行交易的基礎。因此頗有必要提供一個額外的接口來查看裏面的數據的變化。你能夠經過GET的方式發送請求到 "http://localhost:3001/unspentTransactionOutputs" 接口來得到該清單。

注意,若是你起了兩個節點,那麼端口使用3001和3002都是能夠的。 由於該清單是一個分佈式清單,和咱們的區塊鏈同樣,是全網同步的。

有了這些以後,你就能夠方便的經過postman調用相應的接口對錢包和交易功能進行體驗了。

小結

咱們剛剛實現了一個未加密的錢包功能以進行簡單的交易。雖然這個交易算法(mineTransaction接口相關的邏輯)中最多隻能有兩個接收者outputs(一個是接收者,一個是本身), 可是咱們底層的區塊連接口是能支持任意數量的outputs的。好比你能夠建立一個inputs是50個幣,outputs分別是5,15和30個幣的交易, 但你必須手動填充這些數據並調用/mineRawBlock這個接口來達成。

迄今爲止,咱們進行一次交易,仍是須要先進行一次挖礦,才能將交易信息加入到新的區塊裏面。咱們當前的各個節點不會對未記錄在區塊中的交易作任何信息交換。 這些問題都將會在下一個章節進行解決。

本章節完整的代碼請看這裏

第五章

本文由天地會珠海分舵編譯,轉載需受權,喜歡點個贊,吐槽請評論,如能給Github上的項目給個星,將不勝感激.

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息