BCSkill (Block chain skill )
区块链中文技术社区

只讨论区块链底层技术
遵守一切相关法律政策!

Eth2.0 中的 Casper FFG

感谢 Danny Ryan 的讨论和评论

我的上一篇文章讨论了基本的 Casper Friendly Finality Gadget。本文的第一部分将重点介绍信标链中 Casper FFG 机制实现的高级细节。这篇文章的第二部分将讨论分叉选择规则和其他活性考虑因素。

这篇文章直接从 Eth2.0 规范中解释了一些事情。在可能的情况下,规范中的相关参数和功能的链接已包含在内。理解这篇文章不需要遵循这些链接——它们只是为了参考而包含在内。

第 1 部分 - Casper FFG 机制

插槽、时期和证明

Slots

时间被划分为 slot,每个 slot 可以提出一个新的区块。当前时隙的持续时间是SECONDS_PER_SLOT = 12。对于每个插槽,都会分配一个验证器来生成一个新块

Epochs

Casper FFG 机制不对完整的块树进行操作,而是仅考虑来自某些槽的块用于投票。这减少了在尝试通过查看投票来检测最终性时检查太多源-目标对的开销。由于这些特殊插槽之间有足够的空间,预计每次运行 FFG 最终性检查时都会看到来自绝大多数验证者的新投票。时隙被组合成 epoch,当前参数为SLOTS_PER_EPOCH = 32,导致每个 epoch 为 6.4 分钟。FFG 机制仅考虑位于这些 epoch 周期边界的块(“检查点”或“epoch 边界块 (EBB)”)。

证明

证明是 Casper FFG 投票,其中包含诸如源和目标块、证明时的槽号、验证者的标识符等信息。证明由验证者广播到 p2p 网络,并最终被一个块接收生产者被打包成块。

Casper FFG 机制的变化

与我在上一篇文章中提到的定义相比,最终确定的定义发生了变化。

最终确定:一个块在以下B_0情况下被确定:

  • 它是创世区块,或者
  • 以下两个条件成立:
    • 有一系列检查点,按照槽号的递增顺序,[B_0, B_1, ... , B_n]使得n >= 1所有这些块都在同一个链中并且是合理的,并且
    • 超过 2/3 的验证者投票(B_0, B_n)。

定义的这种变化仍然保留了上一篇文章中 Casper FFG 安全证明的大纲。完整的证明可以在本文的“安全”部分找到

检测 Casper FFG 确定性

信标链具有链上 FFG 机制,可处理块和证明以检测最终性。在每个 epoch 边界,该机制处理新的证明并更新其对合理和最终区块的知识。

为了降低在任何可能的源-目标对之间处理证明的开销,链上 FFG 机制只考虑特定的源-目标对。仅处理在当前和上一个 epoch 中进行的证明(不过还有一些条件!)。这导致链上 FFG 机制无法检测到所有确定性实例!总之,链上机制健全但不完善。

还引入了网络同步假设,因为仅处理来自最后两个时期的证明:证明在两个时期内在网络中广播。
链上FFG机制的规范非常简单:

  • 第一步是检测块的合理性。使用最近两个 epoch 的新证明检查两个最近的 epoch 边界块是否合理。
  • 下一步是检测块的最终确定,检查最后两个纪元边界块。最终性检查仅使用四种源-目标组合(用于提高性能和规范的简单性):

第 2 部分 - 分叉选择和验证者时间表

虽然 Casper FFG 机制概述了保证区块最终确定的规则,但它没有提及在实践中如何实现活跃性(注意:这篇文章并不试图证明活跃性,而是概述了预期实现活跃性的过程。有关严格的分析,请参阅本文)。这篇文章的这一部分将重点关注两个主要的活跃度考虑:

  • 验证者为找到链头而执行的分叉选择规则
  • 验证者生成区块和证明的时间表

HLMD GHOST 分叉选择规则

提出区块的验证者必须首先找到其链的本地头,为此他们使用混合最新消息驱动 (HMLD) GHOST 分叉选择规则。
此分叉选择的规格如下:

  1. 在每个 epoch 开始时,在验证器的当前视图中识别最新的合理区块。该变量在该时期被冻结,并在下一个时期开始时再次更新。
  2. 从步骤 1 中过滤掉任何没有对齐块的块,作为该块链中的最新对齐块。
  3. 使用通常的 LMD GHOST 规则通过块树下降,直到找到叶块。

有关前叉选择的更多信息,请查看本文中的“Hybrid LMD GHOST”部分

验证人时间表

每个验证者对网络有两个主要责任:提出新区块,并在他们的本地视图中证明最佳区块。指定验证器时间表以防止混乱并简化网络中的消息传递。该时间表由每个验证者通过从当前信标链状态中获取随机性来计算,这可以防止攻击者指定要启用的时间表。

提案时间表

对于每个 epoch 中的每个时隙,分配一个验证者作为提案者。然后,验证器在其块树的本地视图中使用分叉选择找到链的头部,并为头部生成一个新的子块。验证者看到的证明可以打包到区块中以获得奖励,这些作为最终性检查运行时链上 FFG 机制的输入。

证明时间表

每个验证者都被分配在每个时期的一个插槽中进行证明。在实践中,整个验证人集被随机划分SLOTS_PER_EPOCH为每个 epoch 的大小相等的委员会,并且每个委员会在 epoch 中被分配一个特定的插槽来进行证明。当验证者进行证明时,他们应该将源作为最后一个合理的块,并将目标作为链头后面的最新检查点,根据他们的本地视图。

有关验证器时间表的更多信息,请查看Eth2.0 规范中的验证器指南

参考资料和附加材料

  1. 以太坊 2.0 阶段 0 规范
  2. “结合 GHOST 和 Casper”论文

原文:https://www.adiasg.me/eth2/2020/04/09/casper-ffg-in-eth2-0.html

何为CASPER FFG

译者序:Eth 2.0信标链的共识协议将逐渐从PoW向PoS过渡,在这条全新的链上,如何保障其安全性和活性呢?Casper FFG 作为区块最终确定工具,为eth2带来了福音。通过这篇文章,我们可以对Casper FFG的运行规则、安全性证明和活性证明有一个初步的了解。

为了对用户负责,任何区块链都必须保证链上区块的最终确定性和链的活性,而区块链共识机制的根本就是提供这些保障。在Eth2.0中,共识过程只在信标链中进行,而Casper FFG机制则保证其运行。本文主要描述了Casper FFG机制的基本概念,举例阐释其在信标链上的作用。

Casper FFG作为一项“最终确定性工具”(finality gadget),为区块的最终确定制定了规则,并对已被确定的区块进行检测。FFG独立于区块链本身的增长过程,可以作为一个叠加层为任何有效的区块链协议提供区块最终确定性。

就这方面而言,Casper FFG并非一项成熟的共识协议,因其本身并没有相关设置,以保证链的活性。(或许我会择时再写一篇文章阐释影响Eth2.0信标链活性的相关因素)

让我们先来看看Casper FFG机制的结构,然后深入了解其规则,从而理解FFG如何保证信标链的安全性和活性!

投票


验证者通过验证区块间的交易是否有效,给链上的区块进行投票。每记投票的格式为(S, T),包含以下信息:

  • 来源区块 (S)
  • 目标区块 (T), 必须产生于S之后

实际操作中,一记投票需包含以下内容:验证者的数字签名以及相应区块信息(包括区块哈希和区块高度)。

证明&最终确定

“证明”(Justification) 和“最终确定”(finalization) 是Casper FFG机制下的两个共识执行阶段,可以类比于更为传统的拜占庭容错共识协议 (BFT) 的“准备”和“执行”阶段


2/3验证者投票证明了区块A和区块B

证明

区块B如果满足以下条件,则已被证明:

  • 是创世区块,或者
  • 超过2/3的验证者投出了(A,B)票,A是在B之前产生的区块,且已被证明。

最终确定

区块B如果满足以下条件,则已被最终确定:

  • 是创世区块,或者
  • B已被证明,且超过2/3的验证者投了 (B,C) 票,以及C是B的直接子区块(即C区块高度=B区块高度+1)

注意:关于Eth2.0 Casper FFG机制的最终确定规则,还有更加全面的阐释,详情请参阅此论文。上文所给的具体阐释,只是为了让本文更加通俗易懂。

Casper FFG的规则

Casper FFG机制有两条简单的规则:

验证者不可以进行以下任何一种情况中的 (S1, T1) 和 (S2, T2) 投票 1.区块高度(T1) = 区块高度(T2),或者 2.区块高度(S1) < 区块高度(S2) <区块高度(T2) < 区块高度(T1)

(译者注:这两种情况在Eth2中被描述为“双重投票”和“环绕投票”,恶意验证者会因此受到罚没。)

Violation of Casper FFG Rule 1: height(T1) = height(T2)


Violation of Casper FFG Rule 2: height(S1) < height(S2) < height(T2) < height(T1)

安全性&活性

Casper FFG的目的是保障区块最终确定这一共识执行过程的安全性和活性,下面两点具体阐述了其安全性和活性:

  • 可追责安全性 (Accountable Safety) : 如果两个互相冲突的区块被最终确定,那么至少有1/3的验证者违反了Casper FFG规则,他们则会被标记下来。
  • 合理的活性 (Plausible Liveness) : 不管协议处于哪一阶段,验证者都可以在不违反Casper FFG规则的前提下发起投票,对新区块进行最终确定。

尽管和传统的拜占庭容错共识机制 (BFT)文献相比,本文对FFG安全性和活性的阐释显得有些业余,但是对于大家理解区块链的最终确定机制,却刚好合适。

事实上,如果读者对BFT相关文献非常熟悉,会认为对于“plausible liveness”的解释十分荒谬。然而,由于Casper FFG只是最终确定性机制,因而说到保证系统的活性,其只需要避免以下情况的出现:诚实验证者为了继续提议或证明区块,不得不违反FFG规则。

至于安全性,可靠的安全性尤为重要,如违反规则的验证者会被标记下来,并将标记信息发送到PoS的机制上,从而对恶意验证者进行惩罚。这样做有助于协议的实现,以达到系统的平衡。

安全证明

使得两个相斥的区块A和B最终确定(且互不为对方的子区块),有两种情况:

  • A区块高度=B区块高度
    • 由于A和B在被最终确定之前都需要被证明,至少2/3的验证者需要分别为目标点A和B投票。这就意味着至少有1/3的验证者违背了第一条Casper FFG的规则。
  • A区块高度<B区块高度
    • 区块A要被最终确定,那么至少2/3的验证者都要对区块(A,C)投票,而区块C是A的子区块。
    • B区块要被证明,那么随着区块高度增加,区块应该按[genesis, B_0, B_1, … , B_n, B]排列,其中每个区块都能按顺序证明下一个区块,即至少2/3的验证者要做出类似(G, B_0), (B_0, B_1)的投票。假设B_m是该序列中的首个区块,且A区块高度 < B_m区块高度。
    • 需要注意的是,如果该序列中的任何区块和区块A或C的高度相同,那么形同以上第一种情况,我们已经得到了证明。
    • 在(B_n, B_m)投票中 (n = m-1),B_m能够被证明。但由于B_n或B_m和区块A或C不在同一个区块高度,那我们就能得到区块高度的排列:B_n< A < C < B_m。
    • 因此,有2/3的验证者都违反了Casper FFG的第二条规则。

安全性证明,区块高度A<B
还要注意的是,仅通过检查所有投票集合,找到有冲突的投票并检查相应的验证者签名,我们很容易确定违反Casper FFG规则的验证者。

活性证明

  • 假设P_0是经证明后的最高区块,而Q是某些验证者所认为并且投票的最高区块。
  • 区块P_1作为P_0的子区块且区块高度Q< P_1。此时如果有2/3的验证者对(P_0, P_1)投票使其被证明,并没有违反Casper FFG规则。
  • P_2是P_1的子区块,如果有2/3的验证者对(P_1, P_2)进行投票,并使得P_1被最终确定。这也并没有违反Casper FFG规则。

因此,至少2/3的诚实验证者总是能够对一个新区块进行最终确定,这就保证了共识机制的活性。

转载自:https://www.ethereum.cn/casper-ffg-explainer

以太坊的几种同步模式

查看geth命令行参数geth --help

--syncmode value                    Blockchain sync mode ("snap", "full" or "light") (default: snap)

参数支持"full", "snap", "light" 三种模式「原先的"fast"已被 "snap"替代」

查看官方文档

同步模式
--syncmode <mode> 您可以使用确定网络中节点类型的参数以三种不同同步模式之一启动 Geth 。

这些是:

  • Full : 下载所有区块(包括标题、交易和收据)并通过执行每个区块增量生成区块链的状态。
  • Snap(默认):与快速相同的功能,但具有更快的算法。
  • Light:下载所有区块头、区块数据,并随机验证一些。

官方推荐使用快照同步,简单说就是新的算法替代原先的fast模式,

请注意,快照同步已在 Geth v1.10.0 中提供,但尚未启用。
原因是提供快照同步需要节点已经生成快照加速结构,目前还没有,因为它也在 v1.10.0 中提供。
您可以通过 手动启用快照同步--syncmode snap,
但请注意,我们预计它要在柏林之后的几周内才能找到合适的对等方。
当我们觉得有足够的对等点可以依赖它时,我们将默认启用它。

此时已设置为默认模式

以太坊存档节点有什么用?

举个例子,如果你想知道4,000,000区块的以太坊账户余额,那么就需要运行存档节点,然后查询这个数字。这个节点依赖于一些专门的用例,但是对区块链的安全性和信任模型来说其实并没有影响。

内容引用

https://www.ccvalue.cn/article/2011.html

Golang中defer的实现原理

前言

在Go语言中,可以使用关键字defer向函数注册退出调用,即主函数退出时,defer后的函数才被调用。defer语句的作用是不管程序是否出现异常,均在函数退出时自动执行相关代码。 所以,defer后面的函数通常又叫做延迟函数

defer规则

1.延迟函数的参数在defer语句出现时就已经确定下来了

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

返回结果:0
defer语句中打印的变量i在defer出现时就已经拷贝了一份过来,所以后面对变量i的值进行修改也不会影响defer语句的打印结果

注意:对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,在这种情况下,defer后面的语句对变量的修改可能会影响延迟函数

2.defer的执行顺序与声明顺序相反

简单理解就是:定义defer类似于入栈操作,执行defer类似于出栈操作,先进后出

3.defer可操作主函数返回值

defer语句中的函数会在return语句更新返回值后在执行。因为在函数中定义的匿名函数可以访问该函数包括返回值在内的所有变量。

func deferFuncReturn() (result int) {
    i := 1
    defer func() {
       result++
    }()
    return i
}

返回结果:2
所以上面函数实际返回i++值。

defer实现原理

注意:我会把源码中每个方法的作用都注释出来,可以参考注释进行理解。

数据结构

我们先来看下defer结构体src/src/runtime/runtime2.go:_defer

type _defer struct {
    siz     int32 //defer函数的参数大小
    started bool
    sp      uintptr // sp at time of defer
    pc      uintptr //defer语句下一条语句的地址
    fn      *funcval //需要被延迟执行的函数
    _panic  *_panic //在执行 defer 的 panic 结构体
    link    *_defer //同一个goroutine所有被延迟执行的函数通过该成员链在一起形成一个链表
}

我们知道,在每一个goroutine结构体中都有一个_defer 指针变量用来存放defer单链表。
如下图所示:

defer的创建与执行

我们先来看一下汇编是如何翻译defer关键字的

    0x0082 00130 (test.go:16)   CALL    runtime.deferproc(SB)
    0x0087 00135 (test.go:16)   TESTL   AX, AX
    0x0089 00137 (test.go:16)   JNE 155
    0x008b 00139 (test.go:19)   XCHGL   AX, AX
    0x008c 00140 (test.go:19)   CALL    runtime.deferreturn(SB)

defer 被翻译两个过程,先执行 runtime.deferproc 生成 println 函数及其相关参数的描述结构体,然后将其挂载到当前 g 的 _defer 指针上。
我们先来看 deferproc 函数的实现

deferproc

// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.
//go:nosplit
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
    //用户goroutine才能使用defer
    if getg().m.curg != getg() {
        // go code on the system stack can't defer
        throw("defer on system stack")
    }
    //也就是调用deferproc之前的rsp寄存器的值
    sp := getcallersp()
    // argp指向defer函数的第一个参数
    argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    // 存储的是 caller 中,call deferproc 的下一条指令的地址
    // deferproc函数的返回地址
    callerpc := getcallerpc()

    //创建defer
    d := newdefer(siz)
    if d._panic != nil {
        throw("deferproc: d.panic != nil after newdefer")
    }
    //需要延迟执行的函数
    d.fn = fn
    //记录deferproc函数的返回地址
    d.pc = callerpc
    //调用deferproc之前rsp寄存器的值
    d.sp = sp
    switch siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
    default:
        //通过memmove拷贝defered函数的参数
        memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
    }

    // deferproc通常都会返回0
    return0()
}

比较关键的就是 newdefer

func newdefer(siz int32) *_defer {
    var d *_defer
    sc := deferclass(uintptr(siz))
    //获取当前goroutine的g结构体对象
    gp := getg()
    if sc < uintptr(len(p{}.deferpool)) {
        pp := gp.m.p.ptr()//与当前工作线程绑定的p
        if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
            // Take the slow path on the system stack so
            // we don't grow newdefer's stack.
            systemstack(func() {//切换到系统栈
                lock(&sched.deferlock)
                //从全局_defer对象池拿一些到p的本地_defer对象池
                for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
                    d := sched.deferpool[sc]
                    sched.deferpool[sc] = d.link
                    d.link = nil
                    pp.deferpool[sc] = append(pp.deferpool[sc], d)
                }
                unlock(&sched.deferlock)
            })
        }
        if n := len(pp.deferpool[sc]); n > 0 {
            d = pp.deferpool[sc][n-1]
            pp.deferpool[sc][n-1] = nil
            pp.deferpool[sc] = pp.deferpool[sc][:n-1]
        }
    }
    //如果p的缓存中没有可用的_defer结构体对象则从堆上分配
    if d == nil {
        // Allocate new defer+args.
        //因为roundupsize以及mallocgc函数都不会处理扩栈,所以需要切换到系统栈执行
        systemstack(func() {
            total := roundupsize(totaldefersize(uintptr(siz)))
            d = (*_defer)(mallocgc(total, deferType, true))
        })
        if debugCachedWork {
            // Duplicate the tail below so if there's a
            // crash in checkPut we can tell if d was just
            // allocated or came from the pool.
            d.siz = siz
            //把新分配出来的d放入当前goroutine的_defer链表头
            d.link = gp._defer
            gp._defer = d
            return d
        }
    }
    d.siz = siz
    d.link = gp._defer
    //把新分配出来的d放入当前goroutine的_defer链表头
    gp._defer = d
    return d
}

在函数deferproc中,

  • 先获得调用deferproc之前的rsp寄存器的值,后面进行deferreturn时会通过这个值去进行判断要执行的defer是否属于当前调用者
  • 通过newdefer 函数分配一个 _defer 结构体对象并放入当前 goroutine 的 _defer 链表的表头
  • 然后会将参数部分拷贝到紧挨着defer对象后面的地址:deferArgs(d)=unsafe.Pointer(d)+unsafe.Sizeof(*d)
  • 执行return0函数,正常情况下返回0,经过test %eax,%eax检测后继续执行业务逻辑。异常情况下会返回1,并且直接跳转到deferreturn

deferreturn

// 编译器会在调用过 defer 的函数的末尾插入对 deferreturn 的调用
// 如果有被 defer 的函数的话,这里会调用 runtime·jmpdefer 跳到对应的位置
// 实际效果是会一遍遍地调用 deferreturn 直到 _defer 链表被清空
// 这里不能进行栈分裂,因为我们要该函数的栈来调用 defer 函数
func deferreturn(arg0 uintptr) {
    gp := getg()
    // defer函数链表
    // 也是第一个defer
    d := gp._defer
    if d == nil {
        //由于是递归调用,
        //递归终止
        return
    }
    //获取调用deferreturn时的栈顶位置
    sp := getcallersp()
    // 判断当前栈顶位置是否和defer中保存的一致
    if d.sp != sp {
        //如果保存在_defer对象中的sp值与调用deferretuen时的栈顶位置不一样,直接返回
        //因为sp不一样表示d代表的是在其他函数中通过defer注册的延迟调用函数,比如:
        //a()->b()->c()它们都通过defer注册了延迟函数,那么当c()执行完时只能执行在c中注册的函数
        return
    }

    //把保存在_defer对象中的fn函数需要用到的参数拷贝到栈上,准备调用fn
    //注意fn的参数放在了调用调用者的栈帧中,而不是此函数的栈帧中
    switch d.siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
    default:
        memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
    }
    fn := d.fn
    d.fn = nil
    // 指向 defer 链表下一个节点
    gp._defer = d.link
     // 进行释放,归还到相应的缓冲区或者让gc回收
    freedefer(d)
    //执行defer中的func
    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

在函数deferreturn

  • 判断当前goroutine上是否还有绑定的defer,若没有,直接return。若有,则获取链表头部的defer
  • 通过判断当前defer中存储的sp是否和调用者的sp一致,来证明当前defer不是在此调用函数中声明的。
  • 将保存在_defer对象中的fn函数需要用到的参数拷贝到栈上,准备调用defer后的函数
  • 释放defer
  • 通过jmpdefer函数执行defer后的func

总结

  • 在编译在阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()
  • defer定义的延迟函数参数在defer语句出时就已经确定下来了
  • defer定义顺序与实际执行顺序相反
  • 对匿名函数采用defer机制,可以使其观察函数的返回值

转载自:https://www.jianshu.com/p/387ad32c6441