实用拜占庭协议(PBFT 99)

2023-11-04 01:30
文章标签 协议 实用 99 拜占庭 pbft

本文主要是介绍实用拜占庭协议(PBFT 99),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文献:

  1. 拜占庭容错:Lamport L, Shostak R, Pease M. The Byzantine generals problem[M]//Concurrency: the works of leslie lamport. 2019: 203-226.
  2. 实用拜占庭容错:Castro Miguel and Barbara Liskov. “Practical byzantine fault tolerance.” OsDI. Vol. 99. No. 1999. 1999.

文章目录

  • PBFT(1999)
  • 三段共识算法
    • 阶段一:pre-prepare
    • 阶段二:prepare
    • 阶段三:commit
  • 垃圾回收算法
  • 视图切换算法
  • 其他

PBFT(1999)

Lamport 提出了第一个拜占庭容错协议,但是它工作在可信的同步信道上,且复杂度随节点个数增长而指数级增加。它主要是理论意义上的可行性证明,但并不实用。

1999年,Liskov 等人提出了 Practical byzantine fault tolerance,利用了Hash函数、数字签名算法、消息验证码,它是第一个工作在异步网络上的拜占庭容错协议,且复杂度只有 O ( n 2 ) O(n^2) O(n2)

因为工作在存在恶意敌手的异步网络上,因此一个节点在一轮通信中可以有三种动作:

  1. 有响应,并且是正确的结果,它可能来自诚实节点,但也可能来自恶意节点。
  2. 无响应,可能是因为网络延迟节点故障,也可能是敌手恶意延迟。
  3. 有响应,但是恶意节点发送了错误的结果。故障节点也可能发送错误信息,但随机错误无法通过签名验证,可视为无响应。

假设算法可以最多容忍 f f f个恶意节点,总的节点数为 n n n。某个节点接收其他节点的消息时,某些节点无响应,且响应的节点中包含错误信息;最坏情况下,存在 f f f个恶意节点,存在 f f f个无响应节点,并且两者不重合;此时,节点收到了 n − f n-f nf个消息(包括自己),其中 f f f条消息不可信;为了利用少数服从多数规则得到正确结果,要保证正确节点的消息是大多数,即 n − 2 f > f n-2f>f n2f>f,那么需要
n ≥ 3 f + 1 n \ge 3f+1 n3f+1
PBFT 是一种状态机复制算法(a form of state machine replication),每个状态机都维护服务状态并实现服务操作。令 R R R是这些副本(replicas)的集合,大小为 n = 3 f + 1 n=3f+1 n=3f+1,其中一个副本叫做 primary,其他副本叫做 backups。为了保持分布式系统的活性(liveness),副本在视图(view)中移动。在视图 v v v中,primary 是第 p = v m o d n p=v \mod n p=vmodn 号机器,其他的机器是 backup。

工作流程如下:

  1. client 利用点对点信道发送请求给 primary,形式为
    { R E Q U E S T , o p , t , c } : c \{REQUEST,op,t,c\}:c {REQUEST,op,t,c}:c
    这里 o p op op读写指令 t t t时间戳(timestamp,用于实现 exactly-once semantic), c c c客户ID,符号“ : c :c :c”表示附加上 c c c签名

  2. primary 把得到的请求广播给所有的 backups

  3. 每个 replicas 执行请求,把结果发送给 client,形式为
    { R E P L Y , v , t , c , i , r } : i \{REPLY,v,t,c,i,r\}:i {REPLY,v,t,c,i,r}:i
    这里 v v v当前视图的编号, i i i副本ID r r r计算结果

  4. client 等待 f + 1 f+1 f+1个来自不同 replicas 的验签通过的相同结果 ( t , r ) (t,r) (t,r),那么其中一定有至少 1 1 1个是诚实副本的结果,接受它。

  5. 设置计时器,如果没能等到足够的相同结果,那么就利用广播信道给所有的 replicas 重发请求,并回到 step 4

    1. 如果 replicas 发现这个请求已经接收到了,那么不要重新执行(尤其是不幂等的操作),而是简单重发(re-send)上次的计算结果。这需要日志功能。
    2. 如果 backup 发现之前没接收到过这个请求,那么就转发(relay)给 primary,并设置计时器,等待 primary 的广播。若等不到广播,那么就怀疑 primary 出问题了,可能是出现故障,也可能变成了恶意节点。

这里的 client 也可以是集群中的某个副本,比如 replica i i i 想要更新自身状态保持与集群一致。

三段共识算法

在上述算法中的 step 2,需要让集群中的诚实节点对客户请求达成共识。共识算法分为三个阶段:pre-prepare,prepare,commit

在这里插入图片描述

阶段一:pre-prepare

  1. primary 为请求消息 m m m分配一个递增的序列号(sequence number) n n n,然后广播如下信息
    { { P R E − P R E P A R E , v , n , d } : p , m } \{\{PRE-PREPARE,v,n,d\}:p,\, m\} {{PREPREPARE,v,n,d}:p,m}
    其中 v v v是视图号, p = v m o d ∣ R ∣ p=v \mod |R| p=vmodR是 primary 的身份ID, d = H a s h ( m ) d=Hash(m) d=Hash(m)是摘要。

    注意,pre-prepare 消息不包含客户指令, m m m作为第二个消息被广播。primary 将这两个消息记入自己的日志(log)里。

  2. backup 接收到上述形式的 pre-prepare 消息和指令 m m m后,依次检查:

    1. 是否满足 d = H a s h ( m ) d=Hash(m) d=Hash(m),这关乎 m m m的完整性
    2. 验证是否是 p p p的合法签名,证明信息来源于 primary
    3. 自己是否位于视图 v v v
    4. 之前没接收过另一个 pre-prepare 消息 ( v , n , d ′ ≠ d ) (v,n,d' \neq d) (v,n,d=d),否则可以怀疑 primary 是恶意节点
    5. 序列号 n n n位于自己的高低水位(high/low water mark) h , H h,H h,H之间,这用于防止某节点落后其他节点太多。

    如果检查都通过,那么接受它们,并把它们写入日志。

阶段二:prepare

  1. 如果 backup i i i 接受了 pre-prepare 消息,那么它进入阶段二,对其他所有 replicas(包括 backup 和 primary)广播如下消息
    { P R E P A R E , v , n , d , i } : i \{PREPARE,v,n,d,i\}:i {PREPARE,v,n,d,i}:i
    并将这个 prepare 消息写入日志。

  2. replica 接收到上述 prepare 消息后, 依次检查:

    1. 验证是否是 i i i的合法签名,证明信息来源于 backup i i i
    2. 自己的当前视图是否等于 v v v
    3. 序列号 n n n位于自己的高低水位之间

    如果检查通过,那么接受它,并写入日志。

我们设置谓词(predicate) p r e p a r e d ( m , v , n , i ) prepared(m,v,n,i) prepared(m,v,n,i):它为真 ⟺ \iff 副本 i i i的日志里记录了关于 m m m的: 1 1 1个来自 primary 的 pre-prepare 消息, 2 f 2f 2f个来自不同 backup(包含 i i i自己)的 prepare 消息。

p r e p a r e d prepared prepared谓词确定了请求事件 m 1 , m 2 , ⋯ m_1,m_2,\cdots m1,m2,在视图 v v v下的全序(total order):如果 p r e p a r e d ( m , v , n , i ) = t r u e prepared(m,v,n,i) = true prepared(m,v,n,i)=true,那么对于任意的 m ≠ m ′ m \neq m' m=m,都有 p r e p a r e d ( m ′ , v , n , i ) = f a l s e prepared(m',v,n,i) = false prepared(m,v,n,i)=false,除非找到了Hash碰撞 H a s h ( m ) = H a s h ( m ′ ) Hash(m) = Hash(m') Hash(m)=Hash(m)

proof:因为有 1 + 2 f 1+2f 1+2f个 replicas 接收到了 ( m , n ) (m,n) (m,n)的 pre-prepare 和 prepare 消息,且恶意节点至多有 f f f个,那么就有 1 + f 1+f 1+f个诚实节点接收到了 ( m , n ) (m,n) (m,n),但一共就 3 f + 1 3f+1 3f+1个节点,那么最多有 2 f 2f 2f个节点能接收到 ( m ′ , n ) (m',n) (m,n)。除非有诚实节点对 ( m , n ) , ( m ′ , n ) (m,n),(m',n) (m,n),(m,n)都发送了 prepare 消息,这是不可能的。

阶段三:commit

  1. 当 replica i i i 发现 p r e p a r e d ( m , v , n , i ) = t r u e prepared(m,v,n,i) = true prepared(m,v,n,i)=true,那么它就广播如下消息给所有的 replica
    { C O M M I T , v , n , d , i } : i \{COMMIT,v,n,d,i\}:i {COMMIT,v,n,d,i}:i
    并把它写入日志。

  2. replica 接收到上述消息后,依次检查

    1. 验证是否是 i i i的合法签名,证明信息来源于 replica i i i
    2. 自己的当前视图是否等于 v v v
    3. 序列号 n n n位于自己的高低水位之间

    如果检查通过,那么接受它,并写入日志。

我们设置两个谓词,

  1. c o m m i t t e d ( m , v , n ) committed(m,v,n) committed(m,v,n):它为真 ⟺ \iff 存在 f + 1 f+1 f+1个诚实节点 S S S,使得 p r e p a r e d ( m , v , n , i ) = t r u e , ∀ i ∈ S prepared(m,v,n,i) = true,\forall i \in S prepared(m,v,n,i)=true,iS
  2. c o m m i t t e d − l o c a l ( m , v , n , i ) committed-local(m,v,n,i) committedlocal(m,v,n,i):它为真 ⟺ p r e p a r e d ( m , v , n , i ) = t r u e \iff prepared(m,v,n,i) = true prepared(m,v,n,i)=true 并且 replica i i i 的日志里记录了 2 f + 1 2f+1 2f+1个来自不同 replica(包含自己)的 commit 消息

容易验证,
c o m m i t t e d − l o c a l ( m , v , n , i ) = t r u e ⇒ c o m m i t t e d ( m , v , n ) = t r u e committed-local(m,v,n,i) = true \,\,\Rightarrow\,\, committed(m,v,n) = true committedlocal(m,v,n,i)=truecommitted(m,v,n)=true
只要诚实的 replica i i i 发现某些 ( m , v , n ) (m,v,n) (m,v,n)对应的 c o m m i t t e d − l o c a l ( m , v , n , i ) = t r u e committed-local(m,v,n,i) = true committedlocal(m,v,n,i)=true,那么它就可以确定指令 m m m已经被至少 f + 1 f+1 f+1个诚实节点所达成共识。它根据自身状态(集群状态的副本),按照序列号 n n n从低到高独立地执行 m m m,将结果发送给对应的客户。

垃圾回收算法

在三阶段共识算法中,集群的每个节点都要写入大量的日志,除非它已经知道有 f + 1 f+1 f+1个诚实节点记录了这个消息,能够压倒 f f f个恶意节点。为防止内存溢出,我们需要定期清理日志。然而,如果某条消息已经被多数诚实节点丢弃,但有一个节点没有记录它却需要使用它来修改自身状态,那么就不得不传输整个系统的状态了。因此,每个副本都需要定时证明自身状态是正确的。

实时生成证明(proofs)是昂贵的,所以我们利用序列号来设置一些检查点(checkpoint),一般选取 n ≡ 0 m o d 100 n \equiv 0 \mod 100 n0mod100。拥有 proof 的检查点被叫做稳定检查点(stable checkpoint)

每个 replica 都维护若干个时刻的系统状态: 1 1 1最新稳定检查点,若干不稳定的检查点,还有当前状态(current state)。

  1. 每当 replica i i i 触碰到了新的检查点 n n n(序列号为 n n n的请求执行完毕),对应自身状态为 s s s,那么广播如下消息
    { C H E C K P O I N T , n , d , i } : i \{CHECKPOINT,n,d,i\}:i {CHECKPOINT,n,d,i}:i
    其中 d = H a s h ( s ) d=Hash(s) d=Hash(s)。把这个 checkpoint 消息写入日志。

  2. 每个 replica 收集其他节点发送的 checkpoint 消息,只需验证签名的正确性,并写入日志。直到收集到 2 f + 1 2f+1 2f+1个消息,它们拥有相同的 ( n , d ) (n,d) (n,d),那么这些 checkpoint 消息就构成了系统状态 s s s的proof,它成为了稳定检查点。

对于稳定检查点之前的(包括 n n n)序列号对应的所有 pre-prepare、prepare、commit 消息,都是可以从日志中丢弃的。因为已经有 f + 1 f+1 f+1个诚实节点保持了相同的系统状态副本,这压倒了至多 f f f个恶意节点。

我们将低水位 h h h设为最新的稳定检查点,并且设置高水位 H H H,使得 k = H − h k=H-h k=Hh足够大,阻止恶意的 primary 发送大量 pre-prepare 消息来耗尽序列号空间,并且 replicas 也不必等待检查点变得稳定。可以设置为 k = 200 k=200 k=200

视图切换算法

  1. 如果 backup i i i 认为 primary 有问题,那么它广播(并非点对点发给 p = v + 1 p=v+1 p=v+1)视图切换请求,形式如下
    { V I E W − C H A N G E , v + 1 , n , C , P , i } : i \{VIEW-CHANGE,v+1,n,C,P,i\}:i {VIEWCHANGE,v+1,n,C,P,i}:i

    • n n n:节点 i i i知道的最新稳定检查点,对应系统状态 s s s
    • C C C:对 s s s的稳定性证明,那 2 f + 1 2f+1 2f+1个 checkpoint 消息
    • P P P:集合 P m P_m Pm的集合,这里 P m P_m Pm包含节点 i i i上严格大于 n n n的序列号对应的请求 m m m的有效的 1 1 1个 pre-prepare 消息、 2 f 2f 2f个 prepare 消息,即 p r e p a r e d ( m , v , n , i ) = t r u e prepared(m,v,n,i) = true prepared(m,v,n,i)=true
  2. 如果 replica p = v + 1 m o d ∣ R ∣ p = v+1 \mod |R| p=v+1modR 收到了来自其他 replica(不含自己)的 2 f 2f 2f个有效的(检查签名、检查 C C C、检查 P P P) view-change 消息,那么它作为 primary 发起视图切换,广播如下信息
    { N E W − V I E W , v + 1 , V , O } : p \{NEW-VIEW,v+1,V,O\}:p {NEWVIEW,v+1,V,O}:p

    • V V V:收到的 2 f 2f 2f个 view-change 消息,这证明有 f f f个诚实节点认为视图 v v v的 primary 有问题,视图变更是有必要的。

    • O O O:新视图下的 pre-prepare 消息集合,构建规则如下

      1. primary 查看 V V V里的 view-change 消息,确定最近的稳定检查点 m i n s min_s mins(拥有 C C C,被 f + 1 f+1 f+1个诚实副本维护),以及 prepare 信息的最大序列号 m a x s max_s maxs(谓词 prepared 为真,在 f + 1 f+1 f+1个诚实节点上被准备)

      2. 对于每个序列号 n ∈ [ m i n s , m a x s ] n \in [min_s,max_s] n[mins,maxs](闭区间)的请求,primary 创建它们在新视图 v + 1 v+1 v+1上的 pre-prepare 消息。如果 V V V里存在一些 view-change 消息的 P P P分量中包含 n n n,那么设置
        { P R E − P R E P A R E , v + 1 , n , d } : p \{PRE-PREPARE,v+1,n,d\}:p {PREPREPARE,v+1,n,d}:p
        其中的 d d d是在最高视图下 pre-prepare 消息里的摘要(似乎就是视图 v v v下的?因为 V V V中包含诚实节点的 view-change 消息,它本位于 v v v视图,请求转换到 v + 1 v+1 v+1视图)

        如果没有这种分量(要么是 m i n s = m a x s min_s=max_s mins=maxs,要么序列号跳跃递增),那么设置
        { P R E − P R E P A R E , v + 1 , n , d n u l l } : p \{PRE-PREPARE,v+1,n,d_{null}\}:p {PREPREPARE,v+1,n,dnull}:p
        这里的 d n u l l = H a s h ( n u l l ) d_{null} = Hash(null) dnull=Hash(null)空指令 n u l l null null

    primary 只将 O O O写入自身日志,而 V V V的内容已经在日志里了。如果 m i n s min_s mins大于 primary 已知的稳定检查点,那么使用 m i n s min_s mins作为自己的稳定检查点,记录它的proof,并清理过期数据。

  3. replica 收到了上述 new-view 消息后,

    1. 验证签名的正确性
    2. 验证 V V V是对于视图 v + 1 v+1 v+1的有效 view-change 消息集合
    3. 验证 O O O是正确的 pre-prepare 消息集合,方法类似 step 2

    所有的校验都通过后,切换到 v + 1 v+1 v+1视图,将 V , O V,O V,O写入日志,并为 O O O里的每一个 pre-prepare 消息构造对应的视图 v + 1 v+1 v+1下的 prepare 消息并广播

  4. replica 收到广播的 prepare 消息后,校验正确性,写入日志,并执行 commit 步骤,继续写日志。但是对于 n ∈ [ m i n s , m a x s ] n \in [min_s,max_s] n[mins,maxs]内的已经执行的请求,不要重新执行,而仅仅是重发存储下来的结果。

如果某节点丢失了一些请求信息,那么它直接从 O O O中获取。如果它丢失了某个检查点的状态,那么它可以利用 V V V中的 replica i i i 的 view-change 消息找到稳定检查点 n n n,然后向 replica i i i 索要系统状态 s s s。当然 s s s过于巨大,可以通过状态分区(partition the state),记录各个区的状态最近被哪个序列号改变,那么就只需要更新一小部分状态即可。

其他

看到一篇 详解实用拜占庭协议PBFT 的文章,介绍了 视图协商、增删节点 的算法。论文 PBFT99 里没有给出描述,应该是其他人的相关工作。

学习区块链课程时,了解到一种有意思的改进方案 Hotstuff。它使用星状网络大大降低了网络流量,并让三段共识算法流水线作业。论文没去找,等以后找时间看看。

在这里插入图片描述
在这里插入图片描述

这篇关于实用拜占庭协议(PBFT 99)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/342272

相关文章

5 种使用Python自动化处理PDF的实用方法介绍

《5种使用Python自动化处理PDF的实用方法介绍》自动化处理PDF文件已成为减少重复工作、提升工作效率的重要手段,本文将介绍五种实用方法,从内置工具到专业库,帮助你在Python中实现PDF任务... 目录使用内置库(os、subprocess)调用外部工具使用 PyPDF2 进行基本 PDF 操作使用

MySQL批量替换数据库字符集的实用方法(附详细代码)

《MySQL批量替换数据库字符集的实用方法(附详细代码)》当需要修改数据库编码和字符集时,通常需要对其下属的所有表及表中所有字段进行修改,下面:本文主要介绍MySQL批量替换数据库字符集的实用方法... 目录前言为什么要批量修改字符集?整体脚本脚本逻辑解析1. 设置目标参数2. 生成修改表默认字符集的语句3

HTTP 与 SpringBoot 参数提交与接收协议方式

《HTTP与SpringBoot参数提交与接收协议方式》HTTP参数提交方式包括URL查询、表单、JSON/XML、路径变量、头部、Cookie、GraphQL、WebSocket和SSE,依据... 目录HTTP 协议支持多种参数提交方式,主要取决于请求方法(Method)和内容类型(Content-Ty

从基础到进阶详解Python条件判断的实用指南

《从基础到进阶详解Python条件判断的实用指南》本文将通过15个实战案例,带你大家掌握条件判断的核心技巧,并从基础语法到高级应用一网打尽,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录​引言:条件判断为何如此重要一、基础语法:三行代码构建决策系统二、多条件分支:elif的魔法三、

Python 字符串裁切与提取全面且实用的解决方案

《Python字符串裁切与提取全面且实用的解决方案》本文梳理了Python字符串处理方法,涵盖基础切片、split/partition分割、正则匹配及结构化数据解析(如BeautifulSoup、j... 目录python 字符串裁切与提取的完整指南 基础切片方法1. 使用切片操作符[start:end]2

Java对接MQTT协议的完整实现示例代码

《Java对接MQTT协议的完整实现示例代码》MQTT是一个基于客户端-服务器的消息发布/订阅传输协议,MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛,:本文主要介绍Ja... 目录前言前置依赖1. MQTT配置类代码解析1.1 MQTT客户端工厂1.2 MQTT消息订阅适配器1.

Linux中的自定义协议+序列反序列化用法

《Linux中的自定义协议+序列反序列化用法》文章探讨网络程序在应用层的实现,涉及TCP协议的数据传输机制、结构化数据的序列化与反序列化方法,以及通过JSON和自定义协议构建网络计算器的思路,强调分层... 目录一,再次理解协议二,序列化和反序列化三,实现网络计算器3.1 日志文件3.2Socket.hpp

Linux中的HTTPS协议原理分析

《Linux中的HTTPS协议原理分析》文章解释了HTTPS的必要性:HTTP明文传输易被篡改和劫持,HTTPS通过非对称加密协商对称密钥、CA证书认证和混合加密机制,有效防范中间人攻击,保障通信安全... 目录一、什么是加密和解密?二、为什么需要加密?三、常见的加密方式3.1 对称加密3.2非对称加密四、

Python按照24个实用大方向精选的上千种工具库汇总整理

《Python按照24个实用大方向精选的上千种工具库汇总整理》本文整理了Python生态中近千个库,涵盖数据处理、图像处理、网络开发、Web框架、人工智能、科学计算、GUI工具、测试框架、环境管理等多... 目录1、数据处理文本处理特殊文本处理html/XML 解析文件处理配置文件处理文档相关日志管理日期和

精选20个好玩又实用的的Python实战项目(有图文代码)

《精选20个好玩又实用的的Python实战项目(有图文代码)》文章介绍了20个实用Python项目,涵盖游戏开发、工具应用、图像处理、机器学习等,使用Tkinter、PIL、OpenCV、Kivy等库... 目录① 猜字游戏② 闹钟③ 骰子模拟器④ 二维码⑤ 语言检测⑥ 加密和解密⑦ URL缩短⑧ 音乐播放