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 契约套件均通过,行为一致
This commit is contained in:
selfrelease
2026-06-14 20:47:21 +08:00
parent 8a9ea6b40b
commit 166f460d57
9 changed files with 875 additions and 49 deletions
+287 -38
View File
@@ -1,23 +1,31 @@
// Package main 是 TCS-IPTV 可信数据空间的 ChainMaker 智能合约(Go,三期 A.2)。
// Package main 是 TCS-IPTV 可信数据空间的 ChainMaker 智能合约(Go)。
//
// 本合约为独立合约模块(独立 go.mod),按 ChainMaker docker-go/wasm 合约规范部署。
// 本合约为独立合约模块(独立 go.mod),按 ChainMaker docker-go 合约规范部署。
// 与 internal/chain.Client 接口语义一一对应;MVP/二期用 MemoryChain 等价实现,
// 具备链环境后部署本合约,由 chain-svc 通过 ChainMaker Go SDK 调用替换 MemoryChain
// 具备链环境后部署本合约,由 chain-svc / ChainMakerClient 通过 SDK 调用替换内存实现
//
// 状态键设计(KV):
//
// content:{maCode} -> Content JSON
// binding:{maCode}:{idx} -> HashBinding JSON
// 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)。
// 权限:通过 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"
@@ -28,7 +36,8 @@ import (
type TCSRegistry struct{}
const (
orgRegulator = "regulator" // 监管组织仅其可签发/下架
orgRegulator = "regulator" // 监管组织仅其可签发/下架
orgReviewer = "reviewer" // 审核组织:哈希绑定/版本变更/状态流转
)
// InitContract 合约初始化。
@@ -41,13 +50,46 @@ func (t *TCSRegistry) UpgradeContract() protogo.Response {
return sdk.Success([]byte("tcs_registry upgraded"))
}
// senderOrg 取调用方组织标识(基于证书 OU/OrgId)。
func senderOrg() string {
org, _ := sdk.Instance.GetSenderOrgId()
return org
}
// IssueMA 签发 MA 码并 1:1 强绑定哈希(仅监管组织)。
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")
@@ -56,85 +98,292 @@ func (t *TCSRegistry) IssueMA() protogo.Response {
maCode := string(args["ma_code"])
ctid := string(args["ctid"])
fileHash := string(args["file_hash"])
contentJSON := args["content"]
// MA 不可重复签发
if existing, _ := sdk.Instance.GetStateByte("content", maCode); len(existing) > 0 {
return sdk.Error("MA already issued (1:1 binding immutable)")
}
// 防换壳重发:同哈希不可绑定到不同 MA
if bound, _ := sdk.Instance.GetStateByte("hashidx", fileHash); len(bound) > 0 {
return sdk.Error("content hash already exists")
}
_ = sdk.Instance.PutStateByte("content", maCode, contentJSON)
binding := map[string]string{"hash_type": "file_sha256", "hash_value": fileHash, "version": "v1.0"}
bj, _ := json.Marshal(binding)
_ = sdk.Instance.PutStateByte("binding", maCode+":0", bj)
// 内容主记录(强制 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))
}
// RegisterMapping 注册三方编码映射(MA 必须已签发)。
func (t *TCSRegistry) RegisterMapping() protogo.Response {
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()
maCode := string(args["ma_code"])
if v, _ := sdk.Instance.GetStateByte("content", maCode); len(v) == 0 {
ctid := string(args["ctid"])
ma, _ := sdk.Instance.GetStateByte("ctid2ma", ctid)
if len(ma) == 0 {
return sdk.Error("MA not issued")
}
idx := string(args["idx"])
_ = sdk.Instance.PutStateByte("mapping", maCode+":"+idx, args["mapping"])
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"))
}
// VerifyHash 校验提交哈希是否与绑定哈希一致
func (t *TCSRegistry) VerifyHash() protogo.Response {
// RegisterMapping 注册三方编码映射;MA 必须已签发(任意角色注册本方)
func (t *TCSRegistry) RegisterMapping() protogo.Response {
args := sdk.Instance.GetArgs()
maCode := string(args["ma_code"])
fileHash := string(args["file_hash"])
bound, _ := sdk.Instance.GetStateByte("hashidx", fileHash)
if string(bound) == maCode {
return sdk.Success([]byte("true"))
ctid := string(args["ctid"])
ma, _ := sdk.Instance.GetStateByte("ctid2ma", ctid)
if len(ma) == 0 {
return sdk.Error("MA not issued")
}
return sdk.Success([]byte("false"))
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"))
}
// Revoke 下架(仅监管组织)。
// 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()
maCode := string(args["ma_code"])
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"] = "revoked"
content["status"] = status
nj, _ := json.Marshal(content)
_ = sdk.Instance.PutStateByte("content", maCode, nj)
sdk.Instance.EmitEvent("Revoked", []string{maCode})
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 {
maCode := string(sdk.Instance.GetArgs()["ma_code"])
v, _ := sdk.Instance.GetStateByte("content", maCode)
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() {
err := sandbox.Start(new(TCSRegistry))
if err != nil {
if err := sandbox.Start(new(TCSRegistry)); err != nil {
_ = errors.New(err.Error())
}
}