Spritely 的去中心化应用架构:能力安全、Actor 通信与本地优先数据

2026-09-25 28 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:12 分钟

很多所谓的“去中心化应用”,只是把中心数据库换成了另一种共享基础设施,身份、权限和命名仍由中心服务控制。Spritely 试图从更底层重新设计这套模型:用 Goblins 表达基于能力的权限,以 OCapN 连接跨网络 Actor,用 petname 管理人类可读的本地名称,再结合 Scheme、Hoot/WebAssembly 与 CRDT 构建无需中央服务器也能工作的应用。

这套架构真正值得关注的地方,不是某个单独组件,而是它把“谁能做什么”“对象如何通信”“用户如何识别对象”以及“离线数据如何合并”放进了同一个设计框架。

权限不是角色表,而是可传递的对象引用

传统后端通常先确认用户身份,再查询角色或 ACL:

用户 -> 登录服务 -> 身份令牌 -> 权限数据库 -> 目标资源

能力安全采用不同的思路:持有某个不可伪造的对象引用,本身就代表拥有相应权限。程序不必把全局用户身份附加到每一次操作上,而是直接向能力所指向的对象发送消息。

Goblins 将这种对象能力模型与 Actor 风格的通信结合起来。每个 Actor 管理自己的状态,通过消息与其他对象交互。应用可以只分发一个受限能力,例如只读文档、只能存入但不能取出的投递箱,或者只能消费固定额度的支付能力。

这种模型有几个直接收益:

  • 最小权限更自然:组件只拿到完成任务所需的引用。
  • 授权可以委托:持有者能够把完整能力或衰减后的能力交给另一个参与者。
  • 隔离边界更清晰:对象不能因为知道某个全局名称就自动访问对应资源。
  • 审计更贴近业务:可以记录哪项能力收到了什么消息,而不只是记录哪个用户调用了接口。

不过,能力引用不是“自动解决一切安全问题”的魔法。应用仍需设计撤销、过期、持久化、设备丢失后的恢复,以及能力被意外转发时的处理策略。常见办法包括代理能力、可撤销中间层、短期授权和权限衰减,而不是把永久主能力直接交给所有客户端。

OCapN、petname 与 Hoot 如何组成应用栈

单进程内的能力对象并不难实现,真正棘手的是让它跨进程、跨设备和跨网络工作。OCapN 面向的正是对象能力网络通信:一端持有的远程对象引用,可以成为向另一端 Actor 发送消息的入口,同时保留能力模型的授权语义。

这与普通 RPC 的差异主要体现在抽象层:RPC 经常围绕“服务名、方法名、用户令牌”设计;OCapN 更强调“你当前持有什么对象引用,以及可以向它发送哪些消息”。远程对象不应因为接入网络就暴露成一个所有人都能枚举和调用的公共端点。

petname 系统则处理另一个容易被忽视的问题:人类需要名称,但全局唯一名称往往意味着中央注册机构。Petname 是用户或应用在本地为对象设置的名字。同一个对象在 Alice 的设备上可以叫“家庭相册”,在 Bob 的设备上可以叫“Alice 分享的照片”,无需争夺全局命名空间。

这几个层次可以这样理解:

用户看到的本地名称(petname)
            ↓
持有的对象能力引用
            ↓
Goblins Actor 与消息交互
            ↓
OCapN 跨网络传递对象消息
            ↓
Scheme 应用逻辑 / Hoot WebAssembly 运行环境
            ↓
本地状态与 CRDT 同步

Hoot 和 WebAssembly 让 Scheme 编写的应用逻辑能够进入更广泛的运行环境,包括浏览器一类的沙箱平台。与此同时,本地优先 CRDT 允许设备先修改本地数据,稍后再与其他副本合并。两者解决的问题不同:WebAssembly关注代码在哪里运行,CRDT 关注多个副本如何收敛;能力系统则决定谁有资格读取、修改或同步这些数据。

特别要注意,CRDT 的“最终收敛”不等于“所有业务规则都会自动正确”。余额不能为负、用户名必须唯一、审批只能执行一次等强约束,通常还需要额外的协调协议或不同的数据模型。

用一个可运行示例理解能力衰减与离线合并

下面的代码不是 Goblins 或 OCapN 的真实 API,而是一个可以直接运行的概念模型。它演示两件事:所有者如何把完整文档能力衰减成只读能力,以及两个离线副本如何通过一个简单的 grow-only set CRDT 合并标签。

将下面命令复制到支持 Python 3.10 以上版本的终端中:

cat > spritely_concepts.py <<'PY'
from dataclasses import dataclass, field


def make_document(initial_text: str):
    state = {"text": initial_text}

    def reader(message: str):
        if message != "read":
            raise PermissionError("This capability only permits reading")
        return state["text"]

    def owner(message: str, payload=None):
        if message == "read":
            return state["text"]
        if message == "write":
            state["text"] = str(payload)
            return "updated"
        if message == "share-readonly":
            return reader
        raise ValueError(f"Unknown message: {message}")

    return owner


@dataclass
class TagReplica:
    # 一个最小化的 grow-only set:适合演示合并,但不支持删除。
    values: set[str] = field(default_factory=set)

    def add(self, value: str) -> None:
        self.values.add(value)

    def merge(self, other: "TagReplica") -> None:
        self.values |= other.values


# Alice 持有完整能力。
alice_document = make_document("draft")
alice_document("write", "peer-to-peer notes")

# Alice 只把衰减后的只读能力交给 Bob。
bob_document = alice_document("share-readonly")
print("Bob reads:", bob_document("read"))

try:
    bob_document("write")
except PermissionError as error:
    print("Bob cannot write:", error)

# 两台设备离线添加不同标签。
laptop = TagReplica({"scheme"})
phone = TagReplica({"local-first"})

laptop.add("capabilities")
phone.add("crdt")

# 重新连接后交换并合并状态。
laptop.merge(phone)
phone.merge(laptop)

print("Laptop tags:", sorted(laptop.values))
print("Phone tags:", sorted(phone.values))
assert laptop.values == phone.values
PY

python3 spritely_concepts.py

这里的 alice_document 类似完整能力,bob_document 则是通过包装器衰减后的只读能力。Bob 不需要携带全局用户 ID,也不需要让文档对象查询中央 ACL;他能执行什么操作,由拿到的对象引用决定。

TagReplica 只是最简单的 CRDT 示意:集合合并满足交换律、结合律和幂等性,因此副本交换顺序不同也会得到相同结果。它无法删除元素,生产系统需要选择与业务语义匹配的 CRDT,并同时处理存储膨胀、垃圾回收、恶意副本和访问控制。

如果要把这个概念模型改造成真实应用,可以将本地闭包替换为 Goblins Actor,将跨设备消息交给 OCapN,并让每个参与者只保存自己实际获得的能力引用。不要把一个“万能管理对象”暴露给所有节点,否则系统即使采用了能力框架,最终仍会退化成全局超级权限模型。

采用这类架构前要检查什么

Spritely 更适合从协作边界明确的小功能开始验证,而不是立即重写整个后端。共享文档、个人媒体库、离线消息、多人白板和点对点工具,通常比强依赖全局事务的财务核心系统更适合作为试点。

落地前可以逐项检查:

  • 是否能列出每个组件真正需要持有的最小能力?
  • 完整能力能否衰减成只读、限额、限时或单用途能力?
  • 能力泄露或设备丢失后,是否存在撤销和恢复路径?
  • Petname 的初次引入是否可信,用户能否识别名称背后的对象?
  • CRDT 是否真的符合业务语义,还是只解决了数据结构层面的合并?
  • 离线副本重新连接时,如何验证消息来源并拒绝越权操作?
  • Hoot/WebAssembly 的运行环境是否提供应用需要的网络、存储和调试能力?

Spritely 展示的核心方向,是让去中心化不再只等同于“没有中央数据库”。当权限随对象引用流动、通信围绕 Actor 展开、名称由用户在本地管理、数据可以离线演化时,应用才真正减少了对中央身份、中央命名和中央在线状态的依赖。代价则是开发者必须更认真地设计能力生命周期、冲突语义与恢复机制——这些工作不会消失,只是从中心服务器转移到了更明确的协议边界上。


相关推荐