optimism-rollup 技术原理

optimism-rollup 做为目前最流行的以太坊L2解决方案,最近研究了下,写个笔记。

另外,layer2并不是侧链,而是以太坊的扩展,layer1到layer2的交易,并不是跨链交易,而是跨域交易.

optimism 的项目源码在 https://github.com/ethereum-optimism/optimism
虽然是一个项目,但是分了很多层。为了方便理解,把每层的作用先记录下.

一.代码结构

代码结构.jpg

项目分了5层:

  • l2geth
  • contracts
  • data-transport-layer
  • batch-submitter
  • message-relayer

上面的5层共同构成了optimism-rollup 这个系统。下来分别了解下:

l2geth:

这是fork的以太坊的1.9.10版本,里面增加了个rollup包,实现了layer2上的两种角色:Sequncer,Verifier.

  • Sequencer 用于侦听layer1的跨域消息,并且将交易改为OVMMessage到 虚拟机(OVM)中运行,

  • Verifier 用于验证layer2上的Sequencer提交的交易的正确性。

以下是两种角色的启动代码:

 func (s *SyncService) Start() error {
    if !s.enable {
        return nil
    }
    log.Info("Initializing Sync Service", "eth1-chainid", s.eth1ChainId)

    // When a sequencer, be sure to sync to the tip of the ctc before allowing
    // user transactions.
    if !s.verifier {
        err := s.syncTransactionsToTip()
        if err != nil {
            return fmt.Errorf("Cannot sync transactions to the tip: %w", err)
        }
        // TODO: This should also sync the enqueue'd transactions that have not
        // been synced yet
        s.setSyncStatus(false)
    }

    if s.verifier {
        go s.VerifierLoop()
    } else {
        go s.SequencerLoop()
    }
    return nil
}

packages包下有几个文件夹,不过主要的模块是: batch-submitter,contracts,data-transport-layer,message-relayer. 我们分别说明下先了解下这些结构所伴演的角色:

batch-submitter

向layer1的CTC chain和 SCC chain分别提交layer2的交易和交易的状态根。里面分别实现了两个typescript文件,state-batch-submitter.ts 和 tx-batch-submitter.ts 这两个文件就是通过
分别向 scc chain 和 ctc chain提交状态和交易的两个文件。另外,在CTC chain中,区块叫batch,也是交易的集合。 batch-sumitter.ts就是每隔一段时间,从layer2中,从当前ctc的index开始,获取一批交易.组成一个batch, 提交到ctc chain的 appendSequencerBatch 中去。代码如下:

  public async _submitBatch(
    startBlock: number,
    endBlock: number
  ): Promise<TransactionReceipt> {
    // Do not submit batch if gas price above threshold
    const gasPriceInGwei = parseInt(
      ethers.utils.formatUnits(await this.signer.getGasPrice(), 'gwei'),
      10
    )
    if (gasPriceInGwei > this.gasThresholdInGwei) {
      this.log.warn(
        'Gas price is higher than gas price threshold; aborting batch submission',
        {
          gasPriceInGwei,
          gasThresholdInGwei: this.gasThresholdInGwei,
        }
      )
      return
    }

    const [
      batchParams,
      wasBatchTruncated,
    ] = await this._generateSequencerBatchParams(startBlock, endBlock)
    const batchSizeInBytes = encodeAppendSequencerBatch(batchParams).length / 2
    this.log.debug('Sequencer batch generated', {
      batchSizeInBytes,
    })

    // Only submit batch if one of the following is true:
    // 1. it was truncated
    // 2. it is large enough
    // 3. enough time has passed since last submission
    if (!wasBatchTruncated && !this._shouldSubmitBatch(batchSizeInBytes)) {
      return
    }
    this.log.debug('Submitting batch.', {
      calldata: batchParams,
    })

    const nonce = await this.signer.getTransactionCount()
    const contractFunction = async (gasPrice): Promise<TransactionReceipt> => {
      const tx = await this.chainContract.appendSequencerBatch(batchParams, {
        nonce,
        gasPrice,
      })
      this.log.info('Submitted appendSequencerBatch transaction', {
        nonce,
        txHash: tx.hash,
        contractAddr: this.chainContract.address,
        from: tx.from,
        data: tx.data,
      })
      return this.signer.provider.waitForTransaction(
        tx.hash,
        this.numConfirmations
      )
    }
    return this._submitAndLogTx(contractFunction, 'Submitted batch!')
  }
  

state-batch-submitter.ts 过程和tx-batch-submitter一样,不过state-batch-submitter提交的是区块的状态根(state root),方法是_generateStateCommitmentBatch(startBlock:number,endBlock: number);

调用的是scc chain的 appendStateBatch方法.代码如果下:

public async _submitBatch(
  startBlock: number,
  endBlock: number
): Promise<TransactionReceipt> {
  const batch = await this._generateStateCommitmentBatch(startBlock, endBlock)
  const tx = this.chainContract.interface.encodeFunctionData(
    'appendStateBatch',
    [batch, startBlock]
  )
  const batchSizeInBytes = remove0x(tx).length / 2
  this.log.debug('State batch generated', {
    batchSizeInBytes,
    tx,
  })

  if (!this._shouldSubmitBatch(batchSizeInBytes)) {
    return
  }

  const offsetStartsAtIndex = startBlock - BLOCK_OFFSET // TODO: Remove BLOCK_OFFSET by adding a tx to Geth's genesis
  this.log.debug('Submitting batch.', { tx })

  const nonce = await this.signer.getTransactionCount()
  const contractFunction = async (gasPrice): Promise<TransactionReceipt> => {
    const contractTx = await this.chainContract.appendStateBatch(
      batch,
      offsetStartsAtIndex,
      { nonce, gasPrice }
    )
    this.log.info('Submitted appendStateBatch transaction', {
      nonce,
      txHash: contractTx.hash,
      contractAddr: this.chainContract.address,
      from: contractTx.from,
      data: contractTx.data,
    })
    return this.signer.provider.waitForTransaction(
      contractTx.hash,
      this.numConfirmations
    )
  }
  return this._submitAndLogTx(contractFunction, 'Submitted state root batch!')
}

contracts

Layer2系统中使用的各种智能合约,不过有些需要在layer1上布署,有些要在layer2上布署.需要注意的是: 这些合约要用 optimistic-solc 编译器进行编译,目的是为了保证无论何时,在执行同一个交易的时候,输出结果都是一样的。 因为 OVM_ExecutionManager.sol 中对于一些动态的opcode进行了重写, 比如: timestamp 在evm中获取的是当前区块的时间戳,而ovm中是按交易来的,执行哪个交易,是哪个交易的时间戳。

除了实现了ovm外,还包括一些账户,跨域桥,layer1上的验证者,预编译合约和ctc,scc chain 。这些都是optimism系统的核心。 所有的跨域消息都是通过调用这些合约和侦听合约的事件进行工作的。

data-transport-layer

数据传输层,其实这层就是个事件索引器,通过rpc访问layer1的rpc接口,索引layer1的合约事件,比如:
CTC chain的 TransactionEnqueued事件和SequencerBatchAppended事件,另外还有SCC chain的StateBatchAppended事件,索引到这些事件后,就会存在本地数据库下。然后再提供个rpc接口,供layer2也就是l2geth 来获取这些事件。当然这层也会提供相当的rpc接口,也就是实现了个client专门供layer2来获取数据。
**TransactionEnqueued 事件就是CTC chain的enqueue方法执行完毕,将一个交易提交到了CTC chain 的queue队列.SequencerBatchAppended 就是squencer提交了个batch到CTC chain中。是 appendSequencerBatch 这个接口的事件。StateBatchAppended 当然就是 交易的状态根提交到了SCC chain中 是方法 _appendBatch 的执行事件。

相应的代码如下:

protected async _start(): Promise<void> {
  // This is our main function. It's basically just an infinite loop that attempts to stay in
  // sync with events coming from Ethereum. Loops as quickly as it can until it approaches the
  // tip of the chain, after which it starts waiting for a few seconds between each loop to avoid
  // unnecessary spam.
  while (this.running) {
    try {
      const highestSyncedL1Block =
        (await this.state.db.getHighestSyncedL1Block()) ||
        this.state.startingL1BlockNumber
      const currentL1Block = await this.state.l1RpcProvider.getBlockNumber()
      const targetL1Block = Math.min(
        highestSyncedL1Block + this.options.logsPerPollingInterval,
        currentL1Block - this.options.confirmations
      )

      // We're already at the head, so no point in attempting to sync.
      if (highestSyncedL1Block === targetL1Block) {
        await sleep(this.options.pollingInterval)
        continue
      }

      this.logger.info('Synchronizing events from Layer 1 (Ethereum)', {
        highestSyncedL1Block,
        targetL1Block,
      })

      // I prefer to do this in serial to avoid non-determinism. We could have a discussion about
      // using Promise.all if necessary, but I don't see a good reason to do so unless parsing is
      // really, really slow for all event types.
      await this._syncEvents(
        'OVM_CanonicalTransactionChain',
        'TransactionEnqueued',
        highestSyncedL1Block,
        targetL1Block,
        handleEventsTransactionEnqueued
      )

      await this._syncEvents(
        'OVM_CanonicalTransactionChain',
        'SequencerBatchAppended',
        highestSyncedL1Block,
        targetL1Block,
        handleEventsSequencerBatchAppended
      )

      await this._syncEvents(
        'OVM_StateCommitmentChain',
        'StateBatchAppended',
        highestSyncedL1Block,
        targetL1Block,
        handleEventsStateBatchAppended
      )

      await this.state.db.setHighestSyncedL1Block(targetL1Block)

      if (
        currentL1Block - highestSyncedL1Block <
        this.options.logsPerPollingInterval
      ) {
        await sleep(this.options.pollingInterval)
      }
    } catch (err) {
      if (!this.running || this.options.dangerouslyCatchAllErrors) {
        this.logger.error('Caught an unhandled error', { err })
        await sleep(this.options.pollingInterval)
      } else {
        // TODO: Is this the best thing to do here?
        throw err
      }
    }
  }
}

_syncEvents就是通过某个合约的某个事件,然后通过相应的handle的存储在本地的数据库中。

到这里,有了batch-submitter和data-transport-layer就可以把layer1和layer2上的交易形成循环。

如果有人在layer2上执行了交易,交易在打包后,会通过batch-submitter 提交到layer1的CTC chain,然后data-transport-layer侦听到事件后,会存在本地数据库,这时l2geth可以通过rpc获取 data-transport-layer存的数据。然后再到 layer2上尝试执行,拿结果和layer2已经确定的交易进行比较,如果一样,说明layer1上的交易是正确的,如果不一样,则需要layer1上的验证者去做验证。这是verifier的功能。

l2geth是另一个角 色是Sequencer,他是把data-transport-layer中侦听到的quence的交易,提交到layer2中打包。

然后batch-submitter获取区块的stateroot再提交到layer1的SCC chain中。**
这块逻辑有点绕。需要慢慢理解。。

message-relayer

这是一个 中继服务,是将layer2中的提现交易中继到layer1上。其实现过程,就是利用rpc接口侦听layer2的SentMessages事件,这个事件就是跨域转账或其他跨域消息。然后,relayer侦听到这个事件后,会根据事件的参数。在layer1上调用OVM_L1CrossDomainMessenger的relayMessage方法,进行relay.然后就会到相应的合约上执行相应的方法。以达到跨域转账的目的.

我们先介绍这几个主要的模块代码。希望以大家理解有帮助。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 140,856评论 1 295
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 60,475评论 1 254
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 92,639评论 0 208
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,628评论 0 171
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,311评论 1 250
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,533评论 1 167
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,235评论 2 266
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,017评论 0 161
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,738评论 6 225
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,358评论 0 211
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,115评论 2 211
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,444评论 1 222
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,138评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 26,965评论 2 209
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,349评论 3 200
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,574评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 25,901评论 0 163
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,314评论 2 227
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,433评论 2 228

推荐阅读更多精彩内容