166f460d57
- 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 契约套件均通过,行为一致
295 lines
10 KiB
Go
295 lines
10 KiB
Go
//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
|
||
}
|