Skip to content

guardian: executeRecovery 的 chain↔DB 双写需要对账机制(不是事务) #446

Description

@jhfnetboy

修订说明:本 issue 原标题/正文把问题定性为「两次 DB 写非原子,需要事务」。查证持久层后确认这个定性是错的 —— 事务修不好它。正文已按真实情况重写。短期的爆炸半径收窄已由 PR #449 完成。

真实问题

executeRecovery 链上确认后要把结果落库。链已经动了、回滚不了,而链不可能参与数据库事务。所以这是 chain + DB 的双写问题,不是数据库原子性问题。

不可约的失败场景:

链上 tx 确认  →  进程在任何 DB 写之前崩溃  →  DB 说 pending,链说已恢复

加事务不能消除它,只能把窗口从「两次写之间」缩小到「链确认与事务提交之间」。

已完成(PR #449

两次写改为按爆炸半径排序

  • updateAccountByAddresssignerAddress,安全相关)先写
  • updateRecoveryRequest(状态,账务)后写,单独 catch,记录 tx hash / account / new owner 供人工对账,不重新抛出(恢复确实发生了,抛错等于谎报)

最坏情况从「account 行记着旧 owner 且永不自愈」退化为「request 状态陈旧」。窗口收窄,未闭合。

待做:对账(真正的解法)

让 DB 从链派生,而不是试图与链保持同步写:

  1. 读路径以链为准 —— 返回账户 owner 的地方,对存在 executed/pending recovery 的账户校验链上实际 owner,不一致时以链为准并修正 DB
  2. 周期性 reconciler —— 扫描 status = "pending" 且已过 timelock 的 recovery request,读链上 activeRecovery() 与账户实际 owner:
    • 链上 proposal 已消耗且 owner 已变 → 补写 DB,标 executed
    • 链上仍有 active proposal → 保持 pending
    • 两者都无 → 标记异常待人工

这同时能自动修掉 PR #449 里那个「request 状态陈旧」的残留情况。

为什么不建议先加事务

查证结果(供后来人判断投入):

  • persistence.interface.ts 是 47 行、约 30 个方法的扁平 CRUD 接口,无事务概念
  • PostgresAdapter 构造函数注入 7 个独立 Repository<T>,每次写是 repository.update()。要跨表事务须改注入 DataSource 并用 dataSource.transaction(manager => ...)事务内所有方法必须改用该 manager 而非注入的 repository —— 等于给 30 个方法加一条可选 manager 通路,或重构整个 adapter
  • JsonAdapterfs.writeFile(整个文件),两个操作分别写 recovery-requests.json / accounts.json无锁、无原子 rename、无 fsync —— 连单次写的崩溃安全都不具备。跨两个 JSON 文件做 ACID 需要 WAL 或合并为单文件存储,是 adapter 重写

投入最大、且仍不解决本 issue。若将来因其他原因(例如转账流程)需要事务,那是独立议题,不应挂在这里。

关联

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions