package native import ( "encoding/binary" "errors" "math/big" "github.com/tutus-one/tutus-chain/pkg/config" "github.com/tutus-one/tutus-chain/pkg/core/dao" "github.com/tutus-one/tutus-chain/pkg/core/interop" "github.com/tutus-one/tutus-chain/pkg/core/native/nativeids" "github.com/tutus-one/tutus-chain/pkg/core/native/nativenames" "github.com/tutus-one/tutus-chain/pkg/core/state" "github.com/tutus-one/tutus-chain/pkg/core/storage" "github.com/tutus-one/tutus-chain/pkg/crypto/hash" "github.com/tutus-one/tutus-chain/pkg/smartcontract" "github.com/tutus-one/tutus-chain/pkg/smartcontract/callflag" "github.com/tutus-one/tutus-chain/pkg/smartcontract/manifest" "github.com/tutus-one/tutus-chain/pkg/util" "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" ) // Palam represents the programmed transparency native contract. // Palam (Latin for "openly/publicly") provides role-based encrypted // transaction flows with judicial declassification. type Palam struct { interop.ContractMD Tutus ITutus Vita IVita RoleRegistry IRoleRegistry Lex ILex } // PalamCache represents the cached state for Palam contract. type PalamCache struct { flowCount uint64 requestCount uint64 logCount uint64 attachmentCount uint64 } // Storage key prefixes for Palam. const ( palamPrefixFlow byte = 0x01 // flowID -> Flow palamPrefixFlowByBucket byte = 0x02 // bucket + flowID -> exists palamPrefixFlowByParticipant byte = 0x03 // participant + flowID -> exists palamPrefixFlowByTag byte = 0x04 // tag + flowID -> exists palamPrefixRequest byte = 0x10 // requestID -> DeclassifyRequest palamPrefixRequestByFlow byte = 0x11 // flowID + requestID -> exists palamPrefixRequestByRequester byte = 0x12 // requester + requestID -> exists palamPrefixPendingRequest byte = 0x13 // flowID -> pending requestID palamPrefixAccessLog byte = 0x20 // logID -> AccessLog palamPrefixLogByFlow byte = 0x21 // flowID + logID -> exists palamPrefixLogByAccessor byte = 0x22 // accessor + logID -> exists palamPrefixAttachment byte = 0x30 // attachmentID -> FlowAttachment palamPrefixAttachmentByFlow byte = 0x31 // flowID + attachmentID -> exists palamPrefixFlowCounter byte = 0xF0 // -> uint64 palamPrefixRequestCounter byte = 0xF1 // -> next request ID palamPrefixLogCounter byte = 0xF2 // -> next log ID palamPrefixAttachmentCounter byte = 0xF3 // -> next attachment ID palamPrefixConfig byte = 0xFF // -> PalamConfig ) // Event names for Palam. const ( FlowRecordedEvent = "FlowRecorded" FlowAttachmentEvent = "FlowAttachment" FlowAccessedEvent = "FlowAccessed" DeclassifyRequestedEvent = "DeclassifyRequested" DeclassifyApprovalEvent = "DeclassifyApproval" DeclassifyGrantedEvent = "DeclassifyGranted" DeclassifyDeniedEvent = "DeclassifyDenied" DeclassifyExpiredEvent = "DeclassifyExpired" ) // Role constants for Palam (from RoleRegistry). const ( RolePalamAuditor uint64 = 25 // Can request declassification RolePalamJudge uint64 = 26 // Can approve declassification ) // Various errors for Palam. var ( ErrPalamFlowNotFound = errors.New("flow not found") ErrPalamFlowExists = errors.New("flow already exists") ErrPalamRequestNotFound = errors.New("declassify request not found") ErrPalamRequestExists = errors.New("declassify request already exists") ErrPalamNotParticipant = errors.New("caller is not a participant") ErrPalamNotAuditor = errors.New("caller is not an auditor") ErrPalamNotJudge = errors.New("caller is not a judge") ErrPalamAlreadyApproved = errors.New("already approved this request") ErrPalamRequestExpired = errors.New("request has expired") ErrPalamRequestNotPending = errors.New("request is not pending") ErrPalamRequestDenied = errors.New("request was denied") ErrPalamNoVita = errors.New("caller must have an active Vita") ErrPalamInvalidTag = errors.New("invalid tag") ErrPalamInvalidBucket = errors.New("invalid bucket") ErrPalamInvalidCaseID = errors.New("invalid case ID") ErrPalamInvalidReason = errors.New("invalid reason (minimum 50 characters)") ErrPalamCannotSelfApprove = errors.New("cannot approve own request") ErrPalamNoPermission = errors.New("no permission for this action") ErrPalamInvalidAttachment = errors.New("invalid attachment type") ErrPalamAttachmentNotFound = errors.New("attachment not found") ErrPalamNotCommittee = errors.New("invalid committee signature") ) // NewPalam creates a new Palam native contract. func NewPalam() *Palam { p := &Palam{} p.ContractMD = *interop.NewContractMD(nativenames.Palam, nativeids.Palam) defer p.BuildHFSpecificMD(p.ActiveIn()) desc := NewDescriptor("getConfig", smartcontract.ArrayType) md := NewMethodAndPrice(p.getConfig, 1<<15, callflag.ReadStates) p.AddMethod(md, desc) // ===== Flow Recording ===== desc = NewDescriptor("recordFlow", smartcontract.Hash256Type, manifest.NewParameter("tag", smartcontract.StringType), manifest.NewParameter("amount", smartcontract.IntegerType), manifest.NewParameter("bucket", smartcontract.StringType), manifest.NewParameter("participants", smartcontract.ArrayType), manifest.NewParameter("consumerData", smartcontract.ByteArrayType), manifest.NewParameter("merchantData", smartcontract.ByteArrayType), manifest.NewParameter("distributorData", smartcontract.ByteArrayType), manifest.NewParameter("producerData", smartcontract.ByteArrayType), manifest.NewParameter("ngoData", smartcontract.ByteArrayType), manifest.NewParameter("auditorData", smartcontract.ByteArrayType), manifest.NewParameter("previousFlowID", smartcontract.Hash256Type)) md = NewMethodAndPrice(p.recordFlow, 1<<17, callflag.States|callflag.AllowNotify) p.AddMethod(md, desc) desc = NewDescriptor("attachToFlow", smartcontract.IntegerType, manifest.NewParameter("flowID", smartcontract.Hash256Type), manifest.NewParameter("attachmentType", smartcontract.StringType), manifest.NewParameter("encryptedData", smartcontract.ByteArrayType)) md = NewMethodAndPrice(p.attachToFlow, 1<<16, callflag.States|callflag.AllowNotify) p.AddMethod(md, desc) // ===== Flow Queries ===== desc = NewDescriptor("getFlow", smartcontract.ArrayType, manifest.NewParameter("flowID", smartcontract.Hash256Type)) md = NewMethodAndPrice(p.getFlow, 1<<15, callflag.ReadStates|callflag.AllowNotify) p.AddMethod(md, desc) desc = NewDescriptor("getTotalFlows", smartcontract.IntegerType) md = NewMethodAndPrice(p.getTotalFlows, 1<<15, callflag.ReadStates) p.AddMethod(md, desc) desc = NewDescriptor("getAttachment", smartcontract.ArrayType, manifest.NewParameter("attachmentID", smartcontract.IntegerType)) md = NewMethodAndPrice(p.getAttachment, 1<<15, callflag.ReadStates) p.AddMethod(md, desc) desc = NewDescriptor("getTotalAttachments", smartcontract.IntegerType) md = NewMethodAndPrice(p.getTotalAttachments, 1<<15, callflag.ReadStates) p.AddMethod(md, desc) // ===== Declassification ===== desc = NewDescriptor("requestDeclassify", smartcontract.IntegerType, manifest.NewParameter("flowID", smartcontract.Hash256Type), manifest.NewParameter("caseID", smartcontract.StringType), manifest.NewParameter("reason", smartcontract.StringType), manifest.NewParameter("expirationBlocks", smartcontract.IntegerType)) md = NewMethodAndPrice(p.requestDeclassify, 1<<17, callflag.States|callflag.AllowNotify) p.AddMethod(md, desc) desc = NewDescriptor("approveDeclassify", smartcontract.BoolType, manifest.NewParameter("requestID", smartcontract.IntegerType)) md = NewMethodAndPrice(p.approveDeclassify, 1<<16, callflag.States|callflag.AllowNotify) p.AddMethod(md, desc) desc = NewDescriptor("denyDeclassify", smartcontract.BoolType, manifest.NewParameter("requestID", smartcontract.IntegerType), manifest.NewParameter("reason", smartcontract.StringType)) md = NewMethodAndPrice(p.denyDeclassify, 1<<16, callflag.States|callflag.AllowNotify) p.AddMethod(md, desc) desc = NewDescriptor("getDeclassifyRequest", smartcontract.ArrayType, manifest.NewParameter("requestID", smartcontract.IntegerType)) md = NewMethodAndPrice(p.getDeclassifyRequest, 1<<15, callflag.ReadStates) p.AddMethod(md, desc) desc = NewDescriptor("getTotalRequests", smartcontract.IntegerType) md = NewMethodAndPrice(p.getTotalRequests, 1<<15, callflag.ReadStates) p.AddMethod(md, desc) desc = NewDescriptor("hasDeclassifyGrant", smartcontract.BoolType, manifest.NewParameter("flowID", smartcontract.Hash256Type), manifest.NewParameter("requester", smartcontract.Hash160Type)) md = NewMethodAndPrice(p.hasDeclassifyGrant, 1<<15, callflag.ReadStates) p.AddMethod(md, desc) // ===== Access Logging ===== desc = NewDescriptor("getAccessLog", smartcontract.ArrayType, manifest.NewParameter("logID", smartcontract.IntegerType)) md = NewMethodAndPrice(p.getAccessLog, 1<<15, callflag.ReadStates) p.AddMethod(md, desc) desc = NewDescriptor("getTotalLogs", smartcontract.IntegerType) md = NewMethodAndPrice(p.getTotalLogs, 1<<15, callflag.ReadStates) p.AddMethod(md, desc) // ===== Role & Permissions ===== desc = NewDescriptor("getRolePermissions", smartcontract.ArrayType, manifest.NewParameter("role", smartcontract.IntegerType)) md = NewMethodAndPrice(p.getRolePermissions, 1<<15, callflag.ReadStates) p.AddMethod(md, desc) return p } // Metadata returns the metadata of the contract. func (p *Palam) Metadata() *interop.ContractMD { return &p.ContractMD } // Initialize initializes the Palam contract. func (p *Palam) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { if hf != p.ActiveIn() { return nil } // Initialize counters p.setFlowCounter(ic.DAO, 0) p.setRequestCounter(ic.DAO, 0) p.setLogCounter(ic.DAO, 0) p.setAttachmentCounter(ic.DAO, 0) // Set default config cfg := state.DefaultPalamConfig() cfgItem := cfg.ToStackItem() cfgBytes, err := ic.DAO.GetItemCtx().Serialize(cfgItem, false) if err != nil { return err } ic.DAO.PutStorageItem(p.ID, []byte{palamPrefixConfig}, cfgBytes) return nil } // InitializeCache initializes the contract cache. func (p *Palam) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { return nil // No cache needed - use storage directly } // ActiveIn returns the hardfork at which the contract becomes active. func (p *Palam) ActiveIn() *config.Hardfork { return nil // Always active } // Address returns the contract's script hash. func (p *Palam) Address() util.Uint160 { return p.Hash } // GetFlowInternal returns a flow by ID (for cross-contract use). func (p *Palam) GetFlowInternal(d *dao.Simple, flowID util.Uint256) *state.Flow { return p.getFlowInternal(d, flowID) } // HasDeclassifyGrant checks if requester has declassify grant for a flow (for cross-contract use). func (p *Palam) HasDeclassifyGrant(d *dao.Simple, flowID util.Uint256, requester util.Uint160) bool { return p.hasGrantInternal(d, flowID, requester) } // OnPersist implements the Contract interface. func (p *Palam) OnPersist(ic *interop.Context) error { return nil } // PostPersist implements the Contract interface. func (p *Palam) PostPersist(ic *interop.Context) error { return nil } // ===== Configuration ===== func (p *Palam) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item { cfg := p.getConfigInternal(ic.DAO) return cfg.ToStackItem() } func (p *Palam) getConfigInternal(d *dao.Simple) *state.PalamConfig { si := d.GetStorageItem(p.ID, []byte{palamPrefixConfig}) if si == nil { return state.DefaultPalamConfig() } item, err := stackitem.Deserialize(si) if err != nil { return state.DefaultPalamConfig() } cfg := &state.PalamConfig{} if err := cfg.FromStackItem(item); err != nil { return state.DefaultPalamConfig() } return cfg } // ===== Counter Helpers ===== func (p *Palam) getFlowCounter(d *dao.Simple) uint64 { si := d.GetStorageItem(p.ID, []byte{palamPrefixFlowCounter}) if si == nil || len(si) == 0 { return 0 } return binary.LittleEndian.Uint64(si) } func (p *Palam) setFlowCounter(d *dao.Simple, count uint64) { data := make([]byte, 8) binary.LittleEndian.PutUint64(data, count) d.PutStorageItem(p.ID, []byte{palamPrefixFlowCounter}, data) } func (p *Palam) getRequestCounter(d *dao.Simple) uint64 { si := d.GetStorageItem(p.ID, []byte{palamPrefixRequestCounter}) if si == nil || len(si) == 0 { return 0 } return binary.LittleEndian.Uint64(si) } func (p *Palam) setRequestCounter(d *dao.Simple, count uint64) { data := make([]byte, 8) binary.LittleEndian.PutUint64(data, count) d.PutStorageItem(p.ID, []byte{palamPrefixRequestCounter}, data) } func (p *Palam) getNextRequestID(d *dao.Simple) uint64 { id := p.getRequestCounter(d) + 1 p.setRequestCounter(d, id) return id } func (p *Palam) getLogCounter(d *dao.Simple) uint64 { si := d.GetStorageItem(p.ID, []byte{palamPrefixLogCounter}) if si == nil || len(si) == 0 { return 0 } return binary.LittleEndian.Uint64(si) } func (p *Palam) setLogCounter(d *dao.Simple, count uint64) { data := make([]byte, 8) binary.LittleEndian.PutUint64(data, count) d.PutStorageItem(p.ID, []byte{palamPrefixLogCounter}, data) } func (p *Palam) getNextLogID(d *dao.Simple) uint64 { id := p.getLogCounter(d) + 1 p.setLogCounter(d, id) return id } func (p *Palam) getAttachmentCounter(d *dao.Simple) uint64 { si := d.GetStorageItem(p.ID, []byte{palamPrefixAttachmentCounter}) if si == nil || len(si) == 0 { return 0 } return binary.LittleEndian.Uint64(si) } func (p *Palam) setAttachmentCounter(d *dao.Simple, count uint64) { data := make([]byte, 8) binary.LittleEndian.PutUint64(data, count) d.PutStorageItem(p.ID, []byte{palamPrefixAttachmentCounter}, data) } func (p *Palam) getNextAttachmentID(d *dao.Simple) uint64 { id := p.getAttachmentCounter(d) + 1 p.setAttachmentCounter(d, id) return id } // ===== Flow Recording ===== func (p *Palam) recordFlow(ic *interop.Context, args []stackitem.Item) stackitem.Item { tag, err := stackitem.ToString(args[0]) if err != nil || len(tag) == 0 || len(tag) > 64 { panic(ErrPalamInvalidTag) } amountBI, err := args[1].TryInteger() if err != nil { panic(err) } amount := amountBI.Uint64() bucket, err := stackitem.ToString(args[2]) if err != nil || len(bucket) == 0 || len(bucket) > 32 { panic(ErrPalamInvalidBucket) } participantsArr, ok := args[3].Value().([]stackitem.Item) if !ok { panic("invalid participants array") } participants := make([]util.Uint160, len(participantsArr)) for i, p := range participantsArr { pBytes, err := p.TryBytes() if err != nil { panic(err) } participants[i], err = util.Uint160DecodeBytesBE(pBytes) if err != nil { panic(err) } } consumerData, err := args[4].TryBytes() if err != nil { panic(err) } merchantData, err := args[5].TryBytes() if err != nil { panic(err) } distributorData, err := args[6].TryBytes() if err != nil { panic(err) } producerData, err := args[7].TryBytes() if err != nil { panic(err) } ngoData, err := args[8].TryBytes() if err != nil { panic(err) } auditorData, err := args[9].TryBytes() if err != nil { panic(err) } prevFlowBytes, err := args[10].TryBytes() if err != nil { panic(err) } var previousFlowID util.Uint256 if len(prevFlowBytes) == 32 { previousFlowID, err = util.Uint256DecodeBytesBE(prevFlowBytes) if err != nil { panic(err) } } caller := ic.VM.GetCallingScriptHash() // Verify caller has Vita if p.Vita != nil && !p.Vita.TokenExists(ic.DAO, caller) { panic(ErrPalamNoVita) } // Generate flow ID from content hash flowData := append([]byte(tag), []byte(bucket)...) flowData = append(flowData, caller.BytesBE()...) flowData = append(flowData, consumerData...) flowData = append(flowData, merchantData...) blockBytes := make([]byte, 4) binary.LittleEndian.PutUint32(blockBytes, ic.Block.Index) flowData = append(flowData, blockBytes...) flowID := hash.Sha256(flowData) // Check if flow already exists if p.getFlowInternal(ic.DAO, flowID) != nil { panic(ErrPalamFlowExists) } flow := &state.Flow{ FlowID: flowID, Bucket: bucket, Tag: tag, Amount: amount, Timestamp: ic.Block.Index, Creator: caller, ConsumerData: consumerData, MerchantData: merchantData, DistributorData: distributorData, ProducerData: producerData, NGOData: ngoData, AuditorData: auditorData, Participants: participants, PreviousFlowID: previousFlowID, Status: state.FlowStatusActive, } // Store flow p.putFlow(ic.DAO, flow) // Update counter count := p.getFlowCounter(ic.DAO) + 1 p.setFlowCounter(ic.DAO, count) // Emit event ic.AddNotification(p.Hash, FlowRecordedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(flowID.BytesBE()), stackitem.NewByteArray([]byte(tag)), stackitem.NewByteArray([]byte(bucket)), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ic.Block.Index))), })) return stackitem.NewByteArray(flowID.BytesBE()) } func (p *Palam) putFlow(d *dao.Simple, flow *state.Flow) { flowItem := flow.ToStackItem() flowBytes, err := d.GetItemCtx().Serialize(flowItem, false) if err != nil { panic(err) } // Store main record key := append([]byte{palamPrefixFlow}, flow.FlowID.BytesBE()...) d.PutStorageItem(p.ID, key, flowBytes) // Index by bucket bucketKey := append([]byte{palamPrefixFlowByBucket}, []byte(flow.Bucket)...) bucketKey = append(bucketKey, flow.FlowID.BytesBE()...) d.PutStorageItem(p.ID, bucketKey, []byte{1}) // Index by tag tagKey := append([]byte{palamPrefixFlowByTag}, []byte(flow.Tag)...) tagKey = append(tagKey, flow.FlowID.BytesBE()...) d.PutStorageItem(p.ID, tagKey, []byte{1}) // Index by participants for _, participant := range flow.Participants { partKey := append([]byte{palamPrefixFlowByParticipant}, participant.BytesBE()...) partKey = append(partKey, flow.FlowID.BytesBE()...) d.PutStorageItem(p.ID, partKey, []byte{1}) } } func (p *Palam) getFlowInternal(d *dao.Simple, flowID util.Uint256) *state.Flow { key := append([]byte{palamPrefixFlow}, flowID.BytesBE()...) si := d.GetStorageItem(p.ID, key) if si == nil { return nil } item, err := stackitem.Deserialize(si) if err != nil { return nil } flow := &state.Flow{} if err := flow.FromStackItem(item); err != nil { return nil } return flow } func (p *Palam) getFlow(ic *interop.Context, args []stackitem.Item) stackitem.Item { flowIDBytes, err := args[0].TryBytes() if err != nil { panic(err) } flowID, err := util.Uint256DecodeBytesBE(flowIDBytes) if err != nil { panic(err) } flow := p.getFlowInternal(ic.DAO, flowID) if flow == nil { return stackitem.Null{} } // Log access if configured cfg := p.getConfigInternal(ic.DAO) if cfg.LogAllAccess { caller := ic.VM.GetCallingScriptHash() p.logAccess(ic, flowID, caller, state.AccessTypeView, "getFlow") } return flow.ToStackItem() } func (p *Palam) getTotalFlows(ic *interop.Context, _ []stackitem.Item) stackitem.Item { return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getFlowCounter(ic.DAO))) } // ===== Attachments ===== func (p *Palam) attachToFlow(ic *interop.Context, args []stackitem.Item) stackitem.Item { flowIDBytes, err := args[0].TryBytes() if err != nil { panic(err) } flowID, err := util.Uint256DecodeBytesBE(flowIDBytes) if err != nil { panic(err) } attachmentType, err := stackitem.ToString(args[1]) if err != nil || len(attachmentType) == 0 || len(attachmentType) > 64 { panic(ErrPalamInvalidAttachment) } encryptedData, err := args[2].TryBytes() if err != nil { panic(err) } caller := ic.VM.GetCallingScriptHash() // Verify flow exists flow := p.getFlowInternal(ic.DAO, flowID) if flow == nil { panic(ErrPalamFlowNotFound) } // Verify caller is a participant isParticipant := false for _, p := range flow.Participants { if p.Equals(caller) { isParticipant = true break } } if !isParticipant && !flow.Creator.Equals(caller) { panic(ErrPalamNotParticipant) } attachmentID := p.getNextAttachmentID(ic.DAO) attachment := &state.FlowAttachment{ AttachmentID: attachmentID, FlowID: flowID, AttachmentType: attachmentType, EncryptedData: encryptedData, Attacher: caller, AttachedAt: ic.Block.Index, } p.putAttachment(ic.DAO, attachment) // Emit event ic.AddNotification(p.Hash, FlowAttachmentEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(flowID.BytesBE()), stackitem.NewBigInteger(new(big.Int).SetUint64(attachmentID)), stackitem.NewByteArray([]byte(attachmentType)), })) return stackitem.NewBigInteger(new(big.Int).SetUint64(attachmentID)) } func (p *Palam) putAttachment(d *dao.Simple, att *state.FlowAttachment) { attItem := att.ToStackItem() attBytes, err := d.GetItemCtx().Serialize(attItem, false) if err != nil { panic(err) } idBytes := make([]byte, 8) binary.LittleEndian.PutUint64(idBytes, att.AttachmentID) // Store main record key := append([]byte{palamPrefixAttachment}, idBytes...) d.PutStorageItem(p.ID, key, attBytes) // Index by flow flowKey := append([]byte{palamPrefixAttachmentByFlow}, att.FlowID.BytesBE()...) flowKey = append(flowKey, idBytes...) d.PutStorageItem(p.ID, flowKey, []byte{1}) } func (p *Palam) getAttachmentInternal(d *dao.Simple, attachmentID uint64) *state.FlowAttachment { idBytes := make([]byte, 8) binary.LittleEndian.PutUint64(idBytes, attachmentID) key := append([]byte{palamPrefixAttachment}, idBytes...) si := d.GetStorageItem(p.ID, key) if si == nil { return nil } item, err := stackitem.Deserialize(si) if err != nil { return nil } att := &state.FlowAttachment{} if err := att.FromStackItem(item); err != nil { return nil } return att } func (p *Palam) getAttachment(ic *interop.Context, args []stackitem.Item) stackitem.Item { attachmentID, err := args[0].TryInteger() if err != nil { panic(err) } att := p.getAttachmentInternal(ic.DAO, attachmentID.Uint64()) if att == nil { return stackitem.Null{} } return att.ToStackItem() } func (p *Palam) getTotalAttachments(ic *interop.Context, _ []stackitem.Item) stackitem.Item { return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getAttachmentCounter(ic.DAO))) } // ===== Declassification ===== func (p *Palam) requestDeclassify(ic *interop.Context, args []stackitem.Item) stackitem.Item { flowIDBytes, err := args[0].TryBytes() if err != nil { panic(err) } flowID, err := util.Uint256DecodeBytesBE(flowIDBytes) if err != nil { panic(err) } caseID, err := stackitem.ToString(args[1]) if err != nil || len(caseID) == 0 || len(caseID) > 64 { panic(ErrPalamInvalidCaseID) } reason, err := stackitem.ToString(args[2]) if err != nil || len(reason) < 50 || len(reason) > 1024 { panic(ErrPalamInvalidReason) } expirationBlocks, err := args[3].TryInteger() if err != nil { panic(err) } expBlocks := uint32(expirationBlocks.Uint64()) caller := ic.VM.GetCallingScriptHash() // Verify flow exists flow := p.getFlowInternal(ic.DAO, flowID) if flow == nil { panic(ErrPalamFlowNotFound) } // Verify caller is an auditor if p.RoleRegistry != nil { if !p.RoleRegistry.HasRoleInternal(ic.DAO, caller, RolePalamAuditor, ic.Block.Index) { panic(ErrPalamNotAuditor) } } // Get config for defaults cfg := p.getConfigInternal(ic.DAO) if expBlocks == 0 { expBlocks = cfg.DefaultExpiration } requestID := p.getNextRequestID(ic.DAO) request := &state.DeclassifyRequest{ RequestID: requestID, FlowID: flowID, CaseID: caseID, Reason: reason, Requester: caller, RequesterRole: state.PalamRoleAuditor, RequiredApprovals: cfg.MinApprovals, Approvals: []util.Uint160{}, ApprovalTimes: []uint32{}, Status: state.DeclassifyPending, CreatedAt: ic.Block.Index, ExpiresAt: ic.Block.Index + expBlocks, GrantedAt: 0, } p.putDeclassifyRequest(ic.DAO, request) // Emit event ic.AddNotification(p.Hash, DeclassifyRequestedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(requestID)), stackitem.NewByteArray(flowID.BytesBE()), stackitem.NewByteArray([]byte(caseID)), stackitem.NewByteArray(caller.BytesBE()), })) return stackitem.NewBigInteger(new(big.Int).SetUint64(requestID)) } func (p *Palam) putDeclassifyRequest(d *dao.Simple, req *state.DeclassifyRequest) { reqItem := req.ToStackItem() reqBytes, err := d.GetItemCtx().Serialize(reqItem, false) if err != nil { panic(err) } idBytes := make([]byte, 8) binary.LittleEndian.PutUint64(idBytes, req.RequestID) // Store main record key := append([]byte{palamPrefixRequest}, idBytes...) d.PutStorageItem(p.ID, key, reqBytes) // Index by flow flowKey := append([]byte{palamPrefixRequestByFlow}, req.FlowID.BytesBE()...) flowKey = append(flowKey, idBytes...) d.PutStorageItem(p.ID, flowKey, []byte{1}) // Index by requester reqKey := append([]byte{palamPrefixRequestByRequester}, req.Requester.BytesBE()...) reqKey = append(reqKey, idBytes...) d.PutStorageItem(p.ID, reqKey, []byte{1}) // Track pending request for flow if req.Status == state.DeclassifyPending { pendingKey := append([]byte{palamPrefixPendingRequest}, req.FlowID.BytesBE()...) d.PutStorageItem(p.ID, pendingKey, idBytes) } } func (p *Palam) getRequestInternal(d *dao.Simple, requestID uint64) *state.DeclassifyRequest { idBytes := make([]byte, 8) binary.LittleEndian.PutUint64(idBytes, requestID) key := append([]byte{palamPrefixRequest}, idBytes...) si := d.GetStorageItem(p.ID, key) if si == nil { return nil } item, err := stackitem.Deserialize(si) if err != nil { return nil } req := &state.DeclassifyRequest{} if err := req.FromStackItem(item); err != nil { return nil } return req } func (p *Palam) approveDeclassify(ic *interop.Context, args []stackitem.Item) stackitem.Item { requestID, err := args[0].TryInteger() if err != nil { panic(err) } caller := ic.VM.GetCallingScriptHash() // Verify caller is a judge if p.RoleRegistry != nil { if !p.RoleRegistry.HasRoleInternal(ic.DAO, caller, RolePalamJudge, ic.Block.Index) { panic(ErrPalamNotJudge) } } req := p.getRequestInternal(ic.DAO, requestID.Uint64()) if req == nil { panic(ErrPalamRequestNotFound) } // Check request is pending if req.Status != state.DeclassifyPending { panic(ErrPalamRequestNotPending) } // Check not expired if ic.Block.Index > req.ExpiresAt { req.Status = state.DeclassifyExpired p.putDeclassifyRequest(ic.DAO, req) ic.AddNotification(p.Hash, DeclassifyExpiredEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)), })) panic(ErrPalamRequestExpired) } // Cannot approve own request if caller.Equals(req.Requester) { panic(ErrPalamCannotSelfApprove) } // Check not already approved by this caller for _, approver := range req.Approvals { if approver.Equals(caller) { panic(ErrPalamAlreadyApproved) } } // Add approval req.Approvals = append(req.Approvals, caller) req.ApprovalTimes = append(req.ApprovalTimes, ic.Block.Index) // Check if enough approvals if uint32(len(req.Approvals)) >= req.RequiredApprovals { req.Status = state.DeclassifyApproved req.GrantedAt = ic.Block.Index // Emit granted event ic.AddNotification(p.Hash, DeclassifyGrantedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)), stackitem.NewByteArray(req.FlowID.BytesBE()), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(req.GrantedAt))), })) } p.putDeclassifyRequest(ic.DAO, req) // Emit approval event ic.AddNotification(p.Hash, DeclassifyApprovalEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)), stackitem.NewByteArray(caller.BytesBE()), stackitem.NewBigInteger(new(big.Int).SetInt64(int64(len(req.Approvals)))), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(req.RequiredApprovals))), })) return stackitem.NewBool(true) } func (p *Palam) denyDeclassify(ic *interop.Context, args []stackitem.Item) stackitem.Item { requestID, err := args[0].TryInteger() if err != nil { panic(err) } reason, err := stackitem.ToString(args[1]) if err != nil { panic(err) } caller := ic.VM.GetCallingScriptHash() // Verify caller is a judge if p.RoleRegistry != nil { if !p.RoleRegistry.HasRoleInternal(ic.DAO, caller, RolePalamJudge, ic.Block.Index) { panic(ErrPalamNotJudge) } } req := p.getRequestInternal(ic.DAO, requestID.Uint64()) if req == nil { panic(ErrPalamRequestNotFound) } if req.Status != state.DeclassifyPending { panic(ErrPalamRequestNotPending) } req.Status = state.DeclassifyDenied p.putDeclassifyRequest(ic.DAO, req) // Emit denied event ic.AddNotification(p.Hash, DeclassifyDeniedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)), stackitem.NewByteArray(caller.BytesBE()), stackitem.NewByteArray([]byte(reason)), })) return stackitem.NewBool(true) } func (p *Palam) getDeclassifyRequest(ic *interop.Context, args []stackitem.Item) stackitem.Item { requestID, err := args[0].TryInteger() if err != nil { panic(err) } req := p.getRequestInternal(ic.DAO, requestID.Uint64()) if req == nil { return stackitem.Null{} } return req.ToStackItem() } func (p *Palam) getTotalRequests(ic *interop.Context, _ []stackitem.Item) stackitem.Item { return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getRequestCounter(ic.DAO))) } func (p *Palam) hasDeclassifyGrant(ic *interop.Context, args []stackitem.Item) stackitem.Item { flowIDBytes, err := args[0].TryBytes() if err != nil { panic(err) } flowID, err := util.Uint256DecodeBytesBE(flowIDBytes) if err != nil { panic(err) } requesterBytes, err := args[1].TryBytes() if err != nil { panic(err) } requester, err := util.Uint160DecodeBytesBE(requesterBytes) if err != nil { panic(err) } // Search for approved request by this requester for this flow return stackitem.NewBool(p.hasGrantInternal(ic.DAO, flowID, requester)) } func (p *Palam) hasGrantInternal(d *dao.Simple, flowID util.Uint256, requester util.Uint160) bool { // Iterate through requests for this flow prefix := append([]byte{palamPrefixRequestByFlow}, flowID.BytesBE()...) d.Seek(p.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) < 8 { return true } requestID := binary.LittleEndian.Uint64(k[len(k)-8:]) req := p.getRequestInternal(d, requestID) if req != nil && req.Status == state.DeclassifyApproved && req.Requester.Equals(requester) { return false // Found a grant } return true }) // Note: This is a simplification - in production would track this more efficiently return false } // ===== Access Logging ===== func (p *Palam) logAccess(ic *interop.Context, flowID util.Uint256, accessor util.Uint160, accessType state.AccessType, details string) { logID := p.getNextLogID(ic.DAO) log := &state.AccessLog{ LogID: logID, FlowID: flowID, Accessor: accessor, AccessType: accessType, Timestamp: ic.Block.Index, Details: details, } p.putAccessLog(ic.DAO, log) // Emit event ic.AddNotification(p.Hash, FlowAccessedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(flowID.BytesBE()), stackitem.NewByteArray(accessor.BytesBE()), stackitem.NewByteArray([]byte(details)), })) } func (p *Palam) putAccessLog(d *dao.Simple, log *state.AccessLog) { logItem := log.ToStackItem() logBytes, err := d.GetItemCtx().Serialize(logItem, false) if err != nil { panic(err) } idBytes := make([]byte, 8) binary.LittleEndian.PutUint64(idBytes, log.LogID) // Store main record key := append([]byte{palamPrefixAccessLog}, idBytes...) d.PutStorageItem(p.ID, key, logBytes) // Index by flow flowKey := append([]byte{palamPrefixLogByFlow}, log.FlowID.BytesBE()...) flowKey = append(flowKey, idBytes...) d.PutStorageItem(p.ID, flowKey, []byte{1}) // Index by accessor accKey := append([]byte{palamPrefixLogByAccessor}, log.Accessor.BytesBE()...) accKey = append(accKey, idBytes...) d.PutStorageItem(p.ID, accKey, []byte{1}) } func (p *Palam) getAccessLogInternal(d *dao.Simple, logID uint64) *state.AccessLog { idBytes := make([]byte, 8) binary.LittleEndian.PutUint64(idBytes, logID) key := append([]byte{palamPrefixAccessLog}, idBytes...) si := d.GetStorageItem(p.ID, key) if si == nil { return nil } item, err := stackitem.Deserialize(si) if err != nil { return nil } log := &state.AccessLog{} if err := log.FromStackItem(item); err != nil { return nil } return log } func (p *Palam) getAccessLog(ic *interop.Context, args []stackitem.Item) stackitem.Item { logID, err := args[0].TryInteger() if err != nil { panic(err) } log := p.getAccessLogInternal(ic.DAO, logID.Uint64()) if log == nil { return stackitem.Null{} } return log.ToStackItem() } func (p *Palam) getTotalLogs(ic *interop.Context, _ []stackitem.Item) stackitem.Item { return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getLogCounter(ic.DAO))) } // ===== Role Permissions ===== func (p *Palam) getRolePermissions(ic *interop.Context, args []stackitem.Item) stackitem.Item { roleInt, err := args[0].TryInteger() if err != nil { panic(err) } role := state.PalamRole(roleInt.Uint64()) permissions := state.DefaultRolePermissions(role) return permissions.ToStackItem() }