1. 概述
Quorum 是基于以太坊的 Golang 实现go-ethereum开发而来。 详细的可参考如下链接:
在 go-ethereum 基础上,Quorum 主要做了如下扩展:
- 隐私性(Privacy)
- 共识算法(Alternative Consensus Mechanisms)
- 节点的许可管理(Peer Permissioning)
- 更高的性能(Higher Performance)
2. 架构
Quorum 有两个组件:
- Quorum节点
- Tessera(Java)或者Constellation(Haskell)节点,注:建议使用 Tessera,Constellation 感觉要被废弃
整体架构如下:
Quorum 的本质,是使用密码学技术来防止交易方以外的人看到敏感数据。对于私有交易,会进行加密处理,公链(Quorum chain)上只存储加密后的数据的 hash 值,而私有交易的数据加密后将存储在链下,通过定制的一个模块(Tessera 或者 Constellation)在节点间安全的共享。状态数据库被分成私有状态数据库和公开状态数据库两类。网络中所有节点的公开状态,均完美达成状态共识,而私有状态数据库的情况有所不同,将不再保存整个全局私有状态数据库的状态,如下图所示:
3. 隐私性
3.1. 方案概述
(1)公开合约、交易
与 go-ethereum 基本保持一致
(2)私有合约、交易
通过 privateFor 参数标识合约为私有合约,参数的值为私有合约的其他参与者的公钥,如下所示:
var simple = simpleContract.new(42, {from:web3.eth.accounts[0], data: bytecode, gas: 0x47b760, privateFor: ["ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc="]}, function(e, contract) {
if (e) {
console.log("err creating contract", e);
} else {
if (!contract.address) {
console.log("Contract transaction send: TransactionHash: " + contract.transactionHash + " waiting to be mined...");
} else {
console.log("Contract mined! Address: " + contract.address);
console.log(contract);
}
}
});
通过 privateFor 参数标识交易为私有交易,参数的值为私有交易的其他参与者的公钥,如下所示:
{
"jsonrpc": "2.0",
"method": "eth_sendTransaction",
"params": [
{
"from": "$FROM_AC",
"to": "$TO_AC",
"data": "$CODEHASH",
"privateFor": [
"$PUBKEY1,PUBKEY2"
]
}
],
"id": "$ID"
}
3.2. 案例一
七个节点的例子:7nodes
(1)使用 docker-compose 部署好区块链:
$git clone https://github.com/jpmorganchase/quorum-examples
$cd quorum-examples
$docker-compose up -d
$docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
19522838f213 quorumengineering/quorum:2.2.1 "/bin/sh -c 'UDS_WAI…" 30 minutes ago Up 30 minutes (healthy) 8546/tcp, 21000/tcp, 30303/tcp, 50400/tcp, 30303/udp, 0.0.0.0:22006->8545/tcp quorum-examples_node7_1
6c05e4441202 quorumengineering/quorum:2.2.1 "/bin/sh -c 'UDS_WAI…" 30 minutes ago Up 28 minutes (healthy) 8546/tcp, 21000/tcp, 30303/tcp, 50400/tcp, 30303/udp, 0.0.0.0:22002->8545/tcp quorum-examples_node3_1
e39674824a33 quorumengineering/quorum:2.2.1 "/bin/sh -c 'UDS_WAI…" 30 minutes ago Up 30 minutes (healthy) 8546/tcp, 21000/tcp, 30303/tcp, 50400/tcp, 30303/udp, 0.0.0.0:22001->8545/tcp quorum-examples_node2_1
6df304600bde quorumengineering/quorum:2.2.1 "/bin/sh -c 'UDS_WAI…" 30 minutes ago Up 30 minutes (healthy) 8546/tcp, 21000/tcp, 30303/tcp, 50400/tcp, 30303/udp, 0.0.0.0:22005->8545/tcp quorum-examples_node6_1
3e05a8e19444 quorumengineering/quorum:2.2.1 "/bin/sh -c 'UDS_WAI…" 30 minutes ago Up 30 minutes (healthy) 8546/tcp, 21000/tcp, 30303/tcp, 50400/tcp, 30303/udp, 0.0.0.0:22004->8545/tcp quorum-examples_node5_1
d0c109e234a7 quorumengineering/quorum:2.2.1 "/bin/sh -c 'UDS_WAI…" 30 minutes ago Up 30 minutes (healthy) 8546/tcp, 21000/tcp, 30303/tcp, 50400/tcp, 30303/udp, 0.0.0.0:22003->8545/tcp quorum-examples_node4_1
d246eaea26f6 quorumengineering/quorum:2.2.1 "/bin/sh -c 'UDS_WAI…" 30 minutes ago Up 28 minutes (healthy) 8546/tcp, 21000/tcp, 30303/tcp, 50400/tcp, 30303/udp, 0.0.0.0:22000->8545/tcp quorum-examples_node1_1
764e0941d9b3 quorumengineering/tessera:0.8 "/bin/sh -c 'DDIR=/q…" 31 minutes ago Up 30 minutes (healthy) 9000/tcp, 0.0.0.0:9087->9080/tcp quorum-examples_txmanager7_1
54bc0ed8a974 quorumengineering/tessera:0.8 "/bin/sh -c 'DDIR=/q…" 31 minutes ago Up 30 minutes (healthy) 9000/tcp, 0.0.0.0:9086->9080/tcp quorum-examples_txmanager6_1
5495f660a2d2 quorumengineering/tessera:0.8 "/bin/sh -c 'DDIR=/q…" 31 minutes ago Up 30 minutes (healthy) 9000/tcp, 0.0.0.0:9084->9080/tcp quorum-examples_txmanager4_1
ec124de173a8 quorumengineering/tessera:0.8 "/bin/sh -c 'DDIR=/q…" 31 minutes ago Up 30 minutes (healthy) 9000/tcp, 0.0.0.0:9082->9080/tcp quorum-examples_txmanager2_1
faa801660985 quorumengineering/tessera:0.8 "/bin/sh -c 'DDIR=/q…" 31 minutes ago Up 30 minutes (healthy) 9000/tcp, 0.0.0.0:9085->9080/tcp quorum-examples_txmanager5_1
a2cf351d787a quorumengineering/tessera:0.8 "/bin/sh -c 'DDIR=/q…" 31 minutes ago Up 30 minutes (healthy) 9000/tcp, 0.0.0.0:9083->9080/tcp quorum-examples_txmanager3_1
7931f1f1c14e quorumengineering/tessera:0.8 "/bin/sh -c 'DDIR=/q…" 31 minutes ago Up 30 minutes (healthy) 9000/tcp, 0.0.0.0:9081->9080/tcp quorum-examples_txmanager1_1
(2)在节点 1 和节点 7 之间部署私有智能合约
attach 到节点 1 上部署智能合约,其中 privateFor 为节点 7 的公钥:
a = eth.accounts[0]
web3.eth.defaultAccount = a;
// abi and bytecode generated from simplestorage.sol:
// > solcjs --bin --abi simplestorage.sol
var abi = [{"constant":true,"inputs":[],"name":"storedData","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"retVal","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[{"name":"initVal","type":"uint256"}],"payable":false,"type":"constructor"}];
var bytecode = "0x6060604052341561000f57600080fd5b604051602080610149833981016040528080519060200190919050505b806000819055505b505b610104806100456000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632a1afcd914605157806360fe47b11460775780636d4ce63c146097575b600080fd5b3415605b57600080fd5b606160bd565b6040518082815260200191505060405180910390f35b3415608157600080fd5b6095600480803590602001909190505060c3565b005b341560a157600080fd5b60a760ce565b6040518082815260200191505060405180910390f35b60005481565b806000819055505b50565b6000805490505b905600a165627a7a72305820d5851baab720bba574474de3d09dbeaabc674a15f4dd93b974908476542c23f00029";
var simpleContract = web3.eth.contract(abi);
var simple = simpleContract.new(42, {from:web3.eth.accounts[0], data: bytecode, gas: 0x47b760, privateFor: ["ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc="]}, function(e, contract) {
if (e) {
console.log("err creating contract", e);
} else {
if (!contract.address) {
console.log("Contract transaction send: TransactionHash: " + contract.transactionHash + " waiting to be mined...");
} else {
console.log("Contract mined! Address: " + contract.address);
console.log(contract);
}
}
});
部署的智能合约为一个简单的数据存取的合约:
pragma solidity ^0.4.15;
contract simplestorage {
uint public storedData;
function simplestorage(uint initVal) {
storedData = initVal;
}
function set(uint x) {
storedData = x;
}
function get() constant returns (uint retVal) {
return storedData;
}
}
(3)对于公开交易,则直接在 Quorum 节点间完成,与 go-ethereum 基本一致,如下:
(4)对于私有交易 例如节点 1 上 set(4),分别在节点 4 和节点 7 上 get()则有不同现象:
// Node 1
> private.set(4,{from:eth.accounts[0],privateFor:["ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc="]});
"0x678f838f0e05187228ea3c890f1feeff2d0e948da5de699392b0dcec3c0eee59"
> private.get()
4
// Node 4
> private.get()
0
// Node 7
> private.get()
4
可以发现,私有交易的双方,即节点 1 和 7 都能正确获取,而非交易方节点 4 则无法正确获取,从而实现交易的隐私性。若在这三个节点上查询此交易 0x678f838f0e05187228ea3c890f1feeff2d0e948da5de699392b0dcec3c0eee59,会发现都能查到,只不过交易的 input 参数的值不再是原始交易的 payload,而是加密后的 payload 的 hash 值,且 value 是一个特殊的值,用来标识此交易为私有交易。如下图所示:
交易真实的 payload 会通过 TransactionManager 安全的在节点间共享,Quorum 节点通过加密后的 payload 的 hash 值作为索引在与之关联的Tessera或者Constellation节点上获取解密后的 payload,从而执行交易。如下图所示:
3.3. 案例二
在这个案例中,A 机构和 B 机构构成了私有交易 AB 的交易双方,而 C 机构不参与该交易。
- A 机构发出一笔私有交易 AB 到 Quorum 节点,指定交易的有效负载,并为 A 机构和 B 机构设定 privateFor 参数,形成其公钥;
- A 机构的 Quorum 节点发送交易到与之配对的事务管理器,为其请求保存交易的有效负载;
- A 机构的事务管理器向与之相关的 Enclave 发出请求,验证发送者并加密有效负载;
- A 机构的 Enclave 检验 A 机构的私钥,一旦验证通过,就会进行交易对话。这就需要:
i. 生成系统密钥和随机数;
ii. 对交易有效负载和来自 i)项中的系统密钥进行加密;
iii. 计算 ii)项中已加密有效负载的 SHA3-512 哈希值;
iv. 遍历交易参与方列表(本例中为 A 机构和 B 机构),加密 i)项中的系统密钥和参与者的公钥(PGP 加密);
v. 向事务管理器返回 ii)项中的已加密有效负载、iii)项中的哈希值,iv)项中每个参与者的加密密钥。 - 然后,A 机构的事务管理器使用哈希作为索引,保存已加密有效负载(使用系统密钥加密)和已加密系统密钥,再将哈希值、已加密有效负载和与 B 机构公钥加密而成的已加密系统密钥等安全地发送给事务管理器。B 机构的事务管理器使用 Ack/Nack 进行响应。需要注意的是,如果 A 机构并未从 B 机构处收到响应或 Nack,那么交易将不会传播到整个网络。它是参与者保存通讯有效负载的前提条件。
- 一旦数据成功传到 B 机构的事务管理器,A 机构的事务管理器向 Quorum 节点返回哈希值,该节点就使用哈希值替换交易的初始负载,修改交易的 V 值为 37 或 38,这将对其他节点进行提示,该哈希值表示一个与已加密有效负载相关的私有交易;相反,则提示一个无意义字节码相关的公开交易;
- 接下来,交易将基于标准的以太坊 P2P 协议,传播到剩余网络中;
- 生成一个包含交易 AB 的区块,并且发散到网络中的每一个机构;
- 处理区块时,所有机构都将处理交易。每一个 Quorum 节点将认识到一个 37 或 38 的 V 值,表明交易的有效负载需要加密、需要联系他们本地的事务管理器,从而决定他们是否需要同意这笔交易(使用哈希值作为索引进行查找);
- 由于 C 机构并未同意这笔交易,它将收到一条 NotARecipient 的消息,并将忽略该交易——它将不会升级其私有状态数据库。A 机构和 B 机构将会在他们本地的事务管理器中查找哈希值,识别他们的确赞成该交易,然后每位参与者与其对应的 Enclave 进行通讯,发送已加密有效负载、已加密系统密钥和签名;
- Enclave 验证签名,然后使用 Enclave 中该机构的私钥来解密系统密钥,使用刚刚显示的系统密钥解密交易有效负载,并向事务管理器返回已加密有效负载;
- 机构 A 和 B 的事务管理器,通过执行合约代码,向 EVM 发送已解密有效负载。这次执行将会升级仅在 Quorum 节点的私有状态数据库的状态。注意:代码一旦运行即会无效,所以没有上述流程它将无法阅读。
3.4. 实现细节
注:以 Tessera 为例分析
3.4.1. Quorum 组件
Quorum 组件基于 go-ethereum 修改:
- 共识算法,增加 Raft 和 IBFT 共识
- P2P 网络层,改成只有授权节点才能连入或连出网络
- 区块生成逻辑,由检查“全局状态根”改为检查“全局公开状态根”
- 区块验证逻辑,在区块头,将“全局状态根”替换成“全局公开状态根”
- 状态树,分成公开状态树和私有状态树
- 区块链验证逻辑,改成处理“私有事务”
- 创建事务,改成允许交易数据被加密哈希替代,以维护必需的隐私数据
- 删除以太坊中 Gas 的定价,尽管保留 Gas 本身
当发送私有交易时,即添加 privateFor 参数时,Quorum 节点会检测到这个是一个私有交易,如下:
// SendTransaction will create a transaction from the given arguments and
// tries to sign it with the key associated with args.To. If the given passwd isn't
// able to decrypt the key it fails.
func (s *PrivateAccountAPI) SendTransaction(ctx context.Context, args SendTxArgs, passwd string) (common.Hash, error) {
// Look up the wallet containing the requested signer
account := accounts.Account{Address: args.From}
wallet, err := s.am.Find(account)
if err != nil {
return common.Hash{}, err
}
if args.Nonce == nil {
// Hold the addresse's mutex around signing to prevent concurrent assignment of
// the same nonce to multiple accounts.
s.nonceLock.LockAddr(args.From)
defer s.nonceLock.UnlockAddr(args.From)
}
isPrivate := args.PrivateFor != nil
if isPrivate { // Quorum节点会检测到这个是一个私有交易
data := []byte(*args.Data)
if len(data) > 0 {
log.Info("sending private tx", "data", fmt.Sprintf("%x", data), "privatefrom", args.PrivateFrom, "privatefor", args.PrivateFor)
// 向Tessera组件发送交易数据,Tessera组件返回加密后的交易数据的hash值
data, err := private.P.Send(data, args.PrivateFrom, args.PrivateFor)
log.Info("sent private tx", "data", fmt.Sprintf("%x", data), "privatefrom", args.PrivateFrom, "privatefor", args.PrivateFor)
if err != nil {
return common.Hash{}, err
}
}
// zekun: HACK
d := hexutil.Bytes(data)
args.Data = &d
}
// Set some sanity defaults and terminate on failure
if err := args.setDefaults(ctx, s.b); err != nil {
return common.Hash{}, err
}
// Assemble the transaction and sign with the wallet
tx := args.toTransaction()
var chainID *big.Int
if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) && !isPrivate {
chainID = config.ChainID
}
signed, err := wallet.SignTxWithPassphrase(account, passwd, tx, chainID)
if err != nil {
return common.Hash{}, err
}
return submitTransaction(ctx, s.b, signed, isPrivate)
}
Quorum 节点就会向 Tessera 组件发送交易数据,Tessera 组件返回加密后的交易数据的 hash 值。
func (g *Constellation) Send(data []byte, from string, to []string) (out []byte, err error) {
if g.isConstellationNotInUse {
return nil, ErrConstellationIsntInit
}
out, err = g.node.SendPayload(data, from, to)
if err != nil {
return nil, err
}
g.c.Set(string(out), data, cache.DefaultExpiration)
return out, nil
}
Quorum 节点与 Tessera 的通讯是使用的基于 Unix Domain Socket 的 Private API
func (c *Client) SendPayload(pl []byte, b64From string, b64To []string) ([]byte, error) {
buf := bytes.NewBuffer(pl)
req, err := http.NewRequest("POST", "http+unix://c/sendraw", buf)
if err != nil {
return nil, err
}
if b64From != "" {
req.Header.Set("c11n-from", b64From)
}
req.Header.Set("c11n-to", strings.Join(b64To, ","))
req.Header.Set("Content-Type", "application/octet-stream")
res, err := c.httpClient.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("Non-200 status code: %+v", res)
}
return ioutil.ReadAll(base64.NewDecoder(base64.StdEncoding, res.Body))
}
这样以后,在 Quorum 链上存储的就是加密后的私有交易的 hash 值,而实际的交易内容则被 Tessera 安全的存储在 DB 内。
当出块时,在CommitTransactions阶段会进行交易的执行,如下:
其中 tx 的执行在TransitionDb中:
// TransitionDb will transition the state by applying the current message and
// returning the result including the the used gas. It returns an error if it
// failed. An error indicates a consensus issue.
func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {
if err = st.preCheck(); err != nil {
return
}
msg := st.msg
sender := vm.AccountRef(msg.From())
homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber)
contractCreation := msg.To() == nil
isQuorum := st.evm.ChainConfig().IsQuorum
var data []byte
isPrivate := false
publicState := st.state
// 私有交易
if msg, ok := msg.(PrivateMessage); ok && isQuorum && msg.IsPrivate() {
isPrivate = true
// 向Tessera发起请求获取解密后的交易数据
data, err = private.P.Receive(st.data)
// Increment the public account nonce if:
// 1. Tx is private and *not* a participant of the group and either call or create
// 2. Tx is private we are part of the group and is a call
if err != nil || !contractCreation {
publicState.SetNonce(sender.Address(), publicState.GetNonce(sender.Address())+1)
}
if err != nil {
return nil, 0, false, nil
}
} else {
data = st.data
}
// Pay intrinsic gas
gas, err := IntrinsicGas(st.data, contractCreation, homestead)
if err != nil {
return nil, 0, false, err
}
if err = st.useGas(gas); err != nil {
return nil, 0, false, err
}
var (
evm = st.evm
// vm errors do not effect consensus and are therefor
// not assigned to err, except for insufficient balance
// error.
vmerr error
)
if contractCreation {
ret, _, st.gas, vmerr = evm.Create(sender, data, st.gas, st.value)
} else {
// Increment the account nonce only if the transaction isn't private.
// If the transaction is private it has already been incremented on
// the public state.
if !isPrivate {
publicState.SetNonce(msg.From(), publicState.GetNonce(sender.Address())+1)
}
var to common.Address
if isQuorum {
to = *st.msg.To()
} else {
to = st.to()
}
//if input is empty for the smart contract call, return
if len(data) == 0 && isPrivate {
return nil, 0, false, nil
}
ret, st.gas, vmerr = evm.Call(sender, to, data, st.gas, st.value)
}
if vmerr != nil {
log.Info("VM returned with error", "err", vmerr)
// The only possible consensus-error would be if there wasn't
// sufficient balance to make the transfer happen. The first
// balance transfer may never fail.
if vmerr == vm.ErrInsufficientBalance {
return nil, 0, false, vmerr
}
}
st.refundGas()
st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
if isPrivate {
return ret, 0, vmerr != nil, err
}
return ret, st.gasUsed(), vmerr != nil, err
}
如果是私有交易,Quorum 节点则会向 Tessera 发起请求,以 Quorum 链上存储的 data 为 key,即之前加密后的交易数据的 hash 值,获取解密后的交易的实际数据,然后在 evm 中执行交易。
3.4.2. Tessera 组件
Tessera 组件是 Transaction Manager 的 java 实现,详见:https://github.com/jpmorganchase/tessera/wiki
由两个部分组成:
- Transaction Manager:负责事务隐私,存储并允许访问加密的交易数据,与其他参与方的事务管理器交换加密的有效载荷,但没有访问任何敏感私钥的权限。它用 Enclave 来加密
- Enclave:分布式账本协议,通常利用密码技术来保证事务真实性、参与者身份验证和历史数据存储(即通过加密哈希数据链)。为了实现相关事务的隔离,同时通过特定加密的并行操作来提供性能优化,包括系统密钥生成和数据加解密的大量密码学工作,会委托给 Enclave。
4. 共识算法
4.1. Raft
Quorum 采用了基于 Raft 的共识机制(使用 etcd 的 Raft 实现),而不是以太坊默认的 PoW 方案。这对于不需要拜占庭容错并且需要更快出块时间(以毫秒而非秒为单位)和事务结束(不存在分支)的封闭式成员资格/联盟设置非常有效。
在以太坊中,任意节点都可以作为区块的打包者,只要其在一轮 pow 中胜出。我们知道 Quorum 的节点沿用了以太坊的设计和代码。所以为了连接以太坊节点和 Raft 共识,Quorum 采用了网络节点和 Raft 节点一对一的方式来实现 Raft-based 共识。当一笔 TX 诞生后,TX 会在以太坊的 P2P 网络中传输。同时,Raft 的 leader 竞选一直在同步进行。当前 leader 节点对应的以太坊节点收到 TX 时,以太坊节点就会将 TX 打包成区块并将区块通过 Raft 节点发送给 Raft 网络上的 follower。follower 节点收到区块后将区块交给对应的以太坊节点。然后以太坊节点将区块记录到链上。
与以太坊不同的是,当一个节点收到区块后并不会马上记录到链上。而是等 Raft 网络中的 leader 收到所有 follower 的确认回执后,广播一个执行的消息。然后所有收到执行消息的 follower 才会将区块记录在本地的链上。
4.1.1. Lifecycle of a Transaction
- 客户端发起一笔 TX 并通过 RPC 来呼叫节点。
- 节点通过以太坊的 P2P 协议将节点广播给网络。
- 当前的 Raft leader 对应的以太坊节点收到了 TX 后将 TX 打包成区块。
- 区块被 RLP 编码后传递给对应的 Raft leader。
- leader 收到区块后通过 Raft 算法将区块传递给 follower。这包括如下步骤:
- leader 发送 AppendEntries 指令给 follower。
- follower 收到这个包含区块信息的指令后,返回确认回执给 leader。
- leader 收到不少于指定数量的确认回执后,发送确认 append 的指令给 follower。
- follower 收到确认 append 的指令后将区块信息记录到本地的 Raft log 上。
- Raft 节点将区块传递给对应的 Quorum 节点。Quorum 节点校验区块的合法性,如果合法则记录到本地链上。
4.1.2. Block Race
通常情况下,每一个被传至 Raft 的区块最终都会被添加到链上。但是也会有意外出现。比如因为一些网络的原因,某个 leader 无法与大部分的 follower 进行交互了。这时其他 follower 就会推选出新的 leader。在这期间,旧的 leader 还会产生新的区块。但是因为没有收到足量的 follower 回执,所以它产生的区块都不会最终写到链上。与之相对的,新的 leader 这边则会正常进行区块同步。一旦旧 leader 这边恢复通信,它会将自己产生的 AppendEntries 指令广播出去。由于其发出的指令已经过时了,所以大部分的 follower 不会给予这些指令正确的回执。
具体流程如下:
- Node1 作为 leader 产生一个新的区块:[0x002, parent: 0x001]。这个区块的父块是编号为 0x001 的区块。Node1 通过 Raft 将这个区块进行共识。
- 0X002 区块共识成功后网络出现了问题,Node1 无法与大部分的 follower 进行通信。
- 网络问题并没有影响 Node1 的产块。一个新的区块被产出:[0x003, parent: 0x002]。为了共识这个新的区块,Node1 向 Raft 网络发送 AppendEntries 指令(指令中包含新区块的信息),并等待 follower 的确认回执。因为网络问题,Node1 一直没有收到足够数量的 follower 回执。
- 于此同时,那些无法与 Node1 通信的 follower 因为长时间没收到 leader 的心跳,所以推选出了新的 leader:Node2。
- Node2 产生区块[0x004, parent: 0x002] 后将含此区块信息的 AppendEntries 指令发送给 follower。follower 确认这个指令后返回确认回执。最终这个指令被执行并记录在 Raft log 中。
- 0x004 区块共识完成后网络状态得到恢复。此时第三步中的来自 Node1 的 AppendEntries 指令终于被传递给大部分的 follower。但是此时 follower 的链上的最终块已经是第五步中的 0x004,所以区块 [0x003, parent: 0x002] 无法被执行,因为其 parent 是 0x002 不满足当前链的状态。这条不执行的动作也会被记录到 Raft log 中去。
- Node2 继续生成区块 [0x005, parent: 0x004]。
- 最后整个流程下来,follower 的 Raft log 内容大致会长这样:
得到区块[0x002, parent: 0x001, sender: Node1] - 执行
得到区块[0x004, parent: 0x002, sender: Node2] - 执行
得到区块[0x003, parent: 0x002, sender: Node1] - 不执行
得到区块[0x005, parent: 0x004, sender: Node2] - 执行
需要注意的是,整个共识过程中,Raft 层面只负责记录自己节点的 Raft log。真正执行 log 内容的是 Quorum 节点。Quorum 节点根据其节点对应的 Raft log 来做具体的操作。
4.1.3. Speculative Minting
一个区块从被创建,到经过 Raft 同步,到最后记录到链上多多少少会经历一段时间(尽管非常短)。如果等上一个区块写入到链上以后下一个区块才能生成,那么就会使得 TX 的确认时间增长。为了解决这个问题,同时为了能更有效率的处理区块生成,Quorum 推出了 speculative minting 机制。在这种机制下,新区块可以在其父区块没有完全上链的情况下被创建。如果这个场景重复出现,那么就会出现一串未被上链的区块,这些区块都会有指向其父区块的索引,我们将这类区块串称为 speculative chain。
在维护 speculative chain 的同时,系统还会维护一份被称作 proposedTxes 的数组。这份数组包含了所有 speculative chain 中的 TX。主要是为了记录已经被传输到 Raft 中但是还没被正式上链的交易。防止同一条交易被重复打包。
4.2. IBFT
详见:https://github.com/ethereum/EIPs/issues/650
5. 节点的许可管理
节点的授权,是用来控制哪些节点可以连接到指定节点、以及可以从哪些指定节点拨出的功能。目前,当启动节点时,通过–permissioned 参数在节点级别处进行管理。
如果设置了–permissioned 参数,节点将查找名为/permissioned-nodes.json 的文件。此文件包含此节点可以连接并接受来自其连接的 enodes 白名单。因此,启用权限后,只有 permissioned-nodes.json 文件中列出的节点成为网络的一部分。 如果指定了–permissioned 参数,但没有节点添加到 permissioned-nodes.json 文件,则该节点既不能连接到任何节点也不能接受任何接入的连接。
permissioned-nodes.json 文件的格式如下所示
[
"enode://remoteky1@ip1:port1",
"enode://remoteky1@ip2:port2",
"enode://remoteky1@ip3:port3"
]
在 geth 建立 p2p 连接的时候,如果启用了节点的许可管理,则会调用 isNodePermissioned 方法去检查目标节点是否被授权,如下所示:
func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *discover.Node) error {
...
//START - QUORUM Permissioning
currentNode := srv.NodeInfo().ID
cnodeName := srv.NodeInfo().Name
clog.Trace("Quorum permissioning",
"EnableNodePermission", srv.EnableNodePermission,
"DataDir", srv.DataDir,
"Current Node ID", currentNode,
"Node Name", cnodeName,
"Dialed Dest", dialDest,
"Connection ID", c.id,
"Connection String", c.id.String())
if srv.EnableNodePermission {
clog.Trace("Node Permissioning is Enabled.")
node := c.id.String()
direction := "INCOMING"
if dialDest != nil {
node = dialDest.ID.String()
direction = "OUTGOING"
log.Trace("Node Permissioning", "Connection Direction", direction)
}
if !isNodePermissioned(node, currentNode, srv.DataDir, direction) {
return nil
}
} else {
clog.Trace("Node Permissioning is Disabled.")
}
//END - QUORUM Permissioning
...
}
在 isNodePermissioned 中则会遍历目标节点是否在 permissioned-nodes.json 的节点列表内,如下:
// check if a given node is permissioned to connect to the change
func isNodePermissioned(nodename string, currentNode string, datadir string, direction string) bool {
var permissionedList []string
nodes := parsePermissionedNodes(datadir)
for _, v := range nodes {
permissionedList = append(permissionedList, v.ID.String())
}
log.Debug("isNodePermissioned", "permissionedList", permissionedList)
for _, v := range permissionedList {
if v == nodename {
log.Debug("isNodePermissioned", "connection", direction, "nodename", nodename[:NODE_NAME_LENGTH], "ALLOWED-BY", currentNode[:NODE_NAME_LENGTH])
return true
}
}
log.Debug("isNodePermissioned", "connection", direction, "nodename", nodename[:NODE_NAME_LENGTH], "DENIED-BY", currentNode[:NODE_NAME_LENGTH])
return false
}
6. 更高的性能
6.1. TPS 测试
benchmark: https://github.com/drandreaskrueger/chainhammer 对比结果:
hardware | node type | #nodes | config | peak TPS_av | final TPS_av |
---|---|---|---|---|---|
t2.micro | parity aura | 4 | (D) | 45.5 | 44.3 |
t2.large | parity aura | 4 | (D) | 53.5 | 52.9 |
t2.xlarge | parity aura | 4 | (J) | 57.1 | 56.4 |
t2.2xlarge | parity aura | 4 | (D) | 57.6 | 57.6 |
t2.micro | parity instantseal | 1 | (G) | 42.3 | 42.3 |
t2.xlarge | parity instantseal | 1 | (J) | 48.1 | 48.1 |
t2.2xlarge | geth clique | 3+1 +2 | (B) | 421.6 | 400.0 |
t2.xlarge | geth clique | 3+1 +2 | (B) | 386.1 | 321.5 |
t2.xlarge | geth clique | 3+1 | (K) | 372.6 | 325.3 |
t2.large | geth clique | 3+1 +2 | (B) | 170.7 | 169.4 |
t2.small | geth clique | 3+1 +2 | (B) | 96.8 | 96.5 |
t2.micro | geth clique | 3+1 | (H) | 124.3 | 122.4 |
t2.micro SWAP | quorum crux IBFT | 4 | (I) SWAP! | 98.1 | 98.1 |
t2.micro | quorum crux IBFT | 4 | (F) | lack of RAM | |
t2.large | quorum crux IBFT | 4 | (F) | 207.7 | 199.9 |
t2.xlarge | quorum crux IBFT | 4 | (F) | 439.5 | 395.7 |
t2.xlarge | quorum crux IBFT | 4 | (L) | 389.1 | 338.9 |
t2.2xlarge | quorum crux IBFT | 4 | (F) | 435.4 | 423.1 |
c5.4xlarge | quorum crux IBFT | 4 | (F) | 536.4 | 524.3 |
(1)Raft
- 1000 transactions
- multi-threaded with 23 workers
- average TPS around 160 TPS
- 20 raft blocks per second)
(2)IBFT
- 20 millions gasLimit
- 1 second istanbul.blockperiod
- 20000 transactions
- multi-threaded with 23 workers
- Initial average >400 TPS then drops to below 300 TPS