//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 }