一个绕不开的尴尬

当身份不再由单一机构背书------用 Protocol 契约设计可演进的 DID 基础设施

本文是 GEID 开源项目技术博客第一篇。项目仓库:fumiguo/geid-monorepo

一个绕不开的尴尬

你有没有遇到过这种事:用微信登录某个 App,授权了一堆信息,过后却不知道怎么收回;或者公司账号离职即失效,你在那个平台积累的所有数据一夜归零。

这不是某个产品做得差,是整个身份体系的底层设计问题------你的身份不是你的,是发给你身份的那个机构的。机构说你有,你就有;机构说停,你就停。

AI 时代这个问题会更尖锐。当 Agent 代表你去做事、去交易、去跨系统协作,它需要一个能跨域、可审计、你不高兴就能收回的身份凭证。传统机构背书的模式撑不住这三个需求。

W3C DID 标准很好,但落地各有各的"耦合病"

W3C 在 2022 年发布了 DID 标准规范,方向是对的:去中心化、自主权、可验证。但落地实现时,大家各干各的:

  • 有的绑死在某条链上(换链等于重写)
  • 有的把身份逻辑和业务逻辑揉在一起(改一个动全身)
  • 有的把"发证"和"验证"写死在同一个服务里(没法独立扩展)

问题不在标准,在于实现时没有把契约实现分开。标准告诉你"身份应该长什么样",但没告诉你"代码层面怎么解耦才不会烂"。

我们的方案:两个 Protocol,构造函数注入,实现可热替换

在 GEID 开源项目里,我们用 Python 的 Protocol(PEP 544)做了这件事------把身份系统的契约和实现彻底分开,用构造函数注入把它们连起来

第一步:定义契约,不是定义类

python 复制代码
# geid-core/geid_core/interfaces.py
from typing import Protocol, runtime_checkable

@runtime_checkable
class IBlockchain(Protocol):
    """链上操作。可替换:mock / 真实链 / RPC 客户端。"""
    def generate_wallet(self) -> str: ...
    def mint_tokens(self, to_address: str, amount: int) -> bool: ...
    def burn_tokens(self, from_address: str, amount: int) -> bool: ...
    def freeze_account(self, address: str) -> bool: ...
    def apply_sanction(self, address: str, sanctioned: bool = True) -> bool: ...

@runtime_checkable
class IIdentity(Protocol):
    """身份注册、查询、验证。"""
    async def register_entity(self, data: EntityCreate) -> dict: ...
    def query_entity(self, geid: str) -> dict: ...
    async def trigger_verification(self, geid: str) -> dict: ...

注意三个细节:

  1. Protocol结构性子类型------任何实现了这些方法的对象都算"符合契约",不需要继承
  2. @runtime_checkable 让你能用 isinstance(obj, IBlockchain) 做运行时检查
  3. 契约里只有方法签名,没有任何实现,也没有任何业务依赖

第二步:实现可以换,调用方不关心

python 复制代码
# geid-identity/geid_identity/services/blockchain.py
class BlockchainService:
    """实现 IBlockchain。换成真实链客户端,调用方一行不改。"""

    def generate_wallet(self) -> str:
        return Account.create().address

    def mint_tokens(self, to_address: str, amount: int) -> bool:
        logger.info("mint %s -> %s", amount, to_address)
        return True

    def burn_tokens(self, from_address: str, amount: int) -> bool:
        logger.info("burn %s <- %s", amount, from_address)
        return True

    def freeze_account(self, address: str) -> bool:
        logger.info("freeze %s", address)
        return True

    def apply_sanction(self, address: str, sanctioned: bool = True) -> bool:
        logger.info("sanction %s = %s", address, sanctioned)
        return True

现在是 mock(用 eth_account 生成钱包地址,mint 只记日志)。明天要接真实链,写一个 ChainRPCClient 实现同样的方法,业务代码一行都不用改。

第三步:构造函数注入,不是 import

这是最关键的一步。看注册服务怎么用链:

python 复制代码
# geid-identity/geid_identity/services/registration.py
class RegistrationService:
    """实现 IIdentity.register_entity / query_entity。"""

    def __init__(self, chain=None):
        self._chain = chain or BlockchainService()  # 注入,不是 import

    async def register_entity(self, data: EntityCreate) -> dict:
        if not await self._verify_kyc(data.kyc_token):
            raise HTTPException(403, "KYC verification failed")

        geid = generate_geid(data.region, data.sub_class,
                             data.generation, data.sequence)
        wallet = self._chain.generate_wallet()       # 调接口,不调实现
        tokens = calculate_initial_tokens(data.entity_type.value,
                                          data.tax_amount, data.population)
        self._chain.mint_tokens(wallet, tokens)

        db = get_db()
        db.execute(
            "INSERT INTO entities (geid, entity_type, name, wallet_address, "
            "token_balance, kyc_verified) VALUES (?,?,?,?,?,1)",
            (geid, data.entity_type.value, data.name, wallet, tokens),
        )
        db.commit()
        return {"geid": geid, "wallet": wallet, "tokens": tokens}

关键在 def __init__(self, chain=None)

  • 不写 from blockchain import BlockchainService 然后在方法里 BlockchainService().generate_wallet()
  • 而是 把 chain 作为参数传进来,默认给一个 mock

这一步决定了整个系统的生死。因为 chain 是注入的:

场景 注入什么 业务代码改不改
单体运行 本地 BlockchainService 不改
微服务化 ChainHTTPClient(实现 IBlockchain,内部走 HTTP) 不改
测试 mock 对象,不碰任何真实链 不改

三种部署模式自由切换,业务代码一行不动。 这不是"未来支持",是现在就已经这么写的。

附带:一个带校验位的全局编号

身份得有个唯一编号。我们设计了一个带校验位的方案:

python 复制代码
# geid-identity/geid_identity/utils.py
def generate_geid(region, sub_class, generation, sequence) -> str:
    base = region + sub_class + generation + sequence
    return f"{base}-{generate_check_digit(base)}"

def generate_check_digit(base_id: str) -> str:
    digits = [_char_to_digit(c) for c in base_id]
    weights = list(range(1, len(digits) + 1))
    total = sum(d * w for d, w in zip(digits, weights))
    r = total % 11
    return "X" if r == 10 else str(r)

例如 generate_geid("CN", "A", "01", "0001")CNA010001-4。末尾的 4 是校验位,用加权模 11 算出来的,输入错一位校验位就对不上,防止手滑录错。这个思路跟身份证号最后一位、ISBN 书号校验位是同一类设计。

这东西能用在哪

  • 身份中间件:嵌进既有业务系统,提供注册/验证 API,底层链可替换
  • 多链身份网关:同一套身份逻辑,按租户切不同的链
  • Agent 身份层:给自主决策的 Agent 发可验证凭证,决策行为可审计、可追溯
  • 政务/企业数字身份:合规要求高的场景,链实现可替换成联盟链或政务云

核心卖点只有一个:今天用 mock 跑通,明天接真实链不用改业务代码。 这不是画饼,是已经写好的代码。

为什么用 Protocol 而不是 ABC

有人会问:用抽象基类(ABC)也能解耦,为什么选 Protocol?

  • ABC 是** nominal subtyping**(名义子类型)------必须显式继承,改一个基类牵一发动全身
  • Protocol 是 structural subtyping(结构子类型)------鸭子类型带类型检查,第三方库的类不用改源码也能符合契约

在身份基础设施这种"实现可能来自不同团队、不同链"的场景里,结构子类型意味着我不强迫你继承我的类,你只要方法签名对得上,就算符合我的契约。这对开源项目尤其重要------别人集成你的时候,门槛低很多。

开源了,欢迎来看

完整代码在 GitHub:fumiguo/geid-monorepo

  • geid-core/geid_core/interfaces.py --- 五个 Protocol 契约(IBlockchain / IIdentity / IBalance / IGovBridge / IAgent)
  • geid-identity/ --- DID 身份服务实现
  • geid-balance/ --- 可持续性评估引擎
  • geid-protocol/ --- Agent 治理协议

MIT 协议,欢迎提 Issue、提 PR。如果你也在做身份基础设施或者 Agent 治理,想聊聊怎么把契约设计做得更好------Issue 区见。


作者:fumiguo · 项目持续更新中,欢迎 Star 关注

当身份不再由单一机构背书------用 Protocol 契约设计可演进的 DID 基础设施

本文是 GEID 开源项目技术博客第一篇。项目仓库:fumiguo/geid-monorepo

一个绕不开的尴尬

你有没有遇到过这种事:用微信登录某个 App,授权了一堆信息,过后却不知道怎么收回;或者公司账号离职即失效,你在那个平台积累的所有数据一夜归零。

这不是某个产品做得差,是整个身份体系的底层设计问题------你的身份不是你的,是发给你身份的那个机构的。机构说你有,你就有;机构说停,你就停。

AI 时代这个问题会更尖锐。当 Agent 代表你去做事、去交易、去跨系统协作,它需要一个能跨域、可审计、你不高兴就能收回的身份凭证。传统机构背书的模式撑不住这三个需求。

W3C DID 标准很好,但落地各有各的"耦合病"

W3C 在 2022 年发布了 DID 标准规范,方向是对的:去中心化、自主权、可验证。但落地实现时,大家各干各的:

  • 有的绑死在某条链上(换链等于重写)
  • 有的把身份逻辑和业务逻辑揉在一起(改一个动全身)
  • 有的把"发证"和"验证"写死在同一个服务里(没法独立扩展)

问题不在标准,在于实现时没有把契约实现分开。标准告诉你"身份应该长什么样",但没告诉你"代码层面怎么解耦才不会烂"。

我们的方案:两个 Protocol,构造函数注入,实现可热替换

在 GEID 开源项目里,我们用 Python 的 Protocol(PEP 544)做了这件事------把身份系统的契约和实现彻底分开,用构造函数注入把它们连起来

第一步:定义契约,不是定义类

python 复制代码
# geid-core/geid_core/interfaces.py
from typing import Protocol, runtime_checkable

@runtime_checkable
class IBlockchain(Protocol):
    """链上操作。可替换:mock / 真实链 / RPC 客户端。"""
    def generate_wallet(self) -> str: ...
    def mint_tokens(self, to_address: str, amount: int) -> bool: ...
    def burn_tokens(self, from_address: str, amount: int) -> bool: ...
    def freeze_account(self, address: str) -> bool: ...
    def apply_sanction(self, address: str, sanctioned: bool = True) -> bool: ...

@runtime_checkable
class IIdentity(Protocol):
    """身份注册、查询、验证。"""
    async def register_entity(self, data: EntityCreate) -> dict: ...
    def query_entity(self, geid: str) -> dict: ...
    async def trigger_verification(self, geid: str) -> dict: ...

注意三个细节:

  1. Protocol结构性子类型------任何实现了这些方法的对象都算"符合契约",不需要继承
  2. @runtime_checkable 让你能用 isinstance(obj, IBlockchain) 做运行时检查
  3. 契约里只有方法签名,没有任何实现,也没有任何业务依赖

第二步:实现可以换,调用方不关心

python 复制代码
# geid-identity/geid_identity/services/blockchain.py
class BlockchainService:
    """实现 IBlockchain。换成真实链客户端,调用方一行不改。"""

    def generate_wallet(self) -> str:
        return Account.create().address

    def mint_tokens(self, to_address: str, amount: int) -> bool:
        logger.info("mint %s -> %s", amount, to_address)
        return True

    def burn_tokens(self, from_address: str, amount: int) -> bool:
        logger.info("burn %s <- %s", amount, from_address)
        return True

    def freeze_account(self, address: str) -> bool:
        logger.info("freeze %s", address)
        return True

    def apply_sanction(self, address: str, sanctioned: bool = True) -> bool:
        logger.info("sanction %s = %s", address, sanctioned)
        return True

现在是 mock(用 eth_account 生成钱包地址,mint 只记日志)。明天要接真实链,写一个 ChainRPCClient 实现同样的方法,业务代码一行都不用改。

第三步:构造函数注入,不是 import

这是最关键的一步。看注册服务怎么用链:

python 复制代码
# geid-identity/geid_identity/services/registration.py
class RegistrationService:
    """实现 IIdentity.register_entity / query_entity。"""

    def __init__(self, chain=None):
        self._chain = chain or BlockchainService()  # 注入,不是 import

    async def register_entity(self, data: EntityCreate) -> dict:
        if not await self._verify_kyc(data.kyc_token):
            raise HTTPException(403, "KYC verification failed")

        geid = generate_geid(data.region, data.sub_class,
                             data.generation, data.sequence)
        wallet = self._chain.generate_wallet()       # 调接口,不调实现
        tokens = calculate_initial_tokens(data.entity_type.value,
                                          data.tax_amount, data.population)
        self._chain.mint_tokens(wallet, tokens)

        db = get_db()
        db.execute(
            "INSERT INTO entities (geid, entity_type, name, wallet_address, "
            "token_balance, kyc_verified) VALUES (?,?,?,?,?,1)",
            (geid, data.entity_type.value, data.name, wallet, tokens),
        )
        db.commit()
        return {"geid": geid, "wallet": wallet, "tokens": tokens}

关键在 def __init__(self, chain=None)

  • 不写 from blockchain import BlockchainService 然后在方法里 BlockchainService().generate_wallet()
  • 而是 把 chain 作为参数传进来,默认给一个 mock

这一步决定了整个系统的生死。因为 chain 是注入的:

场景 注入什么 业务代码改不改
单体运行 本地 BlockchainService 不改
微服务化 ChainHTTPClient(实现 IBlockchain,内部走 HTTP) 不改
测试 mock 对象,不碰任何真实链 不改

三种部署模式自由切换,业务代码一行不动。 这不是"未来支持",是现在就已经这么写的。

附带:一个带校验位的全局编号

身份得有个唯一编号。我们设计了一个带校验位的方案:

python 复制代码
# geid-identity/geid_identity/utils.py
def generate_geid(region, sub_class, generation, sequence) -> str:
    base = region + sub_class + generation + sequence
    return f"{base}-{generate_check_digit(base)}"

def generate_check_digit(base_id: str) -> str:
    digits = [_char_to_digit(c) for c in base_id]
    weights = list(range(1, len(digits) + 1))
    total = sum(d * w for d, w in zip(digits, weights))
    r = total % 11
    return "X" if r == 10 else str(r)

例如 generate_geid("CN", "A", "01", "0001")CNA010001-4。末尾的 4 是校验位,用加权模 11 算出来的,输入错一位校验位就对不上,防止手滑录错。这个思路跟身份证号最后一位、ISBN 书号校验位是同一类设计。

这东西能用在哪

  • 身份中间件:嵌进既有业务系统,提供注册/验证 API,底层链可替换
  • 多链身份网关:同一套身份逻辑,按租户切不同的链
  • Agent 身份层:给自主决策的 Agent 发可验证凭证,决策行为可审计、可追溯
  • 政务/企业数字身份:合规要求高的场景,链实现可替换成联盟链或政务云

核心卖点只有一个:今天用 mock 跑通,明天接真实链不用改业务代码。 这不是画饼,是已经写好的代码。

为什么用 Protocol 而不是 ABC

有人会问:用抽象基类(ABC)也能解耦,为什么选 Protocol?

  • ABC 是** nominal subtyping**(名义子类型)------必须显式继承,改一个基类牵一发动全身
  • Protocol 是 structural subtyping(结构子类型)------鸭子类型带类型检查,第三方库的类不用改源码也能符合契约

在身份基础设施这种"实现可能来自不同团队、不同链"的场景里,结构子类型意味着我不强迫你继承我的类,你只要方法签名对得上,就算符合我的契约。这对开源项目尤其重要------别人集成你的时候,门槛低很多。

开源了,欢迎来看

完整代码在 GitHub:fumiguo/geid-monorepo

  • geid-core/geid_core/interfaces.py --- 五个 Protocol 契约(IBlockchain / IIdentity / IBalance / IGovBridge / IAgent)
  • geid-identity/ --- DID 身份服务实现
  • geid-balance/ --- 可持续性评估引擎
  • geid-protocol/ --- Agent 治理协议

MIT 协议,欢迎提 Issue、提 PR。如果你也在做身份基础设施或者 Agent 治理,想聊聊怎么把契约设计做得更好------Issue 区见。


作者:fumiguo · 项目持续更新中,欢迎 Star 关注

相关推荐
hoLzwEge5 小时前
代码风格统一:.editorconfig 从入门到精通
代码规范
电子科技圈10 小时前
先进封装、芯粒架构和3D集成——先进异构集成亟需兼具标准化与定制化能力的互联及总线IP解决方案
tcp/ip·设计模式·架构·软件构建·代码规范·设计规范
杨充1 天前
10.可测试性实战设计
设计模式·开源·代码规范
杨充1 天前
9.重构十二式的实战
设计模式·开源·代码规范
杨充1 天前
8.反模式与坏味道
设计模式·开源·代码规范
梦梦代码精1 天前
基于ThinkPHP6 + Vue3的家政预约系统全解析:从LBS定位到自动派单的完整实现
java·docker·开源·php·代码规范
行者全栈架构师1 天前
混元 Hy3 Agent 实战:季度报告 3 小时变 40 分钟
算法·架构·代码规范
饼干哥哥3 天前
n8n 又活了?用 Codex把跨境电商工作流转成 Skill
人工智能·后端·代码规范
梦梦代码精5 天前
多商户商城技术选型实测:ThinkPHP 8与Vue 3结合,B2B2C架构下的开源实践
docker·开源·代码规范