链眼社区:专注于区块链安全,区块链数据分析, 区块链信息整合,区块链技术服务和区块链技术咨询。

Arbitrum 的 Nitro 项目 Rollup 细节深入
SavourDao
2022-12-10 15:32:23

关于 rollup 向一层提交交易的我们上面文章已经由提到,此处主要主要剖析 rollup 合约,rollup watcher 和欺诈证明之间的交互和细节的逻辑整理。

一. validator 各子模块代码解析

  • block_challenge_backend 和 challenge_manager 区块挑战相关的代码
  • block_validator:区块验证逻辑,这里是衔接 arbnode 和 geth 的区块验证模块
  • rollup_watcher:rollup 合约交易事件的观察者

二. 2. Rollup 细节

汇总协议跟踪汇总块链——为了清楚起见,我们将这些称为“RBlocks”。它们与第 1 层以太坊区块不同,也与第 2 层 Nitro 区块不同。您可以将 RBlocks 视为形成一个单独的链,由 Arbitrum 汇总协议管理和监督。

关于汇总块链相关的解释请参考:nitro/inside-arbitrum-nitro.md at master · OffchainLabs/nitro

rollup_watcher 会去监听 rollup 相关的事件,监听的事件如下:

var rollupInitializedID common.Hash
var nodeCreatedID common.Hash
var challengeCreatedID common.Hash

这些事件分别由 LookupCreation LookupNode LookupNodeChildren 和 LookupChallengedNode 等方法处理,L1Validator 将这些调用这里面的方法获取相关的合约事件信息验证。

RollupNode 定义

 struct Node {
    // Hash of the state of the chain as of this node
    bytes32 stateHash;
    // Hash of the data that can be challenged
    bytes32 challengeHash;
    // Hash of the data that will be committed if this node is confirmed
    bytes32 confirmData;
    // Index of the node previous to this one
    uint64 prevNum;
    // Deadline at which this node can be confirmed
    uint64 deadlineBlock;
    // Deadline at which a child of this node can be confirmed
    uint64 noChildConfirmedBeforeBlock;
    // Number of stakers staked on this node. This includes real stakers and zombies
    uint64 stakerCount;
    // Number of stakers staked on a child node. This includes real stakers and zombies
    uint64 childStakerCount;
    // This value starts at zero and is set to a value when the first child is created. After that it is constant until the node is destroyed or the owner destroys pending nodes
    uint64 firstChildBlock;
    // The number of the latest child of this node to be created
    uint64 latestChildNumber;
    // The block number when this node was created
    uint64 createdAtBlock;
    // A hash of all the data needed to determine this node's validity, to protect against reorgs
    bytes32 nodeHash;
}

参加 rollup 的角色:分别由 WatchtowerStrategy,DefensiveStrategy, StakeLatestStrategy 和 MakeNodesStrategy 代码定义如下,该代码位于 staker.go 里面:

const (
   // Watchtower: don't do anything on L1, but log if there's a bad assertion
   WatchtowerStrategy StakerStrategy = iota
   // Defensive: stake if there's a bad assertion
   DefensiveStrategy
   // Stake latest: stay staked on the latest node, challenging bad assertions
   StakeLatestStrategy
   // Make nodes: continually create new nodes, challenging bad assertions
   MakeNodesStrategy
)
  • Staker start 任务启动之后,先会去更新认证区块的 wasm moudle root, 函数调用如下
err := s.updateBlockValidatorModuleRoot(ctx)

updateBlockValidatorModuleRoot 里面会去从 rollup 的中取到 WasmModuleRoot 更新到 blockValidator 里面, 当然 l1 validator 初始化的时候也会去更新相应的 WasmModuleRoot,并把 WasmModuleRoot 设置成 blockValidator 当前的 ModuleRoot,以供验证使用。

  • 获取到 callOpts 的信息,供下面的函数调用,该信息包含
type CallOpts struct {
   Pending     bool            // Whether to operate on the pending state or the last known one
   From        common.Address  // Optional the sender address, otherwise the first account is used
   BlockNumber *big.Int        // Optional the block number on which the call should be performed
   Context     context.Context // Network context to support cancellation and timeouts (nil = no timeout)
}
  • 清除构建的交易
func (b *ValidatorTxBuilder) ClearTransactions() {
   b.transactions = nil
}
  • 从 rollup 的 StakerMap 里面获取最新质押者的信息,包含挑战的信息
  • 从 validatorUtils 中拿到最新的质押数据信息,调用的合约函数如下:
function latestStaked(IRollupCore rollup, address staker)
    external
    view
    returns (uint64, Node memory)
{
    uint64 num = rollup.latestStakedNode(staker);
    if (num == 0) {
        num = rollup.latestConfirmed();
    }
    Node memory node = rollup.getNode(num);
    return (num, node);
}
  • 判断 Rblocks 是否分叉,validatorUtils.AreUnresolvedNodesLinear -> _ValidatorUtils.contract.Call(opts, &out, "areUnresolvedNodesLinear", rollup)最终调用到这个合约函数进行判断
function areUnresolvedNodesLinear(IRollupCore rollup) external view returns (bool) {
    uint256 end = rollup.latestNodeCreated();
    for (uint64 i = rollup.firstUnresolvedNode(); i <= end; i++) {
        if (i > 0 && rollup.getNode(i).prevNum != i - 1) {
            return false;
        }
    }
    return true;
}
  • 如果发生了分叉,effectiveStrategy将由 DefensiveStrategy 切换成 StakeLatestStrategy,否则将按照 WatchtowerStrategy 往下执行 代码如下:
nodesLinear, err := s.validatorUtils.AreUnresolvedNodesLinear(callOpts, s.rollupAddress)
if err != nil {
   return nil, err
}
if !nodesLinear {
   log.Warn("rollup assertion fork detected")
   if effectiveStrategy == DefensiveStrategy {
      effectiveStrategy = StakeLatestStrategy
   }
   s.inactiveLastCheckedNode = nil
}

s.bringActiveUntilNode 存储并且 info.LatestStakedNode < s.bringActiveUntilNode, effectiveStrategy 会由 DefensiveStrategy 切换到 StakeLatestStrategy

  • 获取 rollup 链最新确认的节点,调用栈 s.rollup.LatestConfirmed(callOpts)->latestConfirmed, 合约函数细节如下:
/// @return Index of the latest confirmed node
function latestConfirmed() public view override returns (uint64) {
    return _latestConfirmed;
}
  • 判断是否需要做质押提升, isRequiredStakeElevated获取CurrentRequiredStake, 和 baseStake 判定是否要做质押提升,(获取现在的 stake, 最终进入 currentRequiredStake 函数, baseStake 进入合约处理逻辑)
  • effectiveStrategy 为 MakeNodesStrategy 策略,或者 effectiveStrategy 为 StakeLatestStrategy 并且 rawInfo(staker 的信息为 nil) 并且需要提升质押的情况,会去处理以下这些逻辑
  • 处理超时的挑战 resolveTimedOutChallenges, 进入处理挑战的细节,下面的内容会说到
  • 处理 unresolve 节点, unresolve 处理逻辑:处理节点的类别有三种,代码如下:
const (
   CONFIRM_TYPE_NONE ConfirmType = iota
   CONFIRM_TYPE_VALID
   CONFIRM_TYPE_INVALID
)

处理逻辑是先去获取 confirmType 和 unresolvedNodeIndex,分别调用 ValidatorUtils 里面的 checkDecidableNextNode 函数和 rollup 的 firstUnresolvedNode,如果是 CONFIRM_TYPE_INVALID 类别的,直接拒绝下一个节点,如果是 CONFIRM_TYPE_VALID,查找并确认下一个节点。核心逻辑代码如下

func (v *L1Validator) resolveNextNode(ctx context.Context, info *StakerInfo, latestConfirmedNode *uint64) (bool, error) {
   callOpts := v.getCallOpts(ctx)
   confirmType, err := v.validatorUtils.CheckDecidableNextNode(callOpts, v.rollupAddress)
   if err != nil {
      return false, err
   }
   unresolvedNodeIndex, err := v.rollup.FirstUnresolvedNode(callOpts)
   if err != nil {
      return false, err
   }
   switch ConfirmType(confirmType) {
   case CONFIRM_TYPE_INVALID:
      addr := v.wallet.Address()
      if info == nil || addr == nil || info.LatestStakedNode <= unresolvedNodeIndex {
         // We aren't an example of someone staked on a competitor
         return false, nil
      }
      log.Info("rejecing node", "node", unresolvedNodeIndex)
      _, err = v.rollup.RejectNextNode(v.builder.Auth(ctx), *addr)
      return true, err
   case CONFIRM_TYPE_VALID:
      nodeInfo, err := v.rollup.LookupNode(ctx, unresolvedNodeIndex)
      if err != nil {
         return false, err
      }
      afterGs := nodeInfo.AfterState().GlobalState
      _, err = v.rollup.ConfirmNextNode(v.builder.Auth(ctx), afterGs.BlockHash, afterGs.SendRoot)
      if err != nil {
         return false, err
      }
      *latestConfirmedNode = unresolvedNodeIndex
      return true, nil
   default:
      return false, nil
   }
}
  • 若 stakeInfo 的不为 nil, 并且最新节点 stake 节点小于最新确认节点,effectiveStrategy 为 WatchtowerStrategy 和 DefensiveStrategy,退还当前在最新确认节点上或之前质押的质押者的质押币,从汇总链中提取发件人拥有的未承诺资金, 调用方法为 s.rollup.ReturnOldDeposit 和 s.rollup.WithdrawStakerFunds,最好执行交易 ExecuteTransactions
  • walletAddressOrZero != (common.Address{})条件下继续做自己取回处理
  • 接下来是解决冲突 handleConflict, 条件满足的情况下回去创建挑战,然后进入挑战细节
  • rawInfo != nil || !resolvingNode || !requiredStakeElevated, 提升质押, 提升质押的细节逻辑
func (s *Staker) advanceStake(ctx context.Context, info *OurStakerInfo, effectiveStrategy StakerStrategy) error {
   active := effectiveStrategy >= StakeLatestStrategy
   action, wrongNodesExist, err := s.generateNodeAction(ctx, info, effectiveStrategy, s.config.MakeAssertionInterval)
   if err != nil {
      return err
   }
   if wrongNodesExist && effectiveStrategy == WatchtowerStrategy {
      log.Error("found incorrect assertion in watchtower mode")
   }
   if action == nil {
      info.CanProgress = false
      return nil
   }

   switch action := action.(type) {
   case createNodeAction:
      if wrongNodesExist && s.config.DisableChallenge {
         log.Error("refusing to challenge assertion as config disables challenges")
         info.CanProgress = false
         return nil
      }
      if !active {
         if wrongNodesExist && effectiveStrategy >= DefensiveStrategy {
            log.Warn("bringing defensive validator online because of incorrect assertion")
            s.bringActiveUntilNode = info.LatestStakedNode + 1
         }
         info.CanProgress = false
         return nil
      }

      // Details are already logged with more details in generateNodeAction
      info.CanProgress = false
      info.LatestStakedNode = 0
      info.LatestStakedNodeHash = action.hash

      // We'll return early if we already havea stake
      if info.StakeExists {
         _, err = s.rollup.StakeOnNewNode(s.builder.Auth(ctx), action.assertion.AsSolidityStruct(), action.hash, action.prevInboxMaxCount)
         return err
      }

      // If we have no stake yet, we'll put one down
      stakeAmount, err := s.rollup.CurrentRequiredStake(s.getCallOpts(ctx))
      if err != nil {
         return err
      }
      _, err = s.rollup.NewStakeOnNewNode(
         s.builder.AuthWithAmount(ctx, stakeAmount),
         action.assertion.AsSolidityStruct(),
         action.hash,
         action.prevInboxMaxCount,
      )
      if err != nil {
         return err
      }
      info.StakeExists = true
      return nil
   case existingNodeAction:
      info.LatestStakedNode = action.number
      info.LatestStakedNodeHash = action.hash
      if !active {
         if wrongNodesExist && effectiveStrategy >= DefensiveStrategy {
            log.Warn("bringing defensive validator online because of incorrect assertion")
            s.bringActiveUntilNode = action.number
            info.CanProgress = false
         } else {
            s.inactiveLastCheckedNode = &nodeAndHash{
               id:   action.number,
               hash: action.hash,
            }
         }
         return nil
      }
      log.Info("staking on existing node", "node", action.number)
      // We'll return early if we already havea stake
      if info.StakeExists {
         _, err = s.rollup.StakeOnExistingNode(s.builder.Auth(ctx), action.number, action.hash)
         return err
      }

      // If we have no stake yet, we'll put one down
      stakeAmount, err := s.rollup.CurrentRequiredStake(s.getCallOpts(ctx))
      if err != nil {
         return err
      }
      _, err = s.rollup.NewStakeOnExistingNode(
         s.builder.AuthWithAmount(ctx, stakeAmount),
         action.number,
         action.hash,
      )
      if err != nil {
         return err
      }
      info.StakeExists = true
      return nil
   default:
      panic("invalid action type")
   }
}

接下来的流程:createConflict->BuildingTransactionCount->ExecuteTransactions

三 附 SavourDao 社区招募信息

  • savour github: https://github.com/savour-labs
  • hailstone: https://github.com/savour-labs/hailstone
  • savour 官网:http://savour.group

目前我们正在在招募 Python, Go, Rust 和 Node 开发工程师,前端开发工程师(Vue 和 React 方向),产品经理, 视觉设计师和密码学研究员,欢迎有兴趣的朋友联系我们,联系方式如下:

  • 邮箱:guoshijiang2012@163.com
  • 微信:LGZAXE
  • discord: https://discord.gg/WW86tqEw
  • telegram: @shijiangguo
  • Twitter: @seek_web3

合作伙伴