惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
月光博客
月光博客
AI
AI
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
CXSECURITY Database RSS Feed - CXSecurity.com
WordPress大学
WordPress大学
GbyAI
GbyAI
L
Lohrmann on Cybersecurity
O
OpenAI News
Schneier on Security
Schneier on Security
P
Palo Alto Networks Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Troy Hunt's Blog
V2EX - 技术
V2EX - 技术
W
WeLiveSecurity
L
LINUX DO - 最新话题
人人都是产品经理
人人都是产品经理
S
Security Affairs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
A
Arctic Wolf
Recorded Future
Recorded Future
Microsoft Security Blog
Microsoft Security Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
G
GRAHAM CLULEY
N
Netflix TechBlog - Medium
TaoSecurity Blog
TaoSecurity Blog
C
Check Point Blog
Scott Helme
Scott Helme
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Vercel News
Vercel News
The Hacker News
The Hacker News
Y
Y Combinator Blog
Latest news
Latest news
SecWiki News
SecWiki News
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
Cisco Blogs
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
宝玉的分享
宝玉的分享
L
LINUX DO - 热门话题

pseudoyu

周报 #106 - OpenClaw 实践、搬家与生活的能量 周报 #105 - 打破无聊的社会 2025 年末回顾 - Anything different is good 周报 #104 - 被遗忘的生活一隅 周报 #103 - 工作随想与被吞噬的生活 周报 #102 - 我是如何使用 AI 的 周报 #101 - 消失了三个月,我的生活发生了什么变化 周报 #100 - 过去这十年 周报 #99 - 离开 RSS3,迷茫与重启人生 周报 #98 - 生活之书与 Agentic Coding 周报 #97 - 日本旅行小记 28 岁的自白:去找寻自己 周报 #96 - 基于 Roo Code/Cursor 的 iOS 开发工作流 周报 #95 - All AI 与 No AI 周报 #94 - 生活的界限与真实感 周报 #93 - 婚礼纪录片与失落的电影院 周报 #92 - 数码产品 Porn 与断舍离 周报 #91 - 一席、消失的时代与自由意志 周报 #90 - OVH 服务器、Coolify 与对 AI Coding 的思考 周报 #89 - 博客改版与 TheWan App 发布 周报 #88 - 方大同、《回留》与人生清单 周报 #87 - 我们主持了自己的宝可梦婚礼,还为它办了一个展 周报 #86 - 用影像打败时间 周报 #85 - 京华烟云 Moment in Peking 周报 #84 - 想起他们 Echos of Reverie 周报 #83 - 年初收纳(房间、设备、服务、软件) 周报 #82 - Brand New Year 与写日记 2024 年末回顾 - 不完美的日子 周报 #81 - Engaged -> Married 与时间的尺度 周报 #80 - 关于求婚、爱情与婚姻 求婚誓言 | 爱的本质是被看见 周报 #79 - 清迈旅居、曼谷之行与 Follow RSSHub 开发实践 #01:Telegram 频道 RSS 订阅实现与部署方案 Follow —— 信息获取的另一种形态 周报 #78 - NAS、Chromebook 与 Zeabur 折腾小记 周报 #77 - 一间只属于自己的房间(对亲密关系的思考) 周报 #76 - 关于 Adulting 的思考 周报 #75 - 回答 Dayu 关于独立博客的 9 个问题 周报 #74 - 清迈异闻录 周报 #73 - 《社交网络》、Ego 与中秋的现充生活 周报 #71 - Indie App 体验计划 周报 #70 - 消失的附近,Burnout 与 Boreout 周报 #69 - AI Coding 体验与学习的源动力 周报 #68 - 关于写博客这件事与我生活的色彩 使用 GoatCounter 与 Zeabur 搭建网站数据统计系统 周报 #67 - 使用 follow 重塑我的信息输入系统 周报 #66 - 10x 工程师、技术热情与个人工具箱 从零开始搭建你的免费博客评论系统(Remark42 + fly.io) 周报 #65 - Adventure X 体验、Apple Notes 笔记实践与 EpubKit 周报 #64 - 生活在此处(《斯通纳》与《Normal People》) 使用 WebP Cloud 与 Cloudflare WAF 为你的图床添加隐私和版权保护 当云服务器崩溃时,我是如何救援重要数据的 周报 #63 - 不愉快的订花经历、商家和消费者与日渐 AI 化的人 从零开始搭建你的免费图床系统(Cloudflare R2 + WebP Cloud) 2024 年了,我的博客有了什么变化 周报 #62 - 香港之行、5am club 计划与 Rust 学习 周报 #61 - 好朋友的婚礼与对婚姻的思考 27 岁的自白:成长是一件很扫兴的事 周报 #60 - 虫子旁、教育理念与 EpubKit 周报 #59 - 沙漠之行、家庭与家人 周报 #58 - 远程工作这一年 周报 #57 - 生活在别处(巴厘岛之行) 周报 #56 - 面基 Randy 与考科目二 周报 #55 - 油画体验、博客系统升级与对 self-hosting 的思考 周报 #54 - 漂流计划、钱包被盗与 Home Server 周报 #53 - 空白的三个月、生活漂流与春节 周报 #52 - 自谦型人格与感情的另一个视角 周报 #51 - 追星小鹿 Lawrence 与生病 周报 #50 - 天坛漫步、重拾阅读和费曼学习法 周报 #49 - 烧焦的键盘、庞贝神话展、健康调整与神奇的梦 周报 #48 - 内蒙之行、清迈游民、重启的周报与生活 周报 #47 - 采访、拳击与工作状态 周报 #46 - 告别长发、周报初心与合约开发 个人信息获取与知识管理系统(Heptabase + Logseq + Readwise) 周报 #45 - 读书笔记、知识管理与双城生活 周报 #44 - 静寂工人、xLog 与日常 周报 #43 - In the Mood for Love 周报 #42 - 感情观、生活状态与自我 周报 #41 - 生日的仪式感、时间管理与北京之行 26 岁的自白:推石头的人 周报 #40 - 开源之夏、代码之外与流逝的睡眠 周报 #39 - 初患新冠、过去的心绪与失而复得的分享欲 周报 #38 - Foundry、Logseq 与 Surge Ponte 周报 #37 - 浮生半月闲( 武汉 | 香港 | 五月天) 周报 #36 - 肠胃炎、捏捏近况与新的旅途 周报 #35 - 登报、搬家和生病的捏捏 周报 #34 - Don't Stop the Clocks 周报 #33 - 北京 x 故事 x 离别 周报 #32 - 武汉、久别重逢与流动的心绪 周报 #31 - 开源、前端开发与 ChatGPT 实践 周报 #30 - 开源预算、写作初心与对技术的谦卑 Cosmos 区块链架构与 Tendermint 共识机制 周报 #29 - 日语学习经验、Steam Deck 与居酒屋 周报 #28 - Mastodon、故乡与捉迷藏的猫 周报 #27 - 何以为家 周报 #26 - 博客、客制化键盘和新服务器 周报 #25 - 基于 Crossbell 的个人信息输出与同步系统(重构) 摄影集 #02 - Moment In Peking (The Autumn) 摄影集 #01 - Cat In The Box 2022 年末回顾 - 迷茫、低谷与改变
通过状态锁在 Solidity 智能合约中实现两阶段提交
pseudoyu · 2022-07-01 · via pseudoyu

前言

在一些牵扯到多个系统或合约交互的智能合约应用场景中,尤其是一些资产/数据准确性较为敏感的业务中,我们需要保证在整个业务流程中数据的原子性。因此,我们需要在合约层面实现类似多阶段提交的机制,即将合约中的状态更改过程分解为预提交和正式提交两个阶段。

本文通过状态锁的机制实现了一个最小化的两阶段提交模型,完整合约代码参见 TwoPhaseCommit.sol,下文将对本合约核心逻辑进行讲解,并尽量遵循风格指南与最佳实践。

注:本合约因初始场景主要考虑的是联盟链中的业务用途,未对 Gas fee 等进行特定优化,仅供学习参考。

合约逻辑

合约结构

两阶段提交场景包含以下方法:

  1. set: 两阶段 - 预提交
  2. commit: 两阶段 - 正式提交
  3. rollback: 两阶段 - 回滚

因 Solidity 语言对于字符串长度判断/比较等有一些限制,为了提升合约代码的可读性,本合约提供了部分辅助方法,主要包含以下方法:

  1. isValidKey: 检查 key 是否合法
  2. isValidValue: 检查 value 是否合法
  3. isEqualString: 比较两个字符串是否相等

两阶段提交核心逻辑

在两阶段提交场景中,本合约提供了一套简易的 set, commit, rollback 方法实现,实现了将合约调用传入的 key-value 键值对存储到链上。我们通过状态锁的机制来实现跨链交易的原子性。我们定义了如下数据结构:

enum State {
    UNLOCKED,
    LOCKED
}

struct Payload {
    State state;
    string value;
    string lockValue;
}

其中,State 为枚举类型,记录了链上 key 值的锁定状态,而 Payload 结构则会对锁定状态、当前值与正在锁定的值进行存储,并通过如下 mapping 结构与 key 进行绑定:

mapping (string => Payload) keyToPayload;

因此,我们可以根据 keyToPaylaod 对合约调用中的每一个 key 进行状态跟踪,并在下述 set, commit, rollback 方法中对 key 的状态进行检查,进行一些异常处理。

set()

set() 方法中,我们会检查 key 的状态,如为 State.LOCKED,则不会进行存储并抛出异常:

if (keyToPayload[_key].state == State.LOCKED) {
    revert TwoPhaseCommit__DataIsLocked();
}

如为 State.UNLOCKED,则会将合约调用传入的值存储至 lockValue 中,并将其状态设置为 LOCKED,等待后续 commitrollback 进行解锁。

keyToPayload[_key].state = State.LOCKED;
keyToPayload[_key].lockValue = _value;

commit()

commit() 方法中,我们会检查 key 的状态,如为 State.UNLOCKED,则不会对该 key 进行操作,并抛出异常:

if (keyToPayload[_key].state == State.UNLOCKED) {
    revert TwoPhaseCommit__DataIsNotLocked();
}

如为 State.LOCKED,我们检查合约调用传入的值是否与 lockValue 相等,如不相等,则抛出异常:

if (!isEqualString(keyToPayload[_key].lockValue, _value)) {
    revert TwoPhaseCommit__DataIsInconsistent();
}

如值相等,则会将该 key 所对应的 value 存储上链,将 key 的状态设置为 UNLOCKED,更新当前值 value,同时将 lockValue 置空:

store[_key] = _value;
keyToPayload[_key].state = State.UNLOCKED;
keyToPayload[_key].value = _value;
keyToPayload[_key].lockValue = "";

rollback()

rollback() 方法中,我们会检查 key 的状态,如为 State.UNLOCKED,则不会对该 key 进行操作,并抛出异常:

if (keyToPayload[_key].state == State.UNLOCKED) {
    revert TwoPhaseCommit__DataIsNotLocked();
}

如为 State.LOCKED,我们检查合约调用传入的值是否与 lockValue 相等,如不相等,则抛出异常:

if (!isEqualString(keyToPayload[_key].lockValue, _value)) {
    revert TwoPhaseCommit__DataIsInconsistent();
}

如值相等,则会将该 key 所对应的 value 存储上链,将 key 的状态设置为 UNLOCKED,并将 lockValue 置空:

keyToPayload[_key].state = State.UNLOCKED;
keyToPayload[_key].lockValue = "";

错误处理逻辑

在合约执行异常场景中,我们会抛出错误并进行回滚。为了更好地提升错误消息的可读性并方便上层应用人员进行错误捕获与处理,我们采用了错误类型定义的方式,定义了各类异常场景,因为我在错误命名中已经包含了大部分信息,所以未定义错误类型额外参数值,可以根据需求自行定制。

error TwoPhaseCommit__DataKeyIsNull();
error TwoPhaseCommit__DataValueIsNull();
error TwoPhaseCommit__DataIsNotExist();
error TwoPhaseCommit__DataIsLocked();
error TwoPhaseCommit__DataIsNotLocked();
error TwoPhaseCommit__DataIsInconsistent();

在具体合约逻辑中,我们通过 revert 方法抛出异常,如:

if (!isValidKey(bytes(_key))) {
    revert TwoPhaseCommit__DataKeyIsNull();
}

if (!isValidValue(bytes(_value))) {
    revert TwoPhaseCommit__DataValueIsNull();
}

if (keyToPayload[_key].state == State.UNLOCKED) {
    revert TwoPhaseCommit__DataIsNotLocked();
}

if (!isEqualString(keyToPayload[_key].lockValue, _value)) {
    revert TwoPhaseCommit__DataIsInconsistent();
}

通用参数校验

我们会对传入参数进行一些合法性校验,为了提供拓展性,我们通过 isValidKey()isValidValue() 方法对 key 与 value 进行独立校验:

/**
 * @notice 数据键格式校验
 * @param _key 数据 - 键
 */
function isValidKey(bytes memory _key) private pure returns (bool)
{
    bytes memory key = _key;

    if (key.length == 0) {
        return false;
    }
    return true;
}

/**
 * @notice 数据值格式校验
 * @param _value 数据 - 值
 */
function isValidValue(bytes memory _value) private pure returns (bool)
{
    bytes memory value = _value;

    if (value.length == 0) {
        return false;
    }
    return true;
}

本合约只进行了非空校验,可根据业务需要自行定制业务逻辑,在需要校验的地方调用即可,如:

if (!isValidKey(bytes(_key))) {
    revert TwoPhaseCommit__DataKeyIsNull();
}

if (!isValidValue(bytes(_value))) {
    revert TwoPhaseCommit__DataValueIsNull();
}

if (!isValidValue(bytes(store[_key]))) {
    revert TwoPhaseCommit__DataIsNotExist();
}

事件机制

此外,我们定义了核心方法对应的 event,并为事件设置了 indexed 以方便上层应用进行监听和处理。

event setEvent(string indexed key, string indexed value);
event getEvent(string indexed key, string indexed value);
event commitEvent(string indexed key, string indexed value);
event rollbackEvent(string indexed key, string indexed value);

在合约方法中通过 emit() 方法抛出 event,如:

emit setEvent(_key, _value);
emit getEvent(_key, _value);
emit commitEvent(_key, _value);
emit rollbackEvent(_key, _value);

总结

以上就是我两阶段提交合约的一个最佳实践,关于 Solidity 基础语法可参看『Solidity 智能合约开发 - 基础』,后续我还会对更多合约场景进行实践与讲解,敬请关注。

参考资料

  1. TwoPhaseCommit.sol 合约源码
  2. Solidity 智能合约开发 - 基础
  3. Solidity 官方文档