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