变更日志

  • 2019-10-22:初始草案

背景

ICS 26 - 路由模块 定义了函数 handlePacketRecv。 在 ICS 26 中,路由模块被定义为位于各应用模块之上的一层,负责验证消息并将其路由到目标模块。它可以实现为一个独立模块,不过我们已经在 baseapp 中具备基于目标标识符路由消息的能力。因此,本 ADR 建议复用现有的 baseapp.router,将数据包路由到应用模块。 通常,路由模块回调包含两个独立步骤:验证与执行。这与 SDK 内部的 AnteHandler-Handler 模型相对应。我们可以在 AnteHandler 中完成验证,以减少样板式验证代码,从而提升开发者体验。 对于原子性的多消息交易,我们希望即使应用侧状态变更发生回滚,IBC 相关的状态修改仍然能够保留。一个例子是:IBC 代币发送消息后紧接着执行质押委托,后者使用前一个数据包消息接收到的代币。如果代币接收因任何原因失败,我们可能不希望继续执行该交易,但也不希望中止整笔交易,否则序列号和承诺会被回滚,通道也会被卡住。为了解决这个问题,本 ADR 提议引入新的 CodeType:CodeTxBreak。

决策

PortKeeper 将持有能力键,该键只能访问绑定到某个端口的通道。持有 PortKeeper 的实体将能够调用其上的方法,这些方法与 ChannelKeeper 上同名的方法相对应,但仅限于被允许的端口。将定义 ChannelKeeper.Port(string, ChannelChecker),以便更方便地构造具备能力安全性的 PortKeeper。这一点将在另一份 ADR 中进一步说明,当前暂时使用不安全的 ChannelKeeper。 当某个处理器返回 !Result.IsOK() 时,baseapp.runMsgs 会中断对消息的遍历循环。然而,如果 Result.IsOK() || Result.Code.IsBreak() 成立,外层逻辑仍会写入缓存存储。当 Result.Code == CodeTxBreak 时,Result.Code.IsBreak() 为真。
func (app *BaseApp) runTx(tx Tx) (result Result) {
  msgs := tx.GetMsgs()
  
  // AnteHandler
  if app.anteHandler != nil {
    anteCtx, msCache := app.cacheTxContext(ctx)
    newCtx, err := app.anteHandler(anteCtx, tx)
    if !newCtx.IsZero() {
      ctx = newCtx.WithMultiStore(ms)
    }

    if err != nil {
      // error handling logic
      return res
    }

    msCache.Write()
  }
  
  // Main Handler
  runMsgCtx, msCache := app.cacheTxContext(ctx)
  result = app.runMsgs(runMsgCtx, msgs)
  // BEGIN modification made in this ADR
  if result.IsOK() || result.IsBreak() {
  // END
    msCache.Write()
  }

  return result
}
Cosmos SDK 将为 IBC 数据包接收定义一个 AnteDecorator。该 AnteDecorator 会遍历交易中包含的消息,使用类型 switch 检查消息是否包含传入的 IBC 数据包;如果包含,则验证其 Merkle 证明。
type ProofVerificationDecorator struct {
  clientKeeper ClientKeeper
  channelKeeper ChannelKeeper
}

func (pvr ProofVerificationDecorator) AnteHandle(ctx Context, tx Tx, simulate bool, next AnteHandler) (Context, error) {
  for _, msg := range tx.GetMsgs() {
    var err error
    switch msg := msg.(type) {
    case client.MsgUpdateClient:
      err = pvr.clientKeeper.UpdateClient(msg.ClientID, msg.Header)
    case channel.MsgPacket:
      err = pvr.channelKeeper.RecvPacket(msg.Packet, msg.Proofs, msg.ProofHeight)
    case channel.MsgAcknowledgement:
      err = pvr.channelKeeper.AcknowledgementPacket(msg.Acknowledgement, msg.Proof, msg.ProofHeight)
    case channel.MsgTimeoutPacket:
      err = pvr.channelKeeper.TimeoutPacket(msg.Packet, msg.Proof, msg.ProofHeight, msg.NextSequenceRecv)
    case channel.MsgChannelOpenInit;
      err = pvr.channelKeeper.CheckOpen(msg.PortID, msg.ChannelID, msg.Channel)
    default:
      continue
    }

    if err != nil {
      return ctx, err
    }
  }
  
  return next(ctx, tx, simulate)
}
其中,MsgUpdateClient、MsgPacket、MsgAcknowledgement、MsgTimeoutPacket 是 sdk.Msg 类型,分别对应路由模块中的 handleUpdateClient、handleRecvPacket、handleAcknowledgementPacket、handleTimeoutPacket。 RecvPacket、VerifyAcknowledgement、VerifyTimeout 的副作用将被提取到独立函数中,分别是 WriteAcknowledgement、DeleteCommitment、DeleteCommitmentTimeout,这些函数会在应用处理器执行完成后被调用。 WriteAcknowledgement 会将确认写入状态,以便对端链可以验证,并递增序列号以防止重复执行。DeleteCommitment 会删除已存储的承诺,DeleteCommitmentTimeout 则会删除承诺,并在有序通道的情况下关闭通道。
func (keeper ChannelKeeper) WriteAcknowledgement(ctx Context, packet Packet, ack []byte) {
  keeper.SetPacketAcknowledgement(ctx, packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence(), ack)
  keeper.SetNextSequenceRecv(ctx, packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
}

func (keeper ChannelKeeper) DeleteCommitment(ctx Context, packet Packet) {
  keeper.deletePacketCommitment(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
}

func (keeper ChannelKeeper) DeleteCommitmentTimeout(ctx Context, packet Packet) {
  k.deletePacketCommitment(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
  
  if channel.Ordering == types.ORDERED [
    channel.State = types.CLOSED
    k.SetChannel(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), channel)
  }
}
每个应用处理器都应在 PortKeeper 上调用相应的收尾方法,以便递增序列号(对于数据包)或移除承诺(对于确认与超时)。调用这些函数意味着应用逻辑已经成功执行。 不过,处理器在调用这些方法之后仍然可以返回带有 CodeTxBreak 的 Result,这样既能持久化已经完成的状态变更,又能在遇到语义无效的数据包时阻止后续消息继续执行。这样可以确保前面 IBC 数据包的序列号已被递增(从而防止重复执行),同时不继续处理后续消息。 无论如何,应用模块都不应该返回会导致状态回滚的结果,否则通道将无法继续推进。 将引入 ChannelKeeper.CheckOpen 方法。这将取代路由模块规范中定义的 onChanOpen*。应用模块不再需要分别定义每个通道握手回调函数,而是可以通过 AppModule 提供 ChannelChecker 函数,并在顶层应用中注入到 ChannelKeeper.Port()。CheckOpen 会根据 PortID 找到正确的 ChannelChecker 并调用它;如果应用不接受,则返回错误。 ProofVerificationDecorator 将被插入到顶层应用中。让每个模块各自负责调用证明验证逻辑并不安全,因为应用可能会因疏忽而在 IBC 协议层面表现异常。 ProofVerificationDecorator 应当紧跟当前 auth.NewAnteHandler 中默认的抗女巫攻击层之后:
// add IBC ProofVerificationDecorator to the Chain of
func NewAnteHandler(
  ak keeper.AccountKeeper, supplyKeeper types.SupplyKeeper, ibcKeeper ibc.Keeper,
  sigGasConsumer SignatureVerificationGasConsumer) sdk.AnteHandler {
  return sdk.ChainAnteDecorators(
    NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first
    ...
    NewIncrementSequenceDecorator(ak),
    ibcante.ProofVerificationDecorator(ibcKeeper.ClientKeeper, ibcKeeper.ChannelKeeper), // innermost AnteDecorator
  )
}
该 ADR 的实现还会在 Packet 中创建一个类型为 []byte 的 Data 字段,接收模块可以将其反序列化为自身的私有类型。具体如何解释和处理该字段由应用模块自行决定,而不是由 IBC keeper 处理。这对于动态 IBC 至关重要。 应用侧用法示例:
type AppModule struct {}

// CheckChannel will be provided to the ChannelKeeper as ChannelKeeper.Port(module.CheckChannel)
func (module AppModule) CheckChannel(portID, channelID string, channel Channel) error {
  if channel.Ordering != UNORDERED {
    return ErrUncompatibleOrdering()
  }
  if channel.CounterpartyPort != "bank" {
    return ErrUncompatiblePort()
  }
  if channel.Version != "" {
    return ErrUncompatibleVersion()
  }
  return nil
}

func NewHandler(k Keeper) Handler {
  return func(ctx Context, msg Msg) Result {
    switch msg := msg.(type) {
    case MsgTransfer:
      return handleMsgTransfer(ctx, k, msg)
    case ibc.MsgPacket:
      var data PacketDataTransfer
      if err := types.ModuleCodec.UnmarshalBinaryBare(msg.GetData(), &data); err != nil {
        return err
      }
      return handlePacketDataTransfer(ctx, k, msg, data)
    case ibc.MsgTimeoutPacket:
      var data PacketDataTransfer
      if err := types.ModuleCodec.UnmarshalBinaryBare(msg.GetData(), &data); err != nil {
        return err
      }
      return handleTimeoutPacketDataTransfer(ctx, k, packet)
    // interface { PortID() string; ChannelID() string; Channel() ibc.Channel }
    // MsgChanInit, MsgChanTry implements ibc.MsgChannelOpen
    case ibc.MsgChannelOpen: 
      return handleMsgChannelOpen(ctx, k, msg)
    }
  }
}

func handleMsgTransfer(ctx Context, k Keeper, msg MsgTransfer) Result {
  err := k.SendTransfer(ctx,msg.PortID, msg.ChannelID, msg.Amount, msg.Sender, msg.Receiver)
  if err != nil {
    return sdk.ResultFromError(err)
  }

  return sdk.Result{}
}

func handlePacketDataTransfer(ctx Context, k Keeper, packet Packet, data PacketDataTransfer) Result {
  err := k.ReceiveTransfer(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetDestinationPort(), packet.GetDestinationChannel(), data)
  if err != nil {
    // TODO: Source chain sent invalid packet, shutdown channel
  }
  k.ChannelKeeper.WriteAcknowledgement([]byte{0x00}) // WriteAcknowledgement increases the sequence, preventing double spending
  return sdk.Result{}
}

func handleCustomTimeoutPacket(ctx Context, k Keeper, packet CustomPacket) Result {
  err := k.RecoverTransfer(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetDestinationPort(), packet.GetDestinationChannel(), data)
  if err != nil {
    // This chain sent invalid packet or cannot recover the funds
    panic(err)
  }
  k.ChannelKeeper.DeleteCommitmentTimeout(ctx, packet)
  // packet timeout should not fail
  return sdk.Result{}
}

func handleMsgChannelOpen(sdk.Context, k Keeper, msg MsgOpenChannel) Result {
  k.AllocateEscrowAddress(ctx, msg.ChannelID())
  return sdk.Result{}
}

状态

提议中

影响

正面

  • 面向开发者的接口更直观,IBC 处理器无需关心 IBC 认证
  • 状态变更承诺逻辑被嵌入到 baseapp.runTx 逻辑中

负面

  • 无法支持动态端口,路由与 baseapp router 绑定

中性

  • 引入新的 AnteHandler 装饰器。
  • 可以通过分层端口标识符支持动态端口,详见 #5290

参考


Changelog

  • 22-10-2019: Initial Draft

Context

ICS 26 - Routing Module defines a function handlePacketRecv. In ICS 26, the routing module is defined as a layer above each application module which verifies and routes messages to the destination modules. It is possible to implement it as a separate module, however, we already have the functionality to route messages upon the destination identifiers in the baseapp. This ADR suggests to utilize existing baseapp.router to route packets to application modules. Generally, routing module callbacks have two separate steps in them, verification and execution. This corresponds to the AnteHandler-Handler model inside the SDK. We can do the verification inside the AnteHandler in order to increase developer ergonomics by reducing boilerplate verification code. For atomic multi-message transaction, we want to keep the IBC related state modification to be preserved even the application side state change reverts. One of the example might be IBC token sending message following with stake delegation which uses the tokens received by the previous packet message. If the token receiving fails for any reason, we might not want to keep executing the transaction, but we also don’t want to abort the transaction or the sequence and commitment will be reverted and the channel will be stuck. This ADR suggests new CodeType, CodeTxBreak, to fix this problem.

Decision

PortKeeper will have the capability key that is able to access only the channels bound to the port. Entities that hold a PortKeeper will be able to call the methods on it which are corresponding with the methods with the same names on the ChannelKeeper, but only with the allowed port. ChannelKeeper.Port(string, ChannelChecker) will be defined to easily construct a capability-safe PortKeeper. This will be addressed in another ADR and we will use insecure ChannelKeeper for now. baseapp.runMsgs will break the loop over the messages if one of the handlers returns !Result.IsOK(). However, the outer logic will write the cached store if Result.IsOK() || Result.Code.IsBreak(). Result.Code.IsBreak() if Result.Code == CodeTxBreak.
func (app *BaseApp) runTx(tx Tx) (result Result) {
  msgs := tx.GetMsgs()
  
  // AnteHandler
  if app.anteHandler != nil {
    anteCtx, msCache := app.cacheTxContext(ctx)
    newCtx, err := app.anteHandler(anteCtx, tx)
    if !newCtx.IsZero() {
      ctx = newCtx.WithMultiStore(ms)
    }

    if err != nil {
      // error handling logic
      return res
    }

    msCache.Write()
  }
  
  // Main Handler
  runMsgCtx, msCache := app.cacheTxContext(ctx)
  result = app.runMsgs(runMsgCtx, msgs)
  // BEGIN modification made in this ADR
  if result.IsOK() || result.IsBreak() {
  // END
    msCache.Write()
  }

  return result
}
The Cosmos SDK will define an AnteDecorator for IBC packet receiving. The AnteDecorator will iterate over the messages included in the transaction, type switch to check whether the message contains an incoming IBC packet, and if so verify the Merkle proof.
type ProofVerificationDecorator struct {
  clientKeeper ClientKeeper
  channelKeeper ChannelKeeper
}

func (pvr ProofVerificationDecorator) AnteHandle(ctx Context, tx Tx, simulate bool, next AnteHandler) (Context, error) {
  for _, msg := range tx.GetMsgs() {
    var err error
    switch msg := msg.(type) {
    case client.MsgUpdateClient:
      err = pvr.clientKeeper.UpdateClient(msg.ClientID, msg.Header)
    case channel.MsgPacket:
      err = pvr.channelKeeper.RecvPacket(msg.Packet, msg.Proofs, msg.ProofHeight)
    case channel.MsgAcknowledgement:
      err = pvr.channelKeeper.AcknowledgementPacket(msg.Acknowledgement, msg.Proof, msg.ProofHeight)
    case channel.MsgTimeoutPacket:
      err = pvr.channelKeeper.TimeoutPacket(msg.Packet, msg.Proof, msg.ProofHeight, msg.NextSequenceRecv)
    case channel.MsgChannelOpenInit;
      err = pvr.channelKeeper.CheckOpen(msg.PortID, msg.ChannelID, msg.Channel)
    default:
      continue
    }

    if err != nil {
      return ctx, err
    }
  }
  
  return next(ctx, tx, simulate)
}
Where MsgUpdateClient, MsgPacket, MsgAcknowledgement, MsgTimeoutPacket are sdk.Msg types correspond to handleUpdateClient, handleRecvPacket, handleAcknowledgementPacket, handleTimeoutPacket of the routing module, respectively. The side effects of RecvPacket, VerifyAcknowledgement, VerifyTimeout will be extracted out into separated functions, WriteAcknowledgement, DeleteCommitment, DeleteCommitmentTimeout, respectively, which will be called by the application handlers after the execution. WriteAcknowledgement writes the acknowledgement to the state that can be verified by the counter-party chain and increments the sequence to prevent double execution. DeleteCommitment will delete the commitment stored, DeleteCommitmentTimeout will delete the commitment and close channel in case of ordered channel.
func (keeper ChannelKeeper) WriteAcknowledgement(ctx Context, packet Packet, ack []byte) {
  keeper.SetPacketAcknowledgement(ctx, packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence(), ack)
  keeper.SetNextSequenceRecv(ctx, packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
}

func (keeper ChannelKeeper) DeleteCommitment(ctx Context, packet Packet) {
  keeper.deletePacketCommitment(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
}

func (keeper ChannelKeeper) DeleteCommitmentTimeout(ctx Context, packet Packet) {
  k.deletePacketCommitment(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
  
  if channel.Ordering == types.ORDERED [
    channel.State = types.CLOSED
    k.SetChannel(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), channel)
  }
}
Each application handler should call respective finalization methods on the PortKeeper in order to increase sequence (in case of packet) or remove the commitment (in case of acknowledgement and timeout). Calling those functions implies that the application logic has successfully executed. However, the handlers can return Result with CodeTxBreak after calling those methods which will persist the state changes that has been already done but prevent any further messages to be executed in case of semantically invalid packet. This will keep the sequence increased in the previous IBC packets(thus preventing double execution) without proceeding to the following messages. In any case the application modules should never return state reverting result, which will make the channel unable to proceed. ChannelKeeper.CheckOpen method will be introduced. This will replace onChanOpen* defined under the routing module specification. Instead of define each channel handshake callback functions, application modules can provide ChannelChecker function with the AppModule which will be injected to ChannelKeeper.Port() at the top level application. CheckOpen will find the correct ChannelChecker using the PortID and call it, which will return an error if it is unacceptable by the application. The ProofVerificationDecorator will be inserted to the top level application. It is not safe to make each module responsible to call proof verification logic, whereas application can misbehave(in terms of IBC protocol) by mistake. The ProofVerificationDecorator should come right after the default sybil attack resistant layer from the current auth.NewAnteHandler:
// add IBC ProofVerificationDecorator to the Chain of
func NewAnteHandler(
  ak keeper.AccountKeeper, supplyKeeper types.SupplyKeeper, ibcKeeper ibc.Keeper,
  sigGasConsumer SignatureVerificationGasConsumer) sdk.AnteHandler {
  return sdk.ChainAnteDecorators(
    NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first
    ...
    NewIncrementSequenceDecorator(ak),
    ibcante.ProofVerificationDecorator(ibcKeeper.ClientKeeper, ibcKeeper.ChannelKeeper), // innermost AnteDecorator
  )
}
The implementation of this ADR will also create a Data field of the Packet of type []byte, which can be deserialised by the receiving module into its own private type. It is up to the application modules to do this according to their own interpretation, not by the IBC keeper. This is crucial for dynamic IBC. Example application-side usage:
type AppModule struct {}

// CheckChannel will be provided to the ChannelKeeper as ChannelKeeper.Port(module.CheckChannel)
func (module AppModule) CheckChannel(portID, channelID string, channel Channel) error {
  if channel.Ordering != UNORDERED {
    return ErrUncompatibleOrdering()
  }
  if channel.CounterpartyPort != "bank" {
    return ErrUncompatiblePort()
  }
  if channel.Version != "" {
    return ErrUncompatibleVersion()
  }
  return nil
}

func NewHandler(k Keeper) Handler {
  return func(ctx Context, msg Msg) Result {
    switch msg := msg.(type) {
    case MsgTransfer:
      return handleMsgTransfer(ctx, k, msg)
    case ibc.MsgPacket:
      var data PacketDataTransfer
      if err := types.ModuleCodec.UnmarshalBinaryBare(msg.GetData(), &data); err != nil {
        return err
      }
      return handlePacketDataTransfer(ctx, k, msg, data)
    case ibc.MsgTimeoutPacket:
      var data PacketDataTransfer
      if err := types.ModuleCodec.UnmarshalBinaryBare(msg.GetData(), &data); err != nil {
        return err
      }
      return handleTimeoutPacketDataTransfer(ctx, k, packet)
    // interface { PortID() string; ChannelID() string; Channel() ibc.Channel }
    // MsgChanInit, MsgChanTry implements ibc.MsgChannelOpen
    case ibc.MsgChannelOpen: 
      return handleMsgChannelOpen(ctx, k, msg)
    }
  }
}

func handleMsgTransfer(ctx Context, k Keeper, msg MsgTransfer) Result {
  err := k.SendTransfer(ctx,msg.PortID, msg.ChannelID, msg.Amount, msg.Sender, msg.Receiver)
  if err != nil {
    return sdk.ResultFromError(err)
  }

  return sdk.Result{}
}

func handlePacketDataTransfer(ctx Context, k Keeper, packet Packet, data PacketDataTransfer) Result {
  err := k.ReceiveTransfer(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetDestinationPort(), packet.GetDestinationChannel(), data)
  if err != nil {
    // TODO: Source chain sent invalid packet, shutdown channel
  }
  k.ChannelKeeper.WriteAcknowledgement([]byte{0x00}) // WriteAcknowledgement increases the sequence, preventing double spending
  return sdk.Result{}
}

func handleCustomTimeoutPacket(ctx Context, k Keeper, packet CustomPacket) Result {
  err := k.RecoverTransfer(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetDestinationPort(), packet.GetDestinationChannel(), data)
  if err != nil {
    // This chain sent invalid packet or cannot recover the funds
    panic(err)
  }
  k.ChannelKeeper.DeleteCommitmentTimeout(ctx, packet)
  // packet timeout should not fail
  return sdk.Result{}
}

func handleMsgChannelOpen(sdk.Context, k Keeper, msg MsgOpenChannel) Result {
  k.AllocateEscrowAddress(ctx, msg.ChannelID())
  return sdk.Result{}
}

Status

Proposed

Consequences

Positive

  • Intuitive interface for developers - IBC handlers do not need to care about IBC authentication
  • State change commitment logic is embedded into baseapp.runTx logic

Negative

  • Cannot support dynamic ports, routing is tied to the baseapp router

Neutral

  • Introduces new AnteHandler decorator.
  • Dynamic ports can be supported using hierarchical port identifier, see #5290 for detail

References