作者:joohhnnn
optimism中区块的传递
区块的传递是整个 optimism rollup 体系中较为重要的概念,在这一章节,咱们将从介绍 optimism 中多种 sync 办法的原理,来揭开整个体系里区块的传递进程。
区块类型
在进行进一步深化前,让咱们了解一些基本的概念。
-
Unsafe L2 Block (不安全的 L2 区块):
-
这是指 L1 链上最高的 L2 区块,其 L1 来源是标准 L1 链的 或许 扩展(如 op-node 所知)。这意味着,虽然该区块链接到 L1 链,但其完好性和正确性没有得到充分验证。
-
Safe L2 Block (安全的 L2 区块):
-
这是指 L1 链上最高的 L2 区块,其 epoch 的序列窗口在标准的 L1 链中是完好的(如 op-node 所知)。这意味着该区块的一切前提条件都已在 L1 链上得到验证,因此它被认为是安全的。
-
Finalized L2 Block (定稿的 L2 区块):
-
这是指已知彻底源自定稿 L1 区块数据的 L2 区块。这意味着该区块不只安全,而且已根据 L1 链的数据彻底承认,不会再发生更改。
sync 类型
-
op-node p2p gossip 同步:
-
op-node 经过 p2p gossip 协议接纳最新的不安全区块,由 sequencer 推送的。
-
op-node 根据 libp2p 的恳求-呼应的逆向区块头同步:
-
经过此同步办法,op-node 能够填补不安全区块的任何缺口。
-
履行层(EL,又叫 engine sync)同步:
-
在 op-node 中有两个标志,答应来自 gossip 的不安全区块触发引擎中向这些区块的长规模同步。相关的标志是
--l2.engine-sync
和--l2.skip-sync-start-check
(用于处理非常旧的安全区块)。然后,假如为此设置了 EL,它能够履行任何同步,例如 snap-sync(需求 op-geth p2p 衔接等,而且需求从某些节点进行同步)。 -
op-node RPC 同步:
-
这是一种根据可信 RPC 办法的同步,当 L1 呈现问题时,这种同步办法相对简单。
-
L2EngineSyncEnabled Flag (
l2.engine-sync
): -
该标志用于启用或禁用履行引擎的 P2P 同步功用。当设置为
true
时,它答应履行引擎经过 P2P 网络与其他节点同步区块数据。它的默许值是false
,意味着在默许情况下,该 P2P 同步功用是禁用的。 -
SkipSyncStartCheck Flag (
l2.skip-sync-start-check
): -
该标志用于在确定同步起始点时,越过对不安全 L2 区块的 L1 来源一致性的合理性查看。当设置为
true
时,它会推迟 L1 来源的验证。假如你正在运用l2.engine-sync
,建议启用此标志来越过初始的一致性查看。它的默许值是false
,意味着在默许情况下,该合理性查看是启用的。
op-node p2p gossip 同步
这种同步的场景处于:当 l2 的块新发生的时分,即在上一节咱们评论的 sequencer 形式下是如何发生新的区块的。
当发生新的区块后,sequencer 经过根据 libp2p 的 P2P 网络的 pub/sub(播送/订阅)模块,向’新 unsafe 区块‘ topic 发出播送。一切订阅了此 topic 的节点都会直接或间接的收到这一播送音讯。详情能够查看[2]
op-node 根据 libp2p 的恳求-呼应的逆向区块头同步
这种同步的场景处于:当节点因为特殊情况,比方宕机后重新链接,或许会发生一些没有同步上的区块(gaps)
当这种情况呈现的时分,能够经过 p2p 网络的反向链的办法快速同步,即经过运用 libp2p 原生的 stream 流来和其他 p2p 节点建立链接,一起发送同步恳求。详情能够查看[3]
履行层(EL,又叫 engine sync)同步
这种同步的场景处于:当有较多区块,一个大规模区块需求同步的时分,从 l1 慢慢派生比较慢,想要快速同步。
运用--l2.engine-sync
和 --l2.skip-sync-start-check
去启动 op-node,发送的 payload 来达到发送长规模同步恳求的目的。
代码层讲解
首先咱们来看一下这两个标志的界说
在 op-node/flags/flags.go
中界说并解释了这两个 flag 的效果
L2EngineSyncEnabled = &cli.BoolFlag{
Name: "l2.engine-sync",
Usage: "Enables or disables execution engine P2P sync",
EnvVars: prefixEnvVars("L2_ENGINE_SYNC_ENABLED"),
Required: false,
Value: false,
}
SkipSyncStartCheck = &cli.BoolFlag{
Name: "l2.skip-sync-start-check",
Usage: "Skip sanity check of consistency of L1 origins of the unsafe L2 blocks when determining the sync-starting point. " +
"This defers the L1-origin verification, and is recommended to use in when utilizing l2.engine-sync",
EnvVars: prefixEnvVars("L2_SKIP_SYNC_START_CHECK"),
Required: false,
Value: false,
}
L2EngineSyncEnabled
L2EngineSyncEnabled
标志用于在 op-node 接纳到新的unsafe
的 payload(区块)后,发送给 op-geth 进一步验证时,触发 op-geth 的 p2p 之间 sync,在 sync 期间一切的unsafe
区块都会被视为经过验证,并进行下一个 unsafe 的流程。op-geth 内部的 p2p sync 比较适用于长规模的unsafe
区块的获取。其实在 op-geth 内部,不论L2EngineSyncEnabled
标志有没有启用,在遇到 parent 区块不存在的时分,都会敞开 sync 去同步数据。
让咱们深化代码层面看一下
首先是 op-node/rollup/derive/engine_queue.go
EngineSync
为L2EngineSyncEnabled
标志的具体表达。在这里嵌套在两个查看函数傍边。
// checkNewPayloadStatus checks returned status of engine_newPayloadV1 request for next unsafe payload.
// It returns true if the status is acceptable.
func (eq *EngineQueue) checkNewPayloadStatus(status eth.ExecutePayloadStatus) bool {
if eq.syncCfg.EngineSync {
// Allow SYNCING and ACCEPTED if engine P2P sync is enabled
return status == eth.ExecutionValid || status == eth.ExecutionSyncing || status == eth.ExecutionAccepted
}
return status == eth.ExecutionValid
}
// checkForkchoiceUpdatedStatus checks returned status of engine_forkchoiceUpdatedV1 request for next unsafe payload.
// It returns true if the status is acceptable.
func (eq *EngineQueue) checkForkchoiceUpdatedStatus(status eth.ExecutePayloadStatus) bool {
if eq.syncCfg.EngineSync {
// Allow SYNCING if engine P2P sync is enabled
return status == eth.ExecutionValid || status == eth.ExecutionSyncing
}
return status == eth.ExecutionValid
}
让咱们把视角转到 op-geth 的 eth/catalyst/api.go
傍边,当 parent 区块缺失后,触发 sync,而且返回SYNCING Status
func (api *ConsensusAPI) newPayload(params engine.ExecutableData) (engine.PayloadStatusV1, error) {
…
// If the parent is missing, we - in theory - could trigger a sync, but that
// would also entail a reorg. That is problematic if multiple sibling blocks
// are being fed to us, and even more so, if some semi-distant uncle shortens
// our live chain. As such, payload execution will not permit reorgs and thus
// will not trigger a sync cycle. That is fine though, if we get a fork choice
// update after legit payload executions.
parent := api.eth.BlockChain().GetBlock(block.ParentHash(), block.NumberU64()-1)
if parent == nil {
return api.delayPayloadImport(block)
}
…
}
func (api *ConsensusAPI) delayPayloadImport(block *types.Block) (engine.PayloadStatusV1, error) {
…
if err := api.eth.Downloader().BeaconExtend(api.eth.SyncMode(), block.Header()); err == nil {
log.Debug("Payload accepted for sync extension", "number", block.NumberU64(), "hash", block.Hash())
return engine.PayloadStatusV1{Status: engine.SYNCING}, nil
}
…
}
SkipSyncStartCheck
SkipSyncStartCheck
这个标识符主要是帮助在选择 sync 形式下,优化功能和削减不必要的查看。在已承认找到一个契合条件的 L2 块后,代码会越过进一步的健全性查看,以加快同步或其他后续处理。这是一种优化手法,用于在确定性高的情况下快速地进行操作。
在op-node/rollup/sync/start.go
目录中
FindL2Heads
函数经过从给定的“开始”(start)点(即之前的不安全 L2 区块)开始逐步回溯,来查找这三种类型的区块。在回溯进程中,该函数会查看各个 L2 区块的 L1 源是否与已知的 L1 标准链匹配,以及是否契合其他一些条件和查看。这答应函数更快地确定 L2 的“安全”头部,然后或许加快整个同步进程。
func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain, lgr log.Logger, syncCfg *Config) (result *FindHeadsResult, err error) {
…
for {
…
if syncCfg.SkipSyncStartCheck && highestL2WithCanonicalL1Origin.Hash == n.Hash {
lgr.Info("Found highest L2 block with canonical L1 origin. Skip further sanity check and jump to the safe head")
n = result.Safe
continue
}
// Pull L2 parent for next iteration
parent, err := l2.L2BlockRefByHash(ctx, n.ParentHash)
if err != nil {
return nil, fmt.Errorf("failed to fetch L2 block by hash %v: %w", n.ParentHash, err)
}
// Check the L1 origin relation
if parent.L1Origin != n.L1Origin {
// sanity check that the L1 origin block number is coherent
if parent.L1Origin.Number+1 != n.L1Origin.Number {
return nil, fmt.Errorf("l2 parent %s of %s has L1 origin %s that is not before %s", parent, n, parent.L1Origin, n.L1Origin)
}
// sanity check that the later sequence number is 0, if it changed between the L2 blocks
if n.SequenceNumber != 0 {
return nil, fmt.Errorf("l2 block %s has parent %s with different L1 origin %s, but non-zero sequence number %d", n, parent, parent.L1Origin, n.SequenceNumber)
}
// if the L1 origin is known to be canonical, then the parent must be too
if l1Block.Hash == n.L1Origin.Hash && l1Block.ParentHash != parent.L1Origin.Hash {
return nil, fmt.Errorf("parent L2 block %s has origin %s but expected %s", parent, parent.L1Origin, l1Block.ParentHash)
}
} else {
if parent.SequenceNumber+1 != n.SequenceNumber {
return nil, fmt.Errorf("sequence number inconsistency %d <> %d between l2 blocks %s and %s", parent.SequenceNumber, n.SequenceNumber, parent, n)
}
}
n = parent
// once we found the block at seq nr 0 that is more than a full seq window behind the common chain post-reorg, then use the parent block as safe head.
if ready {
result.Safe = n
return result, nil
}
}
}
op-node RPC 同步
这种同步场景处于: 当你有信任的 l2 rpc 节点的时分,咱们能够直接和 rpc 通信,发送较短规模的同步恳求,和 2 相似。假如设置,在反向链同步中会优先运用 RPC 而不是 P2P 同步。
关键代码
op-node/node/node.go
初始化 rpcSync,假如 rpcSyncClient 设置,赋值给 rpcSync
func (n *OpNode) initRPCSync(ctx context.Context, cfg *Config) error {
rpcSyncClient, rpcCfg, err := cfg.L2Sync.Setup(ctx, n.log, &cfg.Rollup)
if err != nil {
return fmt.Errorf("failed to setup L2 execution-engine RPC client for backup sync: %w", err)
}
if rpcSyncClient == nil { // if no RPC client is configured to sync from, then don't add the RPC sync client
return nil
}
syncClient, err := sources.NewSyncClient(n.OnUnsafeL2Payload, rpcSyncClient, n.log, n.metrics.L2SourceCache, rpcCfg)
if err != nil {
return fmt.Errorf("failed to create sync client: %w", err)
}
n.rpcSync = syncClient
return nil
}
启动 node,假如 rpcSync 非空,敞开rpcSync eventloop
func (n *OpNode) Start(ctx context.Context) error {
n.log.Info("Starting execution engine driver")
// start driving engine: sync blocks by deriving them from L1 and driving them into the engine
if err := n.l2Driver.Start(); err != nil {
n.log.Error("Could not start a rollup node", "err", err)
return err
}
// If the backup unsafe sync client is enabled, start its event loop
if n.rpcSync != nil {
if err := n.rpcSync.Start(); err != nil {
n.log.Error("Could not start the backup sync client", "err", err)
return err
}
n.log.Info("Started L2-RPC sync service")
}
return nil
}
op-node/sources/sync_client.go
一旦接纳到s.requests
通道里的信号后(区块号),调用fetchUnsafeBlockFromRpc
函数从 RPC 节点中获取相应的区块信息。
// eventLoop is the main event loop for the sync client.
func (s *SyncClient) eventLoop() {
defer s.wg.Done()
s.log.Info("Starting sync client event loop")
backoffStrategy := &retry.ExponentialStrategy{
Min: 1000 * time.Millisecond,
Max: 20_000 * time.Millisecond,
MaxJitter: 250 * time.Millisecond,
}
for {
select {
case <-s.resCtx.Done():
s.log.Debug("Shutting down RPC sync worker")
return
case reqNum := <-s.requests:
_, err := retry.Do(s.resCtx, 5, backoffStrategy, func() (interface{}, error) {
// Limit the maximum time for fetching payloads
ctx, cancel := context.WithTimeout(s.resCtx, time.Second*10)
defer cancel()
// We are only fetching one block at a time here.
return nil, s.fetchUnsafeBlockFromRpc(ctx, reqNum)
})
if err != nil {
if err == s.resCtx.Err() {
return
}
s.log.Error("failed syncing L2 block via RPC", "err", err, "num", reqNum)
// Reschedule at end of queue
select {
case s.requests <- reqNum:
default:
// drop syncing job if we are too busy with sync jobs already.
}
}
}
}
}
接下来咱们来看看从哪里往s.requests
通道发送信号的呢?
同文件下的RequestL2Range
函数,此函数介绍一个需求同步的区块规模,然后将任务经过 for 循环,别离发送出去。
func (s *SyncClient) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef) error {
// Drain previous requests now that we have new information
for len(s.requests) > 0 {
select { // in case requests is being read at the same time, don't block on draining it.
case <-s.requests:
default:
break
}
}
endNum := end.Number
if end == (eth.L2BlockRef{}) {
n, err := s.rollupCfg.TargetBlockNumber(uint64(time.Now().Unix()))
if err != nil {
return err
}
if n <= start.Number {
return nil
}
endNum = n
}
// TODO(CLI-3635): optimize the by-range fetching with the Engine API payloads-by-range method.
s.log.Info("Scheduling to fetch trailing missing payloads from backup RPC", "start", start, "end", endNum, "size", endNum-start.Number-1)
for i := start.Number + 1; i < endNum; i++ {
select {
case s.requests <- i:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
在外层的 OpNode 类型的RequestL2Range
完成办法里。能够清楚的看到rpcSync
类型的反向链同步是优先选择的。
func (n *OpNode) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef) error {
if n.rpcSync != nil {
return n.rpcSync.RequestL2Range(ctx, start, end)
}
if n.p2pNode != nil && n.p2pNode.AltSyncEnabled() {
if unixTimeStale(start.Time, 12*time.Hour) {
n.log.Debug("ignoring request to sync L2 range, timestamp is too old for p2p", "start", start, "end", end, "start_time", start.Time)
return nil
}
return n.p2pNode.RequestL2Range(ctx, start, end)
}
n.log.Debug("ignoring request to sync L2 range, no sync method available", "start", start, "end", end)
return nil
}
总结
理解了这些同步办法后,咱们知道了unsafe的payload
(区块)究竟是怎样进行传递的。不同的 sync 模块对应着在不同场景下的区块数据传递。那么整个网络中如何一步步的将unsafe
的区块变成safe
区块,然后再进行 finalized 的呢?这些内容会在其他章节进行讲解。
此时快讯
【LayerZero已上线Horizen公共EVM兼容侧链和智能合约平台Horizen EON】10月25日消息,LayerZero Labs发文表示,跨链互操作性协议LayerZero已上线Horizen公共EVM兼容侧链和智能合约平台Horizen EON。基于Horizen EON的开发者现可与40多个支持LayerZero的链以及部署在这些区块链上的智能合约进行交互。