第四章 自己动手写比特币之钱包

概览

钱包的目的是为了给用户创建更高层的抽象接口来对交易进行管理。

我们最终的目的是让用户可以方便的:

  • 创建一个新钱包
  • 查看钱包的余额
  • 在钱包之间进行交易

以上这些生效后,用户就不需要知道上一章节中描述的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上的项目给个星,将不胜感激.

posted @ 2019-06-24 09:19  天地会珠海分舵  阅读(682)  评论(0编辑  收藏  举报