https://developers.libra.org/docs/assets/papers/libra-consensus-state-machine-replication-in-the-libra-blockchain.pdf
2019-10
文章目录
1. Introduction
Libra区块链的核心是共识协议LibraBFT - 通过它来排序和落实交易(transaction)。LibraBFT将信任分散到参与共识的一组validator节点。LibraBFT保证诚实的(honest)validators就交易历史达成一致,即使在部分参与者是Byzantine(有错、恶意)时也保证安全。LibraBFT基于已被证明的分布式计算基础,并将扩展到互联网级别。
最初validator节点需要批准才能加入,未来只要拥有Libra币就可以加入。
Key technical approach. LibraBFT按轮次/round推进。每一轮从所有validator节点选出一个leader。Leader propose新block(包含transactions),发给其余validator投票(vote)批准。Leader搜集到多数vote时将结果发给其余validator。Leader不能propose或搜集到多数vote时,由timeout机制强制产生新的轮次,选出新的leader。最终当一个block满足LibraBFT的commit rule时,它被提交(commit),链由此得到延伸。
2. Overview and Definitions
2.1. State Machine Replication
SMR协议定义一个抽象的、分布式的状态机(state machine),在各网络节点之间复制(replicate)。
SMR协议开始于初始execution state。每个节点可以提交commands并观察到一系列commits。Commit -> 执行command -> 再次commit -> …。
假定command的执行是deterministic的,希望达到:
- (safety) 所有诚实节点看到相同顺序的commit
- (liveness) 提交comand能产生commit
注意节点以相同顺序但不要求同一时间 看到一组commit。
2.2. Epochs
现实中,参与协议的节点会随时间而变。LibraBFT通过epoch来支持它:
- 每个epoch始于上个epoch的execution state
- execution state里包含epoch_id 用来标志当前epoch
- 当一个增加epoch_id的command commit时,当前epoch结束,新的epoch开始
2.3. Byzantine Fault Tolerance
假定每个epoch里有固定数量的恶意节点称为Byzantine node,其他节点是honest node。总的投票权(voting power)= N,每个节点α的投票权 V(α) ≥ 0。假定一个安全阈值(security threshold)f 满足 N > 3f(例如 f = (N - 1) / 3)。
所有分析均基于以下假定(BFT assumption):
- (bft-assumption) 任一epoch 所有恶意节点的投票权之和 一定不超过 f
一组投票权之和M 满足 M ≥ N - f 的节点形成一个quorum。
Lemma B1: 满足bft-assumption前提下,一个epoch里的任意2个quorum:一定存在一个honest node 同时存在于这2个quorum里
2.4. Cryptographic Assumptions
假定hash函数和数字签名方案安全。
假定没有hash 冲突,即 hash(m1) = hash(m2) 意味着 m1 = m2。
2.5. Networking Assumptions and Honest Crashes
BFT assumption保证了safety,但liveness还需要其他假定。首先假定网络状况时好(synchrony)时坏(asynchrony)。只有足够长时期的synchrony才能保证liveness。
Asynchrony期间,消息(message)可以丢失/任意延时、honest node可以宕机/重启。Synchrony期间假定honest node之间message延时存在一个上限 δ(此时honest node不会宕机)。
为了简化分析,分成2个时期:GST之前(before GST)和GST之后(after GST)(GST = global stabilization time)。第8章会推导GST之后系统产生一个commit所需的最大时间。
更正式地表述:
- (eventually-synchronous-network) GST之后,honest node之间的所有message传递时间小于某未知值 δ(> 0)
- (eventually-no-crash) GST之后,honest node会响应,不会宕机
注:目前没有考虑message处理所需时间(目前LibraBFT message不限大小)。
2.6. Leaders, Rounds, Blocks, Votes
LibraBFT是leader-based的:每一轮选出一个leader。Leader负责发起proposal、搜集其他节点的proposal(签名后称为vote)。Leader proposal 形成新的block(通过加密hash)添加到链上。
3. Integration with the Libra Blockchain
3.1. Consensus Protocol
期望LibraBFT用在Libra区块链中:
- Validator节点参与LibraBFT,以安全地复制区块链的状态(这部分代码称为SMR模块)
- 从SMR模块的角度,command和execution state都是不透明的。Command被完全委托给执行模块来执行
- 重要地,epoch内 投票权的计算 也被SMR模块委托给系统的其他部分(通过与管理epoch相同的回调)
- 每次一个command被执行时,执行模块会得到一个时间值 - 该值在所有执行该command的节点间保证一致
- SMR模块看到的execution state未必是实际的区块链数据(可能是轻量的例如hash值)。每条commit的command要在每个validator节点本地至少执行一次。未来LibraBFT可能增加在节点间同步本地存储的最近execution state的机制
3.2. Libra Clients
LibraBFT的设计通常不关心 validator节点如何与Libra client交互。但可以观察到:
- client提交的transaction先由validator节点通过mempool 协议共享。Leader在需要propose时从mempool拉取transaction
- 为方便client验证链的状态,节点用签名来证明特定execution state正被提交 - 此签名可以LibraBFT协议无关的方式来验证,只要client知道对应的validator keys(4.1节描述commitments如何与共识数据一起创建)
- 由上,通过与validator的交互,client应该可以获取验证所需的validator keys。未来会提供专门的安全协议来获取
3.3. Security
额外的安全考虑:
- 参与者需要能限制资源的消耗(因为恶意节点的存在)。第4章会描述数据交换层如何让接收者控制数据量
- 节点的经济激励须与SMR协议的安全和性能相关
- Leader可能遭受DoS攻击。Pacemaker(第9章)介绍一种随机选取leader的方式(VRF)。未来需要能更多地选择更安全的节点成为leader
Note on Fairness. 除了safety和liveness,SMR系统也经常讨论fairness。传统定义是honest node提交的每个有效command 最终都要被commit。但这在Libra中通常不那么重要,因为transaction首先进入mempool并按交易费来拍卖。
4. Consensus Data and Networking
2.6节描述了LibraBFT的核心概念,第5章会更具体地描述。这里首先定义数据结构 record - 节点之间交换、以及存储的对象。同时也讨论同步record的通讯框架。
4.1. Records
LibraBFT节点核心状态包括一组record。Record在本地存储,在网络上不停交换。有4种record:
- blocks,round leader用来propose 待执行的command
- votes,节点用来选block及其execution state
- quorum certificates (QCs),所有投票里的一个quorum(for 一个block及其execution state),也可包含client使用的commitment
- timeout,节点用来表示其当前round已经超时
所有record都由其author 签名。Block是链式的:包含前一个block的QC的hash值(除了epoch首个block 使用一个固定值hinit)。Vote和QC包含block及其execution state的hash。按照epoch的投票权,针对同一execution state投票形成quorum时(2.3节)创建QC。QC包含了所选vote的签名。
Data structures. 使用Rust语法描述record。有以下原始数据类型:
- State(execution state, e.g., a reference to local storage)
- Command(e.g., a sequence of transactions)
- EpochId(int)
- Round(int)
- NodeTime(system time of a node)
- BlockHash、QuorumCertificateHash(hash值)
- Author(标志一个节点)
- Signature(数字签名)
Record Store. network records - 刚从网络接收的record;verified records - 按4.2节规则详细校验过的record。
节点在一个epoch存储的所有verified records称为一个record store。如果一个record存在于节点当前epoch的record store里,称为节点认识该record。
epoch的所有record组成一棵树(除了timeout - timeout不链接)。5.7节描述何时从record store清理record。
/// A record read from the network.
enum Record {
/// Proposed block, containing a command, e.g. a set of Libra transactions.
Block(Block),
/// A single vote on a proposed block and its execution state.
Vote(Vote),
/// A quorum of votes related to a given block and execution state.
QuorumCertificate(QuorumCertificate),
/// A signal that a particular round of an epoch has reached a timeout.
Timeout(Timeout),
}
struct Block {
/// User-defined command to execute in the state machine.
command: Command,
/// Time proposed for command execution.
time: NodeTime,
/// Hash of the quorum certificate of the previous block.
previous_quorum_certificate_hash: QuorumCertificateHash,
/// Number used to identify repeated attempts to propose a block.
round: Round,
/// Creator of the block.
author: Author,
/// Signs the hash of the block, that is, all the fields above.
signature: Signature,
}
struct Vote {
/// The current epoch.
epoch_id: EpochId,
/// The round of the voted block.
round: Round,
/// Hash of the certified block.
certified_block_hash: BlockHash,
/// Execution state.
state: State,
/// Execution state of the ancestor block (if any) that will match
/// the commit rule when a QC is formed at this round.
committed_state: Option<State>,
/// Creator of the vote.
author: Author,
/// Signs the hash of the vote, that is, all the fields above.
signature: Signature,
}
struct QuorumCertificate {
/// The current epoch.
epoch_id: EpochId,
/// The round of the certified block.
round: Round,
/// Hash of the certified block.
certified_block_hash: BlockHash,
/// Execution state
state: State,
/// Execution state of the ancestor block (if any) that matches
/// the commit rule thanks to this QC.
committed_state: Option<State>,
/// A collections of votes sharing the fields above.
votes: Vec<(Author, Signature)>,
/// The leader who proposed the certified block should also sign the QC.
author: Author,
/// Signs the hash of the QC, that is, all the fields above.
signature: Signature,
}
struct Timeout {
/// The current epoch.
epoch_id: EpochId,
/// The round that has timed out.
round: Round,
/// Round of the highest block with a quorum certificate.
highest_certified_block_round: Round,
/// Creator of the timeout object.
author: Author,
/// Signs the hash of the timeout, that is, all the fields above.
signature: Signature,
}
Commitments for Libra clients. 节点对block B投票时,如果形成B的QC会导致新的commit(见5.3节 commit rule),则必须设置vote.committed_state字段值。例如B的前置B’ 将要commit,则committed_state要设置 = 链一直到B’(含)的execution state。注意此execution state其实已经包含在B’的QC,将它再包含在B的QC.committed_state字段里 给client提供了一个B’的commit
certificate - 简短地证明B’ commit后的execution state。
4.2. Verification of Network Records
Epoch开始时,各节点就hinit 的初值(类型QuorumCertificateHash)达成一致(例如 = hash(seed || epoch_id),seed是某固定值)。
在插入record store之前,节点必须按顺序校验所有收到的record:
- 所有签名必须是从当前epoch的节点发出
- BlockHash值 = 验证过的blocks
- QuorumCertificateHash值 = 验证过的QC(或hinit初值)
- 在block和QC链里 round值必须是增加的。epoch round 1 发起的proposal block的round=1
- QC.author = 前一block的author
- timeouts、votes、QC的epoch_id 匹配 当前的epoch
- votes、QC的round 匹配待校验的block
- timeout必须包含一个不大于目前已校验的highest QC round 的highest certified block round
- vote、QC的committed_state 须与commit rule(5.3节)一致
- 未校验通过的network record 要丢弃
按照block和QC的hash的约束,除了timeout,节点校验过的record组成一棵树,树的根=hinit(Lemma S1,6.1节)
4.3. Communication Framework(通讯框架)
用于在节点间交换record,含3种操作:
- send,发送record给其他一组节点
- broadcast,广播/推送更新给其他所有节点
- query-all,向其他所有节点请求拉取更新
在GST之前或发送节点不诚实时,broadcast的数据未必能在延时δ 内到达所有接收节点。所以当很久没有收到新的commit、或当前round在timeout后仍不变时,节点会定期触发query-all操作(5.6,7.10,7.11节)。非leader节点的all-to-all通讯仅限用于broadcast timeout和如上2种query-all,这样希望在乐观情况下(leader诚实、网速足够快),只需要线性数量的point-to-point通讯就能产生一个commit。
4.4. Data Synchronization
考虑到节点可能宕机、或有新增节点,希望数据同步是灵活的,例如发送者发起通讯但接收者可以忽略自己已有的数据。LibraBFT提供称为data synchronization(数据同步)的交换协议。
使用send和broadcast的数据同步的步骤:
- 发送者通过datasynchronization service 发布新数据
- 然后发送notification消息给接收者:point-to-point时的一个接收者、或broadcast时的所有接收者
- 接收者也可以向发送者发送request消息,然后通过response消息获取数据
query-all只用在上面的第3步:接收者请求其他所有节点。
交换完成时,接收者需要马上校验收到的数据(4.2节),然后将所有校验过及相关数据准备好以供将来交换(相关数据详见7.12节)。
4.5. Runtime Environment
把节点所处的环境(进程、网络、定时器、操作系统等)抽象为runtime environment。LibraBFT节点 视为 local state + 一组handlers。Handler操作local state,返回结果值。 Runtime environment负责调用handler,解析返回的值。
4.6. Data-Synchronization Handlers
当消息收到或被返回时,有3个对应的handler。create_notification和create_request方法被节点的main handler(5.6节)用来请求runtime
environment发起4.3节里的一个操作。
trait DataSyncNode {
/// Sender role: what to send to initiate a data-synchronization exchange with a receiver.
fn create_notification(&self) -> DataSyncNotification;
/// Query role: what to send to initiate a query exchange and obtain data from a sender.
fn create_request(&self) -> DataSyncRequest;
/// Sender role: handle a request from a receiver.
fn handle_request(&self, request: DataSyncRequest) -> DataSyncResponse;
/// Receiver role: accept or refuse a notification.
fn handle_notification(
&mut self,
notification: DataSyncNotification,
context: &mut SMRContext,
) -> Option<DataSyncRequest>;
/// Receiver role: receive data.
fn handle_response(&mut self, response: DataSyncResponse, context: &mut SMRContext, clock: NodeTime);
}
Data-synchronization handlers不停地查询和更新节点record store,和第5章的main handler互相独立。
4.7. Mathematical Notations
由于未通过校验的record会被丢弃,从这里开始record一般指已校验的record。用α 代表一个节点。record_store(α) 是α 在某时刻的record store。|| 用来连接bit串。
Record的相关标记:
- B - block;C - quorum certificate;V - vote;T - timeout;R - block或certificate
- h,h1,… - QuorumCertificateHash/BlockHash hash值。n,n1,… - round值
- round(B) - block的round字段。更一般地,foo( R ) - record R的foo字段
- 如果ℎ = certified_block_hash( C ),记为 ℎ ← C。对于单个vote V,记为 h ← V。如果ℎ = previous_quorum_certificate_hash(B),记为 h ← B
- 更一般地,用 ← 标记hash|block|vote|quorum certificate之间的关系。写 B ← C 而非 hash(B) ← C,B ← V 而非 hash(B) ← V,C ← B 而非 hash( C ) ← B
- R0 ←* Rn 代表 R0 ← R1 … ← Rn,n ≥ 0
5. The LibraBFT Protocol
5.1. Overview of the Protocol
节点对当前epoch维护一棵record树,即record_store(α)。树的根节点hinit 在建立epoch时确立。树的每个分支是一条record链:hinit ← B1 ← C1 … ← Bn[← Cn]。
当节点成为leader,它必须propose一个新的block(transaction Bn+1),通常是在树当前最长的分支上延续(图里1)。假定proposal Bn+1 广播成功,诚实节点会校验数据、执行这个新的block、向leader发回一个vote(图里2)。在没有执行bug的情况下,诚实节点应该会同意执行Bn+1 后的execution state。当收到足够多的vote 赞同此execution state时,leader会创建一个quorum certificate Cn+1 并广播它(图里3)。链的长度现在加1:hinit ← B1 ← C1 … ← Bn+1 ← Cn+1。当前leader完成,等待下一个leader继续推进。
由于存在网络延时或恶意节点,诚实节点们不一定总是能就延伸哪条分支和给哪个block投票 达成一致。按照 BFT assumption(2.3节),诚实节点遵守的voting constraint(投票限制)保证了 只要包含block B的某分支延伸得足够长并满足commit rule时,B和它的前置再也不能被冲突的proposal挑战。这些区块于是可以commit,状态机的复制过程得以推进。
为保证即使存在恶意节点或leader失去响应 复制过程也能推进,每个proposal包含一个round数值。轮次/round会超时。到下一轮活跃时,新的leader预期会propose一个block。Pacemaker(7.3节)用来使各诚实节点就一个唯一的、活跃的轮次达成足够长时间的一致。
现在可以重新描述一下LibraBFT协议的目标:
- (safety) 新的commit总是延伸一条包含以前所有commit的链
- (liveness) 如果网络synchronous足够长时间,总能产生一个新的commit
5.2. Chains
一条k-chain就是一系列的k blocks和k QCs:
B0 ← C0 ← … ← Bk-1 ← Ck-1
B0称为头/head,Ck-1称为尾/tail。
复习一下,链上的轮次必须增加:round(Bi) < round(Bi+1)。
当轮次刚好只增加1时 – round(Bi) + 1 = round(Bi+1) – 称链的轮次连续(contiguous rounds)。
实践上,链的轮次不一定会连续。例如不诚实的leader propose了一个非法的block、或leader由于网络原因不能按时搜集到足够多vote形成quorum。当一个轮次不能生成QC时,更高轮次的leader会propose 而破坏连续性。
5.3. Commit Rule
当且仅当block B0是一条轮次连续的3-chain的head时,称为B0满足LibraBFT的commit rule。即存在:
B0 ← C0 ← B1 ← C1 ← B2 ← C2
且:
round(B2) = round(B1) + 1 = round(B0) + 2
当发生此情况时,B0及其之前的block 变成已提交(committed)。
按照前面讨论的commitments(4.1节),一个合法的C2 quorum certificate担当commit certificate:它的committed_state字段值必须非空,以证明execution state state(C0) 在当前epoch提交了。注:如果committed_state字段值为空,则C2 不合法 要被丢弃(4.2节)。
5.4. 投票限制1: Increasing Round
Commit rule的safety 依赖于2条投票限制。限制1与轮次相关:
- (increasing-round) 一个诚实节点vote B之后,只能vote B’ 如果 round(B) < round(B’)
这条限制对于QC来说很重要(第6章)。节点α 用变量latest_voted_round(α) 来记录其最近投票的轮次,只会vote B 如果round(B) > latest_voted_round(α)。
5.5. 投票限制2: Locked Round
定义block B的previous round(上一轮)如下:如果存在B’和C’ 满足 B’ ← C’ ← B,那么 previous_round(B) = round(B’);否则 previous_round(B) = 0。
进一步,定义block B的second-previous round(上上轮)如下:如果存在 B’’ ← C’’ ← B’ ← C’ ← B,那么 second_previous_round(B) =
round(B’’);否则second_previous_round(B) = 0。
节点α 的locked round,写作locked_round(α),是节点α 所有投过票的block B里最高的上上轮 second_previous_round(B),如果不存在则是0。换句话说,epoch开始时locked_round(α) = 0,随后每次vote block B时更新为:max(原值,second_previous_round(B))。
限制2:
- (locked-round) 一个诚实节点α 只能vote B 如果:previous_round(B) ≥ locked_round(α)
这条限制是从最新的HotStuff改编而来,LibraBFT将其简化为一个单子句的条件。
5.6. Local State of a Consensus Node and Main Handler API
现在按local state和handler(4.5节)来描述协议。
Local state. 见4.2节,节点α 的state 主要包括其record store,写作record_store(α),包含当前epoch所有已验证的record。
节点state 也包括一组与轮次同步(即leader选举)相关的变量。这些变量合并到一个名为pacemaker 的对象里(7.3节)。
其他的state 变量:
- 当前epoch标志:epoch_id(α),用来侦测当前epoch的结束
- author标志:local_author(α)
- 最近vote的block 轮次:latest_voted_round(α) (初值0)
- locked_round(α) (初值0)
- 最近query-all操作的系统时间:latest_query_all_time(α) (初值epoch的开始时间)
上面latest_query_all_time(α) 变量与liveness 有关。当获取新的commit和新的轮次没有足够进展时,此变量用来触发query-all操作(4.3节、第7章)。
节点state 还包括一个commit tracker对象,用来追踪main handler已经处理的commit、并判断是否需要执行query-all来恢复错过的commit(7.11节)。
最后,节点state 还包括所有以前epoch的record state。这些epoch已经停止,不能再新增record。
Main handler API. LibraBFT里节点的main handler 仅含一个方法update_node,runtime environment在3种情况下必须调用它:
- 当节点启动或 宕机后重启时
- 当数据同步完成时(4.6节)
- handler的上次执行 约定一个时间再次执行
Main handler响应record store的变化,或返回一组action items 由runtime environment来执行。具体:
- main handler可能要求在未来某时间再次调用update_node
- 可能要求发送notification给特定节点们
- 可能要求广播
- 可能要求query-all
Main handler的实现是LibraBFT协议的核心(5.7节)。
Rust definitions. Local state:
struct NodeState {
/// Module dedicated to storing records for the current epoch.
record_store: RecordStoreState,
/// Module dedicated to leader election.
pacemaker: PacemakerState,
/// Current epoch.
epoch_id: EpochId,
/// Identity of this node.
local_author: Author,
/// Highest round voted so far.
latest_voted_round: Round,
/// Current locked round.
locked_round: Round,
/// Time of the latest query-all operation.
latest_query_all_time: NodeTime,
/// Track data to which the main handler has already reacted.
tracker: CommitTracker,
/// Record stores from previous epochs.
past_record_stores: HashMap<EpochId, RecordStoreState>,
}
Main handler API:
trait ConsensusNode {
fn update_node(&mut self, clock: NodeTime, context: &mut SMRContext) -> NodeUpdateActions;
}
’context’ 参数用于一些SMR操作例如执行command。
注:ConsensusNode trait与DataSyncNode(4.6节)并存,后者用于数据同步。
struct NodeUpdateActions {
/// Time at which to call `update_node` again, at the latest.
next_scheduled_update: NodeTime,
/// Whether we need to send a notification to a subset of nodes.
should_send: Vec<Author>,
/// Whether we need to send a notification to all other nodes.
should_broadcast: bool,
/// Whether we need to request data from all other nodes.
should_query_all: bool,
}
5.7. Main Handler Implementation
大致看,update_node方法实现:
- 运行pacemaker,执行要求的操作如创建timeout、propose block
- 投票,遵照限制1(5.4节)和限制2(5.5节)
- 如果节点已经propose block,而且刚刚收到对相同state的vote quorum,创建QC,触发广播,并预约立即再次执行update_node
- 调用process_commits 处理新发现的commit(见下)
- 运行commit tracker更新state并判断是否需执行query-all
- 更新最新的query-all操作时间
Main handler使用后续章节描述的、liveness相关的接口:
- Pacemaker trait的方法update_pacemaker 控制leader选举、timeout、proposal;返回action items由process_pacemaker_actions方法处理。RecordStore的proposed_block方法也使用pacemaker来选择可以vote的active proposal(7.3节)
- CommitTracker 提供最新已处理的commit、及方法update_tracker用来更新最新epoch和commit 以及返回需要的操作例如query-all(7.11节)
最后,process_commits用于:
- 传递commits给SMR context
- 检查commit是否终结当前epoch
- 需要时开始一个新的epoch
此方法在收到数据时使用节点的record store来计算已提交block的最高轮次highest_committed_round。该值与 commit tracker 相关字段比较来判断是否必须发送新的commit。方法committed_states_after的参数是轮次m和record store。假定record store包含一条链 B1 ← C1 ← … ← Bk ← Ck 满足:round(B1) > m (至少) 并且 Bk是最高已提交block,方法committed_states_after 返回一个序列 (round(B1), state(C1), …, round(Bk), state(Ck))。Record store的highest_commit_certificate 定义为最高已提交block的commit rule的最后一个QC。
… 代码略 …
Epoch changes. 一旦节点发送了一个结束当前epoch的commit QC,它停止发送该epoch的commit、储存当前record store、为新的epoch新建record store。
之前描述的safety规则在节点只参与(如vote)新epoch 且从不返回旧epoch 时足以保证safety。创建新epoch的参数必须由导致它创建的已提交execution state唯一决定。
为保证liveness,节点必须保留以前epoch的record store:在数据同步时,节点必须能跟随任意发送者的commit链且执行command,直到该发送者最新epoch的最新commit rule。技术上这意味着在数据同步期间方法process_commits可能被调用、旧epoch可能被停止、新epoch可能启动(另见3.1、7.12节)。
由于网络原因,从包含epoch change的block 到触发commit rule的block 之间的commit链 可能任意长。为避免持久化将来可能不commit的数据,可能会要求当分支发现epoch change时,leader只能propose空的command。