第四章 自己動手寫比特幣之錢包

天地會珠海分舵發表於2019-06-24

概覽

錢包的目的是為了給使用者建立更高層的抽象介面來對交易進行管理。

我們最終的目的是讓使用者可以方便的:

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

以上這些生效後,使用者就不需要知道上一章節中描述的inputs和outpus這些交易的細節,就能對交易進行管理了。就好比在比特幣網路中,你只需要把比特幣打入對應地址就能給別人打入比特幣, 同時,你只需要將你自己的地址釋出出去,別人就能給你打比特幣了。

本章節完整的程式碼請看這裡

生成錢包

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

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

如之前所言,我們的公鑰(錢包地址)是通過私鑰演繹出來的。

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

需要提一提的是,把私鑰明文儲存在檔案中是一個非常不安全的做法。我們這樣子做只是為了演示的簡單起見而已。同時,一個錢包當前只支援一個私鑰,所以,如果你需要一個新的公鑰來作為地址的話,必須要建立一個新的錢包。

錢包餘額

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

這意味著,我們如果要檢視錢包餘額的話,事情就變得非常簡單了:你只需要將該地址下的所有「未消費交易output」記錄的貨幣數加起來就完了。

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上的專案給個星,將不勝感激.

相關文章