一个绕不开的尴尬

当身份不再由单一机构背书------用 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 关注

相关推荐
深圳老胡2 天前
STM32CubeMX 生成 CMake 工程,用 VSCode 编译与调试的完整流程
笔记·stm32·单片机·代码规范
星星泡饭吃2 天前
【信息安全】越权防护设计评审指南
代码规范
GlobalSign数字证书2 天前
申请 EV 代码签名证书需要准备哪些材料?
代码规范
深圳老胡2 天前
STM32F4 OTA 升级双 App 方案设计与实现
笔记·stm32·单片机·代码规范
深圳老胡2 天前
STM32F4 OTA升级:单App与双App方案优缺点对比
笔记·stm32·单片机·代码规范
这个DBA有点耶3 天前
Change Buffer深入:二级索引写入的隐形加速器与它的代价
数据库·mysql·代码规范
知守观4 天前
一个半天需求干了三天:代码腐化的五个信号与自查命令
java·后端·代码规范
深圳老胡4 天前
STM32 MCU 国产替代型号简介
笔记·stm32·单片机·代码规范
深圳老胡4 天前
STM32 在 VSCode 下 -O0 与 -Os 编译条件的区别
笔记·stm32·单片机·代码规范
liangsheng_g6 天前
SpringAOP两套代理创建路线设计哲学与开源实践
spring·开源·代码规范