实现Blockchain/Bitcoin/Ethereum最全教程(第三部分)


(darcylike) #1

我们已经委托“维权骑士”为所有BitTiger的内容维权。**未经允许,禁止转载。**如需转载,请私信 @Ray Cao。文章比较长,超出了知乎专栏的字数限制,因此分为了几个部分:


如何找他人帮忙

如果每次添加交易都需要用户自己挖矿,那么效率会极为低下。我们如何才能利用他人来帮忙呢?这需要我们把未确认的交易提交到这个网络中,并且期待有人能够帮助我们把这次交易写入区块链中。

因此节点直接除了同步区块的信息之外还需要交流未确认的交易信息。

如何保存未确认的交易

我们需要构建一个新的结构“交易池”来保存未确认的交易(Bitcoin中称之为mempool)。 我们可以通过数据来实现:

let transactionPool: Transaction[] = [];

如何使用这个新的提交交易的功能呢?我们可以在创建一个对外的接口POST /sendTransaction。这个方法会在我们本地的节点的交易池中添加我们的新的交易,这也会成为我们默认的提交交易的方法。

app.post('/sendTransaction', (req, res) => {
        ...
    })

这时候我们就不再需要挖矿,而只是把交易记录下来。

const sendTransaction = (address: string, amount: number): Transaction => {
    const tx: Transaction = createTransaction(address, amount, getPrivateFromWallet(), getUnspentTxOuts(), getTransactionPool());
    addToTransactionPool(tx, getUnspentTxOuts());
    return tx;
};

如何通知他人交易信息

当我们添加一个未确认的交易后,我们需要把这个交易告诉整个网络,并且期待有人会把这个交易放入区块链中。我们要如何广播呢?

  • 当一个节点接收到一个新的未确认的交易时,他会广播自己的交易池给所有的节点
  • 当一个节点第一次连接到另一个节点时,他会请求这个节点的交易池

因此我们需要构建两个新的消息:QUERY_TRANSACTION_POOL和 RESPONSE_TRANSACTION_POOL。它们一个负责查询,一个负责回复,具体的代码如下。

enum MessageType {
    QUERY_LATEST = 0,
    QUERY_ALL = 1,
    RESPONSE_BLOCKCHAIN = 2,
    QUERY_TRANSACTION_POOL = 3,
    RESPONSE_TRANSACTION_POOL = 4
}

交易池信息的消息构建如下:

const responseTransactionPoolMsg = (): Message => ({
    'type': MessageType.RESPONSE_TRANSACTION_POOL,
    'data': JSON.stringify(getTransactionPool())
});

const queryTransactionPoolMsg = (): Message => ({
    'type': MessageType.QUERY_TRANSACTION_POOL,
    'data': null
});

为了实现整个广播的逻辑,我们需要添加处理MessageType.RESPONSE_TRANSACTION_POOL消息的业务逻辑。每当我们收到了未确认的交易,我们首先把它加入到自己的消息池中。然后我们会把我们的整个交易池广播给所有我身边的节点。

case MessageType.RESPONSE_TRANSACTION_POOL:
    const receivedTransactions: Transaction[] = JSONToObject<Transaction[]>(message.data);
    receivedTransactions.forEach((transaction: Transaction) => {
        try {
            handleReceivedTransaction(transaction);
            //if no error is thrown, transaction was indeed added to the pool
            //let's broadcast transaction pool
            broadCastTransactionPool();
        } catch (e) {
            //unconfirmed transaction not valid (we probably already have it in our pool)
        }
    });

如何防止重放攻击

每个节点都能发送交易信息,因此我们需要验证是否有风险和错误。如同之前的判断标准一样,我们需要判断交易的格式是否正确,交易的发起者、接收者、签名是否匹配。

但是在整个网络中,一个攻击者可以通过把同一个未花费的交易用在不同的交易中来进行攻击。因此我们需要添加一条验证规则:一个未确认交易中的任何一个未花费交易不能出现在已有的未确认交易中。具体的代码实现如下,

const isValidTxForPool = (tx: Transaction, aTtransactionPool: Transaction[]): boolean => {
    const txPoolIns: TxIn[] = getTxPoolIns(aTtransactionPool);

    const containsTxIn = (txIns: TxIn[], txIn: TxIn) => {
        return _.find(txPoolIns, (txPoolIn => {
            return txIn.txOutIndex === txPoolIn.txOutIndex && txIn.txOutId === txPoolIn.txOutId;
        }))
    };

    for (const txIn of tx.txIns) {
        if (containsTxIn(txPoolIns, txIn)) {
            console.log('txIn already found in the txPool');
            return false;
        }
    }
    return true;
};

我们要如何从交易池中移除一个交易呢?每当一个新的区块产生后,我们会更新交易池。

如何把未确认交易放入区块

当一个节点开始挖矿的时候,他会把自己的交易池作为整个区块的数据。具体代码如下:

const generateNextBlock = () => {
    const coinbaseTx: Transaction = getCoinbaseTransaction(getPublicFromWallet(), getLatestBlock().index + 1);
    const blockData: Transaction[] = [coinbaseTx].concat(getTransactionPool());
    return generateRawNextBlock(blockData);
};
As the transactions are already validated, before they are added to the pool, we are not doing any further validations at this points.

如何更新交易池

当一个新的区块产生时,它会让我们交易池中的很多区块无效,因此我们需要重新验证交易池。场景如下,

  • 一个未确认交易已经被加入新的区块中
  • 一个未花费的交易在新的区块中已经被花费掉,进而影响到包含这个未花费交易的其它交易

因此,我们实现如下代码:

const updateTransactionPool = (unspentTxOuts: UnspentTxOut[]) => {
    const invalidTxs = [];
    for (const tx of transactionPool) {
        for (const txIn of tx.txIns) {
            if (!hasTxIn(txIn, unspentTxOuts)) {
                invalidTxs.push(tx);
                break;
            }
        }
    }
    if (invalidTxs.length > 0) {
        console.log('removing the following transactions from txPool: %s', JSON.stringify(invalidTxs));
        transactionPool = _.without(transactionPool, ...invalidTxs)
    }
};

从以上代码可以看出,我们只需要知道当前尚未花费掉交易即可进行判断。

小结:如何找他人帮忙

我们现在可以通过他人来帮忙把交易加入区块中。但是一个节点为什么要加入其他人的交易信息呢?我们需要给他们支付一定的费用。这个就留给你来实现吧。

我们在下一节会实现能够对区块链进行操作的用户界面。


如何实现用户界面

只是通过接口进行操作还不够直观,让我们在本节中实现用户界面。因为我们的节点已经实现了HTTP的接口,因此我们只需要创建一个网页进行可视化就行了。

那么我们还需要什么接口来支持呢?

  • 查询区块和交易详细信息的接口
  • 查询特定地址详细信息的借口

如何实现查询接口

以下是查询区块详细信息的接口:

app.get('/block/:hash', (req, res) => {
        const block = _.find(getBlockchain(), {'hash' : req.params.hash});
        res.send(block);
    });

以下是查询交易详细信息的接口:

app.get('/transaction/:id', (req, res) => {
        const tx = _(getBlockchain())
            .map((blocks) => blocks.data)
            .flatten()
            .find({'id': req.params.id});
        res.send(tx);
    });

以下是查询具体地址详细信息的接口,你将会看到一个地址的未花费交易,从而能够得到他的余额。

app.get('/address/:address', (req, res) => {
        const unspentTxOuts: UnspentTxOut[] =
            _.filter(getUnspentTxOuts(), (uTxO) => uTxO.address === req.params.address)
        res.send({'unspentTxOuts': unspentTxOuts});
    });

当然,我们也可以添加他已经花费掉的交易的信息,从而得到更加全景的信息。

我们如何实现前端界面

我们使用Vue.js框架来实现整个界面。

如何查看区块链的信息

我们可以实现“区块链查看器”的网站来对区块链的信息进行可视化,基于之前的接口,我们可以得到每个地址的余额等信息。因为这个功能只是进行查询,所以我们只需要对结果进行可视化就行了。一个界面的截图如下:

![](data:image/svg+xml;utf8,)

(来源: NaiveCoin

如何查看钱包的信息

钱包的界面需要更多的交易能力,一个界面的截图如下:

![](data:image/svg+xml;utf8,)

(来源: NaiveCoin

小结:如何实现用户界面

我们为我们的区块链实现了查看器和钱包的界面,这会成为你进一步前进的基础。


总结

我们在这一章实现了带有token功能的区块链,你能想到还有哪些重要的功能需要我们去实现吗?

Ethereum

Ethereum是一个图灵机,通过编写协议来驱动状态的改变。

什么是图灵机?状态和状态转移。

核心功能?所有权、交易、状态转换。

账号是什么?一个160位的二进制。

都是人能够控制的吗?人能够直接控制的是外部账号;通过代码控制的是内部账号。

如何防止有人超长时间占用节点?需要付费。

系统架构

![](data:image/svg+xml;utf8,)

(来源: Matemaz Twitter

Go-ethereum

Ethereum最流行的Go版本的实现。

Solidity

能够编译成供EVM执行的ByteCode的高级语言。

Mist

Ethereum的应用查看器。

总结:Ethereum

我们这里只是对Ethereum的架构进行了基本介绍,让我们在下一节课中继续深入探讨。


全文完。

更多资源,请至www.BitTiger.io查看。


参考资料