主题
03 · 交易跨包全链路(详细签名 + 验证版)
本章以 机构间 Transfer 为主线,给出从 Bank gRPC 入口到 Committer 落账的每一步代码事实:调用入口、谁签名(用什么 key 签什么数据)、验证什么、落到哪个字段。Issue / Redeem 在最后用差异说明。
只引用源码与运行配置;不引用 cbdc-chain/docs/ 与对外公告。
0. 一笔 Transfer 涉及的角色与密钥
| 角色 | 调用形态 | 关键私钥 | 私钥落点 |
|---|---|---|---|
| Institution(Bank,发起方) | FSC view + Token SDK wallet | wallet recipient identity(X.509,长期身份) | sign-kms partition(BCCSP PKCS#11,cbdc-biz/conf/institution-001/core.yaml:93-98) |
| Institution(收款方) | FSC view(被动响应) | wallet recipient identity | 同上(每家银行各自 partition) |
| Endorser FSC node | FSC view + Token SDK 自动注册的 RequestApprovalResponderView | endorser MSP node identity | sign-kms partition KMS_TK(conf/endorser1/core.yaml:78-92) |
| Auditor | FSC view + Token SDK auditor wallet | auditor wallet identity | sign-kms partition |
| Issuer(Transfer 链路不签;Issue 链路必签) | FSC view + Token SDK issuer wallet | issuer wallet identity | sign-kms partition |
| Orderer (Router) | gRPC mTLS server | TLS server cert | fabric-ca 签发,私钥在节点本地或 KMS(部署方决定) |
| Committer Verifier | 按 namespace 查策略后 verify | 无私钥(只验证) | — |
外部签名(非链上):
| 用途 | 私钥 | 数据 | 出处 |
|---|---|---|---|
| 客户端外部 X.509 签名(Bank API 鉴权) | Bank 外部钱包私钥 | base64(MessageToSign),由 PrepareTransfer 返回 | cbdc-biz/app/institution/internal/service/transfer_service.go:101-123 |
| RTGS MT298 报文签名 | Bank RTGS jks keystore 私钥 | MT298 报文 | cbdc-biz/pkg/rtgs/crypto.go、conf/bizhub/core.yaml:79-112 |
1. 总体步骤一览(Transfer 主线 · App 视角)
participant 是部署节点(app 进程)。整笔 Transfer 共 36 步,分 4 个阶段展示,每阶段一张图;步骤号与 §2–§5 文字一一对应。
1.1 一图骨架(仅数据流向)
1.2 阶段 A · 准备待签消息(步骤 1–9)
1.3 阶段 B · Endorser 代理审计(步骤 10–19)
1.4 阶段 C · 收 Owner 签名 + 链码背书(步骤 20–26)
1.5 阶段 D · Broadcast + Committer 落账(步骤 27–36)
下文 §2–§5 按编号给出每一步的代码位置、签名 / 验证细节。
2. 步骤 1–9:Bank 端入口与待签消息生成
步骤 1 — Client → TransferService.PrepareTransfer
来源:cbdc-biz/app/institution/internal/service/transfer_service.go:24-99。
- 入口:gRPC
cbdc_biz.v1.InstitutionOpsService/PrepareTransfer - 入参(核心):
sender、recipients[](每项含wallet_id+amount)、message、可选use_asset_mark/attach_asset_mark(二者不可同设) - 校验:金额合法、
use_asset_mark与attach_asset_mark互斥(service 层)
不签名、不验签。这是外部 gRPC 入口。
步骤 2–3 — Biz 编排 + 启动 View
- Biz 编排:
cbdc-biz/app/institution/internal/biz/transfer_biz.go:80-167:93-138pre-view(bizhub 权限 / 路由 / pending 检查):141-149viewregistry.GetManager(b.node).InitiateView(prepareView)起 FSC View
步骤 4 — PrepareTransferView.Call
来源:cbdc-biz/pkg/views/transfer.go:267-633。
主要动作:
| 行号段 | 动作 |
|---|---|
:267-337 | 解析 sender wallet + 解析所有 recipient identity(wallet_common.GetIdentityResolver().ResolveIdentity()) |
:339-379 | fee.CalculateAndResolveIdentity() 计算费用 + 费用收集方 |
:387-402 | ttx.NewTransaction(vctx, nil) 创建 Token SDK Transaction,设置 ApplicationMetadata |
:429-562 | tx.Transfer(...) —— 选币(memory 或 distributed token_selector)+ ZK 范围证明承诺;标记转账走 marked.NewAssetMarkSelector() |
:565-571 | senderWallet.GetRecipientIdentity() 得签名身份;tx.TokenRequest.MarshalToSign() 序列化待签消息 |
:572-603 | 构造 PrepareTransferResult{TxID, MessageToSign(base64), SignerIdentity, RecipientResults, FeeInfo};Result.SignerIdentity = base64(原始 signerIdentity)(:596),而 ExtractPEMCertificate 抽取的 cert PEM 写入 PendingTx.SignerIdentity(:625,见步骤 4 下方 PendingTransaction 行) |
:620-632 | 构造 PendingTransaction(持 activeSelector,已选中 token 锁定);committed=true 避免 defer 释放 |
此时尚无任何签名
tx.TokenRequest 内还没有 owner sig / auditor sig / endorser sig;仅有 token graph + ZK 承诺。
步骤 5 — 返回 MessageToSign
PrepareTransferResult.MessageToSign 是 base64(待签字节);客户端外部钱包用 sender 私钥对其 ECDSA 签名。
步骤 6 — 外部钱包签名(链外)
私钥位置:调用方银行自己保管(不在本仓库范围)。
步骤 7–9 — Client → SubmitTransfer → SubmitSignatureView
来源:
cbdc-biz/app/institution/internal/service/transfer_service.go:101-123cbdc-biz/app/institution/internal/biz/transfer_biz.go:239-283cbdc-biz/pkg/views/transfer.go(SubmitSignatureView定义于该文件)
SubmitTransfer 解码 base64 signature → PopPendingTransaction(txID) 取出 prepare 阶段 cache 的 selector + tx 句柄 → 构造 SubmitSignatureView{PendingTx, Signature, FinalityRegistry} → InitiateView(cert PEM 不是独立字段,已包含在 PendingTx.SignerIdentity 内)。
SubmitSignatureView.Call(transfer.go:680-741)的主体只做 3 件事:
| 子步骤 | 行号 | 动作 |
|---|---|---|
| 构造 signer | :693-698 | wallet_common.NewPreSubmittedSignatureSigner(signature, MessageToSign, SignerIdentity) |
| [A] Audit + Endorse | :703-711 | RunView(NewAuditAndEndorseView(tx, ttx.WithExternalWalletSigner(...))) → 进入步骤 10–24 |
| [B] Broadcast | :723-730 | RunView(ttx.NewOrderingView(tx)) → 步骤 27–34 |
| [C] Hand off | :735-737 | FinalityRegistry.Register(tx, Selector) 把 selector 移交 worker 异步等 finality |
步骤 7–9 本身不做任何签名校验
service / biz / view.Call 这三层函数体内没有任何 CheckSignature 调用。验签延迟到 步骤 21,由 Token SDK 调 PreSubmittedSignatureSigner.Sign() 时才触发 —— 详见 步骤 21 § external signer 验签流程。
3. 步骤 10–19:Endorser 代理审计(双签前置)
步骤 10 — AuditAndEndorseView.Call
来源:cbdc-biz/pkg/views/endorsement.go:25-98。
Phase 1 (:35-67) Bank → Endorser 发 tx bytes,等 sigma
Phase 2 (:69-81) Bank 内部 verifyAuditorSignature + AddAuditorSignature
Phase 3 (:83-95) Bank 内部 ttx.NewCollectEndorsementsView(tx, WithSkipAuditing())步骤 11 — Bank FSC P2P → Endorser
endorsement.go:44 ctx.GetSession(v, view.Identity("endorser1")) → 通过 FSC P2P 连接到 endorser1 节点(连接对端由 cbdc-biz/conf/institution-001/core.yaml: fsc.endpoint.resolvers[] 解析)。
只挡 mTLS:FSC P2P 是 gRPC + mTLS,证书由 fabric-ca 签发;TLS 握手层不查 CRL(见 09 § 8)。
步骤 12 — Endorser 端 AuditResponderView.Call
来源:cbdc-biz/pkg/views/audit.go:86-128。
| 阶段 | 行号 | 行为 | 是否校验 |
|---|---|---|---|
ar_recv_tx | :93-100 | 接收 tx bytes(含 trace context) | 仅反序列化完整性 |
ar_audit_call | :108-114 | ctx.RunView(&RequestAuditView{TxRaw: txRaw}) 转发到 Auditor | — |
ar_send_sigma | :121-126 | 发 sigma 回 Bank | — |
Endorser 在此处不验任何业务。它只是 "Bank 与 Auditor 的中继"。
一个常见误解:Endorser 是不是"被动转发"
对审计签名而言:是,纯转发。 对链码背书(步骤 22-24)而言:不是,Endorser 用 Token SDK 自动注册的 RequestApprovalResponderView 跑 9 道闸门后由 Endorser 自己签 proposal endorsement。
步骤 13 — Endorser FSC P2P → Auditor
audit.go:55-58 ctx.GetSession(v, view.Identity("auditor"))。Bank 永不直连 Auditor。
步骤 14 — Auditor 端 AuditView.Call
来源:cbdc-biz/pkg/views/audit.go:171-573。这是整条链路里业务校验最重的一段。
| 阶段 | 行号 | 校验内容 | 失败动作 |
|---|---|---|---|
av_recv_tx | :186-190 | ttx.ReceiveTransaction() | panic(assert.NoError) |
av_gated | :213-259 | workerpool 限并发(CPU-bound ZK + 解密) | 返回 ErrPoolSaturated |
↳ audit.Validate(tx) | :220 | ZK 范围证明验证(zkatdlog 同态承诺) | panic |
| ↳ ApplicationMetadata | :223-230 | issue / meta metadata 字段合法 | panic |
↳ audit.Audit() | :232-242 | 解密 inputs/outputs 明文金额、owner、type | error |
av_match_tx | :284-290 | tx_selector SDK 按 marked 类型匹配规则 | error |
av_prefetch | :308-313 | 并发预查 sender + recipient 余额(max 8 并发) | warn 继续 |
av_rules | :315-505 | 7 条拒绝规则(见 §6) | logRejection + error,标 auditOutcomeRejectedLimit |
av_revocation | :524-555 | inputs/outputs 的 RevocationHandle 不在撤销缓存 | 同上,标 auditOutcomeRejectedRevoke |
av_approve | :560-572 | ttx.NewAuditApproveView(w, tx).Call() 签 sigma 并发回 | error,标 auditOutcomeRejectedValidation |
撤销缓存是 Token SDK 的 RevocationHandle,不是 X.509 CRL
来源:cbdc-biz/pkg/common/revocation/cache.go:1-33。读取 FSC KVS 前缀 revocationList,每 30 秒刷新。与 fabric-ca 签发的 X.509 CRL 是两套机制。
步骤 15 — AuditApproveView.signAndSendBack
来源:fabric-token-sdk/token/services/ttx/auditor.go:374-403。
:377-385 aid = a.w.GetAuditorIdentity()
signer = a.w.GetSigner(ctx, aid)
:386 raw = a.tx.MarshallToAudit() // 待签字节
:392 sigma = signer.Sign(raw) // 走 BCCSP PKCS#11 → sign-kms
:398 session.SendTyped(ctx, &SignaturePayload{Signature: sigma}, TypeSignature)签名落点(在 Bank 侧组装时):TokenRequest.AuditorSignatures[*].Signature(fabric-token-sdk/token/request.go:1113-1118,由 AddAuditorSignature 追加)。
步骤 16–18 — sigma 经 Endorser 回到 Bank
纯网络传输,无校验。
步骤 19 — Bank verifyAuditorSignature
来源:cbdc-biz/pkg/views/endorsement.go:69-81 调本地 verifyAuditorSignature(endorsement.go:102-122,逻辑等价于 SDK fabric-token-sdk/token/services/ttx/auditor.go:287-318,但为本地复刻、非调用)。
逻辑:遍历 TMS PublicParameters 的 Auditors(),逐个用对应 AuditorVerifier 的 Verify(MarshallToAudit, sigma),匹配上的 auditor identity 由 endorsement.go:76 AddAuditorSignature 写入 TokenRequest.AuditorSignatures。
这一步是 Bank 在本地验证 Auditor 的签名,不发到任何节点。
4. 步骤 20–24:Owner 签名 + Endorser 9 道闸门
步骤 20 — CollectEndorsementsView(WithSkipAuditing)
来源:fabric-token-sdk/token/services/ttx/collectendorsements.go:95-160(Call)。
跳过审计(因为已在步骤 12-19 完成),直接进入 owner 签名 + chaincode endorsement 阶段。
步骤 21 — Owner 签名
来源:collectendorsements.go:95-160(Call 调度)+ :200-322(requestSignatures 实际签名分支)。
| 行号段 | 分支 | 签名方式 |
|---|---|---|
:105 | issue signers(requestSignaturesOnIssues,Transfer 路径下空) | — |
:110 | transfer signers(requestSignaturesOnTransfers,全体 sender wallet identity) | 见下 |
:117 | tx.TokenRequest.SetSignatures(mergeSigmas(...)) | 落到 TokenRequest.Signatures[](request.go:1121-1138) |
签名分支(requestSignatures,:200-322,按代码内顺序逐 case 命中):
| 类型 | 行号 | 实际 signer |
|---|---|---|
| MultiSig 身份 | :225-245 | 递归 + multisig.JoinSignatures() |
| Policy 身份(OR/AND/N-of-M) | :247-272 | 递归 + boolpolicy.JoinSignatures() |
外部 signer(wallet 存在且已注册 ExternalWalletSigner,先于本地 signer 探测) | :276-296 | signExternal() → externalWalletSigner.Sign(party, requestRaw)(即 SubmitSignatureView 注入的 PendingTx 中带的 base64 signature) |
本地 signer(GetSigner() 成功) | :298-310 | signLocal() → signer.Sign(requestRaw) —— 走 BCCSP PKCS#11 → sign-kms |
| 远程 signer | :312-320 | signRemote():FSC session → SignatureRequest → 收 sigma |
Bank 实际走"外部 signer"分支
SubmitSignatureView 把 client 在步骤 6 用外部钱包做的签名注入到 ExternalWalletSigner,Token SDK :276-296(signExternal)直接取用而不调 BCCSP 再次签名。这意味着 sender 长期私钥不必在 sign-kms 内(Bank 侧密钥由调用方自行托管)。
外部 signer 分支:PreSubmittedSignatureSigner.Sign 的 4 步本地验签
来源:cbdc-biz/pkg/common/wallet_common/pre_submitted_signature_signer.go:71-127。
这是 Token SDK 调 externalWalletSigner.Sign(party, message) 时实际执行的代码 —— 它不真去签(签名已在步骤 6 由调用方做好),只做自验然后把签名以 ASN.1 DER 形式回传给 Token SDK:
| 子步骤 | 行 | 行为 |
|---|---|---|
| 1. PEM 解 cert | :81-86 | pem.Decode(s.signerCertificate),失败直接 fmt.Errorf("failed to decode stored certificate PEM") |
| 2. 解析 X.509 | :87-90 | x509.ParseCertificate(block.Bytes) |
| 3. 签名格式转换 | :107-115 | convertIEEEP1363ToASN1DER —— Web Crypto API 的 r‖s 64 字节转 ASN.1 DER;同时强制 Low-S(:48-54,防 ECDSA 延展性)。非 64 字节直接原样透传(:38-40) |
| 4. 验签 | :118-121 | cert.CheckSignature(cert.SignatureAlgorithm, message, derSignature);失败 → fmt.Errorf("X.509 signature verification failed: %w", err) |
错误传播链:
PreSubmittedSignatureSigner.Sign → error
└─ CollectEndorsementsView.requestSignatures → signExternal (collectendorsements.go:276-296)
└─ AuditAndEndorseView.Phase3 (endorsement.go:84-95)
└─ SubmitSignatureView.Call:710 "failed to collect endorsements"
└─ biz releasePendingTx (UnlockAll selector)
└─ service 返回 5xx,整笔 tx 终止消息一致性是"软告警",不阻断
:97-102 当 message != s.messageToSign(Token SDK 现在传入的字节 ≠ prepare 阶段返回给客户端的 base64 解码字节)只 logger.Warnf,仍用 Token SDK 当前传入的 message 完成验签(:118 第二参数)。也就是说"client 在步骤 6 究竟签了哪段数据"由代码运行时实际传入决定,与 prepare 阶段返回的 MessageToSign 不强绑定。
这是 Bank 自验自,不是独立第三方
私钥在调用方银行手里、cert 是它自己回传的(PendingTx.SignerIdentity)、验签在同一 Bank 节点跑。意义只在 fail-fast —— 避免把烂签名扔到 Auditor / Endorser / Committer 再被拒。owner sig 首次被独立第三方验证仍发生在 步骤 14(Auditor)。
仅支持 ECDSA P-256
convertIEEEP1363ToASN1DER:48 写死 elliptic.P256(),且 64 字节才进 r/s 拆分分支。其它曲线 / 算法走 :38-40 早返回,是否能过最终的 cert.CheckSignature 取决于 cert 自带的 SignatureAlgorithm 与原始签名格式是否吻合。
步骤 22 — requestApproval
来源:fabric-token-sdk/token/services/ttx/collectendorsements.go:396-417(requestApproval)。
go
requestRaw, _ := c.tx.TokenRequest.RequestToBytes()
env, err := network.GetInstance(ctx, c.tx.Network(), c.tx.Channel()).
RequestApproval(ctx, c.tx.TokenRequest.TokenService, requestRaw,
c.tx.Signer, c.tx.TxID, c.Opts.ApprovalMetadata)
c.tx.Envelope = env实际通过 FSC P2P 连接 endorser1 节点的 RequestApprovalView。
步骤 23 — Endorser 端 9 道闸门
Endorser 在这条路径上是真签名者,不是转发器
触发自动注册的条件:cbdc-biz/conf/endorser1/core.yaml:142-143 fsc_endorsement.endorser: true。
注册位置:fabric-token-sdk/token/services/network/fabric/endorsement/fsc/service.go:56-73,当 AmIAnEndorserKey(= services.network.fabric.fsc_endorsement.endorser)为 true 时 Token SDK 自动注册 RequestApprovalResponderView。
入口:fabric-token-sdk/token/services/network/fabric/endorsement/fsc/responder.go:72-106(Call)。
9 个连续校验点:
| # | 闸门 | 代码 | 检查内容 | 失败错误 |
|---|---|---|---|---|
| 1 | 接收 + transient 字段数 | receive():108-123 | TX bytes + transient = 2 或 3(tmsID + token_request ± approval_metadata) | ErrReceivedProposal / ErrInvalidTransient |
| 2 | TMSID 解析 | receive():125-138 | TMSID 各字段非空 + TMS 存在 | 同上 |
| 3 | RWS 完整性 | receive():159-173 | proposal RWS 的 namespaces 为空(防止 RWS 注入) | ErrInvalidProposal |
| 4 | 链码字段 | receive():175-184 | 名 = TMSID.Namespace,版本 "1.0",函数 "invoke",无参数 | 同上 |
| 5 | Proposal 完整性 | validateProposal():206-233 | creator/proposalBytes/signature/header/payload 均非空 | ErrValidateProposal |
| 6 | Creator MSP 合法性 | validateProposal():242-251 | mspManager.IsValid(creator) + acl.CheckACL() | 同上 |
| 7 | Proposal 签名验证 | validateProposal():254-262 | verifier.Verify(proposalBytes, signature) | 同上 |
| 8 | Token 业务校验 | validate():293-329 | validator.UnmarshallAndVerifyWithMetadata():ZK 范围证明、owner 签名、auditor σ、metadata 一致 | 同上 |
| 9 | RWS 翻译 + endorse | endorse():332-355 | 把每个 token action 翻译进 RWS(隐式检测 key 冲突);然后 endorserService.Endorse(ctx, tx, endorserID) 用 endorser FSC node 私钥签 proposal endorsement | ErrEndorseProposal |
步骤 24 — proposal endorsement 写入 envelope
responder.go:348 的 endorse() 用 endorser FSC node 私钥(PKCS#11 → sign-kms KMS_TK partition)对 Fabric proposal hash 签名,写入 tx.Envelope.Endorsements[] 标准字段。
至此 envelope 内含三类签名:
TokenRequest.Signatures[] ← sender (owner) 步骤 21
TokenRequest.AuditorSignatures[*] ← auditor 步骤 19
Envelope.Endorsements[] ← endorser FSC node 步骤 245. 步骤 25–35:分发收方 + Orderer + Committer
步骤 25–26 — 分发 + 收款方 finality
来源:fabric-token-sdk/token/services/ttx/collectendorsements.go:141(Call 内调 distributeTxToParties,函数定义 :469)。
收款方 AcceptTransferView.Call(cbdc-biz/pkg/views/accept_transfer.go:37-72):
| 行 | 行为 |
|---|---|
:43 | RespondRequestRecipientIdentityUsingWallet()(步骤 4 已完成 identity 解析,这里是后续) |
:46-48 | ttx.ReceiveTransaction() 接收已签的 tx |
:50-57 | 校验 outputs 列表含本身 |
:64-65 | ttx.NewAcceptView() 接受(若收方策略要求收方签名也走这里) |
:67-69 | ttx.NewFinalityView() 异步等 finality |
步骤 27 — NewOrderingView → Broadcast
来源:
fabric-token-sdk/token/services/ttx/ordering.go:27-92(NewOrderingView:27 +orderingView.Call:40,broadcast/Broadcast在 :74-82;Transfer 步骤 27 调的是ttx.NewOrderingView(tx),见transfer.go:725。Issue 链路改走NewOrderingAndFinalityView:95-118)fabric-smart-client/platform/fabric/core/generic/ordering/cft.go:45-99、client.go:25-50
实际 gRPC:orderer.AtomicBroadcast/Broadcast(标准 Fabric 流式接口)— Stream.Send(env *common.Envelope)。
mTLS 强制:客户端必须出示 cert。
步骤 28 — Orderer Router 的 3 道过滤
来源:fabric-x-orderer/node/router/router.go:577-583, 349-394、fabric-x-orderer/common/requestfilter/sigfilter.go:35-53。
go
rv := requestfilter.NewRulesVerifier(nil)
rv.AddRule(requestfilter.PayloadNotEmptyRule{})
rv.AddRule(requestfilter.NewMaxSizeFilter(config))
rv.AddStructureRule(requestfilter.NewSigFilter(config, policies.ChannelWriters))| 过滤器 | 检查 | 失败 |
|---|---|---|
PayloadNotEmptyRule | envelope.Payload 非空 | 拒绝 |
MaxSizeFilter | size ≤ AbsoluteMaxBytes | 拒绝 |
SigFilter | 仅当 clientSignatureVerificationRequired=true 或 header type 是 CONFIG_UPDATE 时,按 policies.ChannelWriters 策略验证 creator 签名 | 拒绝 |
SigFilter 默认关
fabric-x-orderer/node/config/config.go:92 ClientSignatureVerificationRequired bool 字段由 YAML 配置决定,无硬编码默认值。fabric-x-orderer/test/utils/utils.go:147 中测试 fixture 显示默认 false。
业务交易在默认配置下只过 size 过滤 + mTLS,签名层校验全靠下游 Committer Verifier(步骤 33)。
步骤 29 — Batcher → Consenter(BFT) → Assembler
- Batcher(
fabric-x-orderer/node/batcher/):将 router 转来的请求聚合成 batch;不验签。 - Consenter(SmartBFT 共识):对 batch 的 digest / hash 达成 BFT 共识;不验 batch 内 tx 内容。
- Assembler:将达成共识的 batch 拼成 Fabric Block 结构,推送给下游 deliver stream。
步骤 30 — Sidecar deliver
来源:fabric-x-committer/service/sidecar/sidecar.go:62-144、relay.go:70-118。
:73-74建 deliver 客户端拉 orderer block:116, 140通过servicepb.CoordinatorClient.BlockProcessing()双向流将 block 推给 Coordinator,并接 status 回传
步骤 31–32 — Coordinator + DependencyGraph
来源:fabric-x-committer/service/coordinator/coordinator.go:38-94、service/coordinator/dependencygraph/。
4 个内部通道(:67-93):
coordinatorToDepGraphTxs
↓
depGraphToSigVerifierFreeTxs ← 经依赖分析无冲突的 tx
↓
sigVerifierToVCServiceValidatedTxs ← 经 Verifier 通过
↓
vcServiceToCoordinatorTxStatus ← 来自 VC 的最终状态依赖分析(dependency_detector.go:9-150)按 read-only / write-only / read-write 三表追踪 RW 冲突(R-W、W-R、W-W)。依赖无关交易并发送验签。
步骤 33 — Verifier 按 namespace 验签
来源:fabric-x-committer/service/verifier/verify.go:125-167、fabric-x-committer/utils/signature/verify.go:24-78。
go
for _, ns := range tx.Namespaces {
if ns.NsId == ConfigNamespaceID { continue } // configtx 由 orderer 负责
nsVerifier, ok := verifiers[ns.NsId]
if !ok { return ABORTED_SIGNATURE_INVALID }
nsVerifier.VerifyNs(tx.Ref.TxId, tx.Content, nsIndex)
}NsVerifier 三种实现(utils/signature/verify.go:42-78):
| 类型 | 用途 | 校验对象 |
|---|---|---|
ThresholdRule (keyVerifier + ECDSA/BLS/EdDSA) | 单公钥,1 签即过 | namespace 默认建立时声明的 single pubkey;本部署最常见 |
MspRule (mspVerifier) | Fabric DSL OR/AND/OutOf(...) | 多组织 endorsement 策略 |
ChannelPolicy (channelPolicyVerifier) | 走 /Channel/Application/LifecycleEndorsement | 仅 meta-namespace 配置变更 |
策略按 namespace 各持一份,通过写 meta-namespace 的事务热更新(VerifierUpdates broadcast,atomic pointer 替换)。
步骤 34 — VC(Validator-Committer)
来源:fabric-x-committer/service/vc/validator.go:22-127、service/vc/committer.go:94-181。
| 子步骤 | 行号 | 检查 |
|---|---|---|
| Validator | validator.go:84-127(validate(),:102 调 validateReads :129-149) | 按 namespace 检查 read conflict(MVCC)→ 冲突经 invalidateTxsOnReadConflicts 标 Status_ABORTED_MVCC_CONFLICT(:175) |
| Committer prepare | committer.go:100 | populateVersionsAndCategorizeBlindWrites |
| Committer commit | committer.go:135 | db.commit 写 update writes / new writes;DB 层检测重复 tx ID + read conflict(:155-158) |
| 重试 | committer.go:125-159 | 冲突重试循环 |
| 状态 | committer.go:165-181 | 有效 = Status_COMMITTED;其他失败码(MVCC / 签名 / 依赖等) |
步骤 35–36 — Finality 回传
来源:
fabric-x-committer/service/sidecar/relay.go:86-115接 status batchfabric-x-committer/service/sidecar/notify.go:71-126, 134, 219维护订阅 + 推送
Bank 侧 ttx.NewFinalityView() 在 OrderingAndFinalityView 里订阅该 txID 的状态,收到 COMMITTED 后 Call 返回,整笔交易闭环。
6. AuditView 的 7 条拒绝规则
来源:cbdc-biz/pkg/auditor/rejection/types.go:1-28、cbdc-biz/pkg/views/audit.go:398-502, 524-554。
| # | 类型常量 | 触发 | 代码行 |
|---|---|---|---|
| 1 | SENDER_BALANCE_BELOW_MIN | sender 扣款后余额 < 规则最小值 | audit.go:404-415 |
| 2 | SENDER_NET_RECEIVED | sender 净收入 > 规则最大值(边界 check) | :417-428 |
| 3 | TRANSFER_ABOVE_MAX | 转账金额 > 规则最大值 | :437-448 |
| 4 | TRANSFER_BELOW_MIN | 转账金额 < 规则最小值 | :450-461 |
| 5 | RECIPIENT_BALANCE_ABOVE_MAX | 收方收账后余额 > 规则最大值 | :478-489 |
| 6 | RECIPIENT_BALANCE_BELOW_MIN | 收方收账后余额 < 规则最小值 | :491-502 |
| 7 | IDENTITY_REVOKED | inputs 或 outputs 的 RevocationHandle 在撤销缓存 | :524-554 |
拒绝日志落地链路:
AuditView.logRejection (audit.go:149-153)
→ auditorbiz.LogRejection (app/auditor/internal/biz/auditlog_biz.go:23-24)
→ auditlog 异步队列
→ bizhubRejectionSender.SendRejectionLogs (auditlog_biz.go:30-52)
→ bizhubv1.InsertRejectionLogs (gRPC)
→ BizHub PG audit_rejection_log 表6.5 发起者(Owner)签名在哪几步被验证
Owner sig 由 sender 在链外步骤 6 用外部钱包对 MessageToSign 做 ECDSA P-256 签名,在步骤 21 的 external signer 分支被 Token SDK 取用并写入 TokenRequest.Signatures[]。它在整条链路上被验证 4 次(第 ⓪ 次是 Bank 自验自,①–③ 才是独立第三方),越往后越严格。
| # | 验证点 | 触发位置 | 实际验证内容 | 失败时 |
|---|---|---|---|---|
| ⓪ Bank 本地 fail-fast | 步骤 21(external signer 分支内) | cbdc-biz/pkg/common/wallet_common/pre_submitted_signature_signer.go:71-127,被 fabric-token-sdk/token/services/ttx/collectendorsements.go:276-296(signExternal)调用 | pem.Decode → x509.ParseCertificate → IEEE P1363→ASN.1 DER(强制 Low-S,:48-54) → cert.CheckSignature(cert.SignatureAlgorithm, message, derSignature)。算法仅支持 ECDSA P-256(:48 elliptic.P256()) | error 返回 → AuditAndEndorseView 报 "failed to collect endorsements" → biz releasePendingTx(UnlockAll)→ service 5xx |
| ① Auditor 端 audit.Validate | 步骤 14 | cbdc-biz/pkg/views/audit.go:220 auditor.Validate(tx) | 走 fabric-token-sdk/token/services/auditor/auditor.go:111-112(Validate → request.AuditCheck,AuditCheck 定义 token/request.go:1250)→ request.IsValid(:1019)→ inputsAndOutputs(verifyActions=true)(:918,verifyActions 分支 :950-951 / :986-987) → 每个 Issue 调 VerifyIssue,每个 Transfer 调 VerifyTransfer。zkatdlog 驱动下,VerifyTransfer 同时校验 ZK 范围证明、token 承诺、所有 owner sig | Auditor 直接 panic(assert.NoError at audit.go:220),sigma 不返回 → Bank 收不到审计签名 → 整笔交易终止 |
| ② Endorser 9 闸门 #8 | 步骤 23 | fabric-token-sdk/token/services/network/fabric/endorsement/fsc/responder.go:293-329 r.validate(...) 内调 validator.UnmarshallAndVerifyWithMetadata() | 同样的 token-level 校验(ZK + owner sig + auditor σ + metadata 一致性);这一次连步骤 19 写入的 auditor σ 也一起验 | 返回 ErrValidateProposal → 不出 proposal endorsement → Bank 拿不到 Envelope.Endorsements[] → 无法 Broadcast |
| ③ Committer Verifier | 步骤 33 | fabric-x-committer/service/verifier/verify.go:125-167 按 tx.Namespaces[i].NsId 查 verifiers[NsId] 后 nsVerifier.VerifyNs(...) | 走 utils/signature/verify.go:42-78 的 NsVerifier:根据 namespace policy 类型用 ThresholdRule(单 pubkey)/ MspRule(Fabric DSL)/ ChannelPolicy 评估提交进来的签名集合 | 标 Status_ABORTED_SIGNATURE_INVALID(verify.go:146, 162),不入账 |
四层校验的语义差别
- 第 ⓪ 步是 Bank 自验自(私钥、cert、验签进程都在调用方 Bank 侧),仅有 fail-fast 作用,不构成治理意义上的"被验"。
- 第 ①、② 步是 Token SDK 内部的 token 业务层校验(ZK + 单笔 action 的 owner sig 是否符合 zkatdlog 协议),与 namespace policy 无关。
- 第 ③ 步是 Fabric-X namespace 级 policy 校验,决定"这套签名集合是否满足该 namespace 创建时声明的策略"(例如某 namespace 要求 OR('Org1.member','Org2.member'))。
①② 保证密码学正确性,③ 保证治理策略合规。任一层失败都阻断落账。
Bank 不"独立"验 owner sig,Orderer 默认也不验
- Bank 在步骤 19 做的是
verifyAuditorSignature(验 auditor 回的 sigma),不对 owner sig 再做一次。 - 步骤 21 子步骤 ⓪ 的
PreSubmittedSignatureSigner.Sign是"自验自",不算独立验证。 - Router SigFilter(步骤 28)只有在
ClientSignatureVerificationRequired=true时才验 envelope 外层 creator 签名(属 mTLS 之上的 Fabric SignatureHeader 校验),与 TokenRequest 内 owner sig 是两回事。本仓库无显式开启配置([未核])。 - 收款方
AcceptTransferView走ttx.NewAcceptView,默认不复验 owner sig。
owner sig 首次被任何"独立第三方"验,是在 Auditor 端的步骤 14;第二次在 Endorser 9 闸门 #8(步骤 23);决定能否落账的最后一次在 Committer Verifier(步骤 33)。
7. 整笔 Transfer 的签名集合(汇总)
| 签名 | 签名方 | 私钥位置 | 数据 | 落点字段 | 何时生成 | 在哪里被验 |
|---|---|---|---|---|---|---|
| 外部 API 签名 | 调用方钱包 | Bank 自托管 | PrepareTransferResult.MessageToSign | SubmitTransfer 参数 | 步骤 6 | Token SDK external signer 分支(步骤 21) |
| Auditor σ | Auditor wallet | sign-kms partition | tx.MarshallToAudit() | TokenRequest.AuditorSignatures[*] | 步骤 15 | Bank 本地 verifyAuditorSignature(步骤 19)+ Endorser 9 闸门 #8(步骤 23)+ Committer Verifier(步骤 33) |
| Owner sig | sender wallet identity | external(Bank 自托管) | TokenRequest.MarshalToSign() 字节 | TokenRequest.Signatures[] | 步骤 21(外部 signer 注入,链外步骤 6 生成) | 4 次(含 1 次自验):⓪ Bank 本地 PreSubmittedSignatureSigner.Sign(步骤 21 子步骤,自验自)→ ① Auditor audit.Validate(步骤 14)→ ② Endorser 9 闸门 #8(步骤 23)→ ③ Committer Verifier 按 namespace policy(步骤 33)。详见 §6.5 |
| Institution proposal 签名 | institution FSC 节点身份 | sign-kms KMS_TK partition | Fabric proposal | SignedProposal.Signature | 步骤 22 起始:RequestApprovalView 先 tx.EndorseProposal()(fabric-token-sdk token/services/network/fabric/endorsement/fsc/initiator.go:77 → fabric-smart-client core/generic/transaction/transasction.go:490-503,sigService.GetSigner(Creator)) | proposal 解包校验 creator.Verify(ProposalBytes, Signature)(fabric-smart-client core/generic/transaction/proposal.go:216) |
| Endorser proposal endorsement | endorser FSC node | sign-kms KMS_TK partition | Fabric proposal hash | Envelope.Endorsements[] | 步骤 24 | Committer Verifier 按 namespace 策略(步骤 33) |
| Institution envelope 信封签名 | institution FSC 节点身份(与 proposal 同一把 key,两次独立 Sign) | sign-kms KMS_TK partition | payload(header + 交易) | Envelope.Signature | 步骤 24→27 之间:tx.Envelope()(fsc/initiator.go:106 → fabric-smart-client core/generic/transaction/envelope.go CreateEndorserSignedTX,signer = Creator) | 默认链路无强制验证点(Router SigFilter 默认关,见下行) |
| mTLS client cert | Bank TLS cert | 节点本地或 KMS | TLS 握手随机数 | TLS layer | gRPC connect | Orderer Router mTLS(步骤 27) |
| 可选 Router SigFilter | Envelope creator | 同上 | protoutil.SignedData{} | — | 步骤 27 envelope.SignatureHeader | policies.ChannelWriters(步骤 28,默认关) |
实际有效身份门只有 3 个
- Auditor 业务规则(步骤 14)
- Endorser 9 道闸门(步骤 23)
- Committer namespace 验签 + VC(步骤 33-34)
mTLS 只能挡证书不在受信 CA 内,不挡撤销、不识别业务身份;Router SigFilter 默认关。详见 02 信任与身份模型。
KMS 调用总账(每笔 Transfer = 4 次 PKCS#11 Sign)
上表中经 sign-kms 的签名共 4 次/笔:institution 节点 2 次(proposal + envelope,同一把 key 两次独立 Sign)、endorser 1 次、auditor σ 1 次;owner 签名外部注入、不占 KMS。Issue 为 issuer 钱包 + 节点 2 次 + endorser + auditor = 5 次 KMS;Redeem 在 Transfer 基础上另加 issuer 背书(pkg/views/redeem.go:149,ttx.NewEndorseView)。KMS 容量含义见 08 扩容指南 · HSM 需要几台。
8. View 注册与 FSC 拓扑
来源:各 cmd/<app>/main.go 中的 viewregistry.GetRegistry(node).RegisterResponder(...) + Token SDK 自动注册。
| 节点 | 显式注册的 Responder | Token SDK 自动注册 | 出处 |
|---|---|---|---|
| Institution-001 | AcceptTransferView(← TransferView)、AcceptIdentityRequestView(← PrepareTransferView)、AcceptPreparedTransferView(← SubmitSignatureView)、AcceptCashView(← IssueCashView) | — | cbdc-biz/app/institution/cmd/institution/main.go:210-220 |
| Auditor | AuditView(← RequestAuditView) | — | cbdc-biz/app/auditor/cmd/auditor/main.go:202-209 |
| Endorser | AuditResponderView(← AuditAndEndorseView) | RequestApprovalResponderView(← RequestApprovalView)— service.go:56-73 在 fsc_endorsement.endorser=true 时 | cbdc-biz/app/endorser/cmd/endorser/main.go:107-110 + conf/endorser1/core.yaml:142-143 |
| Issuer | IssuerRedeemAcceptView(← RedeemView / SubmitSignatureView) | — | cbdc-biz/app/issuer/cmd/issuer/main.go:133-135 |
pkg/views/ 文件清单:transfer.go / accept_transfer.go / issue.go / redeem.go / audit.go / endorsement.go / finality_registry.go / token_optimization.go / audit_metrics.go / warmup.go。
9. Issue · 发行 的差异
来源:pkg/views/issue.go、cbdc-biz/app/bizhub/internal/biz/settlement_biz.go。
与 Transfer 的差异:
| 维度 | Transfer | Issue |
|---|---|---|
| 发起方私钥 | sender wallet(机构钱包) | Issuer wallet |
| Owner sig 分支 | transfer signers(collectendorsements.go:110) | issue signers(:105),用 IssuerVerifier |
| Token Selector | 必须选币(已有 UTXO) | 不需要(无输入) |
| ZK 范围证明 | inputs + outputs 都验 | 仅 outputs |
| RTGS 联动 | 无(除非 Bank 主动) | 由 BizHub SettlementPendingWorker → ProcessIssuanceAsync 触发;成功后写 settlement_request 状态 |
| Auditor 校验规则 | 7 条全过 | 同(sender 余额 / sender net 这两条对 Issue 不适用,但代码仍走规则树) |
Issue 在 BizHub 侧的状态机
来源:cbdc-biz/app/bizhub/internal/data/ent/schema/settlement_request.go、worker/settlement_*.go。
PENDING ─(pending worker poll 5s, conc 10)─> PROCESSING ─(success)─> COMPLETED
│ │
├─(retry worker, retry_count < 3) ├─(timeout 5min)─> FAILED + RETRYABLE (PROCESSING_TIMEOUT, retry_count+1)
│ └─(error type)
└─(error_type=UNRECOVERABLE)─> FAILED ─(compensate)─> COMPENSATION_COMPLETED3 个 worker:
| Worker | 默认参数 | 行为 | 出处 |
|---|---|---|---|
SettlementPendingWorker | poll 5s / conc 10 / batch 100 | status=PENDING → ProcessIssuanceAsync / ProcessRedemptionAsync | worker/settlement_pending_worker.go:20-55 |
SettlementRetryWorker | poll 5s / conc 10 / batch 100 | retriable 且 retry_count < maxRetries(3) → 重跑 | worker/settlement_retry_worker.go:21-58 |
SettlementTimeoutWorker | check 30s / batch 50 / 串行 | status=PROCESSING 且 updated_at > 5min → FAILED、error_code=PROCESSING_TIMEOUT、error_type=RETRYABLE、retry_count+1(repo.FetchAndMarkTimedOutRequests,data/settlement_repo.go:554-603);worker 自身仅打 warn 日志 | worker/settlement_timeout_worker.go:19-55 |
error_type(types/settlement.go:38-40,3 个):RETRYABLE / UNRECOVERABLE / MANUAL_REVIEW。error_code(types/settlement.go:45-51,7 个):RTGS_NAK / RTGS_STATE_UNKNOWN / REMINT_FAILED / RTGS_UNAVAILABLE / SUBMIT_REDEEM_FAILED / INVALID_STATE / RTGS_SETTLED_DB_FAILURE。
10. Redeem · 赎回 的差异
来源:pkg/views/redeem.go、BizHub Settlement worker 同上、cbdc-biz/pkg/rtgs/。
链上侧与 Transfer 同链路(步骤 10-36),差异:
| 维度 | Transfer | Redeem |
|---|---|---|
| Token SDK 动作 | tx.Transfer() | tx.Redeem()(销毁 token,无输出 owner) |
| Issuer 是否签名 | 否 | 是(Issuer 是 redeem 的接收/销毁方,需 issuer sig) |
| Auditor 校验 | 7 条规则全 | 同(part 适用) |
| 链下结算 | 无 | BizHub 侧把对应金额写 RTGS MT298 → SMB 共享目录 → 等回执 |
RTGS 模块(链下结算)
来源:cbdc-biz/pkg/rtgs/。
文件清单:
| 文件 | 用途 |
|---|---|
client.go | RTGS 客户端入口 |
client_smb.go | SMB 协议适配 |
smb_client.go / smb_pool.go | SMB 连接池 |
config.go | 配置 + PKCS#11 keystore |
crypto.go | MT298 报文签名 + 验证 |
mt298/ | SWIFT MT298 编解码 |
BizHub core.yaml:79-112 实际配置(节选):
yaml
rtgs:
request_signing_enabled: true
response_verify_enabled: true
swift:
sender_bic: '5520010001XX' # Platform BIC (MT298 sender)
settlement_payment_code: '66669999' # 发行结算 (SMT201)
reservation_payment_code: '90000005' # 基金预留 (SMT211)
redemption_settlement_payment_code: '66669999' # 赎回结算 (SMT201)
smb:
host: 'host.docker.internal' # 本地 rtgs-mock
port: 445
keystore:
type: jks # "pkcs12" or "jks"
path: ./keystore/test.jks本地用 cbdc-biz/cmd/rtgs-mock/main.go 模拟央行 RTGS(compose-rtgs-mock.yml)。
11. 跨链路角色总图
12. [未核]
- 两步签名 Transfer(
PrepareTransferView+SubmitSignatureView与AcceptIdentityRequestView+AcceptPreparedTransferView的两对 View)的具体业务触发条件 —— 需读pkg/views/中两 Initiator 的实现细节。 fsc.persistences.default.opts中是否 leak owner 私钥:Token SDK 在某些 BCCSP 配置下会把临时 ephemeral key 落本地 keystore,是否对 owner 长期身份生效需要进一步比对。- MetaNamespace policy 在 PG 表的 schema:
fabric-x-committer/service/vc/database.go中policies表的具体字段未详查。 - Recipient 是否对 Transfer 签名(双签 / 单签):
AcceptTransferView:64-65调ttx.NewAcceptView(),但 NewAcceptView 内部是否对 tx sign 取决于 wallet 策略,本仓库默认 [未核]。 - Auditor
audit_rejection_log表 schema:cbdc-biz/app/bizhub/internal/data/ent/schema/audit_rejection_log.go字段详情。 - Router
clientSignatureVerificationRequired在生产 KMS 部署下的实际取值:取决于cbdc-network/configs/test-full-kms*.yaml渲染后注入到 orderer config 的字段,未在仓库现状中找到显式 true 配置。