Mina 开发者中文文档 - 协议架构(下)
共识是指网络决定哪些信息将被保留在区块链中的过程。在像区块链这样的系统中,用新信息扩展区块链的责任分布在整个网络的参与节点上。这些节点不能被假定为网络中的“诚实”节点,因此,网络中的“诚实”参与者必须合作,以将各个节点提出的信息进行选择性保留。有许多不同的方法可以实现分布式共识,我们将其称为共识机制。在我们的代码库中,我们建立了一个单一的接口,通过这个接口,共识机制通知协议的其余部分。通过这个接口,共识机制的各种实现可以在编译时交换出来,从而允许我们编写协议的其余部分而不依赖于所选择的共识机制。因此,我们可以将共识机制看作是某种东西,一旦定义,协议的其余部分就可以建立在它之上。
共识机制控制建立在协议之上的许多方面。为了便于讨论,我们将共识机制的规定分为两部分:数据(可用的数据结构和与之可用的交互)和钩子(构建在共识机制之上的协议调用的特定顶级钩子)。由共识机制提供的数据结构对于与之交互的外部系统来说是抽象的,而主要被共识机制的钩子所使用。
! !注意:本文档只涵盖了Mina中的广义共识机制抽象。有关实施Mina权益证明的信息,请参阅相关文件。
Local_state
是与共识相关的状态,只存储在本机本地。它为共识提供了一个位置,以便在协议状态的最终点上跟踪本地节点上的一些信息。查看frontier_root_transition
钩子了解更多细节
Consensus_transition_data
是包含在Snark_transition
中的一段状态。它为共识提供了一个地方去添加额外的信息,而这些被证明的信息是为了向snark证明一个转换。与Consensus_state
不同,Consensus_transition_data
中的信息仅由创建转换的节点使用,而其他节点不可用。
Consensus_state
包含在协议的Protocol_state
中。它为共识机制提供了一个存储信息的地方,这些信息在协议的每个状态都是可用的,并且可以在snark中得到证明。因为它包含在Protocol_state
,所以网络上的其他节点都可以检查到这个信息。
待办事项
待办事项
●Blockchain_state
● Protocol_state
● Snark_transition
generate_transition
的钩子完全生成一个新的协议状态和共识转换数据,从而作为区块链上先前协议状态的扩展。这个功能提供了新的区块链状态,以及要包括在分段分类帐差异和相关建议数据中的交易。这个钩子旨在由提议者调用,并在作为previous_protocol_state
传入,同时最好tip位于前端。倡议者应该与next_proposal
钩子进行交互,以确定何时应该调用这个钩子。
next_proposal
的钩子会通知协议何时可以有效地生成并提议下一次转换。这个钩子可以返回建议转换的时间,或者再次检查钩子的时间。这允许一种共识机制来分隔提案,并根据协议中的其他信息控制更高级的提案调度。
next_state_checked
钩子将一个经过检查的计算结果返回,该计算结果将计算或约束区块链序列中的下一个共识状态。这个钩子被包含在组成区块链snark约束系统的检查计算中。因此,这是在共识状态上执行验证的主要功能。
select
钩子通知协议,并给定两个状态,表明哪个状态在共识机制的眼里是“更好的”。这个钩子在如何达成共识方面起着至关重要的作用,因为它是决定两条链之间哪个应该被保留的主要操作。当被调用时,它将通知调用者要么Keep
现有状态,要么Take
候选状态。
received_at_valid_time
钩子是有断定性的,用于判断给定的共识状态在给定的时间是否有效。这使得共识机制可以选择性地控制共识状态何时有效生成的时间属性,如果它是在带外接收的,则该协议状态被拒绝接收。协议的实现应该在从网络接收到转换之前,在处理转换并将其添加到边界之前调用这个钩子。
当一个转换发生在最终点(在mina中,这是转换边界的root)时,frontier_root_transition
钩子需要被调用。这为共识机制提供了一个根据网络最终状态更新其本地状态的机会。
should_bootstrap
钩子通知协议通过网络接收到的转换是否需要节点开始引导其状态到当前网络状态。共识机制决定状态结束之前需要多长时间,以及节点是否也应该引导到这个概念。当从网络接收到转换并且初始验证成功执行时,应该调用这个钩子。
Mina中实现的权益证明共识机制是Ouroboros Praos协议的一个版本,针对我们简洁的区块链进行了略微的扩展和修改。本文档将提供关于Ouroboros权益证明如何工作概述,并对我们的更改和增加的内容进行详细介绍。关于Ouroboros协议的详细描述,请参阅Ouroboros协议的原始文件:Original 和 Praos。
正式来说,我们对Ouroboros的实现是Praos的延伸。然而,有一篇新的论文延伸了Praos,叫做Ouroboros Genesis。这个扩展修复了一个涉及长程攻击的漏洞,但论文中的描述不能实现在一个轻量级的区块链协议中。作为替代,我们将介绍一种新的、简洁的方法来防止长程攻击,这将在本文档的另一节中进行详细介绍。
在Ouroboros中,节点需要在前一个epoch开始时实现/保存一个账本,以便计算VRF。这是因为仅仅有一个VRF输出不足以知道一个区块是否是被诚实地提出。为了确认一个区块是由提议它的节点赢得的,VRF输出必须低于提议者的质押决定的阈值,其获胜的概率与账本中的总货币成比例。在一个轻量级的协议中,如Mina,实例化/储存这样的账本在过去不是很容易。与一般的区块链不同,节点不能为了任意请求过去的区块链数据,以重建它们感兴趣的信息而。这意味着,如果我们以一种轻量级的方式来实现这个功能,我们将需要在任何时间点总共保留整个账本的3个副本,并且在我们能够访问该信息之前,在线等待至少2个完整的epoch。不过,只要稍加思考,我们就能做得更好。
Ouroboros和Mina如何证明区块的赢家有着很大的区别。在Ouroboros中,每个节点在收到区块时都需要验证区块提议者是否真的赢得了区块,这意味着它们必须自己查找评估VRF的公钥的余额。而在Mina中,VRF结果的正确性可以在SNARK中计算,所以其他节点只需要验证SNARK就可以知道区块是由获胜者提出的。因为提议者自己证明了这一点,所以它可以将需要存储的关于epoch账本的信息限制为只存储帐户记录和它可以提出的任何帐户(它自己和委托给它帐户)的Merkle路径。此外,由于Ouroboros保证我们将在两个epoch之前达到终局性,提议者可以在最终完成后等待捕获该信息,这进一步限制了它需要存储的信息。信息输入到SNARK这证明:1)VRF是由提供的公钥(账户)对应的私钥生成,和2)epoch账本公钥(账户)余额(token)赢得区块的所对应的VRF阈值应大于提供的VRF。(使用Merkle路径来证明epoch账本的merkle根帐户)。这并不能立即解决提议者节点需要在线足够长的时间来存储必要信息的问题,但它为提议者提供了获取该信息的其他途径。主要是,提议人可以要求一个帐户记录和Merkle路径证明它的存在在一个给定的epoch账本。在目前的实现中,没有节点存储这些信息并使其可供访问,但可以想象,在未来有一种服务将被构建,该服务允许提议者通过提供这些信息更快地在线并活跃起来,但可能需要支付某种费用。
长程攻击防御
待办事项
持续修改配置
待办事项
实现细节
待办事项
大多数协议只有一组主要的节点运营者(通常称为矿工、验证人或区块生产者),而Mina有第二组——Snark工作者。
Snark工作者是Mina网络健康不可或缺的一部分,因为这些节点负责对网络中的事务进行Snark证明。通过制作这些证明,Snark工作者帮助保持Mina区块链的轻量化。
在本文档中,我们将简要介绍为什么需要Snark工作者以及如何协调经济激励机制,并提供实行Snark工作操作细节。请随意点击上面与您的需求最相关的内容。
注意:本文档主要针对节点运营者,不包括zk-SNARKs理论。阅读本节不需要对SNARKs有深入的了解,但是大致了解SNARKs的工作原理和它们的用途将会很有帮助。如果不是这样,请先查看SNARKs的引论。
区块链的简洁性是Mina的独特属性。每个区块生产者在向网络提议一个新区块时,必须同时包含一个zk-SNARK。这允许节点丢弃所有已具备终局性的历史数据,而只保留SNARK数据。如果你不熟悉Mina协议,那这个视频是一个很好的开始。
然而,对于Mina中的区块生产者来说,生成区块的证明是不够的。事务也需要被生成证明。原因是区块链SNARK没有对区块中包含的事务的有效性做出任何声明。
例如,假设区块链的当前头有一个状态哈希a6f8792226...
,我们会收到一个状态哈希为0ffdcf284f...
的新区块。该区块将包含区块生产者选择包含在该区块中的所有事务以及相关的元数据。我们还会收到一个附带的SNARK来验证陈述内容:
“存在一个状态哈希值为0ffdcf284f的区块,接在之前最好的状态哈希值a6f8792226后。”
注意,这条陈述内容没有说明新区块中包含的交易的有效性。如果我们相信这句话,而不做任何其他事情,我们可能会被发送该区块的恶意区块生产者所欺骗。幸运的是,我们有原始区块,我们可以检查每个交易以确保它是有效的。但是,如果网络中的节点只是想接收证明,而不是验证每个区块呢?
为了确保节点可以在不信任Mina的情况下操作,每个节点都可以验证链的状态,而不需要重放事务,这一点很重要。为了达到这个目标,仅靠前述的区块链SNARK是不够的。我们需要知道这些交易也是有效的。好吧,SNARKs确实很适合做这些,简单的建议就是为每笔事务生成SNARK,再把他们组合在一起。
然而,生成SNARK证明在计算上是昂贵的——如果我们必须为每个交易连续地计算SNARK,吞吐量会非常低,区块生成时间会飙升。此外,现实环境中的交易是异步到达的,因此很难预测何时执行下一项工作。
幸运的是,我们可以利用SNARKs的两个属性:
1.证明可以合并——两个证明可以合并形成一个合并证明
2.归并是相关联的-归并证明是相同的,和归并的顺序无关
这两个性质本质上允许我们利用并行性。如果证明可以合并,不管它们是如何合并的,那么SNARK证明就可以并行生成。先完成的证明可以与正在进行的证明结合起来。这可以想象为一个二叉树,其中底部的行(叶)由单个交易证明和每个父行(各自的归并证明集)组成。我们可以一直组合到根节点,根节点代表了通过执行所有交易执行的状态更新。
此外,因为这些证明并不相互依赖,而且我们可以利用并行性,这意味着任何人都可以做这项工作!最终的结果是非受信的分布式工作池的。任何有闲置计算机的人都可以作为Snark工作者加入网络,去观察需要被Snark的交易,并贡献他们的计算。当然,在我们亲切地称之为snarketplace的地方,他们会因为他们的工作得到补偿。
注意:要了解更多关于这个Snark工作方案如何演变的细节,强烈建议观看这个视频:高吞吐量与缓慢的Snark。如果您对函数式编程和扫描状态的细节(如上所述的树形结构)感兴趣,我们有一篇关于技术细节的博客文章,如果您喜欢这种格式,可以看一段视频。
区块生产者使用他们的区块奖励从Snark工作者那里购买Snark工作。
定价与协议无关,对Snark工作者也不存在协议级别的奖励。这种激励纯粹是点对点的,并且动态地建立在一个公共市场上,也就是所谓的snarketplace。
你可能会问,为什么区块生产者需要购买Snarks?问得好,我们之前有提到过这个原因。为了确定Mina区块链的状态头是有效的,交易需要被Snark。但是,如果我们继续增加更多的交易,而未以相同的速度进行处理,那么随着时间的推移,我们就会积累永远无法完成的工作。为了达到稳定的平衡状态,我们需要以与工作增加的速率大致相同的速率处理它。
由于区块生产者从将交易包含在区块中(通过交易费和coinbase交易)获利,他们也有责任向Snark工作者提供的Snark工作支付费用,从而创造对Snark工作的需求。然而,他们总是希望是从snarketplace以最低的价格购买Snark工作。相反,Snark工作者想要最大化他们的利润,出售他们的Snark工作。这两种角色扮演着市场的两方角色,随着时间的推移,以一个市场价格建立起一种平衡。
我们预期的snarketplace动态再平衡-例如,遵循简单的供求规律。虽然每一种Snark工作都适用于不同的交易,但从更大的角度来看,Snark工作在很大程度上是一种商品(这意味着哪一个Snark工作者生产的并不重要——它们都是一样的)。然而,有一些细微差别,所以它可能有助于一些启发式的定价策略:
· 如果市场价格是X,那么以低于X的价格(例如:X - 1)出售Snark工作都是有效的,只要在扣除运营费用后是盈利的。
· 区块生产者被鼓励从同一个Snark工人购买更多单位的Snark工作,因为他们只需要发起一笔费用支付事务。
基本上,区块生产者支付Snark工人的方式是通过一种称为费用转移的特殊交易类型。BP的动机是尽量减少费用转移的数量,因为每一笔交易都是独立的,需要添加到一个区块中(因此会被更多的Snark交易抵消)。因此,最好的情况是从同一个Snark工作者那里购买一捆Snark工作。
· 一些Snark工作比其他更重要,因为它将释放整个树的内存(更多细节请参阅上面的视频)。这是通过不同的工作选择方法实现的。目前,本地支持的两种方法是顺序和随机。然而,这两种方法都没有利用动态市场,这是Mina社区可以开发解决方案的改进领域。
由于所有关于snarketplace和价格的数据都是公开的,有几种方法可以检查snarketplace。一个例子是使用GraphQL API,其他选项包括使用CLI,或者使用自定义解决方案来跟踪snark内存池中的snark。
请继续关注关于snarketplace动态的更详细分析。我们不久还将发布一份经济白皮书,提供更多的背景信息。
参见:SNARKs和Snark工作者常见问题解答。
扫描状态是一个数据结构, 允许解耦事务的SNARKs生产,从区块生产者转移到Snark工作者。
由于区块生产者不再必须生产这些事务SNARKs,不管事务吞吐量如何,区块生产时间可能保持不变。此外,扫描状态数据结构允许交易SNARK证明生成被并行化,并由多个竞争关系的snark工作者完成。
扫描状态由大量的满二叉树构成,树上每个节点都是一位Snark工作者要完成的工作。扫描状态定期从树顶返回一个单独证明,可证明树底部所有交易的正确性。
区块生产者在他们所生产的SNARK区块链中包含发出的账本证明,证实该区块链目前状态有效,证明SNARK账本中所包含的所有事务的有效性。
因此,无论吞吐量如何,区块时间可以保持恒定,而扫描状态能够调整以匹配所需的事务吞吐量。
注意
在稳定状态下,若所有slot已满,所有的所需证明完成,则每区块都会发出一个分类账证明。
区块生产者在构造区块时,所添加的交易量或许会达到扫描状态常数所定义的最高交易量。通过所添的交易,他们可以赚取所有现有的交易费,得到coinbase奖励。他们增加的每一个交易都会被转化为新的基本工作,添加到扫描状态。
对于每个新增交易而言,区块生产者必须添加等量完成的Snark工作,这些工作与扫描状态中现存的一系列作业相对应。这些已完成的作业添加到扫描状态后,会创造新的合并作业,但添加到根节点的任务除外。在这种情况下,证明只是作为一个结果返回。
区块生产者或许会从snark池中现有的任何出价的Snark 工作者那里购买完成的任务,而不是自己完成工作。
扫描状态常数
下列常数规定扫描状态的结构和特性:
● transaction_capacity_log_2
● work_delay
常数 transaction_capacity_log_2 规定了一个区块中所能包含的最多事务量:
max_no_of_transactions = 2^{transaction_capacity_log_2}
工作延迟保证Snark工作者有充足的时间完成任务。若不能获得完成的证明,区块生产者则不能打包任何交易。利用工作延迟,扫描状态中可存在的最多满二叉树量遵照以下规定:
max_number_of_trees = (transaction_capacity_log_2 + 1) * (work_delay + 1) + 1
每个区块可添加的最多证明遵照以下规定:
max_number_of_proofs = 2^{transaction\_capacity_log_2 + 1} - 1
扫描状态的这些约束条件确保每个区块可发出一个证明,在添加与子节点相对应的证明后,即将升级的合并节点一直是空的。
尽管最大事务量可能是固定的,但可以发生动态调整以便与交易吞吐量相适应。同样,扫描状态可以处理无限的交易吞吐量,尽管其代价是交易证明延迟时间增加(呈对数型)。
用max_no_of_transactions = 4
和work_delay = 1
计算扫描状态。相应地,这意味着完成的最大工作量为7,最多树的数量为7. 在创始区块里,扫描状态为空。
区块1:区块生产者在标签为B1
的扫描状态中添加四个交易。这些交易填满第一棵树的底部。
区块2:在第二个区块中,区块生产者又添加了四个交易(B2
)。这些交易添加到第二棵树,再次填满了其底部。由于区块1的工作延迟,无需证明。
区块3:在第三个区块中,一位区块生产者给第三棵树添加了四个B3
交易,但必须在第一棵树上包含4个证明。由于添加这些完成的基础证明,因此创造了两个新的M3
合并作业。
注意:
B
或M
分别表示基础或合并作业,数字表示添加到扫描状态的顺序。
区块4:在第四个区块中,区块生产者在第四颗树的底部添加了四个交易(B4
)。他们必须添加与区块2的工作相对应地四个证明。因此再次创造了两个M4
合并作业。
注意: 任何挂起工作(用橘色显示)都是Snark工作者要完成的任务。Snark工作者向Snark池提交已完成的工作。可能有多名Snark工作者完成这个工作,但是区块生产者可能购买的只有Snark池中价格最低的Snark证明。
区块5:在第五个区块中,添加四个交易填满第五棵树(B5
)底部,必须包含六个证明(B3
和M3
)。M3
合并作业导致第一棵树上一个最终挂起的合并作业(M5
)。
区块6:在第六个区块中,添加四个交易(B6
),填满第六棵树的底部。包含六个证明(B4
和M4
),创造三个新的合并作业(M6
)。
区块7:在第七个区块中,区块生产者另添加四个交易(B7
),填满第七棵树底部。根据指定的扫描状态常数,最多只能有七棵树。包含(B5
和M5
)证明达到最多(7)。这些包含的证明创造三个新的合并作业(M7
),此外,扫描状态发出最顶部的M5
证明。
该证明是第一棵树发出的账本证明,与区块1添加的交易相对应。这棵树的内容会被移除,以便为额外的交易创造空间。
区块8:在第八个区块中,区块生产者添加两个交易(B8
),包含4个(B6)证明。这些包含的证明引起两个新的合并作业(M8
)。注意,添加两个交易只需四个证明。`
注意: Snark工作裹进的工作包,往往包含两个workId,一棵树的最终root证明的情况除外。一个交易按比例分配需要两个证明,因此这就确保了所包含的交易和将购买的snark工作数量相等。
区块9:在第九个区块中,区块生产者添加了三个交易(B9
)。目前没有被填满的树上,那些将被填满的时隙需要三个证明(M6
)。在之前的区块添加了四个证明,因此还只需得到三个证明(鉴于最大工作量为7)。第二棵树的M6
证明作为账本证明返回。第三个B9
交易进入现在的空树,添加了两个B7
证明。
区块10:在区块十中,区块生产者添加四个交易,因而包含七个证明(B7
,M7
,两个B8
)。
区块11:在第十一个区块中,区块生产者添加三个交易(B11
),依次完成五个证明(B9
、B9
、M8
、M8
、M9
)。此外,M9
分类账证明从第四棵树返回。
注意
用mina advanced snark-job-list命令查看扫描状态内容。
扫描状态的新添加作业是snark 工作者需要完成的挂起作业。snark 工作者完成所要求的SNARK交易,为他们的完成工作提交报价。当一个节点收到并证明完成工作的有效性,如果有效并且是所需工作的最低价格的话,会添加其到他们当地的snark池,还会传播给网络上的其他同行。
注意
尽管多位snark 工作者可以完成同一个工作,但是snark池中只包含最低价格的工作。
若一个区块生产者的一个区块中要包含完成的证明,以抵消他们所添加的任何交易,他们可以从snark池中购买相应的工作。比如,延续上述例子,以下一个区块(12)为例。若这位区块生产者想添加三个交易,包括coinbase,用户付费和对snark 工作者手续费转移,他们需购买三个完成的snark工作。这点与6个B9
、B10
、M9
和M10
(来自第七棵树)证明相对应,因为每个snark工作包含两个workId。
在区块生成期间,除所需作业的最佳报价(在例子中,分别是0.025、0.165、0.1和0.5)之外,snark池还可包含完成的工作。
区块生产者在选择交易前,会考虑现有工作价格。区块生产者首先想添加的交易是coinbase交易,因为会有coinbase奖励。如果交易费不足以支付snark工作要被包含在snark池所需的手续费,则这些工作不会被添加。如果工作没有创造经济价值,则区块生产者永远不会购买。
假如按照所需次序,没有完成的snark工作可供购买,则区块中不会包含相应的交易。这或许会导致空区块,而且在没有交易可以添加(包括coinbase交易)的情况下,该区块生产者不会获得奖励。
注意
可通过GraphQL或mina advanced snark-pool
命令CLI查看现有的snark池。
一个时间锁定账户不允许将其余额减少到最小值以下的支付,最小值取决于区块高度。
要创建一个时间锁定,必须在创建新帐户时提供配置。这种情况只会发生在网络刚开始时的初始账本中。在本节中,我们将探索时间锁定背后的机制,并了解如何与锁定时间的帐户交互。
对于当前版本,时间锁定的帐户的值是根据您注册的顺序分配的。
时间锁由以下字段initial_minimum_balance
、cliff
时间、vesting_period
时间和vesting_increment
组成。
如果您的账户有时间锁,您仍然可以使用它,只要您的账户有足够的资金。锁定时间的资金数量在网络开始时以initial_minimum_balance
为准。一旦网络达到与cliff相等的区块高度,时间锁定的数量开始以每一个vesting_period的vesting_increment
数量减少。
有关此过程的更多技术解释,请参阅RFC-0025,它有更深入的概述。
除了时间锁定账户,我们还引入了暴击奖励的概念。一旦账户没有被时间锁定的余额(新账户或整个initial_minimum_balance
已经被解锁),区块奖励将增加1倍(乘以2)。
如果您想披露某个特定时期的锁定账户的流动余额,它受以下函数管理(注意:计算账户的锁定部分):
(*
\* uint32 global_slot -- the "clock" it starts at 0 at the genesis block and ticks up every 3minutes.
\* uint32 cliff_time -- the slot where the cliff is (similar to startup equity vesting)
\* uint32 cliff_amount -- the amount that unlocks at the cliff
\* amount vesting_increment -- unlock this amount every "period"
\* uint32 vesting_period -- the period that we increment the unlocked amount
\* balance initial_minimum_balance -- the total locked amount until the cliff
*)
let min_balance_at_slot ~global_slot ~cliff_time ~cliff_amount ~vesting_period
~vesting_increment ~initial_minimum_balance =
let open Unsigned in
if Global_slot.(global_slot < cliff_time) then initial_minimum_balance
else
match Balance.(initial_minimum_balance - cliff_amount) with
| None ->
Balance.zero
| Some min_balance_past_cliff -> (
(* take advantage of fact that global slots are uint32's *)
let num_periods =
UInt32.(
Infix.((global_slot - cliff_time) / vesting_period)
|> to_int64 |> UInt64.of_int64)
in
let vesting_decrement =
UInt64.Infix.(num_periods * Amount.to_uint64 vesting_increment)
|> Amount.of_uint64
in
match Balance.(min_balance_past_cliff - vesting_decrement) with
| None ->
Balance.zero
| Some amt ->
amt )
在当前版本中,创建锁定时间账户的唯一方法是在初始账本时。在未来的版本中,我们可能会在mina client
和GraphQL API中添加命令,允许您创建一个新的锁定时间的帐户。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!