// Package main 是 TCS-IPTV 可信数据空间的 ChainMaker 智能合约(Go)。 // // 本合约为独立合约模块(独立 go.mod),按 ChainMaker docker-go 合约规范部署。 // 与 internal/chain.Client 接口语义一一对应;MVP/二期用 MemoryChain 等价实现, // 具备链环境后部署本合约,由 chain-svc / ChainMakerClient 通过 SDK 调用替换内存实现。 // // 状态键设计(KV): // // content:{ccCode} -> Content JSON(含 status) // binding:{ccCode}:0 -> 整剧 file 哈希绑定 JSON // binding:{ccCode}:p -> 感知哈希绑定 JSON // ep:{ccCode}:{n} -> 集级绑定 JSON {episode,hash,revoked,reason} // epcount:{ccCode} -> 集数 N // hashidx:{fileHash} -> ccCode(防换壳重发) // mapping:{ccCode}:{idx} -> Mapping JSON // mapcount:{ccCode} -> 映射数 N // version:{ccCode}:{idx} -> VersionChange JSON // vercount:{ccCode} -> 版本变更数 N // ctid2ma:{ctid} -> ccCode // allcccodes -> []ccCode(供 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"` } // appendCCCode 把新发码加入全局列表(供 ListContents 遍历)。 func appendCCCode(ccCode string) { var all []string if v, _ := sdk.Instance.GetStateByte("allcccodes", ""); len(v) > 0 { _ = json.Unmarshal(v, &all) } all = append(all, ccCode) b, _ := json.Marshal(all) _ = sdk.Instance.PutStateByte("allcccodes", "", 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() ccCode := string(args["ma_code"]) ctid := string(args["ctid"]) fileHash := string(args["file_hash"]) if existing, _ := sdk.Instance.GetStateByte("content", ccCode); 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"] = ccCode content["content_twin_id"] = ctid content["status"] = "approved" cj, _ := json.Marshal(content) _ = sdk.Instance.PutStateByte("content", ccCode, cj) // 整剧 file 绑定 + 感知哈希绑定 fb, _ := json.Marshal(epBinding{Episode: 0, HashValue: fileHash}) _ = sdk.Instance.PutStateByte("binding", ccCode+":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", ccCode+":p", pb) } _ = sdk.Instance.PutStateByte("hashidx", fileHash, []byte(ccCode)) _ = sdk.Instance.PutStateByte("ctid2ma", ctid, []byte(ccCode)) // 集级哈希 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", ccCode+":"+strconv.Itoa(ep), eb) if exist, _ := sdk.Instance.GetStateByte("hashidx", hv); len(exist) == 0 { _ = sdk.Instance.PutStateByte("hashidx", hv, []byte(ccCode)) } if ep > n { n = ep } } putInt("epcount", ccCode, n) appendCCCode(ccCode) sdk.Instance.EmitEvent("RegisterSuccess", []string{ccCode, fileHash}) return sdk.Success([]byte(ccCode)) } 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(ccCode, status, event string) protogo.Response { cj, _ := sdk.Instance.GetStateByte("content", ccCode) 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", ccCode, nj) sdk.Instance.EmitEvent(event, []string{ccCode, 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(ccCode, epStr string, revoked bool, reason string) protogo.Response { key := ccCode + ":" + 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 { ccCode := string(sdk.Instance.GetArgs()["ma_code"]) n := getInt("epcount", ccCode) out := make([]epBinding, 0, n) for i := 1; i <= n; i++ { if v, _ := sdk.Instance.GetStateByte("ep", ccCode+":"+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 { ccCode := string(sdk.Instance.GetArgs()["ma_code"]) n := getInt("mapcount", ccCode) maps := make([]map[string]interface{}, 0, n) cdns := []string{} for i := 1; i <= n; i++ { if v, _ := sdk.Instance.GetStateByte("mapping", ccCode+":"+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": ccCode, "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("allcccodes", ""); 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()) } }