区块链研习社区块链大学区块链学习

区块链源码研读

2018-09-23  本文已影响21人  CodingCattwo

未经本人同意,禁止转载

EbookChain亿书链详解

在亿书的源码中有一个logic的文件夹,放置的代码是:

account.js
block.js
transaction.js

可见,account, block, transaction这三个概念可以被理解成区块链中的三个基本单位

我们从这样一个顺序来了解区块链的工作过程与代码架构:

  1. 地址,即account和contact,也就是用户/账户的别名,还包含联系人,修改时与发起交易一样
  2. 加密与签名:crypto与signature,发起交易时通过签名与多重签名来加密信息,验证身份,即怎么加密,怎么利用加密进行签名
  3. 交易:transaction,发起一笔交易要经过哪些过程,以及交易怎么被打包进入区块
  4. 区块与链:blocks,区块之间是怎么生成的,怎么解决分叉的
  5. DPOS共识:delegate,区块生成的时候选出一个区块作为所有节点的同步区块
  6. P2P网络:peer与transport,发起的交易和生成的区块怎么广播到其它节点

地址

Account & Accounts & Contacts

用户角色account

比特币里的用户角色仅仅就是一个比特币地址,该地址是通过Hash算法进行加密处理的字符串,因此我们叫它Hash地址。同时,基于真实网络的复杂性,对于IP地址的追踪也不容易,所以比特币的匿名性很好

亿书作为一款加密货币产品,自然也提供了类似的Hash地址。并基于该地址,扩展提供了其他功能,比如“别名地址”。

比特币地址是使用前缀来区分的,比如:1开头的地址就是我们实际使用的地址,3开头的地址是测试地址。而亿书,使用后缀来区分,通常以L结尾。

Accounts.prototype.generateAddressByPublicKey = function (publicKey) {
    var publicKeyHash = crypto.createHash('sha256').update(publicKey, 'hex').digest();
    var temp = new Buffer(8);
    for (var i = 0; i < 8; i++) {
        temp[i] = publicKeyHash[7 - i];
    }

    var address = bignum.fromBuffer(temp).toString() + 'L';
    if (!address) {
        throw Error("wrong publicKey " + publicKey);
    }
    return address;
};

向某个account发起交易

发起交易SEND类型,即资金转账的功能(后台编码的交易类型TransactionTypes.SEND,见764行),毫无疑问,必须提供三个参数"secret", "amount", "recipientId"。从702行可知,其中recipientId 可以是字符串地址,也可以是用户名。从764行以后的代码还可以了解到,交易时的recipientId和recipientUsername字段都保存在数据库里了。

var transaction = library.logic.transaction.create({
                  // 764行
                  type: TransactionTypes.SEND,
                                    amount: body.amount,
                                    sender: account,
                                    // 接收转账者
                                    recipientId: recipientId,
                                    recipientUsername: recipientUsername,
                                    keypair: keypair,
                                    requester: keypair,
                                    secondKeypair: secondKeypair
                                });

修改account的Username

发起交易USERNAME类型,即注册用户,同时,用户要提供明文密码(secret)和用户名(username)。

var transaction = library.logic.transaction.create({
                                type: TransactionTypes.USERNAME,
                                username: body.username,
                                sender: account,
                                keypair: keypair,
                                secondKeypair: secondKeypair,
                                requester: keypair
});
                    

Contacts联系人(Follow操作)

亿书具备社交功能,维护了一个联系人列表。与传统中心化软件不同的是,加密货币系统里,处处是交易,用户关注其他用户的行为,也是一项交易(内部的交易类型为TransactionTypes.FOLLOW)。所以follow一个人即添加为联系人,意味着发起一笔交易来follow

var transaction = library.logic.transaction.create({
                                type: TransactionTypes.FOLLOW,
                                sender: account,
                                keypair: keypair,
                                secondKeypair: secondKeypair,
                                contactAddress: followingAddress, // 511行
                                requester: keypair
                            });

添加关注需要两个参数"secret"和"following",这里"following"其实就是用户的帐号地址或用户名(见448行)。然后,经过一系列验证之后,写入数据库的contactAddress字段(见511行),一个关注的操作过程就完成了。

同理,获取联系人列表getContacts操作与之类似,但只是查询,并非发起交易,所以输入参数不需要secret进行签名验证。

总结

一个是生成加密货币的Hash地址,另一个是通过交易模块扩展和关联其他功能。其中,Hash地址是基础,在整个亿书的开发设计中,无处不在,签名、验证和交易,以及区块链等需要它。

加密与签名验证

Signatures & Multisignature & Crypto

公钥与私钥

基于非对称加密的特性:

非对称加密算法需要两个 密钥: 公开密钥(publickey)和私有密钥(privatekey)。
公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

加密过程

Node.js的Crypto模块,提供了一种封装安全凭证的方式,用于HTTPS网络或HTTP连接,也对OpenSSL的Hash,HMAC,加密,解密、签名和验证方法进行了封装。

Ebookcoin使用的是sha256Hash算法(除此之外,还有MD5,sha1,sha512等),这是经过很多人验证的有效安全的算法之一(请看参考)。通过Crypto模块,简单加密生成一个哈希值:

var hash = crypto.createHash('sha256').update(data).digest()

这个语句拆开来看,就是crypto.createHash('sha256')先用sha256算法创建一个Hash实例;接着使用.update(data)接受明文data数据,最后调用.digest()方法,获得加密字符串,即密文。

然后,使用Ed25519组件,简单直接地生成对应密钥对:

var keypair = ed.MakeKeypair(hash);

解密验证过程

加密技术的作用,重在传输和验证。所以,加密货币并不需要研究如何解密原文。而是,如何安全、快捷的验证。Ebookcoin使用了Ed25519第三方组件。

该组件是一个数字签名算法。签名过程不依赖随机数生成器,没有时间通道攻击的问题,签名和公钥都很小。签名和验证的性能都极高,一个4核2.4GHz 的 Westmere cpu,每秒可以验证 71000 个签名,安全性极高,等价于RSA约3000bit。一行代码:

var res = ed.Verify(hash, signatureBuffer || ' ', publicKeyBuffer || ' ');

产生公钥

在Ebookcoin世界里,Ebookcoin把用户设定的密码生成私钥和公钥,再将公钥经过16进制字符串转换产生帐号ID(类似于比特币地址)。付款的时候,只要输入这个帐号ID(或用户别名)就是了。

生成公钥:

shared.generatePublickey = function (req, cb) {
    var body = req.body;
    library.scheme.validate(body, {
        ...
        required: ["secret"]
    }, function (err) {
        ...
    // 644行
        privated.openAccount(body.secret, function (err, account) {
            ...
            cb(err, {
                publicKey: publicKey
            });
        });
    });
};

公钥生成地址,将用户密码进行加密处理,然后直接生成了密钥对,接着将公钥继续处理,generateAddressByPublicKey方法对公钥再一次加密,然后做16进制处理,得到所要地址:

// 447行
privated.openAccount = function (secret, cb) {
    var hash = crypto.createHash('sha256').update(secret, 'utf8').digest();
    var keypair = ed.MakeKeypair(hash);

    self.setAccountAndGet({publicKey: keypair.publicKey.toString('hex')}, cb);
};

// 482行
Accounts.prototype.setAccountAndGet = function (data, cb) {
    var address = data.address || null;
    if (address === null) {
        if (data.publicKey) {
      // 486行
            address = self.generateAddressByPublicKey(data.publicKey);
          ...
        }
    }
    ...
  // 494行
    library.logic.account.set(address, data, function (err) {
        ...
    });
};

Accounts.prototype.generateAddressByPublicKey = function (publicKey) {
    var publicKeyHash = crypto.createHash('sha256').update(publicKey, 'hex').digest();
    var temp = new Buffer(8);
    for (var i = 0; i < 8; i++) {
        temp[i] = publicKeyHash[7 - i];
    }

    var address = bignum.fromBuffer(temp).toString() + 'L';
    if (!address) {
        throw Error("wrong publicKey " + publicKey);
    }
    return address;
};

各模块与Crypto加密模块的关系:

class.png

交易

Transaction & Transactions & Types

交易的过程

比特币交易的定义是:把比特币从一个地址转到另一个地址。更准确地说,一笔“交易”就是一个经过签名运算的,表达价值转移的数据结构。每一笔“交易”都经过比特币网络传输,由矿工节点收集并封包至区块中,永久保存在区块链某处。

加密货币交易是指人们通过加密货币网络,把加密货币进行有效转移, 并把交易数据保存到区块链的过程。

加密货币的整个系统,都是为了确保正确地生成交易、快速地传播和验证交易,并最终写入全球交易总账簿——区块链而设计。因此,从开发设计角度考虑,一笔交易必须包括下列过程:

  1. 生成一笔交易。这里是指一条包含交易双方加密货币地址、数量、时间戳和有效签名等信息,而且不含任何私密信息的合法交易数据;
  2. 广播到网络。几乎每个节点都会获得这笔交易数据。
  3. 验证交易合法性。生成交易的节点和其他节点都要验证,没有得到验证的交易,是不能进入加密货币网络的。
  4. 写入区块链。

亿书交易类型

// helpers/transaction-types.js
module.exports = {
    SEND : 0,
    SIGNATURE : 1,
    DELEGATE : 2,
    VOTE : 3,
    USERNAME : 4,
    FOLLOW : 5,
    MULTI: 6,
    DAPP: 7,
    IN_TRANSFER: 8,
    OUT_TRANSFER: 9,
    ARTICALE : 10,
    EBOOK: 11,
  BUY: 12,
    READ: 13
}

SEND是最基本的转账交易,SIGNATURE是上一篇提到的“签名”交易,DELEGATE是注册为受托人,VOTE是投票,USERNAME是注册用户别名地址,FOLLOW是添加联系人,MULTI是注册多重签名帐号,DAPP是侧链应用,IN_TRANSFER是转入Dapp资金,OUT_TRANSFER转出Dapp资金,ARTICALE是发布文章,EBOOK是发布电子书,BUY是购买(电子书或其他商品),READ是付费阅读(电子书等)

交易基本流程

生成交易数据

一笔交易必须包含如下字段:

给合法交易签名

一笔合法交易

验证交易合法性

这里的交易合法性,除了基本信息正确之外,主要是指保证交易是未确认的交易,也不是用户重复提交的交易,即双花交易。双花交易是加密货币特有的现象,通俗的说,就是用户在交易确认之前(有一段时间,比特币时间更长),又一次提交了相同交易信息,导致一笔钱花两次,这种情况是必须要避免的。

一笔交易经过6-10个区块之后,这笔交易被认为是无法更改的,即已确认,因为这时候拒绝、变更的难度已经非常大,理论上已经不可能。

每笔交易在广播到网络之前必须验证合法性,不合法的交易没有机会广播到网络。节点收到新的交易信息时,要重新验证,即一旦交易被广播到网络,在其他节点,这里的验证和处理过程就会重复执行一次。

广播交易到点对点网络

没有中心服务器,必须借助点对点网络,把交易数据写入分布式公共账本——区块链,保证交易数据永远无法篡改,而且可以轻松查询追溯。这在中心化的服务器上,为了应对个别交易摩擦,保证交易记录可追溯,要采取更多的技术手段,记录更多的数据字段,意味着要保持大量数据冗余,付出更多资金成本。

因为交易数据不含私密信息,对网络没有苛刻要求,因此加密货币的网络可以覆盖很广,对网络的编程也变得灵活很多。理论上,只要能保证联通的便捷和快速,具体设计中不需要考虑更多复杂的因素。

生成交易到广播前的代码实现

/ 652行
shared.addTransactions = function (req, cb) {
    var body = req.body;
    library.scheme.validate(body, {
        type: "object",
        properties: {
            secret: {
                type: "string",
                minLength: 1,
                maxLength: 100
            },
            amount: {
                type: "integer",
                minimum: 1,
                maximum: constants.totalAmount
            },
            recipientId: {
                type: "string",
                minLength: 1
            },
            publicKey: {
                type: "string",
                format: "publicKey"
            },
            secondSecret: {
                type: "string",
                minLength: 1,
                maxLength: 100
            },
            multisigAccountPublicKey: {
                type: "string",
                format: "publicKey"
            }
        },
        //
        required: ["secret", "amount", "recipientId"]
    }, function (err) {
        // 验证数据格式
        if (err) {
            return cb(err[0].message);
        }

        // 验证密码信息
        var hash = crypto.createHash('sha256').update(body.secret, 'utf8').digest();
        var keypair = ed.MakeKeypair(hash);

        if (body.publicKey) {
            if (keypair.publicKey.toString('hex') != body.publicKey) {
                return cb("Invalid passphrase");
            }
        }

        var query = {};

        // 乙方(接收方)地址转换,保证可以用户名转账
        var isAddress = /^[0-9]+[L|l]$/g;
        if (isAddress.test(body.recipientId)) {
            query.address = body.recipientId;
        } else {
            query.username = body.recipientId;
        }

        library.balancesSequence.add(function (cb) {
            // 验证乙方用户合法性
            modules.accounts.getAccount(query, function (err, recipient) {
                if (err) {
                    return cb(err.toString());
                }
                if (!recipient && query.username) {
                    return cb("Recipient not found");
                }

                var recipientId = recipient ? recipient.address : body.recipientId;
                var recipientUsername = recipient ? recipient.username : null;

                // 验证甲方(发送方)用户合法性
                if (body.multisigAccountPublicKey && body.multisigAccountPublicKey != keypair.publicKey.toString('hex')) {
                    // 验证多重签名
                    modules.accounts.getAccount({publicKey: body.multisigAccountPublicKey}, function (err, account) {
                        if (err) {
                            return cb(err.toString());
                        }
                        // 多重签名帐号不存在
                        if (!account || !account.publicKey) {
                            return cb("Multisignature account not found");
                        }
                        // 多重签名帐号未激活
                        if (!account || !account.multisignatures) {
                            return cb("Account does not have multisignatures enabled");
                        }
                        // 帐号不属于该多重签名组
                        if (account.multisignatures.indexOf(keypair.publicKey.toString('hex')) < 0) {
                            return cb("Account does not belong to multisignature group");
                        }

                        // 接着验证甲方(发送方)用户合法性
                        modules.accounts.getAccount({publicKey: keypair.publicKey}, function (err, requester) {
                            if (err) {
                                return cb(err.toString());
                            }
                            // 甲方帐号不存在
                            if (!requester || !requester.publicKey) {
                                return cb("Invalid requester");
                            }

                            // 甲方支付密码(二次签名)不正确
                            if (requester.secondSignature && !body.secondSecret) {
                                return cb("Invalid second passphrase");
                            }

                            // 甲方帐号公钥与多重签名帐号公钥是不一样的(因为两个账户是不一样的)
                            if (requester.publicKey == account.publicKey) {
                                return cb("Invalid requester");
                            }

                            var secondKeypair = null;

                            if (requester.secondSignature) {
                                var secondHash = crypto.createHash('sha256').update(body.secondSecret, 'utf8').digest();
                                secondKeypair = ed.MakeKeypair(secondHash);
                            }

                            try {
                                // 763行 把上述数据整理成需要的交易数据结构,并给交易添加时间戳、签名、生成ID、计算交易费等
                                var transaction = library.logic.transaction.create({
                                    type: TransactionTypes.SEND,
                                    amount: body.amount,
                                    sender: account,
                                    recipientId: recipientId,
                                    recipientUsername: recipientUsername,
                                    keypair: keypair,
                                    requester: keypair,
                                    secondKeypair: secondKeypair
                                });
                            } catch (e) {
                                return cb(e.toString());
                            }

                            // 776行 处理交易
                            modules.transactions.receiveTransactions([transaction], cb);
                        });
                    });
                } else {
                    // 直接验证甲方(发送方)用户合法性,这里的请求者requester就是发出交易者sender                
                    ...
    });
}

节点收到新交易的处理

// modules/transactions.js文件
// 337行
Transactions.prototype.processUnconfirmedTransaction = function (transaction, broadcast, cb) {
    modules.accounts.setAccountAndGet({publicKey: transaction.senderPublicKey}, function (err, sender) {
        // 这是个闭包,在下面的程序运行结束的时候才调用,因此是验证完毕,才写入区块链、广播到网络
        function done(err) {
            if (err) {
                return cb(err);
            }
            // 这里 加入区块链 操作
            privated.addUnconfirmedTransaction(transaction, sender, function (err) {
                if (err) {
                    return cb(err);
                }
                // 触发事件,广播到网络
                library.bus.message('unconfirmedTransaction', transaction, broadcast);

                cb();
            });
        }

        if (err) {
            return done(err);
        }

        if (transaction.requesterPublicKey && sender && sender.multisignatures && sender.multisignatures.length) {
            modules.accounts.getAccount({publicKey: transaction.requesterPublicKey}, function (err, requester) {
                if (err) {
                    return done(err);
                }

                if (!requester) {
                    return cb("Invalid requester");
                }
                // 开始执行一系列验证,包括交易是不是已经存在
                library.logic.transaction.process(transaction, sender, requester, function (err, transaction) {
                    if (err) {
                        return done(err);
                    }

                    // 检查是否交易已经存在(包括双花交易)
                    if (privated.unconfirmedTransactionsIdIndex[transaction.id] !== undefined || privated.doubleSpendingTransactions[transaction.id]) {
                        return cb("Transaction already exists");
                    }
                    // 这里是 直接验证交易签名等信息,接着调用闭包 done(),把交易写入区块链并广播到网络
                    library.logic.transaction.verify(transaction, sender, done);
                });
            });
        } else {
            ...
}

区块与链

Block & Blocks & Loader

保存创世区块

创世区块是硬编码到客户端程序里的,会在客户端运行的时候,直接写入数据库。这样做的好处是保证每个客户端都有一个安全、可信的区块链的根。

这是一个非常典型的区块创建过程,我们可以借机会看看一个区块(创世)的数据是什么样的:

// genesisBlock.json 文件
{
  "version": 0,

    // 3行 在创世区块设定初始代币总量,这里是1亿;
  "totalAmount": 10000000000000000,  
  "totalFee": 0,
  "reward": 0,
  "payloadHash": "1cedb278bd64b910c2d4b91339bc3747960b9e0acf4a7cda8ec217c558f429ad",
  "timestamp": 0,
  "numberOfTransactions": 103,
  "payloadLength": 20326,
  "previousBlock": null,
  "generatorPublicKey": "b7b46c08c24d0f91df5387f84b068ec67b8bfff8f7f4762631894fce4aff6c75",

    // 1757行 创世区块高度为1;
  "height": 1, 
  "blockSignature": "2985d896becdb91c283cc2366c4a387a257b7d4751f995a81eae3aa705bc24fdb950c3afbed833e7d37a0a18074da461d68d74a3a223bc5f8e9c1fed2f3fec0e",
  "id": "8593810399212843182",

    // 12行。区块必须包含交易,这里是3种类型的交易,之前分析过,它们分别是转账交易、受托人交易和投票交易。
    // 为了方便阅读,这里把关联的交易信息排版在最后位置
    "transactions": [
    {
      "type": 0,

            // 15行
      "amount": 10000000000000000,
      "fee": 0,
      "timestamp": 0,
      "recipientId": "6722322622037743544L",
      "senderId": "5231662701023218905L",
      "senderPublicKey": "b7b46c08c24d0f91df5387f84b068ec67b8bfff8f7f4762631894fce4aff6c75",
      "signature": "aa413208c32d00b89895049ff21797048fa41c1b2ffc866900ffd97570f8d87e852c87074ed77c6b914f47449ba3f9d6dca99874d9f235ee4c1c83d1d81b6e07",
      "id": "5534571359943011068"
    },
        {
            "type": 2,
            ...
        },

        ...

        {
      "type": 3,
      ...
    }
    ...
  ]
}

加载本地区块

任何节点,都需要先加载验证本地区块链,确保没有被篡改。这个加载过程是软件初始化过程中的一部分,开发中不需要与网络节点联网等其他问题纠缠在一起。因此,代码需要被放在入口文件中去执行。

触发了“bind”事件,会执行所有模块里的“onBind()”方法。该方法运行之前,各个模块仅仅被实例化,处于待命状态,所以“bind”事件是激活各模块的重要事件,是继各模块构造函数运行之后的关键方法

  1. 将调用logic/account.js文件的createTables()方法,创建与用户相关的帐号信息表,全部以“mem_”开头,主要包括“mem_accounts”,“mem_accounts2contacts”,“mem_accounts2u_contacts”,“mem_accounts2delegates”,“mem_accounts2u_delegates”,“mem_accounts2multisignatures”,“mem_accounts2u_multisignatures”,“mem_round”等7个表。这些信息与用户相关,不需要被其他节点同步。
  2. 如果是新客户端,数据库为初始创建,创世区块第一次写入,count == 1,会立刻调用闭包load()函数(428行),加载验证缺失的区块,区块链数据同步完毕,然后触发 “blockchainReady” 事件(398行)。

“Offset” 不是分页数据,而是一次加载的区块数量,这样做的好处是避免一次性加载全部区块链,导致数据请求量过大,影响计算机性能。这个值最大是 limit 的值(380行),limit 等于 “config.json” 文件设定的全局变量

验证本地区块:

  1. 追溯前一区块,无法追溯自然是不正确的。

  2. 验证块签名,防止块内容被篡改。建议认真阅读该行调用的签名验证方法“verifySignature()”(在“logic/block.js”文件的150行,请去源码库查看),这应该是对二进制数据(这里是区块数据)进行签名验证的典型用法。验证失败,就要终止整个循环,删除该块及其以后的块。

  3. 验证块时段(Slot),防止块位置被篡改。实际上是变相验证了区块的高度及其时间戳(相关技术请看开发实践部分《关于时间戳及相关问题》的讨论)。亿书网络按照一定的周期循环(具体请参看《DPOS机制》),每一个块都可以根据其高度计算出它的出块时段,这与它的时间戳是对应的,这就锁定了区块位置,不然就是有问题。为什么要选择验证块时段,而不是简单直接的高度或时间戳?这是因为区块链有分叉的情况,相同高度存在多个块和时间戳,但相同的块时段却只能是一个。

  4. 验证交易。

创建新区块:

把整理好的数据组合成块数据结构(具体形式,类似于前面的创世区块),这里因为是新建数据,重要的是keypair、timestamp、previousBlock、transactions等四个字段信息。

其他字段,诸如:totalFee、reward、payloadHash等,会在“processBlock()”执行时处理。
这里的keypair、timestamp字段是该方法的参数,需要在调用的地方传入。

var block = library.logic.block.create({
                keypair: keypair,
                timestamp: timestamp,
                previousBlock: privated.lastBlock,
                transactions: ready

分叉问题:

  1. 块高度相同,父块不同。可能是父块验证出现问题;
  2. 交易已经存在。可能用户重复提交了交易,典型的就是“双花”问题;
  3. 受托人时段不同。出现时段验证错误,而块时段是与时间戳相关的,所以可能是时间处理出现了问题;
  4. 高度和父块都相同,但块ID不同。这是接收来的块,高度比最新块大于1,父块是最新块时才会正常写入区块链,不然只能写入分叉。块的ID信息是对块进行sha256加密算法得出的结果,可能获得的是不同分支上的块。

解决分叉-同步

该方法通过随机选择节点,并调用这里提供的api获得远程节点的“height”数据,在确保本地区块链高度小于远程节点区块链高度的前提下,如果本地是创世区块就把远程节点整个数据库同步过来,调用“privated.loadFullDb()”方法,不然就调用“privated.findUpdate()”方法更新缺失的区块。前者很简单,属于后者的特殊情况

P2P网络

Peer & Transport & Router
不同模块是如何协作起作用的,区块生成之后是怎样进行广播的,通信是保持一致性的基本需求;这个用Nodejs开发P2P网络有如下架构:

这个P2P网络一旦达到一定的节点数量,就会形成一个互联互通的不死网络

路由Router

modules文件夹下的各个模块文件,这些模块基本都是独立的Express微应用,在开发和设计上相互独立,各不冲突,逻辑清晰

路由扩展

任何应用,只要提供Web访问能力或第三方访问的Api,都需要提供从地址到逻辑的请求分发功能,这就是路由。Ebookcoin是基于http协议的Express应用,Express底层基于Node.js的connect模块,因此其路由设计简单而灵活

// 27行
var Router = function () {
    var router = require('express').Router();

    router.use(function (req, res, next) {
        res.header("Access-Control-Allow-Origin", "*");
        res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        next();
    });

    router.map = map;

    return router;
}

这段代码定义了一个Express路由器Router,并扩展了两个功能:

function map(root, config)
- root: 定义了所要开放Api的逻辑函数;
- config: 定义了路由和root定义的函数的对应关系。

相当于
router.get('/peers', function(req, res, next){
    root.getPeers(...);
})

节点路由

在peer.js里找到map方法

privated.attachApi = function () {

    ...
    
    // 34行
    router.map(shared, {
        "get /": "getPeers",
        "get /version": "version",
        "get /get": "getPeer"
    });
    
    ...
    
    // 44行
    library.network.app.use('/api/peers', router);
    library.network.app.use(function (err, req, res, next) {
        if (!err) return next();
        library.logger.error(req.url, err.toString());
        res.status(500).send({success: false, error: err.toString()});
    });
}

将定义好的路由放在/api/peers前缀之下,可以确信peer.js文件提供了下面3个公共Api地址:

http://ip:port/api/peers/

http://ip:port/api/peers/version

http://ip:port/api/peers/get

可以直接这么调用,要看具体对应的函数是否还有其他的参数要求,比如:/api/peers/get,按照restful的api设计原则,可以理解为是获得具体某个节点信息,那么总该给个id之类的限定条件

// 455行 对请求参数query进行验证,
// 验证规则是:object类型,属性ip_str要求长度不小于1的字符串,属性port要求0~65535之间的整数,并且都不能空(必需)
library.scheme.validate(query, {
        type: "object",
        properties: {
            ip_str: {
                type: "string",
                minLength: 1
            },
            port: {
                type: "integer",
                minimum: 0,
                maximum: 65535
            }
        },
        required: ['ip_str', 'port']
    }, function (err) {
        ...
        // 480行 从sqlite数据库里查询数据表peers
        privated.getByFilter({
            ...
        });
    });

在具体运行过程中,library就是app.js里传过来的scope,该参数包含的scheme代表了一个z_schema实例,z_schema是一个第三方组件,

我们应该这样请求,不然会返回错误信息。

http://ip:port/api/peers/get?ip_str=0.0.0.0&port=1234

节点保存

1.初始化节点

在一个P2P网络中,一个孤立的节点,在没有其他任何节点信息的情况下,仅仅靠网络扫描去寻找其他节点,将是一件很难完成的事情,更别提高效和安全了

在运行软件之前,初始化一些节点供联网使用,是最简单直接的解决方案。这个在配置文件config.json里,有直接体现:

// config.json 15行
"peers": {
        "list": [],
        "blackList": [],
        "options": {
                "timeout": 4000
        }
},
...

list的数据格式为:

[
    {
        ip: 0.0.0.0,
        port: 7000
    },
    ...
    // 当然,也可以在启动的时候,通过参数--peers 1.2.3.4:70001, 2.1.2.3:7002提供(代码见app.js47行)。
]

2. 写入节点

写入节点,就是持久化,或者保存到数据库,或者保存到某个文件。这里保存到sqlite3数据库里的peers表了

节点表格peers表的字段信息:id,ip,port,state,os,sharePort,version,clock

代码如下:

// peer.js 347行
Peer.prototype.onBlockchainReady = function () {
    async.eachSeries(library.config.peers.list, function (peer, cb) {
        library.dbLite.query("INSERT OR IGNORE INTO peers(ip, port, state, sharePort) VALUES($ip, $port, $state, $sharePort)", {
            ip: ip.toLong(peer.ip),
            port: peer.port,
            state: 2, //初始状态为2,都是健康的节点
            sharePort: Number(true)
        }, cb);
    }, function (err) {
        if (err) {
            library.logger.error('onBlockchainReady', err);
        }

        privated.count(function (err, count) {
            if (count) {
                privated.updatePeerList(function (err) {
                    err && library.logger.error('updatePeerList', err);
                    // 364行
                    library.bus.message('peerReady');
                })
                library.logger.info('Peers ready, stored ' + count);
            } else {
                library.logger.warn('Peers list is empty');
            }
        });
    });
}

当区块链(后面篇章分析)加载完毕的时候(触发事件),依次将配置的节点写入数据库,如果数据库已经存在相同的记录就忽略,然后更新节点列表,触发节点加载完毕事件。

这里对数据库Sqlite的插入操作,插入语句是library.dbLite.query("INSERT OR IGNORE INTO peers,有意思的是IGNORE操作字符串,是sqlite3支持的(见参考),当数据库有相同记录的时候,该记录被忽略,继续往下执行。

执行成功,就会调用library.bus.message('peerReady'),进而触发peerReady事件

3. 更新节点

事件onPeerReady函数,如下:

// peer.js 374行
Peer.prototype.onPeerReady = function () {
    setImmediate(function nextUpdatePeerList() {
        privated.updatePeerList(function (err) {
            err && library.logger.error('updatePeerList timer', err);
            setTimeout(nextUpdatePeerList, 60 * 1000);
        })
    });

    setImmediate(function nextBanManager() {
        privated.banManager(function (err) {
            err && library.logger.error('banManager timer', err);
            setTimeout(nextBanManager, 65 * 1000)
        });
    });
}

两个setImmediate函数的调用,一个循环更新节点列表,一个循环更新节点状态。

(1)循环更新节点状态很简单,循环更改state和clock字段,主要是将禁止的状态state=0,修改为1,看代码:

// 142行
privated.banManager = function (cb) {
    library.dbLite.query("UPDATE peers SET state = 1, clock = null where (state = 0 and clock - $now < 0)", {now: Date.now()}, cb);
}

(2)那么怎么updatePeerList循环更新节点列表呢,就是通过网络的随机节点来获取

privated.updatePeerList = function (cb) {
    // 53行 逐个随机的验证节点信息,并将其做删除和更新处理
    modules.transport.getFromRandomPeer({
        api: '/list',
        method: 'GET'
    }, function (err, data) {
        ...
        library.scheme.validate(data.body, {
                    ...
                    // 124行
                    self.update(peer, cb);
                });
            }, cb);
        });
    });
};

在getFromRandomPeer函数中是怎么运行的,看代码:

// transport.js 474行
Transport.prototype.getFromRandomPeer = function (config, options, cb) {
    ...

    // 481行 async.retry该方法就是要重复调用第一个task函数20次,有正确返回结果就传给回调函数
    async.retry(20, function (cb) {
        modules.peer.list(config, function (err, peers) {
            if (!err && peers.length) {
                var peer = peers[0];

                // 485行
                self.getFromPeer(peer, options, cb);
            } else {
                return cb(err || "No peers in db");
            }
        });
   ...
};

在getFromPeer函数中获取到一个可行节点的操作是怎样的:

// transport.js 500行
Transport.prototype.getFromPeer = function (peer, options, cb) {
    ...
    var req = {
        // 519行: 获得节点地址
        url: 'http://' + ip.fromLong(peer.ip) + ':' + peer.port + url,
        ...
    };

    // 532行: 使用`request`组件发送请求
    return request(req, function (err, response, body) {
        if (err || response.statusCode != 200) {
            ...
            if (peer) {
                if (err && (err.code == "ETIMEDOUT" || err.code == "ESOCKETTIMEDOUT" || err.code == "ECONNREFUSED")) {

                    // 542行: 对于无法请求的,自然要删除
                    modules.peer.remove(peer.ip, peer.port, function (err) {
                    ...
                    });
                } else {
                    if (!options.not_ban) {

                        // 549行: 对于状态码不是200的,比如304等禁止状态,就要更改其状态
                        modules.peer.state(peer.ip, peer.port, 0, 600, function (err) {
                        ...
                        });
                    }
                }
            }
            cb && cb(err || ('request status code' + response.statusCode));
            return;
        }

        ...
        if (port > 0 && port <= 65535 && response.headers['version'] == library.config.version) {
            // 595行: 一切问题都不存在
            modules.peer.update({
                ip: peer.ip,
                port: port,
                state: 2, // 598行: 看来健康的节点状态为2
                ...
    });
}

涉及的第三方组件参考:

z_schema组件: https://github.com/Ebookcoin/z_schema

dblite组件: https://github.com/Ebookcoin/dblite

request组件: http://github.com/request/request

SQL As Understood By SQLite: https://www.sqlite.org/lang_conflict.html

广播新区块

在广播交易中与广播新区块中都会用到的广播Broadcast功能:

// Public methods
Transport.prototype.broadcast = function (config, options, cb) {
    config.limit = config.limit || 1;
    modules.peer.list(config, function (err, peers) {
        if (!err) {
            async.eachLimit(peers, 3, function (peer, cb) {
                self.getFromPeer(peer, options);

                setImmediate(cb);
            }, function () {
                cb && cb(null, {body: null, peer: peers});
            })
        } else {
            cb && setImmediate(cb, err);
        }
    });
};

在签名、UTXO未确认交易、新区块、消息通知中,用到broadcast的情形:

Transport.prototype.onSignature = function (signature, broadcast) {
    if (broadcast) {
        self.broadcast({limit: 100}, {api: '/signatures', data: {signature: signature}, method: "POST"});
        library.network.io.sockets.emit('signature/change', {});
    }
}

Transport.prototype.onUnconfirmedTransaction = function (transaction, broadcast) {
    if (broadcast) {
        self.broadcast({limit: 100}, {api: '/transactions', data: {transaction: transaction}, method: "POST"});
        library.network.io.sockets.emit('transactions/change', {});
    }
}

Transport.prototype.onNewBlock = function (block, broadcast) {
    if (broadcast) {
        self.broadcast({limit: 100}, {api: '/blocks', data: {block: block}, method: "POST"});
        library.network.io.sockets.emit('blocks/change', {});
    }
}

Transport.prototype.onMessage = function (msg, broadcast) {
    if (broadcast) {
        self.broadcast({limit: 100, dappid: msg.dappid}, {api: '/dapp/message', data: msg, method: "POST"});
    }
}

DPOS共识

Delegate & Round & Accounts & Slots | Milestones

DPOS过程

  1. 注册受托人,并接受投票
    用户注册为受托人;
    接受投票(得票数排行前101位);

  2. 维持循环,调整受托人

    • 块周期:也称为时段周期(Slot),每个块需要10秒,为一个时段(Slot);
    • 受托人周期:或叫循环周期(Round),每101个区块为一个循环周期(Round)。这些块均由101个代表随机生成,每个代表生成1个块。一个完整循环周期大概需要1010秒(101x10),约16分钟;每个周期结束,前101名的代表都要重新调整一次;
    • 奖励周期:根据区块链高度,设置里程碑时间(Milestone),在某个时间点调整区块奖励。
    • 上述循环,块周期最小(10秒钟),受托人周期其次(16分钟),奖励周期最大(347天)。
  3. 循环产生新区块,广播

委托人生成区块的相关联代码段:(已省略vote投票相关代码,具体查看delegate.js)

privated.getBlockSlotData(currentSlot, lastBlock.height + 1, function (err, currentBlockData) {
        if (err || currentBlockData === null) {
            library.logger.log('Loop', 'skiping slot');
            return setImmediate(cb);
        }

        library.sequence.add(function (cb) {
            if (slots.getSlotNumber(currentBlockData.time) == slots.getSlotNumber()) {
                // 由选举得到的委托人生成一个区块
                modules.blocks.generateBlock(currentBlockData.keypair, currentBlockData.time, function (err) {
                    library.logger.log('Round ' + modules.round.calc(modules.blocks.getLastBlock().height) + ' new block id: ' + modules.blocks.getLastBlock().id + ' height: ' + modules.blocks.getLastBlock().height + ' slot: ' + slots.getSlotNumber(currentBlockData.time) + ' reward: ' + modules.blocks.getLastBlock().reward);
                    cb(err);
                });
            } else {
                // library.logger.log('Loop', 'exit: ' + _activeDelegates[slots.getSlotNumber() % constants.delegates] + ' delegate slot');
                setImmediate(cb);
            }
        }, function (err) {
            if (err) {
                library.logger.error("Failed to get block slot data", err);
            }
            setImmediate(cb);
        });
    });

Slot Round Milestone概念详解

本文整理自:

Nodejs开发区块链

EbookChain

上一篇下一篇

猜你喜欢

热点阅读