Files
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

390 lines
12 KiB
Go
Raw Permalink 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.
// Package main 是 TCS-IPTV 可信数据空间的 ChainMaker 智能合约(Go)。
//
// 本合约为独立合约模块(独立 go.mod),按 ChainMaker docker-go 合约规范部署。
// 与 internal/chain.Client 接口语义一一对应;MVP/二期用 MemoryChain 等价实现,
// 具备链环境后部署本合约,由 chain-svc / ChainMakerClient 通过 SDK 调用替换内存实现。
//
// 状态键设计(KV):
//
// content:{maCode} -> Content JSON(含 status
// binding:{maCode}:0 -> 整剧 file 哈希绑定 JSON
// binding:{maCode}:p -> 感知哈希绑定 JSON
// ep:{maCode}:{n} -> 集级绑定 JSON {episode,hash,revoked,reason}
// epcount:{maCode} -> 集数 N
// hashidx:{fileHash} -> maCode(防换壳重发)
// mapping:{maCode}:{idx} -> Mapping JSON
// mapcount:{maCode} -> 映射数 N
// version:{maCode}:{idx} -> VersionChange JSON
// vercount:{maCode} -> 版本变更数 N
// ctid2ma:{ctid} -> maCode
// allmacodes -> []maCode(供 ListContents 遍历)
//
// 权限:通过 sender 组织证书判断(仅监管组织可 IssueMA/Revoke/RevokeEpisode/Restore)。
package main
import (
"encoding/json"
"errors"
"strconv"
"chainmaker.org/chainmaker/contract-sdk-go/v2/pb/protogo"
"chainmaker.org/chainmaker/contract-sdk-go/v2/sandbox"
"chainmaker.org/chainmaker/contract-sdk-go/v2/sdk"
)
// TCSRegistry 合约。
type TCSRegistry struct{}
const (
orgRegulator = "regulator" // 监管组织:仅其可签发/下架
orgReviewer = "reviewer" // 审核组织:哈希绑定/版本变更/状态流转
)
// InitContract 合约初始化。
func (t *TCSRegistry) InitContract() protogo.Response {
return sdk.Success([]byte("tcs_registry initialized"))
}
// UpgradeContract 合约升级。
func (t *TCSRegistry) UpgradeContract() protogo.Response {
return sdk.Success([]byte("tcs_registry upgraded"))
}
func senderOrg() string {
org, _ := sdk.Instance.GetSenderOrgId()
return org
}
func getInt(key, field string) int {
v, _ := sdk.Instance.GetStateByte(key, field)
if len(v) == 0 {
return 0
}
n, _ := strconv.Atoi(string(v))
return n
}
func putInt(key, field string, n int) {
_ = sdk.Instance.PutStateByte(key, field, []byte(strconv.Itoa(n)))
}
// epBinding 集级绑定的链上结构。
type epBinding struct {
Episode int `json:"episode"`
HashValue string `json:"hash_value"`
Revoked bool `json:"revoked"`
Reason string `json:"revoked_reason,omitempty"`
}
// appendMACode 把新发码加入全局列表(供 ListContents 遍历)。
func appendMACode(maCode string) {
var all []string
if v, _ := sdk.Instance.GetStateByte("allmacodes", ""); len(v) > 0 {
_ = json.Unmarshal(v, &all)
}
all = append(all, maCode)
b, _ := json.Marshal(all)
_ = sdk.Instance.PutStateByte("allmacodes", "", b)
}
// ---- 写方法 ----
// IssueMA 签发 MA 码并 1:1 强绑定哈希;同时登记集级哈希(仅监管组织)。
func (t *TCSRegistry) IssueMA() protogo.Response {
if senderOrg() != orgRegulator {
return sdk.Error("permission denied: only regulator can issue MA")
}
args := sdk.Instance.GetArgs()
maCode := string(args["ma_code"])
ctid := string(args["ctid"])
fileHash := string(args["file_hash"])
if existing, _ := sdk.Instance.GetStateByte("content", maCode); len(existing) > 0 {
return sdk.Error("MA already issued (1:1 binding immutable)")
}
if bound, _ := sdk.Instance.GetStateByte("hashidx", fileHash); len(bound) > 0 {
return sdk.Error("content hash already exists")
}
// 内容主记录(强制 status=approved,与 MemoryChain 一致)
var content map[string]interface{}
_ = json.Unmarshal(args["content"], &content)
if content == nil {
content = map[string]interface{}{}
}
content["ma_code"] = maCode
content["content_twin_id"] = ctid
content["status"] = "approved"
cj, _ := json.Marshal(content)
_ = sdk.Instance.PutStateByte("content", maCode, cj)
// 整剧 file 绑定 + 感知哈希绑定
fb, _ := json.Marshal(epBinding{Episode: 0, HashValue: fileHash})
_ = sdk.Instance.PutStateByte("binding", maCode+":0", fb)
if ph := string(args["perceptual_hash"]); ph != "" {
pb, _ := json.Marshal(map[string]string{"hash_type": "perceptual", "hash_value": ph})
_ = sdk.Instance.PutStateByte("binding", maCode+":p", pb)
}
_ = sdk.Instance.PutStateByte("hashidx", fileHash, []byte(maCode))
_ = sdk.Instance.PutStateByte("ctid2ma", ctid, []byte(maCode))
// 集级哈希
var eps []map[string]interface{}
_ = json.Unmarshal(args["episodes"], &eps)
n := 0
for _, e := range eps {
ep := int(toFloat(e["episode"]))
hv, _ := e["file_sha256"].(string)
if ep <= 0 || hv == "" {
continue
}
eb, _ := json.Marshal(epBinding{Episode: ep, HashValue: hv})
_ = sdk.Instance.PutStateByte("ep", maCode+":"+strconv.Itoa(ep), eb)
if exist, _ := sdk.Instance.GetStateByte("hashidx", hv); len(exist) == 0 {
_ = sdk.Instance.PutStateByte("hashidx", hv, []byte(maCode))
}
if ep > n {
n = ep
}
}
putInt("epcount", maCode, n)
appendMACode(maCode)
sdk.Instance.EmitEvent("RegisterSuccess", []string{maCode, fileHash})
return sdk.Success([]byte(maCode))
}
func toFloat(v interface{}) float64 {
if f, ok := v.(float64); ok {
return f
}
return 0
}
// RegisterHashBinding 追加哈希绑定(如转码版)。MA 必须已签发(审核/监管)。
func (t *TCSRegistry) RegisterHashBinding() protogo.Response {
if o := senderOrg(); o != orgReviewer && o != orgRegulator {
return sdk.Error("permission denied")
}
args := sdk.Instance.GetArgs()
ctid := string(args["ctid"])
ma, _ := sdk.Instance.GetStateByte("ctid2ma", ctid)
if len(ma) == 0 {
return sdk.Error("MA not issued")
}
idx := getInt("bindextra", string(ma)) + 1
_ = sdk.Instance.PutStateByte("bindextra", string(ma)+":"+strconv.Itoa(idx), args["binding"])
putInt("bindextra", string(ma), idx)
return sdk.Success([]byte("ok"))
}
// RegisterMapping 注册三方编码映射;MA 必须已签发(任意角色注册本方)。
func (t *TCSRegistry) RegisterMapping() protogo.Response {
args := sdk.Instance.GetArgs()
ctid := string(args["ctid"])
ma, _ := sdk.Instance.GetStateByte("ctid2ma", ctid)
if len(ma) == 0 {
return sdk.Error("MA not issued")
}
idx := getInt("mapcount", string(ma)) + 1
_ = sdk.Instance.PutStateByte("mapping", string(ma)+":"+strconv.Itoa(idx), args["mapping"])
putInt("mapcount", string(ma), idx)
return sdk.Success([]byte("ok"))
}
// RecordVersionChange 记录版本变更(审核/监管)。
func (t *TCSRegistry) RecordVersionChange() protogo.Response {
if o := senderOrg(); o != orgReviewer && o != orgRegulator {
return sdk.Error("permission denied")
}
args := sdk.Instance.GetArgs()
ctid := string(args["ctid"])
ma, _ := sdk.Instance.GetStateByte("ctid2ma", ctid)
if len(ma) == 0 {
return sdk.Error("MA not issued")
}
idx := getInt("vercount", string(ma)) + 1
_ = sdk.Instance.PutStateByte("version", string(ma)+":"+strconv.Itoa(idx), args["vc"])
putInt("vercount", string(ma), idx)
return sdk.Success([]byte("ok"))
}
// Revoke 整剧下架(仅监管组织)。
func (t *TCSRegistry) Revoke() protogo.Response {
if senderOrg() != orgRegulator {
return sdk.Error("permission denied: only regulator can revoke")
}
return setStatus(string(sdk.Instance.GetArgs()["ma_code"]), "revoked", "Revoked")
}
// SetContentStatus 状态流转(审核/监管):入库/发布等。
func (t *TCSRegistry) SetContentStatus() protogo.Response {
if o := senderOrg(); o != orgReviewer && o != orgRegulator {
return sdk.Error("permission denied")
}
args := sdk.Instance.GetArgs()
return setStatus(string(args["ma_code"]), string(args["status"]), "StatusChanged")
}
func setStatus(maCode, status, event string) protogo.Response {
cj, _ := sdk.Instance.GetStateByte("content", maCode)
if len(cj) == 0 {
return sdk.Error("not found")
}
var content map[string]interface{}
_ = json.Unmarshal(cj, &content)
content["status"] = status
nj, _ := json.Marshal(content)
_ = sdk.Instance.PutStateByte("content", maCode, nj)
sdk.Instance.EmitEvent(event, []string{maCode, status})
return sdk.Success([]byte("ok"))
}
// RevokeEpisode 集级下架(仅监管组织)。
func (t *TCSRegistry) RevokeEpisode() protogo.Response {
if senderOrg() != orgRegulator {
return sdk.Error("permission denied")
}
args := sdk.Instance.GetArgs()
return setEpisodeRevoked(string(args["ma_code"]), string(args["episode"]), true, string(args["reason"]))
}
// RestoreEpisode 集级恢复(仅监管组织)。
func (t *TCSRegistry) RestoreEpisode() protogo.Response {
if senderOrg() != orgRegulator {
return sdk.Error("permission denied")
}
args := sdk.Instance.GetArgs()
return setEpisodeRevoked(string(args["ma_code"]), string(args["episode"]), false, "")
}
// Restore 整剧恢复(仅监管组织)。
func (t *TCSRegistry) Restore() protogo.Response {
if senderOrg() != orgRegulator {
return sdk.Error("permission denied")
}
return setStatus(string(sdk.Instance.GetArgs()["ma_code"]), "published", "Restored")
}
func setEpisodeRevoked(maCode, epStr string, revoked bool, reason string) protogo.Response {
key := maCode + ":" + epStr
v, _ := sdk.Instance.GetStateByte("ep", key)
if len(v) == 0 {
return sdk.Error("not found")
}
var eb epBinding
_ = json.Unmarshal(v, &eb)
eb.Revoked = revoked
eb.Reason = reason
nb, _ := json.Marshal(eb)
_ = sdk.Instance.PutStateByte("ep", key, nb)
return sdk.Success([]byte("ok"))
}
// ---- 读方法 ----
// VerifyHash 校验整剧/转码哈希是否绑定到该 MA。
func (t *TCSRegistry) VerifyHash() protogo.Response {
args := sdk.Instance.GetArgs()
bound, _ := sdk.Instance.GetStateByte("hashidx", string(args["file_hash"]))
if string(bound) == string(args["ma_code"]) {
return sdk.Success([]byte("true"))
}
return sdk.Success([]byte("false"))
}
// VerifyEpisodeHash 校验某集哈希。
func (t *TCSRegistry) VerifyEpisodeHash() protogo.Response {
args := sdk.Instance.GetArgs()
v, _ := sdk.Instance.GetStateByte("ep", string(args["ma_code"])+":"+string(args["episode"]))
if len(v) == 0 {
return sdk.Success([]byte("false"))
}
var eb epBinding
_ = json.Unmarshal(v, &eb)
if eb.HashValue == string(args["file_hash"]) {
return sdk.Success([]byte("true"))
}
return sdk.Success([]byte("false"))
}
// HashExists 返回绑定的 MA(不存在返回空)。
func (t *TCSRegistry) HashExists() protogo.Response {
bound, _ := sdk.Instance.GetStateByte("hashidx", string(sdk.Instance.GetArgs()["file_hash"]))
return sdk.Success(bound)
}
// ListEpisodes 返回某 MA 的集级绑定数组(JSON)。
func (t *TCSRegistry) ListEpisodes() protogo.Response {
maCode := string(sdk.Instance.GetArgs()["ma_code"])
n := getInt("epcount", maCode)
out := make([]epBinding, 0, n)
for i := 1; i <= n; i++ {
if v, _ := sdk.Instance.GetStateByte("ep", maCode+":"+strconv.Itoa(i)); len(v) > 0 {
var eb epBinding
_ = json.Unmarshal(v, &eb)
out = append(out, eb)
}
}
b, _ := json.Marshal(out)
return sdk.Success(b)
}
// QueryContent 查询内容主记录。
func (t *TCSRegistry) QueryContent() protogo.Response {
v, _ := sdk.Instance.GetStateByte("content", string(sdk.Instance.GetArgs()["ma_code"]))
if len(v) == 0 {
return sdk.Error("not found")
}
return sdk.Success(v)
}
// QueryMappings 返回某 MA 的全部映射(JSON {mappings:[],cdn_endpoints:[]})。
func (t *TCSRegistry) QueryMappings() protogo.Response {
maCode := string(sdk.Instance.GetArgs()["ma_code"])
n := getInt("mapcount", maCode)
maps := make([]map[string]interface{}, 0, n)
cdns := []string{}
for i := 1; i <= n; i++ {
if v, _ := sdk.Instance.GetStateByte("mapping", maCode+":"+strconv.Itoa(i)); len(v) > 0 {
var m map[string]interface{}
_ = json.Unmarshal(v, &m)
maps = append(maps, m)
if ep, ok := m["cdn_endpoint"].(string); ok && ep != "" {
cdns = append(cdns, ep)
}
}
}
b, _ := json.Marshal(map[string]interface{}{"ma_code": maCode, "mappings": maps, "cdn_endpoints": cdns})
return sdk.Success(b)
}
// ListContents 按状态返回内容数组(空状态返回全部)。
func (t *TCSRegistry) ListContents() protogo.Response {
status := string(sdk.Instance.GetArgs()["status"])
var all []string
if v, _ := sdk.Instance.GetStateByte("allmacodes", ""); len(v) > 0 {
_ = json.Unmarshal(v, &all)
}
out := make([]map[string]interface{}, 0, len(all))
for _, ma := range all {
v, _ := sdk.Instance.GetStateByte("content", ma)
if len(v) == 0 {
continue
}
var c map[string]interface{}
_ = json.Unmarshal(v, &c)
if status == "" || c["status"] == status {
out = append(out, c)
}
}
b, _ := json.Marshal(out)
return sdk.Success(b)
}
func main() {
if err := sandbox.Start(new(TCSRegistry)); err != nil {
_ = errors.New(err.Error())
}
}