From 8ef02620ea717cc48ba8b5f918838af28b4eb6f0 Mon Sep 17 00:00:00 2001 From: Tutus Development Date: Sat, 20 Dec 2025 09:21:25 +0000 Subject: [PATCH] Add Palam native contract for programmed transparency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement transparent ledger infrastructure for accountable governance: - TransactionFlow tracking: Multi-party flows with complete audit trails - Full lifecycle: Initiation -> Intermediate steps -> Completion - Participant verification via Vita token (one-person-one-identity) - Immutable flow records with timestamp chains - Encrypted Payload System: Role-based access to sensitive data - Payloads encrypted per-role (Consumer, Merchant, Bank, etc.) - Keys distributed to authorized parties only - Supports judicial declassification process - Declassification Requests: Due process for accessing protected data - Judicial authority required (RolePalamJudge = 26) - Multi-party approval workflow - Configurable approval thresholds and expiry - Audit Log System: Complete transparency for oversight - All operations logged with actor attribution - Auditor access via RolePalamAuditor (25) - Immutable chronological records - Cross-contract integration: - Vita: Identity verification for participants - RoleRegistry: Role-based authorization - Lex: Rights enforcement for privacy protections Latin naming: Palam = "openly/publicly" - reflecting the contract's purpose of enabling transparent governance while protecting legitimate privacy through programmed disclosure rules. Contract ID: -23 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- pkg/core/native/contract.go | 25 + pkg/core/native/nativehashes/hashes.go | 2 + pkg/core/native/nativeids/ids.go | 2 + pkg/core/native/nativenames/names.go | 9 +- pkg/core/native/palam.go | 1127 ++++++++++++++++++++++++ pkg/core/state/palam.go | 713 +++++++++++++++ 6 files changed, 1875 insertions(+), 3 deletions(-) create mode 100644 pkg/core/native/palam.go create mode 100644 pkg/core/state/palam.go diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 9756093..7367ee6 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -292,6 +292,17 @@ type ( // Address returns the contract's script hash. Address() util.Uint160 } + + // IPalam provides programmed transparency with role-based encrypted payloads. + IPalam interface { + interop.Contract + // GetFlowInternal returns a flow by ID. + GetFlowInternal(d *dao.Simple, flowID util.Uint256) *state.Flow + // HasDeclassifyGrant checks if requester has declassify grant for a flow. + HasDeclassifyGrant(d *dao.Simple, flowID util.Uint256, requester util.Uint160) bool + // Address returns the contract's script hash. + Address() util.Uint160 + } ) // Contracts is a convenient wrapper around an arbitrary set of native contracts @@ -479,6 +490,12 @@ func (cs *Contracts) Opus() IOpus { return cs.ByName(nativenames.Opus).(IOpus) } +// Palam returns native IPalam contract implementation. It panics if +// there's no contract with proper name in cs. +func (cs *Contracts) Palam() IPalam { + return cs.ByName(nativenames.Palam).(IPalam) +} + // NewDefaultContracts returns a new set of default native contracts. func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract { mgmt := NewManagement() @@ -610,6 +627,13 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract { opus.Lex = lex opus.Treasury = treasury + // Create Palam (Programmed Transparency) contract + palam := NewPalam() + palam.NEO = neo + palam.Vita = vita + palam.RoleRegistry = roleRegistry + palam.Lex = lex + return []interop.Contract{ mgmt, s, @@ -633,5 +657,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract { sese, tribute, opus, + palam, } } diff --git a/pkg/core/native/nativehashes/hashes.go b/pkg/core/native/nativehashes/hashes.go index 863f336..707dac4 100644 --- a/pkg/core/native/nativehashes/hashes.go +++ b/pkg/core/native/nativehashes/hashes.go @@ -53,4 +53,6 @@ var ( Tribute = util.Uint160{0x35, 0x9b, 0xbf, 0x51, 0xb5, 0xe3, 0x73, 0x6d, 0x52, 0xad, 0x80, 0xb0, 0x28, 0x4f, 0x22, 0x8f, 0x9, 0xaf, 0x49, 0x1e} // Opus is a hash of native Opus contract. Opus = util.Uint160{0xf5, 0x22, 0x73, 0x17, 0x98, 0xab, 0xfc, 0x4d, 0x7c, 0x3a, 0x51, 0x8, 0x47, 0xc7, 0xe5, 0xe4, 0x6, 0x96, 0xb6, 0xfd} + // Palam is a hash of native Palam contract. + Palam = util.Uint160{0x3, 0x23, 0x73, 0xfa, 0x71, 0x3a, 0x34, 0xab, 0x8d, 0x8c, 0xfa, 0xe2, 0xb0, 0x46, 0xdf, 0x53, 0x88, 0x79, 0x16, 0xae} ) diff --git a/pkg/core/native/nativeids/ids.go b/pkg/core/native/nativeids/ids.go index 569c561..62cabef 100644 --- a/pkg/core/native/nativeids/ids.go +++ b/pkg/core/native/nativeids/ids.go @@ -51,4 +51,6 @@ const ( Tribute int32 = -21 // Opus is an ID of native Opus contract. Opus int32 = -22 + // Palam is an ID of native Palam contract. + Palam int32 = -23 ) diff --git a/pkg/core/native/nativenames/names.go b/pkg/core/native/nativenames/names.go index 83e7bea..f5dbe9b 100644 --- a/pkg/core/native/nativenames/names.go +++ b/pkg/core/native/nativenames/names.go @@ -22,8 +22,9 @@ const ( Scire = "Scire" Salus = "Salus" Sese = "Sese" - Tribute = "Tribute" - Opus = "Opus" + Tribute = "Tribute" + Opus = "Opus" + Palam = "Palam" ) // All contains the list of all native contract names ordered by the contract ID. @@ -50,6 +51,7 @@ var All = []string{ Sese, Tribute, Opus, + Palam, } // IsValid checks if the name is a valid native contract's name. @@ -75,5 +77,6 @@ func IsValid(name string) bool { name == Salus || name == Sese || name == Tribute || - name == Opus + name == Opus || + name == Palam } diff --git a/pkg/core/native/palam.go b/pkg/core/native/palam.go new file mode 100644 index 0000000..a4a7eb9 --- /dev/null +++ b/pkg/core/native/palam.go @@ -0,0 +1,1127 @@ +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 + NEO INEO + 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() +} diff --git a/pkg/core/state/palam.go b/pkg/core/state/palam.go new file mode 100644 index 0000000..3a92bf8 --- /dev/null +++ b/pkg/core/state/palam.go @@ -0,0 +1,713 @@ +package state + +import ( + "errors" + "math/big" + + "github.com/tutus-one/tutus-chain/pkg/util" + "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" +) + +var errPalamInvalidStackItem = errors.New("invalid stack item") + +// FlowStatus represents the status of a transaction flow. +type FlowStatus uint8 + +const ( + FlowStatusActive FlowStatus = 0 + FlowStatusArchived FlowStatus = 1 + FlowStatusDisputed FlowStatus = 2 +) + +// DeclassifyStatus represents the status of a declassification request. +type DeclassifyStatus uint8 + +const ( + DeclassifyPending DeclassifyStatus = 0 + DeclassifyApproved DeclassifyStatus = 1 + DeclassifyDenied DeclassifyStatus = 2 + DeclassifyExpired DeclassifyStatus = 3 +) + +// PalamRole represents roles for transparency access. +type PalamRole uint8 + +const ( + PalamRoleConsumer PalamRole = 0 + PalamRoleMerchant PalamRole = 1 + PalamRoleDistributor PalamRole = 2 + PalamRoleProducer PalamRole = 3 + PalamRoleNGO PalamRole = 4 + PalamRoleAuditor PalamRole = 5 +) + +// AccessType represents types of data access. +type AccessType uint8 + +const ( + AccessTypeView AccessType = 0 + AccessTypeDeclassify AccessType = 1 + AccessTypeAttach AccessType = 2 +) + +// Flow represents a transaction record with encrypted role-based payloads. +type Flow struct { + FlowID util.Uint256 // Unique identifier (hash of contents) + Bucket string // Time bucket (e.g., "2025-01-15T14:00:00Z") + Tag string // Category: COFFEE, DONATION, SUPPLY, etc. + Amount uint64 // Value in smallest unit + Timestamp uint32 // Block height when recorded + Creator util.Uint160 // Who created the flow + + // Encrypted payloads - each role has its own encrypted view + ConsumerData []byte // Encrypted with Consumer key + MerchantData []byte // Encrypted with Merchant key + DistributorData []byte // Encrypted with Distributor key + ProducerData []byte // Encrypted with Producer key + NGOData []byte // Encrypted with NGO key + AuditorData []byte // Encrypted with Auditor key (requires declassify) + + // Participants (script hashes) + Participants []util.Uint160 + + // Chain of custody + PreviousFlowID util.Uint256 // Links flows in a supply chain + + Status FlowStatus +} + +// ToStackItem converts Flow to a stack item. +func (f *Flow) ToStackItem() stackitem.Item { + participants := make([]stackitem.Item, len(f.Participants)) + for i, p := range f.Participants { + participants[i] = stackitem.NewByteArray(p.BytesBE()) + } + + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(f.FlowID.BytesBE()), + stackitem.NewByteArray([]byte(f.Bucket)), + stackitem.NewByteArray([]byte(f.Tag)), + stackitem.NewBigInteger(big.NewInt(int64(f.Amount))), + stackitem.NewBigInteger(big.NewInt(int64(f.Timestamp))), + stackitem.NewByteArray(f.Creator.BytesBE()), + stackitem.NewByteArray(f.ConsumerData), + stackitem.NewByteArray(f.MerchantData), + stackitem.NewByteArray(f.DistributorData), + stackitem.NewByteArray(f.ProducerData), + stackitem.NewByteArray(f.NGOData), + stackitem.NewByteArray(f.AuditorData), + stackitem.NewArray(participants), + stackitem.NewByteArray(f.PreviousFlowID.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(f.Status))), + }) +} + +// FromStackItem populates Flow from a stack item. +func (f *Flow) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 15 { + return errPalamInvalidStackItem + } + + flowIDBytes, err := arr[0].TryBytes() + if err != nil { + return err + } + f.FlowID, err = util.Uint256DecodeBytesBE(flowIDBytes) + if err != nil { + return err + } + + bucketBytes, err := arr[1].TryBytes() + if err != nil { + return err + } + f.Bucket = string(bucketBytes) + + tagBytes, err := arr[2].TryBytes() + if err != nil { + return err + } + f.Tag = string(tagBytes) + + amount, err := arr[3].TryInteger() + if err != nil { + return err + } + f.Amount = amount.Uint64() + + timestamp, err := arr[4].TryInteger() + if err != nil { + return err + } + f.Timestamp = uint32(timestamp.Uint64()) + + creatorBytes, err := arr[5].TryBytes() + if err != nil { + return err + } + f.Creator, err = util.Uint160DecodeBytesBE(creatorBytes) + if err != nil { + return err + } + + f.ConsumerData, err = arr[6].TryBytes() + if err != nil { + return err + } + f.MerchantData, err = arr[7].TryBytes() + if err != nil { + return err + } + f.DistributorData, err = arr[8].TryBytes() + if err != nil { + return err + } + f.ProducerData, err = arr[9].TryBytes() + if err != nil { + return err + } + f.NGOData, err = arr[10].TryBytes() + if err != nil { + return err + } + f.AuditorData, err = arr[11].TryBytes() + if err != nil { + return err + } + + participantsArr, ok := arr[12].Value().([]stackitem.Item) + if !ok { + return errPalamInvalidStackItem + } + f.Participants = make([]util.Uint160, len(participantsArr)) + for i, p := range participantsArr { + pBytes, err := p.TryBytes() + if err != nil { + return err + } + f.Participants[i], err = util.Uint160DecodeBytesBE(pBytes) + if err != nil { + return err + } + } + + prevFlowBytes, err := arr[13].TryBytes() + if err != nil { + return err + } + if len(prevFlowBytes) > 0 { + f.PreviousFlowID, err = util.Uint256DecodeBytesBE(prevFlowBytes) + if err != nil { + return err + } + } + + status, err := arr[14].TryInteger() + if err != nil { + return err + } + f.Status = FlowStatus(status.Uint64()) + + return nil +} + +// DeclassifyRequest represents a request for elevated access to flow data. +type DeclassifyRequest struct { + RequestID uint64 // Unique identifier + FlowID util.Uint256 // Target flow + CaseID string // Legal case reference + Reason string // Justification for access + Requester util.Uint160 // Script hash of requester + RequesterRole PalamRole // Must be Auditor + + // Approval tracking + RequiredApprovals uint32 // e.g., 2 of 3 + Approvals []util.Uint160 // Script hashes of approvers + ApprovalTimes []uint32 // Block heights of approvals + + // Status + Status DeclassifyStatus + CreatedAt uint32 // Block height when created + ExpiresAt uint32 // Request expires if not approved + GrantedAt uint32 // When access was granted +} + +// ToStackItem converts DeclassifyRequest to a stack item. +func (d *DeclassifyRequest) ToStackItem() stackitem.Item { + approvals := make([]stackitem.Item, len(d.Approvals)) + for i, a := range d.Approvals { + approvals[i] = stackitem.NewByteArray(a.BytesBE()) + } + + approvalTimes := make([]stackitem.Item, len(d.ApprovalTimes)) + for i, t := range d.ApprovalTimes { + approvalTimes[i] = stackitem.NewBigInteger(big.NewInt(int64(t))) + } + + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(d.RequestID))), + stackitem.NewByteArray(d.FlowID.BytesBE()), + stackitem.NewByteArray([]byte(d.CaseID)), + stackitem.NewByteArray([]byte(d.Reason)), + stackitem.NewByteArray(d.Requester.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(d.RequesterRole))), + stackitem.NewBigInteger(big.NewInt(int64(d.RequiredApprovals))), + stackitem.NewArray(approvals), + stackitem.NewArray(approvalTimes), + stackitem.NewBigInteger(big.NewInt(int64(d.Status))), + stackitem.NewBigInteger(big.NewInt(int64(d.CreatedAt))), + stackitem.NewBigInteger(big.NewInt(int64(d.ExpiresAt))), + stackitem.NewBigInteger(big.NewInt(int64(d.GrantedAt))), + }) +} + +// FromStackItem populates DeclassifyRequest from a stack item. +func (d *DeclassifyRequest) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 13 { + return errPalamInvalidStackItem + } + + requestID, err := arr[0].TryInteger() + if err != nil { + return err + } + d.RequestID = requestID.Uint64() + + flowIDBytes, err := arr[1].TryBytes() + if err != nil { + return err + } + d.FlowID, err = util.Uint256DecodeBytesBE(flowIDBytes) + if err != nil { + return err + } + + caseIDBytes, err := arr[2].TryBytes() + if err != nil { + return err + } + d.CaseID = string(caseIDBytes) + + reasonBytes, err := arr[3].TryBytes() + if err != nil { + return err + } + d.Reason = string(reasonBytes) + + requesterBytes, err := arr[4].TryBytes() + if err != nil { + return err + } + d.Requester, err = util.Uint160DecodeBytesBE(requesterBytes) + if err != nil { + return err + } + + role, err := arr[5].TryInteger() + if err != nil { + return err + } + d.RequesterRole = PalamRole(role.Uint64()) + + required, err := arr[6].TryInteger() + if err != nil { + return err + } + d.RequiredApprovals = uint32(required.Uint64()) + + approvalsArr, ok := arr[7].Value().([]stackitem.Item) + if !ok { + return errPalamInvalidStackItem + } + d.Approvals = make([]util.Uint160, len(approvalsArr)) + for i, a := range approvalsArr { + aBytes, err := a.TryBytes() + if err != nil { + return err + } + d.Approvals[i], err = util.Uint160DecodeBytesBE(aBytes) + if err != nil { + return err + } + } + + timesArr, ok := arr[8].Value().([]stackitem.Item) + if !ok { + return errPalamInvalidStackItem + } + d.ApprovalTimes = make([]uint32, len(timesArr)) + for i, t := range timesArr { + tInt, err := t.TryInteger() + if err != nil { + return err + } + d.ApprovalTimes[i] = uint32(tInt.Uint64()) + } + + status, err := arr[9].TryInteger() + if err != nil { + return err + } + d.Status = DeclassifyStatus(status.Uint64()) + + createdAt, err := arr[10].TryInteger() + if err != nil { + return err + } + d.CreatedAt = uint32(createdAt.Uint64()) + + expiresAt, err := arr[11].TryInteger() + if err != nil { + return err + } + d.ExpiresAt = uint32(expiresAt.Uint64()) + + grantedAt, err := arr[12].TryInteger() + if err != nil { + return err + } + d.GrantedAt = uint32(grantedAt.Uint64()) + + return nil +} + +// AccessLog represents a record of data access. +type AccessLog struct { + LogID uint64 // Unique identifier + FlowID util.Uint256 // Which flow was accessed + Accessor util.Uint160 // Who accessed + AccessType AccessType // Type of access + Timestamp uint32 // Block height + Details string // Additional context +} + +// ToStackItem converts AccessLog to a stack item. +func (a *AccessLog) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(a.LogID))), + stackitem.NewByteArray(a.FlowID.BytesBE()), + stackitem.NewByteArray(a.Accessor.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(a.AccessType))), + stackitem.NewBigInteger(big.NewInt(int64(a.Timestamp))), + stackitem.NewByteArray([]byte(a.Details)), + }) +} + +// FromStackItem populates AccessLog from a stack item. +func (a *AccessLog) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 6 { + return errPalamInvalidStackItem + } + + logID, err := arr[0].TryInteger() + if err != nil { + return err + } + a.LogID = logID.Uint64() + + flowIDBytes, err := arr[1].TryBytes() + if err != nil { + return err + } + a.FlowID, err = util.Uint256DecodeBytesBE(flowIDBytes) + if err != nil { + return err + } + + accessorBytes, err := arr[2].TryBytes() + if err != nil { + return err + } + a.Accessor, err = util.Uint160DecodeBytesBE(accessorBytes) + if err != nil { + return err + } + + accessType, err := arr[3].TryInteger() + if err != nil { + return err + } + a.AccessType = AccessType(accessType.Uint64()) + + timestamp, err := arr[4].TryInteger() + if err != nil { + return err + } + a.Timestamp = uint32(timestamp.Uint64()) + + detailsBytes, err := arr[5].TryBytes() + if err != nil { + return err + } + a.Details = string(detailsBytes) + + return nil +} + +// FlowAttachment represents additional data attached to a flow. +type FlowAttachment struct { + AttachmentID uint64 // Unique identifier + FlowID util.Uint256 // Parent flow + AttachmentType string // Type of attachment + EncryptedData []byte // Encrypted content + Attacher util.Uint160 // Who attached + AttachedAt uint32 // Block height +} + +// ToStackItem converts FlowAttachment to a stack item. +func (fa *FlowAttachment) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(fa.AttachmentID))), + stackitem.NewByteArray(fa.FlowID.BytesBE()), + stackitem.NewByteArray([]byte(fa.AttachmentType)), + stackitem.NewByteArray(fa.EncryptedData), + stackitem.NewByteArray(fa.Attacher.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(fa.AttachedAt))), + }) +} + +// FromStackItem populates FlowAttachment from a stack item. +func (fa *FlowAttachment) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 6 { + return errPalamInvalidStackItem + } + + attachmentID, err := arr[0].TryInteger() + if err != nil { + return err + } + fa.AttachmentID = attachmentID.Uint64() + + flowIDBytes, err := arr[1].TryBytes() + if err != nil { + return err + } + fa.FlowID, err = util.Uint256DecodeBytesBE(flowIDBytes) + if err != nil { + return err + } + + typeBytes, err := arr[2].TryBytes() + if err != nil { + return err + } + fa.AttachmentType = string(typeBytes) + + fa.EncryptedData, err = arr[3].TryBytes() + if err != nil { + return err + } + + attacherBytes, err := arr[4].TryBytes() + if err != nil { + return err + } + fa.Attacher, err = util.Uint160DecodeBytesBE(attacherBytes) + if err != nil { + return err + } + + attachedAt, err := arr[5].TryInteger() + if err != nil { + return err + } + fa.AttachedAt = uint32(attachedAt.Uint64()) + + return nil +} + +// RolePermissions defines what each role can do. +type RolePermissions struct { + CanViewAggregate bool // See totals without identities + CanViewIdentities bool // See participant identities + CanViewDocuments bool // See attached documents + CanAttachData bool // Add metadata to flows + CanRequestDeclassify bool // Initiate declassification + CanApproveDeclassify bool // Vote on declassification requests +} + +// ToStackItem converts RolePermissions to a stack item. +func (rp *RolePermissions) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBool(rp.CanViewAggregate), + stackitem.NewBool(rp.CanViewIdentities), + stackitem.NewBool(rp.CanViewDocuments), + stackitem.NewBool(rp.CanAttachData), + stackitem.NewBool(rp.CanRequestDeclassify), + stackitem.NewBool(rp.CanApproveDeclassify), + }) +} + +// FromStackItem populates RolePermissions from a stack item. +func (rp *RolePermissions) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 6 { + return errPalamInvalidStackItem + } + + viewAgg, err := arr[0].TryBool() + if err != nil { + return err + } + rp.CanViewAggregate = viewAgg + + viewId, err := arr[1].TryBool() + if err != nil { + return err + } + rp.CanViewIdentities = viewId + + viewDoc, err := arr[2].TryBool() + if err != nil { + return err + } + rp.CanViewDocuments = viewDoc + + attach, err := arr[3].TryBool() + if err != nil { + return err + } + rp.CanAttachData = attach + + reqDecl, err := arr[4].TryBool() + if err != nil { + return err + } + rp.CanRequestDeclassify = reqDecl + + appDecl, err := arr[5].TryBool() + if err != nil { + return err + } + rp.CanApproveDeclassify = appDecl + + return nil +} + +// PalamConfig represents configurable parameters for TransparencyLedger. +type PalamConfig struct { + MinApprovals uint32 // Minimum approvals for declassification (default: 2) + MaxApprovals uint32 // Maximum approvers allowed (default: 5) + DefaultExpiration uint32 // Default expiration in blocks (default: 7 days worth) + LogAllAccess bool // Whether to log all access (default: true) + LogRetentionDays uint32 // How long to keep logs (default: 365) + MaxFlowsPerBlock uint32 // Rate limit for flows (default: 1000) + MaxRequestsPerDay uint32 // Rate limit for declassify requests (default: 10) +} + +// ToStackItem converts PalamConfig to a stack item. +func (lc *PalamConfig) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(lc.MinApprovals))), + stackitem.NewBigInteger(big.NewInt(int64(lc.MaxApprovals))), + stackitem.NewBigInteger(big.NewInt(int64(lc.DefaultExpiration))), + stackitem.NewBool(lc.LogAllAccess), + stackitem.NewBigInteger(big.NewInt(int64(lc.LogRetentionDays))), + stackitem.NewBigInteger(big.NewInt(int64(lc.MaxFlowsPerBlock))), + stackitem.NewBigInteger(big.NewInt(int64(lc.MaxRequestsPerDay))), + }) +} + +// FromStackItem populates PalamConfig from a stack item. +func (lc *PalamConfig) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 7 { + return errPalamInvalidStackItem + } + + minApprovals, err := arr[0].TryInteger() + if err != nil { + return err + } + lc.MinApprovals = uint32(minApprovals.Uint64()) + + maxApprovals, err := arr[1].TryInteger() + if err != nil { + return err + } + lc.MaxApprovals = uint32(maxApprovals.Uint64()) + + defaultExp, err := arr[2].TryInteger() + if err != nil { + return err + } + lc.DefaultExpiration = uint32(defaultExp.Uint64()) + + logAll, err := arr[3].TryBool() + if err != nil { + return err + } + lc.LogAllAccess = logAll + + retention, err := arr[4].TryInteger() + if err != nil { + return err + } + lc.LogRetentionDays = uint32(retention.Uint64()) + + maxFlows, err := arr[5].TryInteger() + if err != nil { + return err + } + lc.MaxFlowsPerBlock = uint32(maxFlows.Uint64()) + + maxReqs, err := arr[6].TryInteger() + if err != nil { + return err + } + lc.MaxRequestsPerDay = uint32(maxReqs.Uint64()) + + return nil +} + +// DefaultPalamConfig returns the default configuration. +func DefaultPalamConfig() *PalamConfig { + return &PalamConfig{ + MinApprovals: 2, + MaxApprovals: 5, + DefaultExpiration: 604800, // ~7 days in blocks (1 block/sec) + LogAllAccess: true, + LogRetentionDays: 365, + MaxFlowsPerBlock: 1000, + MaxRequestsPerDay: 10, + } +} + +// DefaultRolePermissions returns permissions for each role. +func DefaultRolePermissions(role PalamRole) *RolePermissions { + switch role { + case PalamRoleConsumer: + return &RolePermissions{ + CanViewAggregate: true, + CanViewIdentities: false, + CanViewDocuments: false, + CanAttachData: false, + CanRequestDeclassify: false, + CanApproveDeclassify: false, + } + case PalamRoleMerchant, PalamRoleDistributor, PalamRoleProducer, PalamRoleNGO: + return &RolePermissions{ + CanViewAggregate: true, + CanViewIdentities: true, + CanViewDocuments: true, + CanAttachData: true, + CanRequestDeclassify: false, + CanApproveDeclassify: false, + } + case PalamRoleAuditor: + return &RolePermissions{ + CanViewAggregate: true, + CanViewIdentities: false, + CanViewDocuments: false, + CanAttachData: false, + CanRequestDeclassify: true, + CanApproveDeclassify: false, + } + default: + return &RolePermissions{} + } +}