从 DAS 开始了解 CKB 应用开发(一)—— 如何保证 DAS 帐户的惟一性

近日,DAS 创始人 TimYang (杨敏)在 Nervos CKB 上开发了 DAS
去中心化帐户服务。借着此次的产品开发,TimYang 将经过《从 DAS 开始了解 CKB
应用开发》系列文章,向你们阐述他的设计思路和开发历程,让你们了解如何在世界上第一个基于 UTXO 架构的公链 CKB 上构建产品级应用。数据库

在第一篇文章中,Tim 将向你们介绍他们在设计 DAS 时面临的第一个大问题 ——如何保证 DAS 帐户的惟一性。欢迎阅读及体验。架构

开篇

DAS(Decentralized Account Services),是基于 CKB 构建的去中心化帐户服务。DAS 项目自己,旨在为新世界提供一套兼具抗审查性、惟一性、可识别性的帐户体系。在 DAS 的第一阶段,它看起来像是以太坊的 ENS,而且具有一些比 ENS 更为优秀的特性。但 DAS 要作的,不只仅是更好的 ENS,而是试图为加密世界的「去中心化帐户/身份」这块拼图,带来新的定义。并发

DAS 不是一个概念性产品,它目前已经运行在 CKB 测试网上,并预计于近期上线主网。能够经过 https://da.services 体验测试版本。学习

DAS 是基于 CKB 开发的区块链应用。在诸多公链中,为何咱们要选择基于 CKB 来进行开发呢?缘由有二:区块链

  • PoW 共识 + Cell(UTXO)模型
  • 自定义密码学原语(高度开放的架构),基于此咱们可以实现 DAS 帐户能够被任意公链地址所持有。

CKB 是少有的在 UTXO 模型之上构建智能合约环境,并主张「链下计算,链上验证」的公链平台。这些主张和设计通过了充分的考量,很是具备前瞻性,但同时也带来了全新的去中心化应用开发范式。习惯了中心化应用开发和以太坊智能合约开发的开发者,在刚开始接触 CKB 开发时,会充满不适应。加之目前尚没有标杆应用出现,这让开发者们对于 CKB 到底能作什么,是否是真的值得花精力去学习 CKB,充满疑问。测试

《从 DAS 开始了解 CKB 应用开发》系列文章的目的,也正在于此。咱们将咱们在 DAS 实践过程当中的问题、思考,以及解决方案整理成一系列文章,让你们了解咱们是如何基于 CKB 构建产品级应用的。但愿借此给更多开发者带来启发,了解 CKB 能作什么,应该怎么作。ui

须要说明的是:加密

在面对一个问题时,咱们采用的思路和解决方案,不必定是最优解,甚至大几率不是。但这些知足咱们场景的思路和解决方案,若能给你们带来启发,目的便已达到。
这一系列的文章,都假定读者已经充分理解 Cell 模型和「链下计算,链上验证」模型。

如何保证 DAS 帐户的惟一性

咱们将在第一篇文章中,探讨 DAS 面临的第一个棘手问题:spa

每一个 DAS 帐户都须要一个 Cell 来存储其数据,Cell 是经过不一样交易来建立的,这意味着 DAS
系统的全局状态数据是分散存储在各个角落的。同时每一个 DAS 帐户又必须具备惟一性。那么,当一个 DAS
帐户注册行为发生时,咱们如何判断该帐户是否已经存在呢?

咱们把这个问题通常化:对于分散存储的数据集,在插入数据时,如何保证每条数据的惟一性?设计

对于习惯了中心化应用开发和以太坊智能合约开发的开发者而言,要保证注册的帐户不重复,这一件几乎不用思考的事情,你能够把全部的数据都放入合约的存储空间,因为这些数据是集中存储的,因此在插入数据以前,你只须要先检索一下数据是否存在便可。

但鉴于 CKB 的 Cell 模型,数据分散存储在用户本身的空间中,咱们没法在链上去检索全部数据。毕竟咱们不可能在一笔交易的输入中,放下全部已经存在的 Cell。即使能放下,链上脚本也没法知晓这笔交易在构造时,交易发起人是否真的将全部须要 Cell 都放到了输入中。

咱们将列举全部咱们曾考虑过的保证惟一性的方案。之因此把最终没采用的方案都拿出来分析,是但愿你们能够经过观察咱们走过的「弯路」,开始适应 CKB 的开发范式,避免之后本身走「弯路」。

在讨论方案以前,咱们应先明确咱们的设计原则。正是这些原则,最终决定了咱们采用什么样的方案。这些原则,优先级从高到低依次为:

  • 去中心化程度,对于 DAS 要达成的目标而言,去中心化是最基础性的原则
  • 用户体验,技术方案不容许带来糟糕的用户体验
  • 工程复杂度,越简单的架构每每越有效
  • 费用成本低,能节省的费用尽可能节省

若是你只关心最终方案,能够直接跳转到「方案六」开始阅读。

方案一:把全部帐户存储在一个 Cell 里

这是最符合直觉的一种方案,毕竟以太坊的智能合约就能够这么干。建立一个 GlobalStatusCell,在 GlobalStatusCell 的 data 中存放全部已注册的帐户。当新的注册发生时,在交易中把这个 GlobalStatusCell 做为输入,修改后的GlobalStatusCell 做为输出。type 脚本检查新注册的帐户是否已经存在,若是存在就返回非 0,交易失败;若是不存在,那就检查输出的 GlobalStatusCell 中是否包含了新帐户,而后返回 0,交易成功,注册完成。

这种思路不可行的缘由在于:

  • Cell 竞争问题,每一个新帐户的注册都须要把这个 GlobalStatusCell 做为输入花费掉,而一个 Live Cell 只能花费一次,那意味着同一时刻,永远只能处理一个注册请求。竞争 Cell 失败的用户,不得不一遍又一遍的签署交易,直到成功的竞争到 Cell。
  • 空间成本问题,CKB 是分层架构,Layer 1 上最终的状态空间限制为大约 80 GB,在上面存储数据须要使用 CKB 购买存储空间。假设最终有 100w 个 DAS 帐户被注册,那这个 GlobalStatusCell 须要的 capacity 将会巨大无比。固然,因为这个存储空间是随着注册量逐渐增长的,对于单个用户而言,只需为本次注册所对应的增量空间支付 CKB,单个用户的成本还算能够接受。

事实上咱们会发现,「Cell 竞争问题」是在 CKB 上开发应用时,要时刻警戒的问题。它对用户体验的影响多是致命的。

方案二:那就把全部帐户分散到多个 Cell 里

既然一个 GlobalStatusCell 放全部帐户会致使竞争,那咱们把帐户分散到多个帐户呢?好比,对帐户名作 hash,将全部 hash 值前 3 位相同的已注册帐户放到同一个 SubStatusCell 里。当一个新的注册产生时,必须将对应的 SubStatusCell 消费,以修改其内部数据。

这个方案仍存在一些问题:

  • 依然存在必定的 Cell 竞争,若是按 hash 前3位来建立 SubStatusCell,须要提早建立 4096 个 SubStatusCell,假定在一个周期内有 50 个并发的注册请求,按照「抽屉原理」,仍有 26% 的几率出现 Cell 竞争。尽管 50 的并发请求稍显苛刻,在早期可能根本达不到,但应该认识到:

    • 因为 SubStatusCell 数量固定,这种竞争的几率,不管在哪一个阶段都是同样的 「几率」自己意味着不肯定性,它的用户体验的影响可能没有,也可能很是大。
  • 初始化时存在费用成本,假定一个 SubStatusCell 初始时只须要 100 CKB 做为其 capacity,那初始化全部的 SubStatusCell 就须要 409,600 个 CKB。

再次强调:在 CKB 上开发应用时,应该时刻关注你的应用会占用多少 CKB 存储空间,由于总的状态空间是极其有限的。

方案三:由 DAS 官方来判断一个帐户是否已经注册过

全部的注册都要经过 DAS 官方的服务进行,DAS 官方断定可注册后,用官方私钥签名一笔交易,向用户发放 DAS 帐户 Cell。这个方案在实现上很是简单,但问题也很明显:

  • 不去中心化,如何确保 DAS 官方服务的惟一性判断是正确的。若是官方主动做恶呢?若是官方由于程序故障或者私钥保管不善,致使被动做恶呢?
  • 脏数据问题,不管什么形式的做恶,中心化的判断都是一种链下判断,不能绝对有效的保证惟一性,所以,随时可能产生链上脏数据。如何清理这些脏数据呢?势必要引入一套脏数据清理的机制。
  • 衍生而来的可用性问题,若是官方服务宕机,那整个注册服务就不可用。

方案四:那就多中心化,用多个链下节点一块儿判断一个帐户是否已经注册过

好比,找 7 个「能够信任」的组织做为超级节点,管理各自的私钥。超级节点们运行超级节点服务程序,将全部已注册的帐户存储在本身的中心化数据库中,当一个注册请求产生时(指用户构造一个包含注册信息的 Cell),各个超级节点将判断其是否已经注册过。若是未注册过,那就用私钥签名一笔交易,释放一个代表「本超级节点认为这个帐户能够注册」的 Cell,当有 4 个以上的超级节点都释放了这样的 Cell 时,其中一个节点就会汇聚全部的这些 Cell,做为依据去建立 DAS 帐户。

这种思路,看似能够很好的解决方案三中的一些问题,但却引入了更多的问题:

  • 信任问题,「能够信任」的组织,怎么样算能够信任的组织,咱们应该如何甄选这 7 个节点。一个组织的道德或许是能够信任的,并不表明其行为也是能够信任的。咱们能够找最有公信力的组织来作节点,但在一个项目的早期,最有公信力的组织很难有动力来维护节点。
  • 脏数据问题,因为「必然」存在的程序 bug,这些超级节点们彻底可能作出一致性的错误判断。当一致性的错误出现时,还得有一套脏数据清理逻辑机制
  • 节点轮换问题,因为私钥丢失或者其余缘由,节点不可避免的要进行轮换,轮换如何进行?经过链下商量仍是链上共识?链下商量意味着得有一套公开透明的治理流程;链上共识,意味着要有的复杂的工程实现。
  • 复杂度,这既包括工程的复杂度,也包括治理的复杂度。其中的大量工做都已经偏离了一个 dApp 自己的业务逻辑。试想,若是每一个应用开发者都须要考虑这么多与业务逻辑关联度并不高的问题,那应用不可能被高效的创造出来。这也意味着,多中心的方案必然不能是最佳实践。

方案五:注册时不去重,解析时去重

既然要实现注册时去重这么复杂,那干脆注册时就不去重了。任何人在任什么时候候均可以「注册」任何帐户,而后在用户要查询一个帐户的解析记录时,由解析程序去找出那个最先「注册」的帐户,将其做为合法的帐户返回给用户。

这种独特的思路,存在的问题主要是如何保证客户端运行「合理」的解析程序:

  • 开发者会都运行统一的解析程序吗?
  • 当官方解析程序升级时,那些选择运行官方解析程序的开发者,他们会不会,以及能不能作到及时升级?

若是不能保证你们始终运行相同的最新的解析程序,整个系统势必会在应用层面上不一致。由此会引起各类形式的欺诈,最终你们会对这个系统失去信心。

方案六:有序链表

最后,咱们来介绍 DAS 最终所采用的方案 —— 有序链表。

咱们将咱们要解决的问题,作更通常化的表述:

对于分散存储的数据集,在插入数据时,如何保证每条数据的惟一性?

答案是,使用逻辑上的有序链表。感谢 @guiqing 的启发。

每一个已注册的 DAS 帐户,都有一个 Cell 用来存储其相关的信息,称为 AccountCell。咱们要求全部的 AccountCell 按某种顺序排序,好比按帐户名作字典序升序。当要注册一个新的 DAS 帐户时,其 AccountCell 必须插入到合适的位置,以保证不破坏这种顺序。

AccountCell 的简化结构以下:
在这里插入图片描述

注意:account_id 取值为帐户名,仅仅是为了表述方便,实际上 DAS 使用的是其帐户名 hash 的前 10 位。

咱们假定链上已经有 a.bit,b.bit,如今一个用户要注册 d.bit,注册前链表结构以下:
在这里插入图片描述
注册后的链表结构以下:
在这里插入图片描述
随后,有一个用户要注册 c.bit,那么注册后的链表结构以下:
在这里插入图片描述
从上面咱们能够看到,当须要注册一个新帐户时,须要对链表中处于其前方的AccountCell 的 next_account_id 字段进行修改。这也意味着,须要构造一笔交易,能消费掉其前方的 Cell,并建立相对应的新 Cell。对于应该修改哪一个 Cell,也即新的 DAS 帐户应该插入在链表的哪一个位置,这些都是由用户使用的注册程序根据链上的状态,自动帮用户完成的(看,链下计算)。

那若是注册程序不当心(或者用户恶意的)构造一笔交易,试图建立重复帐户,或者将帐户插入错误的位置,会怎么样呢。这时候咱们的 type 脚本就起做用了,会致使这类交易失败不被打包进区块(看,链上验证)。

Cell 的 type 脚本会在 Cell 做为输入和输出时都运行。咱们的 type 脚本就能够作一些判断,好比:

  • inputs 中,引入的父 AccountCell 的 account_id 是否小于新注册的帐户的 account_id
  • inputs 中,引入的父 AccountCell 的 next_account_id 是否大于新注册的帐户的 account_id
  • outputs 中,新的父 AccountCell 的 next_account_id 是否等于新注册帐户的 account_id
  • outputs 中,新注册帐户的 next_account_id 是否等于 inputs 中引入的父 AccountCell 的 next_account_id

因此上述的这些判断结果若是都为真,且整个交易结构也知足其余一些必要的条件,那么 type 脚本就会返回 0 ,意味着这是一笔合法的交易,当这笔交易归入区块以后,帐户也就注册完整了,DAS 系统的状态完成了更新。而对于不知足这些条件的交易,根本就是不合法的交易,也就不会注册成功。

能够看到,这个方案知足咱们前面设定的 4 个设计原则。

进一步衍生

判断个数据重复性而已,在 CKB 上就要这么复杂吗?

咱们要理解,之因此「复杂」,其背后的本质缘由是 UTXO 模型,是 UTXO 模型致使了数据的分散存储。

那为何 CKB 要采用 UTXO 模型,ETH 的帐户模型不就很好吗?

UTXO 模型和帐户模型各有优劣,UTXO 模型的部分优点在于:

  • 并行计算。ETH 的单个帐户下的全部交易都必须串行,一笔交易卡住,后面全部的交易都没法进行。
  • 用户数据存储在用户本身的 UTXO(Cell)里,而不是集中存储在合约中,不更符合去中心化精神吗?

咱们更应理解,感觉上的「复杂」,更多的来自于咱们对新范式的不适应。

应把链上验证看做一种协议

能够看到,type 脚本的约束,更像是一种协议。他规定了一笔交易应该有什么样的输入和输出,但谁来建立交易,以什么方式建立交易,并非协议所关心的问题。

方案六也有 Cell 竞争问题呀?

是的,若是多个新的注册的帐户都应该直接插入到某个 AccountCell 的后面,那就会面临 Cell 竞争的问题。因此,咱们将在下一篇文章中介绍,如何经过一种咱们称做 「Keeper」的机制,在方案六的基础上,完全解决 Cell 竞争问题。

最后,如咱们在开头提到的那样:

在面对一个问题时,咱们采用的思路和解决方案,不必定是最优解,甚至大几率不是。但这些知足咱们场景的思路和解决方案,若能给你们带来启发,目的便已达到。

未完待续……

在下一篇文章中,Tim 将为咱们介绍一种称为「Keeper」的机制,来处理 Cell 竞争问题。欢迎你们前往 https://talk.nervos.org/t/das-ckb-das/5669 催更。

如若您有更多关于 DAS 产品的使用心得,以及在 CKB 上开发的看法,欢迎前往 Nervos Talk 论坛讨论:
https://talk.nervos.org/

在这里插入图片描述

相关文章
相关标签/搜索