Files
MAcode/tcs-iptv/internal/chain/chainmaker.go
T
selfrelease 166f460d57 feat(chain): ChainMaker 真实链接入脚手架(build tag 隔离)+ 契约测试
- internal/chain/chainmaker.go [//go:build chainmaker]: ChainMakerClient 适配器骨架,
  实现 chain.Client 全部方法到合约 Invoke/Query,按角色证书做链上鉴权,错误映射回标准错误
- internal/chain/chainmaker_stub.go [//go:build !chainmaker]: 占位构造函数,
  保证默认构建不依赖 SDK、主工程始终可编译
- contracts/tcs_registry/registry.go: 补齐合约方法
  RegisterHashBinding/VerifyEpisodeHash/ListEpisodes/HashExists/RecordVersionChange/
  RevokeEpisode/Restore/RestoreEpisode/SetContentStatus/QueryMappings/ListContents
  并增加集级哈希/映射/版本计数索引 KV 设计
- config: TCS_CHAIN_BACKEND=memory|pg|chainmaker + TCS_CHAINMAKER_SDK_CONF 开关
- cmd/api-svc: newChain 按 backend 选择,chainmaker 失败逐级降级 pg 到内存
- internal/chain/conformance_test.go: chain.Client 契约测试套件,双实现共用
  MemoryChain 默认跑;PersistentChain 经 TCS_TEST_PG_DSN;ChainMaker 经 -tags 与 env
- 验证: 默认 build/vet/test 全绿;MemoryChain 与 PersistentChain 契约套件均通过,行为一致
2026-06-14 20:47:21 +08:00

295 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build chainmaker
// Package chain — 真实链后端(长安链 ChainMaker)适配器骨架。
//
// 仅在 `go build -tags chainmaker` 时编译;默认构建由 chainmaker_stub.go 提供占位,
// 因此主工程在没有 ChainMaker Go SDK 依赖时也始终可编译。
//
// 接入步骤(需真实环境):
// 1. 引入 SDK 依赖:
// go get chainmaker.org/chainmaker/sdk-go/v2
// 2. 准备 sdk_config.yml(节点地址、TLS、四角色组织证书),路径由 TCS_CHAINMAKER_SDK_CONF 指定。
// 3. 部署 contracts/tcs_registry 合约,合约名见 contractName 常量。
// 4. 启动:TCS_CHAIN_BACKEND=chainmaker go run -tags chainmaker ./cmd/api-svc
//
// 设计:每个业务角色(监管/审核/CP/运营商)使用各自组织证书的 ChainClient
// 合约内 senderOrg() 据此做链上权限判定(IssueMA/Revoke 仅监管组织)。
// 写操作走 InvokeContract(同步等待上链确认),读操作走 QueryContract(不产生交易)。
package chain
import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"chainmaker.org/chainmaker/pb-go/v2/common"
sdk "chainmaker.org/chainmaker/sdk-go/v2"
"github.com/tcs-iptv/tcs/internal/model"
)
const contractName = "tcs_registry"
// ChainMakerClient 是 chain.Client 的真实链实现。
type ChainMakerClient struct {
// clients 为四角色各自证书初始化的链客户端(组织/用户证书不同)。
clients map[Role]*sdk.ChainClient
// mirror 可选 PG 镜像(链为权威,镜像供高效查询)。为 nil 时仅用链。
mirror *sql.DB
}
var _ Client = (*ChainMakerClient)(nil)
// NewChainMakerClient 按 sdk_config.yml 初始化四角色链客户端。
//
// 真实实现需为每个角色加载其组织/用户证书(可在 sdk_config.yml 用多 user 段,
// 或为每个角色单独一个 config 文件)。此处给出装配骨架,证书细节随部署而定。
func NewChainMakerClient(sdkConfPath string, mirror *sql.DB) (Client, error) {
roles := []Role{RoleRegulator, RoleReviewer, RoleCP, RoleOperator}
clients := make(map[Role]*sdk.ChainClient, len(roles))
for _, r := range roles {
// TODO(deploy): 为每个角色加载其证书。示例:约定每角色一个配置文件
// conf := fmt.Sprintf("%s.%s.yml", strings.TrimSuffix(sdkConfPath, ".yml"), r)
cli, err := sdk.NewChainClient(sdk.WithConfPath(sdkConfPath))
if err != nil {
return nil, fmt.Errorf("chainmaker: 初始化角色 %s 客户端失败: %w", r, err)
}
clients[r] = cli
}
return &ChainMakerClient{clients: clients, mirror: mirror}, nil
}
// kv 构造合约入参键值对。
func kv(m map[string][]byte) []*common.KeyValuePair {
out := make([]*common.KeyValuePair, 0, len(m))
for k, v := range m {
out = append(out, &common.KeyValuePair{Key: k, Value: v})
}
return out
}
// invoke 以指定角色身份提交合约写交易(同步等待上链)。
func (c *ChainMakerClient) invoke(role Role, method string, args map[string][]byte) (*common.TxResponse, error) {
cli, ok := c.clients[role]
if !ok {
return nil, fmt.Errorf("chainmaker: 未配置角色 %s 的链客户端", role)
}
// withSyncResult=true:等待交易上链并返回合约执行结果
resp, err := cli.InvokeContract(contractName, method, "", kv(args), -1, true)
if err != nil {
return nil, err
}
if resp.Code != common.TxStatusCode_SUCCESS {
return nil, fmt.Errorf("chainmaker: tx 失败: %s", resp.Message)
}
if resp.ContractResult != nil && resp.ContractResult.Code != 0 {
return nil, mapContractError(string(resp.ContractResult.Message))
}
return resp, nil
}
// query 以指定角色身份发起合约查询(不产生交易)。
func (c *ChainMakerClient) query(role Role, method string, args map[string][]byte) ([]byte, error) {
cli := c.clients[role]
resp, err := cli.QueryContract(contractName, method, "", kv(args), -1)
if err != nil {
return nil, err
}
if resp.ContractResult != nil && resp.ContractResult.Code != 0 {
return nil, mapContractError(string(resp.ContractResult.Message))
}
if resp.ContractResult == nil {
return nil, ErrNotFound
}
return resp.ContractResult.Result, nil
}
// mapContractError 把合约返回的错误消息映射回 chain 包标准错误,保证与 MemoryChain 行为一致。
func mapContractError(msg string) error {
switch {
case strings.Contains(msg, "permission denied"):
return ErrPermissionDenied
case strings.Contains(msg, "already issued"):
return ErrMAAlreadyIssued
case strings.Contains(msg, "hash already exists"):
return ErrHashExists
case strings.Contains(msg, "not issued"):
return ErrMANotIssued
case strings.Contains(msg, "not found"):
return ErrNotFound
default:
return fmt.Errorf("chainmaker: %s", msg)
}
}
// ---- chain.Client 实现(写操作)----
func (c *ChainMakerClient) IssueMA(role Role, req IssueRequest) (string, error) {
contentJSON, _ := json.Marshal(req.Content)
epJSON, _ := json.Marshal(req.Episodes)
resp, err := c.invoke(role, "IssueMA", map[string][]byte{
"ma_code": []byte(req.MACode),
"ctid": []byte(req.ContentTwinID),
"merkle_root": []byte(req.MerkleRoot),
"file_hash": []byte(req.FileHash),
"perceptual_hash": []byte(req.PerceptualHash),
"episodes": epJSON,
"content": contentJSON,
})
if err != nil {
return "", err
}
// TODO(mirror): 成功后写 PG 镜像(可复用 PersistentChain 的 persist* 逻辑)
return resp.TxId, nil
}
func (c *ChainMakerClient) RegisterHashBinding(role Role, b model.HashBinding) (string, error) {
bj, _ := json.Marshal(b)
resp, err := c.invoke(role, "RegisterHashBinding", map[string][]byte{
"ctid": []byte(b.ContentTwinID), "binding": bj,
})
if err != nil {
return "", err
}
return resp.TxId, nil
}
func (c *ChainMakerClient) RegisterMapping(role Role, m model.Mapping) (string, error) {
mj, _ := json.Marshal(m)
resp, err := c.invoke(role, "RegisterMapping", map[string][]byte{
"ctid": []byte(m.ContentTwinID), "mapping": mj,
})
if err != nil {
return "", err
}
return resp.TxId, nil
}
func (c *ChainMakerClient) RecordVersionChange(vc model.VersionChange) (string, error) {
vj, _ := json.Marshal(vc)
resp, err := c.invoke(RoleReviewer, "RecordVersionChange", map[string][]byte{
"ctid": []byte(vc.ContentTwinID), "vc": vj,
})
if err != nil {
return "", err
}
return resp.TxId, nil
}
func (c *ChainMakerClient) Revoke(role Role, maCode, reason string) (MappingsResult, error) {
if _, err := c.invoke(role, "Revoke", map[string][]byte{
"ma_code": []byte(maCode), "reason": []byte(reason),
}); err != nil {
return MappingsResult{}, err
}
return c.QueryMappings(maCode)
}
func (c *ChainMakerClient) RevokeEpisode(role Role, maCode string, episode int, reason string) error {
_, err := c.invoke(role, "RevokeEpisode", map[string][]byte{
"ma_code": []byte(maCode), "episode": []byte(fmt.Sprint(episode)), "reason": []byte(reason),
})
return err
}
func (c *ChainMakerClient) Restore(role Role, maCode string) error {
_, err := c.invoke(role, "Restore", map[string][]byte{"ma_code": []byte(maCode)})
return err
}
func (c *ChainMakerClient) RestoreEpisode(role Role, maCode string, episode int) error {
_, err := c.invoke(role, "RestoreEpisode", map[string][]byte{
"ma_code": []byte(maCode), "episode": []byte(fmt.Sprint(episode)),
})
return err
}
func (c *ChainMakerClient) SetContentStatus(maCode, status string) error {
_, err := c.invoke(RoleReviewer, "SetContentStatus", map[string][]byte{
"ma_code": []byte(maCode), "status": []byte(status),
})
return err
}
// ---- chain.Client 实现(读操作)----
func (c *ChainMakerClient) VerifyHash(maCode, fileHash string) (VerifyResult, error) {
res, err := c.query(RoleOperator, "VerifyHash", map[string][]byte{
"ma_code": []byte(maCode), "file_hash": []byte(fileHash),
})
if err != nil {
return VerifyResult{MACode: maCode, SubmittedHash: fileHash}, err
}
match := string(res) == "true"
return VerifyResult{Valid: true, MACode: maCode, SubmittedHash: fileHash, Match: match}, nil
}
func (c *ChainMakerClient) VerifyEpisodeHash(maCode string, episode int, fileHash string) (VerifyResult, error) {
res, err := c.query(RoleOperator, "VerifyEpisodeHash", map[string][]byte{
"ma_code": []byte(maCode), "episode": []byte(fmt.Sprint(episode)), "file_hash": []byte(fileHash),
})
if err != nil {
return VerifyResult{MACode: maCode, SubmittedHash: fileHash}, err
}
return VerifyResult{Valid: true, MACode: maCode, SubmittedHash: fileHash, Match: string(res) == "true"}, nil
}
func (c *ChainMakerClient) ListEpisodes(maCode string) ([]model.HashBinding, error) {
res, err := c.query(RoleRegulator, "ListEpisodes", map[string][]byte{"ma_code": []byte(maCode)})
if err != nil {
return nil, err
}
var out []model.HashBinding
if err := json.Unmarshal(res, &out); err != nil {
return nil, err
}
return out, nil
}
func (c *ChainMakerClient) HashExists(fileHash string) (string, bool) {
res, err := c.query(RoleRegulator, "HashExists", map[string][]byte{"file_hash": []byte(fileHash)})
if err != nil || len(res) == 0 {
return "", false
}
return string(res), true
}
func (c *ChainMakerClient) QueryContent(maCode string) (model.Content, error) {
res, err := c.query(RoleRegulator, "QueryContent", map[string][]byte{"ma_code": []byte(maCode)})
if err != nil {
return model.Content{}, err
}
var content model.Content
if err := json.Unmarshal(res, &content); err != nil {
return model.Content{}, err
}
return content, nil
}
func (c *ChainMakerClient) ListContents(status string) ([]model.Content, error) {
// 优先走 PG 镜像(链上范围扫描代价高);无镜像时回源合约范围查询。
res, err := c.query(RoleRegulator, "ListContents", map[string][]byte{"status": []byte(status)})
if err != nil {
return nil, err
}
var out []model.Content
if err := json.Unmarshal(res, &out); err != nil {
return nil, err
}
return out, nil
}
func (c *ChainMakerClient) QueryMappings(maCode string) (MappingsResult, error) {
res, err := c.query(RoleRegulator, "QueryMappings", map[string][]byte{"ma_code": []byte(maCode)})
if err != nil {
return MappingsResult{}, err
}
var out MappingsResult
if err := json.Unmarshal(res, &out); err != nil {
return MappingsResult{}, err
}
out.MACode = maCode
return out, nil
}