166f460d57
- 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 契约套件均通过,行为一致
390 lines
12 KiB
Go
390 lines
12 KiB
Go
// 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())
|
||
}
|
||
}
|