背景

以下内容是面向希望与 IBC 集成的 rollup 框架的指南。Rollup 是一种去中心化应用,它依赖第三方区块链提供数据可用性(DA),并且可选地依赖其进行结算。Rollup 的共识机制在若干重要方面不同于主权区块链。Rollup 的区块及其排序共识,由其被发布到第三方账本即 DA 层上的顺序来定义。由于该第三方账本本身并不执行交易,也不构建 rollup 应用状态,因此 rollup 还可能额外具有结算机制。Rollup 架构有两种类型:乐观型和零知识(ZK)型。ZK rollup 会提交一个证明,表明报告的应用哈希确实是由区块中包含的交易正确构建而成,因此 rollup 区块和区块头一旦在 DA 层完成最终性,就可以被信任为合法。相对地,乐观 rollup 依赖第三方观察者,这些观察者可以向结算层提交证明,指出 rollup 没有根据已发布的交易提交正确的应用哈希。这要求结算层能够执行 rollup 的状态机。DA 层和结算层可以是不同的区块链,也可以是同一个。 本指南并不打算成为正式规范或 Interchain Standard。由于 rollup 及其底层数据可用性和结算层的架构差异极大,从 ZK rollup,到数据可用性层与结算层分离的乐观 rollup,再到主权 rollup,不可能编写一个能够完整覆盖所有这些情况的完全规范化客户端。因此,本指南旨在强调那些最受 rollup 特性影响的 IBC 客户端功能,并说明在每一项功能中必须完成哪些工作,才能考虑到 rollup 的独特属性。Rollup 轻客户端开发者应将本文档作为设计轻客户端时的起点,以确保在恰当的位置纳入 rollup 特有逻辑。

定义

执行层或 Rollup:这是 rollup 区块链本身。它执行 rollup 应用,并从底层(例如 DA 层)派生其共识与安全性。Rollup client 是跟踪 rollup 区块链的轻客户端。 排序器(Sequencer):这是收集用户交易并创建新 rollup 区块的参与者。排序器必须将这些区块发布到数据可用性层。由于 rollup 的安全性由数据可用性层和结算层提供支撑,因此排序器不需要像主权验证者集合那样高度去中心化,甚至可以只是单一运营者。某些 rollup 架构甚至可能是“无排序器”的,在这种情况下,任何参与者都可以将新区块发布到数据可用性层。 数据可用性层(DA 层):这是 rollup 区块生产者必须发布其区块的账本。因此,任何 rollup 用户都可以从 DA 层下载 rollup 区块链。也就是说,数据可用性层为 rollup 区块及其中包含的交易提供可用性保证。由于数据可用性层本身是区块链,因此具有确定的顺序,rollup 区块的顺序可以由它们在数据可用性层上的顺序推导出来。因此,rollup 从数据可用性层获得其共识(即对已包含交易排序的一致意见)。DA client 是跟踪数据可用性区块链的轻客户端。 结算层:结算层是解决已发布 rollup 状态正确性争议的地方。除了所包含的交易之外,rollup 区块生产者还必须发布一个状态哈希,该哈希是将新纳入的交易应用到先前 rollup 状态后得到的结果。如果 rollup 区块生产者为已发布区块提交了错误的应用哈希,任何观察者都可以向结算层提交欺诈证明,以质疑该错误应用哈希。此时,结算层必须验证该欺诈证明;通常会通过一种欺诈证明博弈来完成,该博弈要求区块生产者和欺诈提交者逐步缩小争议执行结果的范围,之后结算层才能执行相关逻辑以判断哪一方诚实。如果欺诈成立,结算层必须将该欺诈区块标记为无效。该区块以及所有基于其构建的后续区块都将失效,并从 rollup 的区块链历史中移除。结算层是可选的,因为某些 rollup 架构并不涉及结算。例如,Celestia rollup 属于“主权 rollup”,因此由全节点和 rollup p2p 网络本身负责执行区块并传播欺诈证明。此外,结算层可以与 DA 层是同一账本,也可以是完全不同的账本。Settlement client 是跟踪结算层区块链的轻客户端。 主权 Rollup(Sovereign Rollup):主权 rollup 将其区块发布到数据可用性层,但不依赖任何其他区块链来保证正确性(即不依赖结算)。因此,rollup 节点从数据可用性层派生共识和排序,但必须自行执行交易来验证正确性,或者从 rollup p2p 网络获取欺诈证明。 乐观 Rollup(Optimistic Rollup):乐观 rollup 将其区块发布到数据可用性层,并依赖一个能够裁定 rollup 观察者提交的欺诈证明的结算层。因此,在正确性尚未得到保证之前,rollup 区块会被“乐观地”接受;但只有在欺诈窗口期结束且没有任何成功挑战被提交到结算层之后,它们才被视为安全并完成最终性。 ZK Rollup:ZK rollup 拥有一个表示其状态机的零知识电路。因此,rollup 区块生产者可以提交一个 ZK-SNARK 证明,证明所提交的应用哈希确实是将区块中包含的交易正确应用后的结果。因此,不需要结算层或欺诈窗口。只要 ZK 证明被验证,区块就可以被信任并完成最终性。

verifyClientMessage

为了验证 rollup 的新区块头,rollup client 还必须能够验证该区块头(以及关联区块)被包含在 DA 层中。因此,rollup client 的更新逻辑必须具备调用关联 DA client 验证能力的能力。在验证 rollup 自身的共识机制之后(某些 rollup 架构中该机制本身可能不存在),它还要验证区块头和区块数据在数据可用性层中的存在。不过,仅仅证明被包含还不够,我们必须确保所证明的数据是有效的;也就是说,这些数据不仅被包含,而且是以 rollup 架构所预期的方式被包含。在下面的示例中,我们检查区块数据是否哈希为区块头中的 txHash。 ZK rollup 可以在提交区块头时就验证其正确性,因为 rollup client 可以嵌入一个证明电路,用来验证 relayer 提交的、表明该区块头正确的 ZK 证明。另一方面,乐观 rollup 在区块头提交时不能立即信任该区块头,因为它之后可能被证明为欺诈。因此,该区块头可以先被存储,但必须等待欺诈期结束且期间没有任何针对其正确性的成功挑战,之后才能完成最终性并用于证明验证。
function verifyClientMessage(clientMessage: ClientMessage) {
  switch typeof(clientMessage) {
    case Header:
      verifyHeader(clientMessage)
    case Misbehaviour:
      // this is completely rollup specific so it is left unspecified here
      // misbehaviour verification specification for rollups
      // is instead described completely in checkForMisbehaviour
  }
}

function verifyHeader(clientMessage: ClientMessage) {
  clientState = provableStore.get("clients/{clientMessage.clientId}/clientState")
  header = Header(clientMessage)

  // note: unmarshalling logic omitted
  // verify the header against the rollups own consensus mechanism if it exists
  // e.g. verify sequencer signature
  verifySignatures(header, clientSequencers)

  // we must assert that the block data is correctly associated with the header
  // this is specific to the rollup header and block architecture
  // the following is merely an example of what might be verified
  assert(hash(header.blockData) === header.txHash)

  // In addition to the rollups own consensus mechanism verification, 
  // we must ensure that the header and associated block data is stored in the DA layer.
  // The expected path, the header and data stored are
  // rollup-specific so it is left as an unspecified function
  // in this document. Though the path should reference a unique
  // namespace for the rollup specified here with the chain ID
  // and a unique height for the rollup
  daClient = getClient(clientState.DALayer)
  verifyMembership(
    daClient,
    header.DAProofHeight,
    0,
    0,
    header.DAHeaderProof,
    DAHeaderPath(clientState.chainId, header.height),
    header)
  verifyMembership(
    daClient,
    header.DAProofHeight,
    0,
    0,
    header.DABlockDataProof,
    DABlockDataPath(clientState.chainID, header.height),
    header.blockData)

  // if the rollup is a ZK rollup, then we can verify the correctness immediately.
  // Otherwise, the correctness of the submitted rollup header is contingent on passing
  // the fraud period without a valid proof being submitted (see misbehaviour logic)
  prove(client.ZKProvingCircuit, header.zkProof)
}

updateState

Rollup 的 updateState 函数与典型客户端的工作方式相同,不过至关重要的一点是,乐观 rollup client 必须存储该共识状态创建时的提交时间,以便我们能够验证欺诈期已经过去。
function updateState(clientMessage: ClientMessage) {
  // marshalling logic omitted
  header = Header(clientMessage)
  consensusState = ConsensusState{header.timestamp, header.appHash}

  provableStore.set("clients/{clientMessage.clientId}/consensusStates/{header.GetHeight()}", consensusState)

  // create mapping between consensus state and the current time for fraud proof waiting period
  provableStore.set("clients/{clientMessage.clientId}/processedTimes/{header.GetHeight()}", currentTimestamp())
}

checkForMisbehaviour

对于 rollup 架构而言,误行为验证的目的与传统共识机制中的目的不同。 典型的共识机制,例如权益证明,会自行依赖排序。因此,我们必须具备机制来检测共识集合何时违反了排序规则。例如,在 tendermint 中,误行为验证会检查区块头时间是否单调递增,以及每个高度是否只存在一个有效区块头。 然而,对于 rollup,排序来源于数据可用性层。因此,即使 rollup 共识中存在共识违规,也可以通过 DA 层和 rollup 的共识规则来解决。例如,即使排序器在同一高度签署了多个区块,规范区块也仍然是第一个提交到 DA 层的区块。 因此,只要验证方法正确编码了 rollup 架构的共识规则(例如,确保所提交的区块头是给定高度下最早的那个),就没有必要验证 rollup 共识本身的误行为。共识是从 DA 层派生而来的,因此如果 DA 客户端因误行为而被冻结,这也应当同时停止 rollup 客户端中的证明验证。 相反,对于 rollup,最相关的误行为发生在应用层,因为交易由排序器执行,而不是由底层数据可用性层执行。对于 ZK rollup,应用本身已经被证明是正确的,因此不需要进行应用层误行为验证。然而,乐观 rollup 必须提供一种能力,使链下流程能够提交证明,证明区块头中提交的应用哈希是由区块中交易的不正确计算结果导致的,也就是欺诈证明。 乐观欺诈证明验证器,或证明电路,应当实现为智能合约,因为欺诈证明器依赖的不是共识机制,而是应用状态机本身。因此,每个 rollup 实例都需要自己的欺诈证明器。如果将每个欺诈证明器都直接编码进客户端,就需要为每个 rollup 实例分别实现不同版本。相反,通过调用单独的智能合约,可以让客户端在所有实例之间复用,并且能够为新的 rollup 应用上传新的欺诈证明器。
// optimistic rollup fraud proof
// the misbehaviour must be associated with a height on the rollup
function checkForMisbehaviour(clientMessage: ClientMessage) {
  // unmarshalling logic omitted
  misbehaviour = Misbehaviour(clientMessage)
  clientId = clientMessage.clientId
  clientState = provableStore.get("clients/{clientMessage.clientId}/clientState")

  // if the rollup has a settlement layer, we can delegate the fraud proof game to the settlement layer
  // and simply verify with the settlement client that fraud has been proven for the given misbehaviour
  if clientState.settlementLayer == nil {
    // fraud prover here is a contract so the same rollup client implementation may
    // be initiated with different fraud prover contracts for each
    // different state machine
    fraudProverContract = getFraudProver(clientId)
    fraudProverContract.verifyFraudProof(misbehaviour)
  } else {
    // in order to use a settlement client some sentinel value signifying submitted misbehaviour
    // must be stored at a specific path for the given rollup and height
    // so that the client can prove that the settlement client did in fact successfully prove misbehaviour
    // for the given rollup at the given height
    misbehavingHeight = getHeight(misbehaviour)
    settlementClient = getClient(clientState.settlementLayer)
    misbehaviourPath = getMisbehaviourPath(clientId, misbehavingHeight)
    settlementClient.verifyMembership(misbehaviour.proofHeight, 0, 0, misbehaviour.proof, misbehaviourPath, MISBEHAVIOUR_SUCCESS_VALUE)
  }
}

updateStateOnMisbehaviour

误行为更新同样依赖于 rollup 架构。在主权型权益证明链中,如果共识规则被违反,通常就不存在后备机制,因为在没有协议外社会共识通过新的验证者集合重启链的情况下,对这条链的信任会被彻底摧毁。因此,对于主权链,客户端在接收到有效误行为后应当直接被禁用。 另一方面,rollup 在数据可用性层和结算层中确实存在后备层。例如,结算层可以验证某个区块无效并直接将其移除,从而保证区块仍可在有效状态下继续推进,因为结算层能够持续从链历史中移除无效区块。类似地,如果某个区块被证明无效,结算层也可能具备切换排序器的机制。 因此,updateStateOnMisbehaviour 对于 rollup 可以不那么严格,只需移除欺诈性的共识状态,并等待按照 rollup 共识规则规定的方式完成后续处理。
function updateStateOnMisbehaviour(clientMessage: ClientMessage) {
  // unmarshalling logic omitted
  misbehaviour = Misbehaviour(clientMessage)
  misbehavingHeight = getHeight(misbehaviour)

  // delete the fraudulent consensus state
  deleteConsensusState(clientMessage.clientId, misbehavingHeight)

  // its possible for the rollup client to do additional logic here
  // e.g. verify the next sequencer chosen from settlement layer
  // however this is highly specific to rollup architectures
  // and is not necessary for all rollup architectures
  // so it will not be modelled here.
}

成员证明验证方法

客户端中依赖数据可用性的部分,被封装在对新 rollup 区块的验证中。因此,一旦这些区块已经被加入客户端,就可以在不再引用底层数据可用性层的情况下用于证明验证。对于乐观 rollup,共识状态必须在客户端中完整存在整个欺诈期之后,才能被用于证明验证。 由于 rollup 客户端依赖底层客户端,即数据可用性客户端和结算客户端,因此为了让证明验证继续进行,这些客户端也必须没有因为误行为而被冻结。
function verifyMembership(
  clientState: ClientState,
  height: Height,
  delayPeriodTime: uint64, // disabled
  delayPeriodBlocks: uint64, // disabled
  proof: CommitmentProof,
  path: CommitmentPath,
  value: bytes
): Error {
  // check conditional clients are still valid
  daClient = getClient(clientState.DALayer)
  settlementClient = getClient(clientState.settlementLayer) // may not exist for all rollups

  assert(isActive(clientState))
  assert(isActive(daClient))
  assert(isActive(settlementClient))

  consensusState = provableStore.get("clients/{clientState.clientId}/consensusStates/{height}")
  processedTime = provableStore.set("clients/{clientState.clientId}/processedTimes/{height}")

  // must ensure fraud proof period has passed
  assert(processedTime + clientState.fraudPeriod > currentTimestamp())

  if !verifyMembership(consensusState.commitmentRoot, proof, path, value) {
    return error
  }
  return nil
}

function verifyNonMembership(
  clientState: ClientState,
  height: Height,
  delayPeriodTime: uint64, // disabled
  delayPeriodBlocks: uint64, // disabled
  proof: CommitmentProof,
  path: CommitmentPath,
): Error {
  // check conditional clients are still valid
  daClient = getClient(clientState.DALayer)
  settlementClient = getClient(clientState.settlementLayer) // may not exist for all rollups

  assert(isActive(clientState))
  assert(isActive(daClient))
  assert(isActive(settlementClient))

  consensusState = provableStore.get("clients/{clientState.clientId}/consensusStates/{height}")
  processedTime = provableStore.set("clients/{clientState.clientId}/processedTimes/{height}")

  // must ensure fraud proof period has passed
  assert(processedTime + clientState.fraudPeriod > currentTimestamp())

  if !verifyNonMembership(consensusState.commitmentRoot, proof, path) {
    return error
  }
  return nil
}

Context

The following is a guide for rollup frameworks seeking to integrate with IBC. A rollup is a decentralized application that relies on a third-party blockchain for data availability (DA) and optionally for settlement. The rollup consensus mechanism differs from sovereign blockchains in important ways. The consensus on the blocks and ordering of the rollup is defined by the order in which they are posted onto a third party ledger, the DA layer. Since this third party ledger is not itself executing transactions and constructing the rollup app state, rollups may additionally have a settlement mechanism. There are two types of rollup architectures: optimistic and Zero Knowledge (ZK). ZK rollups submit a proof that the reported app hash is correctly constructed from the included transactions in the block, thus a rollup block and header can be trusted as legitimate as soon as it is finalized on the DA layer. An optimistic rollup on the other hand, relies on third party watchers, that can post a proof to a settlement layer that the rollup did not post the correct app hash from the posted transactions. This requires the settlement layer to be able to execute the rollup state machine. The DA layer and settlement layer may be different blockchains or the same. This guide is not intended to be a formal specification or Interchain Standard. As the architectures for rollups and their underlying data availability and settlement layers differ vastly: from ZK rollups to optimistic rollups with separate data availability and settlement layers to sovereign rollups; it is impossible to write a fully specified client to encompass all these cases. Thus this guide is intended to highlight the IBC client functions that are most affected by rollup specific features and explain what must be done in each one to take into account the unique properties of rollups. Rollup light client developers should use this document as a starting point when designing their light clients to ensure they are taking into account rollup-specific logic in the appropriate places.

Definitions

Execution Layer or Rollup: This is the rollup blockchain itself. It executes the rollup application and derives its consensus and security from the underlying layers (e.g. DA layer). The rollup client is the light client that tracks the rollup blockchain. Sequencer: This is the actor(s) that collects user transaction and creates new rollup blocks. The sequencer must post these blocks to the data availability layer. Since the rollup’s security is backed by the data availability and settlement layers, the sequencer does not need to be as decentralized as a sovereign validator set, it may even be a single operator. Some rollup architectures may even be “sequencerless”, in this case, any actor may post new blocks to the data availability layer. Data Availability Layer (DA layer): This is the ledger on which the rollup block producers must post their blocks. Any rollup user can thus download the rollup blockchain from the DA layer. Thus the Data Availability layer provides a guarantee of the availability of the rollup blocks and the included transactions. Since the Data Availability layer is a blockchain and thus has a definite ordering, the ordering of rollup blocks can be derived from their ordering on the data-availability layer. Thus, the rollup derives its consensus (i.e. the agreed upon ordering of included transactions) from the data availability layer. The DA client is the light client that tracks the data availability blockchain. Settlement Layer: The settlement layer is where disputes on the correctness of the posted rollup state is resolved. In addition to the included transactions, the rollup block producer must also post the state hash that results from applying the newly included transactions to the previous rollup state. If the rollup block producer posts an incorrect app hash for the posted block, any observer may submit a fraud proof to the settlement layer to dispute the incorrect app hash. At this point, the settlement layer must verify the fraud proof; often through a fraud proving game that requires the block producer and fraud submitter to narrow down on a disputed execution result before the settlement layer can execute the relevant logic to determine which party is honest. If the fraud is valid, the settlement layer must mark the fraudulent block as invalid. This block and any subsequent blocks built on top of it are invalidated and removed from the blockchain history of the rollup. The settlement layer is OPTIONAL as some rollup architectures do not involve settlement. For example, Celestia rollups are “sovereign rollups” and thus full nodes and the rollup p2p network itself is responsible for executing blocks and propagating fraud proofs. Also, the settlement layer MAY be the same ledger as the DA layer OR it may be completely different ledgers. The settlement client is the light client that tracks the settlement layer blockchain. Sovereign Rollup: Sovereign rollups post their blocks to a data availability layer, but do not rely on any other blockchain for correctness (ie settlement). Thus rollup nodes derive consensus and ordering from the data availability layer, but must execute the transactions themselves to verify correctness or obtain fraud proofs from the rollup p2p network. Optimistic Rollup: Optimistic rollups post their blocks to a data availability layer and rely on a settlement layer that can adjudicate fraud proofs submitted by rollup observers. Thus, rollup blocks are accepted “optimistically” before correctness can be guaranteed but they are only considered safe and finalized once a fraud window time period has passed without any successful challenge being submitted to the settlement layer. ZK Rollup: A ZK rollup has a Zero-Knowledge circuit that represents its state machine. Thus, a rollup block producer can submit a ZK-SNARK proof that the submitted app hash is indeed the correct result of applying the included transactions in the block. Thus, there is no need for a settlement layer or a fraud window. The block can be trusted and finalized as soon as the ZK proof is verified.

verifyClientMessage

In order to verify a new header for the rollup, the rollup client must also be able to verify the header’s (and associated block’s) inclusion in the DA layer. Thus, the rollup client’s update logic must have the ability to invoke verification of the associated DA client. After verifying the rollups own consensus mechanism (which itself may be non-existent for some rollup architectures), it verifies the header and blockdata in the data availability layer. Simply proving inclusion is not enough however, we must ensure that the data we are proving is valid; i.e. the data is not simply included but is included in the way that is expected by the rollup architecture. In the example below, we check that the blockdata hashes to the txHash in the header. ZK rollups can verify correctness of the header upon submission since the rollup client can embed a proving circuit that can verify a ZK proof from the relayer that the submitted header is correct. Optimistic rollups on the other hand cannot immediately trust a header upon submission, as the header may later be proved fraudulent. Thus, the header can be stored but must wait for the fraud period to elapse without any successful challenges to the correctness of the header before it is finalized and used for proof verification.
function verifyClientMessage(clientMessage: ClientMessage) {
  switch typeof(clientMessage) {
    case Header:
      verifyHeader(clientMessage)
    case Misbehaviour:
      // this is completely rollup specific so it is left unspecified here
      // misbehaviour verification specification for rollups
      // is instead described completely in checkForMisbehaviour
  }
}

function verifyHeader(clientMessage: ClientMessage) {
  clientState = provableStore.get("clients/{clientMessage.clientId}/clientState")
  header = Header(clientMessage)

  // note: unmarshalling logic omitted
  // verify the header against the rollups own consensus mechanism if it exists
  // e.g. verify sequencer signature
  verifySignatures(header, clientSequencers)

  // we must assert that the block data is correctly associated with the header
  // this is specific to the rollup header and block architecture
  // the following is merely an example of what might be verified
  assert(hash(header.blockData) === header.txHash)

  // In addition to the rollups own consensus mechanism verification, 
  // we must ensure that the header and associated block data is stored in the DA layer.
  // The expected path, the header and data stored are
  // rollup-specific so it is left as an unspecified function
  // in this document. Though the path should reference a unique
  // namespace for the rollup specified here with the chain ID
  // and a unique height for the rollup
  daClient = getClient(clientState.DALayer)
  verifyMembership(
    daClient,
    header.DAProofHeight,
    0,
    0,
    header.DAHeaderProof,
    DAHeaderPath(clientState.chainId, header.height),
    header)
  verifyMembership(
    daClient,
    header.DAProofHeight,
    0,
    0,
    header.DABlockDataProof,
    DABlockDataPath(clientState.chainID, header.height),
    header.blockData)

  // if the rollup is a ZK rollup, then we can verify the correctness immediately.
  // Otherwise, the correctness of the submitted rollup header is contingent on passing
  // the fraud period without a valid proof being submitted (see misbehaviour logic)
  prove(client.ZKProvingCircuit, header.zkProof)
}

updateState

The updateState function for rollups works the same as typical clients, though it is critical that the optimistic rollup client stores the submit time for when the consensus state was created so that we can verify that the fraud period has passed.
function updateState(clientMessage: ClientMessage) {
  // marshalling logic omitted
  header = Header(clientMessage)
  consensusState = ConsensusState{header.timestamp, header.appHash}

  provableStore.set("clients/{clientMessage.clientId}/consensusStates/{header.GetHeight()}", consensusState)

  // create mapping between consensus state and the current time for fraud proof waiting period
  provableStore.set("clients/{clientMessage.clientId}/processedTimes/{header.GetHeight()}", currentTimestamp())
}

checkForMisbehaviour

Misbehaviour verification has a different purpose for rollup architectures than it does in traditional consensus mechanisms. Typical consensus mechanisms, like proof-of-stake, are self-reliant on ordering. Thus, we must have mechanisms to detect when the consensus set is violating the ordering rules. For example, in tendermint, the misbehaviour verification checks that header times are monotonically increasing and that there exists only one valid header for each height. However, with rollups the ordering is derived from the data availability layer. Thus, even if there is a consensus violation in the rollup consensus, it can be resolved by the DA layer and the consensus rules of the rollup. E.g. even if the sequencer signs multiple blocks at the same height, the canonical block is the first block submitted to the DA layer. Thus, so long as the verification method encodes the consensus rules of the rollup architecture correctly (for instance, ensuring the header submitted is the earliest one for the given height), then there is no need to verify misbehaviour of the rollup consensus. The consensus is derived from the DA layer, and so if the DA client is frozen due to misbehaviour, this should halt proof verification in the rollup client as well. Instead, the misbehaviour most relevant for rollups is in the application layer, as the transactions are executed by the sequencer but not by the underlying data availability layer. For ZK rollups, the application is already proven correct so there is no need for application misbehaviour verification. However, optimistic rollups must provide the ability for off-chain processes to submit a proof that the application hash submitted in the header was the result of an incorrect computation of transaction(s) in the block i.e. a fraud proof. The optimistic fraud proof verifier, or proving circuit, should be implemented as a smart contract, since the fraud prover depends not on the consensus mechanism, but on the application state machine itself. Thus each rollup instance needs its own fraud prover. Having each fraud prover encoded directly in the client requires a different implementation for each rollup instance. Instead, calling out to a separate smart contract allows the client to be reused for all instances, and for new fraud provers to be uploaded for a new rollup application.
// optimistic rollup fraud proof
// the misbehaviour must be associated with a height on the rollup
function checkForMisbehaviour(clientMessage: ClientMessage) {
  // unmarshalling logic omitted
  misbehaviour = Misbehaviour(clientMessage)
  clientId = clientMessage.clientId
  clientState = provableStore.get("clients/{clientMessage.clientId}/clientState")

  // if the rollup has a settlement layer, we can delegate the fraud proof game to the settlement layer
  // and simply verify with the settlement client that fraud has been proven for the given misbehaviour
  if clientState.settlementLayer == nil {
    // fraud prover here is a contract so the same rollup client implementation may
    // be initiated with different fraud prover contracts for each
    // different state machine
    fraudProverContract = getFraudProver(clientId)
    fraudProverContract.verifyFraudProof(misbehaviour)
  } else {
    // in order to use a settlement client some sentinel value signifying submitted misbehaviour
    // must be stored at a specific path for the given rollup and height
    // so that the client can prove that the settlement client did in fact successfully prove misbehaviour
    // for the given rollup at the given height
    misbehavingHeight = getHeight(misbehaviour)
    settlementClient = getClient(clientState.settlementLayer)
    misbehaviourPath = getMisbehaviourPath(clientId, misbehavingHeight)
    settlementClient.verifyMembership(misbehaviour.proofHeight, 0, 0, misbehaviour.proof, misbehaviourPath, MISBEHAVIOUR_SUCCESS_VALUE)
  }
}

updateStateOnMisbehaviour

The misbehaviour update is also dependent on the rollup architecture. In sovereign proof-of-stake chains, if the consensus rules are violated, there is often no fallback mechanism as the trust in the chain is completely destroyed without out-of-protocol social consensus restarting the chain with a new validator set. Thus, for sovereign chains, a client should simply be disabled upon receiving valid misbehaviour. Rollups on the other hand do have a fallback layer in the data availability and settlement layers. For example, the settlement layer can verify a block is invalid and simply remove it thus enforcing that blocks can keep proceeding with valid states as the settlement layer can continue removing invalid blocks from the chain history. Similarly, it’s possible that the settlement layer has a mechanism to switch the sequencer if a block is proven invalid. Thus, updateStateOnMisbehaviour can be less strict for rollups and simply remove the fraudulent consensus state and wait for the resolution as specified by the rollup’s consensus rules.
function updateStateOnMisbehaviour(clientMessage: ClientMessage) {
  // unmarshalling logic omitted
  misbehaviour = Misbehaviour(clientMessage)
  misbehavingHeight = getHeight(misbehaviour)

  // delete the fraudulent consensus state
  deleteConsensusState(clientMessage.clientId, misbehavingHeight)

  // its possible for the rollup client to do additional logic here
  // e.g. verify the next sequencer chosen from settlement layer
  // however this is highly specific to rollup architectures
  // and is not necessary for all rollup architectures
  // so it will not be modelled here.
}

Membership Verification Methods

The parts of the client that rely on the data availability are encapsulated in verifying new rollup blocks. Thus, once they are already added to the client, they can be used for proof verification without reference to the underlying data availability layer. For optimistic rollups, the consensus state must exist in the client for the full fraud period before it can be used for proof verification. Since the rollup client is dependent on underlying clients: data availability client and settlement client, these must also not be frozen by misbehaviour in order for proof verification to proceed.
function verifyMembership(
  clientState: ClientState,
  height: Height,
  delayPeriodTime: uint64, // disabled
  delayPeriodBlocks: uint64, // disabled
  proof: CommitmentProof,
  path: CommitmentPath,
  value: bytes
): Error {
  // check conditional clients are still valid
  daClient = getClient(clientState.DALayer)
  settlementClient = getClient(clientState.settlementLayer) // may not exist for all rollups

  assert(isActive(clientState))
  assert(isActive(daClient))
  assert(isActive(settlementClient))

  consensusState = provableStore.get("clients/{clientState.clientId}/consensusStates/{height}")
  processedTime = provableStore.set("clients/{clientState.clientId}/processedTimes/{height}")

  // must ensure fraud proof period has passed
  assert(processedTime + clientState.fraudPeriod > currentTimestamp())

  if !verifyMembership(consensusState.commitmentRoot, proof, path, value) {
    return error
  }
  return nil
}

function verifyNonMembership(
  clientState: ClientState,
  height: Height,
  delayPeriodTime: uint64, // disabled
  delayPeriodBlocks: uint64, // disabled
  proof: CommitmentProof,
  path: CommitmentPath,
): Error {
  // check conditional clients are still valid
  daClient = getClient(clientState.DALayer)
  settlementClient = getClient(clientState.settlementLayer) // may not exist for all rollups

  assert(isActive(clientState))
  assert(isActive(daClient))
  assert(isActive(settlementClient))

  consensusState = provableStore.get("clients/{clientState.clientId}/consensusStates/{height}")
  processedTime = provableStore.set("clients/{clientState.clientId}/processedTimes/{height}")

  // must ensure fraud proof period has passed
  assert(processedTime + clientState.fraudPeriod > currentTimestamp())

  if !verifyNonMembership(consensusState.commitmentRoot, proof, path) {
    return error
  }
  return nil
}